Skip to content
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
-

Expand Down
9 changes: 8 additions & 1 deletion pandas/core/arrays/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new_data = np.asarray(
new_data = new_parr.dt.start_time

the list comprehension is going to be prohibitively slow (might be the cause of the current ASV failures). If you can use the vectorized datetimeindex (DTI) methods we have, I think that might fix things

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thanks @WillAyd, I'll have more time later to look into this, but for now...
new_parr.dt.start_time gives AttributeError: 'PeriodArray' object has no attribute 'dt'
new_parr.start_time gives RecursionError: maximum recursion depth exceeded while calling a Python object

[(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":
Expand Down
5 changes: 2 additions & 3 deletions pandas/tests/indexes/datetimes/methods/test_to_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 12 additions & 0 deletions pandas/tests/indexes/period/methods/test_to_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions pandas/tests/plotting/test_datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 41 additions & 23 deletions pandas/tests/resample/test_period_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
Loading