乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

シミュレーション

今回は老後の資産をどうやって取り崩すか
資産取り崩しのシミュレーションです


解説動画はこちら




老後資産取り崩し


老後の資産を年利 X % で運用しつつ
毎月 X 万円取り崩していく際の
資産推移をシミュレーションします。


資産金額、年利、リスク確率から
破産確率がどれくらいになるのか
これを導き出します。



モンテカルロシミュレーション


資産取り崩しの際のシミュレーションに使うのが
モンテカルロシミュレーションです。

これはサイコロをたくさん振って
結果を求めるようなイメージです。

おおよその内容としては


資産がスタート(例:5,000万円)
毎月、資産が「投資リターン」で少し増えたり減ったりする

例:平均 4%/年、リスク(ブレ幅) 1%/年
これを「正規分布」というサイコロで乱数を作って計算
その後「毎月の生活費を取り崩す」。(例:20万円ずつ減らす)
これを1シナリオとして 最後まで計算

これを 1,000回くらい繰り返す
サイコロを振って、資産の未来を1000通り描くイメージです。

コードでは1000回の結果を可視化していきます。



ライブラリのインストール



Google Colab で使用するには
日本語のライブラリが必要なのでこれを入れておいてください

# 可視化ライブラリのインストール
pip install japanize-matplotlib


モンテカルロシミュレーションのコード

ipywidgetsとmatplotlibで可視化するコードです。

コピペして上から実行していくと
スライダーとボタンが出るはずです。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
import ipywidgets as widgets
from IPython.display import display, clear_output

# シミュレーション関数
def simulate_withdrawal(start_age, years, initial_assets, monthly_withdraw, annual_return, annual_risk, n_sim=1000):
    months = years * 12
    results = []
    for _ in range(n_sim):
        assets = initial_assets
        monthly_returns = np.random.normal(
            loc=(annual_return/100)/12,
            scale=(annual_risk/100)/np.sqrt(12),
            size=months
        )
        trajectory = []
        for r in monthly_returns:
            assets *= (1 + r)           
            assets -= monthly_withdraw  
            assets = max(0, assets)     
            trajectory.append(assets)
        results.append(trajectory)
    return pd.DataFrame(results).T  


# 実行関数
def run_simulation(b):
    clear_output(wait=True)
    display(ui)
    
    # 入力値取得
    start_age = age_slider.value
    years = years_slider.value
    initial_assets = assets_slider.value
    monthly_withdraw = withdraw_slider.value
    annual_return = return_slider.value
    annual_risk = risk_slider.value

    # シミュレーション実行
    df = simulate_withdrawal(start_age, years, initial_assets, monthly_withdraw, annual_return, annual_risk)
    mean_path = df.mean(axis=1)
    median_path = df.median(axis=1)
    best_path = df.max(axis=1)
    worst_path = df.min(axis=1)
    months = np.arange(1, years*12 + 1)

    # 最終結果テキスト
    final_assets = df.iloc[-1]
    print(
        f"最終結果:"
        f"  破産確率: {100 * (final_assets==0).mean():.1f}%"
        f"  平均資産額: {final_assets.mean():,.0f} 万円"
        f"  中央資産額: {final_assets.median():,.0f} 万円"
        f"  最良ケース: {final_assets.max():,.0f} 万円"
        f"  最悪ケース: {final_assets.min():,.0f} 万円"
    )

    # グラフ描画 (matplotlib)
    plt.figure(figsize=(10,6))
    #plt.plot(months, best_path, label="最良ケース", color="green")
    plt.plot(months, worst_path, label="最悪ケース", color="red")
    plt.plot(months, mean_path, label="平均ケース", color="blue")
    plt.plot(months, median_path, label="中央値ケース", color="orange")
    plt.title(f"資産取り崩しシミュレーション(開始年齢: {start_age}歳, 期間: {years}年)")
    plt.xlabel("経過月数")
    plt.ylabel("資産額 (万円)")
    plt.legend()
    plt.grid(True)
    plt.show()


