Skip to main content

polars_core/chunked_array/ops/
zip.rs

1use std::borrow::Cow;
2
3use arrow::bitmap::{Bitmap, BitmapBuilder};
4use 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| arrow::bitmap::or(v, m))
369                        }
370                    } else {
371                        |if_false: Option<&Bitmap>, m: &Bitmap| {
372                            Some(if_false.map_or_else(|| !m, |v| arrow::bitmap::and_not(v, m)))
373                        }
374                    };
375
376                    if if_false.chunks().len() == 1 {
377                        let if_false = if_false.chunks()[0].validity();
378                        let m = mask.chunks()[0]
379                            .as_any()
380                            .downcast_ref::<BooleanArray>()
381                            .unwrap()
382                            .values();
383
384                        let validity = combine(if_false, m);
385                        validity.filter(|v| v.unset_bits() > 0)
386                    } else {
387                        rechunk_bitmaps(
388                            length,
389                            if_false.chunks().iter().zip(mask.downcast_iter()).map(
390                                |(chunk, mask)| {
391                                    (mask.len(), combine(chunk.validity(), mask.values()))
392                                },
393                            ),
394                        )
395                    }
396                },
397                (_, 1) if length != 1 => {
398                    debug_assert!(
399                        if_true
400                            .chunk_lengths()
401                            .zip(mask.chunk_lengths())
402                            .all(|(l, m)| l == m)
403                    );
404
405                    let combine = if if_false.null_count() == 0 {
406                        |if_true: Option<&Bitmap>, m: &Bitmap| {
407                            if_true.map(|v| arrow::bitmap::or_not(v, m))
408                        }
409                    } else {
410                        |if_true: Option<&Bitmap>, m: &Bitmap| {
411                            Some(if_true.map_or_else(|| m.clone(), |v| arrow::bitmap::and(v, m)))
412                        }
413                    };
414
415                    if if_true.chunks().len() == 1 {
416                        let if_true = if_true.chunks()[0].validity();
417                        let m = mask.chunks()[0]
418                            .as_any()
419                            .downcast_ref::<BooleanArray>()
420                            .unwrap()
421                            .values();
422
423                        let validity = combine(if_true, m);
424                        validity.filter(|v| v.unset_bits() > 0)
425                    } else {
426                        rechunk_bitmaps(
427                            length,
428                            if_true.chunks().iter().zip(mask.downcast_iter()).map(
429                                |(chunk, mask)| {
430                                    (mask.len(), combine(chunk.validity(), mask.values()))
431                                },
432                            ),
433                        )
434                    }
435                },
436                (_, _) => {
437                    debug_assert!(
438                        if_true
439                            .chunk_lengths()
440                            .zip(if_false.chunk_lengths())
441                            .all(|(l, r)| l == r)
442                    );
443                    debug_assert!(
444                        if_true
445                            .chunk_lengths()
446                            .zip(mask.chunk_lengths())
447                            .all(|(l, r)| l == r)
448                    );
449
450                    let validities = if_true
451                        .chunks()
452                        .iter()
453                        .zip(if_false.chunks())
454                        .map(|(l, r)| (l.validity(), r.validity()));
455
456                    rechunk_bitmaps(
457                        length,
458                        validities
459                            .zip(mask.downcast_iter())
460                            .map(|((if_true, if_false), mask)| {
461                                (
462                                    mask.len(),
463                                    if_then_else_validity(mask.values(), if_true, if_false),
464                                )
465                            }),
466                    )
467                },
468            };
469
470            // Apply the validity spreading over the chunks of out.
471            if let Some(mut rechunked_validity) = rechunked_validity {
472                assert_eq!(rechunked_validity.len(), out.len());
473
474                let num_chunks = out.chunks().len();
475                let null_count = rechunked_validity.unset_bits();
476
477                // SAFETY: We do not change the lengths of the chunks and we update the null_count
478                // afterwards.
479                let chunks = unsafe { out.chunks_mut() };
480
481                if num_chunks == 1 {
482                    chunks[0] = chunks[0].with_validity(Some(rechunked_validity));
483                } else {
484                    for chunk in chunks {
485                        let chunk_len = chunk.len();
486                        let chunk_validity;
487
488                        // SAFETY: We know that rechunked_validity.len() == out.len()
489                        (chunk_validity, rechunked_validity) =
490                            unsafe { rechunked_validity.split_at_unchecked(chunk_len) };
491                        *chunk = chunk.with_validity(
492                            (chunk_validity.unset_bits() > 0).then_some(chunk_validity),
493                        );
494                    }
495                }
496
497                out.null_count = null_count;
498            } else {
499                // SAFETY: We do not change the lengths of the chunks and we update the null_count
500                // afterwards.
501                let chunks = unsafe { out.chunks_mut() };
502
503                for chunk in chunks {
504                    *chunk = chunk.with_validity(None);
505                }
506
507                out.null_count = 0;
508            }
509        }
510
511        if cfg!(debug_assertions) {
512            let start_length = out.len();
513            let start_null_count = out.null_count();
514
515            out.compute_len();
516
517            assert_eq!(start_length, out.len());
518            assert_eq!(start_null_count, out.null_count());
519        }
520        Ok(out)
521    }
522}