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.

Load the data

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.

The average cut by market type

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.

The cut by sport

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)

How the cut moves over time

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 (%)")

Turn a quoted price into a fair probability

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

Where to go next

Data CC BY 4.0, free to reuse with a credit to Bet Better. Model estimates, not betting advice. 18+.