記事のシリーズ
- pybaseball基礎編① pybaseballのライブラリの概要とインストール
- pybaseball基礎編② シーズン単位のデータ取得
- pybaseball基礎編③ 試合単位のデータ取得
- pybaseball基礎編④ 打席単位のデータ取得【本記事】
- pybaseball基礎編⑤ 1球単位のデータ取得(予定)
- pybaseball基礎編⑥ 打球単位のデータ取得(予定)
- pybaseball基礎編⑦ 投球単位のデータ取得(予定)
はじめに
前回の「pybaseball基礎編③」では、game_pkを使ってMLBの1試合分のStatcastデータを取得しました。
Statcastのデータは基本的に1球につき1行で記録されています。
そのため、1打席を分析するには、複数の投球データを「同じ打席」という単位でまとめて考える必要があります。
そこで今回は、at_bat_numberとpitch_numberを中心に、打席単位でStatcastデータを見る方法を解説します。
この記事では、以下の内容を扱います。
- 1球単位のデータと打席単位のデータの違い
at_bat_numberの意味pitch_numberの意味- 1打席分の投球を取り出す方法
eventsとdescriptionの違い- 初球・最終球を確認する方法
- 1打席に何球投げられたかを集計する方法
- 大谷翔平選手の打席を例に分析する方法
- 打席単位のデータ分析で注意したいポイント
1. Statcastデータは「1球=1行」
まず、前回の記事で取得したgame_dataを確認します。
from pybaseball import statcast_single_game
game_data = statcast_single_game(game_pk=777490)
display(game_data.head())
出力結果
| index | pitch_type | game_date | release_speed | release_pos_x | release_pos_z | player_name | batter | pitcher | events | description | spin_dir | spin_rate_deprecated | break_angle_deprecated | break_length_deprecated | zone | des | game_type | stand | p_throws | home_team |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 202 | FF | 2025-06-16 00:00:00 | 93.5 | -2.44 | 5.35 | Wade, Tyler | 642180 | 489446 | field_out | hit_into_play | NaN | NaN | NaN | NaN | 2 | Tyler Wade pops out to third baseman Max Muncy. | R | L | R | LAD |
| 215 | FS | 2025-06-16 00:00:00 | 86.7 | -2.47 | 5.17 | Wade, Tyler | 642180 | 489446 | NaN | called_strike | NaN | NaN | NaN | NaN | 13 | NaN | R | L | R | LAD |
| 221 | FF | 2025-06-16 00:00:00 | 93.0 | -2.46 | 5.21 | Wade, Tyler | 642180 | 489446 | NaN | foul | NaN | NaN | NaN | NaN | 3 | NaN | R | L | R | LAD |
| 231 | FS | 2025-06-16 00:00:00 | 85.9 | -2.39 | 5.18 | Iglesias, Jose | 578428 | 489446 | field_out | hit_into_play | NaN | NaN | NaN | NaN | 9 | Jose Iglesias lines out to shortstop Mookie Betts. | R | R | R | LAD |
| 241 | FF | 2025-06-16 00:00:00 | 93.4 | -2.35 | 5.38 | Iglesias, Jose | 578428 | 489446 | NaN | foul | NaN | NaN | NaN | NaN | 11 | NaN | R | R | R | LAD |
Statcastのデータでは、基本的に1回の投球が1行として記録されています。
例えば、ある打席で3球投げられた場合、概念的には次のようになります。
| at_bat_number | pitch_number | pitch_name | description |
|---|---|---|---|
| 10 | 1 | 4-Seam Fastball | called_strike |
| 10 | 2 | Slider | ball |
| 10 | 3 | Changeup | hit_into_play |
3行ありますが、これは3打席ではありません。
同じat_bat_numberなので、1つの打席に3球投げられたことを表しています。
Baseball SavantのStatcast CSV仕様では、at_bat_numberは「その試合における打席番号」、pitch_numberは「その打席における投球番号」と定義されています。(Statcast Search CSV Documentation)
2. at_bat_numberとは?
at_bat_numberは、その試合の中で何番目の打席かを表す番号です。
例えば、
at_bat_number = 25
なら、その試合の25番目の打席という意味です。
重要なのは、at_bat_numberが投球番号ではないことです。
同じ打席に5球投げられた場合、
at_bat_number
25
25
25
25
25
のように、5球すべて同じat_bat_numberになります。
一方、pitch_numberは、
1
2
3
4
5
となります。
つまり、
at_bat_number
↓
「何番目の打席?」
pitch_number
↓
「その打席の何球目?」
となります。
3. pitch_numberとは?
pitch_numberは、その打席の中で何球目に投じられた球なのかを表します。
例えば、
| at_bat_number | pitch_number | pitch_name |
|---|---|---|
| 12 | 1 | 4-Seam Fastball |
| 12 | 2 | Slider |
| 12 | 3 | Changeup |
| 12 | 4 | Curveball |
なら、
12番目の打席で、4球の投球があった
ということになります。
4. 打席単位でデータを見る
例えば、大谷翔平選手の打撃データをohtani_dataとして取得しているとします。
まず、打席番号と投球番号を確認します。
OHTANI_ID = 660271
ohtani_data = game_data[
game_data["batter"].astype(str) == str(OHTANI_ID)
].copy()
ohtani_pitches = (
ohtani_data[
[
"game_date",
"inning",
"at_bat_number",
"pitch_number",
"pitch_name",
"release_speed",
"events",
"description"
]
]
.sort_values(
["at_bat_number", "pitch_number"]
)
)
display(ohtani_pitches)
出力結果
| index | game_date | inning | at_bat_number | pitch_number | pitch_name | release_speed | events | description |
|---|---|---|---|---|---|---|---|---|
| 296 | 2025-06-16 00:00:00 | 1 | 6 | 1 | Slider | 89.0 | NaN | ball |
| 286 | 2025-06-16 00:00:00 | 1 | 6 | 2 | 4-Seam Fastball | 99.5 | NaN | called_strike |
| 276 | 2025-06-16 00:00:00 | 1 | 6 | 3 | Slider | 90.6 | NaN | called_strike |
| 266 | 2025-06-16 00:00:00 | 1 | 6 | 4 | Slider | 90.4 | NaN | ball |
| 253 | 2025-06-16 00:00:00 | 1 | 6 | 5 | Slider | 91.4 | NaN | foul |
| 239 | 2025-06-16 00:00:00 | 1 | 6 | 6 | Slider | 91.3 | NaN | ball |
| 229 | 2025-06-16 00:00:00 | 1 | 6 | 7 | Slider | 92.4 | strikeout | swinging_strike |
| 75 | 2025-06-16 00:00:00 | 3 | 22 | 1 | Slider | 92.1 | NaN | blocked_ball |
| 73 | 2025-06-16 00:00:00 | 3 | 22 | 2 | Slider | 91.5 | double | hit_into_play |
| 31 | 2025-06-16 00:00:00 | 4 | 36 | 1 | 4-Seam Fastball | 99.5 | NaN | foul |
| 30 | 2025-06-16 00:00:00 | 4 | 36 | 2 | 4-Seam Fastball | 98.3 | single | hit_into_play |
| 217 | 2025-06-16 00:00:00 | 6 | 53 | 1 | Slider | 88.7 | NaN | swinging_strike |
| 208 | 2025-06-16 00:00:00 | 6 | 53 | 2 | Sinker | 96.1 | NaN | called_strike |
| 195 | 2025-06-16 00:00:00 | 6 | 53 | 3 | Slider | 88.8 | NaN | blocked_ball |
| 192 | 2025-06-16 00:00:00 | 6 | 53 | 4 | Sinker | 95.4 | strikeout | swinging_strike |
| 121 | 2025-06-16 00:00:00 | 8 | 69 | 1 | Curveball | 83.2 | NaN | swinging_strike |
| 118 | 2025-06-16 00:00:00 | 8 | 69 | 2 | 4-Seam Fastball | 96.4 | NaN | ball |
| 113 | 2025-06-16 00:00:00 | 8 | 69 | 3 | 4-Seam Fastball | 97.3 | NaN | ball |
| 111 | 2025-06-16 00:00:00 | 8 | 69 | 4 | Curveball | 84.1 | NaN | ball |
| 107 | 2025-06-16 00:00:00 | 8 | 69 | 5 | Slider | 87.1 | walk | ball |
ここでポイントになるのが、
.sort_values(
["at_bat_number", "pitch_number"]
)
です。
これは、
- 打席の順番
- その打席の中での投球順
という順番でデータを並べています。
Statcastのデータ取得処理でも、pybaseballはgame_date、game_pk、at_bat_number、pitch_numberなどを使ってデータを並べる処理を行っています。(pybaseball GitHub)
5. 1打席だけを取り出す
すべての打席を見ると行数が多くなるので、まず1つの打席だけ取り出してみましょう。
例えば、
target_at_bat = 53
one_pa = ohtani_data[
ohtani_data["at_bat_number"] == target_at_bat
].copy()
display(
one_pa[
[
"game_date",
"inning",
"at_bat_number",
"pitch_number",
"pitch_name",
"release_speed",
"events",
"description"
]
].sort_values("pitch_number")
)
これで、指定した打席の投球だけを確認できます。
例えば、
| index | game_date | inning | at_bat_number | pitch_number | pitch_name | release_speed | events | description |
|---|---|---|---|---|---|---|---|---|
| 217 | 2025-06-16 00:00:00 | 6 | 53 | 1 | Slider | 88.7 | NaN | swinging_strike |
| 208 | 2025-06-16 00:00:00 | 6 | 53 | 2 | Sinker | 96.1 | NaN | called_strike |
| 195 | 2025-06-16 00:00:00 | 6 | 53 | 3 | Slider | 88.8 | NaN | blocked_ball |
| 192 | 2025-06-16 00:00:00 | 6 | 53 | 4 | Sinker | 95.4 | strikeout | swinging_strike |
のようなデータになります。
この場合、
- 1球目:空振りストライク
- 2球目:見逃しストライク
- 3球目:捕手が止めたボール
- 4球目:空振り三振
という流れを確認できます。
6. eventsとdescriptionの違い
打席分析では、eventsとdescriptionの違いを理解しておくことが重要です。
Baseball SavantのCSV仕様では、
events→ 打席の結果description→ その投球の結果
として定義されています。(Statcast Search CSV Documentation)
例えば、
description = ball
description = called_strike
description = swinging_strike
description = hit_into_play
などは、その1球がどうなったかを見るための情報です。
一方、
events = single
events = double
events = home_run
events = strikeout
events = walk
events = field_out
などは、打席の最終的な結果を見るための情報です。
7. 打席の最後の球を取り出す
打席結果を確認するときには、最後の投球を見る方法も便利です。
例えば、
last_pitch = (
one_pa
.sort_values("pitch_number")
.iloc[-1]
)
display(
last_pitch[
[
"pitch_number",
"pitch_name",
"release_speed",
"events",
"description"
]
]
)
出力結果
pitch_number 4
pitch_name Sinker
release_speed 95.4
events strikeout
description swinging_strike
これで、その打席の最後の投球を確認できます。
eventsに打席結果が記録されていれば、
single
double
home_run
strikeout
walk
field_out
など、打席の結果を確認できます。
8. 1打席に何球投げられたか調べる
次に、打席ごとの投球数を集計してみます。
ここではat_bat_numberごとに行数を数えます。
pa_pitch_count = (
ohtani_data
.groupby("at_bat_number")
.size()
.reset_index(name="pitch_count")
)
display(pa_pitch_count)
例えば、
| index | at_bat_number | pitch_count |
|---|---|---|
| 0 | 6 | 7 |
| 1 | 22 | 2 |
| 2 | 36 | 2 |
| 3 | 53 | 4 |
| 4 | 69 | 5 |
のようになります。
これだけでも、
「この試合では、何球粘った打席があったのか?」
といった分析ができます。
9. 打席結果も一緒にまとめる
さらに、打席ごとの最終結果をまとめてみましょう。
pa_summary = (
ohtani_data
.sort_values(["at_bat_number", "pitch_number"])
.groupby("at_bat_number")
.agg(
pitch_count=("pitch_number", "count"),
result=("events", "last")
)
.reset_index()
)
display(pa_summary)
例えば、
| index | at_bat_number | pitch_count | result |
|---|---|---|---|
| 0 | 6 | 7 | strikeout |
| 1 | 22 | 2 | double |
| 2 | 36 | 2 | single |
| 3 | 53 | 4 | strikeout |
| 4 | 69 | 5 | walk |
のような表を作れます。
これによって、**「何球投げられた打席で、どんな結果になったのか」**を比較できます。
10. 初球を分析する
打席分析では、初球を見るのも面白いポイントです。
各打席のpitch_number == 1を抽出してみます。
first_pitches = (
ohtani_data[
ohtani_data["pitch_number"] == 1
]
[
[
"game_date",
"inning",
"at_bat_number",
"pitch_name",
"release_speed",
"description"
]
]
.sort_values("at_bat_number")
)
display(first_pitches)
出力結果
| index | game_date | inning | at_bat_number | pitch_name | release_speed | description |
|---|---|---|---|---|---|---|
| 296 | 2025-06-16 00:00:00 | 1 | 6 | Slider | 89.0 | ball |
| 75 | 2025-06-16 00:00:00 | 3 | 22 | Slider | 92.1 | blocked_ball |
| 31 | 2025-06-16 00:00:00 | 4 | 36 | 4-Seam Fastball | 99.5 | foul |
| 217 | 2025-06-16 00:00:00 | 6 | 53 | Slider | 88.7 | swinging_strike |
| 121 | 2025-06-16 00:00:00 | 8 | 69 | Curveball | 83.2 | swinging_strike |
これで、
大谷翔平選手の各打席で、初球にどんな球が投げられたのか
を確認できます。
さらに球種ごとに集計することもできます。
first_pitch_type = (
ohtani_data[
ohtani_data["pitch_number"] == 1
]
.groupby("pitch_name")
.size()
.reset_index(name="count")
.sort_values("count", ascending=False)
)
display(first_pitch_type)
出力結果
| index | pitch_name | count |
|---|---|---|
| 2 | Slider | 3 |
| 0 | 4-Seam Fastball | 1 |
| 1 | Curveball | 1 |
11. 打席単位の分析で便利な列
ここまで使った列を整理しておきます。
| 列 | 意味 | 打席分析での用途 |
|---|---|---|
game_date |
試合日 | 試合の識別 |
inning |
イニング | 打席が発生した場面 |
at_bat_number |
試合内の打席番号 | 打席の識別 |
pitch_number |
打席内の投球番号 | 投球順の確認 |
pitch_name |
球種 | 球種分析 |
release_speed |
投球速度 | 球速分析 |
events |
打席結果 | 打席結果の分析 |
description |
その投球の結果 | ボール・ストライク・空振りなどの分析 |
launch_speed |
打球速度 | 打球分析 |
launch_angle |
打球角度 | 打球分析 |
hit_distance_sc |
Statcastによる推定飛距離 | 打球の飛距離分析 |
at_bat_numberとpitch_numberは、「打席」と「投球」を結びつけるための基本的な列です。
12. 打席単位で分析すると何が見えてくるのか
打席単位でデータを整理できるようになると、分析の幅が一気に広がります。
例えば、
初球の傾向
初球にストレートが多い
初球に変化球が多い
打席の長さ
平均何球で打席が終わるか
長い打席がどれくらいあるか
最終球
どの球種で打席が終わることが多いか
打席結果
single
double
home_run
walk
strikeout
field_out
などを調べられます。
さらに次のステップでは、
「その打席の中で、どの球に対してスイングしたのか?」
「どのカウントでどの球種が投げられたのか?」
「打球になった球の速度や角度はどうだったのか?」
といった、より細かい分析につなげられます。
13. 今回覚えておきたいコード
この記事のポイントをまとめると、まずは次のコードを覚えておけば十分です。
打席の投球順に並べる
ohtani_pitches = (
ohtani_data
.sort_values(
["at_bat_number", "pitch_number"]
)
)
display(ohtani_pitches)
1打席を抽出する
target_at_bat = 53
one_pa = ohtani_data[
ohtani_data["at_bat_number"] == target_at_bat
].sort_values("pitch_number")
display(one_pa)
打席ごとの投球数を数える
pa_pitch_count = (
ohtani_data
.groupby("at_bat_number")
.size()
.reset_index(name="pitch_count")
)
display(pa_pitch_count)
打席ごとの結果をまとめる
pa_summary = (
ohtani_data
.sort_values(["at_bat_number", "pitch_number"])
.groupby("at_bat_number")
.agg(
pitch_count=("pitch_number", "count"),
result=("events", "last")
)
.reset_index()
)
display(pa_summary)
まとめ
今回は、pybaseballで取得したStatcastデータを打席単位で分析する方法を紹介しました。
特に重要なのは、次の2つです。
at_bat_number
↓
その試合の何番目の打席か
pitch_number
↓
その打席の何球目か
Statcastのデータは1球=1行ですが、at_bat_numberを使うことで複数の投球を1つの打席としてまとめて見ることができます。
また、
events
↓
打席の結果
description
↓
その投球の結果
という違いも重要です。
この違いを理解すると、
「1打席で何球投げられたか」
「初球は何だったか」
「最後の球は何だったか」
「どの球種で打席が終わったか」
といった分析ができるようになります。
そして、複数試合をまとめて分析するときには、
groupby(["game_pk", "at_bat_number"])
のように、試合を識別するgame_pkと打席番号を組み合わせることも重要です。
ここまでできれば、単に「試合のデータを取得する」段階から、1つ1つの打席を時系列で分析する段階へ進めます。
次回の記事
次回の「pybaseball基礎編⑤」では、さらに細かく1球単位のデータ取得・分析に進みます。
例えば、
- 1球ごとのボール・ストライクを確認する
- 投球ごとの球種を調べる
- 投球速度を比較する
pitch_numberを使って投球順を分析するdescriptionから投球結果を確認する- 1打席の中で投球がどのように変化したかを見る
といった内容を扱う予定です。
打席単位の分析から1球単位の分析へ進むことで、Statcastデータの構造をさらに詳しく理解できるようになります。
データ参照元
本記事では、MLBのStatcastデータをpybaseballから取得して分析しています。
Statcastのデータ項目については、MLBのBaseball Savantが公開しているStatcast Search CSV Documentationを参照しています。
Statcast Search CSV Documentation
pybaseballは、Baseball SavantのStatcastデータをPythonから取得し、pandasのDataFrameとして扱いやすくするためのライブラリです。
データを利用・公開する場合は、利用元の最新の規約、データ提供条件、各サービスの利用条件を確認してください。
動作環境
本記事ではGoogle Colabを利用しています。
Pythonとpybaseballのバージョンは、以下のコードで確認できます。
import sys
import pybaseball
print("Python:", sys.version)
print("pybaseball:", pybaseball.__version__)
出力結果
Python: 3.13.15 (main, Aug 6 2026, 11:06:22) [GCC 13.3.0]
pybaseball: 2.2.7