1use crate::matrix::common_io::{file_ext, write_lines};
2use crate::matrix::parquet::{
3 parquet_add_numeric_column, parquet_add_string_column, ParquetWriter,
4};
5use crate::matrix::traits::RunningStatOps;
6use nalgebra_sparse::CscMatrix;
7use num_traits::{Float, ToPrimitive, Zero};
8use parquet::basic::Type as ParquetType;
9use std::fmt::Display;
10use std::iter::Sum;
11use std::ops::AddAssign;
12
13const STAT_COLUMN_NAMES: [&str; 4] = ["nnz", "tot", "mu", "sig"];
14
15fn safe_denom<T: Float>(n: usize) -> T {
18 let n = T::from(n).unwrap_or(T::one());
19 if n > T::zero() {
20 n
21 } else {
22 T::from(1e-8).unwrap_or(T::one())
23 }
24}
25
26#[derive(Clone)]
34pub struct SparseRunningStatistics<T>
35where
36 T: Float,
37{
38 nrows: usize,
39 ncols_processed: usize,
40 npos: Vec<T>,
41 s1: Vec<T>,
42 s2: Vec<T>,
43}
44
45impl<T> SparseRunningStatistics<T>
46where
47 T: Float + AddAssign + Sum + Zero,
48{
49 pub fn new(nrows: usize) -> Self {
55 SparseRunningStatistics {
56 nrows,
57 ncols_processed: 0,
58 npos: vec![T::zero(); nrows],
59 s1: vec![T::zero(); nrows],
60 s2: vec![T::zero(); nrows],
61 }
62 }
63
64 pub fn add_sparse_column(&mut self, row_indices: &[usize], values: &[T]) {
67 debug_assert_eq!(row_indices.len(), values.len());
68
69 for (&row, &val) in row_indices.iter().zip(values.iter()) {
70 if val.is_finite() {
71 if val > T::zero() {
72 self.npos[row] += T::one();
73 }
74 self.s1[row] += val;
75 self.s2[row] += val * val;
76 }
77 }
78 self.ncols_processed += 1;
79 }
80
81 pub fn nrows(&self) -> usize {
82 self.nrows
83 }
84
85 pub fn ncols_processed(&self) -> usize {
86 self.ncols_processed
87 }
88
89 fn denom(&self) -> T {
90 safe_denom::<T>(self.ncols_processed)
91 }
92
93 pub fn to_vecs(&self) -> (Vec<T>, Vec<T>, Vec<T>, Vec<T>) {
95 (self.npos.clone(), self.s1.clone(), self.mean(), self.std())
96 }
97
98 pub fn add_csc(&mut self, csc: &CscMatrix<T>) {
104 for col in csc.col_iter() {
105 let rows = col.row_indices();
106 let vals = col.values();
107 self.add_sparse_column(rows, vals);
108 }
109 }
110
111 pub fn add_dense_column(&mut self, values: &[T]) {
122 debug_assert_eq!(values.len(), self.nrows);
123 let zero = T::zero();
124 let one = T::one();
125 for ((v_in, npos), (s1, s2)) in values
126 .iter()
127 .zip(self.npos.iter_mut())
128 .zip(self.s1.iter_mut().zip(self.s2.iter_mut()))
129 {
130 let v = *v_in;
131 let v = if v.is_finite() { v } else { zero };
134 let pos = if v > zero { one } else { zero };
136 *npos += pos;
137 *s1 += v;
138 *s2 += v * v;
139 }
140 self.ncols_processed += 1;
141 }
142
143 pub fn add_dense_column_scaled(&mut self, values: &[T], scale: T) {
148 debug_assert_eq!(values.len(), self.nrows);
149 let zero = T::zero();
150 let one = T::one();
151 for ((v_in, npos), (s1, s2)) in values
152 .iter()
153 .zip(self.npos.iter_mut())
154 .zip(self.s1.iter_mut().zip(self.s2.iter_mut()))
155 {
156 let v = *v_in * scale;
157 let v = if v.is_finite() { v } else { zero };
158 let pos = if v > zero { one } else { zero };
159 *npos += pos;
160 *s1 += v;
161 *s2 += v * v;
162 }
163 self.ncols_processed += 1;
164 }
165
166 pub fn add_dense_columns(&mut self, dense: &nalgebra::DMatrix<T>)
172 where
173 T: nalgebra::Scalar,
174 {
175 debug_assert_eq!(dense.nrows(), self.nrows);
176 for j in 0..dense.ncols() {
177 let col = dense.column(j);
178 self.add_dense_column(col.as_slice());
179 }
180 }
181
182 pub fn merge(&mut self, other: &Self) {
186 debug_assert_eq!(self.nrows, other.nrows);
187 for (a, b) in self.npos.iter_mut().zip(other.npos.iter()) {
188 *a += *b;
189 }
190 for (a, b) in self.s1.iter_mut().zip(other.s1.iter()) {
191 *a += *b;
192 }
193 for (a, b) in self.s2.iter_mut().zip(other.s2.iter()) {
194 *a += *b;
195 }
196 self.ncols_processed += other.ncols_processed;
197 }
198}
199
200impl<T> SparseRunningStatistics<T>
201where
202 T: Float + AddAssign + Sum + Zero + Display + ToPrimitive,
203{
204 pub fn save(&self, filename: &str, names: &[Box<str>], sep: &str) -> anyhow::Result<()> {
207 let (nnz, tot, mu, sig) = self.to_f32_vecs();
208 write_stat_file(
209 filename,
210 names,
211 sep,
212 StatColumns {
213 nnz: &nnz,
214 tot: &tot,
215 mu: &mu,
216 sig: &sig,
217 },
218 )
219 }
220
221 pub fn to_f32_vecs(&self) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
223 let to_f32_slice =
224 |v: &[T]| -> Vec<f32> { v.iter().map(|x| x.to_f32().unwrap_or(0.0)).collect() };
225 let nnz = to_f32_slice(&self.npos);
226 let tot = to_f32_slice(&self.s1);
227 let mu = to_f32_slice(&self.mean());
228 let sig = to_f32_slice(&self.std());
229 (nnz, tot, mu, sig)
230 }
231
232 pub fn to_string_vec(&self, names: &[Box<str>], sep: &str) -> anyhow::Result<Vec<Box<str>>> {
234 if names.len() != self.nrows {
235 anyhow::bail!(
236 "The number of names ({}) does not match nrows ({})",
237 names.len(),
238 self.nrows
239 );
240 }
241
242 let nnz = self.count_positives();
243 let tot = self.sum();
244 let mu = self.mean();
245 let sig = self.std();
246
247 let out: Vec<Box<str>> = (0..self.nrows)
248 .map(|i| {
249 format!(
250 "{}{}{}{}{}{}{}{}{}",
251 names[i],
252 sep,
253 format_value(nnz[i]),
254 sep,
255 format_value(tot[i]),
256 sep,
257 format_value(mu[i]),
258 sep,
259 format_value(sig[i])
260 )
261 .into_boxed_str()
262 })
263 .collect();
264 Ok(out)
265 }
266}
267
268#[derive(Clone)]
279pub struct SparseColumnRunningStatistics<T>
280where
281 T: Float,
282{
283 nrows: usize,
284 npos: Vec<T>,
285 s1: Vec<T>,
286 s2: Vec<T>,
287}
288
289impl<T> SparseColumnRunningStatistics<T>
290where
291 T: Float + AddAssign + Sum + Zero,
292{
293 pub fn new(ncols: usize, nrows: usize) -> Self {
301 Self {
302 nrows,
303 npos: vec![T::zero(); ncols],
304 s1: vec![T::zero(); ncols],
305 s2: vec![T::zero(); ncols],
306 }
307 }
308
309 pub fn add_csc(&mut self, csc: &CscMatrix<T>, col_offset: usize) {
312 self.add_csc_inner(csc, col_offset, None);
313 }
314
315 pub fn add_csc_masked(&mut self, csc: &CscMatrix<T>, col_offset: usize, row_mask: &[bool]) {
319 debug_assert!(row_mask.len() >= csc.nrows());
320 self.add_csc_inner(csc, col_offset, Some(row_mask));
321 }
322
323 fn add_csc_inner(&mut self, csc: &CscMatrix<T>, col_offset: usize, row_mask: Option<&[bool]>) {
324 for (local_col, col) in csc.col_iter().enumerate() {
325 let c = col_offset + local_col;
326 let rows = col.row_indices();
327 let vals = col.values();
328 for (&row, &val) in rows.iter().zip(vals.iter()) {
329 if let Some(mask) = row_mask {
330 if !mask[row] {
331 continue;
332 }
333 }
334 if !val.is_finite() {
335 continue;
336 }
337 if val > T::zero() {
338 self.npos[c] += T::one();
339 }
340 self.s1[c] += val;
341 self.s2[c] += val * val;
342 }
343 }
344 }
345
346 pub fn ncols(&self) -> usize {
347 self.npos.len()
348 }
349
350 pub fn nrows(&self) -> usize {
351 self.nrows
352 }
353
354 fn denom(&self) -> T {
355 safe_denom::<T>(self.nrows)
356 }
357}
358
359impl<T> SparseColumnRunningStatistics<T>
360where
361 T: Float + AddAssign + Sum + Zero + Display + ToPrimitive,
362{
363 pub fn save(&self, filename: &str, names: &[Box<str>], sep: &str) -> anyhow::Result<()> {
366 let (nnz, tot, mu, sig) = self.to_f32_vecs();
367 write_stat_file(
368 filename,
369 names,
370 sep,
371 StatColumns {
372 nnz: &nnz,
373 tot: &tot,
374 mu: &mu,
375 sig: &sig,
376 },
377 )
378 }
379
380 pub fn to_f32_vecs(&self) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
382 let to_f32_slice =
383 |v: &[T]| -> Vec<f32> { v.iter().map(|x| x.to_f32().unwrap_or(0.0)).collect() };
384 let nnz = to_f32_slice(&self.npos);
385 let tot = to_f32_slice(&self.s1);
386 let mu = to_f32_slice(&self.mean());
387 let sig = to_f32_slice(&self.std());
388 (nnz, tot, mu, sig)
389 }
390}
391
392impl<T> RunningStatOps<T> for SparseColumnRunningStatistics<T>
393where
394 T: Float + AddAssign + Sum + Zero,
395{
396 type Output = Vec<T>;
397
398 fn clear(&mut self) {
399 self.npos.fill(T::zero());
400 self.s1.fill(T::zero());
401 self.s2.fill(T::zero());
402 }
403
404 fn count_positives(&self) -> Vec<T> {
405 self.npos.clone()
406 }
407
408 fn sum(&self) -> Vec<T> {
409 self.s1.clone()
410 }
411
412 fn mean(&self) -> Vec<T> {
413 let n = self.denom();
414 self.s1.iter().map(|&s| s / n).collect()
415 }
416
417 fn variance(&self) -> Vec<T> {
418 let n = self.denom();
419 self.s1
420 .iter()
421 .zip(self.s2.iter())
422 .map(|(&s1, &s2)| {
423 let mu = s1 / n;
424 s2 / n - mu * mu
425 })
426 .collect()
427 }
428
429 fn std(&self) -> Vec<T> {
430 self.variance().into_iter().map(|v| v.sqrt()).collect()
431 }
432}
433
434struct StatColumns<'a> {
437 nnz: &'a [f32],
438 tot: &'a [f32],
439 mu: &'a [f32],
440 sig: &'a [f32],
441}
442
443impl StatColumns<'_> {
444 fn len(&self) -> usize {
445 self.nnz.len()
446 }
447}
448
449fn write_stat_file(
452 filename: &str,
453 names: &[Box<str>],
454 sep: &str,
455 stats: StatColumns<'_>,
456) -> anyhow::Result<()> {
457 let n = stats.len();
458 if names.len() != n {
459 anyhow::bail!(
460 "The number of names ({}) does not match stat length ({})",
461 names.len(),
462 n
463 );
464 }
465
466 match file_ext(filename).unwrap_or(Box::from("")).as_ref() {
467 "parquet" => {
468 let column_names: Vec<Box<str>> =
469 STAT_COLUMN_NAMES.iter().map(|s| (*s).into()).collect();
470 let column_types = vec![ParquetType::FLOAT; STAT_COLUMN_NAMES.len()];
471
472 let parquet_writer = ParquetWriter::new(
473 filename,
474 (n, 4),
475 (Some(names), Some(&column_names)),
476 Some(&column_types),
477 None,
478 )?;
479
480 let mut writer = parquet_writer.get_writer()?;
481 let mut row_group_writer = writer.next_row_group()?;
482
483 parquet_add_string_column(&mut row_group_writer, names)?;
484 parquet_add_numeric_column(&mut row_group_writer, stats.nnz)?;
485 parquet_add_numeric_column(&mut row_group_writer, stats.tot)?;
486 parquet_add_numeric_column(&mut row_group_writer, stats.mu)?;
487 parquet_add_numeric_column(&mut row_group_writer, stats.sig)?;
488
489 row_group_writer.close()?;
490 writer.close()?;
491 }
492 _ => {
493 let mut out: Vec<Box<str>> = (0..n)
494 .map(|i| {
495 format!(
496 "{}{}{}{}{}{}{}{}{}",
497 names[i],
498 sep,
499 format_value(stats.nnz[i]),
500 sep,
501 format_value(stats.tot[i]),
502 sep,
503 format_value(stats.mu[i]),
504 sep,
505 format_value(stats.sig[i])
506 )
507 .into_boxed_str()
508 })
509 .collect();
510 let header = format!("#name{}nnz{}tot{}mu{}sig", sep, sep, sep, sep);
511 out.insert(0, header.into_boxed_str());
512 write_lines(&out, filename)?;
513 }
514 }
515 Ok(())
516}
517
518pub fn save_grouped_stats_parquet(
520 filename: &str,
521 names: &[Box<str>],
522 group_names: &[Box<str>],
523 group_stats: &[SparseRunningStatistics<f32>],
524) -> anyhow::Result<()> {
525 save_grouped_stats_parquet_cols(filename, &[("name", names)], group_names, group_stats)
526}
527
528pub fn save_grouped_stats_parquet_cols(
535 filename: &str,
536 key_cols: &[(&str, &[Box<str>])],
537 group_names: &[Box<str>],
538 group_stats: &[SparseRunningStatistics<f32>],
539) -> anyhow::Result<()> {
540 use crate::matrix::parquet::{write_named_table, Column};
541
542 if group_names.len() != group_stats.len() {
543 anyhow::bail!(
544 "Number of group names ({}) does not match number of group stats ({})",
545 group_names.len(),
546 group_stats.len()
547 );
548 }
549 anyhow::ensure!(
550 !key_cols.is_empty(),
551 "save_grouped_stats_parquet_cols: need at least one key column"
552 );
553 let n_features = group_stats.first().map_or(0, |s| s.nrows());
554 for &(name, vals) in key_cols {
555 anyhow::ensure!(
556 vals.len() == n_features,
557 "key column '{name}' has {} entries but there are {n_features} feature rows",
558 vals.len(),
559 );
560 }
561
562 let total_rows = n_features * group_names.len();
567 let mut keys: Vec<Vec<Box<str>>> = (0..key_cols.len())
568 .map(|_| Vec::with_capacity(total_rows))
569 .collect();
570 let mut all_groups: Vec<Box<str>> = Vec::with_capacity(total_rows);
571 let mut all_nnz: Vec<f32> = Vec::with_capacity(total_rows);
572 let mut all_tot: Vec<f32> = Vec::with_capacity(total_rows);
573 let mut all_mu: Vec<f32> = Vec::with_capacity(total_rows);
574 let mut all_sig: Vec<f32> = Vec::with_capacity(total_rows);
575 for (group_name, stat) in group_names.iter().zip(group_stats.iter()) {
576 let (nnz, tot, mu, sig) = stat.to_f32_vecs();
577 for (j, &(_, vals)) in key_cols.iter().enumerate() {
579 keys[j].extend(vals.iter().cloned());
580 }
581 all_groups.resize(all_groups.len() + n_features, group_name.clone());
582 all_nnz.extend_from_slice(&nnz);
583 all_tot.extend_from_slice(&tot);
584 all_mu.extend_from_slice(&mu);
585 all_sig.extend_from_slice(&sig);
586 }
587
588 let mut columns: Vec<(Box<str>, Column)> = Vec::with_capacity(key_cols.len() + 4);
592 for (&(name, _), col) in key_cols.iter().zip(keys.iter()).skip(1) {
593 columns.push((name.into(), Column::Str(col.as_slice())));
594 }
595 columns.push(("group".into(), Column::Str(all_groups.as_slice())));
596 columns.push(("nnz".into(), Column::F32(all_nnz.as_slice())));
597 columns.push(("tot".into(), Column::F32(all_tot.as_slice())));
598 columns.push(("mu".into(), Column::F32(all_mu.as_slice())));
599 columns.push(("sig".into(), Column::F32(all_sig.as_slice())));
600
601 write_named_table(filename, key_cols[0].0, &keys[0], &columns)
602}
603
604fn format_value<T: Float + Display>(v: T) -> String {
605 let v_f64 = v.to_f64().unwrap_or(0.0);
606 if v_f64.abs() > 1e-4 {
607 format!("{:.4}", v_f64)
608 .trim_end_matches('0')
609 .trim_end_matches('.')
610 .to_string()
611 } else if v_f64.abs() > 1e-20 {
612 format!("{:.4e}", v_f64)
613 } else {
614 "0".to_string()
615 }
616}
617
618impl<T> RunningStatOps<T> for SparseRunningStatistics<T>
619where
620 T: Float + AddAssign + Sum + Zero,
621{
622 type Output = Vec<T>;
623
624 fn clear(&mut self) {
625 self.ncols_processed = 0;
626 self.npos.fill(T::zero());
627 self.s1.fill(T::zero());
628 self.s2.fill(T::zero());
629 }
630
631 fn count_positives(&self) -> Vec<T> {
633 self.npos.clone()
634 }
635
636 fn sum(&self) -> Vec<T> {
638 self.s1.clone()
639 }
640
641 fn mean(&self) -> Vec<T> {
644 let n = self.denom();
645 self.s1.iter().map(|&s| s / n).collect()
646 }
647
648 fn variance(&self) -> Vec<T> {
650 let n = self.denom();
651 self.s1
652 .iter()
653 .zip(self.s2.iter())
654 .map(|(&s1, &s2)| {
655 let mu = s1 / n;
656 s2 / n - mu * mu
657 })
658 .collect()
659 }
660
661 fn std(&self) -> Vec<T> {
663 self.variance().into_iter().map(|v| v.sqrt()).collect()
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670
671 #[test]
672 fn test_sparse_running_stat_basic() {
673 let mut stat = SparseRunningStatistics::<f32>::new(4);
674
675 stat.add_sparse_column(&[0, 2], &[1.0, 2.0]);
677
678 stat.add_sparse_column(&[1, 3], &[3.0, 4.0]);
680
681 assert_eq!(stat.ncols_processed(), 2);
682
683 assert_eq!(stat.count_positives(), vec![1.0, 1.0, 1.0, 1.0]);
685
686 assert_eq!(stat.sum(), vec![1.0, 3.0, 2.0, 4.0]);
688
689 let mean = stat.mean();
691 assert!((mean[0] - 0.5).abs() < 1e-6);
692 assert!((mean[1] - 1.5).abs() < 1e-6);
693 assert!((mean[2] - 1.0).abs() < 1e-6);
694 assert!((mean[3] - 2.0).abs() < 1e-6);
695 }
696
697 #[test]
698 fn test_sparse_running_stat_csc() {
699 use nalgebra_sparse::CooMatrix;
700
701 let mut stat = SparseRunningStatistics::<f32>::new(3);
702
703 let mut coo: CooMatrix<f32> = CooMatrix::new(3, 2);
708 coo.push(0, 0, 1.0);
709 coo.push(1, 1, 2.0);
710 coo.push(2, 0, 3.0);
711 let csc = CscMatrix::from(&coo);
712
713 stat.add_csc(&csc);
714
715 assert_eq!(stat.ncols_processed(), 2);
716 assert_eq!(stat.count_positives(), vec![1.0, 1.0, 1.0]);
717 assert_eq!(stat.sum(), vec![1.0, 2.0, 3.0]);
718 }
719
720 #[test]
721 fn test_sparse_running_stat_f64() {
722 let mut stat = SparseRunningStatistics::<f64>::new(2);
723
724 stat.add_sparse_column(&[0, 1], &[1.0, 2.0]);
725 stat.add_sparse_column(&[0], &[3.0]);
726
727 assert_eq!(stat.ncols_processed(), 2);
728 assert_eq!(stat.sum(), vec![4.0, 2.0]);
729
730 let mean = stat.mean();
731 assert!((mean[0] - 2.0).abs() < 1e-10);
732 assert!((mean[1] - 1.0).abs() < 1e-10);
733 }
734
735 #[test]
736 fn test_sparse_column_running_stat_csc() {
737 use nalgebra_sparse::CooMatrix;
738
739 let mut coo: CooMatrix<f32> = CooMatrix::new(3, 4);
745 coo.push(0, 0, 1.0);
746 coo.push(2, 0, 3.0);
747 coo.push(1, 1, 2.0);
748 coo.push(0, 3, 4.0);
749 coo.push(1, 3, 5.0);
750 let csc = CscMatrix::from(&coo);
751
752 let mut stat = SparseColumnRunningStatistics::<f32>::new(4, 3);
753 stat.add_csc(&csc, 0);
754
755 assert_eq!(stat.count_positives(), vec![2.0, 1.0, 0.0, 2.0]);
756 assert_eq!(stat.sum(), vec![4.0, 2.0, 0.0, 9.0]);
757
758 let mean = stat.mean();
760 assert!((mean[0] - 4.0 / 3.0).abs() < 1e-6);
761 assert!((mean[1] - 2.0 / 3.0).abs() < 1e-6);
762 assert!((mean[2] - 0.0).abs() < 1e-6);
763 assert!((mean[3] - 9.0 / 3.0).abs() < 1e-6);
764 }
765
766 #[test]
767 fn test_sparse_column_running_stat_block_offset() {
768 use nalgebra_sparse::CooMatrix;
769
770 let mut coo_a: CooMatrix<f32> = CooMatrix::new(3, 2);
774 coo_a.push(0, 0, 1.0);
775 coo_a.push(2, 0, 3.0);
776 coo_a.push(1, 1, 2.0);
777 let csc_a = CscMatrix::from(&coo_a);
778
779 let mut coo_b: CooMatrix<f32> = CooMatrix::new(3, 2);
780 coo_b.push(0, 1, 4.0);
781 coo_b.push(1, 1, 5.0);
782 let csc_b = CscMatrix::from(&coo_b);
783
784 let mut stat = SparseColumnRunningStatistics::<f32>::new(4, 3);
785 stat.add_csc(&csc_a, 0);
786 stat.add_csc(&csc_b, 2);
787
788 assert_eq!(stat.count_positives(), vec![2.0, 1.0, 0.0, 2.0]);
789 assert_eq!(stat.sum(), vec![4.0, 2.0, 0.0, 9.0]);
790 }
791
792 #[test]
793 fn test_sparse_column_running_stat_masked() {
794 use nalgebra_sparse::CooMatrix;
795
796 let mut coo: CooMatrix<f32> = CooMatrix::new(4, 2);
800 coo.push(0, 0, 1.0);
801 coo.push(1, 0, 2.0);
802 coo.push(2, 0, 3.0);
803 coo.push(3, 0, 4.0);
804 coo.push(1, 1, 5.0);
805 coo.push(3, 1, 6.0);
806 let csc = CscMatrix::from(&coo);
807
808 let row_mask = vec![true, false, true, false];
809
810 let mut stat = SparseColumnRunningStatistics::<f32>::new(2, 2);
811 stat.add_csc_masked(&csc, 0, &row_mask);
812
813 assert_eq!(stat.count_positives(), vec![2.0, 0.0]);
814 assert_eq!(stat.sum(), vec![4.0, 0.0]);
815 }
816}