Sample Demographics

Demographic and Clinical Characteristics by Group (Table 1)

Author

Jeffrey Girard

Published

October 7, 2026

Note

This report reads participant-level demographic and clinical records that are not publicly shared, so it cannot be re-rendered from this repository. Only aggregate results are shown. The model covariates (age, sex, race, and PTSD status) are included in the shared feature data.

Setup

Code
library(tidyverse)
library(gt)

Data

Code
# Restricted REDCap exports (not shared)
redcap_dir <- Sys.getenv("CMBA_REDCAP_DIR", r"(Z:\CMBA_PTSD\1_RAWDATA\redCAP)")

# Restricted key linking study record IDs to the participant codes in the
# shared data (not shared); it lists exactly the participants in the analyses
analyzed_ids <- read_csv(
  Sys.getenv("CMBA_KEY_FILE", r"(X:\CMBA_PTSD\Journal2026\participant_key_PRIVATE.csv)"),
  col_types = cols(.default = col_character())
)$original_id

redcap_vars <- c(
  "record_id", "race", "ethnicity", "sex", "age", "income", "education",
  "phq_total", "pcl5_m_total", "caps_mnth_total", "caps_mnth_ptsd",
  "suds_account", "suds_impact"
)

read_redcap <- function(file, group) {
  read_csv(
    file.path(redcap_dir, file),
    col_types = cols(.default = col_character())
  ) |>
    select(all_of(redcap_vars)) |>
    summarize(
      .by = record_id,
      group = group,
      across(everything(), \(x) first(x, na_rm = TRUE))
    )
}

demo_all <-
  bind_rows(
    read_redcap("CMBA-Patients_2022-12-05_1438.csv", "clinic"),
    read_redcap("CMBA-Control_2023-03-10_1522.csv", "prolific")
  ) |>
  # participants included in the analyses
  filter(record_id %in% analyzed_ids) |>
  transmute(
    record_id,
    GRP = factor(group),
    Race = factor(
      as.numeric(race),
      levels = 1:7,
      labels = c(
        "American Indian",
        "Asian",
        "Black",
        "Hispanic",
        "More than one race",
        "White",
        "Unknown"
      )
    ),
    Ethnicity = factor(
      as.numeric(ethnicity),
      levels = 1:3,
      labels = c(
        "Not Hispanic or Latino",
        "Hispanic or Latino",
        "Unknown"
      )
    ),
    Sex = factor(
      as.numeric(sex),
      levels = 1:6,
      labels = c(
        "Male",
        "Female",
        "Intersex",
        "Transgender (sex assigned at birth female, now male)",
        "Transgender (sex assigned at birth male, now female)",
        "Prefer not to say"
      )
    ),
    Income = factor(
      as.numeric(income),
      levels = 1:7,
      labels = c(
        "< $10,000",
        "$10,000-19,999",
        "$20,000-39,999",
        "$40,000-79,999",
        "$80,000-169,999",
        "$170,000+",
        "Prefer not to say"
      )
    ),
    Age = as.numeric(age),
    Education = as.numeric(education),
    PHQ9 = parse_number(phq_total),
    PCL5 = parse_number(pcl5_m_total),
    CAPS5 = parse_number(caps_mnth_total),
    PTSD = factor(caps_mnth_ptsd, levels = 1:0, labels = c("PTSD", "Control")),
    SUDS_Account = parse_number(suds_account),
    SUDS_Impact = parse_number(suds_impact)
  ) |>
  # one participant selected "Hispanic" as their race; they are reported as
  # such here and coded as nonwhite in the analyses
  mutate(across(Race:Sex, \(x) fct_na_value_to_level(x, "Unknown")))

stopifnot(nrow(demo_all) == 92)

Sample

Code
n_group <- table(demo_all$PTSD)

demo_all |>
  count(GRP, PTSD) |>
  pivot_wider(names_from = PTSD, values_from = n, values_fill = 0) |>
  mutate(
    GRP = str_to_title(GRP),
    Total = PTSD + Control
  ) |>
  gt(rowname_col = "GRP") |>
  tab_stubhead(label = "Recruitment source") |>
  tab_spanner(label = "Diagnostic group (CAPS-5)", columns = c(PTSD, Control)) |>
  grand_summary_rows(columns = c(PTSD, Control, Total), fns = list(Total ~ sum(.)), fmt = ~ fmt_integer(.)) |>
  cols_align(align = "center", columns = c(PTSD, Control, Total))
Recruitment source
Diagnostic group (CAPS-5)
Total
PTSD Control
Clinic 48 4 52
Prolific 0 40 40
Total 48 44 92

