Year after year, I have been part of a NBA fantasy league. Year after year, I have been losing. Not just losing, but not even making the playoffs. Imagine someone who’s career is in data analytics, completely failing at something where evaluating data is key. Well, I have had enough and there is only so much pounding a person can take. ENOUGH!

It is time to put my superpowers to the test.

This will be a multiple part series of blog posts as I explore what is the best way to win at fantasy sports. The league that I am in is the most difficult because it is an auction draft keeper league. Where it is not just important to have the best players and evaluate based on rank, but how to properly price players will be very important.

Let us start with the basics. Assuming we have perfect data on a players performance for the current year, how do you build the most optimal team. There are many different strategies to doing this. For this example league, assume a 9 category, head-to-head matchup league. There are a couple of approaches people have to this problem, the most common would be to choose 5 or 6 out of the 9 categories and create a linear optimization problem where based on constraints, maximizing over those categories.

My gripe with this strategy is it is not flexible and is not able to adapt based on what other fantasy owners are doing during the draft. What if majority of other fantasy owners are going after the same categories and players are taken off the board?

Fantasy Player Rankings

Below is a list of top players taken from https://www.fantraxhq.com/2019-fantasy-basketball-rankings/. The analyst here says that turnover category is not considered, but ranking is based on per-game production. I have always been curious if value of a player changes based on the size of the league. For eg, does having a player that over-indexes on shot blocking and provides nothing else more beneficial in a 4 person league than a 12 person league? My problem with ranking based on overall production metric is that if there are many players that over index in assists and steals and few that over index in rebounds and blocks, then you may miss the underlying value of players.

An Alternative

Run simulations of different random teams and see which player influences the most toward winning percentage. This would be similar to win-share. As the draft continues and owners draft players, you are able to run random simulations on the remainder of players to draft the next best available player.

Prepping data

For the data mining, I have used Yahoo_fantasy_basketball_analyzer as a starting off point. It includes some other tools and has some ML to do some predictions, but I have just used it for data mining of basketballreference. The github instructions are great. The repo also adds z-scores for each players statistical categories so that you are able to see how much a player deviates from the league average.

What the library does not do is add position of a player, so we will need to add that in.

from IPython.display import clear_output
from yahoo_oauth import OAuth2
import yahoo_fantasy_api as yfa
import pandas as pd
import numpy as np
import timeit
import time
import sys

sc = OAuth2(None, None, from_file = 'oauth2.json')
game = yfa.Game(sc, 'nba')

lg = game.to_league(game.league_ids(year=2019)[0])

# csv file is outputted by Yahoo_fantasy_basketball_analyzer 
players = pd.read_csv('2019-2020_players.csv')
[2020-07-07 16:16:56,900 DEBUG] [yahoo_oauth.oauth.__init__] Checking 
[2020-07-07 16:16:56,901 DEBUG] [yahoo_oauth.oauth.token_is_valid] ELAPSED TIME : 22.219847440719604
[2020-07-07 16:16:56,902 DEBUG] [yahoo_oauth.oauth.token_is_valid] TOKEN IS STILL VALID

Add dummy variables for each player based on whether they are eligible for a position.

Things not shown are some of the data cleaning for some of the players names not matching between basketball reference and yahoo sports API

is_g = []
is_pg = []
is_sg = []
is_f = []
is_sf = []
is_pf = []
is_c = []
is_util = []

for row in players['Player']:
    positions = lg.player_details(row)[0].get('eligible_positions')
    if {'position': 'G'} in positions:
        is_g.append(1)
    else:
        is_g.append(0)
    if {'position': 'PG'} in positions:
        is_pg.append(1)
    else:
        is_pg.append(0)
    if {'position': 'SG'} in positions:
        is_sg.append(1)
    else:
        is_sg.append(0)
    if {'position': 'F'} in positions:
        is_f.append(1)
    else:
        is_f.append(0)
    if {'position': 'SF'} in positions:
        is_sf.append(1)
    else:
        is_sf.append(0)
    if {'position': 'PF'} in positions:
        is_pf.append(1)
    else:
        is_pf.append(0)
    if {'position': 'C'} in positions:
        is_c.append(1)
    else:
        is_c.append(0)
    if {'position': 'Util'} in positions:
        is_util.append(1)
    else:
        is_util.append(0)
        
players['is_g'] = is_g
players['is_pg'] = is_pg
players['is_sg'] = is_sg
players['is_f'] = is_f
players['is_sf'] = is_sf
players['is_pf'] = is_pf
players['is_c'] = is_c
players['is_util'] = is_util

