Edit on GitHub

starplast.strategies

Strategies: named ways of turning the combined data into a claim, each with a test of itself.

A strategy is a way of reasoning from the measurements to something nobody has measured -- hold a category out and look for a map that finds it again, hand over a gene list and look for the one cluster that holds it, carry a label along a crosslink, predict a screen from the other screens. The analysis tabs are the instruments; a strategy is a way of playing them, written down so that it can be repeated, explained and -- the part that matters -- checked.

Every strategy carries four things, and the Strategies tab shows all four:

  • a tooltip that says in one breath what it infers and why that is worth doing;
  • an explanation of why the inference works, what it assumes, and how it fails;
  • a walkthrough: what to choose, what to press, and what to read in the result;
  • a self-test. Known information is hidden, the strategy is asked to recover it from everything else, and the answer is scored against the SAME procedure run on shuffled labels (or random gene sets, or permuted node identities). A strategy passes only when it beats the 95th percentile of its own null by a stated margin. The null is re-run rather than assumed, because every procedure here has a chance level of its own -- a clustering calls some genes right by luck, a random gene list sits in some cluster -- and a raw accuracy without its chance level is the number this project exists not to publish.

The leakage guard is applied before anything else. A strategy scored against compartment never sees compartment, anything measurably restating it, anything the same experiment produced, or an edge layer built from it: :meth:Context.banned and :meth:Context.banned_layers are the project's own closure (search.excluded_detail, search.excluded_edges), not a second copy of it.

The strategies themselves live in strategy_catalog; this module is the machinery they share: the :class:Context they run against, the :class:StrategyResult and :class:TestResult they return, the inference primitives several of them use, and the five holdout test patterns.

Headless and Qt-free, so everything here can be driven from a script or a notebook::

from starplast import strategies
ctx = strategies.Context.shipped("Tg")
s = strategies.get("holdout_search")
print(s.test(ctx).summary())
result = s.run(ctx, target="compartment")
ABSENT = frozenset({'', 'unlabeled', 'unassigned', 'unlabelled', 'unknown', 'nan', 'none', '<na>'})
MIN_CLASS = 15
MIN_HIDDEN = 10
NOISE = -1
NEVER_FEATURES = re.compile('^(gene_id|sequence|product|alphafold_accession|gene_name|symbol)$|^n_papers_|^attention|^lit_|^n_abstracts|^mention')
DERIVED_LAYERS = {'unwritten_interaction': ('xlms', 'ip_ms', 'comention', 'comention_ft'), 'structural_hole': ('coexpression', 'cofitness', 'comention', 'comention_ft')}
LITERATURE_LAYERS = ('comention', 'comention_ft')
PARAM_KINDS = ('category', 'number', 'column', 'layer', 'genes', 'int', 'float', 'grid', 'choice')
def auroc(score, positive) -> float:

Area under the ROC curve: the chance a random positive outranks a random negative.

Missing scores rank below everything, which is what a missing score means here: the method had nothing to say about that gene. Ties are averaged. NaN when either side is empty.

def spearman(a, b) -> float:

Rank correlation over the pairs where both are finite. NaN below five pairs or on a constant.

def bh(p) -> numpy.ndarray:

Benjamini-Hochberg q-values. Every scan here tests many things at once.

def hypergeom_sf(hits: int, draws: int, successes: int, total: int) -> float:

P(at least hits successes in draws draws), exactly. 1.0 when nothing was drawn.

def parse_grid(text, kind=<class 'float'>) -> tuple:

A comma-separated list of values, as the panel's grid boxes hold them. Blanks are skipped.

def expand(values, positions, n: int, fill=-1) -> numpy.ndarray:

Values defined on some genes, written into an array over all n genes.

@dataclass
class StrategyResult:

What one run of a strategy produced, including when that is nothing.

tables are the outputs a person reads, first one first. labels (one per gene, -1 where the gene has none) and coords over positions are what the map can show; both are optional because most strategies produce a list, not a map. genes are the ones worth highlighting.

StrategyResult( strategy: str, summary: str, tables: dict = <factory>, labels: numpy.ndarray | None = None, coords: numpy.ndarray | None = None, positions: numpy.ndarray | None = None, genes: list = <factory>, numbers: dict = <factory>, settings: dict = <factory>, seconds: float = 0.0)
strategy: str
summary: str
tables: dict
labels: numpy.ndarray | None = None
coords: numpy.ndarray | None = None
positions: numpy.ndarray | None = None
genes: list
numbers: dict
settings: dict
seconds: float = 0.0
ok: bool

Whether the run produced anything to read beyond its summary.

def save(self, folder: str) -> list:

Write every table as CSV and the summary and settings as JSON. Returns the paths.

def plot( self, ctx: Context | None = None, color=None, ax=None, size: float = 3.0):

The map this result was computed on, colored by its clusters or by a column of ctx.

Only for results that carry a map (coords); a list-producing strategy has none. Gray is a gene with no cluster or no value -- the application's rule, kept here.

@dataclass
class TestResult:

A strategy's self-test: what was hidden, how much came back, and what luck alone gives.

null holds the metric under the null procedure -- the same strategy on shuffled labels, random gene sets or permuted identities -- so the chance level is measured rather than assumed. Where a re-run is too costly the analytic expectation is used instead and null_kind says so.

TestResult( strategy: str, metric: str, observed: float, null: list, min_effect: float, n_hidden: int, hidden: str, null_kind: str, details: pandas.DataFrame = <factory>, seconds: float = 0.0, note: str = '', quantile: float = 95.0, analytic: tuple | None = None, numbers: dict = <factory>, min_hidden: int = 10, task: str = '', scorecard: dict = <factory>)
strategy: str
metric: str
observed: float
null: list
min_effect: float
n_hidden: int
hidden: str
null_kind: str
details: pandas.DataFrame
seconds: float = 0.0
note: str = ''
quantile: float = 95.0
analytic: tuple | None = None
numbers: dict
min_hidden: int = 10
task: str = ''
scorecard: dict
null_mean: float

The chance level: the mean of the null runs, or the analytic expectation.

null_sd: float

How much the null runs vary, which is what makes a gap over them mean something.

null_high: float

The bar to clear: the quantile-th percentile of the null, analytic or re-run.

effect: float

Observed minus chance: the part of the answer that luck does not explain.

skill: float

(observed - chance) / (1 - chance): 0 is chance, 1 is perfect -- one scale for all.

def card(self) -> pandas.DataFrame:

The scorecard as one table: the verdict block every test shares, then the task's metrics in their standard order, each with its one-line reading.

p_value: float

Share of null runs at least as good as the observed, with the usual +1 correction.

conclusive: bool

Enough was hidden, and a number came back, for the verdict to mean anything.

passed: bool

Beats its own null's high percentile AND by at least the stated margin.

verdict: str

PASS, FAIL or INCONCLUSIVE -- the last when too little could be hidden to score.

def summary(self) -> str:

One line, leading with the verdict and the number it rests on.

def save(self, folder: str) -> list:

Write the verdict as JSON and the per-class details as CSV. Returns the paths.

def to_dict(self) -> dict:

JSON-safe, for the shipped record of what each strategy measured on the real data.

def judge( strategy: str, metric: str, observed: float, null, *, min_effect: float, n_hidden: int, hidden: str, null_kind: str, t0: float, details=None, note: str = '', quantile: float = 95.0, analytic=None, numbers=None, min_hidden: int = 10, task: str = '', scorecard=None) -> TestResult:

Build a :class:TestResult -- the one place a verdict's arithmetic lives.

task names the scorecard task and scorecard carries its metrics; an inconclusive test may leave both empty.

@dataclass(frozen=True)
class Param:

One setting a strategy takes, with the reason it exists.

default may be a callable taking the :class:Context, because the right default depends on the organism -- compartment is the natural held-out label for Toxoplasma and does not exist for Plasmodium.

Param( name: str, kind: str, label: str, tip: str, default: object = None, lo: float = 0, hi: float = 0, step: float = 1, choices: tuple = (), optional: bool = False, space: str = 'this')
name: str
kind: str
label: str
tip: str
default: object = None
lo: float = 0
hi: float = 0
step: float = 1
choices: tuple = ()
optional: bool = False
space: str = 'this'
def resolve(self, ctx) -> object:

The default for this context.

def options(self, ctx) -> tuple:

The fixed choices of a choice parameter; a callable is asked, since some depend on data.

@dataclass
class Strategy:

A named way of inferring something, with its explanation, walkthrough and self-test.

Strategy( key: str, number: int, title: str, family: str, question: str, tooltip: str, explanation: str, walkthrough: tuple, test_description: str, params: tuple, runner: Callable, tester: Callable, cost: str = 'seconds', needs: tuple = (), method: str = '', task: str = '', techniques: tuple = ())
key: str
number: int
title: str
family: str
question: str
tooltip: str
explanation: str
walkthrough: tuple
test_description: str
params: tuple
runner: Callable
tester: Callable
cost: str = 'seconds'
needs: tuple = ()
method: str = ''
task: str = ''
techniques: tuple = ()
name: str

The title with the method it runs in brackets, as every list and heading shows it.

def defaults(self, ctx) -> dict:

Every parameter at its default for this context.

def settings(self, ctx, **overrides) -> dict:

Defaults with overrides applied. An unknown name is an error, not silently ignored.

def run(self, ctx, **params) -> StrategyResult:

Run the strategy on ctx and return what it found.

def test(self, ctx, **params) -> TestResult:

Hide known information, ask the strategy for it back, and score that against its null.

def parameters( self, ctx: Context | None = None) -> pandas.DataFrame:

The settings as a table: name, kind, default (for ctx), and why each exists.

metrics: tuple

The scorecard metrics its self-test reports, in the task's standard order.

def techniques_table(self) -> pandas.DataFrame:

The techniques its method is built from: name, kind, what each does and why.

def scorecard_table(self) -> pandas.DataFrame:

Its scorecard's metrics, each with definition, range, chance level and how to read it.

def help_text(self) -> str:

Everything the panel shows about this strategy, as plain text.

