見出し画像

DHあり vs なし、東京ヤクルトスワローズの得点力はどう変わる? ~シミュレーションによる得点を比較~ part②

前回に引き続き、東京ヤクルトスワローズの得点力がDHの有無によってどのように変化するのかを、Pythonを用いたシミュレーションで評価、検証します。前回の記事(Part①)では、シミュレーションを実施する準備段階として、分析に必要となるデータの取得、整形を行う作業を実施し、各選手の打撃指標を確率モデルとして表現する準備を行いました。
part②の本記事では、part①で得られたデータを用いて実際にシミュレーションを実施し、9イニングの攻撃を行った場合の総得点を算出する方法について記載します(背景や経緯等は前回触れたので、それらは省略し、本記事ではいきなり具体的な内容から始めます)。

1. 分析方法

part①から繰り返しになりますが、まずは今回のゴールをまとめます。
今回のシミュレーションの目的は、1~9番打者からなる任意の打線を指定した場合に、その打線で9イニングの攻撃を行った際の総得点をPythonを用いて算出することです。なお、打撃のシミュレーションにあたっては、各打者の打撃結果を実際の打撃成績に基づいた確率でランダムに決定し、試合の進行に応じて得点を加算するようなシステムを構築します(part①で算出した各事象の確率(single%, double%, triple%, hr%, walk%, out%)に従います)。

また、野球のルールや試合の仕組みをモデル化することで、アウトカウントや塁上の走者等の試合状況、それに伴う得点を管理しますが、ここでは以下のようなルールで状況が更新されるモデルを想定します。

※ルール※

  • 各イニングの初期状態を0アウト、ランナーなしとする。

  • 3アウトになるまでに本塁に生還(4進塁)したランナー1人につき、1得点が加算される。

  • 打者の打撃結果は、安打(シングルヒット)、二塁打、三塁打、本塁打、四死球、アウトのいづれかとなる。

  • 打撃結果が安打、二塁打、三塁打、本塁打の場合、打者は1塁打につき、1進塁し、すでに塁上にランナーがいる場合も同様に1塁打につき、1進塁する。

  • 打撃結果が四死球で、1塁にランナーがいない場合、打者は1塁に進み、その他の状況は変化しない。

  • 打撃結果が四死球で、1塁にランナーがいる場合、打者は1塁に進み、押し出される形となるランナーは1進塁する。

  • 3アウトになるとそのイニングは終了となる。

  • 翌イニングでは、前イニングの続きの打順から攻撃が開始される。

  • 計9イニングの攻撃が行われる。

以上のルールと打撃結果を組み合わせて、ランナーの位置や得点の計算を管理することで、シミュレーションを実施します。

2. 実装

import pandas as pd
import numpy as np
import sys

# 打撃指標取得
df = pd.read_excel("C:/baseball/ys_2024打撃成績_example.xlsx")
pitcher = ['ピッチャー', 0.100, 0.000, 0.000, 0.000, 0.020, 0.880]
df_pitcher = pd.DataFrame([pitcher], columns=df.columns)
df = pd.concat([df, df_pitcher], ignore_index=True)
#print(df)

# 打線指定
players = ['塩見泰隆', '西川遥輝', 'オスナ', '村上宗隆', '山田哲人', 'サンタナ', '中村悠平', '長岡秀樹', 'ピッチャー']

# 打線の人数チェック
if len(players) != 9:
    print("指定した打線の人数が9人ではありません。")
    sys.exit()

# 指定した打者がdfに存在するかチェック
df_players = df['選手名'].tolist()
for player in players:
    if player not in df_players:
        print(f"選手名:{player} のデータが存在しません。")
        sys.exit()

stats = pd.DataFrame(columns=df.columns)
for player in players:
    stats = pd.concat([stats, df[df['選手名'].isin([player])]], ignore_index=True)
#print(stats)

# シミュレーション開始
score = 0
current_batter_idx = 0  # 現在の打者のインデックス

