pub struct Ewma {
target: f64,
sigma: f64,
lambda: f64,
l_factor: f64,
}
#[derive(Debug, Clone)]
pub struct EwmaResult {
pub ewma: f64,
pub ucl: f64,
pub lcl: f64,
pub signal: bool,
pub index: usize,
}
impl Ewma {
pub fn new(target: f64, sigma: f64) -> Option<Self> {
Self::with_params(target, sigma, 0.2, 3.0)
}
pub fn with_params(target: f64, sigma: f64, lambda: f64, l_factor: f64) -> Option<Self> {
if !target.is_finite() {
return None;
}
if !sigma.is_finite() || sigma <= 0.0 {
return None;
}
if !lambda.is_finite() || lambda <= 0.0 || lambda > 1.0 {
return None;
}
if !l_factor.is_finite() || l_factor <= 0.0 {
return None;
}
Some(Self {
target,
sigma,
lambda,
l_factor,
})
}
fn control_limit_half_width(&self, i: usize) -> f64 {
let asymptotic_var = self.lambda / (2.0 - self.lambda);
let decay = (1.0 - self.lambda).powi(2 * i as i32);
let time_varying_var = asymptotic_var * (1.0 - decay);
self.l_factor * self.sigma * time_varying_var.sqrt()
}
pub fn analyze(&self, data: &[f64]) -> Vec<EwmaResult> {
let mut results = Vec::with_capacity(data.len());
let mut z = self.target;
for (i, &x) in data.iter().enumerate() {
if !x.is_finite() {
let half_width = self.control_limit_half_width(i + 1);
results.push(EwmaResult {
ewma: z,
ucl: self.target + half_width,
lcl: self.target - half_width,
signal: false,
index: i,
});
continue;
}
z = self.lambda * x + (1.0 - self.lambda) * z;
let half_width = self.control_limit_half_width(i + 1);
let ucl = self.target + half_width;
let lcl = self.target - half_width;
let signal = z > ucl || z < lcl;
results.push(EwmaResult {
ewma: z,
ucl,
lcl,
signal,
index: i,
});
}
results
}
pub fn signal_points(&self, data: &[f64]) -> Vec<usize> {
self.analyze(data)
.into_iter()
.filter(|r| r.signal)
.map(|r| r.index)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ewma_in_control_stays_near_target() {
let target = 25.0;
let sigma = 2.0;
let ewma = Ewma::new(target, sigma).expect("valid params");
let data: Vec<f64> = vec![target; 50];
let results = ewma.analyze(&data);
assert_eq!(results.len(), 50);
for r in &results {
assert!(
(r.ewma - target).abs() < 1e-10,
"EWMA should stay at target when data == target, got {} at index {}",
r.ewma,
r.index
);
assert!(
!r.signal,
"no signals expected for in-control data at index {}",
r.index
);
}
}
#[test]
fn test_ewma_in_control_with_noise() {
let target = 100.0;
let sigma = 5.0;
let ewma = Ewma::new(target, sigma).expect("valid params");
let data: Vec<f64> = (0..100)
.map(|i| {
if i % 2 == 0 {
target + 0.2 * sigma
} else {
target - 0.2 * sigma
}
})
.collect();
let signals = ewma.signal_points(&data);
assert!(
signals.is_empty(),
"symmetric noise of 0.2sigma should not trigger EWMA signals"
);
}
#[test]
fn test_ewma_gradual_drift_detected() {
let target = 0.0;
let sigma = 1.0;
let ewma = Ewma::new(target, sigma).expect("valid params");
let data: Vec<f64> = (0..100).map(|i| target + 0.1 * sigma * i as f64).collect();
let signals = ewma.signal_points(&data);
assert!(
!signals.is_empty(),
"EWMA should detect gradual linear drift"
);
}
#[test]
fn test_ewma_lambda_1_degenerates_to_shewhart() {
let target = 50.0;
let sigma = 5.0;
let l_factor = 3.0;
let ewma = Ewma::with_params(target, sigma, 1.0, l_factor).expect("valid params");
let data = [50.0, 55.0, 45.0, 70.0, 30.0];
let results = ewma.analyze(&data);
for (i, r) in results.iter().enumerate() {
assert!(
(r.ewma - data[i]).abs() < 1e-10,
"with lambda=1, EWMA should equal the data point: Z_{}={} vs x_{}={}",
i,
r.ewma,
i,
data[i]
);
}
assert!(results[3].signal, "70 should exceed UCL of ~65");
assert!(results[4].signal, "30 should exceed LCL of ~35");
}
#[test]
fn test_ewma_time_varying_limits_converge() {
let target = 0.0;
let sigma = 1.0;
let lambda = 0.2;
let l_factor = 3.0;
let ewma = Ewma::with_params(target, sigma, lambda, l_factor).expect("valid params");
let asymptotic_hw = l_factor * sigma * (lambda / (2.0 - lambda)).sqrt();
let data: Vec<f64> = vec![target; 200];
let results = ewma.analyze(&data);
let first_hw = results[0].ucl - target;
assert!(
first_hw < asymptotic_hw,
"initial limit half-width {} should be less than asymptotic {}",
first_hw,
asymptotic_hw
);
let last_hw = results[199].ucl - target;
assert!(
(last_hw - asymptotic_hw).abs() < 1e-6,
"limit at i=200 should be close to asymptotic: got {}, expected {}",
last_hw,
asymptotic_hw
);
for i in 1..results.len() {
assert!(
results[i].ucl >= results[i - 1].ucl - 1e-15,
"UCL should be non-decreasing: UCL[{}]={} < UCL[{}]={}",
i,
results[i].ucl,
i - 1,
results[i - 1].ucl
);
}
}
#[test]
fn test_ewma_limits_symmetric() {
let target = 42.0;
let sigma = 3.0;
let ewma = Ewma::new(target, sigma).expect("valid params");
let data: Vec<f64> = vec![target; 20];
let results = ewma.analyze(&data);
for r in &results {
let ucl_dist = r.ucl - target;
let lcl_dist = target - r.lcl;
assert!(
(ucl_dist - lcl_dist).abs() < 1e-12,
"limits should be symmetric: UCL-target={}, target-LCL={}",
ucl_dist,
lcl_dist
);
}
}
#[test]
fn test_ewma_empty_data() {
let ewma = Ewma::new(0.0, 1.0).expect("valid params");
let results = ewma.analyze(&[]);
assert!(
results.is_empty(),
"empty data should produce empty results"
);
let signals = ewma.signal_points(&[]);
assert!(signals.is_empty(), "empty data should produce no signals");
}
#[test]
fn test_ewma_single_point() {
let ewma = Ewma::new(0.0, 1.0).expect("valid params");
let results = ewma.analyze(&[0.0]);
assert_eq!(results.len(), 1);
assert!(
!results[0].signal,
"single in-control point should not signal"
);
let results = ewma.analyze(&[100.0]);
assert_eq!(results.len(), 1);
assert!(results[0].signal, "extreme single point should signal");
}
#[test]
fn test_ewma_invalid_params() {
assert!(Ewma::new(0.0, 0.0).is_none());
assert!(Ewma::new(0.0, -1.0).is_none());
assert!(Ewma::new(0.0, f64::NAN).is_none());
assert!(Ewma::new(0.0, f64::INFINITY).is_none());
assert!(Ewma::new(f64::NAN, 1.0).is_none());
assert!(Ewma::new(f64::INFINITY, 1.0).is_none());
assert!(Ewma::with_params(0.0, 1.0, 0.0, 3.0).is_none());
assert!(Ewma::with_params(0.0, 1.0, -0.1, 3.0).is_none());
assert!(Ewma::with_params(0.0, 1.0, 1.1, 3.0).is_none());
assert!(Ewma::with_params(0.0, 1.0, 0.2, 0.0).is_none());
assert!(Ewma::with_params(0.0, 1.0, 0.2, -1.0).is_none());
}
#[test]
fn test_ewma_non_finite_data_skipped() {
let ewma = Ewma::new(0.0, 1.0).expect("valid params");
let data = [0.0, f64::NAN, 0.0, f64::INFINITY, 0.0];
let results = ewma.analyze(&data);
assert_eq!(results.len(), 5);
assert!(!results[1].signal);
assert!(!results[3].signal);
}
#[test]
fn test_ewma_numeric_reference_roberts() {
let ewma = Ewma::with_params(0.0, 1.0, 0.2, 3.0).expect("valid params");
let data = [1.0f64; 3];
let results = ewma.analyze(&data);
assert!(
(results[0].ewma - 0.2).abs() < 1e-10,
"Z_1 expected 0.2, got {}",
results[0].ewma
);
assert!(
(results[1].ewma - 0.36).abs() < 1e-10,
"Z_2 expected 0.36, got {}",
results[1].ewma
);
assert!(
(results[2].ewma - 0.488).abs() < 1e-10,
"Z_3 expected 0.488, got {}",
results[2].ewma
);
let asymptotic_ucl = 3.0 * (0.2f64 / 1.8).sqrt();
assert!(
(asymptotic_ucl - 1.0).abs() < 1e-3,
"UCL_inf expected ≈1.0, got {}",
asymptotic_ucl
);
let long_data: Vec<f64> = vec![0.0; 200];
let long_results = ewma.analyze(&long_data);
let last_ucl = long_results[199].ucl;
assert!(
(last_ucl - asymptotic_ucl).abs() < 1e-6,
"UCL at i=200 should equal asymptotic {}, got {}",
asymptotic_ucl,
last_ucl
);
}
#[test]
fn test_ewma_step_shift_detected() {
let target = 0.0;
let sigma = 1.0;
let ewma = Ewma::new(target, sigma).expect("valid params");
let mut data = vec![0.0; 50];
for x in data.iter_mut().skip(20) {
*x = 2.0; }
let signals = ewma.signal_points(&data);
assert!(
!signals.is_empty(),
"EWMA should detect a 2-sigma step shift"
);
let first_signal = signals[0];
assert!(
first_signal >= 20,
"signal should not appear before the shift, got {}",
first_signal
);
}
#[test]
fn test_ewma_small_lambda_more_smoothing() {
let target = 0.0;
let sigma = 1.0;
let ewma_slow = Ewma::with_params(target, sigma, 0.05, 3.0).expect("valid params");
let ewma_fast = Ewma::with_params(target, sigma, 0.25, 3.0).expect("valid params");
let mut data = vec![0.0; 30];
for x in data.iter_mut().skip(10) {
*x = 2.0;
}
let results_slow = ewma_slow.analyze(&data);
let results_fast = ewma_fast.analyze(&data);
let z_slow_15 = results_slow[15].ewma;
let z_fast_15 = results_fast[15].ewma;
assert!(
z_fast_15 > z_slow_15,
"fast EWMA (lambda=0.25) should respond faster: z_fast={} > z_slow={}",
z_fast_15,
z_slow_15
);
}
}