以下の内容はhttps://yamaimo.hatenablog.jp/entry/2025/08/24/220000より取得しました。


ルドー分析の裏側の話。(その2)

前回は分析用のテーブルを作った。

今回はそれを使って実際の分析を進めていく。

出目の分析

まずは出目の分布がどうなっているか。

分析用のテーブルから出目を抽出して分析する:

# 対象のプレイヤーの出目だけ抽出する
def extract_dices(log_table: pd.DataFrame, player: Player) -> pd.Series:
    dices_series = log_table.loc[log_table["player"] == player, "dices"]

    dice_list = []
    for dices in dices_series:
        # 数値を10進数の数字にして、各桁を数値に戻し、リストに加える
        dice_list += list(map(int, str(dices)))

    return pd.Series(dice_list)

# 各プレイヤーに対する出目を抽出し、分布を得る
dices = {}
dices_dist = {}
for player in Player:
    dices[player] = extract_dices(log_table, player)
    dices_dist[player] = dices[player].value_counts().sort_index()

そして分布を棒グラフで描画:

# 表示での名前と対応するプレイヤーを定義しておく
display = {
    "ばんちょー": Player.R,
    "ししろん": Player.G,
    "わためぇ": Player.B,
    "ちは": Player.Y,
}

# 分布を棒グラフで描画
fig, ax = plt.subplots(2, 2, figsize=(8, 5), sharex=True, sharey=True)
fig.suptitle("出目の分布")

for i, (name, player) in enumerate(display.items()):
    row, col = divmod(i, 2)
    mean = dices[player].mean()
    std = dices[player].std()
    ax[row, col].set_title(f"{name} (平均: {mean:.2f}, 標準偏差: {std:.2f})")
    dices_dist[player].plot.bar(ax=ax[row, col])
    ax[row, col].grid(True, axis="y")
fig.tight_layout()

これで得られたのが次のグラフ:

出目の分布

わためぇの出目がやたらいいのは最初の記事で言及したとおり。

で、じゃあ有効な出目だけで分布を取った場合はどうかなと追加で調べたのが以下:

# 対象のプレイヤーの有効な出目だけ抽出する
# (有効な出目にならなかった場合、出目は0とする)
def extract_valid_dices(log_table: pd.DataFrame, player: Player) -> tuple[pd.Series, int]:
    dices_series = log_table.loc[log_table["player"] == player, "dices"]

    dice_list = []
    invalid_count = 0
    for dices in dices_series:
        # 数値を10進数の数字にして、各桁を数値に戻す
        all_items = list(map(int, str(dices)))
        
        # 1つだけなら常に有効
        # 2つ以上の場合、最後の数字が6ならそれだけ有効
        if len(all_items) == 1:
            dice_list.append(all_items[0])
        else:
            if all_items[-1] == 6:
                dice_list.append(all_items[-1])
                invalid_count += len(all_items) - 1
            else:
                dice_list.append(0)
                invalid_count += len(all_items)

    return pd.Series(dice_list), invalid_count

# 各プレイヤーに対する有効な出目、無効になった数を抽出し、分布を得る
valid_dices = {}
invalid_count = {}
valid_dices_dist = {}
for player in Player:
    valid_dices[player], invalid_count[player] = extract_valid_dices(log_table, player)
    valid_dices_dist[player] = valid_dices[player].value_counts().sort_index()

# 分布を棒グラフで描画
fig, ax = plt.subplots(2, 2, figsize=(8, 5), sharex=True, sharey=True)
fig.suptitle("有効な出目の分布")

for i, (name, player) in enumerate(display.items()):
    row, col = divmod(i, 2)
    mean = valid_dices[player].mean()
    std = valid_dices[player].std()
    ax[row, col].set_title(
        f"{name} (平均: {mean:.2f}, 標準偏差: {std:.2f}, 無効: {invalid_count[player]})"
    )
    valid_dices_dist[player].plot.bar(ax=ax[row, col])
    ax[row, col].grid(True, axis="y")
fig.tight_layout()

これで次のグラフが得られる:

有効な出目の分布(0はパス)

いやー、改めて見ても出目がおかしい(^^; 豪運シープよねぇ。

ただ、全体的な運では「千速のサイコロ」が否定されたとしても、やっぱり短期的にはやってたんじゃないかという疑惑が生まれると思うので、時系列的な分析を行ったのが次の話。

ある一定期間での運を見るということで、移動平均を取ればその目的は果たせるだろうと思い、次のようにグラフを描いてみた:

# 出目を折れ線グラフで描画
fig, ax = plt.subplots(2, 2, figsize=(10, 5), sharex=True, sharey=True)
fig.suptitle("有効な出目の時系列")
x_max = max(len(valid_dices[player]) for player in Player)

for i, (name, player) in enumerate(display.items()):
    row, col = divmod(i, 2)
    ax[row, col].set_title(name)
    valid_dices[player].plot(ax=ax[row, col], alpha=0.5, label="raw")
    # 移動平均(10回)
    valid_dices[player].rolling(10).mean().plot(ax=ax[row, col], label="roll mean(10)")
    # 移動平均(30回)
    valid_dices[player].rolling(30).mean().plot(ax=ax[row, col], label="roll mean(30)")
    ax[row, col].grid(True)
    ax[row, col].set_xlim(0, x_max)
    if i == 3:
        ax[row, col].legend()
fig.tight_layout()

有効な出目の時系列(0はパス)

こうすることで、ちはの後半ですごい結果が出るのかと思ったけど、最初の記事で言及したとおり、著しく運がよかったとは出てこなかったのは意外なところ。 むしろ、ばんちょーの波が激しいことや、ししろんの後半の失速が目についたよね。 そういうところに気付けたのは可視化のよかったところ。

キルの分析

続いてキルリーダーの分析をするために誰が誰を何回踏んだのか確認した:

# キル回数とその対象を抽出する
def extract_kill_count(log_table: pd.DataFrame, player: Player) -> pd.Series:
    kill_series = log_table.loc[log_table["player"] == player, "back_player"]
    return kill_series.value_counts()

# 各プレイヤーに対して、他のプレイヤーを何回踏んだか
kill_count = {}
for player in Player:
    kill_count[player] = extract_kill_count(log_table, player)

# テーブルの形にする
kill_table = pd.DataFrame(kill_count)
# 順番を整える
kill_table = kill_table.loc[list(Player)]
kill_table = kill_table[list(Player)]
# 行が戻された人
kill_table.index.name = "killed"
# 集計を追加
kill_table.loc["total"] = kill_table.sum()
kill_table["total"] = kill_table.sum(axis=1)
kill_table

こうして得られたテーブルが以下:

killed Player.Y Player.B Player.R Player.G total
Player.Y NaN 4.0 1.0 1.0 6.0
Player.B 4.0 NaN 2.0 4.0 10.0
Player.R 2.0 1.0 NaN 6.0 9.0
Player.G 2.0 2.0 4.0 NaN 8.0
total 8.0 7.0 7.0 11.0 33.0

記事にするときにはこの名前を適切なものに変えてる。

それにしてもししろんのキル数すごいよなぁ。

長くなったので一旦区切り。

今日はここまで!




以上の内容はhttps://yamaimo.hatenablog.jp/entry/2025/08/24/220000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14