The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
Acknowledgement. Large language models supplied by Anthropic and OpenAI were used in preparing this vignette, to assist with checking code, testing the veracity of the approaches described, and on occasion editing the text. Responsibility for the content rests with the authors.
Eyewitness identification data can be analyzed using multiple theoretical frameworks, each providing different information. This vignette demonstrates how to use r4lineups’ model comparison framework to fit and compare:
The compare_models() function provides a unified
interface for fitting all models and generating comparison tables and
visualizations.
The Two-High Threshold (2-HT) model is a multinomial processing tree (MPT) that separates identification decisions into distinct latent processes:
Parameters:
Model equations (target-present):
Use when:
Expected Information Gain quantifies how much information witness responses provide about guilt vs. innocence using Shannon entropy:
Formula: EIG = Σ p(x) × [H(prior) - H(p(guilty|x))]
where x = response category (e.g., “suspect_high_confidence”)
Interpretation:
Use when:
Full ROC uses ALL witness responses (suspect IDs, filler IDs, rejections) to compute a threshold-free measure of discriminability:
Measure:
Use when:
library(r4lineups)
lineup_data <- as.data.frame(simulate_lineup_data(
n_tp = 300,
n_ta = 300,
d_prime = 1.5,
c_criterion = 0.9, # see the note below: 0 would be far too liberal
lineup_size = 6,
conf_levels = 5,
foil_bias = c(0.4, 0.2, 0.0, -0.4, -1.0),
seed = 2024
))
head(lineup_data)
#> participant_id target_present lineup_member suspect_pos identification
#> 1 64 TRUE 1 1 suspect
#> 2 89 TRUE 1 1 suspect
#> 3 573 FALSE 2 1 filler
#> 4 393 FALSE 0 1 reject
#> 5 188 TRUE 3 1 filler
#> 6 526 FALSE 3 1 filler
#> confidence
#> 1 5
#> 2 3
#> 3 5
#> 4 1
#> 5 2
#> 6 5The c_criterion = 0.9 is not incidental: getting it
wrong makes one of the three models impossible to fit.
c_criterion is a threshold on the
maximum of the lineup members’ memory strengths, not a
textbook signal-detection criterion. The maximum of six draws from a
standard normal averages about 1.27, so a criterion of 0 — the function
default — is cleared by almost every lineup, and produces a rejection
rate near zero:
too_liberal <- as.data.frame(simulate_lineup_data(
n_tp = 300, n_ta = 300, d_prime = 1.5, c_criterion = 0,
lineup_size = 6, conf_levels = 5, seed = 2024
))
knitr::kable(
data.frame(
`c_criterion` = c(0, 0.9),
`Rejection rate (%)` = round(100 * c(
mean(too_liberal$identification == "reject"),
mean(lineup_data$identification == "reject")
), 1),
check.names = FALSE
),
row.names = FALSE,
caption = "Rejection rate under two decision criteria, simulated data (300 target-present and 300 target-absent lineups)."
)| c_criterion | Rejection rate (%) |
|---|---|
| 0.0 | 0.2 |
| 0.9 | 20.5 |
The 2-HT model’s detect-absence parameter dA is
identified almost entirely by rejections — it is the probability of
correctly concluding the culprit is absent. Here only one witness in 600
rejects, so the likelihood carries almost no information about
dA and the fit fails; fit_winter_2ht() stops
with a message naming that cause. A low rejection percentage is not by
itself fatal in a large sample; the information about dA
depends on the number of rejections.
# Fit all three models
comparison <- compare_models(
lineup_data,
models = c("2ht", "eig", "fullroc"),
lineup_size = 6,
prior_guilt = 0.5
)
# View comparison table
print(comparison)
#>
#> === Lineup Model Comparison ===
#>
#> Models fit: 2ht, eig, fullroc
#> Sample size: 600
#> Lineup size: 6
#>
#> Comparison Table:
#> Model Measure Value
#> 2-HT (Winter et al., 2022) Log-likelihood -557.8100
#> 2-HT (Winter et al., 2022) AIC 1123.6200
#> 2-HT (Winter et al., 2022) BIC 1141.2100
#> 2-HT (Winter et al., 2022) dP (detection presence) 0.4850
#> 2-HT (Winter et al., 2022) dA (detection absence) 0.0490
#> EIG (Starns et al., 2023) Expected Information Gain 0.1869
#> EIG (Starns et al., 2023) Information Efficiency 18.7000
#> EIG (Starns et al., 2023) Prior Entropy 1.0000
#> Full ROC (Smith & Yang, 2020) Full AUC 0.7471
#> Full ROC (Smith & Yang, 2020) Operating Points 11.0000
#> Full ROC (Smith & Yang, 2020) Max Hit Rate 1.0000
#> Interpretation
#> Model fit quality
#> Lower is better
#> Lower is better
#> 0 to 1
#> 0 to 1
#> Higher is better (bits)
#> Percentage (0-100%)
#> Maximum possible IG
#> Higher is better (0-1)
#> Number of decision criteria
#> Cumulative (0-1)
#>
#> Metrics describe different estimands and are not a single model-selection scale.
#>
#> Access fitted models via: $fitted_models$<model_name>
#> Available models: 2ht, eig, fullroc# More detailed output
summary(comparison)
#>
#> === Model Comparison Summary ===
#>
#> Dataset:
#> Total observations: 600
#> Target-present: 300
#> Target-absent: 300
#> Lineup size: 6
#>
#> Models fitted: 3
#> 2ht, eig, fullroc
#>
#> --- 2-HT Model Summary ---
#> Parameters: dP = 0.485 , dA = 0.049 , b = 0 , g = 0.754
#> AIC: 1123.62
#> BIC: 1141.21
#>
#> --- EIG Summary ---
#> EIG: 0.1869 bits
#> Information efficiency: 18.7 %
#> Number of response categories: 11
#>
#> --- Full ROC Summary ---
#> Full AUC: 0.7471
#> Operating points: 11
#> Ordering method: diagnosticity
#>
#> ---
#> Interpretation Guidance:
#> - 2-HT: AIC/BIC are comparable only to other likelihood models fit to the same outcomes
#> - EIG: Higher values = more informative procedure
#> - Full ROC: Higher AUC = better investigator discriminability
#>
#> Each model provides different information; consider reporting more than one.# Access 2-HT model
model_2ht <- comparison$fitted_models$`2ht`
knitr::kable(
data.frame(Parameter = names(model_2ht$parameters),
Estimate = round(unname(model_2ht$parameters), 3),
SE = round(unname(model_2ht$se), 3)),
row.names = FALSE,
caption = "2-HT parameter estimates with standard errors, simulated data."
)| Parameter | Estimate | SE |
|---|---|---|
| dP | 0.485 | NA |
| dA | 0.049 | NA |
| b | 0.000 | NA |
| g | 0.754 | NA |
cat("AIC:", round(model_2ht$aic, 2),
" BIC:", round(model_2ht$bic, 2),
" log-likelihood:", round(model_2ht$loglik, 2), "\n")
#> AIC: 1123.62 BIC: 1141.21 log-likelihood: -557.81Interpreting 2-HT parameters (when available):
# Access EIG model
model_eig <- comparison$fitted_models$eig
# View EIG value
cat("Expected Information Gain:", round(model_eig$eig, 4), "bits\n")
#> Expected Information Gain: 0.1869 bits
cat("Information Efficiency:",
round(model_eig$eig / model_eig$prior_entropy * 100, 1), "%\n")
#> Information Efficiency: 18.7 %
# Most informative responses
cat("\nTop 5 most informative response categories:\n")
#>
#> Top 5 most informative response categories:
print(head(model_eig$response_data[, c("response", "information_gain",
"posterior_guilty")], 5))
#> # A tibble: 5 × 3
#> response information_gain posterior_guilty
#> <chr> <dbl> <dbl>
#> 1 suspect_5 0.742 0.957
#> 2 suspect_3 0.525 0.898
#> 3 suspect_4 0.445 0.871
#> 4 suspect_2 0.278 0.8
#> 5 suspect_1 0.119 0.7Interpreting EIG:
# Access Full ROC model
model_fullroc <- comparison$fitted_models$fullroc
# View AUC
cat("Full ROC AUC:", round(model_fullroc$auc, 4), "\n")
#> Full ROC AUC: 0.7471
cat("Operating Points:", model_fullroc$summary$n_operating_points, "\n")
#> Operating Points: 11
# View diagnosticity table (top 10 points)
cat("\nTop 10 most diagnostic evidence categories:\n")
#>
#> Top 10 most diagnostic evidence categories:
print(head(model_fullroc$diagnosticity_table[, c("evidence_label",
"diagnosticity_ratio")], 10))
#> evidence_label diagnosticity_ratio
#> 13 suspect_5 22.0000000
#> 7 suspect_3 8.8000000
#> 10 suspect_4 6.7500000
#> 4 suspect_2 4.0000000
#> 1 suspect_1 2.3333333
#> 14 filler_5 1.3333333
#> 11 filler_4 1.1111111
#> 8 filler_3 0.5312500
#> 5 filler_2 0.4920635
#> 2 filler_1 0.4666667Interpreting Full ROC:
These methods estimate different quantities; their displayed numbers are not a common model-selection scale and the wrapper does not declare a “best model.”
Use 2-HT when:
Use EIG when:
Use Full ROC when:
The 2-HT fit reports AIC/BIC, but these values are comparative rather than absolute. They can only be compared with alternative likelihood models fitted to the same observations and outcome representation; they cannot be compared with EIG or AUC. A single-condition 2-HT fit is saturated: it has zero residual degrees of freedom, so it reproduces the observed counts exactly and its absolute fit cannot be tested.
cat("2-HT Model Fit:\n")
#> 2-HT Model Fit:
cat(" AIC:", round(comparison$fitted_models$`2ht`$aic, 2), "\n")
#> AIC: 1123.62
cat(" BIC:", round(comparison$fitted_models$`2ht`$bic, 2), "\n")
#> BIC: 1141.21
cat("\nAIC and BIC compare likelihood-based models fitted to the same data.\n")
#>
#> AIC and BIC compare likelihood-based models fitted to the same data.
cat("EIG and full-ROC AUC are not likelihoods, so they cannot be ranked\n")
#> EIG and full-ROC AUC are not likelihoods, so they cannot be ranked
cat("against the 2-HT model by information criteria.\n")
#> against the 2-HT model by information criteria.# Fit only 2-HT and EIG (skip Full ROC)
comparison_subset <- compare_models(
lineup_data,
models = c("2ht", "eig"),
lineup_size = 6,
prior_guilt = 0.5
)
print(comparison_subset)
#>
#> === Lineup Model Comparison ===
#>
#> Models fit: 2ht, eig
#> Sample size: 600
#> Lineup size: 6
#>
#> Comparison Table:
#> Model Measure Value
#> 2-HT (Winter et al., 2022) Log-likelihood -557.8100
#> 2-HT (Winter et al., 2022) AIC 1123.6200
#> 2-HT (Winter et al., 2022) BIC 1141.2100
#> 2-HT (Winter et al., 2022) dP (detection presence) 0.4850
#> 2-HT (Winter et al., 2022) dA (detection absence) 0.0490
#> EIG (Starns et al., 2023) Expected Information Gain 0.1869
#> EIG (Starns et al., 2023) Information Efficiency 18.7000
#> EIG (Starns et al., 2023) Prior Entropy 1.0000
#> Interpretation
#> Model fit quality
#> Lower is better
#> Lower is better
#> 0 to 1
#> 0 to 1
#> Higher is better (bits)
#> Percentage (0-100%)
#> Maximum possible IG
#>
#> Metrics describe different estimands and are not a single model-selection scale.
#>
#> Access fitted models via: $fitted_models$<model_name>
#> Available models: 2ht, eig# Bin confidence for EIG and Full ROC
comparison_binned <- compare_models(
lineup_data,
models = c("eig", "fullroc"),
lineup_size = 6,
confidence_bins = c(0, 40, 70, 100) # Low, medium, high
)
cat("Binned confidence results:\n")
#> Binned confidence results:
print(comparison_binned)
#>
#> === Lineup Model Comparison ===
#>
#> Models fit: eig, fullroc
#> Sample size: 600
#> Lineup size: 6
#>
#> Comparison Table:
#> Model Measure Value
#> EIG (Starns et al., 2023) Expected Information Gain 0.1693
#> EIG (Starns et al., 2023) Information Efficiency 16.9000
#> EIG (Starns et al., 2023) Prior Entropy 1.0000
#> Full ROC (Smith & Yang, 2020) Full AUC 0.7274
#> Full ROC (Smith & Yang, 2020) Operating Points 3.0000
#> Full ROC (Smith & Yang, 2020) Max Hit Rate 1.0000
#> Interpretation
#> Higher is better (bits)
#> Percentage (0-100%)
#> Maximum possible IG
#> Higher is better (0-1)
#> Number of decision criteria
#> Cumulative (0-1)
#>
#> Metrics describe different estimands and are not a single model-selection scale.
#>
#> Access fitted models via: $fitted_models$<model_name>
#> Available models: eig, fullrocsim_data <- as.data.frame(simulate_lineup_data(
n_tp = 300, n_ta = 300, d_prime = 1.5, c_criterion = 0.9,
lineup_size = 6, conf_levels = 5,
foil_bias = c(0.4, 0.2, 0.0, -0.4, -1.0), seed = 2026
))
sim_comparison <- compare_models(
sim_data,
models = c("2ht", "eig", "fullroc"),
lineup_size = 6
)
print(sim_comparison)
#>
#> === Lineup Model Comparison ===
#>
#> Models fit: 2ht, eig, fullroc
#> Sample size: 600
#> Lineup size: 6
#>
#> Comparison Table:
#> Model Measure Value
#> 2-HT (Winter et al., 2022) Log-likelihood -559.1500
#> 2-HT (Winter et al., 2022) AIC 1126.3000
#> 2-HT (Winter et al., 2022) BIC 1143.8900
#> 2-HT (Winter et al., 2022) dP (detection presence) 0.4950
#> 2-HT (Winter et al., 2022) dA (detection absence) 0.0260
#> EIG (Starns et al., 2023) Expected Information Gain 0.2264
#> EIG (Starns et al., 2023) Information Efficiency 22.6000
#> EIG (Starns et al., 2023) Prior Entropy 1.0000
#> Full ROC (Smith & Yang, 2020) Full AUC 0.7858
#> Full ROC (Smith & Yang, 2020) Operating Points 11.0000
#> Full ROC (Smith & Yang, 2020) Max Hit Rate 1.0000
#> Interpretation
#> Model fit quality
#> Lower is better
#> Lower is better
#> 0 to 1
#> 0 to 1
#> Higher is better (bits)
#> Percentage (0-100%)
#> Maximum possible IG
#> Higher is better (0-1)
#> Number of decision criteria
#> Cumulative (0-1)
#>
#> Metrics describe different estimands and are not a single model-selection scale.
#>
#> Access fitted models via: $fitted_models$<model_name>
#> Available models: 2ht, eig, fullrocA single fit tells you little. A useful check is whether the 2-HT parameters move as the generating d′ moves. The data come from a signal detection (MAX) model, not from the 2-HT model, so there are no true 2-HT parameter values to recover:
recovery <- do.call(rbind, lapply(c(0.5, 1.0, 1.5, 2.0, 2.5), function(dp) {
d <- as.data.frame(simulate_lineup_data(
n_tp = 400, n_ta = 400, d_prime = dp, c_criterion = 0.9,
lineup_size = 6, conf_levels = 5,
foil_bias = c(0.4, 0.2, 0.0, -0.4, -1.0), seed = 500 + round(10 * dp)
))
fit <- fit_winter_2ht(d, lineup_size = 6,
target_present = "target_present",
identification = "identification")
data.frame(`True d'` = dp,
`Fitted dP` = round(fit$parameters[["dP"]], 3),
`Fitted dA` = round(fit$parameters[["dA"]], 3),
check.names = FALSE)
}))
knitr::kable(
recovery, row.names = FALSE,
caption = "2-HT detection parameters fitted to data simulated from a signal detection model at known d', 400 lineups per arm."
)| True d’ | Fitted dP | Fitted dA |
|---|---|---|
| 0.5 | 0.113 | 0.038 |
| 1.0 | 0.313 | 0.140 |
| 1.5 | 0.522 | 0.169 |
| 2.0 | 0.677 | 0.124 |
| 2.5 | 0.806 | 0.214 |
dP increases monotonically with the generating d′. That
shows the parameter is sensitive to memory strength. It is not parameter
recovery: dP is not on the same scale as d′, the generating
model is not the 2-HT model, and the check says nothing about bias,
interval coverage or whether the process interpretation is correct.
Recovery would need data simulated from fit_winter_2ht()’s
own equations.
Research question: How well does this lineup procedure discriminate between guilty and innocent suspects?
# Fit all models to assess procedure
procedure_eval <- compare_models(
lineup_data,
models = c("2ht", "eig", "fullroc")
)
fm <- procedure_eval$fitted_models
knitr::kable(
data.frame(
Measure = c("2-HT dP (SE)", "EIG, bits (share of prior entropy)",
"Full ROC AUC (diagnosticity ordering)"),
Estimate = c(sprintf("%.3f (%.3f)", fm$`2ht`$parameters[["dP"]],
fm$`2ht`$se[["dP"]]),
sprintf("%.3f (%.0f%%)", fm$eig$eig,
100 * fm$eig$eig / fm$eig$prior_entropy),
sprintf("%.3f", fm$fullroc$auc))
),
row.names = FALSE,
caption = "Three summaries of the same simulated procedure, prior guilt .5."
)| Measure | Estimate |
|---|---|
| 2-HT dP (SE) | 0.485 (NA) |
| EIG, bits (share of prior entropy) | 0.187 (19%) |
| Full ROC AUC (diagnosticity ordering) | 0.747 |
The table reports estimates, not verdicts. No validated benchmarks
exist that would let a value of dP, EIG or AUC be called
good or poor in itself; the vignette("fullroc_analysis")
makes the same point about AUC. These numbers are informative when
compared with another procedure or condition analysed the same way, with
their uncertainty.
Use 2-HT model to check for bias:
params <- comparison$fitted_models$`2ht`$parameters
model_2ht <- comparison$fitted_models$`2ht`
knitr::kable(
data.frame(Parameter = "b (biased suspect selection)",
Estimate = round(params[["b"]], 3),
`95% Wald interval` = sprintf("[%.3f, %.3f]",
max(0, params[["b"]] - 1.96 * model_2ht$se[["b"]]),
min(1, params[["b"]] + 1.96 * model_2ht$se[["b"]])),
check.names = FALSE),
row.names = FALSE,
caption = "2-HT bias parameter, simulated data (300 target-present and 300 target-absent lineups)."
)| Parameter | Estimate | 95% Wald interval |
|---|---|---|
| b (biased suspect selection) | 0 | [NA, NA] |
b is the probability that a witness who detects nothing
picks the suspect because the suspect stands out. An interval clearly
above zero is a reason to inspect the lineup; how large a b
is acceptable is a policy judgement that the model cannot make. Note
that the interval is a Wald interval clipped to [0, 1];
boot_winter_2ht() gives bootstrap intervals that respect
the boundary.
knitr::kable(
data.frame(
Parameter = c("dP (detect culprit present)",
"dA (detect culprit absent)",
"g (guessing-based selection)"),
Estimate = round(c(params[["dP"]], params[["dA"]], params[["g"]]), 3)
),
row.names = FALSE,
caption = "2-HT detection and guessing parameters, simulated data."
)| Parameter | Estimate |
|---|---|
| dP (detect culprit present) | 0.485 |
| dA (detect culprit absent) | 0.049 |
| g (guessing-based selection) | 0.754 |
Detection and guessing are separately identified here, which is the
point of the model: an identification rate on its own cannot tell you
whether witnesses recognised the culprit or simply picked someone
plausible. Note that dP and g are
probabilities of different events and are not in competition — a
procedure can have both high detection and a high tendency to choose
when detection fails.
# Console-friendly table
table_console <- format_comparison_table(comparison, format = "console")
print(table_console)
#> Model Measure Value
#> 1 2-HT (Winter et al., 2022) Log-likelihood -557.8100
#> 2 2-HT (Winter et al., 2022) AIC 1123.6200
#> 3 2-HT (Winter et al., 2022) BIC 1141.2100
#> dP 2-HT (Winter et al., 2022) dP (detection presence) 0.4850
#> dA 2-HT (Winter et al., 2022) dA (detection absence) 0.0490
#> 11 EIG (Starns et al., 2023) Expected Information Gain 0.1869
#> 12 EIG (Starns et al., 2023) Information Efficiency 18.7000
#> 13 EIG (Starns et al., 2023) Prior Entropy 1.0000
#> 14 Full ROC (Smith & Yang, 2020) Full AUC 0.7471
#> 15 Full ROC (Smith & Yang, 2020) Operating Points 11.0000
#> 16 Full ROC (Smith & Yang, 2020) Max Hit Rate 1.0000
#> Interpretation
#> 1 Model fit quality
#> 2 Lower is better
#> 3 Lower is better
#> dP 0 to 1
#> dA 0 to 1
#> 11 Higher is better (bits)
#> 12 Percentage (0-100%)
#> 13 Maximum possible IG
#> 14 Higher is better (0-1)
#> 15 Number of decision criteria
#> 16 Cumulative (0-1)table_md <- format_comparison_table(comparison, format = "markdown")
# One element per line, so sep = "\n" is required. Plain cat(table_md) joins
# the lines with spaces and the table collapses into a single paragraph.
cat(table_md, sep = "\n")| Model | Measure | Value | Interpretation |
|---|---|---|---|
| 2-HT (Winter et al., 2022) | Log-likelihood | -557.8100 | Model fit quality |
| 2-HT (Winter et al., 2022) | AIC | 1123.6200 | Lower is better |
| 2-HT (Winter et al., 2022) | BIC | 1141.2100 | Lower is better |
| 2-HT (Winter et al., 2022) | dP (detection presence) | 0.4850 | 0 to 1 |
| 2-HT (Winter et al., 2022) | dA (detection absence) | 0.0490 | 0 to 1 |
| EIG (Starns et al., 2023) | Expected Information Gain | 0.1869 | Higher is better (bits) |
| EIG (Starns et al., 2023) | Information Efficiency | 18.7000 | Percentage (0-100%) |
| EIG (Starns et al., 2023) | Prior Entropy | 1.0000 | Maximum possible IG |
| Full ROC (Smith & Yang, 2020) | Full AUC | 0.7471 | Higher is better (0-1) |
| Full ROC (Smith & Yang, 2020) | Operating Points | 11.0000 | Number of decision criteria |
| Full ROC (Smith & Yang, 2020) | Max Hit Rate | 1.0000 | Cumulative (0-1) |
This vignette deliberately gives no poor/moderate/good bands for the 2-HT parameters, EIG or AUC. No study has calibrated such bands, and cut-offs of that kind would read as findings when they are conventions. Interpret each estimate on its own scale, with its uncertainty and its assumptions:
prior_guilt (1 bit only at .5). Compare procedures at
the same prior.# Small sample may lead to unreliable estimates
small_data <- simulate_lineup_data(n_tp = 30, n_ta = 30, d_prime = 1.5, conf_levels = 5)
small_comparison <- compare_models(
small_data,
models = c("2ht", "eig"),
show_warnings = FALSE
)
cat("Warning: Small samples (n=60) may produce unstable parameter estimates\n")
#> Warning: Small samples (n=60) may produce unstable parameter estimates
cat("Recommend: n ≥ 100 per condition for reliable model fitting\n")
#> Recommend: n ≥ 100 per condition for reliable model fittingIf 2-HT model fails to converge:
cat("Important: Don't directly compare across models!\n\n")
#> Important: Don't directly compare across models!
cat("✗ Wrong: 'dP = 0.6 is better than EIG = 0.4'\n")
#> ✗ Wrong: 'dP = 0.6 is better than EIG = 0.4'
cat(" → These are different scales and constructs\n\n")
#> → These are different scales and constructs
cat("✓ Right: 'dP rose from 0.45 to 0.60 between conditions, and EIG\n")
#> ✓ Right: 'dP rose from 0.45 to 0.60 between conditions, and EIG
cat(" (prior .5) rose from 0.30 to 0.40 bits'\n")
#> (prior .5) rose from 0.30 to 0.40 bits'
cat(" → Compare each measure with itself across conditions\n")
#> → Compare each measure with itself across conditionsDon’t rely on a single model: each answers a different question, as set out in The Three Models above. The 2-HT model separates detection from guessing and bias, EIG summarises how much the responses tell an investigator about guilt, and the full ROC measures investigator discriminability.
# Always fit at least 2-3 models
comprehensive <- compare_models(
lineup_data,
models = c("2ht", "eig", "fullroc")
)
cat("Fitting multiple models provides:\n")
#> Fitting multiple models provides:
cat(" • Process-level understanding (2-HT)\n")
#> • Process-level understanding (2-HT)
cat(" • Overall diagnosticity (EIG)\n")
#> • Overall diagnosticity (EIG)
cat(" • Threshold-free performance (Full ROC)\n")
#> • Threshold-free performance (Full ROC)When publishing, consider reporting results from all fitted models:
cat("In your paper, report:\n")
#> In your paper, report:
cat(" • 2-HT: Parameters with SE, AIC/BIC, convergence\n")
#> • 2-HT: Parameters with SE, AIC/BIC, convergence
cat(" • EIG: Value in bits, efficiency percentage\n")
#> • EIG: Value in bits, efficiency percentage
cat(" • Full ROC: AUC, number of operating points\n")
#> • Full ROC: AUC, number of operating points
cat(" • What each model's estimates say about the procedure\n")
#> • What each model's estimates say about the proceduresummary(comparison$fitted_models$`2ht`)
#>
#> Winter et al. (2022) Two-High Threshold MPT Model
#> ==================================================
#>
#> Sample size:
#> Target-present: 300
#> Target-absent: 300
#> Total: 600
#> Lineup size: 6
#>
#> Parameter Estimates:
#> Estimate SE Lower_95 Upper_95
#> dP (culprit presence) 0.4853 NA NA NA
#> dA (culprit absence) 0.0494 NA NA NA
#> b (biased selection) 0.0000 NA NA NA
#> g (guessing) 0.7539 NA NA NA
#>
#>
#> Model Fit:
#> Log-likelihood: -557.81
#> AIC: 1123.62
#> BIC: 1141.21
#>
#> Observed vs. Expected Counts:
#>
#> Target-Present Lineups:
#> Observed Expected Residual
#> Suspect ID 165 165 1.000703e-06
#> Filler ID 97 97 -6.372616e-07
#> Reject 38 38 -3.634414e-07
#>
#> Target-Absent Lineups:
#> Observed Expected Residual
#> Suspect ID 33 35.8 -2.833333e+00
#> Filler ID 182 179.2 2.833334e+00
#> Reject 85 85.0 -3.343707e-07
#>
#> Model identification:
#> Saturated (just-identified): 4 parameters, 4 data df, 0 residual df.
#> Absolute fit is not testable from a single condition (the model reproduces the
#> data by construction); use multiple conditions or constraints to test fit.
#> Residual obs-vs-expected discrepancy = 0.2688 (~= 0 confirms convergence).The summary’s identification note is the part to read. With one
target-present and one target-absent condition the 2-HT model has zero
residual degrees of freedom, so it reproduces the counts exactly and
there is no goodness-of-fit test to read. Convergence, identifiability
(enough rejections to estimate dA) and fit are separate
questions. Fit can be tested only with several conditions and parameters
constrained across them, as in Winter et al. (2022).
The three models are not three estimates of one quantity, and cannot be ranked against each other directly.
| Model | Estimand | Scale | Inference available |
|---|---|---|---|
| 2-HT | Latent probabilities of detection, bias and guessing | Four probabilities in [0, 1] | Standard errors from the observed information matrix; bootstrap
intervals via boot_winter_2ht(). No fit test for a single
condition (zero residual df) |
| EIG | Expected reduction in uncertainty about guilt | Bits, 0 to the prior entropy | None in this package. Tuttle, Starns and Cohen (2025) give 95% credible intervals for EIG from a Bayesian multinomial model of the response proportions; adding this is planned |
| Full ROC | Investigator discriminability | AUC in [0, 1], .5 = no discrimination | None in this package; see the note on ordering bias below |
The model is a multinomial processing tree. Response probabilities are as given in the introduction, and the parameters are estimated by maximum likelihood over the six observed cell counts (three responses by two lineup types). The target-present and target-absent totals are fixed, so the data have 2 × (3 − 1) = 4 degrees of freedom, the same as the number of parameters: a single-condition fit is saturated, with zero residual degrees of freedom (Winter et al., 2022, Figure 1). The standard errors measure how sharply the data pin each parameter down. At the best-fitting values, the log-likelihood falls away steeply in some directions and gently in others; that curvature, the Hessian matrix of second derivatives, is computed numerically, and its inverse gives the sampling variances of the estimates. A flat likelihood (little curvature) means a large standard error. Winter et al. (2022) describe the model and its estimation in full.
Identifiability. The detect-absence parameter
dA is identified almost entirely by rejections. If very few
witnesses reject, the likelihood is close to flat in dA and
the optimiser fails. fit_winter_2ht() checks for this and
reports it as such, rather than failing with a message about non-finite
differences.
Expected information gain is computed over the observed response
categories with the prior supplied by prior_guilt. Because
it is an expectation over a set of categories estimated from the data,
it inherits the binning issue discussed in
vignette("calibration_decision_analysis"), and it has no
standard error here.
Computed as described in vignette("fullroc_analysis").
The default ordering by sample diagnosticity ratio is
optimistically biased, because the ordering is
estimated from the same data used to compute the area; on data with no
signal at all it returns AUC near 0.65 at 100 lineups per arm.
compare_models() uses the default ordering, so treat the
AUC it reports as an upper bound rather than an estimate, and refit with
order = "apriori" for anything inferential.
lineup_size passed here,
and all three models will be misspecified in the same direction. A
member nobody chose in the sample is not necessarily such a foil: in a
fair six-person lineup, a given member receives no choices from ten
witnesses with probability (5/6)^10 ≈ .16. Fitzgerald, Tredoux and Juncu
(2023) discuss correcting error-rate estimates for unequal foil choices
by using effective rather than nominal size; passing an effective size
to these models is planned for a future version.Key Functions:
compare_models(): Fit multiple modelsprint.model_comparison(): View comparison tablesummary.model_comparison(): Detailed resultsplot.model_comparison(): Visualizationsformat_comparison_table(): Publication tablesWhen to Use Each Model:
Best Practices:
Fitzgerald, R. J., Tredoux, C. G., & Juncu, S. (2023). Estimation of eyewitness error rates in fair and biased lineups. Law and Human Behavior, 47(4), 463-483.
Smith, A. M., Yang, Y., & Wells, G. L. (2020). Distinguishing between investigator discriminability and eyewitness discriminability: A method for creating full receiver operating characteristic curves of lineup identification performance. Perspectives on Psychological Science, 15(3), 589-607.
Starns, J. J., Cohen, A. L., & Rotello, C. M. (2023). A complete method for assessing the effectiveness of eyewitness identification procedures: Expected information gain. Psychological Review, 130(3), 677–719. https://doi.org/10.1037/rev0000332
Tuttle, M. D., Starns, J. J., & Cohen, A. L. (2025). Protecting the innocent in eyewitness identification: An analysis of simultaneous and ranking lineups. Journal of Memory and Language, 140, 104581. https://doi.org/10.1016/j.jml.2024.104581
Winter, K., Menne, N. M., Bell, R., & Buchner, A. (2022). Experimental validation of a multinomial processing tree model for analyzing eyewitness identification decisions. Scientific Reports, 12, 15571.
?compare_models for complete parameter
documentationThese binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.