Skip to content
Open
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
17 changes: 16 additions & 1 deletion pandas/plotting/_matplotlib/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
OFFSET_TO_PERIOD_FREQSTR,
FreqGroup,
)
from pandas.errors import Pandas4Warning
from pandas.util._exceptions import find_stack_level

from pandas.core.dtypes.generic import (
ABCDatetimeIndex,
Expand Down Expand Up @@ -74,6 +76,13 @@ def maybe_resample(series: Series, ax: Axes, kwargs: dict[str, Any]):
series = series.to_period(freq=freq)

if ax_freq is not None and freq != ax_freq:
warnings.warn(
"Plotting with mixed-frequency series is deprecated and "
"will raise in a future version. Align series frequencies "
"before plotting instead.",
Comment on lines +81 to +82
Copy link
Member

Choose a reason for hiding this comment

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

Could go as far to suggest f"Call series.resample('{freq}') ..."?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure.

But now im wondering how much this will even help in moving towards #54485. In order to check if we're in this situation we still need the state information attached somehow.

I know "nuke it from space" isn't a viable approach to the plotting code, but it is really really bad.

Pandas4Warning,
stacklevel=find_stack_level(),
)
if is_superperiod(freq, ax_freq): # upsample input
series = series.copy()
# error: "Index" has no attribute "asfreq"
Expand Down Expand Up @@ -293,7 +302,13 @@ def maybe_convert_index(ax: Axes, data: NDFrameT) -> NDFrameT:
if isinstance(data.index, ABCDatetimeIndex):
data = data.tz_localize(None).to_period(freq=freq_str)
elif isinstance(data.index, ABCPeriodIndex):
data.index = data.index.asfreq(freq=freq_str, how="start")
# This will convert e.g. freq="60min" to freq="min", but will
Copy link
Member Author

Choose a reason for hiding this comment

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

this bit is unrelated, can revert and do it in a separate branch

# retain type(freq). It is not clear to @jbrockmendel why
# this is necessary as of 2025-09-24, but 18 tests fail
# without it.
new_freq = to_offset(freq_str, is_period=True)
assert type(new_freq) is type(data.index.freq)
data.index = data.index.asfreq(freq=new_freq, how="start")
return data


Expand Down
56 changes: 43 additions & 13 deletions pandas/tests/plotting/test_datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
BaseOffset,
to_offset,
)
from pandas.errors import Pandas4Warning

from pandas.core.dtypes.dtypes import PeriodDtype

Expand Down Expand Up @@ -628,7 +629,10 @@ def test_gap_upsample(self):

idxh = date_range(low.index[0], low.index[-1], freq="12h")
s = Series(np.random.default_rng(2).standard_normal(len(idxh)), idxh)
s.plot(secondary_y=True)

msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
s.plot(secondary_y=True)
Copy link
Member Author

Choose a reason for hiding this comment

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

it isn't clear to me why the warning is emitted here since ax isn't passed. if that keyword isn't the relevant factor in deciding if the statefulness is needed, then #62442 may not achieve its goals

