Interaction

Interaction is commonly assessed by either

  1. creating a joint exposure factor or
  2. 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.

library(survey)
library(dplyr)
library(NHANES)
library(tidyr)
library(knitr)
library(Publish)
library(kableExtra)
library(svyTable1)
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
f2_levels <- levels(adult_design$variables$ObeseStatus)
kable(f2_levels)
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-04

Model 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.4136666

Joint 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)
Table 1. Joint effects (OR) for Race1 × ObeseStatus vs White & Not Obese
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

kable(m1$regressionTable)
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)
Table 2. Simple effects: Obese vs Not Obese within Race1 strata
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

kable(m2$regressionTable)
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)
Table 3. Additive interaction measures by Race1 with obesity as the binary factor
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.

References

Knol, Mirjam J, and Tyler J VanderWeele. 2012. “Recommendations for Presenting Analyses of Effect Modification and Interaction.” International Journal of Epidemiology 41 (2): 514–20.