Up

差尺度法のベイズ分析

分析スクリプトをCmdStanPy対応に改訂した(2025.10)。

CmdStanPyPythonStan)の解説を、このウェブサイトで行った。

 

 差の一対比較データに基づいて尺度構成を行うものに最尤法による差尺度法(maximum likelihood difference scaling: MLDS; Maloney & Yang, 2003)がある。現代の統計法として、ベイズ統計法が注目されているので、ベイズ統計法による差尺度法を試みた(岡本、2025pp.51-53)。ベイズ統計法の場合も基本となる確率モデルは最尤法の場合と同じであるが、ベイズ統計法を用いる場合は略称としてMLDSを用いることは不適切であるので、BDSBayesian Difference Scaling)を用いることにする。

なお、ファイルはまとめて圧縮ファイルba_files.zipとした。

 

 差尺度法では、感覚の差の比較判断を行う。刺激の感覚量をで表したとき、刺激と刺激の感覚の差はで表され、刺激と刺激の差はで表される。いま、の差をで表したとき、は平均、分散の正規分布に従うとする。すなわち、

 

 

である。

 刺激と刺激の差(違い)の方が刺激と刺激の差より大きいという判断(曲線の不等号記号を用いて「」と表す)はのときに行われるとすると、その確率は

 

 

で与えられる。ここで、は累積標準正規分布関数である。モデル(DS.2)において、感覚量の連続体の原点および単位が未定であるので、

 

 

とおいて原点と単位を設定する(Maloney & Yang, 2003)。ここで、Nは刺激値の総数であり、刺激は刺激値の小さいものから大きさの順に通し番号が付けられているものとする。

 刺激と刺激の差と刺激と刺激の差の比較判断が回行われ、その内「」の判断が回であったとすると、その確率は2項分布

 

 

で与えられる。したがって、比較判断の行われた4つ組の刺激にわたる全体の尤度は

 

 

で与えられる。ここで、

 

 

であり、は刺激の数である。4つ組の刺激において、であれば、3つの刺激に対して差の比較を行うことになる。

 

一般に、互いに異なる刺激からなる4つ組における差の比較判断の場合、の条件のもとでの差との差が構成されたとき(Maloney & Yan, 2003)、刺激の数が個のとき4つ組の可能な数は

 

 

で与えられる。差の比較実験においては、この4つ組の数のときは組であるが、のときは組となり4つ組すべてに対して比較データを取得することは困難になる。Maloney & Yang (2003)のシミュレーション実験では、のときの組の4つ組すべてを用いたときのパラメータ推定値の分散と、のときの組の4つ組からランダムに10%を選んだ484組に対するデータからのパラメータ推定値の分散はほぼ等しいことが報告されている。Maloneyらは、パラメータ推定値の分散の大きさに関係するのは、刺激の総数ではなく、4つ組の比較判断が行われた試行の総数であると説明している。このウェブサイトにおける実験例では、刺激として5個の刺激が用いられ、4つ組の数は9組である。5個のものから4個を取り出す方法は4通りであるが、さらに4個の組(i,j,s,t)から差尺度の実験用に((i,j),(s,t))、((i,s),(j,t))、((i,t),(s,j))の組み合わせがあることに注意。

 モデル(DS.1)においては2つの仮説、が正規分布に従うこと、および正規分布の分散が刺激の組み合わせに関わらず一定値であることが設定されている。これらの仮説に対して差尺度法は頑健であることがシミュレーションによって確認されている(同上)。

 

差尺度法のための実験例をこのウェブサイトに用意した。実験結果は、図1のように得られる。

 

図1

 

本ウェブサイトに用意したスクリプトは、実験結果が提示刺激の組ごとにまとめられた形式のデータを入力ファイルとするので、データをまとめるスクリプトをリスト1のように作成した。

リスト1のスクリプトを実行すると、入力データファイル名を聞いてくる。入力データファイルは、図1の形式のCSVファイルを用意する。次の例では、図1のファイルraw_disc.csvが設定されている。実験結果のファイルraw_disc.csvは、実験を実施したスクリプトの置かれているフォルダ内にあるので、コピーしてリスト1のスクリプトの置かれているフォルダ内に貼り付けてあるものとする。

 

Input Data File (*.csv) = raw_disc.csv

 

