Hold out a category and search, from Python

Hold out a category and search

Strategy 01 answers the founding question: is there a combination of measurements and map settings under which a label nobody showed the map falls out as clusters? This notebook runs it on the hyperLOPIT compartment.

In [1]:
from starplast import strategies as Ss = S.get('holdout_search')s
Out[1]:

01 · Hold out a category and search for a map that finds it (UMAP + HDBSCAN) (Search the map space)

Is there a combination of measurements and map settings under which a label nobody showed the map falls out as clusters -- and which unlabelled genes land in them?

Hides one label entirely, walks feature sets and UMAP/HDBSCAN settings, and keeps the map whose clusters best recover that label; unlabelled genes in its pure clusters become candidates, with the shuffled-label score beside them.

Techniques: UMAP, HDBSCAN, Settings walk.
Scorecard (cluster recovery): Weighted F1, Weighted precision, Weighted recall, Adjusted Rand index (ARI), Normalised mutual information (NMI), Homogeneity, Completeness, Unclustered share.

  1. Pick the held-out label. Compartment is the classic first question.
  2. Leave the grids at their defaults for a first pass (4 maps x 2 cluster sizes x 2 selections); widen n_neighbors to '10, 25, 50, 100' once you know the label is recoverable at all.
  3. Set 'Genes per map' to 3000 for speed, 0 for the whole proteome.
  4. Press Test first: it hides a quarter of the label, lets the search choose the map and each label's cluster on the rest, and reports how well those clusters hold the hidden genes, against the same clusters scored on the hidden genes with their labels permuted. A FAIL means the chosen clusters hold the label no better than chance.
  5. Press Run. The best map is drawn, colored by its clusters; 'configurations' ranks every map, 'categories' says which labels were recovered and at what F1.
  6. Read 'label clusters' -- the cluster chosen for each label and how well it isolates it -- then 'calls', the unlabelled genes in those clusters. Click a row to find the gene; treat a call's F1 as the most it can be trusted.

How it is tested: 25% of the label is hidden (whole orthogroups together, so no gene is recovered through a visible paralog). The walk is built as set -- its genes per map (up to 4,000), feature sets and grids (up to three values each); the configuration, and the one cluster that best isolates each label, are both chosen using visible labels only. Metric: for each label, the F1 of its hidden genes against its chosen cluster, weighted by size -- does the structure found on known genes hold the unknown ones? Null: the same chosen clusters scored after permuting the hidden genes' labels, 100 times. (A second search on shuffled labels was the null once; it picks the largest cluster for every label, which scores F1 near 2p by size alone and made the null beat real labels.) Pass: above the null's 95th percentile by at least 0.05.

In [2]:
s.parameters(S.shipped('Tg'))
Out[2]:
name label kind default why
target Held-out label category compartment The label the strategy is scored against and never allowed to see: the column itself, anything that restates it and the experiment that produced it are removed first, by the same closure the Search tab uses.
n_neighbors UMAP neighbours to try grid 15, 50 Comma-separated UMAP n_neighbors values to walk. Small values keep local detail such as complexes; large values keep the global arrangement such as organelles. Every value multiplies the number of maps built.
min_dist UMAP min_dist to try grid 0.0, 0.25 Comma-separated UMAP min_dist values to walk. Zero packs similar genes tightly, which clusters well; larger values spread them out, which reads better and clusters worse. Every value multiplies the number of maps.
min_cluster_size Cluster sizes to try grid 20, 50 Comma-separated HDBSCAN minimum cluster sizes. Small values find small complexes and also split organelles; large values find organelles and swallow complexes. Cheap to widen: clustering is fast next to embedding.
selection Cluster selection to try grid eom, leaf HDBSCAN's cluster selection, comma-separated: 'eom' keeps the most persistent clusters -- on the real proteome two to six very large ones -- and 'leaf' every leaf of the cluster tree, dozens of small purer clusters with more genes left as noise. Walking both costs little.
sample Genes per map int 3000 How many genes each map embeds. Zero embeds all of them; a sample of a few thousand makes a walk of many maps finish in minutes rather than an hour, at the cost of saying nothing about the genes left out.
features Feature sets choice all 'all' walks one map per setting from every permitted measurement; 'families' also walks each kind of measurement alone, so you learn which evidence carries the label -- at several times the cost.
min_f1 Call a label's cluster at F1 float 0.2 Each label's best cluster names its unlabelled members only if it isolates that label at least this well on the known genes. Lower names more clusters and is wrong more often; every call carries its cluster's F1 as its support.

