Skip to contents

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.

Introduction

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 general approach

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 LL:

Target-present lineups

P(suspect ID)=dP+(1−dP)[b+(1−b)g1L]P(filler ID)=(1−dP)(1−b)gL−1LP(reject)=(1−dP)(1−b)(1−g) \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

P(suspect ID)=(1−dA)[b+(1−b)g1L]P(reject)=dA+(1−dA)(1−b)(1−g) \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}

The four parameters

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/L1/L and on a foil with probability (L−1)/L(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.

Data format

We use the built-in lineup_example dataset, which already contains the required columns:

library(r4lineups)
data(lineup_example)
head(lineup_example)
#>   target_present lineup_member suspect_pos identification confidence
#> 1          FALSE             2           1         filler         40
#> 2          FALSE             3           1         filler         50
#> 3           TRUE             2           1         filler         40
#> 4           TRUE             1           1        suspect         80
#> 5           TRUE             1           1        suspect         60
#> 6          FALSE             0           1         reject         20

1. Fit the model using counts

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.796104687

2. Model diagnostics and plots

The 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.

3. Bootstrap confidence intervals

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.83854453

The 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.

4. What b tells you about lineup fairness

This 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/L1/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.

Does it work?

The claim can be checked against Tredoux’s (1998) E′, computed with esize_T() as 1/∑pi21/\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)."
)
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.

A caution that comes with it

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.

Technical Details

Estimation

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.

Identifiability

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.

Confidence intervals

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.

Assumptions

  1. Each witness contributes one lineup decision, so the six cells partition the sample.
  2. Target-present and target-absent lineups are independent samples.
  3. The detection processes are all-or-none: a witness either detects or does not, with no partial information. This is the “high threshold” assumption, and it is what distinguishes the model from a signal detection account.
  4. lineup_size is the number of functioning alternatives. In a lineup with null foils the nominal size overstates this, and since 1/L1/L appears directly in the equations, g and b will be biased. Check lineup_table() for null foils before fitting.

References

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.

Further Reading