This tutorial includes everything you need to set up decision optimization engines, build mathematical programming models, and arrive at a good working schedule for a sports league's games.
When you finish this tutorial, you'll have a foundational knowledge of Prescriptive Analytics.
This notebook is part of Prescriptive Analytics for Python
It requires either an installation of CPLEX Optimizers or it can be run on IBM Watson Studio Cloud (Sign up for a free IBM Cloud account and you can start using Watson Studio Cloud right away).
Table of contents:
This is a type of discrete optimization problem that can be solved by using either Integer Programming (IP) or Constraint Programming (CP).
Integer Programming is the class of problems defined as the optimization of a linear function, subject to linear constraints over integer variables.
Constraint Programming problems generally have discrete decision variables, but the constraints can be logical, and the arithmetic expressions are not restricted to being linear.
For the purposes of this tutorial, we will illustrate a solution with mathematical programming (MIP).
Prescriptive analytics (decision optimization) technology recommends actions that are based on desired outcomes. It takes into account specific scenarios, resources, and knowledge of past and current events. With this insight, your organization can make better decisions and have greater control of business outcomes.
Prescriptive analytics is the next step on the path to insight-based actions. It creates value through synergy with predictive analytics, which analyzes data to predict future outcomes.
Prescriptive analytics takes that insight to the next level by suggesting the optimal way to handle that future situation. Organizations that can act fast in dynamic conditions and make superior decisions in uncertain environments gain a strong competitive advantage.
With prescriptive analytics, you can:
In [ ]:
import sys
try:
import docplex.mp
except:
raise Exception('Please install docplex. See https://pypi.org/project/docplex/')
If CPLEX is not installed, install CPLEX Community edition.
In [ ]:
try:
import cplex
except:
raise Exception('Please install CPLEX. See https://pypi.org/project/cplex/')
In this scenario, the data is simple. There are eight teams in each division, and the teams must play each team in the division once and each team outside the division once.
Use a Python module, Collections, which implements some data structures that will help solve some problems. Named tuples helps to define meaning of each position in a tuple. This helps the code be more readable and self-documenting. You can use named tuples in any place where you use tuples.
In this example, you create a namedtuple to contain information for points. You are also defining some of the parameters.
In [ ]:
# Teams in 1st division
team_div1 = ["Baltimore Ravens","Cincinnati Bengals", "Cleveland Browns","Pittsburgh Steelers","Houston Texans",
"Indianapolis Colts","Jacksonville Jaguars","Tennessee Titans","Buffalo Bills","Miami Dolphins",
"New England Patriots","New York Jets","Denver Broncos","Kansas City Chiefs","Oakland Raiders",
"San Diego Chargers"]
# Teams in 2nd division
team_div2 = ["Chicago Bears","Detroit Lions","Green Bay Packers","Minnesota Vikings","Atlanta Falcons",
"Carolina Panthers","New Orleans Saints","Tampa Bay Buccaneers","Dallas Cowboys","New York Giants",
"Philadelphia Eagles","Washington Redskins","Arizona Cardinals","San Francisco 49ers",
"Seattle Seahawks","St. Louis Rams"]
In [ ]:
#number_of_matches_to_play = 1 # Number of match to play between two teams on the league
# Schedule parameters
nb_teams_in_division = 5
max_teams_in_division = 10
number_of_matches_inside_division = 1
number_of_matches_outside_division = 1
Use basic HTML and a stylesheet to format the data.
In [ ]:
CSS = """
body {
margin: 0;
font-family: Helvetica;
}
table.dataframe {
border-collapse: collapse;
border: none;
}
table.dataframe tr {
border: none;
}
table.dataframe td, table.dataframe th {
margin: 0;
border: 1px solid white;
padding-left: 0.25em;
padding-right: 0.25em;
}
table.dataframe th:not(:empty) {
background-color: #fec;
text-align: left;
font-weight: normal;
}
table.dataframe tr:nth-child(2) th:empty {
border-left: none;
border-right: 1px dashed #888;
}
table.dataframe td {
border: 2px solid #ccf;
background-color: #f4f4ff;
}
table.dataframe thead th:first-child {
display: none;
}
table.dataframe tbody th {
display: none;
}
"""
from IPython.core.display import HTML
HTML('<style>{}</style>'.format(CSS))
Now you will import the pandas library. Pandas is an open source Python library for data analysis. It uses two data structures, Series and DataFrame, which are built on top of NumPy.
A Series is a one-dimensional object similar to an array, list, or column in a table. It will assign a labeled index to each item in the series. By default, each item receives an index label from 0 to N, where N is the length of the series minus one.
A DataFrame is a tabular data structure comprised of rows and columns, similar to a spreadsheet, database table, or R's data.frame object. Think of a DataFrame as a group of Series objects that share an index (the column names).
In the example, each division (the AFC and the NFC) is part of a DataFrame.
In [ ]:
import pandas as pd
team1 = pd.DataFrame(team_div1)
team2 = pd.DataFrame(team_div2)
team1.columns = ["AFC"]
team2.columns = ["NFC"]
teams = pd.concat([team1,team2], axis=1)
The following display function is a tool to show different representations of objects. When you issue the display(teams) command, you are sending the output to the notebook so that the result is stored in the document.
In [ ]:
from IPython.display import display
display(teams)
Given the number of teams in each division and the number of intradivisional and interdivisional games to be played, you can calculate the total number of teams and the number of weeks in the schedule, assuming every team plays exactly one game per week.
The season is split into halves, and the number of the intradivisional games that each team must play in the first half of the season is calculated.
In [ ]:
import numpy as np
nb_teams = 2 * nb_teams_in_division
teams = range(nb_teams)
# Calculate the number of weeks necessary
nb_inside_div = (nb_teams_in_division - 1) * number_of_matches_inside_division
nb_outside_div = nb_teams_in_division * number_of_matches_outside_division
nb_weeks = nb_inside_div + nb_outside_div
# Weeks to schedule
weeks = range(nb_weeks)
# Season is split into two halves
first_half_weeks = range(int(np.floor(nb_weeks / 2)))
nb_first_half_games = int(np.floor(nb_weeks / 3))
In [ ]:
from collections import namedtuple
match = namedtuple("match",["team1","team2","is_divisional"])
matches = {match(t1,t2, 1 if ( t2 <= nb_teams_in_division or t1 > nb_teams_in_division) else 0)
for t1 in teams for t2 in teams if t1 < t2}
Number of games to play between pairs depends on whether the pairing is intradivisional or not.
In [ ]:
nb_play = { m : number_of_matches_inside_division if m.is_divisional==1
else number_of_matches_outside_division
for m in matches}
In [ ]:
from docplex.mp.environment import Environment
env = Environment()
env.print_information()
In [ ]:
from docplex.mp.model import Model
mdl = Model("sports")
In [ ]:
plays = mdl.binary_var_matrix(matches, weeks, lambda ij: "x_%s_%d" %(str(ij[0]), ij[1]))
In [ ]:
mdl.add_constraints( mdl.sum(plays[m,w] for w in weeks) == nb_play[m]
for m in matches)
mdl.print_information()
In [ ]:
mdl.add_constraints( mdl.sum(plays[m,w] for m in matches if (m.team1 == t or m.team2 == t) ) == 1
for w in weeks for t in teams)
mdl.print_information()
In [ ]:
mdl.add_constraints( plays[m,w] + plays[m,w+1] <= 1
for w in weeks
for m in matches
if w < nb_weeks-1)
mdl.print_information()
In [ ]:
mdl.add_constraints( mdl.sum(plays[m,w] for w in first_half_weeks for m in matches
if (((m.team1 == t or m.team2 == t) and m.is_divisional == 1 )))
>= nb_first_half_games
for t in teams)
mdl.print_information()
In [ ]:
gain = { w : w*w for w in weeks}
# If an intradivisional pair plays in week w, Gain[w] is added to the objective.
mdl.maximize( mdl.sum (m.is_divisional * gain[w] * plays[m,w] for m in matches for w in weeks) )
In [ ]:
mdl.print_information()
assert mdl.solve(), "!!! Solve of the model fails"
mdl.report()
Determine which of the scheduled games will be a replay of one of the last 10 Super Bowls.
We start by creating a pandas DataFrame that contains the year and teams who played the last 10 Super Bowls.
In [ ]:
try: # Python 2
team_league = dict({t : team_div1[t] for t in range(nb_teams_in_division) }.items() + \
{t+nb_teams_in_division : team_div2[t] for t in range(nb_teams_in_division) }.items()
)
except: # Python 3
team_league = dict(list({t : team_div1[t] for t in range(nb_teams_in_division) }.items()) + \
list({t+nb_teams_in_division : team_div2[t] for t in range(nb_teams_in_division) }.items()))
In [ ]:
sol = namedtuple("solution",["week","is_divisional", "team1", "team2"])
solution = [sol(w, m.is_divisional, team_league[m.team1], team_league[m.team2]) for m in matches for w in weeks if plays[m,w].solution_value == 1]
In [ ]:
nfl_finals = [("2016", "Carolina Panthers", "Denver Broncos"),
("2015", "New England Patriots", "Seattle Seahawks"),
("2014", "Seattle Seahawks", "Denver Broncos"),
("2013", "Baltimore Ravens", "San Francisco 49ers"),
("2012", "New York Giants", "New England Patriots "),
("2011", "Green Bay Packers", "Pittsburgh Steelers"),
("2010", "New Orleans Saints", "Indianapolis Colts"),
("2009", "Pittsburgh Steelers", "Arizona Cardinals"),
("2008", "New York Giants", "New England Patriots"),
("2007", "Indianapolis Colts", "Chicago Bears")
]
nfl_meetings = {(t[1], t[2]) for t in nfl_finals}
winners_bd = pd.DataFrame(nfl_finals)
winners_bd.columns = ["year", "team1", "team2"]
In [ ]:
display(winners_bd)
We now look for the games in our solution that are replays of one of the past 10 Super Bowls.
In [ ]:
months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
report = []
for m in solution:
if (m.team1, m.team2) in nfl_meetings:
report.append((m.week, months[m.week//4], m.team1, m.team2))
if (m.team2, m.team1) in nfl_meetings:
report.append((m.week, months[m.week//4], m.team2, m.team1))
print(report)
matches_bd = pd.DataFrame(report)
matches_bd.columns = ["week", "Month", "Team1", "Team2"]
In [ ]:
try: #pandas >= 0.17
display(matches_bd.sort_values(by='week'))
except:
display(matches_bd.sort('week'))
Copyright © 2017-2019 IBM. IPLA licensed Sample Materials.
In [ ]: