├── .gitignore ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | *~ 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smoothed_z_score" 3 | description = "Smoothed z-score algo (very robust thresholding algorithm)" 4 | keywords = [ "signal", "timeseries", "z-score", "dispersion", "peak-detection" ] 5 | homepage = "https://github.com/swizard0/smoothed_z_score" 6 | repository = "https://github.com/swizard0/smoothed_z_score" 7 | readme = "README.md" 8 | version = "0.1.3" 9 | authors = ["Jean-Paul", "Alexey Voznyuk "] 10 | license = "MIT" 11 | 12 | [dependencies] 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smoothed z-score peaks detector 2 | 3 | ## Description 4 | 5 | Rust implementation for [Smoothed z-score algorithm](https://stackoverflow.com/questions/22583391/peak-recognition-in-realtime-timeseries-data/22640362#22640362). 6 | 7 | ## Usage 8 | 9 | Add this to your `Cargo.toml`: 10 | 11 | ```toml 12 | [dependencies] 13 | smoothed_z_score = "0.1" 14 | ``` 15 | 16 | and this to your crate root: 17 | 18 | ```rust 19 | extern crate smoothed_z_score; 20 | ``` 21 | 22 | ## Example usage 23 | 24 | Consider this dataset (from the original stackoverflow reply): 25 | 26 | ![sample dataset](https://i.stack.imgur.com/KdpF7.jpg) 27 | 28 | ```rust 29 | use smoothed_z_score::{Peak, PeaksDetector, PeaksFilter}; 30 | 31 | fn main() { 32 | let input = vec![ 33 | 1.0, 1.0, 1.1, 1.0, 0.9, 1.0, 1.0, 1.1, 1.0, 0.9, 1.0, 1.1, 1.0, 1.0, 0.9, 1.0, 1.0, 1.1, 1.0, 34 | 1.0, 1.0, 1.0, 1.1, 0.9, 1.0, 1.1, 1.0, 1.0, 0.9, 1.0, 1.1, 1.0, 1.0, 1.1, 1.0, 0.8, 0.9, 1.0, 35 | 1.2, 0.9, 1.0, 1.0, 1.1, 1.2, 1.0, 1.5, 1.0, 3.0, 2.0, 5.0, 3.0, 2.0, 1.0, 1.0, 1.0, 0.9, 1.0, 36 | 1.0, 3.0, 2.6, 4.0, 3.0, 3.2, 2.0, 1.0, 1.0, 0.8, 4.0, 4.0, 2.0, 2.5, 1.0, 1.0, 1.0 37 | ]; 38 | let output: Vec<_> = input 39 | .into_iter() 40 | .enumerate() 41 | .peaks(PeaksDetector::new(30, 5.0, 0.0), |e| e.1) 42 | .map(|((i, _), p)| (i, p)) 43 | .collect(); 44 | assert_eq!(output, vec![ 45 | (45, Peak::High), (47, Peak::High), (48, Peak::High), (49, Peak::High), 46 | (50, Peak::High), (51, Peak::High), (58, Peak::High), (59, Peak::High), 47 | (60, Peak::High), (61, Peak::High), (62, Peak::High), (63, Peak::High), 48 | (67, Peak::High), (68, Peak::High), (69, Peak::High), (70, Peak::High), 49 | ]); 50 | } 51 | ``` -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq, Debug)] 2 | pub enum Peak { 3 | Low, 4 | High, 5 | } 6 | 7 | pub struct PeaksDetector { 8 | threshold: f64, 9 | influence: f64, 10 | window: Vec, 11 | } 12 | 13 | impl PeaksDetector { 14 | pub fn new(lag: usize, threshold: f64, influence: f64) -> PeaksDetector { 15 | PeaksDetector { 16 | threshold, 17 | influence, 18 | window: Vec::with_capacity(lag), 19 | } 20 | } 21 | 22 | pub fn signal(&mut self, value: f64) -> Option { 23 | if self.window.len() < self.window.capacity() { 24 | self.window.push(value); 25 | None 26 | } else if let (Some((mean, stddev)), Some(&window_last)) = (self.stats(), self.window.last()) { 27 | self.window.remove(0); 28 | if (value - mean).abs() > (self.threshold * stddev) { 29 | let next_value = 30 | (value * self.influence) + ((1. - self.influence) * window_last); 31 | self.window.push(next_value); 32 | Some(if value > mean { Peak::High } else { Peak::Low }) 33 | } else { 34 | self.window.push(value); 35 | None 36 | } 37 | } else { 38 | None 39 | } 40 | } 41 | 42 | pub fn stats(&self) -> Option<(f64, f64)> { 43 | if self.window.is_empty() { 44 | None 45 | } else { 46 | let window_len = self.window.len() as f64; 47 | let mean = self.window.iter().fold(0., |a, v| a + v) / window_len; 48 | let sq_sum = self.window.iter().fold(0., |a, v| a + ((v - mean) * (v - mean))); 49 | let stddev = (sq_sum / window_len).sqrt(); 50 | Some((mean, stddev)) 51 | } 52 | } 53 | } 54 | 55 | pub struct PeaksIter { 56 | source: I, 57 | signal: F, 58 | detector: PeaksDetector, 59 | } 60 | 61 | pub trait PeaksFilter where I: Iterator { 62 | fn peaks(self, detector: PeaksDetector, signal: F) -> PeaksIter 63 | where F: FnMut(&I::Item) -> f64; 64 | } 65 | 66 | impl PeaksFilter for I where I: Iterator { 67 | fn peaks(self, detector: PeaksDetector, signal: F) -> PeaksIter 68 | where F: FnMut(&I::Item) -> f64 69 | { 70 | PeaksIter { source: self, signal, detector, } 71 | } 72 | } 73 | 74 | impl Iterator for PeaksIter where I: Iterator, F: FnMut(&I::Item) -> f64 { 75 | type Item = (I::Item, Peak); 76 | 77 | fn next(&mut self) -> Option { 78 | while let Some(item) = self.source.next() { 79 | let value = (self.signal)(&item); 80 | if let Some(peak) = self.detector.signal(value) { 81 | return Some((item, peak)); 82 | } 83 | } 84 | None 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::{Peak, PeaksDetector, PeaksFilter}; 91 | 92 | #[test] 93 | fn sample_data() { 94 | let input = vec![ 95 | 1.0, 1.0, 1.1, 1.0, 0.9, 1.0, 1.0, 1.1, 1.0, 0.9, 1.0, 1.1, 1.0, 1.0, 0.9, 1.0, 1.0, 1.1, 1.0, 96 | 1.0, 1.0, 1.0, 1.1, 0.9, 1.0, 1.1, 1.0, 1.0, 0.9, 1.0, 1.1, 1.0, 1.0, 1.1, 1.0, 0.8, 0.9, 1.0, 97 | 1.2, 0.9, 1.0, 1.0, 1.1, 1.2, 1.0, 1.5, 1.0, 3.0, 2.0, 5.0, 3.0, 2.0, 1.0, 1.0, 1.0, 0.9, 1.0, 98 | 1.0, 3.0, 2.6, 4.0, 3.0, 3.2, 2.0, 1.0, 1.0, 0.8, 4.0, 4.0, 2.0, 2.5, 1.0, 1.0, 1.0 99 | ]; 100 | let output: Vec<_> = input 101 | .into_iter() 102 | .enumerate() 103 | .peaks(PeaksDetector::new(30, 5.0, 0.0), |e| e.1) 104 | .map(|((i, _), p)| (i, p)) 105 | .collect(); 106 | assert_eq!(output, vec![ 107 | (45, Peak::High), (47, Peak::High), (48, Peak::High), (49, Peak::High), 108 | (50, Peak::High), (51, Peak::High), (58, Peak::High), (59, Peak::High), 109 | (60, Peak::High), (61, Peak::High), (62, Peak::High), (63, Peak::High), 110 | (67, Peak::High), (68, Peak::High), (69, Peak::High), (70, Peak::High), 111 | ]); 112 | } 113 | } 114 | --------------------------------------------------------------------------------