Group Comparisons

Code
# Formatting helpers (APA style: no leading zero for p and bounded effect sizes)
fmt_p <- function(p) {
  if_else(p < .001, "< .001", sub("^0", "", sprintf("%.3f", p)))
}
fmt_es <- function(x) {
  sub("^(-?)0", "\\1", sprintf("%.2f", x)) |> sub(pattern = "^-", replacement = "−")
}

# Mann-Whitney U test with rank-biserial correlation (positive = higher in PTSD group)
mwu_test <- function(data, var) {
  data <- filter(data, !is.na(.data[[var]]))
  x <- data[[var]][data$PTSD == "PTSD"]
  y <- data[[var]][data$PTSD == "Control"]
  w <- wilcox.test(x, y, exact = FALSE)
  tibble(
    test = "Mann-Whitney U",
    statistic = sprintf("U = %.1f", w$statistic),
    p = w$p.value,
    es_name = "r",
    es = unname(2 * w$statistic / (length(x) * length(y)) - 1),
    n = length(x) + length(y)
  )
}

# Chi-square or Fisher's exact test with Cramér's V
nominal_test <- function(data, var, exact) {
  tab <- table(droplevels(data[[var]]), data$PTSD)
  chi <- suppressWarnings(chisq.test(tab, correct = FALSE))
  v <- sqrt(unname(chi$statistic) / (sum(tab) * (min(dim(tab)) - 1)))
  result <- if (exact) {
    tibble(test = "Fisher's exact", statistic = "", p = fisher.test(tab)$p.value)
  } else {
    tibble(
      test = "Chi-square",
      statistic = sprintf("χ²(%d) = %.2f", chi$parameter, chi$statistic),
      p = chi$p.value
    )
  }
  mutate(result, es_name = "V", es = v, n = sum(tab), min_expected = min(chi$expected))
}

comparisons <- bind_rows(
  # Expected counts are all >= 5 for sex
  nominal_test(demo_all, "Sex", exact = FALSE) |> mutate(variable = "Sex"),
  # Some expected counts are < 5 for race and ethnicity
  nominal_test(demo_all, "Race", exact = TRUE) |> mutate(variable = "Race"),
  nominal_test(filter(demo_all, Ethnicity != "Unknown"), "Ethnicity", exact = TRUE) |>
    mutate(variable = "Ethnicity"),
  # Income is ordinal, so compare the ordered income brackets
  demo_all |>
    filter(Income != "Prefer not to say") |>
    mutate(Income_rank = as.integer(droplevels(Income))) |>
    mwu_test("Income_rank") |>
    mutate(variable = "Income"),
  mwu_test(demo_all, "Age") |> mutate(variable = "Age (years)"),
  mwu_test(demo_all, "Education") |> mutate(variable = "Education (years)"),
  mwu_test(demo_all, "SUDS_Account") |> mutate(variable = "SUDS, Account"),
  mwu_test(demo_all, "SUDS_Impact") |> mutate(variable = "SUDS, Impact"),
  mwu_test(demo_all, "PCL5") |> mutate(variable = "PCL-5")
  # CAPS-5 is not tested because the groups were defined by the CAPS-5 diagnosis
) |>
  relocate(variable)

comparisons |>
  gt(rowname_col = "variable") |>
  fmt(columns = es, fns = fmt_es) |>
  fmt_number(columns = min_expected, decimals = 1) |>
  fmt(columns = p, fns = fmt_p) |>
  sub_missing(missing_text = "") |>
  cols_merge(columns = c(es_name, es), pattern = "{1} = {2}") |>
  cols_label(
    test = "Test",
    statistic = "Statistic",
    p = md("*p*"),
    es_name = "Effect size",
    n = md("*n*"),
    min_expected = "Min. expected count"
  ) |>
  cols_align(align = "center", columns = -c(variable, test)) |>
  tab_source_note(md(paste(
    sprintf(
      "Ethnicity excludes \"Unknown\" (n = %d) and income excludes \"Prefer not to say\" (n = %d).",
      sum(demo_all$Ethnicity == "Unknown"), sum(demo_all$Income == "Prefer not to say")
    ),
    "*V* = Cramér's *V*; *r* = rank-biserial correlation, positive when values are higher in the PTSD group.",
    "Income was compared as ordered brackets.",
    "CAPS-5 was not tested because it defined the groups."
  )))
