Edit on GitHub

starplast.guided

Start here: the question tree that turns what a user HAS into the strategies worth running.

The Strategies tab is a catalogue. A catalogue answers "what is there", and someone who arrives with a gene list from a screen and no idea which of thirty-nine named methods applies to it is not helped by being shown all thirty-nine. This module is the other way round: it asks what the person has, what they want to know, and ends at a short ranked list of strategies with their settings already filled in from the answers.

It holds no Qt, so the tree and the recommendations are testable headless and can be driven from a notebook::

from starplast import guided, strategies
ctx = strategies.shipped("Tg")                   # any Context
answers = {"have": guided.HAVE_LABEL, "space": ctx.organism, "label": "compartment",
           "goal": guided.PREDICT}
for r in guided.recommend(answers, ctx):
    print(r.number, r.title, "--", r.why)

The tree. One question per screen, and a screen's options may depend on every answer before it:

have      what the person has: one gene, a gene list, their own measurement, a label, nothing
space     which organism's table to work in (the registry's available spaces)
gene /    the subject itself: the gene, the gene set, the label column or the numeric column,
genes /   the last two listed with their coverage so an empty column is never chosen blind
label /
measure
label     asked a second time only when a single gene is to be given a label
goal      what they want out of it, from the goals that make sense for what they have
baseline  a second measurement, asked only when the goal is to compare two conditions

:func:route is the whole branching, :func:next_step the one question to ask now, and an answer dictionary with no next_step is finished. Every first answer reaches an end state, and every option leads somewhere: tests/test_guided.py walks the tree exhaustively to check exactly that.

The recommendations. :func:recommend ranks strategies by, in order of weight:

  1. whether the strategy is one of the handful this goal and this kind of subject are FOR (KEYS), best first;
  2. whether its scorecard task is one the goal asks for (GOALS[...].tasks);
  3. what the calibration sweep measured for it ON THIS SPACE (GRADE_WEIGHT) -- a strategy that is reliable for Toxoplasma and weak for Plasmodium is recommended differently in the two;
  4. how long it takes, as a tie-breaker only.

A strategy whose settings cannot be filled from this table at all (no numeric column, no edge layer, no second species) is dropped before ranking rather than recommended and then failing, and a strategy that cannot use the subject the person chose is dropped too -- a label-calling method is not offered for a numeric screen. :func:caveat says plainly when the best thing available is only weak, and :func:views offers the map gallery or the star map where those answer the question better than any strategy does.

Nothing here computes anything about the data beyond reading the columns' coverage: it decides what to offer, and strategies does the work.

HAVE_GENE = 'one gene'
HAVE_SET = 'a gene list'
HAVE_NUMBER = 'my own measurement'
HAVE_LABEL = 'a label'
HAVE_NOTHING = 'nothing yet'
SUBJECT = {'one gene': 'gene', 'a gene list': 'genes', 'my own measurement': 'measure', 'a label': 'label', 'nothing yet': ''}
MORE_LIKE = 'more like mine'
PREDICT = 'predict it'
EXPLAIN = 'explain it'
PARTNERS = 'partners'
COMPARE = 'compare'
LEARNABLE = 'learnable'
TOUR = 'show me'
@dataclass(frozen=True)
class Option:

One answer to one question: what is stored, what is shown, and the tooltip that explains it.

Option(value: str, label: str, hint: str = '', detail: str = '')
value: str
label: str
hint: str = ''
detail: str = ''
@dataclass(frozen=True)
class Step:

One screen: its question in a few plain words, and what kind of answer it takes.

:ivar kind: choice (pick one of :func:options), gene (one identifier), genes (a list, however it is supplied) or column (a choice whose options carry coverage).

Step(key: str, question: str, kind: str, tip: str, note: str = '')
key: str
question: str
kind: str
tip: str
note: str = ''
@dataclass(frozen=True)
class Goal:

One thing a person can want, the scorecard tasks that answer it, and how to say why.

