Build the nec and ec50 prior on the log of the predictor - #304
Conversation
make_inits(), refine_inits() and sample_priors() each held their own table of four distributions, so a prior on any other name reached fcts[[dist]]() as NULL and failed with "attempt to apply non-function". One lookup now serves all three, includes lognormal, and names the distribution it cannot draw from. hurdle_mu_family() returned an identity-link family whatever link the caller supplied, so define_prior() measured the response untransformed while brms puts top and bot on the mean link scale. zero_inflated_beta on the logit link was given a [0, 1] support for parameters that are outside it whenever the plateau exceeds 0.73 or the floor falls below 0.5. Prerequisites for #302. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
The three x_prs entries selected by the predictor's support become one construction: lognormal where the predictor is supplied on the dose scale, normal where it spans negative values and has therefore been logged already. Location is the median of the distinct positive values on the log scale; width is set so the central 95% interval spans the tested doses, which is a stated criterion rather than a chosen constant. gamma(5, 4/m) reaches the highest dose only where that dose is within 2.6 times the median, a ratio of 2.0 on a linear series and 13 to 125 on the nassarius ones, and no fixed shape serves both. Selecting on the support made the prior depend on the units the dose was recorded in. Closes #302. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
median() is applied after logging, so it interpolates between the two central doses on the log axis and returns their geometric mean. That is the log of the median dose only where the count is odd. The claim is corrected in the roxygen, NEWS and vignette, and pinned in a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
Set the width so the prior covers every dose tested, rather than from half the range of the logged series. Half the range gives the interval the right log-width and the wrong centre wherever the series is asymmetric about its median: on nassarius contaminant A it reached 9.96 against a highest dose of 20, which is the defect #302 exists to remove. The width is now the larger of the two half-widths from the location to the ends. define_prior() gains predictor_scale, the vector the predictor-scaled prior is built from and truncated to. The two blocks of a hurdle fit pass the whole predictor, so the mu block no longer takes a prior shaped by the survivors alone and truncated to a wider range, and no longer refuses a design whose survivors all sit at the zero control. This replaces the rebound() closure, which corrected the bounds after the fact and left the prior behind. Corrections to the record: the already-logged entry does change, because it now reads distinct values; the spread across the old branches is about 600-fold on dev, not 300; the k = 1.5 lower bound is a factor of 45 below the lowest dose, not four orders of magnitude; the multiple of sd(log x) is 0.93 to 1.03 on the nassarius series, and the 0.73 figure came from a synthetic design. Tests added for coverage on an asymmetric series in both directions, a low threshold on a wide series, a two-dose design, a control recorded as a small positive value, both hurdle mu-block cases, the integer-predictor refusal, and a lognormal drawn through make_inits(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
Response to the reviewPushed as 1. The width criterion — accepted, code changedCorrect, and the reproduction is exact. Setting The criterion is the part worth keeping, so the code was changed rather than the The rule is otherwise unchanged where the series is symmetric on the log axis, What it changes elsewhere: the multiple of 2. The test was passed by a symmetric design — acceptedAlso correct. The neighbouring test's weaker claim is also addressed: a separate test now 3. The refusal reachable through
|
Qualify two properties as holding of the untruncated prior. Its median on the dose scale is the median dose before truncation; truncating at the highest dose removes part of the upper tail and pulls the median down, to 0.58 against a median dose of 0.88 on nec_data. Record that the adopted prior's dose-scale density falls monotonically across the tested range on the four nassarius series, and why that is not the defect the shape-1.03 gamma has. A lognormal's dose-scale mode is exp(mu - sigma^2), so the density falls because the change of variable redistributes it; the gamma at shape 1.03 has neither its mode nor its median at m on any scale. define_hurdle_prior() takes predictor_scale and passes it down, so the argument is no longer discarded on the hurdle branch. Note in the code and in the test that only the mu block changes: survival_by_x() returns sort(unique(predictor)), so the second block already read the whole predictor's distinct values. The multiple of sd(log x) is 0.92 to 1.03 on the nassarius series, not 0.93; the truncated CDF range 0.47 to 0.95 is the zero-control half of the sweep, 0.43 to 0.95 over all 60 cells. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
Response to the second reviewPushed as Correction to my previous commentThe previous comment stated that under the half-range rule the interval "covered R2-1, the median claim — acceptedThis is the round-1 failure repeated one level down and the diagnosis is right. R2-2, the monotone dose-scale density — accepted, recordedAlso right, and it is now stated in all three documents rather than left out of
The quantiles are the check that separates the two, and they are reported: the R2-6, the confirmatory fit — accepted, re-runYou were right to distrust it. The fit was run at the round-1 head under R2-7, the discarded argument — accepted
R2-8, the hurdle test — acceptedCorrect: R2-4 and R2-5 — accepted0.92, not 0.93, on the R2-9 — noted, no changeAgreed on all three points: the mass discarded above the highest concentration Verification
|
The shape is derived by solving qgamma(0.975, k, (k-1)/m) = max, so its mode is at m by construction. The text stated the opposite: that the density decreases monotonically across the tested range and that neither the mode nor the median is at m. Measured on the nassarius contaminant A series at shape 1.03002, rate 0.18749, the mode is 0.160000 exactly, the density rises 5.7% from the lowest dose to it and then falls to 2.8% of its peak at the highest. The conclusion is unchanged and the supporting quantity is the median, not the mode: that prior's median is 3.85, twenty-four times m and above every dose but the top one, against 0.223 for the prior adopted here, which is the median positive dose exactly. The claim came from the body of #302 and was repeated here without being checked. Also qualify the width criterion as describing the untruncated prior, which the vignette already did and the roxygen and NEWS did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
Response to the third reviewPushed as The shape-1.03 gamma — accepted, and it was wrong three waysYou are right and this is the most useful finding of the three rounds. Verified The claim came from the body of #302 and was repeated in the roxygen, in NEWS The argument is now made on the median, which is the right quantity and a
The text in all three documents now reads: at shape 1.03 the mode is still at I have not edited #302's body, since a comment there would age worse than this The unqualified criterion — accepted
Three of the four places were unqualified for the same reason the R2-1 pair Items 3, 4, 5 and 6 — noted, no changeNothing to add. Thank you for re-deriving the forwarding at all five call sites State of the PREvery finding from the three rounds is now either fixed or answered. The code
|
The contaminant A series has ten positive doses, so 0.223 is exp(mu), the geometric mean of the two central doses, and not a dose. Calling it "the median positive dose" contradicted the even-count rule the same documents state two paragraphs earlier. The vignette had introduced the second defect and closed it before returning to the first, leaving "both defects" without a nearby referent. The shape paragraph now follows the design ratio it belongs to, as it already did in NEWS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
Response to the fourth reviewPushed as the head of the branch. Both items accepted; text only, no code R4-1. Correct. The contaminant A series has ten positive doses, so their R4-2. Also correct. The vignette had introduced the units defect and closed Thank you for the four rounds. The shape-1.03 finding in particular was a false |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
Review record, round 1 of 4The four comments below this one are responses to a review that was run Run against 1. The documented width criterion is not what the code delivers — BLOCKING
Untruncated
On the motivating dataset the prior's 97.5% point is 9.96 against a top dose of 2. The test for that property is passed by an unrepresentative design — BLOCKING
3. A new hard error is reachable through
|
Review record, round 2 of 4Run against Round-1 findings closed1 (width criterion). 2 (the test). Now runs the symmetric series plus 3 (the hurdle refusal). Reproduced as fixed. 4 (family invariance). Now true. Both the mu block's 5, 6, 7 closed. The 8 to 13 closed. All four misreported numbers corrected. The Numbers re-derived
New findingsR2-1 — "its median on the dose scale is that dose" is unqualified and false of
This is round-1 finding 1's failure mode one level down. R2-2 — the truncated prior's dose-scale density now falls monotonically across R2-3 — "it covered neither end on four of them and one end on the rest" is R2-4 — "0.93 to 1.03" understates the lower end. Measured 0.9247 on R2-5 — "0.47 to 0.95" is the zero-control half. 0.427–0.952 without a R2-6 — the confirmatory R2-7 — R2-8 — two of the three assertions in the new hurdle test would have passed R2-9 — the mass discarded above the highest concentration roughly doubled on Checks that found nothing
VerdictNothing in round 2 is blocking. R2-1 and R2-2 are worth acting on, both |
Review record, round 3 of 4Run against 1. The R2-2 argument is wrong in its premise, right in its conclusion — SHOULD-FIXClaimed. "at 1.03 the density decreases monotonically across the whole True. The shape is derived by solving The conclusion is correct on a different quantity — the median:
The shape-1.03 gamma puts its mass 24 times above the median dose, which is 2. The headline criterion is still unqualified in three of four places — SHOULD-FIX
3 to 6
VerdictThe code is sound and I have no remaining objection to it. One item outstanding, |
Review record, round 4 of 4Run against The replacement numbers are all correct. Shape 1.030020, rate 0.187628, mode The argument is sound as a whole, and tighter than before. Removing the No remaining unqualified untruncated-prior property. The other untruncated The NEWS reflow is clean. Two remaining, both one-sentence prose fixesR4-1 — "0.223, the median positive dose exactly" is not right, in all four R4-2 — the vignette insertion is in the wrong place. The paragraph ran: Round 5, at
|
The new nec prior proposes a low enough nec that make_good_inits() now finds initial values for nechormepwr and nechorme4pwr on a bernoulli identity fit of nec_data, where the gamma prior did not. Measured: the draws place nec at 0.076 and 0.183, and with nec confined at or above 1 the search still fails. ?models and the check_models() message both said no parameter value keeps the mean inside (0, 1) wherever the predictor reaches 1. That holds only where nec is also at or above 1, so that a concentration at or above 1 falls below the threshold. A nec below 1 puts every such concentration past it and the mean can be held inside. The exclusion is unchanged and its reason is unchanged in substance: nec is truncated to the predictor range, so every value at or above 1 is one the sampler may propose. The evidence test now asserts both halves: nothing is found with nec confined above 1, and what is found otherwise places nec below 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
CI failure, and what it exposed
The failure is real and it exposed an over-statement in What brokeThat test asserts that Every Why, and why the documented reason was wrongThe mean is
What changedThe exclusion is unchanged. The reason is corrected, in The evidence test now asserts both halves rather than the one that happened A NEWS entry records the change, and states that it is the recorded evidence Verification
|
The top + 1 argument is the sharpest demonstration and not the whole reason, and it says nothing about a predictor confined below 1, where check_models() excludes these models anyway. mu_support() already records what covers every case: the hormesis term has no coefficient the fit can drive towards zero, so the mean is not bounded above by 1 for any parameter values on any predictor. Measured on a predictor below 1, 2,899 of 3,591 grid points over top, slope, beta and nec put the mean above 1; on nec_data with nec below 1, 3,696 of 4,788 do. So bounding nec below 1 is not the fix the earlier wording invited. Two corrections with it. The condition on nec is strictly above 1, not at or above: at nec = 1 exactly no concentration satisfies 1 <= x < nec. And the search succeeds now because P(nec < 0.25) changed from 0.0063 to 0.269 and P(nec < 0.1) from 0.00011 to 0.0975, which is where the successful draws sit; P(nec < 1) moved only 0.481 to 0.670 and does not account for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
Review record, round 6 of 6Run against
1. The corrected reasoning is right in substance, with one imprecisionConfirmed. On The imprecision: the condition on 2. The exclusion should not change, and the new text under-justifies itThe wording framed the problem as the sampler being free to propose a
The reason that covers every case is already in the package: 3. The rewritten test asserts the property
4. The mechanism is attributed to the wrong probabilityNEWS and the roxygen said the lognormal proposes a low What else the new prior could have reachedGrepped the suite for the class that broke — assertions of a negative about VerdictThe behaviour is correct and the fix is right; what is left is documentation: |
Response to the sixth reviewAll three accepted and applied in the head of the branch. Documentation only; The exclusion was under-justified. The The strictness correction is right. At The probability attribution is corrected. On #177. Taken as settled, and I have not touched the exclusion. On the skipped hurdle blocks. Noted as the thing to watch. The three Verification: |
"the mean is not bounded above by 1 for any parameter values" reads as "for no parameter values is it bounded", which is false: a small top and a small nec do keep it inside. The statement that is both true and sharper is about the exponent. 1/(1 + exp(slope)) tends to 0 as slope grows, so x^(1/(1 + exp(slope))) tends to 1 for every concentration above 0, and the mean below the threshold tends to top + 1. For any top above 0 there is therefore a slope at which the mean exceeds 1, whatever range the predictor covers: at x = 0.001 that slope is 4.90 at top = 0.05, 2.19 at top = 0.5 and 0.27 at top = 0.95, against a normal(0, 5) prior on slope. The grid counts are kept as corroboration rather than as the argument. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
What #177 is, and what this PR did and did not change about itThe comments above refer to #177 without saying what it is. This makes that #177
The second was taken. What this PR changedNothing about the exclusion. The recorded reason, which was wrong.
The first sentence is true only under a further condition, and the second is This was invisible until now. The gamma prior on The reason that does holdThe exclusion is unconditional on the data, so a justification resting on "the
against a Corroborated by measurement, on a predictor confined below 1 where the Why this matters beyond tidinessThe old wording invited a fix that does not work. Read literally, it says the So: no new problem is solved and no behaviour changed. A documented |
The PR body has been updated:
|
| family, identity link | cells with the true bot outside the central 95% |
|---|---|
poisson, regularizing |
3 of 3 |
negbinomial, regularizing |
3 of 3 |
Gamma and gaussian, regularizing |
0 of 3 each |
every family, uninformative |
0 of 3 |
Truncated prior CDF at the true value 0.988 to 0.99999 in the failing cells.
And the separation between the two sets is not the same size in every family.
Prior standard deviation, regularizing divided by uninformative:
| family | top |
bot |
|---|---|---|
gaussian |
0.40 | 0.40 |
Gamma |
0.87 | 0.34 |
poisson |
0.83 | 0.10 |
negbinomial |
1.15 | 0.16 |
For negbinomial top, regularizing is wider than uninformative.
Relatedly, the beta branch reads nothing from the response — beta(5, 1)
against beta(5, 2) is a 13% change in width and no change in anchor — so
prior_type means something different in kind for a Beta response than for a
Gamma one.
Both are changes to released defaults for the response-scaled parameters, which
is a separable decision from the predictor-scaled one this PR makes, and neither
is #302. They belong in their own issue.
|
I (human) have reviewed the file changes and think these look ok. |
|
The two response-scaled findings recorded above as out of scope are now #305, with the measurements and the three options for fixing them. |
The script and its write-up are the only reproduction of the measurements cited in #302, PR #304 and #305, and they were living in an untracked working directory. The three CSVs it emits are left out; the script regenerates them. Both carry a provenance header. Section 1 of the write-up describes the nec/ec50 prior as it was at eebccdb and is now a record of the defect rather than of current behaviour; section 2 was fixed in the same PR; the top/bot findings in section 3 and part 2 are open as #305 and still hold. The hard-coded checkout path in the script is replaced by ".". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA
The roxygen and the vignette described the uninformative entries as the ones on CRAN. They are the ones Fisher et al. (2024) describe, and #305 does not touch them, but two earlier changes on dev mean they are not the released ones. #302 and PR #304 replaced the nec and ec50 entry: on a series of 0, 1, ... 10 CRAN gives gamma(5, 0.4) and dev gives lognormal(1.70060, 0.867668). #210 and #232 put positive_scale() in place of the raw quantile on the gamma branch of top and bot, which changes nothing on a response with no zeros and everything on one with many: on a poisson response with 22 zeros of 66, CRAN gives gamma(2, 100) for bot against gamma(2, 0.2849) here, the first being the collapse #210 exists to remove. Neither is a change #305 makes. Both are now stated where the claim was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ftghup233K2iaTrveZ3Q3
Replaces the eight lettered arms with the six named conventions of the redesigned study, which fits each with bnec() defaults and averages over the family's declining set rather than holding the equation at nec4param. Results come from open-AIMS/negative-response-conventions at 5103be3: 7 cells x 100 realisations x 6 conventions, 4,200 model-averaged fits. Drops the fixed-equation/averaging split and the dispersion sub-model section, which the new design has no counterpart for, and the bot prior sensitivity check, which assumed a shared prior the new design does not use. Adds the model-weight result: given the measurements, averaging recovers the generating equation at 0.88 of the weight, and under flooring a zero-asymptote shape takes 0.96. The case studies now run the same six conventions over the same candidate set. vignettes/example7.Rmd is NOT regenerated by this commit: the precompile is deferred until #309 settles which initial values are accepted, and until the branch takes the new default nec prior from dev (#304). Both change the case-study fits. The superseded negative-sgr bib entry is kept alongside the new one so the currently generated vignette still resolves its citation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBmDd2uv8yuhdu8RAUM2Bo
What
The default prior for
necandec50becomes one construction, a normal onthe log of the predictor, replacing the three entries that were selected by the
predictor's support. It is written as a
lognormalwhere the predictor issupplied on the concentration scale and as a
normalwhere the predictor spansnegative values and has therefore already been log transformed. The two are one
rule stated on two scales. Truncation to the observed predictor range is
unchanged.
The width is set by a criterion rather than by a constant: the central 95%
interval of the untruncated prior covers every concentration tested. It is the
larger of the two half-widths from the location to the ends of the logged
series, divided by
qnorm(0.975).Two defects found by the prior sweep behind this work are fixed on the way, both
independent of the issue: a
lognormalprior could not be sampled at all, andthe hurdle and zero-inflated families built their
topandbotpriors on theresponse scale under a non-identity link.
Closes #302. Unblocks #303.
Why it matters
A gamma's spread is tied to its shape, so
gamma(5, 4/m)places its central95% interval at
0.41mto2.56mwhatever the data are, with m the mediandistinct predictor value. The prior reaches the highest concentration tested
only where that concentration is within about 2.6 times the median, and that
ratio is a property of the experimental design: 2.0 for a series spaced evenly
from zero, and 13 to 125 for the four
nassariusseries. Logarithmic spacingis the usual ecotoxicological design. Where the prior does not reach, the
estimate is determined by the prior rather than by the data, and the sampler
reports no difficulty.
No shape serves both designs, and the shape that would is not usable. Solving
for the shape whose maximum density is at m and whose 97.5% point is the
highest dose gives 8.6 on a linear series and 1.03 on the
nassariuscontaminant A series. At 1.03 the mode is still at m, by construction, but
that is all that is: the density rises 5.7% from the lowest dose to the mode and
then falls to 2.8% of its peak at the highest, and the median of that prior is
3.85, twenty-four times m and above every dose tested but the top one.
Selecting the entry on the predictor's support was a second defect. Support is
a property of the units a concentration is recorded in, so the same experiment
received priors differing roughly 600-fold in width according to whether the
dose was recorded on a scale reaching above 1, on one confined to the unit
interval, or logged. The prior is now equivariant under a change of units.
Three consequences a reader should weigh before agreeing.
The already-logged entry is not left exactly as released. Its multiplier of
10 sd(x)is unchanged, but its location and spread are now read from thedistinct predictor values rather than from the observation vector, as on the
other branch, so that replication has no effect on the prior at all. Over the
pooled
log(herbicide$concentration)column, 580 rows and 9 distinct values,the location changes from 1.10 to 2.30 and the spread from 26.3 to 31.5. The two
differ under balanced replication as well. So the herbicide analyses of Fisher
et al. (2024) do change, in the width of this prior alone; the
nec_datawalkthrough changes in shape and location as well, because its predictor takes
the gamma entry.
Keeping that multiplier leaves an inconsistency. The same data analysed as
crf(x)and ascrf(log(x))still receive priors differing about tenfold inwidth, against about 600-fold before. Applying the coverage criterion to that
branch as well would make the two branches the same distribution stated on two
scales, and would remove the difference entirely. It was not done because
10 sd(x)is a published default that #302 rules settled, and changing itwidens the scope of this PR well beyond the gamma entry.
The width is set by the two extreme concentrations, not by the spread of the
series between them, so it responds to how a control is recorded. A control
entered as a nominal small positive value states that the value was applied, and
the prior widens to cover it: on the
nassariuscontaminant A series thestandard deviation is 2.30 with the control recorded as 0, 2.59 at 0.001 and
6.11 at 1e-6. Record a control as 0. Pinned by a test.
What is deliberately not in scope
prior_typeis untouched. Thenecandec50prior is a function of thepredictor alone, so it is identical under
"uninformative"and"regularizing", and a test now asserts that rather than leaving it assumed.Nothing in this PR changes either set.
The sweep that produced the evidence below did, however, find two defects in the
response-scaled priors, which are the ones
prior_typeselects between.Both are real, neither is #302's, and neither is touched here. They are recorded
so that the measurement is not lost.
The
regularizingbotprior is anchored on the sample minimum, which is abiased anchor for a count response. It is
gamma(5, 5 / (min(y) + min(y > 0) / 10)), whose mode is about 0.8 times thesmallest observation. For a continuous response that is defensible; for a count
the smallest observation sits well below the asymptote it is meant to locate.
Measured on responses simulated from a
nec4paramcurve with a truebotof 5,over three designs and four families, priors read through
get_priors()andnothing fitted:
botoutside the central 95%poisson,regularizingnegbinomial,regularizingGammaandgaussian,regularizinguninformativeThe truncated prior CDF at the true value runs 0.988 to 0.99999 in the failing
cells. The
uninformativeequivalent,gamma(2, 2/q25), fails in none.The two prior types are not separated by the same amount in every family.
Prior standard deviation,
regularizingdivided byuninformative, on the sameruns:
topbotgaussianGammapoissonnegbinomialFor
negbinomialtoptheregularizingprior is wider than theuninformativeone. Relatedly, thebetabranch of the response-scaled priorsreads nothing from the response at all —
beta(5, 1)againstbeta(5, 2)is a13% change in width and no change in what the prior is anchored to — so
prior_typemeans something different in kind for a Beta response than for aGamma one.
Neither is a released default ---
prior_typedoes not exist onmaster, so"regularizing"is entirely unreleased --- but both are decisions about theresponse-scaled parameters, which is separable from the predictor-scaled one
this PR makes. Raised as #305.
Evidence
Prior-only; nothing was fitted except the one confirmatory fit noted below.
Priors were obtained through
get_priors(), which runs the samecheck_data()and
define_prior()sequencebnec()runs. Five designs by three predictortransforms by two parameters, on
devateebccdb3, R 4.6.1, brms 2.23.0.The
nec/ec50prior is a function of the predictor alone, so those 30 cellsexhaust it; the invariance across family, link and prior type is asserted in a
test rather than assumed, and this PR restores it for the hurdle and
zero-inflated families, whose mu block previously built the prior from the
survivor subset of the predictor.
Cells in which the true value falls outside the central 95% of the truncated
prior, out of 30:
gamma/beta/normalby support)1.5 sd(log x)Every failing cell is a log-spaced series read on the recorded or the
square-root scale. On the
nassariuscontaminant A series, against a NEC of1.25 simulated from a
nec4paramcurve, the truncatedgamma(5, 25)priorplaces only 1.2 parts in 109 of its mass above the true value.
The sweep places every true value in the upper half of its series, so it cannot
detect a prior that fails at the bottom. That case was checked separately. On
the
nassariuscontaminant B series, whose low doses are sparse so that itsmedian sits above its log mid-range, a threshold at the lowest dose applied sits
at a truncated CDF of 0.030 under the adopted rule and 0.005 under a width taken
from half the range.
The criterion decided the width. A fixed multiple of
sd(log x)states nocriterion, so it cannot guarantee coverage on a design it was not chosen
against, and any multiple broad enough for a densely sampled continuous
predictor puts a large share of the prior below the lowest concentration tested
on a wide dilution series, where the lower truncation bound is a zero control:
k = 1.5k = 2nassariuscontaminant AExpressed as a multiple of
sd(log x), the adopted rule lands between 0.73 and1.18 across the five designs, at 0.92 to 1.03 on the four
nassariusseries andat 1.75 on
nec_data, so it is not equivalent to any one constant.Two properties of the prior follow from measuring density on the log scale, and
the documents state them rather than leaving them to be discovered. Its maximum
density is at the median dose on the log scale, and the median of the
untruncated prior on the dose scale is the median dose; truncation at the
highest dose removes part of the upper tail, so the truncated median is lower,
0.58 against a median dose of 0.88 on
nec_data. And its density on the dosescale falls monotonically across the whole tested range on the four
nassariusseries, because a lognormal's dose-scale mode is
exp(mu - sigma^2). That isnot the defect the shape-1.03 gamma has. Where the mass lies is what separates
them: on the contaminant A series the median of the untruncated prior adopted
here is 0.223, which is the location the rule specifies exactly — that series
has an even number of positive doses, so the location is the geometric mean of
the two central ones — against 3.85 for that gamma, and the truncated CDF at the
true value runs 0.43 to 0.95 across the sweep.
nec_datawas refitted under the new prior to confirm that Stan accepts atruncated lognormal on a parameter whose lower bound is a zero control:
nec3param, Beta on the identity link, 2 chains, 600 iterations, underlognormal(-0.133, 1.683), which is what the code at this head produces forthat predictor. R-hat 1.00 to 1.01, no divergences, NEC 1.54 [1.50, 1.57],
R2 0.96. The generated Stan code
normalises the truncation with
log_diff_exp(lognormal_lcdf(...))and drawsthe prior for
check_priors()by rejection fromlognormal_rng, both of whichare well defined at a lower bound of zero.
Implementation detail
The change
R/define_prior.R. Thex_prsvector and thex_typelookup are replaced bypredictor_prior(), a function of the predictor alone:returning
normal(mu, sigma)orlognormal(mu, sigma).define_prior()gains apredictor_scaleargument, defaulting topredictor,which is the vector the predictor-scaled prior is built from and truncated to.
It differs from
predictoronly for the two blocks of a hurdle orzero-inflated fit, each of which is primed from a subset of the predictor but
evaluated over the whole of it. This replaces the
rebound()closure that usedto correct the bounds after the fact, and corrects the prior with them.
set_distribution(predictor, ...)is retained but its value is no longer read.It is called for its refusal of an integer predictor, which
check_data()alsoraises and for the reason recorded there; this call is the backstop for the
routes that reach prior construction directly. A test now pins that refusal on
this route so a later change to
set_distribution()cannot remove it silently.Decisions taken, and what was rejected
The width is a criterion, not a constant.
sigmais the larger of the twohalf-widths, not half the range. Half the range gives the interval the right
width on the log scale and the wrong centre wherever the series is asymmetric
about its median on the log axis, and the interval then stops short of one end:
on the
nassariuscontaminant A series it runs 0.005 to 9.96 against a seriesrunning 0.01 to 20, which is the defect this issue exists to remove. The larger
half-width reaches both ends on every design measured, by construction.
Distinct values, not the observation vector, on both branches, extending
#269's rule from the gamma rate to the whole construction. Pinned by a test
comparing an evenly and an unevenly replicated copy of the same series. The
consequence for the already-logged branch is stated above the fold.
median()is applied after logging. For an odd count of distinct positivedoses this is the log of the median dose. For an even count it is the log of the
geometric mean of the two central doses — their midpoint on the log axis rather
than on the dose axis, which is the scale the series is spaced on.
The convention moves scale. Fisher et al. (2024) specify maximum density at
the median value of the predictor without saying which scale the density is
measured on. This reads it on the log-dose scale, and it is the only reading
under which a prior peaking at the median can also reach 125 times it. Recorded
in NEWS.
predictor_scaleis forwarded, not consumed.define_hurdle_prior()takesit too and passes it down, so a caller supplying both a hurdle family and a
non-default scale is not silently ignored. No caller does today.
Degenerate designs. Fewer than two distinct positive predictor values gives
no ends to reach and no spread to measure, so
sigmafalls back to 1 on the logscale. Such a design cannot identify a concentration-response curve, so the
fallback is chosen to be harmless rather than to be right. A predictor with no
positive values at all is refused with a named error;
check_data()fails firston such data, so this is a backstop rather than the message a user normally
sees.
The hurdle and zero-inflated path
Both blocks of such a fit are evaluated over the whole predictor range, and
their
necbounds were already rebuilt from it, but the mu block's prior wasshaped by the survivor subset alone. On a series reaching 100 whose survivors
stop at 10 that placed the prior's 97.5% point at 10.0 while its bounds
permitted 100 — the same failure this PR removes from the single-block path. The
predictor-scaled prior now comes from the whole predictor for both blocks. Only
the mu block changes in practice:
survival_by_x()returnssort(unique(predictor)), so the second block already had the whole predictor'sdistinct values.
The response-scaled
topandbotare still taken from the survivors, which iswhat #269 was about; a parameter measured in units of the predictor is not. The
change also removes a refusal this path would otherwise reach: where every
survivor sits at the zero control the mu subset has no positive value, so a
prior built from it could not be constructed at all. Both cases have tests.
The two prerequisite fixes
make_inits(),refine_inits()andsample_priors()each held their own tableof four distributions — gamma, normal, beta, uniform. A prior on any other name
reached
fcts[[dist]](...)asNULLand failed with "attempt to applynon-function", naming neither the prior nor the distribution. The three copies
are replaced by one lookup that includes
lognormal, and an unrecogniseddistribution now raises an error naming it and listing the supported set. The
parsed name is trimmed, so a prior string a user assembled by hand with
surrounding whitespace resolves rather than being reported as unknown.
hurdle_mu_family()returned an identity-link family whatever link the callersupplied, so
define_prior()measured the response untransformed whilebrmsapplies the inverse mean link to the whole non-linear expression, putting
topand
boton the link scale. Underzero_inflated_beta(link = "logit")thatgave
beta(5, 2)bounded to [0, 1] for atopwhose value on the logit scaleexceeds 1 for any response plateau above 0.73, and
beta(2, 5)for abotthatmust be negative for any response floor below 0.5 — a support that excludes the
answer rather than merely misplacing density. These fits now take the unbounded
normal entries, which is what the article specifies for any link mapping to the
whole real line.
link_huandlink_ziare required to be the identity, so thesecond block is unaffected. The group-level standard deviations of such a fit
change with it, because the mu-block response is put on the mean link scale
before their scale is measured from it; NEWS records this.
Documentation
NEWS.mdrecords the replacement, the2/mto4/mcorrection under #273 keptas history because it changed a released default, the hurdle predictor-prior
change, and both prerequisite fixes. It states which published analyses change
and how.
vignettes/example3.Rmd.origis revised: the predictor-scaled prior section isrewritten, and the later sentence naming the
gammapriorbnecchose fornec_datais corrected. The renderedvignettes/example3.Rmdis notregenerated, following the protocol that a precompile runs once at release; its
transcribed
pull_prior()output is stale for this and for #273 alike.Tests
tests/testthat/test-define_prior.R. The tests that pinned the gamma entry arerewritten to pin the property each was asserting rather than the string. New
assertions cover: the prior's median is the median dose tested, and what an even
count of doses does; the central 95% interval covers both ends on a series that
is asymmetric on the log axis in each direction, with the half-range alternative
asserted as failing for contrast; a low threshold on a wide series; the true
NEC of a log-spaced series now falls inside the prior, with the old gamma's
failure asserted alongside; equivariance under a change of units; the
already-logged branch;
ec50reading the same entry; replication having noeffect; a two-dose design; a control recorded as a small positive value; both
degenerate designs; both hurdle mu-block cases; and the integer-predictor
refusal.
tests/testthat/test-inits_functions.Rcovers a truncatedlognormaldrawnthrough
make_inits(), the named error on that route, and a prior string withsurrounding whitespace.
tests/testthat/test-sample_priors.Rcovers the sametwo on the
sample_priors()route.tests/testthat/test-hurdle_family.Rcoversthe mean link, and
test-define_prior.Rthe link-scale priors that result.What this does not do
amend()builds a default prior for each model it adds, so a set fitted acrossthis change is internally inconsistent in its priors. That was already recorded
in NEWS under the 2.2.0 prior entry and the wording is generalised here.
bnec_group()fits each level withbnec(), so levels now differ in the widthof the
necprior as well as its location, because the width depends on eachlevel's lowest and highest dose. Under the gamma only the location varied.
define_group_prior()'s predictor-scaled group scale stays on the dose scale,diff(range(predictor)) / 10, while the population-levelnecprior is nowbuilt on the log scale. That remains coherent —
necis still a dose-scaleparameter and its group deviations are still dose-scale — but the two halves are
derived on different scales and this PR does not change it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AticjTAk1JzCQ7UtPocYTA