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._typing import (
26 ArrayLike,
27 AxisInt,
28 F,
29 ReindexMethod,
30 npt,
31)
32from pandas.compat._optional import import_optional_dependency
34from pandas.core.dtypes.cast import infer_dtype_from
35from pandas.core.dtypes.common import (
36 is_array_like,
37 is_bool_dtype,
38 is_numeric_dtype,
39 is_object_dtype,
40 needs_i8_conversion,
41)
42from pandas.core.dtypes.dtypes import (
43 ArrowDtype,
44 BaseMaskedDtype,
45 DatetimeTZDtype,
46)
47from pandas.core.dtypes.missing import (
48 is_valid_na_for_dtype,
49 isna,
50 na_value_for_dtype,
51)
53if TYPE_CHECKING:
54 from collections.abc import Callable
55 from typing import TypeAlias
57 from pandas import Index
59 _CubicBC: TypeAlias = Literal["not-a-knot", "clamped", "natural", "periodic"]
62def check_value_size(value, mask: npt.NDArray[np.bool_], length: int):
63 """
64 Validate the size of the values passed to ExtensionArray.fillna.
65 """
66 if is_array_like(value):
67 if len(value) != length:
68 raise ValueError(
69 f"Length of 'value' does not match. Got ({len(value)}) "
70 f" expected {length}"
71 )
72 value = value[mask]
74 return value
77def mask_missing(arr: ArrayLike, value) -> npt.NDArray[np.bool_]:
78 """
79 Return a masking array of same size/shape as arr
80 with entries equaling value set to True.
82 Parameters
83 ----------
84 arr : ArrayLike
85 value : scalar-like
86 Caller has ensured `not is_list_like(value)` and that it can be held
87 by `arr`.
89 Returns
90 -------
91 np.ndarray[bool]
92 """
93 dtype, value = infer_dtype_from(value)
95 if (
96 isinstance(arr.dtype, (BaseMaskedDtype, ArrowDtype))
97 and lib.is_float(value)
98 and np.isnan(value)
99 and not is_nan_na()
100 ):
101 # TODO: this should be done in an EA method?
102 if arr.dtype.kind == "f":
103 # GH#55127
104 if isinstance(arr.dtype, BaseMaskedDtype):
105 # error: "ExtensionArray" has no attribute "_data" [attr-defined]
106 mask = np.isnan(arr._data) & ~arr.isna() # type: ignore[attr-defined,operator]
107 return mask
108 else:
109 # error: "ExtensionArray" has no attribute "_pa_array" [attr-defined]
110 import pyarrow.compute as pc
112 mask = pc.is_nan(arr._pa_array).fill_null(False).to_numpy() # type: ignore[attr-defined]
113 return mask
115 elif arr.dtype.kind in "iu":
116 # GH#51237
117 mask = np.zeros(arr.shape, dtype=bool)
118 return mask
120 if isna(value):
121 return isna(arr)
123 # GH 21977
124 mask = np.zeros(arr.shape, dtype=bool)
125 if (
126 is_numeric_dtype(arr.dtype)
127 and not is_bool_dtype(arr.dtype)
128 and lib.is_bool(value)
129 ):
130 # e.g. test_replace_ea_float_with_bool, see GH#62048
131 pass
132 elif (
133 is_bool_dtype(arr.dtype) and is_numeric_dtype(dtype) and not lib.is_bool(value)
134 ):
135 # e.g. test_replace_ea_float_with_bool, see GH#62048
136 pass
137 elif is_numeric_dtype(arr.dtype) and isinstance(value, str):
138 # GH#29553 prevent numpy deprecation warnings
139 pass
140 elif is_object_dtype(arr.dtype):
141 # pre-compute mask to avoid comparison to NA
142 # e.g. test_replace_na_in_obj_column
143 arr_mask = ~isna(arr)
144 mask[arr_mask] = arr[arr_mask] == value
145 else:
146 new_mask = arr == value
148 if not isinstance(new_mask, np.ndarray):
149 # usually BooleanArray
150 new_mask = new_mask.to_numpy(dtype=bool, na_value=False)
151 mask = new_mask
153 return mask
156@overload
157def clean_fill_method(
158 method: Literal["ffill", "pad", "bfill", "backfill"],
159 *,
160 allow_nearest: Literal[False] = ...,
161) -> Literal["pad", "backfill"]: ...
164@overload
165def clean_fill_method(
166 method: Literal["ffill", "pad", "bfill", "backfill", "nearest"],
167 *,
168 allow_nearest: Literal[True],
169) -> Literal["pad", "backfill", "nearest"]: ...
172def clean_fill_method(
173 method: Literal["ffill", "pad", "bfill", "backfill", "nearest"],
174 *,
175 allow_nearest: bool = False,
176) -> Literal["pad", "backfill", "nearest"]:
177 if isinstance(method, str):
178 # error: Incompatible types in assignment (expression has type "str", variable
179 # has type "Literal['ffill', 'pad', 'bfill', 'backfill', 'nearest']")
180 method = method.lower() # type: ignore[assignment]
181 if method == "ffill":
182 method = "pad"
183 elif method == "bfill":
184 method = "backfill"
186 valid_methods = ["pad", "backfill"]
187 expecting = "pad (ffill) or backfill (bfill)"
188 if allow_nearest:
189 valid_methods.append("nearest")
190 expecting = "pad (ffill), backfill (bfill) or nearest"
191 if method not in valid_methods:
192 raise ValueError(f"Invalid fill method. Expecting {expecting}. Got {method}")
193 return method
196# interpolation methods that dispatch to np.interp
198NP_METHODS = ["linear", "time", "index", "values"]
200# interpolation methods that dispatch to _interpolate_scipy_wrapper
202SP_METHODS = [
203 "nearest",
204 "zero",
205 "slinear",
206 "quadratic",
207 "cubic",
208 "barycentric",
209 "krogh",
210 "spline",
211 "polynomial",
212 "from_derivatives",
213 "piecewise_polynomial",
214 "pchip",
215 "akima",
216 "cubicspline",
217]
220def clean_interp_method(method: str, index: Index, **kwargs) -> str:
221 order = kwargs.get("order")
223 if method in ("spline", "polynomial") and order is None:
224 raise ValueError("You must specify the order of the spline or polynomial.")
226 valid = NP_METHODS + SP_METHODS
227 if method not in valid:
228 raise ValueError(f"method must be one of {valid}. Got '{method}' instead.")
230 if method in ("krogh", "piecewise_polynomial", "pchip"):
231 if not index.is_monotonic_increasing:
232 raise ValueError(
233 f"{method} interpolation requires that the index be monotonic."
234 )
236 return method
239def find_valid_index(how: str, is_valid: npt.NDArray[np.bool_]) -> int | None:
240 """
241 Retrieves the positional index of the first valid value.
243 Parameters
244 ----------
245 how : {'first', 'last'}
246 Use this parameter to change between the first or last valid index.
247 is_valid: np.ndarray
248 Mask to find na_values.
250 Returns
251 -------
252 int or None
253 """
254 assert how in ["first", "last"]
256 if len(is_valid) == 0: # early stop
257 return None
259 if is_valid.ndim == 2:
260 # reduce axis 1
261 is_valid = is_valid.any(axis=1) # type: ignore[assignment]
263 if how == "first":
264 idxpos = is_valid[::].argmax()
266 elif how == "last":
267 idxpos = len(is_valid) - 1 - is_valid[::-1].argmax()
269 chk_notna = is_valid[idxpos]
271 if not chk_notna:
272 return None
273 # Incompatible return value type (got "signedinteger[Any]",
274 # expected "Optional[int]")
275 return idxpos # type: ignore[return-value]
278def validate_limit_direction(
279 limit_direction: str,
280) -> Literal["forward", "backward", "both"]:
281 valid_limit_directions = ["forward", "backward", "both"]
282 limit_direction = limit_direction.lower()
283 if limit_direction not in valid_limit_directions:
284 raise ValueError(
285 "Invalid limit_direction: expecting one of "
286 f"{valid_limit_directions}, got '{limit_direction}'."
287 )
288 # error: Incompatible return value type (got "str", expected
289 # "Literal['forward', 'backward', 'both']")
290 return limit_direction # type: ignore[return-value]
293def validate_limit_area(limit_area: str | None) -> Literal["inside", "outside"] | None:
294 if limit_area is not None:
295 valid_limit_areas = ["inside", "outside"]
296 limit_area = limit_area.lower()
297 if limit_area not in valid_limit_areas:
298 raise ValueError(
299 f"Invalid limit_area: expecting one of {valid_limit_areas}, got "
300 f"{limit_area}."
301 )
302 # error: Incompatible return value type (got "Optional[str]", expected
303 # "Optional[Literal['inside', 'outside']]")
304 return limit_area # type: ignore[return-value]
307def infer_limit_direction(
308 limit_direction: Literal["backward", "forward", "both"] | None, method: str
309) -> Literal["backward", "forward", "both"]:
310 # Set `limit_direction` depending on `method`
311 if limit_direction is None:
312 if method in ("backfill", "bfill"):
313 limit_direction = "backward"
314 else:
315 limit_direction = "forward"
316 else:
317 if method in ("pad", "ffill") and limit_direction != "forward":
318 raise ValueError(
319 f"`limit_direction` must be 'forward' for method `{method}`"
320 )
321 if method in ("backfill", "bfill") and limit_direction != "backward":
322 raise ValueError(
323 f"`limit_direction` must be 'backward' for method `{method}`"
324 )
325 return limit_direction
328def get_interp_index(method, index: Index) -> Index:
329 # create/use the index
330 if method == "linear":
331 # prior default
332 from pandas import RangeIndex
334 index = RangeIndex(len(index))
335 else:
336 methods = {"index", "values", "nearest", "time"}
337 is_numeric_or_datetime = (
338 is_numeric_dtype(index.dtype)
339 or isinstance(index.dtype, DatetimeTZDtype)
340 or lib.is_np_dtype(index.dtype, "mM")
341 )
342 valid = NP_METHODS + SP_METHODS
343 if method in valid:
344 if method not in methods and not is_numeric_or_datetime:
345 raise ValueError(
346 "Index column must be numeric or datetime type when "
347 f"using {method} method other than linear. "
348 "Try setting a numeric or datetime index column before "
349 "interpolating."
350 )
351 else:
352 raise ValueError(f"Can not interpolate with method={method}.")
354 if isna(index).any():
355 raise NotImplementedError(
356 "Interpolation with NaNs in the index "
357 "has not been implemented. Try filling "
358 "those NaNs before interpolating."
359 )
360 return index
363def interpolate_2d_inplace(
364 data: np.ndarray, # floating dtype
365 index: Index,
366 axis: AxisInt,
367 method: str = "linear",
368 limit: int | None = None,
369 limit_direction: str = "forward",
370 limit_area: str | None = None,
371 fill_value: Any | None = None,
372 mask=None,
373 **kwargs,
374) -> None:
375 """
376 Column-wise application of _interpolate_1d.
378 Notes
379 -----
380 Alters 'data' in-place.
382 The signature does differ from _interpolate_1d because it only
383 includes what is needed for Block.interpolate.
384 """
385 # validate the interp method
386 clean_interp_method(method, index, **kwargs)
388 if is_valid_na_for_dtype(fill_value, data.dtype):
389 fill_value = na_value_for_dtype(data.dtype, compat=False)
391 if method == "time":
392 if not needs_i8_conversion(index.dtype):
393 raise ValueError(
394 "time-weighted interpolation only works "
395 "on Series or DataFrames with a "
396 "DatetimeIndex"
397 )
398 method = "values"
400 limit_direction = validate_limit_direction(limit_direction)
401 limit_area_validated = validate_limit_area(limit_area)
403 # default limit is unlimited GH #16282
404 limit = algos.validate_limit(nobs=None, limit=limit)
406 indices = _index_to_interp_indices(index, method)
408 def func(yvalues: np.ndarray) -> None:
409 # process 1-d slices in the axis direction
411 _interpolate_1d(
412 indices=indices,
413 yvalues=yvalues,
414 method=method,
415 limit=limit,
416 limit_direction=limit_direction,
417 limit_area=limit_area_validated,
418 fill_value=fill_value,
419 bounds_error=False,
420 mask=mask,
421 **kwargs,
422 )
424 np.apply_along_axis(func, axis, data)
427def _index_to_interp_indices(index: Index, method: str) -> np.ndarray:
428 """
429 Convert Index to ndarray of indices to pass to NumPy/SciPy.
430 """
431 xarr = index._values
432 if needs_i8_conversion(xarr.dtype):
433 # GH#1646 for dt64tz
434 xarr = xarr.view("i8")
436 if method == "linear":
437 inds = xarr
438 inds = cast(np.ndarray, inds)
439 else:
440 inds = np.asarray(xarr)
442 if method in ("values", "index"):
443 if inds.dtype == np.object_:
444 inds = lib.maybe_convert_objects(inds)
446 return inds
449def _interpolate_1d(
450 indices: np.ndarray,
451 yvalues: np.ndarray,
452 method: str = "linear",
453 limit: int | None = None,
454 limit_direction: str = "forward",
455 limit_area: Literal["inside", "outside"] | None = None,
456 fill_value: Any | None = None,
457 bounds_error: bool = False,
458 order: int | None = None,
459 mask=None,
460 **kwargs,
461) -> None:
462 """
463 Logic for the 1-d interpolation. The input
464 indices and yvalues will each be 1-d arrays of the same length.
466 Bounds_error is currently hardcoded to False since non-scipy ones don't
467 take it as an argument.
469 Notes
470 -----
471 Fills 'yvalues' in-place.
472 """
473 if mask is not None:
474 invalid = mask
475 else:
476 invalid = isna(yvalues)
477 valid = ~invalid
479 if not valid.any():
480 return
482 if valid.all():
483 return
485 # These index pointers to invalid values... i.e. {0, 1, etc...
486 all_nans = np.flatnonzero(invalid)
488 first_valid_index = find_valid_index(how="first", is_valid=valid)
489 if first_valid_index is None: # no nan found in start
490 first_valid_index = 0
491 start_nans = np.arange(first_valid_index)
493 last_valid_index = find_valid_index(how="last", is_valid=valid)
494 if last_valid_index is None: # no nan found in end
495 last_valid_index = len(yvalues)
496 end_nans = np.arange(1 + last_valid_index, len(valid))
498 # preserve_nans contains indices of invalid values,
499 # but in this case, it is the final set of indices that need to be
500 # preserved as NaN after the interpolation.
502 # For example if limit_direction='forward' then preserve_nans will
503 # contain indices of NaNs at the beginning of the series, and NaNs that
504 # are more than 'limit' away from the prior non-NaN.
506 # set preserve_nans based on direction using _interp_limit
507 if limit_direction == "forward":
508 preserve_nans = np.union1d(start_nans, _interp_limit(invalid, limit, 0))
509 elif limit_direction == "backward":
510 preserve_nans = np.union1d(end_nans, _interp_limit(invalid, 0, limit))
511 else:
512 # both directions... just use _interp_limit
513 preserve_nans = np.unique(_interp_limit(invalid, limit, limit))
515 # if limit_area is set, add either mid or outside indices
516 # to preserve_nans GH #16284
517 if limit_area == "inside":
518 # preserve NaNs on the outside
519 preserve_nans = np.union1d(preserve_nans, start_nans)
520 preserve_nans = np.union1d(preserve_nans, end_nans)
521 elif limit_area == "outside":
522 # preserve NaNs on the inside
523 mid_nans = np.setdiff1d(all_nans, start_nans, assume_unique=True)
524 mid_nans = np.setdiff1d(mid_nans, end_nans, assume_unique=True)
525 preserve_nans = np.union1d(preserve_nans, mid_nans)
527 is_datetimelike = yvalues.dtype.kind in "mM"
529 if is_datetimelike:
530 yvalues = yvalues.view("i8")
532 if method in NP_METHODS:
533 # np.interp requires sorted X values, #21037
535 indexer = np.argsort(indices[valid])
536 yvalues[invalid] = np.interp(
537 indices[invalid], indices[valid][indexer], yvalues[valid][indexer]
538 )
539 else:
540 yvalues[invalid] = _interpolate_scipy_wrapper(
541 indices[valid],
542 yvalues[valid],
543 indices[invalid],
544 method=method,
545 fill_value=fill_value,
546 bounds_error=bounds_error,
547 order=order,
548 **kwargs,
549 )
551 if mask is not None:
552 mask[:] = False
553 mask[preserve_nans] = True
554 elif is_datetimelike:
555 yvalues[preserve_nans] = NaT.value
556 else:
557 yvalues[preserve_nans] = np.nan
558 return
561def _interpolate_scipy_wrapper(
562 x: np.ndarray,
563 y: np.ndarray,
564 new_x: np.ndarray,
565 method: str,
566 fill_value=None,
567 bounds_error: bool = False,
568 order=None,
569 **kwargs,
570):
571 """
572 Passed off to scipy.interpolate.interp1d. method is scipy's kind.
573 Returns an array interpolated at new_x. Add any new methods to
574 the list in _clean_interp_method.
575 """
576 extra = f"{method} interpolation requires SciPy."
577 import_optional_dependency("scipy", extra=extra)
578 from scipy import interpolate
580 new_x = np.asarray(new_x)
582 # ignores some kwargs that could be passed along.
583 alt_methods: dict[str, Callable[..., np.ndarray]] = {
584 "barycentric": interpolate.barycentric_interpolate,
585 "krogh": interpolate.krogh_interpolate,
586 "from_derivatives": _from_derivatives,
587 "piecewise_polynomial": _from_derivatives,
588 "cubicspline": _cubicspline_interpolate,
589 "akima": _akima_interpolate,
590 "pchip": interpolate.pchip_interpolate,
591 }
593 interp1d_methods = [
594 "nearest",
595 "zero",
596 "slinear",
597 "quadratic",
598 "cubic",
599 "polynomial",
600 ]
601 terp: Callable[..., np.ndarray] | None
602 if method in interp1d_methods:
603 if method == "polynomial":
604 kind = order
605 else:
606 kind = method
607 terp = interpolate.interp1d(
608 x, y, kind=kind, fill_value=fill_value, bounds_error=bounds_error
609 )
610 new_y = terp(new_x)
611 elif method == "spline":
612 # GH #10633, #24014
613 if isna(order) or (order <= 0):
614 raise ValueError(
615 f"order needs to be specified and greater than 0; got order: {order}"
616 )
617 terp = interpolate.UnivariateSpline(x, y, k=order, **kwargs)
618 new_y = terp(new_x)
619 else:
620 # GH 7295: need to be able to write for some reason
621 # in some circumstances: check all three
622 if not x.flags.writeable:
623 x = x.copy()
624 if not y.flags.writeable:
625 y = y.copy()
626 if not new_x.flags.writeable:
627 new_x = new_x.copy()
628 terp = alt_methods.get(method, None)
629 if terp is None:
630 raise ValueError(f"Can not interpolate with method={method}.")
632 # Make sure downcast is not in kwargs for alt methods
633 kwargs.pop("downcast", None)
634 new_y = terp(x, y, new_x, **kwargs)
635 return new_y
638def _from_derivatives(
639 xi: np.ndarray,
640 yi: np.ndarray,
641 x: np.ndarray,
642 order=None,
643 der: int | list[int] | None = 0,
644 extrapolate: bool = False,
645):
646 """
647 Convenience function for interpolate.BPoly.from_derivatives.
649 Construct a piecewise polynomial in the Bernstein basis, compatible
650 with the specified values and derivatives at breakpoints.
652 Parameters
653 ----------
654 xi : array-like
655 sorted 1D array of x-coordinates
656 yi : array-like or list of array-likes
657 yi[i][j] is the j-th derivative known at xi[i]
658 order: None or int or array-like of ints. Default: None.
659 Specifies the degree of local polynomials. If not None, some
660 derivatives are ignored.
661 der : int or list
662 How many derivatives to extract; None for all potentially nonzero
663 derivatives (that is a number equal to the number of points), or a
664 list of derivatives to extract. This number includes the function
665 value as 0th derivative.
666 extrapolate : bool, optional
667 Whether to extrapolate to ouf-of-bounds points based on first and last
668 intervals, or to return NaNs. Default: True.
670 See Also
671 --------
672 scipy.interpolate.BPoly.from_derivatives
674 Returns
675 -------
676 y : scalar or array-like
677 The result, of length R or length M or M by R.
678 """
679 from scipy import interpolate
681 # return the method for compat with scipy version & backwards compat
682 method = interpolate.BPoly.from_derivatives
683 m = method(xi, yi.reshape(-1, 1), orders=order, extrapolate=extrapolate)
685 return m(x)
688def _akima_interpolate(
689 xi: np.ndarray,
690 yi: np.ndarray,
691 x: np.ndarray,
692 der: int = 0,
693 axis: AxisInt = 0,
694):
695 """
696 Convenience function for akima interpolation.
697 xi and yi are arrays of values used to approximate some function f,
698 with ``yi = f(xi)``.
700 See `Akima1DInterpolator` for details.
702 Parameters
703 ----------
704 xi : np.ndarray
705 A sorted list of x-coordinates, of length N.
706 yi : np.ndarray
707 A 1-D array of real values. `yi`'s length along the interpolation
708 axis must be equal to the length of `xi`. If N-D array, use axis
709 parameter to select correct axis.
710 x : np.ndarray
711 Of length M.
712 der : int, optional
713 How many derivatives to extract. This number includes the function
714 value as 0th derivative.
715 axis : int, optional
716 Axis in the yi array corresponding to the x-coordinate values.
718 See Also
719 --------
720 scipy.interpolate.Akima1DInterpolator
722 Returns
723 -------
724 y : scalar or array-like
725 The result, of length R or length M or M by R,
727 """
728 from scipy import interpolate
730 P = interpolate.Akima1DInterpolator(xi, yi, axis=axis)
732 return P(x, nu=der)
735def _cubicspline_interpolate(
736 xi: np.ndarray,
737 yi: np.ndarray,
738 x: np.ndarray,
739 axis: AxisInt = 0,
740 bc_type: _CubicBC | tuple[Any, Any] = "not-a-knot",
741 extrapolate: Literal["periodic"] | bool | None = None,
742) -> np.ndarray:
743 """
744 Convenience function for cubic spline data interpolator.
746 See `scipy.interpolate.CubicSpline` for details.
748 Parameters
749 ----------
750 xi : np.ndarray, shape (n,)
751 1-d array containing values of the independent variable.
752 Values must be real, finite and in strictly increasing order.
753 yi : np.ndarray
754 Array containing values of the dependent variable. It can have
755 arbitrary number of dimensions, but the length along ``axis``
756 (see below) must match the length of ``x``. Values must be finite.
757 x : np.ndarray, shape (m,)
758 axis : int, optional
759 Axis along which `y` is assumed to be varying. Meaning that for
760 ``x[i]`` the corresponding values are ``np.take(y, i, axis=axis)``.
761 Default is 0.
762 bc_type : string or 2-tuple, optional
763 Boundary condition type. Two additional equations, given by the
764 boundary conditions, are required to determine all coefficients of
765 polynomials on each segment [2]_.
766 If `bc_type` is a string, then the specified condition will be applied
767 at both ends of a spline. Available conditions are:
768 * 'not-a-knot' (default): The first and second segment at a curve end
769 are the same polynomial. It is a good default when there is no
770 information on boundary conditions.
771 * 'periodic': The interpolated functions is assumed to be periodic
772 of period ``x[-1] - x[0]``. The first and last value of `y` must be
773 identical: ``y[0] == y[-1]``. This boundary condition will result in
774 ``y'[0] == y'[-1]`` and ``y''[0] == y''[-1]``.
775 * 'clamped': The first derivative at curves ends are zero. Assuming
776 a 1D `y`, ``bc_type=((1, 0.0), (1, 0.0))`` is the same condition.
777 * 'natural': The second derivative at curve ends are zero. Assuming
778 a 1D `y`, ``bc_type=((2, 0.0), (2, 0.0))`` is the same condition.
779 If `bc_type` is a 2-tuple, the first and the second value will be
780 applied at the curve start and end respectively. The tuple values can
781 be one of the previously mentioned strings (except 'periodic') or a
782 tuple `(order, deriv_values)` allowing to specify arbitrary
783 derivatives at curve ends:
784 * `order`: the derivative order, 1 or 2.
785 * `deriv_value`: array-like containing derivative values, shape must
786 be the same as `y`, excluding ``axis`` dimension. For example, if
787 `y` is 1D, then `deriv_value` must be a scalar. If `y` is 3D with
788 the shape (n0, n1, n2) and axis=2, then `deriv_value` must be 2D
789 and have the shape (n0, n1).
790 extrapolate : {bool, 'periodic', None}, optional
791 If bool, determines whether to extrapolate to out-of-bounds points
792 based on first and last intervals, or to return NaNs. If 'periodic',
793 periodic extrapolation is used. If None (default), ``extrapolate`` is
794 set to 'periodic' for ``bc_type='periodic'`` and to True otherwise.
796 See Also
797 --------
798 scipy.interpolate.CubicHermiteSpline
800 Returns
801 -------
802 y : scalar or array-like
803 The result, of shape (m,)
805 References
806 ----------
807 .. [1] `Cubic Spline Interpolation
808 <https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation>`_
809 on Wikiversity.
810 .. [2] Carl de Boor, "A Practical Guide to Splines", Springer-Verlag, 1978.
811 """
812 from scipy import interpolate
814 P = interpolate.CubicSpline(
815 xi, yi, axis=axis, bc_type=bc_type, extrapolate=extrapolate
816 )
818 return P(x)
821def pad_or_backfill_inplace(
822 values: np.ndarray,
823 method: Literal["pad", "backfill"] = "pad",
824 axis: AxisInt = 0,
825 limit: int | None = None,
826 limit_area: Literal["inside", "outside"] | None = None,
827) -> None:
828 """
829 Perform an actual interpolation of values, values will be make 2-d if
830 needed fills inplace, returns the result.
832 Parameters
833 ----------
834 values: np.ndarray
835 Input array.
836 method: str, default "pad"
837 Interpolation method. Could be "bfill" or "pad"
838 axis: 0 or 1
839 Interpolation axis
840 limit: int, optional
841 Index limit on interpolation.
842 limit_area: str, optional
843 Limit area for interpolation. Can be "inside" or "outside"
845 Notes
846 -----
847 Modifies values in-place.
848 """
849 transf = (lambda x: x) if axis == 0 else (lambda x: x.T)
851 # reshape a 1 dim if needed
852 if values.ndim == 1:
853 if axis != 0: # pragma: no cover
854 raise AssertionError("cannot interpolate on an ndim == 1 with axis != 0")
855 values = values.reshape((1, *values.shape))
857 method = clean_fill_method(method)
858 tvalues = transf(values)
860 func = get_fill_func(method, ndim=2)
861 # _pad_2d and _backfill_2d both modify tvalues inplace
862 func(tvalues, limit=limit, limit_area=limit_area)
865def _fillna_prep(
866 values, mask: npt.NDArray[np.bool_] | None = None
867) -> npt.NDArray[np.bool_]:
868 # boilerplate for _pad_1d, _backfill_1d, _pad_2d, _backfill_2d
870 if mask is None:
871 mask = isna(values)
873 return mask
876def _datetimelike_compat(func: F) -> F:
877 """
878 Wrapper to handle datetime64 and timedelta64 dtypes.
879 """
881 @wraps(func)
882 def new_func(
883 values,
884 limit: int | None = None,
885 limit_area: Literal["inside", "outside"] | None = None,
886 mask=None,
887 ):
888 if needs_i8_conversion(values.dtype):
889 if mask is None:
890 # This needs to occur before casting to int64
891 mask = isna(values)
893 result, mask = func(
894 values.view("i8"), limit=limit, limit_area=limit_area, mask=mask
895 )
896 return result.view(values.dtype), mask
898 return func(values, limit=limit, limit_area=limit_area, mask=mask)
900 return cast(F, new_func)
903@_datetimelike_compat
904def _pad_1d(
905 values: np.ndarray,
906 limit: int | None = None,
907 limit_area: Literal["inside", "outside"] | None = None,
908 mask: npt.NDArray[np.bool_] | None = None,
909) -> tuple[np.ndarray, npt.NDArray[np.bool_]]:
910 mask = _fillna_prep(values, mask)
911 if limit_area is not None and not mask.all():
912 _fill_limit_area_1d(mask, limit_area)
913 algos.pad_inplace(values, mask, limit=limit)
914 return values, mask
917@_datetimelike_compat
918def _backfill_1d(
919 values: np.ndarray,
920 limit: int | None = None,
921 limit_area: Literal["inside", "outside"] | None = None,
922 mask: npt.NDArray[np.bool_] | None = None,
923) -> tuple[np.ndarray, npt.NDArray[np.bool_]]:
924 mask = _fillna_prep(values, mask)
925 if limit_area is not None and not mask.all():
926 _fill_limit_area_1d(mask, limit_area)
927 algos.backfill_inplace(values, mask, limit=limit)
928 return values, mask
931@_datetimelike_compat
932def _pad_2d(
933 values: np.ndarray,
934 limit: int | None = None,
935 limit_area: Literal["inside", "outside"] | None = None,
936 mask: npt.NDArray[np.bool_] | None = None,
937) -> tuple[np.ndarray, npt.NDArray[np.bool_]]:
938 mask = _fillna_prep(values, mask)
939 if limit_area is not None:
940 _fill_limit_area_2d(mask, limit_area)
942 if values.size:
943 algos.pad_2d_inplace(values, mask, limit=limit)
944 return values, mask
947@_datetimelike_compat
948def _backfill_2d(
949 values,
950 limit: int | None = None,
951 limit_area: Literal["inside", "outside"] | None = None,
952 mask: npt.NDArray[np.bool_] | None = None,
953):
954 mask = _fillna_prep(values, mask)
955 if limit_area is not None:
956 _fill_limit_area_2d(mask, limit_area)
958 if values.size:
959 algos.backfill_2d_inplace(values, mask, limit=limit)
960 else:
961 # for test coverage
962 pass
963 return values, mask
966def _fill_limit_area_1d(
967 mask: npt.NDArray[np.bool_], limit_area: Literal["outside", "inside"]
968) -> None:
969 """Prepare 1d mask for ffill/bfill with limit_area.
971 Caller is responsible for checking at least one value of mask is False.
972 When called, mask will no longer faithfully represent when
973 the corresponding are NA or not.
975 Parameters
976 ----------
977 mask : np.ndarray[bool, ndim=1]
978 Mask representing NA values when filling.
979 limit_area : { "outside", "inside" }
980 Whether to limit filling to outside or inside the outer most non-NA value.
981 """
982 neg_mask = ~mask
983 first = neg_mask.argmax()
984 last = len(neg_mask) - neg_mask[::-1].argmax() - 1
985 if limit_area == "inside":
986 mask[:first] = False
987 mask[last + 1 :] = False
988 elif limit_area == "outside":
989 mask[first + 1 : last] = False
992def _fill_limit_area_2d(
993 mask: npt.NDArray[np.bool_], limit_area: Literal["outside", "inside"]
994) -> None:
995 """Prepare 2d mask for ffill/bfill with limit_area.
997 When called, mask will no longer faithfully represent when
998 the corresponding are NA or not.
1000 Parameters
1001 ----------
1002 mask : np.ndarray[bool, ndim=1]
1003 Mask representing NA values when filling.
1004 limit_area : { "outside", "inside" }
1005 Whether to limit filling to outside or inside the outer most non-NA value.
1006 """
1007 neg_mask = ~mask.T
1008 if limit_area == "outside":
1009 # Identify inside
1010 la_mask = (
1011 np.maximum.accumulate(neg_mask, axis=0)
1012 & np.maximum.accumulate(neg_mask[::-1], axis=0)[::-1]
1013 )
1014 else:
1015 # Identify outside
1016 la_mask = (
1017 ~np.maximum.accumulate(neg_mask, axis=0)
1018 | ~np.maximum.accumulate(neg_mask[::-1], axis=0)[::-1]
1019 )
1020 mask[la_mask.T] = False
1023_fill_methods = {"pad": _pad_1d, "backfill": _backfill_1d}
1026def get_fill_func(method, ndim: int = 1):
1027 method = clean_fill_method(method)
1028 if ndim == 1:
1029 return _fill_methods[method]
1030 return {"pad": _pad_2d, "backfill": _backfill_2d}[method]
1033def clean_reindex_fill_method(method) -> ReindexMethod | None:
1034 if method is None:
1035 return None
1036 return clean_fill_method(method, allow_nearest=True)
1039def _interp_limit(
1040 invalid: npt.NDArray[np.bool_], fw_limit: int | None, bw_limit: int | None
1041) -> np.ndarray:
1042 """
1043 Get indexers of values that won't be filled
1044 because they exceed the limits.
1046 Parameters
1047 ----------
1048 invalid : np.ndarray[bool]
1049 fw_limit : int or None
1050 forward limit to index
1051 bw_limit : int or None
1052 backward limit to index
1054 Returns
1055 -------
1056 set of indexers
1058 Notes
1059 -----
1060 This is equivalent to the more readable, but slower
1062 .. code-block:: python
1064 def _interp_limit(invalid, fw_limit, bw_limit):
1065 for x in np.where(invalid)[0]:
1066 if invalid[max(0, x - fw_limit) : x + bw_limit + 1].all():
1067 yield x
1068 """
1069 # handle forward first; the backward direction is the same except
1070 # 1. operate on the reversed array
1071 # 2. subtract the returned indices from N - 1
1072 N = len(invalid)
1073 f_idx = np.array([], dtype=np.int64)
1074 b_idx = np.array([], dtype=np.int64)
1075 assume_unique = True
1077 def inner(invalid, limit: int):
1078 limit = min(limit, N - 1)
1079 windowed = np.lib.stride_tricks.sliding_window_view(invalid, limit + 1).all(1)
1080 idx = np.union1d(
1081 np.where(windowed)[0] + limit,
1082 np.where((~invalid[: limit + 1]).cumsum() == 0)[0],
1083 )
1084 return idx
1086 if fw_limit is not None:
1087 if fw_limit == 0:
1088 f_idx = np.where(invalid)[0]
1089 assume_unique = False
1090 else:
1091 f_idx = inner(invalid, fw_limit)
1093 if bw_limit is not None:
1094 if bw_limit == 0:
1095 # then we don't even need to care about backwards
1096 # just use forwards
1097 return f_idx
1098 else:
1099 b_idx = N - 1 - inner(invalid[::-1], bw_limit)
1100 if fw_limit == 0:
1101 return b_idx
1103 return np.intersect1d(f_idx, b_idx, assume_unique=assume_unique)