---
title: "Comparing Models for Eyewitness Identification Data"
author:
- "Colin G. Tredoux"
- "Tamsyn M. Naylor"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Comparing Models for Eyewitness Identification Data}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 5,
  warning = FALSE,
  message = FALSE
)
```

## Introduction

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:

1. **2-HT Model** (Winter et al., 2022): Multinomial Processing Tree model
2. **EIG** (Starns et al., 2023): Expected Information Gain
3. **Full ROC** (Smith & Yang, 2020): Receiver Operating Characteristic

The `compare_models()` function provides a unified interface for fitting all models and generating comparison tables and visualizations.

## The Three Models

### 2-HT Model: Latent Cognitive Processes

The Two-High Threshold (2-HT) model is a multinomial processing tree (MPT) that separates identification decisions into distinct latent processes:

**Parameters:**

- **dP**: Detection of culprit presence (0-1)
- **dA**: Detection of culprit absence (0-1)
- **b**: Biased suspect selection (0-1)
- **g**: Guessing-based selection (0-1)

**Model equations** (target-present):

- P(suspect ID) = dP + (1-dP) × [b + (1-b) × g × (1/L)]
- P(filler ID) = (1-dP) × (1-b) × g × ((L-1)/L)
- P(reject) = (1-dP) × (1-b) × (1-g)

**Use when:**

- You want to understand underlying cognitive mechanisms
- Testing theories about detection vs. guessing
- Comparing conditions with different bias levels

### EIG: Information-Theoretic Measure

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

- EIG = 0: No information (responses don't distinguish guilty/innocent)
- The maximum is the prior entropy, reached when every response resolves guilt
  completely. That is 1 bit only when the prior probability of guilt is .5; at a
  prior of .1 it is about .47 bits. Compare EIG values only at the same prior.
- Higher EIG = more diagnostic procedure

**Use when:**

- Evaluating overall diagnosticity of procedures
- Comparing different identification methods
- Assessing policy-relevant cutoffs

### Full ROC: Investigator Discriminability

Full ROC uses ALL witness responses (suspect IDs, filler IDs, rejections) to compute a threshold-free measure of discriminability:

**Measure:**

- AUC (Area Under the Curve)
- Range: 0 to 1. A value of .5 is the no-discrimination reference, not a lower
  bound: with the a-priori ordering a procedure that does worse than chance gives
  an AUC below .5.

**Use when:**

- Measuring investigator discriminability
- Comparing system variables (lineup procedures)
- Need threshold-free performance measure

## Basic Model Comparison

### Loading Data

```{r load_data}
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)
```

The `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:

```{r criterion_warning}
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)."
)
```

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.

### Comparing All Models

```{r compare_all_models}
# 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)
```


### Detailed Summary

```{r detailed_summary}
# More detailed output
summary(comparison)
```

### Visualizing Model Comparisons

```{r visualize_models, fig.width=10, fig.height=4.5}
# Side-by-side plots
plot(comparison, ncol = 3)
```

## Individual Model Access

### 2-HT Model Results

```{r model_2ht}
# 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."
)

cat("AIC:", round(model_2ht$aic, 2),
    " BIC:", round(model_2ht$bic, 2),
    " log-likelihood:", round(model_2ht$loglik, 2), "\n")
```

**Interpreting 2-HT parameters (when available):**

- **dP**: Probability of detecting guilty suspect
- **dA**: Probability of detecting the culprit's absence
- **b**: Bias toward suspect
- **g**: Tendency to guess vs. reject

### EIG Results

```{r model_eig}
# Access EIG model
model_eig <- comparison$fitted_models$eig

# View EIG value
cat("Expected Information Gain:", round(model_eig$eig, 4), "bits\n")
cat("Information Efficiency:",
    round(model_eig$eig / model_eig$prior_entropy * 100, 1), "%\n")

# Most informative responses
cat("\nTop 5 most informative response categories:\n")
print(head(model_eig$response_data[, c("response", "information_gain",
                                        "posterior_guilty")], 5))
```

**Interpreting EIG:**

- Total information provided: `r round(model_eig$eig, 3)` bits
- Efficiency: `r round(model_eig$eig / model_eig$prior_entropy * 100, 1)`% of maximum possible
- Most informative responses push posterior beliefs strongly toward guilt or innocence

### Full ROC Results

```{r model_fullroc}
# Access Full ROC model
model_fullroc <- comparison$fitted_models$fullroc

# View AUC
cat("Full ROC AUC:", round(model_fullroc$auc, 4), "\n")
cat("Operating Points:", model_fullroc$summary$n_operating_points, "\n")

# View diagnosticity table (top 10 points)
cat("\nTop 10 most diagnostic evidence categories:\n")
print(head(model_fullroc$diagnosticity_table[, c("evidence_label",
                                                  "diagnosticity_ratio")], 10))
```

**Interpreting Full ROC:**

- AUC = `r round(model_fullroc$auc, 3)`: Investigator's ability to discriminate
- Values closer to 1.0 indicate better discriminability
- All witness responses contribute to overall discriminability

## Method-Choice Guidance

These methods estimate different quantities; their displayed numbers are not a
common model-selection scale and the wrapper does not declare a “best model.”

### When to Use Each Model

**Use 2-HT when:**

- Testing cognitive theories (detection, guessing, bias)
- Need to separate memory from response bias
- Comparing procedures that might differ in bias
- Want to understand latent psychological processes

**Use EIG when:**

- Evaluating overall diagnosticity
- Comparing identification procedures
- Need a single summary measure
- Want information-theoretic interpretation

**Use Full ROC when:**

- Need threshold-free discriminability measure
- Comparing system variables
- Want to use all available information
- Policy/practical decision-making focus

### Comparing Model Fits

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.

