Skip to content

Who's the best striker? The Pareto front

The best striker usually means more than one thing: goals, fee, age. The Pareto front throws out every option beaten on all of them and leaves the ones worth arguing about. It also shows why some good choices never top a weighted score, and which 2025/26 Premiership sides nobody beat at both ends.

Intermediate Part 3 of Decision Science Through Football

New to the notation? The symbols explained

Contents

The football question

Who's the best striker in the league? Ask ten fans and you'll get ten answers, and most of the arguments aren't really about the players. One fan means most goals, another means most goals for the money, a third has the player's age in mind. When "best" means more than one thing, there may be no single answer.

Part 1 dealt with this by picking one objective, points, and turning everything into it. This part asks what can be said before anyone agrees on how to weigh things up. The answer is a lot: most options can be ruled out by everyone.

The concept

With more than one measure, one option beats another (economists say it dominates it) if it's at least as good on every measure and better on at least one. A striker who costs less and scores more beats the other on any sensible view. It's the same idea as a dominated choice in game theory.

The options nobody beats make up the Pareto front, named after the Italian economist Vilfredo Pareto. Everything off the front can be dropped straight away, whatever anyone thinks matters most. Everything on it involves a trade-off: to get more of one thing, you have to give up some of another.

A football example

Here's a made-up shortlist of ten strikers, each with a fee and the goals he'd add over a season:

Striker Fee Goals added
A £2m 3
B £4m 7
C £6m 8
D £8m 12
E £12m 15
F £5m 5
G £7m 7
H £9m 10
I £3m 2
J £14m 13

Half of them can go at once:

  • F (£5m, 5 goals) is beaten by B, who's cheaper and scores more.
  • G (£7m, 7) is beaten by B and by C.
  • H (£9m, 10) is beaten by D.
  • I (£3m, 2) is beaten by A.
  • J (£14m, 13) is beaten by E.

That leaves A, B, C, D and E: the Pareto front. Nobody who has to choose a striker should pick one of the other five, whether they care mostly about goals, mostly about money, or anything in between.

Up and to the left is better: more goals for less money. The green line joins the front, the players nobody beats. The faded players are beaten by someone on it. A and C are on the front, but, as the next section shows, no price per goal ever makes either of them the pick.

Choosing on the front

To pick one player from the front, you need to say how much a goal is worth to you. If a goal is worth some amount in £m, each player's value to the club is

$$\text{value} = \text{worth of a goal} \times \text{goals} - \text{fee}$$

In plain football

  • Worth of a goal is what the club would pay for one more goal a season. Part 1 did the same thing in points: 0.65 points for each goal scored.
  • Sign the player with the highest value, or nobody if every value is below zero.
  • This is a weighted score. The weight is how much a goal counts against a pound.

The answer moves along the front as the weight changes:

A goal is worth Sign
under £0.57m nobody
£0.57m to £0.80m B
£0.80m to £1.33m D
over £1.33m E

At £1m a goal, D is the pick: 12 goals for £8m. A club that values goals more highly goes for E, and a careful one for B. A player off the front never comes top, at any price. That's what being beaten means.

The catch: A and C

A and C are on the front, yet no price per goal ever makes either of them the pick. Look at C. Moving from B to C costs £2m for one extra goal. Moving on from C to D costs £2m more and brings four. So at any price where C beats B, D beats C by more. A has the same problem against B and signing nobody: B's goals come cheaper per goal than A's.

They're not bad options. Nobody beats them on both measures. They sit in a dent in the front, below the straight line between their neighbours, and a weighted score can only ever pick a point on the outside of the front, never one in a dent.

A budget brings them back. With £3m to spend, the most goals you can buy are A's 3. With £7m, it's C's 8. A rule like that is the kind of constraint from Parts 1 and 2, and constraints can pick points a weighted score never would. So turning everything into a single number isn't always enough to find the right player. Sometimes the question is better posed as "the most goals we can get for £7m".

The real thing: 2025/26 at both ends

The same idea works on teams. Here's the 2025/26 Scottish Premiership, judged at both ends: goals scored (more is better) and goals conceded (fewer is better).

