From 0b9c5588b76a6a8732e2bc72c219446ad28af9d6 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 28 May 2021 08:58:08 -0700 Subject: [PATCH] BUG: PeriodIndex.get_loc with mismatched freq (#41670) --- doc/source/whatsnew/v1.3.0.rst | 1 + pandas/core/indexes/period.py | 25 ++------------ pandas/tests/indexes/period/test_indexing.py | 35 ++++++++++++++++---- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 4cee0b07288ed..7d31dd9545f2d 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -938,6 +938,7 @@ Indexing - Bug in :meth:`Series.__delitem__` with ``ExtensionDtype`` incorrectly casting to ``ndarray`` (:issue:`40386`) - Bug in :meth:`DataFrame.loc` returning :class:`MultiIndex` in wrong order if indexer has duplicates (:issue:`40978`) - Bug in :meth:`DataFrame.__setitem__` raising ``TypeError`` when using a str subclass as the column name with a :class:`DatetimeIndex` (:issue:`37366`) +- Bug in :meth:`PeriodIndex.get_loc` failing to raise ``KeyError`` when given a :class:`Period` with a mismatched ``freq`` (:issue:`41670`) Missing ^^^^^^^ diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 2600363bc28eb..c1104b80a0a7a 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -4,10 +4,7 @@ datetime, timedelta, ) -from typing import ( - Any, - Hashable, -) +from typing import Hashable import warnings import numpy as np @@ -318,24 +315,6 @@ def _is_comparable_dtype(self, dtype: DtypeObj) -> bool: return False return dtype.freq == self.freq - # ------------------------------------------------------------------------ - # Indexing - - @doc(Index.__contains__) - def __contains__(self, key: Any) -> bool: - if isinstance(key, Period): - if key.freq != self.freq: - return False - else: - return key.ordinal in self._engine - else: - hash(key) - try: - self.get_loc(key) - return True - except KeyError: - return False - # ------------------------------------------------------------------------ # Index Methods @@ -472,6 +451,8 @@ def get_loc(self, key, method=None, tolerance=None): elif is_integer(key): # Period constructor will cast to string, which we dont want raise KeyError(key) + elif isinstance(key, Period) and key.freq != self.freq: + raise KeyError(key) try: key = Period(key, freq=self.freq) diff --git a/pandas/tests/indexes/period/test_indexing.py b/pandas/tests/indexes/period/test_indexing.py index e820c2250256e..a41d02cfbd394 100644 --- a/pandas/tests/indexes/period/test_indexing.py +++ b/pandas/tests/indexes/period/test_indexing.py @@ -338,15 +338,21 @@ def test_get_loc_integer(self): pi2.get_loc(46) # TODO: This method came from test_period; de-dup with version above - def test_get_loc2(self): + @pytest.mark.parametrize("method", [None, "pad", "backfill", "nearest"]) + def test_get_loc_method(self, method): idx = period_range("2000-01-01", periods=3) - for method in [None, "pad", "backfill", "nearest"]: - assert idx.get_loc(idx[1], method) == 1 - assert idx.get_loc(idx[1].asfreq("H", how="start"), method) == 1 - assert idx.get_loc(idx[1].to_timestamp(), method) == 1 - assert idx.get_loc(idx[1].to_timestamp().to_pydatetime(), method) == 1 - assert idx.get_loc(str(idx[1]), method) == 1 + assert idx.get_loc(idx[1], method) == 1 + assert idx.get_loc(idx[1].to_timestamp(), method) == 1 + assert idx.get_loc(idx[1].to_timestamp().to_pydatetime(), method) == 1 + assert idx.get_loc(str(idx[1]), method) == 1 + + key = idx[1].asfreq("H", how="start") + with pytest.raises(KeyError, match=str(key)): + idx.get_loc(key, method=method) + + # TODO: This method came from test_period; de-dup with version above + def test_get_loc3(self): idx = period_range("2000-01-01", periods=5)[::2] assert idx.get_loc("2000-01-02T12", method="nearest", tolerance="1 day") == 1 @@ -401,6 +407,21 @@ def test_get_loc_invalid_string_raises_keyerror(self): assert "A" not in ser assert "A" not in pi + def test_get_loc_mismatched_freq(self): + # see also test_get_indexer_mismatched_dtype testing we get analogous + # behavior for get_loc + dti = date_range("2016-01-01", periods=3) + pi = dti.to_period("D") + pi2 = dti.to_period("W") + pi3 = pi.view(pi2.dtype) # i.e. matching i8 representations + + with pytest.raises(KeyError, match="W-SUN"): + pi.get_loc(pi2[0]) + + with pytest.raises(KeyError, match="W-SUN"): + # even though we have matching i8 values + pi.get_loc(pi3[0]) + class TestGetIndexer: def test_get_indexer(self):