Dyadic Emotional Intelligence Composition and Cooperation in a Repeated Prisoner’s Dilemma

Data and Replication File

Author

MP

Published

October 26, 2026

1 Data and software

This is the executable replication file for the manuscript analyses. It was rendered with R version 4.5.1 (2025-06-13); all code is shown in collapsible blocks.

1.1 Installation and rendering

Install R (version 4.2 or later). Keep analisi_paper.qmd, DB.csv, and DB_time.csv together in one folder. In an R console, install the required packages once:

install.packages(
    c("tidyverse", "knitr", "rmarkdown", "broom", "lme4", "lmerTest", "car", "xfun"),
    repos = "https://cloud.r-project.org"
)
Show the R code
# =====================================================
# Libraries, options,
# =====================================================

library(tidyverse)
library(knitr)
library(broom)
library(lme4)
library(lmerTest)

options(
    scipen = 999,
    dplyr.summarise.inform = FALSE
)

# Create the figure directory expected by manuscript.tex.
figure_dir <- "fig"
dir.create(figure_dir, recursive = TRUE, showWarnings = FALSE)

# =====================================================
# Shared colors and themes
# =====================================================

theme_set(
    theme_minimal(base_size = 12) +
        theme(
            plot.title = element_text(face = "bold"),
            legend.title = element_blank()
        )
)

hist_fill <- "#b6cfa3"
hist_border <- "#7a9e7e"
density_col <- "#e76f51"
mean_line_col <- "#4f4f4f"
mean_text_col <- "#a2b489"
line_col_dark <- "#2f5d3a" # deep green
line_col_mid <- "#c75a3c" # brick orange
line_col_light <- "#7aa1c2" # slate blue (reference lines)
cat_col1 <- line_col_dark
cat_col2 <- line_col_mid
cat_col3 <- "#2b7bbb" # vivid blue for categories

pal_match <- c(HH = cat_col3, LH = cat_col2, LL = cat_col1)
pal_type <- c(Low = cat_col1, High = cat_col3)
ref_baseline_col <- "#6b6b6b"
ref_mean_col <- "#d7c24a"

hist_theme <- list(
    theme_minimal(base_size = 12),
    theme(
        plot.background = element_rect(fill = "white", color = NA),
        panel.background = element_rect(fill = "white", color = NA),
        panel.grid.major = element_line(color = "grey80", linewidth = 0.4),
        panel.grid.minor = element_line(color = "grey90", linewidth = 0.2),
        axis.text = element_text(color = "#1b1b1b"),
        axis.title = element_text(color = "#1b1b1b"),
        plot.title = element_text(color = "#1b1b1b", face = "bold", hjust = 0.5),
        axis.ticks = element_line(color = "grey60"),
        axis.line = element_blank(),
        legend.position = "none"
    )
)

line_theme <- list(
    theme_minimal(base_size = 12),
    theme(
        plot.background = element_rect(fill = "white", color = NA),
        panel.background = element_rect(fill = "white", color = NA),
        panel.grid.major = element_line(color = "grey80", linewidth = 0.4),
        panel.grid.minor = element_line(color = "grey90", linewidth = 0.2),
        axis.text = element_text(color = "#1b1b1b"),
        axis.title = element_text(color = "#1b1b1b"),
        plot.title = element_text(color = "#1b1b1b", face = "bold", hjust = 0.5),
        legend.text = element_text(color = "#1b1b1b"),
        legend.title = element_blank(),
        axis.ticks = element_line(color = "grey60"),
        axis.line = element_blank()
    )
)

1.2 Package versions

Show the R code
replication_packages <- c(
    "tidyverse", "dplyr", "tidyr", "readr", "tibble", "purrr", "ggplot2",
    "scales", "knitr", "rmarkdown", "broom", "lme4", "lmerTest", "Matrix",
    "car", "xfun"
)

data.frame(
    Package = replication_packages,
    Version = vapply(
        replication_packages,
        function(package) as.character(utils::packageVersion(package)),
        character(1)
    ),
    row.names = NULL
) |>
    knitr::kable(caption = "R package versions used for this render")
R package versions used for this render
Package Version
tidyverse 2.0.0
dplyr 1.2.0
tidyr 1.3.1
readr 2.1.5
tibble 3.3.1
purrr 1.1.0
ggplot2 4.0.0
scales 1.4.0
knitr 1.50
rmarkdown 2.29
broom 1.0.12
lme4 1.1.37
lmerTest 3.2.0
Matrix 1.7.3
car 3.1.5
xfun 0.55
Show the R code
# =====================================================
# Helper functions
# =====================================================

round_table <- function(df, digits = 3) {
    df |>
        mutate(across(where(is.numeric), ~ round(.x, digits)))
}

extract_model_table <- function(model, term_map = NULL, digits = 3) {
    out <- tibble::as_tibble(coef(summary(model)), rownames = "term")

    stat_col <- intersect(c("z value", "t value"), names(out))[1]
    p_col <- grep("^Pr\\(", names(out), value = TRUE)[1]

    if (!"df" %in% names(out)) {
        out$df <- NA_real_
    }

    out <- out |>
        transmute(
            term,
            estimate = Estimate,
            std_error = `Std. Error`,
            statistic = .data[[stat_col]],
            df = df,
            p_value = .data[[p_col]]
        )

    if (!is.null(term_map)) {
        out <- out |>
            mutate(term = dplyr::recode(term, !!!term_map))
    }

    round_table(out, digits = digits)
}

prop_pair_test <- function(data, group1, group2, alternative = "greater") {
    x1 <- data |>
        filter(type_match == group1) |>
        slice(1)

    x2 <- data |>
        filter(type_match == group2) |>
        slice(1)

    test <- prop.test(
        x = c(x1$success, x2$success),
        n = c(x1$total, x2$total),
        alternative = alternative,
        correct = FALSE
    )

    broom::tidy(test) |>
        transmute(
            comparison = paste(group1, "vs", group2),
            alternative = alternative,
            rate_1 = estimate1,
            rate_2 = estimate2,
            statistic,
            p_value = p.value
        )
}

t_pair_test <- function(data, value, group1, group2, alternative = "greater") {
    x <- data |>
        filter(type_match == group1) |>
        pull({{ value }})

    y <- data |>
        filter(type_match == group2) |>
        pull({{ value }})

    test <- t.test(x, y, alternative = alternative)

    broom::tidy(test) |>
        transmute(
            comparison = paste(group1, "vs", group2),
            alternative = alternative,
            mean_1 = estimate1,
            mean_2 = estimate2,
            statistic,
            df = parameter,
            p_value = p.value
        )
}

fit_model <- function(formula, data, family = binomial(link = "logit"), nAGQ = 10, ...) {
    tryCatch(
        glmer(
            formula,
            data = data,
            family = family,
            nAGQ = nAGQ,
            control = glmerControl(optimizer = "bobyqa", optCtrl = list(maxfun = 2e5)),
            ...
        ),
        error = function(e) {
            message("Primary glmer fit failed; retrying with nloptwrap. ", conditionMessage(e))
            tryCatch(
                glmer(
                    formula,
                    data = data,
                    family = family,
                    nAGQ = nAGQ,
                    control = glmerControl(optimizer = "nloptwrap", calc.derivs = FALSE),
                    ...
                ),
                error = function(e2) {
                    message("nloptwrap also failed; retrying with bobyqa. ", conditionMessage(e2))
                    glmer(
                        formula,
                        data = data,
                        family = family,
                        nAGQ = nAGQ,
                        control = glmerControl(optimizer = "bobyqa", optCtrl = list(maxfun = 2e5)),
                        ...
                    )
                }
            )
        }
    )
}

fit_model_lmer <- function(formula, data, ...) {
    lmer(formula, data = data, REML = FALSE, ...)
}

mixed_model_information <- function(model, include_residual = FALSE) {
    variance_components <- as.data.frame(VarCorr(model))

    information <- tibble(
        Statistic = c(
            "Observations",
            "Dyads",
            "Random-intercept variance",
            "Log likelihood",
            "AIC",
            "BIC"
        ),
        Value = c(
            nobs(model),
            n_distinct(model.frame(model)$pair_id),
            variance_components$vcov[
                variance_components$grp == "pair_id" &
                    is.na(variance_components$var2)
            ][1],
            as.numeric(logLik(model)),
            AIC(model),
            BIC(model)
        )
    )

    if (include_residual) {
        information <- bind_rows(
            information |> slice_head(n = 3),
            tibble(
                Statistic = "Residual variance",
                Value = variance_components$vcov[
                    variance_components$grp == "Residual"
                ][1]
            ),
            information |> slice(-(1:3))
        )
    }

    round_table(information)
}

lm_model_information <- function(model, dyads) {
    model_summary <- summary(model)

    tibble(
        Statistic = c(
            "Observations",
            "Dyads",
            "R-squared",
            "Adjusted R-squared",
            "Residual standard error",
            "Log likelihood",
            "AIC",
            "BIC"
        ),
        Value = c(
            nobs(model),
            dyads,
            model_summary$r.squared,
            model_summary$adj.r.squared,
            model_summary$sigma,
            as.numeric(logLik(model)),
            AIC(model),
            BIC(model)
        )
    ) |>
        round_table()
}

reg_term_map <- c(
    "type_matchLH" = "LH vs HH",
    "type_matchLL" = "LL vs HH",
    "round" = "Round number",
    "late_round" = "Late period",
    "type_matchLH:round" = "LH x round number",
    "type_matchLL:round" = "LL x round number",
    "type_matchLH:late_round" = "LH x late period",
    "type_matchLL:late_round" = "LL x late period",
    "msceit_score" = "MSCEIT score",
    "matrices_score" = "MATRICES score",
    "panas_positive" = "PANAS positive",
    "panas_negative" = "PANAS negative",
    "age" = "Age",
    "genderMaschile" = "Male",
    "genderMale" = "Male",
    "high_school_mark" = "High-school mark",
    "own_type_belief" = "Belief own type is high",
    "partner_type_belief" = "Belief partner type is high"
)

1.3 Data files and processing

The replication package contains the two input files used by this document. DB.csv is the analysis dataset of participant-round observations, and DB_time.csv contains the Day-2 page-completion records used for duration statistics.

The following links are embedded in the HTML version of this document.

1.3.1 Main data file

1.3.2 Page Times file

Show the R code
raw <- readr::read_csv("DB.csv", show_col_types = FALSE)

n_exact_duplicates <- nrow(raw) - nrow(dplyr::distinct(raw))

invalid_high_school_marks <- raw |>
    distinct(participant.code, player.high_school_final_mark) |>
    filter(
        !is.na(player.high_school_final_mark),
        !between(player.high_school_final_mark, 60, 100)
    ) |>
    transmute(
        participant_id = participant.code,
        reported_high_school_mark = player.high_school_final_mark
    )

d <- raw |>
    distinct() |>
    mutate(.keep="none",
        participant_id = participant.code,
        session_id = session.code,
        pair_id = group.id,
        round = subsession.round_number,
        player_type = factor(
            player.EI, 
            levels = c("low", "high"), 
            labels = c("Low", "High")
            ),
        type_match = factor(
            type_match,
            levels = c("high_high", "low_high", "low_low"),
            labels = c("HH", "LH", "LL")
        ),
        choice = player.choice,
        cooperate = as.integer(player.choice == "A"),
        belief_partner_coop = player.belief,
        msceit_score = player.score,
        eis_score = player.eis_score,
        matrices_score = player.points.Raven,
        panas_positive = player.interested + player.excited + player.strong + player.enthusiastic + player.proud + player.alert + player.inspired + player.determined + player.attentive + player.active,
        panas_negative = player.distressed + player.upset + player.guilty + player.scared + player.hostile + player.irritable + player.ashamed + player.nervous + player.jittery + player.afraid,
        age = player.age,
        gender = factor(player.gender),
        high_school_mark = player.high_school_final_mark,
        own_type_belief= player.belief_own_type,
        partner_type_belief = player.belief_other_type,
        partner_type_actual = factor(player.other_type_EI, levels = c("low", "high"))
    ) |>
    group_by(participant_id) |>
    arrange(round, .by_group = TRUE) |>
    fill(
        player_type,
        eis_score,
        msceit_score,
        matrices_score,
        panas_positive,
        panas_negative,
        age,
        gender,
        high_school_mark,
        own_type_belief,
        partner_type_belief,
        partner_type_actual,
        .direction = "downup"
    ) |>
    ungroup() |>
    mutate(
        # Italian final high-school marks range from 60 to 100. Values outside
        # this range cannot be converted reliably and are treated as missing.
        high_school_mark = if_else(
            between(high_school_mark, 60, 100),
            high_school_mark,
            NA_real_
        ),
        late_round = as.integer(round > 5),
        late_period = factor(
            if_else(round <= 5, "Early (1-5)", "Late (6-10)"),
            levels = c("Early (1-5)", "Late (6-10)")
        )
    )

The raw files are read without alteration. The analysis dataset d below removes exact duplicate rows, standardises names and factor levels, constructs the outcome and covariates used in the models, and carries participant-level fields across the ten repeated-game rows.

Seven participants reported high-school marks outside the admissible Italian 60–100 scale.

The raw responses remain unchanged in DB.csv; the derived analysis variable treats them as missing. Controlled models that include this covariate consequently use 221 participants and 113 dyads (2,210 participant-round observations in the repeated-game models). Treatment-only analyses retain all 228 participants and 114 dyads.

Six participants have missing Day-1 eis_score (SSEIT) records, leaving 222 participants with observed SSEIT scores (N = 222).

Show the R code
invalid_high_school_marks |>
    arrange(reported_high_school_mark) |>
    kable(
        col.names = c("Participant", "Reported high-school mark"),
        caption = paste(
            "High-school marks outside the admissible 60--100 range",
            "and treated as missing"
        )
    )
High-school marks outside the admissible 60–100 range and treated as missing
Participant Reported high-school mark
ngx8klzk 8
9keag7qf 15
l67cfgrm 54
qjxe8pjn 110
azszkurg 110
2dyirzxw 111
4uei7g27 603

2 Participant Flow

The study flow is reported as a descriptive record: 269 participants completed Day~1, and 228 completed Day~2 and are retained in the analysis dataset. The difference is 41 participants (15.2% of the Day-1 count).

3 Participant Payments

Show the R code
# FinalResults is displayed only in round 10. At that point player.payoff is
# the sum of the selected task payoffs; the application adds a fixed €10
# participation payment to this amount.
fixed_payment <- 10

final_payments <- raw |>
    distinct() |>
    filter(subsession.round_number == 10) |>
    distinct(participant.code, .keep_all = TRUE) |>
    transmute(
        `Variable earnings` = player.payoff,
        `Fixed payment` = fixed_payment,
        `Total payment` = player.payoff + fixed_payment
    )

payment_summary <- final_payments |>
    pivot_longer(everything(), names_to = "Payment component", values_to = "Euros") |>
    group_by(`Payment component`) |>
    summarise(
        Participants = n(),
        Mean = mean(Euros),
        SD = sd(Euros),
        Minimum = min(Euros),
        Maximum = max(Euros),
        .groups = "drop"
    )

payment_summary |>
    kable(
        digits = 2,
        caption = "Participant payments, based on the final-round FinalResults payoff"
    )
Participant payments, based on the final-round FinalResults payoff
Payment component Participants Mean SD Minimum Maximum
Fixed payment 228 10.00 0.00 10 10.0
Total payment 228 16.81 3.68 10 23.6
Variable earnings 228 6.81 3.68 0 13.6

The final-round payoff records yield mean total compensation of €16.81 (SD = €3.68; range: €10.00–€23.60).

4 Day 2 Duration

Show the R code
# PageTimes records provide the timestamp at which each page was completed.
# Duration is the elapsed time from the participant's first to final recorded
# page completion on Day 2.
day2_time <- readr::read_csv("DB_time.csv", show_col_types = FALSE)

day2_duration <- day2_time |>
    group_by(participant.code, participant.label, session.code) |>
    summarise(
        duration_minutes = (max(epoch_time_completed) - min(epoch_time_completed)) / 60,
        .groups = "drop"
    )

duration_summary <- day2_duration |>
    summarise(
        Participants = n(),
        `Analysis-sample coverage` = 100 * n() / n_distinct(d$participant_id),
        Sessions = n_distinct(session.code),
        `Mean (minutes)` = mean(duration_minutes),
        `SD (minutes)` = sd(duration_minutes),
        `Median (minutes)` = median(duration_minutes),
        `Minimum (minutes)` = min(duration_minutes),
        `Maximum (minutes)` = max(duration_minutes)
    )

duration_summary |>
    kable(
        digits = 1,
        caption = "Day 2 participant duration based on PageTimes records"
    )
Day 2 participant duration based on PageTimes records
Participants Analysis-sample coverage Sessions Mean (minutes) SD (minutes) Median (minutes) Minimum (minutes) Maximum (minutes)
220 96.5 11 41 6.7 39.5 28.3 55.1

PageTimes records are available for 220 of the 228 analysis participants (96.5%). Their mean Day 2 elapsed duration was 41.0 minutes (SD = 6.7; median = 39.5; range: 28.3–55.1 minutes).

