3. Massey’s Method, Offense and Defense

Massey’s Method of rating is specified as a system of linear equations as follows

\(Mr = p\),

where,

  • \(M\) is a \(n\) x \(n\) matrix,

    • each \(M_{ii}\) is the number of games played by the i-th team

    • each \(M_{ij}\) is the negation of games played by the i-th team agains the j-th team

  • \(r\) are the ratings we are trying to estimate, and

  • \(p\) is the point differentials across games played.

We can breakdown \(M\), \(r\) and \(p\) into individual parts [LM12].

We can breakdown \(M\) as follows.

\(M = T - P\),

where

  • \(T\) is a diagonal matrix and each \(T_{ii}\) is the number of games played by the i-th team, and

  • \(P\) is an off-diagonal matrix where \(P_{ij}\) is the number of games played by the i-th team against the j-th team.

We can breakdown \(r\) as follows.

\(r = o + d\),

where,

  • \(o\) is the offensive rating, and

  • \(d\) is the defensive rating.

We can breakdown \(p\) as follows.

\(p = f - a\),

where,

  • \(f\) is the points scored for a team, and

  • \(a\) is the points scored against a team.

With some algebraic manipulation, we can estimate \(d\) and then \(o\) [LM12]. To estimate \(d\), we need to solve the following.

\((T + P)d = Tr - f\)

Once we have \(d\), then \(o\) is computed as

\(o = r - d\).

3.1. NCAAF 2005

Here’s the advanced version of Massey’s Method applied to the ACC (NCAAF) 2005 season.

[1]:
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression

def get_ncaaf():
    return pd.read_csv('./ranking/acc-2005-ncaaf.csv')

def get_teams(df):
    return sorted(list(set(df.t1) | set(df.t2)))

def get_fap(df):
    def get_f(t):
        return df[df.t1 == t].s1.sum() + df[df.t2 == t].s2.sum()

    def get_a(t):
        return -df[df.t1 == t].s2.sum() + -df[df.t2 == t].s1.sum()

    teams = get_teams(df)
    x = pd.DataFrame([{'for': get_f(t), 'against': get_a(t)} for t in teams], index=teams)
    x['differential'] = x['for'] + x['against']
    return x

def get_M(df):
    def get_games_played(t1, t2):
        if t1 == t2:
            return df[(df.t1 == t1) | (df.t2 == t2)].shape[0]
        else:
            q1 = (df.t1 == t1) & (df.t2 == t2)
            q2 = (df.t1 == t2) & (df.t2 == t1)
            q = q1 | q2
            return -df[q].shape[0]

    teams = get_teams(df)
    mat = [[get_games_played(t1, t2) for t2 in teams] for t1 in teams]
    mat = pd.DataFrame(mat, index=teams, columns=teams)
    return mat

def get_MTP(df):
    M = get_M(df)

    teams = get_teams(df)
    T = pd.DataFrame(np.diag(pd.Series(np.diag(M))), index=teams, columns=teams)
    P = T - M
    return M, T, P

def get_r(df):
    M = get_M(df)
    M.iloc[-1,:] = 1

    p = get_fap(df).differential

    model = LinearRegression()
    model.fit(M, p)

    return pd.Series(model.coef_, index=M.index)

def get_ratings(df):
    f = get_fap(df)['for']
    r = get_r(df)
    _, T, P = get_MTP(df)

    X = T + P
    y = T.dot(r) - f

    model = LinearRegression()
    model.fit(X, y)
    d = pd.Series(model.coef_, index=X.index)
    o = r - d

    return pd.DataFrame({
        'r': r,
        'o': o,
        'd': d
    })

def get_rankings(df):
    return pd.DataFrame({c: df[c].sort_values(ascending=False).index for c in df.columns})

Pretty neat. You can see that Miami is in the first spot overall and in terms of offensive, but in terms of defense, VT is in the first spot.

[2]:
get_ratings(get_ncaaf())
[2]:
r o d
Duke -6.8 9.200000 -16.000000
Miami 36.2 29.200000 7.000000
UNC 10.0 8.600000 1.400000
UVA 14.6 15.066667 -0.466667
VT 36.0 27.933333 8.066667
[3]:
get_rankings(get_ratings(get_ncaaf()))
[3]:
r o d
0 Miami Miami VT
1 VT VT Miami
2 UVA UVA UNC
3 UNC Duke UVA
4 Duke UNC Duke

3.2. NBA, 2021

Here’s the method applied to the NBA 2021 season up to Thanksgiving.

[4]:
def get_nba():
    x = pd.read_csv('./nba/2021.csv')\
        .rename(columns={
            'a_team': 't1',
            'h_team': 't2',
            'a_score': 's1',
            'h_score': 's2'})
    x = x[x.preseason == False]\
        .drop(columns=['preseason'])\
        .reset_index(drop=True)
    return x
[5]:
get_ratings(get_nba())
[5]:
r o d
76ers 2.100543 3.144971 -1.044428
Bucks 1.999356 5.413072 -3.413715
Bulls 4.235154 8.977294 -4.742140
Cavaliers 1.429301 -3.493474 4.922775
Celtics 0.579776 1.640658 -1.060882
Clippers 3.792290 -3.760822 7.553112
Grizzlies -4.525625 -0.503283 -4.022343
Hawks 3.338752 11.853671 -8.514919
Heat 6.488263 3.167715 3.320548
Hornets 1.227789 18.738008 -17.510219
Jazz 8.549796 4.839341 3.710455
Kings 0.089514 5.121812 -5.032299
Knicks -0.306992 -3.623988 3.316997
Lakers -3.357641 12.309501 -15.667142
Magic -9.818479 -0.275372 -9.543107
Mavericks -0.688728 -10.191541 9.502812
Nets 4.254646 3.548519 0.706127
Nuggets 0.755934 -7.294869 8.050802
Pacers 3.138730 6.336963 -3.198232
Pelicans -4.916126 7.943461 -12.859587
Pistons -7.552551 -13.197366 5.644815
Raptors 1.055674 2.166579 -1.110905
Rockets -8.788710 -7.039300 -1.749410
Spurs -3.433295 -10.896243 7.462948
Suns 6.744632 7.833603 -1.088972
Thunder -5.150785 -11.740419 6.589633
Timberwolves 1.276979 1.553942 -0.276963
Trail Blazers 2.456361 5.680494 -3.224132
Warriors 12.899850 1.227749 11.672101
Wizards 1.180180 -2.117107 3.297287

