Lean Baseball

No Engineering, No Baseball.

StatcastデータとPlotlyを使って「打球の到達位置」を可視化する - オオタニサンの打球の行方は!?

先に言っておくとStatcastデータの仕様を翻訳・解説して例も作ったよという前回エントリーの続きです.

「メジャーリーガーの一挙手一投足を事細かに記録したデータ」であるStatcastは, つい先日10勝20ホーマーというベーブ・ルース以来の(ある種恐ろしい)記録を作ったオオタニサンの打球位置をこのような形で可視化できます.

オオタニサンの打球到達位置(2022/8/8までのデータ)

補助線(diamondは内野, lineはフェアゾーンの境界線)を加えた散布図(この例では打球の結果毎に色付けしています)でこのような表現が出来るのですが,

一見すると簡単そうに見えるこの可視化なのですが,

座標の仕様が若干特殊(かつわかりにくい)という難点があり, 苦労しました...

この苦労は二度と味わいたくないのと, 後に続く野球データ好き・スポーツデータ解析・分析をされる方の知見として残すためブログに記録として残そうと思います.

なお, この記事は可視化に際して最も参考になったこちらのエントリー*1を元に記載しています.

baseballwithr.wordpress.com

Plotly(Python)ではなく, Rで分析・可視化する方は上記エントリーがそのまま参考になりますが, ここではPlotlyを使った可視化を最終ゴールとします.

はじめに

Statcastそのものの解説は前のエントリーをご覧ください(読んでいない方はまずそちらに目を通していただけると幸いです).

また, Pythonの可視化FW「Plotly」を使いますが, Plotlyの解説は省略します, 気になる方は公式ページをご覧になるか, 「Pythonインタラクティブ・データビジュアライゼーション入門」をお読みいただくことをおすすめします.

これはめちゃいい本です, 強くおすすめします.

TL;DR

  • 打球到達位置の平面座標(hc_x, hc_y)は独自の座標系に従った数値であるため, 実際のフィールドに合わせた可視化は値の変換が必要.
  • hc_x, hc_y共に, ホームベースをゼロ地点とした座標系に変換した後, 2.5を掛ける事でfeet単位(の近似値)として表現ができる.
  • メートル表記したい場合はfeet変換後の値をm変換する(0.3048を掛ける).

ラインナップ

【おさらい】Statcast #とは

もう一度おさらいすると,

野球選手の「投球」「打撃」「守備」の「一挙手一投足」を事細かに記録したデータをいい感じに可視化したりデータとしてダウンロードできる仕組み

がStatcastであり, このエントリーで取り扱うデータはこのStatcastからダウンロードしたデータの可視化となります.

baseballsavant.mlb.com

(大切なので2回言いますが)こちらの解説・データ仕様についての話は前のエントリーに書いたのでこちらを御覧ください.

shinyorke.hatenablog.com

このエントリーでは必要なTipsに限定して解説します.

打球到達位置の可視化

打球到達位置の可視化のステップは以下の通りです.

  1. 打球到達位置の平面座標(hc_x, hc_y)をホームベースをゼロ地点(x=0, y=0)とした座標系に変換を行う.
  2. 変換した座標系をFeet(もしくはm)に変換を行う.
  3. グラフとして可視化する.

打球到達位置の平面座標はhc_x(横方向)hc_y(奥行き)で表現されますが, どちらも独自の座標系なので変換が必要です*2.

座標系の変換処理

CSVデータをスプレッドシートにしてもいいですし, PandasなどのDataframeにしてもOKですがいずれにせよ生データから変換して別の列として保存します.

ホームベースを基準点とした座標に変換

hc_x(横方向)hc_y(奥行き)が今回使う指標ですが, どちらもゼロ地点がホームベースではないので変換を行います.

  • hc_x - 125.42 これで横方向(X)のゼロ地点をホームベース位置にする.
  • 198.27 - hc_y これで奥行き(Y)のゼロ地点をホームベース位置にする.

これは単純に値を引き算するだけの処理で終わります.

Feet変換を行う

(x=0, y=0) == ホームベース位置 の座標系に変換したらこちらをFeet変換します.

参考にしたブログによると, hc_x, hc_yともに2.5を掛けるとFeetの近似値になるみたいです.

なので, 座標をFeetとして出したい時は以下のような感じになります.

  • (hc_x - 125.42) * 2.5 打球到達位置(横方向, Feet).
  • (198.27 - hc_y) * 2.5 打球到達位置(奥行き, Feet).

m単位にする場合は

メートル単位にするときは「フィートをメートルに変換」するなので, 「フィートの値に対して0.3048を掛ける」これだけです.

  • (hc_x - 125.42) * 2.5 * 0.3048 打球到達位置(横方向, m).
  • (198.27 - hc_y) * 2.5 * 0.3048 打球到達位置(奥行き, m).

ここまでの流れをPythonのPandasで丁寧にやるとこうなります.

