Multivariable Fractional Polynomial Interaction Models

Edwin Kipruto, Patrick Royston and Willi Sauerbrei

1 Introduction

mfpi() assesses whether the effect of a grouping variable, such as treatment, varies with a continuous variable. The treatment effect is modelled as a fractional polynomial (FP) function of the continuous variable, so the variable does not need to be categorised. The procedure has two stages: Stage 1 selects an adjustment model with MFP, and Stage 2 tests each candidate interaction while holding that adjustment model fixed.

This vignette shows how to run the analyses in the accompanying paper (paper reference) and how to read the output. For the methodology, the flexibility variants (flex), the selection criteria and the interface, each section points to the relevant part of the paper (Sections 2 and 3).

library(mfp2)
library(survival)

2 Advanced prostate cancer data

The data come from a randomised trial comparing placebo with diethylstilboestrol (paper, Section 4). The outcome is overall survival (survtime, cens), and the grouping variable is the binary treatment rx. The data set already contains the recoding described in the paper.

Converting rx to a factor with named levels gives Placebo and Treatment labels in printed output, predictions and plots. The first level is the reference group (paper, Section 3.3).

data("advanced_prostate_cancer", package = "mfp2")
advanced_prostate_cancer$rx <- factor(
  advanced_prostate_cancer$rx,
  levels = c(0, 1), labels = c("Placebo", "Treatment")
)

table(advanced_prostate_cancer$rx)
#> 
#>   Placebo Treatment 
#>       237       238
tapply(advanced_prostate_cancer$age, advanced_prostate_cancer$rx, range)
#> $Placebo
#> [1] 48 84
#> 
#> $Treatment
#> [1] 49 89

The question is whether the treatment effect varies with age. The overall treatment comparison, which ignores age, shows no clear difference (paper, Section 4.1):

survdiff(Surv(survtime, cens) ~ rx, data = advanced_prostate_cancer)
#> Call:
#> survdiff(formula = Surv(survtime, cens) ~ rx, data = advanced_prostate_cancer)
#> 
#>                N Observed Expected (O-E)^2/E (O-E)^2/V
#> rx=Placebo   237      180      165      1.33      2.66
#> rx=Treatment 238      158      173      1.27      2.66
#> 
#>  Chisq= 2.7  on 1 degrees of freedom, p= 0.1

cox <- coxph(Surv(survtime, cens) ~ rx, data = advanced_prostate_cancer)
summary(cox)$conf.int
#>             exp(coef) exp(-coef) lower .95 upper .95
#> rxTreatment 0.8374787    1.19406 0.6762658  1.037123

The hazard ratio is 0.84 (95% CI 0.68–1.04). A single overall estimate like this can hide an effect that changes with age.

All analyses below use flex = "flex1" (the same FP powers in both groups) and ties = "breslow", as in the paper. The package defaults are flex = "flex3" and ties = "efron". The paper (Sections 2.5 and 4) explains both choices and how Efron’s method changes the results.

3 Interaction without adjustment

