adamkahle commited on
Commit
7380c22
·
verified ·
1 Parent(s): 01074ec

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +474 -1203
app.py CHANGED
@@ -1,10 +1,11 @@
1
- import pandas as pd
2
- import matplotlib.pyplot as plt
3
- import gradio as gr
4
- import tempfile
5
  import os
 
6
  from datetime import datetime
7
 
 
 
 
 
8
  from statsforecast import StatsForecast
9
  from statsforecast.models import (
10
  HistoricAverage,
@@ -13,1261 +14,531 @@ from statsforecast.models import (
13
  WindowAverage,
14
  SeasonalWindowAverage,
15
  AutoETS,
16
- AutoARIMA
17
  )
18
 
19
  from utilsforecast.evaluation import evaluate
20
- from utilsforecast.losses import *
 
 
 
 
 
 
 
21
 
22
- # Import for TimeGPT
23
  from nixtla import NixtlaClient
24
 
25
- # Function to load and process uploaded CSV
26
- def load_data(file):
27
- if file is None:
28
- return None, "Please upload a CSV file"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  try:
30
- df = pd.read_csv(file)
31
- required_cols = ['unique_id', 'ds', 'y']
32
- missing_cols = [col for col in required_cols if col not in df.columns]
33
- if missing_cols:
34
- return None, f"Missing required columns: {', '.join(missing_cols)}"
35
-
36
- df['ds'] = pd.to_datetime(df['ds'])
37
- df = df[required_cols]
38
- df = df.sort_values(['unique_id', 'ds']).reset_index(drop=True)
39
-
40
- # Check for NaN values
41
- if df['y'].isna().any():
42
- return None, "Data contains missing values in the 'y' column"
43
-
44
- return df, "Data loaded successfully!"
45
  except Exception as e:
46
- return None, f"Error loading data: {str(e)}"
 
47
 
48
- # Helper function to calculate date offset based on frequency and horizon
49
  def calculate_date_offset(freq, horizon):
50
- """Calculate a timedelta based on frequency code and horizon"""
51
- if freq == 'H':
52
  return pd.Timedelta(hours=horizon)
53
- elif freq == 'D':
54
  return pd.Timedelta(days=horizon)
55
- elif freq == 'B':
56
- # For business days, use approximately 1.4x multiplier to account for weekends
57
- return pd.Timedelta(days=int(horizon * 1.4))
58
- elif freq == 'WS':
59
  return pd.Timedelta(weeks=horizon)
60
- elif freq == 'MS':
61
- return pd.Timedelta(days=horizon * 30) # Approximate
62
- elif freq == 'QS':
63
- return pd.Timedelta(days=horizon * 90) # Approximate
64
- elif freq == 'YS':
65
- return pd.Timedelta(days=horizon * 365) # Approximate
66
- else:
67
- # Default fallback
68
- return pd.Timedelta(days=horizon)
69
 
70
- # Function to generate and return a plot for validation results
71
- def create_forecast_plot(forecast_df, original_df, title="Forecasting Results", horizon=None, freq='D'):
 
 
72
  plt.figure(figsize=(12, 7))
73
- unique_ids = forecast_df['unique_id'].unique()
74
- forecast_cols = [col for col in forecast_df.columns if col not in ['unique_id', 'ds', 'cutoff', 'y']]
75
-
 
76
  colors = plt.cm.tab10.colors
77
-
78
- # Track min and max dates for x-axis limits
79
  min_cutoff = None
80
-
81
- for i, unique_id in enumerate(unique_ids):
82
- original_data = original_df[original_df['unique_id'] == unique_id]
83
- plt.plot(original_data['ds'], original_data['y'], 'k-', linewidth=2, label=f'{unique_id} (Actual)')
84
-
85
- forecast_data = forecast_df[forecast_df['unique_id'] == unique_id]
86
-
87
- # Find the earliest cutoff date if available
88
- if 'cutoff' in forecast_data.columns:
89
- cutoffs = pd.to_datetime(forecast_data['cutoff'].unique())
90
  if len(cutoffs) > 0:
91
- earliest_cutoff = cutoffs.min()
92
- if min_cutoff is None or earliest_cutoff < min_cutoff:
93
- min_cutoff = earliest_cutoff
94
-
95
- # Add vertical line at each cutoff
96
  for cutoff in cutoffs:
97
- plt.axvline(x=cutoff, color='gray', linestyle='--', alpha=0.4)
98
-
99
- # Plot main prediction lines
100
  for j, col in enumerate(forecast_cols):
101
- if col in forecast_data.columns:
102
- # Clean up model name for legend
103
- model_name = col.replace('_', ' ').title()
104
- if model_name == 'Timegpt':
105
- model_name = 'TimeGPT'
106
-
107
- plt.plot(forecast_data['ds'], forecast_data[col],
108
- color=colors[j % len(colors)],
109
- linestyle='--',
110
- linewidth=1.5,
111
- label=f'{model_name}')
 
112
 
113
  plt.title(title, fontsize=16)
114
- plt.xlabel('Date', fontsize=12)
115
- plt.ylabel('Value', fontsize=12)
116
  plt.grid(True, alpha=0.3)
117
-
118
- # Better legend with smaller font and outside placement
119
- plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=3, fontsize=10)
120
- plt.tight_layout(rect=[0, 0.05, 1, 0.95]) # Adjust layout to make room for legend
121
-
122
- # Set x-axis limits based on cutoff and horizon
123
  if min_cutoff is not None and horizon is not None:
124
- # Calculate date offset based on frequency and horizon
125
- date_offset = calculate_date_offset(freq, horizon)
126
-
127
- # Calculate start date as 'horizon' units before the first cutoff
128
- start_date = min_cutoff - date_offset
129
-
130
- # Find max date from forecast
131
- max_date = forecast_df['ds'].max()
132
-
133
- plt.xlim(start_date, max_date)
134
-
135
- # Add an annotation for the training/test split
136
- plt.annotate('Training | Test',
137
- xy=(min_cutoff, plt.ylim()[0]),
138
- xytext=(0, -40),
139
- textcoords='offset points',
140
- horizontalalignment='center',
141
- fontsize=10)
142
-
143
- # Format date labels better
144
  fig = plt.gcf()
145
- ax = plt.gca()
146
  fig.autofmt_xdate()
147
-
148
  return fig
149
 
150
- # Function to create a plot for future forecasts
151
- def create_future_forecast_plot(forecast_df, original_df, horizon=None, freq='D'):
152
  plt.figure(figsize=(12, 7))
153
- unique_ids = forecast_df['unique_id'].unique()
154
- forecast_cols = [col for col in forecast_df.columns if col not in ['unique_id', 'ds']]
155
-
156
  colors = plt.cm.tab10.colors
157
-
158
- # Track the forecast start date (min of forecast data)
159
- forecast_start = None
160
- if not forecast_df.empty:
161
- forecast_start = pd.to_datetime(forecast_df['ds'].min())
162
-
163
- for i, unique_id in enumerate(unique_ids):
164
- # Plot historical data
165
- original_data = original_df[original_df['unique_id'] == unique_id]
166
- plt.plot(original_data['ds'], original_data['y'], 'k-', linewidth=2, label=f'{unique_id} (Historical)')
167
-
168
- # Plot forecast data with shaded vertical line separator
169
- forecast_data = forecast_df[forecast_df['unique_id'] == unique_id]
170
-
171
- # Add vertical line at the forecast start
172
- if not forecast_data.empty and not original_data.empty:
173
- forecast_start = forecast_data['ds'].min()
174
- plt.axvline(x=forecast_start, color='gray', linestyle='--', alpha=0.5)
175
-
176
- # Add a shaded area for the forecast period
177
- plt.axvspan(forecast_start, forecast_data['ds'].max(), alpha=0.1, color='blue')
178
-
179
- # Annotate the split point
180
- plt.annotate('Historical | Forecast',
181
- xy=(forecast_start, plt.ylim()[0]),
182
- xytext=(0, -40),
183
- textcoords='offset points',
184
- horizontalalignment='center',
185
- fontsize=10)
186
-
187
- # Plot main prediction lines
188
- for j, col in enumerate(forecast_cols):
189
- if col in forecast_data.columns:
190
- # Clean up model name for legend
191
- model_name = col.replace('_', ' ').title()
192
- if model_name == 'Timegpt':
193
- model_name = 'TimeGPT'
194
-
195
- plt.plot(forecast_data['ds'], forecast_data[col],
196
- color=colors[j % len(colors)],
197
- linestyle='--',
198
- linewidth=1.5,
199
- label=f'{model_name}')
200
-
201
- plt.title('Future Forecast', fontsize=16)
202
- plt.xlabel('Date', fontsize=12)
203
- plt.ylabel('Value', fontsize=12)
204
  plt.grid(True, alpha=0.3)
205
-
206
- # Better legend with smaller font and outside placement
207
- plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=3, fontsize=10)
208
- plt.tight_layout(rect=[0, 0.05, 1, 0.95]) # Adjust layout to make room for legend
209
-
210
- # Set x-axis limits based on forecast start and horizon
211
- if forecast_start is not None and horizon is not None:
212
- # Calculate date offset based on frequency and horizon
213
- date_offset = calculate_date_offset(freq, horizon)
214
-
215
- # Calculate start date as 'horizon' units before the forecast start
216
- start_date = forecast_start - date_offset
217
-
218
- # Get the last date from historical data that's before or at the start_date
219
- historical_dates = pd.to_datetime(original_df['ds'])
220
- historical_dates_before_start = historical_dates[historical_dates <= start_date]
221
-
222
- if not historical_dates_before_start.empty:
223
- # Use the last available date in the historical data that's before our calculated start_date
224
- adjusted_start_date = historical_dates_before_start.max()
225
- else:
226
- # Fallback to using the original start_date
227
- adjusted_start_date = start_date
228
-
229
- # Set the x-axis limits
230
- plt.xlim(adjusted_start_date, forecast_df['ds'].max())
231
-
232
- # Format date labels better
233
  fig = plt.gcf()
234
- ax = plt.gca()
235
  fig.autofmt_xdate()
236
-
237
  return fig
238
 
239
- # Function to export results to CSV
240
- def export_results(eval_df, cv_results, future_forecasts):
241
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
242
-
243
- # Create temp directory if it doesn't exist
 
244
  temp_dir = tempfile.mkdtemp()
