does your score beat its own baseline?
a single-cell potency score is usually validated by correlating it against a known hierarchy. a score that only restates sequencing depth passes that test. this runs the test that separates them.
╭────────────────────────────────────────────────────────╮ │ ✳ own-baseline does your score beat its own baseline? │ ╰────────────────────────────────────────────────────────╯ █████ █ █ █ █ ████ █████ █ █ █ █ ██ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ ██ ██ █ ██ █ █ █ █████ █ █ █ █ ████ ██ █████ ██ ████ ███ █████ █████ █ █████ █ █ █████ █ █ █ █ █ █ █ █ ██ █ █ ████ █████ █████ ████ █ █ █ █ █ ████ █ █ █ █ █ █ █ █ █ ██ █ ████ █ █ █████ █████ █████ █████ █ █ █████ Damir Iskakov v0.2.2 · MIT check does this score order cells beyond the statistic it is closest to? floors the estimator's measured null floor at a given n, rho and kernel primitives compute the low-order statistics from an expression matrix verify re-run a receipt and report whether it still reproduces ownbaseline check cells.h5ad --score obs:cytotrace --ordinal obs:stage \ --ordinal-source experimental ownbaseline <verb> --help · docs/interpreting.md for what a verdict does not mean
the splash, verbatim. four verbs, and the measured null grid ships inside the package.
get it running
python 3.9 or newer, three dependencies. the same package installs asown-baseline under its descriptive name.
two commands, and a third if you want to see the floor it compared against.
install
pip install iskakovrun it on your matrix
the ordinal has to be score-independent, and the flag makes you say so.
ownbaseline check cells.h5ad \
--score obs:cytotrace \
--ordinal obs:stage \
--ordinal-source experimental \
--json report.jsonask what the floor was
measured, not assumed, and answered with no network access.
ownbaseline floors --n 39505 --rho 0.4844a local kernel keeps what you install, so this is a one-time step.
install into the kernel that is running
sys.executable rather than a bare !pip: a notebook kernel is often not the interpreter your shell would pick.
import sys
!{sys.executable} -m pip install iskakovcall it on arrays you already have
polarity does not matter: the score is sign-aligned to the ordinal before residualization.
from own_baseline import conditional_skill_report
report = conditional_skill_report(
score, ordinal, {"gene_count": gene_count}, verbose=False)
print(report["by_primitive"]["gene_count"]
["conditional_skill"]["combined_verdict"])the runtime is thrown away when the session ends, so the install cell runs again every time. put it at the top of the notebook.
first cell
!pip install -q iskakovreach your h5ad on drive
colab has no access to your files until the drive is mounted.
from google.colab import drive
drive.mount("/content/drive")
!ownbaseline check /content/drive/MyDrive/cells.h5ad \
--score obs:cytotrace --ordinal obs:stage \
--ordinal-source experimental --json report.jsonit installs the package and runs the commands itself. fill in the path and the two column names.
paste this
Set up own-baseline and run it on my data. 1. Install it: pip install iskakov 2. My matrix is at PATH.h5ad. The score to test is in obs:SCORE. The ordinal to test it against is in obs:ORDINAL, and that ordinal is experimental, not derived from the score. 3. Compute the primitives once with `ownbaseline primitives`, then run `ownbaseline check` against them, writing --json report.json. 4. Show me the verdict, the conditional skill and the floor it was compared against. If the verdict is INCONCLUSIVE-kernel-disagree, say so and do not pick a side.
codex runs in a sandbox, and the install step needs the network. approve it when it asks, or the first command fails and nothing after it means anything.
paste this
Install own-baseline with `pip install iskakov`. You will need network access for that one step. Then run `ownbaseline check` on PATH.h5ad with --score obs:SCORE, --ordinal obs:ORDINAL and --ordinal-source experimental, writing --json report.json. Report the verdict, the conditional skill and the floor. Do not read INCONCLUSIVE-kernel-disagree as a pass, and do not describe a number the run did not print.
same shape. anything that can run a shell command drives this: there is no integration to install and no server to configure.
paste this into the agent
Install own-baseline with `pip install iskakov`, then run `ownbaseline check` on PATH.h5ad with --score obs:SCORE, --ordinal obs:ORDINAL and --ordinal-source experimental. Report the verdict and the floor it was compared against. Do not read a verdict of INCONCLUSIVE-kernel-disagree as a pass.
what it actually does
most published potency scores are built on a low-order statistic of the same expression matrix: the number of genes detected, the correlation of a cell’s profile with network node degree, the shannon entropy of the profile, the library size.
the field validates such a score by correlating it against a pseudotime, a marker panel or a known hierarchy. depth correlates with the gold standard too, so the criterion cannot separate a score that carries ordering information from one that restates depth. rank correlation against the primitive does not settle it either: a score at 0.93 with node degree can carry substantial skill beyond it, and another at 0.998 carries none.
own-baseline residualizes the score on the statistic its own authors say it approximates, and asks whether what is left still orders cells. four answers are possible.
adds-beyond the residual still orders the ordinal, above the measured floor for this n, rho and kernel.
tautolog what is left after residualization does not clear the floor. the score was restating its primitive.
sign-flipped the residual orders the ordinal backwards. a margin read without checking direction would have called this a pass.
inconclusive-kernel-disagree the two kernels answer differently. the tool refuses to pick one, and so should you.
what it prints
real runs, captured from the command line. the fixture is synthetic and seeded, so these numbers are reproducible rather than illustrative.
a score built as 0.93 times its primitive plus noise. it tracks the ordinal at auc 0.800 and still fails: the residual is +0.0187 against a floor of +0.0479.
╭─ own-baseline 0.2.2 · Damir Iskakov ──────────────────────────────────────────────────────╮ │ score score.npy │ │ ordinal ordinal.npy 5 levels │ │ n 4,000 seed 42 kendalltau (variant=b) │ │ │ │ ▌ DIRECTION CHECK │ │ score vs ordinal AUC 0.800 points with the ordinal │ │ gene_count AUC 0.834 points with the ordinal │ │ -> score and every primitive point the same way; margins read as written. │ │ │ │ ▌ vs gene_count rho(score, primitive) = +0.921 │ │ conditional skill +0.0187 CI95 [+0.0030, +0.0350] (1000 resamples) │ │ own floor +0.0479 │ │ null cell Null B, align on, 12 levels, n=3,000, rho=0.93, │ │ kendalltau, 1 covariate, 3 coupling strengths, 600 seeds │ │ NOTE: the nearest measured n is 3,000, below this run's 4,000. │ │ The floor falls with n, so this threshold is higher than the true one and │ │ clearing it is the safe direction to be wrong in. │ │ -> DOES NOT CLEAR (+0.0187 against +0.0479) │ │ │ │ VERDICT does not order cells beyond its primitive on this ordinal. │ │ Scope: this says the score orders cells beyond these statistics. It │ │ does not say what the residual is. A residual can be developmental │ │ position, manifold structure, or a fifth statistic nobody named. │ │ │ │ ▌ NOT COMPUTED, and therefore not controlled for │ │ PCC(x,degree): no expression matrix and no vector given; pass --pcc-degree <file> │ │ Shannon_H: no expression matrix and no vector given; pass --entropy <file> │ │ log10_library_size: no expression matrix and no vector given; pass --log-libsize <file> │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ wrote report.json
the floor at the paper’s own cell. no network call, and the offset line says how far the answer sits from a measured design point.
floor +0.0247 null mean +0.0175 cell Null B, align on, 12 levels, n=39,505, rho=0.5, kendalltau, 1 covariate, 3 coupling strengths, 600 seeds offset n +0 from the grid point, rho -0.016
a receipt records the environment and a sha256 per input. reading it back tells you what the run was, before recomputing anything.
receipt from 2026-09-05T06:04:29Z
environment matches the receipt
hashes recorded in the receipt:
score 5e4cdaf185e0e6bd2e0c66cec80b780e7593edce83af2dab22976f191d6dd358
ordinal a285d595b879722214ce4a07993368a1552edef4c587024473e6849cb7898a98
gene_count d5cb5a0a7b910ae1c7719a7145c71db98712ba3579295eaa8792415b68efa526
pass --rerun with the same inputs to recompute and compare.it recomputes from the same inputs and compares against what was recorded. a difference above 1e-12 is reported as differing.
receipt from 2026-09-05T06:04:29Z environment matches the receipt ╭─ own-baseline 0.2.2 · Damir Iskakov ──────────────────────────────────────────────────────╮ │ score score.npy │ │ ordinal ordinal.npy 5 levels │ │ n 4,000 seed 42 kendalltau (variant=b) │ │ │ │ ▌ DIRECTION CHECK │ │ score vs ordinal AUC 0.800 points with the ordinal │ │ gene_count AUC 0.834 points with the ordinal │ │ -> score and every primitive point the same way; margins read as written. │ │ │ │ ▌ vs gene_count rho(score, primitive) = +0.921 │ │ conditional skill +0.0187 CI95 [+0.0030, +0.0350] (1000 resamples) │ │ own floor +0.0479 │ │ null cell Null B, align on, 12 levels, n=3,000, rho=0.93, │ │ kendalltau, 1 covariate, 3 coupling strengths, 600 seeds │ │ NOTE: the nearest measured n is 3,000, below this run's 4,000. │ │ The floor falls with n, so this threshold is higher than the true one and │ │ clearing it is the safe direction to be wrong in. │ │ -> DOES NOT CLEAR (+0.0187 against +0.0479) │ │ │ │ VERDICT does not order cells beyond its primitive on this ordinal. │ │ Scope: this says the score orders cells beyond these statistics. It │ │ does not say what the residual is. A residual can be developmental │ │ position, manifold structure, or a fifth statistic nobody named. │ │ │ │ ▌ NOT COMPUTED, and therefore not controlled for │ │ PCC(x,degree): no expression matrix and no vector given; pass --pcc-degree <file> │ │ Shannon_H: no expression matrix and no vector given; pass --entropy <file> │ │ log10_library_size: no expression matrix and no vector given; pass --log-libsize <file> │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ COMPARISON WITH THE RECEIPT gene_count +0.018662 -> +0.018662 delta +0.00e+00 REPRODUCES
what came out of it
calibrated on 39,505 cells across twelve microscopy-staged timepoints, where the ordering is fixed before the cells are processed.
thirteen published scores audited. of the seven testable, five survive residualization and two do not.
direction has to be checked before any margin is read. in sorted haematopoietic progenitors the gene-count primitive orders cells below chance: hscs carry a median 916 detected genes, the gmps below them carry 1,404, and the primitive scores auroc 0.378 against the known hierarchy. a score that beats that baseline has supplied a sign correction, and nothing in the margin shows it.
what a verdict does not mean
ADDS-BEYOND says the score orders cells beyond the primitive you named. it says nothing about
whether what it adds is potency rather than position on a developmental manifold, and nothing about
a primitive you did not name.