solow_stats/
descriptivestats.rs1use crate::normality::jarque_bera;
9use ndarray::Array1;
10use solow_distributions::{norm_ppf, t_ppf};
11
12pub const PERCENTILES: [f64; 9] = [1.0, 5.0, 10.0, 25.0, 50.0, 75.0, 90.0, 95.0, 99.0];
14
15#[derive(Debug, Clone)]
19pub struct Description {
20 pub nobs: f64,
22 pub missing: f64,
24 pub mean: f64,
26 pub std_err: f64,
28 pub upper_ci: f64,
30 pub lower_ci: f64,
32 pub std: f64,
34 pub iqr: f64,
36 pub iqr_normal: f64,
38 pub mad: f64,
40 pub mad_normal: f64,
42 pub coef_var: f64,
44 pub range: f64,
46 pub max: f64,
48 pub min: f64,
50 pub skew: f64,
52 pub kurtosis: f64,
54 pub jarque_bera: f64,
56 pub jarque_bera_pval: f64,
58 pub mode: f64,
60 pub mode_freq: f64,
62 pub median: f64,
64 pub percentiles: Vec<f64>,
66}
67
68fn percentile_sorted(sorted: &[f64], q: f64) -> f64 {
72 let n = sorted.len();
73 if n == 1 {
74 return sorted[0];
75 }
76 let pos = q * (n as f64 - 1.0);
77 let lo = pos.floor() as usize;
78 let hi = pos.ceil() as usize;
79 if lo == hi {
80 return sorted[lo];
81 }
82 let frac = pos - lo as f64;
83 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
84}
85
86pub fn describe(data: &Array1<f64>, alpha: f64, use_t: bool) -> Description {
94 let total = data.len() as f64;
95 let mut clean: Vec<f64> = data.iter().copied().filter(|v| !v.is_nan()).collect();
96 let n = clean.len() as f64;
97 let missing = total - n;
98
99 let mean = clean.iter().sum::<f64>() / n;
100 let var = clean.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / (n - 1.0);
102 let std = var.sqrt();
103 let std_err = std / n.sqrt();
104
105 let q = if use_t {
106 t_ppf(1.0 - alpha / 2.0, n - 1.0)
107 } else {
108 norm_ppf(1.0 - alpha / 2.0)
109 };
110
111 clean.sort_by(|a, b| a.total_cmp(b));
112 let q25 = percentile_sorted(&clean, 0.25);
113 let q50 = percentile_sorted(&clean, 0.5);
114 let q75 = percentile_sorted(&clean, 0.75);
115 let iqr = q75 - q25;
116 let iqr_normal = iqr / (norm_ppf(0.75) - norm_ppf(0.25));
118
119 let mad = clean.iter().map(|&v| (v - mean).abs()).sum::<f64>() / n;
120 let mad_normal = mad / (2.0 / std::f64::consts::PI).sqrt();
121
122 let coef_var = std / mean;
123 let max = *clean.last().unwrap();
124 let min = clean[0];
125 let range = max - min;
126
127 let jb = jarque_bera(&Array1::from(clean.clone()));
128
129 let (mode, mode_count) = compute_mode(&clean);
131 let mode_freq = mode_count / n;
132
133 let percentiles: Vec<f64> = PERCENTILES
134 .iter()
135 .map(|&p| percentile_sorted(&clean, p / 100.0))
136 .collect();
137
138 Description {
139 nobs: n,
140 missing,
141 mean,
142 std_err,
143 upper_ci: mean + q * std_err,
144 lower_ci: mean - q * std_err,
145 std,
146 iqr,
147 iqr_normal,
148 mad,
149 mad_normal,
150 coef_var,
151 range,
152 max,
153 min,
154 skew: jb.skew,
155 kurtosis: jb.kurtosis,
156 jarque_bera: jb.statistic,
157 jarque_bera_pval: jb.pvalue,
158 mode,
159 mode_freq,
160 median: q50,
161 percentiles,
162 }
163}
164
165fn compute_mode(sorted: &[f64]) -> (f64, f64) {
167 let mut best_val = sorted[0];
168 let mut best_count = 0usize;
169 let mut i = 0;
170 while i < sorted.len() {
171 let v = sorted[i];
172 let mut j = i;
173 while j < sorted.len() && sorted[j] == v {
174 j += 1;
175 }
176 let count = j - i;
177 if count > best_count {
178 best_count = count;
179 best_val = v;
180 }
181 i = j;
182 }
183 (best_val, best_count as f64)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use ndarray::array;
190
191 #[test]
192 fn basic_mean_and_median() {
193 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
194 let d = describe(&x, 0.05, false);
195 assert!((d.mean - 3.0).abs() < 1e-12);
196 assert!((d.median - 3.0).abs() < 1e-12);
197 assert!((d.nobs - 5.0).abs() < 1e-12);
198 }
199
200 #[test]
201 fn nan_counts_as_missing() {
202 let x = array![1.0, f64::NAN, 3.0];
203 let d = describe(&x, 0.05, false);
204 assert!((d.missing - 1.0).abs() < 1e-12);
205 assert!((d.nobs - 2.0).abs() < 1e-12);
206 }
207}