乙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万円では全然足りないですねーーーーーー


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

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

それでは。

今回はオンカジに有りそうな
ポーカー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さん
口コミは重要なので消さないでください!!!

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

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

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

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

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

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

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




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

今回は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
こんな感じで軌跡が赤く残るような
動画が作成できます。

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

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

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

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

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

解説動画はこちら



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

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




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

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

出口調査で得られた、ある候補者の得票率を 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を用いると簡単に計算できますね

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

それでは。


このページのトップヘ