Dimensional reduction¶
Linear first, then the embeddings you look at. Every one of these writes a
DimReduc into obj.reductions under a
key, and records the features it actually used — not the features it was asked
for, which are not always the same set.
jack_straw is the permutation test for how many PCs to keep. It is worth
reading its docstring before trusting the number: R's JackRandom seeds each
replicate from its loop index and is therefore deterministic, while this one
seeds from its seed argument and moves. Across 60 seeds on PBMC 3k it keeps
12–15 PCs, mode 13, which is R's answer. That spread is asserted as a band, not
described in prose — see Fidelity.
Linear¶
run_pca
¶
run_pca(seurat, n_pcs: int = 50, features: Optional[list[str]] = None, assay: Optional[str] = None, reduction_name: str = 'pca', reduction_key: str = 'PC_', seed: int = 42, layer: str = 'scale.data') -> None
Compute PCA on scaled data.
Mirrors R's RunPCA(pbmc, features = VariableFeatures(object = pbmc)). Stores a DimReduc in seurat.reductions[reduction_name].
Parameters:
-
n_pcs(int, default:50) –number of principal components
-
features(Optional[list[str]], default:None) –genes to use (defaults to variable features)
-
assay(Optional[str], default:None) –assay name (defaults to active assay)
-
reduction_name(str, default:'pca') –key for storage in seurat.reductions
-
reduction_key(str, default:'PC_') –prefix for dimension names (e.g. 'PC_')
-
seed(int, default:42) –random seed for reproducibility
-
layer(str, default:'scale.data') –which layer to take data from
Source code in truecell/reduction.py
run_spca
¶
run_spca(seurat, graph: str, npcs: int = 50, features: Optional[list[str]] = None, assay: Optional[str] = None, reduction_name: str = 'spca', reduction_key: str = 'SPC_', seed: int = 42, layer: str = 'scale.data') -> None
Supervised PCA — the gene axes that best explain a cell-cell graph.
Mirrors R's RunSPCA(obj, assay = "SCT", graph = "wsnn"). Ordinary PCA
picks the directions of greatest variance and knows nothing about which
cells you consider neighbours. sPCA is handed a graph you already trust —
typically the WNN graph from find_multi_modal_neighbors, which knows about
protein as well as RNA — and finds the directions in gene space that best
reproduce it. Where PCA maximises vᵀXᵀXv, sPCA maximises vᵀXᵀGXv:
the same problem with the identity swapped for the graph. Set G = I and
you get PCA back exactly.
The point is the loadings. Because sPCA is still a linear map from genes to components, a query dataset can be pushed into a reference's graph-defined space with a single matrix multiply, which is what makes it the reduction Azimuth maps onto.
Parameters:
-
graph(str) –key in
seurat.graphs— a cell × cell graph (e.g. "wsnn") -
npcs(int, default:50) –number of components
-
features(Optional[list[str]], default:None) –genes to use (defaults to variable features)
-
assay(Optional[str], default:None) –assay name (defaults to active assay)
-
reduction_name(str, default:'spca') –key for storage in seurat.reductions
-
reduction_key(str, default:'SPC_') –prefix for dimension names (e.g. 'SPC_')
-
seed(int, default:42) –random seed (the eigensolver starts from a random vector)
-
layer(str, default:'scale.data') –which layer to take data from
Notes
Seurat runs irlba on XᵀGX, which is an SVD and so ranks components by
|λ|; we take the largest eigenvalues themselves, since vᵀXᵀGXv is
what is being maximised and a graph can push some eigenvalues negative. With
non-negative edge weights the leading eigenvalues are positive and the two
orderings agree, so this only ever differs in the tail.
Source code in truecell/reduction.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
run_ica
¶
run_ica(seurat, nics: int = 50, features: Optional[list[str]] = None, assay: Optional[str] = None, reduction_name: str = 'ica', reduction_key: str = 'ICA_', seed: int = 42, layer: str = 'scale.data', max_iter: int = 200) -> None
Independent Component Analysis on scaled data.
Mirrors R's RunICA(obj, nics = 50). Stores a DimReduc (embeddings +
loadings) under reduction_name; find_neighbors / run_umap
already accept reduction="ica".
Source code in truecell/reduction.py
glm_pca
¶
glm_pca(seurat, n_components: int = 10, features: Optional[list[str]] = None, assay: Optional[str] = None, reduction_name: str = 'glmpca', reduction_key: str = 'GLMPC_', family: str = 'poisson', layer: str = 'counts', max_iter: int = 100, tol: float = 0.0001, penalty: float = 1.0, learning_rate: float = 0.1, theta: float = 100.0, optimize_theta: bool = True, seed: int = 42) -> None
Fit a Poisson GLM-PCA and store it as a DimReduc.
Mirrors R's RunGLMPCA(obj, L = 10). Takes raw counts, not normalised
or scaled data — the whole point is to model the counts as counts. Stores
factors as cell_embeddings and loadings as feature_loadings, so
find_neighbors(obj, reduction="glmpca") and run_umap work downstream
exactly as they do off PCA.
Parameters:
-
n_components(int, default:10) –rank of the fit (L) — the number of factors
-
features(Optional[list[str]], default:None) –genes to use (defaults to variable features)
-
assay(Optional[str], default:None) –assay name (defaults to active assay)
-
family(str, default:'poisson') –noise model —
"poisson"or"nb"(negative binomial) -
layer(str, default:'counts') –layer to read counts from
-
max_iter(int, default:100) –maximum Fisher scoring iterations
-
tol(float, default:0.0001) –stop when the relative change in deviance falls below this
-
penalty(float, default:1.0) –L2 ridge on U and V. U and V can trade scale freely (
U·Vᵀ = (cU)·(V/c)ᵀ); the ridge is what pins that down. -
learning_rate(float, default:0.1) –initial Fisher step size. Halved on any step that fails to lower the deviance, so this is an opening bid, not a commitment.
-
theta(float, default:100.0) –negative-binomial dispersion (
Var = μ + μ²/θ). Ignored for Poisson. Withoptimize_thetait is only the starting value; otherwise it is held fixed at this number for the whole fit. -
optimize_theta(bool, default:True) –re-estimate
θby maximum likelihood between factor updates (NB only). Turn off to fit at a dispersion you already trust — that also restores strict monotone deviance, since a movingθre-scales the deviance under it. -
seed(int, default:42) –only used if the counts have no structure at all — the fit is started deterministically from the data (see
_init_factors)
Notes
Deviance falls monotonically by construction: a step that raises it (or
overflows) is rejected outright and retried at half the step size. The full
trace is kept in reduction.misc["deviance"] — if it is still dropping
steeply at the end, raise max_iter. misc["converged"] says whether the
fit stopped because it was done or because it ran out of iterations.
Fitting is dense in genes × cells. Pass a few thousand variable features
rather than the whole transcriptome, as you would to run_pca.
Source code in truecell/glmpca.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | |
Non-linear embeddings¶
run_umap
¶
run_umap(seurat, dims: Optional[Union[list[int], range]] = None, reduction: str = 'pca', graph: Optional[str] = None, n_components: int = 2, n_neighbors: int = 30, min_dist: float = 0.3, metric: str = 'euclidean', reduction_name: str = 'umap', reduction_key: str = 'UMAP_', seed: int = 42, assay: Optional[str] = None) -> None
Compute a UMAP embedding.
Mirrors R's RunUMAP(pbmc, dims = 1:10) and RunUMAP(pbmc, graph = "wsnn"). Stores a DimReduc in seurat.reductions[reduction_name].
Two input modes (mutually exclusive):
reduction(default): embed the cells from a low-dimensional reduction (PCA/Harmony/ICA…). The fittedumap-learnmodel is stashed indr.misc["umap_model"]for later transform-only projection.graph: embed a precomputed neighbour graph directly (e.g. the WNN"wsnn"graph fromfind_multi_modal_neighbors), via UMAP'ssimplicial_set_embedding.reduction/dimsare ignored.
Parameters:
-
dims(Optional[Union[list[int], range]], default:None) –which dimensions of 'reduction' to use (0-indexed)
-
reduction(str, default:'pca') –source reduction ('pca' by default)
-
graph(Optional[str], default:None) –name of a precomputed graph in seurat.graphs to embed (takes precedence over
reductionwhen given) -
n_components(int, default:2) –output dimensions (2 for visualization)
-
n_neighbors(int, default:30) –UMAP n_neighbors (Seurat default 30)
-
min_dist(float, default:0.3) –UMAP min_dist (Seurat default 0.3)
-
metric(str, default:'euclidean') –distance metric (reduction mode only)
-
reduction_name(str, default:'umap') –storage key in seurat.reductions
-
seed(int, default:42) –random seed
Source code in truecell/umap.py
run_tsne
¶
run_tsne(seurat, dims: Optional[list[int]] = None, reduction: str = 'pca', n_components: int = 2, perplexity: float = 30.0, reduction_name: str = 'tsne', reduction_key: str = 'tSNE_', seed: int = 42, assay: Optional[str] = None) -> None
t-SNE embedding from an existing reduction.
Mirrors R's RunTSNE(obj, dims = 1:10). Stores a DimReduc under
reduction_name.
Source code in truecell/reduction.py
How many components to keep¶
jack_straw
¶
jack_straw(seurat, reduction: str = 'pca', dims: int = 20, num_replicate: int = 100, prop_freq: float = 0.01, layer: str = 'scale.data', seed: int = 42) -> 'JackStrawData'
Permutation test for the significance of PCA dimensions.
Mirrors R's JackStraw(): a small fraction (prop_freq) of features is
permuted across cells, the PCA is re-run on the permuted matrix, and the
permuted features' loadings in that refit basis form the null distribution
per PC. Each observed loading is then assigned an empirical p-value. Results
are stored on seurat.reductions[reduction].jackstraw and returned.
The refit is the expensive part and it is not optional: an earlier version of
this function built the null by projecting the permuted rows onto the
fixed original embedding, which is far cheaper but produces a much tighter
null — a fixed basis cannot rotate to absorb the scrambled signal, so the
permuted loadings come out too small and ordinary noise features look
extreme against them. On pbmc3k that inflated the count of "significant"
features on the pure-noise PCs 14-20 from R's 0-5 to 109-203, and left
score_jackstraw unable to reject any PC at all.
Cost scales as num_replicate full PCAs; ~1-2 minutes for the Seurat
defaults on a 2000-feature, 2700-cell object. Lower num_replicate when
iterating, but note it also sets the p-value resolution: the smallest
non-zero empirical p is 1 / (num_replicate * n_permuted).
Parameters:
-
reduction(str, default:'pca') –reduction to test (default 'pca')
-
dims(int, default:20) –number of PCs to score
-
num_replicate(int, default:100) –permutation replicates (Seurat default 100)
-
prop_freq(float, default:0.01) –fraction of features permuted per replicate (default 0.01)
-
layer(str, default:'scale.data') –scaled layer feeding the reduction
-
seed(int, default:42) –RNG seed
Source code in truecell/jackstraw.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | |
score_jackstraw
¶
score_jackstraw(seurat, reduction: str = 'pca', dims: Optional[int] = None, score_thresh: float = 1e-05) -> ndarray
Aggregate per-feature JackStraw p-values into one p-value per PC.
Mirrors R's ScoreJackStraw(): count the features whose empirical p-value
falls at or below score_thresh, and test that count against the number
expected under a uniform null (floor(n_features * score_thresh)) with a
two-proportion test. A small returned value marks a significant PC. A PC
with no feature below the threshold scores exactly 1, as in R.
Not a distributional goodness-of-fit test. An earlier version used a one-sided KS test against Uniform(0, 1), which is enormously more sensitive: with thousands of features it returned p-values around 1e-112 or smaller for every PC on pbmc3k, including pure noise, so no PC ever failed and the function could not do the one job it exists for — telling you where to cut.