```{r model_selection}
cat("2-HT Model Fit:\n")
cat("  AIC:", round(comparison$fitted_models$`2ht`$aic, 2), "\n")
cat("  BIC:", round(comparison$fitted_models$`2ht`$bic, 2), "\n")
cat("\nAIC and BIC compare likelihood-based models fitted to the same data.\n")
cat("EIG and full-ROC AUC are not likelihoods, so they cannot be ranked\n")
cat("against the 2-HT model by information criteria.\n")
```

## Advanced Usage

### Fitting Specific Models Only

```{r specific_models}
# 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)
```

### Using Confidence Bins

```{r confidence_bins}
# 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")
print(comparison_binned)
```

### Custom Prior for EIG

```{r custom_prior}
# Use different prior probability for EIG
comparison_prior <- compare_models(
  lineup_data,
  models = "eig",
  prior_guilt = 0.3  # Assume 30% base rate
)

cat("EIG with prior = 0.3:\n")
print(comparison_prior$fitted_models$eig$eig)
```

## Simulated Data Example

### Comparing Models with Known Parameters

```{r simulation_example}
sim_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)
```

### Sensitivity to the Generating d′

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

```{r parameter_recovery}
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."
)
```

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

## Practical Examples

### Example 1: Evaluating a Lineup Procedure

Research question: How well does this lineup procedure discriminate between guilty and innocent suspects?

```{r example_procedure}
# 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."
)
```

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.

### Example 2: Identifying Sources of Bias

Use 2-HT model to check for bias:

```{r example_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)."
)
```

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


### Example 3: Comparing Detection vs. Guessing

```{r example_detection}
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."
)
```

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.


## Formatted Comparison Tables

### Console Format

```{r table_console}
# Console-friendly table
table_console <- format_comparison_table(comparison, format = "console")
print(table_console)
```

### Markdown Format

```{r table_markdown, results='asis'}
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")
```

## Interpretation Guidelines

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:

- **2-HT parameters** are probabilities of latent states. Compare them across
  conditions, ideally with parameters constrained across conditions so that the
  differences can be tested.
- **EIG** runs from 0 to the prior entropy, which depends on `prior_guilt` (1 bit
  only at .5). Compare procedures at the same prior.
- **Full-ROC AUC** lies between 0 and 1, with .5 the no-discrimination reference.
  The default diagnosticity ordering is optimistically biased (see Technical
  Details), so compare procedures with the a-priori ordering.

## Common Pitfalls

### 1. Insufficient Sample Size

```{r pitfall_sample_size}
# 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")
cat("Recommend: n ≥ 100 per condition for reliable model fitting\n")
```

### 2. Model Convergence Issues

If 2-HT model fails to converge:

- Check data quality (sufficient variability)
- Try different starting parameters
- Ensure adequate sample size
- Consider data preprocessing

### 3. Comparing Non-Comparable Measures

```{r pitfall_comparison}
cat("Important: Don't directly compare across models!\n\n")
cat("✗ Wrong: 'dP = 0.6 is better than EIG = 0.4'\n")
cat("  → These are different scales and constructs\n\n")
cat("✓ Right: 'dP rose from 0.45 to 0.60 between conditions, and EIG\n")
cat("          (prior .5) rose from 0.30 to 0.40 bits'\n")
cat("  → Compare each measure with itself across conditions\n")
```

## Best Practices

### 1. Fit Multiple Models

Don't rely on a single model: each answers a different question, as set out in
[The Three Models](#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.

```{r best_practice_multiple}
# Always fit at least 2-3 models
comprehensive <- compare_models(
  lineup_data,
  models = c("2ht", "eig", "fullroc")
)

cat("Fitting multiple models provides:\n")
cat("  • Process-level understanding (2-HT)\n")
cat("  • Overall diagnosticity (EIG)\n")
cat("  • Threshold-free performance (Full ROC)\n")
```

### 2. Report All Model Results

When publishing, consider reporting results from all fitted models:

```{r best_practice_reporting}
cat("In your paper, report:\n")
cat("  • 2-HT: Parameters with SE, AIC/BIC, convergence\n")
cat("  • EIG: Value in bits, efficiency percentage\n")
cat("  • Full ROC: AUC, number of operating points\n")
cat("  • What each model's estimates say about the procedure\n")
```

### 3. Check Model Assumptions

```{r best_practice_assumptions}
summary(comparison$fitted_models$`2ht`)
```

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


## Technical Details

### What each model estimates, and on what scale

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 |

### 2-HT estimation

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.

### EIG

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.

### Full ROC

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.

### Assumptions common to all three

1. Each witness contributes one response, so the response categories partition
   the sample.
2. Target-present and target-absent lineups are independent samples.
3. All lineup members other than the suspect are functioning foils. Where some
   members are implausible alternatives, the effective lineup size is smaller
   than the nominal `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.

## Summary

**Key Functions:**

- `compare_models()`: Fit multiple models
- `print.model_comparison()`: View comparison table
- `summary.model_comparison()`: Detailed results
- `plot.model_comparison()`: Visualizations
- `format_comparison_table()`: Publication tables

**When to Use Each Model:**

- **2-HT**: Understanding cognitive processes
- **EIG**: Overall diagnosticity assessment
- **Full ROC**: Threshold-free discriminability

**Best Practices:**

- Fit multiple models for comprehensive analysis
- Report all model results
- Check convergence and identifiability; test fit only across constrained conditions
- Use adequate sample sizes (n ≥ 100 per condition)
- Interpret each model's estimates on their own scale

## References

::: {.hanging-indent}

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.

:::

## Further Reading

- See `?compare_models` for complete parameter documentation
- See vignette("simulation_power_analysis") for data simulation
- See vignette("pauc_statistical_comparison") for statistical comparison of conditions
