Home

Reliability Coefficient omega  of an Ordinal Categorical Items Scale

Direct Bayesian Estimation

 

A reliability coefficient, McDonaldfs omega (McDonald, 1999), is based on the following model (1).

 denotes the value of person  on item .

 denotes a factor score of person . It is assumed to have the standard normal distribution.

The residual  has the following distribution

The score of person  is given by the sum of categorical values as follows

Based on this model, the reliability coefficient McDonaldfs omeg (McDonald, 1999) is given by eq. (2)

 

In the case of an ordinal categorical items scale, a categorical value of an item is given based on the continuous value of .

Number of categorical values is denoted by , and a categorical value is represented by an integer from 1 to .

Letfs set the following model (Okamoto, 2013).

Category boundaries is set as follows.

Using these boundaries, a categorical response  is given based on  as follows.

Because

we have

 denotes the cumulative function of the standard normal distribution, and it is assumed that

To estimate parameter values, put the following constraint

Furthermore, here, it is assumed that the number of categories is larger than 2

For the reason, see Okamoto (2013).

Now, we have the following likelihood function

Hence, we have the following posterior distribution

The prior distribution  is set to be almost flat (Gelman et al., 2021).

 

A Stan script for the above model is shown in Listing 1.

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

Script files and sample data files are archived into the ZIP file omega_files.zip, which can be freely downloaded and used.

The Python script in Listing 2 was run on Python 3.7 virtual environment on Anaconda on Windows 10.

For information on how to install Python and PyStan (the Python interface to Stan), check this website for Windows, or this website for Linux. This website is for Windows and for Linux.

 

The script in Listing 2 can be executed by the following code

 

(py37) PS D:\omega_files> python CalcOmegaMain.py

 

CalcOmegaMain.py is the name of the script in Listing 2.

When the script starts, the name of the input data file was asked as follows

 

(py37) PS D:\omega_files> python CalcOmegaMain.py

Input file =

 

An input data file should be prepared as an Excel file like one shown in Figure 1.

Figure 1

 

In the first row, the number of categories, which are the same numbers, is set.

In the second row, names of items are set.

In the third row, integer values 1 or 0 are set. Integer 1 means that this item should be included in the analysis, 0 means that this item is excluded from the analysis.

Data of the category responses are set from the fourth row on.

In the first column, case identification strings or numbers are set. In the analysis, these values are not used.

From the second column, category responses of a person are set in each row.

 

When the name of the input data file is set like the following, the program starts.

 

(py37) PS D:\omega_files> python CalcOmegaMain.py

Input file = DataCat5.xlsx

 

After MCMC sampling ends, statistics of the sampling are shown as follows.

 

Elapsed Time: 75.129 seconds (Warm-up)

               31.615 seconds (Sampling)

               106.744 seconds (Total)

 

Inference for Stan model: anon_model_4de137d01c5c96aab1aeacdbb8a86c8c.

4 chains, each with iter=2000; warmup=1000; thin=1;

post-warmup draws per chain=1000, total post-warmup draws=4000.

 

          mean se_mean     sd   2.5%    25%    50%    75%  97.5%  n_eff   Rhat

mu[0]     0.53  7.5e-4   0.04   0.45    0.5   0.53   0.56    0.6   2571    1.0

mu[1]     0.48  7.5e-4   0.04   0.41   0.46   0.48    0.5   0.55   2236    1.0

mu[2]     0.51  6.1e-4   0.04   0.43   0.48   0.51   0.53   0.58   4000    1.0

mu[3]      0.5  7.9e-4   0.04   0.42   0.47    0.5   0.52   0.57   2299    1.0

mu[4]      0.5  7.7e-4   0.04   0.42   0.47    0.5   0.53   0.58   2566    1.0

lmbd[0]   0.33  8.5e-4   0.04   0.25    0.3   0.33   0.36   0.41   2608    1.0

.

.

.

 

Then, posterior distribution of the omega  is shown (Figure 2).

 

Figure 2

 

Close the window of Figure 2, posterior distribution of correlation coefficient between  and  is shown (Figure 3).

Figure 3

 

Close the window of Figure 3, execution of the script ends.

In the terminal, messages like the following are displayed.

 

FigOmega.png was saved.

FigCors,png was saved.

Output.txt was saved.

(py37) PS D:\omega_files>

 

Files FigOmega.png, FigCors,png contain Figure 2 and 3, respectively.

The file Output.txt contains the following outputs.

.

.

.

Lambda...

    Item-1:  0.299   0.328   0.357

    Item-2:  0.290   0.317   0.347

    Item-3:  0.276   0.306   0.337

    Item-4:  0.296   0.324   0.353

    Item-5:  0.295   0.325   0.356

