エージェントシミュレーションで野球の二番強打者理論を簡易検証してみた



- 一時期話題になっていた二番に強打者を置くことで得点力が上がるという理論について検証してみました
- 巨人でいう二番坂本とかヤクルトでいう二番山田みたいなやつです
- だいぶ簡略化してシミュレーションしています


## 打者エージェント

- 打者エージェントは一打席ごとに打撃の結果を返すだけのエージェント
- inputは打率:`BA`、出塁率:`OBP`、長打率:`SLG`のみで、1打席ごとの戻り値は、単打：`single`、二塁打:`double`、三塁打:`triple`、本塁打:`homerun`、アウト:`out`、四球:`walk`のみで他の結果は想定しない
- 打撃結果は、インプットである打率、出塁率、長打率のみに依存し、守備側の影響は無視する
- 厳密には出塁率−打率＝四球率にはならないことは理解しているが、簡易化のためそのように扱う
- また安打数に占める単打と本塁打の割合は`IsoP`(=長打率−打率)と相関が強いので、回帰分析をして確率を計算（算出過程は省略しています）
- 三塁打はどの打者も安打数に占める割合が低く2022年度シーズンの規定到達者の平均が2％程度だったのでこの値を採用

```Python:batter.py
class Batter:

  def __init__(self,BA,OBP,SLG):

    self.BA = BA # 打率 / Batting Average
    self.OBP = OBP # 出塁率 / On Base Percentage
    self.SLG = SLG # 長打率 / Slugging Percentage
    self.IsoP = SLG - BA # Isolated Power

  def batter_box(self):

    # 戻り値は、安打:single、二塁打:double、三塁打:triple、本塁打:homerun、アウト:out、四球:walk
    X = rand()

    p_single = self.IsoP * (-1.3) + 0.85
    p_triple = 0.02
    p_homerun = self.IsoP * 1.15 - 0.05
    p_double = 1 - (p_single + p_triple + p_homerun)

    # 打席結果
    if X <= self.BA:

      batting_result = random.choices(["single","double","triple","homerun"],weights=[p_single,p_double,p_triple,p_homerun])[0]

    elif self.BA < X <= self.OBP:

      batting_result = "walk"

    else:

      batting_result = "out"

    return batting_result
```

## 野球エージェント
- 試合の一連の流れを処理するエージェント
- 変数の説明は以下の通り

|変数名|説明|
|-|-|
|inning|イニング数（1〜9）|
|outcount|アウト数（1〜3）|
|score|得点|
|first_base|一塁ランナー有無(1:有,0:無)|
|second_base|二塁ランナー有無(1:有,0:無)|
|third_base|三塁ランナー有無(1:有,0:無)|

ランナー有無と得点の処理部分は２進数でスマートに書けそうな気はしましたが、いまいち思いつかずで何かいい案ありましたら教えて下さい・・・・

