Zipeng365 commited on
Commit
065d79c
·
verified ·
1 Parent(s): 51fcd03

Add TSDecompose benchmark leaderboard Space

Browse files
Files changed (4) hide show
  1. .gitignore +4 -0
  2. README.md +15 -5
  3. app.py +412 -0
  4. requirements.txt +4 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .gradio/
README.md CHANGED
@@ -1,13 +1,23 @@
1
  ---
2
  title: TSDecompose Benchmark Leaderboard
3
- emoji: 🏢
4
  colorFrom: blue
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: TSDecompose Benchmark Leaderboard
3
+ emoji: 📈
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.49.1
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ datasets:
12
+ - Zipeng365/TSDecompose-Benchmark
13
  ---
14
 
15
+ # TSDecompose Benchmark Leaderboard
16
+
17
+ Interactive leaderboard for the TSDecompose benchmark release.
18
+
19
+ The Space reads leaderboard tables from the dataset repository:
20
+
21
+ `Zipeng365/TSDecompose-Benchmark`
22
+
23
+ The dataset repository remains the source of record. This Space is only the web presentation layer.
app.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from functools import lru_cache
3
+ from pathlib import Path
4
+
5
+ import gradio as gr
6
+ import pandas as pd
7
+ import plotly.express as px
8
+ from huggingface_hub import hf_hub_download
9
+
10
+
11
+ DATASET_REPO = os.getenv("TSDECOMPOSE_DATASET_REPO", "Zipeng365/TSDecompose-Benchmark")
12
+ DATA_VERSION = os.getenv("TSDECOMPOSE_DATA_VERSION", "v1.0.0")
13
+ LOCAL_DATA_DIR = os.getenv("TSDECOMPOSE_LOCAL_DATA_DIR")
14
+
15
+ SITE_FILES = {
16
+ "synthetic": f"site_data/{DATA_VERSION}/leaderboard_synthetic_full22_overall.csv",
17
+ "real_proxy": f"site_data/{DATA_VERSION}/leaderboard_real_proxy22_overall.csv",
18
+ "semisynth": f"site_data/{DATA_VERSION}/leaderboard_semisynth_transfer_by_method.csv",
19
+ "track_b": f"site_data/{DATA_VERSION}/leaderboard_post_rebuttal_real_physics_track_b.csv",
20
+ "track_c": f"site_data/{DATA_VERSION}/leaderboard_post_rebuttal_real_proxy_track_c.csv",
21
+ "methods": f"site_data/{DATA_VERSION}/methods.json",
22
+ "suites": f"site_data/{DATA_VERSION}/suites.json",
23
+ }
24
+
25
+ VIEW_LABELS = {
26
+ "synthetic": "Paper-Aligned Synthetic Full22",
27
+ "real_proxy": "Paper-Aligned Real Proxy 22",
28
+ "semisynth": "Semi-Synthetic Transfer",
29
+ "track_b": "Post-Rebuttal Track B: CO2 / Tides",
30
+ "track_c": "Post-Rebuttal Track C: Real Proxy Panel",
31
+ }
32
+
33
+ VIEW_NOTES = {
34
+ "synthetic": (
35
+ "Core synthetic benchmark: 6 scenarios x 50 draws = 300 generated "
36
+ "time-series instances. The expanded roster gives 22 method rows, "
37
+ "or 6,600 method-instance evaluations. Lower mean metric rank is better."
38
+ ),
39
+ "real_proxy": (
40
+ "Paper-aligned real-data companion track. These are proxy diagnostics, "
41
+ "not exact component-recovery labels. Lower overall mean rank is better."
42
+ ),
43
+ "semisynth": (
44
+ "Known synthetic mechanisms injected into real monthly backgrounds. "
45
+ "The displayed rank is recomputed from trend and seasonal recovery metrics."
46
+ ),
47
+ "track_b": (
48
+ "Post-rebuttal mechanism-aware checks on CO2 and tides. These use "
49
+ "mechanism-informed approximate structure and should be read as companion evidence."
50
+ ),
51
+ "track_c": (
52
+ "Post-rebuttal six-dataset proxy/stability panel. These rows measure "
53
+ "plausibility and robustness where exact real-world components are unavailable."
54
+ ),
55
+ }
56
+
57
+ HIGHER_IS_BETTER = {
58
+ "metric_T_r2_mean": True,
59
+ "metric_S_r2_mean": True,
60
+ "metric_S_spectral_corr_mean": True,
61
+ "metric_S_maxlag_corr_mean": True,
62
+ "coverage_success": True,
63
+ "metric_T_r2": True,
64
+ "metric_S_r2": True,
65
+ "metric_S_spectral_corr": True,
66
+ "metric_S_maxlag_corr": True,
67
+ "trend_r2": True,
68
+ "seasonal_r2": True,
69
+ "quad_fit_r2": True,
70
+ "seasonal_amplitude_slope_sign_match": True,
71
+ "target_frequency_coverage": True,
72
+ "m2_s2_separation_success": True,
73
+ "band_plausibility": True,
74
+ "band_power_ratio": True,
75
+ "band_peak_hit": True,
76
+ "resampling_trend_corr": True,
77
+ "resampling_season_corr": True,
78
+ "resampling_season_spectral_corr": True,
79
+ "resampling_season_maxlag_corr": True,
80
+ "resampling_band_overlap": True,
81
+ "resampling_stability": True,
82
+ "temporal_spectrum_overlap": True,
83
+ "overall_mean_rank": False,
84
+ "metric_T_dtw_mean": False,
85
+ "metric_T_dtw": False,
86
+ "c2_relative_error": False,
87
+ "seasonal_amplitude_slope_error": False,
88
+ "main_peak_frequency_error": False,
89
+ "temporal_trend_smoothness_drift": False,
90
+ "temporal_dominant_frequency_drift": False,
91
+ "mean_metric_rank": False,
92
+ }
93
+
94
+ DEFAULT_METRIC = {
95
+ "synthetic": "mean_metric_rank",
96
+ "real_proxy": "overall_mean_rank",
97
+ "semisynth": "mean_metric_rank",
98
+ "track_b": "seasonal_r2",
99
+ "track_c": "band_plausibility",
100
+ }
101
+
102
+
103
+ def _local_path(filename: str) -> Path:
104
+ if LOCAL_DATA_DIR:
105
+ return Path(LOCAL_DATA_DIR) / Path(filename).name
106
+ return Path(__file__).resolve().parent.parent / "TSDecompose-Benchmark" / filename
107
+
108
+
109
+ def _download(filename: str) -> str:
110
+ local_candidate = _local_path(filename)
111
+ if local_candidate.exists():
112
+ return str(local_candidate)
113
+ token = os.getenv("HF_TOKEN") or None
114
+ return hf_hub_download(
115
+ repo_id=DATASET_REPO,
116
+ repo_type="dataset",
117
+ filename=filename,
118
+ token=token,
119
+ )
120
+
121
+
122
+ def _method_table() -> pd.DataFrame:
123
+ path = _download(SITE_FILES["methods"])
124
+ methods = pd.read_json(path)
125
+ return methods[["method", "display_name", "family"]].drop_duplicates("method")
126
+
127
+
128
+ def _numeric(df: pd.DataFrame) -> pd.DataFrame:
129
+ out = df.copy()
130
+ for col in out.columns:
131
+ if col not in {"method", "display_name", "family", "dataset", "round_id"}:
132
+ try:
133
+ out[col] = pd.to_numeric(out[col])
134
+ except (TypeError, ValueError):
135
+ pass
136
+ return out
137
+
138
+
139
+ def _attach_methods(df: pd.DataFrame, methods: pd.DataFrame) -> pd.DataFrame:
140
+ out = df.merge(methods, on="method", how="left")
141
+ out["display_name"] = out["display_name"].fillna(out["method"])
142
+ out["family"] = out["family"].fillna("unclassified")
143
+ return out
144
+
145
+
146
+ def _add_mean_rank(df: pd.DataFrame, metrics: list[str]) -> pd.DataFrame:
147
+ out = df.copy()
148
+ rank_cols = []
149
+ for metric in metrics:
150
+ if metric not in out.columns:
151
+ continue
152
+ values = pd.to_numeric(out[metric], errors="coerce")
153
+ if values.notna().sum() == 0:
154
+ continue
155
+ ascending = not HIGHER_IS_BETTER.get(metric, True)
156
+ col = f"rank_{metric}"
157
+ out[col] = values.rank(ascending=ascending, method="min")
158
+ rank_cols.append(col)
159
+ if rank_cols:
160
+ out["mean_metric_rank"] = out[rank_cols].mean(axis=1).round(3)
161
+ out["rank"] = out["mean_metric_rank"].rank(ascending=True, method="min").astype(int)
162
+ return out
163
+
164
+
165
+ @lru_cache(maxsize=1)
166
+ def load_data() -> dict[str, pd.DataFrame]:
167
+ methods = _method_table()
168
+
169
+ synthetic = pd.read_csv(_download(SITE_FILES["synthetic"]))
170
+ synthetic = _attach_methods(_numeric(synthetic), methods)
171
+ synthetic = _add_mean_rank(
172
+ synthetic,
173
+ [
174
+ "metric_T_r2_mean",
175
+ "metric_T_dtw_mean",
176
+ "metric_S_r2_mean",
177
+ "metric_S_spectral_corr_mean",
178
+ "metric_S_maxlag_corr_mean",
179
+ ],
180
+ )
181
+
182
+ real_proxy = pd.read_csv(_download(SITE_FILES["real_proxy"]))
183
+ real_proxy = _attach_methods(_numeric(real_proxy), methods)
184
+ real_proxy["rank"] = pd.to_numeric(real_proxy["rank_order"], errors="coerce")
185
+
186
+ semisynth = pd.read_csv(_download(SITE_FILES["semisynth"]))
187
+ semisynth = _attach_methods(_numeric(semisynth), methods)
188
+ semisynth = _add_mean_rank(
189
+ semisynth,
190
+ [
191
+ "metric_T_r2_mean",
192
+ "metric_T_dtw_mean",
193
+ "metric_S_r2_mean",
194
+ "metric_S_spectral_corr_mean",
195
+ "metric_S_maxlag_corr_mean",
196
+ ],
197
+ )
198
+
199
+ track_b = pd.read_csv(_download(SITE_FILES["track_b"]))
200
+ track_b = _attach_methods(_numeric(track_b), methods)
201
+
202
+ track_c = pd.read_csv(_download(SITE_FILES["track_c"]))
203
+ track_c = _attach_methods(_numeric(track_c), methods)
204
+
205
+ return {
206
+ "synthetic": synthetic,
207
+ "real_proxy": real_proxy,
208
+ "semisynth": semisynth,
209
+ "track_b": track_b,
210
+ "track_c": track_c,
211
+ }
212
+
213
+
214
+ def available_metrics(view: str) -> list[str]:
215
+ df = load_data()[view]
216
+ return [
217
+ col
218
+ for col in df.columns
219
+ if col not in {"method", "display_name", "family", "dataset", "round_id"}
220
+ and pd.api.types.is_numeric_dtype(df[col])
221
+ ]
222
+
223
+
224
+ def filter_table(view: str, metric: str, family: str, query: str, top_n: int):
225
+ data = load_data()
226
+ df = data[view].copy()
227
+ metrics = available_metrics(view)
228
+ if metric not in metrics:
229
+ metric = DEFAULT_METRIC[view]
230
+
231
+ if family != "All":
232
+ df = df[df["family"] == family]
233
+ if query.strip():
234
+ q = query.strip().lower()
235
+ text_cols = [col for col in ["method", "display_name", "family", "dataset"] if col in df.columns]
236
+ mask = pd.Series(False, index=df.index)
237
+ for col in text_cols:
238
+ mask = mask | df[col].astype(str).str.lower().str.contains(q, regex=False)
239
+ df = df[mask]
240
+
241
+ ascending = not HIGHER_IS_BETTER.get(metric, True)
242
+ ranked = df.sort_values(metric, ascending=ascending, na_position="last").head(int(top_n))
243
+
244
+ show_cols = [
245
+ col
246
+ for col in [
247
+ "rank",
248
+ "display_name",
249
+ "method",
250
+ "family",
251
+ "dataset",
252
+ metric,
253
+ "mean_metric_rank",
254
+ "overall_mean_rank",
255
+ "coverage_success",
256
+ "row_count",
257
+ "setting_count",
258
+ ]
259
+ if col in ranked.columns
260
+ ]
261
+ remaining_metrics = [
262
+ col
263
+ for col in ranked.columns
264
+ if col not in show_cols
265
+ and col not in {"rank_order"}
266
+ and pd.api.types.is_numeric_dtype(ranked[col])
267
+ ]
268
+ table = ranked[show_cols + remaining_metrics].copy()
269
+ table = table.round(4)
270
+
271
+ plot_df = ranked.copy()
272
+ if "dataset" in plot_df.columns and plot_df["method"].duplicated().any():
273
+ plot_df = (
274
+ plot_df.groupby(["method", "display_name", "family"], as_index=False)[metric]
275
+ .mean(numeric_only=True)
276
+ .sort_values(metric, ascending=ascending)
277
+ .head(int(top_n))
278
+ )
279
+
280
+ title = f"{VIEW_LABELS[view]} - {metric}"
281
+ fig = px.bar(
282
+ plot_df,
283
+ x=metric,
284
+ y="display_name",
285
+ color="family",
286
+ orientation="h",
287
+ title=title,
288
+ hover_data=[col for col in ["method", "family"] if col in plot_df.columns],
289
+ )
290
+ fig.update_layout(
291
+ height=max(420, 28 * max(len(plot_df), 8)),
292
+ margin=dict(l=10, r=20, t=52, b=20),
293
+ yaxis=dict(autorange="reversed", title=""),
294
+ xaxis=dict(title=metric),
295
+ legend_title_text="Family",
296
+ paper_bgcolor="white",
297
+ plot_bgcolor="white",
298
+ )
299
+
300
+ summary = (
301
+ f"**{VIEW_LABELS[view]}** \n"
302
+ f"{VIEW_NOTES[view]} \n\n"
303
+ f"Rows shown: `{len(table)}` from `{len(data[view])}`. "
304
+ f"Metric direction: `{'higher is better' if HIGHER_IS_BETTER.get(metric, True) else 'lower is better'}`. "
305
+ f"Dataset source: [`{DATASET_REPO}`](https://huggingface.co/datasets/{DATASET_REPO})."
306
+ )
307
+ return table, fig, summary
308
+
309
+
310
+ def refresh_metric_choices(view: str):
311
+ metrics = available_metrics(view)
312
+ return gr.update(choices=metrics, value=DEFAULT_METRIC[view])
313
+
314
+
315
+ def stats_html() -> str:
316
+ data = load_data()
317
+ synthetic_methods = data["synthetic"]["method"].nunique()
318
+ return f"""
319
+ <div class="stats-grid">
320
+ <div class="stat"><span class="stat-value">300</span><span class="stat-label">Core synthetic series</span></div>
321
+ <div class="stat"><span class="stat-value">{synthetic_methods}</span><span class="stat-label">Expanded roster rows</span></div>
322
+ <div class="stat"><span class="stat-value">6,600</span><span class="stat-label">Synthetic method-instance rows</span></div>
323
+ <div class="stat"><span class="stat-value">2</span><span class="stat-label">Evidence versions</span></div>
324
+ </div>
325
+ """
326
+
327
+
328
+ CSS = """
329
+ .gradio-container { max-width: 1220px !important; }
330
+ .stats-grid {
331
+ display: grid;
332
+ grid-template-columns: repeat(4, minmax(0, 1fr));
333
+ gap: 10px;
334
+ margin: 10px 0 16px;
335
+ }
336
+ .stat {
337
+ border: 1px solid #d8dee9;
338
+ border-radius: 8px;
339
+ padding: 12px 14px;
340
+ background: #ffffff;
341
+ }
342
+ .stat-value {
343
+ display: block;
344
+ font-size: 24px;
345
+ font-weight: 700;
346
+ color: #0f3b57;
347
+ }
348
+ .stat-label {
349
+ display: block;
350
+ font-size: 13px;
351
+ color: #53606f;
352
+ margin-top: 2px;
353
+ }
354
+ @media (max-width: 780px) {
355
+ .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
356
+ }
357
+ """
358
+
359
+ GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0])
360
+ BLOCKS_KWARGS = {"title": "TSDecompose Benchmark Leaderboard"}
361
+ if GRADIO_MAJOR < 6:
362
+ BLOCKS_KWARGS["css"] = CSS
363
+
364
+ with gr.Blocks(**BLOCKS_KWARGS) as demo:
365
+ gr.Markdown(
366
+ """
367
+ # TSDecompose Benchmark Leaderboard
368
+
369
+ Interactive leaderboard for **Time-Series Decomposition as a Standalone Task: A Mechanism-Identifiable Benchmark**.
370
+ The dataset repository is the source of record; this Space is the web presentation layer.
371
+ """
372
+ )
373
+ gr.HTML(value=stats_html())
374
+
375
+ with gr.Row():
376
+ view = gr.Dropdown(
377
+ choices=[(label, key) for key, label in VIEW_LABELS.items()],
378
+ value="synthetic",
379
+ label="View",
380
+ )
381
+ metric = gr.Dropdown(
382
+ choices=available_metrics("synthetic"),
383
+ value=DEFAULT_METRIC["synthetic"],
384
+ label="Metric",
385
+ )
386
+ with gr.Row():
387
+ family = gr.Dropdown(
388
+ choices=["All", "classical", "extension_proxy", "unclassified"],
389
+ value="All",
390
+ label="Family",
391
+ )
392
+ query = gr.Textbox(label="Search", placeholder="method, family, or dataset")
393
+ top_n = gr.Slider(5, 40, value=22, step=1, label="Rows")
394
+
395
+ note = gr.Markdown()
396
+ plot = gr.Plot()
397
+ table = gr.Dataframe(label="Leaderboard table", wrap=True, interactive=False)
398
+
399
+ inputs = [view, metric, family, query, top_n]
400
+ for control in inputs:
401
+ control.change(filter_table, inputs=inputs, outputs=[table, plot, note])
402
+ view.change(refresh_metric_choices, inputs=view, outputs=metric).then(
403
+ filter_table, inputs=inputs, outputs=[table, plot, note]
404
+ )
405
+ demo.load(filter_table, inputs=inputs, outputs=[table, plot, note])
406
+
407
+
408
+ if __name__ == "__main__":
409
+ if GRADIO_MAJOR >= 6:
410
+ demo.launch(css=CSS)
411
+ else:
412
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=5.0,<6.0
2
+ huggingface_hub>=0.25
3
+ pandas>=2.0
4
+ plotly>=5.20