common_function/scalars/anomaly/
iqr.rs1use std::fmt::Debug;
28use std::ops::Range;
29use std::sync::Arc;
30
31use arrow::array::{Array, ArrayRef, Float64Array};
32use arrow::datatypes::{DataType, Field, FieldRef};
33use datafusion_common::{DataFusionError, Result, ScalarValue};
34use datafusion_expr::{PartitionEvaluator, Signature, Volatility, WindowUDFImpl};
35use datafusion_functions_window_common::field::WindowUDFFieldArgs;
36use datafusion_functions_window_common::partition::PartitionEvaluatorArgs;
37
38use crate::helper::NUMERICS;
39use crate::scalars::anomaly::utils::{cast_to_f64, collect_window_values, percentile_sorted};
40
41const MIN_SAMPLES: usize = 3;
43
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct AnomalyScoreIqr {
46 signature: Signature,
47}
48
49impl AnomalyScoreIqr {
50 pub fn new() -> Self {
51 Self {
52 signature: Signature::uniform(2, NUMERICS.to_vec(), Volatility::Immutable),
53 }
54 }
55}
56
57impl WindowUDFImpl for AnomalyScoreIqr {
58 fn name(&self) -> &str {
59 "anomaly_score_iqr"
60 }
61
62 fn signature(&self) -> &Signature {
63 &self.signature
64 }
65
66 fn field(&self, field_args: WindowUDFFieldArgs) -> Result<FieldRef> {
67 Ok(Arc::new(Field::new(
68 field_args.name(),
69 DataType::Float64,
70 true, )))
72 }
73
74 fn partition_evaluator(
75 &self,
76 _partition_evaluator_args: PartitionEvaluatorArgs,
77 ) -> Result<Box<dyn PartitionEvaluator>> {
78 Ok(Box::new(AnomalyScoreIqrEvaluator { current_row: 0 }))
79 }
80}
81
82#[derive(Debug)]
83struct AnomalyScoreIqrEvaluator {
84 current_row: usize,
86}
87
88impl PartitionEvaluator for AnomalyScoreIqrEvaluator {
89 fn uses_window_frame(&self) -> bool {
90 true
91 }
92
93 fn supports_bounded_execution(&self) -> bool {
94 false
95 }
96
97 fn evaluate(&mut self, values: &[ArrayRef], range: &Range<usize>) -> Result<ScalarValue> {
98 let values_f64 = cast_to_f64(&values[0])?;
100 let array = values_f64
101 .as_any()
102 .downcast_ref::<Float64Array>()
103 .ok_or_else(|| {
104 DataFusionError::Internal(format!(
105 "Expected Float64Array, got: {:?}",
106 values_f64.data_type()
107 ))
108 })?;
109
110 let k_f64 = cast_to_f64(&values[1])?;
112 let k_array = k_f64
113 .as_any()
114 .downcast_ref::<Float64Array>()
115 .ok_or_else(|| {
116 DataFusionError::Internal(format!(
117 "Expected Float64Array for k, got: {:?}",
118 k_f64.data_type()
119 ))
120 })?;
121
122 let current_idx = self.current_row;
124 self.current_row += 1;
125
126 if current_idx >= array.len()
128 || !array.is_valid(current_idx)
129 || !array.value(current_idx).is_finite()
130 {
131 return Ok(ScalarValue::Float64(None));
132 }
133 let current_value = array.value(current_idx);
134
135 if current_idx >= k_array.len()
137 || !k_array.is_valid(current_idx)
138 || !k_array.value(current_idx).is_finite()
139 {
140 return Ok(ScalarValue::Float64(None));
141 }
142 let k = k_array.value(current_idx);
143 if k < 0.0 {
144 return Ok(ScalarValue::Float64(None));
145 }
146
147 let mut window_values = collect_window_values(array, range);
148 if window_values.len() < MIN_SAMPLES {
149 return Ok(ScalarValue::Float64(None));
150 }
151
152 window_values.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
154
155 let q1 = match percentile_sorted(&window_values, 0.25) {
156 Some(v) => v,
157 None => return Ok(ScalarValue::Float64(None)),
158 };
159 let q3 = match percentile_sorted(&window_values, 0.75) {
160 Some(v) => v,
161 None => return Ok(ScalarValue::Float64(None)),
162 };
163 let iqr = q3 - q1;
164
165 let lower_fence = q1 - k * iqr;
166 let upper_fence = q3 + k * iqr;
167
168 let score = if current_value < lower_fence {
169 (lower_fence - current_value) / iqr
170 } else if current_value > upper_fence {
171 (current_value - upper_fence) / iqr
172 } else {
173 0.0
174 };
175
176 Ok(ScalarValue::Float64(Some(score)))
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use std::sync::Arc;
183
184 use arrow::array::Float64Array;
185 use datafusion_expr::WindowUDF;
186
187 use super::*;
188
189 fn eval_iqr(values: &[Option<f64>], k: f64, range: Range<usize>) -> ScalarValue {
190 let array = Arc::new(Float64Array::from(values.to_vec())) as ArrayRef;
191 let k_array = Arc::new(Float64Array::from(vec![Some(k); values.len()])) as ArrayRef;
192 let current_row = range.end.saturating_sub(1);
193 let mut evaluator = AnomalyScoreIqrEvaluator { current_row };
194 evaluator.evaluate(&[array, k_array], &range).unwrap()
195 }
196
197 #[test]
198 fn test_basic_outlier() {
199 let mut values: Vec<Option<f64>> = (1..=10).map(|x| Some(x as f64)).collect();
201 values.push(Some(100.0));
202 let result = eval_iqr(&values, 1.5, 0..11);
203 match result {
204 ScalarValue::Float64(Some(score)) => assert!(score > 0.0, "score={score}"),
205 other => panic!("expected Some(score), got {other:?}"),
206 }
207 }
208
209 #[test]
210 fn test_value_within_fences() {
211 let values: Vec<Option<f64>> = (1..=10).map(|x| Some(x as f64)).collect();
212 let result = eval_iqr(&values, 1.5, 0..10);
214 match result {
215 ScalarValue::Float64(Some(score)) => {
216 assert!(score >= 0.0, "score should be non-negative, got {score}");
217 }
218 other => panic!("expected Some(score), got {other:?}"),
219 }
220 }
221
222 #[test]
223 fn test_constant_sequence() {
224 let values: Vec<Option<f64>> = vec![Some(5.0); 10];
225 let result = eval_iqr(&values, 1.5, 0..10);
227 assert_eq!(result, ScalarValue::Float64(Some(0.0)));
228 }
229
230 #[test]
231 fn test_zero_iqr_outlier_is_infinite() {
232 let values: Vec<Option<f64>> =
234 vec![Some(1.0), Some(1.0), Some(1.0), Some(1.0), Some(100.0)];
235 let result = eval_iqr(&values, 1.5, 0..5);
236 match result {
237 ScalarValue::Float64(Some(score)) => {
238 assert!(score.is_infinite() && score.is_sign_positive())
239 }
240 other => panic!("expected Some(+inf), got {other:?}"),
241 }
242 }
243
244 #[test]
245 fn test_all_null() {
246 let values: Vec<Option<f64>> = vec![None; 5];
247 let result = eval_iqr(&values, 1.5, 0..5);
248 assert_eq!(result, ScalarValue::Float64(None));
249 }
250
251 #[test]
252 fn test_insufficient_samples() {
253 let values: Vec<Option<f64>> = vec![Some(1.0), Some(2.0)];
255 let result = eval_iqr(&values, 1.5, 0..2);
256 assert_eq!(result, ScalarValue::Float64(None));
257 }
258
259 #[test]
260 fn test_negative_k() {
261 let values: Vec<Option<f64>> =
262 vec![Some(48.0), Some(49.0), Some(50.0), Some(51.0), Some(52.0)];
263 let result = eval_iqr(&values, -1.0, 0..5);
264 assert_eq!(result, ScalarValue::Float64(None));
265 }
266
267 #[test]
268 fn test_current_row_null() {
269 let values: Vec<Option<f64>> = vec![Some(1.0), Some(2.0), Some(3.0), None];
270 let result = eval_iqr(&values, 1.5, 0..4);
271 assert_eq!(result, ScalarValue::Float64(None));
272 }
273
274 #[test]
275 fn test_lower_outlier() {
276 let values: Vec<Option<f64>> = vec![
278 Some(48.0),
279 Some(49.0),
280 Some(50.0),
281 Some(51.0),
282 Some(52.0),
283 Some(-100.0),
284 ];
285 let result = eval_iqr(&values, 1.5, 0..6);
286 match result {
287 ScalarValue::Float64(Some(score)) => assert!(score > 0.0, "score={score}"),
288 other => panic!("expected Some(score), got {other:?}"),
289 }
290
291 let values: Vec<Option<f64>> = vec![
293 Some(48.0),
294 Some(49.0),
295 Some(50.0),
296 Some(51.0),
297 Some(52.0),
298 Some(-100.0),
299 Some(50.0),
300 ];
301 let result = eval_iqr(&values, 1.5, 0..7);
302 assert_eq!(result, ScalarValue::Float64(Some(0.0)));
303 }
304
305 #[test]
306 fn test_udwf_creation() {
307 let udwf = WindowUDF::from(AnomalyScoreIqr::new());
308 assert_eq!(udwf.name(), "anomaly_score_iqr");
309 }
310}