Implied Probability, Overround and No-Vig Odds Explained
Convert odds to implied probability, measure a bookmaker's overround and margin, remove the vig to get fair odds, and run the math in Python on live API data.
Implied probability is 1 divided by the decimal odds, so a price of 2.50 implies a 40% chance. Add up the implied probabilities of every outcome in a market and whatever exceeds 100% is the bookmaker's overround; scale it out and you have no-vig, or fair, odds.
This guide works through each formula with real numbers, then turns them into Python helpers that run on the normalized odds the PulseScore API returns for 60+ bookmakers.
What is implied probability?
Decimal odds are the total return per unit staked, stake included. A winning bet at 2.50 returns 2.50 for every 1 staked, so the win rate at which the bet breaks even, the implied probability, is 1 / 2.50 = 0.40.
implied probability = 1 / decimal odds| Decimal odds | Implied probability |
|---|---|
| 1.50 | 66.67% |
| 2.00 | 50.00% |
| 2.50 | 40.00% |
| 4.00 | 25.00% |
| 10.00 | 10.00% |
Every selection in a PulseScore response carries its price as a decimal number in odds (for example 2.1), so for sportsbook data this is the only conversion you need before doing any math.
How do you convert decimal, fractional and American odds?
Fractional odds quote the profit per unit staked. American odds quote the profit on a 100 stake when positive, or the stake needed to win 100 when negative. Decimal is the easiest form to calculate with, so convert everything to decimal first:
fractional a/b -> decimal = 1 + a / b 7/2 -> 4.50
decimal >= 2 -> American = +(decimal - 1) * 100 2.50 -> +150
decimal < 2 -> American = -100 / (decimal - 1) 1.50 -> -200
American +A -> decimal = 1 + A / 100 +150 -> 2.50
American -A -> decimal = 1 + 100 / A -200 -> 1.50Racing endpoints quote both forms side by side: a bet365 runner priced 7/2 carries 4.5000 as its decimal, a handy check for your converter.
from fractions import Fraction
def from_fractional(text):
"""'7/2' -> 4.5"""
num, den = text.split("/")
return 1 + int(num) / int(den)
def to_american(decimal):
"""2.50 -> 150, 1.50 -> -200"""
if decimal >= 2:
return round((decimal - 1) * 100)
return round(-100 / (decimal - 1))
def from_american(american):
"""150 -> 2.5, -200 -> 1.5"""
if american > 0:
return 1 + american / 100
return 1 + 100 / -american
def to_fractional(decimal, max_denominator=100):
"""2.5 -> Fraction(3, 2)"""
return Fraction(decimal - 1).limit_denominator(max_denominator)The fraction you compute is exact, not necessarily the one a bookmaker would show: books round fractional prices to their own ladder. A decimal of 1.5714 converts back to 4/7, while 1.57 becomes 57/100.
What is the overround?
A bookmaker prices every outcome a little short, so the implied probabilities of a complete market add up to more than 100%. That total is the booksum, and the excess is the overround:
booksum = sum of (1 / odds) over every outcome of one market
overround = booksum - 1
margin = 1 - 1 / booksumTake the pre-match full-time result from the API docs example, Ipswich v Liverpool at 5.25 / 4.75 / 1.50:
| Outcome | Odds | Implied probability |
|---|---|---|
| HOME (Ipswich) | 5.25 | 19.05% |
| DRAW | 4.75 | 21.05% |
| AWAY (Liverpool) | 1.50 | 66.67% |
The three add up to a booksum of 1.0677, or 106.77%, so the overround is 6.77%. The margin, 1 - 1 / 1.0677 = 6.34%, is the share of turnover the bookmaker keeps when money arrives in proportion to its prices: stake (1 / odds) / booksum on each outcome and every result pays back 1 / booksum, or 0.9366 per unit staked. Both numbers get called the vig or the juice, so state which formula you report.
Two-way markets work the same way. The docs example's Over/Under 2.5 goals at 1.3636 / 3.20 has a booksum of 1.0459, an overround of 4.59%.
How do you remove the vig?
To estimate fair probabilities, divide each implied probability by the booksum so the outcomes add up to exactly 1. This proportional, or multiplicative, method is the simplest and the usual starting point:
fair probability = (1 / odds) / booksum
fair odds = odds * booksum| Outcome | Odds | Fair probability | Fair odds |
|---|---|---|---|
| HOME | 5.25 | 17.84% | 5.61 |
| DRAW | 4.75 | 19.72% | 5.07 |
| AWAY | 1.50 | 62.44% | 1.60 |
Proportional scaling assumes the margin is spread across outcomes in proportion to their probabilities. Other methods, such as additive, power and Shin's model, spread it differently, mostly between favorites and long shots. If you build a model on fair prices, test which method fits your markets instead of assuming one.
Applying the math to PulseScore odds
Every bookmaker in the API returns the same shape: an event holds markets[], each market has canonicalMarket, period, isActive, an id such as marketId and selections[], and each selection has canonicalOutcome, odds, isActive and, on totals and handicaps, a line. The schema reference documents every field. Three rules keep the math honest:
- Only combine selections that sit in the same market object, and never mix periods such as
FULL_TIMEandFIRST_HALF. - Pair
OVERandUNDERselections that share aline; prices from different lines do not form a book. - Skip anything with
isActive: falseoroddsat or below 1, and compute a booksum only once every outcome is present. A suspended draw makes a 1X2 market look like a bargain.
OUTCOMES = {
"MATCH_RESULT": {"HOME", "DRAW", "AWAY"},
"OVER_UNDER": {"OVER", "UNDER"},
}
def complete_books(event, period="FULL_TIME"):
"""Yield (market name, line, {outcome: odds}) for every complete, active book."""
for market in event.get("markets", []):
needed = OUTCOMES.get(market.get("canonicalMarket"))
if not needed or market.get("period") != period or not market.get("isActive"):
continue
by_line = {}
for sel in market.get("selections", []):
if sel.get("isActive") and sel.get("odds", 0) > 1:
by_line.setdefault(sel.get("line"), {})[sel.get("canonicalOutcome")] = sel["odds"]
for line, prices in by_line.items():
if set(prices) == needed:
yield market.get("rawName"), line, prices
def booksum(prices):
return sum(1 / odds for odds in prices)
def no_vig(prices):
"""{outcome: odds} -> {outcome: fair odds}, margin removed proportionally."""
total = booksum(prices.values())
return {outcome: odds * total for outcome, odds in prices.items()}Run it over a page of pre-match events. The paths are the same for every bookmaker, so only the prefix changes:
import requests
HEADERS = {"X-Secret": "YOUR_API_KEY", "Accept-Encoding": "gzip"}
url = "https://api.pulsescore.net/api/v3/bet365/soccer/events"
events = requests.get(url, headers=HEADERS, params={"limit": 30}, timeout=15).json()["events"]
for event in events:
for name, line, prices in complete_books(event):
total = booksum(prices.values())
fair = no_vig(prices)
label = name if line is None else f"{name} {line}"
print(f"{event['home']} v {event['away']} | {label} | overround {total - 1:.2%}")
for outcome, odds in prices.items():
print(f" {outcome:<5} {odds:>6.2f} fair {fair[outcome]:.2f}")Best prices across bookmakers
Overround is measured per bookmaker, but nothing stops you from assembling a book out of the best price for each outcome across several bookmakers. Because every bookmaker uses the same canonical outcomes, the best-price book is a dictionary merge once the fixture is matched:
def best_prices(books):
"""books: {bookmaker: {outcome: odds}} for one fixture and one market."""
best = {}
for bookmaker, prices in books.items():
for outcome, odds in prices.items():
if odds > best.get(outcome, (0, ""))[0]:
best[outcome] = (odds, bookmaker)
return best
books = { # illustrative prices for one fixture
"fanduel": {"HOME": 2.30, "DRAW": 3.40, "AWAY": 3.10},
"betfair-sb": {"HOME": 2.10, "DRAW": 3.60, "AWAY": 3.50},
}
best = best_prices(books)
total = booksum(odds for odds, _ in best.values())
print(f"best-price booksum {total:.4f}")
if total < 1:
print(f"arbitrage: {1 / total - 1:.2%} return on the total stake")Here the best prices are 2.30, 3.60 and 3.50, a booksum of 0.9983. When the best-price booksum drops below 1, staking every outcome in proportion to its implied probability returns more than the total stake whatever happens: an arbitrage, worth 0.17% in this example.
The arbitrage bot guide covers stake sizing and the practical limits. Matching the same fixture across bookmakers is the hard part, since team names and event ids differ per bookmaker; the odds comparison tutorial walks through it.
Prediction markets: the price is the probability
On Polymarket and Kalshi a contract pays 1 if its outcome happens, so its price is already an implied probability. PulseScore keeps the exchange's lastPrice, bestBid and bestAsk, all between 0 and 1, on each selection, and sets odds to 1 / bestAsk, the decimal price of buying the outcome now, falling back to 1 / lastPrice when no ask is quoted.
| Outcome | Bid | Ask | Last |
|---|---|---|---|
| HOME | 0.44 | 0.47 | 0.455 |
| DRAW | 0.29 | 0.30 | 0.295 |
| AWAY | 0.23 | 0.26 | 0.245 |
The asks in this Polymarket example sum to 1.03: buying every outcome costs 3% more than the 1 it pays out, the exchange's version of an overround. The bids sum to 0.96 and the mid-points, (bid + ask) / 2, to 0.995, so the mid is the market's own estimate and the spread, ask - bid, is the cost of trading. Exchange fees are not part of these prices.
def market_probability(selection):
"""Mid of bid and ask when both are quoted, else the last trade."""
bid, ask = selection.get("bestBid"), selection.get("bestAsk")
if bid is not None and ask is not None and 0 < bid <= ask < 1:
return (bid + ask) / 2
return selection.get("lastPrice")bestBid and bestAsk are optional, since not every selection is quoted on both sides, so fall back to lastPrice, and remember that a last trade can be old on a quiet market. The prediction market guide compares these probabilities with a sportsbook's no-vig line for the same match.
Common pitfalls
- Comparing prices taken at different times: odds move, so compare a live price with a live price and fetch the books you compare close together.
- Mixing markets: a first-half total and a full-time total are different books even when their
linematches. - Rounding too early: keep full precision and round only for display, because rounding errors across three outcomes can create or hide a thin arbitrage.
- Treating each-way terms as a separate price: racing terms such as
EW: 1/5 odds 3 placespay a fraction of the win odds for a place, which is a different calculation from the win market.
Frequently asked questions
How do you calculate implied probability from decimal odds?
Divide 1 by the decimal odds. Odds of 2.50 imply 1 / 2.50 = 0.40, a 40% chance, and odds of 1.50 imply 66.67%.
What is the difference between overround and margin?
Overround is the booksum minus 1: how far a market's implied probabilities add up beyond 100%. Margin is 1 minus 1 divided by the booksum: the share of turnover a bookmaker keeps when money arrives in proportion to its prices. A booksum of 1.0677 is a 6.77% overround and a 6.34% margin.
How do you remove the vig from betting odds?
The simplest method divides each implied probability by the booksum so the probabilities add up to 1, which makes the fair odds the quoted odds multiplied by the booksum. Other methods, such as additive, power or Shin's model, spread the margin differently between favorites and long shots.
How do you convert decimal odds to American odds?
For decimal odds of 2.00 or more, American odds are (decimal - 1) x 100, so 2.50 becomes +150. Below 2.00 they are -100 / (decimal - 1), so 1.50 becomes -200.
How do Polymarket and Kalshi prices relate to odds?
A prediction-market price between 0 and 1 is already an implied probability. PulseScore sets the decimal odds of a Polymarket or Kalshi selection to 1 divided by the best ask, falling back to 1 divided by the last traded price when no ask is quoted, and keeps lastPrice, bestBid and bestAsk on the selection.
Keep reading
Build an Arbitrage Betting Bot Across Bookmakers
Find sure bets across bookmakers in Python: match the same fixture on every book, compare 1X2 and over/under prices, size the stakes, respect rate limits.
How to Build an Odds Comparison Tool with One API
Build an odds comparison tool in Node.js: pull one league from several bookmakers with one API key, match fixtures, show the best price and each book's margin.