diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 81545ada63ce5..016ece3b57e73 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -158,7 +158,7 @@ Interval Indexing ^^^^^^^^ -- +- Bug in indexing on a :class:`Series` or :class:`DataFrame` with a :class:`DatetimeIndex` when passing a string, the return type depended on whether the index was monotonic (:issue:`24892`) - Missing diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 2a2993dedb25d..5745a7960f448 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -604,23 +604,8 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime): return start, end def _can_partial_date_slice(self, reso: Resolution) -> bool: - assert isinstance(reso, Resolution), (type(reso), reso) - if ( - self.is_monotonic - and reso.attrname in ["day", "hour", "minute", "second"] - and self._resolution_obj >= reso - ): - # These resolution/monotonicity validations came from GH3931, - # GH3452 and GH2369. - - # See also GH14826 - return False - - if reso.attrname == "microsecond": - # _partial_date_slice doesn't allow microsecond resolution, but - # _parsed_string_to_bounds allows it. - return False - return True + # History of conversation GH#3452, GH#3931, GH#2369, GH#14826 + return reso > self._resolution_obj def _deprecate_mismatched_indexing(self, key) -> None: # GH#36148 diff --git a/pandas/tests/indexes/datetimes/test_partial_slicing.py b/pandas/tests/indexes/datetimes/test_partial_slicing.py index 882515799f943..5e1fdc3b62f42 100644 --- a/pandas/tests/indexes/datetimes/test_partial_slicing.py +++ b/pandas/tests/indexes/datetimes/test_partial_slicing.py @@ -20,6 +20,55 @@ class TestSlicing: + def test_return_type_doesnt_depend_on_monotonicity(self): + # GH#24892 we get Series back regardless of whether our DTI is monotonic + dti = date_range(start="2015-5-13 23:59:00", freq="min", periods=3) + ser = Series(range(3), index=dti) + + # non-monotonic index + ser2 = Series(range(3), index=[dti[1], dti[0], dti[2]]) + + # key with resolution strictly lower than "min" + key = "2015-5-14 00" + + # monotonic increasing index + result = ser.loc[key] + expected = ser.iloc[1:] + tm.assert_series_equal(result, expected) + + # monotonic decreasing index + result = ser.iloc[::-1].loc[key] + expected = ser.iloc[::-1][:-1] + tm.assert_series_equal(result, expected) + + # non-monotonic index + result2 = ser2.loc[key] + expected2 = ser2.iloc[::2] + tm.assert_series_equal(result2, expected2) + + def test_return_type_doesnt_depend_on_monotonicity_higher_reso(self): + # GH#24892 we get Series back regardless of whether our DTI is monotonic + dti = date_range(start="2015-5-13 23:59:00", freq="min", periods=3) + ser = Series(range(3), index=dti) + + # non-monotonic index + ser2 = Series(range(3), index=[dti[1], dti[0], dti[2]]) + + # key with resolution strictly *higher) than "min" + key = "2015-5-14 00:00:00" + + # monotonic increasing index + result = ser.loc[key] + assert result == 1 + + # monotonic decreasing index + result = ser.iloc[::-1].loc[key] + assert result == 1 + + # non-monotonic index + result2 = ser2.loc[key] + assert result2 == 0 + def test_monotone_DTI_indexing_bug(self): # GH 19362 # Testing accessing the first element in a monotonic descending @@ -38,9 +87,19 @@ def test_monotone_DTI_indexing_bug(self): expected = DataFrame({0: list(range(5)), "date": date_index}) tm.assert_frame_equal(df, expected) - df = DataFrame({"A": [1, 2, 3]}, index=date_range("20170101", periods=3)[::-1]) - expected = DataFrame({"A": 1}, index=date_range("20170103", periods=1)[::-1]) - tm.assert_frame_equal(df.loc["2017-01-03"], expected) + # We get a slice because df.index's resolution is hourly and we + # are slicing with a daily-resolution string. If both were daily, + # we would get a single item back + dti = date_range("20170101 01:00:00", periods=3) + df = DataFrame({"A": [1, 2, 3]}, index=dti[::-1]) + + expected = DataFrame({"A": 1}, index=dti[-1:][::-1]) + result = df.loc["2017-01-03"] + tm.assert_frame_equal(result, expected) + + result2 = df.iloc[::-1].loc["2017-01-03"] + expected2 = expected.iloc[::-1] + tm.assert_frame_equal(result2, expected2) def test_slice_year(self): dti = date_range(freq="B", start=datetime(2005, 1, 1), periods=500)