fit_unadjusted <- mfpi(
  Surv(survtime, cens) ~ rx + fp(age),
  data = advanced_prostate_cancer,
  family = "cox",
  group_var = "rx",
  interaction_vars = "age",
  interaction_forms = c(age = "fp2"),
  flex = "flex1",
  ties = "breslow"
)
#> 
#> ======================================================================
#>   STEP 1: Selected Adjustment Model (MFP)
#> ======================================================================
#> 
#>   Model: Cox proportional hazards
#>   Criterion: p-value
#> 
#>     df_initial select alpha searched_fp df_final power1
#> age          4  0.050 0.050         yes        2      3
#> 
#> 
#>   Selected adjustment variables (1): age
#> 
#> ======================================================================
#>   STEP 2: Evaluating Interactions (flex = flex1)
#> ======================================================================
#> 
#>   Group variable: rx
#>   Selection rule: p-value < 0.050
#>   P-value adjustment: none
#> 
#> ----------------------------------------------------------------------
#>   [1/1] Variable: 'age' x 'rx'
#>   Adjusting for: (none)
#> ----------------------------------------------------------------------
#>  Type deviance_int deviance_diff df pvalue main_power int_power     
#>  FP2  3766.216     9.746         2  0.008  (3, 3)     (3, 3), (3, 3)
#> 
#> 
#> ======================================================================
#>   STEP 3: Interaction Summary
#> ======================================================================
#>   Variable     Type          p-value   selected
#> ----------------------------------------------------------------------
#>   age          FP2             0.008        Yes
#> ----------------------------------------------------------------------
#> 
#> Done: 1 of 1 interactions selected.
print(fit_unadjusted)
#> =========================================
#> MFPI  |  n = 475 (events = 338)  |  flex1
#> =========================================
#> 
#> Call:
#> mfpi(formula = Surv(survtime, cens) ~ rx + fp(age), data = advanced_prostate_cancer, 
#>     group_var = "rx", interaction_vars = "age", interaction_forms = c(age = "fp2"), 
#>     flex = "flex1", family = "cox", ties = "breslow")
#> 
#> Model: Cox proportional hazards
#> 
#> 
#> Step 1 - Selected Adjustment Model (MFP):
#> ----------------------------------------- 
#>     df_initial select alpha searched_fp df_final power1
#> age          4  0.050 0.050         yes        2      3
#> 
#>   Selected adjustment variables (1): age
#> 
#> Step 2 - All Interaction Candidates:
#> ------------------------------------ 
#> 
#>   Grouping variable     : rx
#>   Selection criterion   : p-value
#>   Selection rule        : p-value < 0.050
#>   P-value adjustment    : none
#> 
#>  variable type deviance_int deviance_diff df_int p-value fp_powers_main
#>       age  FP2     3766.216         9.746      2   0.008         (3, 3)
#> 
#>   FP powers by group level:
#>  variable type Group Group level fp_powers_int
#>       age  FP2    rx     Placebo        (3, 3)
#>       age  FP2    rx   Treatment        (3, 3)
#> 
#> Step 3 - Interaction Summary:
#> ----------------------------- 
#>  variable type p-value selected
#>       age  FP2   0.008      Yes

What the arguments do:

How to read the output (paper, Section 4.2):

3.1 Plotting the interaction

plot() returns a list with one element per variable. Each element contains one plot per comparison with the reference group.

p_unadj <- plot(fit_unadjusted, terms = "age", plot_type = "both",
                show_title = FALSE)
Unadjusted analysis: fitted age functions by group (left) and the Treatment-minus-Placebo log hazard ratio with 95% CI (right); paper, Figure 2.

Unadjusted analysis: fitted age functions by group (left) and the Treatment-minus-Placebo log hazard ratio with 95% CI (right); paper, Figure 2.

The right panel shows the log hazard ratio for Treatment vs Placebo as a function of age, the difference function \(\Delta\) (paper, Section 2.3). The dashed line is the constant log hazard ratio from the model without interaction. Here, the benefit of treatment is largest in younger patients and decreases with age. The confidence intervals do not account for the selection of FP powers.

3.2 Predictions at chosen ages

type = "both" returns the fitted function for each group and the difference between groups, all on the linear predictor scale.

age_pred <- predict(
  fit_unadjusted,
  newdata = data.frame(age = c(50, 60, 70)),
  terms = "age",
  level = 0.95,
  type = "both"
)
age_pred
#> =========================================================
#> MFPI prediction  |  term: age  |  scale: linear predictor
#> =========================================================
#> 
#> Partial fitted functions:
#>  age     group estimate    SE  lower  upper
#>   50   Placebo    0.460 0.322 -0.170  1.090
#>   60   Placebo   -0.018 0.126 -0.266  0.230
#>   70   Placebo   -0.154 0.080 -0.310  0.003
#>   50 Treatment   -1.014 0.404 -1.805 -0.223
#>   60 Treatment   -0.821 0.199 -1.212 -0.430
#>   70 Treatment   -0.376 0.123 -0.618 -0.134
#> 
#> Differences:
#>  age          contrast     group reference estimate    SE  lower  upper
#>   50 Treatment-Placebo Treatment   Placebo   -1.474 0.517 -2.488 -0.460
#>   60 Treatment-Placebo Treatment   Placebo   -0.802 0.235 -1.263 -0.342
#>   70 Treatment-Placebo Treatment   Placebo   -0.223 0.144 -0.505  0.060
#> 
#> Note: Partial fitted functions comprise the intercept (when present), group main effect,
#> and group-specific FP function estimated from the adjusted interaction model.
#> Differences are comparison-group minus reference-group partial fitted functions.

The differences are age-specific log hazard ratios. Exponentiating them gives hazard ratios of about 0.23, 0.45 and 0.80 at ages 50, 60 and 70 (paper, Section 4.2.2).

4 Interaction with adjustment for prognostic factors

