Stake sizing, Part 1

In the last post we used Python code to take a look at a classic gambling situation, the coin flip, to make a point about the importance of choosing the highest odds available to bet at. Today, we’ll again use the coin flipping example to investigate another fundamental principal of successful gambling: stake sizing.

Now, imagine we’re one of the lucky punters from the last post who were allowed to bet on a fair coin flip at odds of 2.03. As I stated then, this is pretty much like a license to print money – but how much of your bankroll should you bet on each flip of the coin? Knowing that the coin was indeed fair and you would be getting the best of it, a natural instinct could be to bet as much as you could possibly cough up, steal and borrow in order to maximize your profit. This is a poor strategy though, as we’ll soon come to see.

The reason for this is that even if we do have come across a profitable proposition, our edge when betting at a (I’ll empasize it again: fair) coin flip at 2.03 odds is only 1.5% – meaning that for each 1 unit bet we are expected to net 0.015 units on average. This conclusion should be absolute basics for anyone interested in serious gambling, but to make sure we’re all on the same page I’ll throw some maths at you:

The Expected Value, or EV, of any bet is, simply put, the sum of all outcomes multiplied by their respective probabilities – indicating the punter’s average profit or loss on each bet. So with our coin flip, we’ll win a net of 1.03 units 50% of the time and lose 1 unit 50% of the time; our EV is therefore 1.03 * 0.5 + (-1 * 0.5) = 0.015, for a positive edge of 1.5% and an average profit of 0.015 units per bet. For these simple types of bets though, an easier way to calculate EV is to divide the given odds by the true odds and subtract 1: 2.03 / 2.0 – 1 = 0.015.

An edge of only 1.5% is nothing to scoff at though, empires has been built on less, so we’ll definitely want to bet something – but how much?

Stake sizing is much down to personal preferences about risk aversion and tolerance of the variance innately involved in gambling, but with some Python code we can at least have a look at some different strategies before we set out to chase riches and glory flipping coins. Just like in the last post I’ll just give you the code with some comments in it, which will hopefully guide you along what’s happening  before I briefly explain it.

Here we go:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

def coin_flips(n=10000,odds=1.97,bankroll=100,stake=1,bankrupt=False):
    Simulates 10000 coinflips for a single punter, betting at 1.97 odds,
    also calculates net winnings

    NEW: default bankroll and stake set at 100 and 1, respectively
    now also calculates if player went bankrupt or not

    # create a pandas dataframe for storing coin flip results
    # and calculate net winnings
    df = pd.DataFrame()
    # insert n number of coinflips, 0=loss, 1=win
    df['result'] = np.random.randint(2,size=n)
    # calculate net winnings
    df['net'] = np.where(df['result']==1,stake*odds-stake,-stake)
    # calculate cumulative net winnings
    df['cum_net'] = df['net'].cumsum()

    # calculate total bankroll
    df['bankroll'] = df['cum_net'] + bankroll

    # if bankroll goes below the default stake, punter will stop betting
    # count times bankroll < stake
    df['bankrupt'] = np.where(df['bankroll']<stake,1,0)
    # count cumulative bankruptcies, with column shifted one step down
    df['bankruptcies'] = df['bankrupt'].cumsum().shift(1)
    # in case first flip is a loss, bankruptcies will be NaN, replace with 0
    # drop all flips after first bankruptcy
    if bankrupt:
        df = df[df['bankruptcies']==0]

    return df

First off, we’ll modify our original coin_flips function to take our punter’s bankroll and stake size into consideration, setting the bankrupt threshold at the point where a default sized bet can no longer be made. By default, our punter will have an endless stream of 100 unit bankrolls, but if we set the parameter bankrupt to True, the function will cut away any coin flips after his first bankruptcy.

