File size: 24,443 Bytes
8790313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49dbaf7
8790313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import gradio as gr
import warnings

# Suppress warnings to keep output clean
warnings.filterwarnings("ignore")

# ----------------------------------------------------------------------------
# Import Prophet (Meta's forecasting library)
# ----------------------------------------------------------------------------
try:
    from prophet import Prophet
except Exception as e:
    raise ImportError(
        "Prophet is not installed. Run: pip install prophet\n"
        "If you still get errors, try: pip install cmdstanpy prophet"
    ) from e

# ----------------------------------------------------------------------------
# Import ARIMA and SARIMA from statsmodels
# ----------------------------------------------------------------------------
try:
    from statsmodels.tsa.arima.model import ARIMA
    from statsmodels.tsa.statespace.sarimax import SARIMAX
except Exception as e:
    raise ImportError(
        "statsmodels is not installed. Run: pip install statsmodels"
    ) from e


# ============================================================================
# CONFIGURATION / SETTINGS
# ============================================================================

# What percentage of data to use for training? (0.70 = 70%)
# The remaining 30% will be used for testing our predictions
TRAIN_FRAC = 0.70

# ----------------------------------------------------------------------------
# ARIMA PARAMETERS: (p, d, q)
# ----------------------------------------------------------------------------
# p (AutoRegressive order): How many past values to use
#   - Higher p = model looks further back in time
#   - Example: p=3 means "use the last 3 values to predict the next one"
#
# d (Differencing order): How many times to difference the data
#   - Differencing removes trends by subtracting consecutive values
#   - d=1 means: new_value = original_value - previous_value
#   - This turns an upward trend into a flat line (easier to model!)
#
# q (Moving Average order): How many past forecast errors to use
#   - Helps the model learn from its mistakes
#   - Example: q=2 means "adjust based on the last 2 prediction errors"
#
ARIMA_ORDER = (5, 1, 2)  # We use p=5, d=1, q=2

# ----------------------------------------------------------------------------
# SARIMA PARAMETERS: (p, d, q) x (P, D, Q, s)
# ----------------------------------------------------------------------------
# The first part (p, d, q) is the same as ARIMA (explained above)
#
# The second part (P, D, Q, s) handles SEASONALITY:
#   P: Seasonal autoregressive order (like p, but for seasonal patterns)
#   D: Seasonal differencing (removes seasonal trends)
#   Q: Seasonal moving average order
#   s: The SEASONAL PERIOD - how many time steps until the pattern repeats
#
# IMPORTANT: We use SIMPLIFIED parameters for speed!
# ----------------------------------------------------------------------------
# Original (slow): (2,1,2) x (1,1,1,60) on 3000+ points = 20-60 minutes
# Optimized (fast): (1,1,1) x (1,0,1,24) on ~75 points = ~30 seconds
# ----------------------------------------------------------------------------
SARIMA_ORDER = (1, 1, 1)  # Simplified non-seasonal part
SARIMA_SEASONAL_ORDER = (1, 0, 1, 24)  # s=24 for daily pattern in hourly data

# How much to downsample for SARIMA (use every Nth point)
# 60 = convert minute data to hourly data (average every 60 minutes)
SARIMA_DOWNSAMPLE_FACTOR = 60

# ----------------------------------------------------------------------------
# PROPHET PARAMETERS
# ----------------------------------------------------------------------------
# changepoint_prior_scale: How flexible is the trend?
#   - Higher value = trend can change more dramatically
#   - Lower value = smoother, more stable trend
#   - Default is 0.05, we use 0.05
#
# seasonality_prior_scale: How strong are seasonal effects?
#   - Higher value = stronger seasonal patterns
#   - Lower value = weaker seasonal patterns
#
# n_changepoints: How many points where trend can change direction?
#   - More changepoints = more flexible trend
#
PROPHET_CHANGEPOINT_PRIOR = 0.05
PROPHET_SEASONALITY_PRIOR = 10.0
PROPHET_N_CHANGEPOINTS = 25


# ============================================================================
# DATA GENERATION
# ============================================================================
# We'll create SYNTHETIC environmental sensor data
# This simulates what you might see in a space station or submarine!
#
# We generate 4 types of measurements:
#   1. O2 (Oxygen percentage) - normally around 21%
#   2. CO2 (Carbon Dioxide in ppm) - normally around 400 ppm
#   3. VOCs (Volatile Organic Compounds in ppb) - chemical pollutants
#   4. PM (Particulate Matter in μg/m³) - tiny particles in the air

