Python Sports Modelling

Adding Margin Of Victory To NFL Elo Ratings

Introduction

Every game provides new evidence about the relative strengths of two teams. The purpose of an Elo rating system is to incorporate that evidence gradually, allowing ratings to evolve as more games are played.

In the previous articles we built a baseline NFL Elo model, validated its behaviour, and introduced home advantage as the first contextual adjustment. At that point the model recognised where games were played, but it still treated every victory as equally informative.

Whether a team won by a single point or by thirty points, the Elo update was identical. The winning team gained the same number of rating points, while the losing team lost the same amount.

That naturally raises another important modelling question:

"Does every result provide the same amount of evidence about team strength?"

If the answer is yes, then the existing Elo model is already making appropriate use of the available information. If the answer is no, then the final score margin may contain additional evidence that should influence how quickly ratings change.

The challenge is deciding how to use that information appropriately. Convincing victories should generally influence ratings more than narrow wins, but the model must also avoid overreacting to unusually large scorelines or one-off performances.

In this article we investigate how margin of victory can be incorporated into the Elo framework while preserving the stability, simplicity and interpretability of the rating system.

Why Not Treat Every Win Equally?

The baseline Elo model assumes that every victory provides the same amount of evidence about the relative strengths of two teams.

That assumption keeps the model simple, transparent and remarkably effective. However, it also ignores information contained in the final score. A one-point victory and a comfortable multi-score win are both successful outcomes, yet they may not provide the same evidence about the difference in strength between the two teams.

This does not mean that larger winning margins should produce proportionally larger Elo updates. Football scores contain natural variation arising from late scores, tactical decisions, game management and many other factors that can exaggerate the final margin without reflecting the true difference in team strength.

The objective is therefore not to reward large victories as much as possible. The objective is to determine whether decisive results should influence ratings slightly more than closely contested games, while ensuring that a single exceptional result never dominates the model.

"Good predictive models recognise additional evidence without allowing individual observations to dominate."

Designing a Margin of Victory Adjustment

Having established that the winning margin may contain additional evidence about team strength, the next question is how that evidence should influence the Elo update.

One obvious approach would be to make rating changes directly proportional to the final score margin. Under that approach, a twenty-point victory would produce roughly twice the rating adjustment of a ten-point victory.

Although appealing, that is rarely a good modelling choice. Score margins contain useful information, but they also contain noise. Not every additional point reflects a genuine increase in the difference between two teams. As winning margins become larger, each additional point tends to provide progressively less new evidence about their underlying strengths.

If Elo updates increased linearly with the winning margin, unusually large scorelines could dominate the rating system, causing excessive changes after individual games and reducing the long-term stability of the model.

"More evidence should produce a larger update, but not necessarily in direct proportion to the score margin."

What Should a Good Margin Adjustment Do?

Before choosing a mathematical formula, it helps to define the behaviour we want from the model.

A good margin of victory adjustment should:

  • Increase rating updates after convincing victories.
  • Leave closely contested games behaving similarly to the baseline Elo model.
  • Apply progressively smaller increases as winning margins become larger.
  • Prevent exceptional scorelines from dominating the ratings.

These requirements immediately rule out a simple linear adjustment. Instead, we need a function that increases quickly for small winning margins before gradually flattening as the margin becomes larger.

A logarithmic transformation has exactly these characteristics, making it a natural candidate for scaling the Elo update.

Using a Logarithmic Multiplier

Having established that the Elo update should increase with the winning margin—but by progressively smaller amounts—we now need a mathematical function that behaves in that way.

A logarithm is well suited to this problem because it grows rapidly for small values and then increases more slowly as the input becomes larger.

In practical terms, this means the difference between winning by one point and winning by seven points has a much greater influence than the difference between winning by twenty-eight points and winning by thirty-four points.

That behaviour closely matches the modelling objective. Early increases in the winning margin provide useful additional evidence, but extremely large victories should not produce proportionally larger rating changes.

"The first few extra points tell us much more than the last few."

The Margin Multiplier

We calculate the margin of victory multiplier using the following equation:


margin_multiplier = log(margin + 1)

