NFL Team Rating#
Description: NFL Team Rating problem from the Analytical Decision Modeling course at the Arizona State University.
Tags: educational, quadratic, amplpy, gurobi
Notebook author: Yimin Wang <yimin_wang@asu.edu>, Marcos Dominguez Velad <marcos@ampl.com>
Model author: Yimin Wang <yimin_wang@asu.edu>
References:
Analytical Decision Modeling course at W. P. Carey School of Business. Syllabus: https://catalog.apps.asu.edu/catalog/classes/classlist?keywords=86683&searchType=all&term=2237&collapse=Y
Objective and Prerequisites#
This NFL team rating problem shows you how to determine the optimal rating of sporting teams based on their past performances. We use the root mean squared error (RMSE) as a criteria to determine what set of ratings is most accurate. The objectives of the sports rating problem are:
Find the best possible set of ratings for all teams,
Encorporate possible home team advantages,
Minimize the errors when predicting outcomes of future matches, and
Ensure that the ratings are consistent and identifiable.
Problem Description#
We consider the NFL games from 32 teams. The teams are indexed 1 to 32, for example, team 1 is Arizon, team 2 is Atlanta, and so on.
Team Number |
Team name |
Team Number |
Team name |
---|---|---|---|
1 |
Arizona Cardinals |
17 |
Miami Dolphins |
2 |
Atlanta Falcons |
18 |
Minnesota Vikings |
3 |
Baltimore Ravens |
19 |
New England Patriots |
4 |
Buffalo Bills |
20 |
New Orleans Saints |
5 |
Carolina Panthers |
21 |
New York Giants |
6 |
Chicago Bears |
22 |
New York Jets |
7 |
Cincinnati Bengals |
23 |
Oakland Raiders |
8 |
Cleveland Browns |
24 |
Philadelphia Eagles |
9 |
Dallas Cowboys |
25 |
Pittsburgh Steelers |
10 |
Denver Broncos |
26 |
St. Louis Rams |
11 |
Detroit Lions |
27 |
San Diego Chargers |
12 |
Green Bay Packers |
28 |
San Francisco 49ers |
13 |
Houston Texans |
29 |
Seattle Seahawks |
14 |
Indianapolis Colts |
30 |
Tampa Bay Buccaneers |
15 |
Jacksonville Jaguars |
31 |
Tennessee Titans |
16 |
Kansas City Chiefs |
32 |
Washington Redskins |
Table below illustrates some of the results of the 256 regular season NFL games from the 2015 season. The first game is team 10 Denver versus team 3 Baltimore, played at Denver. Denver won the game by a score of 49 to 27, and the point spread (home team score minus vistor team score) is calculated as the difference between the home team score and the visiting team score. In the above example, the point spread is 49-27=22.
Week |
Match |
Home team index |
Visiting team index |
Home team score |
Visiting team score |
Point spread |
---|---|---|---|---|---|---|
1 |
1 |
10 |
3 |
49 |
27 |
22 |
1 |
2 |
5 |
29 |
7 |
12 |
-5 |
1 |
3 |
15 |
16 |
2 |
28 |
-26 |
1 |
4 |
22 |
30 |
18 |
17 |
1 |
1 |
5 |
26 |
1 |
27 |
24 |
3 |
1 |
6 |
9 |
21 |
36 |
31 |
5 |
1 |
7 |
28 |
12 |
34 |
28 |
6 |
1 |
8 |
20 |
2 |
23 |
17 |
6 |
1 |
9 |
14 |
23 |
21 |
17 |
4 |
1 |
10 |
6 |
7 |
24 |
21 |
3 |
1 |
11 |
11 |
18 |
34 |
24 |
10 |
1 |
12 |
4 |
19 |
21 |
23 |
-2 |
1 |
13 |
25 |
31 |
9 |
16 |
-7 |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
17 |
256 |
17 |
22 |
7 |
20 |
-13 |
We would like to find a best rating system that most accurately predicts the matches.
Model Formulation#
Indices#
\(i,j \in \{1..32\}\): Index to represent teams
\(t \in \{1..256\}\): Index to represent different games
Parameters#
\(p_{ijt}\): point spread between team \(i\) and team \(j\) in game \(t\)
Decision Variables#
\(x_{i}\): Ratings of team \(i\), \(i \in \{1..32\}\)
\(y\): Home team advantage
Objective Function#
Prediction Accuracy. We want to minimize the prediction error.
Constraints#
Python Implementation#
We now import the AMPL Python Module and other Python libraries.
# Install dependencies
%pip install -q amplpy
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["gurobi"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
Set up the inputs#
#####################################################
# Model Formulation
#####################################################
# from 1 to 32
team = [*range(1, 33)]
# from 0 to 255
game = [*range(1, 257)]
team_label = [
"Arizona Cardinals",
"Atlanta Falcons",
"Baltimore Ravens",
"Buffalo Bills",
"Carolina Panthers",
"Chicago Bears",
"Cincinnati Bengals",
"Cleveland Browns",
"Dallas Cowboys",
"Denver Broncos",
"Detroit Lions",
"Green Bay Packers",
"Houston Texans",
"Indianapolis Colts",
"Jacksonville Jaguars",
"Kansas City Chiefs",
"Miami Dolphins",
"Minnesota Vikings",
"New England Patriots",
"New Orleans Saints",
"New York Giants",
"New York Jets",
"Oakland Raiders",
"Philadelphia Eagles",
"Pittsburgh Steelers",
"St. Louis Rams",
"San Diego Chargers",
"San Francisco 49ers",
"Seattle Seahawks",
"Tampa Bay Buccaneers",
"Tennessee Titans",
"Washington Redskins",
]
# past game performance
p = [
[10, 3, 22],
[5, 29, -5],
[15, 16, -26],
[22, 30, 1],
[26, 1, 3],
[9, 21, 5],
[28, 12, 6],
[20, 2, 6],
[14, 23, 4],
[6, 7, 3],
[11, 18, 10],
[4, 19, -2],
[25, 31, -7],
[8, 17, -13],
[32, 24, -6],
[27, 13, -3],
[19, 22, 3],
[1, 11, 4],
[29, 28, 26],
[30, 20, -2],
[13, 31, 6],
[21, 10, -18],
[2, 26, 7],
[6, 18, 1],
[16, 9, 1],
[14, 17, -4],
[3, 8, 8],
[23, 15, 10],
[24, 27, -3],
[4, 5, 1],
[12, 32, 18],
[7, 25, 10],
[24, 16, -10],
[5, 21, 38],
[22, 4, 7],
[25, 6, -17],
[29, 15, 28],
[18, 8, -4],
[17, 2, 4],
[7, 12, 4],
[32, 11, -7],
[28, 14, -20],
[3, 13, 21],
[19, 30, 20],
[20, 1, 24],
[31, 27, 3],
[9, 26, 24],
[10, 23, 16],
[26, 28, -24],
[2, 19, -7],
[16, 21, 24],
[4, 3, 3],
[13, 29, -3],
[18, 25, 7],
[23, 32, -10],
[30, 1, -3],
[8, 7, 11],
[27, 9, 9],
[10, 24, 32],
[11, 6, 8],
[15, 14, -34],
[31, 22, 25],
[20, 17, 21],
[8, 4, 13],
[1, 5, 16],
[12, 11, 13],
[14, 29, 6],
[23, 27, 10],
[9, 10, -3],
[28, 13, 31],
[6, 20, -8],
[21, 24, -15],
[17, 3, -3],
[31, 16, -9],
[26, 15, 14],
[7, 19, 7],
[2, 22, -2],
[6, 21, 6],
[3, 12, -2],
[18, 5, -25],
[28, 1, 12],
[19, 20, 3],
[22, 25, -13],
[8, 11, -14],
[16, 23, 17],
[13, 26, -25],
[29, 31, 7],
[10, 15, 16],
[4, 7, -3],
[30, 24, -11],
[9, 32, 15],
[27, 14, 10],
[1, 29, -12],
[5, 26, 15],
[2, 30, 8],
[25, 3, 3],
[22, 19, 3],
[11, 7, -3],
[12, 8, 18],
[14, 10, 6],
[32, 6, 4],
[15, 27, -18],
[17, 4, -2],
[31, 28, -14],
[16, 13, 1],
[24, 9, -14],
[21, 18, 16],
[30, 5, -18],
[18, 12, -13],
[23, 25, 3],
[1, 2, 14],
[19, 17, 10],
[11, 9, 1],
[20, 4, 18],
[15, 28, -32],
[10, 32, 24],
[7, 22, 40],
[24, 21, -8],
[16, 8, 6],
[26, 29, -5],
[17, 7, 2],
[26, 31, -7],
[5, 2, 24],
[13, 14, -3],
[32, 27, 6],
[22, 20, 6],
[8, 3, 6],
[19, 25, 24],
[9, 18, 4],
[4, 16, -10],
[23, 24, -29],
[29, 30, 3],
[12, 6, -7],
[18, 32, 7],
[14, 26, -30],
[21, 23, 4],
[1, 13, 3],
[28, 5, -1],
[27, 10, -8],
[6, 11, -2],
[31, 15, -2],
[2, 29, -23],
[12, 24, -14],
[25, 4, 13],
[3, 7, 3],
[20, 9, 32],
[30, 17, 3],
[31, 14, -3],
[29, 18, 21],
[6, 3, 3],
[21, 12, 14],
[13, 23, -5],
[30, 2, 13],
[15, 1, -13],
[25, 11, 10],
[20, 28, 3],
[17, 27, 4],
[10, 16, 10],
[24, 32, 8],
[4, 22, 23],
[7, 8, 21],
[5, 19, 4],
[2, 20, -4],
[13, 15, -7],
[11, 30, -3],
[16, 27, -3],
[12, 18, 0],
[21, 9, -3],
[8, 25, -16],
[26, 6, 21],
[19, 10, 3],
[1, 14, 29],
[23, 31, -4],
[17, 5, -4],
[3, 22, 16],
[32, 28, -21],
[3, 25, 2],
[11, 12, 30],
[9, 23, 7],
[4, 2, -3],
[27, 7, -7],
[24, 1, 3],
[18, 6, 3],
[32, 21, -7],
[14, 31, 8],
[22, 17, -20],
[28, 26, 10],
[8, 15, -4],
[5, 30, 21],
[16, 10, -7],
[13, 19, -3],
[29, 20, 27],
[15, 13, 7],
[25, 17, -6],
[7, 14, 14],
[1, 26, 20],
[27, 21, 23],
[28, 29, 2],
[10, 31, 23],
[3, 18, 3],
[30, 4, 21],
[20, 5, 18],
[32, 16, -35],
[12, 2, 1],
[19, 8, 1],
[22, 23, 10],
[24, 11, 14],
[6, 9, 17],
[10, 27, -7],
[8, 6, -7],
[31, 1, -3],
[17, 19, 4],
[18, 24, 18],
[5, 22, 10],
[15, 4, -7],
[30, 28, -19],
[14, 13, 22],
[25, 7, 10],
[21, 29, -23],
[23, 16, -25],
[26, 20, 11],
[9, 12, -1],
[2, 32, 1],
[11, 3, -2],
[16, 14, -16],
[11, 21, -3],
[13, 10, -24],
[26, 30, 10],
[7, 18, 28],
[29, 1, -7],
[22, 8, 11],
[4, 17, 19],
[15, 31, -4],
[32, 9, -1],
[5, 20, 4],
[27, 23, 13],
[3, 19, -34],
[12, 25, -7],
[24, 6, 43],
[28, 2, 10],
[29, 26, 18],
[1, 28, -3],
[9, 24, -2],
[6, 12, -5],
[31, 13, 6],
[27, 16, 3],
[20, 30, 25],
[21, 32, 14],
[14, 15, 20],
[7, 3, 17],
[18, 11, 1],
[19, 4, 14],
[2, 5, -1],
[25, 8, 13],
[23, 10, -20],
[17, 22, -13],
]
Compute the index set to facilitate setting up the model#
# Computing the index set (ijt)
# Valid set of tuples
A = []
for t in game:
# record team pairs and match sequence
i = p[t - 1][0]
j = p[t - 1][1]
tp = i, j, t
A.append(tp)
# print(np.matrix(A))
Setup decisions, objective, and constraints#
%%ampl_eval
# Parameters
param num_teams;
param num_games;
# Sets
set teams := 1..num_teams;
set games := 1..num_games;
# data for point spread is a subset of teams x teams x games
set games_data within {teams, teams, games};
# Point spread between team i and team j in game t
param p{games_data};
# Build decision variables: team ratings and home team advantage
var x{teams} >= 0;
var y >= 0;
%%ampl_eval
# Objective function: Minimize SSE
minimize sse:
sum{(i,j,t) in games_data} (x[i] + y -x[j] - p[i,j,t])^2;
%%ampl_eval
#Constraints
# Fix average rating to be at 85
s.t. fix_average_rating:
sum{i in teams} x[i] = 85*num_teams;
# Load data into ampl
ampl.param["num_teams"] = 32
ampl.param["num_games"] = 256
ampl.set["games_data"] = A
ampl.param["p"] = {(i, j, t): p[t - 1][2] for (i, j, t) in A}
Solve the model#
# Run optimization engine
ampl.option["solver"] = "gurobi"
ampl.solve()
Gurobi 10.0.1: Gurobi 10.0.1: optimal solution; objective 30972.98811
0 simplex iterations
6 barrier iterations
Examine outputs - The minimum SSE#
# check the SSE
print(
"The minimum sum of squared errors are ",
round(ampl.get_objective("sse").value(), 2),
)
The minimum sum of squared errors are 30972.99
Check the optimal team ratings#
# print optimal ratings by team
print(
"\033[1m Home team advantage is \033[0m (", round(ampl.var["y"].value(), 2), ") \n"
)
print("\033[1m Optimal team ratings")
print("------------------------------------------\n")
# loop through all destinations
x = ampl.var["x"]
average_rating = 0
team_count = 0
for i in team:
print(
"\033[1m",
i,
" ",
team_label[i - 1],
"\033[0m:",
round(x[i].value(), 2),
"\n",
end="",
)
average_rating += x[i].value()
team_count += 1
print("------------------------------------------")
print(
"\033[1m Average team ratings: \033[0m", round(average_rating / team_count, 2), ""
)
Home team advantage is ( 3.11 )
Optimal team ratings
------------------------------------------
1 Arizona Cardinals : 91.45
2 Atlanta Falcons : 82.24
3 Baltimore Ravens : 81.47
4 Buffalo Bills : 81.79
5 Carolina Panthers : 94.2
6 Chicago Bears : 80.87
7 Cincinnati Bengals : 90.35
8 Cleveland Browns : 77.31
9 Dallas Cowboys : 84.31
10 Denver Broncos : 96.37
11 Detroit Lions : 83.36
12 Green Bay Packers : 81.89
13 Houston Texans : 77.42
14 Indianapolis Colts : 89.04
15 Jacksonville Jaguars : 73.9
16 Kansas City Chiefs : 91.08
17 Miami Dolphins : 84.16
18 Minnesota Vikings : 78.38
19 New England Patriots : 90.89
20 New Orleans Saints : 93.77
21 New York Giants : 79.6
22 New York Jets : 78.91
23 Oakland Raiders : 77.01
24 Philadelphia Eagles : 86.85
25 Pittsburgh Steelers : 83.05
26 St. Louis Rams : 87.22
27 San Diego Chargers : 87.66
28 San Francisco 49ers : 95.13
29 Seattle Seahawks : 98.04
30 Tampa Bay Buccaneers : 82.33
31 Tennessee Titans : 84.23
32 Washington Redskins : 75.72
------------------------------------------
Average team ratings: 85.0
Conclusion#
The NFL team rating problem shows that an nonlinear optimization model can be used to predict game match performances. The above example illustrates that teams can be rated in such a way to minimize the prediction errors for game outcomes.
A key take away of the above example is that the team ratings should be controlled at a standard level (85 in this case). The reason is that without such a standard level, there exists an infinite set of optimal solutions that achieve the same RMSE. The standard level, however, can be set at any number.
One potential issue with the above team rating optimization approach is that the ratings can over fit historical performance. An exponential weighted performance approach can partially overcome this issue but putting more weight on more recent matches and less weight on distant matchings. However, given that the regular season is not very long, such an issue is unlikely to be a significant concern.
References#
[1] AMPL python reference. https://amplpy.readthedocs.io/en/latest/reference.html
[2] This notebook is developed by Yimin Wang (yimin_wang@asu.edu). Marcos Dominguez (marcos@ampl.com) also contributed to this notebook content.