Solves 500 Errors For Some Users

#1
by Tonic - opened
.claude/skills/add-model/SKILL.md DELETED
@@ -1,27 +0,0 @@
1
- ---
2
- name: add-model
3
- description: Get a newly merged model onto the fev-bench leaderboard. Use when asked to add/register a model, pull in new results, or refresh the leaderboard.
4
- ---
5
-
6
- # Add a new model to the leaderboard
7
-
8
- Models reach the leaderboard by being merged into `autogluon/fev`: their results CSV lands in
9
- `benchmarks/fev_bench/results/` and their display metadata in `benchmarks/fev_bench/models.yaml`.
10
- This repo holds no model data or metadata. The Docker build runs `save_tables.py main`, so the Space
11
- shows whatever is on fev `main` when it was last built.
12
-
13
- If the user supplies metadata (org, URL, type), it belongs in `models.yaml` in the fev repo, not
14
- here; point them at `models/README.md` there.
15
-
16
- ## Steps
17
-
18
- 1. **Preview locally** (optional) to check the new model before it goes live:
19
- ```bash
20
- uv run python save_tables.py # latest fev main
21
- uv run streamlit run fev-leaderboard-app.py --server.port=8501 --server.address=0.0.0.0
22
- ```
23
- `save_tables.py` fails loudly if a results file has no `models.yaml` entry. A `system` or
24
- `closed-api` model only appears after selecting its type in the sidebar.
25
-
26
- 2. **Publish**: rebuild the Space (Settings → Factory rebuild on huggingface.co/spaces/autogluon/fev-bench).
27
- No commit to this repo is needed. Afterwards, the About page should show the expected fev commit.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitattributes CHANGED
@@ -25,7 +25,6 @@
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
  *.wasm filter=lfs diff=lfs merge=lfs -text
@@ -33,3 +32,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
 
28
  *.tflite filter=lfs diff=lfs merge=lfs -text
29
  *.tgz filter=lfs diff=lfs merge=lfs -text
30
  *.wasm filter=lfs diff=lfs merge=lfs -text
 
32
  *.zip filter=lfs diff=lfs merge=lfs -text
33
  *.zst filter=lfs diff=lfs merge=lfs -text
34
  *tfevents* filter=lfs diff=lfs merge=lfs -text
35
+ scale-hf-logo.png filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,4 +1,13 @@
1
- __pycache__
2
- # Generated at build time by save_tables.py (see Dockerfile)
3
- tables/
4
- src/vendor/
 
 
 
 
 
 
 
 
 
 
1
+ auto_evals/
2
+ venv/
3
+ __pycache__/
4
+ .env
5
+ .ipynb_checkpoints
6
+ *ipynb
7
+ .vscode/
8
+
9
+ eval-queue/
10
+ eval-results/
11
+ eval-queue-bk/
12
+ eval-results-bk/
13
+ logs/
.pre-commit-config.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ default_language_version:
16
+ python: python3
17
+
18
+ ci:
19
+ autofix_prs: true
20
+ autoupdate_commit_msg: '[pre-commit.ci] pre-commit suggestions'
21
+ autoupdate_schedule: quarterly
22
+
23
+ repos:
24
+ - repo: https://github.com/pre-commit/pre-commit-hooks
25
+ rev: v4.3.0
26
+ hooks:
27
+ - id: check-yaml
28
+ - id: check-case-conflict
29
+ - id: detect-private-key
30
+ - id: check-added-large-files
31
+ args: ['--maxkb=1000']
32
+ - id: requirements-txt-fixer
33
+ - id: end-of-file-fixer
34
+ - id: trailing-whitespace
35
+
36
+ - repo: https://github.com/PyCQA/isort
37
+ rev: 5.12.0
38
+ hooks:
39
+ - id: isort
40
+ name: Format imports
41
+
42
+ - repo: https://github.com/psf/black
43
+ rev: 22.12.0
44
+ hooks:
45
+ - id: black
46
+ name: Format code
47
+ additional_dependencies: ['click==8.0.2']
48
+
49
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
50
+ # Ruff version.
51
+ rev: 'v0.0.267'
52
+ hooks:
53
+ - id: ruff
.streamlit/config.toml DELETED
@@ -1,4 +0,0 @@
1
- [theme]
2
- base = "light"
3
- # Muted blue for sidebar controls and links, instead of Streamlit's red
4
- primaryColor = "#6093c4"
 
 
 
 
 
