# ============================================================
# FFCWS: Admixture Regression Analysis
# African Ancestry Sample (Both-Black + BW Parents)
# City-Weighted, HC1 Robust SEs
# ============================================================
library(haven)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(sandwich)
library(lmtest)
## Loading required package: zoo
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
library(mice)
##
## Attaching package: 'mice'
## The following object is masked from 'package:stats':
##
## filter
## The following objects are masked from 'package:base':
##
## cbind, rbind
library(sirt)
## Warning: package 'sirt' was built under R version 4.5.3
## - sirt 4.2-133 (2025-09-27 12:57:51)
library(lavaan)
## This is lavaan 0.6-19
## lavaan is FREE software! Please report any bugs.
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.5.2
library(mediation)
## Warning: package 'mediation' was built under R version 4.5.2
## Loading required package: MASS
##
## Attaching package: 'MASS'
## The following object is masked from 'package:dplyr':
##
## select
## Loading required package: Matrix
## Loading required package: mvtnorm
## mediation: Causal Mediation Analysis
## Version: 4.5.1
library(mgcv)
## Loading required package: nlme
##
## Attaching package: 'nlme'
## The following object is masked from 'package:dplyr':
##
## collapse
## This is mgcv 1.9-3. For overview type 'help("mgcv-package")'.
##
## Attaching package: 'mgcv'
## The following object is masked from 'package:sirt':
##
## rmvn
# ==============================================================
# 1. Load Data
# ==============================================================
d <- read_sav("C:/Users/mh198/Documents/Data/FFCWS/DS0001/31622-0001-Data.sav")
PGS <- read_sav("C:/Users/mh198/Documents/Data/FFCWS//DS0007/31622-0007-Data.sav")
d <- merge(d, PGS[, c("IDNUM", "K5ADMIXA", "K5PC1A", "K5PC2A",
"K5PC3A", "K5PC4A", "K5PC5A")],
by = "IDNUM", all.x = TRUE)
# ==============================================================
# 2. African Ancestry PGS Sample
# ==============================================================
d <- d[d$K5ADMIXA %in% c(0, 1), ]
# ==============================================================
# 3. Black Parent Identification
# ==============================================================
d$father_black_nh <- d$CF1ETHRACE == 2
d$mother_black_nh <- d$CM1ETHRACE == 2
d$father_black_hisp <- (d$CF1ETHRACE == 3) & (d$F1H3 == 2)
d$mother_black_hisp <- (d$CM1ETHRACE == 3) & (d$M1H3 == 2)
d$father_black_hisp[is.na(d$father_black_hisp)] <- FALSE
d$mother_black_hisp[is.na(d$mother_black_hisp)] <- FALSE
d$father_any_black <- d$father_black_nh | d$father_black_hisp
d$mother_any_black <- d$mother_black_nh | d$mother_black_hisp
d$any_black_parent <- d$father_any_black | d$mother_any_black
# ==============================================================
# 4. Continental African Exclusion
# ==============================================================
d$mother_african_born <- (d$M1H2 == 2) & (d$M1H2B == 1)
d$father_african_born <- (d$F1H2 == 2) & (d$F1H2B == 1)
d$mother_african_born[is.na(d$mother_african_born)] <- FALSE
d$father_african_born[is.na(d$father_african_born)] <- FALSE
d$gp_african <- (d$F3H1A == 101) | (d$F3H1B == 101) |
(d$M3H1A == 101) | (d$M3H1B == 101)
d$gp_african[is.na(d$gp_african)] <- FALSE
d$african_origin <- d$mother_african_born | d$father_african_born | d$gp_african
# ==============================================================
# 5. Generation & SIRE Dummies
# ==============================================================
d$parent_fb <- (d$M1H2 == 2) | (d$F1H2 == 2)
d$parent_fb[is.na(d$parent_fb)] <- FALSE
d$gp_fb <- (d$F3H1A >= 101) | (d$F3H1B >= 101) |
(d$M3H1A >= 101) | (d$M3H1B >= 101)
d$gp_fb[is.na(d$gp_fb)] <- FALSE
d$gen_2nd <- d$parent_fb & !d$african_origin
d$gen_3rd <- (!d$parent_fb) & d$gp_fb & !d$african_origin
d$sire <- NA_character_
d$sire[d$father_any_black & d$mother_any_black] <- "BB"
d$sire[(d$father_any_black & d$CM1ETHRACE == 1) |
(d$CF1ETHRACE == 1 & d$mother_any_black)] <- "BW"
d$sire[(d$father_any_black & d$CM1ETHRACE == 3 & !d$mother_black_hisp) |
(d$CF1ETHRACE == 3 & !d$father_black_hisp & d$mother_any_black)] <- "BH"
d$sire[d$any_black_parent & is.na(d$sire)] <- "BO"
d$sire[!d$any_black_parent] <- "None"
d$sire <- factor(d$sire, levels = c("BB", "BW", "BH", "BO", "None"))
# ==============================================================
# 6. Child TVIP Exclusion Flag
# ==============================================================
d$ch_tvip3_score <- ifelse(d$CH3TVIPSTD >= 0, d$CH3TVIPSTD, NA)
d$took_tvip3 <- as.numeric(!is.na(d$ch_tvip3_score))
cat("Child TVIP at age 3:", sum(d$took_tvip3), "cases flagged for exclusion\n")
## Child TVIP at age 3: 1 cases flagged for exclusion
# ==============================================================
# 7. Demographics & SES (3-component)
# ==============================================================
d$sex <- ifelse(d$CM1BSEX %in% c(1, 2), d$CM1BSEX - 1, NA)
d$age <- ifelse(d$CH5AGEM >= 0, d$CH5AGEM, NA)
d$single <- ifelse(d$CM1MARF == 0 & d$CM1COHF == 0, 1,
ifelse(d$CM1MARF %in% c(0, 1) & d$CM1COHF %in% c(0, 1), 0, NA))
d$M_edu <- NA
for (col in c("CM5EDU", "CM4EDU", "CM3EDU", "CM2EDU", "CM1EDU")) {
d$M_edu <- ifelse(is.na(d$M_edu) & d[[col]] >= 1, d[[col]], d$M_edu)
}
d$F_edu <- NA
for (col in c("CF5EDU", "CF4EDU", "CF3EDU", "CF2EDU", "CF1EDU")) {
d$F_edu <- ifelse(is.na(d$F_edu) & d[[col]] >= 1, d[[col]], d$F_edu)
}
d$mid_edu <- rowMeans(cbind(d$F_edu, d$M_edu), na.rm = TRUE)
d$mid_edu[is.na(d$F_edu) & is.na(d$M_edu)] <- NA
d$pov_bl <- ifelse(d$CM1INPOV >= 0, d$CM1INPOV, NA)
d$pov_yr1 <- ifelse(d$CM2POVCO >= 0, d$CM2POVCO, NA)
d$pov_yr3 <- ifelse(d$CM3POVCO >= 0, d$CM3POVCO, NA)
d$pov_yr5 <- ifelse(d$CM4POVCO >= 0, d$CM4POVCO, NA)
d$pov_yr9 <- ifelse(d$CM5POVCO >= 0, d$CM5POVCO, NA)
d$ave_pov <- rowMeans(cbind(d$pov_bl, d$pov_yr1, d$pov_yr3, d$pov_yr5, d$pov_yr9), na.rm = TRUE)
d$ave_pov[is.na(d$pov_bl) & is.na(d$pov_yr1) & is.na(d$pov_yr3) & is.na(d$pov_yr5) & is.na(d$pov_yr9)] <- NA
inc_vars <- c("CM1HHINC", "CM2HHINC", "CM3HHINC", "CM4HHINC", "CM5HHINC")
inc_names <- c("inc_bl", "inc_yr1", "inc_yr3", "inc_yr5", "inc_yr9")
for (i in seq_along(inc_vars)) {
d[[inc_names[i]]] <- ifelse(d[[inc_vars[i]]] >= 0, d[[inc_vars[i]]], NA)
d[[paste0("log_", inc_names[i])]] <- log(pmax(d[[inc_names[i]]], 1))
}
d$ave_log_inc <- rowMeans(d[, paste0("log_", inc_names)], na.rm = TRUE)
d$ave_log_inc[rowSums(!is.na(d[, inc_names])) == 0] <- NA
ses_indicators <- data.frame(
mid_edu = as.vector(scale(d$mid_edu)),
ave_pov = as.vector(scale(d$ave_pov)),
ave_log_inc = as.vector(scale(d$ave_log_inc))
)
n_ses_valid <- rowSums(!is.na(ses_indicators))
set.seed(54321)
ses_imp <- mice(ses_indicators, m = 5, method = "pmm", maxit = 10, printFlag = FALSE)
ses_imputed <- complete(ses_imp, action = 1)
d$SES_raw <- rowMeans(ses_imputed, na.rm = FALSE)
d$SES_raw[n_ses_valid == 0] <- NA
d$wt <- ifelse(d$K5CITYWT > 0, d$K5CITYWT, NA)
# ==============================================================
# 8. Child Cognitive Variables: g (GAM-based residualization)
# ==============================================================
d$PPVT <- ifelse(d$CH5PPVTSS >= 0, d$CH5PPVTSS, NA)
d$WJ9 <- ifelse(d$CH5WJ9SS >= 0, d$CH5WJ9SS, NA)
d$WJ10 <- ifelse(d$CH5WJ10SS >= 0, d$CH5WJ10SS, NA)
d$DS <- ifelse(d$CH5DSSS >= 0, d$CH5DSSS, NA)
# GAM-based residualization: smooth age + parametric sex
resid_fun_gam <- function(y, data) {
fit <- gam(y ~ s(age, k = 5) + sex, data = data,
na.action = na.exclude, method = "REML")
as.vector(residuals(fit))
}
d$PPVT_r <- resid_fun_gam(d$PPVT, d)
d$WJ9_r <- resid_fun_gam(d$WJ9, d)
d$WJ10_r <- resid_fun_gam(d$WJ10, d)
d$DS_r <- resid_fun_gam(d$DS, d)
d$PPVT_z <- as.vector(scale(d$PPVT_r))
d$WJ9_z <- as.vector(scale(d$WJ9_r))
d$WJ10_z <- as.vector(scale(d$WJ10_r))
d$DS_z <- as.vector(scale(d$DS_r))
# 3-test g (DS kept for LSEM/CFA only)
cog_imp_data <- data.frame(
PPVT_z = d$PPVT_z,
WJ9_z = d$WJ9_z,
WJ10_z = d$WJ10_z
)
has_any <- rowSums(!is.na(cog_imp_data)) >= 1
set.seed(12345)
imp <- mice(cog_imp_data, m = 5, method = "norm", maxit = 10, printFlag = FALSE)
cog_imputed <- complete(imp, action = 1)
cog_for_pca <- as.matrix(cog_imputed[has_any, ])
pca_out <- prcomp(cog_for_pca, center = FALSE, scale. = FALSE)
d$g_raw <- NA
d$g_raw[has_any] <- pca_out$x[, 1]
cat("PCA loadings (3-test g):", round(pca_out$rotation[, 1], 3), "\n")
## PCA loadings (3-test g): 0.564 0.588 0.58
cat("Variance explained:", round(summary(pca_out)$importance[2, 1] * 100, 1), "%\n")
## Variance explained: 71.7 %
# ==============================================================
# 9. Parental WAIS: Conservative USB + English Filter
# ==============================================================
d$mother_wais_raw <- ifelse(d$CM3COGSC >= 0 & d$CM3COGSC <= 15, d$CM3COGSC, NA)
d$father_wais_raw <- ifelse(d$CF3COGSC >= 0 & d$CF3COGSC <= 15, d$CF3COGSC, NA)
d$mother_confirmed_fb <- as.numeric(d$M1H2B %in% c(1, 2, 3, 4, 5))
d$father_confirmed_fb <- as.numeric(d$F1H2B %in% c(1, 2, 3, 4, 5))
d$mother_confirmed_span <- as.numeric(d$CM3SPAN == 1)
d$father_confirmed_span <- as.numeric(d$CF3SPAN == 1)
d$mother_wais <- ifelse(d$mother_confirmed_fb == 1 | d$mother_confirmed_span == 1,
NA, d$mother_wais_raw)
d$father_wais <- ifelse(d$father_confirmed_fb == 1 | d$father_confirmed_span == 1,
NA, d$father_wais_raw)
cat("\nMother WAIS: total =", sum(!is.na(d$mother_wais_raw)),
", after filter =", sum(!is.na(d$mother_wais)),
", dropped =", sum(!is.na(d$mother_wais_raw)) - sum(!is.na(d$mother_wais)), "\n")
##
## Mother WAIS: total = 1536 , after filter = 1480 , dropped = 56
cat("Father WAIS: total =", sum(!is.na(d$father_wais_raw)),
", after filter =", sum(!is.na(d$father_wais)),
", dropped =", sum(!is.na(d$father_wais_raw)) - sum(!is.na(d$father_wais)), "\n")
## Father WAIS: total = 1276 , after filter = 1231 , dropped = 45
# Age-residualize
d$mother_age <- ifelse(d$CM3AGE > 0, d$CM3AGE, NA)
d$father_age <- ifelse(d$CF3AGE > 0, d$CF3AGE, NA)
m_mask <- !is.na(d$mother_wais) & !is.na(d$mother_age)
if (sum(m_mask) > 10) {
d$mother_age_c <- d$mother_age - mean(d$mother_age[m_mask])
d$mother_age_c2 <- d$mother_age_c^2
d$M_WAIS_res <- NA_real_
d$M_WAIS_res[m_mask] <- residuals(lm(mother_wais ~ mother_age_c + mother_age_c2,
data = d[m_mask, ]))
}
f_mask <- !is.na(d$father_wais) & !is.na(d$father_age)
if (sum(f_mask) > 10) {
d$father_age_c <- d$father_age - mean(d$father_age[f_mask])
d$father_age_c2 <- d$father_age_c^2
d$F_WAIS_res <- NA_real_
d$F_WAIS_res[f_mask] <- residuals(lm(father_wais ~ father_age_c + father_age_c2,
data = d[f_mask, ]))
}
d$ave_WAIS_raw <- rowMeans(cbind(d$M_WAIS_res, d$F_WAIS_res), na.rm = TRUE)
d$ave_WAIS_raw[is.na(d$M_WAIS_res) & is.na(d$F_WAIS_res)] <- NA
cat("Average WAIS (filtered): N =", sum(!is.na(d$ave_WAIS_raw)), "\n")
## Average WAIS (filtered): N = 1533
# ==============================================================
# 10. Build Subsets (after TVIP exclusion)
# ==============================================================
d <- d[d$took_tvip3 == 0, ]
cat("After TVIP exclusion: N =", nrow(d), "\n")
## After TVIP exclusion: N = 1639
d$PC1A_z <- as.vector(scale(d$K5PC1A))
d_apa <- d[d$any_black_parent & !d$african_origin, ]
d_apa$sire <- droplevels(d_apa$sire)
# Parent self-ID flags for individual WAIS models
d_apa$mother_black_sire <- as.numeric(d_apa$CM1ETHRACE == 2)
d_apa$father_black_sire <- as.numeric(d_apa$CF1ETHRACE == 2)
d_apa_noBW <- d_apa[d_apa$sire != "BW", ]
d_apa_noBW$sire <- droplevels(d_apa_noBW$sire)
d_haa <- d[d$CF1ETHRACE == 2 & d$CM1ETHRACE == 2 &
!d$african_origin & !d$parent_fb & !d$gp_fb, ]
cat("APA:", nrow(d_apa), " | APA excl BW:", nrow(d_apa_noBW), " | HAA:", nrow(d_haa), "\n")
## APA: 1588 | APA excl BW: 1516 | HAA: 1323
# ==============================================================
# 11. LSEM Analysis — Liu et al. (2025) approach
# Progressive invariance testing with bootstrap
# ==============================================================
# DS included for model identification (4 indicators, 2 df)
# Run on post-TVIP APA sample to match regression analyses
d_apa_cog <- d_apa[rowSums(!is.na(d_apa[, c("PPVT_z", "WJ9_z", "WJ10_z", "DS_z")])) >= 1, ]
grid_points <- quantile(d_apa_cog$PC1A_z,
probs = c(0.20, 0.35, 0.50, 0.65, 0.80),
na.rm = TRUE)
lsem_model <- 'g =~ PPVT_z + WJ9_z + WJ10_z + DS_z'
# ── Define invariance constraints ────────────────────────────────────────
# Metric (weak) — loadings invariant
weak_constraints <- c("g=~PPVT_z", "g=~WJ9_z", "g=~WJ10_z", "g=~DS_z")
# Scalar (strong) — loadings + intercepts invariant
strong_constraints <- c(weak_constraints,
"PPVT_z~1", "WJ9_z~1", "WJ10_z~1", "DS_z~1")
# Strict — loadings + intercepts + residual variances
strict_constraints <- c(strong_constraints,
"PPVT_z~~PPVT_z", "WJ9_z~~WJ9_z",
"WJ10_z~~WJ10_z", "DS_z~~DS_z")
# ── Configural ───────────────────────────────────────────────────────────
parallel::detectCores()
## [1] 32
lsem_config <- lsem.estimate(
data = as.data.frame(d_apa_cog),
moderator = "PC1A_z",
moderator.grid = grid_points,
lavmodel = lsem_model,
h = 1.5,
estimator = "ML",
meanstructure = TRUE,
standardized = TRUE,
est_joint = TRUE
)
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 415
## Group 2 673
## Group 3 782
## Group 4 793
## Group 5 707
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.0725269442612360
## quasi-Newton steps using NLMINB:
## objective function = 0.0725269442612360
## objective function = 0.0588563752561805
## objective function = 0.0270056466184860
## objective function = 0.0210308952064980
## objective function = 0.0162708728292977
## objective function = 0.0144724321967550
## objective function = 0.0133186275293610
## objective function = 0.0119570202883823
## objective function = 0.0110561959205937
## objective function = 0.0103766602343360
## objective function = 0.0099292582693531
## objective function = 0.0095664544279759
## objective function = 0.0092385440551466
## objective function = 0.0088935697491971
## objective function = 0.0087137702334482
## objective function = 0.0085931967573048
## objective function = 0.0085234683929610
## objective function = 0.0084667862856638
## objective function = 0.0084228674348790
## objective function = 0.0083879322704697
## objective function = 0.0083587123599363
## objective function = 0.0083324027104931
## objective function = 0.0083069817621604
## objective function = 0.0082826012564447
## objective function = 0.0082609647475788
## objective function = 0.0082456969064643
## objective function = 0.0082345497386336
## objective function = 0.0082248423976411
## objective function = 0.0082161307427895
## objective function = 0.0082088721104753
## objective function = 0.0082039966403310
## objective function = 0.0082014389211640
## objective function = 0.0081989492124753
## objective function = 0.0081966453118561
## objective function = 0.0081942421699808
## objective function = 0.0081926960503104
## objective function = 0.0081916222565688
## objective function = 0.0081908949101970
## objective function = 0.0081902528938547
## objective function = 0.0081896970215332
## objective function = 0.0081890880719255
## objective function = 0.0081886035419813
## objective function = 0.0081881598792567
## objective function = 0.0081878762016064
## objective function = 0.0081876784286030
## objective function = 0.0081875354060321
## objective function = 0.0081874283577126
## objective function = 0.0081873565508840
## objective function = 0.0081873064149768
## objective function = 0.0081872675392588
## objective function = 0.0081872377310486
## objective function = 0.0081872179803860
## objective function = 0.0081872052015698
## objective function = 0.0081871949613283
## objective function = 0.0081871859911021
## objective function = 0.0081871765635997
## objective function = 0.0081871713970859
## objective function = 0.0081871683223214
## objective function = 0.0081871668703660
## objective function = 0.0081871657129795
## objective function = 0.0081871647409269
## objective function = 0.0081871639258151
## objective function = 0.0081871633041530
## objective function = 0.0081871629658704
## objective function = 0.0081871627502336
## objective function = 0.0081871626297596
## objective function = 0.0081871625383501
## objective function = 0.0081871624631940
## objective function = 0.0081871624040176
## objective function = 0.0081871623645527
## objective function = 0.0081871623447807
## objective function = 0.0081871623315614
## objective function = 0.0081871623232390
## objective function = 0.0081871623175860
## objective function = 0.0081871623140465
## objective function = 0.0081871623114891
## objective function = 0.0081871623097353
## objective function = 0.0081871623088481
## objective function = 0.0081871623088481
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 77
## number of function evaluations [objective, gradient]: 78 78
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
summary(lsem_config)
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = 1.5,
## standardized = TRUE, est_joint = TRUE, estimator = "ML",
## meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 14:08:25.85806
## Time difference of 4.666388 secs
## Computation Time: 4.666388
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 1.5
## Bandwidth = 0.294
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.082
## 2 cfi 0.989
## 3 tli 0.967
## 4 gfi 0.992
## 5 srmr 0.020
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int lin_slo
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 2 g=~WJ9_z 2 1.315 0.086 0.078 1.211 1.436 1.413 -0.311
## 3 g=~WJ10_z 3 1.257 0.059 0.045 1.205 1.406 1.326 -0.219
## 4 g=~DS_z 4 0.877 0.030 0.023 0.859 0.959 0.910 -0.105
## 5 PPVT_z~~PPVT_z 5 0.568 0.086 0.074 0.476 0.736 0.671 -0.325
## 6 WJ9_z~~WJ9_z 6 0.322 0.023 0.014 0.301 0.386 0.347 -0.081
## 7 WJ10_z~~WJ10_z 7 0.402 0.021 0.019 0.355 0.421 0.400 0.008
## 8 DS_z~~DS_z 8 0.718 0.024 0.019 0.667 0.741 0.691 0.083
## 9 g~~g 9 0.397 0.036 0.030 0.325 0.434 0.354 0.135
## 10 PPVT_z~1 10 -0.001 0.004 0.003 -0.008 0.004 -0.001 -0.001
## 11 WJ9_z~1 11 0.001 0.003 0.002 -0.002 0.008 0.004 -0.007
## 12 WJ10_z~1 12 -0.002 0.005 0.004 -0.007 0.004 0.001 -0.008
## 13 DS_z~1 13 -0.001 0.002 0.002 -0.004 0.001 -0.001 -0.001
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.642 0.045 0.039 0.553 0.690 0.588 0.171
## 16 std__g=~WJ9_z 16 0.824 0.010 0.006 0.796 0.832 0.819 0.017
## 17 std__g=~WJ10_z 17 0.780 0.011 0.009 0.769 0.802 0.780 -0.001
## 18 std__g=~DS_z 18 0.545 0.006 0.005 0.539 0.556 0.545 0.000
## 19 std__PPVT_z~~PPVT_z 19 0.586 0.057 0.050 0.523 0.694 0.654 -0.214
## 20 std__WJ9_z~~WJ9_z 20 0.321 0.016 0.009 0.308 0.366 0.329 -0.028
## 21 std__WJ10_z~~WJ10_z 21 0.392 0.017 0.014 0.356 0.409 0.391 0.002
## 22 std__DS_z~~DS_z 22 0.702 0.006 0.006 0.691 0.710 0.703 0.000
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 24 std__PPVT_z~1 24 -0.001 0.004 0.003 -0.008 0.003 -0.001 -0.001
## 25 std__WJ9_z~1 25 0.001 0.003 0.002 -0.002 0.008 0.004 -0.007
## 26 std__WJ10_z~1 26 -0.002 0.005 0.004 -0.007 0.004 0.001 -0.008
## 27 std__DS_z~1 27 -0.001 0.002 0.002 -0.004 0.001 -0.001 0.000
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## SD_nonlin
## 1 0.000
## 2 0.026
## 3 0.014
## 4 0.013
## 5 0.012
## 6 0.010
## 7 0.021
## 8 0.009
## 9 0.005
## 10 0.004
## 11 0.003
## 12 0.004
## 13 0.002
## 14 0.000
## 15 0.006
## 16 0.009
## 17 0.011
## 18 0.006
## 19 0.008
## 20 0.014
## 21 0.017
## 22 0.006
## 23 0.000
## 24 0.003
## 25 0.003
## 26 0.004
## 27 0.002
## 28 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 419.601
## 2 0.070 0.167 679.165
## 3 0.264 0.233 789.229
## 4 0.453 0.259 800.087
## 5 0.619 0.241 713.065
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 680.229 154.315 419.601 800.087
b_config <- lsem.bootstrap(lsem_config, R = 50, n.core = 14)
## Compute LSEM for bootstrap samples:
## Warning in sqrt(w): NaNs produced
# ── Metric (weak) ────────────────────────────────────────────────────────
lsem_weak <- lsem.estimate(
data = as.data.frame(d_apa_cog),
moderator = "PC1A_z",
moderator.grid = grid_points,
lavmodel = lsem_model,
h = 1.5,
estimator = "ML",
meanstructure = TRUE,
standardized = TRUE,
par_invariant = weak_constraints
)
## Use joint estimation
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 415
## Group 2 673
## Group 3 782
## Group 4 793
## Group 5 707
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.0725269442612357
## quasi-Newton steps using NLMINB:
## objective function = 0.0725269442612357
## objective function = 0.0589336747621080
## objective function = 0.0266784670386390
## objective function = 0.0210113040431718
## objective function = 0.0165662029281500
## objective function = 0.0144912328134312
## objective function = 0.0134192340063573
## objective function = 0.0120333458203883
## objective function = 0.0111935861154176
## objective function = 0.0105554481992239
## objective function = 0.0101974403139898
## objective function = 0.0099022438237459
## objective function = 0.0096521467408002
## objective function = 0.0094300346431928
## objective function = 0.0093082557354885
## objective function = 0.0092221192245195
## objective function = 0.0091853894974883
## objective function = 0.0091683083600652
## objective function = 0.0091613016088296
## objective function = 0.0091578781532012
## objective function = 0.0091560347242648
## objective function = 0.0091549273880945
## objective function = 0.0091542755388411
## objective function = 0.0091538680637045
## objective function = 0.0091536333198369
## objective function = 0.0091535248273743
## objective function = 0.0091534830232298
## objective function = 0.0091534669100903
## objective function = 0.0091534608246759
## objective function = 0.0091534584698128
## objective function = 0.0091534571024759
## objective function = 0.0091534560292262
## objective function = 0.0091534553902843
## objective function = 0.0091534551407963
## objective function = 0.0091534550543652
## objective function = 0.0091534550114223
## objective function = 0.0091534549884046
## objective function = 0.0091534549787392
## objective function = 0.0091534549747107
## objective function = 0.0091534549725652
## objective function = 0.0091534549713624
## objective function = 0.0091534549713624
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 40
## number of function evaluations [objective, gradient]: 41 41
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
summary(lsem_weak)
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = 1.5,
## standardized = TRUE, par_invariant = weak_constraints, estimator = "ML",
## meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 14:22:05.557419
## Time difference of 6.47238 secs
## Computation Time: 6.47238
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 1.5
## Bandwidth = 0.294
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.052
## 2 cfi 0.990
## 3 tli 0.987
## 4 gfi 0.991
## 5 srmr 0.024
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int lin_slo
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 2 g=~WJ9_z 2 1.311 0.000 0.000 1.311 1.311 1.311 0.000
## 3 g=~WJ10_z 3 1.255 0.000 0.000 1.255 1.255 1.255 0.000
## 4 g=~DS_z 4 0.874 0.000 0.000 0.874 0.874 0.874 0.000
## 5 PPVT_z~~PPVT_z 5 0.569 0.079 0.067 0.487 0.723 0.663 -0.296
## 6 WJ9_z~~WJ9_z 6 0.322 0.031 0.027 0.283 0.382 0.359 -0.118
## 7 WJ10_z~~WJ10_z 7 0.402 0.014 0.012 0.370 0.413 0.397 0.017
## 8 DS_z~~DS_z 8 0.718 0.025 0.021 0.669 0.742 0.690 0.088
## 9 g~~g 9 0.396 0.003 0.003 0.388 0.399 0.394 0.006
## 10 PPVT_z~1 10 -0.001 0.004 0.003 -0.008 0.004 -0.001 -0.001
## 11 WJ9_z~1 11 0.001 0.003 0.002 -0.002 0.008 0.004 -0.007
## 12 WJ10_z~1 12 -0.002 0.005 0.004 -0.007 0.004 0.001 -0.008
## 13 DS_z~1 13 -0.001 0.002 0.002 -0.004 0.001 -0.001 -0.001
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.642 0.026 0.022 0.591 0.669 0.611 0.098
## 16 std__g=~WJ9_z 16 0.824 0.013 0.011 0.797 0.840 0.808 0.050
## 17 std__g=~WJ10_z 17 0.780 0.004 0.003 0.777 0.789 0.781 -0.004
## 18 std__g=~DS_z 18 0.545 0.006 0.005 0.539 0.554 0.551 -0.021
## 19 std__PPVT_z~~PPVT_z 19 0.587 0.033 0.029 0.552 0.651 0.626 -0.124
## 20 std__WJ9_z~~WJ9_z 20 0.321 0.022 0.019 0.295 0.365 0.346 -0.082
## 21 std__WJ10_z~~WJ10_z 21 0.392 0.006 0.005 0.377 0.397 0.390 0.007
## 22 std__DS_z~~DS_z 22 0.703 0.006 0.006 0.693 0.709 0.696 0.023
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 24 std__PPVT_z~1 24 -0.001 0.003 0.003 -0.008 0.003 -0.001 -0.001
## 25 std__WJ9_z~1 25 0.001 0.003 0.002 -0.002 0.008 0.004 -0.007
## 26 std__WJ10_z~1 26 -0.002 0.005 0.004 -0.007 0.004 0.001 -0.008
## 27 std__DS_z~1 27 -0.001 0.002 0.002 -0.004 0.001 -0.001 0.000
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## SD_nonlin
## 1 0.000
## 2 0.000
## 3 0.000
## 4 0.000
## 5 0.012
## 6 0.004
## 7 0.013
## 8 0.009
## 9 0.003
## 10 0.004
## 11 0.003
## 12 0.004
## 13 0.002
## 14 0.000
## 15 0.004
## 16 0.001
## 17 0.004
## 18 0.002
## 19 0.005
## 20 0.002
## 21 0.006
## 22 0.002
## 23 0.000
## 24 0.003
## 25 0.003
## 26 0.004
## 27 0.002
## 28 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 419.601
## 2 0.070 0.167 679.165
## 3 0.264 0.233 789.229
## 4 0.453 0.259 800.087
## 5 0.619 0.241 713.065
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 680.229 154.315 419.601 800.087
b_weak <- lsem.bootstrap(lsem_weak, R = 50, n.core = 14)
## Compute LSEM for bootstrap samples:
## Warning in sqrt(w): NaNs produced
# ── Scalar (strong) ──────────────────────────────────────────────────────
lsem_strong <- lsem.estimate(
data = as.data.frame(d_apa_cog),
moderator = "PC1A_z",
moderator.grid = grid_points,
lavmodel = lsem_model,
h = 1.5,
estimator = "ML",
meanstructure = TRUE,
standardized = TRUE,
par_invariant = strong_constraints
)
## Use joint estimation
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 415
## Group 2 673
## Group 3 782
## Group 4 793
## Group 5 707
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.0725269442612355
## quasi-Newton steps using NLMINB:
## objective function = 0.0725269442612355
## objective function = 0.0566749374170481
## objective function = 0.0273724740446447
## objective function = 0.0215787767042751
## objective function = 0.0154079500627411
## objective function = 0.0142616518253383
## objective function = 0.0133152586899534
## objective function = 0.0125822646837887
## objective function = 0.0118615081359439
## objective function = 0.0106721034105831
## objective function = 0.0098718681840125
## objective function = 0.0095931567616584
## objective function = 0.0094202046031605
## objective function = 0.0093063926478368
## objective function = 0.0092455561298926
## objective function = 0.0092140212556277
## objective function = 0.0091950364054556
## objective function = 0.0091833493829232
## objective function = 0.0091776924920001
## objective function = 0.0091752531281609
## objective function = 0.0091741669187282
## objective function = 0.0091736701498349
## objective function = 0.0091734631705778
## objective function = 0.0091733807707360
## objective function = 0.0091733423329667
## objective function = 0.0091733218520558
## objective function = 0.0091733119221287
## objective function = 0.0091733084532284
## objective function = 0.0091733076292169
## objective function = 0.0091733074335419
## objective function = 0.0091733073609105
## objective function = 0.0091733073334915
## objective function = 0.0091733073251845
## objective function = 0.0091733073219766
## objective function = 0.0091733073203694
## objective function = 0.0091733073197574
## objective function = 0.0091733073197574
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 34
## number of function evaluations [objective, gradient]: 36 35
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
summary(lsem_strong)
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = 1.5,
## standardized = TRUE, par_invariant = strong_constraints,
## estimator = "ML", meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 14:36:32.916872
## Time difference of 4.034776 secs
## Computation Time: 4.034776
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 1.5
## Bandwidth = 0.294
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.031
## 2 cfi 0.994
## 3 tli 0.995
## 4 gfi 0.991
## 5 srmr 0.024
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000
## 2 g=~WJ9_z 2 1.311 0.000 0.000 1.311 1.311 1.311
## 3 g=~WJ10_z 3 1.255 0.000 0.000 1.255 1.255 1.255
## 4 g=~DS_z 4 0.874 0.000 0.000 0.874 0.874 0.874
## 5 PPVT_z~~PPVT_z 5 0.569 0.079 0.067 0.487 0.723 0.663
## 6 WJ9_z~~WJ9_z 6 0.322 0.031 0.027 0.283 0.382 0.359
## 7 WJ10_z~~WJ10_z 7 0.402 0.014 0.012 0.370 0.413 0.397
## 8 DS_z~~DS_z 8 0.718 0.025 0.021 0.669 0.742 0.690
## 9 g~~g 9 0.396 0.003 0.003 0.388 0.399 0.394
## 10 PPVT_z~1 10 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 11 WJ9_z~1 11 0.002 0.000 0.000 0.002 0.002 0.002
## 12 WJ10_z~1 12 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 13 DS_z~1 13 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.642 0.026 0.022 0.591 0.669 0.611
## 16 std__g=~WJ9_z 16 0.824 0.013 0.011 0.797 0.840 0.808
## 17 std__g=~WJ10_z 17 0.780 0.004 0.003 0.777 0.789 0.781
## 18 std__g=~DS_z 18 0.545 0.006 0.005 0.539 0.554 0.551
## 19 std__PPVT_z~~PPVT_z 19 0.587 0.033 0.029 0.552 0.651 0.626
## 20 std__WJ9_z~~WJ9_z 20 0.321 0.022 0.019 0.295 0.365 0.346
## 21 std__WJ10_z~~WJ10_z 21 0.392 0.006 0.005 0.377 0.397 0.390
## 22 std__DS_z~~DS_z 22 0.703 0.006 0.006 0.693 0.709 0.696
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000
## 24 std__PPVT_z~1 24 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 25 std__WJ9_z~1 25 0.002 0.000 0.000 0.002 0.002 0.002
## 26 std__WJ10_z~1 26 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 27 std__DS_z~1 27 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000
## lin_slo SD_nonlin
## 1 0.000 0.000
## 2 0.000 0.000
## 3 0.000 0.000
## 4 0.000 0.000
## 5 -0.296 0.012
## 6 -0.118 0.004
## 7 0.017 0.013
## 8 0.088 0.009
## 9 0.006 0.003
## 10 0.000 0.000
## 11 0.000 0.000
## 12 0.000 0.000
## 13 0.000 0.000
## 14 0.000 0.000
## 15 0.098 0.004
## 16 0.050 0.001
## 17 -0.004 0.004
## 18 -0.021 0.002
## 19 -0.124 0.005
## 20 -0.082 0.002
## 21 0.007 0.006
## 22 0.023 0.002
## 23 0.000 0.000
## 24 0.000 0.000
## 25 0.000 0.000
## 26 0.000 0.000
## 27 0.000 0.000
## 28 0.000 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 419.601
## 2 0.070 0.167 679.165
## 3 0.264 0.233 789.229
## 4 0.453 0.259 800.087
## 5 0.619 0.241 713.065
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 680.229 154.315 419.601 800.087
b_strong <- lsem.bootstrap(lsem_strong, R = 50, n.core = 14)
## Compute LSEM for bootstrap samples:
## Warning in sqrt(w): NaNs produced
# ── Strict ───────────────────────────────────────────────────────────────
lsem_strict <- lsem.estimate(
data = as.data.frame(d_apa_cog),
moderator = "PC1A_z",
moderator.grid = grid_points,
lavmodel = lsem_model,
h = 1.5,
estimator = "ML",
meanstructure = TRUE,
standardized = TRUE,
par_invariant = strict_constraints
)
## Use joint estimation
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 415
## Group 2 673
## Group 3 782
## Group 4 793
## Group 5 707
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.0725269442612357
## quasi-Newton steps using NLMINB:
## objective function = 0.0725269442612357
## objective function = 0.0598849149521864
## objective function = 0.0456558227751016
## objective function = 0.0280676116603524
## objective function = 0.0233835766382247
## objective function = 0.0203046704361951
## objective function = 0.0172907948008855
## objective function = 0.0166145122110242
## objective function = 0.0152118914595368
## objective function = 0.0149220340810096
## objective function = 0.0146726699976791
## objective function = 0.0145038426628579
## objective function = 0.0143994581838247
## objective function = 0.0143413746876366
## objective function = 0.0143237628606473
## objective function = 0.0143173566351997
## objective function = 0.0143133119428696
## objective function = 0.0143117244210845
## objective function = 0.0143112455442859
## objective function = 0.0143109822916277
## objective function = 0.0143107648550805
## objective function = 0.0143106236481005
## objective function = 0.0143105939605634
## objective function = 0.0143105894862483
## objective function = 0.0143105889594805
## objective function = 0.0143105888699047
## objective function = 0.0143105888529152
## objective function = 0.0143105888463410
## objective function = 0.0143105888416870
## objective function = 0.0143105888387537
## objective function = 0.0143105888370059
## objective function = 0.0143105888370059
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 30
## number of function evaluations [objective, gradient]: 31 31
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
summary(lsem_strict)
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = 1.5,
## standardized = TRUE, par_invariant = strict_constraints,
## estimator = "ML", meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 14:50:23.673098
## Time difference of 8.916642 secs
## Computation Time: 8.916642
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 1.5
## Bandwidth = 0.294
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.034
## 2 cfi 0.990
## 3 tli 0.994
## 4 gfi 0.986
## 5 srmr 0.030
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000
## 2 g=~WJ9_z 2 1.322 0.000 0.000 1.322 1.322 1.322
## 3 g=~WJ10_z 3 1.260 0.000 0.000 1.260 1.260 1.260
## 4 g=~DS_z 4 0.879 0.000 0.000 0.879 0.879 0.879
## 5 PPVT_z~~PPVT_z 5 0.581 0.000 0.000 0.581 0.581 0.581
## 6 WJ9_z~~WJ9_z 6 0.324 0.000 0.000 0.324 0.324 0.324
## 7 WJ10_z~~WJ10_z 7 0.403 0.000 0.000 0.403 0.403 0.403
## 8 DS_z~~DS_z 8 0.715 0.000 0.000 0.715 0.715 0.715
## 9 g~~g 9 0.391 0.005 0.004 0.383 0.396 0.394
## 10 PPVT_z~1 10 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 11 WJ9_z~1 11 0.002 0.000 0.000 0.002 0.002 0.002
## 12 WJ10_z~1 12 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 13 DS_z~1 13 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.634 0.002 0.002 0.630 0.637 0.636
## 16 std__g=~WJ9_z 16 0.824 0.002 0.001 0.821 0.825 0.825
## 17 std__g=~WJ10_z 17 0.779 0.002 0.002 0.776 0.781 0.780
## 18 std__g=~DS_z 18 0.545 0.003 0.002 0.541 0.548 0.547
## 19 std__PPVT_z~~PPVT_z 19 0.598 0.003 0.003 0.594 0.603 0.596
## 20 std__WJ9_z~~WJ9_z 20 0.322 0.003 0.002 0.319 0.326 0.320
## 21 std__WJ10_z~~WJ10_z 21 0.394 0.003 0.003 0.390 0.398 0.392
## 22 std__DS_z~~DS_z 22 0.703 0.003 0.002 0.700 0.707 0.701
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000
## 24 std__PPVT_z~1 24 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 25 std__WJ9_z~1 25 0.002 0.000 0.000 0.002 0.002 0.002
## 26 std__WJ10_z~1 26 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 27 std__DS_z~1 27 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000
## lin_slo SD_nonlin
## 1 0.000 0.000
## 2 0.000 0.000
## 3 0.000 0.000
## 4 0.000 0.000
## 5 0.000 0.000
## 6 0.000 0.000
## 7 0.000 0.000
## 8 0.000 0.000
## 9 -0.010 0.004
## 10 0.000 0.000
## 11 0.000 0.000
## 12 0.000 0.000
## 13 0.000 0.000
## 14 0.000 0.000
## 15 -0.005 0.002
## 16 -0.003 0.002
## 17 -0.004 0.002
## 18 -0.005 0.002
## 19 0.006 0.003
## 20 0.006 0.002
## 21 0.006 0.003
## 22 0.005 0.002
## 23 0.000 0.000
## 24 0.000 0.000
## 25 0.000 0.000
## 26 0.000 0.000
## 27 0.000 0.000
## 28 0.000 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 419.601
## 2 0.070 0.167 679.165
## 3 0.264 0.233 789.229
## 4 0.453 0.259 800.087
## 5 0.619 0.241 713.065
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 680.229 154.315 419.601 800.087
b_strict <- lsem.bootstrap(lsem_strict, R = 50, n.core = 14)
## Compute LSEM for bootstrap samples:
## Warning in sqrt(w): NaNs produced
# ── Compare fit across invariance levels ─────────────────────────────────
invar_test <- rbind(b_config$fitstats_joint,
b_weak$fitstats_joint,
b_strong$fitstats_joint,
b_strict$fitstats_joint) %>%
mutate(ci_l = value - 1.96 * se,
ci_u = value + 1.96 * se)
invar_test$level <- c(rep("configural", nrow(b_config$fitstats_joint)),
rep("metric", nrow(b_weak$fitstats_joint)),
rep("scalar", nrow(b_strong$fitstats_joint)),
rep("strict", nrow(b_strict$fitstats_joint)))
invar_test$level <- factor(invar_test$level, levels = unique(invar_test$level))
print(invar_test)
## stat value value_bc se ci_l ci_u
## rmsea rmsea 0.08187480 0.07453702 0.022479901 0.037814194 0.12593540
## cfi cfi 0.98886075 0.99148966 0.006223548 0.976662595 1.00105890
## tli tli 0.96658225 0.97446897 0.018670643 0.929987786 1.00317671
## gfi gfi 0.99207405 0.99380322 0.003624108 0.984970803 0.99917731
## srmr srmr 0.01987493 0.01828622 0.004311263 0.011424853 0.02832501
## rmsea1 rmsea 0.05173956 0.04124471 0.015231831 0.021885170 0.08159395
## cfi1 cfi 0.99021359 0.99523860 0.006643360 0.977192600 1.00323457
## tli1 tli 0.98665489 0.99350718 0.009059128 0.968898999 1.00441078
## gfi1 gfi 0.99119013 0.99421749 0.003888456 0.983568756 0.99881151
## srmr1 srmr 0.02425864 0.01626246 0.005372913 0.013727736 0.03478955
## rmsea2 rmsea 0.03050162 0.01947504 0.016554626 -0.001945449 0.06294869
## cfi2 cfi 0.99412531 1.00061827 0.008288920 0.977879029 1.01037159
## tli2 tli 0.99536209 1.00045923 0.006589323 0.982447014 1.00827716
## gfi2 gfi 0.99117039 0.99492553 0.004651413 0.982053621 1.00028716
## srmr2 srmr 0.02432868 0.01727356 0.005738153 0.013081902 0.03557546
## rmsea3 rmsea 0.03415303 0.02385697 0.011172281 0.012255361 0.05605070
## cfi3 cfi 0.98953335 0.99790062 0.009031757 0.971831106 1.00723559
## tli3 tli 0.99418519 0.99883368 0.005017643 0.984350614 1.00401977
## gfi3 gfi 0.98630652 0.99103597 0.004880105 0.976741515 0.99587153
## srmr3 srmr 0.03023610 0.02046019 0.006595546 0.017308827 0.04316337
## level
## rmsea configural
## cfi configural
## tli configural
## gfi configural
## srmr configural
## rmsea1 metric
## cfi1 metric
## tli1 metric
## gfi1 metric
## srmr1 metric
## rmsea2 scalar
## cfi2 scalar
## tli2 scalar
## gfi2 scalar
## srmr2 scalar
## rmsea3 strict
## cfi3 strict
## tli3 strict
## gfi3 strict
## srmr3 strict
# ── Plots ────────────────────────────────────────────────────────────────
p_cfi <- invar_test %>%
filter(stat == "cfi") %>%
ggplot(aes(x = level, y = value)) +
geom_point(size = 3) +
geom_errorbar(aes(ymin = ci_l, ymax = ci_u), width = .2) +
scale_y_continuous(limits = c(.9, 1)) +
ylab("CFI") + xlab("Invariance Level") +
theme_minimal()
p_rmsea <- invar_test %>%
filter(stat == "rmsea") %>%
ggplot(aes(x = level, y = value)) +
geom_point(size = 3) +
geom_errorbar(aes(ymin = ci_l, ymax = ci_u), width = .2) +
scale_y_continuous(limits = c(0, .15)) +
ylab("RMSEA") + xlab("Invariance Level") +
theme_minimal()
p_srmr <- invar_test %>%
filter(stat == "srmr") %>%
ggplot(aes(x = level, y = value)) +
geom_point(size = 3) +
geom_errorbar(aes(ymin = ci_l, ymax = ci_u), width = .2) +
scale_y_continuous(limits = c(0, .15)) +
ylab("SRMR") + xlab("Invariance Level") +
theme_minimal()
print(p_cfi)

