diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 2b437734a451a..a4aa3c28c3594 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -654,6 +654,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.is_year_start` and :meth:`DatetimeIndex.is_quarter_start` returning ``False`` on double-digit frequencies (:issue:`58523`) - Bug in :meth:`DatetimeIndex.union` and :meth:`DatetimeIndex.intersection` when ``unit`` was non-nanosecond (:issue:`59036`) - Bug in :meth:`Series.dt.microsecond` producing incorrect results for pyarrow backed :class:`Series`. (:issue:`59154`) +- Bug in :meth:`_validate_inferred_freq` where DateTimeIndex incorrectly raised a ``ValueError`` when assigning a logically equivalent frequency (:issue:`61086`) - Bug in :meth:`to_datetime` not respecting dayfirst if an uncommon date string was passed. (:issue:`58859`) - Bug in :meth:`to_datetime` on float array with missing values throwing ``FloatingPointError`` (:issue:`58419`) - Bug in :meth:`to_datetime` on float32 df with year, month, day etc. columns leads to precision issues and incorrect result. (:issue:`60506`) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index eba738c926497..76dee09c6f813 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -2519,15 +2519,38 @@ def _validate_inferred_freq( ------- freq : DateOffset or None """ + if inferred_freq is not None: - if freq is not None and freq != inferred_freq: + if freq is not None: + offset1, offset2 = to_offset(freq), to_offset(inferred_freq) + if type(offset1) == type(offset2): + if hasattr(offset1, "startingMonth") and hasattr( + offset2, "startingMonth" + ): + if (offset1.startingMonth - offset2.startingMonth) % 3 != 0: + raise ValueError( + f"Inferred frequency {inferred_freq} from passed " + "values does not conform to passed frequency " + f"{freq.freqstr}" + ) + return freq + if ( + hasattr(offset1, "n") + and hasattr(offset2, "n") + and offset1.n != offset2.n + ): + raise ValueError( + f"Inferred frequency {inferred_freq} from passed " + "values does not conform to passed frequency " + f"{freq.freqstr}" + ) + return freq raise ValueError( f"Inferred frequency {inferred_freq} from passed " "values does not conform to passed frequency " f"{freq.freqstr}" ) - if freq is None: - freq = inferred_freq + freq = inferred_freq return freq diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index c418b2a18008b..c95e42ecd2258 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -1204,3 +1204,19 @@ def test_dti_constructor_object_dtype_dayfirst_yearfirst_with_tz(self): result2 = DatetimeIndex([val], tz="US/Pacific", yearfirst=True) expected2 = DatetimeIndex([yfirst]).as_unit("s") tm.assert_index_equal(result2, expected2) + + def test_validate_inferred_freq_equivalence(self): + idx = date_range("2022-02-01", freq="QS-FEB", periods=4) + + new_idx = DatetimeIndex(idx, freq="QS-MAY") + assert isinstance(new_idx, DatetimeIndex) + + msg = ( + "Inferred frequency .* from passed " + "values does not conform to passed frequency .*" + ) + with pytest.raises( + ValueError, + match=msg, + ): + DatetimeIndex(idx, freq="QS-MAR")