# スライダーUI
age_slider = widgets.IntSlider(value=65, min=50, max=100, step=1, description="開始年齢")
years_slider = widgets.IntSlider(value=30, min=10, max=50, step=1, description="年数")
assets_slider = widgets.IntSlider(value=5000, min=1000, max=20000, step=100, description="資産(万円)")
withdraw_slider = widgets.IntSlider(value=20, min=10, max=100, step=1, description="月取崩(万円)")
return_slider = widgets.IntSlider(value=4, min=1, max=20, step=1, description="利回り%")
risk_slider = widgets.IntSlider(value=1, min=1, max=30, step=1, description="リスク%")

# 実行ボタン
run_button = widgets.Button(description="シミュレーション実行", button_style="success")
run_button.on_click(run_simulation)

# UIまとめて表示
ui = widgets.VBox([age_slider, years_slider, assets_slider,withdraw_slider, return_slider, risk_slider,run_button])
display(ui)
スクリーンショット 2025-08-30 17.39.21

スライダーを調整して
シミュレーションボタンクリックで
結果が出ます。

スクリーンショット 2025-08-30 17.12.13


老後の資産と毎月の取り崩しの金額
利回りとリスクを変更すると

どれくらいの確率で資産が破産するか
求めることができます。

破産確率が0%になるように
取り崩しをしていくと資産が残る形になります。

毎月どれくらい取り崩せるのか
おおよその金額を算出できるので
老後資金が足りるかどうか
資産したい場合は、ぜひ使ってみてください。

老後2000万円問題とか
言われていますが
2000万円では全然足りないですねーーーーーー


資産が底を尽きないように
どんどん増やしていけるように
したいと思っています。

今後も投資系のシミュレーションを
どんどんやっていきますので
リクエストがあれば、どんどん
動画のコメントに書き込んでみてください

それでは。

夏休みの時期ですねー
夏休みの課題は決まりましたか?

今回は2重振り子のシミュレーションです


解説動画はこちら



2重振り子のシミュレーション


スクリーンショット 2025-07-26 16.28.53
こんな感じの2重振り子
よく有りますよね

支点があって
2個の振り子がついてて
ブルンブルンしちゃうやつです。

今回はこれをPythonで計算して
動画にするやつです。



ルンゲ・クッタ法

2重振り子をシミュレーションするには
2点の位置

2つのx,y軸の座標を求める必要があります。

この計算を行うために使用するのが
ルンゲ・クッタ法というものです。


4次のルンゲ・クッタ法(Runge-Kutta 4次法)

微分方程式の数値解法の一つで
特に4次までを考慮したテイラー展開を用いることで
より精度の高い近似解を求める方法だそうです。


この方法では、変化率の推定値を4通り計算し
それらを加重平均することで
次のステップの値を計算することになります。

計算手順は以下のようになります。

k1 → 現在の変化率
k2 → 半歩進んだ時点の変化率(k1からの推定)
k3 → さらに半歩進んだ時点の変化率(k2からの推定)
k4 → 1ステップ進んだ時点の変化率

これらを 1:2:2:1 の比率で足し合わせて
現在の状態を更新します。

𝑑𝑡 / 6 * (𝑘1 + 2*𝑘2 + 2*𝑘3 + 𝑘4) 

4次のルンゲ・クッタ法は、傾きを4回サンプリングし
テイラー展開の4次項まで考慮した近似になるため、
長時間シミュレーションでも精度が高いみたいです。

早速これをコードに落とし込んでいきましょう。


シミュレーションコード

Google colabで実行できるコードになっているので
コピペして実行することができると思います。

まずはライブラリのインポートです。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import warnings
warnings.filterwarnings('ignore')


次にパラメータ設定と
数値計算部分のコードです。
# パラメータ設定
g = 9.81   # 重力加速度
L1, L2 = 1.0, 1.0  # 振り子の長さ
m1, m2 = 1.0, 1.0  # 振り子の質量

# 初期条件(角度と角速度)
theta1 = np.pi/2
theta2 = np.pi/2 + 0.1
omega1 = 0.0
omega2 = 0.0
state = np.array([theta1, omega1, theta2, omega2])

# 時間設定
dt = 0.05
t_max = 25
steps = int(t_max/dt)

# 保存用リスト
x1_list, y1_list = [], []
x2_list, y2_list = [], []