def smooth_noise(n_points, low, high, window=30):
    # Generate random numbers from a UNIFORM distribution
    # Uniform means every number between low and high is equally likely
    raw_noise = np.random.uniform(low, high, n_points)

    # Apply moving average using convolution
    # np.ones(window)/window creates an averaging filter
    # Example: window=3 creates [0.333, 0.333, 0.333]
    smoothed = np.convolve(raw_noise, np.ones(window) / window, mode='same')

    return smoothed


def generate_mission_data(seed=42, n_points=4500):
    # Set random seed so we get the same "random" data each time
    # This is important for reproducibility in science!
    np.random.seed(seed)

    # Create time array: 0, 1, 2, 3, ... n_points-1
    t = np.arange(n_points)

    # Define seasonal periods (in minutes)
    daily_period = 1440    # 24 hours × 60 minutes = 1440 minutes per day
    weekly_period = 10080  # 7 days × 1440 minutes = 10080 minutes per week

    # ========================================================================
    # O2 (OXYGEN) - normally around 20.9%
    # ========================================================================
    # In a closed environment, O2 slowly decreases as people breathe
    # It follows an INVERSE pattern to CO2 (when CO2 goes up, O2 goes down)

    o2_baseline = 20.9                    # Starting value (normal atmospheric O2)
    o2_trend = -0.00005 * t               # Very slow downward trend

    # Seasonal patterns using SINE WAVES
    # Why sine waves? They naturally create smooth, repeating cycles!
    # The +np.pi shifts the wave to be opposite of CO2
    o2_daily = 0.15 * np.sin(2 * np.pi * t / daily_period + np.pi)
    o2_weekly = 0.08 * np.sin(2 * np.pi * t / weekly_period)

    # Add smooth random noise
    o2_noise = smooth_noise(n_points, -0.05, 0.05, window=40)

    # Combine all components and clip to realistic range
    # np.clip ensures values stay between 19.5% and 21.5%
    o2 = np.clip(o2_baseline + o2_trend + o2_daily + o2_weekly + o2_noise, 19.5, 21.5)

    # ========================================================================
    # CO2 (CARBON DIOXIDE) - normally around 400 ppm
    # ========================================================================
    # CO2 increases when people breathe and during activities
    # Has strong daily patterns (higher during active hours)

    co2_baseline = 400                    # Normal atmospheric CO2
    co2_trend = 0.015 * t                 # Gradual upward trend
    co2_daily = 40 * np.sin(2 * np.pi * t / daily_period)    # Daily cycle
    co2_weekly = 20 * np.sin(2 * np.pi * t / weekly_period)  # Weekly cycle
    co2_noise = smooth_noise(n_points, -10, 10, window=30)

    co2 = np.clip(co2_baseline + co2_trend + co2_daily + co2_weekly + co2_noise, 350, 800)

    # ========================================================================
    # VOCs (VOLATILE ORGANIC COMPOUNDS) - chemical pollutants
    # ========================================================================
    # VOCs come from cleaning products, cooking, materials off-gassing
    # Pattern is slightly shifted from CO2 (different sources)

    voc_baseline = 50
    voc_trend = 0.003 * t
    # The -np.pi/4 creates a phase shift (pattern peaks at different times)
    voc_daily = 15 * np.sin(2 * np.pi * t / daily_period - np.pi / 4)
    voc_weekly = 8 * np.sin(2 * np.pi * t / weekly_period)
    voc_noise = smooth_noise(n_points, -5, 5, window=35)

    voc = np.clip(voc_baseline + voc_trend + voc_daily + voc_weekly + voc_noise, 30, 200)

    # ========================================================================
    # PM (PARTICULATE MATTER) - tiny particles in the air
    # ========================================================================
    # Particulates come from dust, cooking, movement
    # The +np.pi/2 shifts the daily pattern by 6 hours

    pm_baseline = 15
    pm_trend = 0.002 * t
    pm_daily = 5 * np.sin(2 * np.pi * t / daily_period + np.pi / 2)
    pm_weekly = 3 * np.sin(2 * np.pi * t / weekly_period)
    pm_noise = smooth_noise(n_points, -2, 2, window=25)

    pm = np.clip(pm_baseline + pm_trend + pm_daily + pm_weekly + pm_noise, 5, 80)

    # ========================================================================
    # Create DataFrame
    # ========================================================================
    # Prophet requires a 'ds' column with datetime values
    ds = pd.date_range(start="2025-01-01", periods=n_points, freq="min")

    df = pd.DataFrame({
        "ds": ds,                      # Datetime (for Prophet)
        "Time_min": t,                 # Minute number (for plotting)
        "O2_percent": o2,
        "CO2_ppm": co2,
        "VOCs_ppb": voc,
        "Particulates_ugm3": pm
    })

    return df


