adamkahle commited on
Commit
8c39017
·
verified ·
1 Parent(s): d3c8efa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -181
app.py CHANGED
@@ -22,9 +22,6 @@ from statsforecast.models import (
22
  from utilsforecast.evaluation import evaluate
23
  from utilsforecast.losses import mae, mape, mase, mse, rmse, smape
24
 
25
- from sklearn.linear_model import Ridge as SkRidge
26
- from sklearn.linear_model import Lasso as SkLasso
27
-
28
 
29
  REQUIRED_COLS = ["unique_id", "ds", "y"]
30
  PLOT_TAIL_POINTS = 300
@@ -119,10 +116,67 @@ def align_future_x_to_horizon(df_train, X_future, xcols, h):
119
  return X_h.reset_index(drop=True)
120
 
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  def create_future_plot(fcst_df, original_df, title="Forecast"):
123
- plt.figure(figsize=(12, 7))
124
  if fcst_df is None or fcst_df.empty:
125
  return None
 
126
  forecast_cols = [c for c in fcst_df.columns if c not in ["unique_id", "ds"]]
127
  unique_ids = fcst_df["unique_id"].unique()
128
  colors = plt.cm.tab10.colors
@@ -160,6 +214,7 @@ def create_future_plot(fcst_df, original_df, title="Forecast"):
160
 
161
  fig = plt.gcf()
162
  fig.autofmt_xdate()
 
163
  return fig
164
 
165
 
@@ -178,110 +233,6 @@ def export_results(eval_df, future_df):
178
  return out
179
 
180
 
181
- def _normalize_timegpt_output(tgpt_raw, df_y, h):
182
- tgpt = _ensure_pandas_df(tgpt_raw)
183
- if tgpt is None or tgpt.empty:
184
- raise ValueError("TimeGPT returned empty output.")
185
- if "unique_id" not in tgpt.columns or "ds" not in tgpt.columns:
186
- raise ValueError(f"TimeGPT output missing required columns. Got: {list(tgpt.columns)}")
187
- tgpt["ds"] = pd.to_datetime(tgpt["ds"], errors="coerce")
188
- if tgpt["ds"].isna().any():
189
- raise ValueError("TimeGPT output has invalid ds values.")
190
- out_col = None
191
- for c in tgpt.columns:
192
- if c.lower() in ("timegpt", "yhat", "y_hat", "forecast"):
193
- out_col = c
194
- break
195
- if out_col is None:
196
- non_id = [c for c in tgpt.columns if c not in ["unique_id", "ds"]]
197
- if non_id:
198
- out_col = non_id[0]
199
- if out_col is None:
200
- raise ValueError("TimeGPT output has no forecast column.")
201
- if out_col != "timegpt":
202
- tgpt = tgpt.rename(columns={out_col: "timegpt"})
203
- last_ds = df_y.groupby("unique_id", as_index=False)["ds"].max().rename(columns={"ds": "last_ds"})
204
- tgpt = tgpt.merge(last_ds, on="unique_id", how="inner")
205
- tgpt = tgpt[tgpt["ds"] > tgpt["last_ds"]].copy()
206
- tgpt = tgpt.sort_values(["unique_id", "ds"]).reset_index(drop=True)
207
- tgpt = tgpt.groupby("unique_id", as_index=False).head(h).copy()
208
- counts = tgpt.groupby("unique_id")["ds"].size()
209
- missing_ids = sorted(set(last_ds["unique_id"]) - set(counts.index))
210
- short_ids = sorted([uid for uid, c in counts.items() if c < h])
211
- if missing_ids or short_ids:
212
- parts = []
213
- if missing_ids:
214
- parts.append(f"Missing TimeGPT future rows for: {', '.join(missing_ids)}")
215
- if short_ids:
216
- parts.append(f"Not enough TimeGPT rows (need {h}) for: {', '.join(short_ids)}")
217
- raise ValueError(" | ".join(parts))
218
- tgpt = tgpt[["unique_id", "ds", "timegpt"]].copy()
219
- return tgpt
220
-
221
-
222
- def _fit_predict_linear_recursive(df_y, X_future_h, xcols, h, model_kind, alpha, n_lags):
223
- df_y = df_y.sort_values(["unique_id", "ds"]).reset_index(drop=True)
224
- Xf = None
225
- if xcols:
226
- Xf = X_future_h.sort_values(["unique_id", "ds"]).reset_index(drop=True)
227
-
228
- out_rows = []
229
- for uid, g in df_y.groupby("unique_id"):
230
- y_hist = g["y"].to_numpy(dtype=float)
231
- if len(y_hist) <= n_lags + 5:
232
- continue
233
-
234
- if xcols:
235
- xf_u = Xf[Xf["unique_id"] == uid].copy()
236
- xf_u = xf_u.sort_values("ds")
237
- if len(xf_u) != h:
238
- raise ValueError(f"X_df horizon mismatch for {uid}. Expected {h}, got {len(xf_u)}")
239
- x_future = xf_u[xcols].to_numpy(dtype=float)
240
- ds_future = xf_u["ds"].to_numpy()
241
- else:
242
- ds_last = g["ds"].max()
243
- ds_future = pd.date_range(ds_last + pd.Timedelta(days=1), periods=h, freq="D").to_numpy()
244
- x_future = None
245
-
246
- X_train = []
247
- y_train = []
248
- if xcols:
249
- hist_exog = g[xcols].to_numpy(dtype=float)
250
- for t in range(n_lags, len(y_hist)):
251
- feats = []
252
- feats.extend(y_hist[t - n_lags : t].tolist())
253
- if xcols:
254
- feats.extend(hist_exog[t].tolist())
255
- X_train.append(feats)
256
- y_train.append(y_hist[t])
257
- X_train = np.asarray(X_train, dtype=float)
258
- y_train = np.asarray(y_train, dtype=float)
259
-
260
- if model_kind == "ridge":
261
- mdl = SkRidge(alpha=float(alpha), random_state=0)
262
- else:
263
- mdl = SkLasso(alpha=float(alpha), random_state=0, max_iter=10000)
264
-
265
- mdl.fit(X_train, y_train)
266
-
267
- preds = []
268
- y_buf = y_hist.copy()
269
- for step in range(h):
270
- feats = []
271
- feats.extend(y_buf[-n_lags:].tolist())
272
- if xcols:
273
- feats.extend(x_future[step].tolist())
274
- yhat = float(mdl.predict(np.asarray(feats, dtype=float).reshape(1, -1))[0])
275
- preds.append(yhat)
276
- y_buf = np.append(y_buf, yhat)
277
-
278
- for i in range(h):
279
- out_rows.append((uid, pd.to_datetime(ds_future[i]), preds[i]))
280
-
281
- colname = model_kind
282
- return pd.DataFrame(out_rows, columns=["unique_id", "ds", colname])
283
-
284
-
285
  def run_forecast(
286
  train_file,
287
  freq,
@@ -291,6 +242,7 @@ def run_forecast(
291
  cv_windows,
292
  future_h,
293
  loss_name,
 
294
  use_histavg,
295
  use_naive,
296
  use_snaive,
@@ -302,11 +254,6 @@ def run_forecast(
302
  nixtla_api_key,
303
  use_nf_lstm,
304
  use_tc_prophet,
305
- use_ridge,
306
- use_lasso,
307
- ridge_alpha,
308
- lasso_alpha,
309
- linear_lags,
310
  xcols,
311
  future_x_file,
312
  last_eval,
@@ -338,7 +285,6 @@ def run_forecast(
338
  cv_step_size = _to_int(cv_step_size, "cv_step_size")
339
  cv_windows = _to_int(cv_windows, "cv_windows")
340
  future_h = _to_int(future_h, "future_h")
341
- linear_lags = _to_int(linear_lags, "linear_lags")
342
 
343
  df, candidate_xcols, msg = load_training_data(train_file)
344
  if df is None:
@@ -348,7 +294,7 @@ def run_forecast(
348
  train_tail = _tail_history(df_y, n=PLOT_TAIL_POINTS)
349
 
350
  use_exog = bool(xcols)
351
- needs_future_x = use_exog and (use_timegpt or use_nf_lstm or use_ridge or use_lasso)
352
  X_future_h = None
353
  df_exog = None
354
 
@@ -402,19 +348,22 @@ def run_forecast(
402
  future_parts = []
403
 
404
  if uni_models:
405
- sf_u = StatsForecast(models=uni_models, freq=freq, n_jobs=-1)
406
- cv_u = sf_u.cross_validation(df=df_y, h=cv_h, step_size=cv_step_size, n_windows=cv_windows)
407
- ev_u = evaluate(cv_u, metrics=[loss_fn])
408
- ev_u["pipeline"] = "univariate"
409
- eval_parts.append(ev_u)
 
410
  fc_u = sf_u.forecast(df=df_y, h=future_h)
 
411
  future_parts.append(fc_u)
412
 
413
  if exog_models:
414
  if not use_exog or X_future_h is None or df_exog is None:
415
  return keep("Exogenous model selected but predictors/X_df are missing.")
416
- sf_x = StatsForecast(models=exog_models, freq=freq, n_jobs=-1)
417
  fc_x = sf_x.forecast(df=df_exog, h=future_h, X_df=X_future_h)
 
418
  future_parts.append(fc_x)
419
 
420
  if use_timegpt:
@@ -458,25 +407,23 @@ def run_forecast(
458
  futr_exog_list = list(xcols)
459
  model = LSTM(
460
  h=future_h,
461
- max_steps=200,
462
- input_size=max(3 * future_h, 30),
463
- encoder_hidden_size=64,
464
- decoder_hidden_size=64,
465
- batch_size=32,
466
  futr_exog_list=futr_exog_list,
467
  alias="lstm",
468
  )
469
  nf = NeuralForecast(models=[model], freq=freq)
470
  nf.fit(df=nf_df)
471
- if use_exog:
472
- pred = nf.predict(futr_df=futr_df).reset_index(drop=False)
473
- else:
474
- pred = nf.predict().reset_index(drop=False)
475
  cols = [c for c in pred.columns if c not in ["unique_id", "ds"]]
476
  if not cols:
477
  return keep("LSTM produced no forecast columns.")
478
  main_col = cols[0]
479
  lstm_df = pred[["unique_id", "ds", main_col]].rename(columns={main_col: "lstm"})
 
480
  future_parts.append(lstm_df)
481
  except Exception as e:
482
  return keep(f"LSTM failed: {type(e).__name__}: {e}")
@@ -489,47 +436,11 @@ def run_forecast(
489
  try:
490
  p = Prophet(alias="prophet")
491
  prop = p.forecast(df=df_y, h=future_h, freq=freq)
492
- prop = _ensure_pandas_df(prop)
493
- if prop is None or prop.empty:
494
- return keep("Prophet returned empty output.")
495
- if "prophet" not in prop.columns:
496
- non_id = [c for c in prop.columns if c not in ["unique_id", "ds"]]
497
- if not non_id:
498
- return keep(f"Prophet output missing forecast column. Got: {list(prop.columns)}")
499
- prop = prop.rename(columns={non_id[0]: "prophet"})
500
- prop = prop[["unique_id", "ds", "prophet"]]
501
  future_parts.append(prop)
502
  except Exception as e:
503
  return keep(f"Prophet failed: {type(e).__name__}: {e}")
504
 
505
- if use_ridge:
506
- if use_exog and X_future_h is None:
507
- return keep("Ridge selected with predictors but X_df is missing/invalid.")
508
- ridge_df = _fit_predict_linear_recursive(
509
- df_y=df_y,
510
- X_future_h=X_future_h,
511
- xcols=list(xcols) if use_exog else [],
512
- h=future_h,
513
- model_kind="ridge",
514
- alpha=float(ridge_alpha),
515
- n_lags=linear_lags,
516
- )
517
- future_parts.append(ridge_df)
518
-
519
- if use_lasso:
520
- if use_exog and X_future_h is None:
521
- return keep("Lasso selected with predictors but X_df is missing/invalid.")
522
- lasso_df = _fit_predict_linear_recursive(
523
- df_y=df_y,
524
- X_future_h=X_future_h,
525
- xcols=list(xcols) if use_exog else [],
526
- h=future_h,
527
- model_kind="lasso",
528
- alpha=float(lasso_alpha),
529
- n_lags=linear_lags,
530
- )
531
- future_parts.append(lasso_df)
532
-
533
  if not future_parts and not eval_parts:
534
  return keep("No models selected.")
535
 
@@ -569,7 +480,7 @@ def on_train_upload(file_obj):
569
  return msg, gr.update(choices=xchoices, value=[])
570
 
571
 
572
- with gr.Blocks(title="StatsForecast + TimeGPT + Extra Models") as demo:
573
  gr.Markdown(
574
  """
575
  # Forecasting Demo
@@ -595,6 +506,7 @@ Optional predictors: extra columns in training CSV + X_df for horizon.
595
  cv_windows = gr.Number(value=3, label="CV windows", precision=0)
596
  future_h = gr.Number(value=30, label="Future forecast horizon", precision=0)
597
  loss_name = gr.Dropdown(choices=["rmse", "mae", "mse", "mape", "smape", "mase"], value="rmse", label="Metric")
 
598
 
599
  with gr.Accordion("StatsForecast models", open=True):
600
  with gr.Row():
@@ -612,15 +524,8 @@ Optional predictors: extra columns in training CSV + X_df for horizon.
612
 
613
  with gr.Accordion("Additional models", open=True):
614
  with gr.Row():
615
- use_nf_lstm = gr.Checkbox(value=False, label="Nixtla NeuralForecast LSTM")
616
  use_tc_prophet = gr.Checkbox(value=False, label="TimeCopilot Prophet")
617
- with gr.Row():
618
- use_ridge = gr.Checkbox(value=False, label="Ridge (sklearn)")
619
- use_lasso = gr.Checkbox(value=False, label="Lasso (sklearn)")
620
- with gr.Row():
621
- ridge_alpha = gr.Number(value=1.0, label="Ridge alpha", precision=6)
622
- lasso_alpha = gr.Number(value=0.001, label="Lasso alpha", precision=6)
623
- linear_lags = gr.Number(value=14, label="Linear model lags", precision=0)
624
 
625
  last_eval = gr.State(None)
626
  last_future = gr.State(None)
@@ -647,6 +552,7 @@ Optional predictors: extra columns in training CSV + X_df for horizon.
647
  cv_windows,
648
  future_h,
649
  loss_name,
 
650
  use_histavg,
651
  use_naive,
652
  use_snaive,
@@ -658,11 +564,6 @@ Optional predictors: extra columns in training CSV + X_df for horizon.
658
  nixtla_api_key,
659
  use_nf_lstm,
660
  use_tc_prophet,
661
- use_ridge,
662
- use_lasso,
663
- ridge_alpha,
664
- lasso_alpha,
665
- linear_lags,
666
  xcols,
667
  future_x_file,
668
  last_eval,
 
22
  from utilsforecast.evaluation import evaluate
23
  from utilsforecast.losses import mae, mape, mase, mse, rmse, smape
24
 
 
 
 
25
 
26
  REQUIRED_COLS = ["unique_id", "ds", "y"]
27
  PLOT_TAIL_POINTS = 300
 
116
  return X_h.reset_index(drop=True)
117
 
118
 
119
+ def _normalize_timegpt_output(tgpt_raw, df_y, h):
120
+ tgpt = _ensure_pandas_df(tgpt_raw)
121
+ if tgpt is None or tgpt.empty:
122
+ raise ValueError("TimeGPT returned empty output.")
123
+ if "unique_id" not in tgpt.columns or "ds" not in tgpt.columns:
124
+ raise ValueError(f"TimeGPT output missing required columns. Got: {list(tgpt.columns)}")
125
+ tgpt["ds"] = pd.to_datetime(tgpt["ds"], errors="coerce")
126
+ if tgpt["ds"].isna().any():
127
+ raise ValueError("TimeGPT output has invalid ds values.")
128
+ out_col = None
129
+ for c in tgpt.columns:
130
+ if c.lower() in ("timegpt", "yhat", "y_hat", "forecast"):
131
+ out_col = c
132
+ break
133
+ if out_col is None:
134
+ non_id = [c for c in tgpt.columns if c not in ["unique_id", "ds"]]
135
+ if non_id:
136
+ out_col = non_id[0]
137
+ if out_col is None:
138
+ raise ValueError("TimeGPT output has no forecast column.")
139
+ if out_col != "timegpt":
140
+ tgpt = tgpt.rename(columns={out_col: "timegpt"})
141
+ last_ds = df_y.groupby("unique_id", as_index=False)["ds"].max().rename(columns={"ds": "last_ds"})
142
+ tgpt = tgpt.merge(last_ds, on="unique_id", how="inner")
143
+ tgpt = tgpt[tgpt["ds"] > tgpt["last_ds"]].copy()
144
+ tgpt = tgpt.sort_values(["unique_id", "ds"]).reset_index(drop=True)
145
+ tgpt = tgpt.groupby("unique_id", as_index=False).head(h).copy()
146
+ counts = tgpt.groupby("unique_id")["ds"].size()
147
+ missing_ids = sorted(set(last_ds["unique_id"]) - set(counts.index))
148
+ short_ids = sorted([uid for uid, c in counts.items() if c < h])
149
+ if missing_ids or short_ids:
150
+ parts = []
151
+ if missing_ids:
152
+ parts.append(f"Missing TimeGPT future rows for: {', '.join(missing_ids)}")
153
+ if short_ids:
154
+ parts.append(f"Not enough TimeGPT rows (need {h}) for: {', '.join(short_ids)}")
155
+ raise ValueError(" | ".join(parts))
156
+ tgpt = tgpt[["unique_id", "ds", "timegpt"]].copy()
157
+ return tgpt
158
+
159
+
160
+ def _ensure_forecast_cols(df_fc, colname):
161
+ if df_fc is None or df_fc.empty:
162
+ return None
163
+ df_fc = _ensure_pandas_df(df_fc)
164
+ if df_fc is None or df_fc.empty:
165
+ return None
166
+ if colname not in df_fc.columns:
167
+ non_id = [c for c in df_fc.columns if c not in ["unique_id", "ds"]]
168
+ if not non_id:
169
+ raise ValueError(f"Forecast output missing forecast column. Got: {list(df_fc.columns)}")
170
+ df_fc = df_fc.rename(columns={non_id[0]: colname})
171
+ df_fc = df_fc[["unique_id", "ds", colname]].copy()
172
+ df_fc["ds"] = pd.to_datetime(df_fc["ds"])
173
+ return df_fc
174
+
175
+
176
  def create_future_plot(fcst_df, original_df, title="Forecast"):
 
177
  if fcst_df is None or fcst_df.empty:
178
  return None
179
+ plt.figure(figsize=(12, 7))
180
  forecast_cols = [c for c in fcst_df.columns if c not in ["unique_id", "ds"]]
181
  unique_ids = fcst_df["unique_id"].unique()
182
  colors = plt.cm.tab10.colors
 
214
 
215
  fig = plt.gcf()
216
  fig.autofmt_xdate()
217
+ plt.close(fig)
218
  return fig
219
 
220
 
 
233
  return out
234
 
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  def run_forecast(
237
  train_file,
238
  freq,
 
242
  cv_windows,
243
  future_h,
244
  loss_name,
245
+ run_cv,
246
  use_histavg,
247
  use_naive,
248
  use_snaive,
 
254
  nixtla_api_key,
255
  use_nf_lstm,
256
  use_tc_prophet,
 
 
 
 
 
257
  xcols,
258
  future_x_file,
259
  last_eval,
 
285
  cv_step_size = _to_int(cv_step_size, "cv_step_size")
286
  cv_windows = _to_int(cv_windows, "cv_windows")
287
  future_h = _to_int(future_h, "future_h")
 
288
 
289
  df, candidate_xcols, msg = load_training_data(train_file)
290
  if df is None:
 
294
  train_tail = _tail_history(df_y, n=PLOT_TAIL_POINTS)
295
 
296
  use_exog = bool(xcols)
297
+ needs_future_x = use_exog and (use_timegpt or use_nf_lstm or use_autoarima)
298
  X_future_h = None
299
  df_exog = None
300
 
 
348
  future_parts = []
349
 
350
  if uni_models:
351
+ sf_u = StatsForecast(models=uni_models, freq=freq, n_jobs=1)
352
+ if run_cv:
353
+ cv_u = sf_u.cross_validation(df=df_y, h=cv_h, step_size=cv_step_size, n_windows=cv_windows)
354
+ ev_u = evaluate(cv_u, metrics=[loss_fn])
355
+ ev_u["pipeline"] = "univariate"
356
+ eval_parts.append(ev_u)
357
  fc_u = sf_u.forecast(df=df_y, h=future_h)
358
+ fc_u = _ensure_forecast_cols(fc_u, "statsforecast")
359
  future_parts.append(fc_u)
360
 
361
  if exog_models:
362
  if not use_exog or X_future_h is None or df_exog is None:
363
  return keep("Exogenous model selected but predictors/X_df are missing.")
364
+ sf_x = StatsForecast(models=exog_models, freq=freq, n_jobs=1)
365
  fc_x = sf_x.forecast(df=df_exog, h=future_h, X_df=X_future_h)
366
+ fc_x = _ensure_forecast_cols(fc_x, "autoarima")
367
  future_parts.append(fc_x)
368
 
369
  if use_timegpt:
 
407
  futr_exog_list = list(xcols)
408
  model = LSTM(
409
  h=future_h,
410
+ max_steps=80,
411
+ input_size=max(2 * future_h, 30),
412
+ encoder_hidden_size=32,
413
+ decoder_hidden_size=32,
414
+ batch_size=16,
415
  futr_exog_list=futr_exog_list,
416
  alias="lstm",
417
  )
418
  nf = NeuralForecast(models=[model], freq=freq)
419
  nf.fit(df=nf_df)
420
+ pred = nf.predict(futr_df=futr_df).reset_index(drop=False) if use_exog else nf.predict().reset_index(drop=False)
 
 
 
421
  cols = [c for c in pred.columns if c not in ["unique_id", "ds"]]
422
  if not cols:
423
  return keep("LSTM produced no forecast columns.")
424
  main_col = cols[0]
425
  lstm_df = pred[["unique_id", "ds", main_col]].rename(columns={main_col: "lstm"})
426
+ lstm_df["ds"] = pd.to_datetime(lstm_df["ds"])
427
  future_parts.append(lstm_df)
428
  except Exception as e:
429
  return keep(f"LSTM failed: {type(e).__name__}: {e}")
 
436
  try:
437
  p = Prophet(alias="prophet")
438
  prop = p.forecast(df=df_y, h=future_h, freq=freq)
439
+ prop = _ensure_forecast_cols(prop, "prophet")
 
 
 
 
 
 
 
 
440
  future_parts.append(prop)
441
  except Exception as e:
442
  return keep(f"Prophet failed: {type(e).__name__}: {e}")
443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  if not future_parts and not eval_parts:
445
  return keep("No models selected.")
446
 
 
480
  return msg, gr.update(choices=xchoices, value=[])
481
 
482
 
483
+ with gr.Blocks(title="Forecasting Demo") as demo:
484
  gr.Markdown(
485
  """
486
  # Forecasting Demo
 
506
  cv_windows = gr.Number(value=3, label="CV windows", precision=0)
507
  future_h = gr.Number(value=30, label="Future forecast horizon", precision=0)
508
  loss_name = gr.Dropdown(choices=["rmse", "mae", "mse", "mape", "smape", "mase"], value="rmse", label="Metric")
509
+ run_cv = gr.Checkbox(value=False, label="Run cross-validation (slower)")
510
 
511
  with gr.Accordion("StatsForecast models", open=True):
512
  with gr.Row():
 
524
 
525
  with gr.Accordion("Additional models", open=True):
526
  with gr.Row():
527
+ use_nf_lstm = gr.Checkbox(value=False, label="NeuralForecast LSTM")
528
  use_tc_prophet = gr.Checkbox(value=False, label="TimeCopilot Prophet")
 
 
 
 
 
 
 
529
 
530
  last_eval = gr.State(None)
531
  last_future = gr.State(None)
 
552
  cv_windows,
553
  future_h,
554
  loss_name,
555
+ run_cv,
556
  use_histavg,
557
  use_naive,
558
  use_snaive,
 
564
  nixtla_api_key,
565
  use_nf_lstm,
566
  use_tc_prophet,
 
 
 
 
 
567
  xcols,
568
  future_x_file,
569
  last_eval,