Test before you run

The self-test hides 25% of the compartment labels (whole orthogroups together), lets the search choose the map and each label's cluster on the rest, and scores how well those clusters hold the hidden genes -- against the same search made on shuffled labels.

In [3]:
test = S.test('holdout_search', target='compartment', n_neighbors='15, 50',              min_dist='0.0', min_cluster_size='20, 50', selection='eom, leaf')test
Out[3]:

PASS -- F1 of hidden genes in the cluster chosen for their label on known genes 0.132 against 0.039 ± 0.011 under 100 permutations of the hidden genes' labels over the same chosen clusters (bar 0.058, p 0.0099); effect +0.093, needed +0.050; 607 hidden

Hidden: 25% of the compartment labels, whole orthogroups at a time. Null: 100 permutations of the hidden genes' labels over the same chosen clusters.

Scorecard (cluster recovery)

Weighted F10.132
Weighted precision0.298
Weighted recall0.104
Adjusted Rand index (ARI)0.010
Normalised mutual information (NMI)0.486
Homogeneity0.731
Completeness0.364
Unclustered share0.593

Run it

In [4]:
result = S.run('holdout_search', target='compartment', n_neighbors='15, 50',                min_dist='0.0', min_cluster_size='20, 50', selection='eom, leaf',                sample=2000)print(result.summary)
Out[4]:
Best of 8 configurations: all permitted, n_neighbors 15, min_dist 0.0, min cluster 20 (leaf). Mean F1 0.271 over the compartment labels, against 0.160 ± 0.006 for the same clustering with the labels shuffled; 2 label(s) recovered at F1 >= 0.5, best 60S ribosome at 0.58. 37 unlabelled genes sit in the cluster that best isolates a label at F1 >= 0.20, and are called with it; 'support' is that F1.
In [5]:
result.tables['configurations'].head(8)
Out[5]:
features n_neighbors min_dist min_cluster_size selection clusters noise mean_f1 best_f1 best_label labels_recovered
all permitted 15 0 20 leaf 16 0.597 0.271 0.583 60S ribosome 2
all permitted 50 0 50 leaf 6 0.545 0.268 0.379 nucleus - chromatin 0
all permitted 50 0 20 eom 6 0.312 0.253 0.427 nucleus - chromatin 0
all permitted 50 0 20 leaf 10 0.492 0.253 0.373 nucleus - chromatin 0
all permitted 15 0 50 leaf 6 0.527 0.252 0.35 nucleus - chromatin 0
all permitted 15 0 20 eom 11 0.369 0.246 0.444 rhoptries 2 0
all permitted 50 0 50 eom 3 0.104 0.22 0.426 nucleus - chromatin 0
all permitted 15 0 50 eom 3 0.113 0.217 0.409 nucleus - chromatin 0
In [6]:
result.tables['label clusters']
Out[6]:
label cluster n_label n_in_cluster precision recall f1_on_known_genes
60S ribosome 10 15 14 0.424 0.933 0.583
mitochondrion - membranes 8 31 15 0.652 0.484 0.556
rhoptries 2 2 18 10 0.385 0.556 0.455
cytosol 4 74 16 0.41 0.216 0.283
dense granules 5 36 23 0.177 0.639 0.277
nucleus - chromatin 11 83 19 0.268 0.229 0.247
mitochondrion - soluble 13 55 11 0.282 0.2 0.234
Golgi 6 25 12 0.154 0.48 0.233
In [7]:
result.tables['calls'].head(10)
Out[7]:
gene_id product prediction support cluster
TGME49_310360 ATP synthase-associated protein mitochondrion - membranes 0.556 8
TGME49_310470 cytochrome c oxidase subunit COX2b mitochondrion - membranes 0.556 8
TGME49_249425 hypothetical protein dense granules 0.277 5
TGME49_312950 RAP-1 family protein dense granules 0.277 5
TGME49_215520 hypothetical protein dense granules 0.277 5
TGME49_219730 hypothetical protein dense granules 0.277 5
TGME49_203740 hypothetical protein dense granules 0.277 5
TGME49_260400 hypothetical protein dense granules 0.277 5
TGME49_248360 hypothetical protein dense granules 0.277 5
TGME49_270170 hypothetical protein dense granules 0.277 5
In [8]:
import matplotlib.pyplot as pltplt.style.use('dark_background')ax = result.plot(S.shipped('Tg'), color='compartment')ax.figure
Out[8]: