Using Machine Learning to Predict if a F1 Driver Will Score | by Caio Santos Pedroso | Towards Data Science

I’m a huge fan of Formula One, all the technology, the mechanics, how fast everything evolves, the amount of investment in research, all these characteristics make me being fascinated about this world, and recently surfing the web I’ve found this dataset in kaggle with compiled data from 1950 to 2020, so I decide to play around with it and see if I was able to find some insights and in the end create a simple model to predict something related to podium, position, or if a driver will or not score.
A lot of people say these days that Formula One is decided by the car features more than the driver itself, and it is partially true, the car is really important to the race result, but the driver skills and the strategy that the team and the driver create before the race given the circumstances, which kind of tire, how many pit stops, if it’s warm or cold, if it’s gonna rain, all of these characteristics matter in the end, and the driver is a really important piece of this equation to feel and use his experience to determine the better strategy to the race, and to improve even more the efficacy of this decision, we see a lot of Machine Learning involved and we probably gonna see more and more as data being collected by the teams is increasing.
“Races are won at the track. Championships are won at the factory.” — Mercedes-AMG Petronas Motorsport
The dataset was distributed in different files, one for drivers, one for the constructor, one for race results, and so on, to get more information about all the available data and fields, please check the kaggle page, the link is above. The first thing I did was to merge all these datasets to create one with all the necessary information I needed. The only dataset that required some transformation was the pit stops one, every pit stop was a row in the dataset, and I wanted to know how many pit stops one pilot did at a race, so I had to group by racerId using count aggregation.
df_pitstops_gb = df_pitstops.groupby([“driverId”, “raceId”]).count().reset_index()
Then I was able to merge the pit stops with the other datasets.
With the base dataset ready, I started to verify the fields and how the data was distributed in then. The first thing I did was to check how driver’s positions in the race were distributed, the result was the chart below.
The two things that gave me a warning flag were the “\N”, which means that the driver did not finish the race, and, besides today’s grid have only 20 cars, there were times where more cars were allowed, at least 24 as per our data. With that information, I was ready to start the Data Preparation part.
By curiosity, I tried to identify what causes most drivers to not finish the race and where, and the result was quite predictable, the problems were related to one of the most important parts of the car, the engine, and the circuit, the well-known of every F1 fan, Monaco.
Knowing a lot about the data, this part was kinda easy. The first thing I did was to decide what to do with the drivers that did not finish a race, the “\N”, as we don’t have enough information about the specifics of the strategy that could cause an engine failure, the most assertive thing to do was to remove this rows.
df_base = df_base[df_base.position == ‘\\N’]
As already explained, in recent years, there were more than 20 cars in the grid, but now there are only 20 cars, to set the “bounds” of the prediction and to be fair with the data, I’ve subtracted the driver position by the difference between the position and 20.
df_base['position'] = df_base['position'].apply(lambda x: x if x <= 20 else x-(x-20))
One of the most critical things was that the data was not that well balanced, in the “original” data we had a lot of drivers in the first positions of the grid and a few in the end, mostly because of the ones who did not finish the race. Having that in mind in combination with, what I judged fewer data to predict the position of the race, I realized that I was only able to predict if the driver would score in the race or not, i.e. the drive would finish in the first 10 positions, so one more column was created, our target, 1 if the driver will score and 0 if not.
df_base['score'] = df_base['position'].apply(lambda x: 1 if x in range(1,11) else 0)
This new way to see the target, made the dataset to be fairly distributed, 40% in class 1 and 60% in class 0.
Then I separated the columns I found useful for the prediction
circuitname: The name of the circuit where the race will happen
grid: The start position in the grid
stop: The number of pist stops the team is planning to do
constructorname: The name of the constructor of the car
The fields with “name” were text ones, to apply a Machine Learning algorithm, I used the “get_dummies” function from pandas, the idea was to apply the one-hot encoding technique and having them as a number.
X = df_base[["circuitname", "grid", "stop", "constructorname"]]y = df_base["podium"]X = pd.get_dummies(X)
To predict our class I decided to test two different models, the Random Forest and XGBoost, the first one was because this problem looks like one that would fit a Decision Tree problem. I went directly to Random Forest as overkill to solve the problem, and the XGBoost was a very similar decision to the first one, besides the fact that is the algorithm that I use the most.
With the data already splited I decided to use the GridSearchCV for both algorithms to try different params and look for the best one, you can check the params and the result below.
params = {            'n_estimators':[100, 300, 500, 800, 1200],            'max_depth': [5, 8, 15, 25, 30],            'min_samples_split': [2, 5, 10, 15, 100],            'min_samples_leaf': [1, 2, 5, 10]         }clf = GridSearchCV(estimator=RandomForestClassifier(), param_grid=params)clf.fit(X_train, y_train)print(clf.best_params_)
max_depth=25, min_samples_leaf=1, min_samples_split=15, n_estimators=1200
params = {            'min_child_weight': [1, 3, 5, 7],            'gamma': [0.3, 0.4, 0.5, 0.7, 1, 1.5, 2, 5],            'subsample': [0.6, 0.8, 1.0],            'colsample_bytree': [0.3, 0.4, 0.5, 0.6, 0.8, 1.0],            'max_depth': [5, 8, 15, 25, 30],            'eval_metric': ['error']        }clf = GridSearchCV(estimator=XGBClassifier(), param_grid=params)clf.fit(X_train, y_train)print(clf.best_params_)
colsample_bytree=1.0,gamma=2, max_depth=5, min_child_weight=1, subsample=1.0
The results of both algorithms at their “state of the art” for this problem were very similar as you check below.
As I don’t want my driver to be misguided by the algorithm saying that he will score, but his team strategy was not that good, I decided to use Precision as the main metric, penalizing the False Positives, and the weighted version of the metric, as I don’t have a big imbalanced dataset. so the chosen one was Random Forest, but as you can see, the difference is just 0.01.
If you have any questions regarding the article’s content, please feel free to reach out to me. You can find me on Linkedin.