Up

Triangular Test

A logistic regression model

Yasuharu Okamoto, 2021.12

 

In triangular test, three stimuli are presented, where two of them are the same. The same two stimuli are denoted by A and A, and the other by B. The observer is required to choose the one, which the observer judged as the B. This is the standard method of the triangular test, and discussed in the first half of the website.

We also consider the following situation:

For the same two stimuli, A and A, the third stimuli to be discriminated/detected, three stimuli, B, C, and BC, are prepared. The stimulus BC has the properties of B and C. The triangular tests consist of the three sets of stimuli, (A, A, B), (A, A, C), and (A, A, BC). We analyze the interaction of B and C in BC by a logistic regression model, which is presented in the second half of the website.

The programs in the website are archived in the file TriTestLogisticFiles.zip, which can be downloaded and used freely on the users responsibility. All rights are reserved.

 

First, consider the standard case of the triangular test.

Denote the probability of detection by . If the observer detects the stimulus B with probability Pd, the observer responds correctly.

If the observer cannot detect the stimulus B, he/she chooses randomly among the three. In this case, the probability of correct response is 1/3.

Hence, the probability of correct response Pc is given as follows:

When the total number of trials of the triangular test is N, using the binomial distribution, we have the probability  that the number of correct responses is k as follows:

 

Stan script based on the equation (2) is shown in Listing 1.

Python script, which uses the Stan script in Listing 1, is shown in Listing 2.

Run the script in Listing 2, values of k and N are required to be set as follows:

 

(py39) PS D:\xxxxx\TriTestLogisticFiles> python .\TriTest.py

INFO:numexpr.utils:NumExpr defaulting to 8 threads.

k = 50

N = 100

 

In the above example, 50 and 100 are set for k and N.

After the value of N is set, calculation starts.

After the calculation ends, the posterior distribution of Pd is shown as in Figure 1.

Figure 1

 

In Figure 1, it is shown that the mode is 0.24, and the 95% HDI is [0.11, 0.40].

The graph is saved automatically with the name FigPd.png.

 

 

Logistic Regression Model

 

The logistic transformation of probability p is given by

Hence, we have

Logistic regression model is constructed by this transformation (Gelman et al., 2014, sec. 3.7).

Let the detection/discrimination probabilities PdB, PdC, and PdBC for stimuli B, C, and BC, respectively, be given as follows

fB, fC, and fBC are factors, which represent the main effects of stimuli B and C, and the interaction by B and C.

Stan script based on the above model is shown in Listing 3.

The Python script in Listing 4 uses the Stan script in Listing 3.

Run the script in Linsting 4, values for k and N for the three sets of triangular tests, (A, A, B), (A, A, C), and (A, A, BC), are required to be set as follows.

 

(py39) PS D:\xxxxx\TriTestLogisticFiles> python .\TriTestLogistic.py

INFO:numexpr.utils:NumExpr defaulting to 8 threads.

kB = 50

NB = 100

kC = 34

NC = 100

kBC = 80

NBC = 100

 

In the above example, ks and Ns are set to be 50 and 100 for (A, A, B), 34 and 100 for (A, A, C), and 80 and 100 for (A, A, BC).

After the input of NBC = 100, calculation starts, and the posterior distributions of factors fB, fC, and fBC are drawn as in Figure 2.

Figure 2

 

In the case where values for effects fB or fC be negative with large absolute values, probabilities of PdB or PdC be small ones (eqs.(3) and (4)). The interaction fBC is 0 if the model (5) is additive one with no interaction between B and C.

In Figure 2, the interaction fBC is clearly positive, which means that the effect of combination of B and C is strong.

When the form in Figure 2 is closed, calculation for the posterior distributions of PdB, PdC, and PdBC starts.

The posterior distributions of PdB, PdC, and PdBC are displayed as in Figure 3.

Figure 3

 

