diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 54175fada6e56..e07a8fa0469f4 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -149,6 +149,8 @@ Indexing - Bug in slicing on a :class:`DatetimeIndex` with a partial-timestamp dropping high-resolution indices near the end of a year, quarter, or month (:issue:`31064`) - Bug in :meth:`PeriodIndex.get_loc` treating higher-resolution strings differently from :meth:`PeriodIndex.get_value` (:issue:`31172`) - Bug in :meth:`Series.at` and :meth:`DataFrame.at` not matching ``.loc`` behavior when looking up an integer in a :class:`Float64Index` (:issue:`31329`) +- Bug in :meth:`PeriodIndex.is_monotonic` incorrectly returning ``True`` when containing leading ``NaT`` entries (:issue:`31437`) +- Missing ^^^^^^^ diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index 2dfc14378baf6..1915eaf6e07dd 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -514,8 +514,7 @@ cdef class PeriodEngine(Int64Engine): return super(PeriodEngine, self).vgetter().view("i8") cdef _call_monotonic(self, values): - # super(...) pattern doesn't seem to work with `cdef` - return Int64Engine._call_monotonic(self, values.view('i8')) + return algos.is_monotonic(values, timelike=True) def get_indexer(self, values): cdef: diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py index 16fa0b0c25925..248df3291f040 100644 --- a/pandas/tests/indexes/period/test_period.py +++ b/pandas/tests/indexes/period/test_period.py @@ -662,3 +662,44 @@ def test_maybe_convert_timedelta(): msg = r"Input has different freq=B from PeriodIndex\(freq=D\)" with pytest.raises(ValueError, match=msg): pi._maybe_convert_timedelta(offset) + + +def test_is_monotonic_with_nat(): + # GH#31437 + # PeriodIndex.is_monotonic should behave analogously to DatetimeIndex, + # in particular never be monotonic when we have NaT + dti = pd.date_range("2016-01-01", periods=3) + pi = dti.to_period("D") + tdi = pd.Index(dti.view("timedelta64[ns]")) + + for obj in [pi, pi._engine, dti, dti._engine, tdi, tdi._engine]: + if isinstance(obj, pd.Index): + # i.e. not Engines + assert obj.is_monotonic + assert obj.is_monotonic_increasing + assert not obj.is_monotonic_decreasing + assert obj.is_unique + + dti1 = dti.insert(0, pd.NaT) + pi1 = dti1.to_period("D") + tdi1 = pd.Index(dti1.view("timedelta64[ns]")) + + for obj in [pi1, pi1._engine, dti1, dti1._engine, tdi1, tdi1._engine]: + if isinstance(obj, pd.Index): + # i.e. not Engines + assert not obj.is_monotonic + assert not obj.is_monotonic_increasing + assert not obj.is_monotonic_decreasing + assert obj.is_unique + + dti2 = dti.insert(3, pd.NaT) + pi2 = dti2.to_period("H") + tdi2 = pd.Index(dti2.view("timedelta64[ns]")) + + for obj in [pi2, pi2._engine, dti2, dti2._engine, tdi2, tdi2._engine]: + if isinstance(obj, pd.Index): + # i.e. not Engines + assert not obj.is_monotonic + assert not obj.is_monotonic_increasing + assert not obj.is_monotonic_decreasing + assert obj.is_unique diff --git a/pandas/tests/reductions/test_reductions.py b/pandas/tests/reductions/test_reductions.py index 8d2058ffab643..0b312fe2f8990 100644 --- a/pandas/tests/reductions/test_reductions.py +++ b/pandas/tests/reductions/test_reductions.py @@ -440,7 +440,8 @@ def test_minmax_period(self): # monotonic idx1 = pd.PeriodIndex([NaT, "2011-01-01", "2011-01-02", "2011-01-03"], freq="D") - assert idx1.is_monotonic + assert not idx1.is_monotonic + assert idx1[1:].is_monotonic # non-monotonic idx2 = pd.PeriodIndex(