5 Data Overview

Show the R code
bind_rows(
    tibble(
        Statistic = c(
            "Participants",
            "Pairs",
            "Sessions"
        ),
        Value = c(
            n_distinct(d$participant_id),
            n_distinct(d$pair_id),
            n_distinct(d$session_id)
        )
    ),
    d |>
        distinct(participant_id, type_match) |>
        count(type_match, name = "Value") |>
        transmute(
            Statistic = paste0("Participants (", type_match, ")"),
            Value
        )
) |>
    kable()
Statistic Value
Participants 228
Pairs 114
Sessions 12
Participants (HH) 76
Participants (LH) 76
Participants (LL) 76

5.1 Emotional Intelligence Measures

Day-1 SSEIT scores (eis_score) are missing for 6 of the 228 participants; SSEIT summaries and analyses therefore use the 222 participants with observed scores (N = 222).

Show the R code
d_eis <- d |>
    select(participant_id, eis_score) |>
    distinct() |>
    drop_na(eis_score)

mean_eis <- mean(d_eis$eis_score)
breaks_eis <- seq(
    floor(min(d_eis$eis_score)) - 1,
    ceiling(max(d_eis$eis_score)) + 1,
    by = 2
)
max_count_eis <- hist(d_eis$eis_score, breaks = breaks_eis, plot = FALSE)$counts |> max()

ggplot(d_eis, aes(x =   eis_score)) +
    geom_histogram(breaks = breaks_eis, fill = hist_fill, color = hist_border, alpha = 0.85) +
    geom_density(aes(y = after_stat(count)), color = density_col, linewidth = 1.3, adjust = 1) +
    geom_vline(xintercept = mean_eis, color = mean_line_col, linetype = "longdash", linewidth = 1.1) +
    annotate(
        "text",
        x = mean_eis + 2,
        y = max_count_eis * 0.9,
        label = paste0("Mean: ", sprintf("%.2f", mean_eis)),
        color = mean_text_col,
        hjust = 0,
        size = 4
    ) +
    scale_x_continuous(limits = c(70, 155), breaks = seq(70, 155, 10)) +
    scale_y_continuous(limits = c(0, 20), breaks = seq(0, 20, 5), expand = expansion(mult = c(0, 0.02))) +
    labs(title = "Distribution of Emotional Intelligence Scores", x = "EI Score", y = "Frequency") +
    hist_theme

Distribution of Emotional Intelligence Scores (EIS)
Show the R code
# display summary statistics for EIS scores
d_eis_summary <- d_eis |>
    summarise(
        mean = mean(eis_score),
        median = median(eis_score),
        sd = sd(eis_score),
        min = min(eis_score),
        max = max(eis_score)
    ) |>
    round_table(2)

d_eis_summary |>
    kable(caption = "Summary statistics for Emotional Intelligence Scores")
Summary statistics for Emotional Intelligence Scores
mean median sd min max
121.56 122 12.95 77 156

Distribution of Emotional Intelligence Scores (EIS)

Show the R code
# ggsave("EIS_distribution.png", width = 7, height = 5, dpi = 300)
Show the R code
d_msceit <- d |>
    select(participant_id, msceit_score) |>
    distinct() |>
    drop_na(msceit_score)

mean_msceit <- mean(d_msceit$msceit_score)
breaks_msceit <- seq(
    floor(min(d_msceit$msceit_score)) - 1,
    ceiling(max(d_msceit$msceit_score)) + 1,
    by = 2
)
max_count_msceit <- hist(d_msceit$msceit_score, breaks = breaks_msceit, plot = FALSE)$counts |> max()

ggplot(d_msceit, aes(x = msceit_score)) +
    geom_histogram(breaks = breaks_msceit, fill = hist_fill, color = hist_border, alpha = 0.85) +
    geom_density(aes(y = after_stat(count)), color = density_col, linewidth = 1.3, adjust = 1) +
    geom_vline(xintercept = mean_msceit, color = mean_line_col, linetype = "longdash", linewidth = 1.1) +
    annotate("text", x = mean_msceit + 2, y = max_count_msceit * 0.9, label = paste0("Mean: ", sprintf("%.2f", mean_msceit)), color = mean_text_col, hjust = 0, size = 4) +
    scale_x_continuous(limits = c(60, 140), breaks = seq(60, 140, 10)) +
    scale_y_continuous(limits = c(0, max_count_msceit * 1.1), breaks = scales::breaks_width(5), expand = expansion(mult = c(0, 0.02))) +
    labs(title = "Distribution of MSCEIT Scores", x = "MSCEIT Score", y = "Frequency") +
    hist_theme

Distribution of MSCEIT Scores
Show the R code
# ggsave("MSCEIT_distribution.png", width = 7, height = 5, dpi = 300)

# display summary statistics for MSCEIT scores
d_msceit_summary <- d_msceit |>
    summarise(
        mean = mean(msceit_score),
        median = median(msceit_score),
        sd = sd(msceit_score),
        min = min(msceit_score),
        max = max(msceit_score)
    ) |>
    round_table(2)

d_msceit_summary |>
    kable(caption = "Summary statistics for MSCEIT scores")
Summary statistics for MSCEIT scores
mean median sd min max
103.8 105 12.13 65 135

Distribution of MSCEIT Scores

Show the R code
# just for internal check, disply MSCEIT socres over time

# raw |>
#     group_by(participant.time_started_utc) |>
#     summarise(mean_msceit = mean(player.score, na.rm = TRUE)) |>
#     ggplot(aes(x = participant.time_started_utc, y = mean_msceit)) +
#     geom_point(size = 2.5, color = line_col_mid) +
#     labs(
#         title = "Mean MSCEIT scores by participant start time (for internal check)",
#         x = "Participant start time (UTC)",
#         y = "Mean MSCEIT score"
#     ) +
#     geom_smooth(method = "loess", color = line_col_dark, se = FALSE, linewidth = 1.1) +
#     line_theme

5.1.1 Correlation between EI and MSCEIT scores

Show the R code
d |>
    select(participant_id, eis_score, msceit_score) |>
    distinct() |>
    na.omit() -> temp_df

    cor.test(temp_df$eis_score, temp_df$msceit_score, use = "complete.obs") |>
    broom::tidy() |>
    select(estimate, p.value) |>
    knitr::kable()
estimate p.value
0.1985181 0.0029705

5.2 MSCEIT scores and types (sanity check)

Show the R code
# display mean MSCEIT scores by EI type 

d |>
    select(participant_id, msceit_score, type_match, player_type) |>
    distinct() |>
    group_by(player_type) |>
    summarise(mean_msceit = mean(msceit_score, na.rm = TRUE), sd = sd(msceit_score, na.rm = TRUE)) |>
    kable(caption = "Mean MSCEIT scores by EI type")
Mean MSCEIT scores by EI type
player_type mean_msceit sd
Low 94.73684 8.233380
High 112.85965 7.879487
Show the R code
wilcox.test(
    msceit_score ~ player_type,
    data = d |>
        select(participant_id, msceit_score, type_match, player_type) |>
        distinct(),
    exact = FALSE
) |>
    broom::tidy() |>
    kable(caption = "Wilcoxon test comparing MSCEIT scores between Low and High EI types")
Wilcoxon test comparing MSCEIT scores between Low and High EI types
statistic p.value method alternative
264 0 Wilcoxon rank sum test with continuity correction two.sided
Show the R code
# check MSCEIT scores by type match (HH, LH, LL)
d |>
    select(participant_id, msceit_score, type_match, player_type) |>
    distinct() |>
    group_by(type_match) |>
    summarise(mean_msceit = mean(msceit_score, na.rm = TRUE), sd = sd(msceit_score, na.rm = TRUE)) |>
    kable(caption = "Mean MSCEIT scores by EI matching type")
Mean MSCEIT scores by EI matching type
type_match mean_msceit sd
HH 112.92105 7.620609
LH 104.19737 11.631016
LL 94.27632 8.692677
Show the R code
type_match_msceit <- d |>
    select(participant_id, msceit_score, type_match, player_type) |>
    distinct()

kruskal.test(
    msceit_score ~ type_match,
    data = type_match_msceit
) |>
    broom::tidy() |>
    kable(caption = "Kruskal-Wallis test comparing MSCEIT scores across EI matching types (HH, LH, LL)")
Kruskal-Wallis test comparing MSCEIT scores across EI matching types (HH, LH, LL)
statistic p.value parameter method
109.7302 0 2 Kruskal-Wallis rank sum test
Show the R code
pairwise.wilcox.test(
    x = type_match_msceit$msceit_score,
    g = type_match_msceit$type_match,
    p.adjust.method = "holm",
    exact = FALSE
) |>
    broom::tidy() |>
    kable(caption = "Pairwise Wilcoxon tests (Holm-adjusted) comparing MSCEIT scores between EI matching types")
Pairwise Wilcoxon tests (Holm-adjusted) comparing MSCEIT scores between EI matching types
group1 group2 p.value
LH HH 0.0000001
LL HH 0.0000000
LL LH 0.0000001

5.3 Cognitive Ability Measure

Show the R code
d_raven <- d |>
    select(participant_id, matrices_score) |>
    distinct() |>
    drop_na(matrices_score)

mean_raven <- mean(d_raven$matrices_score)
breaks_raven <- seq(
    floor(min(d_raven$matrices_score)) - 1,
    ceiling(max(d_raven$matrices_score)) + 1,
    by = 1
)
max_count_raven <- hist(d_raven$matrices_score, breaks = breaks_raven, plot = FALSE)$counts |> max()

d_raven |>
    ggplot(aes(x = matrices_score)) +
    geom_histogram(breaks = breaks_raven, fill = hist_fill, color = hist_border, alpha = 0.85) +
    geom_density(aes(y = after_stat(count)), color = density_col, linewidth = 1.3, adjust = 1) +
    geom_vline(xintercept = mean_raven, color = mean_line_col, linetype = "longdash", linewidth = 1.1) +
    annotate("text", x = mean_raven + 0.6, y = max_count_raven * 0.9, label = paste0("Mean: ", sprintf("%.2f", mean_raven)), color = mean_text_col, hjust = 0, size = 4) +
    scale_x_continuous(limits = c(0, 30), breaks = seq(0, 30, 5)) +
    scale_y_continuous(limits = c(0, max_count_raven * 1.1), breaks = scales::breaks_width(5), expand = expansion(mult = c(0, 0.02))) +
    labs(title = "Distribution of MATRICES scores", x = "MATRICES score", y = "Frequency") +
    hist_theme

Distribution of MATRICES scores
Show the R code
# display summary statistics for MATRICES scores
d_raven_summary <- d_raven |>
    summarise(
        mean = mean(matrices_score),
        median = median(matrices_score),
        sd = sd(matrices_score),
        min = min(matrices_score),
        max = max(matrices_score)
    ) |>
    round_table(2)

d_raven_summary |>
    kable(caption = "Summary statistics for MATRICES scores")
Summary statistics for MATRICES scores
mean median sd min max
15.05 15 4.63 2 26

Distribution of MATRICES scores

5.3.1 Correlation between MATRICES and MSCEIT scores

Show the R code
d |>
    select(participant_id, matrices_score, msceit_score) |>
    distinct() |>
    na.omit() -> temp_df
cor.test(temp_df$matrices_score, temp_df$msceit_score, use = "complete.obs") |>
    broom::tidy() |>
    select(estimate, p.value) |>
    knitr::kable()
estimate p.value
0.0183978 0.78232

5.3.2 Correlation between MATRICES and EIS scores

Show the R code
d |>
    select(participant_id, matrices_score, eis_score) |>
    distinct() |>
    na.omit() -> temp_df
cor.test(temp_df$matrices_score, temp_df$eis_score, use = "complete.obs") |>
    broom::tidy() |>
    select(estimate, p.value) |>
    knitr::kable()
estimate p.value
-0.0859118 0.2022422

5.4 PANAS Scores

Show the R code
# Scoring:
# Positive Affect Score: Add the scores on items 1, 3, 5, 9, 10, 12, 14, 16, 17, and 19. Scores can
# range from 10 – 50, with higher scores representing higher levels of positive affect.
# Mean Scores: 33.3 (SD±7.2)
# Negative Affect Score: Add the scores on items 2, 4, 6, 7, 8, 11, 13, 15, 18, and 20. Scores can
# range from 10 – 50, with lower scores representing lower levels of negative affect.
# Mean Score: 17.4 (SD ± 6.2)

# Calculate PANAS scores

d_panas_pos <- d |>
    select(participant_id, panas_positive) |>
    distinct() |>
    drop_na(panas_positive)



mean_panas_pos <- mean(d_panas_pos$panas_positive)


breaks_panas_pos <- seq(
    floor(min(d_panas_pos$panas_positive)) - 1,
    ceiling(max(d_panas_pos$panas_positive)) + 1,
    by = 1
)

max_count_panas_pos <- hist(d_panas_pos$panas_positive, breaks = breaks_panas_pos, plot = FALSE)$counts |> max()


d_panas_pos |>
    ggplot(aes(x = panas_positive)) +
    geom_histogram(breaks = breaks_panas_pos, fill = hist_fill, color = hist_border, alpha = 0.85) +
    geom_density(aes(y = after_stat(count)), color = density_col, linewidth = 1.3, adjust = 1) +
    geom_vline(xintercept = mean_panas_pos, color = mean_line_col, linetype = "longdash", linewidth = 1.1) +
    annotate("text", x = mean_panas_pos + 1, y = max_count_panas_pos * 0.9, label = paste0("Mean: ", sprintf("%.2f", mean_panas_pos)), color = mean_text_col, hjust = 0, size = 4) +
    scale_x_continuous(limits = c(floor(min(d_panas_pos$panas_positive)) - 1, ceiling(max(d_panas_pos$panas_positive)) + 1), breaks = scales::breaks_width(5)) +
    scale_y_continuous(limits = c(0, max_count_panas_pos * 1.1), breaks = scales::breaks_width(5), expand = expansion(mult = c(0, 0.02))) +
    labs(title = "Distribution of Positive PANAS Scores", x = "Positive PANAS Score", y = "Frequency") +
    hist_theme

Distribution of PANAS Scores - Positive
  • Correlation between PANAS positive and MSCEIT scores
Show the R code
d |>
    select(participant_id, panas_positive, msceit_score) |>
    distinct() |>
    na.omit() -> temp_df

cor.test(temp_df$panas_positive, temp_df$msceit_score, use = "complete.obs") |>
    broom::tidy() |>
    select(estimate, p.value) |>
    knitr::kable()
estimate p.value
-0.13172 0.0469601
Show the R code
d_panas_neg <- d |>
    select(participant_id, panas_negative) |>
    distinct() |>
    drop_na(panas_negative)

mean_panas_neg <- mean(d_panas_neg$panas_negative)

breaks_panas_neg <- seq(
    floor(min(d_panas_neg$panas_negative)) - 1,
    ceiling(max(d_panas_neg$panas_negative)) + 1,
    by = 1
)

max_count_panas_neg <- hist(d_panas_neg$panas_negative, breaks = breaks_panas_neg, plot = FALSE)$counts |> max()

d_panas_neg |>
    ggplot(aes(x = panas_negative)) +
    geom_histogram(breaks = breaks_panas_neg, fill = hist_fill, color = hist_border, alpha = 0.85) +
    geom_density(aes(y = after_stat(count)), color = density_col, linewidth = 1.3, adjust = 1) +
    geom_vline(xintercept = mean_panas_neg, color = mean_line_col, linetype = "longdash", linewidth = 1.1) +
    annotate("text", x = mean_panas_neg + 1, y = max_count_panas_neg * 0.9, label = paste0("Mean: ", sprintf("%.2f", mean_panas_neg)), color = mean_text_col, hjust = 0, size = 4) +
    scale_x_continuous(limits = c(floor(min(d_panas_neg$panas_negative)) - 1, ceiling(max(d_panas_neg$panas_negative)) + 1), breaks = scales::breaks_width(5)) +
    scale_y_continuous(limits = c(0, max_count_panas_neg * 1.1), breaks = scales::breaks_width(5), expand = expansion(mult = c(0, 0.02))) +
    labs(title = "Distribution of Negative PANAS Scores", x = "Negative PANAS Score", y = "Frequency") +
    hist_theme

Distribution of PANAS Scores - Negative
  • Correlation between PANAS negative and MSCEIT scores
Show the R code
d |>
    select(participant_id, panas_negative, msceit_score) |>
    distinct() |>
    na.omit() -> temp_df

cor.test(temp_df$panas_negative, temp_df$msceit_score, use = "complete.obs") |>
    broom::tidy() |>
    select(estimate, p.value) |>
    knitr::kable()
estimate p.value
-0.2230744 0.0006921

6 Hypothesis tests and additional analyses

Show the R code
df_contrib_match <- d |>
    group_by(round, type_match) |>
    count(choice) |>
    mutate(perc = round(n / sum(n) * 100, 1)) |>
    filter(choice == "A")