245
-
246
- result_files = []
247
-
248
- if eval_df is not None:
249
- eval_path = os.path.join(temp_dir, f"evaluation_metrics_{timestamp}.csv")
250
- eval_df.to_csv(eval_path, index=False)
251
- result_files.append(eval_path)
252
-
253
- if cv_results is not None:
254
- cv_path = os.path.join(temp_dir, f"cross_validation_results_{timestamp}.csv")
255
- cv_results.to_csv(cv_path, index=False)
256
- result_files.append(cv_path)
257
-
258
- if future_forecasts is not None:
259
- forecast_path = os.path.join(temp_dir, f"forecasts_{timestamp}.csv")
260
- future_forecasts.to_csv(forecast_path, index=False)
261
- result_files.append(forecast_path)
262
-
263
- return result_files
264
-
265
- # Main forecasting logic
 
 
266
  def run_forecast(
267
- file,
268
- frequency,
269
- eval_strategy,
270
- horizon,
271
- step_size,
272
- num_windows,
273
- use_historical_avg,
 
 
 
274
  use_naive,
275
- use_seasonal_naive,
276
- seasonality,
277
- use_window_avg,
278
- window_size,
279
- use_seasonal_window_avg,
280
- seasonal_window_size,
281
  use_autoets,
282
  use_autoarima,
 
283
  use_timegpt,
284
- finetune_loss,
285
- confidence_level,
286
- future_horizon
 
287
  ):
288
- df, message = load_data(file)
 
289
  if df is None:
290
- return None, None, None, None, None, None, message
291
-
292
- # Initialize results
293
- eval_df = None
294
- cv_results = None
295
- future_forecasts = None
296
-
297
- # Set up traditional statistical models
298
- models = []
299
- model_aliases = []
300
-
301
- if use_historical_avg:
302
- models.append(HistoricAverage(alias='historical_average'))
303
- model_aliases.append('historical_average')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  if use_naive:
305
- models.append(Naive(alias='naive'))
306
- model_aliases.append('naive')
307
- if use_seasonal_naive:
308
- models.append(SeasonalNaive(season_length=seasonality, alias='seasonal_naive'))
309
- model_aliases.append('seasonal_naive')
310
- if use_window_avg:
311
- models.append(WindowAverage(window_size=window_size, alias='window_average'))
312
- model_aliases.append('window_average')
313
- if use_seasonal_window_avg:
314
- models.append(SeasonalWindowAverage(season_length=seasonality, window_size=seasonal_window_size, alias='seasonal_window_average'))
315
- model_aliases.append('seasonal_window_average')
316
  if use_autoets:
317
- models.append(AutoETS(alias='autoets', season_length=seasonality))
318
- model_aliases.append('autoets')
 
319
  if use_autoarima:
320
- models.append(AutoARIMA(alias='autoarima', season_length=seasonality))
321
- model_aliases.append('autoarima')
 
 
 
322
 
323
- if not models and not use_timegpt:
324
- return None, None, None, None, None, None, "Please select at least one forecasting model"
 
 
325
 
326
- try:
327
- # Initialize results with empty DataFrames
328
- combined_eval_df = pd.DataFrame()
329
- combined_cv_results = pd.DataFrame()
330
- combined_future_forecasts = pd.DataFrame()
331
-
332
- # Run traditional statistical models if any are selected
333
- if models:
334
- sf = StatsForecast(models=models, freq=frequency, n_jobs=-1)
335
-
336
- # Run cross-validation for traditional models
337
- if eval_strategy == "Cross Validation":
338
- cv_results = sf.cross_validation(df=df, h=horizon, step_size=step_size, n_windows=num_windows)
339
- evaluation = evaluate(df=cv_results, metrics=[bias, mae, rmse, mape], models=model_aliases)
340
- eval_df = pd.DataFrame(evaluation).reset_index()
341
- eval_df = eval_df.round({col: 2 for col in eval_df.columns[3:]})
342
- else: # Fixed window
343
- cv_results = sf.cross_validation(df=df, h=horizon, step_size=10, n_windows=1) # any step size for 1 window
344
- evaluation = evaluate(df=cv_results, metrics=[bias, mae, rmse, mape], models=model_aliases)
345
- eval_df = pd.DataFrame(evaluation).reset_index()
346
-
347
- # Generate future forecasts
348
- future_forecasts = sf.forecast(df=df, h=future_horizon)
349
-
350
- # Store results
351
- combined_eval_df = eval_df.copy() if eval_df is not None else pd.DataFrame()
352
- combined_cv_results = cv_results.copy() if cv_results is not None else pd.DataFrame()
353
- combined_cv_results = combined_cv_results
354
- combined_future_forecasts = future_forecasts.copy() if future_forecasts is not None else pd.DataFrame()
355
-
356
- # Run TimeGPT if selected
357
- if use_timegpt:
358
- try:
359
- # Get API key from environment variables
360
- nixtla_api_key = os.getenv("NIXTLA_API_KEY")
361
- if not nixtla_api_key:
362
- return None, None, None, None, None, None, "TimeGPT API key not found. Please set the NIXTLA_API_KEY environment variable."
363
-
364
- # Initialize Nixtla client
365
- nixtla_client = NixtlaClient(api_key=nixtla_api_key)
366
-
367
- # Convert confidence_level to list format
368
- level = [float(confidence_level)]
369
-
370
- # Run cross-validation for TimeGPT
371
- if eval_strategy == "Cross Validation":
372
- timegpt_cv_df = nixtla_client.cross_validation(
373
- df=df,
374
- h=horizon,
375
- freq=frequency,
376
- n_windows=num_windows,
377
- step_size=step_size
378
- )
379
- timegpt_cv_eval = evaluate(
380
- df=timegpt_cv_df,
381
- metrics=[mape, mae, rmse, bias],
382
- models=['TimeGPT'],
383
- )
384
- timegpt_eval_df = pd.DataFrame(timegpt_cv_eval).reset_index()
385
- timegpt_eval_df = timegpt_eval_df.round({col: 2 for col in timegpt_eval_df.columns[3:]})
386
- else: # Fixed window
387
- timegpt_cv_df = nixtla_client.cross_validation(
388
- df=df,
389
- h=horizon,
390
- freq=frequency,
391
- n_windows=1,
392
- step_size=10
393
- )
394
- timegpt_cv_eval = evaluate(
395
- df=timegpt_cv_df,
396
- metrics=[mape, mae, rmse, bias],
397
- models=['TimeGPT']
398
- )
399
- timegpt_eval_df = pd.DataFrame(timegpt_cv_eval).reset_index()
400
-
401
- # Generate future forecasts with TimeGPT
402
- forecast_timegpt = nixtla_client.forecast(
403
- df=df,
404
- h=future_horizon,
405
- freq=frequency,
406
- finetune_loss=finetune_loss
407
- )
408
-
409
-
410
- # Combine results - using merge instead of concat to avoid duplicate rows
411
- if not combined_eval_df.empty and not timegpt_eval_df.empty:
412
- # Get common columns for the join
413
- join_columns = ['unique_id', 'metric']
414
- # Merge the dataframes on unique_id and metric
415
- combined_eval_df = pd.merge(
416
- combined_eval_df,
417
- timegpt_eval_df,
418
- on=join_columns,
419
- how='outer',
420
- suffixes=('', '_timegpt')
421
- )
422
-
423
- # Clean up any duplicated columns from the merge
424
- for col in combined_eval_df.columns:
425
- if col.endswith('_timegpt'):
426
- base_col = col.replace('_timegpt', '')
427
- # Fill NaN values in the original column with values from the _timegpt column
428
- if base_col in combined_eval_df.columns:
429
- combined_eval_df[base_col] = combined_eval_df[base_col].fillna(combined_eval_df[col])
430
- # Remove the _timegpt column
431
- combined_eval_df = combined_eval_df.drop(columns=[col])
432
- else:
433
- combined_eval_df = timegpt_eval_df if not timegpt_eval_df.empty else combined_eval_df
434
-
435
- if not combined_cv_results.empty and not timegpt_cv_df.empty:
436
- # Make sure we're not duplicating the 'y' column
437
- if 'y' in combined_cv_results.columns and 'y' in timegpt_cv_df.columns:
438
- timegpt_cv_df_no_y = timegpt_cv_df.drop(columns=['y'])
439
- combined_cv_results = pd.merge(
440
- combined_cv_results,
441
- timegpt_cv_df_no_y,
442
- on=['unique_id', 'ds', 'cutoff'],
443
- how='outer'
444
- )
445
- else:
446
- combined_cv_results = pd.concat([combined_cv_results, timegpt_cv_df], ignore_index=True)
447
- else:
448
- combined_cv_results = timegpt_cv_df if not timegpt_cv_df.empty else combined_cv_results
449
-
450
- if not combined_future_forecasts.empty and not forecast_timegpt.empty:
451
- # Make sure we're merging on common columns
452
- combined_future_forecasts = pd.merge(
453
- combined_future_forecasts,
454
- forecast_timegpt,
455
- on=['unique_id', 'ds'],
456
- how='outer'
457
- )
458
- else:
459
- combined_future_forecasts = forecast_timegpt if not forecast_timegpt.empty else combined_future_forecasts
460
-
461
- except Exception as e:
462
- return None, None, None, None, None, None, f"Error with TimeGPT: {str(e)}"
463
-
464
- # Create plots
465
- if not combined_cv_results.empty:
466
- fig_validation = create_forecast_plot(
467
- combined_cv_results,
468
- df,
469
- f"{eval_strategy} Results"
470
- )
471
- else:
472
- fig_validation = None
473
-
474
- if not combined_future_forecasts.empty:
475
- fig_future = create_future_forecast_plot(combined_future_forecasts, df)
476
- else:
477
- fig_future = None
478
-
479
- # Export results
480
- export_files = export_results(combined_eval_df, combined_cv_results, combined_future_forecasts)
481
-
482
- return combined_eval_df, combined_cv_results, fig_validation, combined_future_forecasts, fig_future, export_files, "Analysis completed successfully!"
483
 