# ============================================================================
# DOWNSAMPLING FUNCTION (FOR SARIMA SPEED OPTIMIZATION)
# ============================================================================

def downsample_series(series, factor):
    arr = np.array(series)
    # Calculate how many complete groups we can make
    n_groups = len(arr) // factor
    # Trim to exact multiple of factor
    trimmed = arr[:n_groups * factor]
    # Reshape into groups and take mean of each group
    reshaped = trimmed.reshape(n_groups, factor)
    downsampled = reshaped.mean(axis=1)
    return downsampled


def upsample_predictions(predictions, factor, target_length):
    # Create x-coordinates for original predictions
    x_original = np.arange(len(predictions)) * factor

    # Create x-coordinates for upsampled predictions
    x_upsampled = np.arange(target_length)

    # Use numpy interpolation to fill in the gaps
    upsampled = np.interp(x_upsampled, x_original, predictions)

    return upsampled


# ============================================================================
# MODEL TRAINING AND PREDICTION FUNCTIONS
# ============================================================================

def train_prophet(train_df, test_df):
    # Create and configure the Prophet model
    model = Prophet(
        growth="linear",              # Use linear trend (not logistic)
        yearly_seasonality=False,     # Our data is only ~3 days, no yearly pattern
        weekly_seasonality=True,      # Enable weekly pattern detection
        daily_seasonality=True,       # Enable daily pattern detection
        changepoint_prior_scale=PROPHET_CHANGEPOINT_PRIOR,
        seasonality_prior_scale=PROPHET_SEASONALITY_PRIOR,
        n_changepoints=PROPHET_N_CHANGEPOINTS,
        interval_width=0.8,           # 80% confidence interval
    )

    # Train the model on historical data
    # Prophet learns the patterns from this data
    model.fit(train_df)

    # Create predictions for the test period
    # We give Prophet the future dates, and it predicts the values
    future = test_df[["ds"]].copy()
    forecast = model.predict(future)

    # Return just the predicted values (yhat = "y hat" = predicted y)
    return forecast["yhat"].values


def train_arima(train_series, test_len):
    try:
        # Create and fit the ARIMA model
        model = ARIMA(train_series, order=ARIMA_ORDER)
        fitted_model = model.fit()

        # Forecast future values
        forecast = fitted_model.forecast(steps=test_len)
        predictions = forecast.values if hasattr(forecast, 'values') else np.array(forecast)

    except Exception as e:
        print(f"ARIMA fitting failed: {e}")
        # Fallback: if model fails, just use the last known value
        # This is called "naive forecasting" or "persistence"
        predictions = np.full(test_len, train_series.iloc[-1])

    return predictions


def train_sarima(train_series, test_len, original_test_len):
    try:
        # Create and fit the SARIMA model (called SARIMAX in statsmodels)
        # The X in SARIMAX stands for "eXogenous variables" (external factors)
        # We don't use external variables here, so it's just SARIMA
        model = SARIMAX(
            train_series,
            order=SARIMA_ORDER,
            seasonal_order=SARIMA_SEASONAL_ORDER,
            enforce_stationarity=False,  # Don't force data to be stationary
            enforce_invertibility=False   # More flexible model fitting
        )

        # Fit the model (this finds the best parameters)
        # disp=False hides the optimization output
        # maxiter=100 limits iterations to prevent hanging
        fitted_model = model.fit(disp=False, maxiter=100)

        # Make predictions on downsampled scale
        forecast = fitted_model.forecast(steps=test_len)
        predictions_downsampled = forecast.values if hasattr(forecast, 'values') else np.array(forecast)

        # Upsample predictions back to original resolution
        predictions = upsample_predictions(
            predictions_downsampled,
            SARIMA_DOWNSAMPLE_FACTOR,
            original_test_len
        )

    except Exception as e:
        print(f"SARIMA fitting failed: {e}")
        # Fallback: use last known value
        last_val = train_series.iloc[-1] if len(train_series) > 0 else 0
        predictions = np.full(original_test_len, last_val)

    return predictions


# ============================================================================
# MAIN PREDICTION FUNCTION
# ============================================================================

