cloud450 commited on
Commit
ed2fa57
·
verified ·
1 Parent(s): 32ab3d8

Upload 4 files

Browse files
Files changed (4) hide show
  1. ETTh1.csv +0 -0
  2. README.md +62 -6
  3. app.py +533 -0
  4. requirements.txt +7 -0
ETTh1.csv ADDED
The diff for this file is too large to render. See raw diff
 
README.md CHANGED
@@ -1,12 +1,68 @@
1
  ---
2
- title: Tsa Project
3
- emoji: 😻
4
- colorFrom: pink
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.12.0
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Transformer Oil Temperature Forecaster
3
+ emoji:
4
+ colorFrom: red
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: "4.0.0"
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Transformer Oil Temperature Forecaster
13
+
14
+ > **ARIMAX · Anomaly Detection · Time Series Analysis**
15
+
16
+ Upload ETT-style transformer CSV data and get:
17
+
18
+ | Feature | Details |
19
+ |---|---|
20
+ | **Model** | ARIMAX — auto-selects best `(p, d, q)` via AIC grid search |
21
+ | **Endog** | `OT` — oil temperature |
22
+ | **Exog** | `HUFL, HULL, MUFL, MULL, LUFL, LULL` — load features |
23
+ | **Stationarity** | ADF test; auto-applies 1st differencing if needed |
24
+ | **Anomaly Detection** | Residual-based, threshold = mean ± 2.5σ |
25
+ | **Evaluation** | MAE + RMSE on 20% hold-out set |
26
+
27
+ ---
28
+
29
+ ## 📂 Expected CSV Format
30
+
31
+ ```
32
+ date,HUFL,HULL,MUFL,MULL,LUFL,LULL,OT
33
+ 2016-07-01 00:00:00,5.827,2.009,1.599,0.462,4.203,1.340,30.531
34
+ ...
35
+ ```
36
+
37
+ The ETT (Electricity Transformer Temperature) dataset works out of the box.
38
+ Download it from: https://github.com/zhouhaoyi/ETDataset
39
+
40
+ ---
41
+
42
+ ## 🚀 Running Locally
43
+
44
+ ```bash
45
+ pip install -r requirements.txt
46
+ python app.py
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 📐 Architecture
52
+
53
+ ```
54
+ CSV Upload
55
+
56
+
57
+ load_data() ← parse datetime index, ffill missing
58
+
59
+
60
+ check_stationarity() ← ADF test → d value
61
+
62
+
63
+ train_arimax() ← grid search (p,q) on 80% train split
64
+
65
+ ├──► forecast() ← out-of-sample N steps
66
+
67
+ └──► detect_anomalies() ← residual threshold flagging
68
+ ```
app.py ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Transformer Oil Temperature Forecasting & Anomaly Detection
3
+ Using ARIMAX model with Gradio UI for Hugging Face Spaces
4
+ """
5
+
6
+ import warnings
7
+ warnings.filterwarnings("ignore")
8
+
9
+ import pandas as pd
10
+ import numpy as np
11
+ import matplotlib
12
+ matplotlib.use("Agg") # Non-interactive backend for server environments
13
+ import matplotlib.pyplot as plt
14
+ import matplotlib.gridspec as gridspec
15
+ import seaborn as sns
16
+ import io
17
+ import gradio as gr
18
+
19
+ from statsmodels.tsa.stattools import adfuller
20
+ from statsmodels.tsa.arima.model import ARIMA
21
+ from sklearn.metrics import mean_absolute_error, mean_squared_error
22
+
23
+ # ─────────────────────────────────────────────
24
+ # Aesthetic config
25
+ # ─────────────────────────────────────────────
26
+ STYLE = {
27
+ "bg": "#0d1117",
28
+ "panel": "#161b22",
29
+ "accent": "#f78166",
30
+ "accent2": "#58a6ff",
31
+ "accent3": "#3fb950",
32
+ "warn": "#d29922",
33
+ "text": "#e6edf3",
34
+ "subtext": "#8b949e",
35
+ "grid": "#21262d",
36
+ }
37
+
38
+ def _apply_style(fig, axes_list):
39
+ """Apply dark industrial style to all axes."""
40
+ fig.patch.set_facecolor(STYLE["bg"])
41
+ for ax in axes_list:
42
+ ax.set_facecolor(STYLE["panel"])
43
+ ax.tick_params(colors=STYLE["subtext"], labelsize=8)
44
+ ax.xaxis.label.set_color(STYLE["subtext"])
45
+ ax.yaxis.label.set_color(STYLE["subtext"])
46
+ ax.title.set_color(STYLE["text"])
47
+ for spine in ax.spines.values():
48
+ spine.set_edgecolor(STYLE["grid"])
49
+ ax.grid(color=STYLE["grid"], linewidth=0.5, linestyle="--", alpha=0.7)
50
+
51
+
52
+ # ─────────────────────────────────────────────
53
+ # 1. DATA LOADING
54
+ # ─────────────────────────────────────────────
55
+ def load_data(file_obj):
56
+ """
57
+ Load CSV, parse 'date' as datetime index, fill missing values.
58
+ Returns cleaned DataFrame.
59
+ """
60
+ df = pd.read_csv(file_obj.name if hasattr(file_obj, "name") else file_obj)
61
+
62
+ # Parse date column
63
+ date_col = [c for c in df.columns if "date" in c.lower()]
64
+ if not date_col:
65
+ raise ValueError("No 'date' column found in CSV.")
66
+ df[date_col[0]] = pd.to_datetime(df[date_col[0]])
67
+ df = df.set_index(date_col[0]).sort_index()
68
+
69
+ # Forward-fill then back-fill missing values
70
+ df = df.ffill().bfill()
71
+
72
+ return df
73
+
74
+
75
+ # ─────────────────────────────────────────────
76
+ # 2. STATIONARITY CHECK
77
+ # ─────────────────────────────────────────────
78
+ def check_stationarity(series):
79
+ """
80
+ Augmented Dickey-Fuller test.
81
+ Returns (result_string, differenced_series, d_value).
82
+ d=0 → already stationary; d=1 → once-differenced.
83
+ """
84
+ result = adfuller(series.dropna(), autolag="AIC")
85
+ adf_stat, p_value = result[0], result[1]
86
+
87
+ lines = [
88
+ f"ADF Statistic : {adf_stat:.4f}",
89
+ f"p-value : {p_value:.4f}",
90
+ f"Critical vals : { {k: f'{v:.3f}' for k, v in result[4].items()} }",
91
+ ]
92
+
93
+ if p_value <= 0.05:
94
+ lines.append("✅ Series is STATIONARY (p ≤ 0.05) — no differencing needed.")
95
+ return "\n".join(lines), series, 0
96
+ else:
97
+ lines.append("⚠️ Series is NON-STATIONARY (p > 0.05) — applying 1st differencing.")
98
+ return "\n".join(lines), series.diff().dropna(), 1
99
+
100
+
101
+ # ─────────────────────────────────────────────
102
+ # 3. ARIMAX TRAINING
103
+ # ─────────────────────────────────────────────
104
+ def train_arimax(endog, exog, d=0):
105
+ """
106
+ Fit ARIMAX(p, d, q) model.
107
+ Auto-selects best (p, q) by AIC over a small grid search.
108
+ Returns fitted model result.
109
+ """
110
+ best_aic = np.inf
111
+ best_order = (1, d, 1)
112
+ best_result = None
113
+
114
+ # Grid search over small p/q space to keep it fast
115
+ for p in range(0, 3):
116
+ for q in range(0, 3):
117
+ try:
118
+ model = ARIMA(endog, exog=exog, order=(p, d, q),
119
+ enforce_stationarity=False,
120
+ enforce_invertibility=False)
121
+ res = model.fit(method_kwargs={"warn_convergence": False})
122
+ if res.aic < best_aic:
123
+ best_aic = res.aic
124
+ best_order = (p, d, q)
125
+ best_result = res
126
+ except Exception:
127
+ continue
128
+
129
+ if best_result is None:
130
+ # Fallback to simple ARIMA(1,d,1)
131
+ model = ARIMA(endog, exog=exog, order=(1, d, 1),
132
+ enforce_stationarity=False, enforce_invertibility=False)
133
+ best_result = model.fit()
134
+
135
+ return best_result, best_order
136
+
137
+
138
+ # ─────────────────────────────────────────────
139
+ # 4. FORECASTING
140
+ # ─────────────────────────────────────────────
141
+ def forecast(model_result, steps, exog_future):
142
+ """
143
+ Produce out-of-sample forecast for `steps` periods.
144
+ exog_future: DataFrame with same columns as training exog, length = steps.
145
+ Returns forecast mean Series.
146
+ """
147
+ pred = model_result.get_forecast(steps=steps, exog=exog_future)
148
+ fc_mean = pred.predicted_mean
149
+ fc_ci = pred.conf_int()
150
+ return fc_mean, fc_ci
151
+
152
+
153
+ # ─────────────────────────────────────────────
154
+ # 5. ANOMALY DETECTION
155
+ # ─────────────────────────────────────────────
156
+ def detect_anomalies(actual, fitted, k=2.5):
157
+ """
158
+ Residual-based anomaly detection.
159
+ Flag points where |residual| > mean + k*std.
160
+ Returns boolean mask of anomalies.
161
+ """
162
+ residuals = actual - fitted
163
+ threshold = residuals.mean() + k * residuals.std()
164
+ anomalies = residuals.abs() > threshold
165
+ return residuals, anomalies
166
+
167
+
168
+ # ─────────────────────────────────────────────
169
+ # PLOT HELPERS
170
+ # ─────────────────────────────────────────────
171
+ def _fig_to_pil(fig):
172
+ """Convert matplotlib figure to PIL Image bytes for Gradio."""
173
+ buf = io.BytesIO()
174
+ fig.savefig(buf, format="png", dpi=130, bbox_inches="tight",
175
+ facecolor=fig.get_facecolor())
176
+ buf.seek(0)
177
+ return buf
178
+
179
+
180
+ def plot_overview(df):
181
+ """OT time series + correlation heatmap."""
182
+ feat_cols = [c for c in df.columns if c != "OT"]
183
+
184
+ fig = plt.figure(figsize=(14, 8), facecolor=STYLE["bg"])
185
+ gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.45, wspace=0.35)
186
+
187
+ # --- OT over time ---
188
+ ax0 = fig.add_subplot(gs[0, :])
189
+ ax0.plot(df.index, df["OT"], color=STYLE["accent2"], linewidth=0.8, alpha=0.9)
190
+ ax0.set_title("Oil Temperature (OT) — Full Series", fontsize=11, fontweight="bold")
191
+ ax0.set_ylabel("OT")
192
+
193
+ # --- Feature lines ---
194
+ ax1 = fig.add_subplot(gs[1, 0])
195
+ palette = [STYLE["accent"], STYLE["accent2"], STYLE["accent3"],
196
+ STYLE["warn"], "#c9d1d9", "#a371f7"]
197
+ for i, col in enumerate(feat_cols):
198
+ ax1.plot(df.index, df[col], linewidth=0.6, alpha=0.7,
199
+ color=palette[i % len(palette)], label=col)
200
+ ax1.set_title("All Load Features", fontsize=10)
201
+ ax1.legend(fontsize=6, ncol=2, facecolor=STYLE["panel"],
202
+ edgecolor=STYLE["grid"], labelcolor=STYLE["text"])
203
+
204
+ # --- Correlation heatmap ---
205
+ ax2 = fig.add_subplot(gs[1, 1])
206
+ corr = df.corr()
207
+ mask = np.triu(np.ones_like(corr, dtype=bool))
208
+ cmap = sns.diverging_palette(220, 10, as_cmap=True)
209
+ sns.heatmap(corr, mask=mask, cmap=cmap, ax=ax2, annot=True,
210
+ fmt=".2f", annot_kws={"size": 7},
211
+ linewidths=0.4, linecolor=STYLE["grid"],
212
+ cbar_kws={"shrink": 0.7})
213
+ ax2.set_title("Correlation Matrix", fontsize=10)
214
+ ax2.tick_params(axis="x", rotation=45, labelsize=7)
215
+ ax2.tick_params(axis="y", rotation=0, labelsize=7)
216
+
217
+ _apply_style(fig, [ax0, ax1])
218
+ plt.tight_layout()
219
+ return _fig_to_pil(fig)
220
+
221
+
222
+ def plot_forecast(df, fc_mean, fc_ci, order, mae, rmse):
223
+ """In-sample fit + out-of-sample forecast with confidence interval."""
224
+ fig, ax = plt.subplots(figsize=(14, 5), facecolor=STYLE["bg"])
225
+
226
+ # Training portion
227
+ ax.plot(df.index, df["OT"], color=STYLE["subtext"],
228
+ linewidth=0.7, alpha=0.6, label="Actual OT")
229
+
230
+ # Forecast
231
+ ax.plot(fc_mean.index, fc_mean.values,
232
+ color=STYLE["accent"], linewidth=1.8, label="Forecast", zorder=5)
233
+ ax.fill_between(fc_ci.index,
234
+ fc_ci.iloc[:, 0], fc_ci.iloc[:, 1],
235
+ color=STYLE["accent"], alpha=0.15, label="95% CI")
236
+
237
+ # Dividing line
238
+ split_t = df.index[-1]
239
+ ax.axvline(split_t, color=STYLE["warn"], linewidth=1.2,
240
+ linestyle="--", alpha=0.8, label="Forecast start")
241
+
242
+ ax.set_title(
243
+ f"ARIMAX{order} Forecast | MAE={mae:.3f} RMSE={rmse:.3f}",
244
+ fontsize=11, fontweight="bold"
245
+ )
246
+ ax.set_ylabel("OT")
247
+ ax.legend(fontsize=8, facecolor=STYLE["panel"],
248
+ edgecolor=STYLE["grid"], labelcolor=STYLE["text"])
249
+
250
+ _apply_style(fig, [ax])
251
+ plt.tight_layout()
252
+ return _fig_to_pil(fig)
253
+
254
+
255
+ def plot_anomalies(df_ot, fitted, residuals, anomalies):
256
+ """Actual vs fitted + residual anomaly plot."""
257
+ fig, axes = plt.subplots(2, 1, figsize=(14, 8),
258
+ facecolor=STYLE["bg"], sharex=True)
259
+
260
+ # Top: actual vs fitted
261
+ axes[0].plot(df_ot.index, df_ot.values,
262
+ color=STYLE["accent2"], linewidth=0.8, alpha=0.8, label="Actual")
263
+ axes[0].plot(fitted.index, fitted.values,
264
+ color=STYLE["accent3"], linewidth=0.8, alpha=0.8, label="Fitted")
265
+ axes[0].scatter(df_ot.index[anomalies], df_ot.values[anomalies],
266
+ color=STYLE["accent"], s=18, zorder=6,
267
+ label=f"Anomalies ({anomalies.sum()})", marker="^")
268
+ axes[0].set_title("Actual vs Fitted — Anomalies Highlighted", fontsize=11, fontweight="bold")
269
+ axes[0].set_ylabel("OT")
270
+ axes[0].legend(fontsize=8, facecolor=STYLE["panel"],
271
+ edgecolor=STYLE["grid"], labelcolor=STYLE["text"])
272
+
273
+ # Bottom: residuals
274
+ axes[1].bar(residuals.index, residuals.values,
275
+ color=STYLE["accent2"], alpha=0.5, width=0.8)
276
+ axes[1].scatter(residuals.index[anomalies], residuals.values[anomalies],
277
+ color=STYLE["accent"], s=18, zorder=6, marker="^")
278
+ thr_val = residuals.mean() + 2.5 * residuals.std()
279
+ axes[1].axhline( thr_val, color=STYLE["accent"], linewidth=1,
280
+ linestyle="--", alpha=0.8, label=f"+ threshold ({thr_val:.2f})")
281
+ axes[1].axhline(-thr_val, color=STYLE["accent"], linewidth=1,
282
+ linestyle="--", alpha=0.8, label=f"- threshold ({-thr_val:.2f})")
283
+ axes[1].set_title("Residuals with Anomaly Thresholds", fontsize=10)
284
+ axes[1].set_ylabel("Residual")
285
+ axes[1].legend(fontsize=7, facecolor=STYLE["panel"],
286
+ edgecolor=STYLE["grid"], labelcolor=STYLE["text"])
287
+
288
+ _apply_style(fig, axes)
289
+ plt.tight_layout()
290
+ return _fig_to_pil(fig)
291
+
292
+
293
+ # ─────────────────────────────────────────────
294
+ # MAIN PIPELINE (called by Gradio)
295
+ # ─────────────────────────────────────────────
296
+ EXOG_COLS = ["HUFL", "HULL", "MUFL", "MULL", "LUFL", "LULL"]
297
+
298
+ def run_pipeline(file_obj, horizon: int):
299
+ """
300
+ Full pipeline: load → stationarity → ARIMAX → forecast → anomalies.
301
+ Returns (overview_img, forecast_img, anomaly_img, adf_text).
302
+ """
303
+ if file_obj is None:
304
+ return None, None, None, "❌ Please upload a CSV file."
305
+
306
+ try:
307
+ horizon = int(horizon)
308
+ if horizon < 1:
309
+ horizon = 1
310
+
311
+ # 1. Load data
312
+ df = load_data(file_obj)
313
+
314
+ # Validate required columns
315
+ missing = [c for c in EXOG_COLS + ["OT"] if c not in df.columns]
316
+ if missing:
317
+ return None, None, None, f"❌ Missing columns: {missing}"
318
+
319
+ # Use at most 2000 rows for speed on free Spaces
320
+ if len(df) > 2000:
321
+ df = df.iloc[-2000:]
322
+
323
+ # 2. Overview plot
324
+ ov_img = plot_overview(df)
325
+
326
+ # 3. Stationarity
327
+ adf_text, _, d = check_stationarity(df["OT"])
328
+
329
+ # 4. Train ARIMAX (use 80% for fit, 20% held for evaluation)
330
+ split = int(len(df) * 0.8)
331
+ train_df = df.iloc[:split]
332
+ test_df = df.iloc[split:]
333
+
334
+ endog_train = train_df["OT"]
335
+ exog_train = train_df[EXOG_COLS]
336
+
337
+ model_result, best_order = train_arimax(endog_train, exog_train, d=d)
338
+
339
+ # In-sample fitted values
340
+ fitted = model_result.fittedvalues
341
+
342
+ # Evaluate on test set (if we have enough rows)
343
+ if len(test_df) > 0:
344
+ exog_test = test_df[EXOG_COLS]
345
+ fc_test, _ = forecast(model_result, len(test_df), exog_test)
346
+ mae = mean_absolute_error(test_df["OT"], fc_test)
347
+ rmse = np.sqrt(mean_squared_error(test_df["OT"], fc_test))
348
+ else:
349
+ mae, rmse = 0.0, 0.0
350
+
351
+ # 5. Out-of-sample forecast
352
+ # Repeat last known exog row for simplicity (flat extrapolation)
353
+ last_exog = df[EXOG_COLS].iloc[[-1]]
354
+ exog_future = pd.concat([last_exog] * horizon, ignore_index=True)
355
+ # Build future datetime index
356
+ freq_guess = pd.infer_freq(df.index) or "h"
357
+ future_idx = pd.date_range(df.index[-1], periods=horizon + 1,
358
+ freq=freq_guess)[1:]
359
+ exog_future.index = future_idx
360
+
361
+ fc_mean, fc_ci = forecast(model_result, horizon, exog_future)
362
+ fc_mean.index = future_idx
363
+ fc_ci.index = future_idx
364
+
365
+ fc_img = plot_forecast(df, fc_mean, fc_ci, best_order, mae, rmse)
366
+
367
+ # 6. Anomaly detection (on training in-sample residuals)
368
+ residuals, anomaly_mask = detect_anomalies(endog_train, fitted)
369
+ an_img = plot_anomalies(endog_train, fitted, residuals, anomaly_mask)
370
+
371
+ # Append metrics + order info to ADF text
372
+ adf_text += (
373
+ f"\n\n📐 Best ARIMAX order : {best_order}"
374
+ f"\n📊 Test MAE : {mae:.4f}"
375
+ f"\n📊 Test RMSE : {rmse:.4f}"
376
+ f"\n🔴 Anomalies found : {anomaly_mask.sum()} / {len(anomaly_mask)}"
377
+ )
378
+
379
+ return ov_img, fc_img, an_img, adf_text
380
+
381
+ except Exception as e:
382
+ import traceback
383
+ tb = traceback.format_exc()
384
+ return None, None, None, f"❌ Error:\n{e}\n\n{tb}"
385
+
386
+
387
+ # ─────────────────────────────────────────────
388
+ # GRADIO UI
389
+ # ─────────────────────────────────────────────
390
+ CSS = """
391
+ /* ── Global reset ── */
392
+ * { box-sizing: border-box; }
393
+ body, .gradio-container {
394
+ background: #0d1117 !important;
395
+ font-family: 'JetBrains Mono', 'Fira Code', monospace !important;
396
+ color: #e6edf3 !important;
397
+ }
398
+
399
+ /* ── Header ── */
400
+ .app-header {
401
+ text-align: center;
402
+ padding: 28px 0 8px;
403
+ border-bottom: 1px solid #21262d;
404
+ margin-bottom: 20px;
405
+ }
406
+ .app-header h1 {
407
+ font-size: 1.7rem;
408
+ font-weight: 700;
409
+ color: #f78166;
410
+ letter-spacing: -0.5px;
411
+ margin: 0;
412
+ }
413
+ .app-header p {
414
+ font-size: 0.82rem;
415
+ color: #8b949e;
416
+ margin-top: 6px;
417
+ }
418
+
419
+ /* ── Panels ── */
420
+ .gr-panel, .gr-box, .gr-form {
421
+ background: #161b22 !important;
422
+ border: 1px solid #21262d !important;
423
+ border-radius: 8px !important;
424
+ }
425
+
426
+ /* ── Buttons ── */
427
+ button.primary {
428
+ background: #f78166 !important;
429
+ border: none !important;
430
+ color: #0d1117 !important;
431
+ font-weight: 700 !important;
432
+ letter-spacing: 0.5px;
433
+ border-radius: 6px !important;
434
+ }
435
+ button.primary:hover {
436
+ background: #ff9580 !important;
437
+ }
438
+
439
+ /* ── Labels ── */
440
+ label, .gr-label {
441
+ color: #8b949e !important;
442
+ font-size: 0.78rem !important;
443
+ text-transform: uppercase;
444
+ letter-spacing: 0.8px;
445
+ }
446
+
447
+ /* ── Textbox (ADF output) ── */
448
+ textarea, .gr-textbox textarea {
449
+ background: #0d1117 !important;
450
+ color: #3fb950 !important;
451
+ border: 1px solid #21262d !important;
452
+ font-family: 'JetBrains Mono', monospace !important;
453
+ font-size: 0.8rem !important;
454
+ }
455
+
456
+ /* ── Tab strip ── */
457
+ .tab-nav button {
458
+ color: #8b949e !important;
459
+ border-bottom: 2px solid transparent !important;
460
+ }
461
+ .tab-nav button.selected {
462
+ color: #58a6ff !important;
463
+ border-bottom-color: #58a6ff !important;
464
+ }
465
+ """
466
+
467
+ with gr.Blocks(css=CSS, title="⚡ Transformer OT Forecaster") as demo:
468
+
469
+ gr.HTML("""
470
+ <div class="app-header">
471
+ <h1>⚡ Transformer Oil Temperature Forecaster</h1>
472
+ <p>ARIMAX · Anomaly Detection · Time Series Analysis — Upload ETT-style CSV data to begin</p>
473
+ </div>
474
+ """)
475
+
476
+ with gr.Row():
477
+ # ── Left column: controls ──
478
+ with gr.Column(scale=1, min_width=260):
479
+ gr.Markdown("### 📂 Data Input")
480
+ file_input = gr.File(
481
+ label="Upload CSV (date, HUFL, HULL, MUFL, MULL, LUFL, LULL, OT)",
482
+ file_types=[".csv"]
483
+ )
484
+ horizon_input = gr.Number(
485
+ label="Forecast Horizon (steps)",
486
+ value=24, minimum=1, maximum=500, step=1,
487
+ precision=0
488
+ )
489
+ run_btn = gr.Button("▶ Run Analysis", variant="primary")
490
+
491
+ gr.Markdown("""
492
+ ---
493
+ **Model:** ARIMAX (auto p,d,q)
494
+ **Endog:** OT (oil temperature)
495
+ **Exog:** HUFL, HULL, MUFL, MULL, LUFL, LULL
496
+ **Anomaly:** Residual ± 2.5σ threshold
497
+ **Eval:** MAE + RMSE on 20% hold-out
498
+ """)
499
+
500
+ # ── Right column: outputs ──
501
+ with gr.Column(scale=3):
502
+ with gr.Tabs():
503
+ with gr.TabItem("📈 Overview"):
504
+ overview_img = gr.Image(
505
+ label="Time Series Overview & Correlations",
506
+ type="filepath", show_download_button=True
507
+ )
508
+ with gr.TabItem("🔮 Forecast"):
509
+ forecast_img = gr.Image(
510
+ label="ARIMAX Forecast",
511
+ type="filepath", show_download_button=True
512
+ )
513
+ with gr.TabItem("🚨 Anomalies"):
514
+ anomaly_img = gr.Image(
515
+ label="Anomaly Detection",
516
+ type="filepath", show_download_button=True
517
+ )
518
+ with gr.TabItem("📋 ADF Report"):
519
+ adf_output = gr.Textbox(
520
+ label="Stationarity Test + Model Metrics",
521
+ lines=14, max_lines=20
522
+ )
523
+
524
+ # Wire up
525
+ run_btn.click(
526
+ fn=run_pipeline,
527
+ inputs=[file_input, horizon_input],
528
+ outputs=[overview_img, forecast_img, anomaly_img, adf_output],
529
+ )
530
+
531
+
532
+ if __name__ == "__main__":
533
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ pandas>=1.5.0
2
+ numpy>=1.23.0
3
+ matplotlib>=3.6.0
4
+ seaborn>=0.12.0
5
+ statsmodels>=0.13.0
6
+ scikit-learn>=1.1.0
7
+ gradio>=4.0.0