Skip to main content

datafusion_functions_aggregate/
approx_percentile_cont.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Debug;
19use std::mem::size_of_val;
20use std::sync::Arc;
21
22use arrow::array::{Array, Float16Array};
23use arrow::compute::{filter, is_not_null};
24use arrow::datatypes::FieldRef;
25use arrow::{
26    array::{ArrayRef, Float32Array, Float64Array},
27    datatypes::{DataType, Field},
28};
29use datafusion_common::types::{NativeType, logical_float64};
30use datafusion_common::{
31    DataFusionError, Result, ScalarValue, downcast_value, internal_err, not_impl_err,
32    plan_err,
33};
34use datafusion_expr::expr::{AggregateFunction, Sort};
35use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs};
36use datafusion_expr::utils::format_state_name;
37use datafusion_expr::{
38    Accumulator, AggregateUDFImpl, Coercion, Documentation, Expr, Signature,
39    TypeSignature, TypeSignatureClass, Volatility,
40};
41use datafusion_functions_aggregate_common::tdigest::{DEFAULT_MAX_SIZE, TDigest};
42use datafusion_macros::user_doc;
43use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
44
45use crate::utils::{get_scalar_value, validate_percentile_expr};
46
47create_func!(ApproxPercentileCont, approx_percentile_cont_udaf);
48
49/// Computes the approximate percentile continuous of a set of numbers
50pub fn approx_percentile_cont(
51    order_by: Sort,
52    percentile: Expr,
53    centroids: Option<Expr>,
54) -> Expr {
55    let expr = order_by.expr.clone();
56
57    let args = if let Some(centroids) = centroids {
58        vec![expr, percentile, centroids]
59    } else {
60        vec![expr, percentile]
61    };
62
63    Expr::AggregateFunction(AggregateFunction::new_udf(
64        approx_percentile_cont_udaf(),
65        args,
66        false,
67        None,
68        vec![order_by],
69        None,
70    ))
71}
72
73#[user_doc(
74    doc_section(label = "Approximate Functions"),
75    description = "Returns the approximate percentile of input values using the t-digest algorithm.",
76    syntax_example = "approx_percentile_cont(percentile [, centroids]) WITHIN GROUP (ORDER BY expression)",
77    sql_example = r#"```sql
78> SELECT approx_percentile_cont(0.75) WITHIN GROUP (ORDER BY column_name) FROM table_name;
79+------------------------------------------------------------------+
80| approx_percentile_cont(0.75) WITHIN GROUP (ORDER BY column_name) |
81+------------------------------------------------------------------+
82| 65.0                                                             |
83+------------------------------------------------------------------+
84> SELECT approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) FROM table_name;
85+-----------------------------------------------------------------------+
86| approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) |
87+-----------------------------------------------------------------------+
88| 65.0                                                                  |
89+-----------------------------------------------------------------------+
90```
91An alternate syntax is also supported:
92```sql
93> SELECT approx_percentile_cont(column_name, 0.75) FROM table_name;
94+-----------------------------------------------+
95| approx_percentile_cont(column_name, 0.75)     |
96+-----------------------------------------------+
97| 65.0                                          |
98+-----------------------------------------------+
99
100> SELECT approx_percentile_cont(column_name, 0.75, 100) FROM table_name;
101+----------------------------------------------------------+
102| approx_percentile_cont(column_name, 0.75, 100)           |
103+----------------------------------------------------------+
104| 65.0                                                     |
105+----------------------------------------------------------+
106```
107"#,
108    standard_argument(name = "expression",),
109    argument(
110        name = "percentile",
111        description = "Percentile to compute. Must be a float value between 0 and 1 (inclusive)."
112    ),
113    argument(
114        name = "centroids",
115        description = "Number of centroids to use in the t-digest algorithm. _Default is 100_. A higher number results in more accurate approximation but requires more memory."
116    )
117)]
118#[derive(Debug, PartialEq, Eq, Hash)]
119pub struct ApproxPercentileCont {
120    signature: Signature,
121}
122
123impl Default for ApproxPercentileCont {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl ApproxPercentileCont {
130    /// Create a new [`ApproxPercentileCont`] aggregate function.
131    pub fn new() -> Self {
132        // Accept any numeric value paired with a float64 percentile
133        let signature = Signature::one_of(
134            vec![
135                // 2 args - numeric, percentile (float)
136                TypeSignature::Coercible(vec![
137                    Coercion::new_implicit(
138                        TypeSignatureClass::Float,
139                        vec![TypeSignatureClass::Numeric],
140                        NativeType::Float64,
141                    ),
142                    Coercion::new_implicit(
143                        TypeSignatureClass::Native(logical_float64()),
144                        vec![TypeSignatureClass::Numeric],
145                        NativeType::Float64,
146                    ),
147                ]),
148                // 3 args - numeric, percentile (float), number of centroid for T-Digest (integer)
149                TypeSignature::Coercible(vec![
150                    Coercion::new_implicit(
151                        TypeSignatureClass::Float,
152                        vec![TypeSignatureClass::Numeric],
153                        NativeType::Float64,
154                    ),
155                    Coercion::new_implicit(
156                        TypeSignatureClass::Native(logical_float64()),
157                        vec![TypeSignatureClass::Numeric],
158                        NativeType::Float64,
159                    ),
160                    Coercion::new_implicit(
161                        TypeSignatureClass::Integer,
162                        vec![TypeSignatureClass::Numeric],
163                        NativeType::Int64,
164                    ),
165                ]),
166            ],
167            Volatility::Immutable,
168        );
169        Self { signature }
170    }
171
172    pub(crate) fn create_accumulator(
173        &self,
174        args: &AccumulatorArgs,
175    ) -> Result<ApproxPercentileAccumulator> {
176        let percentile =
177            validate_percentile_expr(&args.exprs[1], "APPROX_PERCENTILE_CONT")?;
178
179        let is_descending = args
180            .order_bys
181            .first()
182            .map(|sort_expr| sort_expr.options.descending)
183            .unwrap_or(false);
184
185        let percentile = if is_descending {
186            1.0 - percentile
187        } else {
188            percentile
189        };
190
191        let tdigest_max_size = if args.exprs.len() == 3 {
192            Some(validate_input_max_size_expr(&args.exprs[2])?)
193        } else {
194            None
195        };
196
197        let data_type = args.expr_fields[0].data_type();
198        let accumulator: ApproxPercentileAccumulator = match data_type {
199            DataType::Float16 | DataType::Float32 | DataType::Float64 => {
200                if let Some(max_size) = tdigest_max_size {
201                    ApproxPercentileAccumulator::new_with_max_size(
202                        percentile,
203                        data_type.clone(),
204                        max_size,
205                    )
206                } else {
207                    ApproxPercentileAccumulator::new(percentile, data_type.clone())
208                }
209            }
210            other => {
211                return not_impl_err!(
212                    "Support for 'APPROX_PERCENTILE_CONT' for data type {other} is not implemented"
213                );
214            }
215        };
216
217        Ok(accumulator)
218    }
219}
220
221fn validate_input_max_size_expr(expr: &Arc<dyn PhysicalExpr>) -> Result<usize> {
222    let scalar_value = get_scalar_value(expr).map_err(|_e| {
223        DataFusionError::Plan(
224            "Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be a literal"
225                .to_string(),
226        )
227    })?;
228
229    let max_size = match scalar_value {
230        ScalarValue::UInt8(Some(q)) => q as usize,
231        ScalarValue::UInt16(Some(q)) => q as usize,
232        ScalarValue::UInt32(Some(q)) => q as usize,
233        ScalarValue::UInt64(Some(q)) => q as usize,
234        ScalarValue::Int32(Some(q)) if q > 0 => q as usize,
235        ScalarValue::Int64(Some(q)) if q > 0 => q as usize,
236        ScalarValue::Int16(Some(q)) if q > 0 => q as usize,
237        ScalarValue::Int8(Some(q)) if q > 0 => q as usize,
238        sv => {
239            return plan_err!(
240                "Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be UInt > 0 literal (got data type {}).",
241                sv.data_type()
242            );
243        }
244    };
245
246    Ok(max_size)
247}
248
249impl AggregateUDFImpl for ApproxPercentileCont {
250    /// See [`TDigest::to_scalar_state()`] for a description of the serialized
251    /// state.
252    fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
253        Ok(vec![
254            Field::new(
255                format_state_name(args.name, "max_size"),
256                DataType::UInt64,
257                false,
258            ),
259            Field::new(
260                format_state_name(args.name, "sum"),
261                DataType::Float64,
262                false,
263            ),
264            Field::new(
265                format_state_name(args.name, "count"),
266                DataType::Float64,
267                false,
268            ),
269            Field::new(
270                format_state_name(args.name, "max"),
271                DataType::Float64,
272                false,
273            ),
274            Field::new(
275                format_state_name(args.name, "min"),
276                DataType::Float64,
277                false,
278            ),
279            Field::new_list(
280                format_state_name(args.name, "centroids"),
281                Field::new_list_field(DataType::Float64, true),
282                false,
283            ),
284        ]
285        .into_iter()
286        .map(Arc::new)
287        .collect())
288    }
289
290    fn name(&self) -> &str {
291        "approx_percentile_cont"
292    }
293
294    fn signature(&self) -> &Signature {
295        &self.signature
296    }
297
298    #[inline]
299    fn accumulator(&self, acc_args: AccumulatorArgs) -> Result<Box<dyn Accumulator>> {
300        Ok(Box::new(self.create_accumulator(&acc_args)?))
301    }
302
303    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
304        if !arg_types[0].is_numeric() {
305            return plan_err!("approx_percentile_cont requires numeric input types");
306        }
307        if arg_types.len() == 3 && !arg_types[2].is_integer() {
308            return plan_err!(
309                "approx_percentile_cont requires integer centroids input types"
310            );
311        }
312        Ok(arg_types[0].clone())
313    }
314
315    fn supports_within_group_clause(&self) -> bool {
316        true
317    }
318
319    fn documentation(&self) -> Option<&Documentation> {
320        self.doc()
321    }
322}
323
324#[derive(Debug)]
325pub struct ApproxPercentileAccumulator {
326    digest: TDigest,
327    percentile: f64,
328    return_type: DataType,
329}
330
331impl ApproxPercentileAccumulator {
332    pub fn new(percentile: f64, return_type: DataType) -> Self {
333        Self {
334            digest: TDigest::new(DEFAULT_MAX_SIZE),
335            percentile,
336            return_type,
337        }
338    }
339
340    pub fn new_with_max_size(
341        percentile: f64,
342        return_type: DataType,
343        max_size: usize,
344    ) -> Self {
345        Self {
346            digest: TDigest::new(max_size),
347            percentile,
348            return_type,
349        }
350    }
351
352    // pub(crate) for approx_percentile_cont_with_weight
353    pub(crate) fn max_size(&self) -> usize {
354        self.digest.max_size()
355    }
356
357    // pub(crate) for approx_percentile_cont_with_weight
358    pub(crate) fn merge_digests(&mut self, digests: &[TDigest]) {
359        let digests = digests.iter().chain(std::iter::once(&self.digest));
360        self.digest = TDigest::merge_digests(digests)
361    }
362
363    // pub(crate) for approx_percentile_cont_with_weight
364    pub(crate) fn convert_to_float(values: &ArrayRef) -> Result<Vec<f64>> {
365        debug_assert!(
366            values.null_count() == 0,
367            "convert_to_float assumes nulls have already been filtered out"
368        );
369        match values.data_type() {
370            DataType::Float64 => {
371                let array = downcast_value!(values, Float64Array);
372                Ok(array.values().iter().copied().collect::<Vec<_>>())
373            }
374            DataType::Float32 => {
375                let array = downcast_value!(values, Float32Array);
376                Ok(array.values().iter().map(|v| *v as f64).collect::<Vec<_>>())
377            }
378            DataType::Float16 => {
379                let array = downcast_value!(values, Float16Array);
380                Ok(array
381                    .values()
382                    .iter()
383                    .map(|v| v.to_f64())
384                    .collect::<Vec<_>>())
385            }
386            e => internal_err!(
387                "APPROX_PERCENTILE_CONT is not expected to receive the type {e:?}"
388            ),
389        }
390    }
391}
392
393impl Accumulator for ApproxPercentileAccumulator {
394    fn state(&mut self) -> Result<Vec<ScalarValue>> {
395        Ok(self.digest.to_scalar_state().into_iter().collect())
396    }
397
398    fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
399        // Remove any nulls before computing the percentile
400        let mut values = Arc::clone(&values[0]);
401        if values.null_count() > 0 {
402            values = filter(&values, &is_not_null(&values)?)?;
403        }
404        let sorted_values = &arrow::compute::sort(&values, None)?;
405        let sorted_values = ApproxPercentileAccumulator::convert_to_float(sorted_values)?;
406        self.digest = self.digest.merge_sorted_f64(&sorted_values);
407        Ok(())
408    }
409
410    fn evaluate(&mut self) -> Result<ScalarValue> {
411        if self.digest.count() == 0.0 {
412            return ScalarValue::try_from(self.return_type.clone());
413        }
414        let q = self.digest.estimate_quantile(self.percentile);
415
416        // These acceptable return types MUST match the validation in
417        // ApproxPercentile::create_accumulator.
418        Ok(match &self.return_type {
419            DataType::Float16 => ScalarValue::Float16(Some(half::f16::from_f64(q))),
420            DataType::Float32 => ScalarValue::Float32(Some(q as f32)),
421            DataType::Float64 => ScalarValue::Float64(Some(q)),
422            v => unreachable!("unexpected return type {}", v),
423        })
424    }
425
426    fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
427        if states.is_empty() {
428            return Ok(());
429        }
430
431        let states = (0..states[0].len())
432            .map(|index| {
433                states
434                    .iter()
435                    .map(|array| ScalarValue::try_from_array(array, index))
436                    .collect::<Result<Vec<_>>>()
437                    .map(|state| TDigest::from_scalar_state(&state))
438            })
439            .collect::<Result<Vec<_>>>()?;
440
441        self.merge_digests(&states);
442
443        Ok(())
444    }
445
446    fn size(&self) -> usize {
447        size_of_val(self) + self.digest.size() - size_of_val(&self.digest)
448            + self.return_type.size()
449            - size_of_val(&self.return_type)
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use arrow::datatypes::DataType;
456
457    use datafusion_functions_aggregate_common::tdigest::TDigest;
458
459    use crate::approx_percentile_cont::ApproxPercentileAccumulator;
460
461    #[test]
462    fn test_combine_approx_percentile_accumulator() {
463        let mut digests: Vec<TDigest> = Vec::new();
464
465        // one TDigest with 50_000 values from 1 to 1_000
466        for _ in 1..=50 {
467            let t = TDigest::new(100);
468            let values: Vec<_> = (1..=1_000).map(f64::from).collect();
469            let t = t.merge_unsorted_f64(values);
470            digests.push(t)
471        }
472
473        let t1 = TDigest::merge_digests(&digests);
474        let t2 = TDigest::merge_digests(&digests);
475
476        let mut accumulator =
477            ApproxPercentileAccumulator::new_with_max_size(0.5, DataType::Float64, 100);
478
479        accumulator.merge_digests(&[t1]);
480        assert_eq!(accumulator.digest.count(), 50_000.0);
481        accumulator.merge_digests(&[t2]);
482        assert_eq!(accumulator.digest.count(), 100_000.0);
483    }
484}