How Much does Crowd Noise Affect NBA Players? (Season-Final)

Posted on August 11, 2020 in NBA-Basketball

Premise

Basketball is back in the bubble! But it's been drastically different with virtual fans and the lack of crowd noise. I was curious to see the effect that crowd noise had on the players, and the most "controlled" aspect of the game I could think of was free throws. Therefore, I wanted to see if player's free throw percentages improved due to the lack of crowd noise or not.

I used a matched pairs t-test to see whether a "bubble" effect existed on the NBA player's free throw percentage pre-bubble vs in the bubble. One of the disadvantages of the matched pairs t-test was that this treated the pre-bubble data as a single measurement point and the in-bubble data as another measurement point, but the data was actually binomial data.

Therefore, I also used a test of proportions to see whether any players deviated from their pre-bubble free throw shooting percentages

Load the data

I first loaded the box score data that was scraped from basketballreference.com using the code here

I then created dictionaries of each player's pre-bubble / bubble free throws made and free throw attempts. Thankfully no players in the NBA have the same name

import os
from os import path
import pickle
import pandas as pd
from scipy import stats
import numpy as np
import pylab
from statsmodels.stats.proportion import proportions_ztest
import matplotlib.pyplot as plt

def RepresentsInt(s):
    try:
        int(s)
        return True
    except ValueError:
        return False

working_dir = os.getcwd() + "/ft_data"
# Get all pickle files that match basic box score in the 2020 season
pre_bubble_box_scores = dict()
bubble_box_scores = dict()
for file in os.listdir(working_dir):
    if file.endswith(".pickle") and "basic_box_score" in file and "2020" in file:
        with open(working_dir + "/" + file, 'rb') as handle:
            b = pickle.load(handle)
            if 'july' in file or 'august' in file or 'september' in file or 'october' in file:
                bubble_box_scores.update(b)
            else:
                pre_bubble_box_scores.update(b)

pre_bubble_players_ft_attempts = dict()
pre_bubble_players_ft_made = dict()
debug = list()
for key, boxscore in pre_bubble_box_scores.items():
    ft_attempts = boxscore['FTA']
    ft_made = boxscore['FT']
    for index, value in ft_attempts.items():
        if RepresentsInt(value):
            if int(value) != 0:
                if index not in pre_bubble_players_ft_attempts.keys():
                    pre_bubble_players_ft_attempts[index] = int(ft_attempts[index])
                    pre_bubble_players_ft_made[index] = int(ft_made[index])
                else:
                    pre_bubble_players_ft_attempts[index] = pre_bubble_players_ft_attempts[index] + int(ft_attempts[index])
                    pre_bubble_players_ft_made[index] = pre_bubble_players_ft_made[index] + int(ft_made[index])

bubble_players_ft_attempts = dict()
bubble_players_ft_made = dict()
for key, boxscore in bubble_box_scores.items():
    ft_attempts = boxscore['FTA']
    ft_made = boxscore['FT']
    for index, value in ft_attempts.items():
        if RepresentsInt(value):
            if int(value) != 0:
                if index not in bubble_players_ft_attempts.keys():
                    bubble_players_ft_attempts[index] = int(value)
                    bubble_players_ft_made[index] = int(ft_made[index])
                else:
                    bubble_players_ft_attempts[index] = bubble_players_ft_attempts[index] + int(value)
                    bubble_players_ft_made[index] = bubble_players_ft_made[index] + int(ft_made[index])

Create the data frame

Next, I created a dataframe with the columns desired and ran through the players in the bubble to populate the the dataframe. I only took players who had taken at least 30 free throws in the pre-bubble as well as bubble.

  • Player
  • Pre-Bubble Free Throws Made
  • Pre-Bubble Free Throws Attempted
  • Bubble Free Throws Made
  • Bubble Free Throws Attempted
  • P-value of the difference between the two proportions
  • Pre-bubble Free Throw %
  • Bubble Free Throw %
  • Differences of Pre-bubble vs Bubble

Analysis

Test of Proportions

