Score equating · Lord–Wingersky algorithm · REDCap / Qualtrics scoring
How to Equate a Shortened Scale to the Full Measure: Building a Raw-to-T-Score Crosswalk Table
Suppose your research team shortened a 30-item depression or teacher efficacy scale down to a 6-item short form for Wave 2 of a longitudinal study. Immediately, a practical scoring problem arises:
- In Wave 1, raw summed scores ranged from 0 to 120.
- In Wave 2, raw summed scores on the 6-item short form range from 0 to 24.
- Simply multiplying the 6-item sum score by 5 (linear pro-rating) introduces severe non-linear bias because the retained items rarely share the exact difficulty distribution of the full 30-item bank.
The Solution: Common-Metric IRT Calibration & Lord–Wingersky Recursion
Under Item Response Theory (IRT), latent trait parameters ($\theta$) are item-invariant. If you fix the 6 retained items to the discrimination ($a_i$) and threshold ($b_{ik}$) parameters calibrated on the full 30-item bank, you can apply the Lord–Wingersky (1984) recursive algorithm to map every possible raw summed score ($S \in \{0, 1, \dots, 24\}$) to its exact Expected A Posteriori (EAP) latent trait score $\hat{\theta}_S$ and standardized T-score ($T = 50 + 10\hat{\theta}_S$, $\text{SD} = 10$):
Once this lookup table is generated, clinical staff or data engineers can convert simple summed scores in REDCap or Qualtrics directly into full-bank T-scores without running IRT software in production.
Code Example: Building a Summed-Score-to-T-Score Crosswalk in R and Python
R
library(mirt)
# Parameters of the 6 retained items, taken from the 30-item graded response calibration:
# discrimination a and ordered thresholds b (5-point items scored 0-4)
a <- c(1.8, 1.4, 2.1, 1.1, 1.6, 1.3)
b <- rbind(c(-1.2, -0.3, 0.6, 1.5),
c(-0.8, 0.1, 0.9, 1.8),
c(-1.0, 0.0, 0.8, 1.6),
c(-0.5, 0.4, 1.1, 2.0),
c(-1.4, -0.5, 0.4, 1.3),
c(-0.9, 0.2, 1.0, 1.9))
d <- -a * b # mirt stores intercepts d = -a * b
# mirt needs some response data to build the model. Every item parameter is then
# fixed at the values above, so the simulated data do not affect the crosswalk.
set.seed(11)
dat <- simdata(a, d, N = 1000, itemtype = "graded")
pv <- mirt(dat, 1, itemtype = "graded", pars = "values")
pv$value[pv$name == "a1"] <- a[match(pv$item[pv$name == "a1"], colnames(dat))]
for (k in 1:4) {
i <- pv$name == paste0("d", k)
pv$value[i] <- d[match(pv$item[i], colnames(dat)), k]
}
pv$est <- FALSE
mod <- mirt(dat, 1, itemtype = "graded", pars = pv)
# Sum-score EAP (mirt runs the Lord-Wingersky recursion internally), then T scores
tab <- fscores(mod, method = "EAPsum", full.scores = FALSE)
crosswalk <- data.frame(Raw_Sum_Score = tab[, "Sum.Scores"],
Theta_EAP = round(tab[, "F1"], 2),
T_Score = round(50 + 10 * tab[, "F1"], 1),
T_Score_SE = round(10 * tab[, "SE_F1"], 1))
print(crosswalk)Python
import numpy as np
import pandas as pd
from scipy.stats import norm
def build_lord_wingersky_crosswalk(a_params: np.ndarray, b_thresholds: np.ndarray) -> pd.DataFrame:
"""
Implements the exact Lord-Wingersky (1984) recursive algorithm in Python
to convert raw summed scores (0 to K*M) into IRT EAP Theta and T-Scores (Mean=50, SD=10).
"""
I, M = b_thresholds.shape # I = number of items (e.g., 6), M = max category score (e.g., 4)
max_raw_score = I * M
# 1. Define normal quadrature grid for numerical integration over theta in [-4.0, +4.0]
theta_q = np.linspace(-4.0, 4.0, 81)
prior_weights = norm.pdf(theta_q, loc=0.0, scale=1.0)
prior_weights /= prior_weights.sum() # Normalize prior density g(theta)
# 2. Compute category response probabilities P_{ik}(theta) for all items
logits = a_params[None, :, None] * (theta_q[:, None, None] - b_thresholds[None, :, :])
p_star = 1.0 / (1.0 + np.exp(-logits))
p_pad = np.concatenate([np.ones((len(theta_q), I, 1)), p_star, np.zeros((len(theta_q), I, 1))], axis=2)
p_cat = p_pad[:, :, :-1] - p_pad[:, :, 1:] # Shape: (Q, I, M+1)
# 3. Lord-Wingersky Recursion: Convolve summed-score probabilities item by item
score_dist = p_cat[:, 0, :]
for i in range(1, I):
current_max = score_dist.shape[1] - 1
new_dist = np.zeros((len(theta_q), current_max + M + 1))
for k in range(M + 1):
new_dist[:, k : k + current_max + 1] += score_dist * p_cat[:, i, k : k + 1]
score_dist = new_dist
# 4. Compute Posterior Mean (EAP Theta) and Posterior SD for each raw summed score s
rows = []
for s in range(max_raw_score + 1):
likelihood_s = score_dist[:, s]
posterior_unnorm = likelihood_s * prior_weights
posterior_w = posterior_unnorm / max(posterior_unnorm.sum(), 1e-15)
eap_theta = float(np.sum(theta_q * posterior_w))
se_theta = float(np.sqrt(np.sum(((theta_q - eap_theta) ** 2) * posterior_w)))
rows.append({
"Raw_Sum_Score": s,
"EAP_Theta": round(eap_theta, 3),
"Theta_SE": round(se_theta, 3),
"T_Score": round(50.0 + 10.0 * eap_theta, 1),
"T_Score_SE": round(10.0 * se_theta, 1),
})
return pd.DataFrame(rows)
# Same six items as the R example (5-point items scored 0-4)
a = np.array([1.8, 1.4, 2.1, 1.1, 1.6, 1.3])
b = np.array([[-1.2, -0.3, 0.6, 1.5],
[-0.8, 0.1, 0.9, 1.8],
[-1.0, 0.0, 0.8, 1.6],
[-0.5, 0.4, 1.1, 2.0],
[-1.4, -0.5, 0.4, 1.3],
[-0.9, 0.2, 1.0, 1.9]])
print(build_lord_wingersky_crosswalk(a, b).to_string(index=False))Both tabs use the same six items and produce the same table: a raw score of 0 maps to a T-score of 30.1, 12 maps to 53.0, and 24 maps to 74.1. The T-scores agree to one decimal place, and the standard errors to within 0.1. The R tab uses mirt’s EAPsum scoring, which runs the Lord–Wingersky recursion internally; the Python tab writes the recursion out step by step.
Before you use the table
- Fix the parameters; do not re-estimate them. The table puts short-form scores on the full-scale metric only if the six items keep the parameters from the full-bank calibration. Calibrating the short form freely on its own sample sets the metric to that sample instead.
- Score items from 0. The raw sum in the table is the sum of item scores coded 0 to M. If your data use 1 to 5, subtract 1 from every item first (6 from a six-item sum).
- Match the prior to the calibration. The examples assume the calibration population has a standard normal trait distribution, so T = 50 and SD = 10 refer to that population. If your calibration used a different mean and SD, use them in the prior.
- Report the standard error. A six-item score carries real measurement error, as the T-score standard errors in the table show, and EAP scores are pulled toward the mean.
Frequently Asked Questions
Why can’t I just multiply my 6-item short-form summed score by 5 to compare it to a 30-item scale?
Linear pro-rating assumes every item in the short form has the exact same difficulty thresholds and discrimination distribution as the full 30-item bank. Because short forms are typically optimized for specific trait regions (such as clinical cutoffs), linear multiplication introduces severe non-linear bias in the tails. IRT common-metric linking via the Lord–Wingersky algorithm eliminates this bias.
What is the Lord–Wingersky recursive algorithm in IRT scoring?
The Lord–Wingersky (1984) algorithm computes the exact conditional probability distribution of every possible summed score $S$ given latent trait $\theta$ by recursively convolving individual item response probability functions. Integrating over a prior population density yields a simple lookup table mapping each raw sum score directly to an Expected A Posteriori (EAP) $\theta$ score and standardized T-score ($\text{Mean} = 50$, $\text{SD} = 10$).
Can a Lord–Wingersky IRT crosswalk table be used directly inside REDCap or Qualtrics?
Yes. Once the raw-sum-to-T-score lookup table is generated from calibrated IRT parameters, clinical staff and data engineers can embed the conversion table as a simple calculated field in REDCap, Qualtrics, or SQL without running IRT software in production.
Commission a complete custom short-form & crosswalk package
Building a custom short form for a clinical trial, health system, or multi-wave educational grant? Get a full IRT + MILP optimization blueprint, DIF screen, and REDCap/Qualtrics conversion table at Data Analysis & Statistical Solutions.
Generate an instant quote Simulate short-form power on StatPower.org
Related guides: how to shorten a validated survey scale without losing validity and how to defend a shortened scale in peer review.