mean_contrib_match <- mean(df_contrib_match$perc, na.rm = TRUE)

ggplot(df_contrib_match, aes(x = round, y = perc, color = type_match, pch = type_match)) +
    geom_line(linewidth = 1, alpha = .7) +
    geom_point(size = 5, alpha = 0.5) +
    geom_hline(yintercept = mean_contrib_match, color = ref_mean_col, linetype = "longdash", linewidth = 0.9, alpha = 0.8) +
    annotate(
        "text",
        x = -Inf,
        y = mean_contrib_match,
        label = paste0(sprintf("%.1f%%", mean_contrib_match)),
        color = ref_mean_col,
        hjust = 1,
        vjust = -0.4,
        size = 5.2
    ) +
    scale_color_manual(values = pal_match) +
    scale_x_continuous(breaks = seq(1, 10, 1)) +
    scale_y_continuous(labels = scales::percent_format(scale = 1), limits = c(27, 72)) +
    labs(title = "", x = "Round", y = "Cooperation Rate") +
    line_theme +
    theme(
        legend.position = "bottom",
        axis.text = element_text(size = 16),
        axis.title = element_text(size = 18),
        legend.text = element_text(size = 16),
        plot.margin = margin(5.5, 5.5, 5.5, 45)
    )

Cooperation by EI matching condition
Show the R code
ggsave(file.path(figure_dir, "Contributions_by_Matching.png"), width = 9, height = 6, dpi = 300)

6.1 Hypothesis 1a: Cooperation by Matching Type (HH > LH > LL)

6.1.1 Tests

Show the R code
counts_h1a <- d |>
    group_by(type_match, pair_id) |>
    summarise(
        success = sum(cooperate),
        total = n(),
        cooperation_rate = success / total
    )

h1a_overall_counts <- counts_h1a |>
    group_by(type_match) |>
    summarise(
        success = sum(success),
        total = sum(total),
        cooperation_rate = success / total
    )

h1a_overall_counts |>
    kable(caption = "Cooperation rates by EI matching type (HH, LH, LL)")
Cooperation rates by EI matching type (HH, LH, LL)
type_match success total cooperation_rate
HH 371 760 0.4881579
LH 365 760 0.4802632
LL 428 760 0.5631579
Show the R code
bind_rows(
    prop_pair_test(h1a_overall_counts, "HH", "LH", alternative = "greater"),
    prop_pair_test(h1a_overall_counts, "HH", "LL", alternative = "greater"),
    prop_pair_test(h1a_overall_counts, "LH", "LL", alternative = "greater")
) |>
    round_table() |>
    kable(caption = "Preregistered proportion tests for H1a")
Preregistered proportion tests for H1a
comparison alternative rate_1 rate_2 statistic p_value
HH vs LH greater 0.488 0.480 0.095 0.379
HH vs LL greater 0.488 0.563 8.573 0.998
LH vs LL greater 0.480 0.563 10.464 0.999
Show the R code
# Complementary checks use each pair's cooperation rate as the observation,
# avoiding the treatment of repeated decisions from the same pair as independent.

counts_h1a |>
    filter(type_match %in% c("HH", "LH")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LH"))) |>
    wilcox.test(cooperation_rate ~ type_match, data = _, alternative = "greater", exact = FALSE) |>
    broom::tidy() |>
    kable(caption = "Wilcoxon test comparing cooperation rates between HH and LH matching types")
Wilcoxon test comparing cooperation rates between HH and LH matching types
statistic p.value method alternative
705.5 0.5703436 Wilcoxon rank sum test with continuity correction greater
Show the R code
counts_h1a |>
    filter(type_match %in% c("HH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LL"))) |>
    wilcox.test(cooperation_rate ~ type_match, data = _, alternative = "greater", exact = FALSE) |>
    broom::tidy() |>
    kable(caption = "Wilcoxon test comparing cooperation rates between HH and LL matching types")
Wilcoxon test comparing cooperation rates between HH and LL matching types
statistic p.value method alternative
621.5 0.8538217 Wilcoxon rank sum test with continuity correction greater
Show the R code
counts_h1a |>
    filter(type_match %in% c("LH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("LH", "LL"))) |>
    wilcox.test(cooperation_rate ~ type_match, data = _, alternative = "greater", exact = FALSE) |>
    broom::tidy() |>
    kable(caption = "Wilcoxon test comparing cooperation rates between LH and LL matching types")
Wilcoxon test comparing cooperation rates between LH and LL matching types
statistic p.value method alternative
613.5 0.8719926 Wilcoxon rank sum test with continuity correction greater
Show the R code
# Repeat the directional pair-level comparisons using Welch's t-test.

counts_h1a |>
    filter(type_match %in% c("HH", "LH")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LH"))) |>
    t.test(cooperation_rate ~ type_match, data = _, alternative = "greater") |>
    broom::tidy() |>
    kable(caption = "t-test comparing cooperation rates between HH and LH matching types")
t-test comparing cooperation rates between HH and LH matching types
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
0.0078947 0.4881579 0.4802632 0.118697 0.452922 72.43424 -0.1029246 Inf Welch Two Sample t-test greater
Show the R code
counts_h1a |>
    filter(type_match %in% c("HH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LL"))) |>
    t.test(cooperation_rate ~ type_match, data = _, alternative = "greater") |>
    broom::tidy() |>
    kable(caption = "t-test comparing cooperation rates between HH and LL matching types")
t-test comparing cooperation rates between HH and LL matching types
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
-0.075 0.4881579 0.5631579 -1.090905 0.8605635 73.6019 -0.1895256 Inf Welch Two Sample t-test greater
Show the R code
counts_h1a |>
    filter(type_match %in% c("LH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("LH", "LL"))) |>
    t.test(cooperation_rate ~ type_match, data = _, alternative = "greater") |>
    broom::tidy() |>
    kable(caption = "t-test comparing cooperation rates between LH and LL matching types")
t-test comparing cooperation rates between LH and LL matching types
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
-0.0828947 0.4802632 0.5631579 -1.298374 0.9008936 73.59391 -0.1892493 Inf Welch Two Sample t-test greater

6.1.1.1 Two-sided 95% confidence intervals for dyad-level mean differences

The preregistered tests above are directional. For the conventional two-sided 95% confidence intervals reported in the manuscript, the same dyad-level Welch tests are repeated below with a two-sided alternative. Differences and interval bounds are expressed in percentage points.

Show the R code
h1a_welch_ci <- function(data, group1, group2) {
    x <- data |>
        filter(type_match == group1) |>
        pull(cooperation_rate)

    y <- data |>
        filter(type_match == group2) |>
        pull(cooperation_rate)

    test <- t.test(
        x,
        y,
        alternative = "two.sided",
        conf.level = 0.95
    )

    broom::tidy(test) |>
        transmute(
            comparison = paste(group1, "minus", group2),
            difference_pp = 100 * (estimate1 - estimate2),
            conf_low_pp = 100 * conf.low,
            conf_high_pp = 100 * conf.high,
            statistic,
            df = parameter,
            p_value = p.value
        )
}

bind_rows(
    h1a_welch_ci(counts_h1a, "HH", "LH"),
    h1a_welch_ci(counts_h1a, "HH", "LL"),
    h1a_welch_ci(counts_h1a, "LH", "LL")
) |>
    mutate(
        across(c(difference_pp, conf_low_pp, conf_high_pp), ~ round(.x, 1)),
        across(c(statistic, df, p_value), ~ round(.x, 3))
    ) |>
    kable(
        col.names = c(
            "Comparison",
            "Difference (pp)",
            "95% CI lower (pp)",
            "95% CI upper (pp)",
            "t",
            "df",
            "Two-sided p-value"
        ),
        caption = "Two-sided 95% confidence intervals for dyad-level cooperation-rate differences"
    )
Two-sided 95% confidence intervals for dyad-level cooperation-rate differences
Comparison Difference (pp) 95% CI lower (pp) 95% CI upper (pp) t df Two-sided p-value
HH minus LH 0.8 -12.5 14.0 0.119 72.434 0.906
HH minus LL -7.5 -21.2 6.2 -1.091 73.602 0.279
LH minus LL -8.3 -21.0 4.4 -1.298 73.594 0.198

Note. Each comparison reports the cooperation rates for the two matching conditions named in its caption.

6.1.2 Regression analysis for Hypothesis 1a

Outcome: binary cooperation, coded 1 for Choice A and 0 for Choice B. Both specifications include a dyad random intercept; the controlled model additionally includes the listed covariates.

Show the R code
h1a_treatment_model <- fit_model(
    cooperate ~ type_match + (1 | pair_id),
    data = d,
    family = binomial(link = "logit")
)

h1a_treatment_model_table <- extract_model_table(
    h1a_treatment_model,
    term_map = reg_term_map
)

h1a_treatment_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "z", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Treatment-only logit model for Hypothesis 1a"
    )
Treatment-only logit model for Hypothesis 1a
Term Estimate Std. Error z p-value
(Intercept) 0.032 0.259 0.125 0.900
LH vs HH -0.093 0.364 -0.256 0.798
LL vs HH 0.369 0.365 1.012 0.312
Show the R code
mixed_model_information(h1a_treatment_model) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the treatment-only H1a logit model"
    )
Model information for the treatment-only H1a logit model
Model statistic Value
Observations 2280.000
Dyads 114.000
Random-intercept variance 2.178
Log likelihood -1319.026
AIC 2646.051
BIC 2668.979
Show the R code
h1a_model <- fit_model(
    cooperate ~ type_match +
        msceit_score + matrices_score + panas_positive + panas_negative +
        age + gender + high_school_mark + own_type_belief + partner_type_belief +
        (1 | pair_id),
    data = d,
    family = binomial(link = "logit")
)

h1a_model_table <- extract_model_table(h1a_model, term_map = reg_term_map)

h1a_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "z", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Preregistered controlled logit model for Hypothesis 1a"
    )
Preregistered controlled logit model for Hypothesis 1a
Term Estimate Std. Error z p-value
(Intercept) 0.289 1.310 0.220 0.825
LH vs HH -0.162 0.395 -0.411 0.681
LL vs HH 0.397 0.415 0.955 0.340
MSCEIT score 0.005 0.007 0.695 0.487
MATRICES score -0.022 0.015 -1.412 0.158
PANAS positive -0.024 0.011 -2.195 0.028
PANAS negative 0.026 0.013 1.919 0.055
Age 0.015 0.018 0.827 0.408
Male -0.262 0.152 -1.724 0.085
High-school mark -0.003 0.007 -0.390 0.697
Belief own type is high -0.009 0.004 -2.177 0.029
Belief partner type is high 0.007 0.004 1.588 0.112
Show the R code
mixed_model_information(h1a_model) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the controlled H1a logit model"
    )
Model information for the controlled H1a logit model
Model statistic Value
Observations 2210.000
Dyads 113.000
Random-intercept variance 2.504
Log likelihood -1268.932
AIC 2563.864
BIC 2637.974
Show the R code
car::linearHypothesis(h1a_treatment_model, "type_matchLH = type_matchLL", test = "Chisq") |>
    as.data.frame() |>
    tibble::rownames_to_column("Model") |>
    kable(caption = "Treatment-only linear hypothesis test for LH = LL in the H1a cooperation model")
Treatment-only linear hypothesis test for LH = LL in the H1a cooperation model
Model Df Chisq Pr(>Chisq)
1 NA NA NA
2 1 1.623117 0.2026577

6.2 Hypothesis 1b: Cooperation Dynamics (EI and Cooperation Dynamics)

Show the R code
individual_period_coop <- d |>
    group_by(pair_id, type_match, late_period) |>
    summarise(
        cooperation_rate = mean(cooperate, na.rm = TRUE) * 100,
        .groups = "drop"
    )

pal_period <- c(
    "Early (1-5)" = cat_col1,
    "Late (6-10)" = cat_col2
)

ggplot(
    individual_period_coop,
    aes(x = late_period, y = cooperation_rate, fill = type_match)
) +
    geom_violin(
        width = 0.85,
        color = "grey20",
        alpha = 0.55,
        trim = FALSE
    ) +
    geom_point(
        aes(color = type_match),
        position = position_jitter(width = 0.08, height = 0, seed = 2026),
        size = 1.8,
        alpha = 0.7,
        show.legend = FALSE
    ) +
    stat_summary(
        fun = mean, geom = "point", shape = 21,
        fill = ref_mean_col, color = "black", size = 5
    ) +
    stat_summary(
        fun = median, geom = "point", shape = 95,
        color = "black", size = 17
    ) +
    facet_wrap(~type_match, nrow = 1) +
    scale_fill_manual(values = pal_match) +
    scale_color_manual(values = pal_match) +
    scale_x_discrete(labels = c("Early (1-5)" = "Early", "Late (6-10)" = "Late")) +
    scale_y_continuous(labels = scales::percent_format(scale = 1), limits = c(0, 100)) +
    labs(
        title = "",
        x = "Period",
        y = "Cooperation Rate"
    ) +
    line_theme +
    theme(
        legend.position = "none",
        axis.text = element_text(size = 14),
        axis.title = element_text(size = 16),
        strip.text = element_text(size = 15)
    )

Group-level cooperation rates in the early and late periods, by matching type
Show the R code
ggsave(file.path(figure_dir, "Contributions_Early_Late_by_Matching_Violin.png"), width = 10, height = 5, dpi = 300)

Following the preregistration, cooperation should decline over the finite horizon, but the decline should be weaker in HH than in LH dyads. HH and LH dyads are expected to have similar cooperation in early rounds ((C^-)); by late rounds ((C^+)), cooperation is expected to be lower in LH than in HH dyads. Formally, this verbal prediction implies

\[ C^-_{HH} \simeq C^-_{LH}, \qquad C^-_{LH}-C^+_{LH} > C^-_{HH}-C^+_{HH}. \]

6.2.1 Tests

Early and late periods comprise rounds 1–5 and 6–10, respectively. The tests use pooled choices and dyad-level cooperation rates.

Show the R code
counts_h1b <- d |>
    group_by(type_match, late_period, pair_id) |>
    summarise(
        success = sum(cooperate),
        total = n(),
        cooperation_rate = success / total,
        .groups = "drop"
    )

h1b_period_counts <- counts_h1b |>
    group_by(type_match, late_period) |>
    summarise(
        success = sum(success),
        total = sum(total),
        cooperation_rate = success / total,
        .groups = "drop"
    )

h1b_period_counts |>
    kable(caption = "Cooperation rates by EI matching type (HH, LH, LL) and period (Early vs Late)")
Cooperation rates by EI matching type (HH, LH, LL) and period (Early vs Late)
type_match late_period success total cooperation_rate
HH Early (1-5) 208 380 0.5473684
HH Late (6-10) 163 380 0.4289474
LH Early (1-5) 211 380 0.5552632
LH Late (6-10) 154 380 0.4052632
LL Early (1-5) 234 380 0.6157895
LL Late (6-10) 194 380 0.5105263
Show the R code
h1b_prop_test <- function(data, group1, period1, group2, period2, alternative = "two.sided") {
    x1 <- data |>
        filter(type_match == group1, late_period == period1) |>
        slice(1)

    x2 <- data |>
        filter(type_match == group2, late_period == period2) |>
        slice(1)

    broom::tidy(prop.test(
        x = c(x1$success, x2$success),
        n = c(x1$total, x2$total),
        alternative = alternative,
        correct = FALSE
    )) |>
        transmute(
            comparison = paste(group1, period1, "vs", group2, period2),
            alternative = alternative,
            rate_1 = estimate1,
            rate_2 = estimate2,
            statistic,
            p_value = p.value
        )
}

bind_rows(
    h1b_prop_test(h1b_period_counts, "HH", "Early (1-5)", "LH", "Early (1-5)", alternative = "two.sided"),
    h1b_prop_test(h1b_period_counts, "LH", "Early (1-5)", "LH", "Late (6-10)", alternative = "greater")
) |>
    round_table() |>
    kable(caption = "Preregistered proportion tests for H1b")
Preregistered proportion tests for H1b
comparison alternative rate_1 rate_2 statistic p_value
HH Early (1-5) vs LH Early (1-5) two.sided 0.547 0.555 0.048 0.827
LH Early (1-5) vs LH Late (6-10) greater 0.555 0.405 17.127 0.000
Show the R code
# One-sided paired tests of the predicted early-to-late decline within each
# matching condition. The same dyads are observed in both periods.

for (match in c("HH", "LH", "LL")) {
    cat("\n\n### Comparing Early vs Late Periods for", match, "Matching Type\n\n")

    data_subset <- counts_h1b |>
        filter(type_match == match) |>
        select(pair_id, late_period, cooperation_rate) |>
        tidyr::pivot_wider(names_from = late_period, values_from = cooperation_rate) |>
        drop_na(`Early (1-5)`, `Late (6-10)`)

    # Wilcoxon signed-rank test
    wilcox_result <- wilcox.test(
        data_subset$`Early (1-5)`,
        data_subset$`Late (6-10)`,
        alternative = "greater",
        paired = TRUE,
        exact = FALSE
    )
    print(broom::tidy(wilcox_result) |> kable(caption = paste("Wilcoxon test comparing Early vs Late periods for", match, "matching type")))

    # Paired t-test
    t_result <- t.test(
        data_subset$`Early (1-5)`,
        data_subset$`Late (6-10)`,
        alternative = "greater",
        paired = TRUE
    )
    print(broom::tidy(t_result) |> kable(caption = paste("t-test comparing Early vs Late periods for", match, "matching type")))
}

