15  Fixing Item Parameters in mirt

Author

Derek C. Briggs and Claude Code (Opus 4.6 & 4.7)

15.1 Introduction

The purpose of this tutorial test is to show how to fix the values of various item parameters in mirt. There are lots of reasons you might want to do this–I’ll get into this later.

15.2 The starting values for item (and person) parameters in mirt

For now let me just show how this works using the data from Embretson & Reise, Chapter 7, Tables 7.2 & 7.3.

Code
library(mirt)
Loading required package: stats4
Loading required package: lattice
Code
d<-read.csv("/Users/briggsd/Library/CloudStorage/Dropbox/Courses/EDUC 8720/Data Sets/Misc/ER_data_tab7-2.csv")

Instead of calibrating the 2PL, I’m going to run the following command

Code
sv1 <- mirt(d, 1,  itemtype='2PL', pars = 'values',verbose=FALSE)

Let’s have a look at the first 8 rows this object

Code
sv1[1:8,] 
  group item class name parnum  value lbound ubound   est const nconst
1   all  it1  dich   a1      1  0.851   -Inf    Inf  TRUE  none   none
2   all  it1  dich    d      2 -0.107   -Inf    Inf  TRUE  none   none
3   all  it1  dich    g      3  0.000      0      1 FALSE  none   none
4   all  it1  dich    u      4  1.000      0      1 FALSE  none   none
5   all  it2  dich   a1      5  0.851   -Inf    Inf  TRUE  none   none
6   all  it2  dich    d      6  0.107   -Inf    Inf  TRUE  none   none
7   all  it2  dich    g      7  0.000      0      1 FALSE  none   none
8   all  it2  dich    u      8  1.000      0      1 FALSE  none   none
  prior.type prior_1 prior_2
1       none     NaN     NaN
2       none     NaN     NaN
3       none     NaN     NaN
4       none     NaN     NaN
5       none     NaN     NaN
6       none     NaN     NaN
7       none     NaN     NaN
8       none     NaN     NaN
  • This table has 4 rows for each of the first 2 of 10 items in the ER data.
  • Each row corresponds to a different item parameter that it would be possible to specify, and these are listed under the “name” column: a1 is item discrimination, d is location (in mirt’s slope-intercept form), g is the lower asymptote (guessing parameter) and u is an upper asymptote (yes, there is actually a 4PL model).
  • Notice that each item parameter also has a “value” column. These represent “starting values” before mirt invokes the MMLE/EM algorithm. Because we set itemtype to 2PL, the g and u parameters are set to 0 and 1.
  • The “lbound” and “ubound” columns specify the lower and upper bound for possible values of each parameter. Notice that right now there are no bounds on values that can be estimated for a1 and d.
  • The “est” column is very important. It indicates whether or not a given item and parameter combination will be estimated. Right now, this is set to TRUE for all a1 and d item parameters, but FALSE for all g and u parameters
  • Finally, we have the “prior_1” and “prior_2” columns. Right now these are all empty, but mirt gives us the flexibility to specify a unique Bayesian prior distribution for each item parameter.

I also want to show you the last two rows of sv1

Code
sv1[41:42,] 
   group  item     class   name parnum value lbound ubound   est const nconst
41   all GROUP GroupPars MEAN_1     41     0   -Inf    Inf FALSE  none   none
42   all GROUP GroupPars COV_11     42     1      0    Inf FALSE  none   none
   prior.type prior_1 prior_2
41       none     NaN     NaN
42       none     NaN     NaN

These are the only rows in the table that don’t correspond to item parameters. Instead, these are the mean and variance of the person ability distribution (assuming we are specifying a normal distribution for ability, which is the default in mirt). Note that these values, by default are set to 0 and 1 to identify the scale.

15.3 Replicating the first column of ER’s Table 7.3

Now, in the ER example in Table 7.3, they are getting ML estimates (“scores”) of theta for their 23 examinees after imposing different assumptions about the true values of the item parameters in each case. I want to show you how to replicate what they did with mirt by fixing values in the sv1 object I just introduced.

For the first column, all discrimination values are fixed at 1 and item difficulty goes -2 to 2 mostly in increments of .5 logits.

Code
#custom discrimination and easiness values
sv1$value[sv1$name == 'a1'] <- rep(1,10) #Creates a vector of 10 1's
sv1$value[sv1$name == 'd'] <- -1*c(-2,-1.5,-1,-.5,0,0,.5,1,1.5,2) #Multiplied by -1 to make this easiness
#Fix the parameters to these starting values, do NOT estimate them
sv1$est <- FALSE
#Set pop mean and variance as free to be estimated
sv1$est[sv1$name == 'MEAN_1'] <- TRUE
sv1$est[sv1$name == 'COV_11'] <- TRUE