Detection probability PdC is negligible, but when C is combined with B, C boosts the detection probability PdBC far above that of PdB.

Graphs in Figures 2 and 3 are automatically saved in files FigFctrs.png and FigPd.png.

 

 

As another example, the following values are set to ks and Ns.

 

(py39) PS D:\xxxxx\TriTestLogisticFiles> python .\TriTestLogistic.py

INFO:numexpr.utils:NumExpr defaulting to 8 threads.

kB = 60

NB = 100

kC = 60

NC = 100

kBC = 40

NBC = 100

 

In this case, the results are as follows.

Figure 4 shows the posterior distributions of fB, fC, and fBC.

Figure 4

 

The interaction fBC is distributed in the negative region.

The posterior distributions of PdB, PdC, and PdBC are shown in Figure 5.

Figure 5

 

Detection probabilities PdB and PdC are about 0.4, but that of PdBC is negligible, which goes along with the negative value of fBC.

 

 

Reference

Gelman, A., Carlin, J. B., Stern, H. S., Dunson, D. B., Vehtari, A., & Rubin, D. B. (2014). Bayesian data analysis, third edition. CRC Press.

 

 

Listing 1. Stan script for the model (2). (file name: tri_test.stan)

data {

    int N;

    int k;

}

parameters {

    real<lower = 0.0, upper = 1.0> Pd;  //  Discrimination Probability

}

transformed parameters {

    real Pc;                            //  Correct Response Probability

     Pc = Pd + (1.0 - Pd) * (1/3.0);

}

model {

    Pd ~ uniform(0.0, 1.0);

    k ~ binomial(N, Pc);

}

 

 

Listing 2. Python script, which uses the Stan script in listig 1. (file name: TriTest.py)

import numpy as np

import scipy.stats as ss

import matplotlib.pyplot as plt

import pystan

import seaborn as sb

import pickle

 

 

def CalcMAPEst(samples, a = 0.05, n_points = 10000):

    """

        Calculation of the MAP estimate from a KDE graph on [Lp, Up]

        Lp and Up are 100*a/2 and 100(1-a/2) percentile points of samples

    """

    print('Calculating the mode.')

    Lp, Up = np.percentile(samples, [100 * a/2, 100 * (1 - a/2)])  # import numpy as np

    coord = np.linspace(Lp, Up, n_points)

    est_pdf = ss.gaussian_kde(samples).pdf(coord)  # import scipy.stats as ss

    map_idx = np.argmax(est_pdf)

    MAP_Est = coord[map_idx]      

    return MAP_Est, est_pdf[map_idx]

 

 

def calcHDI(sample, a = 0.05):

    """

            Calculation of 100(1-a)% HDI for sample

            Chen-Shao HPD Estimation algorithm

    """

    print('Calculating HDI.')

    smpl = np.sort(sample)

    n = len(smpl)

    ckW = max(smpl) - min(smpl)

    cki = 0

    RB = int(n * (1-a))

    L0 = 0

    U0 = n-1

    while (RB + cki < n):

        if smpl[RB + cki] - smpl[cki] < ckW:

            ckW = smpl[RB + cki] - smpl[cki]

            L0 = cki

            U0 = RB + cki

        cki += 1

    ckW = max(smpl) - min(smpl)

    cki = 1

    LB = n - int(n * (1-a))

    L1 = 0

    U1 = n-1

    while (LB - cki >= 0):

        if smpl[n - cki] - smpl[LB - cki] < ckW:

            ckW = smpl[n - cki] - smpl[LB - cki]

            U1 = n - cki

            L1 = LB - cki

        cki += 1

    return smpl[int((L1+L0)/2)], smpl[int((U1+U0)/2)]

 

 

 

k = int(input('k = '))

N = int(input('N = '))

 

Data = {'N': N, 'k': k}

 

sm = pystan.StanModel(file = 'tri_test.stan')

   

fit = sm.sampling(data = Data, iter = 10_000, n_jobs = 1)

 

