3点識別試験のベイズ分析
グリッド法
3つの刺激の内、2つが同じ刺激Aで1つが異なる刺激Bであるとき、3つの中で他の2つと異なるものとして正しくBが選ばれる頻度を調べる実験(3点識別試験:triangular test, 3AFC Oddity)のデータのベイズ分析によってAとBの感覚量の差 の推定を行うプログラムを作成してみた。プログラムのスクリプトファイルは圧縮ファイルTriTestScriptFiles.zipとしたので、ダウンロード展開して自由に御利用頂きたい。
の推定を行うプログラムを作成してみた。プログラムのスクリプトファイルは圧縮ファイルTriTestScriptFiles.zipとしたので、ダウンロード展開して自由に御利用頂きたい。
信号検出理論に基づいて、刺激Aおよび刺激Bの感覚量が、それぞれ独立に平均 および平均
および平均 の分散
の分散 の正規分布(ガウス分布)に従うものとする(サーストンモデル)。2つの刺激Aの感覚量を
の正規分布(ガウス分布)に従うものとする(サーストンモデル)。2つの刺激Aの感覚量を および
および で表し、刺激Bの感覚量を
で表し、刺激Bの感覚量を で表すと
で表すと

である。
3点試験法において正しくBが選ばれるのは

が成り立つときである(Frijters et al., 1980, p. 177)。
いま、信号検出理論の慣例に倣って、感覚量の平均値の差を標準偏差 を単位として表し、
を単位として表し、

とおく。このとき、正答確率(Bが選ばれる確率) と感覚量の差
と感覚量の差 の関係は次式で表される(Frijters et al., 1980, eq.(1))。
の関係は次式で表される(Frijters et al., 1980, eq.(1))。

式(1)において、 の正負を入れ換え(
の正負を入れ換え( を
を で置き換え)ても値は変わらない。したがって、感覚量の座標の向きをBの平均
で置き換え)ても値は変わらない。したがって、感覚量の座標の向きをBの平均 の方がAの平均
の方がAの平均 より大きくなる方向にとって
より大きくなる方向にとって とおいても一般性は失われない。式(1)より、感覚量の差
とおいても一般性は失われない。式(1)より、感覚量の差 に対する正答確率
に対する正答確率 が与えられる。
が与えられる。
総試行数 の実験において、Bが選ばれた試行数をkとおくと、kは試行数Nの2項分布における成功試行数と考えて、確率は
の実験において、Bが選ばれた試行数をkとおくと、kは試行数Nの2項分布における成功試行数と考えて、確率は

で与えられる。この確率モデル(2)に基づいて の事後分布をグリッド法に依って求めるスクリプトをリスト1に示す。
の事後分布をグリッド法に依って求めるスクリプトをリスト1に示す。
リスト1のスクリプトを実行すると、まずNの入力が求められる。
>>> 
= RESTART:
D:\xxx\TriTestScriptFiles\TriangularTest_grid.py
N =
Nの値を入力すると、次にkの値の入力が求められる。
N = 50
k =
kの値を入力する。次の例では、Nの値として50を入力した後、kの値20が設定されている。
N = 50
k = 20
計算が終了すると、d’の事後分布が図のように表示される。

図1
図中キャプションに、事後分布のモード(d’の推定値)と95%HDIが表示されている。
図は、次のコードによって保存される。
plt.savefig(f'FigN{N}k{k}.png')
図1の場合は、FigN50k20.pngである。
以下に、N=50に対するK=10、30、40、50の場合の分析結果を示す。




N=50、k=10の場合は、正答数kがランダムな回答の確率1/3より小さいので、事後分布は左端により、モードはmode =0である。
これに対して、N=50、k=50は全試行正答であるので、d’の値はいくらでも大きい値であってもデータと矛盾しないが、d’の事前分布が10までの範囲に設定されているので、事後分布は右端に寄った分布となっている。
引用文献
Frijters, J. E. R., Kooistra, A., & Vereijken, P. F. G. (1980). Tables of d' for the triangular method and the 3-AFC signal detection procedure. Perception & Psychophysics, 1980, 27, 176-178.
"""
            
Yasuharu Okamoto, 2021.11, 2022.01
"""
import numpy as np
import scipy.stats as ss
import scipy.integrate as si
import matplotlib.pyplot as plt
import seaborn as sb
N = int(input('N = '))
k = int(input('k = '))
class Triangular:
    def __init__(self, d_prime):
       
