Problem Set 5
Fitting NBA Three-Point Percentage
Group players according to their 2015 three-point percentages, then assign every player in a group the average observed 2016 percentage for that group. Narrower groups give the model more flexibility to follow the observed 2016 percentages.
Both seasons are already observed. The same players’ 2016 percentages are used to calculate the group averages and to assess how closely the fitted values match the data. The resulting RMSE is training error: it describes fit to these players, not how accurately 2015 percentage would forecast a new player or a future season.
The dataset contains one row for every NBA player-season from 1997 through 2016. It has not been filtered according to season or number of three-point attempts. Statistics for players who appeared on multiple teams in one season have already been combined so that each player has at most one row per season.
Prepare the Data
- Download nba_player_tpp.csv,
save it in your
datafolder, and run the following code to load it as a tbl calledshooting_raw.
The columns are:
Player: player nameSeason: seasonTPM: three-pointers madeTPA: three-pointers attemptedTPP: three-point percentage
Use
filter()to keep only the 2015 and 2016 seasons and player-seasons with at least 100 three-point attempts. Save the resulting tbl asshooting_filtered.Some players met the attempt requirement in only one of the two seasons. Starting with
shooting_filtered:- group the data by
Player - use
filter()andn()to keep players with two rows - ungroup the data
- select
Player,Season,TPA, andTPP - use
pivot_wider()to put the 2015 and 2016 values in separate columns
- group the data by
Save the result as shooting. It should have one row per
player and columns named TPA_2015, TPA_2016,
TPP_2015, and TPP_2016.
shooting <- shooting_filtered %>%
group_by(Player) %>%
filter(...) %>%
ungroup() %>%
select(...) %>%
pivot_wider(
names_from = ...,
values_from = ...
)- Use
head()to inspectshooting. Then make a scatterplot withTPP_2015on the horizontal axis andTPP_2016on the vertical axis. Usealphato make overlapping points easier to see, and give the axes informative labels.
Model 0: The Overall Mean
Begin with a constant model that does not use TPP_2015:
assign every player the overall mean of TPP_2016 as a
fitted value.
- Use
mutate()andmean()to add a column calledyhat_0containing this fitted value for every player.
The fitted-value plots use the same layered structure as the plots in
Lecture 5: the first geom_point() layer shows the observed
2016 percentages, and the second geom_point() layer shows
the model’s fitted values.
Create the first layered plot. Plot the observed
TPP_2016values in transparent gray and theyhat_0fitted values in red. Save the result asplot_0.Compute the training RMSE of
yhat_0. Save the result as a one-row tbl calledrmse_0.
Model 1: Two Groups
We can use 2015 three-point percentage by dividing the players into
two groups: those below the overall 2015 average and those above it.
Within each group, we will use the average observed
TPP_2016 as the fitted value.
- Use
cut()to divideTPP_2015at its overall mean. Then group the data by the resulting intervals and add the fitted value as a column calledyhat_1. Remember to ungroup the data and remove the temporarybinscolumn when you finish.
The endpoints 0.15 and 0.60 are slightly beyond the range of
percentages in this dataset. The middle cut point should be
mean(shooting$TPP_2015).
shooting <- shooting %>%
mutate(bins = cut(
TPP_2015,
breaks = c(0.15, mean(shooting$TPP_2015), 0.60),
include.lowest = TRUE
)) %>%
group_by(...) %>%
mutate(yhat_1 = ...) %>%
ungroup() %>%
select(-bins)- Recreate the same layered plot, but replace
yhat_0withyhat_1. Use blue for the fitted-value layer, change the title, and save the result asplot_1.
Models 2–4: Increasing the Number of Bins
We will now repeat the same process with increasingly narrow
intervals. The seq() function will create evenly spaced cut
points between 0.15 and 0.60.
- Divide
TPP_2015into intervals of width 0.05 and add the within-bin fitted value asyhat_2. Then createplot_2using the same two layers as above, with green points for the fitted values.
Repeat the process using intervals of width 0.025. Call the fitted values
yhat_3, use purple fitted-value points, and save the plot asplot_3.Repeat the process one more time using intervals of width 0.01. Call the fitted values
yhat_4, use orange fitted-value points, and save the plot asplot_4.
Compare the Fitted Values Visually
- Run the code below to reshape the five fitted-value columns into a
long tbl called
fit_plot_data. We will use this long tbl only for the faceted plot in this question.
fit_plot_data <- shooting %>%
select(Player, TPP_2015, TPP_2016, starts_with("yhat_")) %>%
pivot_longer(
cols = starts_with("yhat_"),
names_to = "Model",
values_to = "Fitted_Value"
)Use fit_plot_data to make a faceted plot with one panel
for each model. In every panel, use one point layer for the observed
TPP_2016 values in transparent gray and another point layer
for the corresponding Fitted_Value values in blue. Use
facet_wrap() with Model as the faceting
variable.
Compare Training Error
Using
shooting, compute the training RMSE of all five fitted-value columns in a one-row tbl calledmodel_rmse.Reshape the one-row
model_rmsetbl from Question 14 into a two-column tbl namedmodel_rmse_long, with columns namedModelandRMSE. Then make a point-and-line plot of RMSE across the five models. The model names should appear on the horizontal axis in order fromrmse_0throughrmse_4.
As the bins become narrower, the fitted values can follow the observed outcomes increasingly closely, so training RMSE decreases. A bin containing one player would reproduce that player’s 2016 percentage exactly, but that does not establish a pattern that will hold for another player or season. In the next lecture, a straight line will replace these abrupt jumps and allow all of the players to inform the fit across the range of 2015 percentages.