The other baseline factors are now candidates for the adjustment model. Continuous variables enter through fp(); binary variables are entered directly. Age is still the only variable tested for interaction.

fit_adjusted <- mfpi(
  Surv(survtime, cens) ~ rx + fp(age) + fp(wt) + fp(sbp) +
    fp(dbp) + fp(sz) + fp(ap) + fp(hg) + fp(sg) +
    pf + hx + bm + stage + ekg,
  data = advanced_prostate_cancer,
  family = "cox",
  group_var = "rx",
  interaction_vars = "age",
  interaction_forms = c(age = "fp2"),
  flex = "flex1",
  ties = "breslow"
)
#> 
#> ======================================================================
#>   STEP 1: Selected Adjustment Model (MFP)
#> ======================================================================
#> 
#>   Model: Cox proportional hazards
#>   Criterion: p-value
#> 
#>       df_initial select alpha searched_fp df_final power1 power2
#> hx             1  0.050 0.050          no        1      1     NA
#> sz             4  0.050 0.050          no        1      1     NA
#> age            4  0.050 0.050         yes        4      3      3
#> ekg            1  0.050 0.050          no        1      1     NA
#> wt             4  0.050 0.050          no        1      1     NA
#> pf             1  0.050 0.050          no        1      1     NA
#> stage          1  0.050 0.050          no        1      1     NA
#> 
#> 
#>   Selected adjustment variables (7): hx, sz, age, ekg, wt, pf, stage
#> 
#> ======================================================================
#>   STEP 2: Evaluating Interactions (flex = flex1)
#> ======================================================================
#> 
#>   Group variable: rx
#>   Selection rule: p-value < 0.050
#>   P-value adjustment: none
#> 
#> ----------------------------------------------------------------------
#>   [1/1] Variable: 'age' x 'rx'
#>   Adjusting for: hx, sz, ekg, wt, pf, stage
#> ----------------------------------------------------------------------
#>  Type deviance_int deviance_diff df pvalue main_power int_power     
#>  FP2  3706.659     7.203         2  0.027  (3, 3)     (3, 3), (3, 3)
#> 
#> 
#> ======================================================================
#>   STEP 3: Interaction Summary
#> ======================================================================
#>   Variable     Type          p-value   selected
#> ----------------------------------------------------------------------
#>   age          FP2             0.027        Yes
#> ----------------------------------------------------------------------
#> 
#> Done: 1 of 1 interactions selected.
print(fit_adjusted)
#> =========================================
#> MFPI  |  n = 475 (events = 338)  |  flex1
#> =========================================
#> 
#> Call:
#> mfpi(formula = Surv(survtime, cens) ~ rx + fp(age) + fp(wt) + 
#>     fp(sbp) + fp(dbp) + fp(sz) + fp(ap) + fp(hg) + fp(sg) + pf + 
#>     hx + bm + stage + ekg, data = advanced_prostate_cancer, group_var = "rx", 
#>     interaction_vars = "age", interaction_forms = c(age = "fp2"), 
#>     flex = "flex1", family = "cox", ties = "breslow")
#> 
#> Model: Cox proportional hazards
#> 
#> 
#> Step 1 - Selected Adjustment Model (MFP):
#> ----------------------------------------- 
#>       df_initial select alpha searched_fp df_final power1 power2
#> hx             1  0.050 0.050          no        1      1     NA
#> sz             4  0.050 0.050          no        1      1     NA
#> age            4  0.050 0.050         yes        4      3      3
#> ekg            1  0.050 0.050          no        1      1     NA
#> wt             4  0.050 0.050          no        1      1     NA
#> pf             1  0.050 0.050          no        1      1     NA
#> stage          1  0.050 0.050          no        1      1     NA
#> 
#>   Selected adjustment variables (7): hx, sz, age, ekg, wt, pf, stage
#> 
#> Step 2 - All Interaction Candidates:
#> ------------------------------------ 
#> 
#>   Grouping variable     : rx
#>   Selection criterion   : p-value
#>   Selection rule        : p-value < 0.050
#>   P-value adjustment    : none
#> 
#>  variable type deviance_int deviance_diff df_int p-value fp_powers_main
#>       age  FP2     3706.659         7.203      2   0.027         (3, 3)
#> 
#>   FP powers by group level:
#>  variable type Group Group level fp_powers_int
#>       age  FP2    rx     Placebo        (3, 3)
#>       age  FP2    rx   Treatment        (3, 3)
#> 
#> Step 3 - Interaction Summary:
#> ----------------------------- 
#>  variable type p-value selected
#>       age  FP2   0.027      Yes

