import streamlit as st import pandas as pd import matplotlib.pyplot as plt from matplotlib.patches import Circle, Rectangle, Arc import matplotlib.colors def seasons_of_player( playerId ): df = pd.read_html('https://www.basketball-reference.com/players/%s.html' % (playerId) ) try: seasons = df[0]['Season'] games_played = pd.to_numeric(df[0]['G'], errors='coerce') except: seasons = df[1]['Season'] games_played = pd.to_numeric(df[1]['G'], errors='coerce') seasons_played = seasons[ games_played>0 ] seasons_with_shots = [s for s in seasons_played if '-' in s and (int(s.split('-')[0])>=1996)] return seasons_with_shots st.set_page_config(page_title='NBA shots', page_icon=":basketball:") st.sidebar.title('Mapping NBA shots') if False: playerId = { 'Ray Allen':'a/allenra02', 'Kobe Bryant':'b/bryanko01', 'Stephen Curry':'c/curryst01', 'James Harden':'h/hardeja01', 'Allen Iverson':'i/iversal01', 'Michael Jordan':'j/jordami01', 'Damian Lillard':'l/lillada01', 'Karl Malone':'m/malonka01', 'Reggie Miller':'m/millere01', 'Dirk Nowitzki':'n/nowitdi01', "Shaquille O'Neal":'o/onealsh01', 'Trae Young':'y/youngtr01', 'Victor Wembanyama':'w/wembavi01', } else: import json playerId = json.load(open("all_player_names.txt")) selected = st.sidebar.selectbox('Select or type a player name:', ['']+list(playerId.keys()), format_func=lambda x: '...' if x == '' else x) if selected: #st.success('Yay! 🎉') seasons_with_shots = seasons_of_player( playerId[selected] ) season = st.sidebar.selectbox('Season:',seasons_with_shots) if season: seasonYearEnd = str(int(season.split('-')[0]) + 1) urlShots = 'https://www.basketball-reference.com/players/%s/shooting/%s' % (playerId[selected],seasonYearEnd) else: st.warning('No player selected') urlShots = '' # Checkboxes: left, right = st.sidebar.columns(2) # Range slider: valuesRange = st.sidebar.slider( 'Percentage limits for "bad" and "good" shots:', 0.0, 100.0, (10.0, 50.0)) colorBAD = left.color_picker('bad', '#ff0000') colorGOOD = right.color_picker('good', '#97ff33') with left: draw_court_box = st.sidebar.checkbox('Draw court',value=True) with right: draw_title_box = st.sidebar.checkbox('Add title',value=True) def draw_court(ax=None, color='black', lw=2, outer_lines=False): # If an axes object isn't provided to plot onto, just get current one if ax is None: ax = plt.gca() # Create the various parts of an NBA basketball court # Create the basketball hoop # Diameter of a hoop is 18" so it has a radius of 9", which is a value # 7.5 in our coordinate system hoop = Circle((0, 0), radius=7.5, linewidth=lw, color=color, fill=False) # Create backboard backboard = Rectangle((-30, -7.5), 60, -1, linewidth=lw, color=color) # The paint # Create the outer box 0f the paint, width=16ft, height=19ft outer_box = Rectangle((-80, -47.5), 160, 190, linewidth=lw, color=color, fill=False) # Create the inner box of the paint, widt=12ft, height=19ft inner_box = Rectangle((-60, -47.5), 120, 190, linewidth=lw, color=color, fill=False) # Create free throw top arc top_free_throw = Arc((0, 142.5), 120, 120, theta1=0, theta2=180, linewidth=lw, color=color, fill=False) # Create free throw bottom arc bottom_free_throw = Arc((0, 142.5), 120, 120, theta1=180, theta2=0, linewidth=lw, color=color, linestyle='dashed') # Restricted Zone, it is an arc with 4ft radius from center of the hoop restricted = Arc((0, 0), 80, 80, theta1=0, theta2=180, linewidth=lw, color=color) # Three point line # Create the side 3pt lines, they are 14ft long before they begin to arc corner_three_a = Rectangle((-220, -47.5), 0, 140, linewidth=lw, color=color) corner_three_b = Rectangle((220, -47.5), 0, 140, linewidth=lw, color=color) # 3pt arc - center of arc will be the hoop, arc is 23'9" away from hoop # I just played around with the theta values until they lined up with the # threes three_arc = Arc((0, 0), 475, 475, theta1=22, theta2=158, linewidth=lw, color=color) # Center Court center_outer_arc = Arc((0, 422.5), 120, 120, theta1=180, theta2=0, linewidth=lw, color=color) center_inner_arc = Arc((0, 422.5), 40, 40, theta1=180, theta2=0, linewidth=lw, color=color) # List of the court elements to be plotted onto the axes court_elements = [hoop, backboard, outer_box, inner_box, top_free_throw, bottom_free_throw, restricted, corner_three_a, corner_three_b, three_arc, center_outer_arc, center_inner_arc] if outer_lines: # Draw the half court line, baseline and side out bound lines outer_lines = Rectangle((-250, -47.5), 500, 470, linewidth=lw, color=color, fill=False) court_elements.append(outer_lines) # Add the court elements onto the axes for element in court_elements: ax.add_patch(element) return ax def html_to_shot_table( html ): full_table = [] for line in str(html).split('
')[1].split('')[0] clock = line.split('
')[1].split('
')[0] description = line.split('
')[2] score = line.split('
')[3].split('"')[0] made = 1 if 'tooltip miss' in line: made = 0 #print(line) #print(top,left,game,clock,description,score) full_table.append( [top,left,game,clock,description,score,made] ) return full_table # get the html: from urllib.request import urlopen try: if 'http' in urlShots: html = urlopen(urlShots).read() full_table = html_to_shot_table( html ) import pandas as pd df = pd.DataFrame(full_table, columns='top,left,game,clock,description,score,made'.split(',')) X = -1*df['left'] + 240 Y = df['top'] - 45 hbMade = plt.hexbin(X[ df['made']==1 ], Y[ df['made']==1 ], gridsize=(40,20), cmap='cool', extent=(-250,250,-50,420) ) hbMissed = plt.hexbin(X[ df['made']==0 ], Y[ df['made']==0 ], gridsize=(40,20), cmap='cool', extent=(-250,250,-50,420) ) plt.clf() # to flush the two plots we just made: we only wanted to catch the output pctMade = hbMade.get_array() / (hbMade.get_array() + hbMissed.get_array()) # Now convert these numbers to colours: import numpy as np pctMade[ np.isnan(pctMade) ] = 0 plt.figure(figsize=(9,8.5)) plt.subplot(111,facecolor='k') draw_court(outer_lines=True, color="#444444") plt.xlim(-251,251) plt.ylim(-47,423) plt.gca().set_facecolor('k') hb = plt.hexbin(X, Y, gridsize=(40,20), cmap='turbo', extent=(-250,250,-50,420)) total_count = hb.get_array() # convert the number to size: new_size = 0.005*total_count new_size[ new_size>0.1 ] = 0.1 plt.clf() # Replot: plt.figure(figsize=(9,8.5),facecolor='#111111') plt.subplot(111,facecolor='#111111') if draw_court_box: draw_court(outer_lines=True, color="#444444") plt.xlim(-252,252) plt.ylim(-50,425) plt.xticks([]); plt.yticks([]) plt.gca().set_facecolor('k') hb = plt.hexbin(X, Y, gridsize=(40,20), cmap='cool', extent=(-250,250,-50,420), sizes=new_size) ax = plt.gca() ax.figure.canvas.draw() # Now iterate over bins to change their colours: fcolors = hb.get_facecolors() for iii in range(len(fcolors)): if pctMade[iii] < valuesRange[0]/100.: fcolors[iii] = list(matplotlib.colors.to_rgb(colorBAD))+[1] #[1., 0., 0., 1.] elif pctMade[iii] > valuesRange[1]/100.: fcolors[iii] = list(matplotlib.colors.to_rgb(colorGOOD))+[1] #[0.6, 1., 0.2, 1.] else: fcolors[iii] = [0.9, 0.9, 0.9, 1.] hb.set(array=None, facecolors=fcolors) if draw_title_box: plt.text(-230,390,selected,color='w',fontsize=20,fontweight='bold') plt.text(-230,360,season,color='w',fontsize=18,fontweight='bold') st.pyplot(plt.gcf()) st.write('Data from:',urlShots) except: st.warning(f'It looks like there is no available shot data for {selected}. Detailed shot locations are typically not available before the 1996-97 season.') st.write(f'You can check his stats at:\nhttps://www.basketball-reference.com/players/{playerId[selected]}.html')