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.
Files changed (38) hide show
  1. fantasyfb/__init__.py +41 -0
  2. fantasyfb/analysis/__init__.py +1 -0
  3. fantasyfb/analysis/move_analyzer.py +639 -0
  4. fantasyfb/analysis/war_calculator.py +165 -0
  5. fantasyfb/cli.py +202 -0
  6. fantasyfb/configs.py +166 -0
  7. fantasyfb/data/__init__.py +1 -0
  8. fantasyfb/data/nfl_provider.py +102 -0
  9. fantasyfb/data/nflreadpy_provider.py +367 -0
  10. fantasyfb/data/player_data_manager.py +440 -0
  11. fantasyfb/data/yahoo_client.py +466 -0
  12. fantasyfb/drafts/__init__.py +1 -0
  13. fantasyfb/drafts/prep.py +314 -0
  14. fantasyfb/drafts/salary_cap.py +388 -0
  15. fantasyfb/drafts/snake.py +554 -0
  16. fantasyfb/drafts/snake_cockpit.py +288 -0
  17. fantasyfb/drafts/tools.py +638 -0
  18. fantasyfb/io/__init__.py +1 -0
  19. fantasyfb/io/excel_exporter.py +252 -0
  20. fantasyfb/league.py +830 -0
  21. fantasyfb/projections/__init__.py +1 -0
  22. fantasyfb/projections/engine.py +312 -0
  23. fantasyfb/projections/engine_v2.py +321 -0
  24. fantasyfb/projections/model_fitter.py +227 -0
  25. fantasyfb/scoring/__init__.py +1 -0
  26. fantasyfb/scoring/fantasy_scoring.py +202 -0
  27. fantasyfb/scoring/lineup_optimizer.py +197 -0
  28. fantasyfb/scoring/matchup_model.py +275 -0
  29. fantasyfb/sim/__init__.py +1 -0
  30. fantasyfb/sim/backtest.py +267 -0
  31. fantasyfb/sim/schedule_manager.py +142 -0
  32. fantasyfb/sim/season_simulator.py +489 -0
  33. fantasyfb-0.3.0.dist-info/METADATA +150 -0
  34. fantasyfb-0.3.0.dist-info/RECORD +38 -0
  35. fantasyfb-0.3.0.dist-info/WHEEL +5 -0
  36. fantasyfb-0.3.0.dist-info/entry_points.txt +5 -0
  37. fantasyfb-0.3.0.dist-info/licenses/LICENSE +21 -0
  38. 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
+ """