NSBA Draft Analyticsembargoed · 2026-06-06

00_data_audit.md

NSBA Processed-Data Integrity Audit

NSBA Processed-Data Integrity Audit

Auditor role: adversarial data-integrity reviewer (tried to break the parsers). Date: 2026-05-30 Method: independent recompute of every processed table from the raw .xlsx using /home/david/code/nsba/.venv/bin/python, plus hand-verification of 5 random games and targeted teardown of every anomaly. Audit driver: scripts/audit_data.py (read-only; writes nothing to data/raw).

Verdict

TRUSTWORTHY ENOUGH TO MODEL ON, with documented carve-outs. The parsers are faithful: combine reconstructs exactly (0 mismatches) and 181/193 games reconcile exactly. Every one of the 12 reconcile failures is a defect in the source spreadsheet (unplayed template tabs, joke/junk sheets, or sheets whose running-score formula was reset mid-game), not a parser bug. There is no confusion between combine (pyramidal) and game (standard) scoring. The main thing a modeler must do before fitting: filter games on reconciles == True (and drop 0-0 templates), and treat a handful of nsba3 games as team-level-only.

No blocker issues. Issues below are major (must handle before modeling) or minor (cosmetic / documented limitation).


CHECK 1 — Do parsed game scores reconstruct the displayed final scores?

game_id final cell event-sum classification
nsba1 Play-In cn vs ok 140-60 150-56 real game, source running-tally ≠ final cell
nsba1 Play-In aa vs dd 66-106 70-110 real game, source mismatch
nsba1 Playoff1 dv vs sb 65-28 123-122 real game, running formula RESET mid-game
nsba1 Playoff1 aa vs st 42-36 102-78 real game, segmented sheet
nsba1 Playoff1 cn vs me 50-32 106-94 real game, segmented sheet
nsba1 Playoff1 bb vs kk 0-0 44-96 unplayed TEMPLATE tab (Winner banner = "Tie")
nsba1 Playoff1 aa vs kk 0-0 44-12 unplayed template (33 rows, leftover buzz junk)
nsba1 Playoff2 cn vs sb 0-0 54-26 unplayed template
nsba1 Playoff2 a vs kk 0-0 44-12 unplayed template
nsba2 RR11 yared v xpoes 178.339-0 168.51-0 junk sheet (non-integer values, opponent forfeit)
nsba2 RR11 coby vs cryo 53.1-99.318 46-36 junk sheet
nsba2 RR11 caleb vs hansen 80.2-33.252 77.2-33.252 junk sheet

Root causes (all in the source, not the parser): - Segmented / reset running scores (e.g. dv vs sb): the sheet's running-score column resets twice (rows q12 and q23), so the FINAL cell reflects only the last segment (65-28) while the leftover buzz cells from earlier segments remain. The parser naively sums every numeric buzz cell → ~2x inflation. The final score is right; the per-event / per-player rows for these games are inflated. - Unplayed template tabs: 33-row template copies left in the workbook with leftover demo buzzes; Final Score = 0-0, Winner banner = "Tie". - RR11 junk: someone entered fractional "scores" (0.2, 9.629, 9.9, 0.01); the parser correctly clamps tossup cells to {-4,0,4} and so diverges from the garbage final cell.

Hand-reconstruction of clean games confirms parser logic is correct for BOTH layouts: limitless-usmic (paired/nsba3) → 86-46 ✓ ; Connor vs Cryo (wide/nsba2) → 92-44 ✓ , independently summed.

Severity: MAJOR — not because the parser is wrong, but because downstream code must (a) gate on reconciles == True, and (b) not use game_player_box / tossups_long rows from the 5 real-but-inflated nsba1 games (56 player-box rows) or the 5 unplayed 0-0 templates. The published final scores are fine.


CHECK 2 — Combine pyramidal scoring vs game standard scoring (no cross-contamination)

Severity: MINOR (the two -1 combine cells; otherwise clean). The two systems are correctly kept separate.


CHECK 3 — Double-counting of tossups / bonuses

Severity: MINOR — confined to already-flagged junk games, plus 2 isolated mis-attributed bonuses.


CHECK 4 — Category-label normalization