6.2.2 Comparing Early vs Late Periods for HH Matching Type

Wilcoxon test comparing Early vs Late periods for HH matching type
statistic p.value method alternative
342.5 0.0034415 Wilcoxon signed rank test with continuity correction greater
t-test comparing Early vs Late periods for HH matching type
estimate statistic p.value parameter conf.low conf.high method alternative
0.1184211 3.111106 0.0017907 37 0.0542036 Inf Paired t-test greater

6.2.3 Comparing Early vs Late Periods for LH Matching Type

Wilcoxon test comparing Early vs Late periods for LH matching type
statistic p.value method alternative
599 0.0000142 Wilcoxon signed rank test with continuity correction greater
t-test comparing Early vs Late periods for LH matching type
estimate statistic p.value parameter conf.low conf.high method alternative
0.15 5.696153 0.0000008 37 0.1055728 Inf Paired t-test greater

6.2.4 Comparing Early vs Late Periods for LL Matching Type

Wilcoxon test comparing Early vs Late periods for LL matching type
statistic p.value method alternative
410.5 0.0029493 Wilcoxon signed rank test with continuity correction greater
t-test comparing Early vs Late periods for LL matching type
estimate statistic p.value parameter conf.low conf.high method alternative
0.1052632 3.181685 0.0014811 37 0.0494472 Inf Paired t-test greater
Show the R code
diff_h1b <- counts_h1b |>
    select(pair_id, type_match, late_period, cooperation_rate) |>
    tidyr::pivot_wider(names_from = late_period, values_from = cooperation_rate) |>
    drop_na(`Early (1-5)`, `Late (6-10)`) |>
    mutate(decline = `Early (1-5)` - `Late (6-10)`)

lh_decline <- diff_h1b |>
    filter(type_match == "LH") |>
    pull(decline)

hh_decline <- diff_h1b |>
    filter(type_match == "HH") |>
    pull(decline)

# The directional H1b comparison: the early-to-late decline is larger in LH
# than in HH dyads.
bind_rows(
    broom::tidy(wilcox.test(
        lh_decline,
        hh_decline,
        alternative = "greater",
        exact = FALSE
    )),
    broom::tidy(t.test(
        lh_decline,
        hh_decline,
        alternative = "greater"
    ))
) |>
    select(method, estimate, statistic, p.value, alternative) |>
    round_table() |>
    kable(caption = "One-sided tests of whether the cooperation decline is larger in LH than in HH dyads")
One-sided tests of whether the cooperation decline is larger in LH than in HH dyads
method estimate statistic p.value alternative
Wilcoxon rank sum test with continuity correction NA 837.000 0.116 greater
Welch Two Sample t-test 0.032 0.682 0.249 greater

6.2.5 Regression analysis for Hypothesis 1b

Outcome: binary cooperation, coded 1 for Choice A and 0 for Choice B. The treatment-only and controlled mixed-effects logit models include matching condition, period, their interaction, and a dyad random intercept. HH in the early period is the reference category.

Show the R code
h1b_treatment_model <- fit_model(
    cooperate ~ type_match * late_round + (1 | pair_id),
    data = d,
    family = binomial(link = "logit")
)

h1b_treatment_model_table <- extract_model_table(
    h1b_treatment_model,
    term_map = reg_term_map
)

h1b_treatment_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "z", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Treatment-only late-period logit model for Hypothesis 1b"
    )
Treatment-only late-period logit model for Hypothesis 1b
Term Estimate Std. Error z p-value
(Intercept) 0.392 0.280 1.397 0.162
LH vs HH -0.042 0.393 -0.107 0.915
LL vs HH 0.325 0.395 0.823 0.411
Late period -0.717 0.181 -3.950 0.000
LH x late period -0.110 0.251 -0.437 0.662
LL x late period 0.103 0.253 0.406 0.684
Show the R code
mixed_model_information(h1b_treatment_model) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the treatment-only H1b logit model"
    )
Model information for the treatment-only H1b logit model
Model statistic Value
Observations 2280.000
Dyads 114.000
Random-intercept variance 2.300
Log likelihood -1293.307
AIC 2600.614
BIC 2640.737
Show the R code
h1b_model <- fit_model(
    cooperate ~ type_match * late_round +
        msceit_score + matrices_score + panas_positive + panas_negative +
        age + gender + high_school_mark + own_type_belief + partner_type_belief +
        (1 | pair_id),
    data = d,
    family = binomial(link = "logit")
)

h1b_model_table <- extract_model_table(h1b_model, term_map = reg_term_map)

h1b_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "z", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Preregistered controlled late-period logit model for Hypothesis 1b"
    )
Preregistered controlled late-period logit model for Hypothesis 1b
Term Estimate Std. Error z p-value
(Intercept) 0.711 1.336 0.532 0.594
LH vs HH -0.132 0.425 -0.311 0.756
LL vs HH 0.333 0.446 0.745 0.456
Late period -0.771 0.187 -4.134 0.000
MSCEIT score 0.005 0.007 0.698 0.485
MATRICES score -0.022 0.016 -1.431 0.153
PANAS positive -0.025 0.011 -2.244 0.025
PANAS negative 0.027 0.014 1.955 0.051
Age 0.015 0.018 0.835 0.403
Male -0.270 0.154 -1.749 0.080
High-school mark -0.003 0.008 -0.406 0.684
Belief own type is high -0.009 0.004 -2.226 0.026
Belief partner type is high 0.007 0.004 1.589 0.112
LH x late period -0.075 0.256 -0.292 0.771
LL x late period 0.145 0.261 0.555 0.579
Show the R code
mixed_model_information(h1b_model) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the controlled H1b logit model"
    )
Model information for the controlled H1b logit model
Model statistic Value
Observations 2210.000
Dyads 113.000
Random-intercept variance 2.661
Log likelihood -1242.341
AIC 2516.683
BIC 2607.895
Show the R code
# Wald contrasts corresponding directly to the components of H1b. Directional
# p-values are used for directional predictions; the early HH--LH comparison
# remains two-sided because the prediction is similarity rather than ordering.
h1b_wald_contrast <- function(model, weights, hypothesis, alternative) {
    beta <- lme4::fixef(model)
    contrast <- stats::setNames(rep(0, length(beta)), names(beta))
    contrast[names(weights)] <- weights

    estimate <- sum(contrast * beta)
    std_error <- sqrt(as.numeric(t(contrast) %*% stats::vcov(model) %*% contrast))
    z <- estimate / std_error
    p_value <- switch(
        alternative,
        less = stats::pnorm(z),
        greater = stats::pnorm(z, lower.tail = FALSE),
        two.sided = 2 * stats::pnorm(abs(z), lower.tail = FALSE)
    )

    tibble(
        hypothesis,
        alternative,
        estimate,
        std_error,
        statistic = z,
        p_value
    )
}

h1b_contrasts <- bind_rows(
    h1b_wald_contrast(
        h1b_treatment_model,
        c("late_round" = 1),
        "HH: late cooperation < early cooperation",
        "less"
    ),
    h1b_wald_contrast(
        h1b_treatment_model,
        c("late_round" = -1, "type_matchLH:late_round" = -1),
        "LH early cooperation > LH late cooperation",
        "greater"
    ),
    h1b_wald_contrast(
        h1b_treatment_model,
        c("late_round" = 1, "type_matchLL:late_round" = 1),
        "LL: late cooperation < early cooperation",
        "less"
    ),
    h1b_wald_contrast(
        h1b_treatment_model,
        c("type_matchLH" = 1),
        "LH early cooperation = HH early cooperation",
        "two.sided"
    ),
    h1b_wald_contrast(
        h1b_treatment_model,
        c("type_matchLH:late_round" = 1),
        "LH decline > HH decline",
        "less"
    )
)

h1b_contrasts |>
    round_table() |>
    kable(caption = "Treatment-only model-based Wald contrasts for Hypothesis 1b")
Treatment-only model-based Wald contrasts for Hypothesis 1b
hypothesis alternative estimate std_error statistic p_value
HH: late cooperation < early cooperation less -0.717 0.181 -3.950 0.000
LH early cooperation > LH late cooperation greater 0.826 0.174 4.757 0.000
LL: late cooperation < early cooperation less -0.614 0.177 -3.463 0.000
LH early cooperation = HH early cooperation two.sided -0.042 0.393 -0.107 0.915
LH decline > HH decline less -0.110 0.251 -0.437 0.331

6.3 Hypothesis 1c (EI, Beliefs, and Learning)

Show the R code
df_belief_match <- d |>
    group_by(round, type_match) |>
    summarise(mean_belief = mean(belief_partner_coop, na.rm = TRUE), .groups = "drop")

mean_belief_match <- mean(df_belief_match$mean_belief, na.rm = TRUE)

ggplot(df_belief_match, aes(x = round, y = mean_belief, color = type_match, pch = type_match)) +
    geom_line(linewidth = 1, alpha = .7) +
    geom_point(size = 5, alpha = 0.5) +
    geom_hline(yintercept = mean_belief_match, color = ref_mean_col, linetype = "longdash", linewidth = 0.9, alpha = 0.8) +
    annotate(
        "text",
        x = -Inf,
        y = mean_belief_match,
        label = sprintf("%.1f%%", mean_belief_match),
        color = ref_mean_col,
        hjust = 1,
        vjust = -0.4,
        size = 5.2
    ) +
    scale_color_manual(values = pal_match) +
    scale_x_continuous(breaks = seq(1, 10, 1)) +
    scale_y_continuous(labels = scales::percent_format(scale = 1)) +
    labs(
        title = "",
        x = "Round",
        y = "Belief About Partner Cooperation"
    ) +
    line_theme +
    theme(
        legend.position = "bottom",
        axis.text = element_text(size = 16),
        axis.title = element_text(size = 18),
        legend.text = element_text(size = 16),
        plot.margin = margin(5.5, 5.5, 5.5, 45)
    )

Beliefs about partner cooperation by matching type (HH, LH, LL)
Show the R code
ggsave(file.path(figure_dir, "Beliefs_by_Matching.png"), width = 9, height = 6, dpi = 300)

Note. Points show mean beliefs about partner cooperation in each round by matching type; lines connect round-specific means, and the dashed horizontal line marks the overall mean.

Show the R code
pair_belief_h1c <- d |>
    group_by(pair_id, type_match) |>
    summarise(
        pair_mean_belief = mean(belief_partner_coop, na.rm = TRUE),
        .groups = "drop"
    )

pair_belief_h1c |>
    group_by(type_match) |>
    summarise(
        mean_pair_belief = mean(pair_mean_belief, na.rm = TRUE),
        sd_pair_belief = sd(pair_mean_belief, na.rm = TRUE),
        .groups = "drop"
    ) |>
    kable(caption = "Pair-level average beliefs about partner cooperation by matching type")
Pair-level average beliefs about partner cooperation by matching type
type_match mean_pair_belief sd_pair_belief
HH 53.26447 21.17572
LH 50.76579 19.58553
LL 56.23816 17.03989
Show the R code
pair_belief_h1c |>
    filter(type_match %in% c("HH", "LH")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LH"))) |>
    wilcox.test(pair_mean_belief ~ type_match, data = _, alternative = "greater", exact = FALSE) |>
    broom::tidy() |>
    kable(caption = "Wilcoxon test comparing pair-level average beliefs between HH and LH matching types")
Wilcoxon test comparing pair-level average beliefs between HH and LH matching types
statistic p.value method alternative
767 0.3219328 Wilcoxon rank sum test with continuity correction greater
Show the R code
pair_belief_h1c |>
    filter(type_match %in% c("HH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LL"))) |>
    wilcox.test(pair_mean_belief ~ type_match, data = _, alternative = "greater", exact = FALSE) |>
    broom::tidy() |>
    kable(caption = "Wilcoxon test comparing pair-level average beliefs between HH and LL matching types")
Wilcoxon test comparing pair-level average beliefs between HH and LL matching types
statistic p.value method alternative
646 0.7866166 Wilcoxon rank sum test with continuity correction greater
Show the R code
pair_belief_h1c |>
    filter(type_match %in% c("LH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("LH", "LL"))) |>
    wilcox.test(pair_mean_belief ~ type_match, data = _, alternative = "greater", exact = FALSE) |>
    broom::tidy() |>
    kable(caption = "Wilcoxon test comparing pair-level average beliefs between LH and LL matching types")
Wilcoxon test comparing pair-level average beliefs between LH and LL matching types
statistic p.value method alternative
612.5 0.8734337 Wilcoxon rank sum test with continuity correction greater
Show the R code
pair_belief_h1c |>
    filter(type_match %in% c("HH", "LH")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LH"))) |>
    t.test(pair_mean_belief ~ type_match, data = _, alternative = "greater") |>
    broom::tidy() |>
    kable(caption = "t-test comparing pair-level average beliefs between HH and LH matching types")
t-test comparing pair-level average beliefs between HH and LH matching types
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
2.498684 53.26447 50.76579 0.533999 0.2974762 73.55358 -5.296078 Inf Welch Two Sample t-test greater
Show the R code
pair_belief_h1c |>
    filter(type_match %in% c("HH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("HH", "LL"))) |>
    t.test(pair_mean_belief ~ type_match, data = _, alternative = "greater") |>
    broom::tidy() |>
    kable(caption = "t-test comparing pair-level average beliefs between HH and LL matching types")
t-test comparing pair-level average beliefs between HH and LL matching types
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
-2.973684 53.26447 56.23816 -0.6744229 0.74888 70.7612 -10.32243 Inf Welch Two Sample t-test greater
Show the R code
pair_belief_h1c |>
    filter(type_match %in% c("LH", "LL")) |>
    mutate(type_match = factor(type_match, levels = c("LH", "LL"))) |>
    t.test(pair_mean_belief ~ type_match, data = _, alternative = "greater") |>
    broom::tidy() |>
    kable(caption = "t-test comparing pair-level average beliefs between LH and LL matching types")
t-test comparing pair-level average beliefs between LH and LL matching types
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
-5.472368 50.76579 56.23816 -1.29943 0.9010465 72.61037 -12.48896 Inf Welch Two Sample t-test greater

Note. Each observation is a dyad’s mean belief about partner cooperation, averaged across its participants and rounds.

6.3.1 Regression analysis for Hypothesis 1c

Outcome: stated probability (0–100) that the partner chooses A. Both models include a dyad random intercept; the controlled model additionally includes the listed covariates.

Show the R code
h1c_treatment_model <- fit_model_lmer(
    belief_partner_coop ~ type_match + (1 | pair_id),
    data = d
)

h1c_treatment_model_table <- extract_model_table(
    h1c_treatment_model,
    term_map = reg_term_map
)

h1c_treatment_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "t", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Treatment-only belief model for Hypothesis 1c"
    )
Treatment-only belief model for Hypothesis 1c
Term Estimate Std. Error t p-value
(Intercept) 53.264 3.096 17.203 0.000
LH vs HH -2.499 4.379 -0.571 0.569
LL vs HH 2.974 4.379 0.679 0.498
Show the R code
mixed_model_information(h1c_treatment_model, include_residual = TRUE) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the treatment-only H1c belief model"
    )
Model information for the treatment-only H1c belief model
Model statistic Value
Observations 2280.000
Dyads 114.000
Random-intercept variance 336.017
Residual variance 565.168
Log likelihood -10605.222
AIC 21220.444
BIC 21249.104
Show the R code
h1c_model <- fit_model_lmer(
    belief_partner_coop ~ type_match +
        msceit_score + matrices_score + panas_positive + panas_negative +
        age + gender + high_school_mark + own_type_belief + partner_type_belief +
        (1 | pair_id),
    data = d
)

h1c_model_table <- extract_model_table(h1c_model, term_map = reg_term_map)

h1c_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "t", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Preregistered controlled belief model for Hypothesis 1c"
    )
Preregistered controlled belief model for Hypothesis 1c
Term Estimate Std. Error t p-value
(Intercept) 12.334 12.593 0.979 0.327
LH vs HH -2.312 4.194 -0.551 0.583
LL vs HH 0.052 4.364 0.012 0.990
MSCEIT score -0.067 0.070 -0.958 0.338
MATRICES score -0.375 0.146 -2.572 0.010
PANAS positive 0.394 0.104 3.790 0.000
PANAS negative 0.340 0.127 2.682 0.007
Age 0.062 0.182 0.341 0.733
Male 2.053 1.454 1.412 0.158
High-school mark 0.138 0.071 1.958 0.050
Belief own type is high 0.113 0.040 2.836 0.005
Belief partner type is high 0.308 0.040 7.663 0.000
Show the R code
mixed_model_information(h1c_model, include_residual = TRUE) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the controlled H1c belief model"
    )
Model information for the controlled H1c belief model
Model statistic Value
Observations 2210.000
Dyads 113.000
Random-intercept variance 296.608
Residual variance 536.694
Log likelihood -10220.365
AIC 20468.730
BIC 20548.541
Show the R code
car::linearHypothesis(h1c_treatment_model, "type_matchLH = type_matchLL", test = "Chisq") |>
    as.data.frame() |>
    tibble::rownames_to_column("Model") |>
    kable(caption = "Treatment-only linear hypothesis test for LH = LL in the H1c belief model")
Treatment-only linear hypothesis test for LH = LL in the H1c belief model
Model Df Chisq Pr(>Chisq)
1 NA NA NA
2 1 1.561976 0.2113761

6.3.2 Continuous-round belief model

Outcome: stated probability (0–100) that the partner chooses A. The models include matching condition, round, their interaction, and a dyad random intercept.

Show the R code
h1c_round_treatment_model <- fit_model_lmer(
    belief_partner_coop ~ type_match * round + (1 | pair_id),
    data = d
)

h1c_round_treatment_model_table <- extract_model_table(
    h1c_round_treatment_model,
    term_map = reg_term_map
)

h1c_round_treatment_lh <- h1c_round_treatment_model_table |>
    filter(term == "LH x round number")

h1c_round_treatment_ll <- h1c_round_treatment_model_table |>
    filter(term == "LL x round number")

h1c_round_treatment_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "t", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Treatment-only continuous-round belief model for Hypothesis 1c"
    )
