Up

Ordered Categorical Rating Scale

Psychophysical Point of View

Yasuharu Okamoto, 2022.01, 2026.01

 

 

The scripts have been revised for CmdStanPy. A simple installation of CmdStanPy is shown at this website.

 

In the method of ordered categorical scaling, an observer chooses the appropriate category from the ordered categories corresponding to the strength of the sensation caused by the presented stimulus. The method has been discussed by many researchers (e.g., Gescheider, 1997; Torgerson, 1958; Thurstone, 1927).

 

Suppose the stimulus  invokes sensation , which has a normal distribution of a mean  and a variance . That is,

 

 

Suppose that there are  boundaries s of categories, and observers rating  on the stimulus  is category  when the following condition holds:

 

 when

 

where

 

 

Then we have

 

 

where  is the cumulative distribution function of the standard normal distribution.

Set an origin and a unit by the following constraint:

 

, 

 

This condition corresponds to the dynamic range, which is supposed to be approximately the same for all modalities (Teghtsoonian, 1971).

As to standard deviation  of sensation , the following three cases would be considered.

 

Condition F:

No constraints on , that is, free condition.

 

Condition L:

Linear relation between the standard deviation  and the mean  of sensation  of stimulus  is assumed. That is, the following relation

 

 

is set.

This is a generalization of what Stevens (1975, p. 235) calls Ekmans law, from which Stevens power law can be derived.

 

Condition C:

All s are the same, that is constant,

 

 

This condition corresponds to Fechners thinking, and leads to Fechners logarithmic law.

 

In the following, only the condition C is considered. The script files are archived in the file SigmaConst.zip. The archived file can be downloaded and used freely.

 

The script accepts an input data file of the format shown in Figure 1.

 

Figure 1. File name DataC.csv

 

In the first row, variable names are set. The values of the variables are set in rows under the first row.

The first column contains values of the stimuli. Values of the weakest to the strongest stimuli are set from the second to the last (bottom) row.

Columns from the second to the last (K+1st) ones correspond to categories from the first to the Kth ones. The number of rating responses for combinations of stimuli and rating categories are set in the corresponding cells.

The data file should be saved in CSV file format. That is, the file should be saved with the file name, which has the file extension .csv.

 

Listing 1 shows a Stan script (RatingFechner.stan) of Bayesian analysis for the Condition C, Listing 2 shows a Python script (CategoryScaleC.py), which uses the script in Listing 1. An input data file (e.g., DataC.csv) is put in the folder, in which the scripts in Listings 1 and 2 are put.

Execute the script file CategoryScaleC.py in an environment with cmdstanpy installed as shown bellow.

 

(stan) ****/SigmaConst$ ls

CategoryScaleC.py  DataC.csv  RatingFechner.stan

(stan) ****/SigmaConst$ python CategoryScaleC.py

Input file (*.csv) = DataC.csv

 

After the input data file name is set, the Stan script is compiled and MCMC sampling is executed.

A trace plot is shown like Figure 2.

 

Figure 2

 

After MCMC sampling, graphs of posterior distributions of parameters are displayed (Figure 3).

 

Figure 3

 

Close the window of Figure 3, the next graphs (Figure 4) appears.

 

Figure 4

 

Close the window of Figure 4, the script ends.

 

Content of the output text file Results.txt is as follows:

 

 

Input Data File = DataC.csv

 

Data:

         1   51   36   13    0    0    0    0

     1.668   43   46   10    1    0    0    0

     2.783   23   48   27    2    0    0    0

     4.642   10   44   36   10    0    0    0

     7.743    1   21   54   22    2    0    0

    12.915    0   11   37   42    9    1    0

    21.544    0    0   23   49   25    3    0

    35.938    0    0    0   19   49   28    4

    59.948    0    0    0    1   32   37   30

       100    0    0    0    0    3   16   81

 

       Stimulus       Med. Psi

           1.00           0.00

           1.67           0.02

           2.78           0.11

           4.64           0.19

           7.74           0.31

          12.91           0.42

          21.54           0.53

          35.94           0.76

          59.95           0.92

         100.00           1.13

 

Sigma: med =            0.15

 

 

 

 

References

Gescheider, G. A. (1997). Psychophysics: The fundamentals, Third edition. Mahwah: Lawrence Erlbaum Associations, Publishers.

Stevens, S. S. (1975). Psychophysics: Introduction to its perceptual, neural, and social prospects. New York: Wiley.

