Implementation of direct method from The Perron-Frobenius Theorem and the Ranking of Football Teams tested against 2023 NFL regular season results.

%load_ext watermark

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import networkx as nx
from numpy.linalg import eig

np.set_printoptions(suppress=True, precision=8)
pd.options.mode.chained_assignment = None
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)

# ------------------------------------------------------------------------------

rankings_url = "https://gist.githubusercontent.com/jtrive84/c49f9a40879b8b0fb03fb6127b20db5e/raw/d5582c8df15aa1b5aa629e32af2007b4ed769eaf/NFL-2023-Standings.csv"
adj_matrix_url = "https://gist.githubusercontent.com/jtrive84/83f50badadfcc06230f627327868a2c1/raw/75e8bde37910ea96b253a25b6a555d6387d027a9/NFL-2023.csv"

# ------------------------------------------------------------------------------

%watermark --python --conda --hostname --machine --iversions --author="jtrive84" 
Author: jtrive84

Python implementation: CPython
Python version       : 3.11.7
IPython version      : 8.15.0

conda environment: py311

Compiler    : GCC 12.3.0
OS          : Linux
Release     : 5.10.102.1-microsoft-standard-WSL2
Machine     : x86_64
Processor   : x86_64
CPU cores   : 16
Architecture: 64bit

Hostname: jdtpc

matplotlib: 3.8.2
numpy     : 1.26.2
networkx  : 3.2.1
pandas    : 2.1.4

dfranks contains actual 2023 regular season rankings posted on NFL.com.


dfranks1 = pd.read_csv(rankings_url).rename({"rank": "actual_rank"}, axis=1)

dfranks1.head(32)
team wins losses actual_rank
0 Ravens 13 4 1
1 Cowboys 12 5 2
2 Lions 12 5 3
3 49ers 12 5 4
4 Bills 11 6 5
5 Browns 11 6 6
6 Chiefs 11 6 7
7 Dolphins 11 6 8
8 Eagles 11 6 9
9 Texans 10 7 10
10 Rams 10 7 11
11 Steelers 10 7 12
12 Bengals 9 8 13
13 Packers 9 8 14
14 Colts 9 8 15
15 Jaguars 9 8 16
16 Saints 9 8 17
17 Seahawks 9 8 18
18 Buccaneers 9 8 19
19 Broncos 8 9 20
20 Raiders 8 9 21
21 Falcons 7 10 22
22 Bears 7 10 23
23 Vikings 7 10 24
24 Jets 7 10 25
25 Giants 6 11 26
26 Titans 6 11 27
27 Chargers 5 12 28
28 Cardinals 4 13 29
29 Patriots 4 13 30
30 Commanders 4 13 31
31 Panthers 2 15 32

dfadj represents the adjacency matrix. If a team in row i defeated a team in column j, then dfadj.loc[i, j] += 1.


dfadj = pd.read_csv(adj_matrix_url).fillna(0)

dfadj.head(32)
Unnamed: 0 49ers Bears Bengals Bills Broncos Browns Buccaneers Cardinals Chargers Chiefs Colts Commanders Cowboys Dolphins Eagles Falcons Giants Jaguars Jets Lions Packers Panthers Patriots Raiders Rams Ravens Saints Seahawks Steelers Texans Titans Vikings
0 49ers 0.0 0.0 0.0 0.0 0.0 0.0 1.0 2.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 2.0 1.0 0.0 0.0 0.0
1 Bears 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
2 Bengals 1.0 0.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0
3 Bills 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0 1.0 2.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 Broncos 0.0 1.0 0.0 1.0 0.0 1.0 0.0 0.0 2.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
5 Browns 1.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 0.0
6 Buccaneers 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 2.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 1.0
7 Cardinals 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0
8 Chargers 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
9 Chiefs 0.0 1.0 1.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
10 Colts 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 0.0 1.0 0.0 0.0 1.0 1.0 2.0 0.0
11 Commanders 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
12 Cowboys 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 2.0 0.0 0.0 1.0 0.0 2.0 0.0 1.0 1.0 0.0 1.0 1.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
13 Dolphins 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 1.0 0.0 2.0 0.0 0.0 1.0 2.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
14 Eagles 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 2.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
15 Falcons 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0
16 Giants 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 2.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
17 Jaguars 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 2.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 1.0 1.0 0.0
18 Jets 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0
19 Lions 0.0 1.0 0.0 0.0 1.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 2.0
20 Packers 0.0 2.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0
21 Panthers 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0
22 Patriots 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0
23 Raiders 0.0 0.0 0.0 0.0 2.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
24 Rams 1.0 0.0 0.0 0.0 0.0 1.0 0.0 2.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 2.0 0.0 0.0 0.0 0.0
25 Ravens 1.0 0.0 2.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0
26 Saints 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 2.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
27 Seahawks 0.0 0.0 0.0 0.0 0.0 1.0 0.0 2.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
28 Steelers 0.0 0.0 2.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 2.0 0.0 1.0 0.0 0.0 1.0 0.0
29 Texans 0.0 0.0 1.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 2.0 0.0
30 Titans 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31 Vikings 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0

