Skip to main content

mollendorff_forge/bootstrap/
config.rs

1//! Bootstrap Resampling Configuration
2//!
3//! Handles parsing and validation of bootstrap configuration.
4
5use serde::{Deserialize, Serialize};
6
7/// Statistic to compute from bootstrap samples
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9#[serde(rename_all = "lowercase")]
10pub enum BootstrapStatistic {
11    /// Sample mean
12    #[default]
13    Mean,
14    /// Sample median
15    Median,
16    /// Sample standard deviation
17    Std,
18    /// Sample variance
19    Var,
20    /// Specific percentile (requires additional parameter)
21    Percentile,
22    /// Sample minimum
23    Min,
24    /// Sample maximum
25    Max,
26}
27
28/// Configuration for bootstrap resampling
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct BootstrapConfig {
31    /// Number of bootstrap iterations
32    #[serde(default = "default_iterations")]
33    pub iterations: usize,
34    /// Confidence levels for intervals (e.g., 0.90, 0.95, 0.99)
35    #[serde(default = "default_confidence_levels")]
36    pub confidence_levels: Vec<f64>,
37    /// Random seed for reproducibility
38    pub seed: Option<u64>,
39    /// Historical data to resample from
40    #[serde(default)]
41    pub data: Vec<f64>,
42    /// Statistic to compute
43    #[serde(default)]
44    pub statistic: BootstrapStatistic,
45    /// Percentile value (if statistic is Percentile)
46    #[serde(default = "default_percentile")]
47    pub percentile_value: f64,
48}
49
50const fn default_iterations() -> usize {
51    10000
52}
53
54fn default_confidence_levels() -> Vec<f64> {
55    vec![0.90, 0.95, 0.99]
56}
57
58const fn default_percentile() -> f64 {
59    50.0
60}
61
62impl Default for BootstrapConfig {
63    fn default() -> Self {
64        Self {
65            iterations: 10000,
66            confidence_levels: vec![0.90, 0.95, 0.99],
67            seed: None,
68            data: Vec::new(),
69            statistic: BootstrapStatistic::Mean,
70            percentile_value: 50.0,
71        }
72    }
73}
74
75impl BootstrapConfig {
76    /// Create a new configuration
77    #[must_use]
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Set the data
83    #[must_use]
84    pub fn with_data(mut self, data: Vec<f64>) -> Self {
85        self.data = data;
86        self
87    }
88
89    /// Set the number of iterations
90    #[must_use]
91    pub const fn with_iterations(mut self, iterations: usize) -> Self {
92        self.iterations = iterations;
93        self
94    }
95
96    /// Set confidence levels
97    #[must_use]
98    pub fn with_confidence_levels(mut self, levels: Vec<f64>) -> Self {
99        self.confidence_levels = levels;
100        self
101    }
102
103    /// Set the seed
104    #[must_use]
105    pub const fn with_seed(mut self, seed: u64) -> Self {
106        self.seed = Some(seed);
107        self
108    }
109
110    /// Set the statistic
111    #[must_use]
112    pub const fn with_statistic(mut self, stat: BootstrapStatistic) -> Self {
113        self.statistic = stat;
114        self
115    }
116
117    /// Set percentile value
118    #[must_use]
119    pub const fn with_percentile(mut self, percentile: f64) -> Self {
120        self.statistic = BootstrapStatistic::Percentile;
121        self.percentile_value = percentile;
122        self
123    }
124
125    /// Validate configuration
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if data is empty or has fewer than 2 observations,
130    /// iterations is zero, confidence levels are missing or out of range,
131    /// or percentile value is out of range when using `Percentile` statistic.
132    pub fn validate(&self) -> Result<(), String> {
133        if self.data.is_empty() {
134            return Err("Data cannot be empty".to_string());
135        }
136
137        if self.data.len() < 2 {
138            return Err("Data must have at least 2 observations".to_string());
139        }
140
141        if self.iterations == 0 {
142            return Err("Iterations must be positive".to_string());
143        }
144
145        if self.confidence_levels.is_empty() {
146            return Err("At least one confidence level required".to_string());
147        }
148
149        for level in &self.confidence_levels {
150            if *level <= 0.0 || *level >= 1.0 {
151                return Err(format!("Confidence level {level} must be between 0 and 1"));
152            }
153        }
154
155        if self.statistic == BootstrapStatistic::Percentile
156            && (self.percentile_value <= 0.0 || self.percentile_value >= 100.0)
157        {
158            return Err(format!(
159                "Percentile {} must be between 0 and 100",
160                self.percentile_value
161            ));
162        }
163
164        Ok(())
165    }
166}
167
168#[cfg(test)]
169mod config_tests {
170    use super::*;
171
172    #[test]
173    fn test_config_validation() {
174        let config = BootstrapConfig::new()
175            .with_data(vec![1.0, 2.0, 3.0, 4.0, 5.0])
176            .with_iterations(1000);
177
178        assert!(config.validate().is_ok());
179    }
180
181    #[test]
182    fn test_empty_data_rejected() {
183        let config = BootstrapConfig::new();
184        assert!(config.validate().is_err());
185    }
186
187    #[test]
188    fn test_single_observation_rejected() {
189        let config = BootstrapConfig::new().with_data(vec![1.0]);
190        assert!(config.validate().is_err());
191    }
192
193    #[test]
194    fn test_invalid_confidence_rejected() {
195        let config = BootstrapConfig::new()
196            .with_data(vec![1.0, 2.0, 3.0])
197            .with_confidence_levels(vec![1.5]); // Invalid
198
199        assert!(config.validate().is_err());
200    }
201
202    #[test]
203    fn test_invalid_percentile_rejected() {
204        let config = BootstrapConfig::new()
205            .with_data(vec![1.0, 2.0, 3.0])
206            .with_percentile(150.0); // Invalid
207
208        assert!(config.validate().is_err());
209    }
210}