.

.

.

 

For each item, the first quantile value Q1, the median, the third quantile value Q3 are shown in each row.

The values of Lambda of Item-3 are the smallest.

 In data file of Figure 4, integer 0 in set for Item-3 to exclude from the analysis.

Figure 4

 

Then, run the script as follows.

 

(py37) PS D:\omega_files> python CalcOmegaMain.py

Input file = DataCat5del3.xlsx

 

File name DataCat5del3.xlsx is the name of the file shown in Figure 4.

 

Then, we get the results as shown in Figure 5.

Figure 5

 

Values, which are a little smaller than the values in Figures 2 and 3, are shown.

Maybe, effect of excluding the item with smaller lambda is cancelled by that of reducing the number of items.

 

 

References

Gelman, A., Hill, J., & Vehtari, A. (2021). Regression and Other Stories. Cambridge University Press.

McDonald, R. P. (1999). Test Theory: A unified treatment. Lawrence Erlbaum Associations, Publishers.

Okamoto, Y. (2013). gA Direct Bayesian Estimation of Reliabilityh Behaviormetrika, 40, 149-168.

 

 

Listing 1. Stan script for the model (calc_omega.stan)

 

//

//        Yasuharu Okamoto, 2023.02.01

//

data {

    int n_items;

    int n_persons;

    int<lower = 3> K;        //   Number of categories is larger than 2

    int X[n_persons,n_items];

}

transformed data {

    vector[K-2] a_K;

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

        a_K[k] = 1.0;

    }

}

parameters {

    real F[n_persons];

    real mu[n_items];

    real<lower = 0.0> lmbd[n_items];

    real<lower = 0.0> sgm[n_items];

    simplex[K-2] theta;

}

transformed parameters {

    real C[K-1];

    simplex[K] p[n_persons,n_items];

    C[1] = 0.0;

    C[K-1] = 1.0;

    if (K > 3) {

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

            C[j] = C[j-1] + theta[j-1];

        }

    }

    for (i in 1:n_persons) {

        for (j in 1:n_items) {

            p[i][j][1] = normal_cdf(C[1],

                                    mu[j] + lmbd[j] * F[i], sgm[j]);

            p[i][j][K] = 1.0 - normal_cdf(C[K-1],

                                    mu[j] + lmbd[j] * F[i], sgm[j]);

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

                p[i][j][k] = normal_cdf(C[k],

                                    mu[j] + lmbd[j] * F[i], sgm[j]) -

                             normal_cdf(C[k-1],

                                    mu[j] + lmbd[j] * F[i], sgm[j]);

            }

        }

    }

}

model {

    theta ~ dirichlet(a_K);

    for (i in 1:n_persons) {

        F[i] ~ normal(0.0, 1.0);

    }

    for (j in 1:n_items) {

        lmbd[j] ~ exponential(0.1);

        mu[j] ~ normal(0.0, 10.0);

        sgm[j] ~ exponential(0.1);

    }

    for (i in 1:n_persons) {

        for (j in 1:n_items) {

            X[i][j] ~ categorical(p[i][j]);

        }

    }

}

 

 

Listing 2.  Python script file (CalcOmegaMain.py)

 

import pandas as pd

import numpy as np

import pystan

import platform

import pickle

import scipy.stats as ss

import matplotlib.pyplot as plt

import seaborn as sb

 

in_flnm = input('Input file(*.xlsx) = ')       #   Excel file   

out_flnm = 'Output.txt'

out_f = open(out_flnm, 'w')

out_f.write('Input data file = ' + in_flnm + '\n')

#

#        Read in the Excel file

#

fl_xlsx = pd.ExcelFile(in_flnm)

data = pd.read_excel(fl_xlsx, header = None)

data = data.values

K = data[0][1]

print('K =', K)

print(data[:10])

out_f.write('\ndata...\n')

for v in data:

    out_f.write(v.__str__() + '\n')

item_names = data[1]

print(item_names)

ck_items = data[2]

sel_items = ck_items == 1

print(sel_items)

item_names = item_names[sel_items]

print('Selected items...')

print(item_names)

out_f.write('\n\nThe selected item names...' + item_names.__str__() + '\n')

 

data = np.array(data)

X = data[3:].T[1:].T

X = X[:, sel_items[1:]]

print(X[:3])

#

#        MCMC sampling by Stan

#

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

 

X = X.astype(dtype = int)

fit = sm.sampling(data = {'n_items':len(X[0]), 'n_persons':len(X), 'K':K, 'X': X},

                  pars = ['mu', 'lmbd', 'sgm', 'F'],

                             n_jobs = 1 if platform.system() == 'Windows' else -1)

 

