製作分析棒球的資料庫

作者:楊先民
精誠資訊/恆逸教育訓練中心資深講師
※網路引用請註明完整出處
本期是一個臨時興起的主題,看完精典賽 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 意見:
張貼留言