augurs_ets/lib.rs
1//! Exponential smoothing models.
2//!
3//! This crate provides exponential smoothing models for time series forecasting.
4//! The models are implemented in Rust and are based on the [statsforecast][] Python package.
5//!
6//! **Important**: This crate is still in development and the API is subject to change.
7//! Seasonal models are not yet implemented, and some model types have not been tested.
8//!
9//! # Example
10//!
11//! ```
12//! use augurs_core::prelude::*;
13//! use augurs_ets::AutoETS;
14//!
15//! let data: Vec<_> = (0..10).map(|x| x as f64).collect();
16//! let mut search = AutoETS::new(1, "ZZN")
17//! .expect("ZZN is a valid model search specification string");
18//! let model = search.fit(&data).expect("fit should succeed");
19//! let forecast = model.predict(5, 0.95).expect("predict should succeed");
20//! assert_eq!(forecast.point.len(), 5);
21//! assert_eq!(forecast.point, vec![10.0, 11.0, 12.0, 13.0, 14.0]);
22//! ```
23//!
24//! [statsforecast]: https://nixtla.github.io/statsforecast/models.html#autoets
25
26mod auto;
27mod ets;
28pub mod model;
29mod stat;
30#[cfg(feature = "mstl")]
31pub mod trend;
32
33use augurs_core::ModelError;
34pub use auto::{AutoETS, AutoSpec, FittedAutoETS};
35
36/// Errors returned by this crate.
37#[derive(Debug, thiserror::Error)]
38pub enum Error {
39 /// An error occurred while parsing an error specification string.
40 #[error("invalid error component string '{0}', must be one of 'A', 'M', 'Z'")]
41 InvalidErrorComponentString(char),
42 /// An error occurred while parsing a trend or seasonal specification string.
43 #[error("invalid component string '{0}', must be one of 'N', 'A', 'M', 'Z'")]
44 InvalidComponentString(char),
45 /// An error occurred while parsing a model specification string.
46 #[error("invalid model specification '{0}'")]
47 InvalidModelSpec(String),
48
49 /// The bounds of a parameter were inconsistent, i.e. the lower bound was
50 /// greater than the upper bound.
51 #[error("inconsistent parameter boundaries")]
52 InconsistentBounds,
53 /// One or more of the provided parameters was out of range.
54 /// The definition of 'out of range' depends on the type of
55 /// [`Bounds`][model::Bounds] used.
56 #[error("parameters out of range")]
57 ParamsOutOfRange,
58 /// Not enough data was provided to fit a model.
59 #[error("not enough data")]
60 NotEnoughData,
61
62 /// An error occurred solving a linear system while initializing state.
63 #[error("least squares: {0}")]
64 LeastSquares(&'static str),
65
66 /// No suitable model was found.
67 #[error("no model found")]
68 NoModelFound,
69
70 /// The model has not yet been fit.
71 #[error("model not fit")]
72 ModelNotFit,
73}
74
75impl ModelError for Error {}
76
77type Result<T> = std::result::Result<T, Error>;
78
79// Commented out because I haven't implemented seasonal models yet.
80// fn fourier(y: &[f64], period: &[usize], K: &[usize]) -> DMatrix<f64> {
81// let times: Vec<_> = (1..y.len() + 1).collect();
82// let len_p = K.iter().fold(0, |sum, k| sum + k.min(&0));
83// let mut p = vec![f64::NAN; len_p];
84// let idx = 0;
85// for (j, p_) in period.iter().enumerate() {
86// let k = K[j];
87// if k > 0 {
88// for (i, x) in p[idx..(idx + k)].iter_mut().enumerate() {
89// *x = i as f64 / *p_ as f64;
90// }
91// }
92// }
93// p.dedup();
94// // Determine columns where sinpi=0.
95// let k: Vec<bool> = zip(
96// p.iter().map(|x| 2.0 * x),
97// p.iter().map(|x| (2.0 * x).round()),
98// )
99// .map(|(a, b)| a - b > f64::MIN)
100// .collect();
101// let mut x = DMatrix::from_element(times.len(), 2 * p.len(), f64::NAN);
102// for (j, p_) in p.iter().enumerate() {
103// if k[j] {
104// x.column_mut(2 * j - 1)
105// .iter_mut()
106// .enumerate()
107// .for_each(|(i, val)| {
108// *val = (2.0 * p_ * (i + 1) as f64 * std::f64::consts::PI).sin()
109// });
110// }
111// x.column_mut(2 * j)
112// .iter_mut()
113// .enumerate()
114// .for_each(|(i, val)| *val = (2.0 * p_ * (i + 1) as f64 * std::f64::consts::PI).cos());
115// }
116// let cols_to_delete: Vec<_> = x
117// .column_sum()
118// .iter()
119// .enumerate()
120// .filter_map(|(i, sum)| if sum.is_nan() { Some(i) } else { None })
121// .collect();
122// x.remove_columns_at(&cols_to_delete)
123// }