antecedent_data/
transforms.rs1#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
6
7use crate::error::DataError;
8
9fn require_finite(col: &[f64], what: &str) -> Result<(), DataError> {
10 if let Some(i) = col.iter().position(|v| !v.is_finite()) {
11 return Err(DataError::InvalidArgument {
12 message: format!("{what}: non-finite value at index {i}"),
13 });
14 }
15 Ok(())
16}
17
18pub fn equal_width_bin(col: &[f64], n_bins: usize, out: &mut [f64]) -> Result<(), DataError> {
26 if n_bins == 0 {
27 return Err(DataError::InvalidArgument { message: "n_bins must be > 0".into() });
28 }
29 if col.len() != out.len() {
30 return Err(DataError::InvalidArgument { message: "out length != col length".into() });
31 }
32 if col.is_empty() {
33 return Ok(());
34 }
35 require_finite(col, "equal_width_bin")?;
36 let mut min_v = f64::INFINITY;
37 let mut max_v = f64::NEG_INFINITY;
38 for &v in col {
39 min_v = min_v.min(v);
40 max_v = max_v.max(v);
41 }
42 let width = (max_v - min_v).max(1e-12);
43 let last_bin = (n_bins - 1) as f64;
44 for (slot, &v) in out.iter_mut().zip(col.iter()) {
45 let b = ((v - min_v) / width * n_bins as f64).floor();
46 *slot = b.clamp(0.0, last_bin);
47 }
48 Ok(())
49}
50
51pub fn ordinal_patterns(
60 col: &[f64],
61 m: usize,
62 tau: usize,
63 out: &mut [f64],
64) -> Result<usize, DataError> {
65 if m < 2 || tau == 0 {
66 return Err(DataError::InvalidArgument { message: "need m>=2 and tau>=1".into() });
67 }
68 let need = col.len().saturating_sub((m - 1) * tau);
69 if out.len() < need {
70 return Err(DataError::InvalidArgument { message: "out buffer too short".into() });
71 }
72 require_finite(col, "ordinal_patterns")?;
73 let mut idx = vec![0usize; m];
74 for t in 0..need {
75 for (k, slot) in idx.iter_mut().enumerate() {
76 *slot = k;
77 }
78 idx.sort_by(|&a, &b| {
79 col[t + a * tau].partial_cmp(&col[t + b * tau]).unwrap_or(std::cmp::Ordering::Equal)
80 });
81 let mut code = 0usize;
83 for i in 0..m {
84 let mut smaller = 0usize;
85 for j in (i + 1)..m {
86 if idx[j] < idx[i] {
87 smaller += 1;
88 }
89 }
90 code = code * (m - i) + smaller;
91 }
92 out[t] = code as f64;
93 }
94 Ok(need)
95}
96
97pub fn moving_average(col: &[f64], window: usize, out: &mut [f64]) -> Result<(), DataError> {
103 if window == 0 || window % 2 == 0 {
104 return Err(DataError::InvalidArgument { message: "window must be odd and > 0".into() });
105 }
106 if col.len() != out.len() {
107 return Err(DataError::InvalidArgument { message: "out length != col length".into() });
108 }
109 require_finite(col, "moving_average")?;
110 let half = window / 2;
111 let n = col.len();
112 for (i, slot) in out.iter_mut().enumerate() {
113 let lo = i.saturating_sub(half);
114 let hi = (i + half + 1).min(n);
115 let s: f64 = col[lo..hi].iter().sum();
116 *slot = s / (hi - lo) as f64;
117 }
118 Ok(())
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn binning_two_bins() {
127 let col = [0.0, 1.0, 2.0, 3.0];
128 let mut out = [0.0; 4];
129 equal_width_bin(&col, 2, &mut out).unwrap();
130 assert!((out[0] - 0.0).abs() < f64::EPSILON);
131 assert!((out[3] - 1.0).abs() < f64::EPSILON);
132 }
133
134 #[test]
135 fn binning_rejects_nan() {
136 let col = [0.0, f64::NAN, 2.0];
137 let mut out = [0.0; 3];
138 assert!(equal_width_bin(&col, 2, &mut out).is_err());
139 }
140
141 #[test]
142 fn ordinal_runs() {
143 let col: Vec<f64> = (0..20).map(|i| f64::from(i).sin()).collect();
144 let mut out = vec![0.0; 20];
145 let n = ordinal_patterns(&col, 3, 1, &mut out).unwrap();
146 assert!(n > 0);
147 }
148
149 #[test]
150 fn ordinal_rejects_nan() {
151 let col = [0.0, 1.0, f64::NAN, 3.0, 4.0];
152 let mut out = vec![0.0; 5];
153 assert!(ordinal_patterns(&col, 3, 1, &mut out).is_err());
154 }
155}