From b62b8117f448cba064be35c24940738506dcf44d Mon Sep 17 00:00:00 2001 From: VenishPaneliya <141703684+VenishPaneliya@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:31:27 +0530 Subject: [PATCH] Pass min_periods to rolling() in WindowFeatures WindowFeatures documents min_periods as a pandas rolling() passthrough and stores it on the transformer, but transform() calls .rolling() with only the window in both the single-window and the list-of-windows branch. The value never reaches pandas, so rolling() keeps its default of "a full window is required" and the leading rows stay NaN whatever the user asks for. On a 6-row frame with window=3: min_periods=None -> [nan, nan, nan, 2.0, 3.0, 4.0] min_periods=1 -> [nan, nan, nan, 2.0, 3.0, 4.0] pandas reference -> [nan, 1.0, 1.5, 2.0, 3.0, 4.0] ExpandingWindowFeatures already forwards it, so the two transformers disagreed on a parameter they document identically. The default is unchanged: min_periods=None is what rolling() already assumed. --- .../timeseries/forecasting/window_features.py | 4 +- .../test_forecasting/test_window_features.py | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/feature_engine/timeseries/forecasting/window_features.py b/feature_engine/timeseries/forecasting/window_features.py index 24518a3f6..f5f73c7f7 100644 --- a/feature_engine/timeseries/forecasting/window_features.py +++ b/feature_engine/timeseries/forecasting/window_features.py @@ -221,7 +221,7 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: for win in self.window: tmp = ( X[self.variables_] - .rolling(window=win) + .rolling(window=win, min_periods=self.min_periods) .agg(self.functions) .shift(periods=self.periods, freq=self.freq) ) @@ -231,7 +231,7 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: else: tmp = ( X[self.variables_] - .rolling(window=self.window) + .rolling(window=self.window, min_periods=self.min_periods) .agg(self.functions) .shift(periods=self.periods, freq=self.freq) ) diff --git a/tests/test_time_series/test_forecasting/test_window_features.py b/tests/test_time_series/test_forecasting/test_window_features.py index e9701a2ef..1ec83ed8e 100644 --- a/tests/test_time_series/test_forecasting/test_window_features.py +++ b/tests/test_time_series/test_forecasting/test_window_features.py @@ -452,6 +452,43 @@ def test_multiple_windows(df_time): assert df_time_tr.equals(X_tr) +def test_min_periods_is_used(df_time): + # min_periods was accepted and documented but never reached pandas + # rolling(), so the leading rows stayed NaN whatever the user asked for. + variables = ["ambient_temp", "module_temp", "irradiation"] + + transformer = WindowFeatures(window=3, min_periods=1) + df_tr = transformer.fit_transform(df_time) + + expected = ( + df_time[variables].rolling(window=3, min_periods=1).agg("mean").shift(periods=1) + ) + expected.columns = [f"{var}_window_3_mean" for var in variables] + + assert_frame_equal(df_tr[expected.columns], expected) + + # with min_periods=1 only the very first row (shifted out) remains NaN + assert df_tr["ambient_temp_window_3_mean"].isna().sum() == 1 + + # the default is unchanged: pandas requires a full window + df_default = WindowFeatures(window=3).fit_transform(df_time) + assert df_default["ambient_temp_window_3_mean"].isna().sum() == 3 + + +def test_min_periods_is_used_with_multiple_windows(df_time): + transformer = WindowFeatures(window=[2, 3], min_periods=1) + df_tr = transformer.fit_transform(df_time) + + for win in (2, 3): + expected = ( + df_time["ambient_temp"].rolling(window=win, min_periods=1).mean().shift(1) + ) + assert_frame_equal( + df_tr[[f"ambient_temp_window_{win}_mean"]], + expected.to_frame(f"ambient_temp_window_{win}_mean"), + ) + + def test_sort_index(df_time): # Shuffle dataframe Xs = df_time.copy()