```Python:BaseballGame.py
class BaseballGame:
  def __init__(self):

    self.inning = 1
    self.outcount = 0
    self.score = 0
    self.first_base = 0
    self.second_base = 0
    self.third_base = 0

  def play(self,batting_result,print_msg):

    if print_msg:
      print(str(self.inning) + "inning / " + str(self.outcount) + "out")
      print(batting_result)
    else:
      pass

    runner = str(self.first_base) + str(self.second_base) + str(self.third_base)

    if batting_result == "out":
      self.outcount += 1

      # 3アウトの場合、イニング進めてランナー削除
      if self.outcount == 3:
        self.outcount = 0
        self.inning += 1
        self.first_base, self.second_base, self.third_base = 0,0,0
      else:
        pass
    
    elif batting_result == "walk":

      # ランナーなし
      if runner == "000":
        self.first_base, self.second_base, self.third_base = 1,0,0
      
      # ランナー1塁
      elif runner == "100":
        self.first_base, self.second_base, self.third_base = 1,1,0

      # ランナー2塁
      elif runner == "010":
        self.first_base, self.second_base, self.third_base = 1,1,0

      # ランナー3塁
      elif runner == "001":
        self.first_base, self.second_base, self.third_base = 1,0,1

      # ランナー1,2塁
      elif runner == "110":
        self.first_base, self.second_base, self.third_base = 1,1,1

      # ランナー1,3塁
      elif runner == "101":
        self.first_base, self.second_base, self.third_base = 1,1,1

      # ランナー2,3塁
      elif runner == "011":
        self.first_base, self.second_base, self.third_base = 1,1,1

      # ランナー満塁
      elif runner == "111":
        self.first_base, self.second_base, self.third_base = 1,1,1
        self.score += 1

    elif batting_result == "single":

      # ランナーなし
      if runner == "000":
        self.first_base, self.second_base, self.third_base = 1,0,0
      
      # ランナー1塁
      elif runner == "100":
        self.first_base, self.second_base, self.third_base = 1,1,0

      # ランナー2塁
      elif runner == "010":
        self.first_base, self.second_base, self.third_base = 1,1,0

      # ランナー3塁
      elif runner == "001":
        self.first_base, self.second_base, self.third_base = 1,0,0
        self.score += 1

      # ランナー1,2塁
      elif runner == "110":
        self.first_base, self.second_base, self.third_base = 1,1,1

      # ランナー1,3塁
      elif runner == "101":
        self.first_base, self.second_base, self.third_base = 1,1,0
        self.score += 1

      # ランナー2,3塁
      elif runner == "011":
        self.first_base, self.second_base, self.third_base = 1,0,1
        self.score += 1

      # ランナー満塁
      elif runner == "111":
        self.first_base, self.second_base, self.third_base = 1,1,1
        self.score += 1

    elif batting_result == "double":

      # ランナーなし
      if runner == "000":
        self.first_base, self.second_base, self.third_base = 0,1,0
      
      # ランナー1塁
      elif runner == "100":
        self.first_base, self.second_base, self.third_base = 0,1,1

      # ランナー2塁
      elif runner == "010":
        self.first_base, self.second_base, self.third_base = 0,1,0
        self.score += 1

      # ランナー3塁
      elif runner == "001":
        self.first_base, self.second_base, self.third_base = 0,1,0
        self.score += 1

      # ランナー1,2塁
      elif runner == "110":
        self.first_base, self.second_base, self.third_base = 0,1,1
        self.score += 1

      # ランナー1,3塁
      elif runner == "101":
        self.first_base, self.second_base, self.third_base = 0,1,1
        self.score += 1

      # ランナー2,3塁
      elif runner == "011":
        self.first_base, self.second_base, self.third_base = 0,1,0
        self.score += 2

      # ランナー満塁
      elif runner == "111":
        self.first_base, self.second_base, self.third_base = 0,1,1
        self.score += 2

    elif batting_result == "triple":

      # ランナーなし
      if runner == "000":
        self.first_base, self.second_base, self.third_base = 0,0,1
      
      # ランナー1塁
      elif runner == "100":
        self.first_base, self.second_base, self.third_base = 0,0,1
        self.score += 1

      # ランナー2塁
      elif runner == "010":
        self.first_base, self.second_base, self.third_base = 0,0,1
        self.score += 1

      # ランナー3塁
      elif runner == "001":
        self.first_base, self.second_base, self.third_base = 0,0,1
        self.score += 1

      # ランナー1,2塁
      elif runner == "110":
        self.first_base, self.second_base, self.third_base = 0,0,1
        self.score += 2

      # ランナー1,3塁
      elif runner == "101":
        self.first_base, self.second_base, self.third_base = 0,0,1
        self.score += 2

      # ランナー2,3塁
      elif runner == "011":
        self.first_base, self.second_base, self.third_base = 0,0,1
        self.score += 2

      # ランナー満塁
      elif runner == "111":
        self.first_base, self.second_base, self.third_base = 0,0,1
        self.score += 3

    elif batting_result == "homerun":

      # ランナーなし
      if runner == "000":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 1
      
      # ランナー1塁
      elif runner == "100":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 2

      # ランナー2塁
      elif runner == "010":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 2

      # ランナー3塁
      elif runner == "001":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 2

      # ランナー1,2塁
      elif runner == "110":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 3

      # ランナー1,3塁
      elif runner == "101":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 3

      # ランナー2,3塁
      elif runner == "011":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 3

      # ランナー満塁
      elif runner == "111":
        self.first_base, self.second_base, self.third_base = 0,0,0
        self.score += 4

    if print_msg:
      print("score : " + str(self.score))
    else:
      pass

```

## シミュレーション実行

- 今回強打者としては、2022年シーズン三冠王のスワローズ村上選手、一般的な打者としては2021年シーズンのドラゴンズ京田選手の成績をインプットとして利用します
- ９人中８人を京田選手、１人を村上選手として打順を組み、村上選手の打順を１〜９番の9パターンでシミュレーション。１つの打順に対して10000試合シミュレーションして得点を記録していき、村上選手を2番に置くことで得点力が上がるかどうか検証します

※データは[NPB公式サイト](https://npb.jp/statistics/)から引用

```Python:main.py
# バッターエージェント準備
Kyoda = Batter(0.257,0.302,0.315) # 2021シーズン京田陽太
Murakami = Batter(0.318,0.458,0.71) # 2022シーズン村上宗隆

Batting_Order_original = [Kyoda,Kyoda,Kyoda,Kyoda,Kyoda,Kyoda,Kyoda,Kyoda,Kyoda]

#　結果格納用
result = []

# 村上選手の打順を1~9番の９パターンでシミュレーション
for x in range(9):

  # 打順セット
  Batting_Order = Batting_Order_original.copy()
  Batting_Order[x] = Murakami
  score_list = []

  for game_num in range(1000):
    BaseballGame = Baseball_Game()
    i = 0
    #print("Game" + str(game_num+1) + "Playball")
    while True:

      if i >8:
        i = 0
      else:
        pass

      batting_result = Batting_Order[i].batter_box()
      BaseballGame.play(batting_result,False)

      i += 1

      if BaseballGame.inning == 10 and BaseballGame.outcount == 0:
        #print("Game Set")
        score_list.append(BaseballGame.score)
        break
  
  result.append(score_list)
```

## 結果

- 乱数を使っており10,000回のシミュレーションでも微妙に安定しないものの、強打者を上位に置いたほうが良いということは何となく傾向として見られる。ただ、必ずしも二番というわけではなさそう
- 単純に二番に置けば良いという話ではなく、もう少し条件（たとえば出塁率が高いリードオフマンがいるなど）が必要と考えられる
- もう少しセイバーメトリクスの勉強をしてから少し精緻化してシミュレーションしてみます

|村上選手の打順|1番|2番|3番|4番|5番|6番|7番|8番|9番|
|-|-|-|-|-|-|-|-|-|-|
|1試合あたり平均得点|3.09|3.05|3.11|3.06|3.07|3.01|3.00|2.94|2.96|
|無得点試合の割合|0.148|0.150|0.155|0.156|0.155|0.168|0.166|0.172|0.170|
|5点以上試合の割合|0.163|0.161|0.167|0.165|0.169|0.160|0.157|0.150|0.153|