REGISTRY: dict = {'holdout_search': Strategy(key='holdout_search', number=1, title='Hold out a category and search for a map that finds it', family='Search the map space', question='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?', tooltip='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.', explanation="This is the question the map was built to ask. Choose a label -- the hyperLOPIT compartment, a stage, an essentiality class -- and the strategy removes it from the inputs together with everything the leakage closure says restates it: the column, columns measurably associated with it, the experiment that produced it, and any edge layer built from it. What is left is embedded under every combination of UMAP n_neighbors and min_dist on the grid, each map is clustered at each HDBSCAN size, and each clustering is scored by how well single clusters isolate each category (the best cluster's F1 per category, weighted by category size).\n\nWhy it can work: if a compartment is a biological fact, its members share an expression programme, a fitness pattern, a set of modifications -- so they should sit together in a map built only from those. Why it can fail: a walk picks the luckiest of many maps, so the best score is optimistic; every run therefore reports the same clustering's score with the labels shuffled, and the self-test chooses the map on some labels and scores it on labels it never saw. Each label is then given the ONE cluster that best isolates it, and that cluster's unlabelled members are called with it, each call carrying the cluster's F1 -- a pure-looking cluster is a lead, not a result.\n\nChoose 'families' to also walk each kind of measurement alone -- transcription only, fitness only -- which tells you WHICH evidence carries the category, not just whether the combination does.", walkthrough=('Pick the held-out label. Compartment is the classic first question.', "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.", "Set 'Genes per map' to 3000 for speed, 0 for the whole proteome.", "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.", "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.", "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."), test_description="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.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='n_neighbors', kind='grid', label='UMAP neighbours to try', tip='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.', default='15, 50', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='min_dist', kind='grid', label='UMAP min_dist to try', tip='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.', default='0.0, 0.25', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='min_cluster_size', kind='grid', label='Cluster sizes to try', tip='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.', default='20, 50', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='selection', kind='grid', label='Cluster selection to try', tip="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.", default='eom, leaf', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='features', kind='choice', label='Feature sets', tip="'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.", default='all', lo=0, hi=0, step=1, choices=('all', 'families'), optional=False, space='this'), Param(name='min_f1', kind='float', label="Call a label's cluster at F1", tip="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.", default=0.2, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this')), runner=<function _holdout_search_run>, tester=<function _holdout_search_test>, cost='minutes', needs=('a categorical column',), method='UMAP + HDBSCAN', task='cluster recovery', techniques=('umap', 'hdbscan', 'settings_walk')), 'geneset_hunt': Strategy(key='geneset_hunt', number=2, title='Find the map where your gene list is one cluster', family='Search the map space', question='Under some combination of measurements and settings, do the genes on my list fall into a single cluster -- and what else is in it?', tooltip="Walks maps and clusterings looking for one cluster with high precision AND recall for your list; reports that cluster's other members as candidates, and how often a random list of the same size does as well over the same walk.", explanation="Hand over a list -- the hits of a screen, the members of a complex, genes a paper implicates -- and the strategy walks the same space the Search tab walks, but scores each clustering by the single cluster that best captures YOUR list: precision (what share of the cluster is on the list) and recall (what share of the list is in the cluster), combined as F1. A list that is a real biological unit should, under some view of the data, gather into one cluster; the genes that gather with it are the candidates.\n\nThe trap is the walk itself: over many maps and cluster sizes SOMETHING will collect a fair share of any list. So every run also scores random lists of the same size over the same maps and reports the best each achieves -- a search-corrected p-value. A list that does no better than random lists is not a unit this data can see.\n\nIf the list was made from a column of this table (all genes of one compartment, say), name that column under 'Column the list came from': it is then withheld, as a held-out label would be, and the answer is not simply the column read back.", walkthrough=('Paste your accessions into the gene list box (one per line, or comma separated).', 'If the list came from a column of this table, choose it as the column to exclude.', 'Press Test: with a list of 20 or more genes it hides 30% of YOUR list and asks whether the best cluster for the rest contains them; with a shorter list it tests on a known category of similar size. A FAIL on your own list means it is not coherent in this data.', "Press Run. The winning map is drawn; the table 'configurations' shows every map's best cluster with precision, recall and F1.", "Read the summary's search-corrected p-value before the candidates: it is the chance a random list of the same size does as well over the same walk.", "'candidates' lists the cluster's other members, nearest the cluster centre first."), test_description="30% of the set is hidden; the walk as set (genes per map up to 4,000, feature sets, up to three values of each grid) picks the cluster with the best F1 for the other 70%. Metric: F1 of the hidden members against that cluster's other genes -- precision is the share of the cluster's candidates that are hidden members, recall the share of hidden members among them. Null: 20 random sets of the same size through the same walk. Pass: above the null's 95th percentile by at least 0.05 (an F1 margin; random sets score about 0.04).", params=(Param(name='genes', kind='genes', label='Gene list', tip="Gene accessions, one per line or separated by commas or spaces, pasted from anywhere. Matched exactly against this organism's table, ignoring case; anything not found is reported by name rather than silently dropped.", default='', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='exclude', kind='category', label='Column the list came from', tip='If the gene list was made from a column of this table, name it here and that column, everything restating it and its experiment are withheld; otherwise the strategy would find the list again by reading the column that defined it.', default=None, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='n_neighbors', kind='grid', label='UMAP neighbours to try', tip='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.', default='15, 50', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='min_dist', kind='grid', label='UMAP min_dist to try', tip='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.', default='0.0, 0.25', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='min_cluster_size', kind='grid', label='Cluster sizes to try', tip='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.', default='20, 50', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='selection', kind='grid', label='Cluster selection to try', tip="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.", default='eom, leaf', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='features', kind='choice', label='Feature sets', tip="'all' uses every permitted measurement in each map; 'families' also tries each kind of measurement alone, which can find a list that is coherent in one kind of evidence and invisible in the combination.", default='all', lo=0, hi=0, step=1, choices=('all', 'families'), optional=False, space='this'), Param(name='random_sets', kind='int', label='Random lists for the null', tip='How many random lists of the same size are scored over the same walk to give the search-corrected p-value. More is steadier and costs little, because the maps are already built.', default=100, lo=10, hi=2000, step=10, choices=(), optional=False, space='this')), runner=<function _geneset_hunt_run>, tester=<function _geneset_hunt_test>, cost='minutes', needs=('a gene list',), method='UMAP + HDBSCAN', task='set retrieval', techniques=('umap', 'hdbscan', 'settings_walk')), 'recoverability_atlas': Strategy(key='recoverability_atlas', number=3, title='Ask which categories the data can rediscover', family='Search the map space', question='Of all the categories of a label, which ones do the measurements actually encode -- and which would no map, however tuned, ever find?', tooltip="Builds one map blind to a label and scores every category of that label by how strongly its members' map neighbours share it; the result is an atlas of what this data knows and does not know, checked on hidden genes.", explanation="Before searching for a structure it is worth knowing whether there is one to find. This strategy builds a single map with the chosen label and its closure withheld, then asks, category by category, how much the map neighbourhood of a labelled gene is enriched for its own category (AUROC of the neighbour share, and its precision lift over the category's prevalence). Categories with high scores are ENCODED in the measurements -- their members behave alike -- and are worth searching for or predicting. Categories near 0.5 are not: no map built from these data will recover them, and a claimed cluster for one is noise.\n\nThis is knowledge about the data, not about genes: it says which biological distinctions leave a trace in expression, fitness, modification and structure, and which live only in the experiment that defined them. The self-test checks that the atlas generalises: the categories it ranks highest from known genes must be the ones best recovered for genes whose labels were hidden.", walkthrough=('Choose the label to map (compartment, stage, phenotype class...).', "Keep 'Genes per map' at 3000 for a quick atlas, or 0 for the whole proteome.", 'Press Test to confirm the atlas generalises to hidden genes on this label.', "Press Run and read 'categories', sorted by AUROC; 'encoded' marks the ones worth pursuing.", 'Take an encoded category to strategy 01 or 08 for predictions; do not spend a search on a category the atlas calls not encoded.'), test_description="30% of the label is hidden. The atlas is built from visible genes only (leave-one-out neighbour AUROC per category); hidden genes are then scored by their visible neighbours. Metric: mean hidden-gene AUROC over the categories the atlas ranks in its top half. Null: the same with the visible labels shuffled, 20 times. Pass: above the null's 95th percentile by 0.05. The rank agreement between atlas and hidden recovery is reported.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this')), runner=<function _atlas_run>, tester=<function _atlas_test>, cost='a minute', needs=('a categorical column',), method='UMAP + neighbour AUROC', task='ranking', techniques=('umap', 'knn')), 'consensus_modules': Strategy(key='consensus_modules', number=4, title='Keep only the modules that survive the whole walk', family='Search the map space', question='Which groups of genes stay together whatever map settings are chosen -- the structure that is in the data rather than in one lucky configuration?', tooltip='Clusters many maps, counts how often each pair of genes lands in the same cluster, and keeps the groups that co-cluster in most of them; stable modules are claims about the genes, unstable ones are claims about a setting.', explanation="Any single map is one choice among many. Change n_neighbors from 15 to 50 and some clusters survive while others dissolve or merge. This strategy treats that as the signal: it walks the grid, clusters every map, and builds a co-association matrix -- the share of clusterings in which each pair of genes was placed together. Modules are groups whose members co-cluster in at least the chosen share of maps (average linkage on one minus co-association).\n\nA module that survives is robust to the arbitrary choices a map requires, which is the minimum a structure must be before it is interpreted. It is also label-free: no held-out label is used to build it, so any label it explains afterwards is a genuine finding. The self-test predicts hidden labels from each module's visible majority. Choosing a held-out label also removes that label's closure from the maps, so modules can then be read against it honestly.", walkthrough=("Optionally choose a label to hold out; its closure is removed from every map, and the modules table then reports each module's dominant label.", 'Widen the grids (three n_neighbors, two min_dist, two cluster sizes is a good start).', 'Set the co-clustering threshold: 0.5 keeps pairs together in most maps; 0.8 keeps only the rock-solid core.', "Press Test, then Run. The map shows modules; 'modules' lists stability and dominant label.", 'Treat a module with high stability and no dominant known label as a candidate new unit.'), test_description="Modules are built from a small walk on 1,500 genes without the label. 25% of the label is hidden; the module that best isolates each label is chosen on the visible genes. Metric: the size-weighted F1 of each label's hidden genes against its chosen module. Null: the same modules scored after permuting the hidden genes' labels, 100 times -- a large module scores the same either way and earns nothing. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label (optional)', tip='A label whose closure is removed from every map, and which the modules are then read against. Leave empty to build modules from everything; the self-test then uses the default label.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='n_neighbors', kind='grid', label='UMAP neighbours to try', tip='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.', default='15, 30, 50', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='min_dist', kind='grid', label='UMAP min_dist to try', tip='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.', default='0.0, 0.25', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='min_cluster_size', kind='grid', label='Cluster sizes to try', tip='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.', default='20, 50', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='selection', kind='grid', label='Cluster selection to try', tip="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.", default='eom, leaf', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='threshold', kind='float', label='Co-clustering threshold', tip='The share of clusterings in which two genes must be placed together to be in the same module. Higher is stricter: fewer, smaller, more trustworthy modules.', default=0.5, lo=0.2, hi=0.95, step=0.05, choices=(), optional=False, space='this'), Param(name='min_module', kind='int', label='Smallest module', tip='Modules smaller than this are dropped as noise. A module of three genes that co-cluster is a coincidence as often as it is a complex.', default=10, lo=3, hi=500, step=1, choices=(), optional=False, space='this')), runner=<function _consensus_run>, tester=<function _consensus_test>, cost='minutes', needs=(), method='UMAP + HDBSCAN co-clustering', task='cluster recovery', techniques=('umap', 'hdbscan', 'settings_walk', 'co_association')), 'blind_battery': Strategy(key='blind_battery', number=5, title='Tune a map without labels, then read what it encodes', family='Search the map space', question='If I build a map from one kind of evidence only -- expression, say -- and tune it for structure alone, which OTHER measurements do its clusters turn out to separate?', tooltip='Builds a map from one family of measurements, tunes it only for cluster structure, then tests every measurement it never saw against its clusters; replicated associations say what that kind of evidence encodes about the rest of biology.', explanation="The other strategies start from a question. This one starts from the data and lets the question come back. Choose one family of measurements -- transcription, fitness, modification -- and a map is built from that family only, tuned by the project's map-quality score (how much of the proteome clusters, how evenly) without reference to any label. Then the battery runs: every categorical and numeric column the map did NOT use is tested against its clusters (chi-square with Cramér's V, or Kruskal-Wallis with an eta-squared effect), and the p-values are corrected across the whole family.\n\nAn association here is evidence of coupling between two kinds of biology: clusters of co-expressed genes that differ in fitness say that transcriptional programmes track essentiality; clusters that differ in compartment say expression encodes localization. Because nothing about the held-out features shaped the map, this cannot be circular -- but it CAN be a large-sample artefact, which is why the self-test demands the associations replicate on genes the discovery never used.", walkthrough=("Choose the family to build the map from ('transcription' is the richest).", 'Press Test: associations are found on half the genes and checked on the other half.', "Press Run and read 'held-out features' by q-value; the effect column says how strong.", "Click the map's clusters against the top features (color by the feature in the left panel) to see what the association looks like.", 'Repeat with another family and compare: what fitness encodes and what expression encodes are different answers.'), test_description="Pattern 5. One label-free map on up to 2,000 genes. Held-out features significant at q < 0.05 on a random half of the genes are the findings; each is re-tested (p < 0.05) on the other half. Metric: share that replicate. Null: the same with the second half's cluster labels permuted, 10 times. Pass: above the null's 95th percentile by 0.2, with at least three findings.", params=(Param(name='map_from', kind='choice', label='Build the map from', tip='The family of measurements the map is built from; everything else becomes a held-out feature to read it with. Families are the kinds of evidence the table carries: transcription, translation, fitness, modification and so on.', default=<function _family_default>, lo=0, hi=0, step=1, choices=<function <lambda>>, optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this')), runner=<function _battery_run>, tester=<function _battery_test>, cost='a minute', needs=(), method='UMAP + HDBSCAN, chi-square / Kruskal-Wallis', task='replication', techniques=('umap', 'hdbscan', 'map_quality', 'chi_square', 'kruskal_wallis', 'bh_fdr')), 'block_ablation': Strategy(key='block_ablation', number=6, title='Find which kind of evidence carries a label', family='Search the map space', question='Which measurements actually carry the information about this label -- and which are redundant with others or irrelevant to it?', tooltip='Scores each kind of measurement alone and the combination without it, out of fold, for predicting a held-out label; says which experiments encode which biology and which could be dropped without loss.', explanation="A map is built from dozens of datasets, and a good result says nothing about which of them produced it. This strategy asks directly. For each kind of evidence (a family such as transcription or fitness, or each individual block), it measures how well a 15-nearest-neighbour vote in that evidence alone predicts the held-out label, cross-validated with whole orthogroups held out together; then how much the combination loses when that evidence is removed.\n\n'Alone' measures how much a kind of evidence knows; 'loss when removed' measures how much it knows that nothing else does. A dataset strong alone but costing nothing when removed is redundant with another; one weak alone but costly to remove carries something unique. Both are knowledge about the experiments, and they decide where the next experiment should go. The self-test checks that the ranking is real: the evidence ranked first on the visible labels must predict the HIDDEN labels better than a kind of evidence picked at random.", walkthrough=('Choose the label.', "Choose 'family' for a quick answer by kind of measurement, 'block' for every dataset (slower, many more rows).", 'Press Test, then Run.', "Read 'evidence' sorted by 'alone'; then sort by 'loss_when_removed' to find the unique contributors.", "Use the answer to build a focused map in strategy 01 with 'families', or to decide which dataset a new experiment should extend."), test_description="25% of the label is hidden. Each kind of evidence is ranked by cross-validated accuracy on the visible labels; the top-ranked one then predicts the hidden labels. Metric: its hidden accuracy. Null: the hidden accuracy of every kind of evidence, i.e. choosing at random. Pass: above the null's 80th percentile by 0.02 (with ~15 kinds of evidence the 95th would demand the single best, which asks more than a ranking must deliver).", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='unit', kind='choice', label='Unit of evidence', tip="'family' groups the datasets by kind of measurement -- a dozen or so rows, quick. 'block' scores every dataset on its own -- a hundred rows, several minutes, and the level at which an experiment can be named.", default='family', lo=0, hi=0, step=1, choices=('family', 'block'), optional=False, space='this')), runner=<function _ablation_run>, tester=<function _ablation_test>, cost='a minute', needs=('a categorical column',), method='kNN ablation', task='label calls', techniques=('knn', 'ablation', 'grouped_cv')), 'feature_knn': Strategy(key='feature_knn', number=7, title='Call a gene by the genes that behave like it', family='Borrow from neighbours', question='For a gene with no label, what label do the genes most similar to it across every permitted measurement carry?', tooltip="Finds each unlabelled gene's nearest labelled genes across all permitted measurements at once and calls it by their distance-weighted vote -- the plainest guilt-by-association, with no map and no clustering between the data and the answer.", explanation="Every map and every clustering is an approximation of one thing: which genes are similar. This strategy skips the approximation. Each gene is a point in the space of every permitted measurement (rank-scaled, so no screen dominates by units); its k nearest labelled genes vote, weighted by closeness, and the gene is called with the winning label when that label holds at least the chosen share of the vote.\n\nIt is the baseline every other strategy has to beat: if a sophisticated method cannot do better than asking the neighbours, the sophistication is decoration. It is also transparent -- each call can be traced to the genes that made it. Its weakness is the curse of dimension: with hundreds of measurements, many of them missing and filled at the median, 'nearest' can be dominated by missingness rather than biology, which is why the self-test hides whole orthogroups and demands a margin over shuffled labels.", walkthrough=('Choose the label.', 'Leave k at 15; raise the vote threshold to 0.7 when you want fewer, safer calls.', 'Press Test: the number is the share of hidden genes called correctly, abstentions counted as misses, against shuffled labels.', "Press Run and read 'calls', strongest vote first. Click a row to find the gene.", 'Compare with strategies 08 and 11: a call that the measurements, the map and a network all agree on is strategy 31.'), test_description="Pattern 1: 25% of the label hidden by whole orthogroups; each hidden gene called by its k nearest visible genes with the same vote threshold. Metric: hidden genes called correctly. Null: 10 runs with visible labels shuffled. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='min_share', kind='float', label='Call when the vote is at least', tip='The share of the neighbourhood vote the winning label must hold before a gene is called. Higher calls fewer genes and fewer of them wrongly; the self-test uses the same threshold, so its number describes exactly these calls.', default=0.3, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this')), runner=<function _knn_run>, tester=<function _knn_test>, cost='seconds', needs=('a categorical column',), method='kNN', task='label calls', techniques=('knn',)), 'map_neighbours': Strategy(key='map_neighbours', number=8, title='Call a gene by its neighbours on the map', family='Borrow from neighbours', question="On a map built without the label, which label do a gene's nearest placed neighbours carry?", tooltip="Builds one map blind to the label and calls each unlabelled gene by the vote of its nearest labelled neighbours in the map's three dimensions -- what you do by eye when you hover over an unlabelled point, made systematic and scored.", explanation="When you look at the map colored by compartment and see a grey point inside a blue cloud, you are making this inference. The strategy makes it for every gene: a map is built with the label and its closure withheld, and each unlabelled gene is called by the distance-weighted vote of its k nearest labelled genes in the map.\n\nThe difference from strategy 07 is the map. UMAP keeps local neighbourhoods and discards much of the noise in hundreds of partly missing measurements, so map neighbours can be more reliable than raw ones -- or less, when the projection tears a category apart. Which is true for a given label is exactly what running both self-tests tells you. The map is a sample unless 'Genes per map' is zero, and genes outside it are not called at all.", walkthrough=('Choose the label; keep the map settings unless the atlas (03) suggested others.', "Press Test; compare its number with strategy 07's on the same label.", "Press Run: the map is drawn with its clusters, and 'calls' lists the calls.", 'Color the map by the label to see each call in context; click a call to find it.'), test_description="Pattern 1 on the genes the map places (up to 2,500): 25% of the label hidden by whole orthogroups; hidden genes called by their k nearest visible genes in the map. Null: 20 runs on shuffled visible labels. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=4000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='n_neighbors', kind='int', label='UMAP neighbours', tip='The UMAP n_neighbors of the map the neighbours are read from. Larger values give a map whose distances reflect broad organisation; smaller ones, local detail.', default=25, lo=2, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='min_dist', kind='float', label='UMAP min_dist', tip='The UMAP min_dist of that map. Small values pack similar genes together, which is what a neighbourhood vote wants.', default=0.1, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='min_share', kind='float', label='Call when the vote is at least', tip='The share of the neighbourhood vote the winning label must hold before a gene is called. Higher calls fewer genes and fewer of them wrongly; the self-test uses the same threshold, so its number describes exactly these calls.', default=0.3, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this')), runner=<function _map_nn_run>, tester=<function _map_nn_test>, cost='a minute', needs=('a categorical column',), method='UMAP + kNN', task='label calls', techniques=('umap', 'knn')), 'cluster_guilt': Strategy(key='cluster_guilt', number=9, title='Name a cluster by the label it is enriched for', family='Borrow from neighbours', question='Which clusters of a label-blind map hold one label far more often than chance, and what does that make of their unlabelled members?', tooltip='Clusters a label-blind map, tests every cluster against every label for enrichment (hypergeometric, BH-corrected), and calls the unlabelled members of significantly enriched clusters -- guilt by cluster, with a p-value rather than a majority.', explanation="A majority is not evidence: a cluster of five genes, three of them nuclear, is 60% nuclear by chance in a proteome where a fifth of labelled genes are. This strategy asks the statistical question instead. For each cluster and each label it computes the hypergeometric probability of seeing that many of the label among the cluster's labelled members, corrects across every cluster-label pair tested, and calls a cluster only when the label is both significant and enriched by at least the chosen lift over its share of the proteome.\n\nEnrichment is also how a cluster can be NAMED without being pure: a cluster that is 35% apicoplast when apicoplast is 3% of the proteome is an apicoplast-flavoured cluster, and its unlabelled members deserve a look even though most labelled members are elsewhere. That is also its risk, which is why the self-test scores exactly these calls on hidden genes.", walkthrough=('Choose the label and a cluster size (25 is the project default).', 'Press Test, then Run.', "Read 'enrichment': every cluster-label pair with its lift and q-value, called ones marked.", "Read 'calls' for the unlabelled members of called clusters; sort by 'support' (the lift)."), test_description="Pattern 1 scored by precision, on the genes a blind map places (up to 2,500): 25% of the label hidden; the enrichment is recomputed from visible labels, and the calls it makes on hidden genes are scored -- the strategy abstains on noise and unenriched clusters by design, so what matters is how often a call is right. Null: 20 runs on shuffled visible labels. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='min_cluster_size', kind='int', label='Cluster size', tip="HDBSCAN's minimum cluster size for the one map this strategy reads. Enrichment needs clusters big enough to hold several labelled genes, so very small values call almost nothing.", default=25, lo=5, hi=500, step=1, choices=(), optional=False, space='this'), Param(name='selection', kind='choice', label='Cluster selection', tip="HDBSCAN's cluster selection for the one map this strategy reads. 'leaf' gives many small clusters, which is what naming a cluster needs; 'eom' gives a few very large ones, which on this proteome no single label dominates.", default='leaf', lo=0, hi=0, step=1, choices=('leaf', 'eom'), optional=False, space='this'), Param(name='min_lift', kind='float', label='Minimum lift', tip='How many times its proteome-wide share a label must be over-represented in a cluster before the cluster is named for it. Two is conservative for large classes and generous for rare ones.', default=2.0, lo=1.0, hi=20.0, step=0.5, choices=(), optional=False, space='this')), runner=<function _guilt_run>, tester=<function _guilt_test>, cost='a minute', needs=('a categorical column',), method='UMAP + HDBSCAN, hypergeometric', task='label calls', techniques=('umap', 'hdbscan', 'hypergeometric', 'bh_fdr')), 'label_outliers': Strategy(key='label_outliers', number=10, title='Find genes whose label their neighbours contradict', family='Borrow from neighbours', question='Which labelled genes sit among genes that almost all carry a different label -- possible mislabels, dual-localized or moonlighting proteins?', tooltip='Scores every labelled gene by how little its measurement neighbours and network partners share its label; the top of the list is where an annotation is wrong, a protein has two lives, or the measurements are strange.', explanation='Every other strategy trusts the labels and predicts the missing ones. This one turns the question round: given everything else, which EXISTING labels look wrong? For each labelled gene it measures the share of its k nearest labelled genes (in the permitted measurements) that carry its own label, and the share of its labelled partners across the measured networks that do; surprise is one minus their mean.\n\nA high-surprise gene is one of three things, and each is worth knowing: an annotation error (hyperLOPIT assigns by a classifier and is wrong some fraction of the time); a protein with two locations or functions, which the neighbourhood sees as the other one; or a gene whose measurements are unusual, which is itself a finding. The self-test corrupts 5% of the labels on purpose and asks whether those genes rise to the top.', walkthrough=('Choose the label to audit.', 'Press Test: the AUROC says how well the method finds labels that were deliberately swapped; 0.5 would be no better than a random list.', "Press Run; read 'surprising genes' from the top, with the label their neighbours carry instead in 'alternative'.", "Check the top genes' evidence panel before believing either label."), test_description="5% of the labels (at least ten) are swapped to a wrong class, drawn in proportion to class size. Surprise is computed with the corrupted labels. Metric: AUROC of surprise for the swapped genes among all labelled genes. Null: 20 random sets of the same size. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='top', kind='int', label='Genes to list', tip='How many of the most surprising genes to list. The ranking is over every labelled gene; this only limits how much of it is shown and saved.', default=200, lo=10, hi=5000, step=10, choices=(), optional=False, space='this')), runner=<function _outliers_run>, tester=<function _outliers_test>, cost='seconds', needs=('a categorical column',), method='kNN + network neighbours', task='ranking', techniques=('knn', 'network_vote')), 'layer_propagation': Strategy(key='layer_propagation', number=11, title='Diffuse a label across one measured network', family='Walk the networks', question='If labels flow along the edges of one kind of measured relationship, where do they end up -- and how much of a label does that relationship carry?', tooltip='Seeds each label on the genes that carry it and lets it diffuse along one edge layer by random walk with restart; genes are called by the label whose field reaches them most strongly. Also measures how much one kind of relationship knows.', explanation="A network carries a label if genes linked in it tend to share the label. Diffusion exploits that without needing a cluster to form: every class is seeded on its labelled genes, the seed spreads along the layer's edges (degree-normalised, so hubs -- often the most-studied genes -- do not swallow everything), and after the walk settles each gene is called by the strongest field. Genes the layer does not reach are not called.\n\nRun it layer by layer and the self-test numbers become a table of which relationships encode which biology: crosslinks should carry compartment strongly, co-fitness should carry pathway, structural similarity should carry function. A layer built from the label (the compartment layer, for compartment) is refused -- it would recover the label perfectly and mean nothing.", walkthrough=('Choose the label and a layer.', 'Press Test for each layer you are curious about and note the numbers: that is how much each kind of relationship knows about the label.', "Press Run on the best layer and read 'calls'.", 'Lower the restart to let labels travel further (more calls, less certain).'), test_description="Pattern 1: 25% of the label hidden by whole orthogroups; the fields are seeded from visible genes only, so a hidden gene never seeds its own call. Metric: hidden genes called correctly (unreached genes count as misses). Null: 10 runs on shuffled labels. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='layer', kind='layer', label='Edge layer', tip='The measured network the label is walked across. Each layer is a different kind of evidence -- co-expression, co-fitness, crosslinks, structural similarity -- and a layer built from the held-out label itself is refused.', default=<function _layer_default.<locals>.pick>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='restart', kind='float', label='Restart probability', tip='At each step the walk returns to the seeds with this probability. High values keep labels close to where they started; low values let them travel several edges, which reaches more genes with weaker evidence.', default=0.5, lo=0.05, hi=0.95, step=0.05, choices=(), optional=False, space='this')), runner=<function _propagation_run>, tester=<function _propagation_test>, cost='seconds', needs=('a categorical column', 'an edge layer'), method='random walk with restart', task='label calls', techniques=('random_walk_restart',)), 'layer_vote': Strategy(key='layer_vote', number=12, title='Let every network vote, weighted by what it has earned', family='Walk the networks', question="If every measured relationship and the measurements themselves vote on a gene's label, each weighted by how good it has proven to be, what is the verdict?", tooltip="Each permitted edge layer and the measurement-space neighbours vote on every gene; each source's vote is weighted by how far it beat chance on an inner holdout of the known labels, so the evidence that has earned trust counts most.", explanation="No single network reaches every gene, and no single network is right about every label. This strategy asks all of them. Each permitted layer contributes its neighbour vote and the measurement space contributes its k-nearest-neighbour vote; before voting, each source is scored on an inner holdout carved from the known labels, and its weight is its accuracy MINUS its own chance level -- a source that only guesses the commonest class earns nothing.\n\nThe result reaches more genes than any one layer and is steered by the sources that know the label. The 'source weights' table is itself a finding: it ranks the kinds of evidence by how much they know about this label, net of chance -- and a layer with zero weight is one this label is invisible in, however many edges it has.", walkthrough=('Choose the label.', 'Press Test (five shuffled-label repeats; a minute or two on the full table).', "Press Run and read 'source weights' first: zero weight means a source knew nothing beyond chance about this label.", "Read 'calls'; support is the winning label's share of the weighted vote."), test_description="Pattern 1: 25% of the label hidden; the weights are learned on an inner holdout of the visible labels only, then hidden genes are called. Null: 5 runs with visible labels shuffled (weights relearned each time). Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this')), runner=<function _vote_run>, tester=<function _vote_test>, cost='a minute', needs=('a categorical column',), method='chance-weighted ensemble vote', task='label calls', techniques=('knn', 'network_vote', 'chance_weighting')), 'physical_partners': Strategy(key='physical_partners', number=13, title='Place a protein by the proteins it physically touches', family='Walk the networks', question='For a protein crosslinked to or pulled down with labelled proteins, what does its physical company say about where it lives and what it joins?', tooltip='Calls genes by the labels of their measured physical partners -- crosslinks and replicated pulldowns -- weighted by how many times the contact was seen, and names the partners behind every call so each can be checked.', explanation="Two proteins crosslinked by DSS were within about 30 Å of each other in the parasite: they share a compartment, and often a complex. That makes a crosslink the most direct evidence this table has about where an unlocalized protein lives, and unlike co-mention it is not biased toward famous genes. This strategy calls every protein with at least one measured physical partner by the weighted label vote of its partners (crosslink or pulldown counts as weights), and lists the partners so each call can be checked by hand.\n\nIts reach is limited to proteins in the interactomes -- a few thousand at most -- and it inherits the crosslinker's chemistry (lysines, soluble proteins). Within that reach it is expected to be the most precise localization evidence available, and the self-test measures exactly that reach.", walkthrough=('Choose the label (compartment is the natural one).', 'Press Test: only hidden genes with a physical partner are scored.', "Press Run and read 'calls'; the 'partners' column names each partner, its label and the number of contacts.", "For a call that matters, open the partners' evidence and the StarPath interaction page."), test_description="Pattern 1 restricted to genes with at least one physical partner: 25% of the label hidden by whole orthogroups; hidden genes called by their visible partners' weighted vote. Null: 20 runs on shuffled labels. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'),), runner=<function _physical_run>, tester=<function _physical_test>, cost='seconds', needs=('a crosslink or IP-MS layer',), method='weighted partner vote', task='label calls', techniques=('network_vote',)), 'structural_homology': Strategy(key='structural_homology', number=14, title='Annotate function through shared fold', family='Walk the networks', question="What does a protein's fold -- its structural similarity to annotated proteins -- say about its enzymatic class or domain family, even without sequence homology?", tooltip='Transfers a functional annotation (EC class by default) along the structural-similarity layer from predicted models, calling unannotated and hypothetical proteins by their structural neighbours; fold is conserved long after sequence is not.', explanation='Sequence-based annotation fails exactly where this parasite is most interesting: in lineage-specific proteins whose sequence resembles nothing annotated. Fold outlives sequence, and the structural-similarity layer (Foldseek TM-score between predicted models) links proteins that share a fold whatever their sequence. This strategy carries a functional annotation -- the EC number truncated to a chosen depth, or any label -- along that layer, weighted by TM-score, to proteins that lack it.\n\nDepth matters: EC level 1 (oxidoreductase, transferase, hydrolase...) is what a shared fold reliably predicts; level 3 or 4 asks the fold to know the substrate, which it often does not. The self-test scores exactly the chosen depth on hidden annotated proteins that have structural neighbours, so the number tells you how deep to trust.', walkthrough=('Leave the annotation column at ec_number and level 1 for a first pass.', 'Press Test; then raise the level to 2 and test again to see how fast trust decays.', "Press Run; 'calls' puts hypothetical proteins first.", "Check a call against the structure: the neighbours' models are in the evidence panel."), test_description="Pattern 1 restricted to proteins with a structural neighbour: 25% of the annotation (at the chosen level) hidden by whole orthogroups; hidden proteins called by their visible structural neighbours. Null: 20 runs on shuffled annotations. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='column', label='Annotation to transfer', tip='The functional annotation carried along the structural layer: an EC number by default, or any column of labels. It is truncated to the chosen level before anything is compared.', default=<function _ec_default>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='level', kind='int', label='Annotation depth', tip='How many dot-separated parts of the annotation to keep: 1 is the enzyme class, 4 the exact reaction. Zero keeps the whole label. Deeper is more specific and less often predictable from fold.', default=1, lo=0, hi=4, step=1, choices=(), optional=False, space='this'), Param(name='layer', kind='layer', label='Structural layer', tip="The edge layer of structural similarity. Any layer can be chosen, but the strategy's reasoning -- fold outlives sequence -- applies to the structural one.", default=<function _layer_default.<locals>.pick>, lo=0, hi=0, step=1, choices=(), optional=False, space='this')), runner=<function _struct_run>, tester=<function _struct_test>, cost='seconds', needs=('a structural-similarity layer',), method='TM-score-weighted vote', task='label calls', techniques=('tm_score', 'network_vote')), 'multiplex_modules': Strategy(key='multiplex_modules', number=15, title='Find the communities several networks agree on', family='Walk the networks', question='Which groups of genes are communities in more than one kind of measured relationship at once -- co-expressed AND co-fit AND crosslinked?', tooltip='Finds communities in each permitted edge layer and keeps the groupings the layers agree on, as a consensus rather than a merged graph; a module several independent relationships agree on is far more likely to be a real biological unit.', explanation="Merging networks into one graph lets the densest layer decide everything. This strategy does the opposite: communities are found in each layer separately (modularity optimisation), and two genes end up in the same multiplex module only when a sufficient share of the layers that cover both put them together. The result is a set of modules supported by several independent kinds of evidence.\n\nModules are label-free, so a label they explain afterwards is a genuine finding; they are also a list of candidate complexes and pathways to read directly. The self-test chooses the community that best isolates each label on the known genes, and asks how well it holds that label's hidden genes -- which checks that the agreement carries biological signal rather than a shared artefact of the layers, such as a common bias toward abundant proteins.", walkthrough=('Choose a label to read the modules against (it is also withheld from the layers).', 'Keep resolution 1 and agreement 0.5 for a first pass.', 'Press Test, then Run.', "Read 'modules': size, dominant label and share. A large module with no dominant label is worth opening gene by gene."), test_description="On genes placed in a community: 25% of the label hidden; the community that best isolates each label is chosen on the visible genes (the communities themselves never see labels). Metric: the size-weighted F1 of each label's hidden genes against its chosen community. Null: the same communities scored after permuting the hidden genes' labels, 100 times. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='resolution', kind='float', label='Resolution', tip="Modularity resolution for each layer's communities. Above 1 gives more, smaller communities; below 1 fewer, larger ones.", default=1.0, lo=0.1, hi=5.0, step=0.1, choices=(), optional=False, space='this'), Param(name='agreement', kind='float', label='Layer agreement', tip='The share of the layers covering two genes that must put them in one community before the consensus does. Higher is stricter and leaves more genes unplaced.', default=0.5, lo=0.1, hi=1.0, step=0.05, choices=(), optional=False, space='this')), runner=<function _multiplex_run>, tester=<function _multiplex_test>, cost='a minute', needs=('two or more edge layers',), method='modularity + Louvain consensus', task='cluster recovery', techniques=('greedy_modularity', 'louvain')), 'link_prediction': Strategy(key='link_prediction', number=16, title='Predict the contacts an interactome missed', family='Walk the networks', question='Which pairs of proteins are probably in physical contact although the crosslinking or pulldown experiment never saw them together?', tooltip='Learns what distinguishes a measured contact from a random pair -- shared partners, support in the other layers, measurement similarity -- and ranks unmeasured pairs by it; every interactome misses contacts, and this says which ones.', explanation="An interactome is a sample. Crosslinking sees lysine-rich, abundant, soluble contacts and misses the rest; a pulldown sees what survives the wash. So among the pairs never observed are many true contacts, and the other evidence can say which. This strategy trains a logistic model on the layer's own edges against random pairs of the same proteins, using: support in each other measured layer (a direct edge, and shared neighbours), triadic closure within the layer itself (two proteins crosslinked to the same partners), each protein's degree, and the cosine similarity of their measurements. Every unobserved pair with any support is then scored.\n\nLiterature layers and layers derived from the target (unwritten interactions are crosslinks minus co-mentions) are excluded as evidence. The self-test hides a fifth of the layer's edges, retrains without them, and asks whether they outrank random non-edges.", walkthrough=('Choose the interaction layer to complete (crosslinks by default).', 'Press Test: AUROC of the hidden edges against random pairs, against models whose evidence was scrambled across genes.', "Press Run; 'predicted links' lists the strongest unobserved pairs with the layers that support each.", 'A predicted link between a labelled and an unlabelled protein is also a localization hypothesis -- take it to strategy 13.'), test_description="Pattern 3: 20% of the layer's edges hidden; the model is trained on the rest against DEGREE-MATCHED non-edges -- each with a gene of similar degree at both ends -- and scores the hidden edges against fresh degree-matched non-edges. Against random non-edges this test read AUROC 0.99 on every correlation layer, because a random pair is usually two obscure genes and degree separates them. The layer's source measurements leave the similarity feature with it, and derived, annotation and literature layers are refused as targets. Metric: AUROC. Null: 5 models trained with each gene's evidence read from a random other gene (identities permuted). Pass: above the null's 95th percentile by 0.05.", params=(Param(name='layer', kind='layer', label='Interaction layer to complete', tip='The layer whose missing edges are predicted. Its own visible edges are used (shared partners, degree), the other measured layers are evidence, and layers derived from it are excluded.', default=<function _layer_default.<locals>.pick>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='top', kind='int', label='Pairs to list', tip='How many of the highest-scoring unobserved pairs to list. Every candidate is scored; this only limits what is shown and saved.', default=300, lo=10, hi=20000, step=50, choices=(), optional=False, space='this')), runner=<function _link_run>, tester=<function _link_test>, cost='a minute', needs=('an interaction layer',), method='logistic regression', task='ranking', techniques=('logistic_regression', 'triadic_closure', 'degree_matched')), 'attention_correction': Strategy(key='attention_correction', number=17, title='Read the literature for biology, not fame', family='Walk the networks', question='Which pairs of genes are written about together more than their popularity explains -- and are those pairs biologically related?', tooltip="Ranks co-mentioned gene pairs by the residual over what each gene's publication count predicts, rather than by raw counts that reward fame; checks against held-out labels that the corrected ranking finds real relationships.", explanation="Raw co-mention reproduces the literature's popularity contest: the two most-published genes are co-mentioned most often whether or not they have anything to do with each other. The project's attention correction replaces each count with its residual over the count expected from each gene's own publication total, and it is on by default in the application. This strategy uses the correction as an inference tool: pairs with a high residual are pairs the literature treats as related beyond what fame explains.\n\nThe test of that claim is independent biology. Genes the literature links for a reason should share a compartment or complex more often than random co-mentioned pairs do. The strategy reports that share for the corrected ranking, the raw ranking and random pairs; on the real full-text layer the corrected ranking returns known complexes where the raw one returns famous genes.", walkthrough=('Choose the literature layer (full-text paragraphs by default) and the label to check against.', "Press Test: the share of the top corrected pairs that share a label, against random co-mentioned pairs; the raw ranking's share is reported beside it.", "Press Run and read 'corrected ranking'. Pairs with a high residual and no shared label are the literature's hypotheses the labels do not yet explain.", "Compare with 'raw ranking' to see what fame alone would have put at the top."), test_description="The label is never used to build the literature layer. Among co-mentioned pairs with both genes labelled, the top k by corrected residual are taken (k = the chosen number, capped at a fifth of the pool). Metric: the share of those pairs sharing a label. Null: 20 random sets of k co-mentioned pairs. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='layer', kind='layer', label='Literature layer', tip='Which co-mention layer to read: abstracts, or full-text paragraphs. The full-text layer is an open-access subset and is less dominated by review-article lists.', default=<function _literature_layer_default>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='top', kind='int', label='Top pairs', tip='How many of the highest-ranked pairs are compared. The test caps this at a fifth of the pairs whose genes are both labelled, so the top is really a top.', default=200, lo=10, hi=5000, step=10, choices=(), optional=False, space='this')), runner=<function _attention_run>, tester=<function _attention_test>, cost='seconds', needs=('a co-mention layer',), method='publication-count residual', task='ranking', techniques=('attention_residual',)), 'unwritten_links': Strategy(key='unwritten_links', number=18, title='List what the data says and the literature has not written', family='Walk the networks', question='Which gene pairs do several independent measurements link that no paper has ever mentioned together?', tooltip='Counts, for every gene pair, how many independent measurement layers link it, and lists the strongly supported pairs the literature never co-mentions -- after checking that measurement support predicts what the literature does write about.', explanation='The literature is where knowledge is recorded; the measurements are where it could come from. A pair linked by co-expression, co-fitness AND a crosslink but never mentioned together in any abstract or paragraph is a relationship the data asserts and nobody has written down -- a gap in the knowledge map, and a concrete hypothesis.\n\nThat reading needs one thing to be true: that measurement support tracks the kind of relationships biologists do eventually write about. The self-test checks it directly: using the literature only as held-out truth, it asks whether pairs with more measurement support are more often co-mentioned than random pairs of the same genes, against the same score with gene identities scrambled. If that holds, the unwritten pairs at the top of the list are the ones most likely to be written next. Paralogs, shared domains and the compartment layer are left out of the support count: they link genes for reasons that are already written down.', walkthrough=('Set how many independent layers must agree (2 is the minimum that means anything).', 'Press Test to confirm that measurement support predicts co-mention on this table.', "Press Run and read 'unwritten pairs'; the 'linked_in' column names the layers, and the paper counts show whether both genes are obscure or one is famous and the link is not.", 'A pair between an understudied gene and a well-studied one is the cheapest to follow up.'), test_description="Pattern 3 with the literature as truth: co-mentioned pairs among genes in the measurement layers against 5 times as many random pairs. Score: number of measurement layers linking the pair. Metric: AUROC. Null: 10 runs with the measurement layers' gene identities permuted. Pass: above the null's 95th percentile by 0.02.", params=(Param(name='min_layers', kind='int', label='Layers that must agree', tip="How many independent measurement layers must link a pair before it is listed. One layer is a single experiment's word; two or more is agreement between different kinds of evidence.", default=2, lo=1, hi=6, step=1, choices=(), optional=False, space='this'), Param(name='top', kind='int', label='Pairs to list', tip='How many unwritten pairs to list, most supported first. Every pair is counted; this only limits what is shown and saved.', default=500, lo=10, hi=50000, step=50, choices=(), optional=False, space='this')), runner=<function _unwritten_run>, tester=<function _unwritten_test>, cost='seconds', needs=('measurement and literature layers',), method='multi-layer support count', task='ranking', techniques=('support_count',)), 'supervised_classifier': Strategy(key='supervised_classifier', number=19, title='Train a classifier on the known genes and call the rest', family='Learn from examples', question='Given every permitted measurement, which label does a model trained on the labelled genes assign to each unlabelled one -- and which measurements does it rely on?', tooltip='Fits a class-balanced logistic regression from the permitted measurements to the label, scores it on whole orthogroups it never trained on, and calls unlabelled genes with a probability; also lists the measurements each class is recognised by.', explanation="Neighbour votes treat every measurement as equally relevant. A classifier learns which measurements matter for which class: a dense-granule protein might be recognised by its expression in the tachyzoite and its secretion signal, a ribosomal protein by fitness and codon usage. This strategy fits a multinomial logistic regression, class-balanced so the commonest compartment does not win by default, with an L2 penalty whose strength you choose.\n\nThe weights are an interpretable by-product: for each class, the measurements with the largest positive weights are what the model thinks defines it. The self-test hides whole orthogroups -- a paralog left in training makes any classifier look better than it is -- and compares the correct-call rate with the chance level given the predicted and true class frequencies (Cohen's expectation), which is what an uninformed classifier with the same output mix would score.", walkthrough=('Choose the label; keep C at 0.5 (smaller is a stronger penalty and a simpler model).', 'Press Test (one fit; a few seconds to a minute on the full table).', "Press Run; read 'what drives each class' before 'calls' -- a class recognised by sensible measurements earns more trust than one recognised by missingness.", 'Raise the probability threshold to call fewer, surer genes.'), test_description="Pattern 1: 25% of the label hidden by whole orthogroups; the model is trained on the visible genes and calls the hidden ones. Metric: hidden genes called correctly. Null: the analytic chance level for the same predicted and true class mixes (re-running a multinomial fit on shuffled labels ten times would take longer than it tells). Pass: above that level's 95% bound by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='C', kind='float', label='Regularisation C', tip='Inverse penalty strength. Small values force a simple model that uses few measurements strongly; large values let it fit detail, including noise. 0.5 is a middle setting for a few hundred rank-scaled measurements.', default=0.5, lo=0.001, hi=100.0, step=0.1, choices=(), optional=False, space='this'), Param(name='min_probability', kind='float', label='Call when probability is at least', tip="A gene is called only when the model's top probability reaches this. Zero calls every gene; 0.6 keeps the calls the model is sure of.", default=0.0, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this')), runner=<function _classifier_run>, tester=<function _classifier_test>, cost='a minute', needs=('a categorical column',), method='logistic regression', task='label calls', techniques=('logistic_regression', 'grouped_cv')), 'positive_unlabeled': Strategy(key='positive_unlabeled', number=20, title='Learn what makes your list special, from positives alone', family='Learn from examples', question='Given only genes that ARE something -- no list of genes that are not -- which other genes look most like them?', tooltip='Trains many classifiers, each separating your list from a random draw of the rest of the proteome, and ranks every other gene by out-of-bag score; the right tool when all you have is positives and the unlabelled genes include unknown positives.', explanation="Most real gene lists are positives only: hits of a screen, members of a complex, proteins someone localized. Training 'list versus everything else' treats every unknown member as a negative and teaches the model to reject exactly the genes you want to find. Positive-unlabelled bagging avoids that: each of many models sees the list and a small random draw of other genes; because the draw is small, a hidden positive is rarely in it, and every gene is scored only by the models that did not train on it.\n\nUnlike strategy 02, no cluster has to form: the model can combine weak signals from many measurements that no map would separate on. Unlike strategy 25, it uses the measurements rather than the networks. If the list came from a column of this table, name it so its closure is withheld.", walkthrough=('Paste the list; name the column it came from, if any.', 'Press Test: with 20 or more genes it hides 30% of YOUR list; otherwise it tests on a known category of similar size.', "Press Run and read 'candidates', highest score first.", 'Take the top candidates to strategy 24 to see what they share with the list.'), test_description="Pattern 2: 30% of the set hidden; the other 70% are the positives. Metric: AUROC of the hidden members against every other non-query gene. Null: 10 random sets of the same size. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='genes', kind='genes', label='Gene list', tip="Gene accessions, one per line or separated by commas or spaces, pasted from anywhere. Matched exactly against this organism's table, ignoring case; anything not found is reported by name rather than silently dropped.", default='', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='exclude', kind='category', label='Column the list came from', tip='If the gene list was made from a column of this table, name it here and that column, everything restating it and its experiment are withheld; otherwise the strategy would find the list again by reading the column that defined it.', default=None, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='bags', kind='int', label='Bags', tip='How many positive-versus-random-draw models are averaged. More bags make the ranking steadier; 15 is usually enough, and each bag is a small fit.', default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='top', kind='int', label='Candidates to list', tip='How many of the highest-scoring genes not on your list to show. Every gene is scored; this only limits the table.', default=300, lo=10, hi=10000, step=50, choices=(), optional=False, space='this')), runner=<function _pu_run>, tester=<function _pu_test>, cost='seconds', needs=('a gene list',), method='PU bagging, logistic regression', task='ranking', techniques=('pu_bagging', 'logistic_regression')), 'trait_regression': Strategy(key='trait_regression', number=21, title='Predict a measurement, and find the genes that defy the prediction', family='Learn from examples', question='How well does everything else predict this measurement -- and which genes are far from what their profile says they should be?', tooltip='Predicts a numeric measurement such as a fitness score from every other permitted measurement, out of fold; reports how predictable it is, fills it in for genes never measured, and ranks the measured genes that deviate most from expectation.', explanation="Two kinds of knowledge come out of one regression. The first is predictability: if fitness in fibroblasts can be predicted from expression, modification and structure, then essentiality has a signature, and genes the screen missed can be given a value. The second is surprise: a gene whose measured fitness is far from what its profile predicts is doing something its profile does not explain -- an essential protein with the profile of a dispensable one is a candidate for an unusual function, or a screen artefact worth rechecking.\n\nBy default the target's own kind of measurement is left out -- every knockout screen, when the target is a knockout screen -- because predicting one screen from another mostly shows that the screens agree. Predictions for measured genes are made out of fold, whole orthogroups at a time, so a gene is never predicted by a model that saw it or its paralog. The test hides a fifth of the measured values and scores the rank correlation on them against the chance distribution of a rank correlation.", walkthrough=('Choose the measurement (the fibroblast fitness screen by default).', 'Press Test: the rank correlation on hidden values, against shuffled training values.', 'Press Run; the summary gives the out-of-fold correlation over every measured gene.', "Read 'surprising genes' by |z|: positive z is higher than predicted, negative lower.", "Read 'predicted' for genes the experiment never measured -- with the test's correlation as the measure of how far to trust it."), test_description="Pattern 4: 20% of the measured values hidden; the model is trained on the rest. Metric: rank correlation between predicted and hidden values. Null: 3 models trained on the visible values shuffled among the measured genes. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='target', kind='number', label='Measurement to predict', tip="The numeric measurement to predict from everything else. Its own closure -- the same experiment's other outputs and anything restating it -- is withheld first.", default=<function default_numeric>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='model', kind='choice', label='Model', tip="'boosted' is gradient-boosted trees: nonlinear, tolerant of missing values, slower. 'ridge' is a penalised straight-line fit: fast, and its weights can be read.", default='boosted', lo=0, hi=0, step=1, choices=('boosted', 'ridge'), optional=False, space='this'), Param(name='own_kind', kind='choice', label='Its own kind of measurement', tip="'leave out' removes every column measured the same way as the target -- all the fitness screens, when the target is a fitness screen -- so the answer says what OTHER evidence knows. 'include' keeps them, which mostly measures how well the screens agree with each other.", default='leave out', lo=0, hi=0, step=1, choices=('leave out', 'include'), optional=False, space='this'), Param(name='top', kind='int', label='Surprising genes to list', tip='How many of the measured genes furthest from their prediction to list, by absolute z-score of the out-of-fold residual.', default=200, lo=10, hi=5000, step=10, choices=(), optional=False, space='this')), runner=<function _regression_run>, tester=<function _regression_test>, cost='a minute', needs=('a numeric column',), method='gradient boosting / ridge', task='values', techniques=('gradient_boosting', 'ridge', 'grouped_cv')), 'masked_imputation': Strategy(key='masked_imputation', number=22, title='Fill in what was never measured, and say where that is honest', family='Learn from examples', question='For each measurement, can its missing values be estimated from the rest of the table -- and for which measurements is that impossible?', tooltip='Completes the whole measurement table with a low-rank model, after first hiding a tenth of every column to measure how well each one can be reconstructed; imputes only where the reconstruction has been shown to work.', explanation="Most genes are missing most measurements, and every map and model here fills the gaps somehow -- usually at the median, which says 'average' about a gene nobody measured. This strategy asks whether better is possible: if the measurements are correlated (expression across stages, fitness across hosts), a low-rank model of the table can estimate a missing entry from the gene's other entries.\n\nThe answer is column-specific, and the strategy measures it before using it: a tenth of each column's measured values is hidden, the table is completed, and each column gets a reliability -- the rank correlation between reconstructed and hidden values. Columns that reconstruct well can be filled; for the rest, an absence must stay an absence, which is the project's rule that unknown is grey, never zero.", walkthrough=('Optionally choose a label whose closure should be left out of the table.', 'Choose the column you want filled (optional) and a rank (20 is a good default).', 'Press Test: the median reliability across columns, against a table whose columns were shuffled so nothing links them.', "Press Run and read 'column reliability'; then 'filled' for your chosen column, only if its reliability is high."), test_description="Pattern 4 across the whole table: 10% of every column's measured entries hidden, the table completed at the chosen rank. Metric: median over columns of the rank correlation on hidden entries. Null: 3 completions of a table whose columns were each shuffled independently. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='target', kind='category', label='Leave out the closure of', tip='Optionally, a label whose closure is removed from the table before completion, so imputed values can be used against that label without leakage.', default=None, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='column', kind='number', label='Column to fill', tip='The measurement whose missing values are listed after completion. Every column is completed; this only chooses which one to show.', default=<function default_numeric>, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='rank', kind='int', label='Rank', tip='How many underlying patterns the low-rank model may use. Too few blur distinct programmes together; too many fit noise and reconstruct nothing.', default=20, lo=2, hi=200, step=1, choices=(), optional=False, space='this')), runner=<function _impute_run>, tester=<function _impute_test>, cost='a minute', needs=(), method='soft-impute, low-rank SVD', task='values', techniques=('soft_impute',)), 'condition_shift': Strategy(key='condition_shift', number=23, title='Find what matters more in one condition, and why', family='Learn from examples', question='Which genes matter more (or less) in one condition than a baseline predicts -- in the mouse rather than the dish, say -- and can the rest of the data explain which?', tooltip='Takes a condition screen and its baseline, keeps the part of the condition the baseline cannot explain, and asks whether the other measurements predict that condition-specific effect -- the signature of what the parasite needs only there.', explanation="Most genes essential in the mouse are essential in the dish too; the interesting ones are the exceptions. This strategy isolates them: the condition measurement is regressed on its baseline (both rank-scaled) and the residual -- the condition-specific shift -- becomes the target. A gene with a strongly negative shift is needed in the condition beyond what its baseline need predicts: a candidate for host interaction, immune evasion, nutrient acquisition in vivo.\n\nThen it asks whether the shift is predictable from the other measurements, with both screens' closures withheld. If it is, the condition-specific need has a signature -- secretion, host-facing localization, a stage of expression -- and genes never screened in the condition can be ranked by it. If it is not, the shift is idiosyncratic or noise, and the self-test says which. The other screens of the same kind are left out by default: an in vivo shift predicted from another in vivo screen is one experiment confirming another, not an explanation.", walkthrough=('Choose the condition (in vivo peritoneum by default) and its baseline (fibroblasts).', "Press Test: can hidden genes' shifts be predicted?", "Press Run; read 'shifted genes' from the top (most negative: needed more in the condition).", "If the test passed, read 'predicted' for genes not screened in the condition."), test_description="Pattern 4 on the shift: 20% of genes with both measurements hidden; a model trained on the rest predicts their shift. Metric: rank correlation on hidden genes. Null: 3 models trained on shuffled shifts. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='condition', kind='number', label='Condition', tip="The measurement taken in the condition of interest, for example fitness in the mouse. Its closure and the baseline's are withheld from the predictors.", default=<function _condition_default.<locals>.pick>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='baseline', kind='number', label='Baseline', tip='The same measurement in the reference condition, for example fitness in fibroblasts. What the baseline explains is removed; what remains is condition-specific.', default=<function _condition_default.<locals>.pick>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='model', kind='choice', label='Model', tip="'boosted' is gradient-boosted trees: nonlinear, tolerant of missing values, slower. 'ridge' is a penalised straight-line fit: fast, and its weights can be read.", default='boosted', lo=0, hi=0, step=1, choices=('boosted', 'ridge'), optional=False, space='this'), Param(name='own_kind', kind='choice', label='Its own kind of measurement', tip="'leave out' removes every column measured the same way as the target -- all the fitness screens, when the target is a fitness screen -- so the answer says what OTHER evidence knows. 'include' keeps them, which mostly measures how well the screens agree with each other.", default='leave out', lo=0, hi=0, step=1, choices=('leave out', 'include'), optional=False, space='this')), runner=<function _shift_run>, tester=<function _shift_test>, cost='a minute', needs=('two numeric columns',), method='residual + gradient boosting / ridge', task='values', techniques=('residualisation', 'gradient_boosting', 'ridge', 'grouped_cv')), 'set_enrichment': Strategy(key='set_enrichment', number=24, title='Describe what your gene list has in common', family='Start from a gene list', question='What distinguishes the genes on my list from the rest -- which categories are they enriched in, which measurements are shifted, which networks are dense among them?', tooltip='Tests a gene list against every category, every measurement and every measured network at once, corrected together; the significant features form a profile, and the profile is then used to rank every other gene -- which is how it is tested.', explanation="Enrichment analysis is usually run against one annotation at a time. This strategy runs it against everything the table holds, in one family of tests with one correction: hypergeometric enrichment for every class of every categorical column, a rank-sum test (reported as AUROC minus one half) for every measurement, and, for every measured network, whether the list has more internal edges than a random set of the same size would.\n\nA profile is only a description until it predicts something. So the significant features are combined into a score -- shifted measurements weighted by their effect, enriched categories by their log lift, networks by the share of a gene's partners on the list -- and every other gene is ranked by it. The self-test builds the profile from 60% of a known set and asks whether the other 40% rank near the top: a profile that recognises members it never saw has captured what the set has in common.", walkthrough=('Paste the list; name the column it came from, if any, so it is not simply read back.', 'Press Test.', "Press Run and read 'profile' by q-value: kind, feature, direction and effect.", "Read 'resembles the profile' for genes that share the list's characteristics."), test_description="Pattern 2: 40% of the set hidden; the profile is built from the other 60% and scores every gene. Metric: AUROC of the hidden members against every other non-query gene. Null: 10 random sets of the same size. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='genes', kind='genes', label='Gene list', tip="Gene accessions, one per line or separated by commas or spaces, pasted from anywhere. Matched exactly against this organism's table, ignoring case; anything not found is reported by name rather than silently dropped.", default='', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='exclude', kind='category', label='Column the list came from', tip='If the gene list was made from a column of this table, name it here and that column, everything restating it and its experiment are withheld; otherwise the strategy would find the list again by reading the column that defined it.', default=None, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='top', kind='int', label='Genes to rank', tip='How many of the genes that best match the profile, and are not on the list, to show.', default=300, lo=10, hi=10000, step=50, choices=(), optional=False, space='this')), runner=<function _enrichment_run>, tester=<function _enrichment_test>, cost='seconds', needs=('a gene list',), method='hypergeometric + rank-sum', task='ranking', techniques=('hypergeometric', 'rank_sum', 'bh_fdr')), 'seed_expansion': Strategy(key='seed_expansion', number=25, title='Grow your gene list along the networks', family='Start from a gene list', question='Starting from my genes, which others does a walk across every measured network keep returning to?', tooltip="Seeds a random walk on your list and lets it wander across every permitted measured network (and, optionally, a nearest-neighbour graph of the measurements); genes the walk visits most are the list's closest relatives in the data.", explanation="A complex, a pathway, a secretory route: the genes that belong with yours are connected to them -- not necessarily directly, but by many short paths through co-expression, co-fitness, crosslinks and structural similarity. Random walk with restart measures exactly that: the walk starts on your seeds, steps along edges, and returns to the seeds with a fixed probability, so its long-run visiting frequency is high for genes close to the list by many routes and low for genes reached by a single long path.\n\nEach layer is degree-normalised, so hubs do not attract every walk, and the layers are averaged rather than merged, so a dense layer does not drown a sparse, precise one. With 'networks + measurements' a k-nearest-neighbour graph of the measurements joins the average, which lets the walk reach genes no network covers.", walkthrough=('Paste the list; name the column it came from, if any.', "Choose 'networks' for relational evidence only, or 'networks + measurements' to reach genes no network covers.", 'Press Test, then Run.', "Read 'candidates': the score, and which layers link each one directly to the list."), test_description="Pattern 2: 30% of the set hidden; the walk is seeded from the other 70%. Metric: AUROC of the hidden members against every other non-seed gene. Null: 10 random seed sets of the same size. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='genes', kind='genes', label='Gene list', tip="Gene accessions, one per line or separated by commas or spaces, pasted from anywhere. Matched exactly against this organism's table, ignoring case; anything not found is reported by name rather than silently dropped.", default='', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='exclude', kind='category', label='Column the list came from', tip='If the gene list was made from a column of this table, name it here and that column, everything restating it and its experiment are withheld; otherwise the strategy would find the list again by reading the column that defined it.', default=None, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='mode', kind='choice', label='Walk across', tip="'networks' walks only the measured edge layers. 'networks + measurements' adds a nearest-neighbour graph of the measurement table, so genes absent from every network can still be reached.", default='networks + measurements', lo=0, hi=0, step=1, choices=('networks', 'networks + measurements'), optional=False, space='this'), Param(name='restart', kind='float', label='Restart probability', tip='How often the walk jumps back to your seeds. High keeps it close (few, near relatives); low lets it wander (more candidates, further away).', default=0.3, lo=0.05, hi=0.95, step=0.05, choices=(), optional=False, space='this'), Param(name='top', kind='int', label='Candidates to list', tip='How many of the most-visited genes not on your list to show. Every gene is scored by the walk; this only limits the table that is shown and saved.', default=300, lo=10, hi=10000, step=50, choices=(), optional=False, space='this')), runner=<function _expansion_run>, tester=<function _expansion_test>, cost='seconds', needs=('a gene list',), method='random walk with restart', task='ranking', techniques=('random_walk_restart', 'knn_graph')), 'split_clusters': Strategy(key='split_clusters', number=26, title='Find categories that split in two on another measurement', family='Contrast and combine layers', question='Which clusters agree about one thing -- a compartment -- and split cleanly on another -- a stage, a phase, a fitness level?', tooltip='Finds clusters that are homogeneous for one label and divided on a second label or measurement; each is a claim that a category contains two kinds of gene, checked by asking whether the split reappears in genes the discovery never saw.', explanation="A compartment is not one thing: the nucleus holds genes of every cell-cycle phase, the apicoplast holds essential and dispensable proteins. Where a cluster is homogeneous for one label (A) and divides on another (B) -- a categorical label into two enriched groups, or a measurement into two modes -- the data is saying the category has internal structure, and naming the measurement that carries it.\n\nBoth labels are withheld from the map, so the cluster is found without either and the claim is not circular. The map is clustered both ways HDBSCAN can select clusters, and the findings of both are pooled: a split needs a whole category held in one cluster, and which selection gives that depends on the table. Run in both directions for two categorical labels: 'same compartment, split phase' and 'same phase, split compartment' are different biology. The self-test makes the findings on half the genes and asks whether each split reappears in the other half.", walkthrough=('Choose the label that should be shared (A) and the one that should split (B).', 'Press Test: the share of first-half findings that replicate in the second half.', "Press Run and read both tables; 'groups' and 'counts' (or the two means) describe the split, 'genes' lists the minority side, 'selection' which clustering found it.", 'Color the map by B and click into the cluster to see the split.'), test_description="Pattern 5: significant findings (q < 0.05) on a random half of the mapped genes; each is checked on the other half -- the minority group enriched again among the cluster's A-matching genes (hypergeometric p < 0.05), or the measurement bimodal again. Metric: share replicating. Null: 20 runs with B shuffled within the second half. Pass: above the null's 95th percentile by 0.2 with at least three findings.", params=(Param(name='a', kind='category', label='Shared label (A)', tip='The label the cluster must agree on -- what its genes have in common. Withheld from the map, with its closure. A coarse label works best: a cluster can agree on an organelle far more often than on one of two dozen sub-compartments.', default=<function _coarse_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='b', kind='column', label='Splitting label or measurement (B)', tip='The label or measurement the cluster must divide on. A category splits into enriched groups; a measurement into two modes. Withheld from the map too.', default=<function <lambda>>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='min_cluster_size', kind='int', label='Cluster size', tip='HDBSCAN minimum cluster size. A split needs enough genes on both sides, so larger clusters find more splits and smaller ones find purer clusters.', default=40, lo=5, hi=500, step=1, choices=(), optional=False, space='this'), Param(name='selection', kind='grid', label='Cluster selection to try', tip="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.", default='eom, leaf', lo=0, hi=0, step=1, choices=(), optional=False, space='this')), runner=<function _disagreement_run>, tester=<function _disagreement_test>, cost='a minute', needs=('two categorical columns',), method='UMAP + HDBSCAN', task='replication', techniques=('umap', 'hdbscan', 'hypergeometric', 'bimodal_split', 'bh_fdr')), 'conjunctions': Strategy(key='conjunctions', number=27, title='Find kinds of gene defined by two labels at once', family='Contrast and combine layers', question='Which clusters are enriched for a COMBINATION of two labels -- more than either label alone would make them?', tooltip="Finds clusters enriched for a pair of labels beyond what each label's own enrichment explains; each is a kind of gene defined by both at once, checked by asking whether the second label stays enriched within the first in genes the discovery never saw.", explanation="Some biology lives in combinations. Secreted proteins are common and fitness-conferring genes are common, but a cluster where the secreted genes are ALSO the fitness-conferring ones is a finding neither label shows alone. The strategy withholds both labels, clusters a map, and for every cluster and every combination measures the joint enrichment against the stronger of the two single enrichments; a ratio above 1.25 means the combination says more than either margin.\n\nThe replication test checks the interaction itself rather than the enrichment: within the second half's genes of the first label, is the second label still concentrated in this cluster? A cluster enriched for the first label alone cannot pass that, because the second label is then no more common inside the cluster than among the first label's genes anywhere.", walkthrough=('Choose two categorical labels.', 'Use a smaller cluster size than for strategy 26: a combination is a finer unit.', "Press Test, then Run; read 'conjunctions' by q with the interaction ratio.", "The 'genes' column lists unmeasured members -- candidates for the combination."), test_description="Pattern 5: conjunctions found on a random half; each is checked on the other half as the second label's enrichment in the cluster among genes carrying the first label (hypergeometric p < 0.05). Metric: share replicating. Null: 20 runs with the second label shuffled within the second half. Pass: above the null's 95th percentile by 0.2 with at least three findings.", params=(Param(name='a', kind='category', label='First label', tip='One of the two labels whose combination is sought. Withheld from the map with its closure; a coarse label such as the unified localization works best.', default=<function _coarse_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='b', kind='category', label='Second label', tip='The other label. Withheld from the map too, so the cluster is found without either and the combination cannot be read back from the input.', default=<function <lambda>>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='sample', kind='int', label='Genes per map', tip='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.', default=3000, lo=0, hi=20000, step=500, choices=(), optional=False, space='this'), Param(name='min_cluster_size', kind='int', label='Cluster size', tip='HDBSCAN minimum cluster size. Combinations are finer than single labels, so smaller clusters than for a split are usually right.', default=15, lo=5, hi=500, step=1, choices=(), optional=False, space='this'), Param(name='selection', kind='grid', label='Cluster selection to try', tip="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.", default='eom, leaf', lo=0, hi=0, step=1, choices=(), optional=False, space='this')), runner=<function _conjunction_run>, tester=<function _conjunction_test>, cost='a minute', needs=('two categorical columns',), method='UMAP + HDBSCAN', task='replication', techniques=('umap', 'hdbscan', 'hypergeometric', 'bh_fdr')), 'paralog_divergence': Strategy(key='paralog_divergence', number=28, title='Find paralogs that changed jobs', family='Contrast and combine layers', question='Which duplicated genes behave differently across the measurements -- evidence that one copy took on a new role?', tooltip="Compares every paralog pair's measurement profiles and ranks them by divergence; diverged pairs are candidates for sub- or neo-functionalisation, and the test checks that divergence predicts a change of compartment the profiles never saw.", explanation="Gene duplication is where new functions come from, and apicomplexan genomes are full of expanded families. A pair of paralogs that behave alike -- same expression across stages, same fitness, same modifications -- is probably redundant. A pair that behave differently has probably partitioned or changed its job, and that is visible in the data long before anyone characterises either copy.\n\nDivergence is one minus the correlation of the pair's rank-scaled measurements over the columns both have (at least the chosen number). If a label is chosen, its closure is withheld from the profiles and the test asks the question that makes divergence meaningful: do paralogs that sit in DIFFERENT compartments have more divergent profiles than paralogs in the same one? If so, divergence in unlocalised pairs is evidence of a change of place or job.", walkthrough=('Optionally choose a label (compartment); it is withheld from the profiles and shown beside each pair.', 'Press Test: AUROC of divergence for pairs whose labels differ.', "Press Run and read 'paralog pairs' from the top: most diverged first.", "For a diverged pair, compare the two genes' evidence panels side by side."), test_description="Paralog pairs with both genes labelled; the labels are withheld from the profiles. Metric: AUROC of divergence for pairs whose labels differ against pairs whose labels match. Null: 20 random reassignments of the divergence values to pairs. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Label to compare (optional)', tip='A label withheld from the profiles and shown beside each pair; the self-test asks whether divergence predicts a difference in it. The default label is used for the test when this is empty.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='min_shared', kind='int', label='Measurements both must have', tip='A pair is scored only over measurements present for both genes, and only if there are at least this many. Fewer shared measurements give noisier divergence.', default=10, lo=3, hi=500, step=1, choices=(), optional=False, space='this')), runner=<function _paralog_run>, tester=<function _paralog_test>, cost='seconds', needs=('orthogroups',), method='profile correlation', task='ranking', techniques=('profile_correlation',)), 'ortholog_transfer': Strategy(key='ortholog_transfer', number=29, title='Carry what one parasite shows to the other', family='Cross species and strata', question="What does a gene's ortholog in the other parasite say about it -- its essentiality, its stage, its localization?", tooltip='Maps a measurement or label from the other species onto this one through shared orthogroups, learning how the two relate on genes measured in both; a Plasmodium knockout screen becomes a prediction for untested Toxoplasma genes, and back.', explanation='Toxoplasma and Plasmodium diverged hundreds of millions of years ago, but conserved genes often keep their jobs: a ribosomal protein essential in one is essential in the other. Each species has experiments the other lacks -- saturation mutagenesis in P. falciparum, hyperLOPIT in T. gondii -- so an ortholog is a second, independent measurement of the same gene.\n\nThe two tables stay separate, as the project requires: orthology is a bridge, not a merge. The strategy aggregates the source column over each orthogroup in the other species (median, or the commonest label), learns how it relates to the target on genes measured in both -- a sign for two measurements, a lookup for labels -- and applies that to genes measured only on the other side. The test hides a quarter of the genes measured in both and compares the transfer with one whose ortholog values were dealt to random genes.', walkthrough=('Choose the target in this organism and the source column in the other.', "Press Test: how well the ortholog's value predicts the hidden genes, against shuffled orthologs.", "Press Run; 'transferred' lists genes lacking the target that have an ortholog value.", 'Remember what orthology cannot reach: lineage-specific genes have no ortholog -- see 30.'), test_description="Numeric target: 25% of the genes measured in both species hidden; the relation is learned on the rest. Metric: rank correlation of transferred and hidden values. Null: 20 runs with the ortholog values permuted among genes. Categorical target: Pattern 1 on genes with an ortholog value. Pass: above the null's 95th percentile by 0.1 (numeric) or 0.05 (categorical).", params=(Param(name='target', kind='column', label='Target in this organism', tip='The measurement or label to predict here. Numeric targets are scored by rank correlation, labels by correct calls.', default=<function _transfer_target_default>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='source', kind='column', label='Source in the other organism', tip="The measurement or label taken from the other species' orthologs. It is aggregated over each orthogroup before it is compared with anything.", default=<function _source_default>, lo=0, hi=0, step=1, choices=(), optional=False, space='other')), runner=<function _transfer_run>, tester=<function _transfer_test>, cost='seconds', needs=("the other organism's table",), method='orthogroup mapping', task='values', techniques=('orthogroup_mapping',)), 'stratum_focus': Strategy(key='stratum_focus', number=30, title='Test inference on the genes orthology cannot reach', family='Cross species and strata', question='Can lineage-specific, hypothetical or understudied genes be called as reliably as the rest -- and what are they?', tooltip="Calls genes in one stratum -- lineage-specific, hypothetical, understudied -- from measurement neighbours with every conservation and orthology column removed, and measures the error rate on that stratum alone rather than borrowing the proteome's.", explanation="An accuracy measured over the whole proteome is dominated by conserved, well-studied genes, where every method does best. The genes that most need inference -- parasite-specific proteins with no ortholog, 'hypothetical proteins', genes nobody has published on -- are exactly where methods are weakest and where an overall accuracy flatters them most.\n\nThis strategy calls genes within one stratum from their measurement neighbours, with conservation and orthology columns removed so a call cannot simply reflect how conserved a gene is, and the self-test scores ONLY hidden genes of that stratum. The result is an error rate that belongs to the genes being predicted. 'accuracy by stratum' puts it beside the rest of the proteome, which shows how much a global number would have overstated it.", walkthrough=('Choose the label and the stratum (lineage-specific by default).', 'Press Test: correct calls among hidden genes of the stratum only.', "Press Run; compare the two rows of 'accuracy by stratum'.", "Read 'calls' for the stratum's unlabelled genes -- trusted at the stratum's own rate."), test_description="Pattern 1 restricted to the stratum: 25% of the label hidden by whole orthogroups; only hidden genes inside the stratum are scored. Null: 10 runs on shuffled labels. Pass: above the null's 95th percentile by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='stratum', kind='choice', label='Stratum', tip='Which genes to focus on: lineage-specific (no ortholog outside the lineage), hypothetical proteins, understudied genes (no focal or substantive paper), or conserved genes for comparison.', default='lineage-specific', lo=0, hi=0, step=1, choices=('lineage-specific', 'hypothetical protein', 'understudied', 'conserved'), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='min_share', kind='float', label='Call when the vote is at least', tip='The share of the neighbourhood vote the winning label must hold before a gene is called. Higher calls fewer genes and fewer of them wrongly; the self-test uses the same threshold, so its number describes exactly these calls.', default=0.3, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this')), runner=<function _stratum_run>, tester=<function _stratum_test>, cost='seconds', needs=('a categorical column',), method='kNN', task='label calls', techniques=('knn',)), 'triangulation': Strategy(key='triangulation', number=31, title='Call a gene only when independent strategies agree', family='Combine strategies', question='Where do measurement neighbours, a trained classifier and the networks give the same answer -- and how much more often is that answer right?', tooltip='Runs three strategies built on different evidence -- measurement neighbours, a classifier, network partners -- and calls a gene only where enough of them agree; the test measures the precision of agreed calls against agreement between guesses.', explanation="Every strategy has its own failure mode: neighbours are misled by missingness, a classifier by a spurious weight, a network by a hub. Failures of different methods on different evidence are largely independent, so a call on which several agree is much less likely to be wrong than any one of them -- the logic of triangulation in measurement.\n\nThe price is reach: an agreed call exists only where every method can speak, and the self-test therefore scores PRECISION (the share of agreed calls that are right) rather than calls per hidden gene, and reports each single method's precision and number of calls beside it. The null is agreement between the same methods trained on shuffled labels, which is how often chance alone produces a consensus.", walkthrough=("Choose the label; keep 'must agree' at 2 of 3.", "Press Test: compare the agreed precision with each single method's (in the numbers).", "Press Run; 'calls' shows every method's own call beside the agreed one.", "Raise 'must agree' to 3 for the smallest, surest set."), test_description="Pattern 1 scored by precision: 25% of the label hidden; each method is trained on the visible genes and the agreed calls on hidden genes are scored. Metric: share of agreed calls that are correct. Null: 5 runs with visible labels shuffled. Pass: above the null's 95th percentile by 0.1. Single-method precisions are reported.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='min_agree', kind='int', label='Methods that must agree', tip='How many of the independent methods must give the same label before a gene is called. Two of three is the usual balance; three of three is the surest and reaches the fewest genes.', default=2, lo=1, hi=3, step=1, choices=(), optional=False, space='this')), runner=<function _tri_run>, tester=<function _tri_test>, cost='a minute', needs=('a categorical column',), method='kNN + logistic + network vote', task='label calls', techniques=('knn', 'logistic_regression', 'network_vote', 'agreement')), 'understudied_first': Strategy(key='understudied_first', number=32, title='Put the understudied genes first', family='Combine strategies', question='Which genes nobody has written about can the data say something trustworthy about?', tooltip='Makes agreed calls for genes with no focal or substantive publication and ranks them by agreement times novelty; its test scores precision on understudied genes alone, so the trust it reports is earned where the attention is not.', explanation='Research attention is concentrated: a few hundred genes carry most of the literature, and thousands have never been the subject of a paper. Those are where a knowledge map adds the most -- and where it must be most careful, because an inference about an unstudied gene will not be contradicted by anything anyone has written.\n\nThis strategy restricts the triangulated calls of strategy 31 to understudied genes (no focal or substantive paper), ranks them by how many methods agree times how little the gene has been written about, and measures precision on hidden understudied genes only -- never borrowing trust from the well-studied genes, where every method looks better because the measurements themselves were designed around them. A call here is a first hypothesis about a gene with no literature, and the precision beside it is the only evidence for it there is.', walkthrough=('Choose the label.', 'Press Test: precision of agreed calls on hidden understudied genes only.', "Press Run; 'candidates' is ranked by agreement times novelty.", 'Take the top candidates to their evidence panels; each is a first hypothesis about a gene with no literature.'), test_description="Pattern 1 scored by precision and restricted to understudied genes: 25% of the label hidden; agreed calls are scored only on hidden understudied genes. Null: 5 runs with visible labels shuffled. Pass: above the null's 95th percentile by 0.1.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='min_agree', kind='int', label='Methods that must agree', tip='How many of the three methods must agree before an understudied gene is called; these genes have no literature to catch a wrong call, so agreement matters more here than anywhere.', default=2, lo=1, hi=3, step=1, choices=(), optional=False, space='this')), runner=<function _understudied_run>, tester=<function _understudied_test>, cost='a minute', needs=('a categorical column', 'a literature-attention column'), method='kNN + logistic + network vote', task='label calls', techniques=('knn', 'logistic_regression', 'network_vote', 'agreement')), 'neighbour_space': Strategy(key='neighbour_space', number=33, title="Put every layer into one space and read a gene's neighbourhood", family='Combine strategies', question='Which genes are the nearest neighbours of this one when every permitted network and the whole measurement table are combined into a single graph, and what evidence puts each of them there?', tooltip="Builds one graph over the genes from every permitted edge layer plus nearest neighbours in measurement space, learns what each source is worth from held-out edges, and lists a gene's neighbours with the probability and the per-source evidence behind each; measured edges stay marked as measured.", explanation="The thirteen edge layers are deliberately never merged, because each answers a different question and they disagree. That is right for reading a layer and wrong for the question a user actually asks: which genes are near this one? Answering it needs all of them at once. This strategy builds that graph. Candidate pairs are every gene's partners in every permitted layer plus its nearest genes in measurement space, so a pair no network touches can still be a neighbour. For each pair the sources are each layer's weight as a percentile among that layer's own edges, an indicator that the layer does not record the pair at all, the cosine similarity of the permitted measurements, and an Adamic-Adar shared-partner count that discounts partners shared through a hub.\n\nEach source's weight is learned from held-out edges, one fold per layer, and in a layer's own fold that layer is not among its features -- so a source that can only predict itself ends at weight zero. Degree is not a source at all: it is the strongest predictor of an edge and the least interesting, and a model given it learns which genes are famous. The result is one probability per pair with the per-source contributions that produced it, and every table marks whether a pair is measured or inferred. Nothing here is written back into a layer: an integrated edge is a claim, and a crosslink is an experiment.", walkthrough=('Paste the genes whose neighbourhoods you want, or leave the list empty to see the strongest neighbourhoods in the space.', "If you are going to use this while scoring a label, name that label under 'Label being held out' so its whole closure is removed first.", 'Press Test: the AUROC of hidden edges against DEGREE-MATCHED non-pairs, with the easy random-null number reported beside it so you can see the difference.', "Press Run and read 'neighbourhoods': 'measured_in' names the layers that record the pair and is empty for an inferred neighbour; 'top_evidence' says which sources moved it.", "Read 'the three nulls' before quoting any number from this strategy, and 'how each source is weighted' to see which kinds of evidence the table actually rests on."), test_description="A layer's edges are hidden by orthogroup -- whole groups at a time, so no hidden edge survives through a visible paralog -- and the layer is removed from its own features. Metric: AUROC of the hidden edges against DEGREE-MATCHED non-pairs, one per hidden edge, matched on connectivity at both ends. Null: 10 configuration-model rewirings of the hidden edges, which keep their degree sequence and destroy their topology, so a model reading fame cannot beat it. Pass: above the null's 95th percentile by 0.05. The AUROC against random non-pairs and the gap between the two are reported as numbers, not as the verdict.", params=(Param(name='genes', kind='genes', label='Genes to read', tip='Gene accessions whose neighbourhoods to list, one per line or separated by commas or spaces. Leave empty to list the neighbourhoods around the strongest edges in the space instead. Anything not found is reported by name.', default='', lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='exclude', kind='category', label='Label being held out', tip='A label this space must stay blind to, if the neighbourhoods are going to be used while scoring that label. Its whole closure is applied: the column, anything restating it, the experiment that produced it, and any edge layer built from it.', default=None, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='layer', kind='layer', label='Layer to hide in the self-test', tip="Which layer's edges are hidden and asked back when the strategy tests itself. It is removed from its own features, so the question is whether the OTHER evidence finds its edges; a dense layer gives a stricter test than a sparse one.", default=<function _layer_default>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours per gene', tip="How many neighbours to list for each gene given. The space ranks every candidate pair; this only decides how far down each gene's list is printed.", default=10, lo=1, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='top', kind='int', label='Pairs to list', tip="How many of the strongest pairs in the whole space to list in 'the space', and how many to draw the fallback genes from when no gene list was given.", default=500, lo=10, hi=50000, step=50, choices=(), optional=False, space='this'), Param(name='knn', kind='int', label='Nearest genes per gene', tip='How many nearest genes in measurement space become candidate neighbours for each gene, on top of its partners in every layer. This is what lets the space speak about a pair no network touches at all; zero restricts it to pairs some layer already links.', default=15, lo=0, hi=100, step=1, choices=(), optional=False, space='this'), Param(name='per_layer', kind='int', label='Neighbours kept per layer', tip='How many partners per gene are kept from each layer, strongest weight first. A dense layer such as the shared-compartment one would otherwise supply more than a hundred thousand pairs that all make the same claim.', default=25, lo=5, hi=200, step=5, choices=(), optional=False, space='this')), runner=<function _space_run>, tester=<function _space_test>, cost='minutes', needs=('at least one edge layer',), method='logistic edge model', task='ranking', techniques=('logistic_regression', 'knn_graph', 'degree_matched')), 'network_training': Strategy(key='network_training', number=34, title='Train on the networks and rank the edges they are missing', family='Combine strategies', question='Which pairs of genes does the combined evidence imply although no measured layer records them, how strong is each claim, and how good is the model that makes it when it is scored against a degree-matched null rather than a random one?', tooltip='Trains one model per layer, each blind to its own layer, on edges held out by orthogroup; reports every number against three nulls including degree-matched non-pairs; and ranks the pairs the evidence implies that no layer records, each with its calibrated probability and the sources that produced it.', explanation="A network is a sample of a biology, and the pairs it never recorded include real ones. Which ones can be said from everything else: the other layers, the measurements, who is whose neighbour. This strategy learns that, one fold per layer, and holds two rules throughout. Edges are held out by ORTHOGROUP, whole groups at a time, because a randomly held-out edge is recovered through a paralog that stayed visible, which scores the resemblance of a gene to its own copy. And a layer is never a feature for predicting itself, which is enforced in one place and reflected in the weights: a source's weight is the average of what it was worth in every fold, counting as zero the fold where it was the target.\n\nEvery number is reported against three nulls at once, because the usual one flatters. Against random non-pairs a link predictor mostly rediscovers that well-connected genes are well connected -- strategy 16 reports AUROC 0.97 that way. Against DEGREE-MATCHED non-pairs, each positive paired with a non-pair of similar connectivity at both ends, the honest number is smaller and it is the one reported here; against a configuration model, the degree sequence rewired, a model that has only learned fame collapses to chance. The difference between the first two is printed as the fame gap, and a deliberately disqualified degree-only model is fitted beside the real one to show what that gap looks like when fame IS the model. Probabilities are calibrated on held-out pairs and their reliability is reported, because a gap table invites a reader to act on a number.\n\nOne layer defeats this test on the shipped data and the failure is the useful part. Hidden crosslinks are recovered at AUROC 0.875 against degree-matched non-pairs -- and a configuration-model rewiring of those same crosslinks is recovered at 0.832, because a crosslinked complex is a clique and a rewired pair inside one looks, to every source except the crosslink layer itself, exactly like a recorded pair. So this space can say which COMPLEXES the crosslinking missed and cannot say which PAIRS inside them it missed. Read a predicted crosslink that way.\n\nA learned spectral embedding is fitted beside the interpretable baseline and offered for ranking only where it beats it on the degree-matched null by more than 0.01 AUROC and by more than twice its own spread across the held-out layers. On the shipped Toxoplasma table it does not, and the strategy says so and ranks with the baseline: a gap table without per-source attribution is worth less than a slightly worse one that can be read.", walkthrough=('Leave the model on the logistic baseline unless you want to see what the learned embedding does on your table; it is always fitted and always reported either way.', "If you are going to use the result while scoring a label, name that label under 'Label being held out' so its closure is removed before anything is computed.", "Press Test: hidden edges against degree-matched non-pairs, with a configuration-model bar. Read the 'auroc_random' number beside it to see how much the easy null would flatter.", "Press Run and read 'the three nulls' FIRST. If the degree-matched AUROC is near 0.5, the gaps below it are not worth reading whatever their probabilities say.", "Then read 'gaps': each row is a pair no layer records, with its probability and the sources that produced it. Take the top ones to strategy 13 or 16 for a physical reading.", "Check 'how each source is weighted' to see which evidence the ranking rests on, and whether a source you do not trust on this table is carrying it."), test_description="The chosen layer's edges are hidden by orthogroup and the layer is removed from its own features. Metric: AUROC of the hidden edges against DEGREE-MATCHED non-pairs. Null: 10 configuration-model rewirings of the hidden edges, which preserve their degree sequence, so fame alone cannot clear the bar. Pass: above the null's 95th percentile by 0.05. The random-null AUROC, the fame gap, precision@k, the Brier score and the reliability gap are all reported as numbers beside the verdict.", params=(Param(name='exclude', kind='category', label='Label being held out', tip='A label this space must stay blind to, if the neighbourhoods are going to be used while scoring that label. Its whole closure is applied: the column, anything restating it, the experiment that produced it, and any edge layer built from it.', default=None, lo=0, hi=0, step=1, choices=(), optional=True, space='this'), Param(name='layer', kind='layer', label='Layer to hide in the self-test', tip="Which layer's edges are hidden and asked back when the strategy tests itself. It is removed from its own features, so the question is whether the OTHER evidence finds its edges; a dense layer gives a stricter test than a sparse one.", default=<function _layer_default>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='model', kind='choice', label='Model', tip='The logistic baseline on the pair features is interpretable and always fitted. The embedding adds spectral node vectors and is used for ranking only if it beats the baseline on the degree-matched null; otherwise it is reported and the baseline ranks.', default='logistic', lo=0, hi=0, step=1, choices=('logistic', 'embedding'), optional=False, space='this'), Param(name='top', kind='int', label='Gaps to list', tip='How many of the highest-probability pairs that no layer records to list. Every candidate pair is scored; this only limits what is shown and saved.', default=300, lo=10, hi=20000, step=50, choices=(), optional=False, space='this'), Param(name='min_probability', kind='float', label='Minimum probability', tip='Gaps below this calibrated probability are not listed. The probability is against a degree-matched non-pair, not an absolute posterior, so treat it as a ranking with a scale rather than as a chance of being true.', default=0.0, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this'), Param(name='fraction', kind='float', label='Share of orthogroups held out', tip='How much of the graph is hidden for scoring, by orthogroup. A larger share is a harder test on less training data; a smaller one leaves too few hidden edges in a sparse layer to score at all.', default=0.25, lo=0.1, hi=0.5, step=0.05, choices=(), optional=False, space='this'), Param(name='knn', kind='int', label='Nearest genes per gene', tip='How many nearest genes in measurement space become candidate neighbours for each gene, on top of its partners in every layer. This is what lets the space speak about a pair no network touches at all; zero restricts it to pairs some layer already links.', default=15, lo=0, hi=100, step=1, choices=(), optional=False, space='this'), Param(name='per_layer', kind='int', label='Neighbours kept per layer', tip='How many partners per gene are kept from each layer, strongest weight first. A dense layer such as the shared-compartment one would otherwise supply more than a hundred thousand pairs that all make the same claim.', default=25, lo=5, hi=200, step=5, choices=(), optional=False, space='this')), runner=<function _training_run>, tester=<function _training_test>, cost='minutes', needs=('at least one edge layer',), method='logistic / spectral embedding', task='ranking', techniques=('logistic_regression', 'spectral_embedding', 'degree_matched')), 'conformal_calls': Strategy(key='conformal_calls', number=35, title='Call genes with a stated error rate', family='Advanced models', question='Which genes can be given a label with a guaranteed error rate -- and for which does the data leave two or more labels equally possible?', tooltip="Turns a classifier's scores into prediction sets that contain the true label for at least 1 - alpha of new genes, calibrated on orthogroups the model never saw; calls a gene only where its set is a single label, and lists the genes the data cannot decide.", explanation="Every other label strategy returns a call and, at best, a score. A score is not an error rate: a probability of 0.8 from a logistic regression is right far more or far less than 80% of the time depending on the class and the data. Conformal prediction fixes that without assumptions about the model. The labelled genes are split by orthogroup; the model learns on one part, and on the other the strategy records how low a score the TRUE label can get. The threshold that covers all but alpha of those genes then defines, for every new gene, the set of labels that score within it -- and at least 1 - alpha of such sets contain the truth, for any model, as long as new genes resemble the calibration genes.\n\nA set of one label is a call made with that guarantee behind it. A set of two labels is a finding in itself -- the data cannot tell those compartments apart for this gene -- and an empty set says the gene is unlike every calibration gene. With per-class thresholds (Mondrian conformal) each class gets its own guarantee, so a rare compartment is not covered by borrowing the common ones' accuracy. The self-test checks both the calls and the promise: the precision of single-label calls against shuffled labels, and the share of hidden genes whose set contains their true label, against the 1 - alpha it promised.", walkthrough=('Choose the label; keep alpha at 0.1 (90% of sets hold the truth).', "Press Test: read 'set_coverage' in numbers -- it should be close to or above the promised coverage -- and 'mean_set_size' out of 'classes': how far the data narrows the answer.", "Press Run: 'calls' are single-label sets; 'prediction sets' lists every unlabelled gene with its set, smallest first.", 'Lower alpha for a stronger promise (bigger sets, fewer calls); switch the model to kNN for a faster, model-free base score.'), test_description="25% of the label hidden by whole orthogroups; the model is trained and calibrated on the rest (itself split by orthogroup) and builds a set for every hidden gene. Metric: set efficiency, 1 - (mean set size - 1) / (classes - 1) -- how far the sets narrow the possibilities. Null: 10 runs on shuffled labels, where the model learns nothing and the sets must grow to keep the promise. Pass: above the null's 95th percentile by 0.05. Also reported: set coverage on the hidden genes beside the 1 - alpha promised, and the scorecard of the single-label calls.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='alpha', kind='float', label='Error rate allowed (alpha)', tip='The share of new genes whose true answer may fall outside what is returned. 0.1 promises that at least 90% of prediction sets contain the true label (or 90% of intervals the true value); smaller alpha gives bigger sets and wider intervals.', default=0.1, lo=0.01, hi=0.5, step=0.01, choices=(), optional=False, space='this'), Param(name='model', kind='choice', label='Base model', tip='Where the scores the sets are built from come from: a class-balanced logistic regression on every permitted measurement, or the distance-weighted vote of the nearest labelled genes. The guarantee holds for either.', default='logistic', lo=0, hi=0, step=1, choices=('logistic', 'kNN'), optional=False, space='this'), Param(name='thresholds', kind='choice', label='Guarantee', tip="'per class' calibrates a threshold for each label separately (Mondrian conformal), so the promise holds within every class; 'overall' uses one threshold, which can over-cover common classes and under-cover rare ones.", default='per class', lo=0, hi=0, step=1, choices=('per class', 'overall'), optional=False, space='this'), Param(name='C', kind='float', label='Regularisation C', tip='Inverse penalty strength of the logistic base model: small values force a simple model, large values let it fit detail. Unused by the kNN base model.', default=0.5, lo=0.001, hi=100.0, step=0.1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this')), runner=<function _conformal_run>, tester=<function _conformal_test>, cost='minutes', needs=('a categorical column',), method='split conformal prediction', task='label calls', techniques=('split_conformal', 'logistic_regression', 'knn')), 'graph_convolution': Strategy(key='graph_convolution', number=36, title='Smooth the measurements along the networks, then classify', family='Advanced models', question="Does a gene's label follow from its own measurements together with those of its network neighbours -- and how much does the model lean on each?", tooltip="Averages every permitted measurement over each gene's partners in the measured networks, one and two steps out, and trains a class-balanced logistic regression on the gene's own profile beside its neighbourhood's -- a graph neural network without the black box.", explanation="Strategy 19 sees a gene's own measurements; strategies 11-13 see its partners' labels. Neither sees its partners' MEASUREMENTS, which is what a graph neural network learns from. This strategy does it in the simplest form that works well in practice, a simplified graph convolution: the permitted measured layers are merged into one graph with self-loops, normalised by degree, and multiplied into the measurement matrix once and twice. The result is three blocks per gene -- itself, its neighbourhood, and its neighbourhood's neighbourhood -- and a logistic regression learns from all three.\n\nOnly measurements travel along the edges, never labels, so a hidden gene's label cannot leak through its neighbours; the held-out label's closure is removed from both the columns and the layers first. The 'where the model looks' table is a finding: a compartment whose weight sits on the neighbourhood blocks is one the networks encode better than the gene's own profile does. For genes with no edges the neighbourhood blocks equal the gene itself, so the strategy degrades to strategy 19 rather than failing.", walkthrough=('Choose the label; keep two steps and C at 0.5.', "Press Test and compare its scorecard with strategy 19's: the difference is what the networks add.", "Press Run; read 'where the model looks' before the calls.", 'Try one step if the networks are dense -- two steps on a dense graph average most of the proteome together.'), test_description="Pattern 1: 25% of the label hidden by whole orthogroups; the smoothed features are computed from measurements only, and the model is trained on the visible genes. Metric: hidden genes called correctly. Null: the analytic chance level for the same predicted and true class mixes. Pass: above that level's 95% bound by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='hops', kind='int', label='Network steps', tip='How many steps of neighbourhood averaging are added as feature blocks. Zero is strategy 19; one adds direct partners; two adds partners of partners, which helps on sparse networks and blurs everything on dense ones.', default=2, lo=0, hi=4, step=1, choices=(), optional=False, space='this'), Param(name='C', kind='float', label='Regularisation C', tip="Inverse penalty strength of the logistic regression. Smoothing multiplies the number of features, so a stronger penalty (smaller C) than strategy 19's is often right.", default=0.5, lo=0.001, hi=100.0, step=0.1, choices=(), optional=False, space='this'), Param(name='min_probability', kind='float', label='Call when probability is at least', tip="A gene is called only when the model's top probability reaches this. Zero calls every gene; 0.6 keeps the calls the model is sure of.", default=0.0, lo=0.0, hi=1.0, step=0.05, choices=(), optional=False, space='this')), runner=<function _sgc_run>, tester=<function _sgc_test>, cost='a minute', needs=('a categorical column', 'measured edge layers'), method='graph convolution + logistic regression', task='label calls', techniques=('sgc', 'logistic_regression')), 'random_forest': Strategy(key='random_forest', number=37, title='Let a random forest find what defines a label', family='Advanced models', question='Which measurements, in which combinations and past which thresholds, define a label -- and which unlabelled genes carry that definition?', tooltip='Trains a class-balanced random forest on every permitted measurement, calls the unlabelled genes, and ranks the measurements by how much accuracy is lost on held-out orthogroups when each is shuffled -- the non-linear counterpart of strategy 19.', explanation="A logistic regression draws straight boundaries: more of this measurement, more of that class. Biology is often not like that. A secreted protein might be recognised by high tachyzoite expression AND a signal peptide, but not by either alone; an essential gene by fitness below a threshold whatever its other values. A random forest finds such interactions and thresholds by growing hundreds of decision trees on random subsets of genes and measurements and letting them vote, with each class weighted so the commonest compartment does not win by default.\n\nWhat it gains in flexibility it loses in transparency, so the strategy reports permutation importance rather than the forest's own impurity importance: each of the forest's top thirty measurements is shuffled among genes of orthogroups the forest never trained on, and the loss of balanced accuracy is recorded. A measurement the forest merely split on often scores near zero here, which is the point. Compare its scorecard with strategy 19's: if the forest is not better, the label's signal is linear and the simpler model's weights are the better explanation.", walkthrough=('Choose the label; keep 300 trees and a minimum leaf of 2.', 'Press Test and compare the scorecard with strategies 19 and 36.', "Press Run; read 'what defines the label' -- measurements whose importance is within two standard deviations of zero do not matter, whatever their impurity importance.", 'Raise the minimum leaf size if the labels are noisy; more trees only make the answer steadier, never more flexible.'), test_description="Pattern 1: 25% of the label hidden by whole orthogroups; the forest is trained on the visible genes and calls the hidden ones. Metric: hidden genes called correctly. Null: the analytic chance level for the same predicted and true class mixes. Pass: above that level's 95% bound by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='trees', kind='int', label='Trees', tip='How many decision trees vote. More trees give a steadier answer and a smoother probability, never a more flexible model; 300 is past the point of change for a few hundred measurements.', default=300, lo=20, hi=2000, step=20, choices=(), optional=False, space='this'), Param(name='min_leaf', kind='int', label='Genes per leaf at least', tip='The smallest group of genes a tree may end on. Larger leaves smooth the model and resist noisy labels; 1 lets every tree memorise its training genes.', default=2, lo=1, hi=50, step=1, choices=(), optional=False, space='this')), runner=<function _forest_run>, tester=<function _forest_test>, cost='a minute', needs=('a categorical column',), method='random forest + permutation importance', task='label calls', techniques=('random_forest', 'permutation_importance')), 'stacking': Strategy(key='stacking', number=38, title='Learn how much to trust each kind of evidence', family='Advanced models', question='Given measurement neighbours, a linear model and the measured networks, how should their answers be combined for THIS label -- and what does the combination call?', tooltip='Runs three kinds of evidence out of fold on the labelled genes and trains a logistic meta-model on their class scores, so the weight each earns is learned per label and per class rather than fixed; reports that trust beside the calls.', explanation="Strategy 31 calls a gene when enough independent strategies agree; strategy 12 weights each source by one accuracy figure. Stacking learns the combination instead. Each kind of evidence -- the nearest genes in the measurements, a logistic regression on them, and the vote of network partners -- predicts every labelled gene out of fold, from orthogroups it was not trained on, and a second logistic regression learns from those predictions which evidence to believe for which class. The networks might be decisive for complexes and useless for secreted proteins; the meta-model can learn exactly that, and whether a network's silence (no labelled partner) is itself informative.\n\nBecause the meta-model only ever sees out-of-fold predictions, it cannot learn to trust a base model for having memorised its training genes -- the classic failure of naive blending. 'trust by evidence' reports each source's share of the meta-model's weight, a per-label answer to which experiments matter. Stacking is usually the most accurate strategy here and the slowest; its scorecard beside strategies 07, 13 and 19 says how much combining is worth for your label.", walkthrough=('Choose the label; keep 5 folds and 15 neighbours.', 'Press Test (several model fits; a few minutes) and compare the scorecard with 07, 19 and 31.', "Press Run; read 'trust by evidence' before the calls.", 'Fewer folds are faster and give the meta-model noisier training data; more are slower and steadier.'), test_description="Pattern 1: 25% of the label hidden by whole orthogroups; base predictions for the meta-model are out of fold within the visible genes only, so no hidden label reaches either level. Metric: hidden genes called correctly. Null: the analytic chance level for the same predicted and true class mixes. Pass: above that level's 95% bound by 0.05.", params=(Param(name='target', kind='category', label='Held-out label', tip='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.', default=<function default_category>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='k', kind='int', label='Neighbours to consult', tip="How many labelled genes vote on each call. Few neighbours follow fine local structure and are noisy; many are stable and blur small classes into large ones. Fifteen is the project's default everywhere a neighbourhood is scored.", default=15, lo=3, hi=200, step=1, choices=(), optional=False, space='this'), Param(name='folds', kind='int', label='Out-of-fold splits', tip="How many orthogroup folds the base predictions are made in. Each fold's genes are predicted by base models that never saw them, which is what keeps the meta-model honest.", default=5, lo=3, hi=10, step=1, choices=(), optional=False, space='this')), runner=<function _stack_run>, tester=<function _stack_test>, cost='minutes', needs=('a categorical column',), method='stacked logistic regression', task='label calls', techniques=('stacking', 'knn', 'logistic_regression', 'network_vote', 'grouped_cv')), 'conformal_values': Strategy(key='conformal_values', number=39, title='Predict a value with an interval that holds', family='Advanced models', question='For a gene never measured, what value is expected -- and within what range, with a guaranteed chance of containing the truth?', tooltip='Predicts a measurement from every other permitted one and wraps each prediction in an interval calibrated on orthogroups the model never saw, so at least 1 - alpha of new genes fall inside; measured genes outside their interval are listed as surprises.', explanation="Strategy 21 predicts a value; this one says how far to trust it, in the measurement's own units. The measured genes are split by orthogroup: a model learns on one part, and on the other the strategy records how large its errors are. The conformal quantile of those errors becomes the half-width of every interval, and at least 1 - alpha of new genes' true values fall inside -- a guarantee that holds for any model, provided new genes resemble the calibration genes.\n\nTwo readings follow. For unmeasured genes, an interval narrower than the measurement's spread is a prediction worth acting on, and one as wide as the spread says the other measurements know little about this one. For measured genes, a value outside its interval is a surprise the rest of the data cannot explain -- by construction about alpha of genes will be, so the list is ranked by how far outside, and the count is shown beside what chance predicts. The self-test scores the predictions against hidden values and checks the coverage promised on them.", walkthrough=('Choose a measurement (a fitness score is the classic case); keep alpha at 0.1.', "Press Test: read 'interval_coverage' against 'promised_coverage', and the interval width in standard deviations.", "Press Run; 'predicted intervals' fills unmeasured genes, 'surprises' lists measured ones outside their range.", 'Switch to ridge for a faster, linear model; leave its own kind of measurement out so the answer is not one screen predicting another.'), test_description='Pattern 4: 20% of the measured values hidden; the model is trained and calibrated on the rest (split by orthogroup) and predicts the hidden ones. Metric: rank correlation of predicted and hidden values. Null: the chance distribution of a rank correlation, with refits on shuffled values reported. Pass: above its 95th percentile by 0.1. Also reported: the share of hidden values inside their intervals, beside the coverage promised.', params=(Param(name='target', kind='number', label='Measurement to predict', tip="The numeric measurement to predict from everything else. Its own closure -- the same experiment's other outputs and anything restating it -- is withheld first.", default=<function default_numeric>, lo=0, hi=0, step=1, choices=(), optional=False, space='this'), Param(name='alpha', kind='float', label='Error rate allowed (alpha)', tip='The share of new genes whose true answer may fall outside what is returned. 0.1 promises that at least 90% of prediction sets contain the true label (or 90% of intervals the true value); smaller alpha gives bigger sets and wider intervals.', default=0.1, lo=0.01, hi=0.5, step=0.01, choices=(), optional=False, space='this'), Param(name='model', kind='choice', label='Model', tip="'boosted' is gradient-boosted trees: nonlinear, tolerant of missing values, slower. 'ridge' is a penalised straight-line fit: fast, and its weights can be read.", default='boosted', lo=0, hi=0, step=1, choices=('boosted', 'ridge'), optional=False, space='this'), Param(name='own_kind', kind='choice', label='Its own kind of measurement', tip="'leave out' removes every column measured the same way as the target -- all the fitness screens, when the target is a fitness screen -- so the answer says what OTHER evidence knows. 'include' keeps them, which mostly measures how well the screens agree with each other.", default='leave out', lo=0, hi=0, step=1, choices=('leave out', 'include'), optional=False, space='this')), runner=<function _interval_run>, tester=<function _interval_test>, cost='a minute', needs=('a numeric column',), method='gradient boosting / ridge + split conformal', task='values', techniques=('gradient_boosting', 'ridge', 'split_conformal', 'grouped_cv'))}
def register(strategy: Strategy) -> Strategy:

