Spaces:
Running
Running
| 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() | |