Treść z repozytorium z zachowaniem nagłówków, przykładów, kodu, tabel, linków i obrazów.
EDA for Sports Data
Outcome
Understand the data well enough that the next decision is explicit. Sports EDA is not one call to describe(): it is a structured review of grain, keys, time coverage, entities, missingness, targets, schedule structure, outliers, structural breaks, and fields unavailable at prediction time T.
Finish with a written go, repair, or stop decision. A clean EDA report does not certify that engineered features are time-safe.
When to Use This Skill
Use when:
- a sports table is new, freshly loaded, or not yet trusted;
- grain, keys, coverage, missingness, or base rates need an honest audit;
- before feature engineering or first model fit;
- a weird model result might just be bad data structure.
Do not use this skill as a substitute for:
- formal leakage verdicts →
leakage-audit; - feature construction →
feature-rules/time-series-sports; - source selection →
data-sources.
| Need | Go instead |
|---|---|
| Choose a public source | data-sources |
| Build legal features | feature-rules |
| Prove time-safety | leakage-audit |
| Charts only | sports-visualization |
Required context
Before calculating summaries, record the question, target, prediction time T, claimed row grain and key, source/retrieval time, requested period, completion status, and populations included. If grain or T is ambiguous, resolve it first.
Use the EDA checklist as the full run sheet. Read the grain guide when row meaning, keys, or aggregation are in doubt. Use the red-flags guide when a count, base rate, or model result is odd.
Workflow
- Define one row: game, team-game, player-game, play, possession, pitch, or event.
- Count rows/columns; test natural-key uniqueness and rows per contest.
- List seasons, weeks/rounds, dates, gaps, partial periods, and active seasons.
- Count teams/players by season and inspect ID churn and failed joins.
- Measure missingness overall and by season, source, role, and completion state.
- Inspect targets, base rates, impossible values, ties, overtime, and zero inflation.
- Slice meaningful predeclared groups: home/away, phase, role, venue, and era.
- Find rule, schedule, provider, tracking, definition, and population breaks.
- Flag fields unavailable at T; reserve the formal verdict for
leakage-audit. - Write findings, repairs, limitations, and the decision.
If grain is wrong or keys are unexplained duplicates, stop before modeling.
Grain and key diagnostics
| Grain | One row | Typical key | Main trap |
|---|---|---|---|
| Game | one contest | game_id | mixing team perspectives |
| Team-game | one team in one contest | game_id, team | treating doubled rows as games |
| Player-game | one player in one contest | game_id, player_id | ignoring DNP/inactive policy |
| Play/event | one event | game_id, event_id | using post-event fields at pre-event T |
| Pitch | one pitch | game_pk, pitch_no | unstable event ordering |
key = ["game_id", "team"]
dupes = panel.duplicated(key, keep=False)
print("duplicate key rows:", int(dupes.sum()))
print(panel.loc[dupes].sort_values(key).head(20))
print(panel.groupby("game_id", dropna=False).size().value_counts().sort_index())Do not call drop_duplicates() until the cause is known. Repeated pulls, provider revisions, multiple competitions, and mixed grains need different repairs. A normal team-game panel usually has two rows per game.
Coverage and entity balance
coverage = (
panel.groupby(["season", "week"], dropna=False)
.agg(rows=("game_id", "size"), games=("game_id", "nunique"),
teams=("team", "nunique"))
.reset_index()
)
print(coverage.to_string(index=False))
print(panel.groupby(["season", "team"]).size().unstack(0).fillna(0))Investigate empty periods, low game counts, abrupt entity-count changes, and the tail of an active season. Imbalance can be real—byes, playoffs, injuries, promotion/relegation—but can also reveal missing loads. Use stable IDs and a documented crosswalk for relocations and renames.
The portable coverage helper expects season, week, game_id, and team. Map alternate schemas explicitly:
python /path/to/eda-sports/scripts/coverage_table.py --input games.csv
python /path/to/eda-sports/scripts/coverage_table.py \
--input games.parquet --season-col year --period-col round \
--game-col event_id --team-col club_idPanel base rates
On a complete two-row team-game panel, overall won.mean() is near 0.5 because each game contributes both perspectives. It is not home advantage.
home = panel.loc[panel["is_home"].eq(1)].copy()
print("overall team-row win rate:", panel["won"].mean())
print("home win rate:", home["won"].mean())
print(home.groupby("season")["won"].mean())Ties, neutral sites, forfeits, incomplete games, and duplicate perspectives can alter this identity. Explain them rather than forcing the expected value.
Missingness
Overall rates can hide an era or provider boundary:
critical = [c for c in ["event_time", "points_for", "points_against", "won"] if c in panel]
print(panel[critical].isna().mean().sort_values(ascending=False))
print(panel.groupby("season")[critical].agg(lambda s: s.isna().mean()))Classify each important pattern as structurally expected, temporarily unavailable, failed collection/join, not applicable, or unknown. Never silently fill early-history rolling nulls with zero; feature rules must define a prior, missing indicator, or minimum-history threshold.
Targets, distributions, and outliers
print(home["point_diff"].describe(percentiles=[.01, .05, .5, .95, .99]))
print(panel["won"].value_counts(dropna=False, normalize=True))import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.hist(home["point_diff"].dropna(), bins=40, edgecolor="black", alpha=.85)
ax.axvline(0, color="red", linestyle="--")
ax.set(title=f"Home point differential (n={len(home):,})", xlabel="point differential")
ax.grid(alpha=.25, axis="y")
plt.show()Check scores, margins, counts, and rates against sport semantics. Determine whether extremes are authentic, unit errors, or duplicates. For count targets, report zeros and overdispersion; for players, slice by role and playing-time eligibility before comparing distributions.
Structural breaks
Before pooling seasons, check rule and overtime changes, schedule length, shortened seasons, labor disruptions, expansion/relocation, tracking start dates, provider/schema migrations, definition revisions, and playoff mixing. A real break may require a known-at-T era flag, separate analyses, a sliding window, or exclusion. Do not normalize it away without explanation.
Leakage scouts
For a pre-event task, current-event outcomes and summaries are legal labels but illegal predictors: current score, margin, result, yards, EPA, win probability, box-score totals, final-season aggregates, and post-event participation.
suspects = {"points_for", "points_against", "won", "point_diff", "final_score"}
print("present suspects:", sorted(suspects.intersection(panel.columns)))Presence is not failure; use as a pre-event feature is. Keep candidate features separate and send them through feature-rules and leakage-audit.
Automated panel report
panel_report.py reads user-owned CSV, Parquet, JSON, JSONL, or NDJSON and expects season, game_id, team, is_home, and binary won, with mapping flags. Install pandas; Parquet also needs pyarrow or fastparquet.
python /path/to/eda-sports/scripts/panel_report.py \
--input games.parquet --out data/eda.json
python /path/to/eda-sports/scripts/panel_report.py \
--input games.csv --out data/eda.json --season-col year \
--game-col event_id --team-col club --home-col home_flag --outcome-col winDo not guess missing columns. Stop or create an explicit reviewed mapping. The report uses only the documented EDA decisions: GO (status 0), REPAIR (status 1), or STOP (status 2). A game without exactly two distinct team rows and exactly one home row, a null natural key, or a duplicate (game, team) key is structurally incompatible with this helper and returns STOP. Missing outcomes return REPAIR; they may be legitimate future events, but must be separated from completed-event analysis. GO still requires the separate time and leakage reviews described above.
Red flags
| Symptom | Interpretation | Action |
|---|---|---|
| duplicate natural keys | mixed grain or repeated records | inspect groups; repair cause |
| wrong rows per contest | incomplete/mislabeled panel | rebuild or restate grain |
| entity count jump | real expansion or ID churn | verify and document crosswalk |
| empty periods | incomplete pull or active season | quantify and reload if unexpected |
| constant scores/target | corrupt source or filter | stop and verify source |
| overall win rate near .5 on team-game | expected pairing | use one perspective for game claims |
| early form null | no history | specify prior/minimum history |
| near-perfect first model | leakage until disproved | stop; run leakage-audit |
EDA note template
Question / target / decision time T:
Dataset, source, retrieval/version:
Grain and natural key:
Population and period:
Rows / games / entities:
Key and rows-per-game findings:
Coverage gaps:
Missingness findings:
Target, distribution, and slice findings:
Structural breaks:
Leakage suspects:
Repairs performed / remaining limitations:
Artifacts and plots:
Decision: GO | REPAIR | STOP
Reasons and next action:Worked example
For a two-season pre-game team win project: declare (game_id, team); run both helpers; confirm two rows per completed game and expected period coverage; measure home win rate on home rows; separate future scheduled games from failed result joins; mark current score/result fields target-only; record era/provider breaks; and issue GO only for the supported population.
Integrity and resource routing
- State grain before metrics; never model through unexplained duplicate keys.
- Never hide incomplete periods, missing entities, or repairs.
- Compare base rates on the correct independent unit.
- Preserve raw data and separate outcomes from feature candidates.
- Use
references/eda_checklist.mdfor completeness,
references/grain_guide.md for grain/key questions, and references/red_flags.md for diagnosis.
- Run
scripts/coverage_table.pyfor coverage and
scripts/panel_report.py for a machine-readable team-game summary.

