polars_core/chunked_array/ops/
fill_null.rs1use arrow::bitmap::{Bitmap, BitmapBuilder};
2use arrow::legacy::kernels::set::set_at_nulls;
3use bytemuck::Zeroable;
4use num_traits::{NumCast, One, Zero};
5use polars_utils::itertools::Itertools;
6
7use crate::prelude::*;
8
9fn err_fill_null() -> PolarsError {
10 polars_err!(ComputeError: "could not determine the fill value")
11}
12
13impl Series {
14 pub fn fill_null(&self, strategy: FillNullStrategy) -> PolarsResult<Series> {
59 let nc = self.null_count();
61 if nc == 0
62 || (nc == self.len()
63 && matches!(
64 strategy,
65 FillNullStrategy::Forward(_)
66 | FillNullStrategy::Backward(_)
67 | FillNullStrategy::Max
68 | FillNullStrategy::Min
69 | FillNullStrategy::Mean
70 ))
71 {
72 return Ok(self.clone());
73 }
74
75 let physical_type = self.dtype().to_physical();
76
77 match strategy {
78 FillNullStrategy::Forward(None) if !physical_type.is_primitive_numeric() => {
79 fill_forward_gather(self)
80 },
81
82 FillNullStrategy::Forward(Some(limit)) if limit >= nc as IdxSize => {
84 self.fill_null(FillNullStrategy::Forward(None))
85 },
86 FillNullStrategy::Backward(Some(limit)) if limit >= nc as IdxSize => {
87 self.fill_null(FillNullStrategy::Backward(None))
88 },
89
90 FillNullStrategy::Forward(Some(limit)) => fill_forward_gather_limit(self, limit),
91 FillNullStrategy::Backward(None) if !physical_type.is_primitive_numeric() => {
92 fill_backward_gather(self)
93 },
94 FillNullStrategy::Backward(Some(limit)) => fill_backward_gather_limit(self, limit),
95 #[cfg(feature = "dtype-decimal")]
96 FillNullStrategy::One if self.dtype().is_decimal() => {
97 use polars_compute::decimal::i128_to_dec128;
98
99 let ca = self.decimal().unwrap();
100 let precision = ca.precision();
101 let scale = ca.scale();
102 let fill_value = i128_to_dec128(1, precision, scale).ok_or_else(|| {
103 polars_err!(ComputeError: "value '1' is out of range for Decimal({precision}, {scale})")
104 })?;
105 let phys = ca.physical().fill_null_with_values(fill_value)?;
106 Ok(phys.into_decimal_unchecked(precision, scale).into_series())
107 },
108 _ => {
109 let logical_type = self.dtype();
110
111 let s = self.to_physical_repr();
112 use DataType::*;
113 let out = match s.dtype() {
114 #[cfg(feature = "dtype-categorical")]
115 _ if (logical_type.is_categorical() || logical_type.is_enum())
116 && (!matches!(
117 strategy,
118 FillNullStrategy::Forward(_) | FillNullStrategy::Backward(_)
119 )) =>
120 {
121 with_match_categorical_physical_type!(logical_type.cat_physical().unwrap(), |$C| {
122 let ca = self.cat::<$C>().unwrap();
123 fill_null_cat(ca, strategy)
124 })
125 },
126
127 Boolean => fill_null_bool(s.bool().unwrap(), strategy),
128 String => {
129 let s = unsafe { s.cast_unchecked(&Binary)? };
130 let out = s.fill_null(strategy)?;
131 return unsafe { out.cast_unchecked(&String) };
132 },
133 Binary => {
134 let ca = s.binary().unwrap();
135 fill_null_binary(ca, strategy).map(|ca| ca.into_series())
136 },
137 dt if dt.is_primitive_numeric() => {
138 with_match_physical_numeric_polars_type!(dt, |$T| {
139 let ca: &ChunkedArray<$T> = s.as_ref().as_ref().as_ref();
140 fill_null_numeric(ca, strategy).map(|ca| ca.into_series())
141 })
142 },
143 dt => {
144 polars_bail!(InvalidOperation: "fill null strategy not yet supported for dtype: {}", dt)
145 },
146 }?;
147 unsafe { out.from_physical_unchecked(logical_type) }
148 },
149 }
150 }
151}
152
153fn fill_forward_numeric<'a, T>(ca: &'a ChunkedArray<T>) -> ChunkedArray<T>
154where
155 T: PolarsDataType,
156 T::ZeroablePhysical<'a>: Copy,
157{
158 let mut last = T::ZeroablePhysical::zeroed();
160 let values: Vec<T::ZeroablePhysical<'a>> = ca
161 .iter()
162 .map(|v| {
163 last = v.map(|v| v.into()).unwrap_or(last);
164 last
165 })
166 .collect_trusted();
167
168 let num_start_nulls = ca.first_non_null().unwrap_or(ca.len());
170 let mut bm = BitmapBuilder::with_capacity(ca.len());
171 bm.extend_constant(num_start_nulls, false);
172 bm.extend_constant(ca.len() - num_start_nulls, true);
173 ChunkedArray::from_chunk_iter_like(
174 ca,
175 [
176 T::Array::from_zeroable_vec(values, ca.dtype().to_arrow(CompatLevel::newest()))
177 .with_validity_typed(bm.into_opt_validity()),
178 ],
179 )
180}
181
182fn fill_backward_numeric<'a, T>(ca: &'a ChunkedArray<T>) -> ChunkedArray<T>
183where
184 T: PolarsDataType,
185 T::ZeroablePhysical<'a>: Copy,
186{
187 let mut last = T::ZeroablePhysical::zeroed();
189 let values: Vec<T::ZeroablePhysical<'a>> = ca
190 .iter()
191 .rev()
192 .map(|v| {
193 last = v.map(|v| v.into()).unwrap_or(last);
194 last
195 })
196 .collect_reversed();
197
198 let num_end_nulls = ca
200 .last_non_null()
201 .map(|i| ca.len() - 1 - i)
202 .unwrap_or(ca.len());
203 let mut bm = BitmapBuilder::with_capacity(ca.len());
204 bm.extend_constant(ca.len() - num_end_nulls, true);
205 bm.extend_constant(num_end_nulls, false);
206 ChunkedArray::from_chunk_iter_like(
207 ca,
208 [
209 T::Array::from_zeroable_vec(values, ca.dtype().to_arrow(CompatLevel::newest()))
210 .with_validity_typed(bm.into_opt_validity()),
211 ],
212 )
213}
214
215fn fill_null_numeric<T>(
216 ca: &ChunkedArray<T>,
217 strategy: FillNullStrategy,
218) -> PolarsResult<ChunkedArray<T>>
219where
220 T: PolarsNumericType,
221 ChunkedArray<T>: ChunkAgg<T::Native>,
222{
223 let mut out = match strategy {
225 FillNullStrategy::Min => {
226 ca.fill_null_with_values(ChunkAgg::min(ca).ok_or_else(err_fill_null)?)?
227 },
228 FillNullStrategy::Max => {
229 ca.fill_null_with_values(ChunkAgg::max(ca).ok_or_else(err_fill_null)?)?
230 },
231 FillNullStrategy::Mean => ca.fill_null_with_values(
232 ca.mean()
233 .map(|v| NumCast::from(v).unwrap())
234 .ok_or_else(err_fill_null)?,
235 )?,
236 FillNullStrategy::One => return ca.fill_null_with_values(One::one()),
237 FillNullStrategy::Zero => return ca.fill_null_with_values(Zero::zero()),
238 FillNullStrategy::Forward(None) => fill_forward_numeric(ca),
239 FillNullStrategy::Backward(None) => fill_backward_numeric(ca),
240 FillNullStrategy::Forward(_) => unreachable!(),
242 FillNullStrategy::Backward(_) => unreachable!(),
243 };
244 out.rename(ca.name().clone());
245 Ok(out)
246}
247
248fn fill_with_gather<F: Fn(&Bitmap) -> Vec<IdxSize>>(
249 s: &Series,
250 bits_to_idx: F,
251) -> PolarsResult<Series> {
252 let s = s.rechunk();
253 let arr = s.chunks()[0].clone();
254 let validity = arr.validity().expect("nulls");
255
256 let idx = bits_to_idx(validity);
257
258 Ok(unsafe { s.take_slice_unchecked(&idx) })
259}
260
261fn fill_forward_gather(s: &Series) -> PolarsResult<Series> {
262 fill_with_gather(s, |validity| {
263 let mut last_valid = 0;
264 validity
265 .iter()
266 .enumerate_idx()
267 .map(|(i, v)| {
268 if v {
269 last_valid = i;
270 i
271 } else {
272 last_valid
273 }
274 })
275 .collect::<Vec<_>>()
276 })
277}
278
279fn fill_forward_gather_limit(s: &Series, limit: IdxSize) -> PolarsResult<Series> {
280 fill_with_gather(s, |validity| {
281 let mut last_valid = 0;
282 let mut conseq_invalid_count = 0;
283 validity
284 .iter()
285 .enumerate_idx()
286 .map(|(i, v)| {
287 if v {
288 last_valid = i;
289 conseq_invalid_count = 0;
290 i
291 } else if conseq_invalid_count < limit {
292 conseq_invalid_count += 1;
293 last_valid
294 } else {
295 i
296 }
297 })
298 .collect::<Vec<_>>()
299 })
300}
301
302fn fill_backward_gather(s: &Series) -> PolarsResult<Series> {
303 fill_with_gather(s, |validity| {
304 let last = validity.len() as IdxSize - 1;
305 let mut last_valid = last;
306 unsafe {
307 validity
308 .iter()
309 .rev()
310 .enumerate_idx()
311 .map(|(i, v)| {
312 if v {
313 last_valid = last - i;
314 last - i
315 } else {
316 last_valid
317 }
318 })
319 .trust_my_length((last + 1) as usize)
320 .collect_reversed::<Vec<_>>()
321 }
322 })
323}
324
325fn fill_backward_gather_limit(s: &Series, limit: IdxSize) -> PolarsResult<Series> {
326 fill_with_gather(s, |validity| {
327 let last = validity.len() as IdxSize - 1;
328 let mut last_valid = last;
329 let mut conseq_invalid_count = 0;
330 unsafe {
331 validity
332 .iter()
333 .rev()
334 .enumerate_idx()
335 .map(|(i, v)| {
336 if v {
337 last_valid = last - i;
338 conseq_invalid_count = 0;
339 last - i
340 } else if conseq_invalid_count < limit {
341 conseq_invalid_count += 1;
342 last_valid
343 } else {
344 last - i
345 }
346 })
347 .trust_my_length((last + 1) as usize)
348 .collect_reversed()
349 }
350 })
351}
352
353#[cfg(feature = "dtype-categorical")]
354fn fill_null_cat<T: PolarsCategoricalType>(
355 ca: &CategoricalChunked<T>,
356 strategy: FillNullStrategy,
357) -> PolarsResult<Series>
358where
359 ChunkedArray<T::PolarsPhysical>: ChunkAgg<T::Native>,
360{
361 let cat = match strategy {
362 FillNullStrategy::Max => ca.max_categorical(),
363 FillNullStrategy::Min => ca.min_categorical(),
364 FillNullStrategy::Forward(_) => unreachable!(),
365 FillNullStrategy::Backward(_) => unreachable!(),
366 strat => {
367 polars_bail!(InvalidOperation: "fill-null strategy {:?} not supported for datatype {}", strat, ca.dtype)
368 },
369 }.ok_or_else(err_fill_null)?;
370 Ok(ca
371 .physical()
372 .fill_null_with_values(T::Native::from_cat(cat))?
373 .into_series())
374}
375
376fn fill_null_bool(ca: &BooleanChunked, strategy: FillNullStrategy) -> PolarsResult<Series> {
377 match strategy {
378 FillNullStrategy::Min => ca
379 .fill_null_with_values(ca.min().ok_or_else(err_fill_null)?)
380 .map(|ca| ca.into_series()),
381 FillNullStrategy::Max => ca
382 .fill_null_with_values(ca.max().ok_or_else(err_fill_null)?)
383 .map(|ca| ca.into_series()),
384 FillNullStrategy::Mean => polars_bail!(opq = mean, "Boolean"),
385 FillNullStrategy::One => ca.fill_null_with_values(true).map(|ca| ca.into_series()),
386 FillNullStrategy::Zero => ca.fill_null_with_values(false).map(|ca| ca.into_series()),
387 FillNullStrategy::Forward(_) => unreachable!(),
388 FillNullStrategy::Backward(_) => unreachable!(),
389 }
390}
391
392fn fill_null_binary(ca: &BinaryChunked, strategy: FillNullStrategy) -> PolarsResult<BinaryChunked> {
393 match strategy {
394 FillNullStrategy::Min => {
395 ca.fill_null_with_values(ca.min_binary().ok_or_else(err_fill_null)?)
396 },
397 FillNullStrategy::Max => {
398 ca.fill_null_with_values(ca.max_binary().ok_or_else(err_fill_null)?)
399 },
400 FillNullStrategy::Zero => ca.fill_null_with_values(&[]),
401 FillNullStrategy::Forward(_) => unreachable!(),
402 FillNullStrategy::Backward(_) => unreachable!(),
403 strat => polars_bail!(InvalidOperation: "fill-null strategy {:?} is not supported", strat),
404 }
405}
406
407impl<T> ChunkFillNullValue<T::Native> for ChunkedArray<T>
408where
409 T: PolarsNumericType,
410{
411 fn fill_null_with_values(&self, value: T::Native) -> PolarsResult<Self> {
412 Ok(self.apply_kernel(&|arr| Box::new(set_at_nulls(arr, value))))
413 }
414}
415
416impl ChunkFillNullValue<bool> for BooleanChunked {
417 fn fill_null_with_values(&self, value: bool) -> PolarsResult<Self> {
418 self.set(&self.is_null(), Some(value))
419 }
420}
421
422impl ChunkFillNullValue<&[u8]> for BinaryChunked {
423 fn fill_null_with_values(&self, value: &[u8]) -> PolarsResult<Self> {
424 self.set(&self.is_null(), Some(value))
425 }
426}