1use num_traits::AsPrimitive;
2use polars_compute::rolling::QuantileMethod;
3#[cfg(feature = "dtype-f16")]
4use polars_utils::float16::pf16;
5
6use super::*;
7
8pub trait QuantileAggSeries {
9 fn median_reduce(&self) -> Scalar;
11
12 fn quantile_reduce(&self, quantile: f64, method: QuantileMethod) -> PolarsResult<Scalar>;
14
15 fn quantiles_reduce(&self, _quantiles: &[f64], _method: QuantileMethod)
17 -> PolarsResult<Scalar>;
18}
19
20pub fn quantile_indices(
26 n_nonnull: usize,
27 quantile: f64,
28 method: QuantileMethod,
29) -> (usize, usize, f64) {
30 assert!(n_nonnull > 0);
31 let float_idx = (n_nonnull as f64 - 1.0) * quantile;
32 let floor_idx = float_idx as usize;
33 let ceil_idx = (float_idx.ceil() as usize).min(n_nonnull - 1);
34 match method {
35 QuantileMethod::Nearest => {
36 let idx = float_idx.round() as usize;
37 (idx, idx, 0.0)
38 },
39 QuantileMethod::Lower => (floor_idx, floor_idx, 0.0),
40 QuantileMethod::Higher => (ceil_idx, ceil_idx, 0.0),
41 QuantileMethod::Midpoint | QuantileMethod::Linear => {
42 (floor_idx, ceil_idx, float_idx - floor_idx as f64)
43 },
44 QuantileMethod::Equiprobable => {
45 let idx = ((n_nonnull as f64 * quantile).ceil() - 1.0).max(0.0) as usize;
46 (idx, idx, 0.0)
47 },
48 }
49}
50
51pub fn extract_quantiles(quantile: &Series) -> PolarsResult<Vec<f64>> {
53 polars_ensure!(!quantile.is_empty(), ComputeError:
54 "the 'quantile' expression input should produce a single quantile or a list of quantiles, \
55 got an empty input"
56 );
57 polars_ensure!(quantile.len() == 1, ComputeError:
58 "polars does not support varying quantiles yet, \
59 make sure the 'quantile' expression input produces a single quantile or a list of quantiles"
60 );
61 let quantiles = match quantile.dtype() {
62 DataType::List(_) => {
63 let inner = quantile.list()?.get_as_series(0).ok_or_else(
64 || polars_err!(ComputeError: "quantile expression contains null values"),
65 )?;
66 polars_ensure!(!inner.has_nulls(), ComputeError: "quantile expression contains null values");
67 inner
68 .cast(&DataType::Float64)?
69 .f64()?
70 .into_no_null_iter()
71 .collect()
72 },
73 _ => vec![quantile.get(0)?.try_extract()?],
74 };
75 for &q in &quantiles {
76 polars_ensure!(
77 (0.0..=1.0).contains(&q),
78 ComputeError: "`quantile` should be between 0.0 and 1.0, got {}", q,
79 );
80 }
81 Ok(quantiles)
82}
83
84fn quantile_idx(
86 quantile: f64,
87 length: usize,
88 null_count: usize,
89 method: QuantileMethod,
90) -> (usize, f64, usize) {
91 let (lower, upper, fraction) = quantile_indices(length - null_count, quantile, method);
92 (lower + null_count, fraction, upper + null_count)
93}
94
95fn linear_interpol<T: Float>(lower: T, upper: T, fraction: f64) -> T {
97 if lower == upper {
98 lower
99 } else {
100 let proportion: T = T::from(fraction).unwrap();
101 proportion * (upper - lower) + lower
102 }
103}
104fn midpoint_interpol<T: Float>(lower: T, upper: T) -> T {
105 if lower == upper {
106 lower
107 } else {
108 (lower + upper) / (T::one() + T::one())
109 }
110}
111
112fn quantile_slice<T: ToPrimitive + TotalOrd + Copy>(
117 vals: &mut [T],
118 quantile: f64,
119 method: QuantileMethod,
120) -> PolarsResult<Option<f64>> {
121 polars_ensure!((0.0..=1.0).contains(&quantile),
122 ComputeError: "quantile should be between 0.0 and 1.0, got {}", quantile,
123 );
124 if vals.is_empty() {
125 return Ok(None);
126 }
127 if vals.len() == 1 {
128 return Ok(vals[0].to_f64());
129 }
130 let (idx, fraction, top_idx) = quantile_idx(quantile, vals.len(), 0, method);
131
132 let (_lhs, lower, rhs) = vals.select_nth_unstable_by(idx, TotalOrd::tot_cmp);
133 if idx == top_idx {
134 Ok(lower.to_f64())
135 } else {
136 match method {
137 QuantileMethod::Midpoint => {
138 let upper = rhs.iter().copied().min_by(TotalOrd::tot_cmp).unwrap();
139 Ok(Some(midpoint_interpol(
140 lower.to_f64().unwrap(),
141 upper.to_f64().unwrap(),
142 )))
143 },
144 QuantileMethod::Linear => {
145 let upper = rhs.iter().copied().min_by(TotalOrd::tot_cmp).unwrap();
146 Ok(
147 linear_interpol(lower.to_f64().unwrap(), upper.to_f64().unwrap(), fraction)
148 .to_f64(),
149 )
150 },
151 _ => Ok(lower.to_f64()),
152 }
153 }
154}
155
156fn quantiles_slice<T: ToPrimitive + TotalOrd + Copy>(
162 vals: &mut [T],
163 quantiles: &[f64],
164 method: QuantileMethod,
165) -> PolarsResult<Vec<Option<f64>>> {
166 for &q in quantiles {
168 polars_ensure!(
169 (0.0..=1.0).contains(&q),
170 ComputeError: "quantile should be between 0.0 and 1.0, got {}", q
171 );
172 }
173
174 if vals.is_empty() {
175 return Ok(vec![None; quantiles.len()]);
176 }
177 if vals.len() == 1 {
178 let v = vals[0].to_f64();
179 return Ok(vec![v; quantiles.len()]);
180 }
181
182 vals.sort_by(TotalOrd::tot_cmp);
184 let n = vals.len();
185
186 let mut out = Vec::with_capacity(quantiles.len());
187
188 for &q in quantiles {
189 let (idx, fraction, top_idx) = quantile_idx(q, n, 0, method);
190
191 let lower = vals[idx].to_f64().unwrap();
193
194 let opt = match method {
195 QuantileMethod::Midpoint => {
196 if top_idx == idx {
197 Some(lower)
198 } else {
199 let upper = vals[idx + 1].to_f64().unwrap();
200 midpoint_interpol(lower, upper).to_f64()
201 }
202 },
203 QuantileMethod::Linear => {
204 if top_idx == idx {
205 Some(lower)
206 } else {
207 let upper = vals[idx + 1].to_f64().unwrap();
208 linear_interpol(lower, upper, fraction).to_f64()
209 }
210 },
211 _ => Some(lower),
212 };
213
214 out.push(opt);
215 }
216
217 Ok(out)
218}
219
220fn generic_quantiles<T>(
222 ca: ChunkedArray<T>,
223 quantiles: &[f64],
224 method: QuantileMethod,
225) -> PolarsResult<Vec<Option<f64>>>
226where
227 T: PolarsNumericType,
228{
229 for &q in quantiles {
231 polars_ensure!(
232 (0.0..=1.0).contains(&q),
233 ComputeError: "`quantile` should be between 0.0 and 1.0, got {}", q,
234 );
235 }
236
237 let null_count = ca.null_count();
238 let length = ca.len();
239
240 if null_count == length {
241 return Ok(vec![None; quantiles.len()]);
242 }
243
244 let sorted = ca.sort(false);
245 let mut out = Vec::with_capacity(quantiles.len());
246
247 for &q in quantiles {
248 let (idx, fraction, top_idx) = quantile_idx(q, length, null_count, method);
249
250 let lower = sorted.get(idx).map(|v| v.to_f64().unwrap());
251
252 let opt = match method {
253 QuantileMethod::Midpoint => {
254 if top_idx == idx {
255 lower
256 } else {
257 let upper = sorted.get(idx + 1).map(|v| v.to_f64().unwrap());
258 midpoint_interpol(lower.unwrap(), upper.unwrap()).to_f64()
259 }
260 },
261 QuantileMethod::Linear => {
262 if top_idx == idx {
263 lower
264 } else {
265 let upper = sorted.get(idx + 1).map(|v| v.to_f64().unwrap());
266 linear_interpol(lower.unwrap(), upper.unwrap(), fraction).to_f64()
267 }
268 },
269 _ => lower,
270 };
271
272 out.push(opt);
273 }
274
275 Ok(out)
276}
277
278impl<T> ChunkQuantile<f64> for ChunkedArray<T>
279where
280 T: PolarsIntegerType,
281 T::Native: TotalOrd,
282{
283 fn quantile(&self, quantile: f64, method: QuantileMethod) -> PolarsResult<Option<f64>> {
284 if let (Ok(slice), false) = (self.cont_slice(), self.is_sorted_ascending_flag()) {
286 let mut owned = slice.to_vec();
287 quantile_slice(&mut owned, quantile, method)
288 } else {
289 let re_val = generic_quantiles(self.clone(), &[quantile], method)?;
290 Ok(re_val.into_iter().next().unwrap())
291 }
292 }
293
294 fn quantiles(
295 &self,
296 quantiles: &[f64],
297 method: QuantileMethod,
298 ) -> PolarsResult<Vec<Option<f64>>> {
299 if let (Ok(slice), false) = (self.cont_slice(), self.is_sorted_ascending_flag()) {
301 let mut owned = slice.to_vec();
302 quantiles_slice(&mut owned, quantiles, method)
303 } else {
304 generic_quantiles(self.clone(), quantiles, method)
305 }
306 }
307
308 fn median(&self) -> Option<f64> {
309 self.quantile(0.5, QuantileMethod::Linear).unwrap() }
311}
312
313impl<T> ChunkedArray<T>
315where
316 T: PolarsIntegerType,
317 T::Native: TotalOrd,
318{
319 pub(crate) fn quantile_faster(
320 mut self,
321 quantile: f64,
322 method: QuantileMethod,
323 ) -> PolarsResult<Option<f64>> {
324 let is_sorted = self.is_sorted_ascending_flag();
326 if let (Some(slice), false) = (self.cont_slice_mut(), is_sorted) {
327 quantile_slice(slice, quantile, method)
328 } else {
329 self.quantile(quantile, method)
330 }
331 }
332
333 pub(crate) fn median_faster(self) -> Option<f64> {
334 self.quantile_faster(0.5, QuantileMethod::Linear).unwrap()
335 }
336}
337
338macro_rules! impl_chunk_quantile_for_float_chunked {
339 ($T:ty, $CA:ty) => {
340 impl ChunkQuantile<$T> for $CA {
341 fn quantile(&self, quantile: f64, method: QuantileMethod) -> PolarsResult<Option<$T>> {
342 let out = if let (Ok(slice), false) =
344 (self.cont_slice(), self.is_sorted_ascending_flag())
345 {
346 let mut owned = slice.to_vec();
347 quantile_slice(&mut owned, quantile, method)
348 } else {
349 let re_val = generic_quantiles(self.clone(), &[quantile], method)?;
350 Ok(re_val.into_iter().next().unwrap())
351 };
352 out.map(|v| v.map(|v| v.as_()))
353 }
354
355 fn quantiles(
356 &self,
357 quantiles: &[f64],
358 method: QuantileMethod,
359 ) -> PolarsResult<Vec<Option<$T>>> {
360 let out = if let (Ok(slice), false) =
362 (self.cont_slice(), self.is_sorted_ascending_flag())
363 {
364 let mut owned = slice.to_vec();
365 quantiles_slice(&mut owned, quantiles, method)
366 } else {
367 generic_quantiles(self.clone(), quantiles, method)
368 };
369
370 out.map(|vec_t| {
371 vec_t
372 .into_iter()
373 .map(|opt| opt.map(|v| AsPrimitive::<$T>::as_(v)))
374 .collect::<Vec<Option<$T>>>()
375 })
376 }
377
378 fn median(&self) -> Option<$T> {
379 self.quantile(0.5, QuantileMethod::Linear).unwrap() }
381 }
382 };
383}
384
385#[cfg(feature = "dtype-f16")]
386impl_chunk_quantile_for_float_chunked!(pf16, Float16Chunked);
387impl_chunk_quantile_for_float_chunked!(f32, Float32Chunked);
388impl_chunk_quantile_for_float_chunked!(f64, Float64Chunked);
389
390macro_rules! impl_float_chunked {
391 ($T:ty, $CA:ty) => {
392 impl $CA {
393 pub(crate) fn quantile_faster(
394 mut self,
395 quantile: f64,
396 method: QuantileMethod,
397 ) -> PolarsResult<Option<$T>> {
398 let is_sorted = self.is_sorted_ascending_flag();
400 if let (Some(slice), false) = (self.cont_slice_mut(), is_sorted) {
401 Ok(quantile_slice(slice, quantile, method)?.map(AsPrimitive::as_))
402 } else {
403 Ok(self.quantile(quantile, method)?.map(AsPrimitive::as_))
404 }
405 }
406
407 pub(crate) fn median_faster(self) -> Option<$T> {
408 self.quantile_faster(0.5.into(), QuantileMethod::Linear)
409 .unwrap()
410 }
411 }
412 };
413}
414
415#[cfg(feature = "dtype-f16")]
416impl_float_chunked!(pf16, Float16Chunked);
417impl_float_chunked!(f32, Float32Chunked);
418impl_float_chunked!(f64, Float64Chunked);
419
420impl ChunkQuantile<String> for StringChunked {}
421impl ChunkQuantile<Series> for ListChunked {}
422#[cfg(feature = "dtype-array")]
423impl ChunkQuantile<Series> for ArrayChunked {}
424#[cfg(feature = "object")]
425impl<T: PolarsObject> ChunkQuantile<Series> for ObjectChunked<T> {}
426impl ChunkQuantile<bool> for BooleanChunked {}