Tesghtsoonian, R. (1971). On the exponents in Stevens law and the constant in Ekmans law. Psychological Review, 78, 71-80.

Thurstone, L. L. (1927). A law of comparative judgment. Psychological Review, 34, 273-286.

Torgerson, W. S. (1958). Theory and methods of scaling. New York: John Wiley & Sons, Inc.

 

 

 

Listing 1 Stan script of Bayesian analysis for the Condition C (RatingFechner.stan)

 

data {

    int NSt;

    int K;

    array[NSt] real Sts;

    array[NSt, K] int Rs;

}

transformed data {

    vector[K-2] a_C;

    for (i in 1:(K-2)) {

        a_C[i] = 1.0;

    }

}

parameters {

    //simplex[NSt+1] prePsis;

    array[NSt] real Psis;

    simplex[K-2] preC;

    real<lower = 0.0001> sgm;

}

transformed parameters {

    array[K-1] real C;

    array[NSt] simplex[K] theta;

   

    C[1] = 0.0;

    C[K-1] = 1.0;

    for (k in 2:(K-2)) {

        C[k] = C[k-1] + preC[k-1];

    }

    for (s in 1:NSt) {

        theta[s][1] = normal_cdf(C[1] | Psis[s], sgm);

        theta[s][K] = 1.0 - normal_cdf(C[K-1] | Psis[s], sgm);

        for (k in 2:(K-1)) {

            theta[s][k] = normal_cdf(C[k] | Psis[s], sgm) -

                            normal_cdf(C[k-1] | Psis[s], sgm);

        }

    }

}

model {

    preC ~ dirichlet(a_C);

    sgm ~ uniform(0.0001, 1000); 

    for (s in 1:NSt) {

        Psis[s] ~ normal(0.0, 10.0);

        Rs[s] ~ multinomial(theta[s]);

    }

}

 

 

 

Listing 2  Python script, which uses the script in Listing1 (CategoryScaleC.py).

 

import csv

import numpy as np

from cmdstanpy import CmdStanModel

import matplotlib.pyplot as plt

import seaborn as sb

import scipy.stats as ss

import arviz as az

 

fout = open('Results.txt', 'w')

 

fin_nm = input('Input file (*.csv) = ')

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

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

 

fout.write('Input Data File = {}\n'.format(fin_nm))

 

NSt = len(Data_in) - 1

K = len(Data_in[0]) - 1

Sts = np.empty(NSt)

Rs = np.empty((NSt, K), dtype = 'int')

for j in range(NSt):

    Sts[j] = float(Data_in[j+1][0])

    for k in range(K):

        Rs[j][k] = int(Data_in[j+1][k+1])

 

fout.write('\nData:\n')

for j in range(NSt):

    print(f'{Sts[j]:>10g}', end = '')

    fout.write(f'{Sts[j]:>10g}')

    for k in range(K):

        print(f'{Rs[j][k]:>5d}', end = '')

        fout.write(f'{Rs[j][k]:>5d}')

    print()

    fout.write('\n')

 

Data = {'NSt':NSt, 'K':K, 'Sts':Sts, 'Rs':Rs}

 

model = CmdStanModel(stan_file='RatingFechner.stan')

fit = model.sample(data=Data) 

 

print(fit.diagnose())

print(fit.summary())

 

infdata = az.from_cmdstanpy(fit)  # Transforming to Arviz InfereceData

az.plot_trace(infdata, var_names=['Psis', 'C']) 

plt.tight_layout()

plt.savefig('Fig_trace.png')

plt.show()

 

fit = fit.draws_pd()              #  Transforming to Pandas DataFrame

print(fit.keys())

 

Psis = []

for s in range(NSt):

    Psis.append(fit[f'Psis[{s+1}]'])

Psis = np.array(Psis).T

 

sgm = fit['sgm']

 

Cs = []

for k in range(K-1):

    Cs.append(fit[f'C[{k+1}]'])

Cs = np.array(Cs).T

 

plt.figure(figsize=(14,4))

plt.subplot(131)

 

for i in range(NSt):

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

plt.title(r'Posterior Ditributions of $\psi_s$', fontsize = 20)

plt.yticks([])

plt.legend()

plt.tight_layout()

 

plt.subplot(132)

 

sb.kdeplot(sgm)   

plt.title(r'Posterior Distribution of $\sigma$', fontsize = 20)

plt.legend()

 

plt.subplot(133)

 

