Sports betting at the Monte Carlo
Many Americans have picked up a new hobby in recent years: sports betting. The industry has exploded since the US Supreme Court threw out a federal ban in 2018.
While the stereotype is still a smoke-filled room in a dark corner of a casino, these days the majority of sports betting happens online. And analysts say the COVID-19 pandemic helped grow the industry’s online footprint. It gave people something to do from home—at least when sports were on TV. Sports fans have been managing their fantasy teams for years so it felt natural to start betting on games. And with some states now experiencing budget shortfalls they may look to expand the industry further.
So placing a bet is easier than ever. What does the math look like? How often do you need to win to make a profit? The sportsbooks have to be making money somehow.
Sportsbooks do it through vigorish—often called juice. This is the extra percent that a book charges to take your bet. The industry standard is -110, which means you’ll lay $110 for a chance to win $100. But if you lose, the book will keep the whole $110. Juice varies depending on the book, type of bet, and normal market forces like which side has received more action. But in general -110 is considered standard.
At that rate the math works out like this:
Solving for p, win rate, you’ll find that p = 11/21
or about 52.4%. This is the win rate you’d need to break even.
That doesn’t sound like an extraordinarily difficult task. To turn a profit you only have to be slightly better than some guy flipping a coin.
Just for fun, say you have a 55% win rate and you make one-hundred $100 bets. Assuming variance didn’t exist, you’d expect to win 55 times and lose the other 45. Such a run would produce a profit of $550 on $11,000 wagered, or an ROI of 5%. Not bad, right? On average it takes a whole year to see a 9% return in the stock market.
But in the real world very few people can pick 55% over the long run.
And just as important for casual bettors: variance does exist. You’ll experience runs of 65% or better—or lose just as often—over a sample of hundreds of bets. The idea of 55% (or 54 or 53) is easy enough to wrap your head around in abstract terms, but keeping perspective during a hot or cold streak is much more difficult. When the probability of success differs only slightly from a coinflip, bettors will often find themselves in the middle of seemingly impossible streaks—both good and bad.
We can build a simulation (similar to the Monte Carlo method) to visualize sports betting results.
If we know expected win rate it’s very simple. Pick a random number between 0 and 1 and check whether it’s larger or smaller than the win rate. For example, in pseudocode:
function bet(): r = random_number() if r <= win_rate: # win 1.0 unit else: # lose 1.1 units
Each iteration of this function represents a single bet. We can then repeat the process thousands of times and keep a ledger of profit/loss.
A quick simulation of 1,000 bets using a 55% win rate looked like this:
Units are a way of normalizing bet sizes. You can bet $1 or $1,000 and the probability of winning will be the same.
The bettor in this particular simulation was fortunate to see a nice 35-unit profit over 1,000 bets. But it’s important to remember that 1,000 bets will span a lifetime for most casual bettors. This person was well into the red after 700 bets. And that’s with an intentionally inflated assumption of a 55% expected win rate! Simulations running at 53% will be even more volatile. Any rate below 52.38% will trend downward in the long run.
Of course when I run this script again the plot will look completely different. The unpredictability is the point. You can be a great handicapper and it’s still very difficult to profit consistently. There’s a reason hedge funds organize around beating stock markets rather than betting markets. Trading fees leave more meat on the bone.
That said, I probably sound more cynical than I’d like. Sports betting can be a great hobby, especially for data-minded people who have access to nearly unlimited information. Simulations for table games like craps or roulette would be even more dismal because, unlike sports betting, there’s no way to beat those games in the long run.
So what am I doing here? Just adding a little context. I’ve been worn down by the deluge of sports book ads and I know they’re selling an experience that doesn’t exist. Keep expectations in check, remember you aren’t the next Billy Walters, and (most importantly) don’t let me tell you what to do with your money.
Full code:
from random import random import matplotlib.pyplot as plt from matplotlib import style import matplotlib.ticker as ticker def one_bet(win_rate, unit_size): if random() <= win_rate: return unit_size else: return unit_size * -11 / 10 def simulate(win_rate, unit_size, number_sims): profit = 0 x_hist, y_hist = [], [] for bet in range(number_sims): x_hist.append(bet + 1) profit += one_bet(win_rate, unit_size) y_hist.append(profit) return x_hist, y_hist def generate_plot(x_data, y_data, win_rate_label, sims_label, output_filename): style.use("ggplot") fig, ax = plt.subplots(figsize=(12, 6)) fig.subplots_adjust(left=0.063, right=0.976, top=0.935, bottom=0.1) ax.plot(x_data, y_data) x_format = ticker.StrMethodFormatter("{x:,.0f}") ax.xaxis.set_major_formatter(x_format) y_format = ticker.StrMethodFormatter("{x:+.0f}") ax.yaxis.set_major_formatter(y_format) font = "Ubuntu Condensed" plt.xticks(font=font, fontsize=12) plt.yticks(font=font, fontsize=13) ax.set_xlabel("Number of Bets", font=font, size=15, labelpad=6) ax.set_ylabel("Profit (Units)", font=font, size=15, labelpad=6) ax.set_title(f"Sports Betting Profit Simulation | {win_rate_label * 100:.0f}% win rate – {sims_label:,} bets", font=font, size=18) plt.savefig(output_filename, facecolor=fig.get_facecolor()) return WIN_RATE = 0.55 UNIT_SIZE = 1.0 NUMBER_SIMS = 1000 x_ledger, y_ledger = simulate(win_rate=WIN_RATE, unit_size=UNIT_SIZE, number_sims=NUMBER_SIMS) generate_plot(x_ledger, y_ledger, win_rate_label=WIN_RATE, sims_label=NUMBER_SIMS, output_filename="bet_simu.png")