The goal of this notebook is to do some analysis and forecasting as a tool to plan for an upcoming fantasy baseball draft. Data will be pulled from ESPN regarding player statistics and teams in the fantasy league. I will come up with features for prediction from my experience with previous fantasy league drafts.
Some data has been prepared through the data/update.py
script outside of the function of this notebook. We will pull this data in to get us started.
import json
import ast
import numpy as np
import pandas as pd
from sklearn.preprocessing import normalize, MinMaxScaler
# pull in our data from the csv
data = pd.read_csv('analysis/data/draft-players-2023.csv')
# The first column is the original indexes which are not needed, so we will drop it
data = data.drop(data.columns[0], axis=1)
data.head(5)
draftRank | eligibleSlots | playerId | name | projPoints | |
---|---|---|---|---|---|
0 | 1 | [11, 12, 13, 14, 16, 17] | 39832 | Shohei Ohtani | 588.0 |
1 | 2 | [10, 5, 12, 16, 17] | 36969 | Juan Soto | 602.0 |
2 | 3 | [13, 14, 16, 17] | 32081 | Gerrit Cole | 629.0 |
3 | 4 | [3, 7, 19, 11, 12, 16, 17] | 32801 | Jose Ramirez | 676.0 |
4 | 5 | [13, 14, 16, 17] | 39878 | Corbin Burnes | 613.0 |
all_projected_points = data['projPoints'].sum()
print("The total projected points ESPN has calculated for all players in the 2023 season is:", f'{int(all_projected_points):,}')
The total projected points ESPN has calculated for all players in the 2023 season is: 150,474
Many factors go into the decision making of a fantasy draft, and they are dynamically different as a draft progresses. As the baseline of this exercise, we will use the following weights for decision features in our algorithm.
Draft Rank = 45%
Projected Season Points = 15%
Positions needing to be filled on the team = 35%
Position Value = 5%
We will now start to build out mechanics for measuring these features.
For each position that will be used in our league, we are going to evaluate the strength and value of players in those positions. The positions our league uses is: Catcher, 1B, 2B, 3B, SS, 3 OF, DH, 4 Utility, and 9 Pitchers SP are limited to 8 starts per week, so it is best to plan to have 6 SP and 3 RP
We are going to aggregate the following statistics:
# create an object of each position and their ID using the positions lookup file
position_map = {}
with open('lookups/espn-positions-map.json') as f:
position_map = json.load(f)
# We will manually configure which IDs are being used by our league for positions
# and how many starters we want to have at each position to filter our positions object
STARTER_POSITION_IDS = [0, 1, 2, 3, 4, 5, 11, 14, 15]
POSITIONS_STARTERS_CONFIG = {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1, "5": 3, "11": 1, "14": 6, "15": 3, "12": 4, "16": 5}
position_map = {k: v for k, v in position_map.items() if v['posId'] in STARTER_POSITION_IDS}
# Note: we just removed UTIL and BENCH from the position map, even though they exist in the starters_config dict
# This is appropriate for our up front calculations, but we will still consider these player positions later
# appending the number of starters allowed to each position
for k, v in position_map.items():
position_map[k]['startersAllowed'] = POSITIONS_STARTERS_CONFIG[str(v['posId'])]
print(position_map)
{'0': {'posId': 0, 'name': 'Catcher', 'abbrev': 'C', 'startersAllowed': 1}, '1': {'posId': 1, 'name': 'First Base', 'abbrev': '1B', 'startersAllowed': 1}, '2': {'posId': 2, 'name': 'Second Base', 'abbrev': '2B', 'startersAllowed': 1}, '3': {'posId': 3, 'name': 'Third Base', 'abbrev': '3B', 'startersAllowed': 1}, '4': {'posId': 4, 'name': 'Shortstop', 'abbrev': 'SS', 'startersAllowed': 1}, '5': {'posId': 5, 'name': 'Outfield', 'abbrev': 'OF', 'startersAllowed': 3}, '11': {'posId': 11, 'name': 'Designated Hitter', 'abbrev': 'DH', 'startersAllowed': 1}, '14': {'posId': 14, 'name': 'Starting Pitcher', 'abbrev': 'SP', 'startersAllowed': 6}, '15': {'posId': 15, 'name': 'Relief Pitcher', 'abbrev': 'RP', 'startersAllowed': 3}}
# We are going to do some analysis on each positon and update it to the position_map object
for position in STARTER_POSITION_IDS:
# subset all player data based on who is eligible for the position sorted by projected points
position_eligible_players = [values for index, values in data.iterrows() if position in ast.literal_eval(values['eligibleSlots'])]
position_eligible_players = sorted(position_eligible_players, key=lambda k: k['projPoints'], reverse=True)
# parse the max projected points, min projected points, and total projected points for the position
max_projected_points = position_eligible_players[0]['projPoints']
min_projected_points = position_eligible_players[(position_map[str(position)]['startersAllowed']*12)+1]['projPoints']
sum_projected_points = sum([float(player['projPoints']) for player in position_eligible_players])
# save each data point to the position_map object
position_map[str(position)]['totalProjectedPoints'] = sum_projected_points
position_map[str(position)]['maxProjectedPoints'] = max_projected_points
position_map[str(position)]['minProjectedPoints'] = min_projected_points
# Let's take a look at an updated entry from the position_map object
position_map['0']
{'posId': 0, 'name': 'Catcher', 'abbrev': 'C', 'startersAllowed': 1, 'totalProjectedPoints': 10139.5, 'maxProjectedPoints': 452.5, 'minProjectedPoints': 316.5}
# We are going to calculate how many points each position is projected to contribute
# to overall scoring this year. We will then turn that into a weighted score
for k, v in position_map.items():
v['overallWeight'] = v['totalProjectedPoints'] / all_projected_points
# In order to do accurate analysis, we are also going to observe the normalized weight for each position
pos_points_list = [v['totalProjectedPoints'] for k, v in position_map.items()]
norm_pos_points_list = normalize([pos_points_list])[0]
position_map = [{**v, **{'normalizedPositionValue': norm_pos_points_list[i]}} for i, (k, v) in enumerate(position_map.items())]
print("Total Points:", pos_points_list)
print("Normalized Points:", norm_pos_points_list)
position_map[0]
Total Points: [10139.5, 15608.0, 15170.5, 16141.0, 13826.5, 35857.5, 22173.5, 43800.0, 18474.5] Normalized Points: [0.14246494 0.21930005 0.21315296 0.22678896 0.1942691 0.50381544 0.31154854 0.61541146 0.25957578]
{'posId': 0, 'name': 'Catcher', 'abbrev': 'C', 'startersAllowed': 1, 'totalProjectedPoints': 10139.5, 'maxProjectedPoints': 452.5, 'minProjectedPoints': 316.5, 'overallWeight': 0.0673837340670149, 'normalizedPositionValue': 0.14246494211383717}
We are going to weight the following features for individual players:
# loop through each player in our dataset and calculate their positional value
for index, player in data.iterrows():
# identify the relevant starter positions for this player
players_positions = [position for position in ast.literal_eval(player['eligibleSlots']) if position in STARTER_POSITION_IDS]
# if there are multiple eligible positions, this will average them together
scores = []
for pos in players_positions:
position_stats = [position for position in position_map if position['posId'] == pos]
score = position_stats[0]['normalizedPositionValue'] * player['projPoints']
scores.append(score)
# calculate the position-adjusted score for the player based on their eligible positions
players_weights = [position['overallWeight'] for position in position_map if position['posId'] in players_positions]
normalized_players_weights = normalize([players_weights])[0]
avg_score = np.mean(scores) * (len(players_positions) * sum(normalized_players_weights))
# update the player's positional value in our original dataset
data.loc[index, 'positionAdjustedPointsScore'] = avg_score
data.head(25)
draftRank | eligibleSlots | playerId | name | projPoints | positionAdjustedPointsScore | |
---|---|---|---|---|---|---|
0 | 1 | [11, 12, 13, 14, 16, 17] | 39832 | Shohei Ohtani | 588.0 | 732.470167 |
1 | 2 | [10, 5, 12, 16, 17] | 36969 | Juan Soto | 602.0 | 303.296895 |
2 | 3 | [13, 14, 16, 17] | 32081 | Gerrit Cole | 629.0 | 387.093806 |
3 | 4 | [3, 7, 19, 11, 12, 16, 17] | 32801 | Jose Ramirez | 676.0 | 508.392321 |
4 | 5 | [13, 14, 16, 17] | 39878 | Corbin Burnes | 613.0 | 377.247223 |
5 | 6 | [1, 7, 19, 11, 12, 16, 17] | 35002 | Vladimir Guerrero Jr. | 595.0 | 440.090799 |
6 | 7 | [1, 7, 19, 12, 16, 17] | 30193 | Freddie Freeman | 607.0 | 133.115128 |
7 | 8 | [9, 10, 5, 11, 12, 16, 17] | 33192 | Aaron Judge | 612.0 | 686.858738 |
8 | 9 | [13, 14, 16, 17] | 28976 | Max Scherzer | 510.0 | 313.859843 |
9 | 10 | [10, 5, 12, 16, 17] | 33039 | Mookie Betts | 559.5 | 281.884739 |
10 | 11 | [10, 5, 12, 16, 17] | 34967 | Kyle Tucker | 566.0 | 285.159539 |
11 | 12 | [13, 14, 16, 17] | 33709 | Aaron Nola | 562.0 | 345.861239 |
12 | 13 | [13, 14, 16, 17] | 35241 | Sandy Alcantara | 528.0 | 324.937249 |
13 | 14 | [3, 7, 19, 12, 16, 17] | 31097 | Manny Machado | 594.5 | 134.826035 |
14 | 15 | [13, 14, 16, 17] | 6341 | Justin Verlander | 477.0 | 293.551265 |
15 | 16 | [4, 6, 19, 12, 16, 17] | 33710 | Trea Turner | 571.0 | 110.927655 |
16 | 17 | [13, 14, 16, 17] | 32796 | Jacob deGrom | 434.0 | 267.088572 |
17 | 18 | [8, 5, 11, 12, 16, 17] | 36018 | Yordan Alvarez | 588.0 | 659.923101 |
18 | 19 | [13, 14, 16, 17] | 40912 | Shane Bieber | 525.0 | 323.091015 |
19 | 20 | [2, 6, 19, 12, 16, 17] | 32146 | Marcus Semien | 522.5 | 111.372420 |
20 | 21 | [3, 7, 19, 12, 16, 17] | 34886 | Alex Bregman | 515.5 | 116.909707 |
21 | 22 | [13, 14, 16, 17] | 32667 | Kevin Gausman | 540.0 | 332.322187 |
22 | 23 | [13, 14, 16, 17] | 41199 | Shane McClanahan | 522.0 | 321.244780 |
23 | 24 | [1, 7, 19, 11, 12, 16, 17] | 37498 | Pete Alonso | 584.0 | 431.954667 |
24 | 25 | [4, 6, 19, 12, 16, 17] | 32691 | Corey Seager | 537.5 | 104.419640 |
# First, we are going to define the draft order for this season
draft_order_ids_list = []
# We will use the draft-order.json file which describes the order that Fantasy teams will pick in the draft
with open('analysis/data/draft-order-2023.json', 'r') as f:
draft_order = json.load(f)
draft_order = sorted(draft_order, key=lambda k: k['draftOrder'])
print("The draft order is:", [player['teamName'] for player in draft_order])
rounds = 27
# We will loop through the draft order and add the team name to the list for each pick
# There will be some conditions applied to account for the "snake" draft order pattern
for i in range(rounds):
reverse_toggle = i % 2
if i < 3:
reverse_toggle = 1
if reverse_toggle == 1:
for player in draft_order:
draft_order_ids_list.append(player['teamId'])
elif reverse_toggle == 0:
for player in draft_order[::-1]:
draft_order_ids_list.append(player['teamId'])
print("There will be", len(draft_order_ids_list), "picks over", int(len(draft_order_ids_list) / len(draft_order)), "rounds")
The draft order is: ['BG 420', 'The Dissenters', "Loser's Club L", 'RIP Paulie Walnuts', 'Nomar Losing', 'Pecan Sandies', 'Rocket City Trash Pandas', '2-Time Champ Booch', 'Commissioner Getbot420', 'Big Trains', 'Hot Italian Snausages', 'Vandelay Industries'] There will be 324 picks over 27 rounds
# Hold all the owner info in an array
owners = []
with open ('analysis/data/owners.json') as f:
owners = json.load(f)
# Update the owners object to include the positions they need to fill
for owner in owners:
owner['posNeeds'] = {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "11": 1, "14": 1, "15": 1}
# Also include an array to store the players they have drafted
for owner in owners:
owner['picks'] = []
print(owners)
[{'ownerId': '{09B45A77-9B6D-4F38-91ED-8770EB8E1B39}', 'ownerName': 'Zack', 'teamId': 1, 'teamName': 'BG', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{3A5C6608-D4CA-4A22-9C66-08D4CAFA2282}', 'ownerName': 'Kevin', 'teamId': 2, 'teamName': 'Vandelay Industries', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{DD7CBFDF-7F2F-4621-9E9F-6AB906B0A4E0}', 'ownerName': 'Kyle', 'teamId': 3, 'teamName': 'Big Trains', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{496167B6-CD2E-43D3-A167-B6CD2E13D3DC}', 'ownerName': 'Ant', 'teamId': 4, 'teamName': 'RIP PAULIE WALNUTS', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{867A4865-E8ED-4B99-AA91-BA90D083604F}', 'ownerName': 'Mike Q', 'teamId': 5, 'teamName': "Loser's Club", 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{3E3F98D3-2293-4575-90D1-9BFED4AE4C75}', 'ownerName': 'Todd', 'teamId': 6, 'teamName': 'Commissioner Gethbot', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{2F286BB6-5283-441B-B4A5-26AB1F99DDF0}', 'ownerName': 'Nick', 'teamId': 7, 'teamName': 'The Dissenters', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{3E5B2F0F-1310-41CE-95E8-3CAB10720FAB}', 'ownerName': 'Alex', 'teamId': 8, 'teamName': '2-Time Champ Booch', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{0A63036C-23F0-41CD-A303-6C23F041CD26}', 'ownerName': 'Jarrett', 'teamId': 9, 'teamName': 'Pecan Sandies', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{F722388C-B0C3-43CE-88B3-71C5023FBA44}', 'ownerName': 'Mike M', 'teamId': 10, 'teamName': 'Nomar Losing', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{60357C4E-8F4C-4A47-84DA-42B923FA1FE7}', 'ownerName': 'Chris', 'teamId': 11, 'teamName': 'Hot Italian Snausages', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}, {'ownerId': '{2EA0F481-AE10-4E45-A884-A5190F7CEF7D}', 'ownerName': 'Brian', 'teamId': 12, 'teamName': 'Rocket City Trash Pandas', 'posNeeds': {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '11': 1, '14': 1, '15': 1}, 'picks': []}]
Formula for calculating position needs: $$ \int \frac{StartersNeeded - \frac{1}{PlayersEligiblePositions}}{RosterSize -(PickNumber - 1)} $$
The purpose of using this formula is to encourage drafting a player whose fielding position is still needed to be filled on the roster. Additionally, players that have flexible position eligibility should not diminish the weight of a position as strongly as a player who can only fill one specific position.
We will create a function that both calculates this formula and update the weight of a drafters positional needs after every pick.
def updatePositionNeeds(picker: str, owner_list: list, pick: dict):
"""
This function will update the positional needs for the owner who just drafted a player
Parameters:
picker (str): The name of the team that is picking
owner_list (list): The list of owners with their current positional needs
pick (dict): Details about the player that was just drafted
"""
pos_scores = [owner['posNeeds'] for owner in owner_list if owner['teamId'] == picker]
matching_positions = [position for position in ast.literal_eval(pick['eligibleSlots']) if int(position) in STARTER_POSITION_IDS]
positions_player_can_fill = sum([v for k, v in POSITIONS_STARTERS_CONFIG.items() if int(k) in matching_positions]) + POSITIONS_STARTERS_CONFIG['16']
for pos in matching_positions:
players_eligible_positions = len(matching_positions)
if pos in [0, 1, 2, 3, 4, 5, 11, 12]:
starters_needed = positions_player_can_fill + POSITIONS_STARTERS_CONFIG['12']
else:
starters_needed = positions_player_can_fill
teams_pick_number = int(np.ceil(pick['pickNumber'] / 12))
result = (starters_needed * (1 / players_eligible_positions)) / (27 - (teams_pick_number - 1))
if result > 1:
result = 0
new_score = pos_scores[0][str(pos)] * result
[owner for owner in owner_list if owner['teamId'] == picker][0]['posNeeds'][str(pos)] = new_score
Picks will use our previously defined weights and available draft data to make a decision on who to pick. These considerations will include:
A weighted overall score will be assigned to each player available on the draft board using our considerations and weights to determine which player delivers the most value for our drafter.
def generatePick(picker: str, pick_number: int, owner_list: list, current_pick_list: list, remaining_picks: pd.DataFrame):
"""
This function will generate a pick for the given picker
"""
PICK_ROUND = int(np.ceil(pick_number / 12))
TOTAL_PICKS = 27 * 12
BATTER_ELIGIBLE_POSITIONS = [0, 1, 2, 3, 4, 5, 11, 12]
PITCHER_ELIGIBLE_POSITIONS = [14, 15]
FEATURE_WEIGHTS = {'draftRank': 0.45, 'projPoints': 0.15, 'posNeeds': 0.35, 'posValue': 0.05, 'favTeam': 0, 'injuryFactor': 0, 'starPower': 0}
draft_rank_score = TOTAL_PICKS - pick_number
# apply min max scaling to the draft rank score
for index, player in remaining_picks.iterrows():
remaining_picks.loc[index, 'draftRankScore'] = TOTAL_PICKS - player['draftRank']
normalized_draft_rank_score = MinMaxScaler(feature_range=(-1, 1)).fit_transform(np.array(remaining_picks[['draftRankScore']]).reshape(-1, 1)).reshape(-1)
remaining_picks['draftRankScore'] = normalized_draft_rank_score.tolist()
# normalize the position adjusted score
normalized_position_adjusted_score = normalize(np.array(remaining_picks[['positionAdjustedPointsScore']]).reshape(-1, 1), axis=0, norm='max').reshape(-1)
remaining_picks['positionAdjustedScore'] = normalized_position_adjusted_score.tolist()
# calculate the position needs score
for index, player in remaining_picks.iterrows():
matching_positions = [position for position in ast.literal_eval(player['eligibleSlots']) if int(position) in STARTER_POSITION_IDS]
owner = [owner for owner in owner_list if owner['teamId'] == picker][0]
positions_needs_values = [v for k, v in owner['posNeeds'].items() if int(k) in matching_positions]
positions_needs_score = sum(positions_needs_values) / len(positions_needs_values)
remaining_picks.loc[index, 'positionNeedsScore'] = positions_needs_score
# we will add up all the scores and appropriate weights and aggregate an overall score
for index, player in remaining_picks.iterrows():
remaining_picks.loc[index, 'overallScore'] = (player['draftRankScore'] * FEATURE_WEIGHTS['draftRank']) + (player['projPoints'] * FEATURE_WEIGHTS['projPoints']) + (player['positionAdjustedScore'] * FEATURE_WEIGHTS['posValue']) + (player['positionNeedsScore'] * FEATURE_WEIGHTS['posNeeds'])
# the pick will be decided by the player with the highest overall score that is eligible to be drafted
best_pick = remaining_picks.sort_values(by=['overallScore'], ascending=False).iloc[0]
payload = {
'pickNumber': pick_number,
'pickRound': PICK_ROUND,
'teamName': [owner['teamName'] for owner in owner_list if owner['teamId'] == picker][0],
'teamId': picker,
'playerId': best_pick['playerId'],
'playerName': best_pick['name'],
'draftRank': best_pick['draftRank'],
'eligibleSlots': best_pick['eligibleSlots'],
'projPoints': best_pick['projPoints'],
'overallScore': best_pick['overallScore'],
}
return payload
Now that we've built all the pieces of how our program will decide on draft picks, we create a loop to simulate each pick of the draft. As each pick is made, new scores are calculated in the same way individuals re-evaluate the draft table in a real life draft.
The program will iterate through all 324 picks in the draft
pick_number = 1
drafted_players = []
remaining_players = data.copy()
for picker in draft_order_ids_list:
drafted_player = generatePick(picker, pick_number, owners, drafted_players, remaining_players)
updatePositionNeeds(picker, owners, drafted_player)
[owner for owner in owners if owner['teamId'] == picker][0]['picks'].append(drafted_player)
drafted_players.append(drafted_player)
remaining_players = remaining_players[remaining_players['playerId'] != drafted_player['playerId']]
pick_number += 1
# Here we will check how many players each team has drafted at each position
# This will be a small visual check to see the player positions are being factored correctly by each owner
count_results = []
for owner in owners:
roster = {"0": 0, "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "11": 0, "14": 0, "15": 0}
for pick in owner['picks']:
positions = ast.literal_eval(pick['eligibleSlots'])
for position in positions:
if str(position) in roster:
roster[str(position)] += 1
roster_map = {"0": "C", "1": "1B", "2": "2B", "3": "3B", "4": "SS", "5": "OF", "11": "DH", "14": "SP", "15": "RP"}
roster = {roster_map[str(k)]: v for k, v in roster.items()}
count_results.append({"name": owner['teamName'], "roster": roster})
count_results = pd.DataFrame(count_results).set_index('name')
count_results = pd.concat([count_results.drop(['roster'], axis=1), count_results['roster'].apply(pd.Series)], axis=1)
count_results
C | 1B | 2B | 3B | SS | OF | DH | SP | RP | |
---|---|---|---|---|---|---|---|---|---|
name | |||||||||
BG | 1 | 4 | 1 | 2 | 3 | 8 | 4 | 7 | 3 |
Vandelay Industries | 0 | 3 | 1 | 2 | 2 | 4 | 5 | 12 | 4 |
Big Trains | 2 | 5 | 3 | 3 | 2 | 6 | 5 | 8 | 4 |
RIP PAULIE WALNUTS | 1 | 3 | 2 | 3 | 4 | 5 | 4 | 10 | 2 |
Loser's Club | 1 | 2 | 2 | 3 | 2 | 9 | 4 | 7 | 2 |
Commissioner Gethbot | 1 | 5 | 7 | 4 | 1 | 7 | 3 | 6 | 2 |
The Dissenters | 0 | 3 | 0 | 2 | 3 | 6 | 2 | 12 | 3 |
2-Time Champ Booch | 1 | 1 | 4 | 4 | 3 | 8 | 3 | 8 | 3 |
Pecan Sandies | 3 | 3 | 3 | 4 | 2 | 9 | 4 | 8 | 1 |
Nomar Losing | 2 | 2 | 3 | 3 | 2 | 9 | 5 | 7 | 1 |
Hot Italian Snausages | 3 | 2 | 3 | 3 | 5 | 5 | 7 | 6 | 3 |
Rocket City Trash Pandas | 2 | 2 | 5 | 4 | 1 | 9 | 3 | 8 | 2 |
# We will save the draft results to a new json file
owners_list = []
for owner in owners:
owner_copy = owner.copy()
# modify data types for serialization
for pick in owner_copy['picks']:
pick['playerId'] = int(pick['playerId'])
pick['draftRank'] = int(pick['draftRank'])
owners_list.append(owner_copy)
# save the draft results to a json file
with open('analysis/outputs/DRAFT_RESULT.json', 'w') as f:
json.dump(owners_list, f, indent=2)
At this point we have built a pretty robust prediction algorithm. There are some improvement opportunities such as:
I'm happy with the result this far and will consider enhancing to use in future Fantasy Drafts.