HenriLD Claude Opus 4.8 commited on
Commit
b64f8b4
Β·
1 Parent(s): 19682f4

Make "most improved" reliable via a precomputed player_improvement view

Browse files

Instead of dropping the question, give the model a clean path. The failure was
that "most improved" forced a hand-written two-season self-join, which errored
on the free model and fell back unguarded to fringe leaders.

New player_improvement view: season-over-season deltas (ts_pct, efg_pct, and
per-36 pts/reb/ast) joining player_advanced to the prior season (key derived
from the season string), floored at gp >= 20 both seasons so noise jumps can't
top it. A plain view over the small rollup β€” no game-log scan, stays fresh with
player_advanced. "Most improved" is now ORDER BY pts_per36_delta DESC.

Verified read-only: surfaces real breakouts (Alexander-Walker 13.4->22.5/36,
Duren, Brooks, MPJ), not the prior fringe noise. Prompt + schema updated to
route improvement questions here and forbid the hand-rolled join; restored
"Who improved the most since last season?" to the hint bank.

Migration (apply to Neon before prod):
CREATE OR REPLACE VIEW player_improvement AS ... (see db/enrich.sql)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (3) hide show
  1. app/agent.py +13 -2
  2. app/static/index.html +1 -0
  3. db/enrich.sql +24 -0
app/agent.py CHANGED
@@ -239,6 +239,17 @@ player_advanced β€” one row per player per season (all seasons). Precomputed, so
239
  Leaderboards MUST filter gp >= 30 (and pts_per36 >= 15 for an efficient
240
  SCORER) β€” unfiltered, the TS% leaders are low-minute rim-runners.
241
 
 
 
 
 
 
 
 
 
 
 
 
242
  team_advanced β€” one row per team per season (all seasons). Pace-adjusted:
243
  team (3-letter), season, gp, wins, off_rtg, def_rtg (LOWER is better),
244
  net_rtg, pace (poss/game), efg_pct, tov_rate, oreb_rate, ft_rate. Use for
@@ -422,8 +433,8 @@ their team" β€” pick the metric that actually answers it: an EFFICIENCY/RATE \
422
  rest of the league, clutch vs overall), or the relevant split. Examples: "best \
423
  clutch scorers" -> v_clutch by ts_pct; "most efficient scorers" -> \
424
  player_advanced by ts_pct; "is the Thunder offense or defense" -> team_advanced \
425
- comparing off_rtg vs def_rtg league ranks; "most improved" -> player_advanced \
426
- this season minus last.
427
  - MINIMUM-VOLUME GUARD β€” mandatory on EVERY rate/efficiency/ratio/per-game \
428
  leaderboard, or fringe players with a handful of attempts dominate (a 2-for-2 \
429
  night reads as the "best shooter"). Always add BOTH a games floor AND a \
 
239
  Leaderboards MUST filter gp >= 30 (and pts_per36 >= 15 for an efficient
240
  SCORER) β€” unfiltered, the TS% leaders are low-minute rim-runners.
241
 
242
+ player_improvement β€” one row per player per season: season-over-season change vs
243
+ the SAME player last season. player_name, season, gp, prev_gp, and for ts_pct,
244
+ efg_pct, pts_per36, reb_per36, ast_per36: the current value, prev_<x> (last
245
+ season), and <x>_delta (current - prev; POSITIVE = improved). Already floored
246
+ at gp >= 20 in both seasons. Use for "most improved / breakout / fell off /
247
+ declined": just ORDER BY the relevant *_delta (DESC for improved). Do NOT
248
+ hand-write a two-season self-join β€” query this instead. "Most improved
249
+ scorers" -> ORDER BY pts_per36_delta DESC; "biggest efficiency jump" ->
250
+ ts_pct_delta DESC. (Only the last ~5 seasons of deltas are dense; a player
251
+ needs both seasons on record to appear.)
252
+
253
  team_advanced β€” one row per team per season (all seasons). Pace-adjusted:
254
  team (3-letter), season, gp, wins, off_rtg, def_rtg (LOWER is better),
255
  net_rtg, pace (poss/game), efg_pct, tov_rate, oreb_rate, ft_rate. Use for
 
433
  rest of the league, clutch vs overall), or the relevant split. Examples: "best \
434
  clutch scorers" -> v_clutch by ts_pct; "most efficient scorers" -> \
435
  player_advanced by ts_pct; "is the Thunder offense or defense" -> team_advanced \
