Skip to content

How many points is a goal worth?

Goal difference explains 93% of the points a Scottish side takes, at about 0.63 points per goal. Some teams beat their goals by four wins' worth in a season, but it barely carries into the next one. 572 team-seasons show why.

Intermediate

Contents

The research question

Every manager knows goals win matches. But how many points is one more goal actually worth over a season? And some teams finish with far more points than their goals suggest, winning the close ones, or far fewer. Is that a skill, or is it luck?

The dataset

Every Scottish Premiership and Championship season from 2000/01 to 2025/26: 572 team-seasons, each a team's wins, draws, goals for, goals against and points. A handful of stray rows in the files, teams with only a match or two in a division, are left out by keeping team-seasons with at least 20 matches.

The method

Points per goal. Fit a straight line through points per game against goal difference per game, across all 572 team-seasons. The slope is how many points each goal of goal difference is worth.

Pythagorean expectation. A straight line can't respect the fact that no team takes fewer than 0 or more than 3 points a game. Analysts in baseball, basketball and football use a curve instead, named for its resemblance to Pythagoras' theorem:

$$\text{expected share of results} = \frac{\text{GF}^{k}}{\text{GF}^{k} + \text{GA}^{k}}$$

In plain football

  • GF and GA are goals for and goals against over the season.
  • Share of results counts a win as 1 and a draw as a half, divided by games played. An average side sits at 50%.
  • k controls how steeply goals turn into results. It's fitted to the data: the value that makes the formula match real seasons best.

Beating your goals. A team's actual share of results minus its expected share, times games played, is how many wins' worth it finished above or below its goals: a win instead of a defeat counts as one, a draw instead of a defeat as a half.

Luck or skill? Take every team that played consecutive seasons in the same division, 449 pairs, and check whether beating its goals in one season goes with beating them in the next. A skill should carry over; luck shouldn't.

Results

A goal is worth about 0.63 points

Team-seasons grouped by goal difference per game; each dot is a group's average points per game, larger for bigger groups. They sit close to the dashed line: every goal of goal difference is worth about 0.63 points.

$$\begin{aligned} &\text{points a game} \\ &= 1.37 + 0.63 \times \text{goal difference a game} \end{aligned}$$

In plain football

  • 1.37 is what a team with a goal difference of zero takes per game: an average side.
  • 0.63 is the value of each goal of goal difference, and because both sides of the equation are per game, it's per goal over a season too.
  • So ten more goals of goal difference is worth about six points, the difference between a mid-table finish and a European place in many seasons.

Goal difference alone explains 93% of the variation in points per game. Split into its two halves, each goal scored is worth about 0.65 points and each goal conceded costs about 0.60: a goal saved is worth nearly as much as a goal scored.

The Pythagorean curve

The best-fitting exponent is k = 1.2, and with it the formula predicts a team's share of results to within about 3.8 percentage points typically: over a 38-game season, roughly a win and a half. The teams that beat it by most:

Team and season Points GD Wins above
Motherwell, 2013/14 70 +4 +4.3
St Mirren, 2007/08 41 −28 +4.3
St Johnstone, 2010/11 44 −20 +4.3
Celtic, 2016/17 106 +81 +3.7
Ross County, 2002/03* 35 −4 −4.0
Airdrie Utd, 2005/06* 45 +14 −4.0

* Championship. The rest: Premiership.

Motherwell's 2013/14 is the standout: 70 points from a goal difference of just +4, second in the Premiership, about four wins more than their goals deserved. They won lots of close games. At the other end, Airdrie United in 2005/06 had a goal difference of +14 and took just 45 points.

Beating your goals is mostly luck

How much of each carries into the next season. A team's goal difference is strongly repeated. How far it beat its goals barely is.

A team's goal difference is strongly repeated from one season to the next: a correlation of 0.75. How far it beat its goal difference is not: 0.10, only just clear of zero, with a range of ±0.09. Winning the close games in one season tells you very little about the next. If there's any skill in it, it's small; most of it is the luck of which way the tight matches went.

That's the same conclusion, reached a different way, as does the league table never lie?: the table carries a layer of luck on top of quality. Goal difference is closer to the quality underneath.

A warning about small samples

A single season's over- or under-performance is built on a handful of close matches: a +4 means about four results going the other way. The luck test rests on 449 pairs of seasons, which is enough to say the carry-over is small but not to pin down exactly how small; it could be anywhere from nothing to about 0.2.

Limitations

  • Two divisions. The Premiership and Championship play different numbers of games and have different spreads of quality; the per-game measures put them on the same footing, but the fit is shared.
  • Draws counted as half. The Pythagorean share treats a draw as half a win; in points, a draw is worth a third of a win.
  • Points deductions aren't in the data, so a few team-seasons' points differ from the official tables.
  • Goals aren't the whole story. Expected goals would separate lucky finishing from genuinely good chances; these files don't include it.

Conclusion

A goal of goal difference is worth about 0.63 points in Scottish football, and goal difference explains 93% of the variation in points. The gaps between points and goals are real enough to matter, four wins' worth at the extremes, but they don't last: a team that beat its goals this season is barely more likely than anyone else to do it again. When judging a team, trust its goal difference over its points.

Reproduce the analysis

The results files are published by football-data.co.uk. Download the Premiership (SC0) and Championship (SC1) files for each season from 2000/01 to 2025/26 and save each under its own name, such as SC0_2425.csv and SC1_2425.csv; they aren't rehosted on this site. Then:

import csv
from collections import Counter
from math import sqrt

