1use nalgebra_sparse::CsrMatrix;
2use num_traits::{Float, NumCast, FromPrimitive};
3use std::fmt::Debug;
4use single_utilities::traits::FloatOpsTS;
5
6pub fn calculate_log2_fold_change<T>(
8 matrix: &CsrMatrix<T>,
9 row: usize,
10 group1_indices: &[usize],
11 group2_indices: &[usize],
12 pseudo_count: f64,
13) -> anyhow::Result<f64>
14where
15 T: FloatOpsTS,
16{
17 if group1_indices.is_empty() || group2_indices.is_empty() {
18 return Err(anyhow::anyhow!("Group indices cannot be empty"));
19 }
20
21 let mut sum1 = 0.0;
23 for &col in group1_indices {
24 if let Some(entry) = matrix.get_entry(row, col) {
25 let value = entry.into_value();
26 sum1 += value.to_f64().unwrap();
27 }
28 }
30 let mean1 = sum1 / group1_indices.len() as f64 + pseudo_count;
31
32 let mut sum2 = 0.0;
34 for &col in group2_indices {
35 if let Some(entry) = matrix.get_entry(row, col) {
36 let value = entry.into_value();
37 sum2 += value.to_f64().unwrap();
38 }
39 }
41 let mean2 = sum2 / group2_indices.len() as f64 + pseudo_count;
42
43 let log2_fc = (mean2 / mean1).log2();
45
46 Ok(log2_fc)
47}
48
49pub fn calculate_cohens_d<T>(
51 matrix: &CsrMatrix<T>,
52 row: usize,
53 group1_indices: &[usize],
54 group2_indices: &[usize],
55) -> anyhow::Result<f64>
56where
57 T: FloatOpsTS,
58{
59 if group1_indices.len() < 2 || group2_indices.len() < 2 {
60 return Err(anyhow::anyhow!("Each group must have at least 2 samples for Cohen's d"));
61 }
62
63 let mut group1_values = Vec::with_capacity(group1_indices.len());
65 for &col in group1_indices {
66 if let Some(entry) = matrix.get_entry(row, col) {
67 let value = entry.into_value();
68 group1_values.push(value.to_f64().unwrap());
69 } else {
70 group1_values.push(0.0); }
72 }
73
74 let mut group2_values = Vec::with_capacity(group2_indices.len());
76 for &col in group2_indices {
77 if let Some(entry) = matrix.get_entry(row, col) {
78 let value = entry.into_value();
79 group2_values.push(value.to_f64().unwrap());
80 } else {
81 group2_values.push(0.0); }
83 }
84
85 let mean1 = group1_values.iter().sum::<f64>() / group1_values.len() as f64;
87 let mean2 = group2_values.iter().sum::<f64>() / group2_values.len() as f64;
88
89 let var1 = group1_values.iter()
91 .map(|&x| (x - mean1).powi(2))
92 .sum::<f64>() / (group1_values.len() - 1) as f64;
93
94 let var2 = group2_values.iter()
95 .map(|&x| (x - mean2).powi(2))
96 .sum::<f64>() / (group2_values.len() - 1) as f64;
97
98 let n1 = group1_values.len() as f64;
100 let n2 = group2_values.len() as f64;
101 let pooled_sd = ((n1 - 1.0) * var1 + (n2 - 1.0) * var2).sqrt() / ((n1 + n2 - 2.0).sqrt());
102
103 let d = (mean2 - mean1) / pooled_sd;
105
106 Ok(d)
107}
108
109pub fn calculate_hedges_g<T>(
111 matrix: &CsrMatrix<T>,
112 row: usize,
113 group1_indices: &[usize],
114 group2_indices: &[usize],
115) -> anyhow::Result<f64>
116where
117 T: FloatOpsTS,
118{
119 let d = calculate_cohens_d(matrix, row, group1_indices, group2_indices)?;
121
122 let n1 = group1_indices.len() as f64;
124 let n2 = group2_indices.len() as f64;
125 let n = n1 + n2;
126
127 let j = 1.0 - 3.0 / (4.0 * (n - 2.0) - 1.0);
129
130 let g = j * d;
132
133 Ok(g)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use approx::assert_abs_diff_eq;
140 use nalgebra_sparse::{CooMatrix, CsrMatrix};
141
142 fn create_test_matrix() -> CsrMatrix<f64> {
143 let rows = vec![
151 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4 ];
157 let cols = vec![
158 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 3, 4, 5 ];
164 let vals = vec![
165 2.0, 2.2, 1.8, 8.0, 7.5, 8.5, 5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 3.0, 3.3, 2.7, 5.0, 4.7, 5.3, 0.1, 0.2, 0.1, 20.0, 19.0, 21.0, 10.0, 8.0, 12.0 ];
171
172 let coo = CooMatrix::try_from_triplets(
173 5, 6, rows,
176 cols,
177 vals,
178 )
179 .unwrap();
180
181 CsrMatrix::from(&coo)
182 }
183
184 #[test]
185 fn test_log2_fold_change() {
186 let matrix = create_test_matrix();
187 let group1 = vec![0, 1, 2]; let group2 = vec![3, 4, 5]; let pseudo_count = 0.01; let fc0 = calculate_log2_fold_change(&matrix, 0, &group1, &group2, pseudo_count).unwrap();
193 assert_abs_diff_eq!(fc0, 2.0, epsilon = 0.1); let fc1 = calculate_log2_fold_change(&matrix, 1, &group1, &group2, pseudo_count).unwrap();
197 assert_abs_diff_eq!(fc1, 0.0, epsilon = 0.01); let fc2 = calculate_log2_fold_change(&matrix, 2, &group1, &group2, pseudo_count).unwrap();
201 assert_abs_diff_eq!(fc2, 0.737, epsilon = 0.01); let fc3 = calculate_log2_fold_change(&matrix, 3, &group1, &group2, pseudo_count).unwrap();
205 assert_abs_diff_eq!(fc3, 7.13, epsilon = 0.1); let fc4 = calculate_log2_fold_change(&matrix, 4, &group1, &group2, pseudo_count).unwrap();
209 assert!(fc4 > 9.0); }
212
213 #[test]
214 fn test_empty_groups() {
215 let matrix = create_test_matrix();
216
217 let result = calculate_log2_fold_change(&matrix, 0, &[], &[3, 4, 5], 0.01);
219 assert!(result.is_err());
220
221 let result = calculate_log2_fold_change(&matrix, 0, &[0, 1, 2], &[], 0.01);
222 assert!(result.is_err());
223
224 let result = calculate_cohens_d(&matrix, 0, &[0], &[3, 4, 5]);
226 assert!(result.is_err());
227
228 let result = calculate_cohens_d(&matrix, 0, &[0, 1, 2], &[3]);
229 assert!(result.is_err());
230 }
231
232 #[test]
233 fn test_cohens_d() {
234 let matrix = create_test_matrix();
235 let group1 = vec![0, 1, 2]; let group2 = vec![3, 4, 5]; let d0 = calculate_cohens_d(&matrix, 0, &group1, &group2).unwrap();
240 assert_abs_diff_eq!(d0, 15.76, epsilon = 0.1); let d1 = calculate_cohens_d(&matrix, 1, &group1, &group2).unwrap();
244 assert_abs_diff_eq!(d1, 0.0, epsilon = 0.01); let d2 = calculate_cohens_d(&matrix, 2, &group1, &group2).unwrap();
248 assert_abs_diff_eq!(d2, 6.67, epsilon = 0.1); let d3 = calculate_cohens_d(&matrix, 3, &group1, &group2).unwrap();
252 assert!(d3 > 20.0); }
256
257 #[test]
258 fn test_hedges_g() {
259 let matrix = create_test_matrix();
260 let group1 = vec![0, 1, 2]; let group2 = vec![3, 4, 5]; let d0 = calculate_cohens_d(&matrix, 0, &group1, &group2).unwrap();
265 let g0 = calculate_hedges_g(&matrix, 0, &group1, &group2).unwrap();
266
267 assert!(g0 < d0);
269 assert_abs_diff_eq!(g0 / d0, 0.75, epsilon = 0.25);
271
272 let g1 = calculate_hedges_g(&matrix, 1, &group1, &group2).unwrap();
274 assert_abs_diff_eq!(g1, 0.0, epsilon = 0.01);
275
276 let large_group1 = vec![0, 1, 2, 6, 7, 8, 9, 10];
279 let large_group2 = vec![3, 4, 5, 11, 12, 13, 14, 15];
280
281 if matrix.ncols() >= 16 {
284 let d_large = calculate_cohens_d(&matrix, 0, &large_group1, &large_group2).unwrap_or(d0);
285 let g_large = calculate_hedges_g(&matrix, 0, &large_group1, &large_group2).unwrap_or(g0);
286
287 assert!(g_large / d_large > g0 / d0);
289 }
290 }
291
292 #[test]
293 fn test_zero_variance_cases() {
294 let rows = vec![0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1];
296 let cols = vec![0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5];
297 let vals = vec![
298 5.0, 5.0, 5.0, 10.0, 10.0, 10.0, 1.0, 2.0, 3.0, 5.0, 5.0, 5.0 ];
301
302 let coo = CooMatrix::try_from_triplets(2, 6, rows, cols, vals).unwrap();
303 let matrix = CsrMatrix::from(&coo);
304
305 let group1 = vec![0, 1, 2];
306 let group2 = vec![3, 4, 5];
307
308 let fc0 = calculate_log2_fold_change(&matrix, 0, &group1, &group2, 0.01).unwrap();
310 assert_abs_diff_eq!(fc0, 1.0, epsilon = 0.01); let d0 = calculate_cohens_d(&matrix, 0, &group1, &group2);
316 match d0 {
317 Ok(value) => assert!(value.abs() > 100.0 || value.is_infinite()), Err(_) => {} }
320
321 let d1 = calculate_cohens_d(&matrix, 1, &group1, &group2);
323 if let Ok(value) = d1 {
324 assert!(value.abs() > 1.0); }
326 }
327
328 #[test]
329 fn test_negative_values() {
330 let rows = vec![0, 0, 0, 0, 0, 0];
332 let cols = vec![0, 1, 2, 3, 4, 5];
333 let vals = vec![-2.0, -2.2, -1.8, -8.0, -7.5, -8.5]; let coo = CooMatrix::try_from_triplets(1, 6, rows, cols, vals).unwrap();
336 let matrix = CsrMatrix::from(&coo);
337
338 let group1 = vec![0, 1, 2];
339 let group2 = vec![3, 4, 5];
340
341 let fc = calculate_log2_fold_change(&matrix, 0, &group1, &group2, 0.0);
345 assert!(fc.is_ok()); let d = calculate_cohens_d(&matrix, 0, &group1, &group2);
350 assert!(d.is_ok());
351 if let Ok(value) = d {
352 assert!(value < 0.0);
354 assert_abs_diff_eq!(value.abs(), 15.76, epsilon = 0.1);
355 }
356 }
357}