diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index e680c2db55a43..ba2dd71fa8b22 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -904,6 +904,7 @@ Indexing Missing ^^^^^^^ - Calling :meth:`fillna` on an empty Series now correctly returns a shallow copied object. The behaviour is now consistent with :class:`Index`, :class:`DataFrame` and a non-empty :class:`Series` (:issue:`32543`). +- Bug in :meth:`Series.interpolate` where kwarg ``limit_area`` had no effect when using methods ``pad``, ``ffill``, ``backfill`` and ``bfill`` (:issue:`26796`) - Bug in :meth:`replace` when argument ``to_replace`` is of type dict/list and is used on a :class:`Series` containing ```` was raising a ``TypeError``. The method now handles this by ignoring ```` values when doing the comparison for the replacement (:issue:`32621`) - Bug in :meth:`~Series.any` and :meth:`~Series.all` incorrectly returning ```` for all ``False`` or all ``True`` values using the nulllable boolean dtype and with ``skipna=False`` (:issue:`33253`) - Clarified documentation on interpolate with method =akima. The ``der`` parameter must be scalar or None (:issue:`33426`) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 13b98279169fd..2abe814620ba9 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +import functools import inspect import re from typing import TYPE_CHECKING, Any, List, Optional @@ -1091,7 +1092,7 @@ def interpolate( if (self.is_bool or self.is_integer) and not self.is_timedelta: return self if inplace else self.copy() - # a fill na type method + # a fillna type method try: m = missing.clean_fill_method(method) except ValueError: @@ -1103,6 +1104,7 @@ def interpolate( axis=axis, inplace=inplace, limit=limit, + limit_area=limit_area, fill_value=fill_value, coerce=coerce, downcast=downcast, @@ -1131,6 +1133,7 @@ def _interpolate_with_fill( axis: int = 0, inplace: bool = False, limit: Optional[int] = None, + limit_area: Optional[str] = None, fill_value: Optional[Any] = None, coerce: bool = False, downcast: Optional[str] = None, @@ -1152,15 +1155,43 @@ def _interpolate_with_fill( # We only get here for non-ExtensionBlock fill_value = convert_scalar_for_putitemlike(fill_value, self.values.dtype) - values = missing.interpolate_2d( - values, + interpolate_2d = functools.partial( + missing.interpolate_2d, method=method, - axis=axis, limit=limit, fill_value=fill_value, dtype=self.dtype, ) + if limit_area is None: + values = interpolate_2d(values, axis=axis) + else: + + def func(values): + invalid = isna(values) + + if not invalid.any(): + return values + + if not invalid.all(): + first = missing.find_valid_index(values, "first") + last = missing.find_valid_index(values, "last") + + values = interpolate_2d(values) + + if limit_area == "inside": + invalid[first : last + 1] = False + elif limit_area == "outside": + invalid[:first] = False + invalid[last + 1 :] = False + + values[invalid] = np.nan + else: + values = interpolate_2d(values) + return values + + values = np.apply_along_axis(func, axis, values) + blocks = [self.make_block_same_class(values, ndim=self.ndim)] return self._maybe_downcast(blocks, downcast) diff --git a/pandas/tests/series/methods/test_interpolate.py b/pandas/tests/series/methods/test_interpolate.py index db1c07e1bd276..acf82c7ac0ce6 100644 --- a/pandas/tests/series/methods/test_interpolate.py +++ b/pandas/tests/series/methods/test_interpolate.py @@ -429,6 +429,70 @@ def test_interp_limit_area(self): with pytest.raises(ValueError, match=msg): s.interpolate(method="linear", limit_area="abc") + def test_interp_limit_area_with_pad(self): + # https://github.com/pandas-dev/pandas/issues/26796 + s = Series([np.nan, np.nan, 3, np.nan, np.nan, np.nan, 7, np.nan, np.nan]) + + expected = Series([np.nan, np.nan, 3.0, 3.0, 3.0, 3.0, 7.0, np.nan, np.nan]) + result = s.interpolate(method="pad", limit_area="inside") + tm.assert_series_equal(result, expected) + + expected = Series( + [np.nan, np.nan, 3.0, 3.0, np.nan, np.nan, 7.0, np.nan, np.nan] + ) + result = s.interpolate(method="pad", limit_area="inside", limit=1) + tm.assert_series_equal(result, expected) + + expected = Series([np.nan, np.nan, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, 7.0]) + result = s.interpolate(method="pad", limit_area="outside") + tm.assert_series_equal(result, expected) + + expected = Series( + [np.nan, np.nan, 3.0, np.nan, np.nan, np.nan, 7.0, 7.0, np.nan] + ) + result = s.interpolate(method="pad", limit_area="outside", limit=1) + tm.assert_series_equal(result, expected) + + def test_interp_limit_area_with_backfill(self): + # https://github.com/pandas-dev/pandas/issues/26796 + s = Series([np.nan, np.nan, 3, np.nan, np.nan, np.nan, 7, np.nan, np.nan]) + + expected = Series([np.nan, np.nan, 3.0, 7.0, 7.0, 7.0, 7.0, np.nan, np.nan]) + result = s.interpolate(method="bfill", limit_area="inside") + tm.assert_series_equal(result, expected) + + expected = Series( + [np.nan, np.nan, 3.0, np.nan, np.nan, 7.0, 7.0, np.nan, np.nan] + ) + result = s.interpolate(method="bfill", limit_area="inside", limit=1) + tm.assert_series_equal(result, expected) + + expected = Series([3.0, 3.0, 3.0, np.nan, np.nan, np.nan, 7.0, np.nan, np.nan]) + result = s.interpolate(method="bfill", limit_area="outside") + tm.assert_series_equal(result, expected) + + expected = Series( + [np.nan, 3.0, 3.0, np.nan, np.nan, np.nan, 7.0, np.nan, np.nan] + ) + result = s.interpolate(method="bfill", limit_area="outside", limit=1) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("method", ["backfill", "bfill", "pad", "ffill"]) + @pytest.mark.parametrize("limit_area", ["inside", "outside", "both", None]) + def test_interp_limit_area_all_missing(self, method, limit_area): + # https://github.com/pandas-dev/pandas/issues/26796 + ser = Series([np.nan, np.nan, np.nan]) + result = ser.interpolate(method=method, limit_area=limit_area) + tm.assert_series_equal(result, ser) + + @pytest.mark.parametrize("method", ["backfill", "bfill", "pad", "ffill"]) + @pytest.mark.parametrize("limit_area", ["inside", "outside", "both", None]) + def test_interp_limit_area_none_missing(self, method, limit_area): + # https://github.com/pandas-dev/pandas/issues/26796 + ser = Series([1, 2, 3]) + result = ser.interpolate(method=method, limit_area=limit_area) + tm.assert_series_equal(result, ser) + def test_interp_limit_direction(self): # These tests are for issue #9218 -- fill NaNs in both directions. s = Series([1, 3, np.nan, np.nan, np.nan, 11])