Predict NBA Player Lines with Monte Carlo Simulation | by Carter Bouley | Towards Data Science

I do not in any way encourage gambling. I do not encourage the use of my model, in fact, I actively discourage it. This is purely for educational use and should not be used or relied upon with any real money. Seriously.
My previous experience as a professional sports bettor relied heavily on efficient market hypothesis. It suggests that asset prices reflect all available information. In sports betting terms, in a free market (such as an exchange like Betfair exchange) the prices on any event accurately reflects that events chance of occurring.
This theory’s origins come from the early 1900s (and wasn’t popularised till decades later), and one story explains it well. In 1906 the great statistician Francis Galton observed a competition to guess the weight of an ox at a country fair. Eight hundred people entered. Galton, being the kind of man he was, ran statistical tests on the numbers. While some individual guesses were far too high, and others very low, he discovered that the average guess (1,197lb) was extremely close to the actual weight (1,198lb) of the ox. This story was told by James Surowiecki, in his entertaining book The Wisdom of Crowds. I highly recommend it.
But there lie several difficulties with using these assumptions to find value, and place bets:
This assumption is only valid where there is sufficient liquidity on the markets. There are a large number of markets where there is a lack of liquidity. This means we cannot be confident that the price is correct (we have only a very small crowd). As such, less popular markets like player lines cannot be bet on using this technique. Even now as I write this, a German Bundesliga game with £150,000 matched on win/draw/away has only £2,500 matched on the over/under 3.5 goals. The lack of liquidity in this line means we cannot assume it is correctly priced.
The technique I previously used relies solely on beating the market price, meaning one can not trade on these exchanges. Most bookmakers limit people who use this technique, therefore for continuous profit, a different technique must be developed. As well as this, the most useful technique is one which can be used on the exchanges profitably.
There are professional sports bettors and traders that consistently turn a profit by using only the exchange. While they may be in the minority, that must mean there is at least a possibility of being more wise than the crowds.
These limitations led me to explore developing techniques to predict sporting event outcomes that do not rely on the wisdom of the crowd.
Monte Carlo simulations are a broad range of tools which rely on repeated sampling to obtain numerical results. They are often used in physics or maths when it is difficult to use other approaches. They are particularly helpful when looking at predicting events where randomness plays a significant role (I.E. sports events).
This is because instead of giving a deterministic result with a degree of uncertainty, as most typical machine learning algorithms do, Monte Carlo simulations invert this. It solves a deterministic problem using only a range of probabilities. Instead of simply giving us the result, it will output the probability distribution of all the potential results. This is fantastic for sports betting. If we manage to accurately simulate a game, it is possible to create a set of odds, or build fair lines. This result can then be compared to the market and various bookmakers, and we can decide if we have value.
Sports contains a huge amount of randomness and variation, and on top of that there is limited amount of data to work with. Lineups change regularly, teams that played well last season may perform poorly next season. Historical data is largely irrelevant for this reason.
There is so much randomness in games that looking at one prediction of a game result is not particularly helpful. Just like looking at one spin of a roulette wheel is not very helpful — casino’s don’t care about what happens in one spin. They don’t even care what happens in 100, or 1000 spins. They only care about understanding how their games generate an edge over the millions of spins that occur in the casino’s lifetime. This limits how well we can train traditional machine learning models.
Casinos make money by understanding that their edge is realised over the long run, so sharp sports bettors should do the same. However, with only 82 games per team each season in the NBA and even less this season due to coronavirus we have limited data to base our predictions off. Therefore we want the ability to simulate games en masse, thousands of times, if not millions to build an accurate range of outcomes and probabilities.
When applied to sports, what we can essentially do is sample from several known distributions (previous games results) and produce a new, unknown distribution (a new games result and probability distribution).
The data I used was scraped from https://www.basketball-reference.com. It provides game by game statistics on every player in the NBA, and I used data from 2013 onwards.
There are several insights I discovered early on when exploring the data, which will affect how I design my model. Firstly, players in different positions score a different average number of points. Secondly, there is a home advantage in points scoring in every position.
This is hardly surprising, as it’s an assumption that even the most basic sports fan can make. However they are important to check when considering how to build a simulation of the game. Given that a home advantage exists, and positional differences occur, we need to include this information in any simulation.
Another relatively simple assumption I checked, was the effect minutes played has on the number of points scored. It makes sense, that the longer a player is on the court, the more time they have to score points. Below is a regression plot to show the nature of this relationship, as it is difficult to see the density of the scatter points. This correlation carries through to other metrics, which makes sense — more time, more assists, steals, blocks, points, etc.
There is a home advantage
Points scored is related to minutes played
There is a positional difference in points scored, and that carries on through the home advantage.
This is the information I use to build the distributions from which I will sample from. There is a large assumption I make about the metrics, and that is they are in general, normally distributed.
Below is the distribution of minutes played for Nick Young, and while not exactly normally distributed, is close enough for my purposes.
Points scored is certainly more of a stretch, and this should certainly be noted as a limitation to think about adjusting later on.
Potential Methodologies
There are several methodologies for building distributions to repeatedly sample from which I considered:
Bootstrapping: Sampling repeatedly from these known distributions, with replacement. If we repeatedly sampled from the points scored distribution above, there would never be a simulation showing Nick Young scoring 35 points. In the past, he has scored around 29 points and 38 points so it is a situation that is clearly possible, and we want to reflect that.
Build a Poisson distribution: Another option I considered is building a poisson distribution. It forces all numbers to be integers above zero, which is helpful for our purposes. However in a poisson distribution, the mean and standard deviation are the same number. I could then sample from this artificially built distribution
Build a Normal distribution with tweaks: The option I went with is to generate a normal distribution using the mean and standard deviation of the players statistics, and repeatedly sample from that. I then force numbers to be integers, and negative numbers to be zeros. This helps to combat both limitations of the above methodologies.
I generated a distribution of a players game results, using the statistics of his history. This allows me to build a distribution of thousands of games, rather than just the limited historical data that exists.
The new distribution looks as follows. While different to the original, it effectively tackled the issue outlined above, and the shape is broadly similar.
Now finally to the fun part.
I built several functions to perform the simulations.
This first function simply finds the team lineup from the recent season (as we don’t care about players that aren’t in the team) and returns a dictionary with their position, for easy reference later on.
def get_player_position_dict(team):            # This function collects all the players names of the team, and       inputs it into    # A dictionary, along with each player prospective position.home_team_lineup = df.loc[(df['Tm'] == team) & (df['Date'] >= '2017-10-17 00:00:00')][['Player','Pos']]    home_team_lineup = home_team_lineup.drop_duplicates().sort_values(by = 'Player')    home_team_lineup_list = list(home_team_lineup['Player'])    home_team_position_list = list(home_team_lineup['Pos'])    team_player_position_dict = dict(zip(home_team_lineup_list, home_team_position_list ))        return team_player_position_dict
The next function contains a lot of code, so I’ll explain it in words and feel free to skip through it. It is also on my Github.
For the number of simulations requested, and for an individual player, the function randomly samples from the distributions I have built for them. It takes one sample of how long they played, multiplied by one sample of how many points they score per minute, plus a sample on the number of points the opposing team concedes from players in that position. This whole process is one game’s simulation
This number is divided by 2, so offensive and defensive statistics are taken into account, and that score is entered into a list.
def score_sim(mp_mean, scoring_rate_mean, mp_std, scoring_rate_std, away_PTS_conceded_mean, away_PTS_conceded_std, ns):         score_list = []        for i in range(ns):        # How long an individual home team player plays at home        home_minutes_played = np.random.normal(mp_mean,                                          mp_std)# How effective they are at scoring points at home        home_player_scoring_effectiveness =  np.random.normal(scoring_rate_mean, scoring_rate_std)# How often does the team that is away concede when they are away, broken down by location    # Note here location is also technically 'home' because we are looking at them as an Oppaway_positional_conceding = np.random.normal(away_PTS_conceded_mean, away_PTS_conceded_std)# Force negatives to be 0        parameters = [home_minutes_played, home_player_scoring_effectiveness, away_positional_conceding]        parameters = [0 if x<= 0 else x for x in parameters]# Convert to integer as we cannot score 0.5 points        try:            home_player_score = int((((parameters[0] * parameters[1]) + parameters[2])/2))        except:            home_player_score = 'NA'                score_list.append(home_player_score)return score_list
The result of running this on one particular player (Stephen Curry) against the Toronto Raptors, gives the following result (I have converted the list to a histogram for easier viewing. This shows his expected points distribution over 10,000 games.
So in essence what I have built is a new distribution of Stephen Curry scoring ability’s, but with some adjustment based on the defensive ability of the opposing team, namely their ability to defend from players in Stephen Curry’s position.
For simplicity, this first iteration of the simulation of the model is to find fair lines for even money. If we can find fair lines (accurately), then we can search for prices better than the probability we expect, and think about if they provide value. The following function orders the simulated scores, and finds the median value.
def convert_to_line(score):        score.sort()    freq = {x:score.count(x) for x in score}    values_list = list()    for val, count in freq.items():        for count in range(count):            values_list.append(val)median_computed = statistics.median(values_list)    return median_computed
Its output is for this set of results is 17. What this means is that our simulations suggest that, half the time vs the Toronto Raptors Stephen Curry scores above 17, and the other half, below 17.
For sports betting purposes, it means if we can find somewhere offering us better than evens for this line (SC to score over or under 17 points), based purely off this model we may believe it to be a value bet. However a word of warning — this is highly hypothetical. There is likely much more granularity that could go into defining this process before placing any money on it, it was just a simple pet project. I urge you highly NOT to use this for real money bets, nor do I encourage gambling in any way. It is simply a starting point to begin building your own model.
The full simulation model is on my GitHub so feel free to take a look, but the final result produces player lines for every player on both teams. It is however too much code to post on medium, but this is the output of 10,000 simulations (which takes less than 40 seconds)for Golden State Warrior lines vs Toronto Raptors.
This model has not been tested on any previous games, and I am currently looking at different ways to test its accuracy.
Some future improvements:
Weight older data, so it becomes less relevant in the model. What happened in 2013 should not have as much impact as what happened in 2017
Include further, more granular statistics on each of the players stats. This model does not take into account, for example, that Stephen Curry mainly scores 3 pointers, nor does it account for TORs ability to defend against those as opposed to free throws.
Because the game simulation is limited to scoring there is no input to take account for blocks, assists, rebounds and their contribution to scoring. Including this would generate a more realistic and applicable model.
It takes no information such as injuries or what type of game is being played into account. Given the simulated game is an NBA final, we should probably add in some extra assumptions such as playing top players for longer, extra intensity in both offence and defence when compared to any previous games.
If anyone has any inputs please reach out, this is an active pet project of mine, so I would be grateful to hear any comments from more experienced data scientists or those with an interest in basketball!
This article has been inspiring to write, and I am actively looking at applying similar models to other sports that are in action during Lockdown. I am currently looking for sports data on all sports. I am also job searching for an immediate start, so if you would like to reach out for either of those reasons or another, please do so either through the comments, my Linkedin, or my email at carterbouley@gmail.com.