The standardization contract

BAE expects adata.X to be z-scored per gene: mean ≈ 0, standard deviation ≈ 1.

The usual scanpy path gets you there:

import scanpy as sc

sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=2000, subset=True)
sc.pp.scale(adata)          # <- this is the one that matters

sim_scrnaseq_anndata() produces standardized X by default.

What happens if you skip it

fit checks and warns. It does not refuse:

UserWarning: Input data does not appear to be standardized (mean≈0, std≈1).
Standardization is recommended for optimal performance.

The check is a heuristic on the matrix being fitted: the median absolute column mean must be below 0.1, and the median column standard deviation within 0.1 of 1.

Ignoring the warning degrades the method in a specific way. Boosting’s selection criterion is scale-invariant in the predictors, so it will not simply pick the highest-expressed genes, but the reconstruction loss is not scale-invariant, so high-variance genes dominate the target gradient, and every reported number stops being interpretable (see Reading reconstruction quality).

The second job this contract does

The check is not only a warning. Its result decides whether a PCA warm start centers the data.

init_pca=True runs a PCA to initialize the latent code. If the input is already z-scored, centering it again would be a second transformation on top of one you already applied. So the warm start centers only when the data does not look standardized.

Columns are never rescaled by the warm start, in either case. Rescaling would silently apply a second standardization, and on data that is genuinely not standardized, it would mask the very condition the warning exists to report.

Why an MSE of 0.85 is not good

On z-scored input, predicting zero everywhere scores an MSE of exactly 1.0. That is the entire reason the contract earns its keep: it fixes the scale so that a loss can be read at all.

So an MSE of 0.85 is not “close to perfect”. It is 15% of variance explained. Reading reconstruction quality covers what to compare it against instead.

If your data lives in a layer

Pass layer=. The contract applies to whatever matrix is read:

adata.layers["scaled"] = sc.pp.scale(adata, copy=True).X
model.fit(adata, layer="scaled")

The layer is remembered, so transform, reconstruct and the diagnostics all default to it afterwards, so a fitted model always reads the representation it learned on. See What a fit writes into your AnnData.