Item Response Theory
library(lme4)
library(mirt)
Rasch Model
data(LSAT7, package = "mirt")
<- expand.table(LSAT7) dat
<- mirt(dat, model = 1, itemtype = "Rasch") m_rasch
Iteration: 1, Log-Lik: -2664.942, Max-Change: 0.01075
Iteration: 2, Log-Lik: -2664.913, Max-Change: 0.00257
Iteration: 3, Log-Lik: -2664.910, Max-Change: 0.00210
Iteration: 4, Log-Lik: -2664.908, Max-Change: 0.00179
Iteration: 5, Log-Lik: -2664.906, Max-Change: 0.00156
Iteration: 6, Log-Lik: -2664.905, Max-Change: 0.00137
Iteration: 7, Log-Lik: -2664.904, Max-Change: 0.00141
Iteration: 8, Log-Lik: -2664.903, Max-Change: 0.00110
Iteration: 9, Log-Lik: -2664.903, Max-Change: 0.00095
Iteration: 10, Log-Lik: -2664.902, Max-Change: 0.00086
Iteration: 11, Log-Lik: -2664.902, Max-Change: 0.00072
Iteration: 12, Log-Lik: -2664.902, Max-Change: 0.00063
Iteration: 13, Log-Lik: -2664.902, Max-Change: 0.00065
Iteration: 14, Log-Lik: -2664.901, Max-Change: 0.00051
Iteration: 15, Log-Lik: -2664.901, Max-Change: 0.00044
Iteration: 16, Log-Lik: -2664.901, Max-Change: 0.00040
Iteration: 17, Log-Lik: -2664.901, Max-Change: 0.00034
Iteration: 18, Log-Lik: -2664.901, Max-Change: 0.00029
Iteration: 19, Log-Lik: -2664.901, Max-Change: 0.00030
Iteration: 20, Log-Lik: -2664.901, Max-Change: 0.00024
Iteration: 21, Log-Lik: -2664.901, Max-Change: 0.00020
Iteration: 22, Log-Lik: -2664.901, Max-Change: 0.00018
Iteration: 23, Log-Lik: -2664.901, Max-Change: 0.00016
Iteration: 24, Log-Lik: -2664.901, Max-Change: 0.00014
Iteration: 25, Log-Lik: -2664.901, Max-Change: 0.00014
Iteration: 26, Log-Lik: -2664.901, Max-Change: 0.00011
Iteration: 27, Log-Lik: -2664.901, Max-Change: 0.00009
coef(m_rasch, IRTpars = TRUE) # d = intercept = -1 * difficulty
$Item.1
a b g u
par 1 -1.868 0 1
$Item.2
a b g u
par 1 -0.791 0 1
$Item.3
a b g u
par 1 -1.461 0 1
$Item.4
a b g u
par 1 -0.521 0 1
$Item.5
a b g u
par 1 -1.993 0 1
$GroupPars
MEAN_1 COV_11
par 0 1.022
The 1PL is just a reparameterization of the Rasch model:
# 1PL
<- "
mod_1pl THETA = 1-5
CONSTRAIN = (1-5, a1)
"
<- mirt(dat, mod_1pl) m_1pl
Iteration: 1, Log-Lik: -2668.786, Max-Change: 0.06198
Iteration: 2, Log-Lik: -2666.196, Max-Change: 0.03935
Iteration: 3, Log-Lik: -2665.347, Max-Change: 0.02405
Iteration: 4, Log-Lik: -2664.955, Max-Change: 0.00867
Iteration: 5, Log-Lik: -2664.920, Max-Change: 0.00509
Iteration: 6, Log-Lik: -2664.907, Max-Change: 0.00294
Iteration: 7, Log-Lik: -2664.902, Max-Change: 0.00135
Iteration: 8, Log-Lik: -2664.901, Max-Change: 0.00079
Iteration: 9, Log-Lik: -2664.901, Max-Change: 0.00046
Iteration: 10, Log-Lik: -2664.901, Max-Change: 0.00022
Iteration: 11, Log-Lik: -2664.901, Max-Change: 0.00011
Iteration: 12, Log-Lik: -2664.901, Max-Change: 0.00006
coef(m_1pl, IRTpars = TRUE) # b = difficulty
$Item.1
a b g u
par 1.011 -1.848 0 1
$Item.2
a b g u
par 1.011 -0.782 0 1
$Item.3
a b g u
par 1.011 -1.445 0 1
$Item.4
a b g u
par 1.011 -0.516 0 1
$Item.5
a b g u
par 1.011 -1.971 0 1
$GroupPars
MEAN_1 COV_11
par 0 1
Standard Errors
<- mirt(dat, model = 1, itemtype = "Rasch", SE = TRUE) m_rasch
Iteration: 1, Log-Lik: -2664.942, Max-Change: 0.01075
Iteration: 2, Log-Lik: -2664.913, Max-Change: 0.00257
Iteration: 3, Log-Lik: -2664.910, Max-Change: 0.00210
Iteration: 4, Log-Lik: -2664.908, Max-Change: 0.00179
Iteration: 5, Log-Lik: -2664.906, Max-Change: 0.00156
Iteration: 6, Log-Lik: -2664.905, Max-Change: 0.00137
Iteration: 7, Log-Lik: -2664.904, Max-Change: 0.00141
Iteration: 8, Log-Lik: -2664.903, Max-Change: 0.00110
Iteration: 9, Log-Lik: -2664.903, Max-Change: 0.00095
Iteration: 10, Log-Lik: -2664.902, Max-Change: 0.00086
Iteration: 11, Log-Lik: -2664.902, Max-Change: 0.00072
Iteration: 12, Log-Lik: -2664.902, Max-Change: 0.00063
Iteration: 13, Log-Lik: -2664.902, Max-Change: 0.00065
Iteration: 14, Log-Lik: -2664.901, Max-Change: 0.00051
Iteration: 15, Log-Lik: -2664.901, Max-Change: 0.00044
Iteration: 16, Log-Lik: -2664.901, Max-Change: 0.00040
Iteration: 17, Log-Lik: -2664.901, Max-Change: 0.00034
Iteration: 18, Log-Lik: -2664.901, Max-Change: 0.00029
Iteration: 19, Log-Lik: -2664.901, Max-Change: 0.00030
Iteration: 20, Log-Lik: -2664.901, Max-Change: 0.00024
Iteration: 21, Log-Lik: -2664.901, Max-Change: 0.00020
Iteration: 22, Log-Lik: -2664.901, Max-Change: 0.00018
Iteration: 23, Log-Lik: -2664.901, Max-Change: 0.00016
Iteration: 24, Log-Lik: -2664.901, Max-Change: 0.00014
Iteration: 25, Log-Lik: -2664.901, Max-Change: 0.00014
Iteration: 26, Log-Lik: -2664.901, Max-Change: 0.00011
Iteration: 27, Log-Lik: -2664.901, Max-Change: 0.00009
Calculating information matrix...
coef(m_rasch, IRTpars = TRUE)
$Item.1
a b g u
par 1 -1.868 0 1
CI_2.5 NA -2.065 NA NA
CI_97.5 NA -1.671 NA NA
$Item.2
a b g u
par 1 -0.791 0 1
CI_2.5 NA -0.950 NA NA
CI_97.5 NA -0.632 NA NA
$Item.3
a b g u
par 1 -1.461 0 1
CI_2.5 NA -1.640 NA NA
CI_97.5 NA -1.282 NA NA
$Item.4
a b g u
par 1 -0.521 0 1
CI_2.5 NA -0.676 NA NA
CI_97.5 NA -0.367 NA NA
$Item.5
a b g u
par 1 -1.993 0 1
CI_2.5 NA -2.196 NA NA
CI_97.5 NA -1.790 NA NA
$GroupPars
MEAN_1 COV_11
par 0 1.022
CI_2.5 NA 0.765
CI_97.5 NA 1.279
Plots
Item Response Function
Also item characteristic curve
plot(m_rasch, type = "trace")
Item and Test Information
Item information
plot(m_rasch, type = "infotrace")
plot(m_rasch, type = "info")
Assumptions/Model Fit
Global test statistics (analogous to those in factor analysis)
- \(G^2\) test statistic (not valid when matrix is sparse)
- \(M_2\): better approximation for polytomous models
m_rasch
Call:
mirt(data = dat, model = 1, itemtype = "Rasch", SE = TRUE)
Full-information item factor analysis with 1 factor(s).
Converged within 1e-04 tolerance after 27 EM iterations.
mirt version: 1.38.1
M-step optimizer: nlminb
EM acceleration: Ramsay
Number of rectangular quadrature: 61
Latent density type: Gaussian
Information matrix estimated with method: Oakes
Second-order test: model is a possible local maximum
Condition number of information matrix = 4.488772
Log-likelihood = -2664.901
Estimated parameters: 6
AIC = 5341.802
BIC = 5371.248; SABIC = 5352.192
G2 (25) = 43.89, p = 0.0112
RMSEA = 0.028, CFI = NaN, TLI = NaN
M2(m_rasch)
Parallel analysis
::fa.parallel(dat, cor = "poly") psych
Parallel analysis suggests that the number of factors = 2 and the number of components = 1
DIMTEST
# Need the sirt package. See the conf.detect() function
# Need to obtain WLE scores first
<- fscores(m_rasch, method = "WLE")
wle_scores <- sirt::conf.detect(dat,
detect1 score = as.vector(wle_scores),
itemcluster = rep(1, 5)
)
-----------------------------------------------------------
Confirmatory DETECT Analysis
Conditioning on 1 Score
Bandwidth Scale: 1.1
Pairwise Estimation of Conditional Covariances
...........................................................
Nonparametric ICC estimation
...........................................................
Nonparametric Estimation of conditional covariances
5% 10% 15% 20% 25% 30% 35% 40% 45% 50%
-----------------------------------------------------------
unweighted weighted
DETECT -3.437 -3.437
ASSI -1.000 -1.000
RATIO -1.000 -1.000
MADCOV100 3.437 3.437
MCOV100 -3.437 -3.437
# DETECT < .20 suggest essential unidimensionality
Local Independence
residuals(m_rasch, type = "Q3") # Q3
Q3 summary statistics:
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.196 -0.145 -0.116 -0.110 -0.082 0.000
Item.1 Item.2 Item.3 Item.4 Item.5
Item.1 1.000 -0.136 -0.099 -0.080 -0.051
Item.2 -0.136 1.000 0.000 -0.196 -0.169
Item.3 -0.099 0.000 1.000 -0.133 -0.090
Item.4 -0.080 -0.196 -0.133 1.000 -0.149
Item.5 -0.051 -0.169 -0.090 -0.149 1.000
# All |Q3| < .2 in this case
Item Fit
S-\(\chi^2\)
itemfit(m_rasch)
# For large number of items, the p values should be adjusted
itemfit(m_rasch, p.adjust = "holm")
Two-Parameter Logistic
<- mirt(dat, model = 1, itemtype = "2PL") m_2pl
Iteration: 1, Log-Lik: -2668.786, Max-Change: 0.18243
Iteration: 2, Log-Lik: -2663.691, Max-Change: 0.13637
Iteration: 3, Log-Lik: -2661.454, Max-Change: 0.10231
Iteration: 4, Log-Lik: -2659.430, Max-Change: 0.04181
Iteration: 5, Log-Lik: -2659.241, Max-Change: 0.03417
Iteration: 6, Log-Lik: -2659.113, Max-Change: 0.02911
Iteration: 7, Log-Lik: -2658.812, Max-Change: 0.00456
Iteration: 8, Log-Lik: -2658.809, Max-Change: 0.00363
Iteration: 9, Log-Lik: -2658.808, Max-Change: 0.00273
Iteration: 10, Log-Lik: -2658.806, Max-Change: 0.00144
Iteration: 11, Log-Lik: -2658.806, Max-Change: 0.00118
Iteration: 12, Log-Lik: -2658.806, Max-Change: 0.00101
Iteration: 13, Log-Lik: -2658.805, Max-Change: 0.00042
Iteration: 14, Log-Lik: -2658.805, Max-Change: 0.00025
Iteration: 15, Log-Lik: -2658.805, Max-Change: 0.00026
Iteration: 16, Log-Lik: -2658.805, Max-Change: 0.00023
Iteration: 17, Log-Lik: -2658.805, Max-Change: 0.00023
Iteration: 18, Log-Lik: -2658.805, Max-Change: 0.00021
Iteration: 19, Log-Lik: -2658.805, Max-Change: 0.00019
Iteration: 20, Log-Lik: -2658.805, Max-Change: 0.00017
Iteration: 21, Log-Lik: -2658.805, Max-Change: 0.00017
Iteration: 22, Log-Lik: -2658.805, Max-Change: 0.00015
Iteration: 23, Log-Lik: -2658.805, Max-Change: 0.00015
Iteration: 24, Log-Lik: -2658.805, Max-Change: 0.00013
Iteration: 25, Log-Lik: -2658.805, Max-Change: 0.00013
Iteration: 26, Log-Lik: -2658.805, Max-Change: 0.00011
Iteration: 27, Log-Lik: -2658.805, Max-Change: 0.00011
Iteration: 28, Log-Lik: -2658.805, Max-Change: 0.00010
coef(m_2pl, IRTpars = TRUE)
$Item.1
a b g u
par 0.988 -1.879 0 1
$Item.2
a b g u
par 1.081 -0.748 0 1
$Item.3
a b g u
par 1.706 -1.058 0 1
$Item.4
a b g u
par 0.765 -0.635 0 1
$Item.5
a b g u
par 0.736 -2.52 0 1
$GroupPars
MEAN_1 COV_11
par 0 1
plot(m_rasch, type = "trace")
Response function on the same canvas
plot(m_2pl, type = "trace", facet_items = FALSE)
Model Comparison
anova(m_rasch, m_2pl)
Some Polytomous Models
Rating scale model
data(bfi, package = "psych")
# Neuroticism
<- mirt(bfi[16:20], itemtype = "rsm") m_rsm
Iteration: 1, Log-Lik: -23985.274, Max-Change: 1.00910
Iteration: 2, Log-Lik: -22434.284, Max-Change: 0.18792
Iteration: 3, Log-Lik: -22338.797, Max-Change: 0.17826
Iteration: 4, Log-Lik: -22282.201, Max-Change: 0.12715
Iteration: 5, Log-Lik: -22244.160, Max-Change: 0.09083
Iteration: 6, Log-Lik: -22217.562, Max-Change: 0.06646
Iteration: 7, Log-Lik: -22198.665, Max-Change: 0.04953
Iteration: 8, Log-Lik: -22185.225, Max-Change: 0.03738
Iteration: 9, Log-Lik: -22175.730, Max-Change: 0.02839
Iteration: 10, Log-Lik: -22169.095, Max-Change: 0.02270
Iteration: 11, Log-Lik: -22164.507, Max-Change: 0.01858
Iteration: 12, Log-Lik: -22161.370, Max-Change: 0.01513
Iteration: 13, Log-Lik: -22156.185, Max-Change: 0.01992
Iteration: 14, Log-Lik: -22155.234, Max-Change: 0.00485
Iteration: 15, Log-Lik: -22155.115, Max-Change: 0.00311
Iteration: 16, Log-Lik: -22154.996, Max-Change: 0.00344
Iteration: 17, Log-Lik: -22154.963, Max-Change: 0.00141
Iteration: 18, Log-Lik: -22154.950, Max-Change: 0.00099
Iteration: 19, Log-Lik: -22154.935, Max-Change: 0.00132
Iteration: 20, Log-Lik: -22154.930, Max-Change: 0.00040
Iteration: 21, Log-Lik: -22154.929, Max-Change: 0.00027
Iteration: 22, Log-Lik: -22154.928, Max-Change: 0.00033
Iteration: 23, Log-Lik: -22154.928, Max-Change: 0.00012
Iteration: 24, Log-Lik: -22154.928, Max-Change: 0.00008
itemfit(m_rsm, na.rm = TRUE) # not fitting well
Sample size after row-wise response data removal: 2694
plot(m_rsm, type = "trace")
Graded Response model
<- mirt(bfi[16:20], itemtype = "graded") m_grm
Iteration: 1, Log-Lik: -22678.849, Max-Change: 0.98180
Iteration: 2, Log-Lik: -22009.516, Max-Change: 0.62057
Iteration: 3, Log-Lik: -21843.393, Max-Change: 0.31998
Iteration: 4, Log-Lik: -21773.416, Max-Change: 0.25006
Iteration: 5, Log-Lik: -21747.311, Max-Change: 0.16264
Iteration: 6, Log-Lik: -21734.951, Max-Change: 0.15148
Iteration: 7, Log-Lik: -21728.377, Max-Change: 0.09627
Iteration: 8, Log-Lik: -21724.841, Max-Change: 0.07974
Iteration: 9, Log-Lik: -21722.917, Max-Change: 0.06180
Iteration: 10, Log-Lik: -21721.933, Max-Change: 0.02070
Iteration: 11, Log-Lik: -21721.615, Max-Change: 0.01547
Iteration: 12, Log-Lik: -21721.511, Max-Change: 0.01199
Iteration: 13, Log-Lik: -21721.392, Max-Change: 0.00246
Iteration: 14, Log-Lik: -21721.388, Max-Change: 0.00214
Iteration: 15, Log-Lik: -21721.385, Max-Change: 0.00159
Iteration: 16, Log-Lik: -21721.382, Max-Change: 0.00202
Iteration: 17, Log-Lik: -21721.381, Max-Change: 0.00050
Iteration: 18, Log-Lik: -21721.381, Max-Change: 0.00008
itemfit(m_grm, na.rm = TRUE) # not fitting well
Sample size after row-wise response data removal: 2694
plot(m_grm, type = "trace")
plot(m_grm, type = "info")
Estimation of 1-PL Model
For simplicity, I assume \(Da = 1\) here; in practice \(a\) is usually estimated with 1-PL.
Conditional likelihood (assumed \(\theta\) known)
Let \(P_i(\theta_j)\) = \(P(Y_{i} = 1 \mid \theta = \theta_j)\)
If \(Y_{ij} = 1\),
\[\mathcal{L}(b_i; y_{ij}, \theta_j) = P_i(\theta_j)\]
If \(y_{ij} = 0\),
\[\mathcal{L}(b_i; y_{ij}, \theta_j) = 1 - P_i(\theta_j)\]
Marginal Likelihood (Integrating out \(\theta\))
Let \(\theta_j \sim N(0, 1)\), meaning its probability density is
\[f(\theta) = \Phi(\theta) = \frac{1}{\sqrt{2 \pi} \sigma} e^{-\theta^2 / 2}\]
\[\mathcal{L}(b_i; Y_{ij} = 1) = \int_{-\infty}^\infty P_i(t) \Phi(t) \,\mathrm{d} t\]
and
\[\ell(b_i; Y_{ij} = 1) = \log \left[\int_{-\infty}^\infty P_i(t) \Phi(t) \,\mathrm{d} t\right]\]
Combining \(N\) independent observations, and let \(\bar y_i\) be the mean of item \(i\) (i.e., proportion scoring 1),
\[ \begin{aligned} \ell(b_i; y_{i1}, y_{i2}, \ldots, y_{iN}) & = N \left\{\bar y_i \log \left[\int_{-\infty}^\infty P_i(t) \Phi(t) \,\mathrm{d} t \right] \right. \\ & \quad \left. {} + (1 - \bar y_i) \log \left[1 - \int_{-\infty}^\infty P_i(t) \Phi(t) \,\mathrm{d} t \right]\right\} \end{aligned} \]
Maximum likelihood estimation finds values of \(b_i\) that maximizes the above quantity. However, the above quantity involves an integral, which does not generally have an analytic solution. More typically, we use Gaussian-Hermite quadrature to approximate the integral.
Quadrature
See https://en.wikipedia.org/wiki/Gauss%E2%80%93Hermite_quadrature
# Approximate z^2 for z ~ N(0, 1)
<- lme4::GHrule(5)
ghq5 sum(ghq5[, "w"] * ghq5[, "z"]^2)
[1] 1
<- function(theta, bj) plogis(theta - bj)
pj sum(ghq5[, "w"] * pj(ghq5[, "z"], bj = 1))
[1] 0.303218
sum(ghq5[, "w"] * pj(ghq5[, "z"], bj = 2))
[1] 0.1555028
<- function(b, n, ybar, gh_ord = 5) {
loglik_1pl <- lme4::GHrule(gh_ord)
ghq <- sum(ghq[, "w"] * pj(ghq[, "z"], bj = b))
int_pj * (ybar * log(int_pj) + (1 - ybar) * log1p(-int_pj))
n
}loglik_1pl(1, n = 1000, ybar = 828 / 1000)
[1] -1050.196
# Vectorized version
<- function(theta, bj) plogis(outer(theta, Y = bj, FUN = "-"))
pj <- function(b, n, ybar, ghq = lme4::GHrule(5)) {
loglik_1plj <- crossprod(ghq[, "w"], pj(ghq[, "z"], bj = b))
int_pj as.vector(
* (ybar * log(int_pj) + (1 - ybar) * log1p(-int_pj))
n
)
}loglik_1plj(c(-1, 0, 1), n = 1000, ybar = 828 / 1000)
[1] -504.3902 -693.1472 -1050.1957
curve(loglik_1plj(x, n = 1000, ybar = 828 /1000),
from = -4, to = 4,
xlab = expression(b[i]),
ylab = "log-likelihood")
# Optimization
optim(0,
fn = loglik_1plj,
n = 1000, ybar = 828 / 1000,
method = "L-BFGS-B",
# Minimize -2 x ll
control = list(fnscale = -2)
)
$par
[1] -1.862783
$value
[1] -459.0433
$counts
function gradient
7 7
$convergence
[1] 0
$message
[1] "CONVERGENCE: REL_REDUCTION_OF_F <= FACTR*EPSMCH"
While here we assume \(a\) is known, in reality, \(a\) needs to be estimated as well and require we obtain the joint likelihood by adding up the likelihood across all items (assuming local independence).