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

Python

今回は15年間の積立投資シミュレーションです。

SP500とオルカンを毎月10万円15年間
積み立てたらどうなるかをシミュレーションします。


解説動画はこちら




S&P500 VS オールカントリー

今回は積立投資のシミュレーションです。

15年間積立投資を続けていたら、いくらになるのか
レバレッジかけた場合も合わせて検証してみます。


Yファイナンスの株価データを元に
シミュレーションしていきます。

Yファイナンスでの取得コードは
オルカン(ACWI)
S$P500(^GSPC)
となっているので、簡単にデータを取得できます。

シミュレーション条件は
2010年9月から2025年9月までの
15年間分(180ヶ月)のデータで
毎月10万円(新NISAの1800万円分)

という条件で行きたいと思います。

レバレッジは2倍と3倍のも
追加して検証していきます。



Yファイナンスの株価データを取得する


Google ColabではYファイナンスのライブラリで
簡単に株価を取得できるようになっています。

早速データを取得していきましょう。
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

# オルカン(All Country World Index)
acwi_ticker = "ACWI"

# S&P 500
sp500_ticker = "^GSPC"

# 過去15年間の株価データを取得
acwi = yf.download(acwi_ticker, start="2010-09-01", end="2025-09-05")
sp500 = yf.download(sp500_ticker, start="2010-09-01", end="2025-09-05")
acwi_close = acwi["Close"]
sp500_close = sp500["Close"]
acwi_monthly = acwi_close.resample("MS").first()
sp500_monthly = sp500_close.resample("MS").first()
取得用の株価コードは
オルカン : ACWI
S$P500 : ^GSPC
となっていて

株価コード、開始日、終了日を
指定するだけで簡単にデータ取得できます。



株価推移をプロット


この15年間の両者の株価をプロットしてみます。
# オルカン
plt.figure(figsize=(14, 3))
plt.plot(acwi_close, label="ACWI")
plt.title("ACWI - Past 15 Years")
plt.ylabel("Closing Price")
plt.legend()
plt.grid(True)
plt.show()
# S&P500
plt.figure(figsize=(14, 3))
plt.plot(sp500_close, label="S&P 500")
plt.title("S&P 500 - Past 15 Years")
plt.ylabel("Closing Price")
plt.legend()
plt.grid(True)
plt.show()

download-2
download-3

株価が違いすぎるので、一緒にプロットできないので
正規化して、同じプロット内で表示してみましょう。
# 正規化
acwi_norm = acwi_close / acwi_close.iloc[0]
sp500_norm = sp500_close / sp500_close.iloc[0]

# グラフの作成
plt.figure(figsize=(14, 5))
plt.plot(acwi_norm, label="ACWI")
plt.plot(sp500_norm, label="S&P 500")
plt.title("ACWI vs S&P 500 - Past 15 Years")
plt.ylabel("Norm Price")
plt.legend()
plt.grid(True)
plt.show()
download-1

この15年で株価は何倍になっています。

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


積み立てシミュレーション


これが今回のシミュレーション用の関数です。
# --- シミュレーション関数 ---
def simulate_investment(prices, monthly_invest=100000, leverage=1.0):
    returns = prices.pct_change().dropna()
    shares = 0.0
    portfolio = []

    for i, r in enumerate(returns):
        # 毎月積立で購入
        shares += monthly_invest / prices.iloc[i]
        shares *= (1 + r * leverage)  # レバレッジ調整後の値動き
        portfolio.append(shares * prices.iloc[i+1])  # 翌月末時点の評価額

    portfolio = pd.Series(portfolio, index=returns.index)
    return portfolio

実際にシミュレーションするコードはこちら
毎月の投資額を入力すると計算、可視化できます。
# データの結合
df = pd.concat([acwi_monthly,sp500_monthly],axis=1)
# 毎月の投資額
monthly_invest = 100000

# 直近のみに絞るならコメントアウト外す
#df = df.tail(60)

df_assets = pd.DataFrame({
    "S&P500_1x": simulate_investment(df["^GSPC"], monthly_invest, leverage=1.0),
    "S&P500_2x": simulate_investment(df["^GSPC"], monthly_invest, leverage=2.0),
    "S&P500_3x": simulate_investment(df["^GSPC"], monthly_invest, leverage=3.0),
    "ACWI_1x": simulate_investment(df["ACWI"], monthly_invest, leverage=1.0),
    "ACWI_2x": simulate_investment(df["ACWI"], monthly_invest, leverage=2.0),
    "ACWI_3x": simulate_investment(df["ACWI"], monthly_invest, leverage=3.0)
})
df_assets["Input_Amount"] = [(i+1) * monthly_invest for i in range(len(df_assets))]
# プロット
plt.figure(figsize=(14, 5))
plt.plot(df_assets.index, df_assets["S&P500_1x"], label="S&P500 (1x)", color="orange")
plt.plot(df_assets.index, df_assets["ACWI_1x"], label="ACWI (1x)", color="blue")
plt.plot(df_assets.index, df_assets["S&P500_2x"], label="S&P500 (2x)", color="orange", linestyle="-.")
plt.plot(df_assets.index, df_assets["ACWI_2x"], label="ACWI (2x)", color="blue", linestyle="-.")
plt.plot(df_assets.index, df_assets["S&P500_3x"], label="S&P500 (3x)", color="orange", linestyle="--")
plt.plot(df_assets.index, df_assets["ACWI_3x"], label="ACWI (3x)", color="blue", linestyle="--")
plt.plot(df_assets.index, df_assets["Input_Amount"], label="Input Amount", color="black")
plt.title("S&P500 vs ACWI leverage simulation")
plt.ylabel("Asset amount")
plt.grid(True)
plt.legend()
ax = plt.gca()
ax.yaxis.set_major_formatter(ticker.ScalarFormatter(useOffset=False))
ax.ticklabel_format(style="plain", axis="y")
plt.show()
download-4

