Skip to main content

datafusion_functions_aggregate/
approx_percentile_cont_with_weight.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::hash::Hash;
20use std::mem::size_of_val;
21use std::sync::Arc;
22
23use arrow::compute::{and, filter, is_not_null};
24use arrow::datatypes::FieldRef;
25use arrow::{array::ArrayRef, datatypes::DataType};
26use datafusion_common::ScalarValue;
27use datafusion_common::types::{NativeType, logical_float64};
28use datafusion_common::{Result, not_impl_err, plan_err};
29use datafusion_expr::expr::{AggregateFunction, Sort};
30use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs};
31use datafusion_expr::{
32    Accumulator, AggregateUDFImpl, Coercion, Documentation, Expr, Signature,
33    TypeSignature, TypeSignatureClass, Volatility,
34};
35use datafusion_functions_aggregate_common::tdigest::{Centroid, TDigest};
36use datafusion_macros::user_doc;
37
38use crate::approx_percentile_cont::{ApproxPercentileAccumulator, ApproxPercentileCont};
39
40create_func!(
41    ApproxPercentileContWithWeight,
42    approx_percentile_cont_with_weight_udaf
43);
44
45/// Computes the approximate percentile continuous with weight of a set of numbers
46pub fn approx_percentile_cont_with_weight(
47    order_by: Sort,
48    weight: Expr,
49    percentile: Expr,
50    centroids: Option<Expr>,
51) -> Expr {
52    let expr = order_by.expr.clone();
53
54    let args = if let Some(centroids) = centroids {
55        vec![expr, weight, percentile, centroids]
56    } else {
57        vec![expr, weight, percentile]
58    };
59
60    Expr::AggregateFunction(AggregateFunction::new_udf(
61        approx_percentile_cont_with_weight_udaf(),
62        args,
63        false,
64        None,
65        vec![order_by],
66        None,
67    ))
68}
69
70/// APPROX_PERCENTILE_CONT_WITH_WEIGHT aggregate expression
71#[user_doc(
72    doc_section(label = "Approximate Functions"),
73    description = "Returns the weighted approximate percentile of input values using the t-digest algorithm.",
74    syntax_example = "approx_percentile_cont_with_weight(weight, percentile [, centroids]) WITHIN GROUP (ORDER BY expression)",
75    sql_example = r#"```sql
76> SELECT approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) FROM table_name;
77+---------------------------------------------------------------------------------------------+
78| approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) |
79+---------------------------------------------------------------------------------------------+
80| 78.5                                                                                        |
81+---------------------------------------------------------------------------------------------+
82> SELECT approx_percentile_cont_with_weight(weight_column, 0.90, 100) WITHIN GROUP (ORDER BY column_name) FROM table_name;
83+--------------------------------------------------------------------------------------------------+
84| approx_percentile_cont_with_weight(weight_column, 0.90, 100) WITHIN GROUP (ORDER BY column_name) |
85+--------------------------------------------------------------------------------------------------+
86| 78.5                                                                                             |
87+--------------------------------------------------------------------------------------------------+
88```
89An alternative syntax is also supported:
90
91```sql
92> SELECT approx_percentile_cont_with_weight(column_name, weight_column, 0.90) FROM table_name;
93+--------------------------------------------------+
94| approx_percentile_cont_with_weight(column_name, weight_column, 0.90) |
95+--------------------------------------------------+
96| 78.5                                             |
97+--------------------------------------------------+
98```"#,
99    standard_argument(name = "expression", prefix = "The"),
100    argument(
101        name = "weight",
102        description = "Expression to use as weight. Can be a constant, column, or function, and any combination of arithmetic operators."
103    ),
104    argument(
105        name = "percentile",
106        description = "Percentile to compute. Must be a float value between 0 and 1 (inclusive)."
107    ),
108    argument(
109        name = "centroids",
110        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."
111    )
112)]
113#[derive(PartialEq, Eq, Hash, Debug)]
114pub struct ApproxPercentileContWithWeight {
115    signature: Signature,
116    approx_percentile_cont: ApproxPercentileCont,
117}
118
119impl Default for ApproxPercentileContWithWeight {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl ApproxPercentileContWithWeight {
126    /// Create a new [`ApproxPercentileContWithWeight`] aggregate function.
127    pub fn new() -> Self {
128        let signature = Signature::one_of(
129            vec![
130                // 3 args - numeric, weight (float), percentile (float)
131                TypeSignature::Coercible(vec![
132                    Coercion::new_implicit(
133                        TypeSignatureClass::Float,
134                        vec![TypeSignatureClass::Numeric],
135                        NativeType::Float64,
136                    ),
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                // 4 args - numeric, weight (float), percentile (float), centroid (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::Float,
157                        vec![TypeSignatureClass::Numeric],
158                        NativeType::Float64,
159                    ),
160                    Coercion::new_implicit(
161                        TypeSignatureClass::Native(logical_float64()),
162                        vec![TypeSignatureClass::Numeric],
163                        NativeType::Float64,
164                    ),
165                    Coercion::new_implicit(
166                        TypeSignatureClass::Integer,
167                        vec![TypeSignatureClass::Numeric],
168                        NativeType::Int64,
169                    ),
170                ]),
171            ],
172            Volatility::Immutable,
173        );
174        Self {
175            signature,
176            approx_percentile_cont: ApproxPercentileCont::new(),
177        }
178    }
179}
180
181impl AggregateUDFImpl for ApproxPercentileContWithWeight {
182    fn name(&self) -> &str {
183        "approx_percentile_cont_with_weight"
184    }
185
186    fn signature(&self) -> &Signature {
187        &self.signature
188    }
189
190    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
191        if !arg_types[0].is_numeric() {
192            return plan_err!(
193                "approx_percentile_cont_with_weight requires numeric input types"
194            );
195        }
196        if !arg_types[1].is_numeric() {
197            return plan_err!(
198                "approx_percentile_cont_with_weight requires numeric weight input types"
199            );
200        }
201        if arg_types[2] != DataType::Float64 {
202            return plan_err!(
203                "approx_percentile_cont_with_weight requires float64 percentile input types"
204            );
205        }
206        if arg_types.len() == 4 && !arg_types[3].is_integer() {
207            return plan_err!(
208                "approx_percentile_cont_with_weight requires integer centroids input types"
209            );
210        }
211        Ok(arg_types[0].clone())
212    }
213
214    fn accumulator(&self, acc_args: AccumulatorArgs) -> Result<Box<dyn Accumulator>> {
215        if acc_args.is_distinct {
216            return not_impl_err!(
217                "approx_percentile_cont_with_weight(DISTINCT) aggregations are not available"
218            );
219        }
220
221        if acc_args.exprs.len() != 3 && acc_args.exprs.len() != 4 {
222            return plan_err!(
223                "approx_percentile_cont_with_weight requires three or four arguments: value, weight, percentile[, centroids]"
224            );
225        }
226
227        let sub_args = AccumulatorArgs {
228            exprs: if acc_args.exprs.len() == 4 {
229                &[
230                    Arc::clone(&acc_args.exprs[0]), // value
231                    Arc::clone(&acc_args.exprs[2]), // percentile
232                    Arc::clone(&acc_args.exprs[3]), // centroids
233                ]
234            } else {
235                &[
236                    Arc::clone(&acc_args.exprs[0]), // value
237                    Arc::clone(&acc_args.exprs[2]), // percentile
238                ]
239            },
240            expr_fields: if acc_args.exprs.len() == 4 {
241                &[
242                    Arc::clone(&acc_args.expr_fields[0]), // value
243                    Arc::clone(&acc_args.expr_fields[2]), // percentile
244                    Arc::clone(&acc_args.expr_fields[3]), // centroids
245                ]
246            } else {
247                &[
248                    Arc::clone(&acc_args.expr_fields[0]), // value
249                    Arc::clone(&acc_args.expr_fields[2]), // percentile
250                ]
251            },
252            // Unchanged below; we list each field explicitly in case we ever add more
253            // fields to AccumulatorArgs making it easier to see if changes are also
254            // needed here.
255            return_field: acc_args.return_field,
256            schema: acc_args.schema,
257            ignore_nulls: acc_args.ignore_nulls,
258            order_bys: acc_args.order_bys,
259            is_reversed: acc_args.is_reversed,
260            name: acc_args.name,
261            is_distinct: acc_args.is_distinct,
262        };
263        let approx_percentile_cont_accumulator =
264            self.approx_percentile_cont.create_accumulator(&sub_args)?;
265        let accumulator = ApproxPercentileWithWeightAccumulator::new(
266            approx_percentile_cont_accumulator,
267        );
268        Ok(Box::new(accumulator))
269    }
270
271    /// See [`TDigest::to_scalar_state()`] for a description of the serialized
272    /// state.
273    fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
274        self.approx_percentile_cont.state_fields(args)
275    }
276
277    fn supports_within_group_clause(&self) -> bool {
278        true
279    }
280
281    fn documentation(&self) -> Option<&Documentation> {
282        self.doc()
283    }
284}
285
286#[derive(Debug)]
287pub struct ApproxPercentileWithWeightAccumulator {
288    approx_percentile_cont_accumulator: ApproxPercentileAccumulator,
289}
290
291impl ApproxPercentileWithWeightAccumulator {
292    pub fn new(approx_percentile_cont_accumulator: ApproxPercentileAccumulator) -> Self {
293        Self {
294            approx_percentile_cont_accumulator,
295        }
296    }
297}
298
299impl Accumulator for ApproxPercentileWithWeightAccumulator {
300    fn state(&mut self) -> Result<Vec<ScalarValue>> {
301        self.approx_percentile_cont_accumulator.state()
302    }
303
304    fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
305        let mut means = Arc::clone(&values[0]);
306        let mut weights = Arc::clone(&values[1]);
307        // If nulls are present in either array, need to filter those rows out in both arrays
308        match (means.null_count() > 0, weights.null_count() > 0) {
309            // Both have nulls
310            (true, true) => {
311                let predicate = and(&is_not_null(&means)?, &is_not_null(&weights)?)?;
312                means = filter(&means, &predicate)?;
313                weights = filter(&weights, &predicate)?;
314            }
315            // Only one has nulls
316            (false, true) => {
317                let predicate = &is_not_null(&weights)?;
318                means = filter(&means, predicate)?;
319                weights = filter(&weights, predicate)?;
320            }
321            (true, false) => {
322                let predicate = &is_not_null(&means)?;
323                means = filter(&means, predicate)?;
324                weights = filter(&weights, predicate)?;
325            }
326            // No nulls
327            (false, false) => {}
328        }
329        debug_assert_eq!(
330            means.len(),
331            weights.len(),
332            "invalid number of values in means and weights"
333        );
334        let means_f64 = ApproxPercentileAccumulator::convert_to_float(&means)?;
335        let weights_f64 = ApproxPercentileAccumulator::convert_to_float(&weights)?;
336        let mut digests: Vec<TDigest> = vec![];
337        for (mean, weight) in means_f64.iter().zip(weights_f64.iter()) {
338            digests.push(TDigest::new_with_centroid(
339                self.approx_percentile_cont_accumulator.max_size(),
340                Centroid::new(*mean, *weight),
341            ))
342        }
343        self.approx_percentile_cont_accumulator
344            .merge_digests(&digests);
345        Ok(())
346    }
347
348    fn evaluate(&mut self) -> Result<ScalarValue> {
349        self.approx_percentile_cont_accumulator.evaluate()
350    }
351
352    fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
353        self.approx_percentile_cont_accumulator
354            .merge_batch(states)?;
355
356        Ok(())
357    }
358
359    fn size(&self) -> usize {
360        size_of_val(self) - size_of_val(&self.approx_percentile_cont_accumulator)
361            + self.approx_percentile_cont_accumulator.size()
362    }
363}