たまにゃんのメモ帳

情報系関連のメモ書きを主に載せていきます。あわよくば他の人の参考になれば...

読売巨人軍の坂本勇人はなぜ併殺が少ないのか?

元ネタは坂本勇人3129打席24併殺打wwwwwwwwww

読売巨人軍坂本勇人は右打者であるにも関わらずなぜ併殺が少ないのだろうか?このまとめは2013年に更新されたもので、2017年開幕前のデータでは5499打席49併殺打という結果になっている。

ゲッツーが多いと名高い広島東洋カープ新井貴浩は8379打席231併殺打という結果であり、坂本と比べると明らかに併殺打の割合が大きい。新井は坂本に比べておおよそ3倍のペースで併殺を生み出している。 ちなみに俊足で左打者で内野安打が多い中日ドラゴンズ大島洋平は3833打席25併殺で0.7倍ほど坂本より少ない。

坂本がなぜ俊足左打者の大島と比べても遜色がないくらいに併殺が少ないのかをデータを集めて検証したいと思う。

データセットを集める

NPBは公式に打席の結果の詳細まで公開していないので、ヌルデータさん拝借する。データの収集はScrapyを利用した。幸い坂本は2007年から一軍のキャリアをスタートしているので2016年までの全ての打席の成績を手にすることができた。

2007~2016までの坂本勇人全打席(5499打席)のデータを収集する。HTMLのパースは個人的にBeautifulSoupの方が扱いやすいのでこちらを利用する。

# -*- coding: utf-8 -*-
import scrapy
import re
from bs4 import BeautifulSoup
from atbat.items import AtbatItem
import mojimoji

RBI_ELIMINATION = re.compile("\(.*\)")
SHIRT_NUMBER_ELIMINATION = re.compile("[0-9]+")
YEAR_EXTRACTION = re.compile("([0-9]{4,4})")

class NulldataSpider(scrapy.Spider):
    name = "nulldata"
    allowed_domains = ["lcom.sakura.ne.jp"]
    start_urls = [
        'http://lcom.sakura.ne.jp/NulData/2007/Central/C/f/25_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2015/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2014/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2013/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2012/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2011/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2010/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2009/Central/G/f/6_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2008/Central/G/f/61_stat_all.htm',
        'http://lcom.sakura.ne.jp/NulData/2007/Central/G/f/61_stat_all.htm',
    ]

    def parse(self, response):
        soup = BeautifulSoup(response.body)
        # Get Year
        m = YEAR_EXTRACTION.search(response.url)
        year = "2016"
        if m is not None:
            year = m.group(0)
        # Get player_name
        spans = soup.find_all("span", attrs={"style": "font-size:14pt"})
        if len(spans) == 0:
            raise Exception("player name is not found!! Fix matching algorithm")
        player_name = SHIRT_NUMBER_ELIMINATION.sub('', spans[0].text).strip()

        # Get at bat results
        trs = soup.find_all("tr", attrs={"onmouseover": "M_over(this)"})
        for tr in trs:
            divs = tr.find_all("div", attrs={"align": "left"})
            tds = tr.find_all("td")
            if len(divs) == 1 and len(tds) >= 1:
                days = tds[0].text.replace('\r\n', '').replace('\n', '').split('/')
                month = days[0]
                day = days[1]
                results = divs[0].text.replace('\r\n', '').replace('\n', '')
                results = [RBI_ELIMINATION.sub('', i) for i in results.split(',')]

                for i, v in enumerate(results):
                    item = AtbatItem()
                    item['player_name'] = player_name
                    item['day'] = day
                    item['month'] = month
                    item['year'] = year
                    item['atBat'] = "{}".format(i + 1)
                    item['result'] = mojimoji.han_to_zen(v)
                    yield item
# -*- coding: utf-8 -*-
import scrapy


class AtbatItem(scrapy.Item):
    player_name = scrapy.Field() # 選手名
    year = scrapy.Field() # 年
    month = scrapy.Field() # 月
    day = scrapy.Field() # 日
    atBat = scrapy.Field() # 第何打席
    result = scrapy.Field() # 結果

データを取得し終えるとこのような csv ができる。

player_name atBat day month result year
坂本勇人 1 29 3 右飛 2013
坂本勇人 2 29 3 中飛 2013
坂本勇人 3 29 3 遊併打 2013
坂本勇人 1 30 3 右2 2013
坂本勇人 2 30 3 中安 2013
坂本勇人 3 30 3 遊併打 2013
坂本勇人 4 30 3 三ゴロ 2013
坂本勇人 5 30 3 空三振 2013
坂本勇人 6 30 3 四球 2013
坂本勇人 1 31 3 遊ゴロ 2013

iPython Notebookで集計

打席結果をグラフで見る

まずはそれぞれの打席結果の累計を見てみる。

%matplotlib inline
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('ggplot')
font = {'family': 'Ricty Diminished'}
plt.rc('font', **font)

df = pd.read_csv("csv/sakamoto_atbat.csv")
resultDf = pd.DataFrame(dataset['result'].value_counts())
resultDf[resultDf['result'] > 100].plot(kind="bar") # 結果が100回以上のものを出力

最も空三振の数が多いのは大抵の選手に共通するが、中飛、右飛の多さに目が行く。「ぷぷぷしゃかもとまた打ち上げてる」、「ポップフライ 坂本」と言われる要因はここにあるのだろう。

sakamoto_result.png

打席結果を分類してみる

多少強引な方法だが以下のように打席結果を分類する

# ダブルプレー
def is_double_play(x):
    return '併' in x