直近5年に絞り込むには
df のコメントアウトの部分を外して
直近分に絞り込みするとできます。

download

どれもこれも、ずっと持ち続けていたら
資産はすごいことになっていたでしょうね。

レバレッジをかけた商品の場合
上下動が激しすぎて
途中で手放してしまった
なんてこともありそうです。

ずっと持ち続けていたら
投資額を上回っていた、ということにはなります。

この株価のデータは今後も同じ動きをするわけではないので
再現性は0ですが、積立投資自体は
悪い選択肢ではないかなと思っています。

今後両者がどのような動きをするかは
気になるところです。

今回は
SP500とオルカンの
積立投資シミュレーションでした。

それでは。




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


解説動画はこちら




老後資産取り崩し


老後の資産を年利 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万円では全然足りないですねーーーーーー


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

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

それでは。

今回はオンカジに有りそうな
ポーカーBOTについてです。


解説動画はこちら


 

この内容はあくまでも
BOTプログラムに関する内容となります。




プログラムの内容


ポーカーのテキサスホールデムルールのゲームを行える
Streamlitプログラムです。

BOT用のストラテジークラスがあり
それを改変することでBOTの戦略を
構築することができるようになっています。




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

自分だけの手札2枚と場に共有される5枚のカード
(コミュニティカード)を組み合わせて
最も強い5枚の役を作るポーカーゲーム

ゲームはブラインドベットで開始され
プリフロップ、フロップ、ターン、リバーの
4つのラウンドでそれぞれベットが行われる

各ラウンドでは
コール、レイズ、フォールド、チェックなどのアクションを選択し
最終的に残ったプレイヤーの中で
最も強い役を持っていたプレイヤーがポットを獲得



今回のBOTの特徴

プリフロップはそれなり
ポストフロップは運任せ
という戦略です。

細かいルールは

ハンド強度評価を行い
3段階の強度分類でアクションを決めます。

評価基準(スコア10-50程度)

ペア: +30 + ハイカードランク
AA = 30 + 14 = 44点(最強)
KK = 30 + 13 = 43点
22 = 30 + 2 = 32点

スーテッド: +5点
同じスーツなら追加ボーナス

ハイカード: high/2 + low/5
AK = 14/2 + 13/5 = 7 + 2.6 = 9.6点
スーテッドAK = 9.6 + 5 = 14.6点


強度分類

強いハンド (45点以上)
主にAA, KK, QQ相当
積極的にレイズ/ベット
チップが少なければオールイン

中程度のハンド (35-44点)
中位ペア、AK、強いスーテッドコネクター
チェック/コール中心
守備的プレイ

弱いハンド (34点以下)
低ペア、アンスーテッドローカード
チェックできればチェック
ベットがあればフォールド


こんな戦略をとるBOTです。


プログラムの詳細

3つのプログラムからなっています。
main.py : StreamlitのUIなど
poker_engine.py : ポーカーゲームの制御
bot_strategy.py : BOTのストラテジー

ローカルでStreamlitを動かす場合はインストールが必要です。
pip install streamlit


main.py
import streamlit as st
from poker_engine import PokerGame
from bot_strategy import BotStrategy

def init_game():
    """ゲーム初期化"""
    player_names = ["You"] + [f"BOT{i+1}" for i in range(7)]
    st.session_state.game = PokerGame(player_names)
    st.session_state.game.new_hand()
    st.session_state.pending_action = None

def process_pending():
    """保留中のアクションを処理"""
    if st.session_state.pending_action is not None:
        idx, action = st.session_state.pending_action
        st.session_state.pending_action = None
        st.session_state.game.apply_action(idx, action)

def auto_advance_bots_until_user_or_showdown(max_steps=50):
    """Botの連続ターンを自動進行"""
    game = st.session_state.game
    steps = 0
    
    while steps < max_steps:
        if game.phase == "showdown":
            return
        
        current_player = game.get_current_player()
        if current_player is None:
            return
        
        if current_player.name == "You" or not current_player.can_act():
            return
        
        # Bot行動
        bot = BotStrategy(current_player.name)
        action = bot.decide_action(
            current_player.hand, 
            game.community, 
            current_player.chips,
            game.current_bet, 
            current_player.bet_this_round
        )
        
        game.apply_action(game.current_index, action)
        steps += 1

