NBA Salary Cap Explained
NBA Salary Cap Basics
Introduction to the NBA Salary Cap
The NBA Salary Cap is a complex system designed to promote competitive balance and financial stability across the league. Unlike "hard cap" systems in other sports, the NBA employs a soft cap with numerous exceptions that allow teams to exceed the cap under specific circumstances.
Understanding the salary cap is crucial for analyzing team construction, player value, and front office decision-making. The cap affects everything from free agency to trades to the draft, making it one of the most important factors in modern NBA analytics.
2023-24 Salary Cap Numbers
- Salary Cap: $136,021,000
- Luxury Tax Threshold: $165,294,000
- First Apron: $172,346,000
- Second Apron: $182,794,000
- Minimum Team Salary: $122,418,900 (90% of cap)
Soft Cap vs Hard Cap Systems
NBA Soft Cap System
The NBA uses a soft cap, meaning teams can exceed the salary cap limit using various exceptions. This creates flexibility for teams to retain their own players and improve their rosters even when over the cap.
Soft Cap Characteristics:
- Teams can exceed the cap using exceptions (Bird Rights, MLE, etc.)
- No absolute maximum team payroll (though luxury tax creates disincentive)
- Favors player retention and allows competitive teams to reload
- More complex rules requiring strategic cap management
- Creates wide disparity in team payrolls ($150M+ difference possible)
Hard Cap Triggers (New CBA 2023)
While the NBA has a soft cap, certain transactions trigger a hard cap that teams cannot exceed for that season:
- Using Non-Taxpayer Mid-Level Exception: Hard cap at first apron ($172.3M)
- Using Bi-Annual Exception: Hard cap at first apron
- Sign-and-Trade Reception: Hard cap at first apron
Comparison: NFL vs MLB vs NBA
| League | Cap Type | Key Features |
|---|---|---|
| NFL | Hard Cap | Strict limit, no exceptions, heavy roster turnover |
| MLB | Luxury Tax (No Cap) | No salary cap, only competitive balance tax, huge payroll disparities |
| NBA | Soft Cap | Cap with exceptions, luxury tax, apron restrictions |
| NHL | Hard Cap | Strict limit with limited exceptions |
Luxury Tax System
Overview
The luxury tax (officially the "competitive balance tax") is a penalty charged to teams whose total payroll exceeds the luxury tax threshold. It's designed to discourage excessive spending and redistribute wealth to smaller-market teams.
Tax Calculation (Progressive Rates)
The luxury tax is calculated on a progressive scale based on how far over the threshold a team goes:
| Amount Over Tax Line | Tax Rate (Non-Repeater) | Tax Rate (Repeater) |
|---|---|---|
| $0 - $5M | $1.50 per $1 | $2.50 per $1 |
| $5M - $10M | $1.75 per $1 | $2.75 per $1 |
| $10M - $15M | $2.50 per $1 | $3.50 per $1 |
| $15M - $20M | $3.25 per $1 | $4.25 per $1 |
| $20M+ | $3.75 per $1 (incremental increases) | $4.75 per $1 (incremental increases) |
Repeater Tax Definition
A team is a repeater if they paid luxury tax in at least 3 of the 4 previous seasons. Repeater penalties are significantly higher.
Tax Example Calculation
Tax Threshold: $165.3M
Overage: $14.7M
Repeater Status: Non-Repeater
Tax Calculation:
- First $5M over: $5M × $1.50 = $7.5M
- Next $5M over: $5M × $1.75 = $8.75M
- Next $4.7M over: $4.7M × $2.50 = $11.75M
- Total Tax Bill: $28M
- Total Cost: $208M (payroll + tax)
First and Second Aprons (2023 CBA)
The new CBA introduced two additional thresholds above the luxury tax line that trigger increasingly severe team-building restrictions:
First Apron Restrictions ($172.3M in 2023-24)
- Cannot use the Taxpayer Mid-Level Exception (limited to smaller BAE)
- Cannot acquire players via sign-and-trade
- Cannot use cash in trades to acquire players
- More restrictive salary matching rules in trades
Second Apron Restrictions ($182.8M in 2023-24)
- Cannot trade multiple players for one player
- Cannot aggregate salaries in trades
- Future first-round pick frozen (cannot trade 7 years out)
- Stricter trade matching (110% instead of 125%)
- Draft pick penalty: If team is over second apron in 2+ of 5 seasons, their highest-earning pick in next draft moves to end of first round
Salary Cap Exceptions
Exceptions allow teams to sign players even when over the salary cap. Each exception has specific rules, limitations, and strategic implications.
1. Larry Bird Exception
The Bird Exception (named after Larry Bird) allows teams to re-sign their own free agents without cap restrictions, even if over the cap.
Requirements (Full Bird Rights):
- Player must be with team for 3+ consecutive seasons (via any means)
- Player not waived and re-signed in the same or previous season
- Team retains Bird Rights even if player changes teams via trade
Contract Limitations:
- Salary Start: Up to player's maximum salary (based on years of service)
- Length: Maximum 5 years
- Raises: Up to 8% annually
Variations:
- Early Bird: 2 years with team, max 175% of previous salary or average salary (whichever higher), 4 years max, 5% raises
- Non-Bird: 1+ year with team, max 120% of previous salary or minimum salary (whichever higher), 4 years max, 5% raises
2. Mid-Level Exception (MLE)
The MLE allows teams to sign free agents at a set amount regardless of cap space. There are three types based on team cap situation:
| MLE Type | 2023-24 Amount | Max Length | Availability |
|---|---|---|---|
| Non-Taxpayer MLE | $12,405,000 | 4 years | Under luxury tax threshold; triggers hard cap at first apron |
| Taxpayer MLE | $5,000,000 | 3 years | Over luxury tax, under first apron |
| Room MLE | $7,700,000 | 2 years | Used cap space, now under the cap |
Strategic Considerations:
- Non-Taxpayer MLE triggers hard cap: must plan entire roster around it
- Can split MLE among multiple players (total cannot exceed MLE amount)
- Using MLE wisely can be difference between contender and pretender
- Taxpayer MLE significantly less valuable ($5M vs $12.4M)
3. Bi-Annual Exception (BAE)
Details:
- Amount (2023-24): $4,500,000
- Length: Maximum 2 years
- Availability: Can only be used every other year
- Restriction: Not available if over first apron
- Hard Cap: Triggers hard cap at first apron
4. Rookie Exception
Allows teams to sign their drafted rookies or first-round picks acquired via trade, even if over the cap.
- Based on pick number (slotted scale)
- First-round picks: Guaranteed scale contracts (120% to 80% of slot)
- Can sign to max 4 years (2 guaranteed, 2 team options)
5. Minimum Salary Exception
Teams can sign unlimited players to minimum contracts regardless of cap situation.
- Based on years of NBA service
- Only counts against cap at minimum amount (even if player has higher minimum due to service time)
- Crucial for filling out rosters for capped-out teams
6. Disabled Player Exception (DPE)
Criteria:
- Player suffers season-ending or career-ending injury
- Team physician and NBA-designated doctor must certify injury
- Amount: 50% of injured player's salary OR non-taxpayer MLE (whichever less)
- Usage: Sign a replacement player or acquire via trade
- Length: One year only
Maximum Contracts
Max Salary Calculation
Maximum salaries are based on a player's years of NBA service and are calculated as a percentage of the salary cap:
| Years of Service | % of Salary Cap | 2023-24 Max Salary |
|---|---|---|
| 0-6 years | 25% | $34,005,250 |
| 7-9 years | 30% | $40,806,300 |
| 10+ years | 35% | $47,607,350 |
Designated Player Extensions
Teams can offer "supermax" contracts (Designated Veteran Player Extension) to select players:
Criteria for Supermax (35% max):
- Player won MVP in previous 3 seasons, OR
- Player made All-NBA team in previous season or 2 of last 3 seasons, OR
- Player won Defensive Player of the Year in previous season or 2 of last 3 seasons
- Player must be with team that drafted them (or traded for on rookie contract)
Contract Terms:
- Starting Salary: 35% of cap (instead of 30%)
- Length: Up to 5 years
- Raises: 8% annually
- Total Value: Can exceed $250M+ for elite players
Max Contract Examples (2023-24)
| Player | Type | Total Value | Years | Avg/Year |
|---|---|---|---|---|
| Jaylen Brown | Supermax (35%) | $304M | 5 years | $60.8M |
| Nikola Jokić | Supermax (35%) | $276M | 5 years | $55.2M |
| Damian Lillard | Veteran Max (30%) | $216M | 4 years | $54M |
| Anthony Edwards | Rookie Extension (25%) | $207M | 5 years | $41.4M |
Minimum Salary Scale
NBA Minimum Salaries by Years of Service
Minimum salaries increase based on years of NBA experience:
| Years of Service | 2023-24 Minimum | 2024-25 Minimum |
|---|---|---|
| 0 | $1,119,563 | $1,157,153 |
| 1 | $1,836,090 | $1,902,133 |
| 2 | $1,988,965 | $2,087,519 |
| 3 | $2,087,519 | $2,162,606 |
| 4 | $2,162,606 | $2,237,691 |
| 5 | $2,346,614 | $2,425,403 |
| 6 | $2,530,680 | $2,613,120 |
| 7 | $2,714,746 | $2,800,837 |
| 8 | $2,898,812 | $2,988,554 |
| 9 | $2,953,694 | $3,045,234 |
| 10+ | $3,196,448 | $3,303,771 |
Veteran Minimum Cap Hit
Important Rule: When a team signs a veteran to a minimum salary contract, only the 2-year minimum salary ($1,988,965 in 2023-24) counts against the cap, regardless of the player's actual minimum.
This allows teams to sign veteran players without being penalized for their years of service, encouraging teams to add experienced players. The league reimburses the difference.
Cap Hit: $1,988,965 (2-year minimum)
Savings: $1,207,483
Cap Holds
What Are Cap Holds?
Cap holds are placeholder charges against a team's salary cap for players whose rights the team still holds but who are not currently under contract (free agents, unsigned draft picks, etc.).
Types of Cap Holds
1. Free Agent Cap Holds
When a player's contract expires, a cap hold remains on the team's books until the player is renounced or re-signed:
| Bird Rights Status | Cap Hold Amount |
|---|---|
| Full Bird Rights | 190% of previous salary (or maximum salary if less) |
| Early Bird Rights | 130% of previous salary (or maximum salary if less) |
| Non-Bird Rights | 120% of previous salary (or minimum salary if greater) |
| Minimum Salary Player | Minimum salary for player's years of service |
2. Unsigned First-Round Pick Cap Holds
- Equal to 120% of rookie scale amount for that pick
- Remains until player is signed or rights are renounced
- Counts even if pick is traded (until signed by new team)
3. Offer Sheet Cap Holds
- When restricted free agent receives offer sheet
- Hold equals average salary of offer sheet
- Lasts until team matches or declines (3 days)
Strategic Use of Cap Holds
Retaining Cap Holds:
- Preserve Bird Rights: Allows re-signing player over the cap later
- Negotiating leverage: Keep options open during free agency
- Timing flexibility: Sign other free agents first, then use Bird Rights
Renouncing Cap Holds:
- Create cap space: Necessary to clear room for max free agents
- Lose Bird Rights: Cannot re-sign player over cap (only with cap space or exception)
- Irreversible: Once renounced, Bird Rights cannot be recovered
Cap Hold Examples
Scenario 1: Keeping Cap Hold
Player: Starting guard, previous salary $15M
Cap Hold: $28.5M (190% for Full Bird)
Strategy: Team uses cap space on other players, then re-signs guard for
$20M using Bird Rights
Scenario 2: Renouncing Cap Hold
Player: Backup center, previous salary $8M
Cap Hold: $15.2M (190% for Full Bird)
Strategy: Team renounces to create max cap space, lets player walk
Result: Gain $15.2M in cap space but lose Bird Rights
Team Salary Calculation with Cap Holds
Total Team Salary for Cap Purposes:
Total Cap Number =
Active Player Salaries
+ Free Agent Cap Holds
+ Unsigned Draft Pick Cap Holds
+ Trade Bonuses
+ Offer Sheet Cap Holds
- Trade Exceptions (don't count against cap)
Salary Cap Analysis Code Examples
Python Example: Salary Cap Analysis
"""
NBA Salary Cap Analysis Tools
Analyze team cap situations, calculate tax bills, model cap scenarios
Install: pip install pandas numpy matplotlib requests beautifulsoup4
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from typing import Dict, List, Tuple
# 2023-24 Salary Cap Constants
SALARY_CAP = 136_021_000
LUXURY_TAX = 165_294_000
FIRST_APRON = 172_346_000
SECOND_APRON = 182_794_000
MIN_TEAM_SALARY = 122_418_900
# Tax Rates (Non-Repeater)
TAX_RATES = [
(5_000_000, 1.50),
(10_000_000, 1.75),
(15_000_000, 2.50),
(20_000_000, 3.25),
(float('inf'), 3.75)
]
# Repeater Tax Rates
REPEATER_TAX_RATES = [
(5_000_000, 2.50),
(10_000_000, 2.75),
(15_000_000, 3.50),
(20_000_000, 4.25),
(float('inf'), 4.75)
]
# Maximum Salaries by Years of Service
MAX_SALARIES = {
'0-6': 0.25 * SALARY_CAP,
'7-9': 0.30 * SALARY_CAP,
'10+': 0.35 * SALARY_CAP
}
# Minimum Salaries by Years of Service (2023-24)
MIN_SALARIES = {
0: 1_119_563,
1: 1_836_090,
2: 1_988_965,
3: 2_087_519,
4: 2_162_606,
5: 2_346_614,
6: 2_530_680,
7: 2_714_746,
8: 2_898_812,
9: 2_953_694,
10: 3_196_448
}
def calculate_luxury_tax(team_salary: float, is_repeater: bool = False) -> Dict:
"""
Calculate luxury tax bill for a team
Args:
team_salary: Total team salary
is_repeater: Whether team is repeater tax offender
Returns:
Dictionary with tax details
"""
if team_salary <= LUXURY_TAX:
return {
'overage': 0,
'tax_bill': 0,
'total_cost': team_salary,
'effective_rate': 0
}
overage = team_salary - LUXURY_TAX
tax_bill = 0
rates = REPEATER_TAX_RATES if is_repeater else TAX_RATES
previous_threshold = 0
for threshold, rate in rates:
if overage <= previous_threshold:
break
taxable_amount = min(overage, threshold) - previous_threshold
tax_bill += taxable_amount * rate
previous_threshold = threshold
total_cost = team_salary + tax_bill
effective_rate = tax_bill / overage if overage > 0 else 0
return {
'overage': overage,
'tax_bill': tax_bill,
'total_cost': total_cost,
'effective_rate': effective_rate,
'repeater': is_repeater
}
def calculate_max_salary(years_of_service: int, supermax: bool = False) -> float:
"""
Calculate maximum salary for a player
Args:
years_of_service: Player's years in NBA
supermax: Whether player qualifies for supermax (35%)
Returns:
Maximum salary amount
"""
if supermax:
return 0.35 * SALARY_CAP
if years_of_service <= 6:
return MAX_SALARIES['0-6']
elif years_of_service <= 9:
return MAX_SALARIES['7-9']
else:
return MAX_SALARIES['10+']
def calculate_cap_hold(previous_salary: float, bird_rights: str = 'full') -> float:
"""
Calculate cap hold for free agent
Args:
previous_salary: Player's previous season salary
bird_rights: 'full', 'early', or 'non'
Returns:
Cap hold amount
"""
if bird_rights == 'full':
percentage = 1.90
elif bird_rights == 'early':
percentage = 1.30
else: # non-bird
percentage = 1.20
cap_hold = previous_salary * percentage
# Cap hold cannot exceed maximum salary
max_salary = calculate_max_salary(10) # Use highest max as ceiling
return min(cap_hold, max_salary)
def project_contract_value(
starting_salary: float,
years: int,
annual_raise: float = 0.08
) -> pd.DataFrame:
"""
Project multi-year contract values
Args:
starting_salary: First year salary
years: Contract length
annual_raise: Annual raise percentage (default 8% for Bird)
Returns:
DataFrame with year-by-year breakdown
"""
contract = []
for year in range(1, years + 1):
salary = starting_salary * (1 + annual_raise) ** (year - 1)
contract.append({
'Year': year,
'Salary': salary,
'Cumulative': sum([starting_salary * (1 + annual_raise) ** i
for i in range(year)])
})
df = pd.DataFrame(contract)
df['Total_Value'] = df['Cumulative'].iloc[-1]
return df
def analyze_team_cap_situation(roster_salaries: List[float]) -> Dict:
"""
Analyze a team's cap situation
Args:
roster_salaries: List of player salaries
Returns:
Dictionary with cap analysis
"""
total_salary = sum(roster_salaries)
cap_space = max(0, SALARY_CAP - total_salary)
room_exception = 7_700_000 if cap_space > 0 else 0
# Determine available exceptions
if total_salary < LUXURY_TAX:
mle = 12_405_000 # Non-taxpayer MLE
bae = 4_500_000
elif total_salary < FIRST_APRON:
mle = 5_000_000 # Taxpayer MLE
bae = 0
else:
mle = 5_000_000
bae = 0
tax_info = calculate_luxury_tax(total_salary)
return {
'total_salary': total_salary,
'cap_space': cap_space,
'under_cap': total_salary < SALARY_CAP,
'over_tax': total_salary > LUXURY_TAX,
'over_first_apron': total_salary > FIRST_APRON,
'over_second_apron': total_salary > SECOND_APRON,
'available_mle': mle,
'available_bae': bae,
'room_exception': room_exception,
'tax_bill': tax_info['tax_bill'],
'total_cost': tax_info['total_cost']
}
def visualize_luxury_tax_brackets():
"""
Create visualization of luxury tax brackets
"""
overages = np.linspace(0, 50_000_000, 1000)
tax_bills_regular = []
tax_bills_repeater = []
for overage in overages:
salary = LUXURY_TAX + overage
regular = calculate_luxury_tax(salary, is_repeater=False)
repeater = calculate_luxury_tax(salary, is_repeater=True)
tax_bills_regular.append(regular['tax_bill'])
tax_bills_repeater.append(repeater['tax_bill'])
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# Tax bill visualization
ax1.plot(overages / 1e6, np.array(tax_bills_regular) / 1e6,
label='Non-Repeater', linewidth=2, color='#1f77b4')
ax1.plot(overages / 1e6, np.array(tax_bills_repeater) / 1e6,
label='Repeater', linewidth=2, color='#ff7f0e')
ax1.set_xlabel('Overage (Millions)', fontsize=12)
ax1.set_ylabel('Tax Bill (Millions)', fontsize=12)
ax1.set_title('Luxury Tax Bill by Overage Amount', fontsize=14, fontweight='bold')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Total cost visualization
total_cost_regular = (LUXURY_TAX + overages + np.array(tax_bills_regular)) / 1e6
total_cost_repeater = (LUXURY_TAX + overages + np.array(tax_bills_repeater)) / 1e6
ax2.plot(overages / 1e6, total_cost_regular,
label='Non-Repeater', linewidth=2, color='#1f77b4')
ax2.plot(overages / 1e6, total_cost_repeater,
label='Repeater', linewidth=2, color='#ff7f0e')
ax2.axhline(y=LUXURY_TAX / 1e6, color='red', linestyle='--',
linewidth=1, label='Tax Threshold')
ax2.set_xlabel('Overage (Millions)', fontsize=12)
ax2.set_ylabel('Total Cost (Millions)', fontsize=12)
ax2.set_title('Total Team Cost (Salary + Tax)', fontsize=14, fontweight='bold')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('luxury_tax_analysis.png', dpi=300, bbox_inches='tight')
plt.show()
def compare_signing_scenarios(
player_salary: float,
current_team_salary: float,
is_repeater: bool = False
) -> pd.DataFrame:
"""
Compare cost of signing player under different tax scenarios
Args:
player_salary: Proposed player salary
current_team_salary: Current team salary before signing
is_repeater: Repeater tax status
Returns:
DataFrame comparing scenarios
"""
scenarios = []
# Scenario 1: Don't sign player
baseline = calculate_luxury_tax(current_team_salary, is_repeater)
scenarios.append({
'Scenario': 'No Signing',
'Team_Salary': current_team_salary,
'Tax_Bill': baseline['tax_bill'],
'Total_Cost': baseline['total_cost'],
'Marginal_Cost': 0
})
# Scenario 2: Sign player
new_salary = current_team_salary + player_salary
with_player = calculate_luxury_tax(new_salary, is_repeater)
marginal_cost = with_player['total_cost'] - baseline['total_cost']
scenarios.append({
'Scenario': 'Sign Player',
'Team_Salary': new_salary,
'Tax_Bill': with_player['tax_bill'],
'Total_Cost': with_player['total_cost'],
'Marginal_Cost': marginal_cost
})
df = pd.DataFrame(scenarios)
df['Tax_Multiplier'] = df['Marginal_Cost'] / player_salary
return df
# Example usage
if __name__ == "__main__":
print("=== NBA Salary Cap Analysis ===\n")
# Example 1: Calculate luxury tax
print("Example 1: Luxury Tax Calculation")
team_salary = 180_000_000
tax_info = calculate_luxury_tax(team_salary, is_repeater=False)
print(f"Team Salary: ${team_salary:,.0f}")
print(f"Overage: ${tax_info['overage']:,.0f}")
print(f"Tax Bill: ${tax_info['tax_bill']:,.0f}")
print(f"Total Cost: ${tax_info['total_cost']:,.0f}")
print(f"Effective Tax Rate: {tax_info['effective_rate']:.2f}x\n")
# Example 2: Max contract projection
print("Example 2: Supermax Contract Projection")
supermax = project_contract_value(
starting_salary=0.35 * SALARY_CAP,
years=5,
annual_raise=0.08
)
print(supermax)
print(f"\nTotal Contract Value: ${supermax['Total_Value'].iloc[0]:,.0f}\n")
# Example 3: Team cap situation
print("Example 3: Team Cap Situation Analysis")
roster = [45_000_000, 35_000_000, 25_000_000, 18_000_000, 12_000_000,
8_000_000, 5_000_000, 3_000_000, 2_500_000, 2_000_000,
1_800_000, 1_500_000]
cap_situation = analyze_team_cap_situation(roster)
print(f"Total Salary: ${cap_situation['total_salary']:,.0f}")
print(f"Cap Space: ${cap_situation['cap_space']:,.0f}")
print(f"Over Tax Line: {cap_situation['over_tax']}")
print(f"Available MLE: ${cap_situation['available_mle']:,.0f}")
print(f"Tax Bill: ${cap_situation['tax_bill']:,.0f}\n")
# Example 4: Signing scenario comparison
print("Example 4: Marginal Cost of Signing Player")
comparison = compare_signing_scenarios(
player_salary=15_000_000,
current_team_salary=170_000_000,
is_repeater=False
)
print(comparison)
# Create visualizations
print("\nGenerating luxury tax visualization...")
visualize_luxury_tax_brackets()
print("\nAnalysis complete!")
R Example: Salary Cap Analysis
# NBA Salary Cap Analysis in R
# Comprehensive tools for cap calculations and team planning
# Install: install.packages(c("tidyverse", "scales", "ggplot2"))
library(tidyverse)
library(scales)
library(ggplot2)
# 2023-24 Salary Cap Constants
SALARY_CAP <- 136021000
LUXURY_TAX <- 165294000
FIRST_APRON <- 172346000
SECOND_APRON <- 182794000
MIN_TEAM_SALARY <- 122418900
# Maximum Salaries
MAX_SALARIES <- list(
"0-6" = 0.25 * SALARY_CAP,
"7-9" = 0.30 * SALARY_CAP,
"10+" = 0.35 * SALARY_CAP
)
#' Calculate luxury tax bill
#'
#' @param team_salary Total team salary
#' @param is_repeater Whether team is repeater offender
#' @return List with tax details
calculate_luxury_tax <- function(team_salary, is_repeater = FALSE) {
if (team_salary <= LUXURY_TAX) {
return(list(
overage = 0,
tax_bill = 0,
total_cost = team_salary,
effective_rate = 0
))
}
overage <- team_salary - LUXURY_TAX
# Define tax brackets
if (is_repeater) {
brackets <- tibble(
threshold = c(5e6, 10e6, 15e6, 20e6, Inf),
rate = c(2.50, 2.75, 3.50, 4.25, 4.75)
)
} else {
brackets <- tibble(
threshold = c(5e6, 10e6, 15e6, 20e6, Inf),
rate = c(1.50, 1.75, 2.50, 3.25, 3.75)
)
}
# Calculate progressive tax
tax_bill <- 0
previous_threshold <- 0
for (i in 1:nrow(brackets)) {
if (overage <= previous_threshold) break
taxable_amount <- min(overage, brackets$threshold[i]) - previous_threshold
tax_bill <- tax_bill + (taxable_amount * brackets$rate[i])
previous_threshold <- brackets$threshold[i]
}
total_cost <- team_salary + tax_bill
effective_rate <- if (overage > 0) tax_bill / overage else 0
return(list(
overage = overage,
tax_bill = tax_bill,
total_cost = total_cost,
effective_rate = effective_rate,
repeater = is_repeater
))
}
#' Calculate maximum salary
#'
#' @param years_of_service Player's NBA years
#' @param supermax Whether player qualifies for supermax
#' @return Maximum salary amount
calculate_max_salary <- function(years_of_service, supermax = FALSE) {
if (supermax) {
return(0.35 * SALARY_CAP)
}
if (years_of_service <= 6) {
return(MAX_SALARIES[["0-6"]])
} else if (years_of_service <= 9) {
return(MAX_SALARIES[["7-9"]])
} else {
return(MAX_SALARIES[["10+"]])
}
}
#' Project contract value over multiple years
#'
#' @param starting_salary First year salary
#' @param years Contract length
#' @param annual_raise Annual raise percentage
#' @return Tibble with year-by-year breakdown
project_contract <- function(starting_salary, years, annual_raise = 0.08) {
tibble(
Year = 1:years,
Salary = starting_salary * (1 + annual_raise)^(Year - 1)
) %>%
mutate(
Cumulative = cumsum(Salary),
Total_Value = last(Cumulative)
)
}
#' Analyze team cap situation
#'
#' @param roster_salaries Vector of player salaries
#' @return List with cap analysis
analyze_team_cap <- function(roster_salaries) {
total_salary <- sum(roster_salaries)
cap_space <- max(0, SALARY_CAP - total_salary)
# Determine available exceptions
if (total_salary < LUXURY_TAX) {
mle <- 12405000 # Non-taxpayer MLE
bae <- 4500000
} else if (total_salary < FIRST_APRON) {
mle <- 5000000 # Taxpayer MLE
bae <- 0
} else {
mle <- 5000000
bae <- 0
}
tax_info <- calculate_luxury_tax(total_salary)
list(
total_salary = total_salary,
cap_space = cap_space,
under_cap = total_salary < SALARY_CAP,
over_tax = total_salary > LUXURY_TAX,
over_first_apron = total_salary > FIRST_APRON,
over_second_apron = total_salary > SECOND_APRON,
available_mle = mle,
available_bae = bae,
tax_bill = tax_info$tax_bill,
total_cost = tax_info$total_cost
)
}
#' Visualize luxury tax structure
visualize_tax_brackets <- function() {
# Generate data
overages <- seq(0, 50e6, length.out = 1000)
tax_data <- map_dfr(overages, ~{
regular <- calculate_luxury_tax(LUXURY_TAX + .x, is_repeater = FALSE)
repeater <- calculate_luxury_tax(LUXURY_TAX + .x, is_repeater = TRUE)
tibble(
Overage = .x,
Regular_Tax = regular$tax_bill,
Repeater_Tax = repeater$tax_bill,
Regular_Total = regular$total_cost,
Repeater_Total = repeater$total_cost
)
})
# Tax bill plot
p1 <- tax_data %>%
pivot_longer(
cols = c(Regular_Tax, Repeater_Tax),
names_to = "Type",
values_to = "Tax_Bill"
) %>%
mutate(Type = str_remove(Type, "_Tax")) %>%
ggplot(aes(x = Overage / 1e6, y = Tax_Bill / 1e6, color = Type)) +
geom_line(size = 1.5) +
scale_color_manual(values = c("Regular" = "#1f77b4", "Repeater" = "#ff7f0e")) +
labs(
title = "Luxury Tax Bill by Overage Amount",
x = "Overage (Millions)",
y = "Tax Bill (Millions)",
color = "Tax Status"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 14, face = "bold"),
legend.position = "top"
)
# Total cost plot
p2 <- tax_data %>%
pivot_longer(
cols = c(Regular_Total, Repeater_Total),
names_to = "Type",
values_to = "Total_Cost"
) %>%
mutate(Type = str_remove(Type, "_Total")) %>%
ggplot(aes(x = Overage / 1e6, y = Total_Cost / 1e6, color = Type)) +
geom_line(size = 1.5) +
geom_hline(yintercept = LUXURY_TAX / 1e6, linetype = "dashed",
color = "red", size = 0.8) +
scale_color_manual(values = c("Regular" = "#1f77b4", "Repeater" = "#ff7f0e")) +
labs(
title = "Total Team Cost (Salary + Tax)",
x = "Overage (Millions)",
y = "Total Cost (Millions)",
color = "Tax Status"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 14, face = "bold"),
legend.position = "top"
)
# Combine plots
gridExtra::grid.arrange(p1, p2, ncol = 2)
ggsave("luxury_tax_visualization.png",
gridExtra::arrangeGrob(p1, p2, ncol = 2),
width = 14, height = 6, dpi = 300)
}
#' Compare marginal cost of signing player
#'
#' @param player_salary Proposed player salary
#' @param current_salary Current team salary
#' @param is_repeater Repeater status
compare_signing_scenarios <- function(player_salary, current_salary, is_repeater = FALSE) {
baseline <- calculate_luxury_tax(current_salary, is_repeater)
with_player <- calculate_luxury_tax(current_salary + player_salary, is_repeater)
marginal_cost <- with_player$total_cost - baseline$total_cost
tax_multiplier <- marginal_cost / player_salary
tibble(
Scenario = c("No Signing", "Sign Player"),
Team_Salary = c(current_salary, current_salary + player_salary),
Tax_Bill = c(baseline$tax_bill, with_player$tax_bill),
Total_Cost = c(baseline$total_cost, with_player$total_cost),
Marginal_Cost = c(0, marginal_cost),
Tax_Multiplier = c(0, tax_multiplier)
)
}
# Example usage
main <- function() {
cat("=== NBA Salary Cap Analysis ===\n\n")
# Example 1: Luxury tax calculation
cat("Example 1: Luxury Tax Calculation\n")
team_salary <- 180e6
tax_info <- calculate_luxury_tax(team_salary, is_repeater = FALSE)
cat(sprintf("Team Salary: $%s\n", dollar(team_salary)))
cat(sprintf("Overage: $%s\n", dollar(tax_info$overage)))
cat(sprintf("Tax Bill: $%s\n", dollar(tax_info$tax_bill)))
cat(sprintf("Total Cost: $%s\n", dollar(tax_info$total_cost)))
cat(sprintf("Effective Rate: %.2fx\n\n", tax_info$effective_rate))
# Example 2: Supermax projection
cat("Example 2: Supermax Contract Projection\n")
supermax <- project_contract(
starting_salary = 0.35 * SALARY_CAP,
years = 5,
annual_raise = 0.08
)
print(supermax)
cat(sprintf("\nTotal Value: $%s\n\n", dollar(supermax$Total_Value[1])))
# Example 3: Team cap analysis
cat("Example 3: Team Cap Situation\n")
roster <- c(45e6, 35e6, 25e6, 18e6, 12e6, 8e6, 5e6, 3e6, 2.5e6, 2e6, 1.8e6, 1.5e6)
cap_situation <- analyze_team_cap(roster)
cat(sprintf("Total Salary: $%s\n", dollar(cap_situation$total_salary)))
cat(sprintf("Cap Space: $%s\n", dollar(cap_situation$cap_space)))
cat(sprintf("Over Tax: %s\n", cap_situation$over_tax))
cat(sprintf("Available MLE: $%s\n", dollar(cap_situation$available_mle)))
cat(sprintf("Tax Bill: $%s\n\n", dollar(cap_situation$tax_bill)))
# Example 4: Signing comparison
cat("Example 4: Marginal Cost Analysis\n")
comparison <- compare_signing_scenarios(
player_salary = 15e6,
current_salary = 170e6,
is_repeater = FALSE
)
print(comparison)
# Visualizations
cat("\nGenerating visualizations...\n")
visualize_tax_brackets()
cat("\nAnalysis complete!\n")
}
# Run analysis
main()