入力データファイル名が設定されると、計算が始まり、刺激刺激の数の入力が求められる。次の例では、図1のデータに合わせて5が設定されている。

 

Number of stimuli = 5

Output File Name (*.csv) = aggre_disc.csv

 

刺激数を設定すると、次に出力ファイル名の設定が求められる。出力ファイルはCSV形式のファイルなので、ファイル拡張子は「.csv」とする。上の例では、aggre_disc.csvと設定されている。

CSV形式のファイルaggre_disc.csvは、図2のようである。

 

図2

 

1行目1列目のセルに、刺激数5が設定されている。

2行目に、ijstと刺激の添え字文字が書かれ、続いてNTNが書かれている。各行には、これらの名前に相当する値が設定される。

 

図2の形式のCSVファイルが用意できたら、リスト2のスクリプトでベイズ分析を行う。リスト2では、リスStanスクリプトファイル(diff_scale.stan)が用いられている。

 

リスト2のファイル(BDS.py)とリスト3のファイル(diff_scale.stan)、および入力データファイルを同じフォルダに置く。そのフォルダにカレントディレクトリ(フォルダ)を移し、CmdStanPyのインストールされている環境で次のコマンドを実行する。CmdStanPyの解説は、このウェブサイトに用意した。

 

(stan) *****/ba_files$ python BDS.py

 

上のコマンドを実行すると、入力データファイル名を聞いてくる。

 

(stan) *****/ba_files$ python BDS.py

Data(*.csv) = aggre_disc.csv

 

入力データファイルは、図2の形式のCSVファイル名を設定する。上の例では、図2のファイル名aggre_disc.csvが設定されている。

 

ファイル名を設定すると、ファイルが読み込まれ、Stanスクリプトのコンパイルが始まる。コンパイルには、多少時間が掛かる。コンパイル後、MCMCサンプリングが始まり、終了するとトレース図が表示される(図3)。

 

図3

 

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

 

図4

 

ψの事後分布のグラフである。

は、式(DS.3)による仮定であることに注意。

 

図4のWindowを閉じると、σの事後分布を示すグラフが表示される(図5)。

 

図5

 

図5のWindowを閉じると、次に刺激と感覚の関係を示すグラフが表示される(図6)。

 

図6

 

感覚量の事後分布の中央値、50%確信区間、95%確信区間を求め、横軸に刺激の通し番号をとって描かれている。感覚量の値は、パラメータ推定のための仮定(DS.3)であることに注意。

刺激の番号は、円盤の半径と1次式の関係にある。図6に示されている感覚の強さは、半径の上に凸な関数である。

 

図6のフォームを閉じると、データにおける比率N/TNと推定されたパラメータによるモデルに基づく確率の散布図が表示される(図7)。

 

図7

 

データにおける比率とモデルに基づく確率はよく対応していると言える。

 

 

 

参考文献

 

Maloney, L. T., & Yang, J. N. (2003). Maximum likelihood difference scaling. Journal of Vision, 3, 573-585.

岡本安晴(2019)いまさら聞けないPythonでデータ分析.丸善出版

岡本安晴(2025)感覚・知覚測定法.和氣典二・重野純・村上郁也(編)感覚・知覚心理学ハンドブック 第三版(第2章)、誠信書房

 

 

 

リスト1 データをまとめるスクリプト

 

import numpy as np

import pandas as pd

 

fin_nm = input('Input Data File (*.csv) = ') 

df_data = pd.read_csv(fin_nm)

print(df_data)

df_data_val = np.array(df_data.values, dtype=int)

"""

      素データをまとめるための辞書型変数の作成

"""

dict_data = {}

for v in df_data_val:

    a = v[1]

    b = v[2]

    if a > b:

        a, b = b, a

    c = v[3]

    d = v[4]

    if c > d:

        c, d = d, c

    X = v[5]

    a = int(a)

    b = int(b)

    c = int(c)

    d = int(d)

    if (a, b, c, d) in dict_data:

        dict_data[(a, b, c, d)]['TN'] += 1

        if X == 1:

            dict_data[(a, b, c, d)]['N'] += 1

    else:

        if X == 1:

            dict_data[(a, b, c, d)] = {'TN':1, 'N':1}

        else:

            dict_data[(a, b, c, d)] = {'TN':1, 'N':0}       

 

print(dict_data)

