Designed Experiments#

class process_improve.experiments.structures.Column(data=None, index=None, dtype=None, name=None, copy=None, fastpath=<no_default>)[source]#

Bases: Series

Create a column. Can be used as a factor, or a response vector.

Parameters:
  • dtype (Dtype | None)

  • copy (bool | None)

  • fastpath (bool | lib.NoDefault)

pi_index: bool#
pi_numeric: bool#
pi_lo: float | None#
pi_hi: float | None#
pi_range: tuple | None#
pi_center: float | None#
pi_is_coded: bool#
pi_units: str | None#
pi_name: str | None#
pi_levels: dict#
to_coded(center=None, range=None)[source]#

Convert the column vector to coded units.

Parameters:
Return type:

Column

to_realworld(center=None, range=None)[source]#

Convert the column vector to real-world units.

Parameters:
Return type:

Column

copy(deep=True)[source]#

Create a copy of this Column, preserving the name.

Parameters:

deep (bool)

Return type:

Column

extend(values)[source]#

Extend the column with the list of new values.

Parameters:

values (list)

Return type:

Column

class process_improve.experiments.structures.Expt(data=None, index=None, columns=None, dtype=None, copy=None)[source]#

Bases: DataFrame

Dataframe carrying experimental data plus process-improve metadata.

Expt (short for “Experiment”) is a pandas.DataFrame subclass that adds library-managed metadata fields prefixed with pi_ – short for “process-improve”. The prefix is what keeps these reserved attribute names from colliding with column names from a caller-supplied DataFrame.

Pinned metadata (preserved across subsetting via _metadata):

  • pi_title – short human-readable name for the dataset

  • pi_source – provenance string (file path, URL, …)

  • pi_units – units string for the numeric columns

Other pi_* attributes (pi_range, pi_lo, pi_hi, pi_center, pi_name) are set by the experiments factory helpers in this module; see expt() / create_names().

The pi_ prefix is documented in CONTRIBUTING.md and is part of the package’s public API surface; new metadata fields should follow the same prefix.

Parameters:
  • index (Axes | None)

  • columns (Axes | None)

  • dtype (Dtype | None)

  • copy (bool | None)

pi_source: dict | None#
pi_title: str | None#
pi_units: dict | None#
get_title()[source]#

Return the experiment title, or empty string if not set.

Return type:

str

process_improve.experiments.structures.create_names(n, letters=True, prefix='X', start_at=1, padded=True)[source]#

Return default factor names, for a given number of n [integer] factors. The factor name “I” is never used.

If letters is True (default), then at most 25 factors can be returned.

If letters is False, then the prefix is used to construct names which are the combination of the prefix and numbers, starting at start_at.

Example:
>>> create_names(5)
    ["A", "B", "C", "D", "E"]
>>> create_names(3, letters=False)
    ["X1", "X2", "X3"]
>>> create_names(3, letters=False, prefix='Q', start_at=9,
                     padded=True)
    ["Q09", "Q10", "Q11"]
Parameters:
Return type:

list[str]

process_improve.experiments.structures.c(*args, **kwargs)[source]#

Perform the equivalent of the R function “c(…)”, to combine data elements into a Column. Numeric entries are converted to floating point; entries are left as-is for categorical columns (when levels=... is passed, or when the entries cannot be coerced to float).

Inputs#

index: a list of names for the entries in args

name: a name for the column

Usage#

# All equivalent ways of creating a factor, “A”

A = c(-1, 0, +1, -1, +1)

A = c(-1, 0, +1, -1, +1, index=[‘lo’, ‘cp’, ‘hi’, ‘lo’, ‘hi’]) A = c( 4, 5, 6, 4, 6, range=(4, 6)) A = c( 4, 5, 6, 4, 6, center=5, range=(4, 6)) # more explicit A = c( 4, 5, 6, 4, 6, lo=4, hi=6) A = c( 4, 5, 6, 4, 6, lo=4, hi=6, name = ‘A’) A = c([4, 5, 6, 4, 6], lo=4, hi=6, name = ‘A’) A = c([4, 5, 6, 4, 6], lo=4, hi=6, name = ‘A’)

# By default, the assumption is the variable levels supplied are coded # units. But if any one of the following: lo, hi, center, range OR # units are specified, then immediately it is assumed that the variable # values are not coded. # So, to force the specification, you may supply the optional input of # coded as True or False A = c([4, 5, 6, 4, 6], lo=1, hi=3, coded=True) A = c([4, 5, 6, 4, 6], lo=1, hi=3, coded=False, units=”g/mL”)

# Categorical variables B = c(0, 1, 0, 1, 0, 2, levels =(0, 1, 2)) M = c(“Dry”, “Wet”, “Dry”, “Wet”, levels = (“Dry”, “Wet”))

Return type:

Column

process_improve.experiments.structures.expand_grid(**kwargs)[source]#

Create the expanded grid here.

Parameters:

kwargs (Column)

Return type:

list[Column]

process_improve.experiments.structures.supplement(x, **kwargs)[source]#

Supplement an existing column with additional metadata (name, units, lo, hi, etc.).

Parameters:
Return type:

Column

process_improve.experiments.structures.gather(*args, title=None, **kwargs)[source]#

Gathers the named inputs together as columns for a data frame.

Removes any rows that have ANY missing values. If even 1 value in a row is missing, then that row is removed.

Usage#

expt = gather(A=A, B=B, y=y, title=’My experiment in factors A and B’)

A multi-column input (a pandas.DataFrame, e.g. a categorical factor expanded into several indicator columns) is gathered column by column.

Positional arguments are accepted for columns that already carry a name (pi_name or the pandas .name): gather(A, B, y=y). A nameless positional argument raises, since its column label cannot be inferred. (Earlier versions accepted positional arguments and silently discarded them.)

Parameters:
Return type:

Expt

Unified design generation: generate_design() dispatcher.

This module provides a single entry point for creating any standard experimental design. It dispatches to specialised modules based on design_type and applies common post-processing (center points, replication, randomization, coded/actual mapping).

Examples

>>> from process_improve.experiments import generate_design, Factor
>>> factors = [
...     Factor(name="Temperature", low=150, high=200, units="degC"),
...     Factor(name="Pressure", low=1, high=5, units="bar"),
... ]
>>> result = generate_design(factors, design_type="full_factorial")
>>> result.n_runs
7
>>> result.design
process_improve.experiments.designs.generate_design(factors, design_type=None, budget=None, n_center_points=3, n_replicates=1, n_blocks=None, resolution=None, generators=None, alpha=None, cube='full', constraints=None, hard_to_change=None, model_type='interactions', fixed_runs=None, random_seed=42)[source]#

Generate an experimental design matrix.

Parameters:
  • factors (list[Factor]) – Factor specifications. Each Factor has a name, type ("continuous", "categorical", "mixture"), low/high bounds (for continuous), levels (for categorical), and optional units.

  • design_type (str or None) – One of "full_factorial", "fractional_factorial", "plackett_burman", "box_behnken", "ccd", "dsd", "omars", "omars_ilp", "d_optimal", "i_optimal", "a_optimal", "mixture", "taguchi". If None, the design type is chosen automatically based on the factor count, budget, and constraints.

  • budget (int or None) – Maximum number of runs the experimenter can afford.

  • n_center_points (int) – Number of center-point replicates (default 3). For designs that embed their own center points (CCD, Box-Behnken), this parameter controls the count within the design structure.

  • n_replicates (int) – Number of full replicates of the design (default 1 = no replication).

  • n_blocks (int or None) – Number of blocks.

  • resolution (int or None) – Desired minimum resolution for fractional factorials (III=3, IV=4, V=5).

  • generators (list[str] or None) – Explicit generators for fractional factorials, e.g. ["D=ABC", "E=AC"].

  • alpha (str, float, or None) –

    Axial distance for CCD designs: "rotatable", "face_centered", "orthogonal", or a numeric value.

    Note

    A numeric alpha is only honored when cube="fractional". For cube="full" (the default) the underlying pyDOE3 ccdesign call does not accept an arbitrary axial distance, so a numeric value is silently treated as "orthogonal".

  • cube (str) – For CCD designs, how to build the cube (factorial) portion: "full" (default) uses the complete 2^k factorial; "fractional" uses a resolution-V (or higher) fractional factorial, keeping the run count practical for k >= 5. When "fractional" and generators is given, those generators define the cube; otherwise a minimum-aberration half-fraction is chosen automatically.

  • constraints (list[Constraint] or None) – Constraints on the factor space.

  • hard_to_change (list[str] or None) – Names of hard-to-change factors (triggers split-plot structure).

  • model_type (str) – Model the optimal designs ("d_optimal", "i_optimal", "a_optimal") are built for: "main_effects", "interactions" (default), or "quadratic". With a categorical factor present, "quadratic" builds a partial response-surface model (quadratics on the continuous factors only; the categorical enters as a main effect plus its interactions), since a categorical factor has no square. Ignored by the classical (non-optimal) design families.

  • fixed_runs (pandas.DataFrame or None) – Runs to hold fixed while the optimizer fills the rest (design augmentation), for the optimal families only ("d_optimal", "i_optimal", "a_optimal", which use pyoptex). One row per fixed run, one column per factor, in the same coding as the returned design: continuous factors in coded [-1, 1] units, categorical factors as level labels. The fixed runs occupy the first rows of the result and budget counts them, so budget must exceed len(fixed_runs). A common use is to seed a centre point. Raises ValueError if given for a non-optimal design_type.

  • random_seed (int) – Seed for reproducible randomization (default 42).

Returns:

Contains design (coded Expt), design_actual (actual-units Expt), run_order, and design metadata (generators, defining relation, resolution, etc.).

Return type:

DesignResult

Raises:

ValueError – If design_type is unknown, or if factor/budget constraints cannot be satisfied.

Examples

>>> from process_improve.experiments import generate_design, Factor
>>> factors = [
...     Factor(name="T", low=150, high=200, units="degC"),
...     Factor(name="P", low=1, high=5, units="bar"),
... ]
>>> result = generate_design(factors, design_type="full_factorial")
>>> result.design_actual

Integer-programming generator for OMARS designs.

The constructive generator in process_improve.experiments.designs_omars (dispatch_omars) only builds the minimal conference-foldover member of the OMARS family (2k + 1 / 2k + 3 runs). That design is saturated for a full second-order model, so process_improve.experiments.analyze_omars() has no error degrees of freedom to work with. This module builds larger OMARS designs that leave error degrees of freedom, by selecting runs with an integer linear program (ILP).

