From fdeeb05bd5718e652bc83fba31a56e817040343c Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 24 Sep 2025 09:26:43 -0700 Subject: [PATCH 1/2] Comment that BDay deprecation is unnecessary in one case --- pandas/plotting/_matplotlib/timeseries.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index beaf5b6259ef3..e7c349a15761c 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -308,7 +308,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 + # 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 From f88403b2f97dc6a0f1e9cc535920a228c2b8dccf Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 24 Sep 2025 09:46:05 -0700 Subject: [PATCH 2/2] DEPR: secondary_y plotting with implicit resample --- pandas/plotting/_matplotlib/timeseries.py | 9 ++++ pandas/tests/plotting/test_datetimelike.py | 56 +++++++++++++++++----- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index e7c349a15761c..b7bbc6fa1e8e3 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -21,6 +21,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, @@ -77,6 +79,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.", + Pandas4Warning, + stacklevel=find_stack_level(), + ) if is_superperiod(freq, ax_freq): # upsample input series = series.copy() # error: "Index" has no attribute "asfreq" diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 1275f3d6f7d6d..b0d1de827544b 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -15,6 +15,7 @@ BaseOffset, to_offset, ) +from pandas.errors import Pandas4Warning from pandas.core.dtypes.dtypes import PeriodDtype @@ -627,7 +628,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) lines = ax.get_lines() assert len(lines) == 1 assert len(ax.right_ax.get_lines()) == 1 @@ -820,7 +824,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" @@ -833,7 +839,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] @@ -844,7 +852,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() @@ -858,7 +868,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" @@ -951,7 +963,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 @@ -962,7 +976,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( @@ -994,7 +1010,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( @@ -1049,7 +1067,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) @@ -1096,7 +1116,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" @@ -1110,7 +1132,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" @@ -1247,7 +1271,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") @@ -1482,7 +1508,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