我們第一個 function,會是最有彈性的,能包含所有我們在前面幾天介紹的篩選參數,這樣之後再做延伸的 function 的時候,就能使用這個最初的 function 去達到我們想要的新增功能。今天這個 function 會需要用到 requests、io 以及 pandas 這個三個套件幫我們完成。
requests
是一個 Python 套件,幫助我們去發送 HTTP
請求,可以透過這個套件去呼叫 API Endpoint 來取得伺服器資料,也可以使用他的參數 params
來幫我們組好 Search Params
。
如果是使用 Colab 的話不用特別安裝,但如果在我們自己的環境的話,需要執行
pip install requests
想要獲得 Statcast 資料,我們可以參考 pybaseball 的這一行 pybaseball/statcast.py#16 跟 pybaseball/datasources/statcast.py 所提供的 API Url,這個網址會產生一個 csv
檔讓我們下載,可以把它拆解成:
https://baseballsavant.mlb.com/statcast_search/csv
?all=true&......{一長串}
,但現在我們用 requests
就可以不用自己用字串去組,可以使用 dictionary
來組成 params
給 requests
。有了這兩個我們就能寫成:
import requests
def statcast_search(params):
response = requests.get("https://baseballsavant.mlb.com/statcast_search/csv", params=params)
通常我們去取得 API 資料都會使用 HTTP
政策的 GET 來取得資料,他只需要 Url 跟 search params 就能完成大多數的 API 呼叫。如果請求發送成功的話,我們會得到一個 response
,裡面會包含一些重要資訊,像是這次回傳回來的 status code。如果是 200
的話就代表伺服器那邊有成功地給予回應,我們就能藉此寫一個條件式,來檢查我們是否成功獲得資料。
import requests
def statcast_search(params):
response = requests.get("https://baseballsavant.mlb.com/statcast_search/csv", params=params)
if response.status_code == 200:
return response.text
else:
raise Exception(
f"Failed to fetch data: {response.status_code} - {response.text}")
這段程式碼就代表,如果 status_code
回傳 200
,我們的 function 就會回傳 response.text
。response.text
會是被 encode
過的一段字串,會依照 API 伺服器設定的不同,而有不同的 encode
方式,資料格式也會不一樣。可以使用 response.encoding
來知道目前是使用哪個 encode
。我們這次使用的 API 會回傳從 csv
檔轉成的 utf-8
encoding 字串,而如果我們要對這些資料進行操作的話,直接使用字串會不好使用,所以就需要把他轉乘 DataFrame
的型態。
DataFrame
是一種 Python 儲存資料的型態,他打開會像是我們在網路上看到的表格,或是 Excel 試算表,要獲得這樣的格式會需要 pandas
這個處理資料的套件,這個套件在之後也會頻繁地使用它。
pip install pandas
接下來會需要 pandas 的功能 read_csv 來把我們用 requests
下載的 csv
檔,轉換成 pandas
的 DataFrame
型態。不過透過 response.text
獲得的會是一個字串,沒辦法直接用 read_csv
來讀取,所以我們就會需要使用 io
裡的 StringIO 來把我們的字串轉成 Text Stream
讓 read_csv
可以讀。
import requests
import io
import pandas as pd # 習慣把 pandas 改成簡寫 pd
def statcast_search(params):
response = requests.get("https://baseballsavant.mlb.com/statcast_search/csv", params=params)
if response.status_code == 200:
csv_content = io.StringIO(response.text)
return pd.read_csv(csv_content)
else:
raise Exception(
f"Failed to fetch data: {response.status_code} - {response.text}")
Raw Data 的取得流程大概告一個段落,因為 Statcast 的 API 是回傳一個 csv
,所以會使用到 io
。不過其他時候很多會是回傳 json
,像是 FanGraphs
的 Leaderboard API。這樣的話可以參考我在 pybaseball 開的 Pull Request,會使用到的則是 json
套件而不是 io
,或是直接使用 response.json()
獲得 dictionary
。
接下來處理那些篩選參數,第一版會先用一個陽春一點的配置,比較需要注意的是,如果是多選的選項,就算只有選擇一項,後面也要加 |
不然會搜尋不到結果。然後是搜尋球員 id
的時候,如果 pitchers_lookup[]
或 pitchers_lookup[]
為空值的話,也會造成整個搜尋結果搜不到東西,就會需要用判斷式去協助給予正確的數值。大概寫一下會變成這樣:
def statcast_search(season: str | list[str] = "2024", player_type: str = "pitcher",
game_type: str | list[str] = "R|", start_dt: str = "",
end_dt: str = "", month: str | list[str] = "",
pitchers_lookup: str | list[str] = "",
batters_lookup: str | list[str] = "",
team: str | list[str] = "",
opponent: str | list[str] = "") -> pd.DataFrame:
"""
Search for Statcast pitch-level data with custom filters.
Args:
season (str | list[str]): The season(s) to search for.
player_type (str): The type of player to search for.
game_type (str | list[str]): The game type(s) to search for.
start_dt (str): The start date in 'YYYY-MM-DD' format.
end_dt (str): The end date in 'YYYY-MM-DD' format.
month (str | list[str]): The month(s) to search for.
pitchers_lookup (str | list[str]): The pitcher(s) to search for.
batters_lookup (str | list[str]): The batter(s) to search for.
team (str | list[str]): The team(s) to search for.
opponent (str | list[str]): The opponent(s) to search for.
Returns:
pd.DataFrame: A DataFrame containing the Statcast pitch-level data.
"""
params = {
"all": "true",
"player_type": player_type,
"hfSea": season if type(season) == str else "|".join(season),
"hfGT": game_type if type(game_type) == str else "|".join(game_type),
"game_date_gt": start_dt,
"game_date_lt": end_dt,
"hfMo": month if type(month) == str else "|".join(month),
"hfTeam": team if type(team) == str else "|".join(team),
"hfOpponent": opponent if type(opponent) == str else "|".join(opponent),
"type": "details"
}
注意到的是可以在 params 的後面加 =
就會有預設值,然後我現在多選都會用 type
來判斷是否需要使用 |
來 join
,不過這樣的判斷會是很粗糙的,我們之後也還有可能會有像是 all
的字串要去給相對應的值,所以之後會需要另外寫個 function 來幫助判斷,不過因為篇幅問題,今天就先介紹到這邊。
今天我們開始了第一個 function,他的雛型也開發的差不多,明天接下來介紹怎麼使用 .gitignore
來幫助管理不需要上傳到 github 的檔案,還有怎麼在 Github 上面開啟 Pull Request,讓其他開發者可以幫我們做 Code Review。
最後一樣感謝大家耐心地看完今天這篇文章,有任何問題與建議也歡迎留言告訴我,明天見,掰掰。