23  More Item Response Theory, and Generalizability

The Rasch chapter built one item response model from scratch and promised there was a wider world. Here it is. Item Response Theory (IRT) is a whole family of models for the same task - measuring people from their item responses - and generalizability theory (G-theory) is a broad extension of reliability that asks not only “how much error?” but “error from what?” Both are areas where the authors have published extensively, often together in a single study.

23.1 Learning Objectives

  1. See how the 2-parameter (2PL) model generalizes Rasch by letting items differ in discrimination.
  2. Read an item characteristic curve and know what its slope and location mean.
  3. Decompose measurement error into facets with generalizability theory.
  4. Compute a G-coefficient and use a D-study to design a measure - the “how many items?” question.

23.2 Beyond Rasch: Items That Discriminate Differently

Rasch makes a strong, simple assumption: every item is equally good at separating people; items differ only in difficulty. The 2PL model relaxes it, giving each item its own discrimination \(a_i\) (how sharply it distinguishes high from low) alongside its difficulty \(b_i\):

\[P(x_{ij}=1) = \frac{1}{1 + e^{-a_i(\theta_j - b_i)}}\]

The clearest way to feel the difference is to look at the items. An item’s characteristic curve plots the probability of a correct/endorsed response against ability \(\theta\). Difficulty slides the curve left-right; discrimination changes its steepness:

library(tidyverse)    # dplyr, ggplot2, purrr, tibble, readr, stringr, forcats
library(broom)        # tidy(), glance(), augment() for model output
source("_common.R")   # book-wide helpers: round2(), fmt_p(), tidy2()

icc <- function(theta, a, b) 1 / (1 + exp(-a * (theta - b)))   # 2PL curve

items <- tibble(
  item = c("easy, moderate a", "medium, high a (steep)", "hard, low a (flat)"),
  a    = c(1.0, 2.5, 0.5),      # discrimination (steepness)
  b    = c(-1,  0.0, 1.0)       # difficulty (left-right location)
)

crossing(theta = seq(-4, 4, length.out = 300), items) |>
  mutate(p = icc(theta, a, b)) |>
  ggplot(aes(x = theta, y = p, colour = item)) +
  geom_line(linewidth = 1) +
  scale_colour_manual(
    name   = NULL,
    values = c("easy, moderate a"       = "steelblue",
               "medium, high a (steep)" = "firebrick",
               "hard, low a (flat)"     = "darkgreen")
  ) +
  coord_cartesian(ylim = c(0, 1)) +
  labs(x = expression(theta ~ "(ability)"), y = "P(correct)") +
  theme_book() +
  theme(legend.position = "top")
Figure 23.1: Item characteristic curves. Location = difficulty; steepness = discrimination.

The steep red item is discriminating: over a narrow band of ability it flips from “probably wrong” to “probably right,” so it sorts people sharply there. The flat green item barely distinguishes anyone - its responses are nearly a coin flip across a wide ability range. Rasch is the special case where every curve has the same slope; the 2PL, and its polytomous cousin the Graded Response Model (which we met dissecting depression profiles), let the data say otherwise. In practice you fit these with a maintained engine:

library(mirt)                              # shown, not run on the book's server
fit  <- mirt(data, 1, itemtype = "2PL")    # or "graded" for 0..k items (GRM)
coef(fit, simplify = TRUE)                 # each item's discrimination (a) and difficulty (b)

23.3 Generalizability: Reliability, Grown Up

Classical reliability (Ch. 10) splits observed variance into “true” and “error.” But error from where? It could come from the particular item you used, the rater who scored you, or the day you happened to show up. Generalizability theory treats each of these as a facet and estimates how much variance each contributes, turning “reliability” from a single number into an analysis of sources.

The machinery is the variance-partitioning you already know, run with aov. Simulate people crossed with items, then recover how much of the variance is real person differences versus item and residual noise:

NoteWorking in SPSS, Julia, or Python?

The code tabs below assume this chapter’s data is already loaded. Grab the one-file setup for your language from Getting the Book’s Data, run it once, then load what you need by name - this chapter uses ch23-gtheory. For example, book_data("ch23-gtheory") in R, Julia, or Python, or !bookdata name = "ch23-gtheory". in SPSS. Every language reads the same shipped files, so your numbers will match the ones printed here exactly.

set.seed(2026)
np <- 120; ni <- 12
person <- rnorm(np, sd = 1.0)   # real differences between people
item   <- rnorm(ni, sd = 0.5)   # items differ in easiness

d <- expand_grid(p = 1:np, i = 1:ni) |>
  mutate(
    score = person[p] + item[i] + rnorm(np * ni, sd = 0.7),
    p     = factor(p),
    i     = factor(i)
  )

ms  <- aov(score ~ p + i, data = d) |> tidy() |> pull(meansq)   # mean squares
v_e <- ms[3]                    # residual variance
v_p <- (ms[1] - v_e) / ni       # person variance (the signal)
v_i <- (ms[2] - v_e) / np       # item variance

tibble(person = v_p, item = v_i, residual = v_e) |> round2()
person item residual
1.05 0.36 0.48
INSERT FILE='data/sim/ch23-gtheory.sps'.
* Data assumed present as the active dataset with columns score, p (person), i (item).
* VARCOMP returns the person, item, and residual variance components directly,
* no hand EMS math from mean squares.
VARCOMP score BY p i
  /RANDOM = p i
  /METHOD = REML.
/tmp/RtmphZ1tMS/file375d4d705eec.sps:5.1-5.7: error: VARCOMP: VARCOMP is not
yet implemented.
    5 | VARCOMP score BY p i
      | ^~~~~~~