436
+ comparing off_rtg vs def_rtg league ranks; "most improved" -> player_improvement \
437
+ ORDER BY pts_per36_delta DESC (never hand-roll the two-season join).
438
  - MINIMUM-VOLUME GUARD β€” mandatory on EVERY rate/efficiency/ratio/per-game \
439
  leaderboard, or fringe players with a handful of attempts dominate (a 2-for-2 \
440
  night reads as the "best shooter"). Always add BOTH a games floor AND a \
app/static/index.html CHANGED
@@ -400,6 +400,7 @@ const HINT_BANK = [
400
  "Rank every team by net rating",
401
  "Curry's scoring efficiency in wins vs losses",
402
  "Rank every team by defensive rating",
 
403
  "Best scorers per 36 minutes",
404
  ];
405
  const HINTS_SHOWN = 6;
 
400
  "Rank every team by net rating",
401
  "Curry's scoring efficiency in wins vs losses",
402
  "Rank every team by defensive rating",
403
+ "Who improved the most since last season?",
404
  "Best scorers per 36 minutes",
405
  ];
406
  const HINTS_SHOWN = 6;
db/enrich.sql CHANGED
@@ -96,6 +96,30 @@ WHERE season_type = 'Regular Season'
96
  GROUP BY player_id, season;
97
  CREATE UNIQUE INDEX IF NOT EXISTS idx_player_advanced ON player_advanced (player_id, season);
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  -- Advanced per-team-season rollup: pace-adjusted ratings + four factors (and the
100
  -- "allowed" mirror), via the Oliver possession estimate. Pace-fair, so it answers
101
  -- "is X more offense or defense", "fastest team", and opponent-strength tiers far
 
96
  GROUP BY player_id, season;
97
  CREATE UNIQUE INDEX IF NOT EXISTS idx_player_advanced ON player_advanced (player_id, season);
98
 
99
+ -- Season-over-season improvement, precomputed so "most improved / breakout /
100
+ -- declined" is a trivial ORDER BY a delta β€” not a fragile two-season self-join
101
+ -- the model writes by hand (which errored and fell back unguarded). A plain
102
+ -- VIEW over the small player_advanced table (self-join on the prior season, so
103
+ -- it stays fresh when player_advanced is refreshed; no game-log scan). The
104
+ -- prior-season key is derived from the season string: '2025-26' -> '2024-25'.
105
+ -- Floored at gp >= 20 in BOTH seasons so noise jumps (a 5-game sample doubling)
106
+ -- can't top the board; rank by any *_delta. Positive = improved.
107
+ CREATE OR REPLACE VIEW player_improvement AS
108
+ SELECT
109
+ a.player_id, a.player_name, a.season,
110
+ a.gp, b.gp AS prev_gp,
111
+ a.ts_pct, b.ts_pct AS prev_ts_pct, round(a.ts_pct - b.ts_pct, 3) AS ts_pct_delta,
112
+ a.efg_pct, b.efg_pct AS prev_efg_pct, round(a.efg_pct - b.efg_pct, 3) AS efg_pct_delta,
113
+ a.pts_per36, b.pts_per36 AS prev_pts_per36, round(a.pts_per36 - b.pts_per36, 1) AS pts_per36_delta,
114
+ a.reb_per36, b.reb_per36 AS prev_reb_per36, round(a.reb_per36 - b.reb_per36, 1) AS reb_per36_delta,
115
+ a.ast_per36, b.ast_per36 AS prev_ast_per36, round(a.ast_per36 - b.ast_per36, 1) AS ast_per36_delta
116
+ FROM player_advanced a
117
+ JOIN player_advanced b
118
+ ON b.player_id = a.player_id
119
+ AND b.season = (left(a.season, 4)::int - 1)::text || '-'
120
+ || lpad((left(a.season, 4)::int % 100)::text, 2, '0')
121
+ WHERE a.gp >= 20 AND b.gp >= 20;
122
+
123
  -- Advanced per-team-season rollup: pace-adjusted ratings + four factors (and the
124
  -- "allowed" mirror), via the Oliver possession estimate. Pace-fair, so it answers
125
  -- "is X more offense or defense", "fastest team", and opponent-strength tiers far