Club (points) Scored Conceded
Celtic (82) 73 41
Hearts (80) 67 34
Rangers (72) 76 43
Motherwell (61) 59 36
Hibernian (57) 58 44
Falkirk (49) 50 62
Dundee United (45) 49 60
Dundee (42) 42 61
Kilmarnock (40) 50 68
Aberdeen (40) 40 55
St Mirren (34) 30 55
Livingston (21) 40 75

Only three of the twelve are on the front, in bold:

  • Rangers scored the most (76).
  • Hearts conceded the fewest (34).
  • Celtic did neither, but nobody did better at both ends at once: Rangers scored more but conceded more, and Hearts conceded fewer but scored fewer.

The other nine were beaten at both ends by someone. Motherwell had the second-best defence (36), but Hearts both scored more and let in fewer.

The champions were Celtic, who led the league in neither column. And Rangers, on the front with the most goals, finished third, ten points behind. The front says who wasn't outplayed at both ends. Points also depend on when the goals come, which is why goals and points don't always line up.

Why it matters

Most football decisions have more than one measure: a signing's goals, fee, wages and age; a tactic's attack and defence; a squad's quality now and resale value later. The Pareto front is the honest first step. It clears out the options nobody should choose before anyone argues about weights. Then, when a choice is made, it shows what the choice gave up, and it warns that a weighted score can miss good options a budget would find. Part 4 of this series turns to linear programming, where there are many more players and many more rules, and checking them one by one stops being possible.

Limitations

  • The shortlist is made up. Real goal estimates are uncertain, and two scouts' fronts could differ.
  • Two measures is a simplification. With three or more (age, wages, injury record), more players end up on the front, because there are more ways not to be beaten. The front says who to drop, not who to sign.
  • Goals scored and conceded aren't separate skills. Teams that dominate the ball tend to do well at both ends, and the 2025/26 table is one season. Some of the gaps are luck.
  • Points deductions aren't in the data. The points shown are worked out from results alone.

Try it yourself

Find the dent: how many goals would C need before some price per goal made him the pick? (Try 9, then 10, in the code below.) Then pick a recent season from your own league and find which teams were on the front at both ends. Was the champion one of them?

Reproduce the analysis

The shortlist part needs nothing. For the real table, download the 2025/26 Scottish Premiership file (SC0) from football-data.co.uk and save it as SC0_2526.csv; it isn't rehosted on this site. Then:

Show the Python64 lines, ready to copy and run.
import csv

# Made up: a shortlist of strikers, each with a fee (£m) and the goals he'd add in a season.
strikers = {"A": (2, 3), "B": (4, 7), "C": (6, 8), "D": (8, 12), "E": (12, 15),
            "F": (5, 5), "G": (7, 7), "H": (9, 10), "I": (3, 2), "J": (14, 13)}


def beats(x, y, better):
    """x beats y if it's at least as good on every measure and strictly better on one."""
    return all(b(a, c) or a == c for a, c, b in zip(x, y, better)) and x != y


def front(options, better):
    """The Pareto front: every option nobody beats."""
    return [k for k, v in options.items() if not any(beats(w, v, better) for w in options.values())]


cheaper, more = (lambda a, b: a < b), (lambda a, b: a > b)
best = front(strikers, (cheaper, more))
print("Shortlist front:", ", ".join(f"{k} (£{strikers[k][0]}m, {strikers[k][1]} goals)" for k in best))
for k in sorted(set(strikers) - set(best)):
    by = [j for j in strikers if beats(strikers[j], strikers[k], (cheaper, more))]
    print(f"  {k} is beaten by {', '.join(by)}")


def pick(worth):
    """The best signing if a goal is worth `worth` £m: goals × worth − fee, or nobody if none is worth it."""
    k = max(strikers, key=lambda k: worth * strikers[k][1] - strikers[k][0])
    return k if worth * strikers[k][1] - strikers[k][0] > 0 else "nobody"


print()
for worth in [0.5, 0.75, 1.0, 1.5, 2.0]:
    print(f"A goal worth £{worth}m: sign {pick(worth)}")