Add a strategy to the catalogue. A second strategy under one key is refused.

def catalog() -> list:

Every strategy, in catalogue order.

def get(key: str) -> Strategy:

One strategy by key, or a KeyError naming the ones that exist.

def families() -> list:

The families, in the order their first strategy appears.

def overview(organism: str = 'Tg') -> pandas.DataFrame:

Every strategy in one table: number, key, family, name, method, the question it answers, and -- where the calibration sweep measured it -- its grade and skill at defaults and tuned.

def metrics(task: str | None = None) -> pandas.DataFrame:

Every scorecard metric (or one task's), with its definition, range, chance level and how to read it -- the glossary the Strategies tab shows beside each number.

def techniques() -> pandas.DataFrame:

Every technique the strategies are built from: what it does and why a strategy uses it.

def calibration(key: str, organism: str = 'Tg') -> dict:

What the calibration sweep measured for key: grade, skill at defaults and tuned with 95% intervals, per held-out target and per setting. Empty if it has not been measured.

def tuned(key: str, organism: str = 'Tg') -> dict:

The setting calibration found best for key -- pass it on: run(key, **tuned(key)).

def shipped(organism: str = 'Tg') -> Context:

The packaged table of one organism, loaded once and kept, so repeated calls share maps.

def run( key: str, organism: str = 'Tg', ctx: Context | None = None, **settings) -> StrategyResult:

Run strategy key on the shipped table of organism (or on ctx) with settings.

The one-line entry point: strategies.run("geneset_hunt", genes=my_list).

def test( key: str, organism: str = 'Tg', ctx: Context | None = None, **settings) -> TestResult:

Self-test strategy key with settings: hide what is known, ask for it back, score it.

class Context:

One organism's table and graph, with the leakage guard and the caches strategies share.

Built once and handed to every strategy, so a map one strategy embedded is not embedded again by the next, and a closure computed for compartment is computed once. log is called with progress; should_stop is asked between expensive steps and a stop unwinds with jobs.Stopped, which the job runner reports as stopped rather than failed.

graph is a mapping of layer__a, layer__b, layer__w (and optionally layer__r) arrays, positions into nodes; None loads the shipped graph for the organism, and only if it was built over this exact table. other is the other organism's context (or table), for strategies that cross species; None loads the shipped one on first use.

Context( nodes: pandas.DataFrame, graph=None, other=None, organism: str | None = None, seed: int = 42, log=None, should_stop=None)

Wrap a gene table; nothing expensive happens until a strategy asks for it.

nodes
n
gene_ids
organism
seed
log
should_stop
@classmethod
def shipped(cls, organism: str = 'Tg', **kw) -> Context:

The packaged table for one organism, as the application loads it.

def bound(self, log=None, should_stop=None) -> Context:

The same table, graph and caches, with a job's own progress log and stop flag.

Two jobs running at once must not report through each other's log or stop each other, and they should still share the maps either one has already built -- so the caches are shared and only the two callbacks are the job's own.

def check(self):

Unwind with jobs.Stopped if a stop was asked for.

def say(self, message: str):

Report progress, honouring a stop first.

def rng(self, salt: int = 0) -> numpy.random._generator.Generator:

A generator seeded from the context's seed, so every run is repeatable.

index: dict

gene id -> position, case-insensitive.

def resolve_genes(self, text) -> tuple:

(positions, unresolved): a gene list, as typed or pasted, matched to this table.

Separators are anything that is not part of an identifier, so a column pasted from a spreadsheet, a comma list and a space list all work. Matching is exact on the accession, ignoring case -- a partial match would silently answer for the wrong gene.

def product(self, positions) -> list:

Product descriptions for some genes, blank where the table has none.

def groups(self) -> numpy.ndarray:

A group id per gene -- its orthogroup, so paralogs are hidden together, never split.

A gene whose paralog stays visible is recovered by resemblance to its own copy, which is not the inference being tested. Genes with no orthogroup are their own group.

def truth(self, column: str) -> pandas.Series:

A column as labels: plain strings, with every way of saying 'not measured' as NaN.

def values(self, column: str) -> pandas.Series:

A column as numbers, NaN where unmeasured. Booleans are 0/1.

def categorical_columns(self, min_labelled: int = 30, max_classes: int = 60) -> list:

Columns that can be held out as a category: 2..max_classes classes, enough labels.

def numeric_columns(self, exclude=(), min_values: int = 30) -> list:

Measurements a strategy may use as features: numeric, varying, measured often enough.

def numeric_targets(self) -> list:

Numeric columns worth predicting: at least 50 values and at least 5 distinct ones.

def banned(self, target) -> set:

Every column a strategy scored against target must not see, by the project's closure.

def banned_layers(self, target) -> set:

Every edge layer a strategy scored against target must not traverse.

def features(self, target=None, exclude=(), columns=None) -> tuple:

(matrix, column names): the permitted measurements, rank-scaled and median-filled.

def matrix(self, columns, impute: bool = True) -> numpy.ndarray:

Columns rank-scaled to [-0.5, 0.5]; missing values at the median (0) unless not impute.

Ranks rather than z-scores because the screens here have inverted sign conventions, spreads that differ 64-fold and heavy tails -- the same reason embedding defaults to rank.

def blocks(self, target=None) -> dict:

block name -> columns, for the blocks a map scored against target may be built from.

The project's own blocks where the table carries them, dropped whole when any column is banned -- the rule search applies, since a block's other columns came from the same experiment. A table the slot catalogue does not describe is grouped by column prefix.

def family_of(self, block: str) -> str:

The kind of measurement a block is: the slot catalogue's axis, where it has one.

The axis, not the block's name: Tg_essentiality_in_a_second_background and Tg_genetic_interaction_delta_gra17 are knockout screens like Tg_fitness_hff_in_vitro, and grouping by the word in the name let them stand in for the fitness screens a strategy had just left out -- predicting fibroblast fitness at 0.89 from "other" evidence that was a second knockout screen. A table the catalogue does not describe falls back to the prefix.

def families(self, target=None) -> dict:

family -> blocks, so a walk can ask which KIND of measurement carries a signal.

graph: dict

The edge layers, as arrays keyed layer__a and so on. Empty when there is none.

def layers(self) -> list:

The edge layers that exist and carry at least one edge.

def measurement_layers(self, target=None) -> list:

Layers that are measurements rather than literature or derived, minus the banned ones.

def adjacency(self, layer: str, weight: str = 'w'):

One layer as a symmetric sparse matrix of raw weights (w, r, or binary).

def operator(self, layer: str):

One layer degree-normalised for a random walk, as methods.layer_matrix builds it.

def other(self) -> Context | None:

The other organism, for strategies that cross species. None when it is not available.

def sample_rows(self, size: int | None, target=None, must=()) -> numpy.ndarray:

Positions to embed: all of them, or size of them favouring the ones that can be scored.

must are always included -- a gene list's members, say. Labelled genes of target fill up to 70% of the rest, so a sample always carries something to score against; the remainder is unlabelled genes, because a map of labelled genes only is a map of the studied proteome.

def embed( self, blocks, n_neighbors: int = 25, min_dist: float = 0.1, rows=None, target=None) -> tuple:

(coords, positions): a 3-D map of rows built from blocks, by the project's builder.

Cached on everything that changes the answer. positions is which genes the map places -- all of rows unless the builder dropped one with no usable measurement.

def cluster( self, coords, min_cluster_size: int = 25, selection: str = 'eom') -> numpy.ndarray:

HDBSCAN labels for a map, -1 for noise, as every clustering here is made.

selection is HDBSCAN's: "eom" keeps the most persistent clusters, "leaf" every leaf of the cluster tree. On the real proteome "eom" returns two to six clusters, the largest holding most of the map -- a structure no label can dominate -- while "leaf" returns dozens of small, purer ones and calls the rest noise. Strategies that name clusters need "leaf"; a walk tries both.

def blind_map( self, target=None, size: int | None = 3000, n_neighbors: int = 25, min_dist: float = 0.1, min_cluster_size: int = 25, must=(), selection: str = 'leaf') -> tuple:

(coords, positions, labels): one map built without looking at target at all.

def same_kind(self, column: str) -> set:

Every column measured the same WAY as column -- its whole family of blocks.

Not a leak in the closure's sense: fibroblast fitness and macrophage fitness are different experiments. But predicting one knockout screen from another mostly says the screens agree, and strategies that ask what OTHER evidence says leave the whole family out.

def hide( truth: pandas.Series, frac: float = 0.25, seed: int = 0, groups=None, min_class: int = 15) -> tuple:

(visible labels, hidden positions): a share of the labelled genes lose their label.

Classes smaller than min_class are dropped from both sides -- they cannot be split into a part to learn from and a part to test on. With groups, whole groups are hidden together, so a gene is never recovered by resemblance to a paralog left visible.

def shuffled( visible: pandas.Series, rng: numpy.random._generator.Generator) -> pandas.Series:

The same labels on the same genes, dealt out at random: the null every label test uses.

def correct_rate(pred: pandas.Series, truth: pandas.Series, positions) -> float:

Share of positions called correctly. An abstention (NaN) counts as not correct.

Deliberately not accuracy-over-calls, which a strategy can raise by abstaining on anything hard; counting abstentions as misses makes coverage part of the score.

def call_precision(pred: pandas.Series, truth: pandas.Series, positions) -> tuple:

(precision over calls, calls made) on positions -- for strategies that trade reach for trust.

def per_class(pred: pandas.Series, truth: pandas.Series, positions) -> pandas.DataFrame:

Per hidden class: how many, how many recovered, and how precise the calls of that class were.

def analytic_null(pred: pandas.Series, truth: pandas.Series, positions) -> tuple:

(mean, sd) of correct_rate if predictions were independent of the truth.

Cohen's chance term: the product of the predicted and true class shares, summed. Used where re-running a costly strategy on shuffled labels ten times would take longer than it tells.

def with_class_scores(pred: pandas.Series, scores: pandas.DataFrame) -> pandas.Series:

Attach per-class scores (genes x classes, higher = more likely) to a label prediction.

The label test reads them back for the scorecard's macro AUROC and AUPRC. A prediction without them is still scored on everything else; those two are reported missing, not estimated.

def class_scores_of(pred) -> pandas.DataFrame | None:

The per-class scores attached by :func:with_class_scores, or None.

def expand_prediction(pred, positions, n: int) -> pandas.Series:

A prediction over a subset of genes (a map's sample) spread over all n, scores included.

def knn_vote( X: numpy.ndarray, visible: pandas.Series, k: int = 15, query=None) -> tuple:

(prediction, vote share) from the k nearest labelled genes, distance-weighted.

A labelled gene is never its own neighbour. Genes are compared on the rows of X given, which is whatever space the strategy chose -- measurements, or a map.

def onehot(visible: pandas.Series) -> tuple:

(matrix genes x classes, class names) for the labelled genes; unlabelled rows are zero.

def graph_vote(A, visible: pandas.Series, query=None, min_support: float = 0.0) -> tuple:

(prediction, support) from labelled neighbours in one weighted graph, one hop.

Abstains where a gene has no labelled neighbour: no edge is no evidence, and a guess from class frequencies would be a guess dressed as a network result.

def propagate( operator, visible: pandas.Series, query=None, restart: float = 0.5, iterations: int = 30) -> tuple:

(prediction, field strength): random walk with restart, one field per class.

The fields are seeded from visible genes only, so a hidden gene's own label never seeds its own score. Abstains where no class reaches the gene at all.

def diffuse( operator, seeds, restart: float = 0.3, iterations: int = 30) -> numpy.ndarray:

Random walk with restart from a set of seed genes: one score per gene, seeds included.

def knn_operator(X: numpy.ndarray, k: int = 15):

A symmetric, degree-normalised k-nearest-neighbour graph over the rows of X.

What lets a measurement matrix be walked like an edge layer, so a diffusion can use the table and the measured networks in one operator.

def cluster_majority( labels: numpy.ndarray, visible: pandas.Series, min_share: float = 0.5, min_labelled: int = 5) -> dict:

cluster -> (label, share, labelled count) where one visible label dominates the cluster.

def predict_from_clusters( labels: numpy.ndarray, positions, visible: pandas.Series, n: int, min_share: float = 0.5, min_labelled: int = 5) -> tuple:

(prediction over all genes, the cluster table): every member of a dominated cluster is called.

def set_f1(labels: numpy.ndarray, members: numpy.ndarray) -> tuple:

(best cluster, precision, recall, F1) of a gene set against one clustering, noise excluded.

members is a boolean mask over the clustered genes. The best cluster is the one with the highest F1 -- the question is whether ONE cluster holds the set, not how many it touches.

def default_category(ctx: Context) -> str | None:

The held-out label a strategy starts on: the measured localization where one exists.

def default_numeric(ctx: Context) -> str | None:

The numeric trait a regression starts on: the canonical fitness screen where one exists.

def example_set( ctx: Context, target: str | None, lo: int = 30, hi: int = 300, aim: int = 120) -> tuple:

(category, positions): a known gene set to test a gene-list strategy on.

The category of target whose size is closest to aim within [lo, hi] -- big enough to split into a query and a hidden part, small enough to be a set someone would actually hand over.

def label_transfer_test( ctx: Context, key: str, target: str, predict: Callable, *, frac: float = 0.25, n_null: int = 10, min_effect: float = 0.05, restrict=None, null: str = 'shuffle', metric: str = None, precision: bool = False, note: str = '', truth=None) -> TestResult:

Pattern 1: hide a share of a label, predict it back, and compare with shuffled labels.

predict(visible) returns a prediction over all genes (NaN to abstain) from the visible labels alone. truth replaces the column's own labels when a strategy scores a derived label -- the first digit of an EC number, say. restrict is a boolean mask over genes: only hidden genes inside it are scored -- how a strategy that can only speak about some genes (those with a crosslink partner, say) is tested on exactly those. With precision, the metric is precision over calls instead of correct calls per hidden gene -- for strategies whose point is to abstain.

def set_expansion_test( ctx: Context, key: str, members, rank: Callable, *, frac: float = 0.3, n_null: int = 10, min_effect: float = 0.1, universe=None, label: str = 'the gene set', note: str = '') -> TestResult:

Pattern 2: hand over part of a gene set, and ask whether the rest ranks near the top.

rank(query_positions) scores every gene. The metric is the AUROC of the hidden members against every other gene not in the query; the null is the same procedure on random sets of the same size drawn from the same universe (all genes by default).

def pair_test( key: str, score_fn: Callable, positives: numpy.ndarray, negatives: numpy.ndarray, null_fn: Callable, *, n_null: int = 5, min_effect: float = 0.05, hidden: str, null_kind: str, t0: float, note: str = '', ctx: Context | None = None, metric: str = 'AUROC of hidden pairs against random non-pairs') -> TestResult:

Pattern 3: hidden pairs against non-pairs, scored by AUROC, against a permuted null.

score_fn(pairs) and null_fn(pairs, i) return one score per pair (rows of [a, b]). metric names how the non-pairs were drawn, because that decides what the number means: against random non-pairs an AUROC mostly measures degree, against degree-matched ones it measures the pair.

def value_test( ctx: Context, key: str, y: pandas.Series, predict: Callable, *, frac: float = 0.2, n_null: int = 3, min_effect: float = 0.1, label: str = 'the values', note: str = '') -> TestResult:

Pattern 4: hide a share of measured values, predict them, and rank-correlate.

predict(visible_y) returns a prediction for every gene from the visible values only. The bar is the exact chance distribution of a rank correlation between independent variables -- centred on zero with standard deviation 1/sqrt(n - 1) -- because three or five refits on shuffled values give a 95th percentile too unstable to judge by: on a table with nothing in it, three refits once averaged -0.10 and let a correlation of 0.10 through. The refits are still run and reported, as a check that a model trained on nothing predicts nothing.

def replication_test( ctx: Context, key: str, find: Callable, replicate: Callable, scramble: Callable, positions, *, n_null: int = 20, min_effect: float = 0.2, min_findings: int = 3, label: str = 'findings', note: str = '') -> TestResult:

Pattern 5: make findings on half the genes, and ask whether they hold on the other half.

find(half_a) returns findings from genes in half_a only; replicate(finding, half_b, state) says whether one holds on half_b; scramble(half_b, rng) returns a state in which the second half's evidence is permuted, and None means the real one. The metric is the share of findings that replicate; the null is that share with the second half scrambled.

def planted_context( n: int = 480, seed: int = 7, null: bool = False) -> Context:

A synthetic organism whose answers are known, for testing that each strategy can find them.

Every signal the strategies look for is planted: localization carried by expression and by a crosslink layer, a second label carried by fitness, protein families shared through a structural layer, paralogs, a second species with orthologs and correlated fitness, attention that tracks fame rather than biology, and a condition screen that depends on a host-facing trait. With null, the same table is built with every label and every edge dealt out at random, so nothing is there to find -- which is what every self-test must then report.

Column names follow the real tables where a strategy's default reaches for them, so the same defaults work here and on the shipped data.