Contract Valuation in Hockey

Beginner 10 min read 1 views Nov 27, 2025

Valuing Player Contracts

Determining fair contract value requires analyzing player performance, comparing to similar players, projecting future production, and accounting for market dynamics. Statistical models can help quantify a player's worth relative to salary cap percentage.

Contract Valuation Factors

  • WAR (Wins Above Replacement): Total value contribution
  • Age and Contract Length: Aging curve implications
  • Position Scarcity: Premium for rare skillsets
  • Market Comparables: Similar player contracts
  • Cap % vs Production: Value relative to cap hit

Python: Contract Value Analysis

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler

# Load player statistics and contracts
players = pd.read_csv('nhl_player_stats_contracts.csv')

# Calculate Wins Above Replacement (simplified WAR)
def calculate_war(stats):
    """Calculate simplified WAR from player stats"""

    # Goals Above Replacement
    replacement_goals_per_60 = 0.4
    gar = ((stats['goals_per_60'] - replacement_goals_per_60) *
           stats['toi'] / 60)

    # Assists Above Replacement
    replacement_assists_per_60 = 0.5
    aar = ((stats['assists_per_60'] - replacement_assists_per_60) *
           stats['toi'] / 60)

    # Defensive contribution (Corsi Rel, Expected Goals Against)
    defensive_value = stats['rel_corsi_pct'] * 0.1 + stats['xga_diff']

    # Combine components (simplified formula)
    war = (gar * 1.5 + aar + defensive_value) / 10

    return max(0, war)  # WAR can't be negative

# Calculate WAR for all players
players['war'] = players.apply(calculate_war, axis=1)

# Calculate $/WAR (contract efficiency)
SALARY_CAP = 83_500_000

players['cap_pct'] = (players['cap_hit'] / SALARY_CAP) * 100
players['dollars_per_war'] = players['cap_hit'] / players['war'].replace(0, np.nan)

# Market value analysis
def calculate_market_value(war, position, age):
    """Calculate expected market value for player"""

    # Base value per WAR
    base_value_per_war = 3_000_000

    # Position adjustments
    position_multipliers = {
        'C': 1.10,   # Centers command premium
        'D': 1.05,   # Top defensemen valuable
        'LW': 1.00,
        'RW': 1.00,
        'G': 1.15    # Elite goalies expensive
    }

    position_mult = position_multipliers.get(position, 1.0)

    # Age adjustments (peak value ages 24-28)
    if age < 24:
        age_mult = 0.85  # Potential but unproven
    elif age <= 28:
        age_mult = 1.10  # Prime years
    elif age <= 31:
        age_mult = 0.95  # Still good but declining
    else:
        age_mult = 0.75  # High risk of decline

    market_value = war * base_value_per_war * position_mult * age_mult

    return market_value

# Calculate expected market value
players['market_value'] = players.apply(
    lambda x: calculate_market_value(x['war'], x['position'], x['age']),
    axis=1
)

# Contract surplus value (actual cap hit vs market value)
players['surplus_value'] = players['market_value'] - players['cap_hit']
players['value_rating'] = np.where(
    players['surplus_value'] > 0, 'Good Value', 'Overpaid'
)

# Best value contracts
best_values = players.nlargest(10, 'surplus_value')
print("=== Best Value Contracts ===")
print(best_values[['player_name', 'position', 'age', 'war',
                   'cap_hit', 'market_value', 'surplus_value']])

# Worst value contracts (overpaid)
worst_values = players.nsmallest(10, 'surplus_value')
print("\n=== Most Overpaid Contracts ===")
print(worst_values[['player_name', 'position', 'age', 'war',
                    'cap_hit', 'market_value', 'surplus_value']])

# Regression model: Predict fair contract from performance
def build_contract_model(players_df):
    """Build regression model to predict fair contract value"""

    # Features for prediction
    features = ['war', 'age', 'goals_per_60', 'assists_per_60',
                'rel_corsi_pct', 'toi']

    # Prepare data
    X = players_df[features].fillna(0)
    y = players_df['cap_hit']

    # Standardize features
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)

    # Train model
    model = LinearRegression()
    model.fit(X_scaled, y)

    # Predictions
    players_df['predicted_value'] = model.predict(X_scaled)
    players_df['value_diff'] = players_df['cap_hit'] - players_df['predicted_value']

    return model, scaler

model, scaler = build_contract_model(players)

# Identify most over/under valued
print("\n=== Statistical Over/Under Valued Players ===")
print("Most Overpaid (by model):")
print(players.nlargest(5, 'value_diff')[
    ['player_name', 'cap_hit', 'predicted_value', 'value_diff']
])

print("\nMost Underpaid (by model):")
print(players.nsmallest(5, 'value_diff')[
    ['player_name', 'cap_hit', 'predicted_value', 'value_diff']
])

# Contract extension scenario
def recommend_extension(player_stats, years=5):
    """Recommend contract extension terms"""

    current_war = player_stats['war']
    age = player_stats['age']
    position = player_stats['position']

    # Project WAR over contract length (with aging)
    projected_wars = []
    for year in range(years):
        future_age = age + year + 1

        # Aging factor
        if future_age <= 27:
            aging_factor = 1.05
        elif future_age <= 30:
            aging_factor = 1.00
        else:
            aging_factor = 0.93

        projected_war = current_war * (aging_factor ** (year + 1))
        projected_wars.append(projected_war)

    # Calculate total value
    avg_war = np.mean(projected_wars)
    total_value = sum([
        calculate_market_value(w, position, age + i + 1)
        for i, w in enumerate(projected_wars)
    ])

    aav = total_value / years

    return {
        'recommended_aav': aav,
        'total_value': total_value,
        'projected_wars': projected_wars,
        'avg_war': avg_war
    }