We had a total of 49 players who at the end of the bubble had taken more than 30 free throws in the bubble. Only 5 of them had significantly different free throw percentages at a 0.05 level

  • Russell Westbrook - Decrease of 23.5%
  • Jerami Grant - Increase of 13.1%
  • Eric Gordon - Increase of 12.65%
  • Danilo Gallinari - Increase of 10%
  • Bam Adebayo - Increase of 9.3%
ft_pct_df = pd.DataFrame(columns = ['player', 'pre-bubble-made', 'pre-bubble-att', 'bubble-made', 'bubble-att', 'p-val'])

for player in bubble_players_ft_attempts.keys():
    if player in pre_bubble_players_ft_attempts.keys() and pre_bubble_players_ft_attempts[player] > 30 and bubble_players_ft_attempts[player] > 30:
        count = np.array([pre_bubble_players_ft_made[player], bubble_players_ft_made[player]])
        nobs = np.array([pre_bubble_players_ft_attempts[player], bubble_players_ft_attempts[player]])
        stat, pval = proportions_ztest(count, nobs)

        ft_pct_df = ft_pct_df.append({'player': player,
                                      'pre-bubble-made': pre_bubble_players_ft_made[player],
                                      'pre-bubble-att': pre_bubble_players_ft_attempts[player],
                                      'bubble-made': bubble_players_ft_made[player],
                                     'bubble-att': bubble_players_ft_attempts[player],
                                      'p-val': pval}, ignore_index=True)

ft_pct_df['pre-ft-pct'] = (ft_pct_df['pre-bubble-made']/ft_pct_df['pre-bubble-att']).astype(float)
ft_pct_df['bubble-pct'] = (ft_pct_df['bubble-made']/ft_pct_df['bubble-att']).astype(float)
ft_pct_df['ft-pct-diff'] = (ft_pct_df['bubble-pct'] - ft_pct_df['pre-ft-pct']).astype(float)
ft_pct_df = ft_pct_df.reindex(ft_pct_df['ft-pct-diff'].abs().sort_values(ascending=False).index)
ft_pct_df.round(3)
player pre-bubble-made pre-bubble-att bubble-made bubble-att p-val pre-ft-pct bubble-pct ft-pct-diff
23 Russell Westbrook 248 317 23 42 0.001 0.782 0.548 -0.235
25 Jerami Grant 121 163 55 63 0.034 0.742 0.873 0.131
22 Eric Gordon 76 100 39 44 0.082 0.760 0.886 0.126
4 Pascal Siakam 194 248 36 53 0.109 0.782 0.679 -0.103
20 Danilo Gallinari 213 243 42 43 0.051 0.877 0.977 0.100
45 Kristaps Porziņģis 158 202 35 40 0.182 0.782 0.875 0.093
15 Bam Adebayo 206 298 98 125 0.053 0.691 0.784 0.093
5 Kyle Lowry 230 267 54 69 0.107 0.861 0.783 -0.079
46 D.J. Augustin 132 148 33 34 0.155 0.892 0.971 0.079
19 Khris Middleton 168 184 46 55 0.103 0.913 0.836 -0.077
7 Donovan Mitchell 228 264 63 67 0.086 0.864 0.940 0.077
44 Tim Hardaway 112 141 27 31 0.326 0.794 0.871 0.077
3 Jaylen Brown 151 204 60 74 0.224 0.740 0.811 0.071
8 Rudy Gobert 213 343 26 47 0.371 0.621 0.553 -0.068
43 Luka Dončić 345 459 61 89 0.192 0.752 0.685 -0.066
42 Carmelo Anthony 113 134 30 33 0.334 0.843 0.909 0.066
36 Ivica Zubac 91 123 33 41 0.401 0.740 0.805 0.065
40 CJ McCollum 106 145 31 39 0.417 0.731 0.795 0.064
41 Damian Lillard 353 397 79 83 0.084 0.889 0.952 0.063
37 Lou Williams 244 280 35 43 0.306 0.871 0.814 -0.057
30 Montrezl Harrell 207 315 41 68 0.396 0.657 0.603 -0.054
26 Paul Millsap 95 115 45 58 0.427 0.826 0.776 -0.050
33 Kyle Kuzma 96 130 33 42 0.539 0.738 0.786 0.047
6 Norman Powell 103 123 27 34 0.554 0.837 0.794 -0.043
35 Dwight Howard 88 175 30 55 0.581 0.503 0.545 0.043
32 LeBron James 219 319 113 155 0.343 0.687 0.729 0.043
27 Monte Morris 48 59 41 48 0.577 0.814 0.854 0.041
39 Ja Morant 193 250 26 32 0.605 0.772 0.812 0.040
17 Tyler Herro 52 61 57 64 0.523 0.852 0.891 0.038
1 Marcus Smart 103 124 58 67 0.526 0.831 0.866 0.035
11 Michael Porter 29 37 30 40 0.726 0.784 0.750 -0.034
47 Tobias Harris 143 177 24 31 0.663 0.808 0.774 -0.034
18 Giannis Antetokounmpo 340 532 64 105 0.565 0.639 0.610 -0.030
29 Kawhi Leonard 284 319 99 115 0.401 0.890 0.861 -0.029
14 Goran Dragić 147 188 59 73 0.640 0.782 0.808 0.026
13 Jae Crowder 88 112 35 46 0.733 0.786 0.761 -0.025
38 Caris LeVert 109 148 25 33 0.803 0.736 0.758 0.021
28 Paul George 165 187 57 66 0.690 0.882 0.864 -0.019
16 Duncan Robinson 47 52 39 44 0.780 0.904 0.886 -0.017
12 Jimmy Butler 397 473 177 207 0.602 0.839 0.855 0.016
34 Alex Caruso 58 76 27 36 0.879 0.763 0.750 -0.013
2 Kemba Walker 171 198 75 88 0.798 0.864 0.852 -0.011
48 Joel Embiid 293 360 53 66 0.836 0.814 0.803 -0.011
0 Jayson Tatum 215 265 106 130 0.923 0.811 0.815 0.004
10 Nikola Jokić 204 251 72 89 0.938 0.813 0.809 -0.004
9 Jamal Murray 139 155 72 80 0.938 0.897 0.900 0.003
31 Anthony Davis 338 403 165 196 0.922 0.839 0.842 0.003
24 Daniel Theis 77 100 27 35 0.986 0.770 0.771 0.001
21 James Harden 556 653 133 156 0.972 0.851 0.853 0.001

