Up

2項分布の階層的ベイズ分析

比率の比較

2025.12CmdStanPy対応に改訂

 

 

図1のExcelデータは、平成31年度男女別合格者(京都大学ホームページより:合計が200名以上の学部、内数で「認」と「他」を除く)である。

 

図1

 

学部ごとの女子学生の比率に対して、2項分布を用いたベイズ分析を行ってみる。すなわち、学部kの男子学生数を、女子学生数をで表し、女子学生の比率をとしたとき

 

 

とおく。さらに、確率は、当該の大学における女子の比率として共通の母集団からのサンプルとして考える。

 

 

ここで、は、ハイパーパラメータである(Lambert, 2018; Kruschke, 2015)。

なお、図1はデータの例であり、学部を表している列は学部以外のカテゴリで2カテゴリ以上であれば、図1の形式でExcelデータを用意すればよい。行カテゴリも性別以外のカテゴリでよいが、2項分布を適用しているので2カテゴリであればよい。

 

モデル(1)と(2)に基づいてベイズ分析を行うためのStanスクリプトを本ウェブサイト後半に示すリスト1のように用意した。本ウェブサイトのスクリプトファイルなどは圧縮ファイルfilesBinomials.zipにまとめたので、ダウンロード解凍すれば自由に利用できる。

 

リスト1のStanスクリプトを使って図1の形式のデータを分析するPythonスクリプトを、本ウェブサイトの後半に示すリスト2として用意した。

 

リスト1のStanスクリプトファイル(BinModel.stan)、リスト2のPythonスクリプトファイル(MainBinomials.py)、および入力データファイル(例えば、Data.xlsx)を同じフォルダに置く。そのフォルダにカレントディレクトリ(フォルダ)を移して、CmdStanPyのインストールされた環境で次のコマンドを実行する。CmdStanPyの簡単な説明を、このウェブサイトに用意した。

 

(stan) *****/filesBinomials$ python MainBinomials.py

 

上のコマンドを実行すると、入力データファイル名の設定を求めるダイアログウィンドウが表示される(図2)。

 

図2

 

入力データファイルは、図1の形式で用意する。1行目にグループ名(図1の場合は、学部名)を設定し、1列目に2項分布のカテゴリ値(図1の場合は、「男性」と「女性」)を書く。2項分布における該当カテゴリ値(「成功」カテゴリあるいは「1」として説明されることが多い)を3行目に、非該当カテゴリ値(「失敗」カテゴリあるいは「0」として説明されることが多い)を2行目に設定する。各グループにおけるカテゴリ値の数はそれぞれの行・列のセルに設定する。

図2では、図1のExcelデータファイル名「Data.xlsx」が設定されている。設定後、「Open」ボタンをクリックすると、画像保存用ファイル名の設定を求めるダイアログウィンドウが表示される(図3)。

 

図3

 

ファイル拡張子が「.png」である任意のファイル名を設定する。グラフは、設定したファイル名の後ろをグラフに合わせた名前に変更したファイル名で保存される。

ファイル名を設定して「Save」ボタンをクリックすると、次に、計算結果の出力用テキストファイル名の設定を求めるダイアログウィンドウが表示される(図4)。

 

図4

 

テキストファイル用ファイル名を設定して「save」ボタンをクリックすると、入力データファイルが読み込まれて処理が始まる。Stanスクリプトのコンパイルに多少時間が掛かるが、コンパイル後、MCMCサンプリングが始まる。MCMCサンプリングが終了すると、トレース図が表示される(図5)。

 

図5

 

左側に事後分布のグラフ、右側にサンプリングのトレース図が表示されている。

図5のWindowを閉じると、図6のグラフが表示される。

 

図6

 

式(2)のベータ分布のグラフの事後分布である。MCMCサンプルからabのサンプルをランダムに100対選び、各対をパラメータとするベータ分布のグラフを描いたものである。赤の太い実線で描かれた曲線は、それら100本の曲線を平均したものである。

スクリプトを、以下に示す。

 

 

idx_slct = rng.choice(len(fit['a']), size=100, replace=False)

xcoord = np.linspace(0, 1, 1000)

curve_list = []

for idx in idx_slct:

    ycoord = ss.beta.pdf(xcoord, fit['a'][idx], fit['b'][idx])

    plt.plot(xcoord,ycoord, alpha=0.3)

    curve_list.append(ycoord)

 