Treatment-only continuous-round belief model for Hypothesis 1c
Term Estimate Std. Error t p-value
(Intercept) 55.422 3.506 15.810 0.000
LH vs HH 2.161 4.958 0.436 0.663
LL vs HH 0.925 4.958 0.187 0.852
Round number -0.392 0.299 -1.312 0.190
LH x round number -0.847 0.423 -2.004 0.045
LL x round number 0.372 0.423 0.881 0.378
Show the R code
mixed_model_information(h1c_round_treatment_model, include_residual = TRUE) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the treatment-only continuous-round belief model"
    )
Model information for the treatment-only continuous-round belief model
Model statistic Value
Observations 2280.000
Dyads 114.000
Random-intercept variance 336.262
Residual variance 560.275
Log likelihood -10595.805
AIC 21207.610
BIC 21253.465
Show the R code
h1c_round_model <- fit_model_lmer(
    belief_partner_coop ~ type_match * round +
        msceit_score + matrices_score + panas_positive + panas_negative +
        age + gender + high_school_mark + own_type_belief + partner_type_belief +
        (1 | pair_id),
    data = d
)

h1c_round_model_table <- extract_model_table(
    h1c_round_model,
    term_map = reg_term_map
)

h1c_round_lh <- h1c_round_model_table |>
    filter(term == "LH x round number")

h1c_round_ll <- h1c_round_model_table |>
    filter(term == "LL x round number")

h1c_round_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "t", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Additional continuous-round belief model for Hypothesis 1c"
    )
Additional continuous-round belief model for Hypothesis 1c
Term Estimate Std. Error t p-value
(Intercept) 14.888 12.642 1.178 0.239
LH vs HH 1.981 4.777 0.415 0.679
LL vs HH -1.266 4.937 -0.257 0.798
Round number -0.463 0.295 -1.571 0.116
MSCEIT score -0.067 0.070 -0.965 0.335
MATRICES score -0.375 0.145 -2.588 0.010
PANAS positive 0.394 0.104 3.810 0.000
PANAS negative 0.341 0.126 2.701 0.007
Age 0.061 0.181 0.339 0.735
Male 2.054 1.448 1.419 0.156
High-school mark 0.139 0.070 1.971 0.049
Belief own type is high 0.113 0.040 2.845 0.004
Belief partner type is high 0.307 0.040 7.696 0.000
LH x round number -0.782 0.416 -1.879 0.060
LL x round number 0.239 0.420 0.568 0.570
Show the R code
mixed_model_information(h1c_round_model, include_residual = TRUE) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the controlled continuous-round belief model"
    )
Model information for the controlled continuous-round belief model
Model statistic Value
Observations 2210.000
Dyads 113.000
Random-intercept variance 296.948
Residual variance 531.346
Log likelihood -10209.876
AIC 20453.752
BIC 20550.665

6.4 Hypothesis 2: EI and Predictive Inference (Type Detection)

Partner-type accuracy is (^T_i = 100 - |BC_i - T_i|), where (BC_i) is the reported probability that the partner is high EI and (T_i) is the partner’s actual type (100 = high EI; 0 = low EI).

6.4.1 Own-type prediction accuracy

Own-type accuracy uses the same transformation.

Show the R code
d_own_belief <- d |>
    select(participant_id, player_type, own_type_belief) |>
    distinct() |>
    drop_na() |>
    mutate(
        player_type_hl = factor(player_type, levels = c("High", "Low"), labels = c("High", "Low")),
        target = if_else(player_type == "High", 100, 0),
        own_type_accuracy = 100 - abs(own_type_belief - target)
    )

p_own_type <- ggplot(
    d_own_belief,
    aes(x = player_type_hl, y = own_type_accuracy, fill = player_type)
) +
    geom_violin(
        width = 0.85,
        color = "grey20",
        alpha = 0.55,
        trim = FALSE
    ) +
    geom_point(
        aes(color = player_type),
        position = position_jitter(width = 0.08, height = 0, seed = 2026),
        size = 1.8,
        alpha = 0.7,
        show.legend = FALSE
    ) +
    stat_summary(
        fun = mean, geom = "point", shape = 21,
        fill = ref_mean_col, color = "black", size = 5
    ) +
    stat_summary(
        fun = median, geom = "point", shape = 95,
        color = "black", size = 17
    ) +
    scale_fill_manual(values = pal_type) +
    scale_color_manual(values = pal_type) +
    scale_y_continuous(labels = scales::percent_format(scale = 1), limits = c(0, 100)) +
    labs(title = "", x = "EI Type", y = "Prediction Accuracy") +
    line_theme +
    theme(
        legend.position = "none",
        axis.text = element_text(size = 14),
        axis.title = element_text(size = 16)
    )

ggsave(file.path(figure_dir, "Prediction_accuracy_own_type.png"), plot = p_own_type, width = 7, height = 5, dpi = 300)
p_own_type

Own-Type Prediction Accuracy by Actual EI Type
Show the R code
d_own_summary <- d_own_belief |>
    group_by(player_type) |>
    summarise(
        n = n(),
        mean_accuracy = mean(own_type_accuracy, na.rm = TRUE),
        median_accuracy = median(own_type_accuracy, na.rm = TRUE),
        sd_accuracy = sd(own_type_accuracy, na.rm = TRUE),
        .groups = "drop"
    )

d_own_summary |>
    round_table() |>
    kable(
        col.names = c("Actual EI type", "n", "Mean accuracy", "Median accuracy", "SD"),
        caption = "Own-type prediction accuracy by actual EI type"
    )
Own-type prediction accuracy by actual EI type
Actual EI type n Mean accuracy Median accuracy SD
Low 114 45.053 45.0 20.590
High 114 55.132 58.5 18.841

Own-Type Prediction Accuracy by Actual EI Type

Show the R code
own_accuracy_wilcox <- d_own_belief |>
    wilcox.test(own_type_accuracy ~ player_type_hl, data = _, exact = FALSE) |>
    broom::tidy()

own_accuracy_wilcox |>
    kable(caption = "Wilcoxon test: own-type prediction accuracy, comparing actual high and low EI types")
Wilcoxon test: own-type prediction accuracy, comparing actual high and low EI types
statistic p.value method alternative
8592 0.0000257 Wilcoxon rank sum test with continuity correction two.sided

Own-Type Prediction Accuracy by Actual EI Type

Show the R code
own_accuracy_ttest <- d_own_belief |>
    t.test(own_type_accuracy ~ player_type_hl, data = _) |>
    broom::tidy()

own_accuracy_ttest |>
    kable(caption = "t-test: own-type prediction accuracy, comparing actual high and low EI types")
t-test: own-type prediction accuracy, comparing actual high and low EI types
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
10.07895 55.13158 45.05263 3.855755 0.0001507 224.2425 4.927799 15.2301 Welch Two Sample t-test two.sided

Own-Type Prediction Accuracy by Actual EI Type

6.4.2 Prediction accuracy by matching condition

Show the R code
d_other_belief <- d |>
    select(
        participant_id,
        pair_id,
        player_type,
        partner_type_actual,
        partner_type_belief,
        msceit_score,
        matrices_score,
        panas_positive,
        panas_negative,
        age,
        gender,
        high_school_mark
    ) |>
    distinct() |>
    drop_na(
        participant_id,
        pair_id,
        player_type,
        partner_type_actual,
        partner_type_belief
    )

d_matching <- d_other_belief |>
    mutate(
        matching = case_when(
            player_type == "High" & partner_type_actual == "high" ~ "HH",
            player_type == "Low" & partner_type_actual == "low" ~ "LL",
            TRUE ~ "LH"
        ),
        target = if_else(partner_type_actual == "high", 100, 0),
        beta = 100 - abs(partner_type_belief - target),
        matching = factor(matching, levels = c("HH", "LH", "LL"))
    )

p_prediction_accuracy <- ggplot(
    d_matching,
    aes(x = matching, y = beta, fill = matching)
) +
    geom_violin(
        width = 0.85,
        color = "grey20",
        alpha = 0.55,
        trim = FALSE
    ) +
    geom_point(
        aes(color = matching),
        position = position_jitter(width = 0.08, height = 0, seed = 2026),
        size = 1.8,
        alpha = 0.7,
        show.legend = FALSE
    ) +
    stat_summary(
        fun = mean, geom = "point", shape = 21,
        fill = ref_mean_col, color = "black", size = 5
    ) +
    stat_summary(
        fun = median, geom = "point", shape = 95,
        color = "black", size = 17
    ) +
    scale_fill_manual(values = c(HH = pal_match[["HH"]], LH = pal_match[["LH"]], LL = pal_match[["LL"]])) +
    scale_color_manual(values = c(HH = pal_match[["HH"]], LH = pal_match[["LH"]], LL = pal_match[["LL"]])) +
    scale_y_continuous(labels = scales::percent_format(scale = 1), limits = c(0, 100)) +
    labs(
        title = "",
        x = "Matching",
        y = "Prediction Accuracy"
    ) +
    line_theme +
    theme(
        legend.position = "none",
        axis.text = element_text(size = 14),
        axis.title = element_text(size = 16)
    )

ggsave(file.path(figure_dir, "Prediction_accuracy_by_matching.png"), plot = p_prediction_accuracy, width = 8, height = 5, dpi = 300)
p_prediction_accuracy

Partner-Type Prediction Accuracy by Matching Condition
Show the R code
h2_beta_summary <- d_matching |>
    group_by(matching) |>
    summarise(
        n = n(),
        mean_beta = mean(beta, na.rm = TRUE),
        median_beta = median(beta, na.rm = TRUE),
        sd_beta = sd(beta, na.rm = TRUE),
        .groups = "drop"
    )

h2_beta_summary |>
    round_table() |>
    kable(
        col.names = c("Matching", "n", "Mean beta", "Median beta", "SD"),
        caption = "Partner-type prediction accuracy by matching type"
    )
Partner-type prediction accuracy by matching type
Matching n Mean beta Median beta SD
HH 76 54.079 50.0 16.989
LH 76 50.553 50.0 20.112
LL 76 46.184 46.5 18.119

Partner-Type Prediction Accuracy by Matching Condition

Show the R code
h2_kruskal <- broom::tidy(kruskal.test(beta ~ matching, data = d_matching))

h2_kruskal |>
    round_table() |>
    kable(caption = "Kruskal-Wallis test: beta across matching types")
Kruskal-Wallis test: beta across matching types
statistic p.value parameter method
9.293 0.01 2 Kruskal-Wallis rank sum test

Partner-Type Prediction Accuracy by Matching Condition

Show the R code
d_hh_lh <- d_matching |>
    filter(matching %in% c("HH", "LH")) |>
    mutate(matching = droplevels(matching))

d_ll_lh <- d_matching |>
    filter(matching %in% c("LL", "LH")) |>
    mutate(matching = factor(matching, levels = c("LL", "LH")))

d_hh_ll <- d_matching |>
    filter(matching %in% c("HH", "LL")) |>
    mutate(matching = droplevels(matching))

h2_wilcox_hh_lh <- wilcox.test(beta ~ matching, data = d_hh_lh, exact = FALSE, alternative = "greater") |>
    broom::tidy()

h2_wilcox_hh_lh |>
    kable(caption = "Wilcoxon test: beta HH > beta LH (one-sided)")
Wilcoxon test: beta HH > beta LH (one-sided)
statistic p.value method alternative
3135.5 0.1804821 Wilcoxon rank sum test with continuity correction greater
Show the R code
h2_ttest_hh_lh <- t.test(beta ~ matching, data = d_hh_lh, alternative = "greater") |>
    broom::tidy()

h2_ttest_hh_lh |>
    kable(caption = "t-test: beta HH > beta LH (one-sided)")
t-test: beta HH > beta LH (one-sided)
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
3.526316 54.07895 50.55263 1.167676 0.1224209 145.923 -1.472786 Inf Welch Two Sample t-test greater
Show the R code
h2_wilcox_ll_lh <- wilcox.test(beta ~ matching, data = d_ll_lh, exact = FALSE, alternative = "greater") |>
    broom::tidy()

h2_wilcox_ll_lh |>
    kable(caption = "Wilcoxon test: beta LL > beta LH (one-sided)")
Wilcoxon test: beta LL > beta LH (one-sided)
statistic p.value method alternative
2350 0.976576 Wilcoxon rank sum test with continuity correction greater
Show the R code
h2_ttest_ll_lh <- t.test(beta ~ matching, data = d_ll_lh, alternative = "greater") |>
    broom::tidy()

h2_ttest_ll_lh |>
    kable(caption = "t-test: beta LL > beta LH (one-sided)")
t-test: beta LL > beta LH (one-sided)
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
-4.368421 46.18421 50.55263 -1.406817 0.9192138 148.3963 -9.508073 Inf Welch Two Sample t-test greater
Show the R code
h2_wilcox_hh_ll <- wilcox.test(beta ~ matching, data = d_hh_ll, exact = FALSE, alternative = "greater") |>
    broom::tidy()

h2_wilcox_hh_ll |>
    kable(caption = "Wilcoxon test: beta HH > beta LL (one-sided)")
Wilcoxon test: beta HH > beta LL (one-sided)
statistic p.value method alternative
3706 0.001267 Wilcoxon rank sum test with continuity correction greater
Show the R code
h2_ttest_hh_ll <- t.test(beta ~ matching, data = d_hh_ll, alternative = "greater") |>
    broom::tidy()

h2_ttest_hh_ll |>
    kable(caption = "t-test: beta HH > beta LL (one-sided)")
t-test: beta HH > beta LL (one-sided)
estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high method alternative
7.894737 54.07895 46.18421 2.7709 0.0031504 149.3821 3.179041 Inf Welch Two Sample t-test greater

6.4.3 Reported probability by actual partner type

Because the accuracy transformation depends on whether the partner is actually high or low EI, the following diagnostic compares the underlying reported probability that the partner is high EI across the two actual partner types.

Show the R code
h2_partner_type_probability <- d_matching |>
    mutate(
        actual_partner_type = factor(
            partner_type_actual,
            levels = c("high", "low"),
            labels = c("High EI", "Low EI")
        )
    )

h2_partner_type_probability |>
    group_by(actual_partner_type) |>
    summarise(
        n = n(),
        mean_reported_probability = mean(partner_type_belief),
        sd_reported_probability = sd(partner_type_belief),
        .groups = "drop"
    ) |>
    round_table() |>
    kable(
        col.names = c("Actual partner type", "n", "Mean reported probability", "SD"),
        caption = "Reported probability that the partner is high EI, by actual partner type"
    )
Reported probability that the partner is high EI, by actual partner type
Actual partner type n Mean reported probability SD
High EI 114 53.342 17.945
Low EI 114 52.798 18.919
Show the R code
h2_partner_type_ttest <- t.test(
    partner_type_belief ~ actual_partner_type,
    data = h2_partner_type_probability,
    alternative = "two.sided"
)

h2_partner_type_ttest |>
    broom::tidy() |>
    transmute(
        mean_high_ei_partner = estimate1,
        mean_low_ei_partner = estimate2,
        difference_pp = estimate1 - estimate2,
        statistic,
        df = parameter,
        p_value = p.value
    ) |>
    round_table() |>
    kable(
        col.names = c(
            "Mean: high-EI partner",
            "Mean: low-EI partner",
            "Difference (pp)",
            "t",
            "df",
            "p-value"
        ),
        caption = "Two-sided Welch t-test of reported partner-type probabilities"
    )
Two-sided Welch t-test of reported partner-type probabilities
Mean: high-EI partner Mean: low-EI partner Difference (pp) t df p-value
53.342 52.798 0.544 0.223 225.372 0.824

