How to Win Fantasy Hockey
How to Win Fantasy Hockey
A recipe for using python to scrape player projection data and build a value over replacement ranking model
Jupyter notebook for this blog post available here
№1 Identify a data source (CBS Sports is a good option)
№2 get the html data with gazpacho
from gazpacho import get
position = 'F'
base = f'https://www.cbssports.com/fantasy/hockey/stats'
url = f'{base}/{position}/2019/restofseason/projections/'
html = get(url)
№3 Pass the captured html to a Soup parser
from gazpacho import Soup
soup = Soup(html)
№4 find the html tags that contain player projection data
# HTML: <tr class="TableBase-bodyTr ">
rows = soup.find('tr', {'class': 'TableBase-bodyTr '})
№5. Capture a single row (and inspect for good measure)
row = rows[0]
№6 Use find to grab those tags that map to player name, position, and projected Fantasy Points
# name
row.find('span', {'class': 'CellPlayerName--long'}).find('a').text
# position
(row.find('span', {'class': 'CellPlayerName--long'})
.find('span', {'class': 'CellPlayerName-position'}).text
)
# points
float(row.find('td', {'class': 'TableBase-bodyTd'})[1].text)
361.1
№7 Wrap these find operations into a function
row = rows[0]
def parse_row(row):
meta = row.find('span', {'class': 'CellPlayerName--long'})
try:
name = meta.find('a').text
except AttributeError:
name = meta.text
position = meta.find('span', {'class': 'CellPlayerName-position'}).text
points = float(row.find('td', {'class': 'TableBase-bodyTd'})[1].text)
return name, position, points
parse_row(row)
('Nikita Kucherov', 'RW', 361.1)
№8 Make sure that the function works for all the captured rows
players = []
for row in rows:
try:
players.append(parse_row(row))
except AttributeError:
pass
players[-2:]
[('Brandon Pirri', 'LW', 165.4), ('Pavel Buchnevich', 'RW', 164.9)]
№9 Bundle up the logic so that it can be applied to multiple pages
def scrape_position(position):
base = f'https://www.cbssports.com/fantasy/hockey/stats'
url = f'{base}/{position}/2019/restofseason/projections/'
html = get(url)
soup = Soup(html)
rows = soup.find('tr', {'class': 'TableBase-bodyTr '})
data = []
for row in rows:
try:
data.append(parse_row(row))
except AttributeError:
pass
return data
№10 Scrape each page that contains player projection data
# F for Forwards
# D for Defence
# G for goalies
import time
data = []
for position in ['F', 'D', 'G']:
d = scrape_position(position)
data.extend(d)
time.sleep(1)
№11 Stuff the captured data into a pandas DataFrame
import pandas as pd
df = pd.DataFrame(data, columns=['name', 'position', 'points'])
df.sample(5)
name position points
171 Mike Matheson D 92.0
60 David Perron RW 196.9
86 Tomas Tatar LW 176.1
190 Dennis Cholowski D 83.0
257 Thatcher Demko G 89.0
№12 Calculate the value over player replacement score for each player
pool_size = 8
starters = {'C': 1, 'LW': 1, 'RW': 1, 'D': 2, 'G': 1}
for position, slots in starters.items():
replacement = (
df[df['position'] == position]
.sort_values('points', ascending=False)
.head(slots * pool_size)
['points']
.mean()
)
df.loc[df['position'] == position, 'vorp'] = df['points'] - replacement
№13 Re-rank and draft players according to their VORP rank
df['rank'] = df['vorp'].rank(method='average', ascending=False)
df.sort_values('rank').set_index('rank').head(20)
name position points vorp
rank
1.0 Nikita Kucherov RW 361.1 75.38750
2.0 Leon Draisaitl LW 333.1 58.51250
3.0 Connor McDavid C 337.1 45.13750
4.0 Andrei Vasilevskiy G 433.9 41.26250
5.0 Brent Burns D 225.1 34.09375
6.0 Brad Marchand LW 302.1 27.51250
7.0 Patrick Kane RW 312.7 26.98750
8.0 Dustin Byfuglien D 216.7 25.69375
9.0 Mark Giordano D 210.2 19.19375
10.0 Morgan Rielly D 208.0 16.99375
11.0 John Carlson D 207.0 15.99375
12.0 Kris Letang D 205.2 14.19375
13.0 Tyson Barrie D 203.6 12.59375
14.0 Mikko Rantanen RW 297.2 11.48750
15.0 Nathan MacKinnon C 302.8 10.83750
16.0 Alex Ovechkin LW 283.9 9.31250
17.0 Jordan Binnington G 393.5 0.86250
18.0 Torey Krug D 191.8 0.79375
19.0 Steven Stamkos C 292.3 0.33750
20.0 Tuukka Rask G 392.7 0.06250
№14 Profit