Here we have our player 2019-2020 stat summary that we will be using. It has their 9 categories, in addition to FGM, FGA, FTM, FTA, 3PTM, 3PTA. The volume of shots in these categories influences the team performance for FG%, FT%, and 3PT%. So unless you can translate volume and percentage into a single variable, it would not be possible to do linear optimization. Another reason I found to run simulations.

It also has the Zscores that I will not be using, and dummy variables for eligible positions.

players.head(5)
Player FGM FGA FG% FTM FTA FT% 3PTM 3PTA 3PT% ... zTO zTotal is_g is_pg is_sg is_f is_sf is_pf is_c is_util
0 Aaron Gordon 5.414 12.517 0.433 2.362 3.500 0.675 1.172 3.897 0.301 ... -0.076 0.136 0 0 0 1 1 1 0 1
1 Aaron Holiday 3.483 8.552 0.407 1.069 1.241 0.861 1.379 3.500 0.394 ... 0.319 -0.348 1 1 1 0 0 0 0 1
2 Al Horford 4.817 10.900 0.442 0.917 1.217 0.753 1.483 4.400 0.337 ... 0.572 0.141 0 0 0 1 0 1 1 1
3 Al-Farouq Aminu 1.389 4.778 0.291 1.056 1.611 0.655 0.500 2.000 0.250 ... 0.740 -0.590 0 0 0 1 1 1 0 1
4 Alec Burks 4.797 11.881 0.404 3.814 4.254 0.896 1.695 4.627 0.366 ... 0.141 0.219 1 1 1 0 0 0 0 1

5 rows × 44 columns

Draft Random Teams

The plan is to

  1. Run a draft where a player record has a random team
  2. Get the league results based on the draft, each player will be given the result based on random team they were put on
  3. Get a summary of all the drafts and player performance over multiple random teams (get_draft_summary)
  4. Run multiple simulations and shrink the player pool to top performers

Let’s explain number 4. I have run the simulation with a 4 team league. With 300 players in our dataframe and 4 teams only requiring 4 * 14 = 56 players, you are getting a lot of simulations of teams that players never to be drafted in this system. This could create a bias in the result. So there needs to be a methodology that over time, players are removed from the draft pool.