6.4.4 Regression analysis for Hypothesis 2

Outcome: partner-type prediction accuracy. The treatment-only model includes matching condition; the controlled model additionally includes the listed covariates.

Show the R code
h2_treatment_model <- lm(
    beta ~ matching,
    data = d_matching
)

h2_treatment_model_table <- extract_model_table(
    h2_treatment_model,
    term_map = c(
        "matchingLH" = "LH vs HH",
        "matchingLL" = "LL vs HH"
    )
)

h2_treatment_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "t", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Treatment-only regression for Hypothesis 2: partner-type prediction accuracy"
    )
Treatment-only regression for Hypothesis 2: partner-type prediction accuracy
Term Estimate Std. Error t p-value
(Intercept) 54.079 2.117 25.550 0.000
LH vs HH -3.526 2.993 -1.178 0.240
LL vs HH -7.895 2.993 -2.637 0.009
Show the R code
h2_treatment_model_rows <- as.integer(rownames(model.frame(h2_treatment_model)))

lm_model_information(
    h2_treatment_model,
    dyads = n_distinct(d_matching$pair_id[h2_treatment_model_rows])
) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the treatment-only H2 linear model"
    )
Model information for the treatment-only H2 linear model
Model statistic Value
Observations 228.000
Dyads 114.000
R-squared 0.030
Adjusted R-squared 0.021
Residual standard error 18.452
Log likelihood -986.668
AIC 1981.337
BIC 1995.054
Show the R code
h2_model <- lm(
    beta ~ matching + msceit_score + matrices_score + panas_positive + panas_negative +
        age + gender + high_school_mark,
    data = d_matching
)

h2_model_table <- extract_model_table(
    h2_model,
    term_map = c(
        "matchingLH" = "LH vs HH",
        "matchingLL" = "LL vs HH",
        "msceit_score" = "MSCEIT score",
        "matrices_score" = "MATRICES score",
        "panas_positive" = "PANAS positive",
        "panas_negative" = "PANAS negative",
        "age" = "Age",
        "genderMaschile" = "Male",
        "genderMale" = "Male",
        "high_school_mark" = "High-school mark"
    )
)

h2_model_table |>
    select(term, estimate, std_error, statistic, p_value) |>
    kable(
        col.names = c("Term", "Estimate", "Std. Error", "t", "p-value"),
        align = c("l", "r", "r", "r", "r"),
        caption = "Regression results for Hypothesis 2: partner-type prediction accuracy"
    )
Regression results for Hypothesis 2: partner-type prediction accuracy
Term Estimate Std. Error t p-value
(Intercept) 63.022 22.769 2.768 0.006
LH vs HH -4.699 3.187 -1.474 0.142
LL vs HH -8.960 3.843 -2.332 0.021
MSCEIT score -0.070 0.134 -0.523 0.601
MATRICES score 0.081 0.268 0.301 0.764
PANAS positive -0.173 0.170 -1.015 0.311
PANAS negative -0.045 0.232 -0.196 0.845
Age 0.107 0.339 0.316 0.752
Male 3.555 2.705 1.314 0.190
High-school mark 0.009 0.120 0.075 0.940
Show the R code
h2_model_rows <- as.integer(rownames(model.frame(h2_model)))

lm_model_information(
    h2_model,
    dyads = n_distinct(d_matching$pair_id[h2_model_rows])
) |>
    kable(
        col.names = c("Model statistic", "Value"),
        align = c("l", "r"),
        caption = "Model information for the controlled H2 linear model"
    )
Model information for the controlled H2 linear model
Model statistic Value
Observations 221.000
Dyads 113.000
R-squared 0.046
Adjusted R-squared 0.005
Residual standard error 17.930
Log likelihood -946.374
AIC 1914.749
BIC 1952.128
Show the R code
car::linearHypothesis(h2_treatment_model, "matchingLH = matchingLL") |>
    as.data.frame() |>
    tibble::rownames_to_column("Model") |>
    kable(caption = "Treatment-only linear hypothesis test for LH = LL in the partner-type accuracy model")
Treatment-only linear hypothesis test for LH = LL in the partner-type accuracy model
Model Res.Df RSS Df Sum of Sq F Pr(>F)
1 226 77332.89 NA NA NA NA
2 225 76607.74 1 725.1579 2.129818 0.1458526

6.4.5 Robustness to within-dyad dependence

The H2 outcome is observed once per participant, leaving two potentially dependent observations per dyad. The following checks retain the preregistered treatment-only specification but account for this dependence in two ways. The first uses CR1 standard errors clustered by dyad, with inference based on 113 degrees of freedom. The second averages prediction accuracy within dyads and compares HH and LL dyads using the preregistered directional alternative.

Show the R code
# Dyad-clustered CR1 covariance matrix for the treatment-only H2 model.
h2_cluster_rows <- as.integer(rownames(model.frame(h2_treatment_model)))
h2_cluster <- d_matching$pair_id[h2_cluster_rows]
h2_model_matrix <- model.matrix(h2_treatment_model)
h2_residuals <- residuals(h2_treatment_model)

h2_n <- nrow(h2_model_matrix)
h2_k <- ncol(h2_model_matrix)
h2_g <- n_distinct(h2_cluster)

h2_cluster_scores <- rowsum(
    h2_model_matrix * h2_residuals,
    group = h2_cluster,
    reorder = FALSE
)
h2_bread <- solve(crossprod(h2_model_matrix))
h2_cr1_adjustment <- (h2_g / (h2_g - 1)) * ((h2_n - 1) / (h2_n - h2_k))
h2_cluster_vcov <- h2_cr1_adjustment *
    h2_bread %*% crossprod(h2_cluster_scores) %*% h2_bread

h2_cluster_se <- sqrt(diag(h2_cluster_vcov))
h2_cluster_statistic <- coef(h2_treatment_model) / h2_cluster_se

h2_clustered_results <- tibble(
    term = names(coef(h2_treatment_model)),
    estimate = unname(coef(h2_treatment_model)),
    std_error = unname(h2_cluster_se),
    statistic = unname(h2_cluster_statistic),
    df = h2_g - 1,
    p_value = 2 * pt(abs(statistic), df = df, lower.tail = FALSE)
) |>
    mutate(
        term = recode(
            term,
            `(Intercept)` = "Intercept",
            matchingLH = "LH vs HH",
            matchingLL = "LL vs HH"
        )
    )

h2_clustered_results |>
    round_table() |>
    kable(
        col.names = c("Term", "Estimate", "Clustered SE", "t", "df", "p-value"),
        caption = "Treatment-only H2 regression with CR1 standard errors clustered by dyad"
    )
Treatment-only H2 regression with CR1 standard errors clustered by dyad
Term Estimate Clustered SE t df p-value
Intercept 54.079 1.947 27.780 113 0.000
LH vs HH -3.526 2.844 -1.240 113 0.218
LL vs HH -7.895 2.690 -2.935 113 0.004
Show the R code
h2_dyad_accuracy <- d_matching |>
    group_by(pair_id, matching) |>
    summarise(beta = mean(beta), .groups = "drop")

h2_dyad_hh_ll <- h2_dyad_accuracy |>
    filter(matching %in% c("HH", "LL")) |>
    mutate(matching = droplevels(matching))

h2_dyad_welch_hh_ll <- t.test(
    beta ~ matching,
    data = h2_dyad_hh_ll,
    alternative = "greater"
)

h2_dyad_welch_hh_ll |>
    broom::tidy() |>
    transmute(
        mean_hh = estimate1,
        mean_ll = estimate2,
        difference_hh_minus_ll = estimate1 - estimate2,
        statistic,
        df = parameter,
        p_value = p.value
    ) |>
    round_table() |>
    kable(
        col.names = c("Mean HH", "Mean LL", "Difference", "t", "df", "p-value"),
        caption = "Dyad-level one-sided Welch test of HH prediction accuracy exceeding LL"
    )
Dyad-level one-sided Welch test of HH prediction accuracy exceeding LL
Mean HH Mean LL Difference t df p-value
54.079 46.184 7.895 2.922 73.832 0.002

7 Multiple-testing adjustments

7.1 Holm-adjusted robustness check for pairwise tests

The tests reported above retain the preregistered directional comparisons and their original, unadjusted (p)-values. As a robustness check, we additionally apply Holm’s adjustment to the reported pairwise tests within each hypothesis. For H1a, this family comprises the three participant-level proportion tests and the three dyad-level comparisons from each of the Wilcoxon and Welch tests. For H1c and H2, it comprises the three comparisons from each of the Wilcoxon and Welch tests. This table covers the between-condition pairwise comparisons for H1a, H1c, and H2. H1b temporal tests and exploratory belief-dynamics comparisons are assessed separately in the following multiplicity assessment.

Show the R code
dyad_pairwise_test <- function(data, outcome, group1, group2, test = c("Wilcoxon", "Welch t")) {
    test <- match.arg(test)
    x <- data |>
        filter(type_match == group1) |>
        pull({{ outcome }})
    y <- data |>
        filter(type_match == group2) |>
        pull({{ outcome }})

    result <- if (test == "Wilcoxon") {
        wilcox.test(x, y, alternative = "greater", exact = FALSE)
    } else {
        t.test(x, y, alternative = "greater")
    }

    broom::tidy(result) |>
        transmute(
            test,
            contrast = paste(group1, ">", group2),
            p_value = p.value
        )
}

h1a_holm <- bind_rows(
    prop_pair_test(h1a_overall_counts, "HH", "LH", alternative = "greater") |>
        transmute(test = "Proportion", contrast = "HH > LH", p_value),
    prop_pair_test(h1a_overall_counts, "HH", "LL", alternative = "greater") |>
        transmute(test = "Proportion", contrast = "HH > LL", p_value),
    prop_pair_test(h1a_overall_counts, "LH", "LL", alternative = "greater") |>
        transmute(test = "Proportion", contrast = "LH > LL", p_value),
    dyad_pairwise_test(counts_h1a, cooperation_rate, "HH", "LH", "Wilcoxon"),
    dyad_pairwise_test(counts_h1a, cooperation_rate, "HH", "LL", "Wilcoxon"),
    dyad_pairwise_test(counts_h1a, cooperation_rate, "LH", "LL", "Wilcoxon"),
    dyad_pairwise_test(counts_h1a, cooperation_rate, "HH", "LH", "Welch t"),
    dyad_pairwise_test(counts_h1a, cooperation_rate, "HH", "LL", "Welch t"),
    dyad_pairwise_test(counts_h1a, cooperation_rate, "LH", "LL", "Welch t")
) |>
    mutate(hypothesis = "H1a")

h1c_holm <- bind_rows(
    dyad_pairwise_test(pair_belief_h1c, pair_mean_belief, "HH", "LH", "Wilcoxon"),
    dyad_pairwise_test(pair_belief_h1c, pair_mean_belief, "HH", "LL", "Wilcoxon"),
    dyad_pairwise_test(pair_belief_h1c, pair_mean_belief, "LH", "LL", "Wilcoxon"),
    dyad_pairwise_test(pair_belief_h1c, pair_mean_belief, "HH", "LH", "Welch t"),
    dyad_pairwise_test(pair_belief_h1c, pair_mean_belief, "HH", "LL", "Welch t"),
    dyad_pairwise_test(pair_belief_h1c, pair_mean_belief, "LH", "LL", "Welch t")
) |>
    mutate(hypothesis = "H1c")

h2_holm <- bind_rows(
    h2_wilcox_hh_lh |>
        transmute(test = "Wilcoxon", contrast = "HH > LH", p_value = p.value),
    h2_wilcox_hh_ll |>
        transmute(test = "Wilcoxon", contrast = "HH > LL", p_value = p.value),
    h2_wilcox_ll_lh |>
        transmute(test = "Wilcoxon", contrast = "LL > LH", p_value = p.value),
    h2_ttest_hh_lh |>
        transmute(test = "Welch t", contrast = "HH > LH", p_value = p.value),
    h2_ttest_hh_ll |>
        transmute(test = "Welch t", contrast = "HH > LL", p_value = p.value),
    h2_ttest_ll_lh |>
        transmute(test = "Welch t", contrast = "LL > LH", p_value = p.value)
) |>
    mutate(hypothesis = "H2")

holm_pairwise_results <- bind_rows(h1a_holm, h1c_holm, h2_holm) |>
    group_by(hypothesis) |>
    mutate(p_holm = p.adjust(p_value, method = "holm")) |>
    ungroup() |>
    mutate(
        across(c(p_value, p_holm), ~ format.pval(.x, digits = 3, eps = .001))
    ) |>
    select(hypothesis, test, contrast, p_value, p_holm)

holm_pairwise_results |>
    kable(
        col.names = c("Hypothesis", "Test", "Contrast", "Unadjusted p", "Holm-adjusted p"),
        caption = "Holm-adjusted robustness check for preregistered pairwise tests"
    )
Holm-adjusted robustness check for preregistered pairwise tests
Hypothesis Test Contrast Unadjusted p Holm-adjusted p
H1a Proportion HH > LH 0.37906 1.0000
H1a Proportion HH > LL 0.99829 1.0000
H1a Proportion LH > LL 0.99939 1.0000
H1a Wilcoxon HH > LH 0.57034 1.0000
H1a Wilcoxon HH > LL 0.85382 1.0000
H1a Wilcoxon LH > LL 0.87199 1.0000
H1a Welch t HH > LH 0.45292 1.0000
H1a Welch t HH > LL 0.86056 1.0000
H1a Welch t LH > LL 0.90089 1.0000
H1c Wilcoxon HH > LH 0.32193 1.0000
H1c Wilcoxon HH > LL 0.78662 1.0000
H1c Wilcoxon LH > LL 0.87343 1.0000
H1c Welch t HH > LH 0.29748 1.0000
H1c Welch t HH > LL 0.74888 1.0000
H1c Welch t LH > LL 0.90105 1.0000
H2 Wilcoxon HH > LH 0.18048 0.5414
H2 Wilcoxon HH > LL 0.00127 0.0076
H2 Wilcoxon LL > LH 0.97658 1.0000
H2 Welch t HH > LH 0.12242 0.4897
H2 Welch t HH > LL 0.00315 0.0158
H2 Welch t LL > LH 0.91921 1.0000

7.2 Multiple-testing adjustments for temporal and exploratory comparisons

Show the R code
m12_format_p <- function(p) {
    ifelse(p < .001, "<.001", sub("^0", "", sprintf("%.3f", p)))
}

m12_adjusted_p <- function(p) {
    m12_format_p(p)
}

7.2.1 H1b: within-condition cooperation declines

The six dyad-level tests (three conditions, each tested with a paired Wilcoxon signed-rank test and a paired t-test) form one family. Both methods address the same set of predicted declines, so they are adjusted together. A dyad’s early and late cooperation rates are paired; the one-sided alternative is early cooperation greater than late cooperation.

Show the R code
m12_within_declines <- purrr::map_dfr(c("HH", "LH", "LL"), function(condition) {
    periods <- counts_h1b |>
        filter(type_match == condition) |>
        select(pair_id, late_period, cooperation_rate) |>
        tidyr::pivot_wider(names_from = late_period, values_from = cooperation_rate) |>
        tidyr::drop_na(`Early (1-5)`, `Late (6-10)`)

    early <- periods$`Early (1-5)`
    late <- periods$`Late (6-10)`
    tibble(
        condition = condition,
        test = c("Paired Wilcoxon", "Paired t"),
        dyads = length(early),
        p_value = c(
            wilcox.test(early, late, paired = TRUE,
                        alternative = "greater", exact = FALSE)$p.value,
            t.test(early, late, paired = TRUE, alternative = "greater")$p.value
        )
    )
}) |>
    mutate(p_holm = p.adjust(p_value, method = "holm"))

m12_within_declines |>
    transmute(Condition = condition, Test = test, Dyads = dyads,
              `Unadjusted p` = m12_format_p(p_value),
              `Holm p` = m12_adjusted_p(p_holm)) |>
    knitr::kable(caption = "H1b within-condition declines: one Holm family of six tests")
H1b within-condition declines: one Holm family of six tests
Condition Test Dyads Unadjusted p Holm p
HH Paired Wilcoxon 38 .003 .006
HH Paired t 38 .002 .006
LH Paired Wilcoxon 38 <.001 <.001
LH Paired t 38 <.001 <.001
LL Paired Wilcoxon 38 .003 .006
LL Paired t 38 .001 .006

7.2.2 H1b: difference in decline between LH and HH

Show the R code
m12_lh_decline <- diff_h1b |> filter(type_match == "LH") |> pull(decline)
m12_hh_decline <- diff_h1b |> filter(type_match == "HH") |> pull(decline)

m12_between_declines <- tibble(
    test = c("Wilcoxon rank-sum", "Welch t"),
    p_value = c(
        wilcox.test(m12_lh_decline, m12_hh_decline,
                    alternative = "greater", exact = FALSE)$p.value,
        t.test(m12_lh_decline, m12_hh_decline,
               alternative = "greater")$p.value
    )
) |>
    mutate(p_holm = p.adjust(p_value, method = "holm"))

