Projections¶
The projection layer converts weekly NFL stats into per-game points projections (mean + stdev) for every fantasy-relevant player.
ProjectionEngineV2¶
The current production engine. Vegas-backed matchup factors with walk-forward weight fitting.
fantasyfb.projections.engine_v2.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 |
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 |
|
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. |
MatchupModel¶
Applies opponent/Vegas factors on top of base rates.
fantasyfb.scoring.matchup_model.MatchupModel ¶
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 |
required |
schedule_df
|
DataFrame
|
per-team-game schedule with |
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 ¶
Attach a matchup_factor column to a players-shaped DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
players
|
DataFrame
|
must have |
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 |
DataFrame
|
inputs (team implied total, opp_team, opp allowed) joined on |
DataFrame
|
for debuggability. |
Model fitter¶
Walk-forward fitting of the projection-engine weights.
fantasyfb.projections.model_fitter ¶
Walk-forward least-squares fitting of MatchupModel weights.
Stage 3 of the V2 rebuild. The matchup factor is multiplicative around 1.0:
factor = 1 + alpha * z_implied_total + beta * z_allowed_vs_pos
- gamma * (string - 1)
Once a player's neutral_rate (volume * efficiency) is fixed, the alpha
and beta terms are linear in their parameters per position, so OLS does
the work. Gamma (depth-chart penalty) is left at its hand-tuned default
because we don't have reliable historical week-by-week depth charts to
fit against.
Walk-forward construction: for each historical game, we compute features using only data from before that game. That isolates the matchup signal from the player's own future performance and prevents the fit from memorizing rather than generalizing.
_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)}
MatchupModel ¶
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 |
required |
schedule_df
|
DataFrame
|
per-team-game schedule with |
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 ¶
Attach a matchup_factor column to a players-shaped DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
players
|
DataFrame
|
must have |
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 |
DataFrame
|
inputs (team implied total, opp_team, opp allowed) joined on |
DataFrame
|
for debuggability. |
_PositionWeights
dataclass
¶
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 |
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 |
|
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. |
build_training_set ¶
build_training_set(stats_df: DataFrame, schedule_df: DataFrame, training_seasons: List[int], earliest_history: Optional[int] = None, min_neutral_rate: float = 0.5) -> pd.DataFrame
Walk-forward feature/target table for matchup weight fitting.
For each game in training_seasons, we compute:
neutral_rate -- ProjectionEngineV2 output as of the start of
the target week, using all stats from the
earliest history up to but not including the
target week.
z_implied_total -- z-score of the team's implied total against
the league baseline for that cutoff.
z_allowed -- z-score of opponent's allowed-fantasy-points-
vs-position against league baseline.
actual_points -- the actual fantasy points the player scored
in the target week (the regression target).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stats_df
|
DataFrame
|
per-game stats with |
required |
schedule_df
|
DataFrame
|
provider schedule with |
required |
training_seasons
|
List[int]
|
seasons to use as targets (we still pull from earlier seasons for history). |
required |
earliest_history
|
Optional[int]
|
oldest YYYYWW to include in history. Defaults to two full seasons before the earliest training season. |
None
|
min_neutral_rate
|
float
|
drop rows whose neutral_rate falls below this threshold; a tiny denominator turns into a huge meaningless residual that would dominate the fit. |
0.5
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Long-format DataFrame; one row per (player, week) actually played. |
fit_matchup_weights ¶
fit_matchup_weights(training: DataFrame, default_weights: Optional[Dict[str, _PositionWeights]] = None, min_samples: int = 50, coefficient_clip: float = 1.0, ridge_lambda: float = 5.0) -> Dict[str, _PositionWeights]
Per-position OLS fit for alpha (implied total) and beta (allowed).
Solves for each position the Bayesian-ridge problem
minimize ||y - X*beta||^2 + ridge_lambda * ||beta - beta_default||^2
where y = actual / neutral - 1 and X = [z_implied, z_allowed]. Pulling toward the hand-tuned default rather than toward zero matters for the offensive positions: V2's neutral_rate already absorbs most of the "team scoring environment" signal through historical efficiency, so the residual signal that matchup adds is small. A vanilla ridge would collapse alphas to 0 in low-signal positions; ridge-to-defaults preserves the hand-tuned prior unless data overrides it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
training
|
DataFrame
|
output of :func: |
required |
default_weights
|
Optional[Dict[str, _PositionWeights]]
|
prior weights to ridge-shrink toward. Below the
|
None
|
min_samples
|
int
|
minimum row count to attempt a fit; below this, fall through to defaults. |
50
|
coefficient_clip
|
float
|
upper bound for |alpha| and |beta| post-fit. |
1.0
|
ridge_lambda
|
float
|
L2 strength on deviations from defaults. Larger = trust defaults more relative to data. 5.0 is calibrated to yield meaningful movement on positions with hundreds of samples while keeping K/DEF (where data is noisy) close to their priors. |
5.0
|
Returns:
| Type | Description |
|---|---|
Dict[str, _PositionWeights]
|
Dict mapping position to fitted |
Dict[str, _PositionWeights]
|
is carried over from defaults; we don't have reliable historical |
Dict[str, _PositionWeights]
|
depth-chart data to fit it against. |
fit_from_history ¶
fit_from_history(stats_df: DataFrame, schedule_df: DataFrame, training_seasons: List[int], **fit_kwargs) -> Dict[str, _PositionWeights]
Convenience: build the training set, run the fit, return weights.
Heavy compute is in :func:build_training_set (one V2 projection
call per training week). Cache the training DataFrame if you're
going to refit with different hyperparameters.