Find Marker Genes Using Effect Size

1suppressMessages({
2  library(scRNAseq)
3  library(scater)
4  library(scran)
5  library(org.Mm.eg.db)
6  library(VennDiagram)
7  library(wesanderson)
8})

It has been a while since I wrote my last post, for today I decided to focus on something that has been in mind for a long time but never found the time of properly testing, marker gene selection. Cluster marker genes can be defined as genes that drive separation between clusters and allow us to assign biological meaning to each cluster. The most immediate approach is to perform differential gene expression between clusters. Strong DEGs are more likely to be driving the separation of the clusters.

Like many aspects of single cell analysis, things are not as easy as they may seem. Most DE analysis methods treat cells as independent observations, however this is not really accurate when cells are derived from the same biological sample (i.e., cell culture, animal or patient) and therefore the sample-to-sample variability is not properly addressed.

This point becomes more relevant when you think about the typical single cell workflow: cluster cells based on gene expression profiles and then find markers genes to identify/annotate those clusters. This is known as the circularity issue / double deeping / data dredging / fishing / data snooping:

  • Make hypotheses based on your data.
  • Test them on the same data
  • Statistical tests cannot properly control Type I error rate on these situations

Many tutorials and packages perform marker gene selection using classical statistical test like Wilcoxon or t-test. The common assumption is that ranking genes by the P-value, although inflated, is a good enough approach to find marker genes.

The OSCA book chapter on marker gene detection proposes a different approach based on effect size that I think is better. The idea is ranking the genes by a measure of the expression difference, fold change would be an example, to select as marker genes the ones with the biggest difference in expression, instead of the most significant ones.

To test this idea, we will use a couple of datasets from the scRNAseq bioconductor package with annotated cell types. Cell types will be used as a proxy of cell cluster allowing us to check the quality of the selected markers.

Zeisel dataset

  • ~3,000 cells, mouse brain (cortex + hippocampus)
  • Comes with a level1class annotation (7 broad cell types: pyramidal neurons, interneurons, microglia, oligodendrocytes, astrocytes, endothelial, ependymal)
1sce.zeisel <- ZeiselBrainData()

check data

1sce.zeisel
 1## class: SingleCellExperiment 
 2## dim: 20006 3005 
 3## metadata(0):
 4## assays(1): counts
 5## rownames(20006): Tspan12 Tshz1 ... mt-Rnr1 mt-Nd4l
 6## rowData names(1): featureType
 7## colnames(3005): 1772071015_C02 1772071017_G12 ... 1772066098_A12
 8##   1772058148_F03
 9## colData names(9): tissue group # ... level1class level2class
10## reducedDimNames(0):
11## mainExpName: gene
12## altExpNames(2): repeat ERCC

Follow processing as described in OSCA book

1sce.zeisel <- aggregateAcrossFeatures(sce.zeisel, 
2                                      id=sub("_loc[0-9]+$", "", rownames(sce.zeisel)))
3rowData(sce.zeisel)$Ensembl <- mapIds(org.Mm.eg.db, 
4                                      keys=rownames(sce.zeisel), keytype="SYMBOL", column="ENSEMBL")
1## 'select()' returned 1:many mapping between keys and columns
 1stats <- perCellQCMetrics(sce.zeisel, subsets=list(
 2    Mt=rowData(sce.zeisel)$featureType=="mito"))
 3qc <- quickPerCellQC(stats, percent_subsets=c("altexps_ERCC_percent", 
 4    "subsets_Mt_percent"))
 5sce.zeisel <- sce.zeisel[,!qc$discard]
 6set.seed(1000)
 7clusters <- quickCluster(sce.zeisel)
 8sce.zeisel <- computeSumFactors(sce.zeisel, cluster=clusters) 
 9sce.zeisel <- logNormCounts(sce.zeisel)
10dec.zeisel <- modelGeneVarWithSpikes(sce.zeisel, "ERCC")
11top.hvgs <- getTopHVGs(dec.zeisel, prop=0.1)
12
13sce.zeisel <- denoisePCA(sce.zeisel, technical=dec.zeisel, subset.row=top.hvgs)
14sce.zeisel <- runTSNE(sce.zeisel, dimred="PCA")
15sce.zeisel <- runUMAP(sce.zeisel, dimred="PCA")
1plotTSNE( sce.zeisel, colour_by = 'level1class' )
1plotUMAP( sce.zeisel, colour_by = 'level1class' )

P-value

