比率のクロス表のベイズ分析
ロジスティック回帰分析
京都大学のホームページに掲載されている平成19年度と平成29年度の一般入試諸統計から合格者が200名以上の学部の合格者数内数(「認」と「他」を除く)を男女別にまとめると図1のようになった。
図1 Excelデータ(data1929.xlsx)
女子の比率をロジスティック回帰モデルで分析してみる。ロジスティック回帰モデルについては、<岡本安晴「心理学データ分析と測定」勁草書房>第4章で説明している。
表1のように各条件(行i、列j)における注目している事象の確率(図1の場合、女子の確率)をpijで表す。
表1 行条件iと列条件jにおける確率pij |
||||
|
列1 |
・・・ |
列j |
・・・ |
行1 |
p11 |
・・・ |
|
・・・ |
|
|
|
|
|
行i |
|
・・・ |
|
・・・ |
|
|
|
|
|
行条件iと列条件jの組み合わせにおける下段の度数(図1では女子の数)が総数(図1では女子の数と男子の数の和)の2項分布における度数として確率pijの推定を行う。確率pijは、比率「女子の数/(女子の数+男子の数)」に対応するが、ベイズ分析を行うために、総数「女子の数+男子の数」に対する該当事象の度数が「女子の数」である2項分布を適用してpijの分析を行う。
線形回帰モデルを用いるために、確率pijの値域[0、1]をロジット変換した値gijについて考える(Kruschke,2015)。
である。
ロジット変換値gijを行主効果ai、列主効果bj、交互作用(ab)jkの和に分解する。
ここで、制約条件を
と設定する。
このとき、
である。KRとKCは行条件の数と列条件の数を表す。
ロジスティック回帰モデルでは、式(1)より
となる。
各セルにおける度数は、確率pijの2項分布に従うとして、本ウェブサイト後半に示すリスト1のようにStanスクリプトを用意した。リスト1のStanスクリプトを利用して表1のベイズ分析を行うPythonスクリプトは、本ウェブサイト後半に示すリスト2として用意した。これらのファイルなどは圧縮ファイルfilesLogisticReg.zipとしてまとめたので、ダウンロード解凍すれば自由に利用できる。PythonにおけるStanの使い方については、<岡本安晴「いまさら聞けない1Pythonでデータ分析」丸善出版>の第10章〜第12章で説明している。
リスト1ではパラメータgijの事後分布が求められるので、リスト2において主効果および交互作用を式(4)〜(7)に基づく以下のスクリプトで求めている。
n_samples = len(fit['g'])
r_e = np.empty((n_samples, KR))
c_e = np.empty((n_samples, KC))
rc_e = np.empty((n_samples, KR, KC))
i = 0
for vg in fit['g']:
gdd = vg.mean()
# g..
gdc = vg.mean(axis = 0) # g.j
grd = vg.mean(axis = 1) # gi.
r_e[i] = grd - gdd
# 行主効果
c_e[i] = gdc - gdd
# 列主効果
rc_e[i] = vg -
grd.reshape((KR, 1)) - gdc + gdd # 交互作用
i += 1
リスト2のスクリプトを実行すると、入力データファイル名の設定を求めるダイアログウィンドウが表示される(図2)。
図2
入力データファイルは、図1の形式で用意する。第1行目に、列条件のカテゴリ(図1の場合は、学部名)を書き、第1列目に行条件のカテゴリ(図1では年度)、第2列目に度数のカテゴリを書く。度数データは2行目以降、3列目から該当するセルに書く。各行条件・列条件に対応するセルは、上に非該当カテゴリ(図1では「男子」、2項分布の説明では「0」で表されることが多い)の度数、下の段に該当カテゴリ(図1では「女子」、2項分布の説明では「1」で表されることが多い)の度数を書く。列条件あるいは行条件は図1のものより多くてもよい。列および行条件の個数は2以上であればよい。
図2のダイアログウィンドウにファイル名を設定して「開く」ボタンをクリックすると、画像保存用ファイル名の設定を求めるダイアログウィンドウが表示される(図3)。
図3
事後分布のグラフを保存するファイル名であるが、ファイル拡張子が「.png」である任意のファイル名を設定する。設定したファイル名の「.png」の部分が「_ra.png」などに変更された名前のファイルに画像が保存される。
図3のダイアログウィンドウにファイル名を設定して「保存」ボタンをクリックすると、計算結果を保存するためのテキストファイル名の設定を求めるダイアログウィンドウが表示される(図4)。
図4
ファイル拡張子が「.txt」の任意のテキストファイル名を設定する。
テキストファイル名を設定して「保存」ボタンをクリックすると、データの読み込みが行われ、Stanスクリプトのコンパイル・ビルドの後、MCMCサンプリングが始まる。
サンプリングが終了すると、まず行の主効果を表すパラメータai(r_e)の事後分布が表示される(図5)。
図5
平成19年度に比べて平成29年度の分布が負の方向に移動していることから、女子の比率に減少の傾向が窺われる。
図5のウィンドウを閉じると、列の主効果を表すパラメータbi(c_e)の事後分布のグラフが表示される(図6)。
図6
列の主効果から女子の比率は、文学部が最も高く、以下、農学部、法学部、理学部、工学部の順に女子の比率が下がっていることが分かる。理学部と工学部の女子の比率が他の学部より低いことが明瞭に表されている。
図6のウィンドウを閉じると、交互作用のパラメータ(ab)ij(rc_e)の事後分布が表示される(図7)。
図7
交互作用の事後分布の両側に位置しているのは、平成29年×法学部と兵籍19年×法学部の交互作用の事後分布であるが、これは主効果で説明される効果以上に農学部の女子の比率が低い状態から高い状態に増加したことを示している。その次の内側の事後分布の組は平成19年×文学部と平成29年×文学部の交互作用である。これは、主効果で説明される値を基準としたとき、文学部はそれ以上に女子の高い比率から低い比率に変化したことを示している。
図7のウィンドウを閉じると、次にpijの事後分布の中央値のグラフが表示される(図8)。
図8
平成19年度と平成29年度を比べたとき、文学部の女子の比率が減少して、法学部の女子の比率が増加していることが分かる。これは、交互作用のグラフ(図7)に対応している。
図9
図8のウィンドウを閉じると、次に横軸に平成19年と平成29年(行条件)を取ったpijの事後分布の中央値のグラフが表示される。文学部の女子の比率の減少と法学部の女子の比率の増加が表されいる。農学部の女子の比率の減少が見られるが、これは交互作用としては顕著ではない(図7)。
図9のウィンドウを閉じると、保存ファイエル名が出力されて、スクリプトの実行終了である(図10)。
図10 実行終了時の端末画面
実行終了後、図4で指定したテキストファイルををPythonのIDLEのエディタで開こうとすると、図8のようなダイアログボックスが表示されることがある。
図8
このときは、「s-jis」と設定して「OK」ボタンをクリックすればよい(図9)。
図9、
出力テキストファイルは、以下のような内容である。事後分布の平均値、中央値、四分位数、95%CIが出力されている。
入力データファイル =
D:/yasuharu/www/LaCoocan/Python/IntroPyStat/BysLogistic/filesLogisticReg/data1929.xlsx
行変数:['平成19年', '平成29年']
列変数:['文学部' '法学部' '理学部' '工学部' '農学部']
P = 女子 / (男子 + 女子)
行主効果
平成19年
平均値 = 0.037
中央値 = 0.038
Q1 = 0.007, Q3 = 0.067
95%確信区間 = [-0.050, 0.125]
平成29年
平均値 = -0.037
中央値 = -0.038
Q1 =
-0.067, Q3 = -0.007
95%確信区間 = [-0.125, 0.050]
列主効果
文学部
平均値 = 1.074
中央値 = 1.076
Q1 = 1.016, Q3 = 1.131
95%確信区間 = [0.909, 1.233]
法学部
平均値 = 0.169
中央値 = 0.170
Q1 = 0.115, Q3 = 0.223
95%確信区間 = [0.005, 0.329]
理学部
平均値 = -0.811
中央値 = -0.808
Q1 =
-0.881, Q3 = -0.740
95%確信区間 = [-1.020, -0.609]
工学部
平均値 = -1.130
中央値 = -1.128
Q1 =
-1.182, Q3 = -1.077
95%確信区間 = [-1.285, -0.979]
農学部
平均値 = 0.698
中央値 = 0.696
Q1 = 0.645, Q3 = 0.753
95%確信区間 = [0.541, 0.861]
交互作用
平成19年×文学部
平均値 = 0.124
中央値 = 0.125
Q1 = 0.064, Q3 = 0.181
95%確信区間 = [-0.050, 0.300]
平成19年×法学部
平均値 = -0.198
中央値 = -0.199
Q1 =
-0.257, Q3 = -0.141
95%確信区間 = [-0.365, -0.031]
平成19年×理学部
平均値 = 0.047
中央値 = 0.048
Q1 = -0.024, Q3 = 0.119
95%確信区間 = [-0.166, 0.262]
平成19年×工学部
平均値 = -0.046
中央値 = -0.045
Q1 =
-0.098, Q3 = 0.007
95%確信区間 = [-0.199, 0.105]
平成19年×農学部
平均値 = 0.074
中央値 = 0.075
Q1 = 0.020, Q3 = 0.126
95%確信区間 = [-0.081, 0.231]
平成29年×文学部
平均値 = -0.124
中央値 = -0.125
Q1 =
-0.181, Q3 = -0.064
95%確信区間 = [-0.300, 0.050]
平成29年×法学部
平均値 = 0.198
中央値 = 0.199
Q1 = 0.141, Q3 = 0.257
95%確信区間 = [0.031, 0.365]
平成29年×理学部
平均値 = -0.047
中央値 = -0.048
Q1 =
-0.119, Q3 = 0.024
95%確信区間 = [-0.262, 0.166]
平成29年×工学部
平均値 = 0.046
中央値 = 0.045
Q1 =
-0.007, Q3 = 0.098
95%確信区間 = [-0.105, 0.199]
平成29年×農学部
平均値 = -0.074
中央値 = -0.075
Q1 =
-0.126, Q3 = -0.020
95%確信区間 = [-0.231, 0.081]
Kruschke, J. K. (2015). Doing Bayesian Data Analysis, 2nd ed. Academic Press.
リスト1 ロジスティック回帰分析用Stanスクリプト(LogisModel.stan)
data {
int KR;
int KC;
int X[KR, KC, 2];
}
parameters {
real g[KR, KC];
}
transformed parameters {
real<lower = 0.0, upper =
1.0> p[KR, KC];
for (i in 1:KR)
for
(j in 1:KC)
p[i][j] = inv_logit(g[i][j]);
}
model {
for (i in 1:KR)
for (j in 1:KC) {
g[i][j] ~ normal(0.0, 1000.0);
}
for (i in 1:KR)
for
(j in 1:KC) {
X[i][j][2] ~ binomial(X[i][j][1] + X[i][j][2], p[i][j]);
}
}
リスト2 リスト1のStanスクリプトを用いるPythonスクリプト(MainLogistic.py)
#
# Yasuharu
Okamoto, 2020.08
#
import tkinter as tk
import tkinter.filedialog as fd
import pandas as pd
import numpy as np
import pystan
import matplotlib.pyplot as plt
import seaborn as sb
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('入力データExcelファイル', types = [('Excel(*.xlsx)', '*.xlsx')])
foutnm = askoutfilename(msg = '画像保存ファイル', types = [('画像 (*.png)', '*.png')],
deftype = '.png')
ftxtoutnm = askoutfilename(msg = 'テキスト出力ファイル', 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
print(data)
KC = len(data[0]) - 2
print(KC)
KR = (len(data) - 1) // 2
print(KR)
NameC = data[0][2:]
NameR = []
for i in range(KR):
NameR.append(data[2*i +
1][0])
print(NameC)
print(NameR)
print('\nP = {0} / ({1} +
{0})\n'.format(data[2][1], data[1][1]))
fout.write('\n行変数:{}\n'.format(NameR))
fout.write('列変数:{}\n'.format(NameC))
fout.write('\n\nP = {0} / ({1} +
{0})\n'.format(data[2][1], data[1][1]))
X = np.empty((KR, KC, 2), dtype = int)
for i in range(KR):
for j in range(KC):
X[i][j][0]
= data[i*2 + 1][j + 2]
X[i][j][1] = data[i*2 + 2][j + 2]
for i in range(KR):
for j in range(KC):
print('{0:>10d}'.format(X[i][j][0]), end = '')
print()
for j in range(KC):
print('{0:>10d}'.format(X[i][j][1]), end = '')
print()
StanData = {'KR': KR, 'KC': KC, 'X':
X}
sm = pystan.StanModel(file =
'LogisModel.stan')
fit = sm.sampling(data = StanData,
n_jobs = 1) # init = init_f,
n_jobs = 1)
print(fit)
SavedFiles = []
#
# 主効果と交互作用
#
n_samples = len(fit['g'])
r_e = np.empty((n_samples, KR))
c_e = np.empty((n_samples, KC))
rc_e = np.empty((n_samples, KR, KC))
i = 0
for vg in fit['g']:
gdd = vg.mean()
# g..
gdc = vg.mean(axis = 0) # g.j
grd = vg.mean(axis = 1) # gi.
r_e[i] = grd - gdd
# 行主効果
c_e[i] = gdc - gdd
# 列主効果
rc_e[i] = vg -
grd.reshape((KR, 1)) - gdc + gdd # 交互作用
i += 1
# 日本語表記の設定
plt.rcParams['font.family'] = ['Yu
Gothic', 'Noto Sans CJK JP', 'Hiragino Maru Gothic Pro']
plt.title('行主効果', fontsize = 20)
fout.write('\n行主効果\n')
for i in range(KR):
sb.kdeplot(r_e[:, i], label
= NameR[i])
ra_mean = r_e[:, i].mean()
ra_025p, ra_25p, ra_50p,
ra_75p, ra_975p = \
np.percentile(r_e[:, i], [2.5, 25, 50, 75, 97.5])
fout.write('\n {}\n'.format(NameR[i]))
fout.write(' 平均値 =
{0:>.3f}\n'.format(ra_mean))
fout.write(' 中央値 =
{0:>.3f}\n'.format(ra_50p))
fout.write(' Q1 =
{0:>.3f}, Q3 =
{1:>.3f}\n'.format(ra_25p, ra_75p))
fout.write(' 95%確信区間 = [{0:>.3f},
{1:>.3f}]\n'.format(ra_025p, ra_975p))
plt.yticks([])
plt.legend()
plt.tight_layout()
plt.savefig(foutnm[:-4] + '_ra.png')
SavedFiles.append(foutnm[:-4] +
'_ra.png')
plt.show()
plt.title('列主効果', fontsize = 20)
fout.write('\n列主効果\n')
for j in range(KC ):
sb.kdeplot(c_e[:, j], label
= NameC[j])
ca_mean = c_e[:, j].mean()
ca_025p, ca_25p, ca_50p,
ca_75p, ca_975p = \
np.percentile(c_e[:, j], [2.5, 25, 50, 75, 97.5])
fout.write('\n {}\n'.format(NameC[j]))
fout.write(' 平均値 =
{0:>.3f}\n'.format(ca_mean))
fout.write(' 中央値 =
{0:>.3f}\n'.format(ca_50p))
fout.write(' Q1 =
{0:>.3f}, Q3 =
{1:>.3f}\n'.format(ca_25p, ca_75p))
fout.write(' 95%確信区間 = [{0:>.3f}, {1:>.3f}]\n'.format(ca_025p,
ca_975p))
plt.yticks([])
plt.legend()
plt.tight_layout()
plt.savefig(foutnm[:-4] + '_ca.png')
SavedFiles.append(foutnm[:-4] +
'_ca.png')
plt.show()
plt.title('交互作用', fontsize = 20)
fout.write('\n交互作用\n')
for i in range(KR):
for j in range(KC):
sb.kdeplot(rc_e[:, i, j], label = r'{0}$\times${1}'.format(
NameR[i], NameC[j]))
ga_mean = rc_e[:, i, j].mean()
ga_025p, ga_25p, ga_50p, ga_75p, ga_975p = \
np.percentile(rc_e[:, i, j], [2.5, 25, 50, 75, 97.5])
fout.write('\n
{0}×{1}\n'.format(
NameR[i], NameC[j]))
fout.write(' 平均値 =
{0:>.3f}\n'.format(ga_mean))
fout.write(' 中央値 =
{0:>.3f}\n'.format(ga_50p))
fout.write(' Q1 =
{0:>.3f}, Q3 =
{1:>.3f}\n'.format(ga_25p, ga_75p))
fout.write(' 95%確信区間 = [{0:>.3f},
{1:>.3f}]\n'.format(ga_025p, ga_975p))
plt.yticks([])
plt.legend()
plt.tight_layout()
plt.savefig(foutnm[:-4] + '_rca.png')
SavedFiles.append(foutnm[:-4] +
'_rca.png')
plt.show()
p_med = np.empty((KR, KC))
for i in range(KR):
for j in range(KC):
p_med[i][j] = np.percentile(fit['p'][:, i, j], 50)
plt.title('$\hatp_{ij}$の中央値', fontsize = 20)
for i in range(KR):
plt.plot(range(KC),
p_med[i], label = NameR[i])
plt.ylabel('$\hatp_{ij}$')
plt.xticks(range(KC), NameC)
plt.legend()
plt.savefig(foutnm[:-4] + '.pr.png')
SavedFiles.append(foutnm[:-4] +
'_pr.png')
plt.show()
plt.title('$\hatp_{ij}$の中央値', fontsize = 20)
for j in range(KC):
plt.plot(range(KR), p_med[:,
j], label = NameC[j])
plt.ylabel('$\hatp_{ij}$')
plt.xticks(range(KR), NameR)
plt.legend()
plt.savefig(foutnm[:-4] + '_pc.png')
SavedFiles.append(foutnm[:-4] +
'_pc.png')
plt.show()
fout.close()
SavedFiles.append(ftxtoutnm)
print('\n保存ファイル:')
for nm in SavedFiles:
print(' ', nm)
print()