Test Statistic p Effect size n Min. expected count
Sex Chi-square χ²(1) = 20.18 < .001 V = .47 92 14.8
Race Fisher's exact .016 V = .34 92 0.5
Ethnicity Fisher's exact .484 V = .11 91 0.5
Income Mann-Whitney U U = 613.5 .013 r = −.30 84
Age (years) Mann-Whitney U U = 601.0 < .001 r = −.43 92
Education (years) Mann-Whitney U U = 605.5 < .001 r = −.43 92
SUDS, Account Mann-Whitney U U = 1696.5 < .001 r = .61 92
SUDS, Impact Mann-Whitney U U = 1708.5 < .001 r = .62 92
PCL-5 Mann-Whitney U U = 2076.5 < .001 r = .97 92
Ethnicity excludes “Unknown” (n = 1) and income excludes “Prefer not to say” (n = 8). V = Cramér’s V; r = rank-biserial correlation, positive when values are higher in the PTSD group. Income was compared as ordered brackets. CAPS-5 was not tested because it defined the groups.

Because the income test excludes participants who declined to report income, we also compared the rate of declining by group:

Code
income_nonresponse <- table(
  `Declined to report income` = demo_all$Income == "Prefer not to say",
  demo_all$PTSD
)

income_nonresponse |>
  as.data.frame.matrix() |>
  rownames_to_column("Declined to report income") |>
  mutate(`Declined to report income` = if_else(`Declined to report income` == "TRUE", "Yes", "No")) |>
  gt() |>
  cols_align(align = "center", columns = c(PTSD, Control)) |>
  tab_source_note(sprintf(
    "Fisher's exact test, p = %s",
    fmt_p(fisher.test(income_nonresponse)$p.value)
  ))
Declined to report income PTSD Control
No 41 43
Yes 7 1
Fisher's exact test, p = .061

Table 1

Code
tabulate_var <- function(var) {
  tab <- table(droplevels(demo_all[[var]]), demo_all$PTSD)
  pct <- 100 * prop.table(tab, 2)
  tibble(
    row = rownames(tab),
    c1 = as.character(tab[, "PTSD"]),
    c2 = sprintf("%.1f%%", pct[, "PTSD"]),
    c3 = as.character(tab[, "Control"]),
    c4 = sprintf("%.1f%%", pct[, "Control"]),
    type = "level"
  )
}

describe_var <- function(var, label) {
  x <- na.omit(demo_all[[var]][demo_all$PTSD == "PTSD"])
  y <- na.omit(demo_all[[var]][demo_all$PTSD == "Control"])
  tibble(
    row = label,
    c1 = sprintf("%.2f", mean(x)), c2 = sprintf("%.2f", sd(x)),
    c3 = sprintf("%.2f", mean(y)), c4 = sprintf("%.2f", sd(y)),
    type = "continuous"
  )
}

test_cols <- comparisons |>
  transmute(row = variable, p = fmt_p(p), es = paste0("*", es_name, "* = ", fmt_es(es)))

continuous_vars <- c(
  "Age" = "Age (years)",
  "Education" = "Education (years)",
  "SUDS_Account" = "SUDS, Account",
  "SUDS_Impact" = "SUDS, Impact",
  "PCL5" = "PCL-5",
  "CAPS5" = "CAPS-5"
)

table_1 <- bind_rows(
  tibble(row = "Categorical variables", c1 = "n", c2 = "%", c3 = "n", c4 = "%", type = "header"),
  map(c("Sex", "Race", "Ethnicity", "Income"), \(v) {
    bind_rows(tibble(row = v, type = "variable"), tabulate_var(v))
  }) |> list_rbind(),
  tibble(row = "Continuous variables", c1 = "M", c2 = "SD", c3 = "M", c4 = "SD", type = "header"),
  imap(continuous_vars, \(label, var) describe_var(var, label)) |> list_rbind()
) |>
  left_join(test_cols, by = "row") |>
  mutate(
    across(c(p, es), \(x) if_else(row == "CAPS-5", "—", x)),
    across(c(c1:c4, p, es), \(x) replace_na(x, ""))
  )

