はじめに
朝、オフィスで同僚が遅れてやってきて、こう言います。
「すいません、電車が遅れて…」
この「電車が遅れた」という言葉、本当に電車のせいでしょうか?それとも、ただの寝坊を隠すための言い訳でしょうか?
不確実な世の中の事象の因果関係を確率で計算できる「ベイジアンネットワーク」を使えば、この身近な問題を数学的にモデル化し、「結果から原因を逆算する」ことができます。
今回は、Pythonの強力な確率的グラフモデルライブラリpgmpyを使い、2階層のネットワークを構築して、この「遅刻の言い訳」の真相を確率で解き明かしてみましょう。
今回作る2階層ネットワークの構造
まずは、どのような因果関係があるのかを整理します。今回構築するネットワークは以下のようになります。ご提供いただいた図が、この構造を視覚化しています。
遅刻要因のネットワーク (円の大きさ = 発生確率)
この図から、以下の2階層の構造が読み取れます。
- 第1階層(電車の遅延要因): 「悪天候」と「事故」が起きると、「電車遅延」が発生する。
- 第2階層(遅刻の要因): 「電車遅延」と、個人の「寝坊」が合わさることで、最終的な「遅刻」が発生する。
各事象(ノード)の下に書かれている確率(%)は、証拠(結果)がない状態での初期確率(周辺確率)を示しています。例えば、「遅刻」の初期確率は32.8%となっています。
それでは、このネットワークをPythonで実装していきましょう。
実装の準備
Google Colabなどの環境で、まずはライブラリをインストールします。
pip install pgmpy
ネットワークの構築と確率の定義
Pythonコードでネットワークの構造と、それぞれの条件付き確率表(CPT)を定義します。CPTの設定値は、図の周辺確率と整合性を保ちつつ、より自然な数値に設定しました。
from pgmpy.inference import VariableElimination
import matplotlib.pyplot as plt
import networkx as nx
# 推論エンジンの初期化(最新のモデルを使用)
infer_ja = VariableElimination(model)
# 1. 各ノードの周辺確率を計算
marginal_probs_ja = {}
for node in model.nodes():
prob = infer_ja.query(variables=[node], show_progress=False)
marginal_probs_ja[node] = prob.values[1]
# 2. グラフ構造の準備
G_final = nx.DiGraph(model.edges())
# 3. 描画設定(面積を確率に比例させる)
plt.figure(figsize=(12, 8))
base_size = 4000
node_sizes = [base_size + (marginal_probs_ja[node] * 12000) for node in G_final.nodes()]
nx.draw_networkx_nodes(G_final, pos_refined,
node_size=node_sizes,
node_color="#A2D9CE",
edgecolors="#16A085",
linewidths=2)
nx.draw_networkx_edges(G_final, pos_refined,
arrows=True,
arrowstyle='-|>',
arrowsize=40,
width=2.5,
edge_color="#5D6D7E")
# ラベルと数値(%)の描画
for node, (x, y) in pos_refined.items():
p_val = marginal_probs_ja[node]
label = f"{node}\n({p_val*100:.1f}%)"
plt.text(x, y, label,
fontsize=12,
fontweight="bold",
horizontalalignment='center',
verticalalignment='center',
fontproperties=font_prop)
plt.title("確率を反映した遅刻要因ベイジアンネットワーク", size=16, pad=25, fontproperties=font_prop)
plt.axis('off')
plt.tight_layout()
plt.show()
推論してみよう
モデルができたので、推論(Inference)エンジンを使って様々なシチュエーションを計算してみます。
infer = VariableElimination(model)
シナリオ1:普通に予測する(順方向)
「今日はあいにくの悪天候だ。同僚が遅刻する確率は?」
# 証拠(evidence)として悪天候=1を設定し、遅刻の確率を計算
result_forward = infer.query(variables=['遅刻'], evidence={'悪天候': 1})
print(result_forward)
+-------+-----------+
| 遅刻 | phi(遅刻) |
+=======+===========+
| 遅刻(0) | 0.4303 |
+-------+-----------+
| 遅刻(1) | 0.5697 |
+-------+-----------+
実行すると、悪天候の影響で電車遅延リスクが高まり、最終的に遅刻する確率が初期の32.8%からグッと上がることが確認できます。
シナリオ2:結果から原因を暴く(逆推論)★ここが本番
ベイジアンネットワークの醍醐味は、結果から原因の確率を逆算できることです。
「同僚が遅刻してきた!しかも今日は快晴で天気は良い。あいつ、寝坊したんじゃないか?」
これを数学的に暴いてみましょう。
# 証拠:遅刻した(遅刻=1)、かつ悪天候ではない(悪天候=0)
# ターゲット:寝坊の確率
result_backward = infer.query(variables=['寝坊'], evidence={'遅刻': 1, '悪天候': 0})
print(result_backward)
【出力結果のイメージ】
出力結果は、以下のような完全に日本語化された表形式になります。
+-------+-----------+
| 寝坊 | phi(寝坊) |
+=======+===========+
| 寝坊(0) | 0.3552 |
+-------+-----------+
| 寝坊(1) | 0.6448 |
+-------+-----------+
なんと、快晴の日に遅刻してきた場合、それが寝坊によるものである確率が約65.8%まで跳ね上がりました!(デフォルトの寝坊確率は15%でした)。
事故が起きている可能性もゼロではありませんが、確率論的には「寝坊を疑うのが妥当」という結果になります。
まとめ
ベイジアンネットワークを使うと、「もし〜だったらどうなるか」という単純な予測だけでなく、「いま手元にある証拠(結果)から、見えない要因(原因)の確率を逆算して推測する」ことが可能になります。
今回は提供図を活用し、「遅刻」というカジュアルなテーマで2階層のネットワークを構築しましたが、この手法は医療診断(症状から病気を特定)、障害対応(エラーログから故障箇所を特定)など、実務における不確実な事象の推論に幅広く応用できます。ぜひ皆さんも pgmpy と視覚的な図を活用して、複雑な問題の確率的なシミュレーションを楽しんでみてください!
