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.
This vignette demonstrates the two-high-threshold (2-HT) model for lineup data introduced and validated by Winter, Menne, Bell and Buchner (2022).
Attribution. The model, its parameterisation and its validation are theirs; they report four experiments, one targeting each parameter. The implementation here was written from the published model equations and adapted into this package’s data conventions. No code was reused.
The model is a multinomial processing tree (MPT). MPT models differ from signal detection theory, and the two are easily confused.
Signal detection theory posits a continuous evidence variable and a criterion placed on it. An MPT instead posits a small number of discrete latent states, each reached with some probability, with the observable response determined by which state the witness lands in. The parameters are the branching probabilities of that tree. Neither is “the” right description; they answer different questions, and the MPT has the practical advantage that its parameters map onto processes one can manipulate experimentally — which is what Winter et al. did to validate them.
For a lineup the tree runs as follows. A witness either detects the state of the lineup or does not. If they do not detect, they either select someone because the suspect stands out, or they guess, or they reject.
Formally, for a lineup of size \(L\):
Target-present lineups
\[ \begin{aligned} P(\text{suspect ID}) &= d_P + (1-d_P)\bigl[b + (1-b)\,g\,\tfrac{1}{L}\bigr] \\ P(\text{filler ID}) &= (1-d_P)(1-b)\,g\,\tfrac{L-1}{L} \\ P(\text{reject}) &= (1-d_P)(1-b)(1-g) \end{aligned} \]
Target-absent lineups
\[ \begin{aligned} P(\text{suspect ID}) &= (1-d_A)\bigl[b + (1-b)\,g\,\tfrac{1}{L}\bigr] \\ P(\text{reject}) &= d_A + (1-d_A)(1-b)(1-g) \end{aligned} \]
dP — detection of culprit presence. The probability that a witness viewing a target-present lineup recognises the culprit. This is the memory parameter for guilty lineups: a witness in this state identifies the suspect for the right reason.
dA — detection of culprit absence. The probability that a witness viewing a target-absent lineup recognises that the culprit is not there. Note this is a separate process from dP, which is what makes the model “two-high-threshold”: detecting presence and detecting absence are not two ends of one continuum but two distinct states, either of which may fail. A witness may fail to detect absence without thereby detecting presence.
b — biased suspect selection. Given that no detection occurred, the probability of choosing the suspect because the suspect stands out — not because of memory, and not by chance. This is the lineup fairness parameter, and it is treated in detail below.
g — guessing-based selection. Given no detection and no bias, the probability of choosing someone anyway rather than rejecting. This is willingness to choose, and in a fair lineup the choice falls on the suspect with probability \(1/L\) and on a foil with probability \((L-1)/L\) — which is why those fractions appear in the equations.
Read together: dP and dA are about memory, g is about willingness to choose, and b is about the lineup itself. Separating those three is the point of the exercise, since an identification rate confounds all of them.
The implementation in r4lineups accepts either raw data (data frame) or aggregated counts.
We use the built-in lineup_example dataset, which
already contains the required columns:
First, convert the data to the 2 × 3 count format expected by
fit_winter_2ht():
counts <- c(
n_tp_suspect = sum(lineup_example$target_present & lineup_example$identification == "suspect"),
n_tp_filler = sum(lineup_example$target_present & lineup_example$identification == "filler"),
n_tp_reject = sum(lineup_example$target_present & lineup_example$identification == "reject"),
n_ta_suspect = sum(!lineup_example$target_present & lineup_example$identification == "suspect"),
n_ta_filler = sum(!lineup_example$target_present & lineup_example$identification == "filler"),
n_ta_reject = sum(!lineup_example$target_present & lineup_example$identification == "reject")
)
fit <- fit_winter_2ht(counts, lineup_size = 6)
fit$parameters
#> dP dA b g
#> 0.526176573 0.106690695 0.009275303 0.796104687The second plot is the one to check before interpreting anything: it compares observed response proportions against those the fitted model predicts. Visible discrepancies mean the model is not describing the data, and the parameters should not be read as though it were.
set.seed(123)
boot_fit <- boot_winter_2ht(
counts,
lineup_size = 6,
nboot = 200,
conf_level = 0.95,
method = "percentile"
)
boot_fit$ci
#> Lower Upper
#> dP 0.47233774 0.56248806
#> dA 0.02781788 0.17171187
#> b 0.00000000 0.04308407
#> g 0.74713037 0.83854453The percentile intervals stay inside the parameter space, and
plot_2ht_parameters() uses them when given a bootstrap
object:
All four parameters are probabilities bounded in [0, 1]. At a
boundary — b here — regular Hessian standard errors are
unavailable. Percentile intervals respect the parameter space, but this
alone does not establish nominal coverage at a boundary.
For larger datasets, increase nboot to 1000 or more for
stable intervals.
b tells you about lineup fairnessThis is arguably the most useful thing the model does.
Classical lineup fairness measures — Tredoux’s E′, functional size, the proportion of mock witnesses choosing the suspect — are computed from mock-witness data: people who never saw the culprit, choosing from a description. They ask whether the suspect stands out.
The 2-HT model answers the same question from the witness
data themselves, with no mock-witness study required. Look
again at where b sits in the tree: it is the probability of
choosing the suspect given that no detection occurred and before any
guessing. A witness in that state has no memorial basis for
choosing the suspect, so if they do, it is because the lineup pointed
them there.
In a perfectly fair lineup, b = 0: a non-detecting
witness who chooses lands on the suspect with probability \(1/L\), purely by chance, and that is
already captured by the g/L term. Any
b above zero is a measure of unfairness, on a
probability scale, with a confidence interval.
The claim can be checked against Tredoux’s (1998) E′, computed with
esize_T() as \(1/\sum
p_i^2\) over choices across all six members (it is defined in the
main vignette). E′ measures the effective number of functioning
alternatives.
Note where E′ comes from here: the target-absent lineups. Their choice distribution across members gives a resultant effective size, as used by Fitzgerald, Tredoux and Juncu (2023), without running a separate mock-witness study. It is not the same measurement as a mock-witness E′: real witnesses have a memory of the culprit and may reject the lineup, whereas mock witnesses choose from a description, and the two families of measures can differ (Lee, Mansour and Penrod, 2024). See the main vignette on the small-sample properties of the estimate, which are not negligible.
scenarios <- list(
"Fair: foils match suspect" = rep( 0.0, 5),
"Uniformly weak foils" = rep(-1.6, 5),
"Uneven foils, suspect typical" = c( 1.6, 0.9, -1.5, -2.5, -3.5),
"Uneven foils, one dominant" = c( 2.5, -3.0, -3.0, -3.0, -3.0)
)
fairness <- do.call(rbind, lapply(names(scenarios), function(nm) {
d <- as.data.frame(simulate_lineup_data(
n_tp = 1500, n_ta = 1500, d_prime = 1.4, c_criterion = 0.9,
lineup_size = 6, conf_levels = 5,
foil_bias = scenarios[[nm]], seed = 101
))
fit_s <- fit_winter_2ht(d, lineup_size = 6,
target_present = "target_present",
identification = "identification")
ta <- d[!d$target_present, ]
chosen <- ta$lineup_member[ta$lineup_member > 0]
E <- esize_T(table(factor(chosen, levels = 1:6)))
data.frame(
`Lineup` = nm,
`b (2-HT)` = round(fit_s$parameters[["b"]], 3),
`E' (Tredoux)` = round(E, 2),
`dP (2-HT)` = round(fit_s$parameters[["dP"]], 3),
check.names = FALSE
)
}))
knitr::kable(
fairness, row.names = FALSE,
caption = "The 2-HT bias parameter against effective size (Tredoux, 1998), on the same simulated data (1500 target-present and 1500 target-absent lineups, nominal size 6)."
)| Lineup | b (2-HT) | E’ (Tredoux) | dP (2-HT) |
|---|---|---|---|
| Fair: foils match suspect | 0.000 | 6.00 | 0.453 |
| Uniformly weak foils | 0.251 | 1.25 | 0.586 |
| Uneven foils, suspect typical | 0.000 | 2.00 | 0.213 |
| Uneven foils, one dominant | 0.000 | 1.06 | 0.067 |
Weakening all the foils uniformly — the first two rows — makes
b rise and E′ fall together, which is the expected
behaviour.
But b and E′ are not the same measure, and the
last two rows show it. With uneven foils, E′ collapses to 2.00
and then 1.06, because a single dominant foil absorbs most of the
choices and the lineup has almost no functioning alternatives.
b stays at zero throughout — correctly, because in those
lineups the suspect is chosen below chance and is, if anything,
protected by the arrangement. b measures whether the
suspect stands out; E′ measures how many
alternatives are functioning. Report both.
The practical value of b is that it is available
when mock-witness data are not: a lineup already
administered to real witnesses can be assessed for suspect bias
retrospectively.
Notice the dP column across the first two rows. Apparent
detection of culprit presence rises as the foils are weakened
uniformly. That is not better memory. When the suspect stands out, the
culprit in a target-present lineup stands out too, so more witnesses
land on them — and the model, which has no way of knowing why, partly
books this as detection.
So an unfair lineup does not merely inflate false identifications; it
inflates the apparent memory performance of the procedure that produced
them. Report b alongside dP, never
dP alone. A high dP from a lineup
with a high b is not evidence of a good procedure.
Parameters are estimated by maximum likelihood over the six observed
cell counts (three responses by two lineup types), using the multinomial
likelihood implied by the equations above. The target-present and
target-absent totals are each fixed, so each three-category multinomial
contributes two independent proportions: four data degrees of freedom
against four free parameters, leaving zero residual degrees of
freedom. A single-condition fit is therefore saturated (just
identified), as Winter et al. (2022, Figure 1) state and as
summary() reports. The model reproduces the observed counts
by construction, so its absolute fit cannot be tested from one
condition. Testing it needs several conditions with parameters
constrained across them. Standard errors come from the numerically
differentiated Hessian at the optimum, and fit_winter_2ht()
reports AIC, BIC and the log-likelihood, all of which are well defined
here because this is a genuine maximum-likelihood fit.
Note the contrast with fit_lineup_models() in
vignette("lineup_memory_models"), which minimises Pearson
chi-square rather than maximising a likelihood, and for which AIC and
BIC are therefore returned as NA. The two should not be
compared by information criteria.
dA is identified almost entirely by rejections: it is
the probability of correctly concluding the culprit is absent, and a
witness in that state rejects. The amount of information depends on both
the response probabilities and sample size. A low rejection percentage
alone does not establish nonidentifiability. Inspect sparse cells,
boundary estimates and convergence; regular Hessian uncertainty is not
available at a boundary. In simulated data a liberal criterion can
produce few rejections — see
vignette("simulation_power_analysis") on why
c_criterion = 0 is not a neutral value.
fit_winter_2ht() returns regular Hessian standard errors
for interior fits; they are unavailable at a parameter boundary.
boot_winter_2ht() gives approximate percentile or BCa
intervals while keeping TP and TA totals fixed. Respecting the parameter
space is useful, but does not establish coverage near a boundary. The
plotting function uses the supplied intervals.
lineup_size is the number of
functioning alternatives. In a lineup with null foils
the nominal size overstates this, and since \(1/L\) appears directly in the equations,
g and b will be biased. Check
lineup_table() for null foils before fitting.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.
Lee, J., Mansour, J. K., & Penrod, S. D. (2024). How to measure lineup fairness: Concurrent and predictive validity of lineup-fairness measures. Psychology, Crime & Law. https://doi.org/10.1080/1068316X.2024.2307358
Malpass, R. S. (1981). Effective size and defendant bias in eyewitness identification lineups. Law and Human Behavior, 5(4), 299-309.
Tredoux, C. G. (1998). Statistical inference on measures of lineup fairness. Law and Human Behavior, 22(2), 217-237.
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.
?fit_winter_2ht, ?boot_winter_2ht,
?plot_2ht_parametersvignette("model_comparison") for fitting this model
alongside EIG and full ROCb recoversThese 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.