Predicting UFC Fights With Machine Learning | by Charles Pierse | Towards Data Science

TL;DR: check out the predictor for yourself and see how it performs for an upcoming UFC fight card or check out the code on github
As a fan of MMA I often find myself trying to predict the outcome of fights on an upcoming fight card. The problem is that fighting by its nature can be very unpredictable. More so than even boxing, the outcome of an MMA fight can change in a split second, but of course that’s what makes it so interesting.
All the same I wondered if there was a way to apply modern machine learning techniques to historical fight data and see how a model would perform on new fights.
Of course like any ML project I needed data to work with. Fortunately I had a good idea of where I might find some having often visited www.ufcstats.com the day after a fight card to get the stats breakdown for a fight.
So now that I had my data source and a couple of ideas of how I could implement the model I wondered if anyone else had built a project like this and came across this post by Yuan Tian detailing his process for building a fight predictor.
Yuan’s post was very detailed and insightful and my methodology for scraping the stats page was largely inspired by how he structured his scrapers. Thanks Yuan!
In Yuan’s predictor he used fight data that would be available before a fight with stats that I called “static fight stats” e.g. age, reach, weight, W/L record etc. However, the UFC stats page also detailed fight specific stats for historical bouts such as strikes thrown, takedowns , submission attempts, and guard passes.
As there weren’t an awful lot of data points on the UFC stats page to begin with I felt discarding these “dynamic fight stats” would be a shame. So I decided to build the predictor as a two way predictive system:
The first model uses the static stats as the independent variables for fighter 1 and fighter 2 and then predicts the dependent variables, the dynamic fight stats.( A multi target regression problem).
We then pass the static and dynamic fight stats to an overall winner model that predicts whether fighter 1 or fighter 2 is the winner. (A binary classification problem).
The scrapers for this project are broken down into two Scrapy spiders that crawl two sections of UFC Fight Stats:
Bout Scraper
Fighter Scraper
The Bout scrapers data object is broken down as the following Scrapy Item:
class BoutScraperItem(Item):                           event_name = Field()                           event_date = Field()                           event_attendance = Field()                          fighter1 = Field()                          fighter2 = Field()                           str_stat_f1 = Field()                           str_stat_f2 = Field()                           td_stat_f1 = Field()                           td_stat_f2 = Field()                           sub_stat_f1 = Field()                           sub_stat_f2 = Field()                           pass_stat_f1 = Field()                           pass_stat_f2 = Field()                           weight_class = Field()                           win_method_type = Field()                           win_method_finish = Field()                           round_ = Field()                           time = Field()                           winner = Field()
The Bout spider crawls the events section of UFC Fight Stats and records bout specific stats such as str_stat_f1 (striking stats for fighter 1) for each fighter, many of these will become our dependent variables for the multi target regression problem. Fighter1 and Fighter2 are the names of the fighters involved in a bout and we join the fighter scraper data on these values.
The Fighter scraper data object is broken down as :
class FightScraperItem(Item):                           fighter_name = Field()                           fighter_record = Field()                           height = Field()                           weight = Field()                           reach = Field()                          stance = Field()                           date_of_birth = Field()                           slpm = Field()  # strikes landed per min stat                           td_avg = Field()  # takedown average                           strike_acc = Field()  # striking accuracy                           td_acc = Field()  # takedown accuracy                           sapm = Field()  # strikes absorbed per minute                           td_def = Field()  # takedown defence                           strike_def = Field()  # striking defence                           sub_avg = Field()  # submission average
Each item here is individually scraped from the fighter stats page which details career stats for every fighter involved in a UFC bout (although data from the earlier days of the UFC is less populated).
I save both of these data objects as tables into a SQLite database and have a small script for generating a combined fight-bouts CSV file where each row is a bout with fighter1 and fighter2’s individual fighter stats appended. This CSV as well as a CSV representation of just the fighters data object will be the two files we use to build both models.
If you’re interested in seeing the actual scraping logic for each spider take a look at the here.
As is the case with most ML projects of this kind the real work is done in the preprocessing step and how the data is prepared for the modelling stage.
This aspect of the project went through numerous iterations. I began in a Jupyter notebook but as the complexity increased and I decided that I wanted to implement it as front end application I changed the design over to classes that contain shared preprocessing methods for both the winner predictor and the stats predictor as well as task specific preprocessing methods.
A lot of the preprocessing is dedicated to cleaning up values that are missing or in the wrong the format e.g. heights being represented as strings, parsing string fighter records, and getting a fighter’s age at the time of fight.
One of the trickiest aspects of preprocessing was changing the ordering of fighter1 and fighter2’s stats. The reason I had to shuffle this ordering was as a result of the UFC’s bout data always being ordered such that fighter1 is the winner so I had to figure out a way to randomly select half the dataset and swap fighter1 and fighter2’s stats positions with one another so that the final dataset is neatly broken up such that 50% of fighter1 are the winner, and 50% fighter2 are winners. I’d love to tidy up this logic going forward and if anyone has any suggestions on improvements I’d love to hear ‘em.
Another important feature for the processor classes is that they had to be able to handle data processing differently in production as in a production situation the dataframe isn’t neatly constructed like it is for the training step.
In production fighter stats have to be predicted ad hoc and then inserted into the final dataframe for winner prediction. My solution for this was to create additional child classes for the processors that could handle production specific cases.
Both models are built using Keras, with relatively straight forward architectures comprised of only a single fully connected hidden layer for each model. I took a mostly heuristic approach to finding the optimal parameters for both but I’m eager to use newer tools optimization tools like Keras Tuner to fine tune its performance.
Stats Model
The stats model takes a scaled numerical representation of all the stats that are available for both fighters before a fight and it’s output layer is made up of 8 fight specific stats:
output_cols = [                               'pass_stat_f1', 'pass_stat_f2', 'str_stat_f1', 'str_stat_f2',                               'sub_stat_f1', 'sub_stat_f2', 'td_stat_f1', 'td_stat_f2'                           ]
As this is a regression problem the final layer in the stats model has a linear activation (the default activation). For loss I used ‘mse’ with R2 as the models' metric to evaluate performance.
The R2 for this model comes out usually at about 0.63–64 on the validation set, which isn’t too bad, but it’s worth noting that R2 will always increase as an additional predictor (output) is added and some of this could be attributed to random noise that happens to align by chance. To counter this I’ll be adding adjusted R2 to penalize the model for every new output added in a new version.
Winner Prediction Model
Like the stats model this follows a basic single fully connected hidden layer architecture. The dependent variable is either a 0 (fighter1) or 1 (fighter2) so I add a simple sigmoid activation on the final layer to predict one of these outcomes.
This model’s accuracy usually comes out at about 86% although it must be stressed that this accuracy is achieved with perfect information of the dynamic fight stats which in production are predicted values and thus not perfect information, so there is a huge contingency on the predictive power of the stats prediction model.
As I wanted to serve the model and it’s predictions as a front end service I needed to build some API endpoints to access the model(s) and the list of fighters that predictions can be made for.
Using Flask this was relatively straight forward. There are two main endpoints, one serves a list of fighter names for generating predictions and the other queues up the prediction pipeline and returns the result.
The pipeline processes a dataframe for each fighter and then concatenates both into the correct shape and order for stats prediction. Once the stats prediction values are returned they are added as columns with their respective names and positions to the dataframe and passsed to the winner prediction model returning a result which is associated to either fighter 1 or 2.
All of the API’s functionality is then called by a very simple React front end that provides the user with two searchable dropdowns and a results display that shows the predicted winner and the model’s confidence in its prediction.
Setting this up in React was easy and I find React to generally be a joy to work with ( I ❤ hooks).
There’s an awful lot I’d like to and will be adding to the front end such as a table showing a users’ history of predictions, and a infograph showing basic fighter stats.
Hopefully this has been informative for both ML enthusiasts and/or fans of MMA in general, as I’ve mentioned there’s a lot of features I’d like to add this project in the future, and I’d love to hear your thoughts. Thanks!
P.S. Maybe don’t go betting the house down on all of the model’s predicted outcomes just yet.