lines = ax.get_lines()
assert len(lines) == 1
assert len(ax.right_ax.get_lines()) == 1
Expand Down Expand Up @@ -821,7 +825,9 @@ def test_mixed_freq_hf_first(self):
low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl)
_, ax = mpl.pyplot.subplots()
high.plot(ax=ax)
low.plot(ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
low.plot(ax=ax)
for line in ax.get_lines():
assert PeriodIndex(data=line.get_xdata()).freq == "D"

Expand All @@ -834,7 +840,9 @@ def test_mixed_freq_alignment(self):

_, ax = mpl.pyplot.subplots()
ax = ts.plot(ax=ax)
ts2.plot(style="r", ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
ts2.plot(style="r", ax=ax)

assert ax.lines[0].get_xdata()[0] == ax.lines[1].get_xdata()[0]

Expand All @@ -845,7 +853,9 @@ def test_mixed_freq_lf_first(self):
low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl)
_, ax = mpl.pyplot.subplots()
low.plot(legend=True, ax=ax)
high.plot(legend=True, ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
high.plot(legend=True, ax=ax)
for line in ax.get_lines():
assert PeriodIndex(data=line.get_xdata()).freq == "D"
leg = ax.get_legend()
Expand All @@ -859,7 +869,9 @@ def test_mixed_freq_lf_first_hourly(self):
low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl)
_, ax = mpl.pyplot.subplots()
low.plot(ax=ax)
high.plot(ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
high.plot(ax=ax)
for line in ax.get_lines():
assert PeriodIndex(data=line.get_xdata()).freq == "min"

Expand Down Expand Up @@ -952,7 +964,9 @@ def test_to_weekly_resampling(self):
low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl)
_, ax = mpl.pyplot.subplots()
high.plot(ax=ax)
low.plot(ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
low.plot(ax=ax)
for line in ax.get_lines():
assert PeriodIndex(data=line.get_xdata()).freq == idxh.freq

Expand All @@ -963,7 +977,9 @@ def test_from_weekly_resampling(self):
low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl)
_, ax = mpl.pyplot.subplots()
low.plot(ax=ax)
high.plot(ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
high.plot(ax=ax)

expected_h = idxh.to_period().asi8.astype(np.float64)
expected_l = np.array(
Expand Down Expand Up @@ -995,7 +1011,9 @@ def test_from_resampling_area_line_mixed(self, kind1, kind2):

_, ax = mpl.pyplot.subplots()
low.plot(kind=kind1, stacked=True, ax=ax)
high.plot(kind=kind2, stacked=True, ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
high.plot(kind=kind2, stacked=True, ax=ax)

# check low dataframe result
expected_x = np.array(
Expand Down Expand Up @@ -1050,7 +1068,9 @@ def test_from_resampling_area_line_mixed_high_to_low(self, kind1, kind2):
)
_, ax = mpl.pyplot.subplots()
high.plot(kind=kind1, stacked=True, ax=ax)
low.plot(kind=kind2, stacked=True, ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
low.plot(kind=kind2, stacked=True, ax=ax)

# check high dataframe result
expected_x = idxh.to_period().asi8.astype(np.float64)
Expand Down Expand Up @@ -1097,7 +1117,9 @@ def test_mixed_freq_second_millisecond(self):
# high to low
_, ax = mpl.pyplot.subplots()
high.plot(ax=ax)
low.plot(ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
low.plot(ax=ax)
assert len(ax.get_lines()) == 2
for line in ax.get_lines():
assert PeriodIndex(data=line.get_xdata()).freq == "ms"
Expand All @@ -1111,7 +1133,9 @@ def test_mixed_freq_second_millisecond_low_to_high(self):
# low to high
_, ax = mpl.pyplot.subplots()
low.plot(ax=ax)
high.plot(ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
high.plot(ax=ax)
assert len(ax.get_lines()) == 2
for line in ax.get_lines():
assert PeriodIndex(data=line.get_xdata()).freq == "ms"
Expand Down Expand Up @@ -1248,7 +1272,9 @@ def test_secondary_upsample(self):
low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl)
_, ax = mpl.pyplot.subplots()
low.plot(ax=ax)
ax = high.plot(secondary_y=True, ax=ax)
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
ax = high.plot(secondary_y=True, ax=ax)
for line in ax.get_lines():
assert PeriodIndex(line.get_xdata()).freq == "D"
assert hasattr(ax, "left_ax")
Expand Down Expand Up @@ -1483,7 +1509,11 @@ def test_secondary_y_mixed_freq_ts_xlim(self):
_, ax = mpl.pyplot.subplots()
ts.plot(ax=ax)
left_before, right_before = ax.get_xlim()
ts.resample("D").mean().plot(secondary_y=True, ax=ax)

rs = ts.resample("D").mean()
msg = "Plotting with mixed-frequency series is deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
rs.plot(secondary_y=True, ax=ax)
left_after, right_after = ax.get_xlim()

# a downsample should not have changed either limit
Expand Down
Loading