# Create adjacency matrix as Numpy array. 
team_names = dfadj["Unnamed: 0"].values
A = dfadj.drop("Unnamed: 0", axis=1).values

G = nx.from_numpy_array(A)

fig, ax = plt.subplots(1, 1, figsize=(8, 5), tight_layout=True)
ax.set_title(
    "2023 NFL Regular Season", 
    color="#000000", loc="center", weight="normal", fontsize=10
    )
nx.draw_networkx(
    G, node_color="#E02C70", node_size=200, alpha=.90, 
    ax=ax, with_labels=True, pos=nx.spring_layout(G, seed=516)
    )

Compute eigendecomposition of A, extracting the eigenvector associated with the largest eigenvalue (eigenvectors are columns).

e_vec1.real
array([-0.24746344, -0.10597567, -0.22708834, -0.19757815, -0.1630838 ,
       -0.26140419, -0.11020259, -0.10249443, -0.08377124, -0.19741972,
       -0.19867474, -0.06382556, -0.19079771, -0.17116368, -0.18082839,
       -0.10418508, -0.09078209, -0.2015062 , -0.13902082, -0.20022598,
       -0.15959943, -0.04537413, -0.10569893, -0.14799306, -0.20157591,
       -0.32553727, -0.12324484, -0.15561815, -0.287711  , -0.23382507,
       -0.11183296, -0.12535713])
e_vecs[:, e_val1_indx]
array([-0.24746344+0.j, -0.10597567+0.j, -0.22708834+0.j, -0.19757815+0.j,
       -0.1630838 +0.j, -0.26140419+0.j, -0.11020259+0.j, -0.10249443+0.j,
       -0.08377124+0.j, -0.19741972+0.j, -0.19867474+0.j, -0.06382556+0.j,
       -0.19079771+0.j, -0.17116368+0.j, -0.18082839+0.j, -0.10418508+0.j,
       -0.09078209+0.j, -0.2015062 +0.j, -0.13902082+0.j, -0.20022598+0.j,
       -0.15959943+0.j, -0.04537413+0.j, -0.10569893+0.j, -0.14799306+0.j,
       -0.20157591+0.j, -0.32553727+0.j, -0.12324484+0.j, -0.15561815+0.j,
       -0.287711  +0.j, -0.23382507+0.j, -0.11183296+0.j, -0.12535713+0.j])

e_vals, e_vecs = eig(A)

# Get index of largest eigenvalue.
e_val1_indx = np.argmax(e_vals)
e_vec1 = e_vecs[:, e_val1_indx]

# Get rank of each value in e_vec1.
rankings2 = np.argsort(np.abs(e_vec1.real))[::-1]
dfranks2 = (
    pd.DataFrame(enumerate(team_names[rankings2], start=1))
    .rename({0: "predicted_rank", 1: "team"}, axis=1)
    )   

# Merge dfranks1 with dfranks2. 
dfranks = dfranks1.merge(dfranks2, on="team", how="inner")

dfranks.head(32)
team wins losses actual_rank predicted_rank
0 Ravens 13 4 1 1
1 Cowboys 12 5 2 13
2 Lions 12 5 3 9
3 49ers 12 5 4 4
4 Bills 11 6 5 11
5 Browns 11 6 6 3
6 Chiefs 11 6 7 12
7 Dolphins 11 6 8 15
8 Eagles 11 6 9 14
9 Texans 10 7 10 5
10 Rams 10 7 11 7
11 Steelers 10 7 12 2
12 Bengals 9 8 13 6
13 Packers 9 8 14 17
14 Colts 9 8 15 10
15 Jaguars 9 8 16 8
16 Saints 9 8 17 22
17 Seahawks 9 8 18 18
18 Buccaneers 9 8 19 24
19 Broncos 8 9 20 16
20 Raiders 8 9 21 19
21 Falcons 7 10 22 27
22 Bears 7 10 23 25
23 Vikings 7 10 24 21
24 Jets 7 10 25 20
25 Giants 6 11 26 29
26 Titans 6 11 27 23
27 Chargers 5 12 28 30
28 Cardinals 4 13 29 28
29 Patriots 4 13 30 26
30 Commanders 4 13 31 31
31 Panthers 2 15 32 32

dfranks[["actual_rank", "predicted_rank"]].corr(method="spearman")
actual_rank predicted_rank
actual_rank 1.000000 0.859238
predicted_rank 0.859238 1.000000

dfranks[["actual_rank", "predicted_rank"]].corr(method="kendall")
actual_rank predicted_rank
actual_rank 1.000000 0.677419
predicted_rank 0.677419 1.000000

It is interesting to see the Cowboys and Lions with such low ranks. Maybe this is a consequence of strength of schedule, or a limitation of using the direct method which uses a simple binary encoding. The method seems to have done a better job ranking the bottom half of teams.