Edit on GitHub

starplast.searches

A search you can put down and pick up: every configuration it tried, and everything each found.

A climb is expensive and its result is not one map -- it is a hundred of them, with their scores, their metrics and their claims. Keeping only the winner throws away the two things that make a search evaluable afterwards: the runner-up, and how far behind it came. A single best configuration with nothing to compare it against cannot be told apart from a lucky one.

What is written, and what is left to be rebuilt

Written: the manifest (what was asked, of which table, by which version), one row per configuration with its score and every metric, every finding from every configuration, and the cluster labels each configuration produced.

Not written: the coordinates. They are the largest thing by an order of magnitude and the one thing that is exactly reproducible -- a recipe plus a seed rebuilds the same map, which is a promise this project makes everywhere else and may as well keep here. Labels ARE written, because kmeans and HDBSCAN are not seeded identically across versions of their libraries and a clustering that came back different on reload would silently change every claim built on it.

The fingerprint, which is the point of the manifest

A search is a set of statements about genes identified by their POSITION in a node table. Reloaded against a different table -- a rebuilt cache, a newer release, a filtered subset -- those positions address different genes, and every finding in the run quietly becomes a claim about the wrong ones. So the table is fingerprinted when the search is saved, checked when it is loaded, and a mismatch is reported rather than papered over. It is not fatal: the configurations and scores remain readable and comparable. What stops is the mapping from a label to a gene.

ARTEFACTS = ('_labels', '_findings')
def fingerprint(nodes: pandas.DataFrame) -> dict:

What table this search was about, in enough detail to notice it is a different one.

The gene identifiers hashed in order, not merely counted: a table with the same number of rows in a different order is the commonest way for this to go wrong, and a count would not see it.

def from_climb( result: pandas.DataFrame, nodes: pandas.DataFrame = None, **manifest) -> Search:

Split a climb's return into the part that is a table and the part that is artefacts.

class SearchStore:

Saved searches on disk, one directory each.

A directory rather than a single file because the pieces have different natural formats and very different sizes -- a manifest that should stay readable in a text editor, two tables, and a block of integer arrays -- and because a half-written search should be identifiable as one.

SearchStore(root: str)
root
def path(self, name: str) -> str:

Where a search of this name lives, with anything awkward taken out of the name.

def list(self) -> <function SearchStore.list at 0x7fc6c102c4a0>:

Saved searches, newest first, as (name, manifest).

def save(self, search: Search, name: str = None) -> str:

Write a search. Returns the directory it went into.

def load(self, name: str) -> Search:

Read a search back. Missing pieces come back empty rather than raising.