def draw_player_panel():
    """プレイヤー情報パネルを描画"""
    game = st.session_state.game
    cols = st.columns(8)
    
    for i, player in enumerate(game.players):
        with cols[i]:
            # プレイヤー名の装飾
            name = player.name
            if player.allin:
                name += " (ALL-IN)"
            if i == game.dealer_index:
                name += " [D]"
            if i == game.sb_index:
                name += " [SB]"
            if i == game.bb_index:
                name += " [BB]"
            
            # 非アクティブ・フォールドプレイヤーは取り消し線
            if not player.active or player.folded:
                st.markdown(f"~~**{name}**~~")
            else:
                st.markdown(f"**{name}**")
            
            # チップ表示
            st.write(f"Chips: {player.chips}")
            
            # ベット額表示
            if player.bet_this_round > 0:
                st.write(f"Bet: {player.bet_this_round}")
            
            # あなたの手札のみ表示
            if player.name == "You" and player.hand:
                st.write(f"Hand: {player.hand[0]}  {player.hand[1]}")

def draw_community_area():
    """コミュニティエリアを描画"""
    game = st.session_state.game
    
    st.subheader("Dealer Zone")
    
    # コミュニティカード表示
    if game.community:
        community_str = " ".join(game.community)
        st.write(f"Community: {community_str}")
    else:
        st.write("Community: (No cards yet)")
    
    # ポット表示
    st.write(f"Pot: {game.pot}")
    
    # ゲーム状況メッセージ
    st.info(game.message)

def draw_action_interface():
    """アクションインターフェースを描画"""
    game = st.session_state.game
    
    if game.phase == "showdown":
        st.success("Hand complete. Click 'New Hand' to continue.")
        if game.game_over:
            st.warning("Game over. Your chips are gone or opponents busted. Click 'New Hand' to restart.")
        return
    
    current_player = game.get_current_player()
    if current_player is None or current_player.name != "You":
        # あなたのターンではない
        if current_player:
            st.info(f"Waiting for {current_player.name} to act...")
        return
    
    if not current_player.can_act():
        return
    
    # アクション選択UI
    st.subheader("Your Turn")
    
    legal_actions = game.legal_actions(game.current_index)
    to_call = max(0, game.current_bet - current_player.bet_this_round)
    
    # アクションラベルを整形
    action_labels = []
    for action in legal_actions:
        if action == "call":
            action_labels.append(f"Call ({to_call})")
        elif action == "bet":
            action_labels.append("Bet (10)")
        elif action == "raise":
            action_labels.append("Raise (+10)")
        else:
            action_labels.append(action.capitalize())
    
    # ラジオボタンでアクション選択
    selected_action = st.radio(
        "Choose your action:",
        legal_actions,
        format_func=lambda x: action_labels[legal_actions.index(x)],
        horizontal=True,
        key="player_action"
    )
    
    # アクション実行ボタン
    if st.button("Submit Action", type="primary"):
        st.session_state.pending_action = (game.current_index, selected_action)
        st.rerun()

def main():
    st.set_page_config(
        page_title="Texas Hold'em Poker",
        page_icon="♠",
        layout="wide"
    )
    
    st.title("♠ Texas Hold'em Poker")
    st.markdown("---")
    
    # ゲーム初期化
    if "game" not in st.session_state:
        init_game()
    
    # 上部コントロール
    col1, col2, col3 = st.columns([1, 1, 2])
    
    with col1:
        if st.button("New Hand", type="secondary"):
            st.session_state.game.new_hand()
            st.rerun()
    
    with col2:
        if st.button("Restart Game", type="secondary"):
            init_game()
            st.rerun()
    
    with col3:
        game = st.session_state.game
        st.write(f"Phase: **{game.phase.title()}** | Current Bet: **{game.current_bet}**")
    
    st.markdown("---")
    
    # 保留中のアクションを処理
    process_pending()
    
    # Botの自動進行
    auto_advance_bots_until_user_or_showdown()
    
    # プレイヤー情報表示
    st.subheader("Players")
    draw_player_panel()
    
    st.markdown("---")
    
    # コミュニティエリア表示
    draw_community_area()
    
    st.markdown("---")
    
    # アクションインターフェース
    draw_action_interface()

if __name__ == "__main__":
    main()






poker_engine.py
import random
import itertools
from typing import List, Dict, Tuple, Optional

# ========= 基本定義 =========
SUITS = ["♠", "♥", "♦", "♣"]
RANKS = ["2","3","4","5","6","7","8","9","T","J","Q","K","A"]
RANK_VAL = {r:i for i,r in enumerate(RANKS, start=2)}
INITIAL_CHIPS = 1000

def create_deck() -> List[str]:
    """52枚のデッキを作成"""
    return [r+s for r,s in itertools.product(RANKS, SUITS)]

