iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
Python

上次介紹的棒球套件很少更新了,那就只好自己寫一個!?系列 第 18

Day 18 - 用 requests 取得 Raw Data 後使用 pandas 轉成 DataFrame

  • 分享至 

  • xImage
  •  

我們第一個 function,會是最有彈性的,能包含所有我們在前面幾天介紹的篩選參數,這樣之後再做延伸的 function 的時候,就能使用這個最初的 function 去達到我們想要的新增功能。今天這個 function 會需要用到 requestsio 以及 pandas 這個三個套件幫我們完成。

requests

requests 是一個 Python 套件,幫助我們去發送 HTTP 請求,可以透過這個套件去呼叫 API Endpoint 來取得伺服器資料,也可以使用他的參數 params 來幫我們組好 Search Params

安裝 requests

如果是使用 Colab 的話不用特別安裝,但如果在我們自己的環境的話,需要執行

pip install requests

使用 requests 呼叫 API

想要獲得 Statcast 資料,我們可以參考 pybaseball 的這一行 pybaseball/statcast.py#16pybaseball/datasources/statcast.py 所提供的 API Url,這個網址會產生一個 csv 檔讓我們下載,可以把它拆解成:

    • pathname: https://baseballsavant.mlb.com/statcast_search/csv
  • searchParams: ?all=true&......{一長串},但現在我們用 requests 就可以不用自己用字串去組,可以使用 dictionary 來組成 paramsrequests

有了這兩個我們就能寫成:

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.textresponse.text 會是被 encode 過的一段字串,會依照 API 伺服器設定的不同,而有不同的 encode 方式,資料格式也會不一樣。可以使用 response.encoding 來知道目前是使用哪個 encode。我們這次使用的 API 會回傳從 csv 檔轉成的 utf-8 encoding 字串,而如果我們要對這些資料進行操作的話,直接使用字串會不好使用,所以就需要把他轉乘 DataFrame 的型態。

DataFrame & io & pandas

DataFrame 是一種 Python 儲存資料的型態,他打開會像是我們在網路上看到的表格,或是 Excel 試算表,要獲得這樣的格式會需要 pandas 這個處理資料的套件,這個套件在之後也會頻繁地使用它。

安裝 pandas

pip install pandas

使用 pandas.read_csv 打開 csv 檔

接下來會需要 pandas 的功能 read_csv 來把我們用 requests 下載的 csv 檔,轉換成 pandasDataFrame 型態。不過透過 response.text 獲得的會是一個字串,沒辦法直接用 read_csv 來讀取,所以我們就會需要使用 io 裡的 StringIO 來把我們的字串轉成 Text Streamread_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,像是 FanGraphsLeaderboard API。這樣的話可以參考我在 pybaseball 開的 Pull Request,會使用到的則是 json 套件而不是 io,或是直接使用 response.json() 獲得 dictionary

Params

接下來處理那些篩選參數,第一版會先用一個陽春一點的配置,比較需要注意的是,如果是多選的選項,就算只有選擇一項,後面也要加 | 不然會搜尋不到結果。然後是搜尋球員 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
最後一樣感謝大家耐心地看完今天這篇文章,有任何問題與建議也歡迎留言告訴我,明天見,掰掰。


上一篇
Day 17 - Git 指令: branch, checkout
下一篇
Day 19 - 創建 Github Pull Request
系列文
上次介紹的棒球套件很少更新了,那就只好自己寫一個!?31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言