print(p_rmsea)

print(p_srmr)

#LevelRMSEACFITLISRMRConfigural.082 (.031–.131).989 (.973–1.005).967 (.919–1.015).020 (.010–.030)
#Metric.051 (.021–.082).990 (.976–1.005).987 (.967–1.006).024 (.013–.035)
#Scalar.030 (.001–.059).994 (.982–1.006).995 (.986–1.005).024 (.014–.034)
#Strict.034 (.010–.058).990 (.973–1.006).994 (.985–1.004).030 (.020–.041)
# ==============================================================
# 11b. Multi-Group CFA: Measurement Invariance (PC1A tertiles)
# ==============================================================
# Direct test of configural -> metric -> scalar -> strict.
# Complements LSEM: LSEM tests continuous parameter moderation
# but doesn't explicitly test intercepts. This confirms scalar
# invariance directly.
cfa_model <- 'g =~ PPVT_z + WJ9_z + WJ10_z + DS_z'
run_mi_cfa <- function(data, pc_var, label) {
dd <- data[complete.cases(data[, c("PPVT_z", "WJ9_z", "WJ10_z", "DS_z", pc_var)]), ]
dd$tert <- cut(dd[[pc_var]],
breaks = quantile(dd[[pc_var]], probs = c(0, 1/3, 2/3, 1)),
include.lowest = TRUE, labels = c("Low", "Mid", "High"))
cat("\n", paste(rep("=", 80), collapse = ""), "\n")
cat(label, "\n")
cat("N =", nrow(dd), " | Tertile Ns:", table(dd$tert), "\n")
cat(paste(rep("=", 80), collapse = ""), "\n")
fit_config <- cfa(cfa_model, data = dd, group = "tert", estimator = "ML",
meanstructure = TRUE)
fit_metric <- cfa(cfa_model, data = dd, group = "tert", estimator = "ML",
meanstructure = TRUE, group.equal = "loadings")
fit_scalar <- cfa(cfa_model, data = dd, group = "tert", estimator = "ML",
meanstructure = TRUE, group.equal = c("loadings", "intercepts"))
fit_strict <- cfa(cfa_model, data = dd, group = "tert", estimator = "ML",
meanstructure = TRUE, group.equal = c("loadings", "intercepts", "residuals"))
extract_fit <- function(fit, lbl) {
fm <- fitmeasures(fit, c("chisq", "df", "pvalue", "cfi", "tli", "rmsea", "srmr"))
data.frame(Model = lbl,
chisq = round(fm["chisq"], 3),
df = fm["df"],
p = round(fm["pvalue"], 4),
CFI = round(fm["cfi"], 4),
TLI = round(fm["tli"], 4),
RMSEA = round(fm["rmsea"], 4),
SRMR = round(fm["srmr"], 4),
row.names = NULL)
}
fit_table <- rbind(
extract_fit(fit_config, "1. Configural"),
extract_fit(fit_metric, "2. Metric"),
extract_fit(fit_scalar, "3. Scalar"),
extract_fit(fit_strict, "4. Strict")
)
print(fit_table, row.names = FALSE)
cfis <- sapply(list(fit_config, fit_metric, fit_scalar, fit_strict), fitmeasures, "cfi")
steps <- c("Config->Metric", "Metric->Scalar", "Scalar->Strict")
cat("\n")
for (i in 1:3) {
d_cfi <- cfis[i + 1] - cfis[i]
cat(sprintf(" %-20s ΔCFI = %+.4f %s\n", steps[i], d_cfi,
ifelse(abs(d_cfi) < 0.01, "[PASS]", "[FAIL]")))
}
cat("\n")
}
run_mi_cfa(d_apa, "K5PC1A", "APA x African Ancestry PC (PC1A) tertiles")
##
## ================================================================================
## APA x African Ancestry PC (PC1A) tertiles
## N = 1507 | Tertile Ns: 503 502 502
## ================================================================================
## Model chisq df p CFI TLI RMSEA SRMR
## 1. Configural 27.049 6 0.0001 0.9882 0.9647 0.0836 0.0202
## 2. Metric 30.072 12 0.0027 0.9899 0.9849 0.0548 0.0245
## 3. Scalar 44.348 18 0.0005 0.9853 0.9853 0.0540 0.0316
## 4. Strict 64.096 26 0.0000 0.9787 0.9853 0.0540 0.0424
##
## Config->Metric ΔCFI = +0.0017 [PASS]
## Metric->Scalar ΔCFI = -0.0046 [PASS]
## Scalar->Strict ΔCFI = -0.0066 [PASS]
run_mi_cfa(d_haa, "K5PC1A", "HAA x African Ancestry PC (PC1A) tertiles")
##
## ================================================================================
## HAA x African Ancestry PC (PC1A) tertiles
## N = 1261 | Tertile Ns: 421 420 420
## ================================================================================
## Model chisq df p CFI TLI RMSEA SRMR
## 1. Configural 24.688 6 0.0004 0.9877 0.9630 0.0861 0.0209
## 2. Metric 27.769 12 0.0060 0.9896 0.9844 0.0559 0.0258
## 3. Scalar 35.893 18 0.0073 0.9882 0.9882 0.0486 0.0302
## 4. Strict 66.951 26 0.0000 0.9730 0.9813 0.0612 0.0439
##
## Config->Metric ΔCFI = +0.0019 [PASS]
## Metric->Scalar ΔCFI = -0.0014 [PASS]
## Scalar->Strict ΔCFI = -0.0152 [FAIL]
# ==============================================================
# 11c. LSEM Bandwidth Sensitivity
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("ROBUSTNESS: LSEM bandwidth sensitivity (configural only)\n")
## ROBUSTNESS: LSEM bandwidth sensitivity (configural only)
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
for (h_val in c(1.0, 2.0)) {
lsem_h <- lsem.estimate(
data = as.data.frame(d_apa_cog),
moderator = "PC1A_z",
moderator.grid = grid_points,
lavmodel = lsem_model,
h = h_val,
estimator = "ML",
meanstructure = TRUE,
standardized = TRUE,
est_joint = TRUE
)
cat("\nh =", h_val, "\n")
print(summary(lsem_h)$fitstat_joint)
}
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 260
## Group 2 469
## Group 3 568
## Group 4 592
## Group 5 526
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.4754973947033598
## quasi-Newton steps using NLMINB:
## objective function = 0.4754973947033598
## objective function = 0.4310407909366635
## objective function = 0.3143510820102698
## objective function = 0.1542020094541372
## objective function = 1.1001605993795394
## objective function = 0.1304131229638222
## objective function = 0.0935235360325728
## objective function = 0.0744449729434417
## objective function = 0.1136588580712203
## objective function = 0.0577928740295930
## objective function = 0.0501947218570290
## objective function = 0.0362155729052460
## objective function = 0.0274337453316155
## objective function = 0.0228531030155932
## objective function = 0.0196114566771164
## objective function = 0.0175016767418163
## objective function = 0.0157431992278658
## objective function = 0.0140802066247961
## objective function = 0.0130883347630556
## objective function = 0.0122756731364266
## objective function = 0.0116828929902572
## objective function = 0.0111388413994315
## objective function = 0.0106484804178050
## objective function = 0.0103234912375580
## objective function = 0.0101092501167388
## objective function = 0.0099305264135944
## objective function = 0.0096936322734374
## objective function = 0.0096126045050253
## objective function = 0.0095237734414318
## objective function = 0.0094500176843905
## objective function = 0.0093797700575670
## objective function = 0.0093163318601537
## objective function = 0.0092630988053402
## objective function = 0.0092249482170084
## objective function = 0.0092017357033331
## objective function = 0.0091843593734885
## objective function = 0.0091725302905539
## objective function = 0.0091635319056707
## objective function = 0.0091576760816100
## objective function = 0.0091538825444771
## objective function = 0.0091511195656637
## objective function = 0.0091487887009347
## objective function = 0.0091462160392724
## objective function = 0.0091440639201876
## objective function = 0.0091419113645792
## objective function = 0.0091400849735483
## objective function = 0.0091386297166956
## objective function = 0.0091374061436663
## objective function = 0.0091365038108760
## objective function = 0.0091358786871588
## objective function = 0.0091354631727697
## objective function = 0.0091351619084838
## objective function = 0.0091348781332385
## objective function = 0.0091346136740585
## objective function = 0.0091344016601473
## objective function = 0.0091342626298315
## objective function = 0.0091341741928641
## objective function = 0.0091341078379438
## objective function = 0.0091340492025364
## objective function = 0.0091340040948203
## objective function = 0.0091339679742639
## objective function = 0.0091339419812930
## objective function = 0.0091339244067327
## objective function = 0.0091339121810573
## objective function = 0.0091339050142846
## objective function = 0.0091339000311443
## objective function = 0.0091338964401904
## objective function = 0.0091338939518031
## objective function = 0.0091338924274136
## objective function = 0.0091338915867166
## objective function = 0.0091338912090904
## objective function = 0.0091338910702548
## objective function = 0.0091338910107239
## objective function = 0.0091338909707185
## objective function = 0.0091338909304676
## objective function = 0.0091338909029609
## objective function = 0.0091338908791812
## objective function = 0.0091338908622966
## objective function = 0.0091338908502639
## objective function = 0.0091338908443868
## objective function = 0.0091338908419006
## objective function = 0.0091338908404552
## objective function = 0.0091338908393481
## objective function = 0.0091338908381443
## objective function = 0.0091338908376613
## objective function = 0.0091338908376613
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 80
## number of function evaluations [objective, gradient]: 85 81
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
##
##
## h = 1
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = h_val,
## standardized = TRUE, est_joint = TRUE, estimator = "ML",
## meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 15:04:32.017989
## Time difference of 4.17801 secs
## Computation Time: 4.17801
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 1
## Bandwidth = 0.196
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.084
## 2 cfi 0.988
## 3 tli 0.965
## 4 gfi 0.991
## 5 srmr 0.021
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int lin_slo
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 2 g=~WJ9_z 2 1.317 0.099 0.090 1.214 1.485 1.419 -0.325
## 3 g=~WJ10_z 3 1.256 0.060 0.048 1.209 1.407 1.324 -0.214
## 4 g=~DS_z 4 0.879 0.037 0.027 0.848 0.975 0.911 -0.102
## 5 PPVT_z~~PPVT_z 5 0.563 0.096 0.082 0.469 0.733 0.674 -0.352
## 6 WJ9_z~~WJ9_z 6 0.316 0.014 0.011 0.294 0.341 0.330 -0.047
## 7 WJ10_z~~WJ10_z 7 0.409 0.018 0.016 0.373 0.422 0.407 0.005
## 8 DS_z~~DS_z 8 0.723 0.034 0.031 0.662 0.760 0.685 0.120
## 9 g~~g 9 0.397 0.038 0.032 0.327 0.431 0.353 0.140
## 10 PPVT_z~1 10 0.001 0.006 0.004 -0.008 0.013 0.002 -0.001
## 11 WJ9_z~1 11 0.002 0.005 0.004 -0.002 0.013 0.003 -0.004
## 12 WJ10_z~1 12 0.000 0.004 0.004 -0.005 0.005 0.001 -0.005
## 13 DS_z~1 13 -0.001 0.002 0.002 -0.003 0.003 0.000 -0.002
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.644 0.050 0.043 0.555 0.692 0.586 0.183
## 16 std__g=~WJ9_z 16 0.827 0.008 0.007 0.809 0.838 0.825 0.004
## 17 std__g=~WJ10_z 17 0.777 0.011 0.009 0.764 0.797 0.776 0.004
## 18 std__g=~DS_z 18 0.545 0.009 0.008 0.534 0.565 0.547 -0.005
## 19 std__PPVT_z~~PPVT_z 19 0.583 0.063 0.054 0.521 0.692 0.655 -0.230
## 20 std__WJ9_z~~WJ9_z 20 0.317 0.014 0.011 0.298 0.346 0.318 -0.006
## 21 std__WJ10_z~~WJ10_z 21 0.396 0.016 0.014 0.366 0.416 0.398 -0.006
## 22 std__DS_z~~DS_z 22 0.703 0.010 0.009 0.680 0.715 0.701 0.005
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 24 std__PPVT_z~1 24 0.001 0.006 0.004 -0.008 0.012 0.002 -0.001
## 25 std__WJ9_z~1 25 0.002 0.005 0.004 -0.002 0.012 0.003 -0.003
## 26 std__WJ10_z~1 26 0.000 0.004 0.003 -0.005 0.005 0.001 -0.005
## 27 std__DS_z~1 27 -0.001 0.002 0.002 -0.003 0.003 0.000 -0.002
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## SD_nonlin
## 1 0.000
## 2 0.051
## 3 0.022
## 4 0.025
## 5 0.024
## 6 0.006
## 7 0.018
## 8 0.014
## 9 0.011
## 10 0.006
## 11 0.005
## 12 0.004
## 13 0.002
## 14 0.000
## 15 0.013
## 16 0.008
## 17 0.010
## 18 0.009
## 19 0.016
## 20 0.014
## 21 0.016
## 22 0.010
## 23 0.000
## 24 0.005
## 25 0.005
## 26 0.004
## 27 0.002
## 28 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 262.694
## 2 0.070 0.167 473.933
## 3 0.264 0.233 572.817
## 4 0.453 0.259 597.446
## 5 0.619 0.241 531.044
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 487.587 134.150 262.694 597.446
## NULL
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 575
## Group 2 840
## Group 3 936
## Group 4 934
## Group 5 846
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.0363330532126399
## quasi-Newton steps using NLMINB:
## objective function = 0.0363330532126399
## objective function = 0.0279370167711291
## objective function = 0.0153857529769107
## objective function = 0.0133272104962442
## objective function = 0.0110416922998723
## objective function = 0.0102301237819656
## objective function = 0.0095860777667646
## objective function = 0.0090282965794805
## objective function = 0.0086508329545345
## objective function = 0.0084524089896739
## objective function = 0.0082043021034368
## objective function = 0.0079977449504251
## objective function = 0.0078213564944291
## objective function = 0.0076833600586591
## objective function = 0.0076053558631472
## objective function = 0.0075428939581485
## objective function = 0.0074975157835587
## objective function = 0.0074639541003761
## objective function = 0.0074402058494830
## objective function = 0.0074234696281078
## objective function = 0.0074105597352218
## objective function = 0.0073997951802440
## objective function = 0.0073912982889315
## objective function = 0.0073844255708944
## objective function = 0.0073782324906850
## objective function = 0.0073724830020676
## objective function = 0.0073661733332528
## objective function = 0.0073617432153425
## objective function = 0.0073585362794566
## objective function = 0.0073566714062932
## objective function = 0.0073551657051661
## objective function = 0.0073538140686926
## objective function = 0.0073526451094608
## objective function = 0.0073516220349611
## objective function = 0.0073511537137943
## objective function = 0.0073508302576434
## objective function = 0.0073506373226652
## objective function = 0.0073504693542614
## objective function = 0.0073502867918172
## objective function = 0.0073501464427732
## objective function = 0.0073500134091112
## objective function = 0.0073498980684367
## objective function = 0.0073498192366753
## objective function = 0.0073497578053155
## objective function = 0.0073497099858648
## objective function = 0.0073496775789855
## objective function = 0.0073496594640100
## objective function = 0.0073496505572901
## objective function = 0.0073496452634557
## objective function = 0.0073496411753814
## objective function = 0.0073496379519627
## objective function = 0.0073496355783971
## objective function = 0.0073496338176442
## objective function = 0.0073496324457824
## objective function = 0.0073496314035622
## objective function = 0.0073496306685951
## objective function = 0.0073496301655286
## objective function = 0.0073496298094182
## objective function = 0.0073496295472912
## objective function = 0.0073496293551493
## objective function = 0.0073496292246976
## objective function = 0.0073496291466901
## objective function = 0.0073496291015490
## objective function = 0.0073496290698576
## objective function = 0.0073496290427915
## objective function = 0.0073496290200234
## objective function = 0.0073496290026585
## objective function = 0.0073496289948418
## objective function = 0.0073496289916240
## objective function = 0.0073496289905358
## objective function = 0.0073496289900420
## objective function = 0.0073496289900420
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 70
## number of function evaluations [objective, gradient]: 71 71
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
##
##
## h = 2
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = h_val,
## standardized = TRUE, est_joint = TRUE, estimator = "ML",
## meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 15:04:36.554083
## Time difference of 4.390869 secs
## Computation Time: 4.390869
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 2
## Bandwidth = 0.392
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.078
## 2 cfi 0.990
## 3 tli 0.969
## 4 gfi 0.993
## 5 srmr 0.019
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int lin_slo
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 2 g=~WJ9_z 2 1.308 0.079 0.071 1.208 1.433 1.400 -0.291
## 3 g=~WJ10_z 3 1.255 0.054 0.042 1.200 1.380 1.318 -0.202
## 4 g=~DS_z 4 0.874 0.027 0.021 0.847 0.940 0.906 -0.102
## 5 PPVT_z~~PPVT_z 5 0.569 0.077 0.067 0.482 0.720 0.661 -0.292
## 6 WJ9_z~~WJ9_z 6 0.326 0.028 0.019 0.304 0.403 0.357 -0.097
## 7 WJ10_z~~WJ10_z 7 0.398 0.019 0.016 0.356 0.416 0.395 0.009
## 8 DS_z~~DS_z 8 0.715 0.017 0.014 0.674 0.730 0.695 0.061
## 9 g~~g 9 0.398 0.034 0.029 0.330 0.437 0.357 0.129
## 10 PPVT_z~1 10 -0.002 0.002 0.002 -0.004 0.000 -0.001 -0.003
## 11 WJ9_z~1 11 0.000 0.003 0.003 -0.003 0.005 0.004 -0.011
## 12 WJ10_z~1 12 -0.002 0.004 0.004 -0.007 0.004 0.002 -0.012
## 13 DS_z~1 13 -0.002 0.001 0.001 -0.005 0.000 -0.002 0.001
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.642 0.042 0.036 0.560 0.690 0.592 0.157
## 16 std__g=~WJ9_z 16 0.822 0.010 0.006 0.792 0.827 0.814 0.024
## 17 std__g=~WJ10_z 17 0.781 0.008 0.007 0.773 0.799 0.782 -0.001
## 18 std__g=~DS_z 18 0.545 0.003 0.003 0.541 0.550 0.544 0.004
## 19 std__PPVT_z~~PPVT_z 19 0.586 0.052 0.046 0.525 0.686 0.649 -0.198
## 20 std__WJ9_z~~WJ9_z 20 0.325 0.016 0.010 0.315 0.373 0.337 -0.039
## 21 std__WJ10_z~~WJ10_z 21 0.389 0.013 0.011 0.362 0.403 0.389 0.002
## 22 std__DS_z~~DS_z 22 0.703 0.004 0.003 0.698 0.707 0.704 -0.004
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000 0.000
## 24 std__PPVT_z~1 24 -0.002 0.002 0.002 -0.005 0.000 -0.001 -0.004
## 25 std__WJ9_z~1 25 0.000 0.003 0.003 -0.003 0.005 0.004 -0.011
## 26 std__WJ10_z~1 26 -0.002 0.004 0.004 -0.007 0.003 0.002 -0.012
## 27 std__DS_z~1 27 -0.002 0.002 0.001 -0.005 0.000 -0.002 0.001
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000 0.000
## SD_nonlin
## 1 0.000
## 2 0.018
## 3 0.007
## 4 0.005
## 5 0.008
## 6 0.011
## 7 0.018
## 8 0.007
## 9 0.002
## 10 0.002
## 11 0.001
## 12 0.002
## 13 0.001
## 14 0.000
## 15 0.004
## 16 0.008
## 17 0.008
## 18 0.003
## 19 0.006
## 20 0.013
## 21 0.013
## 22 0.003
## 23 0.000
## 24 0.002
## 25 0.001
## 26 0.002
## 27 0.001
## 28 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 580.209
## 2 0.070 0.167 848.175
## 3 0.264 0.233 945.139
## 4 0.453 0.259 942.705
## 5 0.619 0.241 853.379
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 833.921 149.294 580.209 945.139
## NULL
# run invariance models for comparison
cat("\nScalar invariance at h = 1.0:\n")
##
## Scalar invariance at h = 1.0:
lsem_strong_h1 <- lsem.estimate(
data = as.data.frame(d_apa_cog),
moderator = "PC1A_z",
moderator.grid = grid_points,
lavmodel = lsem_model,
h = 1.0,
estimator = "ML",
meanstructure = TRUE,
standardized = TRUE,
par_invariant = strong_constraints
)
## Use joint estimation
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 260
## Group 2 469
## Group 3 568
## Group 4 592
## Group 5 526
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.4754973947033595
## quasi-Newton steps using NLMINB:
## objective function = 0.4754973947033595
## objective function = 0.4323832378287140
## objective function = 0.3874701232351613
## objective function = 0.3482134483756066
## objective function = 0.2804427797398291
## objective function = Inf
## objective function = 0.2379622115766932
## objective function = 0.1730091282698487
## objective function = 0.1680302255292255
## objective function = 0.1567538520268486
## objective function = 0.1247766203257206
## objective function = 0.2271158877186178
## objective function = 0.1086158335287673
## objective function = 0.0957725969251728
## objective function = 0.1052869419504699
## objective function = 0.0745525163747240
## objective function = 0.0731171375826762
## objective function = 0.0554442546532254
## objective function = 0.0435283489168887
## objective function = 0.0347739538808998
## objective function = 0.0320784728835493
## objective function = 0.0230142477661597
## objective function = 0.0194795132922565
## objective function = 0.0175309096612321
## objective function = 0.0150897371076818
## objective function = 0.0136454140263130
## objective function = 0.0124600870458997
## objective function = 0.0115087286161267
## objective function = 0.0109267888074380
## objective function = 0.0107942172087586
## objective function = 0.0106899229950802
## objective function = 0.0106549826098450
## objective function = 0.0106364690668438
## objective function = 0.0106296204274507
## objective function = 0.0106267595722763
## objective function = 0.0106251438164016
## objective function = 0.0106239152061211
## objective function = 0.0106235657222502
## objective function = 0.0106234409695969
## objective function = 0.0106233851458748
## objective function = 0.0106233491320922
## objective function = 0.0106233278272785
## objective function = 0.0106233164413848
## objective function = 0.0106233122049660
## objective function = 0.0106233107152364
## objective function = 0.0106233100159490
## objective function = 0.0106233096438381
## objective function = 0.0106233094940255
## objective function = 0.0106233094441505
## objective function = 0.0106233094262164
## objective function = 0.0106233094179794
## objective function = 0.0106233094140130
## objective function = 0.0106233094124133
## objective function = 0.0106233094119274
## objective function = 0.0106233094119274
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 47
## number of function evaluations [objective, gradient]: 54 48
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
print(summary(lsem_strong_h1)$fitstat_joint)
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = 1,
## standardized = TRUE, par_invariant = strong_constraints,
## estimator = "ML", meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 15:04:41.856415
## Time difference of 5.148766 secs
## Computation Time: 5.148766
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 1
## Bandwidth = 0.196
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.027
## 2 cfi 0.995
## 3 tli 0.996
## 4 gfi 0.990
## 5 srmr 0.026
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000
## 2 g=~WJ9_z 2 1.307 0.000 0.000 1.307 1.307 1.307
## 3 g=~WJ10_z 3 1.250 0.000 0.000 1.250 1.250 1.250
## 4 g=~DS_z 4 0.873 0.000 0.000 0.873 0.873 0.873
## 5 PPVT_z~~PPVT_z 5 0.564 0.088 0.075 0.478 0.720 0.666
## 6 WJ9_z~~WJ9_z 6 0.317 0.031 0.028 0.275 0.364 0.346
## 7 WJ10_z~~WJ10_z 7 0.409 0.013 0.011 0.394 0.431 0.403
## 8 DS_z~~DS_z 8 0.723 0.036 0.034 0.669 0.762 0.684
## 9 g~~g 9 0.398 0.005 0.004 0.387 0.404 0.395
## 10 PPVT_z~1 10 0.002 0.000 0.000 0.002 0.002 0.002
## 11 WJ9_z~1 11 0.002 0.000 0.000 0.002 0.002 0.002
## 12 WJ10_z~1 12 0.000 0.000 0.000 0.000 0.000 0.000
## 13 DS_z~1 13 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.645 0.030 0.026 0.591 0.673 0.611
## 16 std__g=~WJ9_z 16 0.826 0.013 0.012 0.807 0.843 0.813
## 17 std__g=~WJ10_z 17 0.777 0.004 0.003 0.771 0.780 0.778
## 18 std__g=~DS_z 18 0.544 0.009 0.008 0.535 0.557 0.553
## 19 std__PPVT_z~~PPVT_z 19 0.583 0.038 0.033 0.547 0.651 0.626
## 20 std__WJ9_z~~WJ9_z 20 0.317 0.021 0.020 0.289 0.349 0.339
## 21 std__WJ10_z~~WJ10_z 21 0.397 0.006 0.005 0.391 0.406 0.395
## 22 std__DS_z~~DS_z 22 0.704 0.009 0.009 0.690 0.713 0.694
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000
## 24 std__PPVT_z~1 24 0.002 0.000 0.000 0.001 0.002 0.001
## 25 std__WJ9_z~1 25 0.002 0.000 0.000 0.002 0.002 0.002
## 26 std__WJ10_z~1 26 0.000 0.000 0.000 0.000 0.000 0.000
## 27 std__DS_z~1 27 -0.001 0.000 0.000 -0.001 -0.001 -0.001
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000
## lin_slo SD_nonlin
## 1 0.000 0.000
## 2 0.000 0.000
## 3 0.000 0.000
## 4 0.000 0.000
## 5 -0.323 0.022
## 6 -0.090 0.020
## 7 0.019 0.012
## 8 0.124 0.015
## 9 0.010 0.004
## 10 0.000 0.000
## 11 0.000 0.000
## 12 0.000 0.000
## 13 0.000 0.000
## 14 0.000 0.000
## 15 0.109 0.007
## 16 0.041 0.007
## 17 -0.003 0.003
## 18 -0.029 0.004
## 19 -0.139 0.009
## 20 -0.068 0.012
## 21 0.005 0.005
## 22 0.031 0.004
## 23 0.000 0.000
## 24 0.000 0.000
## 25 0.000 0.000
## 26 0.000 0.000
## 27 0.000 0.000
## 28 0.000 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 262.694
## 2 0.070 0.167 473.933
## 3 0.264 0.233 572.817
## 4 0.453 0.259 597.446
## 5 0.619 0.241 531.044
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 487.587 134.150 262.694 597.446
## NULL
cat("\nScalar invariance at h = 2.0:\n")
##
## Scalar invariance at h = 2.0:
lsem_strong_h2 <- lsem.estimate(
data = as.data.frame(d_apa_cog),
moderator = "PC1A_z",
moderator.grid = grid_points,
lavmodel = lsem_model,
h = 2.0,
estimator = "ML",
meanstructure = TRUE,
standardized = TRUE,
par_invariant = strong_constraints
)
## Use joint estimation
## ** Residualize Data
## ** Fit lavaan model
## |*****|
## |lavoptions ... done.
## lavdata ... done.
## Number of observations per group:
## Group 1 575
## Group 2 840
## Group 3 936
## Group 4 934
## Group 5 846
## lavsamplestats ... done.
## lavh1 ... start:
## lavh1 ... done.
## lavpartable bounds ... done.
## lavstart ... done.
## lavmodel ... done.
## lavoptim ... start:
## attempt 1 -- default options
## objective function = 0.0363330532126398
## quasi-Newton steps using NLMINB:
## objective function = 0.0363330532126398
## objective function = 0.0277690480209134
## objective function = 0.0163251982990948
## objective function = 0.0142496399345276
## objective function = 0.0128700138865049
## objective function = 0.0120467045640627
## objective function = 0.0106292798373284
## objective function = 0.0099049490033018
## objective function = 0.0092688999741999
## objective function = 0.0087938395377918
## objective function = 0.0085739861569305
## objective function = 0.0084008216551196
## objective function = 0.0082741335982951
## objective function = 0.0081913254427921
## objective function = 0.0081527112279510
## objective function = 0.0081361921129442
## objective function = 0.0081284871198465
## objective function = 0.0081248618935237
## objective function = 0.0081232184407308
## objective function = 0.0081224615251444
## objective function = 0.0081220217220510
## objective function = 0.0081217527192213
## objective function = 0.0081216095271687
## objective function = 0.0081215389270073
## objective function = 0.0081215085968742
## objective function = 0.0081214992573260
## objective function = 0.0081214971770618
## objective function = 0.0081214966605514
## objective function = 0.0081214964828304
## objective function = 0.0081214964300407
## objective function = 0.0081214964161902
## objective function = 0.0081214964114861
## objective function = 0.0081214964095177
## objective function = 0.0081214964087900
## objective function = 0.0081214964087900
## convergence status (0=ok): 0
## nlminb message says: relative convergence (4)
## number of iterations: 33
## number of function evaluations [objective, gradient]: 34 34
## lavoptim ... done.
## lavimplied ... done.
## lavloglik ... done.
## computing TEST for test(s) = standard ... done.
## lavbaseline ... done.
## post check ... done.
##
## Use lsem.bootstrap() for computing standard errors!
##
## -|
## ** Parameter summary
print(summary(lsem_strong_h2)$fitstat_joint)
## -----------------------------------------------------------------
## Local Structural Equation Model
##
## sirt 4.2-133 (2025-09-27 12:57:51)
## lavaan 0.6-19 (2024-09-26 15:50:14 UTC)
##
## R version 4.5.1 (2025-06-13 ucrt) x86_64, mingw32 | nodename=HUANG | login=mh198
##
## Function 'sirt::lsem.estimate', type='LSEM'
##
##
## Call:
## lsem.estimate(data = as.data.frame(d_apa_cog), moderator = "PC1A_z",
## moderator.grid = grid_points, lavmodel = lsem_model, h = 2,
## standardized = TRUE, par_invariant = strong_constraints,
## estimator = "ML", meanstructure = TRUE)
##
## Date of Analysis: 2026-06-12 15:04:46.372483
## Time difference of 4.361023 secs
## Computation Time: 4.361023
##
## Number of observations in datasets = 1538
## Used observations in analysis = 1524
## Used sampling weights: FALSE
## Bandwidth factor = 2
## Bandwidth = 0.392
## Number of focal points for moderator = 5
##
## Used joint estimation: TRUE
## Used sufficient statistics: TRUE
## Used local linear smoothing: TRUE
## Used pseudo weights: FALSE
## Used lavaan package: TRUE
## Used lavaan.survey package: FALSE
##
## Mean structure modelled: TRUE
##
## lavaan Model
## g =~ PPVT_z + WJ9_z + WJ10_z + DS_z
##
## Global Fit Statistics for Joint Estimation
##
## stat value
## 1 rmsea 0.030
## 2 cfi 0.994
## 3 tli 0.995
## 4 gfi 0.992
## 5 srmr 0.023
##
##
## Parameter Estimate Summary
##
## par parindex M SD MAD Min Max lin_int
## 1 g=~PPVT_z 1 1.000 0.000 0.000 1.000 1.000 1.000
## 2 g=~WJ9_z 2 1.309 0.000 0.000 1.309 1.309 1.309
## 3 g=~WJ10_z 3 1.255 0.000 0.000 1.255 1.255 1.255
## 4 g=~DS_z 4 0.873 0.000 0.000 0.873 0.873 0.873
## 5 PPVT_z~~PPVT_z 5 0.570 0.070 0.060 0.493 0.708 0.654
## 6 WJ9_z~~WJ9_z 6 0.326 0.035 0.028 0.288 0.406 0.368
## 7 WJ10_z~~WJ10_z 7 0.398 0.013 0.011 0.364 0.409 0.392
## 8 DS_z~~DS_z 8 0.715 0.018 0.015 0.674 0.731 0.694
## 9 g~~g 9 0.396 0.004 0.002 0.386 0.398 0.393
## 10 PPVT_z~1 10 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 11 WJ9_z~1 11 0.001 0.000 0.000 0.001 0.001 0.001
## 12 WJ10_z~1 12 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 13 DS_z~1 13 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 14 g~1 14 0.000 0.000 0.000 0.000 0.000 0.000
## 15 std__g=~PPVT_z 15 0.641 0.024 0.020 0.594 0.667 0.613
## 16 std__g=~WJ9_z 16 0.822 0.015 0.012 0.787 0.838 0.804
## 17 std__g=~WJ10_z 17 0.781 0.004 0.003 0.778 0.791 0.783
## 18 std__g=~DS_z 18 0.545 0.004 0.003 0.541 0.551 0.549
## 19 std__PPVT_z~~PPVT_z 19 0.588 0.030 0.026 0.555 0.648 0.624
## 20 std__WJ9_z~~WJ9_z 20 0.324 0.024 0.019 0.298 0.381 0.353
## 21 std__WJ10_z~~WJ10_z 21 0.389 0.006 0.005 0.375 0.395 0.387
## 22 std__DS_z~~DS_z 22 0.703 0.004 0.004 0.697 0.707 0.699
## 23 std__g~~g 23 1.000 0.000 0.000 1.000 1.000 1.000
## 24 std__PPVT_z~1 24 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 25 std__WJ9_z~1 25 0.001 0.000 0.000 0.001 0.001 0.001
## 26 std__WJ10_z~1 26 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 27 std__DS_z~1 27 -0.002 0.000 0.000 -0.002 -0.002 -0.002
## 28 std__g~1 28 0.000 0.000 0.000 0.000 0.000 0.000
## lin_slo SD_nonlin
## 1 0.000 0.000
## 2 0.000 0.000
## 3 0.000 0.000
## 4 0.000 0.000
## 5 -0.264 0.008
## 6 -0.132 0.003
## 7 0.018 0.012
## 8 0.065 0.006
## 9 0.008 0.003
## 10 0.000 0.000
## 11 0.000 0.000
## 12 0.000 0.000
## 13 0.000 0.000
## 14 0.000 0.000
## 15 0.089 0.002
## 16 0.056 0.002
## 17 -0.004 0.004
## 18 -0.014 0.001
## 19 -0.113 0.003
## 20 -0.091 0.003
## 21 0.006 0.006
## 22 0.015 0.001
## 23 0.000 0.000
## 24 0.000 0.000
## 25 0.000 0.000
## 26 0.000 0.000
## 27 0.000 0.000
## 28 0.000 0.000
##
## Distribution of Moderator: Density and Effective Sample Size
##
## M=0.062 | SD=0.851
##
## moderator wgt Neff
## 1 -0.246 0.100 580.209
## 2 0.070 0.167 848.175
## 3 0.264 0.233 945.139
## 4 0.453 0.259 942.705
## 5 0.619 0.241 853.379
##
## variable M SD min max
## 1 moderator 0.062 0.851 -4.930 1.448
## 2 wgt 0.200 0.066 0.100 0.259
## 3 Neff 833.921 149.294 580.209 945.139
## NULL
# ==============================================================
# 12. Helper: WLS with Within-Model Standardization
# ==============================================================
run_model <- function(formula, data, label) {
all_vars <- all.vars(formula)
var_map <- c(
"g" = "g_raw",
"PC1A_z" = "K5PC1A",
"age_c" = "age",
"ave_WAIS_res_z" = "ave_WAIS_raw",
"M_WAIS_z" = "M_WAIS_res",
"F_WAIS_z" = "F_WAIS_res",
"SES" = "SES_raw"
)
raw_vars <- all_vars
for (i in seq_along(raw_vars)) {
if (raw_vars[i] %in% names(var_map)) {
raw_vars[i] <- var_map[raw_vars[i]]
}
}
d_complete <- data[complete.cases(data[, raw_vars]) & !is.na(data$wt), ]
if ("PC1A_z" %in% all_vars) d_complete$PC1A_z <- as.vector(scale(d_complete$K5PC1A))
if ("g" %in% all_vars) d_complete$g <- as.vector(scale(d_complete$g_raw))
if ("age_c" %in% all_vars) d_complete$age_c <- as.vector(scale(d_complete$age))
if ("ave_WAIS_res_z" %in% all_vars) d_complete$ave_WAIS_res_z <- as.vector(scale(d_complete$ave_WAIS_raw))
if ("M_WAIS_z" %in% all_vars) d_complete$M_WAIS_z <- as.vector(scale(d_complete$M_WAIS_res))
if ("F_WAIS_z" %in% all_vars) d_complete$F_WAIS_z <- as.vector(scale(d_complete$F_WAIS_res))
if ("SES" %in% all_vars) d_complete$SES <- as.vector(scale(d_complete$SES_raw))
for (v in c("PPVT", "WJ9", "WJ10")) {
if (v %in% all_vars && v %in% names(d_complete)) {
d_complete[[v]] <- as.vector(scale(d_complete[[v]]))
}
}
model <- lm(formula, data = d_complete, weights = wt)
robust <- coeftest(model, vcov = vcovHC(model, type = "HC1"))
cat("\n", paste(rep("=", 80), collapse = ""), "\n")
cat(label, "\n")
cat("R2 =", round(summary(model)$r.squared, 4),
" Adj-R2 =", round(summary(model)$adj.r.squared, 4), "\n")
cat(paste(rep("=", 80), collapse = ""), "\n")
cat(sprintf("%20s %9s %8s %8s %10s\n", "Variable", "B", "SE", "t", "p"))
cat(paste(rep("-", 60), collapse = ""), "\n")
for (i in 1:nrow(robust)) {
var_name <- rownames(robust)[i]
b <- robust[i, 1]; se <- robust[i, 2]; t <- robust[i, 3]; p <- robust[i, 4]
p_str <- ifelse(p >= 0.0001, sprintf("%.4f", p), sprintf("%.2e", p))
cat(sprintf("%20s %+9.4f %8.4f %+8.3f %10s\n", var_name, b, se, t, p_str))
}
cat("N =", nrow(d_complete), "\n")
invisible(model)
}
# ==============================================================
# 13. APA: Child g Models
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("AFRO-PAN-AMERICAN: Child g as DV\n")
## AFRO-PAN-AMERICAN: Child g as DV
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(g ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c, d_apa,
"APA M1: PC1A + SIRE + Generation + Demographics")
##
## ================================================================================
## APA M1: PC1A + SIRE + Generation + Demographics
## R2 = 0.0194 Adj-R2 = 0.0142
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0061 0.0553 +0.110 0.9121
## PC1A_z -0.1298 0.0474 -2.737 0.0063
## sireBW -0.3055 0.2117 -1.443 0.1491
## sireBH +0.1142 0.1866 +0.612 0.5407
## sireBO +0.0059 0.1435 +0.041 0.9673
## gen_2ndTRUE +0.4220 0.2193 +1.925 0.0545
## gen_3rdTRUE +0.2412 0.2361 +1.022 0.3070
## sex -0.0111 0.0679 -0.164 0.8699
## age_c -0.0178 0.0305 -0.584 0.5591
## N = 1538
run_model(g ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c + ave_WAIS_res_z, d_apa,
"APA M2: + Parental WAIS")
##
## ================================================================================
## APA M2: + Parental WAIS
## R2 = 0.0396 Adj-R2 = 0.0336
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0115 0.0576 +0.199 0.8424
## PC1A_z -0.1041 0.0480 -2.166 0.0304
## sireBW -0.3094 0.2209 -1.401 0.1616
## sireBH +0.1641 0.2119 +0.774 0.4390
## sireBO +0.0945 0.1384 +0.683 0.4946
## gen_2ndTRUE +0.4598 0.3051 +1.507 0.1320
## gen_3rdTRUE +0.3124 0.2421 +1.290 0.1971
## sex -0.0172 0.0712 -0.242 0.8091
## age_c -0.0206 0.0313 -0.659 0.5100
## ave_WAIS_res_z +0.1514 0.0345 +4.391 1.21e-05
## N = 1450
run_model(g ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c + ave_WAIS_res_z + single + SES, d_apa,
"APA M3: + SES + Single Parent")
##
## ================================================================================
## APA M3: + SES + Single Parent
## R2 = 0.1366 Adj-R2 = 0.13
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0032 0.0698 -0.046 0.9635
## PC1A_z -0.0812 0.0470 -1.729 0.0841
## sireBW -0.1897 0.2131 -0.890 0.3735
## sireBH +0.3436 0.1946 +1.765 0.0777
## sireBO +0.1111 0.1284 +0.865 0.3873
## gen_2ndTRUE +0.2863 0.2525 +1.134 0.2571
## gen_3rdTRUE +0.2614 0.2659 +0.983 0.3259
## sex +0.0054 0.0671 +0.081 0.9354
## age_c -0.0052 0.0296 -0.174 0.8618
## ave_WAIS_res_z +0.0695 0.0345 +2.011 0.0445
## single -0.0461 0.0642 -0.719 0.4724
## SES +0.3185 0.0311 +10.228 9.48e-24
## N = 1450
# ==============================================================
# 13b. Unweighted Model Sensitivity
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("ROBUSTNESS: Unweighted models\n")
## ROBUSTNESS: Unweighted models
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
d_uw <- d_apa[complete.cases(d_apa[, c("g_raw", "K5PC1A", "sire",
"gen_2nd", "gen_3rd", "sex", "age")]), ]
d_uw$PC1A_z <- as.vector(scale(d_uw$K5PC1A))
d_uw$g <- as.vector(scale(d_uw$g_raw))
d_uw$age_c <- as.vector(scale(d_uw$age))
m_uw <- lm(g ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c, data = d_uw)
robust_uw <- coeftest(m_uw, vcov = vcovHC(m_uw, type = "HC1"))
cat("APA unweighted M1: PC1A coefficient:\n")
## APA unweighted M1: PC1A coefficient:
print(robust_uw["PC1A_z", ])
## Estimate Std. Error t value Pr(>|t|)
## -0.119666159 0.037005695 -3.233722753 0.001247956
cat("N =", nrow(d_uw), "\n")
## N = 1538
# ==============================================================
# 14. APA: Average Parental WAIS as DV (excluding BW)
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("APA (excl BW): Parental WAIS as DV\n")
## APA (excl BW): Parental WAIS as DV
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(ave_WAIS_res_z ~ PC1A_z + sire + gen_2nd + gen_3rd, d_apa_noBW,
"APA WAIS M1: PC1A + SIRE + Generation")
##
## ================================================================================
## APA WAIS M1: PC1A + SIRE + Generation
## R2 = 0.0151 Adj-R2 = 0.0116
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0558 0.0392 +1.423 0.1549
## PC1A_z -0.1165 0.0358 -3.259 0.0011
## sireBH -0.2848 0.1385 -2.057 0.0399
## sireBO -0.2225 0.1519 -1.465 0.1433
## gen_2ndTRUE +0.0986 0.1309 +0.754 0.4512
## gen_3rdTRUE -0.4637 0.2154 -2.153 0.0315
## N = 1393
# ==============================================================
# 15. APA: Mother & Father WAIS separately (full APA, parent self-ID)
# ==============================================================
# Use parent's own racial self-identification, not couple SIRE.
# Full APA sample (including BW) — child ancestry is the predictor,
# so both parents' WAIS scores are informative in their own models.
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("APA: Mother & Father WAIS separately (full sample, parent self-ID)\n")
## APA: Mother & Father WAIS separately (full sample, parent self-ID)
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(M_WAIS_z ~ PC1A_z + mother_black_sire + gen_2nd + gen_3rd, d_apa,
"APA Mother WAIS M1 (full sample, parent self-ID)")
##
## ================================================================================
## APA Mother WAIS M1 (full sample, parent self-ID)
## R2 = 0.0102 Adj-R2 = 0.0074
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0564 0.1570 +0.359 0.7194
## PC1A_z -0.1002 0.0494 -2.028 0.0428
## mother_black_sire -0.0273 0.1663 -0.164 0.8697
## gen_2ndTRUE +0.0956 0.2313 +0.413 0.6794
## gen_3rdTRUE -0.0389 0.3212 -0.121 0.9035
## N = 1410
run_model(F_WAIS_z ~ PC1A_z + father_black_sire + gen_2nd + gen_3rd, d_apa,
"APA Father WAIS M1 (full sample, parent self-ID)")
##
## ================================================================================
## APA Father WAIS M1 (full sample, parent self-ID)
## R2 = 0.0116 Adj-R2 = 0.0079
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0502 0.1120 +0.448 0.6541
## PC1A_z -0.0816 0.0315 -2.590 0.0097
## father_black_sire -0.0259 0.1179 -0.220 0.8260
## gen_2ndTRUE -0.0114 0.1315 -0.086 0.9311
## gen_3rdTRUE -0.7400 0.3857 -1.919 0.0553
## N = 1076
# ==============================================================
# 16. APA: SES as DV (excluding BW)
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("APA (excl BW): SES as DV\n")
## APA (excl BW): SES as DV
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(SES ~ PC1A_z + sire + gen_2nd + gen_3rd, d_apa_noBW,
"APA SES M1")
##
## ================================================================================
## APA SES M1
## R2 = 0.0228 Adj-R2 = 0.0195
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0701 0.0359 +1.953 0.0510
## PC1A_z -0.0980 0.0354 -2.768 0.0057
## sireBH -0.5652 0.1762 -3.208 0.0014
## sireBO -0.1239 0.1089 -1.138 0.2554
## gen_2ndTRUE +0.5087 0.1679 +3.030 0.0025
## gen_3rdTRUE +0.0188 0.1853 +0.101 0.9194
## N = 1477
# ==============================================================
# 17. APA: Individual Tests (Model 1 spec)
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("APA: Individual Tests (Model 1 spec)\n")
## APA: Individual Tests (Model 1 spec)
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(PPVT ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c, d_apa, "APA: PPVT")
##
## ================================================================================
## APA: PPVT
## R2 = 0.0243 Adj-R2 = 0.0192
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0165 0.0526 -0.314 0.7537
## PC1A_z -0.1618 0.0508 -3.188 0.0015
## sireBW -0.3483 0.2267 -1.537 0.1246
## sireBH +0.1403 0.2376 +0.590 0.5551
## sireBO -0.0264 0.1595 -0.166 0.8684
## gen_2ndTRUE +0.3827 0.2128 +1.799 0.0723
## gen_3rdTRUE +0.0645 0.3968 +0.163 0.8709
## sex +0.0210 0.0661 +0.319 0.7501
## age_c -0.0286 0.0316 -0.905 0.3658
## N = 1531
run_model(WJ9 ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c, d_apa, "APA: WJ Applied Problems")
##
## ================================================================================
## APA: WJ Applied Problems
## R2 = 0.0315 Adj-R2 = 0.0264
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1509 0.0538 -2.803 0.0051
## PC1A_z -0.0791 0.0452 -1.750 0.0803
## sireBW -0.2980 0.2103 -1.417 0.1567
## sireBH -0.0106 0.1556 -0.068 0.9456
## sireBO +0.0998 0.1559 +0.640 0.5221
## gen_2ndTRUE +0.2954 0.1849 +1.597 0.1104
## gen_3rdTRUE +0.1111 0.1836 +0.605 0.5453
## sex +0.3031 0.0641 +4.731 2.44e-06
## age_c -0.0496 0.0306 -1.618 0.1059
## N = 1523
run_model(WJ10 ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c, d_apa, "APA: WJ Letter-Word")
##
## ================================================================================
## APA: WJ Letter-Word
## R2 = 0.0222 Adj-R2 = 0.0171
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0372 0.0569 -0.655 0.5128
## PC1A_z -0.1014 0.0438 -2.315 0.0208
## sireBW -0.1976 0.1991 -0.992 0.3211
## sireBH +0.0681 0.1506 +0.452 0.6511
## sireBO -0.0490 0.1343 -0.365 0.7151
## gen_2ndTRUE +0.3742 0.2112 +1.772 0.0766
## gen_3rdTRUE +0.4718 0.1862 +2.533 0.0114
## sex +0.1105 0.0686 +1.612 0.1072
## age_c -0.0881 0.0293 -3.004 0.0027
## N = 1524
# ==============================================================
# 18. HAA: Child g Models
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("HISTORICAL AFRICAN AMERICAN: Child g as DV\n")
## HISTORICAL AFRICAN AMERICAN: Child g as DV
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(g ~ PC1A_z + sex + age_c, d_haa, "HAA M1: PC1A + Demographics")
##
## ================================================================================
## HAA M1: PC1A + Demographics
## R2 = 0.009 Adj-R2 = 0.0066
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0034 0.0589 +0.058 0.9540
## PC1A_z -0.1012 0.0317 -3.193 0.0014
## sex +0.0263 0.0727 +0.362 0.7177
## age_c +0.0062 0.0332 +0.185 0.8529
## N = 1287
run_model(g ~ PC1A_z + sex + age_c + ave_WAIS_res_z, d_haa, "HAA M2: + Parental WAIS")
##
## ================================================================================
## HAA M2: + Parental WAIS
## R2 = 0.036 Adj-R2 = 0.0329
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0136 0.0613 -0.221 0.8252
## PC1A_z -0.0934 0.0327 -2.858 0.0043
## sex +0.0559 0.0757 +0.738 0.4607
## age_c +0.0032 0.0334 +0.097 0.9226
## ave_WAIS_res_z +0.1675 0.0368 +4.548 5.95e-06
## N = 1234
run_model(g ~ PC1A_z + sex + age_c + ave_WAIS_res_z + single + SES, d_haa, "HAA M3: + SES + Single")
##
## ================================================================================
## HAA M3: + SES + Single
## R2 = 0.1217 Adj-R2 = 0.1174
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0062 0.0775 -0.080 0.9359
## PC1A_z -0.0786 0.0323 -2.434 0.0151
## sex +0.0605 0.0731 +0.827 0.4081
## age_c +0.0187 0.0325 +0.574 0.5661
## ave_WAIS_res_z +0.0877 0.0365 +2.403 0.0164
## single -0.0642 0.0710 -0.904 0.3659
## SES +0.2925 0.0300 +9.764 9.69e-22
## N = 1234
# ==============================================================
# 19. HAA: Parental WAIS as DV
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("HAA: Parental WAIS as DV\n")
## HAA: Parental WAIS as DV
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(ave_WAIS_res_z ~ PC1A_z, d_haa, "HAA WAIS M1")
##
## ================================================================================
## HAA WAIS M1
## R2 = 0.0091 Adj-R2 = 0.0083
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0456 0.0399 +1.144 0.2530
## PC1A_z -0.0979 0.0389 -2.514 0.0121
## N = 1238
# ==============================================================
# 20. HAA: SES as DV
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("HAA: SES as DV\n")
## HAA: SES as DV
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(SES ~ PC1A_z, d_haa, "HAA SES M1")
##
## ================================================================================
## HAA SES M1
## R2 = 0.0062 Adj-R2 = 0.0055
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0787 0.0371 +2.122 0.0340
## PC1A_z -0.0836 0.0400 -2.090 0.0368
## N = 1292
# ==============================================================
# 21. HAA: Individual Tests (Model 1 spec)
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("HAA: Individual Tests (Model 1 spec)\n")
## HAA: Individual Tests (Model 1 spec)
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(PPVT ~ PC1A_z + sex + age_c, d_haa, "HAA: PPVT")
##
## ================================================================================
## HAA: PPVT
## R2 = 0.0158 Adj-R2 = 0.0135
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0442 0.0558 -0.791 0.4290
## PC1A_z -0.1229 0.0317 -3.873 0.0001
## sex +0.1079 0.0719 +1.501 0.1337
## age_c -0.0139 0.0347 -0.400 0.6893
## N = 1281
run_model(WJ9 ~ PC1A_z + sex + age_c, d_haa, "HAA: WJ Applied Problems")
##
## ================================================================================
## HAA: WJ Applied Problems
## R2 = 0.0233 Adj-R2 = 0.021
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1436 0.0565 -2.544 0.0111
## PC1A_z -0.0578 0.0321 -1.803 0.0717
## sex +0.3011 0.0689 +4.372 1.33e-05
## age_c -0.0193 0.0332 -0.579 0.5625
## N = 1273
run_model(WJ10 ~ PC1A_z + sex + age_c, d_haa, "HAA: WJ Letter-Word")
##
## ================================================================================
## HAA: WJ Letter-Word
## R2 = 0.013 Adj-R2 = 0.0107
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0305 0.0605 -0.504 0.6145
## PC1A_z -0.0788 0.0311 -2.534 0.0114
## sex +0.1241 0.0740 +1.677 0.0937
## age_c -0.0770 0.0317 -2.426 0.0154
## N = 1277
# ==============================================================
# 21b. Mediation Analyses
# ==============================================================
# Mediation 1: PC1A -> SES -> child g (SES mediates ancestry-cognition)
# Mediation 2: PC1A -> parental WAIS -> parental SES
# (Mediation 3: PC1A -> parental WAIS -> child g is in Section 21c)
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("MEDIATION ANALYSES\n")
## MEDIATION ANALYSES
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
# Mediation 1: PC1A -> SES -> child g (APA excluding BW)
d_med1_apa <- d_apa_noBW[complete.cases(d_apa_noBW[, c(
"g_raw", "K5PC1A", "SES_raw", "sire", "gen_2nd", "gen_3rd", "sex", "age"
)]) & !is.na(d_apa_noBW$wt), ]
d_med1_apa$PC1A_z <- as.vector(scale(d_med1_apa$K5PC1A))
d_med1_apa$g <- as.vector(scale(d_med1_apa$g_raw))
d_med1_apa$SES <- as.vector(scale(d_med1_apa$SES_raw))
d_med1_apa$age_c <- as.vector(scale(d_med1_apa$age))
# M: PC1A -> SES
med_M1_apa <- lm(SES ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
data = d_med1_apa, weights = wt)
# Y: SES -> g, controlling for PC1A
out_Y1_apa <- lm(g ~ SES + PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
data = d_med1_apa, weights = wt)
set.seed(11111)
med1_apa <- mediate(med_M1_apa, out_Y1_apa, treat = "PC1A_z",
mediator = "SES", boot = TRUE, sims = 1000)
## Running nonparametric bootstrap
summary(med1_apa)
##
## Causal Mediation Analysis
##
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
##
## Estimate 95% CI Lower 95% CI Upper p-value
## ACME -0.0312264 -0.0571902 -0.0067020 0.012 *
## ADE -0.0619238 -0.1256069 0.0019901 0.062 .
## Total Effect -0.0931502 -0.1610801 -0.0261930 0.008 **
## Prop. Mediated 0.3352262 0.0785313 1.0107897 0.016 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Sample Size Used: 1472
##
##
## Simulations: 1000
# Mediation 1: PC1A -> SES -> child g (HAA)
d_med1_haa <- d_haa[complete.cases(d_haa[, c(
"g_raw", "K5PC1A", "SES_raw", "sex", "age"
)]) & !is.na(d_haa$wt), ]
d_med1_haa$PC1A_z <- as.vector(scale(d_med1_haa$K5PC1A))
d_med1_haa$g <- as.vector(scale(d_med1_haa$g_raw))
d_med1_haa$SES <- as.vector(scale(d_med1_haa$SES_raw))
d_med1_haa$age_c <- as.vector(scale(d_med1_haa$age))
med_M1_haa <- lm(SES ~ PC1A_z + sex + age_c,
data = d_med1_haa, weights = wt)
out_Y1_haa <- lm(g ~ SES + PC1A_z + sex + age_c,
data = d_med1_haa, weights = wt)
set.seed(11111)
med1_haa <- mediate(med_M1_haa, out_Y1_haa, treat = "PC1A_z",
mediator = "SES", boot = TRUE, sims = 1000)
## Running nonparametric bootstrap
summary(med1_haa)
##
## Causal Mediation Analysis
##
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
##
## Estimate 95% CI Lower 95% CI Upper p-value
## ACME -0.0251325 -0.0525039 0.0030279 0.074 .
## ADE -0.0760313 -0.1349419 -0.0130390 0.016 *
## Total Effect -0.1011638 -0.1640583 -0.0368243 <2e-16 ***
## Prop. Mediated 0.2484338 -0.0354906 0.7008143 0.074 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Sample Size Used: 1287
##
##
## Simulations: 1000
# Mediation 2: PC1A -> parental WAIS -> parental SES (APA excluding BW)
d_med2_apa <- d_apa_noBW[complete.cases(d_apa_noBW[, c(
"ave_WAIS_raw", "K5PC1A", "SES_raw", "sire", "gen_2nd", "gen_3rd", "sex", "age"
)]) & !is.na(d_apa_noBW$wt), ]
d_med2_apa$PC1A_z <- as.vector(scale(d_med2_apa$K5PC1A))
d_med2_apa$SES <- as.vector(scale(d_med2_apa$SES_raw))
d_med2_apa$W <- as.vector(scale(d_med2_apa$ave_WAIS_raw))
d_med2_apa$age_c <- as.vector(scale(d_med2_apa$age))
# M: PC1A -> parental WAIS
med_M2_apa <- lm(W ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
data = d_med2_apa, weights = wt)
# Y: parental WAIS -> SES, controlling for PC1A
out_Y2_apa <- lm(SES ~ W + PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
data = d_med2_apa, weights = wt)
set.seed(22222)
med2_apa <- mediate(med_M2_apa, out_Y2_apa, treat = "PC1A_z",
mediator = "W", boot = TRUE, sims = 1000)
## Running nonparametric bootstrap
summary(med2_apa)
##
## Causal Mediation Analysis
##
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
##
## Estimate 95% CI Lower 95% CI Upper p-value
## ACME -0.0276413 -0.0463945 -0.0101489 0.006 **
## ADE -0.0487482 -0.1218704 0.0218145 0.168
## Total Effect -0.0763895 -0.1534454 -0.0017425 0.046 *
## Prop. Mediated 0.3618467 0.0102994 1.4462108 0.048 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Sample Size Used: 1392
##
##
## Simulations: 1000
# Mediation 2: PC1A -> parental WAIS -> parental SES (HAA)
d_med2_haa <- d_haa[complete.cases(d_haa[, c(
"ave_WAIS_raw", "K5PC1A", "SES_raw", "sex", "age"
)]) & !is.na(d_haa$wt), ]
d_med2_haa$PC1A_z <- as.vector(scale(d_med2_haa$K5PC1A))
d_med2_haa$SES <- as.vector(scale(d_med2_haa$SES_raw))
d_med2_haa$W <- as.vector(scale(d_med2_haa$ave_WAIS_raw))
d_med2_haa$age_c <- as.vector(scale(d_med2_haa$age))
med_M2_haa <- lm(W ~ PC1A_z + sex + age_c,
data = d_med2_haa, weights = wt)
out_Y2_haa <- lm(SES ~ W + PC1A_z + sex + age_c,
data = d_med2_haa, weights = wt)
set.seed(22222)
med2_haa <- mediate(med_M2_haa, out_Y2_haa, treat = "PC1A_z",
mediator = "W", boot = TRUE, sims = 1000)
## Running nonparametric bootstrap
summary(med2_haa)
##
## Causal Mediation Analysis
##
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
##
## Estimate 95% CI Lower 95% CI Upper p-value
## ACME -0.022874 -0.044549 -0.002494 0.022 *
## ADE -0.051226 -0.126610 0.035647 0.216
## Total Effect -0.074100 -0.153312 0.013584 0.086 .
## Prop. Mediated 0.308693 -0.766854 1.955594 0.104
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Sample Size Used: 1237
##
##
## Simulations: 1000
# ==============================================================
# 21c. Mediation: PC1A -> Parental WAIS -> Child g
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("MEDIATION: PC1A -> Parental WAIS -> Child g\n")
## MEDIATION: PC1A -> Parental WAIS -> Child g
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
# APA (exclude BW)
d_med_pw_apa <- d_apa_noBW[complete.cases(d_apa_noBW[, c(
"g_raw", "K5PC1A", "ave_WAIS_raw", "sire", "gen_2nd", "gen_3rd", "sex", "age"
)]) & !is.na(d_apa_noBW$wt), ]
d_med_pw_apa$PC1A_z <- as.vector(scale(d_med_pw_apa$K5PC1A))
d_med_pw_apa$g <- as.vector(scale(d_med_pw_apa$g_raw))
d_med_pw_apa$W <- as.vector(scale(d_med_pw_apa$ave_WAIS_raw))
d_med_pw_apa$age_c <- as.vector(scale(d_med_pw_apa$age))
# M: PC1A -> parental WAIS
med_M_pw_apa <- lm(W ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
data = d_med_pw_apa, weights = wt)
# Y: parental WAIS -> g, controlling for PC1A
out_Y_pw_apa <- lm(g ~ W + PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
data = d_med_pw_apa, weights = wt)
set.seed(88888)
med_pw_apa <- mediate(med_M_pw_apa, out_Y_pw_apa, treat = "PC1A_z",
mediator = "W", boot = TRUE, sims = 1000)
## Running nonparametric bootstrap
cat("\nAPA (excl BW): PC1A -> Parental WAIS -> Child g\n")
##
## APA (excl BW): PC1A -> Parental WAIS -> Child g
summary(med_pw_apa)
##
## Causal Mediation Analysis
##
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
##
## Estimate 95% CI Lower 95% CI Upper p-value
## ACME -0.0168691 -0.0319366 -0.0050132 0.002 **
## ADE -0.0727504 -0.1380107 -0.0035731 0.032 *
## Total Effect -0.0896195 -0.1572542 -0.0202900 0.016 *
## Prop. Mediated 0.1882302 0.0525785 0.7121987 0.018 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Sample Size Used: 1389
##
##
## Simulations: 1000
# HAA
d_med_pw_haa <- d_haa[complete.cases(d_haa[, c(
"g_raw", "K5PC1A", "ave_WAIS_raw", "sex", "age"
)]) & !is.na(d_haa$wt), ]
d_med_pw_haa$PC1A_z <- as.vector(scale(d_med_pw_haa$K5PC1A))
d_med_pw_haa$g <- as.vector(scale(d_med_pw_haa$g_raw))
d_med_pw_haa$W <- as.vector(scale(d_med_pw_haa$ave_WAIS_raw))
d_med_pw_haa$age_c <- as.vector(scale(d_med_pw_haa$age))
med_M_pw_haa <- lm(W ~ PC1A_z + sex + age_c, data = d_med_pw_haa, weights = wt)
out_Y_pw_haa <- lm(g ~ W + PC1A_z + sex + age_c, data = d_med_pw_haa, weights = wt)
set.seed(88888)
med_pw_haa <- mediate(med_M_pw_haa, out_Y_pw_haa, treat = "PC1A_z",
mediator = "W", boot = TRUE, sims = 1000)
## Running nonparametric bootstrap
cat("\nHAA: PC1A -> Parental WAIS -> Child g\n")
##
## HAA: PC1A -> Parental WAIS -> Child g
summary(med_pw_haa)
##
## Causal Mediation Analysis
##
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
##
## Estimate 95% CI Lower 95% CI Upper p-value
## ACME -0.0143197 -0.0284459 -0.0019528 0.028 *
## ADE -0.0933909 -0.1583070 -0.0267268 0.008 **
## Total Effect -0.1077106 -0.1757673 -0.0427747 <2e-16 ***
## Prop. Mediated 0.1329459 0.0166515 0.4002888 0.028 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Sample Size Used: 1234
##
##
## Simulations: 1000
# ==============================================================
# 21d. Non-Linearity Test: Quadratic Ancestry Term
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("ROBUSTNESS: Quadratic ancestry term\n")
## ROBUSTNESS: Quadratic ancestry term
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
run_model(g ~ PC1A_z + I(PC1A_z^2) + sire + gen_2nd + gen_3rd + sex + age_c,
d_apa, "APA: g ~ PC1A + PC1A² + controls")
##
## ================================================================================
## APA: g ~ PC1A + PC1A² + controls
## R2 = 0.0203 Adj-R2 = 0.0145
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0076 0.0579 -0.131 0.8955
## PC1A_z -0.1040 0.0566 -1.838 0.0662
## I(PC1A_z^2) +0.0236 0.0267 +0.885 0.3764
## sireBW -0.4793 0.2744 -1.747 0.0809
## sireBH +0.1061 0.1914 +0.554 0.5795
## sireBO -0.0081 0.1396 -0.058 0.9537
## gen_2ndTRUE +0.4113 0.2191 +1.877 0.0607
## gen_3rdTRUE +0.2496 0.2353 +1.061 0.2891
## sex -0.0134 0.0680 -0.197 0.8437
## age_c -0.0195 0.0307 -0.635 0.5253
## N = 1538
run_model(g ~ PC1A_z + I(PC1A_z^2) + sex + age_c,
d_haa, "HAA: g ~ PC1A + PC1A² + controls")
##
## ================================================================================
## HAA: g ~ PC1A + PC1A² + controls
## R2 = 0.009 Adj-R2 = 0.0059
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) +0.0014 0.0632 +0.023 0.9817
## PC1A_z -0.0984 0.0433 -2.272 0.0233
## I(PC1A_z^2) +0.0022 0.0158 +0.137 0.8907
## sex +0.0258 0.0722 +0.358 0.7206
## age_c +0.0057 0.0332 +0.172 0.8635
## N = 1287
# Also for PPVT (strongest individual test effect)
run_model(PPVT ~ PC1A_z + I(PC1A_z^2) + sire + gen_2nd + gen_3rd + sex + age_c,
d_apa, "APA: PPVT ~ PC1A + PC1A² + controls")
##
## ================================================================================
## APA: PPVT ~ PC1A + PC1A² + controls
## R2 = 0.0262 Adj-R2 = 0.0205
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0360 0.0559 -0.644 0.5194
## PC1A_z -0.1250 0.0551 -2.270 0.0233
## I(PC1A_z^2) +0.0335 0.0332 +1.011 0.3122
## sireBW -0.5960 0.3582 -1.664 0.0963
## sireBH +0.1288 0.2472 +0.521 0.6023
## sireBO -0.0463 0.1560 -0.297 0.7666
## gen_2ndTRUE +0.3674 0.2137 +1.720 0.0857
## gen_3rdTRUE +0.0764 0.3956 +0.193 0.8470
## sex +0.0178 0.0660 +0.269 0.7877
## age_c -0.0310 0.0316 -0.980 0.3271
## N = 1531
# ==============================================================
# 21f. Complete-case sensitivity check
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("ROBUSTNESS: Complete-case analysis (no imputation)\n")
## ROBUSTNESS: Complete-case analysis (no imputation)
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
# Identify cases with complete raw (non-imputed) data
d_apa_cc <- d_apa[!is.na(d_apa$PPVT) & !is.na(d_apa$WJ9) & !is.na(d_apa$WJ10) &
!is.na(d_apa$K5PC1A) & !is.na(d_apa$sex) & !is.na(d_apa$age) &
!is.na(d_apa$wt) & !is.na(d_apa$M_edu) & !is.na(d_apa$ave_log_inc), ]
# Build g score without imputation (PCA on complete cases)
cog_raw <- as.matrix(d_apa_cc[, c("PPVT_r", "WJ9_r", "WJ10_r")])
cog_raw_z <- apply(cog_raw, 2, scale)
pca_cc <- prcomp(cog_raw_z, center = FALSE, scale. = FALSE)
d_apa_cc$g_cc <- as.vector(pca_cc$x[, 1])
d_apa_cc$PC1A_z <- as.vector(scale(d_apa_cc$K5PC1A))
d_apa_cc$g <- as.vector(scale(d_apa_cc$g_cc))
d_apa_cc$age_c <- as.vector(scale(d_apa_cc$age))
m_cc <- lm(g ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
data = d_apa_cc, weights = wt)
robust_cc <- coeftest(m_cc, vcov = vcovHC(m_cc, type = "HC1"))
cat("APA complete-case: PC1A coefficient:\n")
## APA complete-case: PC1A coefficient:
print(robust_cc["PC1A_z", ])
## Estimate Std. Error t value Pr(>|t|)
## -0.138021014 0.048548511 -2.842950509 0.004530434
cat("N =", nrow(d_apa_cc), "\n")
## N = 1507
# ==============================================================
# 22. Descriptive Table
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("TABLE: Sample Composition\n")
## TABLE: Sample Composition
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
wt_mean <- function(x, w) {
keep <- !is.na(x) & !is.na(w) & w > 0
if (sum(keep) == 0) return(list(m = NA, n = 0))
list(m = weighted.mean(x[keep], w[keep]), n = sum(keep))
}
print_row <- function(label, subset, apa_status, haa_status) {
n <- nrow(subset)
pct <- 100 * n / nrow(d)
pc1a_m <- mean(subset$K5PC1A, na.rm = TRUE)
pc1a_sd <- sd(subset$K5PC1A, na.rm = TRUE)
g_res <- wt_mean(subset$g_raw, subset$wt)
w_res <- wt_mean(subset$ave_WAIS_raw, subset$wt)
g_str <- ifelse(is.na(g_res$m), " --", sprintf("%+.3f", g_res$m))
w_str <- ifelse(is.na(w_res$m), " --", sprintf("%.2f", w_res$m))
sd_str <- ifelse(is.na(pc1a_sd) | n < 2, " --", sprintf("%.4f", pc1a_sd))
cat(sprintf(" %-25s %5d %5.1f%% %+.4f %7s %4s %4s %5d %7s %5d %7s\n",
label, n, pct, pc1a_m, sd_str, apa_status, haa_status,
g_res$n, g_str, w_res$n, w_str))
}
cat(sprintf("\n%-27s %5s %6s %7s %7s %4s %4s %5s %7s %5s %7s\n",
"Category", "N", "%", "PC1A", "SD", "APA", "HAA", "N(g)", "Wt.g", "N(W)", "Wt.W"))
##
## Category N % PC1A SD APA HAA N(g) Wt.g N(W) Wt.W
cat(paste(rep("-", 105), collapse = ""), "\n")
## ---------------------------------------------------------------------------------------------------------
cat("Panel A: Analytic Sample\n")
## Panel A: Analytic Sample
bb_apa <- d[d$any_black_parent & !d$african_origin & d$sire == "BB", ]
print_row("Black x Black", bb_apa, "Yes", "Yes*")
## Black x Black 1418 86.5% +0.0058 0.0128 Yes Yes* 1376 -0.019 1303 0.04
bw_apa <- d[d$any_black_parent & !d$african_origin & d$sire == "BW", ]
print_row("Black x White", bw_apa, "Yes", "Excl")
## Black x White 72 4.4% -0.0671 0.0178 Yes Excl 66 +0.191 62 0.62
bh_apa <- d[d$any_black_parent & !d$african_origin & d$sire == "BH", ]
print_row("Black x Hispanic", bh_apa, "Yes", "Excl")
## Black x Hispanic 32 2.0% -0.0137 0.0304 Yes Excl 32 +0.495 29 -0.26
bo_apa <- d[d$any_black_parent & !d$african_origin & d$sire == "BO", ]
print_row("Black x Other/Missing", bo_apa, "Yes", "Excl")
## Black x Other/Missing 66 4.0% -0.0064 0.0266 Yes Excl 64 +0.177 61 -0.14
cat("\nPanel B: Continental African Origin\n")
##
## Panel B: Continental African Origin
afr <- d[d$any_black_parent & d$african_origin, ]
if (nrow(afr) > 0) print_row("African origin (excl)", afr, "Excl", "Excl")
## African origin (excl) 21 1.3% +0.0031 0.0414 Excl Excl 18 +0.106 11 0.71
cat("\nPanel C: No Black Parent\n")
##
## Panel C: No Black Parent
no_black <- d[!d$any_black_parent, ]
print_row("No Black parent", no_black, "Excl", "Excl")
## No Black parent 30 1.8% -0.0818 0.0418 Excl Excl 28 +0.547 22 0.14
cat(sprintf("\n*HAA = BB 3rd+ gen only: N=%d\n", nrow(d_haa)))
##
## *HAA = BB 3rd+ gen only: N=1323
# ==============================================================
# 23. Regression Plots
# ==============================================================
partial_resid <- function(model, data, xvar) {
r <- residuals(model)
b <- coef(model)[xvar]
r + b * data[[xvar]]
}
plot_ancestry <- function(model, data, xvar, color, ylab, main_title) {
pr <- partial_resid(model, data, xvar)
x <- data[[xvar]]
plot(x, pr, pch = 16, cex = 0.4,
col = adjustcolor("gray30", alpha.f = 0.2),
xlab = "African Ancestry PC (std.)", ylab = ylab, main = main_title)
lo <- loess(pr ~ x, span = 0.75)
x_seq <- seq(min(x), max(x), length.out = 200)
lo_pred <- predict(lo, newdata = data.frame(x = x_seq), se = TRUE)
polygon(c(x_seq, rev(x_seq)),
c(lo_pred$fit + 1.96 * lo_pred$se.fit, rev(lo_pred$fit - 1.96 * lo_pred$se.fit)),
col = adjustcolor(color, alpha.f = 0.15), border = NA)
lines(x_seq, lo_pred$fit, col = color, lwd = 2.5)
abline(lm(pr ~ x), col = color, lwd = 1.5, lty = 2)
b <- coef(model)[xvar]
p <- summary(model)$coefficients[xvar, "Pr(>|t|)"]
p_str <- ifelse(p < 0.001, "p < .001", sprintf("p = %.3f", p))
legend("topright", legend = bquote(beta == .(sprintf("%.3f", b)) ~ "," ~ .(p_str)),
bty = "n", cex = 0.9)
}
d_apa_g <- d_apa[complete.cases(d_apa[, c("g_raw", "K5PC1A", "sire", "gen_2nd", "gen_3rd", "sex", "age")]) & !is.na(d_apa$wt), ]
d_apa_g$g <- as.vector(scale(d_apa_g$g_raw)); d_apa_g$PC1A_z <- as.vector(scale(d_apa_g$K5PC1A)); d_apa_g$age_c <- as.vector(scale(d_apa_g$age))
m_apa_g <- lm(g ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c, data = d_apa_g, weights = d_apa_g$wt)
d_haa_g <- d_haa[complete.cases(d_haa[, c("g_raw", "K5PC1A", "sex", "age")]) & !is.na(d_haa$wt), ]
d_haa_g$g <- as.vector(scale(d_haa_g$g_raw)); d_haa_g$PC1A_z <- as.vector(scale(d_haa_g$K5PC1A)); d_haa_g$age_c <- as.vector(scale(d_haa_g$age))
m_haa_g <- lm(g ~ PC1A_z + sex + age_c, data = d_haa_g, weights = d_haa_g$wt)
d_apa_w <- d_apa_noBW[complete.cases(d_apa_noBW[, c("ave_WAIS_raw", "K5PC1A", "sire", "gen_2nd", "gen_3rd")]) & !is.na(d_apa_noBW$wt), ]
d_apa_w$ave_WAIS_res_z <- as.vector(scale(d_apa_w$ave_WAIS_raw)); d_apa_w$PC1A_z <- as.vector(scale(d_apa_w$K5PC1A))
m_apa_w <- lm(ave_WAIS_res_z ~ PC1A_z + sire + gen_2nd + gen_3rd, data = d_apa_w, weights = d_apa_w$wt)
d_haa_w <- d_haa[complete.cases(d_haa[, c("ave_WAIS_raw", "K5PC1A")]) & !is.na(d_haa$wt), ]
d_haa_w$ave_WAIS_res_z <- as.vector(scale(d_haa_w$ave_WAIS_raw)); d_haa_w$PC1A_z <- as.vector(scale(d_haa_w$K5PC1A))
m_haa_w <- lm(ave_WAIS_res_z ~ PC1A_z, data = d_haa_w, weights = d_haa_w$wt)
pdf("admixture_regression_plots.pdf", width = 10, height = 8)
par(mfrow = c(2, 2), mar = c(4.5, 4.5, 3, 1.5))
plot_ancestry(m_apa_g, d_apa_g, "PC1A_z", "blue", "Child g (z)", "A. Pan-African American: Ancestry -> Child g")
plot_ancestry(m_haa_g, d_haa_g, "PC1A_z", "blue", "Child g (z)", "B. Historical AA: Ancestry -> Child g")
plot_ancestry(m_apa_w, d_apa_w, "PC1A_z", "darkred", "Parental WAIS (z)", "C. Pan-African American: Ancestry -> Parental WAIS")
plot_ancestry(m_haa_w, d_haa_w, "PC1A_z", "darkred", "Parental WAIS (z)", "D. Historical AA: Ancestry -> Parental WAIS")
dev.off()
## png
## 2
cat("\nPlots saved to: admixture_regression_plots.pdf\n")
##
## Plots saved to: admixture_regression_plots.pdf
#_______________________________________________________________________________________
#Exploratory analysis
# ==============================================================
# Epigenetic Clock Analysis
# ==============================================================
epi <- read_sav("C:/Users/mh198/Documents/Data/FFCWS/DS0008/31622-0008-Data.sav")
# Merge with main dataset
d <- merge(d, epi[, c("IDNUM", "K5MK_POAM45", "K5MK_POAM38",
"K5MK_PHENOAGE", "K5MK_GRIM", "K5MK_HORVATH",
"K5MK_SKINBLOOD", "K5MK_PEDBE", "K5MK_HANNUM",
"K5MK_AGE", "K5MK_BATCH")],
by = "IDNUM", all.x = TRUE)
# Clean: set negative codes to NA
epi_vars <- c("K5MK_POAM45", "K5MK_POAM38", "K5MK_PHENOAGE", "K5MK_GRIM",
"K5MK_HORVATH", "K5MK_SKINBLOOD", "K5MK_PEDBE", "K5MK_HANNUM",
"K5MK_AGE", "K5MK_BATCH")
for (v in epi_vars) {
d[[v]] <- ifelse(d[[v]] < -5, NA, d[[v]])
}
cat("Epigenetic data merged. DunedinPACE available: N =", sum(!is.na(d$K5MK_POAM45)), "\n")
## Epigenetic data merged. DunedinPACE available: N = 485
# Rebuild subsets with epi data
d_apa <- d[d$any_black_parent & !d$african_origin, ]
d_apa$sire <- droplevels(d_apa$sire)
d_apa$mother_black_sire <- as.numeric(d_apa$CM1ETHRACE == 2)
d_apa$father_black_sire <- as.numeric(d_apa$CF1ETHRACE == 2)
d_apa_noBW <- d_apa[d_apa$sire != "BW", ]
d_apa_noBW$sire <- droplevels(d_apa_noBW$sire)
d_haa <- d[d$CF1ETHRACE == 2 & d$CM1ETHRACE == 2 &
!d$african_origin & !d$parent_fb & !d$gp_fb, ]
cat("\nDunedinPACE by sample:\n")
##
## DunedinPACE by sample:
cat(sprintf(" APA: N = %d\n", sum(!is.na(d_apa$K5MK_POAM45))))
## APA: N = 469
cat(sprintf(" APA excl BW: N = %d\n", sum(!is.na(d_apa_noBW$K5MK_POAM45))))
## APA excl BW: N = 457
cat(sprintf(" HAA: N = %d\n", sum(!is.na(d_haa$K5MK_POAM45))))
## HAA: N = 400
# ==============================================================
# Full Diagnostics: All Clocks
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("Epigenetic Clocks: Full Diagnostics\n")
## Epigenetic Clocks: Full Diagnostics
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
clock_vars <- c("K5MK_POAM45", "K5MK_POAM38", "K5MK_PHENOAGE", "K5MK_GRIM",
"K5MK_HORVATH", "K5MK_SKINBLOOD", "K5MK_PEDBE", "K5MK_HANNUM")
clock_labels <- c("DunedinPACE", "DunedinPoAm38", "PhenoAge", "GrimAge",
"Horvath", "SkinBlood", "PedBE", "Hannum")
# APA
cat("\nCorrelations: Clocks with Ancestry and Outcomes (APA)\n")
##
## Correlations: Clocks with Ancestry and Outcomes (APA)
cat(sprintf("%-15s %5s %9s %9s %9s %9s %9s %9s\n",
"Clock", "N", "r(PC1A)", "r(g)", "r(PPVT)", "r(WAIS)", "r(SES)", "r(WJ9)"))
## Clock N r(PC1A) r(g) r(PPVT) r(WAIS) r(SES) r(WJ9)
cat(paste(rep("-", 80), collapse = ""), "\n")
## --------------------------------------------------------------------------------
for (i in seq_along(clock_vars)) {
v <- clock_vars[i]
mask_pc <- !is.na(d_apa[[v]]) & !is.na(d_apa$K5PC1A)
mask_g <- !is.na(d_apa[[v]]) & !is.na(d_apa$g_raw)
mask_ppvt <- !is.na(d_apa[[v]]) & !is.na(d_apa$PPVT)
mask_wais <- !is.na(d_apa[[v]]) & !is.na(d_apa$ave_WAIS_raw)
mask_ses <- !is.na(d_apa[[v]]) & !is.na(d_apa$SES_raw)
mask_wj9 <- !is.na(d_apa[[v]]) & !is.na(d_apa$WJ9)
n <- sum(mask_pc)
if (n < 10) next
r_pc <- cor(d_apa[[v]][mask_pc], d_apa$K5PC1A[mask_pc])
r_g <- cor(d_apa[[v]][mask_g], d_apa$g_raw[mask_g])
r_ppvt <- cor(d_apa[[v]][mask_ppvt], d_apa$PPVT[mask_ppvt])
r_wais <- cor(d_apa[[v]][mask_wais], d_apa$ave_WAIS_raw[mask_wais])
r_ses <- cor(d_apa[[v]][mask_ses], d_apa$SES_raw[mask_ses])
r_wj9 <- cor(d_apa[[v]][mask_wj9], d_apa$WJ9[mask_wj9])
cat(sprintf("%-15s %5d %+9.4f %+9.4f %+9.4f %+9.4f %+9.4f %+9.4f\n",
clock_labels[i], n, r_pc, r_g, r_ppvt, r_wais, r_ses, r_wj9))
}
## DunedinPACE 469 +0.1137 +0.0239 +0.0240 -0.0152 -0.0032 -0.0216
## DunedinPoAm38 469 +0.1263 -0.0306 -0.0835 -0.0516 -0.0690 -0.0087
## PhenoAge 146 +0.1252 -0.0189 +0.0486 -0.0096 -0.0241 -0.0142
## GrimAge 469 +0.0509 +0.0325 +0.0539 -0.0018 +0.0066 -0.0270
## Horvath 469 +0.0023 -0.0426 -0.0148 -0.0163 +0.0216 -0.0532
## SkinBlood 469 +0.0584 +0.0090 +0.0161 +0.0119 +0.0255 +0.0338
## PedBE 469 +0.0210 +0.0136 +0.0481 -0.0053 +0.0576 +0.0214
## Hannum 469 +0.0282 +0.0551 +0.0539 -0.0226 +0.0146 -0.0015
# HAA
cat("\nCorrelations: Clocks with Ancestry and Outcomes (HAA)\n")
##
## Correlations: Clocks with Ancestry and Outcomes (HAA)
cat(sprintf("%-15s %5s %9s %9s %9s %9s %9s %9s\n",
"Clock", "N", "r(PC1A)", "r(g)", "r(PPVT)", "r(WAIS)", "r(SES)", "r(WJ9)"))
## Clock N r(PC1A) r(g) r(PPVT) r(WAIS) r(SES) r(WJ9)
cat(paste(rep("-", 80), collapse = ""), "\n")
## --------------------------------------------------------------------------------
for (i in seq_along(clock_vars)) {
v <- clock_vars[i]
mask_pc <- !is.na(d_haa[[v]]) & !is.na(d_haa$K5PC1A)
mask_g <- !is.na(d_haa[[v]]) & !is.na(d_haa$g_raw)
mask_ppvt <- !is.na(d_haa[[v]]) & !is.na(d_haa$PPVT)
mask_wais <- !is.na(d_haa[[v]]) & !is.na(d_haa$ave_WAIS_raw)
mask_ses <- !is.na(d_haa[[v]]) & !is.na(d_haa$SES_raw)
mask_wj9 <- !is.na(d_haa[[v]]) & !is.na(d_haa$WJ9)
n <- sum(mask_pc)
if (n < 10) next
r_pc <- cor(d_haa[[v]][mask_pc], d_haa$K5PC1A[mask_pc])
r_g <- cor(d_haa[[v]][mask_g], d_haa$g_raw[mask_g])
r_ppvt <- cor(d_haa[[v]][mask_ppvt], d_haa$PPVT[mask_ppvt])
r_wais <- cor(d_haa[[v]][mask_wais], d_haa$ave_WAIS_raw[mask_wais])
r_ses <- cor(d_haa[[v]][mask_ses], d_haa$SES_raw[mask_ses])
r_wj9 <- cor(d_haa[[v]][mask_wj9], d_haa$WJ9[mask_wj9])
cat(sprintf("%-15s %5d %+9.4f %+9.4f %+9.4f %+9.4f %+9.4f %+9.4f\n",
clock_labels[i], n, r_pc, r_g, r_ppvt, r_wais, r_ses, r_wj9))
}
## DunedinPACE 400 +0.0907 +0.0322 +0.0410 -0.0125 +0.0004 -0.0218
## DunedinPoAm38 400 +0.0581 -0.0004 -0.0272 -0.0518 -0.0659 +0.0002
## PhenoAge 122 +0.0485 +0.0236 +0.0695 -0.0396 -0.0024 +0.0431
## GrimAge 400 +0.0511 +0.0266 +0.0424 +0.0085 +0.0278 -0.0216
## Horvath 400 -0.0658 -0.0367 -0.0261 -0.0012 +0.0246 -0.0343
## SkinBlood 400 +0.0498 +0.0179 +0.0219 +0.0184 +0.0457 +0.0518
## PedBE 400 +0.0022 -0.0018 +0.0482 -0.0017 +0.0373 +0.0048
## Hannum 400 +0.0337 +0.0570 +0.0568 -0.0110 +0.0207 +0.0067
# ==============================================================
# Mediation: DunedinPACE
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("Mediation: Does DunedinPACE attenuate ancestry-cognition?\n")
## Mediation: Does DunedinPACE attenuate ancestry-cognition?
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
d_apa$PACE_z <- as.vector(scale(d_apa$K5MK_POAM45))
d_haa$PACE_z <- as.vector(scale(d_haa$K5MK_POAM45))
# APA: without PACE (restricted to epi sample)
run_model(g ~ PC1A_z + sire + gen_2nd + gen_3rd + sex + age_c,
d_apa[!is.na(d_apa$K5MK_POAM45), ],
"APA M1 (no PACE, epi subsample)")
##
## ================================================================================
## APA M1 (no PACE, epi subsample)
## R2 = 0.0377 Adj-R2 = 0.0209
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0272 0.1163 -0.234 0.8152
## PC1A_z -0.1603 0.0831 -1.929 0.0543
## sireBW -0.0971 0.4033 -0.241 0.8100
## sireBH +0.3796 0.2193 +1.731 0.0841
## sireBO +0.1759 0.2403 +0.732 0.4644
## gen_2ndTRUE +0.6602 0.3467 +1.904 0.0575
## gen_3rdTRUE +0.0065 0.2571 +0.025 0.9797
## sex +0.0980 0.1255 +0.781 0.4355
## age_c +0.0753 0.0527 +1.429 0.1538
## N = 467
# APA: with PACE
run_model(g ~ PC1A_z + PACE_z + sire + gen_2nd + gen_3rd + sex + age_c, d_apa,
"APA M1 + DunedinPACE")
##
## ================================================================================
## APA M1 + DunedinPACE
## R2 = 0.0382 Adj-R2 = 0.0193
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0309 0.1109 -0.279 0.7806
## PC1A_z -0.1554 0.0839 -1.852 0.0647
## PACE_z -0.0256 0.0845 -0.303 0.7618
## sireBW -0.0851 0.4008 -0.212 0.8320
## sireBH +0.3812 0.2173 +1.754 0.0801
## sireBO +0.1878 0.2329 +0.806 0.4206
## gen_2ndTRUE +0.6770 0.3501 +1.934 0.0537
## gen_3rdTRUE +0.0027 0.2662 +0.010 0.9918
## sex +0.1010 0.1220 +0.828 0.4081
## age_c +0.0757 0.0530 +1.427 0.1543
## N = 467
# HAA: without PACE (restricted to epi sample)
run_model(g ~ PC1A_z + sex + age_c,
d_haa[!is.na(d_haa$K5MK_POAM45), ],
"HAA M1 (no PACE, epi subsample)")
##
## ================================================================================
## HAA M1 (no PACE, epi subsample)
## R2 = 0.0172 Adj-R2 = 0.0097
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0250 0.1190 -0.210 0.8335
## PC1A_z -0.1089 0.0574 -1.896 0.0586
## sex +0.1471 0.1348 +1.091 0.2758
## age_c +0.0907 0.0566 +1.601 0.1102
## N = 398
# HAA: with PACE
run_model(g ~ PC1A_z + PACE_z + sex + age_c, d_haa,
"HAA M1 + DunedinPACE")
##
## ================================================================================
## HAA M1 + DunedinPACE
## R2 = 0.0189 Adj-R2 = 0.0089
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0319 0.1119 -0.285 0.7757
## PC1A_z -0.1048 0.0580 -1.808 0.0714
## PACE_z -0.0461 0.0944 -0.488 0.6255
## sex +0.1563 0.1274 +1.227 0.2207
## age_c +0.0910 0.0568 +1.603 0.1098
## N = 398
# ==============================================================
# Mediation: GrimAge
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("Mediation: Does GrimAge attenuate ancestry-cognition?\n")
## Mediation: Does GrimAge attenuate ancestry-cognition?
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
d_apa$GRIM_z <- as.vector(scale(d_apa$K5MK_GRIM))
d_haa$GRIM_z <- as.vector(scale(d_haa$K5MK_GRIM))
run_model(g ~ PC1A_z + GRIM_z + sire + gen_2nd + gen_3rd + sex + age_c, d_apa,
"APA M1 + GrimAge")
##
## ================================================================================
## APA M1 + GrimAge
## R2 = 0.0377 Adj-R2 = 0.0188
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0273 0.1178 -0.231 0.8171
## PC1A_z -0.1605 0.0834 -1.925 0.0548
## GRIM_z +0.0012 0.0674 +0.018 0.9854
## sireBW -0.0977 0.4027 -0.243 0.8084
## sireBH +0.3796 0.2197 +1.728 0.0847
## sireBO +0.1754 0.2381 +0.737 0.4617
## gen_2ndTRUE +0.6601 0.3467 +1.904 0.0575
## gen_3rdTRUE +0.0063 0.2577 +0.024 0.9806
## sex +0.0982 0.1310 +0.750 0.4538
## age_c +0.0752 0.0531 +1.416 0.1576
## N = 467
run_model(g ~ PC1A_z + GRIM_z + sex + age_c, d_haa,
"HAA M1 + GrimAge")
##
## ================================================================================
## HAA M1 + GrimAge
## R2 = 0.0172 Adj-R2 = 0.0072
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0251 0.1214 -0.207 0.8364
## PC1A_z -0.1090 0.0576 -1.892 0.0592
## GRIM_z +0.0008 0.0739 +0.010 0.9919
## sex +0.1473 0.1397 +1.054 0.2925
## age_c +0.0906 0.0571 +1.587 0.1134
## N = 398
# ==============================================================
# Mediation: All clocks (loop)
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("Mediation Summary: All Clocks (HAA)\n")
## Mediation Summary: All Clocks (HAA)
cat("PC1A_z coefficient with and without each clock\n")
## PC1A_z coefficient with and without each clock
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
cat(sprintf("\n%-15s %5s %9s %9s %9s\n",
"Clock", "N", "PC1A(no)", "PC1A(+clk)", "Change"))
##
## Clock N PC1A(no) PC1A(+clk) Change
cat(paste(rep("-", 50), collapse = ""), "\n")
## --------------------------------------------------
for (i in seq_along(clock_vars)) {
v <- clock_vars[i]
dd <- d_haa[!is.na(d_haa[[v]]) & !is.na(d_haa$g_raw) &
!is.na(d_haa$K5PC1A) & !is.na(d_haa$age) &
!is.na(d_haa$sex) & !is.na(d_haa$wt), ]
if (nrow(dd) < 30) next
dd$PC1A_z <- as.vector(scale(dd$K5PC1A))
dd$g <- as.vector(scale(dd$g_raw))
dd$age_c <- as.vector(scale(dd$age))
dd$clock_z <- as.vector(scale(dd[[v]]))
m0 <- lm(g ~ PC1A_z + sex + age_c, data = dd, weights = wt)
m1 <- lm(g ~ PC1A_z + clock_z + sex + age_c, data = dd, weights = wt)
b0 <- coef(m0)["PC1A_z"]
b1 <- coef(m1)["PC1A_z"]
change_pct <- 100 * (b1 - b0) / b0
cat(sprintf("%-15s %5d %+9.4f %+9.4f %+8.1f%%\n",
clock_labels[i], nrow(dd), b0, b1, change_pct))
}
## DunedinPACE 398 -0.1089 -0.1048 -3.8%
## DunedinPoAm38 398 -0.1089 -0.1077 -1.1%
## PhenoAge 121 -0.1080 -0.1085 +0.5%
## GrimAge 398 -0.1089 -0.1090 +0.0%
## Horvath 398 -0.1089 -0.1150 +5.6%
## SkinBlood 398 -0.1089 -0.1087 -0.2%
## PedBE 398 -0.1089 -0.1094 +0.4%
## Hannum 398 -0.1089 -0.1090 +0.0%
# ==============================================================
# Epigenetic Formal Mediation: PC1A -> DunedinPACE -> g
# ==============================================================
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("FORMAL MEDIATION: PC1A -> DunedinPACE -> g (HAA)\n")
## FORMAL MEDIATION: PC1A -> DunedinPACE -> g (HAA)
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
d_epi_haa <- d_haa[!is.na(d_haa$K5MK_POAM45) & !is.na(d_haa$g_raw) &
!is.na(d_haa$K5PC1A) & !is.na(d_haa$age) &
!is.na(d_haa$sex) & !is.na(d_haa$wt), ]
d_epi_haa$PC1A_z <- as.vector(scale(d_epi_haa$K5PC1A))
d_epi_haa$g <- as.vector(scale(d_epi_haa$g_raw))
d_epi_haa$PACE_z <- as.vector(scale(d_epi_haa$K5MK_POAM45))
d_epi_haa$age_c <- as.vector(scale(d_epi_haa$age))
med_M_epi <- lm(PACE_z ~ PC1A_z + sex + age_c, data = d_epi_haa, weights = wt)
out_Y_epi <- lm(g ~ PACE_z + PC1A_z + sex + age_c, data = d_epi_haa, weights = wt)
set.seed(77777)
med_epi <- mediate(med_M_epi, out_Y_epi, treat = "PC1A_z", mediator = "PACE_z",
boot = TRUE, sims = 1000)
## Running nonparametric bootstrap
summary(med_epi)
##
## Causal Mediation Analysis
##
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
##
## Estimate 95% CI Lower 95% CI Upper p-value
## ACME -0.0041069 -0.0300480 0.0137365 0.680
## ADE -0.1048220 -0.2179324 0.0066505 0.068 .
## Total Effect -0.1089289 -0.2197694 0.0050707 0.060 .
## Prop. Mediated 0.0377025 -0.2252410 0.5003497 0.684
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Sample Size Used: 398
##
##
## Simulations: 1000
################
#"Epigenetic pace-of-aging measures (DunedinPACE, DunedinPoAm38, GrimAge, Horvath, and others) did not attenuate the ancestry-cognition association (maximum change: 3.6%), and showed near-zero correlations with cognitive outcomes (|r| < .05 for all clocks).
#These results do not support epigenetic aging as a mediator of ancestry-related cognitive differences in this sample."
# ==============================================================
# SM-A. Supplementary Material: Moderation Analyses
# ==============================================================
# Note: These use individual subtests (PPVT, WJ9, WJ10) as outcomes to avoid measurement invariance assumptions.
cat("\n", paste(rep("#", 80), collapse = ""), "\n")
##
## ################################################################################
cat("SUPPLEMENTARY MATERIAL: Moderation Analyses\n")
## SUPPLEMENTARY MATERIAL: Moderation Analyses
cat("(Using individual subtests to avoid MI assumptions)\n")
## (Using individual subtests to avoid MI assumptions)
cat(paste(rep("#", 80), collapse = ""), "\n")
## ################################################################################
# SM-A1: Sex × Ancestry on individual tests
cat("\n--- SM-A1: Sex × Ancestry (APA) ---\n")
##
## --- SM-A1: Sex × Ancestry (APA) ---
for (test in c("PPVT", "WJ9", "WJ10")) {
run_model(
as.formula(paste(test, "~ PC1A_z * sex + sire + gen_2nd + gen_3rd + age_c")),
d_apa,
paste("APA:", test, "~ PC1A × Sex")
)
}
##
## ================================================================================
## APA: PPVT ~ PC1A × Sex
## R2 = 0.0309 Adj-R2 = 0.0252
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0198 0.0524 -0.379 0.7051
## PC1A_z -0.2424 0.0594 -4.083 4.68e-05
## sex +0.0161 0.0659 +0.244 0.8075
## sireBW -0.3294 0.2261 -1.457 0.1453
## sireBH +0.1358 0.2210 +0.615 0.5389
## sireBO -0.0315 0.1551 -0.203 0.8389
## gen_2ndTRUE +0.4156 0.2080 +1.998 0.0459
## gen_3rdTRUE +0.0574 0.3918 +0.147 0.8835
## age_c -0.0292 0.0312 -0.935 0.3501
## PC1A_z:sex +0.1706 0.0648 +2.631 0.0086
## N = 1531
##
## ================================================================================
## APA: WJ9 ~ PC1A × Sex
## R2 = 0.0316 Adj-R2 = 0.0258
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1513 0.0539 -2.808 0.0050
## PC1A_z -0.0884 0.0567 -1.559 0.1192
## sex +0.3026 0.0640 +4.730 2.45e-06
## sireBW -0.2953 0.2105 -1.402 0.1610
## sireBH -0.0111 0.1546 -0.072 0.9429
## sireBO +0.0993 0.1559 +0.637 0.5242
## gen_2ndTRUE +0.2991 0.1850 +1.617 0.1061
## gen_3rdTRUE +0.1102 0.1838 +0.600 0.5486
## age_c -0.0496 0.0306 -1.620 0.1054
## PC1A_z:sex +0.0198 0.0555 +0.357 0.7208
## N = 1523
##
## ================================================================================
## APA: WJ10 ~ PC1A × Sex
## R2 = 0.024 Adj-R2 = 0.0182
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0388 0.0569 -0.683 0.4948
## PC1A_z -0.1437 0.0549 -2.619 0.0089
## sex +0.1079 0.0682 +1.580 0.1142
## sireBW -0.1853 0.1964 -0.943 0.3456
## sireBH +0.0660 0.1500 +0.440 0.6600
## sireBO -0.0518 0.1332 -0.389 0.6975
## gen_2ndTRUE +0.3916 0.2110 +1.856 0.0637
## gen_3rdTRUE +0.4688 0.1826 +2.567 0.0103
## age_c -0.0884 0.0292 -3.024 0.0025
## PC1A_z:sex +0.0901 0.0561 +1.606 0.1084
## N = 1524
cat("\n--- SM-A1: Sex × Ancestry (HAA) ---\n")
##
## --- SM-A1: Sex × Ancestry (HAA) ---
for (test in c("PPVT", "WJ9", "WJ10")) {
run_model(
as.formula(paste(test, "~ PC1A_z * sex + age_c")),
d_haa,
paste("HAA:", test, "~ PC1A × Sex")
)
}
##
## ================================================================================
## HAA: PPVT ~ PC1A × Sex
## R2 = 0.0191 Adj-R2 = 0.016
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0483 0.0559 -0.865 0.3872
## PC1A_z -0.1780 0.0443 -4.018 6.22e-05
## sex +0.1032 0.0719 +1.435 0.1515
## age_c -0.0161 0.0344 -0.466 0.6410
## PC1A_z:sex +0.1206 0.0623 +1.938 0.0529
## N = 1281
##
## ================================================================================
## HAA: WJ9 ~ PC1A × Sex
## R2 = 0.0234 Adj-R2 = 0.0203
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1429 0.0562 -2.541 0.0112
## PC1A_z -0.0484 0.0494 -0.981 0.3269
## sex +0.3018 0.0689 +4.382 1.27e-05
## age_c -0.0189 0.0333 -0.569 0.5693
## PC1A_z:sex -0.0207 0.0623 -0.333 0.7394
## N = 1273
##
## ================================================================================
## HAA: WJ10 ~ PC1A × Sex
## R2 = 0.0161 Adj-R2 = 0.013
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0342 0.0603 -0.568 0.5699
## PC1A_z -0.1331 0.0457 -2.916 0.0036
## sex +0.1195 0.0736 +1.623 0.1047
## age_c -0.0790 0.0315 -2.506 0.0123
## PC1A_z:sex +0.1198 0.0601 +1.992 0.0466
## N = 1277
# SM-A2: Age × Ancestry on individual tests
# Note: Age is already controlled for linearly in the main models.
# Here we test whether the ancestry effect varies with age.
# And we use the raw (non-centered) age to make the interaction interpretable.
cat("\n--- SM-A2: Age × Ancestry (APA) ---\n")
##
## --- SM-A2: Age × Ancestry (APA) ---
for (test in c("PPVT", "WJ9", "WJ10")) {
run_model(
as.formula(paste(test, "~ PC1A_z * age_c + sire + gen_2nd + gen_3rd + sex")),
d_apa,
paste("APA:", test, "~ PC1A × Age")
)
}
##
## ================================================================================
## APA: PPVT ~ PC1A × Age
## R2 = 0.0246 Adj-R2 = 0.0189
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0159 0.0526 -0.301 0.7631
## PC1A_z -0.1636 0.0510 -3.206 0.0014
## age_c -0.0286 0.0318 -0.900 0.3683
## sireBW -0.3650 0.2272 -1.606 0.1084
## sireBH +0.1366 0.2376 +0.575 0.5653
## sireBO -0.0259 0.1606 -0.161 0.8719
## gen_2ndTRUE +0.3846 0.2133 +1.803 0.0715
## gen_3rdTRUE +0.0633 0.3953 +0.160 0.8729
## sex +0.0211 0.0661 +0.319 0.7495
## PC1A_z:age_c +0.0175 0.0309 +0.566 0.5715
## N = 1531
##
## ================================================================================
## APA: WJ9 ~ PC1A × Age
## R2 = 0.032 Adj-R2 = 0.0263
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1500 0.0539 -2.781 0.0055
## PC1A_z -0.0815 0.0453 -1.799 0.0723
## age_c -0.0494 0.0306 -1.614 0.1067
## sireBW -0.3210 0.2091 -1.535 0.1250
## sireBH -0.0155 0.1554 -0.100 0.9207
## sireBO +0.1005 0.1556 +0.646 0.5184
## gen_2ndTRUE +0.2977 0.1854 +1.605 0.1086
## gen_3rdTRUE +0.1093 0.1847 +0.592 0.5540
## sex +0.3032 0.0641 +4.730 2.45e-06
## PC1A_z:age_c +0.0237 0.0240 +0.987 0.3236
## N = 1523
##
## ================================================================================
## APA: WJ10 ~ PC1A × Age
## R2 = 0.0224 Adj-R2 = 0.0166
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0379 0.0569 -0.666 0.5058
## PC1A_z -0.0999 0.0444 -2.249 0.0247
## age_c -0.0881 0.0292 -3.014 0.0026
## sireBW -0.1829 0.2003 -0.913 0.3615
## sireBH +0.0713 0.1505 +0.474 0.6358
## sireBO -0.0495 0.1348 -0.367 0.7134
## gen_2ndTRUE +0.3727 0.2102 +1.773 0.0765
## gen_3rdTRUE +0.4732 0.1872 +2.528 0.0116
## sex +0.1105 0.0686 +1.612 0.1072
## PC1A_z:age_c -0.0153 0.0228 -0.673 0.5013
## N = 1524
cat("\n--- SM-A2: Age × Ancestry (HAA) ---\n")
##
## --- SM-A2: Age × Ancestry (HAA) ---
for (test in c("PPVT", "WJ9", "WJ10")) {
run_model(
as.formula(paste(test, "~ PC1A_z * age_c + sex")),
d_haa,
paste("HAA:", test, "~ PC1A × Age")
)
}
##
## ================================================================================
## HAA: PPVT ~ PC1A × Age
## R2 = 0.0158 Adj-R2 = 0.0127
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0442 0.0559 -0.791 0.4290
## PC1A_z -0.1227 0.0316 -3.887 0.0001
## age_c -0.0138 0.0347 -0.397 0.6914
## sex +0.1079 0.0719 +1.502 0.1334
## PC1A_z:age_c -0.0024 0.0266 -0.090 0.9281
## N = 1281
##
## ================================================================================
## HAA: WJ9 ~ PC1A × Age
## R2 = 0.0236 Adj-R2 = 0.0205
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1437 0.0565 -2.545 0.0111
## PC1A_z -0.0566 0.0323 -1.754 0.0796
## age_c -0.0189 0.0334 -0.565 0.5720
## sex +0.3013 0.0689 +4.373 1.33e-05
## PC1A_z:age_c -0.0162 0.0264 -0.611 0.5410
## N = 1273
##
## ================================================================================
## HAA: WJ10 ~ PC1A × Age
## R2 = 0.0139 Adj-R2 = 0.0108
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0306 0.0606 -0.506 0.6130
## PC1A_z -0.0766 0.0312 -2.450 0.0144
## age_c -0.0761 0.0315 -2.416 0.0158
## sex +0.1249 0.0741 +1.686 0.0921
## PC1A_z:age_c -0.0298 0.0280 -1.065 0.2873
## N = 1277
# SM-A3: Parental SES × Ancestry on individual tests
# SES is continuous (standardized). Test whether ancestry effects differ across the SES distribution.
cat("\n--- SM-A3: SES × Ancestry (APA, excl BW) ---\n")
##
## --- SM-A3: SES × Ancestry (APA, excl BW) ---
for (test in c("PPVT", "WJ9", "WJ10")) {
run_model(
as.formula(paste(test, "~ PC1A_z * SES + sire + gen_2nd + gen_3rd + sex + age_c")),
d_apa_noBW,
paste("APA (excl BW):", test, "~ PC1A × SES")
)
}
##
## ================================================================================
## APA (excl BW): PPVT ~ PC1A × SES
## R2 = 0.1289 Adj-R2 = 0.1235
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0496 0.0502 -0.989 0.3230
## PC1A_z -0.0971 0.0334 -2.904 0.0037
## SES +0.3189 0.0309 +10.326 3.59e-24
## sireBH +0.3249 0.2372 +1.369 0.1711
## sireBO +0.0253 0.1484 +0.170 0.8648
## gen_2ndTRUE +0.2068 0.1939 +1.067 0.2862
## gen_3rdTRUE +0.0495 0.4177 +0.118 0.9058
## sex +0.0392 0.0650 +0.602 0.5472
## age_c -0.0074 0.0309 -0.238 0.8119
## PC1A_z:SES +0.0516 0.0344 +1.499 0.1340
## N = 1466
##
## ================================================================================
## APA (excl BW): WJ9 ~ PC1A × SES
## R2 = 0.0921 Adj-R2 = 0.0864
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1829 0.0514 -3.558 0.0004
## PC1A_z -0.0317 0.0341 -0.929 0.3530
## SES +0.2502 0.0300 +8.336 1.77e-16
## sireBH +0.1219 0.1585 +0.769 0.4422
## sireBO +0.1163 0.1499 +0.775 0.4382
## gen_2ndTRUE +0.1671 0.1663 +1.005 0.3150
## gen_3rdTRUE +0.0974 0.1980 +0.492 0.6230
## sex +0.3143 0.0634 +4.956 8.05e-07
## age_c -0.0295 0.0303 -0.974 0.3303
## PC1A_z:SES -0.0153 0.0260 -0.587 0.5570
## N = 1457
##
## ================================================================================
## APA (excl BW): WJ10 ~ PC1A × SES
## R2 = 0.0991 Adj-R2 = 0.0935
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0695 0.0528 -1.316 0.1883
## PC1A_z -0.0498 0.0317 -1.569 0.1169
## SES +0.2823 0.0315 +8.958 9.95e-19
## sireBH +0.2222 0.1427 +1.558 0.1195
## sireBO -0.0205 0.1278 -0.160 0.8727
## gen_2ndTRUE +0.2258 0.1887 +1.197 0.2316
## gen_3rdTRUE +0.4599 0.2009 +2.289 0.0222
## sex +0.1280 0.0658 +1.946 0.0518
## age_c -0.0723 0.0285 -2.541 0.0112
## PC1A_z:SES +0.0068 0.0248 +0.274 0.7841
## N = 1458
cat("\n--- SM-A3: SES × Ancestry (HAA) ---\n")
##
## --- SM-A3: SES × Ancestry (HAA) ---
for (test in c("PPVT", "WJ9", "WJ10")) {
run_model(
as.formula(paste(test, "~ PC1A_z * SES + sex + age_c")),
d_haa,
paste("HAA:", test, "~ PC1A × SES")
)
}
##
## ================================================================================
## HAA: PPVT ~ PC1A × SES
## R2 = 0.1188 Adj-R2 = 0.1153
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0740 0.0530 -1.396 0.1630
## PC1A_z -0.0983 0.0298 -3.298 0.0010
## SES +0.3161 0.0306 +10.319 4.93e-24
## sex +0.1225 0.0691 +1.771 0.0767
## age_c +0.0053 0.0333 +0.161 0.8725
## PC1A_z:SES +0.0015 0.0289 +0.053 0.9575
## N = 1281
##
## ================================================================================
## HAA: WJ9 ~ PC1A × SES
## R2 = 0.0795 Adj-R2 = 0.0758
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.1659 0.0547 -3.033 0.0025
## PC1A_z -0.0366 0.0329 -1.110 0.2673
## SES +0.2373 0.0307 +7.731 2.17e-14
## sex +0.3107 0.0672 +4.620 4.23e-06
## age_c -0.0042 0.0330 -0.127 0.8989
## PC1A_z:SES -0.0131 0.0258 -0.507 0.6124
## N = 1273
##
## ================================================================================
## HAA: WJ10 ~ PC1A × SES
## R2 = 0.0893 Adj-R2 = 0.0857
## ================================================================================
## Variable B SE t p
## ------------------------------------------------------------
## (Intercept) -0.0549 0.0558 -0.983 0.3259
## PC1A_z -0.0573 0.0302 -1.902 0.0574
## SES +0.2781 0.0325 +8.551 3.46e-17
## sex +0.1350 0.0701 +1.927 0.0542
## age_c -0.0591 0.0307 -1.923 0.0547
## PC1A_z:SES +0.0020 0.0242 +0.083 0.9337
## N = 1277