# ========= ハンド評価 =========
def hand_rank(cards5: List[str]) -> Tuple:
    """
    5枚のカードから役の強さを評価
    返り値: (rank_class, high cards...) で大きい方が強い
    rank_class: 8=ストレートフラッシュ, 7=フォーカード, 6=フルハウス,
               5=フラッシュ, 4=ストレート, 3=スリーカード, 2=ツーペア, 1=ワンペア, 0=ハイカード
    """
    vals = sorted([RANK_VAL[c[0]] for c in cards5], reverse=True)
    suits = [c[1] for c in cards5]
    counts = {}
    for v in vals:
        counts[v] = counts.get(v, 0) + 1
    by_count = sorted(counts.items(), key=lambda x:(x[1], x[0]), reverse=True)
    is_flush = len(set(suits)) == 1
    
    # ストレート判定(Aを1として扱うホイール対応)
    uniq = sorted(set(vals), reverse=True)
    def straight_high(vs):
        run = 1
        best = None
        for i in range(len(vs)-1):
            if vs[i] - 1 == vs[i+1]:
                run += 1
                if run >= 5:
                    best = vs[i-3]
            elif vs[i] != vs[i+1]:
                run = 1
        # A-5ストレート
        if set([14,5,4,3,2]).issubset(set(vals)):
            best = max(best or 0, 5)
        return best
    
    straight_hi = straight_high(uniq)

    if is_flush and straight_hi:
        return (8, straight_hi)
    if by_count[0][1] == 4:
        four = by_count[0][0]
        kicker = max([v for v in vals if v != four])
        return (7, four, kicker)
    if by_count[0][1] == 3 and any(c==2 for _,c in by_count[1:]):
        triple = by_count[0][0]
        pair = max([v for v,c in by_count[1:] if c==2])
        return (6, triple, pair)
    if is_flush:
        return (5, *vals)
    if straight_hi:
        return (4, straight_hi)
    if by_count[0][1] == 3:
        triple = by_count[0][0]
        kickers = [v for v in vals if v != triple][:2]
        return (3, triple, *kickers)
    pairs = [v for v,c in by_count if c==2]
    if len(pairs) >= 2:
        top, sec = sorted(pairs, reverse=True)[:2]
        kicker = max([v for v in vals if v not in [top,sec]])
        return (2, top, sec, kicker)
    if len(pairs) == 1:
        pair = pairs[0]
        kickers = [v for v in vals if v != pair][:3]
        return (1, pair, *kickers)
    return (0, *vals)

def best_hand_7(cards7: List[str]) -> Tuple[Tuple, List[str]]:
    """7枚から最強の5枚役を評価"""
    best = None
    best5 = None
    for comb in itertools.combinations(cards7, 5):
        h = hand_rank(comb)
        if (best is None) or (h > best):
            best = h
            best5 = list(comb)
    return best, best5

# ========= プレイヤークラス =========
class Player:
    def __init__(self, name: str, chips: int = INITIAL_CHIPS):
        self.name = name
        self.chips = chips
        self.hand = []
        self.folded = False
        self.allin = False
        self.active = True
        self.bet_this_round = 0
        self.has_acted = False
    
    def reset_for_new_hand(self, hand: List[str]):
        """新しいハンド用にリセット"""
        if self.chips <= 0:
            self.active = False
            self.folded = True
            self.allin = False
            self.hand = []
            self.bet_this_round = 0
            self.has_acted = True
        else:
            self.folded = False
            self.allin = False
            self.hand = hand
            self.bet_this_round = 0
            self.has_acted = False
    
    def reset_for_new_round(self):
        """新しいラウンド用にリセット"""
        self.bet_this_round = 0
        self.has_acted = False
    
    def can_act(self) -> bool:
        """行動可能かどうか"""
        return self.active and not self.folded and not self.allin
    
    def post_blind(self, amount: int) -> int:
        """ブラインドを支払う"""
        actual = min(self.chips, amount)
        self.chips -= actual
        self.bet_this_round += actual
        if self.chips == 0:
            self.allin = True
        return actual