using CSV, DataFrames
df = CSV.read("data/sim/ch23-gtheory.csv", DataFrame)

using MixedModels

# df is a DataFrame with columns score, p (person), i (item).
# The random-effect variances ARE the G-theory person/item components;
# the residual variance is the leftover noise.
fit(MixedModel, @formula(score ~ 1 + (1|p) + (1|i)), df)
Linear mixed model fit by maximum likelihood
 score ~ 1 + (1 | p) + (1 | i)
   logLik   -2 logLik     AIC       AICc        BIC    
 -1736.0572  3472.1143  3480.1143  3480.1422  3501.2039

Variance components:
            Column   Variance Std.Dev. 
p        (Intercept)  1.045302 1.022400
i        (Intercept)  0.338506 0.581813
Residual              0.478553 0.691775
 Number of obs: 1440; levels of grouping factors: 120, 12

  Fixed-effects parameters:
─────────────────────────────────────────────────
                Coef.  Std. Error     z  Pr(>|z|)
─────────────────────────────────────────────────
(Intercept)  0.208062    0.193008  1.08    0.2810
─────────────────────────────────────────────────
import pandas as pd
df = pd.read_csv("data/sim/ch23-gtheory.csv")

import numpy as np
import statsmodels.formula.api as smf

# df has columns score, p (person), i (item).
# Persons and items are crossed random effects; their variances ARE the
# G-theory components, and the model's scale is the residual variance.
vc = {"p": "0 + C(p)", "i": "0 + C(i)"}
smf.mixedlm("score ~ 1", df, groups=np.ones(len(df)), vc_formula=vc).fit().summary()
<class 'statsmodels.iolib.summary2.Summary'>
"""
         Mixed Linear Model Regression Results
========================================================
Model:            MixedLM Dependent Variable: score     
No. Observations: 1440    Method:             REML      
No. Groups:       1       Scale:              0.4785    
Min. group size:  1440    Log-Likelihood:     -1736.7704
Max. group size:  1440    Converged:          Yes       
Mean group size:  1440.0                                
---------------------------------------------------------
            Coef.  Std.Err.    z    P>|z|  [0.025  0.975]
---------------------------------------------------------
Intercept   0.208     0.198  1.050  0.294  -0.180   0.596
i Var       0.362     0.227                              
p Var       1.048     0.213                              
========================================================

"""

Person variance is the signal - the real differences you want to measure. Item and residual variance are noise, now itemized by source. The generalizability coefficient is the same signal-share idea as reliability, but honest about the fact that you averaged over a sample of items:

# G-coefficient for a test of ni items
tibble(n_items = ni, G = v_p / (v_p + v_e / ni)) |> round2()
n_items G
12 0.96

23.3.1 The D-study: Designing the Measure

This is where G-theory pays for itself. Because you know the variance components, you can ask a design question - a decision (D) study: how many items would I need for a target reliability? It is the power analysis of measurement.

tibble(n_items = c(4, 8, 12, 20, 40)) |>
  mutate(G = v_p / (v_p + v_e / n_items)) |>   # generalizability at each test length
  round2()
n_items G
4 0.90
8 0.95
12 0.96
20 0.98
40 0.99

Reliability climbs with test length exactly as the Spearman-Brown logic predicts - and now you can pick the length that buys the reliability you need and stop, instead of guessing. Add a second facet (raters, occasions) and G-theory tells you whether your money is better spent on more items or more raters. That is measurement design as engineering.

NoteFrom the Field

This is the authors’ bread and butter. McKnight and Babcock-Parziale (McKnight and Babcock-Parziale 2007) combined Rasch and generalizability theory in a single study of a vision-rehabilitation outcome - exactly the two halves of this chapter, together. The Rasch analysis of the Mississippi PTSD Scale with Ben Wright (Conrad et al. 2004) showed reverse-scored items misbehaving; and Stroud, McKnight, and Jensen (Stroud et al. 2004) used IRT to shorten a disability scale without losing information - a D-study decision in action. The methods showcase has the full list.

23.4 What This Chapter Teaches

  • IRT is a family. Rasch fixes item discrimination; the 2PL frees it; the GRM extends it to graded items. Read the item characteristic curve - location is difficulty, slope is discrimination.
  • G-theory generalizes reliability by decomposing error into named facets (items, raters, occasions) with the same variance-partitioning you use for ANOVA.
  • The D-study is power analysis for measurement: knowing the variance components, you design the test - how many items, how many raters - to hit a target reliability.

We have now seen latent variables from every angle the authors work in - factors, classes, and item-response continua. That completes our tour of modeling the things we measure. But building a model raises a question models cannot answer on their own: which variables belong in it, and what a coefficient means causally. The next part takes that up directly, with directed acyclic graphs and the logic of causal inference.

Conrad, Kendon J., Benjamin D. Wright, Patrick McKnight, Miles McFall, Alan Fontana, and Robert Rosenheck. 2004. “Comparing Traditional and Rasch Analyses of the Mississippi PTSD Scale: Revealing Limitations of Reverse-Scored Items.” Journal of Applied Measurement 5 (1): 15–30.
McKnight, Patrick E., and Judith Babcock-Parziale. 2007. “Respondent Impact on Functional Ability Outcome Measures in Vision Rehabilitation.” Optometry and Vision Science 84 (8): 721–28.
Stroud, Michael W., Patrick E. McKnight, and Mark P. Jensen. 2004. “Assessment of Self-Reported Physical Activity in Patients with Chronic Pain: Development of an Abbreviated Roland-Morris Disability Scale.” The Journal of Pain 5 (5): 257–63.