---
title: "RAC Analysis: Response Time-Accuracy Characteristics"
author:
- "Colin G. Tredoux"
- "Tamsyn M. Naylor"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    toc: true
    toc_depth: 3
vignette: >
  %\VignetteIndexEntry{RAC Analysis: Response Time-Accuracy Characteristics}
  %\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

This vignette demonstrates **RAC (Response Time-Accuracy Characteristic)
analysis** for eyewitness lineup data.

The RAC *plot* is recent, and the implementation here follows pyWitness
(Mickes et al., 2024). The underlying idea is not recent at all. That
identification accuracy is related to how long a witness takes to decide has
been an active research question since at least the early 1990s: Sporer (1993)
reported that decision times for correct identifications were shorter than for
incorrect ones, and used decision time along with confidence to postdict
accuracy. What RAC adds is a display that puts response time on the same footing
as the CAC plot puts confidence, not a new finding.

## Why response time?

1. **Fast responses often indicate strong memory.** The intuition is that a
   strong memory produces recognition that feels automatic — the face is
   recognised rather than worked out by comparing candidates one against
   another. This is related to Tulving's notion of *ecphory*, the process by
   which a retrieval cue contacts a stored trace and produces the experience of
   remembering; a strong trace yields an immediate ecphoric experience, a weak
   one yields deliberation. Dunning and Perretta (2002) made the automaticity
   version of this argument explicitly.
2. **Response time is objective.** Unlike confidence, it is recorded rather than
   reported, so it is less exposed to post-event influence and to feedback from
   the lineup administrator.
3. **Complementary to confidence.** RAC and CAC together may give a fuller
   picture than either alone.
4. **Forensically practical.** Response time can be captured automatically in
   video and computer-administered lineups at no cost to the witness.

## The 10-to-12-second rule, and what happened to it

Any discussion of response time in this area has to deal with the best-known
claim about it. Dunning and Perretta (2002) reported that a boundary of roughly
10 to 12 seconds separated accurate from inaccurate positive identifications:
witnesses deciding faster than that were close to 90% accurate, those taking
longer around 50%.

The rule did not survive replication. Weber, Brewer, Wells, Semmler and Keast
(2004) analysed a large set of studies and found that although accurate
identifications were indeed faster than inaccurate ones, the *boundary* that
best separated them varied enormously between experiments — from about 5 seconds
to about 29 seconds. There is a real speed-accuracy relationship; there is no
universal number.

