iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0

今天要來看 pybaseball 的原始碼,來知道他們是怎麼取得大聯盟的 Statcast 資料。

Leaderboard

首先想先介紹怎麼獲得 Savant 上面的 Leaderboard 資料,一樣會先從 init.py 開始找相關的檔案。有用到 Leaderboard 的有 statcast_batter.pystatcast_pitcher.pystatcast_fielding.pystatcast_running.py。他們獲得資料的方法都差不多,可以挑一個進去看。不過要注意裡面的 statcast_batterstatcast_pitcher 會跟其他的不太一樣。

這邊我用 statcast_pitcher.py 裡的 statcast_pitcher_spin_dir_comp 來當作例子介紹。

一開始會先定義參數還有回傳值的 type,參數在前面的章節有介紹就不多闡述,有一個我在 FanGraphs 的原始碼篇忘了提到,就是 pybaseball methods 回傳型態都會是 Pandas 的 DataFrame。Pandas 在使用 Python 數據分析很常用到,可以在之前的鐵人賽上找到介紹:

認識強大的Python套件:Pandas(上) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天 (ithome.com.tw)

def statcast_pitcher_spin_dir_comp(
		year: int, # 沒有預設值的就會是必填
		pitch_a: str = "FF", # 可以看到有些參數會有預設值
		pitch_b: str = "CH",
		minP: int = 100,
		pitcher_pov: bool = True
) -> pd.DataFrame: # 回傳 Pandas DataFrame

接下來會是之前作者的註解,可以幫助我們了解這個 function 有哪些參數跟做什麼的,因為是 Open Source 的 pacakage,這樣的註解能讓其他開發者快速知道這段程式碼在幹嘛。

註解看完,會根據 method 的不同,會需要整理一下參數,像是 statcast_pitcher_spin_dir_comp 就會需要確定傳入的球種字串是合法的,會用 norm_pitch_code 這個 function 去檢查:

# 這三行都是整理等一下要呼叫 API 的參數
pitch_a = norm_pitch_code(pitch_a, to_word=True)
pitch_b = norm_pitch_code(pitch_b, to_word=True)
pov = "Pit" if pitcher_pov else "Bat"

norm_pitch_code 可以在 util.py 這個檔案裡找到。這個檔案包含許多用來整理 Statcast method 裡資料的 functions,也可以從裡面找到需要用到的常數像是隊伍簡寫跟球員位置簡寫,還有現在要找的球種簡寫。

def norm_pitch_code(pitch: str, to_word: bool = False) -> str:
	# 從球種全名像是 `Curveball` 轉換成簡寫 `CU` pitch 需在 pitch_name_to_code_map
	# 裡是 key 不分大小寫
	normed = pitch_name_to_code_map.get(pitch.upper())
	# 如果 to_word 是 True 把簡寫轉成全名
	normed = pitch_code_to_name_map.get(normed) if to_word and normed else normed
	# 如果無法從上述 map 裡找到資料的話 throw Error
	# 會根據是不是 `all` 有不同的 Error Message
	if normed is None:
		if pitch.lower() == 'all':
			raise ValueError("'All' is not a valid pitch in this particular context!")
		raise ValueError(f'{pitch} is not a valid pitch!')
	return normed

等參數都調整完,會組成我們用來獲得資料的 url,可以發現這個 URL 是用 Baseball Savant 的 leaderboard 的網址所組成,只不過最後會帶 &csv,也就是說我們也可以直接用這個 url 獲得 csv 檔,可以試試看。

取得 url 後再來會用 python 內建的 requests package 來發送 API request,拿到 response 後再透過 Pandas 的 read_csv 轉成 DataFrame 並處理掉欄位裡的空白。

url = f"https://baseballsavant.mlb.com/leaderboard/spin-direction-comparison?year={year}&type={pitch_a} / {pitch_b}&min={minP}&team=&pov={pov}&sort=11&sortDir=asc&csv=true"
res = requests.get(url, timeout=None).content
data = pd.read_csv(io.StringIO(res.decode('utf-8'))) # 回傳的資料需要做 `utf-8`解碼
# sanitize_statcast_columns 會把 columns 裡多餘的空白消除
data = sanitize_statcast_columns(data)
return data

這樣就完成了 Statcast Leaderboard 的 function,其他通常都是一些參數處理不太一樣,但大致上都差不多。

statcast_batter, statcast_pitcher

前面有提到這兩個 functions 跟其他 leaderboard functions 不一樣,主要是他們用的頁面是 Savant 的 Search 頁來獲得資料,程式碼的最大差別就是他們會呼叫一個 split_request function。他用來把 requests 切成多個,以防一次選取的時間範圍太大,需要下載的資料一次太龐大。使用的手法是把傳入的時間區間,用 while 迴圈,以 60 天為單位來一段一段獲取資料後再用 .concat 接起來。

def split_request(start_dt: str, end_dt: str, player_id: int, url: str) -> pd.DataFrame:
	"""
	Splits Statcast queries to avoid request timeouts
	"""
	# 時間參數整理
	current_dt = datetime.strptime(start_dt, '%Y-%m-%d')
	end_dt_datetime = datetime.strptime(end_dt, '%Y-%m-%d')
	results = []  # list to hold data as it is returned
	player_id_str = str(player_id)	# 把球員 ID 轉成字串
	print('Gathering Player Data')
	# break query into multiple requests
	while current_dt <= end_dt_datetime:
		remaining = end_dt_datetime - current_dt
		# delta 時間區間最大 60 天
		# increment date ranges by at most 60 days
		delta = min(remaining, timedelta(days=2190))
		next_dt = current_dt + delta
		start_str = current_dt.strftime('%Y-%m-%d')
		end_str = next_dt.strftime('%Y-%m-%d')
		# retrieve data
		data = requests.get(url.format(start_str, end_str, player_id_str))
		df = pd.read_csv(io.StringIO(data.text))
		# 獲得一段後先存到 results array
		# add data to list and increment current dates
		results.append(df)
		# 再到下一段時間區間
		current_dt = next_dt + timedelta(days=1)
	return pd.concat(results) # 把 results 裡的資料接起來

statcast

這個 function 跟 statcast_batterstatcast_pitcher 一樣是使用 Savant 的 Search 頁獲得資料,不過因為他是取得所有比賽的 Statcast 資料,所以資料容易過於龐大,因此寫法又更上述兩個 functions 不一樣。

主要獲得資料的程式碼會在 _handle_request 裡,裡面會用 tqdm 這個套件管理進度條,然後如果有開啟 parallel,就會使用 Python 的 concurrent.futures 進行多執行緒處理。

另外在 Day 14 - Statcast 其他篇 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天 (ithome.com.tw) 裡我提到 verbose 的功用,我今天在調查程式碼的時候發現我那天解釋有誤,如果 verboseTrue,那天應該是會多其他文字敘述讓使用者知道詳細執行的狀況。

最後再提醒一點,裡面會用到 statcast_data_range 來定義抓取的時間範圍,從 2008-2020 會跳過休賽季,但因為設定常數 STATCAST_VALID_DATES 沒在更新,所以 2020 之後會連休賽季一起包含進去。

本日小結

今天整理了 pybaseball 怎麼獲得 Statcast 資料的程式碼,這次比上次 FanGraphs 程式碼介紹,多引用了他們實際的程式碼做介紹,希望這樣能讓大家更清楚點。一樣再次感謝大家耐心地看完,明天接下來會介紹另一個數據網站,Baseball Reference。如果程式碼有任何問題歡迎留言告訴我,我會盡我所能回答。


上一篇
Day 15 - Statcast 進階數據解釋
下一篇
Day 17 - Baseball Reference 介紹
系列文
Python 棒球數據分析套件 pybaseball 介紹30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言