Step 1 now selects seven variables, with age as FP2 (3, 3). The interaction is weaker after adjustment but is still selected (p = 0.027; paper, Section 4.3). The plot is similar to the unadjusted one:

p_adj <- plot(fit_adjusted, terms = "age", plot_type = "both",
              show_title = FALSE)
Adjusted analysis: fitted age functions by group (left) and the Treatment-minus-Placebo log hazard ratio with 95% CI (right); paper, Figure 3.

Adjusted analysis: fitted age functions by group (left) and the Treatment-minus-Placebo log hazard ratio with 95% CI (right); paper, Figure 3.

5 Selection with AIC

With criterion = "aic", AIC is used in both stages. The interaction is selected when it lowers AIC by more than min_improvement (default 2). criterion = "bic" works the same way (paper, Section 2.6).

fit_aic <- mfpi(
  Surv(survtime, cens) ~
    rx + fp(age) + fp(wt) + fp(sbp) + fp(dbp) +
    fp(sz) + fp(ap) + fp(hg) + fp(sg) +
    pf + hx + bm + stage + ekg,
  data = advanced_prostate_cancer,
  family = "cox",
  group_var = "rx",
  interaction_vars = "age",
  interaction_forms = c(age = "fp2"),
  flex = "flex1",
  ties = "breslow",
  criterion = "aic",
  min_improvement = 2
)
#> 
#> ======================================================================
#>   STEP 1: Selected Adjustment Model (MFP)
#> ======================================================================
#> 
#>   Model: Cox proportional hazards
#>   Criterion: AIC
#> 
#>     df_initial searched_fp df_final power1 power2
#> hx           1          no        1      1     NA
#> sz           4          no        1      1     NA
#> age          4         yes        4      3      3
#> ekg          1          no        1      1     NA
#> hg           4          no        1      1     NA
#> wt           4          no        1      1     NA
#> sg           4          no        1      1     NA
#> pf           1          no        1      1     NA
#> 
#> 
#>   Selected adjustment variables (8): hx, sz, age, ekg, hg, wt, sg, pf
#> 
#> ======================================================================
#>   STEP 2: Evaluating Interactions (flex = flex1)
#> ======================================================================
#> 
#>   Group variable: rx
#>   Selection rule: AIC reduction > 2.000
#> 
#> ----------------------------------------------------------------------
#>   [1/1] Variable: 'age' x 'rx'
#>   Adjusting for: hx, sz, ekg, hg, wt, sg, pf
#> ----------------------------------------------------------------------
#>  Type AIC_main AIC_interaction dAIC  main_power int_power     
#>  FP2  3715.930 3712.248        3.681 (3, 3)     (3, 3), (3, 3)
#> 
#> 
#> ======================================================================
#>   STEP 3: Interaction Summary
#> ======================================================================
#>   Variable     Type             dAIC   selected
#> ----------------------------------------------------------------------
#>   age          FP2             3.681        Yes
#> ----------------------------------------------------------------------
#> 
#> Done: 1 of 1 interactions selected.
print(fit_aic)
#> =========================================
#> MFPI  |  n = 475 (events = 338)  |  flex1
#> =========================================
#> 
#> Call:
#> mfpi(formula = Surv(survtime, cens) ~ rx + fp(age) + fp(wt) + 
#>     fp(sbp) + fp(dbp) + fp(sz) + fp(ap) + fp(hg) + fp(sg) + pf + 
#>     hx + bm + stage + ekg, data = advanced_prostate_cancer, group_var = "rx", 
#>     interaction_vars = "age", interaction_forms = c(age = "fp2"), 
#>     flex = "flex1", min_improvement = 2, family = "cox", criterion = "aic", 
#>     ties = "breslow")
#> 
#> Model: Cox proportional hazards
#> 
#> 
#> Step 1 - Selected Adjustment Model (MFP):
#> ----------------------------------------- 
#>     df_initial searched_fp df_final power1 power2
#> hx           1          no        1      1     NA
#> sz           4          no        1      1     NA
#> age          4         yes        4      3      3
#> ekg          1          no        1      1     NA
#> hg           4          no        1      1     NA
#> wt           4          no        1      1     NA
#> sg           4          no        1      1     NA
#> pf           1          no        1      1     NA
#> 
#>   Selected adjustment variables (8): hx, sz, age, ekg, hg, wt, sg, pf
#> 
#> Step 2 - All Interaction Candidates:
#> ------------------------------------ 
#> 
#>   Grouping variable     : rx
#>   Selection criterion   : AIC
#>   Selection rule        : AIC reduction > 2.000
#> 
#>  variable type AIC_main AIC_interaction  dAIC fp_powers_main
#>       age  FP2 3715.930        3712.248 3.681         (3, 3)
#> 
#>   FP powers by group level:
#>  variable type Group Group level fp_powers_int
#>       age  FP2    rx     Placebo        (3, 3)
#>       age  FP2    rx   Treatment        (3, 3)
#> 
#> Step 3 - Interaction Summary:
#> ----------------------------- 
#>  variable type  dAIC selected
#>       age  FP2 3.681      Yes

