library(tidyverse)What Player Types Are Driving the NBA’s Three-Point Growth?
BAIS 462 Assignment 7: Ethical Web Scraping
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.
Setup
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