The practical consequence shapes how this vignette proceeds. **A time boundary should be estimated from the data in hand
rather than imported from another study.** The
[continuous approach](#a-continuous-alternative-to-binning) below does that.
It gives a point estimate of the boundary; an interval for the boundary itself
would need a further step, such as bootstrapping the whole fit, which this
vignette does not show.

# Setup

```{r load_package}
library(r4lineups)
library(ggplot2)
```

# Simulating Data with Response Times

Few existing datasets include response times, so we simulate. The choices are
generated by `simulate_lineup_data()`, which draws a memory strength for every
lineup member separately and gives each foil its own mean via `foil_bias`, so
the foils are not interchangeable.

Response times are then added by an explicit model: time falls as the strength
of the evidence rises, plus noise. This part is a construction, not a
simulation of any published finding, and the noise is set so that the
correlation between response time and confidence is moderate — about −0.5 —
rather than the near-perfect relationship that would make the demonstration
trivial.

```{r simulate_data}
set.seed(123)

lineup_data <- as.data.frame(simulate_lineup_data(
  n_tp = 600, n_ta = 600,
  d_prime     = 1.6,
  c_criterion = 0.9,
  lineup_size = 6,
  conf_levels = 9,
  foil_bias   = c(0.4, 0.2, 0.0, -0.5, -1.1),
  seed        = 123
))

# Response time falls as evidence strength (here, ordinal confidence) rises.
# The noise term is deliberately large: response time is a noisy indicator.
lineup_data$response_time <- round(pmax(
  800,
  14000 - 900 * lineup_data$confidence + rnorm(nrow(lineup_data), 0, 3500)
))

# Put confidence on a 20-100 scale, as the other vignettes do
lineup_data$confidence <- round(seq(20, 100, length.out = 9))[lineup_data$confidence]

table(lineup_data$target_present, lineup_data$identification)
```

```{r check_correlation}
cor(lineup_data$response_time, lineup_data$confidence)
```

# Response Time and Accuracy: the simplest analysis

Before any binning, look at the plain correlation between
response time and accuracy. Accuracy is binary, so this is a **point-biserial**
correlation — which is just Pearson's *r* with one dichotomous variable, and
needs no special machinery.

The choice of *which trials to include* is very important:

```{r point_biserial}
# All trials: "correct" means a suspect ID when the target was present, or a
# rejection when it was absent
all_trials <- lineup_data
all_trials$correct <- as.integer(
  (all_trials$target_present  & all_trials$identification == "suspect") |
  (!all_trials$target_present & all_trials$identification == "reject")
)

# Suspect identifications only. Among these, "correct" is simply target-present
choosers <- lineup_data[lineup_data$identification == "suspect", ]
choosers$correct <- as.integer(choosers$target_present)

knitr::kable(
  data.frame(
    Sample = c("All trials", "Suspect identifications only"),
    n = c(nrow(all_trials), nrow(choosers)),
    `Point-biserial r` = round(c(
      cor(all_trials$response_time, all_trials$correct),
      cor(choosers$response_time,   choosers$correct)
    ), 3),
    check.names = FALSE
  ),
  row.names = FALSE,
  caption = "Correlation between response time and accuracy, simulated data (1200 lineups of size 6)."
)
```

The correlation across all trials is close to zero; restricting to witnesses who
identified the suspect roughly triples it. In these simulated data that is a
consequence of how response time was generated: it depends on confidence alone,
and rejections and identifications relate to accuracy differently, so pooling
them mixes two relationships that partly cancel. In real data a further reason
is often proposed, that a witness who searches the lineup thoroughly and then
declines to choose is being careful rather than uncertain, but this simulation
does not model that process and cannot show it.

This is why RAC, like CAC, is computed on choosers. It is also a reminder that
the headline correlation in a response-time study depends on a decision that is
easy to leave unstated.

# Basic RAC Analysis

## Choosing time bins

`make_rac()` takes bin edges. Binning response time is conventional, and it is
what makes the RAC plot comparable to published ones, but it is **not
necessary**: response time is continuous and nothing forces us to discard that.
See [A continuous alternative](#a-continuous-alternative-to-binning) below.

Where bins are used, they must be chosen with reference to the data. Bins fixed
in advance frequently leave the extremes empty, and an accuracy estimated from
one or two observations will plot as a point indistinguishable from one based on
two hundred.

```{r time_bins}
# What is the actual range?
round(quantile(choosers$response_time, c(0, 0.25, 0.5, 0.75, 1)))

time_bins <- c(0, 5000, 7500, 10000, Inf)
```

## Compute and plot

```{r compute_rac}
rac_result <- make_rac(
  data        = lineup_data,
  lineup_size = 6,
  time_bins   = time_bins,
  show_plot   = FALSE
)

print(rac_result)
```

```{r plot_rac}
rac_result$plot + theme_minimal(base_size = 12)
```

Accuracy falls as responses get slower, from about `r round(100*rac_result$rac_data$accuracy[1])`%
in the fastest bin to about `r round(100*rac_result$rac_data$accuracy[nrow(rac_result$rac_data)])`%
in the slowest. The relationship is real but shallower than the corresponding
CAC relationship, which is the usual finding: response time carries information
about accuracy, but less of it than confidence does.

# RAC and CAC Compared

```{r rac_vs_cac}
rac <- make_rac(lineup_data, time_bins = time_bins, show_plot = FALSE)
cac <- make_cac(lineup_data, confidence_bins = c(0, 60, 80, 100),
                show_plot = FALSE)

rac$plot + ggtitle("RAC: response time and accuracy") +
  theme_minimal(base_size = 12)
```

```{r cac_plot}
cac$plot + ggtitle("CAC: confidence and accuracy") +
  theme_minimal(base_size = 12)
```

| Feature | RAC | CAC |
|---------|-----|-----|
| **X-axis** | Response time | Confidence |
| **Measure** | Recorded automatically | Self-reported |
| **Interpretation** | Faster is taken to indicate stronger memory | Higher confidence is taken to indicate stronger memory |
| **Robustness** | A recorded decision time cannot be changed by later feedback; earlier information, procedures and timing conventions can still affect it | Can be inflated by confirming feedback |
| **Typical strength** | Weaker relationship with accuracy | Stronger relationship with accuracy |

The two are related but not redundant: in these data the correlation between
confidence and response time is about `r round(cor(lineup_data$response_time, lineup_data$confidence), 2)`,
which leaves a good deal of independent variation.

# Fine-Grained Analysis

Finer bins reveal more structure, at the cost of stability:

```{r fine_bins}
fine_bins <- c(seq(0, 20000, by = 2500), Inf)

rac_fine <- make_rac(lineup_data, time_bins = fine_bins, show_plot = FALSE,
                     show_errorbars = TRUE, show_n = TRUE)

knitr::kable(
  as.data.frame(rac_fine$rac_data)[, c("response_time", "n_total", "accuracy")],
  digits = 3, row.names = FALSE,
  caption = "RAC with 2.5-second bins plus an overflow bin, simulated data. Note the small n in the extreme bins."
)
```

The extreme bins carry few observations, and their accuracy estimates should not
be read as though they were as trustworthy as the well-populated middle. Always
inspect `n_total`.

This is the fundamental awkwardness of binning: wide bins hide the shape of the
relationship, narrow bins produce estimates too noisy to interpret, and there is
no setting that avoids both. The next section does away with the choice.

# A continuous alternative to binning {#a-continuous-alternative-to-binning}

Response time is continuous, and accuracy among suspect identifications is
binary — which is exactly the situation logistic regression was designed for.
Rather than cutting time into bins and computing a proportion within each, we
can estimate accuracy as a smooth function of time directly.

A spline term lets the shape be determined by the data rather than assumed:

```{r continuous_rac}
fit <- glm(correct ~ splines::ns(response_time, df = 3),
           family = binomial, data = choosers)

grid <- data.frame(
  response_time = seq(min(choosers$response_time),
                      max(choosers$response_time), length.out = 200)
)
pred <- predict(fit, newdata = grid, type = "link", se.fit = TRUE)
grid$accuracy <- plogis(pred$fit)
grid$lower    <- plogis(pred$fit - 1.96 * pred$se.fit)
grid$upper    <- plogis(pred$fit + 1.96 * pred$se.fit)
```

```{r continuous_plot}
ggplot(grid, aes(x = response_time / 1000, y = accuracy)) +
  geom_ribbon(aes(ymin = lower, ymax = upper), alpha = 0.2, fill = "steelblue") +
  geom_line(linewidth = 1, colour = "steelblue") +
  geom_hline(yintercept = 0.9, linetype = "dashed", colour = "red") +
  geom_rug(data = choosers,
           aes(x = response_time / 1000, y = NULL), alpha = 0.15) +
  scale_y_continuous(limits = c(0.5, 1)) +
  labs(
    x = "Response time (seconds)",
    y = "Estimated accuracy",
    title = "Continuous RAC: accuracy as a smooth function of response time",
    subtitle = "Simulated data, 600 target-present and 600 target-absent lineups. Band = 95% CI; dashed line = 90% accuracy"
  ) +
  theme_minimal(base_size = 12)
```

This uses every observation, requires no arbitrary boundaries, and comes with a
confidence band that widens where the data thin out — visible in the
rug along the bottom.

## Estimating a time boundary

With a fitted curve, the question Dunning and Perretta asked can be put
directly: **up to what response time is estimated accuracy at least 90%?**

```{r time_boundary}
above <- grid$response_time[grid$accuracy >= 0.90]
boundary <- if (length(above) > 0) max(above) else NA

# The fitted curve could cross 90% more than once. Count the crossings, so a
# single "boundary" is not reported when accuracy dips and recovers.
n_crossings <- sum(diff(grid$accuracy >= 0.90) != 0)

cat("Estimated accuracy at  3 s:", round(approx(grid$response_time, grid$accuracy, 3000)$y, 3), "\n")
cat("Estimated accuracy at  8 s:", round(approx(grid$response_time, grid$accuracy, 8000)$y, 3), "\n")
cat("Estimated accuracy at 15 s:", round(approx(grid$response_time, grid$accuracy, 15000)$y, 3), "\n\n")
cat("Accuracy is at least 90% up to about", round(boundary / 1000, 1), "seconds\n")
cat("Crossings of the 90% line:", n_crossings, "\n")
```

For these simulated data the boundary falls near
`r round(boundary/1000, 1)` seconds. That number is a property of *this* dataset,
and the point of Weber et al. (2004) is precisely that it will not transfer: had
the simulation used a different criterion or a different d′, the boundary would
move. Refitting on new data rather than importing the number is the appropriate
use of the method.

Two cautions. The shaded band in the plot is a pointwise interval for accuracy at
each time; it is not an interval for the boundary, which would need the whole fit
bootstrapped. And a spline can cross the 90% line more than once, in which case
the largest qualifying time does not mean accuracy exceeded 90% at every shorter
time. The curve here crosses it `r if (n_crossings == 1) "once" else paste(n_crossings, "times")`;
check this before reporting a single boundary.

A note on what is being estimated. Because accuracy among suspect
identifications is a positive predictive value, it depends on the proportion of
target-present lineups in the design — 50% here, which is not a field base rate.
The curve answers "given a suspect identification at this speed, in a study of
this composition, how often is it correct?" That is the same quantity the binned
RAC estimates; the continuous version simply makes it estimable without bins.

# Comparing Conditions

RAC can compare experimental conditions. The comparison is only meaningful if
the conditions genuinely differ, so we simulate two that do: Condition 1 has
stronger memory evidence than Condition 2.

```{r compare_conditions}
make_condition <- function(d_prime, seed) {
  d <- as.data.frame(simulate_lineup_data(
    n_tp = 400, n_ta = 400, d_prime = d_prime, c_criterion = 0.9,
    lineup_size = 6, conf_levels = 9,
    foil_bias = c(0.4, 0.2, 0.0, -0.5, -1.1), seed = seed
  ))
  d$response_time <- round(pmax(
    800, 14000 - 900 * d$confidence + rnorm(nrow(d), 0, 3500)
  ))
  d$confidence <- round(seq(20, 100, length.out = 9))[d$confidence]
  d
}

data_condition1 <- make_condition(d_prime = 2.0, seed = 201)  # good viewing
data_condition2 <- make_condition(d_prime = 1.0, seed = 202)  # poor viewing

condition1_rac <- make_rac(data_condition1, time_bins = time_bins,
                           show_plot = FALSE)
condition2_rac <- make_rac(data_condition2, time_bins = time_bins,
                           show_plot = FALSE)

comparison <- data.frame(
  `Time bin` = condition1_rac$rac_data$response_time,
  `Condition 1 (d' = 2.0)` = round(condition1_rac$rac_data$accuracy, 3),
  `Condition 2 (d' = 1.0)` = round(condition2_rac$rac_data$accuracy, 3),
  check.names = FALSE
)

knitr::kable(
  comparison, row.names = FALSE,
  caption = "RAC accuracy by condition, simulated data (400 target-present and 400 target-absent lineups per condition)."
)
```

Condition 1 is more accurate at every response time. Note that this is a
descriptive comparison: testing whether two RAC curves differ requires a
bootstrap over witnesses, which the package does not yet provide for RAC. The
continuous model above offers a route to it, since a condition term and its
interaction with the time spline can simply be added to the `glm()` call.

# Custom Analysis

The binned data is available for plotting of your own:

```{r custom_analysis}
rac_data <- rac_result$rac_data

ggplot(rac_data, aes(x = mean_time / 1000, y = accuracy)) +
  geom_point(aes(size = n_total), colour = "darkblue", alpha = 0.6) +
  geom_line(colour = "darkblue", alpha = 0.5) +
  theme_bw(base_size = 12) +
  labs(
    x = "Mean response time (seconds)",
    y = "Accuracy",
    title = "Binned RAC, simulated data",
    size = "N suspect IDs"
  ) +
  scale_y_continuous(limits = c(0, 1)) +
  geom_hline(yintercept = 0.5, linetype = "dashed")
```

A smoother is deliberately *not* fitted through the four binned points here.
Fitting a curve to summaries that have already discarded the within-bin
variation compounds the loss rather than repairing it, and the resulting
confidence band understates the uncertainty because it ignores the sample sizes
behind each point. If a smooth curve is what you want, fit it to the raw
observations, as in the previous section.

# Technical Details

## How RAC is computed

For each response time bin:

$$\text{Accuracy} = \frac{\text{Correct Suspect IDs}}{\text{Correct Suspect IDs} + \text{Incorrect Suspect IDs}}$$

Where:

- **Correct suspect IDs**: target-present lineups in which the suspect was
  identified
- **Incorrect suspect IDs**: target-absent lineups in which the designated
  innocent suspect was identified. Where no innocent suspect is designated, foil
  identifications divided by lineup size are used as an estimate instead

## Standard errors

Standard errors assume a binomial distribution:

$$SE = \sqrt{\frac{p(1-p)}{n}}$$

with $p$ the accuracy and $n$ the number of suspect identifications in the bin.
This treats each bin independently and takes no account of the curve being
smooth, which is a further reason the continuous estimate is preferable when a
shape is what interests you.

# Using RAC with Real Data

Your data needs three columns, and a response time column:

```{r real_data_structure}
my_data <- data.frame(
  target_present = c(TRUE, FALSE, TRUE, FALSE, TRUE, TRUE),
  identification = c("suspect", "filler", "suspect", "suspect",
                     "reject", "suspect"),
  response_time  = c(8234, 12456, 6789, 15020, 9310, 4870)
)

str(my_data)
```

With a dataset of adequate size you would pass it to `make_rac()` exactly as the
simulated data was passed above. Six rows is far too few to bin, so there is no
point running it here.

Points to watch:

1. **Units.** Response times must all be in the same units. The package assumes
   milliseconds.
2. **Bins.** Inspect the distribution first, as above. Four to six bins is
   common, but the number is far less important than whether each is populated.
3. **Extreme values.** Very long times often mean an interruption rather than
   deliberation. Consider capping rather than deleting, and report what you did.
4. **Missing times.** Trials without a recorded time are dropped, so check how
   many.
5. **Sequential lineups.** Response time means something different when members
   are viewed one at a time; total time confounds decision time with viewing
   time.

# Exporting Results

```{r export}
# As CSV. Written to a temporary directory here; use your own path
outfile <- file.path(tempdir(), "rac_analysis.csv")
write.csv(rac_result$rac_data, outfile, row.names = FALSE)
```

```{r export_table}
knitr::kable(rac_result$rac_data, digits = 3,
             caption = "RAC analysis results, simulated data.")
```

# Summary

RAC analysis puts response time on the same footing as the CAC plot puts
confidence. In summary:

1. The idea is old (Sporer, 1993); the plot is new.
2. Compute it on choosers. Pooling choosers with rejections mixes two different
   speed-accuracy relationships and cancels most of the signal.
3. There is no universal time boundary. Dunning and Perretta's 10-to-12-second
   rule did not replicate, and the best-separating time ranges from about 5 to
   29 seconds across studies (Weber et al., 2004). Estimate it from your data.
4. Binning is conventional, not necessary. A logistic regression with a spline
   uses every observation, needs no boundaries, and gives an honest confidence
   band.
5. Response time is a weaker indicator of accuracy than confidence, and should
   be reported alongside it rather than instead of it.

# References

::: {.hanging-indent}

Dunning, D., & Perretta, S. (2002). Automaticity and eyewitness accuracy: A 10- to 12-second rule for distinguishing accurate from inaccurate positive identifications. *Journal of Applied Psychology, 87*(5), 951-962.

Mickes, L. (2015). Receiver operating characteristic analysis and confidence-accuracy characteristic analysis in investigations of system variables and estimator variables that affect eyewitness memory. *Journal of Applied Research in Memory and Cognition, 4*(2), 93-102.

Mickes, L., Seale-Carlisle, T. M., Chen, X., & Boogert, S. (2024). pyWitness 1.0: A python eyewitness identification analysis toolkit. *Behavior Research Methods, 56*, 1533-1550.

Seale-Carlisle, T. M., Colloff, M. F., Flowe, H. D., Wells, W., Wixted, J. T., & Mickes, L. (2019). Confidence and response time as indicators of eyewitness identification accuracy in the lab and in the real world. *Journal of Applied Research in Memory and Cognition, 8*(4), 420-428.

Sporer, S. L. (1993). Eyewitness identification accuracy, confidence, and decision times in simultaneous and sequential lineups. *Journal of Applied Psychology, 78*(1), 22-33.

Weber, N., Brewer, N., Wells, G. L., Semmler, C., & Keast, A. (2004). Eyewitness identification accuracy and response latency: The unruly 10-12 second rule. *Journal of Experimental Psychology: Applied, 10*(3), 139-147.

Wixted, J. T., & Wells, G. L. (2017). The relationship between eyewitness confidence and identification accuracy: A new synthesis. *Psychological Science in the Public Interest, 18*(1), 10-65.

:::

---

*This vignette was created for r4lineups version `r packageVersion("r4lineups")`*
