You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add ASCA (ANOVA Simultaneous Component Analysis) and its variant VASCA (Variable-selection ASCA) to bridge the two halves of the library we already have but never connect: the designed-experiments module (experiments/) and the latent-variable models (multivariate/).
This is the single most distinctive capability common to established chemometrics toolboxes that we lack today. It is priority 1 of 4 in an exploratory-methods parity set (the others: MEDA/oMEDA diagnostics, parallel-analysis + double-CV component selection, and PLS-DA).
Motivation
A very large share of chemometrics / process-improvement studies are designed: observations are grouped by factors (grade, supplier, line, operator, day/night, dose level, time point) and often crossed (factor A x factor B). Today a user can run a factorial design or run PCA/PLS, but nothing decomposes a multivariate response matrix by its design factors and then applies component analysis to each factor's effect. ASCA does exactly this, and answers questions a plain PCA cannot:
Which experimental factor (and which interaction) is responsible for which direction of multivariate variation?
Is a given factor's effect statistically significant (via permutation testing), or just noise?
Which variables carry each factor's effect?
We already own both ingredients (per-variable ANOVA lives in experiments/analysis.py; PCA lives in multivariate/), so this is a high-leverage, defensible addition rather than a from-scratch method.
What is ASCA?
Given a response matrix X (N observations x K variables) and a balanced (or near-balanced) experimental design with one or more categorical factors:
Partition the variation by a General Linear Model. Mean-center X, then for each design term f (each main factor, each interaction) estimate an effect matrixX_f (N x K) by least squares, exactly as classical ANOVA does but applied to every column simultaneously. This yields:
X = 1 m^T + sum_f X_f + E
where m is the grand-mean row, X_f are the effect matrices (rows within a factor level are identical), and E is the residual matrix.
Run Simultaneous Component Analysis (PCA) on each effect matrix X_f separately. The scores/loadings of X_f describe the multivariate structure of that single factor's effect. Observations are then projected (scores) onto each factor's subspace.
Test significance by permutation. For each term, permute the factor labels many times (e.g. 1000), recompute the effect sum-of-squares SSQ_f, and compare the observed SSQ_f to the permutation null to get a p-value.
Common variants to support:
APCA / ASCA+: add the residual E back onto each effect matrix before the PCA, so scores show effect-plus-noise scatter (useful for visual separation). Provide as an option (add_residuals=True).
VASCA (Variable-selection ASCA): a more powerful test that ranks variables by their contribution to each term and walks down the ranking, giving FDR-controlled variable selection per factor. See Camacho et al. (2022). Implement as a mode on the same estimator.
Proposed implementation
New module process_improve/multivariate/asca.py (math) with a thin re-export from multivariate/methods.py.
An sklearn-style ASCA estimator following repo conventions (fit() returns self; fitted attributes use trailing _; __init__ sets only constructor params; NumPy-style docstrings; line length 120; ruff/black/mypy clean).
Design specification: accept either (a) a pandas DataFrame of factor columns plus a formula string (the repo already depends on patsy; see issue Move from patsy to formulaic #37 re: a future move to formulaic), or (b) explicit factor arrays. Reuse the GLM/ANOVA decomposition already in experiments/analysis.py rather than writing a new least-squares solver. Do not reimplement the ANOVA math if it already exists there - adapt it.
Reuse the existing PCA class for the per-effect Simultaneous Component Analysis step (one fitted PCA per design term).
Suggested fitted attributes:
grand_mean_
effect_matrices_ (dict: term name -> N x K effect matrix)
residuals_
ssq_ (dict: term -> sum of squares; plus the percentage of total SSQ per term)
models_ (dict: term -> fitted PCA for that effect)
scores_ / loadings_ accessors keyed by term
pvalues_ (dict: term -> permutation p-value), populated by a permutation_test(n_permutations=1000, random_state=...) method
Plotting: per-term score plot (with factor levels colored) and loading plot, reusing plots.py infrastructure. A "factor effect" bar/scree summary (percent SSQ per term) is also worth adding.
Handle the balanced case first; document the unbalanced-design caveat (Type I/II/III SS choice) and pick a sensible default (Type II is common for ASCA).
ScorePilot surfacing (downstream)
ScorePilot already stores per-column identifier roles including CLASS. Once ASCA exists upstream, ScorePilot can offer "analyze by factor": pick one or more class columns as the design, show per-factor score plots and the permutation p-values. No new math in ScorePilot - it should call this estimator (per the ScorePilot core/ boundary rule: never reimplement the multivariate math, adapt process_improve).
Acceptance criteria
ASCA fits on a small known designed dataset and reproduces the textbook effect-matrix decomposition (X = grand_mean + sum(effects) + residuals holds to numerical tolerance).
Permutation test returns stable p-values with a fixed random_state; a deliberately significant factor is flagged and a null factor is not.
Per-term PCA scores/loadings available and plottable.
VASCA mode returns a ranked variable list with FDR control.
Tests use both a synthetic design with an injected factor effect and at least one real dataset; follow the existing test conventions in tests/test_multivariate.py (scale with MCUVScaler, use real datasets alongside synthetic).
Docs: NumPy-style docstrings with an Examples section; a short narrative in the docs.
References
Smilde, Jansen, Hoefsloot, Lamers, van der Greef, Timmerman (2005). "ANOVA-simultaneous component analysis (ASCA): a new tool for analyzing designed metabolomics data." Bioinformatics 21(13):3043-3048.
Jansen et al. (2005). "ASCA: analysis of multivariate data obtained from an experimental design." J. Chemometrics 19:469-481.
Zwanenburg, Hoefsloot, Westerhuis, Jansen, Smilde (2011). "ANOVA-principal component analysis and ANOVA-simultaneous component analysis: a comparison." J. Chemometrics 25:561-567.
Start by reading experiments/analysis.py to reuse its ANOVA decomposition, and multivariate/_pca.py / plots.py for the SCA + plotting steps.
Keep __init__ parameter-only; all computed state in fit().
Bump the version (MINOR - new module/feature) and update CHANGELOG.md per CLAUDE.md; confirm with the maintainer whether a changelog entry is wanted before finalizing.
Summary
Add ASCA (ANOVA Simultaneous Component Analysis) and its variant VASCA (Variable-selection ASCA) to bridge the two halves of the library we already have but never connect: the designed-experiments module (
experiments/) and the latent-variable models (multivariate/).This is the single most distinctive capability common to established chemometrics toolboxes that we lack today. It is priority 1 of 4 in an exploratory-methods parity set (the others: MEDA/oMEDA diagnostics, parallel-analysis + double-CV component selection, and PLS-DA).
Motivation
A very large share of chemometrics / process-improvement studies are designed: observations are grouped by factors (grade, supplier, line, operator, day/night, dose level, time point) and often crossed (factor A x factor B). Today a user can run a factorial design or run PCA/PLS, but nothing decomposes a multivariate response matrix by its design factors and then applies component analysis to each factor's effect. ASCA does exactly this, and answers questions a plain PCA cannot:
We already own both ingredients (per-variable ANOVA lives in
experiments/analysis.py; PCA lives inmultivariate/), so this is a high-leverage, defensible addition rather than a from-scratch method.What is ASCA?
Given a response matrix
X(N observations x K variables) and a balanced (or near-balanced) experimental design with one or more categorical factors:Partition the variation by a General Linear Model. Mean-center
X, then for each design termf(each main factor, each interaction) estimate an effect matrixX_f(N x K) by least squares, exactly as classical ANOVA does but applied to every column simultaneously. This yields:X = 1 m^T + sum_f X_f + Ewhere
mis the grand-mean row,X_fare the effect matrices (rows within a factor level are identical), andEis the residual matrix.Run Simultaneous Component Analysis (PCA) on each effect matrix
X_fseparately. The scores/loadings ofX_fdescribe the multivariate structure of that single factor's effect. Observations are then projected (scores) onto each factor's subspace.Test significance by permutation. For each term, permute the factor labels many times (e.g. 1000), recompute the effect sum-of-squares
SSQ_f, and compare the observedSSQ_fto the permutation null to get a p-value.Common variants to support:
Eback onto each effect matrix before the PCA, so scores show effect-plus-noise scatter (useful for visual separation). Provide as an option (add_residuals=True).Proposed implementation
process_improve/multivariate/asca.py(math) with a thin re-export frommultivariate/methods.py.ASCAestimator following repo conventions (fit()returns self; fitted attributes use trailing_;__init__sets only constructor params; NumPy-style docstrings; line length 120; ruff/black/mypy clean).pandasDataFrame of factor columns plus a formula string (the repo already depends onpatsy; see issue Move frompatsytoformulaic#37 re: a future move toformulaic), or (b) explicit factor arrays. Reuse the GLM/ANOVA decomposition already inexperiments/analysis.pyrather than writing a new least-squares solver. Do not reimplement the ANOVA math if it already exists there - adapt it.PCAclass for the per-effect Simultaneous Component Analysis step (one fittedPCAper design term).grand_mean_effect_matrices_(dict: term name -> N x K effect matrix)residuals_ssq_(dict: term -> sum of squares; plus the percentage of total SSQ per term)models_(dict: term -> fittedPCAfor that effect)scores_/loadings_accessors keyed by termpvalues_(dict: term -> permutation p-value), populated by apermutation_test(n_permutations=1000, random_state=...)methodplots.pyinfrastructure. A "factor effect" bar/scree summary (percent SSQ per term) is also worth adding.ScorePilot surfacing (downstream)
ScorePilot already stores per-column identifier roles including
CLASS. Once ASCA exists upstream, ScorePilot can offer "analyze by factor": pick one or more class columns as the design, show per-factor score plots and the permutation p-values. No new math in ScorePilot - it should call this estimator (per the ScorePilotcore/boundary rule: never reimplement the multivariate math, adaptprocess_improve).Acceptance criteria
ASCAfits on a small known designed dataset and reproduces the textbook effect-matrix decomposition (X = grand_mean + sum(effects) + residualsholds to numerical tolerance).random_state; a deliberately significant factor is flagged and a null factor is not.tests/test_multivariate.py(scale withMCUVScaler, use real datasets alongside synthetic).References
Notes for the implementing session
experiments/analysis.pyto reuse its ANOVA decomposition, andmultivariate/_pca.py/plots.pyfor the SCA + plotting steps.__init__parameter-only; all computed state infit().CHANGELOG.mdperCLAUDE.md; confirm with the maintainer whether a changelog entry is wanted before finalizing.