Scala, Kobak et al. (2021): sequencing claims, on the paper's count tables¶
Paper. Scala F., Kobak D. et al. (2021) Phenotypic variation of transcriptomic cell types in mouse motor cortex. Nature 598:144-150. doi:10.1038/s41586-020-2907-3
Dataset. DANDI:000008, version 0.211014.0809
(the latest published version; the paper cites none). The dandiset holds the recordings only. The claims
here are marked data: elsewhere: the paper deposited the reads at NeMO and GEO (GSE163764) and the count
tables in its code repository (mini-atlas at commit 3bf98a524b8e48aa8647e1b9ae8f2351d9c712c9, data/), and
the reference data it maps to are its preprocessed Allen Institute data (Zenodo record 5118962,
doi:10.5281/zenodo.5118962). The tests run on those count tables, which the claims file names under
where; the cells are linked to the dandiset's files by cell id.
Text the claims were written from. The version of record (Europe PMC PMC8113357).
| Claim | Tested here |
|---|---|
| rnaseq-depth | yes |
| ttype-qc-counts | yes |
| ed2-smartseq-similarity | yes |
import os
os.environ.setdefault("OMP_NUM_THREADS", "1")
import re
import time
import numpy as np
import pandas as pd
from IPython.display import display
import scala
T0 = time.time()
print("dandiset", scala.DANDISET, "version", scala.VERSION)
assets = scala.list_assets()
ids = [re.search(r"_cell-(.+)_icephys", p).group(1).replace("-", "_") for p in assets.path]
ex, it, gl = scala.patchseq_counts()
tab = scala.paper_meta_table()
rnaseqTools = scala.paper_module("rnaseqTools")
dandiset 000008 version 0.211014.0809
print("count tables: genes x cells", ex.shape, "| gene lengths:", gl.shape)
print("dandiset files:", len(ids), "| their cell ids in the count tables:", int(np.isin(ids, ex.columns).sum()),
"| count-table cells without a dandiset file:", sorted(set(ex.columns) - set(ids)))
print("count values: exon", int(ex.values.min()), "to", int(ex.values.max()), "| intron", int(it.values.min()), "to", int(it.values.max()))
det_tab = ((ex.values + it.values) > 0).sum(axis=0)
print("detected genes per cell equal to the table's 'Number of genes detected':",
int((det_tab == pd.to_numeric(tab["Number of genes detected"]).reindex(ex.columns).values).sum()), "of", ex.shape[1])
for kind, cnt, col in [("exon", ex, "Exon reads"), ("intron", it, "Intron reads")]:
ratio = cnt.sum(axis=0) / pd.to_numeric(tab[col]).reindex(cnt.columns)
print(f"{kind} column sums over the table's '{col}': equal for {int(np.isclose(ratio, 1).sum())} cells; ratio median {ratio.median():.3f}, "
f"5th to 95th percentile {ratio.quantile(0.05):.3f} to {ratio.quantile(0.95):.3f}")
print("cells in the paper's table order:", bool((ex.columns == tab.index).all()))
count tables: genes x cells (42466, 1329) | gene lengths: (42466, 4) dandiset files: 1328 | their cell ids in the count tables: 1328 | count-table cells without a dandiset file: ['20190211_sample_10']
count values: exon 0 to 1536853 | intron 0 to 253008
detected genes per cell equal to the table's 'Number of genes detected': 1329 of 1329 exon column sums over the table's 'Exon reads': equal for 14 cells; ratio median 1.035, 5th to 95th percentile 0.384 to 3.078 intron column sums over the table's 'Intron reads': equal for 4 cells; ratio median 1.000, 5th to 95th percentile 0.090 to 10.813 cells in the paper's table order: True
The number of detected genes from the count tables equals the paper's table, whose per-cell fields the
link claim in scala_2021_ephys.ipynb shows equal to the dandiset's (number_of_genes_detected,
exon_reads, intron_reads). The read fields are not the column sums of the count tables: the exon sums
over Exon reads have a median ratio of 1.035 but range from 0.384 to 3.078 (5th to 95th percentile,
printed above). Both are reported below.
rnaseq-depth¶
The 1,329 sequenced cells had median 1.3 million exonic and 0.7 million intronic reads (log10 mean 6.0 and 5.6) and 9,100 detected genes on average (s.d. 3,500).
E = ex.values.T.astype(float)
I = it.values.T.astype(float)
ed, idp = E.sum(axis=1), I.sum(axis=1)
det = ((E + I) > 0).sum(axis=1)
print(f"cells: {E.shape[0]}")
print(f"median exonic reads {np.median(ed):,.0f} (log10 mean {np.mean(np.log10(ed)):.2f} +- {np.std(np.log10(ed)):.2f}); "
f"median intronic reads {np.median(idp):,.0f} (log10 mean {np.mean(np.log10(idp)):.2f} +- {np.std(np.log10(idp)):.2f})")
print(f"detected genes (non-zero exonic or intronic count): mean {det.mean():,.0f}, s.d. {det.std():,.0f}; range {det.min()} to {det.max()}")
d_ids = np.isin(ex.columns, ids)
print(f"the 1,328 cells with a dandiset file: median exonic {np.median(ed[d_ids]):,.0f}, intronic {np.median(idp[d_ids]):,.0f}, "
f"mean detected genes {det[d_ids].mean():,.0f}")
er, ir = pd.to_numeric(tab["Exon reads"]), pd.to_numeric(tab["Intron reads"])
print(f"variant, the read fields of the paper's table (equal to the dandiset's fields): median exonic {er.median():,.0f} "
f"(log10 mean {np.mean(np.log10(er)):.2f}), median intronic {ir.median():,.0f} (log10 mean {np.mean(np.log10(ir)):.2f}); "
f"over the 1,328 dandiset cells {er[ex.columns[d_ids]].median():,.0f} and {ir[ex.columns[d_ids]].median():,.0f}")
cells: 1329 median exonic reads 1,281,492 (log10 mean 5.99 +- 0.59); median intronic reads 687,965 (log10 mean 5.61 +- 0.76) detected genes (non-zero exonic or intronic count): mean 9,097, s.d. 3,533; range 11 to 24833 the 1,328 cells with a dandiset file: median exonic 1,280,301, intronic 689,454, mean detected genes 9,097 variant, the read fields of the paper's table (equal to the dandiset's fields): median exonic 1,190,890 (log10 mean 5.96), median intronic 688,140 (log10 mean 5.61); over the 1,328 dandiset cells 1,190,524 and 688,002
Result: reproduced. Over the 1,329 sequenced cells of the paper's count tables, the median exonic count is 1,281,492 and the median intronic 687,965 (log10 means 5.99 and 5.61), and cells have 9,097 detected genes on average (s.d. 3,533): all within 10% of 1.3 million, 0.7 million and 9,100. The read fields of the paper's table, which equal the dandiset's, give a lower exonic median, 1,190,890, also within 10%, and an intronic median of 688,140.
ttype-qc-counts¶
In the first round of mapping, 78 cells had a highest correlation below 0.4, 14 mapped to non-neural types and 5 were ambiguous between large groups, leaving 1,232 cells; four with excitatory RNA but inhibitory phenotype and one outside MOp were excluded, leaving 1,227.
The first round of the code's consensus_assignment (ttype-assignment.ipynb), with its own
rnaseqTools.map_to_clusters, on the Tasic et al. (2018) reference of the Zenodo record.
tasic = scala.reference("tasic2018")
print("Tasic 2018 reference: cells x genes", tasic["counts"].shape, "| clusters:", len(tasic["clusterNames"]))
_, C = rnaseqTools.map_to_clusters(tasic["counts"], tasic["genes"], E, ex.index.values, tasic["clusters"], returnCmeans=True,
newIntronCounts=I, referenceIntronCounts=tasic["intronCounts"],
newExonLengths=gl.exon_bp.values, newIntronLengths=gl.intron_bp.values,
normalizeNew=True, normalizeReference=True)
C[np.isnan(C)] = 0
lowq = np.amax(C, axis=1) < .4
n1 = int(lowq.sum())
e2 = ~lowq & (np.amax(C[:, :117], axis=1) < .4); lowq |= e2
e3 = ~lowq & np.isin(np.argmax(C, axis=1), [115, 116]); lowq |= e3
borders = [0, 29, 60, 115]
maxorder = np.stack([np.amax(C[:, borders[i]:borders[i + 1]], axis=1) for i in range(3)], axis=1)
delta = np.diff(np.sort(maxorder, axis=1)[:, -2:], axis=1).squeeze()
e4 = ~lowq & (delta < 0.02); lowq |= e4
print(f"highest correlation below 0.4: {n1}; highest neural correlation below 0.4 (non-neural): {int(e2.sum())}; "
f"mapping to Meis2 or CR: {int(e3.sum())}; groups less than 0.02 apart: {int(e4.sum())}; passing: {int((~lowq).sum())}")
excl = tab["Exclusion reasons"].notna().values
print("of the passing cells, with an exclusion reason in the paper's table:", int((~lowq & excl).sum()),
"| passing and not excluded:", int((~lowq & ~excl).sum()))
same = (lowq == (tab["RNA family"].values == "low quality"))
print("cells whose pass or fail agrees with the paper's table ('low quality' family):", int(same.sum()), "of", len(same))
order = np.argmax(maxorder, axis=1)
order[lowq] = -1
Tasic 2018 reference: cells x genes (23822, 3000) | clusters: 133 Using a common set of 2666 genes.
highest correlation below 0.4: 78; highest neural correlation below 0.4 (non-neural): 14; mapping to Meis2 or CR: 0; groups less than 0.02 apart: 5; passing: 1232
of the passing cells, with an exclusion reason in the paper's table: 5 | passing and not excluded: 1227
cells whose pass or fail agrees with the paper's table ('low quality' family): 1329 of 1329
Result: reproduced. The first round of the code's mapping to the Tasic et al. (2018) reference gives 78 cells with a highest correlation below 0.4, 14 more with a highest neural correlation below 0.4, none mapping to Meis2 or CR, and 5 less than 0.02 apart between large groups, leaving 1,232: the paper's four numbers exactly. The pass or fail of every cell agrees with the paper's table, and the 1,232 include the 5 cells the paper then excludes, leaving 1,227.
ed2-smartseq-similarity¶
Patch-seq expression profiles are most similar to the single-nucleus Smart-seq2 reference, and t-type assignments based on Smart-seq2 and 10x references agree well.
Second round of the code's assignment, without the bootstrap: each cell passing quality control is mapped, within its large group from the first round, to each of the seven references; the maximal correlation and the assigned t-type are kept.
sets = ["10X_cells_v2_AIBS", "10X_nuclei_v2_AIBS", "10X_cells_v3_AIBS", "10X_nuclei_v3_AIBS",
"SmartSeq_cells_AIBS", "SmartSeq_nuclei_AIBS", "10X_nuclei_v3_Broad"]
n = E.shape[0]
maxc, assigned = {}, {}
for ds in sets:
m = scala.reference(ds)
K = m["neurons"]["clusterNames"].size
mc, aa = np.full(n, np.nan), np.full(n, np.nan)
for o, key in enumerate(["viplamp", "pvsst", "exc"]):
s = order == o
a_, Cm = rnaseqTools.map_to_clusters(m[key]["counts"], m[key]["genes"], E[s], ex.index.values, m[key]["clusters"],
returnCmeans=True, totalClusters=K, newIntronCounts=I[s],
referenceIntronCounts=m[key]["intronCounts"] if "SmartSeq" in ds else None,
newExonLengths=gl.exon_bp.values, newIntronLengths=gl.intron_bp.values,
normalizeNew=True, normalizeReference="SmartSeq" in ds)
mc[s] = np.nanmax(Cm, axis=1)
aa[s] = a_
maxc[ds], assigned[ds] = mc, aa
del m
mx = pd.DataFrame(maxc)
summary = pd.DataFrame({"median max correlation": mx.median(), "mean": mx.mean(), "cells": mx.notna().sum()}).sort_values("median max correlation", ascending=False)
display(summary.round(4))
a, b = assigned["SmartSeq_cells_AIBS"], assigned["10X_cells_v2_AIBS"]
ok = ~np.isnan(a) & ~np.isnan(b)
print(f"highest median: {summary.index[0]}; single-cell Smart-seq2 and single-cell 10x v2 assign the same t-type for "
f"{np.mean(a[ok] == b[ok]):.3f} of {int(ok.sum())} cells")
better = (mx["SmartSeq_nuclei_AIBS"].values[:, None] > mx.drop(columns="SmartSeq_nuclei_AIBS").values).all(axis=1)
print(f"cells whose highest correlation is with the single-nucleus Smart-seq2 reference: {np.mean(better[ok]):.3f}")
print(f"by group: " + ", ".join(f"{g}: {np.nanmedian(mx['SmartSeq_nuclei_AIBS'].values[order == o]):.3f} (snSS) vs "
f"{np.nanmedian(mx['SmartSeq_cells_AIBS'].values[order == o]):.3f} (scSS)"
for o, g in enumerate(["CGE", "MGE", "excitatory"])))
Using a common set of 490 genes.
Using a common set of 493 genes.
Using a common set of 478 genes.
Using a common set of 483 genes.
Using a common set of 484 genes.
Using a common set of 472 genes.
Using a common set of 491 genes.
Using a common set of 492 genes.
Using a common set of 486 genes.
Using a common set of 488 genes.
Using a common set of 490 genes.
Using a common set of 486 genes.
Using a common set of 421 genes.
Using a common set of 445 genes.
Using a common set of 454 genes.
Using a common set of 460 genes.
Using a common set of 474 genes.
Using a common set of 465 genes.
Using a common set of 487 genes.
Using a common set of 494 genes.
Using a common set of 483 genes.
| median max correlation | mean | cells | |
|---|---|---|---|
| SmartSeq_nuclei_AIBS | 0.7031 | 0.6892 | 1232 |
| 10X_cells_v2_AIBS | 0.6230 | 0.5861 | 1232 |
| SmartSeq_cells_AIBS | 0.6210 | 0.6181 | 1232 |
| 10X_cells_v3_AIBS | 0.5779 | 0.5357 | 1232 |
| 10X_nuclei_v3_Broad | 0.5562 | 0.5133 | 1232 |
| 10X_nuclei_v2_AIBS | 0.5297 | 0.4887 | 1232 |
| 10X_nuclei_v3_AIBS | 0.4969 | 0.4554 | 1232 |
highest median: SmartSeq_nuclei_AIBS; single-cell Smart-seq2 and single-cell 10x v2 assign the same t-type for 0.786 of 1232 cells cells whose highest correlation is with the single-nucleus Smart-seq2 reference: 0.757 by group: CGE: 0.692 (snSS) vs 0.671 (scSS), MGE: 0.714 (snSS) vs 0.608 (scSS), excitatory: 0.689 (snSS) vs 0.613 (scSS)
Result: reproduced. Over the 1,232 cells passing quality control, the median of the maximal correlation is highest for the single-nucleus Smart-seq2 reference (0.703; next single-cell 10x v2, 0.623, and single-cell Smart-seq2, 0.621), and 75.7% of cells correlate best with it. The single-cell Smart-seq2 and single-cell 10x v2 references assign the same t-type for 78.6% of cells, a majority.
Summary¶
| Claim | Outcome |
|---|---|
| rnaseq-depth | reproduced |
| ttype-qc-counts | reproduced |
| ed2-smartseq-similarity | reproduced |
These claims rest on data the paper deposited outside the dandiset (data: elsewhere): the count tables of
its code repository and its preprocessed reference data on Zenodo. They are counted apart from the claims
about the dandiset.
How the analysis differs from the paper's. The second round of the t-type assignment is run without the bootstrap over genes, which the claim does not need: the maximal correlations and the direct assignments are compared. The reference names are brought up to date as the code does.
What was found about the dataset. The count tables' cells match the dandiset's files by cell id (all 1,328, and the paper's one extra cell). The read fields of the paper's table, which the dandiset's files repeat, are not the column sums of the count tables; the numbers of detected genes are equal.
print(f"run time: {(time.time() - T0) / 60:.1f} min")
run time: 4.6 min