Univariable Functional Mendelian Randomization

Overview

This vignette demonstrates Univariable Functional Mendelian Randomization (U-FMR) for estimating the time-varying causal effect of a single longitudinal exposure on a health outcome.

When to Use U-FMR

Use univariable estimation (mvfmr_separate() with G2 = NULL) when:

  • You have a single exposure of interest
  • No need to adjust for other exposures

Installation

devtools::install_github("NicoleFontana/mvfmr")
library(mvfmr)
library(fdapace)
library(ggplot2)

Example: Single Exposure Analysis

Step 1: Simulate Data

set.seed(12345)

# Generate exposure data (we'll only use X1)
sim_data <- getX_multi_exposure(
  N = 300,
  J = 25,
  nSparse = 10
)

cat("Data simulated:\n")
#> Data simulated:
cat("  Sample size:", nrow(sim_data$details$G), "\n")
#>   Sample size: 300
cat("  Instruments:", ncol(sim_data$details$G), "\n")
#>   Instruments: 25

Step 2: Generate Outcome

We create an outcome where only X1 has a causal effect:

outcome_data <- getY_multi_exposure(
  sim_data,
  X1Ymodel = "2",     # Linear effect: β(t) = 0.02×t
  X2Ymodel = "0",     # No effect from X2
  X1_effect = TRUE,
  X2_effect = FALSE,  # X2 does not affect Y
  outcome_type = "continuous"
)

cat("Outcome summary:\n")
#> Outcome summary:
summary(outcome_data$Y)
#>     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
#> -101.272  -30.169   -5.085   -2.914   23.769  103.874

Step 3: FPCA for Single Exposure

# FPCA for exposure 1 only
fpca1 <- FPCA(
  sim_data$X1$Ly_sim, 
  sim_data$X1$Lt_sim,
  list(dataType = 'Sparse', error = TRUE, verbose = FALSE)
)

cat("FPCA completed:\n")
#> FPCA completed:
cat("  Components selected:", fpca1$selectK, "\n")
#>   Components selected: 3
cat("  Variance explained:", 
    round(sum(fpca1$lambda[1:fpca1$selectK]) / sum(fpca1$lambda) * 100, 1), "%\n")
#>   Variance explained: 100 %

Step 4: Univariable Estimation

Estimate the causal effect using mvfmr_separate() with only one exposure:

result <- mvfmr_separate(
  G1 = sim_data$details$G,   # Instruments for X1
  G2 = NULL,                  # No second exposure
  fpca_results = list(fpca1),
  Y = outcome_data$Y,
  outcome_type = "continuous",
  method = "gmm",
  max_nPC1 = 4,
  max_nPC2 = 4,  # Not used when G2 = NULL
  n_cores = 1,
  true_effects = list(model1 = "2", model2 = "0"),
  verbose = FALSE
)
#> [1] "Processing X1"

print(result)
#> 
#> Separate Univariable MR Results
#> ================================
#> Exposures: 1 
#> Separate instruments: TRUE 
#> Outcome: continuous 
#> Method: gmm 
#> 
#> Exposure 1:
#>   Components: 2 
#>   MSE: 0.001915 
#>   Coverage: 0.804

Step 5: Visualize Time-Varying Effect

plot(result)

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 Results

# Coefficients for basis functions
cat("Estimated coefficients:\n")
#> Estimated coefficients:
print(round(coef(result, exposure = 1), 4))
#> [1] -3.8088  1.5315

# Time-varying effect curve
cat("\nFirst 10 time points of β(t):\n")
#> 
#> First 10 time points of β(t):
head(result$exposure1$effect, 10)
#>  [1] 0.02639840 0.03866777 0.05175991 0.06578036 0.08081493 0.09688697
#>  [7] 0.11391916 0.13170830 0.14992295 0.16813235

Step 7: Performance Metrics

cat("Performance:\n")
#> Performance:
cat("  MISE:", round(result$exposure1$performance$MISE, 6), "\n")
#>   MISE: 0.001915
cat("  Coverage:", round(result$exposure1$performance$Coverage, 3), "\n")
#>   Coverage: 0.804
cat("  Components used:", result$exposure1$nPC_used, "\n")
#>   Components used: 2

Binary Outcomes

U-FMR also works with binary outcomes:

# Generate binary outcome
outcome_binary <- getY_multi_exposure(
  sim_data,
  X1Ymodel = "2",
  X2Ymodel = "0",
  X1_effect = TRUE,
  X2_effect = FALSE,
  outcome_type = "binary"
)

# Estimate with control function
result_binary <- mvfmr_separate(
  G1 = sim_data$details$G,
  G2 = NULL,
  fpca_results = list(fpca1),
  Y = outcome_binary$Y,
  outcome_type = "binary",
  method = "cf",      # Control function for binary
  max_nPC1 = 3,
  n_cores = 1,
  verbose = FALSE
)

