Skip to content
78 changes: 78 additions & 0 deletions docs/doctoring/REFERENCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!-- markdownlint-configure-file { "MD013": { "line_length": 120 } } -->

# Psychometric references

This file records, in APA 7th-edition form, the psychometric literature that
grounds the regression and robustness tests under `tests/testthat/`. Each work
was verified against its publisher record (journal, volume, issue, pages, and
DOI) rather than reproduced from memory.

> Note: a local Zotero API is not reachable from the build/CI sandbox, so this
> in-repo, version-controlled reference list is the achievable substitute for a
> Zotero-managed bibliography. Keep it in sync with the citation headers in the
> test files.

## Cited works

- Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation
of item parameters: Application of an EM algorithm. *Psychometrika, 46*(4),
443–459. <https://doi.org/10.1007/BF02293801>
- Cai, L. (2010). High-dimensional exploratory item factor analysis by a
Metropolis–Hastings Robbins–Monro algorithm. *Psychometrika, 75*(1), 33–57.
<https://doi.org/10.1007/s11336-009-9136-x>
- Chalmers, R. P. (2012). mirt: A multidimensional item response theory
package for the R environment. *Journal of Statistical Software, 48*(6),
1–29. <https://doi.org/10.18637/jss.v048.i06>
- Haebara, T. (1980). Equating logistic ability scales by a weighted
least squares method. *Japanese Psychological Research, 22*(3),
144–149. <https://doi.org/10.4992/psycholres1954.22.144>
- Kang, T., & Petersen, N. S. (2012). Linking item parameters to a base
scale. *Asia Pacific Education Review, 13*(2), 311–321.
<https://doi.org/10.1007/s12564-011-9197-2>
- Kim, S. (2006). A comparative study of IRT fixed parameter calibration
methods. *Journal of Educational Measurement, 43*(4), 355–381.
<https://doi.org/10.1111/j.1745-3984.2006.00021.x>
- Kim, S., & Kolen, M. J. (2019). Application of IRT fixed parameter
calibration to multiple-group test data. *Applied Measurement in
Education, 32*(4), 310–324.
<https://doi.org/10.1080/08957347.2019.1660344>
- Kolen, M. J., & Brennan, R. L. (2014). *Test equating, scaling, and
linking: Methods and practices* (3rd ed.). Springer.
<https://doi.org/10.1007/978-1-4939-0317-7>
- Mislevy, R. J., & Wu, P.-K. (1996). *Missing responses and IRT ability
estimation: Omits, choice, time limits, and adaptive testing* (ETS Research
Report No. RR-96-30-ONR). Educational Testing Service.
<https://doi.org/10.1002/j.2333-8504.1996.tb01708.x>
- Stocking, M. L., & Lord, F. M. (1983). Developing a common metric in
item response theory. *Applied Psychological Measurement, 7*(2),
201–210. <https://doi.org/10.1177/014662168300700208>

A previous draft listed Kim and Kolen (2010), "Linking item parameters
to a base scale," *Journal of Educational Measurement, 47*(2), 164–181.
That bibliographic record is not a real JEM article and is not kept.
The title belongs to Kang and Petersen (2012). A real Kim and Kolen
FIPC paper is Kim and Kolen (2019). Stocking and Lord (1983) and
Haebara (1980) are the canonical characteristic-curve linking methods
that FIPC is an alternative to; the tests below pin FIPC (Kim, 2006),
not a Stocking–Lord or Haebara transformation estimator.

## How each test file is grounded

