Method & SQL
How every number in this console is made, what it assumes, and where it can be wrong. Each term is defined in plain words first; the math and the exact SQL follow.
Real dataPython · DuckDB · SQL
Terms, in plain words
- Beatable line
- A line where one side's fair chance is above 54.21%, the lowest breakeven on the payout table (the 6-pick Flex Play). A skilled player could build winning entries from these lines alone.
- How it's computedFair chance of the favored side ≥ lowest breakeven. In the data this is called
exposed: theexposedfield in the board data and CSV exports, theexposedandexposed_t24columns, and therecap_exposed.sqlandrecap_top_exposed.sqlqueries. The screens say “beatable” because on a trading desk “exposure” means money at risk, which is a different thing. - Fair chance
- Our best estimate of how often a side really wins, taken from sportsbook prices.
- How it's computedFor each sportsbook quoting both sides: convert American odds to implied chances, then devig them (remove the book's built-in margin) with the power method: find k so poverk + punderk = 1. Translate each book's line to the PrizePicks line (below), then take the middle value across books. Other pick'em apps are never part of it.
- Line translation
- Sportsbooks and PrizePicks often post different numbers for the same player. This converts the book's chance at its line into a chance at the PrizePicks line.
- How it's computedCount stats (Receptions, Pass TDs) use a Poisson distribution: solve for the average that matches the book's chance, then read the chance at the PrizePicks line. Yardage stats use a normal distribution whose spread is a set share of the line, fitted per stat on Weeks 1–2 (see Model settings).
- Ties
- A whole-number line can land exactly on the number. PrizePicks voids that pick and the entry drops to a smaller payout.
- How it's computedFair chances are measured assuming no tie. Actual win rates leave ties out. The board shows each whole-number line's tie chance.
- Breakeven
- The win rate a player needs on every pick to get their entry fee back over time.
- How it's computedThe per-pick chance where an entry's expected payout equals its entry fee, assuming picks are independent. Solved by bisection on the payout table.
- Hold
- The share of entry fees PrizePicks keeps after paying out. Negative means it paid out more than it took in.
- How it's computed1 − payouts ÷ entry fees. Expected hold prices each pick at its fair chance (picks independent). Actual hold uses settled results.
- CLV (closing line value)
- Whether a player's picks were still the better side when the game kicked off. Skilled players beat the closing price; lucky ones don't.
- How it's computedThe fair chance, at kickoff, of the side a player picked, at the line they picked. Above 50% means the market moved toward the pick after it was made. It separates skill from luck in tens of picks, where win rate needs hundreds.
- Stale line
- The sportsbooks moved and PrizePicks didn't.
- How it's computedBetween two daily snapshots, the sportsbooks' middle line changed and the PrizePicks line stayed the same. Needs at least two snapshots in the week.
- T-72h, T-24h, kickoff
- The three moments each line is recorded in Weeks 1–2: three days before kickoff, one day before, and minutes before.
- How it's computedHistorical snapshots at 72 hours, 24 hours and 10 minutes before each game, plus goblin and demon lines at 10 minutes. The live board adds one snapshot a day at 11 AM ET.
- Entry Tape rules
- Three alerts a Game Ops analyst could act on during a Sunday.
- How it's computed(1) A beatable line gets at least 12 points more of its entry fees on its fair side than the tape overall puts on that side, over 10+ picks in the last 400 entries. (2) An account averages a 53.5%+ fair chance over 12+ picks. (3) Six or more same-game stacks on one game in 120 entries. Expected hold on the tape is priced from fair chances, so a lucky hour doesn't read as a pricing problem.
- Simulated players
- Made-up players with made-up entries, graded on real lines and real results.
- How it's computed1,000 players in four groups (700 casual, 150 stackers, 100 line shoppers, 50 sharps) pick from the real Weeks 1–2 board at one day out. Every pick is graded on the real box score. The detection chart resamples each group's real graded picks.
Where the data comes from
- Lines. The Odds API, a licensed feed. PrizePicks, Underdog and Pick6 lines come from its pick'em region; the sportsbooks are Pinnacle, FanDuel, DraftKings, BetMGM, Caesars, BetRivers and Fanatics. No PrizePicks systems are ever called.
- History. Weeks 1–2 snapshots at three days, one day and 10 minutes before each game, plus goblin and demon lines at 10 minutes. About 6,800 API credits.
- Live board. One snapshot a day (about 96 credits), stored in Vercel Blob. The Live Board compares the latest two.
- Results. nflverse weekly player stats, matched on name, team and week. 1,001 of 1,104 standard lines matched a box score; the rest are dropped.
- Players. Simulated. Their behavior is made up; every pick they make is graded on real lines and real results.
Model settings
| Setting | Value |
|---|---|
| Outcome spread, Pass Yds | 0.32 |
| Outcome spread, Rush Yds | 0.78 |
| Outcome spread, Rec Yds | 0.93 |
| Outcome spread, Rush+Rec Yds | 0.42 |
| Breakeven, 2-pick Power Play | 57.74% |
| Breakeven, 6-pick Flex Play (lowest) | 54.21% |
Outcome spread is the typical miss as a share of the line (for example, 0.32 means a typical miss of about 80 yards on a 250-yard Pass Yds line).
Known limits
- A normal distribution for yardage gives too little room at zero on low lines. Errors grow with the gap between the book's line and PrizePicks'.
- Every sportsbook counts equally. Weighting sharper books (Pinnacle) more is the natural next step.
- Two weeks is a small sample for findings inside one stat. Each one is marked a watch item until it holds for 4+ weeks.
- The Odds API marks pick'em prices as indicative. Real PrizePicks payouts can vary by entry and by state.
- The Payout Lab links picks with one shared setting. Real same-game links vary by stat (a QB and his receiver move together; a running back and a receiver often don't).
- Simulated sharps are defined with the same market prices CLV measures, so how fast they get caught is a best case.
The SQL
The exact queries behind each Line Recap panel, in analysis/sql/, run in DuckDB by analysis/build.py. The macro be() is the lowest breakeven on the payout table.
recap_alts.sql
-- Goblins and demons: realized hit rate of alternate lines vs the consensus fair probability.
select
kind,
stat,
count(*) as n,
round(avg(hit), 4) as hit_rate,
round(avg(p_fair), 4) as fair_p,
round(avg(abs(line - std_line)), 1) as avg_offset
from alts
where p_fair is not null
group by all
order by kind, n desc;recap_by_stat.sql
-- Line accuracy by stat type at the closing PrizePicks line.
-- bias > 0 means players beat the line on average (line set too low).
select
stat,
count(*) as n,
round(avg(actual - close_line), 2) as bias,
round(avg(abs(actual - close_line)), 2) as mae,
round(median(close_line), 1) as median_line,
round(avg(case when actual <> close_line then (actual > close_line)::int end), 4) as over_rate,
round(avg(close_p_over), 4) as fair_over
from props
where close_p_over is not null
group by stat
order by n desc;recap_by_tier.sql
-- More (over) hit rate by line tier. Tiers are terciles of the closing line within each stat,
-- a proxy for player role (high line = featured player, low line = role player).
with t as (
select *, ntile(3) over (partition by stat order by close_line) as tier
from props
)
select
stat,
case tier when 1 then 'Low line' when 2 then 'Mid line' else 'High line' end as tier,
count(*) as n,
round(avg(case when actual <> close_line then (actual > close_line)::int end), 4) as over_rate,
round(avg(close_p_over), 4) as fair_over
from t
group by all
order by stat, tier;recap_by_week.sql
-- More hit rate by week and overall (ties excluded), with a binomial standard error.
select
coalesce(week::varchar, 'All') as week,
count(*) filter (where actual <> close_line) as n,
round(avg(case when actual <> close_line then (actual > close_line)::int end), 4) as over_rate,
round(sqrt(0.25 / count(*) filter (where actual <> close_line)), 4) as se
from props
group by rollup (week)
order by week;recap_calibration.sql
-- Is the sportsbook consensus a good referee? Bucket fair P(over at the PP line), compare to realized.
select
round(floor(close_p_over * 20) / 20, 2) as bucket,
count(*) as n,
round(avg(close_p_over), 4) as predicted,
round(avg(case when actual <> close_line then (actual > close_line)::int end), 4) as realized
from props
where close_p_over is not null
group by all
having count(*) >= 10
order by bucket;recap_exposed.sql
-- Exposure: a prop is "exposed" when the fair probability of either side clears the cheapest
-- entry breakeven (6-pick Flex, ~54.2%). A skilled user can build +EV entries from these alone.
-- Ties void the pick on PrizePicks, so they are excluded from realized hit rates.
with s as (
select
'T-24h' as snapshot,
greatest(t24_p_over, 1 - t24_p_over) as fav_p,
case when actual = t24_line then null
when t24_p_over >= 0.5 then (actual > t24_line)::int
else (actual < t24_line)::int end as fav_hit
from props where t24_p_over is not null and t24_line is not null
union all
select
'Close',
greatest(close_p_over, 1 - close_p_over),
case when actual = close_line then null
when close_p_over >= 0.5 then (actual > close_line)::int
else (actual < close_line)::int end
from props where close_p_over is not null
)
select
snapshot,
fav_p >= be() as exposed,
count(*) as n,
round(avg(fav_p), 4) as fair_fav_p,
round(avg(fav_hit), 4) as realized_fav_hit
from s
group by all
order by snapshot desc, exposed desc;recap_movement.sql
-- Line management between T-24h and close: when a prop was exposed at T-24h, did PrizePicks
-- move the line toward the market before kickoff?
with m as (
select
stat,
greatest(t24_p_over, 1 - t24_p_over) >= be() as exposed_t24,
close_line <> t24_line as moved,
case when t24_p_over >= 0.5 then close_line > t24_line
else close_line < t24_line end as moved_toward_market,
abs(close_line - t24_line) as move_size
from props
where t24_p_over is not null and t24_line is not null
)
select
stat,
exposed_t24,
count(*) as n,
round(avg(moved::int), 4) as moved_rate,
round(avg((moved and moved_toward_market)::int), 4) as moved_toward_rate,
round(avg(case when moved then move_size end), 2) as avg_move
from m
group by all
order by stat, exposed_t24 desc;recap_ties.sql
-- Whole-number lines: how often PrizePicks posts them, and how often they tie (voiding the pick).
select
stat,
count(*) as n,
round(avg((close_line = floor(close_line))::int), 4) as whole_line_share,
round(avg(case when close_line = floor(close_line) then (actual = close_line)::int end), 4) as tie_rate_on_whole
from props
group by stat
order by whole_line_share desc, n desc;recap_top_exposed.sql
-- The most exposed props at close, with how they finished (ties void the pick).
select
week, game, player, stat,
close_line as pp_line,
close_book_line as book_line,
case when close_p_over >= 0.5 then 'More' else 'Less' end as fair_side,
round(greatest(close_p_over, 1 - close_p_over), 4) as fair_p,
actual,
case when actual = close_line then 'Tie (void)'
when (close_p_over >= 0.5) = (actual > close_line) then 'Won'
else 'Lost' end as fair_side_result
from props
where close_p_over is not null
order by greatest(close_p_over, 1 - close_p_over) desc
limit 12;recap_vs_underdog.sql
-- Board alignment with the closest competitor at close.
select
stat,
count(*) as n,
round(avg((close_line = ud_close_line)::int), 4) as same_line_rate,
round(avg(close_line - ud_close_line), 2) as avg_diff,
round(avg(abs(close_line - close_book_line)), 2) as avg_abs_gap_to_books
from props
where ud_close_line is not null and close_book_line is not null
group by stat
order by n desc;