library(rstan)
library(reshape2) # install.packages("reshape2")
library(shinystan)
n_items <- 30 # Items
n_respondents <- 1000 # Respondents
n_obs <- n_items * n_respondents # Total number of observations
theta <- rnorm(n_respondents, 0, 1) # Abilities
alpha <- rnorm(n_items, 0, 1) # Discrimination
beta <- rnorm(n_items, 0, 1) # Difficulty
slope.ability <- outer(theta, alpha) # Multiply slope vector by ability vector
intercept <- matrix(rep(beta, n_respondents), nrow = n_respondents, byrow = TRUE)
prob <- plogis(slope.ability - intercept)
D <- data.frame(t(apply(prob, 1, function(x) rbinom(length(x), 1, x))))
names(D) <- 1:ncol(D)
D <- data.frame(respondent = 1:nrow(D), D)
D$issue <- as.numeric(gsub("X", "", D$variable))
# SET CONSTRAINT ON ALPHA
constraint <- rep(0, n_items)
# Here I just use the minimum alpha as the one I constrain,
# but any, say, clear conservative roll call could be used here.
constraint[alpha == min(alpha)] <- 1
constraint[(which(constraint == 1) + 1):length(constraint)] <- 2
# This constraint vector will read something like 0, 0, 0, 0, 1, 2, 2, 2, 2, 2 ... 2,
# where 1 indicates the constrained alpha, and the 2s are for bookkeeping in the
# transformed parameters block as you can see below
model_data <- list(n_items = length(unique(D$issue)),
n_respondents = length(unique(D$respondent)),
n_obs = length(D$y),
issue = D$issue,
respondent = D$respondent,
y = D$y,
constraint = constraint)
model_code <- "data {
int n_items; // number of items
int n_respondents; // number of persons
int n_obs; // number of responses
int respondent[n_obs]; // respondent ID
int issue[n_obs]; // issue ID
int y[n_obs]; // respondent response
int constraint[n_items]; // constraint on difficulty
}
parameters {
vector[n_respondents] theta; // ability
vector[n_items-1] alpha_raw; // discrimination (-1 because excludes constrained alpha)
real<upper = 0> alpha_raw_constrained; // constrain sign of the assigned alpha
real<lower = 0> mu_alpha; // discrimination mean
real<lower = 0> sigma_alpha; // discrimination scale
vector[n_items] beta; // difficulty
real mu_beta; // difficulty mean
real<lower = 0> sigma_beta; // difficulty scale
}
transformed parameters {
vector[n_items] alpha;
for(i in 1:n_items) {
if(constraint[i] == 0) alpha[i] <- alpha_raw[i];
if(constraint[i] == 1) alpha[i] <- alpha_raw_constrained;
if(constraint[i] == 2) alpha[i] <- alpha_raw[i-1];
}
}
model {
// Hyperparameters
mu_alpha ~ cauchy(0, 3);
sigma_alpha ~ cauchy(0, 3);
mu_beta ~ cauchy(0, 3);
sigma_beta ~ cauchy(0, 3);
// Priors
alpha_raw ~ normal(mu_alpha, sigma_alpha);
beta ~ normal(mu_beta, sigma_beta);
theta ~ normal(0, 1);
// Likelihood
for(i in 1:n_obs) {
y[i] ~ bernoulli_logit(alpha[issue[i]] * theta[respondent[i]] - beta[issue[i]]);
}
}
"
# Compile to C
model_irt <- stan_model(model_code = model_code)
# Sample from posterior
options(mc.cores = 6)
# Sample from posterior
posterior_irt <- sampling(model_irt, data = model_data, warmup = 500, iter = 1000,
open_progress = FALSE, chains = 6)
launch_shinystan(posterior_irt)
# Plot estimates versus true values of theta (ability)
cor(sapply(extract(posterior_irt, paste0("theta[", 1:n_respondents, "]")), mean), theta)
plot(sapply(extract(posterior_irt, paste0("theta[", 1:n_respondents, "]")), mean), theta)
# Plot estimates versus true values of alpha (discrimination)
cor(sapply(extract(posterior_irt, paste0("alpha[", 1:n_items, "]")), mean), alpha)
plot(sapply(extract(posterior_irt, paste0("alpha[", 1:n_items, "]")), mean), alpha)
# Plot estimates versus true values of beta (difficulty)
cor(sapply(extract(posterior_irt, paste0("beta[", 1:n_items, "]")), mean), beta)
plot(sapply(extract(posterior_irt, paste0("beta[", 1:n_items, "]")), mean), beta)