fantasyfb 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fantasyfb/__init__.py +41 -0
- fantasyfb/analysis/__init__.py +1 -0
- fantasyfb/analysis/move_analyzer.py +639 -0
- fantasyfb/analysis/war_calculator.py +165 -0
- fantasyfb/cli.py +202 -0
- fantasyfb/configs.py +166 -0
- fantasyfb/data/__init__.py +1 -0
- fantasyfb/data/nfl_provider.py +102 -0
- fantasyfb/data/nflreadpy_provider.py +367 -0
- fantasyfb/data/player_data_manager.py +440 -0
- fantasyfb/data/yahoo_client.py +466 -0
- fantasyfb/drafts/__init__.py +1 -0
- fantasyfb/drafts/prep.py +314 -0
- fantasyfb/drafts/salary_cap.py +388 -0
- fantasyfb/drafts/snake.py +554 -0
- fantasyfb/drafts/snake_cockpit.py +288 -0
- fantasyfb/drafts/tools.py +638 -0
- fantasyfb/io/__init__.py +1 -0
- fantasyfb/io/excel_exporter.py +252 -0
- fantasyfb/league.py +830 -0
- fantasyfb/projections/__init__.py +1 -0
- fantasyfb/projections/engine.py +312 -0
- fantasyfb/projections/engine_v2.py +321 -0
- fantasyfb/projections/model_fitter.py +227 -0
- fantasyfb/scoring/__init__.py +1 -0
- fantasyfb/scoring/fantasy_scoring.py +202 -0
- fantasyfb/scoring/lineup_optimizer.py +197 -0
- fantasyfb/scoring/matchup_model.py +275 -0
- fantasyfb/sim/__init__.py +1 -0
- fantasyfb/sim/backtest.py +267 -0
- fantasyfb/sim/schedule_manager.py +142 -0
- fantasyfb/sim/season_simulator.py +489 -0
- fantasyfb-0.3.0.dist-info/METADATA +150 -0
- fantasyfb-0.3.0.dist-info/RECORD +38 -0
- fantasyfb-0.3.0.dist-info/WHEEL +5 -0
- fantasyfb-0.3.0.dist-info/entry_points.txt +5 -0
- fantasyfb-0.3.0.dist-info/licenses/LICENSE +21 -0
- fantasyfb-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WAR (Wins Above Replacement) calculation for fantasy football players.
|
|
3
|
+
|
|
4
|
+
This module calculates how many more wins a player would contribute
|
|
5
|
+
compared to an average replacement player at their position.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WARCalculator:
|
|
13
|
+
"""
|
|
14
|
+
Calculates Wins Above Replacement (WAR) for fantasy football players.
|
|
15
|
+
|
|
16
|
+
WAR represents how many more wins per season a player would contribute
|
|
17
|
+
compared to an average replacement player at their position.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, num_sims: int = 10000):
|
|
21
|
+
"""
|
|
22
|
+
Initialize WAR calculator.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
num_sims: Number of Monte Carlo simulations to run
|
|
26
|
+
"""
|
|
27
|
+
self.num_sims = num_sims
|
|
28
|
+
|
|
29
|
+
def calculate_war(self, players_df: pd.DataFrame, stats_df: pd.DataFrame) -> pd.DataFrame:
|
|
30
|
+
"""
|
|
31
|
+
Calculate WAR for all players based on historical performance.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
players_df: DataFrame with player information including points_rate, points_stdev
|
|
35
|
+
stats_df: DataFrame with historical game statistics
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
DataFrame with WAR values added to player data
|
|
39
|
+
"""
|
|
40
|
+
# Create position histograms from historical data
|
|
41
|
+
pos_hists = self._create_position_histograms(stats_df)
|
|
42
|
+
|
|
43
|
+
# Simulate average team performance
|
|
44
|
+
sim_scores = self._simulate_average_teams(pos_hists)
|
|
45
|
+
|
|
46
|
+
# Create player simulation data
|
|
47
|
+
player_sims = self._simulate_player_performance(players_df, sim_scores.shape[0])
|
|
48
|
+
|
|
49
|
+
# Merge simulations
|
|
50
|
+
sim_scores = pd.merge(
|
|
51
|
+
left=sim_scores, right=player_sims, left_index=True, right_index=True
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Calculate WAR for each player
|
|
55
|
+
players_with_war = players_df.copy()
|
|
56
|
+
for player in player_sims.columns:
|
|
57
|
+
if pd.isnull(player):
|
|
58
|
+
print("Null player name for some reason... Skipping...")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
war_value = self._calculate_individual_war(sim_scores, player, players_df)
|
|
62
|
+
players_with_war.loc[players_with_war.name == player, "WAR"] = war_value
|
|
63
|
+
|
|
64
|
+
return players_with_war
|
|
65
|
+
|
|
66
|
+
def _create_position_histograms(self, stats_df: pd.DataFrame) -> dict:
|
|
67
|
+
"""Create probability histograms for each position based on historical points."""
|
|
68
|
+
pos_hists = {"points": np.arange(-10, 50.1, 0.1)}
|
|
69
|
+
|
|
70
|
+
for pos in stats_df.position.unique():
|
|
71
|
+
pos_hists[pos] = np.histogram(
|
|
72
|
+
stats_df.loc[stats_df.position == pos, "points"],
|
|
73
|
+
bins=pos_hists["points"],
|
|
74
|
+
)[0]
|
|
75
|
+
pos_hists[pos] = pos_hists[pos] / sum(pos_hists[pos])
|
|
76
|
+
|
|
77
|
+
# Create FLEX histogram (RB + WR + TE)
|
|
78
|
+
pos_hists["FLEX"] = np.histogram(
|
|
79
|
+
stats_df.loc[stats_df.position.isin(["RB", "WR", "TE"]), "points"],
|
|
80
|
+
bins=pos_hists["points"],
|
|
81
|
+
)[0]
|
|
82
|
+
pos_hists["FLEX"] = pos_hists["FLEX"] / sum(pos_hists["FLEX"])
|
|
83
|
+
|
|
84
|
+
return pos_hists
|
|
85
|
+
|
|
86
|
+
def _simulate_average_teams(self, pos_hists: dict) -> pd.DataFrame:
|
|
87
|
+
"""Simulate team performance using average players at each position."""
|
|
88
|
+
return pd.DataFrame({
|
|
89
|
+
"QB": np.random.choice(
|
|
90
|
+
pos_hists["points"][:-1], p=pos_hists["QB"], size=self.num_sims
|
|
91
|
+
),
|
|
92
|
+
"RB1": np.random.choice(
|
|
93
|
+
pos_hists["points"][:-1], p=pos_hists["RB"], size=self.num_sims
|
|
94
|
+
),
|
|
95
|
+
"RB2": np.random.choice(
|
|
96
|
+
pos_hists["points"][:-1], p=pos_hists["RB"], size=self.num_sims
|
|
97
|
+
),
|
|
98
|
+
"WR1": np.random.choice(
|
|
99
|
+
pos_hists["points"][:-1], p=pos_hists["WR"], size=self.num_sims
|
|
100
|
+
),
|
|
101
|
+
"WR2": np.random.choice(
|
|
102
|
+
pos_hists["points"][:-1], p=pos_hists["WR"], size=self.num_sims
|
|
103
|
+
),
|
|
104
|
+
"TE": np.random.choice(
|
|
105
|
+
pos_hists["points"][:-1], p=pos_hists["TE"], size=self.num_sims
|
|
106
|
+
),
|
|
107
|
+
"FLEX": np.random.choice(
|
|
108
|
+
pos_hists["points"][:-1], p=pos_hists["FLEX"], size=self.num_sims
|
|
109
|
+
),
|
|
110
|
+
"K": np.random.choice(
|
|
111
|
+
pos_hists["points"][:-1], p=pos_hists["K"], size=self.num_sims
|
|
112
|
+
),
|
|
113
|
+
"DEF": np.random.choice(
|
|
114
|
+
pos_hists["points"][:-1], p=pos_hists["DEF"], size=self.num_sims
|
|
115
|
+
),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
def _simulate_player_performance(self, players_df: pd.DataFrame, num_sims: int) -> pd.DataFrame:
|
|
119
|
+
"""Generate simulated performance for all players."""
|
|
120
|
+
return pd.DataFrame({
|
|
121
|
+
players_df.loc[ind, "name"]: np.round(
|
|
122
|
+
np.random.normal(
|
|
123
|
+
loc=players_df.loc[ind, "points_rate"],
|
|
124
|
+
scale=players_df.loc[ind, "points_stdev"],
|
|
125
|
+
size=num_sims,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
for ind in range(players_df.shape[0])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
def _calculate_individual_war(self, sim_scores: pd.DataFrame, player_name: str,
|
|
132
|
+
players_df: pd.DataFrame) -> float:
|
|
133
|
+
"""Calculate WAR for a single player."""
|
|
134
|
+
# Get player position
|
|
135
|
+
pos = players_df.loc[players_df.name == player_name, "position"].values[0]
|
|
136
|
+
|
|
137
|
+
# Determine roster position for comparison
|
|
138
|
+
if pos in ["RB", "WR"]:
|
|
139
|
+
comparison_pos = pos + "1"
|
|
140
|
+
else:
|
|
141
|
+
comparison_pos = pos
|
|
142
|
+
|
|
143
|
+
# Create alternative lineup with this player
|
|
144
|
+
baseline_cols = sim_scores.columns[:9].tolist() # Standard positions
|
|
145
|
+
baseline_cols.remove(comparison_pos)
|
|
146
|
+
baseline_cols.append(player_name)
|
|
147
|
+
|
|
148
|
+
# Calculate total scores
|
|
149
|
+
sim_scores["Total"] = sim_scores.iloc[:, :9].sum(axis=1)
|
|
150
|
+
sim_scores["Alt_Total"] = sim_scores[baseline_cols].sum(axis=1)
|
|
151
|
+
|
|
152
|
+
# Calculate win rate difference
|
|
153
|
+
half_sims = sim_scores.shape[0] // 2
|
|
154
|
+
wins_above_replacement = sum(
|
|
155
|
+
sim_scores.loc[:half_sims - 1, "Alt_Total"].values >
|
|
156
|
+
sim_scores.loc[half_sims:, "Total"].values
|
|
157
|
+
) / half_sims
|
|
158
|
+
|
|
159
|
+
# Convert to WAR (14 games per season, centered at 0.5)
|
|
160
|
+
war_value = (wins_above_replacement - 0.5) * 14
|
|
161
|
+
|
|
162
|
+
# Clean up temporary columns
|
|
163
|
+
del sim_scores["Alt_Total"]
|
|
164
|
+
|
|
165
|
+
return war_value
|
fantasyfb/cli.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for fantasy football analysis.
|
|
3
|
+
|
|
4
|
+
This module handles parsing command-line arguments and setting up
|
|
5
|
+
default values for the fantasy football analysis script.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
import optparse
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def initialize_inputs():
|
|
14
|
+
"""
|
|
15
|
+
Initializing arguments based on command line inputs provided by the user.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
optparse.Values: collection of cleaned input values based on inputs and basic logic.
|
|
19
|
+
"""
|
|
20
|
+
parser = optparse.OptionParser()
|
|
21
|
+
parser.add_option(
|
|
22
|
+
"--season",
|
|
23
|
+
action="store",
|
|
24
|
+
type="int",
|
|
25
|
+
dest="season",
|
|
26
|
+
help="season of interest"
|
|
27
|
+
)
|
|
28
|
+
parser.add_option(
|
|
29
|
+
"--week",
|
|
30
|
+
action="store",
|
|
31
|
+
type="int",
|
|
32
|
+
dest="week",
|
|
33
|
+
help="week to project the season from"
|
|
34
|
+
)
|
|
35
|
+
parser.add_option(
|
|
36
|
+
"--team",
|
|
37
|
+
action="store",
|
|
38
|
+
dest="team",
|
|
39
|
+
help="name of team to analyze in the case of multiple teams in a single season",
|
|
40
|
+
)
|
|
41
|
+
parser.add_option(
|
|
42
|
+
"--earliest",
|
|
43
|
+
action="store",
|
|
44
|
+
type="int",
|
|
45
|
+
dest="earliest",
|
|
46
|
+
help="earliest week of stats being considered, e.g. 201807 corresponds to week 7 of the 2018 season",
|
|
47
|
+
)
|
|
48
|
+
parser.add_option(
|
|
49
|
+
"--games",
|
|
50
|
+
action="store",
|
|
51
|
+
type="int",
|
|
52
|
+
dest="games",
|
|
53
|
+
help="number of games to build each player's prior off of",
|
|
54
|
+
)
|
|
55
|
+
parser.add_option(
|
|
56
|
+
"--basaloppstringtime",
|
|
57
|
+
action="store",
|
|
58
|
+
dest="basaloppstringtime",
|
|
59
|
+
help="scaling factors for basal/opponent/depthchart/time factors, comma-separated string of values",
|
|
60
|
+
)
|
|
61
|
+
parser.add_option(
|
|
62
|
+
"--sims", action="store", type="int", dest="sims", help="number of season simulations"
|
|
63
|
+
)
|
|
64
|
+
parser.add_option(
|
|
65
|
+
"--payouts",
|
|
66
|
+
action="store",
|
|
67
|
+
dest="payouts",
|
|
68
|
+
help="comma separated string containing integer payouts for 1st, 2nd, and 3rd",
|
|
69
|
+
)
|
|
70
|
+
parser.add_option(
|
|
71
|
+
"--injurytries",
|
|
72
|
+
action="store",
|
|
73
|
+
type="int",
|
|
74
|
+
dest="injurytries",
|
|
75
|
+
default=10,
|
|
76
|
+
help="number of times to try pulling injury statuses before rolling with it",
|
|
77
|
+
)
|
|
78
|
+
parser.add_option(
|
|
79
|
+
"--bestball",
|
|
80
|
+
action="store_true",
|
|
81
|
+
dest="bestball",
|
|
82
|
+
help="whether to assess the league of interest in the context of bestball (simulates bench contributions better)",
|
|
83
|
+
)
|
|
84
|
+
parser.add_option(
|
|
85
|
+
"--pickups",
|
|
86
|
+
action="store",
|
|
87
|
+
dest="pickups",
|
|
88
|
+
help='assess possible free agent pickups for the players specified ("all" will analyze all possible pickups)',
|
|
89
|
+
)
|
|
90
|
+
parser.add_option(
|
|
91
|
+
"--adds",
|
|
92
|
+
action="store_true",
|
|
93
|
+
dest="adds",
|
|
94
|
+
help="whether to assess possible free agent adds",
|
|
95
|
+
)
|
|
96
|
+
parser.add_option(
|
|
97
|
+
"--drops",
|
|
98
|
+
action="store_true",
|
|
99
|
+
dest="drops",
|
|
100
|
+
help="whether to assess possible drops",
|
|
101
|
+
)
|
|
102
|
+
parser.add_option(
|
|
103
|
+
"--trades",
|
|
104
|
+
action="store",
|
|
105
|
+
dest="trades",
|
|
106
|
+
help='assess possible trades for the players specified ("all" will analyze all possible trades)',
|
|
107
|
+
)
|
|
108
|
+
parser.add_option(
|
|
109
|
+
"--given",
|
|
110
|
+
action="store",
|
|
111
|
+
dest="given",
|
|
112
|
+
help="given players to start with for multi-player trades",
|
|
113
|
+
)
|
|
114
|
+
parser.add_option(
|
|
115
|
+
"--deltas",
|
|
116
|
+
action="store_true",
|
|
117
|
+
dest="deltas",
|
|
118
|
+
help="whether to assess deltas for each matchup of the current week",
|
|
119
|
+
)
|
|
120
|
+
parser.add_option(
|
|
121
|
+
"--output",
|
|
122
|
+
action="store",
|
|
123
|
+
dest="output",
|
|
124
|
+
help="where to save the final projections spreadsheet",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
options, args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
# Apply default values and validation
|
|
130
|
+
options = _apply_defaults(options)
|
|
131
|
+
options = _validate_inputs(options)
|
|
132
|
+
|
|
133
|
+
return options
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _apply_defaults(options):
|
|
137
|
+
"""Apply default values to command line options."""
|
|
138
|
+
if not options.season:
|
|
139
|
+
options.season = datetime.datetime.now().year - int(datetime.datetime.now().month < 6)
|
|
140
|
+
|
|
141
|
+
# Handle basaloppstringtime parsing
|
|
142
|
+
if options.basaloppstringtime:
|
|
143
|
+
options.basaloppstringtime = options.basaloppstringtime.split(",")
|
|
144
|
+
if all([val.isnumeric() for val in options.basaloppstringtime]) and len(options.basaloppstringtime) == 4:
|
|
145
|
+
options.basaloppstringtime = [float(val) for val in options.basaloppstringtime]
|
|
146
|
+
else:
|
|
147
|
+
print("Invalid rate inference parameters, using defaults...")
|
|
148
|
+
options.basaloppstringtime = None
|
|
149
|
+
|
|
150
|
+
# Handle payouts parsing with team-specific defaults
|
|
151
|
+
if options.payouts:
|
|
152
|
+
options.payouts = options.payouts.split(",")
|
|
153
|
+
if all([val.isnumeric() for val in options.payouts]) and len(options.payouts) == 3:
|
|
154
|
+
options.payouts = [float(val) for val in options.payouts]
|
|
155
|
+
else:
|
|
156
|
+
print("Weird values provided for payouts... Assuming standard payouts...")
|
|
157
|
+
options.payouts = [60.0, 30.0, 10.0]
|
|
158
|
+
elif options.team == "The Algorithm":
|
|
159
|
+
options.payouts = [720, 360, 120]
|
|
160
|
+
elif options.team == "Toothless Wonders":
|
|
161
|
+
options.payouts = [350, 100, 50]
|
|
162
|
+
elif options.team == "The GENIEs":
|
|
163
|
+
options.payouts = [120, 0, 0]
|
|
164
|
+
elif options.team == "The Great Gadsby's":
|
|
165
|
+
options.payouts = [50, 35, 15]
|
|
166
|
+
else:
|
|
167
|
+
options.payouts = [60.0, 30.0, 10.0]
|
|
168
|
+
|
|
169
|
+
return options
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _validate_inputs(options):
|
|
173
|
+
"""Validate and set up output directory."""
|
|
174
|
+
if not options.output:
|
|
175
|
+
options.output = (
|
|
176
|
+
os.path.expanduser("~/Documents/")
|
|
177
|
+
if os.path.exists(os.path.expanduser("~/Documents/"))
|
|
178
|
+
else os.path.expanduser("~/")
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Create team directory if it doesn't exist
|
|
182
|
+
if options.team and not os.path.exists(options.output + options.team.replace(" ", "")):
|
|
183
|
+
os.mkdir(options.output + options.team.replace(" ", ""))
|
|
184
|
+
|
|
185
|
+
# Create season directory if it doesn't exist
|
|
186
|
+
if options.team and not os.path.exists(
|
|
187
|
+
options.output + options.team.replace(" ", "") + "/" + str(options.season)
|
|
188
|
+
):
|
|
189
|
+
os.mkdir(
|
|
190
|
+
options.output
|
|
191
|
+
+ options.team.replace(" ", "")
|
|
192
|
+
+ "/"
|
|
193
|
+
+ str(options.season)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if options.team:
|
|
197
|
+
options.output += options.team.replace(" ", "") + "/" + str(options.season)
|
|
198
|
+
|
|
199
|
+
if options.output[-1] != "/":
|
|
200
|
+
options.output += "/"
|
|
201
|
+
|
|
202
|
+
return options
|
fantasyfb/configs.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Predefined league configurations for fantasy football platforms.
|
|
3
|
+
|
|
4
|
+
This module contains scoring settings and roster configurations for
|
|
5
|
+
various fantasy platforms like Scott Fish Bowl, DraftKings, Underdog, etc.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
# Scott Fish Bowl 13 configuration (commented out - historical)
|
|
11
|
+
# SFB13_CONFIG = {
|
|
12
|
+
# 'settings': {
|
|
13
|
+
# 'playoff_start_week': 12,
|
|
14
|
+
# 'num_playoff_teams': 6
|
|
15
|
+
# },
|
|
16
|
+
# 'scoring': {
|
|
17
|
+
# 'Pass Yds': 0.04, 'Pass Comp': 0.1, 'Pass TD': 6.0, 'Pass 1D': 0.1, 'Pass 300+': 0.0,
|
|
18
|
+
# 'Int Thrown': 0.0, 'Rush Yds': 0.1, 'Rush Att': 0.25, 'Rush TD': 6.0, 'Rush 1D': 1.0, 'Rush 100+': 0.0,
|
|
19
|
+
# 'Rec Yds': 0.1, 'Rec': 1.0, 'Rec TD': 6.0, 'Rec 1D': 1.0, 'Rec 100+': 0.0, 'Ret Yds': 0.0, 'Ret TD': 6.0,
|
|
20
|
+
# 'TE Rec Bonus': 1.0, 'TE 1D Bonus': 1.0, '2-PT': 2.0, 'Fum Lost': 0.0, 'Fum Ret TD': 6.0,
|
|
21
|
+
# 'FG 0-19': 2.0, 'FG 20-29': 2.5, 'FG 30-39': 3.5, 'FG 40-49': 4.5, 'FG 50+': 5.5, 'PAT Made': 3.3,
|
|
22
|
+
# 'Sack': 0.0, 'Int': 0.0, 'Fum Rec': 0.0, 'TD': 0.0, 'Safe': 0.0, 'Blk Kick': 0.0,
|
|
23
|
+
# 'Pts Allow 0': 0.0, 'Pts Allow 1-6': 0.0, 'Pts Allow 7-13': 0.0, 'Pts Allow 14-20': 0.0,
|
|
24
|
+
# 'Pts Allow 21-27': 0.0, 'Pts Allow 28-34': 0.0, 'Pts Allow 35+': 0.0, 'XPR': 0.0
|
|
25
|
+
# },
|
|
26
|
+
# 'roster_spots': pd.DataFrame({
|
|
27
|
+
# 'position': ['QB', 'RB', 'WR', 'TE', 'W/R/T', 'Q/W/R/T', 'K', 'BN'],
|
|
28
|
+
# 'count': [1, 2, 3, 1, 2, 1, 1, 11]
|
|
29
|
+
# })
|
|
30
|
+
# }
|
|
31
|
+
|
|
32
|
+
# Scott Fish Bowl 14 configuration (commented out - historical)
|
|
33
|
+
# SFB14_CONFIG = {
|
|
34
|
+
# 'scoring': {
|
|
35
|
+
# 'Pass Yds': 0.02, 'Pass Comp': 0.0, 'Pass TD': 6.0, 'Pass 1D': 0.0, 'Pass 300+': 0.0,
|
|
36
|
+
# 'Int Thrown': 0.0, 'Rush Yds': 0.1, 'Rush Att': 0.25, 'Rush TD': 6.0, 'Rush 1D': 0.5, 'Rush 100+': 0.0,
|
|
37
|
+
# 'Rec Yds': 0.1, 'Rec': 0.75, 'Rec TD': 6.0, 'Rec 1D': 0.5, 'Rec 100+': 0.0, 'Ret Yds': 0.2, 'Ret TD': 10.0,
|
|
38
|
+
# 'TE Rec Bonus': 0.75, 'TE 1D Bonus': 1.0, '2-PT': 2.0, 'Fum Lost': 0.0, 'Fum Ret TD': 6.0,
|
|
39
|
+
# 'FG 0-19': 2.0, 'FG 20-29': 2.5, 'FG 30-39': 3.5, 'FG 40-49': 4.5, 'FG 50+': 5.5, 'PAT Made': 3.3,
|
|
40
|
+
# 'Sack': 0.0, 'Int': 0.0, 'Fum Rec': 0.0, 'TD': 0.0, 'Safe': 0.0, 'Blk Kick': 0.0,
|
|
41
|
+
# 'Pts Allow 0': 0.0, 'Pts Allow 1-6': 0.0, 'Pts Allow 7-13': 0.0, 'Pts Allow 14-20': 0.0,
|
|
42
|
+
# 'Pts Allow 21-27': 0.0, 'Pts Allow 28-34': 0.0, 'Pts Allow 35+': 0.0, 'XPR': 0.0
|
|
43
|
+
# },
|
|
44
|
+
# 'roster_spots': pd.DataFrame({
|
|
45
|
+
# 'position': ['QB', 'RB', 'WR', 'TE', 'W/R/T', 'Q/W/R/T', 'K', 'BN'],
|
|
46
|
+
# 'count': [1, 1, 1, 1, 5, 1, 1, 11]
|
|
47
|
+
# })
|
|
48
|
+
# }
|
|
49
|
+
|
|
50
|
+
# Scott Fish Bowl 15 configuration (current)
|
|
51
|
+
SFB_CONFIG = {
|
|
52
|
+
'scoring': {
|
|
53
|
+
'Pass Yds': 0.04, 'Pass Comp': 0.0, 'Pass TD': 6.0, 'Pass 1D': 0.0, 'Pass 300+': 0.0,
|
|
54
|
+
'Int Thrown': 0.0, 'Rush Yds': 0.1, 'Rush Att': 0.5, 'Rush TD': 6.0, 'Rush 1D': 1.0, 'Rush 100+': 0.0,
|
|
55
|
+
'Rec Yds': 0.1, 'Rec': 2.5, 'Rec TD': 6.0, 'Rec 1D': 1.0, 'Rec 100+': 0.0, 'Ret Yds': 0.0, 'Ret TD': 6.0,
|
|
56
|
+
'TE Rec Bonus': 1.0, 'TE 1D Bonus': 1.0, '2-PT': 2.0, 'Fum Lost': 0.0, 'Fum Ret TD': 6.0,
|
|
57
|
+
'FG 0-19': 0.0, 'FG 20-29': 0.0, 'FG 30-39': 0.0, 'FG 40-49': 0.0, 'FG 50+': 0.0, 'PAT Made': 0.0,
|
|
58
|
+
'Sack': 0.0, 'Int': 0.0, 'Fum Rec': 0.0, 'TD': 0.0, 'Safe': 0.0, 'Blk Kick': 0.0,
|
|
59
|
+
'Pts Allow 0': 0.0, 'Pts Allow 1-6': 0.0, 'Pts Allow 7-13': 0.0, 'Pts Allow 14-20': 0.0,
|
|
60
|
+
'Pts Allow 21-27': 0.0, 'Pts Allow 28-34': 0.0, 'Pts Allow 35+': 0.0, 'XPR': 0.0
|
|
61
|
+
},
|
|
62
|
+
'roster_spots': pd.DataFrame({
|
|
63
|
+
'position': ['QB', 'RB', 'WR', 'TE', 'W/R/T', 'Q/W/R/T', 'K', 'BN'],
|
|
64
|
+
'count': [0, 0, 0, 0, 9, 2, 0, 11]
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# DraftKings Best Ball configuration
|
|
69
|
+
DRAFTKINGS_CONFIG = {
|
|
70
|
+
'settings': {
|
|
71
|
+
'playoff_start_week': 14,
|
|
72
|
+
'num_playoff_teams': 2
|
|
73
|
+
},
|
|
74
|
+
'scoring': {
|
|
75
|
+
'Pass Yds': 0.04, 'Pass Comp': 0.0, 'Pass TD': 4.0, 'Pass 1D': 0.0, 'Pass 300+': 3.0,
|
|
76
|
+
'Int Thrown': -1.0, 'Rush Yds': 0.1, 'Rush Att': 0.0, 'Rush TD': 6.0, 'Rush 1D': 0.0, 'Rush 100+': 3.0,
|
|
77
|
+
'Rec Yds': 0.1, 'Rec': 1.0, 'Rec TD': 6.0, 'Rec 1D': 0.0, 'Rec 100+': 3.0, 'Ret Yds': 0.0, 'Ret TD': 6.0,
|
|
78
|
+
'TE Rec Bonus': 0.0, 'TE 1D Bonus': 0.0, '2-PT': 2.0, 'Fum Lost': -1.0, 'Fum Ret TD': 6.0,
|
|
79
|
+
'FG 0-19': 0.0, 'FG 20-29': 0.0, 'FG 30-39': 0.0, 'FG 40-49': 0.0, 'FG 50+': 0.0, 'PAT Made': 0.0,
|
|
80
|
+
'Sack': 0.0, 'Int': 0.0, 'Fum Rec': 0.0, 'TD': 0.0, 'Safe': 0.0, 'Blk Kick': 0.0,
|
|
81
|
+
'Pts Allow 0': 0.0, 'Pts Allow 1-6': 0.0, 'Pts Allow 7-13': 0.0, 'Pts Allow 14-20': 0.0,
|
|
82
|
+
'Pts Allow 21-27': 0.0, 'Pts Allow 28-34': 0.0, 'Pts Allow 35+': 0.0, 'XPR': 0.0
|
|
83
|
+
},
|
|
84
|
+
'roster_spots': pd.DataFrame({
|
|
85
|
+
'position': ['QB', 'RB', 'WR', 'TE', 'W/R/T', 'Q/W/R/T', 'K', 'BN'],
|
|
86
|
+
'count': [1, 2, 3, 1, 1, 0, 0, 12]
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Underdog Best Ball configuration
|
|
91
|
+
UNDERDOG_CONFIG = {
|
|
92
|
+
'settings': {
|
|
93
|
+
'playoff_start_week': 14,
|
|
94
|
+
'num_playoff_teams': 2
|
|
95
|
+
},
|
|
96
|
+
'scoring': {
|
|
97
|
+
'Pass Yds': 0.04, 'Pass Comp': 0.0, 'Pass TD': 4.0, 'Pass 1D': 0.0, 'Pass 300+': 0.0,
|
|
98
|
+
'Int Thrown': -1.0, 'Rush Yds': 0.1, 'Rush Att': 0.0, 'Rush TD': 6.0, 'Rush 1D': 0.0, 'Rush 100+': 0.0,
|
|
99
|
+
'Rec Yds': 0.1, 'Rec': 0.5, 'Rec TD': 6.0, 'Rec 1D': 0.0, 'Rec 100+': 0.0, 'Ret Yds': 0.0, 'Ret TD': 0.0,
|
|
100
|
+
'TE Rec Bonus': 0.0, 'TE 1D Bonus': 0.0, '2-PT': 2.0, 'Fum Lost': -2.0, 'Fum Ret TD': 0.0,
|
|
101
|
+
'FG 0-19': 0.0, 'FG 20-29': 0.0, 'FG 30-39': 0.0, 'FG 40-49': 0.0, 'FG 50+': 0.0, 'PAT Made': 0.0,
|
|
102
|
+
'Sack': 0.0, 'Int': 0.0, 'Fum Rec': 0.0, 'TD': 0.0, 'Safe': 0.0, 'Blk Kick': 0.0,
|
|
103
|
+
'Pts Allow 0': 0.0, 'Pts Allow 1-6': 0.0, 'Pts Allow 7-13': 0.0, 'Pts Allow 14-20': 0.0,
|
|
104
|
+
'Pts Allow 21-27': 0.0, 'Pts Allow 28-34': 0.0, 'Pts Allow 35+': 0.0, 'XPR': 0.0
|
|
105
|
+
},
|
|
106
|
+
'roster_spots': pd.DataFrame({
|
|
107
|
+
'position': ['QB', 'RB', 'WR', 'TE', 'W/R/T', 'Q/W/R/T', 'K', 'BN'],
|
|
108
|
+
'count': [1, 2, 3, 1, 1, 0, 0, 10]
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_league_config(platform: str):
|
|
114
|
+
"""
|
|
115
|
+
Get predefined configuration for a specific platform.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
platform: Platform name ('sfb', 'draftkings'/'dk', 'underdog')
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dict containing scoring and roster configuration, or None if not found
|
|
122
|
+
"""
|
|
123
|
+
platform_lower = platform.lower()
|
|
124
|
+
|
|
125
|
+
if platform_lower == 'sfb':
|
|
126
|
+
return SFB_CONFIG
|
|
127
|
+
elif platform_lower in ['dk', 'draftkings']:
|
|
128
|
+
return DRAFTKINGS_CONFIG
|
|
129
|
+
elif platform_lower == 'underdog':
|
|
130
|
+
return UNDERDOG_CONFIG
|
|
131
|
+
else:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def apply_default_scoring_categories(scoring: dict):
|
|
136
|
+
"""
|
|
137
|
+
Add missing scoring categories with default values of 0.0.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
scoring: Dictionary of scoring settings
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary with all expected scoring categories
|
|
144
|
+
"""
|
|
145
|
+
default_categories = [
|
|
146
|
+
'Pass Comp', 'Pass 1D', 'Rush Att', 'Rush 1D', 'Rec 1D',
|
|
147
|
+
'TE Rec Bonus', 'TE 1D Bonus', 'Pass 300+', 'Rush 100+', 'Rec 100+'
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
for category in default_categories:
|
|
151
|
+
if category not in scoring:
|
|
152
|
+
scoring[category] = 0.0
|
|
153
|
+
|
|
154
|
+
# Set default kicker scoring if missing
|
|
155
|
+
if "FG 0-19" not in scoring:
|
|
156
|
+
scoring["FG 0-19"] = 3
|
|
157
|
+
|
|
158
|
+
# Set default reception scoring if missing
|
|
159
|
+
if "Rec" not in scoring:
|
|
160
|
+
scoring["Rec"] = 0
|
|
161
|
+
|
|
162
|
+
# Set default return yardage scoring if missing
|
|
163
|
+
if "Ret Yds" not in scoring:
|
|
164
|
+
scoring["Ret Yds"] = 0
|
|
165
|
+
|
|
166
|
+
return scoring
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data providers and player/league clients."""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NFL data provider interface.
|
|
3
|
+
|
|
4
|
+
Defines the contract that every concrete NFL data backend must satisfy so
|
|
5
|
+
that the rest of fantasyfb is decoupled from any specific source (PFR,
|
|
6
|
+
nflreadpy, ESPN, etc.).
|
|
7
|
+
|
|
8
|
+
The canonical column schema returned by each method is documented on the
|
|
9
|
+
method itself. Concrete providers are responsible for translating their
|
|
10
|
+
upstream schema into this canonical one.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NFLDataProvider(ABC):
|
|
19
|
+
"""Abstract base for NFL data backends.
|
|
20
|
+
|
|
21
|
+
The legacy column name `player_id_sr` is preserved as the canonical
|
|
22
|
+
cross-table player identifier. Concrete providers are free to populate
|
|
23
|
+
it with whatever ID system they natively use (PFR id, GSIS id, etc.) as
|
|
24
|
+
long as the same value joins stats, rosters, and depth charts.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def get_player_stats(self, start: int, finish: int) -> pd.DataFrame:
|
|
29
|
+
"""Per-game player + team-defense stats for the YYYYWW range.
|
|
30
|
+
|
|
31
|
+
Required columns:
|
|
32
|
+
player_id_sr, name, position, team, opponent,
|
|
33
|
+
season, week, game_id, points_allowed,
|
|
34
|
+
rush_yds, rush_att, rush_td, rush_first_down,
|
|
35
|
+
rec, rec_yds, rec_td, rec_first_down,
|
|
36
|
+
pass_yds, pass_cmp, pass_td, pass_first_down, pass_int,
|
|
37
|
+
fumbles_lost, kick_ret_yds, punt_ret_yds,
|
|
38
|
+
kick_ret_td, punt_ret_td, xpm, fgm,
|
|
39
|
+
sacks, def_int, fumbles_rec, def_int_td, fumbles_rec_td
|
|
40
|
+
|
|
41
|
+
Defenses are emitted as one row per team-week with
|
|
42
|
+
position == "DEF" and player_id_sr set to the team abbreviation.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def get_schedule(self, start_year: int, end_year: int) -> pd.DataFrame:
|
|
47
|
+
"""NFL schedule with one row per team-game (two rows per game).
|
|
48
|
+
|
|
49
|
+
Required columns:
|
|
50
|
+
season, week, date, team, home_away, opp_team,
|
|
51
|
+
elo_diff, opp_elo
|
|
52
|
+
|
|
53
|
+
elo_diff and opp_elo carry forward the legacy "opponent strength"
|
|
54
|
+
signal. Providers without true Elo ratings should map a strength
|
|
55
|
+
proxy (e.g. Vegas spread) into elo_diff with consistent sign:
|
|
56
|
+
positive elo_diff means the team is favored.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def get_rosters(self, start_year: int, end_year: int) -> pd.DataFrame:
|
|
61
|
+
"""Active rosters for the year range.
|
|
62
|
+
|
|
63
|
+
Required columns:
|
|
64
|
+
name, current_team, position, player_id_sr
|
|
65
|
+
|
|
66
|
+
Optional columns (populated when available):
|
|
67
|
+
yahoo_id -- enables direct Yahoo->NFL ID linkage
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def get_depth_charts(self) -> pd.DataFrame:
|
|
72
|
+
"""Current-week depth charts.
|
|
73
|
+
|
|
74
|
+
Required columns:
|
|
75
|
+
name, current_team, position, string
|
|
76
|
+
|
|
77
|
+
`string` follows the legacy convention: 1.0 = starter, 2.0 = backup,
|
|
78
|
+
3.0 = third string, etc.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def get_draft(self, year: int) -> pd.DataFrame:
|
|
83
|
+
"""Draft picks for the given year.
|
|
84
|
+
|
|
85
|
+
Required columns:
|
|
86
|
+
name, current_team, player_id_sr
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def team_aliases(self) -> pd.DataFrame:
|
|
91
|
+
"""Translation table between Yahoo and provider-native team codes.
|
|
92
|
+
|
|
93
|
+
Required columns:
|
|
94
|
+
yahoo -- value Yahoo emits as `editorial_team_abbr`
|
|
95
|
+
real_abbrev -- provider-native team code (matches the `team`
|
|
96
|
+
column returned by get_player_stats and
|
|
97
|
+
get_schedule)
|
|
98
|
+
|
|
99
|
+
The column names mirror the legacy team_abbrevs.csv schema so that
|
|
100
|
+
existing callers reading `nfl_teams.real_abbrev` keep working; the
|
|
101
|
+
values now point at the active backend rather than at PFR.
|
|
102
|
+
"""
|