| Title: | BIvariate Response Time Modeling with 'Rcpp' |
|---|---|
| Description: | Item response and response time models fitted from their full conditionals. A built-in model is chosen with ecm(data, model = ...) or gibbs(data, model = ...): the two-parameter logistic model, alone or with log-normal response times (van der Linden, 2007, <doi:10.1007/s11336-006-1478-z>), latent regressions of ability and speed, cross-relations between ability and the log response times, and their quantile (asymmetric Laplace) versions. The main estimator is ECM (ecm(); Meng and Rubin, 1993, <doi:10.1093/biomet/80.2.267>): one conditional maximization per block of parameters, Polya-Gamma bounds for logistic likelihoods (Polson, Scott and Windle, 2013, <doi:10.1080/01621459.2013.829001>), adaptive or Bock-Aitkin quadrature (Bock and Aitkin, 1981, <doi:10.1007/BF02293801>) with normal-linear latent variables integrated exactly, and Louis' standard errors; quantile models maximize the asymmetric Laplace working likelihood, with sandwich standard errors. The same steps drive Gibbs sampling (gibbs(); conjugate updates and Metropolis-adjusted Langevin steps), with samplers in C++ ('RcppArmadillo') that port the 'Julia' package 'ExtendedRtIrtModeling.jl' (its function names are kept as aliases). Both estimators have a check: check_ecm(), and check_sampler() with the Geweke (2004, <doi:10.1198/016214504000001132>) test and simulation-based calibration. equations() and algorithm() write out the model and every step, before or after fitting. |
| Authors: | Jie-Wen Tsai [aut, cre] |
| Maintainer: | Jie-Wen Tsai <[email protected]> |
| License: | GPL-3 |
| Version: | 0.3.0 |
| Built: | 2026-10-31 18:57:12 UTC |
| Source: | https://github.com/jiewenTsai/birtRcpp |
Built-in models of response accuracy and response times (ports of ExtendedRtIrtModeling.jl),
named by model and fitted with ecm() (the main estimator) or gibbs(); algorithm() and
equations() show the steps and the model, check_ecm() and check_sampler() check them:
dat <- input_data(resp = items, time = times, cov = covs, id = "IDSTUD", data = raw) fit_ecm <- ecm(dat, model = "cross") fit <- gibbs(dat, model = "cross", init = fit_ecm, n_iter = 10000, n_chain = 3, seed = 1) summary(fit) estimates(fit) scores(fit)
To start, these are all you need (the other help pages are for diagnostics, simulation, the building blocks of the samplers, the Julia names and deprecated functions):
| step | function |
| data | input_data() |
| fit | ecm() (seconds; estimates, standard errors, AIC, BIC), gibbs() (the posterior) |
| results | summary(), estimates(), scores(), reliability() |
| compare models | anova() (anova.rtirt()), fit_indices() |
| check a Gibbs fit | convergence(), plot(fit, "trace") (plot.rtirt()) |
| settings | ecm_control(), gibbs_control(), rtirt_priors() |
| write up | equations() (the model, also in LaTeX)
|
The tutorial (Traditional Chinese) explains every line: https://github.com/jiewenTsai/birtExamples/blob/main/birtRcpp/tutorial/birtRcpp_intro.md.
The declarative block_model(), vi() and mml() are deprecated (they still work).
The accessors have the names of the birt package. Julia names are aliases (julia_aliases).
The generics shared with birt (estimates, scores, reliability, fit_indices, equations,
convergence, score_test, algorithm) dispatch to the methods of both packages whichever was attached last: when
both are loaded, each package's methods are registered in the other's generics.
Maintainer: Jie-Wen Tsai [email protected]
Useful links:
Report bugs at https://github.com/jiewenTsai/birtRcpp/issues
Lists the steps of the algorithm behind a fit: for gibbs() every full conditional
distribution in the order of one iteration (with collapse = TRUE the partially collapsed
version), for ecm() the E- and M-steps. The prior constants of the fit are substituted into
the formulas, and each step names the building block that draws it (blocks), so a step can
be run, replaced or checked from R. Works before estimation too: give the data and the model as
in ecm() and gibbs().
algorithm(object, ...) ## S3 method for class 'rtirt_qset' algorithm( object, method = NULL, collapse = NULL, format = c("unicode", "latex"), detail = TRUE, ... ) ## S3 method for class 'rtirt' algorithm( object, method = NULL, collapse = NULL, format = c("unicode", "latex"), detail = TRUE, ... ) ## S3 method for class 'rtirt_algorithm' print(x, ...) ## S3 method for class 'input_data' algorithm( object, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, intercept = FALSE, method = NULL, collapse = NULL, format = c("unicode", "latex"), detail = TRUE, ... )algorithm(object, ...) ## S3 method for class 'rtirt_qset' algorithm( object, method = NULL, collapse = NULL, format = c("unicode", "latex"), detail = TRUE, ... ) ## S3 method for class 'rtirt' algorithm( object, method = NULL, collapse = NULL, format = c("unicode", "latex"), detail = TRUE, ... ) ## S3 method for class 'rtirt_algorithm' print(x, ...) ## S3 method for class 'input_data' algorithm( object, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, intercept = FALSE, method = NULL, collapse = NULL, format = c("unicode", "latex"), detail = TRUE, ... )
object |
A fit of |
... |
Unused. |
method |
|
collapse |
For |
format |
|
detail |
|
x |
An |
model, itemtype, speed_var, quantile, cov, intercept
|
For |
An rtirt_algorithm object (printed); as.character() gives the lines.
equations() for the model, blocks for the steps as R functions.
cond <- set_cond(n_subj = 100, n_item = 5) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") algorithm(dat, model = "cross") # Gibbs full conditionals algorithm(dat, model = "cross", collapse = TRUE) # partially collapsed Gibbs algorithm(dat, model = "cross", method = "ecm") # ECM steps algorithm(dat, model = "cross", format = "latex") algorithm(dat, model = "cross", detail = FALSE) # the steps onlycond <- set_cond(n_subj = 100, n_item = 5) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") algorithm(dat, model = "cross") # Gibbs full conditionals algorithm(dat, model = "cross", collapse = TRUE) # partially collapsed Gibbs algorithm(dat, model = "cross", method = "ecm") # ECM steps algorithm(dat, model = "cross", format = "latex") algorithm(dat, model = "cross", detail = FALSE) # the steps only
Fits a block_model() by coordinate ascent variational inference with the same steps as
gibbs() and ecm(). The three engines use the same full conditionals:
## S3 method for class 'block_model_vi' algorithm(object, format = c("unicode", "latex"), ...) ## S3 method for class 'block_model' vi( model, steps = NULL, latent = NULL, nodes = NULL, init = NULL, cycles = 6, max_iter = 1000, tol = 1e-09, verbose = FALSE, quadrature = c("adaptive", "ba81"), ... ) ## S3 method for class 'block_model_vi' print(x, ...)## S3 method for class 'block_model_vi' algorithm(object, format = c("unicode", "latex"), ...) ## S3 method for class 'block_model' vi( model, steps = NULL, latent = NULL, nodes = NULL, init = NULL, cycles = 6, max_iter = 1000, tol = 1e-09, verbose = FALSE, quadrature = c("adaptive", "ba81"), ... ) ## S3 method for class 'block_model_vi' print(x, ...)
format |
|
... |
Unused. |
model |
|
steps |
Steps, as for |
latent |
Parameters integrated out in the E-step (one to three |
nodes |
Quadrature points per latent variable (default 21, 11 or 7 for one, two or three); one
number, or one per latent variable (named, e.g. |
init |
Optional starting values (named list). |
cycles |
Maximum number of recentring rounds of the quadrature. |
max_iter, tol
|
Passed to |
verbose |
Print the ELBO every 25 sweeps. |
quadrature |
|
x, object
|
A fit of |
| step | Gibbs (gibbs()) |
ECM (ecm()) |
CAVI (vi()) |
| logistic part | draw |
at |
, |
| person parameters | draw | integrate (adaptive quadrature) | free-form on the same grid |
step_gibbs(x), normal prior |
draw |
move to |
(truncated if bounded) |
step_gibbs(v), variance |
draw IG | mode | = IG with expected squared residuals
|
Expectations of products such as are exact: the
expressions are multilinear, and under the factorized q every product is a product of first and
second moments. The objective is the evidence lower bound (ELBO); every update raises it. A latent
variable integrated exactly (normal and linear, e.g. speed) uses the steps of ecm() when its parts are
affine in all latent variables: q(u | grid) from a quadratic per person, and in the updates each person's
mean and covariance of the latent variables through 2L points (exact for these quadratic parts; Julier &
Uhlmann, 2004).
Known behavior of mean-field VB, also here: posterior means are close to the posterior, but posterior
SDs of the item parameters are too small (the correlations between parameters are ignored); use
gibbs() for intervals. step_mala() has no CAVI update (the parameter needs a
conditionally conjugate full conditional) and step_shift() is skipped.
A list of class block_model_vi: summary (posterior means and SDs of q), latent (posterior
means and SDs of the person parameters), q, elbo (trace, one value per sweep), elbo_drop (the
largest relative drop between sweeps of one round: 0 when every update is right), iterations, converged, time,
model, steps; algorithm(fit) lists the updates.
Blei, D. M., Kucukelbir, A., & McAuliffe, J. D. (2017). Variational inference: A review for statisticians. Journal of the American Statistical Association, 112, 859–877.
Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of item parameters: Application of an EM algorithm. Psychometrika, 46(4), 443–459. doi:10.1007/BF02293801
Durante, D., & Rigon, T. (2019). Conditionally conjugate mean-field variational Bayes for logistic models. Statistical Science, 34, 472–485.
Julier, S. J., & Uhlmann, J. K. (2004). Unscented filtering and nonlinear estimation. Proceedings of the IEEE, 92(3), 401–422. doi:10.1109/JPROC.2003.823141
set.seed(1); N <- 300; K <- 6 th <- rnorm(N); a0 <- runif(K, 0.8, 2); b0 <- seq(-1.5, 1.5, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(th, a0), 2, a0 * b0))), N) m <- block_model(Y = Y, theta[person] ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), Y ~ bernoulli_logit(a * (theta - b))) fit <- vi(m, list(step_gibbs(theta, a, b))) fit algorithm(fit)set.seed(1); N <- 300; K <- 6 th <- rnorm(N); a0 <- runif(K, 0.8, 2); b0 <- seq(-1.5, 1.5, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(th, a0), 2, a0 * b0))), N) m <- block_model(Y = Y, theta[person] ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), Y ~ bernoulli_logit(a * (theta - b))) fit <- vi(m, list(step_gibbs(theta, a, b))) fit algorithm(fit)
anova(fit0, fit1, ...) compares ecm() fits of the same responses and response times,
as anova() of lm(): the number of parameters, log-likelihood, AIC and BIC of each fit,
and the likelihood ratio test of each fit against the one in the row above
(Chisq = 2 * (logLik - logLik above), Df the difference in the number of parameters).
The rows are ordered by the number of parameters. The test assumes that each fit is nested
in the next, e.g. "null" in "latreg" (the regression coefficients 0) or in "cross"
(equal rho), itemtype = "1pl" in "2pl", speed_var = "fixed" in "free"; AIC and BIC
also compare fits that are not nested. A fit with more parameters but a lower
log-likelihood gives a warning (not nested, or not converged).
## S3 method for class 'rtirt' anova(object, ...) ## S3 method for class 'rtirt_anova' print(x, digits = 3, signif.stars = getOption("show.signif.stars", TRUE), ...)## S3 method for class 'rtirt' anova(object, ...) ## S3 method for class 'rtirt_anova' print(x, digits = 3, signif.stars = getOption("show.signif.stars", TRUE), ...)
object, ...
|
|
x |
The result of |
digits |
Decimals (log-likelihood, AIC, BIC, Chisq, p-value; |
signif.stars |
Significance stars after the p-value, as in |
For gibbs() fits the table has the criteria of fit_indices() (DIC, WAIC, LOOIC with its
standard error), smaller is better, and no test. Fits with prior = "map" (no likelihood),
quantile fits (an ALD working likelihood) and a mix of ECM and Gibbs fits are refused.
A data frame of class rtirt_anova (and anova), printed with a heading that names
the fits.
cond <- set_cond(n_subj = 300, n_item = 8, n_feat = 1) dat <- sim_data(cond, sim_para(cond, "latreg"), "latreg") fit0 <- ecm(dat, model = "null") fit1 <- ecm(dat, model = "latreg") anova(fit0, fit1)cond <- set_cond(n_subj = 300, n_item = 8, n_feat = 1) dat <- sim_data(cond, sim_para(cond, "latreg"), "latreg") fit0 <- ecm(dat, model = "null") fit1 <- ecm(dat, model = "latreg") anova(fit0, fit1)
Deprecated: block_model() and its steps still work (with a warning once per session) but
get no new features. Use the built-in models, ecm(data, model = ...) and
gibbs(data, model = ...).
block_model(...) ## S3 method for class 'block_model' print(x, ...) ## S3 method for class 'block_model' equations(object, format = c("unicode", "latex"), ...)block_model(...) ## S3 method for class 'block_model' print(x, ...) ## S3 method for class 'block_model' equations(object, format = c("unicode", "latex"), ...)
... |
Data (named arguments) and declarations (formulas). |
object, x
|
A declared model. |
format |
|
block_model() declares a model once, the way PyMC does: the data, and one line
name ~ distribution(...) per parameter and per observed matrix. Samplers are then stacked as
steps that only name the parameters they update (step_gibbs(), step_mala(), step_shift(),
run by gibbs()). Each step derives what it needs from the model, so
the steps always target the same posterior (a prior cannot be forgotten in one step);
the Pólya–Gamma variables of a logistic part are hidden: they are drawn
whenever a Gibbs step needs them and are fresh (drawn once per iteration and again after a
MALA step has moved a parameter of that part);
positive parameters are moved on the log scale by MALA, with the Jacobian added;
dimensions are declared once and checked.
Data: named arguments. Matrices are persons x items (all the same size); vectors have one
value per person (length N) or per item (length K); missing responses or log times are NA.
Parameters: name[person] ~ ..., name[item] ~ ... or name ~ ... (a single value).
Distributions
normal(mean, sd, lower = -Inf, upper = Inf): mean and sd may use parameters and data,
e.g. zeta[person] ~ normal(beta * theta, sqrt(tau2)). For an observed matrix the mean is
written with the person and item parameters as they are; they are expanded to the matrix.
bernoulli_logit(eta): logistic responses, e.g. Y ~ bernoulli_logit(a * (theta - b)).
half_t(df, scale) and inv_gamma(shape, scale): priors of a variance v that is used as
sd = sqrt(v); half_t() is the prior of the SD (drawn through the
inverse-gamma auxiliary variable of Huang & Wand, 2013).
Expressions may use + - * / ^, exp(), log() and sqrt(). A line name <- expression
defines a name for later lines, as in rethinking::ulam(), e.g. eta <- a * (theta - b) and then
Y ~ bernoulli_logit(eta); definitions are substituted (they are not parameters).
An object of class block_model; equations() prints the model.
gibbs() and the steps; run_sampler() for samplers written by hand.
set.seed(1); N <- 300; K <- 6 th <- rnorm(N); a0 <- runif(K, 0.8, 2); b0 <- seq(-1.5, 1.5, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(th, a0), 2, a0 * b0))), N) m <- block_model(Y = Y, theta[person] ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), Y ~ bernoulli_logit(a * (theta - b))) m fit <- gibbs(m, list(step_mala(theta), step_gibbs(a, b)), n_iter = 600) summary(fit)[1:4, ] algorithm(fit)set.seed(1); N <- 300; K <- 6 th <- rnorm(N); a0 <- runif(K, 0.8, 2); b0 <- seq(-1.5, 1.5, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(th, a0), 2, a0 * b0))), N) m <- block_model(Y = Y, theta[person] ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), Y ~ bernoulli_logit(a * (theta - b))) m fit <- gibbs(m, list(step_mala(theta), step_gibbs(a, b)), n_iter = 600) summary(fit)[1:4, ] algorithm(fit)
Steps name the parameters they update; gibbs() runs them in the order given, once per
iteration. What a step does is derived from the block_model():
step_gibbs(..., cut = character()) step_mala(..., step = 0.1) step_shift(...)step_gibbs(..., cut = character()) step_mala(..., step = 0.1) step_shift(...)
... |
Parameter names (unquoted); for |
cut |
Names of observed data whose parts are left out of this Gibbs step (a cut). |
step |
Starting step size of MALA. |
step_gibbs(...): draws each parameter from its full conditional, one after the other. A
parameter with a normal prior gets its normal full conditional: the precision and linear term
are added up over every part of the model that uses it (normal parts directly; a logistic part
through its Pólya–Gamma variables, which are drawn automatically when needed). A variance with
a half_t() or inv_gamma() prior gets its conjugate update. The model must be linear in the
parameter (otherwise use step_mala()).
With cut = "logT" the parts of the data named there do not inform this step (a cut; Plummer,
2015): e.g. step_gibbs(theta, cut = "logT") draws the ability from the responses only, while the
response-time parameters still use the ability. The chain then targets the cut distribution, not
the posterior (so the Geweke check of check_sampler() does not apply), and ecm() and
vi() refuse a cut.
step_mala(..., step = 0.1): one MALA step (Roberts & Tweedie, 1996) per person or item, for
the named parameters jointly (they must have the same dimension); the logistic parts use their
exact likelihood (no Pólya–Gamma variables). A parameter with a lower bound is moved on the log
scale with the Jacobian; the step sizes adapt during burn-in.
step_shift(...): moves several parameters together along a direction, e.g.
step_shift(theta = 1, b = 1) (theta + c and b + c) or step_shift(rho = -1, zeta = theta),
with c drawn from its exact conditional (a generalized Gibbs step; Liu & Sabatti, 2000). The
logistic parts must not change along the direction, and the normal parts must be linear in c.
It speeds up directions that only the prior identifies.
A step (a list) for gibbs().
Each draw_*() function draws one block from its full conditional and is named after the C++
function of the samplers that it calls (irt_part() draws nothing: it returns the accuracy part
of the conditional of ability). algorithm() names the function of every step of gibbs().
draw_gauss(prec, num) draw_invgamma(shape, scale) draw_tnorm(mean, sd, lower = -Inf, upper = Inf) draw_omega(a, b, theta) draw_probit_z(Y, eta) draw_pg(eta) irt_part(Y, omega, a, b) draw_items_pg( Y, omega, theta, a, priors = rtirt_priors(), one_pl = FALSE, b_mean = NULL ) draw_items_joint(Y, theta, a, b, priors = rtirt_priors(), b_mean = NULL) draw_lambda(L, m, k1nu, W, mu, sd, lower = 0) draw_sigma2( L, m, lambda, current, priors = rtirt_priors(), nu = NULL, q = NULL ) draw_var(alpha, beta, current, prior = c(df = 3, scale = 1)) draw_nu(r, s, q) draw_theta_collapsed(prec, num, W, y, rho, m0, g, tau) draw_rt_items_collapsed( L, k1nu, W, theta, mz, tau, with_rho, mu, sd, rho_sd, lambda, rho, lower = 0 )draw_gauss(prec, num) draw_invgamma(shape, scale) draw_tnorm(mean, sd, lower = -Inf, upper = Inf) draw_omega(a, b, theta) draw_probit_z(Y, eta) draw_pg(eta) irt_part(Y, omega, a, b) draw_items_pg( Y, omega, theta, a, priors = rtirt_priors(), one_pl = FALSE, b_mean = NULL ) draw_items_joint(Y, theta, a, b, priors = rtirt_priors(), b_mean = NULL) draw_lambda(L, m, k1nu, W, mu, sd, lower = 0) draw_sigma2( L, m, lambda, current, priors = rtirt_priors(), nu = NULL, q = NULL ) draw_var(alpha, beta, current, prior = c(df = 3, scale = 1)) draw_nu(r, s, q) draw_theta_collapsed(prec, num, W, y, rho, m0, g, tau) draw_rt_items_collapsed( L, k1nu, W, theta, mz, tau, with_rho, mu, sd, rho_sd, lambda, rho, lower = 0 )
prec, num, shape, scale, mean, sd, lower, upper
|
Numeric vectors (see Details). |
a, b, theta, lambda, rho, mz, m0, g, tau
|
Numeric vectors. |
Y, L, omega, m, k1nu, W, y, nu, r, s, eta
|
Numeric matrices (persons x items). |
priors |
Priors from |
one_pl |
|
b_mean |
Prior means of b, one per item ( |
mu |
Prior mean (and |
current |
Current values (variances). |
q |
Quantile level of the ALD. |
alpha, beta
|
Exponents of the likelihood of a variance. |
prior |
Prior of a variance: |
with_rho |
Draw rho with lambda. |
rho_sd |
Prior SD of rho. |
The full conditionals used by gibbs(), exposed so that new samplers (other blockings, cut or
semi-modular schemes, nested chains) can be put together in R while every draw still runs in C++.
All of them use R's random number generator, so set.seed() makes a composed sampler
reproducible, and a sampler written with them reproduces gibbs() step by step. Notation as in
the samplers: persons j = 1..N, items i = 1..K, Y the 0/1 responses, L the log response
times, logit P(y = 1) = a (theta - b), log T = lambda - zeta - rho theta + e.
draw_gauss(prec, num): independent draws from N(num / prec, 1 / prec), elementwise (any
conditionally normal scalar: ability, speed, rho_i, a shift).
draw_invgamma(shape, scale), draw_tnorm(mean, sd, lower, upper): inverse gamma and
truncated normal draws, elementwise.
draw_omega(a, b, theta): Pólya–Gamma variables PG(1, a_i (theta_j - b_i)), an N x K matrix.
draw_pg(eta): Pólya–Gamma variables PG(1, eta) for any matrix of linear predictors (models
whose logit is not a (theta - b), e.g. several abilities or covariates in the item response).
draw_probit_z(Y, eta): for a probit response model, the latent normal responses of Albert & Chib
(1993), N(eta, 1) truncated to the positive (y = 1) or negative (y = 0) half line; missing y
(NA) are not truncated. Given them, the probit model is a normal linear model.
irt_part(Y, omega, a, b): the accuracy part of the conditional of
ability given omega, a normal part (precision prec and linear term num, see normal_parts):
draw(irt_part(Y, omega, a, b) + normal_prior(0, 1)).
draw_items_pg(Y, omega, theta, a, priors, one_pl, b_mean): b, then a, given omega (Gibbs);
returns list(a, b).
draw_items_joint(Y, theta, a, b, priors, b_mean): (a, b) jointly with omega integrated out
(Metropolis–Hastings with a Fisher-scoring proposal in (a, -a b)); returns list(a, b) with the
attribute accepted (one logical per item). It mixes much better than draw_items_pg() for very
easy or very difficult items; draw omega anew after it.
The item priors are those of rtirt_priors() (a: truncated normal on a > 0, b: normal);
b_mean gives b a prior mean per item (e.g. W %*% delta in an LLTM with item covariates W),
in place of the mean of priors$b.
draw_lambda(L, m, k1nu, W, mu, sd, lower): time intensities given the RT means
m (N x K, e.g. -zeta - rho theta), the ALD shifts k1nu (zeros for normal times) and the
precisions W (N x K); prior N(mu, sd^2) on (lower, Inf).
draw_sigma2(L, m, lambda, current, priors, nu = NULL, q = NULL): residual variances (normal) or
ALD scales (with the mixing variables nu and the quantile q), from their current values
(a half-t prior draws its inverse-gamma auxiliary variable from them).
draw_var(alpha, beta, current, prior): a variance with likelihood var^-alpha exp(-beta / var)
(n residuals u ~ N(0, var): alpha = n / 2, beta = sum(u^2) / 2) and the prior c(df, scale) (half-t on its square root, two inverse-gamma steps) or c(shape, scale)
(inverse gamma).
draw_nu(r, s, q): ALD mixing variables for residuals r and scales s (matrices).
draw_theta_collapsed(prec, num, W, y, rho, m0, g, tau) and
draw_rt_items_collapsed(L, k1nu, W, theta, mz, tau, with_rho, mu, sd, rho_sd, lambda, rho):
the collapsed steps of gibbs(collapse = TRUE) (speed integrated out).
Missing responses (NA in Y) are skipped by irt_part(), draw_items_pg() and
draw_items_joint(). draw_lambda() and draw_sigma2() need complete log times (they stop on
NA); with missing times use calc_normal_part() with draw() and draw_var(). run_sampler() runs a sampler written from these blocks (chains, burn-in,
storage, summary) and check_sampler() checks it.
These are low-level: arguments are checked only for their dimensions, and the caller is
responsible for a valid sampler (each block must be drawn from its conditional given the current
values of everything else, or from a marginal followed by the integrated variable).
inst/examples/ex07_blocks.R writes the rtirt_cross() sampler with them; ex08_testlet.R,
ex09_mirt_rt.R and ex10_conditional_dependence.R compose models from the literature.
Draws (vectors, matrices or lists, see Details).
set.seed(1) draw_gauss(prec = c(1, 4), num = c(0, 2)) # N(0, 1) and N(0.5, 1/4) theta <- rnorm(200); Y <- matrix(rbinom(600, 1, plogis(theta)), 200) om <- draw_omega(a = rep(1, 3), b = rep(0, 3), theta = theta) str(draw_items_pg(Y, om, theta, a = rep(1, 3), priors = rtirt_priors()))set.seed(1) draw_gauss(prec = c(1, 4), num = c(0, 2)) # N(0, 1) and N(0.5, 1/4) theta <- rnorm(200); Y <- matrix(rbinom(600, 1, plogis(theta)), 200) om <- draw_omega(a = rep(1, 3), b = rep(0, 3), theta = theta) str(draw_items_pg(Y, om, theta, a = rep(1, 3), priors = rtirt_priors()))
The EM counterpart of the Geweke check of check_sampler(). A Gibbs sampler is right when every
step leaves the posterior unchanged; an EM algorithm is right when
check_ecm( model = NULL, steps = NULL, latent = NULL, nodes = NULL, prior = c("map", "ml"), n_start = 10, data = NULL, init = NULL, step = NULL, loglik = NULL, positive = character(), seed = NULL, quadrature = c("adaptive", "ba81") ) ## S3 method for class 'em_check' print(x, ...)check_ecm( model = NULL, steps = NULL, latent = NULL, nodes = NULL, prior = c("map", "ml"), n_start = 10, data = NULL, init = NULL, step = NULL, loglik = NULL, positive = character(), seed = NULL, quadrature = c("adaptive", "ba81") ) ## S3 method for class 'em_check' print(x, ...)
model |
An |
steps, latent, nodes, prior, quadrature
|
As in |
n_start |
Number of starting values. |
data, init, step, loglik, positive
|
A hand-written EM, as in |
seed |
Random seed. |
x |
An |
... |
Unused. |
every step never lowers the objective (from many starting values, not only from the path that EM happened to take);
the E-step agrees with the objective (Fisher's identity: the gradient of the log marginal
likelihood equals the gradient of the expected complete-data log likelihood
at ); and
ECM stops at a maximum (a general optimizer started at the ECM estimate finds nothing better, and the gradient is zero there).
For an ecm() fit of a built-in model (a gibbs() fit or a
model object of the deprecated constructors also works) the three are checked for
the EM of ecm() with its own E- and M-step, from n_start starting values jittered around those
of ecm(): one whole M-step (which updates all blocks) never lowers the objective, the analytic
score (Fisher's identity) equals the numerical gradient of the objective, and the fit is a maximum.
For a quantile model (one level, e.g. check_ecm(ecm(dat, model = "cross", quantile = 0.9)))
the objective is the ALD working log-likelihood, the starts are jittered around the fit, and
prior is not used.
For an EM written by hand, give data, init, step and loglik as for run_em(); checks 1
(from n_start jittered starts) and 3 are done. For a block_model() (deprecated) all three are
checked, with starting values drawn from the declared priors.
A data frame with one row per check (check, worst, limit, passed), class em_check.
# the EM of ecm() for the cross-relation model cond <- set_cond(n_subj = 200, n_item = 5) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross", se = FALSE) check_ecm(fit, prior = "ml", n_start = 3, seed = 1) # an EM written by hand (the 2PL of run_em()) set.seed(1) Y <- matrix(rbinom(300 * 5, 1, plogis(outer(rnorm(300), seq(-1, 1, length.out = 5), "-"))), 300) step <- function(s, d) { e <- estep_irt(d$Y, s$a, s$b) # E-step s[c("a", "b")] <- mstep_items(e, s$a, s$b) # M-step s$.loglik <- e$loglik s } loglik <- function(s, d) estep_irt(d$Y, s$a, s$b)$loglik check_ecm(data = list(Y = Y), init = list(a = rep(1, 5), b = rep(0, 5)), step = step, loglik = loglik, positive = "a", n_start = 3, seed = 1)# the EM of ecm() for the cross-relation model cond <- set_cond(n_subj = 200, n_item = 5) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross", se = FALSE) check_ecm(fit, prior = "ml", n_start = 3, seed = 1) # an EM written by hand (the 2PL of run_em()) set.seed(1) Y <- matrix(rbinom(300 * 5, 1, plogis(outer(rnorm(300), seq(-1, 1, length.out = 5), "-"))), 300) step <- function(s, d) { e <- estep_irt(d$Y, s$a, s$b) # E-step s[c("a", "b")] <- mstep_items(e, s$a, s$b) # M-step s$.loglik <- e$loglik s } loglik <- function(s, d) estep_irt(d$Y, s$a, s$b)$loglik check_ecm(data = list(Y = Y), init = list(a = rep(1, 5), b = rep(0, 5)), step = step, loglik = loglik, positive = "a", n_start = 3, seed = 1)
Tests whether the sampler of gibbs() for a model draws from the correct posterior, with two
general methods that need only the prior and the likelihood:
check_sampler( model = NULL, method = c("geweke", "sbc"), n_persons = 20, n_iter = NULL, n_rep = 200, n_draws = 99, collapse = NULL, itemtype = NULL, priors = rtirt_priors(), sampler_priors = priors, probs = c(0.1, 0.25, 0.5, 0.75, 0.9), n_prior = 1e+05, seed = NULL, steps = NULL, monitor = NULL, step = NULL, prior = NULL, simulate = NULL, ... ) ## S3 method for class 'rtirt_check' print(x, n = 10, ...)check_sampler( model = NULL, method = c("geweke", "sbc"), n_persons = 20, n_iter = NULL, n_rep = 200, n_draws = 99, collapse = NULL, itemtype = NULL, priors = rtirt_priors(), sampler_priors = priors, probs = c(0.1, 0.25, 0.5, 0.75, 0.9), n_prior = 1e+05, seed = NULL, steps = NULL, monitor = NULL, step = NULL, prior = NULL, simulate = NULL, ... ) ## S3 method for class 'rtirt_check' print(x, n = 10, ...)
model |
An |
method |
|
n_persons |
Persons per simulated data set. |
n_iter |
Geweke: iterations of the successive-conditional chain (the first 10% are dropped; default 2e5, about 15 s for three items). SBC: iterations per fit (the first half is burn-in; default 1e4). |
n_rep |
SBC: number of simulated data sets. |
n_draws |
SBC: thinned posterior draws per fit (ranks 0 to |
collapse, itemtype
|
Sampler settings, as in |
priors |
Priors of the simulation and the sampler, from |
sampler_priors |
Priors given to the sampler (default |
probs |
Geweke: probabilities of the prior quantiles that are compared. |
n_prior |
Geweke: prior draws for the reference distribution. |
seed |
Random seed. |
steps |
For a |
monitor |
For a |
step, prior, simulate
|
A sampler written by the user: |
... |
Unused. |
x |
An |
n |
Rows printed. |
method = "geweke" (Geweke, 2004): the successive-conditional simulator. One sweep of the
sampler given the data alternates with new person values and a new data set simulated given the
current parameters. (Simulating new data from the old person values is also exact, but the
persons are then held by their own data and the chain crawls: a false alarm in practice.) If the sampler is correct, the chain of the parameters has the prior as
its distribution. For every parameter and each probability in probs, the share of the chain
below the prior quantile is compared with the probability (z-test with the effective sample
size of the chain); p-values are Holm-adjusted over all comparisons. Cheap: one sweep per
iteration on a small data set.
method = "sbc", simulation-based calibration (Cook, Gelman & Rubin, 2006; Talts et al.,
2018; Modrák et al., 2025): n_rep times, draw the parameters from the prior, simulate
data, run the sampler and record the rank of the true value among n_draws thinned posterior
draws. The ranks are uniform for a correct sampler; a chi-square test over 10 bins per parameter
(Holm-adjusted).
Both use the model only as a template: its type, quantile, settings, number of items and
(the first n_persons rows of) its covariates. Data are simulated, so a small n_persons keeps
the posterior wide and the check fast. The priors must be proper and must not depend on the data
(rtirt_priors_julia() centres lambda on the data and cannot be used).
sampler_priors lets the sampler use other priors than the simulation: a check of the check,
which must then report a failure.
Chain length matters for both. The successive-conditional chain must travel over the whole
prior, and its parameters move by about one posterior width per iteration, so parameters with wide priors
(lambda ~ N(0, 10^2)) need long chains; the print method warns when an effective sample size is
small. In SBC the posterior draws of each fit must be close to independent after thinning: with
short fits a slowly mixing parameter (such as rho) piles its ranks up at one end, which looks
like an error of the sampler. Both effects disappear with longer chains, a real error does not.
Samplers written by the user from the building blocks (run_sampler()'s step()) are checked
with step, prior, simulate and monitor (Geweke test): prior() returns a state drawn from
the prior (all parameters and the person parameters), simulate(s) the data list given the
state, and step(s, data) is the sampler. Keep the model small (10-20 persons, a few items) and
the priors proper and not too wide, so that the chain mixes. The test finds wrong full
conditionals and forgotten or wrong prior terms, which summaries of a fit cannot show; its power
is that of a test on a small model (an error that moves the posterior by a fraction of a
posterior SD can pass), so passing is evidence, not proof.
This checks the code of the sampler, not the convergence of your own analysis: for that use
convergence() and plot(fit, "trace").
A data frame of class rtirt_check with one row per comparison (Geweke: parameter,
probability, share in the chain, effective sample size, z, p-value, Holm-adjusted p-value; SBC:
parameter, chi-square statistic, p-value, Holm-adjusted p-value) and attributes method,
passed (all Holm-adjusted p-values above 0.01), draws (Geweke chain) or ranks (SBC).
Cook, S. R., Gelman, A., & Rubin, D. B. (2006). Validation of software for Bayesian models using posterior quantiles. Journal of Computational and Graphical Statistics, 15, 675-692.
Geweke, J. (2004). Getting it right: Joint distribution tests of posterior simulators. Journal of the American Statistical Association, 99, 799-804.
Modrák, M., Moon, A. H., Kim, S., Bürkner, P., Huurre, N., Faltejsková, K., Gelman, A., & Vehtari, A. (2025). Simulation-based calibration checking for Bayesian computation: The choice of test quantities shapes sensitivity. Bayesian Analysis, 20(2).
Talts, S., Betancourt, M., Simpson, D., Vehtari, A., & Gelman, A. (2018). Validating Bayesian inference algorithms with simulation-based calibration. arXiv:1804.06788.
cond <- set_cond(n_subj = 100, n_item = 3) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross", se = FALSE) # the model to check (its fit is not used) narrow <- rtirt_priors(lambda = c(mean = 0, sd = 1)) # any proper prior; narrow mixes fast check_sampler(fit, n_iter = 1e5, priors = narrow, seed = 1) # Geweke check_sampler(fit, method = "sbc", n_rep = 100, priors = narrow, seed = 1) # a wrong prior in the sampler is detected wrong <- rtirt_priors(lambda = c(mean = 0, sd = 1), a = c(mean = 1, sd = 0.3)) check_sampler(fit, n_iter = 2e4, priors = narrow, sampler_priors = wrong, seed = 1)cond <- set_cond(n_subj = 100, n_item = 3) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross", se = FALSE) # the model to check (its fit is not used) narrow <- rtirt_priors(lambda = c(mean = 0, sd = 1)) # any proper prior; narrow mixes fast check_sampler(fit, n_iter = 1e5, priors = narrow, seed = 1) # Geweke check_sampler(fit, method = "sbc", n_rep = 100, priors = narrow, seed = 1) # a wrong prior in the sampler is detected wrong <- rtirt_priors(lambda = c(mean = 0, sd = 1), a = c(mean = 1, sd = 0.3)) check_sampler(fit, n_iter = 2e4, priors = narrow, sampler_priors = wrong, seed = 1)
Deprecated with vi().
check_vi( model, steps = NULL, latent = NULL, nodes = NULL, n_start = 5, sweeps = 20, seed = NULL, quadrature = c("adaptive", "ba81") )check_vi( model, steps = NULL, latent = NULL, nodes = NULL, n_start = 5, sweeps = 20, seed = NULL, quadrature = c("adaptive", "ba81") )
model |
|
steps |
Steps, as for |
latent |
Parameters integrated out in the E-step (one to three |
nodes |
Quadrature points per latent variable (default 21, 11 or 7 for one, two or three); one
number, or one per latent variable (named, e.g. |
n_start |
Number of starting values. |
sweeps |
Sweeps per start in check 1. |
seed |
Random seed. |
quadrature |
|
The CAVI counterpart of check_ecm() and the Geweke check: coordinate ascent is right when
the ELBO never decreases from one sweep to the next (from n_start starting values drawn
from the declared priors, on a fixed grid); and
the fit is a maximum of the ELBO: moving the mean of any q (normal factors) a little up or down lowers it.
A data frame with one row per check (class em_check, printed like check_ecm()).
The Lagrange multiplier (score) test of van der Linden & Glas (2010): under conditional
independence the distribution of the log time on item i does not depend on whether the response is
correct, given ability and speed. The alternative shifts the log times of correct responses,
, and the
test is of from the maximum likelihood fit of the model without the shift
(ecm()). The score of for person j is (Fisher identity). The information
is the observed information of the model extended by the shifts (central differences of the
analytic score, as for the standard errors of ecm()) with the parameters of the model projected
out, so the statistic is chi-square(1) per item, and chi-square(K) for all items together. (The
outer product of the casewise scores is simpler but rejects too often: about .065 per item and
.10 overall at the 5% level with 500 persons and 10 items.)
A positive delta means that correct responses took longer than incorrect ones.
ci_test(object, ...)ci_test(object, ...)
object |
A model fitted by |
... |
Unused. |
A data frame of class rtirt_ci_test per item: item, delta (one-step estimate of the
shift), statistic, p.value, p.holm; attribute overall (joint test of all items).
van der Linden, W. J., & Glas, C. A. W. (2010). Statistical tests of conditional independence between responses and/or response times on test items. Psychometrika, 75, 120-139.
cond <- set_cond(n_subj = 500, n_item = 8) d <- sim_data(cond, sim_para(cond, "cross"), "cross") ci_test(ecm(d, model = "null", se = FALSE))cond <- set_cond(n_subj = 500, n_item = 8) d <- sim_data(cond, sim_para(cond, "cross"), "cross") ci_test(ecm(d, model = "null", se = FALSE))
As comparePara() in Julia (for simulations with true_para): bias, RMSE and correlation
of the posterior means (or of the ECM estimates) for one parameter block.
compare_para(object, par = "a")compare_para(object, par = "a")
object |
A sampled model with |
par |
Parameter name, e.g. |
General steps that come back in many samplers written from the blocks (blocks); with them the
samplers of the examples ex08-ex12 (testlet, multidimensional, conditional dependence, LNIRT,
variable speed) are a few lines per step. Persons j are the rows and items i the columns of the
matrices.
calc_normal_part(x, z, w = 1, rest = 0, by = c("item", "person")) draw_mvn(prec, num, lower = -Inf, upper = Inf, max_tries = 100) draw_items_mh( Y, theta, a, b, prior_mean, prior_prec, link = c("logit", "probit"), form = c("difficulty", "intercept") ) draw_invwishart(df, scale) draw_mean_mvn(X, Sigma, mean0, cov0) draw_cor(x1, x2, cor, steps = 5, step = 0.05, method = c("rw", "mala")) calc_person_fit(L, fitted, sigma2)calc_normal_part(x, z, w = 1, rest = 0, by = c("item", "person")) draw_mvn(prec, num, lower = -Inf, upper = Inf, max_tries = 100) draw_items_mh( Y, theta, a, b, prior_mean, prior_prec, link = c("logit", "probit"), form = c("difficulty", "intercept") ) draw_invwishart(df, scale) draw_mean_mvn(X, Sigma, mean0, cov0) draw_cor(x1, x2, cor, steps = 5, step = 0.05, method = c("rw", "mala")) calc_person_fit(L, fitted, sigma2)
x, z, w, rest
|
Numbers or N x K matrices (persons x items). |
by |
|
prec, num
|
Precision matrix and linear term (or an array of precisions and a matrix of linear terms, one slice / column per draw). |
lower, upper
|
Bounds of the truncation box (recycled to the dimension). |
max_tries |
Rejection attempts per draw. |
Y |
Responses (0/1, NA for missing), N x K. |
theta |
Abilities. |
a, b
|
Current item parameters. |
prior_mean, prior_prec
|
Prior mean (K x 2 or length 2) and prior precision (2 x 2) of (a, b). |
link |
|
form |
|
df, scale
|
Degrees of freedom and scale matrix. |
X |
Matrix whose rows share the mean. |
Sigma |
Covariance of the rows. |
mean0, cov0
|
Prior mean and covariance of the common mean. |
x1, x2
|
The two variables (standardized by the model). |
cor |
Current correlation. |
steps, step
|
Number and size of the Metropolis steps. |
method |
|
L, fitted, sigma2
|
Log times, their fitted means (N x K) and the residual variances (per item). |
calc_normal_part(x, z, w, rest, by): the part of a full conditional that comes from normal
(pseudo-)observations , , for a parameter of each item (by = "item", sums over persons) or of
each person (by = "person", sums over items): precision and linear term
. Add the prior and draw with draw_gauss() or draw_tnorm().
Pólya–Gamma: z = (Y - 1/2) / omega, w = omega; probit: z from draw_probit_z(), w = 1;
log times: z = log t, w = 1 / sigma2. Missing z (NA) are skipped.
draw_mvn(prec, num, lower, upper): multivariate normal from the
precision prec (Q) and linear term num (h); prec can be an array p x p x n and num a
p x n matrix for n independent draws (one per item or person), optionally truncated to the box
[lower, upper] (rejection; a slice that fails is NA).
draw_items_mh(Y, theta, a, b, prior_mean, prior_prec, link, form): item parameters (a, b) with
the latent responses integrated out, Metropolis–Hastings with a Fisher-scoring proposal (the
cure for the slow mixing of easy and difficult items under data augmentation); logit or probit,
form = "difficulty" for a (theta - b) or "intercept" for a theta - b; prior
N((a, b); prior_mean (a row per item or one row), prior_prec^-1). Returns list(a, b) with the
attribute accepted (as draw_items_joint()); draw the latent responses anew afterwards.
draw_invwishart(df, scale): inverse Wishart.
draw_mean_mvn(X, Sigma, mean0, cov0): the common mean of the rows of X ~ N(mu, Sigma) with
prior mu ~ N(mean0, cov0) (hyperparameters of item- or person-level distributions).
draw_cor(x1, x2, cor, steps, step, method): the correlation of two standard-normal variables
(unit variances, the identification of several abilities), steps Metropolis steps on
atanh(cor) with a uniform prior: random walk ("rw") or MALA ("mala", see draw_mala()).
calc_person_fit(L, fitted, sigma2): the RT person-fit statistic of Marianti et al. (2014)
per person, with the number
of observed times df (missing times are skipped); chi-square with df degrees of freedom.
calc_normal_part(): a normal part (prec, num; see normal_parts); draw_mvn(): a vector or a p x n
matrix; draw_items_mh(): a list; draw_invwishart(): a matrix; draw_mean_mvn(): a vector;
draw_cor(): a number with attribute accepted; calc_person_fit(): a data frame.
Marianti, S., Fox, J.-P., Avetisyan, M., Veldkamp, B. P., & Tijmstra, J. (2014). Testing for aberrant behavior in response time modeling. Journal of Educational and Behavioral Statistics, 39, 426-451.
set.seed(1) N <- 300; K <- 5; theta <- rnorm(N); a <- rep(1.2, K); b <- seq(-1, 1, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(theta, a), 2, a * b))), N) om <- draw_omega(a, b, theta) # b_i | omega: eta = a (theta - b) enters the PG pseudo-observations with x = -a g <- calc_normal_part(matrix(-a, N, K, byrow = TRUE), (Y - 0.5) / om, om, rest = outer(theta, a)) draw_gauss(1 / 9 + g$prec, g$num) # prior N(0, 3^2) # (a, b) jointly with the latent responses integrated out draw_items_mh(Y, theta, a, b, prior_mean = c(1, 0), prior_prec = diag(c(1, 1 / 9))) draw_mvn(diag(2) * 4, c(1, 2), lower = 0)set.seed(1) N <- 300; K <- 5; theta <- rnorm(N); a <- rep(1.2, K); b <- seq(-1, 1, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(theta, a), 2, a * b))), N) om <- draw_omega(a, b, theta) # b_i | omega: eta = a (theta - b) enters the PG pseudo-observations with x = -a g <- calc_normal_part(matrix(-a, N, K, byrow = TRUE), (Y - 0.5) / om, om, rest = outer(theta, a)) draw_gauss(1 / 9 + g$prec, g$num) # prior N(0, 3^2) # (a, b) jointly with the latent responses integrated out draw_items_mh(Y, theta, a, b, prior_mean = c(1, 0), prior_prec = diag(c(1, 1 / 9))) draw_mvn(diag(2) * 4, c(1, 2), lower = 0)
The share of item and structural parameters with effective sample size above ess and
R-hat below rhat (over independent chains), and the worst parameters. With
diagnostics = "rank" (see estimates()) both the bulk and the tail ESS must exceed ess,
and the default R-hat threshold is 1.01 (Vehtari et al., 2021) instead of 1.05.
Parameters that do not vary (fixed ones) are left out. With one chain R-hat cannot be
computed and only the ESS is checked. print() of a gibbs() fit warns with the same
criteria (default thresholds) when a parameter fails them.
Julia: checkConvergence().
convergence(object, ...) ## S3 method for class 'rtirt' convergence( object, ess = 400, rhat = NULL, diagnostics = c("classic", "rank"), ... ) ## S3 method for class 'rtirt_qset' convergence(object, ...) ## S3 method for class 'rtirt_convergence' print(x, ...)convergence(object, ...) ## S3 method for class 'rtirt' convergence( object, ess = 400, rhat = NULL, diagnostics = c("classic", "rank"), ... ) ## S3 method for class 'rtirt_qset' convergence(object, ...) ## S3 method for class 'rtirt_convergence' print(x, ...)
object |
A sampled model, an |
... |
Unused. |
ess, rhat
|
Thresholds ( |
diagnostics |
|
x |
An |
For several quantile levels (an rtirt_qset from gibbs(..., quantile = c(.25, .75)))
each level is checked; the model of one level is fits$models[[k]].
An object of class rtirt_convergence, printed by its print() method: share
(share of parameters that pass), worst (the five parameters with the largest R-hat, or
the smallest ESS with one chain), n (parameters checked), n_chain and the thresholds;
for an ECM fit converged and iterations; for an rtirt_qset one such object per level
(levels) with quantile.
cond <- set_cond(n_subj = 200, n_item = 6) dat <- sim_data(cond, sim_para(cond, "null"), "null") fit <- gibbs(dat, model = "null", n_iter = 1000, n_chain = 2, seed = 1) check <- convergence(fit) # printed only when not assigned check check$sharecond <- set_cond(n_subj = 200, n_item = 6) dat <- sim_data(cond, sim_para(cond, "null"), "null") fit <- gibbs(dat, model = "null", n_iter = 1000, n_chain = 2, seed = 1) check <- convergence(fit) # printed only when not assigned check check$share
As getDic(MCMC) in Julia: is the posterior mean of the complete-data deviance
(after burn-in; the Julia version averaged over the burn-in as well), the
deviance at the posterior means, . The deviance conditions on the
person parameters (and, for the ALD models, on the mixing variables), so DIC values are
not comparable between mean and quantile models or across quantile levels.
dic(object, ...) ## S3 method for class 'rtirt' dic(object, ...)dic(object, ...) ## S3 method for class 'rtirt' dic(object, ...)
object |
A sampled model. |
... |
Unused. |
Some directions of a model are identified only by the priors: adding the same constant to the
abilities, the difficulties and the intercept of a latent regression leaves the likelihood of
a 2PL unchanged. A Gibbs sampler moves along such a direction very slowly. draw_shift() draws
the shift of the move exactly from its conditional
(a generalized Gibbs step; Liu & Sabatti, 2000): only the normal priors of the shifted
parameters depend on . Parameters listed in values without a prior are shifted with
a prior that moves along (e.g. abilities whose prior mean contains the shifted intercept).
draw_shift(values, prior, along = list(), lower = list())draw_shift(values, prior, along = list(), lower = list())
values |
Named list of the current values to shift. |
prior |
Named list of normal priors |
along |
Named list of direction multipliers (default 1). |
lower |
Named list of lower bounds of truncated priors (e.g. positive loadings): the shift is drawn from its conditional truncated so that every bound holds. |
The caller states that the likelihood is unchanged by the shift. Common cases:
| model | shift | values, prior |
| 2PL, theta ~ N(g0 + x'g, 1) | theta + c, b + c, g0 + c | list(theta, b, g0), priors of b and g0 |
| log T = lambda - zeta | lambda + c, zeta + c | list(lambda, zeta), priors of both (speed: N(m_j, 1 / w_j)), lower = list(lambda = 0) for a truncated prior |
| intercept form a theta - d | theta + c, d + a c | along = list(d = a)
|
values shifted, with attribute shift.
Liu, J. S., & Sabatti, C. (2000). Generalised Gibbs sampler and multigrid Monte Carlo for Bayesian computation. Biometrika, 87, 353-369.
s <- draw_shift(list(theta = rnorm(5), b = c(-1, 0, 1), g0 = 0.2), prior = list(b = c(0, 3), g0 = c(0, 10))) attr(s, "shift")s <- draw_shift(list(theta = rnorm(5), b = c(-1, 0, 1), g0 = 0.2), prior = list(b = c(0, 3), g0 = c(0, 10))) attr(s, "shift")
Fits a built-in normal model by maximum marginal likelihood: the EM algorithm of Bock and Aitkin (1981) with ability integrated by Gauss-Hermite quadrature and speed in closed form. The item step uses the Polya-Gamma bound of the logistic likelihood (Polson, Scott & Windle, 2013), so every step is closed form and the log-likelihood never decreases; SQUAREM speeds up convergence. Standard errors come from the observed information (the numerical derivative of the analytic score).
ecm(data, ...) ## S3 method for class 'input_data' ecm( data, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, prior = c("ml", "map"), priors = NULL, se = TRUE, control = ecm_control(), ... )ecm(data, ...) ## S3 method for class 'input_data' ecm( data, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, prior = c("ml", "map"), priors = NULL, se = TRUE, control = ecm_control(), ... )
data |
Data from |
... |
Not used: any other argument is an error before the fit starts. A setting of
|
model |
The model by name (case does not matter):
A misspelled name gets the closest one in the error message. |
itemtype |
|
speed_var |
|
quantile |
|
cov |
Names of the covariates of |
prior |
|
priors |
Prior distributions for |
se |
Compute standard errors. |
control |
Further settings, from |
Quantile models. With quantile = (models "latent" and "cross", as in gibbs())
ecm() maximizes the marginal ALD working likelihood (Yu & Moyeed, 2001): the q-quantile of
the log times ("cross") or of speed given ability and the covariates ("latent") is linear
in the parameters. Ability is integrated on the nodes and speed in closed form, so the
working log-likelihood is smooth. The E-step and the item step are those of the normal models;
the response-time and structural parameters are updated by nlminb() with the analytic
gradient (two iterations per ECM step, which never lowers the working log-likelihood). The
fit starts at the ECM fit of the mean model, moved to the q-quantile. The standard errors are
sandwich (robust) standard errors, with the
sum of the outer products of the scores of the persons: the ALD is not the distribution of the
data, so the observed information alone would be wrong. As in gibbs(), sigma2t of
"cross" is the ALD scale of the log times and var_speed of "latent" the ALD scale of
speed. The working logLik, AIC and BIC compare only fits at the same quantile; anova()
refuses quantile fits. A vector of levels gives one fit per level (an rtirt_qset).
The parameterization and identification are those of gibbs(): ability has mean
x'beta and variance 1, speed mean x'beta (no intercepts: the covariates are centred and
the item parameters carry the location). The fit starts the Gibbs sampler:
fit_ecm <- ecm(dat, model = "cross") fit_gibbs <- gibbs(dat, model = "cross", init = fit_ecm)
Small samples. print() and summary() warn when the estimates suggest that the data
are too few for maximum likelihood: a difficulty with |b| > 6, a discrimination below 0.1
or above 10, or a standard error above 5 (or missing) for a, b or rho. These thresholds
flag only clearly broken fits (with ability N(0, 1), |b| > 6 means almost everyone or no
one solves the item, and an SE of 5 a 95% interval about 20 wide); prior = "map" or
gibbs() regularize such fits.
A fitted model with ecm: estimates (est, se, z, Wald 95% interval on the log scale
for positive parameters and the Fisher-z scale for the correlation), logLik, npar,
AIC, BIC, iterations, converged, trace (log-likelihood per iteration), vcov,
person (EAP and posterior SD) and secs. Accessors: print, summary, coef,
estimates(), scores(), reliability(), fit_indices(), logLik, vcov, AIC, BIC.
Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of item parameters: Application of an EM algorithm. Psychometrika, 46, 443-459.
Polson, N. G., Scott, J. G., & Windle, J. (2013). Bayesian inference for logistic models using Polya-Gamma latent variables. JASA, 108, 1339-1349.
White, H. (1982). Maximum likelihood estimation of misspecified models. Econometrica, 50, 1-25.
Yu, K., & Moyeed, R. A. (2001). Bayesian quantile regression. Statistics & Probability Letters, 54, 437-447.
Varadhan, R., & Roland, C. (2008). Simple and globally convergent methods for accelerating the convergence of any EM algorithm. Scandinavian Journal of Statistics, 35, 335-353.
cond <- set_cond(n_subj = 300, n_item = 8) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross") fit head(estimates(fit)) fit_q <- ecm(dat, model = "cross", quantile = 0.9) # the 0.9-quantile of the log times estimates(fit_q, pars = "^rho")cond <- set_cond(n_subj = 300, n_item = 8) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross") fit head(estimates(fit)) fit_q <- ecm(dat, model = "cross", quantile = 0.9) # the 0.9-quantile of the log times estimates(fit_q, pars = "^rho")
The settings of ecm() and gibbs() that are rarely changed.
ecm_control( max_iter = 1000, tol = 1e-10, nodes = 41, accelerate = TRUE, verbose = FALSE, ... ) gibbs_control( n_burnin = NULL, n_thin = 1, collapse = FALSE, fit_indices = TRUE, verbose = TRUE, ... ) ## S3 method for class 'ecm_control' print(x, ...) ## S3 method for class 'gibbs_control' print(x, ...)ecm_control( max_iter = 1000, tol = 1e-10, nodes = 41, accelerate = TRUE, verbose = FALSE, ... ) gibbs_control( n_burnin = NULL, n_thin = 1, collapse = FALSE, fit_indices = TRUE, verbose = TRUE, ... ) ## S3 method for class 'ecm_control' print(x, ...) ## S3 method for class 'gibbs_control' print(x, ...)
max_iter |
Maximum number of ECM (SQUAREM) iterations. |
tol |
Convergence: relative change of the log-likelihood below |
nodes |
Gauss-Hermite nodes for ability. |
accelerate |
Use SQUAREM. |
verbose |
|
... |
Not used: a name that is not a setting is an error that names the closest
setting, or the function it belongs to ( |
n_burnin |
Burn-in per chain (default |
n_thin |
Thinning of the stored draws. |
collapse |
|
fit_indices |
Compute the marginal log-likelihood after sampling (for DIC, WAIC and
PSIS-LOO in |
x |
Settings from |
A list of settings of class ecm_control or gibbs_control; ecm() refuses a
gibbs_control() and gibbs() an ecm_control(). A plain list with the names of the
settings is also accepted.
fit_settings <- gibbs_control(n_burnin = 500, collapse = TRUE) fit_settingsfit_settings <- gibbs_control(n_burnin = 500, collapse = TRUE) fit_settings
Deprecated with block_model(): use the built-in models, ecm(data, model = ...).
## S3 method for class 'block_model' ecm( data, steps = NULL, latent = NULL, nodes = NULL, prior = c("map", "ml"), init = NULL, cycles = 6, max_iter = 1000, tol = 1e-10, se = TRUE, analytic = TRUE, quadrature = c("adaptive", "ba81"), ... ) ## S3 method for class 'block_model_ecm' print(x, ...) ## S3 method for class 'block_model_ecm' algorithm(object, format = c("unicode", "latex"), ...)## S3 method for class 'block_model' ecm( data, steps = NULL, latent = NULL, nodes = NULL, prior = c("map", "ml"), init = NULL, cycles = 6, max_iter = 1000, tol = 1e-10, se = TRUE, analytic = TRUE, quadrature = c("adaptive", "ba81"), ... ) ## S3 method for class 'block_model_ecm' print(x, ...) ## S3 method for class 'block_model_ecm' algorithm(object, format = c("unicode", "latex"), ...)
data |
|
steps |
Steps, as for |
latent |
Parameters integrated out in the E-step (one to three |
nodes |
Quadrature points per latent variable (default 21, 11 or 7 for one, two or three); one
number, or one per latent variable (named, e.g. |
prior |
|
init |
Optional starting values (named list). |
cycles |
Maximum number of recentring rounds of the quadrature. |
max_iter, tol
|
Passed to |
se |
Standard errors from the Hessian of the objective. |
analytic |
Integrate latent variables that enter the model only through normal parts, linearly (e.g. speed in a log-normal RT model), exactly given the others instead of on the grid. |
quadrature |
|
... |
Unused. |
x |
A fit of |
object |
A fit of |
format |
|
Fits a block_model() by EM with the same steps as gibbs(). Gibbs and EM share the
full conditionals of the model: a step that a sampler uses to draw a parameter is used by EM to
move the parameter to the mode of its full conditional averaged over the latent variables.
E-step: the person parameters (latent, by default every [person] parameter) are
integrated out by adaptive Gauss–Hermite quadrature: a product grid of nodes points per
latent variable, centred at each person's posterior mean and scaled by the posterior SD. The
centres are fixed while EM runs (so the objective is one fixed function and EM never lowers
it), then recentred, for up to cycles rounds. The Pólya–Gamma variables of a logistic part are
replaced by their expectation
at each node (a minorizer: the step still never lowers the objective).
step_gibbs() becomes a conditional maximization: the normal part of the parameter is summed
over the model and over the nodes (weighted by the posterior of each person), and the parameter
moves to its mode (clipped to the bounds); a variance moves to the mode of its
inverse-gamma conditional (prior = "ml": ).
step_mala() becomes a few gradient-ascent steps with step halving on the same weighted
objective (generalized EM), on the log scale for bounded parameters (no Jacobian: a mode does not
need one).
step_shift() is skipped: the latent variables are integrated out.
The objective is the log marginal likelihood (prior = "ml"; the priors of the latent variables
stay, they define the marginal likelihood) or the log marginal posterior (prior = "map", the
default: also the priors of the other parameters). Standard errors come from its Hessian (Louis'
formula: expected complete-data Hessian plus the covariance of the complete-data score, from the
derivatives of the declared expressions); the
posterior means and SDs of the latent variables are in fit$latent. EM runs through run_em()
(SQUAREM, the checks of run_em()). Use check_ecm() to check the steps.
A block_em (see run_em()) with latent (posterior means and SDs per person), model,
steps and cycles; algorithm(fit) lists the E- and M-steps.
ECM. EM needs only the conditional expectations of the complete-data sufficient statistics (Dempster, Laird & Rubin, 1977, Sec. 2) and never lowers the objective when every M-step raises Q (their Theorem 1, generalized EM); the M-step may be a sequence of conditional maximizations (Meng & Rubin, 1993).
Adaptive quadrature. Nodes centred and scaled at each person's posterior (Naylor & Smith, 1982; Liu & Pierce, 1994; Bock & Gibbons, 2021, Sec. 6.4). Schilling & Bock (2005) recentre at every EM iteration; here the centres are kept fixed within a round and recentred between rounds, as Bock & Gibbons (2021, pp. 210–211) suggest to save time and as in the pseudo-adaptive rule of Rizopoulos (2012). With fixed centres the approximated objective is one fixed function, so the monotonicity of Dempster et al. (1977, Theorem 1) applies to it (our argument; no source states it).
quadrature = "ba81". Common nodes and expected counts per node ("artificial data"):
Bock & Aitkin (1981); Bock & Gibbons (2021, Sec. 6.4, Eqs. 6.17–6.22).
Logistic parts. (Polson, Scott & Windle,
2013, Sec. 2.2) makes the M-step an exact EM step for the Pólya–Gamma augmented model (Scott &
Sun, 2013); it equals the Jaakkola–Jordan bound, a minorize–maximize step that never lowers the
likelihood (Durante & Rigon, 2019, Appendix).
Exact integration (analytic). A latent variable u that enters only normal parts linearly,
with variances free of u, has a normal conditional (completing the square;
Bishop, 2006, Sec. 2.3; for speed in the hierarchical RT model, van der Linden, 2007, Eq. 42),
and the integral over u is the integrand at the mode times , exact for a
quadratic log integrand (Bishop, 2006, Eq. 4.135). The RT-IRT marginal likelihood papers we found
integrate speed numerically together with ability (Glas & van der Linden, 2010; Molenaar &
Bolsinova, 2017); integrating it exactly is our reduction, valid only under the condition above
(checked from the declaration; not for heteroscedastic or skewed speed).
Exact variables in the M-step. When every part of an exact variable is linear in all
latent variables jointly, with variances free of them, these parts are quadratic in the latent
variables, and the expectation of a quadratic depends only on the mean and covariance
(, for any distribution). The M-step therefore uses
each person's posterior mean and covariance of the latent variables through the 2L symmetric
points x columns of the Cholesky factor, weight 1/(2L) each, which have exactly
that mean and covariance (Julier & Uhlmann, 2004, Eq. 11). Logistic parts stay on the nodes.
Standard errors. Louis (1982): observed information = expected complete-data information
minus the covariance of the complete-data score. Under prior = "map" the log prior of the
fixed parameters does not involve the latent variables, so its Hessian is added to that of the
log marginal likelihood (Chalmers, 2018, notes that how priors enter Louis' form needs saying). The
squared scores are quartic in an exact variable, so three Gauss–Hermite points per exact
variable are used (n points are exact to degree 2n - 1; DLMF Sec. 3.5(v)).
Bishop, C. M. (2006). Pattern recognition and machine learning. Springer.
Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of item parameters: Application of an EM algorithm. Psychometrika, 46(4), 443–459. doi:10.1007/BF02293801
Bock, R. D., & Gibbons, R. D. (2021). Item response theory. Wiley. doi:10.1002/9781119716723
Chalmers, R. P. (2018). Numerical approximation of the observed information matrix with Oakes' identity. British Journal of Mathematical and Statistical Psychology, 71(3), 415–436. doi:10.1111/bmsp.12127
Dempster, A. P., Laird, N. M., & Rubin, D. B. (1977). Maximum likelihood from incomplete data via the EM algorithm. Journal of the Royal Statistical Society B, 39(1), 1–22. doi:10.1111/j.2517-6161.1977.tb01600.x
DLMF. NIST Digital Library of Mathematical Functions, Sec. 3.5(v), Gauss quadrature. https://dlmf.nist.gov/3.5
Durante, D., & Rigon, T. (2019). Conditionally conjugate mean-field variational Bayes for logistic models. Statistical Science, 34(3), 472–485. doi:10.1214/19-STS712
Glas, C. A. W., & van der Linden, W. J. (2010). Marginal likelihood inference for a model for item responses and response times. British Journal of Mathematical and Statistical Psychology, 63(3), 603–626. doi:10.1348/000711009X481360
Julier, S. J., & Uhlmann, J. K. (2004). Unscented filtering and nonlinear estimation. Proceedings of the IEEE, 92(3), 401–422. doi:10.1109/JPROC.2003.823141
Liu, Q., & Pierce, D. A. (1994). A note on Gauss–Hermite quadrature. Biometrika, 81(3), 624–629. doi:10.1093/biomet/81.3.624
Louis, T. A. (1982). Finding the observed information matrix when using the EM algorithm. Journal of the Royal Statistical Society B, 44(2), 226–233. doi:10.1111/j.2517-6161.1982.tb01203.x
Meng, X.-L., & Rubin, D. B. (1993). Maximum likelihood estimation via the ECM algorithm: A general framework. Biometrika, 80(2), 267–278. doi:10.1093/biomet/80.2.267
Molenaar, D., & Bolsinova, M. (2017). A heteroscedastic generalized linear model with a non-normal speed factor for responses and response times. British Journal of Mathematical and Statistical Psychology, 70(2), 297–316. doi:10.1111/bmsp.12087
Naylor, J. C., & Smith, A. F. M. (1982). Applications of a method for the efficient computation of posterior distributions. Applied Statistics, 31(3), 214–225. doi:10.2307/2347995
Polson, N. G., Scott, J. G., & Windle, J. (2013). Bayesian inference for logistic models using Pólya–Gamma latent variables. Journal of the American Statistical Association, 108(504), 1339–1349. doi:10.1080/01621459.2013.829001
Rizopoulos, D. (2012). Fast fitting of joint models for longitudinal and event time data using a pseudo-adaptive Gaussian quadrature rule. Computational Statistics & Data Analysis, 56(3), 491–501. doi:10.1016/j.csda.2011.09.007
Schilling, S., & Bock, R. D. (2005). High-dimensional maximum marginal likelihood item factor analysis by adaptive quadrature. Psychometrika, 70(3), 533–555. doi:10.1007/s11336-003-1141-x
Scott, J. G., & Sun, L. (2013). Expectation-maximization for logistic regression. arXiv:1306.0040.
van der Linden, W. J. (2007). A hierarchical framework for modeling speed and accuracy on test items. Psychometrika, 72(3), 287–308. doi:10.1007/s11336-006-1478-z
set.seed(1); N <- 300; K <- 6 th <- rnorm(N); a0 <- runif(K, 0.8, 2); b0 <- seq(-1.5, 1.5, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(th, a0), 2, a0 * b0))), N) m <- block_model(Y = Y, theta[person] ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), eta <- a * (theta - b), Y ~ bernoulli_logit(eta)) fit <- ecm(m, list(step_gibbs(a, b)), prior = "ml") fit algorithm(fit)set.seed(1); N <- 300; K <- 6 th <- rnorm(N); a0 <- runif(K, 0.8, 2); b0 <- seq(-1.5, 1.5, length.out = K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(th, a0), 2, a0 * b0))), N) m <- block_model(Y = Y, theta[person] ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), eta <- a * (theta - b), Y ~ bernoulli_logit(eta)) fit <- ecm(m, list(step_gibbs(a, b)), prior = "ml") fit algorithm(fit)
The steps of ecm() as functions that can be combined into an EM for another model (the
counterpart of the sampling blocks, blocks and compose: draw_*() draws a block from its
full conditional, update_*() maximizes, or takes a monotone MM step, in the same block).
calc_nodes(Q = 41) calc_loglik_nodes(Y, a, b, theta) calc_normal_given_nodes(o, w, m, v) calc_node_weights(logL, logw = 0) calc_artificial_data(W, Y) calc_item_sums(Y, a, b, theta, W, score = FALSE) update_items_logistic(S, T0, T1, a, b, prior_a = NULL, prior_b = NULL) em_squarem( x, Fmap, max_iter = 1000, tol = 1e-10, accelerate = TRUE, progress = NULL ) calc_hessian(score, x, h = 1e-04)calc_nodes(Q = 41) calc_loglik_nodes(Y, a, b, theta) calc_normal_given_nodes(o, w, m, v) calc_node_weights(logL, logw = 0) calc_artificial_data(W, Y) calc_item_sums(Y, a, b, theta, W, score = FALSE) update_items_logistic(S, T0, T1, a, b, prior_a = NULL, prior_b = NULL) em_squarem( x, Fmap, max_iter = 1000, tol = 1e-10, accelerate = TRUE, progress = NULL ) calc_hessian(score, x, h = 1e-04)
Q |
Number of nodes. |
Y |
Responses (N x K, 0/1). |
a, b
|
Item parameters. |
theta |
Common nodes (a vector) or person-specific nodes (N x Q). |
o, w, m, v
|
Pseudo-observations (N x K), their precisions (per item), prior means (N x Q or a vector of Q) and prior variance of the normal latent variable. |
logL, logw
|
Log-likelihood at the nodes (N x Q) and log weights of the nodes. |
W |
Posterior weights of the nodes (N x Q). |
score |
Score sums instead of minorizer sums. |
S, T0, T1
|
Item sums (a list with |
prior_a, prior_b
|
Normal priors of a (on (0, Inf)) and b (MAP), or |
x |
Starting value / point. |
Fmap |
Fixed-point map. |
max_iter, tol, accelerate
|
Iteration limit, convergence tolerance (relative change of the
objective and |
progress |
Optional function called as |
h |
Relative step of the central differences. |
E-step
calc_nodes(Q): Gauss–Hermite nodes z and log weights logw of N(0, 1).
calc_loglik_nodes(Y, a, b, theta): the 2PL log-likelihood of every person at every node,
, , an N x Q matrix.
theta is the vector of nodes when they are common to all persons (two matrix products,
Bock–Aitkin), or an N x Q matrix of person-specific nodes (latent regressions; C++).
calc_normal_given_nodes(o, w, m, v): a normal latent variable u (e.g. speed) with prior
N(m, v) at each node (m: N x Q, or a vector of Q), observed through normal
pseudo-observations , : integrated in
closed form. Returns logI (N x Q, add to the log-likelihood), the conditional mean (N x Q) and
var of u.
calc_node_weights(logL, logw): posterior weights W of the nodes (N x Q) and the
log-likelihood ll of every person.
calc_artificial_data(W, Y): Bock–Aitkin's expected counts: n (Q x K, persons at each node
answering each item) and r (Q x K, correct answers).
calc_item_sums(Y, a, b, theta, W, score): weighted item sums over persons and nodes, either the
Pólya–Gamma minorizer sums (score = FALSE) or the
score sums (score = TRUE), r = 0, 1, 2; common nodes go through
the artificial data.
M-step
update_items_logistic(S, T0, T1, a, b, prior_a, prior_b): the Pólya–Gamma (Jaakkola–Jordan)
minorizer step for 2PL items: a 2 x 2 weighted least-squares problem per item from the sums S
of calc_item_sums() and , ; the marginal log-likelihood never decreases. With prior_a = c(mean, sd) and
prior_b = c(mean, sd, lower, upper) (or a list whose mean has one value per item) the conditional modes (MAP).
Driver and information
em_squarem(x, Fmap, ...): SQUAREM (Varadhan & Roland, 2008; SqS3) around a fixed-point map
Fmap(x) returning list(x = , obj = ) (one EM step from x and the log-likelihood at x), with a
fall-back to plain EM steps when the extrapolation lowers the objective.
calc_hessian(score, x): the symmetrized numerical Jacobian of an analytic score function
(central differences); is the covariance of the estimates.
See Details.
Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of item parameters: Application of an EM algorithm. Psychometrika, 46, 443-459.
Varadhan, R., & Roland, C. (2008). Simple and globally convergent methods for accelerating the convergence of any EM algorithm. Scandinavian Journal of Statistics, 35, 335-353.
set.seed(1) N <- 500; K <- 10; a <- runif(K, 0.8, 2); b <- rnorm(K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(rnorm(N), a), 2, a * b))), N) # an EM for the 2PL from the components nd <- calc_nodes(41); kap <- Y - 0.5 Fmap <- function(x) { a <- exp(x[1:K]); b <- x[K + 1:K] nw <- calc_node_weights(calc_loglik_nodes(Y, a, b, nd$z), nd$logw) S <- calc_item_sums(Y, a, b, nd$z, nw$W) new <- update_items_logistic(S, colSums(kap), colSums(kap * drop(nw$W %*% nd$z)), a, b) list(x = c(log(new$a), new$b), obj = sum(nw$ll)) } fit <- em_squarem(c(rep(0, K), rep(0, K)), Fmap) round(cbind(a, a_hat = exp(fit$x[1:K]), b, b_hat = fit$x[K + 1:K]), 2)set.seed(1) N <- 500; K <- 10; a <- runif(K, 0.8, 2); b <- rnorm(K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(rnorm(N), a), 2, a * b))), N) # an EM for the 2PL from the components nd <- calc_nodes(41); kap <- Y - 0.5 Fmap <- function(x) { a <- exp(x[1:K]); b <- x[K + 1:K] nw <- calc_node_weights(calc_loglik_nodes(Y, a, b, nd$z), nd$logw) S <- calc_item_sums(Y, a, b, nd$z, nw$W) new <- update_items_logistic(S, colSums(kap), colSums(kap * drop(nw$W %*% nd$z)), a, b) list(x = c(log(new$a), new$b), obj = sum(nw$ll)) } fit <- em_squarem(c(rep(0, K), rep(0, K)), Fmap) round(cbind(a, a_hat = exp(fit$x[1:K]), b, b_hat = fit$x[K + 1:K]), 2)
Writes the model as equations, with the parameter names of summary() and the priors of
gibbs(). The default is Unicode text for the console; format = "latex" gives an
aligned block for a paper or an appendix (e.g. writeLines(as.character(equations(fit, "latex")), "model.tex")).
Works before estimation too: give the data and the model as in ecm() and gibbs().
equations(object, ...) ## S3 method for class 'rtirt_equations' as.character(x, ...) ## S3 method for class 'rtirt_equations' print(x, ...) ## S3 method for class 'rtirt' equations(object, format = c("unicode", "latex"), priors = TRUE, ...) ## S3 method for class 'rtirt_qset' equations(object, format = c("unicode", "latex"), priors = TRUE, ...) ## S3 method for class 'input_data' equations( object, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, intercept = FALSE, format = c("unicode", "latex"), priors = TRUE, ... )equations(object, ...) ## S3 method for class 'rtirt_equations' as.character(x, ...) ## S3 method for class 'rtirt_equations' print(x, ...) ## S3 method for class 'rtirt' equations(object, format = c("unicode", "latex"), priors = TRUE, ...) ## S3 method for class 'rtirt_qset' equations(object, format = c("unicode", "latex"), priors = TRUE, ...) ## S3 method for class 'input_data' equations( object, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, intercept = FALSE, format = c("unicode", "latex"), priors = TRUE, ... )
object |
A fit of |
... |
Unused. |
x |
An |
format |
|
priors |
Include the priors (omitted for an |
model, itemtype, speed_var, quantile, cov, intercept
|
For |
An rtirt_equations object (printed); as.character() gives the lines.
cond <- set_cond(n_subj = 100, n_item = 5) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") equations(dat, model = "cross") equations(dat, model = "cross", quantile = 0.25, format = "latex")cond <- set_cond(n_subj = 100, n_item = 5) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") equations(dat, model = "cross") equations(dat, model = "cross", quantile = 0.25, format = "latex")
estfun() returns, for a model fitted by ecm(), the matrix of casewise scores: one row per
person, one column per parameter, the derivative of that person's marginal log-likelihood (ability
and speed integrated out). The columns sum to zero at the estimates. The parameters are on the
unconstrained scale used by ecm(): log a, b, lambda, log sigma2t, then the structural parameters
(for model = "latreg" / "null" with free speed variance: the regression coefficients, the
covariance c and the log residual variance log_v of speed given ability). These are the
estimating functions used by score_test() and by strucchange::gefp().
## S3 method for class 'rtirt' estfun(x, ...) ## S3 method for class 'rtirt' bread(x, ...)## S3 method for class 'rtirt' estfun(x, ...) ## S3 method for class 'rtirt' bread(x, ...)
x |
A model fitted by |
... |
Unused. |
A persons x parameters matrix.
bread(): the inverse of the average observed information (unconstrained scale), as in
the 'sandwich' package.
estimates() returns the posterior summary of the item and structural parameters as a
data frame: posterior mean (est), sd, 2.5% and 97.5% quantiles (q025, q975), R-hat and effective sample size (after
burn-in, over the independent chains); sig = "*" when the 95% interval excludes 0. sig
is given only for parameters where 0 is a meaningful value: difficulties b, regression
coefficients beta, cross-relations rho, cor_ability_speed and b_ability. It is NA
for discriminations, variances (sigma2t, var_speed), which are positive and whose
intervals never contain 0, and time intensities lambda.
precis() (the Julia name) returns the same table, printed in a plain layout (assigning it
prints nothing); as.data.frame() gives it as a plain data frame, e.g. for ggplot2. coef() returns the posterior means
as a named vector. For a model fitted by ecm() the table has the maximum likelihood
estimates with standard errors (est, se, z, Wald 95% interval q025, q975); z,
like sig, is NA where 0 is not a meaningful value, and summary() then shows no z or p.
estimates(object, ...) ## S3 method for class 'rtirt' estimates( object, pars = NULL, method = c("auto", "gibbs", "ecm"), diagnostics = c("classic", "rank"), ... ) precis(object, ...) ## S3 method for class 'rtirt' precis(object, pars = NULL, digits = 3, ...) ## S3 method for class 'rtirt_precis' print(x, digits = attr(x, "digits"), ...) ## S3 method for class 'rtirt' as.data.frame(x, row.names = NULL, optional = FALSE, ...) ## S3 method for class 'rtirt' coef(object, ...)estimates(object, ...) ## S3 method for class 'rtirt' estimates( object, pars = NULL, method = c("auto", "gibbs", "ecm"), diagnostics = c("classic", "rank"), ... ) precis(object, ...) ## S3 method for class 'rtirt' precis(object, pars = NULL, digits = 3, ...) ## S3 method for class 'rtirt_precis' print(x, digits = attr(x, "digits"), ...) ## S3 method for class 'rtirt' as.data.frame(x, row.names = NULL, optional = FALSE, ...) ## S3 method for class 'rtirt' coef(object, ...)
object |
A sampled model, or one fitted by |
... |
Unused. |
pars |
Optional regular expression selecting parameters. |
method |
|
diagnostics |
|
digits |
Decimals to print. |
x |
The result of |
row.names, optional
|
Unused. |
estimates(): a tibble of class rtirt_tbl (printed with 3 decimals; x$est and
as.data.frame(x) keep the full precision). coef(): a named vector, printed with 3 decimals.
Gelman, A., & Rubin, D. B. (1992). Inference from iterative simulation using multiple sequences. Statistical Science, 7, 457-472.
Vehtari, A., Gelman, A., Simpson, D., Carpenter, B., & Bürkner, P.-C. (2021). Rank-normalization, folding, and localization: An improved R-hat for assessing convergence of MCMC. Bayesian Analysis, 16, 667-718.
Criteria from the marginal log-likelihood (person parameters integrated out; see
marginal_loglik()), which compare models with different latent structures:
DIC (with pD = mean deviance - deviance at the posterior means of the item and
structural parameters), WAIC (p_waic) and PSIS-LOO (LOOIC, SE_LOOIC; needs the
'loo' package). DIC_complete is the complete-data DIC of dic() (conditional on the
person parameters; for comparisons within one model only). Quantile models use the ALD as
a working likelihood, so their criteria are not comparable with mean models or across
quantile levels. For a model fitted by ecm(): the log-likelihood, AIC and BIC.
fit_indices(object, ...) ## S3 method for class 'rtirt' fit_indices(object, ...)fit_indices(object, ...) ## S3 method for class 'rtirt' fit_indices(object, ...)
object |
A sampled model, or one fitted by |
... |
Unused. |
Bias, RMSE and correlation
get_bias(est, true) get_rmse(est, true) get_corr(est, true)get_bias(est, true) get_rmse(est, true) get_corr(est, true)
est, true
|
Estimates and true values. |
Deprecated with block_model(): use the built-in models, gibbs(data, model = ...).
## S3 method for class 'block_model' gibbs( data, steps = NULL, n_iter = 2000, n_burnin = floor(n_iter/2), n_chain = 2, n_thin = 1, seed = 1, init = NULL, monitor = NULL, ... ) ## S3 method for class 'block_model' algorithm(object, steps = NULL, format = c("unicode", "latex"), ...) ## S3 method for class 'block_model_fit' algorithm(object, format = c("unicode", "latex"), ...)## S3 method for class 'block_model' gibbs( data, steps = NULL, n_iter = 2000, n_burnin = floor(n_iter/2), n_chain = 2, n_thin = 1, seed = 1, init = NULL, monitor = NULL, ... ) ## S3 method for class 'block_model' algorithm(object, steps = NULL, format = c("unicode", "latex"), ...) ## S3 method for class 'block_model_fit' algorithm(object, format = c("unicode", "latex"), ...)
data |
|
steps |
A list of steps, e.g. |
n_iter, n_burnin, n_chain, n_thin, seed
|
As in |
init |
Optional starting values: a named list (one value or one per person/item). The
default starts near the prior means; with two latent variables give data-based values (e.g.
standardized sum scores and mean log times), since a chain can settle in another mode (the
ability taking over the role of speed); |
monitor |
Parameters to store; by default all but the person parameters, whose posterior
means and SDs are kept instead ( |
... |
Unused. |
object |
A |
format |
|
Runs the steps once per iteration in the order given (see block_steps) with run_sampler()
(chains, burn-in, storage, summary). Without steps, every parameter that has a Gibbs step gets
one (in the order of the declarations) and the others get MALA.
A block_fit (see run_sampler()) with the model and the steps; algorithm(fit) lists
the steps with their full conditionals.
Samples a built-in model from its full conditionals: n_chain independent chains, each
from its own random starting values or from an ecm() fit.
## S3 method for class 'input_data' gibbs( data, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, intercept = FALSE, priors = NULL, n_iter = 5000, n_chain = 4, seed = NULL, init = "random", control = gibbs_control(), ... ) gibbs(data, ...)## S3 method for class 'input_data' gibbs( data, model = NULL, itemtype = "2pl", speed_var = c("free", "fixed"), quantile = NULL, cov = NULL, intercept = FALSE, priors = NULL, n_iter = 5000, n_chain = 4, seed = NULL, init = "random", control = gibbs_control(), ... ) gibbs(data, ...)
data |
Data from |
model |
The model, by name:
|
itemtype |
|
speed_var |
|
quantile |
|
cov |
Names of the covariates of |
intercept |
Add an intercept to the latent regressions (default |
priors |
Prior distributions, from |
n_iter, n_chain
|
Iterations per chain and chains. |
seed |
Random seed ( |
init |
|
control |
Further sampler settings, from |
... |
Not used: any other argument is an error before sampling starts. A setting of
|
fit_ecm <- ecm(dat, model = "cross") fit_gibbs <- gibbs(dat, model = "cross", init = fit_ecm)
A fitted model with post: draws (a coda::mcmc.list of the item and structural
parameters, all iterations), mean (posterior means after burn-in, an input_para()),
person (posterior mean and SD of ability and speed), loglik (complete-data
log-likelihood per iteration and chain) and secs. Accessors: summary(), coef(),
estimates(), scores(), reliability(), convergence(), fit_indices(), plot().
Wang, C., & Xu, G. (2015). A mixture hierarchical model for response times and response accuracy. British Journal of Mathematical and Statistical Psychology, 68, 456-477.
cond <- set_cond(n_subj = 300, n_item = 8) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- gibbs(dat, model = "cross", n_iter = 1000, n_chain = 2, seed = 1) summary(fit) head(scores(fit))cond <- set_cond(n_subj = 300, n_item = 8) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- gibbs(dat, model = "cross", n_iter = 1000, n_chain = 2, seed = 1) summary(fit) head(scores(fit))
Collects the item responses, response times and covariates of one test (mirrors
InputData() of the Julia package). Each block is either a matrix / data frame or, with
data, a vector of column names:
input_data( resp, time = NULL, cov = NULL, data = NULL, id = NULL, log_time = NULL )input_data( resp, time = NULL, cov = NULL, data = NULL, id = NULL, log_time = NULL )
resp |
Binary (0/1) responses, persons x items. |
time |
Response times in seconds (positive), same dimensions as |
cov |
Person covariates, persons x covariates. The latent regressions have no
intercept (unless |
data |
Optional data frame from which |
id |
Person identifiers (a vector, or a column name of |
log_time |
Log response times, instead of |
dat <- input_data(resp = items, time = paste0(items, "_S"), cov = covs, id = "IDSTUD", data = raw) dat <- input_data(resp = Y, time = RT, cov = X)
Item names (the column names of resp) label all output. Missing values are not
supported: drop the incomplete persons first.
The data must be in wide format: one row per person, one column per item (for responses
and for times). Data with one row per person and item (long format, as exported by
PsychoPy or E-Prime) are refused, recognized by a single item or by ids that repeat on
most rows; reshape them first, e.g.
tidyr::pivot_wider(long, id_cols = id, names_from = item, values_from = c(correct, rt))
or stats::reshape(long, idvar = "id", timevar = "item", direction = "wide"). A few
duplicated ids give a warning. Covariates must be numeric: dummy-code categorical ones
first, e.g. model.matrix(~ grade, data)[, -1] (k - 1 columns for k categories), and
centre them.
Responses coded as text or factors ("correct" / "wrong") must be recoded to 0/1 first;
the message shows the values found and the recoding. TRUE / FALSE are accepted.
Units of time. Seconds are expected, but the unit only shifts the time intensities:
times in milliseconds give every lambda larger by log(1000) = 6.91 (minutes: smaller by
log(60) = 4.09), and a, b, rho, the residual variances and the correlations do not change.
Report the unit with lambda. The default prior of lambda in gibbs() (N(0, 10^2)) is
meant for seconds, so a median time above 1000 gives a warning that suggests milliseconds.
An object of class input_data with Y, kappa = Y - 1/2, T, log_t, X,
items and id.
Mirrors InputPara: a list of the model parameters (theta, a, b, zeta, lambda,
sigma2t, nu, beta, rho, b_theta, sigma_p). Used for true values in
simulations and for posterior means (fit$post$mean).
input_para(...)input_para(...)
... |
Named parameter values. |
The function names of ExtendedRtIrtModeling.jl, mapped to the snake_case functions:
setCond( nSubj = 2000, nItem = 15, nFeat = 3, nIter = 5000, nChain = 4, nBurnin = NULL, nThin = 1, nRep = 10, qRa = 0.5, qRt = 0.5 ) InputData(Y, T = NULL, X = NULL) InputData4R(Y, kappa = Y - 0.5, logT = NULL, X = NULL) InputPara(...) GibbsMlIrt(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtNull(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrt(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtLatent(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtLatentQr(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtCross(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtCrossQr(Cond, Data, truePara = NULL, Para = NULL) sampleGibbs(MCMC, cov2one = NULL, ...) getDic(MCMC) checkConvergence(MCMC, ...) comparePara(MCMC, par = "a") getBias(est, true) getRmse(est, true) getCorr(est, true) setTrueParaMlIrt(Cond) setTrueParaRtIrt(Cond, trueCorr = 0.3) setTrueParaRtIrtLatent(Cond) setTrueParaRtIrtCross(Cond) setDataMlIrt(Cond, truePara) setDataRtIrt(Cond, truePara) setDataRtIrtNull(Cond, truePara) setDataRtIrtLatent(Cond, truePara, type = "norm") setDataRtIrtCross(Cond, truePara, type = "norm") runSimulation( Cond, truePara, funcData = setDataRtIrt, funcGibbs = GibbsRtIrt, par = c("a", "b", "theta"), ... )setCond( nSubj = 2000, nItem = 15, nFeat = 3, nIter = 5000, nChain = 4, nBurnin = NULL, nThin = 1, nRep = 10, qRa = 0.5, qRt = 0.5 ) InputData(Y, T = NULL, X = NULL) InputData4R(Y, kappa = Y - 0.5, logT = NULL, X = NULL) InputPara(...) GibbsMlIrt(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtNull(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrt(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtLatent(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtLatentQr(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtCross(Cond, Data, truePara = NULL, Para = NULL) GibbsRtIrtCrossQr(Cond, Data, truePara = NULL, Para = NULL) sampleGibbs(MCMC, cov2one = NULL, ...) getDic(MCMC) checkConvergence(MCMC, ...) comparePara(MCMC, par = "a") getBias(est, true) getRmse(est, true) getCorr(est, true) setTrueParaMlIrt(Cond) setTrueParaRtIrt(Cond, trueCorr = 0.3) setTrueParaRtIrtLatent(Cond) setTrueParaRtIrtCross(Cond) setDataMlIrt(Cond, truePara) setDataRtIrt(Cond, truePara) setDataRtIrtNull(Cond, truePara) setDataRtIrtLatent(Cond, truePara, type = "norm") setDataRtIrtCross(Cond, truePara, type = "norm") runSimulation( Cond, truePara, funcData = setDataRtIrt, funcGibbs = GibbsRtIrt, par = c("a", "b", "theta"), ... )
nSubj, nItem, nFeat, nIter, nChain, nBurnin, nThin, nRep, qRa, qRt
|
As in |
Y, T, X, kappa, logT
|
Data, as in |
Cond, Data, truePara, Para, MCMC, par, est, true, ...
|
As in the Julia package. |
cov2one |
Julia's setting: |
trueCorr, type, funcData, funcGibbs
|
As in the Julia functions. |
| Julia | birtRcpp |
setCond(nSubj, nItem, nFeat, nIter, nChain, nBurnin, nThin, nRep, qRa, qRt) |
set_cond() |
InputData(Y, T, X), InputData4R(Y, kappa, logT, X) |
input_data() (resp, time, cov, log_time) |
InputPara(...) |
input_para() |
GibbsMlIrt, GibbsRtIrtNull, GibbsRtIrt |
mlirt(), rtirt_null(), rtirt_latreg() |
GibbsRtIrtLatent, GibbsRtIrtLatentQr |
rtirt_latent(), quantile = NULL / Cond$q_rt |
GibbsRtIrtCross, GibbsRtIrtCrossQr |
rtirt_cross(), quantile = NULL / Cond$q_rt |
sample!(MCMC) |
sampleGibbs(MCMC) = gibbs() |
coef(MCMC), precis(MCMC) |
summary(), precis() |
getDic, checkConvergence, comparePara |
dic(), convergence(), compare_para() |
getBias, getRmse, getCorr |
get_bias(), get_rmse(), get_corr() |
setTruePara<Model>, setData<Model> |
sim_para(), sim_data() |
runSimulation(Cond, truePara; funcData, funcGibbs) |
run_simulation()
|
The constructors take (Cond, Data, truePara, Para) as in Julia and keep the Julia
defaults: the speed variance is fixed at 1 (speed_var = "fixed") in GibbsRtIrtNull,
GibbsRtIrt and GibbsRtIrtCross (sampleGibbs(M, cov2one = FALSE) frees it), and the
Qr versions use the quantile level Cond$q_rt. setCond() keeps the Julia default
dimensions (2000 persons, 15 items, 3 covariates), which must match the data.
A Metropolis–Hastings step whose proposal follows the gradient of the log density (MALA;
Roberts & Tweedie, 1996): ,
accepted with the usual MH probability (the proposal is not symmetric, so its density in both
directions enters the ratio). It replaces a random-walk Metropolis step where a block has no
conjugate full conditional: it moves in the direction of higher density and needs far fewer
rejections, especially in several dimensions (optimal acceptance about 0.574 against 0.234).
draw_mala(x, logpost, step, joint = FALSE) mcmc_adapt(step, accepted, iter, target = 0.574)draw_mala(x, logpost, step, joint = FALSE) mcmc_adapt(step, accepted, iter, target = 0.574)
x |
Current values: a vector (n scalar targets, or one target with |
logpost |
Function of |
step |
Step size(s) |
joint |
|
accepted |
Logical (or acceptance rate) of the last step(s). |
iter |
Iteration number (for the decreasing adaptation rate). |
target |
Target acceptance rate. |
draw_mala(x, logpost, step, joint): by default one step for many
independent targets at once, one per row of x (or per element of a vector x, e.g. one per
person); logpost(x) returns list(lp = , grad = ): the log density of every row (a vector)
and its gradient (like x); step is one number or one per row. With joint = TRUE, x
is one target and lp one number. Returns x with the attribute accepted.
A parameter on a transformed scale (e.g. u = log a) needs the log Jacobian in lp (+ u)
and in grad (+ 1); variables integrated out by the proposal (e.g. Pólya–Gamma omega) must be
drawn anew before the next step uses them.
mcmc_adapt(step, accepted, iter, target): Robbins–Monro adaptation of the step size during
burn-in (Andrieu & Thoms, 2008), ; stop adapting after burn-in so that the chain is a proper Markov chain.
draw_cor() uses it with method = "mala".
draw_mala(): the new x with attribute accepted; mcmc_adapt(): the new step(s).
Roberts, G. O., & Tweedie, R. L. (1996). Exponential convergence of Langevin distributions and their discrete approximations. Bernoulli, 2, 341-363.
Andrieu, C., & Thoms, J. (2008). A tutorial on adaptive MCMC. Statistics and Computing, 18, 343-373.
# abilities of a 2PL with known items: one MALA step per person, all persons at once set.seed(1); N <- 200; a <- c(1, 1.5, 2); b <- c(-1, 0, 1) theta0 <- rnorm(N); Y <- matrix(rbinom(N * 3, 1, plogis(sweep(outer(theta0, a), 2, a * b))), N) lp <- function(th) { p <- plogis(sweep(outer(th, a), 2, a * b)) list(lp = rowSums(Y * log(p) + (1 - Y) * log(1 - p)) - th^2 / 2, grad = drop((Y - p) %*% a) - th) } th <- rep(0, N); eps <- rep(1, N) for (t in 1:2000) { th <- draw_mala(th, lp, eps) if (t <= 500) eps <- mcmc_adapt(eps, attr(th, "accepted"), t) } mean(attr(th, "accepted"))# abilities of a 2PL with known items: one MALA step per person, all persons at once set.seed(1); N <- 200; a <- c(1, 1.5, 2); b <- c(-1, 0, 1) theta0 <- rnorm(N); Y <- matrix(rbinom(N * 3, 1, plogis(sweep(outer(theta0, a), 2, a * b))), N) lp <- function(th) { p <- plogis(sweep(outer(th, a), 2, a * b)) list(lp = rowSums(Y * log(p) + (1 - Y) * log(1 - p)) - th^2 / 2, grad = drop((Y - p) %*% a) - th) } th <- rep(0, N); eps <- rep(1, N) for (t in 1:2000) { th <- draw_mala(th, lp, eps) if (t <= 500) eps <- mcmc_adapt(eps, attr(th, "accepted"), t) } mean(attr(th, "accepted"))
marginal_loglik() returns the log-likelihood of each person with the person parameters
(ability, speed) integrated out, for posterior draws of the item and structural
parameters. fit_indices() uses it for DIC, WAIC and PSIS-LOO (with the 'loo' package);
these, unlike the complete-data DIC of dic(), compare models with different latent
structures (Merkle, Furr & Rabe-Hesketh, 2019). Ability is integrated by Gauss-Hermite
quadrature and speed in closed form (normal log times; normal-ALD convolution for
rtirt_latent quantile models; piecewise-linear ALD log-likelihood for rtirt_cross). Quantile models use the ALD as a
working likelihood, so their criteria are not comparable with mean models or across levels.
marginal_loglik(object, n_draws = NULL, Q = NULL)marginal_loglik(object, n_draws = NULL, Q = NULL)
object |
A sampled model. |
n_draws |
Number of posterior draws (after burn-in, evenly spaced; default 200, 100 for
|
Q |
Gauss-Hermite nodes for ability (default 41; 61 for |
A draws x persons matrix (printed with 3 decimals; see rtirt_tbl).
Deprecated with block_model(): use the built-in models, ecm(data, model = ...).
mml(model, ...) ## S3 method for class 'block_model' mml( model, latent = NULL, nodes = NULL, prior = c("map", "ml"), init = NULL, cycles = 6, max_iter = 1000, tol = 1e-09, se = TRUE, analytic = TRUE, quadrature = c("adaptive", "ba81"), method = c("newton", "quasi-newton"), ... )mml(model, ...) ## S3 method for class 'block_model' mml( model, latent = NULL, nodes = NULL, prior = c("map", "ml"), init = NULL, cycles = 6, max_iter = 1000, tol = 1e-09, se = TRUE, analytic = TRUE, quadrature = c("adaptive", "ba81"), method = c("newton", "quasi-newton"), ... )
model |
|
... |
Unused. |
latent |
Parameters integrated out in the E-step (one to three |
nodes |
Quadrature points per latent variable (default 21, 11 or 7 for one, two or three); one
number, or one per latent variable (named, e.g. |
prior |
|
init |
Optional starting values (named list). |
cycles |
Maximum number of recentring rounds of the quadrature. |
max_iter, tol
|
Passed to |
se |
Standard errors from the Hessian of the objective. |
analytic |
Integrate latent variables that enter the model only through normal parts, linearly (e.g. speed in a log-normal RT model), exactly given the others instead of on the grid. |
quadrature |
|
method |
|
Maximizes the objective of ecm.block_model() (the marginal log-likelihood, or the log posterior
with prior = "map") directly with stats::nlminb() (within the bounds of the parameters), all
parameters together, with the gradient from Fisher's identity and, for method = "newton", the
Hessian from Louis' formula (Glas & van der Linden, 2010, Sec. 3.1, use Newton-Raphson or EM). No
closed-form step is needed, only the derivatives of the declared expressions, so this helps when a
parameter has no closed-form step. It reaches the same maximum as ecm() inside the bounds of the
parameters (ECM keeps a bounded parameter 1e-3 inside its bound; a parameter at its bound gets no
standard error, with a warning); ECM never lowers the objective and its iterations are cheap,
Newton needs few iterations.
Glas, C. A. W., & van der Linden, W. J. (2010). Marginal likelihood inference for a model for item responses and response times. British Journal of Mathematical and Statistical Psychology, 63, 603-626.
Deprecated: name the model in ecm() or gibbs() instead, e.g.
rtirt_cross(dat) becomes ecm(dat, model = "cross") or gibbs(dat, model = "cross").
The constructors still work (with a warning once per session) and get no new features.
mlirt( data, itemtype = c("2pl", "1pl"), intercept = FALSE, cond = NULL, true_para = NULL, para = NULL ) rtirt_null( data, speed_var = c("free", "fixed"), itemtype = c("2pl", "1pl"), cond = NULL, true_para = NULL, para = NULL ) rtirt_latreg( data, speed_var = c("free", "fixed"), itemtype = c("2pl", "1pl"), intercept = FALSE, cond = NULL, true_para = NULL, para = NULL ) rtirt_latent( data, quantile = NULL, itemtype = c("2pl", "1pl"), intercept = FALSE, cond = NULL, true_para = NULL, para = NULL ) rtirt_cross( data, quantile = NULL, speed_var = c("free", "fixed"), itemtype = c("2pl", "1pl"), cond = NULL, true_para = NULL, para = NULL )mlirt( data, itemtype = c("2pl", "1pl"), intercept = FALSE, cond = NULL, true_para = NULL, para = NULL ) rtirt_null( data, speed_var = c("free", "fixed"), itemtype = c("2pl", "1pl"), cond = NULL, true_para = NULL, para = NULL ) rtirt_latreg( data, speed_var = c("free", "fixed"), itemtype = c("2pl", "1pl"), intercept = FALSE, cond = NULL, true_para = NULL, para = NULL ) rtirt_latent( data, quantile = NULL, itemtype = c("2pl", "1pl"), intercept = FALSE, cond = NULL, true_para = NULL, para = NULL ) rtirt_cross( data, quantile = NULL, speed_var = c("free", "fixed"), itemtype = c("2pl", "1pl"), cond = NULL, true_para = NULL, para = NULL )
data |
Data from |
itemtype |
|
intercept |
Add an intercept to the latent regressions (default |
cond |
Optional |
true_para |
Optional true parameters ( |
para |
Optional initial values (not needed; each chain draws its own). |
speed_var |
|
quantile |
|
fit <- gibbs(dat, model = "cross", quantile = 0.25, n_iter = 10000, n_chain = 3, seed = 1)
mlirt: 2PL with a latent regression of ability (no response times).
rtirt_null: joint RT-IRT (van der Linden, 2007), ability and speed correlated.
rtirt_latreg: joint RT-IRT with latent regressions of ability and speed on covariates
(dissertation Chapter 3).
rtirt_latent: speed regressed on ability and covariates, mean regression or, with
quantile, quantile regression (ALD; Chapter 4).
rtirt_cross: cross-relations of ability with each log response time, normal or, with
quantile, ALD residuals (Chapter 5).
The Julia constructors (GibbsRtIrtCross(Cond, Data) etc.) are aliases with the Julia
argument order and defaults (see julia_aliases).
A model object of class c(<model>, "rtirt"); see gibbs().
Most full conditionals of the samplers are normal. A normal part stores one as its precision
and its linear term (prec and num; mean , variance
), so that the contributions of the data and of the prior simply add up:
normal_part(prec, num) normal_prior(mean = 0, sd = 1) reg_part(X, y, w = 1) ## S3 method for class 'normal_part' e1 + e2 draw(part, lower = -Inf, upper = Inf) ## S3 method for class 'normal_part' print(x, ...)normal_part(prec, num) normal_prior(mean = 0, sd = 1) reg_part(X, y, w = 1) ## S3 method for class 'normal_part' e1 + e2 draw(part, lower = -Inf, upper = Inf) ## S3 method for class 'normal_part' print(x, ...)
prec, num
|
Precision (a vector, or a matrix for a regression part) and linear term. |
mean, sd
|
Prior mean and SD (recycled). |
X |
Design matrix (one row per observation). |
y |
Observations. |
w |
Precisions of the observations (one value or one per observation). |
e1, e2
|
Normal parts. |
part, x
|
A normal part. |
lower, upper
|
Truncation bounds. |
... |
Unused. |
irt_part() and calc_normal_part() return the data part of independent scalars (one per
person or item);
reg_part(X, y, w) returns the data part of a vector of regression coefficients in
, (,
), e.g. a latent regression of the abilities on covariates;
normal_prior(mean, sd) turns a normal prior into a part (, );
part1 + part2 adds parts (elementwise, or a prior added to the diagonal of a regression part);
draw(part, lower, upper) draws from it (truncated to [lower, upper] when given;
a regression part is drawn with draw_mvn());
print(part) shows the conditional means and SDs.
theta <- draw(irt_part(Y, omega, a, b) + normal_prior(mean = X %*% g, sd = 1)) g <- draw(reg_part(X, theta) + normal_prior(0, 10))
normal_prior(), normal_part(), reg_part() and +: a normal part; draw(): a vector of draws.
set.seed(1) N <- 200; a <- c(1, 1.5, 2); b <- c(-1, 0, 1); th <- rnorm(N) Y <- matrix(rbinom(N * 3, 1, plogis(sweep(outer(th, a), 2, a * b))), N) om <- draw_omega(a, b, th) p <- irt_part(Y, om, a, b) + normal_prior(0, 1) # data part + prior N(0, 1) p theta <- draw(p) draw(normal_prior(1, 1), lower = 0) # N+(1, 1) X <- cbind(1, rnorm(N)) draw(reg_part(X, theta) + normal_prior(0, 10)) # coefficients of theta on Xset.seed(1) N <- 200; a <- c(1, 1.5, 2); b <- c(-1, 0, 1); th <- rnorm(N) Y <- matrix(rbinom(N * 3, 1, plogis(sweep(outer(th, a), 2, a * b))), N) om <- draw_omega(a, b, th) p <- irt_part(Y, om, a, b) + normal_prior(0, 1) # data part + prior N(0, 1) p theta <- draw(p) draw(normal_prior(1, 1), lower = 0) # N+(1, 1) X <- cbind(1, rnorm(N)) draw(reg_part(X, theta) + normal_prior(0, 10)) # coefficients of theta on X
The person-fit statistic of Marianti, Fox, Avetisyan, Veldkamp & Tijmstra (2014) for the lognormal
RT model: ,
chi-square with K degrees of freedom when the model holds. The samplers of gibbs() evaluate it in
every iteration after burn-in, so the result accounts for the uncertainty of all parameters:
lt: posterior mean of ;
p_flag: posterior probability that exceeds the 95% quantile of chi-square(K), the
probability that the pattern is flagged; Marianti et al. flag a pattern when it is at least .95.
person_fit(object, flag = 0.95, ...)person_fit(object, flag = 0.95, ...)
object |
A model sampled by |
flag |
Posterior probability above which a pattern is flagged. |
... |
Unused. |
Large values point to aberrant timing: rapid guessing, item preknowledge, running out of time,
or a speed that changes during the test. Normal log times only (not gibbs(model = "cross", quantile = )).
A data frame (class rtirt_tbl, printed with 3 decimals) with id, lt, df, p_flag
and flagged; attribute share (share of flagged test takers) and share_mean (share with lt
above the 95% quantile).
Marianti, S., Fox, J.-P., Avetisyan, M., Veldkamp, B. P., & Tijmstra, J. (2014). Testing for aberrant behavior in response time modeling. Journal of Educational and Behavioral Statistics, 39, 426-451.
Fox, J.-P., & Marianti, S. (2017). Person-fit statistics for joint models for accuracy and speed. Journal of Educational Measurement, 54, 243-262.
cond <- set_cond(n_subj = 300, n_item = 10) d <- sim_data(cond, sim_para(cond, "cross"), "cross") f <- gibbs(d, model = "null", n_iter = 2000, n_chain = 2, seed = 1, control = gibbs_control(verbose = FALSE, fit_indices = FALSE) ) pf <- person_fit(f) attr(pf, "share") # close to 0 under the modelcond <- set_cond(n_subj = 300, n_item = 10) d <- sim_data(cond, sim_para(cond, "cross"), "cross") f <- gibbs(d, model = "null", n_iter = 2000, n_chain = 2, seed = 1, control = gibbs_control(verbose = FALSE, fit_indices = FALSE) ) pf <- person_fit(f) attr(pf, "share") # close to 0 under the model
type = "items": posterior means and 95% intervals of the item parameters (ECM fits: estimates
and Wald 95% intervals), one panel
per parameter type (a, b, lambda, sigma2t, rho). type = "structural": the same for the
structural parameters (regression coefficients, cor_ability_speed, b_ability,
var_speed), or those matching pars. type = "trace": trace plots (all
iterations; burn-in shaded) of the structural parameters or of those matching pars.
For plots of your own (e.g. with ggplot2), estimates(fit) (or as.data.frame(fit)) has
the estimates and intervals as a data frame.
## S3 method for class 'rtirt' plot(x, type = c("items", "structural", "trace"), pars = NULL, ...)## S3 method for class 'rtirt' plot(x, type = c("items", "structural", "trace"), pars = NULL, ...)
x |
A sampled model. |
type |
|
pars |
Regular expression selecting parameters ( |
... |
Passed to |
Statistics of Sinharay (2020) for test takers who may have known some items in advance (a set of
possibly compromised items is given), computed from the item parameters of a fit of a model with
response times (ecm() estimates, or the posterior means of gibbs()), with the lognormal RT model
and the 2PL for the scores:
chi_pf: RT person fit with the
speed estimated from the times, chi-square with K - 1 df (it does not use the compromised set);
Lambda_s: signed likelihood ratio (a z statistic) comparing the speed on the compromised items with
the speed on the other items, ,
; large values: faster on the compromised items;
L_s: signed likelihood ratio of the item scores comparing the ability (maximum likelihood) on the
two sets; large values: better on the compromised items.
The p-values are one-sided for Lambda_s and L_s (standard normal) and upper-tail for chi_pf.
Models with the speed entering as : model = "null", "latreg" and
"latent" (mean model).
preknowledge_test(object, compromised, ...)preknowledge_test(object, compromised, ...)
object |
|
compromised |
Items in the compromised set (names or a logical / integer index). |
... |
Unused. |
A data frame (class rtirt_tbl, printed with 3 decimals) with one row per test taker: the three statistics and their p-values.
Sinharay, S. (2020). Detection of item preknowledge using response times. Applied Psychological Measurement, 44, 376-392.
cond <- set_cond(n_subj = 400, n_item = 20) d <- sim_data(cond, sim_para(cond, "null"), "null") pk <- preknowledge_test(ecm(d, model = "null", se = FALSE), compromised = 1:5) colMeans(pk[c("p_chi_pf", "p_Lambda_s", "p_L_s")] < 0.01) # about .01 under the modelcond <- set_cond(n_subj = 400, n_item = 20) d <- sim_data(cond, sim_para(cond, "null"), "null") pk <- preknowledge_test(ecm(d, model = "null", se = FALSE), compromised = 1:5) colMeans(pk[c("p_chi_pf", "p_Lambda_s", "p_L_s")] < 0.01) # about .01 under the model
var(EAP) / (var(EAP) + mean PSD^2) of ability and speed.
reliability(object, ...) ## S3 method for class 'rtirt' reliability(object, ...)reliability(object, ...) ## S3 method for class 'rtirt' reliability(object, ...)
object |
A sampled model. |
... |
Unused. |
Sets the prior distributions used by gibbs() (and, for the item parameters, by
ecm(prior = "map")). The defaults are common weakly informative priors that do not depend on the
data, and every full conditional of the samplers stays conjugate (pure Gibbs):
rtirt_priors( a = c(mean = 1, sd = 1), b = c(mean = 0, sd = 3), lambda = c(mean = 0, sd = 10), sigma2t = c(df = 3, scale = 1), beta = c(sd = 1), rho = c(sd = 1), var_speed = c(df = 3, scale = 1), cov = c(sd = 1) ) rtirt_priors_julia()rtirt_priors( a = c(mean = 1, sd = 1), b = c(mean = 0, sd = 3), lambda = c(mean = 0, sd = 10), sigma2t = c(df = 3, scale = 1), beta = c(sd = 1), rho = c(sd = 1), var_speed = c(df = 3, scale = 1), cov = c(sd = 1) ) rtirt_priors_julia()
a |
Discrimination, normal on (0, Inf): |
b |
Difficulty, normal: |
lambda |
Time intensity, normal: |
sigma2t |
Residual variance (or ALD scale) of the log response times: |
beta |
Regression coefficients of the latent regressions (including |
rho |
Cross-relations of |
var_speed |
Speed variance ( |
cov |
Covariance |
| parameter | default | Gibbs step |
| a (discrimination) | N(1, 1) on (0, Inf) | truncated normal |
| b (difficulty) | N(0, 3^2), not truncated | normal |
| lambda (time intensity) | N(0, 10^2), not truncated | normal |
| sigma2t (residual variance of log RT) | its SD ~ half-t(3, 1) | two inverse-gamma steps |
| speed (residual) variance | its SD ~ half-t(3, 1) | two inverse-gamma steps |
| regression coefficients, rho, c | N(0, 1) | normal |
The half-t prior on a standard deviation (Gelman, 2006) is the scale mixture of Huang & Wand (2013): var | u ~ IG(df / 2, df / u), u ~ IG(1 / 2, 1 / scale^2). The samplers draw u | var ~ IG((df + 1) / 2, df / var + 1 / scale^2) and then var | u, data ~ IG(df / 2 + n / 2, df / u + SS / 2), both conjugate. The auxiliary u is drawn afresh from the current variance in every iteration, so it is not stored.
rtirt_priors_julia() gives the priors of ExtendedRtIrtModeling.jl (Tsai, 2025), with which the
dissertation results are reproduced: a ~ N+(1, 1), b ~ N(0, 1) on [-4, 4], lambda ~ N+ centred
at the mean and SD of the log response times, inverse-gamma(0.001, 0.001) variances.
A variance with c(df, scale) has a half-t prior on its square root and with c(shape, scale)
an inverse-gamma prior on the variance (one conjugate step).
An object of class rtirt_priors.
Gelman, A. (2006). Prior distributions for variance parameters in hierarchical models. Bayesian Analysis, 1, 515-534.
Huang, A., & Wand, M. P. (2013). Simple marginally noninformative prior distributions for covariance matrices. Bayesian Analysis, 8, 439-452.
rtirt_priors() # the defaults rtirt_priors(a = c(mean = 1, sd = 0.5)) # a tighter discrimination prior rtirt_priors_julia() # the priors of the dissertationrtirt_priors() # the defaults rtirt_priors(a = c(mean = 1, sd = 0.5)) # a tighter discrimination prior rtirt_priors_julia() # the priors of the dissertation
fits <- gibbs(data, model = "cross", quantile = c(.1, .5, .9)) (or model = "latent")
samples one ALD model per level and returns them as an rtirt_qset;
ecm(data, model = "cross", quantile = c(.1, .5, .9)) fits them by maximum (working)
likelihood. coef() gives a parameters x
levels matrix, estimates() stacks the tables with a column q, and plot() draws the
quantile coefficients (cross-relations rho, or the regression coefficients of
"latent") against the level with 95% intervals (posterior intervals, or Wald intervals from
the sandwich standard errors of ecm()). convergence() checks every level.
summary() returns an object that prints as print().
The fit of one level is fits$models[[k]] (an ordinary fit: summary(), estimates(),
plot(type = "trace") for a sampled fit).
## S3 method for class 'rtirt_qset' print(x, ...) ## S3 method for class 'rtirt_qset' coef(object, ...) ## S3 method for class 'rtirt_qset' estimates(object, ...) ## S3 method for class 'rtirt_qset' summary(object, ...) ## S3 method for class 'summary.rtirt_qset' print(x, ...) ## S3 method for class 'rtirt_qset' plot(x, pars = NULL, ...)## S3 method for class 'rtirt_qset' print(x, ...) ## S3 method for class 'rtirt_qset' coef(object, ...) ## S3 method for class 'rtirt_qset' estimates(object, ...) ## S3 method for class 'rtirt_qset' summary(object, ...) ## S3 method for class 'summary.rtirt_qset' print(x, ...) ## S3 method for class 'rtirt_qset' plot(x, pars = NULL, ...)
x, object
|
An |
... |
Unused, or passed to |
pars |
Regular expression of the parameters to plot (default: |
summary() of a sampled model (or one fitted by ecm()) returns its tables as one
object: header, fit (fit indices), items and se (one row per item: a, b, lambda,
sigma2t, rho and their posterior SDs or standard errors), parameters (one row per
parameter with its interval and, for Gibbs fits, its prior, as in blavaan), covariance
(person covariance) and reliability. Each element is a tibble; as.data.frame()
returns parameters. print() shows the tables under their names, so s$items gets the
one printed as $items.
## S3 method for class 'rtirt_summary' print(x, tables = NULL, digits = 3, ...) ## S3 method for class 'rtirt_summary' as.data.frame(x, row.names = NULL, optional = FALSE, ...) ## S3 method for class 'rtirt_table' print(x, digits = 3, title = TRUE, ...)## S3 method for class 'rtirt_summary' print(x, tables = NULL, digits = 3, ...) ## S3 method for class 'rtirt_summary' as.data.frame(x, row.names = NULL, optional = FALSE, ...) ## S3 method for class 'rtirt_table' print(x, digits = 3, title = TRUE, ...)
x |
An |
tables |
Names of the tables to print (default all); an unknown name is an error. |
digits |
Decimals. |
... |
Unused. |
row.names, optional
|
Unused. |
title |
Print the title of a table. |
print() returns x invisibly; as.data.frame() the parameter table.
The tables of estimates(), scores(), fit_indices(), compare_para(), person_fit(),
preknowledge_test(), dic() and run_simulation() are tibbles (or data frames) of class
rtirt_tbl. They print numbers with 3 decimals (numbers of 1e10 or more, such as the bound of
a diverging estimate, as e.g. 5.950e+99; counts without decimals, p-values below 0.001
as <0.001, NA blank), and all rows of a table with up to 40 rows (else the first 20). The
values are not rounded: x$est, dplyr verbs and as.data.frame(x) have the full precision.
Named vectors and matrices of numbers (coef(), reliability(),
marginal_loglik()) print with 3 decimals as well.
## S3 method for class 'rtirt_tbl' format(x, digits = 3, n = NULL, ...) ## S3 method for class 'rtirt_tbl' print(x, digits = 3, n = NULL, ...) ## S3 method for class 'rtirt_values' print(x, digits = 3, ...)## S3 method for class 'rtirt_tbl' format(x, digits = 3, n = NULL, ...) ## S3 method for class 'rtirt_tbl' print(x, digits = 3, n = NULL, ...) ## S3 method for class 'rtirt_values' print(x, digits = 3, ...)
x |
A table of birtRcpp. |
digits |
Decimals. |
n |
Rows to show (default: all up to 40 rows, else 20); |
... |
Unused. |
format() the lines; print() returns x invisibly.
cond <- set_cond(n_subj = 200, n_item = 5) fit <- ecm(sim_data(cond, sim_para(cond, "null"), "null"), model = "null", se = FALSE) est <- estimates(fit) est # 3 decimals est$est[1] # full precision as.data.frame(est)[1:2, ]cond <- set_cond(n_subj = 200, n_item = 5) fit <- ecm(sim_data(cond, sim_para(cond, "null"), "null"), model = "null", se = FALSE) est <- estimates(fit) est # 3 decimals est$est[1] # full precision as.data.frame(est)[1:2, ]
The EM counterpart of run_sampler(): the algorithm is one function step(s, data) that does one
E- and M-step from the current state s (a named list of parameters) and returns the new state
with the log-likelihood at the input state in s$.loglik. run_em() packs the state into
a vector (log scale for the positive parameters), accelerates with SQUAREM, checks convergence
and computes standard errors from the numerical Hessian of loglik().
run_em( data, init, step, loglik = NULL, positive = character(), se = !is.null(loglik), max_iter = 1000, tol = 1e-10, accelerate = TRUE, score = NULL, hessian = NULL ) ## S3 method for class 'block_em' print(x, digits = 3, ...) ## S3 method for class 'block_em' summary(object, ...) estep_irt(Y, a, b, mean = 0, sd = 1, nodes = calc_nodes(41)) mstep_items(e, a, b, priors = NULL, b_mean = NULL, one_pl = FALSE)run_em( data, init, step, loglik = NULL, positive = character(), se = !is.null(loglik), max_iter = 1000, tol = 1e-10, accelerate = TRUE, score = NULL, hessian = NULL ) ## S3 method for class 'block_em' print(x, digits = 3, ...) ## S3 method for class 'block_em' summary(object, ...) estep_irt(Y, a, b, mean = 0, sd = 1, nodes = calc_nodes(41)) mstep_items(e, a, b, priors = NULL, b_mean = NULL, one_pl = FALSE)
data |
A list of data. |
init |
Function of |
step |
Function of |
loglik |
Function of |
positive |
Names of the parameters kept positive (estimated on the log scale). |
se |
Compute standard errors. |
max_iter, tol, accelerate
|
As |
score |
Optional function of |
hessian |
Optional function of |
digits |
Number of decimals. |
... |
Unused. |
object, x
|
A |
Y |
Responses (0/1, NA missing), N x K. |
a, b
|
Item parameters. |
mean, sd
|
Mean (one value or one per person) and SD of the ability distribution. |
nodes |
Nodes from |
e |
Result of |
priors |
Item priors from |
b_mean |
Prior means of b, one per item ( |
one_pl |
Keep a fixed. |
For posterior modes (priors in the M-step), s$.loglik and loglik() are the log-likelihood
plus the log prior: the objective that the M-step increases.
run_em() checks the step, because a wrong M-step still converges, to the wrong values. It warns
when the objective decreases between iterations (an EM step never lowers it), when s$.loglik
differs from loglik() at the starting values, and when the estimates are not a maximum of
loglik() (score statistic ).
For item response models the E- and M-steps are ready-made:
estep_irt(Y, a, b, mean, sd, nodes): the Gauss-Hermite E-step of a 2PL with
(missing responses skipped): posterior weights
of the nodes W, the marginal log-likelihood loglik, the EAPs and posterior SDs, and the
Pólya–Gamma sums for the M-step.
mstep_items(e, a, b, priors, b_mean, one_pl): the item parameters from the E-step (Pólya–Gamma
minorizer: never lowers the objective); maximum likelihood, or the posterior mode with the item
priors of priors = rtirt_priors(a = , b = ) and, optionally, a prior mean of b per item
(b_mean), as draw_items_pg().
The components behind them are in em_components.
run_em(): a block_em with estimates (estimate and SE), state, loglik,
iterations, converged, trace and vcov (working scale). estep_irt(): a list;
mstep_items(): list(a, b).
set.seed(1) N <- 1000; K <- 8; a0 <- runif(K, 0.8, 2); b0 <- rnorm(K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(rnorm(N), a0), 2, a0 * b0))), N) fit <- run_em( data = list(Y = Y), init = function(d) list(a = rep(1, ncol(d$Y)), b = rep(0, ncol(d$Y))), step = function(s, d) { e <- estep_irt(d$Y, s$a, s$b) # E-step s[c("a", "b")] <- mstep_items(e, s$a, s$b) # M-step s$.loglik <- e$loglik s }, loglik = function(s, d) estep_irt(d$Y, s$a, s$b)$loglik, positive = "a") fitset.seed(1) N <- 1000; K <- 8; a0 <- runif(K, 0.8, 2); b0 <- rnorm(K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(rnorm(N), a0), 2, a0 * b0))), N) fit <- run_em( data = list(Y = Y), init = function(d) list(a = rep(1, ncol(d$Y)), b = rep(0, ncol(d$Y))), step = function(s, d) { e <- estep_irt(d$Y, s$a, s$b) # E-step s[c("a", "b")] <- mstep_items(e, s$a, s$b) # M-step s$.loglik <- e$loglik s }, loglik = function(s, d) estep_irt(d$Y, s$a, s$b)$loglik, positive = "a") fit
The loop around a sampler: chains, burn-in, thinning, storage, running means of person
parameters and the summary table, as in JAGS. The sampler is one function step(s, data) that
takes the current state s (a named list of everything the sampler updates) and returns the new
state, as in a JAGS model block written out by hand.
run_sampler( data, init, step, monitor, n_iter = 2000, n_burnin = floor(n_iter/2), n_chain = 2, n_thin = 1, seed = 1, keep_mean = NULL ) ## S3 method for class 'block_fit' summary(object, ...) ## S3 method for class 'block_fit' print(x, digits = 3, ...) ## S3 method for class 'block_fit' as.matrix(x, ...)run_sampler( data, init, step, monitor, n_iter = 2000, n_burnin = floor(n_iter/2), n_chain = 2, n_thin = 1, seed = 1, keep_mean = NULL ) ## S3 method for class 'block_fit' summary(object, ...) ## S3 method for class 'block_fit' print(x, digits = 3, ...) ## S3 method for class 'block_fit' as.matrix(x, ...)
data |
A list of data (passed to |
init |
Function of |
step |
Function of |
monitor |
Names of the elements of the state to store. |
n_iter, n_burnin, n_thin
|
Iterations per chain, burn-in and thinning (as |
n_chain |
Number of chains. |
seed |
Seed of the first chain (chain c uses |
keep_mean |
Names of elements whose posterior mean and SD are kept. |
object, x
|
A |
... |
Unused. |
digits |
Number of decimals. |
init(data) returns the starting state (called once per chain, after set.seed(), so random
starting values differ between chains).
step(s, data) updates every block once and returns s. During burn-in s$.burn is TRUE
and s$.iter is the iteration, for step-size adaptation (mcmc_adapt()).
monitor: the names of the elements of s to store (vectors are stored as a[1], a[2], ...).
keep_mean: the names of elements (e.g. person parameters) of which only the posterior mean and
SD are kept.
A block_fit: draws (a coda::mcmc.list), mean (posterior means and SDs of
keep_mean), last (the last state of every chain) and time. summary() gives the table of
posterior means, SDs, 95% intervals, R-hat and effective sample sizes; as.matrix() the draws.
print() warns when an R-hat exceeds 1.05 or an effective sample size is below 100, and names
the usual cures.
check_sampler() to check that the sampler draws from the right posterior, run_em().
set.seed(1) N <- 300; K <- 6; a0 <- runif(K, 0.8, 2); b0 <- rnorm(K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(rnorm(N), a0), 2, a0 * b0))), N) fit <- run_sampler( data = list(Y = Y), init = function(d) list(theta = rnorm(nrow(d$Y)), a = rep(1, ncol(d$Y)), b = rep(0, ncol(d$Y))), step = function(s, d) { s$omega <- draw_omega(s$a, s$b, s$theta) # Polya-Gamma s[c("a", "b")] <- draw_items_pg(d$Y, s$omega, s$theta, s$a) # b, then a s$theta <- draw(irt_part(d$Y, s$omega, s$a, s$b) + normal_prior(0, 1)) s }, monitor = c("a", "b"), keep_mean = "theta", n_iter = 1000) fitset.seed(1) N <- 300; K <- 6; a0 <- runif(K, 0.8, 2); b0 <- rnorm(K) Y <- matrix(rbinom(N * K, 1, plogis(sweep(outer(rnorm(N), a0), 2, a0 * b0))), N) fit <- run_sampler( data = list(Y = Y), init = function(d) list(theta = rnorm(nrow(d$Y)), a = rep(1, ncol(d$Y)), b = rep(0, ncol(d$Y))), step = function(s, d) { s$omega <- draw_omega(s$a, s$b, s$theta) # Polya-Gamma s[c("a", "b")] <- draw_items_pg(d$Y, s$omega, s$theta, s$a) # b, then a s$theta <- draw(irt_part(d$Y, s$omega, s$a, s$b) + normal_prior(0, 1)) s }, monitor = c("a", "b"), keep_mean = "theta", n_iter = 1000) fit
As runSimulation() in Julia: cond$n_rep replications with fixed item parameters and new
persons (and data) in each replication; returns bias, RMSE and correlation per parameter
block and replication.
run_simulation( cond, para, model = sim_models, quantile = NULL, pars = c("a", "b", "ability"), type = "norm", speed_var = c("free", "fixed"), ... )run_simulation( cond, para, model = sim_models, quantile = NULL, pars = c("a", "b", "ability"), type = "norm", speed_var = c("free", "fixed"), ... )
cond |
Conditions. |
para |
True parameters from |
model |
Model name as in |
quantile |
Quantile level of the ALD version ( |
pars |
Parameter blocks to evaluate. |
type |
Residual distribution of the simulated data. |
speed_var |
Variance of speed ( |
... |
Passed to |
A tibble (class rtirt_tbl, printed with 3 decimals) with one row per replication and parameter block.
Tests whether item or structural parameters change along a person-level moderator (DIF,
measurement invariance, item position effects) from a single ecm() fit: the casewise scores
(estfun.rtirt()) are ordered by the moderator, cumulated after decorrelation, and compared with a
Brownian bridge (Merkle & Zeileis, 2013; for IRT, Schneider, Strobl, Zeileis & Debelak, 2022).
score_test(object, ...) ## Default S3 method: score_test(object, ...) ## S3 method for class 'rtirt' score_test( object, moderator, parm = "accuracy", by_item = FALSE, test = c("auto", "DM", "CvM", "maxLM", "LM"), decorrelate = c("block", "full"), cluster = NULL, impact_check = TRUE, ... )score_test(object, ...) ## Default S3 method: score_test(object, ...) ## S3 method for class 'rtirt' score_test( object, moderator, parm = "accuracy", by_item = FALSE, test = c("auto", "DM", "CvM", "maxLM", "LM"), decorrelate = c("block", "full"), cluster = NULL, impact_check = TRUE, ... )
object |
A model fitted by |
... |
Arguments of the method ( |
moderator |
A vector with one value per person: numeric (continuous or ordered) or a factor. |
parm |
Parameters to test: |
by_item |
|
test |
|
decorrelate |
|
cluster |
Optional cluster identifiers (see Details). |
impact_check |
Message when the moderator is related to the ability EAPs but is not a covariate of the model (see Details). |
Moderators derived from the response times. The marginal distribution of the response times
does not involve the accuracy parameters (a, b), so their scores have mean zero given any function
of the times (for example the total log time): the accuracy parameters can be tested along such a
moderator, provided the scores are decorrelated within the tested block (decorrelate = "block",
the default). The response time parameters cannot be tested along a moderator built from the
times. Speed scores (EAPs) also use the responses and are not such a function.
DIF and impact. The model identifies ability as N(x'beta, 1) for everyone, so a moderator
related to ability (impact) shifts all difficulties and is flagged as instability. To test DIF
beyond impact, include the moderator as a covariate of the latent regression
(ecm(input_data(..., cov = z), model = "latreg"), or model = "irt" without response times)
and test that fit; score_test() gives a message when the
moderator correlates with the ability EAPs and is not among the covariates.
Clustered data. With cluster, the scores are summed within clusters, which is exact for a
moderator that is constant within clusters (for example a school characteristic); a person-level
moderator with clustered persons is refused.
A data frame of class rtirt_score_test: parameters, statistic, p.value and, with
by_item, p.holm.
cond <- set_cond(n_subj = 400, n_item = 8) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross", se = FALSE) score_test(fit, rbinom(400, 1, 0.5) == 1, by_item = TRUE) # a random grouping: no DIF score_test(fit, rowSums(dat$log_t)) # along the total log timecond <- set_cond(n_subj = 400, n_item = 8) dat <- sim_data(cond, sim_para(cond, "cross"), "cross") fit <- ecm(dat, model = "cross", se = FALSE) score_test(fit, rbinom(400, 1, 0.5) == 1, by_item = TRUE) # a random grouping: no DIF score_test(fit, rowSums(dat$log_t)) # along the total log time
Tests whether a term added to a part of a declared model is needed, from the ecm() fit of the
model without it (the null model): score_test(fit, add = Y ~ delta[item] * z) tests
in ,
that is uniform DIF along z, for all items together and for each item. Only the null model is
fitted.
## S3 method for class 'block_model_ecm' score_test(object, add, data = NULL, ...)## S3 method for class 'block_model_ecm' score_test(object, add, data = NULL, ...)
object |
A fit of |
add |
A formula |
data |
Optional named list (or data frame) of person-level or persons x items data used in the term and not in the model. |
... |
Unused. |
The term. add is a formula Y ~ term: Y names an observed part of the model, and term is
added to its linear predictor (bernoulli_logit()) or to its mean (normal()). New parameters are
written with their dimension as in block_model() (delta[item], delta[person]) or without one
for a single value (Y ~ delta * z: the same shift for every item). The term must be 0 when the new
parameters are 0, e.g. delta[item] * z, or delta[item] * z * theta for a change of the
discrimination (non-uniform DIF). Other names in the term are parameters of the model or data:
data of the model (block_model(Y = Y, z = z, ...): a vector with one value per person, or a
persons x items matrix, e.g. logT ~ delta[item] * Y for the dependence of the times on the
responses of van der Linden & Glas, 2010; see ci_test());
or a vector given in data = list(z = z), with the same rules (no missing values).
The statistic (Rao, 1948; for IRT, Glas, 1998, 1999; Glas & van der Linden, 2010). With
the score of the new parameters at the null estimates
(with ) and the observed information of
,
chi-square with as many degrees of freedom as new parameters when the null model holds; it equals the
likelihood ratio statistic asymptotically, without fitting the alternative. W is the information
about left after the null parameters are estimated; the score of the null
parameters is 0 at their estimates. When a direction of the new parameters is absorbed exactly by the
null parameters (e.g. a shift of every item and a free mean of the latent variable), its casewise
scores are 0 for every person; the joint test is then made in the other directions (found from the
cross-product of the casewise scores) and df is their number (the per-element tests are not
affected).
The score is the expected complete-data score under the E-step of the fit (Fisher's identity;
Louis, 1982), as for the standard errors of ecm().
The information is the observed information by Louis' formula (expected complete-data Hessian minus the covariance of the complete-data score), on the same quadrature grid. Falk & Monroe (2018) found that LM tests in IRT with the observed (Hessian) information keep their size, and those with the cross-product of the casewise scores reject too often.
Per element: when the new parameters have one dimension (e.g. delta[item]), each item is tested
alone (the alternative in which only that item's parameters are free): ,
chi-square with one degree of freedom per new parameter, with Holm-adjusted p-values (Holm, 1979).
estimate is the one-step estimate of that element (the expected
parameter change; Sörbom, 1989), shown for one new parameter.
With prior = "map" the score and the information are those of the log posterior of the null
parameters; the new parameters get no prior. The usual test is from prior = "ml".
DIF and impact. As for the score tests of the built-in models (score_test.rtirt()): when z is
related to the latent variable, a model whose latent mean does not depend on z shows the
difference in mean as a shift of every item. Put z in the prior of the latent variable,
theta[person] ~ normal(g * z, 1), to test DIF beyond impact; the test of all items together then
has one degree of freedom less (the shift that g absorbs).
A data frame of class block_lm_test: one row for the joint test (all) and, when the new
parameters have one dimension, one row per element (statistic, df, p.value, p.holm,
estimate). Attributes: add, score and W.
Falk, C. F., & Monroe, S. (2018). On Lagrange multiplier tests in multidimensional item response theory: Information matrices and model misspecification. Educational and Psychological Measurement, 78(4), 653–678. doi:10.1177/0013164417714506
Glas, C. A. W. (1998). Detection of differential item functioning using Lagrange multiplier tests. Statistica Sinica, 8(3), 647–667.
Glas, C. A. W. (1999). Modification indices for the 2-PL and the nominal response model. Psychometrika, 64(3), 273–294. doi:10.1007/BF02294296
Glas, C. A. W., & van der Linden, W. J. (2010). Marginal likelihood inference for a model for item responses and response times. British Journal of Mathematical and Statistical Psychology, 63(3), 603–626. doi:10.1348/000711009X481360
Holm, S. (1979). A simple sequentially rejective multiple test procedure. Scandinavian Journal of Statistics, 6(2), 65–70.
Louis, T. A. (1982). Finding the observed information matrix when using the EM algorithm. Journal of the Royal Statistical Society B, 44(2), 226–233. doi:10.1111/j.2517-6161.1982.tb01203.x
Rao, C. R. (1948). Large sample tests of statistical hypotheses concerning several parameters with applications to problems of estimation. Mathematical Proceedings of the Cambridge Philosophical Society, 44(1), 50–57. doi:10.1017/S0305004100023987
Sörbom, D. (1989). Model modification. Psychometrika, 54(3), 371–384. doi:10.1007/BF02294623
van der Linden, W. J., & Glas, C. A. W. (2010). Statistical tests of conditional independence between responses and/or response times on test items. Psychometrika, 75(1), 120–139. doi:10.1007/s11336-009-9129-9
set.seed(1); N <- 400; K <- 6 z <- rbinom(N, 1, 0.5); th <- rnorm(N); b0 <- seq(-1, 1, length.out = K) eta <- outer(th, rep(1, K)) - matrix(b0, N, K, byrow = TRUE) eta[, 2] <- eta[, 2] + 0.8 * z # DIF in item 2 Y <- matrix(rbinom(N * K, 1, plogis(eta)), N) m <- block_model(Y = Y, z = z, theta[person] ~ normal(g * z, 1), g ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), Y ~ bernoulli_logit(a * (theta - b))) fit <- ecm(m, prior = "ml") score_test(fit, add = Y ~ delta[item] * z)set.seed(1); N <- 400; K <- 6 z <- rbinom(N, 1, 0.5); th <- rnorm(N); b0 <- seq(-1, 1, length.out = K) eta <- outer(th, rep(1, K)) - matrix(b0, N, K, byrow = TRUE) eta[, 2] <- eta[, 2] + 0.8 * z # DIF in item 2 Y <- matrix(rbinom(N * K, 1, plogis(eta)), N) m <- block_model(Y = Y, z = z, theta[person] ~ normal(g * z, 1), g ~ normal(0, 1), a[item] ~ normal(1, 1, lower = 0), b[item] ~ normal(0, 3), Y ~ bernoulli_logit(a * (theta - b))) fit <- ecm(m, prior = "ml") score_test(fit, add = Y ~ delta[item] * z)
Posterior means (EAP) and posterior SDs of ability and speed, one row per person.
scores(object, ...) ## S3 method for class 'rtirt' scores(object, ...)scores(object, ...) ## S3 method for class 'rtirt' scores(object, ...)
object |
A sampled model. |
... |
Unused. |
A tibble (class rtirt_tbl: printed with 3 decimals, stored in full precision) with
id, ability, ability_psd and (RT models) speed, speed_psd.
Mirrors setCond() of ExtendedRtIrtModeling.jl (Julia argument names are accepted by the
alias setCond()). For real data it is not needed: the dimensions come from the data and
the MCMC settings are arguments of gibbs() (gibbs(data, model = ...) does not use those
of set_cond()). In simulations it carries the dimensions for
sim_para() / sim_data() and default MCMC settings.
set_cond( n_subj = NULL, n_item = NULL, n_feat = NULL, n_iter = 5000, n_chain = 4, n_burnin = NULL, n_thin = 1, n_rep = 10, q_ra = 0.5, q_rt = 0.5 )set_cond( n_subj = NULL, n_item = NULL, n_feat = NULL, n_iter = 5000, n_chain = 4, n_burnin = NULL, n_thin = 1, n_rep = 10, q_ra = 0.5, q_rt = 0.5 )
n_subj, n_item, n_feat
|
Numbers of persons, items and covariates ( |
n_iter, n_chain
|
Iterations per chain and number of (independent) chains. |
n_burnin |
Burn-in per chain (default |
n_thin |
Thinning interval for the stored draws. |
n_rep |
Replications in |
q_ra |
Kept for compatibility (unused, as in the Julia package). |
q_rt |
Quantile level used by the Julia aliases |
An object of class sim_cond.
cond <- set_cond(n_subj = 500, n_item = 10, n_feat = 2, n_iter = 2000, n_chain = 3) condcond <- set_cond(n_subj = 500, n_item = 10, n_feat = 2, n_iter = 2000, n_chain = 3) cond
sim_para() draws true parameters for a condition; sim_data() simulates data from them
(the person parameters are stored in attr(data, "true_para")). Item parameters:
a ~ N+(1, 0.2^2), b ~ N(0, 0.5^2), sigma2t ~ logN(log 0.3, 0.2^2).
sim_para(cond, model = sim_models, true_corr = 0.3) sim_data(cond, para, model = sim_models, type = c("norm", "tail", "skew"))sim_para(cond, model = sim_models, true_corr = 0.3) sim_data(cond, para, model = sim_models, type = c("norm", "tail", "skew"))
cond |
Conditions from |
model |
The model of |
true_corr |
Correlation of ability and speed ( |
para |
True parameters from |
type |
Residual distribution: |
sim_para: an input_para(); sim_data: an input_data() (items item01, ...;
covariates x1, ...) with the true parameters (item and person) in attr(, "true_para"),
which compare_para() uses after ecm(data, model = ...) or gibbs(data, model = ...).
Item parameters (posterior means), the structural parameters with posterior SD, 95% interval, R-hat and effective sample size, the person covariance, reliability and DIC.
## S3 method for class 'rtirt' summary(object, digits = 3, ...)## S3 method for class 'rtirt' summary(object, digits = 3, ...)
object |
A sampled model. |
digits |
Decimals to print. |
... |
Unused. |
An rtirt_summary: a list of the tables (see rtirt_summary), printed.
Deprecated: vi() still works (with a warning once per session) but gets no new
features. Use ecm() for estimates or gibbs() for the posterior.
vi(model, ...)vi(model, ...)
model |
|
... |
Passed to the method. |
vi() fits a model by mean-field variational Bayes (CAVI); see vi.block_model().