Do you see what we are doing here? Let’s see how the sv1 object has changed

Code
sv1[1:8,] 
  group item class name parnum value lbound ubound   est const nconst
1   all  it1  dich   a1      1   1.0   -Inf    Inf FALSE  none   none
2   all  it1  dich    d      2   2.0   -Inf    Inf FALSE  none   none
3   all  it1  dich    g      3   0.0      0      1 FALSE  none   none
4   all  it1  dich    u      4   1.0      0      1 FALSE  none   none
5   all  it2  dich   a1      5   1.0   -Inf    Inf FALSE  none   none
6   all  it2  dich    d      6   1.5   -Inf    Inf FALSE  none   none
7   all  it2  dich    g      7   0.0      0      1 FALSE  none   none
8   all  it2  dich    u      8   1.0      0      1 FALSE  none   none
  prior.type prior_1 prior_2
1       none     NaN     NaN
2       none     NaN     NaN
3       none     NaN     NaN
4       none     NaN     NaN
5       none     NaN     NaN
6       none     NaN     NaN
7       none     NaN     NaN
8       none     NaN     NaN
Code
sv1[41:42,] 
   group  item     class   name parnum value lbound ubound  est const nconst
41   all GROUP GroupPars MEAN_1     41     0   -Inf    Inf TRUE  none   none
42   all GROUP GroupPars COV_11     42     1      0    Inf TRUE  none   none
   prior.type prior_1 prior_2
41       none     NaN     NaN
42       none     NaN     NaN

When we fit the 2PL under these constraints, we aren’t estimating any item parameters. The only thing getting estimated is the population mean and variance. Then we go straight to estimating thetas.

Code
fit.1 <- mirt(d, 1, pars = sv1,verbose= FALSE)
coef.fit.1 <- coef(fit.1, IRTpars=TRUE, simplify=TRUE)
coef.fit.1
$items
     a    b g u
it1  1 -2.0 0 1
it2  1 -1.5 0 1
it3  1 -1.0 0 1
it4  1 -0.5 0 1
it5  1  0.0 0 1
it6  1  0.0 0 1
it7  1  0.5 0 1
it8  1  1.0 0 1
it9  1  1.5 0 1
it10 1  2.0 0 1

$means
    F1 
-0.393 

$cov
      F1
F1 0.578
Code
# The estimated population SD
sqrt(coef(fit.1)$GroupPars[2]) 
[1] 0.7600784
Code
theta_mle1 <- fscores(fit.1, method = "ML", full.scores.SE = TRUE)
round(theta_mle1,2)
         F1 SE_F1
 [1,]  0.00  0.73
 [2,]  0.00  0.73
 [3,]  0.00  0.73
 [4,]  0.00  0.73
 [5,]  0.00  0.73
 [6,]  0.00  0.73
 [7,] -1.12  0.78
 [8,] -1.12  0.78
 [9,] -1.12  0.78
[10,] -1.12  0.78
[11,] -1.12  0.78
[12,] -1.12  0.78
[13,] -1.12  0.78
[14,] -1.12  0.78
[15,] -2.74  1.12
[16,] -1.79  0.87
[17,] -1.12  0.78
[18,] -0.54  0.74
[19,]  0.00  0.73
[20,]  0.54  0.74
[21,]  1.12  0.78
[22,]  1.79  0.87
[23,]  2.74  1.12
Code
#An estimate of marginal reliability
num <- mean(theta_mle1[,2]) #Average SEM of thetas
den <- var(theta_mle1[,1]) #Total observed Variance of estimated thetas
1 - num/den #Proportion of Observed Variance that is "real" variance
[1] 0.4322175
Code
sqrt((1 - num/den)*den) #The "true" standard deviation of theta (compare to estimated pop SD)
[1] 0.7803674

You can see that this replicates the results in columns for \(\alpha=1\) in ER’s Table 7.3.

15.4 For you to try

For you to try on your own: use the approach above to replicate ER’s columns for \(\alpha=2\) and \(\alpha=2.5\).

15.5 mirt.specify.partable in the R package sirt

There is an even nicer way to specify these values using a function in the package sirt called mirt.specify.partable. To see how it works, first install the sirt package, load it, and then type ?mirt.specify.partable for an illustration of how this works.