484
- except Exception as e:
485
- return None, None, None, None, None, None, f"Error during forecasting: {str(e)}"
486
-
487
- # Sample CSV file generation
488
- def download_sample():
489
- sample_data = """unique_id,ds,y
490
- ^GSPC,2023-01-03,3824.139892578125
491
- ^GSPC,2023-01-04,3852.969970703125
492
- ^GSPC,2023-01-05,3808.10009765625
493
- ^GSPC,2023-01-06,3895.080078125
494
- ^GSPC,2023-01-09,3892.090087890625
495
- ^GSPC,2023-01-10,3919.25
496
- ^GSPC,2023-01-11,3969.610107421875
497
- ^GSPC,2023-01-12,3983.169921875
498
- ^GSPC,2023-01-13,3999.090087890625
499
- ^GSPC,2023-01-17,3990.969970703125
500
- ^GSPC,2023-01-18,3928.860107421875
501
- ^GSPC,2023-01-19,3898.85009765625
502
- ^GSPC,2023-01-20,3972.610107421875
503
- ^GSPC,2023-01-23,4019.81005859375
504
- ^GSPC,2023-01-24,4016.949951171875
505
- ^GSPC,2023-01-25,4016.219970703125
506
- ^GSPC,2023-01-26,4060.429931640625
507
- ^GSPC,2023-01-27,4070.56005859375
508
- ^GSPC,2023-01-30,4017.77001953125
509
- ^GSPC,2023-01-31,4076.60009765625
510
- ^GSPC,2023-02-01,4119.2099609375
511
- ^GSPC,2023-02-02,4179.759765625
512
- ^GSPC,2023-02-03,4136.47998046875
513
- ^GSPC,2023-02-06,4111.080078125
514
- ^GSPC,2023-02-07,4164
515
- ^GSPC,2023-02-08,4117.85986328125
516
- ^GSPC,2023-02-09,4081.5
517
- ^GSPC,2023-02-10,4090.4599609375
518
- ^GSPC,2023-02-13,4137.2900390625
519
- ^GSPC,2023-02-14,4136.1298828125
520
- ^GSPC,2023-02-15,4147.60009765625
521
- ^GSPC,2023-02-16,4090.409912109375
522
- ^GSPC,2023-02-17,4079.090087890625
523
- ^GSPC,2023-02-21,3997.340087890625
524
- ^GSPC,2023-02-22,3991.050048828125
525
- ^GSPC,2023-02-23,4012.320068359375
526
- ^GSPC,2023-02-24,3970.0400390625
527
- ^GSPC,2023-02-27,3982.239990234375
528
- ^GSPC,2023-02-28,3970.14990234375
529
- ^GSPC,2023-03-01,3951.389892578125
530
- ^GSPC,2023-03-02,3981.35009765625
531
- ^GSPC,2023-03-03,4045.639892578125
532
- ^GSPC,2023-03-06,4048.419921875
533
- ^GSPC,2023-03-07,3986.3701171875
534
- ^GSPC,2023-03-08,3992.010009765625
535
- ^GSPC,2023-03-09,3918.320068359375
536
- ^GSPC,2023-03-10,3861.590087890625
537
- ^GSPC,2023-03-13,3855.760009765625
538
- ^GSPC,2023-03-14,3919.2900390625
539
- ^GSPC,2023-03-15,3891.929931640625
540
- ^GSPC,2023-03-16,3960.280029296875
541
- ^GSPC,2023-03-17,3916.639892578125
542
- ^GSPC,2023-03-20,3951.570068359375
543
- ^GSPC,2023-03-21,4002.8701171875
544
- ^GSPC,2023-03-22,3936.969970703125
545
- ^GSPC,2023-03-23,3948.719970703125
546
- ^GSPC,2023-03-24,3970.989990234375
547
- ^GSPC,2023-03-27,3977.530029296875
548
- ^GSPC,2023-03-28,3971.27001953125
549
- ^GSPC,2023-03-29,4027.81005859375
550
- ^GSPC,2023-03-30,4050.830078125
551
- ^GSPC,2023-03-31,4109.31005859375
552
- ^GSPC,2023-04-03,4124.509765625
553
- ^GSPC,2023-04-04,4100.60009765625
554
- ^GSPC,2023-04-05,4090.3798828125
555
- ^GSPC,2023-04-06,4105.02001953125
556
- ^GSPC,2023-04-10,4109.10986328125
557
- ^GSPC,2023-04-11,4108.93994140625
558
- ^GSPC,2023-04-12,4091.949951171875
559
- ^GSPC,2023-04-13,4146.22021484375
560
- ^GSPC,2023-04-14,4137.64013671875
561
- ^GSPC,2023-04-17,4151.31982421875
562
- ^GSPC,2023-04-18,4154.8701171875
563
- ^GSPC,2023-04-19,4154.52001953125
564
- ^GSPC,2023-04-20,4129.7900390625
565
- ^GSPC,2023-04-21,4133.52001953125
566
- ^GSPC,2023-04-24,4137.0400390625
567
- ^GSPC,2023-04-25,4071.6298828125
568
- ^GSPC,2023-04-26,4055.989990234375
569
- ^GSPC,2023-04-27,4135.35009765625
570
- ^GSPC,2023-04-28,4169.47998046875
571
- ^GSPC,2023-05-01,4167.8701171875
572
- ^GSPC,2023-05-02,4119.580078125
573
- ^GSPC,2023-05-03,4090.75
574
- ^GSPC,2023-05-04,4061.219970703125
575
- ^GSPC,2023-05-05,4136.25
576
- ^GSPC,2023-05-08,4138.1201171875
577
- ^GSPC,2023-05-09,4119.169921875
578
- ^GSPC,2023-05-10,4137.64013671875
579
- ^GSPC,2023-05-11,4130.6201171875
580
- ^GSPC,2023-05-12,4124.080078125
581
- ^GSPC,2023-05-15,4136.27978515625
582
- ^GSPC,2023-05-16,4109.89990234375
583
- ^GSPC,2023-05-17,4158.77001953125
584
- ^GSPC,2023-05-18,4198.0498046875
585
- ^GSPC,2023-05-19,4191.97998046875
586
- ^GSPC,2023-05-22,4192.6298828125
587
- ^GSPC,2023-05-23,4145.580078125
588
- ^GSPC,2023-05-24,4115.240234375
589
- ^GSPC,2023-05-25,4151.27978515625
590
- ^GSPC,2023-05-26,4205.4501953125
591
- ^GSPC,2023-05-30,4205.52001953125
592
- ^GSPC,2023-05-31,4179.830078125
593
- ^GSPC,2023-06-01,4221.02001953125
594
- ^GSPC,2023-06-02,4282.3701171875
595
- ^GSPC,2023-06-05,4273.7900390625
596
- ^GSPC,2023-06-06,4283.85009765625
597
- ^GSPC,2023-06-07,4267.52001953125
598
- ^GSPC,2023-06-08,4293.93017578125
599
- ^GSPC,2023-06-09,4298.85986328125
600
- ^GSPC,2023-06-12,4338.93017578125
601
- ^GSPC,2023-06-13,4369.009765625
602
- ^GSPC,2023-06-14,4372.58984375
603
- ^GSPC,2023-06-15,4425.83984375
604
- ^GSPC,2023-06-16,4409.58984375
605
- ^GSPC,2023-06-20,4388.7099609375
606
- ^GSPC,2023-06-21,4365.68994140625
607
- ^GSPC,2023-06-22,4381.89013671875
608
- ^GSPC,2023-06-23,4348.330078125
609
- ^GSPC,2023-06-26,4328.81982421875
610
- ^GSPC,2023-06-27,4378.41015625
611
- ^GSPC,2023-06-28,4376.85986328125
612
- ^GSPC,2023-06-29,4396.43994140625
613
- ^GSPC,2023-06-30,4450.3798828125
614
- ^GSPC,2023-07-03,4455.58984375
615
- ^GSPC,2023-07-05,4446.81982421875
616
- ^GSPC,2023-07-06,4411.58984375
617
- ^GSPC,2023-07-07,4398.9501953125
618
- ^GSPC,2023-07-10,4409.52978515625
619
- ^GSPC,2023-07-11,4439.259765625
620
- ^GSPC,2023-07-12,4472.16015625
621
- ^GSPC,2023-07-13,4510.0400390625
622
- ^GSPC,2023-07-14,4505.419921875
623
- ^GSPC,2023-07-17,4522.7900390625
624
- ^GSPC,2023-07-18,4554.97998046875
625
- ^GSPC,2023-07-19,4565.72021484375
626
- ^GSPC,2023-07-20,4534.8701171875
627
- ^GSPC,2023-07-21,4536.33984375
628
- ^GSPC,2023-07-24,4554.64013671875
629
- ^GSPC,2023-07-25,4567.4599609375
630
- ^GSPC,2023-07-26,4566.75
631
- ^GSPC,2023-07-27,4537.41015625
632
- ^GSPC,2023-07-28,4582.22998046875
633
- ^GSPC,2023-07-31,4588.9599609375
634
- ^GSPC,2023-08-01,4576.72998046875
635
- ^GSPC,2023-08-02,4513.39013671875
636
- ^GSPC,2023-08-03,4501.89013671875
637
- ^GSPC,2023-08-04,4478.02978515625
638
- ^GSPC,2023-08-07,4518.43994140625
639
- ^GSPC,2023-08-08,4499.3798828125
640
- ^GSPC,2023-08-09,4467.7099609375
641
- ^GSPC,2023-08-10,4468.830078125
642
- ^GSPC,2023-08-11,4464.0498046875
643
- ^GSPC,2023-08-14,4489.72021484375
644
- ^GSPC,2023-08-15,4437.85986328125
645
- ^GSPC,2023-08-16,4404.330078125
646
- ^GSPC,2023-08-17,4370.35986328125
647
- ^GSPC,2023-08-18,4369.7099609375
648
- ^GSPC,2023-08-21,4399.77001953125
649
- ^GSPC,2023-08-22,4387.5498046875
650
- ^GSPC,2023-08-23,4436.009765625
651
- ^GSPC,2023-08-24,4376.31005859375
652
- ^GSPC,2023-08-25,4405.7099609375
653
- ^GSPC,2023-08-28,4433.31005859375
654
- ^GSPC,2023-08-29,4497.6298828125
655
- ^GSPC,2023-08-30,4514.8701171875
656
- ^GSPC,2023-08-31,4507.66015625
657
- ^GSPC,2023-09-01,4515.77001953125
658
- ^GSPC,2023-09-05,4496.830078125
659
- ^GSPC,2023-09-06,4465.47998046875
660
- ^GSPC,2023-09-07,4451.14013671875
661
- ^GSPC,2023-09-08,4457.490234375
662
- ^GSPC,2023-09-11,4487.4599609375
663
- ^GSPC,2023-09-12,4461.89990234375
664
- ^GSPC,2023-09-13,4467.43994140625
665
- ^GSPC,2023-09-14,4505.10009765625
666
- ^GSPC,2023-09-15,4450.31982421875
667
- ^GSPC,2023-09-18,4453.52978515625
668
- ^GSPC,2023-09-19,4443.9501953125
669
- ^GSPC,2023-09-20,4402.2001953125
670
- ^GSPC,2023-09-21,4330
671
- ^GSPC,2023-09-22,4320.06005859375
672
- ^GSPC,2023-09-25,4337.43994140625
673
- ^GSPC,2023-09-26,4273.52978515625
674
- ^GSPC,2023-09-27,4274.509765625
675
- ^GSPC,2023-09-28,4299.7001953125
676
- ^GSPC,2023-09-29,4288.0498046875
677
- ^GSPC,2023-10-02,4288.39013671875
678
- ^GSPC,2023-10-03,4229.4501953125
679
- ^GSPC,2023-10-04,4263.75
680
- ^GSPC,2023-10-05,4258.18994140625
681
- ^GSPC,2023-10-06,4308.5
682
- ^GSPC,2023-10-09,4335.66015625
683
- ^GSPC,2023-10-10,4358.240234375
684
- ^GSPC,2023-10-11,4376.9501953125
685
- ^GSPC,2023-10-12,4349.60986328125
686
- ^GSPC,2023-10-13,4327.77978515625
687
- ^GSPC,2023-10-16,4373.6298828125
688
- ^GSPC,2023-10-17,4373.2001953125
689
- ^GSPC,2023-10-18,4314.60009765625
690
- ^GSPC,2023-10-19,4278
691
- ^GSPC,2023-10-20,4224.16015625
692
- ^GSPC,2023-10-23,4217.0400390625
693
- ^GSPC,2023-10-24,4247.68017578125
694
- ^GSPC,2023-10-25,4186.77001953125
695
- ^GSPC,2023-10-26,4137.22998046875
696
- ^GSPC,2023-10-27,4117.3701171875
697
- ^GSPC,2023-10-30,4166.81982421875
698
- ^GSPC,2023-10-31,4193.7998046875
699
- ^GSPC,2023-11-01,4237.85986328125
700
- ^GSPC,2023-11-02,4317.77978515625
701
- ^GSPC,2023-11-03,4358.33984375
702
- ^GSPC,2023-11-06,4365.97998046875
703
- ^GSPC,2023-11-07,4378.3798828125
704
- ^GSPC,2023-11-08,4382.77978515625
705
- ^GSPC,2023-11-09,4347.35009765625
706
- ^GSPC,2023-11-10,4415.240234375
707
- ^GSPC,2023-11-13,4411.5498046875
708
- ^GSPC,2023-11-14,4495.7001953125
709
- ^GSPC,2023-11-15,4502.8798828125
710
- ^GSPC,2023-11-16,4508.240234375
711
- ^GSPC,2023-11-17,4514.02001953125
712
- ^GSPC,2023-11-20,4547.3798828125
713
- ^GSPC,2023-11-21,4538.18994140625
714
- ^GSPC,2023-11-22,4556.6201171875
715
- ^GSPC,2023-11-24,4559.33984375
716
- ^GSPC,2023-11-27,4550.43017578125
717
- ^GSPC,2023-11-28,4554.89013671875
718
- ^GSPC,2023-11-29,4550.580078125
719
- ^GSPC,2023-11-30,4567.7998046875
720
- ^GSPC,2023-12-01,4594.6298828125
721
- ^GSPC,2023-12-04,4569.77978515625
722
- ^GSPC,2023-12-05,4567.18017578125
723
- ^GSPC,2023-12-06,4549.33984375
724
- ^GSPC,2023-12-07,4585.58984375
725
- ^GSPC,2023-12-08,4604.3701171875
726
- ^GSPC,2023-12-11,4622.43994140625
727
- ^GSPC,2023-12-12,4643.7001953125
728
- ^GSPC,2023-12-13,4707.08984375
729
- ^GSPC,2023-12-14,4719.5498046875
730
- ^GSPC,2023-12-15,4719.18994140625
731
- ^GSPC,2023-12-18,4740.56005859375
732
- ^GSPC,2023-12-19,4768.3701171875
733
- ^GSPC,2023-12-20,4698.35009765625
734
- ^GSPC,2023-12-21,4746.75
735
- ^GSPC,2023-12-22,4754.6298828125
736
- ^GSPC,2023-12-26,4774.75
737
- ^GSPC,2023-12-27,4781.580078125
738
- ^GSPC,2023-12-28,4783.35009765625
739
- ^GSPC,2023-12-29,4769.830078125
740
- ^GSPC,2024-01-02,4742.830078125
741
- ^GSPC,2024-01-03,4704.81005859375
742
- ^GSPC,2024-01-04,4688.68017578125
743
- ^GSPC,2024-01-05,4697.240234375
744
- ^GSPC,2024-01-08,4763.5400390625
745
- ^GSPC,2024-01-09,4756.5
746
- ^GSPC,2024-01-10,4783.4501953125
747
- ^GSPC,2024-01-11,4780.240234375
748
- ^GSPC,2024-01-12,4783.830078125
749
- ^GSPC,2024-01-16,4765.97998046875
750
- ^GSPC,2024-01-17,4739.2099609375
751
- ^GSPC,2024-01-18,4780.93994140625
752
- ^GSPC,2024-01-19,4839.81005859375
753
- ^GSPC,2024-01-22,4850.43017578125
754
- ^GSPC,2024-01-23,4864.60009765625
755
- ^GSPC,2024-01-24,4868.5498046875
756
- ^GSPC,2024-01-25,4894.16015625
757
- ^GSPC,2024-01-26,4890.97021484375
758
- ^GSPC,2024-01-29,4927.93017578125
759
- ^GSPC,2024-01-30,4924.97021484375
760
- ^GSPC,2024-01-31,4845.64990234375
761
- ^GSPC,2024-02-01,4906.18994140625
762
- ^GSPC,2024-02-02,4958.60986328125
763
- ^GSPC,2024-02-05,4942.81005859375
764
- ^GSPC,2024-02-06,4954.22998046875
765
- ^GSPC,2024-02-07,4995.06005859375
766
- ^GSPC,2024-02-08,4997.91015625
767
- ^GSPC,2024-02-09,5026.60986328125
768
- ^GSPC,2024-02-12,5021.83984375
769
- ^GSPC,2024-02-13,4953.169921875
770
- ^GSPC,2024-02-14,5000.6201171875
771
- ^GSPC,2024-02-15,5029.72998046875
772
- ^GSPC,2024-02-16,5005.56982421875
773
- ^GSPC,2024-02-20,4975.509765625
774
- ^GSPC,2024-02-21,4981.7998046875
775
- ^GSPC,2024-02-22,5087.02978515625
776
- ^GSPC,2024-02-23,5088.7998046875
777
- ^GSPC,2024-02-26,5069.52978515625
778
- ^GSPC,2024-02-27,5078.18017578125
779
- ^GSPC,2024-02-28,5069.759765625
780
- ^GSPC,2024-02-29,5096.27001953125
781
- ^GSPC,2024-03-01,5137.080078125
782
- ^GSPC,2024-03-04,5130.9501953125
783
- ^GSPC,2024-03-05,5078.64990234375
784
- ^GSPC,2024-03-06,5104.759765625
785
- ^GSPC,2024-03-07,5157.35986328125
786
- ^GSPC,2024-03-08,5123.68994140625
787
- ^GSPC,2024-03-11,5117.93994140625
788
- ^GSPC,2024-03-12,5175.27001953125
789
- ^GSPC,2024-03-13,5165.31005859375
790
- ^GSPC,2024-03-14,5150.47998046875
791
- ^GSPC,2024-03-15,5117.08984375
792
- ^GSPC,2024-03-18,5149.419921875
793
- ^GSPC,2024-03-19,5178.509765625
794
- ^GSPC,2024-03-20,5224.6201171875
795
- ^GSPC,2024-03-21,5241.52978515625
796
- ^GSPC,2024-03-22,5234.18017578125
797
- ^GSPC,2024-03-25,5218.18994140625
798
- ^GSPC,2024-03-26,5203.580078125
799
- ^GSPC,2024-03-27,5248.490234375
800
- ^GSPC,2024-03-28,5254.35009765625
801
- ^GSPC,2024-04-01,5243.77001953125
802
- ^GSPC,2024-04-02,5205.81005859375
803
- ^GSPC,2024-04-03,5211.490234375
804
- ^GSPC,2024-04-04,5147.2099609375
805
- ^GSPC,2024-04-05,5204.33984375
806
- ^GSPC,2024-04-08,5202.39013671875
807
- ^GSPC,2024-04-09,5209.91015625
808
- ^GSPC,2024-04-10,5160.64013671875
809
- ^GSPC,2024-04-11,5199.06005859375
810
- ^GSPC,2024-04-12,5123.41015625
811
- ^GSPC,2024-04-15,5061.81982421875
812
- ^GSPC,2024-04-16,5051.41015625
813
- ^GSPC,2024-04-17,5022.2099609375
814
- ^GSPC,2024-04-18,5011.1201171875
815
- ^GSPC,2024-04-19,4967.22998046875
816
- ^GSPC,2024-04-22,5010.60009765625
817
- ^GSPC,2024-04-23,5070.5498046875
818
- ^GSPC,2024-04-24,5071.6298828125
819
- ^GSPC,2024-04-25,5048.419921875
820
- ^GSPC,2024-04-26,5099.9599609375
821
- ^GSPC,2024-04-29,5116.169921875
822
- ^GSPC,2024-04-30,5035.68994140625
823
- ^GSPC,2024-05-01,5018.39013671875
824
- ^GSPC,2024-05-02,5064.2001953125
825
- ^GSPC,2024-05-03,5127.7900390625
826
- ^GSPC,2024-05-06,5180.740234375
827
- ^GSPC,2024-05-07,5187.7001953125
828
- ^GSPC,2024-05-08,5187.669921875
829
- ^GSPC,2024-05-09,5214.080078125
830
- ^GSPC,2024-05-10,5222.68017578125
831
- ^GSPC,2024-05-13,5221.419921875
832
- ^GSPC,2024-05-14,5246.68017578125
833
- ^GSPC,2024-05-15,5308.14990234375
834
- ^GSPC,2024-05-16,5297.10009765625
835
- ^GSPC,2024-05-17,5303.27001953125
836
- ^GSPC,2024-05-20,5308.1298828125
837
- ^GSPC,2024-05-21,5321.41015625
838
- ^GSPC,2024-05-22,5307.009765625
839
- ^GSPC,2024-05-23,5267.83984375
840
- ^GSPC,2024-05-24,5304.72021484375
841
- ^GSPC,2024-05-28,5306.0400390625
842
- ^GSPC,2024-05-29,5266.9501953125
843
- ^GSPC,2024-05-30,5235.47998046875
844
- ^GSPC,2024-05-31,5277.509765625
845
- ^GSPC,2024-06-03,5283.39990234375
846
- ^GSPC,2024-06-04,5291.33984375
847
- ^GSPC,2024-06-05,5354.02978515625
848
- ^GSPC,2024-06-06,5352.9599609375
849
- ^GSPC,2024-06-07,5346.990234375
850
- ^GSPC,2024-06-10,5360.7900390625
851
- ^GSPC,2024-06-11,5375.31982421875
852
- ^GSPC,2024-06-12,5421.02978515625
853
- ^GSPC,2024-06-13,5433.740234375
854
- ^GSPC,2024-06-14,5431.60009765625
855
- ^GSPC,2024-06-17,5473.22998046875
856
- ^GSPC,2024-06-18,5487.02978515625
857
- ^GSPC,2024-06-20,5473.169921875
858
- ^GSPC,2024-06-21,5464.6201171875
859
- ^GSPC,2024-06-24,5447.8701171875
860
- ^GSPC,2024-06-25,5469.2998046875
861
- ^GSPC,2024-06-26,5477.89990234375
862
- ^GSPC,2024-06-27,5482.8701171875
863
- ^GSPC,2024-06-28,5460.47998046875
864
- ^GSPC,2024-07-01,5475.08984375
865
- ^GSPC,2024-07-02,5509.009765625
866
- ^GSPC,2024-07-03,5537.02001953125
867
- ^GSPC,2024-07-05,5567.18994140625
868
- ^GSPC,2024-07-08,5572.85009765625
869
- ^GSPC,2024-07-09,5576.97998046875
870
- ^GSPC,2024-07-10,5633.91015625
871
- ^GSPC,2024-07-11,5584.5400390625
872
- ^GSPC,2024-07-12,5615.35009765625
873
- ^GSPC,2024-07-15,5631.22021484375
874
- ^GSPC,2024-07-16,5667.2001953125
875
- ^GSPC,2024-07-17,5588.27001953125
876
- ^GSPC,2024-07-18,5544.58984375
877
- ^GSPC,2024-07-19,5505
878
- ^GSPC,2024-07-22,5564.41015625
879
- ^GSPC,2024-07-23,5555.740234375
880
- ^GSPC,2024-07-24,5427.1298828125
881
- ^GSPC,2024-07-25,5399.22021484375
882
- ^GSPC,2024-07-26,5459.10009765625
883
- ^GSPC,2024-07-29,5463.5400390625
884
- ^GSPC,2024-07-30,5436.43994140625
885
- ^GSPC,2024-07-31,5522.2998046875
886
- ^GSPC,2024-08-01,5446.68017578125
887
- ^GSPC,2024-08-02,5346.56005859375
888
- ^GSPC,2024-08-05,5186.330078125
889
- ^GSPC,2024-08-06,5240.02978515625
890
- ^GSPC,2024-08-07,5199.5
891
- ^GSPC,2024-08-08,5319.31005859375
892
- ^GSPC,2024-08-09,5344.16015625
893
- ^GSPC,2024-08-12,5344.39013671875
894
- ^GSPC,2024-08-13,5434.43017578125
895
- ^GSPC,2024-08-14,5455.2099609375
896
- ^GSPC,2024-08-15,5543.22021484375
897
- ^GSPC,2024-08-16,5554.25
898
- ^GSPC,2024-08-19,5608.25
899
- ^GSPC,2024-08-20,5597.1201171875
900
- ^GSPC,2024-08-21,5620.85009765625
901
- ^GSPC,2024-08-22,5570.64013671875
902
- ^GSPC,2024-08-23,5634.60986328125
903
- ^GSPC,2024-08-26,5616.83984375
904
- ^GSPC,2024-08-27,5625.7998046875
905
- ^GSPC,2024-08-28,5592.18017578125
906
- ^GSPC,2024-08-29,5591.9599609375
907
- ^GSPC,2024-08-30,5648.39990234375
908
- ^GSPC,2024-09-03,5528.93017578125
909
- ^GSPC,2024-09-04,5520.06982421875
910
- ^GSPC,2024-09-05,5503.41015625
911
- ^GSPC,2024-09-06,5408.419921875
912
- ^GSPC,2024-09-09,5471.0498046875
913
- ^GSPC,2024-09-10,5495.52001953125
914
- ^GSPC,2024-09-11,5554.1298828125
915
- ^GSPC,2024-09-12,5595.759765625
916
- ^GSPC,2024-09-13,5626.02001953125
917
- ^GSPC,2024-09-16,5633.08984375
918
- ^GSPC,2024-09-17,5634.580078125
919
- ^GSPC,2024-09-18,5618.259765625
920
- ^GSPC,2024-09-19,5713.64013671875
921
- ^GSPC,2024-09-20,5702.5498046875
922
- ^GSPC,2024-09-23,5718.56982421875
923
- ^GSPC,2024-09-24,5732.93017578125
924
- ^GSPC,2024-09-25,5722.259765625
925
- ^GSPC,2024-09-26,5745.3701171875
926
- ^GSPC,2024-09-27,5738.169921875
927
- ^GSPC,2024-09-30,5762.47998046875
928
- ^GSPC,2024-10-01,5708.75
929
- ^GSPC,2024-10-02,5709.5400390625
930
- ^GSPC,2024-10-03,5699.93994140625
931
- ^GSPC,2024-10-04,5751.06982421875
932
- ^GSPC,2024-10-07,5695.93994140625
933
- ^GSPC,2024-10-08,5751.1298828125
934
- ^GSPC,2024-10-09,5792.0400390625
935
- ^GSPC,2024-10-10,5780.0498046875
936
- ^GSPC,2024-10-11,5815.02978515625
937
- ^GSPC,2024-10-14,5859.85009765625
938
- ^GSPC,2024-10-15,5815.259765625
939
- ^GSPC,2024-10-16,5842.47021484375
940
- ^GSPC,2024-10-17,5841.47021484375
941
- ^GSPC,2024-10-18,5864.669921875
942
- ^GSPC,2024-10-21,5853.97998046875
943
- ^GSPC,2024-10-22,5851.2001953125
944
- ^GSPC,2024-10-23,5797.419921875
945
- ^GSPC,2024-10-24,5809.85986328125
946
- ^GSPC,2024-10-25,5808.1201171875
947
- ^GSPC,2024-10-28,5823.52001953125
948
- ^GSPC,2024-10-29,5832.919921875
949
- ^GSPC,2024-10-30,5813.669921875
950
- ^GSPC,2024-10-31,5705.4501953125
951
- ^GSPC,2024-11-01,5728.7998046875
952
- ^GSPC,2024-11-04,5712.68994140625
953
- ^GSPC,2024-11-05,5782.759765625
954
- ^GSPC,2024-11-06,5929.0400390625
955
- ^GSPC,2024-11-07,5973.10009765625
956
- ^GSPC,2024-11-08,5995.5400390625
957
- ^GSPC,2024-11-11,6001.35009765625
958
- ^GSPC,2024-11-12,5983.990234375
959
- ^GSPC,2024-11-13,5985.3798828125
960
- ^GSPC,2024-11-14,5949.169921875
961
- ^GSPC,2024-11-15,5870.6201171875
962
- ^GSPC,2024-11-18,5893.6201171875
963
- ^GSPC,2024-11-19,5916.97998046875
964
- ^GSPC,2024-11-20,5917.10986328125
965
- ^GSPC,2024-11-21,5948.7099609375
966
- ^GSPC,2024-11-22,5969.33984375
967
- ^GSPC,2024-11-25,5987.3701171875
968
- ^GSPC,2024-11-26,6021.6298828125
969
- ^GSPC,2024-11-27,5998.740234375
970
- ^GSPC,2024-11-29,6032.3798828125
971
- ^GSPC,2024-12-02,6047.14990234375
972
- ^GSPC,2024-12-03,6049.8798828125
973
- ^GSPC,2024-12-04,6086.490234375
974
- ^GSPC,2024-12-05,6075.10986328125
975
- ^GSPC,2024-12-06,6090.27001953125
976
- ^GSPC,2024-12-09,6052.85009765625
977
- ^GSPC,2024-12-10,6034.91015625
978
- ^GSPC,2024-12-11,6084.18994140625
979
- ^GSPC,2024-12-12,6051.25
980
- ^GSPC,2024-12-13,6051.08984375
981
- ^GSPC,2024-12-16,6074.080078125
982
- ^GSPC,2024-12-17,6050.60986328125
983
- ^GSPC,2024-12-18,5872.16015625
984
- ^GSPC,2024-12-19,5867.080078125
985
- ^GSPC,2024-12-20,5930.85009765625
986
- ^GSPC,2024-12-23,5974.06982421875
987
- ^GSPC,2024-12-24,6040.0400390625
988
- ^GSPC,2024-12-26,6037.58984375
989
- ^GSPC,2024-12-27,5970.83984375
990
- ^GSPC,2024-12-30,5906.93994140625
991
- ^GSPC,2024-12-31,5881.6298828125
992
- ^GSPC,2025-01-02,5868.5498046875
993
- ^GSPC,2025-01-03,5942.47021484375
994
- ^GSPC,2025-01-06,5975.3798828125
995
- ^GSPC,2025-01-07,5909.02978515625
996
- ^GSPC,2025-01-08,5918.25
997
- ^GSPC,2025-01-10,5827.0400390625
998
- ^GSPC,2025-01-13,5836.22021484375
999
- ^GSPC,2025-01-14,5842.91015625
1000
- ^GSPC,2025-01-15,5949.91015625
1001
- ^GSPC,2025-01-16,5937.33984375
1002
- ^GSPC,2025-01-17,5996.66015625
1003
- ^GSPC,2025-01-21,6049.240234375
1004
- ^GSPC,2025-01-22,6086.3701171875
1005
- ^GSPC,2025-01-23,6118.7099609375
1006
- ^GSPC,2025-01-24,6101.240234375
1007
- ^GSPC,2025-01-27,6012.27978515625
1008
- ^GSPC,2025-01-28,6067.7001953125
1009
- ^GSPC,2025-01-29,6039.31005859375
1010
- ^GSPC,2025-01-30,6071.169921875
1011
- ^GSPC,2025-01-31,6040.52978515625
1012
- ^GSPC,2025-02-03,5994.56982421875
1013
- ^GSPC,2025-02-04,6037.8798828125
1014
- ^GSPC,2025-02-05,6061.47998046875
1015
- ^GSPC,2025-02-06,6083.56982421875
1016
- ^GSPC,2025-02-07,6025.990234375
1017
- ^GSPC,2025-02-10,6066.43994140625
1018
- ^GSPC,2025-02-11,6068.5
1019
- ^GSPC,2025-02-12,6051.97021484375
1020
- ^GSPC,2025-02-13,6115.06982421875
1021
- ^GSPC,2025-02-14,6114.6298828125
1022
- ^GSPC,2025-02-18,6129.580078125
1023
- ^GSPC,2025-02-19,6144.14990234375
1024
- ^GSPC,2025-02-20,6117.52001953125
1025
- ^GSPC,2025-02-21,6013.1298828125
1026
- ^GSPC,2025-02-24,5983.25
1027
- ^GSPC,2025-02-25,5955.25
1028
- ^GSPC,2025-02-26,5956.06005859375
1029
- ^GSPC,2025-02-27,5861.56982421875
1030
- ^GSPC,2025-02-28,5954.5
1031
- ^GSPC,2025-03-03,5849.72021484375
1032
- ^GSPC,2025-03-04,5778.14990234375
1033
- ^GSPC,2025-03-05,5842.6298828125
1034
- ^GSPC,2025-03-06,5738.52001953125
1035
- ^GSPC,2025-03-07,5770.2001953125
1036
- ^GSPC,2025-03-10,5614.56005859375
1037
- ^GSPC,2025-03-11,5572.06982421875
1038
- ^GSPC,2025-03-12,5599.2998046875
1039
- ^GSPC,2025-03-13,5521.52001953125
1040
- ^GSPC,2025-03-14,5638.93994140625
1041
- ^GSPC,2025-03-17,5675.1201171875
1042
- ^GSPC,2025-03-18,5614.66015625
1043
- ^GSPC,2025-03-19,5675.2900390625
1044
- ^GSPC,2025-03-20,5662.89013671875
1045
- ^GSPC,2025-03-21,5667.56005859375
1046
- ^GSPC,2025-03-24,5767.56982421875
1047
- ^GSPC,2025-03-25,5776.64990234375
1048
- ^GSPC,2025-03-26,5712.2001953125
1049
- ^GSPC,2025-03-27,5693.31005859375
1050
- ^GSPC,2025-03-28,5580.93994140625
1051
- ^GSPC,2025-03-31,5611.85009765625
1052
- """
1053
- temp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode='w', newline='')
1054
- temp.write(sample_data)
1055
- temp.close()
1056
- return temp.name
1057
-
1058
- # Global theme
1059
- theme = gr.themes.Soft(
1060
- primary_hue="blue",
1061
- secondary_hue="indigo",
1062
- neutral_hue="gray"
1063
- )
1064
 
