Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Normalize and Select Signal

Why library size matters

One cell may have 20,000 observed UMIs and another 5,000. Directly comparing raw counts makes deeply sampled cells look globally more active. A common exploratory transformation divides each cell by its total, multiplies by a target such as 10,000, and applies log(1 + x).

Requires CLI: this example imports the package.

import "singlecell" as sc

let obj = sc.load("nsclc_like")
    |> sc.filter_genes(3)
    |> sc.filter_cells(20, 2500, 5.0)
    |> sc.normalize(10000.0)

println(sc.summary(obj))

Raw counts remain in layers.counts; normalized values are in norm_matrix and layers.lognorm.

What normalization does not do

Normalization does not:

  • recover transcripts that were not captured;
  • remove batch effects;
  • prove that a gene is differentially expressed;
  • make every cell biologically comparable;
  • replace a replicate-aware statistical model.

It creates a more useful representation for exploration and many downstream geometric methods.

Highly variable genes

Thousands of genes vary little or mostly add noise. Highly variable gene (HVG) selection retains a subset whose variation can help describe cell-to-cell structure:

Requires CLI: this example imports the package.

import "singlecell" as sc

let obj = sc.load("nsclc_like")
    |> sc.filter_genes(3)
    |> sc.filter_cells(20, 2500, 5.0)
    |> sc.normalize()
    |> sc.variable_genes(100)

println(take(obj.hvg_genes, 12))

BioLang currently ranks genes using dispersion (coefficient of variation squared) across all genes at once. Scanpy’s seurat flavor and Seurat’s VST first bin genes by mean expression and rank dispersion within each bin, which compensates for the fact that low-mean genes have higher dispersion for purely sampling reasons. BioLang’s unbinned ranking therefore leans further toward low-expression genes. Exact HVG lists are not expected to match across tools; compare downstream structure and marker behavior, not gene-list identity.

SCTransform

sc.sctransform() provides regularized negative-binomial residuals as an alternative transformation:

Requires CLI: this example imports the package.

import "singlecell" as sc

let transformed = sc.load("nsclc_like")
    |> sc.filter_genes(3)
    |> sc.filter_cells(20, 2500, 5.0)
    |> sc.sctransform()

println(sc.summary(transformed))

Note that the result is dense. Pearson residuals are nonzero where counts were zero, so unlike normalize() this does not preserve sparsity — check the cell and gene counts before running it on a large matrix.

Do not mix normalization strategies without a reason. Record which matrix each later method used.

Decision record

For this step, save:

  • the normalization method and target;
  • whether raw counts were preserved;
  • the HVG method and number selected;
  • genes deliberately excluded, such as mitochondrial or ribosomal sets;
  • plots or summaries used to justify the choice.