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// }