mn_curve = np.mean(curve_list, axis=0)

plt.plot(xcoord, mn_curve, c='r', label='Mean values')

 

 

図6の分布は、0.1から0.4の範囲に集まっているので、学部全体の傾向として、式(2)で与えられる確率(女子学生の比率)は男子よりも小さいと言えそうである。

 

図6のWindowを閉じると、図7のグラフが表示される。

 

図7

 

学部別の女子の比率の事後分布のグラフである。女子の比率に対応する2項分布の確率の事後分布は、工学部と理学部、法学部、農学部と文学部の3グループに分かれていることが表されている。

 

図7のWindowを閉じると、スクリプトの実行終了である。

 

実行終了後、図4で設定した出力ファイルを開くと以下のようになっている。

 

 

 

 

入力データファイル = /mnt/c/WorkSpace/www/LaCoocan/Python/IntroPyStat/BysBinomials/filesBinomials/Data.xlsx

 

K = 5

 

['文学部', '法学部', '理学部', '工学部', '農学部']

 

男性,  女性

 

[[130  83]

 [236  75]

 [281  32]

 [858  79]

 [189 103]]

 

 

 

P(女性)

 

 

文学部:

平均   = 0.37744

中央値 = 0.37635

Q1 = 0.35551     Q3 = 0.39894

95%確信区間 = [0.31461, 0.44447]

 

法学部:

平均   = 0.24023

中央値 = 0.23993

Q1 = 0.22343     Q3 = 0.25655

95%確信区間 = [0.19445, 0.28822]

 

理学部:

平均   = 0.10901

中央値 = 0.10856

Q1 = 0.09709     Q3 = 0.12004

95%確信区間 = [0.07655, 0.14512]

 

工学部:

平均   = 0.08691

中央値 = 0.08656

Q1 = 0.08062     Q3 = 0.09292

95%確信区間 = [0.06948, 0.10654]

 

農学部:

平均   = 0.34543

中央値 = 0.34487

Q1 = 0.32580     Q3 = 0.36416

95%確信区間 = [0.29468, 0.40066]

 

 

 

参考文献

Kruschke, J.K. (2015) Doing Bayesian data analysis. Academic Press

Lambert, B. (2018) A students guide to Bayesian statistics. SAGE.

 

 

 

