很多籃球迷對NBA球員的投籃出手位置感興趣,想要得到如下的這種圖:
經搜索發現網上已有相關資源,基本都來源於How to Create NBA Shot Charts in Python(http://savvastjortjoglou.com/nba-shot-sharts.html),但是現在按照這些教程都不能重現。
本文將介紹怎樣具體可操作的用python的matplotlib包實現繪製NBA球員投籃出手位置圖。
要做到這件事主要是解決兩個大問題:我們將練習到如下知識:怎麼樣通過網頁分析獲取數據API
獲取網頁數據的基礎方式
繪製籃球半場圖
第一部分--獲取球員投籃位置數據NBA官方並沒有提供公共的API方便我們訪問球員的shot log, Web Scraping 201: finding the API(http://www.gregreda.com/2015/02/15/web-scraping-finding-the-api/)這篇文章為我們提供了分析網頁尋找數據API的方法,我們要分析NBA球員shot log可拆解成以下步驟:
1.鎖定目標網站
目標網站:stats.nba.com
2.具體網頁對象
shot log所在的頁面標籤可能會有改變,有時不在很顯眼的位置,這也是很多教程失效的原因(只給了最後API的網址,沒有說這個網址是怎麼來的),所以這個得花時間找一下。
首先打開目標網站stats.nba.com,按下圖所示依次點擊 Player, SeeAllStats
按照圖中1、2順序點擊後會得到以下頁面
頁面表格是每個NBA球員的數據,表頭都是簡寫,通過圖示點擊 GLOSSARY我們得到表頭詳細信息,其中 FGA-FieldGoalsAttempted表示嘗試投籃的位置,表格中每個球員的 FGA列的數字都是可點擊的,我們按上圖所示點擊 JamesHarden的 FGA列數字,跳轉的結果是顯示了 JamesHarden2017-18常規賽的 HexHap
ShotPlot
ShotZones
好了,可以結束了...
等等,我們的目的不是簡單得到 ShotPlot,而是要 練習一些知識,所以,繼續
點擊 JamesHarden的 FGA數據,跳轉後的頁面除了以上3個圖還包括以下這個表格
這個表格的內容結合 ShotPlot的圖,可以確定,要找的 具體網頁對象應該是這個頁面了,但是表格中並沒有直接給出投籃位置信息,這個網頁訪問的API應該包括這些信息,所以我們進入下面的步驟
3.分析shot log API
我們以Chrome瀏覽器為例,在上一步找到的 具體網頁對象頁面打開瀏覽器的開發者選項(更多工具->開發者工具),然後按F5鍵,刷新頁面,你將得到如下頁面
我們按上圖紅色數字標註,依次點擊,選擇 Network,然後點擊 XHR進行過濾, XHR是XMLHttpRequest的簡寫 - 這是一種用來獲取XML或JSON數據的請求類型。經 XHR篩選後表格中的有幾個條目,紅色數字標註為3的既是我們將要查找的shot log API, Preview標籤中包括:
resource - 請求的名稱 shotchartdetail。
parameters - 請求參數,提交給API的請求參數,我們可以理解成SQL語言的條件語句,例如賽季、球員ID等等,我們改變URL中的參數就能得到不同的數據
resultSets - 請求得到的數據集,包含兩個表格。仔細看表頭(headers)第一個表格包含我們想要的shot log信息(LOCX,LOCY)。
與 Preview並列的 Headers標籤包含:
通過API獲取感興趣球員的shot log數據
4.通過API獲取感興趣球員的shot log數據
上一步得到了本賽季MVP熱門人選 JamesHardenshot log的Request URL和Requset Headers,下面我們要做的是通過python代碼獲取shot log數據,以下是代碼
import requests
import pandas as pd
shot_chart_url = 'http://stats.nba.com/stats/shotchartdetail?AheadBehind=&'\
'CFID=&CFPARAMS=&ClutchTime=&Conference=&ContextFilter=&ContextMeasure=FGA'\
'&DateFrom=&DateTo=&Division=&EndPeriod=10&EndRange=28800&GROUP_ID=&GameEventID='\
'&GameID=&GameSegment=&GroupID=&GroupMode=&GroupQuantity=5&LastNGames=0&LeagueID=00'\
'&Location=&Month=0&OnOff=&OpponentTeamID=0&Outcome=&PORound=0&Period=0&PlayerID={PlayerID}'\
'&PlayerID1=&PlayerID2=&PlayerID3=&PlayerID4=&PlayerID5=&PlayerPosition=&PointDiff=&Position='\
'&RangeType=0&RookieYear=&Season={Season}&SeasonSegment=&SeasonType={SeasonType}'\
'&ShotClockRange=&StartPeriod=1&StartRange=0&StarterBench=&TeamID=0&VsConference='\
'&VsDivision=&VsPlayerID1=&VsPlayerID2=&VsPlayerID3=&VsPlayerID4=&VsPlayerID5='\
'&VsTeamID='.format(PlayerID=201935,Season='2017-18',SeasonType='Regular+Season')
header = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'\
' Chrome/62.0.3202.94 Safari/537.36'}
response = requests.get(shot_chart_url,headers=header)
# headers是模擬瀏覽器訪問行為,現在沒有這一項獲取不到數據
headers = response.json()['resultSets'][0]['headers']
shots = response.json()['resultSets'][0]['rowSet']
shot_df = pd.DataFrame(shots, columns=headers)
# View the head of the DataFrame and all its columns
from IPython.display import display
with pd.option_context('display.max_columns', None):
display(shot_df.head())
# Or
#shot_df.head().to_excel('outfile.xls',index=True,header=True)
我們得到的pandas DataFrame: shot_df,表頭及前3行數據展示如下:
shot_chart_url其中PlayerID、Season、SeasonType三項是可變參數,如果想獲得其他球員的PlayerID可以登錄nba.com/players搜索感興趣球員的名字,如下
點擊搜索結果,跳轉到頁面的網址最後一項既是PlayerID,例如: http://www.nba.com/players/giannis/antetokounmpo/203507中的 203507即是字母哥的PlayerID。
我們這一步得到的 shot_df包含了James Harden在2017-18賽季常規賽目前為止(20180219全明星賽)所有投籃嘗試。我們需要的數據為 LOC_X和 LOC_Y兩列,這些是每次投籃嘗試的坐標值,然後可以將這些坐標值繪製到代表籃球場的坐標軸上,當然我們可能還需要 EVENT_TYPE列,來區分投籃是否投進。
第二部分--繪製球員shot log到球場圖關於這一部分,How to Create NBA Shot Charts in Python已經做了非常優秀的工作,我們會延續其框架,並對代碼做修改以達到更好的適用性。
首先我們對上一步得到的James Harden的shot log LOC_X和 LOC_Y進行快速繪圖,看其X、Y是怎麼定義的。
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(4.3,4))
ax = fig.add_subplot(111)
ax.scatter(shot_df.LOC_X, shot_df.LOC_Y)
plt.show()
通過快速預覽圖我們對 LOC_X和 LOC_Y有了一個大概的認識,有一點需要注意: LOC_X其實是觀眾視野從中場面向籃筐來說的, LOC_X是正值則在籃筐的左邊。所以最終繪圖時需要按以下代碼做調整,我們以shot log中Right Side(R) 投籃區域(投籃區域劃分請參考前文 ShotZones圖)的出手作為示例說明。
right_shot_df = shot_df[shot_df.SHOT_ZONE_AREA == "Right Side(R)"]
other_shot_df = shot_df[~(shot_df.SHOT_ZONE_AREA == "Right Side(R)")]
fig = plt.figure(figsize=(4.3,4))
ax = fig.add_subplot(111)
ax.scatter(right_shot_df.LOC_X, right_shot_df.LOC_Y, s=1, c='red', label='Right Side(R)')
ax.scatter(other_shot_df.LOC_X, other_shot_df.LOC_Y, s=1, c='blue', label='Other AREA')
ax.set_ylim(top=-50,bottom=580)
ax.legend()
plt.show()
通過對 LOC_X和 LOC_Y數據的快速畫圖,我們大概知道了籃筐的位置大概就是 LOC_X和 LOC_Y的原點。知道了這一點,我們結合籃球半場的具體尺寸 (下圖)和比例就可以畫出籃球半場圖了。
通過上圖我們知道了籃球場寬度是 50FT,轉換成 INCH是 600IN,籃球場長 94FT,轉換成 INCH是 1128IN,再結合我們上一步畫出的投籃點快速預覽圖,通過對很多球員的 LOC_Y為0時, LOC_X與 SHOT_DISTANCE,我們能夠推測出 LOC_X和 LOC_Y的單位與 IN的換算大概為10/12。
畫圖函數如下:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Arc,Wedge
class data_linewidth_plot():
def __init__(self,**kwargs):
self.ax = kwargs.pop("ax", plt.gca())
self.lw_data = kwargs.pop("linewidth", 1)
self.lw = 1
#self.ax.figure.canvas.draw()
self.ppd=72./fig.dpi
self.trans = self.ax.transData.transform
self._resize()
def _resize(self):
lw = ((self.trans((1,self.lw_data))-self.trans((0,0)))*self.ppd)[1]
self.lw = lw
def draw_half_court(ax=None, unit=1):
lw = unit * 2 #line width
color = 'k'
# 精確line width
court_lw = data_linewidth_plot(ax = ax,linewidth = lw).lw
## Create the basketball hoop
#籃筐直徑(內徑)是18IN.,我們設置半徑為9.2IN刨除line width 0.2IN,正好為籃筐半徑
hoop = Wedge((0, 0), unit * 9.2, 0, 360, width=unit * 0.2, color='#767676')
hoop_neck = Rectangle((unit * -2, unit * -15 ), unit * 4, unit * 6, linewidth=None, color='#767676')
## Create backboard
#Rectangle, left lower at xy = (x, y) with specified width, height and rotation angle
backboard = Rectangle((unit * -36, unit * -15 ), unit * 72, court_lw, linewidth=None, color='#767676')
# List of the court elements to be plotted onto the axes
## Restricted Zone, it is an arc with 4ft radius from center of the hoop
restricted = Arc((0, 0), 96*unit+court_lw, 96*unit+court_lw, theta1=0, theta2=180,linewidth=court_lw, color='#767676', fill=False)
restricted_left = Rectangle((-48*unit-court_lw/2, unit * -15 ), 0, unit * 17, linewidth=court_lw, color='#767676')
restricted_right = Rectangle((unit*48+court_lw/2, unit * -15 ), 0, unit * 17, linewidth=court_lw, color='#767676')
# Create free throw top arc 罰球線弧頂
top_arc_diameter = 6 * 12 * 2*unit - court_lw
top_free_throw = Arc((0, unit * 164), top_arc_diameter, top_arc_diameter, theta1=0, theta2=180,linewidth=court_lw, color=color, fill=False)
# Create free throw bottom arc 罰球底弧
bottom_free_throw = Arc((0, unit * 164), top_arc_diameter, top_arc_diameter, theta1=180, theta2=0,linewidth=abs(court_lw), color=color, linestyle='dashed', fill=False)
# Create the outer box 0f the paint, width=16ft outside , height=18ft 10in
outer_box = Rectangle((court_lw/2 - unit*96, -court_lw/2 - unit*63), 192*unit-court_lw, 230*unit-court_lw, linewidth=court_lw, color=color, fill=False)
# Create the inner box of the paint, widt=12ft, height=height=18ft 10in
inner_box = Rectangle((court_lw/2 - unit*72, -court_lw/2 - unit*63), 144*unit-court_lw, 230*unit-court_lw, linewidth=court_lw, color=color, fill=False)
## Three point line
# Create the side 3pt lines, they are 14ft long before they begin to arc
corner_three_left = Rectangle((-264*unit+court_lw/2, -63*unit-court_lw/2), 0, 14*12*unit +court_lw, linewidth=court_lw, color=color)
corner_three_right = Rectangle((264*unit-court_lw/2, -63*unit-court_lw/2), 0, 14*12*unit +court_lw, linewidth=court_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_diameter = (23 * 12 + 9) * 2*unit - court_lw
three_arc = Arc((0, 0), three_diameter, three_diameter, theta1=21.9, theta2=158, linewidth=court_lw, color=color)
# Center Court
center_outer_arc = Arc((0, (94*12/2-63)*unit), 48*unit+court_lw, 48*unit+court_lw, theta1=180, theta2=0,linewidth=court_lw, color=color)
center_inner_arc = Arc((0, (94*12/2-63)*unit), 144*unit-court_lw, 144*unit-court_lw, theta1=180, theta2=0,linewidth=court_lw, color=color)
# Draw the half court line, baseline and side out bound lines
outer_lines = Rectangle((-25*12*unit - court_lw/2, -63*unit-court_lw/2), 50*12*unit+court_lw, 94/2*12*unit + court_lw, linewidth=court_lw, color=color, fill=False)
#2 IN. WIDE BY 3 FT. DEEP, 28 FT. INSIDE, 3FT. extenf onto court
court_elements = [hoop_neck, backboard, restricted, restricted_left, restricted_right,top_free_throw,bottom_free_throw,
outer_box,inner_box, corner_three_left,corner_three_right,three_arc,center_outer_arc,center_inner_arc,outer_lines]
# Add the court elements onto the axes
for element in court_elements:
ax.add_patch(element)
畫圖:
fig = plt.figure(figsize=(9,9))
ax = fig.add_subplot(111,aspect='equal')
ax.set_xlim(-330,330)
ax.set_ylim(-200,600)
draw_half_court(ax=ax)
plt.show()
添加上投籃出手位置數據
fig = plt.figure(figsize=(9,8))
ax = fig.add_subplot(111,aspect='equal')
ax.set_xlim(-330,330)
ax.set_ylim(top= -100,bottom = 500)
draw_half_court(ax=ax,unit=10/12)
df_missed = shot_df[shot_df.EVENT_TYPE=='Missed Shot'][['LOC_X','LOC_Y']]
ax.scatter(df_missed.LOC_X, df_missed.LOC_Y,s=2,color='r',label = 'Missed Shot',alpha=0.5)
df_made = shot_df[shot_df.EVENT_TYPE=='Made Shot'][['LOC_X','LOC_Y']]
ax.scatter(df_made.LOC_X, df_made.LOC_Y,s=2,color='b',label = 'Made Shot',alpha=0.5)
legend = ax.legend(bbox_to_anchor=(0.49, 0.13), loc=2, borderaxespad=0.,prop={'size':8},ncol=2,frameon=False)
plt.axis('off')
FG = "%.1f" % ((df_made.shape[0]/shot_df.shape[0])*100)
ax.text(-250,440,'FG%:{0}%({1}-{2})'.format(FG,df_made.shape[0],shot_df.shape[0]), fontsize=8)
ax.text(-250,-63,'FGA for Harden, James during the 2017-18 Regular Season'.format(FG,df_made.shape[0],shot_df.shape[0]), fontsize=8)
plt.show()
下圖是我的微信公眾號,會寫一些生物信息及編程相關的文章,歡迎關注。