AIC selects a slightly different adjustment model (eight variables) but the same interaction, with AIC lower by 3.681 (paper, Section 4.4).

6 Several interaction candidates

Several continuous variables can be tested at once. Here age uses FP2 and the other six use the default FP1. p_adjust_method = "holm" adjusts the p-values for the seven tests (paper, Section 2.6).

fit_multiple_interactions <- mfpi(
  Surv(survtime, cens) ~
    rx + fp(age) + fp(wt) + fp(sbp) + fp(dbp) +
    fp(sz) + fp(ap) + fp(hg) + fp(sg) +
    pf + hx + bm + stage + ekg,
  data = advanced_prostate_cancer,
  family = "cox",
  group_var = "rx",
  interaction_vars = c("age", "wt", "sbp", "dbp", "sz", "ap", "hg"),
  interaction_forms = c(age = "fp2"),
  flex = "flex1",
  ties = "breslow",
  p_adjust_method = "holm",
  verbose = FALSE
)
print(fit_multiple_interactions)
#> =========================================
#> MFPI  |  n = 475 (events = 338)  |  flex1
#> =========================================
#> 
#> Call:
#> mfpi(formula = Surv(survtime, cens) ~ rx + fp(age) + fp(wt) + 
#>     fp(sbp) + fp(dbp) + fp(sz) + fp(ap) + fp(hg) + fp(sg) + pf + 
#>     hx + bm + stage + ekg, data = advanced_prostate_cancer, group_var = "rx", 
#>     interaction_vars = c("age", "wt", "sbp", "dbp", "sz", "ap", 
#>         "hg"), interaction_forms = c(age = "fp2"), flex = "flex1", 
#>     family = "cox", ties = "breslow", p_adjust_method = "holm", 
#>     verbose = FALSE)
#> 
#> Model: Cox proportional hazards
#> 
#> 
#> Step 1 - Selected Adjustment Model (MFP):
#> ----------------------------------------- 
#>       df_initial select alpha searched_fp df_final power1 power2
#> hx             1  0.050 0.050          no        1      1     NA
#> sz             4  0.050 0.050          no        1      1     NA
#> age            4  0.050 0.050         yes        4      3      3
#> ekg            1  0.050 0.050          no        1      1     NA
#> wt             4  0.050 0.050          no        1      1     NA
#> pf             1  0.050 0.050          no        1      1     NA
#> stage          1  0.050 0.050          no        1      1     NA
#> 
#>   Selected adjustment variables (7): hx, sz, age, ekg, wt, pf, stage
#> 
#> Step 2 - All Interaction Candidates:
#> ------------------------------------ 
#> 
#>   Grouping variable     : rx
#>   Selection criterion   : p-value
#>   Selection rule        : adjusted p-value < 0.050
#>   P-value adjustment    : holm
#> 
#>  variable type deviance_int deviance_diff df_int p-value fp_powers_main
#>       age  FP2     3706.659         7.203      2   0.027         (3, 3)
#>        wt  FP1     3712.079         0.190      1   0.663           (-2)
#>       sbp  FP1     3713.361         0.395      1   0.529            (3)
#>       dbp  FP1     3713.527         0.178      1   0.673         (-0.5)
#>        sz  FP1     3710.649         3.213      1   0.073            (1)
#>        ap  FP1     3706.813         3.877      1   0.049           (-2)
#>        hg  FP1     3706.688         0.955      1   0.329           (-2)
#> 
#>   FP powers by group level:
#>  variable type Group Group level fp_powers_int
#>       age  FP2    rx     Placebo        (3, 3)
#>       age  FP2    rx   Treatment        (3, 3)
#>        wt  FP1    rx     Placebo          (-2)
#>        wt  FP1    rx   Treatment          (-2)
#>       sbp  FP1    rx     Placebo           (3)
#>       sbp  FP1    rx   Treatment           (3)
#>       dbp  FP1    rx     Placebo        (-0.5)
#>       dbp  FP1    rx   Treatment        (-0.5)
#>        sz  FP1    rx     Placebo           (1)
#>        sz  FP1    rx   Treatment           (1)
#>        ap  FP1    rx     Placebo          (-2)
#>        ap  FP1    rx   Treatment          (-2)
#>        hg  FP1    rx     Placebo          (-2)
#>        hg  FP1    rx   Treatment          (-2)
#> 
#> Step 3 - Interaction Summary:
#> ----------------------------- 
#>  variable type p_raw p_adjusted selected
#>       age  FP2 0.027      0.191       No
#>        wt  FP1 0.663      1.000       No
#>       sbp  FP1 0.529      1.000       No
#>       dbp  FP1 0.673      1.000       No
#>        sz  FP1 0.073      0.365       No
#>        ap  FP1 0.049      0.294       No
#>        hg  FP1 0.329      1.000       No