# 二重振り子の運動方程式
def derivatives(state):
    theta1, omega1, theta2, omega2 = state
    delta = theta2 - theta1
    denom1 = (m1 + m2) * L1 - m2 * L1 * np.cos(delta)**2
    denom2 = (L2/L1) * denom1
    a1 = (m2*L1*omega1**2*np.sin(delta)*np.cos(delta) +
          m2*g*np.sin(theta2)*np.cos(delta) +
          m2*L2*omega2**2*np.sin(delta) -
          (m1+m2)*g*np.sin(theta1)) / denom1
    a2 = (-m2*L2*omega2**2*np.sin(delta)*np.cos(delta) +
          (m1+m2)*(g*np.sin(theta1)*np.cos(delta) -
                   L1*omega1**2*np.sin(delta) -
                   g*np.sin(theta2))) / denom2
    return np.array([omega1, a1, omega2, a2])

# Runge-Kutta 4次法で数値計算
for _ in range(steps):
    k1 = derivatives(state)
    k2 = derivatives(state + dt*k1/2)
    k3 = derivatives(state + dt*k2/2)
    k4 = derivatives(state + dt*k3)
    state += (dt/6)*(k1 + 2*k2 + 2*k3 + k4)
    theta1, omega1, theta2, omega2 = state

    # 座標変換
    x1 = L1 * np.sin(theta1)
    y1 = -L1 * np.cos(theta1)
    x2 = x1 + L2 * np.sin(theta2)
    y2 = y1 - L2 * np.cos(theta2)
    x1_list.append(x1)
    y1_list.append(y1)
    x2_list.append(x2)
    y2_list.append(y2)
for文のところがルンゲクッタ法の部分です。
ここでk1 - k4までを計算し
次のステップに状態を更新しています。

最後に描画用に 2点のx,y 軸としてデータ化します。

動画の生成部分です。
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
ax.set_aspect('equal')
ax.axis("off")
line, = ax.plot([], [], 'o-', lw=2)       # 振り子本体
trace, = ax.plot([], [], 'r-', alpha=0.5) # 軌跡

# 軌跡用リスト
trace_x, trace_y = [], []
def update(i):
    # 点の位置(始点 → 中点 → 先端)
    thisx = [0, x1_list[i], x2_list[i]]
    thisy = [0, y1_list[i], y2_list[i]]
    line.set_data(thisx, thisy)
    trace_x.append(x2_list[i])
    trace_y.append(y2_list[i])
    trace.set_data(trace_x, trace_y)
    return line, trace

ani = FuncAnimation(fig, update, frames=len(x1_list), interval=40, blit=True)
ani.save("double_pendulum.mp4", fps=30, dpi=150)
plt.close(fig)
matplotlibで描画を計算して
最終的にはmp4にします。


ファイル置き場にできた動画を見てみると
from IPython.display import Video
Video("double_pendulum.mp4", embed=True)
スクリーンショット 2025-07-26 16.50.32
こんな感じで軌跡が赤く残るような
動画が作成できます。

なかなか面白いので
長い時間で作ってみるのも
面白いかもしれません。

頑張れば振り子の数を増やしたり
色々なエフェクトを加えたりして
より面白くすることもできるかもしれません。

夏休みの課題が無くて困っているご家庭は
ぜひ面白い改造に挑戦してみてください。

今日はここまでです
それでは。

今回はドルコスト平均法による
積立投資のシミュレーションです

解説動画はこちら



ドルコスト平均法とは

価格が変動する金融商品に対して
一定金額を定期的に購入していく投資方法のことです

今回は毎月10万円積み立てるとして
それがどうなるかをシミュレーションしていきます。



eMaxisSlim米国株式S&P500

リンク先

S&P500指数(配当込み、円換算ベース)に
連動する運用成果を目指す投資信託
2018年7月3日に設定された商品です。

今回はこのデータを用いて
シミュレーションをしていきます。



eMAXIS Slim 米国株式 S&P500の価格推移

価格のデータを定義して
価格推移をだしてみます。

import numpy as np
import pandas as pd
import plotly.express as px
import seaborn as sns
import matplotlib.pyplot as plt

