Replication of Jin, Hayward, & Cheung (2024) — Complete Composite Task

Author

Seojin Lee

The original design for the complete composite task includes four trial-level factors:

One composite trial has four independent factors each with two levels — 2 (alignment) x 2 (congruency) x 2 (cue) x 2 (same/different) = 16 unique trial conditions. The task contained 384 total composite trials, created by generating 16 unique condition combinations (2 Congruency × 2 Alignment × 2 Cue × 2 Same/Different), each repeated 24 times, resulting in 24 trials per condition.

Setup

This part loads required packages.

library(tidyverse)
library(lme4)
library(ggplot2)

Import Pilot Data

Here I load two pilot participants’ CSV files collected via Prolific.

d1 <- read_csv("data/compositeface_20251129_161517.csv")
d2 <- read_csv("data/compositeface_20251129_162056.csv")

raw <- bind_rows(d1, d2)

dim(raw)
[1] 834  36
head(raw)
# A tibble: 6 × 36
  trial_frame      item_width_mm item_height_mm item_width_px px2mm view_dist_mm
  <chr>                    <dbl>          <dbl>         <dbl> <dbl>        <dbl>
1 virtual_chinrest          85.6           54.0           422  4.93         720.
2 test_face                 NA             NA              NA NA             NA 
3 test_face                 NA             NA              NA NA             NA 
4 test_face                 NA             NA              NA NA             NA 
5 test_face                 NA             NA              NA NA             NA 
6 test_face                 NA             NA              NA NA             NA 
# ℹ 30 more variables: rt <dbl>, item_width_deg <dbl>, px2deg <dbl>,
#   win_width_deg <dbl>, win_height_deg <dbl>, trial_type <chr>,
#   trial_index <dbl>, plugin_version <chr>, time_elapsed <dbl>, Subject <chr>,
#   Exp_code <chr>, Exp_name <chr>, CFVersion <chr>, isPavlovia <lgl>,
#   Browser <chr>, Prolific_id <chr>, Trial_num <dbl>, Cue <chr>,
#   Congruency <chr>, Alignment <chr>, SameDifferent <chr>, StimGroup <chr>,
#   StudyFace <chr>, TestFace <chr>, Correct_response <dbl>, MaskFace <chr>, …

Filtering to Experimental Trials

The jsPsych data file contains instruction pages, fixation displays, masks, and other non-response rows. Here, I restrict the dataset to only composite decision trials, defined as “trial_type ==”image-keyboard-response”“. All condition variables are present (Congruent, Alignment, Cue, SameDifferent).

df <- raw %>%
filter(
trial_type == "image-keyboard-response",
!is.na(SameDifferent),
!is.na(Congruency),
!is.na(Alignment),
!is.na(Cue)
)

dim(df)
[1] 832  36
head(df)
# A tibble: 6 × 36
  trial_frame item_width_mm item_height_mm item_width_px px2mm view_dist_mm
  <chr>               <dbl>          <dbl>         <dbl> <dbl>        <dbl>
1 test_face              NA             NA            NA    NA           NA
2 test_face              NA             NA            NA    NA           NA
3 test_face              NA             NA            NA    NA           NA
4 test_face              NA             NA            NA    NA           NA
5 test_face              NA             NA            NA    NA           NA
6 test_face              NA             NA            NA    NA           NA
# ℹ 30 more variables: rt <dbl>, item_width_deg <dbl>, px2deg <dbl>,
#   win_width_deg <dbl>, win_height_deg <dbl>, trial_type <chr>,
#   trial_index <dbl>, plugin_version <chr>, time_elapsed <dbl>, Subject <chr>,
#   Exp_code <chr>, Exp_name <chr>, CFVersion <chr>, isPavlovia <lgl>,
#   Browser <chr>, Prolific_id <chr>, Trial_num <dbl>, Cue <chr>,
#   Congruency <chr>, Alignment <chr>, SameDifferent <chr>, StimGroup <chr>,
#   StudyFace <chr>, TestFace <chr>, Correct_response <dbl>, MaskFace <chr>, …

Cleaning & Set Up Variables

This part converts categorical variables to factors and ensures RT and Correct are numeric.

df <- df %>%
mutate(
Subject = factor(Subject),
Congruency = factor(Congruency, levels = c("congruent", "incongruent")),
Alignment = factor(Alignment, levels = c("aligned", "misaligned")),
Cue = factor(Cue, levels = c("top", "bot")),
SameDifferent = factor(SameDifferent, levels = c("same", "different")),
Correct = as.numeric(Correct),
RT = as.numeric(RT)
)

Reaction Time Exclusions

Following the preregistered cleaning plan, RTs < 200 ms or > 3000 ms are removed.

df <- df %>% filter(RT > 200, RT < 3000)
nrow(df)
[1] 810

Accuracy GLMM (Confirmatory Model)

This is the primary preregistered confirmatory model for accuracy. A logistic GLMM predicts correctness from Congruency, Alignment, and their interaction with a random intercept for subject. This tests the classic composite congruency x alignment effect.

acc_model <- glmer(
Correct ~ Congruency * Alignment + (1 | Subject),
data = df,
family = binomial,
control = glmerControl(optimizer = "bobyqa")
)

summary(acc_model)
Generalized linear mixed model fit by maximum likelihood (Laplace
  Approximation) [glmerMod]
 Family: binomial  ( logit )
Formula: Correct ~ Congruency * Alignment + (1 | Subject)
   Data: df
Control: glmerControl(optimizer = "bobyqa")

      AIC       BIC    logLik -2*log(L)  df.resid 
   1029.2    1052.7    -509.6    1019.2       805 

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-1.7377 -1.0557  0.5755  0.8098  0.9472 

