@@ -156,30 +156,94 @@ def backtest(
156156 # Append results
157157 y_preds .append (y_pred )
158158 if residualize :
159- # Residuals
160- y_resid = _residualize_autoreg (
161- y_train = y_train ,
162- X_train = X_train ,
163- strategy = forecaster .state .strategy ,
164- lags = forecaster .lags ,
165- max_horizons = forecaster .max_horizons ,
166- artifacts = forecaster .state .artifacts ,
159+ # Residuals — only for autoregressive models that have a regressor
160+ artifacts = forecaster .state .artifacts
161+ has_regressor = (
162+ "regressor" in artifacts
163+ or "regressors" in artifacts
164+ or (
165+ "recursive" in artifacts
166+ and "regressor" in artifacts .get ("recursive" , {})
167+ )
167168 )
169+ if has_regressor :
170+ y_resid = _residualize_autoreg (
171+ y_train = y_train ,
172+ X_train = X_train ,
173+ strategy = forecaster .state .strategy ,
174+ lags = forecaster .lags ,
175+ max_horizons = forecaster .max_horizons ,
176+ artifacts = artifacts ,
177+ )
178+ else :
179+ # For non-autoregressive models (e.g. naive, snaive),
180+ # compute naive residuals: y[t] - y[t-1]
181+ _y_train_df = y_train .lazy ().collect (engine = "streaming" ) if isinstance (y_train , pl .LazyFrame ) else y_train
182+ idx_cols = _y_train_df .columns [:2 ]
183+ _target_col = _y_train_df .columns [- 1 ]
184+ y_resid = (
185+ _y_train_df .sort (idx_cols )
186+ .with_columns (
187+ (
188+ pl .col (_target_col )
189+ - pl .col (_target_col ).shift (1 ).over (idx_cols [0 ])
190+ ).alias ("y_resid" )
191+ )
192+ .select (* idx_cols , "y_resid" )
193+ .drop_nulls ()
194+ )
195+ if isinstance (y_resid , pl .LazyFrame ):
196+ y_resid = y_resid .collect (engine = "streaming" )
168197 y_resid = y_resid .with_columns (pl .lit (i ).alias ("split" ))
169198 y_resids .append (y_resid )
170199
171200 y_preds = pl .concat (y_preds )
172201 full_forecaster = forecaster .fit (y = y , X = X )
173202 if residualize :
174- y_resids = _merge_autoreg_residuals (
175- y = y ,
176- X = X ,
177- y_resids = pl . concat ( y_resids ),
178- strategy = full_forecaster . state . strategy ,
179- lags = forecaster . lags ,
180- max_horizons = forecaster . max_horizons ,
181- artifacts = full_forecaster . state . artifacts ,
203+ artifacts = full_forecaster . state . artifacts
204+ has_regressor = (
205+ "regressor" in artifacts
206+ or "regressors" in artifacts
207+ or (
208+ "recursive" in artifacts
209+ and "regressor" in artifacts . get ( "recursive" , {})
210+ )
182211 )
212+ # Collect any lazy residuals
213+ y_resids_collected = [
214+ r .collect (engine = "streaming" ) if isinstance (r , pl .LazyFrame ) else r
215+ for r in y_resids
216+ ]
217+ if has_regressor :
218+ y_resids = _merge_autoreg_residuals (
219+ y = y ,
220+ X = X ,
221+ y_resids = pl .concat (y_resids_collected ),
222+ strategy = full_forecaster .state .strategy ,
223+ lags = forecaster .lags ,
224+ max_horizons = forecaster .max_horizons ,
225+ artifacts = artifacts ,
226+ )
227+ else :
228+ # For non-autoregressive models, compute naive residuals for the full data
229+ _y_df = y .lazy ().collect (engine = "streaming" ) if isinstance (y , pl .LazyFrame ) else y
230+ idx_cols = _y_df .columns [:2 ]
231+ _target_col = _y_df .columns [- 1 ]
232+ y_resid = (
233+ _y_df .sort (idx_cols )
234+ .with_columns (
235+ (
236+ pl .col (_target_col )
237+ - pl .col (_target_col ).shift (1 ).over (idx_cols [0 ])
238+ ).alias ("y_resid" )
239+ )
240+ .select (* idx_cols , "y_resid" )
241+ .drop_nulls ()
242+ )
243+ combined = pl .concat (y_resids_collected )
244+ last_split = combined .get_column ("split" ).max () + 1
245+ y_resid = y_resid .with_columns (pl .lit (last_split ).alias ("split" ))
246+ y_resids = pl .concat ([combined , y_resid ])
183247 pl .disable_string_cache ()
184248 return y_preds , y_resids
185249 pl .disable_string_cache ()
0 commit comments