breakout/
multi.rs

1use crate::Error;
2
3/// Parameters for detecting multiple breakouts.
4pub struct MultiParams {
5    min_size: usize,
6    degree: i32,
7    beta: Option<f64>,
8    percent: Option<f64>,
9}
10
11/// Returns parameters for detecting multiple breakouts.
12pub fn multi() -> MultiParams {
13    MultiParams {
14        min_size: 30,
15        degree: 1,
16        beta: None,
17        percent: None,
18    }
19}
20
21impl MultiParams {
22    /// Sets the minimum observations between breakouts.
23    pub fn min_size(&mut self, value: usize) -> &mut Self {
24        self.min_size = value;
25        self
26    }
27
28    /// Sets the degree of the penalization polynomial.
29    pub fn degree(&mut self, value: i32) -> &mut Self {
30        self.degree = value;
31        self
32    }
33
34    /// Sets the penalization term.
35    pub fn beta<T>(&mut self, value: T) -> &mut Self
36    where
37        T: Into<Option<f64>>,
38    {
39        self.beta = value.into();
40        self
41    }
42
43    /// Sets the minimum percent change in goodness of fit statistic.
44    pub fn percent<T>(&mut self, value: T) -> &mut Self
45    where
46        T: Into<Option<f64>>,
47    {
48        self.percent = value.into();
49        self
50    }
51
52    /// Detects breakouts in a series.
53    pub fn fit(&self, z: &[f64]) -> Result<Vec<usize>, Error> {
54        if self.min_size < 2 {
55            return Err(Error::Parameter("min_size must be at least 2".to_string()));
56        }
57        if self.beta.is_some() && self.percent.is_some() {
58            return Err(Error::Parameter(
59                "beta and percent cannot be passed together".to_string(),
60            ));
61        }
62        if self.degree < 0 || self.degree > 2 {
63            return Err(Error::Parameter("degree must be 0, 1, or 2".to_string()));
64        }
65
66        if z.len() < self.min_size {
67            return Ok(Vec::new());
68        }
69
70        // scale observations
71        let min = z.iter().min_by(|i, j| i.partial_cmp(j).unwrap()).unwrap();
72        let max = z.iter().max_by(|i, j| i.partial_cmp(j).unwrap()).unwrap();
73        let denom = max - min;
74        if denom == 0.0 {
75            return Ok(Vec::new());
76        }
77        let zcounts: Vec<f64> = z.iter().map(|x| (x - min) / denom).collect();
78
79        if self.percent.is_some() {
80            Ok(crate::edm_multi::edm_percent(
81                &zcounts,
82                self.min_size,
83                self.percent.unwrap(),
84                self.degree,
85            ))
86        } else {
87            Ok(crate::edm_multi::edm_multi(
88                &zcounts,
89                self.min_size,
90                self.beta.unwrap_or(0.008),
91                self.degree,
92            ))
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use crate::Error;
100
101    #[rustfmt::skip]
102    fn generate_series() -> Vec<f64> {
103        vec![
104            3.0, 1.0, 2.0, 3.0, 2.0, 1.0, 1.0, 2.0, 2.0, 3.0,
105            6.0, 4.0, 4.0, 5.0, 6.0, 4.0, 4.0, 4.0, 6.0, 5.0,
106            9.0, 8.0, 7.0, 9.0, 8.0, 9.0, 9.0, 9.0, 7.0, 9.0
107        ]
108    }
109
110    #[test]
111    fn test_multi() {
112        let series = generate_series();
113        let breakouts = crate::multi().min_size(5).fit(&series).unwrap();
114        assert_eq!(vec![10, 15, 20], breakouts);
115    }
116
117    #[test]
118    fn test_percent() {
119        let series = generate_series();
120        let breakouts = crate::multi()
121            .min_size(5)
122            .percent(0.5)
123            .fit(&series)
124            .unwrap();
125        assert_eq!(vec![8, 19], breakouts);
126    }
127
128    #[test]
129    fn test_empty() {
130        let series = Vec::new();
131        let breakouts = crate::multi().fit(&series).unwrap();
132        assert!(breakouts.is_empty());
133    }
134
135    #[test]
136    fn test_constant() {
137        let series = vec![1.0; 100];
138        let breakouts = crate::multi().fit(&series).unwrap();
139        assert!(breakouts.is_empty());
140    }
141
142    #[test]
143    fn test_almost_constant() {
144        let mut series = vec![1.0; 100];
145        series[50] = 2.0;
146        let breakouts = crate::multi().fit(&series).unwrap();
147        assert!(breakouts.is_empty());
148    }
149
150    #[test]
151    #[rustfmt::skip]
152    fn test_simple() {
153        let series = vec![
154            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
155            1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
156        ];
157        let breakouts = crate::multi().min_size(5).fit(&series).unwrap();
158        assert_eq!(vec![10], breakouts);
159    }
160
161    #[test]
162    fn test_bad_min_size() {
163        let series = Vec::new();
164        let result = crate::multi().min_size(1).fit(&series);
165        assert_eq!(
166            result.unwrap_err(),
167            Error::Parameter("min_size must be at least 2".to_string())
168        );
169    }
170
171    #[test]
172    fn test_beta_percent() {
173        let series = Vec::new();
174        let result = crate::multi().beta(0.008).percent(0.5).fit(&series);
175        assert_eq!(
176            result.unwrap_err(),
177            Error::Parameter("beta and percent cannot be passed together".to_string())
178        );
179    }
180
181    #[test]
182    fn test_bad_degree() {
183        let series = Vec::new();
184        let result = crate::multi().degree(3).fit(&series);
185        assert_eq!(
186            result.unwrap_err(),
187            Error::Parameter("degree must be 0, 1, or 2".to_string())
188        );
189    }
190}