Make "most improved" reliable via a precomputed player_improvement view
Browse filesInstead 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>
- app/agent.py +13 -2
- app/static/index.html +1 -0
- db/enrich.sql +24 -0
|
@@ -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" ->
|
| 426 |
-
|
| 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 \
|
|
@@ -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;
|
|
@@ -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
|