def run_prediction(seed, model_choice):
    # ========================================================================
    # STEP 1: Generate Data
    # ========================================================================
    df = generate_mission_data(seed=int(seed))
    n = len(df)
    train_size = int(n * TRAIN_FRAC)  # 70% for training
    test_size = n - train_size         # 30% for testing

    # ========================================================================
    # STEP 2: Define Parameters to Predict
    # ========================================================================
    params = [
        ("O2_percent", "O2 (%)"),
        ("CO2_ppm", "CO2 (ppm)"),
        ("VOCs_ppb", "VOCs (ppb)"),
        ("Particulates_ugm3", "PM (μg/m³)"),
    ]

    # Determine which models to run
    if model_choice == "All":
        models_to_run = ["Prophet", "ARIMA", "SARIMA"]
    else:
        models_to_run = [model_choice]

    # ========================================================================
    # STEP 3: Prepare Downsampled Data for SARIMA (if needed)
    # ========================================================================
    # Only do this work if SARIMA is being used
    if "SARIMA" in models_to_run:
        # Calculate downsampled sizes
        train_size_down = train_size // SARIMA_DOWNSAMPLE_FACTOR
        test_size_down = test_size // SARIMA_DOWNSAMPLE_FACTOR

    # ========================================================================
    # STEP 4: Train and Predict for Each Parameter and Model
    # ========================================================================
    all_results = {}  # Store results for plotting
    metrics_data = []  # Store accuracy metrics

    for col, label in params:
        # Prepare training and test data (full resolution)
        train_series = df[col].iloc[:train_size]
        test_actual = df[col].iloc[train_size:].values

        # For Prophet, we need DataFrames with 'ds' and 'y' columns
        train_df_prophet = df.iloc[:train_size][["ds"]].copy()
        train_df_prophet["y"] = train_series.values
        test_df_prophet = df.iloc[train_size:][["ds"]].copy()

        all_results[col] = {
            "train_y": train_series.values,
            "test_actual": test_actual,
            "label": label,
            "predictions": {}
        }

        # Run each selected model
        for model_name in models_to_run:
            if model_name == "Prophet":
                # Prophet uses full resolution data
                pred = train_prophet(train_df_prophet, test_df_prophet)

            elif model_name == "ARIMA":
                # ARIMA uses full resolution data
                pred = train_arima(train_series, test_size)

            elif model_name == "SARIMA":
                # SARIMA uses DOWNSAMPLED data for speed!
                train_series_down = downsample_series(train_series, SARIMA_DOWNSAMPLE_FACTOR)
                pred = train_sarima(
                    pd.Series(train_series_down),
                    test_size_down,
                    test_size  # Original test size for upsampling
                )

            all_results[col]["predictions"][model_name] = pred

            # Calculate error metrics
            mae = np.mean(np.abs(test_actual - pred))
            rmse = np.sqrt(np.mean((test_actual - pred) ** 2))

            metrics_data.append({
                "Parameter": label,
                "Model": model_name,
                "MAE": f"{mae:.4f}",
                "RMSE": f"{rmse:.4f}"
            })

    # ========================================================================
    # STEP 5: Create Visualizations
    # ========================================================================
    # Time arrays for x-axis
    train_time = df["Time_min"].iloc[:train_size].values
    test_time = df["Time_min"].iloc[train_size:].values

    # Color scheme for different models
    model_colors = {
        "Prophet": "orange",
        "ARIMA": "green",
        "SARIMA": "red"
    }

    # Create 2x2 subplot (one for each parameter)
    fig, axs = plt.subplots(2, 2, figsize=(15, 11))
    axs = axs.flatten()

    for idx, (col, label) in enumerate(params):
        ax = axs[idx]
        r = all_results[col]

        # Plot training data (same for all models)
        ax.plot(train_time, r["train_y"], color="blue", alpha=0.5,
                label="Training Data", linewidth=1)

        # Plot actual test data
        ax.plot(test_time, r["test_actual"], color="blue",
                label="Actual", linewidth=2)

        # Plot predictions for each model
        for model_name, pred in r["predictions"].items():
            ax.plot(test_time, pred, color=model_colors[model_name],
                    label=f"{model_name} Predicted", linewidth=1.5,
                    linestyle="--" if len(models_to_run) > 1 else "-")

        # Add vertical line at train/test split
        ax.axvline(x=train_size, color="gray", linestyle=":", alpha=0.7,
                   label="Train/Test Split")

        # Formatting
        ax.set_xlabel("Time (minutes)")
        ax.set_ylabel(label)
        ax.set_title(f"{label}: Actual vs Predicted")
        ax.legend(loc="upper left", fontsize=8)
        ax.grid(True, alpha=0.3)

    # Add overall title
    fig.suptitle(f"Time Series Forecasting Comparison\n"
                 f"Model(s): {', '.join(models_to_run)} | "
                 f"Train: {train_size} pts | Test: {test_size} pts",
                 fontsize=12, fontweight='bold')

    plt.tight_layout()

    # Create metrics DataFrame
    metrics_df = pd.DataFrame(metrics_data)

    return metrics_df, str(seed), fig