"""

      (i,j)(s,t)の比較と、(s,t)(i,j)の比較データをまとめる

"""

ck_dict = {}

for k in dict_data.keys():

    ki, kj, ks, kt = k

    N = dict_data[k]['N']

    TN = dict_data[k]['TN']

    if not k in ck_dict.keys():

        if (ks, kt, ki, kj) in ck_dict.keys():

            ck_dict[(ks, kt, ki, kj)]['N'] += TN - N

            ck_dict[(ks, kt, ki, kj)]['TN'] += TN

        else:

            ck_dict[k] = {'N':N, 'TN':TN}

 

dict_data = ck_dict

 

"""

       まとめたデータをCSV形式のファイルとして出力

"""

for k in dict_data.keys():

    print(k, dict_data[k])

n_st = int(input('Number of stimuli = '))

fout_nm = input('Output File Name (*.csv) = ')

with open(fout_nm, 'w') as f:

    f.write(f'{n_st},,,,,\n')

    f.write('i,j,s,t,N,TN\n')

    for k in dict_data.keys():

        i = k[0]

        j = k[1]

        s = k[2]

        t = k[3]

        TN = dict_data[k]['TN']

        N = dict_data[k]['N']

        f.write(f'{i},{j},{s},{t},{N},{TN}\n')

 

 

 

 