- `test-kim2006-fixed-anchor-invariant.R` — Fixed-item-parameter linking
invariant: anchors keep their old-form values and stay fixed while
non-anchors are estimated onto the base scale (Kim, 2006; Bock & Aitkin,
1981; Chalmers, 2012). This is FIPC, not Stocking–Lord (1983) or
Haebara (1980) characteristic-curve transformation.
- `test-se-hessian-vcov-preservation.R` — `SE = TRUE` observed-information /
covariance matrix and the second-order optimality test are preserved through
linking on the old, new, and linked models (Bock & Aitkin, 1981; Cai, 2010;
Chalmers, 2012).
- `test-concurrent-missing-robustness.R` — Planned-missing / non-overlapping
booklet designs calibrate without error under ignorable missingness, and
anchors stay fixed (Bock & Aitkin, 1981; Mislevy & Wu, 1996; Kim, 2006).
- `test-degenerate-response-robustness.R` — Zero-score, perfect-score, and
skewed (near-degenerate) response patterns produce finite item parameters,
MAP abilities, and expected scores rather than crashing (Bock & Aitkin,
1981; Mislevy & Wu, 1996; Kim, 2006).
- `test-fixed-parameter-calibration.R` (pre-existing) — End-to-end linking
contract fixture (Kim, 2006).
125 changes: 125 additions & 0 deletions tests/testthat/test-concurrent-missing-robustness.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Directive item 3 — Concurrent calibration missing-value robustness.
#
# In common-item nonequivalent-groups (NEAT) and planned-missing booklet
# designs, examinees respond to only a subset of the items; the unadministered
# cells are structurally missing (NA). Under the standard ignorable / MAR
# assumption for such designs, marginal maximum likelihood integrates the
# person likelihood over only the OBSERVED responses, so missing-by-design
# cells neither bias nor break estimation (Bock & Aitkin, 1981; Mislevy & Wu,
# 1996). This test builds two non-overlapping booklets per form — every
# examinee sees all anchor items but only one form-specific block of unique
# items — and asserts that autoFIPC calibrates end-to-end without error while
# the Kim (2006) fixed-anchor invariant still holds.
#
# References (APA 7th):
# Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of
# item parameters: Application of an EM algorithm. Psychometrika, 46(4),
# 443-459. https://doi.org/10.1007/BF02293801
# Kim, S. (2006). A comparative study of IRT fixed parameter calibration
# methods. Journal of Educational Measurement, 43(4), 355-381.
# https://doi.org/10.1111/j.1745-3984.2006.00021.x
# Mislevy, R. J., & Wu, P.-K. (1996). Missing responses and IRT ability
# estimation: Omits, choice, time limits, and adaptive testing (ETS
# Research Report No. RR-96-30-ONR). Educational Testing Service.
# https://doi.org/10.1002/j.2333-8504.1996.tb01708.x

