Skip to main content

polars_core/frame/group_by/aggregations/
boolean.rs

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