self.d_prime = d_prime
        
    def func(self, u):
        v = ss.norm.cdf(-u
* (3.0**0.5) + self.d_prime * ((2/3)**0.5)) + \
               
ss.norm.cdf(-u * (3.0**0.5) - self.d_prime * ((2/3)**0.5)) 
       
return v * np.exp(-0.5 * (u**2)) / ((2.0 * np.pi)**0.5)
    
def Pc_d_prime(d_prime):
    d_func = Triangular(d_prime).func
    return 2 * si.quad(d_func,
0.0, +np.inf)[0]
    
dprimes = np.linspace(0, 10, 1001)
Pc = np.empty(len(dprimes))
for i in range(len(dprimes)):
    if i % 100 == 0:
       
print('{0:}/{1:}    
'.format(i, len(dprimes))) 
#, end = '\r')
    Pc[i] =
Pc_d_prime(dprimes[i])
    
postPdprime = np.empty(len(dprimes))
for i in range(len(dprimes)):
    postPdprime[i] = (Pc[i] **
k) * ((1 - Pc[i]) ** (N - k))
postPdprime = (postPdprime /
np.sum(postPdprime))
map_idx = np.argmax(postPdprime)  
MAP_Est = map_idx / 100       
print('MAP_Est =', MAP_Est)
postPcum = np.cumsum(postPdprime)
a = 0.05
Lp = 0
Rp = 0  
w_t = 1000
L_t = Lp
R_t = Rp
while True:
    if Lp == 0:
       
cumPL = 0.0
    else:
       
cumPL = postPcum[Lp-1]
    while 1.0 - postPcum[Rp]
>= a - cumPL:
        Rp
+= 1
    if Lp == 0:
        L_t
= Lp
        R_t
= Rp
        w_t
= R_t - L_t        
    elif Rp - Lp < w_t:
        L_t
= Lp
        R_t
= Rp
        w_t
= R_t - L_t
    Lp += 1
    if postPcum[Lp] >= a:
        Lp
-= 1
       
break
Lp = 1000
Rp = 1000
w_t = 1000
L_t1 = Lp
R_t1 = Rp
while True:
    if Rp == 1000:
       
cumPR = 0.0
    else:
       
cumPR = 1.0 - postPcum[Rp]
    while postPcum[Lp] >= a -
cumPR:
        if
Lp > 0:
           
Lp -= 1
 
      else:
           
break
    if Rp == 1000:
        L_t1
= Lp
        R_t1
= Rp
        w_t
= R_t1 - L_t1
    elif Rp - Lp < w_t:
        L_t1
= Lp
        R_t1
= Rp
        w_t
= R_t1 - L_t1
    Rp -= 1
    if 1.0 - postPcum[Rp] >=
a:
        Rp
+= 1
       
break
        
HDI_L = ((L_t + L_t1) / 2.0) / 100.0
HDI_R = ((R_t + R_t1) / 2.0) / 100.0
plt.plot(np.linspace(0, 10, 1001),
postPdprime)
plt.plot([MAP_Est,MAP_Est], [0,
postPdprime[map_idx]], label = 'Mode = {}'.format(MAP_Est))
plt.title(f"Psterior Distribution
for d'\nN = {N}, k = {k}", fontsize = 14)
plt.xlabel("d'", fontsize =
12)
plt.plot([0, 10], [0, 0], lw = 1, c =
'k')
plt.yticks([0])
plt.plot([HDI_L, HDI_R], [0, 0], lw =
5, label = '95% HDI = [{0:.2f}, {1:.2f}]'.format(HDI_L, HDI_R))
plt.legend()
plt.savefig(f'FigN{N}k{k}.png')
plt.show()