Skip to main content

converge_analytics/packs/descriptive_stats/
solver.rs

1use super::types::*;
2use converge_pack::gate::GateResult as Result;
3use converge_pack::gate::{ProblemSpec, ReplayEnvelope, SolverReport};
4
5pub struct DescriptiveStatsSolver;
6
7impl DescriptiveStatsSolver {
8    pub fn solve(
9        &self,
10        input: &DescriptiveStatsInput,
11        spec: &ProblemSpec,
12    ) -> Result<(DescriptiveStatsOutput, SolverReport)> {
13        let n = input.values.len() as f64;
14        let mut sorted = input.values.clone();
15        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
16
17        let min = sorted[0];
18        let max = sorted[sorted.len() - 1];
19        let range = max - min;
20        let mean = input.values.iter().sum::<f64>() / n;
21        let median = percentile_sorted(&sorted, 50.0);
22
23        let variance = input.values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
24        let std_dev = variance.sqrt();
25
26        let skewness = if std_dev > 0.0 {
27            input
28                .values
29                .iter()
30                .map(|v| ((v - mean) / std_dev).powi(3))
31                .sum::<f64>()
32                / n
33        } else {
34            0.0
35        };
36
37        let kurtosis = if std_dev > 0.0 {
38            input
39                .values
40                .iter()
41                .map(|v| ((v - mean) / std_dev).powi(4))
42                .sum::<f64>()
43                / n
44                - 3.0 // excess kurtosis
45        } else {
46            0.0
47        };
48
49        let percentiles: Vec<PercentileResult> = input
50            .percentiles
51            .iter()
52            .map(|&p| PercentileResult {
53                percentile: p,
54                value: percentile_sorted(&sorted, p),
55            })
56            .collect();
57
58        let output = DescriptiveStatsOutput {
59            count: input.values.len(),
60            mean,
61            median,
62            std_dev,
63            variance,
64            min,
65            max,
66            range,
67            skewness,
68            kurtosis,
69            percentiles,
70        };
71
72        let replay = ReplayEnvelope::minimal(spec.seed());
73        let report = SolverReport::optimal("descriptive-stats-v1", 1.0, replay);
74
75        Ok((output, report))
76    }
77}
78
79fn percentile_sorted(sorted: &[f64], p: f64) -> f64 {
80    if sorted.is_empty() {
81        return 0.0;
82    }
83    if sorted.len() == 1 {
84        return sorted[0];
85    }
86    let rank = p / 100.0 * (sorted.len() - 1) as f64;
87    let lower = rank.floor() as usize;
88    let upper = rank.ceil() as usize;
89    let frac = rank - lower as f64;
90    if lower == upper {
91        sorted[lower]
92    } else {
93        sorted[lower] * (1.0 - frac) + sorted[upper] * frac
94    }
95}