Skip to content

Simulation

Monte Carlo season simulation and the backtest harness used to compare projection engines.

SeasonSimulator

fantasyfb.sim.season_simulator.SeasonSimulator

SeasonSimulator(league_settings: Dict, roster_spots=None)

Simulates fantasy football seasons using Monte Carlo methods.

Handles regular season play, playoff brackets, and outcome probabilities for any fantasy football league structure.

Initialize simulator with league-specific settings.

Parameters:

Name Type Description Default
league_settings Dict

Dictionary containing: - playoff_start_week: Week when playoffs begin - num_playoff_teams: Number of teams making playoffs - uses_playoff_reseeding: Whether to reseed after each round - num_teams: Total teams in league

required
roster_spots

Roster-spots DataFrame (position/count columns) or {slot: count} dict. Required when simulate_season is called with best_ball=True.

None

simulate_season

simulate_season(player_projections: DataFrame, schedule_df: DataFrame, num_sims: int = 10000, best_ball: bool = False, include_playoffs: bool = True, payouts: List[float] = [800, 300, 100], fixed_winner: Optional[List] = None) -> Tuple[pd.DataFrame, pd.DataFrame]

Simulate a complete fantasy season with playoffs.

Parameters:

Name Type Description Default
player_projections DataFrame

DataFrame with columns [fantasy_team, week, points_avg, points_stdev]. When best_ball=True, must also have position column and one row per player per week; the simulator converts these to team-level projections via optimal lineup selection before running the Monte Carlo.

required
schedule_df DataFrame

DataFrame with columns [week, team_1, team_2, score_1, score_2]

required
num_sims int

Number of Monte Carlo simulations

10000
best_ball bool

When True, compute weekly team scores using the optimal lineup from each team's full player pool (best ball mode). Requires roster_spots to be set on this simulator instance.

False
include_playoffs bool

Whether to simulate playoffs

True
payouts List[float]

Prize amounts for [1st, 2nd, 3rd, ...]

[800, 300, 100]
fixed_winner Optional[List]

Optional [week, team_name] to force a specific outcome

None

Returns:

Type Description
Tuple[DataFrame, DataFrame]

Tuple of (schedule_results, standings_results) DataFrames

ScheduleManager

fantasyfb.sim.schedule_manager.ScheduleManager

ScheduleManager(client: FantasyPlatformClient, teams, settings, lg_id, latest_season)

Manages fantasy league schedule operations including regular season and postseason.

Initialize the schedule manager.

Parameters:

Name Type Description Default
client FantasyPlatformClient

FantasyPlatformClient instance

required
teams

List of team dictionaries

required
settings

League settings dictionary

required
lg_id

League ID

required
latest_season

The most recent NFL season year

required

get_schedule

get_schedule(season, week, current_week, team_key=None)

Pulls the fantasy schedule for the season in question as well as scores for all matchups up to the week in question.

Parameters:

Name Type Description Default
season

Season year

required
week

Current week

required
current_week

Retained for API compatibility; no longer used (schedule treats the as_of week as "start of week N" for any value of week).

required
team_key

Team key for identifying "me" column

None

Returns:

Type Description

DataFrame with fantasy schedule

Backtest

fantasyfb.sim.backtest

Backtest harness for the V2 projection engine.

Walks forward through a held-out test period and, for each (player, week) that actually played, computes:

V2_neutral = V2 engine's points_rate (no matchup adjustment) V2_default = V2 engine's points_rate * MatchupModel(default weights) V2_fitted = V2 engine's points_rate * MatchupModel(LS-fitted weights) baseline = position average

Then reports per-position MAE and RMSE.

Walk-forward construction guards against look-ahead bias: predictions for week W only use stats from games played before W. The fitted weights are trained on the season prior to the test season and applied to the test set.

When a provider is passed to run_backtest, V2_default/V2_fitted use that week's real historical depth-chart strings (via get_depth_charts(season, week)) instead of hardcoding string=1.0 for every player, so MatchupModel's string-penalty term is actually exercised by the walk-forward fit rather than going untested.

_DEFAULT_WEIGHTS module-attribute

_DEFAULT_WEIGHTS: Dict[str, _PositionWeights] = {'QB': _PositionWeights(alpha=0.2, beta=0.15, gamma=0.5), 'RB': _PositionWeights(alpha=0.15, beta=0.2, gamma=0.55), 'WR': _PositionWeights(alpha=0.15, beta=0.2, gamma=0.4), 'TE': _PositionWeights(alpha=0.1, beta=0.2, gamma=0.4), 'K': _PositionWeights(alpha=0.1, beta=0.0, gamma=0.0), 'DEF': _PositionWeights(alpha=-0.3, beta=0.15, gamma=0.0)}

_FANTASY_POSITIONS module-attribute

_FANTASY_POSITIONS = ['QB', 'RB', 'WR', 'TE', 'K', 'DEF']

NFLDataProvider

Bases: ABC

Abstract base for NFL data backends.

