<jupyter_start><jupyter_text>Fantasy Fumball: Using Machine Learning to Predict the Rank of Offensive Players in the NFL for Fantasy Football Drafting   IntroductionFantasy Sports is an industry estimated at over $7B per year (https://qz.com/1068534/how-the-7-billion-us-fantasy-football-industry-makes-its-money-in-2017/). It spans a host of sports and has spawned leagues and even a TV show (The League https://en.wikipedia.org/wiki/The_League) revolving around the players. My project specifically focused on fantasy football and selecting (aka drafting) the players that would be most likely to create a winning team. I set out to discover if it was possible to predict the rank of offensive players in the NFL for fantasy sports based on their statistics across the entire season.  The objective of my project is to come up with a way to rank players so I can increase my probability of picking great starters and finally win my league.   Fantasy Football Drafting and Assumptions  Fantasy football is rife with stats for a data scientist. Most aspects of the game are quantified in discrete terms. Additionally, player rank is a continuous variable that we can test for and predict. I opted to only study offensive players that played positions that could be drafted onto my fantasy team. In this case, that meant evaluating the following positions: Quarterback (QB), Running back (RB), Wide Receiver (WR), Tight End (TE), and Kicker (K).  Fantasy Football teams are comprised of players from all teams in the NFL. Players in the following positions may be drafted: Quarterback (QB), Running back (RB), Wide Receiver (WR), Tight End (TE), and Kicker (K). Each player may only be drafted onto one fantasy team per league. Each player in the league drafts one fantasy player for their team in rounds until the fantasy teams are filled.  As a note, due to time constraints, I opted to ignore defensive positions for this project. The data I pulled was for individual players and we typically draft an entire team defense. I also ignored Points-Per-Reception (PPR) scoring in my calculations since my league does not play by that rule. Future work would also include an option to toggle PPR on or off as it can make a difference in drafting. Data Collection and Preparation My data is pulled from https://github.com/BurntSushi/nflgame, an API to read and retrieve NFL Game Center JSON Data. I will be using 2016 full season stats for my feature set. I also pulled the top 300 player rank, as ranked by ESPN, for my response vector.<jupyter_code># read the data into a DataFrame named nfl16
import pandas as pd
path = '../GA_Class_Project/'
url = path + 'season2016_wy.csv'
nfl16 = pd.read_csv(url, index_col = 0)
nfl16.head()
# Determine total number of players in dataframe
nfl16.shape
# 1573 players in this list with 59 attributes
# Determine how many players would be eligible to be drafted based on open positions in a fantasy roster
nfl16.pos.value_counts()<jupyter_output><empty_output><jupyter_text>Since I was only interested in QB, RB, WR, TE, K, I created new DataFrames using only these positions. This step was not totally necessary but it did simplify my data nicely.<jupyter_code># Create new DataFrames using only QB, WR, RB, TE, K
nfl16_QB = nfl16.loc[nfl16.pos == 'QB']
nfl16_WR = nfl16.loc[nfl16.pos == 'WR']
nfl16_RB = nfl16.loc[nfl16.pos == 'RB']
nfl16_TE = nfl16.loc[nfl16.pos == 'TE']
nfl16_K = nfl16.loc[nfl16.pos == 'K']
# Concat the new DataFrames back together as "nfl16_off"
frames = [nfl16_QB, nfl16_WR, nfl16_RB, nfl16_TE, nfl16_K]
nfl16_off = pd.concat(frames)
nfl16_off.shape
# Perform a Sanity Check to make sure the expected players are still there
nfl16_off.head()<jupyter_output><empty_output><jupyter_text>Fantasy Football ScoringPlayers earn points based on their stats. The goal of your draft is to amass the highest scoring players on to your team. A breakdown point totals is below (https://fantasydata.com/resources/fantasy-scoring-system.aspx):  Offensive Players  •	Passing Yards: 1 point per 25 yards •	Passing Touchdowns: 4 points  •	Passing Interceptions: -2 points  •	Rushing Yards: 1 point per 10 yards  •	Rushing Touchdowns: 6 points  •	Receptions: 1 points (only if using PPR scoring)  •	Receiving Yards: 1 point per 10 yards  •	Receiving Touchdowns: 6 points  •	2-Point Conversions: 2 points  •	Fumbles Lost: -2 points  •	Fumble Recovered for a Touchdown: 6 points  Kicking  •	Point After Touchdown (PAT) Made: 1 point  •	Field Goal (FG) Made (0-49 yards): 3 points  •	FG Made (50+ yards): 5 points  I used the above listed point values and mapped them to each player's statistics to come up with the total number of points a player would have scored across the season. Of note, a kicker's statistics are more single game specific than other player's statistics. For example, a long range field goal (50+ yards) earns an extra 2 points for the kicker. Since I pulled full season data, I did not have the yardage breakdown in an easy to determine format. Future work would include looping through kickers stats for each game to give them their proper bonus points. For now, I assumed all Field Goals made were worth 3 points.<jupyter_code># Determine column headers
nfl16_off.columns.values.tolist()
# Map player stats to points using list comprehensions
fumbles_lost_pts = [x * -2 for x in nfl16_off.fumbles_lost]
passing_yds_pts = [x / 25 for x in nfl16_off.passing_yds]
passing_tds_pts = [x * 4 for x in nfl16_off.passing_tds]
passing_ints_pts = [x * -2 for x in nfl16_off.passing_ints]
rushing_yds_pts = [x / 10 for x in nfl16_off.rushing_yds]
rushing_tds_pts = [x * 6 for x in nfl16_off.rushing_tds]
receiving_yds_pts = [x / 10 for x in nfl16_off.receiving_yds]
receiving_tds_pts = [x * 6 for x in nfl16_off.receiving_tds]
passing_twoptm_pts = [x * 2 for x in nfl16_off.passing_twoptm]
receiving_twoptm_pts = [x * 2 for x in nfl16_off.receiving_twoptm]
rushing_twoptm_pts = [x * 2 for x in nfl16_off.rushing_twoptm]
kicking_xpmade_pts = [x * 1 for x in nfl16_off.kicking_xpmade]
# For now, counting all field goals as 3 points for ease of data
# fgyds doesn't work across entire season
kicking_fgm_pts = [x * 3 for x in nfl16_off.kicking_fgm]
# create DataFrame for list of pts, give it the same index as before
nflpts = pd.DataFrame(list(zip(fumbles_lost_pts, passing_yds_pts, passing_tds_pts, passing_ints_pts, rushing_yds_pts, rushing_tds_pts, receiving_yds_pts, receiving_tds_pts, passing_twoptm_pts, receiving_twoptm_pts, rushing_twoptm_pts, kicking_xpmade_pts, kicking_fgm_pts)),
                      index = nfl16_off.index,
                      columns=['fumbles_lost_pts', 'passing_yds_pts', 'passing_tds_pts', 'passing_ints_pts', 'rushing_yds_pts', 'rushing_tds_pts', 'receiving_yds_pts', 'receiving_tds_pts', 'passing_twoptm_pts', 'receiving_twoptm_pts', 'rushing_twoptm_pts', 'kicking_xpmade_pts', 'kicking_fgm_pts'])

type(nflpts)
nflpts.head()
# Defining function to sum total points using for loop
def point_totals():
    tot_pts = []
    for index, row in nflpts.iterrows():
        tot_pts.append((index, row.sum()))
    return tot_pts
        
point_totals()[0:5]
# summing total points as list comprehension
# totalpoints = [(index, row.sum()) for index, row in nflpts.iterrows()] #this will return a tuple
totalpoints = [row.sum() for index, row in nflpts.iterrows()]
totalpoints
nfl16_off.index
nflpts.describe()<jupyter_output><empty_output><jupyter_text>Adding Response Vector to FeaturesThe nflgame API only included player stats but not player rank. I retrieved a Top 300 Fantasy Football player list from ESPN's website and appended it here.<jupyter_code>url = path + 'rank2016_wy.csv'
nflrank = pd.read_csv(url, index_col = 'name')
nflrank.pos.value_counts()
# I dropped position from nflrank since it was already encoded in my original data
nflrank = nflrank.drop('pos', 1)
nflrank.head()
print nfl16_off.shape
print nflrank.shape
print nflpts.shape
# add totalpoints to nflpts DataFrame
type(totalpoints)
# totalpoints is a list

totalpointslist = pd.Series(totalpoints)
nflpts['totalpoints'] = totalpointslist.values
nflpts.head()
# Add nflrank to nflpts
# Since my original data has more rows than the player ranks, I had to join the DataFrames rather than concatenate them.
nfl = nflpts.join(nflrank)
nfl.head()
nfl.shape
nfl.describe()
# The data from the nflgame API contained many blank cells
# For example, some players may never fumble the ball; QBs rarely receive the ball
nfl.isnull().sum()
nfl.columns.tolist()<jupyter_output><empty_output><jupyter_text>Exploratory Data AnalysisI started with basic statistics and visualization to see if any features immediately stood out.<jupyter_code># Allow graphs to show in notebook
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
nfl.plot(kind='box', rot = 90)
# Rank stands out at first because it's the most even.
# It seems like passing yards and passing touchdowns (tds) have big variance
# It looks like rushing yards, rushing tds, and receiving yards have the biggest outliers with fewer players running/catching the most
# Based on this chart, interceptions and two point conversions (twoptm) will likely have very little effect on player rank across the season
# Look for correlations between variables
nfl.corr()
# looks like rushing yards, rushing tds, receivings yards are the strongest correlated with rank but they're negative
# Scatter plot total points v rank
nfl.plot.scatter('rank','totalpoints');
# Scatter Plot of total points v. rank with linear regression added
import seaborn as sns
sns.lmplot(x='rank',y='totalpoints',data=nfl,fit_reg=True); 
# totalpoints and rank are negatively correlated
# this makes sense since those that score high points will have a 'lower' rank
# for rank, lower is better
# Plot Everything as Scatter Matrix Instead
pd.plotting.scatter_matrix(nfl, alpha=0.2, figsize=(10, 10), diagonal='kde');

# This is a very tiny matrix, I can't tell anything
# Plot just the stronger correlated variables
pd.plotting.scatter_matrix(nfl[['rushing_yds_pts', 'rushing_tds_pts', 'receiving_yds_pts', 'receiving_tds_pts', 'totalpoints', 'rank']], alpha=0.7, figsize=(15, 15), diagonal='kde')
# Nothing appears to be correlated with rank directly
# But there are strong correlations between receiving yards/tds or rushing yards/tds
# This seems like an obvious conclusion since yardage at some point will hopefuly result in a touchdown<jupyter_output><empty_output><jupyter_text>Project Modeling using Random ForestI haven't learned much about rank yet using correlations so I moved on to modeling. I started with random forest to do a quick preliminary model that would also determine important features without having to do feature scaling.<jupyter_code># Need to fill in null values, not sure what to fill with
# Create DataFrame to fill null with column means
nflmean = nfl.copy()

# Create DataFrame to fill null with column medians
nflmedian = nfl.copy()

# Create DataFrame to fill null with zeros
nflzero = nfl.copy()
'''for column in dataframe:
    fill null with column mean/median/zero
    check to make sure no nulls'''

skip_col = ['pos','rank']

for column in nflmean:
    if column not in skip_col:
        nflmean[column].fillna(nflmean[column].mean(), inplace = True)

for column in nflmedian:
    if column not in skip_col:
        nflmedian[column].fillna(nflmedian[column].median(), inplace = True)

for column in nflzero:
    if column not in skip_col:
        nflzero[column].fillna(0, inplace = True)

nflmean.head()
nflmean.isnull().sum()
nflmedian.head()
nflmedian.isnull().sum()
nflzero.head()
nflzero.isnull().sum()
nfl.isnull().sum()
# predicting rank with a Random Forest
# selecting and instantiating the estimator
import numpy as np
from sklearn.ensemble import RandomForestRegressor
rfreg = RandomForestRegressor()
rfreg

# list of values to try for n_estimators
estimator_range = range(10, 310, 10)
# 10 estimators, up to 300, in steps of 10

# Need to drop nulls from dataframe first
nflmean.dropna(inplace = True)
nflmean.shape #(293, 15)
# Dropped to 293 rows from 582 because only pulled top 300 players by rank, some team defenses in the mix.

#define X and y
feature_cols = nflmean.columns.drop('rank')
feature_cols
X_mean = nflmean[feature_cols]
y_mean = nflmean['rank']

# list to store the average RMSE for each value of n_estimators
nflmean_RMSE_scores = []

# use 5-fold cross-validation with each value of n_estimators (WARNING: SLOW!)
from sklearn.model_selection import cross_val_score
for estimator in estimator_range:
    rfreg = RandomForestRegressor(n_estimators=estimator, random_state=1)
    nflmean_MSE_scores = cross_val_score(rfreg, X_mean, y_mean, cv=5, scoring='neg_mean_squared_error')
    nflmean_RMSE_scores.append(np.mean(np.sqrt(-nflmean_MSE_scores)))

print nflmean_RMSE_scores
# write a loop to test cv values
# plot n_estimators (x-axis) versus RMSE (y-axis)
plt.plot(estimator_range, nflmean_RMSE_scores);
plt.xlabel('n_estimators')
plt.ylabel('RMSE (lower is better)')
# somewhere bteween 50 and 100 estimators seems like low RMSE
# show the best RMSE and the corresponding n_estimators
sorted(zip(nflmean_RMSE_scores, estimator_range))[0]
# using n_estimators = 60 seems best
# list of values to try for max_features
feature_range = range(1, len(feature_cols)+1)

# list to store the average RMSE for each value of max_features
nflmean_max_RMSE_scores = []

# use 10-fold cross-validation with each value of max_features (WARNING: SLOW!)
for feature in feature_range:
    rfreg = RandomForestRegressor(n_estimators=60, max_features=feature, random_state=1)
    nflmean_max_MSE_scores = cross_val_score(rfreg, X_mean, y_mean, cv=10, scoring='neg_mean_squared_error')
    nflmean_max_RMSE_scores.append(np.mean(np.sqrt(-nflmean_max_MSE_scores)))
    
print nflmean_max_RMSE_scores
# plot max_features (x-axis) versus RMSE (y-axis)
plt.plot(feature_range, nflmean_max_RMSE_scores);
plt.xlabel('max_features')
plt.ylabel('RMSE (lower is better)')
# 13 features is best
# show the best RMSE and the corresponding max_features
sorted(zip(nflmean_max_RMSE_scores, feature_range))[0]
# using 13 features seems best
# max_features=13 is best and n_estimators=60
rfreg = RandomForestRegressor(n_estimators=60, max_features=13, oob_score=True, random_state=1)
rfreg.fit(X_mean, y_mean)
# compute feature importances
pd.DataFrame({'feature':feature_cols, 'importance':rfreg.feature_importances_}).sort_values('importance', ascending=False)
# looks like totalpoints is most important which seems obvious since fantasy football is based on fantasy points
# next is receiving yards and to a lesser extent, rushing yards
# e.g. WRs and RBs which also seems obvious since they are responsible for a lot of movement on the field<jupyter_output><empty_output><jupyter_text>Conclusions from Random ForestTotalpoints appears to be the most important feature which seems obvious since fantasy football is based on fantasy points. The next most important features are receiving yards and rushing yards. These stats maps to WRs and RBs which also seems obvious since they are responsible for a lot of movement on the field.  I next set out to visualize these important features.<jupyter_code># starting with scatter plots and linreg lines for receiving and rushing yds
print sns.lmplot(x='rank', y='totalpoints', data=nflmean, aspect=1.5, scatter_kws={'alpha':0.2});
print sns.lmplot(x='rank', y='receiving_yds_pts', data=nflmean, aspect=1.5, scatter_kws={'alpha':0.2});
print sns.lmplot(x='rank', y='rushing_yds_pts', data=nflmean, aspect=1.5, scatter_kws={'alpha':0.2});
# all show negative correlation
# makes sense since low rank is good<jupyter_output><seaborn.axisgrid.FacetGrid object at 0x00000000213346A0>
<seaborn.axisgrid.FacetGrid object at 0x000000001CCE4390>
<seaborn.axisgrid.FacetGrid object at 0x000000001CCE4278><jupyter_text>Project Modeling using Linear RegressionSince there appears to be a linear pattern, I used linear regression to determine how much changing a variable (e.g. rushing yards) affects the outcome (rank).<jupyter_code>from sklearn.linear_model import LinearRegression
linreg = LinearRegression()

from sklearn.model_selection import train_test_split

# create a list of features
imp_feature_cols = ['totalpoints','rushing_yds_pts','receiving_yds_pts']
X_lin = nflmean[imp_feature_cols]
y_lin = nflmean['rank']

X_lin_tr, X_lin_te, y_lin_tr, y_lin_te = train_test_split(X_lin, y_lin, random_state=1)
from sklearn import metrics
# fit the model
linreg.fit(X_lin_tr, y_lin_tr)

# predict from model and determine error
y_lin_pred = linreg.predict(X_lin_te)
np.sqrt(metrics.mean_squared_error(y_lin_te, y_lin_pred))
# RMSE = 74.76
print linreg.intercept_
print linreg.coef_
zip(imp_feature_cols, linreg.coef_)
# Compare to null RMSE

# create a NumPy array with the same shape as y_test
y_lin_null = np.zeros_like(y_lin_te, dtype=float)

# fill the array with the mean value of y_test
y_lin_null.fill(y_lin_te.mean())

# compute null RMSE
np.sqrt(metrics.mean_squared_error(y_lin_te, y_lin_null))
# RMSE = 80.80

# My model performs better than null!<jupyter_output><empty_output><jupyter_text>Conclusions from Linear RegressionLooking at the coefficients, a player would have to gain ~1 rushing or receiving yard point to improve their rank by 1. Based on the point mapping, that means they would have to gain at least 10 more yards to improve their rank. Similarly, with total points, players would have to gain ~8 points in order to improve their rank by 1.  This information is helpful when looking at trends in a player's performance.  Test work - write a loop to test linear regressions on different filled in dataframesI filled in NaNs using mean, zero, and medianWrite a loop to test which one compares best against null model<jupyter_code># dropping nulls
nflzero.dropna(inplace = True)
nflmedian.dropna(inplace = True)
# reminder: dataframes are called nflmean, nflzero, nflmedian

# from sklearn.linear_model import LinearRegression
# from sklearn.model_selection import train_test_split
# from sklearn import metrics

nflframes = [nflmean, nflzero, nflmedian]

for frame in nflframes:
    linreg = LinearRegression() # constructing the linear regression object
    # creating feature matrix and response vector
    cols = ['totalpoints','rushing_yds_pts','receiving_yds_pts']
    features = frame[cols]
    response = frame['rank']    
    feat_train, feat_test, res_train, res_test = train_test_split(features, response, random_state=1)
    # fit the model
    linreg.fit(feat_train, res_train)
    # predict from model and determine error
    res_pred = linreg.predict(feat_test)
    print np.sqrt(metrics.mean_squared_error(res_test, res_pred))
    print linreg.intercept_
    print zip(cols, linreg.coef_)

# mean, zero, median
# 74.76, 73.14, 73.68
# nflzero seems to have the least error so filling in with zeros may be a more accurate representation of rank<jupyter_output>74.7562427987
211.808173066
[('totalpoints', -0.12615973386161361), ('rushing_yds_pts', -0.94943358138773681), ('receiving_yds_pts', -0.81969632808597337)]
73.1392613688
201.012791834
[('totalpoints', -0.16848172099993183), ('rushing_yds_pts', -0.82456112687951044), ('receiving_yds_pts', -0.74961096402562311)]
73.6843564336
205.283792028
[('totalpoints', -0.13040242976070446), ('rushing_yds_pts', -0.92294034217393128), ('receiving_yds_pts', -0.82668496976511252)]<jupyter_text>Future work- See if I can predict improvement in players across years.  - Split apart field goals into 3 and 5 points. For now, just scored all field goals made as 3 points since I did not want to loop through all kickers of each game across the entire season to determine which kicks were longer than others.- For future work, I would like to aggregate players of a team's defense and pull a full team score across the season to further improve my rank predictions.- Try to draft position rank besides positions (e.g. WR1, WR2)- Turn this into a product with a user interface to select a player and determine their rank against other players. Unfortunately, while a good lesson, "who should I pick" already exists as a product.<jupyter_code># 2017 top 200 fantasy player rankings http://www.espn.com/fantasy/football/story/_/page/17RanksPreseason200nonPPR/2017-fantasy-football-standard-rankings-non-ppr-top-200
url = path + 'season2017.csv'
nfl17 = pd.read_csv(url, index_col = 0)
nfl17.head()
nfl17.loc[nfl17.index == 'T.Siemian']<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0764955389790844, 'fraction_numerical': 0.0298376480912681, 'mean_word_length': 5.284313725490196, 'pattern_counts': {'":': 0, '<': 42, '<?xml version=': 0, '>': 42, 'https://': 4, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 3, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '99814', 'license': 'MIT', 'n_tokens_mistral': 6935, 'n_tokens_neox': 6345, 'n_words': 2462, 'path': 'project.ipynb', 'repo_name': 'wendybo/NFLrank', 'size': '20511'}