What Player Types Are Driving the NBA’s Three-Point Growth?

BAIS 462 Assignment 7: Ethical Web Scraping

Author

Tommy Aug

Introduction

Three-point shooting has changed how NBA teams build rosters, evaluate players, and design offenses. A useful analytics question is not just whether three-point attempts have increased, but which player types are driving that increase and whether the added volume has been matched by better efficiency.

This report asks:

What player types are responsible for the NBA’s increase in three-point attempts, and has the increase in volume been matched by better shooting efficiency?

This topic is interesting because NBA teams now value spacing at nearly every position. The analysis separates players by position so the report can show whether three-point growth is concentrated among traditional guard shooters or spread across forwards and centers as well. That makes the project more useful than simply showing that the league is taking more threes.

Data and Method

The data for this project was collected from Basketball-Reference NBA per-game player statistics pages for the 2015 through 2025 seasons. Each season page contains an HTML table of player per-game statistics. I scraped the same table structure across multiple season pages using a function and loop in a separate .R script.

The final dataset includes player-season observations with variables such as player name, position, age, team, games played, minutes per game, field goal attempts, three-point attempts, three-point percentage, points per game, season, and source URL.

The scraped data was saved as a static CSV file and then imported into this Quarto report. This makes the report cleaner and avoids repeatedly requesting the same pages.

Source: Basketball-Reference NBA Per-Game Player Statistics

Setup

library(tidyverse)

Import Static Dataset

data_url <- "https://myxavier-my.sharepoint.com/:x:/g/personal/augt_xavier_edu/IQBautRyLm1BRoQIgkU4cD3aAcoMwKtEcyYygWBw5GPxYo8?e=zPttoa&download=1"

nba_stats <- 
  read_csv(data_url)

Inspect the Data

names(nba_stats)
 [1] "season"      "player"      "pos"         "age"         "team"       
 [6] "games"       "minutes"     "fga"         "three_pa"    "three_pct"  
[11] "pts"         "source_url"  "primary_pos"
glimpse(nba_stats)
Rows: 5,878
Columns: 13
$ season      <dbl> 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015…
$ player      <chr> "A.J. Price", "Aaron Brooks", "Aaron Gordon", "Adreian Pay…
$ pos         <chr> "PG", "PG", "PF", "PF", "C", "C", "SF", "SG", "SG", "C", "…
$ age         <dbl> 28, 30, 19, 23, 28, 30, 24, 32, 23, 23, 21, 26, 26, 22, 27…
$ team        <chr> "3TM", "CHI", "ORL", "2TM", "ATL", "CHO", "DAL", "BRK", "U…
$ games       <dbl> 26, 82, 47, 32, 76, 65, 74, 74, 27, 5, 69, 42, 68, 51, 54,…
$ minutes     <dbl> 12.5, 23.0, 17.0, 23.1, 30.5, 30.6, 18.5, 23.6, 33.3, 2.8,…
$ fga         <dbl> 5.3, 10.0, 4.4, 6.9, 12.7, 15.5, 4.8, 5.9, 11.1, 0.8, 5.1,…
$ three_pa    <dbl> 2.2, 3.8, 1.0, 0.3, 0.5, 0.1, 1.7, 2.8, 2.5, 0.0, 0.0, 3.3…
$ three_pct   <dbl> 0.263, 0.387, 0.271, 0.111, 0.306, 0.400, 0.274, 0.348, 0.…
$ pts         <dbl> 5.1, 11.6, 5.2, 6.7, 15.2, 16.6, 5.6, 7.4, 13.9, 0.8, 6.3,…
$ source_url  <chr> "https://www.basketball-reference.com/leagues/NBA_2015_per…
$ primary_pos <chr> "PG", "PG", "PF", "PF", "C", "C", "SF", "SG", "SG", "C", "…
nrow(nba_stats)
[1] 5878
ncol(nba_stats)
[1] 13

Data Cleaning

Some players with very small roles can create misleading shooting statistics. For example, a player who attempts very few threes may have an extreme three-point percentage. To make the analysis more meaningful, I filtered to players with at least 20 games played and at least 10 minutes per game.

analysis_data <- 
  nba_stats %>%
  mutate(
    season = as.numeric(season),
    age = as.numeric(age),
    games = as.numeric(games),
    minutes = as.numeric(minutes),
    fga = as.numeric(fga),
    three_pa = as.numeric(three_pa),
    three_pct = as.numeric(three_pct),
    pts = as.numeric(pts),
    primary_pos = as.factor(primary_pos)
  ) %>%
  filter(
    games >= 20,
    minutes >= 10,
    !is.na(three_pa)
  )
