The ROI of Modern NBA Players

Author

Austin Blumenthal

Published

May 1, 2026

The ROI of Modern NBA Players

Overview

The NBA is a booming market that is valued at over $130 billion. When dealing with these large amounts of money, it is important that teams find ways to maximize their ROI on the players they choose to represent their team. I am going to take a modern analytics approach to seeing which players are worth their pay.

The Salary Landscape

Before we look at the ROI of each player, we first need to understand what the market is like, and how players are valued.

Code
# Here is a summary statistics table that will show some general summaries about these modern salaries.
salary_summary <- recent_salaries %>%
  summarize(
    `Total Players` = n(),
    `Average Salary` = scales::dollar(mean(Salary, na.rm = TRUE)),
    `Median Salary` = scales::dollar(median(Salary, na.rm = TRUE)),
    `Max Salary` = scales::dollar(max(Salary, na.rm = TRUE)),
    `Min Salary` = scales::dollar(min(Salary, na.rm = TRUE))
  )

kable(salary_summary, caption = "Summary Stats: NBA Player Salaries (Recent Seasons)")
Summary Stats: NBA Player Salaries (Recent Seasons)
Total Players Average Salary Median Salary Max Salary Min Salary
1129 $9,082,208 $3,879,840 $55,761,216 $11,997

Below is a histogram that shows NBA salaries are heavily skewed to the right. This shows the rarity of supermax contracts, and how the majority of contracts are either smaller or vet minimum contracts.

Code
# A histogram showcasing the distribution of salaries across the league.
ggplot(recent_salaries, aes(x = Salary)) +
  geom_histogram(bins = 30, fill = "#2c3e50", color = "white", alpha = 0.9) +
  scale_x_continuous(labels = scales::label_dollar(scale = 1e-6, suffix = "M")) +
  theme_minimal() +
  labs(
    title = "Distribution of NBA Player Salaries",
    subtitle = "Highlighting the heavy right-skew of NBA contracts",
    x = "Annual Salary (in Millions)",
    y = "Number of Players"
  )

To narrow down our research, the top 20 earners in recent NBA history are in the below table. This will give you a better idea of who these supermax contracts are being given to.

Code
# This will show the top 20 earners in recent NBA history
top_earners <- recent_salaries %>%
  group_by(Player) %>%
  # Keep only the single highest-salary season for each player
  slice_max(order_by = Salary, n = 1, with_ties = FALSE) %>%
  ungroup() %>%
  arrange(desc(Salary)) %>%
  slice_head(n = 20) %>%
  select(Player, Season, Salary) %>%
  mutate(Salary = scales::dollar(Salary))

kable(top_earners, caption = "Top 20 Highest-Paid NBA Players")
Top 20 Highest-Paid NBA Players
Player Season Salary
Stephen Curry 2025 $55,761,216
Joel Embiid 2025 $51,415,938
Nikola Jokic 2025 $51,415,938
Kevin Durant 2025 $51,179,021
Bradley Beal 2025 $50,203,930
Devin Booker 2025 $49,205,800
Jaylen Brown 2025 $49,205,800
Karl-Anthony Towns 2025 $49,205,800
Kawhi Leonard 2025 $49,205,800
Paul George 2025 $49,205,800
LeBron James 2024 $49,021,953
Jimmy Butler 2025 $48,798,677
Damian Lillard 2025 $48,787,676
Giannis Antetokounmpo 2025 $48,787,676
Zach LaVine 2025 $44,531,940
Klay Thompson 2024 $44,503,661
Rudy Gobert 2025 $43,827,586
Anthony Davis 2025 $43,219,440
Luka Doncic 2025 $43,031,940
Trae Young 2025 $43,031,940
Code
# Here is the next data frame, it can be downloaded at: https://myxavier-my.sharepoint.com/:u:/g/personal/blumenthala_xavier_edu/IQCtmWtWZJT7TLkx-1Exg9kJAcut8TJ6tSuum_JrHzuToeU?e=JmAZ1b
nba_stats <- read_csv("nba_player_totals_24_to_26.csv")

