METHODOLOGY
THE INDEX
The Kalindro value is an integer on a 0–10000 internal scale, displayed 0–100. It is computed per (sport, format), stored append-only, and method-versioned: a value is never edited in place — a correction is a new revision, and the method that produced every number is recorded beside it.
MLB PITCHING LEADERBOARD — QUALIFICATION
Outs, not innings pitched, is the canonical workload quantity: IP is base-3 notation (".1" and ".2" mean one and two outs, not tenths of an inning), so it is never summed, sorted, or thresholded on directly — every one of those operates on outs, and IP is derived (outs ÷ 3) only for display. Sorting a "39.1"-style string would put 39.2 after 39.10 were it read as a number.
The MIN IP control mirrors batting's MIN PA control: a rate leaderboard without a qualifier is noise, and the threshold is a visible preset, not a hidden constant. MLB's own ERA-title qualifier is 1 IP per team game — 162 IP (486 outs) for a 162-game season — and it is offered as the top preset here, but it is NOT the default, because it is a starter-only bar by construction. Measured directly against the 2025 register (873 folded pitchers, one row per player): at 486 outs, 52 starters qualify and zero relievers do, because no reliever workload in modern usage approaches 162 IP (2025's relief population: median 44 outs / 14.7 IP, maximum 250 outs / 83.1 IP). The default threshold is 150 outs (50 IP) instead — comfortably above the median reliever's season workload, so a September call-up does not qualify, while seating 101 real relievers alongside 238 starters (339 total, 2025). Selecting the strict 162 IP preset is a visible, informed choice to view a starters-only board, not a silent exclusion.
MIN IP alone still mixes two different statistical populations on one ranked board: starters and relievers are not drawn from the same distribution, and ranking them together is the same pool-composition error the NFL draft board's gap column had to retire (cross-position ranking) — it makes an ERA or K/9 leaderboard read as whichever role happens to dominate the extremes, not as leadership within either role. Measured directly (2025, MIN IP 50 pool): starting pitchers (n=177) post ERA median 4.11 / mean 4.19 and K/9 median 8.34 / mean 8.32; relief pitchers (n=162) post ERA median 3.29 / mean 3.47 and K/9 median 8.84 / mean 9.12 — a real, non-trivial gap in both directions, not sampling noise.
The ROLE control (ALL / STARTERS / RELIEVERS) filters on this split, and defaults to STARTERS rather than ALL because the mixed board is the one shown above to be misleading. Role is derived MECHANICALLY, never hand-classified: games started ÷ games ≥ 0.5 is a starter, otherwise a reliever — the majority-of-appearances rule, not "started at least one game." That cruder cut was tried and rejected: in the 2025 register it classifies 61 clearly-relief pitchers as starters purely because each made one early-season spot start (e.g. Griffin Jax, 73 games / 2 starts; Wandy Peralta, 71 games / 1 start). The majority rule resolves every one of those 61 to reliever, and still resolves a genuine swingman to exactly one bucket by the same rule (Joey Cantillo, 2025: 34 games, 13 starts, GS/G = 0.38 → reliever) — a stated mechanical cut, not a scouting judgment.
whoaBUH AND UURC+ — KALINDRO'S OWN METRICS
whoaBUH measures a hitter's overall offensive value per plate appearance by weighting each way of reaching base according to how many runs it is actually worth. UURC+ takes whoaBUH and adjusts it for the ballparks the player hit in and for his league's run environment that season. whoaBUH itself is park-neutral — every park adjustment lives in UURC+, so the two can be checked against each other instead of one quietly containing the other.
They are built on a public method — linear weights, described by Tom Tango, Mitchel Lichtman and Andrew Dolphin in The Book: Playing the Percentages in Baseball (2007), and by Pete Palmer before them. They are not that method's published numbers, and they are not FanGraphs' wOBA and wRC+.
The weights are derived here, from 930,651 Retrosheet play-by-play events across 2021–2025 — every regular-season plate appearance in those five seasons. For each season we build a run-expectancy table (how many runs a team scores, on average, from each combination of runners on base and outs), then measure what each outcome is worth by how it moves that expectation. No weight on this site is taken from any published table. Each season gets its own weights, computed from that season's own plays and never pooled with another's: run environments genuinely differ year to year, and averaging five of them together would produce a number that is correct for none of them.
The result is then put on the on-base-percentage scale, so it reads like a number you already know — around .320 is average, .400 is excellent. The multiplier that does this is computed per season from full 30-club league totals:
Our numbers will not match FanGraphs' wOBA or wRC+, and are not meant to. Different weights, different park factors, different league context, different seasons in scope. If you compare the two and find a gap, the gap is real and expected — neither number is a broken version of the other. Where you want FanGraphs' figures, use FanGraphs. We are also not claiming ours are more accurate: measured against the published set, the difference moves a typical hitter about two places in a ranking of 277. The reason to compute our own is that we can show you exactly where every number came from.
UURC+ — WHAT IT ASKS, AND WHAT wRC+ ASKS INSTEAD
UURC+ is indexed so that 100 is league average. 130 means 30% better than league average; 85 means 15% worse.
It is a ratio of on-base-scale rates. FanGraphs' wRC+ answers a runs question. The two correlate very highly and they are not the same quantity — they differ in kind, not in vintage. wRC+ converts a hitter's line into runs created and compares that to the league's runs per plate appearance. UURC+ never converts to runs at any point: it takes whoaBUH, divides out the parks the player hit in, and compares the result to the league's on-base average for that season. We do it that way because our scale is defined so that league whoaBUH equals league on-base average exactly, which is something we can show you rather than estimate. The cost of that choice is this paragraph: a number that looks like wRC+ and sits on the same 100-is-average scale is answering a different question, and you should not read a gap between them as either one being wrong.
HOW THE PARK ADJUSTMENT IS MEASURED, NOT ASSUMED
Most park adjustments assume a hitter took about half his plate appearances at home. We do not assume it — we count it, from the play-by-play, for every player in every season. Across the league the real figure is 48.94% rather than 50%, because the home team does not bat in the bottom of the ninth when it is already ahead. For individual players it ranges from 0% to 100%.
A traded player shows why that matters. Rafael Devers played 2025 for Boston and San Francisco. Fenway Park inflates run scoring; Oracle Park suppresses it. He took more plate appearances in the pitcher's park, so the two nearly cancel:
| PARK | HOME PA | RUNS FACTOR | EFFECT |
|---|---|---|---|
| Oracle Park | 208 | 0.9424 | suppresses |
| Fenway Park | 176 | 1.0961 | inflates |
| combined | 384 of 729 | 1.0068 | almost neutral |
His UURC+ is 116.11. Had we assigned him to one team and applied the conventional half-season assumption, the same batting line would have produced 111.53 as a Red Sox hitter or 120.36 as a Giant — a nine-point spread on a choice about paperwork rather than about anything he did. Counting where he actually batted removes that choice.
Road parks are treated as averaging out. That is an assumption this number carries, and it is the same one wRC+ makes. A hitter whose road schedule happens to be heavy on hitters' parks faced an easier environment than one whose road schedule is heavy on pitchers' parks, and UURC+ does not see the difference. We could measure road exposure too — the data is there — but our park factors are built by comparing a team's home rate to its own full road schedule, so applying them to a partial one would use them in a way they were never checked for. We would rather name the assumption than replace it with an unverified correction.
A KNOWN LIMITATION: ONE PARK FACTOR FOR EVERY KIND OF HIT
UURC+ applies a single runs factor per ballpark. Parks do not actually treat every outcome the same way, and we can show you exactly how wrong that is, because we compute a separate factor for each outcome and simply do not use them:
| PARK | RUNS | HOME RUNS | WHAT THE RUNS FACTOR GETS WRONG |
|---|---|---|---|
| Coors Field | 1.2690 | 1.0840 | overstates home runs by 17.1% |
| Fenway Park | 1.0961 | 0.9375 | inverts the sign — runs up, home runs down |
Coors Field is the most run-inflating park in baseball and only the tenth most home-run-inflating; Fenway raises scoring while suppressing home runs. A single runs factor cannot represent either fact.
The per-outcome factors exist, are published in our park-factor documentation, and are deliberately unused. Using them would mean adjusting each kind of hit before pricing it — and the prices, our linear weights, were themselves measured from real games in real parks, so they already carry league-average park effects. Correcting the counts while leaving the prices uncorrected is not a park adjustment; it is half of one. Doing it properly would require re-deriving the weights from park-neutral plays, which we have not done. Until we do, one runs factor applied to the finished number is the honest version, and this section is where we tell you what it costs.
WHERE UURC+ STOPS EXISTING
UURC+ needs everything whoaBUH needs, plus a ballpark we can measure. Of the 2,938 player-seasons that have a whoaBUH:
The 2021 Blue Jays have no UURC+ at all. COVID border rules split that season across three ballparks — Buffalo, Dunedin and Toronto — and no one of them hosted enough games to measure a reliable factor. Our Rogers Centre figure pools five seasons, of which 2021 contributes a small fraction; using it would describe a ballpark-season that did not exist. Those players show a dash.
A player who never batted at home has a UURC+ with no park adjustment applied — his measured home share is zero, so there is nothing to adjust for. That is a measurement, not a default, and it is recorded as its own case so it can never be confused with a park factor we were missing. All 151 are brief call-ups; the largest had 23 plate appearances.
A missing park factor is never filled in with 1.00. If we cannot measure the ballpark, the player-season gets a dash, not a number computed as though the park were neutral. The same rule as everywhere else on this site: a number that is not measured is not shown. UURC+ also stops where whoaBUH does — nothing outside 2021–2025, nothing in a season still being played.
One thing to know when reading a leaderboard: 100 is the league average per plate appearance, and almost any group you look at will average something other than 100. That is arithmetic, not an error, and it goes in both directions:
| 2025, PLAYERS WITH A UURC+ | N | MEAN | WHY |
|---|---|---|---|
| weighted by plate appearances | 673 | 99.7 | this is the one that should be ~100 |
| one vote per player | 673 | 88.6 | weak hitters bat rarely but count equally |
| 300+ plate appearances only | 277 | 102.1 | regulars are better than average by selection |
The plate-appearance-weighted figure is the one that tests whether the index is calibrated, and it lands at 99.7 for 2025. It drifts upward in older seasons — 102.1 in 2021 — for a reason worth knowing: our batting register holds currently-active players, so the further back you go the more it is missing the players who have since retired, and the ones it still holds were better than the ones it lost. We hold essentially the whole league from 2024 on and about three-quarters of it in 2021.
WHERE whoaBUH STOPS EXISTING
whoaBUH appears only where every input for it exists. Where one is missing you will see a dash, and the reason is recorded alongside it rather than worked out later — we do not fill a missing input in with an average, and we do not estimate around one. Of the 7,421 player-seasons currently in the batting register:
Seasons before 2021 and after 2025 have no whoaBUH at all. Retrosheet publishes play-by-play only for completed seasons, so we can measure weights for 2021 through 2025 and no further. A 2010 season shows a dash — not a zero, and not a value borrowed from a season we did measure.
The current season never has one until it ends. The park factors UURC+ needs are also computed from completed seasons, so UURC+ runs one season behind as a matter of arithmetic, not policy.
A player with no qualifying plate appearances has no whoaBUH. The metric is a rate, and its denominator — at-bats plus unintentional walks plus sacrifice flies plus hit-by-pitches — is zero for pitchers who never bat, September call-ups who did not come to the plate, and pinch-runners. There is nothing to divide by, so there is no number. This is the largest single group inside the seasons we cover.
For UURC+ specifically, some ballparks also drop out: we require 81 home games at a park configuration before publishing a factor for it, which excludes neutral-site games in London, Mexico City, Seoul and Tokyo, and the 2021 Toronto season, which was split across three ballparks with no reliable home baseline. A missing park factor is never filled in with a neutral 1.00 — the player-season simply has no UURC+.
Limits we know about, stated rather than left for you to find: park factors are not split by batter handedness, so an asymmetric ballpark gets one combined number; the regression constant was estimated on the runs factor and reused for the other six outcomes; and the unbalanced schedule is not corrected for, so the road baseline each factor is measured against is not a perfectly even sample of the other 29 parks. The size of that last residual is unknown. Separately, league-wide leaderboards are only meaningful from 2023 onward, because our batting register holds only currently-active players and covers 74% of 2021's league runs against 100% of 2025's.
Source data, and the terms it carries, are on the data credits page. Retrosheet requires the following statement, and it is reproduced here exactly as they publish it:
The information used here was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at "www.retrosheet.org".
WHAT THE NFL INDEX ACTUALLY IS, ON THE ROWS CURRENTLY SERVED
The served NFL redraft value is 100% trailing-production percentile. It has no market component and no projection component. Two projection models were built and evaluated; neither beat trailing production out of sample, so neither ships. The method version on the served rows is named fb-v1.0-backfill.2 — a name that describes a capability (market and projection components) the currently-served rows do not carry: those component columns are null on every served row. The superflex board pairs the 2qb ADP feed with the dynasty_sf index, the only superflex-anchored format served; its value is production-weighted .444 and age-weighted .556 on the served rows.
The 2025-12-30 snapshot date carries TWO method versions per player — fb-v1.0-backfill and fb-v1.0-backfill.2 — because a version is never overwritten, only superseded. A naive query against value_snapshots therefore returns two rows per player on that date. The board and every value shown on this site pick fb-v1.0-backfill.2, the higher-precedence version, via the same rule used everywhere on this site (highest precedence, then highest revision).
SNAPSHOT CURRENCY
Served values are from the 2025-12-30 snapshot and reflect the complete 2025 season. They do NOT reflect 2026 offseason movement — trades, signings, depth-chart changes, or draft capital.
ADP SOURCE
ADP is Fantasy Football Calculator's 2026 preseason aggregate, fetched per feed at teams=12:
Raw rows are stored verbatim and append-only; identity resolution to the player spine is tracked separately and never guessed — an ambiguous name is left unbound rather than bound to the likelier candidate. See data credits for attribution.
WHY SOME PLAYERS HAVE NO VALUE
The index is built from NFL production history. A player with none — 2026 rookies before their first snap, and positions the index does not model (kickers, team defenses) — has an ADP but no value. The board still renders them from the raw ADP columns, marked "ADP only, not valued", with value, rank, and gap shown as an em-dash. They are never silently dropped.
WHY THE INDEX STOPS RANKING PART-WAY DOWN THE BOARD
Production in fb-v1.0 is value over replacement. Replacement is the last starter at a position in a 12-team league — QB12, RB24, WR36, TE12 for the single-QB formats; QB24 for superflex, which starts two. A player below that line is freely available on waivers, so the value of acquiring them is zero, and the index records exactly that: zero. Negative production is clamped to zero rather than carried, deliberately.
The consequence is arithmetic, not a flaw. Exactly one fewer player than the replacement rank clears the line at each position — measured on the current snapshot: 23 QB, 23 RB, 35 WR, 11 TE. Every other valued player shares one identical production score. Roughly half of each draft-board feed sits there: 47.9% of the superflex pool and 52–58% of the single-QB pools.
So the board does not print a production rank for them. PROD RK, PROD POS, TIER and GAP all show an em-dash, and the row is marked "at replacement, not ranked" — the same treatment, for the same reason, as "ADP only, not valued" above. A column headed PROD RK must not contain a number that is not a production rank. Ranking them by their remaining value would be ranking them by age, which is a dynasty signal, not a production one.
Under ORDER: VALUE the board draws a visible line at that point and orders everything below it by last season's fantasy points in the selected scoring format, with players who have no season row sorting last. That ordering is a display convention so the rows are not arbitrary. It is not a Kalindro ranking, it is not derived from the index, and it changes no stored value. The index itself is unchanged: the same snapshot, the same numbers, the same frozen method version.
THE INTERSECTION RULE
Every rank on the draft board — cross-position and within-position, production and ADP — is computed over the same pool: players holding BOTH a value and an ADP row. Mixing pools would make ranks incomparable and gap meaningless. The board states the intersection size it is ranked over, and every rank that feeds gap is shown as its own column, so the arithmetic can be checked on screen, not taken on faith.
For the single-QB scoring family, that pool is a SINGLE fixed set — always the PPR feed's intersection — not recomputed against whichever of PPR/Half-PPR/Standard is on screen. This matters because FFC's standard and half-PPR pulls sample fewer mock drafts than PPR and so return smaller player pools (measured live, 2026/12-team: 150 and 159 valued players vs PPR's 180). Ranking each format over its own, differently-sized intersection would move rank numbers purely from pool size — no change in any player's value or ADP — which is the same defect the scoring selector's honesty requirement exists to prevent. Anchoring to one pool removes it: the board states the same pool size (180) no matter which format is selected.
A pool member with no ADP row anywhere in the selected format's own pull — 32 of 180 for Standard, 21 of 180 for Half-PPR, measured live (e.g. Drake London has no adp_rows row at all in the Standard pull) — is still shown, with value, PROD RK, and PROD POS populated from the fixed pool, and ADP, ADP RK, ADP POS, and GAP rendered as an em-dash. This is the mirror of "ADP only, not valued" above: that case has ADP but no value, this one has a value but no ADP in this specific format. Neither is silently dropped.
GAP IS WITHIN POSITION — NOT VALUE-OVER-REPLACEMENT, AND NOT CROSS-POSITION EITHER
Gap is PROD POS − ADP POS: each player's rank among players at their OWN position, within the intersection, by raw trailing-production value minus their rank among that same position by ADP. Negative means the index ranks the player earlier (higher, better) WITHIN THEIR POSITION than the market does; positive means lower (worse) within their position; zero means agreement. No color coding — green/red would itself be a buy/sell signal.
This retired an earlier definition that ranked both sides CROSS-POSITION (across all positions in the intersection at once), which itself had replaced an even earlier value-over-replacement (VOR) definition. VOR was rejected because fb-v1.0 values are an ordinal percentile, not a cardinal point on a shared scale, so subtracting a per-position replacement level measured pool composition (how many players exist at a position), not disagreement. Cross-position rank subtraction turned out to have the SAME defect from the other side: a raw production percentile has no notion of positional scarcity, but ADP prices it directly — few teams start two QBs or two TEs, so replacement sits close behind the starters and the market compresses those positions early. Comparing an unscarce percentile-rank to a scarcity-priced market-rank, cross-position, doesn't measure disagreement either — it measures how differently the two sides treat pool composition.
Evidence — ppr/redraft @ 12 teams, the full intersection (n=180), cross-position gap vs. within-position gap by position:
| POS | N | CROSS MEAN | CROSS MED | CROSS SD | CROSS SD/N | WITHIN MED | WITHIN SD | WITHIN SD/n |
|---|---|---|---|---|---|---|---|---|
| QB | 28 | −6.2 | 0.0 | 19.3 | 0.107 | 0.0 | 3.1 | 0.111 |
| RB | 57 | 4.9 | 3.0 | 24.6 | 0.137 | 1.0 | 6.8 | 0.119 |
| TE | 23 | −12.8 | 0.0 | 26.4 | 0.147 | 0.0 | 3.2 | 0.139 |
| WR | 72 | 2.6 | 3.0 | 24.0 | 0.133 | 1.5 | 10.1 | 0.140 |
Superflex (2qb/dynasty_sf @ 12 teams, n=163):
| POS | N | CROSS MEAN | CROSS MED | CROSS SD | CROSS SD/N | WITHIN MED | WITHIN SD | WITHIN SD/n |
|---|---|---|---|---|---|---|---|---|
| QB | 32 | 8.2 | −1.0 | 41.0 | 0.251 | −1.5 | 9.3 | 0.291 |
| RB | 49 | 27.3 | 22.0 | 42.8 | 0.263 | −2.0 | 14.0 | 0.286 |
| TE | 18 | −37.3 | −45.0 | 35.7 | 0.219 | −0.5 | 4.3 | 0.239 |
| WR | 64 | −14.5 | −14.0 | 42.1 | 0.258 | −0.5 | 18.6 | 0.291 |
CORRECTION: an earlier version of this page argued that within-position standard deviation "collapses 3–8×" versus cross-position, and presented that as the evidence for this fix. That comparison was invalid — standard deviation is scale-dependent, and cross-position gaps range over the full 163–180-player intersection while within-position gaps range over as few as 18 players at a single position, so a smaller raw SD is guaranteed by pool size alone, independent of any real change in disagreement. The CROSS SD/N and WITHIN SD/n columns above are that comparison done correctly: normalized by pool size, ppr's dispersion is essentially unchanged (0.107 vs 0.111 for QB, 0.147 vs 0.139 for TE), and 2qb's within-position dispersion is slightly HIGHER on every position. Standard deviation is not evidence for this fix, in either direction, and the columns are shown so that can be checked directly rather than taken on faith.
The real evidence is the MEAN, and it is evidence for a specific reason: the cross-position gap summed over the ENTIRE intersection is exactly zero (prodRank and adpRank are each a permutation of 1..n_pool, so their totals are identical). That fixed zero has to land somewhere, and it lands unevenly on positions — in ppr, weighted by count, 28×(−6.2) + 57×(4.9) + 23×(−12.8) + 72×(2.6) ≈ 0 (exactly zero before the means were rounded to one decimal for display). QB and TE are negative, RB and WR positive, and they cancel. That is a systematic, per-position REDISTRIBUTION, not noise — it is what happens when one side of the comparison (ADP) prices position-relative scarcity and the other (a cross-position production percentile) has no notion of position at all. Within-position ranking removes the redistribution by construction: both sides are now ranked over an identical, single-position pool, which is also why the within-position mean is forced to exactly 0.0 for every position (not merely small) — there is no cross-position imbalance left for it to carry.
The same decomposition shows up player by player. Patrick Mahomes' PPR/REDRAFT cross-position gap was −61; within QB alone it is −10. (Feed matters: his 2QB/DYNASTY_SF gap is a different +42 cross / +13 within — a different market, ADP 103rd overall in single-QB vs 13th in superflex. Both are correct; neither contradicts the other.) The ppr −51 difference was never disagreement about Mahomes specifically — it was ADP pricing QB scarcity that a production percentile cannot see. George Kittle: −82 cross, −7 within (ppr). Colston Loveland: +37 cross, +9 within (ppr). Superflex TE is the sharpest case: ADP crowds every rosterable TE into a narrow 65–124 overall-pick band (Trey McBride to Travis Kelce, the entire position), while cross-position production rank spreads them across the full 163-player pool — comparing the two was measuring the width of that ADP band, not disagreement about any player.
The cross-position PROD RK and ADP RK columns are still shown, as reference — they are real information, just not comparable to each other across positions, so they no longer feed gap.
ADP TIEBREAK
ADP is published to one decimal and exact ties are common. ADP order everywhere is: adp asc, times_drafted desc, stdev asc, ffc_player_id asc — the same rule the ingest job documents, so the ordering (and therefore the gap column) is reproducible between runs.
EOY RANK TIEBREAK
Season point totals land on one-decimal boundaries and roughly 600 skill-position players a season makes exact ties common, not rare — without a stated rule, e.g. WR34 vs. WR35 could flip between renders for no reason a reader could see. Order is: total points desc, games asc, player_id asc. Fewer games for the identical total is the better per-game season, so it sorts first; player_id is the final, arbitrary-but-stable tiebreaker, the same role ffc_player_id plays in the ADP order above.
This rule is computed INDEPENDENTLY within each scoring format — standard, half-PPR, and full PPR each have their own tie set, not a PPR ordering reused elsewhere. Verified directly (2023–2025, QB/RB/WR/TE, exact-tie groups per season/position): standard totals produced 127 tied groups (404 players) across all 12 season/position combinations; full PPR produced 95 groups (317 players); half-PPR produced 114 groups (354 players) — three different counts, confirming the tie sets genuinely differ by format rather than one format's ties simply carrying over. QB, the shallowest pool, had zero ties in two of three seasons; WR, the deepest, had as many as 28 tied groups in a single season.
SCORING SELECTOR & MIXED PROVENANCE
The draft board's PPR / Half-PPR / Standard selector switches two things: which FFC ADP pull is joined in, and which stored season total EOY RANK is computed from (half-PPR is derived at read time as the average of the standard and full-PPR totals, never stored — the same convention as lib/battingRates.ts and lib/pitchingRates.ts). It does NOT switch PROD RK or GAP. Those two columns are always computed from fb-v1.0, which is trained and scored on full-PPR production and frozen into value_snapshots at write time, append-only. There is no code path by which selecting Standard or Half-PPR changes a single value_bp, prodRank, prodRankPos, or gap number on screen — the selector's reach stops at ADP and EOY RANK.
This means that while a non-PPR format is selected, GAP (prodRankPos − adpRankPos) is comparing a PPR-based production rank against a non-PPR-based ADP rank — a mixed-provenance number, not an apples-to-apples one. The board states this on screen whenever scoring ≠ PPR, rather than leaving it implicit: production and market are being measured in two different scoring languages at that moment, and the reader should know it. This is a deliberate product choice, not an oversight left for a future fix — building a Standard- or Half-PPR-scored production index would mean re-deriving fb-v1.0's underlying player values from non-PPR statlines, which is a new value-method version (fb-v1.1 or later), not a parameter on the existing one. Until that method version exists, PPR is the only scoring fb-v1.0 can honestly claim to measure, and the board says so rather than implying otherwise.
GAP'S DENOMINATOR MISMATCH — A SECOND, SMALLER RESIDUAL
PROD POS is ranked over the fixed 180-player PPR pool (see THE INTERSECTION RULE) — invariant, by design, regardless of the scoring selector. ADP POS is ranked over however many of those 180 actually carry an ADP row in the SELECTED format, which is a strictly smaller number for Standard and Half-PPR (FFC samples fewer mock drafts in those formats). Measured live: PPR 180 (QB 28, RB 57, TE 23, WR 72, no gap since PPR is the pool itself); Standard 148 of 180 (QB 23/28, RB 48/57, TE 16/23, WR 61/72); Half-PPR 159 of 180 (QB 24/28, RB 50/57, TE 19/23, WR 66/72). A player missing a format's ADP row entirely already renders ADP POS and GAP as an em-dash — that part is not in question. The residual is smaller but real: for the players who DO have both ranks, GAP is comparing a rank drawn from a 180-wide (72-wide, for WR) field against one drawn from a 148-wide (61-wide) field, and that scale difference shows up as bias even with zero true disagreement.
The size of that bias is position-dependent and concentrated below the top of each position, not evenly spread. Top-of-position players barely move: CeeDee Lamb's WR gap is +4 in PPR and +5 in Standard, Christian McCaffrey's RB gap is −2 and −3, Josh Allen's QB gap is 0 in both. Mid-pack WRs move more: Jordan Addison's gap goes from +3 (PPR) to +11 (Standard), Calvin Ridley from +1 to +9, Xavier Worthy from +2 to +9 — a several-point shift, driven entirely by ADP POS's smaller field, since PROD POS for each of these players is identical in both formats.
We considered and rejected computing GAP on percentiles (prodRankPos ÷ pool size minus adpRankPos ÷ format-specific count) instead of raw ranks, which would cancel this bias algebraically. Rejected for two reasons. First, it does not fully solve the problem it appears to solve: a percentile is still computed over two different N's, so a player missing this format's ADP row still cannot be given one — the em-dash cases are unchanged either way. Second, and decisively, it breaks the one property this board's design treats as non-negotiable: every number on screen checks by eye. PROD POS − ADP POS currently equals the displayed GAP exactly ("WR10" minus "WR6" is "+4", verifiable without trusting the page) — see THE INTERSECTION RULE. A percentile-based GAP would no longer subtract from the two rank columns already on screen; it would be a third, independently-computed number a reader has to take on faith. We judge a knowable, bounded, now-quantified rank-scale bias to be cheaper than that.
So the board keeps raw-rank GAP, computed exactly as described above, with the residual stated here rather than silently absorbed: for a non-PPR format, GAP for players who clear both ranks is measurably biased toward appearing more positive (production ranked ahead of market) by an amount that grows the further a player sits below the top of their position, because ADP POS's field is smaller than PROD POS's by the counts above. This is a rank-scale artifact, not a market signal, and is independent of the mixed-provenance (PPR-vs-selected-scoring) effect described above — the two compound in the same direction for a non-PPR format but are separate mechanisms.
ROUND.PICK DERIVATION
Round.pick is derived from adp uniformly for every league size: n = round(adp), round = (n−1)÷teams + 1, pick = (n−1) mod teams + 1. FFC's own formatted string is stored as provenance but never displayed — FFC publishes it only for the league sizes we fetched, which would make 12-team exact and every other size an approximation with nothing marking the difference. This derivation is imprecise by one pick for a player whose adp ends in exactly .5: FFC rounds those on internal precision the public feed does not carry.
TIERS
Tiers are mechanical and apply only to valued players. Within a position, players are sorted by value descending; a new tier begins where the drop from the previous player exceeds 1.0 displayed point (100 internal). The served index is a percentile, so values bunch tightly and tier counts are small — that is the honest structure of the data, not a display choice.
TRAILING PRODUCTION RANK (COLUMN "PROD RK") — RENAMED FROM "KALINDRO RANK"
This column orders the intersection pool by raw value_bp, descending, across every position at once — the same kind of rank ADP already is. Ties break by higher raw value, then earlier ADP order, then ffc_player_id. There is no replacement level, no value-over-replacement, and no per-position adjustment anywhere in this computation; see the gap section above for why.
It was called "Kalindro rank," which reads as a claim about how good a player is. It measures one thing: recent PPR production, percentiled. The underlying value's window is the player's trailing UP TO 17 games actually PLAYED — not a fixed calendar range. run_nfl_backfill.py's build_windows (lines 82–127) keeps a per-player deque(maxlen=17) that advances only when a real stat row exists for that player that week (lines 105–111), and — per its own comment at line 84 — "rolls across season boundaries" for anyone short of 17 games in the current season. The window is NOT bounded to a single season by construction, even though for a full-season starter in a season-ending snapshot it happens to land entirely within that season. Because the mechanism genuinely spans seasons for short-sampled players (an injury return, a rookie's first month), a year-specific label ("2025 production rank") would be right for most rows and silently wrong for others. "Trailing production rank" is accurate for all of them.
Concretely, for the 2025-12-30 snapshot: that date is week_monday(2025, 18) — week 18 is the last week of the 2025 regular season, and week_monday is a SYNTHETIC label ("the Tuesday after the week's Sunday," run_nfl_backfill.py:65–73, whose own comment says "the snapshot identity is (season, week), not this date") — not a claim about any real calendar date. Nothing in this pipeline stores a per-game date; the stat grain is (gsis, season, week, season_type) (pipeline/nfl_staging.py:9), and no schedule/game-date table exists in the raw nflverse archive this repo has ingested. For Justin Jefferson specifically, the window at that snapshot holds all 17 of his possible 2025 regular-season weeks except one (his week-6 bye) — verified directly against the staged weekly parquet. His low rank is not a small-sample artifact: he played nearly the entire season and still produced little in several of those weeks (weeks 10–15 ranged 2.4–11.1 PPR points). A low PROD RK can mean either "played little" or "played plenty and produced little" — the column does not distinguish the two, which is one more reason it is not a quality judgment.
TRUE END-OF-SEASON POSITIONAL RANK — NOT SHOWN, AND WHY
PROD RK is a cross-position rank of a percentile, not a positional finish, so it cannot be checked against a public leaderboard (there is no public leaderboard of "percentile rank across all positions"). The honest complement would be each player's actual 2025 positional finish by total PPR points — WR12, RB5, and so on — independent of the 17-game window, sourced from the same fantasy_points_ppr field the production component consumes. That field lives in data/staging/nfl_weekly_stats.parquet — a local file the Python pipeline reads directly, never loaded into any Postgres table. This site's read path is a single read-only Postgres connection; no table in it (checked against every table in the public schema) carries a raw or season-summed NFL fantasy-points figure — value_snapshots.production_comp is already the same percentile PROD RK derives from, not an independent total. Shipping this column would mean a new ingest job and a new table, not a query against what already exists — recomputing something, which was out of scope this session. This is a known gap, not an oversight.
PER-GAME SAMPLE SIZE — NOT SHOWN, AND WHY
A rookie with no NFL snaps renders "ADP only, not valued" rather than a value the index has no basis for. A player who missed most of the season on the same underlying production-history model does not currently get the equivalent treatment — the board has no per-player games-in-window figure to threshold on. value_snapshots.confidence is a weighted blend of a sample term (games played, capped and further reduced below a low- sample threshold), a fixed agreement term, and a fixed freshness term — not games played itself, and not invertible back to a game count without the exact weights used to compute it. No table reachable from the site's read-only connection carries a raw per-game count for NFL players; the one staging table shaped to hold one (internal.stg_stat_lines) is unpopulated. Surfacing a thin-sample marker here would require recomputing part of the index from its inputs, which this site's read path does not do. This is a known gap, not an oversight.
DRAFT-DAY STATE — NOT PERSISTED
The board lets you click players off as they're drafted (by anyone) and mark which picks are your own, so it stays useful while drafting by hand on another site. This is plain browser state: nothing is written to a cookie, localStorage, or the database. Reloading the page clears every pick and every roster-slot edit. Marked picks are shared between the PPR and superflex tabs, because FFC's player id is stable across feeds — it is one real draft regardless of which value lens is on screen.
NEED RANK is the second rank column, always shown beside PROD RK, never combined with it into one number. Rule: a player whose position still has an open starting slot — its own slot, FLEX, or SUPERFLEX — sorts entirely ahead of every player whose eligible starting slots are already full; within each of those two groups, ties break by production rank. Bench slots do not create need. Need rank is defined only for valued players in the intersection pool, same as production rank. It never changes the table's row order (that stays ADP/TIEBREAK_SQL order, as always) — it is a number to read, not a re-sort.
The my-roster panel fills slots with a stated greedy rule: each of your own picks, in the order you drafted them, is assigned to the most specific still-open eligible slot — its own dedicated slot first, then FLEX, then SUPERFLEX, then bench. A shared FLEX or SUPERFLEX slot counts as an open slot for every position it accepts at once; that is a true fact about the slot, not double-booking.