Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Test Information Function (TIF) = Σ_i I_i(θ)
Standard Error of Measurement = 1 / √TIF(θ)
Differential Item Functioning (DIF)
An item shows DIF if examinees from different groups (e.g., gender, ethnicity)
with the same ability have different probabilities of answering correctly.
Uniform DIF: systematic advantage for one group across all θ levels
Non-uniform DIF: group advantage reverses across θ levels
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import minimize
from scipy.stats import chi2
deficc_3pl(theta: np.ndarray, a: float, b: float, c: float) -> np.ndarray:
"""
Three-parameter logistic (3PL) item characteristic curve.
Args:
theta: Array of ability values.
a: Discrimination parameter (a > 0, typically 0.5–2.5).
b: Difficulty parameter (typically -3 to +3).
c: Guessing parameter (typically 0.0–0.35).
Returns:
Array of P(correct | theta) values in [c, 1].
"""return c + (1 - c) / (1 + np.exp(-a * (theta - b)))
deficc_2pl(theta: np.ndarray, a: float, b: float) -> np.ndarray:
"""Two-parameter logistic (2PL) ICC — special case of 3PL with c=0."""return icc_3pl(theta, a, b, c=0.0)
deficc_1pl(theta: np.ndarray, b: float) -> np.ndarray:
"""One-parameter logistic / Rasch ICC — special case with a=1, c=0."""return icc_3pl(theta, a=1.0, b=b, c=0.0)
defiif_3pl(theta: np.ndarray, a: float, b: float, c: float) -> np.ndarray:
"""
Item information function for the 3PL model.
Args:
theta: Ability values.
a: Discrimination.
b: Difficulty.
c: Guessing.
Returns:
Array of item information values I(θ).
"""
p = icc_3pl(theta, a, b, c)
q = 1 - p
info = (a ** 2) * ((p - c) ** 2) * q / ((1 - c) ** 2 * p)
return info
deftif(theta: np.ndarray, params: list[tuple]) -> np.ndarray:
"""
Test information function — sum of item information functions.
Args:
theta: Ability values.
params: List of (a, b, c) tuples for each item.
Returns:
Array of test information TIF(θ).
"""
total = np.zeros_like(theta)
for a, b, c in params:
total += iif_3pl(theta, a, b, c)
return total
defsem_from_tif(theta: np.ndarray, params: list[tuple]) -> np.ndarray:
"""Standard error of measurement from test information: SEM(θ) = 1/√TIF(θ)."""
test_info = tif(theta, params)
return1.0 / np.sqrt(np.maximum(test_info, 1e-6))
Step 2 — 2PL Parameter Estimation (Marginal MLE)
defestimate_2pl_jml(
response_matrix: np.ndarray,
n_iter: int = 100,
lr_theta: float = 0.1,
lr_params: float = 0.05,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Joint Maximum Likelihood Estimation for 2PL IRT model.
Alternates between updating ability (θ) estimates and item parameters (a, b)
using gradient ascent on the log-likelihood. For production use, prefer
marginal MLE via the R `mirt` package.
Args:
response_matrix: Binary matrix of shape (n_persons, n_items).
1 = correct, 0 = incorrect.
n_iter: Number of EM/JML iterations.
lr_theta: Learning rate for ability updates.
lr_params: Learning rate for item parameter updates.
Returns:
Tuple (theta_hat, a_hat, b_hat) — estimated abilities and item parameters.
"""
n_persons, n_items = response_matrix.shape
theta = np.zeros(n_persons)
a = np.ones(n_items)
b = np.zeros(n_items)
for iteration inrange(n_iter):
# E-step equivalent: update theta for fixed item paramsfor s inrange(n_persons):
p = icc_2pl(np.array([theta[s]] * n_items), a, b)
p = np.clip(p, 1e-6, 1 - 1e-6)
y = response_matrix[s]
grad_theta = np.sum(a * (y - p))
theta[s] += lr_theta * grad_theta
# M-step equivalent: update item params for fixed thetafor i inrange(n_items):
p = icc_2pl(theta, a[i], b[i])
p = np.clip(p, 1e-6, 1 - 1e-6)
y = response_matrix[:, i]
residuals = y - p
grad_a = np.sum(residuals * (theta - b[i]))
grad_b = np.sum(residuals * (-a[i]))
a[i] += lr_params * grad_a
b[i] += lr_params * grad_b
a[i] = max(a[i], 0.05) # Discrimination must be positiveif (iteration + 1) % 20 == 0:
p_mat = np.array([icc_2pl(theta, a[i], b[i]) for i inrange(n_items)]).T
p_mat = np.clip(p_mat, 1e-6, 1 - 1e-6)
ll = np.sum(response_matrix * np.log(p_mat) + (1 - response_matrix) * np.log(1 - p_mat))
print(f" Iteration {iteration + 1:3d}: log-likelihood = {ll:.2f}")
return theta, a, b