nba_roi <- recent_salaries %>%
  inner_join(nba_stats, by = c("Player", "Season")) %>%
  mutate(
    PTS = as.numeric(PTS),
    AST = as.numeric(AST),
    Total_Offense = PTS + (AST * 2)
  ) %>%
  filter(Season == 2025) %>%
  # Here are the top 20 earners
  mutate(
    Salary_Rank = min_rank(desc(Salary)),
    Top_20_Flag = if_else(Salary_Rank <= 20, "Top 20 Earner", "Rest of League")
  ) %>%
  # Get rid of players who barely have any minutes of play time
  filter(as.numeric(MP) > 500)

The chart beneath showcases the total payrolls by each NBA team. The interesting part of this data is that some of the high spending teams are not performing well and some of the teams with lower payrolls are performing very well. This shows that many teams are either overspending on players, while others may have even more room for growth and future success.

Code
team_payroll <- nba_roi %>%
  # Ensure we only count each player's salary once per season
  group_by(Player) %>%
  slice_head(n = 1) %>%
  ungroup() %>%
  group_by(Team) %>%
  summarize(Total_Payroll = sum(Salary, na.rm = TRUE)) %>%
  # Filter out NA teams, "TOT", and Kaggle's "2TM", "3TM" trade designations
  filter(!is.na(Team), Team != "TOT", !str_detect(Team, "TM")) %>% 
  arrange(desc(Total_Payroll))

# Create a ranked bar chart of team payrolls
ggplot(team_payroll, aes(x = reorder(Team, Total_Payroll), y = Total_Payroll, fill = Total_Payroll)) +
  geom_col(alpha = 0.9, width = 0.7) +
  coord_flip() +
  geom_text(aes(label = scales::dollar(Total_Payroll, scale = 1e-6, suffix = "M")), 
            hjust = 1.1, 
            color = "white", 
            fontface = "bold",
            size = 3) +
  scale_y_continuous(labels = scales::label_dollar(scale = 1e-6, suffix = "M")) +
  theme_minimal() +
  labs(
    title = "Total Committed Payroll by NBA Franchise (2024)",
    subtitle = "Aggregated salary commitments across active rosters",
    x = "Franchise",
    y = "Total Team Payroll",
    fill = "Payroll Size"
  ) +
  scale_fill_viridis_c(option = "cividis", direction = -1, guide = "none") +
  theme(
    panel.grid.major.y = element_blank(),
    plot.title = element_text(face = "bold", size = 14)
  )

Evaluating the High-Rollers and Steals

Now that we have a good idea of what the salary landscape looks like in the NBA, it is time to see how well these top earners are performing on the court. I am going to integrate a second data frame that contains the season player stats of all NBA players since 2024.

The scatterplot beneath identifies these top 20 earners and the amount of offensive production that they create. Both Joel Embiid and Jimmy Butler are listed as clear under performers, since they both were hurt for the majority of the 2025 season.

Code
median_salary <- median(nba_roi$Salary, na.rm = TRUE)
median_offense <- median(nba_roi$Total_Offense, na.rm = TRUE)

ggplot(nba_roi, aes(x = Total_Offense, y = Salary)) +
  # Draw the Rest of League as highly transparent, smaller dots (reduces clutter)
  geom_point(
    data = filter(nba_roi, Top_20_Flag == "Rest of League"), 
    color = "#bdc3c7", 
    alpha = 0.4, 
    size = 2
  ) +
  # Draw the Top 20 Earners as solid, larger dots
  geom_point(
    data = filter(nba_roi, Top_20_Flag == "Top 20 Earner"), 
    color = "#c0392b", 
    alpha = 0.9, 
    size = 4
  ) +
  # Draw the Quadrant Lines
  geom_vline(xintercept = median_offense, linetype = "dashed", color = "#34495e", alpha = 0.7) +
  geom_hline(yintercept = median_salary, linetype = "dashed", color = "#34495e", alpha = 0.7) +
  # Add labels to the Top 20 earners with clean spacing
  geom_text_repel(
    data = filter(nba_roi, Top_20_Flag == "Top 20 Earner"),
    aes(label = Player),
    size = 3.5,
    fontface = "bold",
    color = "#2c3e50",
    box.padding = 0.6,
    max.overlaps = 15
  ) +
  # Add text annotations for the quadrants
  annotate("text", x = max(nba_roi$Total_Offense) * 0.85, y = max(nba_roi$Salary) * 0.95, label = "Expected Output", color = "#7f8c8d", fontface = "italic") +
  annotate("text", x = max(nba_roi$Total_Offense) * 0.85, y = min(nba_roi$Salary) * 1.5, label = "High Value / Underpaid", color = "#27ae60", fontface = "italic") +
  annotate("text", x = min(nba_roi$Total_Offense) * 1.5, y = max(nba_roi$Salary) * 0.95, label = "Low Value / Overpaid", color = "#c0392b", fontface = "italic") +
  scale_y_continuous(labels = scales::label_dollar(scale = 1e-6, suffix = "M")) +
  theme_minimal() +
  labs(
    title = "NBA Contract Efficiency (2025)",
    subtitle = "Dashed lines represent league medians for Salary and Total Offense",
    x = "Total Offense Generated (Points + Assists * 2)",
    y = "Annual Salary"
  )

