Skip to content

Commit 566972a

Browse files
committed
Fix log_training_metric causing IndexError for time series models
1 parent 1c9835d commit 566972a

5 files changed

Lines changed: 88 additions & 7 deletions

File tree

flaml/automl/ml.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,12 @@ def _eval_estimator(
616616
logger.warning(f"ValueError {e} happened in `metric_loss_score`, set `val_loss` to `np.inf`")
617617
metric_for_logging = {"pred_time": pred_time}
618618
if log_training_metric:
619-
train_pred_y = get_y_pred(estimator, X_train, eval_metric, task)
619+
# For time series forecasting, X_train may be a sampled dataset whose
620+
# test partition can be empty. Use the training partition from X_val
621+
# (which is the dataset used to define y_train above) to keep shapes
622+
# aligned and avoid empty prediction inputs.
623+
X_train_for_metric = X_val.X_train if isinstance(X_val, TimeSeriesDataset) else X_train
624+
train_pred_y = get_y_pred(estimator, X_train_for_metric, eval_metric, task)
620625
metric_for_logging["train_loss"] = metric_loss_score(
621626
eval_metric,
622627
train_pred_y,

flaml/automl/time_series/tcn.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ def fit(self, X_train: TimeSeriesDataset, y_train=None, budget=None, **kwargs):
264264
def predict(self, X):
265265
X = self.enrich(X)
266266
if isinstance(X, TimeSeriesDataset):
267-
df = X.X_val
267+
# Use X_train if X_val is empty (e.g., when computing training metrics)
268+
df = X.X_val if len(X.test_data) > 0 else X.X_train
268269
else:
269270
df = X
270271
dataset = DataframeDataset(

flaml/automl/time_series/tft.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,11 @@ def predict(self, X):
197197
last_data_cols = self.group_ids.copy()
198198
last_data_cols.append(self.target_names[0])
199199
last_data = self.data[lambda x: x.time_idx == x.time_idx.max()][last_data_cols]
200-
decoder_data = X.X_val if isinstance(X, TimeSeriesDataset) else X
200+
# Use X_train if test_data is empty (e.g., when computing training metrics)
201+
if isinstance(X, TimeSeriesDataset):
202+
decoder_data = X.X_val if len(X.test_data) > 0 else X.X_train
203+
else:
204+
decoder_data = X
201205
if "time_idx" not in decoder_data:
202206
decoder_data = add_time_idx_col(decoder_data)
203207
decoder_data["time_idx"] += encoder_data["time_idx"].max() + 1 - decoder_data["time_idx"].min()

flaml/automl/time_series/ts_model.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,13 @@ def predict(self, X: Union[TimeSeriesDataset, DataFrame], **kwargs):
194194

195195
elif isinstance(X, TimeSeriesDataset):
196196
data = X
197-
X = data.test_data[[self.time_col] + X.regressors]
197+
# By default we predict on the dataset's test partition.
198+
# Some internal call paths (e.g., training-metric logging) may pass a
199+
# dataset whose test partition is empty; fall back to train partition.
200+
if data.test_data is not None and len(data.test_data):
201+
X = data.test_data[data.regressors + [data.time_col]]
202+
else:
203+
X = data.train_data[data.regressors + [data.time_col]]
198204

199205
if self._model is not None:
200206
forecast = self._model.predict(X, **kwargs)
@@ -301,7 +307,13 @@ def predict(self, X, **kwargs):
301307

302308
if isinstance(X, TimeSeriesDataset):
303309
data = X
304-
X = data.test_data[data.regressors + [data.time_col]]
310+
# By default we predict on the dataset's test partition.
311+
# Some internal call paths (e.g., training-metric logging) may pass a
312+
# dataset whose test partition is empty; fall back to train partition.
313+
if data.test_data is not None and len(data.test_data):
314+
X = data.test_data[data.regressors + [data.time_col]]
315+
else:
316+
X = data.train_data[data.regressors + [data.time_col]]
305317

306318
X = X.rename(columns={self.time_col: "ds"})
307319
if self._model is not None:
@@ -327,11 +339,19 @@ def predict(self, X, **kwargs) -> pd.Series:
327339

328340
if isinstance(X, TimeSeriesDataset):
329341
data = X
330-
X = data.test_data[data.regressors + [data.time_col]]
342+
# By default we predict on the dataset's test partition.
343+
# Some internal call paths (e.g., training-metric logging) may pass a
344+
# dataset whose test partition is empty; fall back to train partition.
345+
if data.test_data is not None and len(data.test_data):
346+
X = data.test_data[data.regressors + [data.time_col]]
347+
else:
348+
X = data.train_data[data.regressors + [data.time_col]]
331349
else:
332350
X = X[self.regressors + [self.time_col]]
333351

334352
if isinstance(X, DataFrame):
353+
if X.shape[0] == 0:
354+
return pd.Series([], name=self.target_names[0], dtype=float)
335355
start = X[self.time_col].iloc[0]
336356
end = X[self.time_col].iloc[-1]
337357
if len(self.regressors):
@@ -829,6 +849,13 @@ def predict(self, X, **kwargs):
829849
if isinstance(X, TimeSeriesDataset):
830850
data = X
831851
X = data.test_data
852+
# By default we predict on the dataset's test partition.
853+
# Some internal call paths (e.g., training-metric logging) may pass a
854+
# dataset whose test partition is empty; fall back to train partition.
855+
if data.test_data is not None and len(data.test_data):
856+
X = data.test_data
857+
else:
858+
X = data.train_data
832859

833860
if self._model is not None:
834861
X = X[self.regressors]

test/automl/test_forecast.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,11 +681,55 @@ def split_by_date(df: pd.DataFrame, dt: datetime.date):
681681
print("yahoo!")
682682

683683

684+
def test_log_training_metric_ts_models():
685+
"""Test that log_training_metric=True works with time series models (arima, sarimax, holt-winters)."""
686+
import statsmodels.api as sm
687+
688+
from flaml.automl.task.time_series_task import TimeSeriesTask
689+
690+
estimators_all = TimeSeriesTask("forecast").estimators.keys()
691+
estimators_to_test = ["xgboost", "arima", "lassolars", "tcn", "snaive", "prophet", "orbit"]
692+
estimators = [
693+
est for est in estimators_to_test if est in estimators_all
694+
] # not all estimators available in current python env
695+
print(f"Testing estimators: {estimators}")
696+
697+
# Prepare data
698+
data = sm.datasets.co2.load_pandas().data["co2"]
699+
data = data.resample("MS").mean()
700+
data = data.bfill().ffill()
701+
data = data.to_frame().reset_index()
702+
data = data.rename(columns={"index": "ds", "co2": "y"})
703+
num_samples = data.shape[0]
704+
time_horizon = 12
705+
split_idx = num_samples - time_horizon
706+
df = data[:split_idx]
707+
708+
# Test each time series model with log_training_metric=True
709+
for estimator in estimators:
710+
print(f"\nTesting {estimator} with log_training_metric=True")
711+
automl = AutoML()
712+
settings = {
713+
"time_budget": 3,
714+
"metric": "mape",
715+
"task": "forecast",
716+
"eval_method": "holdout",
717+
"label": "y",
718+
"log_training_metric": True, # This should not cause errors
719+
"estimator_list": [estimator],
720+
}
721+
automl.fit(dataframe=df, **settings, period=time_horizon, force_cancel=True)
722+
print(f" ✅ {estimator} SUCCESS with log_training_metric=True")
723+
if automl.best_estimator:
724+
assert automl.best_estimator == estimator
725+
726+
684727
if __name__ == "__main__":
685728
# test_forecast_automl(60)
686729
# test_multivariate_forecast_num(5)
687730
# test_multivariate_forecast_cat(5)
688-
test_numpy()
731+
# test_numpy()
689732
# test_forecast_classification(5)
690733
# test_forecast_panel(5)
691734
# test_cv_step()
735+
test_log_training_metric_ts_models()

0 commit comments

Comments
 (0)