diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 1b12735f0e7c1..ca10fa46fe122 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -710,6 +710,7 @@ I/O Period ^^^^^^ +- Bug in :meth:`PeriodIndex.to_timestamp` casting to a DatetimeIndex of timestamps at the end of the period, instead of at the beginning of the period. (:issue:`59371`) - Fixed error message when passing invalid period alias to :meth:`PeriodIndex.to_timestamp` (:issue:`58974`) - diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index ae92e17332c76..963f2c85f22bb 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -822,7 +822,14 @@ def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray: new_parr = self.asfreq(freq, how=how) - new_data = libperiod.periodarr_to_dt64arr(new_parr.asi8, base) + is_start = how == "S" + if is_start: + new_data = np.asarray( + [(NaT if period is NaT else period.start_time) for period in new_parr] + ) + else: + new_data = libperiod.periodarr_to_dt64arr(new_parr.asi8, base) + dta = DatetimeArray._from_sequence(new_data, dtype=np.dtype("M8[ns]")) if self.freq.name == "B": diff --git a/pandas/tests/indexes/datetimes/methods/test_to_period.py b/pandas/tests/indexes/datetimes/methods/test_to_period.py index cd4a142dd5b30..a619430c06eb9 100644 --- a/pandas/tests/indexes/datetimes/methods/test_to_period.py +++ b/pandas/tests/indexes/datetimes/methods/test_to_period.py @@ -116,13 +116,12 @@ def test_to_period_infer(self): tm.assert_index_equal(pi1, pi2) - @pytest.mark.filterwarnings(r"ignore:PeriodDtype\[B\] is deprecated:FutureWarning") def test_period_dt64_round_trip(self): - dti = date_range("1/1/2000", "1/7/2002", freq="B") + dti = date_range("1/1/2000", "1/7/2002", freq="D") pi = dti.to_period() tm.assert_index_equal(pi.to_timestamp(), dti) - dti = date_range("1/1/2000", "1/7/2002", freq="B") + dti = date_range("1/1/2000", "1/7/2002", freq="D") pi = dti.to_period(freq="h") tm.assert_index_equal(pi.to_timestamp(), dti) diff --git a/pandas/tests/indexes/period/methods/test_to_timestamp.py b/pandas/tests/indexes/period/methods/test_to_timestamp.py index 4fe429ce71ee4..9411e9b773f7d 100644 --- a/pandas/tests/indexes/period/methods/test_to_timestamp.py +++ b/pandas/tests/indexes/period/methods/test_to_timestamp.py @@ -141,6 +141,18 @@ def test_to_timestamp_1703(self): result = index.to_timestamp() assert result[0] == Timestamp("1/1/2012") + def test_cast_to_timestamps_at_beginning_of_period(self): + # GH 59371 + index = period_range("2000", periods=3, freq="M") + result = index.to_timestamp("M") + + expected = DatetimeIndex( + ["2000-01-01", "2000-02-01", "2000-03-01"], + dtype="datetime64[ns]", + freq="MS", + ) + tm.assert_equal(result, expected) + def test_ms_to_timestamp_error_message(): # https://github.com/pandas-dev/pandas/issues/58974#issuecomment-2164265446 diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 1275f3d6f7d6d..cb4bd3dedae20 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -862,15 +862,12 @@ def test_mixed_freq_lf_first_hourly(self): for line in ax.get_lines(): assert PeriodIndex(data=line.get_xdata()).freq == "min" - @pytest.mark.filterwarnings(r"ignore:PeriodDtype\[B\] is deprecated:FutureWarning") def test_mixed_freq_irreg_period(self): ts = Series( np.arange(30, dtype=np.float64), index=date_range("2020-01-01", periods=30) ) irreg = ts.iloc[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 18, 29]] - msg = r"PeriodDtype\[B\] is deprecated" - with tm.assert_produces_warning(FutureWarning, match=msg): - rng = period_range("1/3/2000", periods=30, freq="B") + rng = period_range("1/3/2000", periods=30, freq="D") ps = Series(np.random.default_rng(2).standard_normal(len(rng)), rng) _, ax = mpl.pyplot.subplots() irreg.plot(ax=ax) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index e17529dfab00c..b87d2e7a95ec7 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -127,20 +127,30 @@ def test_selection(self, freq, kwargs): @pytest.mark.parametrize("month", MONTHS) @pytest.mark.parametrize("meth", ["ffill", "bfill"]) - @pytest.mark.parametrize("conv", ["start", "end"]) @pytest.mark.parametrize( - ("offset", "period"), [("D", "D"), ("B", "B"), ("ME", "M"), ("QE", "Q")] + ("offset", "period", "conv"), + [ + ("D", "D", "start"), + ("D", "D", "end"), + ("B", "B", "start"), + ("B", "B", "end"), + ("MS", "M", "start"), + ("ME", "M", "end"), + ("QS", "Q", "start"), + ("QE", "Q", "end"), + ], ) def test_annual_upsample_cases( self, offset, period, conv, meth, month, simple_period_range_series ): ts = simple_period_range_series("1/1/1990", "12/31/1991", freq=f"Y-{month}") - warn = FutureWarning if period == "B" else None - msg = r"PeriodDtype\[B\] is deprecated" - if warn is None: - msg = "Resampling with a PeriodIndex is deprecated" - warn = FutureWarning - with tm.assert_produces_warning(warn, match=msg): + + msg = ( + r"PeriodDtype\[B\] is deprecated" + if period == "B" + else "Resampling with a PeriodIndex is deprecated" + ) + with tm.assert_produces_warning(FutureWarning, match=msg): result = getattr(ts.resample(period, convention=conv), meth)() expected = result.to_timestamp(period, how=conv) expected = expected.asfreq(offset, meth).to_period() @@ -217,21 +227,29 @@ def test_annual_upsample2(self): tm.assert_series_equal(result, expected) @pytest.mark.parametrize("month", MONTHS) - @pytest.mark.parametrize("convention", ["start", "end"]) @pytest.mark.parametrize( - ("offset", "period"), [("D", "D"), ("B", "B"), ("ME", "M")] + ("offset", "period", "convention"), + [ + ("D", "D", "start"), + ("D", "D", "end"), + ("B", "B", "start"), + ("B", "B", "end"), + ("MS", "M", "start"), + ("ME", "M", "end"), + ], ) def test_quarterly_upsample( self, month, offset, period, convention, simple_period_range_series ): freq = f"Q-{month}" ts = simple_period_range_series("1/1/1990", "12/31/1995", freq=freq) - warn = FutureWarning if period == "B" else None - msg = r"PeriodDtype\[B\] is deprecated" - if warn is None: - msg = "Resampling with a PeriodIndex is deprecated" - warn = FutureWarning - with tm.assert_produces_warning(warn, match=msg): + + msg = ( + r"PeriodDtype\[B\] is deprecated" + if period == "B" + else "Resampling with a PeriodIndex is deprecated" + ) + with tm.assert_produces_warning(FutureWarning, match=msg): result = ts.resample(period, convention=convention).ffill() expected = result.to_timestamp(period, how=convention) expected = expected.asfreq(offset, "ffill").to_period() @@ -242,12 +260,12 @@ def test_quarterly_upsample( def test_monthly_upsample(self, target, convention, simple_period_range_series): ts = simple_period_range_series("1/1/1990", "12/31/1995", freq="M") - warn = None if target == "D" else FutureWarning - msg = r"PeriodDtype\[B\] is deprecated" - if warn is None: - msg = "Resampling with a PeriodIndex is deprecated" - warn = FutureWarning - with tm.assert_produces_warning(warn, match=msg): + msg = ( + "Resampling with a PeriodIndex is deprecated" + if target == "D" + else r"PeriodDtype\[B\] is deprecated" + ) + with tm.assert_produces_warning(FutureWarning, match=msg): result = ts.resample(target, convention=convention).ffill() expected = result.to_timestamp(target, how=convention) expected = expected.asfreq(target, "ffill").to_period() @@ -923,7 +941,7 @@ def test_resample_with_offset_month(self): rs = ser.resample("M", offset="3h") result = rs.mean() result = result.to_timestamp("M") - expected = ser.to_timestamp().resample("ME", offset="3h").mean() + expected = ser.to_timestamp().resample("MS", offset="3h").mean() # TODO: is non-tick the relevant characteristic? (GH 33815) expected.index = expected.index._with_freq(None) tm.assert_series_equal(result, expected)