The legacy column name player_id_sr is preserved as the canonical cross-table player identifier. Concrete providers are free to populate it with whatever ID system they natively use (PFR id, GSIS id, etc.) as long as the same value joins stats, rosters, and depth charts.

get_player_stats abstractmethod

get_player_stats(start: int, finish: int) -> pd.DataFrame

Per-game player + team-defense stats for the YYYYWW range.

Required columns

player_id_sr, name, position, team, opponent, season, week, game_id, points_allowed, rush_yds, rush_att, rush_td, rush_first_down, rec, rec_yds, rec_td, rec_first_down, pass_yds, pass_cmp, pass_td, pass_first_down, pass_int, fumbles_lost, kick_ret_yds, punt_ret_yds, kick_ret_td, punt_ret_td, xpm, fgm, sacks, def_int, fumbles_rec, def_int_td, fumbles_rec_td

Defenses are emitted as one row per team-week with position == "DEF" and player_id_sr set to the team abbreviation.

get_schedule abstractmethod

get_schedule(start_year: int, end_year: int) -> pd.DataFrame

NFL schedule with one row per team-game (two rows per game).

Required columns

season, week, date, team, home_away, opp_team, elo_diff, opp_elo

elo_diff and opp_elo carry forward the legacy "opponent strength" signal. Providers without true Elo ratings should map a strength proxy (e.g. Vegas spread) into elo_diff with consistent sign: positive elo_diff means the team is favored.

get_rosters abstractmethod

get_rosters(start_year: int, end_year: int) -> pd.DataFrame

Active rosters for the year range.

Required columns

name, current_team, position, player_id_sr

Optional columns (populated when available): yahoo_id -- enables direct Yahoo->NFL ID linkage

get_depth_charts abstractmethod

get_depth_charts(season: int | None = None, week: int | None = None) -> pd.DataFrame

Depth charts, current-week by default or a historical snapshot.

Parameters:

Name Type Description Default
season int | None

if given, look up this season's depth chart instead of the live/current one. Historical lookups are only meaningful for seasons before the provider's rolling current-snapshot feed took over (nflreadpy: pre-2025).

None
week int | None

if given alongside season, the specific week within that season. Ignored when season is omitted.

None
Required columns

name, current_team, position, string

string follows the legacy convention: 1.0 = starter, 2.0 = backup, 3.0 = third string, etc.

get_draft abstractmethod

get_draft(year: int) -> pd.DataFrame

Draft picks for the given year.

Required columns

name, current_team, player_id_sr

team_aliases abstractmethod

team_aliases() -> pd.DataFrame

Translation table between Yahoo and provider-native team codes.

Required columns

yahoo -- value Yahoo emits as editorial_team_abbr real_abbrev -- provider-native team code (matches the team column returned by get_player_stats and get_schedule)

The column names mirror the legacy team_abbrevs.csv schema so that existing callers reading nfl_teams.real_abbrev keep working; the values now point at the active backend rather than at PFR.

MatchupModel

MatchupModel(weights: Optional[Dict[str, _PositionWeights]] = None)

Per-game multiplicative matchup factor.

Build via :meth:from_history, which precomputes the league-wide distributions of implied total and allowed-vs-position used to z-score each input. Then call :meth:apply_factors to attach a matchup_factor column to a players DataFrame for a given week.

from_history classmethod

from_history(stats_df: DataFrame, schedule_df: DataFrame, weights: Optional[Dict[str, _PositionWeights]] = None) -> 'MatchupModel'

Fit the league-average baselines from historical data.

Parameters:

Name Type Description Default
stats_df DataFrame

per-game player stats with points, position, opponent, season, week columns. Used to compute "fantasy points allowed by team X to position P per game" via the player's opponent field.

required
schedule_df DataFrame

per-team-game schedule with implied_total. Used for the league mean/std of implied totals.

required
weights Optional[Dict[str, _PositionWeights]]

optional per-position coefficient overrides.

None

factor

factor(position: str, team_implied_total: float, opp_implied_total: float, opp_team: str, string: float = 1.0) -> float

Per-player matchup multiplier centered at 1.0.

apply_factors

apply_factors(players: DataFrame, schedule: DataFrame, as_of: int) -> pd.DataFrame

Attach a matchup_factor column to a players-shaped DataFrame.

Parameters:

Name Type Description Default
players DataFrame

must have current_team, position, and string.

required
schedule DataFrame

provider schedule output (one row per team-week).

required
as_of int

YYYYWW of the week we're projecting.

required

Returns:

Type Description
DataFrame

A copy of players with matchup_factor and the underlying

DataFrame

inputs (team implied total, opp_team, opp allowed) joined on

DataFrame

for debuggability.

_PositionWeights dataclass

_PositionWeights(alpha: float, beta: float, gamma: float)

Coefficient bundle for one position's matchup factor.

factor = 1 + alpha * z(team_implied_total) + beta * z(opp_allowed_vs_position) - gamma * string_penalty(string)