It’s interesting to note that the Warriors’ success is driven by their defense and not offense! Steph Curry?!

[6]:
get_rankings(get_ratings(get_nba()))
[6]:
r o d
0 Warriors Hornets Warriors
1 Jazz Lakers Mavericks
2 Suns Hawks Nuggets
3 Heat Bulls Clippers
4 Nets Pelicans Spurs
5 Bulls Suns Thunder
6 Clippers Pacers Pistons
7 Hawks Trail Blazers Cavaliers
8 Pacers Bucks Jazz
9 Trail Blazers Kings Heat
10 76ers Jazz Knicks
11 Bucks Nets Wizards
12 Cavaliers Heat Nets
13 Timberwolves 76ers Timberwolves
14 Hornets Raptors 76ers
15 Wizards Celtics Celtics
16 Raptors Timberwolves Suns
17 Nuggets Warriors Raptors
18 Celtics Magic Rockets
19 Kings Grizzlies Pacers
20 Knicks Wizards Trail Blazers
21 Mavericks Cavaliers Bucks
22 Lakers Knicks Grizzlies
23 Spurs Clippers Bulls
24 Grizzlies Rockets Kings
25 Pelicans Nuggets Hawks
26 Thunder Mavericks Magic
27 Pistons Spurs Pelicans
28 Rockets Thunder Lakers
29 Magic Pistons Hornets

3.3. NFL, 2021

Here’s the method applied to the NFL 2021 season up to Thanksgiving.

[7]:
def get_nfl():
    x = pd.read_csv('./nfl/2021.csv')\
        .rename(columns={
            'team1': 't1',
            'team2': 't2',
            'score1': 's1',
            'score2': 's2'})\
        .drop(columns=['week'])
    x['t1'] = x['t1'].apply(lambda s: s.strip())
    x['t2'] = x['t2'].apply(lambda s: s.strip())

    return x
[8]:
get_ratings(get_nfl())
[8]:
r o d
49ers 1.565063 -1.060366 2.625429
Bears -9.322540 -8.056754 -1.265787
Bengals -0.607977 -1.024311 0.416334
Bills 8.425259 4.682797 3.742462
Broncos -2.431469 -7.665542 5.234073
Browns -2.723871 -0.667741 -2.056130
Buccaneers 6.321022 4.566463 1.754559
Cardinals 8.235687 5.565128 2.670558
Chargers -0.551756 -0.169482 -0.382274
Chiefs 2.706593 1.612262 1.094330
Colts 3.115332 4.487480 -1.372148
Cowboys 5.819441 6.456709 -0.637268
Dolphins -8.305769 -5.904583 -2.401186
Eagles 1.696797 3.164563 -1.467766
Falcons -12.409125 -8.468787 -3.940338
Giants -6.524430 -6.992540 0.468110
Jaguars -10.338981 -8.568697 -1.770285
Jets -14.139647 -6.891844 -7.247803
Lions -12.281915 -7.614925 -4.666989
Packers 1.443518 -0.488621 1.932140
Panthers -1.742287 -3.588010 1.845723
Patriots 6.349719 1.738920 4.610799
Raiders -5.414601 0.159942 -5.574542
Rams 2.472228 1.561124 0.911104
Ravens -1.608072 -2.053706 0.445635
Saints 0.808172 1.026320 -0.218148
Seahawks -0.880815 -5.960129 5.079315
Steelers -3.707054 -4.286576 0.579522
Texans -11.365255 -10.348406 -1.016850
Titans 2.584503 2.925093 -0.340590
Vikings 0.737758 0.032268 0.705490
Washington -3.649900 -3.579373 -0.070528

The Broncos has the best defense, but one of the worst offenses!

[9]:
get_rankings(get_ratings(get_nfl()))
[9]:
r o d
0 Bills Cowboys Broncos
1 Cardinals Cardinals Seahawks
2 Patriots Bills Patriots
3 Buccaneers Buccaneers Bills
4 Cowboys Colts Cardinals
5 Colts Eagles 49ers
6 Chiefs Titans Packers
7 Titans Patriots Panthers
8 Rams Chiefs Buccaneers
9 Eagles Rams Chiefs
10 49ers Saints Rams
11 Packers Raiders Vikings
12 Saints Vikings Steelers
13 Vikings Chargers Giants
14 Chargers Packers Ravens
15 Bengals Browns Bengals
16 Seahawks Bengals Washington
17 Ravens 49ers Saints
18 Panthers Ravens Titans
19 Broncos Washington Chargers
20 Browns Panthers Cowboys
21 Washington Steelers Texans
22 Steelers Dolphins Bears
23 Raiders Seahawks Colts
24 Giants Jets Eagles
25 Dolphins Giants Jaguars
26 Bears Lions Browns
27 Jaguars Broncos Dolphins
28 Texans Bears Falcons
29 Lions Falcons Lions
30 Falcons Jaguars Raiders
31 Jets Texans Jets