Step 3 now shows raw and adjusted p-values. Age (raw p = 0.027) and ap (raw p = 0.049) are below 0.05 before adjustment, but no interaction is selected after Holm adjustment (paper, Section 4.5).

7 Fixed linear adjustment model

The adjustment model can be prespecified instead of selected:

fit_fixed_linear <- mfpi(
  Surv(survtime, cens) ~
    rx +
    fp(age, df = 1) + fp(wt, df = 1) + fp(sbp, df = 1) +
    fp(dbp, df = 1) + fp(sz, df = 1) + fp(ap, df = 1) +
    fp(hg, df = 1) + fp(sg, df = 1) +
    pf + hx + bm + stage + ekg,
  data = advanced_prostate_cancer,
  family = "cox",
  group_var = "rx",
  interaction_vars = "age",
  interaction_forms = c(age = "fp2"),
  flex = "flex1",
  ties = "breslow",
  criterion = "pvalue",
  keep = c("age", "wt", "sbp", "dbp", "sz", "ap", "hg", "sg",
           "pf", "hx", "bm", "stage", "ekg")
)
#> 
#> ======================================================================
#>   STEP 1: Selected Adjustment Model (MFP)
#> ======================================================================
#> 
#>   Model: Cox proportional hazards
#>   Criterion: p-value
#> 
#>       df_initial select alpha searched_fp df_final power1
#> hx             1  1.000 0.050          no        1      1
#> sz             1  1.000 0.050          no        1      1
#> age            1  1.000 0.050          no        1      1
#> ekg            1  1.000 0.050          no        1      1
#> hg             1  1.000 0.050          no        1      1
#> wt             1  1.000 0.050          no        1      1
#> sg             1  1.000 0.050          no        1      1
#> pf             1  1.000 0.050          no        1      1
#> bm             1  1.000 0.050          no        1      1
#> ap             1  1.000 0.050          no        1      1
#> dbp            1  1.000 0.050          no        1      1
#> sbp            1  1.000 0.050          no        1      1
#> stage          1  1.000 0.050          no        1      1
#> 
#> 
#>   Selected adjustment variables (13): hx, sz, age, ekg, hg, wt, sg, pf, bm, ap, dbp, sbp, stage
#> 
#> ======================================================================
#>   STEP 2: Evaluating Interactions (flex = flex1)
#> ======================================================================
#> 
#>   Group variable: rx
#>   Selection rule: p-value < 0.050
#>   P-value adjustment: none
#> 
#> ----------------------------------------------------------------------
#>   [1/1] Variable: 'age' x 'rx'
#>   Adjusting for: hx, sz, ekg, hg, wt, sg, pf, bm, ap, dbp, sbp, stage
#> ----------------------------------------------------------------------
#>  Type deviance_int deviance_diff df pvalue main_power int_power     
#>  FP2  3694.694     8.004         2  0.018  (3, 3)     (3, 3), (3, 3)
#> 
#> 
#> ======================================================================
#>   STEP 3: Interaction Summary
#> ======================================================================
#>   Variable     Type          p-value   selected
#> ----------------------------------------------------------------------
#>   age          FP2             0.018        Yes
#> ----------------------------------------------------------------------
#> 
#> Done: 1 of 1 interactions selected.
print(fit_fixed_linear)
#> =========================================
#> MFPI  |  n = 475 (events = 338)  |  flex1
#> =========================================
#> 
#> Call:
#> mfpi(formula = Surv(survtime, cens) ~ rx + fp(age, df = 1) + 
#>     fp(wt, df = 1) + fp(sbp, df = 1) + fp(dbp, df = 1) + fp(sz, 
#>     df = 1) + fp(ap, df = 1) + fp(hg, df = 1) + fp(sg, df = 1) + 
#>     pf + hx + bm + stage + ekg, data = advanced_prostate_cancer, 
#>     group_var = "rx", interaction_vars = "age", interaction_forms = c(age = "fp2"), 
#>     flex = "flex1", family = "cox", criterion = "pvalue", keep = c("age", 
#>         "wt", "sbp", "dbp", "sz", "ap", "hg", "sg", "pf", "hx", 
#>         "bm", "stage", "ekg"), ties = "breslow")
#> 
#> Model: Cox proportional hazards
#> 
#> 
#> Step 1 - Selected Adjustment Model (MFP):
#> ----------------------------------------- 
#>       df_initial select alpha searched_fp df_final power1
#> hx             1  1.000 0.050          no        1      1
#> sz             1  1.000 0.050          no        1      1
#> age            1  1.000 0.050          no        1      1
#> ekg            1  1.000 0.050          no        1      1
#> hg             1  1.000 0.050          no        1      1
#> wt             1  1.000 0.050          no        1      1
#> sg             1  1.000 0.050          no        1      1
#> pf             1  1.000 0.050          no        1      1
#> bm             1  1.000 0.050          no        1      1
#> ap             1  1.000 0.050          no        1      1
#> dbp            1  1.000 0.050          no        1      1
#> sbp            1  1.000 0.050          no        1      1
#> stage          1  1.000 0.050          no        1      1
#> 
#>   Selected adjustment variables (13): hx, sz, age, ekg, hg, wt, sg, pf, bm, ap, dbp, sbp, stage
#> 
#> Step 2 - All Interaction Candidates:
#> ------------------------------------ 
#> 
#>   Grouping variable     : rx
#>   Selection criterion   : p-value
#>   Selection rule        : p-value < 0.050
#>   P-value adjustment    : none
#> 
#>  variable type deviance_int deviance_diff df_int p-value fp_powers_main
#>       age  FP2     3694.694         8.004      2   0.018         (3, 3)
#> 
#>   FP powers by group level:
#>  variable type Group Group level fp_powers_int
#>       age  FP2    rx     Placebo        (3, 3)
#>       age  FP2    rx   Treatment        (3, 3)
#> 
#> Step 3 - Interaction Summary:
#> ----------------------------- 
#>  variable type p-value selected
#>       age  FP2   0.018      Yes