print(result_binary)
cat("Cases:", sum(outcome_binary$Y == 1), "\n")
cat("Controls:", sum(outcome_binary$Y == 0), "\n")

Advanced Topics

Available Effect Models

The package includes 10 pre-defined time-varying effect shapes:

  • "0": No effect (null)
  • "1": Constant (β = 0.1)
  • "2": Linear increasing
  • "3": Linear decreasing
  • "4": Early life effect
  • "5": Late life effect
  • "6": Early decreasing
  • "7": Late increasing
  • "8": Quadratic (U-shape)
  • "9": Cubic

Bootstrap Inference

# Get robust confidence intervals via bootstrap
result_boot <- mvfmr_separate(
  G1 = sim_data$details$G,
  G2 = NULL,
  fpca_results = list(fpca1),
  Y = outcome_data$Y,
  outcome_type = "continuous",
  bootstrap = TRUE,
  n_bootstrap = 100,
  max_nPC1 = 4,
  verbose = FALSE
)

# Bootstrap CIs are stored in result_boot$exposure1$...

Two-Sample Design

If you have GWAS summary statistics instead of individual-level outcome data:

# Simulate GWAS summary statistics
by_outcome <- rnorm(25, 0.02, 0.01)
sy_outcome <- runif(25, 0.005, 0.015)

result_2sample <- fmvmr_separate_twosample(
  G1_exposure = sim_data$details$G,
  G2_exposure = NULL,
  fpca_results = list(fpca1),
  by_outcome1 = by_outcome,
  by_outcome2 = NULL,
  sy_outcome1 = sy_outcome,
  sy_outcome2 = NULL,
  ny_outcome = 50000,
  max_nPC1 = 3,
  verbose = FALSE
)

print(result_2sample)

Next Steps

Learn More

  • Multivariable FMR: See vignette("multivariable-fmr") for joint estimation
  • Complete examples: Check inst/examples/test_U-FMR.R for all scenarios
  • Manuscript: See paper for methodological details

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.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] ggplot2_4.0.3  fdapace_0.6.0  mvfmr_0.1.0    rmarkdown_2.31
#> 
#> loaded via a namespace (and not attached):
#>  [1] gtable_0.3.6        shape_1.4.6.1       xfun_0.59          
#>  [4] bslib_0.11.0        htmlwidgets_1.6.4   lattice_0.22-9     
#>  [7] numDeriv_2016.8-1.1 vctrs_0.7.3         tools_4.6.1        
#> [10] generics_0.1.4      parallel_4.6.1      tibble_3.3.1       
#> [13] cluster_2.1.8.2     pkgconfig_2.0.3     Matrix_1.7-5       
#> [16] data.table_1.18.4   checkmate_2.3.4     RColorBrewer_1.1-3 
#> [19] S7_0.2.2            lifecycle_1.0.5     compiler_4.6.1     
#> [22] farver_2.1.2        stringr_1.6.0       progress_1.2.3     
#> [25] codetools_0.2-20    htmltools_0.5.9     sys_3.4.3          
#> [28] buildtools_1.0.0    sass_0.4.10         yaml_2.3.12        
#> [31] glmnet_5.0          htmlTable_2.5.0     Formula_1.2-5      
#> [34] pracma_2.4.6        crayon_1.5.3        pillar_1.11.1      
#> [37] jquerylib_0.1.4     MASS_7.3-65         cachem_1.1.0       
#> [40] Hmisc_5.2-6         iterators_1.0.14    rpart_4.1.27       
#> [43] foreach_1.5.2       tidyselect_1.2.1    digest_0.6.39      
#> [46] stringi_1.8.7       dplyr_1.2.1         labeling_0.4.3     
#> [49] maketools_1.3.2     splines_4.6.1       fastmap_1.2.0      
#> [52] grid_4.6.1          colorspace_2.1-2    cli_3.6.6          
#> [55] magrittr_2.0.5      base64enc_0.1-6     survival_3.8-6     
#> [58] withr_3.0.3         foreign_0.8-91      prettyunits_1.2.0  
#> [61] scales_1.4.0        backports_1.5.1     otel_0.2.0         
#> [64] nnet_7.3-20         gridExtra_2.3.1     hms_1.1.4          
#> [67] evaluate_1.0.5      knitr_1.51          doParallel_1.0.17  
#> [70] rlang_1.3.0         Rcpp_1.1.2          glue_1.8.1         
#> [73] pROC_1.19.0.1       rstudioapi_0.19.0   jsonlite_2.0.0     
#> [76] R6_2.6.1