New to these reports? Start here
- Dotted-underlined words have a plain-English definition — hover or tap them. Every term is also on the glossary page.
- "Null" means we found nothing, not that something broke. Most reports here are negative results, on purpose — knowing an idea doesn't work is the point.
- Two questions get asked separately. First, is the effect real? Second, is it already priced into the betting odds? An effect can be completely real and still useless to bet on.
- A "calibration" row is a self-check. It runs the same method on something already known to be true. If that fails, the whole report is unreliable — so it's reported alongside the findings.
- If a confidence interval includes zero, the real effect might be nothing at all, so no claim gets made.
Did the Weather + FTN Features Actually Earn Their Keep?
Measurement of commit 7ba65e1, which wired real Open-Meteo weather and
trailing FTN charting features into the fantasy projection engine and the
betting feature builder.
Verdict: modest real gain on fantasy (concentrated exactly where weather should bite, with a clear biasbiasWhether the misses lean consistently one way. A projection can have a good average error size but still be biased if it is almost always too high. Bias is often the more fixable problem. correction). No confirmed betting edge.
TL;DR
| Side | Measure | Result | Verdict |
|---|---|---|---|
| Fantasy | passing-yards MAEmean absolute errorAverage size of the miss, ignoring direction. If a projection is off by 3 one week and -5 the next, the MAE is 4. Lower is better. | 62.73 → 62.04 (−1.10%) | ✅ real, modest |
| Fantasy | passing-yards bias | +6.98 → +4.06 (−42%) | ✅ the clearer win |
| Fantasy | all 12 stat categories | 5 better, 6 worse (worse ones ≤ +0.23%) | mixed, net positive |
| Betting | wind → closing total | survives holdoutholdoutData deliberately set aside and never looked at while developing an idea, then used once at the end as a fair test. Peeking at it first would defeat the purpose., but tail fails | ❌ not exploitable |
| Betting | wind ≥15mph unders (holdout) | 50.0% hit rate (need 52.4%) | ❌ no edge |
| Betting | precipitation / temp → market | nullnull resultA test that found nothing. "Null" is the starting assumption that there is no real effect; a "null result" means the data gave us no reason to abandon that assumption. It does not mean the data was missing or the test failed to run. | ❌ priced |
| Betting | FTN → market errormarket errorHow far the actual result landed from what the betting market predicted. If this is unrelated to some factor, the market already accounted for that factor. | nothing clears the floor | ❌ priced |
Fantasy side — measured on the real walk-forward harness
scripts/evaluate_fantasy_mae.py, season 2025, weeks 5–18, strict
walk-forwardwalk-forwardEvaluating week by week using only what was knowable before each week, mimicking how the model would actually have been used at the time., A/B on the weather_adjustments config flag. 3,328 projected
player-weeks (2,654 with a stat line).
Unlike the injury multipliers, weather is not gated behind
_is_future_week — the archive has real observations for completed games — so
the standard harness measures it directly.
MAE by category
| Stat | n | weather OFF | weather ON | Δ | % |
|---|---|---|---|---|---|
| passing_yards | 313 | 62.7271 | 62.0363 | −0.6908 | −1.10% |
| passing_tds | 313 | 0.9755 | 0.9709 | −0.0046 | −0.47% |
| receiving_tds | 2341 | 0.2795 | 0.2783 | −0.0012 | −0.43% |
| receiving_yards | 2341 | 18.2230 | 18.1512 | −0.0718 | −0.39% |
| rushing_yards | 1034 | 20.3959 | 20.3926 | −0.0033 | −0.02% |
| receptions | 2341 | 1.3915 | 1.3919 | +0.0004 | +0.03% |
| interceptions | 313 | 0.6351 | 0.6353 | +0.0002 | +0.03% |
| targets | 2341 | 1.7666 | 1.7675 | +0.0009 | +0.05% |
| passing_attempts | 313 | 7.3105 | 7.3189 | +0.0084 | +0.11% |
| rushing_tds | 1034 | 0.3905 | 0.3910 | +0.0005 | +0.13% |
| rushing_attempts | 1034 | 3.3524 | 3.3602 | +0.0078 | +0.23% |
| fumbles_lost | 2654 | 0.0448 | 0.0448 | 0.0000 | tie |
The pattern is the point, not the headline number. Every improvement is in an efficiency/production stat where weather physically acts (passing yards, passing TDs, receiving yards, receiving TDs). Every regression is in a volume stat (attempts, targets, receptions) and is ≤ +0.23%. That is what a correctly-located signal looks like; a spurious one would scatter.
Bias — the clearer result
| Stat | bias OFF | bias ON | |
|---|---|---|---|
| passing_yards | +6.9751 | +4.0618 | −42% |
| passing_tds | +0.0405 | +0.0201 | −50% |
| rushing_yards | −1.0934 | −0.9274 | −15% |
| receiving_yards | +0.1874 | −0.0837 | −55% |
All four major categories improved. The engine was systematically over-projecting passing, and real weather removes a meaningful share of it. For a projection used to set lineups, removing a persistent 7-yard-per-QB-game over-projection matters more than a 1% MAE move.
Betting side — the two-stage frame
Stage AStage AThe first question: is the effect real at all? Measured against what actually happened, ignoring betting markets.: is there a real effect on the outcome? Stage BStage BThe second question: is the effect already reflected in the betting odds? An effect can be completely real and still worthless to bet, because the price already includes it.: is it already in the
closing lineclosing lineThe final odds right before a game starts. It reflects everything the betting market knows, which makes it the hardest benchmark to beat. (formal null E[market_error | feature] = 0)? Books have had
forecasts and charting for years, so "priced" was the prior.
⚠ A unit error in the first pass nearly produced a false positive, and is
worth recording. Slopes were initially reported per 1.0 unit of feature and
compared against a points-denominated tradeable floortradeable floorThe minimum edge needed to overcome the bookmaker's cut. Anything smaller is real but unprofitable.. But wx_pass_mult spans
only ~0.85–1.05, so "per 1.0 unit" is ~5× its entire observed range — which
made several features look wildly "TRADEABLE". Re-expressed per +1 SD of the
feature, most of them collapsed. Same class of error as the earlier
starter-misidentification artifact: the statistics were fine, the units were
not.
Weather vs the market (per +1 SD, develop 2015–2021)
| Feature | 1 SD | Effect on total error | 95% CI95% confidence intervalThe range the true value is plausibly in. If this range includes zero, we cannot rule out that the real effect is nothing at all. | vs 0.79 floor |
|---|---|---|---|---|
| wind | 4.23 mph | −1.480 | [−2.341, −0.849] | above |
| wx pass multiplier | 0.060 | +1.537 | [+0.856, +2.108] | above (collinear with wind) |
| precipitation | 0.065 in | −0.707 | [−1.606, +0.170] | null |
| temperature | 16.5 °F | +0.317 | [−0.211, +0.886] | null |
Spread error: wind and precipitation both null.
The pre-registered holdout test — and why it still fails
Wind was the one survivor, so it got exactly one holdout test:
| Split | Effect per +1 SD | 95% CI |
|---|---|---|
| Develop 2015–2021 | −1.480 | [−2.341, −0.849] |
| Holdout 2022–2025 | −1.109 | [−2.292, −0.351] |
The continuous slope does survive. But three things kill it as an edge:
1. The actual bet doesn't clear the vigvigThe bookmaker's built-in cut. It is why a coin-flip bet at -110 needs you to win about 52.4% of the time just to break even.. In the holdout, games at wind ≥15 mph: n=44, mean total error −0.35, under hit rate 50.0% against the 52.4% needed. Betting it would break even at best.
2. The linear fit is not describing the tail. Mean total error by wind bin:
| Wind (mph) | All | Develop | Holdout | n (all) |
|---|---|---|---|---|
| 0–4 | +1.35 | +1.30 | +1.44 | 353 |
| 4–8 | +0.79 | +0.57 | +1.18 | 832 |
| 8–12 | −0.54 | −0.87 | +0.11 | 561 |
| 12–16 | −2.34 | −1.30 | −4.18 | 225 |
| 16–20 | −0.85 | −1.92 | +1.20 | 81 |
| 20+ | −8.77 | −11.88 | −0.50 | 22 |
The 20+ bin rests on 22 games total, 16 of them in develop, and its −11.88 develop mean is a huge leverage point on the linear slope. The holdout tail reverses (16–20 is +1.20). The slope predicts ~−2.2 points at 15 mph; the observed windy-game mean is −0.35. The estimate and the reality disagree by 6×.
3. It explains almost nothing. corr(wind, total error) = −0.104, so wind accounts for 1.08% of the variance in market error. Overall mean total error is +0.022 — the market is essentially unbiased in aggregate.
The one genuinely interesting pattern is a gradient rather than a level bias: calm games trend over (+1.35 at 0–4 mph) and mid-windy games trend under. That is directionally consistent across both splits and is worth watching forward, but it is not a bet at these magnitudes.
FTN vs the market (per +1 SD, develop 2022–2023, n=447)
| Feature | Effect | 95% CI | Verdict |
|---|---|---|---|
| sum catchable rate → total error | +0.190 | [−0.671, +1.131] | null |
| sum drop rate → total error | −0.814 | [−1.520, +0.187] | null |
| sum pressure proxy → total error | +0.814 | [+0.134, +1.452] | marginal — CI floor 0.134, far below 0.79 |
| diff catchable → spread error | −0.269 | [−0.347, −0.195] | real but below floor |
| diff blitz → spread error | −0.294 | [−0.443, −0.149] | real but below floor |
FTN features move the outcome decisively (Stage A: sum-catchable +20.7 pts on actual totals, sum-drop −34.1) — they clearly measure something real about offensive quality. But essentially none of it survives against the closing line. The two spread effects are statistically solid and economically too small; the pressure effect's confidence intervalconfidence intervalA range of values the true answer is plausibly in. If it includes zero, the effect might genuinely be zero, so we cannot claim it is real. reaches nearly to zero.
The holdout for FTN was not opened, since nothing cleared the floor in-samplein-sampleMeasured on the same data used to build or tune the idea. Nearly always looks better than reality..
Caveats
- FTN covers only 2022–2025, so its develop split is 447 games. UnderpoweredunderpoweredNot enough data to detect an effect even if it is really there. An underpowered null means "we could not tell", which is very different from "there is nothing there". for anything but a large effect.
- The fantasy A/B is one season (2025). The direction is consistent and the improvements are correctly located, but a single season of walk-forward is not a strong test of a ~1% MAE move.
- Weather Stage B tests the closing line only; nothing here can detect a stale-opening-line or line-movement edge, since nflverse carries no openers.
- Wind and
wx_pass_multare collinear by construction (the multiplier is a function of wind), so they are one finding, not two.
Bottom line
Keep the weather features on the fantasy side. They cut a systematic passing over-projection by ~42% and improve exactly the categories where weather physically acts, at negligible cost elsewhere.
Do not build a weather or FTN betting model expecting edge. The market prices both. Wind shows a real, holdout-surviving gradient against the closing total, but it explains 1% of variance, its magnitude is inflated by ~22 extreme games, and the tradeable version of it hits exactly 50% out of sample. The features remain worth having in the betting feature set for calibrationcalibrationA deliberate sanity check on the method itself: run it on something already known to be true. If it fails to detect the known thing, the method is broken and its other results mean nothing., not for edge — and any future claim of edge from them needs a forward test, not another backtest.
Reproduce:
python3 scripts/evaluate_fantasy_mae.py --season 2025 --weeks 5-18 \
--variant current --set weather_adjustments=0 --tag wx_off_2025
python3 scripts/evaluate_fantasy_mae.py --season 2025 --weeks 5-18 \
--variant current --set weather_adjustments=1 --tag wx_on_2025
python3 backtests/weather_ftn_value/measure_betting_value.py