1065
- # Gradio interface
1066
- with gr.Blocks(title="Time Series Forecasting App", theme=theme) as app:
1067
- gr.Markdown("# 📈 Time Series Forecasting App")
1068
- gr.Markdown("Upload a CSV with `unique_id`, `ds`, and `y` columns to apply forecasting models.")
1069
-
1070
- # Disclaimer about external predictors
1071
- with gr.Accordion("Disclaimer", open=True):
1072
- gr.Markdown("""
1073
- **Disclaimer:** For simplicity, this app does not allow the use of external predictors.
1074
- However, they can be easily included in the underlying statsforecast (for AutoARIMA)
1075
- and the TimeGPT implementation by Nixtla. To use external predictors, you would need to modify
1076
- the code to include them in your forecasting models.
1077
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
 
1079
  with gr.Row():
1080
- with gr.Column(scale=2):
1081
- file_input = gr.File(label="Upload CSV file", file_types=[".csv"])
1082
-
1083
- download_btn = gr.Button("Download Sample Data", variant="secondary")
1084
- download_output = gr.File(label="Click to download", visible=True)
1085
- download_btn.click(fn=download_sample, outputs=download_output)
1086
-
1087
- with gr.Accordion("Data & Validation Settings", open=True):
1088
- frequency = gr.Dropdown(
1089
- choices=[
1090
- ("Hourly", "H"),
1091
- ("Business Day", "B"),
1092
- ("Daily", "D"),
1093
- ("Weekly", "WS"),
1094
- ("Monthly", "MS"),
1095
- ("Quarterly", "QS"),
1096
- ("Yearly", "YS")
1097
- ],
1098
- label="Data Frequency",
1099
- value="D"
1100
- )
1101
-
1102
- # Evaluation Strategy
1103
- eval_strategy = gr.Radio(
1104
- choices=["Fixed Window", "Cross Validation"],
1105
- label="Evaluation Strategy",
1106
- value="Cross Validation"
1107
- )
1108
-
1109
- # Fixed Window settings
1110
- with gr.Group(visible=True) as fixed_window_box:
1111
- gr.Markdown("### Fixed Window Settings")
1112
- horizon = gr.Slider(1, 100, value=10, step=1, label="Validation Horizon (steps ahead to predict)")
1113
-
1114
- # Cross Validation settings
1115
- with gr.Group(visible=True) as cv_box:
1116
- gr.Markdown("### Cross Validation Settings")
1117
- with gr.Row():
1118
- step_size = gr.Slider(1, 50, value=10, step=1, label="Step Size (distance between windows)")
1119
- num_windows = gr.Slider(1, 20, value=5, step=1, label="Number of Windows")
1120
-
1121
- # Future forecast settings (always visible)
1122
- with gr.Group():
1123
- gr.Markdown("### Future Forecast Settings")
1124
- future_horizon = gr.Slider(1, 100, value=10, step=1, label="Future Forecast Horizon (steps to predict)")
1125
-
1126
- with gr.Accordion("Model Configuration", open=True):
1127
- with gr.Tabs() as model_tabs:
1128
- # Traditional Statistical Models Tab
1129
- with gr.TabItem("Statistical Models"):
1130
- gr.Markdown("## Basic Models")
1131
- with gr.Row():
1132
- use_historical_avg = gr.Checkbox(label="Historical Average", value=True)
1133
- use_naive = gr.Checkbox(label="Naive", value=True)
1134
-
1135
- # Common seasonality parameter at the top level
1136
- with gr.Group():
1137
- gr.Markdown("### Seasonality Configuration")
1138
- gr.Markdown("This seasonality period affects Seasonal Naive, Seasonal Window Average, AutoETS, and AutoARIMA models")
1139
- seasonality = gr.Number(label="Seasonality Period", value=5)
1140
-
1141
- gr.Markdown("### Seasonal Models")
1142
- with gr.Row():
1143
- use_seasonal_naive = gr.Checkbox(label="Seasonal Naive", value=True)
1144
-
1145
- gr.Markdown("### Window-based Models")
1146
- with gr.Row():
1147
- use_window_avg = gr.Checkbox(label="Window Average", value=False)
1148
- window_size = gr.Number(label="Window Size", value=10)
1149
-
1150
- with gr.Row():
1151
- use_seasonal_window_avg = gr.Checkbox(label="Seasonal Window Average", value=False)
1152
- seasonal_window_size = gr.Number(label="Seasonal Window Size", value=2)
1153
-
1154
- gr.Markdown("### Advanced Models (use seasonality from above)")
1155
- with gr.Row():
1156
- use_autoets = gr.Checkbox(label="AutoETS (Exponential Smoothing)", value=False)
1157
- use_autoarima = gr.Checkbox(label="AutoARIMA", value=False)
1158
-
1159
- # Transformer Models Tab (TimeGPT)
1160
- with gr.TabItem("Transformer Models"):
1161
- gr.Markdown("## TimeGPT Model")
1162
- gr.Markdown("TimeGPT uses a transformer architecture for state-of-the-art time series forecasting")
1163
-
1164
- with gr.Row():
1165
- use_timegpt = gr.Checkbox(label="Use TimeGPT", value=True)
1166
-
1167
- with gr.Group():
1168
- gr.Markdown("### TimeGPT Configuration")
1169
- with gr.Row():
1170
- finetune_loss = gr.Dropdown(
1171
- choices=["mape", "mae", "rmse", "smape"],
1172
- label="Finetune Loss Metric",
1173
- value="mape"
1174
- )
1175
- confidence_level = gr.Slider(50, 99, value=95, step=1, label="Confidence Level (%)")
1176
-
1177
- gr.Markdown("""
1178
- **Note:** Using TimeGPT requires a valid API key. The API key should
1179
- be set as an environment variable named `NIXTLA_API_KEY`. This space uses a trial key, which is rate limited.
1180
- """)
1181
-
1182
- with gr.Column(scale=3):
1183
- message_output = gr.Textbox(label="Status Message")
1184
-
1185
- with gr.Tabs() as tabs:
1186
- with gr.TabItem("Validation Results"):
1187
- eval_output = gr.Dataframe(label="Evaluation Metrics")
1188
- validation_plot = gr.Plot(label="Validation Plot")
1189
- validation_output = gr.Dataframe(label="Validation Data", visible=False)
1190
-
1191
- with gr.Row():
1192
- show_data_btn = gr.Button("Show Validation Data")
1193
- hide_data_btn = gr.Button("Hide Validation Data", visible=False)
1194
-
1195
- def show_data():
1196
- return gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
1197
-
1198
- def hide_data():
1199
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
1200
-
1201
- show_data_btn.click(
1202
- fn=show_data,
1203
- outputs=[validation_output, hide_data_btn, show_data_btn]
1204
- )
1205
-
1206
- hide_data_btn.click(
1207
- fn=hide_data,
1208
- outputs=[validation_output, hide_data_btn, show_data_btn]
1209
- )
1210
-
1211
- with gr.TabItem("Future Forecast"):
1212
- forecast_plot = gr.Plot(label="Future Forecast Plot")
1213
- forecast_output = gr.Dataframe(label="Future Forecast Data", visible=False)
1214
-
1215
- with gr.Row():
1216
- show_forecast_btn = gr.Button("Show Forecast Data")
1217
- hide_forecast_btn = gr.Button("Hide Forecast Data", visible=False)
1218
-
1219
- def show_forecast():
1220
- return gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
1221
-
1222
- def hide_forecast():
1223
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
1224
-
1225
- show_forecast_btn.click(
1226
- fn=show_forecast,
1227
- outputs=[forecast_output, hide_forecast_btn, show_forecast_btn]
1228
- )
1229
-
1230
- hide_forecast_btn.click(
1231
- fn=hide_forecast,
1232
- outputs=[forecast_output, hide_forecast_btn, show_forecast_btn]
1233
- )
1234
-
1235
- with gr.TabItem("Export Results"):
1236
- export_files = gr.Files(label="Download Results")
1237
-
1238
- with gr.Row(visible=True) as run_row:
1239
- submit_btn = gr.Button("Run Validation and Forecast", variant="primary", size="lg")
1240
-
1241
- # Update visibility of the appropriate box based on evaluation strategy
1242
- def update_eval_boxes(strategy):
1243
- return (gr.update(visible=strategy == "Fixed Window"),
1244
- gr.update(visible=strategy == "Cross Validation"))
1245
-
1246
- eval_strategy.change(
1247
- fn=update_eval_boxes,
1248
- inputs=[eval_strategy],
1249
- outputs=[fixed_window_box, cv_box]
1250
  )
1251
 
1252
- # Run forecast when button is clicked
1253
- submit_btn.click(
1254
  fn=run_forecast,
1255
  inputs=[
1256
- file_input, frequency, eval_strategy, horizon, step_size, num_windows,
1257
- use_historical_avg, use_naive, use_seasonal_naive, seasonality,
1258
- use_window_avg, window_size, use_seasonal_window_avg, seasonal_window_size,
1259
- use_autoets, use_autoarima, use_timegpt, finetune_loss, confidence_level,
1260
- future_horizon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1261
  ],
1262
- outputs=[
1263
- eval_output,
1264
- validation_output,
1265
- validation_plot,
1266
- forecast_output,
1267
- forecast_plot,
1268
- export_files,
1269
- message_output]
1270
  )
1271
 
1272
- if __name__ == "__main__":
1273
- app.launch(share=False)
 
 
 
 
 
1
  import os
2
+ import tempfile
3
  from datetime import datetime
4
 
5
+ import gradio as gr
6
+ import matplotlib.pyplot as plt
7
+ import pandas as pd
8
+
9
  from statsforecast import StatsForecast
10
  from statsforecast.models import (
11
  HistoricAverage,
 
14
  WindowAverage,
15
  SeasonalWindowAverage,
16
  AutoETS,
17
+ AutoARIMA,
18
  )
19
 
20
  from utilsforecast.evaluation import evaluate
21
+ from utilsforecast.losses import (
22
+ mae,
23
+ mape,
24
+ mase,
25
+ mse,
26
+ rmse,
27
+ smape,
28
+ )
29
 
30
+ # TimeGPT
31
  from nixtla import NixtlaClient
32
 
33
+
34
+ # -----------------------------
35
+ # Data helpers
36
+ # -----------------------------
37
+ REQUIRED_COLS = ["unique_id", "ds", "y"]
38
+
39
+
40
+ def load_training_data(file_obj):
41
+ """Load training CSV. Keep ALL columns, but validate required cols."""
42
+ if file_obj is None:
43
+ return None, [], "Please upload a CSV file."
44
+
45
+ try:
46
+ df = pd.read_csv(file_obj.name if hasattr(file_obj, "name") else file_obj)
47
+ missing = [c for c in REQUIRED_COLS if c not in df.columns]
48
+ if missing:
49
+ return None, [], f"Missing required columns: {', '.join(missing)}"
50
+
51
+ df["ds"] = pd.to_datetime(df["ds"])
52
+ df = df.sort_values(["unique_id", "ds"]).reset_index(drop=True)
53
+
54
+ if df["y"].isna().any():
55
+ return None, [], "Data contains missing values in the 'y' column."
56
+
57
+ candidate_xcols = [c for c in df.columns if c not in REQUIRED_COLS]
58
+ return df, candidate_xcols, "Training data loaded successfully!"
59
+ except Exception as e:
60
+ return None, [], f"Error loading data: {e}"
61
+
62
+
63
+ def load_future_x(file_obj, xcols):
64
+ """Load future predictors (X_df) for horizon. Must have unique_id, ds, and selected xcols."""
65
+ if not xcols:
66
+ return None, "No predictors selected."
67
+
68
+ if file_obj is None:
69
+ return None, "You selected predictors, so please upload a future predictors CSV (X_df)."
70
+
71
  try:
72
+ X_df = pd.read_csv(file_obj.name if hasattr(file_obj, "name") else file_obj)
73
+ required = ["unique_id", "ds"] + list(xcols)
74
+ missing = [c for c in required if c not in X_df.columns]
75
+ if missing:
76
+ return None, f"Future predictors CSV missing: {', '.join(missing)}"
77
+
78
+ X_df["ds"] = pd.to_datetime(X_df["ds"])
79
+ X_df = X_df.sort_values(["unique_id", "ds"]).reset_index(drop=True)
80
+ return X_df, "Future predictors loaded."
 
 
 
 
 
 
81
  except Exception as e:
82
+ return None, f"Error loading future predictors: {e}"
83
+
84
 
 
85
  def calculate_date_offset(freq, horizon):
86
+ """Rough offset for plot windowing."""
87
+ if freq == "H":
88
  return pd.Timedelta(hours=horizon)
89
+ if freq == "D":
90
  return pd.Timedelta(days=horizon)
91
+ if freq == "B":
92
+ return pd.Timedelta(days=int(horizon * 1.4)) # approx weekends
93
+ if freq == "WS":
 
94
  return pd.Timedelta(weeks=horizon)
95
+ if freq == "MS":
96
+ return pd.Timedelta(days=horizon * 30)
97
+ if freq == "QS":
98
+ return pd.Timedelta(days=horizon * 90)
99
+ if freq == "YS":
100
+ return pd.Timedelta(days=horizon * 365)
101
+ return pd.Timedelta(days=horizon)
102
+
 
103
 
104
+ # -----------------------------
105
+ # Plotting
106
+ # -----------------------------
107
+ def create_cv_plot(cv_df, original_df, title, horizon=None, freq="D"):
108
  plt.figure(figsize=(12, 7))
109
+
110
+ forecast_cols = [c for c in cv_df.columns if c not in ["unique_id", "ds", "cutoff", "y"]]
111
+ unique_ids = cv_df["unique_id"].unique()
112
+
113
  colors = plt.cm.tab10.colors
 
 
114
  min_cutoff = None
115
+
116
+ for uid in unique_ids:
117
+ hist = original_df[original_df["unique_id"] == uid]
118
+ plt.plot(hist["ds"], hist["y"], "k-", linewidth=2, label=f"{uid} (Actual)")
119
+
120
+ f = cv_df[cv_df["unique_id"] == uid]
121
+
122
+ if "cutoff" in f.columns:
123
+ cutoffs = pd.to_datetime(f["cutoff"].unique())
 
124
  if len(cutoffs) > 0:
125
+ earliest = cutoffs.min()
126
+ min_cutoff = earliest if (min_cutoff is None or earliest < min_cutoff) else min_cutoff
 
 
 
127
  for cutoff in cutoffs:
128
+ plt.axvline(x=cutoff, color="gray", linestyle="--", alpha=0.35)
129
+
 
130
  for j, col in enumerate(forecast_cols):
131
+ if col in f.columns:
132
+ name = col.replace("_", " ").title()
133
+ if name == "Timegpt":
134
+ name = "TimeGPT"
135
+ plt.plot(
136
+ f["ds"],
137
+ f[col],
138
+ linestyle="--",
139
+ linewidth=1.5,
140
+ color=colors[j % len(colors)],
141
+ label=name,
142
+ )
143
 
144
  plt.title(title, fontsize=16)
145
+ plt.xlabel("Date")
146
+ plt.ylabel("Value")
147
  plt.grid(True, alpha=0.3)
148
+ plt.legend(loc="upper center", bbox_to_anchor=(0.5, -0.15), ncol=3, fontsize=10)
149
+ plt.tight_layout(rect=[0, 0.05, 1, 0.95])
150
+
 
 
 
151
  if min_cutoff is not None and horizon is not None:
152
+ start = min_cutoff - calculate_date_offset(freq, horizon)
153
+ plt.xlim(start, cv_df["ds"].max())
154
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  fig = plt.gcf()
 
156
  fig.autofmt_xdate()
 
157
  return fig
158
 
159
+
160
+ def create_future_plot(fcst_df, original_df, title="Future Forecast", horizon=None, freq="D"):
161
  plt.figure(figsize=(12, 7))
162
+
163
+ forecast_cols = [c for c in fcst_df.columns if c not in ["unique_id", "ds"]]
164
+ unique_ids = fcst_df["unique_id"].unique()
165
  colors = plt.cm.tab10.colors
166
+
167
+ for uid in unique_ids:
168
+ hist = original_df[original_df["unique_id"] == uid]
169
+ f = fcst_df[fcst_df["unique_id"] == uid]
170
+
171
+ plt.plot(hist["ds"], hist["y"], "k-", linewidth=2, label=f"{uid} (Historical)")
172
+
173
+ if not f.empty:
174
+ start = f["ds"].min()
175
+ plt.axvline(x=start, color="gray", linestyle="--", alpha=0.5)
176
+ plt.axvspan(start, f["ds"].max(), alpha=0.1)
177
+
178
+ for j, col in enumerate(forecast_cols):
179
+ name = col.replace("_", " ").title()
180
+ if name == "Timegpt":
181
+ name = "TimeGPT"
182
+ plt.plot(
183
+ f["ds"],
184
+ f[col],
185
+ linestyle="--",
186
+ linewidth=1.5,
187
+ color=colors[j % len(colors)],
188
+ label=name,
189
+ )
190
+
191
+ plt.title(title, fontsize=16)
192
+ plt.xlabel("Date")
193
+ plt.ylabel("Value")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  plt.grid(True, alpha=0.3)
195
+ plt.legend(loc="upper center", bbox_to_anchor=(0.5, -0.15), ncol=3, fontsize=10)
196
+ plt.tight_layout(rect=[0, 0.05, 1, 0.95])
197
+
198
+ if horizon is not None and not fcst_df.empty:
199
+ start = pd.to_datetime(fcst_df["ds"].min()) - calculate_date_offset(freq, horizon)
200
+ plt.xlim(start, fcst_df["ds"].max())
201
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  fig = plt.gcf()
 
203
  fig.autofmt_xdate()
 
204
  return fig
205
 
206
+
207
+ # -----------------------------
208
+ # Export
209
+ # -----------------------------
210
+ def export_results(eval_df, cv_df, future_df):
211
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
212
  temp_dir = tempfile.mkdtemp()
213
+ out = []
214
+
215
+ if eval_df is not None and not eval_df.empty:
216
+ p = os.path.join(temp_dir, f"evaluation_metrics_{ts}.csv")
217
+ eval_df.to_csv(p, index=False)
218
+ out.append(p)
219
+
220
+ if cv_df is not None and not cv_df.empty:
221
+ p = os.path.join(temp_dir, f"cross_validation_results_{ts}.csv")
222
+ cv_df.to_csv(p, index=False)
223
+ out.append(p)
224
+
225
+ if future_df is not None and not future_df.empty:
226
+ p = os.path.join(temp_dir, f"future_forecasts_{ts}.csv")
227
+ future_df.to_csv(p, index=False)
228
+ out.append(p)
229
+
230
+ return out
231
+
232
+
233
+ # -----------------------------
234
+ # Core run
235
+ # -----------------------------
236
  def run_forecast(
237
+ train_file,
238
+ freq,
239
+ season_length,
240
+ cv_h,
241
+ cv_step_size,
242
+ cv_windows,
243
+ future_h,
244
+ loss_name,
245
+ # models
246
+ use_histavg,
247
  use_naive,
248
+ use_snaive,
249
+ use_winavg,
250
+ use_swinavg,
 
 
 
251
  use_autoets,
252
  use_autoarima,
253
+ # timegpt
254
  use_timegpt,
255
+ nixtla_api_key,
256
+ # predictors
257
+ xcols,
258
+ future_x_file,
259
  ):
260
+ # 1) Load training
261
+ df, candidate_xcols, msg = load_training_data(train_file)
262
  if df is None:
263
+ return None, None, None, msg, None
264
+
265
+ # base y-only df
266
+ df_y = df[["unique_id", "ds", "y"]].copy()
267
+
268
+ use_exog = bool(xcols)
269
+
270
+ # 2) Load future X if needed
271
+ X_future = None
272
+ if use_exog:
273
+ X_future, xmsg = load_future_x(future_x_file, xcols)
274
+ if X_future is None:
275
+ return None, None, None, xmsg, None
276
+
277
+ # enforce ordering expected by many workflows: unique_id, ds, y, x1..xk
278
+ df_exog = df[["unique_id", "ds", "y"] + list(xcols)].copy()
279
+
280
+ # 3) Choose loss function
281
+ loss_map = {
282
+ "mae": mae,
283
+ "mape": mape,
284
+ "mase": mase,
285
+ "mse": mse,
286
+ "rmse": rmse,
287
+ "smape": smape,
288
+ }
289
+ loss_fn = loss_map.get(loss_name, rmse)
290
+
291
+ # 4) Build models
292
+ univariate_models = []
293
+ exog_models = []
294
+
295
+ if use_histavg:
296
+ univariate_models.append(HistoricAverage(alias="hist_avg"))
297
  if use_naive:
298
+ univariate_models.append(Naive(alias="naive"))
299
+ if use_snaive:
300
+ univariate_models.append(SeasonalNaive(alias="seasonal_naive", season_length=season_length))
301
+ if use_winavg:
302
+ univariate_models.append(WindowAverage(alias="window_avg", window_size=season_length))
303
+ if use_swinavg:
304
+ univariate_models.append(SeasonalWindowAverage(alias="seasonal_window_avg", season_length=season_length))
 
 
 
 
305
  if use_autoets:
306
+ univariate_models.append(AutoETS(alias="autoets", season_length=season_length))
307
+
308
+ # AutoARIMA can use exogenous; route it accordingly if X selected
309
  if use_autoarima:
310
+ model = AutoARIMA(alias="autoarima", season_length=season_length)
311
+ if use_exog:
312
+ exog_models.append(model)
313
+ else:
314
+ univariate_models.append(model)
315
 
316
+ # 5) Cross-validation + evaluation + future forecast
317
+ eval_parts = []
318
+ cv_parts = []
319
+ future_parts = []
320
 
321
+ # Univariate pipeline
322
+ if univariate_models:
323
+ sf_u = StatsForecast(models=univariate_models, freq=freq, n_jobs=-1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
+ cv_u = sf_u.cross_validation(df=df_y, h=cv_h, step_size=cv_step_size, n_windows=cv_windows)
326
+ cv_parts.append(cv_u)
327
+
328
+ eval_u = evaluate(cv_u, metrics=[loss_fn])
329
+ eval_u["pipeline"] = "univariate"
330
+ eval_parts.append(eval_u)
331
+
332
+ fc_u = sf_u.forecast(df=df_y, h=future_h)
333
+ future_parts.append(fc_u)
334
+
335
+ # Exogenous pipeline (StatsForecast)
336
+ if exog_models:
337
+ sf_x = StatsForecast(models=exog_models, freq=freq, n_jobs=-1)
338
+
339
+ cv_x = sf_x.cross_validation(df=df_exog, h=cv_h, step_size=cv_step_size, n_windows=cv_windows)
340
+ cv_parts.append(cv_x)
341
+
342
+ eval_x = evaluate(cv_x, metrics=[loss_fn])
343
+ eval_x["pipeline"] = "exog"
344
+ eval_parts.append(eval_x)
345
+
346
+ fc_x = sf_x.forecast(df=df_exog, h=future_h, X_df=X_future)
347
+ future_parts.append(fc_x)
348
+
349
+ # TimeGPT
350
+ if use_timegpt:
351
+ if not nixtla_api_key:
352
+ return None, None, None, "TimeGPT selected but no Nixtla API key provided.", None
353
+
354
+ client = NixtlaClient(api_key=nixtla_api_key)
355
+
356
+ # Provide historical exog cols if selected
357
+ df_tgpt = df[["unique_id", "ds", "y"] + list(xcols)].copy() if use_exog else df_y
358
+
359
+ # Forecast
360
+ tgpt_fc = client.forecast(
361
+ df=df_tgpt,
362
+ h=future_h,
363
+ freq=freq,
364
+ X_df=X_future if use_exog else None,
365
+ hist_exog_list=list(xcols) if use_exog else None,
366
+ )
367
+
368
+ # Normalize column name to match plots/merges
369
+ # Nixtla typically outputs: unique_id, ds, TimeGPT (or yhat)
370
+ # We'll rename forecast column to "timegpt" if needed.
371
+ tgpt = tgpt_fc.copy()
372
+ for c in tgpt.columns:
373
+ if c.lower() in ("timegpt", "yhat", "y_hat", "forecast"):
374
+ tgpt = tgpt.rename(columns={c: "timegpt"})
375
+ break
376
+ if "timegpt" not in tgpt.columns:
377
+ # fallback: pick the first non-id col
378
+ non_id = [c for c in tgpt.columns if c not in ["unique_id", "ds"]]
379
+ if non_id:
380
+ tgpt = tgpt.rename(columns={non_id[0]: "timegpt"})
381
+
382
+ future_parts.append(tgpt)
383
+
384
+ if not eval_parts and not future_parts:
385
+ return None, None, None, "No models selected.", None
386
+
387
+ # 6) Merge CV, eval, future forecasts
388
+ eval_df = pd.concat(eval_parts, ignore_index=True) if eval_parts else None
389
+ cv_df = pd.concat(cv_parts, ignore_index=True) if cv_parts else None
390
+
391
+ # Merge future forecasts on (unique_id, ds)
392
+ future_df = None
393
+ if future_parts:
394
+ future_df = future_parts[0].copy()
395
+ for part in future_parts[1:]:
396
+ # avoid duplicate cols
397
+ overlap = set(future_df.columns).intersection(set(part.columns)) - {"unique_id", "ds"}
398
+ part2 = part.drop(columns=list(overlap), errors="ignore")
399
+ future_df = future_df.merge(part2, on=["unique_id", "ds"], how="outer")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
+ future_df = future_df.sort_values(["unique_id", "ds"]).reset_index(drop=True)
402
+
403
+ # 7) Plots
404
+ cv_plot = create_cv_plot(
405
+ cv_df, df_y, title="Cross-Validation Forecasts", horizon=cv_h, freq=freq
406
+ ) if cv_df is not None and not cv_df.empty else None
407
+
408
+ future_plot = create_future_plot(
409
+ future_df, df_y, title="Future Forecast", horizon=future_h, freq=freq
410
+ ) if future_df is not None and not future_df.empty else None
411
+
412
+ # 8) Export files
413
+ files = export_results(eval_df, cv_df, future_df)
414
+
415
+ # Message
416
+ msg2 = msg
417
+ if use_exog:
418
+ msg2 += f" | Predictors used: {', '.join(xcols)}"
419
+
420
+ return eval_df, cv_plot, future_df, msg2, files
421
+
422
+
423
+ # -----------------------------
424
+ # UI wiring
425
+ # -----------------------------
426
+ def on_train_upload(file_obj):
427
+ df, xchoices, msg = load_training_data(file_obj)
428
+ return (
429
+ msg,
430
+ gr.update(choices=xchoices, value=[]),
431
+ gr.update(visible=True),
432
+ )
433
+
434
+
435
+ with gr.Blocks(title="StatsForecast + TimeGPT (with Predictors)") as demo:
436
+ gr.Markdown(
437
+ """
438
+ # StatsForecast + TimeGPT Forecasting Demo (with optional predictors)
439
+
440
+ **Training CSV required columns:** `unique_id, ds, y`
441
+ **Optional predictors:** any extra columns in the training CSV.
442
+
443
+ If you select predictors, upload a **future predictors CSV** (`X_df`) with columns:
444
+ `unique_id, ds, <selected predictors...>` covering the forecast horizon.
445
+ """
446
+ )
447
 
448
  with gr.Row():
449
+ train_file = gr.File(label="Upload training CSV", file_types=[".csv"])
450
+ status = gr.Textbox(label="Status", value="Upload a CSV to begin.", interactive=False)
451
+
452
+ xcols = gr.Dropdown(
453
+ choices=[],
454
+ multiselect=True,
455
+ label="Predictor columns (X) (optional)",
456
+ info="Select exogenous/predictor columns from the training data. Requires X_df for the forecast horizon.",
457
+ )
458
+ future_x_file = gr.File(
459
+ label="Upload future predictors CSV (X_df) (required if predictors selected)",
460
+ file_types=[".csv"],
461
+ )
462
+
463
+ with gr.Accordion("Forecast settings", open=True):
464
+ with gr.Row():
465
+ freq = gr.Dropdown(
466
+ choices=["H", "D", "B", "WS", "MS", "QS", "YS"],
467
+ value="D",
468
+ label="Frequency (pandas offset alias)",
469
+ )
470
+ season_length = gr.Number(value=7, label="Season length", precision=0)
471
+
472
+ with gr.Row():
473
+ cv_h = gr.Number(value=14, label="CV horizon (h)", precision=0)
474
+ cv_step_size = gr.Number(value=1, label="CV step size", precision=0)
475
+ cv_windows = gr.Number(value=3, label="CV windows", precision=0)
476
+
477
+ future_h = gr.Number(value=30, label="Future forecast horizon", precision=0)
478
+
479
+ loss_name = gr.Dropdown(
480
+ choices=["rmse", "mae", "mse", "mape", "smape", "mase"],
481
+ value="rmse",
482
+ label="Evaluation metric",
483
+ )
484
+
485
+ with gr.Accordion("Models", open=True):
486
+ with gr.Row():
487
+ use_histavg = gr.Checkbox(value=False, label="HistoricAverage")
488
+ use_naive = gr.Checkbox(value=True, label="Naive")
489
+ use_snaive = gr.Checkbox(value=False, label="SeasonalNaive")
490
+ use_winavg = gr.Checkbox(value=False, label="WindowAverage")
491
+ use_swinavg = gr.Checkbox(value=False, label="SeasonalWindowAverage")
492
+ use_autoets = gr.Checkbox(value=True, label="AutoETS")
493
+ use_autoarima = gr.Checkbox(value=True, label="AutoARIMA (supports predictors)")
494
+
495
+ with gr.Accordion("TimeGPT", open=False):
496
+ use_timegpt = gr.Checkbox(value=False, label="Enable TimeGPT")
497
+ nixtla_api_key = gr.Textbox(
498
+ label="Nixtla API Key",
499
+ placeholder="Paste your Nixtla API key here",
500
+ type="password",
501
+ )
502
+
503
+ run_btn = gr.Button("Run Forecast", variant="primary")
504
+
505
+ gr.Markdown("## Outputs")
506
+ eval_df_out = gr.Dataframe(label="Evaluation metrics (by pipeline)")
507
+ cv_plot_out = gr.Plot(label="Cross-validation plot")
508
+ future_df_out = gr.Dataframe(label="Future forecasts")
509
+ files_out = gr.Files(label="Download results")
510
+
511
+ # Update predictor choices when training file is uploaded
512
+ train_file.change(
513
+ fn=on_train_upload,
514
+ inputs=[train_file],
515
+ outputs=[status, xcols, xcols], # last output toggles visible; harmless
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  )
517
 
518
+ run_btn.click(
 
519
  fn=run_forecast,
520
  inputs=[
521
+ train_file,
522
+ freq,
523
+ season_length,
524
+ cv_h,
525
+ cv_step_size,
526
+ cv_windows,
527
+ future_h,
528
+ loss_name,
529
+ use_histavg,
530
+ use_naive,
531
+ use_snaive,
532
+ use_winavg,
533
+ use_swinavg,
534
+ use_autoets,
535
+ use_autoarima,
536
+ use_timegpt,
537
+ nixtla_api_key,
538
+ xcols,
539
+ future_x_file,
540
  ],
541
+ outputs=[eval_df_out, cv_plot_out, future_df_out, status, files_out],
 
 
 
 
 
 
 
542
  )
543
 
544
+ demo.launch()