Note: the two scoring systems intentionally use different code vocabularies (games: b/ch/cs/ess/m/p; combine: bio/chem/cs/ess/math/phys). This is consistent within each system but a join across systems needs an explicit crosswalk (b↔bio, ch↔chem, m↔math, p↔phys). Document this.

Severity: MINOR (≈12 junk subject cells; cross-system crosswalk needed).


CHECK 5 — Season mapping (2022/2023/2025 → nsba1/2/3)

Severity: NONE — mapping is correct.


CHECK 6 — Identity-map collisions / wrong merges

Severity: MINOR (3 wrong display labels; no wrong merges).


CHECK 7 — Missing players / empty tabs / silently dropped rows

Severity: MINOR (nsba2 pyramidal granularity missing; 4 nsba3 games are team-level only — both are source limitations, faithfully represented).


CHECK 8 — Does CS appear as a game tossup subject in nsba2 & nsba3?

season subject counts CS present legacy ENE present
nsba1_2022 b417 ch429 ess420 m421 p456 ene282 No (correct) Yes
nsba2_2023 b342 ch373 cs358 ess370 m335 p374 Yes No
nsba3_2025 b267 ch284 cs231 ess276 m280 p286 Yes No

Confirmed: CS replaces Energy starting nsba2; nsba1 still uses legacy ene. This matches the ground-truth spec exactly.

Severity: NONE — correct.


Issue summary (tagged)

# Severity Issue
1 MAJOR 12/193 games don't reconcile. 5 are real games whose per-event/per-player rows are INFLATED (segmented/reset running-score sheets — nsba1 Play-In ×2, Playoff1 ×3). Final scores are correct; tossups_long/game_player_box rows (56 box rows) are wrong. Must drop these for player-level modeling.
2 MAJOR 5 unplayed 0-0 template/forfeit games present in games_meta as winner="Tie" (4 nsba1 + nsba2 RR10), and the 4 nsba1 ones emit ~50 spurious player-box rows from leftover template buzzes. Will corrupt W/L, PF/PA, and player aggregates unless filtered.
3 MAJOR The 3 nsba2 RR11 sheets are junk (fractional/garbage values, one opponent forfeit at 0). Exclude from any analysis.
4 MINOR 2 bonuses mis-attributed to the wrong team (nsba1 Round3 aa vs sb q14, Round8 na vs sb q8) — baked-in source scorekeeping errors.
5 MINOR ~12 game tossup rows have un-normalized/junk subject codes (en, ban, jhg, jkj, jkjk). 152 tossups have null subject (expected for dead tossups).
6 MINOR 2 nsba1 combine cells store -1 in the points column instead of penalty (Avaneesh/Chem3, Harry Jin/Phys2). Reconstruct still matches official total.
7 MINOR Identity canonical_name picker mislabels 3 clusters with a discord display handle instead of the real name (P0368→Kaiwen Yao, P0148→McNair Shah, P0595→McWei Wei). Clustering is correct; only the label is wrong.
8 MINOR (limitation) nsba2 combine is aggregate-only → no per-question pyramidal data. 4 nsba3 Round-1 games are team-level-only (no player names). Faithfully represented, but plan around them.
NONE Season mapping correct; CS↔Energy handling correct; combine↔game scoring NOT confused; combine reconstructs exactly; no double-counted bonuses; no wrong identity merges.

Recommended guardrails before modeling

  1. Always filter games_meta on reconciles == True for any team- or player-level aggregation, AND drop score_a==0 & score_b==0 games. This removes all 12 problem games (the 5 inflated real games, 5 templates, RR11 junk). 181 clean games remain.
  2. When using game_player_box/tossups_long, join back to games_meta and exclude reconciles==False game_ids so the 56 inflated box rows never enter player stats.
  3. Add a crosswalk for the two subject vocabularies (b↔bio, ch↔chem, m↔math, p↔phys, cs↔cs, ess↔ess; ene = legacy energy, nsba1 only).
  4. Treat nsba2 combine as category-level only; treat the 4 named nsba3 games as team-level only.
  5. (Optional) Fix pick_name/looks_like_realname so a real name always beats a discord display handle for canonical_name.

NSBA Draft Analytics · embargoed until after the SSB draft · ← hub