Predict Football Outcomes with ML
Train a Random Forest model on 226K matches and generate AI match previews.
Introduction
30 Second Summary
Every football fan has an opinion on how the weekend's matches will play out. But what if you could back up those gut feelings with 25 years of real match data and a trained machine learning model?
In this project, you will build a football match prediction system that learns from 226,000+ real matches across 42 leagues, predicts outcomes using a Random Forest classifier, and generates pundit-style match previews using the OpenAI API.
What You'll Build
Picture this: you feed in any upcoming match's pre-game stats and your system returns a predicted result, a confidence percentage, and a natural-language preview explaining why the model made that call.
By the end of this project, you'll have:
- A trained machine learning model that predicts match outcomes (Home Win, Draw, or Away Win) from real football data, complete with confidence scores for each prediction.
- A set of engineered features (Elo advantage, form momentum, implied probabilities) that demonstrably improve prediction accuracy over raw inputs.
- A working script that generates AI match previews explaining each prediction in engaging, pundit-style English.
- Secret Mission: A confidence-filtered batch system that only generates previews for high-confidence predictions and produces an entire matchday report.
Are there any prerequisites?
No prior machine learning experience is needed. This guide walks you through every step from installing Python packages to training your model.
You will need Python 3.11.9 installed, an IDE or text editor, and an OpenAI API key (the project costs only a few cents in API usage).
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up Your Environment
Your prediction system needs three things before it can process 226,000 football matches: libraries to handle data and train models, an AI service to generate natural-language previews, and the match dataset itself.
In this step, you'll install the required Python packages, configure your OpenAI API key, and download 25 years of real football match data from 42 leagues worldwide.
In this step, get ready to:
- Install pandas, scikit-learn, and the OpenAI SDK.
- Configure your OpenAI API key as an environment variable.
- Download the football match dataset from Kaggle.
Install the required Python packages
- Move to your Desktop and create a project folder by running these commands:
cd ~/Desktop
mkdir football-predictor
cd football-predictor
- Install the three packages your project needs by running this command:
pip install pandas==3.0.3 scikit-learn==1.9.0 openai==2.46.0
What do these packages do?
- pandas loads and manipulates tabular data. You'll use it to read the 226K-row CSV and compute new columns.
- scikit-learn provides machine learning tools. You'll use it to train a Random Forest classifier and measure accuracy.
- openai is the official Python SDK for the OpenAI API. You'll use it to generate natural-language match previews.
You should see "Successfully installed" messages for all three packages in your terminal output.
Seeing a permissions error?
- On macOS/Linux, try adding --user to the end of the pip command to install to your user directory.
- Make sure you're using the correct Python. Run python3 --version to confirm you're on 3.11.9.
- If you see "No matching distribution found," check that you typed the version numbers exactly as shown above.
Still stuck? Help me fix a pip install error for pandas, scikit-learn, or openai.
Set your OpenAI API key
The OpenAI API needs an API key to authenticate your requests. You'll store it as an environment variable so your Python code can access it automatically without hardcoding secrets.
- Set your API key as an environment variable by running the command for your operating system:
macOS
- Run this command in your terminal, replacing the placeholder with your actual API key:
export OPENAI_API_KEY="your-api-key-here"
How long does this last?
This export command sets the variable for your current terminal session only. If you close and reopen the terminal, you'll need to run it again. To make it permanent, add the same line to your ~/.zshrc file.
Windows
- Run this command in PowerShell, replacing the placeholder with your actual API key:
setx OPENAI_API_KEY "your-api-key-here"
- Close and reopen your terminal for the variable to take effect.
- Navigate back to your project folder by running:
cd ~/Desktop/football-predictor
Why do I need to reopen the terminal?
The setx command saves the variable permanently to Windows, but existing terminal sessions don't pick it up until restarted.
Not sure where to find your API key?
Your API key is available in the OpenAI dashboard under API keys. If you haven't created one yet, click Create new secret key and copy the value immediately. OpenAI only shows the full key once.
Need more help? Help me find or create my OpenAI API key.
Download the football match dataset
The dataset you'll work with contains approximately 226,000 real football match records from 2000 to 2025, spanning 42 leagues including the Premier League, La Liga, and Bundesliga.
- Go to the Kaggle dataset page: Club Football Match Data (2000-2025).
- Click the Download button near the top of the page. You may need to sign in with a free Kaggle account.
- Extract the downloaded ZIP file.
- Find the Matches.csv file and move it into your football-predictor folder on your Desktop.
- Confirm the file is in the right place by running:
ls
You should see Matches.csv listed in the output.
Don't see Matches.csv?
- Make sure you extracted the ZIP file before moving. The CSV is inside the archive, not the ZIP itself.
- Double check that you moved Matches.csv (not a subfolder) directly into football-predictor.
- Confirm your terminal is in the correct folder by running pwd. It should end with football-predictor.
Still having trouble? Help me get Matches.csv into my project folder.
Before you run the final check, what do you think will happen if one of the three packages failed to install?
- Verify that all three libraries are importable by running:
python -c "import pandas; import sklearn; import openai; print('All libraries ready')"
You should see All libraries ready printed with no errors.
Seeing ModuleNotFoundError?
- If the error names a specific package, re-run the pip install command for that package alone.
- Make sure you're running the same python that pip installed to. Try python3 instead of python if the command fails.
Need help? Help me fix a ModuleNotFoundError after installing packages with pip.
Your environment is ready. Next up, you'll load 226,000 rows of real match data and explore what Premier League, La Liga, and Bundesliga results actually look like in a dataframe.
Load and Explore the Match Data
Your environment is set up with all the libraries you need. Now it's time to meet the data you'll train your model on.
Before building any model, you need to understand what 226,000 rows of football data actually look like. This exploration phase reveals what's missing, what patterns exist, and shows you real Premier League matches flowing through your terminal.
In this step, get ready to:
- Load 226,000+ match records into a pandas DataFrame and inspect the shape and columns.
- Explore data quality by checking missing values and the target variable distribution.
- Print summary statistics to understand the ranges of key numeric features.
Create the script and load the dataset
Every data science project starts by loading the raw data and taking a first look. You'll create a single Python file that will grow throughout this project.
- Create a new file called football_predictor.py in your project folder (the same folder where Matches.csv lives).
- Add the following code to load the dataset and print basic information:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report
from openai import OpenAI
# --- Load and Explore ---
df = pd.read_csv("Matches.csv")
print(f"Dataset shape: {df.shape}")
print(f"\nFirst 5 rows:")
print(df.head())
print(f"\nData types:")
print(df.dtypes)
What does this code do?
- The imports at the top bring in all the libraries you'll use across the entire project. You won't use sklearn or openai in this step, but setting them up now avoids revisiting the top of the file later.
- pd.read_csv("Matches.csv") reads the entire CSV file into a DataFrame called df.
- df.shape tells you how many rows and columns exist. df.head() shows the first 5 rows so you can see actual match data.
- df.dtypes reveals whether each column is numeric, text, or another type.
- Save football_predictor.py.
- Run the script by executing this command in your terminal:
python football_predictor.py
You should see the dataset shape printed as approximately (226000, 42), followed by the first 5 rows of match data and a list of column types.
Seeing a FileNotFoundError?
Make sure Matches.csv is in the same folder as football_predictor.py. The file name is case-sensitive.
Also confirm your terminal is in the correct directory by running ls (macOS/Linux) or dir (Windows) and checking that both files appear.
help me fix a FileNotFoundError when loading Matches.csv with pandas
Explore data quality and the target variable
Real-world datasets always have gaps. Knowing where the missing values are tells you which columns are safe to rely on for your model.
- Add the following code below your existing print(df.dtypes) line in football_predictor.py:
print(f"\nMissing values per column:")
print(df.isnull().sum())
print(f"\nResult distribution:")
print(df["FTResult"].value_counts())
# Show some Premier League matches
premier_league = df[df["Division"] == "E0"].head(10)
print(f"\nSample Premier League matches:")
print(premier_league[["MatchDate", "HomeTeam", "AwayTeam", "FTHome", "FTAway", "FTResult"]])
What does this code do?
- df.isnull().sum() counts how many missing values each column has. Columns with lots of gaps may be unreliable for training.
- df["FTResult"].value_counts() shows how many Home wins (H), Draws (D), and Away wins (A) exist in the dataset. This is your target variable that the model will learn to predict.
- The Premier League filter uses Division == "E0" to show familiar English team names so the data feels real and tangible.
- Save football_predictor.py and run the script again:
python football_predictor.py
Scroll to the new output at the bottom. You should see FTResult counts showing H (Home win) is most common, followed by A (Away win), then D (Draw).
Why are Draws the least common?
In football, home advantage is real. Teams win at home more often than they draw or lose. This class imbalance is exactly why predicting Draws is the hardest challenge for any model.
Seeing a KeyError for FTResult or Division?
Double-check that you downloaded Matches.csv (not one of the other CSV files from the dataset). Column names are case-sensitive, so ensure they match exactly as written.
help me debug a KeyError when accessing FTResult or Division in my football dataset
Print summary statistics for key features
Summary statistics reveal the range and spread of numeric columns. This tells you whether features like Elo ratings and form points are on similar scales or wildly different.
- Add this code at the bottom of football_predictor.py, below the Premier League print statement:
# Summary statistics
print(f"\nSummary statistics:")
print(df[["HomeElo", "AwayElo", "Form3Home", "Form3Away", "Form5Home", "Form5Away"]].describe())
What does .describe() show you?
The .describe() method prints the count, mean, standard deviation, min, max, and percentiles for each column. You'll see that Elo ratings range roughly from 1000 to 2000+, while form points are on a much smaller scale (0-9 for 3-match form, 0-15 for 5-match form).
- Save football_predictor.py.
Before you run the script this time, think about what you expect to see. Do you think Elo ratings and form points will be on similar scales, or very different ones?
- Run the script one more time:
python football_predictor.py
At the bottom of the output, you'll see the summary statistics table. Elo ratings sit in the 1000-2000 range while form columns stay between 0 and 15. They're on very different scales.
Why does scale matter?
When features have different scales, some machine learning algorithms can be thrown off. Random Forest (the algorithm you'll use) handles mixed scales gracefully, which is one reason it's a great starting point.
✔️ Awesome, I've got everything!
Great. Double check you've saved football_predictor.py and that running it prints the dataset shape, result distribution, Premier League matches, and summary statistics.
ⓧ I'd like to double check the full code
Here is the complete football_predictor.py at this point:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report
from openai import OpenAI
# --- Load and Explore ---
df = pd.read_csv("Matches.csv")
print(f"Dataset shape: {df.shape}")
print(f"\nFirst 5 rows:")
print(df.head())
print(f"\nData types:")
print(df.dtypes)
print(f"\nMissing values per column:")
print(df.isnull().sum())
print(f"\nResult distribution:")
print(df["FTResult"].value_counts())
# Show some Premier League matches
premier_league = df[df["Division"] == "E0"].head(10)
print(f"\nSample Premier League matches:")
print(premier_league[["MatchDate", "HomeTeam", "AwayTeam", "FTHome", "FTAway", "FTResult"]])
# Summary statistics
print(f"\nSummary statistics:")
print(df[["HomeElo", "AwayElo", "Form3Home", "Form3Away", "Form5Home", "Form5Away"]].describe())
You now have a clear picture of your dataset. Next up, you'll use this data to train a baseline machine learning model and see how well it can predict match outcomes with just the raw features.
Train a Baseline Model
In the last step, you explored 226,000 rows of football data and got a feel for the features available. Now it's time to put those features to work and see how well a machine learning model can predict match outcomes.
You'll train a Random Forest classifier using only the raw Elo ratings and form data. This gives you a starting point to measure future improvements against.
In this step, get ready to:
- Select features and prepare clean training data.
- Train a Random Forest classifier on 80% of the data.
- Evaluate baseline accuracy and identify where the model struggles.
Select your features and prepare the data
Before training a model, you need to decide which columns are inputs (features) and which column is the output (target). You also need to remove any rows with missing values so the model has clean data to learn from.
- In football_predictor.py, scroll to the bottom of your file (after the summary statistics print).
- Add the following code to define your baseline feature set and clean the data:
# --- Baseline Model ---
baseline_cols = ["HomeElo", "AwayElo", "Form3Home", "Form3Away", "Form5Home", "Form5Away"]
target = "FTResult"
df_baseline = df[baseline_cols + [target]].dropna()
print(f"\nRows after dropping missing values: {len(df_baseline)}")
What does this code do?
- baseline_cols lists the six raw features: Elo ratings for each team and their recent form (3-match and 5-match).
- target is set to FTResult which contains H (Home Win), D (Draw), or A (Away Win).
- dropna() removes any rows with missing values in these columns so the model only trains on complete data.
- Save football_predictor.py and run it:
python football_predictor.py
At the bottom of the output (after all the exploration prints), you should see the row count. It will be slightly less than 226,000 because some rows had missing Elo or form values.
Seeing a KeyError?
Make sure the column names in baseline_cols match exactly what your dataset uses. Column names are case-sensitive.
Check that your new code is below the existing exploration code, not replacing it.
Split the data and train the classifier
Machine learning models need to be tested on data they have never seen during training. You'll hold back 20% of the data for testing and train on the remaining 80%.
- Add this code below the row count print statement to split the data and train your model:
X_base = df_baseline[baseline_cols]
y_base = df_baseline[target]
X_train_base, X_test_base, y_train_base, y_test_base = train_test_split(
X_base, y_base, test_size=0.2, random_state=42
)
baseline_model = RandomForestClassifier(n_estimators=100, random_state=42)
baseline_model.fit(X_train_base, y_train_base)
What does this code do?
- X_base holds the feature columns (inputs) and y_base holds the target column (what we want to predict).
- train_test_split randomly splits the data into training (80%) and test (20%) sets. The random_state=42 ensures you get the same split every time you run.
- RandomForestClassifier creates an ensemble of 100 decision trees that each vote on the predicted result. The .fit() call trains all 100 trees on the training data.
Evaluate the baseline model
The model is trained. Time to see how well it actually predicts football results on data it has never seen before.
- Add the evaluation code below the .fit() line:
baseline_preds = baseline_model.predict(X_test_base)
baseline_accuracy = accuracy_score(y_test_base, baseline_preds)
print(f"\n{'='*50}")
print(f"BASELINE MODEL ACCURACY: {baseline_accuracy:.4f}")
print(f"{'='*50}")
print(classification_report(y_test_base, baseline_preds))
What does this code do?
- predict() asks the trained model to predict H, D, or A for every match in the test set.
- accuracy_score compares predictions to actual results and returns the percentage correct.
- classification_report breaks down accuracy per class (H, D, A) so you can see which outcomes the model handles well and which it struggles with.
Before you run this, think about it: with three possible outcomes (Home, Draw, Away), random guessing would get about 33% right. How much better do you think raw Elo ratings and form can do?
- Save football_predictor.py and run it:
python football_predictor.py
You should see a baseline accuracy of approximately 0.49 to 0.52. That is only about 50% correct. Look at the classification report closely.
Why is the accuracy so low?
Around 50% means the model is barely better than flipping a coin between two outcomes. Notice the report shows the model almost never predicts D (Draw) correctly. The recall for Draws is near zero.
This is the designed obstacle. Raw Elo numbers and form points alone do not capture the relative strength between two teams. Knowing that the home team has an Elo of 1600 is far less useful than knowing they are 200 Elo points above their opponent. That gap is exactly what you will fix in the next step with feature engineering.
Seeing a ValueError or accuracy of 0.0?
If you see a ValueError about inconsistent sample numbers, make sure X_base and y_base both come from df_baseline (after dropna), not from the original df.
Help me debug my Random Forest baseline model.
✔️ Awesome, I've got everything!
Great. Double check you have saved football_predictor.py with all three new sections: feature selection, training, and evaluation.
ⓧ I'd like to double check the full code
Your file should contain these sections in order. Scroll through your code and confirm each is present:
- The import block at the top (pandas, sklearn, openai).
- The Load and Explore section that prints shape, head, dtypes, missing values, result distribution, Premier League samples, and summary statistics.
- The Baseline Model section starting with the comment # --- Baseline Model --- that defines baseline_cols and target.
- The dropna and row count print.
- The train/test split with random_state=42 and the RandomForestClassifier with n_estimators=100.
- The prediction, accuracy print, and classification_report at the end.
A coin flip would get you 33% on a three-class problem. Your model only manages around 50%. In the next step, you will engineer new features that capture the relative strength between teams and watch that accuracy jump.
Engineer Features and Improve the Model
Your baseline model achieved roughly 50% accuracy. That is barely better than random guessing across three outcomes.
The problem is that raw Elo numbers and form points do not capture the relative strength between two teams. A home Elo of 1600 means nothing on its own. It only becomes predictive when you compare it to the opponent's Elo. In this step, you will engineer features that encode these relationships and watch the accuracy jump.
In this step, get ready to:
- Create derived features that encode team-vs-team differences, momentum, and bookmaker-implied probabilities.
- Train an improved Random Forest model with the engineered features.
- Compare the improved accuracy against the baseline and identify which features matter most.
Create derived features that capture relative strength
Feature engineering is the process of creating new columns from existing data that better represent the underlying patterns. Instead of feeding the model two separate Elo ratings, you will give it the difference between them.
- In football_predictor.py, scroll to the bottom of the file (after the baseline classification report prints).
- Add the following code to create three groups of engineered features:
# --- Feature Engineering ---
df_eng = df.copy()
# Elo-based: capture relative rating advantage
df_eng["EloDiff"] = df_eng["HomeElo"] - df_eng["AwayElo"]
df_eng["EloTotal"] = df_eng["HomeElo"] + df_eng["AwayElo"]
df_eng["EloAdvantage"] = df_eng["EloDiff"] / df_eng["EloTotal"]
# Form-based: capture momentum differences
df_eng["FormDiff3"] = df_eng["Form3Home"] - df_eng["Form3Away"]
df_eng["FormDiff5"] = df_eng["Form5Home"] - df_eng["Form5Away"]
df_eng["MomentumHome"] = df_eng["Form3Home"] - (df_eng["Form5Home"] - df_eng["Form3Home"])
df_eng["MomentumAway"] = df_eng["Form3Away"] - (df_eng["Form5Away"] - df_eng["Form3Away"])
df_eng["MomentumDiff"] = df_eng["MomentumHome"] - df_eng["MomentumAway"]
# Odds-based: convert bookmaker odds to implied probabilities
df_eng["ImpliedProbHome"] = 1 / df_eng["OddHome"]
df_eng["ImpliedProbDraw"] = 1 / df_eng["OddDraw"]
df_eng["ImpliedProbAway"] = 1 / df_eng["OddAway"]
What does this code do?
You are creating three families of features that all share one idea: relationships beat raw numbers.
- EloDiff and EloAdvantage express how much stronger the home team is relative to the away team, as an absolute gap and as a ratio.
- MomentumHome compares recent form (last 3 matches) against longer-term form (last 5). A team whose short-term form exceeds their longer run is trending upward.
- ImpliedProbHome converts decimal bookmaker odds into a probability (1 / odds). Bookmakers price markets based on huge datasets, so their implied probabilities are very informative.
- Save football_predictor.py.
Build the improved feature set and train the model
Now you need to define which columns the improved model will use, filter out rows with missing values, and train a more powerful Random Forest with 200 trees and a depth limit to prevent overfitting.
- Add the following code directly below the implied probability lines you just wrote:
engineered_cols = [
"HomeElo", "AwayElo", "EloDiff", "EloAdvantage",
"Form3Home", "Form3Away", "Form5Home", "Form5Away",
"FormDiff3", "FormDiff5", "MomentumDiff",
"ImpliedProbHome", "ImpliedProbDraw", "ImpliedProbAway",
]
# Keep team names and metadata for later use in match previews
df_eng_model = df_eng[engineered_cols + [target, "HomeTeam", "AwayTeam", "Division", "MatchDate"]].dropna(
subset=engineered_cols + [target]
)
print(f"\nRows with engineered features: {len(df_eng_model)}")
X_eng = df_eng_model[engineered_cols]
y_eng = df_eng_model[target]
X_train_eng, X_test_eng, y_train_eng, y_test_eng = train_test_split(
X_eng, y_eng, test_size=0.2, random_state=42
)
# More trees and limited depth to reduce overfitting
improved_model = RandomForestClassifier(n_estimators=200, random_state=42, max_depth=15)
improved_model.fit(X_train_eng, y_train_eng)
What does this code do?
- engineered_cols lists the 14 features the improved model will train on. This includes both raw columns and the derived ones you just created.
- dropna(subset=engineered_cols + [target]) removes rows where any of the model's input features or the target is missing. Rows without betting odds are dropped here.
- max_depth=15 prevents individual trees from memorising the training data. Without a depth limit, trees can grow until they perfectly fit noise.
- Save football_predictor.py and run it by executing:
python football_predictor.py
At the end of the output (after the baseline report), you should see a new line reading Rows with engineered features: followed by a number. This confirms the feature engineering and data filtering ran successfully.
Seeing a KeyError on OddHome?
- Make sure you are using the original Matches.csv file from the dataset. The odds columns (OddHome, OddDraw, OddAway) are in that file.
- Check that your df_eng = df.copy() line is placed after the baseline model section. It needs access to the original df variable.
- Help me debug a KeyError in my feature engineering code.
Evaluate the improved model and compare accuracy
The model is trained. Now you need to measure how much better it performs and see which features contributed the most to predictions.
- Add the following evaluation code at the bottom of football_predictor.py:
improved_preds = improved_model.predict(X_test_eng)
improved_accuracy = accuracy_score(y_test_eng, improved_preds)
print(f"\n{'='*50}")
print(f"IMPROVED MODEL ACCURACY: {improved_accuracy:.4f}")
print(f"IMPROVEMENT OVER BASELINE: +{improved_accuracy - baseline_accuracy:.4f}")
print(f"{'='*50}")
print(classification_report(y_test_eng, improved_preds))
print("\nFeature Importances (top 10):")
importances = pd.Series(
improved_model.feature_importances_, index=engineered_cols
).sort_values(ascending=False)
print(importances.head(10))
What does this code do?
- accuracy_score compares predictions against actual results and returns the fraction that were correct.
- The improvement line subtracts baseline_accuracy from improved_accuracy so you see the absolute gain from feature engineering.
- feature_importances_ is a Random Forest property that tells you how much each feature reduced uncertainty during training. Higher importance means the model relied on that feature more.
Before you run this, think about what you expect. Do you think the improved model will beat 50%? By how much?
- Save football_predictor.py and run it:
python football_predictor.py
At the bottom of the output, you should see the improved accuracy printed at approximately 0.54-0.58. That is a clear jump from the baseline's ~0.50. You will also see the feature importances ranked, with implied probabilities and Elo-based features dominating the top positions.
Why is 55% actually good for football?
Football is inherently unpredictable. With three possible outcomes (Home, Draw, Away), random guessing gives you 33%. Even professional betting models with access to squad data, injuries, and advanced metrics rarely exceed 55-60% on three-class prediction.
Your model jumped from ~50% to ~55% purely by encoding relationships between features. The classification report will also show that Draws remain the hardest class to predict. This is expected because draws are the rarest outcome and the most random.
Accuracy not higher than baseline?
- Check that your engineered_cols list includes all 14 features. Missing the implied probability columns removes the biggest accuracy boost.
- Make sure random_state=42 is set in both train_test_split and RandomForestClassifier. Different random states produce different splits and trees.
- Verify that n_estimators=200 and max_depth=15 are set on the improved model. The baseline used 100 estimators with no depth limit.
- Help me debug why my improved model accuracy isn't higher than baseline.
✔️ Awesome, I've got everything!
Great. Double check you have saved football_predictor.py with all three chunks: the feature creation, the data prep and training, and the evaluation block.
ⓧ I'd like to double check the full code
Your football_predictor.py is now too long to show in one block. Scroll through your file and confirm these sections exist in order:
- Imports at the top (pandas, sklearn, openai).
- # --- Load and Explore --- section with data loading, shape printing, and summary statistics.
- # --- Baseline Model --- section with the 100-estimator model and classification report.
- # --- Feature Engineering --- section starting with df_eng = df.copy() and ending with the implied probability calculations.
- engineered_cols list definition with 14 feature names.
- Data preparation (df_eng_model, train_test_split) and improved_model training with 200 estimators.
- Evaluation block printing IMPROVED MODEL ACCURACY, IMPROVEMENT OVER BASELINE, classification report, and feature importances.
Your model now predicts football matches significantly better than the baseline. You have proven that feature engineering matters more than throwing raw numbers at a model. Next up, you will use OpenAI to turn these dry predictions into pundit-style match previews that a football fan would actually want to read.
Generate AI Match Previews
Your improved model can now predict match outcomes with measurably better accuracy. But a raw prediction of "H" or "A" means nothing to a football fan without context.
In this step, you will feed the match features and model confidence to OpenAI and get back a pundit-style explanation that makes each prediction interpretable and shareable.
In this step, get ready to:
- Select a sample match from the test set and extract the model's prediction with confidence scores.
- Build a detailed prompt that feeds match stats and the prediction to OpenAI.
- Generate and print a natural-language match preview in pundit-style English.
Select a sample match and get predictions
Before you can ask an AI to explain a prediction, you need the prediction itself. You will pick one match from the test set, run it through the improved model, and capture the confidence scores for all three outcomes.
- In football_predictor.py, scroll to the bottom of the file (below the feature importances print statement).
- Add the following code to initialize the OpenAI client, select a sample match, and extract the prediction:
# --- AI Match Previews ---
client = OpenAI()
# Pick the first match from the test set
sample_index = X_test_eng.index[0]
sample_features = X_test_eng.loc[[sample_index]]
sample_prediction = improved_model.predict(sample_features)[0]
sample_probas = improved_model.predict_proba(sample_features)[0]
class_labels = list(improved_model.classes_)
# Get full match details for the prompt
match_info = df_eng_model.loc[sample_index]
result_map = {"H": "Home Win", "D": "Draw", "A": "Away Win"}
What does this code do?
The OpenAI() client reads your OPENAI_API_KEY environment variable automatically. No need to pass it explicitly.
You grab the first match in the test set by index, then call predict() to get the outcome label (H, D, or A) and predict_proba() to get the confidence percentage for each outcome. The result_map dictionary translates the short codes into human-readable labels.
- Save football_predictor.py and run it to confirm no errors in the new code:
python football_predictor.py
- You should see all the previous output (dataset shape, baseline accuracy, improved accuracy, feature importances) and the script completes without any errors at the end.
Seeing an authentication error?
If you see a message about a missing API key, your OPENAI_API_KEY environment variable is not set in this terminal session. Make sure you closed and reopened your terminal after running the setx command from Step 1.
help me fix my OpenAI API key authentication error
Build the match preview prompt
The quality of the AI output depends entirely on the prompt you send. A detailed prompt with concrete stats produces a specific, insightful preview. A vague prompt produces generic fluff.
- Below the result_map line you just added, paste this prompt that packages all the match data for the AI:
prompt = f"""You are a football analyst. Based on the following pre-match data and model prediction,
write a brief match preview (3-4 sentences) explaining why the model predicted this outcome.
Match: {match_info['HomeTeam']} vs {match_info['AwayTeam']}
League: {match_info['Division']}
Date: {match_info['MatchDate']}
Pre-match data:
- Home team Elo: {match_info['HomeElo']:.0f}
- Away team Elo: {match_info['AwayElo']:.0f}
- Elo difference (positive favors home): {match_info['EloDiff']:.1f}
- Home form (last 3 matches): {match_info['Form3Home']:.0f}/9 points
- Away form (last 3 matches): {match_info['Form3Away']:.0f}/9 points
- Home form (last 5 matches): {match_info['Form5Home']:.0f}/15 points
- Away form (last 5 matches): {match_info['Form5Away']:.0f}/15 points
- Bookmaker implied probability - Home win: {match_info['ImpliedProbHome']:.1%}
- Bookmaker implied probability - Draw: {match_info['ImpliedProbDraw']:.1%}
- Bookmaker implied probability - Away win: {match_info['ImpliedProbAway']:.1%}
Model prediction: {result_map[sample_prediction]}
Model confidence: Home {sample_probas[class_labels.index('H')]:.1%}, Draw {sample_probas[class_labels.index('D')]:.1%}, Away {sample_probas[class_labels.index('A')]:.1%}
Write the preview in an engaging, pundit-style tone."""
What does this prompt include?
The prompt gives the AI everything it needs to write an informed preview. It includes the team names, the Elo ratings (overall strength), recent form (momentum), bookmaker-implied probabilities (market consensus), and the model's own prediction with confidence scores.
The f-string format specifiers like :.0f and :.1% clean up the numbers. :.0f rounds to a whole number and :.1% converts a decimal to a percentage with one decimal place.
- Save football_predictor.py.
Call the API and print the preview
With the prompt assembled, you can send it to OpenAI and print the generated match preview alongside the prediction details.
- Below the prompt, add this code to call the API and display the result:
response = client.responses.create(
model="gpt-5.6",
input=[{"role": "user", "content": prompt}],
)
print(f"\n{'='*50}")
print("AI MATCH PREVIEW")
print(f"{'='*50}")
print(f"Match: {match_info['HomeTeam']} vs {match_info['AwayTeam']}")
print(f"Prediction: {result_map[sample_prediction]}")
print(f"Confidence: H={sample_probas[class_labels.index('H')]:.1%} D={sample_probas[class_labels.index('D')]:.1%} A={sample_probas[class_labels.index('A')]:.1%}")
print(f"\n{response.output_text}")
What does this code do?
The client.responses.create() call sends your prompt to the gpt-5.6 model and returns a response object. You access the generated text via response.output_text.
The print statements display the match details, the model's prediction with confidence percentages, and then the AI-generated preview. This gives you the full picture: the data that went in, the model's decision, and the AI's explanation of why.
- Save football_predictor.py.
Before you run this, what do you think the AI preview will focus on? Will it mention Elo ratings, recent form, or bookmaker odds most heavily?
- Run the script to see your AI match preview:
python football_predictor.py
After all the model training output, you should see an "AI MATCH PREVIEW" section with the match name, the predicted result, confidence percentages, and a 3-4 sentence preview written in pundit-style English referencing specific stats like Elo advantage and form.
Seeing a rate limit or model error?
If you see a rate limit error, wait 30 seconds and run the script again. Free-tier accounts have lower request limits.
If you see an error about the model not being found, double-check that your model parameter is set to gpt-5.6 exactly as written.
help me debug my OpenAI API call
Why use an LLM instead of a template?
A template approach ("Team X has a higher Elo so they will win") produces robotic, repetitive text. The LLM weighs multiple factors together and produces varied, natural language each time. It can also surface non-obvious insights like "despite strong recent form, the bookmakers still rate them as underdogs."
✔️ Awesome, I've got everything!
Great. Double check you have saved football_predictor.py and your AI preview printed successfully.
ⓧ I'd like to double check the full code
Your football_predictor.py is now quite long. Scroll through and confirm these sections are present in order:
- Imports (pandas, sklearn, openai) at the top.
- Load and Explore section with df = pd.read_csv("Matches.csv") and print statements.
- Baseline Model section with RandomForestClassifier(n_estimators=100).
- Feature Engineering section creating EloDiff, EloAdvantage, MomentumDiff, and implied probabilities.
- Improved model with RandomForestClassifier(n_estimators=200, max_depth=15).
- AI Match Previews section with client = OpenAI(), sample selection, prompt, and client.responses.create() call.
You now have a complete prediction pipeline: raw data goes in, a trained model predicts the outcome, and an AI explains the reasoning in natural language. Next up, you will take this further in the Secret Mission by adding confidence filtering and batch-generating previews for an entire matchday.
Secret mission
Batch Previews with Confidence Filtering
A real prediction system doesn't waste API calls on uncertain predictions. Can you add confidence-based filtering so the system only generates match previews when the model is genuinely sure about the outcome, then batch-generate an entire matchday report?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs. The OpenAI API calls you made during the project cost a few cents total.
Resources you used:
- Project files: football_predictor.py and Matches.csv in your project folder.
- Python packages: pandas, scikit-learn, and openai installed via pip.
- Environment variable: OPENAI_API_KEY set in your system.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with the football prediction model or want to tweak the features and re-run the script.
- Your project files stay in your project folder, ready to run again.
- Your OPENAI_API_KEY remains set for future API calls.
- The Python packages remain installed and available for other projects.
Pause - I'll come back to this later
Shut down any running processes to free up memory. Your files and packages stay intact for when you return.
- Close your terminal window if you still have one open.
- Close the football_predictor.py file in your IDE.
- Everything else stays as-is. There are no running services or ongoing costs.
Delete - I don't want to use this again
Remove all project resources and start fresh. This removes the files, packages, and environment variable you created during this project.
- Delete your project files. On macOS/Linux, navigate to the parent folder in your terminal and run:
rm football_predictor.py Matches.csv
On Windows PowerShell:
Remove-Item football_predictor.py, Matches.csv
- Remove the OPENAI_API_KEY environment variable. In PowerShell, run:
setx OPENAI_API_KEY ""
On macOS/Linux, remove the export line from your shell config file (e.g. ~/.zshrc or ~/.bashrc).
- Uninstall the Python packages by running:
pip uninstall pandas scikit-learn openai -y
That's a Wrap!
That's a Wrap!
Nice work! You've just built a complete football match prediction system that processes 25 years of real match data, trains a machine learning model, and generates AI-powered match previews.
You've learned how to:
- Loaded and explored 226,000+ football match records spanning 42 leagues, cleaned messy data with missing values, and prepared it for machine learning.
- Trained a baseline Random Forest model, saw it struggle at ~50% accuracy, then engineered features like Elo advantage and bookmaker-implied probabilities that measurably improved prediction accuracy.
- Used the OpenAI API to generate natural-language match previews that explain each prediction in pundit-style English, turning raw model output into something a football fan would actually want to read.
- Secret Mission: Built a confidence-filtered batch preview system that only generates previews for high-confidence predictions and formats them as a full matchday report.
Ready to quiz yourself?