underdog-lab / scripts /build_historical_dataset.py
Moftah
Fit and validate World Cup forecast model
b38dafd
Raw
History Blame Contribute Delete
4.17 kB
from __future__ import annotations
"""Build a real historical international match dataset for model fitting.
Source: eloratings.net's per-year results files (e.g.
https://www.eloratings.net/2024_results.tsv), the same source already cited
in data/world_cup_2026/snapshot.json. Each row gives the match date, the two
teams (eloratings.net country codes), the score, the host country (if the
match was played on neutral ground), and each team's Elo rating going into
the match -- so no separate historical-Elo lookup is needed.
Usage:
python scripts/build_historical_dataset.py --start-year 2015 --end-year 2026
"""
import argparse
import csv
import urllib.request
from datetime import date
from pathlib import Path
from underdog_lab.config import DATA_DIR
RAW_DIR = DATA_DIR / "historical" / "raw"
OUTPUT_PATH = DATA_DIR / "historical" / "matches.csv"
# (date, home_code, away_code) for matches in data/raw/challenge_matches.json
# that fall within the fetched year range. These are reserved for the LLM
# extraction challenge and must not leak into model fitting or evaluation.
CHALLENGE_MATCH_EXCLUSIONS = {
(date(2022, 11, 22), "AR", "SA"),
(date(2018, 6, 27), "DE", "KR"),
(date(2022, 12, 18), "AR", "FR"),
(date(2022, 12, 10), "MA", "PT"),
(date(2022, 12, 9), "HR", "BR"),
(date(2021, 7, 11), "EN", "IT"),
(date(2018, 7, 15), "FR", "HR"),
(date(2018, 7, 2), "BE", "JP"),
(date(2021, 7, 10), "BR", "AR"),
(date(2022, 11, 25), "EN", "US"),
(date(2018, 7, 11), "HR", "EN"),
(date(2016, 7, 10), "FR", "PT"),
}
def fetch_year(year: int) -> Path:
RAW_DIR.mkdir(parents=True, exist_ok=True)
path = RAW_DIR / f"{year}_results.tsv"
if not path.exists():
url = f"https://www.eloratings.net/{year}_results.tsv"
with urllib.request.urlopen(url, timeout=30) as response:
path.write_bytes(response.read())
return path
def parse_year(path: Path) -> list[dict]:
rows = []
with path.open(encoding="utf-8") as f:
for line in f:
fields = line.rstrip("\n").split("\t")
if len(fields) < 12:
continue
year, month, day, home, away = fields[0:5]
home_goals, away_goals = fields[5:7]
tournament, host = fields[7:9]
home_elo, away_elo = fields[10:12]
match_date = date(int(year), int(month), int(day))
if (match_date, home, away) in CHALLENGE_MATCH_EXCLUSIONS:
continue
rows.append(
{
"date": match_date.isoformat(),
"home_team": home,
"away_team": away,
"home_goals": int(home_goals),
"away_goals": int(away_goals),
"home_elo": float(home_elo),
"away_elo": float(away_elo),
"neutral": host != "" and host != home,
"tournament": tournament,
}
)
return rows
def main() -> None:
parser = argparse.ArgumentParser(
description="Build data/historical/matches.csv from eloratings.net."
)
parser.add_argument("--start-year", type=int, default=2015)
parser.add_argument("--end-year", type=int, default=2026)
args = parser.parse_args()
all_rows = []
for year in range(args.start_year, args.end_year + 1):
path = fetch_year(year)
all_rows.extend(parse_year(path))
all_rows.sort(key=lambda r: r["date"])
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
with OUTPUT_PATH.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(
f,
fieldnames=[
"date",
"home_team",
"away_team",
"home_goals",
"away_goals",
"home_elo",
"away_elo",
"neutral",
"tournament",
],
)
writer.writeheader()
writer.writerows(all_rows)
print(f"Wrote {len(all_rows)} matches to {OUTPUT_PATH}")
if __name__ == "__main__":
main()