This vignette introduces Multivariable Functional Mendelian Randomization (MV-FMR), a method to estimate time-varying causal effects of multiple correlated longitudinal exposures on health outcomes.
Use joint multivariable estimation (mvfmr()) when:
# Install from GitHub
devtools::install_github("NicoleFontana/mvfmr")library(mvfmr)
library(fdapace)
library(ggplot2)
#> Warning: package 'ggplot2' was built under R version 4.3.3We generate genetic instruments and two longitudinal exposures:
set.seed(12345)
# Generate exposure data
sim_data <- getX_multi_exposure(
N = 300, # Sample size
J = 25, # Number of genetic instruments
nSparse = 10 # Observations per subject
)
# Check dimensions
cat("Sample size:", nrow(sim_data$details$G), "\n")
#> Sample size: 300
cat("Number of instruments:", ncol(sim_data$details$G), "\n")
#> Number of instruments: 25We create an outcome with linear effect from X1 and quadratic effect from X2:
outcome_data <- getY_multi_exposure(
sim_data,
X1Ymodel = "2", # Linear: β₁(t) = 0.02 * t
X2Ymodel = "5", # Late-age effect: β₂(t) = 0.1 * (t > 30)
X1_effect = TRUE,
X2_effect = TRUE,
outcome_type = "continuous"
)
cat("Outcome summary:\n")
#> Outcome summary:
summary(outcome_data$Y)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> -111.070 -31.849 -5.190 -3.012 26.394 112.621We apply FPCA to both exposures to extract principal components:
# FPCA for exposure 1
fpca1 <- FPCA(
sim_data$X1$Ly_sim,
sim_data$X1$Lt_sim,
list(dataType = 'Sparse', error = TRUE, verbose = FALSE)
)
# FPCA for exposure 2
fpca2 <- FPCA(
sim_data$X2$Ly_sim,
sim_data$X2$Lt_sim,
list(dataType = 'Sparse', error = TRUE, verbose = FALSE)
)
cat("FPCA completed:\n")
#> FPCA completed:
cat(" Exposure 1:", fpca1$selectK, "components selected\n")
#> Exposure 1: 3 components selected
cat(" Exposure 2:", fpca2$selectK, "components selected\n")
#> Exposure 2: 4 components selectedNow we perform joint estimation using mvfmr():
result_joint <- mvfmr(
G = sim_data$details$G,
fpca_results = list(fpca1, fpca2),
Y = outcome_data$Y,
outcome_type = "continuous",
method = "gmm",
max_nPC1 = 4,
max_nPC2 = 4,
improvement_threshold = 0.001,
bootstrap = FALSE,
n_cores = 1,
true_effects = list(model1 = "2", model2 = "5"),
X_true = list(X1_true = sim_data$details$X1, X2_true = sim_data$details$X2),
verbose = FALSE
)
# View results
print(result_joint)
#>
#> Functional Multivariable MR Result
#> ==================================
#> Exposures: 2
#> Sample size: 300
#> Outcome: continuous
#> Method: gmm
#> Components selected: nPC1 = 2 , nPC2 = 2
#>
#> Performance Metrics:
#> Exposure 1 - MISE: 0.001899 , Coverage: 0.824
#> Exposure 2 - MISE: 0.001591 , Coverage: 0.922The estimated time-varying causal effects can be visualized using the built-in plot method:
# Plot both effects
plot(result_joint)The solid colored lines show the estimated time-varying causal effects, with shaded bands representing 95% confidence intervals. The dashed red lines (when present) indicate the true time-varying effects used in the simulation.
# Estimated beta coefficients for basis functions
coef(result_joint)
#> [1] -3.7775154 1.5166093 -0.3552057 0.4624341
# Time-varying effects at each time point
head(result_joint$effects$effect1)
#> [1] 0.02665083 0.03882021 0.05180349 0.06570553 0.08061147 0.09654441
head(result_joint$effects$effect2)
#> [1] -0.04923079 -0.05503448 -0.06005135 -0.06404133 -0.06679739 -0.06817082Since we simulated data with known truth, we can evaluate performance:
cat("Performance Metrics:\n")
#> Performance Metrics:
cat("\nExposure 1 (Linear effect):\n")
#>
#> Exposure 1 (Linear effect):
cat(" MISE:", round(result_joint$performance$MISE1, 6), "\n")
#> MISE: 0.001899
cat(" Coverage:", round(result_joint$performance$Coverage1, 3), "\n")
#> Coverage: 0.824
cat("\nExposure 2 (Quadratic effect):\n")
#>
#> Exposure 2 (Quadratic effect):
cat(" MISE:", round(result_joint$performance$MISE2, 6), "\n")
#> MISE: 0.001591
cat(" Coverage:", round(result_joint$performance$Coverage2, 3), "\n")
#> Coverage: 0.922We can compare joint (multivariable) with separate (univariable) estimation:
result_separate <- mvfmr_separate(
G1 = sim_data$details$G,
G2 = sim_data$details$G,
fpca_results = list(fpca1, fpca2),
Y = outcome_data$Y,
outcome_type = "continuous",
method = "gmm",
max_nPC1 = 4,
max_nPC2 = 4,
n_cores = 1,
true_effects = list(model1 = "2", model2 = "5"),
verbose = FALSE
)
#> [1] "Processing X1"
#> [1] "Processing X2"
print(result_separate)
#>
#> Separate Univariable MR Results
#> ================================
#> Exposures: 2
#> Separate instruments: TRUE
#> Outcome: continuous
#> Method: gmm
#>
#> Exposure 1:
#> Components: 2
#> MSE: 0.005231
#> Coverage: 1
#>
#> Exposure 2:
#> Components: 2
#> MSE: 0.075436
#> Coverage: 0.706comparison <- data.frame(
Method = rep(c("Joint (MV-FMR)", "Separate (U-FMR)"), each = 2),
Exposure = rep(c("X1", "X2"), times = 2),
MISE = c(
result_joint$performance$MISE1,
result_joint$performance$MISE2,
result_separate$exposure1$performance$MISE,
result_separate$exposure2$performance$MISE
),
Coverage = c(
result_joint$performance$Coverage1,
result_joint$performance$Coverage2,
result_separate$exposure1$performance$Coverage,
result_separate$exposure2$performance$Coverage
)
)
print(comparison)
#> Method Exposure MISE Coverage
#> 1 Joint (MV-FMR) X1 0.001899365 0.8235294
#> 2 Joint (MV-FMR) X2 0.001591153 0.9215686
#> 3 Separate (U-FMR) X1 0.005230941 1.0000000
#> 4 Separate (U-FMR) X2 0.075436010 0.7058824Key Insight: Joint estimation (MV-FMR) typically performs better when exposures are correlated or share pleiotropic instruments.
Check instrument strength using F-statistics:
# Calculate F-statistics
K_total <- result_joint$nPC_used$nPC1 + result_joint$nPC_used$nPC2
fstats <- IS(
J = ncol(sim_data$details$G),
K = K_total,
PC = 1:K_total,
datafull = cbind(
sim_data$details$G,
cbind(fpca1$xiEst[, 1:result_joint$nPC_used$nPC1],
fpca2$xiEst[, 1:result_joint$nPC_used$nPC2])
),
Y = outcome_data$Y
)
print(fstats)
#> PC RR FF cFF Qvalue df pvalue
#> [1,] 1 0.06929360 0.8160015 0.5010741 227.2324 21 1.468923e-36
#> [2,] 2 0.06516010 0.7639326 0.5788612 227.2324 21 1.468923e-36
#> [3,] 3 0.08536319 1.0228984 0.4612395 227.2324 21 1.468923e-36
#> [4,] 4 0.09989841 1.2164033 1.0989657 227.2324 21 1.468923e-36Rule of thumb: cFF > 10 indicates strong instruments.
MV-FMR also supports binary outcomes using the control function approach:
# Generate binary outcome
outcome_binary <- getY_multi_exposure(
sim_data,
X1Ymodel = "2",
X2Ymodel = "5",
outcome_type = "binary"
)
# Estimate with control function
result_binary <- mvfmr(
G = sim_data$details$G,
fpca_results = list(fpca1, fpca2),
Y = outcome_binary$Y,
outcome_type = "binary",
method = "cf", # Control function for binary
max_nPC1 = 3,
max_nPC2 = 3,
n_cores = 1,
verbose = FALSE
)
print(result_binary)vignette("univariable-fmr") for single exposure analysisinst/examples/test_MV-FMR.R for complete scenariosIf you use this package, please cite:
Fontana, N., Ieva, F., Zuccolo, L., Di Angelantonio, E., & Secchi, P. (2025). Unraveling time-varying causal effects of multiple exposures: integrating Functional Data Analysis with Multivariable Mendelian Randomization. arXiv preprint arXiv:2512.19064.
sessionInfo()
#> R version 4.3.0 (2023-04-21)
#> Platform: aarch64-apple-darwin20 (64-bit)
#> Running under: macOS Ventura 13.6
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0
#>
#> locale:
#> [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#>
#> time zone: Europe/Rome
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] ggplot2_3.5.2 fdapace_0.5.9 mvfmr_0.1.0
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 shape_1.4.6 xfun_0.41
#> [4] bslib_0.6.1 htmlwidgets_1.6.4 lattice_0.21-8
#> [7] numDeriv_2016.8-1.1 vctrs_0.6.5 tools_4.3.0
#> [10] generics_0.1.3 parallel_4.3.0 tibble_3.2.1
#> [13] fansi_1.0.6 highr_0.10 cluster_2.1.4
#> [16] pkgconfig_2.0.3 Matrix_1.6-4 data.table_1.14.10
#> [19] checkmate_2.3.1 lifecycle_1.0.4 farver_2.1.1
#> [22] compiler_4.3.0 stringr_1.5.1 progress_1.2.3
#> [25] munsell_0.5.0 codetools_0.2-19 htmltools_0.5.7
#> [28] sass_0.4.8 yaml_2.3.8 glmnet_4.1-8
#> [31] htmlTable_2.4.2 Formula_1.2-5 pracma_2.4.4
#> [34] pillar_1.9.0 crayon_1.5.3 jquerylib_0.1.4
#> [37] MASS_7.3-58.4 cachem_1.0.8 Hmisc_5.1-1
#> [40] iterators_1.0.14 rpart_4.1.19 foreach_1.5.2
#> [43] tidyselect_1.2.0 digest_0.6.33 stringi_1.8.3
#> [46] dplyr_1.1.4 labeling_0.4.3 splines_4.3.0
#> [49] fastmap_1.1.1 grid_4.3.0 colorspace_2.1-0
#> [52] cli_3.6.2 magrittr_2.0.3 base64enc_0.1-3
#> [55] survival_3.5-7 utf8_1.2.4 withr_2.5.2
#> [58] foreign_0.8-84 prettyunits_1.2.0 scales_1.3.0
#> [61] backports_1.4.1 rmarkdown_2.25 nnet_7.3-18
#> [64] gridExtra_2.3 hms_1.1.3 evaluate_0.23
#> [67] knitr_1.45 doParallel_1.0.17 rlang_1.1.6
#> [70] Rcpp_1.0.11 glue_1.6.2 pROC_1.18.5
#> [73] rstudioapi_0.15.0 jsonlite_1.8.8 R6_2.5.1
#> [76] plyr_1.8.9