Skip to content
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

BUG: PeriodIndex.to_datetime inconsistent with its docstring #61077

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -745,6 +745,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
10 changes: 9 additions & 1 deletion pandas/core/arrays/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,15 @@ 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:
start_time = np.vectorize(
lambda period: (NaT if period is NaT else period.start_time)
)
new_data = start_time(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
110 changes: 75 additions & 35 deletions pandas/tests/resample/test_period_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,32 @@ 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, check_stacklevel=False
):
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 @@ -183,7 +195,9 @@ def test_basic_upsample(self, freq, simple_period_range_series):
result = ts.resample("Y-DEC").mean()

msg = "The 'convention' keyword in Series.resample is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
resampled = result.resample(freq, convention="end").ffill()
expected = result.to_timestamp(freq, how="end")
expected = expected.asfreq(freq, "ffill").to_period(freq)
Expand All @@ -194,7 +208,9 @@ def test_upsample_with_limit(self):
ts = Series(np.random.default_rng(2).standard_normal(len(rng)), rng)

msg = "The 'convention' keyword in Series.resample is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
result = ts.resample("M", convention="end").ffill(limit=2)
expected = ts.asfreq("M").reindex(result.index, method="ffill", limit=2)
tm.assert_series_equal(result, expected)
Expand All @@ -217,21 +233,31 @@ 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, check_stacklevel=False
):
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 +268,14 @@ 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, check_stacklevel=False
):
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 @@ -328,7 +356,7 @@ def test_with_local_timezone(self, tz):
series = Series(1, index=index)
series = series.tz_convert(local_timezone)
msg = "Converting to PeriodArray/Index representation will drop timezone"
with tm.assert_produces_warning(UserWarning, match=msg):
with tm.assert_produces_warning(UserWarning, match=msg, check_stacklevel=False):
result = series.resample("D").mean().to_period()

# Create the expected series
Expand Down Expand Up @@ -420,7 +448,7 @@ def test_weekly_upsample(self, day, target, convention, simple_period_range_seri
if warn is None:
msg = "Resampling with a PeriodIndex is deprecated"
warn = FutureWarning
with tm.assert_produces_warning(warn, match=msg):
with tm.assert_produces_warning(warn, match=msg, check_stacklevel=False):
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 @@ -455,7 +483,9 @@ def test_resample_to_quarterly_start_end(self, simple_period_range_series, how):
# conforms, but different month
ts = simple_period_range_series("1990", "1992", freq="Y-JUN")
msg = "The 'convention' keyword in Series.resample is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
result = ts.resample("Q-MAR", convention=how).ffill()
expected = ts.asfreq("Q-MAR", how=how)
expected = expected.reindex(result.index, method="ffill")
Expand Down Expand Up @@ -505,7 +535,9 @@ def test_upsample_daily_business_daily(self, simple_period_range_series):

ts = simple_period_range_series("1/1/2000", "2/1/2000")
msg = "The 'convention' keyword in Series.resample is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
result = ts.resample("h", convention="s").asfreq()
exp_rng = period_range("1/1/2000", "2/1/2000 23:00", freq="h")
expected = ts.asfreq("h", how="s").reindex(exp_rng)
Expand Down Expand Up @@ -568,7 +600,7 @@ def test_resample_tz_localized2(self):

# for good measure
msg = "Converting to PeriodArray/Index representation will drop timezone "
with tm.assert_produces_warning(UserWarning, match=msg):
with tm.assert_produces_warning(UserWarning, match=msg, check_stacklevel=False):
result = s.resample("D").mean().to_period()
ex_index = period_range("2001-09-20", periods=1, freq="D")
expected = Series([1.5], index=ex_index)
Expand Down Expand Up @@ -867,7 +899,9 @@ def test_resample_with_nat(self, periods, values, freq, expected_values):
)
expected = DataFrame(expected_values, index=expected_index)
msg = "Resampling with a PeriodIndex is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
rs = frame.resample(freq)
result = rs.mean()
tm.assert_frame_equal(result, expected)
Expand Down Expand Up @@ -906,7 +940,9 @@ def test_resample_with_offset(self, start, end, start_freq, end_freq, offset):
pi = period_range(start, end, freq=start_freq)
ser = Series(np.arange(len(pi)), index=pi)
msg = "Resampling with a PeriodIndex is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
rs = ser.resample(end_freq, offset=offset)
result = rs.mean()
result = result.to_timestamp(end_freq)
Expand All @@ -919,11 +955,13 @@ def test_resample_with_offset_month(self):
pi = period_range("19910905 12:00", "19910909 1:00", freq="h")
ser = Series(np.arange(len(pi)), index=pi)
msg = "Resampling with a PeriodIndex is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
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 Expand Up @@ -967,7 +1005,9 @@ def test_sum_min_count(self):
data[3:6] = np.nan
s = Series(data, index).to_period()
msg = "Resampling with a PeriodIndex is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(
FutureWarning, match=msg, check_stacklevel=False
):
rs = s.resample("Q")
result = rs.sum(min_count=1)
expected = Series(
Expand Down Expand Up @@ -1055,7 +1095,7 @@ def test_corner_cases_period(simple_period_range_series):
len0pts = simple_period_range_series("2007-01", "2010-05", freq="M")[:0]
# it works
msg = "Resampling with a PeriodIndex is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with tm.assert_produces_warning(FutureWarning, match=msg, check_stacklevel=False):
result = len0pts.resample("Y-DEC").mean()
assert len(result) == 0

Expand Down
Loading