# ========= ゲームエンジンクラス =========
class PokerGame:
    def __init__(self, player_names: List[str]):
        self.players = [Player(name) for name in player_names]
        self.dealer_index = 0
        self.sb_index = 0
        self.bb_index = 0
        self.deck = []
        self.community = []
        self.pot = 0
        self.phase = "preflop"  # preflop, flop, turn, river, showdown
        self.current_bet = 0
        self.current_index = 0
        self.last_raiser = None
        self.message = ""
        self.game_over = False
    
    def new_hand(self):
        """新しいハンドを開始"""
        self.deck = create_deck()
        random.shuffle(self.deck)
        self.community = []
        self.pot = 0
        self.phase = "preflop"
        self.current_bet = 0
        self.last_raiser = None
        
        # ディーラーボタン順回し
        self.dealer_index = (self.dealer_index + 1) % len(self.players)
        self.sb_index = (self.dealer_index + 1) % len(self.players)
        self.bb_index = (self.dealer_index + 2) % len(self.players)
        
        SB_amount = 10
        BB_amount = 20
        
        # 配牌と状態リセット
        for player in self.players:
            if player.chips <= 0:
                player.reset_for_new_hand([])
            else:
                hand = [self.deck.pop(), self.deck.pop()]
                player.reset_for_new_hand(hand)
        
        # ブラインドを支払う
        sb_posted = self.players[self.sb_index].post_blind(SB_amount)
        bb_posted = self.players[self.bb_index].post_blind(BB_amount)
        self.pot += sb_posted + bb_posted
        self.current_bet = BB_amount
        
        # プリフロップの最初の行動者はBBの次
        self.current_index = self._next_index(self.bb_index)
        self.message = f"New hand: Dealer={self.players[self.dealer_index].name}, SB={self.players[self.sb_index].name}({sb_posted}), BB={self.players[self.bb_index].name}({bb_posted})"
    
    def _alive_players(self) -> List[int]:
        """生存プレイヤーのインデックスリスト"""
        return [i for i, p in enumerate(self.players) if p.active and not p.folded]
    
    def _next_index(self, idx: int) -> Optional[int]:
        """次の行動可能プレイヤーのインデックス"""
        n = len(self.players)
        for step in range(1, n+1):
            j = (idx + step) % n
            if self.players[j].can_act():
                return j
        return None
    
    def _everyone_done_this_round(self) -> bool:
        """全員がこのラウンドで行動完了したか"""
        for i in self._alive_players():
            p = self.players[i]
            if p.allin:
                continue
            if p.bet_this_round != self.current_bet:
                return False
            if not p.has_acted:
                return False
        return True
    
    def _advance_phase(self):
        """次のフェーズに進む"""
        # ラウンド終了処理
        for p in self.players:
            p.reset_for_new_round()
        self.current_bet = 0
        self.last_raiser = None
        
        # コミュニティカード追加
        if self.phase == "preflop":
            for _ in range(3):  # フロップ3枚
                self.community.append(self.deck.pop())
            self.phase = "flop"
        elif self.phase == "flop":
            self.community.append(self.deck.pop())  # ターン1枚
            self.phase = "turn"
        elif self.phase == "turn":
            self.community.append(self.deck.pop())  # リバー1枚
            self.phase = "river"
        elif self.phase == "river":
            self.phase = "showdown"
        
        self.current_index = self._next_index(-1)  # 左から探索
        if self.phase == "showdown":
            self._do_showdown()
    
    def _do_showdown(self):
        """ショーダウン処理"""
        contenders = self._alive_players()
        if len(contenders) == 0:
            self.message = "All folded."
            return
        
        # 役判定
        scores = []
        for i in contenders:
            cards7 = self.players[i].hand + self.community
            score, best5 = best_hand_7(cards7)
            scores.append((score, i))
        
        # 勝者決定
        scores.sort(reverse=True, key=lambda x: x[0])
        top_score = scores[0][0]
        winners = [i for (sc, i) in scores if sc == top_score]
        
        # ポット分配
        share = self.pot // len(winners)
        for i in winners:
            self.players[i].chips += share
        
        self.message = f"Showdown! Winners: {', '.join(self.players[i].name for i in winners)} (+{share} each)"
        self.pot = 0
        
        # 脱落処理
        for p in self.players:
            if p.chips <= 0:
                p.active = False
                p.folded = True
                p.allin = False
        
        # ゲーム終了判定
        active_count = sum(1 for p in self.players if p.active)
        player_alive = any(p.active for p in self.players if p.name == "You")
        self.game_over = not (player_alive and active_count >= 2)
    
    def legal_actions(self, player_index: int) -> List[str]:
        """プレイヤーが取れる合法的なアクション"""
        player = self.players[player_index]
        to_call = max(0, self.current_bet - player.bet_this_round)
        
        if player.chips <= 0:
            return ["check"]
        
        actions = []
        if to_call == 0:
            actions += ["check", "bet", "raise", "fold", "allin"]
        else:
            actions += ["call", "raise", "fold", "allin"]
        
        return actions
    
    def apply_action(self, player_index: int, action: str) -> bool:
        """アクションを適用"""
        player = self.players[player_index]
        to_call = max(0, self.current_bet - player.bet_this_round)
        
        if action == "fold":
            player.folded = True
            player.has_acted = True
            self.message = f"{player.name} folds."
            
        elif action == "check":
            if to_call != 0:
                self.message = f"{player.name} cannot check (must call {to_call})."
                return False
            player.has_acted = True
            self.message = f"{player.name} checks."
            
        elif action == "call":
            pay = min(to_call, player.chips)
            player.chips -= pay
            player.bet_this_round += pay
            self.pot += pay
            if player.chips == 0:
                player.allin = True
            player.has_acted = True
            self.message = f"{player.name} calls {pay}."
            
        elif action == "bet":
            if self.current_bet != 0:
                return self.apply_action(player_index, "raise")
            amount = min(10, player.chips)
            player.chips -= amount
            player.bet_this_round += amount
            self.pot += amount
            self.current_bet = player.bet_this_round
            self.last_raiser = player_index
            if player.chips == 0:
                player.allin = True
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} bets {amount}."
            
        elif action == "raise":
            if player.chips <= to_call:
                if player.chips < to_call:
                    return self.apply_action(player_index, "allin")
                else:
                    return self.apply_action(player_index, "call")
            call_amt = to_call
            raise_extra = min(10, player.chips - call_amt)
            total = call_amt + raise_extra
            player.chips -= total
            player.bet_this_round += total
            self.pot += total
            self.current_bet = max(self.current_bet, player.bet_this_round)
            self.last_raiser = player_index
            if player.chips == 0:
                player.allin = True
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} raises to {player.bet_this_round}."
            
        elif action == "allin":
            need = max(0, self.current_bet - player.bet_this_round)
            all_amount = need + player.chips
            player.chips = 0
            player.bet_this_round += all_amount
            self.pot += all_amount
            player.allin = True
            self.current_bet = max(self.current_bet, player.bet_this_round)
            self.last_raiser = player_index
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} goes all-in ({player.bet_this_round})."
            
        else:
            self.message = "Unknown action."
            return False
        
        # 次のプレイヤーへ
        self.current_index = self._next_index(player_index)
        
        # ラウンド終了チェック
        alive_count = len(self._alive_players())
        if alive_count <= 1:
            # 残り1人なら即勝利
            self.phase = "showdown"
            winners = self._alive_players()
            if winners:
                winner = self.players[winners[0]]
                winner.chips += self.pot
                self.message = f"{winner.name} wins (others folded). +{self.pot}"
                self.pot = 0
            self._do_showdown()
            return True
        
        if self._everyone_done_this_round():
            self._advance_phase()
        
        return True
    
    def get_current_player(self) -> Optional[Player]:
        """現在の行動プレイヤー"""
        if self.current_index is None:
            return None
        return self.players[self.current_index]






