opencode-skills-collection 4.1.7 → 4.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +7 -1
- package/bundled-skills/court/SKILL.md +141 -0
- package/bundled-skills/court/references/jurors.json +14 -0
- package/bundled-skills/court/scripts/court.py +348 -0
- package/bundled-skills/darkmoon-pentest/SKILL.md +123 -0
- package/bundled-skills/lognorm/SKILL.md +86 -0
- package/bundled-skills/lognorm/references/playbooks.md +76 -0
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +6 -6
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +1 -1
- package/bundled-skills/parallel-search-mcp/SKILL.md +12 -0
- package/bundled-skills/scarf-single-cell/SKILL.md +326 -0
- package/bundled-skills/scarf-single-cell/references/LICENSE.md +29 -0
- package/bundled-skills/scarf-single-cell/references/clustering-and-embedding.md +270 -0
- package/bundled-skills/scarf-single-cell/references/data-access.md +269 -0
- package/bundled-skills/scarf-single-cell/references/features-and-graphs.md +279 -0
- package/bundled-skills/scarf-single-cell/references/gene-blacklists.md +182 -0
- package/bundled-skills/scarf-single-cell/references/integration-and-comparisons.md +295 -0
- package/bundled-skills/scarf-single-cell/references/markers-and-annotation.md +280 -0
- package/bundled-skills/scarf-single-cell/references/performance-and-export.md +291 -0
- package/bundled-skills/scarf-single-cell/references/pipeline-runs-and-artifacts.md +273 -0
- package/bundled-skills/scarf-single-cell/references/plotting.md +260 -0
- package/bundled-skills/scarf-single-cell/references/quality-control.md +290 -0
- package/bundled-skills/session-handoff/SKILL.md +112 -0
- package/bundled-skills/shark/SKILL.md +134 -0
- package/bundled-skills/shark/references/investors.json +12 -0
- package/bundled-skills/shark/scripts/shark.py +302 -0
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package-lock.json +6 -6
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package.json +1 -1
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package-lock.json +6 -6
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package.json +1 -1
- package/package.json +1 -1
- package/skills_index.json +201 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Study design, integration, and condition comparisons
|
|
2
|
+
|
|
3
|
+
Identify donors, samples, batches and conditions, correct a technical batch with Harmony and score
|
|
4
|
+
it, then compare composition and expression at the donor level. Docs: <https://scarf.readthedocs.io/en/latest/tutorials/batch_correction.html>, and in the same folder
|
|
5
|
+
`pseudobulk_and_differential_expression.html`, `condition_comparisons.html`,
|
|
6
|
+
`dataset_merging.html`, `mapping_and_label_transfer.html`, `downsampling.html`.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
- The store has several donors, samples, batches or conditions (most CELLxGENE atlases), or a
|
|
11
|
+
layout separates by donor or batch and you must decide whether to correct it.
|
|
12
|
+
- The question compares conditions (abundance of a population, or expression within one), or you
|
|
13
|
+
need to combine stores, map a query onto a fixed reference, or work on fewer cells.
|
|
14
|
+
|
|
15
|
+
## Key concepts
|
|
16
|
+
|
|
17
|
+
- **Units.** A donor is the biological replicate; a sample is one specimen from one donor; a
|
|
18
|
+
library is one capture; a batch is a processing group. Cells are never replicates: the n of a
|
|
19
|
+
condition comparison is the number of donors. Sample, library and batch columns may be absent.
|
|
20
|
+
- **Nesting.** Donors nested in conditions need between-donor tests. A donor sampled at several
|
|
21
|
+
timepoints is repeated measures: pair on the donor. A donor in both arms needs the rules below.
|
|
22
|
+
- **Denominators.** A share of all cells moves when any other population moves. A state's share of
|
|
23
|
+
its lineage parent (an activated state as a share of CD14 monocytes) does not depend on the
|
|
24
|
+
parent's abundance. Excluding contaminants (erythrocytes, platelets) or unresolved cells raises
|
|
25
|
+
every remaining share by `1 / (1 - excluded share)`, a factor that differs between samples.
|
|
26
|
+
- **Harmony corrects coordinates only.** `run_harmony` returns a `batch_correction` ref that
|
|
27
|
+
replaces PCA for ANN, neighbours, graph, UMAP and clustering; counts are untouched. A batch
|
|
28
|
+
holding one condition loses that condition's signal; Scarf ran silently with `condition`.
|
|
29
|
+
- **Metrics are evidence, not a verdict.** `metric_*` take exact neighbour or graph refs and a live
|
|
30
|
+
column and return a float. iLISI (mixing) should rise while cLISI and connectivity hold.
|
|
31
|
+
- **What Scarf tests.** `run_statistical_testing` runs donor-level rank tests (a
|
|
32
|
+
`statistical_tests` artifact; several keys share one BH pass). No count GLM (DESeq2, edgeR) or
|
|
33
|
+
compositional model (scCODA, propeller): export pseudobulk counts ("Export pseudobulk counts" in
|
|
34
|
+
`performance-and-export.md`) and model one population at a time with donor-level terms.
|
|
35
|
+
|
|
36
|
+
## Recipes
|
|
37
|
+
|
|
38
|
+
### Audit the study design
|
|
39
|
+
|
|
40
|
+
Later recipes reuse these imports and `ds`. All column names are placeholders; `cell_type` stands
|
|
41
|
+
for your own annotation column, never for author labels that are held out.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import re
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
import pandas as pd
|
|
48
|
+
import scarf
|
|
49
|
+
from scarf.plotting import CellField
|
|
50
|
+
|
|
51
|
+
ds = scarf.DataStore("analysis.zarr", min_features_per_cell=-1) # reopen; see data-access.md
|
|
52
|
+
hits = re.compile(r"donor|individual|patient|subject|sample|library|batch|"
|
|
53
|
+
r"condition|disease|treatment|time|day|assay|sex", re.I)
|
|
54
|
+
print([c for c in ds.cells.columns if hits.search(c)])
|
|
55
|
+
|
|
56
|
+
meta = ds.cells.to_pandas_dataframe(["donor_id", "condition", "timepoint", "batch"], key="I")
|
|
57
|
+
print(meta.isna().sum()) # resolve missing labels first
|
|
58
|
+
print(meta.groupby("donor_id").agg(
|
|
59
|
+
cells=("condition", "size"), conditions=("condition", "nunique"),
|
|
60
|
+
timepoints=("timepoint", "nunique"), batches=("batch", "nunique"),
|
|
61
|
+
))
|
|
62
|
+
print(meta.drop_duplicates(["donor_id", "condition"])["condition"].value_counts()) # donors per arm
|
|
63
|
+
units = meta.drop_duplicates(["donor_id", "batch", "condition"])
|
|
64
|
+
print(pd.crosstab(units["batch"], units["condition"])) # donors per batch x condition
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`conditions == 1` for every donor means nested; `timepoints > 1` means repeated measures;
|
|
68
|
+
`conditions > 1` puts a donor in two arms. A batch row with donors in one condition only is
|
|
69
|
+
confounded with it. No sample column: join donor and timepoint strings (every row, none missing)
|
|
70
|
+
and `ds.cells.insert("sample_id", values, overwrite=True)`; `insert` writes to the store at once.
|
|
71
|
+
|
|
72
|
+
### Harmony through the pipeline, with a matched baseline
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
design_cols = ("batch", "donor_id", "sample_id", "condition") # Harmony columns first
|
|
76
|
+
native = ds.pipeline.run(label="native", snapshot_columns=design_cols)
|
|
77
|
+
corrected = ds.pipeline.run(
|
|
78
|
+
label="harmony_batch", harmony_batch_columns=["batch"], snapshot_columns=design_cols,
|
|
79
|
+
)
|
|
80
|
+
assert native["pca"] == corrected["pca"] # same frozen cells and PCA; only the graph differs
|
|
81
|
+
print(corrected["harmony"]) # ArtifactRef(kind='batch_correction')
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The run's input snapshot is `names`, filter columns, Harmony columns, then `snapshot_columns`, in
|
|
85
|
+
order; another list or order gives a new analysis selection and recomputes everything (24
|
|
86
|
+
artifacts created, then 16 reused by the Harmony run; 10x 5K PBMC docs dataset). `run.cells`
|
|
87
|
+
exposes only `snapshot_columns`: read a filter or Harmony column with
|
|
88
|
+
`ds.cells.fetch_all(col)[run.cells.fetch_all("I")]`. Doublet scores still use plain PCA.
|
|
89
|
+
|
|
90
|
+
As a branch of a run, `harmony = ds.run_harmony(run["pca"], ["batch"])` returns a ref to pass to
|
|
91
|
+
`build_ann_index` in place of PCA (chain in `features-and-graphs.md`; keep the run's `k`). Score it
|
|
92
|
+
below with the run's `neighbors` and `connectivity_map` versus the new pair.
|
|
93
|
+
|
|
94
|
+
### Score native versus corrected
|
|
95
|
+
|
|
96
|
+
A shuffled batch column gives the iLISI of perfect mixing for the same graph and `k`. Label
|
|
97
|
+
concordance (ARI) needs two clustering refs on the same frozen cell selection.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
rng = np.random.default_rng(0)
|
|
101
|
+
ds.cells.insert("batch_shuffled", rng.permutation(ds.cells.fetch_all("batch")), overwrite=True)
|
|
102
|
+
|
|
103
|
+
def scores(neighbors, graph):
|
|
104
|
+
return {
|
|
105
|
+
"iLISI batch": ds.metric_ilisi("batch", neighbors),
|
|
106
|
+
"iLISI ceiling": ds.metric_ilisi("batch_shuffled", neighbors),
|
|
107
|
+
"batch mixing": ds.metric_proportional_batch_mixing("batch", neighbors),
|
|
108
|
+
"cLISI cell_type": ds.metric_clisi("cell_type", neighbors),
|
|
109
|
+
"connectivity cell_type": ds.metric_graph_connectivity("cell_type", graph),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
print(pd.DataFrame({
|
|
113
|
+
"native": scores(native["neighbors"], native["connectivity_map"]),
|
|
114
|
+
"harmony": scores(corrected["neighbors"], corrected["connectivity_map"]),
|
|
115
|
+
}).T.round(3))
|
|
116
|
+
print(ds.metric_label_concordance(native["clusters"], corrected["clusters"], metric="ari"))
|
|
117
|
+
for r in (native, corrected): # a condition-only cluster that vanishes signals overcorrection
|
|
118
|
+
print(pd.crosstab(r.cells.fetch("clusters"), r.cells.fetch("condition")))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### A donor sampled in two arms
|
|
122
|
+
|
|
123
|
+
Example: a donor's first sample is in arm A and a later one in arm B (ventilation status changed
|
|
124
|
+
between timepoints). Decide per comparison and write the rule down before testing:
|
|
125
|
+
|
|
126
|
+
- **Pool to one value per donor** when all of its samples fall in one group of this comparison
|
|
127
|
+
(patients versus healthy donors). Pool donors with several samples in one arm the same way.
|
|
128
|
+
- **Drop it from the between-group test** of the arms it spans (A versus B). Its samples share
|
|
129
|
+
genotype and history: counting it twice makes the groups dependent and invents a donor.
|
|
130
|
+
- **Describe it within the donor only.** Its B minus A change is one pair (n = 1): report the two
|
|
131
|
+
values, never a p-value. An exact Wilcoxon needs 6 such pairs to reach 0.03125.
|
|
132
|
+
- If dropping leaves 3 versus 3 donors (minimum exact two-sided p 0.1), report n, medians and
|
|
133
|
+
effect sizes without a test. Add a sensitivity run that excludes the donor everywhere.
|
|
134
|
+
- `sample_by="donor_id"` makes Scarf refuse such a donor ("Each sample must belong to exactly one
|
|
135
|
+
group"); `sample_by="sample_id"` silently counts it in both arms.
|
|
136
|
+
|
|
137
|
+
### Composition per donor with exact tests
|
|
138
|
+
|
|
139
|
+
Per-unit proportions (a donor's samples pooled), two-sided Mann-Whitney between independent donor
|
|
140
|
+
groups, BH across populations, and the n = 1 rows of two-arm donors.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from math import comb
|
|
144
|
+
|
|
145
|
+
from scipy.stats import PermutationMethod, false_discovery_control, mannwhitneyu
|
|
146
|
+
|
|
147
|
+
run = ds.pipeline.open(label="baseline")
|
|
148
|
+
in_run = np.asarray(run.cells.fetch_all("I"), dtype=bool)
|
|
149
|
+
cells = ds.cells.to_pandas_dataframe(["donor_id", "sample_id", "condition", "cell_type",
|
|
150
|
+
"cell_state"])[in_run]
|
|
151
|
+
excluded = ["Erythrocyte", "Platelet", "Unresolved"] # your contaminant and unresolved labels
|
|
152
|
+
drop = cells["cell_type"].isin(excluded)
|
|
153
|
+
print(drop.groupby(cells["sample_id"]).mean().round(3)) # report the removed share per sample
|
|
154
|
+
kept = cells[~drop]
|
|
155
|
+
arms = kept.groupby("donor_id")["condition"].nunique()
|
|
156
|
+
two_arm = arms.index[arms > 1].tolist() # donors sampled in both arms
|
|
157
|
+
|
|
158
|
+
per_sample = pd.crosstab(kept["sample_id"], kept["cell_type"], normalize="index")
|
|
159
|
+
independent = kept[~kept["donor_id"].isin(two_arm)] # two-arm donors leave the tests
|
|
160
|
+
per_donor = pd.crosstab(independent["donor_id"], independent["cell_type"], normalize="index")
|
|
161
|
+
arm = independent.groupby("donor_id")["condition"].first()
|
|
162
|
+
|
|
163
|
+
def exact_tests(props, arm, g1, g2):
|
|
164
|
+
rows = []
|
|
165
|
+
for pop in props.columns:
|
|
166
|
+
x = props[pop].reindex(arm.index[arm == g1]).dropna() # units without a value drop out
|
|
167
|
+
y = props[pop].reindex(arm.index[arm == g2]).dropna()
|
|
168
|
+
row = {"population": pop, "n_1": len(x), "n_2": len(y), "median_1": x.median(),
|
|
169
|
+
"median_2": y.median(), "auc": np.nan, "p_exact": np.nan}
|
|
170
|
+
if min(len(x), len(y)) >= 2: # as in Scarf: fewer than 2 units in a group is not tested
|
|
171
|
+
exact = comb(len(x) + len(y), len(x)) <= 100_000 # Scarf's switch to asymptotic
|
|
172
|
+
method = PermutationMethod(n_resamples=np.inf) if exact else "asymptotic"
|
|
173
|
+
u = mannwhitneyu(x, y, method=method) # the exact null counts ties
|
|
174
|
+
row.update(auc=u.statistic / (len(x) * len(y)), p_exact=u.pvalue)
|
|
175
|
+
rows.append(row)
|
|
176
|
+
table = pd.DataFrame(rows)
|
|
177
|
+
tested = table["p_exact"].notna() # untested rows keep NaN and stay out of the correction
|
|
178
|
+
table["p_bh"] = np.nan
|
|
179
|
+
table.loc[tested, "p_bh"] = false_discovery_control(table.loc[tested, "p_exact"], method="bh")
|
|
180
|
+
return table
|
|
181
|
+
|
|
182
|
+
print(exact_tests(per_donor, arm, "ctrl", "case").round(4))
|
|
183
|
+
parent = kept[kept["cell_type"] == "CD14 monocyte"] # lineage parent as denominator
|
|
184
|
+
state = (parent["cell_state"] == "CD14 monocyte (state 1)").groupby(parent["donor_id"]).mean()
|
|
185
|
+
print(exact_tests(state.to_frame("state 1 of CD14 monocytes"), arm, "ctrl", "case").round(4))
|
|
186
|
+
sample_info = kept.groupby("sample_id")[["donor_id", "condition"]].first()
|
|
187
|
+
print(per_sample.join(sample_info)[lambda t: t["donor_id"].isin(two_arm)].round(3)) # n = 1
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The same test in Scarf (a saved artifact): a 0/1 column averaged per donor is the donor's share,
|
|
191
|
+
and the cell selection is the denominator (snapshot the parent's cells for a parent share).
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
labels, donors = ds.cells.fetch_all("cell_type"), ds.cells.fetch_all("donor_id")
|
|
195
|
+
keys = []
|
|
196
|
+
for pop in per_donor.columns:
|
|
197
|
+
keys.append("is_" + re.sub(r"\W+", "_", pop))
|
|
198
|
+
ds.cells.insert(keys[-1], (labels == pop).astype(np.float64), overwrite=True)
|
|
199
|
+
counted = in_run & ~np.isin(labels, excluded) & ~np.isin(donors, two_arm)
|
|
200
|
+
ds.cells.insert("in_composition", counted, overwrite=True)
|
|
201
|
+
result = ds.run_statistical_testing(
|
|
202
|
+
keys, CellField("condition"), cell_selection=ds.snapshot_cell_selection("in_composition"),
|
|
203
|
+
groups=["ctrl", "case"], test="mann_whitney", sample_by="donor_id",
|
|
204
|
+
)
|
|
205
|
+
print(result.p_value_method, pd.concat([result.tables[k].assign(key=k) for k in keys]))
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Both routes gave identical exact and BH p-values on a synthetic design. Report n per group,
|
|
209
|
+
medians, AUC (`U / (n_1 n_2)`; 0.5 is no shift) and BH values, then repeat with all cells as the
|
|
210
|
+
denominator. `ds.plots.composition(..., kind="per_sample", study_design=...)` draws a strip of
|
|
211
|
+
sample proportions per category (its intervals count samples, so a two-sample donor counts twice);
|
|
212
|
+
`kind="stacked", sample_by=...` draws one stacked bar per sample.
|
|
213
|
+
|
|
214
|
+
### Donor-level expression tests
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
is_b = in_run & (labels == "B cell")
|
|
218
|
+
ds.cells.insert("is_b_cell", is_b, overwrite=True)
|
|
219
|
+
ds.cells.insert("one_arm_donor", ~np.isin(donors, two_arm), overwrite=True)
|
|
220
|
+
b_cells = ds.snapshot_cell_selection("is_b_cell") # frozen cell_selection ref
|
|
221
|
+
between = ds.run_statistical_testing( # donors nested in condition
|
|
222
|
+
["CD79A", "MS4A1"], CellField("condition"), cell_selection=b_cells, subset_by="one_arm_donor",
|
|
223
|
+
groups=["ctrl", "case"], test="mann_whitney", sample_by="donor_id",
|
|
224
|
+
)
|
|
225
|
+
within = ds.run_statistical_testing( # same donor, two timepoints
|
|
226
|
+
["CD79A", "MS4A1"], CellField("timepoint"), cell_selection=b_cells,
|
|
227
|
+
groups=["t0", "t1"], test="wilcoxon", sample_by="sample_id", pair_by="donor_id",
|
|
228
|
+
)
|
|
229
|
+
print(between.tables["CD79A"], within.tables["CD79A"], between.artifact, sep="\n")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Expression is averaged per sample, then ranked; `groups` fixes the order (`mean_difference =
|
|
233
|
+
mean_1 - mean_2`). Mann-Whitney is exact up to 100,000 group splits (`result.p_value_method`);
|
|
234
|
+
Wilcoxon keeps donors seen at both levels (`n_pairs`). Reload: `ds.get_statistical_tests(ref)`.
|
|
235
|
+
|
|
236
|
+
### Merge, map, downsample
|
|
237
|
+
|
|
238
|
+
`scarf.DataStoreMerge(datasets=[ds_a, ds_b], zarr_path=..., names=[...], assays=["RNA"],
|
|
239
|
+
prepend_text="orig", source_column="dataset", overwrite=True).dump()` gives an uncorrected joint
|
|
240
|
+
store; drop `is_primary_data == False` cells first and score iLISI before Harmony (not executed:
|
|
241
|
+
`MemoryError` under a 3 GB budget). Mapping keeps a reference fixed; the query is another store.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
reference = ds.get_mapping_reference(ds.build_mapping_reference(native["neighbors"]))
|
|
245
|
+
query = scarf.DataStore("query.zarr", default_assay="RNA")
|
|
246
|
+
mapping_ref = query.run_mapping(reference, query.snapshot_cell_selection("I"), save_k=5)
|
|
247
|
+
print(query.get_mapping_result(mapping_ref, reference=reference).diagnostics)
|
|
248
|
+
labels = query.get_target_classes(mapping_ref, "cell_type", reference=reference,
|
|
249
|
+
threshold_fraction=0.6)
|
|
250
|
+
query.cells.insert("transferred_cell_type", labels.to_numpy(), overwrite=True) # "NA" = abstained
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`ds.run_topacedo_sampler(graph, paris, max_sampling_rate=0.1)` keeps graph coverage, not
|
|
254
|
+
proportions (`sampled` is in graph-row order); to balance donors, snapshot a seeded mask instead.
|
|
255
|
+
|
|
256
|
+
## Parameters that matter
|
|
257
|
+
|
|
258
|
+
| Parameter | Default | Change when |
|
|
259
|
+
| --- | --- | --- |
|
|
260
|
+
| `harmony_batch_columns` / `batch_columns` | `None` (off) / required | Technical columns only |
|
|
261
|
+
| `neighbors_k` / `query_neighbors(k=)` | 11 | 21 or more when LISI metrics decide |
|
|
262
|
+
| metric `perplexity` | `None` = `floor(k/3)` | Keep fixed across graphs; capped at `k/3` |
|
|
263
|
+
| `sample_by` / `pair_by` | `None` | Always `sample_by`; `pair_by` only if repeated |
|
|
264
|
+
| `sample_stat` | `"mean"` | `"fraction"` + `expression_cutoff` for detection |
|
|
265
|
+
| `subset_by` | `None` | A boolean column, e.g. to drop two-arm donors |
|
|
266
|
+
|
|
267
|
+
## Check before moving on
|
|
268
|
+
|
|
269
|
+
- Every sample maps to one donor and one condition; donors per arm is the n; read confounding from
|
|
270
|
+
donors (`units`), not cells.
|
|
271
|
+
- Each donor counts once per test: `n_1` and `n_2` are donors, two-arm donors are pooled or
|
|
272
|
+
dropped as decided, and the denominator and the excluded share per sample are reported.
|
|
273
|
+
- After Harmony: iLISI moves toward the shuffled ceiling, cLISI and connectivity do not drop, no
|
|
274
|
+
condition-only cluster vanishes, and the ARI against native is explainable.
|
|
275
|
+
|
|
276
|
+
## Pitfalls
|
|
277
|
+
|
|
278
|
+
- `insert` stores `None` as `""` (Harmony silently treats it as a level; metrics raise) and `pd.NA`
|
|
279
|
+
as the literal `"<NA>"` (a real group everywhere). `to_pandas_dataframe` shows missingness.
|
|
280
|
+
- Correcting on `donor_id` or `sample_id` with a donor-level condition aligns donors and can absorb
|
|
281
|
+
the condition. Prefer library or batch; otherwise recheck condition-specific clusters.
|
|
282
|
+
- iLISI depends on `k`: a random synthetic batch scored 0.34, 0.67 and 0.76 at `k` 11, 21 and 31
|
|
283
|
+
(10x 5K PBMC docs dataset). CELLxGENE `cell_type` may come from the authors' own integration,
|
|
284
|
+
biasing cLISI toward it.
|
|
285
|
+
- `pair_by` on donors nested in conditions raises "Duplicate (pair, group) rows"; use `sample_by`.
|
|
286
|
+
- `scipy.stats.mannwhitneyu(method="exact")` ignores ties (donors at 0 tie), so it disagrees with
|
|
287
|
+
Scarf; `PermutationMethod(n_resamples=np.inf)` matches it but is exhaustive (12 versus 12: 20 s).
|
|
288
|
+
- Without `cell_selection`, a `CellField` grouping covers every stored cell, `I` False included.
|
|
289
|
+
- Marker search, Welch and ANOVA treat cells as observations; never use them for condition claims.
|
|
290
|
+
- A Symphony-style mapping correction (`query_batches`) needs a technical batch, never a condition.
|
|
291
|
+
|
|
292
|
+
## See also
|
|
293
|
+
|
|
294
|
+
- `quality-control.md`, `features-and-graphs.md`, `markers-and-annotation.md`, `plotting.md`
|
|
295
|
+
(`composition`), `pipeline-runs-and-artifacts.md`, `performance-and-export.md` (pseudobulk).
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Markers and annotation
|
|
2
|
+
|
|
3
|
+
Find cluster markers, turn marker evidence into cautious cell-type labels, separate progenitors
|
|
4
|
+
from multiplets, score gene sets and the cell cycle, and run a final comparison with existing
|
|
5
|
+
labels. Docs: <https://scarf.readthedocs.io/en/latest/tutorials/annotation.html>, <https://scarf.readthedocs.io/en/latest/tutorials/gene_set_scoring.html>,
|
|
6
|
+
<https://scarf.readthedocs.io/en/latest/tutorials/cell_cycle.html>, <https://scarf.readthedocs.io/en/latest/reference/api/datastore.html>.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
- A partition is chosen (`clustering-and-embedding.md`) and its clusters need names.
|
|
11
|
+
- You need marker tables, panel checks, multiplet checks, signature scores or cell-cycle phases.
|
|
12
|
+
- Labels are final and a held-out reference exists for a post-hoc evaluation.
|
|
13
|
+
|
|
14
|
+
## Key concepts
|
|
15
|
+
|
|
16
|
+
- `run_marker_search(clusters, features=...)` takes an exact `cluster_labels` or `cluster_cut` ref
|
|
17
|
+
and a feature-selection ref, streams counts once (remote for mounted stores) and returns
|
|
18
|
+
`ArtifactRef(kind="marker_table")`. `run["markers"]` is the table for `run["clusters"]`.
|
|
19
|
+
- Columns: `score` (Scarf specificity, 0 to 1, summing to 1 across groups for each gene; near
|
|
20
|
+
1/n_groups means ubiquitous), `mean`/`mean_rest`, `frac_exp`/`frac_exp_rest`, `fold_change`,
|
|
21
|
+
`auc`, two-sided Mann-Whitney `p_value` and `p_value_adjusted` (BH within each group). They are
|
|
22
|
+
one-versus-rest statistics over cells, not replicate-aware differential expression.
|
|
23
|
+
- `get_markers` returns a DataFrame with a string `group_id`, rows sorted by `score` within each
|
|
24
|
+
group and groups in natural order (`"2"` before `"10"`). Defaults `min_score=0.25`,
|
|
25
|
+
`min_frac_exp=0.2` hide most genes; pass `-1` for both to get every gene for every group.
|
|
26
|
+
- A label is a hypothesis. It needs named positive markers, low lineage-negative markers and a
|
|
27
|
+
plausible QC and doublet profile. Keep an explicit `unresolved` label instead of forcing a name.
|
|
28
|
+
- Doublet scores rise with RNA content, so plasma cells and other high-RNA types score high as
|
|
29
|
+
whole clusters. Judge doublets within a lineage and per cell, not by a global cut.
|
|
30
|
+
- No public API saves user-assigned labels as an artifact. Store them as a live column with
|
|
31
|
+
`ds.cells.insert` (mutable, unversioned, written into the store).
|
|
32
|
+
- AUCell and WAGGR return `kind="enrichment_scores"` (activity scores, not p-values); cell-cycle
|
|
33
|
+
scoring returns `kind="cell_cycle"` (`phase`, `s_score`, `g2m_score`). Rows follow the selection.
|
|
34
|
+
|
|
35
|
+
## Recipes
|
|
36
|
+
|
|
37
|
+
### Get and read marker tables
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import numpy as np
|
|
41
|
+
import pandas as pd
|
|
42
|
+
import scarf
|
|
43
|
+
|
|
44
|
+
scarf.configure_output(level="WARNING", progress=False)
|
|
45
|
+
ds = scarf.DataStore("analysis.zarr", nthreads=4, min_features_per_cell=-1)
|
|
46
|
+
run = ds.pipeline.open(label="baseline")
|
|
47
|
+
clusters, markers = run["clusters"], run["markers"]
|
|
48
|
+
# Another partition needs its own table (one pass over counts):
|
|
49
|
+
markers_res1 = ds.run_marker_search(run["leiden_1.0"], features=run["feature_universe"])
|
|
50
|
+
|
|
51
|
+
top = ds.get_markers(marker=markers) # min_score=0.25, min_frac_exp=0.2
|
|
52
|
+
print(top.groupby("group_id", sort=False).head(8)
|
|
53
|
+
.groupby("group_id", sort=False)["feature_name"].agg(", ".join))
|
|
54
|
+
one = ds.get_markers(marker=markers, group_id=3) # an int matches the label "3"
|
|
55
|
+
print(one[["feature_name", "score", "frac_exp", "frac_exp_rest", "auc", "p_value_adjusted"]].head())
|
|
56
|
+
everything = ds.get_markers(marker=markers, min_score=-1, min_frac_exp=-1) # all genes x groups
|
|
57
|
+
# ds.export_markers_to_csv(markers, "markers.csv") writes marker names per group, wide format
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Tabulate a canonical marker panel
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
panel = ["CD3E", "IL7R", "CD8A", "NKG7", "GNLY", "MS4A1", "CD79A", "MZB1", "JCHAIN", "CD14",
|
|
64
|
+
"LYZ", "FCGR3A", "FCER1A", "LILRA4", "PPBP", "HBB", "CD34", "MKI67"]
|
|
65
|
+
order = list(dict.fromkeys(everything["group_id"])) # pivot would sort "10" before "2"
|
|
66
|
+
hits = everything[everything["feature_name"].isin(panel)]
|
|
67
|
+
frac = (hits.pivot(index="group_id", columns="feature_name", values="frac_exp")
|
|
68
|
+
.reindex(index=order, columns=[g for g in panel if g in set(hits["feature_name"])]))
|
|
69
|
+
score = hits.pivot(index="group_id", columns="feature_name", values="score").reindex_like(frac)
|
|
70
|
+
print(frac.round(2), score.round(2), sep="\n\n") # detection versus exclusivity
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Flag clusters for review: QC, doublets, multiplets and progenitors
|
|
74
|
+
|
|
75
|
+
Per cell, count lineages whose two specific genes are both detected, and the progenitor program
|
|
76
|
+
genes detected, from one raw-count panel export. Ambient genes (LYZ, HBB, JCHAIN, Ig) are left out
|
|
77
|
+
because they are detected almost everywhere.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
in_run = run.cells.fetch_all("I").astype(bool)
|
|
81
|
+
review = run.cells.to_pandas_dataframe(["ids", "clusters", "doublet_score"]).set_index("ids")
|
|
82
|
+
review = review.assign(**{c: ds.cells.fetch_all(c)[in_run]
|
|
83
|
+
for c in ("RNA_nCounts", "RNA_percentMito")})
|
|
84
|
+
lineages = {"T": ["CD3D", "CD3E"], "B": ["MS4A1", "CD79B"], "myeloid": ["CD14", "FCN1"],
|
|
85
|
+
"plasma": ["MZB1", "TNFRSF17"]}
|
|
86
|
+
program = ["CD34", "PRSS57", "SPINK2", "CYTL1", "SOX4"] # SOX4 alone is broadly detected
|
|
87
|
+
wanted = list(dict.fromkeys(sum(lineages.values(), []) + program))
|
|
88
|
+
present = [g for g in wanted if g in set(ds.RNA.feats.fetch_all("names"))]
|
|
89
|
+
counts = ds.to_anndata(from_assay="RNA", feature_names=present) # obs_names are cell ids
|
|
90
|
+
hit = (pd.DataFrame(counts.X.toarray() > 0, index=counts.obs_names, columns=present)
|
|
91
|
+
.reindex(index=review.index, columns=wanted, fill_value=False))
|
|
92
|
+
review["n_lineages"] = sum(hit[genes].all(axis=1).astype(int) for genes in lineages.values())
|
|
93
|
+
review["progenitor"] = hit["CD34"] & (hit[program[1:]].sum(axis=1) >= 2) # coherent program
|
|
94
|
+
review["doublet_rank"] = review.groupby("clusters")["doublet_score"].rank(pct=True)
|
|
95
|
+
summary = review.groupby("clusters").agg(
|
|
96
|
+
n=("doublet_score", "size"), doublet=("doublet_score", "median"),
|
|
97
|
+
counts=("RNA_nCounts", "median"), mito=("RNA_percentMito", "median"),
|
|
98
|
+
two_lineages=("n_lineages", lambda s: (s >= 2).mean()), progenitor=("progenitor", "mean"))
|
|
99
|
+
summary["n_markers"] = (top["group_id"].value_counts()
|
|
100
|
+
.reindex(summary.index.astype(str), fill_value=0).to_numpy())
|
|
101
|
+
print(summary.round(2))
|
|
102
|
+
suspects = review[(review["n_lineages"] >= 2) & (review["doublet_rank"] >= 0.9)]
|
|
103
|
+
print(len(suspects), "cells co-detect two lineages and sit in their cluster's top doublet decile")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Read the flags together, per cluster and per cell:
|
|
107
|
+
|
|
108
|
+
- Progenitor (HSPC): a coherent program in the same cells (CD34 with PRSS57, SPINK2, CYTL1; SOX4
|
|
109
|
+
supports) and few two-lineage cells. Primed progenitors add early genes of one lineage
|
|
110
|
+
(GATA2, KLF1, CPA3) that vary between cells, not mature markers of two lineages per cell.
|
|
111
|
+
- Multiplet: mature markers of two lineages in the same cells (T with myeloid, B or plasma with
|
|
112
|
+
T or myeloid), counts or features above both parent lineages, few markers of its own and a
|
|
113
|
+
high doublet rank. Single multiplets inside large clusters show up only in `suspects`.
|
|
114
|
+
- Doublet scores: compare a cluster with clusters of the same lineage, not the global median. A
|
|
115
|
+
whole plasma cluster scoring high is expected; a plasma subset co-detecting T genes is not.
|
|
116
|
+
- Ambiguous (a program plus two-lineage cells, or high counts without co-detection): keep
|
|
117
|
+
`unresolved` and record both readings.
|
|
118
|
+
|
|
119
|
+
### Assign labels and store them
|
|
120
|
+
|
|
121
|
+
Write the evidence next to each label. The map below is a template with placeholder IDs: never
|
|
122
|
+
copy a map from another dataset. Clusters not named stay `unresolved`.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
cluster_values = run.cells.fetch("clusters").astype(str)
|
|
126
|
+
labels = { # TEMPLATE: replace "<id>" and every gene with this dataset's own evidence
|
|
127
|
+
# "<id>": "CD14 monocyte", # + CD14, LYZ, FCN1 | - CD3E, MS4A1 | confidence high
|
|
128
|
+
# "<id>": "CD4 T", # + CD3E, IL7R, CD40LG | - CD8A, NKG7 | medium: CD4 sparse
|
|
129
|
+
# "<id>": "HSPC", # + CD34, PRSS57, SPINK2 | - no two-lineage cells | medium
|
|
130
|
+
# "<id>": "unresolved", # + CD3E with CD14 in the same cells, high counts | low
|
|
131
|
+
}
|
|
132
|
+
per_cell = pd.Series(cluster_values).map(labels).fillna("unresolved").to_numpy()
|
|
133
|
+
print(pd.Series(per_cell).value_counts())
|
|
134
|
+
annotation = np.full(ds.cells.N, "not analyzed", dtype=object)
|
|
135
|
+
annotation[in_run] = per_cell
|
|
136
|
+
ds.cells.insert("annotation_v1", annotation, overwrite=True) # live column, full cell axis
|
|
137
|
+
ds.cells.insert("annotation_v1_cells", in_run, overwrite=True)
|
|
138
|
+
named = sorted(int(c) for c, name in labels.items() if name != "unresolved")
|
|
139
|
+
keep = ds.select_cells(clusters, include=named) if named else run["analysis_cell_selection"]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`select_cells(include=[])` raises, hence the guard; `keep` can seed a graph without unresolved
|
|
143
|
+
clusters. Plot evidence with `ds.plots.dotplot(features=..., group_by="annotation_v1",
|
|
144
|
+
cell_key="annotation_v1_cells")` and `ds.plots.marker_heatmap(marker=markers, topn=3)`.
|
|
145
|
+
|
|
146
|
+
### Score gene sets with AUCell and WAGGR
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
net = pd.DataFrame({ # or scarf.read_gmt("sets.gmt"): one source-target row per gene
|
|
150
|
+
"source": ["T_cell"] * 4 + ["B_cell"] * 3 + ["Monocyte"] * 4 + ["NK"] * 4,
|
|
151
|
+
"target": ["CD3D", "CD3E", "TRAC", "IL7R", "MS4A1", "CD79A", "CD79B",
|
|
152
|
+
"CD14", "LYZ", "S100A8", "FCN1", "NKG7", "GNLY", "KLRD1", "PRF1"]})
|
|
153
|
+
cells = run["analysis_cell_selection"]
|
|
154
|
+
universe = ds.select_all_features(from_assay="RNA")
|
|
155
|
+
aucell = ds.run_aucell(net, cells, features=universe, tmin=3) # one pass over counts
|
|
156
|
+
waggr = ds.run_waggr(net.assign(weight=1.0), cells, features=universe, tmin=3)
|
|
157
|
+
result = ds.get_enrichment(aucell) # lazy; sources=[...] loads a subset
|
|
158
|
+
assert result.cell_selection == cells
|
|
159
|
+
scores = pd.DataFrame(result.data.compute(), columns=list(result.source_names))
|
|
160
|
+
print(scores.groupby(run.cells.fetch("clusters")).mean().round(2))
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
AUCell ranks raw counts within each cell (0 to 1, `n_up` defaults to 5% of the universe). WAGGR
|
|
164
|
+
averages library-size-normalized expression with edge weights (unbounded, signed weights allowed).
|
|
165
|
+
|
|
166
|
+
### Read or compute cell-cycle phase
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
phase = run.cells.to_pandas_dataframe(["clusters", "cell_cycle_phase", "s_score", "g2m_score"])
|
|
170
|
+
print(pd.crosstab(phase["clusters"], phase["cell_cycle_phase"], normalize="index").round(2))
|
|
171
|
+
cycle = ds.run_cell_cycle_scoring(keep) # new artifact; one pass over counts
|
|
172
|
+
print(pd.Series(np.asarray(ds.load_artifact(cycle)["phase"][:]).astype(str)).value_counts())
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Compare with existing labels (final post-hoc evaluation only)
|
|
176
|
+
|
|
177
|
+
Run once, after labels are frozen. Read the reference column here first, write the map from its
|
|
178
|
+
vocabulary before any crosstab, and never tune labels or parameters on the result.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import hashlib
|
|
182
|
+
from sklearn.metrics import adjusted_rand_score, classification_report
|
|
183
|
+
|
|
184
|
+
ds.cells.to_pandas_dataframe(["ids", "annotation_v1"]).to_csv("labels_v1_frozen.csv", index=False)
|
|
185
|
+
print("sha256", hashlib.sha256(open("labels_v1_frozen.csv", "rb").read()).hexdigest())
|
|
186
|
+
reference = pd.Series(ds.cells.fetch_all("author_cell_type")[in_run]).astype(str) # held out
|
|
187
|
+
print(sorted(reference.unique())) # the vocabulary only; now write the map below
|
|
188
|
+
to_reference = { # TEMPLATE: our label -> reference label, fixed before seeing any agreement
|
|
189
|
+
# "CD4 T": "<reference label>",
|
|
190
|
+
}
|
|
191
|
+
ours = pd.Series(ds.cells.fetch_all("annotation_v1")[in_run]).astype(str)
|
|
192
|
+
pred = ours.map(to_reference).fillna("unmatched")
|
|
193
|
+
print(pd.crosstab(pred, reference))
|
|
194
|
+
shared = sorted(set(to_reference.values()))
|
|
195
|
+
both = (pred != "unmatched") & reference.isin(shared) # drops unresolved and unmapped classes
|
|
196
|
+
print("ARI all cells", round(adjusted_rand_score(reference, pred), 3))
|
|
197
|
+
if both.any():
|
|
198
|
+
print("ARI mapped only", round(adjusted_rand_score(reference[both], pred[both]), 3))
|
|
199
|
+
print(classification_report(reference, pred, labels=shared, zero_division=0))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Report precision and recall per class, ARI with and without ambiguous classes, and reference
|
|
203
|
+
classes with no counterpart. Disagreement is evidence: inspect large off-diagonal cells.
|
|
204
|
+
|
|
205
|
+
## Canonical markers
|
|
206
|
+
|
|
207
|
+
Confirm with dataset markers and use two or more genes. Human symbols; mouse symbols are title
|
|
208
|
+
case (`Cd3e`, `Ptprc`, `Mki67`), and panel lookups are exact, so convert the panel.
|
|
209
|
+
|
|
210
|
+
| Population | Positive markers | Notes and negatives |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| T cell | CD3D, CD3E, TRAC | MS4A1, CD14 low |
|
|
213
|
+
| CD4 T | IL7R, CD40LG, CD4 (often poorly detected) | CD8A, CD8B low; CCR7, SELL, LEF1 mark naive T |
|
|
214
|
+
| CD8 T | CD8A, CD8B | effector GZMK, GZMH, NKG7 |
|
|
215
|
+
| Treg | FOXP3, IL2RA, CTLA4 | within CD4 T |
|
|
216
|
+
| MAIT, gamma-delta T | SLC4A10, KLRB1 (MAIT); TRDC, TRGC1 (gamma-delta) | often within CD8 or effector T |
|
|
217
|
+
| NK | NKG7, GNLY, KLRD1, KLRF1, NCAM1 | CD3E low; NKG7, GNLY also in CD8 effector T |
|
|
218
|
+
| B cell | MS4A1, CD79A, CD19 | naive IGHD, TCL1A |
|
|
219
|
+
| Plasma cell, plasmablast | MZB1, JCHAIN, XBP1, TNFRSF17 | MS4A1 low; plasmablasts express MKI67 |
|
|
220
|
+
| CD14 monocyte | CD14, LYZ, S100A8, S100A9, FCN1 | |
|
|
221
|
+
| CD16 monocyte | FCGR3A, MS4A7, CDKN1C | FCGR3A also on NK |
|
|
222
|
+
| Conventional DC | FCER1A, CD1C, CLEC10A; CLEC9A (cDC1) | FCER1A also on basophils |
|
|
223
|
+
| Plasmacytoid DC | LILRA4, IL3RA, CLEC4C | GZMB shared with NK; MZB1 also high |
|
|
224
|
+
| Basophil, mast cell | CPA3, HDC, MS4A2; TPSAB1, KIT (mast) | |
|
|
225
|
+
| Neutrophil | CSF3R, FCGR3B, CXCR2 | low counts; rare after density gradients |
|
|
226
|
+
| Platelet, megakaryocyte | PPBP, PF4 | often attached to monocytes |
|
|
227
|
+
| Erythroid | HBB, HBA1 | frequent ambient contamination |
|
|
228
|
+
| HSPC | CD34, PRSS57, SPINK2, CYTL1, SOX4 | rare in blood; primed subsets add GATA2, KLF1 or CPA3 |
|
|
229
|
+
| Cycling (a state) | MKI67, TOP2A | label as a state, not a type |
|
|
230
|
+
| Immune, macrophage | PTPRC; C1QA, CD68 (macrophage) | |
|
|
231
|
+
| Epithelium, gut | EPCAM, KRT8, KRT18; LGR5, OLFM4 (stem) | goblet MUC2; colonocyte CA1 |
|
|
232
|
+
| Fibroblast | PDGFRA, COL1A1, DCN | subtypes vary by tissue, e.g. APOD or CCN2 (CTGF) high; `^CCN` is in the default HVG blacklist, markers still test it |
|
|
233
|
+
| Endothelium | PECAM1, VWF, CDH5 | |
|
|
234
|
+
| Smooth muscle, pericyte | ACTA2, MYH11, DES; RGS5 (pericyte) | ACTA2 also on myofibroblasts |
|
|
235
|
+
| Keratinocyte, melanocyte | KRT5, KRT14 (basal), KRT10 (suprabasal); PMEL, MLANA (melanocyte) | |
|
|
236
|
+
| Neuron | RBFOX3, SNAP25; SLC17A7 (excitatory); GAD1, GAD2 (inhibitory) | |
|
|
237
|
+
| Astrocyte, oligodendrocyte | GFAP, AQP4; MBP, PLP1, MOG (oligodendrocyte) | |
|
|
238
|
+
| OPC, microglia | PDGFRA, CSPG4 (OPC); CSF1R, P2RY12 (microglia) | CSF1R also on macrophages |
|
|
239
|
+
|
|
240
|
+
## Parameters that matter
|
|
241
|
+
|
|
242
|
+
| Parameter | Default | Change when |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `min_score` (`get_markers`) | `0.25` | `-1` to see negatives and weak genes; raise for a short list |
|
|
245
|
+
| `min_frac_exp` (`get_markers`) | `0.2` | `-1` with `min_score=-1` for complete panel tables |
|
|
246
|
+
| `features` (`run_marker_search`) | required | `run["feature_universe"]` tests every gene; an HVG ref limits the search |
|
|
247
|
+
| `tmin` (AUCell, WAGGR) | `5` | Lower for short sets; sources with fewer matched targets are dropped |
|
|
248
|
+
| `n_up` (AUCell) | `None` (5% of universe) | Fix it when comparing runs; changes the score scale |
|
|
249
|
+
| `s_genes` / `g2m_genes` | built-in human and mouse lists | Other species or naming schemes |
|
|
250
|
+
|
|
251
|
+
## Check before moving on
|
|
252
|
+
|
|
253
|
+
- Every named cluster has at least two positive markers with high `frac_exp` and `score` well above
|
|
254
|
+
1/n_groups, and lineage negatives with low `frac_exp`. The evidence is written next to the label.
|
|
255
|
+
Few markers usually means a split within one lineage: merge, or name a state only with evidence.
|
|
256
|
+
- Clusters with two lineages' markers in the same cells, counts above both parents or a high
|
|
257
|
+
within-lineage doublet rank are `unresolved`. A CD34 program without co-detection is HSPC.
|
|
258
|
+
- Rare types hide in big clusters; check finer resolutions or subcluster the lineage.
|
|
259
|
+
- The label map, marker ref and run ID are recorded. Held-out labels were not read before this.
|
|
260
|
+
|
|
261
|
+
## Pitfalls
|
|
262
|
+
|
|
263
|
+
- Default `get_markers` filters hide negative markers. `group_id` is a string while Leiden labels
|
|
264
|
+
are integers; convert before joining.
|
|
265
|
+
- Direct calls do not reuse pipeline outputs: `run_marker_search(run["clusters"], ...)` and
|
|
266
|
+
`run_cell_cycle_scoring(...)` write new artifacts and reread counts. Read the run's refs instead.
|
|
267
|
+
- Ambient genes (HBB, LYZ, S100A8, JCHAIN, immunoglobulins) show nonzero `frac_exp` in most
|
|
268
|
+
clusters. Use `score` and `auc`, not detection alone, and leave them out of co-detection checks.
|
|
269
|
+
- Small groups inflate `fold_change` and can pass `min_score` with odd genes; check group size.
|
|
270
|
+
- `ds.cells.insert(..., overwrite=True)` silently replaces a column; use versioned names.
|
|
271
|
+
- `select_cells(include=...)` needs integers for integer cluster artifacts (`TypeError` otherwise).
|
|
272
|
+
- Scores from different feature universes, `n_up` or WAGGR `mode`/`log_transform` are not comparable.
|
|
273
|
+
|
|
274
|
+
## See also
|
|
275
|
+
|
|
276
|
+
- `clustering-and-embedding.md`: choosing the partition and aligning artifacts to cell IDs.
|
|
277
|
+
- `features-and-graphs.md`: subclustering one lineage before naming its states.
|
|
278
|
+
- `quality-control.md`: doublet scores and QC covariates behind unresolved clusters.
|
|
279
|
+
- `plotting.md` (`dotplot`, `marker_heatmap`); `integration-and-comparisons.md` (label transfer,
|
|
280
|
+
condition comparisons); `pipeline-runs-and-artifacts.md` (run fields, lineage).
|