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