Demultiplexing and pooled screens¶
Two workflows that both start from a second assay of oligo counts and end in a
per-cell call written back into meta_data.
Cell hashing assigns pooled samples from hashtag counts. hto_demux is
Seurat's HTODemux — CLR, cluster into k = n_hashtags + 1, per-hashtag
negative-binomial background threshold, then singlet / doublet / negative.
multiseq_demux is the MULTI-seq alternative, a Gaussian-KDE quantile threshold
per barcode. On the cross-species ground truth they are 99.81 % call-concordant
with R.
Mixscape separates real CRISPR knockouts from escapers. calc_perturb_sig
subtracts each cell's nearest non-targeting controls; run_mixscape then fits
the two-component mixture per guide; mixscape_lda builds the supervised map on
which each guide population separates.
The CLR margin defaults are deliberate
hto_demux and multiseq_demux normalize across features (margin 1),
not across cells. That is what HTODemux does, and it is not the same
default as the general-purpose CLR path. Changing it to 2 to "make them
consistent" would break agreement with Seurat.
Cell hashing¶
hto_demux
¶
hto_demux(seurat, assay: str = 'HTO', positive_quantile: float = 0.99, init: Optional[int] = None, nstarts: int = 10, kfunc: str = 'clara', nsamples: int = 100, normalize: bool = True, margin: int = 1, seed: int = 42, verbose: bool = False)
Demultiplex pooled samples from hashtag counts (Seurat's HTODemux).
Mirrors HTODemux(object, assay = "HTO", positive.quantile = 0.99). Each
hashtag's positive/negative cutoff is learned by fitting a negative binomial
to the tag's background — the cluster in which it is least expressed — and
thresholding at positive_quantile. Cells positive for zero / one / many
hashtags are called Negative / Singlet / Doublet.
The object is mutated in place: five <assay>_* metadata columns plus
hash.ID are written (matching Seurat), the active identity is set to
hash.ID, and the learned cutoffs are stashed in obj.misc["hto_demux"].
Parameters:
-
seurat–a
Truecellobject carrying a hashtag assay. -
assay(str, default:'HTO') –the hashtag assay to demultiplex (default
"HTO"). -
positive_quantile(float, default:0.99) –quantile of each tag's fitted background at which the positive cutoff is set (Seurat default 0.99).
-
init(Optional[int], default:None) –number of clusters; default
n_hashtags + 1. -
nstarts(int, default:10) –kmeansonly — restarts (n_init). Seurat uses 100; 10 is a faster, usually-equivalent default. Ignored byclara. -
kfunc(str, default:'clara') –"clara"(default, Seurat's k-medoids — seetruecell._clara) or"kmeans". The two rarely disagree on which cluster is a tag's background, so the calls usually match either way (~1% of cells differ on synthetic panels, rising with tag count). Both scale linearly in cells;claracosts a roughly constant 4× (~1.3 s vs ~0.3 s at 100k cells), so the choice is about matching R, not speed. -
nsamples(int, default:100) –claraonly — sub-samples to draw (Seurat's default, 100). Ignored bykmeans. -
normalize(bool, default:True) –CLR-normalize the counts internally for clustering and margins (default). Set False to use the assay's existing
datalayer (e.g. a priornormalize_data(method="CLR")). -
margin(int, default:1) –CLR margin when
normalizeis True — 1 (per hashtag across cells; Seurat's default, and what the hashing vignette normalizes with) or 2 (per cell across hashtags). -
seed(int, default:42) –kmeansonly — random seed. Has no effect onclara(the default), which draws from its own generator that R cannot seed either — seetruecell._clara. -
verbose(bool, default:False) –print each hashtag's learned cutoff.
Returns:
-
Truecell–seurat, with the classification metadata andhash.IDidentity.
Source code in truecell/hto.py
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 | |
multiseq_demux
¶
multiseq_demux(seurat, assay: str = 'HTO', quantile: float = 0.7, autothresh: bool = False, maxiter: int = 5, qrange: Optional[Sequence[float]] = None, normalize: bool = True, margin: int = 1, verbose: bool = False)
Demultiplex pooled samples from barcode counts (Seurat's MULTIseqDemux).
Mirrors MULTIseqDemux(object, assay = "HTO", quantile = 0.7). For each
barcode a Gaussian-kernel-density threshold is placed a fraction quantile
of the way between its background and positive modes; cells positive for zero /
one / many barcodes are called Negative / Singlet / Doublet.
The object is mutated in place: MULTI_ID and MULTI_classification
metadata columns are written, the active identity is set to MULTI_ID, and
the learned thresholds are stashed in obj.misc["multiseq_demux"].
Parameters:
-
seurat–a
Truecellobject carrying a hashtag/barcode assay. -
assay(str, default:'HTO') –the barcode assay to demultiplex (default
"HTO"). -
quantile(float, default:0.7) –fraction between each barcode's background and positive modes at which its positive cutoff is placed (Seurat default 0.7). Ignored when
autothreshis True. -
autothresh(bool, default:False) –sweep
qrangefor the quantile that maximizes the singlet rate, iteratively removing negatives and re-thresholding the remainder (up tomaxiterrounds). Overridesquantile. -
maxiter(int, default:5) –maximum auto-threshold rounds (default 5).
-
qrange(Optional[Sequence[float]], default:None) –quantiles swept when
autothreshis True (default0.1, 0.15, … 0.9). -
normalize(bool, default:True) –CLR-normalize the counts internally (default). Set False to use the assay's existing
datalayer (e.g. a priornormalize_data(method="CLR")). -
margin(int, default:1) –CLR margin when
normalizeis True — 1 (per barcode across cells; Seurat's default) or 2 (per cell across barcodes). -
verbose(bool, default:False) –print each auto-threshold round's chosen quantile.
Returns:
-
Truecell–seurat, with theMULTI_IDclassification and identity.
Source code in truecell/multiseq.py
Mixscape¶
calc_perturb_sig
¶
calc_perturb_sig(seurat, assay: str = 'RNA', features: Optional[Sequence[str]] = None, layer: str = 'data', labels: str = 'gene', nt_class: str = 'NT', split_by: Optional[str] = None, num_neighbors: int = 20, reduction: str = 'pca', ndims: int = 15, new_assay: str = 'PRTB')
Compute each cell's local perturbation signature (Seurat's CalcPerturbSig).
Mirrors CalcPerturbSig(object, assay, gd.class, nt.cell.class, reduction,
ndims, num.neighbors, new.assay.name = "PRTB"). For every cell, the mean
expression of its num_neighbors nearest non-targeting (NT) control cells
— in the first ndims dimensions of reduction — is subtracted from its
own expression. The residual (the deviation from the controls the cell most
resembles) is stored as a new assay, ready for run_mixscape.
Parameters:
-
seurat–a
Truecellobject with a guide-assignment metadata column and a computedreduction. -
assay(str, default:'RNA') –source expression assay (default
"RNA"). -
features(Optional[Sequence[str]], default:None) –genes to include (default: the assay's variable features, or all features if none are set).
-
layer(str, default:'data') –expression layer to difference (default
"data", the log-normalized values). -
labels(str, default:'gene') –metadata column holding each cell's target-gene / guide class.
-
nt_class(str, default:'NT') –the value in
labelsmarking non-targeting control cells. -
split_by(Optional[str], default:None) –optional metadata column; neighbours are found only within the same group (e.g. replicate), so batch is not mistaken for signal.
-
num_neighbors(int, default:20) –NT neighbours averaged per cell (Seurat default 20); capped at the number of NT cells available in the group.
-
reduction(str, default:'pca') –reduction whose embedding defines "nearest" (default
"pca"). -
ndims(int, default:15) –leading dimensions of
reductionto use (default 15). -
new_assay(str, default:'PRTB') –name of the perturbation-signature assay to create (default
"PRTB").
Returns:
-
Truecell–seurat, with the perturbation-signature assaynew_assayattached.
Source code in truecell/mixscape.py
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 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
run_mixscape
¶
run_mixscape(seurat, assay: str = 'PRTB', labels: str = 'gene', nt_class: str = 'NT', de_assay: str = 'RNA', layer: str = 'data', min_de_genes: int = 5, min_cells: int = 5, logfc_threshold: float = 0.25, min_pct: float = 0.05, pval_cutoff: float = 0.05, iter_num: int = 10, prtb_type: str = 'KO', new_class: str = 'mixscape_class', de_test: str = 'wilcox', seed: int = 0, verbose: bool = False)
Classify perturbed vs. escaping cells per guide (Seurat's RunMixscape).
Mirrors RunMixscape(object, assay = "PRTB", labels = "gene",
nt.class.name = "NT", de.assay = "RNA", min.de.genes = 5, iter.num = 10,
prtb.type = "KO"). Operating on the perturbation signature from
calc_perturb_sig, each target gene's cells are split into knockout
(KO) and non-perturbed (NP) by an iterative two-component Gaussian
mixture over their projection onto the gene's perturbation vector (see the
module docstring). NT cells stay NT.
The object is mutated in place: mixscape_class (also set as the active
identity), mixscape_class.global, and mixscape_class_p_<type> metadata
columns are written, and per-gene bookkeeping is stashed in
obj.misc["mixscape"].
Parameters:
-
seurat–a
Truecellobject carrying theassayperturbation signature and alabelsguide column. -
assay(str, default:'PRTB') –perturbation-signature assay (default
"PRTB"). -
labels(str, default:'gene') –metadata column of per-cell target-gene / guide class.
-
nt_class(str, default:'NT') –value in
labelsmarking non-targeting controls. -
de_assay(str, default:'RNA') –assay used for the gene-vs-NT differential expression (default
"RNA"). -
layer(str, default:'data') –signature layer to project (default
"data"). -
min_de_genes(int, default:5) –a gene needs at least this many DE genes to be testable; otherwise all its cells are NP (Seurat default 5).
-
min_cells(int, default:5) –a gene needs at least this many cells; otherwise NP.
-
logfc_threshold(float, default:0.25) –|avg_log2FC|DE cutoff passed tofind_markers(0.25). -
min_pct(float, default:0.05) –min.pctDE cutoff passed tofind_markers(0.05). -
pval_cutoff(float, default:0.05) –adjusted-p cutoff a DE gene must clear (0.05).
-
iter_num(int, default:10) –maximum mixture-refinement rounds per gene (Seurat 10).
-
prtb_type(str, default:'KO') –label for the perturbed class (default
"KO"; use e.g."KD"for a knock-down screen). Also names the posterior columnmixscape_class_p_<type>. -
new_class(str, default:'mixscape_class') –base name for the output columns / identity (default
"mixscape_class"). -
de_test(str, default:'wilcox') –find_markerstest for the gene-vs-NT DE (default"wilcox"). -
seed(int, default:0) –random state for the Gaussian mixture (determinism).
-
verbose(bool, default:False) –print each gene's DE-gene count and final KO count.
Returns:
-
Truecell–seurat, with themixscape_classclassification and identity.
Source code in truecell/mixscape.py
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 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 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | |
mixscape_lda
¶
mixscape_lda(seurat, labels: str = 'gene', nt_class: str = 'NT', assay: str = 'PRTB', de_assay: str = 'RNA', layer: str = 'data', npcs: int = 10, logfc_threshold: float = 0.25, min_pct: float = 0.1, pval_cutoff: float = 0.05, de_test: str = 'wilcox', reduction_name: str = 'lda', reduction_key: str = 'LDA_', scale_max: float = 10.0, seed: int = 42, verbose: bool = False)
Linear-discriminant projection that separates the guide classes (Seurat's MixscapeLDA).
Mirrors MixscapeLDA(object, pc.assay = "PRTB", labels = "gene",
nt.class.name = "NT", npcs = 10), which asks a complementary question to
run_mixscape: not which cells are perturbed but how do the whole
guide populations differ from one another and from control. It builds a
single supervised 2-D-ish map on which every guide class (and NT) forms its
own cloud, the classic mixscape LDA plot. The only prerequisite is a
perturbation-signature assay from calc_perturb_sig; the mixscape KO/NP
calls are not used — cells are grouped by their raw guide label.
The construction (Seurat's PrepLDA → RunLDA):
- Per-guide feature blocks. For each target gene, its cells are tested
against NT (on
de_assay) to find that guide's response genes; a guide with fewer thannpcs + 1such genes is dropped (it cannot supportnpcscomponents). Restricted to those genes on the perturbation-signatureassay, a PCA is fit on that guide's cells plus the NT cells, and then every cell in the dataset is projected onto that guide'snpcs-dim subspace. Each surviving guide thus contributesnpcscolumns describing where all cells fall along its perturbation axes. - One LDA over the concatenation. The per-guide blocks are stacked side by
side into one cell × (guides ·
npcs) matrix and a single linear discriminant analysis (sklearnLinearDiscriminantAnalysis) is fit with the guide label as the class — finding then_classes − 1directions that best separate the guide populations (including NT). The discriminant scores are stored as a reduction (default"lda", key"LDA_"), and the per-cell class assignment and posteriors are written to metadata (lda_assignmentsandLDAP_<class>).
Two choices differ from a literal reading of R, both documented:
- The per-guide subspace is read from the signature's
datalayer, scaled against the guide-plus-NT reference, in place of Seurat'sScaleData→RunPCA→ProjectCellEmbeddingschain. The composition is the same map: centre/scale each response gene by the reference cells' mean and SD, project through the reference PCA loadings. - The leave-one-out CV posterior (MASS
lda(..., CV = TRUE)) that Seurat stashes inmiscis not computed; only the resubstitution assignment and posterior are kept, which is all the plot and the metadata columns use.
Parameters:
-
seurat–a
Truecellobject carrying theassayperturbation signature (fromcalc_perturb_sig) and alabelsguide column. -
labels(str, default:'gene') –metadata column of per-cell target-gene / guide class.
-
nt_class(str, default:'NT') –value in
labelsmarking non-targeting controls. -
assay(str, default:'PRTB') –perturbation-signature assay projected for the LDA features (default
"PRTB"). -
de_assay(str, default:'RNA') –assay for the per-guide guide-vs-NT differential expression (default
"RNA"). -
layer(str, default:'data') –signature layer to project (default
"data"). -
npcs(int, default:10) –per-guide PCA components (Seurat default 10); a guide needs at least
npcs + 1DE genes to contribute. -
logfc_threshold(float, default:0.25) –|avg_log2FC|DE cutoff passed tofind_markers(0.25). -
min_pct(float, default:0.1) –min.pctDE cutoff passed tofind_markers(0.1, as in Seurat'sTopDEGenesMixscape). -
pval_cutoff(float, default:0.05) –adjusted-p cutoff a DE gene must clear (0.05).
-
de_test(str, default:'wilcox') –find_markerstest for the guide-vs-NT DE ("wilcox"). -
reduction_name(str, default:'lda') –key under which the LDA reduction is stored (default
"lda"). -
reduction_key(str, default:'LDA_') –prefix for the discriminant dimension names (
"LDA_"). -
scale_max(float, default:10.0) –clip scaled values to
±scale_maxbefore PCA, matching Seurat'sScaleData(default 10;Noneto disable). -
seed(int, default:42) –random state for the per-guide PCA (determinism).
-
verbose(bool, default:False) –print each guide's DE-gene count and whether it contributed.
Returns:
-
Truecell–seurat, with thereduction_nameLDA reduction and thelda_assignments/LDAP_<class>metadata columns.
Source code in truecell/mixscape.py
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 | |