spacr.qt.widgets.dose_response¶
Dose–response — a four-parameter logistic that will refuse to name an EC50.
spaCR runs concentration series and has, until this module, had no curve
fitter at all: a screener with eight doses and three replicates exports a CSV
and opens Prism. The point of writing one is not that “it fits” — every
optimiser fits — but that the answer is often “you cannot tell from this
experiment”, and no fitter in common use says so. curve_fit will return
four numbers and a covariance matrix for a dilution series that never reached
a plateau, for a bell-shaped cytotoxicity curve, and for eight points of pure
noise. All three come back looking like an EC50 with a confidence interval.
So the decisions here are almost all about refusing, and each one is written down.
Why log10(EC50) is the parameter, and EC50 is derived¶
The model is fitted in log10_ec50, never in EC50:
Three reasons, all of which show up in real data:
The doses are geometric. A 2- or 3-fold dilution series is evenly spaced in log concentration and violently uneven in linear concentration. The information the experiment carries about the midpoint is information about which dilution step it sits on, which is a statement in log space.
The likelihood surface is close to symmetric in log10(EC50) and badly skewed in EC50. That is what makes a quadratic (Wald) approximation defensible on
log10_ec50and indefensible onEC50.A linear-space interval routinely reaches below zero, and a negative concentration is not a thing. An EC50 of
1.0 ± 1.4 µMis not a wide interval, it is a broken one.
Reporting therefore back-transforms the interval: 10 ** [lo, hi]. The
result is multiplicative (0.62 – 1.61 µM around 1.0, a 1.6-fold factor
either way), always positive, and reads the way a potency actually behaves.
Which way the curve goes¶
The parameterisation has an exact symmetry: (bottom, top, L, h) and
(top, bottom, L, -h) are the same function, so the optimiser may return
either. Left alone, two runs on the same data can report Hill slopes of
opposite sign. Every fit is therefore canonicalised the way pharmacology
already writes it (and GraphPad’s variable-slope equations do):
top >= bottomalways — they are the larger and smaller plateau, not the right-hand and left-hand one;the sign of the Hill slope carries the direction:
hill < 0is inhibition (response falls as dose rises, andtopis the low-dose control plateau),hill > 0is activation.
The direction is inferred from the data (Spearman rho of response against
log dose) and never assumed, so an activation series is not fitted upside
down. DIRECTION_INHIBITION / DIRECTION_ACTIVATION pin it when
the user knows better than the correlation does.
Two confidence intervals, and why the default is the slow one¶
Both are offered on log10_ec50 and both back-transform:
CI_WALD— asymptotic, from the covariance matrixcurve_fitreturns, with a t quantile onn - 4degrees of freedom. Not a normal quantile: eight concentrations with no replicates leaves 4 df, wheret = 2.776againstz = 1.960. That is a 42% difference in the width of the published interval, which is not cosmetic.CI_PROFILE— profile likelihood, the default. For a grid of candidatelog10_ec50values the other three parameters are re-optimised and the residual sum of squares is compared againstSSE_min * (1 + t²/(n - 4)), the standard F-based profile region for one parameter out of four. (F(1, ν) = t(ν)², so the two intervals use literally the same quantile and differ only in the shape of the surface they walk.)
The profile is the default for one reason that outweighs its cost: the Wald
interval is finite by construction. It is L ± t·SE, so it returns a
tidy, symmetric, entirely fictional interval for a curve whose midpoint the
data does not locate at all — which is exactly the case this module exists
to catch. The profile interval can fail to close: the residual sum of
squares stays under the threshold all the way out to a concentration ten
thousand times the highest dose tested, and that failure is the diagnostic.
It is reported as an open bound, not smoothed into a number.
A residual bootstrap was the other candidate and was rejected. It shares the profile’s homoscedastic-normal assumption so it buys no robustness; it needs a seed and gives a slightly different published number per seed; and — the deciding objection — percentiles of a bootstrap distribution are always finite, so it launders an unidentified parameter into a confident interval in precisely the situation that matters. Nothing here resamples and nothing here is random: the same data gives the same interval, every time.
bottom, top and the Hill slope get Wald intervals only. Profiling
each of them would quadruple the work to improve numbers nobody quotes; the
EC50 is the number that leaves the building.
The three things that actually go wrong¶
The curve is incomplete — one plateau was never reached, or the midpoint sits outside the tested range. Then the EC50 is an extrapolation.
DoseResponseResult.ec50isNonein that case and the number lives inDoseResponseResult.ec50_unconstrained, under a name that cannot be mistaken for a result;DoseResponseResult.ec50_boundedisFalseandDoseResponseResult.bound_statement()gives the one-sided fact the experiment does support (“EC50 > 30 µM, the highest concentration tested”). Three independent detectors have to agree it is fine before a number is released: the fitted midpoint inside the tested dose range, the observed responses bracketing the fitted half-maximum, and neither fitted plateau more thanPLATEAU_SLACKof the observed response span outside the observed responses.The data is not monotone. A bell shape — the classic being cytotoxicity killing the signal at the top dose — is not a 4PL, and a 4PL fitted to it returns a confident EC50 for a curve of the wrong shape. Detected from the concentration-ordered per-dose medians and refused, with the concentrations where it turns named in the message. See
monotonicity().There is not enough experiment. Four parameters need at least four distinct concentrations (
MIN_DOSES) and, to say anything about uncertainty, more observations than parameters (MIN_OBSERVATIONS). A constant response has no curve in it. A zero concentration is not an error — a vehicle control is normal and belongs in the file — so it is excluded from the fit deliberately, counted, and reported as a reference response, never fed tolog10. A negative concentration has no such reading and is refused.
Two models, and why the second one is offered rather than chosen¶
MODEL_5PL fits five_parameter_logistic(), a 4PL with one more
parameter that lets the curve approach its two plateaus at different rates.
Real dose–responses are asymmetric — receptor occupancy with cooperativity,
anything read through a saturating stain — and a 4PL fitted to an
asymmetric curve does not fail, it moves the EC50: on the calibration
series in tests/qt/test_dose_response_fits_an_asymmetric_curve.py a
symmetric fit to a curve whose true EC50 is 1.0 reports 0.67, with a tight
interval and an R² of 0.997.
It is nonetheless not the default, and that is a decision rather than an
oversight. The fifth parameter trades off against the Hill slope — many
(slope, asymmetry) pairs draw nearly the same curve — so on the eight or ten
concentrations a plate actually carries it is weakly identified, and the
profile interval it produces is correspondingly wider. The module therefore
offers it, fits it from the 4PL solution so the asymmetric search starts
at the symmetric answer, and reports an extra-sum-of-squares F test of the
one added parameter in DoseResponseResult.asymmetry_p so a reader
can see whether the asymmetry was worth what it cost.
The parameterisation is the EC50-preserving one: log10_ec50 means the
same thing under both models, so every rule in this file — boundedness,
profiling, the back-transformed reporting — reads one number and not two.
five_parameter_logistic() explains why that needs a constant in the
denominator and what goes wrong without it.
Hormesis, which is the other reason a curve turns around¶
monotonicity() can see that a series reverses. It cannot say which
reversal it is, and the two mean opposite things:
the response collapses at the top dose — usually the compound killing whatever was being counted. The advice is to drop that dose;
the response rises at the bottom dose before falling — hormesis. The advice is emphatically not to drop those doses, because the rise is the finding.
hormesis() tells them apart by fitting brain_cousens() — the 4PL
with a low-dose stimulation term, which is the 4PL exactly when that term is
zero — and comparing the two nested fits by an extra-sum-of-squares F test
(the likelihood-ratio test, under this module’s Gaussian errors), a
small-sample-corrected AIC, and a minimum-effect threshold. All three,
plus a positive coefficient, or the answer is no. The threshold is there
because a tight assay will reach p < 0.001 on a hump worth 2% of the
response span, and 2% is not a biological claim.
The test runs on any series whose low-dose end departs from control against
the trend, not only on the ones monotonicity() refuses — a hump can
carry a fifth of the response span and still sit inside
MAX_REVERSAL, in which case today’s fit succeeds, quotes an EC50
displaced by the hump, and says nothing. Those fits now carry the finding in
DoseResponseResult.hormesis and in their caveats.
Fit quality, and the reason R² is printed with a warning attached¶
Every result carries the residual standard error, R², and — when the design has replicates — a lack-of-fit F test against pure error. That last one is the statistic that answers the question people think R² answers.
R² on a sigmoid is nearly useless. Any monotone curve through a well-sampled
dose–response scores above 0.95, because the total sum of squares is
dominated by the difference between the two plateaus and any S-shaped line
captures that. DoseResponseResult.caveats() says so next to the number,
every time.
The lack-of-fit test does the real work when replicates exist: pure error
(within-concentration scatter, n - m df) is a model-free estimate of
noise, and the residual variance in excess of it (m - 4 df) is
model-misspecification. A small p-value means a 4PL is the wrong shape for
this data whatever the R² says. With no replicates there is no pure-error
estimate and the test does not exist — which is itself reported, because
“cannot be tested” and “passed” are different states.
No Qt in here¶
numpy, pandas and scipy only, like spacr.qt.widgets.pca_model and
spacr.selection: usable from a notebook, testable without a display,
and with nothing in the fitting path that knows a widget exists. There is not
a single PySide6 import in this file, and the two seams that would need
one — candidate_concentration_columns() and
candidate_response_columns(), which re-use the Local Data Filter’s
column classifier through spacr.qt.widgets.graph_spec.column_kinds()
rather than inventing a second one — do it inside the function body, so
importing this module and fitting a curve pulls in no Qt of its own.
(spacr/qt/widgets/__init__.py eagerly imports the widget modules, so
reaching this module through the package still costs a PySide6 import
today. That is a property of the package’s __init__, not of this file:
nothing here would have to change for the fitter to run in an environment
without PySide6.)
Exceptions¶
A dose–response that cannot mean anything, with the way out in the text. |
Classes¶
A two-agent dose grid pulled out of a well table, with its two axes. |
|
One fitted curve, plus everything needed to know whether to quote it. |
|
Every curve on a plate, in the order the levels were seen. |
|
Which columns to fit and under what policy. |
|
One level of the grouping column: a result, or the refusal. |
|
Whether the low-dose end of the series is stimulated, and by how much. |
|
A checkerboard's interaction, per cell, with its own axes. |
|
Whether the concentration-ordered response only ever goes one way. |
|
What one plate's controls said, and whether the plate may be fitted. |
|
Which column is the plate, and which wells on it are the controls. |
|
One EC50 across replicate plates, with plate as a random effect. |
|
Host toxicity over parasite killing, with the interval it deserves. |
Functions¶
|
Bliss independence over a checkerboard. |
|
The Brain–Cousens hormesis model: a 4PL with a low-dose hump on it. |
|
Columns worth offering as the concentration axis. |
|
Columns worth offering as the response axis: every continuous one. |
|
Read a checkerboard off a well table, single-agent axes and all. |
|
Fit one concentration series, or refuse it. |
|
Fit one curve per level of |
|
The 5PL curve, parameterised so |
|
The 4PL curve, parameterised in |
|
Is the low-dose end of this series stimulated, or is it noise? |
|
Loewe additivity over a checkerboard, as a combination index. |
|
Is this concentration series consistent with a single sigmoid? |
|
Percent inhibition against each plate's own controls. |
|
The per-plate verdict alone, without normalising anything. |
|
Combine per-plate fits into one EC50 with plate as a random effect. |
|
Fit each plate in |
|
Divide a host EC50 by a parasite EC50, and carry the uncertainty. |
Module Contents¶
- exception spacr.qt.widgets.dose_response.DoseResponseError[source]¶
Bases:
ValueErrorA dose–response that cannot mean anything, with the way out in the text.
Raised rather than returned as an empty result, for the same reason
spacr.qt.widgets.pca_model.PCAErroris: every one of these is a sentence a screener can act on — “the response turns around between 10 and 30 µM, which is usually cytotoxicity; drop the top dose” — and a caller that swallowed it would draw an empty axis with no explanation.fit_frame()catches it per group and keeps the message beside that group’s row, so one bad compound does not take the plate down.Initialize self. See help(type(self)) for accurate signature.
- class spacr.qt.widgets.dose_response.Checkerboard[source]¶
A two-agent dose grid pulled out of a well table, with its two axes.
The three surface functions take parallel arrays; a plate reader hands you a table. This is the join between them, and it keeps the single-agent rows – the row where B is zero and the column where A is zero – because
bliss_surface()andloewe_surface()are both calibrated against those axes and a caller who filtered them out would silently get a surface with no reference.- Parameters:
dose_a – agent A concentration per well, combination wells included.
dose_b – agent B concentration per well.
response – the measured response per well.
a_alone –
(dose, response)for the wells where B is zero.b_alone –
(dose, response)for the wells where A is zero.
- class spacr.qt.widgets.dose_response.DoseResponseResult[source]¶
One fitted curve, plus everything needed to know whether to quote it.
The important field is
ec50_bounded. When it isFalse,ec50isNone— there is no way to read a point estimate out of this object without passing the flag, which is the whole design. The fitted number is still there, underec50_unconstrained, because hiding it would only make people re-derive it; the name says what it is.- Parameters:
group – the level this curve belongs to, or
"".bottom – smaller plateau.
top – larger plateau.
log10_ec50 – the fitted parameter. Always finite when the fit converged, whether or not it is inside the tested range.
hill – slope; negative for inhibition, positive for activation.
ec50 – the quotable half-maximal concentration, or
Nonewhen the experiment does not bound it.ec50_unconstrained –
10 ** log10_ec50, always. An extrapolation whenec50_boundedisFalse.ec50_bounded – whether the data bound the EC50: it lies inside the tested range, the curve’s midpoint is within the observed responses, both plateaus are reached and the interval is closed on both sides.
ec50isNoneunless it isTrue.ec50_low – back-transformed lower end of the interval, or
Nonefor an open side.ec50_high – back-transformed upper end of the interval, or
Nonefor an open side.log10_ec50_ci – the same interval before back-transformation.
hill_ci – Wald interval for the Hill slope.
top_ci – Wald interval for the top asymptote.
bottom_ci – Wald interval for the bottom asymptote.
bound_direction – one of
BOUND_OK,BOUND_ABOVE,BOUND_BELOW,BOUND_OPEN.dose – the concentrations actually fitted, in input order.
response – the responses actually fitted, aligned with
dose(positive concentrations only), in input order.n_obs – observations fitted.
n_doses – distinct concentrations.
dof – residual degrees of freedom —
n_obsless the model’s parameter count, son_obs - 4for a 4PL andn_obs - 5for a 5PL.dose_min – lowest distinct positive concentration fitted.
dose_max – highest distinct positive concentration fitted.
sse – residual sum of squares of the fitted curve.
rse – residual standard error, in response units.
r_squared – with the health warning in
caveats()attached.lack_of_fit_f – F statistic of the test against pure error.
lack_of_fit_p – p value of the test against pure error, or
Nonewhen the design cannot support it.lack_of_fit_df –
(numerator, denominator)df of that test.covariance – the 4×4 matrix, or
Nonewhen it could not be estimated.covariance_ok – whether it is finite and usable.
optimizer_notes – every warning scipy raised during the fit, captured rather than allowed to escape — an
OptimizeWarning: Covariance of the parameters could not be estimatedis a result, not console noise.vehicle_response – mean response at concentration 0, or
None.n_vehicle – how many vehicle observations there were.
n_excluded – rows dropped for a missing or non-finite value.
check – the
MonotonicityCheckthis fit passed (or was forced past).model –
MODEL_4PLorMODEL_5PL— which curve these parameters belong to.asymmetry – the 5PL exponent. Exactly
1.0underMODEL_4PL, where it is not a parameter at all.asymmetry_ci – its Wald interval, or
(None, None).asymmetry_f – extra-sum-of-squares F of the 5PL against the 4PL on the same points, or
NoneunderMODEL_4PL.asymmetry_p – its p value. A large p is the interesting case: it says the asymmetry the 5PL fitted is inside what the noise would produce, and the symmetric fit is the one to quote.
hormesis – the
HormesisCheckwhen the low-dose end of the series was worth testing, elseNone. A fit can be perfectly monotone bymonotonicity()and still carry a hormetic hump that displaces its EC50, so this is reported beside a successful fit and not only inside a refusal.
- bound_statement() str[source]¶
The one-sided fact the experiment supports, when it supports no two-sided one.
Returns
""for a bounded fit, so a caller can print it unconditionally.
- curve(points: int = 200) Tuple[numpy.ndarray, numpy.ndarray][source]¶
(x, y)for drawing, geometrically spaced across the tested range with half a decade of margin at each end.Geometric because the axis a dose–response is read on is logarithmic; an evenly spaced grid would put nine tenths of its points in the top dilution and draw the interesting part as three line segments.
- curve_frame(points: int = 200) pandas.DataFrame[source]¶
curve()as a two-column frame, for export.
- parameter_frame() pandas.DataFrame[source]¶
One row per parameter: estimate and interval.
EC50appears as its own row, back-transformed, and its estimate isNaNwhen the experiment does not bound it — the frame carries the same refusal the object does, so an exported CSV cannot quietly disagree with the screen.
- points_frame() pandas.DataFrame[source]¶
The fitted observations with their fitted values and residuals.
- predict(x) numpy.ndarray[source]¶
The fitted response at
x.- Parameters:
x – concentration(s), in the fitted units; evaluated with this result’s model and parameters.
- property ec50_fold_uncertainty: float | None[source]¶
sqrt(high / low)— the interval as a multiplicative factor.The natural way to state a potency’s uncertainty: “1.0 µM, within a factor of 1.6”.
Nonewhen either side is open.
- property model_function: Callable[..., numpy.ndarray][source]¶
- property parameters: Tuple[float, ...][source]¶
The vector this result’s model takes.
(bottom, top, log10_ec50, hill)underMODEL_4PLand that withasymmetryappended underMODEL_5PL— in both cases exactly whatmodel_functionexpects, sopredictis one line and no caller has to know which model it is holding.
- class spacr.qt.widgets.dose_response.DoseResponseSet[source]¶
Every curve on a plate, in the order the levels were seen.
- Parameters:
fits – one
GroupFitper level.spec – the spec they were all fitted under.
- get(group: str) GroupFit | None[source]¶
The fit for one level, or
None.- Parameters:
group – the level name to look up, as stored in
GroupFit.group(""for an ungrouped fit).
- results() Tuple[DoseResponseResult, ...][source]¶
Every level that produced a curve, bounded or not.
- table() pandas.DataFrame[source]¶
One row per level — the results grid, with refusals in it.
Columns:
group,status,n,concentrations,ec50and its interval,ec50_unconstrained,hill,top,bottom,r_squared,rse,lack_of_fit_p,note.ec50isNaNfor anything notSTATUS_FITTED, andnotesays which of the two reasons it is.
- class spacr.qt.widgets.dose_response.DoseResponseSpec[source]¶
Which columns to fit and under what policy.
Frozen and JSON round-tripping, like
spacr.qt.widgets.pca_model.PCASpec, so the analysis behind a figure is something a settings file or a methods section can carry verbatim.- Parameters:
concentration – column holding the dose. Only the positive values are fitted; zeros are the vehicle control and are reported separately.
response – column holding the measured response.
group – optional column giving one curve per level — per gene, per compound.
Nonefits the table as a single series.ci_method –
CI_PROFILE(default) orCI_WALD.confidence – nominal coverage, strictly between 0 and 1.
unit – concentration unit, for the sentences only. It never enters the arithmetic; an EC50 is reported in whatever the column is in.
direction –
DIRECTION_AUTO(default) or a pinned direction.allow_non_monotone – fit a bell-shaped series anyway. Offered because a user may know that the reversal is one bad well; never the default, and the result keeps the check so the caveat survives.
max_reversal – the
monotonicity()threshold.model –
MODEL_4PL(default) orMODEL_5PL. The asymmetric model is a choice the user makes and never one the data makes for them: it is offered because real asymmetry exists and a 4PL absorbs it into a displaced EC50, and it is not the default because the fifth parameter is weakly identified on the eight or ten concentrations a plate actually carries. A fit that chose 5PL reports whether the extra parameter earned itself — seeDoseResponseResult.asymmetry_p.
- Raises:
DoseResponseError – on an unknown method, direction or model, or a confidence outside (0, 1) — at the point the spec is built, not halfway through a plate.
- __post_init__() None[source]¶
Normalise the column names and validate the fit settings.
- Raises:
DoseResponseError – if
ci_method,directionormodelis not one this module offers; ifconfidenceis not strictly between 0 and 1 – it is a coverage probability, so a 95% interval is 0.95 and not 95; or ifmax_reversalis not a fraction of the response span in(0, 1].
- classmethod from_dict(payload: Mapping[str, Any]) DoseResponseSpec[source]¶
Rebuild from
to_dict().Unknown keys are ignored and missing keys defaulted, so an analysis written by another build of spaCR still opens.
- Parameters:
payload – mapping as written by
to_dict(); only the spec’s own field names are used.
- classmethod from_json(text: str) DoseResponseSpec[source]¶
Inverse of
to_json().- Parameters:
text – JSON text as written by
to_json().
- with_ci_method(method: str) DoseResponseSpec[source]¶
A copy using a different interval.
- Parameters:
method –
CI_PROFILEorCI_WALD; any other value raisesDoseResponseError.
- with_columns(concentration: str, response: str, group: str | None = None) DoseResponseSpec[source]¶
A copy pointed at different columns.
- Parameters:
concentration – column holding the dose.
response – column holding the measured response.
- with_unit(unit: str) DoseResponseSpec[source]¶
A copy that says the concentrations are in
unit.- Parameters:
unit – concentration unit for the sentences; surrounding whitespace is stripped.
- class spacr.qt.widgets.dose_response.GroupFit[source]¶
One level of the grouping column: a result, or the refusal.
Both are first-class. A plate where three compounds fit and one is bell-shaped has four rows in its table, and the fourth says why — hiding it would turn a refusal into a missing row, which reads as “no data”.
- Parameters:
group – the level.
result – its
DoseResponseResult, orNone.error – the refusal message, or
None.n_rows – rows the group had before anything was dropped.
- class spacr.qt.widgets.dose_response.HormesisCheck[source]¶
Whether the low-dose end of the series is stimulated, and by how much.
monotonicity()can see that a series turns around. It cannot say which turn it is, and the two turns a concentration series produces mean opposite things: a collapse at the top dose is usually the compound killing whatever was being counted, and the fix is to drop that dose, while a hump at the bottom is hormesis, and dropping doses there throws the finding away. This check names which one it is by fitting the model that has a hump in it and asking whether the hump paid for itself.- Parameters:
is_hormetic – the verdict, and it needs all four criteria below.
stimulation – the fitted Brain–Cousens coefficient, in response units per concentration unit. Positive is a hump.
max_stimulation – the largest departure from the control plateau the fitted hormetic curve reaches inside the tested range, in response units.
max_stimulation_fraction – that departure over the fitted curve’s response span — the number the minimum-effect threshold is applied to.
peak_dose – where that maximum sits, or
Nonewhen there is no fitted curve to read it off.f_statistic – extra-sum-of-squares F for the one added parameter.
p_value – its p value, on 1 and
dofdegrees of freedom.delta_aic – corrected AIC of the monotone fit minus that of the hormetic fit. Positive favours hormesis.
sse_monotone – residual sum of squares of the 4PL.
sse_hormetic – residual sum of squares of the Brain–Cousens fit.
n_obs – observations both models were fitted on.
dof – residual degrees of freedom of the hormetic fit,
n - 5.ec50_monotone – the EC50 the monotone fit reports, so the cost of ignoring the hump is visible as a number rather than an argument.
ec50_hormetic – the EC50 the hormetic fit reports.
min_effect – the
HORMESIS_MIN_EFFECTthis used.alpha – the
HORMESIS_ALPHAthis used.min_delta_aic – the
HORMESIS_MIN_AICthis used.note – why the verdict is what it is — the first criterion that failed, or the caveat that survives a positive verdict.
- class spacr.qt.widgets.dose_response.InteractionSurface[source]¶
A checkerboard’s interaction, per cell, with its own axes.
THE SURFACE IS THE RESULT AND A SINGLE INDEX IS NOT. One number for a whole checkerboard hides exactly the concentration-dependent structure that makes synergy interesting. Real combinations are frequently synergistic in one corner of the grid and additive or antagonistic in another, and a mean over the grid reports neither.
SIGN CONVENTION, stated because every paper states a different one: POSITIVE MEANS MORE EFFECT THAN EXPECTED – synergy for an inhibition assay. The expected surface is what the model predicts from the two single-agent curves;
excessis observed minus expected.- Parameters:
model –
SYNERGY_BLISSorSYNERGY_LOEWE.dose_a – the unique concentrations of agent A, ascending.
dose_b – the unique concentrations of agent B, ascending.
observed –
(len(dose_a), len(dose_b))effect, 0 to 1.expected – what
modelpredicts for each cell.excess –
observed - expected. NaN where a cell was not tested.n_cells – cells with an observation.
note – what could not be computed, and why.
- summary() Dict[str, Any][source]¶
Headline numbers, each said to be over the grid rather than of it.
Deliberately NOT a synergy index. The strongest cell and where it sits are reportable; a mean over the whole grid is the number this class exists to avoid, so it is absent rather than provided-with-a-warning.
- class spacr.qt.widgets.dose_response.MonotonicityCheck[source]¶
Whether the concentration-ordered response only ever goes one way.
Computed on the per-concentration medians, not the raw points. The median limits the influence of one outlier well on the fit decision.
- Parameters:
doses – the distinct positive concentrations, ascending.
medians – the median response at each of them.
span –
max(medians) - min(medians), the yardstick everything else is measured against.reversal – the excursion that no monotone trend explains, in response units. It is the smaller of the largest fall after a rise and the largest rise after a fall, so a clean increasing series scores ~0 (its falls are noise), a clean decreasing series scores ~0, and a bell scores most of the span whichever way you read it.
reversal_fraction –
reversal / span.sign_changes – how many times the direction of the successive median differences flips, ignoring steps flatter than
FLAT_FRACTIONof the span.turning_points – the concentrations at which those flips happen.
spearman_rho – rank correlation of median response against log concentration. Near zero with a large reversal is the signature of a symmetric bell.
threshold – the
reversalat which this check would have failed.is_monotone – the verdict.
- class spacr.qt.widgets.dose_response.PlateReport[source]¶
What one plate’s controls said, and whether the plate may be fitted.
ONE ROW PER PLATE, REFUSALS INCLUDED. A plate that is dropped leaves a report saying why it was dropped, so a user comparing eight plates and seeing six curves can find the other two without re-running anything.
- Parameters:
status –
STATUS_FITTEDwhen the plate normalised, orSTATUS_REFUSED– the same vocabulary the curve fits speak.zprime – the plate’s Z-factor, or
Nonewhen it has no Z’ because a control had fewer than two wells.Noneis not a failure; it is the absence of a number, and is reported as such.note – the sentence to show the user. Empty when nothing is wrong.
plate – the plate identifier this report is about, as it appears in the plate column of the frame.
n_positive – how many positive-control wells were found on it. Reported even when the plate is refused, because “two” and “none” are different problems and the note alone does not distinguish them.
n_negative – the same for negative controls.
- class spacr.qt.widgets.dose_response.PlateSpec[source]¶
Which column is the plate, and which wells on it are the controls.
THE ENGINE HAD NO NOTION OF A PLATE, which is why it could fit a clean EC50 on a plate the rest of the package already knew was bad. This is that notion: the plate column, the control column, and the levels in it that mean “full effect” and “no effect”.
Frozen and JSON round-tripping like
DoseResponseSpec, so the normalisation behind a figure travels with the fit that used it.- Parameters:
plate – column identifying the plate. Required.
control – column naming each well’s control role. Required.
positive – levels of
controlthat are the positive control – the full-effect wells, which normalise to 100.negative – levels that are the negative control – vehicle or untreated, which normalise to 0.
min_zprime – refuse every plate whose Z’ falls below this, and say the Z’ in the refusal.
None(default) gates nothing and still reports the Z’ beside each plate.ZPRIME_MARGINALis the conventional 0.5 if you want one.
- Raises:
DoseResponseError – when a column or a control level is missing, at the point the spec is built rather than halfway through a plate.
- __post_init__() None[source]¶
Normalise the names and insist both controls exist.
- Raises:
DoseResponseError – when
plateorcontrolis blank, or when either control has no levels – percent inhibition is a two-point scale and one control cannot define it.
- class spacr.qt.widgets.dose_response.PooledFit[source]¶
One EC50 across replicate plates, with plate as a random effect.
THREE EC50s AND AN EYEBALL is what this replaces. A user with three replicate plates today fits three curves and averages the numbers by hand, which throws away how well each one was determined and says nothing about whether the three agreed.
TWO-STAGE, NOT ONE. Each plate is fitted on its own – by the same
fit_dose_response()that fits everything else, with the same refusals – and the per-plate log10 EC50s are then combined with a random-effects weight. One joint nonlinear mixed model would be the other way to do it; it would also mean a second fitting path with a second set of failure modes, and a plate thatfit_dose_response()refuses would have to be refused again, differently, inside it. This way a refusal on one plate stays exactly the refusal this module already speaks.RANDOM, NOT FIXED. Fixed-effect pooling assumes every plate measures the same value and differs only by noise. Three tight plates that disagree then give a narrow interval around a value none of them support. The DerSimonian-Laird estimate of the between-plate variance is added to each plate’s own, so real variation between plates widens the answer instead of being weighted away.
- Parameters:
status –
STATUS_FITTED, orSTATUS_REFUSEDwhen too few plates fitted or the plates disagree beyond what their own uncertainty explains.ec50 – the pooled EC50, or
Nonewhen refused.tau – the between-plate SD on the log10 scale – it shows how far the plates agree about this compound, which no average of three EC50 values can report.
i_squared – the share of the observed spread that is real rather than sampling noise, in
[0, 1].q – Cochran’s Q against the null that every plate measured the same EC50.
q_p – the p-value of that Q.
per_plate – the individual fits, kept so the pooled number can always be taken apart again.
note – why, when refused or when the plates sit uneasily together.
log10_ec50 – the pooled estimate on the log10 scale, which is where the pooling is actually done – EC50s are log-normal, so averaging them in linear units weights the high plates more than the data warrants.
log10_se – the standard error of that estimate, on the same scale.
ec50_low – the low end of the confidence interval, back on the linear scale the user reads.
ec50_high – its high end.
- class spacr.qt.widgets.dose_response.SelectivityIndex[source]¶
Host toxicity over parasite killing, with the interval it deserves.
THE NUMBER THAT DECIDES WHETHER ANYBODY CARES about an anti-parasitic compound is not the EC50, it is this ratio. A compound that kills the parasite at 1 uM and the host monolayer at 1.2 uM is not a hit, and an EC50 quoted with a clean confidence interval says nothing about that.
QUOTED WITH ITS INTERVAL OR NOT AT ALL, for the same reason this module already refuses a naked EC50: a ratio of two uncertain numbers is more uncertain than either of them, and a selectivity index without its interval invites a reader to treat 1.2 and 12 as the same kind of claim.
- Parameters:
status –
STATUS_FITTED,STATUS_UNBOUNDEDorSTATUS_REFUSED, reusing the vocabulary the single-curve fits already speak rather than inventing a second one.index – the quotable ratio, or
Nonewhen it is not quotable.index_low – lower end of the interval, or
Nonefor an open side.index_high – upper end, or
Nonefor an open side.log10_index – the difference of the two log10 midpoints, always present when both curves fitted. An extrapolation when either EC50 is unbounded, exactly as
DoseResponseResult.ec50_unconstrainedis.host – the host-viability fit.
pathogen – the parasite fit.
note – why, when the index is refused or one-sided.
- spacr.qt.widgets.dose_response.bliss_surface(dose_a, dose_b, response, *, fit_a: DoseResponseResult, fit_b: DoseResponseResult) InteractionSurface[source]¶
Bliss independence over a checkerboard.
THE MODEL IN ONE LINE: if two agents act independently, the fraction surviving both is the product of the fractions surviving each, so the expected effect is
Ea + Eb - Ea*Eb. Excess over that is synergy.BLISS NEEDS NOTHING FROM THE COMBINATION FITS, which is why it is the cheaper of the two and the one to reach for first: both single-agent curves already give an effect at every concentration on the grid.
- Parameters:
dose_a – agent A concentration per well.
dose_b – agent B concentration per well.
response – measured response per well, same length.
fit_a – the single-agent fit for A (B held at zero).
fit_b – the single-agent fit for B.
- Returns:
- spacr.qt.widgets.dose_response.brain_cousens(x, bottom, top, log10_ec50, hill, stimulation)[source]¶
The Brain–Cousens hormesis model: a 4PL with a low-dose hump on it.
y = four_parameter_logistic(x, ...) - sign(hill)·stimulation·x·w(x)withw(x) = 1 / (1 + (x / EC50) ** |hill|), the weight that is 1 at zero dose, ½ at the EC50 and 0 above it.For an inhibition curve (
hill < 0) that is the published model (Brain & Cousens, Weed Research 29, 1989) written in this module’s parameterisation:wis then exactly the 4PL’s own sigmoid factor, and the expression collapses tobottom + (top - bottom + f·x)·w, which is Brain–Cousens term for term. For an activation curve it is that model reflected, because hormesis is a departure from the control against the direction the compound eventually pushes the readout, and on an activation series that is a low-dose dip rather than a low-dose rise. Writing it as one function with the sign taken from the Hill slope means the same hypothesis test covers both, instead of a second model nobody would remember to run.stimulation == 0is the 4PL exactly, which is what makes the pair nested andhormesis()’ F test a one-parameter test.- Parameters:
x – concentration(s).
bottom – the smaller plateau of the underlying 4PL.
top – the larger plateau of the underlying 4PL.
log10_ec50 – base-10 log of the underlying 4PL’s EC50.
hill – slope; its sign carries the direction, as everywhere here.
stimulation – the hormesis term’s coefficient, in response units per concentration unit. Positive is hormesis; zero is no hormesis; negative is a curve that leaves the control plateau faster than a 4PL, which is a shape correction and is never called hormesis.
- Returns:
the modelled response, same shape as
x.
- spacr.qt.widgets.dose_response.candidate_concentration_columns(frame: pandas.DataFrame) Tuple[str, ...][source]¶
Columns worth offering as the concentration axis.
Numeric, not a key or free text, and carrying at least
MIN_DOSESdistinct positive values — a column that never takes four different positive values cannot be a dilution series whatever it is called, and offering it only produces a refusal one click later.Note what is not required:
CONTINUOUS. The shared classifier calls a low-cardinality numeric column categorical, which is the right call forcell_countand the wrong one here — an eight-point dilution series has exactly eight levels by design, so the classifier’s own rule would hide every concentration column in the project. The classifier is still what excludes object keys and free text (UNPLOTTABLE), which is the part of its judgement that transfers; the continuous/categorical split does not identify dose columns reliably.- Parameters:
frame – the table whose columns are screened; they are returned in sorted name order.
- spacr.qt.widgets.dose_response.candidate_response_columns(frame: pandas.DataFrame) Tuple[str, ...][source]¶
Columns worth offering as the response axis: every continuous one.
Here the classifier’s continuous/categorical split is the right cut: a response is a measured quantity, and a column with four levels is a label or a count rather than something a sigmoid passes through.
- Parameters:
frame – the table whose columns are classified; the continuous ones are returned in sorted name order.
- spacr.qt.widgets.dose_response.checkerboard_from_frame(frame: pandas.DataFrame, *, dose_a: str, dose_b: str, response: str) Checkerboard[source]¶
Read a checkerboard off a well table, single-agent axes and all.
- Parameters:
frame – one row per well.
dose_a – column holding agent A’s concentration.
dose_b – column holding agent B’s.
response – column holding the measurement.
- Returns:
a
Checkerboardready forbliss_surface(),loewe_surface()and the single-agent fits they need.- Raises:
DoseResponseError – when a column is missing, when either agent has no single-agent wells – without them there is no curve to predict the combination from, and a surface computed against the combination wells themselves would be comparing the data to itself – or when no well has both agents present, which is a pair of dose series and not a checkerboard.
- spacr.qt.widgets.dose_response.fit_dose_response(doses: Sequence[float], responses: Sequence[float], spec: DoseResponseSpec | None = None, *, group: str = '') DoseResponseResult[source]¶
Fit one concentration series, or refuse it.
The whole policy is in the module docstring. The short version: zeros are vehicle controls and are reported rather than logged; a series that is not monotone is refused rather than fitted; the fit is canonicalised so the Hill slope carries the direction; the EC50 is reported only when the experiment actually locates it, and otherwise as a one-sided bound with
DoseResponseResult.ec50set toNone.Two things happen here that the model choice does not change. A series whose low-dose end rises against the trend is tested for hormesis (
hormesis()) whether or not it also fails the monotonicity check, because a hump can be worth a fifth of the response span and still be insideMAX_REVERSAL. A hormetic series that fails the monotonicity check is refused as hormesis rather than as the cytotoxicity a bell shape usually is — a different sentence, a different thing to do next. A hormetic series that passes it is fitted, and carries the finding as a caveat and asDoseResponseResult.hormesis.The 5PL is fitted from the 4PL solution, so the asymmetric fit starts at the symmetric answer and can only leave it for something better; the improvement is then reported as an F test on the one extra parameter.
- Parameters:
doses – concentrations, one per observation, replicates included.
responses – the matching responses.
spec – a
DoseResponseSpec. Only its policy fields matter here — includingmodel; the column names are forfit_frame().group – a label carried through onto the result.
- Raises:
DoseResponseError – for every series that cannot carry the chosen model — too few concentrations, a flat response, a negative concentration, a bell shape, a hormetic curve, or an optimiser that never converged. The message says which and what to do about it.
- spacr.qt.widgets.dose_response.fit_frame(frame: pandas.DataFrame, spec: DoseResponseSpec) DoseResponseSet[source]¶
Fit one curve per level of
spec.group— the whole plate at once.A refusal in one group is kept beside that group and does not stop the others: a plate where one compound is cytotoxic at the top dose should still report the other twenty-three, with the cytotoxic one visibly labelled rather than missing.
- Parameters:
frame – the table holding the concentration, response and optional grouping columns named by
spec.spec – the columns to fit and the fitting policy; with a
groupcolumn, one curve is fitted per level.
- Raises:
DoseResponseError – only for something wrong with the table — a column that is not there, or a group column with no levels. Per-curve failures land in
GroupFit.error.
- spacr.qt.widgets.dose_response.five_parameter_logistic(x, bottom, top, log10_ec50, hill, asymmetry)[source]¶
The 5PL curve, parameterised so
log10_ec50is still the EC50.y = bottom + (top - bottom) / (1 + a·10 ** ((log10_ec50 - log10 x) * hill)) ** asymmetry, witha = 2 ** (1 / asymmetry) - 1.That scale factor is the whole point of this function. Written the way the 5PL usually is —
\[y = \mathrm{bottom} + \frac{\mathrm{top} - \mathrm{bottom}} {\left(1 + 10^{(c - \log_{10}x)h}\right)^{s}}\]— the parameter
cis not the half-maximal concentration onces != 1: atx = 10**cthe response isbottom + (top - bottom) / 2**s, which fors = 3is an eighth of the way up rather than half. A 5PL reported as ifcwere the EC50 is wrong by a factor that grows with the asymmetry, and it is a published mistake often enough to have its own literature (Gottschalk & Dunn, Anal. Biochem. 343, 2005). Scaling the exponential by2 ** (1 / s) - 1moves the half-maximal point back tox = EC50exactly, so:asymmetry == 1reproducesfour_parameter_logistic()term for term, which is what makes the two models nested and the F test infit_dose_response()legitimate;every downstream rule in this module — the boundedness tests, the profile interval, the back-transformed reporting — keeps reading
log10_ec50as the same quantity under both models, and none of them had to learn a second meaning.
- Parameters:
x – concentration(s).
0evaluates to the low-dose plateau, as infour_parameter_logistic().bottom – the smaller plateau.
top – the larger plateau.
log10_ec50 – base-10 log of the half-maximal concentration. It is the EC50 here, not the inflection parameter.
hill – slope. Negative is inhibition, positive is activation.
asymmetry – the exponent.
1is the symmetric 4PL; below 1 the curve leaves the low-dose plateau slowly and reaches the high-dose one abruptly, above 1 the reverse.
- Returns:
the modelled response, same shape as
x.- Raises:
DoseResponseError – if
asymmetryis not strictly positive — the exponent of a nonneg quantity cannot be zero or negative here, and returningnanwould let the optimiser wander into it.
- spacr.qt.widgets.dose_response.four_parameter_logistic(x, bottom, top, log10_ec50, hill)[source]¶
The 4PL curve, parameterised in
log10(EC50).y = bottom + (top - bottom) / (1 + 10 ** ((log10_ec50 - log10 x) * hill)).At
x == EC50the exponent is zero and the response is exactly halfway between the plateaus, which is the definition the EC50 is quoted under.- Parameters:
x – concentration(s), in the user’s units.
0is evaluated at its limit (the low-dose plateau) rather than raising, because the clipped exponent below makeslog10(0) = -infwell behaved; the fit itself never sees a zero — seefit_dose_response().bottom – the smaller plateau, after canonicalisation.
top – the larger plateau.
log10_ec50 – base-10 log of the half-maximal concentration.
hill – slope. Negative is inhibition, positive is activation.
- Returns:
the modelled response, same shape as
x.
- spacr.qt.widgets.dose_response.hormesis(doses: Sequence[float], responses: Sequence[float], *, direction: str = DIRECTION_AUTO, min_effect: float = HORMESIS_MIN_EFFECT, alpha: float = HORMESIS_ALPHA, min_delta_aic: float = HORMESIS_MIN_AIC) HormesisCheck[source]¶
Is the low-dose end of this series stimulated, or is it noise?
Fits a 4PL and a Brain–Cousens curve (
brain_cousens()) to the same points and compares them. The two models are nested — Brain–Cousens is the 4PL atstimulation = 0— so the comparison is the classical extra-sum-of-squares F test on one degree of freedom, which under Gaussian errors is the likelihood-ratio test written in the units this module already reports. It is required to agree with a corrected AIC, and both are required to agree with a minimum effect size.Four criteria, and all four have to hold. Any one of them alone is a way to be wrong:
the fitted
stimulationis positive — a negative coefficient makes the curve leave the control plateau faster, which is a 4PL that does not quite fit rather than a compound that stimulates;p < alphaon the F test;ΔAICc >= min_delta_aic, computed with the small-sample correction because these are five- and six-parameter models on twenty-odd wells;the hump reaches
min_effectof the fitted response span — the minimum-effect threshold, without which a very tight assay reports hormesis at 2% and a reader acts on it.
Why the effect is measured against the response span and not as a percentage of the control, which is the convention in the hormesis literature: this module normalises plates to percent inhibition, where the control is 0 by construction and a percentage of it is either infinite or meaningless. The span is the window the curve moves through under either scaling.
The F test is two-sided and the hypothesis is one-sided, since only a positive stimulation is hormesis. Criterion 1 does the one-sided part by inspection rather than by halving the p value, so the reported p is conservative by about a factor of two. Stated rather than corrected: a conservative p on a screen that already refuses this data is the right direction to be wrong in.
- Parameters:
doses – concentrations, one per observation. Zeros are vehicle controls and are dropped, as everywhere in this module.
responses – the matching responses.
direction –
DIRECTION_AUTOor a pinned direction. It sets which way “stimulated” points — for an inhibition series hormesis is a rise above control, for an activation series a dip below it.min_effect – the minimum-effect threshold, as a fraction of the fitted response span. See
HORMESIS_MIN_EFFECT.alpha – significance the F test has to reach.
min_delta_aic – corrected-AIC gap the hormetic fit has to win by.
- Returns:
a
HormesisCheck. Readis_hormetic; when it isFalsethenotesays which criterion failed.- Raises:
DoseResponseError – only for data that cannot be read at all — a negative concentration, or a concentration and response column of different lengths. Too few points to test is a
Falseverdict with a note, not an exception: “we could not test this” is an answer.
- spacr.qt.widgets.dose_response.loewe_surface(dose_a, dose_b, response, *, fit_a: DoseResponseResult, fit_b: DoseResponseResult) InteractionSurface[source]¶
Loewe additivity over a checkerboard, as a combination index.
THE MODEL IN ONE LINE: if two agents are dilutions of one another, then reaching an effect costs a constant total dose, so
a/Da + b/Db = 1whereDaandDbare the single-agent doses giving that same effect. Below 1 is synergy.REPORTED AS EXCESS RATHER THAN AS THE INDEX ITSELF, so that the sign convention matches Bliss and a reader comparing the two surfaces is not also flipping a comparison in their head:
excess = 1 - CI, positive for synergy.WHY IT CAN BE NaN WHERE BLISS IS NOT: Loewe needs the INVERSE curve – the dose achieving an observed effect – and that dose does not exist when the observed effect lies outside a single agent’s own plateaus. A combination that kills more than either agent can alone has no Loewe answer, and NaN is the honest one.
- Parameters:
dose_a – agent A concentration per well.
dose_b – agent B concentration per well.
response – measured response per well.
fit_a – the single-agent fit for A.
fit_b – the single-agent fit for B.
- spacr.qt.widgets.dose_response.monotonicity(doses: Sequence[float], responses: Sequence[float], *, max_reversal: float = MAX_REVERSAL) MonotonicityCheck[source]¶
Is this concentration series consistent with a single sigmoid?
A 4PL is monotone by construction. Data that is not monotone is not described by one, and fitting it anyway returns a confident EC50 for a curve of the wrong shape — the specific failure this module exists to prevent.
The test is an excursion test rather than a sign-change count, because a sign-change count cannot tell a 5% wobble on a plateau from a collapse at the top dose, and on a ten-point series with replicates the wobbles are guaranteed. Both numbers are reported; only the excursion decides.
- Parameters:
doses – positive concentrations, one per observation. Replicates allowed and expected.
responses – the matching responses.
max_reversal – the fraction of the response span an excursion against the trend may reach. See
MAX_REVERSAL.
- Returns:
a
MonotonicityCheck; readis_monotone.
- spacr.qt.widgets.dose_response.normalise_to_controls(frame: pandas.DataFrame, spec: PlateSpec, *, response: str, out: str = PERCENT_COLUMN) Tuple[pandas.DataFrame, Tuple[PlateReport, ...]][source]¶
Percent inhibition against each plate’s own controls.
RAW RESPONSES ARE NOT COMPARABLE ACROSS PLATES. Two plates read on different days differ in absolute signal by more than most compounds move it, so three replicate plates fitted raw produce three EC50s whose spread is mostly instrument drift. Scaling each plate to its own controls – negative reads 0, positive reads 100 – removes exactly that and leaves the biology.
percent = 100 * (value - mean_negative) / (mean_positive - mean_negative)The formula is signed and direction-agnostic on purpose: whichever way the raw readout runs, the positive control reads 100 by construction, so a viability readout and a burden readout normalise the same way and the fit downstream does not need to be told which it got.
REFUSED RATHER THAN SCALED BY NOISE. When a plate’s two controls do not separate there is no assay window, and dividing by that near-zero difference would turn well-to-well noise into hundreds of percent inhibition and a confident EC50 on a plate that measured nothing. Such a plate’s rows come back with a NaN response and a
PlateReportsaying so.- Parameters:
frame – one row per well, with the plate, control and response columns the spec and this call name.
spec – the plate, its control column and its two control levels.
response – the raw column to normalise.
out – column to write the percent into. Defaults to
PERCENT_COLUMN; pass another name to keep several readouts (host viability and parasite burden, say) side by side.
- Returns:
(frame, reports)– a copy of the frame carryingout, and onePlateReportper plate in the order the plates first appear. Rows on a refused plate carry NaN, so a caller that fits the whole table drops those plates without having to filter first.- Raises:
DoseResponseError – when a named column is missing, or when no plate has both controls – there is nothing to normalise against and a frame of NaN would be a worse answer than a sentence.
- spacr.qt.widgets.dose_response.plate_reports(frame: pandas.DataFrame, spec: PlateSpec, *, response: str) Tuple[PlateReport, ...][source]¶
The per-plate verdict alone, without normalising anything.
For the screen that wants to show the plate table before the user has chosen a readout to fit, and for a caller that only wants to know which plates would be dropped. A table where every plate is refused returns its refusals rather than raising – here the refusals ARE the answer.
- Parameters:
frame – the long-format table, one row per well.
spec – which column names the plate, which the control, and which control values are the two ends.
response – the readout column whose control means decide the plate’s Z’. Keyword-only, because a plate’s verdict is about a PARTICULAR readout and passing it positionally invites reading the report as a property of the plate alone.
- Raises:
DoseResponseError – when a named column is missing.
- spacr.qt.widgets.dose_response.pool_across_plates(fits: Mapping[str, DoseResponseResult], *, confidence: float = DEFAULT_CONFIDENCE, max_heterogeneity: float = MAX_HETEROGENEITY) PooledFit[source]¶
Combine per-plate fits into one EC50 with plate as a random effect.
POOLED ON THE LOG10 SCALE, because that is the scale the EC50 is estimated on and the scale its interval is symmetric on. Averaging three EC50s of 1, 10 and 100 uM arithmetically gives 37 uM; pooling their logarithms gives 10, which is the middle of the three in the only sense that matters for a concentration.
- Parameters:
fits – plate label to that plate’s fit. Only fits that are
STATUS_FITTEDwith a closed interval can carry a weight; the rest are counted, named in the note and left out of the arithmetic, because a plate whose EC50 is unbounded has no variance to weight by and dropping it silently would make the pooled interval look better than the experiment was.confidence – coverage for the pooled interval.
max_heterogeneity – refuse above this I-squared.
- Returns:
a
PooledFit, refused rather than empty when the plates cannot support a single number.- Raises:
DoseResponseError – when
max_heterogeneityis not in(0, 1].
- spacr.qt.widgets.dose_response.pool_frame(frame: pandas.DataFrame, spec: DoseResponseSpec, *, plate: str, max_heterogeneity: float = MAX_HETEROGENEITY) PooledFit[source]¶
Fit each plate in
frameon its own, then pool them.The convenience over
pool_across_plates()for the common case: one table, one compound, a plate column. A plate that raisesDoseResponseErroris kept out of the pool and named in the note, exactly asfit_frame()keeps one bad compound from taking a plate down.- Parameters:
frame – the long-format table, one row per well, with every replicate plate in it.
spec – the fit specification, applied unchanged to every plate – which is what makes the per-plate EC50s comparable in the first place.
plate – the column identifying the replicate.
- Raises:
DoseResponseError – when
plateis not a column, or when no plate produced a fit at all – there is nothing to pool and a refusal with no plates in it would say nothing about why.
- spacr.qt.widgets.dose_response.selectivity_index(pathogen: DoseResponseResult | None, host: DoseResponseResult | None, *, confidence: float | None = None) SelectivityIndex[source]¶
Divide a host EC50 by a parasite EC50, and carry the uncertainty.
THE TWO FITS COME OFF ONE PLATE, which is the argument for computing this here rather than in a spreadsheet. spaCR segments host cell and pathogen as separate object types from the same image, so host viability and parasite burden are measured at the same doses in the same run. Most selectivity indices divide two numbers from two experiments and hope the conditions matched; these cannot fail to match.
THE INTERVAL IS PROPAGATED IN LOG SPACE, where the fit lives and where a ratio is a difference:
log10 SI = log10 CC50 - log10 EC50, and the two variances add. Back-transforming at the end gives an interval that is asymmetric in linear space, which is the honest shape for a ratio.REFUSAL PROPAGATES TOO. If either curve was refused the index is refused; if either EC50 is unbounded the index is unbounded, and whichever side of the interval the data still supports is reported rather than dropped – “at least 8-fold” is a useful sentence and this returns it.
- Parameters:
pathogen – the parasite-burden fit, or
Noneif it was refused.host – the host-viability fit, or
Noneif it was refused.confidence – overrides the level carried by the fits.
- Returns:
a
SelectivityIndex, never an exception, because a refusal is a result the caller has to show.
Nested helpers¶
- PooledFit.summary_row.num(value)¶
Noneas NaN, so a refused fit still fills its columns.A refusal has no EC50, and leaving the cell empty would make the row a different shape from a fitted one – which is what a table cannot have. NaN is the value that says “not a number here” without changing the columns.
spacr/qt/widgets/dose_response.py:3899
- _effect_curve.effect(dose: np.ndarray) np.ndarray¶
The affected fraction at each dose, in [0, 1].
Zero and negative doses are clamped to a tiny positive number rather than refused: a checkerboard’s first row IS zero, and a 4PL has no value there because log10(0) is undefined. The clamp puts them at the curve’s own baseline, which is what an untreated well measures.
- Parameters:
dose – concentrations, any shape.
- Returns:
affected fraction, same shape.
spacr/qt/widgets/dose_response.py:3173
- _profile_five_sse.objective(point) float¶
SSE at one
(log slope magnitude, log asymmetry)point.spacr/qt/widgets/dose_response.py:2367
- fit_dose_response.wald(index: int) Tuple[float | None, float | None]¶
One parameter’s Wald interval, or
Nonewhen it cannot be formed.Returns None rather than an interval when the covariance is unusable: an interval computed from a bad covariance looks like a result and is not one.
spacr/qt/widgets/dose_response.py:2687
- selectivity_index._ratio(numerator, denominator)¶
One end of the interval, or
Nonewhen that end is open.Returns
Nonerather than raising or substituting a sentinel: an open side of a one-sided index is a fact about the experiment, and a number here would make it look bounded.- Parameters:
numerator – a host EC50 bound, or
None.denominator – a parasite EC50 bound, or
None.
spacr/qt/widgets/dose_response.py:3032