# ============================================================================
# GRADIO USER INTERFACE
# ============================================================================
# Gradio is a library for creating simple web interfaces for ML models
# It automatically creates a web page where users can interact with our code

with gr.Blocks() as demo:
    # ------------------------------------------------------------------------
    # Header and Instructions
    # ------------------------------------------------------------------------
    gr.Markdown("""
    # Time Series Forecasting: Prophet vs ARIMA vs SARIMA

    ## What This Tool Does
    This tool generates synthetic environmental sensor data and uses different
    forecasting models to predict future values. Compare how well each model
    captures trends and seasonal patterns!

    ## The Models
    | Model | Best For | Handles Seasonality? | Speed |
    |-------|----------|---------------------|-------|
    | **Prophet** | Business forecasting, multiple seasonalities | Yes (automatic) | Fast |
    | **ARIMA** | Data with trends, no clear seasonality | No | Fast |
    | **SARIMA** | Data with trends AND seasonality | Yes (manual config) | Medium* |

    *SARIMA uses downsampled data for speed optimization (see code comments for details)

    ## How to Use
    1. Choose a random seed (different seeds = different data)
    2. Select which model(s) to compare
    3. Click "Generate & Predict" and wait for results
    """)

    # ------------------------------------------------------------------------
    # Input Controls
    # ------------------------------------------------------------------------
    with gr.Row():
        seed_input = gr.Number(
            label="Random Seed",
            value=42,
            precision=0,
            info="Change this to generate different data patterns"
        )
        model_dropdown = gr.Dropdown(
            choices=["Prophet", "ARIMA", "SARIMA", "All"],
            value="All",
            label="Model Selection",
            info="Choose which forecasting model(s) to use"
        )
        run_btn = gr.Button("Generate & Predict", variant="primary")

    # ------------------------------------------------------------------------
    # Output Displays
    # ------------------------------------------------------------------------
    seed_box = gr.Textbox(label="Seed Used", interactive=False)

    gr.Markdown("### Accuracy Metrics")
    gr.Markdown("""
    - **MAE** (Mean Absolute Error): Average prediction error. Lower = better.
    - **RMSE** (Root Mean Square Error): Similar to MAE but penalizes large errors more.
    """)
    metrics_out = gr.Dataframe(label="Model Performance")

    gr.Markdown("### Visualization")
    plot_out = gr.Plot(label="Forecasting Results")

    # ------------------------------------------------------------------------
    # Educational Footer
    # ------------------------------------------------------------------------
    gr.Markdown("""
    ---
    ## Understanding the Results

    **Blue line** = Actual data (what really happened)
    **Colored dashed lines** = Model predictions (what the model thinks will happen)

    A good model's predictions should closely follow the actual data in the test region
    (right side of the red dashed line).

    ### Tips for Interpretation
    - If predictions are flat while actual data has waves → Model missed seasonality
    - If predictions drift away from actual → Model has trouble with the trend
    - Lower MAE/RMSE = Better predictions

    ### Why is SARIMA using downsampled data?
    SARIMA can be very slow on large datasets (20-60 minutes for 4500 points!).
    To make it practical, we:
    1. Downsample minute data to hourly (75 points instead of 4500)
    2. Train SARIMA on the smaller dataset (~30 seconds)
    3. Upsample predictions back to minute resolution

    This trades a small amount of accuracy for much faster results.
    """)

    # ------------------------------------------------------------------------
    # Connect Button to Function
    # ------------------------------------------------------------------------
    run_btn.click(
        fn=run_prediction,
        inputs=[seed_input, model_dropdown],
        outputs=[metrics_out, seed_box, plot_out]
    )

# Launch the web interface
# share=True creates a public link (useful for sharing with others)
# ssr_mode=False for better compatibility
demo.launch(share=True, ssr_mode=False)