Interaction
Interaction is commonly assessed by either
- creating a joint exposure factor or
- by including main effects and a product term.
When the specification is saturated, these parameterizations are mathematically equivalent and encode the same set of contrasts on the log-odds scale.
For clear presentation, it is often helpful to report the full set of joint effects relative to a single reference, and selected simple effects within strata, alongside additive interaction measures (Knol and VanderWeele 2012).
The motivating example in this document uses publicly available NHANES 2009–2012 data, studying hypertension defined by systolic blood pressure ≥ 130 mmHg and examining whether the effect of obesity differs across levels of self reported race or ethnicity.
We will use interaction analysis functions (jointeffects, inteffects, addint, addintlist from svyTable1 package) to compute and present joint effects, simple effects, and additive interaction measures with appropriate survey adjustments from saturated logistic regression models.
Data and variables
We use NHANES 2009–2012 adults (Age ≥ 20) with survey design variables. The binary outcome is Hypertension_130, defined from average systolic BP. The two interacting exposures are Race1 and ObeseStatus derived from BMI ≥ 30, with "White" and "Not Obese" as reference levels. We adjust for Age.
data(NHANESraw, package = "NHANES")
nhanes_adults <- NHANESraw %>%
filter(Age >= 20) %>%
mutate(
ObeseStatus = factor(ifelse(BMI >= 30, "Obese", "Not Obese"),
levels = c("Not Obese", "Obese")),
Hypertension_130 = factor(ifelse(BPSysAve >= 130, "Yes", "No"),
levels = c("No", "Yes")),
Race1 = relevel(as.factor(Race1), ref = "White")
) %>%
select(Age, Race1, BPSysAve, BMI, ObeseStatus, Hypertension_130,
SDMVPSU, SDMVSTRA, WTMEC2YR) %>%
drop_na()
adult_design <- svydesign(
id = ~SDMVPSU,
strata = ~SDMVSTRA,
weights = ~WTMEC2YR,
nest = TRUE,
data = nhanes_adults
)
f1_levels <- levels(adult_design$variables$Race1)
kable(f1_levels)| x |
|---|
| White |
| Black |
| Hispanic |
| Mexican |
| Other |
| x |
|---|
| Not Obese |
| Obese |
Models
Model 1. Joint variable model
Create one factor (variable) for all combinations of Race1 × ObeseStatus with "White_Not Obese" as the reference, and fit a survey-weighted logistic regression adjusting for Age.
# Create joint exposure factor inside the data
nhanes_adults <- nhanes_adults %>%
mutate(
Race1_ObeseStatus = interaction(Race1, ObeseStatus, sep = "_"),
Race1_ObeseStatus = relevel(Race1_ObeseStatus, ref = "White_Not Obese")
)
kable(levels(nhanes_adults$Race1_ObeseStatus))| x |
|---|
| White_Not Obese |
| Black_Not Obese |
| Hispanic_Not Obese |
| Mexican_Not Obese |
| Other_Not Obese |
| White_Obese |
| Black_Obese |
| Hispanic_Obese |
| Mexican_Obese |
| Other_Obese |
# Recreate survey design with the new variable included
adult_design_joint <- svydesign(
id = ~SDMVPSU,
strata = ~SDMVSTRA,
weights = ~WTMEC2YR,
nest = TRUE,
data = nhanes_adults
)
# Fit joint model using the explicitly named variable
joint_model <- svyglm(
Hypertension_130 ~ Race1_ObeseStatus + Age,
design = adult_design_joint,
family = binomial()
)
m1 <- publish(joint_model)
#> Variable Units OddsRatio CI.95 p-value
#> Race1_ObeseStatus White_Not Obese Ref
#> Black_Not Obese 2.11 [1.73;2.58] < 1e-04
#> Hispanic_Not Obese 0.99 [0.77;1.27] 0.9374741
#> Mexican_Not Obese 1.17 [0.95;1.43] 0.1510628
#> Other_Not Obese 1.05 [0.87;1.28] 0.6050711
#> White_Obese 1.40 [1.21;1.61] 0.0001361
#> Black_Obese 2.53 [1.98;3.24] < 1e-04
#> Hispanic_Obese 1.65 [1.27;2.13] 0.0009516
#> Mexican_Obese 1.92 [1.55;2.39] < 1e-04
#> Other_Obese 1.15 [0.74;1.80] 0.5437189
#> Age 1.06 [1.06;1.06] < 1e-04Model 2. Interaction term model
Include main effects and their product term for Race1 and ObeseStatus, adjusting for Age. This is the saturated parameterization equivalent to the joint-variable model.
interaction_model <- survey::svyglm(
Hypertension_130 ~ Race1 * ObeseStatus + Age,
design = adult_design,
family = binomial()
)
m2 <- publish(interaction_model)
#> Variable Units OddsRatio CI.95 p-value
#> Age 1.06 [1.06;1.06] < 1e-04
#> Race1(White): ObeseStatus(Obese vs Not Obese) 1.40 [1.21;1.61] < 1e-04
#> Race1(Black): ObeseStatus(Obese vs Not Obese) 1.20 [0.95;1.52] 0.1302843
#> Race1(Hispanic): ObeseStatus(Obese vs Not Obese) 1.66 [1.25;2.20] 0.0004009
#> Race1(Mexican): ObeseStatus(Obese vs Not Obese) 1.65 [1.30;2.10] < 1e-04
#> Race1(Other): ObeseStatus(Obese vs Not Obese) 1.09 [0.68;1.77] 0.7152699
#> ObeseStatus(Not Obese): Race1(Black vs White) 2.11 [1.73;2.58] < 1e-04
#> ObeseStatus(Not Obese): Race1(Hispanic vs White) 0.99 [0.77;1.27] 0.9367881
#> ObeseStatus(Not Obese): Race1(Mexican vs White) 1.17 [0.95;1.43] 0.1374845
#> ObeseStatus(Not Obese): Race1(Other vs White) 1.05 [0.87;1.28] 0.6000542
#> ObeseStatus(Obese): Race1(Black vs White) 1.81 [1.41;2.33] < 1e-04
#> ObeseStatus(Obese): Race1(Hispanic vs White) 1.18 [0.92;1.51] 0.1960934
#> ObeseStatus(Obese): Race1(Mexican vs White) 1.38 [1.03;1.84] 0.0296074
#> ObeseStatus(Obese): Race1(Other vs White) 0.82 [0.52;1.31] 0.4136666Joint effects from either parameterization
Retrieve the joint effects for each Race1 × ObeseStatus combination relative to "White & Not Obese". From the interaction model we obtain these by post-estimation transformation using the function jointeffects. From the joint model they correspond directly to exponentiated coefficients for the non-reference levels.
joint_from_interaction <- jointeffects(
interaction_model = interaction_model,
factor1_name = "Race1",
factor2_name = "ObeseStatus",
scale = "ratio",
digits = 2
)
kable(joint_from_interaction,
caption = "Table 1. Joint effects (OR) for Race1 × ObeseStatus vs White & Not Obese") %>%
kable_styling(full_width = FALSE)| Level1 | Level2 | Estimate | SE | CI.low | CI.upp |
|---|---|---|---|---|---|
| White | Not Obese | 1.00 | 0.00 | 1.00 | 1.00 |
| Black | Not Obese | 2.11 | 0.21 | 1.73 | 2.58 |
| Hispanic | Not Obese | 0.99 | 0.12 | 0.77 | 1.27 |
| Mexican | Not Obese | 1.17 | 0.12 | 0.95 | 1.43 |
| Other | Not Obese | 1.05 | 0.10 | 0.87 | 1.28 |
| White | Obese | 1.40 | 0.10 | 1.21 | 1.61 |
| Black | Obese | 2.53 | 0.32 | 1.98 | 3.24 |
| Hispanic | Obese | 1.65 | 0.22 | 1.27 | 2.13 |
| Mexican | Obese | 1.92 | 0.21 | 1.55 | 2.39 |
| Other | Obese | 1.15 | 0.26 | 0.74 | 1.80 |
Check equivalence of results obtained from joint variable model
| Variable | Units | OddsRatio | CI.95 | p-value |
|---|---|---|---|---|
| Race1_ObeseStatus | White_Not Obese | Ref | ||
| Black_Not Obese | 2.11 | [1.73;2.58] | < 1e-04 | |
| Hispanic_Not Obese | 0.99 | [0.77;1.27] | 0.9374741 | |
| Mexican_Not Obese | 1.17 | [0.95;1.43] | 0.1510628 | |
| Other_Not Obese | 1.05 | [0.87;1.28] | 0.6050711 | |
| White_Obese | 1.40 | [1.21;1.61] | 0.0001361 | |
| Black_Obese | 2.53 | [1.98;3.24] | < 1e-04 | |
| Hispanic_Obese | 1.65 | [1.27;2.13] | 0.0009516 | |
| Mexican_Obese | 1.92 | [1.55;2.39] | < 1e-04 | |
| Other_Obese | 1.15 | [0.74;1.80] | 0.5437189 | |
| Age | 1.06 | [1.06;1.06] | < 1e-04 |
Simple effects within strata
From the joint model we compute simple effects such as Obese vs Not Obese within each Race1 level using inteffects function.
simple_from_joint <- inteffects(
joint_model = joint_model,
joint_var_name <- "Race1_ObeseStatus",
factor1_name = "Race1",
factor2_name = "ObeseStatus",
factor1_levels = f1_levels,
factor2_levels = f2_levels,
level_separator = "_",
scale = "ratio",
digits = 2
)
kable(simple_from_joint,
caption = "Table 2. Simple effects: Obese vs Not Obese within Race1 strata") %>%
kable_styling(full_width = FALSE)| Comparison | Estimate | SE | CI.low | CI.upp | p-value |
|---|---|---|---|---|---|
| ObeseStatus(Obese vs Not Obese): Race1(White) | 1.40 | 0.10 | 1.21 | 1.61 | 0.0000049 |
| ObeseStatus(Obese vs Not Obese): Race1(Black) | 1.20 | 0.14 | 0.95 | 1.52 | 0.1302843 |
| ObeseStatus(Obese vs Not Obese): Race1(Hispanic) | 1.66 | 0.24 | 1.25 | 2.20 | 0.0004009 |
| ObeseStatus(Obese vs Not Obese): Race1(Mexican) | 1.65 | 0.20 | 1.30 | 2.10 | 0.0000415 |
| ObeseStatus(Obese vs Not Obese): Race1(Other) | 1.09 | 0.27 | 0.68 | 1.77 | 0.7152699 |
| Race1(Black vs White): ObeseStatus(Not Obese) | 2.11 | 0.21 | 1.73 | 2.58 | 0.0000000 |
| Race1(Hispanic vs White): ObeseStatus(Not Obese) | 0.99 | 0.12 | 0.77 | 1.27 | 0.9367881 |
| Race1(Mexican vs White): ObeseStatus(Not Obese) | 1.17 | 0.12 | 0.95 | 1.43 | 0.1374845 |
| Race1(Other vs White): ObeseStatus(Not Obese) | 1.05 | 0.10 | 0.87 | 1.28 | 0.6000542 |
| Race1(Black vs White): ObeseStatus(Obese) | 1.81 | 0.23 | 1.41 | 2.33 | 0.0000037 |
| Race1(Hispanic vs White): ObeseStatus(Obese) | 1.18 | 0.15 | 0.92 | 1.51 | 0.1960934 |
| Race1(Mexican vs White): ObeseStatus(Obese) | 1.38 | 0.20 | 1.03 | 1.84 | 0.0296074 |
| Race1(Other vs White): ObeseStatus(Obese) | 0.82 | 0.19 | 0.52 | 1.31 | 0.4136666 |
Check equivalence of results obtained from interaction model
| Variable | Units | OddsRatio | CI.95 | p-value |
|---|---|---|---|---|
| Age | 1.06 | [1.06;1.06] | < 1e-04 | |
| Race1(White): ObeseStatus(Obese vs Not Obese) | 1.40 | [1.21;1.61] | < 1e-04 | |
| Race1(Black): ObeseStatus(Obese vs Not Obese) | 1.20 | [0.95;1.52] | 0.1302843 | |
| Race1(Hispanic): ObeseStatus(Obese vs Not Obese) | 1.66 | [1.25;2.20] | 0.0004009 | |
| Race1(Mexican): ObeseStatus(Obese vs Not Obese) | 1.65 | [1.30;2.10] | < 1e-04 | |
| Race1(Other): ObeseStatus(Obese vs Not Obese) | 1.09 | [0.68;1.77] | 0.7152699 | |
| ObeseStatus(Not Obese): Race1(Black vs White) | 2.11 | [1.73;2.58] | < 1e-04 | |
| ObeseStatus(Not Obese): Race1(Hispanic vs White) | 0.99 | [0.77;1.27] | 0.9367881 | |
| ObeseStatus(Not Obese): Race1(Mexican vs White) | 1.17 | [0.95;1.43] | 0.1374845 | |
| ObeseStatus(Not Obese): Race1(Other vs White) | 1.05 | [0.87;1.28] | 0.6000542 | |
| ObeseStatus(Obese): Race1(Black vs White) | 1.81 | [1.41;2.33] | < 1e-04 | |
| ObeseStatus(Obese): Race1(Hispanic vs White) | 1.18 | [0.92;1.51] | 0.1960934 | |
| ObeseStatus(Obese): Race1(Mexican vs White) | 1.38 | [1.03;1.84] | 0.0296074 | |
| ObeseStatus(Obese): Race1(Other vs White) | 0.82 | [0.52;1.31] | 0.4136666 |
Additive interaction measures
We also summarize additive interaction using addintlist to report RERI, AP, and S, with "White & Not Obese" as the joint reference.
add_tab <- addintlist(
model = interaction_model,
factor1_name = "Race1",
factor2_name = "ObeseStatus",
measures = "all",
digits = 3
)
kable(add_tab,
caption = "Table 3. Additive interaction measures by Race1 with obesity as the binary factor") %>%
kable_styling(full_width = FALSE)| Factor1 | Level1 | Factor2 | Level2 | Measure | Estimate | SE | CI_low | CI_upp |
|---|---|---|---|---|---|---|---|---|
| Race1 | Black | ObeseStatus | Obese | RERI | 0.025 | 0.312 | -0.587 | 0.637 |
| Race1 | Black | ObeseStatus | Obese | AP | 0.010 | 0.122 | -0.230 | 0.250 |
| Race1 | Black | ObeseStatus | Obese | S | 1.016 | 0.205 | 0.680 | 1.518 |
| Race1 | Hispanic | ObeseStatus | Obese | RERI | 0.258 | 0.194 | -0.123 | 0.639 |
| Race1 | Hispanic | ObeseStatus | Obese | AP | 0.157 | 0.105 | -0.048 | 0.362 |
| Race1 | Hispanic | ObeseStatus | Obese | S | 1.667 | 0.382 | 0.788 | 3.525 |
| Race1 | Mexican | ObeseStatus | Obese | RERI | 0.360 | 0.253 | -0.136 | 0.857 |
| Race1 | Mexican | ObeseStatus | Obese | AP | 0.187 | 0.116 | -0.041 | 0.415 |
| Race1 | Mexican | ObeseStatus | Obese | S | 1.639 | 0.348 | 0.829 | 3.241 |
| Race1 | Other | ObeseStatus | Obese | RERI | -0.299 | 0.305 | -0.896 | 0.299 |
| Race1 | Other | ObeseStatus | Obese | AP | -0.259 | 0.316 | -0.878 | 0.360 |
| Race1 | Other | ObeseStatus | Obese | S | 0.337 | 1.758 | 0.011 | 10.567 |
Reporting guideline
The NHANES example illustrates that, under saturation, the joint-variable and interaction-term parameterizations are equivalent on the multiplicative scale. The joint-variable model directly presents the set of contrasts most often recommended for reporting, while the interaction model provides the traditional interaction coefficient and requires transformation to recover the same joint effects. Either model supports simple effects within strata. Reporting both joint and selected simple effects, together with additive interaction measures, provides a more complete summary.
Key messages
- Under saturation, joint and interaction models encode the same multiplicative contrasts.
- Present the full set of joint effects against a single reference profile.
- Include selected simple effects and additive interaction measures for clarity.