Adding one ensures that the calculation remains valid for every possible winning margin. A one-point victory therefore produces a positive adjustment, while drawn games continue to use the standard Elo update without any additional scaling.

The multiplier is then applied directly to the normal Elo update. Small winning margins produce values close to one, while larger margins increase the update more gradually as the logarithm begins to flatten.

The result is a rating system that responds more strongly to convincing victories while remaining resistant to unusually large scorelines.

"Margin of victory should sharpen the signal, not overpower it."
Interactive Demo

Try It Yourself: How Winning Margin Changes Elo Updates

Adjust the ratings and winning margin to see how a logarithmic multiplier increases Elo updates without letting large scorelines dominate.

Expected Favourite Even Match
Rating Difference +0.0 Elo
Expected Win Probability
Team A
50.0%
Team B
50.0%
Standard Elo Update +10.0 No margin adjustment
With Margin of Victory +25.6 Multiplier: 2.56×
Margin Multiplier
Winning margin: 12 log(12 + 1) 2.56×
Live Calculation Standard update: 20 × (1 − 0.500) = +10.0
With margin: +10.0 × 2.56 = +25.6
Margin Multiplier Curve
1 point 10 20 30 40 points
Why did this happen?

This margin provides useful extra evidence, so the Elo update increases, but not in direct proportion to the scoreline.

Implementing Margin of Victory in Python

With the modelling framework established, incorporating margin of victory into the Elo model requires only a small extension to the implementation developed in the previous articles.

The overall structure of the algorithm remains unchanged. Games are still processed chronologically, expected probabilities are calculated before each game, and both teams' ratings are updated immediately after the final result.

The only difference is that the size of the Elo update is now scaled using the margin of victory multiplier. Close games continue to behave much like the baseline model, while more convincing victories produce proportionally larger—but carefully controlled—rating adjustments.

"The objective is not to redesign Elo. The objective is to make better use of the evidence contained in each result."

The complete implementation is shown below. Compared with the previous article, only a small number of lines have been added.


import polars as pl
import math
# ------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------

INPUT_FILE = "nfl_games_data.csv"
GAME_LEVEL_OUTPUT_CSV = "nfl_elo_game_level_home_mov.csv"
GAME_LEVEL_OUTPUT_PARQUET = "nfl_elo_game_level_home_mov.pq"

BASE_ELO = 1500.0
K = 20.0
HOME_ADVANTAGE = 65.0

# ------------------------------------------------------------------
# Elo helper functions
# ------------------------------------------------------------------

def expected_score(r_a, r_b):
    return 1.0 / (1.0 + 10 ** ((r_b - r_a) / 400.0))

def margin_multiplier(margin):
    # Logarithmic scaling of the winning margin
    return math.log(margin + 1.0)

# ------------------------------------------------------------------
# Load and prepare the historical results
# ------------------------------------------------------------------

df = pl.read_csv(INPUT_FILE)

df = df.with_columns(
    pl.when(pl.col("neutral").is_null())
      .then(False)
      .otherwise(pl.col("neutral"))
      .cast(pl.Boolean)
      .alias("neutral")
)

# ------------------------------------------------------------------
# Process games in chronological order
# ------------------------------------------------------------------

df = df.sort(["date_timestamp", "match_id"])

elo = {}
rows = []

for row in df.iter_rows(named=True):

    home = row["home_team"]
    away = row["away_team"]

    hs = row["home_score"]
    aas = row["away_score"]

    if hs is None or aas is None:
        continue

    r_home = elo.get(home, BASE_ELO)
    r_away = elo.get(away, BASE_ELO)

    # --------------------------------------------------------------
    # Apply home advantage when calculating expectations
    # --------------------------------------------------------------

    if row["neutral"]:
        r_home_adj = r_home
    else:
        r_home_adj = r_home + HOME_ADVANTAGE

    e_home = expected_score(r_home_adj, r_away)
    e_away = 1.0 - e_home

    # --------------------------------------------------------------
    # Convert the final score into Elo outcomes
    # --------------------------------------------------------------

    if hs > aas:
        a_home, a_away = 1.0, 0.0
        margin = hs - aas
    elif hs < aas:
        a_home, a_away = 0.0, 1.0
        margin = aas - hs
    else:
        a_home, a_away = 0.5, 0.5
        margin = 0

    # --------------------------------------------------------------
    # Calculate the margin-of-victory multiplier
    # --------------------------------------------------------------

    mov_mult = 1.0 if margin == 0 else margin_multiplier(margin)

    # --------------------------------------------------------------
    # Scale the standard Elo update
    # --------------------------------------------------------------

    new_home = r_home + K * mov_mult * (a_home - e_home)
    new_away = r_away + K * mov_mult * (a_away - e_away)

    elo[home] = new_home
    elo[away] = new_away

    row.update({
        "home_elo_pre": r_home,
        "home_elo_post": new_home,
        "away_elo_pre": r_away,
        "away_elo_post": new_away,
        "margin": margin,
        "mov_multiplier": mov_mult,
    })

    rows.append(row)