This ranked bar chart shows the interesting findings of both Jalen Williams and Josh Giddey being underpaid for the value that they provide for their teams. Jalen Williams is currently on the OKC Thunder, and Josh Giddey was traded away from the thunder for value as well. This may explain the extreme success and dominance of the Thunder these past 2 seasons.

Code
# Let's search for the players who are the best bang for their buck.
value_players <- nba_roi %>%
  filter(Total_Offense > median_offense, Salary < median_salary) %>%
  # Sort to find the players outperforming their salaries
  arrange(desc(Total_Offense)) %>%
  slice_head(n = 15)

# Create a ranked bar chart highlighting these players
ggplot(value_players, aes(x = reorder(Player, Total_Offense), y = Total_Offense, fill = Salary)) +
  geom_col(alpha = 0.9, width = 0.7) +
  coord_flip() +
  geom_text(aes(label = scales::dollar(Salary, scale = 1e-6, suffix = "M")), 
            hjust = 1.2, 
            color = "white", 
            fontface = "bold",
            size = 3.5) +
  theme_minimal() +
  labs(
    title = "Top 15 Underpaid Offensive Engines",
    subtitle = "Players exceeding league median in offense while making less than median salary",
    x = "Player",
    y = "Total Offense Generated (Points + Assists * 2)",
    fill = "Salary Cap Hit"
  ) +
  scale_fill_viridis_c(option = "mako", direction = -1, labels = scales::label_dollar(scale = 1e-6, suffix = "M")) +
  theme(
    legend.position = "right",
    panel.grid.major.y = element_blank()
  )

This final chart shows the amount of ‘value’ players that each NBA team has. This may be the most interesting finding yet, since the Utah Jazz and Washington Wizards have the most of this kind, yet they are currently having the least amount of success of all teams.

Code
team_steals <- nba_roi %>%
  group_by(Player) %>%
  slice_head(n = 1) %>%
  ungroup() %>%

  filter(Total_Offense > median_offense, Salary < median_salary) %>%
  group_by(Team) %>%
  summarize(Value_Contracts = n()) %>%

  filter(!is.na(Team), Team != "TOT", !str_detect(Team, "TM")) %>%
  arrange(desc(Value_Contracts))

ggplot(team_steals, aes(x = reorder(Team, Value_Contracts), y = Value_Contracts, fill = Value_Contracts)) +
  geom_col(alpha = 0.85, width = 0.7) +
  coord_flip() +
  geom_text(aes(label = Value_Contracts), 
            hjust = -0.5, 
            color = "#2c3e50", 
            fontface = "bold",
            size = 4) +
  scale_y_continuous(breaks = scales::pretty_breaks()) +
  theme_minimal() +
  labs(
    title = "'Value' Contracts per Franchise (2025)",
    subtitle = "Number of rostered players exceeding league median offense on below-median salaries",
    x = "Franchise",
    y = "Count of High-Value Players",
    fill = "Player Count"
  ) +
  scale_fill_viridis_c(option = "mako", direction = -1, guide = "none") +
  theme(
    panel.grid.major.y = element_blank(),
    plot.title = element_text(face = "bold", size = 14)
  )

What Stands Out?

There is a large distribution of total payroll being spent by each team in the NBA, and the amount of success that they are receiving from these payrolls. The OKC Thunder are currently leading in finding people to out perform the salary they are being paid, and this may be a factor driving their recent success. The other large takeaway, is that injuries to players on large contacts can put teams into a rough position.