--- title: "Multivariable Fractional Polynomial Interaction Models" author: "Edwin Kipruto, Patrick Royston and Willi Sauerbrei" output: rmarkdown::html_vignette: toc: true toc_depth: 2 number_sections: true vignette: > %\VignetteIndexEntry{Multivariable Fractional Polynomial Interaction Models} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} editor_options: markdown: wrap: 72 --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 9, fig.height = 4.5, fig.align = "center", out.width = "100%" ) options(width = 80) has_ggplot2 <- requireNamespace("ggplot2", quietly = TRUE) has_geepack <- requireNamespace("geepack", quietly = TRUE) has_patchwork <- requireNamespace("patchwork", quietly = TRUE) ``` # 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](#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). ```{r packages} library(mfp2) library(survival) ``` # 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). ```{r data} 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) tapply(advanced_prostate_cancer$age, advanced_prostate_cancer$rx, range) ``` 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): ```{r overall-treatment-effect} survdiff(Surv(survtime, cens) ~ rx, data = advanced_prostate_cancer) cox <- coxph(Surv(survtime, cens) ~ rx, data = advanced_prostate_cancer) summary(cox)$conf.int ``` 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. # Interaction without adjustment ```{r unadjusted-fit} 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" ) print(fit_unadjusted) ``` What the arguments do: - `fp(age)` puts age in the Stage 1 adjustment model. It does not set the form of the interaction. - `group_var` names the grouping variable. - `interaction_vars` lists the continuous variables tested for interaction with `group_var`. - `interaction_forms = c(age = "fp2")` models the interaction with an FP2 function. All 36 FP2 power pairs are compared. Variables not listed use FP1. - `flex = "flex1"` selects the FP powers from the model without interaction and uses them in both groups (paper, Section 2.5). How to read the output (paper, Section 4.2): - **Step 1** is the adjustment model. Age is selected as FP1 with power 3. Here age is the only adjustment variable, and it is removed from the adjustment set when its own interaction is tested. - **Step 2** is the interaction test. The FP2 powers (3, 3) are used in both groups. Adding the interaction reduces the deviance by 9.746 on 2 df. - **Step 3** summarises the decision: the interaction is selected (p = 0.008). ## Plotting the interaction `plot()` returns a list with one element per variable. Each element contains one plot per comparison with the reference group. ```{r unadjusted-plot, eval=has_patchwork && has_ggplot2, fig.cap="Unadjusted analysis: fitted age functions by group (left) and the Treatment-minus-Placebo log hazard ratio with 95% CI (right); paper, Figure 2."} p_unadj <- plot(fit_unadjusted, terms = "age", plot_type = "both", show_title = FALSE) ``` 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. ## Predictions at chosen ages `type = "both"` returns the fitted function for each group and the difference between groups, all on the linear predictor scale. ```{r unadjusted-predict} age_pred <- predict( fit_unadjusted, newdata = data.frame(age = c(50, 60, 70)), terms = "age", level = 0.95, type = "both" ) age_pred ``` 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). # 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. ```{r adjusted-fit} 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" ) print(fit_adjusted) ``` 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: ```{r adjusted-plot, eval=has_patchwork && has_ggplot2, fig.cap="Adjusted analysis: fitted age functions by group (left) and the Treatment-minus-Placebo log hazard ratio with 95% CI (right); paper, Figure 3."} p_adj <- plot(fit_adjusted, terms = "age", plot_type = "both", show_title = FALSE) ``` # 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). ```{r aic-fit} 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 ) print(fit_aic) ``` AIC selects a slightly different adjustment model (eight variables) but the same interaction, with AIC lower by 3.681 (paper, Section 4.4). # 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). ```{r multiple-fit} 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) ``` 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). # Fixed linear adjustment model The adjustment model can be prespecified instead of selected: - `fp(x, df = 1)` gives a linear adjustment effect, with no FP power search. - `keep` forces the named variables into the adjustment model. ```{r fixed-linear-fit} 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") ) print(fit_fixed_linear) ``` `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). # 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). # 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). ```{r xy-data} 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`: ```{r xy-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" ) print(fit_unadjusted_xy) ``` 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: ```{r xy-other-analyses, eval=FALSE} 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: ```{r xy-fixed-linear, eval=FALSE} 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). # 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). ## Paper reference {#paper-reference} Kipruto, E., Royston, P., and Sauerbrei, W. *Multivariable Fractional Polynomial Interaction Models in R*. Manuscript submitted to the *Journal of Statistical Software*.