bot_strategy.py
import random

RANK_ORDER = {r:i for i,r in enumerate(list("23456789TJQKA"), start=2)}

def approx_hand_strength_preflop(hand):
    # hand: ["As","Kd"] のような2枚
    r1, r2 = hand[0][0], hand[1][0]
    s1, s2 = hand[0][1], hand[1][1]
    pair = (r1 == r2)
    suited = (s1 == s2)
    v1, v2 = RANK_ORDER[r1], RANK_ORDER[r2]
    high = max(v1, v2)
    low  = min(v1, v2)

    score = 0
    if pair: score += 30 + high
    if suited: score += 5
    score += high/2 + low/5
    return score  # だいたい 10〜50 程度

def decide_strengthish_action(strength, to_call, chips, can_check):
    """ごく簡単な方針:
    - 強: レイズ/ベット or オールイン
    - 中: コール/チェック
    - 弱: フォールド(ただしチェックできるならチェック)
    """
    if chips <= 0:
        return "check" if can_check else "call"

    if strength >= 45:
        if chips <= to_call:  # ほぼショートならオールイン
            return "allin"
        return "raise" if to_call > 0 else "bet"
    elif strength >= 35:
        return "check" if can_check else ("call" if to_call <= chips else "fold")
    else:
        return "check" if can_check else ("fold" if to_call > 0 else "check")

class BotStrategy:
    def __init__(self, name):
        self.name = name

    def decide_action(self, hand, community_cards, chips, current_bet, bet_this_round):
        # to_call: ラウンド内で必要な追加額
        to_call = max(0, current_bet - bet_this_round)
        can_check = (to_call == 0)

        # 超簡易:プリフロップは上の関数、以降はランダム寄り + コール重視
        if len(community_cards) == 0:
            strength = approx_hand_strength_preflop(hand)
        else:
            # 盤面が出たらレンジ広めに
            strength = random.uniform(25, 50)

        action = decide_strengthish_action(strength, to_call, chips, can_check)

        # たまにブラフ/ミックス
        if action in ("check","call") and not can_check and random.random() < 0.07 and chips > to_call + 10:
            action = "raise"
        if action == "raise" and chips < to_call + 10:
            action = "call" if chips >= to_call else "allin"
        if action == "bet" and chips < 10:
            action = "allin"

        return action




3つのファイルをコピペして作って
下記のコマンドを実行すると動くと思います。

streamlit run main.py
スクリーンショット 2025-08-23 17.07.28


自分のターンの際に
Callなどのラジオボタンで行動選択
Submit Actionボタンで行動決定です。

1ゲーム終わったら
左上の「New Hand」で次のゲームへ
チップがなくなったら「Restart Game」でゲームリセット

一応、バグがちょろちょろ有るので
ちゃんとは動かないところもあるかもしれませんが
ちょっと試してみるは出来ると思います。


自分で自分のBOTと対戦するも良し
BOTを複数繋げて対戦させ、一番強いBOTを探るもよし
色々改造して遊べると思います。

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

どうやら口コミが消されている事件があるようなので
見てみることにしました。



解説動画はこちら






ジャングリア沖縄

2025年7月25日に開業したテーマパーク
「大自然没入型」を掲げ、恐竜や絶景などを
テーマにしたアトラクションや温泉施設を備える

口コミが香ばしいので、全部見てみました。



口コミデータを取得する方法

(Google Chromeの場合)
Google口コミから、ジャングリアの口コミを開く
最下段までスクロールする

その状態で「右クリックから検証」で検証ツールを開く
HTMLのソースをクリックして
「Copy outerHTML」を実行

テキストエディターに貼り付けて
htmlファイルとして保存する



HTMLをBeautiful Soup で解析する


クラス名の部分は変更される可能性があるので
データが取れない場合は、変更してみてください。
(HTMLファイル内のclassの部分の文字列に変更する)
from bs4 import BeautifulSoup

# ファイル名は適宜変更する
with open("google.html") as _f:
    html_data = _f.read()

soup = BeautifulSoup(html_data, "html.parser")
all_div = soup.find("div",class_="aSzfg")
divs = all_div.find_all("div",class_="bwb7ce")