# データ定義
data = {
    2018 : [0,0,0,0,0,0,10038,10458,10709,11051,10188,10468],
    2019 : [8809,9854,10408,10565,10900,10031,10725,10978,10491,10890,11106,11680],
    2020 : [11873,11892,10825,9474,10655,11205,11465,11886,12707,12182,11767,12989],
    2021 : [13339,13407,14010,15217,15754,15924,16540,16712,17301,16677,18267,18005],
    2022 : [19291,18130,17601,19347,18798,18658,18052,19265,19394,18280,20283,19728],
    2023 : [17690,18714,19167,19388,20234,20666,22867,23260,23411,22911,22677,24155],
    2024 : [24154,25486,27473,28563,28573,29833,31693,29763,29789,29974,31336,32768],
    2025 : [33928,34065,32500,30512,28931,30867]
}

records = []
for year, prices in data.items():
    for month_idx, price in enumerate(prices):
        month = month_idx + 1
        if price == 0:
            continue
        date_str = f"{year}-{month:02d}-01"
        date = pd.to_datetime(date_str)
        records.append({"Date": date, "Price": price})

df = pd.DataFrame(records)
df = df.sort_values("Date")

# Plotlyでプロット
fig = px.line(df, x="Date", y="Price", title="Emaxis Slim value")
fig.show()
スクリーンショット 2025-06-21 16.43.01


良い感じに右肩上がりのようですが
この商品を毎月10万円ずつ積み立てたらどうなっていたでしょうか



2018年から2025年まで毎月積み立てた際のシミュレーション

毎月10万円
購入できる購入数量を算出して
2025年の最後の価格で
どれだけのリターンになっているかを算出します。
monthly_investment = 100000  # 毎月XX万円

# シミュレーション開始
total_units = 0  # 累計購入数量
total_invested = 0  # 累計投資金額
for year in sorted(data.keys()):
    for month_idx, price in enumerate(data[year], 1):
        if price == 0:
            continue
        units_bought = monthly_investment / price
        total_units += units_bought
        total_invested += monthly_investment

latest_year = max(data.keys())
latest_price = data[latest_year][-1]
current_value = total_units * latest_price
profit = current_value - total_invested
roi = (current_value / total_invested - 1) * 100

# 結果表示
print(f"総投資額: {total_invested:,.0f}円")
print(f"最終評価額: {current_value:,.0f}円")
print(f"損益: {profit:,.0f}円")
print(f"累計購入数量: {total_units:.4f}口")
print(f"リターン: {roi:.2f}%")
total_return = (100 + roi) / 100
years = 8
cagr = (total_return ** (1 / years)) - 1
print(f"年平均リターン: {cagr * 100:.2f}%")

総投資額: 8,400,000円
最終評価額: 16,458,895円
損益: 8,058,895円
累計購入数量: 533.2198口
リターン: 95.94%
年平均リターン: 8.77%


この価格推移のデータ上では
年平均8%超

7年で二倍近くの運用成績になっています。



ターンヒートマップ


今度は開始年から終了年までの
リターンヒートマップを出してみましょう。


開始と終了年を設定して
開始終了までのリターンを算出します。

years = list(data.keys())
monthly_invest = 100000
results = pd.DataFrame(index=years[:-1], columns=years[1:])
for start in years[:-1]:
    for end in years[years.index(start)+1:]:
        total_invest = 0
        total_units = 0
        for y in range(start, end+1):
            for price in data[y]:
                if price == 0:
                    continue
                units = monthly_invest / price
                total_units += units
                total_invest += monthly_invest
        # 最後の年の最後の価格で評価額計算
        final_price = [p for p in data[end] if p != 0][-1]
        final_value = total_units * final_price
        return_pct = (final_value - total_invest) / total_invest * 100
        results.loc[start, end] = return_pct

# ヒートマップ描画
results = results.astype(float)
plt.figure(figsize=(10, 7))
sns.heatmap(results, annot=True, fmt=".2f", cmap="coolwarm", center=0)
plt.title("dollar cost averaging method return heatmap (%)")
plt.xlabel("end year")
plt.ylabel("start year")
plt.show()
download

年単位だとどの年で買って売っても
プラスにはなっているようです。



まとめ

eMAXIS Slim 米国株式 S&P500のドルコスト平均法による積み立ては
年単位での運用であればマイナスは無いようです。

ただし、月単位だと乱高下があり
損する場合も有ったので、やはり長期積立で
見守るのが良いのではないかと思われます。

投資は自己判断で行うことが大切です。
その金融商品の良し悪しを判断し
投資するかどうかは人に言われるでなく
自分自身の判断で行うのが重要です。

