world-cup-oracle / data /loader.py
robomotic's picture
Initial release v0.0.1 — World Cup Oracle
0f06d7c
Raw
History Blame Contribute Delete
5.49 kB
"""Historical World Cup data loader (jfjelstul/worldcup, 1930-2022)."""
import unicodedata
from pathlib import Path
import pandas as pd
DATA_DIR = Path(__file__).parent / "historical"
_cache: dict[str, pd.DataFrame] = {}
def _load(name: str) -> pd.DataFrame:
if name not in _cache:
csv_path = DATA_DIR / f"{name}.csv"
pq_path = DATA_DIR / f"{name}.parquet"
if pq_path.exists():
_cache[name] = pd.read_parquet(pq_path)
else:
df = pd.read_csv(csv_path, low_memory=False)
df.to_parquet(pq_path, index=False)
_cache[name] = df
return _cache[name]
def get_matches() -> pd.DataFrame:
return _load("matches")
def get_goals() -> pd.DataFrame:
return _load("goals")
def get_squads() -> pd.DataFrame:
return _load("squads")
def get_players() -> pd.DataFrame:
return _load("players")
def get_player_appearances() -> pd.DataFrame:
return _load("player_appearances")
def get_team_appearances() -> pd.DataFrame:
return _load("team_appearances")
def _norm(name: str) -> str:
"""Lowercase + strip unicode accents for fuzzy name matching."""
nfkd = unicodedata.normalize("NFKD", name)
return "".join(c for c in nfkd if not unicodedata.combining(c)).lower().strip()
def get_h2h(team_a: str, team_b: str, limit: int = 10) -> list[dict]:
"""Return World Cup matches between team_a and team_b (most recent first)."""
matches = get_matches()
mask = (
(
(matches["home_team_name"] == team_a)
& (matches["away_team_name"] == team_b)
)
| (
(matches["home_team_name"] == team_b)
& (matches["away_team_name"] == team_a)
)
)
df = matches[mask].copy()
df["match_date"] = pd.to_datetime(df["match_date"], errors="coerce")
df = df.sort_values("match_date", ascending=False).head(limit)
rows = []
for _, r in df.iterrows():
home, away = r["home_team_name"], r["away_team_name"]
score = f"{int(r['home_team_score'])}-{int(r['away_team_score'])}"
if r["penalty_shootout"]:
score += f" (pen {int(r['home_team_score_penalties'])}-{int(r['away_team_score_penalties'])})"
if r["home_team_win"]:
winner = home
elif r["away_team_win"]:
winner = away
else:
winner = "Draw"
rows.append(
{
"year": str(r["match_date"].year) if pd.notna(r["match_date"]) else r["tournament_name"],
"stage": r["stage_name"],
"home": home,
"away": away,
"score": score,
"winner": winner,
}
)
return rows
def get_team_wc_record(team_name: str) -> dict:
"""Aggregate World Cup stats for a team across all men's tournaments."""
ta = get_team_appearances()
df = ta[
(ta["team_name"] == team_name)
& (ta["tournament_name"].str.contains("Men", na=False))
]
if df.empty:
return {}
record = {
"total_matches": len(df),
"wins": int(df["win"].sum()),
"draws": int(df["draw"].sum()),
"losses": int(df["lose"].sum()),
"goals_for": int(df["goals_for"].sum()),
"goals_against": int(df["goals_against"].sum()),
}
# Tournament list (chronological, deduplicated)
tournaments = (
df[["tournament_id", "tournament_name"]]
.drop_duplicates("tournament_id")
.sort_values("tournament_id")["tournament_name"]
.tolist()
)
record["tournaments"] = tournaments
return record
def _player_display_name(given_name: str, family_name: str) -> str:
"""Return display name: use family_name only when given_name is 'not applicable'."""
if str(given_name).strip().lower() in ("not applicable", "n/a", "", "nan"):
return str(family_name).strip()
return f"{given_name} {family_name}".strip()
def get_team_top_scorers(team_name: str, limit: int = 5) -> list[dict]:
"""Top men's World Cup goal-scorers for a team (all-time, excl. own goals)."""
goals = get_goals()
df = goals[
(goals["team_name"] == team_name)
& (~goals["own_goal"])
& (goals["tournament_name"].str.contains("Men", na=False))
]
if df.empty:
return []
agg = (
df.groupby(["player_id", "given_name", "family_name"])
.size()
.reset_index(name="goals")
.sort_values("goals", ascending=False)
.head(limit)
)
return [
{
"name": _player_display_name(r["given_name"], r["family_name"]),
"goals": int(r["goals"]),
}
for _, r in agg.iterrows()
]
def get_player_wc_stats(given_name: str, family_name: str) -> dict:
"""WC caps and goals for a specific player (by name)."""
pa = get_player_appearances()
norm_given = _norm(given_name)
norm_family = _norm(family_name)
mask = pa.apply(
lambda r: _norm(str(r["given_name"])) == norm_given
and _norm(str(r["family_name"])) == norm_family,
axis=1,
)
player_apps = pa[mask]
caps = len(player_apps)
goals = get_goals()
goal_mask = goals.apply(
lambda r: _norm(str(r["given_name"])) == norm_given
and _norm(str(r["family_name"])) == norm_family
and not r["own_goal"],
axis=1,
)
wc_goals = int(goal_mask.sum())
return {"caps": caps, "goals": wc_goals}