sklears_preprocessing/temporal/
seasonal.rs1use scirs2_core::ndarray::{s, Array1, Array2};
7use sklears_core::{
8 error::{Result, SklearsError},
9 traits::{Fit, Trained, Transform, Untrained},
10 types::Float,
11};
12use std::marker::PhantomData;
13
14#[derive(Debug, Clone)]
16pub struct SeasonalDecomposerConfig {
17 pub season_length: usize,
19 pub method: DecompositionMethod,
21 pub include_trend: bool,
23 pub include_seasonal: bool,
25 pub include_residual: bool,
27 pub include_seasonal_strength: bool,
29 pub include_trend_strength: bool,
31}
32
33#[derive(Debug, Clone, Copy)]
35pub enum DecompositionMethod {
36 Additive,
38 Multiplicative,
40}
41
42impl Default for SeasonalDecomposerConfig {
43 fn default() -> Self {
44 Self {
45 season_length: 12, method: DecompositionMethod::Additive,
47 include_trend: true,
48 include_seasonal: true,
49 include_residual: false,
50 include_seasonal_strength: true,
51 include_trend_strength: true,
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct SeasonalDecomposer<S> {
59 config: SeasonalDecomposerConfig,
60 n_features_out_: Option<usize>,
61 _phantom: PhantomData<S>,
62}
63
64impl SeasonalDecomposer<Untrained> {
65 pub fn new() -> Self {
67 Self {
68 config: SeasonalDecomposerConfig::default(),
69 n_features_out_: None,
70 _phantom: PhantomData,
71 }
72 }
73
74 pub fn season_length(mut self, season_length: usize) -> Self {
76 self.config.season_length = season_length;
77 self
78 }
79
80 pub fn method(mut self, method: DecompositionMethod) -> Self {
82 self.config.method = method;
83 self
84 }
85
86 pub fn include_trend(mut self, include_trend: bool) -> Self {
88 self.config.include_trend = include_trend;
89 self
90 }
91
92 pub fn include_seasonal(mut self, include_seasonal: bool) -> Self {
94 self.config.include_seasonal = include_seasonal;
95 self
96 }
97
98 pub fn include_residual(mut self, include_residual: bool) -> Self {
100 self.config.include_residual = include_residual;
101 self
102 }
103
104 pub fn include_seasonal_strength(mut self, include_seasonal_strength: bool) -> Self {
106 self.config.include_seasonal_strength = include_seasonal_strength;
107 self
108 }
109
110 pub fn include_trend_strength(mut self, include_trend_strength: bool) -> Self {
112 self.config.include_trend_strength = include_trend_strength;
113 self
114 }
115
116 fn calculate_n_features_out(&self) -> usize {
118 let mut count = 0;
119 if self.config.include_trend {
120 count += 1;
121 }
122 if self.config.include_seasonal {
123 count += 1;
124 }
125 if self.config.include_residual {
126 count += 1;
127 }
128 if self.config.include_seasonal_strength {
129 count += 1;
130 }
131 if self.config.include_trend_strength {
132 count += 1;
133 }
134 count
135 }
136}
137
138impl SeasonalDecomposer<Trained> {
139 pub fn n_features_out(&self) -> usize {
141 self.n_features_out_.expect("Decomposer should be fitted")
142 }
143
144 fn decompose(
146 &self,
147 data: &Array1<Float>,
148 ) -> Result<(Array1<Float>, Array1<Float>, Array1<Float>)> {
149 let n = data.len();
150 let season_length = self.config.season_length;
151
152 if n < 2 * season_length {
153 return Err(SklearsError::InvalidInput(format!(
154 "Data length {} is too short for season length {}",
155 n, season_length
156 )));
157 }
158
159 let mut trend = Array1::<Float>::zeros(n);
161 let mut seasonal = Array1::<Float>::zeros(n);
162
163 let half_season = season_length / 2;
165 for i in half_season..(n - half_season) {
166 let start = i - half_season;
167 let end = i + half_season + 1;
168 let sum: Float = data.slice(s![start..end]).sum();
169 trend[i] = sum / season_length as Float;
170 }
171
172 if half_season > 0 {
174 let start_slope = (trend[half_season + 1] - trend[half_season]) / 1.0;
175 let end_slope = (trend[n - half_season - 1] - trend[n - half_season - 2]) / 1.0;
176
177 for i in 0..half_season {
178 trend[i] = trend[half_season] - start_slope * (half_season - i) as Float;
179 }
180 for i in (n - half_season)..n {
181 trend[i] =
182 trend[n - half_season - 1] + end_slope * (i - (n - half_season - 1)) as Float;
183 }
184 }
185
186 let mut seasonal_averages = vec![0.0; season_length];
188 let mut seasonal_counts = vec![0; season_length];
189
190 match self.config.method {
191 DecompositionMethod::Additive => {
192 let detrended: Array1<Float> = data - &trend;
194
195 for (i, &value) in detrended.iter().enumerate() {
197 let season_idx = i % season_length;
198 seasonal_averages[season_idx] += value;
199 seasonal_counts[season_idx] += 1;
200 }
201
202 for i in 0..season_length {
203 if seasonal_counts[i] > 0 {
204 seasonal_averages[i] /= seasonal_counts[i] as Float;
205 }
206 }
207
208 let seasonal_mean =
210 seasonal_averages.iter().sum::<Float>() / season_length as Float;
211 for avg in &mut seasonal_averages {
212 *avg -= seasonal_mean;
213 }
214
215 for i in 0..n {
217 seasonal[i] = seasonal_averages[i % season_length];
218 }
219 }
220 DecompositionMethod::Multiplicative => {
221 let detrended: Array1<Float> =
223 data.mapv(|x| x.max(1e-10)) / trend.mapv(|x| x.max(1e-10));
224
225 for (i, &value) in detrended.iter().enumerate() {
227 let season_idx = i % season_length;
228 seasonal_averages[season_idx] += value;
229 seasonal_counts[season_idx] += 1;
230 }
231
232 for i in 0..season_length {
233 if seasonal_counts[i] > 0 {
234 seasonal_averages[i] /= seasonal_counts[i] as Float;
235 }
236 }
237
238 let geometric_mean = seasonal_averages.iter().map(|&x| x.ln()).sum::<Float>()
240 / season_length as Float;
241 let geometric_mean = geometric_mean.exp();
242
243 for avg in &mut seasonal_averages {
244 *avg /= geometric_mean;
245 }
246
247 for i in 0..n {
249 seasonal[i] = seasonal_averages[i % season_length];
250 }
251 }
252 }
253
254 let residual = match self.config.method {
256 DecompositionMethod::Additive => data - &trend - &seasonal,
257 DecompositionMethod::Multiplicative => {
258 let trend_seasonal = &trend * &seasonal;
259 data.mapv(|x| x.max(1e-10)) / trend_seasonal.mapv(|x| x.max(1e-10))
260 }
261 };
262
263 Ok((trend, seasonal, residual))
264 }
265
266 fn calculate_seasonal_strength(&self, data: &Array1<Float>, seasonal: &Array1<Float>) -> Float {
268 let seasonal_var = seasonal.var(0.0);
269 let residual = match self.config.method {
270 DecompositionMethod::Additive => data - seasonal,
271 DecompositionMethod::Multiplicative => {
272 data.mapv(|x| x.max(1e-10)) / seasonal.mapv(|x| x.max(1e-10))
273 }
274 };
275 let residual_var = residual.var(0.0);
276
277 if residual_var > 1e-10 {
278 (seasonal_var / residual_var).clamp(0.0, 1.0)
279 } else {
280 1.0
281 }
282 }
283
284 fn calculate_trend_strength(&self, trend: &Array1<Float>) -> Float {
286 let trend_mean = trend.mean().unwrap_or(0.0);
287 let trend_std = trend.std(0.0);
288
289 if trend_mean.abs() > 1e-10 {
290 (trend_std / trend_mean.abs()).min(1.0)
291 } else {
292 0.0
293 }
294 }
295}
296
297impl Default for SeasonalDecomposer<Untrained> {
298 fn default() -> Self {
299 Self::new()
300 }
301}
302
303impl Fit<Array1<Float>, ()> for SeasonalDecomposer<Untrained> {
304 type Fitted = SeasonalDecomposer<Trained>;
305
306 fn fit(self, x: &Array1<Float>, _y: &()) -> Result<Self::Fitted> {
307 let n_features_out = self.calculate_n_features_out();
308
309 if n_features_out == 0 {
310 return Err(SklearsError::InvalidParameter {
311 name: "feature_selection".to_string(),
312 reason: "No features selected for extraction".to_string(),
313 });
314 }
315
316 if x.len() < 2 * self.config.season_length {
317 return Err(SklearsError::InvalidInput(format!(
318 "Data length {} is too short for season length {}",
319 x.len(),
320 self.config.season_length
321 )));
322 }
323
324 Ok(SeasonalDecomposer {
325 config: self.config,
326 n_features_out_: Some(n_features_out),
327 _phantom: PhantomData,
328 })
329 }
330}
331
332impl Transform<Array1<Float>, Array2<Float>> for SeasonalDecomposer<Trained> {
333 fn transform(&self, x: &Array1<Float>) -> Result<Array2<Float>> {
334 let n_samples = x.len();
335 let n_features_out = self.n_features_out();
336
337 let (trend, seasonal, residual) = self.decompose(x)?;
338
339 let mut result = Array2::<Float>::zeros((n_samples, n_features_out));
340 let mut feature_idx = 0;
341
342 if self.config.include_trend {
343 for (i, &value) in trend.iter().enumerate() {
344 result[[i, feature_idx]] = value;
345 }
346 feature_idx += 1;
347 }
348
349 if self.config.include_seasonal {
350 for (i, &value) in seasonal.iter().enumerate() {
351 result[[i, feature_idx]] = value;
352 }
353 feature_idx += 1;
354 }
355
356 if self.config.include_residual {
357 for (i, &value) in residual.iter().enumerate() {
358 result[[i, feature_idx]] = value;
359 }
360 feature_idx += 1;
361 }
362
363 if self.config.include_seasonal_strength {
364 let seasonal_strength = self.calculate_seasonal_strength(x, &seasonal);
365 for i in 0..n_samples {
366 result[[i, feature_idx]] = seasonal_strength;
367 }
368 feature_idx += 1;
369 }
370
371 if self.config.include_trend_strength {
372 let trend_strength = self.calculate_trend_strength(&trend);
373 for i in 0..n_samples {
374 result[[i, feature_idx]] = trend_strength;
375 }
376 }
377
378 Ok(result)
379 }
380}