# Example: Recommend extension for top performer
top_player = players.nlargest(1, 'war').iloc[0]
extension = recommend_extension(top_player, years=6)

print(f"\n=== Extension Recommendation: {top_player['player_name']} ===")
print(f"6-year contract recommendation:")
print(f"Total Value: ${extension['total_value']:,.0f}")
print(f"AAV: ${extension['recommended_aav']:,.0f}")
print(f"Avg Projected WAR: {extension['avg_war']:.2f}")
print(f"Projected WAR by year: {[f'{w:.2f}' for w in extension['projected_wars']]}")

R: Contract Efficiency Analysis

library(tidyverse)
library(scales)

# Load player data
players <- read_csv("nhl_player_stats_contracts.csv")

SALARY_CAP <- 83500000

# Calculate WAR
calculate_war <- function(data) {
  data %>%
    mutate(
      gar = ((goals_per_60 - 0.4) * toi / 60),
      aar = ((assists_per_60 - 0.5) * toi / 60),
      defensive_value = rel_corsi_pct * 0.1 + xga_diff,
      war = pmax(0, (gar * 1.5 + aar + defensive_value) / 10)
    )
}

players <- calculate_war(players)

# Calculate contract efficiency
players <- players %>%
  mutate(
    cap_pct = (cap_hit / SALARY_CAP) * 100,
    dollars_per_war = cap_hit / ifelse(war == 0, NA, war)
  )

# Market value calculation
calculate_market_value <- function(war, position, age) {
  base_value_per_war <- 3000000

  position_mult <- case_when(
    position == "C" ~ 1.10,
    position == "D" ~ 1.05,
    position == "G" ~ 1.15,
    TRUE ~ 1.00
  )

  age_mult <- case_when(
    age < 24 ~ 0.85,
    age <= 28 ~ 1.10,
    age <= 31 ~ 0.95,
    TRUE ~ 0.75
  )

  war * base_value_per_war * position_mult * age_mult
}

players <- players %>%
  rowwise() %>%
  mutate(
    market_value = calculate_market_value(war, position, age),
    surplus_value = market_value - cap_hit,
    value_rating = ifelse(surplus_value > 0, "Good Value", "Overpaid")
  ) %>%
  ungroup()

# Best value contracts
cat("=== Best Value Contracts ===\n")
best_values <- players %>%
  arrange(desc(surplus_value)) %>%
  head(10) %>%
  select(player_name, position, age, war, cap_hit, market_value, surplus_value)
print(best_values)

# Worst value contracts
cat("\n=== Most Overpaid Contracts ===\n")
worst_values <- players %>%
  arrange(surplus_value) %>%
  head(10) %>%
  select(player_name, position, age, war, cap_hit, market_value, surplus_value)
print(worst_values)

# Visualize contract efficiency
ggplot(players %>% filter(war > 0),
       aes(x = war, y = cap_hit, color = position)) +
  geom_point(size = 3, alpha = 0.6) +
  geom_smooth(method = "lm", se = FALSE, color = "black", linetype = "dashed") +
  scale_y_continuous(labels = dollar_format(scale = 1e-6, suffix = "M")) +
  labs(title = "Contract Value vs Performance",
       subtitle = "Cap Hit vs WAR by Position",
       x = "Wins Above Replacement (WAR)",
       y = "Cap Hit",
       color = "Position") +
  theme_minimal()

# Dollars per WAR by position
dollars_per_war_summary <- players %>%
  filter(!is.na(dollars_per_war), war > 0) %>%
  group_by(position) %>%
  summarise(
    avg_dollars_per_war = mean(dollars_per_war),
    median_dollars_per_war = median(dollars_per_war),
    players = n()
  ) %>%
  arrange(desc(avg_dollars_per_war))

cat("\n=== Average $/WAR by Position ===\n")
print(dollars_per_war_summary)

# Contract recommendation function
recommend_extension <- function(current_war, age, position, years = 5) {
  projected_wars <- numeric(years)

  for (i in 1:years) {
    future_age <- age + i

    aging_factor <- case_when(
      future_age <= 27 ~ 1.05,
      future_age <= 30 ~ 1.00,
      TRUE ~ 0.93
    )

    projected_wars[i] <- current_war * (aging_factor ^ i)
  }

  total_value <- sum(sapply(1:years, function(i) {
    calculate_market_value(projected_wars[i], position, age + i)
  }))

  list(
    recommended_aav = total_value / years,
    total_value = total_value,
    projected_wars = projected_wars,
    avg_war = mean(projected_wars)
  )
}

# Example extension recommendation
top_player <- players %>%
  arrange(desc(war)) %>%
  slice(1)

extension <- recommend_extension(
  top_player$war,
  top_player$age,
  top_player$position,
  years = 6
)

cat(sprintf("\n=== Extension Recommendation: %s ===\n", top_player$player_name))
cat(sprintf("Total Value: $%s\n", format(extension$total_value, big.mark = ",")))
cat(sprintf("AAV: $%s\n", format(extension$recommended_aav, big.mark = ",")))
cat(sprintf("Avg Projected WAR: %.2f\n", extension$avg_war))

Comparable Player Analysis

Identifying comparable players (similar age, position, production level) provides market benchmarks for contract negotiations. Statistical clustering can identify the most similar players and their contract terms.

Contract Valuation Best Practices

  • Use multiple valuation methods (WAR-based, comparables, projections)
  • Account for aging curves when valuing multi-year deals
  • Consider positional scarcity and market timing
  • Balance current performance with projected future value

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.