Edit on GitHub

starplast.tuning

Hyperparameter walks, saved embeddings, and importing your own table.

Three jobs that belong together because they are what a user does before trusting a map:

  • walk_umap_iter — search n_neighbors x min_dist on a seeded random subsample, so a sweep costs seconds rather than the minutes a full 8,140-gene UMAP takes, and yield each configuration as it is computed rather than a table at the end. walk_umap is that collected and ranked, for callers who want the answer rather than the process. The subsample is drawn with a fixed seed so two sweeps are comparable; the seed is recorded in every row.
  • EmbeddingStore — save a coordinate set together with the full EmbeddingSpec that produced it. An embedding without its recipe cannot be compared with another or reported in a methods section.
  • import_table — read a user's CSV and align it to gene ids, with regex repair when the identifier column is formatted differently.

On scoring a UMAP. There is no ground truth for "a good embedding", so the walk reports several quantities that measure different things and lets the user decide, rather than manufacturing one number:

trustworthiness      does the map preserve the original neighborhoods (0-1, higher better)
continuity_proxy     correlation between original and embedded pairwise distances
n_clusters_hdbscan   how many clusters fall out, and how much is noise

A setting that maximises trustworthiness often produces one blob; a setting that produces many clusters often distorts. Both numbers are shown because that trade-off is the actual decision.

DEFAULT_SEED = 42
WALK_MIN_CLUSTER_SIZE = 15
@dataclass
class WalkStep:

One configuration of a walk, complete: its scores and the map that produced them.

The walk used to hand back a table when the whole sweep had finished, so a 288-configuration sweep -- half an hour -- showed nothing at all until it ended, and the embeddings themselves were computed, scored and thrown away. A step carries the coordinates as well as the numbers, which is what lets configuration 12 be looked at while 13 is still computing.

genes is a boolean mask over the node table rather than a count, because a walk embeds a subsample: without it there is no way to say which gene each coordinate belongs to, and a map whose points cannot be named is a picture rather than a map.

WalkStep( index: int, total: int, row: dict, coords: numpy.ndarray, genes: numpy.ndarray, spec: starplast.embedding.EmbeddingSpec, name: str = '')
index: int
total: int
row: dict
coords: numpy.ndarray
genes: numpy.ndarray
name: str = ''
label: str

The configuration in one line, for a thumbnail caption or a status message.

def walk_umap_iter( nodes: pandas.DataFrame, spec: starplast.embedding.EmbeddingSpec, n_neighbors_values=(5, 15, 25, 50, 100), min_dist_values=(0.0, 0.1, 0.25, 0.5), sample_size: int = 2000, seed: int = 42, cluster_check: bool = True, store=None, should_stop=None, log=<built-in function print>):

Sweep UMAP hyperparameters, yielding each configuration as it finishes.

This is the walk; walk_umap is this collected into a table. Emitting per configuration is what makes a sweep watchable -- rows and thumbnails appear one at a time rather than all at once at the end -- and it costs nothing, because the embedding was being built anyway.

Given a store, every configuration is saved with its full recipe as it is computed, so a walk that is stopped half way still leaves behind everything it had finished.

should_stop is asked before each configuration and simply ends the sweep: the caller keeps every step already yielded. A stop is a decision that enough has been seen rather than an error, so nothing is raised and nothing is discarded.

def walk_umap( nodes: pandas.DataFrame, spec: starplast.embedding.EmbeddingSpec, n_neighbors_values=(5, 15, 25, 50, 100), min_dist_values=(0.0, 0.1, 0.25, 0.5), sample_size: int = 2000, seed: int = 42, cluster_check: bool = True, store=None, on_step=None, should_stop=None, log=<built-in function print>) -> pandas.DataFrame:

Sweep UMAP hyperparameters on a seeded subsample of the genes, and rank the settings.

on_step is called with each WalkStep the moment it is computed, which is how the interface fills its table and its gallery a configuration at a time. The ranked table is still returned, because ranking needs the whole sweep and a caller that only wants the answer should not have to accumulate it.

class EmbeddingStore:

Named embeddings on disk: coordinates plus the recipe that produced them.

EmbeddingStore(root: str)
root
def save( self, name: str, coords: numpy.ndarray, spec: starplast.embedding.EmbeddingSpec, gene_ids=None, features=None, extra: dict | None = None) -> str:

Store coordinates with the full recipe that produced them.

def load(self, name: str):

Reload a stored embedding, its gene ids and its recipe.

def list(self) -> pandas.DataFrame:

Every stored embedding, newest first.

GENE_RX = re.compile('TGME49_\\d{5,6}', re.IGNORECASE)
IMPORT_RX = re.compile('TG(?:ME49|GT1|VEG)_\\d{5,6}', re.IGNORECASE)
LOOSE_RX = re.compile('(tgme49|tggt1|tgveg)\\s*[._\\- ]?\\s*\\d{5,6}', re.IGNORECASE)
def suggest_gene_column(df: pandas.DataFrame):

Return (column, fraction_matching, needs_repair) for the likeliest identifier column.

def suggest_repair(values) -> str | None:

A regex/replacement pair that would turn a malformed identifier column into canonical form.

def import_table( path_or_df, gene_column: str | None = None, pattern: str | None = None, replacement: str = '\\g<0>', resolve=None, prefix: str = 'imported_', log=<built-in function print>) -> pandas.DataFrame:

Read a user table and return it keyed by canonical gene id.

pattern is applied to the identifier column before matching, so a column formatted TgME49.208830 or gene|TGME49_208830|v2 can be repaired without editing the file:

import_table(df, "id", pattern=r"TgME49\.(\d+)", replacement=r"TGME49_\1")

resolve should be identity.GeneIndex-backed, so previous and strain accessions map to current ones -- the failure that cost a published screen every one of its rows.