Contract Valuation Models

Beginner 10 min read 0 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.