A football match produces about 2,000 events in 90 minutes. A pundit watches it once, forms an impression, and talks for an hour. Ask them why their team lost and you get narrative: "they wanted it more", "the ref changed it", "we were unlucky".
Last month I watched a match that finished 3-1 and thought the loser was battered. Then I pulled the event data: the losing team had more expected goals (xG 1.54 vs 1.81 - close), created the better chances, and only collapsed after a 63rd-minute red card. The scoreline told one story. The data told a completely different one - and nobody watching could see it.
MatchLens is the tool that closes that gap: an offline, zero-dependency match analyst that turns raw event CSVs into an evidence-cited tactical report.
What MatchLens does
You export match events (minute, team, type, player, xG) and team info to CSV. MatchLens produces:
- xG comparison - who actually created what, not who scored
- Key moment timeline - every goal, penalty, red and yellow card with the live scoreline attached, so you see the swing points
- Six insight types - CLINICAL_FINISHING (outscoring your xG), WASTEFUL_ATTACK (chances without end product), MAN_DOWN_EFFECT (performance before vs after a red card), SUB_IMPACT (did the changes work), MOMENTUM_PHASE (which 15-minute block each team owned), DISCIPLINE (cards, fouls)
- A natural-language match report a coach could read in two minutes
- Follow-up analysis suggestions - what to look at next
Every insight carries evidence citations: the event rows and numbers it used. No vibes, no narrative hand-waving.
A real run
$ python -m matchlens analyze examples/match_events.csv examples/teams.csv
MATCH: Blue Lions 3-1 Red Hawks
xG: 1.81 - 1.54 (scoreline flatters the winners)
KEY MOMENTS
23' GOAL Blue Lions 1-0 (K. Sato, xG 0.62)
41' RED Red Hawks - D. Okafor (2nd yellow) score stays 1-0
58' PEN Red Hawks 1-1 from the spot (xG 0.76)
71' GOAL Blue Lions 2-1 (counter after save, xG 0.31)
88' GOAL Blue Lions 3-1 (xG 0.09 - cleanup, game over)
INSIGHTS
[MAN_DOWN_EFFECT] Red Hawks xG 1.39 -> 0.15 after the red card
evidence: events 1-31 (pre) vs 32-58 (post); red card at 41'
[CLINICAL_FINISHING] Blue Lions scored 3 from 1.81 xG
evidence: goal minutes 23/71/88 vs cumulative xG by minute
[MOMENTUM_PHASE] Blue Lions owned 75-90 (xG 0.94 vs 0.08)
evidence: 15-minute bins from the event xG column
[WASTEFUL_ATTACK] Red Hawks 1.54 xG, 1 goal - chances without payoff
evidence: 12 shots, 6 on target, xG 1.54
REPORT: A tight match turned by one red card. Before it, Red Hawks were
ahead on xG. After it, they managed 0.15 xG in 32 minutes. The 3-1
scoreline describes the card, not the football.
The red-card finding is the one I care about: the tool splits the match at the dismissal and compares both halves on xG. "They were unlucky" becomes "they generated 1.39 xG with 11 men and 0.15 with 10" - a number you can argue with.
It ships with evals
$ python eval.py
PASS xg_totals_match_event_data
PASS scoreline_updates_on_every_goal
PASS red_card_splits_the_match_timeline
PASS stoppage_time_binned_into_90
PASS momentum_phase_uses_15_minute_bins
PASS sub_impact_compares_pre_post_minutes
PASS every_insight_cites_event_rows
PASS no_network_access
... 19/19 assertions passed.
Building it caught three real bugs, all of the kind that make demo numbers quietly wrong: the scoreline wasn't initialized for the away team (so the first away goal rendered "1" instead of "1-0"), stoppage-time minutes like 90+3' overflowed the 15-minute bins, and one eval assertion had its own expected value wrong. The eval suite is what found them - not me reading the code.
Why offline matters
Event data for professional matches is licensed and expensive; for grassroots and academy football it's usually a coach's spreadsheet. MatchLens runs entirely on your machine - pure standard library, no accounts, no uploads. Your team's data stays yours.
Tech stack
- Pure Python stdlib (
csv,datetime,argparse) - clone and run - Deterministic rules with transparent thresholds - every number traceable
- Natural-language report generation from computed facts
Try it
Repo: github.com/Zhiyilang/matchlens with a full example match (Blue Lions 3-1 Red Hawks, 58 events) and the 19-assertion eval suite. Run python eval.py, then point it at your own team's export.
I built MatchLens for the Microsoft x Premier League "Inside the Game" hackathon, but the use case is bigger than that: anyone who has ever argued about a match with data they couldn't check. The next time someone says a team "didn't deserve it", you'll have the xG split, the red-card before-and-after, and the minute-by-minute momentum bins to settle it.
What insight would you add? Mine is a candidate: set-piece xG share - how much of a team's chance creation comes from dead balls, which is the difference between a good team and a good set-piece coach.
Top comments (0)