print(fit)

 

sb.kdeplot(fit['Pd'])

plt.hist(fit['Pd'], density = True, color = 'g', alpha = 0.5)

 

MAPEst = CalcMAPEst(fit['Pd'])[0]

plt.scatter([MAPEst], [0], marker = 'o', s = 200,

            c = 'magenta', label = f'mode = {MAPEst:.2f}')

L_bndry, R_bndry = calcHDI(fit['Pd'], 0.05)

plt.plot([L_bndry, R_bndry], [0, 0], c = 'r', lw = 5,

         label = '95% HDI [{0:.2f}, {1:.2f}]'.format(L_bndry, R_bndry))

plt.yticks([])

plt.xlabel('Pd', fontsize = 12)

plt.title(f'Posterior distribution\nk = {k}, N = {N}', fontsize = 16)

plt.legend()

plt.savefig('FigPd.png')

plt.show()

 

 

Listing 3. Stan script of the logistic regression model (file name: tri_test_logistic.stan)

//

//      Yasuharu Okamoto,  2021.12

//

data {

    int NB;

    int kB;

    int NC;

    int kC;

    int NBC;

    int kBC;   

}

parameters {

    real fB;     //   factor for B

    real fC;     //   factor for C

    real fBC;    //   interaction factor of B and C

}

transformed parameters {

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

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

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

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

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

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

    //

    //      Logistic regression model

    //

    PdB = 1.0 / (1.0 + exp(-fB));

    PdC = 1.0 / (1.0 + exp(-fC));

    PdBC = 1.0 / (1.0 + exp(-(fB + fC + fBC)));

   

    PcB = PdB + (1.0 - PdB) * (1.0 / 3.0);

    PcC = PdC + (1.0 - PdC) * (1.0 / 3.0);

    PcBC = PdBC + (1.0 - PdBC) * (1.0 / 3.0);

}

model {

    //

    //    Weakly informative priors

    //

    fB ~ normal(0.0, 5.0);

    fC ~ normal(0.0, 5.0);

    fBC ~ normal(0.0, 5.0);

    //

    //    Distributions of the correct responses

    //

    kB ~ binomial(NB, PcB);

    kC ~ binomial(NC, PcC);

    kBC ~ binomial(NBC, PcBC);

}

 

 

Listing 4. Python script, which uses the Stan script in Listing 3 (file name: TriTestLogistic.py)

import numpy as np

import scipy.stats as ss

import pystan

import matplotlib.pyplot as plt

import seaborn as sb

import pickle

 

 

def CalcMAPEst(samples, a = 0.05, n_points = 10000):

    """

        Calculation of the MAP estimate from a KDE graph on [Lp, Up]

        Lp and Up are 100*a/2 and 100(1-a/2) percentile points of samples

    """

    print('Calculating the mode.')

    Lp, Up = np.percentile(samples, [100 * a/2, 100 * (1 - a/2)])  # import numpy as np

    coord = np.linspace(Lp, Up, n_points)

    est_pdf = ss.gaussian_kde(samples).pdf(coord)  # import scipy.stats as ss

    map_idx = np.argmax(est_pdf)

    MAP_Est = coord[map_idx]      

    return MAP_Est, est_pdf[map_idx]

 

 

def calcHDI(sample, a = 0.05):

    """

            Calculation of 100(1-a)% HDI for sample

            Chen-Shao HPD Estimation algorithm

    """

    print('Calculating the HDI.')

    smpl = np.sort(sample)

    n = len(smpl)

    ckW = max(smpl) - min(smpl)

    cki = 0

    RB = int(n * (1-a))

    L0 = 0

    U0 = n-1

    while (RB + cki < n):

        if smpl[RB + cki] - smpl[cki] < ckW:

            ckW = smpl[RB + cki] - smpl[cki]

            L0 = cki

            U0 = RB + cki

        cki += 1

    ckW = max(smpl) - min(smpl)

    cki = 1

    LB = n - int(n * (1-a))

    L1 = 0

    U1 = n-1

    while (LB - cki >= 0):

        if smpl[n - cki] - smpl[LB - cki] < ckW:

            ckW = smpl[n - cki] - smpl[LB - cki]

            U1 = n - cki

            L1 = LB - cki

        cki += 1

    return smpl[int((L1+L0)/2)], smpl[int((U1+U0)/2)]

 

 

