Experimental Strategy Recommendation#
Before running any experiments, the most important question is: “How should
I plan my experimental program?” The recommend_strategy function answers
this by generating a complete multi-stage experimental plan - screening,
optimization, and confirmation - using deterministic decision rules from
Montgomery, NIST, and the Stat-Ease SCOR framework.
The recommender is fully deterministic: identical inputs always produce identical outputs. There is no randomness and no LLM - just ~50 codified rules that encode best practices from the DOE literature.
When to Use This Tool#
Before your first experiment - plan the entire workflow upfront so that budget and time are spent efficiently.
When you have many candidate factors - the tool decides whether a screening stage is needed and which design to use.
When budget is limited - it allocates runs across stages to maximize information per experiment.
When working in a specialized domain - domain-specific templates (fermentation, cell culture, pharma, etc.) adjust design choices and center-point requirements automatically.
Concepts#
Multi-stage workflows#
Most experimental programs follow a three-stage sequence:
Screening - Identify the vital few factors from many candidates. Typical designs: Definitive Screening Design (DSD), Plackett-Burman, or fractional factorial.
Optimization - Fit a response surface model for the significant factors. Typical designs: face-centred Central Composite Design (CCD), Box-Behnken, or D-optimal.
Confirmation - Run replicates at the predicted optimum to verify that the model predictions hold.
Each stage has transition rules that tell you what to do next based on the results. For example, after screening:
0–1 significant factors found: broaden factor ranges or check the measurement system.
2–5 significant factors: proceed to optimization.
6+ significant factors: sub-group factors or run additional screening.
Curvature detected at center points: augment the factorial to a CCD.
Six or more continuous factors are screened with a DSD by default. Its main
effects are orthogonal to every two-factor interaction and square, whereas in a
12-run Plackett-Burman design each interaction biases the main effects of the
factors outside it by a third of its size, which can hide a real factor. The DSD
gets the centre runs that leave its analysis (analyze_omars) four degrees of
freedom for error; under a budget too tight for them, it keeps only its own.
Conditional stages#
An optimisation stage is often not needed. When the screening design can test
for curvature (a DSD, or a two-level design with centre runs), the optimisation
stage after it is conditional: stage["conditional"] is True and
stage["run_if"] names the evidence that calls for it and the analysis that
gives that evidence. Without that evidence, the screening model already locates
the best settings and the study goes straight to confirmation.
minimum_total_runs counts the runs when no conditional stage is called for,
and total_estimated_runs counts them all. A screening design that cannot
test for curvature, such as a Plackett-Burman design with a categorical factor,
is followed by an optimisation stage that is always run.
Budget allocation#
When a budget is specified, runs are allocated across stages using the 25-40-55-15 framework (Montgomery / Stat-Ease):
Screening: 25–40% of the budget
Optimization: 40–55% of the budget
Confirmation: 5–15% of the budget
Domain templates can shift these weights. For example, fermentation allocates more to optimization (50%) because biological variability demands extra center points for reliable error estimation.
Quick Start#
A 7-factor fermentation problem with a budget of 40 runs:
from process_improve.experiments.factor import Factor, Response
from process_improve.experiments.strategy import recommend_strategy
factors = [
Factor(name="Temperature", low=25, high=40, units="degC"),
Factor(name="pH", low=5.0, high=7.5),
Factor(name="Glucose", low=10, high=50, units="g/L"),
Factor(name="Yeast extract", low=1, high=10, units="g/L"),
Factor(name="Agitation", low=100, high=400, units="rpm"),
Factor(name="Aeration", low=0.5, high=2.0, units="vvm"),
Factor(name="Inoculum", low=2, high=10, units="%v/v"),
]
responses = [Response(name="Yield", goal="maximize", units="g/L")]
strategy = recommend_strategy(
factors=factors,
responses=responses,
budget=40,
domain="fermentation",
)
for stage in strategy["stages"]:
print(f"Stage {stage['stage_number']}: {stage['stage_name']}")
print(f" Design: {stage['design_type']}, Runs: {stage['estimated_runs']}")
print(f" Purpose: {stage['purpose']}")
This outputs:
Stage 1: Screening
Design: plackett_burman, Runs: 11
Purpose: Screen 7 candidate factors to identify the vital few.
Stage 2: Optimization
Design: ccd, Runs: 19
Purpose: Fit quadratic response surface model for the 3 significant factors. ...
Stage 3: Confirmation
Design: replicates_at_optimum, Runs: 3
Purpose: Run replicates at the predicted optimum to verify the model predictions. ...
The engine selected Plackett-Burman screening (the fermentation domain default) with 3 centre runs, a face-centred CCD for response surface optimization, and 3 confirmation replicates - all within the 40-run budget. The CCD is conditional on the centre runs showing curvature, so the study needs 14 runs when they do not and 33 when they do.
Interpreting the Output#
recommend_strategy returns a dictionary with these keys:
Key |
Description |
|---|---|
|
Ordered list of experimental stages. Each stage contains
|
|
Sum of estimated runs across all stages, the conditional ones included. |
|
Sum of estimated runs across the stages that are not conditional. |
|
Dictionary mapping stage names to allocated run counts. |
|
Step-by-step explanation of the decision logic. |
|
Key assumptions underlying the recommendation (e.g. factor ranges are wide enough, measurement system is adequate). |
|
Potential issues and warnings (e.g. tight budget, split-plot requirements). |
|
Brief descriptions of other approaches worth considering. |
|
Deterministic hash of the input - same inputs always produce the same ID. |
|
The application domain used. |
|
|
To inspect transition rules after screening:
for rule in strategy["stages"][0]["transition_rules"]:
print(f"If {rule['condition']}:")
print(f" -> {rule['action']}")
print(f" Otherwise -> {rule['fallback']}")
Working with Budget Constraints#
The budget parameter controls how many total runs are available. The engine adjusts stage complexity accordingly:
for b in [60, 25, 20, None]:
result = recommend_strategy(
factors=factors, responses=responses, budget=b, domain="fermentation"
)
designs = [stage["design_type"] for stage in result["stages"]]
print(f"Budget={str(b):>4s}: {result['total_estimated_runs']:>2d} runs, {designs}")
Budget= 60: 33 runs, ['plackett_burman', 'ccd', 'replicates_at_optimum']
Budget= 25: 23 runs, ['dsd', 'replicates_at_optimum']
Budget= 20: 20 runs, ['dsd', 'replicates_at_optimum']
Budget=None: 33 runs, ['plackett_burman', 'ccd', 'replicates_at_optimum']
With a tight budget, the engine changes the designs, not only their run
counts: it drops center points, replaces a stage’s design with a smaller one
(a Plackett-Burman screen, a D-optimal quadratic design), and plans a single
Definitive Screening Design when separate screening and optimization stages
cannot fit. It plans that DSD too, for six or more factors, when the reduction
would take the centre runs out of the screening design: the optimisation stage
would then have to be run whatever the screening showed, and one DSD both
screens and tests for curvature. The budget must hold the conditional stages
too, since the screening may call for them. estimated_runs is always the size of the design the
stage’s design_params build, and result["risks"] says what was
reduced, or that the budget is below the smallest plan. When budget=None,
the ideal designs are used without constraint.
Using Prior Knowledge#
If you already know something about which factors matter, pass a free-text
description via the prior_knowledge parameter. The engine parses
keywords to set a confidence level:
High confidence (0.9): “confirmed”, “validated”, “published”, “well-established”
Medium confidence (0.7): “literature suggests”, “preliminary data”, “pilot study”
Low confidence (0.4): “suspect”, “expected”, “based on theory”
No knowledge (0.1): “no prior data”, “first time”, “exploratory”
High confidence (>= 0.8 with supporting data) skips the screening stage entirely:
# No prior knowledge - full screening
s1 = recommend_strategy(factors=factors, budget=40, domain="fermentation")
print(f"No prior: {len(s1['stages'])} stages")
# Low confidence - still screens
s2 = recommend_strategy(
factors=factors,
budget=40,
domain="fermentation",
prior_knowledge="We suspect Temperature and pH are important.",
)
print(f"Low confidence: {len(s2['stages'])} stages")
# High confidence - screening skipped
s3 = recommend_strategy(
factors=factors,
budget=40,
domain="fermentation",
prior_knowledge=(
"Published and validated results confirm Temperature " "and pH are significant."
),
)
print(f"High confidence: {len(s3['stages'])} stages")
No prior: 2 stages
Low confidence: 2 stages
High confidence: 1 stages
(With no response to optimise, seven factors get no optimisation stage.)
Domain-Specific Strategies#
The domain parameter selects a domain template that adjusts screening
design preferences, RSM design choices, center-point counts, and budget
weights. Eight domains are available:
Domain |
Screening / RSM preference |
Notes |
|---|---|---|
|
Plackett-Burman / CCD |
Extra center points (5+) for biological variability. |
|
DSD / Box-Behnken |
Minimizes runs for expensive, slow experiments (14–21 days). |
|
DSD / Face-centered CCD |
ICH QbD framework; design space definition for regulatory submissions. |
|
Fractional factorial / BBD |
Mixture handling; avoids extreme factor combinations. |
|
Fractional factorial / CCD |
Strongly quadratic responses; a rotatable CCD where the ranges are not hard limits. |
|
Fractional factorial / CCD |
AQbD / ICH Q2/Q14; includes robustness study stage. |
|
Plackett-Burman / CCD |
Scale-up considerations for bench-to-production transfer. |
|
Rule-engine defaults |
No domain-specific adjustments. |
Comparing two domains on the same factors shows how design choices differ:
for domain in ["fermentation", "cell_culture"]:
result = recommend_strategy(factors=factors, budget=40, domain=domain)
screening = result["stages"][0]
print(
f"{domain:>15s}: {screening['design_type']}, "
f"{screening['estimated_runs']} screening runs"
)
fermentation: plackett_burman, 11 screening runs
cell_culture: dsd, 20 screening runs
Fermentation uses Plackett-Burman (efficient, many-factor screening, here with 3 centre runs), while cell culture uses a Definitive Screening Design because it combines screening and curvature detection in a single stage - saving an entire experimental cycle when each run takes 2–3 weeks.
Hard-to-Change Factors#
When some factors are expensive or time-consuming to reset between runs
(e.g. reactor temperature, equipment configuration), flag them with
hard_to_change_factors. generate_design builds split-plot structure in
the optimal designs, so each stage that contains a hard-to-change factor becomes
a D-optimal split-plot design of the same size, with hard_to_change naming
its whole-plot factors:
result = recommend_strategy(
factors=factors,
responses=responses,
budget=40,
domain="fermentation",
hard_to_change_factors=["Temperature"],
)
for stage in result["stages"]:
params = stage["design_params"]
if "hard_to_change" in params:
print(f"{stage['stage_name']}: {stage['design_type']}, {params['model_type']} model")
print(f" Whole-plot (hard to change): {params['hard_to_change']}")
Screening: d_optimal, main_effects model
Whole-plot (hard to change): ['Temperature']
Optimization: d_optimal, quadratic model
Whole-plot (hard to change): ['Temperature']
With split-plot designs, runs are grouped within whole-plot factor levels
to minimize the number of hard-to-change factor resets. The design records each
run’s whole plot in a WholePlot column. The output risks include a reminder
that standard ANOVA gives incorrect p-values for split-plot experiments: runs in
one whole plot share its error, so ordinary least squares makes the
hard-to-change factors look more significant than they are. Analyse the results
with analysis_type="split_plot", a restricted maximum likelihood (REML) fit
that tests each factor against the right error; see Split-plot experiments.
Multiple Responses#
When optimizing for more than one response, define each with its own goal:
responses = [
Response(name="Yield", goal="maximize", units="g/L"),
Response(name="Purity", goal="maximize", units="%"),
Response(name="Cost", goal="minimize", units="USD/kg"),
]
result = recommend_strategy(
factors=factors,
responses=responses,
budget=40,
domain="fermentation",
)
The strategy structure is the same - the engine plans the experimental
stages needed to build models for all responses simultaneously. After
running the experiments, fit each response with
analyze_experiment() and
coding="coded", and pass the results to
optimize_responses() with desirability
functions to find the best trade-off across responses.
The optimiser evaluates the models at coded settings, from -1 to +1, so it
needs coefficients on that scale. Fitted in actual units (degrees, minutes,
bar), a model’s coefficients would be searched over -1 to +1 of those units,
so the optimiser refuses them and says how to refit. Coded coefficients
carry the ranges they were coded from, so the optimum comes back in actual
units as well. When the data’s extremes are not the levels that should be
-1 and +1, as with a central composite design’s axial runs, give the ranges
instead: coding={"T": {"low": 150, "high": 200}, ...}.
See Also#
Designed Experiments - Full API reference for all DOE functions.
generate_design()- Generate the actual design matrix once you know which design to use.analyze_experiment()- Analyze the results after running experiments.optimize_responses()- Find optimal factor settings for single or multiple responses.