The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Introduction to Multivariable Functional Mendelian Randomization

Nicole Fontana

2026-07-14

Overview

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.

Key Features

When to Use MV-FMR

Use joint multivariable estimation (mvfmr()) when:

Installation

# Install from CRAN
install.packages("mvfmr")
library(mvfmr)
library(fdapace)
library(ggplot2)
#> Warning: package 'ggplot2' was built under R version 4.3.3

Example: Joint Estimation of Two Exposures

Step 1: Simulate Data

We generate genetic instruments and two longitudinal exposures:

set.seed(473920)

# Generate exposure data
sim_data <- getX_multi_exposure(
  N = 300,              # Sample size
  J = 25,               # Number of genetic instruments
  nSparse = 10,          # Observations per subject
  n_exposures = 2        # Number of exposures (m)
)

# 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: 25

Step 2: Generate Outcome

We create an outcome with linear effect from exposure 1 and exposure 2:

outcome_data <- getY_multi_exposure(
  sim_data,
  XYmodels = c("2", "2"),     # Exposure 1/2: linear beta(t) = 0.02*t
  X_effects = c(TRUE, TRUE),
  outcome_type = "continuous"
)

cat("Outcome summary:\n")
#> Outcome summary:
summary(outcome_data$Y)
#>    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#> -236.50  -71.40  -26.03  -23.83   28.53  182.65

Step 3: Functional Principal Component Analysis

We apply FPCA to each exposure to extract principal components:

fpca_results <- lapply(sim_data$exposures, function(exp_k) {
  FPCA(
    exp_k$Ly_sim,
    exp_k$Lt_sim,
    list(dataType = 'Sparse', error = TRUE, verbose = FALSE)
  )
})

cat("FPCA completed:\n")
#> FPCA completed:
for (k in seq_along(fpca_results)) {
  cat("  Exposure", k, ":", fpca_results[[k]]$selectK, "components selected\n")
}
#>   Exposure 1 : 3 components selected
#>   Exposure 2 : 3 components selected

Step 4: Joint Multivariable Estimation

Now we perform joint estimation using mvfmr(). All exposure-related arguments (fpca_results, max_nPC, true_effects, X_true, …) are lists or vectors of length m, one entry per exposure:

result_joint <- mvfmr(
  G = sim_data$details$G,
  fpca_results = fpca_results,
  Y = outcome_data$Y,
  outcome_type = "continuous",
  method = "gmm",
  max_nPC = c(4, 4),
  improvement_threshold = 0.001,
  bootstrap = FALSE,
  n_cores = 1,
  true_effects = c("2", "2"),
  X_true = sim_data$details$X_list,
  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.002458 , Coverage: 1 
#>   Exposure 2 - MISE: 0.003881 , Coverage: 0.745

Step 5: Visualize Time-Varying Effects

The estimated time-varying causal effects can be visualized using the built-in plot method:

# Plot every exposure's effect
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.

Step 6: Extract Coefficients

# Estimated beta coefficients for basis functions
coef(result_joint)
#> [1] -4.035692 -1.647212 -3.634343 -1.451868

# Time-varying effects at each time point (one entry per exposure)
head(result_joint$effects[[1]])
#> [1] 0.06182589 0.07321675 0.08677954 0.10220947 0.11906258 0.13675702
head(result_joint$effects[[2]])
#> [1] 0.02302308 0.02248962 0.02740122 0.03681268 0.04977623 0.06537926

Step 7: Performance Metrics

Since we simulated data with known truth, we can evaluate performance:

cat("Performance Metrics:\n")
#> Performance Metrics:
for (k in seq_along(result_joint$effects)) {
  cat("\nExposure", k, ":\n")
  cat("  MISE:", round(result_joint$performance$MISE[[k]], 6), "\n")
  cat("  Coverage:", round(result_joint$performance$Coverage[[k]], 3), "\n")
}
#> 
#> Exposure 1 :
#>   MISE: 0.002458 
#>   Coverage: 1 
#> 
#> Exposure 2 :
#>   MISE: 0.003881 
#>   Coverage: 0.745

Comparison: Joint vs. Separate Estimation

We can compare joint (multivariable) with separate (univariable) estimation. For mvfmr_separate(), instruments are passed as G_list, a list of length m (here the same shared instrument matrix is reused for both exposures):

result_separate <- mvfmr_separate(
  G_list = list(sim_data$details$G, sim_data$details$G),
  fpca_results = fpca_results,
  Y = outcome_data$Y,
  outcome_type = "continuous",
  method = "gmm",
  max_nPC = c(4, 4),
  n_cores = 1,
  true_effects = c("2", "2"),
  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.178885 
#>   Coverage: 0 
#> 
#> Exposure 2 :
#>   Components: 2 
#>   MSE: 0.252422 
#>   Coverage: 0.157

Performance Comparison

comparison <- 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$MISE[[1]],
    result_joint$performance$MISE[[2]],
    result_separate$exposures[[1]]$performance$MISE,
    result_separate$exposures[[2]]$performance$MISE
  ),
  Coverage = c(
    result_joint$performance$Coverage[[1]],
    result_joint$performance$Coverage[[2]],
    result_separate$exposures[[1]]$performance$Coverage,
    result_separate$exposures[[2]]$performance$Coverage
  )
)