where z(x) = (x - league_avg) / league_std, and string_penalty is a position-aware function of depth-chart string (see _string_penalty_slots) that is 0 for genuinely-starting depth and only grows once a player is beyond the slots a position actually plays.

ProjectionEngineV2

ProjectionEngineV2(time_decay: float = _DEFAULT_TIME_DECAY, shrinkage_n: Optional[Dict[str, int]] = None)

Volume x efficiency projection engine.

Returns the same points_rate/points_stdev/num_games schema as V1 so the rest of fantasyfb (lineup optimizer, season simulator, move analyzer) keeps working unchanged. Per-week matchup adjustment happens downstream in lineup_optimizer, same as V1.

Parameters:

Name Type Description Default
time_decay float

Per-week exponential decay rate applied when aggregating a player's volume and efficiency over their game history. 0 = no decay (treat all games equally), higher = trust recent games more.

_DEFAULT_TIME_DECAY
shrinkage_n Optional[Dict[str, int]]

Bayesian shrinkage strength per position, expressed as equivalent prior games. Defaults to _DEFAULT_SHRINKAGE.

None

calculate_projections

calculate_projections(stats_df: DataFrame, earliest_weeks: Dict[str, int], current_week: int, nfl_schedule: Optional[DataFrame] = None) -> pd.DataFrame

Project per-game fantasy points for every player in stats_df.

Parameters:

Name Type Description Default
stats_df DataFrame

Historical game stats, must include points (added upstream by FantasyScorer) plus the position-specific opportunity columns referenced in _OPPORTUNITY_DEF.

required
earliest_weeks Dict[str, int]

Per-position YYYYWW cutoff -- games older than this are excluded.

required
current_week int

Training cutoff in YYYYWW. Games on or after this week are excluded (no look-ahead).

required
nfl_schedule Optional[DataFrame]

Accepted for V1 API parity; unused in Stage 1.

None

Returns:

Name Type Description
DataFrame

DataFrame with one row per player plus one avg_<POS> row

DataFrame

per position (used as the rookie fallback by fantasyfb.py).

Columns DataFrame

player_id_sr, position, points_rate, points_stdev,

DataFrame

num_games, volume_rate, efficiency_rate.

BacktestRow dataclass

BacktestRow(player_id_sr: str, position: str, season: int, week: int, actual: float, v2_neutral: float, v2_default: float, v2_fitted: float, baseline: float)

Per-(player, week) prediction record across all model variants.

_historical_strings

_historical_strings(provider: Optional[NFLDataProvider], season: int, week: int) -> pd.DataFrame

Depth-chart strings as of a specific historical week.

Returns a player_id_sr -> string lookup, empty if no provider was given or the provider has no data for that week (e.g. depth charts older than nflreadpy's legacy-schema coverage). Callers should treat a miss the same as an unlisted player: fall back to string=1.0 (nflreadpy depth charts only include the top handful of strings per team, so most bench/inactive players legitimately don't appear).

_v2_predictions

_v2_predictions(engine: ProjectionEngineV2, matchup: Optional[MatchupModel], stats: DataFrame, schedule: DataFrame, cutoff: int, strings: Optional[DataFrame] = None) -> pd.DataFrame

V2 engine output, optionally multiplied by a matchup factor.

Parameters:

Name Type Description Default
strings Optional[DataFrame]

player_id_sr -> string lookup for this week's real historical depth chart (see _historical_strings). Players missing from it (not on a team's top handful of strings, or no historical data available) fall back to string=1.0, i.e. "assume starter" rather than "assume backup" -- consistent with the live-analysis default in player_data_manager.py.

None

run_backtest

run_backtest(stats: DataFrame, schedule: DataFrame, test_season: int, test_weeks: List[int], fitted_weights: Optional[Dict[str, _PositionWeights]] = None, provider: Optional[NFLDataProvider] = None) -> pd.DataFrame

Per-(player, week) predictions for every model variant.

Parameters:

Name Type Description Default
stats DataFrame

per-game stats with points populated; should cover multiple seasons before test_season for history.

required
schedule DataFrame

provider schedule with Vegas fields.

required
test_season int

season to backtest in.

required
test_weeks List[int]

weeks within test_season to evaluate.

required
fitted_weights Optional[Dict[str, _PositionWeights]]

LS-fit weights for V2_fitted. If None, V2_fitted falls back to position-average predictions.

None
provider Optional[NFLDataProvider]

if given, used to pull each test week's real historical depth-chart strings (via get_depth_charts(season, week)) so the matchup model's string penalty is actually exercised during backtesting instead of every player being treated as string=1.0. Only meaningful for pre-2025 seasons, where nflreadpy's depth-chart feed has week-level historical data; omit for seasons before nflreadpy's coverage starts or when the string penalty isn't under test.

None

Returns:

Type Description
DataFrame

Long DataFrame, one row per (player, week, variant). Columns:

DataFrame

player_id_sr, position, season, week, variant, prediction, actual.

evaluate

evaluate(predictions: DataFrame) -> pd.DataFrame

Per-(variant, position) MAE and RMSE.