Matched Pairs T-Test

The effect size of the bubble on free throw percentage increased these 49 players free throw percentage on average by 0.8%. Note that this treats all players equally, regardless of how many free throws they've attempted because we group by each player.

The matched pair's t-test assumes that the observations are independent of one another and that the dependent variable should be approximately normally distributed. Plotting a kernal density estimator and a q-q plot, we see that we have approximate normality. From the q-q plot, we can see that Russell Westbrook was our lone outlier from normality.

A matched pair's t-test shows a p-value of .388, so we do not have enough evidence to reject the null hypothesis that the effect of the bubble on a player's free throw percentage is 0.

fig, ax = plt.subplots()
ax = ft_pct_df['ft-pct-diff'].plot.kde()
fig.suptitle('KDE of Free Throw Difference\n Pre-Bubble to Bubble')
plt.show()

stats.probplot(ft_pct_df['ft-pct-diff'], dist="norm", plot=pylab)
pylab.show()

print('P-value of Shapiro-Wilks Test for Normality = ' + str(round(stats.shapiro(ft_pct_df['ft-pct-diff'])[1],3)))

print('Effect size of the bubble = ' + str(round(ft_pct_df['ft-pct-diff'].mean(),3)))
pval = stats.ttest_rel(ft_pct_df['pre-ft-pct'], ft_pct_df['bubble-pct'])[1]
print('P-value of Matched Pairs T-Test = ' + str(round(pval,3)))

png

png

P-value of Shapiro-Wilks Test for Normality = 0.045
Effect size of the bubble = 0.008
P-value of Matched Pairs T-Test = 0.388

Conclusion

The data only shows that 5 players have a significant difference in their free throw percentage before the bubble and in the bubble. Using a matched pairs t-test, we can conclude that a player's free throw percentage does not increase in the bubble.

In an earlier version of the analysis performed on 8/10/2020, the matched t-test did show a significant effect of 2.6%. The fact that we do not see any effect at the end of the bubble could be due to a regression to the mean or the fact that players adjusted to playing in the bubble.