The fastest IRT library: JAX-accelerated MMLE-EM for 1PL/2PL/3PL/4PL
Project description
theta
The fastest IRT library. JAX-accelerated Marginal Maximum Likelihood (MMLE-EM, the Bock–Aitkin algorithm) for the 1PL, 2PL, 3PL, and 4PL logistic item-response models — item calibration, ability scoring, and standard errors, all JIT-compiled and GPU-ready.
import theta
# fit item parameters from a 0/1 response matrix (N persons x J items)
model = theta.fit(responses, "2PL")
print(model.a, model.b) # discrimination, difficulty
# score people (ability estimates + standard errors)
ability, se = model.score(responses, method="EAP")
# item-parameter standard errors
errs = model.standard_errors(responses)
Benchmark
theta and girth fit the same 2PL
model by the same method — Marginal Maximum Likelihood on a 61-point
Gauss–Hermite grid — so this is apples-to-apples. They recover identical
parameters (Pearson r = 1.000 on both discrimination and difficulty at every
size); theta is 15–50× faster, and the gap widens with scale.
Calibrating a 2PL model. CPU (Apple M-series),
Q = 61quadrature nodes, warm (steady-state — JIT already compiled). Log scale; lower is better.
| size (persons × items) | theta | girth | speedup | param agreement (a, b) |
|---|---|---|---|---|
| 1,000 × 50 | 61 ms | 923 ms | 15× | 1.000, 1.000 |
| 5,000 × 50 | 88 ms | 2.3 s | 26× | 1.000, 1.000 |
| 20,000 × 100 | 0.44 s | 22.9 s | 52× | 1.000, 1.000 |
| 50,000 × 100 | 1.0 s | 47.6 s | 47× | 1.000, 1.000 |
Reproduce with uv run --group bench python bench/compare.py.
Scaling on its own (theta, 2PL, 50 items, warm): 100k persons in 0.9 s,
500k in 4.8 s — roughly 5M responses/s on a laptop CPU, and the identical
code runs on GPU through the cuda extra. The first fit of a given shape pays a
one-time XLA compile (~0.5–1 s); every fit after is warm.
Why it's fast
Calibration is Marginal Maximum Likelihood via EM, the same algorithm used
by mirt, BILOG and MULTILOG, but the hot path is written so XLA can fuse it:
- E-step = two matrix multiplies. The latent ability is integrated out on a
Gauss–Hermite grid of
Qnodes. The per-person likelihood over the grid isX @ logP + (1−X) @ log(1−P), and the expected sufficient statistics areposteriorᵀ @ X. Pure BLAS/GEMM — it vectorizes and runs on GPU. - M-step decouples across items. Each item is fit independently by a damped
(Levenberg) Newton step with a backtracking line search,
vmap-ed over items. The line search makes every M-step monotonically improve the penalized EM objective (model.objective_history, non-decreasing), which is what tames the weakly-identified 3PL/4PL asymptotes. Note the unpenalized marginal log-likelihood (model.loglik_history) can dip when asymptote priors are active — under priors it is the penalized objective, not the raw likelihood, that the EM is guaranteed to ascend. - Everything but the convergence check is JIT-compiled, so the iteration
runs as fused kernels with no Python overhead.
float64throughout (required for trustworthy standard errors).
Derivatives for the 3PL/4PL M-step and for the standard errors come from JAX autodiff — no hand-coded Hessians.
Install
uv add theta-irt # CPU
uv add "theta-irt[cuda]" # + NVIDIA GPU (CUDA 12)
Prefer pip? pip install theta-irt (or pip install "theta-irt[cuda]").
Develop, build & publish (uv)
The project is fully uv-managed — one tool for the venv, tests, benchmarks,
the wheel, and the PyPI upload:
git clone https://github.com/vuciv/theta && cd theta
uv sync # create .venv + install theta and deps
uv run pytest # run the test suite
uv run python bench/benchmark.py # scaling benchmark
uv run --group bench python bench/compare.py # head-to-head vs girth (+ chart)
uv build # wheel + sdist into dist/
uv publish # upload to PyPI (set UV_PUBLISH_TOKEN)
Models
P(correct | θ) for ability θ and item parameters:
| Model | Formula | Free parameters |
|---|---|---|
| 1PL | σ(a(θ − b)) |
one shared a, per-item b |
| 2PL | σ(a(θ − b)) |
per-item a, b |
| 3PL | c + (1−c)·σ(a(θ − b)) |
a, b, c (guessing) |
| 4PL | c + (d−c)·σ(a(θ − b)) |
a, b, c, d (slip) |
where a = discrimination, b = difficulty, c = lower asymptote
(pseudo-guessing), d = upper asymptote (1 − slip).
Estimation is pure MMLE for the slope and difficulty (no penalty by
default), so 1PL/2PL are plain Marginal Maximum Likelihood. The 3PL/4PL
asymptotes are weakly identified — a property of the model, not the estimator —
so theta puts light Beta priors on c/d by default (making those two
parameters MAP / penalized-MML). Slope and difficulty recover cleanly;
asymptotes need a lot of data and show large standard errors when the data
can't pin them down. Disable the asymptote priors for a fully unpenalized fit:
flat = theta.Priors(c_a=None, c_b=None, slip_a=None, slip_b=None)
model = theta.fit(responses, "3PL", priors=flat)
Item-parameter standard errors are computed from the likelihood information (not the penalized objective), the conventional frequentist quantity.
API
import theta
# --- fit ---------------------------------------------------------------
model = theta.fit(
responses, # [N, J] of 0/1, NaN = missing/not-reached
"3PL", # "1PL" | "2PL" | "3PL" | "4PL"
n_points=61, # Gauss-Hermite quadrature nodes
max_iter=500, tol=1e-4,
priors=theta.Priors(c_a=2, c_b=8), # optional MAP priors
)
model.a, model.b, model.c, model.d # item parameters (np arrays)
model.params() # structured array of estimated params
model.loglik, model.converged, model.n_iter
model.objective_history # penalized EM objective (monotone)
model.aic(); model.bic(n_persons)
model.prob(theta_values) # P matrix [len(theta), J]
# --- score people ------------------------------------------------------
ability, se = model.score(responses, method="EAP") # "EAP" | "MAP" | "ML"
# --- item standard errors ---------------------------------------------
errs = model.standard_errors(responses) # dict: a, b, c, d, cov
# --- simulate data -----------------------------------------------------
sim = theta.simulate(n_persons=5000, n_items=40, model="3PL", seed=0)
sim.responses, sim.theta, sim.a, sim.b, sim.c, sim.d
Ability estimators: EAP (posterior mean, vectorized, always finite — the
default), MAP (posterior mode, follows the scoring prior), ML (likelihood
mode; bounded to ±6 and NaN for all-correct / all-incorrect patterns).
Status
v0.1 — dichotomous unidimensional models (1/2/3/4PL). Planned: polytomous
models (GRM, GPCM), multidimensional IRT, and fixed-θ / online scoring.
MIT licensed. Issues and PRs welcome.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file theta_irt-0.1.0.tar.gz.
File metadata
- Download URL: theta_irt-0.1.0.tar.gz
- Upload date:
- Size: 65.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74f2f244dc85a191087feff444db163d5c4228b8e6d85044c66d920af7386189
|
|
| MD5 |
a2a0017c1e496731c83731a579fe6fe5
|
|
| BLAKE2b-256 |
de83e369e0eb66401f7ead2f55affbf7f012ed99ae0011ccf437078a3391309e
|
File details
Details for the file theta_irt-0.1.0-py3-none-any.whl.
File metadata
- Download URL: theta_irt-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe79d72970accedc9d79e15bfab562a1d261b26e986e2f3b05141086e9a05185
|
|
| MD5 |
adb07ce6607b0ab43f98bf91d8a6de1c
|
|
| BLAKE2b-256 |
6b75b162b53c8fd4e84f9e5474319ee2050304a8354acab8ee9762dcf0725472
|