Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/vendor
/img
/.direnv
mutants.out/
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cargo-features = ["edition2024"]

[package]
name = "sliding_features"
version = "7.1.3"
version = "8.0.0"
authors = ["MathisWellmann <wellmannmathis@gmail.com>"]
edition = "2024"
license-file = "LICENSE"
Expand Down
8 changes: 5 additions & 3 deletions benches/vsct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use rand::{
use sliding_features::{
View,
pure_functions::Echo,
sliding_windows::Vsct,
sliding_windows::ZScoreStandardization,
};

fn criterion_benchmark(c: &mut Criterion) {
Expand All @@ -26,7 +26,8 @@ fn criterion_benchmark(c: &mut Criterion) {
group.bench_function("f64", |b| {
let vals = Vec::<f64>::from_iter((0..N).map(|_| rng.random()));
b.iter(|| {
let mut view = Vsct::<f64, _>::new(Echo::new(), NonZeroUsize::new(1024).unwrap());
let mut view =
ZScoreStandardization::<f64, _>::new(Echo::new(), NonZeroUsize::new(1024).unwrap());
for v in vals.iter() {
view.update(*v);
let _ = black_box(view.last());
Expand All @@ -36,7 +37,8 @@ fn criterion_benchmark(c: &mut Criterion) {
group.bench_function("f32", |b| {
let vals = Vec::<f32>::from_iter((0..N).map(|_| rng.random()));
b.iter(|| {
let mut view = Vsct::<f32, _>::new(Echo::new(), NonZeroUsize::new(1024).unwrap());
let mut view =
ZScoreStandardization::<f32, _>::new(Echo::new(), NonZeroUsize::new(1024).unwrap());
for v in vals.iter() {
view.update(*v);
let _ = black_box(view.last());
Expand Down
4 changes: 2 additions & 2 deletions examples/basic_chainable_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use sliding_features::{
pure_functions::Echo,
sliding_windows::{
Alma,
Vsct,
ZScoreStandardization,
},
};

Expand All @@ -24,7 +24,7 @@ fn main() {
let window_len = NonZeroUsize::new(20).unwrap();
let mut chain = Alma::new(
// first, define the last function which gets applied in the chain
Vsct::new(Echo::new(), window_len), // Make the first transformation in the chain a VSCT
ZScoreStandardization::new(Echo::new(), window_len), // Make the first transformation in the chain a VSCT
window_len,
);
for v in &rands {
Expand Down
18 changes: 9 additions & 9 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
taplo
cargo-semver-checks
cargo_upgrades
cargo-mutants
];
nix_tools = with pkgs; [
alejandra # Nix code formatter
Expand Down
Binary file modified img/trend_flex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/vsct.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/welford_online_sliding.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/sliding_windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ mod sma;
mod super_smoother;
mod trend_flex;
mod variance_stabilizing_transformation;
mod vsct;
mod welford_online;
mod z_score_standardization;

pub use alma::Alma;
pub use binary_entropy::BinaryEntropy;
Expand All @@ -54,5 +54,5 @@ pub use sma::Sma;
pub use super_smoother::SuperSmoother;
pub use trend_flex::TrendFlex;
pub use variance_stabilizing_transformation::Vst;
pub use vsct::Vsct;
pub use welford_online::WelfordOnline;
pub use z_score_standardization::ZScoreStandardization;
92 changes: 0 additions & 92 deletions src/sliding_windows/vsct.rs

This file was deleted.

66 changes: 63 additions & 3 deletions src/sliding_windows/welford_online.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,29 @@ where

#[inline]
fn update_stats_remove(&mut self, old_value: T) {
let delta = old_value - self.mean;
self.mean = self.mean - (delta / T::from(self.count).unwrap());
self.m2 = self.m2 - (delta * (old_value - self.mean));
// Derive sum and sum-of-squares from Welford state:
// mean = sum / n, m2 = sum_sq - n * mean^2
// => sum = n * mean, sum_sq = m2 + n * mean^2
let n = T::from(self.count).unwrap();
let sum = self.mean * n;
let sum_sq = self.m2 + sum * self.mean;

let sum_new = sum - old_value;
let sum_sq_new = sum_sq - old_value * old_value;
self.count -= 1;

if self.count > 0 {
let n_new = T::from(self.count).unwrap();
self.mean = sum_new / n_new;
self.m2 = sum_sq_new - n_new * self.mean * self.mean;
// Clamp floating-point noise that could push m2 slightly negative.
if self.m2 < T::zero() {
self.m2 = T::zero();
}
} else {
self.mean = T::zero();
self.m2 = T::zero();
}
}

/// Return the variance of the sliding window
Expand Down Expand Up @@ -139,6 +158,47 @@ mod tests {
assert_eq!(round(w_std_dev, 4), round(std_dev, 4));
}

#[test]
fn welford_online_sliding_matches_direct_computation() {
// Feed [1, 2, 3, 4, 5, 6] through a window of length 3.
// After each step, verify the std dev matches the direct formula for
// the window contents.
let mut wo = WelfordOnline::new(Echo::new(), NonZeroUsize::new(3).unwrap());
let all: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];

// Direct std-dev helper (sample sd).
let direct_sd = |vs: &[f64]| -> f64 {
let n = vs.len() as f64;
let m = vs.iter().sum::<f64>() / n;
let var = vs.iter().map(|v| (v - m).powi(2)).sum::<f64>() / (n - 1.0);
var.sqrt()
};

for i in 0..all.len() {
wo.update(all[i]);
let start = if i < 2 { 0 } else { i - 2 }; // i+1-window
let window = &all[start..=i];
let expected = direct_sd(&window);
if let Some(got) = wo.last() {
let diff = (got - expected).abs();
if diff > 1e-12 {
panic!(
"step {} window {:?}: expected sd={}, got={}, diff={}",
i, window, expected, got, diff
);
}
} else {
// window not full yet → None is correct for i < 2
assert!(
i < 2,
"step {}: got None but window is full ({:?})",
i,
window
);
}
}
}

#[test]
fn welford_online_plot() {
let mut wo = WelfordOnline::new(Echo::new(), NonZeroUsize::new(16).unwrap());
Expand Down
Loading
Loading