1use std::sync::Arc;
4
5use arrow::array::{Array, AsArray, Float64Array, Int64Array};
6use arrow::compute::cast;
7use arrow::datatypes::{DataType, Float64Type, Int64Type};
8
9use crate::NdArrowArray;
10use crate::dimensions::Dimensions;
11use crate::error::{Result, nd_err};
12
13pub fn histogram(a: &NdArrowArray, edges: &NdArrowArray, dim: &str) -> Result<NdArrowArray> {
19 let edges = cast(edges.values().as_ref(), &DataType::Float64)?;
20 let edges = edges.as_primitive::<Float64Type>();
21 if edges.len() < 2 || edges.null_count() > 0 || edges.values().windows(2).any(|w| w[1] <= w[0])
22 {
23 return Err(nd_err!(
24 InvalidArgument,
25 "histogram edges must be at least two increasing, non-null values"
26 ));
27 }
28 let edges = edges.values();
29 let bins = edges.len() - 1;
30 let mut counts = vec![0i64; bins];
31 let values = cast(a.values().as_ref(), &DataType::Float64)?;
32 for v in values.as_primitive::<Float64Type>().iter().flatten() {
33 if v.is_nan() || v < edges[0] || v > edges[bins] {
34 continue;
35 }
36 let bin = edges
37 .partition_point(|&e| e <= v)
38 .saturating_sub(1)
39 .min(bins - 1);
40 counts[bin] += 1;
41 }
42 NdArrowArray::try_new(
43 Arc::new(Int64Array::from(counts)),
44 Dimensions::try_new([(dim, bins)])?,
45 )
46}
47
48pub fn bincount(
54 a: &NdArrowArray,
55 weights: Option<&NdArrowArray>,
56 min_length: usize,
57 dim: &str,
58) -> Result<NdArrowArray> {
59 if !a.data_type().is_integer() {
60 return Err(nd_err!(
61 InvalidArgument,
62 "bincount needs integers, got {}",
63 a.data_type()
64 ));
65 }
66 let values = cast(a.values().as_ref(), &DataType::Int64)?;
67 let values = values.as_primitive::<Int64Type>();
68 if values.iter().flatten().any(|v| v < 0) {
69 return Err(nd_err!(
70 InvalidArgument,
71 "bincount values must be non-negative"
72 ));
73 }
74 let len = values
75 .iter()
76 .flatten()
77 .max()
78 .map_or(0, |m| m as usize + 1)
79 .max(min_length);
80 let dims = Dimensions::try_new([(dim, len)])?;
81 match weights {
82 None => {
83 let mut counts = vec![0i64; len];
84 for v in values.iter().flatten() {
85 counts[v as usize] += 1;
86 }
87 NdArrowArray::try_new(Arc::new(Int64Array::from(counts)), dims)
88 }
89 Some(w) => {
90 if w.dims() != a.dims() {
91 return Err(nd_err!(
92 ShapeMismatch,
93 "bincount weights {} do not match values {}",
94 w.dims(),
95 a.dims()
96 ));
97 }
98 let w = cast(w.values().as_ref(), &DataType::Float64)?;
99 let w = w.as_primitive::<Float64Type>();
100 let mut sums = vec![0.0f64; len];
101 for (v, weight) in values.iter().zip(w.iter()) {
102 if let (Some(v), Some(weight)) = (v, weight) {
103 sums[v as usize] += weight;
104 }
105 }
106 NdArrowArray::try_new(Arc::new(Float64Array::from(sums)), dims)
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use arrow::array::Int32Array;
114
115 use super::*;
116
117 fn nd(values: Vec<f64>, dims: &[(&str, usize)]) -> NdArrowArray {
118 NdArrowArray::try_new(
119 Arc::new(Float64Array::from(values)),
120 Dimensions::try_new(dims.iter().copied()).unwrap(),
121 )
122 .unwrap()
123 }
124
125 #[test]
126 fn histogram_matches_numpy() {
127 let a = nd(
128 vec![0.5, 1.0, 1.5, 2.0, 2.0, 3.0, f64::NAN, -1.0],
129 &[("x", 8)],
130 );
131 let edges = nd(vec![0.0, 1.0, 2.0], &[("e", 3)]);
132 let h = histogram(&a, &edges, "bin").unwrap();
133 assert_eq!(h.values().as_primitive::<Int64Type>().values(), &[1, 4]);
134 assert!(histogram(&a, &nd(vec![1.0], &[("e", 1)]), "bin").is_err());
135 }
136
137 #[test]
138 fn bincount_with_and_without_weights() {
139 let a = NdArrowArray::try_new(
140 Arc::new(Int32Array::from(vec![Some(0), Some(2), Some(2), None])),
141 Dimensions::try_new([("x", 4)]).unwrap(),
142 )
143 .unwrap();
144 let c = bincount(&a, None, 5, "value").unwrap();
145 assert_eq!(
146 c.values().as_primitive::<Int64Type>().values(),
147 &[1, 0, 2, 0, 0]
148 );
149 let w = nd(vec![0.5, 1.0, 2.0, 9.0], &[("x", 4)]);
150 let s = bincount(&a, Some(&w), 0, "value").unwrap();
151 assert_eq!(
152 s.values().as_primitive::<Float64Type>().values(),
153 &[0.5, 0.0, 3.0]
154 );
155 }
156}