nd_arrow_array/compute/
window.rs1use std::sync::Arc;
4
5use arrow::array::{Array, AsArray, Float64Array};
6use arrow::compute::cast as arrow_cast;
7use arrow::datatypes::{DataType, Float64Type};
8
9use crate::NdArrowArray;
10use crate::error::{Result, nd_err};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RollingOp {
15 Sum,
17 Mean,
19 Min,
21 Max,
23 Count,
25}
26
27pub fn rolling(
34 a: &NdArrowArray,
35 dim: &str,
36 window: usize,
37 op: RollingOp,
38 min_periods: Option<usize>,
39 center: bool,
40) -> Result<NdArrowArray> {
41 if window == 0 {
42 return Err(nd_err!(
43 InvalidArgument,
44 "rolling window must be at least 1"
45 ));
46 }
47 let min_periods = min_periods.unwrap_or(window).max(1);
48 let axis = a.dims().axis(dim)?;
49 let shape = a.shape();
50 let n = shape[axis];
51 let outer: usize = shape[..axis].iter().product();
52 let inner: usize = shape[axis + 1..].iter().product();
53 let values = arrow_cast(a.values().as_ref(), &DataType::Float64)?;
54 let values = values.as_primitive::<Float64Type>();
55 let get = |p: usize| values.is_valid(p).then(|| values.value(p));
56
57 let shift = if center { window / 2 } else { 0 };
59 let mut out = vec![None; a.len()];
60 let mut lane = Vec::with_capacity(n);
61 for o in 0..outer {
62 for j in 0..inner {
63 lane.clear();
64 lane.extend((0..n).map(|i| get((o * n + i) * inner + j)));
65 for i in 0..n {
66 let end = (i + shift).min(n.saturating_sub(1));
67 let start = (i + shift + 1).saturating_sub(window);
68 let items = lane[start..=end].iter().flatten();
69 let valid = items.clone().count();
70 let p = (o * n + i) * inner + j;
71 out[p] = match op {
72 RollingOp::Count => Some(valid as f64),
73 _ if valid < min_periods => None,
74 RollingOp::Sum => Some(items.sum()),
75 RollingOp::Mean => Some(items.sum::<f64>() / valid as f64),
76 RollingOp::Min => items.copied().reduce(f64::min),
77 RollingOp::Max => items.copied().reduce(f64::max),
78 };
79 }
80 }
81 }
82 let result: Arc<dyn Array> = if op == RollingOp::Count {
83 arrow_cast(&Float64Array::from(out), &DataType::Int64)?
84 } else {
85 Arc::new(Float64Array::from(out))
86 };
87 NdArrowArray::try_new(result, a.dims().clone())
88}
89
90#[cfg(test)]
91mod tests {
92 use arrow::array::{ArrayRef, Int32Array};
93
94 use super::*;
95 use crate::Dimensions;
96
97 fn lane(values: Vec<Option<i32>>) -> NdArrowArray {
98 let n = values.len();
99 NdArrowArray::try_new(
100 Arc::new(Int32Array::from(values)) as ArrayRef,
101 Dimensions::try_new([("t", n)]).unwrap(),
102 )
103 .unwrap()
104 }
105
106 fn f64s(a: &NdArrowArray) -> Vec<Option<f64>> {
107 a.values().as_primitive::<Float64Type>().iter().collect()
108 }
109
110 #[test]
111 fn trailing_mean_matches_xarray() {
112 let a = lane(vec![Some(1), Some(2), Some(3), Some(4)]);
113 let m = rolling(&a, "t", 2, RollingOp::Mean, None, false).unwrap();
114 assert_eq!(f64s(&m), vec![None, Some(1.5), Some(2.5), Some(3.5)]);
115 }
116
117 #[test]
118 fn centered_sum_with_min_periods() {
119 let a = lane(vec![Some(1), None, Some(3), Some(4)]);
120 let s = rolling(&a, "t", 3, RollingOp::Sum, Some(1), true).unwrap();
121 assert_eq!(f64s(&s), vec![Some(1.0), Some(4.0), Some(7.0), Some(7.0)]);
122 let c = rolling(&a, "t", 3, RollingOp::Count, None, true).unwrap();
123 assert_eq!(c.data_type(), &DataType::Int64);
124 }
125}