From 5c6643ade30abc378710b0c57c0c841be95ef5d4 Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 19 Jan 2021 08:44:29 -0800 Subject: [PATCH 1/2] ENH: Period comparisons with mismatched freq use py3 behavior --- doc/source/whatsnew/v1.3.0.rst | 2 +- pandas/_libs/tslibs/period.pyx | 4 ++++ pandas/core/arrays/datetimelike.py | 7 +++--- pandas/tests/arithmetic/test_period.py | 29 ++++++++++++----------- pandas/tests/scalar/period/test_period.py | 10 +++----- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 6a85bfd852e19..66eac1b05af77 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -320,7 +320,7 @@ I/O Period ^^^^^^ - +- Comparisons of :class:`Period` objects or :class:`Index`, :class:`Series`, or :class:`DataFrame` with mismatched ``PeriodDtype`` now behave like other mismatched-type comparisons, returning ``False`` for equals, ``True`` for not-equal, and raising ``TypeError`` for inequality checks (:issue:`??`) - - diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 5d3ad559ea718..ef7566f4980a9 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1541,6 +1541,10 @@ cdef class _Period(PeriodMixin): def __richcmp__(self, other, op): if is_period_object(other): if other.freq != self.freq: + if op == Py_EQ: + return False + if op == Py_NE: + return True msg = DIFFERENT_FREQ.format(cls=type(self).__name__, own_freq=self.freqstr, other_freq=other.freqstr) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index ef93e80d83d04..b2629e606f8f5 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -21,6 +21,7 @@ from pandas._libs import algos, lib from pandas._libs.tslibs import ( BaseOffset, + IncompatibleFrequency, NaT, NaTType, Period, @@ -441,7 +442,7 @@ def _validate_comparison_value(self, other): try: # GH#18435 strings get a pass from tzawareness compat other = self._scalar_from_string(other) - except ValueError: + except (ValueError, IncompatibleFrequency): # failed to parse as Timestamp/Timedelta/Period raise InvalidComparison(other) @@ -451,7 +452,7 @@ def _validate_comparison_value(self, other): other = self._scalar_type(other) # type: ignore[call-arg] try: self._check_compatible_with(other) - except TypeError as err: + except (TypeError, IncompatibleFrequency) as err: # e.g. tzawareness mismatch raise InvalidComparison(other) from err @@ -465,7 +466,7 @@ def _validate_comparison_value(self, other): try: other = self._validate_listlike(other, allow_object=True) self._check_compatible_with(other) - except TypeError as err: + except (TypeError, IncompatibleFrequency) as err: if is_object_dtype(getattr(other, "dtype", None)): # We will have to operate element-wise pass diff --git a/pandas/tests/arithmetic/test_period.py b/pandas/tests/arithmetic/test_period.py index c7a99fbb5c1a8..e2f69a2cdb9a0 100644 --- a/pandas/tests/arithmetic/test_period.py +++ b/pandas/tests/arithmetic/test_period.py @@ -278,32 +278,32 @@ def test_parr_cmp_pi_mismatched_freq_raises(self, freq, box_with_array): base = PeriodIndex(["2011-01", "2011-02", "2011-03", "2011-04"], freq=freq) base = tm.box_expected(base, box_with_array) - msg = "Input has different freq=A-DEC from " - with pytest.raises(IncompatibleFrequency, match=msg): + msg = rf"Invalid comparison between dtype=period\[{freq}\] and Period" + with pytest.raises(TypeError, match=msg): base <= Period("2011", freq="A") - with pytest.raises(IncompatibleFrequency, match=msg): + with pytest.raises(TypeError, match=msg): Period("2011", freq="A") >= base # TODO: Could parametrize over boxes for idx? idx = PeriodIndex(["2011", "2012", "2013", "2014"], freq="A") - rev_msg = r"Input has different freq=(M|2M|3M) from PeriodArray\(freq=A-DEC\)" + rev_msg = r"Invalid comparison between dtype=period\[A-DEC\] and PeriodArray" idx_msg = rev_msg if box_with_array in [tm.to_array, pd.array] else msg - with pytest.raises(IncompatibleFrequency, match=idx_msg): + with pytest.raises(TypeError, match=idx_msg): base <= idx # Different frequency - msg = "Input has different freq=4M from " - with pytest.raises(IncompatibleFrequency, match=msg): + msg = rf"Invalid comparison between dtype=period\[{freq}\] and Period" + with pytest.raises(TypeError, match=msg): base <= Period("2011", freq="4M") - with pytest.raises(IncompatibleFrequency, match=msg): + with pytest.raises(TypeError, match=msg): Period("2011", freq="4M") >= base idx = PeriodIndex(["2011", "2012", "2013", "2014"], freq="4M") - rev_msg = r"Input has different freq=(M|2M|3M) from PeriodArray\(freq=4M\)" + rev_msg = r"Invalid comparison between dtype=period\[4M\] and PeriodArray" idx_msg = rev_msg if box_with_array in [tm.to_array, pd.array] else msg - with pytest.raises(IncompatibleFrequency, match=idx_msg): + with pytest.raises(TypeError, match=idx_msg): base <= idx @pytest.mark.parametrize("freq", ["M", "2M", "3M"]) @@ -354,12 +354,13 @@ def test_pi_cmp_nat_mismatched_freq_raises(self, freq): idx1 = PeriodIndex(["2011-01", "2011-02", "NaT", "2011-05"], freq=freq) diff = PeriodIndex(["2011-02", "2011-01", "2011-04", "NaT"], freq="4M") - msg = "Input has different freq=4M from Period(Array|Index)" - with pytest.raises(IncompatibleFrequency, match=msg): + msg = rf"Invalid comparison between dtype=period\[{freq}\] and PeriodArray" + with pytest.raises(TypeError, match=msg): idx1 > diff - with pytest.raises(IncompatibleFrequency, match=msg): - idx1 == diff + result = idx1 == diff + expected = np.array([False, False, False, False], dtype=bool) + tm.assert_numpy_array_equal(result, expected) # TODO: De-duplicate with test_pi_cmp_nat @pytest.mark.parametrize("dtype", [object, None]) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 9b87e32510b41..1a52cc945ad09 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -34,9 +34,7 @@ def test_construction(self): i4 = Period("2005", freq="M") i5 = Period("2005", freq="m") - msg = r"Input has different freq=M from Period\(freq=A-DEC\)" - with pytest.raises(IncompatibleFrequency, match=msg): - i1 != i4 + assert i1 != i4 assert i4 == i5 i1 = Period.now("Q") @@ -1071,11 +1069,9 @@ def test_comparison_mismatched_freq(self): jan = Period("2000-01", "M") day = Period("2012-01-01", "D") + assert not jan == day + assert jan != day msg = r"Input has different freq=D from Period\(freq=M\)" - with pytest.raises(IncompatibleFrequency, match=msg): - jan == day - with pytest.raises(IncompatibleFrequency, match=msg): - jan != day with pytest.raises(IncompatibleFrequency, match=msg): jan < day with pytest.raises(IncompatibleFrequency, match=msg): From 63778f195f783b3117d1d5a2a38790c1da7dc740 Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 19 Jan 2021 15:44:40 -0800 Subject: [PATCH 2/2] suggested edits --- pandas/_libs/tslibs/period.pyx | 2 +- pandas/tests/arithmetic/test_period.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index ef7566f4980a9..e6014a72b0192 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1543,7 +1543,7 @@ cdef class _Period(PeriodMixin): if other.freq != self.freq: if op == Py_EQ: return False - if op == Py_NE: + elif op == Py_NE: return True msg = DIFFERENT_FREQ.format(cls=type(self).__name__, own_freq=self.freqstr, diff --git a/pandas/tests/arithmetic/test_period.py b/pandas/tests/arithmetic/test_period.py index e2f69a2cdb9a0..577b8dec1181d 100644 --- a/pandas/tests/arithmetic/test_period.py +++ b/pandas/tests/arithmetic/test_period.py @@ -272,7 +272,7 @@ def test_parr_cmp_pi(self, freq, box_with_array): tm.assert_equal(base <= idx, exp) @pytest.mark.parametrize("freq", ["M", "2M", "3M"]) - def test_parr_cmp_pi_mismatched_freq_raises(self, freq, box_with_array): + def test_parr_cmp_pi_mismatched_freq(self, freq, box_with_array): # GH#13200 # different base freq base = PeriodIndex(["2011-01", "2011-02", "2011-03", "2011-04"], freq=freq)