Design a sensory screen before it runs: blocked, carry-over balanced serving plans - #481
Open
kgdunn wants to merge 5 commits into
Open
Design a sensory screen before it runs: blocked, carry-over balanced serving plans#481kgdunn wants to merge 5 commits into
kgdunn wants to merge 5 commits into
Conversation
Everything in `process_improve.sensory` so far analyses panel data that already exists. This adds the step before it: turning a long candidate list into a serving sheet a sensory lab can run. A panel screen is not an ordinary designed experiment, and the new module handles the three reasons why: - Session capacity. An assessor can only judge a handful of samples in one sitting, so once the candidate list is longer than a session the design is an incomplete block design with the assessor-session as the block. `cyclic_block_design` builds the blocks by a greedy fill followed by a hill-climb over two move types (substitution to level replication, exchange to level concurrence once replication has settled). Both moves are needed: with replication already level, every substitution makes it worse, so without the exchange the search stalls one step short of an exact balanced incomplete block design. - Carry-over and position. `williams_design` returns serving orders in which every treatment appears once per position and every ordered pair occurs equally often; an odd treatment count needs the mirrored second square. - Sensitivity. `detectable_difference` / `required_panelists` convert a residual SD and a panel size into the smallest resolvable difference and back, with a Bonferroni correction for the comparison family. `sensory_screening_plan` assembles the three into one sheet, optionally anchoring a reference first in every block. The sheet uses the `descriptive_long` column names, so adding scores feeds it straight into `compare_products` with `panelist_id` as the block. A panel too small to cover the candidate list is reported in `warnings` rather than silently dropping candidates. 36 tests, including the Fano plane as a known balanced incomplete block design and the ordered-pair balance of the Williams squares. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqdSBoXb1DpSobB76pMzFi
The "exactly one of n_per_product / difference" guard was written as a single equality on two `is None` tests, which mypy cannot use to narrow either field, so `difference: float | None` reached `required_panelists`. Written as two positive tests instead, each branch narrows at its own call site and the ambiguous case falls through to the same error response. Also restores a Field() call that the formatter had collapsed; it is not part of this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqdSBoXb1DpSobB76pMzFi
Sphinx builds with -W, so the one-character-short underline under "Before the panel: designing the screen" failed the docs job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqdSBoXb1DpSobB76pMzFi
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Two fixes for CI on this branch. 1. `test_registered_in_registry` asserted that dummies registered by two OTHER tests in the class were in the global registry. That only holds when all three run in the same process, and pytest-xdist distributes them across workers, so it was one scheduling change away from failing. Adding tools to the registry was that change. It now registers its own tool and asserts on that, so it is independently runnable. 2. Screening coverage 88% -> 99%. The uncovered lines were the guard clauses, and those are worth testing individually: the message is the deliverable, because a scientist who asks for an impossible design needs to know which number to change. Also covers the tool's ValueError path (an unreachable target difference comes back as ok=False rather than raising). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqdSBoXb1DpSobB76pMzFi
The Cochran & Cox and Naes et al. citations carried specific chapter numbers I could not verify. A precise-looking wrong pointer is worse than no pointer, so they are replaced with what each source actually contributes - and the Naes entry now says plainly that the block construction is NOT in it, which is the thing a reader would otherwise go looking for there and not find. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqdSBoXb1DpSobB76pMzFi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
process_improve.sensorycurrently analyses panel data that already exists (validate, panel check, relate,compare_products). This PR adds the step before that: turning a long candidate list into a serving sheet a sensory lab can actually run.New module
src/process_improve/sensory/screening.py. A panel screen is not an ordinary designed experiment, and the module handles the three reasons why:Session capacity. An assessor can only judge a handful of samples in one sitting before fatigue and carry-over dominate. Once the candidate list is longer than a session, every assessor sees only a subset, so the design is an incomplete block design with the assessor-session as the block.
cyclic_block_designbuilds the blocks so replication is equal (or as near-equal as the numbers allow) and every pair of candidates meets inside a block about equally often - the pairwise concurrence, which decides how precisely two candidates can be compared with each other.Carry-over and position.
williams_designreturns serving orders in which every treatment appears once per position and every ordered pair occurs equally often, so no candidate is systematically favoured by what was tasted before it. An even treatment count needs one square; an odd count needs a mirrored second one.Sensitivity.
detectable_differenceconverts a residual standard deviation and a panel size into the smallest difference the screen can resolve;required_panelistsinverts it. Both Bonferroni-correct for the size of the comparison family, because testing 20 candidates against one control is 20 comparisons.sensory_screening_planassembles all three into one sheet, optionally anchoring a reference sample first in every block so session and assessor drift can be removed later. The sheet uses thedescriptive_longcolumn names, so filling in the scores feeds it straight intocompare_productswithpanelist_idas the block - the design half and the analysis half now close the loop.Design note: why two move types in the block construction
The greedy fill lands close to balance but not on it. The hill-climb that follows uses two moves and needs both:
With replication already level, every substitution makes it worse, so a substitution-only search stalls one step short. With the exchange added, the construction recovers an exact balanced incomplete block design when one exists - verified against the Fano plane (t=7, k=3, b=7, r=3, lambda=1).
Behaviour worth knowing
warnings, not silently truncated. Quietly screening 18 of your 21 candidates is the failure mode worth being loud about.plan_diagnosticssays whether the blocks are an exact BIBD (balanced), plus replication and concurrence min/max/mean, control coverage and position balance, so the plan is contestable rather than taken on faith.Also included
sensory_screening_planandsensory_detectable_difference(@tool_spec, registered inget_sensory_tool_specs).docs/user_guide/sensory_panel.rst.CITATION.cffandCHANGELOG.mdkept in sync.Test plan
uv run pytest tests/test_sensory_screening.py --no-cov- 36 tests passcyclic_block_designrecovers the Fano plane; near-equal replication (max - min <= 1) for a (20, 6, 10) case with no exact BIBD; deterministic for a seeddetectable_differencemonotone in n and in the comparison count, linear in sd;required_panelistsverified to be the smallest n that reaches the target (n-1 does not)uv run ruff checkanduv run mypy src/process_improve/sensory/screening.pycleanNote:
uv.lockis deliberately not touched.Generated by Claude Code