2つの独立なデータの正規分布によるベイズ分析
CmdStanPy対応
正規分布は次式で与えられる(Gelman et
al., 2014)。
独立な2のデータ、XとY、が与えられたとき、それらが正規分布に従うとしてベイズ分析を行うStanスクリプトをリスト1のように用意した、パラメータは、データXに関するものは添字1を付け、データYに関するものは添字2を付けている。ファイルはbanormalcspfiles.zipにまとめた。
リスト1 正規分布に従う2つの独立なデータのStanスクリプト(ba_normal.stan)
data {
int n1;
array[n1] real x;
int n2;
array[n2] real y;
real min1;
real max1;
real min2;
real max2;
}
parameters {
real<lower=0, upper=1>
vmu1;
real<lower=0, upper=1>
vsgm1;
real<lower=0, upper=1>
vmu2;
real<lower=0, upper=1>
vsgm2;
}
transformed parameters {
real mu1;
real<lower=0> sgm1;
real mu2;
real<lower=0> sgm2;
/*
Parameters rescaled
*/
mu1 = min1 + (max1 - min1) *
vmu1;
sgm1 = 0.0001 + (max1 -
min1) * vsgm1 / 2.0;
mu2 = min2 + (max2 - min2) *
vmu2;
sgm2 = 0.0001 + (max2 -
min2) * vsgm2 / 2.0;
}
model {
vsgm1 ~ beta(1, 1);
vmu2 ~ beta(1, 1);
vsgm2 ~ beta(1, 1);
x ~ normal(mu1, sgm1);
y ~ normal(mu2, sgm2);
}
乱数の生成の変域を区間に制約して生成している。すなわち
vmu1 ~ beta(1, 1);
vsgm1 ~ beta(1, 1);
vmu2 ~ beta(1, 1);
vsgm2 ~ beta(1, 1);
である。これらの乱数を、データに合わせた変域の乱数に変換して、モデルをデータに適用している。すなわち、
mu1 = min1 + (max1 - min1) *
vmu1;
sgm1 = 0.0001 + (max1 -
min1) * vsgm1 / 2.0;
mu2 = min2 + (max2 - min2) *
vmu2;
sgm2 = 0.0001 + (max2 -
min2) * vsgm2 / 2.0;
と変換したパラメータにより、データとモデルの関係が次のように設定されている。
x ~ normal(mu1, sgm1);
y ~ normal(mu2, sgm2);
リスト1のStanスクリプトをCmdStanPyを利用して用い、ベイズ分析を行うPythonスクリプトをリスト2のように用意した。CmdStanPyの簡単な準備方法の説明を、このウェブサイトで行った。
リスト2 リスト1のStanスクリプトをCmdStanPyを利用して用いるPythonスクリプト(ba_normal.py)
from cmdstanpy import CmdStanModel
import numpy as np
import scipy.stats as ss
import matplotlib.pyplot as plt
import arviz as az
import pandas as pd
import seaborn as sb
inf_nm = input('Input data
file(*.xlsx) = ')
dframe = pd.read_excel(inf_nm)
# データの読み込み
df_keys = dframe.keys()
x_name = df_keys[1]
y_name = df_keys[2]
print(x_name, y_name)
x = dframe[x_name].values
x = x[np.isnan(x)==False] # 空セルデータの削除
print('X =\n', x)
y = dframe[y_name].values
y = y[np.isnan(y)==False] # 空セルデータの削除
print('Y =\n', y)
mean1 = np.mean(x)
sd1 = np.std(x)
print('mean1 =', mean1, ' sd1 =', sd1)
mean2 = np.mean(y)
sd2 = np.std(y)
print('mean2 =', mean2, ' sd2 =', sd2)
min1 = np.min(x)
max1 = np.max(x)
min2 = np.min(y)
max2 = np.max(y)
print(min1,' ', max1)
print(min2,' ', max2)
Data = {'n1':len(x), 'x':x,
'min1':min1, 'max1':max1,
'n2':len(y), 'y':y, 'min2':min2, 'max2':max2}
model =
CmdStanModel(stan_file="ba_normal.stan")
# コンパイル
fit = model.sample(data=Data)
# サンプリング
inf_data = az.from_cmdstanpy(fit)
# Arviz(az)用のデータに変換
# トレースプロット
az.plot_trace(inf_data,
var_names=['mu1','sgm1','mu2','sgm2'])
plt.tight_layout()
plt.show()
pdf_fit = fit.draws_pd() # Pandas DataFrameに変換
"""
事後分布の中央値を点推定値とする
"""
mu1 = np.median(pdf_fit['mu1'])
sgm1 = np.median(pdf_fit['sgm1'])
mu2 = np.median(pdf_fit['mu2'])
sgm2 = np.median(pdf_fit['sgm2'])
"""
パラメータmu1とmu2の事後分布
"""
plt.title('Posterior
distributions' +f'\nmu1={mu1:.2f},
mu2={mu2:.2f}')
sb.kdeplot(pdf_fit['mu1'],
label=x_name)
sb.kdeplot(pdf_fit['mu2'],
label=y_name)
plt.xlabel('mu',
fontsize=14)
plt.legend()
plt.show()
"""
パラメータmu1とmu2の差の事後分布と事後確率
"""
diff_mu1_mu2 = pdf_fit['mu1'] -
pdf_fit['mu2']
diff_mu_med = np.median(diff_mu1_mu2)
pos_idx = diff_mu1_mu2 > 0
neg_idx = diff_mu1_mu2 < 0
prop_pos = np.mean(pos_idx) # P(mu1-mu2>0)
prop_neg = np.mean(neg_idx) # P(mu1-mu2<0)
sb.kdeplot(diff_mu1_mu2)
plt.xlabel('mu1-mu2', fontsize=14)
plt.title(f'P(mu1>mu2)={prop_pos:.5f},
P(mu1<mu2)={prop_neg:.5f}' +
f' med.={diff_mu_med:.2f}')
plt.tight_layout()
plt.show()
"""
パラメータsgm1とsgm2の事後分布
"""
plt.title('Posterior distributions'
f'\nsgm1={sgm1:.1f},
sgm2={sgm2:.2f}')
sb.kdeplot(pdf_fit['sgm1'],
label=x_name)
sb.kdeplot(pdf_fit['sgm2'],
label=y_name)
plt.xlabel('sgm', fontsize=14)
plt.legend()
plt.show()
"""
パラメータsgm1とsgm2の差の事後分布と事後確率
"""
diff_sgm1_sgm2 = pdf_fit['sgm1'] -
pdf_fit['sgm2']
diff_sgm_med =
np.median(diff_sgm1_sgm2)
prop_pos = np.mean(diff_sgm1_sgm2 >
0) # P(mu1-mu2>0)
prop_neg = np.mean(diff_sgm1_sgm2 <
0) # P(mu1-mu2<0)
sb.kdeplot(diff_sgm1_sgm2)
plt.xlabel('sgm1-sgm2', fontsize=14)
plt.title(f'P(sgm1>sgm2)={prop_pos:.5f},
P(sgm1<sgm2)={prop_neg:.5f}' +
f' med.={diff_sgm_med:.2f}')
plt.tight_layout()
plt.show()
"""
ヒストグラムと密度関数
"""
plt.hist(x, density=True, color='b',
alpha=0.3, label=x_name)
plt.hist(y, density=True, color='r',
alpha=0.3, label=y_name)
minxy = min1 if min1 < min2 else
min2
maxxy = max1 if max1 > max2 else
max2
x_coord = np.linspace(minxy, maxxy,
1000)
y_coord1 = ss.norm.pdf(x_coord,
loc=mu1, scale=sgm1)
plt.plot(x_coord, y_coord1, color='b',
label=x_name)
y_coord2 = ss.norm.pdf(x_coord,
loc=mu2, scale=sgm2)
plt.plot(x_coord, y_coord2, color='r',
label=y_name)
plt.title(f'{x_name}: mu1={mu1:.1f}, sgm1={sgm1:.1f}' +
f'\n{y_name}:
mu2={mu2:.1f},
sgm2={sgm2:.1f}')
plt.legend()
plt.show()
Stanスクリプトのコンパイルを、次のコードで行っている。
model = CmdStanModel(stan_file="ba_normal.stan")
コンパイルされたmodelによるMCMCサンプリングを次のコードで実行している。
fit = model.sample(data=Data)
サンプルの統計を、次のコードで表示している。
print(fit.summary())
MCMCサンプリングのトレースを描くために、MCMCサンプリングの結果をarviz(az)のInferenceData型に変換している。
inf_data = az.from_cmdstanpy(fit)
変換されたデータinf_dataを用いて、次のスクリプトによりトレース図を描いている。
az.plot_trace(inf_data,
var_names=['mu1','sgm1','mu2','sgm2'])
plt.tight_layout()
plt.show()
MCMCサンプルを取り出すために、次のコードでサンプルfitをPandasのDataFrame型に変換している。
pdf_fit = fit.draws_pd()
各パラメータのサンプルは、例えば次のスクリプトのようにPandasのDataFrameから取り出せる。
plt.title('Posterior distributions'
+f'\nmu1={mu1:.2f}, mu2={mu2:.2f}')
sb.kdeplot(pdf_fit['mu1'],
label=x_name)
sb.kdeplot(pdf_fit['mu2'],
label=y_name)
plt.xlabel('mu', fontsize=14)
plt.legend()
plt.show()
上のスクリプトは、パラメータmu1とmu2のサンプルを取り出して事後分布のグラフを描くものである。
リスト2のスクリプトを、MiniConda/Ubuntu24/WSL/Windowsにおいて実行した。CmdStanPyの簡単な準備方法は、このウェブサイトで説明した。
リスト1とリスト2のファイル、および入力データファイルを同じフォルダにおいて、リスト2のスクリプトを実行する。
次のコマンド
(stan) *****/banormalcspfiles$
python ba_normal.py
を実行すると、入力データファイル名を聞いてくる。次の例では、data.xlsxを設定している。
Input data
file(*.xlsx) = data.xlsx
入力データファイルは、図1a、図1bの形式で、Excelファイルとして用意する。
図1a
図1b
ファイルの形式は、1行目に変数名を設定する。1列目は空欄でもよい。データは2行目から設定する。1列目はデータの識別用であり、分析では用いられない。2列目と3列目にデータ値を設定する。データ数が2つのグループで異なるときは、少ない方のデータは空欄にしておけばよい。空欄はスクリプトの実行時に無視される。
入力データファイル名を設定してEnterキーを押すと、データが読み込まれ、Stanスクリプトのコンパイルが始まる。コンパイルには、時間が掛かるが、コンパイル後、MCMCサンプリングが実行される。
MCMCサンプリングが終了すると、トレース図が表示される(図2)。
図2
図2のWindowの右上角のX印アイコンをクリックして閉じると、次に、パラメータmu1とmu2の事後分布が表示される(図3)。
図3
図3のWindowを閉じると、図4のグラフが表示される(図4)。
図4
パメータの差mu1-mu2の事後分布のグラフである。mu1-mu2の正である確率が90%であるので、かなりの確信をもってmu1-mu2が正であると言える。
図4のWindowを閉じると、図5のグラフが表示される。
図5
sgm1とsgm2の事後分布のグラフである。
図5のWindowを閉じると、図6のグラフが表示される。
図6
パラメータの差sgm1-sgm2の事後分布と、差sgm1-sgm2の事後分布が正である確率および負である確率が表示されている。正である確率が6%であるので、sgm1の方が小さい傾向にあると言える。
図6のWindowを閉じると、図7のグラフが表示される。
図7
データのヒストグラムと、パラメータの点推定値によるモデルのグラフである。点推定値は、中央値を採用している。Gelman,ら(2021)は、平均値より中央値の方が安定しているとして、中央値の採用を勧めている。
図7のWindowを閉じると、スクリプトの実行終了である。端末には、以下のような出力が表示されている。
16:16:19 - cmdstanpy - INFO - CmdStan
done processing.
Mean
MCSE StdDev ...
ESS_tail ESS_bulk/s R_hat
lp__ -111.045000 0.059641 1.748200 ...
942.659
18647.6 1.005230
vmu1 0.406446 0.001400 0.074091 ...
2272.680
51898.7 1.004370
vsgm1 0.612878 0.003088 0.111810 ... 783.638 31889.7 1.002200
vmu2 0.366525 0.001283 0.061478 ...
1803.470
43799.1 1.001790
vsgm2 0.548595 0.002366 0.095910 ...
1214.510
41730.8 0.999883
mu1 60.786900 0.040610 2.148640 ...
2272.680
51898.7 1.004360
sgm1 8.886830 0.044776 1.621250 ... 783.638 31889.7 1.002200
mu2 56.226700 0.060297 2.889490 ...
1803.470
43799.1 1.001790
sgm2 12.892100 0.055610 2.253890 ...
1214.510
41730.8 0.999883
[9 rows x 11 columns]
(stan) *****/banormalcspfiles$
Gelman, A., Carlin, J. B., Stern, H. S., Dunson, D. B.,
Vehtari, A., & Rubin, D. B. (2014). Bayesian data analysis, 3rd. Ed.
CRC Press.
Gelman, A., Hill, J., &
Vehtari, A. (2021). Regression and other stories. Cambridge University