Who marks whom? The assignment problem at set pieces
Five markers, five attackers at a corner. Putting your best header on their most dangerous man sounds right and concedes more. The assignment problem finds the best set of pairings, the Hungarian method solves it by hand, and 190 real corners a season turn the difference into goals.
Intermediate Part 7 of Decision Science Through Football
New to the notation? The symbols explained
Contents
The football question
A corner against. The other side sends five into the box: a target man, their centre-back, a near-post runner, a late arrival from the edge and their striker. You have five markers. Who should pick up whom?
The instinct is simple: put your best header on their most dangerous man, then work down. It sounds like common sense. On the numbers below, it concedes nearly a fifth more than the best plan.
The concept
Pairing each of one group with exactly one of another, at the lowest total cost, is the assignment problem: workers to jobs, taxis to passengers, markers to attackers. With five markers there are 5 × 4 × 3 × 2 × 1 = 120 possible plans, few enough to check every one. With eleven, in man-to-man marking across the pitch, there are 39,916,800. With twenty there are about 2.4 billion billion.
The classic way to solve it without trying every plan is the Hungarian method, published by Harold Kuhn in 1955 and named after the Hungarian mathematicians Dénes Kőnig and Jenő Egerváry, whose earlier work it built on. In 2006 it turned out that Carl Gustav Jacobi had already solved the problem in the 19th century, in a paper published in Latin after his death.
A football example
Here's how many goals we'd concede per 1,000 corners for each pairing (made-up numbers), if that marker picks up that attacker:
A plan picks one number from each row and each column, and the goals conceded add up:
$$\text{conceded} = \sum_{\text{pairs}} \text{goals per pairing}$$
In plain football
- Each pairing has its own cost: how often that attacker scores if that marker has him.
- A plan uses each marker once and covers each attacker once, so it's one number from every row and every column of the grid.
- The best plan is the one whose five numbers add up to the least.
Why the obvious rule loses
Their target man is the biggest threat, so the obvious rule gives him the marker who handles him best: our big centre-back (6). Next comes their centre-back. Our second centre-back is poor against him (13), so he goes to the holding midfielder (12). Working down the list gives a plan worth 38 goals per 1,000 corners.
The best plan, found by checking all 120, swaps our two centre-backs: the big centre-back takes their centre-back (6) and the second centre-back takes the target man (8). The target man gets a slightly worse marker, 8 instead of 6, but their centre-back gets a much better one, 6 instead of 12. The rest falls into place for a total of 32.
The obvious rule looks at one attacker at a time. The best plan looks at all of them together. Our big centre-back is good against both of their big men, our second centre-back against only one, so the big centre-back should take the one the other can't handle.
The best plan also has the smallest worst pairing any plan can manage (8, against the obvious rule's 12). Here, "concede the fewest in total" and "avoid the worst mismatch" lead to the same plan. They don't have to: as in Part 1, a different objective can give a different answer.
The Hungarian method by hand
Once the two centre-backs are paired off, here are the other three markers against the other three attackers:
| Marker | Runner, late, striker |
|---|---|
| Holding midfielder | 7, 5, 7 |
| Full-back | 5, 8, 6 |
| Winger | 7, 7, 9 |
The Hungarian method's first two steps:
- Take each row's smallest number off that row. Each marker's best option becomes 0, and the rest show how much worse they are.
- Then take each column's smallest off that column, so each attacker's best marker also shows a 0.
That leaves
| Marker | Runner, late, striker |
|---|---|
| Holding midfielder | 2, 0, 1 |
| Full-back | 0, 3, 0 |
| Winger | 0, 0, 1 |
Now look for a 0 in every row and every column: holding midfielder on the late arriver, full-back on the striker, winger on the runner. That plan costs nothing extra over the minimums taken off, so it's the best: 5 + 6 + 7 = 18, the same three pairings the full search found. When the zeros don't line up like this, the method has a further step that shifts the numbers until they do. James Munkres showed in 1957 that it always gets there in a number of steps that grows only as a power of the problem's size, not explosively like the number of plans. scipy's linear_sum_assignment, used in the snippet for the full grid, uses a later and faster method for the same problem, a version of the Jonker–Volgenant algorithm.
Over a season
Real corners give the made-up numbers a scale. In the 2025/26 Scottish Premiership, a team faced on average 190 corners over the season. At those numbers, the best plan concedes 6.1 goals from corners and the obvious rule 7.2: about one goal a season, or 0.7 points at the 0.60 points a goal conceded is worth (from how many points is a goal worth?). That's small for one plan, but it comes from changing nothing except who stands next to whom. For how much corners matter overall, see the corners myth.
Why it matters
Pairing problems are everywhere in a club: markers to attackers, scouts to regions, coaches to players, players to positions. The obvious way to solve them, best first and work down, is called a greedy method. It's quick and often close, but it can lock in an early choice that costs more later, as it did with the two centre-backs. The assignment problem looks at every pairing together. The Hungarian method shows it can be solved fast and by hand for small cases, and with software for big ones. Part 8 moves from pairings to routes: the best way through a network, such as passing lanes through a press.
Limitations
- The threat numbers are made up. Real values would come from tracking data on aerial duels and finishing, which our data doesn't have.
- Corners aren't only man-to-man. Many teams mark zones or mix zones with men, and attackers move to escape their marker. The assignment problem fits man-marking best.
- The corners count is real, the rest isn't. 190 is the 2025/26 average; the goals per corner come from the made-up table.
- Pairings interact. A marker's job can depend on where his teammates are, which a table of separate pairings can't capture.
Try it yourself
Make our second centre-back better against their centre-back: change his 13 to 7. Before running the code, guess: does the obvious rule now find the best plan? Then add a sixth attacker and a sixth marker, and see how the number of plans grows.
Reproduce the analysis
This needs scipy (pip install scipy) and, for the season figures, the 2025/26 Scottish Premiership file (SC0) from football-data.co.uk, saved as SC0_2526.csv; it isn't rehosted on this site. Then:
Show the Python64 lines, ready to copy and run.
import csv
from itertools import permutations
from math import factorial
from scipy.optimize import linear_sum_assignment
# Made up: goals conceded per 1,000 corners if each of our markers (rows) picks up each of their attackers (columns).
markers = ["Big centre-back", "Second centre-back", "Holding midfielder", "Full-back", "Winger"]
attackers = ["Target man", "Their centre-back", "Near-post runner", "Late arriver", "Striker"]
threat = [[6, 6, 10, 8, 8],
[8, 13, 9, 7, 8],
[15, 12, 7, 5, 7],
[18, 13, 5, 8, 6],
[21, 15, 7, 7, 9]]
n = len(markers)
total = lambda plan: sum(threat[i][plan[i]] for i in range(n)) # plan[i] = the attacker marker i picks up
def show(label, plan):
pairs = "; ".join(f"{markers[i]} on {attackers[plan[i]]}" for i in range(n))
print(f"{label}: {total(plan)} per 1,000 corners\n {pairs}")
# The obvious rule: take their most dangerous attacker first, give him our best marker for him, and so on down.
danger = sorted(range(n), key=lambda j: -sum(row[j] for row in threat))
free, greedy = set(range(n)), [None] * n
for j in danger:
i = min(free, key=lambda i: threat[i][j])
greedy[i] = j
free.remove(i)
show("Best marker on their most dangerous man first", greedy)
plans = list(permutations(range(n)))
best = min(plans, key=total)
show(f"Best of all {len(plans)} plans", best)
print(f" (the worst plan concedes {max(map(total, plans))})")
rows, cols = linear_sum_assignment(threat)
print(f"scipy's linear_sum_assignment agrees: {list(cols) == list(best)}")
worst_pair = lambda plan: max(threat[i][plan[i]] for i in range(n))
print(f"Worst single pairing: {worst_pair(greedy)} with the obvious rule, {worst_pair(best)} with the best plan; "
f"the smallest any plan can manage is {min(map(worst_pair, plans))}")
# The Hungarian method by hand, on the last three markers and attackers.
sub = [row[2:] for row in threat[2:]]
reduced = [[v - min(row) for v in row] for row in sub] # take each row's smallest off the row
reduced = [[row[j] - min(r[j] for r in reduced) for j in range(3)] for row in reduced] # then each column's
print("\nHungarian method on the holding midfielder, full-back and winger:")
for name, row in zip(markers[2:], reduced):
print(f" {name:<19}" + "".join(f"{v:4}" for v in row))
zeros = [p for p in permutations(range(3)) if all(reduced[i][p[i]] == 0 for i in range(3))]
print(" a zero in every row and column: " + "; ".join(f"{markers[2 + i]} on {attackers[2 + zeros[0][i]]}" for i in range(3)))
print("\nHow many plans to try:")
for k in (5, 11, 20):
print(f" {k} markers: {factorial(k):,}")
# Real: corners per team in the 2025/26 Scottish Premiership (needs SC0_2526.csv from football-data.co.uk).
with open("SC0_2526.csv", encoding="latin-1") as f:
games = [r for r in csv.DictReader(f) if r.get("HC")]
corners = sum(int(r["HC"]) + int(r["AC"]) for r in games) / (2 * len(games)) * 38
print(f"\nCorners faced per team-season, 2025/26: {corners:.0f}")
print(f"Goals conceded from them: {corners * total(best) / 1000:.1f} with the best plan, "
f"{corners * total(greedy) / 1000:.1f} with the obvious rule; "
f"about {corners * (total(greedy) - total(best)) / 1000 * 0.60:.1f} points a season at 0.60 a goal")
It prints:
Show the Text21 lines, ready to copy and run.
Best marker on their most dangerous man first: 38 per 1,000 corners
Big centre-back on Target man; Second centre-back on Striker; Holding midfielder on Their centre-back; Full-back on Near-post runner; Winger on Late arriver
Best of all 120 plans: 32 per 1,000 corners
Big centre-back on Their centre-back; Second centre-back on Target man; Holding midfielder on Late arriver; Full-back on Striker; Winger on Near-post runner
(the worst plan concedes 59)
scipy's linear_sum_assignment agrees: True
Worst single pairing: 12 with the obvious rule, 8 with the best plan; the smallest any plan can manage is 8
Hungarian method on the holding midfielder, full-back and winger:
Holding midfielder 2 0 1
Full-back 0 3 0
Winger 0 0 1
a zero in every row and column: Holding midfielder on Late arriver; Full-back on Striker; Winger on Near-post runner
How many plans to try:
5 markers: 120
11 markers: 39,916,800
20 markers: 2,432,902,008,176,640,000
Corners faced per team-season, 2025/26: 190
Goals conceded from them: 6.1 with the best plan, 7.2 with the obvious rule; about 0.7 points a season at 0.60 a goal
The obvious rule ranks their attackers by their average threat across our markers. Ranking them another way changes its plan, but not the lesson: any rule that settles one pairing at a time can miss the best set.
Further reading
- Assignment problem, Wikipedia. The problem and the main ways to solve it.
- Hungarian algorithm, Wikipedia. Kuhn's method step by step, its history and Jacobi's earlier solution.
- scipy.optimize.linear_sum_assignment, the SciPy documentation for the solver used here.