runs = []   # try every value from £0.001m to £5m a goal and note where the pick changes
for w in range(1, 5001):
    k = pick(w / 1000)
    if not runs or runs[-1][0] != k:
        runs.append([k, w / 1000, w / 1000])
    runs[-1][2] = w / 1000
print("Pick by value of a goal:", "; ".join(f"{k} £{a:.3f}m to £{b:.3f}m" for k, a, b in runs))
print("On the front but never picked:", ", ".join(k for k in best if k not in {r[0] for r in runs}))
for budget in [3, 7]:   # a budget instead of a price per goal: the most goals you can afford
    k = max((k for k in strikers if strikers[k][0] <= budget), key=lambda k: strikers[k][1])
    print(f"Most goals for £{budget}m or less: {k}")

# Real: the 2025/26 Scottish Premiership, goals scored (more is better) and conceded (fewer is better).
# Needs SC0_2526.csv from football-data.co.uk (see above).
table = {}
with open("SC0_2526.csv", encoding="latin-1") as f:
    for r in csv.DictReader(f):
        if r.get("FTR") not in ("H", "D", "A"):
            continue
        for team, scored, conceded in ((r["HomeTeam"], int(r["FTHG"]), int(r["FTAG"])),
                                       (r["AwayTeam"], int(r["FTAG"]), int(r["FTHG"]))):
            gf, ga, pts = table.get(team, (0, 0, 0))
            table[team] = (gf + scored, ga + conceded, pts + 3 * (scored > conceded) + (scored == conceded))
ends = {k: v[:2] for k, v in table.items()}
print()
for k, (gf, ga, pts) in sorted(table.items(), key=lambda kv: -kv[1][2]):
    print(f"  {k:<14} scored {gf:3}  conceded {ga:3}  points {pts:3}")
print("2025/26 front (scored, conceded, points):",
      ", ".join(f"{k} ({table[k][0]}, {table[k][1]}, {table[k][2]})" for k in front(ends, (more, cheaper))))
print("Champions:", max(table, key=lambda k: table[k][2]))

It prints:

Show the Text31 lines, ready to copy and run.
Shortlist front: A (£2m, 3 goals), B (£4m, 7 goals), C (£6m, 8 goals), D (£8m, 12 goals), E (£12m, 15 goals)
  F is beaten by B
  G is beaten by B, C
  H is beaten by D
  I is beaten by A
  J is beaten by E

A goal worth £0.5m: sign nobody
A goal worth £0.75m: sign B
A goal worth £1.0m: sign D
A goal worth £1.5m: sign E
A goal worth £2.0m: sign E
Pick by value of a goal: nobody £0.001m to £0.571m; B £0.572m to £0.799m; D £0.800m to £1.333m; E £1.334m to £5.000m
On the front but never picked: A, C
Most goals for £3m or less: A
Most goals for £7m or less: C

  Celtic         scored  73  conceded  41  points  82
  Hearts         scored  67  conceded  34  points  80
  Rangers        scored  76  conceded  43  points  72
  Motherwell     scored  59  conceded  36  points  61
  Hibernian      scored  58  conceded  44  points  57
  Falkirk        scored  50  conceded  62  points  49
  Dundee United  scored  49  conceded  60  points  45
  Dundee         scored  42  conceded  61  points  42
  Kilmarnock     scored  50  conceded  68  points  40
  Aberdeen       scored  40  conceded  55  points  40
  St Mirren      scored  30  conceded  55  points  34
  Livingston     scored  40  conceded  75  points  21
2025/26 front (scored, conceded, points): Rangers (76, 43, 72), Celtic (73, 41, 82), Hearts (67, 34, 80)
Champions: Celtic

The search over prices per goal tries every value in steps of £0.001m, so the switch points it finds are within £0.001m of the exact ones: £4m ÷ 7 goals, £0.80m and £4m ÷ 3 goals.

Further reading

Get new pieces by email

An email when something new is published, and the occasional update. Unsubscribe in one click. How your email is used.