Skip to content

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 was c, and tau meant an L1 bound in one model and an L1 penalty in another.
  • Contracts. Models broke scikit-learn's contract in ways its tooling notices: score returned an array, a constructor parameter ended in _, and n_features_in_ was a list.
  • Duplicates. Deprecated aliases (SCCA_PMD, CCA_EY, GPCCA and 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 in CCA, PLSCanonical and PCA. Parameters after n_components are keyword-only, as in scikit-learn, so CCA(2, False) must be written CCA(2, center=False). Positional calls silently broke whenever a parameter was added or reordered.
  • c → shrinkage. c read as the SVM's inverse penalty C, which runs the opposite way. shrinkage in [0, 1] is the name scikit-learn's covariance estimators and LDA use for this convex combination with the identity.
  • One name, one meaning.
  • tau was an L1 bound in PMDCCA and an L1 penalty in ADMMCCA and ParkhomenkoCCA. It is now l1_bound for the bound and alpha for the penalty.
  • ADMM's mu is rho, the name of the augmented-Lagrangian parameter in the ADMM literature (Boyd et al., 2011).
  • scikit-learn's names for the same knobs:
  • n_init for random restarts (n_starts, n_restarts)
  • support_fraction as in MinCovDet (h_frac)
  • reg as in LocallyLinearEmbedding (lle_reg)
  • n_alphas (n_lambda)
  • learning_rate (lr)
  • reg_covar for the deep models' covariance ridge (eps), as in GaussianMixture
  • n_iter_no_change
  • n_warmup and n_posterior_samples in place of NumPyro's num_*
  • No trailing underscore on parameters. CCAR3(lambda_=) and ECCA(lambda_=) are now alpha=. scikit-learn reserves a trailing _ for fitted attributes, and check_is_fitted treated these models as fitted before fit.
  • 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

  • score returns a float, the mean canonical correlation. scikit-learn's searches and cross_val_score need a scalar, and 3.x returned an array. For per-dimension correlations, use average_pairwise_correlations(pairwise_correlations(model.transform(views))) from cca_zoo.metrics.
  • Duplicated methods are removed. Models carried pairwise_correlations, average_pairwise_correlations and get_factor_loadings, copies of the functions in cca_zoo.metrics. The weights property duplicated weights_, and procrustes_rotation duplicated scipy.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.
  • transform takes 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), with None for missing views.
  • GaussianProcessCCA.transform(views, return_std=True) is now transform(views) plus posterior_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, or None)
  • ConvergenceWarning when a fit stops at max_iter
  • validated parameters
  • a ValueError when n_components exceeds a view's width. CCA silently 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, RandomizedSearchCV and the two halving searches, plus OptunaSearchCV with the optuna extra.
  • cross_val_score, cross_validate, cross_val_predict, learning_curve and validation_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, fit cca_zoo.linear.CCA or MCCA to the training encodings.
  • loss(batch) takes the whole batch. The autoencoder models now log their full loss when validating and testing; DVCCA and SplitAE previously 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=0 was fitted as mu=1, mu was ignored at shrinkage=0, and the shrinkage entered the penalty where the paper needs its square root.
  • Now: mu is the penalty on group means relative to the deviations from them. mu=1 is ridge MCCA, and mu=0 shrinks each weight towards its group's mean.
  • Also: feature_groups moves from fit to the constructor, since groups describe the features, not the samples.
  • GraphicalLassoCCA(alpha=) now penalises each view's correlations, not its covariance, so the same alpha works in any units. For features of variance s**2, the old alpha is roughly alpha / 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, as DPCCA's already was. 3.x's rho=x is 4.0's rho=1 - x. The default behaves as before.
  • StochasticCCAEY:
  • momentum is 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's SGDRegressor(learning_rate="adaptive"): the step is divided by 5 whenever the loss stalls.
  • learning_rate is now relative to each view's curvature, so the default suits views in any units.
  • GAMCCA follows mgcv.
  • n_knots and alpha become k and sp, and m is 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, tol and random_state are gone.
  • GAMCCA shipped only in 3.3.0, so this has no deprecation.
  • GaussianProcessCCA is solved in closed form, which replaces an L-BFGS-B run that stopped short of the optimum. max_iter, tol and n_iter_ are gone.
  • ProjectionPursuitCCA:
  • projection_index and mcd_support_fraction are 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.
  • WaijenborgCCA is merged into IPLSCCA. 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.
  • ProbabilisticCCA and VariationalBayesCCA posterior samples hold noise_sd_{i} and noise_corr_{i} in place of log_psi_{i}, and no z. See the next section for why.
  • The tree models now default to n_estimators=200, max_depth=3 and min_child_weight=20, alongside the fix to the boosting itself.
  • The kernel models' degree default is 3, scikit-learn's default for kernel="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.

  • ProbabilisticCCA and VariationalBayesCCA were 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, KGCCA and KTCCA now 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 any n.
  • GCCA ignored shrinkage in its weights.
  • Fits depended on a view's units through an absolute eigenvalue floor in MCCA, GCCA, TCCA, GRCCA, PartialCCA and GraphicalLassoCCA. The floor is now relative to each view's largest eigenvalue.
  • ManifoldCCA's transform of the training views now returns the training embedding exactly.
  • Training scores are centred. GaussianProcessCCA with inducing points, the tree models and ManifoldCCA returned scores with a nonzero mean. predict and inverse_transform have 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's earth; NRDCCA; LeJEPA; DPCCA; DVCCAPrivate.
  • scikit-learn integration:
  • sample_weight on the models fitted from second moments
  • Array API support (PyTorch and CuPy, on GPU) in CCA, RidgeCCA, PLS, MCCA and GCCA
  • feature names, and set_output for pandas and polars
  • skops serialisation
  • 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.