m12_between_declines |>
    transmute(Test = test, Comparison = "LH decline > HH decline",
              `Unadjusted p` = m12_format_p(p_value),
              `Holm p` = m12_adjusted_p(p_holm)) |>
    knitr::kable(caption = "H1b difference in decline: one Holm family of two tests")
H1b difference in decline: one Holm family of two tests
Test Comparison Unadjusted p Holm p
Wilcoxon rank-sum LH decline > HH decline .116 .231
Welch t LH decline > HH decline .249 .249

7.2.3 H1b: model-based sensitivity checks and pooled-choice benchmarks

Show the R code
m12_wald_contrasts <- h1b_contrasts |>
    mutate(p_holm = p.adjust(p_value, method = "holm"),
           hypothesis = if_else(
               alternative == "two.sided",
               "Early LH vs HH (two-sided difference test)", hypothesis
           ))

m12_wald_contrasts |>
    transmute(Contrast = hypothesis, Alternative = alternative,
              `Unadjusted p` = m12_format_p(p_value),
              `Holm p` = m12_adjusted_p(p_holm)) |>
    knitr::kable(caption = "H1b treatment-only Wald contrasts: a five-test sensitivity family")
H1b treatment-only Wald contrasts: a five-test sensitivity family
Contrast Alternative Unadjusted p Holm p
HH: late cooperation < early cooperation less <.001 <.001
LH early cooperation > LH late cooperation greater <.001 <.001
LL: late cooperation < early cooperation less <.001 <.001
Early LH vs HH (two-sided difference test) two.sided .915 .915
LH decline > HH decline less .331 .662

7.2.4 Exploratory belief-dynamics interactions

Show the R code
m12_interaction_p <- function(model, specification) {
    # Use the fitted model, not extract_model_table(), which rounds p-values.
    coefficients <- coef(summary(model))
    interaction_terms <- c("type_matchLH:round", "type_matchLL:round")
    tibble(
        specification = specification,
        comparison = c("LH vs HH", "LL vs HH"),
        p_value = unname(coefficients[interaction_terms, "Pr(>|t|)"])
    )
}

m12_belief_interactions <- bind_rows(
    m12_interaction_p(h1c_round_treatment_model, "Treatment only"),
    m12_interaction_p(h1c_round_model, "With controls (secondary)")
) |>
    group_by(specification) |>
    mutate(p_holm_two = p.adjust(p_value, method = "holm")) |>
    ungroup() |>
    mutate(p_holm_four = p.adjust(p_value, method = "holm"))

m12_belief_interactions |>
    transmute(Specification = specification, Comparison = comparison,
              `Unadjusted p` = m12_format_p(p_value),
              `Holm p (2 per model)` = m12_adjusted_p(p_holm_two),
              `Holm p (4 pooled)` = m12_adjusted_p(p_holm_four)) |>
    knitr::kable(caption = "Exploratory belief-slope interactions: Holm adjustments")
Exploratory belief-slope interactions: Holm adjustments
Specification Comparison Unadjusted p Holm p (2 per model) Holm p (4 pooled)
Treatment only LH vs HH .045 .090 .181
Treatment only LL vs HH .378 .378 .757
With controls (secondary) LH vs HH .060 .121 .181
With controls (secondary) LL vs HH .570 .570 .757
Show the R code
m12_belief_lh <- m12_belief_interactions |>
    filter(specification == "Treatment only", comparison == "LH vs HH")

8 Appendix: Power and sensitivity analyses

8.1 Dyad-aware reconstruction of the registered calculation

The registered target is reconstructed using 39 dyads per condition as the independent units. Cohen’s (h=.40) is mapped to cooperation probabilities of .50 and approximately .695. With 20 choices per dyad, the variance of the dyad-level cooperation rate is

\[ \operatorname{Var}\!\left(\bar{C}_{d}\right) = \frac{p(1-p)}{m}\left[1+(m-1)\rho\right]. \]

Power is reported for several intradyad correlations using one-sided (\(\alpha=.05\)) and the conservative Bonferroni benchmark (\(\alpha=.05/3\)) for the three pairwise comparisons of H1a. The realised one-way ANOVA ICC is also reported as a descriptive anchor under the same exchangeable-correlation approximation; it is not used to estimate achieved power.

Show the R code
planned_dyads_per_condition <- 39
choices_per_dyad <- 20
reference_probability <- .50
registered_h <- .40
comparison_probability <- sin(
    asin(sqrt(reference_probability)) + registered_h / 2
)^2
registered_difference <- comparison_probability - reference_probability

anova_icc <- function(outcome, cluster) {
    cluster <- factor(cluster)
    cluster_sizes <- table(cluster)

    if (length(unique(cluster_sizes)) != 1L) {
        stop("The one-way ANOVA ICC calculation requires equal cluster sizes.")
    }

    anova_table <- summary(aov(outcome ~ cluster))[[1]]
    ms_between <- anova_table[1, "Mean Sq"]
    ms_within <- anova_table[2, "Mean Sq"]
    cluster_size <- as.numeric(unique(cluster_sizes))

    (ms_between - ms_within) /
        (ms_between + (cluster_size - 1) * ms_within)
}

realised_icc_data <- d |>
    mutate(dyad_id = interaction(session_id, pair_id, drop = TRUE)) |>
    select(dyad_id, type_match, cooperate)

realised_icc_summary <- bind_rows(
    tibble(
        scope = "All matching conditions",
        dyads = n_distinct(realised_icc_data$dyad_id),
        choices_per_dyad = choices_per_dyad,
        intradyad_correlation = anova_icc(
            realised_icc_data$cooperate,
            realised_icc_data$dyad_id
        )
    ),
    realised_icc_data |>
        group_by(type_match) |>
        summarise(
            dyads = n_distinct(dyad_id),
            choices_per_dyad = n() / dyads,
            intradyad_correlation = anova_icc(cooperate, dyad_id),
            .groups = "drop"
        ) |>
        transmute(
            scope = paste(type_match, "matching condition"),
            dyads,
            choices_per_dyad,
            intradyad_correlation
        )
)

realised_icc_summary |>
    round_table() |>
    kable(
        col.names = c(
            "Scope",
            "Dyads",
            "Choices per dyad",
            "One-way ANOVA ICC"
        ),
        caption = "Realised intradyad correlations for binary cooperation choices"
    )
Realised intradyad correlations for binary cooperation choices
Scope Dyads Choices per dyad One-way ANOVA ICC
All matching conditions 114 20 0.299
HH matching condition 38 20 0.349
LH matching condition 38 20 0.247
LL matching condition 38 20 0.300
Show the R code
rho_from_pooled_dyad_sd <- function(data, group1, group2) {
    x <- data |>
        filter(type_match == group1) |>
        pull(cooperation_rate)

    y <- data |>
        filter(type_match == group2) |>
        pull(cooperation_rate)

    pooled_variance <- (
        (length(x) - 1) * var(x) + (length(y) - 1) * var(y)
    ) / (length(x) + length(y) - 2)
    pooled_probability <- mean(c(x, y))
    implied_rho <- (
        choices_per_dyad * pooled_variance /
            (pooled_probability * (1 - pooled_probability)) - 1
    ) / (choices_per_dyad - 1)

    tibble(
        comparison = paste(group1, "vs", group2),
        pooled_dyad_sd = sqrt(pooled_variance),
        implied_intradyad_correlation = implied_rho
    )
}

bind_rows(
    rho_from_pooled_dyad_sd(counts_h1a, "HH", "LH"),
    rho_from_pooled_dyad_sd(counts_h1a, "HH", "LL"),
    rho_from_pooled_dyad_sd(counts_h1a, "LH", "LL")
) |>
    round_table() |>
    kable(
        col.names = c(
            "Comparison",
            "Pooled dyad-level SD",
            "Implied intradyad correlation"
        ),
        caption = paste(
            "Intradyad correlations implied by observed pooled dyad-level",
            "standard deviations"
        )
    )
Intradyad correlations implied by observed pooled dyad-level standard deviations
Comparison Pooled dyad-level SD Implied intradyad correlation
HH vs LH 0.290 0.302
HH vs LL 0.300 0.326
LH vs LL 0.278 0.274
Show the R code
dyad_mean_variance <- function(probability, rho, choices_per_dyad = 20) {
    probability * (1 - probability) / choices_per_dyad *
        (1 + (choices_per_dyad - 1) * rho)
}

registered_power_for_rho <- function(rho, sig_level) {
    reference_variance <- dyad_mean_variance(
        reference_probability, rho, choices_per_dyad
    )
    comparison_variance <- dyad_mean_variance(
        comparison_probability, rho, choices_per_dyad
    )
    pooled_dyad_sd <- sqrt((reference_variance + comparison_variance) / 2)

    power.t.test(
        n = planned_dyads_per_condition,
        delta = registered_difference,
        sd = pooled_dyad_sd,
        sig.level = sig_level,
        type = "two.sample",
        alternative = "one.sided"
    )$power
}

rho_grid <- c(.10, .20, .30, .40, .50)

registered_power_sensitivity <- tibble(
    intradyad_correlation = rho_grid,
    power_one_sided = vapply(
        rho_grid, registered_power_for_rho, numeric(1), sig_level = .05
    ),
    power_bonferroni = vapply(
        rho_grid, registered_power_for_rho, numeric(1), sig_level = .05 / 3
    )
)

registered_power_sensitivity |>
    mutate(across(where(is.numeric), ~ round(.x, 3))) |>
    kable(
        caption = paste(
            "Dyad-aware reconstruction of the registered calculation:",
            "39 dyads per condition and a 19.5-percentage-point contrast"
        )
    )
Dyad-aware reconstruction of the registered calculation: 39 dyads per condition and a 19.5-percentage-point contrast
intradyad_correlation power_one_sided power_bonferroni
0.1 0.999 0.994
0.2 0.976 0.929
0.3 0.922 0.820
0.4 0.855 0.712
0.5 0.789 0.619
Show the R code
rho_threshold <- function(sig_level) {
    uniroot(
        function(rho) registered_power_for_rho(rho, sig_level) - .80,
        interval = c(0, .99)
    )$root
}

tibble(
    comparison = c(
        "Single directional comparison",
        "Bonferroni benchmark for three comparisons"
    ),
    sig_level = c(.05, .05 / 3),
    maximum_rho_for_80_percent_power = c(
        rho_threshold(.05),
        rho_threshold(.05 / 3)
    )
) |>
    mutate(across(where(is.numeric), ~ round(.x, 3))) |>
    kable(
        caption = "Maximum intradyad correlation consistent with 80% power"
    )
Maximum intradyad correlation consistent with 80% power
comparison sig_level maximum_rho_for_80_percent_power
Single directional comparison 0.050 0.482
Bonferroni benchmark for three comparisons 0.017 0.318

8.2 Post-data dyad-level sensitivity analysis

The realised-sample MDE is calculated from dyad-level cooperation rates, using 38 dyads per condition and 80% power. Results are reported for one-sided () and the Bonferroni benchmark (/3). Cohen’s (d) uses the pooled dyad-level standard deviation; Cohen’s (h) assumes a .50 reference cooperation probability.

Show the R code
dyad_rates_sensitivity <- counts_h1a |>
    ungroup() |>
    select(type_match, pair_id, cooperation_rate)

mde_for_contrast <- function(data, condition_1, condition_2,
                             sig_level, target_power = .80) {
    contrast_data <- data |>
        filter(type_match %in% c(condition_1, condition_2))

    x <- contrast_data |>
        filter(type_match == condition_1) |>
        pull(cooperation_rate)

    y <- contrast_data |>
        filter(type_match == condition_2) |>
        pull(cooperation_rate)

    if (length(x) != length(y)) {
        stop("The MDE calculation assumes equal numbers of dyads per condition.")
    }

    pooled_sd <- sqrt(
        ((length(x) - 1) * var(x) + (length(y) - 1) * var(y)) /
            (length(x) + length(y) - 2)
    )

    mde <- power.t.test(
        n = length(x),
        power = target_power,
        sd = pooled_sd,
        sig.level = sig_level,
        type = "two.sample",
        alternative = "one.sided"
    )$delta

    cohen_h_at_p50 <- 2 * asin(sqrt(.50 + mde)) -
        2 * asin(sqrt(.50))

    tibble(
        contrast = paste(condition_1, "vs", condition_2),
        dyads_per_condition = length(x),
        pooled_dyad_sd = pooled_sd,
        mde = mde,
        mde_percentage_points = 100 * mde,
        cohen_d = mde / pooled_sd,
        cohen_h_at_p50 = cohen_h_at_p50
    )
}

contrasts_h1a <- list(
    c("HH", "LH"),
    c("HH", "LL"),
    c("LH", "LL")
)

mde_unadjusted <- bind_rows(lapply(
    contrasts_h1a,
    \(x) mde_for_contrast(
        dyad_rates_sensitivity, x[1], x[2], sig_level = .05
    )
)) |>
    select(contrast, dyads_per_condition, pooled_dyad_sd,
           mde_percentage_points, cohen_d, cohen_h_at_p50) |>
    rename(
        `MDE (percentage points)` = mde_percentage_points,
        `Cohen's d` = cohen_d,
        `Cohen's h (at p = .50)` = cohen_h_at_p50
    )

mde_bonferroni <- bind_rows(lapply(
    contrasts_h1a,
    \(x) mde_for_contrast(
        dyad_rates_sensitivity, x[1], x[2], sig_level = .05 / length(contrasts_h1a)
    )
)) |>
    select(contrast, mde_percentage_points, cohen_d, cohen_h_at_p50) |>
    rename(
        `MDE, Bonferroni (percentage points)` = mde_percentage_points,
        `Cohen's d, Bonferroni` = cohen_d,
        `Cohen's h, Bonferroni (at p = .50)` = cohen_h_at_p50
    )

left_join(mde_unadjusted, mde_bonferroni, by = "contrast") |>
    mutate(across(where(is.numeric), ~ round(.x, 3))) |>
    kable(
        caption = paste(
            "Post-data dyad-level sensitivity analysis for Hypothesis 1a:",
            "80% power; one-sided alpha = .05; Bonferroni alpha = .05/3"
        )
    )
Post-data dyad-level sensitivity analysis for Hypothesis 1a: 80% power; one-sided alpha = .05; Bonferroni alpha = .05/3
contrast dyads_per_condition pooled_dyad_sd MDE (percentage points) Cohen’s d Cohen’s h (at p = .50) MDE, Bonferroni (percentage points) Cohen’s d, Bonferroni Cohen’s h, Bonferroni (at p = .50)
HH vs LH 38 0.290 16.693 0.576 0.340 20.060 0.692 0.413
HH vs LL 38 0.300 17.255 0.576 0.352 20.735 0.692 0.428
LH vs LL 38 0.278 16.024 0.576 0.326 19.256 0.692 0.395

9 Variable description appendix

This appendix documents the fields in each downloadable input file and the derived variables used in the analyses. DB.csv is the primary analysis file: each row is one participant-round observation. DB_time.csv contains one page-completion record per row.

Show the R code
panas_items <- c(
    "interested", "distressed", "excited", "upset", "strong", "guilty",
    "scared", "hostile", "enthusiastic", "proud", "irritable", "alert",
    "ashamed", "inspired", "nervous", "determined", "attentive",
    "jittery", "active", "afraid"
)

db_field_descriptions <- tibble::tribble(
    ~variable, ~category, ~description,
    "participant.code", "Identifier", "oTree participant identifier.",
    "participant.label", "Identifier", "Pseudonymous participant label used to link study stages.",
    "session.code", "Identifier", "oTree session identifier.",
    "participant.time_started_utc", "Participation metadata", "UTC timestamp at which the participant started the session.",
    "participant.payoff", "Payment", "Participant-level payoff field in the oTree export.",
    "player.payoff", "Payment", "Player-level payoff accumulated in the oTree export; at round 10 this is the variable component used in the payment summary.",
    "player.code", "Identifier", "Player code stored in the Day-2 application.",
    "player.score", "Emotional intelligence", "MSCEIT overall score imported from Day 1.",
    "player.EI", "Experimental design", "Within-session EI classification: low or high.",
    "player.belief", "Repeated-game belief", "Reported probability, from 0 to 100, that the partner chooses A in the current round.",
    "player.choice", "Repeated-game choice", "Participant's current choice: A (cooperation) or B (defection).",
    "player.other_choice", "Repeated-game outcome", "Partner's realised choice in the current round.",
    "player.payoff_game", "Repeated-game outcome", "Payoff from the current Prisoner's Dilemma round.",
    "player.belief_own_type", "EI-type belief", "Reported probability, from 0 to 100, that the participant belongs to the high-EI half.",
    "player.belief_other_type", "EI-type belief", "Reported probability, from 0 to 100, that the partner belongs to the high-EI half.",
    "player.own_type_EI", "EI-type belief", "Realised own EI classification shown in the export.",
    "player.other_type_EI", "EI-type belief", "Realised partner EI classification shown in the export.",
    "player.age", "Demographics", "Self-reported age in years.",
    "player.gender", "Demographics", "Self-reported gender category.",
    "player.high_school_final_mark", "Demographics", "Raw self-reported final high-school mark; values outside the admissible 60--100 range are retained in DB.csv but treated as missing in the derived analysis variable.",
    "player.degree_course", "Demographics", "Self-reported degree course.",
    "player.cond_A", "Post-game questionnaire", "Hypothetical own choice if the partner had chosen A.",
    "player.cond_B", "Post-game questionnaire", "Hypothetical own choice if the partner had chosen B.",
    "player.other_behavior", "Post-game questionnaire", "Categorical assessment of the partner's overall behaviour.",
    "player.own_behavior", "Post-game questionnaire", "Categorical assessment of own overall behaviour.",
    "group.id_in_subsession", "Identifiers", "oTree group identifier within the round-specific subsession.",
    "subsession.round_number", "Repeated-game design", "Prisoner's Dilemma round number, from 1 to 10.",
    "player.points.Raven", "Cognitive ability", "Number of correct responses in the Raven-based MATRICES task; labelled MATRICES score in this replication file.",
    "player.eis_score", "Emotional intelligence", "SSEIT total score imported from Day 1; missing for 6 participants (N = 222 with observed scores).",
    "type_match", "Experimental design", "Server-assigned EI matching condition: high_high, low_high, or low_low.",
    "group.id", "Identifiers", "Stable dyad identifier reused across all ten rounds."
)

