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. https://arxiv.org/abs/2512.19064
@misc{fontana2025unravelingtimevaryingcausaleffects,
title={Unraveling time-varying causal effects of multiple exposures: integrating Functional Data Analysis with Multivariable Mendelian Randomization},
author={Nicole Fontana and Francesca Ieva and Luisa Zuccolo and Emanuele Di Angelantonio and Piercesare Secchi},
year={2025},
eprint={2512.19064},
archivePrefix={arXiv},
primaryClass={stat.AP},
url={https://arxiv.org/abs/2512.19064},
}The mvfmr package implements Multivariable Functional Mendelian Randomization methods to estimate time-varying causal effects of longitudinal exposures on health outcomes. The package supports:
- Multivariable Functional MR (MV-FMR): Joint estimation of an arbitrary number of correlated time-varying exposures
- Univariable Functional MR (U-FMR): Separate estimation of each exposure independently (including single-exposure analysis)
- Both continuous and binary outcomes
- One-sample and two-sample MR designs (the latter using outcome GWAS summary statistics)
- Automatic component selection via cross-validation
- Mediation pathways between exposures
- Bootstrap inference for confidence intervals
- Instrument strength diagnostics
Install the released version from CRAN:
install.packages("mvfmr")Or install the development version from GitHub:
install.packages("devtools") # Install devtools if not already installed
devtools::install_github("NicoleFontana/mvfmr")The package ships with test scripts demonstrating different use cases. Once installed, they live inside the package itself, so you can run or open them with demo() / system.file() — no download needed:
Reproduces the main simulation scenarios from the manuscript:
- Scenario 1-3: pleiotropy, null effects, mediation
- Exposure effects: linear and quadratic
- Performance comparison: MV-FMR vs U-FMR across scenarios
- Evaluation: MISE, coverage rates
# Run the manuscript simulations
demo("tests_manuscript", package = "mvfmr")Complete tutorial for using joint multivariable estimation:
- Data simulation with two exposures
- FPCA and component selection
- Joint estimation with
mvfmr() - Instrument diagnostics
- Performance metrics and visualization
- Binary outcome analysis
- Comparison with univariable estimation
# Run the MV-FMR tutorial
source(system.file("examples", "test_MV-FMR.R", package = "mvfmr"))
# Or just locate it to open/copy/adapt it:
system.file("examples", "test_MV-FMR.R", package = "mvfmr")Complete tutorial for single exposure analysis:
- Single exposure simulation
- Univariable estimation with
mvfmr_separate() - Instrument diagnostics
- Performance metrics and visualization
- Comparison of different exposure effects
- Binary outcome analysis
# Run the U-FMR tutorial
source(system.file("examples", "test_U-FMR.R", package = "mvfmr"))Note: These scripts serve as templates for your own analyses. Open them (via system.file()), modify the parameters, and adapt to your specific research questions.
Exposure-related arguments (fpca_results, max_nPC, true_effects, X_true, ...) are lists or vectors of length m, one entry per exposure.
library(mvfmr)
library(fdapace)
# Step 1: Simulate exposure data (m = 2 exposures)
set.seed(12345)
sim_data <- getX_multi_exposure(
N = 1000, # Sample size
J = 50, # Number of genetic instruments
nSparse = 10, # Sparse observations per subject
n_exposures = 2 # Number of exposures (m)
)
# Step 2: Generate outcome
outcome_data <- getY_multi_exposure(
sim_data,
XYmodels = c("2", "8"), # Linear effect for exposure 1, quadratic for exposure 2
X_effects = c(TRUE, TRUE),
outcome_type = "continuous"
)
# Step 3: Functional PCA for each exposure
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)
)
})
# Step 4: Joint estimation with MV-FMR
result <- mvfmr(
G = sim_data$details$G,
fpca_results = fpca_results,
Y = outcome_data$Y,
outcome_type = "continuous",
method = "gmm",
max_nPC = c(10, 10),
bootstrap = TRUE,
n_bootstrap = 100
)
# View results
print(result)
summary(result)
plot(result) # Visualize time-varying effects for every exposure
# Extract coefficients and effects
coef(result)
result$effects[[1]] # Time-varying effect for exposure 1
result$effects[[2]] # Time-varying effect for exposure 2Continuing from Example 1: compare joint vs. separate (univariable) estimation
for the same two exposures. 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):
# Separate estimation (U-FMR for each exposure independently), reusing
# sim_data / outcome_data / fpca_results simulated in Example 1
result_separate <- mvfmr_separate(
G_list = list(sim_data$details$G, sim_data$details$G),
fpca_results = fpca_results,
Y = outcome_data$Y
)
# Compare performance: joint (`result`, from Example 1) vs. separate
result$performance
result_separate$exposures[[1]]$performance
result_separate$exposures[[2]]$performanceThe package can also be used for single exposure analysis (U-FMR), by simulating and passing a single exposure (n_exposures = 1, G_list of length 1):
library(mvfmr)
library(fdapace)
# Step 1: Simulate a single exposure
set.seed(12345)
sim_data <- getX_multi_exposure(
N = 1000,
J = 50,
nSparse = 10,
n_exposures = 1
)
# Step 2: Generate outcome
outcome_data <- getY_multi_exposure(
sim_data,
XYmodels = "2", # Linear effect
X_effects = TRUE,
outcome_type = "continuous"
)
# Step 3: FPCA for the (only) exposure
fpca1 <- FPCA(
sim_data$exposures[[1]]$Ly_sim,
sim_data$exposures[[1]]$Lt_sim,
list(dataType = 'Sparse', error = TRUE, verbose = FALSE)
)
# Step 4: Univariable estimation
result <- mvfmr_separate(
G_list = list(sim_data$details$G), # A list of length 1
fpca_results = list(fpca1),
Y = outcome_data$Y,
outcome_type = "continuous",
method = "gmm",
max_nPC = 10
)
# View results
print(result)
coef(result, exposure = 1)
result$exposures[[1]]$effect # Time-varying effectNothing 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(2026)
sim_data3 <- getX_multi_exposure(N = 1000, J = 50, nSparse = 10, n_exposures = 3)
outcome_data3 <- getY_multi_exposure(
sim_data3,
XYmodels = c("2", "5", "8"),
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))
})
# Joint estimation across all 3 exposures
result_joint3 <- mvfmr(
G = sim_data3$details$G,
fpca_results = fpca_results3,
Y = outcome_data3$Y,
outcome_type = "continuous",
method = "gmm",
max_nPC = c(10, 10, 10),
true_effects = c("2", "5", "8"),
X_true = sim_data3$details$X_list
)
print(result_joint3)
plot(result_joint3) # One panel per exposure
# Separate estimation across all 3 exposures
result_separate3 <- mvfmr_separate(
G_list = list(sim_data3$details$G, sim_data3$details$G, sim_data3$details$G),
fpca_results = fpca_results3,
Y = outcome_data3$Y,
max_nPC = c(10, 10, 10),
true_effects = c("2", "5", "8")
)
# Access any exposure by index (1..m)
result_joint3$effects[[3]]
coef(result_separate3, exposure = 3)Use outcome GWAS summary statistics instead of individual-level outcome data.
library(mvfmr)
library(fdapace)
# Step 1: Simulate exposure data (individual-level)
set.seed(12345)
sim_data <- getX_multi_exposure(
N = 5000, # Exposure sample size
J = 30, # Number of genetic instruments (SNPs)
nSparse = 10,
n_exposures = 2
)
# Perform FPCA on longitudinal exposures
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))
})
# Step 2: Get outcome GWAS summary statistics (from a separate study)
# Simulate obtaining summary statistics from a separate GWAS
# (this mimics what you'd get from a published GWAS)
by_outcome <- rnorm(30, mean = 0.02, sd = 0.01) # SNP-outcome associations
sy_outcome <- runif(30, 0.005, 0.015) # Standard errors
ny_outcome <- 100000 # GWAS sample size
# Step 3: Two-sample MV-FMR estimation
result_twosample <- fmvmr_twosample(
G_exposure = sim_data$details$G, # Genotypes from the exposure sample
fpca_results = fpca_results, # FPCA from the exposures
by_outcome = by_outcome, # GWAS betas (from the outcome study)
sy_outcome = sy_outcome, # GWAS standard errors
ny_outcome = ny_outcome, # GWAS sample size
max_nPC = c(3, 3),
verbose = TRUE
)
# Step 4: View results
print(result_twosample)
# Extract time-varying effects
result_twosample$effects[[1]]
result_twosample$effects[[2]]getX_multi_exposure() - Generate genetic instruments and exposure data for m exposures
getX_multi_exposure(
N = 1000, # Sample size
J = 50, # Number of genetic instruments
nSparse = 10, # Observations per subject
n_exposures = 2, # Number of exposures (m)
shared_effect = TRUE, # Whether all exposures share the same time-varying confounding
separate_G = FALSE, # Whether to use separate instruments per exposure
shared_G_proportion = 0.15 # Proportion of shared instruments (0-1, if separate_G = TRUE)
)getX_multi_exposure_mediation() - Generate data with mediation pathways between exposures
getX_multi_exposure_mediation(
N = 1000, # Sample size
J = 50, # Number of genetic instruments
nSparse = 10, # Observations per subject
n_exposures = 2, # Number of exposures (m)
mediation_strength = NULL, # m x m matrix: entry [j, k] (j < k) is the strength
# with which exposure j mediates its effect onto
# exposure k. Default: NULL = no mediation.
mediation_type = "linear" # "linear", "nonlinear", "time_varying" (scalar or
# m x m matrix mirroring mediation_strength)
)getY_multi_exposure() - Generate outcome with time-varying effects
getY_multi_exposure(
RES, # Output from getX_multi_exposure() or getX_multi_exposure_mediation()
XYmodels = NULL, # Length-m vector of effect models, one per exposure (see below); default '1' for all
X_effects = NULL, # Length-m logical vector: include each exposure's effect?; default TRUE for all
outcome_type = "continuous" # "continuous" or "binary"
)Available effect models:
"0"- No effect (null)"1"- Constant effect (β = 0.1)"2"- Linear increasing (β(t) = 0.02×t)"3"- Linear decreasing (β(t) = 0.5 - 0.02×t)"4"- Early life effect (β(t) = 0.1 for t < 20)"5"- Late life effect (β(t) = 0.1 for t > 30)"6"- Early decreasing (β(t) = 0.05×(20-t) for t < 20)"7"- Late increasing (β(t) = 0.05×(t-30) for t > 30)"8"- Quadratic (β(t) = 0.002×t² - 0.11×t + 0.5)"9"- Cubic (β(t) = -0.00002×t³ + 0.004×t² - 0.2×t + 1)
mvfmr() - Joint multivariable estimation
mvfmr(
G, # Genetic instrument matrix (N x J)
fpca_results, # List of length m of FPCA objects, one per exposure
Y, # Outcome vector
outcome_type = "continuous", # "continuous" or "binary"
method = "gmm", # "gmm", "cf" (control function), or "cf-lasso"
nPC = NA, # Fixed number of components per exposure (length 1 or m; NA = select automatically)
max_nPC = NA, # Maximum number of components per exposure (length 1 or m)
improvement_threshold = 0.001, # Minimum CV improvement required to add a component
bootstrap = FALSE, # Whether to compute bootstrap confidence intervals
n_bootstrap = 100, # Number of bootstrap replicates
n_cores = parallel::detectCores() - 1, # Number of CPU cores for parallel computations
true_effects = NULL, # Length-m vector of true effect model codes (simulation only)
X_true = NULL, # Length-m list of true X curves (simulation only)
verbose = FALSE # Print progress and diagnostic messages
)mvfmr_separate() - Separate univariable estimation
mvfmr_separate(
G_list, # List of length m of genetic instrument matrices, one per exposure
# (use a list of length 1 to analyze a single exposure)
fpca_results, # List of length m of FPCA objects, same length as G_list
Y, # Outcome vector
outcome_type = "continuous",
method = "gmm",
nPC = NA,
max_nPC = NA,
improvement_threshold = 0.001,
bootstrap = FALSE,
n_bootstrap = 100,
n_cores = parallel::detectCores() - 1,
true_effects = NULL,
X_true = NULL,
verbose = FALSE
)fmvmr_twosample() - Two-sample joint multivariable estimation
fmvmr_twosample(
G_exposure, # Genetic instrument matrix from the exposure sample (N x J)
fpca_results, # List of length m of FPCA objects
by_outcome, # Vector of SNP-outcome betas from the outcome GWAS, length J
sy_outcome, # Vector of standard errors for SNP-outcome effects, length J
ny_outcome, # Sample size of the outcome GWAS
max_nPC = NA, # Maximum number of components per exposure (length 1 or m)
true_effects = NULL, # Length-m vector of true effect model codes (simulation only)
verbose = TRUE
)fmvmr_separate_twosample() - Two-sample separate univariable estimation
fmvmr_separate_twosample(
G_list, # List of length m of genetic instrument matrices
fpca_results, # List of length m of FPCA objects
by_outcome_list, # List of length m of SNP-outcome beta vectors
sy_outcome_list, # List of length m of SNP-outcome standard error vectors
ny_outcome, # Outcome GWAS sample size
max_nPC = NA,
true_effects = NULL,
verbose = TRUE
)IS() - Calculate instrument strength (F-statistics)
IS(
J, # Number of genetic instruments
K, # Number of exposures/components
PC, # Vector of indices indicating which columns in datafull correspond to the principal components
datafull, # Data frame containing instruments (first J columns) and principal components (subsequent columns) [G, X]
Y # Optional outcome vector; if provided, Q-statistic for overidentification is calculated
)The package supports three estimation methods:
-
GMM (Generalized Method of Moments) - For continuous outcomes
- Efficient two-step GMM estimation
- Optimal weighting matrix
-
Control Function (CF) - For binary outcomes
- Two-stage residual inclusion (2SRI)
- Logistic regression second stage
-
Control Function (CF)-LASSO - Control function with LASSO regularization
- Cross-validated penalty selection
result <- mvfmr(...)
names(result)Components:
coefficients- Estimated β coefficients for basis functions (stacked across all exposures)vcov- Variance-covariance matrixeffects- List of length m, one time-varying effect curve per exposureconfidence_intervals-lower/upper, each a list of length mnPC_used- Vector of length m: components selected per exposureperformance-MISEandCoverage(lists of length m), only for simulationsplots-effects(list of m ggplot2 objects) andplot_beta(combined coefficient plot)
Methods:
print(),summary()- Display resultsplot()- Visualize time-varying effects for every exposurecoef()- Extract coefficientsvcov()- Extract variance-covariance matrix
result <- mvfmr_separate(...)
names(result)Components:
exposures- List of length m; each entry hascoefficients,vcov,effect,nPC_used,performanceplots-effects, a list of m ggplot2 objects
Methods:
coef(result, exposure = k)- Extract coefficients for exposurek(1..m)vcov(result, exposure = k)- Extract variance-covariance matrix for exposurek
For binary outcomes, use method = "cf" or method = "cf-lasso":
# Generate binary outcome
outcome_binary <- getY_multi_exposure(
sim_data,
XYmodels = c("2", "8"),
outcome_type = "binary"
)
# Estimate with control function
result <- mvfmr(
G = sim_data$details$G,
fpca_results = list(fpca1, fpca2),
Y = outcome_binary$Y,
outcome_type = "binary",
method = "cf"
)Automatic selection via cross-validation:
result <- mvfmr(
G = G,
fpca_results = list(fpca1, fpca2),
Y = Y,
max_nPC = c(10, 10), # Search up to 10 components per exposure
improvement_threshold = 0.01 # Stop if improvement < 1%
)
# View selected components
result$nPC_usedresult <- mvfmr(
G = G,
fpca_results = list(fpca1, fpca2),
Y = Y,
bootstrap = TRUE,
n_bootstrap = 200 # Number of bootstrap replicates
)
# Bootstrap confidence intervals available in:
result$confidence_intervalsresult <- mvfmr(
G = G,
fpca_results = list(fpca1, fpca2),
Y = Y,
n_cores = 4 # Use 4 cores for cross-validation
)mediation_strength is an m x m matrix: entry [j, k] (with j < k) is the
strength with which exposure j mediates its effect onto exposure k. Any
exposure can mediate onto any later one, each with its own strength, so
mediation chains with more than two exposures (e.g. X1 -> X2, X1 -> X3, X2 -> X3)
are supported directly.
# Generate data where exposure 1 mediates onto exposure 2
mediation_strength <- matrix(0, 2, 2)
mediation_strength[1, 2] <- 0.5
sim_mediation <- getX_multi_exposure_mediation(
N = 1000,
J = 50,
n_exposures = 2,
mediation_strength = mediation_strength,
mediation_type = "linear"
)
outcome <- getY_multi_exposure(
sim_mediation,
XYmodels = c("2", "1"), # Direct effect of exposure 1; effect of exposure 2 (mediator)
outcome_type = "continuous"
)
fpca_results <- lapply(sim_mediation$exposures, function(exp_k) {
FPCA(exp_k$Ly_sim, exp_k$Lt_sim, list(dataType = 'Sparse', error = TRUE, verbose = FALSE))
})
# Estimate with MV-FMR to capture mediation
result <- mvfmr(
G = sim_mediation$details$G,
fpca_results = fpca_results,
Y = outcome$Y
)Check instrument strength with F-statistics (IS() is generic in the number of exposures/components K):
# After FPCA
K_total <- sum(sapply(fpca_results, function(f) f$selectK))
PC_stacked <- do.call(cbind, lapply(fpca_results, function(f) f$xiEst[, 1:f$selectK]))
fstats <- IS(
J = ncol(G),
K = K_total,
PC = 1:K_total,
datafull = cbind(G, PC_stacked)
)
# View conditional F-statistics (cFF)
print(fstats)When true effects are provided (simulations):
- MISE (Mean Integrated Squared Error): Average squared difference between estimated and true effect curves
- Coverage: Proportion of time points where true effect falls within 95% CI
This package extends the univariable functional Mendelian Randomization framework to the multivariable setting. Key related work:
Our implementation builds upon and extends the TVMR package by Tian et al.:
Tian, H., Mason, A. M., Liu, C., & Burgess, S. (2024). Estimating time‐varying exposure effects through continuous‐time modelling in Mendelian randomization. Statistics in Medicine, 43(26), 5006-5024. https://doi.org/10.1002/sim.10222
GitHub: https://github.com/HDTian/TVMR
Nicole Fontana
MIT — see the LICENSE file for details.
For questions and issues:
- Open an issue on GitHub
- Email: nicole.fontana@polimi.it