plt.plot([0, 0], [0, 5], label = r'$C_0$')

for i in range(1, K-2):

    sb.kdeplot(Cs.T[i], label = r'$C_{}$'.format(i+1))

plt.plot([1,1], [0, 5], label = r'$C_{}$'.format(K-1))

plt.title('Posterior Distributions of Cs', fontsize = 20)

plt.yticks([])

plt.legend()

plt.savefig('FigCsPost.png')

plt.show()

 

plt.figure(figsize=(9,4))

 

plt.subplot(121)

 

v_psis = np.empty(NSt)

for i in range(NSt):

    v_psis[i] = np.median(Psis.T[i]) 

v_sgm = np.median(sgm)

 

fout.write('\n{0:>15s}{1:>15s}\n'.format('Stimulus', 'Med. Psi'))

for vst, vpsi in zip(Sts, v_psis):

    fout.write('{0:>15.2f}{1:>15.2f}\n'.format(vst, vpsi))

fout.write('\nSigma: med = {0:>15.2f}\n'.format(v_sgm))

   

v_Cs = np.empty(K-1)

v_Cs[0] = 0.0

v_Cs[K-2] = 1.0

for k in range(1, K-2):

    v_Cs[k] = np.median(Cs.T[k])

for k in range(K-1):

    plt.plot([Sts[0], Sts[-1]], [v_Cs[k], v_Cs[k]], color = 'y')

plt.plot([], 'y-', label = 'C')

plt.plot(Sts, v_psis, color = 'b', lw = 3, label = r'$\psi$')

plt.plot(Sts, v_psis - v_sgm, color = 'g', linestyle = '--', lw = 1,

         label = r'$\psi - \sigma$')

plt.plot(Sts, v_psis + v_sgm, color = 'g', linestyle = '--', lw = 1,

         label = r'$\psi+ \sigma$')

   

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

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

plt.legend(loc = 'upper left')

plt.title(r'Relation of Stimulus and $\psi$', fontsize = 18)

plt.tight_layout()

 

plt.subplot(122)

 

Probs = np.empty((NSt, K))

for s in range(NSt):

    Probs[s][0] = ss.norm(loc = v_psis[s], scale = v_sgm).cdf(v_Cs[0])

    Probs[s][K-1] = 1.0 - ss.norm(loc = v_psis[s], scale = v_sgm).cdf(v_Cs[K-2])

    for k in range(1, K-1):

        Probs[s][k] = ss.norm(loc = v_psis[s], scale = v_sgm).cdf(v_Cs[k]) -\

                        ss.norm(loc = v_psis[s], scale = v_sgm).cdf(v_Cs[k-1])

           

print('Predicted Prob(Rating|Stimulus):')

for s in range(NSt):

    for k in range(K):

        print(f'  {Probs[s][k]:7.3f}', end = '')

    print()

   

Pred_Rs = np.empty((NSt, K))

N_Rs = np.sum(Rs, axis = 1)

for i in range(NSt):

    Pred_Rs[i] = N_Rs[i] * Probs[i]

   

print('Predicted Frequencies of Rating for Stimulus:')

for s in range(NSt):

    for k in range(K):

        print(f'  {Pred_Rs[s][k]:7.1f}', end = '')

    print()

 

cum_data_Rs = np.cumsum(Rs, axis = 1)

cum_pred_Rs = np.cumsum(Pred_Rs, axis = 1)

 

print(cum_data_Rs)

 

for s in range(NSt):

    if s == 0:

        plt.plot(range(K), cum_data_Rs[s]/cum_data_Rs[s][K-1], 'g:', lw = 3,

                 label = 'Data')

    else:

        plt.plot(range(K), cum_data_Rs[s]/cum_data_Rs[s][K-1], 'g:', lw = 3 )

    if s == 0:

        plt.plot(range(K), cum_pred_Rs[s]/cum_pred_Rs[s][K-1], 'b-',

                 label = 'Model')

    else:

        plt.plot(range(K), cum_pred_Rs[s]/cum_pred_Rs[s][K-1], 'b-')

 

plt.xticks(range(K), range(1, K+1))

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

plt.ylabel('Cum. Prop.', fontsize = 14)

plt.legend()

plt.title('Data and Model Prediction', fontsize = 18)

plt.tight_layout()

plt.savefig('FigDataModel.png')

plt.show()

 

fout.close()

print('Results.txt was saved.')

 

 

 

Up