Spine build: player_season_master.csv + team_season.csv
Spine build: player_season_master.csv + team_season.csv
Date: 2026-05-30
Script: scripts/build_master.py (idempotent; re-run reproduces byte-identical output)
Outputs:
data/processed/player_season_master.csv (207 rows × 50 cols),
data/processed/team_season.csv (41 rows × 19 cols)
This is the analytical backbone every downstream analysis reads. It is built only from GAME data (standard Science Bowl: TU +4 / neg −4 / bonus +10). The combine — a pyramidal, gameable tryout — is attached as a same-season link, never mixed into value stats. Per the priors, the combine is treated as a gamed ceiling (a seed signal); game data is the trusted value source.
Method
- Clean game set.
games_metafiltered toreconciles==TrueAND not(score_a==0 & score_b==0) → 180 games (nsba1=66, nsba2=61, nsba3=53). All player/team stats inner-join to thisgame_idset. - Filter note:games_metahas 181 reconciling games; the not-both-zero guard drops one more — nsba2RR10|cryo v caleb, a genuine 0-0 game — yielding 180. This applies the stated guardrail filter exactly. - Team-name canonicalization. Folded case/whitespace variants within each season
(e.g.
Mount Ellis≡mount ellis,no like blue≡nolikeblue) to the most-frequent spelling, applied across all game tables so win%/rosters don't split. Merged 8 split team-seasons (1 nsba1, 7 nsba3); team_season went 49 → 41 rows. - Identity. Joined game players via
canonical_players.csvon (source='game', season, player_raw). All 241 clean-game player-seasons matched — 0 join losses. - Value stats from clean games: games_played, toss_points, n_correct, n_negs,
neg_rate, points_per_game, and PPTF = toss_points / tossups_faced.
-
tossups_faced= recordedtossups_present(TUH) where the sheet has it, elsegames × season-mean tossups/game.tuh_source∈ {sheet, estimated, mixed} flags this. TUH is on-sheet for all nsba1 games and ~13 nsba2 games; nsba3 records no TUH (paired layout), so its PPTF denominator is a games×rate proxy. - Per-category points + per-subject PPTF for b/ch/cs/ess/m/p (+ene for nsba1). - Context & links: team bonus points (team-attributed), team win% and playoff-game
count, same-season combine (overall/rank/paid/eligible + per-category, crosswalked),
and qualitative flags from
discord_player_signals.csv(merged by canonical_id).
Numbers
| table | rows | nsba1 | nsba2 | nsba3 |
|---|---|---|---|---|
| player_season_master | 207 | 75 | 60 | 72 |
| team_season | 41 | 12 | 13 | 16 |
- Distinct canonical players: 181 (some span multiple seasons).
- Combine link: 114 / 207 player-seasons (55%) — nsba1 8/75 (11%), nsba2 46/60 (77%), nsba3 60/72 (83%). nsba1 is low by nature: nsba1 game sheets use nicknames/handles, the combine stores real names, so few resolve. Join-loss accounting: of 203 combine overall rows in game seasons, 199 resolved to a canonical_id; 114 of those also have game data (the actual link); 85 are combine-only players with no game appearance (correctly absent from the master).
- Qualitative signals: 17 player-seasons carry a flag — the 12 nsba4-era signal handles that also have game history, propagated to each of their seasons via canonical_id (e.g. Anurag Sodhi flagged in both nsba1 and nsba2).
- tuh_source: 78 sheet, 56 mixed, 73 estimated (the estimated rows are essentially all nsba3, where no TUH exists).
- Distributions (sanity): PPTF mean 0.27 (min −0.16 for a net-negative buzzer, max 1.21 for the top buzzer); neg_rate mean 0.19.
Validation passed
- Per-player toss_points reconciles exactly with buzz-attributed
tossups_longsums: nsba1 3752, nsba2 3596, nsba3 2596 (both sources agree). - Each season's team_season
games= exactly 2× clean games (132 / 122 / 106). - Every player's
(season, team)exists in team_season; all team_win_pct ∈ [0, 0.89]. - 240 nsba3 toss-up points from the 4 team-level-only Round-1 games stay at team level and correctly add zero player rows.
- Build is idempotent (identical md5 on re-run).
Limitations / caveats
- nsba3 PPTF is a proxy. The paired layout records no per-player tossups-heard, so
nsba3
tossups_facedisgames × season-mean, not an exact heard count. Treat nsba3 PPTF as games-rate-adjusted, not a true per-question rate. Cross-season PPTF comparisons should weight nsba1/nsba2 (sheet-TUH) more confidently. - Bonus is team-level only. Science Bowl bonuses are answered by the team, not
attributable to an individual; the master carries
team_bonus_points_seasonas context, not a personal stat. - 3 player-seasons have ≤8 toss-up points on blank/junk-subject buzzes (
nan/enin the source), so their per-categorypts_*sum is up to 8 short oftoss_points. A documented source-sheet residual, not a parser/build error. - Small n everywhere. Three seasons, 41 team-seasons, many players with <11 games. Per-subject PPTF especially is thin (some subjects = a handful of tossups per player). Quantify uncertainty before ranking on these rates.
- nsba1 combine coverage is sparse (8 links). Any combine↔game analysis that needs nsba1 is effectively unpowered; nsba2/nsba3 are the usable seasons for that join.
- Fuzzy identity rows carry through. Game players resolved by
fuzzy_firstname(0.6) or in the 65needs_reviewset keep their raw mapping; use those rows cautiously. - The combine is gameable ("biking"); never read
combine_overallas ability. CS is the cleaner combine signal where present.