Random effects:
 Groups  Name        Variance  Std.Dev. 
 Subject (Intercept) 2.904e-16 1.704e-08
Number of obs: 810, groups:  Subject, 2

Fixed effects:
                                          Estimate Std. Error z value Pr(>|z|)
(Intercept)                                 1.1051     0.1616   6.841 7.89e-12
Congruencyincongruent                      -0.9966     0.2142  -4.654 3.26e-06
Alignmentmisaligned                        -0.1607     0.2256  -0.712    0.476
Congruencyincongruent:Alignmentmisaligned   0.4742     0.3023   1.569    0.117
                                             
(Intercept)                               ***
Congruencyincongruent                     ***
Alignmentmisaligned                          
Congruencyincongruent:Alignmentmisaligned    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) Cngrnc Algnmn
Cngrncyncng -0.754              
Algnmntmslg -0.716  0.540       
Cngrncync:A  0.534 -0.709 -0.746
optimizer (bobyqa) convergence code: 0 (OK)
boundary (singular) fit: see help('isSingular')

Z-scoring RTs within Subject

RTs vary across subjects, so I recompute RT z-scores within each subject. This follows the analysis approach used in the original paper.

df <- df %>%
  mutate(
    RT = as.numeric(RT),
    Subject = factor(Subject)
  ) %>%
  group_by(Subject) %>%
  mutate(RT_z = scale(RT)[,1]) %>%
  ungroup()

RT LMER (Secondary Model)

On correct trials only, I examine RTs using a linear mixed-effects model.

  • DV = RT_z

  • Fixed effects = Congruency, Alignment, and their interaction

  • Random intercept for subject

rt_model <- lmer(
  RT_z ~ Congruency * Alignment + (1 | Subject),
  data = df %>% filter(Correct == 1)
)

summary(rt_model)
Linear mixed model fit by REML ['lmerMod']
Formula: RT_z ~ Congruency * Alignment + (1 | Subject)
   Data: df %>% filter(Correct == 1)

REML criterion at convergence: 1421

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-1.4109 -0.6830 -0.2596  0.3532  4.7813 

Random effects:
 Groups   Name        Variance Std.Dev.
 Subject  (Intercept) 0.0000   0.000   
 Residual             0.8538   0.924   
Number of obs: 527, groups:  Subject, 2

Fixed effects:
                                          Estimate Std. Error t value
(Intercept)                               -0.28312    0.07446  -3.802
Congruencyincongruent                      0.08328    0.11629   0.716
Alignmentmisaligned                        0.37282    0.10711   3.481
Congruencyincongruent:Alignmentmisaligned -0.13382    0.16264  -0.823

Correlation of Fixed Effects:
            (Intr) Cngrnc Algnmn
Cngrncyncng -0.640              
Algnmntmslg -0.695  0.445       
Cngrncync:A  0.458 -0.715 -0.659
optimizer (nloptwrap) convergence code: 0 (OK)
boundary (singular) fit: see help('isSingular')

Accuracy Plot

This shows group-level accuracy across the 2x2 Congruency x Alignment conditions. It provides a quick sanity check that the expected pattern (aligned-incongruent condition being the hardest) appears.

acc_summary <- df %>%
group_by(Congruency, Alignment) %>%
summarise(acc = mean(Correct))

ggplot(acc_summary, aes(Congruency, acc, fill = Alignment)) +
geom_col(position = "dodge") +
labs(title = "Pilot Accuracy by Condition",
y = "Accuracy") +
theme_minimal()

RT Plot

Similar to above, but for mean reaction times among correct responses.

rt_summary <- df %>%
filter(Correct == 1) %>%
group_by(Congruency, Alignment) %>%
summarise(rt = mean(RT))

ggplot(rt_summary, aes(Congruency, rt, fill = Alignment)) +
geom_col(position = "dodge") +
labs(title = "Pilot RT by Condition",
y = "Reaction Time (ms)") +
theme_minimal()

d’ Plot (Sensitivity)

To mirror the original paper’s signal detection analysis, this section computes d’ for each Subject x Congruency x Alignment condition.

  • Hits = correct “same” responses

  • FA = incorrect “same” responses

  • A loglinear correction is used to avoid infinite values.

compute_dprime <- function(hits, fas, n_hit, n_fa) {
  # loglinear correction
  hit_rate <- (hits + 0.5) / (n_hit + 1)
  fa_rate  <- (fas + 0.5) / (n_fa + 1)
  dprime <- qnorm(hit_rate) - qnorm(fa_rate)
  return(dprime)
}

dp <- df %>%
  group_by(Subject, Congruency, Alignment) %>%
  summarise(
    hits = sum(Correct == 1 & SameDifferent == "same"),
    fas  = sum(Correct == 0 & SameDifferent == "different"),
    n_hit = sum(SameDifferent == "same"),
    n_fa  = sum(SameDifferent == "different"),
    dprime = compute_dprime(hits, fas, n_hit, n_fa),
    .groups = "drop"
  )
dp_summary <- dp %>%
  group_by(Congruency, Alignment) %>%
  summarise(
    mean_dp = mean(dprime),
    se_dp = sd(dprime) / sqrt(n())
  )

ggplot(dp_summary, aes(Congruency, mean_dp, fill = Alignment)) +
  geom_col(position = "dodge") +
  geom_errorbar(aes(ymin = mean_dp - se_dp,
                    ymax = mean_dp + se_dp),
                width = 0.15,
                position = position_dodge(0.9)) +
  labs(title = "d′ by Congruency × Alignment",
       y = "d′ (Sensitivity)") +
  theme_minimal()