リスト2 リスト3Stanスクリプト(diff_scale.stan)を用いてベイズ分析を行うPythonスクリプト(BDS.py

 

from cmdstanpy import CmdStanModel

import numpy as np

import scipy.stats as ss

import arviz as az

import csv

import matplotlib.pyplot as plt

import seaborn as sb

 

fin_nm = input('Data(*.csv) = ')

with open(fin_nm, 'r') as f:

    raw_data = [v for v in csv.reader(f)]

 

N_st = int(raw_data[0][0])       #   刺激値の総数

"""

        データ(i, j, s, t, N, TN)の設定

"""

N_data = 0

ID_i = []

ID_j = []

ID_s = []

ID_t = []

N = []

TN = []

for v in raw_data[2:]:

    N_data += 1

    ID_i.append(int(v[0]))

    ID_j.append(int(v[1]))

    ID_s.append(int(v[2]))

    ID_t.append(int(v[3]))

    N.append(int(v[4]))

    TN.append(int(v[5]))

 

print('N_data = ', N_data)

 

for i in range(N_data):

    print("{0:>3}: {1}, {2}, {3}, {4}/  {5}, {6}".

          format((i + 1), ID_i[i], ID_j[i], ID_s[i], ID_t[i], N[i], TN[i]))

   

Data = {'N_data': N_data, 'N_st': N_st, \

        'ID_i': ID_i, 'ID_j': ID_j, 'ID_s': ID_s, 'ID_t': ID_t, \

        'N': N, 'TN': TN}

 

model = CmdStanModel(stan_file="diff_scale.stan")  #   コンパイル

fit = model.sample(data=Data)                      #   サンプリング

 

print(fit.summary())

 

inf_data = az.from_cmdstanpy(fit)           #   Arviz(az)用のデータに変換

az.plot_trace(inf_data)                     #   トレースプロット

plt.tight_layout()

plt.show()

 

df_smpl = fit.draws_pd()                    #   Pandas DataFrameに変換

 

"""

        ψの事後分布

"""

Psi = [0]*N_st

for i in range(N_st):

    Psi[i] = df_smpl[f'psi[{i+1}]'].values

for i in range(N_st):

    print(Psi[i])

 

plt.plot([0, 0], [0, 10], label = r'$\psi1$')   #    ψ0=0 に固定

for i in range(1, N_st-1):

    sb.kdeplot(Psi[i], label = r'$\psi${}'.format(i+1))

plt.plot([1, 1], [0, 10], label = r'$\psi${}'.format(N_st))      #   ψN_st=1 に固定

plt.legend()

plt.yticks([])

plt.xlabel(r'$\psi$', fontsize = 16)

plt.title('Posterior Distributions', fontsize = 18)

plt.savefig('FigPsi.png')

plt.show()

 

"""

         σの事後分布

"""

Sgm = df_smpl['sgm'] 

sb.kdeplot(Sgm)

plt.xlabel(r'$\sigma$', fontsize = 14)

plt.title(r'Posterir distribution of $\sigma$', fontsize = 18)

plt.yticks([])

plt.savefig('FigSgm.png')

plt.show()

 

Sgm_med = np.percentile(Sgm, 50)

print('Sgm_med =', Sgm_med)

 

Psi_med = []

Psi_025 = []

Psi_25 = []

Psi_75 = []

Psi_975 = []

 

for i in range(N_st):

    q025, q25, med, q75, q975 = np.percentile(Psi[i], [2.5, 25, 50, 75, 97.5])

    Psi_med.append(med)

    Psi_025.append(q025)

    Psi_25.append(q25)

    Psi_75.append(q75)

    Psi_975.append(q975)

   

stimuli = []

for i in range(N_st):

    stimuli.append(i + 1)

"""

      刺激値と感覚量の関係

"""

plt.plot(stimuli, Psi_med, c='b', marker='o', lw=0, label = 'Median')

plt.plot(stimuli, Psi_25, c='g', marker='o', fillstyle='none', lw=0, label = '50% CI')

plt.plot(stimuli, Psi_75, c='g', marker='o', fillstyle='none', lw=0)

plt.plot(stimuli, Psi_025, c='y', marker='o', fillstyle='none', lw=0, label = '95% CI')

plt.plot(stimuli, Psi_975, c='y', marker='o', fillstyle='none', lw=0)

plt.plot([stimuli[0], stimuli[N_st-1]], [0, 1], c='r')

plt.title('Sensation and Stimulus', fontsize = 18)

plt.xticks(np.arange(N_st)+1, fontsize=12)

plt.xlabel('Stimulus ID', fontsize = 14)

plt.ylabel(r'Sensation $\psi$', fontsize = 14)

plt.legend() 

plt.tight_layout()

plt.savefig('FigSummary.png')

plt.show()

 

Prop_data = np.empty(N_data)

Prop_model = np.empty(N_data)

for i in range(N_data):

    Prop_data[i] = N[i] / TN[i]

    Prop_model[i] = ss.norm(0, 1).cdf((abs(Psi_med[ID_i[i]-1] - Psi_med[ID_j[i]-1]) -

                                   abs(Psi_med[ID_s[i]-1] - Psi_med[ID_t[i]-1])) / Sgm_med)

"""

       データ値とモデル値の関係

"""

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

plt.scatter(Prop_model, Prop_data)

plt.plot([0,1], [0,1])

plt.title('Scattergram of (Prob_Model, Prop_Data)', fontsize = 14)

plt.ylabel('N/TN', fontsize = 14)

plt.xlabel('Prob.', fontsize = 16)

plt.tight_layout()

plt.savefig('FigScatter.png')

plt.show()

 

 

 

 

リスト3 MCMCサンプリングのためのStanスクリプト(diff_scale.stan

 

data {

    int N_data;

    int N_st;

    /*

          刺激値 i, j, s, t の値

    */

    array[N_data] int<lower = 1, upper = N_st> ID_i;

    array[N_data] int<lower = 1, upper = N_st> ID_j;

    array[N_data] int<lower = 1, upper = N_st> ID_s;

    array[N_data] int<lower = 1, upper = N_st> ID_t;

    array[N_data] int<lower = 0> N;

    array[N_data] int<lower = 1> TN;

}

transformed data {

    vector[N_st-1] a;

    for (i in 1:N_st-1)

        a[i] = 1/(N_st-1.0);

}

parameters {

    simplex[N_st - 1] theta;

    real<lower = 0.0, upper = 1.0> vsgm;

}

transformed parameters {

    real sgm;

    /*

           感覚量ψ

    */

    array[N_st] real psi;

    sgm = 0.00001 + vsgm*10.0;

    psi[1] = 0.0;

    for (i in 2:N_st)

        psi[i] = psi[i - 1] + theta[i - 1];

}

model {

    theta ~ dirichlet(a);

    vsgm ~ beta(1.0, 1.0);

    /*

          総試行数TNにおいて(i,j)がN回選ばれたとする

    */

    for (i in 1:N_data)

        N[i] ~ binomial(TN[i],

                        Phi((abs(psi[ID_i[i]] - psi[ID_j[i]])

                         - abs(psi[ID_s[i]] - psi[ID_t[i]])) / sgm));

}

 

 

 

 

Home