Contract Valuation Models
Beginner
10 min read
1 views
Nov 27, 2025
# Contract Valuation
Contract valuation in sports analytics combines economic principles with performance metrics to assess player value and contract efficiency. This analysis helps teams make informed decisions about player acquisitions, extensions, and trades.
## Dollars Per Win Models
### Theoretical Framework
The dollars per win ($/WAR or $/Win) model establishes a market value for wins by analyzing free agent contracts relative to player performance. This creates a standardized currency for comparing player value.
**Core Formula:**
```
$/Win = Total Contract Value / Expected Wins Above Replacement
Market Value = Player WAR × $/Win
```
### Calculating Market $/Win
The market rate for wins fluctuates based on salary cap changes, free agent market conditions, and league revenues.
**Python Implementation:**
```python
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from scipy import stats
class DollarsPerWinModel:
"""
Calculate market dollars per win using free agent contract data
"""
def __init__(self):
self.model = LinearRegression()
self.market_rate = None
self.confidence_interval = None
def calculate_market_rate(self, contracts_df, year=None):
"""
Calculate $/Win from free agent contracts
Parameters:
-----------
contracts_df : DataFrame
Columns: player, year, aav (average annual value), war, age
year : int
Specific year to calculate (None = most recent)
"""
if year:
df = contracts_df[contracts_df['year'] == year].copy()
else:
df = contracts_df.copy()
# Filter for quality free agents (avoid negative WAR outliers)
df = df[df['war'] > 0.5]
# Calculate $/Win for each contract
df['dollars_per_win'] = df['aav'] / df['war']
# Use median to avoid outlier influence
self.market_rate = df['dollars_per_win'].median()
# Calculate confidence interval
self.confidence_interval = stats.t.interval(
0.95,
len(df) - 1,
loc=df['dollars_per_win'].mean(),
scale=stats.sem(df['dollars_per_win'])
)
return self.market_rate
def regression_approach(self, contracts_df):
"""
Alternative: Regression-based $/Win calculation
Regresses AAV on WAR to find the cost per win
"""
X = contracts_df[['war']].values
y = contracts_df['aav'].values
self.model.fit(X, y)
# Slope represents $/Win
self.market_rate = self.model.coef_[0]
return self.market_rate
def adjust_for_inflation(self, base_rate, base_year, target_year,
salary_cap_data):
"""
Adjust $/Win for salary cap inflation
"""
base_cap = salary_cap_data.loc[base_year, 'salary_cap']
target_cap = salary_cap_data.loc[target_year, 'salary_cap']
inflation_factor = target_cap / base_cap
adjusted_rate = base_rate * inflation_factor
return adjusted_rate
def calculate_player_value(self, war, position_adjustment=0):
"""
Calculate a player's market value
Parameters:
-----------
war : float
Player's wins above replacement
position_adjustment : float
Premium/discount for scarce positions (e.g., +20% for QB)
"""
if self.market_rate is None:
raise ValueError("Must calculate market rate first")
base_value = war * self.market_rate
adjusted_value = base_value * (1 + position_adjustment)
return adjusted_value
# Example usage
def example_usage():
# Sample free agent contract data
contracts = pd.DataFrame({
'player': ['Player A', 'Player B', 'Player C', 'Player D'],
'year': [2024, 2024, 2024, 2024],
'aav': [25000000, 18000000, 12000000, 8000000],
'war': [5.2, 3.8, 2.5, 1.8],
'age': [27, 29, 26, 30]
})
model = DollarsPerWinModel()
# Calculate market rate
market_rate = model.calculate_market_rate(contracts)
print(f"Market $/Win: ${market_rate:,.0f}")
print(f"95% CI: ${model.confidence_interval[0]:,.0f} - "
f"${model.confidence_interval[1]:,.0f}")
# Value a player with 4.0 WAR
player_value = model.calculate_player_value(4.0)
print(f"4.0 WAR Player Value: ${player_value:,.0f}")
# QB with positional premium (20%)
qb_value = model.calculate_player_value(4.0, position_adjustment=0.20)
print(f"4.0 WAR QB Value: ${qb_value:,.0f}")
```
**R Implementation:**
```r
library(tidyverse)
library(broom)
calculate_dollars_per_win <- function(contracts_df, method = "median") {
#' Calculate market dollars per win from free agent contracts
#'
#' @param contracts_df DataFrame with columns: aav, war, age
#' @param method Either "median", "mean", or "regression"
#' @return List with market rate and confidence interval
# Filter for quality free agents
filtered <- contracts_df %>%
filter(war > 0.5)
if (method == "median") {
# Median approach (robust to outliers)
filtered <- filtered %>%
mutate(dollars_per_win = aav / war)
market_rate <- median(filtered$dollars_per_win)
# Bootstrap confidence interval
boot_samples <- replicate(10000, {
sample_df <- filtered %>% sample_frac(replace = TRUE)
median(sample_df$dollars_per_win)
})
ci <- quantile(boot_samples, c(0.025, 0.975))
} else if (method == "regression") {
# Regression approach
model <- lm(aav ~ war + 0, data = filtered) # Force through origin
market_rate <- coef(model)["war"]
ci <- confint(model, "war", level = 0.95)
}
list(
market_rate = market_rate,
ci_lower = ci[1],
ci_upper = ci[2]
)
}
# Positional value adjustments
calculate_positional_value <- function(war, market_rate, position) {
#' Adjust player value based on position scarcity
position_premiums <- c(
"QB" = 0.20,
"EDGE" = 0.10,
"CB" = 0.10,
"OT" = 0.08,
"WR" = 0.00,
"RB" = -0.15,
"K" = -0.30
)
premium <- position_premiums[position]
if (is.na(premium)) premium <- 0
base_value <- war * market_rate
adjusted_value <- base_value * (1 + premium)
adjusted_value
}
# Example usage
contracts <- tibble(
player = c("Player A", "Player B", "Player C"),
aav = c(25000000, 18000000, 12000000),
war = c(5.2, 3.8, 2.5),
age = c(27, 29, 26)
)
result <- calculate_dollars_per_win(contracts, method = "median")
cat(sprintf("Market $/Win: $%.0f\n", result$market_rate))
cat(sprintf("95%% CI: $%.0f - $%.0f\n", result$ci_lower, result$ci_upper))
# Value a 4.0 WAR quarterback
qb_value <- calculate_positional_value(4.0, result$market_rate, "QB")
cat(sprintf("4.0 WAR QB Value: $%.0f\n", qb_value))
```
### Historical $/Win Trends
The market rate for wins has increased with salary cap growth:
| Year | MLB $/WAR | NBA $/Win | NFL $/WAR | NHL $/WAR |
|------|-----------|-----------|-----------|-----------|
| 2015 | $7.0M | $2.5M | $3.8M | $2.2M |
| 2017 | $8.0M | $2.8M | $4.5M | $2.6M |
| 2019 | $8.5M | $3.2M | $5.2M | $3.0M |
| 2021 | $8.2M | $3.5M | $5.8M | $3.2M |
| 2023 | $9.1M | $4.0M | $6.5M | $3.6M |
| 2024 | $9.5M | $4.2M | $7.0M | $3.8M |
## Surplus Value Analysis
### Concept
Surplus value represents the difference between a player's production value and their salary cost. This is crucial for evaluating trades, drafting strategies, and contract negotiations.
**Formula:**
```
Surplus Value = (Player WAR × $/Win) - Annual Salary
Total Surplus = Σ(Annual Surplus) over contract length
```
### Implementation
**Python Surplus Value Calculator:**
```python
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class Contract:
"""Represents a player contract"""
player_name: str
years: int
total_value: float
guaranteed: float
signing_bonus: float
annual_salaries: List[float]
class SurplusValueCalculator:
"""
Calculate surplus value for player contracts
"""
def __init__(self, dollars_per_win: float, discount_rate: float = 0.05):
"""
Parameters:
-----------
dollars_per_win : float
Market rate for wins
discount_rate : float
Discount rate for future value (typically 3-7%)
"""
self.dollars_per_win = dollars_per_win
self.discount_rate = discount_rate
def calculate_annual_surplus(self, projected_war: float, salary: float):
"""Calculate surplus value for a single year"""
market_value = projected_war * self.dollars_per_win
surplus = market_value - salary
return surplus
def calculate_contract_surplus(self, war_projections: List[float],
contract: Contract):
"""
Calculate total surplus value over contract length
Parameters:
-----------
war_projections : List[float]
Projected WAR for each year of contract
contract : Contract
Contract details
"""
if len(war_projections) != len(contract.annual_salaries):
raise ValueError("WAR projections must match contract years")
annual_surplus = []
npv_surplus = []
for year, (war, salary) in enumerate(
zip(war_projections, contract.annual_salaries), start=1
):
# Calculate annual surplus
market_value = war * self.dollars_per_win
surplus = market_value - salary
annual_surplus.append(surplus)
# Calculate NPV of surplus
discount_factor = 1 / (1 + self.discount_rate) ** (year - 1)
npv = surplus * discount_factor
npv_surplus.append(npv)
return {
'annual_surplus': annual_surplus,
'total_surplus': sum(annual_surplus),
'npv_surplus': npv_surplus,
'total_npv_surplus': sum(npv_surplus),
'avg_annual_surplus': np.mean(annual_surplus)
}
def compare_contracts(self, contracts_data: List[Dict]):
"""
Compare multiple contracts by surplus value
Parameters:
-----------
contracts_data : List[Dict]
Each dict contains 'contract' and 'war_projections'
"""
results = []
for data in contracts_data:
contract = data['contract']
projections = data['war_projections']
surplus = self.calculate_contract_surplus(projections, contract)
results.append({
'player': contract.player_name,
'total_value': contract.total_value,
'total_surplus': surplus['total_surplus'],
'npv_surplus': surplus['total_npv_surplus'],
'surplus_rate': surplus['total_surplus'] / contract.total_value,
'years': contract.years
})
return pd.DataFrame(results).sort_values('npv_surplus', ascending=False)
def rookie_contract_value(self, draft_position: int,
projected_war: List[float]):
"""
Calculate surplus value for rookie contracts
Rookie contracts have fixed slotted values
"""
# Simplified rookie salary scale (varies by league)
rookie_salaries = self._get_rookie_salaries(draft_position)
total_surplus = 0
for year, (war, salary) in enumerate(
zip(projected_war, rookie_salaries), start=1
):
market_value = war * self.dollars_per_win
surplus = market_value - salary
discount_factor = 1 / (1 + self.discount_rate) ** (year - 1)
total_surplus += surplus * discount_factor
return total_surplus
def _get_rookie_salaries(self, draft_position: int) -> List[float]:
"""Simplified rookie salary scale"""
# This is a simplified example - actual scales vary by league
base_salary = max(1000000, 10000000 - (draft_position * 200000))
return [base_salary * (1.05 ** i) for i in range(4)]
# Example usage
def example_surplus_analysis():
calculator = SurplusValueCalculator(
dollars_per_win=7000000,
discount_rate=0.05
)
# Example contract
contract = Contract(
player_name="Star Player",
years=5,
total_value=150000000,
guaranteed=100000000,
signing_bonus=30000000,
annual_salaries=[25000000, 28000000, 30000000, 32000000, 35000000]
)
# Projected performance (declining over time)
war_projections = [6.0, 5.5, 4.8, 4.0, 3.2]
surplus = calculator.calculate_contract_surplus(war_projections, contract)
print(f"Player: {contract.player_name}")
print(f"Total Contract Value: ${contract.total_value:,.0f}")
print(f"\nYear-by-Year Analysis:")
for year, (war, salary, surplus_val) in enumerate(
zip(war_projections, contract.annual_salaries, surplus['annual_surplus']),
start=1
):
market_val = war * calculator.dollars_per_win
print(f" Year {year}: {war} WAR, ${salary:,.0f} salary, "
f"${market_val:,.0f} market value, "
f"${surplus_val:,.0f} surplus")
print(f"\nTotal Surplus: ${surplus['total_surplus']:,.0f}")
print(f"NPV of Surplus: ${surplus['total_npv_surplus']:,.0f}")
print(f"Surplus Rate: {(surplus['total_surplus']/contract.total_value)*100:.1f}%")
```
**R Implementation:**
```r
library(tidyverse)
calculate_surplus_value <- function(war_projections, salaries,
dollars_per_win, discount_rate = 0.05) {
#' Calculate surplus value for a contract
#'
#' @param war_projections Vector of projected WAR by year
#' @param salaries Vector of annual salaries
#' @param dollars_per_win Market rate for wins
#' @param discount_rate Discount rate for NPV calculation
#' @return DataFrame with surplus analysis
years <- length(war_projections)
surplus_df <- tibble(
year = 1:years,
war = war_projections,
salary = salaries,
market_value = war * dollars_per_win,
surplus = market_value - salary,
discount_factor = 1 / (1 + discount_rate) ^ (year - 1),
npv_surplus = surplus * discount_factor
)
surplus_df
}
# Compare multiple contracts
compare_contracts_surplus <- function(contracts_list, dollars_per_win) {
#' Compare surplus value across multiple contracts
#'
#' @param contracts_list List of lists, each with player, war, salaries
#' @param dollars_per_win Market rate for wins
results <- map_df(contracts_list, function(contract) {
surplus_df <- calculate_surplus_value(
contract$war,
contract$salaries,
dollars_per_win
)
tibble(
player = contract$player,
total_value = sum(contract$salaries),
total_surplus = sum(surplus_df$surplus),
npv_surplus = sum(surplus_df$npv_surplus),
avg_annual_war = mean(contract$war),
years = length(contract$war)
)
})
results %>%
mutate(surplus_rate = total_surplus / total_value) %>%
arrange(desc(npv_surplus))
}
# Example usage
contracts <- list(
list(
player = "Player A",
war = c(6.0, 5.5, 4.8, 4.0, 3.2),
salaries = c(25e6, 28e6, 30e6, 32e6, 35e6)
),
list(
player = "Player B",
war = c(4.5, 4.2, 3.8, 3.5),
salaries = c(18e6, 20e6, 22e6, 24e6)
)
)
comparison <- compare_contracts_surplus(contracts, dollars_per_win = 7e6)
print(comparison)
# Visualize surplus over time
plot_surplus_timeline <- function(war_projections, salaries,
dollars_per_win, player_name) {
surplus_df <- calculate_surplus_value(war_projections, salaries,
dollars_per_win)
ggplot(surplus_df, aes(x = year)) +
geom_col(aes(y = surplus / 1e6, fill = surplus > 0)) +
geom_line(aes(y = market_value / 1e6), color = "blue", size = 1) +
geom_line(aes(y = salary / 1e6), color = "red", size = 1) +
scale_fill_manual(values = c("TRUE" = "green", "FALSE" = "red")) +
labs(
title = paste("Surplus Value Timeline -", player_name),
x = "Contract Year",
y = "Value (Millions)",
fill = "Positive Surplus"
) +
theme_minimal()
}
```
## Age and Injury Risk Factors
### Age Curves
Player performance typically follows a predictable age curve, with peak performance in late 20s followed by decline.
**Python Age Adjustment Model:**
```python
import pandas as pd
import numpy as np
from scipy.optimize import curve_fit
import matplotlib.pyplot as plt
class AgeCurveModel:
"""
Model player performance aging patterns
"""
def __init__(self):
self.curve_params = None
self.position_curves = {}
def quadratic_age_curve(self, age, peak_age, peak_value, decay_rate):
"""
Quadratic age curve model
Performance peaks at peak_age then declines
"""
return peak_value - decay_rate * (age - peak_age) ** 2
def fit_age_curve(self, ages, performance, position=None):
"""
Fit age curve to historical data
Parameters:
-----------
ages : array
Player ages
performance : array
Performance metric (WAR, rating, etc.)
position : str
Position group for position-specific curves
"""
# Initial parameter guesses
p0 = [27, np.max(performance), 0.1] # peak_age, peak_value, decay
params, _ = curve_fit(
self.quadratic_age_curve,
ages,
performance,
p0=p0,
bounds=([23, 0, 0], [32, np.inf, 1])
)
if position:
self.position_curves[position] = params
else:
self.curve_params = params
return params
def project_aging(self, current_age, current_performance,
future_years, position=None):
"""
Project performance over future years accounting for aging
Returns:
--------
List of projected performance values
"""
if position and position in self.position_curves:
params = self.position_curves[position]
elif self.curve_params is not None:
params = self.curve_params
else:
raise ValueError("Must fit age curve first")
peak_age, peak_value, decay_rate = params
# Calculate current performance vs. expected
expected_current = self.quadratic_age_curve(
current_age, peak_age, peak_value, decay_rate
)
# Calculate talent level (performance above/below curve)
talent_adjustment = current_performance - expected_current
# Project future performance
projections = []
for year in range(future_years):
future_age = current_age + year + 1
expected = self.quadratic_age_curve(
future_age, peak_age, peak_value, decay_rate
)
projected = expected + talent_adjustment
projections.append(max(0, projected)) # Floor at 0
return projections
def calculate_age_multiplier(self, age, position=None):
"""
Calculate performance multiplier based on age
1.0 = peak age, <1.0 = declining
"""
if position and position in self.position_curves:
params = self.position_curves[position]
else:
params = self.curve_params
peak_age, peak_value, decay_rate = params
current_expected = self.quadratic_age_curve(
age, peak_age, peak_value, decay_rate
)
multiplier = current_expected / peak_value
return multiplier
class InjuryRiskModel:
"""
Model injury risk and adjust projections accordingly
"""
def __init__(self):
self.injury_rates = {}
def calculate_injury_risk(self, age, injury_history, position):
"""
Calculate injury risk probability
Parameters:
-----------
age : int
Player age
injury_history : list
List of past injuries (severity scores)
position : str
Player position
Returns:
--------
float : Probability of significant injury (0-1)
"""
# Base rates by position (example values)
position_base_rates = {
'RB': 0.25,
'WR': 0.15,
'QB': 0.12,
'OL': 0.20,
'DL': 0.22,
'LB': 0.18,
'DB': 0.14
}
base_rate = position_base_rates.get(position, 0.15)
# Age multiplier (increases with age)
age_multiplier = 1.0 + max(0, (age - 27) * 0.08)
# Injury history impact
if len(injury_history) > 0:
recent_injuries = [inj for inj in injury_history if inj > 0.5]
history_multiplier = 1.0 + (len(recent_injuries) * 0.15)
else:
history_multiplier = 0.8 # Bonus for clean history
# Combined risk
injury_risk = min(0.9, base_rate * age_multiplier * history_multiplier)
return injury_risk
def adjust_projection_for_injury(self, base_projection, injury_risk,
severity=0.5):
"""
Adjust performance projection for injury risk
Parameters:
-----------
base_projection : float
Baseline performance projection
injury_risk : float
Probability of injury (0-1)
severity : float
Expected performance impact if injured (0-1)
"""
# Expected value accounting for injury probability
healthy_value = base_projection * (1 - injury_risk)
injured_value = base_projection * (1 - severity) * injury_risk
adjusted_projection = healthy_value + injured_value
return adjusted_projection
def calculate_injury_adjusted_war(self, war_projections, age,
injury_history, position):
"""
Adjust multi-year WAR projections for injury risk
"""
adjusted_projections = []
for year, base_war in enumerate(war_projections):
current_age = age + year
# Injury risk increases with age
injury_risk = self.calculate_injury_risk(
current_age,
injury_history,
position
)
# Adjust projection
adjusted_war = self.adjust_projection_for_injury(
base_war,
injury_risk,
severity=0.4 # Injury reduces performance by 40%
)
adjusted_projections.append(adjusted_war)
return adjusted_projections
# Example usage
def example_age_injury_analysis():
# Age curve analysis
age_model = AgeCurveModel()
# Sample historical data
ages = np.array([23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33])
performance = np.array([3.2, 4.1, 4.8, 5.5, 5.8, 5.7, 5.3, 4.7, 4.0, 3.2, 2.5])
# Fit age curve
params = age_model.fit_age_curve(ages, performance, position='WR')
print(f"WR Age Curve - Peak Age: {params[0]:.1f}, "
f"Peak Value: {params[1]:.2f}, Decay: {params[2]:.3f}")
# Project aging for 29-year-old player with 5.0 WAR
projections = age_model.project_aging(
current_age=29,
current_performance=5.0,
future_years=5,
position='WR'
)
print("\nAge-Based Projections:")
for year, proj in enumerate(projections, start=1):
print(f" Year {year} (Age {29+year}): {proj:.2f} WAR")
# Injury risk analysis
injury_model = InjuryRiskModel()
# Player with 2 significant injuries
injury_history = [0.8, 0.6]
# Adjust projections for injury risk
adjusted = injury_model.calculate_injury_adjusted_war(
projections,
age=29,
injury_history=injury_history,
position='WR'
)
print("\nInjury-Adjusted Projections:")
for year, (base, adj) in enumerate(zip(projections, adjusted), start=1):
reduction = (1 - adj/base) * 100
print(f" Year {year}: {base:.2f} -> {adj:.2f} WAR "
f"({reduction:.1f}% reduction)")
```
**R Implementation:**
```r
library(tidyverse)
library(mgcv) # For GAM models
fit_age_curve <- function(player_data, position = NULL) {
#' Fit age curve using GAM (Generalized Additive Model)
#'
#' @param player_data DataFrame with age and performance columns
#' @param position Filter for specific position
if (!is.null(position)) {
player_data <- filter(player_data, position == !!position)
}
# Fit smooth curve
model <- gam(performance ~ s(age, k = 5), data = player_data)
model
}
project_aging <- function(current_age, current_performance, years,
age_curve_model) {
#' Project performance with aging
# Calculate talent level (above/below curve)
expected_current <- predict(age_curve_model,
newdata = data.frame(age = current_age))
talent_level <- current_performance - expected_current
# Project future ages
future_ages <- current_age + 1:years
expected_future <- predict(age_curve_model,
newdata = data.frame(age = future_ages))
# Add talent level back
projections <- expected_future + talent_level
projections <- pmax(projections, 0) # Floor at 0
tibble(
year = 1:years,
age = future_ages,
projected_performance = projections
)
}
# Injury risk adjustment
calculate_injury_risk <- function(age, injury_history_count, position) {
#' Calculate injury probability
# Base rates by position
base_rates <- c(
"RB" = 0.25, "WR" = 0.15, "QB" = 0.12,
"OL" = 0.20, "DL" = 0.22, "LB" = 0.18, "DB" = 0.14
)
base_rate <- base_rates[position]
if (is.na(base_rate)) base_rate <- 0.15
# Age multiplier
age_multiplier <- 1.0 + max(0, (age - 27) * 0.08)
# Injury history multiplier
history_multiplier <- ifelse(
injury_history_count > 0,
1.0 + injury_history_count * 0.15,
0.8 # Clean history bonus
)
injury_risk <- min(0.9, base_rate * age_multiplier * history_multiplier)
injury_risk
}
adjust_for_injury_risk <- function(projections_df, age, injury_history,
position, severity = 0.4) {
#' Adjust projections for injury risk
projections_df %>%
mutate(
current_age = age + year,
injury_risk = map_dbl(current_age, ~calculate_injury_risk(
.x, injury_history, position
)),
injury_adjusted = projected_performance * (
(1 - injury_risk) + injury_risk * (1 - severity)
)
)
}
# Example usage
player_history <- tibble(
age = 23:33,
performance = c(3.2, 4.1, 4.8, 5.5, 5.8, 5.7, 5.3, 4.7, 4.0, 3.2, 2.5)
)
age_model <- fit_age_curve(player_history)
# Project 29-year-old with 5.0 current performance
projections <- project_aging(29, 5.0, 5, age_model)
# Adjust for injury risk (2 significant injuries)
final_projections <- adjust_for_injury_risk(
projections,
age = 29,
injury_history = 2,
position = "WR",
severity = 0.4
)
print(final_projections)
```
## Contract Projections
### Multi-Year Contract Valuation
Projecting contract value requires combining performance projections, aging curves, injury risk, and market rates.
**Python Comprehensive Contract Projection:**
```python
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class PlayerProfile:
"""Complete player profile for contract projection"""
name: str
age: int
position: str
current_war: float
injury_history: List[float]
contract_years: int
class ContractProjector:
"""
Comprehensive contract valuation and projection system
"""
def __init__(self, age_model: AgeCurveModel,
injury_model: InjuryRiskModel,
dollars_per_win: float,
discount_rate: float = 0.05):
self.age_model = age_model
self.injury_model = injury_model
self.dollars_per_win = dollars_per_win
self.discount_rate = discount_rate
def project_performance(self, player: PlayerProfile) -> List[float]:
"""
Project player performance over contract length
Accounts for aging and injury risk
"""
# Age-based projections
age_projections = self.age_model.project_aging(
current_age=player.age,
current_performance=player.current_war,
future_years=player.contract_years,
position=player.position
)
# Adjust for injury risk
injury_adjusted = self.injury_model.calculate_injury_adjusted_war(
age_projections,
age=player.age,
injury_history=player.injury_history,
position=player.position
)
return injury_adjusted
def calculate_fair_value(self, player: PlayerProfile) -> Dict:
"""
Calculate fair market value for contract
"""
projections = self.project_performance(player)
annual_values = []
npv_values = []
for year, war in enumerate(projections, start=1):
# Market value for projected performance
annual_value = war * self.dollars_per_win
annual_values.append(annual_value)
# NPV calculation
discount_factor = 1 / (1 + self.discount_rate) ** (year - 1)
npv = annual_value * discount_factor
npv_values.append(npv)
return {
'player': player.name,
'age': player.age,
'position': player.position,
'contract_years': player.contract_years,
'war_projections': projections,
'annual_values': annual_values,
'total_value': sum(annual_values),
'npv_value': sum(npv_values),
'aav': np.mean(annual_values),
'year_1_value': annual_values[0] if annual_values else 0
}
def structure_contract(self, player: PlayerProfile,
total_budget: float = None) -> Dict:
"""
Create optimal contract structure
Front-loaded, back-loaded, or flat based on age curve
"""
fair_value = self.calculate_fair_value(player)
if total_budget is None:
total_budget = fair_value['total_value']
projections = fair_value['war_projections']
# Distribute budget proportional to projected value
total_war = sum(projections)
if total_war > 0:
annual_salaries = [
(war / total_war) * total_budget
for war in projections
]
else:
annual_salaries = [total_budget / len(projections)] * len(projections)
# Calculate signing bonus (typically 20-40% of total)
signing_bonus = total_budget * 0.30
# Adjust salaries for signing bonus
adjusted_salaries = [
salary * 0.7 for salary in annual_salaries
]
return {
'player': player.name,
'years': player.contract_years,
'total_value': total_budget,
'signing_bonus': signing_bonus,
'aav': total_budget / player.contract_years,
'annual_salaries': adjusted_salaries,
'guaranteed_at_signing': signing_bonus + adjusted_salaries[0],
'structure': self._classify_structure(projections)
}
def _classify_structure(self, projections: List[float]) -> str:
"""Classify contract structure"""
if len(projections) < 2:
return "flat"
first_half = np.mean(projections[:len(projections)//2])
second_half = np.mean(projections[len(projections)//2:])
if first_half > second_half * 1.2:
return "front-loaded"
elif second_half > first_half * 1.2:
return "back-loaded"
else:
return "flat"
def compare_contract_offers(self, player: PlayerProfile,
offers: List[Dict]) -> pd.DataFrame:
"""
Compare multiple contract offers against fair value
Parameters:
-----------
offers : List[Dict]
Each dict has 'team', 'years', 'total_value', 'guaranteed'
"""
fair_value = self.calculate_fair_value(player)
comparisons = []
for offer in offers:
aav = offer['total_value'] / offer['years']
# Calculate surplus for team
if offer['years'] <= len(fair_value['war_projections']):
relevant_projections = fair_value['war_projections'][:offer['years']]
relevant_values = [
war * self.dollars_per_win
for war in relevant_projections
]
team_surplus = sum(relevant_values) - offer['total_value']
else:
team_surplus = None
# Value score
value_score = (fair_value['total_value'] - offer['total_value']) / fair_value['total_value']
comparisons.append({
'team': offer['team'],
'years': offer['years'],
'total_value': offer['total_value'],
'aav': aav,
'guaranteed': offer['guaranteed'],
'fair_value': fair_value['total_value'],
'difference': offer['total_value'] - fair_value['total_value'],
'team_surplus': team_surplus,
'value_score': value_score,
'guaranteed_pct': offer['guaranteed'] / offer['total_value']
})
return pd.DataFrame(comparisons).sort_values('total_value', ascending=False)
# Example usage
def example_contract_projection():
# Setup models
age_model = AgeCurveModel()
injury_model = InjuryRiskModel()
# Assume we've fitted age curve previously
age_model.curve_params = [27.5, 6.0, 0.08] # peak at 27.5, value 6.0
projector = ContractProjector(
age_model=age_model,
injury_model=injury_model,
dollars_per_win=7000000,
discount_rate=0.05
)
# Define player
player = PlayerProfile(
name="Elite WR",
age=26,
position="WR",
current_war=5.5,
injury_history=[],
contract_years=5
)
# Calculate fair value
fair_value = projector.calculate_fair_value(player)
print(f"Player: {player.name}")
print(f"Age: {player.age}, Position: {player.position}")
print(f"\nProjected Performance:")
for year, war in enumerate(fair_value['war_projections'], start=1):
value = war * projector.dollars_per_win
print(f" Year {year}: {war:.2f} WAR = ${value:,.0f}")
print(f"\nFair Contract Value:")
print(f" Total: ${fair_value['total_value']:,.0f}")
print(f" NPV: ${fair_value['npv_value']:,.0f}")
print(f" AAV: ${fair_value['aav']:,.0f}")
# Structure contract
contract = projector.structure_contract(player)
print(f"\nRecommended Structure:")
print(f" {contract['years']} years, ${contract['total_value']:,.0f}")
print(f" Signing Bonus: ${contract['signing_bonus']:,.0f}")
print(f" Guaranteed at Signing: ${contract['guaranteed_at_signing']:,.0f}")
print(f" Structure Type: {contract['structure']}")
# Compare offers
offers = [
{'team': 'Team A', 'years': 5, 'total_value': 140000000, 'guaranteed': 90000000},
{'team': 'Team B', 'years': 4, 'total_value': 120000000, 'guaranteed': 100000000},
{'team': 'Team C', 'years': 6, 'total_value': 160000000, 'guaranteed': 85000000},
]
comparison = projector.compare_contract_offers(player, offers)
print("\nContract Offer Comparison:")
print(comparison.to_string())
```
**R Implementation:**
```r
library(tidyverse)
project_contract_value <- function(player_age, current_war, position,
years, injury_history_count,
dollars_per_win, age_model,
discount_rate = 0.05) {
#' Comprehensive contract value projection
# Performance projections with aging
age_projections <- project_aging(player_age, current_war, years, age_model)
# Adjust for injury risk
injury_adjusted <- age_projections %>%
mutate(
injury_history = injury_history_count,
position = position
) %>%
adjust_for_injury_risk(player_age, injury_history_count,
position, severity = 0.4)
# Calculate values
valuation <- injury_adjusted %>%
mutate(
market_value = injury_adjusted * dollars_per_win,
discount_factor = 1 / (1 + discount_rate) ^ (year - 1),
npv = market_value * discount_factor
)
list(
projections = valuation,
total_value = sum(valuation$market_value),
npv_value = sum(valuation$npv),
aav = mean(valuation$market_value)
)
}
structure_optimal_contract <- function(war_projections, total_budget) {
#' Create optimal contract structure based on projections
# Distribute proportionally to projected value
total_war <- sum(war_projections)
if (total_war > 0) {
salaries <- (war_projections / total_war) * total_budget
} else {
salaries <- rep(total_budget / length(war_projections),
length(war_projections))
}
# Signing bonus (30% of total)
signing_bonus <- total_budget * 0.30
# Adjust salaries
adjusted_salaries <- salaries * 0.7
tibble(
year = seq_along(war_projections),
projected_war = war_projections,
salary = adjusted_salaries,
signing_bonus = c(signing_bonus, rep(0, length(war_projections) - 1))
)
}
compare_contract_offers <- function(fair_value_result, offers_df) {
#' Compare multiple contract offers
#'
#' @param offers_df DataFrame with team, years, total_value, guaranteed
offers_df %>%
mutate(
aav = total_value / years,
fair_value = fair_value_result$total_value,
difference = total_value - fair_value,
value_score = difference / fair_value,
guaranteed_pct = guaranteed / total_value,
surplus = fair_value - total_value
) %>%
arrange(desc(total_value))
}
# Example usage
fair_value <- project_contract_value(
player_age = 26,
current_war = 5.5,
position = "WR",
years = 5,
injury_history_count = 0,
dollars_per_win = 7e6,
age_model = age_model,
discount_rate = 0.05
)
cat(sprintf("Fair Total Value: $%.0f million\n",
fair_value$total_value / 1e6))
cat(sprintf("NPV: $%.0f million\n", fair_value$npv_value / 1e6))
cat(sprintf("AAV: $%.0f million\n", fair_value$aav / 1e6))
# Structure contract
contract_structure <- structure_optimal_contract(
fair_value$projections$injury_adjusted,
fair_value$total_value
)
print(contract_structure)
# Compare offers
offers <- tibble(
team = c("Team A", "Team B", "Team C"),
years = c(5, 4, 6),
total_value = c(140e6, 120e6, 160e6),
guaranteed = c(90e6, 100e6, 85e6)
)
comparison <- compare_contract_offers(fair_value, offers)
print(comparison)
```
## Key Considerations
### Market Inefficiencies
1. **Positional Value**: Certain positions (QB, EDGE, OT) command premiums due to scarcity
2. **Age Premium**: Teams overpay for proven veterans, underpay for young talent
3. **Recent Performance Bias**: Recent seasons weighted too heavily
4. **Guaranteed Money**: True cost vs. stated value varies significantly
### Risk Management
1. **Injury Clauses**: Offset language, guaranteed money structure
2. **Performance Incentives**: Align compensation with production
3. **Team Options**: Provide flexibility for aging or injury concerns
4. **Signing Bonus vs. Salary**: Cap management implications
### Analytics Best Practices
1. **Update $/Win Annually**: Market rates change with salary cap
2. **Position-Specific Models**: Age curves differ by position
3. **Injury Database**: Track injury patterns and recovery rates
4. **Comparable Analysis**: Use recent similar contracts as benchmarks
5. **Monte Carlo Simulation**: Model uncertainty in projections
## References
- Woolner, K. (2002). "Understanding and Measuring Replacement Level"
- Tango, T. & Lichtman, M. (2013). "The Book: Playing the Percentages in Baseball"
- Schuckers, M. (2011). "An Alternative to Plus-Minus for Hockey"
- Yurko, R. et al. (2019). "Going Deep: Models for Continuous-Time Within-Play Valuation"
- Alamar, B. (2013). "Sports Analytics: A Guide for Coaches, Managers, and Other Decision Makers"
---
*Last Updated: 2024*
Discussion
Have questions or feedback? Join our community discussion on
Discord or
GitHub Discussions.
Table of Contents
Related Topics
Quick Actions