Every betting price has a cut built in. The Bet Better margin index measures that cut every day across 12 sports, from the prices quoted by around 30 bookmakers, and publishes it as a CSV with a codebook under CC BY 4.0. This notebook loads the file and reproduces the headline numbers.
idx <- read.csv("betbetter-daily-margin-index.csv", stringsAsFactors = FALSE)
nrow(idx); range(idx$as_of_date)
## [1] 5395
## [1] "2026-07-01" "2026-09-26"
head(idx[, c("as_of_date", "sport", "market_class", "shape", "markets_n", "mean_pct")])
## as_of_date sport market_class shape markets_n mean_pct
## 1 2026-07-01 ALL h2h 2way 3632 5.054
## 2 2026-07-01 ALL h2h 3way 1763 6.882
## 3 2026-07-01 ALL spreads 2way 2100 4.923
## 4 2026-07-01 ALL totals 2way 2561 5.445
## 5 2026-07-01 americanfootball_nfl h2h 2way 852 4.599
## 6 2026-07-01 americanfootball_nfl spreads 2way 948 4.988
Each row is one day, one sport and one market type:
markets_n markets measured across books_n
bookmakers, with the mean, median, minimum and maximum margin in
percent.
by_type <- aggregate(mean_pct ~ market_class + shape, data = idx, FUN = mean)
by_type <- by_type[order(-by_type$mean_pct), ]
by_type$mean_pct <- round(by_type$mean_pct, 2)
knitr::kable(by_type, col.names = c("Market type", "Shape", "Average bookmaker margin (%)"), row.names = FALSE)
| Market type | Shape | Average bookmaker margin (%) |
|---|---|---|
| h2h | 3way | 7.50 |
| player_props | 2way | 7.45 |
| other | 2way | 6.65 |
| totals | 2way | 6.15 |
| spreads | 2way | 5.35 |
| h2h | 2way | 5.11 |
Three-way match results and player props carry the biggest cut; plain two-way match results are the cheapest market.
by_sport <- aggregate(mean_pct ~ sport, data = idx[idx$market_class == "h2h", ], FUN = mean)
by_sport <- by_sport[order(by_sport$mean_pct), ]
op <- par(mar = c(4, 7, 2, 1))
barplot(by_sport$mean_pct, names.arg = by_sport$sport, horiz = TRUE, las = 1,
col = "#0b5fa5", border = NA, xlab = "Average head-to-head margin (%)")
par(op)
daily <- aggregate(mean_pct ~ as_of_date, data = idx, FUN = mean)
daily$date <- as.Date(daily$as_of_date)
plot(daily$date, daily$mean_pct, type = "l", lwd = 2, col = "#0b5fa5",
xlab = "", ylab = "Average margin, all markets (%)")
With the cut known, any quoted decimal price can be turned into a
fair probability. For a two-way market with prices a and
b:
fair <- function(a, b) { pa <- 1 / a; pb <- 1 / b; c(fair_a = pa / (pa + pb), fair_b = pb / (pa + pb), cut_pct = 100 * (pa + pb - 1)) }
fair(1.91, 1.91)
## fair_a fair_b cut_pct
## 0.500000 0.500000 4.712042
fair(1.50, 2.75)
## fair_a fair_b cut_pct
## 0.6470588 0.3529412 3.0303030
betbetter R package.Data CC BY 4.0, free to reuse with a credit to Bet Better. Model estimates, not betting advice. 18+.