def many_coin_flips(punters=100,n=10000,odds=1.97,bankroll=100,stake=1,color='r',plot=False,bankrupt=False):
    Simulates 10000 coinflips for 100 different punters,
    all betting at 1.97 odds,
    also calculates and plots net winnings for each punter

    NEW: now also saves punter bankruptcies

    # create pandas dataframe for storing punter results
    punter_df = pd.DataFrame()
    # loop through all punters
    for i in np.arange(punters):
        # simulate coin flips
        df = coin_flips(n,odds,bankroll,stake,bankrupt)
        # calculate net
        net = df['net'].sum()
        # check for bankruptcy
        bankruptcy = df['bankrupt'].sum()

        # append to our punter dataframe
        punter_df = punter_df.append({'odds':odds,

        if plot:
            # plot the cumulative winnings over time

    # check if punters ended up in profit
    punter_df['winning'] = np.where(punter_df['net']>0,1,0)

    return punter_df

We also want to modify the many_coin_flips function so that it’ll also take bankroll and stake size into consideration, counting up how many of our punters went bankrupt.

We won’t use the compare_odds function here, instead we’ll write a new one to compare stake sizing – but if we ever want to use it again sometime in the future a few minor changes will be needed here as well:

def compare_odds(punters=100,n=10000,odds=[1.97,2.00,2.03]):
    Simulates and compare coin flip net winnings
    after 10000 flips for 3 groups of punters,
    betting at odds of 1.97, 2.00 and 2.03, respectively.
    Also plots every punters net winnings

    # create figure and ax objects to plot on
    fig, ax = plt.subplots()

    # set y coordinates for annotating text for each group of punters
    ys = [0.25,0.5,0.75]
    # assign colors to each group of punters
    cs = ['r','y','g']

    # loop through the groups of punters, with their respective odds,
    # chosen color and y for annotating text
    for odd, color, y in zip(odds,cs,ys):
        # run coin flip simulation with given odds, plot with chosen color
        df = many_coin_flips(punters,n,odd,color=color,plot=True)
        # calculate how many punters in the group ended up in profit
        winning_punters = df['winning'].mean()
        # set a text to annotate
        win_text = '%.2f: %.0f%%' %(odd,winning_punters * 100)
        # annotate odds and chance of profit for each group of punters
                    xycoords='axes fraction', color=color,va='center')

    # set title
    ax.set_title('Chances of ending up in profit after %s coin flips' %n)
    # set x and y axis labels
    ax.set_xlabel('Number of flips')
    ax.set_ylabel('Net profit')
    # add annotation 'legend'
    ax.annotate('odds: chance',xy=(1.02,1.0),
                xycoords=('axes fraction'),fontsize=10,va='center')
    # add horizontal line at breakeven point
    # set y axis range at some nice number

    # show plot

Now, with all our previous coin flip functions taking bankroll and stake size into consideration, we can go ahead and evaluate a few stake sizing strategies with a new function:

def compare_stakes(punters=200,n=10000,odds=2.03,stakes=[100,50,25,10,5,2,1,0.5],bankroll=100):
    Similar to compare_odds, but here we instead want to compare different
    staking sizes for our coin flips betting at 2.03 odds

    Increased number of punters in each group, from 100 to 200

    Also prints out the results

    # pandas df to store results
    results_df = pd.DataFrame(columns=['stake','win','lose','bankrupt'])

    # colors to use in plot later, green=1=win, yellow=4=lost, red=2=bankrupt
    colors = [sns.color_palette()[i] for i in (1,4,2)]

    # loop through the groups of punters, with their respective odds
    for stake in stakes:
        # run coin flip simulation with given stake
        df = many_coin_flips(punters,n,odds,stake=stake,bankrupt=True)
        # calculate how many punters in the group ended up in profit
        winning_punters = df['winning'].mean()
        # ...and how many went bankrupt
        bankrupt_punters = df['bankrupt'].mean()
        # lost money but not bankrupt
        lose = 1 - winning_punters - bankrupt_punters

        # append to dataframe
        results_df = results_df.append({'stake':stake,

    # set stake as index

    # plot
    fig = plt.figure()
    # create ax object
    ax = results_df.plot(kind='bar',stacked=True,color=colors,alpha=0.8)
    # fix title, axis labels etc
    ax.set_title('Simulation results: betting %s coin flips at %s odds, starting bankroll %s' %(n,odds,bankroll))
    # set legend outside plot

    # add percentage annotation for both win and bankrupt
    for x, w, l, b in zip(np.arange(len(results_df)),results_df['win'],results_df['lose'],results_df['bankrupt']):
        # calculate y coordinates
        win_y = w/2
        lost_y = w + l/2
        bankr_y = w + l + b/2

        # annotate win, lose and bankrupt %, only if >=2%
        if w >= 0.04:
            ax.annotate('%.0f%%' %(w * 100),xy=(x,win_y),va='center',ha='center')
        if l >= 0.04:
            ax.annotate('%.0f%%' %(l * 100),xy=(x,lost_y),va='center',ha='center')
        if b >= 0.04:
            ax.annotate('%.0f%%' %(b * 100),xy=(x,bankr_y),va='center',ha='center')

By default, our new compare_stakes function creates a number of punter groups, all betting on fair coin flips at 2.03 odds with a starting bankroll of a 100 units. For each group and their different staking plan, the function takes note of how many ended up in profit, how many lost and how many went bankrupt.

As we can see on the plot below, the results differ substantially:


Just like last time, I want to remind you that any numbers here are only rough estimates, and increasing the size of each punter group as well as the number of coin flips will get us closer to the true values.

So what can we learn from the above plot? Well, the main lesson is that even if you have a theoretically profitable bet, your edge will account for nearly nothing if you are too bold with your staking. Putting your whole bankroll at risk will see you go bankrupt around 96% of the time, and even if you bet as small as 2 units, you’ll still face a considerable risk of screwing up a lucrative proposition. The truth is that with such a small edge, keeping your bet small as well is the way to go if you want to make it in the long run.

But what if some fool offered us even higher odds, let’s say 2.20? First off, we would have to check if the person was A: mentally stable, and B: rich enough to pay us if (or rather, when) we win, before we go ahead and bet. Here our edge would be 10% (2.2 / 2.0 – 1), nearly 10 times as large as in the 2.03 situation, so we’ll likely be able to bet more – but how much? Well, the functions are written with this in mind, enabling us to play around with different situations and strategies. Specifying the odds parameter of our new function as 2.20, here’s what betting at a fair coin flip at 2.20 odds would look like:


As can be seen from the new plot, with a larger edge we can go ahead and raise our stake size considerably, hopefully boosting our winnings as well. So the main take-away from this small exercise is that even if you have an edge, if you want to make it in the long run you’ll have to be careful with your staking to avoid blowing up your bankroll – but also that the larger your edge, the larger you can afford to bet.

That’s it for now, but I’ll hopefully be back soon with a Part 2 about stake sizing, looking at a staking plan that actually takes your (perceived) edge into account when calculating the optimal stake size: The Kelly Criterion.

Stake sizing, Part 1