Disclosure first: I built the scrapers that produced this data (OddsFrance on Apify). The CSV used below is free and needs no account.
French licensed sportsbooks are oddly absent from the usual odds APIs. I wanted a simple answer to a simple question: how much margin does each French book take on a football 1X2 market? Here is how to get it from one daily snapshot.
The data
One row per outcome: bookmaker, sport, competition, teams, start time (UTC), market (1x2 or match_winner), outcome, decimal odds.
import pandas as pd
url = ("https://huggingface.co/datasets/oddsfrance/french-bookmakers-odds/"
"resolve/main/data/french_bookmakers_odds_2026-11-13.csv")
df = pd.read_csv(url)
df = df[df["oddsDecimal"] > 1]
print(len(df), df["bookmaker"].value_counts().to_dict())
The 13 November snapshot has 1,722 rows. Winamax lists far more matches than the other two, which matters below.
Margin (overround) per bookmaker
For a 1X2 market the margin is 1/home + 1/draw + 1/away - 1. Keep only events where the three outcomes are present:
x12 = df[(df["sport"] == "football") & (df["marketType"] == "1x2")]
full = x12.groupby(["bookmaker", "eventName"]).filter(
lambda g: set(g["outcome"]) == {"home", "draw", "away"})
margin = full.groupby(["bookmaker", "eventName"])["oddsDecimal"].apply(lambda o: (1 / o).sum() - 1)
print((margin.groupby("bookmaker").median() * 100).round(1))
| Bookmaker | Median 1X2 margin | Events |
|---|---|---|
| PokerStars Sports | 9.7 % | 23 |
| NetBet | 12.1 % | 35 |
| Winamax | 16.5 % | 516 |
Read it with care. Winamax prices hundreds of small leagues, where every book charges more, so its median is pulled up. The fair comparison is on the same matches, which is the next step.
Same outcome, several books
Match outcomes across books on home team, away team and start time:
key = ["homeTeam", "awayTeam", "startTime", "marketType", "outcome"]
multi = df.groupby(key).filter(lambda g: g["bookmaker"].nunique() >= 2)
best = (multi.sort_values("oddsDecimal", ascending=False)
.groupby(key).agg(best=("oddsDecimal", "first"), worst=("oddsDecimal", "last"),
book=("bookmaker", "first")))
best["gap_%"] = ((best["best"] / best["worst"] - 1) * 100).round(1)
print(len(best), best["gap_%"].median())
print(best.sort_values("gap_%", ascending=False).head(5))
96 outcomes were priced by at least two books. The median gap between best and worst price was 3.6 %, and on a few long shots it went above 20 %. Team names are written the French way by all three books (« Écosse », « Pays de Galles »), which is why a naive join works here; across other books you need a mapping table.
What this is not
One snapshot during an international break is not a study. Margins move with the competition, the time to kick-off and the market. Stack daily files (a new one lands every morning) to see that. And none of this is betting advice: 18+, and check the bookmaker's own page before any decision.
Links
- Free dataset, updated daily: Hugging Face and Kaggle
- Fresh odds, other sports or live: the Apify actor Football Odds API France (mine, pay per result)
Drafted with AI assistance; the code was run and every number checked against the data before publishing.
If you know a cleaner way to match team names across books, I would like to read it.
Top comments (0)