Working at scale¶
Two independent answers to a dataset that will not fit: analyse a representative subset, or keep the matrix on disk.
Sketching draws a leverage-weighted subset — rare states kept rather than
sampled away — analyses that, then extends the result back to every cell.
leverage_score gets the per-cell scores via a CountSketch, without a full SVD.
LazyMatrix is the on-disk path, memory-mapped compressed-sparse-column
arrays in BPCells' spirit but with no new dependency. A slice reads only the
cells it touches, col_blocks streams a million cells at bounded RAM, and it
drops straight into an Assay5 layer. Against BPCells on PBMC 3k, truecell's
on-disk and in-memory paths are bit-identical to each other; Seurat's differ by
1.0e-06. The comparison.
Sketching¶
leverage_score
¶
leverage_score(obj, nsketch: int = 5000, ndims: Optional[int] = None, features: Optional[list[str]] = None, assay: Optional[str] = None, layer: str = 'data', var_name: Optional[str] = 'leverage.score', eps: float = 0.5, seed: int = 123) -> ndarray
Per-cell statistical leverage (Seurat's LeverageScore).
A cell's leverage is how much it influences the column space of the data — low in a dense, redundant cloud, high in a sparse, distinctive corner — so sampling proportional to it keeps the rare states a uniform draw would lose.
Which of the two regimes runs is decided exactly as Seurat decides it. With
fewer than nsketch * 1.5 cells the scores come from a rank-50 truncated
SVD and sum to 50; above that a CountSketch embedding, a QR and a
Johnson–Lindenstrauss projection stand in for the SVD, and the scores are on
the projection's scale rather than summing to 50. Compare scores within one
call, never across the two regimes.
Parameters:
-
obj–a
Truecellobject (normalized). -
nsketch(int, default:5000) –rows of the random sketch, and the threshold that picks the regime.
-
ndims(Optional[int], default:None) –dimension the JL projection targets before
epsshrinks it (default: the cell count, as in Seurat). Sketched regime only. -
features(Optional[list[str]], default:None) –features to score on (default: the assay's variable features).
-
assay(Optional[str], default:None) –assay to use (default: active assay).
-
layer(str, default:'data') –layer to draw the data from. Defaults to
"data"— the log-normalized values, which is what Seurat scores — not"scale.data". -
var_name(Optional[str], default:'leverage.score') –if given, the scores are also written to
obj.meta_data[var_name]. -
eps(float, default:0.5) –Johnson–Lindenstrauss distortion,
0 < eps <= 1(Seurat's 0.5). Smaller keeps more projected dimensions. Sketched regime only. -
seed(int, default:123) –random seed for the sketch and the projection.
Returns:
-
ndarray–One leverage score per cell, in
obj.cell_names()order.
Source code in truecell/sketch.py
sketch_data
¶
sketch_data(obj, ncells: int = 5000, method: str = 'LeverageScore', features: Optional[list[str]] = None, assay: Optional[str] = None, layer: str = 'data', nsketch: int = 5000, sketched_assay: str = 'sketch', var_name: Optional[str] = 'leverage.score', seed: int = 123)
Draw a leverage-weighted subset of cells (Seurat's SketchData).
Mirrors SketchData(object, ncells = 5000, method = "LeverageScore"). Each
cell is sampled without replacement with probability proportional to its
leverage_score, so the rare states a uniform sample would drop are kept
(indeed over-represented). The leverage scores are written back onto obj's
metadata, and the drawn subset is returned as a standalone
Truecell object — run the expensive analysis (PCA, clustering,
UMAP) on it and use project_data to extend the results to every cell.
This departs from Seurat, which stores the sketch as an extra assay on the same
object; here it is a separate object, matching the roadmap and truecell's
subset model. Its active assay is renamed to sketched_assay so the
provenance is visible, and obj.misc["sketch"] records how it was drawn.
Parameters:
-
obj–a
Truecellobject (normalized). -
ncells(int, default:5000) –cells to keep (capped at the number available).
-
method(str, default:'LeverageScore') –"LeverageScore"(leverage-weighted) or"Uniform"(equal weights), as in Seurat."Uniform"is the control that shows what leverage weighting is buying. -
features(Optional[list[str]], default:None) –features to score on (default: variable features).
-
assay(Optional[str], default:None) –assay to use (default: active assay).
-
layer(str, default:'data') –layer to draw the data from (default
"data"). -
nsketch(int, default:5000) –sketch size passed to
leverage_score. -
sketched_assay(str, default:'sketch') –name the returned object's active assay is renamed to.
-
var_name(Optional[str], default:'leverage.score') –metadata column the scores are written to on
obj. -
seed(int, default:123) –random seed for scoring and sampling.
Returns:
-
Truecell–The sketched subset (a new object).
Source code in truecell/sketch.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 | |
project_data
¶
project_data(full, sketch, reduction: str = 'pca', full_reduction: str = 'pca.full', umap_reduction: str = 'umap', full_umap_reduction: str = 'ref.umap', refdata: Optional[Union[str, dict]] = None, project_umap: bool = True, dims: Optional[Union[list[int], range]] = None, k_weight: int = 50, sd_weight: float = 1.0, layer: str = 'scale.data')
Extend a sketch's analysis to the full dataset (Seurat's ProjectData).
The inverse of sketch_data: once the sketch has been reduced and
(optionally) clustered, every full-dataset cell is placed into the sketch's
coordinate system and, if asked, given the sketch's labels.
- PCA. Each full cell is pushed through the sketch's PCA loadings — the
same "project into a space this cell never helped define" linear map that
truecell.project_umapuses — and stored asfull.reductions[full_reduction]. - UMAP (when
project_umapand the sketch carries a fitted UMAP model): the projected cells are run through the sketch's UMAP viatruecell.project_umap, stored asfull.reductions[full_umap_reduction]. - Labels (when
refdatais given): a weighted k-nearest-neighbour vote inside the projected reduction, where the sketch's own rows are the reference — Seurat'sTransferSketchLabels. Written ontofull.meta_data.
Step 3 is deliberately not the truecell.transfer anchor path, which
is what an earlier version of this function used. Seurat does not use anchors
here, and the difference is not academic: finding anchors between the sketch
and the full dataset costs exactly what sketching exists to avoid, so on the
million-cell objects this is written for the anchor route is unusable rather
than merely different. On ifnb the two now agree per-cell 98.1 % of the
time, at matching accuracy.
full is mutated in place and returned.
Parameters:
-
full–the full
Truecellobject (normalized + scaled on the sketch's PCA features). -
sketch–the sketched object from
sketch_data, already carrying a PCA (and optionally a fitted UMAP). -
reduction(str, default:'pca') –sketch reduction whose loadings project the full data.
-
full_reduction(str, default:'pca.full') –storage key for the projected PCA on
full. -
umap_reduction(str, default:'umap') –sketch reduction holding the fitted UMAP model.
-
full_umap_reduction(str, default:'ref.umap') –storage key for the projected UMAP on
full. -
refdata(Optional[Union[str, dict]], default:None) –sketch metadata to transfer, as in Seurat: a
dict{new_col: sketch_col}writes each label undernew_colplusnew_col.score, and a barestris shorthand for{col: col}. Must name a column on the sketch — like R, raw label arrays are not taken.Noneskips transfer. -
project_umap(bool, default:True) –also project the sketch's UMAP when a fitted model exists.
-
dims(Optional[Union[list[int], range]], default:None) –reduction dimensions used for the UMAP model and the label vote (default: all).
-
k_weight(int, default:50) –neighbours each cell votes over (Seurat's
k.weight). -
sd_weight(float, default:1.0) –bandwidth of the distance kernel (Seurat fixes this at 1).
-
layer(str, default:'scale.data') –layer to draw the full data's expression from.
Returns:
-
Truecell–full, now carrying the projected reduction(s) and any transferred labels.
Source code in truecell/sketch.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 | |
Out-of-core matrices¶
LazyMatrix
¶
LazyMatrix(path: Union[str, Path], shape: Tuple[int, int], data: ndarray, indices: ndarray, indptr: ndarray)
A memory-mapped, on-disk compressed-sparse-column matrix.
Instances are created by write_lazy_matrix (persist an in-memory
matrix) or open_lazy_matrix (map an existing store); they are not
constructed directly. The three CSC arrays are memory-mapped read-only, so
the object is cheap to hold and its footprint is the slices you touch — not
the whole matrix.
Supports the slicing idioms the assay layer accessors use — m[rows, cols],
m[np.ix_(rows, cols)], m[idx, :], m[:, idx], contiguous slices —
returning a scipy.sparse.csc_matrix block. Tuple indexing is always an
outer (cross-product) selection, matching np.ix_ and how layers are
block-subset throughout truecell; element-wise pair indexing is not supported.
Source code in truecell/lazy.py
col_blocks
¶
Stream the matrix in blocks of block_size columns (cells).
Yields (start, stop, block) where block is an in-memory
csc_matrix of columns [start:stop). This is the primitive for an
out-of-core reduction: process a million cells at bounded peak memory.
Source code in truecell/lazy.py
sum
¶
Sum over axis (0 → per-cell, 1 → per-feature, None → scalar).
Source code in truecell/lazy.py
mean
¶
Mean over axis, dividing the streamed sums by the matrix extent.
Source code in truecell/lazy.py
nnz_per_col
¶
nnz_per_row
¶
Non-zeros per row (feature) — the min_cells count.
The column counterpart is free from indptr; this one has to look at
every non-zero's row index, so it streams in cell-blocks rather than
mapping the whole indices array at once.
Source code in truecell/lazy.py
to_scipy
¶
Read the whole matrix into an in-memory csc_matrix.
toarray
¶
close
¶
Release the memory-mapped arrays.
Source code in truecell/lazy.py
write_lazy_matrix
¶
write_lazy_matrix(matrix, path: Union[str, Path], *, overwrite: bool = False) -> LazyMatrix
Write matrix to path as an on-disk CSC store and open it lazily.
matrix may be a scipy sparse matrix, a dense array-like, or another
LazyMatrix; it is canonicalised to sorted, duplicate-summed CSC
before being saved as three .npy arrays plus a JSON header. Returns a
LazyMatrix mapping the freshly written store.
Source code in truecell/lazy.py
open_lazy_matrix
¶
open_lazy_matrix(path: Union[str, Path]) -> LazyMatrix
Memory-map an on-disk CSC store written by write_lazy_matrix.
Source code in truecell/lazy.py
is_lazy
¶
True if x is a LazyMatrix (an on-disk, memory-mapped layer).