2×2クロス表のベイズ分析
2025.11改訂
Spiegelhalter(2019)は、腕組み(arm crossing)において左腕が上になる確率に性差があるかどうかの検定例を取り上げている。
女性14名において5名が左腕が上であり、男性40名において17名が左腕が上であった。Spiegelhalter(2019)では、カイ2乗検定が説明されているが、ベイズ分析を試みる。
いま、2つの条件、列1と列2、のそれぞれにおいて2つの属性、行1と行2、のデータが表1に示す個数得られたとする。Spiegelhalterの例では、列1が女性、列2が男性であり、行1が左腕が上、行2が右腕が上である。
|
表1 2×2分割表 |
||
|
|
列1 |
列2 |
|
行1 |
|
|
|
行2 |
|
|
すなわち、表2のようになる。
|
表2 2×2分割表(表1の改変) |
||
|
|
女性 |
男性 |
|
左腕が上 |
|
|
|
右腕が上 |
|
|
女性の条件(列1)において左腕が上(行1)である確率を
、男性(列2)において左腕が上(行1)である確率を
とおき、次の2項分布を設定する。
![]()
![]()
いま、表2のデータにおいて女性である(列1である)確率を
で表し、2項分布を適用する。すなわち、
![]()
とおく。
上記のモデルによるベイズ分析を行うStanスクリプトをリスト1のように用意した。
リスト1のStanスクリプトを用いてベイズ分析を行うスクリプトをリスト2のように用意した。ファイルは、c10files.zipにまとめた。
リスト1のスクリプトファイル(xtable.stan)とリスト2のスクリプトファイルを同じフォルダに置く。そのフォルダにカレントディレクトリ(フォルダ)を移す。CmdStanPyのインストールされている環境において、次のコマンドを実行する。CmdStanPyについては、簡単な説明をこのウェブサイトに用意した。
(stan) *****/c10files$
python ba_xtable.py
上のコマンドを実行すると、Stanスクリプトのコンパイルが始まる。このコンパイルは、多少時間が掛かる。コンパイルが終了すると、MCMCサンプリングが始まり、終了後、トレース図が表示される(図1)。

図1
左側に事後分布のグラフ、右側にサンプリングのトレースが描かれている。サンプリングに特に問題は無いようである。
図1のWindowを閉じると、図2のグラフが表示される。

図2
女性の条件(列1)において左腕が上(行1)である確率
、男性(列2)において左腕が上(行1)である確率
の事後分布のグラフである。ほぼ重なっており、差はなさそうである。
図2のWindowを閉じると、図3のグラフが表示される。

図3
確率
と
の差
の事後分布のグラフである。0より大きい事後確率が65.8%、0より小さい事後確率が34.2%であるので、
は0である、
と
に差はないと考えられる。すなわち、どちらの腕が上であるかの確率に、男女差は認められないと言える。
図3のWindowを閉じると、図4のグラフが表示される。

図4
パラメータ
の事後分布のグラフである。パラメータ
は、表2のデータにおいて、サンプルが女性である確率を表している。
が0.5より小さい事後確率が100.0%であるので、女性の方が少数であると言える。
図4のWindowを閉じると、スクリプトの実行終了である。
リスト1 表1のデータに対するStanスクリプト(xtable.stan)
data {
/*
o11 o12
o21 o22
*/
int o11;
int o12;
int o21;
int o22;
}
parameters {
real<lower=0, upper=1>
pfm;
//
(o11+o21)/(o11+o21+o12+o22)
real<lower=0, upper=1>
p1; // o11/(o11 + o21)
real<lower=0, upper=1>
p2; // o12/(o12 + o22)
}
model {
o11 + o21 ~
binomial(o11+o21+o12+o22, pfm);
o11 ~ binomial(o11+o21, p1);
o12 ~ binomial(o12+o22, p2);
}
generated quantities {
real diff_p;
diff_p
= p2 - p1;
}
リスト2 リスト1のStanスクリプトによりベイズ分析を行るPythonスクリプト
from cmdstanpy
import CmdStanModel
import numpy
as np
import pandas as pd
import arviz
as az
import matplotlib.pyplot
as plt
import seaborn as sb
#
# Spiegelhalter(2019), Table
10.2
#
Female Male
data = [[5, 17], # Left arm on top
[9,
23]] # Right arm on top
Data = {'o11':data[0][0],
'o12':data[0][1],
'o21':data[1][0], 'o22':data[1][1]}
model = CmdStanModel(stan_file='xtable.stan')
fit = model.sample(data=Data)
print(fit.summary())
infdata = az.from_cmdstanpy(fit) # Arviz InfereceData型へ変換
summary = az.summary(infdata)
print('\nSummary...\n',
summary)
az.plot_trace(infdata)
plt.tight_layout()
plt.show()
dframe = fit.draws_pd()
# Pandas DataFrame型へ変換
sb.kdeplot(dframe['p1'],
color='b', label='p1')
sb.kdeplot(dframe['p2'],
color='g', label='p2')
plt.xlabel('p1, p2', fontsize=14)
plt.legend(fontsize=16)
plt.tight_layout()
plt.show()
diff_p = np.array(dframe['diff_p'])
L_diff_p = diff_p <=
0.0
print('L_diff_p
=',np.mean(L_diff_p))
R_diff_p = diff_p
>= 0.0
print('R_diff_p
=', np.mean(R_diff_p))
az.plot_kde(dframe['diff_p'])
plt.vlines([0.0], 0.0, 3.5)
plt.text(0.01, 3.3, 'p2$-$p1$=0$', fontsize=16)
plt.hlines([0.0], -0.4, 0.4)
plt.text(-0.152, 0.21, f'{np.mean(L_diff_p)*100:.1f}%',
va='bottom', fontsize=16)
plt.text(0.05, 0.21, f'{np.mean(R_diff_p)*100:.1f}%',
va='bottom', fontsize=16)
plt.xlabel('p2$-$p1', fontsize=16)
plt.tight_layout()
plt.show()
pfm = np.array(dframe['pfm'])
L_pfm = pfm <=
0.5
print('L_pfm
=', np.mean(L_pfm))
R_pfm = pfm >=
0.5
print('R_pfm
=', np.mean(R_pfm))
az.plot_kde(dframe['pfm'])
plt.vlines([0.5], 0.0, 3.5)
plt.hlines([0.0], 0.0, 1.0)
plt.text(0.2, 0.21, f'{np.mean(L_pfm)*100:.1f}%',
va='bottom', fontsize=16)
plt.text(0.51, 0.21, f'{np.mean(R_pfm)*100:.1f}%',
va='bottom', fontsize=16)
plt.text(0.5, 3.5, 'pfm=0.5',
fontsize=16)
plt.xticks(np.arange(0,
1.01, 0.1))
plt.xlabel('pfm', fontsize=16)
plt.tight_layout()
plt.show()
参考文献
岡本安晴(2019) いまさら聞けないPythonでデータ分析.丸善出版
Spiegelhalter, D. (2019). The art of statistics: Learning from data. Pelican