fp(age, df = 1) affects only the adjustment model. The interaction with treatment is still FP2, as set by interaction_forms. To use a known transformation in the adjustment model (for example, log(x)), create the transformed variable first and enter it with df = 1 (paper, Section 4.6).

8 More than two groups

group_var may have more than two levels. Each non-reference group is compared with the reference group, and plot() and predict() return one comparison per non-reference group (paper, Section 4.7).

9 The xy interface

mfpi() also accepts a predictor data frame x and a response y instead of a formula. Columns are matched by name, so their order does not matter (paper, Appendix A).

y <- with(advanced_prostate_cancer, Surv(survtime, cens))
x <- advanced_prostate_cancer[, c(
  "rx", "age", "wt", "sbp", "dbp", "sz", "ap", "hg", "sg",
  "pf", "hx", "bm", "stage", "ekg"
)]

The unadjusted analysis passes only rx and age, and gives the same results as fit_unadjusted:

fit_unadjusted_xy <- mfpi(
  x = x[, c("rx", "age")],
  y = y,
  family = "cox",
  group_var = "rx",
  interaction_vars = "age",
  interaction_forms = c(age = "fp2"),
  flex = "flex1",
  ties = "breslow"
)
#> 
#> ======================================================================
#>   STEP 1: Selected Adjustment Model (MFP)
#> ======================================================================
#> 
#>   Model: Cox proportional hazards
#>   Criterion: p-value
#> 
#>     df_initial select alpha searched_fp df_final power1
#> age          4  0.050 0.050         yes        2      3
#> 
#> 
#>   Selected adjustment variables (1): age
#> 
#> ======================================================================
#>   STEP 2: Evaluating Interactions (flex = flex1)
#> ======================================================================
#> 
#>   Group variable: rx
#>   Selection rule: p-value < 0.050
#>   P-value adjustment: none
#> 
#> ----------------------------------------------------------------------
#>   [1/1] Variable: 'age' x 'rx'
#>   Adjusting for: (none)
#> ----------------------------------------------------------------------
#>  Type deviance_int deviance_diff df pvalue main_power int_power     
#>  FP2  3766.216     9.746         2  0.008  (3, 3)     (3, 3), (3, 3)
#> 
#> 
#> ======================================================================
#>   STEP 3: Interaction Summary
#> ======================================================================
#>   Variable     Type          p-value   selected
#> ----------------------------------------------------------------------
#>   age          FP2             0.008        Yes
#> ----------------------------------------------------------------------
#> 
#> Done: 1 of 1 interactions selected.
print(fit_unadjusted_xy)
#> =========================================
#> MFPI  |  n = 475 (events = 338)  |  flex1
#> =========================================
#> 
#> Call:
#> mfpi(x = x[, c("rx", "age")], y = y, group_var = "rx", interaction_vars = "age", 
#>     interaction_forms = c(age = "fp2"), flex = "flex1", family = "cox", 
#>     ties = "breslow")
#> 
#> Model: Cox proportional hazards
#> 
#> 
#> Step 1 - Selected Adjustment Model (MFP):
#> ----------------------------------------- 
#>     df_initial select alpha searched_fp df_final power1
#> age          4  0.050 0.050         yes        2      3
#> 
#>   Selected adjustment variables (1): age
#> 
#> Step 2 - All Interaction Candidates:
#> ------------------------------------ 
#> 
#>   Grouping variable     : rx
#>   Selection criterion   : p-value
#>   Selection rule        : p-value < 0.050
#>   P-value adjustment    : none
#> 
#>  variable type deviance_int deviance_diff df_int p-value fp_powers_main
#>       age  FP2     3766.216         9.746      2   0.008         (3, 3)
#> 
#>   FP powers by group level:
#>  variable type Group Group level fp_powers_int
#>       age  FP2    rx     Placebo        (3, 3)
#>       age  FP2    rx   Treatment        (3, 3)
#> 
#> Step 3 - Interaction Summary:
#> ----------------------------- 
#>  variable type p-value selected
#>       age  FP2   0.008      Yes