Goal( key: str, label: str, hint: str, tasks: tuple, reason: str, noun: str = '')
key: str
label: str
hint: str
tasks: tuple
reason: str
noun: str = ''
GOALS = {'more like mine': Goal(key='more like mine', label='Find more genes like mine', hint='Rank every other gene by how much it resembles the ones you gave, and say how often that ranking has been right on gene sets whose answer was known.', tasks=('ranking', 'set retrieval'), reason='ranks every other gene by how much it looks like yours', noun='finding more genes like yours'), 'predict it': Goal(key='predict it', label='Predict it for the genes nobody has measured', hint='Learn the chosen label or measurement from the genes that have one, and give a value to the genes that have none.', tasks=('label calls', 'values'), reason='gives the unmeasured genes a value and says how often that value is right', noun='predicting it for the unmeasured genes'), 'explain it': Goal(key='explain it', label='Explain what defines it', hint='Say which measurements carry the thing you chose, which are redundant, and which genes sit where they should not.', tasks=('label calls', 'ranking', 'values', 'replication'), reason='says which measurements carry it and which add nothing', noun='explaining what defines it'), 'partners': Goal(key='partners', label='Find partners or complex members', hint='Rank the genes most likely to be physically or functionally tied to yours, with the evidence each link rests on.', tasks=('ranking',), reason='ranks the genes most likely to be tied to yours, with the evidence for each', noun='finding partners'), 'compare': Goal(key='compare', label='Compare two conditions', hint='Ask which genes matter more in one condition than the other measurements say they should -- and whether the rest of the table explains which.', tasks=('values', 'replication'), reason='finds the genes that shift between your two measurements more than the rest of the table explains', noun='comparing two conditions'), 'learnable': Goal(key='learnable', label='Check whether it is learnable at all', hint='Before trusting any prediction: is this thing in the measurements at all, and for which of its categories?', tasks=('cluster recovery', 'label calls', 'replication', 'ranking'), reason='measures whether the data holds it at all, before anything is predicted from it', noun='checking whether it is learnable at all'), 'show me': Goal(key='show me', label='Show me what this data can do', hint='The strategies that need nothing from you: what the measurements organise, and what they link that nobody has written down.', tasks=('replication', 'ranking', 'cluster recovery'), reason='needs nothing from you and says what the measurements already organise', noun='a first look at what this data holds')}
GOALS_FOR = {'one gene': ('partners', 'more like mine', 'predict it'), 'a gene list': ('more like mine', 'partners', 'explain it', 'learnable'), 'a label': ('predict it', 'explain it', 'learnable'), 'my own measurement': ('predict it', 'compare', 'explain it', 'learnable'), 'nothing yet': ()}
KEYS = {('more like mine', 'gene'): ('seed_expansion', 'neighbour_space', 'link_prediction', 'set_enrichment'), ('more like mine', 'genes'): ('set_enrichment', 'seed_expansion', 'positive_unlabeled', 'neighbour_space', 'geneset_hunt', 'network_training'), ('partners', 'gene'): ('link_prediction', 'unwritten_links', 'neighbour_space', 'seed_expansion', 'structural_homology'), ('partners', 'genes'): ('link_prediction', 'unwritten_links', 'neighbour_space', 'seed_expansion', 'multiplex_modules'), ('predict it', 'gene'): ('feature_knn', 'layer_propagation', 'triangulation', 'supervised_classifier', 'physical_partners', 'conformal_calls'), ('predict it', 'label'): ('supervised_classifier', 'conformal_calls', 'random_forest', 'stacking', 'graph_convolution', 'feature_knn', 'cluster_guilt', 'layer_propagation', 'triangulation'), ('predict it', 'measure'): ('trait_regression', 'conformal_values', 'masked_imputation'), ('explain it', 'genes'): ('set_enrichment', 'geneset_hunt', 'blind_battery'), ('explain it', 'label'): ('block_ablation', 'recoverability_atlas', 'supervised_classifier', 'label_outliers', 'blind_battery'), ('explain it', 'measure'): ('trait_regression', 'masked_imputation', 'blind_battery'), ('learnable', 'genes'): ('geneset_hunt', 'set_enrichment'), ('learnable', 'label'): ('recoverability_atlas', 'holdout_search', 'block_ablation', 'supervised_classifier', 'blind_battery'), ('learnable', 'measure'): ('trait_regression', 'masked_imputation', 'conformal_values'), ('compare', 'measure'): ('condition_shift', 'trait_regression', 'split_clusters'), ('show me', ''): ('blind_battery', 'recoverability_atlas', 'unwritten_links', 'link_prediction', 'consensus_modules', 'label_outliers')}
GRADE_WEIGHT = {'reliable': 1.5, 'works when tuned': 0.8, 'weak': -0.4, 'no skill': -3.0, 'untestable': -1.0, '': 0.0}
SUBJECT_WEIGHT = 0.7
COST_WEIGHT = {'seconds': 0.15, 'a minute': 0.05, 'minutes': 0.0}
TRUSTED = ('reliable', 'works when tuned')
STEPS = {'have': Step(key='have', question='What do you have?', kind='choice', tip='Everything else follows from this answer, and you can come back and change it from the trail at the top.', note=''), 'space': Step(key='space', question='Which organism?', kind='choice', tip='The table the questions are asked of. Each species is measured, and its strategies calibrated, on its own.', note=''), 'gene': Step(key='gene', question='Which gene?', kind='gene', tip='Type an accession, a symbol or a word from the product description, and pick the gene from the list.', note='search by id, symbol or product'), 'genes': Step(key='genes', question='Which genes?', kind='genes', tip='The set the strategies start from: paste it, load a file, take the genes gated on the map, or try an example set of a known category.', note='paste, load a file, use the gated genes, or try an example'), 'label': Step(key='label', question='Which label?', kind='column', tip='The annotation the strategies work on. Each is shown with how many genes carry it and how many classes it has, so an almost-empty column is never picked by accident.', note='how many genes carry each one is shown'), 'measure': Step(key='measure', question='Which measurement?', kind='column', tip='The number per gene the strategies work on -- a screen, a fitness score, an abundance. Each is shown with how many genes have a value.', note='how many genes have a value is shown'), 'goal': Step(key='goal', question='What do you want to know?', kind='choice', tip='The goal decides which strategies are recommended and how they are ranked.', note=''), 'baseline': Step(key='baseline', question='Compared with what?', kind='column', tip='The second measurement: the baseline the first one is compared against, such as growth in the dish behind growth in the animal.', note='the measurement the first is compared against')}
def answered(answers, key: str) -> bool:

Whether key holds a real answer -- an empty list or an empty string is not one.

def subject(answers) -> str:

Which subject step this branch uses: gene, genes, label, measure or none.

def goal_of(answers) -> str:

The goal in force: the chosen one, or the tour that "nothing yet" implies.

def route(answers) -> list:

The steps this branch asks, in order, as far as the answers so far determine them.

The list grows as questions are answered: an unanswered step is the last entry, because what comes after it is not yet known. When every step in the route is answered the route is complete and :func:next_step returns nothing.

def next_step(answers) -> str:

The one question to ask now, or "" when the answers are complete and the list is due.

def done(answers) -> bool:

Whether this branch has reached its end state: the recommendations.

def trail(answers) -> list:

(step, value, shown) for every answer so far, in the order they were given.

The progress trail at the top of the panel, and the way back to any earlier question.

def shown(step: str, value) -> str:

One answer in a few words, as the trail shows it.

def options(step: str, answers, ctx=None) -> list:

The options of one step, given the answers before it and the table they are asked of.

A gene or genes step has no fixed options -- the answer is an identifier or a list -- and returns an empty tuple, which is how the view knows to draw a search box instead of buttons.

def categorical_options(ctx) -> list:

Every label that can be held out, each with its coverage: the space's own held-out labels first, then the rest by how many genes carry them.

Coverage is on the option, not behind it, because the commonest way to waste an afternoon here is to pick a column that eleven genes carry; and the space's declared targets lead because a column with a value for every gene is usually a derived one, not a measured one.

def numeric_options(ctx) -> list:

Every numeric column worth predicting, the space's own screens first, then by coverage.

def example_genes(ctx, answers=None) -> tuple:

(category, gene ids): a real gene set to try the gene-list branch on, from the default label.

The same set strategies.example_set gives the Strategies tab's "example" button, so the two offer the same thing.

def read_gene_file(path: str) -> list:

Every identifier in a gene-list file: the first column of a CSV or TSV, or a plain list.

Qt-free, so a list can be loaded in a script exactly as the panel's "Load a file" button loads it, and so the reading is testable without a file dialog.

def gene_ids(answers, ctx) -> list:

The genes the answers name, resolved against this table; empty when none were given.

@dataclass(frozen=True)
class Recommendation:

One recommended strategy: why it is here, and the settings the answers already decided.

Recommendation( key: str, number: int, title: str, method: str, task: str, grade: str, score: float, why: str, settings: dict = <factory>, weak: bool = False)
key: str
number: int
title: str
method: str
task: str
grade: str
score: float
why: str
settings: dict
weak: bool = False
def usable(strategy, ctx) -> bool:

Whether this table can fill everything the strategy needs before a user chooses anything.

Asked of the strategy's own defaults rather than of a list kept here: a required column, layer or second species that resolves to nothing on this table means the strategy has nothing to run on, whatever the user answers. Gene lists are exempt -- the user supplies those.

def fits_subject(strategy, answers, ctx) -> bool:

Whether the strategy can actually take the subject the person chose.

A label-calling method offered for someone's numeric screen would silently run on a default label instead of on their measurement, and report a number about something they never asked about. That is the failure this rule exists to prevent.

def grade_of(key: str, organism: str) -> str:

What the calibration sweep graded this strategy on this space; "" if it never measured it.

def prefill(strategy, answers, ctx) -> dict:

The settings the answers decide, as Strategy.settings takes them.

Only parameters the strategy actually has are filled, and only from an answer that means the same thing: the gene list into its gene-list parameter, the chosen label into its held-out label, the chosen measurement into the number it predicts, the baseline into the baseline. Everything else stays at the strategy's own default.

def recommend(answers, ctx, limit: int = 6) -> list:

The strategies worth running for these answers, best first, each with its reason.

Ranked by :func:_score; anything this table cannot fill, or that cannot take the subject the person chose, is dropped first. When the handful a goal is FOR leaves fewer than MIN_RECOMMENDED, strategies whose task matches the goal fill the list rather than leaving it short -- so no answer is ever a dead end.

def caveat(recommendations) -> str:

One plain sentence when the best thing available is not one to rely on, else "".

Said rather than hidden: a person who ran the top recommendation and read a number off it should know beforehand that nothing better than "weak" exists here for what they asked.

VIEW_MAPS = 'maps'
VIEW_STAR = 'star map'
VIEW_DISCOVERIES = 'discoveries'
def views(answers, ctx=None) -> list:

Panels, not strategies, that answer this goal better -- each with the reason it is offered.