panas_field_descriptions <- tibble(
    variable = paste0("player.", panas_items),
    category = "Current affect (PANAS)",
    description = paste0(
        "PANAS item response for feeling '", panas_items,
        "' at the time of reporting (five-point response scale)."
    )
)

db_field_descriptions <- bind_rows(
    db_field_descriptions,
    panas_field_descriptions
)

unmapped_db_fields <- setdiff(names(raw), db_field_descriptions$variable)
if (length(unmapped_db_fields) > 0) {
    stop("Missing DB.csv codebook entries: ", paste(unmapped_db_fields, collapse = ", "))
}

db_codebook <- tibble(
    variable = names(raw),
    stored_type = purrr::map_chr(raw, ~ class(.x)[1])
) |>
    left_join(db_field_descriptions, by = "variable") |>
    select(variable, stored_type, category, description)

time_field_descriptions <- tibble::tribble(
    ~variable, ~category, ~description,
    "participant.code", "Identifier", "oTree participant identifier.",
    "participant.label", "Identifier", "Pseudonymous participant label.",
    "session.code", "Identifier", "oTree session identifier.",
    "session_code_time", "Identifier", "Session identifier recorded in the PageTimes export.",
    "participant_id_in_session", "Identifier", "Numeric participant position within the session.",
    "page_index", "Page metadata", "Sequential index of the recorded page event.",
    "app_name", "Page metadata", "oTree application containing the page.",
    "page_name", "Page metadata", "oTree page name.",
    "round_number", "Page metadata", "Round number associated with the page event, where applicable.",
    "epoch_time_completed", "Timing", "Unix timestamp when the page was completed.",
    "previous_epoch_time_completed", "Timing", "Unix timestamp of the preceding recorded page completion.",
    "time_on_page_sec", "Timing", "Recorded seconds spent on the page.",
    "timeout_happened", "Page metadata", "Indicator that the page timed out.",
    "is_wait_page", "Page metadata", "Indicator that the page was an oTree wait page.",
    "source_file", "Provenance", "Original PageTimes CSV from which the record was assembled."
)

day2_time_codebook <- tibble(
    variable = names(day2_time),
    stored_type = purrr::map_chr(day2_time, ~ class(.x)[1])
) |>
    left_join(time_field_descriptions, by = "variable") |>
    select(variable, stored_type, category, description)

derived_field_descriptions <- tibble::tribble(
    ~variable, ~description,
    "participant_id", "Pseudonymous participant identifier.",
    "session_id", "Session identifier.",
    "pair_id", "Stable dyad identifier.",
    "round", "Repeated-game round number, from 1 to 10.",
    "player_type", "Own EI classification, recoded as Low or High.",
    "type_match", "Dyad EI matching condition, recoded as HH, LH, or LL.",
    "choice", "Raw current-round choice, A or B.",
    "cooperate", "Binary cooperation outcome: 1 for Choice A and 0 for Choice B.",
    "belief_partner_coop", "Reported probability that the partner chooses A in the current round.",
    "msceit_score", "MSCEIT overall score.",
    "eis_score", "SSEIT total score imported from Day 1; missing for 6 participants (N = 222 with observed scores).",
    "matrices_score", "MATRICES score: number of correct responses in the Raven-based cognitive task.",
    "panas_positive", "Sum of the ten positive PANAS items.",
    "panas_negative", "Sum of the ten negative PANAS items.",
    "age", "Self-reported age in years.",
    "gender", "Self-reported gender category.",
    "high_school_mark", "Self-reported final high-school mark, with values outside the admissible 60--100 range recoded as missing.",
    "own_type_belief", "Reported probability of own high-EI classification.",
    "partner_type_belief", "Reported probability of partner high-EI classification.",
    "partner_type_actual", "Partner's realised EI classification.",
    "late_round", "Indicator equal to 1 in rounds 6--10 and 0 in rounds 1--5.",
    "late_period", "Early (rounds 1--5) or Late (rounds 6--10) period factor."
)

unmapped_derived_fields <- setdiff(names(d), derived_field_descriptions$variable)
if (length(unmapped_derived_fields) > 0) {
    stop("Missing derived-data codebook entries: ", paste(unmapped_derived_fields, collapse = ", "))
}

derived_codebook <- tibble(
    variable = names(d),
    stored_type = purrr::map_chr(d, ~ class(.x)[1])
) |>
    left_join(derived_field_descriptions, by = "variable") |>
    select(variable, stored_type, description)

tibble(
    Dataset = c("DB.csv", "DB_time.csv", "Derived analysis data `d`"),
    Rows = c(nrow(raw), nrow(day2_time), nrow(d)),
    Columns = c(ncol(raw), ncol(day2_time), ncol(d)),
    Unit = c("Participant-round", "Page completion", "Participant-round")
) |>
    kable(caption = "Overview of replication datasets")
Overview of replication datasets
Dataset Rows Columns Unit
DB.csv 2280 51 Participant-round
DB_time.csv 22570 15 Page completion
Derived analysis data d 2280 22 Participant-round
Show the R code
db_codebook |>
    rename(
        Variable = variable,
        `Stored type` = stored_type,
        Category = category,
        Description = description
    ) |>
    kable(caption = "Codebook for DB.csv")
Codebook for DB.csv
Variable Stored type Category Description
participant.code character Identifier oTree participant identifier.
participant.label character Identifier Pseudonymous participant label used to link study stages.
session.code character Identifier oTree session identifier.
participant.time_started_utc POSIXct Participation metadata UTC timestamp at which the participant started the session.
participant.payoff numeric Payment Participant-level payoff field in the oTree export.
player.payoff numeric Payment Player-level payoff accumulated in the oTree export; at round 10 this is the variable component used in the payment summary.
player.code character Identifier Player code stored in the Day-2 application.
player.score numeric Emotional intelligence MSCEIT overall score imported from Day 1.
player.EI character Experimental design Within-session EI classification: low or high.
player.belief numeric Repeated-game belief Reported probability, from 0 to 100, that the partner chooses A in the current round.
player.choice character Repeated-game choice Participant’s current choice: A (cooperation) or B (defection).
player.other_choice character Repeated-game outcome Partner’s realised choice in the current round.
player.payoff_game numeric Repeated-game outcome Payoff from the current Prisoner’s Dilemma round.
player.interested numeric Current affect (PANAS) PANAS item response for feeling ‘interested’ at the time of reporting (five-point response scale).
player.distressed numeric Current affect (PANAS) PANAS item response for feeling ‘distressed’ at the time of reporting (five-point response scale).
player.excited numeric Current affect (PANAS) PANAS item response for feeling ‘excited’ at the time of reporting (five-point response scale).
player.upset numeric Current affect (PANAS) PANAS item response for feeling ‘upset’ at the time of reporting (five-point response scale).
player.strong numeric Current affect (PANAS) PANAS item response for feeling ‘strong’ at the time of reporting (five-point response scale).
player.guilty numeric Current affect (PANAS) PANAS item response for feeling ‘guilty’ at the time of reporting (five-point response scale).
player.scared numeric Current affect (PANAS) PANAS item response for feeling ‘scared’ at the time of reporting (five-point response scale).
player.hostile numeric Current affect (PANAS) PANAS item response for feeling ‘hostile’ at the time of reporting (five-point response scale).
player.enthusiastic numeric Current affect (PANAS) PANAS item response for feeling ‘enthusiastic’ at the time of reporting (five-point response scale).
player.proud numeric Current affect (PANAS) PANAS item response for feeling ‘proud’ at the time of reporting (five-point response scale).
player.irritable numeric Current affect (PANAS) PANAS item response for feeling ‘irritable’ at the time of reporting (five-point response scale).
player.alert numeric Current affect (PANAS) PANAS item response for feeling ‘alert’ at the time of reporting (five-point response scale).
player.ashamed numeric Current affect (PANAS) PANAS item response for feeling ‘ashamed’ at the time of reporting (five-point response scale).
player.inspired numeric Current affect (PANAS) PANAS item response for feeling ‘inspired’ at the time of reporting (five-point response scale).
player.nervous numeric Current affect (PANAS) PANAS item response for feeling ‘nervous’ at the time of reporting (five-point response scale).
player.determined numeric Current affect (PANAS) PANAS item response for feeling ‘determined’ at the time of reporting (five-point response scale).
player.attentive numeric Current affect (PANAS) PANAS item response for feeling ‘attentive’ at the time of reporting (five-point response scale).
player.jittery numeric Current affect (PANAS) PANAS item response for feeling ‘jittery’ at the time of reporting (five-point response scale).
player.active numeric Current affect (PANAS) PANAS item response for feeling ‘active’ at the time of reporting (five-point response scale).
player.afraid numeric Current affect (PANAS) PANAS item response for feeling ‘afraid’ at the time of reporting (five-point response scale).
player.belief_own_type numeric EI-type belief Reported probability, from 0 to 100, that the participant belongs to the high-EI half.
player.belief_other_type numeric EI-type belief Reported probability, from 0 to 100, that the partner belongs to the high-EI half.
player.own_type_EI character EI-type belief Realised own EI classification shown in the export.
player.other_type_EI character EI-type belief Realised partner EI classification shown in the export.
player.age numeric Demographics Self-reported age in years.
player.gender character Demographics Self-reported gender category.
player.high_school_final_mark numeric Demographics Raw self-reported final high-school mark; values outside the admissible 60–100 range are retained in DB.csv but treated as missing in the derived analysis variable.
player.degree_course character Demographics Self-reported degree course.
player.cond_A character Post-game questionnaire Hypothetical own choice if the partner had chosen A.
player.cond_B character Post-game questionnaire Hypothetical own choice if the partner had chosen B.
player.other_behavior character Post-game questionnaire Categorical assessment of the partner’s overall behaviour.
player.own_behavior character Post-game questionnaire Categorical assessment of own overall behaviour.
group.id_in_subsession numeric Identifiers oTree group identifier within the round-specific subsession.
subsession.round_number numeric Repeated-game design Prisoner’s Dilemma round number, from 1 to 10.
player.points.Raven numeric Cognitive ability Number of correct responses in the Raven-based MATRICES task; labelled MATRICES score in this replication file.
player.eis_score numeric Emotional intelligence SSEIT total score imported from Day 1; missing for 6 participants (N = 222 with observed scores).
type_match character Experimental design Server-assigned EI matching condition: high_high, low_high, or low_low.
group.id character Identifiers Stable dyad identifier reused across all ten rounds.
Show the R code
day2_time_codebook |>
    rename(
        Variable = variable,
        `Stored type` = stored_type,
        Category = category,
        Description = description
    ) |>
    kable(caption = "Codebook for DB_time.csv")
Codebook for DB_time.csv
Variable Stored type Category Description
participant.code character Identifier oTree participant identifier.
participant.label character Identifier Pseudonymous participant label.
session.code character Identifier oTree session identifier.
session_code_time character Identifier Session identifier recorded in the PageTimes export.
participant_id_in_session numeric Identifier Numeric participant position within the session.
page_index numeric Page metadata Sequential index of the recorded page event.
app_name character Page metadata oTree application containing the page.
page_name character Page metadata oTree page name.
round_number numeric Page metadata Round number associated with the page event, where applicable.
epoch_time_completed numeric Timing Unix timestamp when the page was completed.
previous_epoch_time_completed numeric Timing Unix timestamp of the preceding recorded page completion.
time_on_page_sec numeric Timing Recorded seconds spent on the page.
timeout_happened numeric Page metadata Indicator that the page timed out.
is_wait_page numeric Page metadata Indicator that the page was an oTree wait page.
source_file character Provenance Original PageTimes CSV from which the record was assembled.
Show the R code
derived_codebook |>
    rename(
        Variable = variable,
        `Stored type` = stored_type,
        Description = description
    ) |>
    kable(caption = "Codebook for the derived analysis dataset")
Codebook for the derived analysis dataset
Variable Stored type Description
type_match factor Dyad EI matching condition, recoded as HH, LH, or LL.
participant_id character Pseudonymous participant identifier.
session_id character Session identifier.
pair_id character Stable dyad identifier.
round numeric Repeated-game round number, from 1 to 10.
player_type factor Own EI classification, recoded as Low or High.
choice character Raw current-round choice, A or B.
cooperate integer Binary cooperation outcome: 1 for Choice A and 0 for Choice B.
belief_partner_coop numeric Reported probability that the partner chooses A in the current round.
msceit_score numeric MSCEIT overall score.
eis_score numeric SSEIT total score imported from Day 1; missing for 6 participants (N = 222 with observed scores).
matrices_score numeric MATRICES score: number of correct responses in the Raven-based cognitive task.
panas_positive numeric Sum of the ten positive PANAS items.
panas_negative numeric Sum of the ten negative PANAS items.
age numeric Self-reported age in years.
gender factor Self-reported gender category.
high_school_mark numeric Self-reported final high-school mark, with values outside the admissible 60–100 range recoded as missing.
own_type_belief numeric Reported probability of own high-EI classification.
partner_type_belief numeric Reported probability of partner high-EI classification.
partner_type_actual factor Partner’s realised EI classification.
late_round integer Indicator equal to 1 in rounds 6–10 and 0 in rounds 1–5.
late_period factor Early (rounds 1–5) or Late (rounds 6–10) period factor.

10 R session information

The following records the R version, platform, attached packages, and loaded namespaces at the end of this render, including their versions.

Show the R code
sessionInfo()
R version 4.5.1 (2025-06-13)
Platform: aarch64-apple-darwin20
Running under: macOS 27.0.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8

time zone: Europe/Rome
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] lmerTest_3.2-0  lme4_1.1-37     Matrix_1.7-3    broom_1.0.12   
 [5] knitr_1.50      lubridate_1.9.4 forcats_1.0.0   stringr_1.5.2  
 [9] dplyr_1.2.0     purrr_1.1.0     readr_2.1.5     tidyr_1.3.1    
[13] tibble_3.3.1    ggplot2_4.0.0   tidyverse_2.0.0

loaded via a namespace (and not attached):
 [1] gtable_0.3.6        xfun_0.55           htmlwidgets_1.6.4  
 [4] lattice_0.22-7      tzdb_0.5.0          numDeriv_2016.8-1.1
 [7] vctrs_0.7.2         tools_4.5.1         Rdpack_2.6.4       
[10] generics_0.1.4      parallel_4.5.1      pkgconfig_2.0.3    
[13] RColorBrewer_1.1-3  S7_0.2.0            lifecycle_1.0.5    
[16] compiler_4.5.1      farver_2.1.2        textshaping_1.0.3  
[19] carData_3.0-5       htmltools_0.5.8.1   yaml_2.3.10        
[22] Formula_1.2-5       car_3.1-5           pillar_1.11.1      
[25] nloptr_2.2.1        crayon_1.5.3        MASS_7.3-65        
[28] reformulas_0.4.1    abind_1.4-8         boot_1.3-31        
[31] nlme_3.1-168        mime_0.13           tidyselect_1.2.1   
[34] digest_0.6.37       stringi_1.8.7       labeling_0.4.3     
[37] splines_4.5.1       fastmap_1.2.0       grid_4.5.1         
[40] cli_3.6.5           magrittr_2.0.4      withr_3.0.2        
[43] scales_1.4.0        backports_1.5.0     bit64_4.6.0-1      
[46] timechange_0.3.0    rmarkdown_2.29      bit_4.6.0          
[49] ragg_1.5.0          hms_1.1.3           evaluate_1.0.5     
[52] rbibutils_2.3       rlang_1.1.7         Rcpp_1.1.0         
[55] glue_1.8.0          vroom_1.6.6         minqa_1.2.8        
[58] jsonlite_2.0.0      R6_2.6.1            systemfonts_1.3.1