投資判断を行うための材料作りとして
プログラムを用いたシミュレーションは
かなり役に立ちます。

データさえあればシミュレーションでき
結果の良し悪しから
投資判断の材料に使えるようになると思うので
プログラミングが出来るようになっていると
すぐに試すことができるのでお勧めです。

投資 x プログラミング
というテーマを今後も取り扱っていくので
両方できるようになりたい方は
要チェックしてみてください。

それでは


今回は昔懐かしい
カイジの沼のシミュレーションです。

解説動画はこちら




カイジの沼のシミュレーション

沼とは

漫画『賭博破戒録カイジ』に登場する架空のパチンコ台

帝愛グループの運営するカジノに設置されている
パチンコの中でも特別な台で玉が1発あたり4000円
挑戦は最低300万円からという設定

その一方で、今までの挑戦者が消費した玉が
全てプールされる仕組みになっており
運良く大当たりを引ければ総取りで莫大な額の金を手にできるとか


仕掛け・役物

ここからは確率を計算するための
沼のギミックについてです。

1.釘の森

釘が密集した配置になっている「釘の森」
通常時の設定Cは1/100程度

2.スル

開閉する羽根のある電動役物「スルー」
釘の森を突破した玉を更にふるい落とす
確率は不明なので
ここでは通過率を1/5としておきます

3.3段クルーン

沼の本丸である「三段クルーン」
円形の皿に穴が3つ/4つ/5つ空いていて
当たりの穴に入ると次の段に進める

数学的にはクルーンに入ってさえしまえば
約1/60の確立で当たる


というような設定にはなっていますが
漫画の設定上ではこの台にはイカサマがあり
この確率では入らないようにはなっています。

しかし、今回は考慮しないで
イカサマなしだとどうなるかを見ていきたいと思います。



沼のシミュレーションコード


簡易な確率計算で大当たりしたかどうかだけ返す関数を用いて
シミュレーションしていきます。
最大回数は10万回(4億円相当)とします。

import numpy as np
import random
import pandas as pd
import matplotlib.pyplot as plt

# 確率でヒットするかどうかを判定する
def is_hit(n: int) -> bool:
    return random.randint(1, n) == 1

def numa_charenge():
  # 釘の森の通過
  mori = is_hit(100)
  if not mori:
    return False

  # スルーの通過
  throw = is_hit(5)
  if not throw:
    return False

  # クルーン1段目通過
  first = is_hit(3)
  if not first:
    return False

  # クルーン2段目通過
  second = is_hit(4)
  if not second:
    return False

  # クルーン3段目通過
  third = is_hit(5)
  if not third:
    return False
  else:
    return True


# 1000回施行して測ってみる
ball_price = 4000 # パチンコ玉の価格

def numa_result(num):
  all_result = []
  for a in range(1000):
    cumulative = 0
    for i in range(num):
      cumulative += ball_price
      is_atari = numa_charenge()
      if is_atari:
        all_result.append([i+1, cumulative,1])
        break
      if i == num-1:
        all_result.append([i+1, cumulative,0])
  return all_result

num = 100000 # 1回あたりの最大施行回数
all_result = numa_result(num)

df = pd.DataFrame(all_result,columns=["balls","cumulative","hit"])
df.head()
balls cumulative hit
0 30959 123836000 1
1 29604 118416000 1
...

plt.figure(figsize=(12, 6))
df["cumulative"].hist(bins=20)
plt.show()
download

おおよそこのような結果になります。


どれくらい注ぎ込めば当たるのか?


シミュレーション結果は使った額の累計を入れているので
それを用いれば、その金額以下の回数と
全体の回数から大体の確率を求められます。
times_300 = len(df[df["cumulative"]<3000000])
print(f"300万円で大当たりになる可能性 : {times_300/1000*100} % ")

なおこのシミュレーションの結果は
幾何分布に近似するため以下のようなコードで
回数あたりの大当たり確率の理論値が出せます。
# 幾何分布上の理論値
for i in range(10):
  p = 1 / 30000
  n = (i+1) * 10000
  prob = 1 - (1 - p)**n
  print(f"{i+1:02}万回で当たる確率 : {prob:.6f}")