print(comparison)
#>             Method Exposure        MISE  Coverage
#> 1   Joint (MV-FMR)       X1 0.002458118 1.0000000
#> 2   Joint (MV-FMR)       X2 0.003881328 0.7450980
#> 3 Separate (U-FMR)       X1 0.178885369 0.0000000
#> 4 Separate (U-FMR)       X2 0.252422123 0.1568627

Instrument Strength Diagnostics

Check instrument strength using F-statistics. IS() is already generic in the number of exposures/components K:

# Calculate F-statistics
K_total <- sum(result_joint$nPC_used)

PC_stacked <- do.call(cbind, lapply(seq_along(fpca_results), function(k) {
  fpca_results[[k]]$xiEst[, 1:result_joint$nPC_used[k]]
}))

fstats <- IS(
  J = ncol(sim_data$details$G),
  K = K_total,
  PC = 1:K_total,
  datafull = cbind(sim_data$details$G, PC_stacked),
  Y = outcome_data$Y
)

fstats_df <- cbind(
  "Exposure" = unlist(lapply(seq_along(result_joint$nPC_used), function(k) {
    rep(paste0("X", k), result_joint$nPC_used[k])
  })),
  as.data.frame(fstats)
)

print(fstats_df[, c("Exposure", "PC", "cFF")])
#>   Exposure PC       cFF
#> 1       X1  1 0.3913644
#> 2       X1  2 1.1379411
#> 3       X2  3 0.3986037
#> 4       X2  4 0.9953631

Rule of thumb: cFF > 10 indicates strong instruments.

Binary Outcomes

MV-FMR also supports binary outcomes using the control function approach:

# Generate binary outcome
outcome_binary <- getY_multi_exposure(
  sim_data,
  XYmodels = c("2", "2"),
  outcome_type = "binary"
)

# Estimate with control function
result_binary <- mvfmr(
  G = sim_data$details$G,
  fpca_results = fpca_results,
  Y = outcome_binary$Y,
  outcome_type = "binary",
  method = "cf",  # Control function for binary
  max_nPC = c(3, 3),
  n_cores = 1,
  verbose = FALSE
)

print(result_binary)

Extending to More Than Two Exposures

Nothing changes in the API when moving from 2 to m exposures: fpca_results, max_nPC, true_effects and X_true simply grow to length m.

set.seed(163918)#281046
sim_data3 <- getX_multi_exposure(N = 500, J = 50, nSparse = 10, n_exposures = 3)

outcome_data3 <- getY_multi_exposure(
  sim_data3,
  XYmodels = c("2", "2", "2"),
  outcome_type = "continuous"
)

fpca_results3 <- lapply(sim_data3$exposures, function(exp_k) {
  FPCA(exp_k$Ly_sim, exp_k$Lt_sim, list(dataType = 'Sparse', error = TRUE, verbose = FALSE))
})

result_joint3 <- mvfmr(
  G = sim_data3$details$G,
  fpca_results = fpca_results3,
  Y = outcome_data3$Y,
  outcome_type = "continuous",
  method = "gmm",
  max_nPC = c(4, 4, 4),
  n_cores = 1,
  true_effects = c("2", "2", "2"),
  X_true = sim_data3$details$X_list,
  verbose = FALSE
)

print(result_joint3)
#> 
#> Functional Multivariable MR Result
#> ==================================
#> Exposures: 3 
#> Sample size: 500 
#> Outcome: continuous 
#> Method: gmm 
#> Components selected: nPC1 = 2, nPC2 = 2, nPC3 = 2 
#> 
#> Performance Metrics:
#>   Exposure 1 - MISE: 0.001228 , Coverage: 1 
#>   Exposure 2 - MISE: 0.004026 , Coverage: 0.706 
#>   Exposure 3 - MISE: 0.006513 , Coverage: 0.588
plot(result_joint3)

Next Steps

Learn More

Citation

If 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.

Session Info

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.2.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.5.0     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.1.0          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

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.