Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/missing.py: 16%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Routines for filling missing data.
3"""
5from __future__ import annotations
7from functools import wraps
8from typing import (
9 TYPE_CHECKING,
10 Any,
11 Literal,
12 cast,
13 overload,
14)
16import numpy as np
18from pandas._config import is_nan_na
20from pandas._libs import (
21 NaT,
22 algos,
23 lib,
24)
25from pandas.compat._optional import import_optional_dependency
27from pandas.core.dtypes.cast import infer_dtype_from
28from pandas.core.dtypes.common import (
29 is_array_like_deprecate_non_pandas,
30 is_arrow_temporal_dtype,
31 is_bool_dtype,
32 is_numeric_dtype,
33 is_object_dtype,
34 needs_i8_conversion,
35)
36from pandas.core.dtypes.dtypes import (
37 ArrowDtype,
38 BaseMaskedDtype,
39 DatetimeTZDtype,
40)
41from pandas.core.dtypes.missing import (
42 is_valid_na_for_dtype,
43 isna,
44 na_value_for_dtype,
45)
47if TYPE_CHECKING:
48 from collections.abc import Callable
49 from typing import TypeAlias
51 from pandas._typing import (
52 ArrayLike,
53 AxisInt,
54 F,
55 ReindexMethod,
56 npt,
57 )
59 from pandas import Index
61 _CubicBC: TypeAlias = Literal["not-a-knot", "clamped", "natural", "periodic"]
64def check_value_size(value, mask: npt.NDArray[np.bool_], length: int):
65 """
66 Validate the size of the values passed to ExtensionArray.fillna.
67 """
68 if isinstance(value, dict):
69 raise TypeError(
70 "ExtensionArray.fillna does not support filling with a dict. "
71 "Use Series.fillna instead."
72 )
73 if is_array_like_deprecate_non_pandas(value):
74 if len(value) != length:
75 raise ValueError(
76 f"Length of 'value' does not match. Got ({len(value)}) "
77 f" expected {length}"
78 )
79 value = value[mask]
81 return value
84def mask_missing(arr: ArrayLike, value) -> npt.NDArray[np.bool_]:
85 """
86 Return a masking array of same size/shape as arr
87 with entries equaling value set to True.
89 Parameters
90 ----------
91 arr : ArrayLike
92 value : scalar-like
93 Caller has ensured `not is_list_like(value)` and that it can be held
94 by `arr`.
96 Returns
97 -------
98 np.ndarray[bool]
99 """
100 dtype, value = infer_dtype_from(value)
102 if (
103 isinstance(arr.dtype, (BaseMaskedDtype, ArrowDtype))
104 and lib.is_float(value)
105 and np.isnan(value)
106 and not is_nan_na()
107 ):
108 # TODO: this should be done in an EA method?
109 if arr.dtype.kind == "f":
110 # GH#55127
111 if isinstance(arr.dtype, BaseMaskedDtype):
112 # error: "ExtensionArray" has no attribute "_data" [attr-defined]
113 mask = np.isnan(arr._data) & ~arr.isna() # type: ignore[attr-defined]
114 return mask
115 else:
116 # error: "ExtensionArray" has no attribute "_pa_array" [attr-defined]
117 import pyarrow.compute as pc
119 mask = pc.is_nan(arr._pa_array).fill_null(False).to_numpy() # type: ignore[attr-defined]
120 return mask
122 elif arr.dtype.kind in "iu":
123 # GH#51237
124 mask = np.zeros(arr.shape, dtype=bool)
125 return mask
127 if isna(value):
128 return isna(arr)
130 # GH 21977
131 mask = np.zeros(arr.shape, dtype=bool)
132 if (
133 is_numeric_dtype(arr.dtype)
134 and not is_bool_dtype(arr.dtype)
135 and lib.is_bool(value)
136 ):
137 # e.g. test_replace_ea_float_with_bool, see GH#62048
138 pass
139 elif (
140 is_bool_dtype(arr.dtype) and is_numeric_dtype(dtype) and not lib.is_bool(value)
141 ):
142 # e.g. test_replace_ea_float_with_bool, see GH#62048
143 pass
144 elif is_numeric_dtype(arr.dtype) and isinstance(value, str):
145 # GH#29553 prevent numpy deprecation warnings
146 pass
147 elif is_object_dtype(arr.dtype):
148 # pre-compute mask to avoid comparison to NA
149 # e.g. test_replace_na_in_obj_column
150 arr_mask = ~isna(arr)
151 mask[arr_mask] = arr[arr_mask] == value
152 else:
153 new_mask = arr == value
155 if not isinstance(new_mask, np.ndarray):
156 # usually BooleanArray
157 new_mask = new_mask.to_numpy(dtype=bool, na_value=False)
158 mask = new_mask
160 return mask
163@overload
164def clean_fill_method(
165 method: Literal["ffill", "pad", "bfill", "backfill"],
166 *,
167 allow_nearest: Literal[False] = ...,
168) -> Literal["pad", "backfill"]: ...
171@overload
172def clean_fill_method(
173 method: Literal["ffill", "pad", "bfill", "backfill", "nearest"],
174 *,
175 allow_nearest: Literal[True],
176) -> Literal["pad", "backfill", "nearest"]: ...
179def clean_fill_method(
180 method: Literal["ffill", "pad", "bfill", "backfill", "nearest"],
181 *,
182 allow_nearest: bool = False,
183) -> Literal["pad", "backfill", "nearest"]:
184 if isinstance(method, str):
185 # error: Incompatible types in assignment (expression has type "str", variable
186 # has type "Literal['ffill', 'pad', 'bfill', 'backfill', 'nearest']")
187 method = method.lower() # type: ignore[assignment]
188 if method == "ffill":
189 method = "pad"
190 elif method == "bfill":
191 method = "backfill"
193 valid_methods = ["pad", "backfill"]
194 expecting = "pad (ffill) or backfill (bfill)"
195 if allow_nearest:
196 valid_methods.append("nearest")
197 expecting = "pad (ffill), backfill (bfill) or nearest"
198 if method not in valid_methods:
199 raise ValueError(f"Invalid fill method. Expecting {expecting}. Got {method}")
200 return method
203# interpolation methods that dispatch to np.interp
205NP_METHODS = ["linear", "time", "index", "values"]
207# interpolation methods that dispatch to _interpolate_scipy_wrapper
209SP_METHODS = [
210 "nearest",
211 "zero",
212 "slinear",
213 "quadratic",
214 "cubic",
215 "barycentric",
216 "krogh",
217 "spline",
218 "polynomial",
219 "from_derivatives",
220 "piecewise_polynomial",
221 "pchip",
222 "akima",
223 "cubicspline",
224]
227def clean_interp_method(method: str, index: Index, **kwargs) -> str:
228 order = kwargs.get("order")
230 if method in ("spline", "polynomial") and order is None:
231 raise ValueError("You must specify the order of the spline or polynomial.")
233 valid = NP_METHODS + SP_METHODS
234 if method not in valid:
235 raise ValueError(f"method must be one of {valid}. Got '{method}' instead.")
237 if method in ("krogh", "piecewise_polynomial", "pchip"):
238 if not index.is_monotonic_increasing:
239 raise ValueError(
240 f"{method} interpolation requires that the index be monotonic."
241 )
243 return method
246def find_valid_index(how: str, is_valid: npt.NDArray[np.bool_]) -> int | None:
247 """
248 Retrieves the positional index of the first valid value.
250 Parameters
251 ----------
252 how : {'first', 'last'}
253 Use this parameter to change between the first or last valid index.
254 is_valid: np.ndarray
255 Mask to find na_values.
257 Returns
258 -------
259 int or None
260 """
261 assert how in ["first", "last"]
263 if len(is_valid) == 0: # early stop
264 return None
266 if is_valid.ndim == 2:
267 # reduce axis 1
268 is_valid = is_valid.any(axis=1)
270 if how == "first":
271 idxpos = is_valid[::].argmax()
273 elif how == "last":
274 idxpos = len(is_valid) - 1 - is_valid[::-1].argmax()
276 chk_notna = is_valid[idxpos]
278 if not chk_notna:
279 return None
280 # Incompatible return value type (got "signedinteger[Any]",
281 # expected "Optional[int]")
282 return idxpos # type: ignore[return-value]
285def validate_limit_direction(
286 limit_direction: str,
287) -> Literal["forward", "backward", "both"]:
288 valid_limit_directions = ["forward", "backward", "both"]
289 limit_direction = limit_direction.lower()
290 if limit_direction not in valid_limit_directions:
291 raise ValueError(
292 "Invalid limit_direction: expecting one of "
293 f"{valid_limit_directions}, got '{limit_direction}'."
294 )
295 # error: Incompatible return value type (got "str", expected
296 # "Literal['forward', 'backward', 'both']")
297 return limit_direction # type: ignore[return-value]
300def validate_limit_area(limit_area: str | None) -> Literal["inside", "outside"] | None:
301 if limit_area is not None:
302 valid_limit_areas = ["inside", "outside"]
303 limit_area = limit_area.lower()
304 if limit_area not in valid_limit_areas:
305 raise ValueError(
306 f"Invalid limit_area: expecting one of {valid_limit_areas}, got "
307 f"{limit_area}."
308 )
309 # error: Incompatible return value type (got "Optional[str]", expected
310 # "Optional[Literal['inside', 'outside']]")
311 return limit_area # type: ignore[return-value]
314def infer_limit_direction(
315 limit_direction: Literal["backward", "forward", "both"] | None, method: str
316) -> Literal["backward", "forward", "both"]:
317 # Set `limit_direction` depending on `method`
318 if limit_direction is None:
319 if method in ("backfill", "bfill"):
320 limit_direction = "backward"
321 else:
322 limit_direction = "forward"
323 else:
324 if method in ("pad", "ffill") and limit_direction != "forward":
325 raise ValueError(
326 f"`limit_direction` must be 'forward' for method `{method}`"
327 )
328 if method in ("backfill", "bfill") and limit_direction != "backward":
329 raise ValueError(
330 f"`limit_direction` must be 'backward' for method `{method}`"
331 )
332 return limit_direction
335def get_interp_index(method, index: Index) -> Index:
336 # create/use the index
337 if method == "linear":
338 # prior default
339 from pandas import RangeIndex
341 index = RangeIndex(len(index))
342 else:
343 methods = {"index", "values", "nearest", "time"}
344 is_numeric_or_datetime = (
345 is_numeric_dtype(index.dtype)
346 or isinstance(index.dtype, DatetimeTZDtype)
347 or lib.is_np_dtype(index.dtype, "mM")
348 )
349 valid = NP_METHODS + SP_METHODS
350 if method in valid:
351 if method not in methods and not is_numeric_or_datetime:
352 raise ValueError(
353 "Index column must be numeric or datetime type when "
354 f"using {method} method other than linear. "
355 "Try setting a numeric or datetime index column before "
356 "interpolating."
357 )
358 else:
359 raise ValueError(f"Can not interpolate with method={method}.")
361 if isna(index).any():
362 raise NotImplementedError(
363 "Interpolation with NaNs in the index "
364 "has not been implemented. Try filling "
365 "those NaNs before interpolating."
366 )
367 return index
370def interpolate_2d_inplace(
371 data: np.ndarray, # floating dtype
372 index: Index,
373 axis: AxisInt,
374 method: str = "linear",
375 limit: int | None = None,
376 limit_direction: str = "forward",
377 limit_area: str | None = None,
378 fill_value: Any | None = None,
379 mask=None,
380 **kwargs,
381) -> None:
382 """
383 Column-wise application of _interpolate_1d.
385 Notes
386 -----
387 Alters 'data' in-place.
389 The signature does differ from _interpolate_1d because it only
390 includes what is needed for Block.interpolate.
391 """
392 # validate the interp method
393 clean_interp_method(method, index, **kwargs)
395 if is_valid_na_for_dtype(fill_value, data.dtype):
396 fill_value = na_value_for_dtype(data.dtype, compat=False)
398 if method == "time":
399 if not needs_i8_conversion(index.dtype) and not is_arrow_temporal_dtype(
400 index.dtype
401 ):
402 raise ValueError(
403 "time-weighted interpolation only works "
404 "on Series or DataFrames with a "
405 "DatetimeIndex"
406 )
407 method = "values"
409 limit_direction = validate_limit_direction(limit_direction)
410 limit_area_validated = validate_limit_area(limit_area)
412 # default limit is unlimited GH #16282
413 limit = algos.validate_limit(nobs=None, limit=limit)
415 indices = _index_to_interp_indices(index, method)
417 def func(yvalues: np.ndarray) -> None:
418 # process 1-d slices in the axis direction
420 _interpolate_1d(
421 indices=indices,
422 yvalues=yvalues,
423 method=method,
424 limit=limit,
425 limit_direction=limit_direction,
426 limit_area=limit_area_validated,
427 fill_value=fill_value,
428 bounds_error=False,
429 mask=mask,
430 **kwargs,
431 )
433 np.apply_along_axis(func, axis, data)
436def _arrow_temporal_to_i8(arr: ArrayLike) -> np.ndarray:
437 """
438 View an ArrowDtype timestamp/duration array as int64.
440 ``np.asarray`` is not usable here: for tz-aware timestamps it yields an
441 object array of ``Timestamp`` objects, which cannot be viewed as i8.
443 Parameters
444 ----------
445 arr : ArrowExtensionArray
446 Array whose dtype satisfies ``is_arrow_temporal_dtype``.
448 Returns
449 -------
450 np.ndarray
451 int64 view of the underlying values, with ``iNaT`` for missing values.
452 """
453 pa = import_optional_dependency("pyarrow")
454 if pa.types.is_timestamp(arr.dtype.pyarrow_dtype): # type: ignore[union-attr]
455 return arr._to_datetimearray().asi8 # type: ignore[union-attr]
456 return arr._to_timedeltaarray().asi8 # type: ignore[union-attr]
459def _index_to_interp_indices(index: Index, method: str) -> np.ndarray:
460 """
461 Convert Index to ndarray of indices to pass to NumPy/SciPy.
462 """
463 xarr = index._values
464 if needs_i8_conversion(xarr.dtype):
465 # GH#1646 for dt64tz
466 xarr = xarr.view("i8")
467 elif is_arrow_temporal_dtype(xarr.dtype):
468 xarr = _arrow_temporal_to_i8(xarr)
470 if method == "linear":
471 inds = xarr
472 inds = cast("np.ndarray", inds)
473 else:
474 inds = np.asarray(xarr)
476 if method in ("values", "index"):
477 if inds.dtype == np.object_:
478 inds = lib.maybe_convert_objects(inds)
480 return inds
483def _interpolate_1d(
484 indices: np.ndarray,
485 yvalues: np.ndarray,
486 method: str = "linear",
487 limit: int | None = None,
488 limit_direction: str = "forward",
489 limit_area: Literal["inside", "outside"] | None = None,
490 fill_value: Any | None = None,
491 bounds_error: bool = False,
492 order: int | None = None,
493 mask=None,
494 **kwargs,
495) -> None:
496 """
497 Logic for the 1-d interpolation. The input
498 indices and yvalues will each be 1-d arrays of the same length.
500 Bounds_error is currently hardcoded to False since non-scipy ones don't
501 take it as an argument.
503 Notes
504 -----
505 Fills 'yvalues' in-place.
506 """
507 if mask is not None:
508 invalid = mask
509 else:
510 invalid = isna(yvalues)
511 valid = ~invalid
513 if not valid.any():
514 return
516 if valid.all():
517 return
519 # These index pointers to invalid values... i.e. {0, 1, etc...
520 all_nans = np.flatnonzero(invalid)
522 first_valid_index = find_valid_index(how="first", is_valid=valid)
523 if first_valid_index is None: # no nan found in start
524 first_valid_index = 0
525 start_nans = np.arange(first_valid_index)
527 last_valid_index = find_valid_index(how="last", is_valid=valid)
528 if last_valid_index is None: # no nan found in end
529 last_valid_index = len(yvalues)
530 end_nans = np.arange(1 + last_valid_index, len(valid))
532 # preserve_nans contains indices of invalid values,
533 # but in this case, it is the final set of indices that need to be
534 # preserved as NaN after the interpolation.
536 # For example if limit_direction='forward' then preserve_nans will
537 # contain indices of NaNs at the beginning of the series, and NaNs that
538 # are more than 'limit' away from the prior non-NaN.
540 # set preserve_nans based on direction using _interp_limit
541 if limit_direction == "forward":
542 preserve_nans = np.union1d(start_nans, _interp_limit(invalid, limit, 0))
543 elif limit_direction == "backward":
544 preserve_nans = np.union1d(end_nans, _interp_limit(invalid, 0, limit))
545 else:
546 # both directions... just use _interp_limit
547 preserve_nans = np.unique(_interp_limit(invalid, limit, limit))
549 # if limit_area is set, add either mid or outside indices
550 # to preserve_nans GH #16284
551 if limit_area == "inside":
552 # preserve NaNs on the outside
553 preserve_nans = np.union1d(preserve_nans, start_nans)
554 preserve_nans = np.union1d(preserve_nans, end_nans)
555 elif limit_area == "outside":
556 # preserve NaNs on the inside
557 mid_nans = np.setdiff1d(all_nans, start_nans, assume_unique=True)
558 mid_nans = np.setdiff1d(mid_nans, end_nans, assume_unique=True)
559 preserve_nans = np.union1d(preserve_nans, mid_nans)
561 is_datetimelike = yvalues.dtype.kind in "mM"
563 if is_datetimelike:
564 yvalues = yvalues.view("i8")
566 if method in NP_METHODS:
567 # np.interp requires sorted X values, #21037
569 indexer = np.argsort(indices[valid])
570 yvalues[invalid] = np.interp(
571 indices[invalid], indices[valid][indexer], yvalues[valid][indexer]
572 )
573 else:
574 yvalues[invalid] = _interpolate_scipy_wrapper(
575 indices[valid],
576 yvalues[valid],
577 indices[invalid],
578 method=method,
579 fill_value=fill_value,
580 bounds_error=bounds_error,
581 order=order,
582 **kwargs,
583 )
585 if mask is not None:
586 mask[:] = False
587 mask[preserve_nans] = True
588 elif is_datetimelike:
589 yvalues[preserve_nans] = NaT.value
590 else:
591 yvalues[preserve_nans] = np.nan
592 return
595def _interpolate_scipy_wrapper(
596 x: np.ndarray,
597 y: np.ndarray,
598 new_x: np.ndarray,
599 method: str,
600 fill_value=None,
601 bounds_error: bool = False,
602 order=None,
603 **kwargs,
604):
605 """
606 Passed off to scipy.interpolate.interp1d. method is scipy's kind.
607 Returns an array interpolated at new_x. Add any new methods to
608 the list in _clean_interp_method.
609 """
610 extra = f"{method} interpolation requires SciPy."
611 import_optional_dependency("scipy", extra=extra)
612 from scipy import interpolate
614 new_x = np.asarray(new_x)
616 # ignores some kwargs that could be passed along.
617 alt_methods: dict[str, Callable[..., np.ndarray]] = {
618 "barycentric": interpolate.barycentric_interpolate,
619 "krogh": interpolate.krogh_interpolate,
620 "from_derivatives": _from_derivatives,
621 "piecewise_polynomial": _from_derivatives,
622 "cubicspline": _cubicspline_interpolate,
623 "akima": _akima_interpolate,
624 "pchip": interpolate.pchip_interpolate,
625 }
627 interp1d_methods = [
628 "nearest",
629 "zero",
630 "slinear",
631 "quadratic",
632 "cubic",
633 "polynomial",
634 ]
635 terp: Callable[..., np.ndarray] | None
636 if method in interp1d_methods:
637 if method == "polynomial":
638 kind = order
639 else:
640 kind = method
641 terp = interpolate.interp1d(
642 x, y, kind=kind, fill_value=fill_value, bounds_error=bounds_error
643 )
644 new_y = terp(new_x) # pyright: ignore[reportOptionalCall]
645 elif method == "spline":
646 # GH #10633, #24014
647 if isna(order) or (order <= 0): # pyright: ignore[reportOptionalOperand]
648 raise ValueError(
649 f"order needs to be specified and greater than 0; got order: {order}"
650 )
651 terp = interpolate.UnivariateSpline(x, y, k=order, **kwargs)
652 new_y = terp(new_x) # pyright: ignore[reportOptionalCall]
653 else:
654 # GH 7295: need to be able to write for some reason
655 # in some circumstances: check all three
656 if not x.flags.writeable:
657 x = x.copy()
658 if not y.flags.writeable:
659 y = y.copy()
660 if not new_x.flags.writeable:
661 new_x = new_x.copy()
662 terp = alt_methods.get(method, None)
663 if terp is None:
664 raise ValueError(f"Can not interpolate with method={method}.")
666 # Make sure downcast is not in kwargs for alt methods
667 kwargs.pop("downcast", None)
668 new_y = terp(x, y, new_x, **kwargs)
669 return new_y
672def _from_derivatives(
673 xi: np.ndarray,
674 yi: np.ndarray,
675 x: np.ndarray,
676 order=None,
677 der: int | list[int] | None = 0,
678 extrapolate: bool = False,
679):
680 """
681 Convenience function for interpolate.BPoly.from_derivatives.
683 Construct a piecewise polynomial in the Bernstein basis, compatible
684 with the specified values and derivatives at breakpoints.
686 Parameters
687 ----------
688 xi : array-like
689 sorted 1D array of x-coordinates
690 yi : array-like or list of array-likes
691 yi[i][j] is the j-th derivative known at xi[i]
692 order: None or int or array-like of ints. Default: None.
693 Specifies the degree of local polynomials. If not None, some
694 derivatives are ignored.
695 der : int or list
696 How many derivatives to extract; None for all potentially nonzero
697 derivatives (that is a number equal to the number of points), or a
698 list of derivatives to extract. This number includes the function
699 value as 0th derivative.
700 extrapolate : bool, optional
701 Whether to extrapolate to out-of-bounds points based on first and last
702 intervals, or to return NaNs. Default: True.
704 See Also
705 --------
706 scipy.interpolate.BPoly.from_derivatives
708 Returns
709 -------
710 y : scalar or array-like
711 The result, of length R or length M or M by R.
712 """
713 from scipy import interpolate
715 # return the method for compat with scipy version & backwards compat
716 method = interpolate.BPoly.from_derivatives
717 m = method(xi, yi.reshape(-1, 1), orders=order, extrapolate=extrapolate)
719 return m(x)
722def _akima_interpolate(
723 xi: np.ndarray,
724 yi: np.ndarray,
725 x: np.ndarray,
726 der: int = 0,
727 axis: AxisInt = 0,
728):
729 """
730 Convenience function for akima interpolation.
731 xi and yi are arrays of values used to approximate some function f,
732 with ``yi = f(xi)``.
734 See `Akima1DInterpolator` for details.
736 Parameters
737 ----------
738 xi : np.ndarray
739 A sorted list of x-coordinates, of length N.
740 yi : np.ndarray
741 A 1-D array of real values. `yi`'s length along the interpolation
742 axis must be equal to the length of `xi`. If N-D array, use axis
743 parameter to select correct axis.
744 x : np.ndarray
745 Of length M.
746 der : int, optional
747 How many derivatives to extract. This number includes the function
748 value as 0th derivative.
749 axis : int, optional
750 Axis in the yi array corresponding to the x-coordinate values.
752 See Also
753 --------
754 scipy.interpolate.Akima1DInterpolator
756 Returns
757 -------
758 y : scalar or array-like
759 The result, of length R or length M or M by R,
761 """
762 from scipy import interpolate
764 P = interpolate.Akima1DInterpolator(xi, yi, axis=axis)
766 return P(x, nu=der)
769def _cubicspline_interpolate(
770 xi: np.ndarray,
771 yi: np.ndarray,
772 x: np.ndarray,
773 axis: AxisInt = 0,
774 bc_type: _CubicBC | tuple[Any, Any] = "not-a-knot",
775 extrapolate: Literal["periodic"] | bool | None = None,
776) -> np.ndarray:
777 """
778 Convenience function for cubic spline data interpolator.
780 See `scipy.interpolate.CubicSpline` for details.
782 Parameters
783 ----------
784 xi : np.ndarray, shape (n,)
785 1-d array containing values of the independent variable.
786 Values must be real, finite and in strictly increasing order.
787 yi : np.ndarray
788 Array containing values of the dependent variable. It can have
789 arbitrary number of dimensions, but the length along ``axis``
790 (see below) must match the length of ``x``. Values must be finite.
791 x : np.ndarray, shape (m,)
792 axis : int, optional
793 Axis along which `y` is assumed to be varying. Meaning that for
794 ``x[i]`` the corresponding values are ``np.take(y, i, axis=axis)``.
795 Default is 0.
796 bc_type : string or 2-tuple, optional
797 Boundary condition type. Two additional equations, given by the
798 boundary conditions, are required to determine all coefficients of
799 polynomials on each segment [2]_.
800 If `bc_type` is a string, then the specified condition will be applied
801 at both ends of a spline. Available conditions are:
802 * 'not-a-knot' (default): The first and second segment at a curve end
803 are the same polynomial. It is a good default when there is no
804 information on boundary conditions.
805 * 'periodic': The interpolated functions is assumed to be periodic
806 of period ``x[-1] - x[0]``. The first and last value of `y` must be
807 identical: ``y[0] == y[-1]``. This boundary condition will result in
808 ``y'[0] == y'[-1]`` and ``y''[0] == y''[-1]``.
809 * 'clamped': The first derivative at curves ends are zero. Assuming
810 a 1D `y`, ``bc_type=((1, 0.0), (1, 0.0))`` is the same condition.
811 * 'natural': The second derivative at curve ends are zero. Assuming
812 a 1D `y`, ``bc_type=((2, 0.0), (2, 0.0))`` is the same condition.
813 If `bc_type` is a 2-tuple, the first and the second value will be
814 applied at the curve start and end respectively. The tuple values can
815 be one of the previously mentioned strings (except 'periodic') or a
816 tuple `(order, deriv_values)` allowing to specify arbitrary
817 derivatives at curve ends:
818 * `order`: the derivative order, 1 or 2.
819 * `deriv_value`: array-like containing derivative values, shape must
820 be the same as `y`, excluding ``axis`` dimension. For example, if
821 `y` is 1D, then `deriv_value` must be a scalar. If `y` is 3D with
822 the shape (n0, n1, n2) and axis=2, then `deriv_value` must be 2D
823 and have the shape (n0, n1).
824 extrapolate : {bool, 'periodic', None}, optional
825 If bool, determines whether to extrapolate to out-of-bounds points
826 based on first and last intervals, or to return NaNs. If 'periodic',
827 periodic extrapolation is used. If None (default), ``extrapolate`` is
828 set to 'periodic' for ``bc_type='periodic'`` and to True otherwise.
830 See Also
831 --------
832 scipy.interpolate.CubicHermiteSpline
834 Returns
835 -------
836 y : scalar or array-like
837 The result, of shape (m,)
839 References
840 ----------
841 .. [1] `Cubic Spline Interpolation
842 <https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation>`_
843 on Wikiversity.
844 .. [2] Carl de Boor, "A Practical Guide to Splines", Springer-Verlag, 1978.
845 """
846 from scipy import interpolate
848 P = interpolate.CubicSpline(
849 xi, yi, axis=axis, bc_type=bc_type, extrapolate=extrapolate
850 )
852 return P(x)
855def pad_or_backfill_inplace(
856 values: np.ndarray,
857 method: Literal["pad", "backfill"] = "pad",
858 axis: AxisInt = 0,
859 limit: int | None = None,
860 limit_area: Literal["inside", "outside"] | None = None,
861) -> None:
862 """
863 Perform an actual interpolation of values, values will be make 2-d if
864 needed fills inplace, returns the result.
866 Parameters
867 ----------
868 values: np.ndarray
869 Input array.
870 method: str, default "pad"
871 Interpolation method. Could be "bfill" or "pad"
872 axis: 0 or 1
873 Interpolation axis
874 limit: int, optional
875 Index limit on interpolation.
876 limit_area: str, optional
877 Limit area for interpolation. Can be "inside" or "outside"
879 Notes
880 -----
881 Modifies values in-place.
882 """
883 transf = (lambda x: x) if axis == 0 else (lambda x: x.T)
885 # reshape a 1 dim if needed
886 if values.ndim == 1:
887 if axis != 0: # pragma: no cover
888 raise AssertionError("cannot interpolate on an ndim == 1 with axis != 0")
889 values = values.reshape((1, *values.shape))
891 method = clean_fill_method(method)
892 tvalues = transf(values)
894 func = get_fill_func(method, ndim=2)
895 # _pad_2d and _backfill_2d both modify tvalues inplace
896 func(tvalues, limit=limit, limit_area=limit_area)
899def _fillna_prep(
900 values, mask: npt.NDArray[np.bool_] | None = None
901) -> npt.NDArray[np.bool_]:
902 # boilerplate for _pad_1d, _backfill_1d, _pad_2d, _backfill_2d
904 if mask is None:
905 mask = isna(values)
907 return mask
910def _datetimelike_compat(func: F) -> F:
911 """
912 Wrapper to handle datetime64 and timedelta64 dtypes.
913 """
915 @wraps(func)
916 def new_func(
917 values,
918 limit: int | None = None,
919 limit_area: Literal["inside", "outside"] | None = None,
920 mask=None,
921 ):
922 if needs_i8_conversion(values.dtype):
923 if mask is None:
924 # This needs to occur before casting to int64
925 mask = isna(values)
927 result, mask = func(
928 values.view("i8"), limit=limit, limit_area=limit_area, mask=mask
929 )
930 return result.view(values.dtype), mask
932 return func(values, limit=limit, limit_area=limit_area, mask=mask)
934 return cast("F", new_func)
937@_datetimelike_compat
938def _pad_1d(
939 values: np.ndarray,
940 limit: int | None = None,
941 limit_area: Literal["inside", "outside"] | None = None,
942 mask: npt.NDArray[np.bool_] | None = None,
943) -> tuple[np.ndarray, npt.NDArray[np.bool_]]:
944 mask = _fillna_prep(values, mask)
945 if limit_area is not None and not mask.all():
946 _fill_limit_area_1d(mask, limit_area)
947 algos.pad_inplace(values, mask, limit=limit)
948 return values, mask
951@_datetimelike_compat
952def _backfill_1d(
953 values: np.ndarray,
954 limit: int | None = None,
955 limit_area: Literal["inside", "outside"] | None = None,
956 mask: npt.NDArray[np.bool_] | None = None,
957) -> tuple[np.ndarray, npt.NDArray[np.bool_]]:
958 mask = _fillna_prep(values, mask)
959 if limit_area is not None and not mask.all():
960 _fill_limit_area_1d(mask, limit_area)
961 algos.backfill_inplace(values, mask, limit=limit)
962 return values, mask
965@_datetimelike_compat
966def _pad_2d(
967 values: np.ndarray,
968 limit: int | None = None,
969 limit_area: Literal["inside", "outside"] | None = None,
970 mask: npt.NDArray[np.bool_] | None = None,
971) -> tuple[np.ndarray, npt.NDArray[np.bool_]]:
972 mask = _fillna_prep(values, mask)
973 if limit_area is not None:
974 _fill_limit_area_2d(mask, limit_area)
976 if values.size:
977 algos.pad_2d_inplace(values, mask, limit=limit)
978 return values, mask
981@_datetimelike_compat
982def _backfill_2d(
983 values,
984 limit: int | None = None,
985 limit_area: Literal["inside", "outside"] | None = None,
986 mask: npt.NDArray[np.bool_] | None = None,
987):
988 mask = _fillna_prep(values, mask)
989 if limit_area is not None:
990 _fill_limit_area_2d(mask, limit_area)
992 if values.size:
993 algos.backfill_2d_inplace(values, mask, limit=limit)
994 else:
995 # for test coverage
996 pass
997 return values, mask
1000def _fill_limit_area_1d(
1001 mask: npt.NDArray[np.bool_], limit_area: Literal["outside", "inside"]
1002) -> None:
1003 """Prepare 1d mask for ffill/bfill with limit_area.
1005 Caller is responsible for checking at least one value of mask is False.
1006 When called, mask will no longer faithfully represent when
1007 the corresponding are NA or not.
1009 Parameters
1010 ----------
1011 mask : np.ndarray[bool, ndim=1]
1012 Mask representing NA values when filling.
1013 limit_area : { "outside", "inside" }
1014 Whether to limit filling to outside or inside the outer most non-NA value.
1015 """
1016 neg_mask = ~mask
1017 first = neg_mask.argmax()
1018 last = len(neg_mask) - neg_mask[::-1].argmax() - 1
1019 if limit_area == "inside":
1020 mask[:first] = False
1021 mask[last + 1 :] = False
1022 elif limit_area == "outside":
1023 mask[first + 1 : last] = False
1026def _fill_limit_area_2d(
1027 mask: npt.NDArray[np.bool_], limit_area: Literal["outside", "inside"]
1028) -> None:
1029 """Prepare 2d mask for ffill/bfill with limit_area.
1031 When called, mask will no longer faithfully represent when
1032 the corresponding are NA or not.
1034 Parameters
1035 ----------
1036 mask : np.ndarray[bool, ndim=1]
1037 Mask representing NA values when filling.
1038 limit_area : { "outside", "inside" }
1039 Whether to limit filling to outside or inside the outer most non-NA value.
1040 """
1041 neg_mask = ~mask.T
1042 if limit_area == "outside":
1043 # Identify inside
1044 la_mask = (
1045 np.maximum.accumulate(neg_mask, axis=0)
1046 & np.maximum.accumulate(neg_mask[::-1], axis=0)[::-1]
1047 )
1048 else:
1049 # Identify outside
1050 la_mask = (
1051 ~np.maximum.accumulate(neg_mask, axis=0)
1052 | ~np.maximum.accumulate(neg_mask[::-1], axis=0)[::-1]
1053 )
1054 mask[la_mask.T] = False
1057_fill_methods = {"pad": _pad_1d, "backfill": _backfill_1d}
1060def get_fill_func(method, ndim: int = 1):
1061 method = clean_fill_method(method)
1062 if ndim == 1:
1063 return _fill_methods[method]
1064 return {"pad": _pad_2d, "backfill": _backfill_2d}[method]
1067def clean_reindex_fill_method(method) -> ReindexMethod | None:
1068 if method is None:
1069 return None
1070 return clean_fill_method(method, allow_nearest=True)
1073def _interp_limit(
1074 invalid: npt.NDArray[np.bool_], fw_limit: int | None, bw_limit: int | None
1075) -> np.ndarray:
1076 """
1077 Get indexers of values that won't be filled
1078 because they exceed the limits.
1080 Parameters
1081 ----------
1082 invalid : np.ndarray[bool]
1083 fw_limit : int or None
1084 forward limit to index
1085 bw_limit : int or None
1086 backward limit to index
1088 Returns
1089 -------
1090 set of indexers
1092 Notes
1093 -----
1094 This is equivalent to the more readable, but slower
1096 .. code-block:: python
1098 def _interp_limit(invalid, fw_limit, bw_limit):
1099 for x in np.where(invalid)[0]:
1100 if invalid[max(0, x - fw_limit) : x + bw_limit + 1].all():
1101 yield x
1102 """
1103 # handle forward first; the backward direction is the same except
1104 # 1. operate on the reversed array
1105 # 2. subtract the returned indices from N - 1
1106 N = len(invalid)
1107 f_idx = np.array([], dtype=np.int64)
1108 b_idx = np.array([], dtype=np.int64)
1109 assume_unique = True
1111 def inner(invalid, limit: int):
1112 limit = min(limit, N - 1)
1113 windowed = np.lib.stride_tricks.sliding_window_view(invalid, limit + 1).all(1)
1114 idx = np.union1d(
1115 np.where(windowed)[0] + limit,
1116 np.where((~invalid[: limit + 1]).cumsum() == 0)[0],
1117 )
1118 return idx
1120 if fw_limit is not None:
1121 if fw_limit == 0:
1122 f_idx = np.where(invalid)[0]
1123 assume_unique = False
1124 else:
1125 f_idx = inner(invalid, fw_limit)
1127 if bw_limit is not None:
1128 if bw_limit == 0:
1129 # then we don't even need to care about backwards
1130 # just use forwards
1131 return f_idx
1132 else:
1133 b_idx = N - 1 - inner(invalid[::-1], bw_limit)
1134 if fw_limit == 0:
1135 return b_idx
1137 return np.intersect1d(f_idx, b_idx, assume_unique=assume_unique)