01万回で当たる確率 : 0.283473
02万回で当たる確率 : 0.486589
03万回で当たる確率 : 0.632127
04万回で当たる確率 : 0.736409
05万回で当たる確率 : 0.811130
06万回で当たる確率 : 0.864669
07万回で当たる確率 : 0.903032
08万回で当たる確率 : 0.930520
09万回で当たる確率 : 0.950215
10万回で当たる確率 : 0.964328


シミュレーション結果もこの理論値に
近似してますね。


まとめ


沼の設定が玉が1発あたり4000円
3万発に1回の確率(100x5x3x4x5)
で当たるのだとしたら3億注ぎ込めば
9割くらいは大当たりするのではないか?

あくまでイカサマが無い事が前提ですが.....
イカサマさえなければ無尽蔵にお金を注ぎ込めるなら
勝てる気がしなくもない今日この頃でした。

それでは

今回は最近流行りのポーカー
に関する確率のシミュレーションです

解説動画はこちら


 

ポーカーについて

ポーカーはトランプ5枚の手札の組み合わせで
役を作るゲームです。

1.ハイカード(強いカードの所持 2 < A)
2.ワンペア(同じ数字の組み合わせが1つ)
3.ツーペア(同じ数字の組み合わせが2つ)
4.スリーカード(同じ数字3つ)
5.ストレート(2,3,4,5,6 などの数字の並び)
6.フラッシュ(同じスートの組み合わせ HDCS)
7.フルハウス(スリーカードに加えて、同じ数字の組み合わせが1つ)
8.フォーカード(同じ数字4つ)
9.ストレートフラッシュ(ストレート + フラッシュ)
10.ロイヤルフラッシュ(AKQJTのフラッシュ)


テキサスホールデムルール


ポーカーのルールの一つで
プレイヤーそれぞれに配られた2枚のカードと
プレイヤー全員が共有する公開された
コミュニティカード"枚の計7枚で役を作り
チップをベットするなどの駆け引きを行うゲームルールです
(ベット周りの詳細なルールは割愛)


ここからはテキサスホールデムの
初期手札による勝率がどうなるのかを
検証するシミュレーションプログラムについてです。




役を計算するプログラム

シミュレーションを行うには
ポーカーの役の判定を行うプログラムが必要ですね

import random
from collections import Counter
from itertools import combinations

# ポーカーの役
POKER_HANDS = {
    0: "ハイカード",
    1: "ワンペア",
    2: "ツーペア",
    3: "スリーカード",
    4: "ストレート",
    5: "フラッシュ",
    6: "フルハウス",
    7: "フォーカード",
    8: "ストレートフラッシュ",
    9: "ロイヤルストレートフラッシュ"
}

# カードのランクとスート(HA=ハートのエース, D2=ダイヤの2)
RANKS = "23456789TJQKA"
SUITS = "HDSC"  # ハート, ダイヤ, スペード, クラブ
DECK = [s + r for r in RANKS for s in SUITS]

# 役の評価(スコアをタプルで返す)
def evaluate_hand(hand):
    ranks = sorted([RANKS.index(c[1]) for c in hand], reverse=True)
    suits = [c[0] for c in hand]
    rank_counts = Counter(ranks)
    flush = len(set(suits)) == 1
    straight = len(rank_counts) == 5 and (max(ranks) - min(ranks) == 4 or set(ranks) == {12, 3, 2, 1, 0})  # A-2-3-4-5対応

    # 役の判定(同率はRankで比較)
    if straight and flush:
        if set(ranks) == {12, 11, 10, 9, 8}:  # A, K, Q, J, 10
            return (9, max(ranks))  # ロイヤルストレートフラッシュ
        return (8, max(ranks)) # ストレートフラッシュ
    if 4 in rank_counts.values():
        return (7, max(k for k, v in rank_counts.items() if v == 4))  # フォーカード
    if 3 in rank_counts.values() and 2 in rank_counts.values():
        return (6, max(k for k, v in rank_counts.items() if v == 3))  # フルハウス
    if flush:
        return (5, ranks)  # フラッシュ
    if straight:
        return (4, max(ranks))  # ストレート
    if 3 in rank_counts.values():
        return (3, max(k for k, v in rank_counts.items() if v == 3))  # スリーカード
    if list(rank_counts.values()).count(2) == 2:
        return (2, sorted([k for k, v in rank_counts.items() if v == 2], reverse=True))  # ツーペア
    if 2 in rank_counts.values():
        return (1, max(k for k, v in rank_counts.items() if v == 2))  # ワンペア
    return (0, ranks)  # ハイカード