names = [f"{y % 100:02d}{(y + 1) % 100:02d}" for y in range(2000, 2026)]
seasons = {}  # (season, division, team) -> wins, draws, goals for, goals against, games, points
for s in names:
    for div in ("SC0", "SC1"):  # Premiership and Championship
        tally = {}
        with open(f"{div}_{s}.csv", encoding="latin-1") as f:
            for r in csv.DictReader(f):
                if r.get("FTR") not in ("H", "D", "A"):
                    continue
                h, a, x, y = r["HomeTeam"], r["AwayTeam"], int(r["FTHG"]), int(r["FTAG"])
                for team, scored, conceded in ((h, x, y), (a, y, x)):
                    t = tally.setdefault(team, Counter())
                    t["w"] += scored > conceded; t["d"] += scored == conceded
                    t["gf"] += scored; t["ga"] += conceded; t["n"] += 1
                    t["pts"] += 3 * (scored > conceded) + (scored == conceded)
        seasons.update({(s, div, team): t for team, t in tally.items() if t["n"] >= 20})  # drops a few stray rows

def fit(xs, ys):  # straight line through the points: slope, intercept, correlation
    mx, my = sum(xs) / len(xs), sum(ys) / len(ys)
    sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
    sxx, syy = sum((x - mx) ** 2 for x in xs), sum((y - my) ** 2 for y in ys)
    return sxy / sxx, my - sxy / sxx * mx, sxy / sqrt(sxx * syy)

t = list(seasons.values())
slope, start, r = fit([(v["gf"] - v["ga"]) / v["n"] for v in t], [v["pts"] / v["n"] for v in t])
print(f"{len(t)} team-seasons: points a game = {start:.2f} + {slope:.3f} x goal difference a game; explains {r * r:.0%}")

edges = [-9, -1.2, -0.8, -0.4, 0, 0.4, 0.8, 1.2, 1.6, 9]  # average points a game in bands of goal difference a game
for lo, hi in zip(edges, edges[1:]):
    band = [v for v in t if lo < (v["gf"] - v["ga"]) / v["n"] <= hi]
    gd_avg = sum((v["gf"] - v["ga"]) / v["n"] for v in band) / len(band)
    print(f"  goal difference {gd_avg:+.2f} a game ({len(band)} team-seasons): {sum(v['pts'] / v['n'] for v in band) / len(band):.2f} points a game")

# goals scored and goals conceded separately (two-variable least squares)
x1, x2, y = [v["gf"] / v["n"] for v in t], [v["ga"] / v["n"] for v in t], [v["pts"] / v["n"] for v in t]
m1, m2, my = sum(x1) / len(t), sum(x2) / len(t), sum(y) / len(t)
s11, s22 = sum((a - m1) ** 2 for a in x1), sum((b - m2) ** 2 for b in x2)
s12 = sum((a - m1) * (b - m2) for a, b in zip(x1, x2))
s1y, s2y = sum((a - m1) * (c - my) for a, c in zip(x1, y)), sum((b - m2) * (c - my) for b, c in zip(x2, y))
det = s11 * s22 - s12 ** 2
print(f"each goal scored {(s22 * s1y - s12 * s2y) / det:+.2f} points, each goal conceded {(s11 * s2y - s12 * s1y) / det:+.2f}")

# Pythagorean: share of results (a draw counts half) against goals for^k / (goals for^k + goals against^k)
share = lambda v: (v["w"] + v["d"] / 2) / v["n"]
pyth = lambda v, k: v["gf"] ** k / (v["gf"] ** k + v["ga"] ** k)
error, k = min((sum((share(v) - pyth(v, k / 100)) ** 2 for v in t), k / 100) for k in range(80, 250))
print(f"best exponent k = {k}, typical miss {sqrt(error / len(t)):.1%} of results")
beat = {key: (share(v) - pyth(v, k)) * v["n"] for key, v in seasons.items()}  # wins' worth above expectation
ranked = sorted(beat, key=beat.get)
for key in ranked[-5:][::-1] + ranked[:5]:
    v = seasons[key]
    print(f"  20{key[0][:2]}/{key[0][2:]} {key[2]} ({'Premiership' if key[1] == 'SC0' else 'Championship'}): "
          f"{v['pts']} points, goal difference {v['gf'] - v['ga']:+}, {beat[key]:+.1f} wins' worth")

# does beating your goals carry over to next season? same team, same division, consecutive seasons
pairs = [(key, (names[names.index(key[0]) + 1], key[1], key[2])) for key in seasons
         if key[0] != names[-1] and (names[names.index(key[0]) + 1], key[1], key[2]) in seasons]
luck = fit([beat[a] / seasons[a]["n"] for a, _ in pairs], [beat[b] / seasons[b]["n"] for _, b in pairs])[2]
gd = fit([(seasons[a]["gf"] - seasons[a]["ga"]) / seasons[a]["n"] for a, _ in pairs],
         [(seasons[b]["gf"] - seasons[b]["ga"]) / seasons[b]["n"] for _, b in pairs])[2]
print(f"{len(pairs)} pairs of seasons: beating your goals correlates {luck:.2f} with next season (±{1.96 / sqrt(len(pairs)):.2f}); goal difference {gd:.2f}")

Further reading

  • Pythagorean expectation, Wikipedia. The formula's origins in baseball and how it's been adapted for basketball, hockey and American football.
  • Regression toward the mean, Wikipedia. Why teams that overachieve one season tend to come back to earth.
  • Goal difference, Wikipedia. How it's used to separate teams, and its alternatives.

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.