import pandas as pd

df = pd.read_csv('StatcastのCSVデータ')

# 座標系を(0, 0) == ホームベース位置に
df['hc_x_center'] = round(df['hc_x'] - 125.42, 2)
df['hc_y_center'] = round(198.27 - df['hc_y'], 2)

# Feet変換
df['hc_x_center_ft'] = round(df['hc_x_center'] * 2.5, 2)
df['hc_y_center_ft'] = round(df['hc_y_center'] * 2.5, 2)

# m変換(Feetの値を使う)
df['hc_x_center_m'] = round(df['hc_x_center_ft'] * 0.3048, 2)
df['hc_y_center_m'] = round(df['hc_y_center_ft'] * 0.3048, 2)

可視化してみる

可視化はplotlyを使ってみました.

すでに座標に必要な値は出しているので後はシュッと書いてあげればOKです.

Feetで可視化

アメリカの単位系なので最初はFeetで試してみました.

オオタニサンの打球到達位置(Feet表記)

こちらの可視化コードは以下のとおりです.

# hit location

# 内野のDIAMOND大きさ定義
DIAMOND_X_FT = [0, -63, 0, 63,0]
DIAMOND_Y_FT = [0, 63, 126, 63, 0]

fig = px.scatter(
    df.sort_values('events'), 
    x="hc_x_center_ft", 
    y="hc_y_center_ft", 
    color='events', 
    hover_name="events", hover_data=["pitcher_name", "pitch_name"]
)

# 内野の描画
fig.add_trace(go.Scatter(
    name='diamond',
    x=DIAMOND_X_FT, 
    y=DIAMOND_Y_FT,
    # fillcolor='darkviolet',
    line_color='black')
)

# フェアゾーンのライン
fig.add_trace(go.Scatter(
    name='line',
    x=[-300, 0, 300], 
    y=[300, 0, 300],
    line_color='black',)
)


fig.update_layout(
    title='Ohtani, Shohei Batting, Location',
    autosize=False,
    width=1500,
    height=1200,
)
fig.update_xaxes(range=[-300, 300], title='hc_x(ft)', dtick=25)
fig.update_yaxes(range=[0, 540], title='hc_y(ft)', dtick=25)
fig.show()

参考にしたブログの可視化をそのまま移植して終わりました, ちゃんと描けてるので良さげです.

メートルにすると

普段見慣れているメートル表記にする場合はこんな感じです.

内野とライン, グラフの単位をメートルに合わせて微調整してあげればOKです.

メートル版

# hit location m

# 内野のDIAMOND大きさ定義
DIAMOND_X_M = [0, -19.2, 0, 19.2,0]
DIAMOND_Y_M = [0, 19.2, 38.4, 19.2, 0]

fig = px.scatter(
    df.sort_values('events'), 
    x="hc_x_center_m", 
    y="hc_y_center_m", 
    color='events', 
    hover_name="events", hover_data=["pitcher_name", "pitch_name"]
)

# 内野の描画
fig.add_trace(go.Scatter(
    name='diamond',
    x=DIAMOND_X_M, 
    y=DIAMOND_Y_M,
    # fillcolor='darkviolet',
    line_color='black')
)

# フェアゾーンのライン
fig.add_trace(go.Scatter(
    name='line',
    x=[-100, 0, 100], 
    y=[100, 0, 100],
    # fillcolor='darkviolet',
    line_color='black',)
)

fig.update_layout(
    title='Ohtani, Shohei Batting, Location',
    autosize=False,
    width=1500,
    height=1200,
    # hovermode='x unified'
)
fig.update_xaxes(range=[-100 , 100], title='hc_x(m)', dtick=40)
fig.update_yaxes(range=[0, 160], title='hc_y(m)', dtick=40)
fig.show()

これでちゃんとした距離感を把握しつつ, 打球到達位置をプロットすることが出来るようになりました.

結び

というわけで, 思ったより苦戦しましたがなんとか打球到達位置の可視化が出来るようになりました.

しかし, オオタニサンはもっと引っ張り気味に打ってるかと思いきや, 意外と打球が色んな所に飛んでてちょっとびっくりしました, これはこれで調べたいな...*3

というのが自分でデータ触ったり, 可視化出来るようになるとわかって楽しいので気になる方は(ちょっと苦労すると思いますが)ぜひお試しください.

最後までお読みいただきありがとうございました.

*1:Rによるセイバーメトリクス入門の元ネタブログでもあります.

*2:一見するとFeetっぽい数字でそのままFeetと思って変換したりしたのですが, 変な値しか出なくて怪しいと思い調べたら, 参考にしたブログに行き着きました...これは知らないとわからんちょっとした罠です.

*3:考えてみればたまに綺麗な流し打ちとか見るし「引っ張りそこねた打球がいい感じに流し打ってシングル」とかあるんだろうなと思ったり.