kB = int(input('kB = '))

NB = int(input('NB = '))

kC = int(input('kC = '))

NC = int(input('NC = '))

kBC = int(input('kBC = '))

NBC = int(input('NBC = '))

 

sm = pystan.StanModel(file = 'tri_test_logistic.stan')

 

Data = {'kB': kB, 'NB': NB, 'kC': kC, 'NC': NC, 'kBC': kBC, 'NBC': NBC}

fit = sm.sampling(data = Data, iter = 10_000, n_jobs = 1)

print(fit)

#

#       Posterior distributions of factors fB, fC, and fBC

#

MAPfB = CalcMAPEst(fit['fB'])[0]

MAPfC = CalcMAPEst(fit['fC'])[0]

MAPfBC = CalcMAPEst(fit['fBC'])[0]

L_HDIfB, R_HDIfB = calcHDI(fit['fB'])

L_HDIfC, R_HDIfC = calcHDI(fit['fC'])

L_HDIfBC, R_HDIfBC = calcHDI(fit['fBC'])

sb.kdeplot(fit['fB'], label = 'fB, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPfB, L_HDIfB, R_HDIfB))

sb.kdeplot(fit['fC'], label = 'fC, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPfC, L_HDIfC, R_HDIfC))

sb.kdeplot(fit['fBC'], label = 'fBC, mode = {0:.2f}, \n     95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPfBC, L_HDIfBC, R_HDIfBC))

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

plt.yticks([])

plt.title('Posterior distribtion\n' + \

          ('kB = {0:}, NB = {1:}, kC = {2:}, NC = {3:}\n'.format(kB, NB, kC, NC)) +\

          ('kBC = {0:}, NBC = {1:}'.format(kBC, NBC)), fontsize = 16)

plt.legend()

plt.tight_layout()

plt.savefig('FigFctrs.png')

plt.show()

#

#        Posterior distributions of PdB, PdC, and PdBC

#

MAPPB = CalcMAPEst(fit['PdB'])[0]

MAPPC = CalcMAPEst(fit['PdC'])[0]

MAPPBC = CalcMAPEst(fit['PdBC'])[0]

L_HDIPB, R_HDIPB = calcHDI(fit['PdB'])

L_HDIPC, R_HDIPC = calcHDI(fit['PdC'])

L_HDIPBC, R_HDIPBC = calcHDI(fit['PdBC'])

sb.kdeplot(fit['PdB'], label = 'PdB, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPPB, L_HDIPB, R_HDIPB))

sb.kdeplot(fit['PdC'], label = 'PdC, mode = {0:.2f}, \n    95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPPC, L_HDIPC, R_HDIPC))

sb.kdeplot(fit['PdBC'], label = 'PdBC, mode = {0:.2f}, \n     95% HDI = [{1:.2f}, {2:.2f}]'.

           format(MAPPBC, L_HDIPBC, R_HDIPBC))

plt.xlabel('Prob. of detection', fontsize = 14)

plt.yticks([])

plt.title('Posterior distribtion\n' + \

          ('kB = {0:}, NB = {1:}, kC = {2:}, NC = {3:}\n'.format(kB, NB, kC, NC)) +\

          ('kBC = {0:}, NBC = {1:}'.format(kBC, NBC)), fontsize = 16)

plt.legend()

plt.tight_layout()

plt.savefig('FigPd.png')

plt.show()

 

 

Up