def create_teams(num_teams, eligible_players):
    # Uses global players dataframe
    # Adds rand_team assignment to dataframe
    # For each team and position, find a random player and assign to team
    # End of player selection process, only keep drafted players
    # ----------------------
    # Get team performance compared to other teams
    # Number of categories won compared to other teams
    # Number of head to head matchups won compared to other teams
    # return summary dataframe (player, rand_team, performance metrics)

    draft_players = players
    draft_players = draft_players[draft_players['Player'].isin(eligible_players)]
    draft_players['rand_team'] = 0

    for i in range(1,num_teams + 1):
        # find a PG
        rand_player = draft_players[(draft_players['is_pg']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a SG
        rand_player = draft_players[(draft_players['is_sg']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a G
        rand_player = draft_players[(draft_players['is_g']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a SF
        rand_player = draft_players[(draft_players['is_sf']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a PF
        rand_player = draft_players[(draft_players['is_pf']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a F
        rand_player = draft_players[(draft_players['is_f']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a C
        rand_player = draft_players[(draft_players['is_c']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a C
        rand_player = draft_players[(draft_players['is_c']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a Util
        rand_player = draft_players[(draft_players['is_util']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a Util
        rand_player = draft_players[(draft_players['is_util']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a Util
        rand_player = draft_players[(draft_players['is_util']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a Util
        rand_player = draft_players[(draft_players['is_util']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a Util
        rand_player = draft_players[(draft_players['is_util']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

        # find a Util
        rand_player = draft_players[(draft_players['is_util']==1) & (draft_players['rand_team']==0)].sample()
        rand_player = rand_player.Player.to_string(index=False).strip()
        draft_players.loc[draft_players['Player'] == rand_player, ['rand_team']] = i

    # select only players drafted
    draft_players = draft_players[draft_players['rand_team']>0]
    
    # get draft team results
    draft_teams = draft_players.groupby(['rand_team'])[['FGM','FGA','FTM','FTA','3PTM','PTS','REB','AST','ST','BLK','TO']].apply(sum).reset_index()

    draft_teams['FGP'] = draft_teams['FGM'] / draft_teams['FGA']
    draft_teams['FTP'] = draft_teams['FTM'] / draft_teams['FTA']
    
    # get scores
    cat_win = []
    cat_loss = []
    matchup_win = []
    matchup_loss = []
    for curr_team in draft_teams['rand_team']:
        hh_cat_win = 0
        hh_cat_loss = 0
        hh_match_win = 0
        hh_match_loss = 0

        for matchup_team in draft_teams['rand_team']:
            hhwins = 0
            hhlosses = 0
            # loop only for other teams
            if curr_team != matchup_team:
                # FGP
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['FGP']) > float(draft_teams[draft_teams['rand_team']==matchup_team]['FGP'])):
                    hhwins += 1
                else:
                    hhlosses += 1
                # FTP
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['FTP']) > float(draft_teams[draft_teams['rand_team']==matchup_team]['FTP'])):
                    hhwins += 1
                else:
                    hhlosses += 1
                # PTS
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['PTS']) > float(draft_teams[draft_teams['rand_team']==matchup_team]['PTS'])):
                    hhwins += 1
                else:
                    hhlosses += 1
                # REBS
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['REB']) > float(draft_teams[draft_teams['rand_team']==matchup_team]['REB'])):
                    hhwins += 1
                else:
                    hhlosses += 1
                # AST
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['AST']) > float(draft_teams[draft_teams['rand_team']==matchup_team]['AST'])):
                    hhwins += 1
                else:
                    hhlosses += 1
                # ST
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['ST']) > float(draft_teams[draft_teams['rand_team']==matchup_team]['ST'])):
                    hhwins += 1
                else:
                    hhlosses += 1
                # BLK
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['BLK']) > float(draft_teams[draft_teams['rand_team']==matchup_team]['BLK'])):
                    hhwins += 1
                else:
                    hhlosses += 1
                # BLK
                if (float(draft_teams[draft_teams['rand_team']==curr_team]['TO']) < float(draft_teams[draft_teams['rand_team']==matchup_team]['TO'])):
                    hhwins += 1
                else:
                    hhlosses += 1

                # Add heads up result
                if hhwins > hhlosses:
                    hh_match_win += 1
                else:
                    hh_match_loss += 1

            # Add heads up category totals
            hh_cat_win = hh_cat_win + hhwins
            hh_cat_loss = hh_cat_loss + hhlosses

        # when current team match over append scores
        cat_win.append(hh_cat_win)
        cat_loss.append(hh_cat_loss)
        matchup_win.append(hh_match_win)
        matchup_loss.append(hh_match_loss)

    draft_teams['cat_win'] = cat_win
    draft_teams['cat_loss'] = cat_loss
    draft_teams['matchup_win'] = matchup_win
    draft_teams['matchup_loss'] = matchup_loss
    
    rand_summary = draft_players[['Player','rand_team']].merge(draft_teams[['rand_team','cat_win','cat_loss','matchup_win','matchup_loss']], on='rand_team')

    return rand_summary
def get_draft_summary(draft_players):
    sim_summary = draft_players.groupby(['Player'])[['cat_win','cat_loss','matchup_win','matchup_loss']].apply(sum).reset_index()
    sim_summary['cat_perc'] = sim_summary['cat_win'] / (sim_summary['cat_win'] + sim_summary['cat_loss'])
    sim_summary['matchup_perc'] = sim_summary['matchup_win'] / (sim_summary['matchup_win'] + sim_summary['matchup_loss'])
    return sim_summary
def fantasy_player_rank(num_teams, simulations):

    draft_players = players
    eligible_players = players['Player'].tolist()
    start = timeit.default_timer()
    
    staticTrimSize = 10
    staticTrimInterval = 20
    
    num_trim_players = 0
    
    for r in range(1,simulations + 1):
        clear_output(wait=True)
        draft_players = draft_players.append(create_teams(num_teams, eligible_players), ignore_index=True)

        stop = timeit.default_timer()

        if( r/simulations*100) < 5:
            expected_time = "Calculating..."
        else:
            time_perc = timeit.default_timer()
            expected_time = np.round( (time_perc-start)/(r/simulations) / 60,2)

        print("Current progress:", np.round(r/simulations*100,2), "%")
        print("Current run time:", np.round((stop - start)/60,2), "minutes")
        print("Exptected run time:", expected_time, "minutes")
        
        # get the number of times player has been drafted from still eligible Player dataframe
        # remove players from simulation that will not be drafted by teams, and bias simulation
        
        num_player_drafted_df = draft_players[['Player','rand_team']].groupby(['Player']).agg('count').reset_index()
        num_player_drafted_df.columns = ['Player','occurrences']
        num_player_drafted_df = num_player_drafted_df.merge(players['Player'], how='inner', on='Player')
        
        # min_occurrences from eligible players 300 - (num_trim_players * num_trim_per_round)
        num_player_drafted_df = num_player_drafted_df.sort_values(by=['occurrences'], ascending=False)
        num_player_drafted_df = num_player_drafted_df[:(300 - num_trim_players * staticTrimSize)]
        min_occurrences_of_player = num_player_drafted_df['occurrences'].min()
        
        print('Number of teams:', num_teams)
        print('Num Trim Players:', num_trim_players)
        print('Number of rows in eligible players dataframe:', len(eligible_players))
        print('Minimum times player has been drafted in eligible num_player_drafted dataframe:', min_occurrences_of_player)
        print('Number of rows in num_player_drafted:', len(num_player_drafted_df))
        
        # for first trimming of players, must have 300 players drafted and each player at least 5 occurrences
        # subsequent times, must have at least 5 more occurences than the last
        # also need to make sure there are enough players left to draft
        if ((min_occurrences_of_player == staticTrimInterval) and len(num_player_drafted_df) == 300 and (len(players) > num_teams * 16 )) or \
        ((min_occurrences_of_player == staticTrimInterval * (num_trim_players + 1)) and (len(eligible_players) > num_teams * 16 + staticTrimSize)) :
            
            # evaluate players and number of eligible players = 300 - (num_trim_players * num_trim_per_round)
            num_trim_players += 1
            sim_summary = get_draft_summary(draft_players).sort_values(by=['cat_perc'], ascending=False)
            eligible_players = sim_summary['Player'][:(300 - num_trim_players * staticTrimSize)].tolist()

    sim_summary = draft_players.groupby(['Player'])[['cat_win','cat_loss','matchup_win','matchup_loss']].apply(sum).reset_index()
    sim_summary['cat_perc'] = sim_summary['cat_win'] / (sim_summary['cat_win'] + sim_summary['cat_loss'])
    sim_summary['matchup_perc'] = sim_summary['matchup_win'] / (sim_summary['matchup_win'] + sim_summary['matchup_loss'])
    return sim_summary
#hide
players = pd.read_csv('2019-2020_players_with_positions.csv')
players = players.drop(columns=['Unnamed: 0'])
fantasy_6teams_2ndrun = fantasy_player_rank(6,2000)
Current progress: 100.0 %
Current run time: 16.64 minutes
Exptected run time: 16.64 minutes
Number of teams: 6
Num Trim Players: 18
Number of rows in eligible players dataframe: 120
Minimum times player has been drafted in eligible num_player_drafted dataframe: 845
Number of rows in num_player_drafted: 120
fantasy_6teams_2ndrun.sort_values(by=['cat_perc'], ascending=False).head(20)
Player cat_win cat_loss matchup_win matchup_loss cat_perc matchup_perc
12 Anthony Davis 11200.0 7745.0 1510.0 595.0 0.591185 0.717340
132 James Harden 9009.0 6246.0 1210.0 485.0 0.590560 0.713864
165 Kawhi Leonard 8779.0 6296.0 1176.0 499.0 0.582355 0.702090
164 Karl-Anthony Towns 9562.0 7043.0 1255.0 590.0 0.575851 0.680217
186 Kyrie Irving 8814.0 6666.0 1169.0 551.0 0.569380 0.679651
52 Damian Lillard 7750.0 5885.0 1020.0 495.0 0.568390 0.673267
147 John Collins 10536.0 8274.0 1341.0 749.0 0.560128 0.641627
241 Paul George 7955.0 6445.0 1015.0 585.0 0.552431 0.634375
283 Trae Young 7171.0 5834.0 915.0 530.0 0.551403 0.633218
192 LeBron James 11333.0 9277.0 1440.0 850.0 0.549879 0.628821

Runtime Problem

The results have not settled with even 5000 simulations, and the runtime is ballooning. If we wanted to run simulations during a live draft, we only have so much time to make a decision, so we are going to have to make this go faster.

Multiprocessing, Windows, Jupyter Python Interactive Shell So many problems and so many attempts to make this work. Long story short, in order for parallel processing to work, it needs to exist in its own script and main.

## main in mock_draft_parallel.py
if __name__ == '__main__':

  # regular run
  # starttime = time.perf_counter()
  # results = create_teams(6)
  # for i in range(5):
  #   results = results.append(create_teams(6))

  # endtime = time.perf_counter()
  # print(f'Finished in {round(endtime-starttime,2)} seconds(s)')

  # parallel run
  starttime = time.perf_counter()

  draft_players = players
  eligible_players = players['Player'].tolist()
  num_trim_players = 0
  num_teams = int(sys.argv[1])
  num_simulations = int(sys.argv[2])
  filename = sys.argv[3]

  staticTrimSize = 10
  staticTrimInterval = 20

  with concurrent.futures.ProcessPoolExecutor() as executor:

    # list comprehension
    results = [executor.submit(create_teams, num_teams, eligible_players) for _ in range(num_simulations)]

    for f in concurrent.futures.as_completed(results):
      draft_players = draft_players.append(f.result(), ignore_index=True)
      
      # get the number of times player has been drafted from still eligible Player dataframe
      # remove players from simulation that will not be drafted by teams, and bias simulation
      
      if(num_teams * 16 < len(eligible_players)):
        num_player_drafted_df = draft_players[['Player','rand_team']].groupby(['Player']).agg('count').reset_index()
        num_player_drafted_df.columns = ['Player','occurrences']
        
        # min_occurrences from eligible players 300 - (num_trim_players * num_trim_per_round)
        num_player_drafted_df = num_player_drafted_df.sort_values(by=['occurrences'], ascending=False)
        num_player_drafted_df = num_player_drafted_df[:(300 - num_trim_players * staticTrimSize)]
        min_occurrences_of_player = num_player_drafted_df['occurrences'].min()
      
      # for first trimming of players, must have 300 players drafted and each player at least 5 occurrences
      # subsequent times, must have at least 5 more occurences than the last
      # also need to make sure there are enough players left to draft
      if ((min_occurrences_of_player == staticTrimInterval) and len(num_player_drafted_df) == 300 and (len(players) > num_teams * 16 )) or \
      ((min_occurrences_of_player == staticTrimInterval * (num_trim_players + 1)) and (len(eligible_players) > num_teams * 16)) :
          
        # evaluate players and number of eligible players = 300 - (num_trim_players * num_trim_per_round)
        num_trim_players += 1
        sim_summary = get_draft_summary(draft_players).sort_values(by=['cat_perc'], ascending=False)
        eligible_players = sim_summary['Player'][:(300 - num_trim_players * staticTrimSize)].tolist()
          

  endtime = time.perf_counter()
  print(f'Finished in {round(endtime-starttime,2)} seconds(s)')

  sim_summary = draft_players.groupby(['Player'])[['cat_win','cat_loss','matchup_win','matchup_loss']].apply(sum).reset_index()
  sim_summary['cat_perc'] = sim_summary['cat_win'] / (sim_summary['cat_win'] + sim_summary['cat_loss'])
  sim_summary['matchup_perc'] = sim_summary['matchup_win'] / (sim_summary['matchup_win'] + sim_summary['matchup_loss'])
  sim_summary = sim_summary.sort_values(by=['cat_perc'], ascending=False)
  sim_summary.to_csv(filename)
import mock_draft_parallel
%run mock_draft_parallel 6 2000 parallel_result.csv
Finished in 253.84 seconds(s)
pd.read_csv('parallel_result.csv').drop(columns=['Unnamed: 0']).head(20)
Player cat_win cat_loss matchup_win matchup_loss cat_perc matchup_perc
0 Anthony Davis 17520.0 11080.0 2218.0 1357.0 0.612587 0.620420
1 James Harden 12132.0 8508.0 1445.0 1135.0 0.587791 0.560078
2 Kawhi Leonard 12492.0 8908.0 1471.0 1204.0 0.583738 0.549907
3 Kyrie Irving 13217.0 9463.0 1556.0 1279.0 0.582760 0.548854
4 John Collins 16638.0 12002.0 1972.0 1608.0 0.580936 0.550838
5 Hassan Whiteside 12553.0 9247.0 1487.0 1238.0 0.575826 0.545688
6 Jimmy Butler 12365.0 9355.0 1419.0 1296.0 0.569291 0.522652
7 Nikola Jokic 16066.0 12254.0 1865.0 1675.0 0.567302 0.526836
8 Chris Paul 9703.0 7457.0 1124.0 1021.0 0.565443 0.524009
9 Karl-Anthony Towns 12902.0 9938.0 1496.0 1359.0 0.564886 0.523993
10 Bradley Beal 9490.0 7390.0 1073.0 1037.0 0.562204 0.508531
11 Damian Lillard 10021.0 7819.0 1133.0 1097.0 0.561715 0.508072
12 Deandre Ayton 12189.0 9531.0 1406.0 1309.0 0.561188 0.517864
13 Richaun Holmes 16545.0 13055.0 1846.0 1854.0 0.558953 0.498919
14 Giannis Antetokounmpo 11715.0 9245.0 1394.0 1226.0 0.558922 0.532061
15 Jonathan Isaac 12010.0 9510.0 1386.0 1304.0 0.558086 0.515242
16 Andre Drummond 16652.0 13188.0 1962.0 1768.0 0.558043 0.526005
17 Clint Capela 15562.0 12358.0 1767.0 1723.0 0.557378 0.506304
18 DeMar DeRozan 12458.0 9902.0 1369.0 1426.0 0.557156 0.489803
19 Ben Simmons 9960.0 7920.0 1137.0 1098.0 0.557047 0.508725

Parallel Time Improvement

The parallel run improved the run time for the 2000 simulations from 1000 seconds to 253 seconds. That is a 4x improvement. Draft time usually allows for 2 minutes per round, so will need to run slightly less simulations, but as teams become filled and number of permutations decreases, it will need less simulations to become accurate.

Top Fantasy Players 2019-2020 Season

I have ran simulations for all players and here are the rankings for 6-team, 8-team, 10-team, 12-team, 14-team leagues.

team6_rank_df = pd.read_csv('teams_6_simulations.csv').drop(columns=['Unnamed: 0'])
team8_rank_df = pd.read_csv('teams_8_simulations.csv').drop(columns=['Unnamed: 0'])
team10_rank_df = pd.read_csv('teams_10_simulations.csv').drop(columns=['Unnamed: 0'])
team12_rank_df = pd.read_csv('teams_12_simulations.csv').drop(columns=['Unnamed: 0'])
team14_rank_df = pd.read_csv('teams_14_simulations.csv').drop(columns=['Unnamed: 0'])
team6_rank_df = team6_rank_df.rename(columns={"Player": "6-team"})
team8_rank_df = team8_rank_df.rename(columns={"Player": "8-team"})
team10_rank_df = team10_rank_df.rename(columns={"Player": "10-team"})
team12_rank_df = team12_rank_df.rename(columns={"Player": "12-team"})
team14_rank_df = team14_rank_df.rename(columns={"Player": "14-team"})
pd.set_option('display.max_rows', 100)
team6_rank_df[['6-team']].merge(team8_rank_df[['8-team']], left_index=True, right_index=True) \
    .merge(team10_rank_df[['10-team']], left_index=True, right_index=True) \
    .merge(team12_rank_df[['12-team']], left_index=True, right_index=True) \
    .merge(team14_rank_df[['14-team']], left_index=True, right_index=True) \
    .head(100)
6-team 8-team 10-team 12-team 14-team
0 Anthony Davis Anthony Davis Anthony Davis Anthony Davis Anthony Davis
1 James Harden Kawhi Leonard Kawhi Leonard Kawhi Leonard Kawhi Leonard
2 Kawhi Leonard James Harden James Harden Kyrie Irving James Harden
3 Kyrie Irving Kyrie Irving Hassan Whiteside James Harden Kyrie Irving
4 Hassan Whiteside Hassan Whiteside Kyrie Irving Hassan Whiteside Hassan Whiteside
5 John Collins John Collins John Collins Jimmy Butler Jimmy Butler
6 Jimmy Butler Jimmy Butler Jimmy Butler John Collins John Collins
7 Nikola Jokic Jonathan Isaac Nikola Jokic Nikola Jokic Jonathan Isaac
8 Damian Lillard Nikola Jokic Deandre Ayton Karl-Anthony Towns Karl-Anthony Towns
9 Deandre Ayton Damian Lillard Karl-Anthony Towns Deandre Ayton Nikola Jokic
10 Karl-Anthony Towns Karl-Anthony Towns Damian Lillard Damian Lillard Deandre Ayton
11 Jonathan Isaac Chris Paul Giannis Antetokounmpo Jonathan Isaac Ben Simmons
12 DeMar DeRozan Giannis Antetokounmpo Ben Simmons Ben Simmons Clint Capela
13 Bam Adebayo Richaun Holmes Chris Paul Chris Paul Damian Lillard
14 LeBron James Deandre Ayton DeMar DeRozan Richaun Holmes Chris Paul
15 Giannis Antetokounmpo DeMar DeRozan Richaun Holmes Clint Capela Giannis Antetokounmpo
16 Chris Paul Andre Drummond LeBron James Giannis Antetokounmpo DeMar DeRozan
17 Richaun Holmes Clint Capela Joel Embiid Rudy Gobert Andre Drummond
18 Clint Capela Joel Embiid Jonathan Isaac DeMar DeRozan Richaun Holmes
19 Rudy Gobert Bam Adebayo Clint Capela Andre Drummond Russell Westbrook
20 Ben Simmons Ben Simmons Andre Drummond Bam Adebayo LeBron James
21 Russell Westbrook LeBron James Bam Adebayo LeBron James Joel Embiid
22 Andre Drummond LaMarcus Aldridge LaMarcus Aldridge Joel Embiid Bam Adebayo
23 Jayson Tatum Rudy Gobert Russell Westbrook Bradley Beal Rudy Gobert
24 Devin Booker Russell Westbrook Rudy Gobert Russell Westbrook LaMarcus Aldridge
25 LaMarcus Aldridge Nikola Vucevic Bradley Beal Khris Middleton Bradley Beal
26 Joel Embiid Devin Booker Nikola Vucevic Jayson Tatum Nikola Vucevic
27 Khris Middleton Jayson Tatum Khris Middleton Nikola Vucevic Jayson Tatum
28 Shai Gilgeous-Alexander Pascal Siakam Devin Booker LaMarcus Aldridge Khris Middleton
29 Nikola Vucevic Khris Middleton Stephen Curry Devin Booker Devin Booker
30 Jrue Holiday Luka Doncic Trae Young Brandon Ingram Brandon Ingram
31 Bradley Beal Stephen Curry Luka Doncic Stephen Curry Luka Doncic
32 Brandon Ingram Jrue Holiday Kyle Lowry Jrue Holiday Stephen Curry
33 Stephen Curry Brandon Ingram Fred VanVleet Shai Gilgeous-Alexander Jrue Holiday
34 Luka Doncic Bradley Beal Pascal Siakam Domantas Sabonis Shai Gilgeous-Alexander
35 Gordon Hayward Shai Gilgeous-Alexander Jayson Tatum Luka Doncic Pascal Siakam
36 Jonas Valanciunas Trae Young Shai Gilgeous-Alexander Mitchell Robinson Domantas Sabonis
37 Fred VanVleet Fred VanVleet Brandon Ingram Pascal Siakam Gordon Hayward
38 Domantas Sabonis Kristaps Porzingis Jrue Holiday Trae Young Mitchell Robinson
39 Trae Young Gordon Hayward Mitchell Robinson Fred VanVleet Fred VanVleet
40 Kyle Lowry Kyle Lowry Domantas Sabonis Gordon Hayward Kristaps Porzingis
41 T.J. Warren Jonas Valanciunas Gordon Hayward Kyle Lowry Trae Young
42 Mitchell Robinson Domantas Sabonis Kristaps Porzingis Jonas Valanciunas Kyle Lowry
43 Ricky Rubio Mitchell Robinson Ricky Rubio Kristaps Porzingis T.J. Warren
44 Zach LaVine Ricky Rubio T.J. Warren Paul George Jonas Valanciunas
45 Pascal Siakam Paul George Jonas Valanciunas Ricky Rubio Ricky Rubio
46 Dejounte Murray Tobias Harris Paul George T.J. Warren Paul George
47 Kristaps Porzingis Zach LaVine Dejounte Murray Tobias Harris Tobias Harris
48 Paul George Robert Covington Jamal Murray Nerlens Noel Jamal Murray
49 Jamal Murray T.J. Warren Nerlens Noel Dejounte Murray Dejounte Murray
50 Robert Covington Donovan Mitchell Robert Covington Mikal Bridges Robert Covington
51 OG Anunoby Nerlens Noel Zach LaVine Myles Turner Kelly Oubre
52 Kemba Walker Norman Powell Kris Dunn Zach LaVine Nerlens Noel
53 Kelly Oubre Jamal Murray Kelly Oubre Jamal Murray Zach LaVine
54 Jarrett Allen Kelly Oubre Will Barton Kelly Oubre Myles Turner
55 Norman Powell Will Barton Tobias Harris Derrick Favors Mikal Bridges
56 Donovan Mitchell Daniel Theis Myles Turner Robert Covington Norman Powell
57 Elfrid Payton Brook Lopez Norman Powell Norman Powell Eric Bledsoe
58 Will Barton Dejounte Murray Mikal Bridges Daniel Theis Marcus Smart
59 Nerlens Noel Mikal Bridges Brook Lopez Kemba Walker Donovan Mitchell
60 Kris Dunn Derrick Favors Derrick Favors Donovan Mitchell Kris Dunn
61 De'Aaron Fox Kemba Walker Donovan Mitchell OG Anunoby Al Horford
62 Tobias Harris Al Horford Kemba Walker Brook Lopez OG Anunoby
63 Montrezl Harrell Malcolm Brogdon Brandon Clarke Jabari Parker Jarrett Allen
64 Brook Lopez Jarrett Allen OG Anunoby Will Barton Brook Lopez
65 Eric Bledsoe Jabari Parker Danilo Gallinari Steven Adams Montrezl Harrell
66 Derrick Favors Kris Dunn Eric Bledsoe Al Horford Daniel Theis
67 Marcus Smart Eric Bledsoe Jaylen Brown Kris Dunn Derrick Favors
68 Al Horford Myles Turner Derrick Jones Marcus Smart Jeremy Lamb
69 Steven Adams Marcus Smart Montrezl Harrell Eric Bledsoe Kemba Walker
70 Mikal Bridges Derrick Rose Jabari Parker Derrick Jones Jaylen Brown
71 Myles Turner Jeremy Lamb Malcolm Brogdon Brandon Clarke Will Barton
72 Jeremy Lamb Brandon Clarke Jarrett Allen De'Aaron Fox De'Aaron Fox
73 Brandon Clarke De'Aaron Fox De'Aaron Fox CJ McCollum CJ McCollum
74 Ja Morant OG Anunoby Daniel Theis Jarrett Allen Malcolm Brogdon
75 Jabari Parker Jaylen Brown Steven Adams Nemanja Bjelica Jabari Parker
76 Daniel Theis Steven Adams Derrick Rose Montrezl Harrell Steven Adams
77 CJ McCollum Montrezl Harrell Al Horford Derrick Rose Brandon Clarke
78 Derrick Rose Elfrid Payton Evan Fournier Jeremy Lamb Derrick Rose
79 Derrick Jones CJ McCollum DeAndre Jordan Malcolm Brogdon Danilo Gallinari
80 Draymond Green Willie Cauley-Stein Marcus Smart Lonzo Ball Alec Burks
81 Donte DiVincenzo Lonzo Ball CJ McCollum Danilo Gallinari Zion Williamson
82 Danilo Gallinari Derrick Jones Jeremy Lamb Jaylen Brown Elfrid Payton
83 Jaylen Brown Alec Burks Willie Cauley-Stein Willie Cauley-Stein Derrick Jones
84 D'Angelo Russell Evan Fournier Alec Burks Alec Burks Derrick White
85 Kevin Love Danilo Gallinari Elfrid Payton Zion Williamson Evan Fournier
86 Evan Fournier Thomas Bryant Derrick White Elfrid Payton Kevin Love
87 Collin Sexton Draymond Green Andrew Wiggins DeAndre Jordan Thomas Bryant
88 Malcolm Brogdon Rui Hachimura Ja Morant D'Angelo Russell Lonzo Ball
89 Lonzo Ball Andrew Wiggins Nemanja Bjelica Derrick White Draymond Green
90 Larry Nance Wendell Carter Donte DiVincenzo Donte DiVincenzo Nemanja Bjelica
91 Delon Wright D'Angelo Russell Marquese Chriss Evan Fournier Andrew Wiggins
92 Andrew Wiggins Markelle Fultz Draymond Green Serge Ibaka DeAndre Jordan
93 Marquese Chriss Kevin Love D'Angelo Russell Patrick Beverley Willie Cauley-Stein
94 Alec Burks Larry Nance Serge Ibaka Ja Morant Serge Ibaka
95 Jaren Jackson Aaron Gordon Lonzo Ball Tomas Satoransky Paul Millsap
96 Jae Crowder Derrick White Justin Holiday Andrew Wiggins Collin Sexton
97 Glenn Robinson DeAndre Jordan Rui Hachimura Kevin Love Glenn Robinson
98 DeAndre Jordan Ja Morant Glenn Robinson Larry Nance Aaron Gordon
99 Nemanja Bjelica Nemanja Bjelica Wendell Carter Marquese Chriss Rui Hachimura

Reflection

There is not that much difference in value across different league size as I was expencting.

Is this a valid approach with a larger player size? What if there was 10,000 players and 30 teams? This process would still be possible with methods to filter the population quicker and learn who are the eligible players.

With perfect information of the season ahead, this methodology does a good job of ranking players. Future steps will involve predicting player season. The good thing about what I have created is, you can add in players/rookies and edit stats for the year and run the simulation over again. For example, you will probably want to edit Klay Thompson and Steph Curry who were injured with 2020-2021 predicted stats.

Next Steps

Part 2: Develop a system to draft in a snake draft in process. Let us take this concept for a test drive in a mock draft.