From be2d91721323388b25252e20412c0ecec25d9bc3 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Wed, 14 Feb 2024 16:07:00 +0000 Subject: [PATCH 1/5] Add seasonality detection using periodograms, and Python/JS bindings --- Cargo.toml | 1 + README.md | 2 + crates/augurs-js/Cargo.toml | 1 + crates/augurs-js/src/lib.rs | 1 + crates/augurs-js/src/seasons.rs | 11 + crates/augurs-seasons/Cargo.toml | 26 + crates/augurs-seasons/README.md | 39 + crates/augurs-seasons/benches/periodogram.rs | 24 + crates/augurs-seasons/src/lib.rs | 21 + crates/augurs-seasons/src/periodogram.rs | 252 +++ crates/augurs-seasons/src/test_data.rs | 17 + crates/augurs-testing/src/data.rs | 1450 ++++++++++++++++++ crates/pyaugurs/Cargo.toml | 1 + crates/pyaugurs/augurs.pyi | 2 + crates/pyaugurs/src/lib.rs | 2 + crates/pyaugurs/src/seasons.rs | 20 + 16 files changed, 1870 insertions(+) create mode 100644 crates/augurs-js/src/seasons.rs create mode 100644 crates/augurs-seasons/Cargo.toml create mode 100644 crates/augurs-seasons/README.md create mode 100644 crates/augurs-seasons/benches/periodogram.rs create mode 100644 crates/augurs-seasons/src/lib.rs create mode 100644 crates/augurs-seasons/src/periodogram.rs create mode 100644 crates/augurs-seasons/src/test_data.rs create mode 100644 crates/pyaugurs/src/seasons.rs diff --git a/Cargo.toml b/Cargo.toml index 136fddf..695892b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ keywords = [ augurs-core = { version = "0.1.0-alpha.0", path = "crates/augurs-core" } augurs-ets = { version = "0.1.0-alpha.0", path = "crates/augurs-ets" } augurs-mstl = { version = "0.1.0-alpha.0", path = "crates/augurs-mstl" } +augurs-seasons = { version = "0.1.0-alpha.0", path = "crates/augurs-seasons" } augurs-testing = { version = "0.1.0-alpha.0", path = "crates/augurs-testing" } distrs = "0.2.1" diff --git a/README.md b/README.md index 6f6fdec..be231ba 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ APIs are subject to change, and functionality may not be fully implemented. | [`augurs-core`][] | Common structs and traits | alpha - API is flexible right now | | [`augurs-ets`][] | Automatic exponential smoothing models | alpha - non-seasonal models working and tested against statsforecast | | [`augurs-mstl`][] | Multiple Seasonal Trend Decomposition using LOESS (MSTL) | beta - working and tested against R | +| [`augurs-seasons`][] | Seasonality detection using periodograms | alpha - working and tested against Python in limited scenarios | | [`augurs-testing`][] | Testing data and, eventually, evaluation harness for implementations | alpha - just data right now | | [`augurs-js`][] | WASM bindings to augurs | alpha - untested, should work though | | [`pyaugurs`][] | Python bindings to augurs | alpha - untested, should work though | @@ -40,5 +41,6 @@ Licensed under the Apache License, Version 2.0 ` Vec { + PeriodogramDetector::builder().build(y).detect().collect() +} diff --git a/crates/augurs-seasons/Cargo.toml b/crates/augurs-seasons/Cargo.toml new file mode 100644 index 0000000..ca6eed1 --- /dev/null +++ b/crates/augurs-seasons/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "augurs-seasons" +version.workspace = true +authors.workspace = true +documentation.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +keywords.workspace = true +description = "Seasonality detection using periodograms" + +[dependencies] +itertools.workspace = true +num-traits = "0.2.18" +thiserror.workspace = true +tracing.workspace = true +welch-sde = "0.1.0" + +[dev-dependencies] +augurs-testing.workspace = true +criterion.workspace = true +pprof.workspace = true + +[[bench]] +name = "periodogram" +harness = false diff --git a/crates/augurs-seasons/README.md b/crates/augurs-seasons/README.md new file mode 100644 index 0000000..5fe2e0f --- /dev/null +++ b/crates/augurs-seasons/README.md @@ -0,0 +1,39 @@ +# Seasonality detection for time series + +`augurs-seasons` contains methods for detecting seasonality or periodicity in time series. + +It currently contains implementations to do so using periodograms, similar to the [`seasonal`] Python package. + +## Usage + +```rust +use augurs_seasons::{Detector, PeriodogramDetector}; + +# fn main() { +let y = &[ + 0.1, 0.3, 0.8, 0.5, + 0.1, 0.31, 0.79, 0.48, + 0.09, 0.29, 0.81, 0.49, + 0.11, 0.28, 0.78, 0.53, + 0.1, 0.3, 0.8, 0.5, + 0.1, 0.31, 0.79, 0.48, + 0.09, 0.29, 0.81, 0.49, + 0.11, 0.28, 0.78, 0.53, +]; +let periods: Vec<_> = PeriodogramDetector::builder().build(y).detect().collect(); +assert_eq!(periods[0], 4); +# } +``` + +## Credits + +This implementation is based heavily on the [`seasonal`] Python package. +It also makes heavy use of the [`welch-sde`] crate. + +[`seasonal`]: https://github.com/welch/seasonal +[`welch-sde`]: https://crates.io/crates/welch-sde + +## License + +Dual-licensed to be compatible with the Rust project. +Licensed under the Apache License, Version 2.0 `` or the MIT license ``, at your option. diff --git a/crates/augurs-seasons/benches/periodogram.rs b/crates/augurs-seasons/benches/periodogram.rs new file mode 100644 index 0000000..5a7a6f1 --- /dev/null +++ b/crates/augurs-seasons/benches/periodogram.rs @@ -0,0 +1,24 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use pprof::criterion::{Output, PProfProfiler}; + +use augurs_seasons::{Detector, PeriodogramDetector}; +use augurs_testing::data::SEASON_EIGHT; + +fn season_eight(c: &mut Criterion) { + let y = SEASON_EIGHT; + c.bench_function("season_eight", |b| { + b.iter(|| { + PeriodogramDetector::builder() + .build(y) + .detect() + .collect::>() + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Protobuf)); + targets = season_eight +} +criterion_main!(benches); diff --git a/crates/augurs-seasons/src/lib.rs b/crates/augurs-seasons/src/lib.rs new file mode 100644 index 0000000..3b4cdbc --- /dev/null +++ b/crates/augurs-seasons/src/lib.rs @@ -0,0 +1,21 @@ +#![doc = include_str!("../README.md")] +#![warn( + missing_docs, + missing_debug_implementations, + rust_2018_idioms, + unreachable_pub +)] + +mod periodogram; +#[cfg(test)] +mod test_data; + +pub use periodogram::{ + Builder as PeriodogramDetectorBuilder, Detector as PeriodogramDetector, Periodogram, +}; + +/// A detector of periodic signals in a time series. +pub trait Detector { + /// Detects the periods of the time series. + fn detect(&self) -> impl Iterator; +} diff --git a/crates/augurs-seasons/src/periodogram.rs b/crates/augurs-seasons/src/periodogram.rs new file mode 100644 index 0000000..43f66e3 --- /dev/null +++ b/crates/augurs-seasons/src/periodogram.rs @@ -0,0 +1,252 @@ +use std::cmp::Ordering; + +use itertools::Itertools; +use welch_sde::{Build, SpectralDensity}; + +// Default number of cycles of data assumed when establishing FFT window sizes. +const DEFAULT_MIN_FFT_CYCLES: f64 = 3.0; + +/// Default maximum period assumed when establishing FFT window sizes. +const DEFAULT_MAX_FFT_PERIOD: f64 = 512.0; + +/// A builder for a periodogram detector. +#[derive(Debug, Clone)] +pub struct Builder { + min_period: usize, + max_period: Option, + threshold: f64, +} + +impl Default for Builder { + fn default() -> Self { + Self { + min_period: 4, + max_period: None, + threshold: 0.9, + } + } +} + +impl Builder { + /// Set the minimum period to consider when detecting seasonal periods. + /// + /// The default is 4. + #[must_use] + pub fn min_period(mut self, min_period: usize) -> Self { + self.min_period = min_period; + self + } + + /// Set the maximum period to consider when detecting seasonal periods. + /// + /// The default is the length of the data divided by 3, or 512, whichever is smaller. + #[must_use] + pub fn max_period(mut self, max_period: usize) -> Self { + self.max_period = Some(max_period); + self + } + + /// Set the threshold for detecting peaks in the periodogram. + /// + /// The value will be clamped to the range 0.01 to 0.99. + /// + /// The default is 0.9. + #[must_use] + pub fn threshold(mut self, threshold: f64) -> Self { + self.threshold = threshold.clamp(0.01, 0.99); + self + } + + /// Build the periodogram detector. + /// + /// The data is the time series to detect seasonal periods in. + #[must_use] + pub fn build(self, data: &[f64]) -> Detector<'_> { + Detector { + data, + min_period: self.min_period, + max_period: self.max_period.unwrap_or_else(|| default_max_period(data)), + threshold: self.threshold, + } + } +} + +fn default_max_period(data: &[f64]) -> usize { + (data.len() as f64 / DEFAULT_MIN_FFT_CYCLES).min(DEFAULT_MAX_FFT_PERIOD) as usize +} + +/// A periodogram of a time series. +#[derive(Debug, Clone, PartialEq)] +pub struct Periodogram { + /// The periods of the periodogram. + pub periods: Vec, + /// The powers of the periodogram. + pub powers: Vec, +} + +impl Periodogram { + /// Find the peaks in the periodogram. + /// + /// The peaks are defined as the periods which have a power greater than `threshold` times the + /// maximum power in the periodogram. + pub fn peaks(&self, threshold: f64) -> impl Iterator { + // Scale the threshold so that it's relative to the maximum power. + let keep = self + .powers + .iter() + .copied() + .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)) + .unwrap_or(1.0) + * threshold; + + // We're going to window by 3 and zip this up with the powers, but we want the + // middle element to represent the periodogram value we're looking at, so + // we need to prepend and append a 0 to the periods. + std::iter::once(0) + .chain(self.periods.iter().copied()) + .chain(std::iter::once(0)) + .tuple_windows() + .zip(self.powers.iter().copied()) + .filter_map(|((prev_period, period, next_period), power)| { + (power >= keep).then_some(Period { + power, + period, + prev_period, + next_period, + }) + }) + .sorted_by(|a, b| a.power.partial_cmp(&b.power).unwrap_or(Ordering::Equal)) + } +} + +/// A peak in the periodogram. +#[derive(Debug, Clone, PartialEq)] +pub struct Period { + /// The power of the peak. + pub power: f64, + /// The period of the peak. + pub period: usize, + /// The previous period in the periodogram. + pub prev_period: usize, + /// The next period in the periodogram. + pub next_period: usize, +} + +/// A season detector which uses a periodogram to identify seasonal periods. +/// +/// The detector works by calculating a robust periodogram of the data using +/// Welch's method. The peaks in the periodogram represent likely seasonal periods +/// in the data. +#[derive(Debug)] +pub struct Detector<'a> { + data: &'a [f64], + min_period: usize, + max_period: usize, + threshold: f64, +} + +impl<'a> Detector<'a> { + /// Create a new detector builder. + #[must_use] + pub fn builder() -> Builder { + Builder::default() + } + + /// Calculate the periodogram of the data. + /// + /// The periodogram is a frequency domain representation of the data, and is calculated using the + /// Welch method. + /// + /// The periodogram can then be used to identify peaks, which are returned as periods which + /// correspond to likely seasonal periods in the data. + #[must_use] + pub fn periodogram(&self) -> Periodogram { + let frequency = 1.0; + let data_len = self.data.len(); + let n_per_segment = (self.max_period * 2).min(data_len / 2); + let max_fft_size = (n_per_segment as f64).log2().floor() as usize; + let n_segments = (data_len as f64 / n_per_segment as f64).ceil() as usize; + + let welch: SpectralDensity<'_, f64> = SpectralDensity::builder(self.data, frequency) + .n_segment(n_segments) + .dft_log2_max_size(max_fft_size) + .build(); + let sd = welch.periodogram(); + + let freqs = sd.frequency(); + // Periods are the reciprocal of the frequency, since we've used a frequency of 1. + // Make sure we skip the first one, which is 0, and the first power, which corresponds to + // that. + let periods = freqs.iter().skip(1).map(|x| x.recip().round() as usize); + let power = sd.iter().skip(1).copied(); + + let (periods, powers) = periods + .zip(power) + .filter(|(per, _)| { + // Filter out periods that are too short or too long, and the period corresponding to the + // segment length. + *per >= self.min_period && *per < self.max_period && *per != n_per_segment + }) + // Group by period, and keep the maximum power for each period. + .group_by(|(per, _)| *per) + .into_iter() + .map(|(per, group)| { + let max_power = group + .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Equal)) + .unwrap_or((0, 0.0)); + (per, max_power.1) + }) + .unzip(); + Periodogram { periods, powers } + } +} + +impl<'a> crate::Detector for Detector<'a> { + fn detect(&self) -> impl Iterator { + self.periodogram().peaks(self.threshold).map(|x| x.period) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{test_data::*, Detector as _}; + + #[test] + fn smoke() { + #[rustfmt::skip] + let y = &[ + 0.1, 0.3, 0.8, 0.5, + 0.1, 0.31, 0.79, 0.48, + 0.09, 0.29, 0.81, 0.49, + 0.11, 0.28, 0.78, 0.53, + 0.1, 0.3, 0.8, 0.5, + 0.1, 0.31, 0.79, 0.48, + 0.09, 0.29, 0.81, 0.49, + 0.11, 0.28, 0.78, 0.53, + ]; + let periods = Detector::builder().build(y).detect().collect::>(); + assert_eq!(periods[0], 4); + } + + #[test] + fn test_detect() { + for (i, test_case) in CASES.iter().enumerate() { + let TestCase { + data, + season_lengths: expected, + } = test_case; + let detector = Detector::builder().build(data); + assert_eq!( + detector + .periodogram() + .peaks(0.5) + .map(|x| x.period) + .collect_vec(), + *expected, + "Test case {}", + i + ); + } + } +} diff --git a/crates/augurs-seasons/src/test_data.rs b/crates/augurs-seasons/src/test_data.rs new file mode 100644 index 0000000..a1f198a --- /dev/null +++ b/crates/augurs-seasons/src/test_data.rs @@ -0,0 +1,17 @@ +use augurs_testing::data::{SEASON_EIGHT, SEASON_SEVEN}; + +pub(crate) struct TestCase { + pub(crate) season_lengths: &'static [usize], + pub(crate) data: &'static [f64], +} + +pub(crate) static CASES: &[TestCase] = &[ + TestCase { + season_lengths: &[8], + data: SEASON_EIGHT, + }, + TestCase { + season_lengths: &[7], + data: SEASON_SEVEN, + }, +]; diff --git a/crates/augurs-testing/src/data.rs b/crates/augurs-testing/src/data.rs index c7b4fa4..95c123e 100644 --- a/crates/augurs-testing/src/data.rs +++ b/crates/augurs-testing/src/data.rs @@ -43,3 +43,1453 @@ pub static AUSTRES: &[f64] = &[ 16697.0, 16777.2, 16833.1, 16891.6, 16956.8, 17026.3, 17085.4, 17106.9, 17169.4, 17239.4, 17292.0, 17354.2, 17414.2, 17447.3, 17482.6, 17526.0, 17568.7, 17627.1, 17661.5, ]; + +/// A trendless, seasonal time series with season length 7. +pub static SEASON_SEVEN: &[f64] = &[ + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526266544243022, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526463530349485, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526340412607388, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.01052624192136851, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526192678988796, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526229611227154, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526340412607388, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.0035087965529582666, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526266543897444, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526266543897444, + 0.010526365035741093, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.01052641428269969, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526291166685565, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.01052624192136851, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.00701739612499386, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526365035741093, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.01052641428269969, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526291166685565, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526389659133991, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526389659133991, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017617729309431, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526389659133991, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017469991543949, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526266543897444, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.0035087965529582666, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526389659133991, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.007017543859649122, + 0.0, + 0.007017543859649122, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.010526315789473682, + 0.003508771929824561, + 0.0, + 0.003508771929824561, + 0.010526315789473682, +]; + +/// A trendless, seasonal time series with season length 8. +pub static SEASON_EIGHT: &[f64] = &[ + 0.4070175438596491, + 0.39999999999999997, + 0.39999999999999997, + 0.4245614035087719, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4140350877192982, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4175438596491228, + 0.39999999999999997, + 0.39999999999999997, + 0.41052631578947363, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4245614035087719, + 0.39999999999999997, + 0.39999999999999997, + 0.40701754385964906, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.431578947368421, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.4350877192982456, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4385964912280701, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4350877192982456, + 0.4070175438596491, + 0.39999999999999997, + 0.39999999999999997, + 0.4350877192982456, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4350877192982456, + 0.40350662975477447, + 0.39999999999999997, + 0.39999999999999997, + 0.4245614035087719, + 0.44210526315789467, + 0.44210526315789467, + 0.44212465544068963, + 0.44210526315789467, + 0.40701754385964906, + 0.39999999999999997, + 0.39999999999999997, + 0.4175409049352831, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.41052631578947363, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.40350877192982454, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4245614035087719, + 0.39999999999999997, + 0.39999999999999997, + 0.40350877192982454, + 0.4350877192982455, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.43157894736842106, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4350877192982455, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.43859649122807015, + 0.40350877192982454, + 0.3999964912896265, + 0.39999999999999997, + 0.4245614035087719, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4070175438596491, + 0.39999999999999997, + 0.39999999999999997, + 0.42105263157894735, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4175438596491228, + 0.39999999999999997, + 0.39999999999999997, + 0.40701754385964906, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.41052631578947363, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4245614035087719, + 0.39999999999999997, + 0.39999999999999997, + 0.40350877192982454, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.43158123732789067, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4350877192982455, + 0.40351087721514334, + 0.4000014035186212, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.41052631578947363, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4175438596491228, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.41052631578947363, + 0.43859649122807015, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4280670237438342, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.41052631578947363, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4175438596491228, + 0.39999999999999997, + 0.39999999999999997, + 0.40701754385964906, + 0.44210526315789467, + 0.44210526315789467, + 0.4421100647363582, + 0.44210526315789467, + 0.4245614035087719, + 0.39999999999999997, + 0.39999999999999997, + 0.40350877192982454, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42807017543859643, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4350877192982456, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.4280701754385965, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.40701754385964906, + 0.39999999999999997, + 0.40000631598892594, + 0.4280701754385965, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4385964912280701, + 0.40701754385964906, + 0.39999999999999997, + 0.39999999999999997, + 0.42105263157894735, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4140350877192982, + 0.39999999999999997, + 0.39999999999999997, + 0.4140379932694654, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4140350877192982, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210376116781946, + 0.44210526315789467, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.44210526315789467, + 0.4421029363125817, + 0.44210526315789467, + 0.44210526315789467, + 0.4280701754385965, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.4350846168487139, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.43157894736842106, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.4421015082222319, + 0.44210526315789467, + 0.44210526315789467, + 0.43859649122807015, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.42105263157894735, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4070175438596491, + 0.39999999999999997, + 0.39999999999999997, + 0.42105263157894735, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.41052631578947363, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4421091289979215, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.41052631578947363, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4245614035087719, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210368729945504, + 0.44210526315789467, + 0.44210526315789467, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.40701754385964906, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42807017543859643, + 0.39999999999999997, + 0.39999999999999997, + 0.40350877192982454, + 0.4350877192982456, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42807017543859643, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4280701754385965, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4350877192982455, + 0.4070175438596491, + 0.39999999999999997, + 0.39999999999999997, + 0.42105263157894735, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4140350877192982, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4140350877192982, + 0.39999999999999997, + 0.39999999999999997, + 0.41052631578947363, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4245614035087719, + 0.39999999999999997, + 0.39999999999999997, + 0.40701754385964906, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.42807017543859643, + 0.39999999999999997, + 0.39999999999999997, + 0.41052631578947363, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.431578947368421, + 0.39999999999999997, + 0.39999999999999997, + 0.40350877192982454, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.431578947368421, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4350877192982455, + 0.45010666666666665, + 0.4508029090909091, + 0.4522948571428572, + 0.44898340740740744, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.43859649122807015, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4070175438596491, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4175438596491228, + 0.41811133333333333, + 0.41811133333333333, + 0.4266148888888889, + 0.45617110000000005, + 0.441192787037037, + 0.43245774786324787, + 0.4318670906862745, + 0.42105263157894735, + 0.39999999999999997, + 0.39999999999999997, + 0.40701754385964906, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4280701754385965, + 0.39999999999999997, + 0.3999971930218523, + 0.39999999999999997, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44211006473635817, + 0.4280701754385965, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4385964912280701, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4315774207555576, + 0.44210526315789467, + 0.44210526315789467, + 0.4421082672012879, + 0.4385964912280701, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4105249122905509, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4210489997559292, + 0.39999999999999997, + 0.39999999999999997, + 0.40701754385964906, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4280701754385965, + 0.39999999999999997, + 0.39999999999999997, + 0.4000014035186211, + 0.4385964912280701, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4280701754385965, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.4350877192982456, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4385964912280701, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.42105263157894735, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.41052631578947363, + 0.39999999999999997, + 0.39999999999999997, + 0.41403656510732045, + 0.44210526315789467, + 0.44211235479458344, + 0.44210526315789467, + 0.44210526315789467, + 0.4245614035087719, + 0.39999999999999997, + 0.39999508783994775, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4421068390384518, + 0.4245614035087719, + 0.39999999999999997, + 0.4068306052757674, + 0.40683058481247536, + 0.4447592183207115, + 0.4418504846043413, + 0.4249532996632996, + 0.43239620432457737, + 0.4280671222342955, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.431578947368421, + 0.4421052631578947, + 0.4421083164479009, + 0.4421052631578947, + 0.43508993538805485, + 0.39999999999999997, + 0.3999978947590025, + 0.39999999999999997, + 0.431578947368421, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4350877192982456, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210304711473947, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.41052631578947363, + 0.39999999999999997, + 0.39999999999999997, + 0.4140350877192982, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.41403508771929826, + 0.39999999999999997, + 0.39999999999999997, + 0.41403508771929826, + 0.4421052631578947, + 0.4421052631578947, + 0.4421052631578947, + 0.4421052631578947, + 0.42456140350877186, + 0.39999999999999997, + 0.39999999999999997, + 0.4105263157894737, + 0.43859649122807015, + 0.4421052631578947, + 0.44210762698702466, + 0.4421052631578947, + 0.4245614035087719, + 0.39999999999999997, + 0.39999999999999997, + 0.4070175438596491, + 0.4385964912280701, + 0.4421000924631029, + 0.44210526315789467, + 0.44210526315789467, + 0.4280701754385965, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.4385948907468424, + 0.44210526315789467, + 0.44210526315789467, + 0.44210378579060755, + 0.4280701754385965, + 0.39999999999999997, + 0.4000014035186212, + 0.39999999999999997, + 0.43508624193095846, + 0.44210526315789467, + 0.4421028624444765, + 0.44210526315789467, + 0.4350877192982456, + 0.39999087760909147, + 0.39999999999999997, + 0.39999999999999997, + 0.43157894736842106, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.44210526315789467, + 0.4070175438596491, + 0.39999999999999997, + 0.39999578956232495, + 0.42456140350877186, + 0.4421052631578947, + 0.4421052631578947, + 0.4421083164479009, + 0.4421052631578947, + 0.40350877192982454, + 0.39999999999999997, + 0.39999999999999997, + 0.4175438596491228, + 0.44210526315789467, + 0.4421109512015715, + 0.44210526315789467, + 0.44210526315789467, + 0.4140350877192982, + 0.39999999999999997, + 0.40000280705694113, + 0.4105263157894737, + 0.4421052631578947, + 0.44210755311736427, + 0.4421022099535938, + 0.4421052631578947, + 0.42456140350877186, + 0.39999999999999997, + 0.39999999999999997, + 0.40350877192982454, + 0.43859649122807015, + 0.4421052631578947, + 0.4421052631578947, + 0.4421052631578947, + 0.4245614035087719, + 0.39999999999999997, + 0.3999964912896265, + 0.40350877192982454, + 0.4350877192982456, + 0.4421052631578947, + 0.4421052631578947, + 0.4421052631578947, + 0.431578947368421, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, + 0.4350862173081703, + 0.4421052631578947, + 0.4421052631578947, + 0.4421052631578947, + 0.41726220532813507, + 0.39507753968253967, + 0.39160266666666665, + 0.39160266666666665, + 0.39160266666666665, + 0.4364861481481481, + 0.4376113821041213, + 0.43611022727272725, + 0.4353893101851851, + 0.4280701754385965, + 0.39999999999999997, + 0.39999999999999997, + 0.39999999999999997, +]; diff --git a/crates/pyaugurs/Cargo.toml b/crates/pyaugurs/Cargo.toml index fbddf0d..9514125 100644 --- a/crates/pyaugurs/Cargo.toml +++ b/crates/pyaugurs/Cargo.toml @@ -18,6 +18,7 @@ crate-type = ["cdylib"] augurs-core.workspace = true augurs-ets = { workspace = true, features = ["mstl"] } augurs-mstl.workspace = true +augurs-seasons.workspace = true numpy = "0.20.0" pyo3 = { version = "0.20.0", features = ["extension-module"] } pyo3-log = "0.9.0" diff --git a/crates/pyaugurs/augurs.pyi b/crates/pyaugurs/augurs.pyi index 4eb2baf..c021ac0 100644 --- a/crates/pyaugurs/augurs.pyi +++ b/crates/pyaugurs/augurs.pyi @@ -68,3 +68,5 @@ class AutoETS: def fit(self, y: npt.NDArray[np.float64]) -> None: ... def predict(self, horizon: int, level: float | None) -> Forecast: ... def predict_in_sample(self, level: float | None) -> Forecast: ... + +def seasonalities(y: npt.NDArray[np.float64]) -> npt.NDArray[np.uint64]: ... diff --git a/crates/pyaugurs/src/lib.rs b/crates/pyaugurs/src/lib.rs index 16e1ab0..aff37b0 100644 --- a/crates/pyaugurs/src/lib.rs +++ b/crates/pyaugurs/src/lib.rs @@ -16,6 +16,7 @@ use pyo3::prelude::*; pub mod ets; pub mod mstl; +pub mod seasons; pub mod trend; /// Forecasts produced by augurs models. @@ -110,5 +111,6 @@ fn augurs(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_function(wrap_pyfunction!(seasons::seasonalities, m)?)?; Ok(()) } diff --git a/crates/pyaugurs/src/seasons.rs b/crates/pyaugurs/src/seasons.rs new file mode 100644 index 0000000..b23d0b3 --- /dev/null +++ b/crates/pyaugurs/src/seasons.rs @@ -0,0 +1,20 @@ +//! Bindings for seasonality detection. + +use numpy::{PyArray1, PyReadonlyArray1, ToPyArray}; +use pyo3::prelude::*; + +use augurs_seasons::{Detector, PeriodogramDetector}; + +/// Detect the seasonal periods in a time series. +#[pyfunction] +pub fn seasonalities( + py: Python<'_>, + y: PyReadonlyArray1<'_, f64>, +) -> PyResult>> { + Ok(PeriodogramDetector::builder() + .build(y.as_slice()?) + .detect() + .collect::>() + .to_pyarray(py) + .into()) +} From 74791f0f922e715f82e5f74bafc29fd9f30bbaa3 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Wed, 14 Feb 2024 17:01:43 +0000 Subject: [PATCH 2/5] Use u32 instead of usize; return Vec instead of impl Iterator for lower MSRV --- crates/augurs-js/src/seasons.rs | 4 +-- crates/augurs-seasons/README.md | 2 +- crates/augurs-seasons/benches/periodogram.rs | 7 +--- crates/augurs-seasons/src/lib.rs | 2 +- crates/augurs-seasons/src/periodogram.rs | 37 +++++++++++--------- crates/augurs-seasons/src/test_data.rs | 2 +- crates/pyaugurs/src/seasons.rs | 6 +--- 7 files changed, 27 insertions(+), 33 deletions(-) diff --git a/crates/augurs-js/src/seasons.rs b/crates/augurs-js/src/seasons.rs index 67bb3ff..efcfc08 100644 --- a/crates/augurs-js/src/seasons.rs +++ b/crates/augurs-js/src/seasons.rs @@ -6,6 +6,6 @@ use augurs_seasons::{Detector, PeriodogramDetector}; /// Detect the seasonal periods in a time series. #[wasm_bindgen] -pub fn seasonalities(y: &[f64]) -> Vec { - PeriodogramDetector::builder().build(y).detect().collect() +pub fn seasonalities(y: &[f64]) -> Vec { + PeriodogramDetector::builder().build(y).detect() } diff --git a/crates/augurs-seasons/README.md b/crates/augurs-seasons/README.md index 5fe2e0f..91d2833 100644 --- a/crates/augurs-seasons/README.md +++ b/crates/augurs-seasons/README.md @@ -20,7 +20,7 @@ let y = &[ 0.09, 0.29, 0.81, 0.49, 0.11, 0.28, 0.78, 0.53, ]; -let periods: Vec<_> = PeriodogramDetector::builder().build(y).detect().collect(); +let periods = PeriodogramDetector::builder().build(y).detect(); assert_eq!(periods[0], 4); # } ``` diff --git a/crates/augurs-seasons/benches/periodogram.rs b/crates/augurs-seasons/benches/periodogram.rs index 5a7a6f1..bcf74b7 100644 --- a/crates/augurs-seasons/benches/periodogram.rs +++ b/crates/augurs-seasons/benches/periodogram.rs @@ -7,12 +7,7 @@ use augurs_testing::data::SEASON_EIGHT; fn season_eight(c: &mut Criterion) { let y = SEASON_EIGHT; c.bench_function("season_eight", |b| { - b.iter(|| { - PeriodogramDetector::builder() - .build(y) - .detect() - .collect::>() - }); + b.iter(|| PeriodogramDetector::builder().build(y).detect()); }); } diff --git a/crates/augurs-seasons/src/lib.rs b/crates/augurs-seasons/src/lib.rs index 3b4cdbc..00e9787 100644 --- a/crates/augurs-seasons/src/lib.rs +++ b/crates/augurs-seasons/src/lib.rs @@ -17,5 +17,5 @@ pub use periodogram::{ /// A detector of periodic signals in a time series. pub trait Detector { /// Detects the periods of the time series. - fn detect(&self) -> impl Iterator; + fn detect(&self) -> Vec; } diff --git a/crates/augurs-seasons/src/periodogram.rs b/crates/augurs-seasons/src/periodogram.rs index 43f66e3..6404114 100644 --- a/crates/augurs-seasons/src/periodogram.rs +++ b/crates/augurs-seasons/src/periodogram.rs @@ -12,8 +12,8 @@ const DEFAULT_MAX_FFT_PERIOD: f64 = 512.0; /// A builder for a periodogram detector. #[derive(Debug, Clone)] pub struct Builder { - min_period: usize, - max_period: Option, + min_period: u32, + max_period: Option, threshold: f64, } @@ -32,7 +32,7 @@ impl Builder { /// /// The default is 4. #[must_use] - pub fn min_period(mut self, min_period: usize) -> Self { + pub fn min_period(mut self, min_period: u32) -> Self { self.min_period = min_period; self } @@ -41,7 +41,7 @@ impl Builder { /// /// The default is the length of the data divided by 3, or 512, whichever is smaller. #[must_use] - pub fn max_period(mut self, max_period: usize) -> Self { + pub fn max_period(mut self, max_period: u32) -> Self { self.max_period = Some(max_period); self } @@ -71,15 +71,15 @@ impl Builder { } } -fn default_max_period(data: &[f64]) -> usize { - (data.len() as f64 / DEFAULT_MIN_FFT_CYCLES).min(DEFAULT_MAX_FFT_PERIOD) as usize +fn default_max_period(data: &[f64]) -> u32 { + (data.len() as f64 / DEFAULT_MIN_FFT_CYCLES).min(DEFAULT_MAX_FFT_PERIOD) as u32 } /// A periodogram of a time series. #[derive(Debug, Clone, PartialEq)] pub struct Periodogram { /// The periods of the periodogram. - pub periods: Vec, + pub periods: Vec, /// The powers of the periodogram. pub powers: Vec, } @@ -125,11 +125,11 @@ pub struct Period { /// The power of the peak. pub power: f64, /// The period of the peak. - pub period: usize, + pub period: u32, /// The previous period in the periodogram. - pub prev_period: usize, + pub prev_period: u32, /// The next period in the periodogram. - pub next_period: usize, + pub next_period: u32, } /// A season detector which uses a periodogram to identify seasonal periods. @@ -140,8 +140,8 @@ pub struct Period { #[derive(Debug)] pub struct Detector<'a> { data: &'a [f64], - min_period: usize, - max_period: usize, + min_period: u32, + max_period: u32, threshold: f64, } @@ -163,7 +163,7 @@ impl<'a> Detector<'a> { pub fn periodogram(&self) -> Periodogram { let frequency = 1.0; let data_len = self.data.len(); - let n_per_segment = (self.max_period * 2).min(data_len / 2); + let n_per_segment = (self.max_period * 2).min(data_len as u32 / 2); let max_fft_size = (n_per_segment as f64).log2().floor() as usize; let n_segments = (data_len as f64 / n_per_segment as f64).ceil() as usize; @@ -177,7 +177,7 @@ impl<'a> Detector<'a> { // Periods are the reciprocal of the frequency, since we've used a frequency of 1. // Make sure we skip the first one, which is 0, and the first power, which corresponds to // that. - let periods = freqs.iter().skip(1).map(|x| x.recip().round() as usize); + let periods = freqs.iter().skip(1).map(|x| x.recip().round() as u32); let power = sd.iter().skip(1).copied(); let (periods, powers) = periods @@ -202,8 +202,11 @@ impl<'a> Detector<'a> { } impl<'a> crate::Detector for Detector<'a> { - fn detect(&self) -> impl Iterator { - self.periodogram().peaks(self.threshold).map(|x| x.period) + fn detect(&self) -> Vec { + self.periodogram() + .peaks(self.threshold) + .map(|x| x.period) + .collect() } } @@ -225,7 +228,7 @@ mod test { 0.09, 0.29, 0.81, 0.49, 0.11, 0.28, 0.78, 0.53, ]; - let periods = Detector::builder().build(y).detect().collect::>(); + let periods = Detector::builder().build(y).detect(); assert_eq!(periods[0], 4); } diff --git a/crates/augurs-seasons/src/test_data.rs b/crates/augurs-seasons/src/test_data.rs index a1f198a..f591f88 100644 --- a/crates/augurs-seasons/src/test_data.rs +++ b/crates/augurs-seasons/src/test_data.rs @@ -1,7 +1,7 @@ use augurs_testing::data::{SEASON_EIGHT, SEASON_SEVEN}; pub(crate) struct TestCase { - pub(crate) season_lengths: &'static [usize], + pub(crate) season_lengths: &'static [u32], pub(crate) data: &'static [f64], } diff --git a/crates/pyaugurs/src/seasons.rs b/crates/pyaugurs/src/seasons.rs index b23d0b3..1d2699f 100644 --- a/crates/pyaugurs/src/seasons.rs +++ b/crates/pyaugurs/src/seasons.rs @@ -7,14 +7,10 @@ use augurs_seasons::{Detector, PeriodogramDetector}; /// Detect the seasonal periods in a time series. #[pyfunction] -pub fn seasonalities( - py: Python<'_>, - y: PyReadonlyArray1<'_, f64>, -) -> PyResult>> { +pub fn seasonalities(py: Python<'_>, y: PyReadonlyArray1<'_, f64>) -> PyResult>> { Ok(PeriodogramDetector::builder() .build(y.as_slice()?) .detect() - .collect::>() .to_pyarray(py) .into()) } From 25137543f2f99a5cd3e8533c30c7beebcc4b2960 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 15 Feb 2024 12:11:12 +0000 Subject: [PATCH 3/5] Change Detector trait to pass data to detect This means detectors don't need to store the data and can avoid a lifetime parameter. --- crates/augurs-js/src/seasons.rs | 2 +- crates/augurs-seasons/README.md | 12 +++++- crates/augurs-seasons/benches/periodogram.rs | 3 +- crates/augurs-seasons/src/lib.rs | 4 +- crates/augurs-seasons/src/periodogram.rs | 41 +++++++++++--------- crates/pyaugurs/src/seasons.rs | 4 +- 6 files changed, 41 insertions(+), 25 deletions(-) diff --git a/crates/augurs-js/src/seasons.rs b/crates/augurs-js/src/seasons.rs index efcfc08..1e769a9 100644 --- a/crates/augurs-js/src/seasons.rs +++ b/crates/augurs-js/src/seasons.rs @@ -7,5 +7,5 @@ use augurs_seasons::{Detector, PeriodogramDetector}; /// Detect the seasonal periods in a time series. #[wasm_bindgen] pub fn seasonalities(y: &[f64]) -> Vec { - PeriodogramDetector::builder().build(y).detect() + PeriodogramDetector::builder().build().detect(y) } diff --git a/crates/augurs-seasons/README.md b/crates/augurs-seasons/README.md index 91d2833..bb3c350 100644 --- a/crates/augurs-seasons/README.md +++ b/crates/augurs-seasons/README.md @@ -20,7 +20,17 @@ let y = &[ 0.09, 0.29, 0.81, 0.49, 0.11, 0.28, 0.78, 0.53, ]; -let periods = PeriodogramDetector::builder().build(y).detect(); +// Use the detector with default parameters. +let periods = PeriodogramDetector::default().detect(y); +assert_eq!(periods[0], 4); + +// Customise the detector using the builder. +let periods = PeriodogramDetector::builder() + .min_period(4) + .max_period(8) + .threshold(0.8) + .build() + .detect(y); assert_eq!(periods[0], 4); # } ``` diff --git a/crates/augurs-seasons/benches/periodogram.rs b/crates/augurs-seasons/benches/periodogram.rs index bcf74b7..71ca675 100644 --- a/crates/augurs-seasons/benches/periodogram.rs +++ b/crates/augurs-seasons/benches/periodogram.rs @@ -6,8 +6,9 @@ use augurs_testing::data::SEASON_EIGHT; fn season_eight(c: &mut Criterion) { let y = SEASON_EIGHT; + let detector = PeriodogramDetector::builder().build(); c.bench_function("season_eight", |b| { - b.iter(|| PeriodogramDetector::builder().build(y).detect()); + b.iter(|| detector.detect(y)); }); } diff --git a/crates/augurs-seasons/src/lib.rs b/crates/augurs-seasons/src/lib.rs index 00e9787..0ad51c4 100644 --- a/crates/augurs-seasons/src/lib.rs +++ b/crates/augurs-seasons/src/lib.rs @@ -16,6 +16,6 @@ pub use periodogram::{ /// A detector of periodic signals in a time series. pub trait Detector { - /// Detects the periods of the time series. - fn detect(&self) -> Vec; + /// Detects the periods of a time series. + fn detect(&self, data: &[f64]) -> Vec; } diff --git a/crates/augurs-seasons/src/periodogram.rs b/crates/augurs-seasons/src/periodogram.rs index 6404114..a5ab63b 100644 --- a/crates/augurs-seasons/src/periodogram.rs +++ b/crates/augurs-seasons/src/periodogram.rs @@ -61,11 +61,10 @@ impl Builder { /// /// The data is the time series to detect seasonal periods in. #[must_use] - pub fn build(self, data: &[f64]) -> Detector<'_> { + pub fn build(self) -> Detector { Detector { - data, min_period: self.min_period, - max_period: self.max_period.unwrap_or_else(|| default_max_period(data)), + max_period: self.max_period, threshold: self.threshold, } } @@ -138,14 +137,13 @@ pub struct Period { /// Welch's method. The peaks in the periodogram represent likely seasonal periods /// in the data. #[derive(Debug)] -pub struct Detector<'a> { - data: &'a [f64], +pub struct Detector { min_period: u32, - max_period: u32, + max_period: Option, threshold: f64, } -impl<'a> Detector<'a> { +impl Detector { /// Create a new detector builder. #[must_use] pub fn builder() -> Builder { @@ -160,14 +158,15 @@ impl<'a> Detector<'a> { /// The periodogram can then be used to identify peaks, which are returned as periods which /// correspond to likely seasonal periods in the data. #[must_use] - pub fn periodogram(&self) -> Periodogram { + pub fn periodogram(&self, data: &[f64]) -> Periodogram { + let max_period = self.max_period.unwrap_or_else(|| default_max_period(data)); let frequency = 1.0; - let data_len = self.data.len(); - let n_per_segment = (self.max_period * 2).min(data_len as u32 / 2); + let data_len = data.len(); + let n_per_segment = (max_period * 2).min(data_len as u32 / 2); let max_fft_size = (n_per_segment as f64).log2().floor() as usize; let n_segments = (data_len as f64 / n_per_segment as f64).ceil() as usize; - let welch: SpectralDensity<'_, f64> = SpectralDensity::builder(self.data, frequency) + let welch: SpectralDensity<'_, f64> = SpectralDensity::builder(data, frequency) .n_segment(n_segments) .dft_log2_max_size(max_fft_size) .build(); @@ -185,7 +184,7 @@ impl<'a> Detector<'a> { .filter(|(per, _)| { // Filter out periods that are too short or too long, and the period corresponding to the // segment length. - *per >= self.min_period && *per < self.max_period && *per != n_per_segment + *per >= self.min_period && *per < max_period && *per != n_per_segment }) // Group by period, and keep the maximum power for each period. .group_by(|(per, _)| *per) @@ -201,9 +200,15 @@ impl<'a> Detector<'a> { } } -impl<'a> crate::Detector for Detector<'a> { - fn detect(&self) -> Vec { - self.periodogram() +impl Default for Detector { + fn default() -> Self { + Self::builder().build() + } +} + +impl crate::Detector for Detector { + fn detect(&self, data: &[f64]) -> Vec { + self.periodogram(data) .peaks(self.threshold) .map(|x| x.period) .collect() @@ -228,7 +233,7 @@ mod test { 0.09, 0.29, 0.81, 0.49, 0.11, 0.28, 0.78, 0.53, ]; - let periods = Detector::builder().build(y).detect(); + let periods = Detector::default().detect(y); assert_eq!(periods[0], 4); } @@ -239,10 +244,10 @@ mod test { data, season_lengths: expected, } = test_case; - let detector = Detector::builder().build(data); + let detector = Detector::default(); assert_eq!( detector - .periodogram() + .periodogram(data) .peaks(0.5) .map(|x| x.period) .collect_vec(), diff --git a/crates/pyaugurs/src/seasons.rs b/crates/pyaugurs/src/seasons.rs index 1d2699f..4dab9f0 100644 --- a/crates/pyaugurs/src/seasons.rs +++ b/crates/pyaugurs/src/seasons.rs @@ -9,8 +9,8 @@ use augurs_seasons::{Detector, PeriodogramDetector}; #[pyfunction] pub fn seasonalities(py: Python<'_>, y: PyReadonlyArray1<'_, f64>) -> PyResult>> { Ok(PeriodogramDetector::builder() - .build(y.as_slice()?) - .detect() + .build() + .detect(y.as_slice()?) .to_pyarray(py) .into()) } From 178d30fd2c8e584cdd439d3085539f879f29acd6 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 15 Feb 2024 14:00:17 +0000 Subject: [PATCH 4/5] Allow seasonality detector params to be customised in JS bindings --- crates/augurs-js/Cargo.toml | 1 + crates/augurs-js/src/seasons.rs | 47 +++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/augurs-js/Cargo.toml b/crates/augurs-js/Cargo.toml index 3a41611..4b7dd8b 100644 --- a/crates/augurs-js/Cargo.toml +++ b/crates/augurs-js/Cargo.toml @@ -28,6 +28,7 @@ augurs-seasons = { workspace = true } console_error_panic_hook = { version = "0.1.7", optional = true } getrandom = { version = "0.2.10", features = ["js"] } js-sys = "0.3.64" +serde.workspace = true serde-wasm-bindgen = "0.6.0" tracing-wasm = { version = "0.2.1", optional = true } wasm-bindgen = "0.2.87" diff --git a/crates/augurs-js/src/seasons.rs b/crates/augurs-js/src/seasons.rs index 1e769a9..b29452e 100644 --- a/crates/augurs-js/src/seasons.rs +++ b/crates/augurs-js/src/seasons.rs @@ -1,11 +1,54 @@ //! Javascript bindings for augurs seasonality detection. +use serde::Deserialize; use wasm_bindgen::prelude::*; use augurs_seasons::{Detector, PeriodogramDetector}; +/// Options for detecting seasonal periods. +#[derive(Debug, Default, Deserialize)] +pub struct SeasonalityOptions { + /// The minimum period to consider when detecting seasonal periods. + /// + /// The default is 4. + pub min_period: Option, + + /// The maximum period to consider when detecting seasonal periods. + /// + /// The default is the length of the data divided by 3, or 512, whichever is smaller. + pub max_period: Option, + + /// The threshold for detecting peaks in the periodogram. + /// + /// The value will be clamped to the range 0.01 to 0.99. + /// + /// The default is 0.9. + pub threshold: Option, +} + +impl From for PeriodogramDetector { + fn from(options: SeasonalityOptions) -> Self { + let mut builder = PeriodogramDetector::builder(); + if let Some(min_period) = options.min_period { + builder = builder.min_period(min_period); + } + if let Some(max_period) = options.max_period { + builder = builder.max_period(max_period); + } + if let Some(threshold) = options.threshold { + builder = builder.threshold(threshold); + } + builder.build() + } +} + /// Detect the seasonal periods in a time series. #[wasm_bindgen] -pub fn seasonalities(y: &[f64]) -> Vec { - PeriodogramDetector::builder().build().detect(y) +pub fn seasonalities(y: &[f64], options: JsValue) -> Vec { + let options: SeasonalityOptions = + serde_wasm_bindgen::from_value::>(options) + .ok() + .flatten() + .unwrap_or_default(); + PeriodogramDetector::from(options).detect(y) } From 30d7ee0d49f91d2de236acf7c4d2f1d38d1e181a Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 15 Feb 2024 14:02:34 +0000 Subject: [PATCH 5/5] Allow seasonality detector params to be customised in Python bindings --- crates/pyaugurs/augurs.pyi | 21 +++++++++++++++++++-- crates/pyaugurs/src/seasons.rs | 26 ++++++++++++++++++++------ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/pyaugurs/augurs.pyi b/crates/pyaugurs/augurs.pyi index c021ac0..0522a1b 100644 --- a/crates/pyaugurs/augurs.pyi +++ b/crates/pyaugurs/augurs.pyi @@ -1,5 +1,5 @@ import abc -from typing import Sequence +from typing import Optional, Sequence import numpy as np import numpy.typing as npt @@ -69,4 +69,21 @@ class AutoETS: def predict(self, horizon: int, level: float | None) -> Forecast: ... def predict_in_sample(self, level: float | None) -> Forecast: ... -def seasonalities(y: npt.NDArray[np.float64]) -> npt.NDArray[np.uint64]: ... +def seasonalities( + y: npt.NDArray[np.float64], + min_period: Optional[int] = None, + max_period: Optional[int] = None, + threshold: Optional[float] = None, +) -> npt.NDArray[np.uint64]: ... +""" +Determine the seasonalities of a time series. + +:param y: the time series to analyze. +:param min_period: the minimum period to consider. The default is 4. +:param max_period: the maximum period to consider. The default is the length of the + data divided by 3, or 512, whichever is smaller. +:param threshold: the threshold for detecting peaks in the periodogram. + The value will be clamped to the range 0.01 to 0.99. + The default is 0.9. +:return: an array of season lengths. +""" diff --git a/crates/pyaugurs/src/seasons.rs b/crates/pyaugurs/src/seasons.rs index 4dab9f0..78bce30 100644 --- a/crates/pyaugurs/src/seasons.rs +++ b/crates/pyaugurs/src/seasons.rs @@ -7,10 +7,24 @@ use augurs_seasons::{Detector, PeriodogramDetector}; /// Detect the seasonal periods in a time series. #[pyfunction] -pub fn seasonalities(py: Python<'_>, y: PyReadonlyArray1<'_, f64>) -> PyResult>> { - Ok(PeriodogramDetector::builder() - .build() - .detect(y.as_slice()?) - .to_pyarray(py) - .into()) +pub fn seasonalities( + py: Python<'_>, + y: PyReadonlyArray1<'_, f64>, + min_period: Option, + max_period: Option, + threshold: Option, +) -> PyResult>> { + let mut builder = PeriodogramDetector::builder(); + + if let Some(min_period) = min_period { + builder = builder.min_period(min_period); + } + if let Some(max_period) = max_period { + builder = builder.max_period(max_period); + } + if let Some(threshold) = threshold { + builder = builder.threshold(threshold); + } + + Ok(builder.build().detect(y.as_slice()?).to_pyarray(py).into()) }