Bet Better BET BETTER PRO

R at Bet Better

NFL home field from closing prices in R

2 November 2026. Base R.

A closing moneyline with the margin removed is a probability, and a season of them adds up to a forecast you can check. This post takes the 97 NFL games from weeks 1 to 7 of 2026, sums what the market priced for home teams and compares it with what they did. Everything runs in base R on one open CSV. The write-up of the same numbers is the NFL home field study.

1. Load the file

One row per game. home_no_vig_close_prob is the home team's chance from the closing moneylines, with each book's margin removed and then averaged across books. home_won is 1 or 0.

u <- paste0("https://raw.githubusercontent.com/edushinka/sports-closing-price-studies/",
            "main/data/nfl_2026_closing_prices_vs_results.csv")
nfl <- read.csv(u)
nrow(nfl)
range(nfl$date_utc)
table(nfl$week)
[1] 97
[1] "2026-09-10" "2026-10-27"

 1  2  3  4  5  6  7 
 8 16 16 16 13 14 14 

2. Priced against actual

The mean of the probabilities is the home win rate the market expected. Their sum is the expected number of home wins.

p <- nfl$home_no_vig_close_prob
y <- nfl$home_won
round(c(priced = mean(p), won = mean(y), gap = mean(y) - mean(p)), 3)
c(expected_wins = round(sum(p), 1), actual_wins = sum(y))
priced    won    gap 
 0.546  0.474 -0.072 
expected_wins   actual_wins 
           53            46 

Home teams were priced at 54.6 per cent and won 47.4 per cent: 46 wins against 53 expected.

3. Is seven points a lot in 97 games?

Resample the games with replacement and recompute the gap. The spread of those gaps shows how far a sample this size can wander.

set.seed(2026)
boot <- replicate(4000, {
  i <- sample.int(length(y), replace = TRUE)
  mean(y[i]) - mean(p[i])
})
round(quantile(boot, c(0.025, 0.975)), 3)
  2.5%  97.5% 
-0.169  0.022 

The 95 per cent range runs from 16.9 points below the price to 2.2 points above it. Zero is inside, so these 97 games do not show that the market misjudged home field.

4. The same question as a regression

A logistic regression with the logit of the price as an offset has one free number, the intercept. If the prices were exactly right, the intercept would be zero. A negative intercept means home teams did worse than priced after allowing for how strong each one was.

fit <- glm(home_won ~ 1 + offset(qlogis(home_no_vig_close_prob)),
           family = binomial, data = nfl)
round(summary(fit)$coefficients, 3)
round(confint(fit), 3)
            Estimate Std. Error z value Pr(>|z|)
(Intercept)   -0.323      0.216  -1.499    0.134
 2.5 % 97.5 % 
-0.748  0.099 

The shift is -0.32 on the log-odds scale with a standard error of 0.22, and its interval also covers zero. Two methods, one answer: a large gap so far, and not enough games to call it.

5. Week by week

aggregate() with cbind() gives both means per week.

wk <- aggregate(cbind(priced = home_no_vig_close_prob, won = home_won) ~ week,
                data = nfl, FUN = mean)
wk$games <- as.vector(table(nfl$week))
wk$gap <- wk$won - wk$priced
print(transform(wk, priced = round(priced, 3), won = round(won, 3), gap = round(gap, 3)),
      row.names = FALSE)

nfl$month <- substr(nfl$date_utc, 1, 7)
by_month <- aggregate(cbind(priced = home_no_vig_close_prob, won = home_won) ~ month,
                      data = nfl, FUN = mean)
by_month$games <- as.vector(table(nfl$month))
print(transform(by_month, priced = round(priced, 3), won = round(won, 3)), row.names = FALSE)
 week priced   won games    gap
    1  0.550 0.500     8 -0.050
    2  0.613 0.438    16 -0.176
    3  0.510 0.688    16  0.177
    4  0.543 0.500    16 -0.043
    5  0.514 0.385    13 -0.129
    6  0.563 0.429    14 -0.135
    7  0.524 0.357    14 -0.167
   month priced   won games
 2026-09  0.559 0.550    40
 2026-10  0.537 0.421    57

Week 3 is the only week above the price. Split by month and almost the whole gap sits in October: September home teams matched their price, 55.0 per cent against 55.9, and October home teams won 42.1 per cent against 53.7. Treat that split with care. It was chosen after looking at the data, and 57 games is a small sample.

6. Points as well as wins

Wins throw away the margin, so check the scores too. Regressing the home margin on the logit of the price gives a line whose intercept is the average margin in a game priced at 50 per cent.

nfl$margin <- nfl$home_score - nfl$away_score
mean(nfl$margin)
lin <- lm(margin ~ qlogis(home_no_vig_close_prob), data = nfl)
round(summary(lin)$coefficients, 2)
[1] -0.9175258
                               Estimate Std. Error t value Pr(>|t|)
(Intercept)                       -2.16       1.28   -1.69     0.09
qlogis(home_no_vig_close_prob)     5.91       1.68    3.51     0.00

Home teams were outscored by 0.9 points a game on average. In a game priced even, the fitted home margin is -2.2 points with a standard error of 1.3, which tells the same story as the win rate. The slope shows that the price still sorts games well: about 5.9 points per unit of log-odds.

7. Home favourites and away favourites

Fold every game to the favourite, then compare the two kinds.

nfl$fav <- ifelse(p >= 0.5, "home favourite", "away favourite")
nfl$fav_p <- pmax(p, 1 - p)
nfl$fav_won <- ifelse(p >= 0.5, y, 1 - y)
f <- aggregate(cbind(priced = fav_p, won = fav_won) ~ fav, data = nfl, FUN = mean)
f$games <- as.vector(table(nfl$fav))
print(transform(f, priced = round(priced, 3), won = round(won, 3)), row.names = FALSE)
            fav priced   won games
 away favourite  0.611 0.674    43
 home favourite  0.671 0.593    54

Home favourites won less than priced and away favourites won more. That is the same home shortfall seen from another angle, so it does not count as a second finding.

8. Plot it

This draws the weekly lines from the table in section 5.

plot(wk$week, wk$won, type = "b", pch = 19, ylim = c(0.2, 0.8),
     xlab = "Week", ylab = "Share of home wins",
     main = "NFL 2026: home teams, won (solid) vs priced (dashed)")
lines(wk$week, wk$priced, type = "b", lty = 2)
abline(h = 0.5, col = "grey70")

Where the data comes from

The CSV and a one-file base R script, reproduce_study.R, are in the sports-closing-price-studies repository on GitHub, and the archived copy has the DOI 10.5281/zenodo.23774214. Data is CC BY 4.0. The file holds no ties, so home_won needs no special handling here. To get the same no-vig number from a pair of prices of your own, use the no-vig calculator. More closing-price files for other leagues are on the studies page, and the rest of the R series is on R at Bet Better.

18+. Analysis, not advice.

Get tomorrow's free pick by email

Free. One email a day, tops. Unsubscribe in one tap.

18+ only. Please gamble responsibly.