This document outlines the key procedures for conducting the constrained CD-CAT simulation. We have provided a single replication as an illustrative example of our code. To perform additional replications, simply modify the line sim_num <- 1 to the desired number of replications. In our case study, we conducted 100 replications using the specified seed indicated in this document.
Furthermore, the CcdcatR package was developed by our team and includes several useful functions. For more detailed information, please refer to our GitHub repository at https://github.com/Jeanwong1222/CcdcatR.
#----------- load useful packages ----------#
# Install devtools package (if not installed yet)
# install.packages("devtools")
# Install remotes package (if not installed yet)
# install.packages("remotes")
# Using devtools to install packages from github
options(repos = c(CRAN = "https://cloud.r-project.org/"))
devtools::install_github("Jeanwong1222/CcdcatR")
## Skipping install of 'CcdcatR' from a github remote, the SHA1 (b34be50f) has not changed since last install.
## Use `force = TRUE` to force installation
library(CcdcatR) # including useful functions
library(MASS) # For statistical functions
library(gtools) # For combination calculations
#==============================================================================
#==============================================================================
# Simulation parameters
sim_num <- 1 # replication times
K <- 4 # Number of attributes
S <- 4 # Number of content areas
J <- 32 # Total number of items in each linear item pool
N <- 200 # Sample size for one test
M <- c(16,24,32) # Fixed test length for CD-CAT
lower <- 1 # lower bound for each content area
upper <- ceiling(M/S) # upper bound for each content area
min_attr <- ceiling(M/K) # the minimum number of items required that measures each attribute
#==============================================================================
#==============================================================================
# Step1: Item Bank Design and Calibration
itembank_CD_CAT <- matrix(NA, nrow = J*5, ncol = 8*sim_num)
itembank_linear <- matrix(NA, nrow = J*sim_num, ncol = 8*5)
# Create basic matrix units
Q_0 <- matrix(c(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1), nrow = 4, byrow = TRUE)
colnames(Q_0) <- c("vector", "relationship", "algebra", "visualization")
Q_00 <- do.call(rbind,replicate(8, Q_0, simplify = FALSE))
# Content areas: 1 = kinematics; 2 = forces; 3 = energy; 4 = momentum
content_area <- rep(c(1:S), each = J/S)
set.seed(1972)
for (s in 1:sim_num){
# Create 5 parallel small item bank for the linear test
for (i in 1:5) {
assign(paste0("itembank_", i),
cbind(
item_id = paste0(s,i, sprintf("%02d", 1:J)),
slipping = round(runif(J, .05, .25), 3),
guessing = round(runif(J, .05, .25), 3),
Q_00,
content_area = as.numeric(content_area)
)
)
}
# Create the comprehensive item bank and Q matrix for the CD-CAT
itembank_com <- apply(rbind(itembank_1,itembank_2,itembank_3,itembank_4,itembank_5),2,as.numeric)
itembank_CD_CAT[,c(((s*8)-7):(s*8))] <- itembank_com[sample(nrow(itembank_com)),]
itembank_linear[c(((J*s)-(J-1)):(J*s)),] = cbind(apply(itembank_1,2,as.numeric),
apply(itembank_2,2,as.numeric),
apply(itembank_3,2,as.numeric),
apply(itembank_4,2,as.numeric),
apply(itembank_5,2,as.numeric))
}
head(itembank_CD_CAT,10)
## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
## [1,] 1124 0.059 0.234 0 0 0 1 3
## [2,] 1407 0.134 0.102 0 0 1 0 1
## [3,] 1132 0.197 0.119 0 0 0 1 4
## [4,] 1215 0.130 0.204 0 0 1 0 2
## [5,] 1507 0.116 0.059 0 0 1 0 1
## [6,] 1329 0.127 0.177 1 0 0 0 4
## [7,] 1529 0.074 0.086 1 0 0 0 4
## [8,] 1418 0.112 0.244 0 1 0 0 3
## [9,] 1214 0.236 0.159 0 1 0 0 2
## [10,] 1302 0.094 0.064 0 1 0 0 1
#==============================================================================
#==============================================================================
# Step 2: Examinee Generation
KS_0 <- matrix(NA, nrow = N, ncol = K*sim_num)
# Define the attribute patterns and their probabilities
patterns <- c("0000", "0001", "0010", "0100", "1000", # low mastery
"1100", "1010", "1001", "0110", "0101", "0011", # medium mastery
"1110", "1101", "1011", "0111", "1111") # high mastery
probabilities <- c(.05,0.0625,0.0625,0.0625,0.0625,
.067,.067,.067,.067,.067,.067,
0.0625,0.0625,0.0625,0.0625,.05)
probabilities <- probabilities / sum(probabilities) # Normalize the probabilities to sum to 1
set.seed(1222) # Set seed for reproducibility
for (s in 1:sim_num){
for (j in 1:N) {
selected_pattern <- sample(patterns, 1, prob = probabilities)
KS_0[j,c(((K*s)-3):(K*s))] <- as.numeric(unlist(strsplit(selected_pattern, "")))
}
}
head(KS_0,10)
## [,1] [,2] [,3] [,4]
## [1,] 1 0 0 0
## [2,] 0 0 1 0
## [3,] 1 1 0 0
## [4,] 0 0 1 0
## [5,] 0 1 0 1
## [6,] 1 1 0 1
## [7,] 1 0 1 1
## [8,] 1 0 0 1
## [9,] 0 0 1 1
## [10,] 1 0 0 0
#==============================================================================
#==============================================================================
# Step 3: linear test
PCCR_linear <- numeric(sim_num)
ACCR_linear <- matrix(NA, nrow = sim_num, ncol = K)
KS_EAP_linear <- matrix(NA, nrow = N, ncol = K*sim_num)
set.seed(1112) # Set seed for reproducibility
for (s in 1:sim_num){
for (i in 1:N) {
# randomly select one test from the 5 parallel linear item bank
col_groups <- split(1:ncol(itembank_linear), ceiling((1:ncol(itembank_linear)) / 8))
selected_linear <- sample(1:length(col_groups), size = 1)
selected_cols <- col_groups[[selected_linear]]
itembank_0 = itembank_linear[c(((J*s)-(J-1)):(J*s)),selected_cols]
Q_linear = itembank_0[, c(4,5,6,7), drop = FALSE]
eta <- ETA(1, KS_0[i,c(((K*s)-3):(K*s))], Q_linear)
# simulate the responses of examinee
response <- response_gen(1, itembank_0[,2], itembank_0[,3], eta)
KS_EAP_linear[i,c(((K*s)-3):(K*s))] <- EAP_estimation(itembank_0,response,K)
}
# The accuracy of attributes estimation
PCCR_linear[s] <- round(PCCR(KS_0[,c(((K*s)-3):(K*s))],KS_EAP_linear[,c(((K*s)-3):(K*s))]),3)
ACCR_linear[s,] <- round(ACCR(KS_0[,c(((K*s)-3):(K*s))],KS_EAP_linear[,c(((K*s)-3):(K*s))]),3)
}
head(KS_EAP_linear,10)
## [,1] [,2] [,3] [,4]
## [1,] 1 0 0 0
## [2,] 0 0 1 0
## [3,] 1 1 0 0
## [4,] 0 0 1 0
## [5,] 0 1 0 1
## [6,] 1 1 0 1
## [7,] 1 0 1 1
## [8,] 1 0 0 1
## [9,] 0 0 1 1
## [10,] 1 0 0 0
PCCR_linear
## [1] 0.945
ACCR_linear
## [,1] [,2] [,3] [,4]
## [1,] 0.975 0.985 0.99 0.995
#==============================================================================
#==============================================================================
# Step 4: CD-CAT
# You need to change the values for different conditions
M0 = M[3]
min_con0 = upper[3]
min_attr0 = min_attr[3]
PCCR_CD_CAT <- numeric(sim_num)
ACCR_CD_CAT <- matrix(NA, nrow = sim_num, ncol = K)
KS_EAP_CD_CAT <- matrix(NA, nrow = N, ncol = K*sim_num)
# Set seed for PWKL & M[1,2,3] reproducibility
# set.seed(19961)
# set.seed(19962)
# set.seed(19963)
# Set seed for JSD & M[1,2,3] reproducibility
# set.seed(19964)
# set.seed(19965)
set.seed(19966)
for (ss in 1:sim_num){
itembank_CD_CAT0 <- itembank_CD_CAT[,c(((ss*8)-7):(ss*8))]
used_items_id <- integer(0)
used_attr_num <- matrix(0,nrow = 1, ncol = K)
used_cont_num <- matrix(0,nrow = 1, ncol = S)
Q <- itembank_CD_CAT0[,c(4,5,6,7)]
for(i in 1:N){
# 1. select the first group items
# Select first items considering both the content and attribute balancing
num_first <- lower*K
selected_items <- matrix(NA, nrow = num_first, ncol = dim(itembank_CD_CAT0)[2])
content_areas <- unique(selected_items[, 8])
while (length(content_areas) < 4) {
for (ii in 1:K) {
sub_itembank <- itembank_CD_CAT0[which(itembank_CD_CAT0[, (ii + 3)] == 1), , drop = FALSE]
selected_items[ii,] <- sub_itembank[sample(1:nrow(sub_itembank), 1), , drop = FALSE]
}
content_areas <- unique(selected_items[, 8])
}
first_items_id <- selected_items[,1]
# 2. update the used item information
used_items <- itembank_CD_CAT0[which(itembank_CD_CAT0[,1] %in% first_items_id),]
for(nn in 1:K){
used_attr_num[,nn] <- sum(used_items[,(nn+3)])
for(sss in 1:S){
used_cont_num[,sss] <- nrow(used_items[which(used_items[,8] %in% sss),,drop = FALSE])
}
}
used_items_id <- used_items[,1]
# 3. estimate the responses for the first selected items
eta <- ETA(1, KS_0[i,c(((K*ss)-3):(K*ss))], used_items[, c(4,5,6,7), drop = FALSE])
response <- response_gen(1, used_items[,2], used_items[,2], eta)
while (length(used_items_id) < M0) {
# 4. update the EAP for the current examinee
KS_EAP_CD_CAT[i,c(((K*ss)-3):(K*ss))] <- EAP_estimation(used_items,response,K)
# 5. defined the unused items
unused_items <- itembank_CD_CAT0[!itembank_CD_CAT0[,1] %in% used_items_id,]
# 6. select the next item for the examinee
# the current KS estimation
KS_estimate <- KS_EAP_CD_CAT[i, c(((K*ss)-3):(K*ss)), drop = FALSE]
# (a) posterior-weighted Kullback-Leibler information
PWKL_estimation <- PWKLest(K,unused_items,KS_estimate)
# (b) Jensen-Shannon divergence index
JSD_estimation <- JSDest(K,unused_items)
#----compute an attribute-balancing index----
unused_Q <- unused_items[,c(4,5,6,7)]
attr_weight <- numeric(nrow(unused_items))
for (tt in 1:nrow(unused_items)) {
attr_weight[tt] <- prod(((min_attr0 - used_attr_num)/min_attr0)^unused_Q[tt,])
}
#----compute an content-balancing index----
cont_weight <- numeric(nrow(unused_items))
# create a C matrix imitate Q matrix
C = matrix(0,nrow = nrow(unused_items), ncol = S)
for (cc in 1:nrow(unused_items)){
v <- as.numeric(unused_items[,8])
C[cc, v[cc]] <- 1 # Update the value to 1 based on the vector
}
for (tc in 1:nrow(unused_items)) {
cont_weight[tc] <- prod(((min_con0 - used_cont_num)/min_con0)^C[tc,])
}
# Final Item Selection Index (ISI)
# ISI <- attr_weight*cont_weight*PWKL_estimation
ISI <- attr_weight*cont_weight*JSD_estimation
selected_next_item_id <- unused_items[which.max(ISI),1]
selected_next_item <- unused_items[which(unused_items[,1] %in% selected_next_item_id), ,drop = FALSE]
# 7. update the used item information
used_items_id0 <- c(used_items_id, as.numeric(selected_next_item_id))
used_items <- itembank_CD_CAT0[which(itembank_CD_CAT0[,1] %in% used_items_id0),]
for(nn in 1:K){
used_attr_num[,nn] <- sum(used_items[,(nn+3)])
for(sss in 1:S){
used_cont_num[,sss] <- nrow(used_items[which(used_items[,8] %in% sss),,drop = FALSE])
}
}
used_items_id <- used_items[,1]
# 8. update the response
eat_next <- ETA(1, KS_0[i,c(((K*ss)-3):(K*ss))], selected_next_item[, c(4,5,6,7), drop = FALSE])
response_next <- response_gen(1, selected_next_item[,2], selected_next_item[,3], eat_next)
index_next <- which(used_items[,1] %in% selected_next_item_id)
response <- append(response, response_next, after = index_next-1)
}
}
# The accuracy of attributes estimation
PCCR_CD_CAT[ss] <- round(PCCR(KS_0[,c(((K*ss)-3):(K*ss))],KS_EAP_CD_CAT[,c(((K*ss)-3):(K*ss))]),3)
ACCR_CD_CAT[ss,] <- round(ACCR(KS_0[,c(((K*ss)-3):(K*ss))],KS_EAP_CD_CAT[,c(((K*ss)-3):(K*ss))]),3)
}
head(KS_EAP_CD_CAT,10)
## [,1] [,2] [,3] [,4]
## [1,] 1 0 0 0
## [2,] 0 0 1 0
## [3,] 1 1 0 0
## [4,] 0 0 1 0
## [5,] 0 1 0 1
## [6,] 1 1 0 1
## [7,] 1 0 1 1
## [8,] 1 0 0 1
## [9,] 0 0 1 1
## [10,] 1 0 0 0
PCCR_CD_CAT
## [1] 0.99
ACCR_CD_CAT
## [,1] [,2] [,3] [,4]
## [1,] 0.995 1 0.995 1