Code
Wide format: 100 rows
The three-model sequence and the k-pattern tests
The wide-format SEM is the classical specification for the distinguishable APIM. The three nested models — unconstrained, slopes-equal, fully constrained — give you three likelihood ratio tests, each of which has a clean substantive interpretation.
This tutorial reproduces the Olsen & Kenny (2006) specification and the Kenny & Ledermann (2010) k-pattern tests.
The unconstrained model estimates a separate slope for every path. Actor (male) and partner (female) intercepts, slopes, and residual variances are all free to differ.
model_unconstrained <- '
satisfaction_a ~ a_h*wnc_a + p_h*wnc_p + ar_h*recovery_a +
pr_h*recovery_p + c_h*has_children + d_h*dual_earner
satisfaction_p ~ a_w*wnc_p + p_w*wnc_a + ar_w*recovery_p +
pr_w*recovery_a + c_w*has_children + d_w*dual_earner
satisfaction_a ~ int_a*1
satisfaction_p ~ int_p*1
satisfaction_a ~~ var_a*satisfaction_a
satisfaction_p ~~ var_p*satisfaction_p
wnc_a ~~ wnc_p
recovery_a ~~ recovery_p
satisfaction_a ~~ res_cov*satisfaction_p
'
fit_unconstrained <- sem(model_unconstrained, data = ddw)
summary(fit_unconstrained, standardized = TRUE, fit.measures = TRUE)lavaan 0.6-21 ended normally after 37 iterations
Estimator ML
Optimization method NLMINB
Number of model parameters 27
Number of observations 100
Model Test User Model:
Test statistic 43.676
Degrees of freedom 12
P-value (Chi-square) 0.000
Model Test Baseline Model:
Test statistic 284.840
Degrees of freedom 27
P-value 0.000
User Model versus Baseline Model:
Comparative Fit Index (CFI) 0.877
Tucker-Lewis Index (TLI) 0.724
Loglikelihood and Information Criteria:
Loglikelihood user model (H0) -646.240
Loglikelihood unrestricted model (H1) -624.402
Akaike (AIC) 1346.481
Bayesian (BIC) 1416.820
Sample-size adjusted Bayesian (SABIC) 1331.547
Root Mean Square Error of Approximation:
RMSEA 0.162
90 Percent confidence interval - lower 0.112
90 Percent confidence interval - upper 0.216
P-value H_0: RMSEA <= 0.050 0.000
P-value H_0: RMSEA >= 0.080 0.995
Standardized Root Mean Square Residual:
SRMR 0.145
Parameter Estimates:
Standard errors Standard
Information Expected
Information saturated (h1) model Structured
Regressions:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
satisfaction_a ~
wnc_a (a_h) -0.296 0.051 -5.794 0.000 -0.296 -0.470
wnc_p (p_h) -0.116 0.056 -2.093 0.036 -0.116 -0.170
recvry_ (ar_h) 0.240 0.052 4.633 0.000 0.240 0.330
rcvry_p (pr_h) 0.076 0.049 1.568 0.117 0.076 0.112
hs_chld (c_h) 0.089 0.092 0.962 0.336 0.089 0.066
dul_rnr (d_h) 0.318 0.097 3.266 0.001 0.318 0.225
satisfaction_p ~
wnc_p (a_w) -0.276 0.055 -5.040 0.000 -0.276 -0.390
wnc_a (p_w) -0.170 0.050 -3.369 0.001 -0.170 -0.261
rcvry_p (ar_w) 0.254 0.048 5.303 0.000 0.254 0.360
recvry_ (pr_w) 0.145 0.051 2.837 0.005 0.145 0.193
hs_chld (c_w) -0.023 0.091 -0.251 0.802 -0.023 -0.016
dul_rnr (d_w) 0.287 0.096 2.995 0.003 0.287 0.197
Covariances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
wnc_a ~~
wnc_p 0.516 0.110 4.682 0.000 0.516 0.530
recovery_a ~~
rcvry_p 0.218 0.087 2.502 0.012 0.218 0.258
.satisfaction_a ~~
.stsfct_ (rs_c) 0.045 0.020 2.233 0.026 0.045 0.229
Intercepts:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.stsfc_ (int_a) 5.040 0.211 23.875 0.000 5.040 7.765
.stsfc_ (int_p) 4.580 0.208 22.040 0.000 4.580 6.842
wnc_a 0.247 0.103 2.397 0.017 0.247 0.240
wnc_p -0.033 0.095 -0.353 0.724 -0.033 -0.035
rcvry_ 3.005 0.089 33.684 0.000 3.005 3.368
rcvry_ 3.235 0.095 34.131 0.000 3.235 3.413
Variances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.stsfct_ (var_) 0.199 0.028 7.071 0.000 0.199 0.472
.stsfct_ (vr_p) 0.193 0.027 7.071 0.000 0.193 0.430
wnc_a 1.058 0.150 7.071 0.000 1.058 1.000
wnc_p 0.896 0.127 7.071 0.000 0.896 1.000
recvry_ 0.796 0.113 7.071 0.000 0.796 1.000
rcvry_p 0.898 0.127 7.071 0.000 0.898 1.000
The slopes-equal model constrains actor and partner slopes to be the same. Intercepts and residual variances are free to differ.
model_slopes_equal <- '
satisfaction_a ~ a*wnc_a + p*wnc_p + ar*recovery_a +
pr*recovery_p + c*has_children + d*dual_earner
satisfaction_p ~ a*wnc_p + p*wnc_a + ar*recovery_p +
pr*recovery_a + c*has_children + d*dual_earner
satisfaction_a ~ int_a*1
satisfaction_p ~ int_p*1
satisfaction_a ~~ var_h*satisfaction_a
satisfaction_p ~~ var_w*satisfaction_p
wnc_a ~~ wnc_p
recovery_a ~~ recovery_p
satisfaction_a ~~ res_cov*satisfaction_p
'
fit_slopes_equal <- sem(model_slopes_equal, data = ddw)
summary(fit_slopes_equal, standardized = TRUE, fit.measures = TRUE)lavaan 0.6-21 ended normally after 33 iterations
Estimator ML
Optimization method NLMINB
Number of model parameters 27
Number of equality constraints 6
Number of observations 100
Model Test User Model:
Test statistic 47.242
Degrees of freedom 18
P-value (Chi-square) 0.000
Model Test Baseline Model:
Test statistic 284.840
Degrees of freedom 27
P-value 0.000
User Model versus Baseline Model:
Comparative Fit Index (CFI) 0.887
Tucker-Lewis Index (TLI) 0.830
Loglikelihood and Information Criteria:
Loglikelihood user model (H0) -648.023
Loglikelihood unrestricted model (H1) -624.402
Akaike (AIC) 1338.046
Bayesian (BIC) 1392.755
Sample-size adjusted Bayesian (SABIC) 1326.432
Root Mean Square Error of Approximation:
RMSEA 0.127
90 Percent confidence interval - lower 0.084
90 Percent confidence interval - upper 0.172
P-value H_0: RMSEA <= 0.050 0.003
P-value H_0: RMSEA >= 0.080 0.962
Standardized Root Mean Square Residual:
SRMR 0.146
Parameter Estimates:
Standard errors Standard
Information Expected
Information saturated (h1) model Structured
Regressions:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
satisfaction_a ~
wnc_a (a) -0.288 0.035 -8.141 0.000 -0.288 -0.446
wnc_p (p) -0.146 0.035 -4.141 0.000 -0.146 -0.208
recovery_ (ar) 0.241 0.034 6.998 0.000 0.241 0.324
recovry_p (pr) 0.111 0.034 3.214 0.001 0.111 0.158
hs_chldrn (c) 0.025 0.072 0.347 0.728 0.025 0.018
dual_ernr (d) 0.304 0.076 4.006 0.000 0.304 0.210
satisfaction_p ~
wnc_p (a) -0.288 0.035 -8.141 0.000 -0.288 -0.416
wnc_a (p) -0.146 0.035 -4.141 0.000 -0.146 -0.230
recovry_p (ar) 0.241 0.034 6.998 0.000 0.241 0.349
recovery_ (pr) 0.111 0.034 3.214 0.001 0.111 0.151
hs_chldrn (c) 0.025 0.072 0.347 0.728 0.025 0.018
dual_ernr (d) 0.304 0.076 4.006 0.000 0.304 0.213
Covariances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
wnc_a ~~
wnc_p 0.516 0.110 4.682 0.000 0.516 0.530
recovery_a ~~
rcvry_p 0.218 0.087 2.502 0.012 0.218 0.258
.satisfaction_a ~~
.stsfct_ (rs_c) 0.043 0.020 2.122 0.034 0.043 0.217
Intercepts:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.stsfc_ (int_a) 4.955 0.167 29.664 0.000 4.955 7.462
.stsfc_ (int_p) 4.688 0.167 28.103 0.000 4.688 7.158
wnc_a 0.247 0.103 2.397 0.017 0.247 0.240
wnc_p -0.033 0.095 -0.353 0.724 -0.033 -0.035
rcvry_ 3.005 0.089 33.684 0.000 3.005 3.368
rcvry_ 3.235 0.095 34.131 0.000 3.235 3.413
Variances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.stsfct_ (vr_h) 0.202 0.029 7.071 0.000 0.202 0.459
.stsfct_ (vr_w) 0.196 0.028 7.071 0.000 0.196 0.456
wnc_a 1.058 0.150 7.071 0.000 1.058 1.000
wnc_p 0.896 0.127 7.071 0.000 0.896 1.000
recvry_ 0.796 0.113 7.071 0.000 0.796 1.000
rcvry_p 0.898 0.127 7.071 0.000 0.898 1.000
The fully-constrained model forces every parameter to be equal across the two roles. This is the indistinguishable model.
model_full_equal <- '
satisfaction_a ~ a*wnc_a + p*wnc_p + ar*recovery_a +
pr*recovery_p + c*has_children + d*dual_earner
satisfaction_p ~ a*wnc_p + p*wnc_a + ar*recovery_p +
pr*recovery_a + c*has_children + d*dual_earner
satisfaction_a ~ alpha*1
satisfaction_p ~ alpha*1
satisfaction_a ~~ var_res*satisfaction_a
satisfaction_p ~~ var_res*satisfaction_p
wnc_a ~~ wnc_p
recovery_a ~~ recovery_p
satisfaction_a ~~ res_cov*satisfaction_p
'
fit_full_equal <- sem(model_full_equal, data = ddw)
summary(fit_full_equal, standardized = TRUE, fit.measures = TRUE)lavaan 0.6-21 ended normally after 27 iterations
Estimator ML
Optimization method NLMINB
Number of model parameters 27
Number of equality constraints 8
Number of observations 100
Model Test User Model:
Test statistic 66.150
Degrees of freedom 20
P-value (Chi-square) 0.000
Model Test Baseline Model:
Test statistic 284.840
Degrees of freedom 27
P-value 0.000
User Model versus Baseline Model:
Comparative Fit Index (CFI) 0.821
Tucker-Lewis Index (TLI) 0.758
Loglikelihood and Information Criteria:
Loglikelihood user model (H0) -657.477
Loglikelihood unrestricted model (H1) -624.402
Akaike (AIC) 1352.955
Bayesian (BIC) 1402.453
Sample-size adjusted Bayesian (SABIC) 1342.446
Root Mean Square Error of Approximation:
RMSEA 0.152
90 Percent confidence interval - lower 0.112
90 Percent confidence interval - upper 0.193
P-value H_0: RMSEA <= 0.050 0.000
P-value H_0: RMSEA >= 0.080 0.998
Standardized Root Mean Square Residual:
SRMR 0.150
Parameter Estimates:
Standard errors Standard
Information Expected
Information saturated (h1) model Structured
Regressions:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
satisfaction_a ~
wnc_a (a) -0.256 0.037 -6.971 0.000 -0.256 -0.394
wnc_p (p) -0.178 0.037 -4.846 0.000 -0.178 -0.252
recovery_ (ar) 0.226 0.036 6.300 0.000 0.226 0.302
recovry_p (pr) 0.125 0.036 3.476 0.001 0.125 0.177
hs_chldrn (c) 0.026 0.072 0.364 0.716 0.026 0.019
dual_ernr (d) 0.305 0.076 4.010 0.000 0.305 0.209
satisfaction_p ~
wnc_p (a) -0.256 0.037 -6.971 0.000 -0.256 -0.363
wnc_a (p) -0.178 0.037 -4.846 0.000 -0.178 -0.275
recovry_p (ar) 0.226 0.036 6.300 0.000 0.226 0.321
recovery_ (pr) 0.125 0.036 3.476 0.001 0.125 0.167
hs_chldrn (c) 0.026 0.072 0.364 0.716 0.026 0.019
dual_ernr (d) 0.305 0.076 4.010 0.000 0.305 0.209
Covariances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
wnc_a ~~
wnc_p 0.516 0.110 4.682 0.000 0.516 0.530
recovery_a ~~
rcvry_p 0.218 0.087 2.502 0.012 0.218 0.258
.satisfaction_a ~~
.stsfct_ (rs_c) 0.027 0.022 1.246 0.213 0.027 0.126
Intercepts:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.stsfct_ (alph) 4.824 0.164 29.359 0.000 4.824 7.221
.stsfct_ (alph) 4.824 0.164 29.359 0.000 4.824 7.236
wnc_a 0.247 0.103 2.397 0.017 0.247 0.240
wnc_p -0.033 0.095 -0.353 0.724 -0.033 -0.035
recvry_ 3.005 0.089 33.684 0.000 3.005 3.368
rcvry_p 3.235 0.095 34.131 0.000 3.235 3.413
Variances:
Estimate Std.Err z-value P(>|z|) Std.lv Std.all
.stsfct_ (vr_r) 0.215 0.022 9.922 0.000 0.215 0.482
.stsfct_ (vr_r) 0.215 0.022 9.922 0.000 0.215 0.484
wnc_a 1.058 0.150 7.071 0.000 1.058 1.000
wnc_p 0.896 0.127 7.071 0.000 0.896 1.000
recvry_ 0.796 0.113 7.071 0.000 0.796 1.000
rcvry_p 0.898 0.127 7.071 0.000 0.898 1.000
cat("LRT 1: Unconstrained vs Slopes Equal\n")LRT 1: Unconstrained vs Slopes Equal
cat("(Do slopes differ by gender?)\n\n")(Do slopes differ by gender?)
lrt1 <- lavTestLRT(fit_unconstrained, fit_slopes_equal)
print(lrt1)
Chi-Squared Difference Test
Df AIC BIC Chisq Chisq diff RMSEA Df diff Pr(>Chisq)
fit_unconstrained 12 1346.5 1416.8 43.676
fit_slopes_equal 18 1338.0 1392.8 47.242 3.5659 0 6 0.7352
cat("\nLRT 2: Slopes Equal vs Fully Constrained\n")
LRT 2: Slopes Equal vs Fully Constrained
cat("(Do intercepts differ by gender?)\n\n")(Do intercepts differ by gender?)
lrt2 <- lavTestLRT(fit_slopes_equal, fit_full_equal)
print(lrt2)
Chi-Squared Difference Test
Df AIC BIC Chisq Chisq diff RMSEA Df diff Pr(>Chisq)
fit_slopes_equal 18 1338 1392.8 47.242
fit_full_equal 20 1353 1402.5 66.150 18.909 0.29076 2 7.836e-05
fit_slopes_equal
fit_full_equal ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
cat("\nLRT 3: Unconstrained vs Fully Constrained\n")
LRT 3: Unconstrained vs Fully Constrained
cat("(Overall indistinguishability)\n\n")(Overall indistinguishability)
lrt3 <- lavTestLRT(fit_unconstrained, fit_full_equal)
print(lrt3)
Chi-Squared Difference Test
Df AIC BIC Chisq Chisq diff RMSEA Df diff Pr(>Chisq)
fit_unconstrained 12 1346.5 1416.8 43.676
fit_full_equal 20 1353.0 1402.5 66.150 22.474 0.13451 8 0.004109
fit_unconstrained
fit_full_equal **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
In our data, LRT 1 should be non-significant (equal slopes) and LRT 2 should be significant (different intercepts). This is the standard empirical pattern: distinguishable by means, not by effects.
Build a side-by-side table of fit measures for the three models.
fits <- c("chisq", "df", "pvalue", "cfi", "tli", "rmsea", "srmr")
fit_indices <- data.frame(
unconstrained = fitMeasures(fit_unconstrained, fits),
slopes_equal = fitMeasures(fit_slopes_equal, fits),
full_equal = fitMeasures(fit_full_equal, fits)
)
print(round(fit_indices, 4)) unconstrained slopes_equal full_equal
chisq 43.6759 47.2417 66.1502
df 12.0000 18.0000 20.0000
pvalue 0.0000 0.0002 0.0000
cfi 0.8771 0.8866 0.8210
tli 0.7236 0.8299 0.7584
rmsea 0.1625 0.1275 0.1519
srmr 0.1451 0.1459 0.1503
---
title: "Distinguishable dyads: SEM in wide format"
subtitle: "The three-model sequence and the k-pattern tests"
---
The wide-format SEM is the classical specification for the
distinguishable APIM. The three nested models — unconstrained,
slopes-equal, fully constrained — give you three likelihood ratio
tests, each of which has a clean substantive interpretation.
This tutorial reproduces the Olsen & Kenny (2006) specification
and the Kenny & Ledermann (2010) k-pattern tests.
## Setup
```{r}
#| label: setup
#| message: false
#| warning: false
library(lavaan)
library(dplyr)
load("../../../data/dyad_data.RData")
cat("Wide format:", nrow(ddw), "rows\n")
```
## Model 1: unconstrained (all paths free)
The unconstrained model estimates a separate slope for every path.
Actor (male) and partner (female) intercepts, slopes, and residual
variances are all free to differ.
```{r}
#| label: model-unconstrained
model_unconstrained <- '
satisfaction_a ~ a_h*wnc_a + p_h*wnc_p + ar_h*recovery_a +
pr_h*recovery_p + c_h*has_children + d_h*dual_earner
satisfaction_p ~ a_w*wnc_p + p_w*wnc_a + ar_w*recovery_p +
pr_w*recovery_a + c_w*has_children + d_w*dual_earner
satisfaction_a ~ int_a*1
satisfaction_p ~ int_p*1
satisfaction_a ~~ var_a*satisfaction_a
satisfaction_p ~~ var_p*satisfaction_p
wnc_a ~~ wnc_p
recovery_a ~~ recovery_p
satisfaction_a ~~ res_cov*satisfaction_p
'
fit_unconstrained <- sem(model_unconstrained, data = ddw)
summary(fit_unconstrained, standardized = TRUE, fit.measures = TRUE)
```
## Model 2: slopes equal, intercepts free
The slopes-equal model constrains actor and partner slopes to be
the same. Intercepts and residual variances are free to differ.
```{r}
#| label: model-slopes-equal
model_slopes_equal <- '
satisfaction_a ~ a*wnc_a + p*wnc_p + ar*recovery_a +
pr*recovery_p + c*has_children + d*dual_earner
satisfaction_p ~ a*wnc_p + p*wnc_a + ar*recovery_p +
pr*recovery_a + c*has_children + d*dual_earner
satisfaction_a ~ int_a*1
satisfaction_p ~ int_p*1
satisfaction_a ~~ var_h*satisfaction_a
satisfaction_p ~~ var_w*satisfaction_p
wnc_a ~~ wnc_p
recovery_a ~~ recovery_p
satisfaction_a ~~ res_cov*satisfaction_p
'
fit_slopes_equal <- sem(model_slopes_equal, data = ddw)
summary(fit_slopes_equal, standardized = TRUE, fit.measures = TRUE)
```
## Model 3: fully constrained (slopes, intercepts, variances equal)
The fully-constrained model forces every parameter to be equal
across the two roles. This is the indistinguishable model.
```{r}
#| label: model-full-equal
model_full_equal <- '
satisfaction_a ~ a*wnc_a + p*wnc_p + ar*recovery_a +
pr*recovery_p + c*has_children + d*dual_earner
satisfaction_p ~ a*wnc_p + p*wnc_a + ar*recovery_p +
pr*recovery_a + c*has_children + d*dual_earner
satisfaction_a ~ alpha*1
satisfaction_p ~ alpha*1
satisfaction_a ~~ var_res*satisfaction_a
satisfaction_p ~~ var_res*satisfaction_p
wnc_a ~~ wnc_p
recovery_a ~~ recovery_p
satisfaction_a ~~ res_cov*satisfaction_p
'
fit_full_equal <- sem(model_full_equal, data = ddw)
summary(fit_full_equal, standardized = TRUE, fit.measures = TRUE)
```
## The three nested LRTs
```{r}
#| label: lrts
cat("LRT 1: Unconstrained vs Slopes Equal\n")
cat("(Do slopes differ by gender?)\n\n")
lrt1 <- lavTestLRT(fit_unconstrained, fit_slopes_equal)
print(lrt1)
cat("\nLRT 2: Slopes Equal vs Fully Constrained\n")
cat("(Do intercepts differ by gender?)\n\n")
lrt2 <- lavTestLRT(fit_slopes_equal, fit_full_equal)
print(lrt2)
cat("\nLRT 3: Unconstrained vs Fully Constrained\n")
cat("(Overall indistinguishability)\n\n")
lrt3 <- lavTestLRT(fit_unconstrained, fit_full_equal)
print(lrt3)
```
::: {.callout-tip}
## Reading the three tests
- **LRT 1** tests whether actor and partner slopes are equal. A
non-significant *p*-value supports the *couple pattern* (equal
slopes). Significant *p*-value means the dyads are
distinguishable by effects, not just by means.
- **LRT 2** tests whether the intercepts and residual variances
are equal, given equal slopes. A significant *p*-value means
the dyads are distinguishable by *means*.
- **LRT 3** tests overall indistinguishability (slopes, intercepts,
residual variances all equal). A non-significant *p*-value
means the dyads are indistinguishable on every parameter.
In our data, LRT 1 should be non-significant (equal slopes) and
LRT 2 should be significant (different intercepts). This is the
standard empirical pattern: distinguishable by means, not by
effects.
:::
## Fit indices comparison
Build a side-by-side table of fit measures for the three models.
```{r}
#| label: fit-indices
fits <- c("chisq", "df", "pvalue", "cfi", "tli", "rmsea", "srmr")
fit_indices <- data.frame(
unconstrained = fitMeasures(fit_unconstrained, fits),
slopes_equal = fitMeasures(fit_slopes_equal, fits),
full_equal = fitMeasures(fit_full_equal, fits)
)
print(round(fit_indices, 4))
```
## What to take away
::: {.callout-takeaway}
## Key takeaways
- The wide-format SEM gives you the three classical tests of
distinguishability: slopes, intercepts, overall.
- LRT 1 (slopes) is the most important test for the APIM: it
tells you whether the *effects* of predictors differ by role.
- LRT 2 (intercepts) tells you whether the dyads are
distinguishable by *means* (often they are, even when slopes
are equal).
- LRT 3 (overall) is the strict test; it is rarely used in
practice because LRT 1 and LRT 2 are more informative.
:::
## What to read next
- The [two-intercept tutorial](two-intercept.html) — recommended
when LRT 1 is non-significant but LRT 2 is significant (i.e.
dyads are distinguishable by means, not effects).
- The [Hahn et al. (2014) replication
tutorial](../moderated/hahn2014.html) — moderated APIM with a
dyad-level moderator.