# ------------------------------------------------------------------
# Create the final game-level Elo dataset
# ------------------------------------------------------------------

df_elo = pl.DataFrame(rows)


# ------------------------------------------------------------------
# Save the updated Elo dataset
# ------------------------------------------------------------------

df_elo.write_csv(GAME_LEVEL_OUTPUT_CSV)
df_elo.write_parquet(GAME_LEVEL_OUTPUT_PARQUET)

What Changed?

Although the complete script looks very similar to the previous implementation, only four conceptual changes have been introduced.

  • Import the math module.
    This provides the logarithm used to calculate the margin of victory multiplier.
  • Define the margin multiplier.
    Rather than using the raw winning margin directly, a logarithmic transformation produces larger Elo updates with diminishing returns as score margins increase.
  • Calculate the winning margin.
    After each game, the absolute difference between the two final scores is used to determine the strength of the adjustment.
  • Scale the Elo update.
    The expected probabilities remain unchanged. Only the size of the Elo rating update is multiplied by the margin factor before the new ratings are calculated.

Every other part of the algorithm remains unchanged. Home advantage is still applied only when calculating expected probabilities, games are processed sequentially, and the Elo ratings continue to represent long-term team strength.

"Well-designed predictive models usually evolve through small, carefully justified improvements rather than complete redesigns."

Understanding the Effect of Margin of Victory

Incorporating margin of victory does not fundamentally change the Elo rating system. Teams are still rated using the same underlying framework, games are still processed chronologically, and ratings continue to evolve one result at a time.

What changes is how strongly each result influences the next rating update. Narrow victories continue to produce updates very similar to the baseline model, while more convincing wins contribute slightly more evidence about the relative strengths of the two teams.

Because the adjustment uses a logarithmic multiplier, that additional influence grows more slowly as winning margins become larger. This allows the model to recognise dominant performances without allowing exceptional scorelines to overwhelm the ratings.

"Good predictive models learn more from stronger evidence, but they never allow a single observation to dominate."

What Has Changed?

Compared with the baseline Elo model, the rating updates now incorporate three separate sources of evidence:

  • Who won the game.
  • Where the game was played.
  • How convincing the victory was.

Importantly, the meaning of the Elo ratings has not changed. They continue to represent an estimate of each team's underlying strength. Margin of victory simply influences how quickly the ratings respond to new evidence.

Key Takeaways

  • Not every game provides the same amount of evidence about team strength.
  • Winning margin can improve Elo updates without changing what the ratings represent.
  • A logarithmic multiplier provides larger updates with diminishing returns.
  • Large victories receive additional weight without allowing exceptional scorelines to dominate the model.
  • Small, carefully justified improvements can substantially strengthen a predictive model over time.

Next Steps

After four articles, our Elo model now incorporates three important sources of evidence from every completed game:

  • The game result.
  • Home advantage.
  • Margin of victory.

One important challenge still remains. Team strength evolves from season to season as rosters change, coaches change and organisations develop over time. A rating system should recognise those changes without discarding useful historical information every time a new season begins.

That naturally leads to our next modelling question:

"How much information from previous seasons should an Elo rating retain?"

In the next article, Adding Seasonal Carryover to NFL Elo Ratings , we introduce seasonal carryover and investigate how ratings can transition smoothly between seasons while remaining responsive to long-term changes in team strength.

Continue the Series