Introduction

This code through explores if the statement “defense wins championships” is really true in the NBA. The tutorial examines NBA champions and finals runner-ups from 1996 to 2025 to explore the relationship between defensive ranking and playoff success.


Content Overview

Specifically, we’ll explain and demonstrate how to import and combine NBA data sets, categorize teams by defensive ranking, and create bar charts and interactive scatter plots using R.


Why You Should Care

This topic is valuable because it allows us to examine whether a strong defense is associated with championship success. This can help organizations build a defensive minded team to hopefully have playoff success.


Learning Objectives

Specifically, you’ll learn how to import and merge NBA data sets, organize teams by defensive rankings, and create bar charts and scatter plots between defense and playoff success.



# Analyzing NBA Defensive Rankings and Playoff Success

Here, we’ll show how to use NBA regular-season and playoff data from 1996 to 2025 to examine how defensive rankings related to postseason success. We’ll combine data sets, identify NBA champion and Final runner-ups, and create visualizations to compare their defensive performances.


Further Exposition

This analysis uses NBA regular-season advanced statistics and playoff results from 1996-2025. By combining these datasets in R, we can compare defensive rankings across teams and examine whether stronger defenses are associated with greater playoff success.


Basic Example

A basic example shows how to use Base R and plotly package to import NBA data sets and prepare them for visualization. We will use read.csv() to load and merge() to combine regular-season statistic with playoff results. The last table shows the NBA champion along with their defensive rating.

# Load the plotly package
library(plotly)

# Import the NBA data sets
regular <- read.csv("regular_season_advanced_stats_1996_2025.csv")
playoffs <- read.csv("playoff_wins_1996_2025.csv")

# Combine regular-season statistics with playoff results
playoff_stats <- merge(
  playoffs,
  regular,
  by = c("SEASON", "TEAM_ID")
)

# Display the first six rows
head(playoff_stats)
# Identify the team with the most playoff wins each season
champions <- playoffs[
  ave(
    playoffs$PLAYOFF_WINS,
    playoffs$SEASON,
    FUN = max
  ) == playoffs$PLAYOFF_WINS,
]

# Combine champions with their regular-season statistics
champion_stats <- merge(
  champions,
  regular,
  by = c("SEASON", "TEAM_ID")
)

# Display each champion's season, team, and defensive ranking
champion_stats[
  , c("SEASON", "TEAM_NAME.x", "DEF_RATING_RANK")
]


Advanced Examples

More specifically, this can be used to categorize NBA teams by defensive ranking and compare defensive performances of NBA champions and Finals runner-ups. Using R, we will create a bar chart to visualize the differences.

# Group NBA champions by defensive ranking
defense_groups <- cut(
  champion_stats$DEF_RATING_RANK,
  breaks = c(0, 5, 10, Inf),
  labels = c("Top 5", "6-10", "11+")
)

# Count champions in each category
table(defense_groups)
## defense_groups
## Top 5  6-10   11+ 
##    19     7     3
# Identify the NBA Finals runners-up from 1996-2025
finals_losers <- data.frame(
  SEASON = c(
    "1996-97", "1997-98", "1998-99", "1999-00",
    "2000-01", "2001-02", "2002-03", "2003-04",
    "2004-05", "2005-06", "2006-07", "2007-08",
    "2008-09", "2009-10", "2010-11", "2011-12",
    "2012-13", "2013-14", "2014-15", "2015-16",
    "2016-17", "2017-18", "2018-19", "2019-20",
    "2020-21", "2021-22", "2022-23", "2023-24",
    "2024-25"
  ),
  TEAM_NAME = c(
    "Utah Jazz", "Utah Jazz", "New York Knicks",
    "Indiana Pacers", "Philadelphia 76ers",
    "New Jersey Nets", "New Jersey Nets",
    "Los Angeles Lakers", "Detroit Pistons",
    "Dallas Mavericks", "Cleveland Cavaliers",
    "Los Angeles Lakers", "Orlando Magic",
    "Boston Celtics", "Miami Heat",
    "Oklahoma City Thunder", "San Antonio Spurs",
    "Miami Heat", "Cleveland Cavaliers",
    "Golden State Warriors", "Cleveland Cavaliers",
    "Cleveland Cavaliers", "Golden State Warriors",
    "Miami Heat", "Phoenix Suns",
    "Boston Celtics", "Miami Heat",
    "Dallas Mavericks", "Indiana Pacers"
  )
)

# Match runners-up with their regular-season statistics
finalists <- merge(
  finals_losers,
  playoff_stats,
  by.x = c("SEASON", "TEAM_NAME"),
  by.y = c("SEASON", "TEAM_NAME.x")
)

# Check that all 29 runners-up were matched
nrow(finalists)
## [1] 29
# Group Finals runners-up by defensive ranking
runner_up_groups <- cut(
  finalists$DEF_RATING_RANK,
  breaks = c(0, 5, 10, Inf),
  labels = c("Top 5", "6-10", "11+")
)

# Compare champions and runners-up
defense_comparison <- rbind(
  Champions = table(defense_groups),
  Runner_Ups = table(runner_up_groups)
)

# Calculate percentages within each group
percentages <- round(
  prop.table(defense_comparison, margin = 1) * 100,
  1
)

