modewise_results/
tables.rs

1//! Cohort tables written for MATLAB, R and spreadsheets.
2
3use std::path::Path;
4
5use modewise_study::FeatureTable;
6
7use crate::ResultsError;
8
9/// Tab-separated: `recording`, `subject`, `condition`, the participant columns, then one column
10/// per feature. Missing values and NaN are written as `n/a` (BIDS).
11pub fn write_feature_table(path: &Path, table: &FeatureTable) -> Result<(), ResultsError> {
12    let io = |e: std::io::Error| ResultsError::Io {
13        path: path.display().to_string(),
14        message: e.to_string(),
15    };
16    let metadata: Vec<&str> = table.metadata_columns().collect();
17    let mut out = String::new();
18    let header: Vec<&str> = ["recording", "subject", "condition"]
19        .into_iter()
20        .chain(metadata.iter().copied())
21        .chain(table.feature_names().iter().map(String::as_str))
22        .collect();
23    out.push_str(&header.join("\t"));
24    out.push('\n');
25    let text = |v: Option<&str>| v.unwrap_or("n/a").to_string();
26    for (row, o) in table.observations().iter().enumerate() {
27        let mut cells = vec![
28            o.recording.clone(),
29            text(o.subject.as_deref()),
30            text(o.condition.as_deref()),
31        ];
32        for column in &metadata {
33            let values = table.metadata(column).map_err(|e| ResultsError::Io {
34                path: path.display().to_string(),
35                message: e.to_string(),
36            })?;
37            cells.push(text(values[row].as_deref()));
38        }
39        cells.extend(table.values().row(row).iter().map(|v| {
40            if v.is_nan() {
41                "n/a".to_string()
42            } else {
43                v.to_string()
44            }
45        }));
46        out.push_str(&cells.join("\t"));
47        out.push('\n');
48    }
49    if let Some(parent) = path.parent() {
50        std::fs::create_dir_all(parent).map_err(io)?;
51    }
52    std::fs::write(path, out).map_err(io)
53}
54
55#[cfg(test)]
56mod tests {
57    use std::collections::BTreeMap;
58
59    use modewise_study::{Observation, Participants};
60
61    use super::*;
62
63    #[test]
64    fn writes_ids_metadata_and_features_with_missing_as_na() {
65        let rows = vec![
66            Observation {
67                recording: "subject-01".into(),
68                subject: Some("01".into()),
69                condition: Some("face".into()),
70            },
71            Observation {
72                recording: "subject-02".into(),
73                subject: None,
74                condition: None,
75            },
76        ];
77        let p = Participants::new(
78            vec!["group".into()],
79            BTreeMap::from([(
80                "01".to_string(),
81                BTreeMap::from([("group".to_string(), "Patient".to_string())]),
82            )]),
83        )
84        .unwrap();
85        let table = FeatureTable::new(
86            rows,
87            vec!["m0:a~b".into()],
88            ndarray::array![[0.25], [f64::NAN]],
89        )
90        .unwrap()
91        .with_participants(&p)
92        .unwrap();
93        let path =
94            std::env::temp_dir().join(format!("modewise-features-{}/t.tsv", std::process::id()));
95        write_feature_table(&path, &table).unwrap();
96        let text = std::fs::read_to_string(&path).unwrap();
97        assert_eq!(
98            text,
99            "recording\tsubject\tcondition\tgroup\tm0:a~b\nsubject-01\t01\tface\tPatient\t0.25\nsubject-02\tn/a\tn/a\tn/a\tn/a\n"
100        );
101    }
102}