Goal
Take a metabolomics peak table (NMR or LC-MS; MetaboAnalyst CSV, generic wide CSV, or m/z/RT-coded LC-MS table) into an AnnData and run the canonical clean-up chain — load → impute → sample-normalize → feature-transform — plus optional MS-specific drift / batch / sample-outlier correction. The output is an analysis-ready AnnData with obs['group'] populated, raw intensities preserved as a layer, and the matrix in the right scale for the downstream skill (univariate stats vs. multivariate model).
Stay focused on preprocessing. Univariate differential / anova is a thin add-on shown for completeness; multivariate (PLS-DA, OPLS-DA), pathway enrichment (MSEA), biomarker selection, DGCA, and multi-omics MOFA are separate downstream skills.
Quick Workflow
- Pick the loader by file format and pass
group_col— every downstream test readsadata.obs['group']. - Impute missing values; method depends on the missingness mechanism (MAR →
knn; MNAR / left-censored LC-MS →qrilc). - Normalize each sample row to remove dilution effects — PQN is canonical.
- Transform each feature column for variance stabilization (
log), and Pareto-scale on top only when a multivariate model will consume the matrix. - For LC-MS with QC pools and injection order: run
drift_correct(LOESS-on-QC) →serrf(RF on QC) →ov.bulk.batch_correction(ComBat on residual batch shifts), in that order. - Run
sample_qcon the corrected real samples to flag outliers; inspect manually before removing. - Validate that
adata.obs['group']exists, no NaNs remain inadata.X, andrawmatrix is preserved in a layer.
Interface Summary
Loaders (each returns AnnData with obs=samples, var=metabolites, obs['group'] populated):
ov.metabol.read_metaboanalyst(path, *, group_col, sample_col=None, transpose=False)— MetaboAnalyst-format CSV.ov.metabol.read_wide(path, *, sample_col, group_col, ...)— generic wide CSV with explicit axes.ov.metabol.read_lcms(path, *, feature_id_sep='/', sample_col=None, group_col=None, label_row=None, transpose=True)— LC-MS peak table; parsesm/zandRTfrom feature IDs intovar['mz']/var['rt'].
Preprocessing chain (each is AnnData-in / AnnData-out and stashes the previous matrix into adata.layers when stash_raw=True):
ov.metabol.impute(adata, *, method='qrilc'|'knn'|'half_min'|'zero', missing_threshold=0.5, n_neighbors=5, q=0.01, seed=0).ov.metabol.normalize(adata, *, method='pqn'|'tic'|'median'|'mstus', reference='median'|'mean', missing_threshold=0.5).ov.metabol.transform(adata, *, method='log'|'glog'|'autoscale'|'pareto', pseudocount=1.0, stash_raw=True).
QC and MS-specific correction:
ov.metabol.cv_filter(adata, *, qc_mask=None, cv_threshold=0.30, across='qc'|'all')— drop high-CV features.ov.metabol.blank_filter(adata, *, blank_mask, ratio=3.0)— drop features not above blank intensity.ov.metabol.drift_correct(adata, *, injection_order, qc_mask, frac=0.5)— LOESS regression on QC samples vs. injection order.ov.metabol.serrf(adata, *, qc_col, qc_label='QC', batch_col=None, top_k=10, n_estimators=100, min_qc_samples=5, layer=None, seed=0)— RF residual correction (Fan 2019). Addsvar['cv_qc_raw']andvar['cv_qc_serrf'].ov.bulk.batch_correction(adata, *, batch_key, key_added='batch_correction')— ComBat; corrected matrix lands atadata.layers['batch_correction'].ov.metabol.sample_qc(adata, *, n_components=2, alpha=0.95, center=True, scale=True, layer=None) → pd.DataFramewithT2,DModX,T2_crit,DModX_crit,is_outlier.ov.metabol.sample_qc_plot(qc_df, *, ax=None, figsize=(5, 4), normal_color, outlier_color)— Hotelling T² vs. DModX scatter with critical-value lines.
Univariate stats (kept here for completeness — they consume the log-transformed matrix, not the Pareto one):
ov.metabol.differential(adata, *, group_col='group', group_a, group_b, method='welch_t'|'mannwhitney'|'paired_t', layer=None, log_transformed=True) → pd.DataFramewithlog2fc,pvalue,padj,mean_a,mean_b.ov.metabol.anova(adata, *, group_col='group', groups=None, method='welch_anova'|'anova'|'kruskal', layer=None) → pd.DataFrame.ov.metabol.volcano(deg, *, padj_thresh=0.05, log2fc_thresh=1.0, label_top_n=10, use_pvalue=False, clip_log2fc=None, ax=None, figsize=(5.5, 4.5)).
Chained class API:
ov.metabol.pyMetabo(adata, random_state=0)— keepsself.rawfrozen, exposes.cv_filter / .drift_correct / .blank_filter / .impute / .normalize / .transform / .differential / .plsda / .opls_da(all chainable, all returnself), plus.deg_table,.plsda_result,.vip_table(),.significant_metabolites(padj_thresh, log2fc_thresh).
Boundary
Keep this skill focused on the deterministic preprocessing chain plus MS-specific correction. Inside scope:
- Loading any of the three peak-table formats.
- Imputation, sample-normalization, feature-transformation in any order, with reproducible seeding.
- LC-MS drift / SERRF / ComBat / sample-QC stack.
- Demonstration that the
pyMetabochain produces identical numerics to the functional API (notebook asserts equality to machine precision).
Outside scope — separate skill:
- PLS-DA / OPLS-DA model fitting, VIP scoring, S-plot (multivariate skill).
- MSEA, mummichog, KEGG / LIPID MAPS / LION / HMDB ID mapping (pathway skill).
- Biomarker panel + nested CV (biomarker skill).
- DGCA differential correlation (correlation skill).
- ASCA / mixed model / MEBA multi-factor designs (multifactor skill).
- MOFA multi-view integration (multi-omics skill).
- LIPID MAPS lipid parsing + class aggregation (lipidomics skill).
If a future tutorial only swaps the imputation method, normalization choice, or adds a new transformation primitive, update this skill rather than creating a duplicate.
Branch Selection
Imputer (impute(method=...)) by missingness mechanism
qrilc— left-censored / below-detection (LC-MS default). Draws from a truncated quantile-regressed normal.knn— missing-at-random (sample mishandling, integration glitches). Sensitive ton_neighbors.half_min— fast baseline, fills with feature half-minimum. Use only when distributional fidelity is not critical.zero— only when zeros are structurally meaningful (e.g., truly absent metabolite); never the right call for LC-MS NaNs.
Sample normalizer (normalize(method=...))
pqn— Probabilistic Quotient Normalization. Default for both NMR and LC-MS.tic— Total Ion Count. Only safe when the matrix is conserved (all samples on the same instrument run, same injection volume).median— sample-median ratio. Robust to extreme features; less aggressive than PQN.mstus— MS Total Useful Signal (LC-MS only — drops low-coverage features before TIC).
Feature transform (transform(method=...)) — sequence matters
logthen downstream univariate stats (differential/anova). Passlog_transformed=Trueso fold-changes are computed on un-logged means.log→paretofor downstream multivariate (plsda,opls_da,asca). Pareto centres each feature and divides bysqrt(SD)— preserves dominant signal structure.autoscale(z-score) only if you specifically want unit variance per feature.glogfor very-low-intensity LC-MS features wherelogover-amplifies noise.
Correction stack (LC-MS only) — order matters
drift_correct— injection-order drift via LOESS on QC. Skip if QC samples aren't interspersed.serrf— within-batch QC variability via Random Forest. Needs ≥5 QC pools per batch.ov.bulk.batch_correction(ComBat) — between-batch additive shifts. Use only if residual batch effect remains after SERRF and you have ≥3 samples per batch.
pyMetabo vs. functional API
- Use
pyMetabofor reproducible single-seed pipelines and when downstream code wants.deg_table/.plsda_result/.significant_metabolites()accessors. - Use the functional API when you want to inspect intermediate
AnnDatabetween every step or chain in / out of non-metabol code.
Input Contract
AnnDatawithobs=samples,var=metabolites(loaders enforce this — passtranspose=Trueif your file is the other way).adata.obsmust carry the factor column you'll pass asgroup_col; the loader writes it asobs['group'].- For LC-MS drift / SERRF:
adata.obsneedssample_type(with a'QC'label) andbatch; fordrift_correctit also needs an injection-order column (numeric, monotonic). - Missing values must be NaN, not zero (loaders handle this for the supported formats; check if you built the AnnData manually).
adata.Xis dense float — sparse matrices are not supported by the imputers / SERRF.
Minimal Execution Patterns
# Functional one-shot (NMR / MetaboAnalyst CSV)
import omicverse as ov
ov.plot_set()
adata = ov.metabol.read_metaboanalyst('human_cachexia.csv', group_col='Muscle loss')
adata = ov.metabol.impute(adata, method='qrilc', q=0.01, seed=0)
adata = ov.metabol.normalize(adata, method='pqn')
log_adata = ov.metabol.transform(adata, method='log')
pareto_adata = ov.metabol.transform(log_adata, method='pareto', stash_raw=False)
deg = ov.metabol.differential(
log_adata, group_col='group', group_a='cachexic', group_b='control',
method='welch_t', log_transformed=True,
)
fig, ax = ov.metabol.volcano(deg, padj_thresh=0.10, log2fc_thresh=0.3, label_top_n=8)
# Chained class API — same numerics, more compact
m = (
ov.metabol.pyMetabo(adata.copy())
.impute(method='qrilc', seed=0)
.normalize(method='pqn')
.transform(method='log')
.differential(group_col='group', group_a='cachexic', group_b='control',
method='welch_t', log_transformed=True)
.transform(method='pareto', stash_raw=False)
)
m.deg_table.head()
m.significant_metabolites(padj_thresh=0.10, log2fc_thresh=0.3)
# LC-MS correction stack
adata = ov.metabol.drift_correct(
adata,
injection_order='order',
qc_mask=(adata.obs['sample_type'] == 'QC').to_numpy(),
frac=0.5,
)
adata = ov.metabol.serrf(
adata,
qc_col='sample_type', qc_label='QC',
batch_col='batch',
top_k=8, n_estimators=100, seed=0,
)
ov.bulk.batch_correction(adata, batch_key='batch', key_added='batch_correction')
# Corrected matrix at adata.layers['batch_correction']
real = adata[adata.obs['sample_type'] == 'real'].copy()
qc_df = ov.metabol.sample_qc(real, n_components=3, alpha=0.95)
ov.metabol.sample_qc_plot(qc_df)
flagged = qc_df.index[qc_df['is_outlier']].tolist()
Validation
'group' in adata.obs.columnsafter loading — if missing, the loader'sgroup_colwas wrong.not np.isnan(adata.X).any()afterimpute(...)— if NaNs remain, missingness exceededmissing_thresholdfor at least one column; either lower the threshold or drop those features.- After
normalize(method='pqn'), row-median dispersion (max/min) should drop versus the raw matrix — if it doesn't, picked the wrong method. - After
transform(method='pareto', ...), every feature's mean should be ≈0 — if not, Pareto wasn't applied (likely because data wasn't log-transformed first). - After
serrf,adata.var['cv_qc_serrf']should be lower thancv_qc_rawfor most features; the scattercv_qc_rawvscv_qc_serrfshould sit below the diagonal. - After
sample_qc, manually inspect samples flagged withis_outlier=Truebefore dropping — biological outliers (rare phenotypes) and technical outliers look identical here. - The
pyMetabochain and the functional API must agree to machine precision when the same seed and method choices are used (assert (m.deg_table['pvalue'].values == deg['pvalue'].values).all()). - If you only validated the chain on the cachexia NMR dataset, do not claim LC-MS correctness; both ingest paths share imputation / normalization but diverge on
read_lcmsparsing and on the drift / SERRF / ComBat stack.
Resource Map
- See
reference.mdfor copy-paste-ready snippets for each stage. - See
references/source-grounding.mdfor the live API signatures and the docstring-supplementation log (pyMetabochainable methods + plotting helpers were docstring-empty; this skill drove their backfill). - For multivariate (PLS-DA / OPLS-DA / VIP / S-plot) downstream of this preprocessing, see the multivariate skill (separate).
- For pathway enrichment (MSEA ORA / GSEA, mummichog), see the pathway-multifactor skill (separate).
- For biomarker selection with nested CV, see the biomarker skill.
- For multi-omics integration (MOFA on metabolomics + RNA-seq), see the multi-omics MOFA skill.
Examples
- "Load
human_cachexia.csv(MetaboAnalyst format, factor columnMuscle loss), impute with QRILC, PQN-normalize, log-transform, run a Welch t-test for cachexic vs. control, and plot a volcano with padj<0.10." - "My LC-MS run has injection-order drift and 3 batches with QC pools — apply drift_correct + SERRF + ComBat in that order; confirm SERRF dropped median QC CV% by at least 30 %."
- "Run sample QC on the real-sample subset and list any samples above the 0.95 critical Hotelling line."
References
- Tutorial notebooks:
t_metabol_01_intro.ipynb— full pipeline on the cachexia NMR cohort.t_metabol_06_batch_correction.ipynb— drift / SERRF / ComBat on synthetic LC-MS.
- Live API verified against
omicverse.metabolat the time of writing — seereferences/source-grounding.md.