for inning in range(9):
    outs = 0
    bases = {'1st': False, '2nd': False, '3rd': False}  # 走者状況の管理

    print(f"\n=== {inning + 1}回の攻撃 ===")

    while outs < 3:
        batter = stats.iloc[current_batter_idx]

        probabilities = [
            batter['single%'],
            batter['double%'],
            batter['triple%'],
            batter['hr%'],
            batter['walk%'],
            batter['out%']
        ]
        
        outcomes = ['single', 'double', 'triple', 'hr', 'walk', 'out']
        result = np.random.choice(outcomes, p=probabilities)
        print(f"{current_batter_idx + 1}番打者 {batter['選手名']}: {result}")

        if result == 'out':
            outs += 1
            print(f"凡退。 アウトカウント: {outs}")
        else:
            runs = 0
            if result == 'single':
                if bases['3rd']:
                    runs += 1
                bases['3rd'] = bases['2nd']
                bases['2nd'] = bases['1st']
                bases['1st'] = True
            elif result == 'double':
                if bases['3rd']:
                    runs += 1
                if bases['2nd']:
                    runs += 1
                bases['3rd'] = bases['1st']
                bases['2nd'] = True
                bases['1st'] = False
            elif result == 'triple':
                runs += sum(bases.values())
                bases['3rd'] = True
                bases['2nd'] = False
                bases['1st'] = False
            elif result == 'hr':
                runs += sum(bases.values()) + 1
                bases['3rd'] = False
                bases['2nd'] = False
                bases['1st'] = False
            elif result == 'walk':
                if bases['1st']:
                    if bases['2nd']:
                        if bases['3rd']:
                            runs += 1
                        bases['3rd'] = True
                    bases['2nd'] = True
                bases['1st'] = True

            score += runs
            print(f"走者状況: 1塁={bases['1st']}, 2塁={bases['2nd']}, 3塁={bases['3rd']}, 追加得点: {runs}")
        current_batter_idx = (current_batter_idx + 1) % 9

print(f"\n最終得点: {score}")

簡単ですが全体的な補足説明を加えておきます。
まず最初に、part①にて作成したExcelファイルを参照し、各選手の打撃指標を取得します。なお、このデータには野手の打撃指標のみが記録されているため、DHなしのシミュレーションを行う際は投手の打撃指標を追加する必要があります。ここでは、投手の打撃指標は別途定義し、これを野手の打撃指標に結合します(投手の打撃指標は、2024年の投手の打撃指標平均値として、single%:0.100, double%:0.000, triple%:0.000, hr%:0.000, walk%:0.020, out%:0.880を用いました)。

次に、1~9番打者からなる任意の打線を設定します(ここでは、2024年の開幕戦のスタメン打順として、「1番塩見選手、2番西川選手、3番オスナ選手、4番村上選手、5番山田選手、6番サンタナ選手、7番中村選手、8番長岡選手、9番ピッチャー」と設定しました)。
なお、9人の選手が指定されていない場合や、打撃指標のデータがない選手が入力されている場合はエラーを出力させます。

次に攻撃のシミュレーション部分に関して、まずは初期状態を設定します。

  • 現在の得点数:0点

  • 現在の打者:1番(インデックスは0番)

また、各イニングの初期状態を設定します。

  • アウトカウント:0アウト

  • 走者状況:ランナーなし

なお、今回は走者状況を、
bases = {'1st': False, '2nd': False, '3rd': False}
と辞書型で定義し、1, 2, 3塁それぞれにおいて、Falseなら走者なし、Trueなら走者ありとして管理します。

その後は、現在の打者に対応するデータから確率指標に基づいた打撃結果を出力し、「※ルール※」に従うように分岐先の試合状況や得点を更新した後、次の打者を指定する流れとなっています。

3. 結果

今回は、シミュレーションの結果(試合の進行状況)をターミナルにテキストとして表示するようにしました。実行結果を確認すると、以下のように各回の攻撃内容やその結果、得点等がうまく出力できていることが確認できました(内容をすべて載せると長くなるため、一例として序盤と終盤の攻撃回の様子を示しました)。

1回に4安打で1得点!
9回に1四死球と3安打で1得点!
1試合(9イニング)で計4得点!

4. まとめ

本記事では、指定した打線での9イニングの攻撃をシミュレーションし、実際の打撃指標に基づいた得点計算を行える仕組みを構築しました。また、シミュレーションの動作を確認した結果、攻撃の進行や得点計算が正しく行われていることが確認できました。

続くpart③では、DH制の有無による得点の違いを具体的に比較・分析し、DHが得点力に与える影響を定量的に評価していきます。

いいなと思ったら応援しよう!