analysis_data %>%
  summarise(
    rows = n(),
    players = n_distinct(player),
    seasons = n_distinct(season),
    avg_3pa = mean(three_pa, na.rm = TRUE),
    avg_3p_pct = mean(three_pct, na.rm = TRUE)
  )
# A tibble: 1 × 5
   rows players seasons avg_3pa avg_3p_pct
  <int>   <int>   <int>   <dbl>      <dbl>
1  4308    1033      11    3.02      0.325

Analysis 1: League-Wide Three-Point Trend by Season

The first step is to measure whether three-point attempts increased over time.

season_summary <- 
  analysis_data %>%
  group_by(season) %>%
  summarise(
    avg_3pa = mean(three_pa, na.rm = TRUE),
    median_3pa = median(three_pa, na.rm = TRUE),
    avg_3p_pct = mean(three_pct, na.rm = TRUE),
    players = n()
  ) %>%
  arrange(season)

season_summary
# A tibble: 11 × 5
   season avg_3pa median_3pa avg_3p_pct players
    <dbl>   <dbl>      <dbl>      <dbl>   <int>
 1   2015    2.17       1.9       0.299     386
 2   2016    2.27       2         0.306     377
 3   2017    2.57       2.35      0.312     374
 4   2018    2.78       2.6       0.325     384
 5   2019    3.03       2.7       0.324     392
 6   2020    3.24       3         0.334     381
 7   2021    3.41       3.1       0.335     395
 8   2022    3.37       3         0.329     409
 9   2023    3.29       3         0.333     399
10   2024    3.35       3.1       0.342     399
11   2025    3.59       3.3       0.334     412
season_summary %>%
  ggplot(aes(x = season, y = avg_3pa)) +
  geom_line() +
  geom_point() +
  labs(
    title = "Average Three-Point Attempts Per Game by Season",
    x = "Season",
    y = "Average 3PA Per Game"
  )

The table and chart show a clear increase in three-point volume. Average three-point attempts rose from 2.17 per game in 2015 to 3.59 per game in 2025, and the median increased from 1.9 to 3.3 attempts per game. This supports the idea that three-point shooting became a larger part of the average NBA player’s role.

Analysis 2: Did Efficiency Rise With Volume?

The next question is whether the league became more efficient as volume increased. More attempts are not automatically better because added volume can come with lower-quality shot attempts.

season_summary_long <- 
  season_summary %>%
  select(season, avg_3pa, avg_3p_pct) %>%
  pivot_longer(
    cols = c(avg_3pa, avg_3p_pct),
    names_to = "metric",
    values_to = "value"
  )

season_summary_long %>%
  ggplot(aes(x = season, y = value)) +
  geom_line() +
  geom_point() +
  facet_wrap(~ metric, scales = "free_y") +
  labs(
    title = "Three-Point Volume and Efficiency Over Time",
    x = "Season",
    y = "Value"
  )

This visualization shows that three-point volume increased more sharply than three-point percentage. Average three-point attempts rose from 2.17 in 2015 to 3.59 in 2025, while average three-point percentage moved from .299 to .334. This suggests the league’s change is mainly about shot selection, spacing, and offensive style rather than only improved shooting accuracy.

Analysis 3: Which Positions Are Driving the Increase?

This analysis compares three-point attempts by position over time.

position_summary <- 
  analysis_data %>%
  group_by(season, primary_pos) %>%
  summarise(
    avg_3pa = mean(three_pa, na.rm = TRUE),
    avg_3p_pct = mean(three_pct, na.rm = TRUE),
    players = n(),
    .groups = "drop"
  ) %>%
  filter(primary_pos %in% c("PG", "SG", "SF", "PF", "C"))

position_summary
# A tibble: 55 × 5
   season primary_pos avg_3pa avg_3p_pct players
    <dbl> <fct>         <dbl>      <dbl>   <int>
 1   2015 C             0.260      0.190      68
 2   2015 PF            1.34       0.252      84
 3   2015 PG            2.94       0.319      80
 4   2015 SF            2.80       0.346      72
 5   2015 SG            3.30       0.344      82
 6   2016 C             0.347      0.171      73
 7   2016 PF            1.65       0.291      78
 8   2016 PG            2.85       0.335      77
 9   2016 SF            3.05       0.340      72
10   2016 SG            3.43       0.355      77
# ℹ 45 more rows
position_summary %>%
  ggplot(aes(x = season, y = avg_3pa, color = primary_pos)) +
  geom_line() +
  geom_point() +
  labs(
    title = "Average Three-Point Attempts by Position",
    x = "Season",
    y = "Average 3PA Per Game",
    color = "Position"
  )

This chart identifies which positions are connected to the increase in three-point attempts. Guards remain high-volume shooters, but the position trends also show how forwards and centers fit into the league’s spacing shift. The position breakdown helps show that modern three-point growth is about changing player roles, not only traditional guard shooting.

