I Learned Machine Learning by Building an F1 Predictor in C#
I explore how I learned machine learning by building an F1 race prediction model in C# using ML.NET, OpenF1 data, and familiar software engineering principles.

I have always been very interested in AI and machine learning, but being interested doesn't mean you understand it. What I really wanted was to go beyond just reading about machine learning and instead build something myself.
The only issue was this.
I didn't know Python.
When I first started looking into machine learning, the same advice kept coming up: learn Python. And that was reasonable, since Python is widely used in the machine-learning ecosystem and has a huge number of libraries, frameworks, tutorials, and community resources.
I already knew how to write software.
I am most at ease with C# and have also worked with Java and TypeScript. To me, learning a completely new programming language while trying to grasp machine-learning ideas seemed like excessive mental effort. If you are a developer who is thinking about getting into machine learning, you should begin with the language that you are most familiar with, whatever that may be, such as C#, Java, TypeScript, or any other language. There's no need to use Python when you first start exploring machine learning. Using the language of your choice makes the experience more accessible and gives you the confidence to focus on understanding the new concepts rather than getting bogged down in syntax.
The problem wasn't that Python seemed difficult. The issue was that learning Python and learning machine learning together meant learning two things at once.
So I chose to try something else.
Rather than learning Python first and then moving on to machine learning, I wanted to see if I could learn machine learning by building a project in C#.
The aim was never to show that C# is better than Python, since that doesn't interest me much. Python is a great language for machine learning, and its ecosystem is hard to overlook.
The goal was much simpler:
Remove an unnecessary obstacle to learning something new by using what I already understand.
This let me focus on what I really wanted to learn: classification, features, training data, probability, model evaluation, prediction, data preprocessing, overfitting, and imbalanced datasets.
Why Formula 1?
I decided to build an F1 prediction model.
I wanted to learn machine learning to some extent, but mainly because I'm passionate about Formula 1.
Because the project was based on something that I already cared about, I had questions which I really wanted the model to answer.
- Can I predict the probability that a driver will finish on the podium?
- Can I estimate the probability that they finish in the points?
- Could I later on use those predictions to investigate the results of championship races?
That was a big enough problem to begin with.
The outcome is F1 Predictor, a web application that takes in Formula 1 data, extracts features, trains machine-learning models, and then uses the models to generate race predictions.
The dashboard is only the visible part of the project; the more interesting aspect is what happens beneath it.
From Raw Data to a Prediction
At a high level, the application follows a fairly straightforward pipeline:
OpenF1
│
▼
Data ingestion
│
▼
Feature engineering
│
▼
Model training
│
▼
Model evaluation
│
▼
Race predictions
Building the Application
Each stage of the pipeline is implemented in C#.
For the application architecture, I chose CQRS with ASP.NET Core Minimal APIs. CQRS clearly separates operations that change state from operations that retrieve data, while Minimal APIs provide a lightweight way to expose those operations without unnecessary framework overhead.
The application uses PostgreSQL for persistence, hosted through Neon. This gives me a managed PostgreSQL database for storing both the raw data retrieved from OpenF1 and the feature data generated during preprocessing.
Interestingly, this is not the first project where I have used this approach. I have become sufficiently fond of the combination of C#, Clean Architecture, CQRS, and Minimal APIs that I actually built a tool to generate the foundations of applications using these patterns.
That tool is Clean Architecture Generator.
The F1 Predictor was another opportunity to put those architectural ideas into practice, but this time in a machine-learning application rather than a more traditional business application.
What's interesting is what happens at every stage.
1. OpenF1, Data Ingestion
The first problem was obtaining the data.
The F1 Predictor obtains its historical Formula 1 data from the OpenF1 API. Its data ingestion pipeline goes through each meeting and session, gathering information including race results, the starting grids, the drivers, the pit stops, and the weather.
One point is that the starting grid must be based on the qualifying session, not the race session itself.
For a sprint weekend, you should use the qualifying session that corresponds to the sprint, whereas a normal Grand Prix makes use of the qualifying session linked to the race.
It may seem like a minor implementation detail, but machine learning projects have plenty of them. A model can still give poor results even if it is perfectly implemented because the data fed to it does not represent the problem which you believe it should represent.
The ingestion process is also designed to be idempotent, so sessions that have already been ingested do not need to be downloaded again unless they are specifically forced to be scheduled; races are rechecked so they can automatically change from scheduled to completed when the event has taken place.
Sprints are also used to earn championship points, but they are clearly indicated so they can be left out of the downstream feature dataset used to predict the race.
The importance of that distinction is that a sprint has a grid and a finishing order just like a Grand Prix; if the explicit exclusion is not included, it could become another training example even though it involves a fundamentally different prediction problem.
That was one of the earliest lessons I learned about machine learning, well before the model was developed.
2. Feature Engineering
When the raw data was available, my next step was to convert it into a form that the model could use.
The application reconstructs the DriverRaceFeature dataset from the raw tables and produces one feature row for each classified driver in each Grand Prix.
The features I started with are:
- Starting grid position — the position from which the driver starts the race on the grid.
- Constructor — the team involved in the race is represented as a categorical variable in order to show differences in team performance.
- Qualifying position — the position that the driver obtained during the qualifying session.
- Laps completed this season — the number of laps the driver has finished this season up to the present race, a way of measuring the driver's experience and reliability.
- Rain at the start of the race — whether it was raining at the beginning of the race, because weather conditions have a significant impact on the outcome of the race.
Together with those features, each row includes two labels:
- Podium — whether the driver finished in the top three
- PointsFinish — whether the driver finished in the top ten
This was perhaps the most educational section of the project for me.
It's easy for someone who is new to machine learning to regard features as just "the data you give the model."
They are far more than that.
That is what feature engineering consists of — deciding on the mathematical way in which the real world should be represented.
For instance, I had to work out the best way to show a pit-lane start, determine what action to take when qualifying data was not available, and decide how to indicate rainfall. Even such small decisions as these end up becoming assumptions built into the model.
The model is currently deliberately based on a fairly small number of features because I wanted it to be simple enough for people to understand, rather than including dozens of variables and hoping that would lead to a good result.
There is also plenty of scope for improving the model at a later stage.
3. Training the Models
The application uses ML.NET to train two individual binary classification models.
One predicts:
What are the chances that this driver will finish in the top three?
The other predicts:
What are the chances that this driver will finish in the points?
The two models have different labels, even though they use the same five features.
The training pipeline is deliberately simple:
Five features
│
▼
Concatenate
│
▼
NormalizeMinMax
│
▼
SDCA Logistic Regression
│
▼
Trained model
ML.NET's SDCA logistic regression algorithm was a suitable starting point because its basic principles are easier for me to understand than moving straight into a more complex model.
I also use a constant random seed so the training process can be reproduced.
Another choice is important since I do not use the whole season when assessing the model.
I offer the most recently finished race entirely.
All races before that one can be used for training, while the race left out serves as an example I can compare with reality.
That gives me two distinct ways to evaluate something.
4. Evaluating the Model
The initial evaluation takes place during model training.
The classifier is assessed by ML.NET using a range of metrics including:
- AUC (Area Under the ROC Curve)
- F1 Score
This matters because accuracy can be misleading for the podium classifier.
Only a small number of drivers end up on the podium; a model could thus achieve an apparently high degree of accuracy by stating that most drivers won't finish in the top three.
That led to an important lesson:
The metric that you select is part of the machine-learning problem.
A prediction from the model isn't sufficient; you must determine whether it makes sense.
The second one is the season holdout.
The trained models are tested on the race intentionally left out of training, and the application provides the predicted probabilities alongside the actual finishing order.
What I get as a result is something much more useful than a training metric.
It lets me ask:
How accurate was the model's prediction regarding a race it had never observed?
I never understood that distinction before I had built the project.
5. Converting a Model into a Prediction
Once the models have been trained, the application can take a driver's feature set and produce two probabilities:
Driver
│
├── Podium probability
│
└── Points-finish probability
The dashboard ranks drivers based on those probabilities.
The models are saved in .zip format and loaded by the prediction service; the predictor can also detect when a model file has changed, so it can identify newly trained models without restarting the application.
By this stage, the project began to seem less like a machine-learning experiment and more like an actual software system.
I was no longer merely training a model.
I was developing an application based on one.
The Problem With a Prediction
A basic lesson from this project was that constructing a model involves more than simply producing a number.
You have to determine whether that figure actually conveys any useful information.
The usefulness of a machine-learning model depends entirely on the data, the features, the assumptions, the evaluation strategy, and the way the problem is defined.
What struck me as particularly interesting was that it brought machine learning back to something familiar from my experience as a software engineer: garbage in, garbage out.
The point is that, in machine learning, it's generally harder to identify exactly where the garbage got into the system.
A probability like 72% podium probability seems too exact.
It doesn't imply precision.
It is the result of a model working on a certain set of assumptions and based on the data used for its training.
That distinction has changed how I view the predictions shown on the dashboard.
Going Beyond Race Predictions
The project has also moved beyond its original aim.
The initial aim was to predict which cars would finish in the top three; the programme has since been expanded to predict point scores and now features championship projections.
For the championship forecast, I take a different approach from the race classifiers.
A Plackett–Luce model is used to estimate relative driver strength, after which these estimates are input into a Monte Carlo simulation consisting of 10,000 runs, and this simulation gives the probabilities of winning both the Drivers' and Constructors' championships.
What was especially interesting was that it brought in another machine-learning and statistical concept which I hadn't originally intended to look into: simulation.
Rather than trying to predict one specific result for the whole championship, I can simulate some possible seasons and then examine the distribution of outcomes.
This is much more in keeping with how uncertainty really works.
What's Next?
The model currently has a simple design, and I don't expect it to stay that way.
A further experiment that I would like to carry out is using ML.NET AutoML. Instead of selecting a single algorithm and assuming it is the most suitable, AutoML will try various methods and assess their performance. I want to give you a straightforward way to get started with AutoML, either by using the Model Builder in Visual Studio or by following simple code examples. I recommend taking one of your own datasets and having AutoML recommend the best algorithms and parameters — a practical way to explore machine learning without spending much time on manual model tuning.
That will provide an interesting comparison between the one I chose manually and those selected by automated experimentation.
I'd also like to look into creating and training a custom machine-learning model using Microsoft's tools in Visual Studio.
There is still a great deal I would like to learn, such as improving feature engineering, choosing the right model, tuning hyperparameters, handling extra variables, and understanding how the model behaves as the data itself changes.
That is the main lesson learned from the project.
I began creating F1 Predictor to gain machine learning experience.
I found out that the machine-learning model is only one element of the problem.
The data matters.
The features matter.
The evaluation strategy matters.
The assumptions matter.
Above all else, the questions you ask matter.
I never began this project because I knew machine learning.
I started it because I didn't.
And that was a good reason to build something.