# Create the bar chart
bp <- barplot(
  defense_comparison,
  beside = TRUE,
  main = "NBA Finals Teams by Defensive Ranking (1996-2025)",
  xlab = "Regular Season Defensive Ranking",
  ylab = "Number of Teams",
  col = c("goldenrod", "red"),
  ylim = c(0, 25),
  legend.text = c("NBA Champions", "Finals Runners-Up"),
  args.legend = list(x = "topright", bty = "n")
)

# Add percentages above each bar
text(
  x = bp,
  y = defense_comparison + 1,
  labels = paste0(percentages, "%"),
  cex = 0.9
)

The bar chart compares the regular season defensive ranking of the NBA champions and the Finals runner-ups. The cut() function groups teams by defensive ranking, while barplot() displays the results. The percentage helps visualize the frequency each group finished in the top 5, between 6 and 10, or outside the top 10.

This shows us that over 65 percent of the past 29 NBA champions have been ranked as a top 5 defensive team for the season. Only 3 teams have won it all outside of the top 10 ranking.

Comparing this to the runner-ups, we see the that defensive ranking is also helps teams reach the finals but being outside of the top 10 brings in a higher percentage of losing.

Most notably, it’s valuable for examining the relationship between regular season defensive rankings and playoff success. Using the plotly package, we can create an interactive scatter plot that shows us NBA champions, Finals runner-ups, and other playoff teams. This allows readers to explore individual teams.

# Identify NBA champions
playoff_stats$Champion <- ifelse(
  paste(playoff_stats$SEASON, playoff_stats$TEAM_ID) %in%
    paste(champions$SEASON, champions$TEAM_ID),
  "NBA Champion",
  "Other Playoff Team"
)

# Identify NBA Finals runners-up
playoff_stats$Finals_Loser <- paste(
  playoff_stats$SEASON,
  playoff_stats$TEAM_NAME.x
) %in% paste(
  finals_losers$SEASON,
  finals_losers$TEAM_NAME
)

# Separate teams into three groups
other_teams <- subset(
  playoff_stats,
  Champion == "Other Playoff Team" & !Finals_Loser
)

finalists_plot <- subset(
  playoff_stats,
  Finals_Loser
)

winning_teams <- subset(
  playoff_stats,
  Champion == "NBA Champion"
)

# Create the interactive scatter plot
plot_ly() %>%

  # Other playoff teams (blue)
  add_markers(
    data = other_teams,
    x = ~DEF_RATING_RANK,
    y = ~PLAYOFF_WINS,
    name = "Other Playoff Teams",
    marker = list(
      color = "blue",
      size = 8,
      opacity = 0.75
    ),
    text = ~paste(
      "Team:", TEAM_NAME.x,
      "<br>Season:", SEASON,
      "<br>Defensive Rank:", DEF_RATING_RANK,
      "<br>Playoff Wins:", PLAYOFF_WINS
    ),
    hoverinfo = "text"
  ) %>%

  # NBA Finals runners-up (red)
  add_markers(
    data = finalists_plot,
    x = ~DEF_RATING_RANK,
    y = ~PLAYOFF_WINS,
    name = "Finals Runners-Up",
    marker = list(
      color = "red",
      size = 11,
      opacity = 1,
      line = list(color = "black", width = 1)
    ),
    text = ~paste(
      "Team:", TEAM_NAME.x,
      "<br>Season:", SEASON,
      "<br>Defensive Rank:", DEF_RATING_RANK,
      "<br>Playoff Wins:", PLAYOFF_WINS
    ),
    hoverinfo = "text"
  ) %>%

  # NBA champions (gold)
  add_markers(
    data = winning_teams,
    x = ~DEF_RATING_RANK,
    y = ~PLAYOFF_WINS,
    name = "NBA Champions",
    marker = list(
      color = "goldenrod",
      size = 13,
      opacity = 1,
      line = list(color = "black", width = 1)
    ),
    text = ~paste(
      "Team:", TEAM_NAME.x,
      "<br>Season:", SEASON,
      "<br>Defensive Rank:", DEF_RATING_RANK,
      "<br>Playoff Wins:", PLAYOFF_WINS
    ),
    hoverinfo = "text"
  ) %>%

  # Customize graph titles and axes
  layout(
    title = "NBA Playoff Success vs. Defensive Ranking (1996-2025)",

    xaxis = list(
      title = "Regular Season Defensive Ranking (1 = Best)",
      range = c(0.5, 30.5),
      tickmode = "array",
      tickvals = seq(5, 30, 5),
      showline = TRUE,
      linecolor = "black",
      linewidth = 2,
      ticks = "outside",
      tickcolor = "black",
      zeroline = FALSE
    ),

    yaxis = list(
      title = "Number of Playoff Wins",
      range = c(0, 17),
      showline = TRUE,
      linecolor = "black",
      linewidth = 2,
      ticks = "outside",
      tickcolor = "black",
      zeroline = FALSE
    )
  )

The scatter plot shows how regular season defensive ranking relate to playoff wins. Many NBA champions appear among the higher ranked defensive teams, suggesting that strong defense is associated with postseason success. It is also important to note that some teams with lower defensive ratings also reached or won the Finals. This shows that defense alone does not determine playoff outcomes.

Some interesting things I won were the 2022-2023 Cleveland Cavaliers had the best defensive rank but only won one playoff game. My Phoenix Suns were ranked 6 when they made it to the finals in 2021. They ended up losing to the Milwaukee Bucks who were ranked 9. This hurts.

Further Resources

Learn more about [package, technique, dataset] with the following:




Works Cited

This code through references and cites the following sources: