Skip to main content

rust_igraph/algorithms/properties/
running_mean.rs

1//! Running mean (ALGO-PR-037).
2//!
3//! Counterpart of `igraph_running_mean()` from
4//! `references/igraph/src/misc/other.c`.
5//!
6//! Computes the running mean of a data vector with a given bin width.
7
8use crate::core::{IgraphError, IgraphResult};
9
10/// Compute the running mean of a data vector.
11///
12/// For a data vector of length `n` and bin width `w`, returns a vector
13/// of length `n - w + 1` where entry `i` is the mean of
14/// `data[i..i+w]`.
15///
16/// # Errors
17///
18/// - `InvalidArgument` if `binwidth < 1`.
19/// - `InvalidArgument` if `data.len() < binwidth`.
20///
21/// # Examples
22///
23/// ```
24/// use rust_igraph::running_mean;
25///
26/// let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
27/// let result = running_mean(&data, 3).unwrap();
28/// assert_eq!(result.len(), 3);
29/// assert!((result[0] - 2.0).abs() < 1e-10); // mean(1,2,3)
30/// assert!((result[1] - 3.0).abs() < 1e-10); // mean(2,3,4)
31/// assert!((result[2] - 4.0).abs() < 1e-10); // mean(3,4,5)
32/// ```
33pub fn running_mean(data: &[f64], binwidth: usize) -> IgraphResult<Vec<f64>> {
34    if binwidth < 1 {
35        return Err(IgraphError::InvalidArgument(
36            "running_mean: binwidth must be at least 1".into(),
37        ));
38    }
39
40    if data.len() < binwidth {
41        return Err(IgraphError::InvalidArgument(format!(
42            "running_mean: data length {} is smaller than binwidth {binwidth}",
43            data.len()
44        )));
45    }
46
47    let result_len = data.len() - binwidth + 1;
48    let mut result: Vec<f64> = Vec::with_capacity(result_len);
49
50    #[allow(clippy::cast_precision_loss)]
51    let bw_f64 = binwidth as f64;
52
53    let mut sum: f64 = data[..binwidth].iter().sum();
54    result.push(sum / bw_f64);
55
56    for i in 1..result_len {
57        sum -= data[i - 1];
58        sum += data[i + binwidth - 1];
59        result.push(sum / bw_f64);
60    }
61
62    Ok(result)
63}
64
65/// Expand a vertex path into consecutive pairs for edge lookup.
66///
67/// Converts `[v0, v1, v2, v3]` into `[(v0, v1), (v1, v2), (v2, v3)]`.
68/// Useful for converting a path of vertex IDs into pairs that can be
69/// passed to edge-lookup functions.
70///
71/// Returns an empty vector if the path has fewer than 2 elements.
72///
73/// # Examples
74///
75/// ```
76/// use rust_igraph::expand_path_to_pairs;
77///
78/// let path = vec![0, 1, 2, 3];
79/// let pairs = expand_path_to_pairs(&path);
80/// assert_eq!(pairs, vec![(0, 1), (1, 2), (2, 3)]);
81/// ```
82pub fn expand_path_to_pairs(path: &[u32]) -> Vec<(u32, u32)> {
83    if path.len() < 2 {
84        return Vec::new();
85    }
86    path.windows(2).map(|w| (w[0], w[1])).collect()
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn running_mean_basic() {
95        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
96        let r = running_mean(&data, 3).unwrap();
97        assert_eq!(r.len(), 3);
98        assert!((r[0] - 2.0).abs() < 1e-10);
99        assert!((r[1] - 3.0).abs() < 1e-10);
100        assert!((r[2] - 4.0).abs() < 1e-10);
101    }
102
103    #[test]
104    fn running_mean_binwidth_1() {
105        let data = vec![5.0, 3.0, 7.0];
106        let r = running_mean(&data, 1).unwrap();
107        assert_eq!(r.len(), 3);
108        assert!((r[0] - 5.0).abs() < 1e-10);
109        assert!((r[1] - 3.0).abs() < 1e-10);
110        assert!((r[2] - 7.0).abs() < 1e-10);
111    }
112
113    #[test]
114    fn running_mean_binwidth_equals_len() {
115        let data = vec![1.0, 2.0, 3.0];
116        let r = running_mean(&data, 3).unwrap();
117        assert_eq!(r.len(), 1);
118        assert!((r[0] - 2.0).abs() < 1e-10);
119    }
120
121    #[test]
122    fn running_mean_binwidth_too_large() {
123        let data = vec![1.0, 2.0];
124        let err = running_mean(&data, 3).unwrap_err();
125        assert!(matches!(err, IgraphError::InvalidArgument(_)));
126    }
127
128    #[test]
129    fn running_mean_binwidth_zero() {
130        let data = vec![1.0, 2.0, 3.0];
131        let err = running_mean(&data, 0).unwrap_err();
132        assert!(matches!(err, IgraphError::InvalidArgument(_)));
133    }
134
135    #[test]
136    fn running_mean_constant() {
137        let data = vec![4.0; 10];
138        let r = running_mean(&data, 5).unwrap();
139        assert_eq!(r.len(), 6);
140        for v in &r {
141            assert!((*v - 4.0).abs() < 1e-10);
142        }
143    }
144
145    #[test]
146    fn expand_pairs_basic() {
147        let pairs = expand_path_to_pairs(&[0, 1, 2, 3]);
148        assert_eq!(pairs, vec![(0, 1), (1, 2), (2, 3)]);
149    }
150
151    #[test]
152    fn expand_pairs_single_edge() {
153        let pairs = expand_path_to_pairs(&[5, 10]);
154        assert_eq!(pairs, vec![(5, 10)]);
155    }
156
157    #[test]
158    fn expand_pairs_empty() {
159        let pairs = expand_path_to_pairs(&[]);
160        assert!(pairs.is_empty());
161    }
162
163    #[test]
164    fn expand_pairs_single_vertex() {
165        let pairs = expand_path_to_pairs(&[7]);
166        assert!(pairs.is_empty());
167    }
168
169    #[test]
170    fn expand_pairs_cycle() {
171        let pairs = expand_path_to_pairs(&[0, 1, 2, 0]);
172        assert_eq!(pairs, vec![(0, 1), (1, 2), (2, 0)]);
173    }
174}