CCA-Zoo 4.0¶
CCA-Zoo 4.0 is a breaking release. This page explains what changed and why, section by section, and ends with a table giving the replacement for every removed or renamed name. The changelog lists every change individually.
Why a major version¶
Across 3.x, CCA-Zoo grew to more than forty models. Each one kept the conventions of whoever wrote it, which meant three problems:
- Names. The same concept had several names. The number of latent dimensions was
latent_dimensions, the ridge wasc, andtaumeant an L1 bound in one model and an L1 penalty in another. - Contracts. Models broke scikit-learn's contract in ways its tooling notices:
scorereturned an array, a constructor parameter ended in_, andn_features_in_was a list. - Duplicates. Deprecated aliases (
SCCA_PMD,CCA_EY,GPCCAand a dozen more) sat alongside their replacements, so every concept had two or three spellings in tutorials, issues and user code.
4.0 fixes all three at once. Every model now follows one contract: fit, transform,
predict, inverse_transform, score and feature_importances_per_view_. Wherever
scikit-learn has a name for a concept, CCA-Zoo uses it. Every deprecated alias is gone.
Why there is no deprecation period. The aliases being removed were already deprecated through 3.x, and adding another round of warnings for the new renames would double the API surface for another major cycle. Instead, every removal has a one-to-one replacement in the migration table, and most migrations are a find-and-replace.
What stays the same. The fitted models are unchanged except where 3.x was wrong
(see Results that change). Inputs are still a list of views,
each an (n_samples, n_features_i) array, and outputs are still one array of scores
per view.
Why n_components¶
This is the rename every user will meet, so here is the reasoning in full.
It is the name the rest of the ecosystem uses. scikit-learn calls the number of
dimensions a model extracts n_components in CCA, PLSCanonical, PLSRegression,
PCA, KernelPCA, FactorAnalysis, FastICA, NMF, TruncatedSVD,
SpectralEmbedding and LocallyLinearEmbedding. With the same name,
cca_zoo.linear.CCA(n_components=2) and sklearn.cross_decomposition.CCA(n_components=2)
are called identically, and code, tutorials and habits carry over between them.
Tools key on it. Search grids, Pipeline parameter paths
(cca__n_components), get_feature_names_out (which names the outputs cca0,
cca1, ...) and the fitted n_components_ attribute all use scikit-learn's naming. A
different name meant writing latent_dimensions in some places and n_components in
others within the same pipeline.
latent_dimensions was ambiguous. In the probabilistic and deep models, the latent
variable itself has dimensions, and in GFA the parameter is only an upper bound on
them. "Dimensions" was also easy to confuse with the number of features. n_components
names what you choose for every model: the number of columns transform returns per
view. n_components_ then says how many were actually fitted, which is fewer when
GFA prunes dimensions or the data are too narrow.
Migrating is mechanical. It is still the first parameter, so CCA(2) works
unchanged. Keyword calls need latent_dimensions= replaced by n_components=, and search
grids need "latent_dimensions" replaced by "n_components".
Requirements¶
4.0 requires Python 3.11+, numpy 2.0, scipy 1.13 and scikit-learn 1.8.
3.x declared numpy 1.26 and scikit-learn 1.6 as minimums, but CI never tested those
versions, and most models failed on them. These are now the oldest versions the test
suite passes on, and a CI job runs the suite against them, so the minimums in
pyproject.toml are true.
Python 3.10 is dropped for three reasons:
- It reaches end of life in October 2026.
- scikit-learn 1.8 no longer supports it, so 3.10 users would be stuck on an older scikit-learn than the one CCA-Zoo is tested against.
Names follow scikit-learn¶
A model that uses scikit-learn's names works with scikit-learn's tools without
adapters: clone, get_params, GridSearchCV, Pipeline and check_is_fitted. It
also reads the same to anyone who already knows scikit-learn.
latent_dimensions→n_components, the name scikit-learn uses inCCA,PLSCanonicalandPCA. Parameters aftern_componentsare keyword-only, as in scikit-learn, soCCA(2, False)must be writtenCCA(2, center=False). Positional calls silently broke whenever a parameter was added or reordered.c→shrinkage.cread as the SVM's inverse penaltyC, which runs the opposite way.shrinkagein[0, 1]is the name scikit-learn's covariance estimators and LDA use for this convex combination with the identity.- One name, one meaning.
tauwas an L1 bound inPMDCCAand an L1 penalty inADMMCCAandParkhomenkoCCA. It is nowl1_boundfor the bound andalphafor the penalty.- ADMM's
muisrho, the name of the augmented-Lagrangian parameter in the ADMM literature (Boyd et al., 2011). - scikit-learn's names for the same knobs:
n_initfor random restarts (n_starts,n_restarts)support_fractionas inMinCovDet(h_frac)regas inLocallyLinearEmbedding(lle_reg)n_alphas(n_lambda)learning_rate(lr)reg_covarfor the deep models' covariance ridge (eps), as inGaussianMixturen_iter_no_changen_warmupandn_posterior_samplesin place of NumPyro'snum_*- No trailing underscore on parameters.
CCAR3(lambda_=)andECCA(lambda_=)are nowalpha=. scikit-learn reserves a trailing_for fitted attributes, andcheck_is_fittedtreated these models as fitted beforefit. - Per-view attributes are named for it.
n_features_in_(a list) →n_features_per_view_feature_importances_→feature_importances_per_view_
scikit-learn defines both originals as a single integer or array, and tools such as
SelectFromModel read them that way. Keeping the names with a different shape broke
those tools silently.
- Class and function names:
- rCCA → RidgeCCA, matching every other class's CapWords.
- JointData(...).sample() → make_joint_data(...), a scikit-learn-style make_*
generator.
- The loaders return a Bunch, as scikit-learn's do; return_views=True gives the
list of views.
- random_state defaults to None everywhere, as in scikit-learn. The
probabilistic models, GaussianProcessCCA and the tree models defaulted to 0, so their fits looked reproducible while every other model's varied
between runs. Pass random_state=0 to get the old behaviour.
- The eps parameter is removed from the linear and kernel models. It was a
numerical floor, never a modelling choice, and it is now fixed internally. In the
deep models, where it is a ridge you tune, it is reg_covar. It is also relative to each view's scale
(see Results that change).
One contract for every model¶
scorereturns a float, the mean canonical correlation. scikit-learn's searches andcross_val_scoreneed a scalar, and 3.x returned an array. For per-dimension correlations, useaverage_pairwise_correlations(pairwise_correlations(model.transform(views)))fromcca_zoo.metrics.- Duplicated methods are removed. Models carried
pairwise_correlations,average_pairwise_correlationsandget_factor_loadings, copies of the functions incca_zoo.metrics. Theweightsproperty duplicatedweights_, andprocrustes_rotationduplicatedscipy.linalg.orthogonal_procrustes. Each existed in two places, which could drift apart. The functions now take the model's output, so they work the same on every model, including ones that have no weights. transformtakes views and returns one array per view, on every model.- The probabilistic models returned a single-element list holding the joint posterior
mean. They now return each view's posterior mean given that view. The joint mean
given any subset of views is
posterior_mean(views), withNonefor missing views. GaussianProcessCCA.transform(views, return_std=True)is nowtransform(views)plusposterior_std(views).ManifoldCCA.weights_→embedding_, the name scikit-learn's manifold learners use. Everywhere else,weights_is a weight matrix.- Also on every model:
n_components_n_iter_as an int (it was a list, orNone)ConvergenceWarningwhen a fit stops atmax_iter- validated parameters
- a
ValueErrorwhenn_componentsexceeds a view's width.CCAsilently returned fewer components; other models returned degenerate ones or failed inside numpy. - Fitted models no longer store their training data. A pickled model no longer
carries the dataset. The exceptions are the kernel and manifold models, whose
out-of-sample map is built from the training rows; they keep them as
views_fit_.
Model selection without a wrapper¶
3.x's MultiviewWrapper stacked the views into one array so that scikit-learn's
searches could slice them. 4.0 provides the searches directly:
GridSearchCV,RandomizedSearchCVand the two halving searches, plusOptunaSearchCVwith theoptunaextra.cross_val_score,cross_validate,cross_val_predict,learning_curveandvalidation_curve.
All of them take the list of views, and per-view parameters are named like
shrinkage__1. The search classes subclass their scikit-learn namesakes rather than
copying them, so they take exactly scikit-learn's parameters and follow its changes;
isinstance(search, sklearn.model_selection.GridSearchCV) holds. MultiviewWrapper is
now private.
Deep models are Lightning-native¶
The deep models had their own transform and score next to Lightning's. The old
score fitted a CCA to the very data it scored, so it was optimistic.
4.0 makes Lightning the only interface:
trainer.predict(model, loader)returns each view's encoding. For canonical variates, fitcca_zoo.linear.CCAorMCCAto the training encodings.loss(batch)takes the whole batch. The autoencoder models now log their full loss when validating and testing;DVCCAandSplitAEpreviously logged 0.- Hyperparameters are saved to checkpoints.
DCCA is the two-view method, as CCA is. DCCA(objective=MCCALoss()) duplicated
DMCCA, and the same held for DGCCA and DTCCA, so the objective argument is gone.
DVCCA takes one encoder: the published model encodes only the first view.
Changes to what a parameter means¶
These changes affect your results even if your code still runs, so check any model you use from this list:
GRCCA(mu=).- What was wrong: the default
mu=0was fitted asmu=1,muwas ignored atshrinkage=0, and the shrinkage entered the penalty where the paper needs its square root. - Now:
muis the penalty on group means relative to the deviations from them.mu=1is ridge MCCA, andmu=0shrinks each weight towards its group's mean. - Also:
feature_groupsmoves fromfitto the constructor, since groups describe the features, not the samples. GraphicalLassoCCA(alpha=)now penalises each view's correlations, not its covariance, so the samealphaworks in any units. For features of variances**2, the oldalphais roughlyalpha / s**2.- The graphical lasso is now solved by ADMM (Boyd et al., 2011). scikit-learn's solver raised on strongly correlated features, or stopped away from the optimum, at scattered penalties.
mode=is gone.DCCANOI(rho=)is now Wang et al.'s time constant, the weight of the previous running moments, asDPCCA's already was. 3.x'srho=xis 4.0'srho=1 - x. The default behaves as before.StochasticCCAEY:momentumis gone. Momentum at a fixed step hovers at the noise of its mini-batches instead of converging. The model now uses the adaptive step of scikit-learn'sSGDRegressor(learning_rate="adaptive"): the step is divided by 5 whenever the loss stalls.learning_rateis now relative to each view's curvature, so the default suits views in any units.GAMCCAfollowsmgcv.n_knotsandalphabecomekandsp, andmis new.- Each smooth is
s(x, bs="ps", k, m). 3.x cited P-splines but applied a ridge towards zero rather than their difference penalty. - The fit is now one closed-form solve, so
max_iter,tolandrandom_stateare gone. GAMCCAshipped only in 3.3.0, so this has no deprecation.GaussianProcessCCAis solved in closed form, which replaces an L-BFGS-B run that stopped short of the optimum.max_iter,tolandn_iter_are gone.ProjectionPursuitCCA:projection_indexandmcd_support_fractionare gone. The MCD index fits a robust covariance at every evaluation, so a default fit took hours.- The index is Spearman's correlation, which was already the default.
SAR(random_state=)is gone. SAR starts from the ridge-CCA direction, which needs no seed. From 3.x's random start it could return all-zero weights.WaijenborgCCAis merged intoIPLSCCA. The two ran the same regressions and gave weights with the same directions and sparsity.IPLSCCA(l1_ratio=0.5)is Waaijenborg et al.'s elastic net.ProbabilisticCCAandVariationalBayesCCAposterior samples holdnoise_sd_{i}andnoise_corr_{i}in place oflog_psi_{i}, and noz. See the next section for why.- The tree models now default to
n_estimators=200,max_depth=3andmin_child_weight=20, alongside the fix to the boosting itself. - The kernel models'
degreedefault is 3, scikit-learn's default forkernel="poly"; it was 1.
Results that change¶
These fits change without any change to your code, because 3.x computed the wrong thing. Each fix is covered by a test that compares against an answer from outside CCA-Zoo.
ProbabilisticCCAandVariationalBayesCCAwere factor analysis, not CCA.- What was wrong: each view's noise was diagonal, so the latent variable had to explain each view's own correlations as well as the shared ones.
- Effect: on views dominated by a private factor, the latent tracked that factor (correlation 0.998) and missed the shared one (0.041).
- Now: noise is a full covariance, as in Bach and Jordan (2005), and the models find the shared factor (0.97).
- Kernel models solved an inconsistent eigenproblem. The kernel was never centred.
KCCA,KGCCAandKTCCAnow match Nyström features plus the linear model to machine precision. - Deflation models returned the wrong later components.
- What was wrong: PMD, ADMM, IPLS, Span, Parkhomenko and ProjectionPursuitCCA applied weights fitted on the deflated views to the original views.
- Now: the weights are
W (P'W)^-1, as in scikit-learn's PLS. - Sparse penalties depended on the sample size. Stacking a dataset on itself leaves
every covariance unchanged, but it took
IPLSCCA(alpha=0.05)from four nonzero weights per view to none. The penalties now mean the same at anyn. GCCAignoredshrinkagein its weights.- Fits depended on a view's units through an absolute eigenvalue floor in
MCCA,GCCA,TCCA,GRCCA,PartialCCAandGraphicalLassoCCA. The floor is now relative to each view's largest eigenvalue. ManifoldCCA'stransformof the training views now returns the training embedding exactly.- Training scores are centred.
GaussianProcessCCAwith inducing points, the tree models andManifoldCCAreturned scores with a nonzero mean.predictandinverse_transformhave no intercept, so this turned into error. - Unchanged: the plain linear models (
CCA,RidgeCCA,PLS,MCCA) give the same fits as in 3.x, apart from the unit fix above.
Migration table¶
| 3.x | 4.0 |
|---|---|
latent_dimensions= |
n_components= |
positional arguments after n_components |
keywords |
c=, c__0 |
shrinkage=, shrinkage__0 |
eps= |
removed |
rCCA |
RidgeCCA |
JointData(...).sample() |
make_joint_data(...) |
X1, X2 = load_linnerud() |
load_linnerud(return_views=True) |
PMDCCA(tau=) |
l1_bound= |
ADMMCCA(tau=, mu=), ParkhomenkoCCA(tau=) |
alpha=, rho= |
CCAR3(lambda_=), ECCA(lambda_=) |
alpha= |
CCAR3(highdim=False) |
alpha=0 |
TrimmedCCA(n_starts=, h_frac=), ProjectionPursuitCCA(n_restarts=) |
n_init=, support_fraction= |
SAR(n_lambda=, random_state=) |
n_alphas=; no seed |
ManifoldCCA(lle_reg=), .weights_ |
reg=, .embedding_ |
GRCCA().fit(views, feature_groups=g) |
GRCCA(feature_groups=g).fit(views) |
GraphicalLassoCCA(mode=) |
removed |
ProjectionPursuitCCA(projection_index=, mcd_support_fraction=) |
removed |
StochasticCCAEY(momentum=) |
removed |
GAMCCA(n_knots=, alpha=) |
GAMCCA(k=, sp=) |
GaussianProcessCCA(max_iter=, tol=), transform(views, return_std=True) |
removed; posterior_std(views) |
ProbabilisticCCA(num_warmup=, num_samples=), .mcmc_ |
n_warmup=, n_posterior_samples=, .posterior_samples_ |
VariationalBayesCCA(num_steps=, num_posterior_samples=), .guide_, .svi_result_ |
n_iter=, n_posterior_samples=, .posterior_samples_, .losses_ |
GFA(num_posterior_samples=) |
n_posterior_samples= |
posterior_samples_["log_psi_{i}"], ["z"] |
["noise_sd_{i}"], ["noise_corr_{i}"]; posterior_mean(views) |
probabilistic transform(views)[0] |
posterior_mean(views) |
GFA.view_relevance_, VariationalBayesCCA.ard_relevance_ |
ard_precision_ |
train_views_ (kernel, manifold) |
views_fit_ |
n_features_in_ (list) |
n_features_per_view_ |
feature_importances_ |
feature_importances_per_view_ |
n_iter_ as a list or None |
an int |
random_state=0 default |
None; pass 0 for the old behaviour |
model.score(views) → array |
float; per dimension via cca_zoo.metrics |
model.weights |
model.weights_ |
model.pairwise_correlations(views) |
metrics.pairwise_correlations(model.transform(views)) |
model.average_pairwise_correlations(views) |
metrics.average_pairwise_correlations(...) |
model.get_factor_loadings(views) |
metrics.factor_loadings(views, model.transform(views)) |
model_selection.procrustes_rotation(ref, tgt) |
scipy.linalg.orthogonal_procrustes(tgt, ref)[0] |
MultiviewWrapper |
cca_zoo.model_selection searches; PerViewTransformer in a Pipeline |
PermutationTestResult.p_values_ etc. |
same names without _ |
DCCANOI(rho=x) |
DCCANOI(rho=1 - x) |
DCCA(objective=MCCALoss()) etc. |
DMCCA, DGCCA, DTCCA |
DVCCA(encoders=[e1, e2]) |
DVCCA(encoder=e1) |
deep lr=, max_epochs= |
learning_rate=; the Trainer's max_epochs |
deep models' and losses' eps= |
reg_covar= |
deep model.transform(loader), model.score(loader) |
trainer.predict(model, loader); cca_zoo.metrics |
custom loss(representations, independent_representations) |
loss(batch) |
SCCA_PMD, SCCAPMD |
cca_zoo.sparse.PMDCCA |
SCCA_ADMM, SCCAADMM |
cca_zoo.sparse.ADMMCCA |
SCCA_IPLS, SCCAIPLS |
cca_zoo.sparse.IPLSCCA |
SCCA_Span, SCCASpan |
cca_zoo.sparse.SpanCCA |
ElasticCCA, WaijenborgCCA |
cca_zoo.sparse.IPLSCCA(l1_ratio=0.5) |
cca_zoo.linear.ParkhomenkoCCA, .SAR, .StochasticCCAEY |
cca_zoo.sparse.*, cca_zoo.stochastic.StochasticCCAEY |
CCA_EY, MCCAEY, MCCA_EY, PLS_EY |
CCAEY, PLSEY |
GPCCA |
cca_zoo.gp.GaussianProcessCCA |
DCCA_EY, DCCA_NOI, DCCA_SDL |
DCCAEY, DCCANOI, DCCASDL |
New in 4.0¶
These are covered in detail in the changelog:
- New models:
MARSCCA, which follows R'searth;NRDCCA;LeJEPA;DPCCA;DVCCAPrivate. - scikit-learn integration:
sample_weighton the models fitted from second moments- Array API support (PyTorch and CuPy, on GPU) in
CCA,RidgeCCA,PLS,MCCAandGCCA - feature names, and
set_outputfor pandas and polars skopsserialisation- Speed: KTCCA at n=1000 goes from 49 s to under 2 s, TCCA is about 19× faster, and GCCA is 17× faster at n=4000.