1use crate::filtering::filter_events_by_gate;
2use crate::types::Gate;
3use flow_fcs::Fcs;
4use anyhow::{Context, Result};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct GateStatistics {
37 pub event_count: usize,
39 pub percentage: f64,
41 pub centroid: (f64, f64),
43 pub x_stats: ParameterStatistics,
45 pub y_stats: ParameterStatistics,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ParameterStatistics {
56 pub parameter: String,
58 pub mean: f64,
60 pub geometric_mean: f64,
62 pub median: f64,
64 pub std_dev: f64,
66 pub min: f64,
68 pub max: f64,
70 pub q1: f64,
72 pub q3: f64,
74 pub cv: f64,
76}
77
78impl GateStatistics {
79 pub fn calculate(fcs: &Fcs, gate: &Gate) -> Result<Self> {
81 let indices = filter_events_by_gate(fcs, gate, None)?;
83 let event_count = indices.len();
84
85 if event_count == 0 {
86 return Ok(Self::empty(gate));
87 }
88
89 let x_param = gate.x_parameter_channel_name();
91 let y_param = gate.y_parameter_channel_name();
92
93 let x_slice = fcs
94 .get_parameter_events_slice(x_param)
95 .with_context(|| format!("Failed to get parameter data for {}", x_param))?;
96 let y_slice = fcs
97 .get_parameter_events_slice(y_param)
98 .with_context(|| format!("Failed to get parameter data for {}", y_param))?;
99
100 let x_values: Vec<f64> = indices.iter().map(|&i| x_slice[i] as f64).collect();
102 let y_values: Vec<f64> = indices.iter().map(|&i| y_slice[i] as f64).collect();
103
104 let total_events = fcs.data_frame.height();
106 let percentage = (event_count as f64 / total_events as f64) * 100.0;
107
108 let centroid = (
110 x_values.iter().sum::<f64>() / event_count as f64,
111 y_values.iter().sum::<f64>() / event_count as f64,
112 );
113
114 let x_stats = ParameterStatistics::calculate(x_param, &x_values)?;
116 let y_stats = ParameterStatistics::calculate(y_param, &y_values)?;
117
118 Ok(Self {
119 event_count,
120 percentage,
121 centroid,
122 x_stats,
123 y_stats,
124 })
125 }
126
127 fn empty(gate: &Gate) -> Self {
129 Self {
130 event_count: 0,
131 percentage: 0.0,
132 centroid: (0.0, 0.0),
133 x_stats: ParameterStatistics::empty(gate.x_parameter_channel_name()),
134 y_stats: ParameterStatistics::empty(gate.y_parameter_channel_name()),
135 }
136 }
137}
138
139impl ParameterStatistics {
140 pub fn calculate(parameter: &str, values: &[f64]) -> Result<Self> {
142 if values.is_empty() {
143 return Ok(Self::empty(parameter));
144 }
145
146 let n = values.len() as f64;
147
148 let mean = values.iter().sum::<f64>() / n;
150
151 let geometric_mean = if values.iter().all(|&v| v > 0.0) {
153 let log_sum: f64 = values.iter().map(|&v| v.ln()).sum();
154 (log_sum / n).exp()
155 } else {
156 f64::NAN
157 };
158
159 let variance = values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / n;
161 let std_dev = variance.sqrt();
162
163 let cv = if mean != 0.0 {
165 (std_dev / mean.abs()) * 100.0
166 } else {
167 f64::NAN
168 };
169
170 let min = values
172 .iter()
173 .copied()
174 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
175 .unwrap_or(f64::NAN);
176 let max = values
177 .iter()
178 .copied()
179 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
180 .unwrap_or(f64::NAN);
181
182 let mut sorted = values.to_vec();
184 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
185
186 let median = percentile(&sorted, 50.0);
187 let q1 = percentile(&sorted, 25.0);
188 let q3 = percentile(&sorted, 75.0);
189
190 Ok(Self {
191 parameter: parameter.to_string(),
192 mean,
193 geometric_mean,
194 median,
195 std_dev,
196 min,
197 max,
198 q1,
199 q3,
200 cv,
201 })
202 }
203
204 fn empty(parameter: &str) -> Self {
206 Self {
207 parameter: parameter.to_string(),
208 mean: f64::NAN,
209 geometric_mean: f64::NAN,
210 median: f64::NAN,
211 std_dev: f64::NAN,
212 min: f64::NAN,
213 max: f64::NAN,
214 q1: f64::NAN,
215 q3: f64::NAN,
216 cv: f64::NAN,
217 }
218 }
219}
220
221fn percentile(sorted_values: &[f64], p: f64) -> f64 {
225 if sorted_values.is_empty() {
226 return f64::NAN;
227 }
228
229 let n = sorted_values.len();
230 if n == 1 {
231 return sorted_values[0];
232 }
233
234 let rank = (p / 100.0) * (n - 1) as f64;
236 let lower_index = rank.floor() as usize;
237 let upper_index = rank.ceil() as usize;
238
239 if lower_index == upper_index {
240 sorted_values[lower_index]
241 } else {
242 let lower_value = sorted_values[lower_index];
244 let upper_value = sorted_values[upper_index];
245 let fraction = rank - lower_index as f64;
246 lower_value + fraction * (upper_value - lower_value)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_percentile() {
256 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
257 assert_eq!(percentile(&data, 0.0), 1.0);
258 assert_eq!(percentile(&data, 50.0), 3.0);
259 assert_eq!(percentile(&data, 100.0), 5.0);
260 }
261
262 #[test]
263 fn test_parameter_statistics() {
264 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
265 let stats = ParameterStatistics::calculate("test", &values).expect("stats");
266
267 assert_eq!(stats.mean, 3.0);
268 assert_eq!(stats.median, 3.0);
269 assert_eq!(stats.min, 1.0);
270 assert_eq!(stats.max, 5.0);
271 assert!((stats.std_dev - 1.4142).abs() < 0.01);
272 }
273
274 #[test]
275 fn test_geometric_mean() {
276 let values = vec![1.0, 2.0, 4.0, 8.0];
277 let stats = ParameterStatistics::calculate("test", &values).expect("stats");
278
279 assert!((stats.geometric_mean - 2.828).abs() < 0.01);
281 }
282
283 #[test]
284 fn test_coefficient_of_variation() {
285 let values = vec![10.0, 20.0, 30.0, 40.0, 50.0];
286 let stats = ParameterStatistics::calculate("test", &values).expect("stats");
287
288 assert!((stats.cv - 47.14).abs() < 1.0);
290 }
291
292 #[test]
293 fn test_empty_statistics() {
294 let stats = ParameterStatistics::calculate("test", &[]).expect("stats");
295 assert!(stats.mean.is_nan());
296 assert!(stats.median.is_nan());
297 }
298}