Flagging repeaters on raw score gain punishes candidates who studied or remediated. retestR asks a different question: is this gain larger than this candidate’s circumstances predict, and is it concentrated where preknowledge would put it?
Data
Each repeater took two disjoint forms drawn from a calibrated bank in
which every item is marked either exposed (long-running,
possibly compromised) or new. The simulation plants preknowledge in 5%
of repeaters.
library(retestR)
sim <- rt_simulate(n_persons = 800, form_exposed = 30, form_new = 15, seed = 1)
head(sim$data$persons)
#> person days_between remediation
#> 1 C00001 322 1
#> 2 C00002 354 0
#> 3 C00003 320 0
#> 4 C00004 177 0
#> 5 C00005 94 0
#> 6 C00006 58 1
table(sim$truth$preknowledge)
#>
#> FALSE TRUE
#> 758 42With real data, assemble the same structure with
rt_data(responses, persons, bank).
Expected gain
The growth model uses attempt 1 and only the new items of attempt 2, so preknowledge cannot inflate the expected gain.
fit <- rt_fit(sim, growth = ~ log(days_between) + remediation)
fit
#> <rt_fit> expected-gain model, 800 repeaters
#> attempt-1 ability: N(-0.513, 0.732^2)
#> growth coefficients:
#> estimate se
#> (Intercept) -0.446 0.209
#> log(days_between) 0.144 0.041
#> remediation 0.392 0.052
#> growth SD: 0.260 | log-RT residual SD: 0.502Evidence and risk
risk <- rt_risk(fit, n_null = 4, alpha = 0.01, seed = 1)
risk
#> <rt_risk> 800 repeaters | evidence: exposed + rt | null draws: 3200
#> 44 flagged at p < 0.01 (about 8.0 expected if everyone were honest); 38 with q < 0.05
#>
#> person S1 S2 z_gain z_exposed z_rt T p_gain p_exposed p_rt p_value
#> C00737 10 25 2.760 5.71 5.77 11.49 0.001250 0.000312 0.000312 0.000312
#> C00660 3 19 3.948 6.52 4.79 11.31 0.000312 0.000312 0.000312 0.000312
#> C00257 5 25 3.981 6.68 4.50 11.19 0.000312 0.000312 0.000312 0.000312
#> C00717 11 26 2.603 3.87 6.30 10.18 0.002499 0.000312 0.000312 0.000312
#> C00055 7 18 1.645 4.61 4.35 8.96 0.049984 0.000312 0.000312 0.000312
#> C00265 13 30 3.543 5.33 3.21 8.54 0.000312 0.000312 0.000625 0.000312
#> C00651 14 27 3.200 4.15 4.37 8.52 0.000625 0.000312 0.000312 0.000312
#> C00176 15 25 0.616 2.95 5.16 8.11 0.276476 0.001250 0.000312 0.000312
#> C00012 21 34 2.129 3.13 4.89 8.02 0.014683 0.000937 0.000312 0.000312
#> C00525 25 33 1.502 2.67 5.16 7.84 0.064980 0.002812 0.000312 0.000312
#> q_value flag
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUE
#> 0.00893 TRUEp_value is calibrated by simulating complete honest
administrations through the same pipeline, so it is the false-positive
rate for an honest repeater.
table(flagged = risk$flag, preknowledge = sim$truth$preknowledge)
#> preknowledge
#> flagged FALSE TRUE
#> FALSE 754 2
#> TRUE 4 40Compared with raw-gain flagging
Flag the same number of candidates by raw gain and look at who gets caught:
raw <- rt_raw_gain(sim)
k <- sum(risk$flag)
raw_flag <- raw$gain >= sort(raw$gain, decreasing = TRUE)[k]
honest_remediated <- !sim$truth$preknowledge & sim$truth$remediation == 1
c(model_caught = sum(risk$flag & sim$truth$preknowledge),
raw_caught = sum(raw_flag & sim$truth$preknowledge),
model_flags_remediated = sum(risk$flag & honest_remediated),
raw_flags_remediated = sum(raw_flag & honest_remediated))
#> model_caught raw_caught model_flags_remediated
#> 40 17 2
#> raw_flags_remediated
#> 17