Passing all columns of x makes every other variable a candidate for the adjustment model. The following calls give the same results as the adjusted, AIC and multiple-candidate formula fits above:

fit_adjusted_xy <- mfpi(
  x = x, y = y, family = "cox",
  group_var = "rx", interaction_vars = "age",
  interaction_forms = c(age = "fp2"), flex = "flex1", ties = "breslow"
)

fit_aic_xy <- mfpi(
  x = x, y = y, family = "cox",
  group_var = "rx", interaction_vars = "age",
  interaction_forms = c(age = "fp2"), flex = "flex1", ties = "breslow",
  criterion = "aic", min_improvement = 2
)

fit_multiple_xy <- mfpi(
  x = x, y = y, family = "cox",
  group_var = "rx",
  interaction_vars = c("age", "wt", "sbp", "dbp", "sz", "ap", "hg"),
  interaction_forms = c(age = "fp2"), flex = "flex1", ties = "breslow",
  p_adjust_method = "holm", verbose = FALSE
)

Without a formula, settings that fp() would set are given as named vectors. For the fixed linear adjustment model, df sets each continuous variable to linear and keep forces all variables in:

fit_fixed_linear_xy <- mfpi(
  x = x, y = y, family = "cox",
  group_var = "rx", interaction_vars = "age",
  interaction_forms = c(age = "fp2"), flex = "flex1", ties = "breslow",
  criterion = "pvalue",
  df = c(age = 1, wt = 1, sbp = 1, dbp = 1,
         sz = 1, ap = 1, hg = 1, sg = 1),
  keep = c("age", "wt", "sbp", "dbp", "sz", "ap", "hg", "sg",
           "pf", "hx", "bm", "stage", "ekg")
)

Centering (center, center_type) works the same in both interfaces. It does not change the fitted values, the difference function, the selected powers or the p-values (paper, Appendix A.7).

10 Further details

See ?mfpi, ?predict.mfpi and ?plot.mfpi for all arguments. The paper describes the MFPI methodology (Section 2), the software interface and data requirements (Section 3), the application in more detail (Section 4) and a comparison with the Stata mfpi command (Section 5 and Appendix B).

10.1 Paper reference

Kipruto, E., Royston, P., and Sauerbrei, W. Multivariable Fractional Polynomial Interaction Models in R. Manuscript submitted to the Journal of Statistical Software.