Method#

Every design here is a foldover [H; -H; 0]: a half-design H, its mirror image -H, and a single centre run. The foldover structure makes three of the four OMARS-defining conditions hold automatically:

  • balance - h and -h cancel, so every main-effect column sums to zero;

  • main effects clear of the two-factor interactions - x_i x_a x_b is an odd function, so its contributions from h and -h cancel;

  • main effects clear of the pure quadratics - x_i x_j^2 is odd in x_i, so those contributions cancel too;

and the centre run makes every pure quadratic estimable (each x_i^2 column takes the value 0 there). The only condition that is not automatic is the mutual orthogonality of the main effects, which is linear in the binary “include this half-run” variables s_r: for each pair i < j, sum_r (x[r,i] x[r,j]) s_r = 0. The run count is 2 * sum_r s_r + 1.

So the ILP selects a half-design from the (3**k - 1) / 2 distinct non-mirror three-level runs subject to a handful of linear equalities - only k(k-1)/2 of them - which keeps it tractable up to seven factors. Because the coefficients are integers, the equalities are exact; the floating-point is_omars() re-check only guards against mistakes. A pure feasibility solve, however, returns an arbitrary OMARS design that is usually far from the most efficient member. To search for a high-quality design the solve is repeated with random linear objectives (a multistart): each random objective sends the solver to a different vertex of the feasibility polytope, so the retained designs span the high-D-efficiency / low-A members. The best is then chosen by a satisficing-and-dominance rule over D-efficiency and the maximum second-order correlation, following the selection philosophy of Nunez Ares and Goos (2020). This makes the generator competitive with their enumerated catalogue without consulting it.

This realises, for OMARS designs, the integer-programming construction of Nunez Ares and Goos (2020); the ILP-over-design-points framing is shared with their trend-robust run-order work (Nunez Ares and Goos, 2019). An exhaustively enumerated OMARS catalogue exists but is unlicensed and is not redistributed here. Only the (dominant) foldover OMARS family is generated; the rarer non-foldover members are a documented future extension.

References

  • Nunez Ares, J. and Goos, P. (2020). “Enumeration and multicriteria selection of orthogonal minimally aliased response surface designs.” Technometrics, 62(1):21-36.

  • Nunez Ares, J. and Goos, P. (2019). “An integer linear programming approach to find trend-robust run orders of experimental designs.” Journal of Quality Technology.

class process_improve.experiments.designs_omars_ilp.OmarsSearchReport(n_factors=0, half_pool_size=0, n_restarts=0, ilp_iterations=0, feasible_designs=0, run_size=0, total_solve_seconds=0.0, search_mode='', enumerated_designs=0)[source]#

Bases: object

Diagnostics from the ILP search, recorded on DesignResult.metadata.

Parameters:
  • n_factors (int)

  • half_pool_size (int)

  • n_restarts (int)

  • ilp_iterations (int)

  • feasible_designs (int)

  • run_size (int)

  • total_solve_seconds (float)

  • search_mode (str)

  • enumerated_designs (int)

n_factors#

Number of factors.

Type:

int

half_pool_size#

Number of distinct non-mirror three-level runs the ILP chose from.

Type:

int

n_restarts#

Number of randomized-objective ILP solves the multistart was allowed (the actual count can be lower when early-stopping ends it).

Type:

int

ilp_iterations#

Number of ILP solves (the outer search iterations): the minimize-size probe, the baseline feasibility solve, and every randomized-objective restart.

Type:

int

feasible_designs#

Number of distinct verified OMARS designs found and ranked.

Type:

int

run_size#

Run count of the winning design.

Type:

int

total_solve_seconds#

Cumulative wall-clock time spent inside the ILP solver.

Type:

float

search_mode#

"exhaustive" when the feasible design class was enumerated in full (the selection is then exact), "multistart" when the randomized ILP multistart was used (the selection is then the best design found, which may miss the optimum).

Type:

str

enumerated_designs#

Number of feasible designs enumerated on the exhaustive path (0 on the multistart path).

Type:

int

process_improve.experiments.designs_omars_ilp.solve_omars_ilp(half_pool, *, n_half=None, half_bounds=None, minimize_size=False, objective=None, exclude_solutions=None, solver_options=None)[source]#

Select a half-design from half_pool and return the foldover OMARS design.

Exactly one of n_half (exact half count) or half_bounds (inclusive (min, max) half count) sets the size constraint. The returned design has 2 * n_half + 1 runs.

Parameters:
  • half_pool (np.ndarray) – Candidate half-runs of shape (n_candidates, n_factors), coded to {-1, 0, +1} (see _half_pool()).

  • n_half (int, optional) – Exact number of half-runs to select.

  • half_bounds (tuple[int, int], optional) – Inclusive (min, max) half-run count.

  • minimize_size (bool, optional) – When True the objective minimises the half-run count (smallest feasible design); otherwise the solve is a pure feasibility search.

  • objective (np.ndarray, optional) – Per-candidate linear cost of shape (n_candidates,). When given, the solver minimises sum_r objective[r] * s_r instead of running a pure feasibility (or minimise-size) search. A random objective drives the solver to a different vertex of the OMARS-feasibility polytope, which is how generate_omars() samples diverse, high-quality designs. Takes precedence over minimize_size.

  • exclude_solutions (list[list[int]], optional) – Previously found half-index sets to forbid via no-good cuts.

  • solver_options (dict, optional) – {"msg": bool, "time_limit": int seconds}.

Returns:

(design or None, solver_status, chosen_half_indices). None means the solver returned no feasible selection.

Return type:

tuple

Raises:

ImportError – If PuLP (the ilp extra) is not installed.

process_improve.experiments.designs_omars_ilp.generate_omars(factors, *, n_runs=None, n_runs_range=None, selection_criterion='dominance', satisfice=None, center_runs=1, n_restarts=50, max_candidates=6, model='full_second_order', solver_options=None, tol=1e-09, random_seed=42, verify=True)[source]#

Generate a foldover OMARS design by exhaustive or integer-programming run selection.

Builds a three-level OMARS design large enough to leave error degrees of freedom for the chosen analysis model, so it can be analysed with process_improve.experiments.analyze_omars(). The design is a foldover [H; -H; 0] (half-runs, their mirrors, and a centre run) plus any further centre runs, for 2*h + center_runs runs in total. Regardless of model, the design is a genuine OMARS design: the main effects stay orthogonal to every second-order term (quadratics and interactions alike).