CLAUDE.md DELETED
@@ -1,97 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- fev-bench Leaderboard is a Streamlit web application displaying time series forecasting model evaluation results from the fev-bench benchmark. It evaluates 30+ forecasting models using multiple metrics (SQL, MASE, WQL, WAPE) across 100 benchmark tasks.
8
-
9
- ## Common Commands
10
-
11
- ```bash
12
- # Run the Streamlit app locally
13
- uv run streamlit run fev-leaderboard-app.py --server.port=8501 --server.address=0.0.0.0
14
-
15
- # Generate data from the autogluon/fev repo (required once before running locally)
16
- uv run python save_tables.py [commit] # defaults to latest main
17
- uv run python save_tables.py --fev-repo ../fev # use an existing local checkout
18
-
19
- # Docker build and run
20
- docker build -t fev-leaderboard .
21
- docker run -p 8501:8501 fev-leaderboard
22
- ```
23
-
24
- Note: Use `uv run` prefix for all Python commands in this project.
25
-
26
- No test or lint frameworks are configured.
27
-
28
- ## Architecture
29
-
30
- ```
31
- fev-leaderboard-app.py # Main entry point (Streamlit multi-page router)
32
- save_tables.py # Generates tables/ and src/vendor/ from a single fev commit (run at Docker build)
33
- pages/
34
- ├── fev_bench.py # Main leaderboard: computes leaderboard/pairwise, renders them
35
- └── about.py # Help page with links
36
- src/
37
- ├── utils.py # Visualization, formatting, model metadata lookups, color palette
38
- ├── strings.py # UI text, metric descriptions, paper citations
39
- ├── task_groups.py # Task groupings by frequency and domain
40
- └── vendor/ # GENERATED, gitignored: compute logic copied from the fev repo
41
- └── fev_bench_compute.py
42
- tables/ # GENERATED by save_tables.py, gitignored
43
- ├── summaries.csv # Raw evaluation summaries (leaderboards are computed from these)
44
- ├── models.csv # Display metadata, from the fev repo's benchmarks/fev_bench/models.yaml
45
- ├── pivot_*.csv # Per-task error matrices (+ `_baseline_imputed` / `_leakage_imputed`)
46
- └── source.json # Which fev commit the tables came from
47
- ```
48
-
49
- **Data flow**: GitHub (autogluon/fev) → `save_tables.py` → `tables/` + vendored compute → `fev_bench.py`
50
-
51
- `tables/` and `src/vendor/` are not committed. The Dockerfile runs `save_tables.py main` at build time,
52
- so rebuilding the Space (Settings → Factory rebuild) picks up whatever is on fev `main`. The About
53
- page shows which fev commit the running Space was built from.
54
-
55
- Leaderboard and pairwise tables are **not** precomputed. Win rate and skill score depend on which
56
- models are included, and the sidebar lets readers include/exclude model types, so `fev_bench.py`
57
- computes them per (task group, metric, included types) and caches with `st.cache_data`.
58
-
59
- ## Key Modules
60
-
61
- **`src/utils.py`**: Core module containing:
62
- - `get_model_metadata()`: Model attributes loaded from `tables/models.csv` (organization, url, model_type, zero_shot, commercial_use, display_name)
63
- - `ALL_METRICS`: Dict with SQL, MASE, WQL, WAPE definitions
64
- - `format_leaderboard()`, `construct_bar_chart()`, `construct_pairwise_chart()`, `construct_pivot_table()`: Styling functions
65
- - `COLORS`: Custom palette (purple, gold, silver, bronze)
66
-
67
- **`src/strings.py`**: Documentation strings for metric formulas, win rate/skill score calculations, imputation strategies
68
-
69
- ## Metrics
70
-
71
- | Metric | Type | Description |
72
- |--------|------|-------------|
73
- | SQL | Probabilistic | Scaled Quantile Loss (scale-invariant) |
74
- | MASE | Point | Mean Absolute Scaled Error (scale-invariant) |
75
- | WQL | Probabilistic | Weighted Quantile Loss (scale-dependent) |
76
- | WAPE | Point | Weighted Absolute Percentage Error (scale-dependent) |
77
-
78
- ## Model Types
79
-
80
- Each submission declares a `model_type` in `benchmarks/fev_bench/models.yaml` in the fev repo:
81
- `pretrained`, `task-specific`, `statistical`, `system`, or `closed-api`. This drives the name color
82
- on the leaderboard. `system` and `closed-api` models are hidden until the reader opts in from the
83
- sidebar, because their results cannot be reproduced independently or compared to single models.
84
-
85
- Model metadata is never edited here: it is declared by submitters in the fev repo and pulled in by
86
- `save_tables.py`. Models flagged `hidden: true` there are dropped before any metric is computed.
87
-
88
- ## Imputation Strategy
89
-
90
- - **Failed tasks**: Replaced with Seasonal Naive scores
91
- - **Leaky tasks** (training corpus overlap for zero-shot models): Replaced with Chronos-Bolt scores
92
-
93
- ## External References
94
-
95
- - fev-bench paper: https://arxiv.org/abs/2509.26468
96
- - fev library docs: https://autogluon.github.io/fev/latest/
97
- - GitHub: https://github.com/autogluon/fev
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,27 +0,0 @@
1
- FROM python:3.13.5-slim
2
-
3
- RUN useradd -m -u 1000 user
4
- WORKDIR /app
5
-
6
- RUN apt-get update && apt-get install -y \
7
- build-essential \
8
- curl \
9
- git \
10
- && rm -rf /var/lib/apt/lists/*
11
-
12
- COPY --chown=user ./requirements.txt requirements.txt
13
- COPY --chown=user . /app
14
-
15
- RUN pip3 install -r requirements.txt
16
-
17
- # Results, metadata and leaderboard compute all come from the latest autogluon/fev main at build time.
18
- # The ADD changes whenever main moves, so Docker re-runs save_tables.py instead of reusing a cached
19
- # layer: rebuilding the Space picks up newly merged models.
20
- ADD https://api.github.com/repos/autogluon/fev/commits/main /tmp/fev-main.json
21
- RUN python save_tables.py main
22
-
23
- EXPOSE 8501
24
-
25
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
26
-
27
- ENTRYPOINT ["streamlit", "run", "fev-leaderboard-app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Makefile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: style format
2
+
3
+
4
+ style:
5
+ python -m black --line-length 119 .
6
+ python -m isort .
7
+ ruff check --fix .
8
+
9
+
10
+ quality:
11
+ python -m black --check --line-length 119 .
12
+ python -m isort --check-only .
13
+ ruff check .
README.md CHANGED
@@ -1,20 +1,44 @@
1
  ---
2
- title: fev-bench
3
- emoji: 🏆
4
  colorFrom: green
5
  colorTo: indigo
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Forecast evaluation benchmark
12
  license: apache-2.0
13
  ---
14
 
15
- # Welcome to Streamlit!
16
 
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
 
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Fev Leaderboard
3
+ emoji: 🥇
4
  colorFrom: green
5
  colorTo: indigo
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: true
 
 
 
9
  license: apache-2.0
10
  ---
11
 
12
+ # Start the configuration
13
 
14
+ Most of the variables to change for a default leaderboard are in `src/env.py` (replace the path for your leaderboard) and `src/about.py` (for tasks).
15
 
16
+ Results files should have the following format and be stored as json files:
17
+ ```json
18
+ {
19
+ "config": {
20
+ "model_dtype": "torch.float16", # or torch.bfloat16 or 8bit or 4bit
21
+ "model_name": "path of the model on the hub: org/model",
22
+ "model_sha": "revision on the hub",
23
+ },
24
+ "results": {
25
+ "task_name": {
26
+ "metric_name": score,
27
+ },
28
+ "task_name2": {
29
+ "metric_name": score,
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Request files are created automatically by this tool.
36
+
37
+ If you encounter problem on the space, don't hesitate to restart it to remove the create eval-queue, eval-queue-bk, eval-results and eval-results-bk created folder.
38
+
39
+ # Code logic for more complex edits
40
+
41
+ You'll find
42
+ - the main table' columns names and properties in `src/display/utils.py`
43
+ - the logic to read all results and request files, then convert them in dataframe lines, in `src/leaderboard/read_evals.py`, and `src/populate.py`
44
+ - the logic to allow or filter submissions in `src/submission/submit.py` and `src/submission/check_validity.py`
app.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fev
2
+ import gradio as gr
3
+ import pandas as pd
4
+
5
+ from src import about
6
+ from src.custom_html_js import custom_css
7
+ from src.formatting import make_clickable_model
8
+
9
+ # Load the CSV data into a pandas DataFrame
10
+ df = pd.read_csv(
11
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/seasonal_naive.csv"
12
+ )
13
+
14
+
15
+ summary_urls = [
16
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/auto_arima.csv",
17
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/auto_ets.csv",
18
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/auto_theta.csv",
19
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_base.csv",
20
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_large.csv",
21
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_mini.csv",
22
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_small.csv",
23
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_tiny.csv",
24
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_bolt_base.csv",
25
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_bolt_mini.csv",
26
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_bolt_small.csv",
27
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/chronos_bolt_tiny.csv",
28
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/moirai_base.csv",
29
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/moirai_large.csv",
30
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/moirai_small.csv",
31
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/seasonal_naive.csv",
32
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/timesfm.csv",
33
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/timesfm-2.0.csv",
34
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/ttm-r2.csv",
35
+ "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/results/tirex.csv",
36
+ ]
37
+
38
+ rename_cols = {
39
+ "gmean_relative_error": "Average relative error",
40
+ "avg_rank": "Average rank",
41
+ "median_inference_time_s": "Median inference time (s)",
42
+ "training_corpus_overlap": "Training corpus overlap (%)",
43
+ }
44
+ selected_cols = list(rename_cols.keys())
45
+
46
+
47
+ def highlight_zeroshot(styler):
48
+ """Highlight training overlap for zero-shot models with bold green."""
49
+
50
+ def style_func(val):
51
+ if val == 0:
52
+ return "color: green; font-weight: bold"
53
+ else:
54
+ return "color: black"
55
+
56
+ return styler.map(style_func, subset=["Training corpus overlap (%)"])
57
+
58
+
59
+ leaderboards = {}
60
+ for metric in ["WQL", "MASE"]:
61
+ lb = fev.leaderboard(summary_urls, metric_column=metric)[selected_cols].rename(columns=rename_cols)
62
+ lb = lb.astype("float64").round(3).reset_index()
63
+ lb["Training corpus overlap (%)"] = (lb["Training corpus overlap (%)"] * 100).round(1)
64
+ lb["model_name"] = lb["model_name"].apply(make_clickable_model)
65
+ leaderboards[metric] = highlight_zeroshot(lb.style).format(precision=3)
66
+
67
+
68
+ with gr.Blocks(css=custom_css) as demo:
69
+ gr.HTML(about.TITLE)
70
+ gr.Markdown(about.INTRODUCTION_TEXT, elem_classes="markdown-text")
71
+
72
+ with gr.Tabs(elem_classes="tab-buttons"):
73
+ with gr.Tab("🏅 Chronos Benchmark II", id=0):
74
+ with gr.Column():
75
+ gr.Markdown(about.CHRONOS_BENCHMARK, elem_classes="markdown-text")
76
+ with gr.Tabs():
77
+ with gr.Tab("📊 Probabilistic forecast (WQL)"):
78
+ gr.Markdown("""Forecast accuracy measured by Weighted Quantile Loss.""")
79
+ gr.Dataframe(
80
+ value=leaderboards["WQL"],
81
+ datatype=["markdown", "number", "number", "number"],
82
+ interactive=False,
83
+ )
84
+
85
+ with gr.Tab("📈 Point forecast (MASE)"):
86
+ gr.Markdown("""Forecast accuracy measured by Mean Absolute Scaled Error.""")
87
+ gr.Dataframe(
88
+ value=leaderboards["MASE"],
89
+ datatype=["markdown", "number", "number", "number"],
90
+ interactive=False,
91
+ )
92
+
93
+ with gr.Tab("📝 About", id=1):
94
+ gr.Markdown(about.ABOUT_LEADERBOARD)
95
+
96
+ if __name__ == "__main__":
97
+ demo.launch(ssr_mode=False)
fev-leaderboard-app.py DELETED
@@ -1,9 +0,0 @@
1
- import streamlit as st
2
-
3
- pages = [
4
- st.Page("pages/fev_bench.py", title="fev-bench", icon=":material/trophy:"),
5
- st.Page("pages/about.py", title="About", icon=":material/info:"),
6
- ]
7
-
8
- page = st.navigation(pages)
9
- page.run()
 
 
 
 
 
 
 
 
 
 
pages/about.py DELETED
@@ -1,34 +0,0 @@
1
- import json
2
- from pathlib import Path
3
-
4
- import streamlit as st
5
-
6
- # Written by save_tables.py, which the Docker build runs against the latest autogluon/fev main
7
- SOURCE_JSON = Path(__file__).resolve().parent.parent / "tables" / "source.json"
8
-
9
- ABOUT_LEADERBOARD = """
10
- ## About
11
-
12
- [**fev**](https://github.com/autogluon/fev) is a lightweight wrapper around the 🤗 [datasets](https://huggingface.co/docs/datasets/en/index) library designed to streamline
13
- benchmarking of time series forecasting models.
14
-
15
- ### 📚 Resources
16
- - **Documentation**: [Official docs](https://autogluon.github.io/fev/latest/)
17
- - **Publication**: ["fev-bench: A Realistic Benchmark for Time Series Forecasting"](https://arxiv.org/abs/2509.26468)
18
- - **Source Code**: [GitHub repository](https://github.com/autogluon/fev)
19
- - **Issues & Questions**: [GitHub Issues](https://github.com/autogluon/fev/issues)
20
-
21
- ### 🚀 Submit Your Model
22
- Ready to add your model to the leaderboard? Follow this [tutorial](https://autogluon.github.io/fev/latest/tutorials/05-add-your-model/) to evaluate your model with fev and contribute your results.
23
- """
24
- st.set_page_config(layout="wide", page_title="About FEV", page_icon=":material/info:")
25
- st.markdown(ABOUT_LEADERBOARD)
26
-
27
- source = json.loads(SOURCE_JSON.read_text())
28
- commit_url = f"https://github.com/autogluon/fev/tree/{source['commit']}"
29
- st.markdown(
30
- f"""
31
- ### 🔁 Reproducibility
32
- Results shown are from [autogluon/fev@{source['commit']}]({commit_url}), generated on {source['generated_at'][:10]}.
33
- """
34
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/fev_bench.py DELETED
@@ -1,353 +0,0 @@
1
- import sys
2
- from pathlib import Path
3
-
4
- sys.path.append(str(Path(__file__).parent))
5
-
6
- import pandas as pd
7
- import streamlit as st
8
- from src.strings import (
9
- CITATION_FEV,
10
- CITATION_HEADER,
11
- FEV_BENCHMARK_DETAILS,
12
- PAIRWISE_BENCHMARK_DETAILS,
13
- get_pivot_legend,
14
- )
15
- from src.task_groups import (
16
- ALL_TASKS,
17
- DOMAIN_GROUPS,
18
- FREQUENCY_GROUPS,
19
- MINI_TASKS,
20
- TASK_TYPE_GROUPS,
21
- )
22
- from src.utils import (
23
- COLORS,
24
- DEFAULT_VISIBLE_MODEL_TYPES,
25
- MODEL_TYPE_LABELS,
26
- TAG_COLORS,
27
- construct_pairwise_chart,
28
- format_leaderboard,
29
- format_metric_name,
30
- get_metric_description,
31
- get_model_display_name,
32
- select_model_names,
33
- validate_model_metadata,
34
- )
35
-
36
- # Leaderboard computation vendored from the fev repo by save_tables.py, so the numbers shown here are
37
- # produced by the same code as the paper figures.
38
- from src.vendor.fev_bench_compute import (
39
- AVAILABLE_METRICS,
40
- BASELINE_MODEL,
41
- LEAKAGE_IMPUTATION_MODEL,
42
- SORT_COL,
43
- TOP_K_MODELS_TO_PLOT,
44
- compute_leaderboard,
45
- compute_pairwise,
46
- )
47
- from streamlit.elements.lib.column_types import ColumnConfig
48
-
49
- st.set_page_config(
50
- layout="wide", page_title="fev leaderboard", page_icon=":material/trophy:"
51
- )
52
-
53
- TITLE = "<h1 style='text-align: center; font-size: 350%;'>fev-bench</h1>"
54
-
55
- MODEL_TYPES_HELP = (
56
- "Win rates are computed among the included models. "
57
- f"{BASELINE_MODEL} and {LEAKAGE_IMPUTATION_MODEL} are always included, as they are used for imputation. "
58
- 'Click "See details" below the leaderboard for a description of each model type.'
59
- )
60
- NON_COMMERCIAL_HELP = "Include models whose license does not permit commercial use."
61
-
62
- # Group type options
63
- GROUP_TYPES = [
64
- "Full (100 tasks)",
65
- "Mini (20 tasks)",
66
- "By frequency",
67
- "By domain",
68
- "By task type",
69
- ]
70
- FREQUENCY_OPTIONS = list(FREQUENCY_GROUPS.keys())
71
- DOMAIN_OPTIONS = list(DOMAIN_GROUPS.keys())
72
- TASK_TYPE_OPTIONS = list(TASK_TYPE_GROUPS.keys())
73
-
74
-
75
- def get_subset_description(
76
- group_type: str, subgroup: str | None, num_tasks: int, metric_name: str
77
- ) -> str:
78
- """Generate a description of the current subset."""
79
- base = f"Results for various forecasting models, measured using the **{metric_name}** metric, on **{num_tasks} tasks**"
80
- if group_type == "Full (100 tasks)":
81
- subset_desc = "from the full **fev-bench** benchmark"
82
- elif group_type == "Mini (20 tasks)":
83
- subset_desc = "from the **fev-bench-mini** subset"
84
- elif group_type == "By frequency":
85
- subset_desc = f"with **{subgroup.lower()}** frequency"
86
- elif group_type == "By task type":
87
- subset_desc = f"of type **{subgroup.lower()}**"
88
- else: # By domain
89
- subset_desc = f"from the **{subgroup}** domain"
90
- paper_link = "[fev-bench: A Realistic Benchmark for Time Series Forecasting](https://arxiv.org/abs/2509.26468)"
91
- return f"{base} {subset_desc}, as described in {paper_link}."
92
-
93
-
94
- @st.cache_data()
95
- def get_summaries() -> pd.DataFrame:
96
- return pd.read_csv("tables/summaries.csv")
97
-
98
-
99
- @st.cache_data()
100
- def get_leaderboard(
101
- metric_name: str, task_list: tuple[str, ...], model_names: tuple[str, ...]
102
- ) -> pd.DataFrame:
103
- """Leaderboard for the selected tasks and models.
104
-
105
- Win rate and skill score depend on which models are included, so these are computed here rather
106
- than precomputed: including an extra model type changes every row.
107
- """
108
- return compute_leaderboard(_filter_summaries(metric_name, task_list, model_names), metric_name)
109
-
110
-
111
- @st.cache_data()
112
- def get_pairwise(
113
- metric_name: str, task_list: tuple[str, ...], model_names: tuple[str, ...]
114
- ) -> pd.DataFrame:
115
- summaries = _filter_summaries(metric_name, task_list, model_names)
116
- leaderboard_df = get_leaderboard(metric_name, task_list, model_names)
117
- top_k_models = (
118
- leaderboard_df.sort_values(by=SORT_COL, ascending=False)
119
- .head(TOP_K_MODELS_TO_PLOT)["model_name"]
120
- .tolist()
121
- )
122
- return compute_pairwise(summaries, metric_name, top_k_models)
123
-
124
-
125
- def _filter_summaries(
126
- metric_name: str, task_list: tuple[str, ...], model_names: tuple[str, ...]
127
- ) -> pd.DataFrame:
128
- summaries = get_summaries()
129
- # The baseline and leakage-imputation models must stay in the data for imputation to work, even
130
- # if the reader excluded their type.
131
- included = set(model_names) | {BASELINE_MODEL, LEAKAGE_IMPUTATION_MODEL}
132
- return summaries[
133
- summaries["task_name"].isin(task_list) & summaries["model_name"].isin(included)
134
- ]
135
-
136
-
137
- @st.cache_data()
138
- def get_pivot_table(
139
- metric_name: str,
140
- ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
141
- pivot_df = pd.read_csv(f"tables/pivot_{metric_name}.csv")
142
- baseline_imputed = pd.read_csv(f"tables/pivot_{metric_name}_baseline_imputed.csv")
143
- leakage_imputed = pd.read_csv(f"tables/pivot_{metric_name}_leakage_imputed.csv")
144
- return pivot_df, baseline_imputed, leakage_imputed
145
-
146
-
147
- with st.sidebar:
148
- # Task group selection
149
- selected_group_type = st.selectbox("Subset", options=GROUP_TYPES)
150
-
151
- # Conditional sub-selection for frequency/domain
152
- selected_subgroup = None
153
- if selected_group_type == "By frequency":
154
- selected_subgroup = st.selectbox("Frequency", options=FREQUENCY_OPTIONS)
155
- elif selected_group_type == "By domain":
156
- selected_subgroup = st.selectbox("Domain", options=DOMAIN_OPTIONS)
157
- elif selected_group_type == "By task type":
158
- selected_subgroup = st.selectbox("Task type", options=TASK_TYPE_OPTIONS)
159
-
160
- # Determine the tasks making up the selected subset
161
- if selected_group_type in ["Full (100 tasks)", "Mini (20 tasks)"]:
162
- task_list = (
163
- ALL_TASKS if selected_group_type == "Full (100 tasks)" else MINI_TASKS
164
- )
165
- else:
166
- if selected_group_type == "By frequency":
167
- task_list = FREQUENCY_GROUPS[selected_subgroup]
168
- elif selected_group_type == "By task type":
169
- task_list = TASK_TYPE_GROUPS[selected_subgroup]
170
- else:
171
- task_list = DOMAIN_GROUPS[selected_subgroup]
172
-
173
- st.caption(f"{len(task_list)} tasks")
174
-
175
- st.divider()
176
-
177
- selected_metric = st.selectbox(
178
- "Evaluation Metric", options=AVAILABLE_METRICS, format_func=format_metric_name
179
- )
180
- st.caption(get_metric_description(selected_metric))
181
-
182
- st.divider()
183
-
184
- included_model_types = st.pills(
185
- "Model types",
186
- options=list(MODEL_TYPE_LABELS),
187
- format_func=MODEL_TYPE_LABELS.get,
188
- selection_mode="multi",
189
- default=list(DEFAULT_VISIBLE_MODEL_TYPES),
190
- help=MODEL_TYPES_HELP,
191
- )
192
- include_non_commercial = st.toggle(
193
- "Include non-commercial models", value=True, help=NON_COMMERCIAL_HELP
194
- )
195
- included_models = tuple(
196
- select_model_names(included_model_types, include_non_commercial)
197
- )
198
- if included_models:
199
- # The imputation models are always part of the computation, so count them too
200
- num_models = len(
201
- set(included_models) | {BASELINE_MODEL, LEAKAGE_IMPUTATION_MODEL}
202
- )
203
- st.caption(f"{num_models} models")
204
-
205
- cols = st.columns(spec=[0.025, 0.95, 0.025])
206
-
207
- with cols[1] as main_container:
208
- st.markdown(TITLE, unsafe_allow_html=True)
209
-
210
- if not included_models:
211
- st.warning("No models match the current selection. Select at least one model type in the sidebar.")
212
- st.stop()
213
-
214
- task_names = tuple(task_list)
215
- metric_df = get_leaderboard(selected_metric, task_names, included_models).sort_values(
216
- by=SORT_COL, ascending=False
217
- )
218
- validate_model_metadata(metric_df["model_name"])
219
-
220
- pairwise_df = get_pairwise(selected_metric, task_names, included_models)
221
-
222
- st.markdown("## :material/trophy: Leaderboard", unsafe_allow_html=True)
223
- st.markdown(
224
- get_subset_description(
225
- selected_group_type, selected_subgroup, len(task_list), selected_metric
226
- ),
227
- unsafe_allow_html=True,
228
- )
229
- df_styled = format_leaderboard(metric_df)
230
- st.dataframe(
231
- df_styled,
232
- width="stretch",
233
- hide_index=True,
234
- column_config={
235
- "model_name": st.column_config.LinkColumn(
236
- label="Model", display_text=r"#(.*)$"
237
- ),
238
- "win_rate": st.column_config.NumberColumn(
239
- label="Win rate (%)", format="%.1f"
240
- ),
241
- "skill_score": st.column_config.NumberColumn(
242
- label="Skill score (%)", format="%.1f"
243
- ),
244
- "median_inference_time_s_per100": st.column_config.NumberColumn(
245
- label="Runtime (s / 100 series)",
246
- format="%.1f",
247
- help="Median end-to-end runtime per 100 time series.",
248
- ),
249
- "training_corpus_overlap": st.column_config.NumberColumn(
250
- label="Leakage (%)", format="%d"
251
- ),
252
- "num_failures": st.column_config.NumberColumn(
253
- label="Failed (%)",
254
- format="%.0f",
255
- help="Percentage of tasks where the model failed to produce a forecast.",
256
- ),
257
- "tags": st.column_config.MultiselectColumn(
258
- label="Tags",
259
- options=list(TAG_COLORS),
260
- color=list(TAG_COLORS.values()),
261
- help="Model type; Zero-shot = no training on the task; Non-commercial = license does not permit commercial use.",
262
- ),
263
- "org": ColumnConfig(label="Organization", alignment="left"),
264
- },
265
- )
266
-
267
- with st.expander("See details"):
268
- st.markdown(FEV_BENCHMARK_DETAILS, unsafe_allow_html=True)
269
-
270
- st.markdown("## :material/bar_chart: Pairwise comparison", unsafe_allow_html=True)
271
- chart_col_1, _, chart_col_2 = st.columns(spec=[0.45, 0.1, 0.45])
272
-
273
- with chart_col_1:
274
- st.altair_chart(
275
- construct_pairwise_chart(
276
- pairwise_df, col="win_rate", metric_name=selected_metric
277
- ),
278
- use_container_width=True,
279
- )
280
-
281
- with chart_col_2:
282
- st.altair_chart(
283
- construct_pairwise_chart(
284
- pairwise_df, col="skill_score", metric_name=selected_metric
285
- ),
286
- use_container_width=True,
287
- )
288
-
289
- with st.expander("See details"):
290
- st.markdown(PAIRWISE_BENCHMARK_DETAILS, unsafe_allow_html=True)
291
-
292
- st.markdown(
293
- "## :material/table_chart: Results for individual tasks", unsafe_allow_html=True
294
- )
295
- with st.expander("Show detailed results"):
296
- st.markdown(
297
- get_pivot_legend("Seasonal Naive", "Chronos-Bolt"), unsafe_allow_html=True
298
- )
299
- pivot_df, baseline_imputed, leakage_imputed = get_pivot_table(selected_metric)
300
- pivot_df = pivot_df.set_index("Task name")
301
- baseline_imputed = baseline_imputed.set_index("Task name")
302
- leakage_imputed = leakage_imputed.set_index("Task name")
303
-
304
- # Keep only the models included by the sidebar toggles
305
- excluded_cols = [c for c in pivot_df.columns if c not in included_models]
306
- pivot_df = pivot_df.drop(columns=excluded_cols)
307
- baseline_imputed = baseline_imputed.drop(columns=excluded_cols)
308
- leakage_imputed = leakage_imputed.drop(columns=excluded_cols)
309
-
310
- # Filter pivot table to only show tasks in the selected group
311
- available_tasks = [t for t in task_list if t in pivot_df.index]
312
- pivot_df = pivot_df.loc[available_tasks]
313
- baseline_imputed = baseline_imputed.loc[available_tasks]
314
- leakage_imputed = leakage_imputed.loc[available_tasks]
315
-
316
- # Apply display-name overrides to model columns
317
- col_rename = {c: get_model_display_name(c) for c in pivot_df.columns}
318
- pivot_df = pivot_df.rename(columns=col_rename)
319
- baseline_imputed = baseline_imputed.rename(columns=col_rename)
320
- leakage_imputed = leakage_imputed.rename(columns=col_rename)
321
-
322
- def style_pivot_table(errors, is_baseline_imputed, is_leakage_imputed):
323
- rank_colors = {1: COLORS["gold"], 2: COLORS["silver"], 3: COLORS["bronze"]}
324
-
325
- def highlight_by_position(styler):
326
- for row_idx in errors.index:
327
- row_ranks = errors.loc[row_idx].rank(method="min")
328
- for col_idx in errors.columns:
329
- rank = row_ranks[col_idx]
330
- style_parts = []
331
- if rank <= 3:
332
- style_parts.append(f"background-color: {rank_colors[rank]}")
333
- if is_leakage_imputed.loc[row_idx, col_idx]:
334
- style_parts.append(f"color: {COLORS['leakage_impute']}")
335
- elif is_baseline_imputed.loc[row_idx, col_idx]:
336
- style_parts.append(f"color: {COLORS['failure_impute']}")
337
- elif not style_parts:
338
- style_parts.append(f"color: {COLORS['text_default']}")
339
- if style_parts:
340
- styler = styler.map(
341
- lambda x, s="; ".join(style_parts): s,
342
- subset=pd.IndexSlice[row_idx:row_idx, col_idx:col_idx],
343
- )
344
- return styler
345
-
346
- return highlight_by_position(errors.style).format(precision=3)
347
-
348
- st.dataframe(style_pivot_table(pivot_df, baseline_imputed, leakage_imputed))
349
-
350
- st.divider()
351
- st.markdown("### :material/format_quote: Citation", unsafe_allow_html=True)
352
- st.markdown(CITATION_HEADER)
353
- st.markdown(CITATION_FEV)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -1,12 +1,13 @@
1
- [project]
2
- name = "hf-leaderboard"
3
- version = "0.1.0"
4
- requires-python = ">=3.11"
5
- dependencies = [
6
- "altair>=6.0.0",
7
- "fev>=0.7.0",
8
- "numpy<2.2",
9
- "pyarrow<21",
10
- "scipy<1.15",
11
- "streamlit>=1.53.1",
12
- ]
 
 
1
+ [tool.ruff]
2
+ # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
3
+ select = ["E", "F"]
4
+ ignore = ["E501"] # line too long (black is taking care of this)
5
+ line-length = 119
6
+ fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
7
+
8
+ [tool.isort]
9
+ profile = "black"
10
+ line_length = 119
11
+
12
+ [tool.black]
13
+ line-length = 119
requirements.txt CHANGED
@@ -1,7 +1,11 @@
 
 
 
 
 
 
 
1
  matplotlib
2
  numpy
3
  pandas
4
- requests
5
- streamlit==1.53.1
6
- fev>=0.6.0
7
- altair>=5.5.0
 
1
+ APScheduler
2
+ black
3
+ datasets
4
+ gradio
5
+ gradio[oauth]
6
+ gradio_client
7
+ huggingface-hub>=0.18.0
8
  matplotlib
9
  numpy
10
  pandas
11
+ fev==0.4.0
 
 
 
save_tables.py DELETED
@@ -1,240 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Refresh the leaderboard's data from the autogluon/fev repo.
3
-
4
- One command regenerates everything the app needs from a single fev commit:
5
-
6
- uv run python save_tables.py # latest main
7
- uv run python save_tables.py abc123 # a specific commit
8
- uv run python save_tables.py --fev-repo ../fev # an existing local checkout
9
-
10
- Writes into ``tables/``:
11
-
12
- summaries.csv raw evaluation summaries (the app computes leaderboards from these)
13
- models.csv display metadata, from the fev repo's benchmarks/fev_bench/models.yaml
14
- pivot_<metric>.csv per-task error matrix (+ `_baseline_imputed` / `_leakage_imputed` masks)
15
- source.json which fev commit the tables came from
16
-
17
- and vendors the fev repo's leaderboard computation into ``src/vendor/`` so the app's win rates and
18
- pairwise comparisons are produced by exactly the code at that commit (the same code that generates
19
- the paper figures). Leaderboard and pairwise tables are *not* precomputed: they depend on which
20
- model types the reader includes, so the app computes them on the fly and caches the result.
21
- """
22
-
23
- from __future__ import annotations
24
-
25
- import argparse
26
- import importlib.util
27
- import json
28
- import shutil
29
- import subprocess
30
- import sys
31
- import tempfile
32
- from datetime import datetime, timezone
33
- from pathlib import Path
34
- from types import ModuleType
35
-
36
- import pandas as pd
37
-
38
- GITHUB_REPO = "https://github.com/autogluon/fev.git"
39
- TABLES_DIR = Path(__file__).parent / "tables"
40
- # Modules copied out of the fev repo so the app runs the same compute logic as the pinned commit.
41
- VENDOR_DIR = Path(__file__).parent / "src" / "vendor"
42
- VENDORED_MODULES = {"fev_bench_compute.py": "scripts/generate_fev_bench_figures.py"}
43
- VENDOR_HEADER = """# Generated file - do not edit.
44
- # Vendored from {source} in autogluon/fev at commit {commit} by save_tables.py.
45
- # Regenerate with: uv run python save_tables.py [commit]
46
- """
47
-
48
-
49
- def clone_fev_repo(commit: str, dest: Path) -> None:
50
- """Shallow-fetch a single fev commit (works for both branch names and SHAs)."""
51
- print(f"Fetching {GITHUB_REPO} at {commit}...")
52
- run = lambda *args: subprocess.run(args, cwd=dest, check=True, stdout=subprocess.DEVNULL) # noqa: E731
53
- run("git", "init", "--quiet")
54
- run("git", "remote", "add", "origin", GITHUB_REPO)
55
- run("git", "fetch", "--depth", "1", "--quiet", "origin", commit)
56
- run("git", "checkout", "--quiet", "FETCH_HEAD")
57
-
58
-
59
- def resolved_commit(fev_repo: Path) -> str:
60
- """Short SHA of the checkout, so the generated tables record their exact source."""
61
- result = subprocess.run(
62
- ["git", "rev-parse", "--short", "HEAD"], cwd=fev_repo, capture_output=True, text=True, check=True
63
- )
64
- return result.stdout.strip()
65
-
66
-
67
- def load_module_from(path: Path, name: str) -> ModuleType:
68
- """Import a module from a file path, with its directory importable (for sibling imports)."""
69
- sys.path.insert(0, str(path.parent))
70
- spec = importlib.util.spec_from_file_location(name, path)
71
- module = importlib.util.module_from_spec(spec)
72
- # Registered before executing so that pydantic models in the module can resolve their annotations
73
- sys.modules[name] = module
74
- spec.loader.exec_module(module)
75
- return module
76
-
77
-
78
- def vendor_compute_modules(fev_repo: Path, commit: str) -> None:
79
- """Copy the fev repo's compute logic into src/vendor/ with a generated-file header."""
80
- VENDOR_DIR.mkdir(parents=True, exist_ok=True)
81
- (VENDOR_DIR / "__init__.py").write_text("")
82
- for target, source in VENDORED_MODULES.items():
83
- source_path = fev_repo / source
84
- if not source_path.exists():
85
- raise FileNotFoundError(f"Expected {source} in the fev checkout at {fev_repo}")
86
- header = VENDOR_HEADER.format(source=source, commit=commit)
87
- (VENDOR_DIR / target).write_text(header + "\n" + source_path.read_text())
88
- print(f"Vendored: src/vendor/{target} (from {source})")
89
-
90
-
91
- def load_model_metadata(fev_repo: Path) -> pd.DataFrame:
92
- """Load benchmarks/fev_bench/models.yaml, validated against the results files."""
93
- metadata_module = load_module_from(fev_repo / "scripts" / "fev_bench_metadata.py", "fev_bench_metadata")
94
- metadata = metadata_module.load_model_metadata(
95
- fev_repo / "benchmarks" / "fev_bench" / "results",
96
- fev_repo / "benchmarks" / "fev_bench" / "models.yaml",
97
- )
98
- columns = [
99
- "model_name",
100
- "display_name",
101
- "organization",
102
- "url",
103
- "model_type",
104
- "zero_shot",
105
- "commercial_use",
106
- "hidden",
107
- "results_file",
108
- ]
109
- return pd.DataFrame(metadata.values())[columns].sort_values("model_name")
110
-
111
-
112
- def load_summaries(results_dir: Path) -> pd.DataFrame:
113
- csv_files = sorted(results_dir.glob("*.csv"))
114
- if not csv_files:
115
- raise FileNotFoundError(f"No CSV files found in {results_dir}")
116
- print(f"Loading {len(csv_files)} result files from {results_dir}")
117
- return pd.concat([pd.read_csv(f) for f in csv_files], ignore_index=True)
118
-
119
-
120
- def compute_pivot_table(
121
- summaries: pd.DataFrame,
122
- metric_name: str,
123
- baseline_model: str,
124
- leakage_imputation_model: str,
125
- ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
126
- import fev
127
-
128
- errors = fev.pivot_table(summaries=summaries, metric_column=metric_name, task_columns=["task_name"])
129
- train_overlap = (
130
- fev.pivot_table(summaries=summaries, metric_column="trained_on_this_dataset", task_columns=["task_name"])
131
- .fillna(False)
132
- .astype(bool)
133
- )
134
-
135
- is_imputed_baseline = errors.isna()
136
- is_leakage_imputed = train_overlap
137
-
138
- # Handle imputations
139
- errors = errors.mask(train_overlap, errors[leakage_imputation_model], axis=0)
140
- for col in errors.columns:
141
- if col != baseline_model:
142
- errors[col] = errors[col].fillna(errors[baseline_model])
143
-
144
- errors = errors[errors.rank(axis=1).mean().sort_values().index]
145
- is_imputed_baseline = is_imputed_baseline[errors.columns]
146
- is_leakage_imputed = is_leakage_imputed[errors.columns]
147
-
148
- for df in (errors, is_imputed_baseline, is_leakage_imputed):
149
- df.index.rename("Task name", inplace=True)
150
-
151
- return errors.reset_index(), is_imputed_baseline.reset_index(), is_leakage_imputed.reset_index()
152
-
153
-
154
- def main() -> int:
155
- parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
156
- parser.add_argument(
157
- "commit",
158
- nargs="?",
159
- default="main",
160
- help="Commit SHA or branch in autogluon/fev to generate the tables from (default: main)",
161
- )
162
- parser.add_argument(
163
- "--fev-repo",
164
- type=Path,
165
- default=None,
166
- help="Use an existing local fev checkout instead of fetching from GitHub (ignores `commit`)",
167
- )
168
- args = parser.parse_args()
169
-
170
- with tempfile.TemporaryDirectory() as tmp_dir:
171
- if args.fev_repo is not None:
172
- fev_repo = args.fev_repo.resolve()
173
- print(f"Using local fev checkout: {fev_repo}")
174
- else:
175
- fev_repo = Path(tmp_dir)
176
- clone_fev_repo(args.commit, fev_repo)
177
- commit = resolved_commit(fev_repo)
178
-
179
- summaries = load_summaries(fev_repo / "benchmarks" / "fev_bench" / "results")
180
- models = load_model_metadata(fev_repo)
181
- vendor_compute_modules(fev_repo, commit)
182
- compute = load_module_from(VENDOR_DIR / "fev_bench_compute.py", "fev_bench_compute")
183
-
184
- # Models hidden from the leaderboard are dropped before any metric is computed, so that they do
185
- # not influence other models' win rates (same as the paper figures).
186
- hidden_models = models.loc[models["hidden"], "model_name"].tolist()
187
- if hidden_models:
188
- print(f"Dropping {len(hidden_models)} hidden models: {hidden_models}")
189
- summaries = summaries[~summaries["model_name"].isin(hidden_models)]
190
- # `hidden` is not written out: the app only ever sees models it is meant to display.
191
- models = models[~models["hidden"]].drop(columns="hidden")
192
-
193
- missing_metadata = sorted(set(summaries["model_name"]) - set(models["model_name"]))
194
- if missing_metadata:
195
- raise ValueError(f"Summaries contain models without metadata: {missing_metadata}")
196
-
197
- TABLES_DIR.mkdir(exist_ok=True)
198
- summaries.to_csv(TABLES_DIR / "summaries.csv", index=False)
199
- print(f"Saved: summaries.csv ({summaries['model_name'].nunique()} models, {summaries['task_name'].nunique()} tasks)")
200
- models.to_csv(TABLES_DIR / "models.csv", index=False)
201
- print(f"Saved: models.csv ({len(models)} models)")
202
-
203
- for metric in compute.AVAILABLE_METRICS:
204
- pivot_df, baseline_imputed, leakage_imputed = compute_pivot_table(
205
- summaries, metric, compute.BASELINE_MODEL, compute.LEAKAGE_IMPUTATION_MODEL
206
- )
207
- pivot_df.to_csv(TABLES_DIR / f"pivot_{metric}.csv", index=False)
208
- baseline_imputed.to_csv(TABLES_DIR / f"pivot_{metric}_baseline_imputed.csv", index=False)
209
- leakage_imputed.to_csv(TABLES_DIR / f"pivot_{metric}_leakage_imputed.csv", index=False)
210
- print(f"Saved: pivot_{metric}.csv")
211
-
212
- (TABLES_DIR / "source.json").write_text(
213
- json.dumps(
214
- {
215
- "repo": GITHUB_REPO,
216
- "commit": commit,
217
- "requested": args.commit if args.fev_repo is None else str(args.fev_repo),
218
- "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
219
- },
220
- indent=2,
221
- )
222
- + "\n"
223
- )
224
- print(f"Saved: source.json (fev commit {commit})")
225
-
226
- # Leaderboard and pairwise tables used to be precomputed per task group; the app now computes
227
- # them so the reader can include/exclude model types. Clean up anything left from older runs.
228
- for stale in sorted(p for p in TABLES_DIR.iterdir() if p.is_dir()):
229
- shutil.rmtree(stale)
230
- print(f"Removed stale table directory: {stale.name}/")
231
- for stale in sorted(TABLES_DIR.glob("leaderboard_*.csv")) + sorted(TABLES_DIR.glob("pairwise_*.csv")):
232
- stale.unlink()
233
- print(f"Removed stale table: {stale.name}")
234
-
235
- print(f"\nAll tables written to {TABLES_DIR}/")
236
- return 0
237
-
238
-
239
- if __name__ == "__main__":
240
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/about.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TITLE = """<h1 align="center" id="space-title">Forecast evaluation leaderboard</h1>"""
2
+
3
+ # What does your leaderboard evaluate?
4
+ INTRODUCTION_TEXT = """
5
+ This space hosts evaluation results for time series forecasting models.
6
+
7
+ The results are obtained using [fev](https://github.com/autogluon/fev) - a lightweight library for evaluating time series forecasting models.
8
+ """
9
+
10
+ ABOUT_LEADERBOARD = """
11
+ ## What is `fev`?
12
+
13
+ [`fev`](https://github.com/autogluon/fev) is a lightweight wrapper around the 🤗 [`datasets`](https://huggingface.co/docs/datasets/en/index) library that makes it easy to benchmark time series forecasting models.
14
+
15
+ For more information about `fev`, please check out [github.com/autogluon/fev](https://github.com/autogluon/fev).
16
+
17
+ Currently, the results in this space are a minimal proof of concept. We plan to add new benchmark datasets and tasks in the future.
18
+
19
+ ## How is `fev` different from other benchmarking tools?
20
+ Existing forecasting benchmarks usually fall into one of two categories:
21
+
22
+ - Standalone datasets without any supporting infrastructure. These provide no guarantees that the results obtained by different users are comparable. For example, changing the start date or duration of the forecast horizon totally changes the meaning of the scores.
23
+ - Bespoke end-to-end systems that combine models, datasets and forecasting tasks. Such packages usually come with lots of dependencies and assumptions, which makes extending or integrating these libraries into existing systems difficult.
24
+
25
+ `fev` aims for the middle ground - it provides the core benchmarking functionality without introducing unnecessary constraints or bloated dependencies. The library supports point & probabilistic forecasting, different types of covariates, as well as all popular forecasting metrics.
26
+
27
+
28
+ ## Submitting your model
29
+ For instructions on how to evaluate your model using `fev` and contribute your results to the leaderboard, please follow the [instructions in the GitHub repo](https://github.com/autogluon/fev/blob/main/docs/04-models.ipynb).
30
+ """
31
+
32
+ CHRONOS_BENCHMARK = """
33
+ ## Chronos Benchmark II results
34
+
35
+ This tab contains results for various forecasting models on the 27 datasets used in Benchmark II in the publication [Chronos: Learning the Language of Time Series](https://arxiv.org/abs/2403.07815).
36
+
37
+ These datasets were used for zero-shot evaluation of Chronos models (i.e., Chronos models were not trained on these datasets), but some other models did include certain datasets in their training corpus.
38
+
39
+ Each table contains the following information:
40
+
41
+ * **Average relative error**: Geometric mean of the relative errors for each task. The relative error for each task is computed as `model_error / baseline_error`.
42
+ * **Average rank**: Arithmetic mean of the ranks achieved by each model on each task.
43
+ * **Median inference time (s)**: Median of the times required to make predictions for the entire dataset (in seconds).
44
+ * **Training corpus overlap (%)**: Percentage of the datasets used in the benchmark that were included in the model's training corpus. Zero-shot models are highlighted in <span style="color:green; font-weight:bold;">green</span>.
45
+
46
+ Lower values are better for all of the above metrics.
47
+
48
+ Task definitions and the detailed results are available on [GitHub](https://github.com/autogluon/fev/tree/main/benchmarks/chronos_zeroshot). More information for the datasets is available in [Table 3 of the paper](https://arxiv.org/abs/2403.07815).
49
+
50
+ """
src/colors.py DELETED
@@ -1,6 +0,0 @@
1
- # Legacy colors - kept for backward compatibility if needed elsewhere
2
- VERY_PALE_PURPLE = "#e8d9f3"
3
- VERY_PALE_GREEN = "#cffdbc"
4
- VERY_PALE_BLUE = "#d6fffe"
5
- DEEP_LAVENDER = "#8d5eb7"
6
- GRASS_GREEN = "#3f9b0b"
 
 
 
 
 
 
 
src/custom_html_js.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ custom_css = """
2
+
3
+ .markdown-text {
4
+ font-size: 20px !important;
5
+ }
6
+
7
+ """
8
+
9
+
10
+ # .tab-buttons button {
11
+ # font-size: 20px;
12
+ # }
13
+
14
+ # #citation-button span {
15
+ # font-size: 16px !important;
16
+ # }
17
+
18
+ # #citation-button textarea {
19
+ # font-size: 16px !important;
20
+ # }
21
+
22
+ # #citation-button > label > button {
23
+ # margin: 6px;
24
+ # transform: scale(1.3);
25
+ # }
26
+
27
+
28
+ # #leaderboard-table-lite {
29
+ # margin-top: 15px
30
+ # }
31
+
32
+ # #search-bar-table-box > div:first-child {
33
+ # background: none;
34
+ # border: none;
35
+ # }
36
+
37
+ # #search-bar {
38
+ # padding: 0px;
39
+ # }
40
+
41
+ # /* Hides the final AutoEvalColumn */
42
+ # #llm-benchmark-tab-table table td:last-child,
43
+ # #llm-benchmark-tab-table table th:last-child {
44
+ # display: none;
45
+ # }
46
+
47
+ # /* Limit the width of the first AutoEvalColumn so that names don't expand too much */
48
+ # table td:first-child,
49
+ # table th:first-child {
50
+ # max-width: 400px;
51
+ # overflow: auto;
52
+ # white-space: nowrap;
53
+ # }
54
+
55
+
56
+ # #scale-logo {
57
+ # border-style: none !important;
58
+ # box-shadow: none;
59
+ # display: block;
60
+ # margin-left: auto;
61
+ # margin-right: auto;
62
+ # max-width: 600px;
63
+ # }
64
+
65
+ # #scale-logo .download {
66
+ # display: none;
67
+ # }
68
+ # #filter_type{
69
+ # border: 0;
70
+ # padding-left: 0;
71
+ # padding-top: 0;
72
+ # }
73
+ # #filter_type label {
74
+ # display: flex;
75
+ # }
76
+ # #filter_type label > span{
77
+ # margin-top: var(--spacing-lg);
78
+ # margin-right: 0.5em;
79
+ # }
80
+ # #filter_type label > .wrap{
81
+ # width: 103px;
82
+ # }
83
+ # #filter_type label > .wrap .wrap-inner{
84
+ # padding: 2px;
85
+ # }
86
+ # #filter_type label > .wrap .wrap-inner input{
87
+ # width: 1px
88
+ # }
89
+ # #filter-columns-type{
90
+ # border:0;
91
+ # padding:0.5;
92
+ # }
93
+ # #filter-columns-size{
94
+ # border:0;
95
+ # padding:0.5;
96
+ # }
97
+ # #box-filter > .form{
98
+ # border: 0
99
+ # }
src/formatting.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def model_hyperlink(link, model_name):
2
+ return f'<a target="_blank" href="{link}" style="color: var(--link-text-color); text-decoration: underline;text-decoration-style: dotted;">{model_name}</a>'
3
+
4
+
5
+ MODEL_URLS = {
6
+ "chronos_tiny": "amazon/chronos-t5-tiny",
7
+ "chronos_mini": "amazon/chronos-t5-mini",
8
+ "chronos_small": "amazon/chronos-t5-small",
9
+ "chronos_base": "amazon/chronos-t5-base",
10
+ "chronos_large": "amazon/chronos-t5-large",
11
+ "chronos_bolt_tiny": "amazon/chronos-bolt-tiny",
12
+ "chronos_bolt_mini": "amazon/chronos-bolt-mini",
13
+ "chronos_bolt_small": "amazon/chronos-bolt-small",
14
+ "chronos_bolt_base": "amazon/chronos-bolt-base",
15
+ "moirai_large": "Salesforce/moirai-1.1-R-large",
16
+ "moirai_base": "Salesforce/moirai-1.1-R-base",
17
+ "moirai_small": "Salesforce/moirai-1.1-R-small",
18
+ "timesfm": "google/timesfm-1.0-200m-pytorch",
19
+ "timesfm-2.0": "google/timesfm-2.0-500m-pytorch",
20
+ "ttm-r2": "ibm-granite/granite-timeseries-ttm-r2",
21
+ "tirex": "NX-AI/TiRex",
22
+ }
23
+
24
+
25
+ def make_clickable_model(model_name):
26
+ if model_name in MODEL_URLS:
27
+ model_path = MODEL_URLS.get(model_name)
28
+ link = f"https://huggingface.co/{model_path}"
29
+ return model_hyperlink(link, model_name)
30
+ else:
31
+ return model_name
src/streamlit_app.py DELETED
@@ -1,9 +0,0 @@
1
- import streamlit as st
2
-
3
- pages = [
4
- st.Page("../pages/fev_bench.py", title="fev-bench", icon=":material/trophy:"),
5
- st.Page("../pages/about.py", title="About", icon=":material/info:"),
6
- ]
7
-
8
- page = st.navigation(pages)
9
- page.run()
 
 
 
 
 
 
 
 
 
 
src/strings.py DELETED
@@ -1,135 +0,0 @@
1
- from src.utils import COLORS, MODEL_TYPE_LABELS, NON_COMMERCIAL_TAG, TAG_COLORS, ZERO_SHOT_TAG
2
-
3
-
4
- def tag_badge(tag: str) -> str:
5
- """Inline HTML badge that matches the chip a tag gets in the leaderboard's Tags column."""
6
- return (
7
- f"<span style='background: {TAG_COLORS[tag]}; color: {COLORS['text_default']}; "
8
- "padding: 1px 8px; border-radius: 999px; font-size: 0.85em; white-space: nowrap;'>"
9
- f"{tag}</span>"
10
- )
11
-
12
-
13
- INTRODUCTION_TEXT = """
14
- This space hosts evaluation results for time series forecasting models. The results are obtained using [fev](https://github.com/autogluon/fev) - a lightweight library for evaluating time series forecasting models.
15
- """
16
-
17
- LEGEND = """
18
- """
19
-
20
- TABLE_INFO = f"""
21
- The leaderboard summarizes the performance of all models evaluated on the 100 tasks comprising **fev-bench**. More details available in the [paper](https://arxiv.org/abs/2509.26468).
22
-
23
- Click a model name to open its page.
24
-
25
- The full matrix $E_{{rj}}$ with the error of each model $j$ on task $r$ is available at the bottom of the page.
26
-
27
- #### Performance
28
-
29
- * **Win rate (%)**: Fraction of all possible model pairs and tasks where this model achieves lower error than the competing model. For model $j$, defined as $W_j = \\frac{{1}}{{R(M-1)}} \\sum_{{r=1}}^{{R}} \\sum_{{k \\neq j}} (\\mathbf{{1}}(E_{{rj}} < E_{{rk}}) + 0.5 \\cdot \\mathbf{{1}}(E_{{rj}} = E_{{rk}}))$ where $R$ is number of tasks, $M$ is number of models. Ties count as half-wins.
30
-
31
- Ranges from 0% (worst) to 100% (best). Higher values are better. This value changes as new models are added to the benchmark.
32
-
33
- * **Skill score (%)**: Measures how much the model reduces forecasting error compared to the Seasonal Naive baseline. Computed as $S_j = 100 \\times (1 - \\sqrt[R]{{\\prod_{{r=1}}^{{R}} E_{{rj}}/E_{{r\\beta}}}})$, where $E_{{r\\beta}}$ is baseline error on task $r$. Relative errors are clipped between 0.01 and 100 before aggregation to avoid extreme outliers. Positive values indicate better-than-baseline performance, negative values indicate worse-than-baseline performance.
34
-
35
- Higher values are better. This value does not change as new models are added to the benchmark.
36
-
37
- * **Runtime (s / 100 series)**: Median end-to-end time (training + prediction across all evaluation windows) in seconds per 100 time series. Note that inference times depend on hardware, batch sizes, and implementation details, so these serve as a rough guide rather than definitive performance benchmarks.
38
-
39
- #### Tags
40
-
41
- * **Model type**: {tag_badge(MODEL_TYPE_LABELS["pretrained"])} open-weights foundation model pretrained on large amounts of data, {tag_badge(MODEL_TYPE_LABELS["task-specific"])} traditional ML model trained from scratch on each task, {tag_badge(MODEL_TYPE_LABELS["statistical"])} classical method fit per task, {tag_badge(MODEL_TYPE_LABELS["system"])} pipeline combining or selecting several models, {tag_badge(MODEL_TYPE_LABELS["closed-api"])} served behind a closed API, or weights unavailable.
42
- * **Zero-shot**: {tag_badge(ZERO_SHOT_TAG)} the model makes predictions without task-specific training.
43
- * **Non-commercial**: {tag_badge(NON_COMMERCIAL_TAG)} the model's license does not permit commercial use. Follow the model's link to check its license.
44
-
45
- Tags are defined in [`benchmarks/fev_bench/models.yaml`](https://github.com/autogluon/fev/blob/main/benchmarks/fev_bench/models.yaml).
46
-
47
- #### Imputation
48
-
49
- * **Leakage (%)**: For zero-shot models, percentage of benchmark datasets included in the model's training corpus. Results for tasks with reported overlap are replaced with Chronos-Bolt (Base) performance to prevent data leakage.
50
-
51
- * **Failed (%)**: Percentage of tasks where the model failed to produce a forecast. Results for failed tasks are replaced with Seasonal Naive performance.
52
-
53
- """
54
-
55
- CHRONOS_BENCHMARK_BASIC_INFO = f"""
56
- **Chronos Benchmark II** contains results for various forecasting models on the 27 datasets used in Benchmark II in the paper [Chronos: Learning the Language of Time Series](https://arxiv.org/abs/2403.07815). {LEGEND}
57
- """
58
-
59
- CHRONOS_BENCHMARK_DETAILS = f"""
60
- {TABLE_INFO}
61
-
62
- Task definitions and the detailed results are available on [GitHub](https://github.com/autogluon/fev/tree/main/benchmarks/chronos_zeroshot). More information for the datasets is available in [Table 3 of the paper](https://arxiv.org/abs/2403.07815).
63
- """
64
-
65
- FEV_BENCHMARK_BASIC_INFO = f"""
66
- Results for various forecasting models on 100 tasks of the **fev-bench** benchmark, as described in [fev-bench: A Realistic Benchmark for Time Series Forecasting](https://arxiv.org/abs/2509.26468). {LEGEND}
67
- """
68
-
69
- FEV_BENCHMARK_DETAILS = f"""
70
- {TABLE_INFO}
71
-
72
- Task definitions and the detailed results are available on [GitHub](https://github.com/autogluon/fev/tree/main/benchmarks/fev_bench/). For model wrappers, see [`models/`](https://github.com/autogluon/fev/tree/main/models). Datasets used for evaluation are available on [Hugging Face](https://huggingface.co/datasets/autogluon/fev_datasets).
73
- """
74
-
75
- CITATION_HEADER = """
76
- If you find this leaderboard useful for your research, please consider citing the associated paper(s):
77
-
78
- """
79
- CITATION_FEV = """
80
- ```
81
- @article{shchur2025fev,
82
- title={{fev-bench}: A Realistic Benchmark for Time Series Forecasting},
83
- author={Shchur, Oleksandr and Ansari, Abdul Fatir and Turkmen, Caner and Stella, Lorenzo and Erickson, Nick and Guerron, Pablo and Bohlke-Schneider, Michael and Wang, Yuyang},
84
- year={2025},
85
- eprint={2509.26468},
86
- archivePrefix={arXiv},
87
- primaryClass={cs.LG}
88
- }
89
- ```
90
- """
91
-
92
-
93
- def get_pivot_legend(baseline_model: str, leakage_imputation_model: str) -> str:
94
- return f"""
95
- Task definitions and raw results in CSV format are available on [GitHub](https://github.com/autogluon/fev/tree/main/benchmarks/fev_bench).
96
-
97
- Best results for each task are marked with
98
- <span style='background: {COLORS["gold"]}; color: {COLORS["text_default"]}; padding: 3px; border-radius: 5px;'>🥇 1st</span>
99
- <span style='background: {COLORS["silver"]}; color: {COLORS["text_default"]}; padding: 3px; border-radius: 5px;'>🥈 2nd</span>
100
- <span style='background: {COLORS["bronze"]}; color: {COLORS["text_default"]}; padding: 3px; border-radius: 5px;'>🥉 3rd</span>
101
- <br><br>
102
- **Imputation:**
103
- - <span style='color: {COLORS["failure_impute"]}; font-weight: bold;'>Failed tasks</span> imputed by {baseline_model}
104
- - <span style='color: {COLORS["leakage_impute"]}; font-weight: bold;'>Leaky tasks</span> imputed by {leakage_imputation_model}
105
- """
106
-
107
-
108
- PAIRWISE_BENCHMARK_DETAILS = """
109
- The pairwise charts show head-to-head results between models:
110
-
111
- * **Win rate**: Percentage of tasks where Model 1 achieves lower error than Model 2 (ties count as half-wins).
112
- A value above 50% means Model 1 is more accurate than Model 2 on average.
113
-
114
- * **Skill score**: Average relative error reduction of Model 1 with respect to Model 2.
115
- A positive value means Model 1 reduces forecasting error compared to Model 2 on average.
116
-
117
- **Confidence Intervals**: 95% intervals are estimated using 1000 bootstrap samples over tasks.
118
- For each bootstrap sample, tasks are resampled with replacement and the pairwise win rate / skill score are recomputed.
119
- The intervals correspond to the 2.5th and 97.5th percentiles of these bootstrap distributions,
120
- capturing how model comparisons vary under alternative benchmark compositions.
121
- """
122
-
123
-
124
- CITATION_CHRONOS = """
125
- ```
126
- @article{ansari2024chronos,
127
- title={Chronos: Learning the Language of Time Series},
128
- author={Ansari, Abdul Fatir and Stella, Lorenzo and Turkmen, Caner and Zhang, Xiyuan, and Mercado, Pedro and Shen, Huibin and Shchur, Oleksandr and Rangapuram, Syama Syndar and Pineda Arango, Sebastian and Kapoor, Shubham and Zschiegner, Jasper and Maddix, Danielle C. and Wang, Hao and Mahoney, Michael W. and Torkkola, Kari and Gordon Wilson, Andrew and Bohlke-Schneider, Michael and Wang, Yuyang},
129
- journal={Transactions on Machine Learning Research},
130
- issn={2835-8856},
131
- year={2024},
132
- url={https://openreview.net/forum?id=gerNCVqqtR}
133
- }
134
- ```
135
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/task_groups.py DELETED
@@ -1,266 +0,0 @@
1
- """Task groupings for filtering the leaderboard by subsets."""
2
-
3
- # All tasks in the benchmark (100 tasks)
4
- ALL_TASKS = [
5
- "ETT_15T", "ETT_1D", "ETT_1H", "ETT_1W",
6
- "LOOP_SEATTLE_1D", "LOOP_SEATTLE_1H", "LOOP_SEATTLE_5T",
7
- "M_DENSE_1D", "M_DENSE_1H",
8
- "SZ_TAXI_15T", "SZ_TAXI_1H",
9
- "australian_tourism",
10
- "bizitobs_l2c_1H", "bizitobs_l2c_5T",
11
- "boomlet_1062", "boomlet_1209", "boomlet_1225", "boomlet_1230", "boomlet_1282",
12
- "boomlet_1487", "boomlet_1631", "boomlet_1676", "boomlet_1855", "boomlet_1975",
13
- "boomlet_2187", "boomlet_285", "boomlet_619", "boomlet_772", "boomlet_963",
14
- "ecdc_ili",
15
- "entsoe_15T", "entsoe_1H", "entsoe_30T",
16
- "epf_be", "epf_de", "epf_fr", "epf_np", "epf_pjm",
17
- "ercot_1D", "ercot_1H", "ercot_1M", "ercot_1W",
18
- "favorita_stores_1D", "favorita_stores_1M", "favorita_stores_1W",
19
- "favorita_transactions_1D", "favorita_transactions_1M", "favorita_transactions_1W",
20
- "fred_md_2025/cee", "fred_md_2025/macro",
21
- "fred_qd_2025/cee", "fred_qd_2025/macro",
22
- "gvar",
23
- "hermes",
24
- "hierarchical_sales_1D", "hierarchical_sales_1W",
25
- "hospital", "hospital_admissions_1D", "hospital_admissions_1W",
26
- "jena_weather_10T", "jena_weather_1D", "jena_weather_1H",
27
- "kdd_cup_2022_10T", "kdd_cup_2022_1D", "kdd_cup_2022_30T",
28
- "m5_1D", "m5_1M", "m5_1W",
29
- "proenfo_gfc12", "proenfo_gfc14", "proenfo_gfc17",
30
- "redset_15T", "redset_1H", "redset_5T",
31
- "restaurant",
32
- "rohlik_orders_1D", "rohlik_orders_1W", "rohlik_sales_1D", "rohlik_sales_1W",
33
- "rossmann_1D", "rossmann_1W",
34
- "solar_1D", "solar_1W", "solar_with_weather_15T", "solar_with_weather_1H",
35
- "uci_air_quality_1D", "uci_air_quality_1H",
36
- "uk_covid_nation_1D/cumulative", "uk_covid_nation_1D/new",
37
- "uk_covid_nation_1W/cumulative", "uk_covid_nation_1W/new",
38
- "uk_covid_utla_1D/new", "uk_covid_utla_1W/cumulative",
39
- "us_consumption_1M", "us_consumption_1Q", "us_consumption_1Y",
40
- "walmart",
41
- "world_co2_emissions", "world_life_expectancy", "world_tourism",
42
- ]
43
-
44
- # Mini benchmark - representative subset (20 tasks)
45
- MINI_TASKS = [
46
- "jena_weather_1H",
47
- "M_DENSE_1D",
48
- "bizitobs_l2c_5T",
49
- "rohlik_orders_1D",
50
- "boomlet_1282",
51
- "rossmann_1D",
52
- "rossmann_1W",
53
- "boomlet_1676",
54
- "solar_with_weather_1H",
55
- "boomlet_619",
56
- "uci_air_quality_1H",
57
- "uk_covid_nation_1D/cumulative",
58
- "us_consumption_1Y",
59
- "epf_np",
60
- "world_co2_emissions",
61
- "ETT_15T",
62
- "ETT_1H",
63
- "proenfo_gfc14",
64
- "hospital_admissions_1D",
65
- "hospital_admissions_1W",
66
- ]
67
-
68
- # Frequency-based groupings
69
- FREQUENCY_GROUPS = {
70
- "Sub-hourly": [
71
- # T (1 minute)
72
- "boomlet_1225", "boomlet_1282", "boomlet_285", "boomlet_619", "boomlet_772", "boomlet_963",
73
- # 5T (5 minutes)
74
- "LOOP_SEATTLE_5T", "bizitobs_l2c_5T", "redset_5T",
75
- "boomlet_1062", "boomlet_1209", "boomlet_1230", "boomlet_1487",
76
- # 10T (10 minutes)
77
- "jena_weather_10T", "kdd_cup_2022_10T",
78
- # 15T (15 minutes)
79
- "ETT_15T", "SZ_TAXI_15T", "entsoe_15T", "redset_15T", "solar_with_weather_15T",
80
- # 30T (30 minutes)
81
- "entsoe_30T", "kdd_cup_2022_30T", "boomlet_1631", "boomlet_1676",
82
- ],
83
- "Hourly": [
84
- "ETT_1H", "LOOP_SEATTLE_1H", "M_DENSE_1H", "SZ_TAXI_1H",
85
- "bizitobs_l2c_1H", "entsoe_1H", "ercot_1H",
86
- "epf_be", "epf_de", "epf_fr", "epf_np", "epf_pjm",
87
- "jena_weather_1H",
88
- "proenfo_gfc12", "proenfo_gfc14", "proenfo_gfc17",
89
- "redset_1H", "solar_with_weather_1H", "uci_air_quality_1H",
90
- "boomlet_1855", "boomlet_1975", "boomlet_2187",
91
- ],
92
- "Daily": [
93
- "ETT_1D", "LOOP_SEATTLE_1D", "M_DENSE_1D",
94
- "ercot_1D", "kdd_cup_2022_1D", "solar_1D",
95
- "favorita_stores_1D", "favorita_transactions_1D",
96
- "hierarchical_sales_1D", "m5_1D",
97
- "restaurant",
98
- "rohlik_orders_1D", "rohlik_sales_1D", "rossmann_1D",
99
- "jena_weather_1D", "uci_air_quality_1D",
100
- "hospital_admissions_1D",
101
- "uk_covid_nation_1D/cumulative", "uk_covid_nation_1D/new", "uk_covid_utla_1D/new",
102
- ],
103
- "Weekly": [
104
- "ETT_1W", "ercot_1W", "solar_1W",
105
- "favorita_stores_1W", "favorita_transactions_1W",
106
- "hierarchical_sales_1W", "m5_1W",
107
- "hermes", "walmart",
108
- "rohlik_orders_1W", "rohlik_sales_1W", "rossmann_1W",
109
- "ecdc_ili",
110
- "hospital_admissions_1W",
111
- "uk_covid_nation_1W/cumulative", "uk_covid_nation_1W/new", "uk_covid_utla_1W/cumulative",
112
- ],
113
- "Monthly+": [
114
- # Monthly
115
- "ercot_1M",
116
- "favorita_stores_1M", "favorita_transactions_1M", "m5_1M",
117
- "fred_md_2025/cee", "fred_md_2025/macro",
118
- "hospital",
119
- "us_consumption_1M",
120
- # Quarterly
121
- "australian_tourism", "gvar",
122
- "fred_qd_2025/cee", "fred_qd_2025/macro",
123
- "us_consumption_1Q",
124
- # Yearly
125
- "us_consumption_1Y",
126
- "world_co2_emissions", "world_life_expectancy", "world_tourism",
127
- ],
128
- }
129
-
130
- # Domain-based groupings
131
- DOMAIN_GROUPS = {
132
- "Energy": [
133
- "ETT_15T", "ETT_1D", "ETT_1H", "ETT_1W",
134
- "entsoe_15T", "entsoe_1H", "entsoe_30T",
135
- "epf_be", "epf_de", "epf_fr", "epf_np", "epf_pjm",
136
- "ercot_1D", "ercot_1H", "ercot_1M", "ercot_1W",
137
- "kdd_cup_2022_10T", "kdd_cup_2022_1D", "kdd_cup_2022_30T",
138
- "proenfo_gfc12", "proenfo_gfc14", "proenfo_gfc17",
139
- "solar_1D", "solar_1W", "solar_with_weather_15T", "solar_with_weather_1H",
140
- ],
141
- "Retail": [
142
- "favorita_stores_1D", "favorita_stores_1M", "favorita_stores_1W",
143
- "favorita_transactions_1D", "favorita_transactions_1M", "favorita_transactions_1W",
144
- "hermes",
145
- "hierarchical_sales_1D", "hierarchical_sales_1W",
146
- "m5_1D", "m5_1M", "m5_1W",
147
- "restaurant",
148
- "rohlik_orders_1D", "rohlik_orders_1W", "rohlik_sales_1D", "rohlik_sales_1W",
149
- "rossmann_1D", "rossmann_1W",
150
- "walmart",
151
- ],
152
- "Nature": [
153
- "jena_weather_10T", "jena_weather_1D", "jena_weather_1H",
154
- "uci_air_quality_1D", "uci_air_quality_1H",
155
- ],
156
- "Cloud": [
157
- "bizitobs_l2c_1H", "bizitobs_l2c_5T",
158
- "boomlet_1062", "boomlet_1209", "boomlet_1225", "boomlet_1230", "boomlet_1282",
159
- "boomlet_1487", "boomlet_1631", "boomlet_1676", "boomlet_1855", "boomlet_1975",
160
- "boomlet_2187", "boomlet_285", "boomlet_619", "boomlet_772", "boomlet_963",
161
- "redset_15T", "redset_1H", "redset_5T",
162
- ],
163
- "Health": [
164
- "ecdc_ili",
165
- "hospital", "hospital_admissions_1D", "hospital_admissions_1W",
166
- "uk_covid_nation_1D/cumulative", "uk_covid_nation_1D/new",
167
- "uk_covid_nation_1W/cumulative", "uk_covid_nation_1W/new",
168
- "uk_covid_utla_1D/new", "uk_covid_utla_1W/cumulative",
169
- ],
170
- "Econ": [
171
- "australian_tourism",
172
- "fred_md_2025/cee", "fred_md_2025/macro",
173
- "fred_qd_2025/cee", "fred_qd_2025/macro",
174
- "gvar",
175
- "us_consumption_1M", "us_consumption_1Q", "us_consumption_1Y",
176
- "world_co2_emissions", "world_life_expectancy", "world_tourism",
177
- ],
178
- "Mobility": [
179
- "LOOP_SEATTLE_1D", "LOOP_SEATTLE_1H", "LOOP_SEATTLE_5T",
180
- "M_DENSE_1D", "M_DENSE_1H",
181
- "SZ_TAXI_15T", "SZ_TAXI_1H",
182
- ],
183
- }
184
- # Task type groupings (overlapping: a multivariate task with covariates
185
- # belongs to both "Covariate-informed" and "Multivariate").
186
- # - Multivariate: target is a list of columns
187
- # - Covariate-informed: known_dynamic_columns or past_dynamic_columns is non-empty
188
- # - Univariate: neither of the above
189
- TASK_TYPE_GROUPS = {
190
- "Univariate": [
191
- "LOOP_SEATTLE_1D", "LOOP_SEATTLE_1H", "LOOP_SEATTLE_5T",
192
- "M_DENSE_1D", "M_DENSE_1H",
193
- "SZ_TAXI_15T", "SZ_TAXI_1H",
194
- "australian_tourism", "ecdc_ili",
195
- "ercot_1D", "ercot_1H", "ercot_1M", "ercot_1W",
196
- "hierarchical_sales_1D", "hierarchical_sales_1W",
197
- "hospital", "hospital_admissions_1D", "hospital_admissions_1W",
198
- "redset_15T", "redset_1H", "redset_5T",
199
- "restaurant",
200
- "solar_1D", "solar_1W",
201
- "uk_covid_utla_1D/new", "uk_covid_utla_1W/cumulative",
202
- "us_consumption_1M", "us_consumption_1Q", "us_consumption_1Y",
203
- "world_co2_emissions", "world_life_expectancy", "world_tourism",
204
- ],
205
- "Covariate-informed": [
206
- "entsoe_15T", "entsoe_1H", "entsoe_30T",
207
- "epf_be", "epf_de", "epf_fr", "epf_np", "epf_pjm",
208
- "favorita_stores_1D", "favorita_stores_1M", "favorita_stores_1W",
209
- "favorita_transactions_1D", "favorita_transactions_1M", "favorita_transactions_1W",
210
- "fred_md_2025/cee", "fred_qd_2025/cee",
211
- "gvar", "hermes",
212
- "kdd_cup_2022_10T", "kdd_cup_2022_1D", "kdd_cup_2022_30T",
213
- "m5_1D", "m5_1M", "m5_1W",
214
- "proenfo_gfc12", "proenfo_gfc14", "proenfo_gfc17",
215
- "rohlik_orders_1D", "rohlik_orders_1W", "rohlik_sales_1D", "rohlik_sales_1W",
216
- "rossmann_1D", "rossmann_1W",
217
- "solar_with_weather_15T", "solar_with_weather_1H",
218
- "uci_air_quality_1D", "uci_air_quality_1H",
219
- "uk_covid_nation_1D/cumulative", "uk_covid_nation_1D/new",
220
- "uk_covid_nation_1W/cumulative", "uk_covid_nation_1W/new",
221
- "walmart",
222
- ],
223
- "Multivariate": [
224
- "ETT_15T", "ETT_1D", "ETT_1H", "ETT_1W",
225
- "bizitobs_l2c_1H", "bizitobs_l2c_5T",
226
- "boomlet_1062", "boomlet_1209", "boomlet_1225", "boomlet_1230", "boomlet_1282",
227
- "boomlet_1487", "boomlet_1631", "boomlet_1676", "boomlet_1855", "boomlet_1975",
228
- "boomlet_2187", "boomlet_285", "boomlet_619", "boomlet_772", "boomlet_963",
229
- "fred_md_2025/cee", "fred_md_2025/macro", "fred_qd_2025/cee", "fred_qd_2025/macro",
230
- "gvar",
231
- "jena_weather_10T", "jena_weather_1D", "jena_weather_1H",
232
- "uci_air_quality_1D", "uci_air_quality_1H",
233
- "uk_covid_nation_1D/cumulative", "uk_covid_nation_1D/new",
234
- "uk_covid_nation_1W/cumulative", "uk_covid_nation_1W/new",
235
- ],
236
- }
237
-
238
-
239
- def get_task_group(group_type: str, group_value: str | None = None) -> list[str]:
240
- """Get the list of tasks for a given group type and value.
241
-
242
- Args:
243
- group_type: One of "full", "mini", "frequency", "domain", "task_type"
244
- group_value: Required for "frequency", "domain", and "task_type" types
245
-
246
- Returns:
247
- List of task names belonging to the group
248
- """
249
- if group_type == "full":
250
- return ALL_TASKS
251
- elif group_type == "mini":
252
- return MINI_TASKS
253
- elif group_type == "frequency":
254
- if group_value is None:
255
- raise ValueError("group_value required for frequency grouping")
256
- return FREQUENCY_GROUPS[group_value]
257
- elif group_type == "domain":
258
- if group_value is None:
259
- raise ValueError("group_value required for domain grouping")
260
- return DOMAIN_GROUPS[group_value]
261
- elif group_type == "task_type":
262
- if group_value is None:
263
- raise ValueError("group_value required for task_type grouping")
264
- return TASK_TYPE_GROUPS[group_value]
265
- else:
266
- raise ValueError(f"Unknown group_type: {group_type}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/utils.py DELETED
@@ -1,401 +0,0 @@
1
- import functools
2
- from pathlib import Path
3
-
4
- import altair as alt
5
- import fev
6
- import pandas as pd
7
- import pandas.io.formats.style
8
-
9
- # Color constants - all colors defined in one place
10
-
11
- COLORS = {
12
- "bar_fill": "#8d5eb7",
13
- "error_bar": "#222222",
14
- "point": "#111111",
15
- "text_white": "white",
16
- "text_black": "black",
17
- "text_default": "#111",
18
- "gold": "#F7D36B",
19
- "silver": "#E5E7EB",
20
- "bronze": "#E6B089",
21
- "leakage_impute": "#3B82A0",
22
- "failure_impute": "#E07B39",
23
- }
24
- HEATMAP_COLOR_SCHEME = "purplegreen"
25
-
26
- # Display metadata for every model on the leaderboard. Generated by save_tables.py from
27
- # benchmarks/fev_bench/models.yaml in the fev repo, where submitters declare it themselves.
28
- MODELS_CSV = Path(__file__).resolve().parent.parent / "tables" / "models.csv"
29
-
30
- # Human-readable labels for the model types.
31
- MODEL_TYPE_LABELS = {
32
- "pretrained": "Pretrained",
33
- "task-specific": "Task-specific",
34
- "statistical": "Statistical",
35
- "system": "System",
36
- "closed-api": "Closed API",
37
- }
38
- # Tags shown in the leaderboard's Tags column, in display order, with the chip color for each.
39
- # Pale tints drawn from the page palette (the purple bars, the purple-green heatmap and the medal
40
- # colors), so the tags stay readable without competing with the numbers. Non-commercial gets the warmest tint,
41
- # as the one tag readers usually scan for.
42
- ZERO_SHOT_TAG = "Zero-shot"
43
- NON_COMMERCIAL_TAG = "Non-commercial"
44
- TAG_COLORS = {
45
- "Pretrained": "#E9DDF5", # pale lavender
46
- "Task-specific": "#DCE6F2", # pale blue
47
- "Statistical": "#ECE6DD", # pale sand
48
- "System": "#DCEEE4", # pale sage
49
- "Closed API": "#F3DEDE", # pale rose
50
- ZERO_SHOT_TAG: "#ECEDEF", # neutral gray
51
- NON_COMMERCIAL_TAG: "#F7E2B0", # pale gold
52
- }
53
- # Types shown unless the reader opts into more. Results for the others either cannot be reproduced
54
- # independently (closed-api) or are not comparable to single models (system).
55
- DEFAULT_VISIBLE_MODEL_TYPES = ("pretrained", "task-specific", "statistical")
56
-
57
-
58
- @functools.lru_cache(maxsize=1)
59
- def get_model_metadata() -> dict[str, dict]:
60
- """Display metadata for each model, keyed by the `model_name` used in the results."""
61
- df = pd.read_csv(MODELS_CSV)
62
- return {row["model_name"]: row for row in df.to_dict("records")}
63
-
64
-
65
- def validate_model_metadata(model_names) -> None:
66
- """Raise if any displayed model lacks metadata, so the app never shows blank attributes."""
67
- metadata = get_model_metadata()
68
- missing = sorted({m for m in model_names if m not in metadata})
69
- if missing:
70
- raise ValueError(
71
- f"Missing metadata (organization, link, type) for models: {missing}. "
72
- "Add an entry to benchmarks/fev_bench/models.yaml in the fev repo, "
73
- "then regenerate the tables with `uv run python save_tables.py`."
74
- )
75
-
76
-
77
- def select_model_names(model_types, include_non_commercial: bool = True) -> list[str]:
78
- """Names of the models with a type in `model_types`, optionally excluding non-commercial ones."""
79
- model_types = set(model_types)
80
- return sorted(
81
- name
82
- for name, entry in get_model_metadata().items()
83
- if entry["model_type"] in model_types and (include_non_commercial or entry["commercial_use"])
84
- )
85
-
86
-
87
- ALL_METRICS = {
88
- "SQL": (
89
- "SQL: Scaled Quantile Loss",
90
- "The [Scaled Quantile Loss (SQL)](https://auto.gluon.ai/dev/tutorials/timeseries/forecasting-metrics.html#autogluon.timeseries.metrics.SQL) is a **scale-invariant** metric for evaluating **probabilistic** forecasts.",
91
- ),
92
- "MASE": (
93
- "MASE: Mean Absolute Scaled Error",
94
- "The [Mean Absolute Scaled Error (MASE)](https://auto.gluon.ai/dev/tutorials/timeseries/forecasting-metrics.html#autogluon.timeseries.metrics.MASE) is a **scale-invariant** metric for evaluating **point** forecasts.",
95
- ),
96
- "WQL": (
97
- "WQL: Weighted Quantile Loss",
98
- "The [Weighted Quantile Loss (WQL)](https://auto.gluon.ai/dev/tutorials/timeseries/forecasting-metrics.html#autogluon.timeseries.metrics.WQL), is a **scale-dependent** metric for evaluating **probabilistic** forecasts.",
99
- ),
100
- "WAPE": (
101
- "WAPE: Weighted Absolute Percentage Error",
102
- "The [Weighted Absolute Percentage Error (WAPE)](https://auto.gluon.ai/dev/tutorials/timeseries/forecasting-metrics.html#autogluon.timeseries.metrics.WAPE) is a **scale-dependent** metric for evaluating **point** forecasts.",
103
- ),
104
- }
105
-
106
-
107
- def format_metric_name(metric_name: str):
108
- return ALL_METRICS[metric_name][0]
109
-
110
-
111
- def get_metric_description(metric_name: str):
112
- return ALL_METRICS[metric_name][1]
113
-
114
-
115
- def get_model_link(model_name):
116
- return get_model_metadata()[model_name]["url"]
117
-
118
-
119
- def get_model_organization(model_name):
120
- return get_model_metadata()[model_name]["organization"]
121
-
122
-
123
- def get_model_type(model_name):
124
- return get_model_metadata()[model_name]["model_type"]
125
-
126
-
127
- def get_model_type_label(model_name):
128
- return MODEL_TYPE_LABELS[get_model_type(model_name)]
129
-
130
-
131
- def get_model_display_name(model_name):
132
- """Display name declared in models.yaml, falling back to the name used in the results."""
133
- entry = get_model_metadata().get(model_name)
134
- return entry["display_name"] if entry else model_name
135
-
136
-
137
- def get_model_name_link(model_name):
138
- """Model URL with the display name as fragment, rendered as a named link by the page's LinkColumn."""
139
- return f"{get_model_link(model_name)}#{get_model_display_name(model_name)}"
140
-
141
-
142
- def get_model_tags(model_name) -> list[str]:
143
- """Tags describing a model: its type, whether it is zero-shot, and whether its license is non-commercial."""
144
- entry = get_model_metadata()[model_name]
145
- tags = [MODEL_TYPE_LABELS[entry["model_type"]]]
146
- if entry["zero_shot"]:
147
- tags.append(ZERO_SHOT_TAG)
148
- if not entry["commercial_use"]:
149
- tags.append(NON_COMMERCIAL_TAG)
150
- return tags
151
-
152
-
153
- def format_leaderboard(df: pd.DataFrame):
154
- df = df.copy()
155
- df["skill_score"] = df["skill_score"].round(1)
156
- df["win_rate"] = df["win_rate"].round(1)
157
- # Leakage only applies to zero-shot models; report 0 for models trained on each task
158
- is_zero_shot = df["model_name"].map(lambda m: bool(get_model_metadata()[m]["zero_shot"]))
159
- df["training_corpus_overlap"] = (
160
- (df["training_corpus_overlap"] * 100).round().astype(int).where(is_zero_shot, 0)
161
- )
162
- # Model type, zero-shot and licensing are folded into a single column of tags to keep the table
163
- # narrow. Non-commercial is only shown as the exception.
164
- df["tags"] = df["model_name"].map(get_model_tags)
165
- df["org"] = df["model_name"].apply(get_model_organization)
166
- df["model_name"] = df["model_name"].apply(get_model_name_link)
167
- df = df[
168
- [
169
- "model_name",
170
- "win_rate",
171
- "skill_score",
172
- "median_inference_time_s_per100",
173
- "training_corpus_overlap",
174
- "num_failures",
175
- "org",
176
- "tags",
177
- ]
178
- ]
179
- return df.style.apply(
180
- lambda x: [
181
- "background-color: #f8f9fa" if i % 2 == 1 else "" for i in range(len(x))
182
- ],
183
- axis=0,
184
- )
185
-
186
-
187
- def construct_bar_chart(df: pd.DataFrame, col: str, metric_name: str):
188
- label = "Skill Score" if col == "skill_score" else "Win Rate"
189
-
190
- tooltip = [
191
- alt.Tooltip("model_name:N"),
192
- alt.Tooltip(f"{col}:Q", format=".2f"),
193
- alt.Tooltip(f"{col}_lower:Q", title="95% CI Lower", format=".2f"),
194
- alt.Tooltip(f"{col}_upper:Q", title="95% CI Upper", format=".2f"),
195
- ]
196
-
197
- base_encode = {
198
- "y": alt.Y("model_name:N", title="Forecasting Model", sort=None),
199
- "tooltip": tooltip,
200
- }
201
-
202
- bars = (
203
- alt.Chart(df)
204
- .mark_bar(color=COLORS["bar_fill"], cornerRadius=4)
205
- .encode(
206
- x=alt.X(f"{col}:Q", title=f"{label} (%)", scale=alt.Scale(zero=False)),
207
- **base_encode,
208
- )
209
- )
210
-
211
- error_bars = (
212
- alt.Chart(df)
213
- .mark_errorbar(ticks={"height": 5}, color=COLORS["error_bar"])
214
- .encode(
215
- y=alt.Y("model_name:N", title=None, sort=None),
216
- x=alt.X(f"{col}_lower:Q", title=f"{label} (%)"),
217
- x2=alt.X2(f"{col}_upper:Q"),
218
- tooltip=tooltip,
219
- )
220
- )
221
-
222
- points = (
223
- alt.Chart(df)
224
- .mark_point(filled=True, color=COLORS["point"])
225
- .encode(x=alt.X(f"{col}:Q", title=f"{label} (%)"), **base_encode)
226
- )
227
-
228
- return (
229
- (bars + error_bars + points)
230
- .properties(height=500, title=f"{label} ({metric_name}) with 95% CIs")
231
- .configure_title(fontSize=16)
232
- )
233
-
234
-
235
- def construct_pairwise_chart(df: pd.DataFrame, col: str, metric_name: str):
236
- # Color ranges kept in sync with the fev repo's generate_fev_bench_figures.py
237
- config = {
238
- "win_rate": ("Win Rate", [0, 100], 50, f"abs(datum.{col} - 50) > 30"),
239
- "skill_score": ("Skill Score", [-15, 15], 0, f"abs(datum.{col}) > 10"),
240
- }
241
- cbar_label, domain, domain_mid, text_condition = config[col]
242
-
243
- df = df.copy()
244
- for c in [col, f"{col}_lower", f"{col}_upper"]:
245
- df[c] *= 100
246
-
247
- df["model_1"] = df["model_1"].apply(get_model_display_name)
248
- df["model_2"] = df["model_2"].apply(get_model_display_name)
249
-
250
- model_order = (
251
- df.groupby("model_1")[col].mean().sort_values(ascending=False).index.tolist()
252
- )
253
-
254
- tooltip = [
255
- alt.Tooltip("model_1:N", title="Model 1"),
256
- alt.Tooltip("model_2:N", title="Model 2"),
257
- alt.Tooltip(f"{col}:Q", title=cbar_label.split(" ")[0], format=".1f"),
258
- alt.Tooltip(f"{col}_lower:Q", title="95% CI Lower", format=".1f"),
259
- alt.Tooltip(f"{col}_upper:Q", title="95% CI Upper", format=".1f"),
260
- ]
261
-
262
- base = alt.Chart(df).encode(
263
- x=alt.X(
264
- "model_2:N",
265
- sort=model_order,
266
- title="Model 2",
267
- axis=alt.Axis(orient="top", labelAngle=-90),
268
- ),
269
- y=alt.Y("model_1:N", sort=model_order, title="Model 1"),
270
- )
271
-
272
- heatmap = base.mark_rect().encode(
273
- color=alt.Color(
274
- f"{col}:Q",
275
- legend=None,
276
- scale=alt.Scale(
277
- scheme=HEATMAP_COLOR_SCHEME,
278
- domain=domain,
279
- domainMid=domain_mid,
280
- clamp=True,
281
- ),
282
- ),
283
- tooltip=tooltip,
284
- )
285
-
286
- text_main = base.mark_text(dy=-8, fontSize=8, baseline="top", yOffset=5).encode(
287
- text=alt.Text(f"{col}:Q", format=".1f"),
288
- color=alt.condition(
289
- text_condition,
290
- alt.value(COLORS["text_white"]),
291
- alt.value(COLORS["text_black"]),
292
- ),
293
- tooltip=tooltip,
294
- )
295
-
296
- return (
297
- (heatmap + text_main)
298
- .properties(
299
- height=550,
300
- title={
301
- "text": f"Pairwise {cbar_label} ({metric_name}) with 95% CIs",
302
- "fontSize": 16,
303
- },
304
- )
305
- .configure_axis(labelFontSize=11, titleFontSize=13, titleFontWeight="bold")
306
- .resolve_scale(color="independent")
307
- )
308
-
309
-
310
- def construct_pivot_table_from_df(
311
- errors: pd.DataFrame, metric_name: str
312
- ) -> pd.io.formats.style.Styler:
313
- """Construct styled pivot table from precomputed DataFrame."""
314
-
315
- def highlight_by_position(styler):
316
- rank_colors = {1: COLORS["gold"], 2: COLORS["silver"], 3: COLORS["bronze"]}
317
-
318
- for row_idx in errors.index:
319
- row_ranks = errors.loc[row_idx].rank(method="min")
320
- for col_idx in errors.columns:
321
- rank = row_ranks[col_idx]
322
- style_parts = []
323
-
324
- # Rank background colors
325
- if rank <= 3:
326
- style_parts.append(f"background-color: {rank_colors[rank]}")
327
- else:
328
- style_parts.append(f"color: {COLORS['text_default']}")
329
-
330
- if style_parts:
331
- styler = styler.map(
332
- lambda x, s="; ".join(style_parts): s,
333
- subset=pd.IndexSlice[row_idx:row_idx, col_idx:col_idx],
334
- )
335
- return styler
336
-
337
- return highlight_by_position(errors.style).format(precision=3)
338
-
339
-
340
- def construct_pivot_table(
341
- summaries: pd.DataFrame,
342
- metric_name: str,
343
- baseline_model: str,
344
- leakage_imputation_model: str,
345
- ) -> pd.io.formats.style.Styler:
346
- errors = fev.pivot_table(
347
- summaries=summaries, metric_column=metric_name, task_columns=["task_name"]
348
- )
349
- train_overlap = (
350
- fev.pivot_table(
351
- summaries=summaries,
352
- metric_column="trained_on_this_dataset",
353
- task_columns=["task_name"],
354
- )
355
- .fillna(False)
356
- .astype(bool)
357
- )
358
-
359
- is_imputed_baseline = errors.isna()
360
- is_leakage_imputed = train_overlap
361
-
362
- # Handle imputations
363
- errors = errors.mask(train_overlap, errors[leakage_imputation_model], axis=0)
364
- for col in errors.columns:
365
- if col != baseline_model:
366
- errors[col] = errors[col].fillna(errors[baseline_model])
367
-
368
- errors = errors[errors.rank(axis=1).mean().sort_values().index]
369
- errors.index.rename("Task name", inplace=True)
370
-
371
- def highlight_by_position(styler):
372
- rank_colors = {1: COLORS["gold"], 2: COLORS["silver"], 3: COLORS["bronze"]}
373
-
374
- for row_idx in errors.index:
375
- row_ranks = errors.loc[row_idx].rank(method="min")
376
- for col_idx in errors.columns:
377
- rank = row_ranks[col_idx]
378
- style_parts = []
379
-
380
- # Rank background colors
381
- if rank <= 3:
382
- style_parts.append(f"background-color: {rank_colors[rank]}")
383
-
384
- # Imputation text colors
385
- if is_leakage_imputed.loc[row_idx, col_idx]:
386
- style_parts.append(f"color: {COLORS['leakage_impute']}")
387
- elif is_imputed_baseline.loc[row_idx, col_idx]:
388
- style_parts.append(f"color: {COLORS['failure_impute']}")
389
- elif not style_parts or (
390
- len(style_parts) == 1 and "font-weight" in style_parts[0]
391
- ):
392
- style_parts.append(f"color: {COLORS['text_default']}")
393
-
394
- if style_parts:
395
- styler = styler.map(
396
- lambda x, s="; ".join(style_parts): s,
397
- subset=pd.IndexSlice[row_idx:row_idx, col_idx:col_idx],
398
- )
399
- return styler
400
-
401
- return highlight_by_position(errors.style).format(precision=3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
uv.lock DELETED
The diff for this file is too large to render. See raw diff