The Prowise Learn update algorithm is an advanced parameter estimation method designed to address the problem of rating drift in computer adaptive testing systems. This algorithm was developed for the Prowise Learn platform and introduces paired item updates to maintain parameter stability while still providing real-time adaptation.
In this vignette, we will explore the mathematical foundations of the Prowise Learn algorithm, its unique paired update mechanism, and how to implement and customize it for your research.
Mathematical Foundation
Core Update Equations
The Prowise Learn algorithm builds upon the Maths Garden approach but adds a crucial innovation: paired item updates to prevent rating drift. The update equations are:
Person Ability Update:
Paired Item Difficulty Updates: For consecutive responses by the same person, the difficulty updates are paired:
Where: - is the ability of person - is the difficulty of item - is the observed response (0 or 1) of person to item - is the expected probability of a correct response - and are learning rates for abilities and difficulties respectively - and are the current and previous responses - and are the expected probabilities for current and previous items
Expected Response Probability
The expected probability is calculated using the logistic function:
This uses the 1-parameter logistic (1PL) item response model.
Rating Drift Problem
Rating drift occurs when item difficulty estimates systematically increase or decrease over time, often due to:
- Sequential Bias: Later items in a sequence may be systematically easier or harder
- Person Ability Changes: If person abilities change during testing, item difficulties may be incorrectly adjusted
- Selection Bias: Adaptive selection may create correlations between item difficulties and administration order
Paired Update Solution
The paired update mechanism addresses rating drift by:
- Balancing Updates: When updating a pair of items, the total difficulty change sums to zero
- Relative Positioning: Items maintain their relative difficulty positions
- Drift Prevention: Systematic increases or decreases in difficulty are prevented
Implementation in meow
Function Overview
The update_prowise_learn()
function implements this algorithm:
update_prowise_learn <- function(theta, diff, resp) {
# Initialize updated parameters
theta_updated <- theta
diff_updated <- diff
# Learning rates (K values) - these could be tuned
K_theta <- 0.1 # Learning rate for ability
K_beta <- 0.1 # Learning rate for difficulty
# Calculate expected probabilities for all responses
E_Sij <- stats::plogis(theta[resp$id] - diff[resp$item])
# Update theta (ability) for each person
for (j in unique(resp$id)) {
resp_j <- resp[resp$id == j, ]
update_term <- K_theta * sum(resp_j$resp - E_Sij[resp$id == j])
theta_updated[j] <- theta[j] + update_term
}
# Paired item updates
update_count <- 0
for (person in unique(resp$id)) {
person_idx <- which(resp$id == person)
if (length(person_idx) >= 2) {
for (i in 2:length(person_idx)) {
idx_now <- person_idx[i]
idx_prev <- person_idx[i - 1]
item_now <- resp$item[idx_now]
item_prev <- resp$item[idx_prev]
s_now <- resp$resp[idx_now]
s_prev <- resp$resp[idx_prev]
e_now <- E_Sij[idx_now]
e_prev <- E_Sij[idx_prev]
kappa <- 0.5 * (K_beta * (s_now - e_now) - K_beta * (s_prev - e_prev))
diff_updated[item_now] <- diff_updated[item_now] + kappa
diff_updated[item_prev] <- diff_updated[item_prev] - kappa
update_count <- update_count + 1
}
}
}
cat("Prowise item updates triggered:", update_count, "\n")
out <- list(
theta_est = theta_updated,
diff_est = diff_updated,
resp_cur = resp
)
return(out)
}
Step-by-Step Breakdown
1. Person Ability Updates
for (j in unique(resp$id)) {
resp_j <- resp[resp$id == j, ]
update_term <- K_theta * sum(resp_j$resp - E_Sij[resp$id == j])
theta_updated[j] <- theta[j] + update_term
}
This follows the same approach as Maths Garden for person ability updates.
2. Paired Item Updates
for (person in unique(resp$id)) {
person_idx <- which(resp$id == person)
if (length(person_idx) >= 2) {
for (i in 2:length(person_idx)) {
idx_now <- person_idx[i]
idx_prev <- person_idx[i - 1]
item_now <- resp$item[idx_now]
item_prev <- resp$item[idx_prev]
s_now <- resp$resp[idx_now]
s_prev <- resp$resp[idx_prev]
e_now <- E_Sij[idx_now]
e_prev <- E_Sij[idx_prev]
kappa <- 0.5 * (K_beta * (s_now - e_now) - K_beta * (s_prev - e_prev))
diff_updated[item_now] <- diff_updated[item_now] + kappa
diff_updated[item_prev] <- diff_updated[item_prev] - kappa
}
}
}
This implements the paired update mechanism: 1. For each person with multiple responses, consider consecutive pairs 2. Calculate the update term based on both current and previous responses 3. Apply to the current item and to the previous item 4. This ensures the total difficulty change for the pair is zero
Using the Prowise Learn Algorithm
Basic Usage
Customizing Learning Rates
You can modify the learning rates by creating a wrapper function:
update_prowise_learn_custom <- function(theta, diff, resp, K_theta = 0.05, K_beta = 0.05) {
theta_updated <- theta
diff_updated <- diff
# Calculate expected probabilities
E_Sij <- stats::plogis(theta[resp$id] - diff[resp$item])
# Update theta (ability) for each person
for (j in unique(resp$id)) {
resp_j <- resp[resp$id == j, ]
update_term <- K_theta * sum(resp_j$resp - E_Sij[resp$id == j])
theta_updated[j] <- theta[j] + update_term
}
# Paired item updates with custom learning rate
update_count <- 0
for (person in unique(resp$id)) {
person_idx <- which(resp$id == person)
if (length(person_idx) >= 2) {
for (i in 2:length(person_idx)) {
idx_now <- person_idx[i]
idx_prev <- person_idx[i - 1]
item_now <- resp$item[idx_now]
item_prev <- resp$item[idx_prev]
s_now <- resp$resp[idx_now]
s_prev <- resp$resp[idx_prev]
e_now <- E_Sij[idx_now]
e_prev <- E_Sij[idx_prev]
kappa <- 0.5 * (K_beta * (s_now - e_now) - K_beta * (s_prev - e_prev))
diff_updated[item_now] <- diff_updated[item_now] + kappa
diff_updated[item_prev] <- diff_updated[item_prev] - kappa
update_count <- update_count + 1
}
}
}
out <- list(
theta_est = theta_updated,
diff_est = diff_updated,
resp_cur = resp
)
return(out)
}
# Use with custom learning rates
results <- meow(
select_fun = select_max_info,
update_fun = update_prowise_learn_custom,
data_loader = data_simple_1pl,
update_args = list(K_theta = 0.05, K_beta = 0.05),
data_args = list(N_persons = 100, N_items = 50)
)
Advantages and Limitations
Advantages
- Prevents Rating Drift: The paired update mechanism prevents systematic changes in item difficulties
- Maintains Relative Positions: Items maintain their relative difficulty ordering
- Real-time Updates: Both person and item parameters are updated simultaneously
- Computational Efficiency: Fast computation suitable for real-time applications
- Interpretable: The paired update mechanism has clear intuitive meaning
Limitations
- Requires Multiple Responses: Paired updates only work when a person has multiple responses
- Order Dependency: The effectiveness depends on the order of item administration
- Fixed Learning Rates: Uses constant learning rates that don’t adapt to data
- No Uncertainty Quantification: Doesn’t provide confidence intervals or standard errors
- Assumes 1PL Model: Based on the Rasch model, may not fit data requiring 2PL or 3PL models
Extensions and Modifications
Adaptive Learning Rates
You can implement adaptive learning rates that change based on the amount of data:
update_prowise_learn_adaptive <- function(theta, diff, resp) {
theta_updated <- theta
diff_updated <- diff
# Calculate expected probabilities
E_Sij <- stats::plogis(theta[resp$id] - diff[resp$item])
# Adaptive learning rates based on number of responses
for (j in unique(resp$id)) {
resp_j <- resp[resp$id == j, ]
n_responses <- nrow(resp_j)
# Decrease learning rate with more responses
K_theta_adaptive <- 0.1 / (1 + n_responses * 0.1)
update_term <- K_theta_adaptive * sum(resp_j$resp - E_Sij[resp$id == j])
theta_updated[j] <- theta[j] + update_term
}
# Paired item updates with adaptive rates
update_count <- 0
for (person in unique(resp$id)) {
person_idx <- which(resp$id == person)
if (length(person_idx) >= 2) {
for (i in 2:length(person_idx)) {
resp_person <- resp[resp$id == person, ]
n_responses <- nrow(resp_person)
K_beta_adaptive <- 0.1 / (1 + n_responses * 0.1)
idx_now <- person_idx[i]
idx_prev <- person_idx[i - 1]
item_now <- resp$item[idx_now]
item_prev <- resp$item[idx_prev]
s_now <- resp$resp[idx_now]
s_prev <- resp$resp[idx_prev]
e_now <- E_Sij[idx_now]
e_prev <- E_Sij[idx_prev]
kappa <- 0.5 * (K_beta_adaptive * (s_now - e_now) - K_beta_adaptive * (s_prev - e_prev))
diff_updated[item_now] <- diff_updated[item_now] + kappa
diff_updated[item_prev] <- diff_updated[item_prev] - kappa
update_count <- update_count + 1
}
}
}
out <- list(
theta_est = theta_updated,
diff_est = diff_updated,
resp_cur = resp
)
return(out)
}
Constrained Updates
You can add constraints to prevent extreme parameter values:
update_prowise_learn_constrained <- function(theta, diff, resp,
theta_bounds = c(-4, 4),
diff_bounds = c(-4, 4)) {
theta_updated <- theta
diff_updated <- diff
# Calculate expected probabilities
E_Sij <- stats::plogis(theta[resp$id] - diff[resp$item])
# Update theta with constraints
for (j in unique(resp$id)) {
resp_j <- resp[resp$id == j, ]
update_term <- 0.1 * sum(resp_j$resp - E_Sij[resp$id == j])
theta_updated[j] <- theta[j] + update_term
# Apply constraints
theta_updated[j] <- max(theta_bounds[1], min(theta_bounds[2], theta_updated[j]))
}
# Paired item updates with constraints
update_count <- 0
for (person in unique(resp$id)) {
person_idx <- which(resp$id == person)
if (length(person_idx) >= 2) {
for (i in 2:length(person_idx)) {
idx_now <- person_idx[i]
idx_prev <- person_idx[i - 1]
item_now <- resp$item[idx_now]
item_prev <- resp$item[idx_prev]
s_now <- resp$resp[idx_now]
s_prev <- resp$resp[idx_prev]
e_now <- E_Sij[idx_now]
e_prev <- E_Sij[idx_prev]
kappa <- 0.5 * (0.1 * (s_now - e_now) - 0.1 * (s_prev - e_prev))
diff_updated[item_now] <- diff_updated[item_now] + kappa
diff_updated[item_prev] <- diff_updated[item_prev] - kappa
# Apply constraints
diff_updated[item_now] <- max(diff_bounds[1], min(diff_bounds[2], diff_updated[item_now]))
diff_updated[item_prev] <- max(diff_bounds[1], min(diff_bounds[2], diff_updated[item_prev]))
update_count <- update_count + 1
}
}
}
out <- list(
theta_est = theta_updated,
diff_est = diff_updated,
resp_cur = resp
)
return(out)
}
Best Practices
- Monitor Update Counts: Pay attention to the number of paired updates triggered
- Use Appropriate Learning Rates: Start with small learning rates (0.05-0.1) and adjust
- Consider Response Order: The effectiveness depends on the sequence of responses
- Validate Stability: Check that item difficulties remain stable over time
- Compare with Alternatives: Test against other methods to ensure effectiveness
- Handle Edge Cases: Consider what happens with few responses or single responses per person
Example: Complete Workflow
# Load required packages
library(meow)
# Define improved Prowise Learn function
update_prowise_learn_improved <- function(theta, diff, resp) {
theta_updated <- theta
diff_updated <- diff
# Calculate expected probabilities
E_Sij <- stats::plogis(theta[resp$id] - diff[resp$item])
# Adaptive learning rates for person abilities
for (j in unique(resp$id)) {
resp_j <- resp[resp$id == j, ]
n_responses <- nrow(resp_j)
K_theta_adaptive <- 0.1 / (1 + n_responses * 0.05)
update_term <- K_theta_adaptive * sum(resp_j$resp - E_Sij[resp$id == j])
theta_updated[j] <- theta[j] + update_term
theta_updated[j] <- max(-4, min(4, theta_updated[j])) # Constraints
}
# Paired item updates with adaptive rates
update_count <- 0
for (person in unique(resp$id)) {
person_idx <- which(resp$id == person)
if (length(person_idx) >= 2) {
for (i in 2:length(person_idx)) {
resp_person <- resp[resp$id == person, ]
n_responses <- nrow(resp_person)
K_beta_adaptive <- 0.1 / (1 + n_responses * 0.05)
idx_now <- person_idx[i]
idx_prev <- person_idx[i - 1]
item_now <- resp$item[idx_now]
item_prev <- resp$item[idx_prev]
s_now <- resp$resp[idx_now]
s_prev <- resp$resp[idx_prev]
e_now <- E_Sij[idx_now]
e_prev <- E_Sij[idx_prev]
kappa <- 0.5 * (K_beta_adaptive * (s_now - e_now) - K_beta_adaptive * (s_prev - e_prev))
diff_updated[item_now] <- diff_updated[item_now] + kappa
diff_updated[item_prev] <- diff_updated[item_prev] - kappa
# Apply constraints
diff_updated[item_now] <- max(-4, min(4, diff_updated[item_now]))
diff_updated[item_prev] <- max(-4, min(4, diff_updated[item_prev]))
update_count <- update_count + 1
}
}
}
cat("Prowise item updates triggered:", update_count, "\n")
out <- list(
theta_est = theta_updated,
diff_est = diff_updated,
resp_cur = resp
)
return(out)
}
# Run simulation
results <- meow(
select_fun = select_max_info,
update_fun = update_prowise_learn_improved,
data_loader = data_simple_1pl,
data_args = list(N_persons = 100, N_items = 50, data_seed = 123)
)
# Analyze results
print(head(results$results))