test_that("autoFIPC calibrates planned-missing booklets and keeps anchors fixed", {
skip_on_cran()
skip_if_not_installed("mirt")

set.seed(20260731)

old_item_names <- paste0("old_", 1:7)
new_item_names <- paste0("new_", 1:7)
# Items 1:4 are the fully-observed anchors; 5:7 are the form-specific block
# split across two booklets.
old_common_items <- old_item_names[1:4]
new_common_items <- new_item_names[1:4]

common_a <- c(0.95, 1.18, 0.82, 1.27)
common_d <- c(-0.70, -0.15, 0.40, 0.85)
old_a <- matrix(c(common_a, 1.05, 0.88, 1.16), ncol = 1)
old_d <- c(common_d, -0.35, 0.30, 0.60)
new_a <- matrix(c(common_a, 0.90, 1.22, 0.79), ncol = 1)
new_d <- c(common_d, 0.20, -0.50, 0.45)

n_persons <- 1600
old_data <- as.data.frame(mirt::simdata(
a = old_a, d = old_d,
itemtype = rep("2PL", length(old_item_names)), N = n_persons
))
new_data <- as.data.frame(mirt::simdata(
a = new_a, d = new_d,
itemtype = rep("2PL", length(new_item_names)), N = n_persons
))
names(old_data) <- old_item_names
names(new_data) <- new_item_names

# Planned-missing booklet spiral: anchors seen by everyone; the two unique
# blocks are administered to disjoint halves, the rest are NA (missing by
# design, not by response behavior).
booklet <- rep(c(1L, 2L), length.out = n_persons)
old_block_a <- old_item_names[5:6]
old_block_b <- old_item_names[7]
new_block_a <- new_item_names[5:6]
new_block_b <- new_item_names[7]
old_data[booklet == 1L, old_block_b] <- NA
old_data[booklet == 2L, old_block_a] <- NA
new_data[booklet == 1L, new_block_b] <- NA
new_data[booklet == 2L, new_block_a] <- NA

# Structural missingness must be present but the anchors must stay complete.
expect_true(anyNA(old_data))
expect_true(anyNA(new_data))
expect_false(anyNA(old_data[, old_common_items]))
expect_false(anyNA(new_data[, new_common_items]))

old_model <- mirt::mirt(
old_data, 1, itemtype = "2PL", method = "EM", SE = TRUE,
verbose = FALSE, technical = list(NCYCLES = 800)
)
new_model <- mirt::mirt(
new_data, 1, itemtype = "2PL", method = "EM", SE = TRUE,
verbose = FALSE, technical = list(NCYCLES = 800)
)

# A thrown error here fails the test: the calibration must complete despite
# the structurally missing (planned-missing) cells.
linked <- aFIPC::autoFIPC(
newformXData = new_model,
oldformYData = old_model,
newformCommonItemNames = new_common_items,
oldformCommonItemNames = old_common_items,
itemtype = "2PL",
checkIPD = FALSE,
tryEM = TRUE,
freeMEAN = FALSE,
forceNormalZeroOne = TRUE,
confirmCommonItems = TRUE
)

expect_type(linked, "list")
expect_true(all(c("LinkedModel", "ThetaLinkedform") %in% names(linked)))

# Anchors remain fixed to their old-form values despite the missing blocks.
old_values <- mirt::mod2values(old_model)
linked_values <- mirt::mod2values(linked$LinkedModel)
for (i in seq_along(old_common_items)) {
old_fixed <- old_values[
old_values$item == old_common_items[i] &
old_values$name %in% c("a1", "d"),
c("name", "value")
]
linked_fixed <- linked_values[
linked_values$item == new_common_items[i] &
linked_values$name %in% c("a1", "d"),
c("name", "value", "est")
]
expect_equal(linked_fixed$value, old_fixed$value, tolerance = 1e-6)
expect_false(any(linked_fixed$est))
}

# Every calibrated person receives a finite ability estimate on the linked
# scale even though each answered only a booklet-sized subset.
expect_true(all(is.finite(linked$ThetaLinkedform)))
})
117 changes: 117 additions & 0 deletions tests/testthat/test-degenerate-response-robustness.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Directive item 4 — Zero-score / perfect-score robustness.
#
# Real calibration samples contain examinees who answer every item incorrectly
# (all-0) or every item correctly (all-1), and items whose responses are highly
# skewed toward one category. Maximum-likelihood ability estimates diverge to
# +/- infinity for perfect and zero scores, but the marginal / Bayesian
# machinery aFIPC relies on keeps them finite: MML estimation integrates over a
# proper population prior (Bock & Aitkin, 1981), and the MAP ability scores
# autoFIPC returns are shrunk toward that prior and therefore finite even at the
# boundaries (Mislevy & Wu, 1996). This test injects all-0 and all-1 response
# vectors plus deliberately extreme (near-degenerate) unique items and asserts
# that fixed-item calibration (Kim, 2006) neither crashes nor emits non-finite
# item parameters, ability estimates, or expected scores.
#
# Note on scope: a fully CONSTANT item (a single observed category) is not
# identifiable under any IRT estimator, so this fixture keeps every item
# non-constant by construction (the injected all-0 and all-1 rows guarantee at
# least one response in each category) while still exercising the skewed,
# boundary-heavy regime the directive targets.
#
# References (APA 7th):
# Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of
# item parameters: Application of an EM algorithm. Psychometrika, 46(4),
# 443-459. https://doi.org/10.1007/BF02293801
# Kim, S. (2006). A comparative study of IRT fixed parameter calibration
# methods. Journal of Educational Measurement, 43(4), 355-381.
# https://doi.org/10.1111/j.1745-3984.2006.00021.x
# Mislevy, R. J., & Wu, P.-K. (1996). Missing responses and IRT ability
# estimation: Omits, choice, time limits, and adaptive testing (ETS
# Research Report No. RR-96-30-ONR). Educational Testing Service.
# https://doi.org/10.1002/j.2333-8504.1996.tb01708.x

