| Title: | Differential Item Functioning for AI-Scored Assessments |
|---|---|
| Description: | Detects and quantifies differential item functioning (DIF) in AI-scored educational and psychological assessments. Provides a fully self-contained robust DIF engine (M-estimation via iteratively re-weighted least squares with the bi-square loss) alongside the Differential AI Scoring Bias (DASB) test, which detects item-level scoring shifts that differ across subgroups when comparing human and AI scoring conditions. Supports independent and paired scoring designs, robust linking of the cross-condition contrast, multiplicity control, conversion of fitted 'mirt' models to package inputs, simulation utilities, anchor weight diagnostics, and an AI-effect classification framework. Methods follow Halpin (2024) <doi:10.1007/s11336-024-09957-6>. |
| Authors: | Subir Hait [aut, cre] (ORCID: <https://orcid.org/0009-0004-9871-9677>) |
| Maintainer: | Subir Hait <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 0.2.0 |
| Built: | 2026-09-20 09:22:26 UTC |
| Source: | https://github.com/subirhait/aidif |
Compares the DIF flagging patterns from the human and AI scoring
conditions and labels each item "stable_clean" (not flagged in
either), "stable_dif" (flagged in both, same direction),
"introduced" (flagged only under AI), "masked" (flagged
only under human), or "new_direction" (flagged in both, opposite
directions).
ai_effect_summary(dif_human, dif_ai = NULL, alpha = 0.05)ai_effect_summary(dif_human, dif_ai = NULL, alpha = 0.05)
dif_human |
A DIF table from |
dif_ai |
A DIF table from |
alpha |
Flagging threshold. Default |
A data.frame with columns human_delta, ai_delta,
human_flag, ai_flag, and status. Values are
unrounded.
These labels are descriptive only. They are derived from a pair
of significance flags, and a difference in significance is not itself a
significant difference (Gelman and Stern, 2006). An item can move from
"not flagged" to "flagged" through sampling variation alone. Use
scoring_bias_test to test whether the group contrast
actually changed; use these labels to describe the pattern afterwards.
Gelman, A. and Stern, H. (2006). The difference between "significant" and "not significant" is not itself statistically significant. The American Statistician, 60(4), 328–331.
eg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) ai_effect_summary(mod)eg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) ai_effect_summary(mod)
Returns the bi-square weights assigned to each item under each scoring condition. Items with weight near zero are effectively excluded from the robust scaling estimate, indicating likely DIF contamination.
anchor_weights(object)anchor_weights(object)
object |
An |
A data.frame with column human_weight and, when AI data
were supplied, ai_weight. Values are unrounded.
eg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) anchor_weights(mod)eg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) anchor_weights(mod)
Extracts group-specific item parameter estimates and their asymptotic
covariance matrices from a fitted multiple-group IRT model and returns
the mle list structure that fit_aidif and
scoring_bias_test expect.
as_aidif(object, groups = NULL, ...) ## S3 method for class 'MultipleGroupClass' as_aidif(object, groups = NULL, ...)as_aidif(object, groups = NULL, ...) ## S3 method for class 'MultipleGroupClass' as_aidif(object, groups = NULL, ...)
object |
A fitted model. Currently only multiple-group
|
groups |
Optional character vector of length two giving the reference and focal group names, in that order. Defaults to the first two groups in the fitted object, in the order mirt stores them. Supply this explicitly: silently taking the first two groups is the kind of default that produces a valid-looking but reversed contrast. |
... |
Passed to methods. |
This removes the manual extraction step that 0.1.0 required, in which users had to slice a joint covariance matrix and reassemble parameter data frames by hand.
A list with components par.names, est, and
var.cov, suitable as human_mle or ai_mle.
if (requireNamespace("mirt", quietly = TRUE)) { grp <- rep(c("reference", "focal"), each = 196) mod <- mirt::multipleGroup(mirt::Science, 1, group = grp, SE = TRUE, verbose = FALSE) dat <- as_aidif(mod, groups = c("reference", "focal")) str(dat$est$group.1) # In practice the two arguments to fit_aidif() come from two separate # calibrations, one per scoring condition: # fit <- fit_aidif(as_aidif(human_mod), as_aidif(ai_mod)) }if (requireNamespace("mirt", quietly = TRUE)) { grp <- rep(c("reference", "focal"), each = 196) mod <- mirt::multipleGroup(mirt::Science, 1, group = grp, SE = TRUE, verbose = FALSE) dat <- as_aidif(mod, groups = c("reference", "focal")) str(dat$est$group.1) # In practice the two arguments to fit_aidif() come from two separate # calibrations, one per scoring condition: # fit <- fit_aidif(as_aidif(human_mod), as_aidif(ai_mod)) }
Estimates a robust location parameter for the vector of IRT scaling functions using iteratively re-weighted least squares (IRLS) with the bi-square loss. This is the core estimation engine of aiDIF.
estimate_robust_scale( mle, alpha = 0.05, scale_by = "pooled", tol = 1e-07, maxit = 100L )estimate_robust_scale( mle, alpha = 0.05, scale_by = "pooled", tol = 1e-07, maxit = 100L )
mle |
A validated mle list. |
alpha |
Significance level controlling the bi-square
tuning parameter |
scale_by |
Scaling denominator; passed to
|
tol |
Convergence tolerance. Default |
maxit |
Maximum IRLS iterations. Default |
A list of class rdif with elements:
estEstimated robust scale parameter.
weightsBi-square item weights.
rho_valueValue of objective at solution.
n_iterNumber of iterations used.
kTuning parameter used.
yRaw scaling function values.
vcov_estCovariance matrix of y at solution.
dif_testWald item-level DIF test (data.frame).
dtf_testWald test of differential test functioning.
dat <- simulate_aidif_data(n_items = 5, seed = 1) fit <- estimate_robust_scale(dat$human) print(fit$est)dat <- simulate_aidif_data(n_items = 5, seed = 1) fit <- estimate_robust_scale(dat$human) print(fit$est)
The primary estimation function of aiDIF. Runs the robust DIF
procedure under both human and AI scoring using the built-in IRLS engine
(estimate_robust_scale), then tests for Differential AI
Scoring Bias (DASB).
fit_aidif( human_mle, ai_mle = NULL, alpha = 0.05, scale_by = c("pooled", "ref", "focal"), design = c("independent", "paired"), cross_cov = NULL, metric = c("common", "linked"), adjust = "none", tol = 1e-07, maxit = 100L )fit_aidif( human_mle, ai_mle = NULL, alpha = 0.05, scale_by = c("pooled", "ref", "focal"), design = c("independent", "paired"), cross_cov = NULL, metric = c("common", "linked"), adjust = "none", tol = 1e-07, maxit = 100L )
human_mle |
A validated mle list for the human scoring condition, or
an |
ai_mle |
A validated mle list for the AI scoring condition, or
|
alpha |
Significance level. Default |
scale_by |
Denominator for standardising intercept differences:
|
design |
Passed to |
cross_cov |
Cross-condition covariance list, required when
|
metric |
Passed to |
adjust |
Multiplicity adjustment across items for the DASB tests.
Any |
tol |
IRLS convergence tolerance. Default |
maxit |
Maximum IRLS iterations. Default |
An object of class "aidif", a list with components
human_fit and ai_fit (objects of class rdif),
dif_human and dif_ai (Wald DIF tables),
scoring_bias (a dasb table), ai_effect
(classification table), data, and the settings used.
estimate_robust_scale,
scoring_bias_test, as_aidif
dat <- simulate_aidif_data(n_items = 6, seed = 1) mod <- fit_aidif(dat$human, dat$ai) print(mod) # Robustly centred DASB with Benjamini-Hochberg adjustment mod2 <- fit_aidif(dat$human, dat$ai, metric = "linked", adjust = "BH")dat <- simulate_aidif_data(n_items = 6, seed = 1) mod <- fit_aidif(dat$human, dat$ai) print(mod) # Robustly centred DASB with Benjamini-Hochberg adjustment mod2 <- fit_aidif(dat$human, dat$ai, metric = "linked", adjust = "BH")
Constructs and returns the built-in example dataset: paired human and AI item parameter estimates for 6 items in two groups, with known DIF and DASB planted at specific items.
make_aidif_eg()make_aidif_eg()
The data-generating model includes:
Item 1: DIF under human scoring (intercept +0.5 in focal group).
Item 3: Differential AI Scoring Bias (DASB) — AI scoring adds +0.4 to the focal-group intercept only.
Impact: 0.5 SD (focal group higher on latent trait).
AI drift: uniform +0.1 calibration offset on all items in both groups.
A list with elements human and ai, each a
validated mle list (see simulate_aidif_data for
format details).
simulate_aidif_data, fit_aidif
eg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) summary(mod)eg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) summary(mod)
"aidif"
S3 plot method for class "aidif"
## S3 method for class 'aidif' plot(x, type = "dif_forest", ...)## S3 method for class 'aidif' plot(x, type = "dif_forest", ...)
x |
An object of class |
type |
Character. One of:
|
... |
Additional graphical parameters. |
x, invisibly.
"aidif"
Prints a compact summary of the estimated robust scaling parameters and, when available, the number of items flagged for DIF and DASB.
## S3 method for class 'aidif' print(x, digits = 4, ...)## S3 method for class 'aidif' print(x, digits = 4, ...)
x |
An object of class |
digits |
Number of digits to display. Default |
... |
Further arguments (currently ignored). |
x, invisibly.
"summary.aidif" objectsPrint method for "summary.aidif" objects
## S3 method for class 'summary.aidif' print(x, digits = 4, ...)## S3 method for class 'summary.aidif' print(x, digits = 4, ...)
x |
An object of class |
digits |
Number of digits to display. Default |
... |
Further arguments (currently ignored). |
x, invisibly.
Takes two mle lists (one per scoring condition) and returns a validated
aidif_data object for use in fit_aidif.
read_ai_scored(human_mle, ai_mle)read_ai_scored(human_mle, ai_mle)
human_mle |
An mle list for human-scored data. Must contain
|
ai_mle |
An mle list for AI-scored data in the same format. |
A list of class "aidif_data" with elements
human and ai.
fit_aidif, make_aidif_eg,
simulate_aidif_data
For each item, computes the change in item intercept from human to AI scoring within each group, then tests whether that scoring shift differs across groups. A nonzero DASB indicates that the move from human to AI scoring changed the group contrast for that item, over and above any uniform recalibration that affects both groups equally.
scoring_bias_test( human_mle, ai_mle, design = c("independent", "paired"), cross_cov = NULL, metric = c("common", "linked"), adjust = "none", alpha = 0.05, tune = NULL )scoring_bias_test( human_mle, ai_mle, design = c("independent", "paired"), cross_cov = NULL, metric = c("common", "linked"), adjust = "none", alpha = 0.05, tune = NULL )
human_mle |
An mle list for the human scoring condition. |
ai_mle |
An mle list for the AI scoring condition, with the same item and group structure. |
design |
Either |
cross_cov |
Optional list with elements |
metric |
Either |
adjust |
Multiplicity adjustment applied across items within this
DASB analysis. Any method accepted by |
alpha |
Confidence level complement for the returned intervals.
Default |
tune |
Bi-square tuning constant used when |
A data frame of class "dasb", one row per item (per
threshold for polytomous items), with columns shift_g1,
shift_g2, DASB, se, lower, upper,
z, p_val, and — when adjust != "none" —
p_adj. Values are unrounded. Attributes record design,
metric, adjust, alpha, and, when linked, the
estimated offset and its bi-square offset_weights.
Let denote the intercept for item , group
, under scoring condition .
The within-group scoring shift is
and
If the four calibrations each carry their own arbitrary metric origin
, those constants contribute
to every item's DASB.
Raw DASB is therefore identified only up to an item-common additive
offset, which is perfectly confounded with a uniform DASB affecting all
items equally. Two options:
metric = "common"Assumes the four calibrations are already on a commensurate metric, so the offset is zero. This is the 0.1.0 behaviour and is appropriate only when linking was established upstream.
metric = "linked"Estimates the offset from the data with the same bi-square M-estimator the package uses for within-condition robust scaling, and reports DASB relative to it. This assumes the majority of items have no scoring-condition-by-group effect — the same majority assumption that underlies robust DIF scaling.
With design = "independent" the variance is the sum of the four
marginal variances. This is the 0.1.0 formula. It is appropriate when the
human and AI calibrations come from disjoint respondent samples.
With design = "paired" — the same responses scored twice — the
cross-condition covariance within each group is subtracted:
Cross-group covariances are taken to be zero, since the reference and focal groups comprise disjoint respondents.
Because that cross-condition covariance is typically positive, the
independence variance is an upper bound in paired designs: using
design = "independent" on paired data yields conservative
(too large) p-values, not anti-conservative ones.
eg <- make_aidif_eg() # 0.1.0-equivalent behaviour scoring_bias_test(eg$human, eg$ai) # Robustly centred, with Benjamini-Hochberg adjustment scoring_bias_test(eg$human, eg$ai, metric = "linked", adjust = "BH")eg <- make_aidif_eg() # 0.1.0-equivalent behaviour scoring_bias_test(eg$human, eg$ai) # Robustly centred, with Benjamini-Hochberg adjustment scoring_bias_test(eg$human, eg$ai, metric = "linked", adjust = "BH")
Generates a synthetic aidif_data-compatible list suitable for
benchmarking and method evaluation. The data-generating model contains:
classical DIF in the human scoring condition (controlled via
dif_items and dif_mag), differential AI scoring bias
(controlled via dasb_items and dasb_mag), and a latent
group mean difference (impact).
simulate_aidif_data( n_items = 10L, n_obs = 500L, impact = 0.5, dif_items = 1L, dif_mag = 0.5, dasb_items = 3L, dasb_mag = 0.4, ai_drift = 0.1, seed = 42L )simulate_aidif_data( n_items = 10L, n_obs = 500L, impact = 0.5, dif_items = 1L, dif_mag = 0.5, dasb_items = 3L, dasb_mag = 0.4, ai_drift = 0.1, seed = 42L )
n_items |
Integer. Number of items. Default: |
n_obs |
Integer. Approximate number of observations per group,
used to scale the covariance matrices. Default: |
impact |
Numeric. Latent mean difference (group 2 minus group 1)
in SD units. Default: |
dif_items |
Integer vector. Indices of items with DIF in the human
scoring condition (intercept shift added to group 2). Default:
|
dif_mag |
Numeric. Magnitude of the intercept DIF effect (in
IRT metric). Default: |
dasb_items |
Integer vector. Indices of items where AI scoring
introduces differential bias (intercept shift added to group 2 in the
AI condition only). Default: |
dasb_mag |
Numeric. Magnitude of the DASB effect. Default:
|
ai_drift |
Numeric. Uniform intercept shift applied to ALL items in
BOTH groups under AI scoring (simulates AI calibration offset).
Default: |
seed |
Integer seed for reproducibility, or |
Rather than simulating item responses and refitting IRT models (which
requires additional dependencies), this function directly simulates
maximum-likelihood estimates and their asymptotic covariance matrices,
consistent with a 2PL model fitted to n_obs observations per
group.
A list with elements human and ai, each formatted
identically to the output of
read_ai_scored. Can be passed directly to
fit_aidif.
dat <- simulate_aidif_data( n_items = 8, n_obs = 600, dif_items = c(1, 2), dasb_items = 5 ) mod <- fit_aidif(dat$human, dat$ai) summary(mod)dat <- simulate_aidif_data( n_items = 8, n_obs = 600, dif_items = c(1, 2), dasb_items = 5 ) mod <- fit_aidif(dat$human, dat$ai) summary(mod)
"aidif"
Assembles the DIF test tables for each scoring condition, the DASB table, and the AI-effect classification into a single object.
## S3 method for class 'aidif' summary(object, adjust = NULL, ...)## S3 method for class 'aidif' summary(object, adjust = NULL, ...)
object |
An object of class |
adjust |
Optional multiplicity adjustment for the DASB p-values,
recomputed from the unadjusted values. Any
|
... |
Further arguments (currently ignored). |
An object of class "summary.aidif". Print it to see the
report, or extract components directly.
eg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) s <- summary(mod, adjust = "BH") s$dasbeg <- make_aidif_eg() mod <- fit_aidif(eg$human, eg$ai) s <- summary(mod, adjust = "BH") s$dasb