Skip to main content

polars_core/chunked_array/ops/
zip.rs

1use std::borrow::Cow;
2
3use polars_arrow::bitmap::{Bitmap, BitmapBuilder};
4use polars_arrow::compute::utils::{combine_validities_and, combine_validities_and_not};
5use polars_compute::if_then_else::{IfThenElseKernel, if_then_else_validity};
6use polars_error::PolarsContext;
7use polars_utils::broadcast::broadcast_len;
8
9#[cfg(feature = "object")]
10use crate::chunked_array::object::ObjectArray;
11use crate::prelude::*;
12use crate::utils::{align_chunks_binary, align_chunks_ternary};
13
14const SHAPE_MISMATCH_STR: &str =
15    "shapes of `self`, `mask` and `other` are not suitable for `zip_with` operation";
16
17fn if_then_else_broadcast_mask<T: PolarsDataType>(
18    mask: bool,
19    if_true: &ChunkedArray<T>,
20    if_false: &ChunkedArray<T>,
21) -> PolarsResult<ChunkedArray<T>>
22where
23    ChunkedArray<T>: ChunkExpandAtIndex<T>,
24{
25    let src = if mask { if_true } else { if_false };
26    let other = if mask { if_false } else { if_true };
27    let len = broadcast_len([src.len(), other.len()]).context(SHAPE_MISMATCH_STR)?;
28    let ret = src.broadcast_to(len)?.into_owned();
29    Ok(ret.with_name(if_true.name().clone()))
30}
31
32fn bool_null_to_false(mask: &BooleanArray) -> Bitmap {
33    if mask.null_count() == 0 {
34        mask.values().clone()
35    } else {
36        mask.values() & mask.validity().unwrap()
37    }
38}
39
40/// Combines the validities of ca with the bits in mask using the given combiner.
41///
42/// If the mask itself has validity, those null bits are converted to false.
43fn combine_validities_chunked<
44    T: PolarsDataType,
45    F: Fn(Option<&Bitmap>, Option<&Bitmap>) -> Option<Bitmap>,
46>(
47    ca: &ChunkedArray<T>,
48    mask: &BooleanChunked,
49    combiner: F,
50) -> ChunkedArray<T> {
51    let (ca_al, mask_al) = align_chunks_binary(ca, mask);
52    let chunks = ca_al
53        .downcast_iter()
54        .zip(mask_al.downcast_iter())
55        .map(|(a, m)| {
56            let bm = bool_null_to_false(m);
57            let validity = combiner(a.validity(), Some(&bm));
58            a.clone().with_validity_typed(validity)
59        });
60    ChunkedArray::from_chunk_iter_like(ca, chunks)
61}
62
63impl<T> ChunkZip<T> for ChunkedArray<T>
64where
65    T: PolarsDataType<IsStruct = FalseT>,
66    T::Array: for<'a> IfThenElseKernel<Scalar<'a> = T::Physical<'a>>,
67    ChunkedArray<T>: ChunkExpandAtIndex<T>,
68{
69    fn zip_with(
70        &self,
71        mask: &BooleanChunked,
72        other: &ChunkedArray<T>,
73    ) -> PolarsResult<ChunkedArray<T>> {
74        let if_true = self;
75        let if_false = other;
76
77        // Broadcast mask.
78        if mask.len() == 1 {
79            return if_then_else_broadcast_mask(mask.get(0).unwrap_or(false), if_true, if_false);
80        }
81
82        // Broadcast both.
83        let ret = if if_true.len() == 1 && if_false.len() == 1 {
84            match (if_true.get(0), if_false.get(0)) {
85                (None, None) => ChunkedArray::full_null_like(if_true, mask.len()),
86                (None, Some(_)) => combine_validities_chunked(
87                    &if_false.new_from_index(0, mask.len()),
88                    mask,
89                    combine_validities_and_not,
90                ),
91                (Some(_), None) => combine_validities_chunked(
92                    &if_true.new_from_index(0, mask.len()),
93                    mask,
94                    combine_validities_and,
95                ),
96                (Some(t), Some(f)) => {
97                    let dtype = if_true.downcast_iter().next().unwrap().dtype();
98                    let chunks = mask.downcast_iter().map(|m| {
99                        let bm = bool_null_to_false(m);
100                        let t = t.clone();
101                        let f = f.clone();
102                        IfThenElseKernel::if_then_else_broadcast_both(dtype.clone(), &bm, t, f)
103                    });
104                    ChunkedArray::from_chunk_iter_like(if_true, chunks)
105                },
106            }
107
108        // Broadcast neither.
109        } else if if_true.len() == if_false.len() {
110            polars_ensure!(mask.len() == if_true.len(), ShapeMismatch: SHAPE_MISMATCH_STR);
111            let (mask_al, if_true_al, if_false_al) = align_chunks_ternary(mask, if_true, if_false);
112            let chunks = mask_al
113                .downcast_iter()
114                .zip(if_true_al.downcast_iter())
115                .zip(if_false_al.downcast_iter())
116                .map(|((m, t), f)| IfThenElseKernel::if_then_else(&bool_null_to_false(m), t, f));
117            ChunkedArray::from_chunk_iter_like(if_true, chunks)
118
119        // Broadcast true value.
120        } else if if_true.len() == 1 {
121            polars_ensure!(mask.len() == if_false.len(), ShapeMismatch: SHAPE_MISMATCH_STR);
122            if let Some(true_scalar) = if_true.get(0) {
123                let (mask_al, if_false_al) = align_chunks_binary(mask, if_false);
124                let chunks = mask_al
125                    .downcast_iter()
126                    .zip(if_false_al.downcast_iter())
127                    .map(|(m, f)| {
128                        let bm = bool_null_to_false(m);
129                        let t = true_scalar.clone();
130                        IfThenElseKernel::if_then_else_broadcast_true(&bm, t, f)
131                    });
132                ChunkedArray::from_chunk_iter_like(if_true, chunks)
133            } else {
134                combine_validities_chunked(if_false, mask, combine_validities_and_not)
135            }
136
137        // Broadcast false value.
138        } else if if_false.len() == 1 {
139            polars_ensure!(mask.len() == if_true.len(), ShapeMismatch: SHAPE_MISMATCH_STR);
140            if let Some(false_scalar) = if_false.get(0) {
141                let (mask_al, if_true_al) = align_chunks_binary(mask, if_true);
142                let chunks =
143                    mask_al
144                        .downcast_iter()
145                        .zip(if_true_al.downcast_iter())
146                        .map(|(m, t)| {
147                            let bm = bool_null_to_false(m);
148                            let f = false_scalar.clone();
149                            IfThenElseKernel::if_then_else_broadcast_false(&bm, t, f)
150                        });
151                ChunkedArray::from_chunk_iter_like(if_false, chunks)
152            } else {
153                combine_validities_chunked(if_true, mask, combine_validities_and)
154            }
155        } else {
156            polars_bail!(ShapeMismatch: SHAPE_MISMATCH_STR)
157        };
158
159        Ok(ret.with_name(if_true.name().clone()))
160    }
161}
162
163// Basic implementation for ObjectArray.
164#[cfg(feature = "object")]
165impl<T: PolarsObject> IfThenElseKernel for ObjectArray<T> {
166    type Scalar<'a> = &'a T;
167
168    fn if_then_else(mask: &Bitmap, if_true: &Self, if_false: &Self) -> Self {
169        mask.iter()
170            .zip(if_true.iter())
171            .zip(if_false.iter())
172            .map(|((m, t), f)| if m { t } else { f })
173            .collect_arr()
174    }
175
176    fn if_then_else_broadcast_true(
177        mask: &Bitmap,
178        if_true: Self::Scalar<'_>,
179        if_false: &Self,
180    ) -> Self {
181        mask.iter()
182            .zip(if_false.iter())
183            .map(|(m, f)| if m { Some(if_true) } else { f })
184            .collect_arr()
185    }
186
187    fn if_then_else_broadcast_false(
188        mask: &Bitmap,
189        if_true: &Self,
190        if_false: Self::Scalar<'_>,
191    ) -> Self {
192        mask.iter()
193            .zip(if_true.iter())
194            .map(|(m, t)| if m { t } else { Some(if_false) })
195            .collect_arr()
196    }
197
198    fn if_then_else_broadcast_both(
199        _dtype: ArrowDataType,
200        mask: &Bitmap,
201        if_true: Self::Scalar<'_>,
202        if_false: Self::Scalar<'_>,
203    ) -> Self {
204        mask.iter()
205            .map(|m| if m { if_true } else { if_false })
206            .collect_arr()
207    }
208}
209
210#[cfg(feature = "dtype-struct")]
211impl ChunkZip<StructType> for StructChunked {
212    fn zip_with(
213        &self,
214        mask: &BooleanChunked,
215        other: &ChunkedArray<StructType>,
216    ) -> PolarsResult<ChunkedArray<StructType>> {
217        let min_length = self.length.min(mask.length).min(other.length);
218        let max_length = self.length.max(mask.length).max(other.length);
219
220        let length = if min_length == 0 { 0 } else { max_length };
221
222        debug_assert!(self.length == 1 || self.length == length);
223        debug_assert!(mask.length == 1 || mask.length == length);
224        debug_assert!(other.length == 1 || other.length == length);
225
226        let mut if_true: Cow<ChunkedArray<StructType>> = Cow::Borrowed(self);
227        let mut if_false: Cow<ChunkedArray<StructType>> = Cow::Borrowed(other);
228
229        // Special case. In this case, we know what to do.
230        // @TODO: Optimization. If all mask values are the same, select one of the two.
231        if mask.length == 1 {
232            // pl.when(None) <=> pl.when(False)
233            let is_true = mask.get(0).unwrap_or(false);
234            return Ok(if is_true {
235                self.broadcast_to(length)?.into_owned()
236            } else {
237                other
238                    .broadcast_to(length)?
239                    .into_owned()
240                    .with_name(self.name().clone())
241            });
242        }
243
244        // align_chunks_ternary can only align chunks if:
245        // - Each chunkedarray only has 1 chunk
246        // - Each chunkedarray has an equal length (i.e. is broadcasted)
247        //
248        // Therefore, we broadcast only those that are necessary to be broadcasted.
249        let needs_broadcast =
250            if_true.chunks().len() > 1 || if_false.chunks().len() > 1 || mask.chunks().len() > 1;
251        if needs_broadcast && length > 1 {
252            if_true = self.broadcast_to(length)?;
253            if_false = other.broadcast_to(length)?;
254        }
255
256        let if_true = if_true.as_ref();
257        let if_false = if_false.as_ref();
258
259        let (if_true, if_false, mask) = align_chunks_ternary(if_true, if_false, mask);
260
261        // Prepare the boolean arrays such that Null maps to false.
262        // This prevents every field doing that.
263        // # SAFETY
264        // We don't modify the length and update the null count.
265        let mut mask = mask.into_owned();
266        unsafe {
267            for arr in mask.downcast_iter_mut() {
268                let bm = bool_null_to_false(arr);
269                *arr = BooleanArray::from_data_default(bm, None);
270            }
271            mask.set_null_count(0);
272        }
273
274        // Zip all the fields.
275        let fields = if_true
276            .fields_as_series()
277            .iter()
278            .zip(if_false.fields_as_series())
279            .map(|(lhs, rhs)| lhs.zip_with_same_type(&mask, &rhs))
280            .collect::<PolarsResult<Vec<_>>>()?;
281
282        let mut out = StructChunked::from_series(self.name().clone(), length, fields.iter())?;
283
284        fn rechunk_bitmaps(
285            total_length: usize,
286            iter: impl Iterator<Item = (usize, Option<Bitmap>)>,
287        ) -> Option<Bitmap> {
288            let mut rechunked_length = 0;
289            let mut rechunked_validity = None;
290            for (chunk_length, validity) in iter {
291                if let Some(validity) = validity {
292                    if validity.unset_bits() > 0 {
293                        let v = rechunked_validity.get_or_insert_with(|| {
294                            let mut bm = BitmapBuilder::with_capacity(total_length);
295                            bm.extend_constant(rechunked_length, true);
296                            bm
297                        });
298                        v.extend_constant(rechunked_length - v.len(), true);
299                        v.extend_from_bitmap(&validity);
300                    }
301                }
302
303                rechunked_length += chunk_length;
304            }
305
306            if let Some(rechunked_validity) = rechunked_validity.as_mut() {
307                rechunked_validity.extend_constant(total_length - rechunked_validity.len(), true);
308            }
309
310            rechunked_validity.map(BitmapBuilder::freeze)
311        }
312
313        // Zip the validities.
314        //
315        // We need to take two things into account:
316        // 1. The chunk lengths of `out` might not necessarily match `l`, `r` and `mask`.
317        // 2. `l` and `r` might still need to be broadcasted.
318        if (if_true.null_count + if_false.null_count) > 0 {
319            // Create one validity mask that spans the entirety of out.
320            let rechunked_validity = match (if_true.len(), if_false.len()) {
321                (1, 1) if length != 1 => {
322                    match (if_true.null_count() == 0, if_false.null_count() == 0) {
323                        (true, true) => None,
324                        (false, true) => {
325                            if mask.chunks().len() == 1 {
326                                let m = mask.chunks()[0]
327                                    .as_any()
328                                    .downcast_ref::<BooleanArray>()
329                                    .unwrap()
330                                    .values();
331                                Some(!m)
332                            } else {
333                                rechunk_bitmaps(
334                                    length,
335                                    mask.downcast_iter()
336                                        .map(|m| (m.len(), Some(m.values().clone()))),
337                                )
338                            }
339                        },
340                        (true, false) => {
341                            if mask.chunks().len() == 1 {
342                                let m = mask.chunks()[0]
343                                    .as_any()
344                                    .downcast_ref::<BooleanArray>()
345                                    .unwrap()
346                                    .values();
347                                Some(m.clone())
348                            } else {
349                                rechunk_bitmaps(
350                                    length,
351                                    mask.downcast_iter().map(|m| (m.len(), Some(!m.values()))),
352                                )
353                            }
354                        },
355                        (false, false) => Some(Bitmap::new_zeroed(length)),
356                    }
357                },
358                (1, _) if length != 1 => {
359                    debug_assert!(
360                        if_false
361                            .chunk_lengths()
362                            .zip(mask.chunk_lengths())
363                            .all(|(r, m)| r == m)
364                    );
365
366                    let combine = if if_true.null_count() == 0 {
367                        |if_false: Option<&Bitmap>, m: &Bitmap| {
368                            if_false.map(|v| polars_arrow::bitmap::or(v, m))
369                        }
370                    } else {
371                        |if_false: Option<&Bitmap>, m: &Bitmap| {
372                            Some(
373                                if_false
374                                    .map_or_else(|| !m, |v| polars_arrow::bitmap::and_not(v, m)),
375                            )
376                        }
377                    };
378
379                    if if_false.chunks().len() == 1 {
380                        let if_false = if_false.chunks()[0].validity();
381                        let m = mask.chunks()[0]
382                            .as_any()
383                            .downcast_ref::<BooleanArray>()
384                            .unwrap()
385                            .values();
386
387                        let validity = combine(if_false, m);
388                        validity.filter(|v| v.unset_bits() > 0)
389                    } else {
390                        rechunk_bitmaps(
391                            length,
392                            if_false.chunks().iter().zip(mask.downcast_iter()).map(
393                                |(chunk, mask)| {
394                                    (mask.len(), combine(chunk.validity(), mask.values()))
395                                },
396                            ),
397                        )
398                    }
399                },
400                (_, 1) if length != 1 => {
401                    debug_assert!(
402                        if_true
403                            .chunk_lengths()
404                            .zip(mask.chunk_lengths())
405                            .all(|(l, m)| l == m)
406                    );
407
408                    let combine = if if_false.null_count() == 0 {
409                        |if_true: Option<&Bitmap>, m: &Bitmap| {
410                            if_true.map(|v| polars_arrow::bitmap::or_not(v, m))
411                        }
412                    } else {
413                        |if_true: Option<&Bitmap>, m: &Bitmap| {
414                            Some(
415                                if_true
416                                    .map_or_else(|| m.clone(), |v| polars_arrow::bitmap::and(v, m)),
417                            )
418                        }
419                    };
420
421                    if if_true.chunks().len() == 1 {
422                        let if_true = if_true.chunks()[0].validity();
423                        let m = mask.chunks()[0]
424                            .as_any()
425                            .downcast_ref::<BooleanArray>()
426                            .unwrap()
427                            .values();
428
429                        let validity = combine(if_true, m);
430                        validity.filter(|v| v.unset_bits() > 0)
431                    } else {
432                        rechunk_bitmaps(
433                            length,
434                            if_true.chunks().iter().zip(mask.downcast_iter()).map(
435                                |(chunk, mask)| {
436                                    (mask.len(), combine(chunk.validity(), mask.values()))
437                                },
438                            ),
439                        )
440                    }
441                },
442                (_, _) => {
443                    debug_assert!(
444                        if_true
445                            .chunk_lengths()
446                            .zip(if_false.chunk_lengths())
447                            .all(|(l, r)| l == r)
448                    );
449                    debug_assert!(
450                        if_true
451                            .chunk_lengths()
452                            .zip(mask.chunk_lengths())
453                            .all(|(l, r)| l == r)
454                    );
455
456                    let validities = if_true
457                        .chunks()
458                        .iter()
459                        .zip(if_false.chunks())
460                        .map(|(l, r)| (l.validity(), r.validity()));
461
462                    rechunk_bitmaps(
463                        length,
464                        validities
465                            .zip(mask.downcast_iter())
466                            .map(|((if_true, if_false), mask)| {
467                                (
468                                    mask.len(),
469                                    if_then_else_validity(mask.values(), if_true, if_false),
470                                )
471                            }),
472                    )
473                },
474            };
475
476            // Apply the validity spreading over the chunks of out.
477            if let Some(mut rechunked_validity) = rechunked_validity {
478                assert_eq!(rechunked_validity.len(), out.len());
479
480                let num_chunks = out.chunks().len();
481                let null_count = rechunked_validity.unset_bits();
482
483                // SAFETY: We do not change the lengths of the chunks and we update the null_count
484                // afterwards.
485                let chunks = unsafe { out.chunks_mut() };
486
487                if num_chunks == 1 {
488                    chunks[0] = chunks[0].with_validity(Some(rechunked_validity));
489                } else {
490                    for chunk in chunks {
491                        let chunk_len = chunk.len();
492                        let chunk_validity;
493
494                        // SAFETY: We know that rechunked_validity.len() == out.len()
495                        (chunk_validity, rechunked_validity) =
496                            unsafe { rechunked_validity.split_at_unchecked(chunk_len) };
497                        *chunk = chunk.with_validity(
498                            (chunk_validity.unset_bits() > 0).then_some(chunk_validity),
499                        );
500                    }
501                }
502
503                out.null_count = null_count;
504            } else {
505                // SAFETY: We do not change the lengths of the chunks and we update the null_count
506                // afterwards.
507                let chunks = unsafe { out.chunks_mut() };
508
509                for chunk in chunks {
510                    *chunk = chunk.with_validity(None);
511                }
512
513                out.null_count = 0;
514            }
515        }
516
517        if cfg!(debug_assertions) {
518            let start_length = out.len();
519            let start_null_count = out.null_count();
520
521            out.compute_len();
522
523            assert_eq!(start_length, out.len());
524            assert_eq!(start_null_count, out.null_count());
525        }
526        Ok(out)
527    }
528}