From 861d8cf9f7d428b09eafc794727b69b926f95ddc Mon Sep 17 00:00:00 2001 From: aram-cinnamon Date: Fri, 4 Oct 2024 01:07:16 +0200 Subject: [PATCH 1/3] check how == 'S' --- pandas/core/arrays/period.py | 9 ++++++++- .../indexes/period/methods/test_to_timestamp.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 7d0ad74f851f0..09d6b791ff1fd 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -811,7 +811,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( + [getattr(period, "start_time", period) 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/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 From 3dac96ac2c1ff6c001b0147703c0bbf7056ee023 Mon Sep 17 00:00:00 2001 From: aram-cinnamon Date: Fri, 4 Oct 2024 01:14:09 +0200 Subject: [PATCH 2/3] whatsnew --- doc/source/whatsnew/v3.0.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index a5b4560a47bc4..d9cf8aacebd62 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -640,6 +640,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`) - From ea089e77186cae809301d56ac5fc2d124d73e0f7 Mon Sep 17 00:00:00 2001 From: aram-cinnamon Date: Sun, 6 Oct 2024 11:44:20 +0200 Subject: [PATCH 3/3] tests --- pandas/core/arrays/period.py | 2 +- pandas/tests/resample/test_period_index.py | 64 ++++++++++++++-------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 09d6b791ff1fd..7f8661758d4d7 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -814,7 +814,7 @@ def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray: is_start = how == "S" if is_start: new_data = np.asarray( - [getattr(period, "start_time", period) for period in new_parr] + [(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) 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)