# ゴロアウト ※併殺は除く
def is_ground_ball_out(x):
    return 'ゴロ' in x \
    and '併' not in x \
    and '安' not in x

# フライアウト ※併殺は除く
def is_fly_ball_out(x):
    return '飛' in x \
    and '犠' not in x \
    and '併' not in x \
    and '失' not in x \
    and '安' not in x \
    and '本' not in x

# ライナーアウト ※併殺は除く
def is_line_drive_ball_out(x):
    return '直' in x \
    and '併' not in x \
    and '失' not in x \
    and '安' not in x \
    and '左直2' not in x and '左直3' not in x \
    and '中直2' not in x and '中直3' not in x \
    and '右直2' not in x and '右直3' not in x


# 三振
def is_strike_out(x):
    return '三振' in x or '振逃' in x

# 犠飛 or 犠打
def is_sacrifice(x):
    return '犠' in x

# 四死球
def is_BB_or_HBP_out(x):
    return '四球' in x \
    or '敬遠' in x \
    or '死球' in x

# 野選
def is_fielders_choice(x):
    return '野選' in x

# エラー
def is_error(x):
    return '失' in x and '犠' not in x

# Hit
def is_hit(x):
    return '本' in x or '安' in x \
    or '中2' in x or '中3' in x \
    or '左2' in x or '左3' in x \
    or '右2' in x or '右3' in x \
    or '左直2' in x or '左直3' in x \
    or '中直2' in x or '中直3' in x \
    or '右直2' in x or '右直3' in x

df['is_double_play'] = df.result.apply(is_double_play)
df['is_ground_ball_out'] = df.result.apply(is_ground_ball_out)
df['is_fly_ball_out'] = df.result.apply(is_fly_ball_out)
df['is_line_drive_ball_out'] = df.result.apply(is_line_drive_ball_out)
df['is_strike_out'] = df.result.apply(is_strike_out)
df['is_sacrifice'] = df.result.apply(is_sacrifice)
df['is_BB_or_HBP_out'] = df.result.apply(is_BB_or_HBP_out)
df['is_fielders_choice'] = df.result.apply(is_fielders_choice)
df['is_error'] = df.result.apply(is_error)
df['is_hit'] = df.result.apply(is_hit)

次に三振以外でアウトになった場合の打球傾向について調べる。

三振以外のアウトカウント = 打数 - 三振 - ヒット - エラー - 野選

安打を考慮しない理由は、中安と書かれていても、ゴロかフライかライナーかどうかが分からないためである。{

double_play_count = df['is_double_play'].value_counts()[True]
ground_ball_out_count = df['is_ground_ball_out'].value_counts()[True]
fly_ball_out_count = df['is_fly_ball_out'].value_counts()[True]
line_drive_ball_out_count = df['is_line_drive_ball_out'].value_counts()[True]
strike_out_count = df['is_strike_out'].value_counts()[True]
sacrifice_count = df['is_sacrifice'].value_counts()[True]
BB_or_HBP_out_count = df['is_BB_or_HBP_out'].value_counts()[True]
fielders_choice_count =  df['is_fielders_choice'].value_counts()[True]
error_count = df['is_error'].value_counts()[True]
hit_count = df['is_hit'].value_counts()[True]

atbat = len(df) # 打席数
atbats = atbat - (sacrifice_count + BB_or_HBP_out_count) # 打数
out_count_without_strike_out = atbats - is_hit_count \
                                - strike_out_count \
                                - is_error_count \
                                - fielders_choice_count # 三振以外のアウト数

三振以外のアウトの打球傾向の割合を出す。

print("Double Play Count / Out Count without Strike Out = {0:.1f}%".format(100 * (double_play_count / (out_count_without_strike_out))))
print("Fly Ball Out count / Out Count without Strike Out = {0:.1f}%".format(100 * (fly_ball_out_count / (out_count_without_strike_out))))
print("Ground Ball Out count / Out Count without Strike Out = {0:.1f}%".format(100 * (ground_ball_out_count / (out_count_without_strike_out))))
print("The Others count / Out Count without Strike Out = {0:.1f}%".format(100 * (1 - ((double_play_count + fly_ball_out_count + ground_ball_out_count) / (out_count_without_strike_out)))))
Double Play Count / Out Count without Strike Out = 1.2%
Fly Ball Out count / Out Count without Strike Out = 40.9%
Ground Ball Out count / Out Count without Strike Out = 24.9%
The Others count / Out Count without Strike Out = 32.9%

坂本は三振以外のアウトの42.5%がフライアウトという事が分かった。これだけフライの傾向が多ければ、右打者であろうと併殺チャンスを回避することができると思われる。

新井貴浩大島洋平との比較

同じように新井貴浩大島洋平のデータを集めてみると、「各プレイヤーの三振以外でアウトになった打席結果の割合」は以下のような結果になった。なお併殺、ゴロアウト、フライアウト以外はThe Othersとなる。これを見ると他の選手と比べて、坂本のゴロアウトの少なさが分かるだろう。大島の一塁到達速度は坂本より(おそらく)速いがゴロアウトの割合が坂本に比べて多いので、ボールインプレー時には坂本より併殺になる可能性が高い。

out_result.png

結論

坂本勇人の併殺数が少ない理由はフライボールヒッター(とそこそこ速いであろう一塁到達タイム1)のおかげだと推測される。

参考

中日の大島は本当にセカンドゴロ製造機なのか (2015年の打席結果データを作りました)


  1. 定かではないので明言はさける