Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ those changes.

## [Unreleased]

## [1.63.0] - 2026-08-03

### Added

- `process_improve.sensory.screening`: design a descriptive-panel screen before
any tasting happens. `sensory_screening_plan` turns a candidate list plus the
panel's session capacity into a serving sheet: incomplete blocks (one block =
one assessor-session) built by `cyclic_block_design` for near-equal
replication and pairwise concurrence, ordered inside each block by
`williams_design` so first-order carry-over cannot favour any candidate, and
with an optional reference anchored first in every block. The sheet comes back
in the `descriptive_long` shape, so adding scores feeds it straight into
`compare_products`.
- `detectable_difference` / `required_panelists`: the smallest difference a
panel size can resolve, and the panel size a target difference needs, with a
Bonferroni correction for the size of the comparison family.
- `plan_diagnostics` reports replication, pairwise concurrence and whether the
blocks form an exact balanced incomplete block design; a panel too small to
cover the candidate list is reported as a warning rather than silently
dropping candidates.
- Agent tools `sensory_screening_plan` and `sensory_detectable_difference`.

## [1.62.2] - 2026-07-29

### Changed
Expand Down Expand Up @@ -2787,7 +2809,8 @@ this entry records them together.
- Reworked the README with a sharper value proposition and a
"Why not scikit-learn?" comparison table.

[Unreleased]: https://github.com/kgdunn/process-improve/compare/v1.62.2...HEAD
[Unreleased]: https://github.com/kgdunn/process-improve/compare/v1.63.0...HEAD
[1.63.0]: https://github.com/kgdunn/process-improve/compare/v1.62.2...v1.63.0
[1.62.2]: https://github.com/kgdunn/process-improve/compare/v1.62.1...v1.62.2
[1.62.1]: https://github.com/kgdunn/process-improve/compare/v1.62.0...v1.62.1
[1.62.0]: https://github.com/kgdunn/process-improve/compare/v1.61.0...v1.62.0
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ authors:
repository-code: "https://github.com/kgdunn/process-improve"
url: "https://kgdunn.github.io/process-improve/"
license: MIT
version: 1.62.2
date-released: "2026-07-29"
version: 1.63.0
date-released: '2026-08-03'
keywords:
- chemometrics
- multivariate analysis
Expand Down
71 changes: 71 additions & 0 deletions docs/user_guide/sensory_panel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,73 @@ with correctly calibrated thresholds.
all-pairwise comparisons. *Journal of Computational and Graphical Statistics*,
13(2), 456-466.

Before the panel: designing the screen
--------------------------------------

Everything above analyses panel data that already exists. When you are at the
other end - a long list of candidate samples and no scores yet - the question is
which samples each assessor should taste, in which order, and whether the panel
is big enough to see the effect at all. :mod:`process_improve.sensory.screening`
covers that step.

A screen is not an ordinary designed experiment, for three reasons:

**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.
:func:`~process_improve.sensory.screening.cyclic_block_design` builds the blocks
so replication is equal, or as near-equal as the arithmetic allows, and so every
pair of candidates meets inside a block about equally often. That pairwise
**concurrence** is what decides how precisely two candidates can be compared with
each other; when an exact balanced incomplete block design exists for the given
numbers, the construction finds one, and ``diagnostics["balanced"]`` says so.

**Carry-over and position.** What was tasted just before changes this score, and
the first sample of a session is not scored like the last.
:func:`~process_improve.sensory.screening.williams_design` returns serving orders
in which every treatment appears once in each position and every *ordered* pair
occurs equally often, so no candidate is systematically favoured by its
neighbours. An even number of treatments needs one square; an odd number needs a
mirrored second one.

**Sensitivity.** Panel scores are noisy, so a screen that is too small returns
"no significant effect" whatever the truth is.
:func:`~process_improve.sensory.screening.detectable_difference` turns a residual
standard deviation and a panel size into the smallest difference the screen can
actually resolve, and
:func:`~process_improve.sensory.screening.required_panelists` inverts it. Correct
for the comparison family with ``n_comparisons``: testing twenty candidates
against one control is twenty comparisons, and pretending otherwise overstates
what the screen can see.

:func:`~process_improve.sensory.screening.sensory_screening_plan` assembles all
three into one serving sheet, optionally anchoring a reference sample first in
every block so session and assessor drift can be removed later:

.. code-block:: python

from process_improve.sensory import sensory_screening_plan

result = sensory_screening_plan(
[f"Candidate {i:02d}" for i in range(1, 22)],
n_panelists=12,
samples_per_session=6, # 5 candidates + the anchored reference
control="Reference",
replicates=2,
seed=0,
)
result.plan.head() # panelist_id, session, position, product, role, block
result.diagnostics # replication, concurrence, balanced, control_coverage
result.warnings # e.g. a panel too small to cover the list

The plan comes back in the same long shape the rest of this page consumes: fill
in the scores next to ``product`` and it feeds straight into
:func:`~process_improve.sensory.compare_products`, with ``panelist_id`` as the
block. A capacity shortfall is reported in ``warnings`` rather than silently
dropping candidates, because quietly screening eighteen of your twenty-one
candidates is the failure mode worth being loud about.

Worked example
--------------

Expand Down Expand Up @@ -541,6 +608,10 @@ and covariate tables as lists of row-records and returning JSON:
option (``"none"`` / ``"align"`` / ``"drop"``), the MAM results, and (unless
``discriminator`` is set false) the cross-validated discriminator in its
output.
- ``sensory_screening_plan`` - the blocked, carry-over balanced serving sheet
for a screen that has not been run yet.
- ``sensory_detectable_difference`` - what a given panel size can resolve, or
the panel size a given difference needs.

The analyze tool validates first and refuses to run if validation fails, so an
agent cannot skip the gate.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "process-improve"
version = "1.62.2"
version = "1.63.0"
description = 'Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis.'
readme = "README.md"
license = "MIT"
Expand Down
18 changes: 18 additions & 0 deletions src/process_improve/sensory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
from process_improve.sensory.mam import MAMResult, align_scores, mixed_assessor_model
from process_improve.sensory.panel import PanelScorecard, apply_correction, panel_scorecard
from process_improve.sensory.recipes import SENSORY_RECIPES
from process_improve.sensory.screening import (
SERVING_PLAN_COLUMNS,
ScreeningPlan,
cyclic_block_design,
detectable_difference,
plan_diagnostics,
required_panelists,
sensory_screening_plan,
williams_design,
)
from process_improve.sensory.validation import (
DESCRIPTIVE_LONG_COLUMNS,
ValidationResult,
Expand All @@ -45,26 +55,34 @@
__all__ = [
"DESCRIPTIVE_LONG_COLUMNS",
"SENSORY_RECIPES",
"SERVING_PLAN_COLUMNS",
"AnalysisResult",
"ComparisonResult",
"MAMResult",
"PanelScorecard",
"ScreeningPlan",
"ValidationResult",
"aggregate_to_product",
"align_scores",
"analyze_descriptive",
"apply_correction",
"compare_products",
"cyclic_block_design",
"detectable_difference",
"discriminate_observational",
"dunnett_vs_control",
"factorial_anova",
"mixed_assessor_model",
"panel_scorecard",
"permutation_column_null",
"plan_diagnostics",
"product_means",
"relate_designed",
"relate_observational",
"required_panelists",
"reshape_to_long",
"sensory_screening_plan",
"tukey_hsd",
"validate_descriptive",
"williams_design",
]
Loading