Skip to main content

polars_core/frame/group_by/aggregations/
boolean.rs

1use polars_arrow::bitmap::and_not;
2use polars_arrow::bitmap::bitmask::BitMask;
3
4use super::*;
5use crate::chunked_array::cast::CastOptions;
6use crate::chunked_array::from_iterator_par::{collect_bool_opt_par, collect_bool_par};
7
8pub fn _agg_helper_idx_bool<F>(groups: &GroupsIdx, f: F) -> Series
9where
10    F: Fn((IdxSize, &IdxVec)) -> Option<bool> + Send + Sync,
11{
12    let ca: BooleanChunked = RAYON.install(|| {
13        let groups_len = groups.len();
14        let first = groups.first();
15        let all = groups.all();
16        collect_bool_opt_par(groups_len, |g| f((first[g], &all[g])))
17    });
18    ca.into_series()
19}
20
21pub fn _agg_helper_slice_bool<F>(groups: &[[IdxSize; 2]], f: F) -> Series
22where
23    F: Fn([IdxSize; 2]) -> Option<bool> + Send + Sync,
24{
25    let ca: BooleanChunked = RAYON.install(|| collect_bool_opt_par(groups.len(), |g| f(groups[g])));
26    ca.into_series()
27}
28
29/// For each group, the position of the first valid value equal to `target`, or else of the first
30/// valid value. This is the arg min (`target = false`) or arg max (`target = true`) of the group.
31///
32/// # Safety
33/// The groups must be in bounds of `arr`.
34unsafe fn arg_first_in_slices(arr: &BooleanArray, groups: &[[IdxSize; 2]], target: bool) -> Series {
35    if arr.null_count() == arr.len() {
36        return IdxCa::full_null(PlSmallStr::EMPTY, groups.len()).into_series();
37    }
38    // Without nulls the values mark the targets, and a search for false counts the leading ones.
39    let valid_targets;
40    let (targets, find_false) = match arr.validity() {
41        None => (BitMask::from_bitmap(arr.values()), !target),
42        Some(validity) => {
43            valid_targets = if target {
44                arr.values() & validity
45            } else {
46                and_not(validity, arr.values())
47            };
48            (BitMask::from_bitmap(&valid_targets), false)
49        },
50    };
51    let validity = arr.validity().map(BitMask::from_bitmap);
52    // SAFETY: The groups are in bounds.
53    _agg_helper_slice::<IdxType, _>(groups, |[first, len]| {
54        let (first, len) = (first as usize, len as usize);
55        if len == 1 {
56            return validity.is_none_or(|v| v.get(first)).then_some(0);
57        }
58        let window = unsafe { targets.sliced_unchecked(first, len) };
59        let i = if find_false {
60            window.leading_ones()
61        } else {
62            window.leading_zeros()
63        };
64        if i < len {
65            return Some(i as IdxSize);
66        }
67        let i = validity.map_or(0, |v| {
68            unsafe { v.sliced_unchecked(first, len) }.leading_zeros()
69        });
70        (i < len).then_some(i as IdxSize)
71    })
72}
73
74#[cfg(feature = "bitwise")]
75impl BooleanChunked {
76    pub(crate) unsafe fn agg_and(&self, groups: &GroupsType) -> BooleanChunked {
77        self.agg_all(groups, true)
78    }
79
80    pub(crate) unsafe fn agg_or(&self, groups: &GroupsType) -> BooleanChunked {
81        self.agg_any(groups, true)
82    }
83
84    pub(crate) unsafe fn agg_xor(&self, groups: &GroupsType) -> BooleanChunked {
85        self.bool_agg(
86            groups,
87            true,
88            |values, idxs| {
89                idxs.iter()
90                    .map(|i| {
91                        <IdxSize as From<bool>>::from(unsafe {
92                            values.get_bit_unchecked(*i as usize)
93                        })
94                    })
95                    .sum::<IdxSize>()
96                    % 2
97                    == 1
98            },
99            |values, validity, idxs| {
100                idxs.iter()
101                    .map(|i| {
102                        <IdxSize as From<bool>>::from(unsafe {
103                            validity.get_bit_unchecked(*i as usize)
104                                & values.get_bit_unchecked(*i as usize)
105                        })
106                    })
107                    .sum::<IdxSize>()
108                    % 2
109                    == 1
110            },
111            |_, _, _| unreachable!(),
112            |values, start, length| {
113                unsafe { values.sliced_unchecked(start as usize, length as usize) }.set_bits() % 2
114                    == 1
115            },
116            |values, validity, start, length| {
117                let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
118                let validity =
119                    unsafe { validity.sliced_unchecked(start as usize, length as usize) };
120                values.num_intersections_with(validity) % 2 == 1
121            },
122            |_, _, _, _| unreachable!(),
123        )
124    }
125}
126
127impl BooleanChunked {
128    pub(crate) unsafe fn agg_min(&self, groups: &GroupsType) -> Series {
129        // faster paths
130        if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
131            match self.is_sorted_flag() {
132                IsSorted::Ascending => {
133                    return self.clone().into_series().agg_first_non_null(groups);
134                },
135                IsSorted::Descending => {
136                    return self.clone().into_series().agg_last_non_null(groups);
137                },
138                _ => {},
139            }
140        }
141        let ca_self = self.rechunk();
142        let arr = ca_self.downcast_iter().next().unwrap();
143        let no_nulls = arr.null_count() == 0;
144        match groups {
145            GroupsType::Idx(groups) => _agg_helper_idx_bool(groups, |(first, idx)| {
146                debug_assert!(idx.len() <= self.len());
147                if idx.is_empty() {
148                    None
149                } else if idx.len() == 1 {
150                    arr.get(first as usize)
151                } else if no_nulls {
152                    take_arg_min_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
153                        .map(|p| arr.value_unchecked(idx[p] as usize))
154                } else {
155                    take_arg_min_bool_iter_unchecked_nulls(arr, idx2usize(idx))
156                        .map(|p| arr.value_unchecked(idx[p] as usize))
157                }
158            }),
159            GroupsType::Slice {
160                groups: groups_slice,
161                ..
162            } => _agg_helper_slice_bool(groups_slice, |[first, len]| {
163                debug_assert!(len <= self.len() as IdxSize);
164                match len {
165                    0 => None,
166                    1 => self.get(first as usize),
167                    _ => {
168                        let arr_group = _slice_from_offsets(self, first, len);
169                        arr_group.min()
170                    },
171                }
172            }),
173        }
174    }
175    pub(crate) unsafe fn agg_max(&self, groups: &GroupsType) -> Series {
176        // faster paths
177        if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
178            match self.is_sorted_flag() {
179                IsSorted::Ascending => return self.clone().into_series().agg_last_non_null(groups),
180                IsSorted::Descending => {
181                    return self.clone().into_series().agg_first_non_null(groups);
182                },
183                _ => {},
184            }
185        }
186
187        let ca_self = self.rechunk();
188        let arr = ca_self.downcast_iter().next().unwrap();
189        let no_nulls = arr.null_count() == 0;
190        match groups {
191            GroupsType::Idx(groups) => _agg_helper_idx_bool(groups, |(first, idx)| {
192                debug_assert!(idx.len() <= self.len());
193                if idx.is_empty() {
194                    None
195                } else if idx.len() == 1 {
196                    self.get(first as usize)
197                } else if no_nulls {
198                    take_arg_max_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
199                        .map(|p| arr.value_unchecked(idx[p] as usize))
200                } else {
201                    take_arg_max_bool_iter_unchecked_nulls(arr, idx2usize(idx))
202                        .map(|p| arr.value_unchecked(idx[p] as usize))
203                }
204            }),
205            GroupsType::Slice {
206                groups: groups_slice,
207                ..
208            } => _agg_helper_slice_bool(groups_slice, |[first, len]| {
209                debug_assert!(len <= self.len() as IdxSize);
210                match len {
211                    0 => None,
212                    1 => self.get(first as usize),
213                    _ => {
214                        let arr_group = _slice_from_offsets(self, first, len);
215                        arr_group.max()
216                    },
217                }
218            }),
219        }
220    }
221
222    pub(crate) unsafe fn agg_arg_min(&self, groups: &GroupsType) -> Series {
223        // faster paths
224        if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
225            match self.is_sorted_flag() {
226                IsSorted::Ascending => {
227                    return self.clone().into_series().agg_arg_first_non_null(groups);
228                },
229                IsSorted::Descending => {
230                    return self.clone().into_series().agg_arg_last_non_null(groups);
231                },
232                _ => {},
233            }
234        }
235
236        let ca_self = self.rechunk();
237        let arr = ca_self.downcast_iter().next().unwrap();
238        let no_nulls = arr.null_count() == 0;
239        match groups {
240            GroupsType::Idx(groups) => agg_helper_idx_on_all::<IdxType, _>(groups, |idx| {
241                debug_assert!(idx.len() <= ca_self.len());
242                if idx.is_empty() {
243                    None
244                } else if idx.len() == 1 {
245                    arr.get(idx[0] as usize).map(|_| 0)
246                } else if no_nulls {
247                    take_arg_min_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
248                        .map(|p| p as IdxSize)
249                } else {
250                    take_arg_min_bool_iter_unchecked_nulls(arr, idx2usize(idx))
251                        .map(|p| p as IdxSize)
252                }
253            }),
254            GroupsType::Slice {
255                groups: groups_slice,
256                ..
257            } => arg_first_in_slices(arr, groups_slice, false),
258        }
259    }
260
261    pub(crate) unsafe fn agg_arg_max(&self, groups: &GroupsType) -> Series {
262        // faster paths
263        if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
264            match self.is_sorted_flag() {
265                IsSorted::Ascending => {
266                    return self.clone().into_series().agg_arg_last_non_null(groups);
267                },
268                IsSorted::Descending => {
269                    return self.clone().into_series().agg_arg_first_non_null(groups);
270                },
271                _ => {},
272            }
273        }
274
275        let ca_self = self.rechunk();
276        let arr = ca_self.downcast_iter().next().unwrap();
277        let no_nulls = arr.null_count() == 0;
278        match groups {
279            GroupsType::Idx(groups) => agg_helper_idx_on_all::<IdxType, _>(groups, |idx| {
280                debug_assert!(idx.len() <= ca_self.len());
281                if idx.is_empty() {
282                    None
283                } else if idx.len() == 1 {
284                    arr.get(idx[0] as usize).map(|_| 0)
285                } else if no_nulls {
286                    take_arg_max_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
287                        .map(|p| p as IdxSize)
288                } else {
289                    take_arg_max_bool_iter_unchecked_nulls(arr, idx2usize(idx))
290                        .map(|p| p as IdxSize)
291                }
292            }),
293            GroupsType::Slice {
294                groups: groups_slice,
295                ..
296            } => arg_first_in_slices(arr, groups_slice, true),
297        }
298    }
299
300    pub(crate) unsafe fn agg_sum(&self, groups: &GroupsType) -> Series {
301        self.cast_with_options(&IDX_DTYPE, CastOptions::Overflowing)
302            .unwrap()
303            .agg_sum(groups)
304    }
305
306    /// # Safety
307    ///
308    /// Groups should be in correct.
309    #[expect(clippy::too_many_arguments)]
310    unsafe fn bool_agg(
311        &self,
312        groups: &GroupsType,
313        ignore_nulls: bool,
314
315        idx_no_valid: impl Fn(BitMask, &[IdxSize]) -> bool + Send + Sync,
316        idx_validity: impl Fn(BitMask, BitMask, &[IdxSize]) -> bool + Send + Sync,
317        idx_kleene: impl Fn(BitMask, BitMask, &[IdxSize]) -> Option<bool> + Send + Sync,
318
319        slice_no_valid: impl Fn(BitMask, IdxSize, IdxSize) -> bool + Send + Sync,
320        slice_validity: impl Fn(BitMask, BitMask, IdxSize, IdxSize) -> bool + Send + Sync,
321        slice_kleene: impl Fn(BitMask, BitMask, IdxSize, IdxSize) -> Option<bool> + Send + Sync,
322    ) -> BooleanChunked {
323        let name = self.name().clone();
324        let values = self.rechunk();
325        let values = values.downcast_as_array();
326
327        let groups_len = groups.len();
328
329        let ca = RAYON.install(|| {
330            let validity = values
331                .validity()
332                .filter(|v| v.unset_bits() > 0)
333                .map(BitMask::from_bitmap);
334            let values = BitMask::from_bitmap(values.values());
335
336            if !ignore_nulls && let Some(validity) = validity {
337                match groups {
338                    GroupsType::Idx(idx) => {
339                        let all = idx.all();
340                        collect_bool_opt_par(groups_len, |g| idx_kleene(values, validity, &all[g]))
341                    },
342                    GroupsType::Slice { groups, .. } => collect_bool_opt_par(groups_len, |g| {
343                        let [s, l] = groups[g];
344                        slice_kleene(values, validity, s, l)
345                    }),
346                }
347            } else {
348                match groups {
349                    GroupsType::Idx(idx) => {
350                        let all = idx.all();
351                        match validity {
352                            None => collect_bool_par(groups_len, |g| idx_no_valid(values, &all[g])),
353                            Some(validity) => collect_bool_par(groups_len, |g| {
354                                idx_validity(values, validity, &all[g])
355                            }),
356                        }
357                    },
358                    GroupsType::Slice { groups, .. } => match validity {
359                        None => collect_bool_par(groups_len, |g| {
360                            let [s, l] = groups[g];
361                            slice_no_valid(values, s, l)
362                        }),
363                        Some(validity) => collect_bool_par(groups_len, |g| {
364                            let [s, l] = groups[g];
365                            slice_validity(values, validity, s, l)
366                        }),
367                    },
368                }
369            }
370        });
371        ca.with_name(name)
372    }
373
374    /// # Safety
375    ///
376    /// Groups should be in correct.
377    pub unsafe fn agg_any(&self, groups: &GroupsType, ignore_nulls: bool) -> BooleanChunked {
378        self.bool_agg(
379            groups,
380            ignore_nulls,
381            |values, idxs| {
382                idxs.iter()
383                    .any(|i| unsafe { values.get_bit_unchecked(*i as usize) })
384            },
385            |values, validity, idxs| {
386                idxs.iter().any(|i| unsafe {
387                    validity.get_bit_unchecked(*i as usize) & values.get_bit_unchecked(*i as usize)
388                })
389            },
390            |values, validity, idxs| {
391                let mut saw_null = false;
392                for i in idxs.iter() {
393                    let is_valid = unsafe { validity.get_bit_unchecked(*i as usize) };
394                    let is_true = unsafe { values.get_bit_unchecked(*i as usize) };
395
396                    if is_valid & is_true {
397                        return Some(true);
398                    }
399                    saw_null |= !is_valid;
400                }
401                (!saw_null).then_some(false)
402            },
403            |values, start, length| {
404                unsafe { values.sliced_unchecked(start as usize, length as usize) }.leading_zeros()
405                    < length as usize
406            },
407            |values, validity, start, length| {
408                let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
409                let validity =
410                    unsafe { validity.sliced_unchecked(start as usize, length as usize) };
411                values.intersects_with(validity)
412            },
413            |values, validity, start, length| {
414                let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
415                let validity =
416                    unsafe { validity.sliced_unchecked(start as usize, length as usize) };
417
418                if values.intersects_with(validity) {
419                    Some(true)
420                } else if validity.unset_bits() == 0 {
421                    Some(false)
422                } else {
423                    None
424                }
425            },
426        )
427    }
428
429    /// # Safety
430    ///
431    /// Groups should be in correct.
432    pub unsafe fn agg_all(&self, groups: &GroupsType, ignore_nulls: bool) -> BooleanChunked {
433        self.bool_agg(
434            groups,
435            ignore_nulls,
436            |values, idxs| {
437                idxs.iter()
438                    .all(|i| unsafe { values.get_bit_unchecked(*i as usize) })
439            },
440            |values, validity, idxs| {
441                idxs.iter().all(|i| unsafe {
442                    !validity.get_bit_unchecked(*i as usize) | values.get_bit_unchecked(*i as usize)
443                })
444            },
445            |values, validity, idxs| {
446                let mut saw_null = false;
447                for i in idxs.iter() {
448                    let is_valid = unsafe { validity.get_bit_unchecked(*i as usize) };
449                    let is_true = unsafe { values.get_bit_unchecked(*i as usize) };
450
451                    if is_valid & !is_true {
452                        return Some(false);
453                    }
454                    saw_null |= !is_valid;
455                }
456                (!saw_null).then_some(true)
457            },
458            |values, start, length| {
459                let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
460                values.unset_bits() == 0
461            },
462            |values, validity, start, length| {
463                let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
464                let validity =
465                    unsafe { validity.sliced_unchecked(start as usize, length as usize) };
466                values.num_intersections_with(validity) == validity.set_bits()
467            },
468            |values, validity, start, length| {
469                let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
470                let validity =
471                    unsafe { validity.sliced_unchecked(start as usize, length as usize) };
472
473                let num_non_nulls = validity.set_bits();
474
475                if values.num_intersections_with(validity) < num_non_nulls {
476                    Some(false)
477                } else if num_non_nulls < values.len() {
478                    None
479                } else {
480                    Some(true)
481                }
482            },
483        )
484    }
485}