Batch Effect Correction: The Secrets Behind Those Default Values
You know Harmony's
θdefault is 2, and scVI'sn_latentdefault is 10—but do you know why? Do you know when adjusting them improves results and when it breaks everything?
This article isn't about "Harmony vs scVI vs Seurat MNN: which is better"—there are already plenty of those. Instead, it covers: what the key parameters mean mathematically when you get a new dataset, why their default values work in most cases, and which boundary conditions cause them to fail.
Building Intuition: What Batch Effect Means Mathematically
The essence of batch effects is that gene expression vectors for the same cell type, measured under different experimental conditions, exhibit systematic shifts in high-dimensional space.
Correction methods generally fall into two categories:
- Alignment-based (Harmony, Seurat MNN): Pull embeddings of cells from different batches together.
- Generative model-based (scVI, scANVI, totalVI): Explicitly model "batch as a noise variable" and learn a batch-free latent representation.
The parameter logic differs fundamentally between these two. We'll discuss them separately.
I. Deep Dive into scVI / scANVI Parameters
scVI's core is a Variational Autoencoder (VAE) with the objective function:
$$\mathcal{L} = \underbrace{\mathbb{E}{q(z|x,s)}[\log p(x|z,s)]}{\text{Reconstruction Loss}} - \underbrace{\beta \cdot D_{KL}(q(z|x,s) | p(z))}_{\text{Regularization Loss}}$$
where $x$ is the observed gene count, $s$ is the batch label, and $z$ is the latent space vector. Understanding this formula unlocks all core scVI parameters.
1.1 n_latent: Latent Space Dimensionality
Default: 10
$z \in \mathbb{R}^{n_latent}$ is the "batch-free" cell representation learned by the encoder.
Why is 10 usually sufficient?
In typical scRNA-seq datasets, biological variation in cells is primarily driven by a limited number of cell types/states. Ten dimensions are enough to distinguish 8–12 major cell types in PBMCs and their continuous states. Studies show that for typical datasets of 10k–50k cells, the first 10 PCA components explain > 70% of the variance—the choice of n_latent=10 aligns with this empirical rule.
When should you increase it?
| Scenario | Suggestion |
|---|---|
| Dataset > 100k cells, many cell types (e.g., whole-organ atlas) | Increase to 20–30 |
| Contains rare cell subpopulations (< 0.5%) requiring fine discrimination | Increase to 15–20 |
| Developmental trajectory data (continuous cell states) | Increase to 15–20 |
| Only doing batch correction, no downstream subtyping | Keep at 10, no need to increase |
When does it break?
n_latenttoo large (e.g., 50+): Too many model parameters, prone to overfitting on small datasets (< 5k cells)—training loss looks good, but UMAP still shows separated batch points (the model "hides" batch information in extra dimensions).n_latenttoo small (e.g., 5): Information bottleneck is too tight, representations of different cell types start overlapping, reducing downstream clustering accuracy.
Quick diagnostic: After training, run model.get_latent_representation(), then perform PCA and look at the explained variance curve for the first n_latent components. If the curve flattens quickly (e.g., near zero after the 6th component), your n_latent can be smaller—the model is wasting capacity.
1.2 n_hidden: Number of Hidden Layer Neurons
Default: 128
The number of neurons in each hidden layer of the encoder/decoder. This controls the capacity of the non-linear mapping.
Why 128 is sufficient: For typical inputs of 2000–5000 highly variable genes, a hidden layer with 128 neurons can capture sufficient non-linear features. Increasing it usually doesn't significantly improve performance on most datasets, only increases training time.
Exception: When the input gene count is very large (e.g., whole-genome transcriptome, 10k+ genes), consider increasing n_hidden=256; otherwise, the first layer's compression step may be too aggressive, losing details.
1.3 dispersion: Dispersion Parameter for Negative Binomial Distribution
Options: "gene" | "gene-batch" | "gene-label" | "gene-cell"
Default: "gene"
scVI models gene counts using a Negative Binomial (NB) or Zero-Inflated Negative Binomial (ZINB) distribution:
$$p(x_{ng} | \mu_{ng}, \theta_g) = \text{NB}(\mu_{ng}, \theta_g)$$
$\theta_g$ (sometimes denoted as dispersion) controls the "spread" of the count distribution for each gene: smaller $\theta_g$ means a more dispersed distribution (heavier tails); as $\theta_g \to \infty$, it approaches a Poisson distribution.
| Option | Meaning | Use Case |
|---|---|---|
"gene" |
One $\theta_g$ per gene, shared across all cells/batches | Most scenarios, default preferred |
"gene-batch" |
One $\theta$ per (gene × batch), allowing different dispersion across batches | When sequencing depth varies drastically between batches (e.g., 10x vs Smart-seq2 mix) |
"gene-label" |
One $\theta$ per (gene × cell type) | When strong prior knowledge indicates different cell types have vastly different dispersion for the same gene |
"gene-cell" |
One $\theta$ per (gene × cell) | Most flexible, but most parameters, requires large datasets (> 50k cells) |
Why the default "gene" can break:
When mixing single-end sequencing (10x) and full-length sequencing (Smart-seq2), the count distributions for the same gene differ dramatically—10x produces sparse counts, while Smart-seq2 yields more continuous high counts. Using the same $\theta_g$ to model both batches forces one parameter to fit two completely different distributions. Switching to "gene-batch" can significantly improve ELBO for such datasets.
1.4 gene_likelihood: Probability Distribution for Gene Counts
Options: "zinb" | "nb" | "poisson"
Default: "zinb" (Zero-Inflated Negative Binomial)
ZINB adds a Bernoulli gate on top of NB to model technical zeros:
$$p(x_{ng} = 0) = \pi_{ng} + (1 - \pi_{ng}) \cdot \text{NB}(0 | \mu_{ng}, \theta_g)$$
When to use "nb" instead of "zinb":
- Newer datasets with higher sequencing depth (> 3000 UMI/cell), fewer zeros
- Data has been quality-controlled, removing low-quality cells and doublets
- UMAP shows strange splitting of cell clusters when using
"zinb"—this is often the zero-inflation parameter overfitting noise
1.5 β (kl_weight): Weight of KL Divergence
Default: 1.0
This is one of the most overlooked yet impactful parameters.
Revisiting the loss function:
$$\mathcal{L} = \text{Reconstruction Loss} - \beta \cdot D_{KL}(q | p)$$
- Role of KL term: Pulls the posterior $q(z|x,s)$ towards the standard normal prior $p(z) = \mathcal{N}(0, I)$, forcing the latent space to be smooth, continuous, and interpolatable. This constraint is what "mixes" cells from different batches in the latent space.
- Role of Reconstruction term: Ensures the decoder can reconstruct the original counts from $z$, preserving biological information.
They are antagonistic:
| β Value | Effect |
|---|---|
| β = 0 | Pure autoencoder, no regularization, batch effects fully retained in z |
| β < 1 | Light regularization, good reconstruction, weak batch correction, suitable for data with small batch effects |
| β = 1 (default) | Standard VAE, balanced point, suitable for most scenarios |
| β > 1 (β-VAE) | Strong regularization, good batch mixing, but biological details may be compressed and lost |
| β >> 10 | Posterior collapse, all cell z converge, clustering becomes meaningless |
Practical tuning suggestions:
- Start with default
kl_weight=1.0, run and check scVI's UMAP - If batch separation is still evident: try
kl_weight=1.5–2.0 - If boundaries between cell types are blurred: reduce to
kl_weight=0.5–0.8 - scVI has a
kl_weightwarm-up mechanism (linearly increases from 0 to the target value over the first few epochs). This warm-up is important; do not disable it.
Note: In scVI's Python API, kl_weight is passed via the train() method using kl_weight or plan_kwargs={"kl_weight": ...}, not during model initialization. The API location varies slightly by version; check the documentation.
1.6 alpha (scANVI-specific): Supervised Signal Strength
scANVI adds cell type label supervision on top of scVI:
$$\mathcal{L}{scANVI} = \mathcal{L}{scVI} + \alpha \cdot \mathcal{L}_{classifier}$$
alpha controls the weight of the classifier loss. Default alpha=1.
- Increasing
alpha: Stronger classifier, more regularized representations for labeled cells, but unlabeled cells rely more on classifier predictions (risk of overfitting). - Decreasing
alpha: More reliance on the unsupervised part, suitable when the proportion of labeled cells is < 10%.
II. Deep Dive into Harmony2 Parameters
Harmony's core idea is iterative soft clustering + centroid movement:
- In PCA space, perform soft K-means clustering on all cells, obtaining membership probabilities for each cell to each cluster.
- Calculate the centroid deviation of cells from each batch within each cluster.
- Shift each cell's embedding in the opposite direction of its batch centroid (removing batch bias).
- Repeat until convergence.
Harmony2 (latest version) introduces soft cosine correction, allowing the correction direction to vary continuously in space, rather than a hard translation per cluster.
2.1 theta (θ): Batch Diversity Penalty
Default: 2 (can be a vector, one value per batch covariate)
$\theta$ controls the penalty strength for batch inhomogeneity when Harmony computes soft cluster centroids:
$$\text{Within-cluster penalty} \propto \theta \cdot \sum_b \frac{n_b}{n} \log \frac{n_b / n}{R_{kb}}$$
where $R_{kb}$ is the membership of cells from batch $b$ to cluster $k$, and $n_b/n$ is the proportion of cells from batch $b$.
Intuition: Larger $\theta$ means Harmony tries harder to make the proportion of cells from each batch within each cluster close to the global proportion, i.e., forces mixing.
| θ Value | Effect |
|---|---|
| θ = 0 | No penalty, equivalent to no batch correction |
| θ = 2 (default) | Moderate correction, suitable for data with moderate batch effects |
| θ = 4–6 | Strong correction, good batch mixing, but may force different cell types together |
| θ > 8 | Risk of over-correction—biological differences start being removed as batch effects |
When to increase θ:
- Very strong batch differences (different sequencing platforms)
- Dataset has verified same cell types, but UMAP shows them as two separate clusters
When to decrease θ:
- Your "batch" is actually a real biological difference (e.g., disease vs normal, different time points)
- After correction, some cell types that should be separate are merged
θ vector for multiple covariates:
harmony_kwargs = {
"theta": [2, 1], # Strong correction for first covariate (sequencing batch), weaker for second (donor)
}
2.2 sigma (σ): Soft Clustering Bandwidth
Default: 0.1
Controls the "fuzziness" of the soft K-means clustering. Smaller $\sigma$ means each cell is more deterministically assigned to one cluster (tending towards hard clustering); larger $\sigma$ means membership distribution is more uniform.
Why 0.1 is usually sufficient: This value ensures that the membership of a cell to the nearest cluster in PCA space is much higher than to others, without becoming a hard assignment.
When it breaks: Data distribution is very uneven (large differences in cell type proportions). With small $\sigma$, rare cell types might be assigned to neighboring large clusters, leading to their "assimilation" after correction. In this case, increase $\sigma$ (e.g., 0.2) to give rare groups more "buffer."
2.3 nclust: Number of Soft Clusters
Default: min(100, N_cells / 30) (approximately)
The number of clusters for Harmony's internal soft K-means. This parameter is often overlooked but has a significant impact.
nclusttoo small: Each "super-cluster" mixes too many different cell types, averaging out real heterogeneity during correction.nclusttoo large: Exceeds the actual number of biological groups, converges slowly, and centroid estimation becomes unstable when each cluster has too few cells.
Suggestion: At least 5–10 times the number of expected cell types. For a 50k cell dataset, nclust=100–200 is usually reasonable.
2.4 block_size: Proportion of Cells Used per Update
Default: 0.05 (i.e., 5% of cells per iteration)
Harmony does not use all cells to update parameters in each iteration; instead, it samples a block. This is a stochastic gradient approach that:
- Reduces memory usage
- Introduces randomness into the convergence process, avoiding local minima
When to adjust:
- Very small datasets (< 5k cells):
block_size=0.05uses only 250 cells per iteration, too much randomness, unstable convergence. Increase to0.2–0.5. - Very large datasets (> 500k cells): Default 5% is still a lot (25k cells per iteration), can decrease to
0.01–0.02to speed up each step.
2.5 max_iter_harmony and eps_harmony: Convergence Control
Default: max_iter_harmony=10, eps_harmony=1e-4
Harmony stops when the change in the objective function is less than eps_harmony or the maximum number of iterations is reached.
10 iterations are usually enough, but sometimes not: When batch effects are complex or there are many covariates, observe Harmony's objective function curve (harmony_object.objective_harmony). If it hasn't converged by the 10th iteration (curve still decreasing), increase max_iter_harmony=20.
III. totalVI: Additional Parameters for Multi-modal Data
totalVI processes both RNA and protein (CITE-seq / ASAP-seq) data, adding to scVI:
3.1 Protein Background Prior
CITE-seq protein signals contain non-specific background noise, which totalVI models with a mixture model:
$$p(\text{protein}) = \pi \cdot \mathcal{N}(\mu_{bg}, \sigma_{bg}) + (1-\pi) \cdot \mathcal{N}(\mu_{fg}, \sigma_{fg})$$
protein_background_prior_mean(default-10, in log-normal space): Prior mean of the background signal.protein_background_prior_scale(default0.5): Uncertainty of the background prior.
When these parameters break:
If your protein panel contains many proteins not expressed on target cells (abundant negative controls), the background mean will be systematically high. In this case, protein_background_prior_mean should be increased (e.g., to -5); otherwise, the model will incorrectly classify truly low-expressed proteins as background.
3.2 protein_dispersion
Similar to scVI's dispersion, controls the dispersion modeling for protein counts. Usually keep the default ("protein", one parameter per protein), but when you have multiple measurement batches, "protein-batch" is more appropriate.
IV. General Parameter Tuning Workflow
Regardless of the tool, follow this order:
1. Run with default parameters first
→ Check UMAP: Batch separation? Clear cell type boundaries?
→ Batch separation present → Consider strengthening correction parameters (θ↑ or kl_weight↑)
→ Blurred cell type boundaries → Consider reducing correction strength
2. Tune one parameter, fix others
→ Compare LISI scores (iLISI measures batch mixing, cLISI measures cell type preservation)
→ Look at both scores simultaneously; don't just pursue high iLISI
3. Check biological plausibility
→ Are marker genes highly expressed in the correct clusters?
→ Have rare cell types been "assimilated" and disappeared?
Quick Reference Table
| Problem | Possible Cause | Tuning Direction |
|---|---|---|
| Batches still separate in UMAP | Insufficient correction strength | Harmony: θ↑; scVI: kl_weight↑ |
| Different cell types merged | Over-correction | Harmony: θ↓; scVI: kl_weight↓ |
| Rare cell types disappear | nclust too small / σ too small | Harmony: nclust↑, sigma↑ |
| Training doesn't converge | Learning rate / warm-up issue | Check kl_weight curve, add warm-up |
| OOM on large datasets | block_size too large | Harmony: block_size↓; scVI: batch_size↓ |
| ZINB produces strange small clusters | Zero-inflation parameter overfitting | Switch to gene_likelihood="nb" |
| scANVI label transfer poor | alpha too small | alpha↑, or increase proportion of labeled cells |
Summary
Choosing batch effect correction parameters is fundamentally about finding a balance between two opposing goals:
- Batch mixing (integration) vs Biological preservation
- Model capacity vs Generalization
Default values are reasonable starting points validated on many datasets, but they are not universal. Understanding what each parameter controls mathematically allows you to know where to go when defaults fail—rather than blindly grid searching.
Related tool documentation:
- scVI-tools: docs.scvi-tools.org
- Harmony (harmonypy): github.com/immunogenomics/harmony
- totalVI: scvi-tools.org/en/latest/api/reference/scvi.model.TOTALVI.html