Remove the bookmaker margin in Python
Bookmaker odds are not probabilities: they have a margin built in. This guide shows how to turn decimal odds into fair probabilities in a few lines of Python, then average across bookmakers. No libraries needed beyond the standard one.
Reviewed 2026-10-14
1. Odds include a margin
Decimal odds of 2.00 imply a 50% chance (1 divided by the odds). If you add up the implied probabilities of every outcome in a market, bookmakers make the total come to more than 100%. The extra is their margin, also called the overround or vig.
odds = {"home": 2.10, "draw": 3.40, "away": 3.60} # example decimal odds
implied = {k: 1 / v for k, v in odds.items()}
overround = sum(implied.values())
print({k: round(v, 4) for k, v in implied.items()})
print("total:", round(overround, 4)) # above 1.0
print("margin:", round((overround - 1) * 100, 2), "%")2. Proportional method: the simple baseline
Divide each implied probability by the total. The results add to exactly 100%. It assumes the margin is spread evenly across outcomes, which is not exactly true, but it is the usual starting point and easy to explain.
def devig_proportional(odds):
implied = {k: 1 / v for k, v in odds.items()}
total = sum(implied.values())
return {k: v / total for k, v in implied.items()}
print(devig_proportional(odds)) # adds up to 1.03. Power method: for uneven pricing
Bookmakers often shade longshots more than favourites. The power method raises each implied probability to a power k and chooses k so the total is 1, which takes proportionally more margin off longshots. There are other methods too, such as Shin's, but proportional and power cover most needs.
def devig_power(odds):
# find k so that sum((1/odds)**k) == 1
implied = {k: 1 / v for k, v in odds.items()}
lo, hi = 0.01, 20.0
for _ in range(80):
k = (lo + hi) / 2
if sum(p ** k for p in implied.values()) > 1:
lo = k
else:
hi = k
k = (lo + hi) / 2
return {name: p ** k for name, p in implied.items()}
print(devig_power(odds))4. Average across bookmakers
One bookmaker's price is noisy. Remove the margin from each bookmaker separately, then average the fair probabilities. Average probabilities, not odds, because averaged odds do not add up to 100%.
def consensus(books):
# books: list of {"home": .., "draw": .., "away": ..} decimal odds, one per bookmaker
cleaned = [devig_proportional(b) for b in books if all(v > 1 for v in b.values())]
n = len(cleaned)
return {k: sum(c[k] for c in cleaned) / n for k in ("home", "draw", "away")}
books = [
{"home": 2.10, "draw": 3.40, "away": 3.60},
{"home": 2.05, "draw": 3.50, "away": 3.70},
{"home": 2.12, "draw": 3.30, "away": 3.55},
] # example prices from three bookmakers
print(consensus(books))5. Do it on real odds from the API
The odds endpoint returns one row per bookmaker for a fixture. It needs a Developer plan, which the 14-day trial includes (start it at /register, no card needed). Some fixtures have no odds yet and return a 404, which is normal for early or smaller-league matches.
import requests
API = "https://api.predictiqpro.com"
HEADERS = {"Authorization": "Token YOUR_API_KEY"}
FIXTURE_ID = 123456 # replace with a real fixture id from /fixtures/
r = requests.get(f"{API}/odds/{FIXTURE_ID}", headers=HEADERS, timeout=15)
r.raise_for_status()
rows = r.json()["results"]
books = []
for row in rows:
try:
b = {"home": float(row["home"]), "draw": float(row["draw"]), "away": float(row["away"])}
except (TypeError, ValueError, KeyError):
continue # skip incomplete rows
books.append(b)
print(len(books), "bookmakers")
print(consensus(books))6. Check your work
The fair probabilities should add to 1.0. If a bookmaker's total comes out below 100% before you de-vig, treat it as bad data and skip it. You can test any set of prices with the free odds calculator at /odds-calculator. If you want the consensus without doing the maths, the prediction API serves it directly where odds exist, with a basis field saying so.
Related guides
Ready to build?
Full endpoint reference is in the docs; plans are on the pricing page.
Start 14-day trial