Yasuharu Okamoto, 2017/07
Stan/Python Scripts for Analysis in Signal Detection
Theory (SDT)
Two programs were developed, one is for a Yes/No type with equal variance assumption and the other is for a Yes/No method using a rating method without equal variance assumption.
Equal Variance Model
Under the condition of equal variances for noise and signal stimuli, sensation by the noise is assumed to have mean 0 and variance 1, and sensation by the signal is assumed to have mean and variance 1. With a criterion of judgment, we have the following equations.
The difference of means for signal and noise is designated by , so we have
Suppose that we have data shown in Table 1, where N(CR) denotes the number of responses “No” to “Noise” stimulus (Correct Rejection), and so on.
Table 1 |
|
|
|
No (No “Signal”) |
Yes (Yes, “Signal” displayed) |
Noise |
N(CR) |
N(FA) |
Signal |
N(Miss) |
N(Hit) |
We have the following likelihood function.
Stan script for the above model is given as follows
================== YN_EqSgm.stan ==========================
data {
int<lower = 0>
Freq_Noise[2]; // [Correct Rejection, False Alarm]
int<lower = 0>
Freq_Signal[2];
// [Miss, Hit]
}
parameters {
real mu;
real<lower = 0> c;
}
transformed parameters {
real pyes_n;
real pyes_s;
pyes_n = 1.0 - Phi(c);
//
Probability of FA
pyes_s = 1.0 - Phi(c -
mu); // Probability of
Hit
}
model {
Freq_Noise[2] ~
binomial(Freq_Noise[1] + Freq_Noise[2], pyes_n);
Freq_Signal[2] ~
binomial(Freq_Signal[1] + Freq_Signal[2], pyes_s);
}
====================================================
An example data of Table 1 is shown in Table 2.
Table 2 |
|
|
|
No |
Yes |
Noise |
69 |
31 |
Signal |
31 |
69 |
For the data of Table 2, the following Python script which uses the above Stan script was written. Numbers of responses to Noise are set in Data_Noise list, and those to Signal in Data_Signal list.
======================================================
import pystan
import matplotlib.pyplot as plt
import numpy as np
Data_Noise = [69, 31] # [Correct Rejection, False Alarm]
Data_Signal = [31, 69] # [Miss, Hit]
Data = {'Freq_Noise': Data_Noise,
'Freq_Signal': Data_Signal}
fit = pystan.stan(file =
'YN_EqSgm.stan', data = Data, iter = 11000, warmup = 1000, seed = 999, n_jobs =
1)
print(fit)
mu = fit['mu']
c = fit['c']
mu_s = np.sort(mu)
c_s = np.sort(c)
mu_med = mu_s[int(len(mu_s)/2)]
c_med = c_s[int(len(c_s)/2)]
plt.hist(mu)
plt.title("mu (d') = {0:<.3f}
(med)".format(mu_med))
plt.show()
plt.hist(c)
plt.title("Criterion =
{0:<.3f} (med)".format(c_med))
plt.show()
=======================================================
Run this script, a histogram of samples from the posterior distribution of (Figure 1) is shown after MCMC sampling by Stan.
Figure 1
Above the histogram, the median is shown.
When the form is closed by clicking on the icon X at the upper right corner, the next histogram is shown (Figure 2).
Figure 2
On the form of Figure 2, a histogram of samples from the posterior distribution of criterion and the median are shown.
When the form of Figure 2 is closed, the program ends.
The program files are archived in SDT_EqSgm.zip . Click on the file name SDT_EqSgm.zip to down load.
Model, which allows unequal variances
To set an origin and a unit, mean and variance for noise are assumed to be 0 and 1, respectively. Mean and variance for signal are denoted by and , respectively. This type of model with variances, which may be unequal each other, requires data which gives more than one points with respect to ROC curve. We can get more than one points by employing a rating method.
Suppose that an observer responds to the stimulus choosing one from K rating categories, Category-1 (the strongest confidence in judgment of “no signal”) to Category-K (the strongest confidence in judgment of “signal”). Denote the criterion between Category-k and Category-(k+1) by . Then, we have the following model.
The difference of means for noise and signal is denoted by , i.e.,
When we denote the number of responses of Category-k to noise by N(nk) and to signal by N(sk), we have Table 3.
Table 3 |
|
|
|
|
Category-1 |
・・・ |
Category-K |
Noise |
N(n1) |
・・・ |
N(nK) |
Signal |
N(s1) |
・・・ |
N(sK) |
A likelihood function for Table 3 can be set as follows.
A Stan script for the above model is given as follows.
================== YN_rating.stan =======================
data {
int<lower = 3> K;
int<lower = 0>
Freq_Noise[K];
int<lower = 0>
Freq_Signal[K];
}
parameters {
real mu;
real<lower = 0.0>
sigma;
ordered[K-1] c;
}
transformed parameters {
vector[K] theta_n;
vector[K] theta_s;
theta_n[1] = Phi(c[1]);
theta_n[K] = 1.0 -
Phi(c[K-1]);
for (k in 2:(K-1))
theta_n[k] = Phi(c[k]) - Phi(c[k-1]);
theta_s[1] = Phi((c[1] - mu)
/ sigma);
theta_s[K] = 1.0 -
Phi((c[K-1] - mu) / sigma);
for (k in 2:(K-1))
theta_s[k] = Phi((c[k] - mu) / sigma) - Phi((c[k-1] - mu) / sigma);
}
model {
Freq_Noise ~
multinomial(theta_n);
Freq_Signal ~
multinomial(theta_s);
}
=======================================
An example data in format of Table 3 is shown in Table 4, where “Category-1” to “Category-4” represent judgment of “Noise only”, “Probably Noise only”, “Probably Signal present” to “Signal present”, respectively.
Table 4 |
|
|
|
|
|
Category-1 |
Category-2 |
Category-3 |
Category-4 |
Noise |
10 |
9 |
7 |
1 |
Signal |
2 |
1 |
19 |
5 |
To analyze the data of Table 4, the following Python script using the above Stan script is given as follows. The frequencies of responses of each category to “Noise” and “Signal” are set in lists Data_Noise and Data_Signal, respectively.
==========================================
import
pystan
import
matplotlib.pyplot as plt
import numpy
as np
import
pickle
from pystan
import StanModel
from
CumNormalDistri import *
K = 4
Data_Noise =
[10, 9, 7, 1]
Data_Signal
= [2, 1, 19, 5]
Data = {'K':
K, 'Freq_Noise': Data_Noise, 'Freq_Signal': Data_Signal}
fit =
pystan.stan(file = 'YN_rating.stan', data = Data, iter = 10000, warmup = 1000,
seed = 12345, n_jobs = 1)
print(fit)
mu =
fit['mu']
# print('mu
= \n', mu)
sigma =
fit['sigma']
# print('sigma
= \n', sigma)
mu_s =
np.sort(mu)
sgm_s =
np.sort(sigma)
mu_med =
mu_s[int(len(mu_s) / 2)]
sgm_med =
sgm_s[int(len(sgm_s) / 2)]
print('\nmu_med
= ', mu_med)
print('\nsgm_sgm
= ', sgm_med)
plt.hist(mu)
plt.title("mu
(Delta-m) = {0:<.3f} (med)".format(mu_med))
plt.show()
plt.hist(sigma)
plt.title("Sigma
= {0:<.3f} (med)".format(sgm_med))
plt.show()
c = fit['c']
c_comp = []
for i in
range(3):
c_comp.append([])
for v in c:
for j in range(3):
c_comp[j].append(v[j])
z_values =
np.arange(-4.0, 4.001, 0.1)
x_values =
[]
y_values =
[]
for z in
z_values:
x = 1.0 - cum_normal(z)
x_values.append(x)
y = 1.0 - cum_normal((z -
mu_med) / sgm_med)
y_values.append(y)
plt.figure(figsize
= (5,5))
plt.plot(x_values,
y_values, 'b-')
plt.title("ROC-curve")
plt.xlabel("P(False
Alarm)")
plt.ylabel("P(Hit)")
cum_freq_n =
[]
cum_freq_s =
[]
for k in
range(K):
cum_freq_n.append(Data_Noise[K - 1 - k])
cum_freq_s.append(Data_Signal[K - 1 - k])
for k in
range(1, K):
cum_freq_n[k] +=
cum_freq_n[k-1]
cum_freq_s[k] +=
cum_freq_s[k-1]
x_fa = []
y_ht = []
for k in
range(K-1):
x_fa.append(cum_freq_n[k] /
cum_freq_n[K-1])
y_ht.append(cum_freq_s[k] /
cum_freq_s[K-1])
plt.plot(x_fa,
y_ht, 'ro')
c_s = []
c_med = []
for k in
range(K-1):
c_s.append([])
c_s[k] = np.sort(c_comp[k])
c_med.append(c_s[k][int(len(c_s[k]) / 2)])
print('\nCriterions
= ')
for k in
range(K-1):
print('c-{0} = {1:<.3f}
(med)'.format(k + 1, c_med[k]))
x_fa = []
y_ht = []
for k in
range(K-1):
x_fa.append(1.0 -
cum_normal(c_med[k]))
y_ht.append(1.0 -
cum_normal((c_med[k] - mu_med) / sgm_med))
plt.plot(x_fa,
y_ht, 'gs')
plt.show()
n_smpl =
len(c_s[0])
min_c =
c_s[0][0]
max_c =
c_s[0][0]
for k in
range(K-1):
for i in range(n_smpl):
if
min_c > c_s[k][i]:
min_c = c_s[k][i]
if
max_c < c_s[k][i]:
max_c = c_s[k][i]
c_cnt = []
for k in
range(K-1):
c_cnt.append([])
for i in range(20):
c_cnt[k].append(0)
for i in range(n_smpl):
pos
= 20.0 * (c_s[k][i] - min_c) / (max_c - min_c)
pos
= int(pos) if pos < 20.0 else 19
c_cnt[k][pos] += 1
x_v = [0]*20
for i in
range(20):
x_v[i] = min_c + (max_c -
min_c) * (i + 0.5) / 20.0
for k in
range(K-1):
plt.plot(x_v, c_cnt[k],
'b-')
title_str =
"Criterions (medians)\nc{0}({1:<.2f})".format(1, c_med[0])
for k in
range(1, K-1):
title_str += " c{0}({1:<.2f})".format(k+1,
c_med[k])
plt.title(title_str)
plt.show()
===========================================================
Run this program, then a form of Figure 3 is shown after MCMC sampling by Stan.
Figure 3
In Figure 3, a histogram and the median of the samples from the posterior distribution of are shown. “Delta-m” denotes .
Click the icon X at the upper right corner of the form, then the form is closed and the next form is presented (Figure 4).
Figure 4
Figure 4 shows a histogram and the median of the samples from the posterior distribution of .
Close the form of Figure 4, then the next form is presented (Figure 5).
Figure 5
Figure 5 shows the ROC curve. Small red circles represent data points, and small green squares represent the corresponding points estimated by the model using the median values of the samples.
Close the form of Figure 5, then the next form is shown (Figure 6).
Figure 6
In Figure 6, histograms drawn by lines and medians of samples from the posterior distributions of criterions s are shown.
When the form is closed, the program ends.
The program files are archived into file SDT_NESgm.zip ,which can be down loaded by clicking the file name SDT_NESgm.zip.