これを用いてシミュレーションを行なっていきます。



テキサスホールデムのシミュレーション

初期手札2枚と公開札5枚で、役を作り、勝率がどうなるか
4人で対戦をする際の勝率を出す
シミュレーションプログラムです。

user_num のところが対戦人数になるので
変更すれば、その人数での確率を求めることができます。
# モンテカルロ法で勝率計算
def monte_carlo_win_rate(my_hand, num_simulations=10000):
    wins = 0
    user_num = 3
    for _ in range(num_simulations):
        deck = DECK.copy()
        for card in my_hand:
            deck.remove(card)

        # 4人分の手札をランダムに配る
        random.shuffle(deck)
        opponent_hands = [deck[i * 2: (i + 1) * 2] for i in range(user_num)]
        community_cards = deck[6:11]

        # 各プレイヤーのベストハンドを評価
        my_best = max(evaluate_hand(list(comb)) for comb in combinations(my_hand + community_cards, 5))
        opponent_best = [max(evaluate_hand(list(comb)) for comb in combinations(hand + community_cards, 5)) for hand in opponent_hands]

        # 自分が最も強い手を持っているか判定
        if my_best > max(opponent_best):
            wins += 1

    return wins / num_simulations

# 例: 自分の手札をセットして勝率を計算
my_hand = ["HA", "HK"]  # ハートのエース・キング
win_rate = monte_carlo_win_rate(my_hand, num_simulations=5000)
print(f"勝率: {win_rate:.2%}")
勝率: 33.02%

カードの組み合わせを変えれば
その都度計算が行えます。



初期カードの組み合わせでの勝率

2種の13 * 13 枚の組み合わせにおける
勝率を計算してみましょう。

RANKS = "23456789TJQKA"
H_DECK = [s + r for r in RANKS for s in "H"]
D_DECK = [s + r for r in RANKS for s in "D"]
combi = [[h, d] for h in H_DECK for d in D_DECK]
result = {":".join(c):0 for c in combi}
for my_hand in combi:
  result[":".join(my_hand)] = monte_carlo_win_rate(my_hand, num_simulations=5000)

これで、13x13=169通りの結果が出せます。

これをヒートマップにしてみましょう。


勝率をヒートマップにする


import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# キーのソート用の関数
def get_rank_order(card):
    return RANKS.index(card[1])

# キーを分離してデータフレームを作成
data_list = []
for key, value in result.items():
    row_key, col_key = key.split(':')
    data_list.append({'row': row_key, 'col': col_key, 'value': value})

df = pd.DataFrame(data_list)
unique_rows = sorted(df['row'].unique(), key=get_rank_order)
unique_cols = sorted(df['col'].unique(), key=get_rank_order)

# ピボットテーブルを作成
pivot_df = df.pivot(index='row', columns='col', values='value')

# インデックスと列を順序通りに並び替え
pivot_df = pivot_df.reindex(index=unique_rows, columns=unique_cols)

# ヒートマップの作成
plt.figure(figsize=(10, 8))
sns.heatmap(pivot_df,
            cmap='RdYlGn',
            vmin=0,
            vmax=1,
            annot=True,
            fmt='.2f',
            cbar_kws={'label': 'Value'})

plt.title('Win rate for first hand combination')
plt.tight_layout()
plt.show()
heatmap

やはり、最初にワンペアを持っているだけで勝率は高くなりますね
とはいえ、それだけでは勝てないのが
このテキサスホールデムルールの面白いところ

初期手札の読み合いやベットなどの駆け引き
この辺りが組み合わさることで
ゲーム性が高くかなり面白いものになっています。

とはいえ
日本ではまだまだ、カジノがないので
ポーカーを楽しむには
単純なゲームとしてのポーカーしか出来ません

オンラインで展開されているものは
ほぼ日本国内では違法ではあるので
手を出さないように気を付けないといけません!!!

カジノが出来たら
もっともっとシミュレーションしましょう。

今回はここまでです
それでは

このページのトップヘ