Is a half-time lead worth what it looks like? Matching like with like
Sides a goal up at half-time win 70% of the time; sides level win 32%. But the sides in front were usually the better sides before kick-off. Matching each leader with level sides that were just as fancied beforehand shows what the lead itself is worth, and what matching can never fix.
Intermediate Part 7 of Cause and Effect Through Football
Contents
The football question
In the Scottish Premiership since 2002/03, sides a goal up at half-time have gone on to win 69.9% of the time. Sides level at half-time have won 32.0%. That's a gap of 37.9 points.
So is a one-goal half-time lead worth 38 points on your chance of winning?
Not quite. The sides in front at half-time weren't a random choice. Before kick-off, the bookmakers gave them a 44.8% chance of winning on average, against 37.3% for the sides that were level. Better sides score first more often, and better sides win more often anyway. The raw gap mixes the two.
The concept
Matching compares each side that got the "treatment", here a one-goal lead at half-time, only with sides that didn't but were alike in every other way you can measure. If the only difference left is the lead, the gap in results is the lead's doing.
Here the obvious thing to match on is how strong each side was before the match. The bookmakers' prices sum that up neatly: the pre-match chance of winning, with their margin taken out, as in the bookmaker odds research. In the language of Part 2, team strength is a : it causes both the half-time lead and the win, so it's the thing to hold fixed.
So each side one up at half-time is compared with level sides that had the same pre-match chance, to within 5 points.
What matching shows
Every one of the 2,390 sides one up at half-time had level sides to compare with. First, the check that matters most: are the groups now alike? After matching, the level sides being compared had a pre-match chance of 44.7%, against 44.8% for the leaders. The difference in strength is gone.
Like for like, a one-goal half-time lead adds 31.5 points to the chance of winning, with a luck margin of ±2.4. About 17% of the raw gap was simply the stronger sides being the ones in front.
In plain football
- Take sides that were about 45% shots before kick-off. Those level at half-time won about four times in ten (38%).
- The same kind of side a goal up at half-time won about seven times in ten (70%).
- That difference, about 31 points, is what the goal itself is worth. The other 6 points of the raw gap belonged to being the better team.
Matching mostly confirmed the obvious here: half-time leads are worth a great deal. But it trimmed the number by a sixth, and in other questions that trim can be the whole story. In do substitutions work?, matching changes made while losing with sides in the same position that hadn't changed yet turned an apparent pick-up into almost nothing.
For what a lead is worth in points rather than wins, by every half-time margin, see shoot, cross or keep the ball?, and for the most famous lead of all, is 2–0 the most dangerous lead?
What matching can't fix
Matching only holds fixed what you measured. Anything else that both makes a lead more likely and makes winning more likely is still mixed in. Two examples here:
- Things that happen on the day. A side whose key opponent is missing through a late injury, or who has a man sent off against them early, is both more likely to lead at half-time and more likely to win. The pre-match price can't know about a first-half red card.
- Things the price gets slightly wrong. If the bookmakers underrate a side, that side will lead and win more than its price says, and some of that will look like the lead's effect.
And there's a trap in trying to fix it. It's tempting to also match on red cards, since the data has them. But a red card can come in the second half, after the lead, and the score may well affect who commits the fouls that earn one. Matching on something the treatment causes is the Part 2 mistake of holding a fixed. Draw the diagram first.
Why it matters
Matching is the workhorse of cause-and-effect work when there's no experiment: medicine, economics, and in football, almost every "players who do X do better" claim. Its strength is that it's easy to understand and easy to check: are the groups alike now? Its weakness is the one no amount of care removes: it can only make groups alike on what you measured. A randomised experiment makes them alike on everything, which is why it beats matching whenever one is possible.
That's the end of this series' tour of the main methods. Every one of them was a different way of answering the question from Part 1: what would have happened otherwise?
Limitations
- Pre-match prices aren't a perfect measure of strength. They're the best single number available, but they carry the bookmakers' own errors and their margin, which is taken out evenly here.
- Bands of 5 points. Sides within a band can still differ a little. Halving the bands to 2.5 points gives the same answer, 31.6 points (±2.4), with the groups' pre-match chances level at 44.8%.
- Both sides of every match count. A level match gives two level sides, so the 4,264 level sides come from 2,132 matches. That's fine for the averages, but the treats them as separate.
- From 2002/03 only, when Bet365 prices start in the data.
Reproduce the analysis
Save the Premiership results files (SC0) from football-data.co.uk in one folder; the download script fetches them under the names the snippet expects. It needs only Python 3 and takes under a second:
Show the Python47 lines, ready to copy and run.
# From Football Data Science by Bryan McGuire. Free to use with credit.
# https://www.footballdatascience.co.uk/learn/matching-half-time-leads
import csv
from collections import defaultdict
from math import sqrt
from statistics import mean, pvariance
# every side in every match with Bet365 prices (from 2002/03): its pre-match chance of winning, its half-time
# margin, and whether it won
sides = []
for y in range(2002, 2026):
with open(f"SC0_{y % 100:02d}{(y + 1) % 100:02d}.csv", encoding="latin-1") as f:
for r in csv.DictReader(f):
if r.get("FTR") not in ("H", "D", "A") or not r.get("B365H") or r.get("HTHG", "") == "":
continue
h, d, a = (1 / float(r[k]) for k in ("B365H", "B365D", "B365A"))
ht = int(r["HTHG"]) - int(r["HTAG"])
sides.append({"chance": h / (h + d + a), "ht": ht, "won": r["FTR"] == "H"}) # the margin taken out
sides.append({"chance": a / (h + d + a), "ht": -ht, "won": r["FTR"] == "A"})
up = [s for s in sides if s["ht"] == 1]
level = [s for s in sides if s["ht"] == 0]
print(f"{len(sides) // 2:,} matches: {len(up):,} sides one goal up at half-time, {len(level):,} level")
print(f"won: one up {mean(s['won'] for s in up):.1%}, level {mean(s['won'] for s in level):.1%}, "
f"a gap of {mean(s['won'] for s in up) - mean(s['won'] for s in level):.1%}")
print(f"pre-match chance of winning: one up {mean(s['chance'] for s in up):.1%}, level {mean(s['chance'] for s in level):.1%}")
# matching: compare each one-up side only with level sides that had the same pre-match chance, to within a band
def matched(width):
groups = defaultdict(lambda: ([], []))
for s in up:
groups[int(s["chance"] / width)][0].append(s)
for s in level:
groups[int(s["chance"] / width)][1].append(s)
used = [g for g in groups.values() if g[0] and g[1]]
n = sum(len(u) for u, _ in used)
effect = sum(len(u) * (mean(s["won"] for s in u) - mean(s["won"] for s in v)) for u, v in used) / n
se = sqrt(sum((len(u) / n) ** 2 * (pvariance([s["won"] for s in u]) / len(u) + pvariance([s["won"] for s in v]) / len(v))
for u, v in used))
alike = sum(len(u) * mean(s["chance"] for s in v) for u, v in used) / n # the level sides, weighted to match
return n, effect, se, alike
raw = mean(s["won"] for s in up) - mean(s["won"] for s in level)
for width in (0.05, 0.025):
n, effect, se, alike = matched(width)
print(f"\nmatched to within {width:.1%} on the pre-match chance ({n:,} of {len(up):,} one-up sides have level sides to match):")
print(f" level sides, weighted to match: pre-match chance {alike:.1%}")
print(f" what the half-time lead itself adds to the chance of winning: {effect:.1%} (luck margin ± {2 * se:.1%})")
print(f" share of the raw gap that was the stronger sides being the ones in front: {1 - effect / raw:.0%}")
Further reading
- Matching (statistics), Wikipedia. The method, including propensity scores, which match on the chance of getting the treatment when there are many things to match on.
- Confounding, Wikipedia. Why a cause of both the treatment and the outcome has to be held fixed, and why unmeasured ones are the hardest problem.
Confounder
A third thing that drives two others, so they move together without one causing the other. Dominance drives both fewer fouls and more points.
Mediator
Something in the middle of a chain of causes, such as the extra shots on target that home advantage works through. Hold it fixed and the effect you wanted to measure disappears.
Luck margin (standard error)
How much a figure could move by chance alone, given how many matches it rests on. A gap smaller than this could be luck.