#

#           Results of MCMC sampling

#

print(fit)

out_f.write('\nfit...\n')

out_f.write(fit.__str__())

#

#      Calculation on parameter mu

#

print('mu =\n', fit['mu'[:10]])

out_f.write('\nMu (Q1, Med, Q3)...\n')

for j in range(len(item_names)):

    out_f.write('    ' + item_names[j] + ':  ')

    mu_q1, mu_med, mu_q3 = np.percentile(fit['mu'].T[j], [25, 50, 75])

    out_f.write('{0:.3f}   {1:.3f}   {2:.3f}\n'.format(mu_q1, mu_med, mu_q3))

#

#       Calculation on parameter lambda

#

print('lmbd =\n', fit['lmbd'][:10])

print('Lambda =\n', fit['lmbd'[:10]])

out_f.write('\nLambda (Q1, Med, Q3)...\n')

for j in range(len(item_names)):

    out_f.write('    ' + item_names[j] + ':  ')

    lmbd_q1, lmbd_med, lmbd_q3 = np.percentile(fit['lmbd'].T[j], [25, 50, 75])

    out_f.write('{0:.3f}   {1:.3f}   {2:.3f}\n'.format(lmbd_q1, lmbd_med, lmbd_q3))

#

#         Calculation on parameter sigma

#

print('Sigma =\n', fit['sgm'[:10]])

out_f.write('\nSigma (Q1, Med, Q3)...\n')

for j in range(len(item_names)):

    out_f.write('    ' + item_names[j] + ':  ')

    sgm_q1, sgm_med, sgm_q3 = np.percentile(fit['sgm'].T[j], [25, 50, 75])

    out_f.write('{0:.3f}   {1:.3f}   {2:.3f}\n'.format(sgm_q1, sgm_med, sgm_q3))

#

#         Calculation on omega

#

sum_lmbd = fit['lmbd'].sum(axis=1)

sum_sqr_sgm = (fit['sgm']**2).sum(axis=1)

omegas = sum_lmbd**2 / (sum_lmbd**2 + sum_sqr_sgm)

 

def calcMAPEst(samples, a = 0.05, n_points = 1000):

    """         Estimation of the MAP estimate by kde

         samples:    Input data for which MAP estimate is calculated.

         a:          Each side of a/2 of samples is ignored, Default value is 0.05

         n_points:   number of points used in kde.  Default value is 1000

    """

    import numpy as np

    import scipy.stats as ss

 

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

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

    est_pdf = ss.gaussian_kde(samples).pdf(coord)

    map_idx = np.argmax(est_pdf)

    MAP_Est = coord[map_idx]     

    return MAP_Est, est_pdf[map_idx]    #   MAP estimate,  the value at MAP estimate

 

 

MAP = calcMAPEst(omegas)[0]

print('MAP =', MAP)

Q1, Med, Q3 = np.percentile(omegas, [25, 50, 75])

print(Q1, Med, Q3)

out_f.write('\nomega (MAP / Q1, Med, Q3)...\n')

out_f.write('    {0:.3f}\n'.format(MAP))

out_f.write('    {0:.3f}   {1:.3f}   {2:.3f}\n'.format(Q1, Med, Q3))

 

sb.kdeplot(omegas, color = 'b')

plt.title('MAP = {0:.2f}\nQ1 = {1:.2f}  Med = {2:.2f}  Q3 = {3:.2f}'.format(MAP, Q1, Med, Q3))

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

plt.savefig('FigOmega.png')

plt.show()

#

#          Calculation on Pearson correlation coefficient

#            between factor F and the sum score of Xjs

#

SumX = X.sum(axis=1)

Cors = []

for Fvalues in fit['F']:

    r = ss.pearsonr(SumX, Fvalues)[0]

    Cors.append(r)

   

r_map = calcMAPEst(Cors)[0]

r_q1, r_med, r_q3 = np.percentile(Cors, [25, 50, 75])

 

out_f.write('\nCorrelation between F and X (MAP / Q1, Med, Q3)...\n')

out_f.write('    {0:.3f}\n'.format(r_map))

out_f.write('    {0:.3f}   {1:.3f}   {2:.3f}\n'.format(r_q1, r_med, r_q3))

   

sb.kdeplot(Cors)

plt.title('MAP = {0:.2f}\nQ1 = {1:.2f}  Med = {2:.2f}  Q3 = {3:.2f}'.

         format(r_map, r_q1, r_med, r_q3), fontsize = 14)

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

plt.savefig('FigCors.png')

plt.show()

print('FigOmega.png was saved.')

print('FigCors,png was saved.')

out_f.close()

print(out_flnm + ' was saved.')

 

 

 

Home