data = []
for div in divs:
    date_tag = div.find("span",class_="y3Ibjb")
    date = date_tag.text.replace("最終編集: ","")
    
    name_tag = div.find("div",class_="Vpc5Fe")
    name = name_tag.text

    role_tag = div.find("div",class_="GSM50")
    roles = role_tag.text.split("·")
    role = roles[0] if len(roles)==3 else ""
    num = roles[1].replace(" 件のクチコミ","") if len(roles)==3 else roles[0].replace(" 件のクチコミ","")
    photo = roles[2].replace(" 枚の写真","") if len(roles)>2 else ("" if len(roles)==1 else roles[1].replace(" 枚の写真",""))
    
    rating_tag = div.find("div",class_="dHX2k")
    rating = rating_tag.get("aria-label").replace("5 点中 ","").replace(" 点の評価","")

    comment_tag = div.find("div",class_="OA1nbd")
    comment = comment_tag.text.replace(" …もっと見る","") if comment_tag is not None else ""
    tmp = [date,name,role,num,photo,rating,comment]
    data.append(tmp)

データフレームに変換する
import pandas as pd
import re

# 日付変換用関数
def convert_to_days(value):
    if "週間前" in value:
        weeks = int(re.search(r"(\d+)", value).group(1))
        return -weeks * 7
    elif "日前" in value:
        days = int(re.search(r"(\d+)", value).group(1))
        return -days
    elif "時間前" in value:
        return -1
    else:
        return 0

columns = ["日付", "名前", "職業", "口コミ数", "写真の数", "星の数", "コメント"]
df = pd.DataFrame(data,columns = columns)

df["口コミ数"] = df["口コミ数"].replace("", 0).astype(int)
df["写真の数"] = df["写真の数"].replace("", 0).astype(int)
df["星の数"] = df["星の数"].replace("", 0).astype(float)
df["日前"] = df["日付"].apply(convert_to_days)


星の数などを可視化する

データができたら可視化していきましょう

要ライブラリ
pip install japanize_matplotlib
pip install janome
pip install wordcloud


星の数ごとのカウント
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib

# ================
# 1. 星の数のヒストグラム
# ================
plt.figure(figsize=(6, 4))
sns.histplot(df["星の数"], bins=10, kde=True, color="purple")
plt.title("星の数のヒストグラム")
plt.xlabel("星の数")
plt.ylabel("件数")
plt.show()
download



散布図
# ================
# 2. 散布図 (2種類)
# ================
plt.figure(figsize=(12, 5))

# (1) 口コミ数 vs 星の数
plt.subplot(1, 3, 1)
sns.scatterplot(x="口コミ数", y="星の数", data=df, color="blue")
plt.title("口コミ数 vs 星の数")

# (2) 写真の数 vs 星の数
plt.subplot(1, 3, 2)
sns.scatterplot(x="写真の数", y="星の数", data=df, color="green")
plt.title("写真の数 vs 星の数")

# (3) X日前 vs 星の数
plt.subplot(1, 3, 3)
sns.scatterplot(x="日前", y="星の数", data=df, color="red")
plt.title("日前 vs 星の数")

plt.tight_layout()
plt.show()
download-2


日毎の星の数
# ▼ 棒グラフ作成
plt.figure(figsize=(12, 6))
sns.countplot(data=df, x="日前", hue="星の数", palette="viridis")

# ▼ 軸・タイトルの設定
plt.title("日前ごとの星の数カウント", fontsize=16)
plt.xlabel("日前", fontsize=14)
plt.ylabel("カウント数", fontsize=14)
plt.xticks(rotation=45)

plt.legend(title="星の数")
plt.tight_layout()
plt.show()
download-1



ワードクラウドでコメントを可視化する

フォントパスについては環境ごとに変更が必要です
下記はmacの例
import pandas as pd
from janome.tokenizer import Tokenizer
from wordcloud import WordCloud
import matplotlib.pyplot as plt

# Janome で形態素解析
tokenizer = Tokenizer()

def extract_nouns(text):
    """文章から名詞を抽出してリストで返す"""
    words = []
    for token in tokenizer.tokenize(text):
        part = token.part_of_speech.split(',')[0]
        if part == "名詞":
            words.append(token.surface)
    return words

# 全コメントから名詞を抽出
all_words = []
for comment in df["コメント"]:
    all_words.extend(extract_nouns(comment))
text_for_wc = " ".join(all_words)

# WordCloud 作成
wc = WordCloud(
    font_path="/System/Library/Fonts/ヒラギノ角ゴシック W6.ttc",
    width=800, height=600,
    background_color="white",
    colormap="viridis"
).generate(text_for_wc)

# 画像表示
plt.figure(figsize=(10, 8))
plt.imshow(wc, interpolation="bilinear")
plt.axis("off")
plt.tight_layout()
plt.show()
download-3



終わりに

はじめにデータを取得した際は
63件ありました。

でも動画を撮る際には
58件に減っていました。

ここから言えることとしては

Google口コミが消されています!!!


Googleさん
口コミは重要なので消さないでください!!!

消した口コミ
元に戻してください

消費者が正しい判断ができるように
口コミは正しくあるべきです

