Skip to content

Read the control scale off the controls that did not fit the transform - #98

Merged
timtreis merged 5 commits into
mainfrom
fix/hits-held-out-cutoff
Sep 20, 2026
Merged

timtreis merged 5 commits into
mainfrom
fix/hits-held-out-cutoff

Conversation

@timtreis

@timtreis timtreis commented Sep 20, 2026

Copy link
Copy Markdown
Member

Both issues are one confusion: two obs columns claim to be "distance from the controls", and the one everything read was not honest.

hit_calling fits the centroid and covariance on half the controls, then writes hits_row_distance for every row. The fitting half sits closer to the centroid it placed, so dose_response's 3 * MAD cutoff came out low and every hitcall was permissive (#83). Separately, cytotoxicity defaulted distance_key to hits_distance, the group statistic broadcast over its rows, so the median it documents returned the value it was handed (#84).

hit_calling now writes obs[key_added + "_reference_held_out"], the mask it already computed and discarded. held_out_reference in _core/masks.py narrows a control mask with it, reading the flag through _flag_mask like the other two flag columns, and falls back to every control when the column is absent. dose_response and cytotoxicity both use it; cytotoxicity now defaults to hits_row_distance.

Two things found while re-running the tutorials, both kept as separate commits:

  • pl.dose_response drew a heavy-tailed distance on a linear axis, so one stray well flattened every other point onto the baseline. It is drawn on a log scale when the response is positive.
  • OASIS records one concentration as both 3.704 and 3.7037, because two of its twelve plate maps use three decimals and the rest four. Metadata_Perturbation was built from that column, so a treatment's replicates split across the two spellings. Metadata_Concentration still holds what was recorded; the new Metadata_ConcentrationNominal puts levels agreeing to within 1% onto the value the most wells carry, and names the groups.

This changes published hitcall numbers, cytotoxicity distances and the OASIS replicate groups, so tutorials 5, 8 and 11 and the oasis_pilot page are re-run. Tutorial 11's prose is corrected where it argued from numbers that moved.

Verified: full suite green, ruff, mypy, pre-commit and a -W docs build.

Closes #83
Closes #84

hit_calling fits the centroid and covariance on half the controls, then writes
a distance for every row. The fitting half sits closer to the centroid it
placed, so dose_response's 3 * MAD cutoff came out low and every hit call was
permissive. The held-out half is now recorded in obs so anything reading a
scale off the controls can read it off the honest rows.

Closes #83
distance_key defaulted to hits_distance, the group statistic broadcast over
its rows, so the median cytotoxicity takes returned the value it was handed
and no genuinely per-row response could be given. It now defaults to
hits_row_distance and compares against the held-out controls.

The DMSO distance in tutorial 8's prose was already stale against main.

Closes #84
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@read-the-docs-community

read-the-docs-community Bot commented Sep 20, 2026

Copy link
Copy Markdown

@codecov-commenter

codecov-commenter commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.21739% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.05%. Comparing base (60eead3) to head (93a653c).

Files with missing lines Patch % Lines
src/mantispy/ds/_datasets.py 20.00% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #98      +/-   ##
==========================================
- Coverage   89.15%   89.05%   -0.11%     
==========================================
  Files          80       80              
  Lines        5865     5884      +19     
==========================================
+ Hits         5229     5240      +11     
- Misses        636      644       +8     
Files with missing lines Coverage Δ
src/mantispy/_core/masks.py 100.00% <100.00%> (ø)
src/mantispy/_core/schema.py 95.00% <ø> (ø)
src/mantispy/pl/_hits.py 98.09% <100.00%> (+0.03%) ⬆️
src/mantispy/tl/_design.py 93.54% <100.00%> (ø)
src/mantispy/tl/_dose.py 97.19% <100.00%> (ø)
src/mantispy/tl/_hits.py 95.69% <100.00%> (+0.04%) ⬆️
src/mantispy/ds/_datasets.py 24.01% <20.00%> (-0.19%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The response is a distance from the controls, with a long right tail, so one
stray well stretched the linear axis until every other point sat flat on the
baseline. Staurosporine's wells climb from 5 to 18 over the dose range in
tutorial 11 and none of it was visible.

A response that reaches zero or below is still drawn on a linear scale.
Two of the twelve plate maps record the dose to three decimals where the rest
use four, so one concentration arrives as both 3.704 and 3.7037. Metadata_
Perturbation was built from the raw column, so the wells of a single treatment
split across the two spellings and most groups held too few wells to be
replicates of anything.

Metadata_Concentration still holds what each plate map recorded. The new
Metadata_ConcentrationRounded puts levels agreeing to within 1% onto the value
the most wells carry, so every dose it holds is one that was written down, and
it names the perturbation groups. No rounding precision does this: none
separates the pairs that differ from those that do not.
The column is produced by clustering levels within a tolerance, not by
rounding, so Metadata_ConcentrationNominal says what it holds. Two places
still read the raw column and should not have: the loader's summary log
counted the split levels it exists to correct, and the tutorial drew wells
against raw doses beside a curve fitted on aligned ones.

_aligned_doses splits the sorted levels at the gaps rather than walking them
with an index, and maps through Series.replace as the cell-line fixup beside
it does.
@timtreis
timtreis merged commit 16a425c into main Sep 20, 2026
12 checks passed
@timtreis
timtreis deleted the fix/hits-held-out-cutoff branch September 20, 2026 16:57
timtreis added a commit that referenced this pull request Sep 20, 2026
#98 added Metadata_ConcentrationNominal, which merges levels within 1% across
the whole plate map. That catches 3.704 against 3.7037 but not 0.015 against
0.0152, and not 0.000762 against 0.001, where one batch rounds to three decimals
and a small dose loses a third of its value. Widening the tolerance is not
available: berberine's 25 uM and the main ladder's 33.3 uM are a third apart and
are different doses.

The plate maps disagree on precision, not on value, so a level written to fewer
decimals is the level that rounds to it. Matching runs within a compound, whose
levels are one dilution series and cannot collide. Every dosed compound then
lands on the ten three-fold steps it was plated on, or the eight two-fold ones
the assay-development plates used.

That belongs to the loader, so dose_tolerance and the binning behind it are gone
from the four dose functions, along with the binned dose dose_direction used to
broadcast. Every dose call in the tutorial reads the nominal column.

The amplitude floor now holds each drawn control group out of the scale it is
measured in, as hit_calling's own cutoff does after #98. Scored against a
baseline they helped define, control groups sit closer to it than a treated
group can: 64% low at sixteen control wells, 30% at thirty-two, 4% at the OASIS
pilot's 256.

Tutorial 11 re-executed. Cycloheximide is now called in both cell lines, so the
cross-line paragraph is #98's corrected one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants