premier league tactical analytics
turning raw match events into a fingerprint of how a team plays.
python · statsbombpy · pandas · numpy · duckdb · pyarrow · networkx · mplsoccer
active, case study done, league-wide modelling next.
My biggest and most ongoing project, a python pipeline that pulls open StatsBomb event data and rebuilds each match as a set of possession chains, then measures how a team builds, presses, progresses, and transitions. The aim is to quantify playstyle from the events themselves rather than from summary stats, and to watch how that style shifts across matches, seasons, and eras. It's still very much in progress, and the long-term goal is to grow it into a genuinely useful football-analytics framework.
why i built it
I started this as a way to actually learn machine learning, but the whole of it, not just the modelling. I wanted to go through every stage myself, collecting the data, engineering it, analysing it, scaling it up to larger and larger amounts of data, and only then getting to the machine learning phase. Most tutorials hand you a clean dataset and skip everything that comes before it, and I wanted to feel the parts that actually take the time.
Football was the obvious subject. It was World Cup season, I wanted to build something that fit the moment, and I've always been drawn to the small tactical details of the game, the way a team builds, the little ball movements and patterns that add up to an identity and eventually to a result. Public football stats are mostly single numbers like possession percentage or xG, which tell you what happened but not how a team actually plays, and I wanted to understand football analytics from start to finish, the whole path from raw events to something meaningful.
The end goal has always been for this to be genuinely useful, not just a learning exercise I abandon halfway. I want it to grow into something real that other people could actually use for analysis and prediction, a proper framework for understanding how teams play and how the game has changed over time.
the thinking
The key decision was choosing the possession chain as the unit of analysis instead of the match or the player. A chain has a tempo, a directness, a spatial progression, the pressure it faced, and an outcome like a shot or a turnover, and if you aggregate those you get a style rather than a scoreline. The second decision was to validate every feature back against the raw events instead of trusting it because it looked reasonable, since a metric that quietly miscounts is worse than no metric at all. That discipline was slow, but it's exactly what let the same engine later run unchanged on a completely different team and still be believed.

the analysis
The anchor case study is the opening match of Arsenal's unbeaten 2003/04 season, Arsenal 2-1 Everton, used to test one hypothesis, that Arsenal played a structured, deliberate, ground-based game against Everton's older, faster, more direct English style.

does the ball-progression method separate the two styles?
I measured each possession's directness and how it advanced up the pitch, then compared the distributions. Arsenal's chains are longer, more circulated, and progress through the thirds, while Everton's are shorter and more vertical, so the hypothesis holds at the level of how the ball moves.

what does each team's passing structure actually look like?
Building pass networks (nodes = players, edges weighted by pass volume, plus betweenness) turns "possession" into a shape you can see, where the ball is circulated and who the connectors are.

does the framework generalize, or did i just describe one match?
The real test of a framework is whether the same diagnostic engine works on a team it wasn't tuned for. I ran it byte-for-byte unchanged on Leicester's 2015/16 title run. It held, same feature universe, same clean design, and surfaced the sharpest finding, opposite context axes. Arsenal's style is venue-driven (home vs away), while Leicester's is opponent-driven (they counter stronger sides with fewer, more direct passes). Same machinery, opposite context.

how it works
The pipeline runs outside-in. Extraction pulls matches and events per season into
resumable parquet (with the raw JSON preserved for schema debugging), and a normaliser
flattens StatsBomb's nested JSON, turning location arrays into x/y and sub-dicts
into columns. On top of that, three feature families build possession (chains,
tempo, rhythm, outcomes), passing (intent, buildup, pressure escape, pass
networks), and transition (offensive and defensive) tables, each validated against
the raw data. By the scaling phase that's a full league season, 760 rows spanning all
20 teams across 38 matches, with per-possession score-state engineered in.
what i learned
The lesson that stuck was that generalization is the real proof. It's easy to describe one match with enough hand-tuned metrics, but it's much harder to build an engine that runs unchanged on a different team and still says something true, and when it finally did, the Arsenal-versus-Leicester contrast was more interesting than anything I would have found in a single match. The validate-everything-against-the-raw-data habit slowed me down constantly and was worth every minute.
More than any single result, this project has been my crash course in the full shape of machine learning work, the collecting, the cleaning, the endless engineering, and the scaling, long before any model shows up. It's large and still unfinished by design, and the next phases turn the engine into a proper era-normalized framework for comparing teams, identifying styles, and tracking how football has evolved, with match prediction further down the road. It's the project I'm most invested in seeing become genuinely useful.