良いものは良い
悪いものは悪い

悪いものが良いように操作されたり
その逆も絶対にあってはならんことです。

また、口コミを鵜呑みにせず
実際に見てみることも重要だったりします。

まあ、今回の件は
確実に消されているので
正しい口コミとして
判断できない状態になっています。

今後の展開が楽しみですね!!!
それでは。




テレビの選挙特番などで
「当確」って出てきますよね。
あれの推定方法などについてです。

解説動画はこちら



テレビ番組によっては開票が行われてすぐに
当確が出てしまったりするケースがあります。

なぜこんなにも早く当確が出るのか
選挙の出口予想の仕組みについて解説します。




出口調査と区間推定の仕組み

選挙の当確を出すためには、それぞれの候補者の得票率を
「だいたい、ここからこの位までの区間に入ってるんじゃないか?」
という区間推定を行なっています。

出口調査で得られた、ある候補者の得票率を p とすると

その区間は次のような計算式で求められます。
スクリーンショット 2025-07-19 15.20.10
ここで必要になってくるのは
出口調査に必要な人数( n ) です。

この区間の誤差を少なくするためには
ある程度の人数が必要で、一般的には無作為に選ばれた
400人ほどが必要になってきます。




区間推定の計算

たとえばある選挙において
候補者 A , B の二人がいるとして
出口調査400人の結果が 
A : 220人
B : 180人
だったとします。

この際のA候補の得票率は

p = 220/400 = 0.55 

全体での得票率の区間の推定は
スクリーンショット 2025-07-19 17.52.43

50.12% ~ 59.88%
となります。

得票率の下限が50%を上回っているため
このまま行けば勝ちが見えてきます。



これが400人中210人だった場合はどうでしょうか

推定得票率: 52.50%
95% 信頼区間: 47.61% ~ 57.39%

これだと得票率の下限が50%を下回っているので
まだ決着がつけられません

もう少しサンプル数が増えた場合はどうなるでしょうか

今度は出口調査で4000人中2100人としてみます。
推定得票率: 52.50%
95% 信頼区間: 50.95% ~ 54.05%

n 調査人数が増えるほど誤差が少なくなり
推定された区間は短くなります。




実際には当確を決めるには

実際には候補者も多く、出口調査のみでは
正確には決まらないことが多いです。


スクリーンショット 2025-07-19 15.52.41

という計算式で当確ラインがもとまります。

これを用いて開票率が進むにつれ
区間推定の下限が当確ラインを超える得票率が獲得できている場合
当確が出せるということになります。






区間推定で当確を計算するコード

候補者が2人
開票が進んで、得票数と全体の数が分かったとします。
関数の引数に入力すると、当確結果がわかります。

最初は全体400 , 獲得210票とします。
import math

def check_win(candidate_votes, total_samples, confidence=0.99):
    if total_samples == 0:
        print("❌ 標本数が0のため、判定できません。")
        return

    # 推定得票率
    phat = candidate_votes / total_samples
    
    # z値の選択
    z = 1.96 if confidence == 0.95 else 2.58 if confidence == 0.99 else 1.64
    
    # 標準誤差と信頼区間
    se = math.sqrt(phat * (1 - phat) / total_samples)
    lower = phat - z * se
    upper = phat + z * se
    
    # 当確ラインの計算
    win_threshold = (1 + math.sqrt(z**2 / (z**2 + total_samples))) / 2

    # 表示
    print(f"推定得票率 : {phat:.2%}")
    print(f"{int(confidence * 100)}% 信頼区間 : {lower:.2%} ~ {upper:.2%}")
    print(f"当確ライン(開票数={total_samples}) : {win_threshold:.2%}")
    
    # 当確判定
    if lower >= win_threshold:
        print("OK : 候補者は『当確』と判断できます。")
    else:
        print("X :  まだ『当確』とは言えません。")

# 使用例
check_win(candidate_votes=210, total_samples=400, confidence=0.95)
推定得票率 : 52.50%
95% 信頼区間 : 47.61% ~ 57.39%
当確ライン(開票数=400) : 54.88%
X :  まだ『当確』とは言えません。

信頼区間の下限は
当確ラインを上回らないので、まだ当確出ません。


2100 , 4000 でやってみると

推定得票率 : 52.50%
95% 信頼区間 : 50.95% ~ 54.05%
当確ライン(開票数=4000) : 51.55%
X :  まだ『当確』とは言えません。

少し、区間が狭まりましたが
信頼区間の下限は
当確ラインを上回らないので、まだ当確出ません。


21000 , 40000 でやってみると

推定得票率 : 52.50%
95% 信頼区間 : 52.01% ~ 52.99%
当確ライン(開票数=40000) : 50.49%
OK : 候補者は『当確』と判断できます。


ようやく上回りました
これでようやく当確が出せるようになります。


出口調査で最初から大差がついている場合は
開票前にすでに決着がついている場合もあるようです。


今回は選挙の当確や
出口調査の仕組みについてでした。

こういった統計を用いた計算なんかも
Pythonを用いると簡単に計算できますね

選挙以外にも使えるので
覚えておくと仕事の幅が広がって便利です。

それでは。


このページのトップヘ