Parameters:
  • factors (list[Factor]) – At least three continuous factors.

  • n_runs (int, optional) – Exact total run size of the returned design, centre runs included. n_runs - center_runs must be a positive even number (the half-runs and their mirrors), and n_runs must exceed the number of parameters in the chosen model (1 + 2k + k(k-1)/2 for "full_second_order", 1 + 2k for "main_quadratic"). If None a size is chosen automatically.

  • n_runs_range (tuple[int, int], optional) – Inclusive (min, max) total-run-size window to search when n_runs is None; the smallest feasible size is used.

  • selection_criterion ({"dominance", "d_efficiency", "min_second_order_correlation", "a_optimal"}) – How to choose among the feasible designs. When the design class at the chosen size is small enough (currently up to four factors at moderate sizes), the search enumerates it exhaustively and the selection is exact for the stated objective; the metadata reports search_mode="exhaustive". Otherwise the criterion selects the best design among those found by the randomized multistart (search_mode="multistart"), which may miss the optimum. "dominance" (default) keeps the Pareto front on D-efficiency and the maximum second-order correlation, then prefers the smallest, most efficient design. "a_optimal" selects the design with the lowest summed coefficient variance trace((X'X)^-1) of the sizing model (lower prediction variance on average), which is the natural choice when the design is judged on precision rather than on aliasing. A design containing a constant second-order column (a term the design cannot estimate) scores inf on the correlation metric, so it is never selected by "min_second_order_correlation" when an alternative with every term present exists.

  • satisfice (dict, optional) – Acceptability thresholds applied before selection: a design is kept only if it clears every threshold. Supported keys are "d_efficiency" (a minimum, higher is better) and "max_second_order_correlation" (a maximum, lower is better), for example {"d_efficiency": 5.0, "max_second_order_correlation": 0.7}. A ValueError is raised if no enumerated design meets the thresholds.

  • center_runs (int, optional) – Number of centre runs in the design (at least one; the foldover already contributes one). Centre runs count towards n_runs: asking for n_runs=17, center_runs=3 returns 17 rows, 3 of them centre runs. Default 1.

  • n_restarts (int, optional) – Number of randomized-objective ILP solves used to search for a high-quality design. Each restart drives the solver to a different feasible OMARS design; the best one (by selection_criterion) is kept. Higher values explore more of the feasible set and approach the catalogue-optimal designs more closely, at a roughly linear cost in runtime. The search early-stops once the feasible set stops yielding new designs, so small factor counts finish quickly regardless. Default 50, which reaches catalogue-competitive D-efficiency for up to seven factors. Deterministic for a fixed random_seed.

  • max_candidates (int, optional) – Legacy alias retained for backward compatibility. It now sets a floor on n_restarts (the effective restart budget is max(n_restarts, max_candidates)), so calls that raised it to enumerate more designs still explore at least that many. Default 6.

  • model ({"full_second_order", "main_quadratic"}, optional) – The analysis model the design is sized for. "full_second_order" (default) leaves room for every two-factor interaction, so the smallest feasible design must exceed 1 + 2k + k(k-1)/2 runs. "main_quadratic" sizes for only the main effects and pure quadratics (1 + 2k parameters), admitting smaller designs such as a thirteen-run, four-factor OMARS; the interactions are still present in the design and confined to the second-order block, they are simply not part of the model the run count is chosen for. The D-efficiency reported in the metadata is read from this same model.

  • solver_options (dict, optional) – Passed to the solver: {"msg": bool, "time_limit": int seconds}.

  • tol (float, optional) – Tolerance for the floating-point is_omars() re-check.

  • random_seed (int, optional) – Seed for both the randomized-objective search (which design is found) and the run-order randomisation of the returned design. A fixed seed makes the whole call reproducible.

  • verify (bool, optional) – When True (default) every selected design is re-checked with is_omars() before it is accepted.

Returns:

The OMARS design, with ILP provenance and search diagnostics under metadata (family, sparsity, omars_search report, …).

Return type:

DesignResult

Raises:
  • ValueError – If fewer than three factors are given, the factor count exceeds the combinatorial cap, model is not recognised, n_runs is too small or incompatible with center_runs, or no feasible design is found.

  • ImportError – If PuLP (the ilp extra) is not installed.

Examples

>>> from process_improve.experiments import Factor, generate_omars, analyze_omars
>>> factors = [Factor(name=n, low=-1, high=1) for n in "ABCDE"]
>>> result = generate_omars(factors)
>>> result.metadata["omars_verified"]
True

Various factorial designs.

process_improve.experiments.designs_factorial.full_factorial(nfactors, names=None)[source]#

Create a full factorial (2^k) design for the case when there are nfactors [integer] number of factors.

The optional list of names can be provided. The entries in the list should be strings. If not provided, the names will be created.

Raises:

ValueError – If nfactors < 1 (the empty design is undefined) or nfactors > settings.max_factors_combinatorial (SEC-19 #268: a request for 2**40 rows is a memory-exhaustion attack, not a legitimate design).

Parameters:
  • nfactors (int)

  • names (list | None)

Return type:

list

Backwards-compatible re-exporter for process_improve.experiments.

The implementation now lives in process_improve.experiments._lm (ENG-23 / #305): the renamed file makes filename-ranked tooling (Jump-to-File, fuzzy search, codecov reports) less ambiguous about which models.py is being shown.

Every public name remains importable as before:

from process_improve.experiments.models import Model, lm, predict, summary, validate_formula_is_safe
class process_improve.experiments.models.Model(OLS_instance, model_spec, aliasing=None, name=None)[source]#

Bases: OLS

Just a thin wrapper around the OLS class from Statsmodels.

Parameters:
  • OLS_instance (Any)

  • model_spec (str)

  • aliasing (dict | None)

  • name (str | None)

name: str | None#
data: Expt | None#
aliasing: dict | None#
summary(alpha=0.05, print_to_screen=True)[source]#

Build the OLS summary table for this model and return it.

The returned object is the statsmodels summary instance, with the underlying self._OLS.summary() adjusted to label the residual standard error row. The method does NOT print anything by itself; the top-level summary() wrapper handles screen output via its own show flag. The alpha and print_to_screen arguments are unused and kept for backwards compatibility.

Parameters:
Return type:

Any

get_parameters(drop_intercept=True)[source]#

Get the parameter values; return them in a Pandas dataframe.

Parameters:

drop_intercept (bool)

Return type:

DataFrame

get_factor_names(level=1)[source]#

Get the factors in a model which correspond to a certain level.

1 : pure factors 2 : 2-factor interactions and quadratic terms 3 : 3-factor interactions and cubic terms 4 : etc

Parameters:

level (int)

Return type:

list[str]

get_response_name()[source]#

Get the name of the response variable from the model specification.

Return type:

str

get_title()[source]#

Get the model’s title, if it has one. Always returns a string.

Return type:

str

get_aliases(aliasing_up_to_level=2, drop_intercept=True, websafe=False)[source]#

Return a list, containing strings, representing the aliases of the fitted effects.

aliasing_up_to_level: up to which level of interactions shown

drop_intercept: default is True, but sometimes it is interesting to

know which effects are aliased with the intercept

websafe: default is False; if True, will print the first term

in the aliasing in bold, since that is the nominally estimated effect.

Parameters:
  • aliasing_up_to_level (int)

  • drop_intercept (bool | None)

  • websafe (bool | None)

Return type:

list

exception process_improve.experiments.models.UnsafeFormulaError[source]#

Bases: ValueError

Raised when a model formula contains tokens outside the safe Wilkinson subset.

Patsy and statsmodels evaluate each formula term as a Python expression, so a formula coming from an untrusted source (for example the fit_linear_model MCP tool) is a code-execution vector. validate_formula_is_safe() rejects anything that is not a plain Wilkinson formula over known data columns before it ever reaches patsy.

process_improve.experiments.models.forg(x, prec=3)[source]#

Yanked from the code for Statsmodels / iolib / summary.py and adjusted.

Formats x with prec significant/decimal digits, switching to the g format for very large or very small magnitudes. Any positive prec is supported; prec=3 and prec=4 reproduce the original widths.

Parameters:
Return type:

str

process_improve.experiments.models.lm(model_spec, data, name=None, alias_threshold=0.995)[source]#

Create a linear model.

Parameters:
  • model_spec (str)

  • data (Expt)

  • name (str | None)

  • alias_threshold (float | None)

Return type:

Model

process_improve.experiments.models.predict(model, **kwargs)[source]#

Make predictions from the model.

Parameters:
Return type:

Any

process_improve.experiments.models.summary(model, show=True, aliasing_up_to_level=3)[source]#

Print a summary to the screen of the model.

Appends, if there is any aliasing, a summary of those aliases, up to the (integer) level of interaction: aliasing_up_to_level.

Parameters:
  • model (Model)

  • show (bool | None)

  • aliasing_up_to_level (int)

Return type:

Any

process_improve.experiments.models.validate_formula_is_safe(formula, allowed_names, *, allow_transforms=False, allow_numpy=False)[source]#

Reject a model formula that is not a safe Wilkinson formula over allowed_names.

This is the guard for untrusted callers (e.g. the fit_linear_model tool). Patsy evaluates every formula term as a Python expression with builtins and numpy in scope, so a string such as y ~ I(__import__('os').system('id')) would execute arbitrary code.

By default only a plain Wilkinson formula is allowed:

  • identifiers that name an actual data column,

  • the operators ~ + - * : ^ and grouping parentheses,

  • integer literals (for powers like (A + B)**2) and whitespace.

Any quote, dot, comma, dunder, or unknown identifier (np, I, __import__, …) is rejected.

The optional flags relax this for trusted-but-still-validated callers. They switch on an AST-based check that admits a curated set of transforms while still rejecting attribute access, string literals, dunders, and any call other than the allowlisted ones:

  • allow_transforms - permit I(...) / Q(...) wrapping arithmetic over data columns (e.g. the quadratic shorthand’s I(A ** 2)).

  • allow_numpy - additionally permit a curated allowlist of element-wise numpy calls such as np.log(A) or np.power(A, 2).

Parameters:
  • formula (str) – The model formula in Wilkinson notation, e.g. "y ~ A*B".

  • allowed_names (Iterable[str]) – The legal identifier names, i.e. the columns present in the data.

  • allow_transforms (bool) – If true, permit I(...) / Q(...) transforms of column arithmetic.

  • allow_numpy (bool) – If true, permit a curated allowlist of element-wise np.<func> calls.

Raises:

UnsafeFormulaError – If formula is not a string, contains a __ dunder, or references a token / construct outside the permitted subset.

Return type:

None

process_improve.experiments.models.validate_identifier_is_safe(name)[source]#

Reject a column / response name that is not a plain Python identifier.

User-supplied names (design_matrix dict keys, response_column) are interpolated into a patsy formula, so a name such as "A); import os; (" is an injection vector. We require a bare identifier and forbid dunders.

Parameters:

name (object) – The candidate column or response name.

Raises:

UnsafeFormulaError – If name is not a string, contains __, or is not a plain identifier.

Return type:

None

Design evaluation: quality metrics for experimental designs.

Provides evaluate_design(), which computes properties and quality metrics of an existing design matrix. Supported metrics include efficiency values (D/I/G), prediction variance, VIF, condition number, power analysis, alias structure, confounding, resolution, defining relation, clear effects, minimum aberration, moment aberration, and degrees of freedom.

Example

>>> from process_improve.experiments import evaluate_design, generate_design, Factor
>>> factors = [Factor(name="A", low=0, high=10), Factor(name="B", low=0, high=10)]
>>> result = generate_design(factors, design_type="full_factorial", n_center_points=0)
>>> metrics = evaluate_design(result, model="interactions", metric=["d_efficiency", "vif"])
process_improve.experiments.evaluate.evaluate_design(design_matrix, model=None, metric='d_efficiency', effect_size=None, alpha=0.05, sigma=None, region='cuboidal', n_samples=100000, include_vertices=True, random_seed=42, fds_resolution=None)[source]#

Compute quality metrics for an experimental design.

Parameters:
  • design_matrix (DataFrame or DesignResult) – The design to evaluate. If a DesignResult is passed, the coded design matrix and any generator / defining-relation metadata are extracted automatically.

  • model (str or None) – Model type: "main_effects", "interactions", "quadratic", or an explicit patsy formula. None defaults to "interactions".

  • metric (str or list[str]) – One or more metric names to compute, or the special value "all" to compute every metric. Valid names: "d_efficiency", "i_efficiency", "g_efficiency", "a_optimality", "e_optimality", "correlation", "alias_matrix", "fds", "prediction_variance", "vif", "condition_number", "power", "degrees_of_freedom", "alias_structure", "confounding", "resolution", "defining_relation", "clear_effects", "minimum_aberration", "moment_aberration". The optimality-criterion metrics also accept the opposite suffix as an alias (e.g. "d_optimality" for "d_efficiency", "a_efficiency" for "a_optimality"); the result is keyed under the canonical name.

  • effect_size (float or None) – Expected effect size for power calculation. When None, a power curve over a range of effect sizes is returned instead.

  • alpha (float) – Significance level for power calculation (default 0.05).

  • sigma (float or None) – Estimated noise standard deviation. Defaults to 1.0 when needed but not provided.

  • region ({"cuboidal", "spherical"}) – Design region over which the region-based metrics (i_efficiency, g_efficiency, fds) integrate the prediction variance. "cuboidal" (default) is [-1, 1]^k; "spherical" is the ball of radius sqrt(k).

  • n_samples (int) – Number of random samples drawn over the region (default 100,000).

  • include_vertices (bool) – When True (default), all 2**k cube vertices are added to the region sample so the worst-case (G) value at a corner is represented.

  • random_seed (int) – Seed for the region sampler (full reproducibility).

  • fds_resolution (int or None) – Resolution of the dense FDS curve. When None (default) the fds metric returns only the coarse 11-point quantiles summary. When set (e.g. 200), a curve sub-dict with length-fds_resolution fraction / prediction_variance / scaled_prediction_variance arrays is added for smooth plotting; the endpoints are the minimum and maximum prediction variance.

Returns:

Results keyed by metric name. The structure of each value depends on the metric - see individual metric documentation.

Return type:

dict[str, Any]

Examples

>>> from process_improve.experiments import evaluate_design, generate_design, Factor
>>> factors = [Factor(name="A", low=0, high=10), Factor(name="B", low=0, high=10)]
>>> result = generate_design(factors, design_type="full_factorial", n_center_points=0)
>>> metrics = evaluate_design(result, model="main_effects", metric="d_efficiency")
>>> metrics["d_efficiency"]
100.0
process_improve.experiments.evaluate.evaluate_all(design_matrix, model=None, effect_size=None, alpha=0.05, sigma=None, region='cuboidal', n_samples=100000, include_vertices=True, random_seed=42, fds_resolution=None)[source]#

Compute every available metric for a design in one call.

Thin convenience wrapper around evaluate_design() with metric="all" so callers need not enumerate the metric list. All parameters have the same meaning as in evaluate_design().

Returns:

Results keyed by metric name (the union of every registered metric).

Return type:

dict[str, Any]

Parameters:
  • design_matrix (DataFrame | DesignResult)

  • model (str | None)

  • effect_size (float | None)

  • alpha (float)

  • sigma (float | None)

  • region (str)

  • n_samples (int)

  • include_vertices (bool)

  • random_seed (int)

  • fds_resolution (int | None)

See also

evaluate_design

Compute one or more named metrics.

Experiment analysis: fit models, ANOVA, diagnostics, residuals.

Provides analyze_experiment(), the main analytical workhorse for designed experiments (Tool 3 in the DOE tool architecture).

Uses statsmodels and scipy for the heavy lifting, with thin custom code for lack-of-fit, curvature test, Lenth’s method, pred-R², adequate precision, and confirmation run testing.

process_improve.experiments.analysis.build_formula(response, factors, model=None)[source]#

Build a patsy/statsmodels formula string.

Parameters:
  • response (str) – Name of the response column.

  • factors (list[str]) – Factor column names.

  • model (str or None) – "main_effects", "interactions", "quadratic", or an explicit formula string. None defaults to "interactions".

Returns:

A formula like "Y ~ A + B + A:B".

Return type:

str

class process_improve.experiments.analysis.AnalysisResult(ols_result=None, formula='', results=<factory>)[source]#

Bases: object

Internal, unused container.

Retained for backwards compatibility with any external caller that imports the name from this module. analyze_experiment() returns a plain dict (see its Returns section), not an instance of this class, so downstream code that expects the return type of analyze_experiment should key into that dict rather than access attributes here.

Parameters:
  • ols_result (RegressionResultsWrapper)

  • formula (str)

  • results (dict[str, Any])

ols_result: RegressionResultsWrapper = None#
formula: str = ''#
results: dict[str, Any]#
process_improve.experiments.analysis.analyze_experiment(design_matrix, responses=None, model=None, analysis_type='anova', significance_level=0.05, transform=None, coding='coded', new_points=None, observed_at_new=None, response_column=None)[source]#

Fit models, run ANOVA, compute effects, diagnose residuals.

Parameters:
  • design_matrix (DataFrame) – Factor settings per run. May also contain the response column(s).

  • responses (DataFrame, Series, or None) – Response column(s). If None, response_column must name a column already present in design_matrix. When a DataFrame with more than one column is passed, only the first column is analysed; the rest are added to the working frame but ignored by every subsequent step. Use response_column to pick a specific column explicitly.

  • model (str or None) – "main_effects", "interactions", "quadratic", an explicit formula, or None (defaults to "interactions").

  • analysis_type (str or list[str]) – One or more of: "anova", "effects", "coefficients", "significance", "residual_diagnostics", "lack_of_fit", "curvature_test", "model_selection", "box_cox", "lenth_method", "confidence_intervals", "prediction", "confirmation_test".

  • significance_level (float) – Default 0.05.

  • transform (str or None) – "log", "sqrt", "inverse", "box_cox", or None.

  • coding (str, default "coded") – Reserved for a future coded/actual factor-scale switch. Currently accepted for API stability but not consumed by the analysis.

  • new_points (DataFrame or None) – For prediction or confirmation testing.

  • observed_at_new (list[float] or None) – Observed values at new_points (for confirmation testing).

  • response_column (str or None) – Name of the response column when it lives inside design_matrix.

Returns:

Results keyed by analysis type. Always includes "model_summary" with the keys:

  • formula - the resolved patsy formula that was fitted.

  • r_squared - R^2 of the fit.

  • r_squared_adj - adjusted R^2.

  • r_squared_pred - prediction R^2 (leave-one-out style).

  • adequate_precision - signal-to-noise ratio (>= 4 is considered adequate).

  • n_obs - number of observations used to fit.

  • n_terms - number of columns in the model matrix.

  • model_rank - numerical rank of the model matrix; less than n_terms implies aliasing / rank deficiency.

  • rank_deficient - True if model_rank < n_terms.

  • df_model - model degrees of freedom.

  • df_residual - residual degrees of freedom.

  • mse_residual - mean squared error of the residuals.

Return type:

dict[str, Any]

Examples

>>> import pandas as pd
>>> from process_improve.experiments.analysis import analyze_experiment
>>> df = pd.DataFrame({
...     "A": [-1, 1, -1, 1], "B": [-1, -1, 1, 1],
...     "y": [28, 36, 18, 31],
... })
>>> result = analyze_experiment(df, response_column="y", analysis_type="coefficients")
>>> result["coefficients"][0]["term"]
'Intercept'

Design augmentation: extend or modify an existing experimental design.

Provides augment_design(), which takes an existing design matrix and augments it by adding runs (foldover, semifold, center points, axial points, D-optimal runs), upgrading to a response surface design, adding n_blocks, or replicating.

Example

>>> import pandas as pd
>>> from process_improve.experiments.augment import augment_design
>>> design = pd.DataFrame({"A": [-1, 1, -1, 1], "B": [-1, -1, 1, 1]})
>>> result = augment_design(design, augmentation_type="add_center_points", n_additional_runs=3)
>>> pd.DataFrame(result["augmented_design"]).shape
(7, 2)
process_improve.experiments.augment.augment_design(existing_design, augmentation_type, target_model=None, n_additional_runs=None, fold_on=None, alpha=None, generators=None)[source]#

Extend or modify an existing experimental design.

Parameters:
  • existing_design (DataFrame) – The current design matrix with factor columns in coded units (-1/+1).

  • augmentation_type (str) – One of "foldover", "semifold", "add_center_points", "add_axial_points", "add_runs_optimal", "upgrade_to_rsm", "add_blocks", "replicate".

  • target_model (str or None) – Desired model after augmentation: "main_effects", "interactions", "quadratic". Used by "add_runs_optimal" and "upgrade_to_rsm".

  • n_additional_runs (int or None) – Budget for additional runs. Interpretation depends on the augmentation type (number of center points, number of D-optimal runs, number of blocks, …). For "replicate", this is the number of complete copies of the existing design that are appended (each copy adds len(existing_design) runs); the default of None becomes 1 complete copy.

  • fold_on (str or None) – For "semifold" only: which factor to fold on. If None, the best factor is auto-selected.

  • alpha (str, float, or None) – Axial distance for "add_axial_points" and "upgrade_to_rsm". String values: "rotatable", "face_centered", "orthogonal". Or a numeric value.

  • generators (list[str] or None) – Generator strings from the original design (e.g. ["D=ABC"]). Needed for meaningful alias analysis in foldover/semifold.

Returns:

Keys include "augmented_design" (list of dicts), "new_runs" (list of dicts), "n_runs_before", "n_runs_after", "explanation" (narrative), "before_metrics", "after_metrics", and augmentation-specific keys.

Return type:

dict[str, Any]

Raises:

ValueError – If augmentation_type is unknown, or if required parameters are missing for the requested augmentation.

Examples

>>> import pandas as pd
>>> from process_improve.experiments.augment import augment_design
>>> design = pd.DataFrame({
...     "A": [-1, 1, -1, 1, -1, 1, -1, 1],
...     "B": [-1, -1, 1, 1, -1, -1, 1, 1],
...     "C": [-1, -1, -1, -1, 1, 1, 1, 1],
... })
>>> result = augment_design(design, "add_center_points", n_additional_runs=3)
>>> result["n_runs_after"]
11

Response optimization for designed experiments (Tool 4).

Find optimal factor settings for one or multiple responses after fitting a model with analyze_experiment() (Tool 3).

Implemented methods#

  • desirability - Derringer-Suich desirability functions (single and multi-response) with scipy.optimize.minimize (SLSQP).

  • steepest_ascent / steepest_descent - Move along the gradient of a first-order model from the design centre.

  • stationary_point - Locate the stationary point of a second-order model via numpy.linalg.solve.

  • canonical_analysis - Eigenvalue decomposition of the B matrix to classify the stationary point (max / min / saddle).

  • ridge_analysis - Trace the constrained optimum along spheres of increasing radius, by solving Draper’s secular equation for the Lagrange multiplier.

  • pareto_front - The non-dominated set over several responses, via augmented Chebyshev scalarisation on a Das-Dennis weight lattice.

process_improve.experiments.optimization.evaluate_model(coefficients, factor_names, point)[source]#

Evaluate predicted response at an arbitrary coded point.

Parameters:
  • coefficients (list[dict]) – Coefficient list from analyze_experiment.

  • factor_names (list[str]) – Ordered factor names.

  • point (dict[str, float]) – Factor settings in coded units, e.g. {"A": 0.5, "B": -1.0}.

Returns:

Predicted response value.

Return type:

float

process_improve.experiments.optimization.optimize_responses(fitted_models, goals=None, method='desirability', factor_ranges=None, step_size=0.5, n_steps=10, response_importance=None, fitted_results=None, significance_level=0.05, search_bounds=None, desirability_weights=None, ridge_direction='maximize', n_pareto_points=21)[source]#

Find optimal factor settings for one or multiple responses.

Parameters:
  • fitted_models (list[dict]) –

    Each dict describes a fitted model with keys:

    • "response_name" (str) - name of the response.

    • "coefficients" (list[dict]) - coefficient list, each with "term" and "coefficient" keys as returned by analyze_experiment(..., analysis_type="coefficients").

    • "factor_names" (list[str]) - ordered factor names.

    • "mse_residual" (float, optional) - mean squared error.

    • "r_squared" (float, optional) - model R-squared.

  • goals (list[dict] or None) –

    Per-response optimisation goals. Each dict has keys:

    • "response" (str) - response name. Matched against each model’s "response_name"; when both sides name their responses the goals are reordered to match, so the two lists need not be in the same order. When either side omits a name, goals are taken in list order.

    • "goal" (str) - "maximize", "minimize", or "target".

    • "target" (float, optional) - target value (required when goal="target").

    • "low" (float) - lower acceptable bound.

    • "high" (float) - upper acceptable bound.

    • "weight" (float, default 1) - the exponent shaping this response’s desirability ramp between low and high. Above 1 concentrates desirability near the good end; below 1 flattens it.

    • "weight_high" (float, optional) - a separate exponent for the falling side of a "target" goal. Defaults to "weight".

    • "importance" (float, default 1) - how much this response counts relative to the others when the composite is formed. Unlike weight, it has no effect on this response’s own ramp.

  • method (str) – Optimisation method: "desirability", "steepest_ascent", "steepest_descent", "stationary_point", "canonical_analysis", "ridge_analysis", "pareto_front".

  • factor_ranges (dict or None) – Maps factor name to {"low": float, "high": float} in actual units. Used for coded ↔ actual conversion.

  • step_size (float) – Step magnitude for steepest ascent/descent (coded units).

  • n_steps (int) – Number of steps along a path: the steepest ascent/descent steps, or the radii reported by ridge analysis over and above the centre.

  • response_importance (list[float] or None) – Relative importance per response, overriding the per-goal "importance" values. Aligned with fitted_models.

  • fitted_results (list or None) – Optional statsmodels results objects, one per entry in fitted_models and in the same order, as returned by lm() or by analyze_experiment. When supplied, a confidence interval and a prediction interval for each response are reported at the optimum. The models must have been fitted on the coded factors, since the optimum is located in coded units.

  • significance_level (float) – Alpha for those intervals. The default of 0.05 gives 95% intervals.

  • search_bounds (tuple, dict, or None) –

    The coded region to search, and the region against which a stationary point is judged inside or outside. Defaults to the factorial cube, (-1, 1) on every factor.

    That default suits a two-level design but understates a central composite design, whose axial runs sit at plus or minus alpha: leaving it at the cube would refuse to consider settings the experiment actually covered. Pass (-1.41, 1.41) for a two-factor rotatable central composite design, or a mapping such as {"T": (-1.41, 1.41)} to widen one factor only. Factors left out of a mapping keep the (-1, 1) default.

  • desirability_weights (list[float] or None) – Deprecated alias for response_importance. The name was misleading: these values are importances, not the weight that shapes an individual ramp.

  • ridge_direction ({"maximize", "minimize"}) – Which ridge method="ridge_analysis" traces.

  • n_pareto_points (int) – Target number of weight vectors for method="pareto_front". The front returned is usually smaller, since dominated and duplicate solutions are dropped.

Returns:

Results keyed by method. Always includes "method" and "factor_names".

Return type:

dict[str, Any]

Raises:

ValueError – If method is unknown, if fitted_models is empty, if a method that needs goals is called without them, or if both response_importance and desirability_weights are given.

Examples

>>> from process_improve.experiments.optimization import optimize_responses
>>> model = {
...     "response_name": "yield",
...     "coefficients": [
...         {"term": "Intercept", "coefficient": 40.0},
...         {"term": "A", "coefficient": 5.25},
...         {"term": "B", "coefficient": -2.0},
...         {"term": "I(A ** 2)", "coefficient": -3.0},
...         {"term": "I(B ** 2)", "coefficient": -1.5},
...         {"term": "A:B", "coefficient": 1.5},
...     ],
...     "factor_names": ["A", "B"],
... }
>>> result = optimize_responses(
...     fitted_models=[model],
...     method="stationary_point",
... )
>>> result["stationary_point"]["classification"]
'maximum'

The fractional-factorial trade-off: n_runs against n_factors.

The central question when screening many n_factors is how few n_runs you can get away with, and what you pay for that saving. This module answers it in two ways, mirroring the R pid package:

  • get_trade_off_table_entry() reports, for one (runs, factors) pair, the design’s resolution, its generators, its defining relation, and which effects end up aliased with which.

  • trade_off_table() prints the whole grid at once, the Python counterpart of the trade-off table figure in the course notes.

Unlike the R version, which looks designs up in the FrF2 catalogue, the generators here are derived by a minimum-aberration search: for a given number of n_runs and n_factors, every admissible set of generators is scored on its word-length pattern and the best is kept. The search reproduces the table in the course notes exactly, and extends past its printed edge.

Also see#

process_improve.experiments.designs.generate_design : builds the design matrix. process_improve.experiments.evaluate.evaluate_design : evaluates one you have.

class process_improve.experiments.trade_off.TradeOffTableEntry(n_runs, n_factors, n_generators, resolution, roman, generators=<factory>, defining_relation=<factory>, aliases=<factory>, n_replicates=1, label='')[source]#

Bases: object

What you get, and what you give up, at a given (runs, factors) pair.

Parameters:
n_runs#

Number of experiments in the design.

Type:

int

n_factors#

Number of factors studied.

Type:

int

n_generators#

The p in 2^(k-p): how many factors are added on top of the k - p base n_factors. Zero for a full factorial.

Type:

int

resolution#

Design resolution as an integer (3, 4, 5, …), or None for a full factorial, which has no defining relation and so no resolution.

Type:

int or None

roman#

The same resolution in roman numerals ("III", "IV", …), the way it is written as a subscript on 2^(k-p).

Type:

str or None

generators#

Generators, e.g. ["D=AB", "E=AC"]. Empty for a full factorial. Every generator may be used with either sign; D=-AB gives the complementary fraction, which is equally valid.

Type:

list[str]

defining_relation#

The full defining relation, e.g. ["I=ABD", "I=ACE", "I=BCDE"]. Empty for a full factorial.

Type:

list[str]

aliases#

Alias chains for the main effects and the two-factor interactions, e.g. "A = BD + CE + ...". Empty for a full factorial.

Type:

list[str]

n_replicates#

How many times the full factorial fits into the run budget. 1 in the usual case; 2 means the budget pays for the full factorial twice over, and so on.

Type:

int

label#

Compact description of the design, e.g. "2^(5-2) III", "2^3 (full)" or "2^3 (twice)".

Type:

str

process_improve.experiments.trade_off.minimum_aberration_generators(n_runs, n_factors)[source]#

Find the minimum-aberration generators for a 2^(k-p) design.

Every way of assigning the p extra n_factors to interaction columns of the base factorial is enumerated, scored by its word-length pattern, and the best-scoring one is returned. Ties are broken in favour of the generator set that comes first by word length and then alphabetically.

Parameters:
  • n_runs (int) – Number of runs; must be a power of two.

  • n_factors (int) – Number of factors, greater than log2(n_runs).

Returns:

Generators such as ("D=AB", "E=AC").

Return type:

tuple[str, …]

Raises:

ValueError – If n_runs is not a power of two, if the design is not fractional (n_factors <= log2(n_runs)), if there are too few interaction columns to hold the extra n_factors, or if the search space is too large to enumerate.

Examples

>>> minimum_aberration_generators(8, 5)
('D=AB', 'E=AC')
>>> minimum_aberration_generators(16, 5)
('E=ABCD',)

Notes

A minimum-aberration design is unique only up to relabelling the factors, so a textbook may print a different but equivalent set of generators.

process_improve.experiments.trade_off.get_trade_off_table_entry(n_runs=8, n_factors=7, display=True)[source]#

Report the resolution, generators and aliasing at a (runs, factors) pair.

Answers the screening question “if I can afford n_runs experiments and I want to study n_factors n_factors, what do I lose?”. The loss is aliasing: effects that the design cannot tell apart.

Parameters:
  • n_runs (int, default 8) – Number of experiments you can afford. Must be a power of two.

  • n_factors (int, default 7) – Number of factors to study.

  • display (bool, default True) – Print a human-readable report as well as returning it. Set to False to keep the function quiet.

Returns:

Resolution, generators, defining relation and alias chains. See the class for the full field list.

Return type:

TradeOffTableEntry

Raises:

ValueError – If n_runs or n_factors is not an integer, if n_runs is not a power of two, if n_factors is below 2, or if the n_factors cannot fit into the run budget.

Examples

>>> result = get_trade_off_table_entry(n_runs=8, n_factors=5, display=False)
>>> result.label
'2^(5-2) III'
>>> result.generators
['D=AB', 'E=AC']

A run budget larger than the full factorial needs is reported as replication rather than as an error:

>>> get_trade_off_table_entry(n_runs=16, n_factors=3, display=False).label
'2^3 (twice)'

Also see#

trade_off_table : the same information for a whole grid of designs.

process_improve.experiments.trade_off.trade_off_table(runs=(4, 8, 16, 32, 64), factors=(3, 4, 5, 6, 7, 8, 9), display=True)[source]#

Return the runs-against-factors trade-off table.

The Python counterpart of R’s tradeOffTable(), which displays the table as a static image. Here it is computed, so it can be widened past the printed edge and the cells can be read programmatically.

Reading the table: going down a column costs more experiments but buys resolution; going across a row studies more factors for the same money, at the cost of heavier aliasing. Blank cells are designs that do not exist (too many factors for that many runs).

Parameters:
  • runs (Sequence[int], default (4, 8, 16, 32, 64)) – Run budgets, one per row. Each must be a power of two.

  • factors (Sequence[int], default (3, 4, 5, 6, 7, 8, 9)) – Factor counts, one per column.

  • display (bool, default True) – Print the table as well as returning it. Set to False to keep the function quiet.

Returns:

Rows indexed by run count, columns by factor count. Each cell is a label such as "2^(5-2) III", "2^3 (full)" or "2^3 (twice)"; impossible combinations are the empty string.

Return type:

pd.DataFrame

Examples

>>> table = trade_off_table()
>>> table.loc[8, 5]
'2^(5-2) III'
>>> table.loc[16, 4]
'2^4 (full)'

Also see#

get_trade_off_table_entry : generators and alias chains for a single cell of this table.

The OMARS trade-off: which model does a given run budget buy.

The two-level trade-off table in process_improve.experiments.trade_off answers “I can afford N runs and want k factors, what do I give up?” with a resolution and an alias structure. That currency does not transfer to OMARS designs, because an OMARS design always has its main effects orthogonal to each other and to every second-order term. Resolution is constant, so it cannot be what the table reports.

What varies instead is which model is estimable at all, and that is set by the foldover structure. A foldover is [H; -H; 0], and every second-order term is an even function, so the quadratic and interaction columns of H and -H are identical. The even block therefore has at most h + 1 distinct rows, against 1 + k(k+1)/2 columns for the full second-order model. The main effects live in the odd block and contribute at most k more, so for every foldover

\[\mathrm{rank}(X) \le k + \min\!\left(h + 1,\; 1 + \tfrac{k(k+1)}{2}\right)\]

with equality for half-designs in general position. Three capability classes follow, and they are the OMARS analogue of resolution:

Full

N >= k^2 + k + 1. Main effects, pure quadratics and every two-factor interaction are jointly estimable, so a response surface can be fitted without a follow-up design.

Quad

N >= 2k + 3. Main effects and the pure quadratics, with degrees of freedom left to test them, so curvature can be judged factor by factor. The two-factor interactions are present in the design but not in the model.

Satd

N = 2k + 1. The definitive screening design at its minimal size: estimable but exactly saturated, so there are point estimates and no inference.

Alphabetically Full < Quad < Satd, which is also decreasing capability, so the ordering is easy to keep straight.

Every number here is closed-form, so the table is instant and exact: no integer program, no solver, and no dependence on a search budget. The quality of a particular design at a given size (its D-efficiency, its second-order correlations) does need the ILP, and lives on generate_omars() instead.

Also see#

process_improve.experiments.trade_off : the two-level counterpart. process_improve.experiments.designs_omars_ilp.generate_omars : build the design. process_improve.experiments.omars.analyze_omars : the staged analysis it feeds.

process_improve.experiments.omars_trade_off.CAPABILITIES: tuple[str, ...] = ('full', 'quad', 'satd')#

Capability classes, best first. The four-character tags line up in a table, and sort alphabetically in decreasing order of capability.

process_improve.experiments.omars_trade_off.DEFAULT_RUNS: tuple[int, ...] = (9, 13, 17, 21, 25, 31, 37, 43, 57)#

odd run counts spanning the useful band for three to seven factors. The last one is the smallest budget at which every one of those factor counts reaches Full.

Type:

Default rows

process_improve.experiments.omars_trade_off.DEFAULT_FACTORS: tuple[int, ...] = (3, 4, 5, 6, 7)#

Default columns.

class process_improve.experiments.omars_trade_off.OmarsTradeOffTableEntry(n_runs, n_factors, exists, capability, tag, model, model_params, error_df, label, min_runs_full, min_runs_quad, min_runs_satd, reason='')[source]#

Bases: object

What one run budget buys for one factor count.

Parameters:
  • n_runs (int)

  • n_factors (int)

  • exists (bool)

  • capability (str)

  • tag (str)

  • model (str | None)

  • model_params (int)

  • error_df (int)

  • label (str)

  • min_runs_full (int)

  • min_runs_quad (int)

  • min_runs_satd (int)

  • reason (str)

n_runs#

The run budget.

Type:

int

n_factors#

Number of factors.

Type:

int

exists#

Whether any foldover OMARS design has this run count for this many factors. False for an even n_runs or one below 2k + 1.

Type:

bool

capability#

"full", "quad", "satd", or "none" when exists is False.

Type:

str

tag#

The four-character table tag: "Full", "Quad", "Satd", or the empty string.

Type:

str

model#

The largest model this run count supports: "full_second_order", "main_quadratic", or None.

Type:

str or None

model_params#

Parameter count of that model; zero when there is no design.

Type:

int

error_df#

Degrees of freedom left for error after fitting it. Zero for Satd.

Type:

int

label#

Self-contained cell text, e.g. "Full df=11".

Type:

str

min_runs_full#

Smallest run count reaching Full for this many factors, k^2 + k + 1.

Type:

int

min_runs_quad#

Smallest run count reaching Quad, 2k + 3.

Type:

int

min_runs_satd#

Smallest run count that is a design at all, 2k + 1.

Type:

int

reason#

Why a cell is empty; empty string when exists is True.

Type:

str

process_improve.experiments.omars_trade_off.omars_minimum_runs(n_factors, capability='full')[source]#

Return the smallest run count reaching capability for n_factors.

Parameters:
  • n_factors (int) – Number of factors, k, between 3 and 25.

  • capability ({"full", "quad", "satd"}, default "full") – Which class to reach. See the module docstring.

Returns:

The (odd) run count: k^2 + k + 1 for "full", 2k + 3 for "quad", 2k + 1 for "satd".

Return type:

int

Raises:

ValueError – If n_factors is out of range or capability is not a known class.

Examples

>>> omars_minimum_runs(5)
31
>>> omars_minimum_runs(5, "quad")
13
>>> [omars_minimum_runs(k) for k in (3, 4, 5, 6, 7)]
[13, 21, 31, 43, 57]
process_improve.experiments.omars_trade_off.get_omars_trade_off_table_entry(n_runs, n_factors, display=True)[source]#

Report which model a run budget buys, for a foldover OMARS design.

The OMARS counterpart of get_trade_off_table_entry(). Because OMARS main effects are always clear of the second-order terms, the answer is not a resolution: it is which model is estimable, and how much is left over to test it with.

Parameters:
  • n_runs (int) – Run budget. A foldover has 2h + 1 runs, so an even value is never a design.

  • n_factors (int) – Number of factors, between 3 and 25.

  • display (bool, default True) – Print a short report as well as returning it.

Returns:

The capability class, the model it supports, and the error degrees of freedom. See the class for the full field list.

Return type:

OmarsTradeOffTableEntry

Raises:

ValueError – If n_factors is out of range, or n_runs is not a positive integer.

Examples

>>> get_omars_trade_off_table_entry(21, 4, display=False).label
'Full df=6'
>>> get_omars_trade_off_table_entry(17, 4, display=False).label
'Quad df=8'
>>> get_omars_trade_off_table_entry(9, 4, display=False).label
'Satd df=0'

Also see#

omars_trade_off_table : the same answer across a grid of budgets.

process_improve.experiments.omars_trade_off.omars_trade_off_table(runs=(9, 13, 17, 21, 25, 31, 37, 43, 57), factors=(3, 4, 5, 6, 7), display=True)[source]#

Return the run-budget against factor-count table for OMARS designs.

Each cell says which model that budget supports and how much error is left to test it with, for example "Full df=11". Blank cells are budgets that are not a foldover design at all.

Parameters:
  • runs (sequence of int, default DEFAULT_RUNS) – Run budgets, one per row. Even values are always blank.

  • factors (sequence of int, default DEFAULT_FACTORS) – Factor counts, one per column.

  • display (bool, default True) – Print the table left-aligned, with the df= label written once per column rather than in every cell.

Returns:

Rows indexed by run count, columns by factor count. Cells are self-contained labels; the once-per-column compression applies only to the printed view.

Return type:

pd.DataFrame

Examples

>>> table = omars_trade_off_table(display=False)
>>> table.loc[21, 4]
'Full df=6'
>>> table.loc[9, 3]
'Quad df=2'

Also see#

get_omars_trade_off_table_entry : the detail behind one cell.

Teaching simulators used in the Process Improvement using Data course.

Each function here is a small, deliberately opaque process that a student drives with a designed experiment. They are ports of the simulators in the companion R package (pid): popcorn(), grocery() and manufacture().

All three add random noise to the response, so that repeated runs at the same settings do not give an identical answer. Pass random_state to make a run reproducible; leave it at None (the default) for fresh noise on every call, which is what the classroom exercise expects.

Every simulator refuses vector input on purpose: the point of the exercise is sequential experimentation, one run at a time, with the fewest number of runs.

process_improve.experiments.simulations.popcorn(t=120, T=None, *, random_state=None)[source]#

Simulate stovetop popcorn cooking.

Returns the number of popped kernels after cooking a fixed set of kernels, at the same heat setting on the stove, for t seconds. There is only one factor: the cooking time.

Parameters:
  • t (float, default 120) – Number of seconds the pot is left on the stove. Cooking times less than 77 seconds are not supported: nothing has popped yet. A vector (list or array) of time values is not permitted, since the goal is to perform sequential experimentation to determine the optimum time, with the fewest number of function calls.

  • T (float or None, default None) – Alias for t, matching the popcorn(T=...) spelling used in the R package. When given, it overrides t.

  • random_state (int, np.random.Generator, or None, default None) – Seed or generator for the noise term. None (the default) draws fresh noise on every call, which is the intended behaviour for the classroom exercise; pass an int to make a call reproducible.

Returns:

The number of popped kernels, with random noise added for realism. Never negative.

Return type:

int

Raises:

ValueError – If t is not a single finite number, or is below 77 seconds.

Examples

>>> popcorn(t=135, random_state=13)
94

Source#

Kevin Dunn, Process Improvement using Data, Chapter 5, 2010 to 2026, https://learnche.org/pid

Also see#

grocery manufacture

process_improve.experiments.simulations.grocery(p=3.46, h=150, P=None, H=None, *, random_state=None)[source]#

Simulate grocery store profits for a single product.

The hourly profit made when selling the product at price p and the product is displayed at height h [cm up from the ground] on the shelf.

Simulates a grocery store profit function where there are 2 factors:

  • p = selling price of the product, measured in dollars and cents

  • h = height of the product on the shelf, measured in centimeters above the ground.

Typical values are p = $3.50 and h = 150cm. The outcome is the profit made per hour [dollars/hour], with random noise added, for realism.

Parameters:
  • p (float, default 3.46) – Selling price of the product [dollars].

  • h (float, default 150) – Height of the product on the shelf [cm above the ground].

  • P (float or None, default None) – Alias for p, matching the grocery(P=...) spelling used in the R package. When given, it overrides p.

  • H (float or None, default None) – Alias for h. When given, it overrides h.

  • random_state (int, np.random.Generator, or None, default None) – Seed or generator for the noise term. None (the default) draws fresh noise on every call; pass an int to make a call reproducible.

Returns:

Profit made per hour [dollars/hour], with random noise added.

Return type:

int

Raises:
  • ValueError – If either input is a vector, is not finite, or is negative.

  • Source

  • ------

  • Kevin Dunn, Process Improvement using Data, Chapter 5, 2010 to 2026,

  • https://learnche.org/pid

  • Also see

  • --------

  • popcorn

  • manufacture

process_improve.experiments.simulations.manufacture(p=0.75, t=325, P=None, T=None, *, random_state=None)[source]#

Simulate the hourly profit of a manufacturing facility.

Two factors affect the outcome:

  • p = selling price of the product, measured in dollars and cents

  • t = throughput (production rate) of the process, in parts per hour

Typical values are p = $0.75 and t = 325 parts per hour. The outcome is the profit made per hour [dollars/hour], with random noise added for realism. The aim of the exercise is to maximize that profit.

Parameters:
  • p (float, default 0.75) – Selling price of the product [dollars].

  • t (float, default 325) – Throughput (production rate) of the process [parts per hour].

  • P (float or None, default None) – Alias for p, matching the manufacture(P=...) spelling used in the R package. When given, it overrides p.

  • T (float or None, default None) – Alias for t. When given, it overrides t.

  • random_state (int, np.random.Generator, or None, default None) – Seed or generator for the noise term. None (the default) draws fresh noise on every call; pass an int to make a call reproducible.

Returns:

Profit made per hour [dollars/hour], with random noise added.

Return type:

int

Raises:

ValueError – If either input is a vector, is not finite, or is negative.

Examples

>>> manufacture(p=1.5, t=320, random_state=42)
601

Source#

Kevin Dunn, Process Improvement using Data, Chapter 5, 2010 to 2026, https://learnche.org/pid

Also see#

grocery popcorn

process_improve.experiments.datasets.distillateflow()[source]#

Return the flow rate of distillate from the top of a distillation column.

These are actual data, taken 1 minute apart in time, of the flow rate leaving the top of a continuous distillation column (data are from a 31 day period in time). The data are fetched from the canonical hosted location on openmv.net rather than bundled with the package.

Dimensions#

A data frame containing 44640 observations of 1 variable.

Source#

http://openmv.net/info/distillate-flow

Return type:

DataFrame

process_improve.experiments.datasets.pollutant()[source]#

Return water treatment example data from BHH2, Ch 5, Question 19.

Description#

The data are from the first 8 rows of the pollutant water treatment example n the book by Box, Hunter and Hunter, 2nd edition, Chapter 5, Question 19.

The 3 factors (C, T, and S) are in coded units where: C = -1 is chemical brand A; C = +1 is chemical brand B T = -1 is 72F for treatment temperature; T = +1 is 100F for the temperature S = -1 is No stirring; S = +1 is with fast stirring

The outcome variable is: y = the pollutant amount in the discharge [lb/day].

The aim is to find treatment conditions that MINIMIZE the amount of pollutant discharged each day, where the limit is 10 lb/day.

Dimensions#

A data frame containing 8 observations of 4 variables (C, S, T and y).

Source#

Box, G. E. P. and Hunter, J. S. and Hunter, W. G.r, Statistics for Experimenters, Wiley, 2nd edition, Chapter 5, Question 19, page 232.

Return type:

DataFrame

process_improve.experiments.datasets.oildoe()[source]#

Return industrial designed experiment data to improve the volumetric heat capacity of a product.

Description#

Four materials: A, B, C and D are added in a blend to achieve a desired heat capacity, the response variable, y.

The amounts were varied in a factorial manner for the 4 materials.

The data are scaled and coded for confidentiality. All that may be disclosed is that variable C is either added (“Yes”) or not added not added (“No”). The data are fetched from the canonical hosted location on openmv.net rather than bundled with the package.

Dimensions#

A data frame containing 19 observations of 5 variables (A, B, C, D, and the response, y).

Source#

http://openmv.net/info/oil-company-doe Data from a confidential industrial source.

Return type:

DataFrame

process_improve.experiments.datasets.golf()[source]#

Return full factorial experiment data to maximize a golfer’s driving distance.

A full factorial experiment with four factors run by a golf enthusiast. The objective of the experiments was for the golfer to maximize her driving distance at a specific tee off location on her local golf course. The golfer considered the following factors:

H = Tee height (cm) N = Holes: number of golf balls played for prior to experimental tee shot C = Club type T = Time of day (on the 24 hour clock)

The data are in standard order, however the actual experiments were run in random order.

Coded values for H, N, C and T should be used in the linear regression model analysis, with -1 representing the low value and +1 the high value.

Dimensions#

A data frame containing 16 observations of 4 variables (H, N, C, T) and a column y, as a response variable. C and T are stored as text labels ("Callaway" / "Titleist" and "9:00" / "14:00"); code them to -1 / +1 before fitting a linear model.

Source#

A MOOC on Design of Experiments, “Experimentation for Improvement”, https://learnche.org

Return type:

DataFrame

process_improve.experiments.datasets.boilingpot()[source]#

Return full factorial experiment data for stove-top boiling of water.

Description#

The data are from boiling water in a pot under various conditions. The response variable, y, is the time taken, in minutes to reach 90 degrees Celsius. Accurately measuring the time to actual boiling is hard, hence the 90 degrees Celsius point is used instead.

Three factors are varied in a full factorial manner (the first 8 observations). The data are in standard order, however the actual experiments were run in random order. The last 3 rows are runs close to, or interior to the factorial.

Factors varied were:

A = Amount of water: low level was 500 mL, and high level was 600 mL B = Lid off (low level) or lid on (high level) C = Size of pot used: low level was 2 L, and high level was 3 L.

Coded values for A, B and C should be used in the linear regression model analysis, with -1 representing the low value and +1 the high value.

Dimensions#

A data frame containing 11 observations of 4 variables (A, B, C, with y as a response variable.

Source#

MOOC on Design of Experiments, “Experimentation for Improvement”, https://learnche.org

Return type:

DataFrame

process_improve.experiments.datasets.solar()[source]#

Return solar panel example data from Box, Hunter and Hunter, 2nd edition, Chapter 5, page 230.

Description#

The data are from a solar panel simulation case study.

The original source that Box, Hunter and Hunter used is https://www.sciencedirect.com/science/article/abs/pii/0038092X67900515

A theoretical model for a commercial system was made. A 2^4 factorial design was used (center point is not included in this dataset).

The factors are dimensionless groups (https://en.wikipedia.org/wiki/Dimensionless_quantity), related to:

A = total daily insolation, B = the tank capacity, C = the water flow through the absorber, D = solar intermittency coming in.

All 4 factors are coded as -1 for the low level, and +1 for the high lever.

The responses variables are y1: collection efficiency, and y2: the energy delivery efficiency.

Dimensions#

A data frame containing 16 observations of 6 variables (A, B, C, D, with y1 and y2 as responses.)

Source#

Box, G. E. P. and Hunter, J. S. and Hunter, W. G., Statistics for Experimenters, 2nd edition, Wiley, Chapter 5, page 230.

Return type:

DataFrame

process_improve.experiments.datasets.data(dataset)[source]#

Return the dataset given by the string name.

The Python counterpart of R’s data(<name>): a single dispatcher over the loaders in this module, for callers that hold the dataset name as a string (a CLI argument, a config file, a tool call) rather than as an identifier.

Parameters:

dataset (str) – Name of the dataset. One of "boilingpot", "distillateflow", "golf", "oildoe", "pollutant", "solar". The aliases "oil.doe" and "oilDOE" also resolve to oildoe().

Returns:

The dataset, exactly as returned by the corresponding loader.

Return type:

pd.DataFrame

Raises:

ValueError – If dataset is not a known name.

Examples

>>> data("pollutant").shape
(8, 4)

Notes

"distillateflow" and "oildoe" are fetched over the network from openmv.net; the rest are bundled with the package.

Strategy Recommender#

Multi-stage experimental strategy recommender.

Given a DOE problem specification (factors, responses, budget, constraints, domain, prior knowledge), recommend a multi-stage experimental strategy using deterministic decision rules from Montgomery, NIST, and Stat-Ease SCOR.

Quick start:

from process_improve.experiments.strategy import recommend_strategy

result = recommend_strategy(
    factors=[Factor(name="A", low=0, high=100), ...],
    responses=[Response(name="Yield", goal="maximize")],
    budget=40,
    domain="fermentation",
)
process_improve.experiments.strategy.recommend_strategy(*, factors, responses=None, budget=None, constraints=None, hard_to_change_factors=None, prior_knowledge=None, existing_data=None, domain=None, detail_level='intermediate')[source]#

Recommend a multi-stage experimental strategy.

Given a DOE problem description, apply deterministic decision rules to recommend a staged experimental plan (screening → optimisation → confirmation).

Parameters:
  • factors (list[Factor]) – All candidate experimental factors.

  • responses (list[Response] or None) – Response variables with optimisation goals.

  • budget (int or None) – Total run budget across all stages. None = no constraint.

  • constraints (list[Constraint] or None) – Factor-space constraints (linear or nonlinear).

  • hard_to_change_factors (list[str] or None) – Factor names that are expensive to reset between runs.

  • prior_knowledge (str or None) – Free-text description of what the user already knows.

  • existing_data (DataFrame or None) – Prior experimental data (summary extracted internally).

  • domain (str or None) – Application domain (e.g. "fermentation"). Defaults to "general".

  • detail_level (str) – "novice" or "intermediate" (default).

Returns:

JSON-serialisable dictionary with the ExperimentalStrategy fields.

Return type:

dict

Examples

>>> from process_improve.experiments.factor import Factor, Response
>>> factors = [Factor(name=chr(65+i), low=0, high=100) for i in range(7)]
>>> result = recommend_strategy(factors=factors, budget=40, domain="fermentation")
>>> result["total_estimated_runs"] <= 40
True

Deterministic rule engine for DOE strategy recommendation.

Implements ~50 decision rules from Montgomery, NIST, and Stat-Ease SCOR to recommend multi-stage experimental strategies. No LLM or randomness - identical inputs always produce identical outputs.

process_improve.experiments.strategy.engine.recommend_strategy(*, factors, responses=None, budget=None, constraints=None, hard_to_change_factors=None, prior_knowledge=None, existing_data=None, domain=None, detail_level='intermediate')[source]#

Recommend a multi-stage experimental strategy.

Given a DOE problem description, apply deterministic decision rules to recommend a staged experimental plan (screening → optimisation → confirmation).

Parameters:
  • factors (list[Factor]) – All candidate experimental factors.

  • responses (list[Response] or None) – Response variables with optimisation goals.

  • budget (int or None) – Total run budget across all stages. None = no constraint.

  • constraints (list[Constraint] or None) – Factor-space constraints (linear or nonlinear).

  • hard_to_change_factors (list[str] or None) – Factor names that are expensive to reset between runs.

  • prior_knowledge (str or None) – Free-text description of what the user already knows.

  • existing_data (DataFrame or None) – Prior experimental data (summary extracted internally).

  • domain (str or None) – Application domain (e.g. "fermentation"). Defaults to "general".

  • detail_level (str) – "novice" or "intermediate" (default).

Returns:

JSON-serialisable dictionary with the ExperimentalStrategy fields.

Return type:

dict

Examples

>>> from process_improve.experiments.factor import Factor, Response
>>> factors = [Factor(name=chr(65+i), low=0, high=100) for i in range(7)]
>>> result = recommend_strategy(factors=factors, budget=40, domain="fermentation")
>>> result["total_estimated_runs"] <= 40
True

Pydantic models for the DOE strategy recommender.

Defines the input specification (DOEProblemSpec), the output (ExperimentalStrategy, ExperimentalStage, TransitionRule), and supporting types (DomainType, PriorKnowledge).

class process_improve.experiments.strategy.models.DomainType(value)[source]#

Bases: str, Enum

Application domain for domain-specific strategy adjustments.

pharma_formulation = 'pharma_formulation'#
fermentation = 'fermentation'#
food_science = 'food_science'#
extraction = 'extraction'#
analytical_method = 'analytical_method'#
cell_culture = 'cell_culture'#
bioprocess = 'bioprocess'#
general = 'general'#
class process_improve.experiments.strategy.models.PriorKnowledge(*, raw_text='', confidence=0.0, known_significant_factors=<factory>, known_ranges_reliable=False, has_supporting_data=False)[source]#

Bases: BaseModel

Parsed prior knowledge with a confidence score.

Parameters:
  • raw_text (str) – The original free-text description provided by the user.

  • confidence (float) – Confidence score between 0.0 (no knowledge) and 1.0 (confirmed).

  • known_significant_factors (list[str]) – Factor names identified as significant in the prior knowledge.

  • known_ranges_reliable (bool) – Whether the user’s factor ranges are informed by prior data.

  • has_supporting_data (bool) – Whether the prior knowledge is backed by experimental data.

raw_text: str#
confidence: float#
known_significant_factors: list[str]#
known_ranges_reliable: bool#
has_supporting_data: bool#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class process_improve.experiments.strategy.models.TransitionRule(*, condition, action, fallback)[source]#

Bases: BaseModel

Rule governing the transition between consecutive experimental stages.

Parameters:
  • condition (str) – Human-readable condition, e.g. "2-5 significant factors identified".

  • action (str) – Action to take when the condition is met, e.g. "proceed_to_rsm".

  • fallback (str) – Action if the condition is not met, e.g. "broaden_factor_ranges".

condition: str#
action: str#
fallback: str#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class process_improve.experiments.strategy.models.ExperimentalStage(*, stage_number, stage_name, design_type, design_params=<factory>, factors=<factory>, estimated_runs=0, purpose='', success_criteria=<factory>, transition_rules=<factory>)[source]#

Bases: BaseModel

One stage in a multi-stage experimental strategy.

Parameters:
  • stage_number (int) – 1-based stage index.

  • stage_name (str) – Human-readable name, e.g. "Screening", "Optimization".

  • design_type (str) – Design type key, e.g. "plackett_burman", "ccd", "bbd".

  • design_params (dict) – Design-specific parameters (resolution, n_center_points, alpha, etc.).

  • factors (list[str]) – Factor names involved in this stage.

  • estimated_runs (int) – Estimated number of experimental runs.

  • purpose (str) – Brief description of what this stage accomplishes.

  • success_criteria (dict) – Criteria for deeming this stage successful.

  • transition_rules (list[TransitionRule]) – Rules governing the transition to the next stage.

stage_number: int#
stage_name: str#
design_type: str#
design_params: dict[str, Any]#
factors: list[str]#
estimated_runs: int#
purpose: str#
success_criteria: dict[str, Any]#
transition_rules: list[TransitionRule]#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class process_improve.experiments.strategy.models.ExperimentalStrategy(*, strategy_id='', stages=<factory>, total_estimated_runs=0, budget_allocation=<factory>, assumptions=<factory>, risks=<factory>, alternative_strategies=<factory>, domain='general', detail_level='intermediate', reasoning=<factory>)[source]#

Bases: BaseModel

Complete multi-stage experimental strategy recommendation.

Parameters:
  • strategy_id (str) – Deterministic hash of the input specification.

  • stages (list[ExperimentalStage]) – Ordered list of experimental stages.

  • total_estimated_runs (int) – Sum of estimated runs across all stages.

  • budget_allocation (dict[str, int]) – Stage name to allocated run count mapping.

  • assumptions (list[str]) – Key assumptions underlying the recommendation.

  • risks (list[str]) – Risks and potential issues with the strategy.

  • alternative_strategies (list[str]) – Brief descriptions of alternative approaches.

  • domain (str) – The domain used for domain-specific adjustments.

  • detail_level (str) – The detail level used for explanations.

  • reasoning (list[str]) – Step-by-step explanation of the decision logic.

strategy_id: str#
stages: list[ExperimentalStage]#
total_estimated_runs: int#
budget_allocation: dict[str, int]#
assumptions: list[str]#
risks: list[str]#
alternative_strategies: list[str]#
domain: str#
detail_level: str#
reasoning: list[str]#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class process_improve.experiments.strategy.models.DOEProblemSpec(*, factors, responses=<factory>, budget=None, constraints=None, hard_to_change_factors=None, prior_knowledge=None, existing_data_summary=None, domain=DomainType.general, detail_level='intermediate')[source]#

Bases: BaseModel

Validated input specification for the strategy recommender.

Wraps all inputs into a single object for pipeline processing.

Parameters:
  • factors (list[Factor]) – All candidate experimental factors.

  • responses (list[Response]) – Response variables with optimisation goals.

  • budget (int or None) – Total run budget across all stages.

  • constraints (list[Constraint] or None) – Factor-space constraints.

  • hard_to_change_factors (list[str] or None) – Factor names that are expensive to reset between runs.

  • prior_knowledge (PriorKnowledge or None) – Parsed prior knowledge with confidence score.

  • existing_data_summary (dict or None) – Summary of any existing experimental data.

  • domain (DomainType) – Application domain.

  • detail_level (str) – "novice" or "intermediate".

factors: list[Factor]#
responses: list[Response]#
budget: int | None#
constraints: list[Constraint] | None#
hard_to_change_factors: list[str] | None#
prior_knowledge: PriorKnowledge | None#
existing_data_summary: dict[str, Any] | None#
domain: DomainType#
detail_level: Literal['novice', 'intermediate']#
property n_factors: int#

Total number of factors.

property factor_names: list[str]#

Ordered list of factor names.

property n_continuous: int#

Number of continuous factors.

property n_categorical: int#

Number of categorical factors.

property n_mixture: int#

Number of mixture factors.

property has_mixture: bool#

Whether any mixture factors are present.

property has_hard_to_change: bool#

Whether any hard-to-change factors are specified.

property has_constraints: bool#

Whether any constraints are specified.

property goal_includes_optimization: bool#

Whether any response has an optimisation goal.

model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Domain-specific strategy templates for DOE recommendations.

Each domain template provides preferred design choices, budget weight adjustments, and domain-specific advice. Templates are Python dicts (not YAML) because they encode algorithmic adjustments, not reference data.

Sources:
  • ICH Q8/Q9/Q10 for pharma QbD

  • Stat-Ease SCOR framework

  • NIST Engineering Statistics Handbook section 5.3.3

  • Montgomery, Design and Analysis of Experiments, 10th ed.

process_improve.experiments.strategy.domain_templates.get_domain_template(domain)[source]#

Return the domain template for the given domain string.

Parameters:

domain (str) – Domain key (e.g. "fermentation"). Falls back to "general" if the key is not recognised.

Returns:

The domain template dictionary.

Return type:

dict

Budget allocation logic for multi-stage DOE strategies.

Implements the 25-40-55-15 framework:
  • Screening: 25-40 % of total budget

  • Optimisation: 40-55 %

  • Confirmation: 5-15 % (minimum 3 runs)

Sources:
  • Montgomery, Design and Analysis of Experiments, 10th ed. (25% rule)

  • Stat-Ease SCOR framework

  • NIST Engineering Statistics Handbook section 5.3.3

process_improve.experiments.strategy.budget.estimate_screening_runs(n_factors, design_type)[source]#

Estimate the number of runs for a screening design.

Parameters:
  • n_factors (int) – Number of factors to screen.

  • design_type (str) – One of "plackett_burman", "definitive_screening", "fractional_factorial", "full_factorial".

Returns:

Estimated run count including center points.

Return type:

int

process_improve.experiments.strategy.budget.estimate_rsm_runs(n_factors, design_type, n_center_points=3)[source]#

Estimate the number of runs for an RSM design.

Parameters:
  • n_factors (int) – Number of factors (typically 2-5 after screening).

  • design_type (str) – One of "ccd", "box_behnken", "ccd_face_centered", "d_optimal".

  • n_center_points (int) – Number of center point replicates (default 3).

Returns:

Estimated run count.

Return type:

int

process_improve.experiments.strategy.budget.estimate_confirmation_runs(min_runs=3)[source]#

Return the number of confirmation runs.

Parameters:

min_runs (int) – Minimum confirmation runs (default 3).

Returns:

Confirmation run count (always at least 3).

Return type:

int

process_improve.experiments.strategy.budget.allocate_budget(total_budget, n_factors, needs_screening, needs_rsm, screening_design='plackett_burman', rsm_design='box_behnken', domain_weights=None, min_confirmation=3, n_center_points=3)[source]#

Allocate a total run budget across experimental stages.

Parameters:
  • total_budget (int or None) – Total runs across all stages. If None, computes an ideal budget.

  • n_factors (int) – Total number of candidate factors.

  • needs_screening (bool) – Whether a screening stage is needed.

  • needs_rsm (bool) – Whether an RSM optimisation stage is needed.

  • screening_design (str) – Preferred screening design type.

  • rsm_design (str) – Preferred RSM design type.

  • domain_weights (dict or None) – Stage-to-fraction mapping from the domain template.

  • min_confirmation (int) – Minimum confirmation runs (domain-dependent).

  • n_center_points (int) – Center points for RSM design.

Returns:

Keys: "screening", "optimization", "confirmation", "total", "ideal_total", "is_tight", "warnings".

Return type:

dict