Skip to main content

u_analytics/
decomposition.rs

1//! Seasonal-trend decomposition of a univariate series by Loess (STL).
2//!
3//! A series `y` with a known period is split additively into a trend, a
4//! seasonal component and a remainder, `y = trend + seasonal + residual`, by
5//! the procedure of Cleveland, Cleveland, McRae & Terpenning (1990). The
6//! period is an input — [`crate::seasonality::estimate_period`] estimates it
7//! when it is not known.
8//!
9//! # Algorithm
10//!
11//! An **inner loop** refines the two components, starting from a zero trend:
12//!
13//! 1. *Detrend*: `y − trend`.
14//! 2. *Cycle-subseries smoothing*: the values at each position of the cycle
15//!    (every `period`-th value) are smoothed by Loess with span `seasonal`,
16//!    and each subseries is extended one cycle before and after by Loess
17//!    extrapolation.
18//! 3. *Low-pass filter* of that extended series: moving averages of length
19//!    `period`, `period` and 3, then Loess with span `low_pass`.
20//! 4. *Seasonal* = step 2 − step 3 (the low-pass removes what of the trend
21//!    leaked into the subseries).
22//! 5. *Trend* = Loess with span `trend` of `y − seasonal`.
23//!
24//! The **outer loop** (only when `robust`) gives each point a robustness
25//! weight from its remainder — the bisquare `(1 − (r/h)²)²` with
26//! `h = 6 · median|r|` — and runs the inner loop again with every Loess fit
27//! weighted by it, so that outliers go to the remainder instead of bending
28//! the trend and seasonal components.
29//!
30//! Each Loess fit is a local polynomial of degree 0 or 1 with tricube
31//! weights over the `span` nearest points. This is the original Fortran
32//! implementation (netlib `stl`), which statsmodels' `STL` also follows, with
33//! its defaults: degree 1 everywhere, `seasonal = 7`, `trend` the smallest
34//! odd integer at least `1.5 · period / (1 − 1.5 / seasonal)`, `low_pass` the
35//! smallest odd integer above `period`, and 5 inner / 0 outer iterations
36//! (2 / 15 when robust).
37//!
38//! # Strength of trend and seasonality
39//!
40//! The result also reports `F_T = max(0, 1 − Var(R)/Var(T + R))` and
41//! `F_S = max(0, 1 − Var(R)/Var(S + R))` (Wang, Smith & Hyndman 2006): near 1
42//! when the component explains most of what the remainder does not, near 0
43//! when it is indistinguishable from noise.
44//!
45//! # Complexity
46//!
47//! O(iterations · n · span) for spans `seasonal`, `trend` and `low_pass`.
48//!
49//! # References
50//! - Cleveland, R. B., Cleveland, W. S., McRae, J. E. & Terpenning, I. (1990).
51//!   "STL: A seasonal-trend decomposition procedure based on Loess."
52//!   *Journal of Official Statistics*, 6(1), 3–73.
53//! - Wang, X., Smith, K. & Hyndman, R. J. (2006). "Characteristic-based
54//!   clustering for time series data." *Data Mining and Knowledge Discovery*,
55//!   13(3), 335–364.
56
57/// Default span of the seasonal smoother (statsmodels' `seasonal=7`).
58pub const DEFAULT_SEASONAL: usize = 7;
59
60/// Why a decomposition was refused.
61#[derive(Debug, Clone, PartialEq)]
62pub enum StlError {
63    /// An option is outside its domain. `min` / `max` are inclusive bounds;
64    /// `exclusive_min` an exclusive one (a span longer than the period).
65    OptionOutOfRange {
66        /// The option's name as the builder and the wire formats spell it.
67        option: &'static str,
68        /// What that option has to satisfy.
69        requirement: &'static str,
70        /// The value given.
71        got: f64,
72        /// Inclusive lower bound.
73        min: Option<f64>,
74        /// Exclusive lower bound.
75        exclusive_min: Option<f64>,
76        /// Inclusive upper bound.
77        max: Option<f64>,
78    },
79    /// A span is even: a Loess span is centred on its point, so it is odd.
80    EvenSpan {
81        /// `seasonal`, `trend` or `low_pass`.
82        option: &'static str,
83        /// The value given.
84        got: usize,
85    },
86    /// Fewer than two full cycles.
87    TooFewObservations {
88        /// `2 · period`.
89        needed: usize,
90        /// How many were given.
91        got: usize,
92    },
93    /// The series holds a NaN or an infinity.
94    ValueNotFinite {
95        /// Position of the first such value.
96        index: usize,
97    },
98}
99
100impl core::fmt::Display for StlError {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        match self {
103            StlError::OptionOutOfRange {
104                option,
105                requirement,
106                got,
107                ..
108            } => write!(f, "stl: `{option}` must be {requirement} (got {got})"),
109            StlError::EvenSpan { option, got } => write!(
110                f,
111                "stl: `{option}` must be odd — a Loess span is centred on its point (got {got})"
112            ),
113            StlError::TooFewObservations { needed, got } => write!(
114                f,
115                "stl: needs at least two full cycles, {needed} values (got {got})"
116            ),
117            StlError::ValueNotFinite { index } => {
118                write!(f, "stl: the value at index {index} is not finite")
119            }
120        }
121    }
122}
123
124impl std::error::Error for StlError {}
125
126/// An STL decomposition: `data[i] = trend[i] + seasonal[i] + residual[i]`.
127#[derive(Debug, Clone, PartialEq)]
128pub struct StlResult {
129    /// Trend component.
130    pub trend: Vec<f64>,
131    /// Seasonal component.
132    pub seasonal: Vec<f64>,
133    /// Remainder `data − trend − seasonal`.
134    pub residual: Vec<f64>,
135    /// Robustness weight of each point in the last pass, in `[0, 1]` (all 1
136    /// without `robust`). A weight near 0 marks a point treated as an outlier.
137    pub weights: Vec<f64>,
138    /// Strength of the trend, `max(0, 1 − Var(R)/Var(T + R))`.
139    pub trend_strength: f64,
140    /// Strength of the seasonality, `max(0, 1 − Var(R)/Var(S + R))`.
141    pub seasonal_strength: f64,
142    /// The period decomposed.
143    pub period: usize,
144    /// Span of the seasonal smoother used.
145    pub seasonal_span: usize,
146    /// Span of the trend smoother used (the default when none was given).
147    pub trend_span: usize,
148    /// Span of the low-pass smoother used (the default when none was given).
149    pub low_pass_span: usize,
150}
151
152/// STL decomposition with a builder for its options.
153///
154/// # Examples
155///
156/// ```
157/// use u_analytics::decomposition::Stl;
158///
159/// // A rising line plus a cycle of four.
160/// let pattern = [2.0, -1.0, 0.5, -1.5];
161/// let y: Vec<f64> = (0..48).map(|i| 0.3 * i as f64 + pattern[i % 4]).collect();
162/// let r = Stl::new(4).decompose(&y).unwrap();
163/// assert!((r.seasonal[20] - 2.0).abs() < 1e-6);
164/// assert!(r.seasonal_strength > 0.99);
165/// ```
166#[derive(Debug, Clone, PartialEq)]
167pub struct Stl {
168    period: usize,
169    seasonal: usize,
170    trend: Option<usize>,
171    low_pass: Option<usize>,
172    seasonal_degree: usize,
173    trend_degree: usize,
174    low_pass_degree: usize,
175    robust: bool,
176    inner_iterations: Option<usize>,
177    outer_iterations: Option<usize>,
178}
179
180impl Stl {
181    /// STL for a series with this period, with statsmodels' defaults.
182    pub fn new(period: usize) -> Self {
183        Self {
184            period,
185            seasonal: DEFAULT_SEASONAL,
186            trend: None,
187            low_pass: None,
188            seasonal_degree: 1,
189            trend_degree: 1,
190            low_pass_degree: 1,
191            robust: false,
192            inner_iterations: None,
193            outer_iterations: None,
194        }
195    }
196
197    /// Span of the seasonal smoother: odd, at least 3. Larger = a seasonal
198    /// pattern that changes more slowly from cycle to cycle.
199    pub fn seasonal(mut self, span: usize) -> Self {
200        self.seasonal = span;
201        self
202    }
203
204    /// Span of the trend smoother: odd, at least 3, above the period.
205    pub fn trend(mut self, span: usize) -> Self {
206        self.trend = Some(span);
207        self
208    }
209
210    /// Span of the low-pass smoother: odd, at least 3, above the period.
211    pub fn low_pass(mut self, span: usize) -> Self {
212        self.low_pass = Some(span);
213        self
214    }
215
216    /// Degree (0 or 1) of the seasonal Loess.
217    pub fn seasonal_degree(mut self, degree: usize) -> Self {
218        self.seasonal_degree = degree;
219        self
220    }
221
222    /// Degree (0 or 1) of the trend Loess.
223    pub fn trend_degree(mut self, degree: usize) -> Self {
224        self.trend_degree = degree;
225        self
226    }
227
228    /// Degree (0 or 1) of the low-pass Loess.
229    pub fn low_pass_degree(mut self, degree: usize) -> Self {
230        self.low_pass_degree = degree;
231        self
232    }
233
234    /// Down-weight outliers with the outer loop.
235    pub fn robust(mut self, robust: bool) -> Self {
236        self.robust = robust;
237        self
238    }
239
240    /// Passes of the inner loop (default 5, or 2 when robust). At least 1.
241    pub fn inner_iterations(mut self, n: usize) -> Self {
242        self.inner_iterations = Some(n);
243        self
244    }
245
246    /// Passes of the outer loop (default 0, or 15 when robust).
247    pub fn outer_iterations(mut self, n: usize) -> Self {
248        self.outer_iterations = Some(n);
249        self
250    }
251
252    /// The trend span used when none is given: the smallest odd integer at
253    /// least `1.5 · period / (1 − 1.5 / seasonal)` (Cleveland et al. 1990,
254    /// §3.5; statsmodels' default).
255    pub fn default_trend_span(period: usize, seasonal: usize) -> usize {
256        let t = (1.5 * period as f64 / (1.0 - 1.5 / seasonal as f64)).ceil() as usize;
257        if t % 2 == 0 {
258            t + 1
259        } else {
260            t
261        }
262    }
263
264    /// The low-pass span used when none is given: the smallest odd integer
265    /// above the period.
266    pub fn default_low_pass_span(period: usize) -> usize {
267        let l = period + 1;
268        if l % 2 == 0 {
269            l + 1
270        } else {
271            l
272        }
273    }
274
275    /// Decompose `data`.
276    ///
277    /// # Errors
278    ///
279    /// [`StlError`] when an option is outside its domain, a span is even,
280    /// there are fewer than two full cycles, or a value is not finite.
281    pub fn decompose(&self, data: &[f64]) -> Result<StlResult, StlError> {
282        let period = self.period;
283        if period < 2 {
284            return Err(StlError::OptionOutOfRange {
285                option: "period",
286                requirement: "at least 2",
287                got: period as f64,
288                min: Some(2.0),
289                exclusive_min: None,
290                max: None,
291            });
292        }
293        let span = |option: &'static str, got: usize, above_period: bool| {
294            if got < 3 || (above_period && got <= period) {
295                let (min, exclusive_min, requirement) = if above_period && period >= 3 {
296                    (None, Some(period as f64), "odd and above the period")
297                } else {
298                    (Some(3.0), None, "odd and at least 3")
299                };
300                return Err(StlError::OptionOutOfRange {
301                    option,
302                    requirement,
303                    got: got as f64,
304                    min,
305                    exclusive_min,
306                    max: None,
307                });
308            }
309            if got % 2 == 0 {
310                return Err(StlError::EvenSpan { option, got });
311            }
312            Ok(got)
313        };
314        let seasonal = span("seasonal", self.seasonal, false)?;
315        let trend = span(
316            "trend",
317            self.trend
318                .unwrap_or_else(|| Self::default_trend_span(period, seasonal)),
319            true,
320        )?;
321        let low_pass = span(
322            "low_pass",
323            self.low_pass
324                .unwrap_or_else(|| Self::default_low_pass_span(period)),
325            true,
326        )?;
327        for (option, degree) in [
328            ("seasonal_degree", self.seasonal_degree),
329            ("trend_degree", self.trend_degree),
330            ("low_pass_degree", self.low_pass_degree),
331        ] {
332            if degree > 1 {
333                return Err(StlError::OptionOutOfRange {
334                    option,
335                    requirement: "0 or 1",
336                    got: degree as f64,
337                    min: Some(0.0),
338                    exclusive_min: None,
339                    max: Some(1.0),
340                });
341            }
342        }
343        let inner = self
344            .inner_iterations
345            .unwrap_or(if self.robust { 2 } else { 5 });
346        if inner == 0 {
347            return Err(StlError::OptionOutOfRange {
348                option: "inner_iterations",
349                requirement: "at least 1",
350                got: 0.0,
351                min: Some(1.0),
352                exclusive_min: None,
353                max: None,
354            });
355        }
356        let outer = self
357            .outer_iterations
358            .unwrap_or(if self.robust { 15 } else { 0 });
359
360        let n = data.len();
361        if n < 2 * period {
362            return Err(StlError::TooFewObservations {
363                needed: 2 * period,
364                got: n,
365            });
366        }
367        if let Some(index) = data.iter().position(|v| !v.is_finite()) {
368            return Err(StlError::ValueNotFinite { index });
369        }
370
371        let spans = Spans {
372            period,
373            seasonal,
374            trend,
375            low_pass,
376            seasonal_degree: self.seasonal_degree,
377            trend_degree: self.trend_degree,
378            low_pass_degree: self.low_pass_degree,
379        };
380        let mut trend_c = vec![0.0; n];
381        let mut seasonal_c = vec![0.0; n];
382        let mut weights = vec![1.0; n];
383        let mut use_weights = false;
384        let mut pass = 0;
385        loop {
386            inner_loop(
387                data,
388                &spans,
389                inner,
390                use_weights.then_some(weights.as_slice()),
391                &mut seasonal_c,
392                &mut trend_c,
393            );
394            pass += 1;
395            if pass > outer {
396                break;
397            }
398            let fit: Vec<f64> = trend_c
399                .iter()
400                .zip(&seasonal_c)
401                .map(|(t, s)| t + s)
402                .collect();
403            robustness_weights(data, &fit, &mut weights);
404            use_weights = true;
405        }
406        if outer == 0 {
407            weights.iter_mut().for_each(|w| *w = 1.0);
408        }
409
410        let residual: Vec<f64> = (0..n)
411            .map(|i| data[i] - trend_c[i] - seasonal_c[i])
412            .collect();
413        let var_r = variance(&residual);
414        let strength = |component: &[f64]| {
415            let sum: Vec<f64> = component
416                .iter()
417                .zip(&residual)
418                .map(|(c, r)| c + r)
419                .collect();
420            let v = variance(&sum);
421            if v > 0.0 {
422                (1.0 - var_r / v).max(0.0)
423            } else {
424                0.0
425            }
426        };
427        Ok(StlResult {
428            trend_strength: strength(&trend_c),
429            seasonal_strength: strength(&seasonal_c),
430            trend: trend_c,
431            seasonal: seasonal_c,
432            residual,
433            weights,
434            period,
435            seasonal_span: seasonal,
436            trend_span: trend,
437            low_pass_span: low_pass,
438        })
439    }
440}
441
442struct Spans {
443    period: usize,
444    seasonal: usize,
445    trend: usize,
446    low_pass: usize,
447    seasonal_degree: usize,
448    trend_degree: usize,
449    low_pass_degree: usize,
450}
451
452fn variance(x: &[f64]) -> f64 {
453    let n = x.len() as f64;
454    let mean = x.iter().sum::<f64>() / n;
455    x.iter().map(|v| (v - mean) * (v - mean)).sum::<f64>() / n
456}
457
458/// One run of the inner loop (`stlstp`): `passes` refinements of the
459/// seasonal and trend components, in place.
460fn inner_loop(
461    y: &[f64],
462    s: &Spans,
463    passes: usize,
464    rw: Option<&[f64]>,
465    seasonal: &mut [f64],
466    trend: &mut [f64],
467) {
468    let n = y.len();
469    let np = s.period;
470    let mut detrended = vec![0.0; n];
471    let mut cycle = vec![0.0; n + 2 * np];
472    let mut low = vec![0.0; n];
473    for _ in 0..passes {
474        for i in 0..n {
475            detrended[i] = y[i] - trend[i];
476        }
477        cycle_subseries(
478            &detrended,
479            np,
480            s.seasonal,
481            s.seasonal_degree,
482            rw,
483            &mut cycle,
484        );
485        let filtered = low_pass_filter(&cycle, np);
486        loess(&filtered, s.low_pass, s.low_pass_degree, None, &mut low);
487        for i in 0..n {
488            seasonal[i] = cycle[np + i] - low[i];
489            detrended[i] = y[i] - seasonal[i];
490        }
491        loess(&detrended, s.trend, s.trend_degree, rw, trend);
492    }
493}
494
495/// Cycle-subseries smoothing (`stlss`): each subseries smoothed and extended
496/// one value at each end; the result has `n + 2 · period` values.
497fn cycle_subseries(
498    y: &[f64],
499    np: usize,
500    span: usize,
501    degree: usize,
502    rw: Option<&[f64]>,
503    out: &mut [f64],
504) {
505    let n = y.len();
506    let mut sub = Vec::with_capacity(n / np + 1);
507    let mut sub_w = Vec::with_capacity(n / np + 1);
508    let mut smooth = Vec::with_capacity(n / np + 3);
509    let mut work = vec![0.0; n / np + 1];
510    for j in 0..np {
511        sub.clear();
512        sub_w.clear();
513        sub.extend((j..n).step_by(np).map(|i| y[i]));
514        if let Some(rw) = rw {
515            sub_w.extend((j..n).step_by(np).map(|i| rw[i]));
516        }
517        let w = rw.map(|_| sub_w.as_slice());
518        let k = sub.len();
519        smooth.clear();
520        smooth.resize(k + 2, 0.0);
521        loess(&sub, span, degree, w, &mut smooth[1..=k]);
522        let right = span.min(k);
523        smooth[0] = local_fit(&sub, span, degree, 0.0, 1, right, w, &mut work).unwrap_or(smooth[1]);
524        let left = if k >= span { k - span + 1 } else { 1 };
525        smooth[k + 1] = local_fit(&sub, span, degree, (k + 1) as f64, left, k, w, &mut work)
526            .unwrap_or(smooth[k]);
527        for (m, v) in smooth.iter().enumerate() {
528            out[m * np + j] = *v;
529        }
530    }
531}
532
533/// Moving averages of length `period`, `period`, 3 (`stlfts`): `n + 2 · period`
534/// values in, `n` out.
535fn low_pass_filter(x: &[f64], np: usize) -> Vec<f64> {
536    moving_average(&moving_average(&moving_average(x, np), np), 3)
537}
538
539/// Running-sum moving average of length `len` (`stlma`).
540fn moving_average(x: &[f64], len: usize) -> Vec<f64> {
541    let out_len = x.len() + 1 - len;
542    let flen = len as f64;
543    let mut out = Vec::with_capacity(out_len);
544    let mut v: f64 = x[..len].iter().sum();
545    out.push(v / flen);
546    for k in 1..out_len {
547        v = v - x[k - 1] + x[k + len - 1];
548        out.push(v / flen);
549    }
550    out
551}
552
553/// Loess smoothing of `y` at every position (`stless`, jump 1).
554fn loess(y: &[f64], span: usize, degree: usize, rw: Option<&[f64]>, out: &mut [f64]) {
555    let n = y.len();
556    if n < 2 {
557        out[0] = y[0];
558        return;
559    }
560    let mut work = vec![0.0; n];
561    if span >= n {
562        for i in 1..=n {
563            out[i - 1] =
564                local_fit(y, span, degree, i as f64, 1, n, rw, &mut work).unwrap_or(y[i - 1]);
565        }
566        return;
567    }
568    let half = span.div_ceil(2);
569    let (mut left, mut right) = (1, span);
570    for i in 1..=n {
571        if i > half && right != n {
572            left += 1;
573            right += 1;
574        }
575        out[i - 1] =
576            local_fit(y, span, degree, i as f64, left, right, rw, &mut work).unwrap_or(y[i - 1]);
577    }
578}
579
580/// Local fit at `xs` from the points `left..=right` (1-based) with tricube
581/// weights (`stlest`). `None` when every weight is zero.
582#[allow(clippy::too_many_arguments)]
583fn local_fit(
584    y: &[f64],
585    span: usize,
586    degree: usize,
587    xs: f64,
588    left: usize,
589    right: usize,
590    rw: Option<&[f64]>,
591    w: &mut [f64],
592) -> Option<f64> {
593    let n = y.len();
594    let range = (n - 1) as f64;
595    let mut h = (xs - left as f64).max(right as f64 - xs);
596    if span > n {
597        h += ((span - n) / 2) as f64;
598    }
599    let h9 = 0.999 * h;
600    let h1 = 0.001 * h;
601    let mut a = 0.0;
602    for j in left..=right {
603        let r = (j as f64 - xs).abs();
604        let mut wj = 0.0;
605        if r <= h9 {
606            wj = if r <= h1 {
607                1.0
608            } else {
609                (1.0 - (r / h).powi(3)).powi(3)
610            };
611            if let Some(rw) = rw {
612                wj *= rw[j - 1];
613            }
614            a += wj;
615        }
616        w[j - 1] = wj;
617    }
618    if a <= 0.0 {
619        return None;
620    }
621    for wj in &mut w[left - 1..right] {
622        *wj /= a;
623    }
624    if h > 0.0 && degree > 0 {
625        let a: f64 = (left..=right).map(|j| w[j - 1] * j as f64).sum();
626        let b = xs - a;
627        let c: f64 = (left..=right)
628            .map(|j| w[j - 1] * (j as f64 - a) * (j as f64 - a))
629            .sum();
630        if c.sqrt() > 0.001 * range {
631            let b = b / c;
632            for j in left..=right {
633                w[j - 1] *= b * (j as f64 - a) + 1.0;
634            }
635        }
636    }
637    Some((left..=right).map(|j| w[j - 1] * y[j - 1]).sum())
638}
639
640/// Bisquare robustness weights from the remainder (`stlrwt`).
641fn robustness_weights(y: &[f64], fit: &[f64], rw: &mut [f64]) {
642    let n = y.len();
643    let r: Vec<f64> = y.iter().zip(fit).map(|(a, b)| (a - b).abs()).collect();
644    let mut sorted = r.clone();
645    let m1 = n / 2; // 0-based of Fortran's n/2 + 1
646    let m2 = n - m1 - 1; // 0-based of n − mid1 + 1
647    sorted.sort_by(|a, b| a.total_cmp(b));
648    let cmad = 3.0 * (sorted[m1] + sorted[m2]);
649    let scale = y.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
650    if cmad <= 1e-12 * scale {
651        // More than half the points fit to the rounding of the data: the
652        // remainder is arithmetic noise, not a scale to judge outliers by.
653        rw.iter_mut().for_each(|w| *w = 1.0);
654        return;
655    }
656    let c9 = 0.999 * cmad;
657    let c1 = 0.001 * cmad;
658    for (w, &ri) in rw.iter_mut().zip(&r) {
659        *w = if ri <= c1 {
660            1.0
661        } else if ri <= c9 {
662            let u = ri / cmad;
663            (1.0 - u * u).powi(2)
664        } else {
665            0.0
666        };
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673
674    fn seasonal_line(n: usize) -> Vec<f64> {
675        let pattern = [2.0, -1.0, 0.5, -1.5];
676        (0..n)
677            .map(|i| 0.3 * i as f64 + pattern[i % 4] + 0.05 * (((i * 7919) % 13) as f64 - 6.0))
678            .collect()
679    }
680
681    #[test]
682    fn components_add_up_to_the_series() {
683        let y = seasonal_line(40);
684        let r = Stl::new(4).decompose(&y).unwrap();
685        for (i, v) in y.iter().enumerate() {
686            assert!((r.trend[i] + r.seasonal[i] + r.residual[i] - v).abs() < 1e-12);
687        }
688        assert_eq!((r.seasonal_span, r.trend_span, r.low_pass_span), (7, 9, 5));
689        assert!(r.weights.iter().all(|&w| w == 1.0));
690    }
691
692    #[test]
693    fn default_spans_follow_statsmodels() {
694        assert_eq!(Stl::default_trend_span(12, 7), 23);
695        assert_eq!(Stl::default_trend_span(7, 7), 15);
696        assert_eq!(Stl::default_low_pass_span(12), 13);
697        assert_eq!(Stl::default_low_pass_span(7), 9);
698    }
699
700    #[test]
701    fn refusals_name_the_option() {
702        let y = seasonal_line(40);
703        assert!(matches!(
704            Stl::new(1).decompose(&y),
705            Err(StlError::OptionOutOfRange {
706                option: "period",
707                ..
708            })
709        ));
710        assert!(matches!(
711            Stl::new(4).seasonal(8).decompose(&y),
712            Err(StlError::EvenSpan {
713                option: "seasonal",
714                got: 8
715            })
716        ));
717        assert!(matches!(
718            Stl::new(4).trend(3).decompose(&y),
719            Err(StlError::OptionOutOfRange { option: "trend", exclusive_min: Some(e), .. }) if e == 4.0
720        ));
721        assert!(matches!(
722            Stl::new(4).trend_degree(2).decompose(&y),
723            Err(StlError::OptionOutOfRange {
724                option: "trend_degree",
725                ..
726            })
727        ));
728        assert_eq!(
729            Stl::new(4).decompose(&y[..7]),
730            Err(StlError::TooFewObservations { needed: 8, got: 7 })
731        );
732        let mut bad = y.clone();
733        bad[5] = f64::NAN;
734        assert_eq!(
735            Stl::new(4).decompose(&bad),
736            Err(StlError::ValueNotFinite { index: 5 })
737        );
738    }
739
740    #[test]
741    fn robust_pass_isolates_an_outlier() {
742        let mut y = seasonal_line(48);
743        y[21] += 50.0;
744        let plain = Stl::new(4).decompose(&y).unwrap();
745        let robust = Stl::new(4).robust(true).decompose(&y).unwrap();
746        assert!(robust.weights[21] < 0.01);
747        assert!(robust.residual[21] > 45.0);
748        assert!(robust.residual[21] > plain.residual[21]);
749    }
750
751    #[test]
752    fn two_cycles_fit_exactly_and_rounding_marks_no_outlier() {
753        // Two values per subseries: the seasonal fit interpolates them, the
754        // remainder is rounding — robust weights must not read it as outliers.
755        let y = seasonal_line(24);
756        let r = Stl::new(12).robust(true).decompose(&y).unwrap();
757        assert!(r.residual.iter().all(|v| v.abs() < 1e-9));
758        assert!(r.weights.iter().all(|&w| w == 1.0));
759    }
760
761    #[test]
762    fn unit_and_offset_do_not_change_the_shape() {
763        let y = seasonal_line(48);
764        let base = Stl::new(4).robust(true).decompose(&y).unwrap();
765        for scale in [1e-9, 1e9] {
766            let ys: Vec<f64> = y.iter().map(|v| v * scale + 7.0 * scale).collect();
767            let r = Stl::new(4).robust(true).decompose(&ys).unwrap();
768            for i in 0..y.len() {
769                assert!((r.seasonal[i] / scale - base.seasonal[i]).abs() < 1e-6);
770                assert!((r.weights[i] - base.weights[i]).abs() < 1e-9);
771            }
772            assert!((r.seasonal_strength - base.seasonal_strength).abs() < 1e-9);
773        }
774    }
775}
776
777/// STL against statsmodels 0.15.0 `STL(y, period, ...).fit()` on seeded
778/// series (rounded to 3 places): plain, robust with outliers, explicit spans,
779/// degree 0, a robust walk, a seasonal span longer than the subseries.
780#[cfg(test)]
781#[allow(clippy::approx_constant)] // seeded data, not mathematical constants
782mod statsmodels_reference {
783    use super::*;
784
785    // Generated by stl_ref.py - statsmodels 0.15.0 STL, seed 20261027.
786    // case a: n=60 period=12 plain {}
787    const Y_A: [f64; 60] = [
788        -0.837, 1.092, 2.924, 2.522, 2.875, 1.517, -0.253, -2.621, -1.958, -1.908, -2.977, -2.431,
789        0.283, 2.564, 4.129, 3.407, 3.442, 2.796, 1.797, -1.15, -1.333, -3.02, -1.687, -0.955,
790        -0.182, 2.155, 3.519, 4.249, 3.793, 3.02, 2.088, -0.256, -2.699, -1.328, -0.227, -0.623,
791        0.974, 4.007, 3.766, 5.139, 4.737, 3.505, 1.892, 0.397, -1.058, -0.66, -1.369, 0.869,
792        1.651, 3.504, 5.64, 5.463, 6.604, 3.893, 1.852, 1.122, 0.75, -1.866, 0.37, 0.427,
793    ];
794    const T_A: [f64; 60] = [
795        -0.7146485637078752,
796        -0.6164832210335263,
797        -0.5181516830760083,
798        -0.4196844754873599,
799        -0.32110131859995555,
800        -0.2225207692561986,
801        -0.12423555675326292,
802        -0.02655075377094378,
803        0.07045746328083816,
804        0.16696728502205307,
805        0.26338291889548854,
806        0.36043534415399964,
807        0.44834507397027185,
808        0.5280747524929036,
809        0.6002229363186898,
810        0.6611364297563962,
811        0.7062536526796435,
812        0.7365967978054677,
813        0.7584153148362388,
814        0.7761606513272636,
815        0.7923364502503742,
816        0.8093936697027934,
817        0.8296387158227432,
818        0.8518631642150369,
819        0.8773150661668782,
820        0.9087084512521527,
821        0.9464431041493069,
822        0.9938060179447082,
823        1.054557457023869,
824        1.1249976100247374,
825        1.199865521544602,
826        1.2737985030102073,
827        1.3436229324321316,
828        1.4106131175610293,
829        1.47365203587191,
830        1.530351119602024,
831        1.5832977086243996,
832        1.6333441280837124,
833        1.6832582510569378,
834        1.7331639399795236,
835        1.7828201268627029,
836        1.8322863129013094,
837        1.8800526846196015,
838        1.9286762638050863,
839        1.9832437306954993,
840        2.0422080162708522,
841        2.1022504858694147,
842        2.163628155127377,
843        2.2235596908381847,
844        2.268879067727309,
845        2.3095696049217467,
846        2.3490127808421275,
847        2.387720572510347,
848        2.4257236700994116,
849        2.462893961270545,
850        2.4991492682583005,
851        2.534496788787009,
852        2.568912499320822,
853        2.6022119100174876,
854        2.634401344550984,
855    ];
856    const S_A: [f64; 60] = [
857        -0.17020102326156605,
858        1.726556436052315,
859        3.5192952736717285,
860        2.801246475765134,
861        2.916301498311998,
862        1.8679901171016524,
863        0.385417573060896,
864        -2.4616280221274254,
865        -2.1589307740556993,
866        -2.624717947693237,
867        -2.9033791431503775,
868        -2.569979272275229,
869        -0.3949369581430479,
870        1.747341176718214,
871        3.1565219997842857,
872        2.96737239548915,
873        2.9255075319079604,
874        1.8498863004764663,
875        0.4439005079073706,
876        -2.0819707336913336,
877        -2.5733476274600338,
878        -2.8396345182460423,
879        -2.716653100846297,
880        -2.232532099802241,
881        -0.5813597218343396,
882        1.7742640717146578,
883        2.846471756328533,
884        3.1091422937578663,
885        2.977866441638085,
886        1.8125842519758864,
887        0.44065198659717053,
888        -1.7333062411783062,
889        -2.877358310844143,
890        -3.1228573040388334,
891        -2.5814859038818416,
892        -1.923662331769904,
893        -0.6589242066436268,
894        1.654448134865381,
895        2.7902213581215958,
896        3.196646615959359,
897        3.378743148206909,
898        1.6664508812510805,
899        0.03202021680374002,
900        -1.5119953659601402,
901        -2.6801711348329444,
902        -3.4754184948832547,
903        -2.53811618842274,
904        -1.8778570095140745,
905        -0.6882272489291716,
906        1.5713092637842123,
907        2.769808959128488,
908        3.291131276516374,
909        3.8327348075558714,
910        1.5116377387724362,
911        -0.4415786081405177,
912        -1.3117640740058498,
913        -2.3837945654928134,
914        -3.8267872984984574,
915        -2.570245955232843,
916        -1.8449407343136188,
917    ];
918    const W_A: [f64; 60] = [
919        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
920        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
921        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
922        1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
923    ];
924
925    // case b: n=61 period=7 outlier {'robust': True}
926    const Y_B: [f64; 61] = [
927        -0.025, 1.069, 3.35, 3.942, -1.766, -2.181, -2.153, 0.516, 2.39, 3.566, 2.573, -0.231,
928        -2.106, -0.692, 1.693, 3.686, 4.933, 2.177, -0.175, -1.992, 22.633, 0.344, 3.065, 3.292,
929        1.623, 1.014, -1.099, -1.114, 0.943, 3.337, 4.413, 2.255, 0.203, 0.065, -1.017, 2.144,
930        5.494, 3.122, 2.931, 1.845, -18.96, 0.318, 1.661, 5.137, 6.434, 3.989, 0.746, -0.527,
931        -0.577, 2.835, 4.683, 6.083, 3.598, 2.246, -0.686, 0.157, 2.89, 4.609, 6.722, 4.243, 0.62,
932    ];
933    const T_B: [f64; 61] = [
934        -0.3442510947669004,
935        -0.23062424435746,
936        -0.11685262949421589,
937        -0.002469718500161184,
938        0.11274093104960016,
939        0.2289590590691011,
940        0.34669621930681893,
941        0.4641410268250529,
942        0.5918511164309425,
943        0.7336052455122047,
944        0.884323396730363,
945        1.0360398044200754,
946        1.1636505625530387,
947        1.245837797738338,
948        1.286580011423615,
949        1.290432042200402,
950        1.2442565697083043,
951        1.164855851233538,
952        1.063971152139175,
953        0.9472392292523064,
954        0.8329130621863098,
955        0.7639822496461729,
956        0.7514682659961501,
957        0.771524888489271,
958        0.8125694910960958,
959        0.8849167375990827,
960        0.9743782676420534,
961        1.0741244472560494,
962        1.1874184134193109,
963        1.3058037914844787,
964        1.424472836211481,
965        1.5601850416849212,
966        1.7151903867377953,
967        1.8770424109280648,
968        2.044406635246177,
969        2.2091025127247335,
970        2.3597781235451203,
971        2.4870176685979795,
972        2.580461292190404,
973        2.6573004568075813,
974        2.7290947382698336,
975        2.768976483362874,
976        2.775035332744085,
977        2.7573688301410217,
978        2.7140131730950867,
979        2.645656622357796,
980        2.58230075551713,
981        2.524365699956627,
982        2.4667235014472038,
983        2.4239646079783688,
984        2.4138328106340237,
985        2.428357847754513,
986        2.453585556625788,
987        2.4814970344726954,
988        2.513165475314854,
989        2.5480943246238934,
990        2.5842123463425395,
991        2.620406502455848,
992        2.6563542843093986,
993        2.6922064934988774,
994        2.728112204519468,
995    ];
996    const S_B: [f64; 61] = [
997        0.2806980187795315,
998        1.4424643683777076,
999        3.309859801264952,
1000        1.8893345562968902,
1001        -1.66195680288314,
1002        -2.9614845621636157,
1003        -2.2475741103014277,
1004        0.15409820365643145,
1005        1.7409003514457582,
1006        3.1842055432617626,
1007        1.5373450003572735,
1008        -1.5168859483966926,
1009        -2.7663839335106224,
1010        -2.28021361493963,
1011        0.02868900722678074,
1012        2.0190783320351633,
1013        3.055816690497411,
1014        1.209301757094185,
1015        -1.392748079058499,
1016        -2.557150028631557,
1017        -2.3283217690321734,
1018        -0.08277082758805875,
1019        2.26725469072666,
1020        2.957666606262043,
1021        0.8714716745433282,
1022        -1.3087917264876636,
1023        -2.257299363938695,
1024        -2.3825384207403197,
1025        -0.20064860020793335,
1026        2.3923190059595694,
1027        3.049359773786675,
1028        0.7423574645215033,
1029        -1.3291972803458882,
1030        -2.27500508802811,
1031        -2.5646136176361476,
1032        -0.11024987307195383,
1033        2.408543693561008,
1034        3.283056252051252,
1035        0.8446694985351011,
1036        -1.4320403635697274,
1037        -2.602957908650004,
1038        -2.63457485800498,
1039        0.06978123588918689,
1040        2.338058755805587,
1041        3.537084619549874,
1042        1.0453567720871335,
1043        -1.6473847818099971,
1044        -2.8964683776519093,
1045        -2.637053599801231,
1046        0.22227977439498894,
1047        2.2364162812135167,
1048        3.7987107994003124,
1049        1.2751337860004317,
1050        -1.877933876664946,
1051        -3.218492433488227,
1052        -2.620328624516707,
1053        0.3863930871033018,
1054        2.1108508097586216,
1055        4.051933593412693,
1056        1.5187065616313047,
1057        -2.114415902711208,
1058    ];
1059    const W_B: [f64; 61] = [
1060        0.9997292269068923,
1061        0.9695652397933096,
1062        0.9809542316008585,
1063        0.0,
1064        0.9423651229627944,
1065        0.734405988773694,
1066        0.9232852137137939,
1067        0.9917886807301683,
1068        0.9940206965090626,
1069        0.8889394114902743,
1070        0.9971333240611042,
1071        0.9303323027332928,
1072        0.760428447475915,
1073        0.86637004183294,
1074        0.8339724328930439,
1075        0.8424500018058209,
1076        0.6052806385563598,
1077        0.9432511306815423,
1078        0.9808039178471991,
1079        0.8567328788380366,
1080        0.0,
1081        0.8849392676251042,
1082        0.9985147579627649,
1083        0.7986949677342619,
1084        0.9943515347120641,
1085        0.0,
1086        0.9664450636558382,
1087        0.9614963970696023,
1088        0.9989195146322842,
1089        0.852399171466217,
1090        0.9951604693611221,
1091        0.9977196196085271,
1092        0.9408384687736601,
1093        0.7908358409889987,
1094        0.7534909697893347,
1095        0.9901271716333155,
1096        0.5527536287838176,
1097        0.0,
1098        0.7648624635843374,
1099        0.6529578612993352,
1100        0.0,
1101        0.95046361562721,
1102        0.12304637167915942,
1103        0.9961449380002448,
1104        0.9534801123689413,
1105        0.8917773805691143,
1106        0.9607518121979802,
1107        0.9769105086278805,
1108        0.8343505718889977,
1109        0.945216049382716,
1110        0.999470548492894,
1111        0.9760149956852816,
1112        0.9807071000485312,
1113        0.0,
1114        0.9997986989258324,
1115        0.9469564672618057,
1116        0.9967764725460029,
1117        0.9830052938078254,
1118        0.9999124350801569,
1119        0.9991755348009493,
1120        0.9999553771693069,
1121    ];
1122
1123    // case c: n=50 period=5 walk {'seasonal': 11, 'trend': 17, 'low_pass': 7}
1124    const Y_C: [f64; 50] = [
1125        2.899, 1.404, -1.09, -1.191, 1.22, 1.92, 1.528, -1.227, -1.219, 0.985, 4.068, 3.003, 0.249,
1126        0.273, 2.171, 1.173, 0.425, -0.886, -2.126, -0.761, 1.133, 0.238, -0.288, 1.395, 3.725,
1127        4.073, 1.126, -2.636, -4.191, -1.998, -1.086, -2.35, -4.801, -6.16, -4.845, -3.834, -4.53,
1128        -4.438, -4.914, -4.304, -2.863, -3.678, -5.134, -4.719, -2.169, -1.517, -2.931, -4.141,
1129        -4.018, -1.645,
1130    ];
1131    const T_C: [f64; 50] = [
1132        0.3595203697419831,
1133        0.4133930642182823,
1134        0.4747768840530127,
1135        0.5435546680555853,
1136        0.619198114753108,
1137        0.7006157218307316,
1138        0.7857221424193703,
1139        0.8726106252710931,
1140        0.9722872255626283,
1141        1.0672531388379005,
1142        1.1095123655999835,
1143        1.1087515830857375,
1144        1.062241411605581,
1145        0.9564646591506943,
1146        0.7940533028867663,
1147        0.5869080747761846,
1148        0.37508509647620825,
1149        0.23031218932473474,
1150        0.21030891092760245,
1151        0.3074701544826551,
1152        0.46153339094123064,
1153        0.5941559397386557,
1154        0.6383212264487284,
1155        0.5719836910961416,
1156        0.42021275781920436,
1157        0.19694515652981875,
1158        -0.1280104380675565,
1159        -0.5930481811158689,
1160        -1.2029650206895672,
1161        -1.9105559538321204,
1162        -2.6131289498929537,
1163        -3.1972277710202404,
1164        -3.6137910639406168,
1165        -3.8913542357283624,
1166        -4.084695546244734,
1167        -4.232297509050723,
1168        -4.336838455231511,
1169        -4.3695194142421405,
1170        -4.307424797070813,
1171        -4.163872109579644,
1172        -3.9851002160601596,
1173        -3.815136335577719,
1174        -3.665168545308158,
1175        -3.5115010353483163,
1176        -3.352547491530393,
1177        -3.189082529735992,
1178        -3.022825774562644,
1179        -2.8554492683085404,
1180        -2.687940406880207,
1181        -2.520467223905384,
1182    ];
1183    const S_C: [f64; 50] = [
1184        1.9811770199303513,
1185        0.9964325672477123,
1186        -1.6639496911020202,
1187        -1.7074363882518788,
1188        0.48439109227970223,
1189        1.8971925211052052,
1190        0.8767641970404536,
1191        -1.5650572383620416,
1192        -1.6403984718625118,
1193        0.5075879485704992,
1194        1.8265999571068101,
1195        0.7716356997912741,
1196        -1.4679414387841518,
1197        -1.5869990529831526,
1198        0.5123046197969641,
1199        1.7642672260377779,
1200        0.6856967005677134,
1201        -1.361321906507085,
1202        -1.5368582227494896,
1203        0.4993615211265455,
1204        1.6909560314383185,
1205        0.5994208438145355,
1206        -1.2267892658829993,
1207        -1.468209067376339,
1208        0.4848383868349154,
1209        1.5872641684260307,
1210        0.4792404577037621,
1211        -1.1167226637194017,
1212        -1.3821272181795852,
1213        0.4995775453353193,
1214        1.540497098999799,
1215        0.3958723226446683,
1216        -1.0968850533070467,
1217        -1.33581145863842,
1218        0.5583095598817691,
1219        1.50199003911766,
1220        0.32930446900491595,
1221        -1.0679208315418778,
1222        -1.300179407712935,
1223        0.5912549540997699,
1224        1.4462761765144987,
1225        0.2639949024928305,
1226        -1.0226430045502393,
1227        -1.2463805712810467,
1228        0.624199874857744,
1229        1.3748658333196466,
1230        0.18963002569731405,
1231        -0.9811266241591107,
1232        -1.1788791887612886,
1233        0.6766480651519423,
1234    ];
1235    const W_C: [f64; 50] = [
1236        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1237        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1238        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1239    ];
1240
1241    // case d: n=37 period=4 plain {'seasonal_deg': 0, 'trend_deg': 0, 'low_pass_deg': 0}
1242    const Y_D: [f64; 37] = [
1243        0.39, 1.758, 0.311, -3.652, -0.216, 1.701, 0.579, -2.206, 0.144, 3.245, 1.514, -3.3, 0.043,
1244        3.213, -0.027, -0.615, 0.923, 2.396, 0.684, -2.364, 1.723, 4.237, 1.198, -1.383, 0.675,
1245        4.491, 1.556, -1.63, -0.445, 5.039, 1.742, -1.983, 1.801, 3.564, 2.798, -1.145, 2.008,
1246    ];
1247    const T_D: [f64; 37] = [
1248        -0.29061594267302715,
1249        -0.2973927741153516,
1250        -0.3073351786431117,
1251        -0.33505220397377183,
1252        -0.3528489557335598,
1253        -0.19649320522216404,
1254        0.0015922709096244866,
1255        0.2563307771874918,
1256        0.46800059701173313,
1257        0.4614461082021752,
1258        0.39318706163134826,
1259        0.3623442483985528,
1260        0.31390915172430783,
1261        0.43178562855612257,
1262        0.6346582301858984,
1263        0.6933269615676654,
1264        0.6858712436708,
1265        0.6810799527108491,
1266        0.6961762130679717,
1267        0.8353162435221444,
1268        1.0504968974462212,
1269        1.2171926624324578,
1270        1.3034891280694905,
1271        1.3002424793726404,
1272        1.284409768450904,
1273        1.232090942519993,
1274        1.1317427109376321,
1275        1.144457822901661,
1276        1.1614598274190113,
1277        1.182347001119035,
1278        1.3048667475329754,
1279        1.4304187646720594,
1280        1.5053123019505208,
1281        1.6567582985022575,
1282        1.727099183257099,
1283        1.7613622559962399,
1284        1.7873551899593902,
1285    ];
1286    const S_D: [f64; 37] = [
1287        0.11612195647986145,
1288        2.286570671419279,
1289        0.43281480402315803,
1290        -2.823946636684542,
1291        0.10299028357493027,
1292        2.3068525711905274,
1293        0.3950957416846182,
1294        -2.7864712784457293,
1295        0.07385800252794973,
1296        2.342457241902704,
1297        0.33444143049657177,
1298        -2.71854857507006,
1299        -0.003007898777023888,
1300        2.456249404673185,
1301        0.1469183033101349,
1302        -2.6506653723197506,
1303        0.0741383971341427,
1304        2.581125321312145,
1305        -0.04872975464060914,
1306        -2.590364519446663,
1307        -0.09283995102014814,
1308        2.811044525732896,
1309        0.0684953240208349,
1310        -2.750899327511905,
1311        -0.33664736335837303,
1312        2.896990376996522,
1313        0.30837041696305617,
1314        -2.9085954324631014,
1315        -0.28606373645371885,
1316        2.858851132982773,
1317        0.3834687089169155,
1318        -2.9563709321899485,
1319        -0.2944567709915975,
1320        2.860919506781294,
1321        0.4185738278504357,
1322        -2.985719585141617,
1323        -0.2970207951936875,
1324    ];
1325    const W_D: [f64; 37] = [
1326        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1327        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1328        1.0,
1329    ];
1330
1331    // case e: n=72 period=6 walk {'robust': True, 'seasonal': 9}
1332    const Y_E: [f64; 72] = [
1333        4.042, 5.551, 4.218, 3.099, 4.548, 7.424, 9.549, 8.295, 7.601, 3.885, 4.435, 6.254, 6.491,
1334        5.891, 2.247, 0.91, 2.648, 6.619, 7.529, 4.301, 1.437, 0.36, 3.008, 5.856, 5.425, 4.295,
1335        2.234, 2.376, 4.021, 5.98, 8.462, 6.309, 4.615, 4.104, 7.024, 9.063, 9.708, 10.862, 10.036,
1336        9.95, 11.243, 12.526, 13.671, 11.613, 6.923, 5.188, 8.301, 11.248, 12.66, 12.045, 9.566,
1337        7.377, 7.505, 9.268, 10.486, 10.731, 7.876, 8.43, 9.713, 10.987, 12.515, 11.422, 8.938,
1338        9.57, 10.861, 13.347, 14.964, 12.097, 10.042, 8.738, 10.763, 13.8,
1339    ];
1340    const T_E: [f64; 72] = [
1341        3.339306089958577,
1342        3.936040801996553,
1343        4.513634527373273,
1344        5.075725970051677,
1345        5.614832651267695,
1346        6.0683623780139815,
1347        6.181311572026499,
1348        6.153886286287091,
1349        6.070148485580003,
1350        5.898473948102176,
1351        5.541241355166631,
1352        5.111083944943771,
1353        4.66813222273837,
1354        4.288545143975496,
1355        4.034269325238091,
1356        3.890909374187796,
1357        3.73906765585241,
1358        3.560939709361493,
1359        3.459624331809137,
1360        3.4403711504012495,
1361        3.3946846794457546,
1362        3.34982867709257,
1363        3.3939078100554814,
1364        3.5084558197784657,
1365        3.700409001638404,
1366        3.8968127769838627,
1367        4.072908914767636,
1368        4.341796781310583,
1369        4.702158733733236,
1370        5.058445212925836,
1371        5.401853515859843,
1372        5.806971010092806,
1373        6.268468654449328,
1374        6.756668732663874,
1375        7.360134551973213,
1376        8.13906742863687,
1377        9.002951248471529,
1378        9.818516724457417,
1379        10.506075819602279,
1380        11.063411373712176,
1381        11.39344614357849,
1382        11.465363428807176,
1383        11.220689137195905,
1384        10.853423936605795,
1385        10.644705192861233,
1386        10.486584943448353,
1387        10.331462836588795,
1388        10.227850324730847,
1389        10.28987472663667,
1390        10.14422469058004,
1391        9.847429251258013,
1392        9.51545630322874,
1393        9.240702219063252,
1394        9.059832640212925,
1395        9.074539040286753,
1396        9.279917912435625,
1397        9.571662000982107,
1398        9.84376783217375,
1399        10.059888991860799,
1400        10.236556699656727,
1401        10.434106025265493,
1402        10.656183133857752,
1403        10.937951965978689,
1404        11.261390761658166,
1405        11.520065205611745,
1406        11.684189475376927,
1407        11.737357833534594,
1408        11.708505384958219,
1409        11.704033954115422,
1410        11.693595803020095,
1411        11.677894141351029,
1412        11.664305406011493,
1413    ];
1414    const S_E: [f64; 72] = [
1415        1.1126902137899433,
1416        1.8379682033882991,
1417        -0.927071577446163,
1418        -2.3238633529327313,
1419        -1.2284197104748877,
1420        1.5359778414382725,
1421        1.4047229533612233,
1422        1.595614520920293,
1423        -1.1454993343666757,
1424        -2.3518690349787548,
1425        -1.0445088219928764,
1426        1.5379476124877398,
1427        1.6769094513632663,
1428        1.362520392555553,
1429        -1.3345685339187126,
1430        -2.3565301285315505,
1431        -0.8721897999797636,
1432        1.5089071688733984,
1433        1.9441842262045688,
1434        1.1480801878829157,
1435        -1.5183913666279765,
1436        -2.3466488367937175,
1437        -0.7123780044790777,
1438        1.4804547629483278,
1439        2.136443552326483,
1440        0.894099525883592,
1441        -1.5957807491918263,
1442        -2.3225593071949175,
1443        -0.5925688623681842,
1444        1.447935936177117,
1445        2.117685328503887,
1446        0.7537995877402939,
1447        -1.3190812555563058,
1448        -2.128220934647446,
1449        -0.7219658188317613,
1450        1.1528277157911888,
1451        2.1215467318319248,
1452        0.8568272917558482,
1453        -1.1341572582869293,
1454        -2.010951974927005,
1455        -0.8899197129330984,
1456        0.8950277113474407,
1457        2.077046539284205,
1458        1.0388518177054709,
1459        -1.1293932849701909,
1460        -1.8717932049623573,
1461        -0.9320068913964269,
1462        0.8334259895662277,
1463        2.1350820675942908,
1464        1.007931965545156,
1465        -1.3013537516608538,
1466        -1.9021995095323365,
1467        -0.9084178171162189,
1468        1.0310998679169345,
1469        2.2578451026966886,
1470        0.943348384895613,
1471        -1.4520157504566429,
1472        -2.018746437089826,
1473        -0.916577749317464,
1474        1.2427566354732489,
1475        2.3936208032606547,
1476        0.8754973995565433,
1477        -1.6296243983051377,
1478        -2.1453100006550945,
1479        -0.8896514111482849,
1480        1.4675882585751068,
1481        2.546493075368808,
1482        0.7809012506411024,
1483        -1.835016183294741,
1484        -2.2928259604857235,
1485        -0.835198338818193,
1486        1.7229189005107237,
1487    ];
1488    const W_E: [f64; 72] = [
1489        0.9401165139649331,
1490        0.9876068156092807,
1491        0.876950773282701,
1492        0.9612801236722237,
1493        0.9919584851011588,
1494        0.9881418586247281,
1495        0.20743174403561665,
1496        0.9170418564428926,
1497        0.0,
1498        0.9644944849112693,
1499        0.9992069563405819,
1500        0.953852508099292,
1501        0.9968581477424536,
1502        0.9812457709849693,
1503        0.9376624898765162,
1504        0.8797447877694339,
1505        0.9828637757719135,
1506        0.4144038623780093,
1507        0.12027826338340111,
1508        0.9708344475119918,
1509        0.9361147893912269,
1510        0.8740040807275263,
1511        0.9667270170094414,
1512        0.7833753726323832,
1513        0.9423509675665717,
1514        0.9273683194305313,
1515        0.9826285464756584,
1516        0.9604072839051773,
1517        0.997720180476704,
1518        0.9148685738764122,
1519        0.7529768396354195,
1520        0.9808597866668539,
1521        0.9660631941661675,
1522        0.9172071367009891,
1523        0.9538031560745991,
1524        0.9840571222159182,
1525        0.4771059260415243,
1526        0.9890107263787425,
1527        0.8683971207372099,
1528        0.7673469315935755,
1529        0.838306888028481,
1530        0.9916911211086208,
1531        0.9570748598884854,
1532        0.9759546419486816,
1533        0.0,
1534        0.0,
1535        0.665610573243546,
1536        0.9890288666540932,
1537        0.982736338927162,
1538        0.7703251192512738,
1539        0.7070474163811469,
1540        0.9826181433631739,
1541        0.8012852544333962,
1542        0.8027683208231016,
1543        0.7922314772664304,
1544        0.9223648278373678,
1545        0.9815822781413498,
1546        0.890377388772185,
1547        0.9020001884367043,
1548        0.9268013694133308,
1549        0.9700890121951367,
1550        0.9962159725017129,
1551        0.957911345947805,
1552        0.9374306237042292,
1553        0.9835909068631806,
1554        0.9882631495511387,
1555        0.8626900372898038,
1556        0.9529699167079625,
1557        0.9908436728118781,
1558        0.8695666651620589,
1559        0.9980522765504592,
1560        0.9480112199625286,
1561    ];
1562
1563    // case f: n=45 period=6 outlier {'seasonal': 35}
1564    const Y_F: [f64; 45] = [
1565        1.056, 3.197, 3.911, -0.245, -4.601, -2.297, 1.244, 1.754, 2.532, 0.779, -3.543, -1.519,
1566        1.139, 3.565, 3.235, 25.844, -2.516, -2.279, 1.154, 4.023, 4.291, 1.902, -2.419, -3.06,
1567        1.374, 4.344, 4.052, 2.651, -1.69, -1.157, -15.845, 3.859, 4.079, 0.858, -1.596, -0.349,
1568        1.416, 4.74, 4.014, 1.605, 0.355, -0.696, 3.752, 4.817, 4.537,
1569    ];
1570    const T_F: [f64; 45] = [
1571        0.5612109998433703,
1572        0.40904308280921975,
1573        0.3063556110383589,
1574        0.22385919710585547,
1575        0.09288356206653685,
1576        -0.06107135622732654,
1577        -0.10624822414054202,
1578        0.014656325116133434,
1579        0.08007451660115499,
1580        0.17480881836228845,
1581        0.34546558881282635,
1582        1.0492469568824512,
1583        2.732084319389148,
1584        4.266955192867556,
1585        4.889599645363789,
1586        4.967180414397434,
1587        4.925277894417086,
1588        4.40743780439494,
1589        3.0939602933044923,
1590        1.6068942617149302,
1591        1.0262740696598924,
1592        0.9947585154941324,
1593        1.0073511748106747,
1594        1.0672624282725802,
1595        1.1447399240254628,
1596        1.2797580965614856,
1597        1.0521677462295975,
1598        0.08610739958879997,
1599        -0.9072612628388329,
1600        -1.3994989371162496,
1601        -1.5683716226276025,
1602        -1.589653319134234,
1603        -1.164231654289145,
1604        -0.0933757185688997,
1605        1.0720090219362588,
1606        1.534751267723855,
1607        1.6603674495583198,
1608        1.8085935594848528,
1609        1.910476451507037,
1610        2.0833589830238215,
1611        2.3070527257477664,
1612        2.4249224569681234,
1613        2.586710395846874,
1614        2.7919327883617777,
1615        2.9968909089345734,
1616    ];
1617    const S_F: [f64; 45] = [
1618        -0.08345334568748934,
1619        1.6366687579701773,
1620        2.2622933452398972,
1621        5.431115109096656,
1622        -5.3922930507010385,
1623        -3.505289669825159,
1624        -0.6191111992026053,
1625        1.8810402778161728,
1626        2.3220436132227444,
1627        4.834300782291442,
1628        -4.774952916421866,
1629        -3.3041102488029876,
1630        -1.1313723504797544,
1631        2.122313273146454,
1632        2.378702967592607,
1633        4.219940286112908,
1634        -4.157014623089618,
1635        -3.102247585149322,
1636        -1.623120106864433,
1637        2.3587764651119216,
1638        2.4301355561852147,
1639        3.5983008476292553,
1640        -3.5404359142344077,
1641        -2.902168758924804,
1642        -2.0928761240257416,
1643        2.5935060023538785,
1644        2.480730507566989,
1645        2.9521352217578634,
1646        -2.9210860698571364,
1647        -2.699118331152398,
1648        -2.5475910193111697,
1649        2.830743247941963,
1650        2.534882383818799,
1651        2.2798838568181043,
1652        -2.2968557478495795,
1653        -2.4888933119395698,
1654        -2.997565358026707,
1655        3.073266962087194,
1656        2.5949563104032625,
1657        1.5759042735441489,
1658        -1.667681434069707,
1659        -2.270656579310069,
1660        -3.434954163903007,
1661        3.3192723414156355,
1662        2.6589654682730384,
1663    ];
1664    const W_F: [f64; 45] = [
1665        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1666        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1667        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1668    ];
1669
1670    fn check(y: &[f64], t: &[f64], s: &[f64], w: &[f64], stl: Stl) {
1671        let r = stl.decompose(y).unwrap();
1672        let scale = y.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
1673        let d = |a: &[f64], b: &[f64]| {
1674            a.iter()
1675                .zip(b)
1676                .fold(0.0_f64, |m, (x, y)| m.max((x - y).abs()))
1677        };
1678        let (dt, ds, dw) = (d(&r.trend, t), d(&r.seasonal, s), d(&r.weights, w));
1679        assert!(dt <= 1e-10 * scale && ds <= 1e-10 * scale && dw <= 1e-10);
1680    }
1681
1682    #[test]
1683    fn matches_statsmodels() {
1684        check(&Y_A, &T_A, &S_A, &W_A, Stl::new(12));
1685    }
1686    #[test]
1687    fn matches_statsmodels_b() {
1688        check(&Y_B, &T_B, &S_B, &W_B, Stl::new(7).robust(true));
1689        check(
1690            &Y_C,
1691            &T_C,
1692            &S_C,
1693            &W_C,
1694            Stl::new(5).seasonal(11).trend(17).low_pass(7),
1695        );
1696    }
1697    #[test]
1698    fn matches_statsmodels_d() {
1699        check(
1700            &Y_D,
1701            &T_D,
1702            &S_D,
1703            &W_D,
1704            Stl::new(4)
1705                .seasonal_degree(0)
1706                .trend_degree(0)
1707                .low_pass_degree(0),
1708        );
1709    }
1710    #[test]
1711    fn matches_statsmodels_e() {
1712        check(&Y_E, &T_E, &S_E, &W_E, Stl::new(6).robust(true).seasonal(9));
1713    }
1714    #[test]
1715    fn matches_statsmodels_f() {
1716        check(&Y_F, &T_F, &S_F, &W_F, Stl::new(6).seasonal(35));
1717    }
1718}