table_1 |>
  gt() |>
  cols_hide(type) |>
  tab_header(
    title = "Table 1",
    subtitle = "Demographic and Clinical Characteristics by Group"
  ) |>
  tab_spanner(label = sprintf("PTSD (n = %d)", n_group[["PTSD"]]), columns = c(c1, c2)) |>
  tab_spanner(label = sprintf("Control (n = %d)", n_group[["Control"]]), columns = c(c3, c4)) |>
  cols_label(row = "", c1 = "", c2 = "", c3 = "", c4 = "", p = md("*p*"), es = "Effect size") |>
  fmt_markdown(columns = es) |>
  cols_width(row ~ px(210), c(c1, c3) ~ px(55), c(c2, c4) ~ px(75), p ~ px(60), es ~ px(85)) |>
  # keep Quarto from overriding the column widths
  tab_options(quarto.disable_processing = TRUE) |>
  cols_align(align = "center", columns = c(c1:c4, p, es)) |>
  tab_style(style = cell_text(weight = "bold"), locations = cells_body(rows = type == "header")) |>
  tab_style(style = cell_text(style = "italic"), locations = cells_body(columns = c1:c4, rows = type == "header")) |>
  tab_style(style = cell_text(indent = px(12)), locations = cells_body(columns = row, rows = type %in% c("variable", "continuous"))) |>
  tab_style(style = cell_text(indent = px(28)), locations = cells_body(columns = row, rows = type == "level")) |>
  tab_source_note(md(paste(
    sprintf(
      "*Note.* The control group includes %d participants recruited through Prolific and %d recruited from the clinic who did not meet criteria for PTSD.",
      sum(demo_all$GRP == "prolific" & demo_all$PTSD == "Control"),
      sum(demo_all$GRP == "clinic" & demo_all$PTSD == "Control")
    ),
    "Percentages are column percentages within each group.",
    if (any(demo_all$Race == "Hispanic")) "One participant selected Hispanic as their race (coded as nonwhite in the analyses)." else "",
    "Sex was compared with a chi-square test, and race and ethnicity with Fisher's exact test",
    "because some expected cell counts were below 5; effect sizes are Cramér's *V*.",
    "Income (as ordered brackets) and continuous variables were compared with Mann-Whitney *U* tests;",
    "effect sizes are rank-biserial correlations (*r*), positive when values are higher in the PTSD group.",
    "Tests of ethnicity and income excluded \"Unknown\" and \"Prefer not to say\" responses, respectively.",
    "CAPS-5 was not tested because it defined the groups.",
    "SUDS = Subjective Units of Distress Scale, rated after the trauma account and impact statement tasks;",
    "PCL-5 = PTSD Checklist for DSM-5; CAPS-5 = Clinician-Administered PTSD Scale for DSM-5 total severity."
  )))
Table 1
Demographic and Clinical Characteristics by Group
PTSD (n = 48)
Control (n = 44)
p Effect size
Categorical variables n % n %
Sex < .001 V = .47
Male 6 12.5% 25 56.8%
Female 42 87.5% 19 43.2%
Race .016 V = .34
Asian 0 0.0% 3 6.8%
Black 5 10.4% 1 2.3%
Hispanic 0 0.0% 1 2.3%
More than one race 4 8.3% 0 0.0%
White 39 81.2% 39 88.6%
Ethnicity .484 V = .11
Not Hispanic or Latino 47 97.9% 43 97.7%
Hispanic or Latino 0 0.0% 1 2.3%
Unknown 1 2.1% 0 0.0%
Income .013 r = −.30
< $10,000 6 12.5% 2 4.5%
$10,000-19,999 2 4.2% 1 2.3%
$20,000-39,999 8 16.7% 5 11.4%
$40,000-79,999 13 27.1% 13 29.5%
$80,000-169,999 10 20.8% 16 36.4%
$170,000+ 2 4.2% 6 13.6%
Prefer not to say 7 14.6% 1 2.3%
Continuous variables M SD M SD
Age (years) 36.23 10.99 48.05 15.47 < .001 r = −.43
Education (years) 14.31 2.33 16.18 2.19 < .001 r = −.43
SUDS, Account 78.71 12.16 52.68 25.87 < .001 r = .61
SUDS, Impact 72.65 19.60 42.39 26.80 < .001 r = .62
PCL-5 52.60 12.92 8.20 11.68 < .001 r = .97
CAPS-5 36.54 7.30 4.20 5.80 — —
Note. The control group includes 40 participants recruited through Prolific and 4 recruited from the clinic who did not meet criteria for PTSD. Percentages are column percentages within each group. One participant selected Hispanic as their race (coded as nonwhite in the analyses). Sex was compared with a chi-square test, and race and ethnicity with Fisher’s exact test because some expected cell counts were below 5; effect sizes are Cramér’s V. Income (as ordered brackets) and continuous variables were compared with Mann-Whitney U tests; effect sizes are rank-biserial correlations (r), positive when values are higher in the PTSD group. Tests of ethnicity and income excluded “Unknown” and “Prefer not to say” responses, respectively. CAPS-5 was not tested because it defined the groups. SUDS = Subjective Units of Distress Scale, rated after the trauma account and impact statement tasks; PCL-5 = PTSD Checklist for DSM-5; CAPS-5 = Clinician-Administered PTSD Scale for DSM-5 total severity.