リスト1 2項分による分析のStanスクリプト(BinModel.stan

 

data {

    int K;

    array[K, 2] int Data;

}

parameters {

    real<lower=0.0001, upper=1000> a;             //  Hyper parameter

    real<lower=0.0001, upper=1000> b;             //  Hyper parameter

    array[K] real<lower = 0.0, upper = 1.0> p;

}

model {

    a ~ uniform(0.0001, 1000);  //  Hyper distribution

    b ~ uniform(0.0001, 1000);  //  Hyper distribution

    for (i in 1:K)

        p[i] ~ beta(a, b);  

    for (i in 1:K)

        Data[i][2] ~ binomial(Data[i][1] + Data[i][2], p[i]);

}

 

 

 

リスト2 リスト1のStanスクリプトによるベイズ分析(MainBinomial.py

 

import tkinter as tk

import tkinter.filedialog as fd

import pandas as pd

import numpy as np

import scipy.stats as ss

from cmdstanpy import CmdStanModel

import matplotlib.pyplot as plt

import japanize_matplotlib

import seaborn as sb

import arviz as az

 

def askinfilename(msg = None, types = [('', '*.*')]):

    """

            入力用ファイル名の設定

    """

    rt = tk.Tk()

    rt.withdraw()

    flnm = fd.askopenfilename(title = msg, filetypes = types)

    rt.destroy()

    return flnm

 

def askoutfilename(msg = None, types = [('', '*.*')], deftype = '.txt'):

    """

            出力用ファイル名の設定

    """

    rt = tk.Tk()

    rt.withdraw()

    flnm = fd.asksaveasfilename(title = msg, filetypes = types,

                                           defaultextension = deftype)

    rt.destroy()

    return flnm

 

finnm = askinfilename('Input Data File', types = [('Excel(*.xlsx)', '*.xlsx')])

foutnm = askoutfilename(msg = 'Output Image File',

                        types = [('Image(*.png)', '*.png')], deftype = '.png')

ftxtoutnm = askoutfilename(msg = 'Output Text File',

                           types = [('Text (*.txt)', '*.txt')],

                           deftype = '.txt')

 

fout = open(ftxtoutnm, 'w')

fout.write('入力データファイル = {}\n'.format(finnm))

 

xlsx_fl = pd.ExcelFile(finnm)

data_xlsx = pd.read_excel(xlsx_fl,  header = None)

data = data_xlsx.values

 

K = len(data[0]) - 1

print('K = ', K)

fout.write('\nK = {}\n'.format(K))

VarGrp = []

for i in range(1, K + 1):

    VarGrp.append(data[0][i])

print(VarGrp)

fout.write('\n{}\n'.format(VarGrp))

 

VarCnd1 = data[1][0]

VarCnd2 = data[2][0]

print(VarCnd1, ', ', VarCnd2)

fout.write('\n{0:},  {1:}\n'.format(VarCnd1, VarCnd2))

 

Data = np.empty((K, 2), dtype = int)

for k in range(K):

    for i in range(2):

        Data[k][i] = data[i + 1][k + 1]

 

print(Data)

fout.write('\n{}\n'.format(Data))

 

StanData = {'K': K, 'Data': Data}

 

model = CmdStanModel(stan_file='BinModel.stan')

fit = model.sample(data=StanData)

print(fit.diagnose())

print(fit.summary())

 

 

 

infdata = az.from_cmdstanpy(fit)    # Arviz InfereceData型へ変換

az.plot_trace(infdata) 

plt.tight_layout()

plt.savefig(foutnm[:-4] + '_trace.png')

plt.show()

 

fit = fit.draws_pd()                #  Pandas DataFrame型への変換

 

print(fit.keys())

 

rng = np.random.default_rng()

 

idx_slct = rng.choice(len(fit['a']), size=100, replace=False)

xcoord = np.linspace(0, 1, 1000)

curve_list = []

for idx in idx_slct:

    ycoord = ss.beta.pdf(xcoord, fit['a'][idx], fit['b'][idx])

    plt.plot(xcoord,ycoord, alpha=0.3)

    curve_list.append(ycoord)

 

mn_curve = np.mean(curve_list, axis=0)

plt.plot(xcoord, mn_curve, c='r', label='Mean values')

plt.legend()

plt.title('Beta Distributions', fontsize=16)

plt.tight_layout()

plt.savefig(foutnm[:-4] + '_beta.png')

plt.show()

 

 

plt.figure(figsize = (9, 5))

plt.title('pの事後分布', fontsize = 18)

for k in range(K):

    med = np.median(fit[f'p[{k+1}]'])

    sb.kdeplot(fit[f'p[{k+1}]'], label = VarGrp[k] + f'\nMed.={med:.3f}')

plt.yticks([])

plt.xlabel('p', fontsize = 14)

plt.legend(loc = 'upper right', bbox_to_anchor = (1.25, 1.0), fontsize = 12)

plt.tight_layout()

plt.savefig(foutnm[:-4] + '_p.png')

plt.show()

 

print('\n\nP({})\n'.format(VarCnd2))

fout.write('\n\n\nP({})\n\n'.format(VarCnd2))

for k in range(K):

    print('\n{}:'.format(VarGrp[k]))

    fout.write('\n{}:\n'.format(VarGrp[k]))

    p_mean = fit[f'p[{k+1}]'].mean()

    p_025, Q1, Med, Q3, p_975 = np.percentile(fit[f'p[{k+1}]'],

                                              [2.5, 25, 50, 75, 97.5])

    print('平均   = {0:.5f}'.format(p_mean))

    fout.write('平均   = {0:.5f}\n'.format(p_mean))

    print('中央値 = {0:.5f}'.format(Med))

    fout.write('中央値 = {0:.5f}\n'.format(Med))

    print('Q1 = {0:.5f}     Q3 = {1:.5f}'.format(Q1, Q3))

    fout.write('Q1 = {0:.5f}     Q3 = {1:.5f}\n'.format(Q1, Q3))

    print('95%確信区間 = [{0:.5f}, {1:.5f}]'.format(p_025, p_975))

    fout.write('95%確信区間 = [{0:.5f}, {1:.5f}]\n'.format(p_025, p_975))

 

fout.close()

 

 

 

 

Up