Analysis 4: Relationship Between Volume and Efficiency

This scatterplot shows the relationship between a player’s three-point volume and three-point percentage.

analysis_data %>%
  filter(!is.na(three_pct)) %>%
  ggplot(aes(x = three_pa, y = three_pct)) +
  geom_point(alpha = 0.35) +
  geom_smooth(method = "lm", se = FALSE) +
  labs(
    title = "Relationship Between Three-Point Volume and Efficiency",
    x = "Three-Point Attempts Per Game",
    y = "Three-Point Percentage"
  )

This chart compares three-point volume and three-point accuracy at the player level. The trend line gives a quick visual summary of the relationship between attempts and percentage. In this dataset, the scatterplot is useful because it shows that higher three-point volume does not automatically translate into better shooting accuracy for every player.

Analysis 5: Is the Growth Coming From Stars or From More Players?

One possible explanation is that the league-wide increase is driven only by superstar shooters. Another possibility is that more regular rotation players are taking more threes. This table counts how many players reached different three-point attempt thresholds by season.

shooter_counts <- 
  analysis_data %>%
  group_by(season) %>%
  summarise(
    players_5plus_3pa = sum(three_pa >= 5, na.rm = TRUE),
    players_7plus_3pa = sum(three_pa >= 7, na.rm = TRUE),
    players_10plus_3pa = sum(three_pa >= 10, na.rm = TRUE),
    total_players = n()
  )

shooter_counts
# A tibble: 11 × 5
   season players_5plus_3pa players_7plus_3pa players_10plus_3pa total_players
    <dbl>             <int>             <int>              <int>         <int>
 1   2015                38                 4                  0           386
 2   2016                31                 8                  1           377
 3   2017                48                12                  1           374
 4   2018                64                14                  1           384
 5   2019                69                17                  2           392
 6   2020                78                26                  2           381
 7   2021                99                30                  3           395
 8   2022               104                35                  1           409
 9   2023                85                33                  4           399
10   2024                96                30                  2           399
11   2025               111                37                  5           412
shooter_counts %>%
  select(season, players_5plus_3pa, players_7plus_3pa, players_10plus_3pa) %>%
  pivot_longer(
    cols = -season,
    names_to = "threshold",
    values_to = "players"
  ) %>%
  ggplot(aes(x = season, y = players, color = threshold)) +
  geom_line() +
  geom_point() +
  labs(
    title = "Number of High-Volume Three-Point Shooters by Season",
    x = "Season",
    y = "Number of Players",
    color = "Threshold"
  )

The number of high-volume shooters increased substantially across the period. Players attempting at least five threes per game increased from 38 in 2015 to 111 in 2025, while players attempting at least seven threes per game increased from 4 to 37. This shows that the trend is not only explained by a few outlier stars; more rotation players are being expected to take threes.

Analysis 6: Three-Point Attempts by Position in the Most Recent Season

This boxplot compares three-point attempt distribution by position in the most recent season.

most_recent_season <- max(analysis_data$season, na.rm = TRUE)

analysis_data %>%
  filter(
    season == most_recent_season,
    primary_pos %in% c("PG", "SG", "SF", "PF", "C")
  ) %>%
  ggplot(aes(x = primary_pos, y = three_pa)) +
  geom_boxplot() +
  labs(
    title = paste("Three-Point Attempts by Position in", most_recent_season),
    x = "Position",
    y = "Three-Point Attempts Per Game"
  )

This chart shows how three-point volume is distributed across positions in the most recent season. The boxplot makes it easier to compare guards, wings, forwards, and centers in the same year. The spread across positions supports the idea that three-point shooting has become part of more player roles in the modern NBA.

Conclusion

This analysis shows how NBA three-point shooting changed from 2015 to 2025 and identifies which player types are connected to the increase in three-point volume. The most important evidence comes from comparing three-point attempts by position and counting how many players reached high-volume shooting thresholds each season.

The results show that the NBA’s three-point growth is best understood as a league-wide change in player roles. Modern teams are not only relying on traditional guard shooters. They are asking more players across more positions to create spacing.

The efficiency results are also important. Three-point percentage did not rise as much as three-point attempts, so the increase in volume appears to reflect strategic changes in shot selection more than a simple improvement in shooting ability. Overall, the project suggests that the NBA’s three-point boom is about both roster construction and offensive philosophy.

Source Attribution

Data was scraped from Basketball-Reference NBA per-game player statistics pages for the 2015 through 2025 seasons.

Source homepage: Basketball-Reference

Specific page pattern used:

https://www.basketball-reference.com/leagues/NBA_YEAR_per_game.html