Designed Experiments#

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

Bases: Series

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

Parameters:
  • dtype (Dtype | None)

  • copy (bool | None)

pi_index: bool | None = None#
pi_numeric: bool | None = None#
pi_lo: float | None = None#
pi_hi: float | None = None#
pi_range: tuple | None = None#
pi_center: float | None = None#
pi_is_coded: bool | None = None#
pi_units: str | None = None#
pi_name: str | None = None#
pi_levels: dict | None = None#
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 = None#
pi_title: str | None = None#
pi_units: dict | None = 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. A list, a tuple or a one-dimensional NumPy array is a column of values. Any other input raises a TypeError that names it (#677); it was dropped without a word before.

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=None, n_replicates=1, n_blocks=None, resolution=None, generators=None, alpha=None, cube='full', categorical_method='dsd', constraints=None, hard_to_change=None, model_type='interactions', fixed_runs=None, candidates=None, random_state=42, backend='auto', block_generators=None)[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. The names "RunOrder" and "Block" are reserved for the columns the design adds.

  • 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", "e_optimal", "g_optimal" (smallest worst-case prediction variance over the region), "k_optimal" (best-conditioned X'X), "mixture", "taguchi", "supersaturated", and the space-filling types "latin_hypercube", "maximin_lhs", "uniform", "sobol", "halton", "maximin" and "maxpro" (budget runs, default 10 * k). If None, the design type is chosen automatically based on the factor count, budget, and constraints, so that the design, centre points and replicates included, fits within budget (a warning is logged when no design does). resolution or generators choose a fractional factorial. An automatically chosen fractional factorial for six or more factors is the smallest one of resolution IV, and an automatically chosen D-optimal design is built for the largest model, up to model_type, that budget can estimate.

  • budget (int or None) – Maximum number of runs the experimenter can afford, centre points and replicates included. The optimal, mixture, space-filling and supersaturated designs, and OMARS with a budget, are built to it (OMARS to the largest foldover size within it). A fractional factorial without generators is the minimum-aberration fraction of the largest power of two of runs that fits, at most the half fraction, with a UserWarning when that is fewer runs than the budget leaves; with resolution as well, it must reach it. A family whose size the design structure fixes (full factorials, a fraction from generators, Plackett-Burman, Box-Behnken, CCD, Taguchi) raises ValueError when it needs more runs than budget. A DSD with a budget above its minimal size gains fake factors (error degrees of freedom); for an odd number of factors (5, 7, 9 or 11, all continuous), a budget of 2k + 1 or 2k + 2 runs gives Jones and Nachtsheim’s (2011) design of 2k + 1 runs, two fewer than the default, whose main effects are slightly correlated.

  • n_center_points (int, str or None) – Number of centre runs in the whole design: they are not replicated with n_replicates. None (the default) means 3 for the full and fractional factorials, Plackett-Burman, CCD and Box-Behnken designs, and the design’s own centre runs for the others. A design whose factors are all categorical has no centre, so None gives it none: a 3 x 3 factorial has 9 runs. Give a number to add runs to it anyway. CCD and Box-Behnken designs place them within the design structure. For a DSD or OMARS design it is the total number of centre runs, at least the one the design has (two for a DSD with categorical factors). A DSD or OMARS design that leaves the full second-order model no error degrees of freedom (the default for an even number of factors) warns, since analyze_omars() could not test its effects; extra centre runs fix that. Any other design type takes no centre points, so a positive value raises ValueError. For a CCD, "uniform_precision" or "orthogonal" sets the number by Box and Hunter’s rule for a rotatable design in one block (ccd_center_points()), and makes the design rotatable: four factors get 7 or 12 centre runs.

  • n_replicates (int) – Number of replicates of the design’s runs, centre runs excepted (default 1 = no replication): a 2^3 factorial with n_center_points=3 and n_replicates=2 has 16 factorial runs and 3 centre runs. A CCD’s orthogonal axial distance allows for it. Cannot be combined with fixed_runs.

  • n_blocks (int or None) – Number of blocks the runs are made in (days, batches, raw-material lots), with a Block column on the design. A regular two-level factorial confounds chosen interactions with the blocks. A CCD puts its axial runs in a block of their own and its cube runs in the others (n_blocks 2, 3, 5, …), the cube split by confounding as a factorial is; without alpha it takes the axial distance at which the blocks are orthogonal to every model term, squares included (alpha_rule "orthogonal_blocks"; Box and Hunter 1957), and metadata["blocking"]["orthogonal"] says whether they are. A D-, A- or I-optimal design from the built-in exchange chooses its runs for the model with the blocks as fixed effects, so the factor effects are estimated as precisely as they can be once block-to-block differences are removed (metadata["blocking"]["method"] is "optimal_exchange"); with fixed_runs, those runs are the first block, and the new runs fill the others. Any other design is split into near-equal blocks after it is built.

  • resolution (int or None) – Desired minimum resolution for fractional factorials (III=3, IV=4, V=5), and for the cube of a CCD with cube="fractional". The design is the minimum-aberration fraction with the fewest runs that reaches it. Without resolution or generators, a fractional factorial is the half fraction 2^(k-1), of resolution k. The result reports the resolution the design achieves. With generators, generators that fall short of it raise ValueError. Any other design type raises ValueError.

  • generators (list[str] or None) – Explicit generators for fractional factorials (and the cube of a CCD with cube="fractional"), e.g. ["D=ABC", "E=AC"]; "D=-ABC" gives the alternate fraction. When every factor has three levels (a continuous factor with three levels, or a categorical factor with three), the generators make a 3^(k-p) fraction in arithmetic modulo 3: "D=A2BC2" sets x_D = 2 x_A + x_B + 2 x_C (mod 3), the digits 0, 1 and 2 being each factor’s levels from the lowest (see designs_three_level). Any other design type raises ValueError.

  • alpha (str, float, or None) – Axial distance for CCD designs: "rotatable", "face_centered", "inscribed", "orthogonal" (the default), or a positive number. Any other value raises ValueError.

  • cube (str) – For CCD designs, how to build the cube (factorial) portion: "full" (default) uses the complete 2^k factorial; "fractional" uses a fractional factorial, keeping the run count practical for k >= 5. When "fractional" and generators or resolution is given, they define the cube; otherwise it is the minimum-aberration half-fraction, which must leave the full quadratic model estimable (resolution V, or for three factors Hartley’s small composite design). A cube given that aliases two-factor interactions in pairs is built with a warning, for a model with one interaction of each pair; metadata["aliased_interactions"] lists the pairs.

  • categorical_method ({"dsd", "orth"}) – For a DSD with two-level categorical factors, how they are added (Jones and Nachtsheim 2013): "dsd" (default), the DSD-augment method, two centre runs and a little correlation between a categorical and the other main effects; or "orth", the ORTH-augment method, every main effect orthogonal to every other (up to four categorical factors) but partly aliased with some second-order effects, in the same runs for one categorical factor and two more centre runs for more. Any other design type refuses "orth".

  • constraints (list[Constraint] or None) – Inequalities on the continuous factors, in actual units, e.g. Constraint(expression="3*T + 5*D <= 600"); a dict of Constraint fields or a bare expression string is accepted too. Honoured by the optimal families ("d_optimal", chosen automatically when constraints are given, "i_optimal", "a_optimal", "e_optimal", "g_optimal" and "k_optimal"), whose runs are selected from a candidate set of feasible points; by "mixture" (linear constraints in the proportions); and by the "sobol", "halton", "maximin" and "maxpro" space-filling designs. Other design types do not enforce them and set metadata["constraints_enforced"] = False.

  • hard_to_change (list[str] or None) – Names of hard-to-change factors, for a split-plot design: they are set once for each whole plot, and the design is D-, I- or A-optimal on the generalised least squares information X'V^-1 X, with the whole-plot variance half the run-to-run variance. Only the optimal families build one, so any other design_type raises ValueError; with design_type unset, a D-optimal design is chosen.

  • model_type (str) – Model the optimal designs ("d_optimal", "i_optimal", "a_optimal", "e_optimal", "g_optimal", "k_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", "e_optimal", "g_optimal", "k_optimal"). 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.

  • candidates (pandas.DataFrame or None) – The settings the runs must be chosen from, for the optimal families only: for example historical operating points, the discrete settings a piece of equipment offers, or blends that can actually be made up. One row per candidate, one column per factor, in actual units (categorical factors as labels, mixtures as proportions). Replaces the generated candidate grid, so it also defines the region I-optimality averages over. Rows that break a constraint are dropped. A candidate may be chosen more than once, and metadata["selected_candidates"] counts the picks per index label.

  • random_state (int, numpy.random.Generator or None) – Seeds the run-order randomisation and any random search (default 42, so the same call gives the same design). None draws fresh entropy; see Reproducibility Contract (RNG Handling).

  • backend ({"auto", "exchange", "pyoptex"}) – Engine for the optimal families. "auto" (default) and "exchange" use the built-in engines, so the same call gives the same design whether or not the optional pyoptex package is installed: the coordinate exchange of split_plot_design() for a split-plot design (hard_to_change), the candidate exchange otherwise. "pyoptex" asks for pyoptex’s coordinate exchange.

  • block_generators (list[str] or None) – The interaction words that define the blocks of a two-level full or fractional factorial, in place of the words the library would choose: ["ADE", "BCE"] makes four blocks, with ADE, BCE and their product ABCD confounded with them (join names longer than one letter with *). n_blocks, when given, must be 2 to the power of their number. Words that confound a main effect, depend on each other, or lie in the fraction’s defining relation are refused. For a fraction, metadata["blocking"]["aliases"] lists each confounded word’s aliases, which go with it to the blocks.

Returns:

Contains design (coded Expt), design_actual (actual-units Expt), run_order, and design metadata (generators, defining relation, resolution, etc.). generators, defining_relation and resolution describe the design built, and are None for a design without a defining relation.

Return type:

DesignResult

Raises:

ValueError – If design_type is unknown, if factor/budget constraints cannot be satisfied, or if an argument is given that the design type cannot honour.

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

Factor, Constraint, Response, and DesignResult definitions for design generation.

class process_improve.experiments.factor.FactorType(value)[source]#

Bases: str, Enum

Type of experimental factor.

class process_improve.experiments.factor.Factor(*, name, type=FactorType.continuous, low=None, high=None, levels=None, units='', hard_limits=False)[source]#

Bases: BaseModel

Specification for a single experimental factor.

Parameters:
  • name (str) – Name of the factor (e.g. “Temperature”, “Pressure”).

  • type (FactorType) – One of “continuous”, “categorical”, or “mixture”.

  • low (float or None) – Low level for continuous/mixture factors: finite, and below high. A mixture component’s bounds are proportions, 0 <= low < high <= 1 (default 0 and 1).

  • high (float or None) – High level for continuous/mixture factors.

  • levels (list or None) – Explicit levels for categorical factors (distinct, none missing), or for continuous factors with more than 2 levels.

  • units (str) – Engineering units (e.g. “degC”, “bar”, “g/L”).

  • hard_limits (bool) – Whether low and high are limits of the process, not only the range studied: no run may set the factor past them (a safety limit, an instrument’s range, a rule of a lab). Designs and augmentations then keep every run inside them, a strategy plans none that steps outside, and the next round never moves or widens the range. Default False.

Examples

>>> Factor(name="Temperature", low=150, high=200, units="degC")
>>> Factor(name="Pressure", low=1, high=3, units="bar", hard_limits=True)
>>> Factor(name="Catalyst", type="categorical", levels=["A", "B", "C"])
>>> Factor(name="x1", type="mixture", low=0.0, high=1.0)
property center: float | None#

Return the center point value, or None for categorical factors.

property range: tuple[float, float] | None#

Return the (low, high) range tuple, or None for categorical factors.

classmethod from_data(data, *, name=None, type=FactorType.continuous, units='', low=None, high=None, levels=None)[source]#

Create a Factor from a column of historical / real-world data.

Parameters:
  • data (array-like or pandas.Series) – Observed values for the factor.

  • name (str or None) – Factor name. If None, the name attribute of data (e.g. a pandas Series column name) is used; an error is raised if neither is available.

  • type (FactorType or str) – One of “continuous”, “categorical”, or “mixture”. Default “continuous”.

  • units (str) – Engineering units.

  • low (float or None) – Low/high levels for continuous or mixture factors. If not given, they are inferred from the minimum and maximum of data.

  • high (float or None) – Low/high levels for continuous or mixture factors. If not given, they are inferred from the minimum and maximum of data.

  • levels (list or None) – Explicit levels for categorical factors. If not given, the unique values of data (in order of first appearance) are used.

Returns:

A validated Factor instance.

Return type:

Factor

Examples

>>> Factor.from_data(df["Temperature"], type="continuous")
>>> Factor.from_data(df["Catalyst"], type="categorical")
>>> Factor.from_data(df["Pressure"], type="continuous", low=4, high=8)
model_config = {}#

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

class process_improve.experiments.factor.Constraint(*, expression, type='linear')[source]#

Bases: BaseModel

A constraint on the experimental factor space.

Parameters:
  • expression (str) – A string expression, e.g. "A + B <= 1.0" or "3*T + 5*D <= 600". Factor names in the expression must match the name fields of the corresponding Factor objects.

  • type (str) – Either "linear" or "nonlinear". Informational only: the design engines classify each expression themselves, so the label does not change how the constraint is enforced.

Examples

>>> Constraint(expression="A + B <= 1.0")
>>> Constraint(expression="3*T + 5*D <= 600", type="linear")
model_config = {}#

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

process_improve.experiments.factor.as_constraints(constraints)[source]#

Accept constraints as Constraint objects, their dict form or bare expressions.

The dict form is what a JSON round trip of a Constraint gives, for example the design_params of a stage from recommend_strategy.

Parameters:

constraints (Sequence[Constraint | dict | str] or None) – The constraints in any of the three forms, mixed freely.

Returns:

One Constraint per item, in order; None when constraints is None.

Return type:

list[Constraint] or None

Raises:

TypeError – For an item that is none of the three forms, or a single limit passed on its own instead of in a list (a string is itself a sequence, of its characters).

class process_improve.experiments.factor.ResponseGoal(value)[source]#

Bases: str, Enum

Goal direction for a response variable.

class process_improve.experiments.factor.Response(*, name, goal=ResponseGoal.maximize, target=None, low=None, high=None, units='', importance=1.0)[source]#

Bases: BaseModel

Specification for a response variable in a DOE study.

Parameters:
  • name (str) – Name of the response (e.g. “Yield”, “Purity”, “Cost”).

  • goal (ResponseGoal) – Optimisation direction: "maximize", "minimize", or "target".

  • target (float or None) – Target value (required when goal="target").

  • low (float or None) – Lower acceptable bound for desirability calculations.

  • high (float or None) – Upper acceptable bound for desirability calculations.

  • units (str) – Engineering units (e.g. “%”, “mg/L”, “min”).

  • importance (float) – Relative importance weight for multi-response optimisation (default 1.0).

Examples

>>> Response(name="Yield", goal="maximize", low=60, high=95, units="%")
>>> Response(name="Cost", goal="minimize", low=10, high=50, units="USD")
>>> Response(name="pH", goal="target", target=7.0, low=6.5, high=7.5)
model_config = {}#

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

class process_improve.experiments.factor.DesignResult(design, design_actual, run_order, design_type, n_runs, n_factors, factor_names, generators=None, defining_relation=None, resolution=None, alpha=None, blocks=None, metadata=<factory>)[source]#

Bases: object

Container for a generated experimental design and its metadata.

Parameters:
  • design (Expt) – Design matrix in coded units (-1 / +1).

  • design_actual (Expt) – Design matrix in actual (real-world) units.

  • run_order (list[int]) – The standard-order position (1-based) of each run. design and design_actual are already in randomised run order, so run_order[i] is where their i-th row sits in the unrandomised design; sorting the rows on it recovers the standard order.

  • design_type (str) – Name of the design that was generated.

  • n_runs (int) – Total number of experimental runs.

  • n_factors (int) – Number of factors in the design.

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

  • generators (list[str] or None) – Generator strings for fractional factorials (e.g. ["E=ABC"]).

  • defining_relation (list[str] or None) – Defining relation words (e.g. ["I=ABCE"]).

  • resolution (int or None) – Resolution of the design (III, IV, V, …).

  • alpha (float or None) – Axial distance used for CCD designs.

  • blocks (list[int] or None) – Block assignment for each run.

  • metadata (dict[str, Any]) – Additional design-specific information.

Screening designs: fractional factorial, Plackett-Burman, Taguchi.

All functions accept a list of Factor objects and return a raw coded numpy array. Post-processing (center points, replication, randomization, Column/Expt conversion) is handled by designs_utils.build_design_result.

process_improve.experiments.designs_screening.dispatch_fractional_factorial(factors, resolution=None, generators=None, n_runs=None)[source]#

Generate a 2-level fractional factorial design.

Parameters:
  • factors (list[Factor]) – Continuous factors (all treated as 2-level).

  • resolution (int or None) – Desired minimum resolution (3 or more). The design is the minimum-aberration fraction with the fewest runs that reaches it, so its resolution can be higher. When neither resolution nor generators is given, the design is the half fraction 2^(k-1), of resolution k. With generators, it is checked instead: generators that reach a lower resolution raise ValueError.

  • generators (list[str] or None) – Explicit generator strings, e.g. ["D=ABC", "E=AC"]. When given, these are translated into fracfact generator notation.

  • n_runs (int or None) – Runs of the fraction, a power of two: the minimum-aberration fraction of that size (#956). With resolution, the fraction must reach it. Ignored with generators.

Returns:

Coded design matrix (-1 / +1) and metadata dict with keys "generators_used", "defining_relation" and "resolution", the resolution the design achieves. A word of the defining relation that is -1 on every run (from a negated generator such as "D=-AB") is written "I=-ABD". A defining relation of more than 1023 words (more than ten generators) is not listed: "defining_relation_note" says so, and defining_relation() lists it.

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – If fewer than 3 factors are given without generators, if resolution is below 3 or above the number of factors, if generators or the n_runs fraction fall short of resolution, or if no minimum-aberration fraction of n_runs runs is tabulated or quick to search.

process_improve.experiments.designs_screening.defining_relation(design)[source]#

Return every word of a two-level fraction’s defining relation, however many generators it has.

A design lists its defining relation only up to ten generators (1023 words), because the relation doubles with each generator: a saturated 2^(15-11) design has 2047 words. This function works them out from the design’s generators on request, in the design’s own form: shortest first, each as "I=ABD", or "I=-ABD" where the word is -1 on every run.

Parameters:

design (DesignResult) – A two-level fraction, or a design built on one (such as a composite design on a fractional cube), from generate_design().

Returns:

The words of the defining relation; empty for a full factorial.

Return type:

list[str]

Raises:

ValueError – If the design has no generators and is not a full factorial, so it has no defining relation (a Plackett-Burman or optimal design, for example).

Examples

>>> from process_improve.experiments import Factor, generate_design
>>> factors = [Factor(name=name, low=-1, high=1) for name in "ABCDEFGHJKLMNOP"]
>>> design = generate_design(factors, design_type="fractional_factorial", resolution=3)
>>> design.n_runs, design.defining_relation
(19, None)
>>> relation = defining_relation(design)
>>> len(relation), relation[0]
(2047, 'I=ABE')
process_improve.experiments.designs_screening.plackett_burman_runs(n_factors)[source]#

Return the number of runs in the Plackett-Burman design for n_factors factors.

The smallest multiple of 4 above n_factors for which a Hadamard matrix is built here: every order up to 232. The first gaps are 236, 260 and 268, which step up to the next multiple of 4.

Parameters:

n_factors (int)

Return type:

int

process_improve.experiments.designs_screening.dispatch_plackett_burman(factors)[source]#

Generate a Plackett-Burman screening design.

The N-run design is k columns of a normalised Hadamard matrix of order N, the smallest multiple of 4 above k that can be built (see plackett_burman_runs()). Where N - 1 is a prime 3 mod 4 (12, 20, 24, 44, 48, … runs) the design is Paley’s cyclic one, which is what Plackett and Burman (1946) printed for 12, 20, 24 and 44 runs; for 36 runs it is their own generating row. Powers of 2 are the classical construction (Sylvester’s doubling), and the other orders (28, 40, 52, …, and the Goethals-Seidel arrays at 92, 116, 156, 172 and 188) come from the constructions in process_improve.experiments._finite_fields, checked against H'H = N I. A doubled design is used only where no cyclic one is built: it aliases some products of three columns completely with the intercept. A UserWarning names both run counts when N is above the smallest multiple of 4 above k because that order is not built here.

Parameters:

factors (list[Factor]) – Continuous factors.

Returns:

Coded design matrix (-1 / +1) and metadata, including the construction.

Return type:

tuple[np.ndarray, dict]

process_improve.experiments.designs_screening.dispatch_taguchi(factors)[source]#

Generate a Taguchi orthogonal-array design.

Uses the smallest of the standard arrays that has a column for every factor: two levels for a continuous factor, the number of its levels for a categorical factor or a continuous factor given explicit levels. Each factor takes its own column (one with its level count, or else one whose levels divide evenly into its own, as a 6-level column carries a 3-level factor); the other columns are left unused. When that leaves a factor without a column, a column whose level count is a product of two factors’ counts carries both, as a digit each of its level: the 6-level column of L18(6^1 3^6) is the 2-level and 3-level columns of Taguchi’s L18 for one 2-level and seven 3-level factors. The chosen columns are checked to be balanced in every pair (strength 2) before use. No centre points are added.

Parameters:

factors (list[Factor]) – Continuous factors (two levels, or the values in levels) and categorical factors.

Returns:

Coded design matrix (-1 / +1 for two levels, equally spaced codes otherwise) and metadata with the orthogonal_array and the array columns used, a column named twice when two factors share it.

Return type:

tuple[np.ndarray, dict]

Response surface designs: CCD, Box-Behnken, Definitive Screening Design.

All functions accept a list of Factor objects and return a raw coded numpy array. Post-processing is handled by designs_utils.build_design_result.

process_improve.experiments.designs_response_surface.dispatch_ccd(factors, n_center_points=3, alpha=None, cube='full', generators=None, resolution=None, n_replicates=1, n_blocks=None)[source]#

Generate a Central Composite Design (CCD).

Parameters:
  • factors (list[Factor]) – Continuous factors.

  • n_center_points (int) – Number of center points (split between cube and axial portions).

  • alpha (str, float, or None) – Axial distance: "rotatable" (F ** 0.25 for F cube runs), "face_centered" (1), "inscribed" (axial runs at +/-1, the cube shrunk inside a rotatable design), "orthogonal" (the default: the distance that makes the quadratic columns mutually orthogonal, see orthogonal_alpha()), or a positive number used as the distance itself. Any other value raises ValueError.

  • cube (str) – How to build the cube (factorial) portion: "full" (default) uses the complete 2^k factorial; "fractional" uses a fractional factorial, keeping the run count practical for k >= 5. By default it must leave the full quadratic model estimable: of resolution V, or a small composite design whose cube aliases interactions only with main effects (Hartley, 1959). A cube given by generators or resolution that aliases interactions in pairs is built with a warning; full_quadratic_estimable and aliased_interactions in the metadata say what it can estimate.

  • generators (list[str] or None) – Explicit cube generators (e.g. ["E=ABCD"]), used only when cube="fractional". When omitted, a minimum-aberration half-fraction is chosen automatically.

  • resolution (int or None) – Desired minimum cube resolution, used only when cube="fractional" and generators is not given.

  • n_replicates (int) – How many times the cube and axial runs will be replicated (the centre runs are not), which the orthogonal axial distance depends on.

  • n_blocks (int or None) – Blocks the design will be run in: the axial runs in one, the cube runs in the others. Without alpha, the axial distance is then the one that blocks the design orthogonally (blocking_alpha()), recorded as alpha_rule "orthogonal_blocks".

Returns:

Coded design matrix and metadata: alpha_value, the ratio of the axial distance to the cube’s half-width (for the inscribed design, whose cube is shrunk, the axial runs sit at +/-1 and the cube at +/-1/alpha); face, the geometry ("circumscribed", "faced" or "inscribed"); and alpha_rule, how alpha was chosen ("orthogonal", "rotatable", "face_centered", "inscribed", "orthogonal_blocks" or "user"); and axial_portion, which rows are the axial runs and their centre runs.

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – If fewer than two factors are given, or alpha or cube is not recognised.

Notes

Center points are embedded in the CCD structure itself (via ccdesign’s center parameter). The caller should set n_center_points=0 in build_design_result to avoid adding duplicate center points.

process_improve.experiments.designs_response_surface.orthogonal_alpha(n_cube_runs, n_runs, n_axial_replicates=1)[source]#

Axial distance that makes the quadratic columns of a central composite design mutually orthogonal.

alpha = (F * (sqrt(N) - sqrt(F)) ** 2 / (4 * s ** 2)) ** (1 / 4) for F cube runs, each axial point run s times, and N runs in all, centre runs included (Box and Hunter 1957; Myers, Montgomery and Anderson-Cook, Response Surface Methodology, section 7.4, for s = 1). With it the centred squared columns are orthogonal, so the quadratic coefficients are estimated independently of one another.

The squares of two factors are both non-zero only on the cube runs, so their product sums to F; each has the sum F + 2 s alpha**2; the centred columns are orthogonal when F N = (F + 2 s alpha**2) ** 2. A design replicated whole (F, s and N all times r) keeps its alpha; one whose centre runs are not replicated has relatively fewer of them, and a smaller alpha.

Parameters:
  • n_cube_runs (int) – Runs in the cube (factorial) portion, full or fractional, replicates included.

  • n_runs (int) – Every run of the design: cube, axial and centre runs.

  • n_axial_replicates (int) – How many times each of the 2k axial points is run.

Returns:

The axial distance in coded units.

Return type:

float

process_improve.experiments.designs_response_surface.CENTER_POINT_RULES = ('uniform_precision', 'orthogonal')#

Box and Hunter’s (1957) rules for the number of centre runs of a rotatable central composite design.

process_improve.experiments.designs_response_surface.ccd_center_points(rule, n_cube_runs, n_factors, n_replicates=1)[source]#

Return the number of centre runs of a rotatable central composite design by Box and Hunter’s rule.

In a rotatable CCD, with alpha = F ** 0.25 for F cube runs, the variance of a prediction depends only on its distance from the centre. The centre runs set how it changes with that distance, through the design’s pure fourth moment

\[\lambda_4 = \frac{N F}{r\,(F + 2\sqrt{F})^2},\]

with N runs in all, of which r (F + 2k) are the cube and axial runs (the design run r times, its centre runs once) for k factors (Box and Hunter 1957; Myers, Montgomery and Anderson-Cook, Response Surface Methodology, section 7.4).

  • "uniform_precision": the variance at the centre equals the variance one unit from it, in Box and Hunter’s unit of distance: the root mean square of a factor over the runs. That needs \(\lambda_4 = (k + 3 + \sqrt{9k^2 + 14k - 7}) / (4(k + 2))\).

  • "orthogonal": \(\lambda_4 = 1\), which makes the centred squared columns orthogonal, so the quadratic coefficients are estimated independently of one another. The number is then \(r(4\sqrt{F} + 4 - 2k)\).

Parameters:
  • rule (str) – "uniform_precision" or "orthogonal".

  • n_cube_runs (int) – F, the runs of one copy of the cube, full or fractional.

  • n_factors (int) – k, the number of factors.

  • n_replicates (int) – r, how many times the cube and axial runs are run.

Returns:

The number of centre runs, rounded to the nearest whole run and at least 1.

Return type:

int

Raises:

ValueError – If rule is not one of CENTER_POINT_RULES.

Examples

Box and Hunter’s table for four factors, 16 cube runs:

>>> ccd_center_points("uniform_precision", 16, 4), ccd_center_points("orthogonal", 16, 4)
(7, 12)
process_improve.experiments.designs_response_surface.blocking_alpha(n_cube_runs, n_center_cube, n_factors, n_center_axial, n_axial_replicates=1)[source]#

Axial distance at which a CCD’s cube and axial blocks are orthogonal to the squares too.

alpha**2 = F (2 k s + c_a) / (2 s (F + c_c)) for F cube runs and c_c centre runs in the cube blocks, k factors whose 2k axial points are each run s times, and c_a centre runs in the axial block (Box and Hunter 1957; Montgomery, Design and Analysis of Experiments, section 11.4). Each block then holds the same share of every factor’s sum of squares as of the runs. With two factors, a 2^2 cube and three centre runs in each block it is sqrt(2), rotatable too.

Parameters:
  • n_cube_runs (int) – Runs in the cube portion, replicates included.

  • n_center_cube (int) – Centre runs in the cube blocks.

  • n_factors (int) – Number of factors k.

  • n_center_axial (int) – Centre runs in the axial block.

  • n_axial_replicates (int) – How many times each axial point is run.

Returns:

The axial distance in coded units.

Return type:

float

process_improve.experiments.designs_response_surface.dispatch_box_behnken(factors, n_center_points=3)[source]#

Generate a Box-Behnken design.

Three to five factors: a two-level factorial in every pair of factors (bbdesign), which is the published design. Six, seven, nine, ten, eleven, twelve and sixteen factors: the published blocks in _BOX_BEHNKEN_BLOCKS (48, 56, 120, 160, 176, 192 and 384 runs plus centre points). Box and Behnken published no design for eight, thirteen to fifteen, or more than sixteen factors: these pair every two factors, and metadata["construction"] says "all_pairs".

Parameters:
  • factors (list[Factor]) – Continuous factors (requires at least 3).

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

Returns:

Coded design matrix (-1 / 0 / +1) and metadata with the construction.

Return type:

tuple[np.ndarray, dict]

Notes

Center points are embedded in the BB structure. The caller should set n_center_points=0 in build_design_result.

References

Box, G. E. P. and Behnken, D. W. (1960). Some new three level designs for the study of quantitative variables. Technometrics, 2(4), 455-475.

process_improve.experiments.designs_response_surface.dsd_conference_order(n_factors)[source]#

Return the order of the conference matrix a definitive screening design for n_factors factors is built from.

n_factors itself when it is even and n_factors + 1 when it is odd, stepped up to the next order where a conference matrix can be built: none exists at 22 or 34, so 21 and 22 factors use order 24, and 33 and 34 factors use order 38 (36 needs a construction not implemented here). The extra columns are fake factors, dropped from the design.

Raises:

ValueError – For fewer than 3 factors.

Parameters:

n_factors (int)

Return type:

int

process_improve.experiments.designs_response_surface.dsd_run_count(n_factors, n_categorical=0, categorical_method='dsd')[source]#

Return the number of runs in the definitive screening design for n_factors factors.

2m + 1 for the conference order m of dsd_conference_order(): the familiar 2k + 1 for even k and 2k + 3 for odd k, except where the order steps up. Two-level categorical factors (counted in n_factors) need two centre runs instead of one (Jones and Nachtsheim 2013), and four with the ORTH-augment method (categorical_method="orth") when there are two or more of them.

Examples

>>> dsd_run_count(6), dsd_run_count(7), dsd_run_count(22), dsd_run_count(6, n_categorical=2)
(13, 17, 49, 14)
>>> dsd_run_count(6, n_categorical=2, categorical_method="orth")
16
Parameters:
  • n_factors (int)

  • n_categorical (int)

  • categorical_method (str)

Return type:

int

process_improve.experiments.designs_response_surface.CATEGORICAL_METHODS = ('dsd', 'orth')#

"dsd", the DSD-augment method, or "orth", the ORTH-augment method.

Type:

How a DSD takes two-level categorical factors (Jones and Nachtsheim 2013)

process_improve.experiments.designs_response_surface.dispatch_dsd(factors, budget=None, categorical_method='dsd')[source]#

Generate a Definitive Screening Design (DSD).

The conference-matrix construction of Jones and Nachtsheim (2011), as given by Xiao, Lin and Bai (2012): with C a conference matrix of order m (C'C = (m - 1) I), the design is [C; -C; 0] with 2m + 1 runs, and its first k columns are the factors. m is k for even k and k + 1 for odd k, stepped up where no conference matrix of that order can be built (see dsd_conference_order()). Main effects are then orthogonal to each other and to every quadratic and two-factor interaction column.

Conference matrices come from process_improve.experiments._finite_fields: Paley’s construction over GF(q) for every prime power q = m - 1 (including 9, 25, 27, 49) and the doubling of an antisymmetric matrix (orders 16, 40, 56, …). Every matrix is checked against C'C = (m - 1) I before use; no approximate matrix is ever used.

Two-level categorical factors use the DSD-augment method of Jones and Nachtsheim (2013): a categorical factor takes a conference-matrix column like any other, its two zeros (one in each half of the foldover) become +z and -z, and the single centre run becomes two, with the categorical factors at +b and -b. The signs are chosen to maximise the determinant of the main-effects information matrix. Every main effect stays orthogonal to every second-order effect, and the continuous main effects stay orthogonal to each other. Resolving the zeros costs some orthogonality between a categorical main effect and the other main effects: it is slightly correlated with each continuous main effect (|r| about 0.17 for four continuous factors and one categorical factor in 14 runs) and with the other categorical main effects.

The ORTH-augment method (categorical_method="orth", Jones and Nachtsheim 2013, Section 3) keeps every main effect orthogonal to every other instead, in the same runs for one categorical factor and two more runs for two to four. The price is that the main effects are no longer clear of the second-order effects: each is partly aliased with some quadratic effects and two-factor interactions (the paper’s Table 5). A categorical factor’s two zeros both become +1, which leaves it orthogonal to the other columns of the foldover but adds 2 to its sum; centre runs balance it. One categorical factor takes two centre runs at -1. Two to four take four, whose categorical settings are the columns of a 4 x 4 block with one +1 in each column and each row, orthogonal to each other. More than four reuse the block’s columns in turn, so a categorical factor is correlated only with those that share its column: 2 n_B (c - 2 n_B - 2) nonzero off-diagonal entries of the main-effects information matrix for c categorical factors, n_B = c // 4.

A budget larger than the minimal design adds fake factors: the conference matrix of the largest buildable order whose design fits the budget is used, and its extra columns are left out. Jones and Nachtsheim (2017) recommend this, because the unused columns give error degrees of freedom free of any second-order effect, which the analysis of a DSD (process_improve.experiments.analyze_omars()) needs.

A budget of 2k + 1 or 2k + 2 runs for an odd k of 5, 7, 9 or 11 continuous factors, too small for the conference-matrix design, gives Jones and Nachtsheim’s (2011) design of 2k + 1 runs instead (their Figure 1). It saves two runs; its main effects are still orthogonal to every second-order effect, but correlated with each other (|r| up to 0.25, 0.167, 0.125 and 0.3), and it has no fake factors, so the full second-order model leaves it no error degrees of freedom. The metadata’s note says so.

Parameters:
  • factors (list[Factor]) – At least three factors: continuous, or categorical with exactly two levels.

  • budget (int or None) – Largest number of runs. None gives the minimal design.

  • categorical_method ({"dsd", "orth"}) – How two-level categorical factors are added: DSD-augment (the default) or ORTH-augment.

Returns:

Coded design matrix (categorical factors at -1 / +1) and metadata: construction, conference_order, fake_factors and, with categorical factors, n_categorical and categorical_method. Without a budget, when the order steps up (21, 22, 33 or 34 factors), a note says so and a UserWarning gives the same text.

Return type:

tuple[np.ndarray, dict]

References

  • Jones, B. and Nachtsheim, C. J. (2011). “A class of three-level designs for definitive screening in the presence of second-order effects.” Journal of Quality Technology, 43(1):1-15.

  • Xiao, L., Lin, D. K. J. and Bai, F. (2012). “Constructing definitive screening designs using conference matrices.” Journal of Quality Technology, 44(1):2-8.

  • Jones, B. and Nachtsheim, C. J. (2013). “Definitive screening designs with added two-level categorical factors.” Journal of Quality Technology, 45(2):121-129.

  • Jones, B. and Nachtsheim, C. J. (2017). “Effective design-based model selection for definitive screening designs.” Technometrics, 59(3):319-329.

Optimal designs: D-optimal, I-optimal, A-optimal, E-optimal.

Two engines, chosen per request by _dispatch_optimal():

  • pyoptex (coordinate exchange), when installed and no constraints are given. It also builds split-plot designs for hard_to_change factors. pyoptex is not a process-improve extra because it pins plotly~=5.24 (< 6), which conflicts with this project’s plotly>=6.5.2; install it separately (pip install pyoptex) in its own environment.

  • The built-in candidate exchange in designs_constrained.py otherwise: a Fedorov exchange over a grid of candidate points for D-, I-, A- or E-optimality, with constraints, categorical factors and fixed runs, but no split-plot structure.

pyoptex’s continuous factors take the levels {-1, 0, 1}, or five levels from -1 to 1 under a quadratic model.

Mixture factors always use designs_mixture_constrained.py, and mixture components together with process factors designs_mixture_process.py.

process_improve.experiments.designs_optimal.dispatch_d_optimal(factors, budget=None, hard_to_change=None, constraints=None, model_type='interactions', fixed_runs=None, random_state=None, candidates=None, backend='auto', n_blocks=None)[source]#

Generate a D-optimal design (maximises det(X'X), the precision of the coefficients jointly).

Parameters:
  • factors (list[Factor]) – Factor specifications. All-mixture factors get a Scheffé-model design over the (possibly constrained) simplex.

  • budget (int or None) – Number of runs. Defaults to 2 * n_factors + 1, or with fixed_runs to enough runs beyond them to estimate the model. A budget below the model’s number of coefficients raises a ValueError, and so does one that leaves too few runs for the block effects (n_blocks) or for the rank fixed_runs lack.

  • hard_to_change (list[str] or None) – Names of hard-to-change factors: a split-plot design, found by the built-in coordinate exchange on the GLS information (see split_plot_design()), or by pyoptex with backend="pyoptex". A name that is not a factor raises ValueError. Ignored, with a warning, and recorded as hard_to_change_ignored, when the design is built by the candidate exchange: with constraints, a candidate set, fixed runs (unless backend="pyoptex"), blocks, or the E-, G- or K-criterion.

  • constraints (list[Constraint] or None) – Inequalities in actual units over the continuous factors, e.g. "3*T + 5*D <= 600". Enforced: every run the optimizer places satisfies them. Fixed runs outside the region are kept as given, with a warning, and counted in metadata["n_fixed_runs_outside_region"].

  • model_type (str) – Model assumption: "main_effects", "interactions", or "quadratic".

  • fixed_runs (pd.DataFrame or None) – Runs to hold fixed while the optimizer fills the rest (design augmentation). Occupies the first rows of the returned design and counts towards budget.

  • random_state (int, numpy.random.Generator or None) – Seed for the candidate exchange’s random starts.

  • candidates (pd.DataFrame or None) – Settings to choose the runs from, in actual units (proportions for a mixture), one column per factor. Replaces the generated grid; rows breaking a constraint are dropped. metadata["selected_candidates"] counts how often each row (by index label) was picked.

  • backend ({"auto", "exchange", "pyoptex"}) – "auto" and "exchange" use the built-in engines: the coordinate exchange for a split-plot design, the candidate exchange otherwise. "pyoptex" asks for pyoptex.

  • n_blocks (int or None) – Blocks the design is run in. With the built-in exchange, the blocks are fixed effects in the model the runs are chosen for, and metadata["block_labels"] gives each run’s block (the fixed runs a block of their own). Otherwise the blocks are assigned afterwards, by generate_design().

Returns:

The coded design and its metadata; metadata["backend"] names the engine ("split_plot_exchange", "candidate_exchange" or "pyoptex").

Return type:

tuple[np.ndarray, dict]

process_improve.experiments.designs_optimal.dispatch_i_optimal(factors, budget=None, hard_to_change=None, constraints=None, model_type='interactions', fixed_runs=None, random_state=None, candidates=None, backend='auto', n_blocks=None)[source]#

Generate an I-optimal design (minimises the average prediction variance over the region).

Parameters:
  • factors (list[Factor]) – Factor specifications. All-mixture factors get a Scheffé-model design over the (possibly constrained) simplex.

  • budget (int or None) – Number of runs. Defaults to 2 * n_factors + 1, or with fixed_runs to enough runs beyond them to estimate the model. A budget below the model’s number of coefficients raises a ValueError, and so does one that leaves too few runs for the block effects (n_blocks) or for the rank fixed_runs lack.

  • hard_to_change (list[str] or None) – Names of hard-to-change factors: a split-plot design, found by the built-in coordinate exchange on the GLS information (see split_plot_design()), or by pyoptex with backend="pyoptex". A name that is not a factor raises ValueError. Ignored, with a warning, and recorded as hard_to_change_ignored, when the design is built by the candidate exchange: with constraints, a candidate set, fixed runs (unless backend="pyoptex"), blocks, or the E-, G- or K-criterion.

  • constraints (list[Constraint] or None) – Inequalities in actual units over the continuous factors, e.g. "3*T + 5*D <= 600". Enforced: every run the optimizer places satisfies them. Fixed runs outside the region are kept as given, with a warning, and counted in metadata["n_fixed_runs_outside_region"].

  • model_type (str) – Model assumption: "main_effects", "interactions", or "quadratic".

  • fixed_runs (pd.DataFrame or None) – Runs to hold fixed while the optimizer fills the rest (design augmentation). Occupies the first rows of the returned design and counts towards budget.

  • random_state (int, numpy.random.Generator or None) – Seed for the candidate exchange’s random starts.

  • candidates (pd.DataFrame or None) – Settings to choose the runs from, in actual units (proportions for a mixture), one column per factor. Replaces the generated grid; rows breaking a constraint are dropped. metadata["selected_candidates"] counts how often each row (by index label) was picked.

  • backend ({"auto", "exchange", "pyoptex"}) – "auto" and "exchange" use the built-in engines: the coordinate exchange for a split-plot design, the candidate exchange otherwise. "pyoptex" asks for pyoptex.

  • n_blocks (int or None) – Blocks the design is run in. With the built-in exchange, the blocks are fixed effects in the model the runs are chosen for, and metadata["block_labels"] gives each run’s block (the fixed runs a block of their own). Otherwise the blocks are assigned afterwards, by generate_design().

Returns:

The coded design and its metadata; metadata["backend"] names the engine ("split_plot_exchange", "candidate_exchange" or "pyoptex").

Return type:

tuple[np.ndarray, dict]

process_improve.experiments.designs_optimal.dispatch_a_optimal(factors, budget=None, hard_to_change=None, constraints=None, model_type='interactions', fixed_runs=None, random_state=None, candidates=None, backend='auto', n_blocks=None)[source]#

Generate an A-optimal design (minimises the summed variance of the coefficients).

Parameters:
  • factors (list[Factor]) – Factor specifications. All-mixture factors get a Scheffé-model design over the (possibly constrained) simplex.

  • budget (int or None) – Number of runs. Defaults to 2 * n_factors + 1, or with fixed_runs to enough runs beyond them to estimate the model. A budget below the model’s number of coefficients raises a ValueError, and so does one that leaves too few runs for the block effects (n_blocks) or for the rank fixed_runs lack.

  • hard_to_change (list[str] or None) – Names of hard-to-change factors: a split-plot design, found by the built-in coordinate exchange on the GLS information (see split_plot_design()), or by pyoptex with backend="pyoptex". A name that is not a factor raises ValueError. Ignored, with a warning, and recorded as hard_to_change_ignored, when the design is built by the candidate exchange: with constraints, a candidate set, fixed runs (unless backend="pyoptex"), blocks, or the E-, G- or K-criterion.

  • constraints (list[Constraint] or None) – Inequalities in actual units over the continuous factors, e.g. "3*T + 5*D <= 600". Enforced: every run the optimizer places satisfies them. Fixed runs outside the region are kept as given, with a warning, and counted in metadata["n_fixed_runs_outside_region"].

  • model_type (str) – Model assumption: "main_effects", "interactions", or "quadratic".

  • fixed_runs (pd.DataFrame or None) – Runs to hold fixed while the optimizer fills the rest (design augmentation). Occupies the first rows of the returned design and counts towards budget.

  • random_state (int, numpy.random.Generator or None) – Seed for the candidate exchange’s random starts.

  • candidates (pd.DataFrame or None) – Settings to choose the runs from, in actual units (proportions for a mixture), one column per factor. Replaces the generated grid; rows breaking a constraint are dropped. metadata["selected_candidates"] counts how often each row (by index label) was picked.

  • backend ({"auto", "exchange", "pyoptex"}) – "auto" and "exchange" use the built-in engines: the coordinate exchange for a split-plot design, the candidate exchange otherwise. "pyoptex" asks for pyoptex.

  • n_blocks (int or None) – Blocks the design is run in. With the built-in exchange, the blocks are fixed effects in the model the runs are chosen for, and metadata["block_labels"] gives each run’s block (the fixed runs a block of their own). Otherwise the blocks are assigned afterwards, by generate_design().

Returns:

The coded design and its metadata; metadata["backend"] names the engine ("split_plot_exchange", "candidate_exchange" or "pyoptex").

Return type:

tuple[np.ndarray, dict]

Split-plot optimal designs, found by coordinate exchange on the GLS information matrix.

In a split-plot design the hard-to-change factors are set once for each whole plot, a group of consecutive runs, and the easy-to-change factors are reset for every run. The runs of a whole plot share its random effect, so their responses are correlated: \(V = I + \eta Z Z^T\) in units of the run-to-run variance, with \(\eta\) the ratio of the whole-plot variance to it and \(Z\) the runs’ whole-plot indicators. A design is judged on \(M = X^T V^{-1} X\), the information of the generalised least squares estimates (Goos and Jones, Optimal Design of Experiments: A Case Study Approach, chapter 10):

  • D-optimal: the largest \(|M|\);

  • A-optimal: the smallest \(\operatorname{tr}(M^{-1})\);

  • I-optimal: the smallest \(\operatorname{tr}(M^{-1} W)\), the variance of a prediction averaged over the factor box, \(W\) holding the averages of the products of the model terms there.

The search is a coordinate exchange. From a random start, each whole plot’s setting of each hard-to-change factor, then each run’s setting of each easy-to-change factor, is replaced by the level that improves the criterion most, until a pass changes nothing; the best of several starts is kept. \(V^{-1}\) is block diagonal, so \(M\) is a sum over the whole plots,

\[M = \sum_w X_w^T X_w - c_w (X_w^T \mathbf{1})(\mathbf{1}^T X_w), \qquad c_w = \frac{\eta}{1 + m_w \eta},\]

for a whole plot of \(m_w\) runs, and a change replaces one whole plot’s term.

A two-way split-plot (strip-plot) design has two such groupings, crossed: rows, whose factors are set once for the runs of a row, and columns, whose factors are set once for the runs of a column (strip_plot_design()). Then \(V = I + \eta_r Z_r Z_r^T + \eta_c Z_c Z_c^T\), and the search updates \(M\) by the runs a change touches.

process_improve.experiments.designs_split_plot.CRITERIA = ('d_optimal', 'a_optimal', 'i_optimal')#

The criteria the search optimises.

class process_improve.experiments.designs_split_plot.SplitPlotOptions(criterion='d_optimal', model_type='interactions', n_whole_plots=4, variance_ratio=0.5, n_starts=10)[source]#

Bases: object

How a split-plot design is searched for.

Parameters:
  • criterion (str)

  • model_type (str)

  • n_whole_plots (int)

  • variance_ratio (float)

  • n_starts (int)

criterion#

One of CRITERIA.

Type:

str

model_type#

"main_effects", "interactions" or "quadratic". A categorical factor has no square, so "quadratic" squares the continuous factors only.

Type:

str

n_whole_plots#

The number of whole plots. The runs are shared out in order, the last whole plot taking any that are left over, as the pyoptex backend does.

Type:

int

variance_ratio#

\(\eta\), the whole-plot variance over the run-to-run variance.

Type:

float

n_starts#

Random starts; the best design of them is kept.

Type:

int

process_improve.experiments.designs_split_plot.split_plot_design(factors, hard_to_change, n_runs, options, random_state=None)[source]#

Search for a split-plot design that is optimal on the GLS information \(X^T V^{-1} X\).

Parameters:
  • factors (list[Factor]) – Continuous factors (coded from -1 to +1) and categorical factors.

  • hard_to_change (list[str]) – The factors set once for each whole plot.

  • n_runs (int) – The number of runs.

  • options (SplitPlotOptions) – The criterion, the model, the whole plots, the variance ratio and the number of starts.

  • random_state (int, numpy.random.Generator or None) – Seeds the random starts.

Returns:

The design, one run per row in whole-plot order (continuous factors coded, categorical factors as their labels), and metadata: optimality_criterion, model_type, backend ("split_plot_exchange"), hard_to_change, whole_plot (each run’s whole plot, from 0), n_whole_plots, whole_plot_variance_ratio, n_starts, and the criterion of the design on \(M = X^T V^{-1} X\): log_det_information for D, trace_criterion (\(\operatorname{tr}(M^{-1})\) or \(\operatorname{tr}(M^{-1} W)\)) for A and I.

Return type:

tuple[numpy.ndarray, dict]

Raises:

ValueError – If the criterion is not one of CRITERIA, the variance ratio is not a finite number of at least 0, there are fewer runs than whole plots or than model terms, or no design of this structure can estimate the model.

class process_improve.experiments.designs_split_plot.StripPlotLayout(row_factors, column_factors, cells)[source]#

Bases: object

The structure of a two-way split-plot (strip-plot) design.

Parameters:
row_factors, column_factors

The factors set once for each row and once for each column of the grid, as the two stages of a process set them (lots, then curing cycles, say). Any other factor is set for each run.

Type:

tuple of str

cells#

Each run’s row and column, from 0.

Type:

tuple of (int, int)

class process_improve.experiments.designs_split_plot.StripPlotOptions(criterion='d_optimal', model_type='main_effects', row_variance_ratio=1.0, column_variance_ratio=1.0, n_starts=10)[source]#

Bases: object

How a two-way split-plot design is searched for.

Parameters:
  • criterion (str)

  • model_type (str)

  • row_variance_ratio (float)

  • column_variance_ratio (float)

  • n_starts (int)

criterion#

One of CRITERIA.

Type:

str

model_type#

"main_effects", "interactions" or "quadratic".

Type:

str

row_variance_ratio, column_variance_ratio

\(\eta_r\) and \(\eta_c\): the row and the column variance over the run-to-run variance.

Type:

float

n_starts#

Random starts; the best design of them is kept.

Type:

int

process_improve.experiments.designs_split_plot.strip_plot_design(factors, layout, options, random_state=None)[source]#

Search for a two-way split-plot (strip-plot) design that is optimal on \(X^T V^{-1} X\).

The runs sit in the cells of a grid. A row’s factors are set once for all its runs and a column’s once for all of its runs; any other factor is set for each run. The rows and the columns are crossed random effects, \(V = I + \eta_r Z_r Z_r^T + \eta_c Z_c Z_c^T\) in units of the run-to-run variance (Goos and Jones, chapter 11). The search is the coordinate exchange of split_plot_design(): each row’s setting of each row factor, each column’s setting of each column factor and each run’s setting of the other factors is replaced by the level that improves the criterion most, until a pass changes nothing.

Parameters:
  • factors (list[Factor]) – Continuous factors (coded from -1 to +1) and categorical factors.

  • layout (StripPlotLayout) – The row factors, the column factors and the cell of each run.

  • options (StripPlotOptions) – The criterion, the model, the two variance ratios and the number of starts.

  • random_state (int, numpy.random.Generator or None) – Seeds the random starts.

Returns:

The design, one run per row in the order of layout.cells (continuous factors coded, categorical factors as their labels), and metadata: optimality_criterion, model_type, backend ("strip_plot_exchange"), row_factors, column_factors, row and column (each run’s), row_variance_ratio, column_variance_ratio, n_starts, and log_det_information for D or trace_criterion for A and I, on \(M = X^T V^{-1} X\).

Return type:

tuple[numpy.ndarray, dict]

Raises:

ValueError – If the criterion is not one of CRITERIA, a variance ratio is not a finite number of at least 0, a row or column factor is not a factor or is both, a cell is not a pair of whole numbers from 0, there are fewer runs than model terms, or no design on this grid can estimate the model.

Examples

Goos and Jones’s twelve runs in four rows and four columns (Table 11.9), each row in three columns: x1 and x2 set by row, x3 by column.

>>> from process_improve.experiments import Factor
>>> cells = tuple((r, (r + c) % 4) for r in range(4) for c in range(3))
>>> layout = StripPlotLayout(("x1", "x2"), ("x3",), cells)
>>> design, meta = strip_plot_design(
...     [Factor(name=n, low=-1, high=1) for n in ("x1", "x2", "x3")], layout, StripPlotOptions(), random_state=1
... )
>>> design.shape, round(meta["log_det_information"], 3)
((12, 3), 3.767)

Optimal designs on experimental units with known covariates, each unit used once.

Some experiments run on units that carry a measured property the experimenter cannot set: a sample’s composition, a field’s slope, or the time at which a run is made. The units come from a list, each can be used once, and the factors are set for each run on top. The model has the covariates’ main effects beside the factors’ terms, and a design is judged on \(M = X^T X\) of the whole model (Goos and Jones, Optimal Design of Experiments: A Case Study Approach, chapter 9).

The units used and the factors’ settings are found together by coordinate exchange: each run’s unit is swapped for the unused unit, and each run’s setting of each factor for the level, that improves the criterion most, until a pass changes nothing; the best of several random starts is kept. A change replaces one row of the model matrix, so it changes \(M\) by two outer products.

A time trend is the case where every unit is used: the units are the run positions, with the time of each as its covariate, and the search places the factors’ settings in time.

class process_improve.experiments.designs_covariates.CovariateOptions(criterion='d_optimal', model_type='main_effects', n_starts=10)[source]#

Bases: object

How a design on units with covariates is searched for.

Parameters:
  • criterion (str)

  • model_type (str)

  • n_starts (int)

criterion#

One of "d_optimal", "a_optimal" and "i_optimal".

Type:

str

model_type#

The factors’ terms: "main_effects", "interactions" or "quadratic". The covariates enter by their main effects.

Type:

str

n_starts#

Random starts; the best design of them is kept.

Type:

int

process_improve.experiments.designs_covariates.covariate_design(units, factors, n_runs, options, random_state=None)[source]#

Choose units, each at most once, and set the factors for each, optimally for a model with the covariates.

Parameters:
  • units (pandas.DataFrame) – The candidate units, one per row, with a column per covariate: numeric covariates enter the model as they are given (code them to -1 and +1 for a criterion on the coded scale), and the others as categorical, effect-coded. The index names the units.

  • factors (list[Factor]) – The factors set for each run: continuous (coded from -1 to +1) or categorical.

  • n_runs (int) – The number of runs, at most the number of units. With every unit used, the search only sets the factors: a time trend, the units being the run positions.

  • options (CovariateOptions) – The criterion, the factors’ model and the number of starts.

  • random_state (int, numpy.random.Generator or None) – Seeds the random starts.

Returns:

The design, one run per row: the unit’s covariates (indexed by the unit, in the order of units) and the factors’ settings (continuous coded, categorical as labels); and metadata: optimality_criterion, model_type, backend ("covariate_exchange"), units (the units used), n_starts, and log_det_information for D or trace_criterion for A and I. The I criterion averages over the factor box and over the candidate units.

Return type:

tuple[pandas.DataFrame, dict]

Raises:

ValueError – If the criterion is not one of CRITERIA, there are no units, n_runs exceeds their number or is below the model’s number of terms, a factor shares a covariate’s name, or no choice of units estimates the model.

Examples

A linear trend in time over eight runs: the 2^3 placed so that every main effect is orthogonal to the trend.

>>> import numpy as np
>>> import pandas as pd
>>> from process_improve.experiments import Factor
>>> times = pd.DataFrame({"time": np.linspace(-1, 1, 8)})
>>> factors = [Factor(name=n, low=-1, high=1) for n in ("x1", "x2", "x3")]
>>> design, meta = covariate_design(times, factors, 8, CovariateOptions(), random_state=1)
>>> bool(np.allclose(design[["x1", "x2", "x3"]].T @ design["time"], 0.0))
True

Mixture designs: simplex-lattice and simplex-centroid points sized to the Scheffé model.

For mixture experiments the factor levels represent proportions that must sum to 1. These designs operate directly in actual proportions rather than coded -1/+1 units.

process_improve.experiments.designs_mixture.dispatch_mixture(factors, budget=None, constraints=None, model_type='interactions', random_state=None)[source]#

Generate a mixture design.

On the full simplex (every component bounded by 0 and 1, no constraints) the design is sized to the Scheffé model: the pure components, the binary 50:50 blends for a quadratic model, and the ternary 1/3 blends for a special cubic one, each plus the overall centroid (Cornell 2002). These are the simplex-centroid points that blend at most one, two or three components; with three components the quadratic and special cubic designs are the full 7-run simplex centroid. A full cubic model is sized by the {q, 3} simplex lattice, every blend in thirds, plus the overall centroid when it is not one of them (four or more components). A budget smaller than that design gives a D-optimal subset of the candidate blends instead (see below), so the design never exceeds the budget. A budget below the number of model terms raises a ValueError.

When any component has a bound inside (0, 1), or constraints are given, the region is a polytope inside the simplex and the design comes from constrained_mixture_design(): vertices, edge midpoints and centroid without a budget, a D-optimal subset of the candidate blends with one.

Parameters:
  • factors (list[Factor]) – Mixture factors (proportions summing to 1); low/high are component bounds.

  • budget (int or None) – Maximum number of runs.

  • constraints (list[Constraint] or None) – Linear inequalities in the proportions, e.g. "x1 + x2 <= 0.7".

  • model_type (str) – Scheffé model the design is built for: "scheffe_linear" ("main_effects" maps here), "scheffe_quadratic" ("interactions" and "quadratic" map here), "scheffe_special_cubic" or "scheffe_cubic" ("cubic" maps here).

  • random_state (int, numpy.random.Generator or None) – Seed for the exchange of a D-optimal design.

Returns:

Design matrix (in proportions, not coded) and metadata.

Return type:

tuple[np.ndarray, dict]

L-pseudocomponents: a mixture region with lower bounds, put back on the whole simplex.

When each component \(i\) of a mixture must be at least \(L_i\), the blends that remain form a smaller simplex of the same shape inside the original (Cornell; Goos and Jones, section 6.3.4). The L-pseudocomponents

\[x'_i = \frac{x_i - L_i}{T - \sum_j L_j}, \qquad x_i = L_i + \left(T - \sum_j L_j\right) x'_i,\]

with \(T\) the mixture’s total (1 for proportions, 100 for percentages), map that smaller simplex onto the whole one: a component at its lower bound has \(x'_i = 0\), and \(x'_i = 1\) is the blend with every other component at its lower bound. A design built for the full simplex, a simplex lattice or centroid, is then made in pseudocomponents and converted back, and a Scheffe model fitted in pseudocomponents is better conditioned than one fitted in the proportions of a small region.

Examples

Lower bounds of 0.2, 0.2 and 0: the pure first pseudocomponent is the blend (0.8, 0.2, 0.0).

>>> from_pseudocomponents([1.0, 0.0, 0.0], [0.2, 0.2, 0.0]).tolist()
[0.8, 0.2, 0.0]
>>> to_pseudocomponents([0.5, 0.3, 0.2], [0.2, 0.2, 0.0]).round(4).tolist()
[0.5, 0.1667, 0.3333]
process_improve.experiments.pseudocomponents.to_pseudocomponents(blends, lower, total=1.0)[source]#

Return blends as L-pseudocomponents, \(x'_i = (x_i - L_i)/(T - \sum_j L_j)\).

Parameters:
  • blends (pandas.DataFrame, numpy.ndarray or Sequence) – The blends, a component per column (a single blend may be one sequence).

  • lower (Mapping[str, float] or Sequence[float]) – The lower bound of each component: by column name for a DataFrame (a component not named has the bound 0), or one per column.

  • total (float) – The mixture’s total, \(T\): 1 for proportions, 100 for percentages.

Returns:

The pseudocomponents, in the shape and type of blends (a DataFrame keeps its index and columns; a sequence comes back as an array).

Return type:

pandas.DataFrame or numpy.ndarray

Raises:
  • ValueError – If a bound is negative or not a finite number, the bounds leave no room (\(\sum_j L_j \ge T\)), lower does not match the components, or a blend has a value that is not a finite number.

  • TypeError – If lower names the components but blends is not a DataFrame.

process_improve.experiments.pseudocomponents.from_pseudocomponents(pseudocomponents, lower, total=1.0)[source]#

Return L-pseudocomponents as blends, \(x_i = L_i + (T - \sum_j L_j)\,x'_i\).

Parameters:
Returns:

The blends, in the shape and type of pseudocomponents.

Return type:

pandas.DataFrame or numpy.ndarray

Raises:

ValueError – As to_pseudocomponents().

Optimal designs (D, I, A and E) over a constrained factor region, by candidate exchange.

A Constraint such as "3*T + 5*D <= 600" cuts a corner off the factor box. Classical designs cannot follow that cut, so the design is chosen from a set of feasible candidate points instead, in three steps:

  1. Candidate set. A grid over the box, plus the points where each constraint boundary crosses a grid line (found by bisection) and the vertices where linear constraints meet each other or the box, minus every infeasible point. The boundary points matter: a D-optimal design pushes its runs to the edge of the region, and without them the nearest grid point can sit well inside it.

  2. Model matrix. Each candidate is expanded into the columns of the model the design is for (intercept, main effects, two-factor interactions, squares).

  3. Exchange. A Fedorov exchange swaps design runs for candidates while the criterion improves (see Criterion: the determinant of the information matrix X'X for D, a weighted trace of its inverse for A and I, its smallest eigenvalue for E), with several random starts. For D, A and I each run in turn takes its best swap, with (X'X)^-1 kept current by rank-one updates; when the grid was coarse or sampled, each start’s design is then polished by moving single coordinates on a 5-level lattice.

Constraint expressions are parsed into a small arithmetic tree and evaluated with numpy. Nothing is passed to eval, so an expression from an untrusted caller can only compute a number.

process_improve.experiments.designs_constrained.MAX_CANDIDATES = 100000#

Upper limit on the candidate grid; a larger grid is refused before allocation.

process_improve.experiments.designs_constrained.factor_lookup(names)[source]#

Map each factor name’s NFKC form, the name a parsed limit holds, to the name as given.

Raises:

ValueError – If two names share an NFKC form, so a limit could not tell which factor it names.

Parameters:

names (Iterable[str])

Return type:

dict[str, str]

process_improve.experiments.designs_constrained.parse_constraint(expression, names)[source]#

Parse a constraint into inequality functions, each feasible where g(x) <= 0.

Parameters:
  • expression (str) – An inequality in actual units, e.g. "3*T + 5*D <= 600". A chained comparison such as "400 <= 3*T + 5*D <= 600" gives two inequalities. Strict and non-strict comparisons are treated alike, since the boundary of a continuous region has no volume.

  • names (set[str]) – Names that may appear in the expression (the continuous factors). A name is matched through its Unicode NFKC form, as Python’s parser writes it, so a factor named with a decomposed accent or a full-width letter can appear in a limit.

Returns:

One function per comparison. Each takes a mapping from factor name to an array of actual values and returns an array g.

Return type:

list[Callable]

Raises:

ValueError – For an expression that is too long, is not a comparison, uses == or !=, or uses a name or syntax outside the allowed set; or for two names that share an NFKC form.

process_improve.experiments.designs_constrained.rename_factors(expression, names)[source]#

Rename factors in a constraint expression, all at once, and return it in canonical form.

Every name is replaced in a single pass over the parsed expression, so a swap such as {"A": "B", "B": "A"} works. Function names (sqrt, log, …) are left alone even when a factor shares them. The result is ast.unparse of the renamed expression, so rename_factors(expression, {}) gives the canonical text of an expression, and renaming back with the inverse mapping gives that canonical text.

Parameters:
  • expression (str) – A constraint, e.g. "3*T + 5*D <= 600", as parse_constraint() accepts.

  • names (Mapping[str, str]) – Old name to new name. Names the expression does not use are ignored. An old name is matched through its Unicode NFKC form, as parse_constraint() does.

Returns:

The renamed expression, e.g. "3 * Temp + 5 * D <= 600".

Return type:

str

Raises:

ValueError – If expression is not a constraint parse_constraint() accepts (before or after renaming, which also bounds the length of the result), a new name is not a valid identifier, or two different factors of the expression would end up with the same name.

Examples

>>> rename_factors("3*T + 5*D <= 600", {"T": "D", "D": "T"})
'3 * D + 5 * T <= 600'
>>> rename_factors("sqrt(log) <= 2", {"log": "L"})
'sqrt(L) <= 2'
process_improve.experiments.designs_constrained.build_candidates(region, n_levels=None, model_type='quadratic', criterion='d_optimal')[source]#

Return the feasible candidate points in coded units.

The candidates are the grid (see _grid_levels()), the points where each constraint boundary crosses a grid edge (_boundary_points()), and the vertices of the region cut by its linear constraints (_polytope_vertices()), each with every categorical level combination.

Parameters:
  • region (_Region) – Factors and constraints.

  • n_levels (int or None) – Grid levels per continuous factor; None picks them (see _grid_levels()).

  • model_type (str) – The model the design is for: without squared terms, 2 levels may be used.

  • criterion (str) – The optimality criterion the design is for: I- and G-optimality may use 4 levels on a quadratic model, the others go from 5 levels to 3 (see _level_choices()).

Returns:

Coded continuous values (n, k_cont), categorical level indices (n, k_cat), and counts for the metadata.

Return type:

tuple[np.ndarray, np.ndarray, dict]

process_improve.experiments.designs_constrained.model_matrix(region, coded, cats, model_type)[source]#

Expand points into model columns: intercept, main effects, interactions, squares.

A categorical factor with L levels contributes L - 1 effect-coded (sum-to-zero) columns, the coding evaluate_design uses (see categorical_effect_columns()): a two-level factor is -1 and +1, and with more levels the last one, in sorted order, is -1 in every column. The levels are taken in sorted order, as patsy reads a label column, so the criterion value reported here is the one evaluate_design gives for the returned design.

The D-, I- and G-criteria do not depend on which full-rank coding is used, so they select the same designs under any coding. The A-, E- and K-criteria do depend on it, and here optimise the effect-coded model, the usual convention in DoE software.

Parameters:
Return type:

ndarray

class process_improve.experiments.designs_constrained.Criterion(name, weights=None, region_rows=None, segment=None)[source]#

Bases: object

The optimality criterion an exchange maximises.

"d_optimal" maximises log det(M), with M = X'X. "a_optimal" and "i_optimal" minimise trace(M^-1 W): with W = I that is the summed variance of the coefficients (A), and with W the average of f(x) f(x)' over the region it is the average prediction variance over the region (I), which is what evaluate_design reports as I-efficiency. "e_optimal" maximises the smallest eigenvalue of M, which bounds the variance of the worst-estimated linear combination of the coefficients.

Use d(), a(), i() or e() to build one.

Parameters:
classmethod d()[source]#

D-optimality.

Return type:

Criterion

classmethod a(n_parameters)[source]#

A-optimality for a model with n_parameters coefficients.

Parameters:

n_parameters (int)

Return type:

Criterion

classmethod i(region_rows)[source]#

I-optimality, with the moment matrix estimated from model rows sampled uniformly in the region.

Parameters:

region_rows (ndarray)

Return type:

Criterion

classmethod e()[source]#

E-optimality.

Return type:

Criterion

classmethod g(region_rows)[source]#

G-optimality: the largest prediction variance over the model rows region_rows.

Parameters:

region_rows (ndarray)

Return type:

Criterion

classmethod k()[source]#

K-optimality: the condition number of X'X (Ye and Zhou 2013).

Return type:

Criterion

blocked(n_block_columns, segment)[source]#

Return this criterion for a design run in blocks: the factor effects, adjusted for the blocks.

The model rows gain n_block_columns block columns, and segment gives the block of each candidate row; a run is only ever swapped for a candidate of its own block, so the block sizes stay as they started. With them fixed, det(M) = det(Z'Z) det(S), where S is the factor information adjusted for the blocks (the Schur complement of the block part Z'Z of M), so D maximises det(S). The trace criteria give the block columns no weight, and trace(M^-1 W) = trace(S^-1 W). Only D, A and I are blocked this way.

Parameters:
Return type:

Criterion

value(info)[source]#

Score an information matrix; higher is better.

log det(M) (D), -trace(M^-1 W) (A, I), lambda_min (E), minus the largest prediction variance over the region (G), and -log(lambda_max / lambda_min) (K).

Parameters:

info (ndarray)

Return type:

float

property n_phases: int#

E-optimality climbs phi_p first, then polishes lambda_min.

Type:

Exchange phases

best_swap(info, f_design, f_cand, phase=0)[source]#

Return (i, j, gain) for the best single swap: design row i out, candidate j in.

Parameters:
Return type:

tuple[int, int, float]

swap_gains(m_inv, f_design, f_cand)[source]#

Return the gain of every swap (design run i out, candidate j in), shape (n_design, n_cand).

With A = M^-1 and d(a, b) = f(a)' A f(b), D-optimality multiplies the determinant by 1 + d(j) - d(i) - [d(i) d(j) - d(i, j)**2] (Fedorov 1972). For the trace criteria, two Sherman-Morrison updates (add j, then remove i) with b(a, b) = f(a)' A W A f(b) give the new trace in closed form:

s = 1 + d(j)
trace_new = trace - b(j)/s + [b(i) - 2 d(i,j) b(i,j)/s + d(i,j)**2 b(j)/s**2] / [1 - d(i) + d(i,j)**2/s]

so both kinds are scored for all pairs in a few matrix products.

Parameters:
Return type:

ndarray

process_improve.experiments.designs_constrained.criterion_metadata(criterion, value)[source]#

Report the criterion value under a name that says what it is.

Parameters:
Return type:

dict[str, float]

process_improve.experiments.designs_constrained.fedorov_exchange(f_cand, n_free, f_fixed, rng, criterion=None)[source]#

Choose n_free candidate rows that maximise criterion (D-optimality by default).

For D, A and I each design run in turn is swapped for the candidate that improves the criterion most (_row_exchange(), the modified Fedorov exchange of Cook and Nachtsheim 1980), with the gains in the closed form of Criterion.swap_gains() (Fedorov 1972). E and K make the best single swap over all pairs at each step. Rows in f_fixed stay in the design and are never swapped out. Candidates may repeat, which gives replicated runs where the criterion wants them. The best of several random starts is kept: _N_STARTS for E and K, and up to _MAX_STARTS for a small D, A or I problem (see _n_starts()).

Returns:

Selected candidate indices and the criterion value of the full design (see Criterion.value()). G-optimality runs its own search, _g_exchange().

Return type:

tuple[np.ndarray, float]

Parameters:
class process_improve.experiments.designs_constrained.PolishLattice(n_levels, model_rows, feasible=None)[source]#

Bases: object

Where polish_runs() may move a run.

Parameters:
  • n_levels (int) – Each continuous coordinate (coded) may move to one of n_levels evenly spaced levels from -1 to +1.

  • model_rows (Callable[[np.ndarray, np.ndarray], np.ndarray]) – Model rows of points given as coded continuous values and categorical level indices.

  • feasible (Callable[[np.ndarray, np.ndarray], np.ndarray] or None) – True for each point (coded continuous values, categorical level indices) that satisfies the constraints; None for the whole box.

process_improve.experiments.designs_constrained.polish_levels(counts)[source]#

Levels of the lattice to polish on (see polish_runs()), or None when the candidate grid already holds it.

A grid coarser than _POLISH_LEVELS levels, or a sampled one, is polished on the _POLISH_LEVELS-level lattice: runs may then use settings between the grid levels, which can raise the criterion even for D-optimality (a few runs at +/-0.5 when some runs are fixed, for instance).

Parameters:

counts (dict)

Return type:

int | None

process_improve.experiments.designs_constrained.polish_runs(coded, cats, f_fixed, criterion, lattice)[source]#

Coordinate exchange on a finer or complete grid, after the candidate exchange on a coarse or sampled one.

Each run in turn is moved to the best of its neighbours, the points that differ from it in one continuous coordinate (set to another level of lattice), when that improves criterion; passes repeat until no move helps. The gains are those of Criterion.swap_gains(), so only D, A and I are polished. This reaches grid points the candidate set left out, at a cost that grows with the number of runs and factors and not with the size of the grid.

Parameters:
  • coded (np.ndarray) – The free runs, continuous factors in coded units, shape (n, k_cont).

  • cats (np.ndarray) – Their categorical level indices, shape (n, k_cat); these are not moved.

  • f_fixed (np.ndarray) – Model rows of the runs that stay in the design.

  • criterion (Criterion) – D, A or I.

  • lattice (PolishLattice) – The levels, constraints and model rows.

Returns:

The polished runs (coded, categorical) and the number of moves made.

Return type:

tuple[np.ndarray, np.ndarray, int]

class process_improve.experiments.designs_constrained.CandidatePool(coded, cats, rows, lattice=None)[source]#

Bases: object

Candidate points, their model rows, and the lattice their runs may be polished on.

Parameters:
  • coded (np.ndarray) – Continuous factor settings in coded units, shape (N, k_cont).

  • cats (np.ndarray) – Categorical level indices, shape (N, k_cat).

  • rows (np.ndarray) – Model rows of the candidates, shape (N, p).

  • lattice (PolishLattice or None) – Where each start’s design is polished (polish_runs()); None for no polish.

process_improve.experiments.designs_constrained.select_runs(pool, n_free, f_fixed, rng, criterion)[source]#

Choose n_free runs by fedorov_exchange(), polishing each start’s design when the pool says so.

The polish can move a run off the candidate set, and the start whose design is best after polishing is kept: on the 10-factor augmentation of the 1.97.0 review, the best design before polishing was not the best one after it in three seeds out of four.

Returns:

The candidate indices chosen (None once polished, as runs may then lie between candidates), the runs’ coded settings and categorical levels, and the criterion value of the full design.

Return type:

tuple

Parameters:
class process_improve.experiments.designs_constrained.ConstrainedOptions(model_type='interactions', criterion='d_optimal', fixed_runs=None, n_levels=None, candidates=None, n_blocks=None)[source]#

Bases: object

Optional settings for constrained_optimal_design() and the constrained mixture design.

Parameters:
  • model_type (str) – "main_effects", "interactions" or "quadratic" (a mixture design maps these to Scheffé models).

  • criterion (str) – "d_optimal" (default), "i_optimal", "a_optimal", "e_optimal", "g_optimal" or "k_optimal".

  • fixed_runs (pandas.DataFrame or None) – Runs kept in the design (continuous in coded units, categorical as labels), already validated by the caller. They count towards the budget.

  • n_levels (int or None) – Grid levels per continuous factor. None picks the finest of 5, 4 and 3 (and 2, for a model without squared terms; 4 is skipped for a quadratic model unless the criterion is I or G) that keeps the grid within max(5000, 200 p) points for p model coefficients, and samples the coarsest grid down to that when none does. A sampled or coarse grid is then polished (see polish_runs()).

  • candidates (pandas.DataFrame or None) – Settings the runs must be chosen from, in actual units (proportions for a mixture), one column per factor, instead of a generated grid. Rows that break a constraint are dropped, and the I-optimality average is taken over the remaining rows.

  • n_blocks (int or None) – Blocks the design is run in. For D, A and I the blocks enter the exchange as fixed effects (see Criterion.blocked()), and metadata["block_labels"] gives each run’s block; the fixed runs, already made, are a block of their own. Other criteria leave the blocks to be assigned afterwards.

process_improve.experiments.designs_constrained.block_columns(labels, n_blocks)[source]#

Return sum-to-zero columns for the 0-based block labels: n_blocks - 1 of them, the last block -1 in each.

analyze_experiment codes blocks this way, so a criterion over these columns is the one the analysis estimates with, and the intercept is the average block.

Parameters:
Return type:

ndarray

process_improve.experiments.designs_constrained.selection_counts(labels, rows)[source]#

Count how often each supplied candidate (by index label) was chosen.

Parameters:
Return type:

dict[str, int]

process_improve.experiments.designs_constrained.refuse_budget_short_of_fixed_runs(f_fixed, n_parameters, budget)[source]#

Raise when the budget leaves too few new runs to supply the rank the fixed runs lack.

Fixed runs that repeat a point (three centre runs, say) carry less information than their count, so a budget above the number of coefficients can still be too small.

Raises:

ValueError – If budget minus the fixed runs is below the rank they lack.

Parameters:
Return type:

None

process_improve.experiments.designs_constrained.make_criterion(name, n_parameters, region_rows, candidate_rows=None)[source]#

Build the Criterion called name.

region_rows (model rows drawn uniformly in the region) is only evaluated for I- and G-optimality. G takes its maximum over those rows plus candidate_rows, which hold the vertices and boundary points where the worst case usually sits.

Parameters:
Return type:

Criterion

process_improve.experiments.designs_constrained.constrained_optimal_design(factors, budget, constraints, options=None, random_state=None)[source]#

Generate a D-, I-, A- or E-optimal design from a candidate set, every chosen run satisfying constraints.

This is also the optimal-design backend without constraints, unless the design is a split-plot design or pyoptex is asked for. Runs in options.fixed_runs are kept as given, even outside the region (with a warning); metadata["n_fixed_runs_outside_region"] counts them.

Parameters:
  • factors (list[Factor]) – Continuous and categorical factors. Mixture factors are not supported here.

  • budget (int) – Total number of runs, including fixed_runs. A budget that leaves too few free runs for the rank the fixed runs lack, or for the block effects, raises a ValueError.

  • constraints (list[Constraint]) – Inequalities in actual units over the continuous factors, e.g. Constraint(expression="3*T + 5*D <= 600"). May be empty.

  • options (ConstrainedOptions or None) – Model type, criterion, fixed runs and grid resolution; defaults to a D-optimal design for an interactions model, no fixed runs and an automatic grid.

  • random_state (int, numpy.random.Generator or None) – Seed for the random starts of the exchange.

Returns:

The design in coded units (categorical factors as labels) and metadata.

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – If model_type is unknown, a constraint cannot be parsed, no candidate point is feasible, or the feasible region cannot support the requested model.

process_improve.experiments.designs_constrained.region_with_rules(factors, constraints)[source]#

Return the region of continuous and categorical factors cut by constraints, their rules included.

A constraint that names a categorical factor, or joins comparisons with and, or or not, is read by compile_rules(); every other one by parse_constraint(), as before.

Parameters:
Return type:

_Region

process_improve.experiments.designs_constrained.independent_columns(matrix)[source]#

Return the columns of matrix kept, in order, while each adds rank: a basis of its column space.

Worked on the triangular factor of a QR decomposition, which has the column space’s geometry at the size of the number of columns. A column is kept when its part not explained by the columns kept before it is longer than _ESTIMABLE_TOL of its own length.

Parameters:

matrix (ndarray)

Return type:

ndarray

The experimental region: the set of factor settings a design, an evaluation and an optimum may use.

A DesignRegion joins the factors and their constraints into one object, so the same region can drive the three places it matters:

  • generate_design() places runs inside it;

  • evaluate_design() averages (I-efficiency) and maximises (G-efficiency) the prediction variance over it, not over the full box;

  • optimize_responses() searches for an optimum inside it, so the recommended settings are ones the plant can run.

Three kinds of region exist, and each works in its own design units:

  • "box": continuous (and categorical) factors, in coded units [-1, 1], with optional inequality constraints written in actual units;

  • "mixture": mixture components, as proportions summing to 1, with optional component bounds and linear inequality constraints;

  • "mixture_process": mixture components and process factors together, the product of a mixture region and a box region. Each constraint limits one of the two parts.

A box region’s constraints may also name categorical factors (see _rules): "not (catalyst == 'B' and solvent == 'Y')" disallows a combination of levels, and "not (catalyst == 'B' and T > 180)" limits T on one level only. Such a region is one region of continuous settings for each allowed combination of levels: allowed_combinations() lists them, given() returns the one for a combination, and feasible() and sample_frame() take the levels into account.

generate_design records the region it used in DesignResult.metadata["region"] as a plain dict, which DesignRegion.from_dict() turns back into a region.

A box region also describes itself, in actual units, for someone writing limits on the settings: the share of the box it keeps (volume_fraction(), exact for straight limits), whether anything is left (is_empty()), the range each factor can still reach (implied_bounds()), a central setting to hold other factors at (centroid()), the limits that remove nothing (redundant_constraints()), each limit’s affine form (linear_forms()), its corners (vertices()), and pictures of it on any two factors: its shadow (shadow()), a slice through it (slice_polygon(), slice_lines()) and the slack of a grid (slice_slack()). Every answer is deterministic, and fast enough to recompute on each edit of 8 limits on 8 factors.

class process_improve.experiments.region.DesignRegion(factors, constraints=None)[source]#

Bases: object

Factors plus constraints, with feasibility tests and uniform sampling in design units.

Parameters:
  • factors (list[Factor]) – Mixture components, continuous and categorical factors, or mixture components together with process factors.

  • constraints (Sequence[Constraint | dict | str] or None) – Inequalities in actual units (proportions for mixtures): Constraint objects, their dict form (as to_dict() writes it) or bare expressions such as "3*T + 5*D <= 600", mixed freely. In a mixture-process region each one names components only or process factors only. In a box region a constraint may also compare a categorical factor with its levels and join comparisons with and, or and not, as in "not (catalyst == 'B' and T > 180)".

Examples

>>> region = DesignRegion(
...     [Factor(name="T", low=100, high=150), Factor(name="D", low=20, high=60)],
...     ["3*T + 5*D <= 600"],
... )
>>> region.feasible(np.array([[1.0, 1.0], [-1.0, -1.0]]))  # coded (150, 60) and (100, 20)
array([False,  True])
names#

Names of the numeric columns the region acts on, in order.

rules: Rules | None#

The rules on categorical factors, if any constraint names one (a box region only).

property mixture_names: list[str]#

all of a mixture’s, none of a box’s.

Type:

Names of the columns that hold mixture proportions, which sum to 1

property is_constrained: bool#

True unless the region is the plain box or the full simplex.

to_dict()[source]#

Return a JSON-friendly description, as stored in DesignResult.metadata["region"].

Return type:

dict[str, Any]

classmethod from_dict(spec)[source]#

Rebuild a region from to_dict() output.

The constraints go through the constructor, so they may take any form it accepts (Constraint dicts as to_dict() writes them, or bare expressions), and a missing or None entry means no constraints.

Parameters:

spec (dict[str, Any]) – {"factors": [...], "constraints": [...]}; a "kind" entry is ignored, since the factors decide it.

Returns:

The region.

Return type:

DesignRegion

property inequalities: list[Callable[[ndarray], ndarray]]#

Functions g(x) of design-unit points (n, k); a point is feasible where every g <= 0.

property bounds: list[tuple[float, float]]#

(-1, 1) for a process factor, (low, high) for a component.

Type:

Per-column bounds in design units

feasible(x, tol=1e-09, labels=None)[source]#

Return a boolean per row of x (design units): inside the bounds and every constraint.

Parameters:
  • x (np.ndarray) – Points (n, k) in design units (coded for a box, proportions for a mixture, both for a mixture-process region).

  • tol (float) – How far a point may lie outside. For a straight limit of a box region it is a distance in coded units (the limit’s value divided by the length of its coded gradient), so a point within tol of the boundary passes and one further out fails, however the limit is scaled and whatever its units: X <= 0.5e-9 on a range of 1e-9 is judged as X <= 0.5 on a range of 1. For a curved limit it is tol times the largest size the limit takes over the box, at its corners and its centre; for one that compares numbers only, and for a mixture, tol in the limit’s own units. The corners the region reports (vertices(), shadow(), slice_polygon()) pass, whose rounding (see vertices()) moves a coordinate by at most 1e-10 in coded units, or a few times that for a factor whose values pass 100000 times its range.

  • labels (Mapping[str, Any] or None) – Each point’s level of every categorical factor the rules name (a DataFrame, or a mapping from name to values). Needed when the region has rules: a point in a combination of levels the rules do not allow is not feasible, and a limit that applies on some levels only is tested on those points.

Returns:

A boolean per row.

Return type:

np.ndarray

Raises:

ValueError – If the region has rules and labels is missing or lacks one of their factors.

allowed_combinations()[source]#

Return the combinations of levels the rules allow, with the limits that apply on each.

Returns:

One row per allowed combination of the levels of the categorical factors the rules name, one column per factor, and a "limits" column listing the inequalities that apply there besides those that apply everywhere. Without rules, an empty frame.

Return type:

pandas.DataFrame

Examples

>>> factors = [
...     Factor(name="T", low=150, high=200),
...     Factor(name="catalyst", type="categorical", levels=["A", "B"]),
...     Factor(name="solvent", type="categorical", levels=["X", "Y"]),
... ]
>>> rules = ["not (catalyst == 'B' and solvent == 'Y')", "not (catalyst == 'B' and T > 180)"]
>>> region = DesignRegion(factors, rules)
>>> region.allowed_combinations()
  catalyst solvent      limits
0        A       X          []
1        A       Y          []
2        B       X  [T <= 180]
given(**levels)[source]#

Return the region of continuous settings for one combination of levels.

Parameters:

**levels (object) – The level of every categorical factor the rules name, such as catalyst="B", solvent="X".

Returns:

A region of the numeric factors only, cut by the constraints that apply everywhere and by the limits that apply at this combination. Its diagnostics (vertices(), volume_fraction(), …) describe that combination.

Return type:

DesignRegion

Raises:

ValueError – If the region has no rules, a factor the rules name is not given, or the rules do not allow the combination.

sample_frame(n, rng)[source]#

Draw n points uniformly from the region, as a frame with a column per factor.

Numeric factors are in design units, as sample() gives them, and categorical factors hold their levels. Every setting the region allows is equally likely: each allowed combination of the levels the rules name is drawn in proportion to the volume of the region its limits leave (the volume_fraction() of given()), so one whose limits leave 60% of the box is drawn 0.6 times as often as one they leave whole, and one they leave no volume is not drawn. Every other categorical factor takes each level with the same probability, and the numeric settings of a point are uniform over the region its levels leave.

Parameters:
Returns:

n rows, one column per factor, in factor order.

Return type:

pandas.DataFrame

support_frame()[source]#

Return support_points() of each allowed combination of the levels the rules name, as a frame.

Each combination’s points come from given(). The frame has a column per numeric factor (design units) and per factor the rules name.

Raises:

ValueError – If the region has no rules, or the points would be more than 100000 rows.

Return type:

DataFrame

broken_limits(x, tol=1e-09, labels=None)[source]#

Return, for each point, the limits it breaks: the factor ranges it leaves, then the constraints it fails.

A range is written in actual units, "100 <= T <= 150", and a constraint as its expression, each once. A point that feasible() accepts with the same tol breaks none, so this says why a setting cannot be run.

Parameters:
  • x (np.ndarray) – Points (n, k) in coded units.

  • tol (float) – How far a point may lie outside, as for feasible().

  • labels (Mapping[str, Any] or None) – Each point’s levels, as for feasible(); needed when the region has rules, which a point breaks by its combination of levels or by a limit on its levels.

Returns:

One list per row of x, empty for a feasible point.

Return type:

list[list[str]]

Raises:

ValueError – For a mixture region.

Examples

>>> region = DesignRegion(
...     [Factor(name="T", low=100, high=150), Factor(name="D", low=20, high=60)],
...     ["3*T + 5*D <= 600"],
... )
>>> region.broken_limits(np.array([[1.0, 1.0], [-1.0, -1.0], [1.5, -1.0]]))
[['3*T + 5*D <= 600'], [], ['100 <= T <= 150']]
sample(n, rng)[source]#

Draw n points uniformly from the region, in design units: an (n, 0) array without continuous factors.

A box region is sampled from the coded cube. A mixture region is sampled from the smallest simplex holding the lower bounds (the L-pseudocomponent simplex), where a flat Dirichlet draw is uniform, so tight lower bounds cost nothing. When the constraints leave too small a part of that set for rejection to be cheap, hit-and-run chains started from the region’s support points take over (see UniformSampler).

Raises:

ValueError – If no point of the region can be found, or the region has rules on categorical factors (use sample_frame(), which draws the levels too).

Parameters:
Return type:

ndarray

seed_points()[source]#

Return support_points(), or none for a box too large for a boundary grid.

Return type:

ndarray

support_points()[source]#

Return points on the region’s boundary, in design units: where worst-case variance sits.

For a mixture these are the extreme vertices and edge midpoints; for a box, the feasible grid points and the points where each constraint crosses a grid edge; for a mixture-process region, every such blend at every such process setting.

Return type:

ndarray

volume_fraction(n_samples=16384)[source]#

Return the share of the factor box that the constraints leave: 1 without constraints, 0 when empty.

With straight constraints only, the share is exact, to rounding. The factors are split into the groups the constraints tie together (a factor no constraint names keeps its whole range), and the share is the product of the groups’ shares: in closed form for a group under one constraint, a chained one such as "400 <= 3*T + 5*D <= 600" included, and from the volume of the polytope’s convex hull for a group of up to five factors under several (see box_share()).

With a curved constraint, the share is integrated when, in each group of factors a curved constraint ties, one factor is named by every constraint that names two, and none names three: a constraint on one or two factors, or one beside a straight one that shares a factor with it, as "T*F <= 3600" beside "T + 25*P <= 250". At each value of that factor, each other one’s allowed length is found along a line across the box, and their product is integrated by adaptive quadrature, to about 1e-10 (see curved_share()). A stretch a curved constraint allows, or removes, narrower than 1/128 of such a line can be missed.

Otherwise (a curved constraint on three factors, a chain of them that no one factor runs through, or six factors or more that several straight constraints tie together), the share is counted over n_samples points of the unscrambled Sobol sequence (moved to the midpoints of their cells). Beside a curved constraint, the share the straight ones leave is still exact, and the points count only what the curved ones take from it: the share is that exact share times the fraction of the points inside the straight constraints that meet every constraint. When the straight constraints leave too thin a region for any of the points to lie in it, n_samples points uniform on that region take their place, for up to five factors. In every other case the share is the fraction of all the points that meet every constraint. The points are fixed, so the result is the same on every run and on every platform; with the default 2**14 points it is within about 0.005 of the exact share for the regions of a few factors that limits describe.

For a mixture region the share is of the blends the components’ ranges allow (the whole simplex when every range is 0 to 1), and it is exact: a mixture’s constraints are straight, so it is the volume of the polytope they leave over that of the polytope the ranges leave, each the hull of its extreme vertices (#810). A mixture-process region is the product of its two parts, so its share is the product of theirs.

Parameters:

n_samples (int) – Number of points, when the share is sampled; a power of two keeps the sequence balanced.

Returns:

The share of the box, or of the mixtures, between 0 and 1.

Return type:

float

Raises:

ValueError – If n_samples is below 1, or a mixture’s vertices are too many to list.

Examples

>>> T, D = Factor(name="T", low=100, high=150), Factor(name="D", low=20, high=60)
>>> round(DesignRegion([T, D], ["3*T + 5*D <= 600"]).volume_fraction(), 3)  # exactly 0.625
0.625
>>> blends = [Factor(name=name, type="mixture") for name in ("A", "B", "C")]
>>> DesignRegion(blends, ["A <= 0.5"]).volume_fraction()  # all but the corner where A > 0.5, 1/4
0.75
centroid()[source]#

Return a central setting of the region, in actual units: where to hold factors that are not plotted.

It is the mean of the allowed points of the Sobol sample behind volume_fraction() (of the points is_empty() searches with, when the region is too thin for that sample), so it is the region’s centroid to within the sample’s accuracy. A region that is not convex, such as a ring, can have its centroid outside it; the allowed point nearest the centroid is returned then, so the result can always be run.

Returns:

{name: value} for every numeric factor, rounded as vertices() describes; None when no allowed point is found (see is_empty()).

Return type:

dict[str, float] or None

Raises:

ValueError – For a mixture region.

Examples

>>> factors = [Factor(name="A", low=-1, high=1), Factor(name="B", low=-1, high=1)]
>>> middle = DesignRegion(factors, ["A + B >= 1"]).centroid()  # the triangle's centroid is (2/3, 2/3)
>>> {name: round(value, 2) for name, value in middle.items()}
{'A': 0.66, 'B': 0.66}
is_empty()[source]#

Return True when no setting inside the factor ranges meets every constraint.

The linear constraints are decided exactly, from the vertices of the box they cut (a linear program when there are too many vertices to list). When some constraints are curved, a setting is searched for among a Sobol sample of the box and points spread through the polytope the linear constraints leave, its vertices included; a region so thin that none of these falls inside it is reported empty.

Returns:

Whether the region holds no setting at all.

Return type:

bool

Raises:

ValueError – For a mixture region.

implied_bounds()[source]#

Return the range each factor can still take, in actual units, given the linear constraints.

A limit can keep a factor inside its own range everywhere: with T from 150 to 200 and P from 1 to 3, T + 25*P <= 215 keeps T at or below 190 (P is at least 1) and P at or below 2.6 (T is at least 150). These bounds are the smallest and largest value of each factor over the region cut by the linear constraints, from its vertices (or a linear program per bound when there are too many vertices). Curved constraints are left out, so with any of them the bounds are outer bounds: no setting lies outside them, but the extremes may not be reachable.

Returns:

{name: (low, high)} for every numeric factor, in factor order, rounded as vertices() describes; None when the linear constraints leave no setting.

Return type:

dict[str, tuple[float, float]] or None

Raises:

ValueError – For a mixture region.

Examples

>>> factors = [Factor(name="T", low=150, high=200), Factor(name="P", low=1, high=3)]
>>> DesignRegion(factors, ["T + 25*P <= 215"]).implied_bounds()
{'T': (150.0, 190.0), 'P': (1.0, 2.6)}
redundant_constraints()[source]#

Return the indices of the constraints that remove nothing the others do not already remove.

Each constraint is tested against the region left by all the others: a linear one by a linear program per inequality (its largest value over that region), a curved one, or one the linear test cannot clear because some other constraint is curved, by checking that it removes none of the points that is_empty() searches with (a Sobol sample of the box, and points spread through the polytope of the linear constraints) that the others keep. Two copies of the same constraint are both reported, since each removes nothing given the other; removing both is not safe.

Returns:

Indices into constraints, in order.

Return type:

list[int]

Raises:

ValueError – For a mixture region.

curved_constraints()[source]#

Return the indices of the constraints that are curved: not affine over the factor box.

A constraint is straight when each of its comparisons is affine over the box, by the test the design engines use, so "400 <= 3*T + 5*D <= 600" and "T <= 2*D" are straight, and so is "abs(T) <= 3" over a box where T keeps one sign. The diagnostics treat the straight ones exactly and the curved ones by sampling (see is_empty()); implied_bounds() leaves the curved ones out, and vertices() refuses a region that has any.

Returns:

Indices into constraints, in order.

Return type:

list[int]

Raises:

ValueError – For a mixture region.

Examples

>>> T, D = Factor(name="T", low=100, high=150), Factor(name="D", low=20, high=60)
>>> DesignRegion([T, D], ["3*T + 5*D <= 600", "T * D <= 7000", "T <= 2*D"]).curved_constraints()
[1]
linear_forms()[source]#

Return each constraint as (coefficients, low, high) in actual units, or None where it is not affine.

"3*T + 5*D <= 600" gives ({"T": 3.0, "D": 5.0}, None, 600.0) and "400 <= 3*T + 5*D <= 600" gives ({"T": 3.0, "D": 5.0}, 400.0, 600.0): the constraint reads low <= sum(coefficient * factor) <= high, with None for a side it does not bound. The factors stay on the side they were written on, so "600 >= 3*T + 5*D" gives the same form, and "T <= 2*D" gives ({"T": 1.0, "D": -2.0}, None, 0.0). Each comparison is tested for being affine over the factor box, the test the design engines use. Its form is then read off the expression in exact arithmetic, with each number taken as written, so "TK + 0.01*P <= 340" gives 0.01 and not 0.00999999999999; where the expression is affine over the box only (abs(T) with T positive), it is read off a fit there. A coefficient whose effect over the box is below 1e-9 of the largest is set to zero, its factor held at the centre of its range, where its term’s value joins the bound; and every number is rounded to 12 significant figures, so floating-point residue such as 1.65e-14 or 250.00000000000009 does not appear.

Returns:

One entry per constraint, in order: the tuple above, or None for a constraint that is not affine, compares only numbers, chains two different combinations of factors ("A <= B <= 2"), or joins comparisons with and, or or not.

Return type:

list

vertices()[source]#

Return the vertices of the region, in actual units, when every constraint is linear.

The region is then a convex polytope: the box cut by the constraints. Its vertices are the extreme settings, where a D-optimal design for a main-effects model puts its runs (one for a quadratic model also puts runs between them). They are found by cutting the box one constraint at a time (see box_vertices()), so the work grows with the number of vertices rather than with the ways of choosing k boundaries.

Returns:

(n, k) vertices, one column per numeric factor, without duplicates, sorted row by row in coded order; no rows when the region is empty. Each value is rounded to 12 significant figures, so that 250.00000000000009 reads 250.0, or to more where its factor’s range is narrow next to its values (a year, an absolute pressure, a time stamp): to the decimal place of 1e-10 times the range, up to 16 figures, about what a double holds. Every vertex then passes feasible(), short of a factor whose values pass a million times its range, which a double cannot resolve that finely.

Return type:

np.ndarray

Raises:

ValueError – For a mixture region, a region with a curved constraint, or a region with too many vertices to list (more than 65536).

shadow(x, y)[source]#

Return the region’s shadow on the factors x and y: the settings of the two that some run can use.

A pair (x, y) is in the shadow when at least one setting of the other factors makes it allowed: the region seen from above, along the other factors’ axes. For a region whose constraints are all linear the shadow is exact: the convex hull of the vertices’ projections. Otherwise it is a mask of 40 by 40 cells, each marked where an allowed point falls, among those is_empty() searches with.

Parameters:
  • x (str) – Two different numeric factors.

  • y (str) – Two different numeric factors.

Returns:

{"kind": "polygon", "points": [[x, y], ...]} in actual units: the corners counter-clockwise from the one with the smallest x; two points when the shadow is a segment, one when it is a point, none when the region is empty. Or {"kind": "mask", "x": [...], "y": [...], "z": [[0 or 1, ...], ...]}: the 40 cell centres along each axis, and z[i][j] for the cell at (x[j], y[i]).

Return type:

dict

Raises:

ValueError – For a mixture region, or when x and y are not two different numeric factors.

slice_polygon(x, y, at=None)[source]#

Return the settings of x and y that are allowed with every other factor held at at.

For linear constraints this is exact: the x-y rectangle of the two factors’ ranges, clipped by each constraint’s half-plane at the held values (Sutherland-Hodgman). With a curved constraint it is a mask on a 61 by 61 grid over the rectangle.

Parameters:
  • x (str) – Two different numeric factors.

  • y (str) – Two different numeric factors.

  • at (Mapping[str, float] or None) – Values (actual units) of the other numeric factors; a factor not given is held at the centre of its range, and entries for x and y are ignored. A value outside a factor’s range leaves nothing to run.

Returns:

{"kind": "polygon", "points": [[x, y], ...]} in actual units, as for shadow() (no points when nothing can be run there), or {"kind": "mask", "x": [...], "y": [...], "z": [[0 or 1, ...], ...]} with the 61 grid values along each axis and z[i][j] for the point (x[j], y[i]).

Return type:

dict

Raises:

ValueError – For a mixture region, when x and y are not two different numeric factors, or when at names a factor the region does not have or gives a value that is not a finite number.

slice_lines(x, y, at=None)[source]#

Return where each linear constraint’s boundary crosses the x-y rectangle, the others held at at.

These are the lines a picture of the region draws and numbers: for a constraint in x and y alone the line does not depend on at; for one that also names other factors it is the line in the slice at at (see slice_polygon()). Each boundary is clipped to the rectangle of the two factors’ ranges only, not by the other constraints.

Parameters:
  • x (str) – Two different numeric factors.

  • y (str) – Two different numeric factors.

  • at (Mapping[str, float] or None) – Values (actual units) of the other numeric factors, as for slice_polygon().

Returns:

{"limit": i, "points": [[x1, y1], [x2, y2]]} in actual units, one per inequality whose boundary crosses the rectangle, in constraint order; a chained constraint such as "400 <= 3*T + 5*D <= 600" can give two, both with its index i. A boundary that misses the rectangle, only touches a corner of it, or does not involve x or y gives none, and curved constraints give none.

Return type:

list[dict]

Raises:

ValueError – For a mixture region, when x and y are not two different numeric factors, or when at names a factor the region does not have or gives a value that is not a finite number.

slice_slack(x, y, x_grid, y_grid, at=None)[source]#

Return how far each grid point is from being allowed: <= 0 exactly where it can be run.

Each value is the largest slack over the factor ranges and the constraints, with the other factors held at at. A linear constraint’s slack is its signed distance from its boundary in coded units, so it varies linearly over the grid and a contour of it at zero draws a straight limit straight; a curved constraint’s slack is its own value (the left side minus the right), and +inf where it cannot be evaluated. A value within the default tolerance of feasible() (1e-9, as a coded distance for a straight limit) is returned as zero, so <= 0 here agrees with feasible() at every point.

Parameters:
  • x (str) – Two different numeric factors.

  • y (str) – Two different numeric factors.

  • x_grid (np.ndarray) – Values of x and y in actual units; they are broadcast together, so the two arrays from np.meshgrid (or a row and a column) give a grid.

  • y_grid (np.ndarray) – Values of x and y in actual units; they are broadcast together, so the two arrays from np.meshgrid (or a row and a column) give a grid.

  • at (Mapping[str, float] or None) – Values (actual units) of the other numeric factors, each one not given held at the centre of its range; entries for x and y are ignored.

Returns:

The slack, of the broadcast shape of x_grid and y_grid.

Return type:

np.ndarray

Raises:

ValueError – For a mixture region, when x and y are not two different numeric factors, or when at names a factor the region does not have or gives a value that is not a finite number.

A straight limit proposed from settings: the cut that rules out the settings that cannot be run.

Someone who knows which settings their process cannot run (the corner where it is both hottest and fastest, say) often knows it better than they can write the inequality. Two functions turn that knowledge into a linear Constraint in actual units, written for a person to read ("T + 25 * P <= 250"):

  • separating_constraint() takes the settings that cannot be run and those that can, and proposes the one straight cut that rules out the first and keeps the second, as far from both as it can be;

  • constraint_through_cuts() takes one corner of the factor box and, for each factor, how far it can go from that corner with the others held there, and writes the limit through those cut points.

When no straight cut separates the two sets of settings, separating_constraint() raises NotSeparableError, which names the settings that lie inside the other set.

Only numpy, scipy.optimize (linprog with HiGHS, nnls and SLSQP), pandas (to accept the settings as a DataFrame) and the standard library are used, and every step is deterministic, so a proposal is the same on every platform, Pyodide included.

exception process_improve.experiments.separating.NotSeparableError(message, inside_can=(), inside_cannot=())[source]#

Bases: ValueError

No straight cut rules out the settings that cannot be run and keeps all those that can.

Raised by separating_constraint(). It names the settings to blame, when some are: a setting that cannot be run but lies between settings that can (inside their convex hull), so any straight cut that rules it out rules out one of them too; and a setting that can be run but lies between settings that cannot, so any straight cut that rules them out rules it out too. When the two sets overlap without any one setting lying inside the other (the four corners of a square marked alternately, say), both lists are empty.

Parameters:
  • message (str) – The explanation shown to the reader.

  • inside_can (Sequence[int]) – Indices into cannot of the settings that lie inside the hull of can.

  • inside_cannot (Sequence[int]) – Indices into can of the settings that lie inside the hull of cannot.

Return type:

None

inside_can#

As the parameter, in increasing order.

Type:

list[int]

inside_cannot#

As the parameter, in increasing order.

Type:

list[int]

process_improve.experiments.separating.separating_constraint(factors, cannot, can)[source]#

Propose the straight limit that rules out the settings that cannot be run and keeps those that can.

The limit is found in two steps, in coded units (each factor’s range mapped to [-1, 1], so a factor counts by its range, not by its units):

  1. Whether any straight cut separates the two sets: a linear program maximises the margin t of a plane w @ x = c with every |w_j| <= 1. When the best margin is not positive, NotSeparableError is raised; one small linear program per setting finds the settings that lie inside the other set’s convex hull, to name them.

  2. The cut itself: the plane of maximum margin, which lies as far from the nearest settings of both sets as it can (the hard-margin support vector machine). It is unique, so a symmetric pattern gives a symmetric cut, and a factor the pattern does not depend on gets exactly zero and drops out of the limit.

The plane is then written for a reader, in actual units. Its coefficients are scaled so the smallest is 1, and also so the largest is 1, and each scaling is rounded to the fewest significant digits at which the coefficients (coded, scaled to a sum of sizes of 1) move by at most 1% in total and every setting stays on its side; the scaling whose coefficients take fewer digits to write is kept. The rounding starts from the coefficients rounded to 10 significant digits, so the solver’s last-bit noise, which differs between platforms, cannot decide which way a coefficient exactly halfway between two candidates goes: it goes to the even one, and equal coefficients stay equal. The right-hand side is the roundest number (a multiple of 1, 2 or 5 times a power of ten, as large as fits) in the middle fifth of the gap between the two sets, nearest its middle. Every setting is then checked, in exact decimal arithmetic, to be strictly on its side.

Parameters:
  • factors (Sequence[Factor]) – The continuous factors the settings are given for; their ranges set the coded units, and their names are written into the limit. A categorical factor is refused: for settings at one level, propose the limit over the continuous factors and write it with that level, as not (K == "B" and T + 25 * P > 250).

  • cannot (np.ndarray, pandas.DataFrame, or a sequence of sequences or of mappings) – The settings that cannot be run, in actual units: one row per setting with a value per factor in factor order (a 2-D array or a list of lists), a pandas DataFrame with a column per factor, or a list of {name: value} mappings. Each must lie within the factors’ ranges.

  • can (np.ndarray, pandas.DataFrame, or a sequence of sequences or of mappings) – The settings that can be run, in the same forms. Settings in neither list (another group of marked corners, say) do not count either way.

Returns:

A linear constraint in actual units, such as "T + 25 * P <= 250", under which every setting of cannot is ruled out and every setting of can is allowed, none of them on the boundary. Its first term is positive.

Return type:

Constraint

Raises:
  • NotSeparableError – When no straight cut separates the two sets.

  • ValueError – When either set is empty, a setting is outside the factors’ ranges or has the wrong number of values, or a factor is not continuous or has a name that cannot be written in a limit.

See also

constraint_through_cuts

the limit through a cut point on each factor from one corner.

Examples

>>> import itertools
>>> factors = [
...     Factor(name="T", low=150, high=200),
...     Factor(name="P", low=1, high=3),
...     Factor(name="F", low=10, high=20),
... ]
>>> corners = [list(c) for c in itertools.product((150, 200), (1, 3), (10, 20))]
>>> cannot = [c for c in corners if c[0] == 200 and c[1] == 3]  # T and P both high
>>> can = [c for c in corners if c not in cannot] + [[175, 2, 15]]  # the rest, and the centre
>>> separating_constraint(factors, cannot, can).expression
'T + 25 * P <= 250'
process_improve.experiments.separating.constraint_through_cuts(factors, corner, cuts)[source]#

Write the straight limit that cuts a corner off the box at given values of the factors.

From the corner (a setting that cannot be run), each limited factor can go up to (or down to) its cut value with the other factors held at the corner. The limit is the plane through those cut points, sum((X_j - v_j) / (cut_j - v_j)) >= 1 over the limited factors, where v is the corner: it rules out the corner and the settings near it, keeps the cut points on its boundary, and does not depend on a factor whose cut is None. The arithmetic is exact (each value is read as the decimal its float shows), so round inputs give whole-number coefficients. The limit is scaled so its smallest coefficient is 1, so its largest is 1, or to whole numbers, keeping whichever of these writes the coefficients exactly with the fewest digits; it is written as separating_constraint() writes it.

Parameters:
  • factors (Sequence[Factor]) – The continuous factors.

  • corner (Mapping[str, float] or Sequence[float]) – A corner of the box, in actual units: each factor at its low or its high value, as {name: value} or a sequence in factor order.

  • cuts (Mapping[str, float or None]) – For each factor, the value it can reach from the corner with the others held there, or None (or no entry) when the factor does not limit this. A cut must lie inside the factor’s range and differ from the corner’s value.

Returns:

A linear constraint in actual units, such as "T + 20 * P <= 250".

Return type:

Constraint

Raises:

ValueError – When a factor is not continuous or its name cannot be written in a limit, the corner is not a corner of the box, cuts names a factor that is not given, no factor is cut, or a cut lies outside its factor’s range or at or beyond the corner.

See also

separating_constraint

the limit proposed from settings that cannot and can be run.

Examples

>>> factors = [Factor(name="T", low=150, high=200), Factor(name="P", low=1, high=3)]
>>> constraint_through_cuts(factors, {"T": 200, "P": 3}, {"T": 190, "P": 2.5}).expression
'T + 20 * P <= 250'

Mixture designs over a constrained simplex: extreme vertices and D-optimal selection.

Component bounds (Factor(type="mixture", low=0.1, high=0.6)) and linear constraints ("x1 + x2 <= 0.7") cut the simplex down to a convex polytope. The designs here are built from the geometry of that polytope, in three steps:

  1. Extreme vertices. Inside the plane sum(x) = 1 a vertex is a point where q - 1 constraints are active. Every choice of q - 1 constraints is solved as one linear system (all at once, batched), and the feasible solutions are the vertices (McLean and Anderson 1966, with general linear constraints as in XVERT).

  2. Candidate points. The vertices, the midpoints of the edges joining them and the points a third of the way along them, the centroid of each constraint face, the overall centroid, and the axial check blends halfway between each vertex and the centroid.

  3. Selection. Without a run budget the vertices, the edge midpoints (for a quadratic model) or edge third-points (for a full cubic one), the 2-face centroids (for a special or full cubic one) and the overall centroid are returned, unless that is more than three runs per model term; with a budget, or for such a large design, a D-optimal subset is chosen for the Scheffé model by the same Fedorov exchange used for constrained process designs.

The Scheffé models have no intercept, since sum(x) = 1 makes an intercept redundant: linear has one term per component, quadratic adds x_i x_j, special_cubic adds x_i x_j x_k, and the full cubic adds x_i x_j (x_i - x_j) beside those.

process_improve.experiments.designs_mixture_constrained.SCHEFFE_MODELS = ('scheffe_linear', 'scheffe_quadratic', 'scheffe_special_cubic', 'scheffe_cubic')#

Scheffé model names, and the design model_type names that map onto them.

process_improve.experiments.designs_mixture_constrained.SCHEFFE_DEGREE = {'scheffe_cubic': 3, 'scheffe_linear': 1, 'scheffe_quadratic': 2, 'scheffe_special_cubic': 3}#

Highest number of components multiplied in one term of each Scheffé model.

process_improve.experiments.designs_mixture_constrained.MAX_VERTEX_SUBSETS = 500000#

Largest number of constraint subsets solved when enumerating vertices, and how many are solved at once.

process_improve.experiments.designs_mixture_constrained.scheffe_model(model_type)[source]#

Return the Scheffé model name for a model_type, e.g. "quadratic" -> "scheffe_quadratic".

Parameters:

model_type (str)

Return type:

str

process_improve.experiments.designs_mixture_constrained.scheffe_matrix(x, model)[source]#

Expand proportions x (n, q) into the columns of a Scheffé model.

Parameters:
Return type:

ndarray

process_improve.experiments.designs_mixture_constrained.scheffe_formula_rhs(names, model)[source]#

Return the patsy right-hand side of a Scheffé model, e.g. "-1 + (A + B + C) ** 2".

The full cubic adds I(A * B * (A - B)) for each pair of components to the special cubic.

Parameters:
Return type:

str

process_improve.experiments.designs_mixture_constrained.cubic_difference_term(a, b)[source]#

Return the full cubic Scheffé model’s term in components a and b: I(a * b * (a - b)).

Parameters:
Return type:

str

process_improve.experiments.designs_mixture_constrained.mixture_inequalities(factors, constraints)[source]#

Return (A, b) so the feasible mixtures are A @ x <= b with sum(x) = 1.

The rows are the component lower bounds, the upper bounds, then one row per inequality in constraints.

Raises:

ValueError – If the bounds cannot be met by any mixture (sum(low) > 1 or sum(high) < 1).

Parameters:
Return type:

tuple[np.ndarray, np.ndarray]

process_improve.experiments.designs_mixture_constrained.extreme_vertices(a_mat, b_vec)[source]#

Enumerate the vertices of {x : A x <= b, sum(x) = 1}.

Each inequality is first scaled to unit length and the ones implied by the others are dropped (see _drop_redundant_rows()), which leaves the region unchanged. Every subset of q - 1 remaining rows is then made active, stacked with the row sum(x) = 1 into a q by q system, and the systems are solved in batches. With every row of unit length, a system’s determinant is at most 1 in size, so the test for a singular system does not depend on how a constraint was scaled. Solutions that satisfy every constraint are vertices.

Raises:

ValueError – If more than MAX_VERTEX_SUBSETS systems would have to be solved.

Parameters:
Return type:

ndarray

process_improve.experiments.designs_mixture_constrained.mixture_candidates(a_mat, b_vec)[source]#

Return candidate blends by kind.

The kinds are vertices, edge midpoints and the points a third and two thirds of the way along each edge (edge_third, the points the x_i x_j (x_i - x_j) terms of a full cubic model need), centroids of the 2-dimensional faces (plane_centroid, the points the x_i x_j x_k terms of a special cubic model need), centroids of the facets (face_centroid), the overall centroid, and axial blends halfway from each vertex to it. Two vertices share an edge when the constraints active at both, together with sum(x) = 1, have rank q - 1: the set of points satisfying them is a line.

Parameters:
Return type:

dict[str, ndarray]

process_improve.experiments.designs_mixture_constrained.constrained_mixture_design(factors, budget, constraints=None, options=None, random_state=None)[source]#

Generate a mixture design over component bounds and linear constraints.

Parameters:
  • factors (list[Factor]) – Mixture factors; low and high are the component bounds as proportions.

  • budget (int or None) – Number of runs. None returns, for the model, the vertices and the overall centroid, plus the edge midpoints for a quadratic model, plus the centroids of the 2-dimensional faces for a special cubic one, and the edge third-points and 2-face centroids for a full cubic one. (This is not McLean and Anderson’s design, which adds the facet centroids instead.) When that is more than three runs per model term, as with many bounded components, a D-optimal design of n_terms + 5 runs is returned instead, and metadata["n_runs_vertices_edges_centroid"] records the size avoided. With options.candidates, None means a D-optimal design of n_terms + 3 runs. With a budget, a D-optimal subset of all candidate blends is chosen; blends may be replicated. A budget below the number of model terms raises a ValueError.

  • constraints (list[Constraint] or None) – Linear inequalities in the proportions, e.g. "x1 + x2 <= 0.7".

  • options (ConstrainedOptions or None) – model_type: "scheffe_linear", "scheffe_quadratic", "scheffe_special_cubic" or "scheffe_cubic", or the process-design names "main_effects", "interactions" and "quadratic", which map to linear, quadratic and quadratic. criterion: "d_optimal" (default), "i_optimal" (average prediction variance over the constrained simplex), "a_optimal" or "e_optimal".

  • random_state (int, numpy.random.Generator or None) – Seed for the exchange’s random starts.

Returns:

Proportions (n_runs, q) whose rows sum to 1, and metadata. n_vertices counts the region’s vertices and n_candidates the distinct blends the runs were chosen from (the feasible supplied ones, with options.candidates).

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – If the region is empty, a constraint is not linear, or the region cannot support the model (for example a region that is a single blend).

Mixture-process models: mixture components and process factors in one model.

In a mixture-process experiment the proportions x_1, ..., x_q of the ingredients sum to 1, while the process factors z_1, ..., z_m (temperature, mixing speed) range freely, in coded units from -1 to +1, or take named levels (a catalyst, a supplier). A model of both is a list of terms, each a product of atoms: a component x_i, a process factor z_k, a square z_k^2, or the cubic difference x_i x_j (x_i - x_j) of a full cubic Scheffé model. A categorical process factor of L levels is an atom of L - 1 sum-to-zero columns, and a term’s columns are the products of its atoms’ columns. The model matrix the design engine optimises and the formula recorded for evaluate_design() and analyze_experiment() are both built from that list, so they cannot disagree.

Three families of model are built (Goos and Jones 2011, section 6.3):

  • By order, "main_effects", "interactions" or "quadratic": the polynomial of that order in all q + m factors, less the terms that sum(x) = 1 makes redundant. The intercept is sum(x_i), a square x_i^2 is x_i - sum_{j != i} x_i x_j, and a process main effect z_k is sum_i x_i z_k. What remains is

    • first order: x_i and z_k, q + m terms;

    • with interactions: x_i, x_i x_j, x_i z_k and z_k z_l;

    • second order: those and z_k^2, the model of Kowalski, Cornell and Vining (2000), with q + q(q - 1)/2 + qm + m(m - 1)/2 + m terms; "kcv" names it too.

  • Additive, "<mixture model> + <process model>": a Scheffé model plus a process model without its intercept. "scheffe_quadratic + main_effects" is the model of Goos and Jones’s Table 6.2, q + q(q - 1)/2 + m terms.

  • Crossed, "<mixture model> * <process model>": every Scheffé term multiplied by every term of the process model, its intercept included. "scheffe_quadratic * interactions" has [q + q(q - 1)/2] x [1 + m + m(m - 1)/2] terms.

The mixture model on the left of + or * is a Scheffé model name ("scheffe_linear", "scheffe_quadratic", "scheffe_special_cubic", "scheffe_cubic"), and the process model on the right is "main_effects", "interactions" or "quadratic". A categorical factor has no square.

Without an intercept, patsy codes a categorical factor with a column for every level wherever the term below it is missing from the formula (spray beside the blends, and T:catalyst in the "interactions" model, where T enters only through x_i:T). Those columns add up to columns the blends already span, so the model could not be estimated. ReducedRank keeps every categorical factor at L - 1 columns, and evaluate_design() and analyze_experiment() code the formula with it.

The design (mixture_process_design()) is chosen from every candidate blend of the constrained simplex (the extreme vertices, edge points, face centroids and centroid of mixture_candidates()) at every candidate process setting (the grid of build_candidates()), by the Fedorov exchange the other optimal designs use. The I- and G-criteria average and maximise over points drawn uniformly from the mixture-process DesignRegion.

process_improve.experiments.designs_mixture_process.PROCESS_MODELS = ('main_effects', 'interactions', 'quadratic')#

The process models, and the order-based mixture-process models, by name.

process_improve.experiments.designs_mixture_process.MAX_CANDIDATES = 50000#

Most candidate runs (blends times process settings) the exchange searches; more are sampled down to this.

class process_improve.experiments.designs_mixture_process.Atom(code, names, power='plain', levels=())[source]#

Bases: object

One factor of a model term: a component or process factor, a square, a cubic difference, or a categorical.

Parameters:
  • code (str) – How the atom is written in a formula: "x1", "I(T ** 2)", "I(x1 * x2 * (x1 - x2))", "catalyst".

  • names (tuple[str, ...]) – The factors it is made of: one for a plain factor, a square or a categorical, two for a cubic difference.

  • power ({"plain", "square", "cubic", "categorical"}) – Which of the four it is.

  • levels (tuple) – A categorical factor’s levels, in the order they were declared; empty otherwise.

property width: int#

L - 1 for a categorical factor of L levels, else 1.

Type:

Return the number of model columns

columns(data)[source]#

Return the atom’s model columns at each point, shape (n_points, width).

data maps a factor name to its column: proportions or coded values, or the labels of a categorical factor, which are coded sum-to-zero, as categorical_effect_columns() does.

Parameters:

data (Mapping[str, ndarray])

Return type:

ndarray

process_improve.experiments.designs_mixture_process.Term#

the product of its atoms. The empty term is the intercept.

Type:

A model term

alias of tuple[Atom, …]

class process_improve.experiments.designs_mixture_process.ReducedRank(contrast=None)[source]#

Bases: object

A patsy contrast that codes a categorical factor in L - 1 columns wherever it appears.

Patsy gives a categorical factor all L columns where the term below it is absent from the formula, which in a model without an intercept makes the columns add up to ones the blends already span (see the module docstring). This contrast hands patsy the L - 1 columns of contrast in both cases.

Parameters:

contrast (object or None) – A patsy contrast such as Sum() or Treatment(). None means the sum-to-zero coding of the design engine, chosen by the number of levels: Sum(omit=0) for two (the first, in sorted order, at -1), Sum() for more (the last at -1 in every column).

code_without_intercept(levels)[source]#

Return the L - 1 columns of the contrast.

Parameters:

levels (list)

Return type:

object

code_with_intercept(levels)[source]#

Return the same L - 1 columns: the blends stand in for the intercept.

Parameters:

levels (list)

Return type:

object

class process_improve.experiments.designs_mixture_process.ReducedRankFormula(text, categorical)[source]#

Bases: str

A model formula whose categorical factors patsy codes in L - 1 columns wherever they appear.

It is the formula as written, so it reads and validates as a string, and statsmodels keeps it as the model’s formula: a refit from model.formula, and a prediction at new runs, code the factors the same way. Patsy reads it through __patsy_get_model_desc__, which swaps each factor that names a column in categorical for one carrying ReducedRank. The coefficient names stay readable: catalyst[S.A], T:catalyst[S.A].

Parameters:
  • text (str) – The formula, "y ~ -1 + x1 + ...", or its right-hand side.

  • categorical (Iterable[str]) – The names of the categorical columns.

Return type:

ReducedRankFormula

class process_improve.experiments.designs_mixture_process.MixtureProcessModel(name, terms)[source]#

Bases: object

A model in mixture components and process factors, as its list of terms.

Parameters:
  • name (str) – The model’s name: "interactions", "scheffe_quadratic + main_effects", …

  • terms (tuple[Term, ...]) – The terms, each a product of atoms. No term is an intercept, since the components sum to 1.

property n_parameters: int#

The number of coefficients, one per term and L - 1 per categorical factor in it.

property has_squares: bool#

True when a term holds a squared process factor, which then needs three levels.

formula_rhs()[source]#

Return the right-hand side of the model’s formula, without an intercept: "-1 + x1 + x2 + ...".

Return type:

str

matrix(data)[source]#

Return the model matrix, a column per term: components as proportions, process factors coded.

Parameters:

data (Mapping[str, np.ndarray]) – One array per factor, all the same length: labels for a categorical factor.

Returns:

The model matrix, of shape (n_points, n_parameters).

Return type:

np.ndarray

process_improve.experiments.designs_mixture_process.scheffe_terms(names, model)[source]#

Return a Scheffé model’s terms, in the column order of scheffe_matrix().

That is the components, their products in pairs, the cubic differences (full cubic model only) and the products in threes (special and full cubic models).

Parameters:
Return type:

list[tuple[Atom, …]]

process_improve.experiments.designs_mixture_process.process_terms(names, model, levels=None)[source]#

Return a process model’s terms, intercept first: the intercept, main effects, interactions, squares.

This is the column order of the candidate exchange’s model matrix (model_matrix()). A factor named in levels is categorical, so it has no square.

Parameters:
Return type:

list[tuple[Atom, …]]

process_improve.experiments.designs_mixture_process.mixture_process_model(mixture, process, model_type, levels=None)[source]#

Build the mixture-process model model_type names, for these components and process factors.

Parameters:
  • mixture (list[str]) – The mixture components’ names.

  • process (list[str]) – The process factors’ names.

  • model_type (str) – "main_effects", "interactions", "quadratic" (or "kcv"), or a Scheffé model and a process model joined by + (added) or * (crossed), such as "scheffe_quadratic * interactions". See the module docstring.

  • levels (Mapping[str, Sequence] or None) – The levels of each categorical process factor, keyed by its name; the process factors it does not name are continuous.

Returns:

The model, with its terms in order.

Return type:

MixtureProcessModel

Raises:

ValueError – If model_type names no such model.

Examples

>>> mixture_process_model(["x1", "x2", "x3"], ["T", "P"], "quadratic").n_parameters
15
>>> mixture_process_model(["x1", "x2", "x3"], ["T", "P"], "scheffe_quadratic * interactions").n_parameters
24
>>> mixture_process_model(["x1", "x2", "x3"], ["T", "cat"], "interactions", {"cat": "ABC"}).n_parameters
17
process_improve.experiments.designs_mixture_process.split_constraints(constraints, mixture, process)[source]#

Return the constraints on the components and the constraints on the process factors.

A mixture-process region is the product of the constrained simplex and the constrained process box, so each constraint must name factors of one side only. A constraint that cannot be parsed, or names no factor, goes to the process side, whose parser then gives its usual message.

Raises:

ValueError – If a constraint names both a component and a process factor.

Parameters:
Return type:

tuple[list[Constraint], list[Constraint]]

process_improve.experiments.designs_mixture_process.category_levels(factors)[source]#

Return the levels of each categorical factor, keyed by its name.

Parameters:

factors (list[Factor])

Return type:

dict[str, tuple]

process_improve.experiments.designs_mixture_process.mixture_process_design(factors, budget, constraints=None, options=None, random_state=None)[source]#

Generate an optimal design for mixture components together with process factors.

Parameters:
  • factors (list[Factor]) – Mixture components (low and high their bounds as proportions), and continuous and categorical process factors, in any order.

  • budget (int or None) – Number of runs. None means the model’s number of terms plus 3. A budget below the number of terms raises a ValueError. A candidate run may be chosen more than once.

  • constraints (list[Constraint] or None) – Linear inequalities in the proportions, and inequalities in the process factors’ actual units; each names components only or process factors only.

  • options (ConstrainedOptions or None) – model_type, a mixture-process model (see mixture_process_model(); default "interactions"), and criterion: "d_optimal" (default), "i_optimal", "a_optimal", "e_optimal", "g_optimal" or "k_optimal".

  • random_state (int, numpy.random.Generator or None) – Seed for the exchange’s random starts and the region sample.

Returns:

The runs (n_runs, n_factors) in the factors’ order, components as proportions, continuous process factors in coded units and categorical ones as level labels (the array is then of objects), and metadata. formula is the model’s right-hand side, to give evaluate_design() or analyze_experiment().

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – If a constraint names both kinds of factor or a categorical factor, the region is empty, or the candidates cannot support the model.

Supersaturated two-level designs: more factors than runs.

A supersaturated design screens k factors in n < k + 1 runs. The columns cannot all be orthogonal, so the design is chosen to keep them as close to orthogonal as possible, measured by E(s^2): the average of s_ij^2 over all pairs of columns, with s_ij = x_i' x_j. It only works under effect sparsity, when a few of the many factors are active, and the analysis has to search for them (stepwise selection, the lasso, or the Dantzig selector), not fit them all at once.

Construction (Lin 1993): take a normalised Hadamard matrix of order N (first column all +1), pick one of its other columns as the branching column, and keep the N / 2 rows where it is +1. The remaining N - 2 columns are balanced (half +1, half -1) in those rows, and nearly orthogonal. Each choice of branching column gives a different design, so all are tried and the one with the fewest fully aliased pairs of columns (|s_ij| = N / 2), then the smallest E(s^2), then the smallest max |s_ij|, is kept. When fewer than N - 2 factors are needed, the columns that contribute most to E(s^2) are dropped one at a time.

Hadamard matrices come from Paley’s construction H = I + C (C the skew conference matrix, for N - 1 a prime power congruent to 3 mod 4), the classical Plackett-Burman construction, and the other constructions in process_improve.experiments._finite_fields; every multiple of 4 up to 232 is covered.

process_improve.experiments.designs_supersaturated.hadamard(order)[source]#

Return a normalised Hadamard matrix of order (first column all +1), or None if none is built here.

Paley’s type I matrix comes first where it exists: its cyclic structure gives half-fractions without identical columns, where the Sylvester-type matrices for 16, 24 and 32 always have some. The classical Plackett-Burman matrices come next, then the other constructions (Paley II, doubling and Goethals-Seidel) for the remaining orders.

Parameters:

order (int)

Return type:

ndarray | None

process_improve.experiments.designs_supersaturated.e_s2(design)[source]#

E(s^2): the mean of (x_i' x_j)^2 over all pairs of distinct columns.

Parameters:

design (ndarray)

Return type:

float

process_improve.experiments.designs_supersaturated.e_s2_lower_bound(n_runs, n_factors)[source]#

Return a lower bound on E(s^2) for balanced two-level designs with n_factors >= n_runs.

The bound is Nguyen’s (1996) and Tang and Wu’s (1997) n^2 (m - n + 1) / ((m - 1)(n - 1)) for m factors in n runs, raised to 4 when n = 2 mod 4: two balanced columns then have s_ij = 2 mod 4, so every |s_ij| >= 2, a floor the general bound falls below for small m. The sharper bounds of Bulutoglu and Cheng (2004) are not used, so a design can be E(s^2)-optimal with an efficiency below 1. For m < n (not supersaturated) the bound is 0.

Parameters:
  • n_runs (int)

  • n_factors (int)

Return type:

float

process_improve.experiments.designs_supersaturated.n_fully_aliased(design)[source]#

Count column pairs that are identical or opposite (|s_ij| = n): their effects cannot be told apart.

Parameters:

design (ndarray)

Return type:

int

process_improve.experiments.designs_supersaturated.supersaturated_runs(k, budget)[source]#

Return the largest run count up to budget with an unaliased supersaturated design for k factors.

The run count is even, at most k (fewer than k + 1 runs), 2 * n is a Hadamard order built here, and Lin’s half-fraction has no fully aliased pair of factors. None when there is no such count, or when budget reaches k + 1 and so is not supersaturated.

Parameters:
Return type:

int | None

process_improve.experiments.designs_supersaturated.supersaturated_available(k, budget)[source]#

Whether an unaliased supersaturated design for k factors fits in budget runs.

Automatic design selection asks this before choosing a supersaturated design, so a budget with no such design falls through to the other families instead of raising. The budget is a ceiling: supersaturated_runs() gives the run count used.

Parameters:
Return type:

bool

process_improve.experiments.designs_supersaturated.dispatch_supersaturated(factors, budget=None)[source]#

Generate a supersaturated design: k two-level factors in fewer than k + 1 runs.

Parameters:
  • factors (list[Factor]) – Continuous factors, run at their low and high levels (coded -1 / +1).

  • budget (int or None) – Number of runs. It must be even, below k + 1, and 2 * budget must be a Hadamard order built here (every multiple of 4 up to 232). None picks the smallest order N with N - 2 >= k and N / 2 <= k whose half-fraction has no fully aliased pair of factors, giving N / 2 runs. A budget that forces fully aliased pairs is honoured, with a UserWarning naming the run count that avoids them.

Returns:

The coded design and metadata: e_s2, its lower bound (see e_s2_lower_bound()) and their ratio e_s2_efficiency (1 means the design attains the bound, so nothing balanced can do better), max_abs_s, and the Hadamard order used. The bound is not attainable at every size, so an efficiency below 1 does not by itself mean a better balanced design exists: 15 factors in 12 runs report about 0.63.

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – For non-continuous factors, fewer than 3 factors, a fractional budget, a budget that is not supersaturated (budget >= k + 1: use "plackett_burman"), a budget with no Hadamard matrix of order 2 * budget, or no budget when no unaliased design exists (3 to 5 factors).

Space-filling designs: points spread evenly through the region, for any model.

Model-based designs place runs where a chosen polynomial is best estimated. A space-filling design assumes no model: it spreads the runs out, which suits computer experiments, Gaussian-process or machine-learning surrogates, and exploratory work where the shape of the response is unknown.

Seven methods, in two groups:

  • On the factor box only (projections onto each factor are what they control):

    • "latin_hypercube": each factor’s range is cut into n equal slices, and each slice is used exactly once (McKay, Beckman and Conover 1979).

    • "maximin_lhs": a Latin hypercube, with each run at the centre of its slices, whose smallest distance between two runs is made large by swapping values within a column while the Morris-Mitchell criterion improves.

    • "uniform": a Latin hypercube on the slice centres (a U-type design) with low centred L2 discrepancy, the criterion of uniform designs (Fang and Wang).

  • On any region, including constrained boxes and constrained mixtures:

    • "sobol" and "halton": scrambled low-discrepancy sequences. In a constrained region the sequence is mapped to the region (onto the simplex for a mixture) and its infeasible points are skipped, keeping the order of the rest.

    • "maximin": runs chosen from a dense uniform sample of the region, plus its boundary points, by a farthest-point build followed by exchanges that raise the smallest distance between runs.

    • "maxpro": maximum projection designs (Joseph, Gul and Ba 2015), which keep the runs apart in every projection onto a subset of the factors, not only in the full space. On the box: the best of several Latin hypercubes, improved by swapping values within a column and then by a bounded continuous search. In a constrained or mixture region: runs chosen from a uniform sample of the region by exchange.

Distances are measured in design units: coded [-1, 1] for a box, proportions for a mixture.

process_improve.experiments.designs_space_filling.BOX_ONLY = ('latin_hypercube', 'maximin_lhs', 'uniform')#

Methods that work only on the plain factor box.

process_improve.experiments.designs_space_filling.ANY_REGION = ('sobol', 'halton', 'maximin', 'maxpro')#

Methods that also work in constrained and mixture regions.

process_improve.experiments.designs_space_filling.maxpro_criterion(points)[source]#

Joseph, Gul and Ba’s (2015) maximum projection criterion (lower is better).

psi(D) = [ (1 / C(n, 2)) * sum_{i<j} 1 / prod_l (x_il - x_jl)^2 ]^(1/k). A pair of runs that nearly coincide in any one factor makes its term large, so a small value means the runs are spread in every projection. Computed in log space.

Parameters:

points (ndarray)

Return type:

float

process_improve.experiments.designs_space_filling.space_filling_design(factors, n_runs, method, constraints=None, random_state=None)[source]#

Generate a space-filling design.

Parameters:
  • factors (list[Factor]) – Continuous factors, or mixture components. Categorical factors are refused: distance between levels has no meaning.

  • n_runs (int or None) – Number of runs. None uses 10 * k, the usual starting size for fitting a Gaussian-process surrogate (Loeppky, Sacks and Welch 2009).

  • method (str) – One of SPACE_FILLING_METHODS; see the module docstring.

  • constraints (list[Constraint] or None) – Inequalities in actual units (proportions for a mixture). Only the "sobol", "halton", "maximin" and "maxpro" methods accept them.

  • random_state (int, numpy.random.Generator or None) – Seed for the scrambling, the sampling and the swaps.

Returns:

Coded points (proportions for a mixture) and metadata: the smallest and the mean nearest-neighbour distance between runs, and on the plain box centered_l2_discrepancy, Hickernell’s squared centred L2 discrepancy CD^2 of the points mapped to [0, 1]^k (lower is more uniform), as scipy.stats.qmc.discrepancy() computes it.

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – For an unknown method, a categorical factor, fewer than 2 runs or a fractional run count, a box-only method asked for a constrained or mixture region, or a "sobol" / "halton" request in a region too thin to fill from the first 2**22 points of the sequence.

Notes

On the plain box "maximin" pushes the runs to the faces and corners, as the maximin criterion does: in 8 or more factors every run sits on the 3-level grid {-1, 0, 1}, so many runs coincide when projected onto a few factors. When the projections matter (a surrogate in which only some factors are active), use "maximin_lhs", whose runs take n distinct values in every factor.

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 puts every factor at its middle level. The 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. Each factor must also reach an outer level in some half-run, sum_r |x[r,i]| s_r >= 1; otherwise its column is all zeros, orthogonal but not three-level. 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 constraints - k(k-1)/2 equalities and k coverage rows - which keeps it tractable up to seven factors. The ILP is solved with HiGHS, through scipy.optimize.milp. Because the coefficients are integers, the constraints are exact; every selection the solver returns is re-checked exactly, and 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 steers the solver towards a different feasible design, so the retained designs span the high-D-efficiency / low-A members. Each of these solves stops after a fixed number of branch-and-bound nodes, which ends it at the same point on every run, so the search is reproducible for a fixed seed. Designs from which the sizing model cannot be estimated are set aside, and each one teaches the solver a cover cut that keeps later solves away from every design sharing its rank deficiency (_estimability_cuts()). Designs that differ only in run order, factor order or factor signs are recognised as one (_canonical_key()). When the selection is driven by D-efficiency, every design found is then improved by a local search over run swaps that keep the main effects orthogonal (_SwapIndex). The best design is 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, node_limit=100, time_limit=60.0, node_limited_solves=0, time_limited_solves=0, rank_deficient_designs=0, size_proven_minimal=None, run_sizes_searched=0, plateau=None)[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)

  • node_limit (int | None)

  • time_limit (float)

  • node_limited_solves (int)

  • time_limited_solves (int)

  • rank_deficient_designs (int)

  • size_proven_minimal (bool | None)

  • run_sizes_searched (int)

  • plateau (int | None)

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 (enumerated, on the exhaustive path). The rank_deficient_designs among them are set aside; the rest are 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

node_limit#

Branch-and-bound node budget of each randomized-objective solve (None means unlimited).

Type:

int or None

time_limit#

Wall-clock cap, in seconds, on each ILP solve.

Type:

float

node_limited_solves#

Number of ILP solves that the node budget stopped before optimality was proven. These are expected and deterministic.

Type:

int

time_limited_solves#

Number of ILP solves that the wall-clock cap stopped. When this is nonzero the search depends on machine speed, so a fixed random_state may not reproduce the design.

Type:

int

rank_deficient_designs#

Number of distinct OMARS designs found, but set aside because the sizing model is not estimable from them (rank below the parameter count).

Type:

int

size_proven_minimal#

True when the returned run size was proven to be the smallest feasible one in the window. False when a limit stopped the minimise-size solve first, or when no design at the proven minimum could estimate the model and the search moved up. None when the run size was pinned by n_runs or lies beyond the distinct half-runs (see n_runs_range in generate_omars()).

Type:

bool or None

run_sizes_searched#

Number of run sizes searched. More than one means that no design at the smallest feasible size could estimate the sizing model, so the search moved up the window.

Type:

int

plateau#

The plateau rule of the multistart: it stopped at a size once this many consecutive estimable designs failed to improve the Pareto front on D-efficiency and the maximum second-order correlation. None (the generate_omars() default) means no plateau rule: only n_restarts and the restart patience end the multistart.

Type:

int or None

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, require_any=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; n_half wins if both are given. The returned design has 2 * h + 1 runs for h selected half-runs. The integer program is solved with HiGHS, through scipy.optimize.milp.

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 towards a different feasible OMARS design, which is how generate_omars() samples diverse, high-quality designs. Takes precedence over minimize_size. Only solves with an objective are subject to the node_limit solver option, and they run without the RINS and RENS sub-MIP heuristics and with pseudocost branching in place of strong branching, which together took most of each such solve.

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

  • require_any (list[list[int]], optional) – Cover cuts: for each listed set of half-pool rows, at least one of them must be selected. generate_omars() uses these to steer the search away from rank-deficient designs (see _estimability_cuts()).

  • solver_options (dict, optional) –

    Any of the keys:

    • "msg" (bool, default False): print the HiGHS log to standard output.

    • "time_limit" (float, default 60.0): wall-clock cap in seconds on the solve; math.inf removes it. A solve stopped by this cap depends on machine speed.

    • "node_limit" (int or None, default 100): branch-and-bound node budget for a solve with an objective. The budget stops the solve at the same point on every run, so results stay reproducible; None solves to proven optimality.

Returns:

(design or None, solver_status, chosen_half_indices). None means the solver returned no feasible selection. solver_status is one of "Optimal", "Node limit" or "Time limit" (a limit stopped the solve; the best selection found is returned), "Infeasible", or "Not Solved".

Return type:

tuple

Raises:
  • TypeError – If solver_options is not a dict, or one of its values has the wrong type.

  • ValueError – If neither n_half nor half_bounds is given, or solver_options has an unknown key or an out-of-range value.

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=None, model='full_second_order', solver_options=None, tol=1e-09, verify=True, random_state=42)[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 search starts at the smallest feasible size and moves up the window only if no design at a size can estimate the model. A window that ends below the smallest estimable size raises ValueError. Sizes needing more half-runs than the (3**k - 1) / 2 distinct ones are reachable only by repeating half-runs, which the exhaustive search does for three and four factors.

  • 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 and ranks last under every criterion, so it is selected only when no design with every term present was found.

  • 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 steers the solver towards 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; 0 runs only the plain feasibility solve at each size. The search early-stops once the feasible set stops yielding new designs, so small factor counts finish quickly regardless. The budget applies at each run size the search visits. Default 50, which reaches catalogue-competitive D-efficiency for up to seven factors. For the "dominance" and "d_efficiency" criteria each design found is also improved by a local search over run swaps, whose cost does not count against this budget. Deterministic for a fixed random_state, as long as no solve hits solver_options["time_limit"] (see random_state).

  • max_candidates (int, optional) – Legacy alias retained for backward compatibility. When given, it 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 None: no floor.

  • 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) – Settings for the HiGHS solves, any of: "msg" (bool, default False) prints the HiGHS log to standard output; "time_limit" (float seconds, default 60.0) caps each solve’s wall-clock time; "node_limit" (int or None, default 100) caps the branch-and-bound nodes of each randomized-objective solve, and None solves each one to proven optimality. Unknown keys raise ValueError. See solve_omars_ilp().

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

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

  • random_state (int, numpy.random.Generator or None, default 42) – 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 on a given SciPy version: the node budget, not the clock, ends each solve. A solve stopped by the wall-clock time_limit instead depends on machine speed; metadata["omars_search"].time_limited_solves counts them. Another SciPy release ships another HiGHS, which may return a different design for the same seed.

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, n_restarts is negative, solver_options has an unknown key or an out-of-range value, or no feasible design from which the sizing model can be estimated is found.

  • TypeError – If solver_options is not a dict, or one of its values has the wrong type.

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 two-level full factorial (2^k) design in coded units.

Parameters:
  • nfactors (int) – Number of factors, k. The design has 2**k runs.

  • names (list of str or None) – One name per factor. If None, names are created (A, B, …).

Returns:

One Column per factor, each of length 2**k, holding the coded settings -1 and +1 in standard (Yates) order.

Return type:

list[Column]

Raises:

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

Examples

>>> from process_improve.experiments.designs_factorial import full_factorial
>>> columns = full_factorial(2, names=["Temp", "Pressure"])
>>> len(columns), len(columns[0])
(2, 4)

Crossed arrays for robust design: an inner array of controllable factors run at every point of an outer array.

In a robust-design study (Taguchi; Montgomery, Design and Analysis of Experiments, chapter 12) the inner array sets the factors that will be controlled in production, and the outer array the noise factors, which can be controlled only during the experiment. Crossing the two runs every inner run at every outer run, so each inner run gives as many readings as the outer array has runs, and their mean and variance show how the controllable settings fare across the noise. signal_to_noise() sums up each inner run’s readings as one of Taguchi’s signal-to-noise ratios.

process_improve.experiments.crossed_arrays.cross_arrays(inner, outer, *, units='coded')[source]#

Run every run of an inner array at every run of an outer array.

Parameters:
  • inner (DesignResult or pandas.DataFrame) – The inner array: the controllable factors, one run per row.

  • outer (DesignResult or pandas.DataFrame) – The outer array: the noise factors, one run per row. To read each pair of runs more than once, repeat the outer runs (generate_design(..., n_replicates=3)).

  • units ({"coded", "actual"}, default "coded") – For a DesignResult, which of its tables to cross: design or design_actual. A DataFrame is crossed as it is given.

Returns:

One row for each pair of runs, len(inner) * len(outer) rows, ordered by the inner run and then by the outer run. The columns are inner_run and outer_run (the position of the run in its array, from 1), then the inner array’s factors and the outer array’s. The RunOrder column of a design is left out. To lay the readings out as the sources print them, one row per inner run and one column per outer run, use crossed.pivot(index="inner_run", columns="outer_run", values="y").

Return type:

pandas.DataFrame

Raises:

ValueError – If an array has no runs or no factors, has a Block or WholePlot column, or shares a factor name with the other array, or if units is neither "coded" nor "actual".

Notes

A DesignResult is crossed in the order of its table, which is its randomised run order. For the standard order, pass result.design.iloc[numpy.argsort(result.run_order)].

The mean and the variance of each inner run’s readings, the summaries of a robust-design study, are crossed.groupby("inner_run")["y"].agg(["mean", "var"]).

Examples

Montgomery’s leaf springs (Table 12.1): a 2^(4-1) inner array in the controllable factors A to D, run three times at each level of the noise factor E, so each inner run gives six readings.

>>> from process_improve.experiments import Factor, cross_arrays, generate_design
>>> inner = generate_design([Factor(name=n, low=-1, high=1) for n in "ABCD"], "fractional_factorial",
...                         n_center_points=0, random_state=1)
>>> outer = generate_design([Factor(name="E", low=-1, high=1)], "full_factorial", n_center_points=0,
...                         n_replicates=3, random_state=1)
>>> crossed = cross_arrays(inner, outer)
>>> crossed.shape
(48, 7)
>>> list(crossed.columns)
['inner_run', 'outer_run', 'A', 'B', 'C', 'D', 'E']
process_improve.experiments.crossed_arrays.signal_to_noise(readings, kind)[source]#

Return Taguchi’s signal-to-noise ratio of repeated readings, in decibels: the higher, the better.

Parameters:
  • readings (array_like) – The readings at one setting, or a table of them with one row for each setting (each inner run of a crossed array) and one column for each reading. A blank (NaN) reading is left out.

  • kind ({"larger", "smaller", "nominal"}) – What makes a reading good. "larger" (larger is better) is -10 log10 of the mean of 1/y^2, "smaller" (smaller is better) is -10 log10 of the mean of y^2, and "nominal" (nominal is best) is 10 log10 of the squared mean over the variance.

Returns:

The ratio of the readings, or one ratio for each row of a table.

Return type:

float or numpy.ndarray

Raises:

ValueError – If kind is not one of the three, or readings is not one or two dimensional.

Notes

Larger is better for a reading of 0 is -inf, and nominal is best for readings that do not vary is +inf. Nominal is best needs two readings for the variance: with fewer it is NaN.

For nominal is best, Taguchi’s two-step optimisation then picks the settings with the highest ratio and moves the mean to the target with a factor that changes the mean but not the ratio.

Examples

Schmidt and Launsby’s signal-to-noise example: the first two settings of a 2^(3-1) design, each read twice.

>>> from process_improve.experiments import signal_to_noise
>>> signal_to_noise([[20, 30], [9, 11]], "nominal").round(2)
array([10.97, 16.99])
>>> round(signal_to_noise([20, 30], "larger"), 2)
27.43

Three-level fractional factorials, \(3^{k-p}\), made from generators in arithmetic modulo 3.

Each factor takes the digits 0, 1 and 2, its low, middle and high levels. A \(3^{k-p}\) fraction runs the full \(3^{k-p}\) design in its base factors and sets each of the \(p\) derived factors by a generator, a sum of base digits modulo 3 (Montgomery, Design and Analysis of Experiments, section 9.3):

"D=A2BC2"   means   x_D = 2 x_A + x_B + 2 x_C  (mod 3)

A generator gives a word of the defining relation: here \(A^2BC^2D^2\), the derived factor moved across with the exponent \(-1 = 2\). A word and its square are the same component, written with its first exponent 1, so this one is \(AB^2CD\), Montgomery’s I = AB2CD (all exponents times 2, modulo 3). The relation holds every product of powers of the generator words, \((3^p - 1)/2\) components in all, and the fewest letters in any of them is the design’s resolution. An effect is aliased with its products with each component and with that component’s square.

Exponents are written as a digit after a one-letter factor name (AB2C), or as ^2 or ²; names longer than one letter are joined with * (Temp*Feed^2).

process_improve.experiments.designs_three_level.MODULUS = 3#

its generators and defining relation work modulo 3.

Type:

The arithmetic of a three-level fraction

process_improve.experiments.designs_three_level.dispatch_three_level_fraction(factors, generators, codes, resolution=None)[source]#

Build a \(3^{k-p}\) fraction of three-level factors from its generators.

Parameters:
  • factors (list[Factor]) – Factors with three levels each: continuous factors with three levels, or categorical factors with three levels.

  • generators (list[str]) – One generator for each derived factor, such as "D=A2BC2", its right-hand side a word in the base factors.

  • codes (list[numpy.ndarray]) – The three coded levels of each factor, in the order of the digits 0, 1 and 2.

  • resolution (int or None) – The least resolution the design must reach; generators that fall short raise ValueError.

Returns:

The coded design matrix, the first base factor changing fastest, and metadata with generators_used, modulus (3), levels_per_factor, resolution, defining_relation (each component as "I=AB2CD", fewest letters first) and aliases (each main effect’s alias chain). A relation of more than 1023 components (more than six generators) is not listed: defining_relation_note says so, and more than ten generators leave the resolution unknown as well.

Return type:

tuple[np.ndarray, dict]

Raises:

ValueError – If a generator is not of the form "D=A2BC2" (one derived factor on the left, a word in the base factors on the right), a factor is derived twice, or the design falls short of resolution.

The orthogonal components of the interactions of three-level factors.

In a \(3^k\) factorial each main effect has two degrees of freedom, and an interaction of \(m\) factors has \(2^m\). Arithmetic modulo 3 splits the interaction into \(2^{m-1}\) components of two degrees of freedom each (Montgomery, Design and Analysis of Experiments, section 9.2; Davies, The Design and Analysis of Industrial Experiments, chapter 9). The component \(AB^2\) groups the runs by \(x_A + 2 x_B\) (mod 3), each factor’s level written 0, 1 or 2, and its sum of squares is that of the three group totals:

\[SS = \sum_{u=0}^{2} \frac{T_u^2}{n_u} - \frac{T^2}{N}.\]

A component and its square group the runs alike, so each is written with its first exponent 1, as in designs_three_level. In a full factorial, replicated or not, the components of an interaction add up to its sum of squares.

Yates named the components of a two-factor interaction I and J, and those of a three-factor interaction W, X, Y and Z: \(I(AB) = AB^2\) and \(J(AB) = AB\), and

\[W(ABC) = I[I(AB) \times C] = AB^2C^2, \quad X(ABC) = J[I(AB) \times C] = AB^2C, \quad Y(ABC) = I[J(AB) \times C] = ABC^2, \quad Z(ABC) = J[J(AB) \times C] = ABC.\]
process_improve.experiments.three_level_components.interaction_components(data, response, factors)[source]#

Return each main effect and each component of interaction of three-level factors, with its sum of squares.

Parameters:
  • data (pandas.DataFrame) – One row per run: the factors’ settings and the response.

  • response (str) – The response column.

  • factors (sequence of str) – The three-level factors, in the order the components are written in. A factor’s three settings are its levels 0, 1 and 2: the categories in order for a categorical column, otherwise its values sorted, lowest first. The components of an interaction depend on this order; their total does not.

Returns:

One row per main effect and per component, main effects first and then the interactions of two, three and more factors: effect ("AB"), component ("AB2", Montgomery’s form, or "Temp*Feed^2" when a name is longer than one letter), yates ("I(AB)" or "W(ABC)", and empty for an interaction of more than three factors, which Yates did not name), df (2) and sum_of_squares.

Return type:

pandas.DataFrame

Raises:

ValueError – If the response or a factor is not a column, a factor is named twice or does not have exactly three settings, or the response is missing or not finite in a run.

Examples

Montgomery’s tool life (Example 5.5): two runs at each of the nine settings of the angle and the speed. The two components of their interaction add up to its 61.33.

>>> tool = pd.DataFrame({
...     "A": [15, 15, 15, 15, 15, 15, 20, 20, 20, 20, 20, 20, 25, 25, 25, 25, 25, 25],
...     "B": [125, 125, 150, 150, 175, 175] * 3,
...     "y": [-2, -1, -3, 0, 2, 3, 0, 2, 1, 3, 4, 6, -1, 0, 5, 6, 0, -1],
... })
>>> table = interaction_components(tool, "y", ["A", "B"])
>>> table[["component", "yates"]].to_numpy().tolist()
[['A', ''], ['B', ''], ['AB', 'J(AB)'], ['AB2', 'I(AB)']]
>>> table["sum_of_squares"].round(3).tolist()
[24.333, 25.333, 33.333, 28.0]

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 and to leave out the date and time of the call, so that the same fit gives the same summary. 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 Series indexed by term.

Parameters:

drop_intercept (bool)

Return type:

Series

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.

The title is the data’s pi_title. lm also accepts a plain DataFrame, which has none, so a missing attribute is an empty title rather than an error.

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.999999999)[source]#

Create a linear model.

Parameters:
  • model_spec (str) – The model formula, e.g. "y ~ A * B". A column named like a function the formula calls, as I in I(P ** 2), is fitted under a stand-in name and reported under its own.

  • data (Expt) – The data; a plain Expt or DataFrame works too.

  • name (str or None) – The model’s name; defaults to the data’s pi_title, if it has one.

  • alias_threshold (float or None) – Two model columns whose absolute correlation exceeds this are treated as aliases, and the longer term is dropped from the fit and reported as an alias. The default detects only exact aliasing (a correlation of one up to rounding), as in a fractional factorial; a column that is merely highly correlated, such as a square in actual units, is kept. None drops nothing.

Returns:

The fitted model.

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; or if a column has the name of a function the formula calls, such as I in I(P ** 2).

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

F tests between nested models: the extra sum of squares (#789 B2).

compare_models() tests a model against a fuller one fitted to the same runs, as R’s anova(reduced, full) does. group_test() tests a group of a model’s terms together, with the smaller model built by leaving them out. Neither reads an ANOVA table, so a group of terms is tested at once, however many rows of a table it would span.

process_improve.experiments.model_comparison.compare_models(reduced, full, error=None)[source]#

Test a model against a fuller one fitted to the same runs: the extra-sum-of-squares F test.

The sum of squares is the reduced model’s residual sum of squares less the full model’s, on the difference of their residual degrees of freedom. It is tested against the full model’s residual mean square, or against that of error, a third model that contains the full one, as Design-Expert tests a mixture’s linear blending against the residual of the quadratic model. With the full model saturated in the design’s factors, the test is the test of lack of fit against pure error.

Parameters:
  • reduced (Model or RegressionResultsWrapper) – The smaller model, from lm() or a statsmodels least-squares fit. Every column of its model matrix must be a combination of the full model’s columns.

  • full (Model or RegressionResultsWrapper) – The fuller model, fitted to the same runs of the same response.

  • error (Model or RegressionResultsWrapper, optional) – A model that contains the full one, whose residual mean square is the denominator.

Returns:

ss, df and ms of the difference; f and p_value; and ss_error, df_error and ms_error, the denominator’s.

Return type:

dict[str, float]

Raises:

ValueError – If the models are not fitted to the same runs of the same response (a missing value in a column only one of them uses drops that run from it alone), the reduced model is not nested in the full one, or the full one in error, the full model has no more estimable terms than the reduced one, or the denominator has no residual.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import compare_models, lm
>>> runs = pd.DataFrame({"A": [-1, 1, -1, 1, 0, 0], "B": [-1, -1, 1, 1, 0, 0], "y": [3, 5, 4, 7, 4.6, 4.9]})
>>> found = compare_models(lm("y ~ A + B", runs), lm("y ~ A * B", runs))
>>> found["df"], round(found["ss"], 4), round(found["f"], 3)
(1, 0.25, 11.111)
process_improve.experiments.model_comparison.group_test(model, terms)[source]#

Test a group of a model’s terms together: the model against itself without them.

The F test of compare_models(), with the reduced model built by leaving terms out of model and fitted to the same runs, such as the first-order terms of a quadratic model, or the interactions an analysis pools. A categorical factor’s main effect left out beside its interaction tests nothing, since the interaction’s columns then span it, as in R.

Parameters:
  • model (Model or RegressionResultsWrapper) – A model from lm(), or a statsmodels least-squares fit made from a formula.

  • terms (str or Sequence[str]) – The terms to test, each named as in the formula: "A", "A:B" (or "B:A"), "I(A ** 2)", "Intercept".

Returns:

What compare_models() returns, with the reduced formula it built and the terms tested.

Return type:

dict[str, Any]

Raises:
  • TypeError – If the model was not fitted from a formula.

  • ValueError – If terms is empty, names a term the model does not have, or names several terms in one string; and as compare_models() raises.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import group_test, lm
>>> runs = pd.DataFrame({"A": [-1, 1, -1, 1, 0, 0], "B": [-1, -1, 1, 1, 0, 0], "y": [3, 5, 4, 7, 4.6, 4.9]})
>>> found = group_test(lm("y ~ A * B", runs), ["A:B"])
>>> found["reduced"], found["df"], round(found["ss"], 4)
('y ~ A + B', 1, 0.25)

Analysis of data from OMARS designs (orthogonal minimally aliased response surface).

OMARS designs are three-level response surface designs in which the main effects are mutually orthogonal and orthogonal to the second-order effects, while the second-order effects (two-factor interactions and quadratic terms) are only minimally aliased with one another. This orthogonality structure permits a staged analysis that first resolves the main effects and then, using a pooled estimate of the error variance, searches the second-order space for the small number of active interaction and quadratic terms.

analyze_omars() implements that staged protocol. It does not require the design to have been produced by any particular generator, but the protocol is valid only for a design with the OMARS structure: main effects that are balanced, mutually orthogonal, and orthogonal to every second-order term. The main effects are estimated without adjusting for the second-order terms, and the pooled error and the second-order projection both rely on that orthogonality, so on any other design (a three-level design with runs missing, a Plackett-Burman design) a pure quadratic effect leaks into the main-effect tests. analyze_omars() checks the structure and warns when it is absent.

The procedure, summarised:

  1. Code each factor to the [-1, +1] range and centre the response.

  2. Estimate the error variance from the residuals of the full second-order model (intercept + main effects + all quadratics and interactions).

  3. Test the main effects with t-tests at alpha_main and split them into active and inactive sets.

  4. Pool the inactive main effects back into the error estimate, increasing the error degrees of freedom and (usually) sharpening the variance.

  5. Gate the second-order space with an overall F-test at alpha_second_overall.

  6. If the gate passes, build a candidate set of second-order terms subject to the requested heredity rules, then run a best-subset search, adding terms until the residual second-order variation is no longer significant at alpha_second_subset. The search is exhaustive at every subset size with at most 200,000 subsets; past that it continues by a beam search refined by sequential replacement, so a design with many factors finishes in seconds rather than hours.

References

The staged analysis generalises the design-based model-selection approach for definitive screening designs to the wider OMARS family. This is an independent implementation of that published methodology.

  • Jones, B. and Nachtsheim, C. J. (2017). “Effective Design-Based Model Selection for Definitive Screening Designs.” Technometrics 59(3), 319-329.

  • Ismail Hameed, M. S., Nunez Ares, J. and Goos, P. (2023). “Analysis of data from orthogonal minimally aliased response surface designs.” Journal of Quality Technology 55(3).

  • Nunez Ares, J. and Goos, P. (2020). “Enumeration and Multicriteria Selection of Orthogonal Minimally Aliased Response Surface Designs.” Technometrics 62(1), 21-36.

class process_improve.experiments.omars.OmarsResult(success=False, initial_error_df=0, initial_rmse=nan, active_main_effects=<factory>, forced_main_effects=<factory>, main_effect_p_values=<factory>, updated_error_df=0, updated_rmse=nan, second_order_overall_p_value=nan, active_interactions=<factory>, active_quadratics=<factory>, final_p_value=nan, subset_limit=0, subset_limit_reason='', details=<factory>)[source]#

Bases: object

Container returned by analyze_omars().

Parameters:
success#

False when the full second-order model leaves no error degrees of freedom (the design cannot support the analysis). All other fields are only meaningful when success is True.

Type:

bool

initial_error_df#

Error degrees of freedom from the full second-order model (step 2).

Type:

int

initial_rmse#

Square root of the initial error-variance estimate (step 2).

Type:

float

active_main_effects#

Factor names declared active by the main-effect t-tests (step 3), including any forced in via force_main_effects.

Type:

list[str]

forced_main_effects#

Factor names that were forced active despite being statistically inactive.

Type:

list[str]

main_effect_p_values#

Two-sided p-value for each factor’s main effect.

Type:

dict[str, float]

updated_error_df#

Error degrees of freedom after pooling inactive main effects (step 4).

Type:

int

updated_rmse#

Square root of the pooled error-variance estimate (step 4).

Type:

float

second_order_overall_p_value#

p-value of the overall second-order F-gate (step 5). nan when the analysis was main-effects only.

Type:

float

active_interactions#

Two-factor interaction terms declared active, labelled "A:B".

Type:

list[str]

active_quadratics#

Quadratic terms declared active, labelled "A^2".

Type:

list[str]

final_p_value#

p-value of the final (failed) F-test that terminated the subset search. nan when the second-order gate did not open.

Type:

float

subset_limit#

Maximum number of second-order terms the subset search was allowed to consider.

Type:

int

subset_limit_reason#

Human-readable explanation of how subset_limit was chosen.

Type:

str

details#

Additional diagnostic values (term ranks, candidate counts, …). details["subset_search"] says whether every subset size the search reached was searched exhaustively, or from which size it continued by a beam search refined by sequential replacement.

Type:

dict[str, Any]

process_improve.experiments.omars.analyze_omars(design_matrix, response, *, alpha_main=0.05, alpha_second_overall=0.2, alpha_second_subset=0.2, quadratic_heredity='none', interaction_heredity='none', effects_to_drop=None, force_main_effects=None, second_order=True, max_subset_terms=None)[source]#

Analyse data from an OMARS design with the staged main-then-second-order protocol.

Parameters:
  • design_matrix (DataFrame) – One column per factor, one row per run. Factors must be quantitative with two or three levels; they are coded internally to [-1, +1]. Do not include second-order columns; they are constructed internally.

  • response (Series, DataFrame, or ndarray) – The (single) response, one value per run.

  • alpha_main (float, optional) – Significance level for the main-effect t-tests (step 3). Default 0.05. This and the other two levels must lie strictly between 0 and 1.

  • alpha_second_overall (float, optional) – Significance level for the overall second-order F-gate (step 5). Default 0.20.

  • alpha_second_subset (float, optional) – Significance level for the subset-search F-tests (step 6). Default 0.20.

  • quadratic_heredity ({"none", "strong"}, optional) – Heredity rule for quadratic terms. "strong" only considers the quadratic of a factor whose main effect is active. Default "none".

  • interaction_heredity ({"none", "weak", "strong"}, optional) – Heredity rule for two-factor interactions. "strong" requires both parent main effects to be active; "weak" requires at least one; "none" considers all interactions. Default "none".

  • effects_to_drop (list of str, optional) – Second-order effect labels to exclude from the candidate set, e.g. ["A^2", "B:C"] ("C:B" means the same). Every label is checked against the design’s factors before the analysis starts.

  • force_main_effects (list of str, optional) – Factor names to force into the active set even if statistically inactive. Useful for borderline effects.

  • second_order (bool, optional) – When False the analysis stops after the main effects (step 3). Default True.

  • max_subset_terms (int, optional) – Hard cap on the number of second-order terms the subset search may consider, zero or more. None (default) lets the routine choose a cap from the design’s estimability.

Returns:

Structured analysis result. See the class documentation for fields.

Return type:

OmarsResult

Raises:

ValueError – If inputs are malformed (non-numeric, contain missing values, mismatched lengths, unknown heredity option, unknown effect label, a significance level outside (0, 1), or a negative max_subset_terms).

Warns:

RuntimeWarning – When the design’s main effects are not balanced, mutually orthogonal and orthogonal to the second-order terms, which the staged protocol needs. details["design_structure"] records each of the three checks.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import analyze_omars
>>> result = analyze_omars(design, y, quadratic_heredity="strong")
>>> result.active_main_effects
['A', 'C']

Prediction from marginal means: Taguchi’s additive model.

process_improve.experiments.marginal_means.marginal_means_prediction(design_matrix, responses, at, interactions=())[source]#

Predict a response from the marginal means of the factors’ levels, as Taguchi’s additive model does.

The prediction at a setting is the grand mean T plus, for each factor, the mean of the runs at its level less T; each pair named in interactions adds the mean of the runs at its two levels, less the two factors’ means, plus T. For one 2-level factor A and an interaction with B this is T + (A - T) + (B - T) + (AB - A - B + T).

When every factor’s levels, and every pair of factors’ level combinations, occur equally often (as in an orthogonal array), the main-effect terms are the least-squares estimates of a model of main effects. Otherwise each marginal mean also carries the effects of the other factors’ levels it happens to meet, and the prediction is not the least-squares one; a UserWarning says so.

Parameters:
  • design_matrix (pd.DataFrame) – The runs, with a column for each factor in at. Levels may be numbers or labels; a level in at is matched to the runs by equality.

  • responses (pd.Series or sequence of float) – One response per run, in the order of design_matrix. A run whose response is missing is left out of every mean.

  • at (pd.DataFrame or mapping) – The settings to predict: one row per setting (or one mapping), with a level for each factor in the model. The model’s factors are the columns of at.

  • interactions (sequence of (str, str), optional) – Pairs of factors whose two-factor means enter the prediction.

Returns:

The prediction at each setting, indexed as at (from 0 for a mapping), named "predicted".

Return type:

pd.Series

Raises:

ValueError – If responses and design_matrix differ in length, a factor of at or of interactions is not a column of design_matrix (or not in at), or no run has a level or pair of levels a setting asks for.

Examples

The grand mean is 14, A’s level 2 averages 17 and B’s 16, so the additive model gives 19; the interaction adds the cell’s 20 less 17 and 16 plus 14, which returns the cell’s own mean.

>>> import pandas as pd
>>> runs, y = pd.DataFrame({"A": [1, 1, 2, 2], "B": [1, 2, 1, 2]}), [10.0, 12.0, 14.0, 20.0]
>>> marginal_means_prediction(runs, y, {"A": 2, "B": 2}).tolist()
[19.0]
>>> marginal_means_prediction(runs, y, {"A": 2, "B": 2}, interactions=[("A", "B")]).tolist()
[20.0]

Least-squares means of a factor’s levels, and their Tukey, Dunnett or unadjusted comparisons.

When every level of a factor meets every level of the others equally often, the average of the runs at a level is a fair summary of it. In an incomplete block design, a Youden square or an unbalanced experiment it is not: a level that happened to fall in the better blocks looks better than it is. The least-squares mean (adjusted mean, estimated marginal mean) is the fitted model’s prediction at that level, averaged with equal weight over the levels of every other factor in the model, with each numeric covariate at its average. It is a linear combination \(\ell^\top \hat\beta\) of the coefficients, so its standard error is \(\sqrt{\ell^\top \widehat{\operatorname{cov}}(\hat\beta)\, \ell}\), on the residual degrees of freedom; so are the differences of two of them.

  • Tukey’s comparisons ("tukey") test every pair against the studentized range of the number of levels, so the family of all pairs has the stated confidence (Tukey-Kramer).

  • Dunnett’s (Comparisons("dunnett", control=...)) compare each level with one control, against the largest of the correlated t statistics, with their correlations from the model.

  • "none" gives each pair’s plain t test and interval (Fisher’s least significant difference), and the confidence the whole family of those intervals has, which is lower.

at fixes other factors at one level (or a covariate at one value) instead of averaging over them: the comparisons of one factor’s levels within one level of another, on the error of the whole model. Tukey’s and the unadjusted comparisons also give connecting letters: levels that share a letter do not differ significantly.

reference_scale is the scale of the t distribution, on the residual degrees of freedom, that Box, Hunter and Hunter (1978, sections 6.4 and 8D) slide along the axis under a set of means: means that cannot all sit under it at once are unlikely to differ by chance alone. By default it is the square root of half the average variance of a difference of two means. That is \(s/\sqrt{n}\) when each level has n runs, and \(\sqrt{k s^2/(\lambda t)}\) in a balanced incomplete block design. Comparisons(reference_rule="average_replication") gives \(s/\sqrt{\bar n}\) instead, with \(\bar n\) the average number of runs at a level, as the book scales the unequally replicated blood-coagulation diets.

Examples

>>> import pandas as pd
>>> from process_improve.experiments.least_squares_means import least_squares_means
>>> runs = pd.DataFrame({"t": list("AABBCC"), "b": list("121212"), "y": [1.0, 2.0, 3.0, 4.5, 2.0, 3.5]})
>>> found = least_squares_means(runs, "y ~ t + b", "t")
>>> [round(m["mean"], 2) for m in found["means"]]
[1.5, 3.75, 2.75]
process_improve.experiments.least_squares_means.REFERENCE_RULES = ('pairwise', 'average_replication')#

How the scale of the reference distribution is worked out (see the module’s text).

class process_improve.experiments.least_squares_means.Comparisons(method='tukey', control=None, conflevel=0.95, reference_rule='pairwise')[source]#

Bases: object

How the levels of a factor are compared, and at what confidence.

Parameters:
method#

All pairs by Tukey’s studentized range, each level against control by Dunnett’s multivariate t, or all pairs unadjusted.

Type:

{“tukey”, “dunnett”, “none”}

control#

For Dunnett’s comparisons, the control level; the first level by default.

Type:

str or None

conflevel#

The confidence of the intervals, and of the family for Tukey’s and Dunnett’s.

Type:

float

reference_rule#

How reference_scale is worked out: from the variances of the differences of two means (default), or from the residual mean square and the average replication.

Type:

{“pairwise”, “average_replication”}

process_improve.experiments.least_squares_means.least_squares_means(design_matrix, formula, factor, comparisons='tukey', at=None)[source]#

Return the least-squares mean of each level of a factor, and their comparisons.

Parameters:
  • design_matrix (DataFrame) – The runs: the factors and the response. A categorical factor is a column of labels.

  • formula (str) – The model, a plain formula over the columns, "y ~ treatment + block"; its right-hand side must hold factor. C(temperature) takes a numeric column as levels, such as a temperature set at three values, rather than as a covariate with a slope; a column of labels is always taken as levels.

  • factor (str) – The factor whose levels are compared, taken as levels even when they are numbers.

  • comparisons (str or Comparisons) – How the levels are compared: "tukey", "dunnett" or "none", or a Comparisons that also names Dunnett’s control or the confidence (0.95).

  • at (dict[str, Any] or None) – Other factors held at one level, {"temperature": 70}, and covariates held at one value, instead of averaged over: the comparisons are then within that level, on the residual of the whole model.

Returns:

means, for each level (in the order of the data) its mean, std_error and interval on the residual degrees of freedom, and by_level, the same keyed by level; comparisons, one row per pair (level less versus) with the difference, std_error, t, adjusted p_value and simultaneous interval (and its half_width); pairs, the same keyed "level - versus"; the residual df; the method; the critical_value that multiplies each difference’s standard error; individual_confidence, the confidence each interval has alone, and family_confidence, the confidence that every interval holds its difference (for "none", the studentized range’s, exact when the levels are equally replicated). Tukey’s also give the studentized_range point, the critical value times the square root of 2, and Tukey’s and the unadjusted give letters, each level’s connecting letters at the significance level 1 - the confidence. reference_scale is the scale of Box, Hunter and Hunter’s reference t distribution for the means, on df degrees of freedom, by the reference_rule the comparisons name (see the module’s text).

Return type:

dict[str, Any]

Yates’s algorithm for a two-level factorial, and the algorithm run in reverse (#803 H1).

Yates’s algorithm turns the responses of a full \(2^k\) factorial, written in standard order, into the contrasts of its effects, by k passes of one rule: the first half of each new column holds the sums of successive pairs of the column before, and the second half their differences, the second of each pair less the first. After k passes the first entry is the total, and each other entry is the contrast of one effect, in standard order (A, B, AB, C, …). An effect is its contrast divided by \(N/2\), and its sum of squares is the contrast squared divided by N.

Each pass doubles the sum of squares of its column, so the sums of squares are the algorithm’s check on the arithmetic. Run in reverse, on the contrasts of the effects a model keeps, the same passes give N times the model’s fitted values (Box, Hunter and Hunter 1978, Appendix 10A).

Examples

>>> import pandas as pd
>>> from process_improve.experiments import yates_table
>>> runs = pd.DataFrame({"A": [-1, 1, -1, 1], "B": [-1, -1, 1, 1], "y": [10.0, 14.0, 12.0, 20.0]})
>>> table = yates_table(runs, "y")
>>> table[["(1)", "(2)", "effect", "estimate"]].values.tolist()
[[24.0, 56.0, 'mean', 14.0], [32.0, 12.0, 'A', 6.0], [4.0, 8.0, 'B', 4.0], [8.0, 4.0, 'A:B', 2.0]]
process_improve.experiments.yates.reverse_yates(table, keep)[source]#

Return the fitted values of a model that keeps some of the effects, by Yates’s algorithm run in reverse.

The contrasts of the kept effects, with the others set to zero, are written in reverse standard order (the highest interaction first, the total last) and passed through the algorithm k times; the last column, read from the bottom up, is N times the fitted values in standard order.

Parameters:
  • table (pd.DataFrame) – A table from yates_table().

  • keep (Sequence[str]) – The effects the model keeps, by their names in table["effect"], such as ["A", "B", "A:C"]. The mean is kept whether it is named or not.

Returns:

One row per run in standard order, as in table: the factors and the response; effect, and kept, its contrast, or 0 for an effect the model drops; the reverse passes’ columns (1) to (k), each read from the bottom up, as the algorithm writes it; fitted, the last column divided by N; and residual. attrs["sum_of_squares"] holds the sum of squares of kept and of each column, each twice the one before, then of fitted (the crude sum of squares of the fitted values) and of residual.

Return type:

pd.DataFrame

Raises:

ValueError – If table is not a table from yates_table(), or keep names an effect it does not have.

Examples

The 2^2 factorial of yates_table(), fitted with A alone: B and AB go into the residuals.

>>> runs = pd.DataFrame({"A": [-1, 1, -1, 1], "B": [-1, -1, 1, 1], "y": [10.0, 14.0, 12.0, 20.0]})
>>> fit = reverse_yates(yates_table(runs, "y"), keep=["A"])
>>> fit[["effect", "kept", "fitted", "residual"]].values.tolist()
[['mean', 56.0, 11.0, -1.0], ['A', 12.0, 17.0, -3.0], ['B', 0.0, 11.0, 1.0], ['A:B', 0.0, 17.0, 3.0]]
>>> fit.attrs["sum_of_squares"]["fitted"], fit.attrs["sum_of_squares"]["residual"]
(820.0, 20.0)
process_improve.experiments.yates.yates_table(design, response, factors=None)[source]#

Return Yates’s algorithm for a full two-level factorial: its columns, its effects and their sums of squares.

Parameters:
  • design (pd.DataFrame) – The runs of a full \(2^k\) factorial, each factor coded -1 and +1, each run once, in any order.

  • response (str) – The column that holds the response.

  • factors (Sequence[str], optional) – The factors, the first the one that changes fastest in standard order. By default every column of design but the response, in their order.

Returns:

One row per run in standard order: the factors and the response; the algorithm’s columns (1) to (k); effect, the effect the row’s last column estimates ("mean", then "A", "B", "A:B", …); its estimate, the last column divided by N/2 (by N for the mean); and its sum_sq, the last column squared and divided by N (for the mean, the correction for the mean). attrs["sum_of_squares"] holds the sum of squares of the response and of each column, each twice the one before.

Return type:

pd.DataFrame

Raises:

ValueError – If the response is not a column, a factor is not coded -1 and +1, the runs are not each run of the full factorial once, a response is missing, or a name would collide with a column of the table.

Examples

A 2^2 factorial, its runs in any order. The table puts them in standard order, and each pass of the algorithm doubles the sum of squares of the responses, 840.

>>> runs = pd.DataFrame({"A": [1, -1, 1, -1], "B": [1, 1, -1, -1], "y": [20.0, 12.0, 14.0, 10.0]})
>>> table = yates_table(runs, "y")
>>> table["y"].tolist()
[10.0, 14.0, 12.0, 20.0]
>>> dict(zip(table["effect"], table["sum_sq"].tolist(), strict=True))
{'mean': 784.0, 'A': 36.0, 'B': 16.0, 'A:B': 4.0}
>>> table.attrs["sum_of_squares"]
{'y': 840.0, '(1)': 1680.0, '(2)': 3360.0}

Analyses of blocked and two-way layouts that the classical sources print (#800).

  • tukey_nonadditivity(): Tukey’s one degree of freedom for nonadditivity, and the power of the response that would remove it;

  • estimate_missing(): the least-squares estimates of the runs whose response is missing, and the approximate analysis of the table they complete.

process_improve.experiments.layouts.estimate_missing(design_matrix, formula, decimals=None)[source]#

Return the least-squares estimates of the runs whose response is missing, and the table they complete.

Each estimate is the model’s prediction at the missing run, fitted to the runs that were observed: the value that leaves that run a residual of zero. For one missing value of a randomized block design it is Yates’s formula; several are estimated together. The exact analysis is that of the observed runs alone (analyze_experiment gives it). The approximate one, which the classical sources print, treats the completed table as complete, with the residual degrees of freedom reduced by one for each estimate; it overstates the effects a little.

Parameters:
  • design_matrix (pd.DataFrame) – The runs, the missing ones with their factor settings and no response.

  • formula (str) – The model, with the response on the left, such as "y ~ C(block) + C(treatment)".

  • decimals (int, optional) – Round the estimates to this many decimals before they complete the table, as a source does that analyses the estimates it prints.

Returns:

estimates, one row per missing run: its index, its factor settings and the estimate under the response’s name; completed, the runs with the estimates filled in; and approximate_anova, the sequential analysis of the completed table, one row per term and the residual, with df, sum_sq, mean_sq, f and p_value.

Return type:

dict[str, Any]

Raises:

ValueError – If no response is missing, or the formula’s response is not a column of the runs.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import estimate_missing
>>> runs = pd.DataFrame({
...     "block": ["I", "I", "I", "II", "II", "II"], "treatment": ["A", "B", "C"] * 2,
...     "y": [10.0, 12.0, 15.0, 11.0, None, 16.0],
... })
>>> round(estimate_missing(runs, "y ~ C(block) + C(treatment)")["estimates"][0]["y"], 6)  # 12 + (27 - 25) / 2
13.0
process_improve.experiments.layouts.tukey_nonadditivity(design_matrix, formula, error='remainder')[source]#

Return Tukey’s one degree of freedom for nonadditivity, and the power of the response that would remove it.

The additive model, such as "y ~ C(block) + C(treatment)", is fitted again with one more regressor, the square of its fitted values (Tukey 1949). The sum of squares that regressor takes is the part of the interaction that is proportional to the product of the row and column effects, \(D a_i b_j\), with \(D\) twice the regressor’s coefficient. A power \(1 - D \bar{y}\) of the response would remove that part, so a significant test points to a transformation: near 0, a log; near -1, a reciprocal.

Parameters:
  • design_matrix (pd.DataFrame) – The runs: the factors and the response.

  • formula (str) – The additive model, with the response on the left.

  • error ({"remainder", "pure"}) – What the sum of squares is tested against: the residual of the model with the regressor (default), as for one run per cell; or the pure error between replicated runs of the same cell, as Box, Hunter and Hunter (1978) test the toxic-agents data.

Returns:

ss on df (1), its f and p_value; ss_error, df_error and ms_error, the denominator’s; remainder_ss and remainder_df, the interaction left once ss is taken out (the denominator itself, with one run per cell); coefficient, the regressor’s (Box, Hunter and Hunter’s theta); and power.

Return type:

dict[str, float]

Raises:

ValueError – If error is not one of the two, "pure" is asked of runs with no replicated cell, or the denominator has no degrees of freedom.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import tukey_nonadditivity
>>> impurity = pd.DataFrame({  # Montgomery (2013), Table 5.10
...     "temperature": [100] * 5 + [125] * 5 + [150] * 5,
...     "pressure": [25, 30, 35, 40, 45] * 3,
...     "y": [5, 4, 6, 3, 5, 3, 1, 4, 2, 3, 1, 1, 3, 1, 2],
... })
>>> found = tukey_nonadditivity(impurity, "y ~ C(temperature) + C(pressure)")
>>> round(found["ss"], 4), round(found["ss_error"], 4), found["df_error"], round(found["f"], 2)
(0.0985, 1.9015, 7.0, 0.36)

Multistratum analysis of variance: split-plot, strip-plot and other designs with several sizes of unit.

A design that restricts its randomisation (a hard-to-change factor set once for several runs, a batch that holds four strips, a furnace load that crosses an implanter batch) has runs that fall into strata: the whole plots, the subplots within them, and so on. Each stratum is a set of contrasts among the runs with its own error, and each treatment term is estimated in the stratum its contrasts fall in. This is R’s aov(y ~ treatments + Error(blocks)):

  • the strata are given as error terms, "rep/plot" (replicates, then whole plots within them) or "row + column" (a strip plot); a last stratum, "Within", holds the contrasts left;

  • each stratum’s basis is the part of its error term’s indicator columns that the strata before it do not span, so the strata split the n - 1 contrasts among the runs between them;

  • in each stratum the treatment terms are fitted in order, each one’s degrees of freedom the rank it adds there, and what is left is the stratum’s residual.

Without a response it gives the skeleton analysis of variance: the sources and degrees of freedom a design will have, before it is run. With one, each term’s sum of squares, and its F test on the residual of its stratum where that residual has degrees of freedom.

Examples

>>> import itertools
>>> import pandas as pd
>>> from process_improve.experiments.multistratum import multistratum_anova
>>> runs = pd.DataFrame(list(itertools.product([0, 1], [0, 1], [0, 1], [0, 1])), columns=["S", "T", "rep", "A"])
>>> layout = multistratum_anova(runs, "S * T * A", "S:T:rep")
>>> layout["df"]["S:T:rep"]
{'S': 1, 'T': 1, 'S:T': 1, 'Residual': 4}
>>> layout["df"]["Within"]
{'A': 1, 'S:A': 1, 'T:A': 1, 'S:T:A': 1, 'Residual': 4}
process_improve.experiments.multistratum.error_terms(strata)[source]#

Expand a block structure into its error terms, in order: "a/b" is a and a:b, "a + b" is both.

Parameters:

strata (str or list[str]) – The block structure, "rep/plot", "row + column", or a list of error terms.

Returns:

The error terms, each a factor or an interaction of factors such as "rep:plot".

Return type:

list[str]

process_improve.experiments.multistratum.multistratum_anova(design_matrix, model, strata, response=None)[source]#

Return the analysis of variance of a design with several strata of units, or its skeleton without a response.

Parameters:
  • design_matrix (DataFrame) – The runs: the treatment factors, the columns that name the units (replicate, whole plot, batch), and the response if there is one. Every factor is taken as levels.

  • model (str) – The treatment terms, a plain formula over the columns: "S * T * A".

  • strata (str or list[str]) – The block structure as error terms (see error_terms()): "rep/plot" for whole plots within replicates, "S:T" for a whole plot at each combination of S and T, "row + column" for a strip plot.

  • response (str or None) – The response column; without one, the result holds degrees of freedom only.

Returns:

strata, one entry per stratum (each error term, then "Within") with its df, its units (the number of groups its error term makes, the runs for "Within"), the terms estimated in it, each with its df (and with a response ss, ms, f and p_value), and its residual; df, each stratum’s terms and "Residual" mapped to their degrees of freedom; and table, every row as a DataFrame. A term whose contrasts fall in two strata, as in a design that is not orthogonal, has a row in each.

Return type:

dict[str, Any]

Raises:

ValueError – If the model or an error term names a column the runs do not have.

Public API for DOE visualization (Tool 6).

visualize_doe() is the single entry point. It dispatches to the correct plot class, builds the ChartSpec IR, runs the requested backend adapters, and returns a JSON-serialisable dict.

main_effects_plot() is a convenience wrapper that accepts either a fitted Model or a DataFrame, and returns a Plotly figure directly.

process_improve.experiments.visualization.api.visualize_doe(*, plot_type, analysis_results=None, design_data=None, response_column=None, factors_to_plot=None, hold_values=None, highlight_significant=True, confidence_level=0.95, factor_labels=None, backend='both', model=None, region=None)[source]#

Generate a DOE visualisation.

Parameters:
  • plot_type (str) – One of the supported DOE plot types (see plots).

  • analysis_results (dict or None) – Output dict from analyze_experiment(). Required for most plot types that need fitted model data. "reference_distribution" takes the output of least_squares_means() instead.

  • design_data (list[dict] or None) – Raw design matrix as a list of row-dicts. Used when analysis_results is not provided (e.g. main-effects from raw data).

  • response_column (str or None) – Name of the response column in design_data.

  • factors_to_plot (list[str] or None) – Subset of factors to display (2 for contour/interaction).

  • hold_values (dict or None) – Fixed values for factors not being plotted.

  • highlight_significant (bool) – Auto-highlight significant effects (Pareto / half-normal).

  • confidence_level (float) – Confidence level for reference lines (default 0.95).

  • factor_labels (dict or None) – Mapping {factor_symbol: full_name} used for axis labels and bar/legend entries (e.g. {"A": "Temperature [°C]"}).

  • backend (str) – "both", "plotly", or "echarts".

  • model (str or None) – Model the design-quality plots ("fds_plot", "power_curve", "prediction_variance") are computed for, as in evaluate_design(). When None, the formula of analysis_results is used, else the richest of "quadratic", "interactions" and "main_effects" that the design can estimate.

  • region (DesignRegion, dict or None) – The settings that can be run, as a DesignRegion or its to_dict() form; its factor ranges define the coded units. The "contour" and "prediction_variance" plots shade the settings it rules out (with the factors not plotted held at hold_values), and "surface_3d" leaves them out; the other plot types ignore it. With None the figures are those of the whole cube.

Returns:

Keys: plot_type, title, plotly (Plotly figure dict), echarts (ECharts option dict), data (raw computed data).

Return type:

dict[str, Any]

Raises:

ValueError – If backend is not one of "both", "plotly" or "echarts".

process_improve.experiments.visualization.api.main_effects_plot(model_or_data, response_column=None, factors_to_plot=None, *, factor_labels=None)[source]#

Generate a main-effects plot from a fitted model or a design DataFrame.

For each factor, the mean response at every observed level is drawn as a line. A flat line indicates no main effect; a steep line indicates a strong one. The grand mean is shown as a horizontal dotted reference line.

Parameters:
  • model_or_data (Model or pandas.DataFrame) – Either a fitted Model returned by lm(), or a DataFrame containing one column per factor plus a response column.

  • response_column (str, optional) – Name of the response column. Required when model_or_data is a DataFrame; ignored (and inferred from the model formula) when a fitted Model is supplied.

  • factors_to_plot (list of str, optional) – Subset of factors to display. By default all main-effect factors are shown - for a Model these are the level-1 factors from the formula; for a DataFrame they are all columns other than response_column.

  • factor_labels (dict, optional) – Mapping {factor_symbol: full_name} used for legend entries (e.g. {"A": "Temperature [°C]"}).

Returns:

Plotly figure ready for display or further customisation.

Return type:

plotly.graph_objects.Figure

Examples

From a fitted model:

from process_improve.experiments import c, gather, lm, main_effects_plot
A = c(-1, +1, -1, +1)
B = c(-1, -1, +1, +1)
y = c(52, 74, 62, 80, name="y")
expt = gather(A=A, B=B, y=y)
model = lm("y ~ A + B", expt)
fig = main_effects_plot(model)

From a raw DataFrame:

fig = main_effects_plot(df, response_column="yield")

See also

visualize_doe

General DOE plotting entry point.

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.CATEGORICAL_CODINGS = ('effect', 'treatment')#

Codings for a categorical (label) factor; see categorical_coding in evaluate_design().

process_improve.experiments.evaluate.SECOND_ORDER_PAIRS = ('two_squares', 'square_and_own_interaction', 'square_and_other_interaction', 'two_interactions')#

two squares (qq, ss), a square and an interaction in its factor (qq, qs), a square and an interaction without it (qq, st), and two interactions (st, uv), with a factor in common or not.

Type:

The kinds of pair of second-order terms, in Jones and Nachtsheim’s (2011) notation

process_improve.experiments.evaluate.SCREENING_HYPOTHESES = ((1, 'linear effect', 'the constant and the factor'), (2, 'quadratic effect', 'main effects and the square'), (3, 'interaction effect', 'main effects and the interaction'), (4, 'linear effect', 'full quadratic model of the two factors'), (5, 'quadratic effect', 'full quadratic model of the two factors'), (6, 'interaction effect', 'full quadratic model of the two factors'))#

each hypothesis the test of one kind of term, in the model it is made in, averaged over every factor or pair of factors it can be asked of.

Type:

Jones and Nachtsheim’s (2011) power study (their Table 6)

process_improve.experiments.evaluate.evaluate_design(design_matrix, model=None, metric='d_efficiency', effect_size=None, alpha=0.05, sigma=None, region=None, n_samples=100000, include_vertices=True, fds_resolution=None, random_state=42, categorical_coding='effect', integration='sample', max_search='sample', random_effects=None, alias_terms='interactions')[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. A DataFrame must be in coded units (-1 and +1 at the low and high levels; proportions for mixture components), since the efficiencies and the region are defined on that scale; settings far outside it raise a UserWarning. Categorical factors are label columns. A run with a missing factor setting raises ValueError.

  • model (str or None) – Model type: "main_effects", "interactions", "quadratic", a Scheffé mixture model ("scheffe_linear", "scheffe_quadratic", "scheffe_special_cubic", "scheffe_cubic"), or an explicit patsy formula. Over a mixture region the process-model names map to Scheffé models as in generate_design() ("interactions" and "quadratic" to "scheffe_quadratic", "special_cubic" to "scheffe_special_cubic", "cubic" to "scheffe_cubic"), and None means the model the design was generated for, else "scheffe_quadratic". Over a mixture-process region a mixture-process model name ("interactions", "scheffe_quadratic + main_effects", …; see mixture_process_model()) becomes that model, and None means the model the design was generated for, else "interactions". Otherwise None means "interactions". A formula may name a Block column ("A + B + Block") to include the blocks as a categorical factor; otherwise the blocks are left out and a note says so. Numbered blocks are then in numeric order, block 10 after block 9, and a contrast names one by its number: C(Block, Treatment(1)) for block 1. Naming it as text, C(Block, Treatment('1')), is deprecated since 2.5.0 and will be removed in 3.0.

  • 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", "average_prediction_variance" (the I-criterion), "g_efficiency", "a_optimality", "e_optimality", "correlation", "estimate_correlation" (the correlations between the estimated second-order effects, by kind of pair), "alias_matrix", "fds", "prediction_variance", "vif", "condition_number", "scaled_condition_number" (with each column of the model matrix scaled to unit length, so the factors’ units do not count), "power", "screening_power" (the average power of each kind of test of a screening study, Jones and Nachtsheim’s Table 7; it ignores model), "degrees_of_freedom", "alias_structure", "confounding", "resolution", "defining_relation", "clear_effects", "minimum_aberration", "moment_aberration". "g_efficiency" is 100 * p / (N * max d(x)) (Atkinson, Donev and Tobias; Montgomery), where JMP reports its square root. The optimality-criterion metrics also accept the opposite suffix as an alias (e.g. "d_optimality" for "d_efficiency", "i_optimality" for "average_prediction_variance", "a_efficiency" for "a_optimality"); the result is keyed under the canonical name.

  • effect_size (float or None) – Anticipated size of a model coefficient in coded units, for the power calculation (JMP’s “anticipated coefficient”). For a two-level factor coded -1 / +1 this is half the high-minus-low effect. When None, a power curve over coefficients from 0.5 * sigma to 3 * sigma 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"}, DesignRegion, pandas.DataFrame, or None) – Design region over which the region-based metrics (average_prediction_variance, g_efficiency, fds) integrate the prediction variance. "cuboidal" is [-1, 1]^k; "spherical" is the ball of radius sqrt(k). A DesignRegion restricts the average and the maximum to settings that satisfy its constraints (a box), lie on its constrained simplex (a mixture), or both (a mixture-process region): a constrained design should not be judged on corners it may not visit. A DataFrame is a discrete region, such as the candidate points an optimal design was chosen from: one row per point and a column per factor, in the design’s units (coded, for a DesignResult). The average and the maximum are then taken over exactly those points, each counted once, with no sampling. None (default) uses the region a DesignResult recorded when it was generated with constraints or mixture factors, and the cube otherwise.

  • n_samples (int) – Number of random samples drawn uniformly over the region (default 100,000; at least 1). The region average and the FDS curve are taken over this sample.

  • include_vertices (bool) – When True (default), boundary points are added for the maximum, so the worst-case (G) value is represented: for the cuboidal or spherical region the 2**k cube vertices of the quantitative factors, crossed with every combination of categorical levels; for a DesignRegion, its support points (extreme vertices and edge midpoints for a mixture). They are used only for the maximum, not for the region average.

  • 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.

  • random_state (int, numpy.random.Generator or None) – Seed for the region sampler (default 42, so repeated calls agree).

  • categorical_coding ({"effect", "treatment"}) – How a categorical (label) factor enters the model. "effect" (default) is sum-to-zero effect coding, the usual convention in DoE software and the coding the built-in optimal designs are built in: a two-level factor is -1 and +1 (first and second level in sorted order), and a factor with L > 2 levels gets L - 1 columns, the last level at -1 in every column (names such as "C[S.lo]"). "treatment" is patsy’s 0/1 dummy coding against the first level ("C[T.y]"), which earlier releases used. The A- and E-criteria, VIF, both condition numbers, power and the alias matrix depend on the coding; the prediction-variance metrics, D rankings and the degrees of freedom do not. A Block column named in the formula is sum-coded under "effect", as analyze_experiment codes blocks.

  • integration ({"sample", "exact"}) – How the region average of the prediction variance (the I-criterion, in average_prediction_variance) is worked out. "sample" (default) averages it over the uniform sample. "exact" integrates it over the cuboidal region by Gauss-Legendre quadrature, trace((X'X)^-1 W) with W the region’s moment matrix: exact for a model of degree up to four in each factor, a categorical factor averaged over its levels. It falls back to the sample, with a note, for another region or a quadrature grid of more than 250,000 points.

  • max_search ({"sample", "optimize"}) – How the region maximum of the prediction variance (in g_efficiency) is found. "sample" (default) takes the largest value over the sample and the boundary points. "optimize" then climbs from the best of those points by a compass search inside the cube, so a maximum between the vertices is found rather than missed: the variance of a design’s projection often peaks inside a face or an edge. Over a DesignRegion it is the sample’s maximum.

  • random_effects (mapping of str to float, or None) – For runs that share random effects: each key a column of the design that labels a run’s group (the WholePlot of a split-plot design; the rows and the columns of a two-way split-plot, or strip-plot, design; random blocks), each value that effect’s variance over the run variance, at least 0 (1 is a common assumption). The columns are not factors. Every metric then uses the generalized least squares information X' V^-1 X, with V = I + sum_k eta_k Z_k Z_k' and Z_k the indicators of effect k’s groups, in place of X'X: the variances are in units of the run variance, as JMP reports them for split-plot and two-way split-plot designs, and ratios of 0 give back the ordinary least squares metrics. The degrees of freedom and the power keep the run-level residual degrees of freedom N - p. None (default) treats the runs as independent.

  • alias_terms ({"interactions", "quadratic"}) – The terms outside the model that alias_matrix works out the biases of. "interactions" (default): the two-factor interactions the model leaves out. "quadratic": those and the squares of the numeric factors, the terms of the full second-order model, so the matrix shows how curvature biases a first-order design’s intercept (Davies, 11.33: b0 -> b0 + b11 + b22 + …).

Returns:

Results keyed by metric name. The structure of each value depends on the metric - see individual metric documentation. A metric that needs to explain its result (for example a rank-deficient model) adds its note to result["notes"][metric_name].

Return type:

dict[str, Any]

Raises:

ValueError – If alpha is not in (0, 1), sigma is not positive, n_samples is below 1, a factor setting is missing, categorical_coding is unknown, or random_effects names a column the design lacks or has a negative ratio.

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=None, n_samples=100000, include_vertices=True, fds_resolution=None, random_state=42, categorical_coding='effect', integration='sample', max_search='sample', random_effects=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:

See also

evaluate_design

Compute one or more named metrics.

Compare designs for one fixed model on nine measures of design quality.

The question a comparison answers is narrow on purpose: the model is already chosen, and the designs differ only in where they place their runs. Each design is scored on the same model, in coded units over the cube \([-1, 1]^k\), on the measures that Process Improvement using Data defines in “Nine measures down one column” (BOOK_URL):

  • five alphabetic optimality criteria, each one summary of the information matrix \(\mathbf{M} = \mathbf{X}^T\mathbf{X}\) of the model matrix \(\mathbf{X}\): \(A/p\) and \(I\), which average; \(E\) and \(G\), which take the worst case of the same two quantities; and \(D\), which does neither;

  • the largest absolute correlation between two second-order terms, read off the design’s correlation map;

  • the power to detect a main effect, a two-factor interaction and a quadratic of a stated size.

They come after three rows that decide whether the design belongs in the comparison at all: its runs, whether it can fit the model, and the degrees of freedom it leaves to estimate the error. No single design is best on every row, and the comparison does not pick one.

When some settings cannot be run, a DesignRegion with the limits on them can be given: \(I\) and \(G\) are then taken over the settings that can be run instead of over the whole cube.

Also see#

process_improve.experiments.evaluate.evaluate_design : many more measures, one design at a time. process_improve.experiments.true_process.TrueProcess : simulate a design’s results from a known model.

process_improve.experiments.compare.MODELS = ('main_effects', 'interactions', 'main_quadratic', 'quadratic')#

The models a comparison can be made for. main_quadratic is the book’s model for the nine measures: the intercept, the main effects and the pure quadratics, without the two-factor interactions.

process_improve.experiments.compare.BOOK_URL = 'https://learnche.org/pid/design-analysis-experiments/omars-designs#doe-omars-metric-choice'#

Where the book defines the measures.

class process_improve.experiments.compare.Measure(key, label, direction, group, description)[source]#

Bases: object

One row of a DesignComparison.

Parameters:
key#

The key the measure has in DesignComparison.values.

Type:

str

label#

The row label.

Type:

str

direction#

"higher" or "lower", whichever is preferred; empty for the feasibility rows, which are facts about the design rather than a scale.

Type:

str

group#

"Feasibility", "Averages", "Worst cases", "Neither" or "Power": the book’s grouping of the measures.

Type:

str

description#

What the measure is, in one sentence.

Type:

str

process_improve.experiments.compare.MEASURES: tuple[Measure, ...]#

The rows of a comparison, in the order the table lists them.

class process_improve.experiments.compare.DesignComparison(model, terms, effect_size, significance_level, designs, values, notes, correlations, g_points, region=None, _inverses=<factory>, _model=None)[source]#

Bases: object

Designs scored for one model on the measures in MEASURES.

Parameters:
model#

The model every design is scored on, one of MODELS.

Type:

str

terms#

The model’s terms, named as analyze_experiment() names them ("Intercept", "A", "A:B", "I(A ** 2)").

Type:

tuple[str, …]

effect_size#

The coefficient size, in noise standard deviations, at which power is computed.

Type:

float

significance_level#

The level of the tests behind the power.

Type:

float

designs#

The designs’ labels, each with its size ("DSD [n=9]"), in the order given.

Type:

tuple[str, …]

values#

For each design label, each measure’s value by its Measure.key. A measure the design cannot provide is None: every measure after the feasibility rows when the design cannot fit the model, power when no degrees of freedom are left, and power for a term type the model does not have.

Type:

Mapping[str, Mapping[str, Any]]

notes#

For each design label, sentences that qualify its numbers.

Type:

Mapping[str, tuple[str, …]]

correlations#

For each design label, the absolute Pearson correlation between its main effects, squares and two-factor interactions, in that order; NaN for a column that does not vary. The block of squares and interactions is the one "max_r" reads.

Type:

Mapping[str, pandas.DataFrame]

g_points#

The points over which G was searched, shared by every design, and over a region with a curved limit the local searches that go on from them for each design.

Type:

str

region#

The region I and G were taken over, as it was given; None for the cube \([-1, 1]^k\).

Type:

DesignRegion or None

property measures: tuple[Measure, ...]#

Return the rows of the comparison, with I and G defined over the region when there is one.

Returns:

MEASURES without a region, or with one that has no constraints (the cube’s exact moments are used then); otherwise the same rows with the descriptions of I and G saying they are taken over the region.

Return type:

tuple[Measure, …]

property table: DataFrame#

Return the comparison in the book’s layout, for reading.

Returns:

One row per measure, labelled by Measure.label; a "Better" column with an arrow for the preferred direction; then one column per design. The entries are text: counts as integers, the measures to four significant figures as the book prints them, "Fits the model?" as "yes" or "no (rank r)", and an empty string where the design cannot provide a value. values and long hold the numbers.

Return type:

pandas.DataFrame

property long: DataFrame#

Return one row per design and measure.

Returns:

Columns design, key, measure, group, direction and value.

Return type:

pandas.DataFrame

relative(design, reference, n_points=100000, random_state=42)[source]#

Return how one design compares with another on the model: efficiencies, variances, FDS curves.

Every value comes from the information matrices the scores were computed from, so the two never disagree: \(\mathbf{X}^T\mathbf{V}^{-1}\mathbf{X}\) for a split-plot design.

Parameters:
  • design (str) – The designs’ labels, as designs lists them ("DSD [n=9]") or as they were given ("DSD").

  • reference (str) – The designs’ labels, as designs lists them ("DSD [n=9]") or as they were given ("DSD").

  • n_points (int, default 100000) – Points drawn uniformly from the cube, or from the region, at which the two designs’ prediction variances are compared for their FDS curves.

  • random_state (int, numpy.random.Generator or None, default 42) – Seeds the points, as evaluate_design seeds its sample.

Returns:

  • d_efficiency: \((|\mathbf{M}_1| / |\mathbf{M}_2|)^{1/p}\), the design’s joint precision of the coefficients over the reference’s;

  • a_efficiency, i_efficiency and g_efficiency: the reference’s \(A/p\), \(I\) and \(G\) over the design’s, so that every efficiency above 1 favours the design;

  • variance_ratio: for each term, the design’s coefficient variance over the reference’s;

  • fds_crossings: the fractions of the design space, to the nearest 0.001, at which the two FDS curves cross (empty when one lies below the other everywhere), and fds_lower_share, the share of the fractions at which the design’s curve is the lower;

  • design, reference and n_points.

Return type:

dict

Raises:

ValueError – If a label names no design of the comparison, either design cannot fit the model, or n_points is below 2.

to_dict()[source]#

Return the comparison as JSON-safe Python values.

Returns:

The attributes, with the measures’ definitions (measures) under "measures", each correlation matrix as {"terms": [...], "matrix": [[...]]} and the region as DesignRegion.to_dict() writes it (None without one); NaN and infinities become None.

Return type:

dict

process_improve.experiments.compare.compare_designs(designs, model='main_quadratic', *, effect_size=1.0, significance_level=0.05, region=None)[source]#

Score designs for one fixed model on the nine measures of design quality.

The model is fixed first, and every design is scored on it, so the comparison is about where each design places its runs. Each design is compared at its own size: the table labels it with its run count rather than scaling the measures by it, so a larger design scores better on the measures that improve with runs, as it does at the bench.

Write \(\mathbf{M} = \mathbf{X}^T\mathbf{X}\) for the model matrix \(\mathbf{X}\) of a design with \(N\) runs, \(p\) the model’s terms, and \(\mathbf{f}(\mathbf{x})\) the model’s row at a point \(\mathbf{x}\) of the cube \([-1, 1]^k\). The measures, as the book defines them (BOOK_URL):

  • \(A/p = \mathrm{tr}(\mathbf{M}^{-1})/p\), the average coefficient variance;

  • \(I = \mathrm{tr}(\mathbf{M}^{-1}\mathbf{B})\), the average prediction variance over the cube, with \(\mathbf{B}\) the exact averages of the products of the model terms there;

  • \(E = \lambda_{\min}(\mathbf{M})\), the smallest eigenvalue;

  • \(G = \max_{\mathbf{x}} \mathbf{f}^T\mathbf{M}^{-1}\mathbf{f}\), the largest prediction variance, over the 7-level grid up to five factors (see DesignComparison.g_points above that);

  • \(D = |\mathbf{M}|^{1/p}\), the joint precision of the coefficients;

  • max \(|r|\), the largest absolute correlation between two second-order terms (the squares and two-factor interactions, in the model or not);

  • the power of the test of the least precisely estimated main effect, interaction and quadratic in the model: a non-central \(F(1, N - p)\) with non-centrality \(\lambda = (|\beta|/\sigma)^2 / c\), where \(c\) is the term’s diagonal entry of \(\mathbf{M}^{-1}\).

\(A/p\), \(I\) and \(G\) are in units of \(\sigma^2\), the run-to-run noise variance.

When some settings cannot be run, region holds the limits on them, and \(I\) and \(G\) are taken over the settings that can be run rather than over the cube: \(\mathbf{B}\) is averaged over 20000 points drawn uniformly from the region (with a fixed seed, so the numbers are the same on every run), and \(G\) is searched over the region’s vertices (up to 65536 of them: a region with more has none listed, and its \(G\) can come out below the largest variance, as g_points then says), its support points (where its limits cross the edges of a 3-level grid), points on the boundary of a curved limit (one on a ray from each point of the sample), the points of the grid above that lie in it, and the same sample. With a curved limit, local searches (SLSQP) then climb each design’s prediction variance from up to 8 of those points where it is largest: the largest variance usually sits where the curved boundary meets faces of the box, which no fixed set of points reaches. The other measures do not depend on where predictions are made, and are the same with or without a region. A design with runs outside the region is noted. Without a region, or with one that has no constraints, the cube’s exact moments are used.

A split-plot design’s runs share whole plots, so they are not independent. When the design records the whole-plot variance ratio \(\eta\) it was made for (a split-plot DesignResult keeps it in metadata["whole_plot_variance_ratio"]; a DataFrame can carry one in attrs["whole_plot_variance_ratio"]), every measure uses \(\mathbf{X}^T\mathbf{V}^{-1}\mathbf{X}\) in place of \(\mathbf{M}\), with \(\mathbf{V} = \mathbf{I} + \eta \mathbf{Z}\mathbf{Z}^T\) and \(\mathbf{Z}\) the indicators of the WholePlot column, as evaluate_design() does with random_effects: \(A/p\), \(I\) and \(G\) are then in units of the run variance, and the power keeps the run-level \(N - p\). The design’s note says so.

Parameters:
  • designs (mapping of str to design, or sequence of DesignResult) – The designs, in coded units: a DesignResult (its coded design), a DataFrame of factor columns (RunOrder and Block are ignored), or an array with one column per factor. A mapping’s keys label the designs; in a sequence each design is labelled with its design_type. Every design has the same factors, under the same names.

  • model ({"main_quadratic", "main_effects", "interactions", "quadratic"}) – The model every design is scored on. "main_quadratic", the default and the book’s model for the nine measures, has the intercept, the main effects and the pure quadratics; "quadratic" adds the two-factor interactions.

  • effect_size (float, default 1.0) – The coefficient, in noise standard deviations (\(|\beta|/\sigma\)), whose detection the power measures. With factors coded from -1 to +1, 1.0 is a low-to-high change of two standard deviations.

  • significance_level (float, default 0.05) – The level of the tests behind the power.

  • region (DesignRegion, dict or None) – The settings that can be run: a box region (or its DesignRegion.to_dict() form) over the designs’ factors, whose coded units the designs are in. A factor it does not name is free over its whole range. None, the default, is the cube \([-1, 1]^k\).

Returns:

The values, the table in the book’s layout (DesignComparison.table), notes on each design and its correlation map; DesignComparison.relative() compares two of the designs: their relative efficiencies, coefficient variances and FDS curves.

Return type:

DesignComparison

Raises:
  • ValueError – If model is not one of MODELS, effect_size is not positive, significance_level is not between 0 and 1, no design is given, the designs differ in their factors, or region is a mixture region, has limits that name a categorical level (compare over each region.given(...) instead), names a factor the designs do not have, or leaves no setting to run.

  • TypeError – If a single design is passed instead of a collection of them, or region is neither a DesignRegion nor its dict form.

Examples

>>> from process_improve.experiments import Factor, compare_designs, generate_design
>>> factors = [Factor(name=n, low=-1, high=1) for n in "ABC"]
>>> comparison = compare_designs({
...     "Box-Behnken": generate_design(factors, "box_behnken", n_center_points=3),
...     "DSD": generate_design(factors, "dsd"),
... })
>>> comparison.designs
('Box-Behnken [n=15]', 'DSD [n=9]')
>>> round(comparison.values["Box-Behnken [n=15]"]["power_quadratic"], 3)
0.395

Balanced incomplete block designs: the intrablock, interblock and combined estimates of the treatments.

In a balanced incomplete block design (BIBD) each of \(b\) blocks holds \(k\) of the \(a\) treatments, each treatment appears \(r\) times and each pair of treatments shares \(\lambda\) blocks. A treatment total then carries the effects of the blocks it fell in, so the treatments are compared within blocks (Montgomery, section 4.4; Yates 1940):

  • the adjusted treatment total \(Q_i = y_{i.} - \tfrac{1}{k}\sum_j n_{ij}\,y_{.j}\) removes them, with \(n_{ij}\) 1 when treatment \(i\) is in block \(j\);

  • the intrablock estimate of the treatment effect is \(\hat\tau_i = k Q_i/(\lambda a)\), and the adjusted treatment sum of squares \(k\sum_i Q_i^2/(\lambda a)\).

When the blocks are a random sample, the block totals also carry information about the treatments, Yates’s recovery of interblock information:

  • the interblock estimate \(\tilde\tau_i = [\sum_j n_{ij}\,y_{.j} - k r \bar y_{..}]/(r - \lambda)\);

  • the combined estimate weights the two by their variances,

    \[\tau_i^* = \frac{k Q_i (\sigma^2 + k\sigma_\beta^2) + \left(\sum_j n_{ij}\,y_{.j} - k r \bar y_{..}\right)\sigma^2}{(r - \lambda)\sigma^2 + \lambda a (\sigma^2 + k\sigma_\beta^2)},\]

    with \(\sigma^2\) the intrablock mean square for error and \(\hat\sigma_\beta^2 = [MS_\text{blocks (adjusted)} - MS_E](b - 1)/[a(r - 1)]\), taken as 0 when it comes out negative.

Examples

Montgomery’s Example 4.5, four catalysts in four batches of three:

>>> import pandas as pd
>>> runs = pd.DataFrame({
...     "catalyst": [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
...     "batch": [1, 2, 4, 2, 3, 4, 1, 2, 3, 1, 3, 4],
...     "time": [73, 74, 71, 75, 67, 72, 73, 75, 68, 75, 72, 75],
... })
>>> found = bibd_analysis(runs, "catalyst", "batch", "time")
>>> found["concurrences"], {t: round(v, 2) for t, v in found["interblock_effects"].items()}
(2, {'1': 10.5, '2': -3.5, '3': -0.5, '4': -6.5})
process_improve.experiments.incomplete_blocks.bibd_analysis(data, treatment, block, response)[source]#

Analyse a balanced incomplete block design: intrablock, interblock and combined treatment estimates.

Parameters:
  • data (pandas.DataFrame) – One row per run.

  • treatment (str) – The column of the treatment labels.

  • block (str) – The column of the block labels.

  • response (str) – The column of the response.

Returns:

The design’s n_treatments (a in Montgomery’s notation), n_blocks (b), block_size (k), replicates (r, the runs of each treatment), concurrences (lambda, the blocks each pair of treatments shares) and n_runs (N); by treatment label (as a string): treatment_totals \(y_{i.}\), adjusted_treatment_totals \(Q_i\), intrablock_effects \(\hat\tau_i\), totals_of_blocks_with \(\sum_j n_{ij} y_{.j}\), interblock_effects \(\tilde\tau_i\) and combined_effects \(\tau_i^*\); by block label: block_totals \(y_{.j}\) and adjusted_block_totals \(Q'_j = y_{.j} - \tfrac{1}{r}\sum_i n_{ij}\,y_{i.}\); sums_of_squares (total, treatments_unadjusted, treatments_adjusted, blocks_unadjusted, blocks_adjusted, error), df_error, ms_error and sigma2_blocks (\(\hat\sigma_\beta^2\)).

Return type:

dict[str, Any]

Raises:

ValueError – If a column is missing, a response is not a finite number, a treatment appears twice in a block, or the design is not a balanced incomplete block design: blocks of different sizes, a block holding every treatment, treatments run a different number of times, or pairs of treatments sharing different numbers of blocks.

Variance dispersion: how a design’s prediction variance spreads over spheres about its centre.

The scaled prediction variance of a design of \(N\) runs at a point \(x\) (in coded units) is

\[v(x) = N\,\mathrm{Var}(\hat y(x))/\sigma^2 = N\,f(x)^\top (X^\top X)^{-1} f(x),\]

with \(f(x)\) the model’s terms at \(x\) and \(X\) the model matrix of the runs. A variance dispersion graph plots its minimum, average and maximum over the sphere of each radius \(r\) about the centre (Myers, Montgomery and Anderson-Cook; Montgomery, section 11.4). A design is rotatable when \(v\) is the same at every point of each sphere, so that the minimum and the maximum meet: a rotatable central composite design, or a Box-Behnken design in four or seven factors.

The model’s terms are polynomials in the factors, so \(v\) is one too. It is written once in monomials, which makes the sphere’s average exact: the average of a monomial over the unit sphere in \(k\) dimensions is

\[\mathrm{E}\left[\prod_i u_i^{e_i}\right] = \frac{\prod_i (e_i - 1)!!}{k (k + 2) \cdots (k + |e| - 2)}\]

when every \(e_i\) is even, and 0 otherwise. The minimum and the maximum are searched for: the best of a seeded sample of directions, refined by a local search on the sphere.

Examples

A Box-Behnken design in four factors is rotatable, and one in three is not:

>>> from process_improve.experiments import Factor, generate_design, variance_dispersion
>>> def box_behnken(k):
...     factors = [Factor(name=f"x{i + 1}", low=-1, high=1) for i in range(k)]
...     return generate_design(factors, design_type="box_behnken", n_center_points=3)
>>> variance_dispersion(box_behnken(4))["rotatable"], variance_dispersion(box_behnken(3))["rotatable"]
(True, False)
process_improve.experiments.variance_dispersion.MAX_DEGREE = 3#

The highest degree of a model, in the factors, whose prediction variance is written in monomials.

process_improve.experiments.variance_dispersion.ROTATABLE_TOLERANCE = 1e-08#

The minimum and the maximum on a sphere meet within this fraction of its average on every sphere of a rotatable design.

process_improve.experiments.variance_dispersion.N_STARTS = 4#

The best directions of the sample from which the minimum and the maximum are refined.

process_improve.experiments.variance_dispersion.variance_dispersion(design_matrix, model='quadratic', radii=21, n_directions=2000, random_state=42)[source]#

Return the minimum, average and maximum scaled prediction variance on spheres about the centre.

Parameters:
  • design_matrix (pandas.DataFrame or DesignResult) – The design, in coded units, its factors all numeric. RunOrder and Block columns are set aside, as evaluate_design() does when the model does not name them.

  • model (str or None) – "main_effects", "interactions", "quadratic" (the default, as response-surface designs are compared), or a formula; None means "interactions". Its terms must be polynomials of degree 3 or less in the factors.

  • radii (int or Sequence[float]) – The radii of the spheres: a number of them, evenly spaced from 0 to the largest distance of a run from the centre, or the radii themselves.

  • n_directions (int) – The directions sampled on each sphere, the same on every sphere, before the minimum and the maximum are refined.

  • random_state (int, numpy.random.Generator or None) – Seeds the directions; see Reproducibility Contract (RNG Handling).

Returns:

  • radius: the radii;

  • minimum, mean and maximum: the scaled prediction variance on each sphere; the mean is the exact average over the sphere;

  • spread: the maximum less the minimum, 0 on every sphere of a rotatable design;

  • rotatable: True when the spread is within ROTATABLE_TOLERANCE of the mean on every sphere;

  • design_radius: the largest distance of a run from the centre;

  • n_runs and n_terms: \(N\), and the model’s columns.

Return type:

dict[str, Any]

Raises:

ValueError – If a factor is not numeric, the design cannot estimate the model, the model is not a polynomial of degree 3 or less in the factors, a radius is negative or not a finite number, or n_directions is not a whole number of at least 1.

A known process, for simulating the results of a designed experiment.

Before running a design on the plant, it can be run on a process whose true model is known: simulate the responses, analyse them as if they were real, and compare what the analysis finds with the truth. That shows what a design can and cannot recover, such as an interaction aliased with a main effect, or a quadratic too small to detect at the noise level, before any experiment is spent.

Also see#

process_improve.experiments.compare.compare_designs : the same question asked of the design alone.

class process_improve.experiments.true_process.TrueProcess(coefficients, sigma)[source]#

Bases: object

A process with a known second-order model, in coded units, and normal noise.

The response at coded settings \(\mathbf{x}\) is

\[y = b_0 + \sum_i b_i x_i + \sum_{i<j} b_{ij} x_i x_j + \sum_i b_{ii} x_i^2 + \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, \sigma^2),\]

with the coefficients named as analyze_experiment() reports them, so the analysis of simulated runs can be set beside the truth term by term. A factor of the design that no term names is inert: it has no effect on the response.

Parameters:
  • coefficients (mapping of str to float) – The terms and their coefficients. A term is "Intercept", a factor ("A"), a product of two factors ("A:B" or "A*B") or a square ("I(A ** 2)", "A**2" or "A^2"); each is stored under the name analyze_experiment uses. Terms of third order and above are refused, as is a term given twice under two spellings.

  • sigma (float) – The standard deviation of the run-to-run noise, in the response’s units.

coefficients#

The terms under their canonical names, read-only.

Type:

Mapping[str, float]

sigma#

The noise standard deviation.

Type:

float

Raises:

ValueError – If a term cannot be read, a coefficient is not a finite number, or sigma is not a positive finite number.

Parameters:

Examples

>>> import pandas as pd
>>> process = TrueProcess({"Intercept": 62, "A": 4, "B*A": 1.5, "C^2": -2.25}, sigma=0.5)
>>> dict(process.coefficients)
{'Intercept': 62.0, 'A': 4.0, 'B:A': 1.5, 'I(C ** 2)': -2.25}
>>> runs = pd.DataFrame({"A": [-1, 1, 0], "B": [1, 1, 0], "C": [0, 1, -1]})
>>> process.mean(runs).tolist()
[56.5, 65.25, 59.75]
property factors: tuple[str, ...]#

Return the factors the terms use, in the order they first appear.

mean(design)[source]#

Return the noise-free response at each run of a design.

Parameters:

design (DesignResult or pandas.DataFrame) – The runs in coded units: a DesignResult (its coded design) or a DataFrame with a column per factor; RunOrder and Block are ignored.

Returns:

One value per run.

Return type:

numpy.ndarray

Raises:
  • ValueError – If a term uses a factor the design does not have.

  • TypeError – If the design is not a DesignResult or a DataFrame.

simulate(design, *, random_state=None)[source]#

Return simulated responses: the true mean at each run plus normal noise.

Parameters:
Returns:

One response per run, named "y", on the design’s index.

Return type:

pandas.Series

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, categorical=())[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", a Scheffé mixture model ("scheffe_linear", "scheffe_quadratic", "scheffe_special_cubic", "scheffe_cubic"), or an explicit formula string. None defaults to "interactions".

  • categorical (Collection[str]) – Factors that are categorical. "quadratic" adds no square for them, since a category has no square.

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='auto', new_points=None, observed_at_new=None, response_column=None, whole_plot=None, family=None, link=None, trials=None, dispersion=None, information='expected', df_method='satterthwaite', sigma=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). A RunOrder column, which every generated design carries, is not a factor: it is the order the runs were made in, and "residual_diagnostics" checks the residuals for serial dependence in that order. A Block column with two or more blocks is added to the named non-mixture models as a fixed effect (sum-coded contrast columns Block1, …; one "Block" row in the ANOVA), so block-to-block differences do not inflate the error; drop the column to analyse without it. An explicit formula gets the same fixed effect where it names Block as a term ("y ~ Block + A + B + A:B"). Pure error, for the lack of fit and the curvature test, is taken within blocks whenever the model has the blocks, in either form or written C(Block), C(Block, Treatment(1)) and so on. A WholePlot column, which generate_design adds to a split-plot design, labels each run’s whole plot: it is used by analysis_type="split_plot", and any other analysis warns that it treats the runs as independent. Runs with a missing response or factor setting are left out, with a warning. A factor named like a function the model calls, as I in a model with squares, is fitted under a stand-in name and reported under its own.

  • 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. An unnamed Series takes the name response_column.

  • model (str or None) – "main_effects", "interactions", "quadratic", a Scheffé mixture model ("scheffe_linear", "scheffe_quadratic", "scheffe_special_cubic", "scheffe_cubic", fitted without an intercept since the components sum to 1), an explicit formula whose left-hand side is the response, or None: "scheffe_quadratic" when every run’s factor settings are proportions summing to 1, and "interactions" otherwise. Blends that sum to 1 only to within 0.01, as proportions printed to three decimals do, are blends too, with a warning. A formula with no intercept, over blends alone, whose terms make up the constant is analysed as a Scheffé model. That default becomes "main_effects", with a warning and model_summary["model_note"], when the design cannot estimate the interactions and does not alias them in whole chains (a Plackett-Burman or definitive screening design); "model_selection" still searches the interactions. The model may expand to at most settings.max_formula_terms terms.

  • 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", "split_plot", "glm".

    The ANOVA uses Type II sums of squares (reported as anova_type). The ANOVA and significance test each term at the centre of the design: they are computed with the numeric factors coded to -1/+1, so they do not depend on the factors’ units. On the factors as given, a main effect alongside its interaction or square would be tested where those are zero (0 degC, say), and so would differ between degC and kelvin. A model that coding would change, such as an interaction without its main effects or an I() term that is not a product of whole powers of the factors (I(x ** 0.5), I(z2 - 0.2 * z1)), is tested as given. The ANOVA and significance list exactly aliased terms once, as their alias chain ("A:B + C:D"), as the effects do; the coefficients keep one entry per term. A factor nested in another, whose levels each fall in one of that other’s, leaves the model matrix short of full rank: the ANOVA then tests each term on the degrees of freedom it adds, gives a term that adds none no test, and says so in anova_note, naming the strata for multistratum_anova(). Effects and Lenth’s method use the factors coded to -1/+1: a numeric factor not already coded is mapped from its own minimum and maximum, and a two-level categorical factor from its first level to its second (sorted); the mapping is reported under effects_coding. An effect is the change in response from the lowest to the highest value of its model column: twice the coefficient for a main effect or interaction, the coefficient itself for a square such as I(A**2). A model with an I() term that is not a product of whole powers of the factors has no coded scale, so its effects and Lenth’s method raise ValueError unless its numeric factors are already coded. The curvature test uses pure error from the replicated runs.

    Each of the "coefficients" carries its std_error, t_value, p_value, its interval ci_low to ci_high at the confidence_level of 1 - significance_level, and its standardised_coefficient: the coefficient times the standard deviation of its column of the model matrix, over the standard deviation of the response (the “beta” coefficient, MYSTAT’s STD COEF). It has no units, so terms in different units compare; the intercept’s is 0. Like the coefficient, it depends on coding when the model has interactions or squares. The "coefficients" analysis also gives estimate_correlations, the correlation of each pair of estimates, from \((X'X)^{-1}\) alone, and joint_confidence_region: its sum_of_squares bounds the values of all the coefficients together at 1 - significance_level, \(S_R (1 + p F / (n - p))\) with \(S_R\) the residual_sum_of_squares (Box, Hunter and Hunter, eq. 14.45); with a sigma, \(S_R\) plus sigma squared times the upper chi-square point on \(p\) degrees of freedom.

    "residual_diagnostics" tests the residuals for normality (Shapiro-Wilk), independence (Durbin-Watson) and constant variance (Breusch-Pagan), and gives each run’s leverage, Cook’s distance and studentized residuals. Its durbin_watson is worked in the rows’ order. With a RunOrder column, its run_order entry holds the residuals’ lag-1 autocorrelation in run order, with its standard error 1/sqrt(N), and the Durbin-Watson statistic in run order; a note says when the autocorrelation is more than twice its standard error, a sign that successive runs are serially dependent, which is why Box, Hunter and Hunter randomize the run order. Without one, run_order["known"] is False.

    "split_plot" is for a design whose hard-to-change factors change only between whole plots. Every other analysis is ordinary least squares, which treats the runs as independent, so it makes the whole-plot effects look more significant than they are and the subplot effects less. This one fits the model with a random whole-plot effect, by REML, and tests each term on Satterthwaite’s denominator degrees of freedom, so a whole-plot term is judged against the whole-plot error and a subplot term against the run-to-run error. In a balanced design this is the classical split-plot ANOVA. The tests are marginal (Type III), with every categorical factor coded to sum-to-zero contrasts for them whatever coding the formula gives it, so a main effect is averaged over the factors it interacts with. The coefficients are in the formula’s coding; a named model sum-codes its categorical factors. The result, under "split_plot", has the variance_components (whole_plot, residual and their ratio eta, which a split-plot design is built for), the error_df of the two strata, coefficients and per-term tests (each with its df, stratum and p-value), and the significant_terms. The degrees of freedom use the expected information, so they equal Kenward and Roger’s (as R’s pbkrtest gives them); R’s lmerTest uses the observed information, and gives slightly larger ones in an unbalanced design. df_method="kenward_roger" also applies Kenward and Roger’s inflation of the standard errors and their scaling of the F tests, and information="observed" computes the df from the observed information: the two together are JMP’s analysis. In a balanced design all of these agree. A design run in random blocks is analysed the same way, the blocks named as the whole plots. With new_points, the result also has predictions from the generalized least-squares fit: the predicted mean with its std_error, df and interval (ci_low, ci_high), and the interval for one new run there (pi_low, pi_high, with pi_std_error and pi_df), which falls in a new whole plot, so its variance adds every variance component. The result carries response_name, factor_names and coding as "coefficients" does, so it can be passed to optimize_responses as a fitted model. The fit is made on the runs as given: its coefficients are coded when the factors are already coded, and in actual units otherwise. "glm" fits a generalized linear model of the family given, for a response that is a count of defects ("poisson"), a number of successes out of a number of trials ("binomial") or a positive amount whose spread grows with its size ("gamma"). The coefficients are found by maximum likelihood and tested by Wald’s z; the interval for a mean is worked on the linear predictor’s scale and carried through the inverse link, so it stays in range. The result, under "glm", has the coefficients (with exp_coefficient, an odds ratio or a rate ratio, under a logit or log link), the log_likelihood, deviance and pearson_chi2, the goodness_of_fit tests (Hosmer and Lemeshow’s for the binomial family), and the fitted mean of each run with its interval; with new_points, predictions there as well. See run_glm().

  • significance_level (float) – Default 0.05. Used by every test’s significant flag and every interval.

  • transform (str or None) – "log" (natural), "log10", "sqrt", "inverse", "box_cox", or None. The two logs give the same F tests and p-values; the sums of squares, coefficients and predictions are on the scale chosen. The model is fitted on the transformed response, so predictions and intervals are on that scale (reported as prediction_scale / scale), and confirmation runs are transformed the same way before they are compared. Box-Cox chooses lambda by the profile likelihood of the model. A response outside the transform’s domain raises ValueError.

  • coding ({"auto", "actual", "coded"}, dict or None, default "auto") –

    The scale of the "coefficients" and the "confidence_intervals"; the other analyses do not use it.

    • "auto" (default): "coded", except for a Scheffe mixture model (a keyword, or a formula analysed as one), whose components are proportions, and a model with an I() term that is not a product of whole powers of the factors, such as I(x ** 0.5); these keep "actual", and asking for another scale raises ValueError. None means the same. Before 2.0.0 the default was "actual"; pass it to keep the factors’ own scale.

    • "actual": the factors as given.

    • "coded": the model refitted with each numeric factor mapped from its minimum and maximum to -1 and +1 (a factor already in coded units is left as it is), a two-level categorical factor from its first level to its second, as for the effects, and, in a named model, a categorical factor with more levels given sum-to-zero contrasts.

    • A dict such as {"T": {"low": 150, "high": 200}}: as "coded", but each factor it names is mapped from that range, the same form as optimize_responses’ factor_ranges. Use it when the data’s extremes are not the levels that should be -1 and +1, such as a central composite design’s axial runs.

    Whenever the coefficients are refitted on the coded scale, the result also holds them on the factors’ own scale, as coefficients_actual: the equation to use for predictions in actual units.

    On the coded scale a coefficient is half the change in response across its factor’s range, and in a model with interactions a main effect is tested at the centre of the design. On the actual scale it is tested where the other factors are zero, which can be far outside the experiment (0 degC, say), so its sign and p-value can differ. optimize_responses evaluates models at coded settings, so pass it coded coefficients. The result reports the scale as coding, and, as factor_coding, the low and high that map to -1 and +1 for each factor not already coded.

  • new_points (DataFrame or None) – For prediction or confirmation testing, and for the predictions of "split_plot" and "glm". In a blocked model, a point is predicted in the block its Block column names, one of the runs’ blocks; a point that names no block (no Block column, or a missing value in it), and does not give the block contrast columns, is predicted for the average block.

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

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

  • whole_plot (str, sequence of str, or None) – The column labelling each run’s whole plot, for "split_plot". None (default) uses the WholePlot column when there is one. The column is never a factor. Several columns are several crossed random effects: the rows and the columns of a two-way split-plot (strip-plot) design, whose runs share a row’s first-stage setting and a column’s second-stage processing. The result then has one variance component, error df and set of BLUPs per column, named by it.

  • family (str or None) – For "glm": "binomial", "poisson", "gamma", "gaussian" or "inverse_gaussian".

  • link (str or None) – For "glm": "logit", "probit", "cloglog", "log", "identity", "inverse", "inverse_squared" or "sqrt". None (default) uses the family’s canonical link.

  • trials (str, float, sequence of float or None) – For a binomial "glm": the number of trials of each run, whose response is then its number of successes. A column of design_matrix (never a factor), one number for every run, or one per run. None (default) takes the response as 0 or 1.

  • dispersion ({"pearson", "deviance", "ml"}, float or None) – For "glm": how the dispersion is found. None (default) fixes it at 1 for the binomial and Poisson families and estimates it by Pearson’s statistic over the residual degrees of freedom for the others, as R’s glm does; "ml" estimates it by maximum likelihood, as SAS’s GENMOD does. A number fixes it there.

  • information ({"expected", "observed"}) – For "glm": whether the covariance of the coefficients inverts the expected information (default, as R does) or the observed information (as SAS’s GENMOD does). The two are the same under each family’s canonical link. For "split_plot": the REML information the degrees of freedom are computed from, the expected (default, as SAS and R’s pbkrtest do) or the observed (as JMP and R’s lmerTest do).

  • df_method ({"satterthwaite", "kenward_roger"}) – For "split_plot": "satterthwaite" (default) tests each term on Satterthwaite’s df with the model-based standard errors; "kenward_roger" adjusts the covariance of the estimates for the uncertainty in the variance components and scales the F tests, as Kenward and Roger (1997) do. A single coefficient’s df are the same either way.

  • sigma (float or None) – The error standard deviation when it is known before these runs, from past records or earlier experiments. The standard errors of the "coefficients", "effects", "confidence_intervals", "significance", "prediction" and "confirmation_test" then use it instead of the residual, and their tests and intervals read the normal distribution, not the t, since sigma is not estimated from the runs: each t_value is a z value. A saturated design gets standard errors this way too. The ANOVA, the lack-of-fit test, Lenth’s method and a mixture model’s "significance" and "effects" keep the residual. Give sigma in the units of the response as it is fitted, after transform. None (the default) uses the residual.

Returns:

Results keyed by analysis type. Always includes "response_name", "factor_names" (the factor columns, in order), so that the result with "coefficients" can be passed to optimize_responses as a fitted model, and "model_summary" with the keys:

  • formula - the resolved patsy formula that was fitted.

  • model - the model fitted: a model name, or "formula".

  • transform - the response transform, or None; with box_cox_lambda for Box-Cox.

  • 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: the range of the fitted values over the square root of average_fitted_variance. Box and Wetz’s check reads a fitted surface only when its fitted values change by several standard errors; below 4, Design-Expert’s threshold for this ratio, model_note says the surface is not estimated precisely enough to read its contours or its optimum.

  • average_fitted_variance - the average variance of the fitted values over the runs, p s²/n with s² the residual mean square (p sigma²/n with a sigma): the runs’ leverages sum to p, the number of terms the model estimates, whatever the design (Box and Wetz). Its square root is the fitted values’ average standard error.

  • n_obs - number of observations used to fit.

  • n_terms - number of columns in the model matrix.

  • model_rank - rank of the model matrix with the numeric factors coded to -1/+1, the number of terms the design can estimate whatever the units; less than n_terms implies aliasing / rank deficiency.

  • rank_deficient - True if model_rank < n_terms.

  • ill_conditioned - True if, in the factors’ own units, the model matrix lost rank in floating point although the design did not: the units are too unequal, and the fit is unreliable.

  • df_model - model degrees of freedom.

  • df_residual - residual degrees of freedom.

  • mse_residual - mean squared error of the residuals.

  • prior_sigma - the sigma given, when one is: the standard errors, adequate_precision and average_fitted_variance use it, not the square root of mse_residual.

  • f_statistic and f_p_value - the overall F test of the model against the intercept alone (against zero, on uncorrected sums of squares, with no intercept).

  • log_likelihood, aic and bic - the Gaussian log-likelihood and the criteria that count the error variance as a parameter, as R’s AIC does; aic_step, n ln(RSS/n) + 2p, as R’s step prints it.

  • ss_total, ss_mean and ss_total_uncorrected - the total sum of squares about the mean, the correction for the mean (n times the mean squared), and the crude total, the sum of the squared responses, which is the other two added.

  • ss_model - the model’s sum of squares on the basis of r_squared and f_statistic: about the mean, or about zero for a model whose terms do not span a constant; ss_model_uncorrected - the sum of the squared fitted values, the model’s sum about zero; ss_residual - the residual sum of squares.

  • coefficient_of_variation - 100 times the residual standard deviation over the mean response, Design-Expert’s “C.V. %”; None when the mean is zero.

  • With blocks in the model, ss_blocks (the blocks’ sum of squares, fitted first) and r_squared_without_blocks, r_squared_adj_without_blocks and r_squared_pred_without_blocks: the three R-squared values over the total less the blocks’ sum of squares, as Design-Expert reports them.

  • model_note, only when there is something to say: how the default model was changed to one the design can estimate (see model), and that adequate_precision is below 4.

Notes from individual analyses are kept apart, as anova_note, effects_note, lenth_note, significance_note and split_plot_note. The model_summary is of the least-squares fit, also for "split_plot".

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'

With the error standard deviation known from earlier runs, the four runs, which leave no residual, still give each coefficient a standard error, sigma over the square root of 4:

>>> known = analyze_experiment(df, response_column="y", analysis_type="coefficients", sigma=2.0)
>>> [round(row["std_error"], 6) for row in known["coefficients"]]
[1.0, 1.0, 1.0, 1.0]

Gaussian process models for computer experiments, fitted by maximum likelihood.

A computer model gives the same output every time it is run at the same inputs, so its experiments are modelled by interpolation rather than regression. The usual model (ordinary kriging; Sacks, Welch, Mitchell and Wynn, 1989) treats the output as a constant \(\mu\) plus a Gaussian process of variance \(\sigma^2\) whose correlation between two runs falls with their distance in each input:

\[R_{ij} = \exp\Big(-\sum_k \theta_k (x_{ik} - x_{jk})^2\Big).\]

This is the model and the parameterisation of Montgomery’s Example 11.4 and of JMP’s Gaussian process platform: the inputs in their own units, \(\mu\) and \(\sigma^2\) by maximum likelihood at each \(\theta\), and \(\theta\) maximising the likelihood,

\[-2 \log L = n \log(2 \pi \hat\sigma^2) + \log|R| + n, \qquad \hat\mu = \frac{1^T R^{-1} y}{1^T R^{-1} 1}, \qquad \hat\sigma^2 = \frac{(y - 1\hat\mu)^T R^{-1} (y - 1\hat\mu)}{n}.\]

The prediction at \(x\) is \(\hat\mu + r(x)^T w\) with weights \(w = R^{-1}(y - 1\hat\mu)\), which passes through every run, and its variance is \(\hat\sigma^2 [1 - r^T R^{-1} r + (1 - 1^T R^{-1} r)^2 / (1^T R^{-1} 1)]\).

The sensitivity indices split the variance of the prediction over the box of the inputs into the share each input explains alone (the main effect) and the share it is part of with the others (the total effect), as Sobol’s indices do. For this correlation the integrals over the box separate into one integral per input, worked by Gauss-Legendre quadrature to machine precision. They are arranged so that no two large terms cancel: a smooth output is fitted with a small \(\theta\), and then the weights are large and of both signs, where the usual \(E[f^2] - E[f]^2\) loses its digits.

class process_improve.experiments.gaussian_process.GaussianProcessFit(inputs, theta, mu, sigma2, minus_two_log_likelihood, weights, runs, _factor)[source]#

Bases: object

A Gaussian process model fitted by fit_gaussian_process().

Parameters:
inputs#

The inputs, in the order of theta.

Type:

tuple[str, …]

theta#

The correlation parameter of each input, in the inputs’ own units.

Type:

numpy.ndarray

mu#

The constant mean.

Type:

float

sigma2#

The process variance.

Type:

float

minus_two_log_likelihood#

\(-2 \log L\) at the estimates.

Type:

float

weights#

\(R^{-1}(y - 1\hat\mu)\), one per run: the prediction is \(\hat\mu + r(x)^T w\).

Type:

numpy.ndarray

runs#

The inputs of the runs, one row each.

Type:

numpy.ndarray

predict(points)[source]#

Predict at new points, with the standard error of each prediction.

Parameters:

points (pandas.DataFrame) – A column per input, in its own units.

Returns:

predicted and std_error, one row per point; at a run the prediction is the run’s output and the standard error is 0.

Return type:

pandas.DataFrame

sensitivity(ranges=None)[source]#

Return each input’s main-effect and total sensitivity index over the box of the inputs.

Parameters:

ranges (dict or None) – Each input’s range, {"x": (low, high)}; by default the range of the runs.

Returns:

main_effect and total_effect, one row per input: the shares of the prediction’s variance over the box that the input explains alone, and that it explains alone or together with the others. For two inputs, total_effect - main_effect is the share of their interaction.

Return type:

pandas.DataFrame

Raises:

ValueError – If a range names no input, or does not go from a low end to a higher one.

Notes

The prediction is \(\hat\mu + \sum_i w_i \prod_k c_{ik}(x_k)\), with \(c_{ik}(t) = \exp(-\theta_k (t - x_{ik})^2)\). Over the box, write \(s_k\) for the vector of the averages of \(c_{ik}\), \(D_k\) for their covariance matrix, and \(P_k = s_k s_k^T + D_k\). With products taken entry by entry, the main effect and the total effect of input \(j\) are \(V_j / V\) and \(T_j / V\), where

\[V_j = (w \circ a_j)^T D_j (w \circ a_j), \quad a_j = \prod_{k \ne j} s_k, \qquad T_j = w^T \Big(D_j \circ \prod_{k \ne j} P_k\Big) w\]

are the variance that input \(j\) explains alone and the variance left, on average, when the other inputs are held, and the variance of the prediction is

\[V = \sum_m (w \circ b_m)^T \Big(D_m \circ \prod_{k < m} P_k\Big) (w \circ b_m), \quad b_m = \prod_{k > m} s_k.\]
process_improve.experiments.gaussian_process.fit_gaussian_process(data, response, inputs=None, *, random_state=0)[source]#

Fit a Gaussian process (ordinary kriging) model by maximum likelihood.

Parameters:
  • data (pandas.DataFrame) – The runs of a computer experiment: the inputs, in their own units, and the output.

  • response (str) – The output’s column.

  • inputs (sequence of str or None) – The input columns; every other column by default.

  • random_state (int, numpy.random.Generator or None) – Seeds the random starts of the likelihood search.

Returns:

The estimates, with predict and sensitivity.

Return type:

GaussianProcessFit

Raises:

ValueError – If there are fewer than three runs, a value is missing or not finite, an input or the output does not vary, or two runs share their inputs (the model passes through every run, so it cannot take two outputs at one setting).

Examples

A smooth function of one input, interpolated from six runs:

>>> import numpy as np
>>> import pandas as pd
>>> runs = pd.DataFrame({"x": np.linspace(0, 1, 6)}).assign(y=lambda d: np.sin(3 * d["x"]))
>>> fit = fit_gaussian_process(runs, "y")
>>> predicted = fit.predict(pd.DataFrame({"x": [0.4]}))["predicted"].iloc[0]
>>> print(f"{predicted:.3f} against sin(1.2) = {np.sin(1.2):.3f}")
0.932 against sin(1.2) = 0.932

Models fitted across blocks: fit, choose the form, and predict the next block.

An experiment run in blocks (days, batches of raw material, sensory panel sessions) sees every response shifted a little in each block. The unit of data is then one run’s result in one block, and each model here carries a block effect coded to sum to zero, so its intercept is the average block. Runs tested in more than one block tie the blocks together.

fit_across_blocks() fits each candidate form (a model formula’s right-hand side) to every response, chooses the form with the lowest leave-one-run-out error (each run predicted by a fit without it, in every block it was tested), and returns the chosen fit. Its BlockedFit.predict() predicts new runs with prediction intervals in a known block, in the average block, or in a coming block whose shift is read from runs tested again in it.

class process_improve.experiments.across_blocks.BlockedFit(chosen_form, formula, selection, coefficients, standard_errors, block_shift, block_shift_se, residual_sd, df, _design=None, _fits=<factory>)[source]#

Bases: object

A model fitted across blocks by fit_across_blocks(): the chosen form, its fit, and how it was chosen.

Parameters:
chosen_form#

The name of the chosen form; None when no candidate could be fitted.

Type:

str or None

formula#

The chosen form’s right-hand side.

Type:

str

selection#

One row per candidate form, best first: estimable (every response can be fitted), n_parameters (terms and block columns), loo_rmse (the mean over the responses of each one’s leave-one-run-out RMSE), one column per response with its own leave-one-run-out RMSE, and runs_alone (runs that alone carry a term or a block, so cannot be left out).

Type:

pandas.DataFrame

coefficients#

The chosen form’s coefficients, a row per term (Intercept is the average block), a column per response.

Type:

pandas.DataFrame

standard_errors#

Their standard errors, shaped as coefficients.

Type:

pandas.DataFrame

block_shift#

Each block’s shift from the average block, a row per block, a column per response; the shifts sum to 0.

Type:

pandas.DataFrame

block_shift_se#

Their standard errors, shaped as block_shift.

Type:

pandas.DataFrame

residual_sd#

Each response’s residual standard deviation.

Type:

pandas.Series

df#

Each response’s residual degrees of freedom.

Type:

pandas.Series

predict(new, *, block=None, anchors=None, conf_level=0.95)[source]#

Predict new runs, with prediction intervals, in a known block, the average block or a coming block.

Parameters:
  • new (pandas.DataFrame) – The runs to predict: a column per factor the formula names.

  • block (hashable or None) – A block of the fit: the prediction includes its shift.

  • anchors (pandas.DataFrame or None) – Runs tested again in a coming block, with their factors and the responses measured there. That block’s shift is read from them: the prediction is \((x_0 - \bar{x}_a)\hat\beta + \bar{y}_a\), whose error variance is \(\sigma^2 (1 + 1/n_a + (x_0 - \bar{x}_a)^T (X^TX)^{-1} (x_0 - \bar{x}_a))\).

  • conf_level (float) – The coverage of the intervals.

Returns:

Indexed as new, with columns (response, prediction | lower | upper).

Return type:

pandas.DataFrame

Notes

With neither block nor anchors, the prediction is for the average block, and the interval adds the spread of the fitted block shifts: what a block not yet tested may do.

process_improve.experiments.across_blocks.fit_across_blocks(data, responses, forms, *, block)[source]#

Fit candidate model forms across blocks, choose one by leave-one-run-out error, and return its fit.

Each form is fitted to each response by least squares, with a block effect coded to sum to zero. The rule, fixed before the data: the form that can be fitted to every response with the lowest mean (over the responses) leave-one-run-out RMSE. Leaving out a run removes it from every block it was tested in; the error is that of predicting it there from the other runs, so a form is judged on runs it has not seen.

Parameters:
  • data (pandas.DataFrame) – One row per run per block. The index names the run, so a run tested in several blocks has several rows with one index value. The columns are the factors (numeric, or categorical as text), the block, and the responses; a response not measured in a row is NaN. A factor’s value is the one the run was tested at.

  • responses (sequence of str) – The response columns.

  • forms (mapping of str to str) – Candidate forms, by name: each a model formula’s right-hand side over the factors, such as "A + B + A:B + I(A**2)". A categorical factor can be written C(F, Sum) for effects coding; a square root as np.sqrt(A). Code the factors first when a form’s meaning depends on their centre and scale.

  • block (str) – The block column.

Returns:

The chosen form, its coefficients and block shifts, the table of every candidate, and predict.

Return type:

BlockedFit

Raises:

ValueError – If a column is missing, a form is not a safe right-hand side over the factors, or a factor is missing in a row (patsy’s NA_action="raise").

Examples

>>> fit = fit_across_blocks(data, ["y"], {"linear": "A + B", "quadratic": "A + B + A:B + I(A**2)"}, block="day")
>>> fit.chosen_form, fit.selection
>>> fit.predict(new_runs, anchors=repeats_tested_today)

Curve responses in a designed experiment: FPCA of the curves, a model of each score, bands on predicted curves.

Each run’s response is a curve (a time course, a profile, a spectrum). FPCA reduces the curves to a few scores, and each score is modelled against the design factors with analyze_experiment(), so coding, mixture models and split-plot whole plots behave as they do for a scalar response. Because the eigenfunctions rebuild the curve, a prediction of the scores is a prediction of the whole curve. See docs/development/functional_doe.rst.

process_improve.experiments.functional.VARIANCE_KEPT = 0.99#

Components are kept up to this fraction of the curves’ variance.

process_improve.experiments.functional.VARIANCE_TESTED = 0.999#

Later components, up to this fraction, are kept as well when the design factors explain their scores. A factor can act on a shape that carries little of the variance; a rule on the variance alone would drop it.

class process_improve.experiments.functional.FunctionalFit(fpca, components, scores, residual_covariance, formula, _design, _model_matrix, _curves, _groups, _significance_level, _blocks=(), _settings=None)[source]#

Bases: object

Curves from a designed experiment, fitted by analyze_functional_experiment().

Parameters:
fpca#

The functional PCA of the curves, with every component the curves support.

Type:

FPCA

components#

One row per component up to 99.9% of the variance: variance_ratio, cumulative, the F-test of its score on the design factors (f_statistic, p_value), kept, and the reason it is kept: "variance" up to 99% of the variance, "factor test" after that.

Type:

pandas.DataFrame

scores#

For each kept component, the result of analyze_experiment() on its score: the ANOVA, the coefficients, the lack of fit, and the split-plot analysis when the design has a WholePlot column.

Type:

dict[str, dict]

residual_covariance#

The covariance of the kept scores’ residuals. The scores are uncorrelated across runs, but their residuals after the model is fitted are not.

Type:

pandas.DataFrame

formula#

The right-hand side of the model fitted to each score.

Type:

str

predict(new_points, *, n_bootstrap=400, level=0.95, random_state=None)[source]#

Return the predicted curve at each new point, with pointwise and simultaneous bands.

The bands come from a wild bootstrap. Each run’s whole residual curve, scaled by 1/sqrt(1 - h) for the run’s leverage h, is multiplied by +1 or -1 at random and added back to its fitted curve; the FPCA, the choice of components and the models of the scores are then fitted again, and the new points predicted. The design is kept fixed. With a WholePlot column the sign is drawn once per whole plot, so runs that share a whole plot keep their shared error. A band that treated the eigenfunctions as known would leave out their uncertainty, and cover far less often than it claims.

The bootstrap measures variance, not bias. The smoothing still pulls each curve a little near the ends of the grid, so the bands cover somewhat less often than level there: in a simulation of an 18-run design with a correct model, 95% bands covered the true curve 87% of the time pointwise and 88% of the time over the whole curve.

Parameters:
  • new_points (pandas.DataFrame) – Factor settings, one row per curve to predict, with the factors the model uses. When the model has blocks, each curve is predicted for the average over the blocks.

  • n_bootstrap (int, default=400) – Number of bootstrap replicates. The simultaneous band rests on a high quantile of a maximum, which a few hundred replicates are needed to estimate.

  • level (float, default=0.95) – Coverage of the bands.

  • random_state (int, numpy.random.Generator, or None, default=None) – Seeds the bootstrap, per the reproducibility contract.

Returns:

curve, standard_error, pointwise_low, pointwise_high, simultaneous_low and simultaneous_high: DataFrames with a row per new point and a column per grid point. critical_value: each new point’s multiple of the standard error that the simultaneous band spans. level and n_bootstrap, as used.

Return type:

dict

curve_maps(*, grid=None, n_bootstrap=400, random_state=None)[source]#

Return the fitted curve, and each wild-bootstrap replicate of it, as an affine map of a model row.

At settings whose row of the model matrix is x, the predicted curve is mean + x @ map and replicate b’s is replicate_means[b] + x @ replicate_maps[b]. The bootstrap is the one predict() describes, and predict() forms its bands from these maps. A caller that predicts at many settings, as a page does while a reader moves them, needs only these numbers and the row.

Parameters:
  • grid (numpy.ndarray or None, default=None) – Points to give the curves at, within the fitted range; the FPCA’s grid when not given. A coarser grid keeps the maps small for a curve measured at many points, such as a spectrum.

  • n_bootstrap (int, default=400) – Number of bootstrap replicates.

  • random_state (int, numpy.random.Generator, or None, default=None) – Seeds the bootstrap, per the reproducibility contract.

Returns:

terms, the model matrix’s columns; mean, one value per grid point; map, a row per term; replicate_means and replicate_maps, the same for each replicate; and df_residual.

Return type:

dict

match_target(target, *, bounds=None, n_starts=20, random_state=None)[source]#

Return the factor settings whose predicted curve comes closest to a target curve.

The eigenfunctions are orthonormal, so the L2 distance between two curves in their span is the distance between their scores: the target is scored by the FPCA, and the search moves the predicted scores of the kept components towards it. The part of the target outside the kept components is out of reach of any setting, and is the floor of the distance. The search is a bounded quasi-Newton search from n_starts random starts, of which the best is kept; for mixture components it is held to blends whose proportions sum to one. Different settings can reproduce one target equally well, and the search returns one of them.

Parameters:
  • target (pandas.Series or numpy.ndarray) – The target curve on the grid the curves were measured on; NaN where it has no value.

  • bounds (mapping, optional) – For each factor, a (low, high) range to search, or one value to hold it at. A numeric factor not named is searched over the range the design covers; a categorical factor must be given its level.

  • n_starts (int, default=20) – Number of random starting settings.

  • random_state (int, numpy.random.Generator, or None, default=None) – Seeds the starting settings, per the reproducibility contract.

Returns:

settings (a Series of the factors’ values), curve (the predicted curve there), distance (the L2 distance from the target over the grid), scores and target_scores (the kept components’ scores, predicted and of the target).

Return type:

dict

process_improve.experiments.functional.analyze_functional_experiment(design_matrix, curves, model=None, *, fpca=None, significance_level=0.05)[source]#

Model curves measured in a designed experiment: FPCA of the curves, then a model of each score.

The curves are reduced by functional PCA. The components are kept up to 99% of the curves’ variance, and any later component up to 99.9% is kept too when the design factors explain its score (an F-test at significance_level): a factor can act on a shape that carries little of the variance. Each kept score is then analysed with analyze_experiment(), under the same model. FunctionalFit.predict() predicts whole curves at new factor settings, with bands.

Parameters:
  • design_matrix (pandas.DataFrame) – Factor settings, one row per run. A WholePlot column marks the whole plots of a split-plot design: each score then also gets the split-plot analysis, and the bootstrap of the bands draws one sign per whole plot.

  • curves (pandas.DataFrame) – One curve per run, with the same index as design_matrix, and one column per grid point (see FPCA); NaN where a run was not measured.

  • model (str or None, default=None) – The model of each score, as analyze_experiment() takes it: a named model or a right-hand side.

  • fpca (FPCA or None, default=None) – The functional PCA to use, unfitted, for its smoothing settings; its n_components is ignored, as the components are chosen here. FPCA() when not given.

  • significance_level (float, default=0.05) – Level of the F-test that keeps a later component, and of the score analyses.

Returns:

The FPCA, the components kept and why, each kept score’s analysis, and the residual covariance.

Return type:

FunctionalFit

Raises:

ValueError – If the design and the curves are not the same runs, if the model has no residual degrees of freedom left, or if the model’s terms cannot be read from the design.

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, random_state=42, factor_names=None, hard_limits=None, region=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). Only the factor columns are augmented and returned; see factor_names.

  • augmentation_type (str) – One of "foldover", "semifold", "add_center_points", "add_axial_points", "add_runs_optimal", "upgrade_to_rsm", "add_blocks", "replicate". A mixture design, whose runs are blends (proportions that sum to 1), takes three of them: "add_axial_points" adds its axial check blends, each midway between the centroid of the simplex and a vertex, "add_center_points" adds runs at the centroid, and "replicate" copies the runs.

  • target_model (str or None) – Desired model after augmentation: "main_effects", "interactions", "quadratic". "add_runs_optimal" chooses runs for it (default "interactions"); "upgrade_to_rsm" checks that the upgraded design can estimate it (default "quadratic"), warning and listing the aliased terms when it cannot.

  • n_additional_runs (int or None) – Budget for additional runs, a positive whole number. Interpretation depends on the augmentation type (number of center points, number of D-optimal runs, number of blocks, …). For "upgrade_to_rsm" it is the number of centre runs in the second stage, beside the axial runs; the default of None brings the centre runs to five. A regular two-level design splits into 2, 4, 8, … blocks; another count raises. 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.

  • random_state (int, numpy.random.Generator or None, default 42) – Seeds the exchange’s random starts for "add_runs_optimal"; the default keeps the result reproducible, and None draws fresh starts.

  • factor_names (list[str] or None) – The factor columns. None takes every column except RunOrder and Block, and then refuses a column that does not look like a coded factor (non-numeric, or all on one side of 0 and beyond [-1, 1]), such as a response measured on the runs: augmenting it as a factor would add axial runs on it or negate it in a foldover. Name the factors to keep other columns out.

  • hard_limits (list[str] or None) – The factors whose coded -1 and +1 are limits of the process (see Factor.hard_limits): no new run may set them past those levels. With any named, "add_axial_points" and "upgrade_to_rsm" put their axial runs on the faces of the cube (alpha = 1) unless alpha is given, and refuse an alpha above 1. Every other augmentation keeps its new runs inside -1 to +1 anyway.

  • region (DesignRegion, dict or None) – Limits on the settings that every new run keeps to: a region of the design’s factors (see DesignRegion), or its dict form, such as metadata["region"] of the design generate_design made. The region’s factors give the coding: their low and high are the design’s -1 and +1. "add_runs_optimal" then chooses its runs among the settings the limits allow, and "add_center_points" puts the centre runs at the region’s central setting when the centre of the box breaks a limit. The ranges are limits too, so axial runs sit on the faces of the cube, as with hard_limits. Any other new run that breaks a limit (a folded run, an axial or centre run, a copy) is refused, and the message names it and the limit. Existing runs outside the region are kept, with a UserWarning.

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. With a region, "existing_runs_outside_region" lists the index labels of the existing runs outside it.

Return type:

dict[str, Any]

Raises:

ValueError – If augmentation_type is unknown, if required parameters are missing for the requested augmentation, if a column would be treated as a factor without looking like one (see factor_names), if hard_limits names a column that is not a factor, if axial runs would pass a hard limit, if region does not hold exactly the design’s numeric factors or leaves no setting, or if a new run would break one of its limits. For a mixture design, also if the augmentation is not one of the three it takes, if alpha is given for its axial points, or if its blends do not reach every vertex of the simplex they span (the region is then not a simplex).

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, ridge_direction='maximize', n_pareto_points=21, region=None, random_state=42, n_alternatives=0, flat_tolerance=1e-08)[source]#

Find optimal factor settings for one or multiple responses.

Parameters:
  • fitted_models (list[dict]) –

    One model for the single-response methods ("stationary_point", "canonical_analysis", "steepest_ascent", "steepest_descent", "ridge_analysis"), one or more for "desirability" and two or more for "pareto_front". The models are evaluated at coded settings, so their coefficients must be on the coded scale: pass the result of analyze_experiment(..., analysis_type="coefficients", coding="coded") as it is. One fitted with coding="actual" on factors that are not already coded is refused, since its optimum would be searched for between -1 and +1 in actual units. 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.

    • "coding" and "factor_coding" (optional) - the scale of the coefficients and each factor’s coded range, as analyze_experiment reports them. A model without them is taken to be coded.

  • 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, and names that do not pair up one to one raise a ValueError. When either side omits a name, goals are taken in list order, with a warning.

    • "goal" (str) - "maximize", "minimize", "target" or "in_range". An "in_range" goal (Design-Expert’s “is in range”) holds the predicted response between low and high: the search keeps to settings where it does, its individual desirability is 1 there, and it is left out of the composite. At least one goal must be of another kind. "pareto_front" takes the other three only.

    • "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. A coded model from analyze_experiment supplies the ranges of its own factors, and a range given here for one of them must agree with it.

  • step_size (float) – Step length for steepest ascent/descent: the Euclidean distance, in coded units, between successive points on the path. Must be positive.

  • 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. "ridge_analysis" traces spheres out to the largest |bound| and flags each path point with inside_search_bounds, since a sphere cannot follow bounds that differ between factors or are asymmetric.

    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.

  • 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.

  • region (DesignRegion or None) – The region the optimum must lie in, for "desirability" and "pareto_front": the same constraints the design was built under (DesignRegion.from_dict(result.metadata["region"])), so the recommended settings are ones the process can run. For a box region the constraints apply on top of search_bounds. For a mixture region the factors are the proportions the Scheffé model was fitted on, their bounds replace search_bounds, and the optimum sums to 1. The desirability result then carries within_region. Other methods ignore it, with a warning.

  • random_state (int, Generator, or None) – Seed for the random starting points of the multistart search used by "desirability" and "pareto_front"; the other methods are deterministic. The default of 42 keeps results reproducible; try another seed to check that an optimum does not depend on where the search began.

  • n_alternatives (int) – For "desirability": how many other settings that do almost as well to report beside the best, under "alternatives". Each is a local optimum of the multistart search whose composite desirability is at least 95% of the best’s, at least 0.5 apart in coded units from the best and from every other alternative, best first. Asking for any searches from ten more starts for each. The default of 0 reports none and leaves the search as it was.

  • flat_tolerance (float) – For "stationary_point" and "canonical_analysis": an eigenvalue of the pure-quadratic matrix B at most this share of the largest in size is read as zero, so the surface is classified as a ridge along its eigenvector, and so is the slope along that eigenvector: a ridge rises only when its slope is above this share of the largest eigenvalue. The default, 1e-8, reads only rounding as zero. An estimated eigenvalue is rarely exactly zero, and the sources read a small one as flat: Box, Hunter and Hunter’s stationary ridge has eigenvalues -9.02 and 0.0017 and a slope of 0.005 along it, which flat_tolerance=0.05 reads as a stationary ridge; their rising ridge, with a slope of 2.97, still rises. Below 5% of the largest, the result’s classification_note says so.

Returns:

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

Return type:

dict[str, Any]

Raises:

ValueError – If method is unknown, if flat_tolerance is not in [0, 1), if fitted_models is empty or a model lacks "coefficients" or "factor_names", if "stationary_point" or "canonical_analysis" gets a factor name that no model term uses, if a single-response method ("stationary_point", "canonical_analysis", "steepest_ascent", "steepest_descent", "ridge_analysis") gets more than one model, if a method that needs goals is called without them, or if a model’s coefficients are in actual units, two models code a factor from different ranges, or factor_ranges disagrees with a model’s coding.

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 sequential method of response surfaces: a path of steepest ascent, and the next design on it (#798).

A first design finds the direction in which the response improves. The experimenter steps along it, runs a few settings on the path, and centres the next design on the best of them. Two functions set that out from a fitted model:

  • steepest_path() steps along the steepest ascent (or descent) as sources set it out: a round amount of one leading factor, in its own units, with the other factors in proportion to the slopes;

  • next_design() centres a new design on a step of that path, each factor’s range kept or rescaled.

exception process_improve.experiments.sequential.NoSteepestDirectionError[source]#

Bases: ValueError

The fitted model is flat at the base of the path, so no direction improves it fastest.

Raised by steepest_path() when the gradient of the model at the base is zero: at the design centre, when every first-order coefficient is zero.

process_improve.experiments.sequential.steepest_path(model, step=0.5, n_steps=10, *, base=None, goal='maximize')[source]#

Return the path of steepest ascent (or descent) of a fitted model, as a table of steps (#798 S1).

The path leaves base along the gradient of the fitted model there, which is the first-order coefficients when the base is the centre of the design, and moves every factor in proportion to its slope in coded units. A curved surface turns away from this straight line; ridge analysis (optimize_responses(..., method="ridge_analysis")) follows the curve instead.

Parameters:
  • model (dict) – A fitted model, as analyze_experiment() returns it, or written out by hand: its coefficients on the coded factors, its factor_names, and optionally its factor_coding (each factor’s low and high), which gives each step in actual units too.

  • step (float or dict) –

    How far each step goes.

    • A number: the distance between successive points, in coded units (default 0.5).

    • {lead: amount}: the lead factor moves amount a step, in its own units (its actual units when the model has its coding, else coded units), up or down as its slope says, and the others move in proportion. This is how sources set a path out: Davies (Table 11-22) steps 10 c.c. of solvent, Box, Hunter and Hunter (Table 15.2) 5 minutes of reaction time, and Schmidt and Launsby one coded unit of the factor with the smaller slope.

  • n_steps (int) – The number of steps away from the base (default 10). The table holds n_steps + 1 rows: step 0 is the base.

  • base (dict or None) – Where the path starts, each factor in its own units, as for step. A factor left out is at the centre of its range; by default the path starts at the centre of the design.

  • goal ({"maximize", "minimize"}) – "maximize" follows the steepest ascent, "minimize" the steepest descent.

Returns:

  • goal, and direction: "ascent" or "descent".

  • lead: the factor that sets each step, or None for a distance in coded units.

  • step_size: the distance between successive points, in coded units.

  • direction_vector: the unit vector of the path, in coded units.

  • increment: each factor’s change a step, coded and, with the coding, actual.

  • factor_ranges: the ranges the coding gives, which next_design() reads.

  • steps: one row per step, with its step number, its coded settings, its actual settings when the model has its coding, and its predicted_response.

The intervals on the predictions need the fit itself: pass the coded settings of the steps as new_points to analyze_experiment(..., analysis_type="prediction").

Return type:

dict

Raises:
  • NoSteepestDirectionError – If the gradient of the model is zero at the base.

  • ValueError – If step, n_steps, base or goal is not one the path can take, or the lead factor has no slope at the base, so stepping it cannot set the path.

Examples

>>> from process_improve.experiments import steepest_path
>>> model = {
...     "coefficients": [
...         {"term": "Intercept", "coefficient": 75.9},
...         {"term": "x1", "coefficient": 4.55},
...         {"term": "x2", "coefficient": -1.45},
...     ],
...     "factor_names": ["x1", "x2"],
...     "factor_coding": {"x1": {"low": 100, "high": 200}, "x2": {"low": 125, "high": 175}},
... }
>>> path = steepest_path(model, step={"x2": 25}, n_steps=1)
>>> round(path["steps"][1]["actual"]["x1"], 1), path["steps"][1]["actual"]["x2"]
(306.9, 125.0)
process_improve.experiments.sequential.next_design(path, at_step, *, scale=None, **design)[source]#

Return a new design centred on a step of a path of steepest ascent (#798 S2).

The design’s centre is the path’s setting at at_step, and each factor keeps the width of its range, times scale[factor] where given: Davies (11.42) enlarges the units of the factors whose effects were small, Box, Hunter and Hunter widen the ranges near a maximum, and others narrow them. A factor whose range the path does not know (a model without its coding) is set out in the coded units of the first design, from -1 to +1 about its new centre.

Parameters:
  • path (dict) – A path from steepest_path().

  • at_step (int) – The step to centre on: 0 is the base of the path.

  • scale (dict or None) – A factor’s new range width over its old one; 1 for a factor left out.

  • **design – The design to make, as generate_design() takes it: design_type="full_factorial", n_center_points=3, say.

Returns:

The design, with design_actual in the factors’ units about the new centre.

Return type:

DesignResult

Raises:

ValueError – If at_step is not a step of the path, or scale names a factor the path does not hold or gives a factor a width that is not a positive number.

Examples

>>> from process_improve.experiments import next_design, steepest_path
>>> model = {
...     "coefficients": [
...         {"term": "Intercept", "coefficient": 40.0},
...         {"term": "A", "coefficient": 2.0},
...         {"term": "B", "coefficient": 1.0},
...     ],
...     "factor_names": ["A", "B"],
...     "factor_coding": {"A": {"low": 100, "high": 120}, "B": {"low": 1, "high": 3}},
... }
>>> path = steepest_path(model, step={"A": 10}, n_steps=3)
>>> new = next_design(path, at_step=3, scale={"B": 2.0}, design_type="full_factorial", n_center_points=3)
>>> sorted(set(new.design_actual["A"])), sorted(set(new.design_actual["B"]))
([130.0, 140.0, 150.0], [1.5, 3.5, 5.5])

Checks on a fitted response surface before it is read (#797).

A second-order surface fitted to a composite design is read for its optimum. Three questions come first: does the quadratic fail along an axis, how large is the noise the surface is read against, and how well is its stationary point located?

  • third_order_contrasts() compares, for each factor, the slope its axial runs give with the slope its factorial runs give, which agree under a second-order model.

  • pooled_pure_error() pools the replicated runs of every design of one investigation, each set of replicates compared with its own mean.

  • stationary_point_region() gives the confidence region of the stationary point: the settings at which the fitted surface’s slope does not differ significantly from zero.

Whether the fitted values change by enough to read at all is analyze_experiment’s model_summary["adequate_precision"], with a note in model_note when it is below 4.

process_improve.experiments.surface_checks.third_order_contrasts(design, response_col, factor_cols, sigma=None)[source]#

Return, for each factor of a composite design, the slope its axial runs give less the slope its cube gives.

A composite design measures each factor’s slope twice: from its axial runs, at plus and minus alpha with the other factors at their centres, and from the runs of its factorial cube. Under a second-order model the two agree. Their difference estimates (alpha^2 - 1) b_iii less the sum of b_ijj over the other factors j, which is b_iii - b_ijj for two factors at alpha = sqrt(2), as Box, Hunter and Hunter (15.20, 15.21) write it. A contrast many standard errors from zero says the quadratic fails along that axis, which the lack-of-fit F test, pooling every axis, does not. Each slope is a difference within one set of runs, so a block effect between the cube and the axial runs cancels.

Parameters:
  • design (DataFrame) – The runs: the factors in coded units (-1 and +1 at the cube’s levels), and the response.

  • response_col (str) – The response column.

  • factor_cols (sequence of str) – The factors, two or more.

  • sigma (float or None) – The standard deviation of one run, when it is known beforehand (Box, Hunter and Hunter take 1.5). By default it is the pure error of the replicated runs, each set compared with its own mean, within blocks when the design has a Block column.

Returns:

contrasts: for each factor, its axial_slope, factorial_slope, their difference contrast, and the std_error of that difference. sigma is the standard deviation used, and sigma_df its degrees of freedom when it comes from the runs (None for a given sigma).

Return type:

dict

Raises:

ValueError – If there are fewer than two factors, no runs of a factorial cube, a factor without an axial run on each side of the centre, or no replicated runs and no sigma.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import third_order_contrasts
>>> a = 2 ** 0.5
>>> runs = pd.DataFrame(
...     [(-1, -1, 78.8), (1, -1, 84.5), (-1, 1, 91.2), (1, 1, 77.4), (-a, 0, 83.3), (a, 0, 81.2),
...      (0, -a, 81.2), (0, a, 79.5), (0, 0, 87.0), (0, 0, 86.0)],
...     columns=["time", "temperature", "y"],
... )
>>> out = third_order_contrasts(runs, "y", ["time", "temperature"], sigma=1.5)
>>> {name: round(c["contrast"], 2) for name, c in out["contrasts"].items()}
{'time': 1.28, 'temperature': -1.93}
>>> round(out["contrasts"]["time"]["std_error"], 2)
1.06
process_improve.experiments.surface_checks.pooled_pure_error(designs, response_col, factor_cols)[source]#

Return the pure error pooled over the designs of one investigation (#797 R3).

Each design’s replicated runs are compared with their own mean, and the sums of squares and the degrees of freedom are added over the designs. Runs of two designs at the same coded setting are not replicates of each other, since the design’s centre moves from one to the next. Box, Hunter and Hunter (15.16, 15.24) pool the centre runs of three designs this way. A blocked design whose blocks may differ goes in as one design per block.

Parameters:
  • designs (sequence of DataFrame) – The designs, each with its factor columns and the response.

  • response_col (str) – The response column.

  • factor_cols (sequence of str) – The factors whose settings say which runs are replicates.

Returns:

ss_pure_error, df_pure_error, ms_pure_error (the pooled variance) and std_dev, its square root, with per_design, each design’s ss and df.

Return type:

dict

Raises:

ValueError – If no design is given, or none has replicated runs.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import pooled_pure_error
>>> first = pd.DataFrame({"x": [0, 0, 0], "y": [60.3, 64.3, 62.3]})
>>> later = pd.DataFrame({"x": [0, 0], "y": [89.7, 86.8]})
>>> round(pooled_pure_error([first, later], "y", ["x"])["ms_pure_error"], 2)
4.07
process_improve.experiments.surface_checks.stationary_point_region(design, response_col, factor_cols, sigma=None, significance_level=0.05)[source]#

Return the confidence region of the stationary point of the second-order surface fitted to the runs (#797 R5).

At the true stationary point the true surface’s slope in every direction, its gradient b + 2Bx, is zero. The fitted surface’s gradient at any setting x is linear in the estimated coefficients, so its covariance follows from theirs, and an F test on k and n - p degrees of freedom asks whether it differs from zero there. The settings at which it does not, at level significance_level, form a 1 - significance_level confidence region for the stationary point (Box and Hunter, 1954, “A confidence region for the solution of a set of simultaneous equations with an application to experimental design”, Biometrika 41, 190-199). The region need not be an ellipse around the estimate, nor bounded: along a ridge it runs out of the experiment. With one factor it is Fieller’s interval for -b1 / (2 b11).

The full second-order model is fitted with a fixed effect for each block when the design has a Block column, and with each factor coded from its smallest setting to its largest, so that no choice of units costs precision. The region does not depend on the coding, and every setting and result is in the factors’ units as given. Beside it, std_error gives the delta-method standard error of each coordinate of the estimated stationary point x_s = -B^-1 b / 2, whose derivative with respect to the coefficients is -(2B)^-1 times the gradient’s: a summary that holds near x_s when the region is small and nearly elliptical.

Parameters:
  • design (DataFrame) – The runs: the factors and the response, and optionally a Block column.

  • response_col (str) – The response column.

  • factor_cols (sequence of str) – The factors of the surface, one or more.

  • sigma (float or None) – The standard deviation of one run, when it is known beforehand. The test is then on the chi-square distribution with k degrees of freedom, scaled to the F statistic’s units. By default it is the residual standard deviation of the fit.

  • significance_level (float) – Default 0.05, for a 95% region.

Returns:

stationary_point, the estimate in the units of the factors as given (None when the fitted surface has none: a rising ridge), classification ("maximum", "minimum", "saddle_point", "stationary_ridge" or "rising_ridge", as optimize_responses() reads the eigenvalues of B), and std_error, each coordinate’s delta-method standard error (None for a ridge, along which the point is not determined). statistic is a function of settings returning the F statistic of the gradient there, and contains one returning whether each setting is in the region, where the statistic is at most critical_value. Both take a DataFrame or a mapping of the factors’ names to values or arrays (a grid from numpy.meshgrid, say), or an array whose last axis holds the factors in order, and return an array of the shape of the settings. confidence_level is 1 - significance_level; sigma is the standard deviation used, and sigma_df its degrees of freedom when it comes from the runs (None for a given sigma).

Return type:

dict

Raises:

ValueError – If there is no factor, a run has a missing value, the runs cannot estimate every term of the second-order model, the response does not vary, there is no residual and no sigma, or sigma or significance_level is out of range.

Examples

Box, Hunter and Hunter’s composite design (Table 15.4), at alpha = sqrt(2). The fitted surface is a maximum whose smaller eigenvalue is 3% of the larger, nearly a ridge, so its stationary point lies far outside the runs and is poorly located: the centre of the design is inside the 95% region, while (1, 0) is not.

>>> import pandas as pd
>>> from process_improve.experiments import stationary_point_region
>>> a = 2 ** 0.5
>>> runs = pd.DataFrame(
...     [(-1, -1, 78.8), (1, -1, 84.5), (-1, 1, 91.2), (1, 1, 77.4), (0, 0, 89.7), (0, 0, 86.8),
...      (-a, 0, 83.3), (a, 0, 81.2), (0, -a, 81.2), (0, a, 79.5), (0, 0, 87.0), (0, 0, 86.0)],
...     columns=["time", "temperature", "y"],
... )
>>> region = stationary_point_region(runs, "y", ["time", "temperature"])
>>> region["classification"]
'maximum'
>>> {name: round(value, 1) for name, value in region["stationary_point"].items()}
{'time': -3.7, 'temperature': 3.0}
>>> {name: round(value) for name, value in region["std_error"].items()}
{'time': 22, 'temperature': 18}
>>> region["contains"]({"time": [0.0, 1.0], "temperature": [0.0, 0.0]}).tolist()
[True, False]

The fractional-factorial trade-off: runs against factors.

The central question when screening many factors is how few 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 runs and 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 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

exception process_improve.experiments.trade_off.SearchLimitError[source]#

Bases: ValueError

The design exists, but finding its minimum-aberration generators would take too long.

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 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 factors.

  • SearchLimitError – A subclass of ValueError: the design exists, but the search space is too large to enumerate and the design is not in the tabulated minimum-aberration catalogue.

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 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 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). A "?" marks a design that exists but whose minimum-aberration generators are too costly to search for and are not in the tabulated catalogue.

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, and designs too costly to search for are "?".

Return type:

pd.DataFrame

Raises:

ValueError – If a run count is not a power of two, or a factor count is below 2.

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 foldover 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. The bound counts one centre run. A foldover can carry none, or several: N = 2h + c for c centre runs, and the even block sees h distinct rows plus one more when c >= 1. For a given N the most distinct rows come from one centre run when N is odd, (N + 1) / 2 of them, and from none or two when N is even, N / 2. An even budget therefore buys the model of the odd budget one run below it, with one more error degree of freedom. Three capability classes follow, and they are the OMARS analogue of resolution:

Full

N >= k^2 + k + 1, or k^2 + k + 2 for an even N. 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 + 2. 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. The smallest, 2k + 2 runs, is a definitive screening design with a second centre run.

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 bound here is for foldover OMARS designs, the class that generate_omars() builds. OMARS designs in general (Nunez Ares and Goos, 2020) need not be foldovers, and a non-foldover member can do better: a 19-run, four-factor OMARS design from which the full second-order model is estimable exists, against the 21 runs a foldover needs. The table therefore gives the smallest foldover size for each class, not the smallest OMARS size.

exists says that a foldover OMARS design of that size exists, not that generate_omars() builds it. That generator needs error degrees of freedom, so it refuses the saturated Satd size (for an even k, generate_design(design_type="dsd") gives that design); it needs at least one centre run, so it builds an even size with two; and it repeats half-runs only for three and four factors, up to a cap. Larger sizes in the table, such as 39 runs for three factors, exist but are not built.

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.

process_improve.experiments.omars_trade_off.REFERENCE_DESIGNS: tuple[str, ...] = ('dsd', 'bbd')#

Named standard designs the table can mark, smallest first. The definitive screening design sits at the small end of the OMARS family and the Box-Behnken design is among the largest, so marking both shows the span a column covers rather than only its middle.

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 a run count 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 + 2.

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 foldover OMARS run count reaching capability for n_factors.

The bound holds for foldover designs [H; -H; 0]; a non-foldover OMARS design can reach a class with fewer runs (see the module docstring).

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 run count: k^2 + k + 1 for "full", 2k + 2 for "quad", 2k + 1 for "satd". The "quad" size is even: a foldover with two centre runs, one more than the saturated design.

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")
12
>>> [omars_minimum_runs(k) for k in (3, 4, 5, 6, 7)]
[13, 21, 31, 43, 57]
process_improve.experiments.omars_trade_off.definitive_screening_runs(n_factors)[source]#

Return the run count of the definitive screening design for n_factors.

Parameters:

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

Returns:

2m + 1 for a conference matrix of order m: 2k + 1 for an even n_factors and 2k + 3 for an odd one, except where no conference matrix of that order exists.

Return type:

int

Notes

The construction folds a conference matrix of order k, available for an even k. For an odd k it uses one of order k + 1 and drops the last column, so the design arrives with two runs to spare and lands one capability class above saturated. No conference matrix of order 22 exists (Belevitch’s condition: m - 1 must be a sum of two squares), so 21 and 22 factors use order 24 and 49 runs.

Examples

>>> [definitive_screening_runs(k) for k in (3, 4, 5, 6, 7)]
[9, 9, 13, 13, 17]
process_improve.experiments.omars_trade_off.box_behnken_runs(n_factors)[source]#

Return the run count of the published Box-Behnken design for n_factors.

Parameters:

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

Returns:

Design runs plus the centre runs the published tables quote, or None when Box and Behnken did not publish a design for that factor count.

Return type:

int or None

Examples

>>> [box_behnken_runs(k) for k in (3, 4, 5, 6, 7)]
[15, 27, 46, 54, 62]
>>> box_behnken_runs(8) is None
True
process_improve.experiments.omars_trade_off.omars_anchor_entry(design, n_factors)[source]#

Report what a named standard design buys at n_factors.

The anchor counterpart of get_omars_trade_off_table_entry(), for the two designs that bracket the OMARS family rather than for a run budget.

Parameters:
  • design ({"dsd", "bbd"}) – Which standard design: the definitive screening design, at the small end of the family, or the Box-Behnken design, among the largest.

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

Returns:

None where no such design was published for that factor count, which is every Box-Behnken design above seven factors.

Return type:

OmarsTradeOffTableEntry or None

Raises:

ValueError – If design is not a known name, or n_factors is out of range.

Notes

get_omars_trade_off_table_entry() reports a budget at its best centre-run arrangement, one centre run for an odd count and none or two for an even one. A named design carries whatever centre replication its published form specifies instead: a Box-Behnken design has three or six centre runs, so its total is even from five factors upwards. Its capability is that of its distinct half-rows, which extra centre runs do not alter.

Examples

>>> omars_anchor_entry("bbd", 5).n_runs, omars_anchor_entry("bbd", 5).label
(46, 'Full df=25')
>>> omars_anchor_entry("dsd", 4).label
'Satd df=0'
>>> omars_anchor_entry("bbd", 9) is None
True

Also see#

omars_trade_off_table : puts these on the table as anchor rows.

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 capability is that of the foldover class; a non-foldover OMARS design of the same size can support a larger model (see the module docstring). exists does not mean that generate_omars() builds the size; see the module docstring for the sizes it refuses.

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. An odd budget is a foldover with one centre run, 2h + 1 runs; an even one is a foldover with no centre run or with two, and buys the model of the odd budget one run below it, with one more error degree of freedom.

  • 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'
>>> get_omars_trade_off_table_entry(10, 4, display=False).label
'Quad df=1'
>>> get_omars_trade_off_table_entry(22, 4, display=False).label
'Full df=7'

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, anchors=False)[source]#

Return the run-budget against factor-count table for foldover 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 below the smallest foldover design. The bounds are for foldover designs, and not every listed size is one that generate_omars() builds; see the module docstring for both points.

Parameters:
  • runs (sequence of int, default DEFAULT_RUNS) – Run budgets, one per row. An even budget is read as a foldover with no centre run or with two.

  • 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.

  • anchors (bool, default False) – Add a DSD row above the budgets and a BBD row below them, giving the two ends of the family a fixed place on the table. Their run counts change from column to column, so their cells lead with the run count, for example "46 Full df=25". Turning this on makes the index mix strings with the integer budgets.

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'
>>> anchored = omars_trade_off_table(display=False, anchors=True)
>>> anchored.loc[9, 4]
'Satd df=0 | DSD'
>>> anchored.loc[46, 5]
'Full df=25 | BBD'
>>> anchored.loc[54, 5]
''

Also see#

get_omars_trade_off_table_entry : the detail behind one cell. definitive_screening_runs, box_behnken_runs : the anchor run counts. omars_anchor_entry : the detail behind one anchor 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 (a string or None included), or is below 77 seconds.

Examples

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

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 a finite number (a string or None included), 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 a finite number (a string or None included), or is negative.

Examples

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

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 rows are not in standard order (the first factor, H, does not alternate from row to row), so code each factor from its own column, not from the row position. 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.corrosion()[source]#

Return the corrosion-resistance split-plot experiment from Box, Hunter and Hunter, chapter 9.

Description#

Steel bars were treated with four coatings, C1 to C4, and baked at three furnace temperatures, 360, 370 and 380 degrees C, to see which combination resists corrosion best. Resetting the furnace temperature is slow, so the experimenters ran six heats, each temperature twice, in the order given by Heat; in each heat, one bar with each coating was placed at a randomly chosen position in the furnace.

The heats are the whole plots, and temperature, which changes only from heat to heat, is the whole-plot factor; the bars are the subplots, and coating the subplot factor. Analysing these data as if the 24 bars were independent finds temperature significant and coating not, the opposite of the correct split-plot analysis: see analyze_experiment(..., analysis_type="split_plot").

Dimensions#

A data frame containing 24 observations of 5 variables: Heat (1 to 6), Temperature (degrees C), Position (1 to 4 in the furnace), Coating ("C1" to "C4") and the response Resistance.

Source#

Box, G. E. P., Hunter, J. S. and Hunter, W. G., Statistics for Experimenters, 2nd edition, Wiley, 2005, chapter 9, Table 9.1; as corrosion.data in the BHH2 R package.

Return type:

DataFrame

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

Return the NIR spectra of 72 biscuit doughs whose four main ingredients were varied.

Description#

A standard short-dough biscuit recipe was varied to give a broad range of each of four ingredients: fat, sucrose, dry flour and water. The near-infrared reflectance spectrum of each unbaked dough was then recorded from 1100 to 2498 nm. Each run’s response is therefore a curve of 700 points, not one number.

The four percentages add to 98.08 in every dough (the remaining ingredients are held fixed), so they are mixture components: only three of them vary independently, and a model of the spectrum on the composition is a mixture model. The first 40 doughs are the calibration set and the other 32 the validation set. Sample 23 (calibration) and sample 61 (the 21st of the validation set) are known outliers.

Dimensions#

A data frame of 72 rows and 706 columns: sample (1 to 72), set ("calibration" or "validation"), the four ingredient percentages fat, sucrose, dry_flour and water, then the reflectance at each wavelength in the 700 columns "1100", "1102", …, "2498" (nm).

Source#

Osborne, B. G., Fearn, T., Miller, A. R. and Douglas, S., Application of near infrared reflectance spectroscopy to the compositional analysis of biscuits and biscuit doughs, Journal of the Science of Food and Agriculture, 35, 99-105, 1984; as cookie in the ppls R package. The values are the source’s measurements, which the MIT licence of this package does not cover; cite the source when you use them. See the NOTICE beside the file.

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 "biscuit", "boilingpot", "corrosion", "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.

The reference catalogue: designed experiments from published sources, reproduced with this library.

The reference catalogue holds worked examples, exercises, case studies and designs of published sources (a textbook, an encyclopedia article, a course’s quizzes, tutorial slides, journal papers), recorded so that software can be checked against them. Each entry holds its data exactly as printed (factors, responses, runs) and every number the source prints for it, as the text printed, with where it is printed. A number also carries a check, saying how to recompute it (the kind of result, the term, the model, the runs it uses), or why it is not recomputed. A number known not to reproduce records why (its disagreement) and the cause of the difference, one of DISAGREEMENT_CAUSES; a number not recomputed records the cause too, one of UNCHECKED_CAUSES, which tells a number the library cannot compute yet from one with nothing to compute.

reproduce_reference_catalogue() recomputes every checked number with this library and reports, row by row, the printed text, the value found, whether they agree to within the source’s own rounding, and where the number is printed.

Each entry also lists its subject-matter topics: what the experiment is about (semiconductors, food and beverages, …), as opposed to how it is designed. reference_topics() returns the vocabulary, each topic with its nearest classes in UN ISIC Rev. 4 (industries) and OECD FORD (fields of research), and the topic argument of reference_experiments() selects by it.

The catalogue’s files hold facts and short quotations from their sources, not the sources themselves. The MIT licence of this package covers its code, not them: each source states its own terms in rights (see reference_sources() and the folder’s NOTICE.md).

Examples

>>> from process_improve.experiments.reference_catalogue import (
...     reference_experiments,
...     reproduce_reference_catalogue,
... )
>>> [e["name"] for e in reference_experiments("ullmann-doe-2012")][:2]
['ullmann-catalyst-temperature', 'ullmann-agitator']
>>> report = reproduce_reference_catalogue("ullmann-doe-2012")
>>> bool(report.loc[report["checked"], "agrees"].all())
True
process_improve.experiments.reference_catalogue.reference_topics()[source]#

Return the subject-matter topics of the reference catalogue: what its experiments and designs are about.

Every entry lists one or two of these topics in topics, its main topic first. A topic says what the experiment is about, not how it is designed (design.type says that); an entry whose source states no application, such as a table of designs, is "general". The topics are the catalogue’s own classification, not the sources’. Each one names its nearest classes in two standards, so the catalogue rolls up to either: an industry in UN ISIC Rev. 4, and a field of research in OECD FORD (Frascati Manual 2015).

Returns:

For each topic id, in the vocabulary’s order: label, description, isic (ISIC Rev. 4 codes: a section letter, a two-digit division or a three-digit group; empty when no class fits), ford (a FORD field code such as "2.4", or None) and ford_field (that field’s name, or None).

Return type:

dict[str, dict]

Examples

>>> topic = reference_topics()["semiconductors"]
>>> topic["label"], topic["isic"], topic["ford"]
('Semiconductors and electronic assembly', ['261'], '2.2')
process_improve.experiments.reference_catalogue.reference_sources()[source]#

Return the citation of each source of the reference catalogue, by its key.

Returns:

For each source key, its citation: authors, title, url, year, and where they apply work, volume, pages, publisher, doi, the version and commit that was read, read: when and from what the numbers were taken, and rights: the terms of the source’s numbers and quotations, which the MIT licence of this package does not cover.

Return type:

dict[str, dict]

Examples

>>> reference_sources()["ullmann-doe-2012"]["doi"]
'10.1002/14356007.e08_e01.pub3'
process_improve.experiments.reference_catalogue.reference_experiments(source=None, topic=None)[source]#

Return the experiments of the reference catalogue: their data, where each was published, every printed number.

Parameters:
  • source (str, optional) – The key of one source (see reference_sources()); every source when None.

  • topic (str, optional) – The id of one subject-matter topic (see reference_topics()): only the experiments that list it, as their main topic or their second. Every topic when None.

Returns:

One dict per experiment, with keys name, title, topics, source, kind, where, factors, responses, design, runs (None when the source does not print them, and data_source then says where they come from), expected and notes. Each item of expected has what, term and value (the printed text), where (a file and line, or a page, with a quotation) and check. A response measured as a curve, not as one number, also has curve: its columns in the run table, in order along the curve, and its argument (name and units, and either values, the points every run shares, or columns, where each run records its own).

Return type:

list[dict]

Raises:

ValueError – If source names no source, or topic no topic.

Examples

>>> experiment = reference_experiments("ullmann-doe-2012")[0]
>>> experiment["name"], experiment["expected"][0]["value"]
('ullmann-catalyst-temperature', '10')
>>> [e["name"] for e in reference_experiments("montgomery-2013", topic="paper")]
['montgomery-problem-3-34', 'montgomery-problem-4-42', 'montgomery-problem-5-18']
process_improve.experiments.reference_catalogue.reference_designs(source=None, topic=None)[source]#

Return the designs of the reference catalogue: designs without responses whose properties a source states.

Parameters:
  • source (str, optional) – The key of one source; every source when None.

  • topic (str, optional) – The id of one subject-matter topic; every topic when None.

Returns:

One dict per design, as for reference_experiments() but without responses.

Return type:

list[dict]

Raises:

ValueError – If source names no source, or topic no topic.

process_improve.experiments.reference_catalogue.DISAGREEMENT_CAUSES: dict[str, str] = {'incorrect_option': 'An option of a quiz that its answer key marks incorrect, as the library finds it to be.', 'library': 'A bug or a limitation of the library.', 'method': 'The source uses another convention, definition, model or software than the library.', 'source_error': "The source misprints the number or works it out wrongly; the library's value follows from the source's own data.", 'source_rounding': 'The source works the number from values it has already rounded or truncated, or prints fewer digits than it carries.', 'unexplained': 'The cause is not known yet.'}#

Why a recomputed number differs from its source, as a disagreement’s cause records it. In the first two the library’s value is the right one: the source is in error, or the number is an option of a quiz that its answer key marks incorrect, as the library finds it to be.

process_improve.experiments.reference_catalogue.UNCHECKED_CAUSES: dict[str, str] = {'check_gap': 'The library computes it, but no check of the catalogue reads it yet.', 'data_not_printed': 'The source does not print the data the number is worked from.', 'library_gap': 'The library has no function for it yet.', 'not_a_result': 'Nothing to recompute: a statement in words, an input or a choice, a reading by eye, a unit conversion or an intermediate of a number that is checked.'}#

Why a printed number is not recomputed, as its check’s cause records it beside the reason (manual).

process_improve.experiments.reference_catalogue.reproduce_reference_catalogue(source=None)[source]#

Recompute every printed number of the reference catalogue with this library.

Each number with a check is recomputed: the experiment’s runs are fitted with the model the check names (on the runs it names), and the result compared with the printed text to within half a unit in its last printed digit. A design’s generators, resolution, defining relation and aliases are compared with the fraction this library makes, and a list of runs (those making a fraction, or their standard order) with the runs’ coded settings. A fitted quadratic’s stationary point and canonical form, and a setting on a path of steepest ascent, are recomputed with optimize_responses(), and a Box-Cox power with the library’s Box-Cox analysis, and a variance component with its REML fit of random intercepts. A run read several times can be analysed through each run’s mean or standard deviation of its readings, or with every reading an observation of its own; the setting of a factor that brings a fitted model to a target is solved from the library’s predictions, and a process capability index is the library’s Cpk. A term’s degrees of freedom, its share of the total sum of squares and Taguchi’s percent contribution are read from the library’s analysis of variance, and the split of a regression between two nested models from the fits of both. Two groups of runs are compared with the library’s two-sample and paired t-tests. The interactions a source confounds with blocks are compared with those of the library’s blocking of the same factorial, and with the products of the effects the source chose to generate the blocks. A test of lack of fit is also worked as the F test of the model against a fuller one, and the pseudo t-ratio of an unreplicated design’s effect on Lenth’s PSE. A fraction’s word lengths are read from the defining relation of the library’s fraction made from its generators; a printed column of signs is the product of the factor columns it is headed with; a Plackett-Burman design is matched to the library’s by its projections onto one, two and three columns; and a stepwise regression’s tests are nested tests, held to what the rounding of responses printed rounded carries. A 2^k factorial’s columns of Yates’s algorithm, run forwards or in reverse, and the checks on them are those of yates_table() and reverse_yates(). A 3^k design’s components of interaction, blocks and fractions are worked from their defining contrasts modulo 3; screening designs are compared on their main-effect and two-factor interaction columns; and JMP’s relative variances code a categorical factor as JMP does. A response surface design is compared on the determinant of (X’X)^-1, its trace and the prediction variance at a point, and on how far its runs lie from the centre, whether its blocks are orthogonal and whether its runs are fold-over pairs; several fitted responses are scored at a point by the library’s desirability functions, and a mixture is fitted in pseudocomponents. A printed alias chain can be held to every alias up to an order, and the effects confounded with a fraction’s blocks are read as alias chains. A response measured as a curve is analysed by FPCA, for the share of variance its components explain and each run’s scores. A number without a check is reported with the reason it is not recomputed.

Parameters:

source (str, optional) – The key of one source; every source when None.

Returns:

One row per check, with columns source, experiment, what, term, printed (the text compared), found (the value recomputed, as text), checked, agrees (None where not checked), reason (why a number is not recomputed, or the error raised recomputing it), cause (why a number is not recomputed, one of UNCHECKED_CAUSES, or why it differs, one of DISAGREEMENT_CAUSES), disagreement (for a number known not to reproduce, the explanation of why) and where.

Return type:

pd.DataFrame

Raises:

ValueError – If source names no source.

Examples

>>> report = reproduce_reference_catalogue("ullmann-doe-2012")
>>> int(report["checked"].sum()) > 50
True

The design catalog: published designs and designs this library generates, described for searching.

Each record describes one design by the facets a reader searches and filters on: its family, its size, the kinds of factors it has, which models it can estimate, whether it is blocked or run as a split plot, where and when it was published, what it is applied to (its topics), and a compact picture of its coded runs (the glyph). The published records are the experiments and designs of the reference catalogue (process_improve.experiments.reference_catalogue); the generated records are designs that generate_design() makes, one for each family and size listed in GENERATED. The browser app’s catalog page reads both, written once when the app is built.

Examples

>>> from process_improve.experiments.design_catalog import catalog_records, normalise_family
>>> normalise_family("2^(5-2) fractional factorial")
'fractional_factorial'
>>> record = next(r for r in catalog_records("generated") if r["id"] == "lib-ccd-k3-face-centered")
>>> record["n_runs"], record["models"]["quadratic"]
(17, True)
class process_improve.experiments.design_catalog.Family(label, year=None, reference='')[source]#

Bases: object

A family of designs: its name on the page, and the year and work that first published it.

Parameters:
  • label (str)

  • year (int | None)

  • reference (str)

process_improve.experiments.design_catalog.FAMILIES: dict[str, Family] = {'block_design': Family(label='Randomised blocks', year=None, reference=''), 'box_behnken': Family(label='Box-Behnken', year=1960, reference='Box, G. E. P. and Behnken, D. W. (1960), Some new three level designs for the study of quantitative variables, Technometrics'), 'ccd': Family(label='Central composite', year=1951, reference='Box, G. E. P. and Wilson, K. B. (1951), On the experimental attainment of optimum conditions, J. Royal Statistical Society B'), 'comparison': Family(label='Comparison or regression', year=None, reference=''), 'dsd': Family(label='Definitive screening', year=2011, reference='Jones, B. and Nachtsheim, C. J. (2011), A class of three-level designs for definitive screening in the presence of second-order effects, J. Quality Technology'), 'fractional_factorial': Family(label='Fractional factorial', year=1945, reference='Finney, D. J. (1945), The fractional replication of factorial arrangements, Annals of Eugenics'), 'full_factorial': Family(label='Full factorial', year=1926, reference='Fisher, R. A. (1926), The arrangement of field experiments, J. Ministry of Agriculture'), 'latin_square': Family(label='Latin square', year=None, reference=''), 'mixture': Family(label='Mixture', year=1958, reference='Scheffé, H. (1958), Experiments with mixtures, J. Royal Statistical Society B'), 'omars': Family(label='OMARS', year=2020, reference='Núñez Ares, J. and Goos, P. (2020), Enumeration and multicriteria selection of orthogonal minimally aliased response surface designs, Technometrics'), 'optimal': Family(label='Optimal (D, I, G)', year=1959, reference='Kiefer, J. (1959), Optimum experimental designs, J. Royal Statistical Society B'), 'orthogonal_array': Family(label='Orthogonal array (Taguchi)', year=1947, reference='Rao, C. R. (1947), Factorial experiments derivable from combinatorial arrangements of arrays, Supplement to the J. Royal Statistical Society'), 'other': Family(label='Other', year=None, reference=''), 'plackett_burman': Family(label='Plackett-Burman', year=1946, reference='Plackett, R. L. and Burman, J. P. (1946), The design of optimum multifactorial experiments, Biometrika'), 'split_plot': Family(label='Split plot', year=None, reference=''), 'supersaturated': Family(label='Supersaturated', year=1962, reference='Booth, K. H. V. and Cox, D. R. (1962), Some systematic supersaturated designs, Technometrics')}#

Every family a record can belong to. year and reference name the work that first published the family, where one work is generally credited with it.

process_improve.experiments.design_catalog.normalise_family(text)[source]#

Return the family a free-text design type names: a key of FAMILIES.

Parameters:

text (str) – A design type as a source describes it, such as "2^(5-2) fractional factorial" or "CCD".

Returns:

The family’s key; "other" when no rule recognises the text.

Return type:

str

Examples

>>> normalise_family("rotatable central composite design")
'ccd'
>>> normalise_family("Taguchi L18")
'orthogonal_array'
process_improve.experiments.design_catalog.GLYPH_ROWS = 96#

The most runs a glyph draws; a larger design is drawn from evenly spaced runs.

process_improve.experiments.design_catalog.glyph(coded)[source]#

Return the picture of a design’s coded runs that the catalog draws: one letter per setting, row by row.

A setting from -2 to +2 is one of 21 letters, "a" at -2 to "u" at +2, so -1, 0 and +1 are "f", "k" and "p"; a missing setting is ".". A design of more than GLYPH_ROWS runs is drawn from that many evenly spaced runs.

Parameters:

coded (np.ndarray) – The runs in coded units, one column per factor.

Returns:

The letters, as many per row as the design has factors.

Return type:

str

Examples

>>> import numpy as np
>>> glyph(np.array([[-1.0, 0.0], [1.0, np.nan]]))
'fkp.'
process_improve.experiments.design_catalog.design_facets(coded, kinds)[source]#

Return the facets read off a design’s coded runs: its size, its factors, its center runs and its models.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • kinds (Sequence[str]) – Each factor’s type: "numeric", "categorical" or "mixture".

Returns:

n_runs, n_factors, n_numeric, n_categorical, n_mixture, levels (the number of distinct settings of each factor), n_centre (runs with every numeric factor at 0), runs_per_parameter (runs per coefficient of the largest estimable model), models (see estimable_models()), glyph (see glyph()); and, for the largest model the design estimates, run_loss_model, survives_run_loss, critical_runs, worst_d_ratio and leverage (see run_loss()); and quadratic_projection and projectivity (see design_measures; None for a design with a categorical factor of three or more labels, whose coding makes it arbitrary).

Return type:

dict[str, Any]

process_improve.experiments.design_catalog.figure_name(note)[source]#

Return the name a reader can use for a figure, from the catalogue’s note of where it is printed.

The book’s entries record each figure as a note for the catalogue’s maintainers: its file, the lines of the source that print it, and its caption or alt text, in a few forms. The name is the first of these that the note holds: the figure’s caption; its alt text; a description in brackets after the file; its Sphinx label made readable (DOE-fig-Cube-plot is “Cube plot”); and, last, its file’s name made readable. A note that names no file and no label, such as “Figure 6.8”, is kept as it is, without a note of its lines. The name never holds a file path, a note of lines or a label’s prefix.

Parameters:

note (str) – A figure, as the figures of an entry’s where record it.

Returns:

The figure’s name, with a capital first letter and no full stop.

Return type:

str

Examples

>>> figure_name("The thirty batches in run order (../figures/doe/omars-worked-study-plan.png)")
'The thirty batches in run order'
>>> figure_name("../figures/doe/x.png (no caption; alt 'Histogram of the differences'), lines 244-248")
'Histogram of the differences'
>>> figure_name("DOE-fig-Cube-plot (factorial-two-levels-two-variables-no-analysis.png)")
'Cube plot'
>>> figure_name("factorial-two-level-surface-example-cropped.png (true response surface)")
'True response surface'
>>> figure_name("Figure 1(a)")
'Figure 1(a)'
process_improve.experiments.design_catalog.table_name(note)[source]#

Return the name a reader can use for a table, from the catalogue’s note of where it is printed.

The book’s entries record each table as a note for the catalogue’s maintainers: its caption, or its kind and columns when it has none, with the lines of the source that print it. The name keeps the caption and drops the note of lines and the column headings written after it; a table with no caption is named by its columns. A note that says no table is printed names none, and a table as a printed book numbers it, such as “Table 10.1”, is kept as it is.

Parameters:

note (str) – A table, as the tables of an entry’s where record it.

Returns:

The table’s name, with a capital first letter and no full stop; "" for a note of no table.

Return type:

str

Examples

>>> table_name("Factor ranges (list-table at line 46, no caption): Factor / Low / Centre / High")
'Factor ranges'
>>> table_name("Grid table, lines 1075-1093 (Experiment, D, R, P, Taste)")
'Table of Experiment, D, R, P and Taste'
>>> table_name("Recipe / True titer, g/L / Gain over current (list-table at line 538, no caption)")
'A table: Recipe / True titer, g/L / Gain over current'
>>> table_name("Table 10.1")
'Table 10.1'
process_improve.experiments.design_catalog.where_url(where)[source]#

Return the address of the place a source prints an entry: its page, and the section’s anchor on it.

A source read on the web records each entry’s page (where["url"]); a page of the book also records the Sphinx label of the section (where["label"]), which the page carries as an anchor.

Parameters:

where (dict, str or None) – An entry’s where, as the reference catalogue records it.

Returns:

The page’s address, with # and the section’s anchor when there is a label; None for a source that records no address, such as a printed book.

Return type:

str or None

Examples

>>> where_url({"url": "https://learnche.org/pid/a/b", "label": "DOE-omars-estimability-frontier"})
'https://learnche.org/pid/a/b#doe-omars-estimability-frontier'
>>> where_url({"pages": "307-324"}) is None
True
process_improve.experiments.design_catalog.shown_where(where)[source]#

Return the place a source prints an entry as the catalog shows it: section, pages, tables and figures.

Each table is named by table_name() and each figure by figure_name(); the notes kept for the catalogue’s maintainers (the source file, its lines and commit) are left out.

Parameters:

where (dict[str, Any] | str | None)

Return type:

dict[str, Any] | str | None

process_improve.experiments.design_catalog.published_runs(entry)[source]#

Return an entry’s runs in coded units, one column per factor; None when the source prints no runs.

Parameters:

entry (dict) – An experiment or design of the reference catalogue.

Returns:

The coded runs, in the order the source prints them, with NaN for a setting that is not printed as one.

Return type:

np.ndarray or None

process_improve.experiments.design_catalog.PLACES_NAMED = 3#

The most tables and figures the few words of _where() name; the others are counted.

process_improve.experiments.design_catalog.GENERATED: tuple[dict[str, Any], ...] = ({'design_type': 'full_factorial', 'k': 2, 'options': {'n_center_points': 0}}, {'design_type': 'full_factorial', 'k': 3, 'options': {'n_center_points': 0}}, {'design_type': 'full_factorial', 'k': 4, 'options': {'n_center_points': 0}}, {'design_type': 'full_factorial', 'k': 5, 'options': {'n_center_points': 0}}, {'design_type': 'full_factorial', 'k': 6, 'options': {'n_center_points': 0}}, {'design_type': 'full_factorial', 'k': 7, 'options': {'n_center_points': 0}}, {'design_type': 'fractional_factorial', 'k': 3, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 4, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 4, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 5, 'options': {'n_center_points': 0, 'resolution': 5}}, {'design_type': 'fractional_factorial', 'k': 5, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 5, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 6, 'options': {'n_center_points': 0, 'resolution': 5}}, {'design_type': 'fractional_factorial', 'k': 6, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 6, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 7, 'options': {'n_center_points': 0, 'resolution': 5}}, {'design_type': 'fractional_factorial', 'k': 7, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 7, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 8, 'options': {'n_center_points': 0, 'resolution': 5}}, {'design_type': 'fractional_factorial', 'k': 8, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 8, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 9, 'options': {'n_center_points': 0, 'resolution': 5}}, {'design_type': 'fractional_factorial', 'k': 9, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 9, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 10, 'options': {'n_center_points': 0, 'resolution': 5}}, {'design_type': 'fractional_factorial', 'k': 10, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 10, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'fractional_factorial', 'k': 11, 'options': {'n_center_points': 0, 'resolution': 5}}, {'design_type': 'fractional_factorial', 'k': 11, 'options': {'n_center_points': 0, 'resolution': 4}}, {'design_type': 'fractional_factorial', 'k': 11, 'options': {'n_center_points': 0, 'resolution': 3}}, {'design_type': 'plackett_burman', 'k': 2, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 3, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 4, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 5, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 6, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 7, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 8, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 9, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 10, 'options': {'n_center_points': 0}}, {'design_type': 'plackett_burman', 'k': 11, 'options': {'n_center_points': 0}}, {'design_type': 'dsd', 'k': 3, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 4, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 5, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 6, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 7, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 8, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 9, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 10, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 11, 'options': {'n_center_points': 1}}, {'design_type': 'dsd', 'k': 12, 'options': {'n_center_points': 1}}, {'design_type': 'omars', 'k': 3, 'options': {'n_center_points': 1}}, {'design_type': 'omars', 'k': 4, 'options': {'n_center_points': 1}}, {'design_type': 'omars', 'k': 5, 'options': {'n_center_points': 1}}, {'design_type': 'omars', 'k': 6, 'options': {'n_center_points': 1}}, {'design_type': 'omars', 'k': 7, 'options': {'n_center_points': 1}}, {'design_type': 'omars', 'k': 8, 'options': {'n_center_points': 1}}, {'design_type': 'box_behnken', 'k': 3, 'options': {'n_center_points': 3}}, {'design_type': 'box_behnken', 'k': 4, 'options': {'n_center_points': 3}}, {'design_type': 'box_behnken', 'k': 5, 'options': {'n_center_points': 3}}, {'design_type': 'box_behnken', 'k': 6, 'options': {'n_center_points': 3}}, {'design_type': 'box_behnken', 'k': 7, 'options': {'n_center_points': 3}}, {'design_type': 'ccd', 'k': 2, 'options': {'alpha': 'face_centered', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 2, 'options': {'alpha': 'rotatable', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 3, 'options': {'alpha': 'face_centered', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 3, 'options': {'alpha': 'rotatable', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 4, 'options': {'alpha': 'face_centered', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 4, 'options': {'alpha': 'rotatable', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 5, 'options': {'alpha': 'face_centered', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 5, 'options': {'alpha': 'rotatable', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 6, 'options': {'alpha': 'face_centered', 'cube': 'fractional', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 6, 'options': {'alpha': 'rotatable', 'cube': 'fractional', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 7, 'options': {'alpha': 'face_centered', 'cube': 'fractional', 'n_center_points': 3}}, {'design_type': 'ccd', 'k': 7, 'options': {'alpha': 'rotatable', 'cube': 'fractional', 'n_center_points': 3}}, {'design_type': 'd_optimal', 'k': 2, 'options': {'budget': 7, 'model_type': 'main_effects'}}, {'design_type': 'd_optimal', 'k': 3, 'options': {'budget': 8, 'model_type': 'main_effects'}}, {'design_type': 'd_optimal', 'k': 4, 'options': {'budget': 9, 'model_type': 'main_effects'}}, {'design_type': 'd_optimal', 'k': 5, 'options': {'budget': 10, 'model_type': 'main_effects'}}, {'design_type': 'd_optimal', 'k': 6, 'options': {'budget': 11, 'model_type': 'main_effects'}}, {'design_type': 'd_optimal', 'k': 7, 'options': {'budget': 12, 'model_type': 'main_effects'}}, {'design_type': 'd_optimal', 'k': 8, 'options': {'budget': 13, 'model_type': 'main_effects'}}, {'design_type': 'd_optimal', 'k': 2, 'options': {'budget': 8, 'model_type': 'interactions'}}, {'design_type': 'd_optimal', 'k': 3, 'options': {'budget': 11, 'model_type': 'interactions'}}, {'design_type': 'd_optimal', 'k': 4, 'options': {'budget': 15, 'model_type': 'interactions'}}, {'design_type': 'd_optimal', 'k': 5, 'options': {'budget': 20, 'model_type': 'interactions'}}, {'design_type': 'd_optimal', 'k': 6, 'options': {'budget': 26, 'model_type': 'interactions'}}, {'design_type': 'd_optimal', 'k': 2, 'options': {'budget': 10, 'model_type': 'quadratic'}}, {'design_type': 'd_optimal', 'k': 3, 'options': {'budget': 14, 'model_type': 'quadratic'}}, {'design_type': 'd_optimal', 'k': 4, 'options': {'budget': 19, 'model_type': 'quadratic'}}, {'design_type': 'd_optimal', 'k': 5, 'options': {'budget': 25, 'model_type': 'quadratic'}}, {'design_type': 'i_optimal', 'k': 2, 'options': {'budget': 10, 'model_type': 'quadratic'}}, {'design_type': 'i_optimal', 'k': 3, 'options': {'budget': 14, 'model_type': 'quadratic'}}, {'design_type': 'i_optimal', 'k': 4, 'options': {'budget': 19, 'model_type': 'quadratic'}}, {'design_type': 'supersaturated', 'k': 10, 'options': {}}, {'design_type': 'supersaturated', 'k': 14, 'options': {}}, {'design_type': 'supersaturated', 'k': 18, 'options': {}}, {'design_type': 'supersaturated', 'k': 22, 'options': {}}, {'design_type': 'mixture', 'k': 3, 'options': {'model_type': 'linear'}}, {'design_type': 'mixture', 'k': 4, 'options': {'model_type': 'linear'}}, {'design_type': 'mixture', 'k': 5, 'options': {'model_type': 'linear'}}, {'design_type': 'mixture', 'k': 3, 'options': {'model_type': 'quadratic'}}, {'design_type': 'mixture', 'k': 4, 'options': {'model_type': 'quadratic'}}, {'design_type': 'mixture', 'k': 5, 'options': {'model_type': 'quadratic'}}, {'design_type': 'mixture', 'k': 3, 'options': {'model_type': 'special_cubic'}}, {'design_type': 'mixture', 'k': 4, 'options': {'model_type': 'special_cubic'}}, {'design_type': 'mixture', 'k': 5, 'options': {'model_type': 'special_cubic'}}, {'design_type': 'mixture', 'k': 3, 'options': {'model_type': 'cubic'}}, {'design_type': 'mixture', 'k': 4, 'options': {'model_type': 'cubic'}}, {'design_type': 'mixture', 'k': 5, 'options': {'model_type': 'cubic'}}, {'array': 'L4(2^3)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L8(2^7)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L9(3^4)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L12(2^11)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L16(2^15)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L16(4^5)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L18(6^1 3^6)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L25(5^6)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L27(2^1 3^12)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L32(2^31)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L32(2^1 4^9)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L36(3^23)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L50(2^1 5^11)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L54(2^1 3^25)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L64(4^21)', 'design_type': 'taguchi', 'options': {}}, {'array': 'L81(3^40)', 'design_type': 'taguchi', 'options': {}})#

the shape the reference catalogue’s checks name a library design in. Orthogonal arrays are named by array instead of k.

Type:

Every design the catalog generates, as {"design_type", "k", "options"}

process_improve.experiments.design_catalog.INDEX_FIELDS = ('id', 'title', 'collection', 'kind', 'family', 'design_type', 'source', 'year', 'added', 'where', 'topics', 'n_runs', 'n_factors', 'n_numeric', 'n_categorical', 'n_mixture', 'levels', 'n_centre', 'runs_per_parameter', 'models', 'blocked', 'n_blocks', 'split_plot', 'has_responses', 'n_responses', 'checks', 'glyph', 'survives_run_loss', 'critical_runs', 'worst_d_ratio', 'quadratic_projection', 'projectivity', 'design_class')#

the facets it searches, filters, counts, maps and draws. The rest (factors, runs, notes) is read when one design is opened.

Type:

The fields of a record that the catalog page reads for every design at once

process_improve.experiments.design_catalog.printed_checks(entry)[source]#

Count an entry’s printed numbers: all of them, those recomputed, those that agree, those known not to.

reproduce_reference_catalogue() recomputes every number that carries a check, and the test suite requires each to agree with the printed text unless the catalogue records why it does not (its disagreement). So, on a commit whose tests pass, the counts here are what that report finds, without the minutes it takes to run.

Parameters:

entry (dict) – An experiment or design of the reference catalogue.

Returns:

printed (every number the source prints for it), checked (those recomputed), agree (those that reproduce) and explained (those known not to, with the reason).

Return type:

dict[str, int]

process_improve.experiments.design_catalog.catalog_records(collection='both')[source]#

Return the catalog: a record for each published design, each generated design, or both.

Parameters:

collection ({"both", "published", "generated"}) – Which designs to describe: the reference catalogue’s, the ones GENERATED lists, or both.

Returns:

One record per design. kind says what a published entry holds (see _kind()): an experiment (a design and its results), a design, a sample (results with no factor varied) or facts about designs (neither factors nor runs); a generated record is a design. An entry that names one two-level fraction by its generators alone has the fraction the library makes from them as its runs, and those generators in runs_from; it is None for every other record. The fields in INDEX_FIELDS describe it for searching; a published record’s checks counts its printed numbers (see printed_checks()) and a generated record’s is None. Records with the same design_class hold one design, printed in different sources or made by the library (see design_classes()). factors, responses, coded_runs, response_values, details, notes and where_full hold what a reader sees on opening a design, and where_url the address of the place its source prints it (see where_url()). blocks holds each run’s block as its source prints it, where the run table has them, and is None otherwise: a block is not one of the factors, nor a column of coded_runs. topics lists a published entry’s applications (see catalog_topics()); a generated design has none. listed says whether a published entry is one of reference_experiments() or of reference_designs(), and spec names a generated design as GENERATED does.

Return type:

list[dict]

Raises:

ValueError – If collection is none of the three.

process_improve.experiments.design_catalog.short_citation(cite)[source]#

Return the authors’ surnames of a citation, as a list of designs names its source: “Box, Hunter and Hunter”.

Examples

>>> short_citation("Núñez Ares, J. and Goos, P. (2020), Enumeration and multicriteria selection")
'Núñez Ares and Goos'
Parameters:

cite (str)

Return type:

str

process_improve.experiments.design_catalog.SOURCE_FORMS: dict[str, str] = {'book': 'Books', 'other': 'Handbooks, encyclopedias and courses', 'paper': 'Journal papers'}#

The forms a published source takes, in the order the reproduction page groups them, each with its name there.

process_improve.experiments.design_catalog.source_form(source)[source]#

Return the form of a published source, read from what the reference catalogue records of it.

A source that is a work of its own, with no work it is printed in, is a "book"; one printed in an issue of a journal is a "paper"; any other (an encyclopedia article, a chapter of a handbook, a course’s quizzes, tutorial slides) is "other". See SOURCE_FORMS.

Examples

>>> source_form(reference_sources()["box-hunter-hunter-1978"]), source_form(reference_sources()["lenth-1989"])
('book', 'paper')
Parameters:

source (dict[str, Any])

Return type:

str

process_improve.experiments.design_catalog.catalog_sources()[source]#

Return what the catalog shows of each published source: its citation, address, year and terms of use.

short names the source as the list of designs does: its authors’ surnames (see short_citation()).

Return type:

dict[str, dict[str, Any]]

process_improve.experiments.design_catalog.GENERAL_LABEL = 'General (no stated application)'#

What the catalog calls the topic of an entry whose source states no application.

process_improve.experiments.design_catalog.catalog_topics()[source]#

Return each application the catalog filters on: its name on the page, and what it covers.

The applications are the subject-matter topics of the reference catalogue (see reference_topics()), in its order: semiconductors, food and beverages, and so on. "general", the topic of an entry whose source states no application, is named GENERAL_LABEL.

Examples

>>> catalog_topics()["food-beverages"]["label"]
'Food and beverages'
Return type:

dict[str, dict[str, str]]

process_improve.experiments.design_catalog.catalog_families()[source]#

Return each family’s name on the page, what it is, the work that first published it, and where it can lead.

description says what a design of the family is in plain words, as the knowledge base of design types describes it to a newcomer (its description["novice"]), or "" where it has none. next lists the families a design of this one can be augmented to, each with how (how), as the knowledge base records them (its can_augment_to).

Return type:

dict[str, dict[str, Any]]

process_improve.experiments.design_catalog.catalog_errata()[source]#

List every printed number of the reference catalogue that the library does not reproduce, with the reason.

The test suite requires every recomputed number to agree with its source unless the catalogue records why it does not (its disagreement): the source’s rounding, a misprint, or a method that differs from the library’s. This lists each of those, in the catalogue’s order.

Returns:

One per number: id (the entry that prints it), source, what and term (the quantity), printed (the value as printed), where (its page, in a few words), url (its address, for a web source), quote (the source’s words around it), reason (why the library’s value differs) and rounding (whether the reason mentions rounding).

Return type:

list[dict]

Examples

>>> from process_improve.experiments.design_catalog import catalog_errata
>>> first = catalog_errata()[0]
>>> sorted(first)[:4]
['id', 'printed', 'quote', 'reason']
process_improve.experiments.design_catalog.REPRODUCTION_TIERS: dict[str, float] = {'not_reproduced': inf, 'reproduced': 0.0, 'within_1': 0.01, 'within_5': 0.05}#

How well the library reproduces a published case, best first, each with the largest relative difference from its source that it allows. reproduced: every recomputed number agrees to within the source’s rounding.

process_improve.experiments.design_catalog.LIBRARY_RIGHT = frozenset({'incorrect_option', 'source_error'})#

the source is in error, or the number is a quiz option that its answer key marks incorrect. A case’s tier leaves these out.

Type:

The causes of a difference in which the library’s value is the right one

process_improve.experiments.design_catalog.catalog_reproduction(source=None)[source]#

Say, for every case of the reference catalogue, how closely the library reproduces what its source prints.

A number agrees when it is within the source’s own rounding; the test suite requires that of every recomputed number unless the catalogue records why it does not (its disagreement) and the cause, one of DISAGREEMENT_CAUSES. Only those are recomputed here, to measure how far each one is from its source, so this takes a minute or two rather than the several minutes of reproduce_reference_catalogue(). A case falls in the best of REPRODUCTION_TIERS that holds every number of it that differs, leaving out those in which the library’s value is the right one (LIBRARY_RIGHT).

Parameters:

source (str, optional) – The key of one source; every source when None.

Returns:

One per experiment and design, in the catalogue’s order: id, title, source, kind (a worked example, an exercise, …), checked (how many of its printed numbers are recomputed), tier (None when none is), worst (the largest relative difference of the numbers the tier holds, None when one has no relative size, 0 when none differs), differs, unchecked and missing. differs lists each number that differs, with what, term, printed, found (the library’s value, as text), error (its relative difference, see worst), where (its place in the source, in a few words), reason (why it differs) and cause. unchecked counts the printed numbers not recomputed by their cause, one of UNCHECKED_CAUSES, and missing lists those the library has no function for yet ("library_gap"), each with reason.

Return type:

list[dict]

Examples

>>> cases = catalog_reproduction("ullmann-doe-2012")
>>> sorted({case["tier"] or "not recomputed" for case in cases})
['not recomputed', 'reproduced']
process_improve.experiments.design_catalog.catalog_feed(records)[source]#

Return an Atom feed of the catalog’s sources: one entry for each, by the date it entered the catalogue.

A reader who follows the feed learns when a source is added. Each entry links to the catalog filtered to the source’s designs, and says how many it holds and how many of their printed numbers the library recomputes. A source whose date of entry is not recorded is left out.

Parameters:

records (Sequence[dict]) – Records of the catalog (see catalog_records()); the published ones are counted by source.

Returns:

The feed, as an XML document, the newest source first.

Return type:

str

process_improve.experiments.design_catalog.COMPARED_FACTORS = 12#

The most factors, and the most runs, of a design the catalog scores for comparison.

process_improve.experiments.design_catalog.COMPARED_RUNS = 1000#

The most factors, and the most runs, of a design the catalog scores for comparison.

process_improve.experiments.design_catalog.catalog_comparisons(records)[source]#

Score each design of numeric factors on the measures of compare_designs, for each model, with its FDS curve.

A design is scored on its own: its values do not depend on which other designs are scored, so any designs with the same number of factors can be set side by side afterwards, as compare_designs() would set them. Designs with a categorical or mixture factor, a missing setting, more than COMPARED_FACTORS factors or more than COMPARED_RUNS runs are not scored.

Parameters:

records (Sequence[dict]) – Records of the catalog (see catalog_records()).

Returns:

For each scored design’s id, for each model in MODELS, the values compare_designs reports (the keys of MEASURES, and the model matrix’s rank), to four significant figures, and fds, the design’s fds_curve() for the model (None when the design cannot fit it).

Return type:

dict

Examples

>>> from process_improve.experiments.design_catalog import catalog_comparisons, catalog_records
>>> ccd = [r for r in catalog_records("generated") if r["id"] == "lib-ccd-k3-face-centered"]
>>> scored = catalog_comparisons(ccd)["lib-ccd-k3-face-centered"]
>>> scored["quadratic"]["fits"], scored["quadratic"]["residual_df"]
(True, 7)
process_improve.experiments.design_catalog.CATALOG_URL = 'https://factori.al/catalog/'#

a design’s page is this, then #d= and its id.

Type:

Where the catalog is published

class process_improve.experiments.design_catalog.CatalogQuery(min_factors=None, max_factors=None, min_runs=None, max_runs=None, model=None, min_categorical=None, max_categorical=None, families=None, topics=None, collection='both', has_results=False, survives_run_loss=False, min_quadratic_projection=None, min_projectivity=None, year_from=None, year_to=None, text='', sort='runs')[source]#

Bases: object

The filters of a search of the catalog, each one a design must pass, and the order of what passes.

Every filter left at its default keeps every design.

Parameters:
  • min_factors (int | None)

  • max_factors (int | None)

  • min_runs (int | None)

  • max_runs (int | None)

  • model (str | None)

  • min_categorical (int | None)

  • max_categorical (int | None)

  • families (tuple[str, ...] | None)

  • topics (tuple[str, ...] | None)

  • collection (Literal['published', 'generated', 'both'])

  • has_results (bool)

  • survives_run_loss (bool)

  • min_quadratic_projection (int | None)

  • min_projectivity (int | None)

  • year_from (int | None)

  • year_to (int | None)

  • text (str)

  • sort (Literal['runs', 'factors', 'newest'])

min_factors, max_factors

The fewest and most factors.

Type:

int or None

min_runs, max_runs

The fewest and most runs.

Type:

int or None

model#

A model the design must estimate in full.

Type:

{“main_effects”, “interactions”, “quadratic”} or None

min_categorical, max_categorical

The fewest and most categorical factors.

Type:

int or None

families#

The families to keep (keys of FAMILIES).

Type:

tuple of str or None

topics#

The applications to keep (keys of catalog_topics()): a design passes when it lists any of them. A generated design has no application, so any application drops them.

Type:

tuple of str or None

collection#

Published designs, those the library makes, or both.

Type:

{“both”, “published”, “generated”}

has_results#

Keep only published experiments printed with their results.

Type:

bool

survives_run_loss#

Keep only designs that still estimate their largest model after losing any one run.

Type:

bool

min_quadratic_projection#

Keep only designs whose every projection onto this many factors estimates the full quadratic.

Type:

int or None

min_projectivity#

Keep only two-level designs of at least this projectivity.

Type:

int or None

year_from, year_to

The years the source appeared in; a generated design has none, so either drops them.

Type:

int or None

text#

Words, each looked for in the design’s title, family, design type, source and applications.

Type:

str

sort#

The order: fewest runs, fewest factors, or the most recent source first.

Type:

{“runs”, “factors”, “newest”}

process_improve.experiments.design_catalog.search_catalog(query=None, **filters)[source]#

Return the catalog’s designs that pass every filter given, as the catalog page filters them.

Parameters:
  • query (CatalogQuery, optional) – The filters and the order; every design, the fewest runs first, when not given.

  • **filters – Fields of CatalogQuery, by name, such as min_factors=6: they replace the query’s.

Returns:

Each design’s facets (the fields of INDEX_FIELDS), with family_label, source_short (its source’s authors and year), where_url (the address of the place its source prints it, see where_url()), same_design (how many other designs of the catalog are this one) and url (its page in the catalog).

Return type:

list[dict]

Raises:

Examples

>>> found = search_catalog(min_factors=6, max_factors=6, max_runs=17, min_quadratic_projection=3)
>>> sorted({d["family"] for d in found})
['dsd', 'omars']

Measures read off a design’s coded runs: what it estimates, what losing a run costs, what its projections hold.

Each function takes a design as its runs in coded units (one row per run, one column per factor) and the kind of each factor ("numeric", "categorical" or "mixture"), and needs no fitted model and no responses: these are properties of the design, known before it is run.

  • estimable_models(): which of main effects, two-factor interactions and the full quadratic the design estimates in full.

  • run_loss(): whether the design still estimates its model if any one run is lost, and the D-efficiency left after the worst loss. Losing run i scales \(\det(X^\top X)\) by \(1 - h_i\), where \(h_i\) is the run’s leverage, so one hat matrix answers it for every run at once.

  • quadratic_projection() and projectivity(): the most factors such that every projection onto that many still estimates the full quadratic (any design), or still holds every combination of their two levels (a two-level design).

  • fingerprint() and same_design(): whether two designs are one design, with their runs in another order, their factors in another order, or a factor’s levels swapped.

  • fds_curve(): the fraction-of-design-space curve, the prediction variance that each fraction of the cube does not exceed.

Examples

>>> import numpy as np
>>> from process_improve.experiments.design_measures import run_loss
>>> square = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]], dtype=float)
>>> run_loss(square, ["numeric", "numeric"], "main_effects")["survives"]
True
>>> run_loss(square, ["numeric", "numeric"], "interactions")["survives"]
False
process_improve.experiments.design_measures.MODELS: tuple[Literal['main_effects', 'interactions', 'quadratic'], ...] = ('main_effects', 'interactions', 'quadratic')#

The models a design is described by, each including the ones before it, as the browser app names them.

process_improve.experiments.design_measures.MOST_SUBSETS = 20000#

past it, the measure stops and reports the largest size it checked in full, a lower bound.

Type:

The most factor subsets a projection measure checks for one projection size

process_improve.experiments.design_measures.MOST_STEPS = 200000#

past it, two designs are not called one.

Type:

The most steps relabelling() takes before giving up

process_improve.experiments.design_measures.FDS_SAMPLES = 4096#

The points drawn uniformly from the cube that a fraction-of-design-space curve is read from.

process_improve.experiments.design_measures.model_matrix(coded, kinds, model)[source]#

Return a design’s model matrix for a model in coded units, or None when the model does not apply.

The intercept, then each factor’s main effect (a categorical factor’s as one indicator column per level after its first), then, for "interactions" and "quadratic", the products of every pair of factors’ columns, then, for "quadratic", the square of every numeric factor.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • kinds (Sequence[str]) – Each factor’s type: "numeric", "categorical" or "mixture".

  • model ({"main_effects", "interactions", "quadratic"}) – The model.

Returns:

The model matrix; None for a mixture design, whose models are Scheffé’s, or when a setting is missing.

Return type:

np.ndarray or None

process_improve.experiments.design_measures.full_rank(matrix)[source]#

Say whether a model matrix estimates every one of its coefficients: as many runs as columns, and full rank.

Parameters:

matrix (ndarray)

Return type:

bool

process_improve.experiments.design_measures.estimable_models(coded, kinds)[source]#

Say which models a design estimates in full: main effects, two-factor interactions, the full quadratic.

Each model includes the ones before it (see model_matrix()). A model is estimable when its model matrix has full column rank.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • kinds (Sequence[str]) – Each factor’s type: "numeric", "categorical" or "mixture".

Returns:

For each name in MODELS, whether the design estimates it. Every value is None for a mixture design, whose models are Scheffé’s, or when a setting is missing. The interactions are None for a single factor; the quadratic is None when no factor is numeric, and False when a numeric factor has fewer than three levels.

Return type:

dict[str, bool | None]

Examples

>>> import numpy as np
>>> square = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]])
>>> estimable_models(square, ["numeric", "numeric"])
{'main_effects': True, 'interactions': True, 'quadratic': False}
process_improve.experiments.design_measures.leverages(matrix)[source]#

Return each run’s leverage, the diagonal of the hat matrix, for a model matrix of full column rank.

Parameters:

matrix (np.ndarray) – The model matrix, one row per run.

Returns:

\(h_i = x_i^\top (X^\top X)^{-1} x_i\) for each run, from 0 to 1; they add up to the number of columns.

Return type:

np.ndarray

process_improve.experiments.design_measures.run_loss(coded, kinds, model)[source]#

Say what losing any one run costs a design: whether it still estimates the model, and how well.

Deleting run i from the model matrix \(X\) scales \(\det(X^\top X)\) by \(1 - h_i\), where \(h_i\) is the run’s leverage. So the design still estimates the model after losing run i exactly when \(h_i < 1\), and keeps \((1 - h_i)^{1/p}\) of its D-efficiency, with p coefficients. A saturated design (as many runs as coefficients) has every \(h_i = 1\): no run can be lost.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • kinds (Sequence[str]) – Each factor’s type: "numeric", "categorical" or "mixture".

  • model ({"main_effects", "interactions", "quadratic"}) – The model the design is to estimate.

Returns:

survives (whether the model is estimable after losing any one run), critical_runs (how many runs cannot be lost), worst_d_ratio (the D-efficiency left after the worst loss, relative to the whole design’s), and leverage (each run’s). None when the design does not estimate the model, or the model does not apply (see model_matrix()).

Return type:

dict or None

Examples

>>> import numpy as np
>>> square = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1], [0, 0]], dtype=float)
>>> found = run_loss(square, ["numeric", "numeric"], "interactions")
>>> found["survives"], found["critical_runs"], found["leverage"]
(True, 0, [0.95, 0.95, 0.95, 0.95, 0.2])
>>> run_loss(square[:4], ["numeric", "numeric"], "interactions")["critical_runs"]
4
process_improve.experiments.design_measures.fds_curve(coded, kinds, model, *, n_points=21, random_state=0)[source]#

Return a design’s fraction-of-design-space curve: the prediction variance at each fraction of the cube.

The variance of the model’s prediction at a point \(\mathbf{x}\) of the cube \([-1, 1]^k\) is \(\mathbf{f}(\mathbf{x})^\top (X^\top X)^{-1} \mathbf{f}(\mathbf{x})\) in units of \(\sigma^2\), the run-to-run noise variance, where \(\mathbf{f}(\mathbf{x})\) is the model’s row at the point (see model_matrix()). The curve gives, for each fraction from 0 to 1, the variance that this fraction of the cube’s points does not exceed: its value at 0.5 is the median prediction variance over the cube, and at 1 the largest found. The variance is not scaled by the number of runs, so a design is read at its own size, as compare_designs() reads it.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • kinds (Sequence[str]) – Each factor’s type; the curve is drawn for designs whose factors are all "numeric".

  • model ({"main_effects", "interactions", "quadratic"}) – The model whose predictions are read.

  • n_points (int, default 21) – The fractions, spaced evenly from 0 to 1, at which the curve is given.

  • random_state (int, np.random.Generator or None, default 0) – Seeds the FDS_SAMPLES points drawn uniformly from the cube, so the same design gives the same curve.

Returns:

The prediction variance at each fraction, rising, to four significant figures; None when a factor is not numeric, a setting is missing, or the design does not estimate the model.

Return type:

list[float] or None

Examples

>>> import numpy as np
>>> square = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]], dtype=float)
>>> curve = fds_curve(square, ["numeric", "numeric"], "main_effects", n_points=3)
>>> curve[0], curve[-1] <= 0.75
(0.25, True)
process_improve.experiments.design_measures.quadratic_projection(coded, kinds, most=20000)[source]#

Return the most factors such that every projection onto that many estimates the full quadratic in them.

Projecting a design onto p of its factors keeps every run and drops the other factors’ columns. When only p factors turn out to matter, a design whose every p-factor projection estimates the full quadratic (intercept, main effects, two-factor interactions and squares in those p) can fit their response surface from the runs already made: the property definitive screening and OMARS designs are built for. Only the numeric factors are projected onto.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • kinds (Sequence[str]) – Each factor’s type: "numeric", "categorical" or "mixture".

  • most (int, default MOST_SUBSETS) – The most projections checked for one size. Past it the search stops, and the largest size checked in full is returned: a lower bound.

Returns:

The largest such p; 0 when a single factor has fewer than three levels. None for a mixture design, one with a missing setting, or one with no numeric factor.

Return type:

int or None

Examples

>>> import numpy as np
>>> from process_improve.experiments import Factor, generate_design
>>> factors = [Factor(name=n, low=-1, high=1) for n in "ABCDEF"]
>>> dsd = generate_design(factors, design_type="dsd", n_center_points=3)
>>> quadratic_projection(dsd.design[list("ABCDEF")].to_numpy(dtype=float), ["numeric"] * 6)
3
process_improve.experiments.design_measures.projectivity(coded, most=20000)[source]#

Return a two-level design’s projectivity: the most factors whose every projection holds all their combinations.

A design of projectivity P holds, for every choice of P factors, every one of the \(2^P\) combinations of their levels: a full factorial in any P factors, possibly replicated (Box and Tyssedal, 1996). A regular fraction of resolution R has projectivity R - 1. Center runs and any other run off the cube are set aside: the measure is of the design’s two-level runs.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • most (int, default MOST_SUBSETS) – The most projections checked for one size; past it, the largest size checked in full is returned, a lower bound.

Returns:

The projectivity; None when a setting is missing, or the runs on the cube are not a two-level design in every factor.

Return type:

int or None

Examples

>>> import numpy as np
>>> half = np.array([[-1, -1, 1], [1, -1, -1], [-1, 1, -1], [1, 1, 1]], dtype=float)
>>> projectivity(half)
2
process_improve.experiments.design_measures.run_counts(matrix)[source]#

Return how often each run occurs, its settings rounded to 6 decimals: a design’s runs, whatever their order.

Parameters:

matrix (ndarray)

Return type:

Counter

process_improve.experiments.design_measures.relabelling(first, second, *, signs=True, most=200000)[source]#

Return, for each column of first, the column of second and the sign that make their runs one.

A design keeps its properties when its runs or its columns are put in another order, or a column’s levels are swapped (its signs changed, in coded units). The search assigns the columns of first in turn, and drops an assignment as soon as the runs on the columns assigned so far differ. first may use only some of the columns of second, as factors are assigned to some columns of an orthogonal array and the others are left for interactions.

Parameters:
  • first (np.ndarray) – Two designs in coded units, with the same number of runs.

  • second (np.ndarray) – Two designs in coded units, with the same number of runs.

  • signs (bool, default True) – Whether a column’s signs may be changed.

  • most (int, default MOST_STEPS) – The most assignments tried; past it the search gives up and returns None.

Returns:

For each column of first, the column of second and the sign (1 or -1); None when no relabelling makes the runs one, or none was found within most steps.

Return type:

list of (int, int) or None

process_improve.experiments.design_measures.same_design(first, second)[source]#

Say whether two designs are one: the same runs, once runs, factors and each factor’s two signs are reordered.

Parameters:
  • first (np.ndarray) – Two designs in coded units.

  • second (np.ndarray) – Two designs in coded units.

Returns:

Whether a relabelling of second (see relabelling()) gives the runs of first, with the same number of factors.

Return type:

bool

process_improve.experiments.design_measures.fingerprint(coded)[source]#

Return a short hash of a design that does not change when its runs or factors are reordered or a sign swapped.

The hash is of the number of runs and factors, each factor’s levels (as their sizes, sorted), and, for every set of one to four factors, the size of the sum over the runs of the product of their settings (their J-characteristic), sorted; and the same sums of their squares, for one or two factors. Two designs that are one (see same_design()) have the same fingerprint; two with the same fingerprint are very likely one, and same_design() says for sure.

Parameters:

coded (np.ndarray) – The runs in coded units, one column per factor.

Returns:

Sixteen hexadecimal digits; None when a setting is missing.

Return type:

str or None

Examples

>>> import numpy as np
>>> square = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]], dtype=float)
>>> fingerprint(square) == fingerprint(-square[::-1, ::-1])
True
process_improve.experiments.design_measures.design_classes(designs)[source]#

Group designs that are one design (see same_design()): return each design’s class, by its name.

Designs are first grouped by their fingerprint(); within a group, each design joins the first class whose first member it is the same design as, or starts a class of its own.

Parameters:

designs (dict[str, np.ndarray]) – Each design’s coded runs, by its name. A design with a missing setting is left out.

Returns:

Each design’s class: its fingerprint, followed by -2, -3, … for a second or later class of designs that share a fingerprint but are not one.

Return type:

dict[str, str]

process_improve.experiments.design_measures.design_structure(coded, names=None, blocks=None)[source]#

Describe what a design’s runs are made of: levels, middle-level zeros, centre runs, mirror pairs, replicates.

These are the counts a three-level screening design is described by (Núñez Ares and Goos, 2020): how often each factor sits at its middle level, and how often each product of two three-level factors is zero, both counted over the runs that are not centre runs (n0ME and n0IE). A centre run has every three-level factor at 0; a two-level factor, which has no middle level, may take any value in it.

Parameters:
  • coded (np.ndarray) – The runs in coded units, one column per factor.

  • names (Sequence[str] or None) – The factors’ names; "x1", "x2", … by default.

  • blocks (np.ndarray or None) – Each run’s block label, to count the runs in each block and say whether every factor is balanced across the blocks (the same average level in every block, so the blocks are orthogonal to the main effects).

Returns:

  • n_runs, n_factors, n_distinct_runs;

  • levels, each factor’s distinct levels, and level_counts, by level (written as "-1", "0", "1"), how often each factor takes it;

  • n_two_level, n_three_level: how many factors have two and three levels;

  • n_center_runs: the runs with every three-level factor at 0;

  • zeros: main_effects, each three-level factor’s zeros, and interactions, each product of two three-level factors’ zeros, both over the runs that are not centre runs, with n0ME and n0IE, the value they all share (None when they differ), and zeros_with_center_runs, the same counted over every run;

  • mirror_pairs: how many pairs of the runs that are not centre runs are each other’s negative, and foldover, whether every such run is in one;

  • replicated: the groups of runs (0-based) with the same settings, each of two or more;

  • with blocks, block_sizes (runs per block) and blocks_balanced.

Return type:

dict[str, Any]

Examples

>>> import numpy as np
>>> dsd = np.array([[0, 1, 1], [0, -1, -1], [1, 0, -1], [-1, 0, 1], [1, 1, 0], [-1, -1, 0], [0, 0, 0]])
>>> found = design_structure(dsd)
>>> found["n_center_runs"], found["zeros"]["n0ME"], found["mirror_pairs"], found["foldover"]
(1, 2, 3, True)

Run Sheets#

Run sheets: a design written out for the lab, and the results read back in.

A run sheet is the table that leaves the analysis and comes back filled in: a spreadsheet for the people making the runs, the upload an online study asks for, a CSV from a laboratory system. run_sheet() writes one, in actual or coded units, under the factors’ own names or labels such as X01 to Xnn, with empty columns for the responses. read_run_sheet() reads one back into the factors’ own names and units, whichever units and labels it was written in.

process_improve.experiments.run_sheet.read_run_sheet(source, factors, *, labels=None, units='auto')[source]#

Read a run sheet back into the factors’ own names and actual units.

Parameters:
  • source (path, bytes, file object or DataFrame) – The sheet: a path to a .csv, .xlsx or .xls file, the bytes of an Excel file, a text or binary file object holding CSV, or a DataFrame. In a workbook, the first sheet that has a column for every factor is read.

  • factors (sequence of Factor) – The factors the sheet sets.

  • labels (mapping, "X" or None) – How the factors’ columns are named, as for run_sheet(). A factor’s column is also found under its own name, and matching ignores case, spaces and the leading zeros of an X label (x1 is X01).

  • units ({"auto", "actual", "coded"}, default "auto") – The units of the settings. "auto" reads them as coded when every setting is between -1 and +1 and at least one of them cannot be an actual value (it lies outside its factor’s range); otherwise as actual.

Returns:

A column per factor, under its name and in its actual units, then the sheet’s other columns (responses, run numbers) as they were. Rows with no setting at all are left out. attrs["units"] says which units the settings were read in, and attrs["columns"] maps each factor to the column it was read from.

Return type:

pandas.DataFrame

Raises:

ValueError – If the source cannot be read, a factor has no column, a setting is not a number (or not one of a categorical factor’s levels), or units is unknown.

Warns:

UserWarning – For a setting outside its factor’s range, which the runs did not keep to.

process_improve.experiments.run_sheet.run_sheet(runs, factors, *, units='actual', labels=None, responses=('y',))[source]#

Write runs as a run sheet: one row per run, in the order given, with empty response columns.

Parameters:
  • runs (DesignResult, DataFrame or list of dicts) – The runs in the factors’ actual units: a DesignResult (its design_actual, in its randomised run order), or a table with a column per factor, such as the runs that recommend_next_step() returns. A Block column is kept.

  • factors (sequence of Factor) – The factors, which give the ranges for coding and the order of the columns.

  • units ({"actual", "coded"}, default "actual") – The units the settings are written in. Coded units run from -1 at a factor’s low value to +1 at its high value; a categorical factor keeps its levels either way.

  • labels (mapping, "X" or None) – The column names: the factors’ own names (None), X01, X02, … in the order of factors ("X"; three digits from 100 factors), or a mapping from a factor’s name to its label, with unmapped factors under their own names.

  • responses (sequence of str, default ("y",)) – The response columns, written empty for the results.

Returns:

Columns Run (1, 2, …), Block when the runs have one, a column per factor and a column per response. Save it with to_csv or to_excel. attrs["columns"] maps each factor to its column.

Return type:

pandas.DataFrame

Raises:

ValueError – If a factor is missing from the runs, a label is given twice or clashes with another column, a response is named twice, or units is unknown.

Examples

>>> from process_improve.experiments import Factor, generate_design
>>> factors = [Factor(name="Temperature", low=60, high=100), Factor(name="Time", low=20, high=60)]
>>> sheet = run_sheet(generate_design(factors, "full_factorial"), factors, labels="X")
>>> list(sheet.columns)
['Run', 'X01', 'X02', 'y']

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

The Next Round#

Recommend the next round of a designed experiment from the results of the rounds so far.

A study is a sequence of rounds: screen the factors, resolve what the screen left open (an effect aliased with an interaction, curvature it could not model), move towards the optimum when it lies past the ranges studied, and confirm the predicted optimum. After every round the question is the same: given every run so far, what should the next round be? recommend_next_step() answers it with one action, the evidence for it, the runs to make, and how to judge them once they are in.

The decision is deterministic and follows the evidence in this order:

  1. Confirmation runs at the predicted optimum are judged by their mean against the prediction interval for a mean of that many runs. Confirmed, the study stops, unless the goal still improves past an edge of a range that may move. Missed, the next round adds what the design could not show (axial runs, a fold-over); else, when the model chosen again with the missed runs as data puts its optimum elsewhere, that optimum is confirmed; else a miss that may be chance is confirmed again, judged on the new runs alone, and a gross or repeated miss calls for a look at the process (investigate).

  2. A model that leaves fewer than two degrees of freedom for error cannot be judged: add centre runs to a two-level design of numeric factors, else replicate the runs.

  3. Nothing stands out from the noise: with fewer than four degrees of freedom for error, add runs first; else widen the ranges, or replicate when they cannot move; and when that was done before (history), stop: no factor moves the response by more than the noise.

  4. An active effect is aliased with a plausible interaction, so the data cannot say which of the two is real: fold the design over, or add runs chosen for the model that holds both.

  5. Curvature (centre runs off the plane of the corners, lack of fit, or a square in the model) that the design cannot model: add axial runs, which make a face-centred central composite design, or runs chosen for the full quadratic model. Curvature the centre runs show could be any factor’s, so the axial runs go in every numeric factor (up to six).

  6. A lack of fit that the full quadratic model in the active factors does not remove: the model misses effects. Every factor with some evidence of an effect joins the active ones, and runs are chosen for the full quadratic model in all of them.

  7. The best settings sit at an edge of a range and the goal improves past it by more than twice the noise: when the ranges may move, run the path of steepest ascent beyond it.

  8. Otherwise, confirm at the predicted optimum.

How the terms are judged matters as much as the rules. Each term is tested in the largest standard model the runs estimate with two degrees of freedom for error (one column per chain of exact aliases), or by Lenth’s method, with simulated critical values, on every contrast of a saturated two-level design, or by the staged analysis of a definitive screening design. Whether anything stands out at all is one test at alpha whose critical value is simulated for the design (_stands_out()), so pure noise raises a false alarm in one study in twenty, not in most of them. A model that does not fit the repeated runs has its terms tested against the repeats’ own variation instead of its residual, which holds what it misses. Interactions of the active factors (strong heredity) that the judging could not test are then tested in the model of the active terms.

A factor the selected model leaves out does not change the prediction, so any setting in its range will do. The runs recommended set it at the end its small, not significant, effect favours: a setting the study has run, and never a guess.

process_improve.experiments.next_step.ACTIONS = ('stop', 'confirm', 'add_center_points', 'replicate', 'widen_ranges', 'de_alias', 'augment_to_rsm', 'add_runs_for_model', 'move_region', 'investigate')#

The actions recommend_next_step() can recommend.

process_improve.experiments.next_step.recommend_next_step(factors, runs, response, *, goal='maximize', target=None, confirmation_runs=None, model_terms=None, budget=None, ranges_can_move=True, n_confirmation_runs=3, alpha=0.05, random_state=42, history=None, block=None)[source]#

Recommend the next round of a designed experiment from every run so far.

Parameters:
  • factors (list[Factor]) – The study’s factors: continuous, or categorical at two levels. Their low and high define the coded scale (-1 to +1) and the region the optimum is sought in.

  • runs (pandas.DataFrame) – Every run of the rounds so far, one row each, with a column per factor in its own units (a categorical factor’s level) and the response. Runs with no response yet are left out.

  • response (str) – The response column.

  • goal ({"maximize", "minimize", "target"}) – What the study wants of the response.

  • target (float or None) – The value aimed at, for goal="target".

  • confirmation_runs (pandas.DataFrame or None) – Runs made to confirm a predicted optimum, judged against the model the other runs give, with the same columns as runs.

  • model_terms (list[str] or None) – The model to use, as terms ("A", "A:B", "I(A ** 2)"), its terms tested in it; None judges every term the runs can test and keeps the active ones (see the module documentation).

  • budget (int or None) – Runs left. An action that needs more falls back to confirmation runs, or to stopping.

  • ranges_can_move (bool) – Whether the next round may set factors past their ranges: the path of steepest ascent, or wider ranges. False keeps every recommended run inside them.

  • n_confirmation_runs (int) – Runs made to confirm an optimum.

  • alpha (float) – Significance level of every test the decision reads.

  • random_state (int) – Seed for the search for the optimum and for runs chosen by exchange.

  • history (list[str] or None) – The actions of the earlier rounds of this study, oldest first, as this function returned them. With "widen_ranges" among them (or "replicate", when the ranges cannot move), a study in which nothing stands out stops rather than widen the ranges again.

  • block (str or None) – The column of runs that holds each run’s block: the round it was made in, say. Every model then holds a shift for each block, so a process that drifted between the rounds does not bias the effects or swell the noise. The blocks are taken in the order they first appear, and runs made next are predicted as the process was in the latest one. Confirmation runs need no block; when they miss and join the runs, they count in the latest block.

Returns:

action (one of ACTIONS), headline, reasons (the evidence, with its numbers), runs (the next round, in the factors’ own units, one dict per run), n_runs, after (how to judge those runs), model (the terms, coefficients and p-values of the model the decision read, in the factors’ names), evidence (active factors, curvature, lack of fit, aliasing, the optimum with its prediction and the edges it sits on, the confirmation runs’ verdict), alternatives (other actions that apply, with their run counts) and, for some actions, new_ranges (widen_ranges), or path_predictions and direction, each moving factor’s step in coded units per unit of the factor that moves most (move_region).

Return type:

dict[str, Any]

Raises:

ValueError – For a mixture factor, a categorical factor not at two levels, a factor or response missing from the runs, a level not among a factor’s levels, a target goal without a target, an action in history not among ACTIONS, a block that is not a column of the runs, or fewer than two runs with a response.

Examples

>>> import pandas as pd
>>> from process_improve.experiments import Factor, TrueProcess, generate_design
>>> factors = [Factor(name=n, low=-1, high=1) for n in "ABCDEFGH"]
>>> design = generate_design(factors, design_type="dsd", n_center_points=5).design_actual
>>> truth = TrueProcess({"Intercept": 100, "A": 10, "C": 7, "G": -6, "A:C": 8}, sigma=3)
>>> runs = design.assign(y=truth.simulate(design.drop(columns="RunOrder"), random_state=1))
>>> step = recommend_next_step(factors, runs, "y", ranges_can_move=False)
>>> step["action"], step["evidence"]["active_terms"]
('confirm', ['A', 'C', 'G', 'A:C'])

Simulated Studies#

How a design would do on a process you can only hypothesise: simulated studies, judged as the next step judges them.

compare_designs() and evaluate_design() score a design for one fixed model, analytically. Two things they cannot see decide how a real study goes: terms outside that model (an interaction a screening design was not built to estimate) bias the effects inside it, and the analysis has to choose the model from the same noisy runs. simulate_design_performance() sees both, by making each design many times on a TrueProcess and judging every simulated study the way recommend_next_step() judges real ones.

process_improve.experiments.simulate.simulate_design_performance(designs, truth, *, goal='learn', target=None, n_simulations=200, alpha=0.05, random_state=42)[source]#

Simulate studies with each design on a hypothesised process, and report what their analysis finds.

Each design is made n_simulations times on truth, with fresh noise each time, and each simulated study is judged as recommend_next_step() judges real runs: the largest standard model the runs estimate, the staged analysis of a definitive screening design, Lenth’s method for a saturated two-level design, or a stepwise search (AICc) when the second-order terms are partly aliased; and a gate on whether anything stands out from the noise at all. So the numbers include what an analytic power calculation leaves out: the bias that terms outside the model put on the terms inside it, and the chance that the analysis picks the wrong model.

A true term counts as found when the analysis finds it, or a term that the design makes identical to it (an exact alias, such as D for A:B in a resolution III fraction, or the curvature at the centre runs for a square). Every other term found is a false alarm.

Parameters:
  • designs (mapping of str to design, sequence of DesignResult, or one design) – The designs in coded units, as compare_designs() takes them: a DesignResult, a DataFrame of factor columns (RunOrder and Block are ignored) or an array, under a label. One design alone is also taken. Every design has the same factors.

  • truth (TrueProcess) – The process the runs are made on: its terms in coded units, and the noise. A factor no term names is inert. Its terms are the ones a design should find.

  • goal ({"learn", "maximize", "minimize", "target"}, default "learn") – With a goal other than "learn", each study also predicts its best settings, as the next step does before confirming, and the report says how far the true response there falls short of the true best inside the ranges.

  • target (float or None) – The response wanted, for goal="target".

  • n_simulations (int, default 200) – Simulated studies per design. A share from 200 studies is within about 0.07 of its limit (95%), at worst.

  • alpha (float, default 0.05) – The level of every test of the judging.

  • random_state (int, numpy.random.Generator or None, default 42) – Seed for the noise. Each design gets its own stream, drawn in the order the designs are given, so the same arguments give the same report.

Returns:

truth (the terms and sigma), goal, n_simulations, alpha, precision (the widest 95% margin of a share), true_optimum toward a goal (the coded settings and the true response there), and designs: for each design label (with its size, as "DSD [n=21]"), a dict with

  • n_runs;

  • found: each true term’s share of studies that find it;

  • found_all: the share that find every true term;

  • found_exactly: the share that find every true term and nothing else;

  • false_alarms: the mean number of terms found that are not true, and false_alarm_terms, the most frequent of them with their shares;

  • nothing_stood_out: the share in which nothing stood out from the noise;

  • judged_by: how the studies were judged, with the share judged each way;

  • toward a goal, shortfall: how far the true response at each study’s predicted best settings falls short of the true best (mean, median, p90, worst), in the response’s units and, as mean_in_sigma, in noise standard deviations;

  • summary: one sentence with the main numbers.

Return type:

dict[str, Any]

Raises:
  • ValueError – If a design is not a table of finite numbers, the designs differ in their factors, the truth uses a factor the designs do not have, goal is unknown or a target goal has no target, n_simulations is below 1, or alpha is not between 0 and 1.

  • TypeError – If truth is not a TrueProcess.

Examples

>>> from process_improve.experiments import Factor, TrueProcess, generate_design
>>> factors = [Factor(name=n, low=-1, high=1) for n in "ABCDEFGH"]
>>> truth = TrueProcess({"Intercept": 100, "A": 10, "C": 7, "G": -6, "A:C": 8}, sigma=3)
>>> report = simulate_design_performance(
...     {"DSD": generate_design(factors, "dsd", n_center_points=5)}, truth, n_simulations=50
... )
>>> report["designs"]["DSD [n=21]"]["found_all"]
1.0

Pydantic models for the DOE strategy recommender.

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

process_improve.experiments.strategy.models.NON_GENERATE_DESIGN_IDS: dict[str, str] = {'ccd_face_centered': "A domain template's RSM preference for design_type='ccd' with alpha='face_centered'; stages carry the generate_design form.", 'replicates_at_optimum': 'The confirmation stage: replicate runs at the optimum that optimize_responses finds, not a design built from the factor ranges.'}#

Ids a strategy can name that are not generate_design design types, and what each one means.

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>, conditional=False, run_if='')[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) – A generate_design design type, e.g. "plackett_burman", "ccd", "box_behnken", so generate_design(factors, design_type=stage.design_type, **stage.design_params) builds the stage. The one exception is the confirmation stage, "replicates_at_optimum": its runs are replicates at the optimum that optimize_responses finds, not a design built from the ranges.

  • design_params (dict) – generate_design keyword arguments (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.

  • conditional (bool) – Whether the stage is run only when the stage before shows it is needed. An optimisation stage after a screening design that can test for curvature is conditional: when the screening finds none, the study goes straight to confirmation and these runs are never made.

  • run_if (str) – For a conditional stage, the evidence that calls for it and the analysis that gives that evidence; empty otherwise.

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]#
conditional: bool#
run_if: str#
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, minimum_total_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, the conditional ones included: the most the plan can need.

  • minimum_total_runs (int) – Sum of estimated runs across the stages that are not conditional: what the plan needs when the screening shows no conditional stage is called for.

  • 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#
minimum_total_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", "dsd" (or "definitive_screening"), "fractional_factorial", "full_factorial", "supersaturated". Any other value gets the Plackett-Burman estimate.

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. For a CCD it is the size generate_design builds with cube="full" up to five factors and cube="fractional" (a half-fraction cube) from six, which is what recommend_strategy passes.

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