Spaces:
Sleeping
Sleeping
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('<div class="shot-area">')[1].split('<div'): | |
if 'style="top:' in line: | |
top = int(line.split('top:')[1].split('px')[0]) | |
left = int(line.split('left:')[1].split('px')[0]) | |
game = line.split('tip="')[1].split('<br>')[0] | |
clock = line.split('<br>')[1].split('<br>')[0] | |
description = line.split('<br>')[2] | |
score = line.split('<br>')[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') | |