solow_stats/
weightstats.rs1use ndarray::Array1;
4use solow_distributions::{norm_cdf, norm_sf, t_cdf, t_sf};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Alternative {
9 TwoSided,
11 Larger,
13 Smaller,
15}
16
17#[derive(Debug, Clone, Copy)]
19pub struct TTestResult {
20 pub statistic: f64,
22 pub pvalue: f64,
24 pub df: f64,
26}
27
28#[derive(Debug, Clone)]
34pub struct DescrStatsW {
35 data: Array1<f64>,
36 weights: Array1<f64>,
37 ddof: f64,
38}
39
40impl DescrStatsW {
41 pub fn new(data: Array1<f64>, weights: Option<Array1<f64>>, ddof: f64) -> Self {
44 let weights = weights.unwrap_or_else(|| Array1::ones(data.len()));
45 assert_eq!(
46 data.len(),
47 weights.len(),
48 "data and weights length mismatch"
49 );
50 DescrStatsW {
51 data,
52 weights,
53 ddof,
54 }
55 }
56
57 pub fn sum_weights(&self) -> f64 {
59 self.weights.sum()
60 }
61
62 pub fn nobs(&self) -> f64 {
64 self.sum_weights()
65 }
66
67 pub fn sum(&self) -> f64 {
69 self.data.dot(&self.weights)
70 }
71
72 pub fn mean(&self) -> f64 {
74 self.sum() / self.sum_weights()
75 }
76
77 pub fn sumsquares(&self) -> f64 {
79 let m = self.mean();
80 self.data
81 .iter()
82 .zip(self.weights.iter())
83 .map(|(&x, &w)| w * (x - m) * (x - m))
84 .sum()
85 }
86
87 pub fn var_ddof(&self, ddof: f64) -> f64 {
89 self.sumsquares() / (self.sum_weights() - ddof)
90 }
91
92 pub fn var(&self) -> f64 {
94 self.var_ddof(self.ddof)
95 }
96
97 pub fn std_ddof(&self, ddof: f64) -> f64 {
99 self.var_ddof(ddof).sqrt()
100 }
101
102 pub fn std(&self) -> f64 {
104 self.var().sqrt()
105 }
106
107 pub fn std_mean(&self) -> f64 {
112 let mut std = self.std();
113 if self.ddof != 0.0 {
114 std *= ((self.sum_weights() - self.ddof) / self.sum_weights()).sqrt();
115 }
116 std / (self.sum_weights() - 1.0).sqrt()
117 }
118
119 pub fn ttest_mean(&self, value: f64, alternative: Alternative) -> TTestResult {
124 let tstat = (self.mean() - value) / self.std_mean();
125 let dof = self.sum_weights() - 1.0;
126 let pvalue = match alternative {
127 Alternative::TwoSided => t_sf(tstat.abs(), dof) * 2.0,
128 Alternative::Larger => t_sf(tstat, dof),
129 Alternative::Smaller => t_cdf(tstat, dof),
130 };
131 TTestResult {
132 statistic: tstat,
133 pvalue,
134 df: dof,
135 }
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum UseVar {
142 Pooled,
144 Unequal,
146}
147
148pub fn ttest_ind(
154 x1: &Array1<f64>,
155 x2: &Array1<f64>,
156 alternative: Alternative,
157 usevar: UseVar,
158 value: f64,
159) -> TTestResult {
160 let d1 = DescrStatsW::new(x1.clone(), None, 0.0);
161 let d2 = DescrStatsW::new(x2.clone(), None, 0.0);
162 let n1 = d1.nobs();
163 let n2 = d2.nobs();
164 let ss1 = d1.sumsquares();
165 let ss2 = d2.sumsquares();
166 let var1 = ss1 / n1;
168 let var2 = ss2 / n2;
169
170 let (stdm, dof) = match usevar {
171 UseVar::Pooled => {
172 let var_pooled = (ss1 + ss2) / (n1 - 1.0 + n2 - 1.0);
173 let stdm = (var_pooled * (1.0 / n1 + 1.0 / n2)).sqrt();
174 (stdm, n1 - 1.0 + n2 - 1.0)
175 }
176 UseVar::Unequal => {
177 let sem1 = var1 / (n1 - 1.0);
178 let sem2 = var2 / (n2 - 1.0);
179 let semsum = sem1 + sem2;
180 let stdm = semsum.sqrt();
181 let z1 = (sem1 / semsum).powi(2) / (n1 - 1.0);
182 let z2 = (sem2 / semsum).powi(2) / (n2 - 1.0);
183 let dof = 1.0 / (z1 + z2);
184 (stdm, dof)
185 }
186 };
187
188 let tstat = (d1.mean() - d2.mean() - value) / stdm;
189 let pvalue = match alternative {
190 Alternative::TwoSided => t_sf(tstat.abs(), dof) * 2.0,
191 Alternative::Larger => t_sf(tstat, dof),
192 Alternative::Smaller => t_cdf(tstat, dof),
193 };
194 TTestResult {
195 statistic: tstat,
196 pvalue,
197 df: dof,
198 }
199}
200
201pub fn ztest(x1: &Array1<f64>, value: f64, alternative: Alternative, ddof: f64) -> TTestResult {
208 let n1 = x1.len() as f64;
209 let mean = x1.sum() / n1;
210 let var0 = x1.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / n1;
212 let var = var0 / (n1 - ddof);
213 let std_diff = var.sqrt();
214 let zstat = (mean - value) / std_diff;
215 let pvalue = match alternative {
216 Alternative::TwoSided => norm_sf(zstat.abs()) * 2.0,
217 Alternative::Larger => norm_sf(zstat),
218 Alternative::Smaller => norm_cdf(zstat),
219 };
220 TTestResult {
221 statistic: zstat,
222 pvalue,
223 df: f64::INFINITY,
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use ndarray::array;
231
232 #[test]
233 fn unweighted_mean_matches_plain_mean() {
234 let d = DescrStatsW::new(array![1.0, 2.0, 3.0, 4.0], None, 0.0);
235 assert!((d.mean() - 2.5).abs() < 1e-12);
236 assert!((d.sum_weights() - 4.0).abs() < 1e-12);
237 }
238
239 #[test]
240 fn weighted_mean_basic() {
241 let d = DescrStatsW::new(array![1.0, 3.0], Some(array![1.0, 3.0]), 0.0);
242 assert!((d.mean() - 2.5).abs() < 1e-12);
244 }
245}