We will use the findMarkers function from scran package (very similar to the seurat's function with the same name, or rank_genes_groups from scanpy. It defaults to a t-test, use the test.type argument to change it:

  • "t", defaults to a Welch t-tests
  • "wilcox", pairwise Wilcoxon rank sum test
  • "binom", pairwise binomial tests

it works by comparing each cluster/cell type/ group of cells to every other independently and summarizing the results as follows

pval.type Question being asked Best for
any (default) Does this gene separate the cluster from some other cluster? Marker combinations, general clustering QC
some Does this gene separate the cluster from most other clusters? Compromise, robust to one similar neighboring cluster
all Does this gene separate the cluster from every other cluster? Canonical, cluster-exclusive markers; best with well-separated, non-redundant clusters (like Zeisel's 7 broad types)

We use pairwise comparisons rather than comparing each cell type to all other cells because the latter approach is sensitive to the population composition while pairwise comparisons naturally provide more information to interpret cell population structure.

all

We start with this option which will find genes completely specific to the selected cell type

1markers <- findMarkers( sce.zeisel, groups = sce.zeisel$level1class, direction="up", pval.type = 'all' )
2names(markers)
1## [1] "astrocytes_ependymal" "endothelial-mural"    "interneurons"        
2## [4] "microglia"            "oligodendrocytes"     "pyramidal CA1"       
3## [7] "pyramidal SS"

we will focus on the astrocytes

1marker.set <- markers[["astrocytes_ependymal"]]
2head(marker.set)
 1## DataFrame with 6 rows and 9 columns
 2##            p.value         FDR summary.logFC logFC.endothelial-mural
 3##          <numeric>   <numeric>     <numeric>               <numeric>
 4## Aqp4   9.44070e-65 1.87294e-60       4.41445                 4.44859
 5## Cldn10 4.50175e-59 4.46552e-55       3.31739                 3.29126
 6## Clu    3.59649e-53 2.37836e-49       5.29763                 5.20606
 7## Ntsr2  1.45913e-51 7.23694e-48       3.19148                 3.25929
 8## Aldoc  3.37659e-50 1.33976e-46       3.89718                 4.06435
 9## Mt2    1.69515e-49 5.60501e-46       4.09232                 3.73900
10##        logFC.interneurons logFC.microglia logFC.oligodendrocytes
11##                 <numeric>       <numeric>              <numeric>
12## Aqp4              4.66310         4.41445                4.42523
13## Cldn10            3.44648         3.31739                3.24379
14## Clu               3.60647         5.29763                5.26771
15## Ntsr2             3.42934         3.19148                3.23559
16## Aldoc             3.88556         3.89718                4.07250
17## Mt2               4.66218         4.09232                4.31965
18##        logFC.pyramidal CA1 logFC.pyramidal SS
19##                  <numeric>          <numeric>
20## Aqp4               4.58979            4.62308
21## Cldn10             3.20419            3.37479
22## Clu                4.83699            4.13796
23## Ntsr2              3.31741            3.39548
24## Aldoc              4.29262            4.17278
25## Mt2                4.64615            4.73419

and use the top 25 genes

1top.markers <- rownames(marker.set)[1:25]
2plotHeatmap( sce.zeisel, 
3             features=top.markers, 
4             order_columns_by="level1class", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.zeisel, 
2                    features=top.markers, 
3                    group="level1class", 
4                    center=TRUE
5                  )

Most are strong, literature-validated astrocyte markers:

  • Aqp4: aquaporin-4, the textbook pan-astrocyte marker, especially enriched at perivascular endfeet
  • Slc1a3 (GLAST): classic astrocytic glutamate transporter, used in countless astrocyte-labeling mouse lines
  • Gja1 (Connexin-43): canonical astrocyte gap-junction protein
  • Atp1b2 (AMOG): long-established astrocyte-specific Na+/K+-ATPase subunit
  • Slc4a4 (NBCe1): canonical astrocyte bicarbonate transporter
  • Ndrg2: specifically expressed in astrocytes of the brain and regarded as a novel, reliable astrocytic marker
  • Fabp7 (BLBP): classic radial glia/astrocyte marker
  • Mlc1, Gpr37l1, Ptprz1, Bcan, Fgfr3: repeatedly reported as astrocyte- or astrocyte/OPC-enriched genes in cortical scRNA-seq atlases
  • Ntsr2, Aldoc, Htra1, Clu, Cldn10, Prdx6, Acsbg1, Lcat: consistently top astrocyte-enriched genes in mouse brain scRNA-seq references (including this Zeisel dataset's own original marker panel)

Worth a second look genes

  • Mt2 (metallothionein 2): a general stress-response/metal-binding gene; enriched in astrocytes but less specific, since it can be induced in reactive states across cell types
  • Scg3 (secretogranin III): stands out; typically associated with neuroendocrine secretory granules rather than astrocyte biology. Could be a real but less-documented signal, low-level ambient/background contamination in droplet-based data, or an artifact of one particularly distinctive pairwise comparison driving its some-type combined significance.
  • Tmem47: reported in some astrocyte transcriptomic studies but far less characterized than the others; plausible but not a "textbook" marker

some

We try now this option which will find genes enriched but possible not completely specific to the selected cell type

1markers <- findMarkers( sce.zeisel, groups = sce.zeisel$level1class, direction="up", pval.type = 'some' )
2names(markers)
1## [1] "astrocytes_ependymal" "endothelial-mural"    "interneurons"        
2## [4] "microglia"            "oligodendrocytes"     "pyramidal CA1"       
3## [7] "pyramidal SS"
1marker.set <- markers[["astrocytes_ependymal"]]
2head(marker.set)
 1## DataFrame with 6 rows and 9 columns
 2##             p.value          FDR summary.logFC logFC.endothelial-mural
 3##           <numeric>    <numeric>     <numeric>               <numeric>
 4## Apoe   8.97746e-129 1.78104e-124       5.36624                 5.34228
 5## Slc1a3 2.06515e-114 2.04852e-110       5.58194                 5.05382
 6## Mt1    3.02832e-108 2.00262e-104       5.45505                 4.20940
 7## Clu    4.11625e-107 2.04156e-103       5.20606                 5.20606
 8## Gja1   6.65536e-103  2.64072e-99       4.29960                 3.90276
 9## Atp1a2 1.74957e-102  5.78495e-99       6.04912                 3.33541
10##        logFC.interneurons logFC.microglia logFC.oligodendrocytes
11##                 <numeric>       <numeric>              <numeric>
12## Apoe              6.30921        0.340815                5.36624
13## Slc1a3            5.71075        4.781766                4.58897
14## Mt1               5.30828        3.585627                3.79059
15## Clu               3.60647        5.297635                5.26771
16## Gja1              4.70872        3.960957                4.29960
17## Atp1a2            6.31122        4.569073                4.91068
18##        logFC.pyramidal CA1 logFC.pyramidal SS
19##                  <numeric>          <numeric>
20## Apoe               6.05976            6.14838
21## Slc1a3             5.58194            5.62350
22## Mt1                5.45505            5.44808
23## Clu                4.83699            4.13796
24## Gja1               4.56702            4.62901
25## Atp1a2             6.04912            5.70395
1top.markers.some <- rownames(marker.set)[1:25]
2plotHeatmap( sce.zeisel, 
3             features=top.markers.some, 
4             order_columns_by="level1class", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.zeisel, 
2                    features=top.markers.some, 
3                    group="level1class", 
4                    center=TRUE
5                  )

Most are strong, literature-validated astrocyte markers, including several top-tier "textbook" genes not seen in the all-type list:

  • Apoe: the major lipid/cholesterol transport protein secreted by astrocytes in the CNS; one of the most-cited astrocyte identity genes in the field
  • Slc1a3 (GLAST), Slc1a2 (GLT-1/EAAT2): the two primary astrocytic glutamate transporters; Slc1a2 is arguably even more central to astrocyte identity than Slc1a3
  • Gja1 (Connexin-43): canonical astrocyte gap-junction protein
  • Atp1a2: canonical astrocyte-specific Na+/K+-ATPase alpha subunit
  • Sparcl1 (Hevin): canonical astrocyte-secreted synaptogenic factor
  • Glul: glutamine synthetase, arguably the textbook astrocyte enzyme, central to the glutamate-glutamine cycle
  • Aqp4, Slc4a4, Ndrg2, Ptprz1, Ntsr2, Aldoc, Gpr37l1, Bcan, Prdx6, Ppap2b: repeatedly reported as astrocyte-enriched genes in cortical scRNA-seq atlases

Worth a second look because they are less classic:

  • Mt1, Mt2 (metallothioneins): general stress-response/metal-binding genes; enriched in astrocytes but less specific, inducible in reactive states across cell types
  • Pla2g7 (Lp-PLA2/PAFAH): more strongly associated in much of the literature with monocyte/macrophage/microglial biology than astrocytes; could reflect a genuine subpopulation, ambient RNA from nearby microglia, or a doublet effect
  • Cst3 (cystatin C): fairly broadly expressed across glial and even non-neural tissue, so less specific than the top-tier genes above
  • Sepp1 (Selenoprotein P): known astrocyte-secreted antioxidant protein; well documented but less famous than Glul
  • Scd2, Gstm1: plausible astrocyte-enriched metabolic genes, fit the recurring lipid-metabolism theme, but less "textbook" than the top markers

Cohen's D

We now move to an analysis not based on P-values using the scoreMarkers function from scran package. To the best of my knowledge there is no equivalent in seurat or scanpy but you can try scranpy in Python (check my post on BiocPy: Bioconductor in Python).

As before, we will compare each cell type to each other independently but, instead of performing a statistical test it calculates several scores to quantify the differences in the expression distributions. Next, we will rank candidate markers based on one of these effect size summaries.

There are two main scores:

  • The area under the curve (AUC) quantifies our ability to distinguish between two distributions in a pairwise comparison. The AUC represents the probability that a randomly chosen observation from the cell type of interest is greater than a randomly chosen observation from the other cell type.
  • Cohen’s d is a standardized log-fold change that measures effect size where the difference in the mean log-expression between groups is scaled by the average standard deviation across groups. Same idea as the log-fold change but log-fold change tells you the magnitude of the difference while effect size tells you the reliability/separability of the difference. A good marker gene ideally has both, large mean difference and non-overlapping distributions.

Following calculation, the pairwise scores are aggregated using typical summary statistics like the mean, median or maximum. We will focus on the following two:

  • The median effect size, a large positive value indicates that the gene is upregulated in the cell type of interest compared to the average of the other ones. The median provides greater robustness to outliers than the mean.
  • The minimum value is the most stringent summary for identifying upregulated genes, as a large value indicates that the gene is upregulated in the cell type of interest compared to all the other ones.

median

We start with the mean that evaluates differences to most but not all cell types in the dataset.

1marker.info <- scoreMarkers( sce.zeisel, groups = sce.zeisel$level1class )
2names(marker.info)
1## [1] "astrocytes_ependymal" "endothelial-mural"    "interneurons"        
2## [4] "microglia"            "oligodendrocytes"     "pyramidal CA1"       
3## [7] "pyramidal SS"
1chosen <- marker.info[["astrocytes_ependymal"]]
2ordered <- chosen[order(chosen$median.logFC.cohen,decreasing=TRUE),]
3head(ordered)
 1## DataFrame with 6 rows and 19 columns
 2##         self.average other.average self.detected other.detected
 3##            <numeric>     <numeric>     <numeric>      <numeric>
 4## Slc1a3       5.97823      0.754770      0.983240       0.265159
 5## Apoe         6.56046      1.632683      1.000000       0.351455
 6## Gja1         4.85359      0.508912      0.983240       0.214221
 7## Clu          6.21790      1.492429      0.994413       0.618106
 8## Gpr37l1      4.86027      0.453929      0.972067       0.158365
 9## Pla2g7       4.39260      0.619424      0.944134       0.239782
10##         mean.logFC.cohen min.logFC.cohen median.logFC.cohen max.logFC.cohen
11##                <numeric>       <numeric>          <numeric>       <numeric>
12## Slc1a3           3.67601        2.712489            3.68077         4.60039
13## Apoe             3.37960        0.168621            3.66780         5.04830
14## Gja1             3.58473        2.813154            3.65425         4.36901
15## Clu              3.47752        3.007695            3.59248         3.63657
16## Gpr37l1          3.43198        2.785091            3.40197         4.20804
17## Pla2g7           3.06031        1.386777            3.33164         3.64626
18##         rank.logFC.cohen  mean.AUC   min.AUC median.AUC   max.AUC  rank.AUC
19##                <integer> <numeric> <numeric>  <numeric> <numeric> <integer>
20## Slc1a3                 2  0.971184  0.941412   0.974113  0.987160         3
21## Apoe                   1  0.900640  0.495989   0.976723  0.994548         1
22## Gja1                   2  0.973603  0.954197   0.976803  0.987536         2
23## Clu                    1  0.976374  0.966779   0.977677  0.980761         1
24## Gpr37l1                2  0.963477  0.945269   0.963971  0.982537         4
25## Pla2g7                 2  0.943827  0.859332   0.960745  0.965692         5
26##         mean.logFC.detected min.logFC.detected median.logFC.detected
27##                   <numeric>          <numeric>             <numeric>
28## Slc1a3             2.030578          0.9313558              2.089780
29## Apoe               1.838217          0.1139562              1.963239
30## Gja1               2.378503          1.3997949              2.393263
31## Clu                0.817669          0.0474345              0.916262
32## Gpr37l1            2.708740          1.8132196              2.717976
33## Pla2g7             2.292453          0.4145697              2.539440
34##         max.logFC.detected rank.logFC.detected
35##                  <numeric>           <integer>
36## Slc1a3             3.05016                 211
37## Apoe               3.03333                 214
38## Gja1               3.72940                  70
39## Clu                1.54091                1262
40## Gpr37l1            3.78132                  63
41## Pla2g7             3.19337                 114
1top.cohen.markers <- rownames(ordered)[1:length(top.markers)]
2plotHeatmap( sce.zeisel, 
3             features=top.cohen.markers, 
4             order_columns_by="level1class", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.zeisel, 
2                    features=top.cohen.markers, 
3                    group="level1class", 
4                    center=TRUE
5                  )

This list is very strong, possibly even more canonical than the findMarkers list, because it captures several extremely well-known astrocyte identity genes that didn't show up in the previous list.

Canonical, high-confidence astrocyte markers (new in this list)

  • Apoe: the major lipid/cholesterol transport protein secreted by astrocytes in the CNS; one of the most-cited astrocyte identity genes in the field, notably absent from the findMarkers top-25
  • Slc1a2 (GLT-1/EAAT2): the primary astrocytic glutamate transporter (arguably even more central to astrocyte identity than Slc1a3/GLAST), also missing from the earlier list
  • Sparcl1 (Hevin): canonical astrocyte-secreted synaptogenic factor
  • Atp1a2: canonical astrocyte-specific Na+/K+-ATPase alpha subunit (pairs with Atp1b2 from the earlier list)

These four alone are a strong sign that scoreMarkers's median-Cohen's-d ranking is picking up genuinely central astrocyte biology, not just statistically convenient genes.

Same genes/themes as the findMarkers list: Gstm1, Mt1 (paralog of Mt2), Cst3: reasonable astrocyte-enriched genes, though Cst3 (cystatin C) is fairly broadly expressed across glial and even non-neural tissue, so it's less specific than the top-tier genes above. Scd2 fits the recurring lipid-metabolism theme (alongside Aldoc, Sparcl1, Apoe): consistent with astrocytes' core role in lipid handling.

Worth double-checking with different lineage associations

  • Pla2g7 (Lp-PLA2/PAFAH): a flag. In much of the literature, Pla2g7 is more strongly associated with monocyte/macrophage/microglial biology than astrocytes. Its presence here could reflect a genuine astrocyte-expressed subpopulation, ambient RNA from nearby microglia, or a doublet effect. Worth a violin/dot plot across all 7 cell types.
  • Mmd2: "monocyte to macrophage differentiation-associated 2": similar concern; its name reflects a different original expression context, though some brain atlases do report low-level astrocyte enrichment. Same recommendation: check specificity directly.

min

Using the minium value, only gene upregulated in comparison to every other cell type will be detected.

1ordered <- chosen[order(chosen$min.logFC.cohen,decreasing=TRUE),]
2head(ordered)
 1## DataFrame with 6 rows and 19 columns
 2##         self.average other.average self.detected other.detected
 3##            <numeric>     <numeric>     <numeric>      <numeric>
 4## Clu          6.21790      1.492429      0.994413       0.618106
 5## Gja1         4.85359      0.508912      0.983240       0.214221
 6## Gpr37l1      4.86027      0.453929      0.972067       0.158365
 7## Aqp4         4.79917      0.271795      0.932961       0.113509
 8## Slc1a3       5.97823      0.754770      0.983240       0.265159
 9## Aldoc        4.54419      0.480026      0.916201       0.241445
10##         mean.logFC.cohen min.logFC.cohen median.logFC.cohen max.logFC.cohen
11##                <numeric>       <numeric>          <numeric>       <numeric>
12## Clu              3.47752         3.00770            3.59248         3.63657
13## Gja1             3.58473         2.81315            3.65425         4.36901
14## Gpr37l1          3.43198         2.78509            3.40197         4.20804
15## Aqp4             2.91641         2.74180            2.90287         3.12477
16## Slc1a3           3.67601         2.71249            3.68077         4.60039
17## Aldoc            2.67529         2.42491            2.62957         2.92491
18##         rank.logFC.cohen  mean.AUC   min.AUC median.AUC   max.AUC  rank.AUC
19##                <integer> <numeric> <numeric>  <numeric> <numeric> <integer>
20## Clu                    1  0.976374  0.966779   0.977677  0.980761         1
21## Gja1                   2  0.973603  0.954197   0.976803  0.987536         2
22## Gpr37l1                2  0.963477  0.945269   0.963971  0.982537         4
23## Aqp4                   4  0.953356  0.946032   0.953218  0.960740         6
24## Slc1a3                 2  0.971184  0.941412   0.974113  0.987160         3
25## Aldoc                 10  0.934024  0.924724   0.933837  0.944700        14
26##         mean.logFC.detected min.logFC.detected median.logFC.detected
27##                   <numeric>          <numeric>             <numeric>
28## Clu                0.817669          0.0474345              0.916262
29## Gja1               2.378503          1.3997949              2.393263
30## Gpr37l1            2.708740          1.8132196              2.717976
31## Aqp4               3.004573          2.6200184              2.965608
32## Slc1a3             2.030578          0.9313558              2.089780
33## Aldoc              1.997170          1.0667036              1.986321
34##         max.logFC.detected rank.logFC.detected
35##                  <numeric>           <integer>
36## Clu                1.54091                1262
37## Gja1               3.72940                  70
38## Gpr37l1            3.78132                  63
39## Aqp4               3.65411                  83
40## Slc1a3             3.05016                 211
41## Aldoc              2.85822                 157
1top.cohen.min <- rownames(ordered)[1:length(top.markers)]
2plotHeatmap( sce.zeisel, 
3             features=top.cohen.min, 
4             order_columns_by="level1class", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.zeisel, 
2                    features=top.cohen.min, 
3                    group="level1class", 
4                    center=TRUE
5                  )

This list is the cleanest of the four, essentially every gene is a canonical, well-established astrocyte marker, with no ambiguous or unusual entries to flag:

  • Clu, Gja1, Gpr37l1, Aqp4, Slc1a3, Slc1a2, Aldoc, Cldn10, Ntsr2, Mt2, Ptprz1, Ndrg2, Slc4a4, Mt1, Ppap2b, Bcan, Prdx6, Gstm1: all repeatedly validated astrocyte-enriched genes, same core set seen across the other three lists
  • Mlc1, Fabp7 (BLBP), Htra1, Fgfr3, Acsbg1, Acsl6, Lcat: the same "exclusive/uniquely separating" genes that were distinctive to findMarkers.all, reflecting the shared underlying logic of both approaches (see comparison below)

No genes here required flagging. This ranking criterion (minimum Cohen's d across all pairwise comparisons) produced the most conservative and biologically clean marker set of the four.

compare results

 1dataset <- "Zeisel"
 2namedGenesList <- list( findMarkers.all = top.markers, findMarkers.some = top.markers.some, 
 3  cohen.median = top.cohen.markers, cohen.min = top.cohen.min
 4)
 5venn.diagram( namedGenesList,
 6                  paste0(dataset, "Venn.diagram.tiff"),
 7                  fill=wes_palette("FantasticFox1")[1:length(namedGenesList)],  
 8                  alpha=rep( 0.25, length(namedGenesList) ),
 9                  cex = 2.5, cat.fontface=4,
10                  category.names = names(namedGenesList),
11                  col=wes_palette("FantasticFox1")[1:length(namedGenesList)]
12                )
1## [1] 1
1log.files <- list.files('.', pattern = "*Venn.diagram.*.log", full.names = T)
2file.remove(log.files)
1## [1] TRUE
1cmd <- paste0( 'convert ', dataset, 'Venn.diagram.tiff ', dataset, 'Venn.diagram.png && rm ', dataset, 'Venn.diagram.tiff ' )
2system( cmd )

{.center .fiftypercent}

List Underlying logic Character of output Outlier genes flagged
findMarkers.all IUT, gene must be DE vs. every other cluster Clean, exclusive, "uniquely expressed" markers Scg3, Tmem47
findMarkers.some Holm-min, gene must be DE vs. most (≥50%) other clusters Broader, includes top-tier genes missed by all Pla2g7, Cst3, Sepp1, Mt1/Mt2
cohen.median Median effect size across all pairwise comparisons Very similar composition to findMarkers.some Pla2g7, Mmd2, Cst3
cohen.min Minimum (worst-case) effect size across all pairwise comparisons Cleanest, most conservative, same logic family as all None

Key pattern: the four lists split into two logic families rather than two methods:

  • "Most conservative" logic (findMarkers.all and cohen.min) both require a gene to hold up against every other cluster, one via a formal intersection-union p-value, the other via the minimum effect size. This shared logic explains why cohen.min recovers the same "exclusive" genes unique to findMarkers.all (Mlc1, Fabp7, Htra1, Fgfr3, Acsbg1, Acsl6, Lcat), genes that are strong markers against most, but not necessarily every, cluster equally.
  • "Majority/average" logic (findMarkers.some and cohen.median) both reward genes that are DE against most (not all) other clusters, or have a typical (not worst-case) effect size. These two lists converge almost completely (23/25 shared genes) and both surface additional top-tier astrocyte genes (Apoe, Slc1a2, Sparcl1, Atp1a2, Glul) that the stricter all/min approaches miss, simply because one weak pairwise comparison (e.g. against a very similar glial subtype) is enough to sink them under a worst-case criterion.

Practical takeaway: the "Most conservative" pair (all / cohen.min) gives the safest, most false-positive-resistant marker set, ideal for a small validated marker panel. The "majority/average" pair (some / cohen.median) gives a broader, more sensitive set that captures more of the true underlying biology (including some of the most-cited astrocyte genes in the literature) at the cost of occasionally admitting a less-specific or cross-lineage gene, worth spot-checking with expression plots before treating as definitive.

Baron pancreas data

  • ~8,500–14,000 cells depending on donor, human pancreas
  • ~14 annotated cell types (alpha, beta, delta, acinar, ductal, etc.), including some rare populations
1sce.baron <- BaronPancreasData()

check data

1sce.baron
 1## class: SingleCellExperiment 
 2## dim: 20125 8569 
 3## metadata(0):
 4## assays(1): counts
 5## rownames(20125): A1BG A1CF ... ZZZ3 pk
 6## rowData names(0):
 7## colnames(8569): human1_lib1.final_cell_0001 human1_lib1.final_cell_0002
 8##   ... human4_lib3.final_cell_0700 human4_lib3.final_cell_0701
 9## colData names(2): donor label
10## reducedDimNames(0):
11## mainExpName: NULL
12## altExpNames(0):

The OSCA book does not provide a dedicated Baron pancreas chapter, but we can adapt the Muraro chapter, which is the closest template, since it's also a droplet/CEL-seq-style pancreas dataset with pre-existing cell type labels.

1sce.baron <- addPerCellQC(sce.baron)
2qc <- quickPerCellQC(sce.baron)
3sce.baron <- sce.baron[, !qc$discard]
4sce.baron <- logNormCounts(sce.baron)
5dec <- modelGeneVar(sce.baron)
6top.hvgs <- getTopHVGs(dec, n = 2000)
7sce.baron <- runPCA(sce.baron, subset_row = top.hvgs)
8sce.baron <- runTSNE(sce.baron, dimred = "PCA")
9sce.baron <- runUMAP(sce.baron, dimred = "PCA")
1plotTSNE( sce.baron, colour_by = 'label' )

Since the dataset has 4 donors

1table(sce.baron$donor)
1## 
2## GSM2230757 GSM2230758 GSM2230759 GSM2230760 
3##       1937       1724       3605       1303

we will subset to the donor with highest number of cells

 1sce.baron <- sce.baron[, sce.baron$donor == 'GSM2230759' ]
 2sce.baron <- addPerCellQC(sce.baron)
 3qc <- quickPerCellQC(sce.baron)
 4sce.baron <- sce.baron[, !qc$discard]
 5sce.baron <- logNormCounts(sce.baron)
 6dec <- modelGeneVar(sce.baron)
 7top.hvgs <- getTopHVGs(dec, n = 2000)
 8sce.baron <- runPCA(sce.baron, subset_row = top.hvgs)
 9sce.baron <- runTSNE(sce.baron, dimred = "PCA")
10sce.baron <- runUMAP(sce.baron, dimred = "PCA")
1plotTSNE( sce.baron, colour_by = 'label' )
1plotUMAP( sce.baron, colour_by = 'label' )

P-value

all

1markers <- findMarkers( sce.baron, groups = sce.baron$label, direction="up", pval.type = 'all' )
1## Warning in FUN(...): no within-block comparison between schwann and acinar
1## Warning in FUN(...): no within-block comparison between schwann and
2## activated_stellate
1## Warning in FUN(...): no within-block comparison between schwann and alpha
1## Warning in FUN(...): no within-block comparison between schwann and beta
1## Warning in FUN(...): no within-block comparison between schwann and delta
1## Warning in FUN(...): no within-block comparison between schwann and ductal
1## Warning in FUN(...): no within-block comparison between schwann and endothelial
1## Warning in FUN(...): no within-block comparison between schwann and epsilon
1## Warning in FUN(...): no within-block comparison between schwann and gamma
1## Warning in FUN(...): no within-block comparison between schwann and macrophage
1## Warning in FUN(...): no within-block comparison between schwann and mast
1## Warning in FUN(...): no within-block comparison between schwann and
2## quiescent_stellate
1## Warning in FUN(...): no within-block comparison between t_cell and schwann
1names(markers)
1##  [1] "acinar"             "activated_stellate" "alpha"             
2##  [4] "beta"               "delta"              "ductal"            
3##  [7] "endothelial"        "epsilon"            "gamma"             
4## [10] "macrophage"         "mast"               "quiescent_stellate"
5## [13] "schwann"            "t_cell"

We will use Beta cells, a cell type with plently of cells in this dataset and very well-defined markers (INS, insulin)

1marker.set <- markers[["beta"]]
2head(marker.set)
 1## DataFrame with 6 rows and 16 columns
 2##              p.value         FDR summary.logFC logFC.acinar
 3##            <numeric>   <numeric>     <numeric>    <numeric>
 4## ADCYAP1  3.82197e-52 7.69171e-48      2.804818     2.947574
 5## DLK1     7.09409e-51 7.13843e-47      1.165101     1.185885
 6## ENDOD1   3.12929e-31 2.09923e-27      0.599409     0.661405
 7## HADH     4.02317e-27 2.02416e-23      0.903507     1.472314
 8## C1orf127 8.02899e-26 3.23167e-22      0.300417     0.327315
 9## SYNGR4   6.49800e-24 2.17954e-20      1.088293     1.212451
10##          logFC.activated_stellate logFC.alpha logFC.delta logFC.ductal
11##                         <numeric>   <numeric>   <numeric>    <numeric>
12## ADCYAP1                  2.924677    2.907981    2.864506     2.878854
13## DLK1                     1.191400    1.183111    1.159609     1.123609
14## ENDOD1                   0.613597    0.601804    0.632186     0.641972
15## HADH                     1.326232    1.531510    0.903507     1.427523
16## C1orf127                 0.309279    0.316580    0.300417     0.318895
17## SYNGR4                   1.202426    0.537712    1.090644     1.178889
18##          logFC.endothelial logFC.epsilon logFC.gamma logFC.macrophage
19##                  <numeric>     <numeric>   <numeric>        <numeric>
20## ADCYAP1           2.869060      2.978076    2.804818         2.978076
21## DLK1              1.179136      1.203797    1.203797         1.203797
22## ENDOD1            0.599409      0.683211    0.683211         0.683211
23## HADH              1.440996      1.575161    1.575161         1.575161
24## C1orf127          0.332038      0.332038    0.332038         0.332038
25## SYNGR4            1.175324      1.221243    1.188864         1.221243
26##          logFC.mast logFC.quiescent_stellate logFC.schwann logFC.t_cell
27##           <numeric>                <numeric>     <numeric>    <numeric>
28## ADCYAP1    2.978076                 2.859069            NA     2.978076
29## DLK1       1.203797                 1.165101            NA     1.203797
30## ENDOD1     0.683211                 0.657396            NA     0.683211
31## HADH       1.575161                 1.452821            NA     1.575161
32## C1orf127   0.332038                 0.316126            NA     0.332038
33## SYNGR4     1.221243                 1.088293            NA     1.221243
1top.markers <- rownames(marker.set)[1:25]
2plotHeatmap( sce.baron, 
3             features=top.markers, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.markers, 
3                    group="label", 
4                    center=TRUE
5                  )

Most are strong, plausible beta-cell markers, several matching known canonical beta-cell biology:

  • INS.IGF2: the insulin/IGF2 locus region, essentially the insulin signal itself, the single most definitive beta-cell marker
  • ABCC8 (SUR1), KCNK16, G6PC2, SLC30A8 (ZnT8): canonical beta-cell ion channel/metabolic genes central to glucose-sensing and insulin secretion machinery
  • HADH: metabolic enzyme repeatedly reported as beta-cell-enriched in human islet scRNA-seq atlases (e.g. Segerstolpe et al.)
  • ADCYAP1 (PACAP): repeatedly reported as beta-cell-enriched in human islet studies
  • C1orf127: reported as a beta-cell-specific locus in human islet transcriptomic studies
  • DLK1: reported as enriched in a subset of human beta cells in several islet atlases
  • SPINK2, SYNGR4, PRPH, GPM6A, NPTX2, PLCH2, LRFN2, CTNNA2, HHATL, GREM2, PPP1R1A: plausible beta-cell-enriched genes reported in various human islet single-cell studies, though less "textbook" than the ion-channel/metabolic genes above

Worth a second look:

  • MAPT (Tau): more classically a neuronal cytoskeletal gene; its appearance here is plausible given beta cells' neuroendocrine character, but it's not a classic beta marker
  • ENDOD1, SLCO1A2, APOBEC2, ACPP: less characterized in beta-cell literature; recommend a sanity check with expression plots

some

1markers <- findMarkers( sce.baron, groups = sce.baron$label, direction="up", pval.type = 'some' )
1## Warning in FUN(...): no within-block comparison between schwann and acinar
1## Warning in FUN(...): no within-block comparison between schwann and
2## activated_stellate
1## Warning in FUN(...): no within-block comparison between schwann and alpha
1## Warning in FUN(...): no within-block comparison between schwann and beta
1## Warning in FUN(...): no within-block comparison between schwann and delta
1## Warning in FUN(...): no within-block comparison between schwann and ductal
1## Warning in FUN(...): no within-block comparison between schwann and endothelial
1## Warning in FUN(...): no within-block comparison between schwann and epsilon
1## Warning in FUN(...): no within-block comparison between schwann and gamma
1## Warning in FUN(...): no within-block comparison between schwann and macrophage
1## Warning in FUN(...): no within-block comparison between schwann and mast
1## Warning in FUN(...): no within-block comparison between schwann and
2## quiescent_stellate
1## Warning in FUN(...): no within-block comparison between t_cell and schwann
1names(markers)
1##  [1] "acinar"             "activated_stellate" "alpha"             
2##  [4] "beta"               "delta"              "ductal"            
3##  [7] "endothelial"        "epsilon"            "gamma"             
4## [10] "macrophage"         "mast"               "quiescent_stellate"
5## [13] "schwann"            "t_cell"
1marker.set <- markers[["beta"]]
2head(marker.set)
 1## DataFrame with 6 rows and 16 columns
 2##              p.value          FDR summary.logFC logFC.acinar
 3##            <numeric>    <numeric>     <numeric>    <numeric>
 4## PPP1R1A 2.10143e-275 4.22912e-271       2.61068      2.61068
 5## NLRP1   1.13034e-250 1.13741e-246       2.03041      2.10552
 6## ADCYAP1 5.53611e-216 3.71381e-212       2.90798      2.94757
 7## HADH    1.52578e-197 7.67658e-194       1.53151      1.47231
 8## PCSK1   3.31573e-133 1.33458e-129       1.23063      1.35322
 9## PCP4    7.66596e-125 2.57129e-121       1.10613      1.13139
10##         logFC.activated_stellate logFC.alpha logFC.delta logFC.ductal
11##                        <numeric>   <numeric>   <numeric>    <numeric>
12## PPP1R1A                  2.50328    1.081112    1.201034      2.51503
13## NLRP1                    1.94944    0.392301   -0.482129      2.03041
14## ADCYAP1                  2.92468    2.907981    2.864506      2.87885
15## HADH                     1.32623    1.531510    0.903507      1.42752
16## PCSK1                    1.29893    1.230633   -0.594472      1.33812
17## PCP4                     1.10460    0.737415   -0.740737      1.10613
18##         logFC.endothelial logFC.epsilon logFC.gamma logFC.macrophage logFC.mast
19##                 <numeric>     <numeric>   <numeric>        <numeric>  <numeric>
20## PPP1R1A           2.57604     2.6480664    2.648066          2.64807    2.64807
21## NLRP1             2.00262     2.1298050    0.509874          2.12980    2.12980
22## ADCYAP1           2.86906     2.9780757    2.804818          2.97808    2.97808
23## HADH              1.44100     1.5751614    1.575161          1.57516    1.57516
24## PCSK1             1.29129    -0.0759007    0.534346          1.36476    1.36476
25## PCP4              1.07768     0.3159199    1.084762          1.13733    1.13733
26##         logFC.quiescent_stellate logFC.schwann logFC.t_cell
27##                        <numeric>     <numeric>    <numeric>
28## PPP1R1A                  2.46717            NA      2.64807
29## NLRP1                    1.82306            NA      2.12980
30## ADCYAP1                  2.85907            NA      2.97808
31## HADH                     1.45282            NA      1.57516
32## PCSK1                    1.30591            NA      1.36476
33## PCP4                     1.13733            NA      1.13733
1top.markers.some <- rownames(marker.set)[1:25]
2plotHeatmap( sce.baron, 
3             features=top.markers.some, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.markers.some, 
3                    group="label", 
4                    center=TRUE
5                  )

Also strong, with substantial overlap with findMarkers.all, plus several additional canonical secretory/endocrine genes:

  • IAPP (amylin): co-secreted with insulin, one of the most canonical beta-cell markers , notably absent from findMarkers.all but recovered here
  • PCSK1: proinsulin-processing enzyme, canonical beta-cell secretory machinery gene
  • INSM1: pan-endocrine transcription factor, well-established islet marker
  • GAD2 (GAD65): in human islets (unlike rodent), GAD65 is a well-documented beta-cell autoantigen and marker
  • SNAP25: vesicle-fusion machinery central to regulated hormone secretion
  • ADCYAP1, HADH, PPP1R1A, SYNGR4, ENDOD1, ABCC8, DLK1: overlapping with the all list, same canonical status
  • FXYD2: ion-transport regulatory subunit reported in beta cells

Worth a second look:

  • NLRP1: inflammasome component, unusual for a canonical beta marker; could reflect a genuine stress/inflammatory subpopulation or noise
  • SCGB2A1 (secretoglobin): more classically associated with epithelial secretory tissues (mammary, prostate) than islet endocrine cells, flag for a sanity check
  • RBP4: retinol-binding protein, more classically a liver/adipose-secreted factor; plausible but non-canonical for beta cells specifically
  • PCP4, C1QL1, CNIH2, TMOD1: less-characterized in beta-cell-specific literature, some (PCP4) more associated with other islet cell types (e.g. delta cells) in certain atlases
  • SEC11C, EEF1A2, ERO1B: general secretory-pathway/translation genes, plausible given beta cells' high secretory demand but not beta-exclusive
  • SCG3: chromogranin-family, pan-endocrine rather than beta-specific

Cohen's D

median

1marker.info <- scoreMarkers( sce.baron, groups = sce.baron$label )
2names(marker.info)
1##  [1] "acinar"             "activated_stellate" "alpha"             
2##  [4] "beta"               "delta"              "ductal"            
3##  [7] "endothelial"        "epsilon"            "gamma"             
4## [10] "macrophage"         "mast"               "quiescent_stellate"
5## [13] "schwann"            "t_cell"
1chosen <- marker.info[["beta"]]
2ordered <- chosen[order(chosen$median.logFC.cohen,decreasing=TRUE),]
3head(ordered)
 1## DataFrame with 6 rows and 19 columns
 2##        self.average other.average self.detected other.detected mean.logFC.cohen
 3##           <numeric>     <numeric>     <numeric>      <numeric>        <numeric>
 4## INS        10.30467       2.23340      1.000000       0.747760          5.69385
 5## PCSK1N      3.78043       1.44517      0.993647       0.352881          3.15496
 6## SCG5        3.36036       1.09667      0.974587       0.371592          2.81317
 7## IAPP        5.76456       0.44600      0.954257       0.249669          3.22748
 8## CPE         3.94977       1.34238      0.978399       0.411526          2.65009
 9## GNAS        5.15825       2.69687      0.996188       0.907319          2.45593
10##        min.logFC.cohen median.logFC.cohen max.logFC.cohen rank.logFC.cohen
11##              <numeric>          <numeric>       <numeric>        <integer>
12## INS           3.922247            5.57851         8.52054                1
13## PCSK1N       -1.395328            4.08598         6.72972                1
14## SCG5         -0.591512            3.56926         5.24534                2
15## IAPP          2.673148            3.28697         3.82606                2
16## CPE           0.207575            3.09983         5.18685                3
17## GNAS          0.383697            2.79430         4.52860                3
18##         mean.AUC   min.AUC median.AUC   max.AUC  rank.AUC mean.logFC.detected
19##        <numeric> <numeric>  <numeric> <numeric> <integer>           <numeric>
20## INS     0.995418  0.987641   0.996124  1.000000         1            0.339909
21## PCSK1N  0.775174  0.127700   0.978564  0.996823         1            1.844320
22## SCG5    0.851958  0.300375   0.971840  0.987294         3            1.682381
23## IAPP    0.962887  0.942186   0.964938  0.977128         2            1.706809
24## CPE     0.864538  0.582184   0.972500  0.989199         3            1.450672
25## GNAS    0.926215  0.611872   0.980423  0.993837         2            0.136635
26##        min.logFC.detected median.logFC.detected max.logFC.detected
27##                 <numeric>             <numeric>          <numeric>
28## INS            0.00000000             0.3103401           1.000000
29## PCSK1N        -0.00663086             2.5616449           3.756186
30## SCG5          -0.02465152             1.5603110           3.917464
31## IAPP          -0.04468058             1.8439430           2.382529
32## CPE           -0.03064459             1.3875083           3.733905
33## GNAS          -0.00536079             0.0731451           0.442317
34##        rank.logFC.detected
35##                  <integer>
36## INS                      1
37## PCSK1N                   1
38## SCG5                     3
39## IAPP                     4
40## CPE                      2
41## GNAS                  5020
1top.cohen.markers <- rownames(ordered)[1:length(top.markers)]
2plotHeatmap( sce.baron, 
3             features=top.cohen.markers, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.cohen.markers, 
3                    group="label", 
4                    center=TRUE
5                  )

The strongest, most canonical list of the four, dominated by textbook beta-cell identity genes:

  • INS: insulin itself, the definitive beta-cell marker, correctly ranked #1
  • IAPP, PCSK1N, SCG5, CPE, PTPRN (IA-2), CHGA, CHGB, SCG2, VGF: canonical insulin-processing and secretory-granule genes central to beta-cell identity
  • SCGN (secretagogin): well-established islet/beta-cell calcium-binding protein marker
  • GNAS, ADCYAP1, HADH, PPP1R1A, GAD2: overlapping with the findMarkers lists, same canonical status
  • UCHL1 (PGP9.5): classic neuroendocrine marker, consistent with beta-cell identity

Worth a second look:

  • PEMT: phospholipid metabolism enzyme; some literature association with beta cells, but less textbook
  • EEF1A2, FXYD2, BEX1: same as before, plausible but not beta-exclusive
  • TTR (transthyretin): notable outlier, in several human islet atlases TTR is reported as more alpha- or delta-cell-enriched rather than beta-specific; worth a direct expression check
  • GNG4: G-protein subunit, plausible neuroendocrine-secretory association but not classic

min

1ordered <- chosen[order(chosen$min.logFC.cohen,decreasing=TRUE),]
2head(ordered)
 1## DataFrame with 6 rows and 19 columns
 2##         self.average other.average self.detected other.detected
 3##            <numeric>     <numeric>     <numeric>      <numeric>
 4## INS        10.304669     2.2334012      1.000000      0.7477599
 5## IAPP        5.764555     0.4459999      0.954257      0.2496686
 6## ADCYAP1     2.978076     0.0590821      0.871665      0.0319387
 7## DLK1        1.203797     0.0183638      0.459975      0.0108082
 8## ENDOD1      0.683211     0.0288237      0.477764      0.0241528
 9## HADH        1.575161     0.1131713      0.773825      0.0834194
10##         mean.logFC.cohen min.logFC.cohen median.logFC.cohen max.logFC.cohen
11##                <numeric>       <numeric>          <numeric>       <numeric>
12## INS              5.69385        3.922247            5.57851         8.52054
13## IAPP             3.22748        2.673148            3.28697         3.82606
14## ADCYAP1          2.18174        2.059576            2.19313         2.27847
15## DLK1             1.10189        1.013443            1.10931         1.12984
16## ENDOD1           1.13472        0.988859            1.15631         1.22311
17## HADH             1.86643        0.953038            1.95827         2.11988
18##         rank.logFC.cohen  mean.AUC   min.AUC median.AUC   max.AUC  rank.AUC
19##                <integer> <numeric> <numeric>  <numeric> <numeric> <integer>
20## INS                    1  0.995418  0.987641   0.996124  1.000000         1
21## IAPP                   2  0.962887  0.942186   0.964938  0.977128         2
22## ADCYAP1                3  0.928407  0.917902   0.929269  0.935832         3
23## DLK1                  12  0.725817  0.712742   0.726728  0.729987        22
24## ENDOD1                13  0.728405  0.708614   0.729434  0.738882        26
25## HADH                   4  0.863378  0.742386   0.878122  0.886912         4
26##         mean.logFC.detected min.logFC.detected median.logFC.detected
27##                   <numeric>          <numeric>             <numeric>
28## INS                0.339909          0.0000000               0.31034
29## IAPP               1.706809         -0.0446806               1.84394
30## ADCYAP1            3.278615          0.9043219               3.75832
31## DLK1               3.161470          0.5459433               3.64493
32## ENDOD1             2.672024          0.5634156               2.94207
33## HADH               2.556059          0.8227821               2.60465
34##         max.logFC.detected rank.logFC.detected
35##                  <numeric>           <integer>
36## INS                1.00000                   1
37## IAPP               2.38253                   4
38## ADCYAP1            4.61303                   4
39## DLK1               4.98694                   4
40## ENDOD1             4.18583                   3
41## HADH               4.85088                   2
1top.cohen.min <- rownames(ordered)[1:length(top.markers)]
2plotHeatmap( sce.baron, 
3             features=top.cohen.min, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.cohen.min, 
3                    group="label", 
4                    center=TRUE
5                  )

Also very strong, with the same canonical core as findMarkers.all, reinforcing that both "worst-case" criteria converge well here:

  • INS, IAPP: correctly recovered, INS at rank #1
  • MAFA: canonical beta-cell master transcription factor, a strong addition not seen in either findMarkers list
  • ABCC8, G6PC2, KCNK16, ADCYAP1, HADH, PPP1R1A, GAD2, C1orf127, NPTX2, SYNGR4, ACPP, CTNNA2: overlapping with findMarkers.all, same canonical/plausible status
  • IGF2: consistent with the INS.IGF2 locus signal seen in findMarkers.all
  • CDKN1C (p57): reported as enriched in mature, non-proliferating beta cells in several human islet studies
  • STX1A: canonical exocytosis-machinery gene central to regulated insulin secretion
  • MAPT, ENDOD1, DLK1: same as findMarkers.all

Worth a second look:

  • SIL1, WDR25, HMGN5: less-characterized in beta-cell-specific literature

compare results

 1dataset <- "Baron"
 2namedGenesList <- list( findMarkers.all = top.markers, findMarkers.some = top.markers.some, 
 3  cohen.median = top.cohen.markers, cohen.min = top.cohen.min
 4)
 5venn.diagram( namedGenesList,
 6                  paste0(dataset, "Venn.diagram.tiff"),
 7                  fill=wes_palette("FantasticFox1")[1:length(namedGenesList)],  
 8                  alpha=rep( 0.25, length(namedGenesList) ),
 9                  cex = 2.5, cat.fontface=4,
10                  category.names = names(namedGenesList),
11                  col=wes_palette("FantasticFox1")[1:length(namedGenesList)]
12                )
1## [1] 1
1log.files <- list.files('.', pattern = "*Venn.diagram.*.log", full.names = T)
2file.remove(log.files)
1## [1] TRUE
1cmd <- paste0( 'convert ', dataset, 'Venn.diagram.tiff ', dataset, 'Venn.diagram.png && rm ', dataset, 'Venn.diagram.tiff ' )
2system( cmd )

{.center .fiftypercent}

List Canonical hits (INS/IAPP/MAFA/etc.) Overall signal quality Outlier genes flagged
findMarkers.all INS.IGF2, ABCC8, G6PC2, SLC30A8, HADH Strong MAPT, ENDOD1, SLCO1A2, APOBEC2, ACPP
findMarkers.some IAPP, PCSK1, INSM1, GAD2, SNAP25 Strong, broader secretory-pathway coverage NLRP1, SCGB2A1, RBP4, PCP4, C1QL1
cohen.median INS (#1), IAPP, PTPRN, CHGA, CHGB, SCGN Strongest — most textbook genes at top ranks TTR, PEMT, GNG4
cohen.min INS (#1), IAPP, MAFA, STX1A, CDKN1C Strongest — best coverage of both hormone and TF/exocytosis machinery SIL1, WDR25, HMGN5

Key contrast with the epsilon cell results: unlike epsilon cells, where findMarkers badly under-recovered the canonical marker (GHRL absent from "some", buried at rank 14 in "all") and only scoreMarkers reliably found it, all four methods correctly rank INS (or the INS-linked locus) at or near the top for beta cells, and all four surface a substantial, overlapping core of well-established beta-cell genes (IAPP, ABCC8, HADH, ADCYAP1, PPP1R1A, GAD2, G6PC2, C1orf127, DLK1, ENDOD1, SYNGR4).

This is the expected "positive control" behavior: with a large, abundant, transcriptionally distinct cluster, statistical power is high enough that both p-value-based (findMarkers) and effect-size-based (scoreMarkers) approaches converge on the same biological answer, regardless of the specific combining rule (all/some/median/min) used. This reinforces the interpretation from the epsilon cell case: the divergence between findMarkers and scoreMarkers is driven primarily by cluster size and effect magnitude, not by an inherent flaw in either method — with enough cells and a strong enough biological signal, both methods agree well.

Rare cell type

The previous tests focused on very clear cell types with many cells in the dataset. For stress-testing these methods, we will focus now in Epsilon cells. Epsilon cells (ghrelin-producing) are consistently the smallest endocrine population in pancreatic islet atlases, which makes them an ideal stress test.

P-value

all

1markers <- findMarkers( sce.baron, groups = sce.baron$label, direction="up", pval.type = 'all' )
1## Warning in FUN(...): no within-block comparison between schwann and acinar
1## Warning in FUN(...): no within-block comparison between schwann and
2## activated_stellate
1## Warning in FUN(...): no within-block comparison between schwann and alpha
1## Warning in FUN(...): no within-block comparison between schwann and beta
1## Warning in FUN(...): no within-block comparison between schwann and delta
1## Warning in FUN(...): no within-block comparison between schwann and ductal
1## Warning in FUN(...): no within-block comparison between schwann and endothelial
1## Warning in FUN(...): no within-block comparison between schwann and epsilon
1## Warning in FUN(...): no within-block comparison between schwann and gamma
1## Warning in FUN(...): no within-block comparison between schwann and macrophage
1## Warning in FUN(...): no within-block comparison between schwann and mast
1## Warning in FUN(...): no within-block comparison between schwann and
2## quiescent_stellate
1## Warning in FUN(...): no within-block comparison between t_cell and schwann
1names(markers)
1##  [1] "acinar"             "activated_stellate" "alpha"             
2##  [4] "beta"               "delta"              "ductal"            
3##  [7] "endothelial"        "epsilon"            "gamma"             
4## [10] "macrophage"         "mast"               "quiescent_stellate"
5## [13] "schwann"            "t_cell"

Select Epsilon cells.

1marker.set <- markers[["epsilon"]]
2head(marker.set)
 1## DataFrame with 6 rows and 16 columns
 2##             p.value       FDR summary.logFC logFC.acinar
 3##           <numeric> <numeric>     <numeric>    <numeric>
 4## CLU      0.00162492         1       5.83598      5.10869
 5## C10orf10 0.00291337         1       2.85518      2.54781
 6## DNAJC12  0.00357871         1       2.04196      1.86718
 7## EIF4A1   0.00357871         1       2.04196      1.03643
 8## TMEM59   0.00357871         1       2.04196      1.63054
 9## ITFG1    0.00357890         1       2.04196      1.96372
10##          logFC.activated_stellate logFC.alpha logFC.beta logFC.delta
11##                         <numeric>   <numeric>  <numeric>   <numeric>
12## CLU                       5.19351    0.676458   4.286503    2.868398
13## C10orf10                  2.76758    1.579832   1.177291    2.081857
14## DNAJC12                   1.94468    0.925978   0.585280    0.823184
15## EIF4A1                    0.73903    1.219088   1.546688    1.451831
16## TMEM59                    1.30457    0.687245   0.572086    0.543979
17## ITFG1                     1.89768    1.798522   1.806795    1.809807
18##          logFC.ductal logFC.endothelial logFC.gamma logFC.macrophage logFC.mast
19##             <numeric>         <numeric>   <numeric>        <numeric>  <numeric>
20## CLU          4.424696           5.53682    1.477849          5.74437    1.98222
21## C10orf10     2.362217           2.08370    2.338561          2.85518    2.85518
22## DNAJC12      1.994867           2.00371    0.603961          1.90655    1.76280
23## EIF4A1       0.806659           1.02513    1.515684          1.31952    1.72868
24## TMEM59       1.299768           1.29398    0.847936          1.45605    1.50853
25## ITFG1        1.896463           1.92235    1.711640          1.84485    2.04196
26##          logFC.quiescent_stellate logFC.schwann logFC.t_cell
27##                         <numeric>     <numeric>    <numeric>
28## CLU                      5.325832            NA      5.83598
29## C10orf10                 2.802829            NA      2.85518
30## DNAJC12                  1.882799            NA      2.04196
31## EIF4A1                   0.571178            NA      2.04196
32## TMEM59                   1.285782            NA      2.04196
33## ITFG1                    1.970115            NA      2.04196

No significant (FDR < 0.05) where found, we will continue with the top genes for comparative purposes.

1top.markers <- rownames(marker.set)[1:25]
2plotHeatmap( sce.baron, 
3             features=top.markers, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.markers, 
3                    group="label", 
4                    center=TRUE
5                  )

Only a few genes here are epsilon-specific, and even the one unambiguous marker is buried mid-list rather than at the top:

  • GHRL: ghrelin, epsilon cells are identified by high GHRL expression, and this is the single most-established epsilon marker in the literature. Its presence at rank 14 (not top-ranked) despite being the canonical, near-exclusive marker for this cell type is itself a signal that a strict "beat every other cluster" p-value criterion loses power with very few cells.
  • SERPINA1, VTN: not classic epsilon markers, more associated with liver/exocrine or vascular biology; plausibly reflect ambient RNA contamination given the tiny cluster size
  • CLU, RBCK1, DNAJC12, EIF4A1, TMEM59, ITFG1, ST6GALNAC6, RBP1, SYBU, CIDEB, NGFRAP1, CALY, RASD1, AHSA1, PDXDC1, NAPB, PCYT1A, TMEM87B, NUS1, GINM1, SNHG21, C10orf10: largely generic/housekeeping genes (translation factors, ubiquitin-related, lipid metabolism, ribosomal/RNA-processing) without established epsilon-specific biology in the literature, most likely likely statistical noise from testing with so few cells, rather than genuine markers

some

1markers <- findMarkers( sce.baron, groups = sce.baron$label, direction="up", pval.type = 'some' )
1## Warning in FUN(...): no within-block comparison between schwann and acinar
1## Warning in FUN(...): no within-block comparison between schwann and
2## activated_stellate
1## Warning in FUN(...): no within-block comparison between schwann and alpha
1## Warning in FUN(...): no within-block comparison between schwann and beta
1## Warning in FUN(...): no within-block comparison between schwann and delta
1## Warning in FUN(...): no within-block comparison between schwann and ductal
1## Warning in FUN(...): no within-block comparison between schwann and endothelial
1## Warning in FUN(...): no within-block comparison between schwann and epsilon
1## Warning in FUN(...): no within-block comparison between schwann and gamma
1## Warning in FUN(...): no within-block comparison between schwann and macrophage
1## Warning in FUN(...): no within-block comparison between schwann and mast
1## Warning in FUN(...): no within-block comparison between schwann and
2## quiescent_stellate
1## Warning in FUN(...): no within-block comparison between t_cell and schwann
1names(markers)
1##  [1] "acinar"             "activated_stellate" "alpha"             
2##  [4] "beta"               "delta"              "ductal"            
3##  [7] "endothelial"        "epsilon"            "gamma"             
4## [10] "macrophage"         "mast"               "quiescent_stellate"
5## [13] "schwann"            "t_cell"
1marker.set <- markers[["epsilon"]]
2head(marker.set)
 1## DataFrame with 6 rows and 16 columns
 2##              p.value         FDR summary.logFC logFC.acinar
 3##            <numeric>   <numeric>     <numeric>    <numeric>
 4## CLU      1.39031e-17 2.79799e-13      5.744375      5.10869
 5## RBCK1    8.63534e-16 8.68931e-12      1.491910      1.72944
 6## PPP2R1A  8.50449e-15 5.70509e-11      1.837770      2.10601
 7## C10orf10 5.73089e-14 2.88335e-10      2.767581      2.54781
 8## RBP1     7.15700e-13 2.88069e-09      1.494734      1.19092
 9## LRRFIP1  1.00735e-11 3.37883e-08      0.599206      1.46438
10##          logFC.activated_stellate logFC.alpha logFC.beta logFC.delta
11##                         <numeric>   <numeric>  <numeric>   <numeric>
12## CLU                       5.19351    0.676458    4.28650    2.868398
13## RBCK1                     1.89499    1.569998    1.71417    1.799870
14## PPP2R1A                   1.50579    1.585841    1.92267    1.958516
15## C10orf10                  2.76758    1.579832    1.17729    2.081857
16## RBP1                      1.72283    1.647284    1.63449    0.984536
17## LRRFIP1                   1.46639    1.301833    1.82010    1.666693
18##          logFC.ductal logFC.endothelial logFC.gamma logFC.macrophage logFC.mast
19##             <numeric>         <numeric>   <numeric>        <numeric>  <numeric>
20## CLU          4.424696           5.53682     1.47785          5.74437   1.982218
21## RBCK1        1.491910           1.88664     1.84736          1.96061   1.420154
22## PPP2R1A      1.226239           1.77772     1.98399          2.12545   1.853787
23## C10orf10     2.362217           2.08370     2.33856          2.85518   2.855182
24## RBP1         0.817196           1.60877     1.49473          2.04196   2.041960
25## LRRFIP1      0.599206           1.54870     1.62696          1.31015   0.636798
26##          logFC.quiescent_stellate logFC.schwann logFC.t_cell
27##                         <numeric>     <numeric>    <numeric>
28## CLU                       5.32583            NA      5.83598
29## RBCK1                     1.82740            NA      2.04196
30## PPP2R1A                   1.83777            NA      1.43285
31## C10orf10                  2.80283            NA      2.85518
32## RBP1                      1.63215            NA      2.04196
33## LRRFIP1                   1.53628            NA      1.01602
1top.markers.some <- rownames(marker.set)[1:25]
2plotHeatmap( sce.baron, 
3             features=top.markers.some, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.markers.some, 
3                    group="label", 
4                    center=TRUE
5                  )

Notably worse: GHRL does not appear anywhere in this top-25 list at all, a real red flag for this parameterization on a rare cell type.

  • Nearly the entire list (RPL5, EEF1A2, CALM2, SKP1, ATP6AP1, DYNLRB1, GABARAPL2, CHMP4B, LRRFIP1, ECH1, CD82, DAP, NR3C1, PPP2R1A) consists of broadly-expressed housekeeping/translation/vesicle-trafficking genes with no epsilon-specific role documented
  • This is the weakest of the four lists, with "some"'s majority-vote logic combined with very low cell numbers, the ranking appears to be dominated by noise rather than biology

Cohen's D

median

1marker.info <- scoreMarkers( sce.baron, groups = sce.baron$label )
2names(marker.info)
1##  [1] "acinar"             "activated_stellate" "alpha"             
2##  [4] "beta"               "delta"              "ductal"            
3##  [7] "endothelial"        "epsilon"            "gamma"             
4## [10] "macrophage"         "mast"               "quiescent_stellate"
5## [13] "schwann"            "t_cell"
1chosen <- marker.info[["epsilon"]]
2ordered <- chosen[order(chosen$median.logFC.cohen,decreasing=TRUE),]
3head(ordered)
 1## DataFrame with 6 rows and 19 columns
 2##         self.average other.average self.detected other.detected
 3##            <numeric>     <numeric>     <numeric>      <numeric>
 4## VTN          2.74298    0.00533242             1     0.00497212
 5## GHRL         9.19138    0.00164257             1     0.00131614
 6## SYBU         2.04196    0.03471349             1     0.02799088
 7## CALY         3.21008    0.23398177             1     0.13566012
 8## CIDEB        2.04196    0.08525482             1     0.05109872
 9## TMEM179      1.83091    0.06949539             1     0.05435582
10##         mean.logFC.cohen min.logFC.cohen median.logFC.cohen max.logFC.cohen
11##                <numeric>       <numeric>          <numeric>       <numeric>
12## VTN             28.37347        16.94799           31.87112         31.8711
13## GHRL            24.18584        23.74169           24.29966         24.2997
14## SYBU            38.59628         5.60475           18.56236         88.9349
15## CALY            14.13938         2.33228           16.25252         23.8458
16## CIDEB           28.27451         2.71421           10.29078         88.9349
17## TMEM179          7.67959         3.36127            9.29101          9.7340
18##         rank.logFC.cohen  mean.AUC   min.AUC median.AUC   max.AUC  rank.AUC
19##                <integer> <numeric> <numeric>  <numeric> <numeric> <integer>
20## VTN                    1  1.000000  1.000000          1         1         1
21## GHRL                   1  1.000000  1.000000          1         1         1
22## SYBU                   1  0.998219  0.987929          1         1         1
23## CALY                   3  0.995919  0.951327          1         1         1
24## CIDEB                  1  0.980123  0.857143          1         1         1
25## TMEM179                7  0.994246  0.968584          1         1         1
26##         mean.logFC.detected min.logFC.detected median.logFC.detected
27##                   <numeric>          <numeric>             <numeric>
28## VTN                 1.52593           1.000000               1.58496
29## GHRL                1.53619           1.000000               1.58496
30## SYBU                1.46471           1.000000               1.50696
31## CALY                1.25261           0.435899               1.49381
32## CIDEB               1.40517           1.000000               1.43296
33## TMEM179             1.40462           1.000000               1.56970
34##         max.logFC.detected rank.logFC.detected
35##                  <numeric>           <integer>
36## VTN                1.58496                   1
37## GHRL               1.58496                   1
38## SYBU               1.58496                   1
39## CALY               1.58496                   1
40## CIDEB              1.58496                   1
41## TMEM179            1.58496                   1
1top.cohen.markers <- rownames(ordered)[1:length(top.markers)]
2plotHeatmap( sce.baron, 
3             features=top.cohen.markers, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.cohen.markers, 
3                    group="label", 
4                    center=TRUE
5                  )

Much stronger, GHRL appears at rank 2:

  • GHRL: correctly surfaces near the top
  • VTN: same caveat as before, non-specific
  • SERPINA1, SPINK1: pancreatic-associated but typically linked to exocrine/acinar cells rather than epsilon specifically
  • PCSK1N: proprotein convertase inhibitor, plausible, since epsilon cells process peptide hormones like ghrelin, this fits the hormone-processing machinery theme even if not epsilon-exclusive
  • CIDEB, ACSL1, NAPB, SYBU, CALY, RASD1: lipid handling/secretory-vesicle genes, biologically plausible for a hormone-secreting cell type but not classic textbook markers
  • FRZB, ENPP2, CXCL16, SMOC1, C12orf75, TMEM179, PGPEP1, ST6GALNAC6, ITFG1, CLU, RBCK1, C10orf10: mostly uncharacterized or broadly-expressed genes for this context

min

1ordered <- chosen[order(chosen$min.logFC.cohen,decreasing=TRUE),]
2head(ordered)
 1## DataFrame with 6 rows and 19 columns
 2##            self.average other.average self.detected other.detected
 3##               <numeric>     <numeric>     <numeric>      <numeric>
 4## GHRL            9.19138    0.00164257             1     0.00131614
 5## VTN             2.74298    0.00533242             1     0.00497212
 6## FRZB            3.00081    0.01920769             1     0.01258725
 7## SYBU            2.04196    0.03471349             1     0.02799088
 8## NAPB            1.83091    0.03786699             1     0.03227985
 9## ST6GALNAC6      2.04196    0.14528431             1     0.11050636
10##            mean.logFC.cohen min.logFC.cohen median.logFC.cohen max.logFC.cohen
11##                   <numeric>       <numeric>          <numeric>       <numeric>
12## GHRL               24.18584        23.74169           24.29966        24.29966
13## VTN                28.37347        16.94799           31.87112        31.87112
14## FRZB                8.26435         6.44749            8.72608         8.72608
15## SYBU               38.59628         5.60475           18.56236        88.93492
16## NAPB                7.80530         5.37676            8.24785         9.73400
17## ST6GALNAC6         19.49805         4.07685            5.69624        88.93492
18##            rank.logFC.cohen  mean.AUC   min.AUC median.AUC   max.AUC  rank.AUC
19##                   <integer> <numeric> <numeric>  <numeric> <numeric> <integer>
20## GHRL                      1  1.000000  1.000000          1         1         1
21## VTN                       1  1.000000  1.000000          1         1         1
22## FRZB                      4  1.000000  1.000000          1         1         1
23## SYBU                      1  0.998219  0.987929          1         1         1
24## NAPB                      6  0.998446  0.990683          1         1         1
25## ST6GALNAC6                2  0.990433  0.928571          1         1         1
26##            mean.logFC.detected min.logFC.detected median.logFC.detected
27##                      <numeric>          <numeric>             <numeric>
28## GHRL                   1.53619           1.000000               1.58496
29## VTN                    1.52593           1.000000               1.58496
30## FRZB                   1.50560           1.000000               1.58496
31## SYBU                   1.46471           1.000000               1.50696
32## NAPB                   1.45248           1.000000               1.52463
33## ST6GALNAC6             1.26321           0.980891               1.22362
34##            max.logFC.detected rank.logFC.detected
35##                     <numeric>           <integer>
36## GHRL                  1.58496                   1
37## VTN                   1.58496                   1
38## FRZB                  1.58496                   1
39## SYBU                  1.58496                   1
40## NAPB                  1.58496                   1
41## ST6GALNAC6            1.58496                   1
1top.cohen.min <- rownames(ordered)[1:length(top.markers)]
2plotHeatmap( sce.baron, 
3             features=top.cohen.min, 
4             order_columns_by="label", 
5             center=TRUE
6           )
1plotGroupedHeatmap( sce.baron, 
2                    features=top.cohen.min, 
3                    group="label", 
4                    center=TRUE
5                  )

GHRL ranks #1, the strongest recovery of the canonical marker across all four lists:

  • GHRL: correctly identified as the single strongest distinguishing gene
  • VTN, FRZB, SYBU, NAPB, CIDEB, ACSL1: recurring, same caveats as above
  • ST6GALNAC6, ITFG1, TMEM87B, SNHG21, TMEM179, PDXDC1, PCYT1A, AHSA1, NUS1: same generic genes seen in the findMarkers.all list
  • MFF, SPTSSB, BMP8B, SGSM1, BACE1, ZNF142, CNOT2, BIRC2, AP3B2: no established epsilon-cell association found; likely noise

compare results

 1dataset <- "Epsilon"
 2namedGenesList <- list( findMarkers.all = top.markers, findMarkers.some = top.markers.some, 
 3  cohen.median = top.cohen.markers, cohen.min = top.cohen.min
 4)
 5venn.diagram( namedGenesList,
 6                  paste0(dataset, "Venn.diagram.tiff"),
 7                  fill=wes_palette("FantasticFox1")[1:length(namedGenesList)],  
 8                  alpha=rep( 0.25, length(namedGenesList) ),
 9                  cex = 2.5, cat.fontface=4,
10                  category.names = names(namedGenesList),
11                  col=wes_palette("FantasticFox1")[1:length(namedGenesList)]
12                )
1## [1] 1
1log.files <- list.files('.', pattern = "*Venn.diagram.*.log", full.names = T)
2file.remove(log.files)
1## [1] TRUE
1cmd <- paste0( 'convert ', dataset, 'Venn.diagram.tiff ', dataset, 'Venn.diagram.png && rm ', dataset, 'Venn.diagram.tiff ' )
2system( cmd )

{.center .fiftypercent}

List GHRL rank Overall signal quality
findMarkers.all 14/25 Weak, canonical marker present but buried
findMarkers.some absent Weakest, canonical marker missing entirely
cohen.median 2/25 Moderate-strong
cohen.min 1/25 Strongest, canonical marker correctly top-ranked

Key finding: on a very rare, small cluster, scoreMarkers's effect-size-based ranking (Cohen's d) recovers the one biologically definitive marker (GHRL) far more reliably than findMarkers's p-value-based approach does, and counterintuitively, the "Most conservative" (cohen.min) criterion does better than the "typical-case" (cohen.median) one here, because GHRL's expression is so dramatically and consistently exclusive to epsilon cells that it wins even the strictest pairwise effect-size comparison.

The likely explanation: p-values are highly sensitive to sample size, and with epsilon cells being such a tiny population, individual pairwise t-tests/Wilcoxon tests lack statistical power even when the underlying effect (expression difference) is huge, this is exactly why GHRL can fail to reach "significant in every comparison" (findMarkers.all) or "significant in most comparisons" (findMarkers.some) despite being the textbook marker. Effect sizes (Cohen's d), by contrast, don't require large sample sizes to be large — a huge mean difference is a huge effect size regardless of n, which is why scoreMarkers recovers it reliably even here.

Practical takeaway: this is a clean, well-documented example of exactly the kind of divergence Baron's rare cell types were meant to surface, findMarkers struggling with statistical power on tiny clusters, scoreMarkers remaining robust because it isn't gated by a significance threshold.

Conclusions

  • For clearly separated clusters with enough cells both approaches work similarly, although Cohen's d seems to work slightly better.
  • Cohen's d has the extra advantange of being faster (or more efficient), compare them on a dataset of hundred thousand cells and you will see.
  • Things get more complicated with small of not clear clusters, see the last example.
  • The number of 'good' marker genes depends on the experimental model and the specific clustering performed.
  • Domain knowledge and previous information on the dataset under study is key to properly understand the clusters identity

Further reading

comments powered by Disqus