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 |