test_that("autoFIPC survives zero-score, perfect-score, and skewed items", {
skip_on_cran()
skip_if_not_installed("mirt")

set.seed(20260801)

old_item_names <- paste0("old_", 1:6)
new_item_names <- paste0("new_", 1:6)
old_common_items <- old_item_names[1:4]
new_common_items <- new_item_names[1:4]

common_a <- c(1.00, 0.90, 1.20, 0.85)
common_d <- c(-0.60, -0.10, 0.30, 0.65)
# Unique items 5:6 are deliberately extreme in difficulty to create heavily
# skewed, near-degenerate response columns.
old_a <- matrix(c(common_a, 1.15, 0.80), ncol = 1)
old_d <- c(common_d, 3.2, -3.0)
new_a <- matrix(c(common_a, 0.95, 1.10), ncol = 1)
new_d <- c(common_d, -3.1, 3.3)

old_data <- as.data.frame(mirt::simdata(
a = old_a, d = old_d,
itemtype = rep("2PL", length(old_item_names)), N = 1500
))
new_data <- as.data.frame(mirt::simdata(
a = new_a, d = new_d,
itemtype = rep("2PL", length(new_item_names)), N = 1500
))
names(old_data) <- old_item_names
names(new_data) <- new_item_names

# Inject boundary examinees: rows 1:3 all-incorrect, rows 4:6 all-correct.
# This also guarantees every item has >=3 zeros and >=3 ones (non-constant).
old_data[1:3, ] <- 0
old_data[4:6, ] <- 1
new_data[1:3, ] <- 0
new_data[4:6, ] <- 1

# Sanity guard: no item collapsed to a single category.
expect_true(all(vapply(old_data, function(x) length(unique(x)) >= 2, logical(1))))
expect_true(all(vapply(new_data, function(x) length(unique(x)) >= 2, logical(1))))

old_model <- mirt::mirt(
old_data, 1, itemtype = "2PL", method = "EM", SE = TRUE,
verbose = FALSE, technical = list(NCYCLES = 800)
)
new_model <- mirt::mirt(
new_data, 1, itemtype = "2PL", method = "EM", SE = TRUE,
verbose = FALSE, technical = list(NCYCLES = 800)
)

# A thrown error here fails the test: calibration must complete despite the
# boundary (all-0 / all-1) examinees and skewed items.
linked <- aFIPC::autoFIPC(
newformXData = new_model,
oldformYData = old_model,
newformCommonItemNames = new_common_items,
oldformCommonItemNames = old_common_items,
itemtype = "2PL",
checkIPD = FALSE,
tryEM = TRUE,
freeMEAN = FALSE,
forceNormalZeroOne = TRUE,
confirmCommonItems = TRUE
)

expect_type(linked, "list")

# Linked item parameters must all be finite despite the boundary examinees.
linked_values <- mirt::mod2values(linked$LinkedModel)
item_params <- linked_values[
linked_values$name %in% c("a1", "d"), "value"
]
expect_true(all(is.finite(item_params)))

# MAP ability estimates on all three forms remain finite at the boundaries.
expect_true(all(is.finite(linked$ThetaOldform)))
expect_true(all(is.finite(linked$ThetaNewform)))
expect_true(all(is.finite(linked$ThetaLinkedform)))

# Expected test scores remain finite as well.
expect_true(all(is.finite(linked$ExpectedScoreOldform)))
expect_true(all(is.finite(linked$ExpectedScoreNewform)))
expect_true(all(is.finite(linked$ExpectedScoreLinkedform)))
})
Loading
Loading