birt fits item
response theory models with one or two latent variables from
lavaan-style syntax. Typical use: a joint model of response accuracy
(binary items, logit link) and log response times, with ability and
speed; or a Likert scale moderated by a total response time.
By default birt(model, data) fits the model by
ELGM, approximate Bayesian inference for extended
latent Gaussian models (Stringer, Brown & Stafford, 2023), in
‘RTMB’:
dpriors(), is integrated by an outer adaptive quadrature
over the hyperparameters (Gaussian in the other directions);compare() gives Bayes factors.The same marginal likelihood can be maximized instead
(rtmb_control(method = "aghq")): maximum likelihood, needed
by score_test(), quantile_score(),
ipcr() and the deprecated dif_tree(), and the
natural choice for AIC / BIC. The JAGS engine
(engine = "jags", optional) samples the conditional
likelihood and is needed for spike-and-slab priors, ald()
quantile models, DIC / WAIC / PSIS-LOO and ppc().
library(birt)
d <- sim_rtirt(N = 500, K = 10, seed = 1) # y1..y10 (0/1), t1..t10 (log RT), x1, x2
items <- paste0("y", 1:10)
times <- paste0("t", 1:10)
model <- rtirt_syntax(items, times) # van der Linden (2007)
cat(model) # ability =~ y's; speed =~ -1*t's; ability ~~ speed
fit <- birt(model, data = d) # ELGM (default), about 20 s
fit # log marginal likelihood, inner / outer quadrature
summary(fit) # posterior means, SDs, 95% intervals, priors=~ line of a factor gives its measurement
loadings; later lines add cross-loadings
(e.g. rtirt_syntax(items, times, cross = "free"): ability
on each log RT, Bolsinova & Tijmstra, 2018).ability has variance 1 (it has free loadings);
speed, whose loadings are all fixed, has a free variance;
they are uncorrelated unless ability ~~ speed is
added.dpriors()
(dp = dpriors(cross = "dnorm(0, 100)") changes a class), or
prior("dnorm(0, 100)")*t1 for one parameter (JAGS precision
parameterization).estimates(fit) # lavaan-like table: est, se (posterior SD), lower, upper, prior
scores(fit) # EAP and posterior SD per person and latent variable
reliability(fit) # var(EAP) / (var(EAP) + mean PSD^2)
summary(fit)$items # a, b (IRT parameterization), RT intercepts, loadings, variances
fit_indices(fit) # log marginal likelihood
convergence(fit) # optimizer, outer nodes, quadrature check
equations(fit) # the model as equations (format = "latex" for a paper)
algorithm(fit) # how the fit was computed, step by stepThese tables and numbers print with 3 decimals (p-values below 0.001
as <0.001, counts without decimals); the returned values
keep full precision, e.g. estimates(fit)$est.
fc <- birt(rtirt_syntax(items, times, cross = "free"), d) # item cross-loadings
fi <- birt(sub("ability ~~ speed", "", model, fixed = TRUE), d) # independent
compare(correlated = fit, cross = fc, independent = fi) # logML, Bayes factors
# maximum likelihood: AIC and BIC
ml <- birt(model, d, control = rtmb_control(method = "aghq"))
mlc <- birt(rtirt_syntax(items, times, cross = "free"), d, control = rtmb_control(method = "aghq"))
compare(correlated = ml, cross = mlc)Marginal likelihoods depend on the priors; report them with the priors.
Instead of normal residuals, shash() gives sinh-arcsinh
residuals (skewness, tail weight); V(y) ~ z in the syntax
moderates a log scale. quantiles(fit, p) then gives the
effects on every conditional quantile from one fit, with simultaneous
bands over p; quantile_test() tests whether an
effect changes with p.
reg <- rtirt_syntax(items, times, structure = "path", cov = c("x1", "x2")) # speed ~ ability + x1 + x2
fs <- birt(reg, d, family = list(speed = shash()), control = rtmb_control(k_hyper = 13))
m5 <- paste(rtirt_syntax(items, times, cross = "free"),
sprintf("V(%s) ~ ability", paste(times, collapse = " + ")), sep = "\n")
f5 <- birt(m5, d)
quantiles(f5, p = c(0.1, 0.5, 0.9))
quantile_test(f5, p = c(0.1, 0.5, 0.9))
cond_reliability(f5) # reliability of ability given its value: accuracy, RT, both
rt_information(f5) # where the RT information lies; what a time limit keepsIf ELGM warns that the posterior is too wide for the outer grid along
a hyperparameter (more mass on its two outermost nodes than a Gaussian
posterior 1.5 times wider would put there, and at least 2.5%), check
whether that parameter is weakly determined and increase
rtmb_control(k_hyper = ).
score_test(ml, d$x1 > 0, by_item = TRUE) # measurement invariance from one fit
score_test(ml, rowSums(d[times])) # along the total log time
qs <- quantile_score(birt(m5, d, control = rtmb_control(method = "aghq")), p = 0.9)
qs # influence functions, robust SEs
score_test(qs, d$x2)
ipcr(ml, d[c("x1", "x2")]) # which parameters change with which covariates
rtmb_code(ml) # a stand-alone RTMB scriptipcr() regresses each person’s contribution to every
estimate on the covariates (individual parameter contribution
regression; Arnold et al., 2020), so one call screens all parameters
against several covariates.
dif_tree(model, data, partition) (package partykit) is
deprecated.
With ordered = TRUE the indicators are ordinal (graded
response model) and person-level moderators enter as moderation
statements on conditional moments, as in MNLFA:
E(y1 + y2) ~ z shifts the thresholds (uniform DIF),
E(y1) ~ z:f moderates the discrimination (nonuniform DIF),
E(f) ~ z and V(f) ~ z the latent mean and
variance. hgrm() writes this syntax from a few options;
both fit by ELGM by default.
dh <- sim_hgrm(N = 600, J = 8, seed = 1)
dh$logT <- as.numeric(scale(dh$logT))
m <- 'SA =~ y1 + y2 + y3 + y4 + y5 + y6 + y7 + y8
E(y1 + y2 + y3 + y4 + y5 + y6 + y7 + y8) ~ pa*logT:SA
E(y1 + y2) ~ logT
E(SA) ~ logT'
f_ord <- birt(m, dh, ordered = TRUE)
summary(f_ord)
ordinal_information(f_ord)
h0 <- hgrm("SA =~ y1 + y2 + y3 + y4 + y5 + y6 + y7 + y8", dh, moderators = "logT",
control = rtmb_control(method = "aghq"))
score_test(h0, dh$logT, by_item = TRUE) # DIF screen, item by itemfit_ssp <- birt(rtirt_syntax(items, times, cross = "ssp"), d, engine = "jags")
summary(fit_ssp)$selection # spike-and-slab: inclusion probabilities, Bayes factors
compare(M2b = fit_ssp, M1 = birt(model, d, engine = "jags")) # LOOIC, ELPD
ppc(fit_ssp) # posterior predictive checks incl. conditional dependence
jags_code(fit_ssp) # the JAGS model
q <- birt(reg, d, family = list(speed = ald(c(0.1, 0.5, 0.9))), engine = "jags")log_rt = TRUE and for data that are
already logged; do not compare it with models for times in seconds.fit_indices = FALSE and
add_fit_indices(fit, n_draws = 200) later. Posterior
intervals under ALD working likelihoods can be too narrow (Yang, Wang
& He, 2016).Arnold, M., Oberski, D. L., Brandmaier, A. M., & Voelkle, M. C. (2020). Identifying heterogeneity in dynamic panel models with individual parameter contribution regression. Structural Equation Modeling, 27, 613-628. https://doi.org/10.1080/10705511.2019.1667240
Bolsinova, M., & Tijmstra, J. (2018). Improving precision of ability estimation: Getting more from response times. British Journal of Mathematical and Statistical Psychology, 71, 13-38.
Stringer, A., Brown, P., & Stafford, J. (2023). Fast, scalable approximations to posterior distributions in extended latent Gaussian models. Journal of Computational and Graphical Statistics, 32, 84-98. https://doi.org/10.1080/10618600.2022.2099403
van der Linden, W. J. (2007). A hierarchical framework for modeling speed and accuracy on test items. Psychometrika, 72, 287-308.
Yang, Y., Wang, H. J., & He, X. (2016). Posterior inference in Bayesian quantile regression with asymmetric Laplace likelihood. International Statistical Review, 84, 327-344.