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

By Nedim Yel, Ph.D. · Principal Statistician, Data Analysis & Statistical Solutions (DASS) ·

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:

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$):

$$\hat{\theta}_S = \frac{\int_{-\infty}^{\infty} \theta \, P(S = s \mid \theta) g(\theta) \, d\theta}{\int_{-\infty}^{\infty} P(S = s \mid \theta) g(\theta) \, d\theta}, \qquad T_S = 50 + 10\hat{\theta}_S$$

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

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.