import json import os from functools import lru_cache from html import escape from pathlib import Path import gradio as gr import pandas as pd import plotly.graph_objects as go from huggingface_hub import hf_hub_download DATASET_REPO = os.getenv("TSDECOMPOSE_DATASET_REPO", "Zipeng365/TSDecompose-Benchmark") DATA_VERSION = os.getenv("TSDECOMPOSE_DATA_VERSION", "v1.0.0") LOCAL_DATA_DIR = os.getenv("TSDECOMPOSE_LOCAL_DATA_DIR") SITE_FILES = { "paper_global": "data/paper_tables/global_performance_summary.csv", "paper_radar": "data/paper_figures/selected_radar_charts.png", "synthetic": f"site_data/{DATA_VERSION}/leaderboard_synthetic_full22_overall.csv", "synthetic_rank": "data/synthetic_full22_extension/results/summary/ranking_paper_5metric_overall.csv", "real_proxy": f"site_data/{DATA_VERSION}/leaderboard_real_proxy22_overall.csv", "semisynth": f"site_data/{DATA_VERSION}/leaderboard_semisynth_transfer_by_method.csv", "semisynth_rank": "data/semisynth_transfer/results/summary/ranking_paper_5metric_overall.csv", "track_b": f"site_data/{DATA_VERSION}/leaderboard_post_rebuttal_real_physics_track_b.csv", "track_c": f"site_data/{DATA_VERSION}/leaderboard_post_rebuttal_real_proxy_track_c.csv", "methods": f"site_data/{DATA_VERSION}/methods.json", "suites": f"site_data/{DATA_VERSION}/suites.json", "metrics": f"site_data/{DATA_VERSION}/evaluation_metrics.json", } VIEW_LABELS = { "paper_global": "Paper Table 2 / Figure 3 Anchors", "synthetic": "Benchmark 22-Method Synthetic Extension", "real_proxy": "Benchmark 22-Method Real Proxy", "semisynth": "Benchmark 22-Method Semi-Synthetic Transfer", "track_b": "Post-Rebuttal Track B: CO2 / Tides", "track_c": "Post-Rebuttal Track C: Real Proxy Panel", } VIEW_NOTES = { "paper_global": ( "Camera-ready paper-aligned view. This reproduces Table 2's six-family " "global performance summary from the 6-scenario, 50-draw core protocol. " "Table 2 reports two of the five paper-core component-recovery metrics, " "using tier-balanced stationary aggregation over Tiers 1-2 and Tier 3 for " "the non-stationary columns; Figure 3 shows the " "five-axis capability profile. This is not a single universal ranking." ), "synthetic": ( "Benchmark-only expanded synthetic board: 6 scenarios x 50 draws = 300 generated " "time-series instances. The expanded roster gives 22 method rows, " "or 6,600 method-instance evaluations. This is a living extension view, not the " "paper's primary Table 2/Figure 3 view. It reuses the five component-recovery " "metrics and uses the released five-metric mean within-setting ranking." ), "real_proxy": ( "Benchmark-only real-data companion track. These are proxy diagnostics, " "not exact component-recovery labels and not camera-ready paper rankings. " "Lower overall mean rank is better." ), "semisynth": ( "Benchmark-only 22-method semi-synthetic transfer track: known synthetic " "mechanisms injected into six real monthly backgrounds with three mechanisms, " "two background scales, and eight windows per setting. This is a living " "benchmark extension, not a camera-ready paper table; the displayed rank uses " "the released five-metric mean within-setting ranking." ), "track_b": ( "Post-rebuttal mechanism-aware checks on CO2 and tides. These use " "mechanism-informed approximate structure and should be read as companion evidence." ), "track_c": ( "Post-rebuttal six-dataset proxy/stability panel. These rows measure " "plausibility and robustness where exact real-world components are unavailable." ), } HIGHER_IS_BETTER = { "stationary_trend_r2": True, "stationary_seasonal_spectral_corr": True, "nonstationary_trend_r2": True, "nonstationary_seasonal_spectral_corr": True, "metric_T_r2_mean": True, "metric_S_r2_mean": True, "metric_S_spectral_corr_mean": True, "metric_S_maxlag_corr_mean": True, "coverage_success": True, "metric_T_r2": True, "metric_S_r2": True, "metric_S_spectral_corr": True, "metric_S_maxlag_corr": True, "trend_r2": True, "seasonal_r2": True, "quad_fit_r2": True, "seasonal_amplitude_slope_sign_match": True, "target_frequency_coverage": True, "m2_s2_separation_success": True, "band_plausibility": True, "band_power_ratio": True, "band_peak_hit": True, "resampling_trend_corr": True, "resampling_season_corr": True, "resampling_season_spectral_corr": True, "resampling_season_maxlag_corr": True, "resampling_band_overlap": True, "resampling_stability": True, "temporal_spectrum_overlap": True, "overall_mean_rank": False, "metric_T_dtw_mean": False, "metric_T_dtw": False, "c2_relative_error": False, "seasonal_amplitude_slope_error": False, "main_peak_frequency_error": False, "temporal_trend_smoothness_drift": False, "temporal_dominant_frequency_drift": False, "mean_metric_rank": False, } DEFAULT_METRIC = { "paper_global": "stationary_trend_r2", "synthetic": "overall_mean_rank", "real_proxy": "overall_mean_rank", "semisynth": "overall_mean_rank", "track_b": "seasonal_r2", "track_c": "band_plausibility", } COLUMN_LABELS = { "stationary_trend_r2": "Stationary Trend R2", "stationary_seasonal_spectral_corr": "Stationary Seasonal Spectral Corr", "nonstationary_trend_r2": "Non-stationary Trend R2", "nonstationary_seasonal_spectral_corr": "Non-stationary Seasonal Spectral Corr", "metric_T_r2": "Trend R2", "metric_T_r2_mean": "Trend R2 mean", "metric_T_dtw": "Trend DTW", "metric_T_dtw_mean": "Trend DTW mean", "metric_S_r2": "Seasonal R2", "metric_S_r2_mean": "Seasonal R2 mean", "metric_S_spectral_corr": "Seasonal spectral corr", "metric_S_spectral_corr_mean": "Seasonal spectral corr mean", "metric_S_maxlag_corr": "Seasonal max-lag corr", "metric_S_maxlag_corr_mean": "Seasonal max-lag corr mean", "mean_metric_rank": "Mean metric rank", "overall_mean_rank": "Overall mean rank", "overall_std_rank": "Overall rank SD", "coverage_success": "Coverage", "row_count": "Rows", "setting_count": "Settings", } TABLE_LABELS = { "display_name": "Method", "family": "Family", "method": "Method ID", "stationary_trend_r2": "Stat. T R2", "stationary_seasonal_spectral_corr": "Stat. S spec.", "nonstationary_trend_r2": "Nonstat. T R2", "nonstationary_seasonal_spectral_corr": "Nonstat. S spec.", "metric_T_r2_mean": "T R2", "metric_T_dtw_mean": "T DTW", "metric_S_r2_mean": "S R2", "metric_S_spectral_corr_mean": "S spec.", "metric_S_maxlag_corr_mean": "S max-lag", "overall_mean_rank": "Mean rank", "overall_std_rank": "Rank SD", } PAPER_GLOBAL_METRICS = [ "stationary_trend_r2", "stationary_seasonal_spectral_corr", "nonstationary_trend_r2", "nonstationary_seasonal_spectral_corr", ] FAMILY_COLORS = { "paper family": "#2563eb", "classical": "#2563eb", "extension_proxy": "#d97706", "neural_block": "#7c3aed", "unclassified": "#64748b", } def _local_path(filename: str) -> Path: if LOCAL_DATA_DIR: return Path(LOCAL_DATA_DIR) / Path(filename).name return Path(__file__).resolve().parent.parent / "TSDecompose-Benchmark" / filename def _download(filename: str) -> str: local_candidate = _local_path(filename) if local_candidate.exists(): return str(local_candidate) token = os.getenv("HF_TOKEN") or None return hf_hub_download( repo_id=DATASET_REPO, repo_type="dataset", filename=filename, token=token, ) @lru_cache(maxsize=1) def metric_metadata() -> dict: with open(_download(SITE_FILES["metrics"]), encoding="utf-8") as handle: return json.load(handle) def _metric_label(metric: str) -> str: return COLUMN_LABELS.get(metric, metric) def _table_label(column: str) -> str: return TABLE_LABELS.get(column, COLUMN_LABELS.get(column, column)) def metrics_html() -> str: meta = metric_metadata() metrics = meta.get("metrics", []) cards = [] for item in metrics: direction = "higher is better" if item.get("direction") == "higher" else "lower is better" cards.append( f"""
{escape(item.get("display_name", ""))}
{escape(direction)}
{escape(item.get("formula", ""))}

{escape(item.get("description", ""))}

""" ) return f"""
Paper Evaluation Protocol

The classic paper benchmark uses {int(meta.get("paper_core_metric_count", 5))} core component-recovery metrics. Each metric is computed per generated draw after output alignment, then averaged by scenario, tier, or regime.

Table 2 displays only {int(meta.get("paper_table2_display_metric_count", 2))} of those metrics: Trend R2 and Seasonal spectral correlation, split over stationary regimes (Tiers 1-2) and the non-stationary regime (Tier 3). Its stationary columns use a tier-balanced average of Tier 1 and Tier 2 means over valid metric values; non-stationary columns use Tier 3 means. Figure 3 is the five-metric profile view.

{''.join(cards)}
""" def _method_table() -> pd.DataFrame: path = _download(SITE_FILES["methods"]) methods = pd.read_json(path) return methods[["method", "display_name", "family"]].drop_duplicates("method") def _paper_family_table() -> pd.DataFrame: df = pd.read_csv(_download(SITE_FILES["paper_global"])) df = _numeric(df) df["display_name"] = df["method_family"] df["method"] = df["method_family"] df["family"] = "paper family" return df def _numeric(df: pd.DataFrame) -> pd.DataFrame: out = df.copy() for col in out.columns: if col not in {"method", "display_name", "family", "dataset", "round_id"}: try: out[col] = pd.to_numeric(out[col]) except (TypeError, ValueError): pass return out def _attach_methods(df: pd.DataFrame, methods: pd.DataFrame) -> pd.DataFrame: out = df.merge(methods, on="method", how="left") out["display_name"] = out["display_name"].fillna(out["method"]) out["family"] = out["family"].fillna("unclassified") return out def _add_mean_rank(df: pd.DataFrame, metrics: list[str]) -> pd.DataFrame: out = df.copy() rank_cols = [] for metric in metrics: if metric not in out.columns: continue values = pd.to_numeric(out[metric], errors="coerce") if values.notna().sum() == 0: continue ascending = not HIGHER_IS_BETTER.get(metric, True) col = f"rank_{metric}" out[col] = values.rank(ascending=ascending, method="min") rank_cols.append(col) if rank_cols: out["mean_metric_rank"] = out[rank_cols].mean(axis=1).round(3) out["rank"] = out["mean_metric_rank"].rank(ascending=True, method="min").astype(int) return out def _attach_released_rank(df: pd.DataFrame, rank_file: str) -> pd.DataFrame: rank = pd.read_csv(_download(rank_file)) rank = _numeric(rank) keep = [ col for col in ["method", "overall_mean_rank", "overall_std_rank", "setting_count", "rank_order"] if col in rank.columns ] out = df.drop(columns=[col for col in keep if col != "method" and col in df.columns]) out = out.merge(rank[keep], on="method", how="left") if "rank_order" in out.columns: out["rank"] = pd.to_numeric(out["rank_order"], errors="coerce") elif "overall_mean_rank" in out.columns: values = pd.to_numeric(out["overall_mean_rank"], errors="coerce") out["rank"] = values.rank(ascending=True, method="min") return out @lru_cache(maxsize=1) def load_data() -> dict[str, pd.DataFrame]: methods = _method_table() paper_global = _paper_family_table() synthetic = pd.read_csv(_download(SITE_FILES["synthetic"])) synthetic = _attach_methods(_numeric(synthetic), methods) synthetic = _attach_released_rank(synthetic, SITE_FILES["synthetic_rank"]) real_proxy = pd.read_csv(_download(SITE_FILES["real_proxy"])) real_proxy = _attach_methods(_numeric(real_proxy), methods) real_proxy["rank"] = pd.to_numeric(real_proxy["rank_order"], errors="coerce") semisynth = pd.read_csv(_download(SITE_FILES["semisynth"])) semisynth = _attach_methods(_numeric(semisynth), methods) semisynth = _attach_released_rank(semisynth, SITE_FILES["semisynth_rank"]) track_b = pd.read_csv(_download(SITE_FILES["track_b"])) track_b = _attach_methods(_numeric(track_b), methods) track_c = pd.read_csv(_download(SITE_FILES["track_c"])) track_c = _attach_methods(_numeric(track_c), methods) return { "paper_global": paper_global, "synthetic": synthetic, "real_proxy": real_proxy, "semisynth": semisynth, "track_b": track_b, "track_c": track_c, } def available_metrics(view: str) -> list[str]: df = load_data()[view] return [ col for col in df.columns if col not in {"method", "display_name", "family", "dataset", "round_id"} and pd.api.types.is_numeric_dtype(df[col]) ] def metric_choices(view: str) -> list[tuple[str, str]]: return [(_metric_label(metric), metric) for metric in available_metrics(view)] def filter_table(view: str, metric: str, family: str, query: str, top_n: int): data = load_data() df = data[view].copy() metrics = available_metrics(view) if metric not in metrics: metric = DEFAULT_METRIC[view] if family != "All": df = df[df["family"] == family] if query.strip(): q = query.strip().lower() text_cols = [col for col in ["method", "display_name", "family", "dataset"] if col in df.columns] mask = pd.Series(False, index=df.index) for col in text_cols: mask = mask | df[col].astype(str).str.lower().str.contains(q, regex=False) df = df[mask] ascending = not HIGHER_IS_BETTER.get(metric, True) if view == "paper_global": ranked = df.head(int(top_n)) else: ranked = df.sort_values(metric, ascending=ascending, na_position="last").head(int(top_n)) if view == "paper_global": show_cols = ["display_name", metric] show_cols += [col for col in PAPER_GLOBAL_METRICS if col != metric] remaining_metrics = [] else: show_cols = [] for col in [ "rank", "display_name", "family", "dataset", metric, "method", "mean_metric_rank", "overall_mean_rank", "coverage_success", "row_count", "setting_count", ]: if col in ranked.columns and col not in show_cols: show_cols.append(col) remaining_metrics = [ col for col in ranked.columns if col not in show_cols and col not in {"rank_order"} and pd.api.types.is_numeric_dtype(ranked[col]) ] table = ranked[show_cols + remaining_metrics].copy() table = table.round(4) table_html = _table_html(table) plot_df = ranked.copy() if "dataset" in plot_df.columns and plot_df["method"].duplicated().any(): plot_df = ( plot_df.groupby(["method", "display_name", "family"], as_index=False)[metric] .mean(numeric_only=True) .sort_values(metric, ascending=ascending) .head(int(top_n)) ) metric_name = _metric_label(metric) fig = _bar_figure(plot_df, metric, metric_name, view) summary = ( f"**{VIEW_LABELS[view]}** \n" f"{VIEW_NOTES[view]} \n\n" f"Rows shown: `{len(table)}` from `{len(data[view])}`. " f"Metric: `{metric_name}`. " f"Metric direction: `{'higher is better' if HIGHER_IS_BETTER.get(metric, True) else 'lower is better'}`. " f"Dataset source: [`{DATASET_REPO}`](https://huggingface.co/datasets/{DATASET_REPO})." ) figure = _figure_update(view) return table_html, fig, summary, figure def _table_html(table: pd.DataFrame) -> str: display = table.copy() if "family" in display.columns: display["family"] = display["family"].replace( {"extension_proxy": "extension proxy"} ) display = display.rename(columns={col: _table_label(col) for col in display.columns}) html = display.to_html(index=False, escape=False, classes="leaderboard-table") return f"""
Leaderboard table
{html}
""" def _bar_figure(plot_df: pd.DataFrame, metric: str, metric_name: str, view: str) -> go.Figure: data = plot_df.copy() data[metric] = pd.to_numeric(data[metric], errors="coerce") data = data[data[metric].notna()] labels = data["display_name"].astype(str).tolist() values = [float(value) for value in data[metric].tolist()] families = data.get("family", pd.Series(["unclassified"] * len(data))).astype(str).tolist() colors = [FAMILY_COLORS.get(family, "#0f766e") for family in families] hover = [ f"Family: {family}
Method: {method}" for family, method in zip(families, data.get("method", data["display_name"]).astype(str)) ] fig = go.Figure() fig.add_bar( x=values, y=labels, orientation="h", marker=dict(color=colors, line=dict(color="#1f2937", width=0.5)), customdata=hover, hovertemplate="%{y}
" + escape(metric_name) + ": %{x:.4f}
%{customdata}", ) title = f"{VIEW_LABELS[view]} - {metric_name}" fig.update_layout( title=dict(text=title, x=0.01, xanchor="left"), height=max(360, 34 * max(len(labels), 6)), margin=dict(l=150, r=24, t=56, b=48), yaxis=dict( autorange="reversed", title="", type="category", categoryorder="array", categoryarray=labels, tickfont=dict(size=13), ), xaxis=dict(title=metric_name, zeroline=True, rangemode="tozero"), showlegend=False, paper_bgcolor="white", plot_bgcolor="white", bargap=0.28, ) return fig def _figure_update(view: str): if view != "paper_global": return gr.update(value=None, visible=False) bundled = Path(__file__).resolve().parent / "assets" / "selected_radar_charts.png" if bundled.exists(): return gr.update(value=str(bundled), visible=True) return gr.update(value=_download(SITE_FILES["paper_radar"]), visible=True) def refresh_controls(view: str): metrics = available_metrics(view) return gr.update(choices=metric_choices(view), value=DEFAULT_METRIC[view]), gr.update(value="All") def stats_html() -> str: data = load_data() paper_families = data["paper_global"]["method"].nunique() synthetic_methods = data["synthetic"]["method"].nunique() return f"""
{paper_families}Paper method families
5Paper core metrics
6 x 50Classic synthetic instances
{synthetic_methods}Expansion method rows
2Evidence versions
""" CSS = """ .gradio-container { max-width: 1220px !important; } .stats-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; margin: 10px 0 16px; } .stat { border: 1px solid #d8dee9; border-radius: 8px; padding: 12px 14px; background: #ffffff; } .stat-value { display: block; font-size: 24px; font-weight: 700; color: #0f3b57; } .stat-label { display: block; font-size: 13px; color: #53606f; margin-top: 2px; } .metric-panel { border: 1px solid #d8dee9; border-radius: 8px; padding: 14px; background: #ffffff; margin: 0 0 16px; } .metric-title { font-size: 16px; font-weight: 700; color: #1f2937; margin-bottom: 6px; } .metric-panel p { color: #374151; font-size: 14px; margin: 6px 0; } .metric-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; margin-top: 12px; } .metric-card { border: 1px solid #e5e9f0; border-radius: 8px; padding: 10px; background: #fbfcfd; min-width: 0; } .metric-name { font-size: 13px; font-weight: 700; color: #1f2937; } .metric-direction { font-size: 12px; color: #53606f; margin: 3px 0 6px; } .metric-card code { display: block; white-space: normal; overflow-wrap: anywhere; background: #f3f5f8; border-radius: 6px; padding: 6px; font-size: 11px; color: #273142; } .metric-card p { font-size: 12px; margin: 7px 0 0; } .table-title { font-size: 14px; color: #303946; margin: 12px 0 8px; } .table-wrap { overflow-x: auto; border: 1px solid #d8dee9; border-radius: 8px; background: #ffffff; max-height: 520px; } .leaderboard-table { border-collapse: collapse; width: max-content; min-width: 100%; font-size: 13px; } .leaderboard-table th, .leaderboard-table td { border-bottom: 1px solid #edf0f4; padding: 8px 10px; text-align: left; white-space: nowrap; } .leaderboard-table th { position: sticky; top: 0; background: #f7f9fb; color: #1f2937; z-index: 1; } .leaderboard-table th:nth-child(1), .leaderboard-table td:nth-child(1) { min-width: 48px; } .leaderboard-table th:nth-child(2), .leaderboard-table td:nth-child(2) { min-width: 210px; } .leaderboard-table th:nth-child(3), .leaderboard-table td:nth-child(3) { min-width: 130px; } .leaderboard-table tr:nth-child(even) td { background: #fbfcfd; } @media (max-width: 780px) { .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .metric-grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } } """ GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0]) BLOCKS_KWARGS = {"title": "TSDecompose Benchmark Leaderboard"} if GRADIO_MAJOR < 6: BLOCKS_KWARGS["css"] = CSS with gr.Blocks(**BLOCKS_KWARGS) as demo: gr.Markdown( """ # TSDecompose Benchmark Leaderboard Interactive leaderboard for **Time-Series Decomposition as a Standalone Task: A Mechanism-Identifiable Benchmark**. The dataset repository is the source of record; this Space is the web presentation layer. """ ) gr.HTML(value=stats_html()) gr.HTML(value=metrics_html()) with gr.Row(): view = gr.Dropdown( choices=[(label, key) for key, label in VIEW_LABELS.items()], value="paper_global", label="View", ) metric = gr.Dropdown( choices=metric_choices("paper_global"), value=DEFAULT_METRIC["paper_global"], label="Metric", ) with gr.Row(): family = gr.Dropdown( choices=["All", "paper family", "classical", "extension_proxy"], value="All", label="Family", ) query = gr.Textbox(label="Search", placeholder="method, family, or dataset") top_n = gr.Slider(5, 40, value=22, step=1, label="Rows") note = gr.Markdown() figure_ref = gr.Image( label="Paper Figure 3 reference", type="filepath", interactive=False, visible=True, ) plot = gr.Plot() table = gr.HTML() inputs = [view, metric, family, query, top_n] for control in inputs: control.change(filter_table, inputs=inputs, outputs=[table, plot, note, figure_ref]) view.change(refresh_controls, inputs=view, outputs=[metric, family]).then( filter_table, inputs=inputs, outputs=[table, plot, note, figure_ref] ) demo.load(filter_table, inputs=inputs, outputs=[table, plot, note, figure_ref]) if __name__ == "__main__": if GRADIO_MAJOR >= 6: demo.launch(css=CSS) else: demo.launch()