Skip to content

BUG: PeriodIndex.to_datetime inconsistent with its docstring #60350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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