2026年5月28日 星期四

製作分析棒球的資料庫

 


作者:
楊先民
  
精誠資訊/恆逸教育訓練中心資深講師


※網路引用請註明完整出處


本期是一個臨時興起的主題,看完精典賽 WBC之後,對於情蒐這件事情我覺得非常重要,而且我在上課時就一直在講資料分析,投打對決,佈陣防守,這些原本可以讓一個實力比較沒這麼強的隊,能夠擊敗強隊(例如日本)。


設計OLTP資料庫

要設計一個用來分析職棒球員「每個打席(Plate Appearance, PA)」的資料庫,核心概念是要將靜態的資料檔(球員、球隊)動態的事件(比賽、打席、甚至單球紀錄)進行合理的解耦與關聯。

為了滿足資料分析(如進階數據 Sabermetrics、左右投分拆、得點圈打擊率等)的需求,這裡建議採用正規化的關連式資料庫架構。而且使用 OLTP設計資料庫,顧名思義就是方便球探或是記錄人員輸入逐球資訊。


核心資料表設計

主檔類(Dimensions

球隊資料表(Teams)

● TeamID (INT, PK): 球隊代碼

● TeamName (VARCHAR): 球隊名稱

● Location (VARCHAR): 主場城市

球員資料表(Players)

● PlayerID (INT, PK): 球員代碼

● PlayerName (VARCHAR): 球員姓名

● PrimaryPosition (VARCHAR): 主要守備位置

● Bats (CHAR): 投打習慣-打擊 (L: 左打, R: 右打, S: 兩打)

● Throws (CHAR): 投打習慣-投球 (L: 左投, R: 右投)


賽事與事件類(Facts

比賽資料表(Games)

● GameID (INT, PK): 比賽代碼

● GameDate (DATE): 比賽日期

● Season (INT): 賽季年份 ( 2026)

● Venue (VARCHAR): English/中文 球場名稱

● HomeTeamID (INT, FK): 主隊 ID

● AwayTeamID (INT, FK): 客隊 ID

● HomeScore (INT): 主隊最終得分 (完賽後更新)

● AwayScore (INT): 客隊最終得分 (完賽後更新)


打席紀錄表 (Plate_Appearances) —— 核心事實表
這個資料表是分析的靈魂,必須記錄下打席發生當下的時空背景(情境)與最終結果

欄位名稱

資料型態

屬性

說明

PA_ID

BIGINT

PK

打席唯一代碼

GameID

INT

FK

關聯到 Games

Inning

TINYINT

局數 (1~9+)

InningHalf

CHAR(1)

上下半局 (T: 上半局, B: 下半局)

BatterID

INT

FK

打者球員 ID

PitcherID

INT

FK

投手球員 ID

BatterSequence

TINYINT

打序 (1~9)

OutsBeforePA

TINYINT

打席開始前的出局數 (0, 1, 2)

RunnerOn1st

BIT/BOOLEAN

打席開始時一壘是否有人 (0/1)

RunnerOn2nd

BIT/BOOLEAN

打席開始時二壘是否有人 (0/1)

RunnerOn3rd

BIT/BOOLEAN

打席開始時三壘是否有人 (0/1)

PAResult

VARCHAR(10)

打席結果代碼 (如:1B, 2B, 3B, HR, BB, SO, GO, FO...)

RBI

TINYINT

該打席產生的打點數

RunsScored

TINYINT

該打席打者是否得分 (0/1)

PitchCount

TINYINT

該打席總投球數

進階擴充:單球紀錄表 (Pitch_Records)

如果未來規劃引入類似 Statcast 的進階軌跡分析,或者想分析「兩好球後的打擊率」,就需要建立打席下層的單球紀錄

單球紀錄表 (Pitch_Records)

● PitchID (BIGINT, PK): 單球唯一代碼

● PA_ID (BIGINT, FK): 關聯到打席紀錄

● PitchNumber (TINYINT): 該打席的第幾球 (1, 2, 3...)

● Balls (TINYINT): 投球前的壞球數 (0~3)

●  Strikes (TINYINT): 投球前的好球數 (0~2)

● PitchType (VARCHAR): 球種 ( FF: 四縫線, SL: 滑球, CH: 變速球)

● Velocity (DECIMAL): 球速 (mph km/h)

● PitchResult (VARCHAR): 該球結果 (S: 空振, C: 看著好球, B: 壞球, F: 界外, X: 擊入球場)


資料分析維度與索引(Indexing)最佳化策略


為了讓分析查詢(如計算特定球員在「得點圈有人、面對左投」的打擊率)跑得動,索引的設計至關重要:

1.       複合索引 (Composite Index)Plate_Appearances 表上,建立 (BatterID, PitcherID) 的複合索引。因為大量的分析都是圍繞在「特定打者的表現」或「對戰特定投手」。

2.       情境分析索引: 建立 (RunnerOn2nd, RunnerOn3rd, OutsBeforePA) 的索引,這能讓系統瞬間撈出得點圈有人兩出局後的關鍵打席。

3.       分拆數據 (Splits) 要分析左打對左投(Left-on-Left),可以透過 Plate_Appearances 內部關聯到 Players 表的 Bats Throws 欄位。


分析查詢範例 (SQL)

計算特定打者 (假設 ID 9527) 得點圈有人時的打擊率 (BA)

SELECT

    COUNT(CASE WHEN PAResult IN ('1B', '2B', '3B', 'HR') THEN 1 END) * 1.0 /

    NULLIF(COUNT(CASE WHEN PAResult IN ('1B', '2B', '3B', 'HR', 'GO', 'FO', 'SO', 'E') THEN 1 END), 0) AS RISP_BA

FROM Plate_Appearances

WHERE BatterID = 9527

  AND (RunnerOn2nd = 1 OR RunnerOn3rd = 1);


架構擴充與演進方向

如果是個人或小型專案,傳統的關連式資料庫(如 SQL Server, PostgreSQL)非常適合處理這類結構化且關聯性強的資料。

當資料量隨賽季累積到數百萬筆(包含單球與軌跡數據)時,可以考慮將打席與單球紀錄表獨立出來,採用星狀模型(Star Schema)轉化為資料倉儲架構,將常用維度(如打者習慣、球隊)直接反正規化(Denormalization)灌入事實表中,以空間換取分析查詢的時間。


App介面該怎麼設計?(如何讓紀錄員流暢輸入)

在實際球場上,紀錄員是不可能用「打字」來記逐球的。App 的設計必須依循「防呆、自動化、全圖像化」三個原則。

介面設計核心:狀態機(State Machine)

App 畫面應該是一個棒球場的九宮格或點擊盤。

欄位自動帶入:當你設定好這一局的打者與投手,App 就會自動鎖定這兩個 ID,紀錄員不需要每球重複選取。

好壞球自動累計:只要畫面上點擊「S(好球)」或「B(壞球)」,系統底層的 Balls Strikes 就會自動變更(例如:0B-1S 變成 0B-2S)。當達到 3 壞球再點壞球、或 2 好球再點空振時,App 自動跳出打席結束的選單(保送或三振),根本不需要人工計算。

進壘點與球種:直接畫一個九宮格(代表好球帶),紀錄員只要在格子內點一下(記錄進壘點),再從旁邊的快速選單點選「直球/滑球/曲球」,一秒鐘就能完成一球的紀錄。


逐球資訊(Pitch_Records)的實體資料表設計

在傳統資料庫中,這樣的資訊其實佔用的空間非常小。我們來看看精簡且效率極高的實體欄位設計:

CREATE TABLE Pitch_Records (

    PitchID BIGINT IDENTITY(1,1) PRIMARY KEY, -- 自動遞增的主鍵

    PA_ID BIGINT NOT NULL,                    -- 關聯到外鍵打席表

    PitchNumber TINYINT NOT NULL,             -- 該打席第幾球 (1, 2, 3...)

    CurrentBalls TINYINT NOT NULL,            -- 投球前壞球數 (0~3)

    CurrentStrikes TINYINT NOT NULL,          -- 投球前好球數 (0~2)

    PitchType CHAR(2) NOT NULL,               -- 球種代碼 (FF:直球, SL:滑球, CH:變速)

    PitchResult CHAR(1) NOT NULL,             -- 該球結果 (S:揮空, C:接殺/看著好球, B:壞球, F:界外, X:擊中球)

    ReleaseSpeed DECIMAL(4,1) NULL,           -- 球速 ( 145.5)

    LocationX SMALLINT NULL,                  -- 進壘點 X 座標 (像素或公分)

    LocationY SMALLINT NULL                   -- 進壘點 Y 座標

);


不需要擔心「記不完」或「硬碟爆掉」?

我們來精算一下一整年的資料量:

單場比賽:兩隊合計約 300

單季資料量(以中華職棒 360 場為例)$360 \times 300 = 108,000$ 筆逐球資料。

單季資料量(以美國職棒 MLB 2,430 場為例)$2,430 \times 300 = 729,000$ 筆逐球資料。

在資料庫的世界裡,73 萬筆資料是非常微小的數字

以上述的資料結構來看,一筆 Pitch 紀錄佔用的記憶體大約只有 30 50 個位元組(Bytes

● 一年 73 萬筆資料,實際的資料檔案大小大約只有 35 MB 50 MB

● 就算加上索引(Index),一張極限容量的手機記憶卡,就能裝下整個大聯盟十幾年來的逐球紀錄。

所以,資料庫絕對「記的完」,而且對現代任何一個輕量級資料庫(如 SQLite, PostgreSQL)來說,處理這種等級的查詢都是輕而易舉。


資料庫如何擴充「落點欄位」?

當打席結果是安打、球場內出局(飛球/滾地球)或失誤時(即 PitchResult = 'X' 擊中球),我們必須記錄兩個核心物理資訊:落點座標擊球型態

建議在 Plate_Appearances(打席表)中增加以下欄位:

ALTER TABLE Plate_Appearances ADD

    HitLocationX DECIMAL(5,2) NULL,  -- 擊球落點 X 座標 (以本壘板為原點 0,0)

    HitLocationY DECIMAL(5,2) NULL,  -- 擊球落點 Y 座標

    HitType CHAR(2) NULL,            -- 擊球型態 (GB: 滾地球, FB: 飛球, LD: 平飛球, PU: 內野高飛球)

    ExitVelocity DECIMAL(4,1) NULL,  -- 擊球初速 (mph km/h,非必要,有更好)

    LaunchAngle DECIMAL(3,1) NULL;   -- 擊球仰角 (度數,非必要,有更好)


座標系統怎麼記?

App 介面上,紀錄員不需要輸入數字。App 畫面會呈現一個棒球場縮圖,當球落地時,紀錄員只要在棒球場的對應位置「點一下」,App 就會自動把螢幕點擊的 $(X, Y)$ 相對座標轉換後存入資料庫。


防守佈陣的分析實務(SQL 與資料庫運用)

有了這些數據後,教練團或防守球員在賽前會怎麼撈資料?

情境:我們今天要對付強力拉打的左打者 (假設 ID 1001)

內野手想知道:「這個打者打出滾地球時,通常都滾向哪裡?我們該怎麼移防?」

SQL 查詢範例:

SELECT HitLocationX, HitLocationY, COUNT(*) as HitCount

FROM Plate_Appearances

WHERE BatterID = 1001

  AND HitType = 'GB' -- 只看滾地球 (Ground Ball)

  AND PAResult IN ('1B', 'GO', 'E') -- 安打、滾地出局、失誤

GROUP BY HitLocationX, HitLocationY;


後端如何轉化為防守決策?

資料庫回傳一堆座標後,系統會在前端把這些點畫在球場圖上,形成「打擊落點熱區圖 (Spray Chart)」:

1.       內野佈陣:如果撈出來的資料顯示,左打者有 85% 的滾地球都落在二壘與一壘之間(右半邊)。App 畫面就會顯示一個強烈的紅色熱區。教練看到圖後,就會下達指令:「游擊手站到二壘後方,二壘手往一壘線邊大後防移防」

2.       外野佈陣:如果撈出該打者面對「右投手、兩好球後」的飛球(FB)有極高比例是右外野深遠飛球。外野手就可以在兩好球後,集體向右後方後退 5 10 步。


更進階的佈陣分析:情境交叉篩選

真正的職棒分析不會只看整體的落點,因為打者面對不同的球速、球種,打出去的方位完全不同。因為你的資料庫有把「打席」跟「逐球」關聯起來,你可以做到非常恐怖的交叉分析:

對戰球速篩選:篩選投手球速在 145 km/h 以上時,該打者的擊球落點(通常會因為跟不上球速,落點往相反方向位移,例如右打者變成推打到右外野)。

球數情境篩選:球數領先(打者設定打直球) vs 球數落後(打者被動破壞滑球)的落點差異。


最後

看來中華隊應該是有這套系統,所以能夠在大賽中打敗強勁的對手吧,還想分析什麼有趣的資料庫呢?

 

0 意見:

張貼留言