MOFA2: Multi-Omics Factor Analysis
When to Use
- Integrating 2+ omics layers (RNA-seq, proteomics, ATAC/methylation, metabolomics) measured on the same samples into a shared low-dimensional representation.
- Separating "shared" biological signal (appears in all views) from "view-specific" signal (e.g., epigenetic-only or proteomic-only variation).
- Handling missing views/samples (e.g., proteomics missing for a subset of patients) -- MOFA2 natively supports this, unlike PCA on a concatenated matrix.
- Finding candidate genes/proteins/CpGs driving a factor, then running gene set enrichment on the top weights.
- User says "MOFA", "MOFA2", "multi-omics factor analysis", "shared vs specific variance", or "factor analysis across omics layers".
Version Compatibility
- mofapy2 >= 0.7 (Python >= 3.8), muon >= 0.1.6, anndata >= 0.10, mudata >= 0.2
- R alternative: MOFA2 Bioconductor package >= 1.12 (same model, R interface)
- HDF5 output format is shared between languages -- a model trained in Python can be loaded in R with
MOFA2::load_model() and vice versa.
Prerequisites
pip install mofapy2 muon (or BiocManager::install("MOFA2") in R)
- Each omics view pre-processed to a samples x features matrix, ideally variance-stabilized/log-normalized and roughly Gaussian (count data should be transformed first; see
bio-differential-expression-deseq2-basics or bio-single-cell-preprocessing)
- Views share the same sample IDs (partial overlap is fine)
Core Workflow
Goal: Fit a MOFA2 model on matched omics views and get per-factor, per-view variance explained.
Approach: Build a MuData object from per-view AnnData, run muon.tl.mofa, then load the trained model to extract Z (factors) and variance decomposition.
import numpy as np
import anndata as ad
import muon as mu
def build_mudata(rna_df, prot_df, meth_df):
"""Assemble a MuData object from samples x features DataFrames sharing an index.
Each df: rows=samples, cols=features, already normalized/scaled per view.
"""
mdata = mu.MuData({
"rna": ad.AnnData(rna_df.values, obs=rna_df[[]], var=rna_df.T[[]]),
"prot": ad.AnnData(prot_df.values, obs=prot_df[[]], var=prot_df.T[[]]),
"meth": ad.AnnData(meth_df.values, obs=meth_df[[]], var=meth_df.T[[]]),
})
mdata.update()
return mdata
def run_mofa(mdata, n_factors=15, outfile="mofa_model.hdf5", seed=42):
"""Train a MOFA2 model on a MuData object and save it to HDF5.
n_factors: start high (15-20) and prune inactive factors afterward.
"""
mu.tl.mofa(
mdata,
n_factors=n_factors,
outfile=outfile,
seed=seed,
convergence_mode="fast",
)
return mdata # factors written to mdata.obsm["X_mofa"], weights in mdata.varm
For direct control over the generative model (long-format data, custom priors), use mofapy2 directly:
from mofapy2.run.entry_point import entry_point
def run_mofa_low_level(data_df, n_factors=10, outfile="mofa_model.hdf5"):
"""Train MOFA2 via mofapy2 entry_point on long-format data.
data_df columns: sample, group, feature, view, value.
"""
ent = entry_point()
ent.set_data_options(scale_groups=False, scale_views=True)
ent.set_data_df(data_df)
ent.set_model_options(factors=n_factors, spikeslab_weights=True, ard_weights=True)
ent.set_train_options(iter=1000, convergence_mode="fast", seed=42)
ent.build()
ent.run()
ent.save(outfile)
return ent
Goal: Decompose variance explained (R^2) per factor per view to distinguish shared vs. view-specific factors.
Approach: Read the trained model back with mofax (or parse the HDF5 directly) and tabulate R^2.
import pandas as pd
def variance_decomposition(model_path):
"""Load a MOFA2 HDF5 model and return an R2 table (views x factors).
Requires: pip install mofax
"""
import mofax as mfx
m = mfx.mofa_model(model_path)
r2 = m.get_r2() # columns: Factor, View, R2, (Group)
table = r2.pivot_table(index="View", columns="Factor", values="R2")
m.close()
return table
def flag_shared_vs_specific(r2_table, threshold=2.0):
"""Classify factors as shared (high R2 in >=2 views) or view-specific.
threshold: minimum R2 (%) to count a view as "active" for a factor.
"""
active = r2_table >= threshold
n_active_views = active.sum(axis=0)
return pd.DataFrame({
"n_active_views": n_active_views,
"classification": np.where(n_active_views >= 2, "shared", "view-specific"),
})
Model Parameters
| Parameter |
Default |
Notes |
n_factors |
10-15 |
Start at 15-20, prune factors with R^2 < 2% in every view |
convergence_mode |
"fast" |
fast/medium/slow; use "slow" for final published results |
spikeslab_weights |
True |
Sparse feature weights via ARD prior |
scale_views |
True |
Normalize each view to unit variance so high-dimensional views don't dominate |
Factor Interpretation Workflow
- Correlate factor scores (
mdata.obsm["X_mofa"]) with sample metadata (type, clinical variables, batch).
- Rank features by
|weight| per view/factor -> candidate genes/proteins/CpGs.
- Run gene set enrichment (e.g., KS test or GSEA) on ranked RNA weights for a factor of interest -> pathway interpretation. See
bio-pathway-analysis-gsea.
MOFA2 vs PCA
| Aspect |
PCA |
MOFA2 |
| Input |
Single matrix |
Multiple matrices (views) |
| Shared vs specific |
Mixed together |
Explicit per-view decomposition |
| Missing views/samples |
Cannot handle |
Native support |
| Sparse weights |
No |
Yes (ARD/spike-slab prior) |
Pitfalls
- Factor 1 depth artifact: check whether Factor 1 correlates with library size/sequencing depth rather than biology before interpreting it.
- Batch effects: if batch confounds biology, MOFA will capture batch as its own factor -- always correlate factors against known technical covariates.
- View scaling: always set
scale_views=True; otherwise a high-dimensional view (e.g., 20k genes vs. 150 proteins) dominates the shared factors purely by feature count.
- Too few factors: under-specifying
n_factors merges distinct biological and technical signals into one factor -- start high and prune post hoc.
- Non-Gaussian input: raw counts violate the default Gaussian likelihood; log/VST-transform RNA-seq and normalize methylation M-values first.
See Also
bio-multi-omics-integration-mofa-integration
bio-multi-omics-integration-mixomics-analysis
bio-multi-omics-integration-data-harmonization
bio-pathway-analysis-gsea
1---2name: bio-applied-mofa23description: Run MOFA2 (mofapy2/muon) to fuse RNA-seq, proteomics, methylation into latent factors; decompose per-view R2, interpret weights. Use for multi-omics integration, MOFA/MOFA2 analysis, or latent factor discovery.4---56# MOFA2: Multi-Omics Factor Analysis78## When to Use910- Integrating 2+ omics layers (RNA-seq, proteomics, ATAC/methylation, metabolomics) measured on the same samples into a shared low-dimensional representation.11- Separating "shared" biological signal (appears in all views) from "view-specific" signal (e.g., epigenetic-only or proteomic-only variation).12- Handling missing views/samples (e.g., proteomics missing for a subset of patients) -- MOFA2 natively supports this, unlike PCA on a concatenated matrix.13- Finding candidate genes/proteins/CpGs driving a factor, then running gene set enrichment on the top weights.14- User says "MOFA", "MOFA2", "multi-omics factor analysis", "shared vs specific variance", or "factor analysis across omics layers".1516## Version Compatibility1718- mofapy2 >= 0.7 (Python >= 3.8), muon >= 0.1.6, anndata >= 0.10, mudata >= 0.219- R alternative: MOFA2 Bioconductor package >= 1.12 (same model, R interface)20- HDF5 output format is shared between languages -- a model trained in Python can be loaded in R with `MOFA2::load_model()` and vice versa.2122## Prerequisites2324- `pip install mofapy2 muon` (or `BiocManager::install("MOFA2")` in R)25- Each omics view pre-processed to a samples x features matrix, ideally variance-stabilized/log-normalized and roughly Gaussian (count data should be transformed first; see `bio-differential-expression-deseq2-basics` or `bio-single-cell-preprocessing`)26- Views share the same sample IDs (partial overlap is fine)2728## Core Workflow2930**Goal:** Fit a MOFA2 model on matched omics views and get per-factor, per-view variance explained.3132**Approach:** Build a `MuData` object from per-view AnnData, run `muon.tl.mofa`, then load the trained model to extract `Z` (factors) and variance decomposition.3334```python35import numpy as np36import anndata as ad37import muon as mu383940def build_mudata(rna_df, prot_df, meth_df):41 """Assemble a MuData object from samples x features DataFrames sharing an index.4243 Each df: rows=samples, cols=features, already normalized/scaled per view.44 """45 mdata = mu.MuData({46 "rna": ad.AnnData(rna_df.values, obs=rna_df[[]], var=rna_df.T[[]]),47 "prot": ad.AnnData(prot_df.values, obs=prot_df[[]], var=prot_df.T[[]]),48 "meth": ad.AnnData(meth_df.values, obs=meth_df[[]], var=meth_df.T[[]]),49 })50 mdata.update()51 return mdata525354def run_mofa(mdata, n_factors=15, outfile="mofa_model.hdf5", seed=42):55 """Train a MOFA2 model on a MuData object and save it to HDF5.5657 n_factors: start high (15-20) and prune inactive factors afterward.58 """59 mu.tl.mofa(60 mdata,61 n_factors=n_factors,62 outfile=outfile,63 seed=seed,64 convergence_mode="fast",65 )66 return mdata # factors written to mdata.obsm["X_mofa"], weights in mdata.varm67```6869For direct control over the generative model (long-format data, custom priors), use `mofapy2` directly:7071```python72from mofapy2.run.entry_point import entry_point737475def run_mofa_low_level(data_df, n_factors=10, outfile="mofa_model.hdf5"):76 """Train MOFA2 via mofapy2 entry_point on long-format data.7778 data_df columns: sample, group, feature, view, value.79 """80 ent = entry_point()81 ent.set_data_options(scale_groups=False, scale_views=True)82 ent.set_data_df(data_df)83 ent.set_model_options(factors=n_factors, spikeslab_weights=True, ard_weights=True)84 ent.set_train_options(iter=1000, convergence_mode="fast", seed=42)85 ent.build()86 ent.run()87 ent.save(outfile)88 return ent89```9091**Goal:** Decompose variance explained (R^2) per factor per view to distinguish shared vs. view-specific factors.9293**Approach:** Read the trained model back with `mofax` (or parse the HDF5 directly) and tabulate R^2.9495```python96import pandas as pd979899def variance_decomposition(model_path):100 """Load a MOFA2 HDF5 model and return an R2 table (views x factors).101102 Requires: pip install mofax103 """104 import mofax as mfx105106 m = mfx.mofa_model(model_path)107 r2 = m.get_r2() # columns: Factor, View, R2, (Group)108 table = r2.pivot_table(index="View", columns="Factor", values="R2")109 m.close()110 return table111112113def flag_shared_vs_specific(r2_table, threshold=2.0):114 """Classify factors as shared (high R2 in >=2 views) or view-specific.115116 threshold: minimum R2 (%) to count a view as "active" for a factor.117 """118 active = r2_table >= threshold119 n_active_views = active.sum(axis=0)120 return pd.DataFrame({121 "n_active_views": n_active_views,122 "classification": np.where(n_active_views >= 2, "shared", "view-specific"),123 })124```125126## Model Parameters127128| Parameter | Default | Notes |129|---|---|---|130| `n_factors` | 10-15 | Start at 15-20, prune factors with R^2 < 2% in every view |131| `convergence_mode` | "fast" | fast/medium/slow; use "slow" for final published results |132| `spikeslab_weights` | True | Sparse feature weights via ARD prior |133| `scale_views` | True | Normalize each view to unit variance so high-dimensional views don't dominate |134135## Factor Interpretation Workflow1361371. Correlate factor scores (`mdata.obsm["X_mofa"]`) with sample metadata (type, clinical variables, batch).1382. Rank features by `|weight|` per view/factor -> candidate genes/proteins/CpGs.1393. Run gene set enrichment (e.g., KS test or GSEA) on ranked RNA weights for a factor of interest -> pathway interpretation. See `bio-pathway-analysis-gsea`.140141## MOFA2 vs PCA142143| Aspect | PCA | MOFA2 |144|---|---|---|145| Input | Single matrix | Multiple matrices (views) |146| Shared vs specific | Mixed together | Explicit per-view decomposition |147| Missing views/samples | Cannot handle | Native support |148| Sparse weights | No | Yes (ARD/spike-slab prior) |149150## Pitfalls151152- **Factor 1 depth artifact**: check whether Factor 1 correlates with library size/sequencing depth rather than biology before interpreting it.153- **Batch effects**: if batch confounds biology, MOFA will capture batch as its own factor -- always correlate factors against known technical covariates.154- **View scaling**: always set `scale_views=True`; otherwise a high-dimensional view (e.g., 20k genes vs. 150 proteins) dominates the shared factors purely by feature count.155- **Too few factors**: under-specifying `n_factors` merges distinct biological and technical signals into one factor -- start high and prune post hoc.156- **Non-Gaussian input**: raw counts violate the default Gaussian likelihood; log/VST-transform RNA-seq and normalize methylation M-values first.157158## See Also159160- `bio-multi-omics-integration-mofa-integration`161- `bio-multi-omics-integration-mixomics-analysis`162- `bio-multi-omics-integration-data-harmonization`163- `bio-pathway-analysis-gsea`