1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct BiasDatasetConfig {
16 pub min_samples_per_category: usize,
18 pub frequency_weighted: bool,
20 pub validate_distributions: bool,
22 pub evaluation_seeds: Vec<u64>,
24 pub confidence_level: f64,
26 pub detailed: bool,
28}
29
30impl Default for BiasDatasetConfig {
31 fn default() -> Self {
32 Self {
33 min_samples_per_category: 30,
34 frequency_weighted: false,
35 validate_distributions: false,
36 evaluation_seeds: vec![42, 123, 456, 789, 999],
37 confidence_level: 0.95,
38 detailed: false,
39 }
40 }
41}
42
43impl BiasDatasetConfig {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn with_frequency_weighting(mut self) -> Self {
51 self.frequency_weighted = true;
52 self
53 }
54
55 pub fn with_validation(mut self) -> Self {
57 self.validate_distributions = true;
58 self
59 }
60
61 pub fn with_min_samples(mut self, min: usize) -> Self {
63 self.min_samples_per_category = min;
64 self
65 }
66
67 pub fn with_seeds(mut self, seeds: Vec<u64>) -> Self {
69 self.evaluation_seeds = seeds;
70 self
71 }
72
73 pub fn with_detailed(mut self, detailed: bool) -> Self {
75 self.detailed = detailed;
76 self
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct StatisticalBiasResults {
87 pub mean: f64,
89 pub std_dev: f64,
91 pub ci_95: (f64, f64),
93 pub min: f64,
95 pub max: f64,
97 pub effect_size: Option<f64>,
99 pub n: usize,
101 pub std_error: f64,
103}
104
105impl StatisticalBiasResults {
106 pub fn from_values(values: &[f64], confidence_level: f64) -> Self {
108 if values.is_empty() {
109 return Self {
110 mean: 0.0,
111 std_dev: 0.0,
112 ci_95: (0.0, 0.0),
113 min: 0.0,
114 max: 0.0,
115 effect_size: None,
116 n: 0,
117 std_error: 0.0,
118 };
119 }
120
121 let n = values.len();
122 let mean = values.iter().sum::<f64>() / n as f64;
123 let variance = if n > 1 {
124 values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64
125 } else {
126 0.0
127 };
128 let std_dev = variance.sqrt();
129 let std_error = std_dev / (n as f64).sqrt();
130
131 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
132 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
133
134 let z_score = if confidence_level == 0.95 {
136 1.96
137 } else if confidence_level == 0.99 {
138 2.576
139 } else {
140 1.96 * (confidence_level / 0.95)
142 };
143 let margin = z_score * std_error;
144 let ci_95 = (mean - margin, mean + margin);
145
146 Self {
147 mean,
148 std_dev,
149 ci_95,
150 min,
151 max,
152 effect_size: None,
153 n,
154 std_error,
155 }
156 }
157
158 pub fn compute_effect_size(group1: &[f64], group2: &[f64]) -> f64 {
160 if group1.is_empty() || group2.is_empty() {
161 return 0.0;
162 }
163
164 let mean1 = group1.iter().sum::<f64>() / group1.len() as f64;
165 let mean2 = group2.iter().sum::<f64>() / group2.len() as f64;
166
167 let var1 = if group1.len() > 1 {
168 group1.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / (group1.len() - 1) as f64
169 } else {
170 0.0
171 };
172
173 let var2 = if group2.len() > 1 {
174 group2.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / (group2.len() - 1) as f64
175 } else {
176 0.0
177 };
178
179 let pooled_std = ((var1 + var2) / 2.0).sqrt();
180 if pooled_std == 0.0 {
181 return 0.0;
182 }
183
184 (mean1 - mean2) / pooled_std
185 }
186
187 pub fn format_with_ci(&self) -> String {
189 format!(
190 "{:.3} (95% CI: {:.3} - {:.3}, n={}, SD={:.3})",
191 self.mean, self.ci_95.0, self.ci_95.1, self.n, self.std_dev
192 )
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct FrequencyWeightedResults {
203 pub unweighted_rate: f64,
205 pub weighted_rate: f64,
207 pub frequency_distribution: HashMap<String, f64>,
209 pub n: usize,
211}
212
213impl FrequencyWeightedResults {
214 pub fn new(recognized: &[bool], frequencies: &HashMap<String, f64>, names: &[String]) -> Self {
216 if recognized.is_empty() {
217 return Self {
218 unweighted_rate: 0.0,
219 weighted_rate: 0.0,
220 frequency_distribution: frequencies.clone(),
221 n: 0,
222 };
223 }
224
225 let unweighted_rate =
226 recognized.iter().filter(|&&r| r).count() as f64 / recognized.len() as f64;
227
228 let mut weighted_sum = 0.0;
230 let mut total_weight = 0.0;
231
232 for (i, &rec) in recognized.iter().enumerate() {
233 if i < names.len() {
234 let freq = frequencies
235 .get(&names[i])
236 .copied()
237 .unwrap_or(1.0 / names.len() as f64);
238 if rec {
239 weighted_sum += freq;
240 }
241 total_weight += freq;
242 }
243 }
244
245 let weighted_rate = if total_weight > 0.0 {
246 weighted_sum / total_weight
247 } else {
248 unweighted_rate
249 };
250
251 Self {
252 unweighted_rate,
253 weighted_rate,
254 frequency_distribution: frequencies.clone(),
255 n: recognized.len(),
256 }
257 }
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct DistributionValidation {
267 pub is_valid: bool,
269 pub max_deviation: f64,
271 pub category_deviations: HashMap<String, f64>,
273 pub tolerance: f64,
275}
276
277impl DistributionValidation {
278 pub fn validate(
280 observed: &HashMap<String, f64>,
281 reference: &HashMap<String, f64>,
282 tolerance: f64,
283 ) -> Self {
284 let mut max_deviation: f64 = 0.0;
285 let mut category_deviations = HashMap::new();
286
287 for (category, &ref_value) in reference {
288 let obs_value = observed.get(category).copied().unwrap_or(0.0);
289 let deviation = (obs_value - ref_value).abs();
290 category_deviations.insert(category.clone(), deviation);
291 max_deviation = max_deviation.max(deviation);
292 }
293
294 for category in observed.keys() {
296 if !reference.contains_key(category) {
297 let deviation = observed[category];
298 category_deviations.insert(category.clone(), deviation);
299 max_deviation = max_deviation.max(deviation);
300 }
301 }
302
303 let is_valid = max_deviation <= tolerance;
304
305 Self {
306 is_valid,
307 max_deviation,
308 category_deviations,
309 tolerance,
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_statistical_results() {
320 let values = vec![0.8, 0.82, 0.79, 0.81, 0.83];
321 let results = StatisticalBiasResults::from_values(&values, 0.95);
322
323 assert!((results.mean - 0.81).abs() < 0.01);
324 assert!(results.n == 5);
325 assert!(results.ci_95.0 < results.mean);
326 assert!(results.ci_95.1 > results.mean);
327 }
328
329 #[test]
330 fn test_effect_size() {
331 let group1 = vec![0.9, 0.91, 0.89, 0.92, 0.88];
332 let group2 = vec![0.7, 0.71, 0.69, 0.72, 0.68];
333
334 let d = StatisticalBiasResults::compute_effect_size(&group1, &group2);
335 assert!(d > 0.0); assert!(d < 100.0); }
340
341 #[test]
342 fn test_frequency_weighted() {
343 let recognized = vec![true, false, true, true, false];
344 let mut frequencies = HashMap::new();
345 frequencies.insert("Name1".to_string(), 0.5);
346 frequencies.insert("Name2".to_string(), 0.3);
347 frequencies.insert("Name3".to_string(), 0.2);
348 let names = vec![
349 "Name1".to_string(),
350 "Name2".to_string(),
351 "Name3".to_string(),
352 "Name1".to_string(),
353 "Name2".to_string(),
354 ];
355
356 let results = FrequencyWeightedResults::new(&recognized, &frequencies, &names);
357 assert!(results.unweighted_rate > 0.0);
358 assert!(results.weighted_rate > 0.0);
359 }
360
361 #[test]
362 fn test_distribution_validation() {
363 let mut observed = HashMap::new();
364 observed.insert("A".to_string(), 0.5);
365 observed.insert("B".to_string(), 0.5);
366
367 let mut reference = HashMap::new();
368 reference.insert("A".to_string(), 0.48);
369 reference.insert("B".to_string(), 0.52);
370
371 let validation = DistributionValidation::validate(&observed, &reference, 0.1);
372 assert!(validation.is_valid); assert!(validation.max_deviation < 0.1);
374 }
375}