Aug 26, 2025
7min read
Building a Full Stack MLOps System: Predicting the 2025/2026 English Premier League Season
Here’s a fun fact about me. I love football! I live, breathe, and sleep football. The story of how I found myself in Machine Learning Engineering is quite interesting. Back at University, as I was in the latter stages of my Bachelor’s degree in Informatics and Computer Science, I never knew what I wanted to do for my Capstone project. One weekend, as I settled in to watch a football match, my curiosity was piqued. What if I could create a system that could predict the outcome of football matches? Mixing something I love with an area of interest? What could go wrong? Well, everything! The project didn’t work, lol, but a passion for Sports Machine Learning Engineering was born! A dream I have been chasing ever since.
Press enter or click to view image in full size
Figure 1: Bruno Fernandes, Manchester United Midfielder
I challenged myself to roll back the years and see how I would have done the project better with the vast knowledge I have gained over the years. This project aims to demonstrate various MLOps concepts involved in production-ready systems and indulge in something close to my heart.
MLOps is a set of best practices and tools that bring together software development (DevOps) and machine learning to streamline the ML lifecycle. This is the first article in a series documenting the creation of a full-stack MLOps project aimed at predicting the outcomes of the English Premier League 2025 - 2026 (Hoping to be ready by Gameweek 8!). The full project will be linked here. The project is expected to be loosely structured like this:Phase 0: Project setup — Setting up our configurations and file structurePhase 1: Data Ingestion — Ingesting data from footballdata.ukPhase 2: Data Cleaning and Transformation — Cleaning Merged datasets and loading them into a Serverless Postgres Database — Neon DBPhase 3: Exploratory Data Analysis — Unravelling insights about the beautiful game.Phase 4: Feature Engineering and Selection — Selecting the right features and creating new ones.Phase 5: Model Training and Inference— Selecting the right model for the job and making the predictions. Tracking and versioning models and their metricsPhase 6: Evaluation and Deployment — Making the model available for use.Phase 7: Model Monitoring and Continuous Integration and Deployment— Detecting drift and bias.It is worth noting that these steps are not fixed and vary as per business/project requirements. At any stage, if you believe something could have been done better or have your thoughts, reach out to me through LinkedIn. Let’s learn together!😄Find what you love and let it kill you. Let it drain you of your all — Charles BukwoskiPhase 0: Project Setup
Press enter or click to view image in full size
Figure 2: AWS Maturity Model
The core principles in MLOps include, but are not limited to:
Reproducibility — Ensuring that your work can be reliably reproduced by standardizing environments and workflows, making it easier to validate and iterate models.
Scalability — Ensuring that systems can be scaled to handle varying workloads and accommodate changes in volume and complexity.
Collaboration — MLOps projects have many moving parts that require different teams, i.e, data scientists, software engineers, data analysts, and machine learning engineers, to work together.
Automation — Automating workflows for model training, testing, validation, deployment, and monitoring to enhance efficiency and reduce errors.
Version Control — Implement version control for both code and data to track changes, reproduce experiments, and maintain model lineage.
Continuous Integration and Deployment (CI/CD) — Establish CI/CD pipelines tailored for machine learning to facilitate model iteration and deployment
Monitoring and Governance — Continuously monitoring model performance and data drift in production to ensure models remain viable and compliant with established standards.
For this phase, we’ll start by creating a GitHub repository, cloning the project into our local environment, setting up the initial project structure, and configuring a custom logger to collect system logs.
Let’s create a repository. I wrote an article detailing the best practices for creating a GitHub repository. Go ahead and check it out.Git Started: Leveling Up Your Workflow with Essentials, Pro-Level Commits, and LLM-Ready PromptsCollaboration and version control are not just good practices; they’re essential survival skillsmedium.comCurrent Project Structure at this stageREADME.mdThis is a markdown file containing the project’s description and contents..gitignoreSpecifies intentionally untracked files that Git should ignorerequirements.txtThis .txt file contains all the packages needed for this project. At this stage, we don’t require any packages, so skip this for now.datasets/This file directory stores the various datasets used in this project.experiments/This file directory provides data scientists with a playground for experimentation. This directory contains the notebook subdirectory. This is where we will start phase 1, Data Ingestion.src/This is the most important directory in your project. It organizes the core moving parts of your machine learning project like a package, keeping your project modular, maintainable, and importable. Without it, imports and project structure would be messy and harder to scale.src/__init__.pyThe __init__.py file marks the entire src directory (or any other directory)as a Python package. It allows the whole directory to be used as a package by using from src.<file_name> import <file_name_method>
For any machine learning project, it is wise to collect information that helps trace the various actions taken by you and other developers within the project. This makes it easy to know exactly what happened and when it happened. These are logs. The two files responsible for creating and storing logs are src/exception.py and src/logger.py.src/logger.pylogger.py creates a logs/ directory in your current working directory. It generates a log file with a time stamp in its name. Foe
Figure 3: Generated logs
Different log messages have different meanings and are stored at different levels. The different levels of Python log messages are:
NOTSET (0) — No logging level is set, and the messages will be processed based on the handler’s level
DEBUG (10) — Detailed information, typically of interest only when diagnosing problems
INFO (20) — Confirmation that things are working as expected.
WARN (30) — An indication that something unexpected happened, or indicative of some problem soon (e.g., ‘disk space low’). However, the software is still working as expected.
ERROR (40) — The software has failed a function due to an issue.
CRITICAL (50) — A serious error, indicating that the program itself may be unable to continue running.
logger.py configures Python’s logging to write INFO-level logs (and above) to the log files, with timestamps, line numbers, logger names, and related messages. Logs are sensitive since they contain a lot of information about the system, which could be confidential. Be sure to add the logs directory to your gitignore file so that they are not tracked by Git
src/exception.py
This file provides custom error handling for the project. Various organisations have established standards for how their logs need to be collected. The error_message_detail function builds a detailed error message, including the filename, line number, and error message for any exception.
The CustomException class extends Python’s built-in class, Exception, which, when raised, uses the function error_message_detail to create a clear, informative log message.
If we run python -m src.exception, an error message is logged in the logs/ folder and today’s date ( 08–20–2025 21:58:09hrs ) as the file name
And we are set!
The initial set-up is complete. We have:
Created a GitHub repository for the project.
Set up the initial directories
Gone through the various Python log messages
Created a logger.py script that logs messages from the actions performed
Created an exception.py script that provides custom error handling.
A few considerations worth noting:
There are better libraries, such as loguru, that are ready to use out of the box without requiring boilerplate code. However, different companies may have different formatting conventions for their system logs, so defining your own would be best.
I avoid new/highly abstracted libraries/tools due to the low level of control, which may cause problems in production when you are not familiar with the inner workings of the library/tool.
Simple is better and easier to trace when an error arises.
Thank you for reading. I’m trying to make the articles in this series as short as possible. Let me know if I have missed anything.
The next article in this series is Building a Full Stack MLOps System: Predicting the 2025/2026 English Premier League Season — Phase 1: Data Ingestion.

