Designed Experiments#
- class process_improve.experiments.structures.Column(data=None, index=None, dtype=None, name=None, copy=None)[source]#
Bases:
SeriesCreate a column. Can be used as a factor, or a response vector.
- Parameters:
dtype (Dtype | None)
copy (bool | None)
- class process_improve.experiments.structures.Expt(data=None, index=None, columns=None, dtype=None, copy=None)[source]#
Bases:
DataFrameDataframe carrying experimental data plus process-improve metadata.
Expt(short for “Experiment”) is apandas.DataFramesubclass that adds library-managed metadata fields prefixed withpi_– 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 datasetpi_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; seeexpt()/create_names().The
pi_prefix is documented inCONTRIBUTING.mdand 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)
- 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"]
- 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 (whenlevels=...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:
- process_improve.experiments.structures.expand_grid(**kwargs)[source]#
Create the expanded grid here.
- process_improve.experiments.structures.supplement(x, **kwargs)[source]#
Supplement an existing column with additional metadata (name, units, lo, hi, etc.).
- 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 aTypeErrorthat names it (#677); it was dropped without a word before.Positional arguments are accepted for columns that already carry a name (
pi_nameor 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.)
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
Factorhas 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-conditionedX'X),"mixture","taguchi","supersaturated", and the space-filling types"latin_hypercube","maximin_lhs","uniform","sobol","halton","maximin"and"maxpro"(budgetruns, default10 * k). IfNone, 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
UserWarningwhen 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) raisesValueErrorwhen 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 of2k + 1or2k + 2runs gives Jones and Nachtsheim’s (2011) design of2k + 1runs, 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, soNonegives 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, sinceanalyze_omars()could not test its effects; extra centre runs fix that. Any other design type takes no centre points, so a positive value raisesValueError. 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=3andn_replicates=2has 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
Blockcolumn 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_blocks2, 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), andmetadata["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 raiseValueError. Any other design type raisesValueError.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 threelevels, 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 (seedesigns_three_level). Any other design type raisesValueError.alpha (str, float, or None) – Axial distance for CCD designs:
"rotatable","face_centered","inscribed","orthogonal"(the default), or a positive number. Any other value raisesValueError.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 ofConstraintfields 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 setmetadata["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 raisesValueError; 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 andbudgetcounts them, sobudgetmust exceedlen(fixed_runs). A common use is to seed a centre point. RaisesValueErrorif given for a non-optimaldesign_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).
Nonedraws 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 ofsplit_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(codedExpt),design_actual(actual-unitsExpt),run_order, and design metadata (generators, defining relation, resolution, etc.).generators,defining_relationandresolutiondescribe the design built, and areNonefor a design without a defining relation.- Return type:
- 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.Factor(*, name, type=FactorType.continuous, low=None, high=None, levels=None, units='', hard_limits=False)[source]#
Bases:
BaseModelSpecification 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
lowandhighare 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 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
nameattribute ofdata(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:
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:
BaseModelA 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 thenamefields of the correspondingFactorobjects.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
Constraintobjects, their dict form or bare expressions.The dict form is what a JSON round trip of a
Constraintgives, for example thedesign_paramsof a stage fromrecommend_strategy.- Parameters:
constraints (Sequence[Constraint | dict | str] or None) – The constraints in any of the three forms, mixed freely.
- Returns:
One
Constraintper item, in order;NonewhenconstraintsisNone.- 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]#
-
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:
BaseModelSpecification 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:
objectContainer 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.
designanddesign_actualare already in randomised run order, sorun_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.
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.
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 intofracfactgenerator 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, anddefining_relation()lists it.- Return type:
- 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:
- 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_factorsfactors.The smallest multiple of 4 above
n_factorsfor 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.
- process_improve.experiments.designs_screening.dispatch_plackett_burman(factors)[source]#
Generate a Plackett-Burman screening design.
The
N-run design iskcolumns of a normalised Hadamard matrix of orderN, the smallest multiple of 4 abovekthat can be built (seeplackett_burman_runs()). WhereN - 1is 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 inprocess_improve.experiments._finite_fields, checked againstH'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. AUserWarningnames both run counts whenNis above the smallest multiple of 4 abovekbecause that order is not built here.
- 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
levelsfor 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 ofL18(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_arrayand the arraycolumnsused, a column named twice when two factors share it.- Return type:
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:
n_center_points (int) – Number of center points (split between cube and axial portions).
alpha (str, float, or None) – Axial distance:
"rotatable"(F ** 0.25forFcube 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, seeorthogonal_alpha()), or a positive number used as the distance itself. Any other value raisesValueError.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_estimableandaliased_interactionsin the metadata say what it can estimate.generators (list[str] or None) – Explicit cube generators (e.g.
["E=ABCD"]), used only whencube="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 asalpha_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"); andalpha_rule, how alpha was chosen ("orthogonal","rotatable","face_centered","inscribed","orthogonal_blocks"or"user"); andaxial_portion, which rows are the axial runs and their centre runs.- Return type:
- 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’scenterparameter). The caller should setn_center_points=0inbuild_design_resultto 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)forFcube runs, each axial point runstimes, andNruns in all, centre runs included (Box and Hunter 1957; Myers, Montgomery and Anderson-Cook, Response Surface Methodology, section 7.4, fors = 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 sumF + 2 s alpha**2; the centred columns are orthogonal whenF N = (F + 2 s alpha**2) ** 2. A design replicated whole (F,sandNall timesr) keeps its alpha; one whose centre runs are not replicated has relatively fewer of them, and a smaller alpha.- Parameters:
- Returns:
The axial distance in coded units.
- Return type:
- 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.25forFcube 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
Nruns in all, of whichr (F + 2k)are the cube and axial runs (the design runrtimes, its centre runs once) forkfactors (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:
- Returns:
The number of centre runs, rounded to the nearest whole run and at least 1.
- Return type:
- 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))forFcube runs andc_ccentre runs in the cube blocks,kfactors whose2kaxial points are each runstimes, andc_acentre 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 issqrt(2), rotatable too.- Parameters:
- Returns:
The axial distance in coded units.
- Return type:
- 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, andmetadata["construction"]says"all_pairs".- Parameters:
- Returns:
Coded design matrix (-1 / 0 / +1) and metadata with the
construction.- Return type:
Notes
Center points are embedded in the BB structure. The caller should set
n_center_points=0inbuild_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_factorsfactors is built from.n_factorsitself when it is even andn_factors + 1when 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:
- 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_factorsfactors.2m + 1for the conference ordermofdsd_conference_order(): the familiar2k + 1for evenkand2k + 3for oddk, except where the order steps up. Two-level categorical factors (counted inn_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
- 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
Ca conference matrix of orderm(C'C = (m - 1) I), the design is[C; -C; 0]with2m + 1runs, and its firstkcolumns are the factors.miskfor evenkandk + 1for oddk, stepped up where no conference matrix of that order can be built (seedsd_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 powerq = m - 1(including 9, 25, 27, 49) and the doubling of an antisymmetric matrix (orders 16, 40, 56, …). Every matrix is checked againstC'C = (m - 1) Ibefore 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
+zand-z, and the single centre run becomes two, with the categorical factors at+band-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+1in 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 forccategorical factors,n_B = c // 4.A
budgetlarger 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
budgetof2k + 1or2k + 2runs for an oddkof 5, 7, 9 or 11 continuous factors, too small for the conference-matrix design, gives Jones and Nachtsheim’s (2011) design of2k + 1runs 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’snotesays so.- Parameters:
factors (list[Factor]) – At least three factors: continuous, or categorical with exactly two levels.
budget (int or None) – Largest number of runs.
Nonegives 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_factorsand, with categorical factors,n_categoricalandcategorical_method. Without abudget, when the order steps up (21, 22, 33 or 34 factors), anotesays so and aUserWarninggives the same text.- Return type:
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 forhard_to_changefactors.pyoptexis not a process-improve extra because it pinsplotly~=5.24(< 6), which conflicts with this project’splotly>=6.5.2; install it separately (pip install pyoptex) in its own environment.The built-in candidate exchange in
designs_constrained.pyotherwise: 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 withfixed_runsto enough runs beyond them to estimate the model. A budget below the model’s number of coefficients raises aValueError, and so does one that leaves too few runs for the block effects (n_blocks) or for the rankfixed_runslack.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 withbackend="pyoptex". A name that is not a factor raisesValueError. Ignored, with a warning, and recorded ashard_to_change_ignored, when the design is built by the candidate exchange: with constraints, a candidate set, fixed runs (unlessbackend="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 inmetadata["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, bygenerate_design().
- Returns:
The coded design and its metadata;
metadata["backend"]names the engine ("split_plot_exchange","candidate_exchange"or"pyoptex").- Return type:
- 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 withfixed_runsto enough runs beyond them to estimate the model. A budget below the model’s number of coefficients raises aValueError, and so does one that leaves too few runs for the block effects (n_blocks) or for the rankfixed_runslack.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 withbackend="pyoptex". A name that is not a factor raisesValueError. Ignored, with a warning, and recorded ashard_to_change_ignored, when the design is built by the candidate exchange: with constraints, a candidate set, fixed runs (unlessbackend="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 inmetadata["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, bygenerate_design().
- Returns:
The coded design and its metadata;
metadata["backend"]names the engine ("split_plot_exchange","candidate_exchange"or"pyoptex").- Return type:
- 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 withfixed_runsto enough runs beyond them to estimate the model. A budget below the model’s number of coefficients raises aValueError, and so does one that leaves too few runs for the block effects (n_blocks) or for the rankfixed_runslack.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 withbackend="pyoptex". A name that is not a factor raisesValueError. Ignored, with a warning, and recorded ashard_to_change_ignored, when the design is built by the candidate exchange: with constraints, a candidate set, fixed runs (unlessbackend="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 inmetadata["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, bygenerate_design().
- Returns:
The coded design and its metadata;
metadata["backend"]names the engine ("split_plot_exchange","candidate_exchange"or"pyoptex").- Return type:
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,
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:
objectHow a split-plot design is searched for.
- Parameters:
- model_type#
"main_effects","interactions"or"quadratic". A categorical factor has no square, so"quadratic"squares the continuous factors only.- Type:
- 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:
- 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_informationfor D,trace_criterion(\(\operatorname{tr}(M^{-1})\) or \(\operatorname{tr}(M^{-1} W)\)) for A and I.- Return type:
- 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:
objectThe 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.
- 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:
objectHow a two-way split-plot design is searched for.
- Parameters:
- row_variance_ratio, column_variance_ratio
\(\eta_r\) and \(\eta_c\): the row and the column variance over the run-to-run variance.
- Type:
- 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,rowandcolumn(each run’s),row_variance_ratio,column_variance_ratio,n_starts, andlog_det_informationfor D ortrace_criterionfor A and I, on \(M = X^T V^{-1} X\).- Return type:
- 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:
objectHow a design on units with covariates is searched for.
- model_type#
The factors’ terms:
"main_effects","interactions"or"quadratic". The covariates enter by their main effects.- Type:
- 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, andlog_det_informationfor D ortrace_criterionfor A and I. The I criterion averages over the factor box and over the candidate units.- Return type:
- Raises:
ValueError – If the criterion is not one of
CRITERIA, there are no units,n_runsexceeds 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/highare 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:
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
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:
- Raises:
ValueError – If a bound is negative or not a finite number, the bounds leave no room (\(\sum_j L_j \ge T\)),
lowerdoes not match the components, or a blend has a value that is not a finite number.TypeError – If
lowernames the components butblendsis 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:
pseudocomponents (pandas.DataFrame, numpy.ndarray or Sequence) – The pseudocomponents, a component per column (a single blend may be one sequence).
lower (Mapping[str, float] or Sequence[float]) – The lower bound of each component, as
to_pseudocomponents()takes them.total (float) – The mixture’s total, \(T\).
- Returns:
The blends, in the shape and type of
pseudocomponents.- Return type:
- Raises:
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:
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.
Model matrix. Each candidate is expanded into the columns of the model the design is for (intercept, main effects, two-factor interactions, squares).
Exchange. A Fedorov exchange swaps design runs for candidates while the criterion improves (see
Criterion: the determinant of the information matrixX'Xfor 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)^-1kept 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.
- 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 isast.unparseof the renamed expression, sorename_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", asparse_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:
- Raises:
ValueError – If
expressionis not a constraintparse_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;
Nonepicks 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:
- 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
Llevels contributesL - 1effect-coded (sum-to-zero) columns, the codingevaluate_designuses (seecategorical_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 oneevaluate_designgives 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.
- class process_improve.experiments.designs_constrained.Criterion(name, weights=None, region_rows=None, segment=None)[source]#
Bases:
objectThe optimality criterion an exchange maximises.
"d_optimal"maximiseslog det(M), withM = X'X."a_optimal"and"i_optimal"minimisetrace(M^-1 W): withW = Ithat is the summed variance of the coefficients (A), and withWthe average off(x) f(x)'over the region it is the average prediction variance over the region (I), which is whatevaluate_designreports as I-efficiency."e_optimal"maximises the smallest eigenvalue ofM, which bounds the variance of the worst-estimated linear combination of the coefficients.Use
d(),a(),i()ore()to build one.- Parameters:
- classmethod i(region_rows)[source]#
I-optimality, with the moment matrix estimated from model rows sampled uniformly in the region.
- classmethod g(region_rows)[source]#
G-optimality: the largest prediction variance over the model rows
region_rows.
- 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_columnsblock columns, andsegmentgives 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), whereSis the factor information adjusted for the blocks (the Schur complement of the block partZ'ZofM), so D maximisesdet(S). The trace criteria give the block columns no weight, andtrace(M^-1 W) = trace(S^-1 W). Only D, A and I are blocked this way.
- 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).
- property n_phases: int#
E-optimality climbs
phi_pfirst, then polisheslambda_min.- Type:
Exchange phases
- best_swap(info, f_design, f_cand, phase=0)[source]#
Return
(i, j, gain)for the best single swap: design rowiout, candidatejin.
- swap_gains(m_inv, f_design, f_cand)[source]#
Return the gain of every swap (design run
iout, candidatejin), shape(n_design, n_cand).With
A = M^-1andd(a, b) = f(a)' A f(b), D-optimality multiplies the determinant by1 + d(j) - d(i) - [d(i) d(j) - d(i, j)**2](Fedorov 1972). For the trace criteria, two Sherman-Morrison updates (addj, then removei) withb(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.
- process_improve.experiments.designs_constrained.criterion_metadata(criterion, value)[source]#
Report the criterion value under a name that says what it is.
- process_improve.experiments.designs_constrained.fedorov_exchange(f_cand, n_free, f_fixed, rng, criterion=None)[source]#
Choose
n_freecandidate rows that maximisecriterion(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 ofCriterion.swap_gains()(Fedorov 1972). E and K make the best single swap over all pairs at each step. Rows inf_fixedstay 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_STARTSfor E and K, and up to_MAX_STARTSfor a small D, A or I problem (see_n_starts()).
- class process_improve.experiments.designs_constrained.PolishLattice(n_levels, model_rows, feasible=None)[source]#
Bases:
objectWhere
polish_runs()may move a run.- Parameters:
n_levels (int) – Each continuous coordinate (coded) may move to one of
n_levelsevenly 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_LEVELSlevels, 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).
- 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 improvescriterion; passes repeat until no move helps. The gains are those ofCriterion.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:
- class process_improve.experiments.designs_constrained.CandidatePool(coded, cats, rows, lattice=None)[source]#
Bases:
objectCandidate 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_freeruns byfedorov_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:
- Parameters:
pool (CandidatePool)
n_free (int)
f_fixed (ndarray)
rng (Generator)
criterion (Criterion)
- 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:
objectOptional 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.
Nonepicks 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 withinmax(5000, 200 p)points forpmodel coefficients, and samples the coarsest grid down to that when none does. A sampled or coarse grid is then polished (seepolish_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()), andmetadata["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 - 1of them, the last block -1 in each.analyze_experimentcodes blocks this way, so a criterion over these columns is the one the analysis estimates with, and the intercept is the average block.
- process_improve.experiments.designs_constrained.selection_counts(labels, rows)[source]#
Count how often each supplied candidate (by index label) was chosen.
- 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
budgetminus 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
Criterioncalledname.region_rows(model rows drawn uniformly in the region) is only evaluated for I- and G-optimality. G takes its maximum over those rows pluscandidate_rows, which hold the vertices and boundary points where the worst case usually sits.
- 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_runsare 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 aValueError.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:
- Raises:
ValueError – If
model_typeis 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
factorscut byconstraints, their rules included.A constraint that names a categorical factor, or joins comparisons with
and,orornot, is read bycompile_rules(); every other one byparse_constraint(), as before.- Parameters:
constraints (list[Constraint])
- Return type:
_Region
- process_improve.experiments.designs_constrained.independent_columns(matrix)[source]#
Return the columns of
matrixkept, 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_TOLof its own length.
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:
objectFactors 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):
Constraintobjects, their dict form (asto_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 withand,orandnot, 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
- to_dict()[source]#
Return a JSON-friendly description, as stored in
DesignResult.metadata["region"].
- 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 (
Constraintdicts asto_dict()writes them, or bare expressions), and a missing orNoneentry means no constraints.
- property inequalities: list[Callable[[ndarray], ndarray]]#
Functions
g(x)of design-unit points(n, k); a point is feasible where everyg <= 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
tolof the boundary passes and one further out fails, however the limit is scaled and whatever its units:X <= 0.5e-9on a range of1e-9is judged asX <= 0.5on a range of 1. For a curved limit it istoltimes 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,tolin the limit’s own units. The corners the region reports (vertices(),shadow(),slice_polygon()) pass, whose rounding (seevertices()) moves a coordinate by at most1e-10in 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
labelsis 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:
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:
- 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
npoints 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 (thevolume_fraction()ofgiven()), 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:
n (int) – Number of points.
rng (numpy.random.Generator) – Source of randomness.
- Returns:
nrows, one column per factor, in factor order.- Return type:
- 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:
- 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 thatfeasible()accepts with the sametolbreaks 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:
- 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
npoints 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:
- seed_points()[source]#
Return
support_points(), or none for a box too large for a boundary grid.- Return type:
- 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:
- 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 (seebox_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 (seecurved_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_samplespoints 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_samplespoints 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 default2**14points 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:
- Raises:
ValueError – If
n_samplesis 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 pointsis_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 asvertices()describes;Nonewhen no allowed point is found (seeis_empty()).- Return type:
- 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:
- 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
Tfrom 150 to 200 andPfrom 1 to 3,T + 25*P <= 215keepsTat or below 190 (Pis at least 1) andPat or below 2.6 (Tis 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 asvertices()describes;Nonewhen the linear constraints leave no setting.- Return type:
- 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:
- 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 whereTkeeps one sign. The diagnostics treat the straight ones exactly and the curved ones by sampling (seeis_empty());implied_bounds()leaves the curved ones out, andvertices()refuses a region that has any.- Returns:
Indices into
constraints, in order.- Return type:
- 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 readslow <= sum(coefficient * factor) <= high, withNonefor 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"gives0.01and not0.00999999999999; where the expression is affine over the box only (abs(T)withTpositive), it is read off a fit there. A coefficient whose effect over the box is below1e-9of 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 as1.65e-14or250.00000000000009does not appear.- Returns:
One entry per constraint, in order: the tuple above, or
Nonefor a constraint that is not affine, compares only numbers, chains two different combinations of factors ("A <= B <= 2"), or joins comparisons withand,orornot.- Return type:
- 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 choosingkboundaries.- 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 that250.00000000000009reads250.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 of1e-10times the range, up to 16 figures, about what a double holds. Every vertex then passesfeasible(), 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
xandy: 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 thoseis_empty()searches with.- Parameters:
- Returns:
{"kind": "polygon", "points": [[x, y], ...]}in actual units: the corners counter-clockwise from the one with the smallestx; 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, andz[i][j]for the cell at(x[j], y[i]).- Return type:
- Raises:
ValueError – For a mixture region, or when
xandyare not two different numeric factors.
- slice_polygon(x, y, at=None)[source]#
Return the settings of
xandythat are allowed with every other factor held atat.For linear constraints this is exact: the
x-yrectangle 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
xandyare ignored. A value outside a factor’s range leaves nothing to run.
- Returns:
{"kind": "polygon", "points": [[x, y], ...]}in actual units, as forshadow()(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 andz[i][j]for the point(x[j], y[i]).- Return type:
- Raises:
ValueError – For a mixture region, when
xandyare not two different numeric factors, or whenatnames 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-yrectangle, the others held atat.These are the lines a picture of the region draws and numbers: for a constraint in
xandyalone the line does not depend onat; for one that also names other factors it is the line in the slice atat(seeslice_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 indexi. A boundary that misses the rectangle, only touches a corner of it, or does not involvexorygives none, and curved constraints give none.- Return type:
- Raises:
ValueError – For a mixture region, when
xandyare not two different numeric factors, or whenatnames 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:
<= 0exactly 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+infwhere it cannot be evaluated. A value within the default tolerance offeasible()(1e-9, as a coded distance for a straight limit) is returned as zero, so<= 0here agrees withfeasible()at every point.- Parameters:
x (str) – Two different numeric factors.
y (str) – Two different numeric factors.
x_grid (np.ndarray) – Values of
xandyin actual units; they are broadcast together, so the two arrays fromnp.meshgrid(or a row and a column) give a grid.y_grid (np.ndarray) – Values of
xandyin actual units; they are broadcast together, so the two arrays fromnp.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
xandyare ignored.
- Returns:
The slack, of the broadcast shape of
x_gridandy_grid.- Return type:
np.ndarray
- Raises:
ValueError – For a mixture region, when
xandyare not two different numeric factors, or whenatnames 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:
ValueErrorNo 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:
- Return type:
None
- 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):Whether any straight cut separates the two sets: a linear program maximises the margin
tof a planew @ x = cwith every|w_j| <= 1. When the best margin is not positive,NotSeparableErroris raised; one small linear program per setting finds the settings that lie inside the other set’s convex hull, to name them.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 ofcannotis ruled out and every setting ofcanis allowed, none of them on the boundary. Its first term is positive.- Return type:
- 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_cutsthe 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)) >= 1over the limited factors, wherevis 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 isNone. 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 asseparating_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:
- 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,
cutsnames 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_constraintthe 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:
Extreme vertices. Inside the plane
sum(x) = 1a vertex is a point whereq - 1constraints are active. Every choice ofq - 1constraints 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).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.
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".
- process_improve.experiments.designs_mixture_constrained.scheffe_matrix(x, model)[source]#
Expand proportions
x(n, q) into the columns of a Scheffé model.
- 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.
- process_improve.experiments.designs_mixture_constrained.cubic_difference_term(a, b)[source]#
Return the full cubic Scheffé model’s term in components
aandb:I(a * b * (a - b)).
- process_improve.experiments.designs_mixture_constrained.mixture_inequalities(factors, constraints)[source]#
Return
(A, b)so the feasible mixtures areA @ x <= bwithsum(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) > 1orsum(high) < 1).- Parameters:
constraints (list[Constraint] | None)
- 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 ofq - 1remaining rows is then made active, stacked with the rowsum(x) = 1into aqbyqsystem, 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_SUBSETSsystems would have to be solved.- Parameters:
- Return type:
- 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 thex_i x_j (x_i - x_j)terms of a full cubic model need), centroids of the 2-dimensional faces (plane_centroid, the points thex_i x_j x_kterms 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 withsum(x) = 1, have rankq - 1: the set of points satisfying them is a line.
- 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;
lowandhighare the component bounds as proportions.budget (int or None) – Number of runs.
Nonereturns, 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 ofn_terms + 5runs is returned instead, andmetadata["n_runs_vertices_edges_centroid"]records the size avoided. Withoptions.candidates,Nonemeans a D-optimal design ofn_terms + 3runs. 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 aValueError.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_verticescounts the region’s vertices andn_candidatesthe distinct blends the runs were chosen from (the feasible supplied ones, withoptions.candidates).- Return type:
- 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 allq + mfactors, less the terms thatsum(x) = 1makes redundant. The intercept issum(x_i), a squarex_i^2isx_i - sum_{j != i} x_i x_j, and a process main effectz_kissum_i x_i z_k. What remains isfirst order:
x_iandz_k,q + mterms;with interactions:
x_i,x_i x_j,x_i z_kandz_k z_l;second order: those and
z_k^2, the model of Kowalski, Cornell and Vining (2000), withq + q(q - 1)/2 + qm + m(m - 1)/2 + mterms;"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 + mterms.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:
objectOne 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 - 1for a categorical factor ofLlevels, else 1.- Type:
Return the number of model columns
- process_improve.experiments.designs_mixture_process.Term#
the product of its atoms. The empty term is the intercept.
- Type:
A model term
- class process_improve.experiments.designs_mixture_process.ReducedRank(contrast=None)[source]#
Bases:
objectA patsy contrast that codes a categorical factor in
L - 1columns wherever it appears.Patsy gives a categorical factor all
Lcolumns 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 theL - 1columns ofcontrastin both cases.- Parameters:
contrast (object or None) – A patsy contrast such as
Sum()orTreatment().Nonemeans 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).
- class process_improve.experiments.designs_mixture_process.ReducedRankFormula(text, categorical)[source]#
Bases:
strA model formula whose categorical factors patsy codes in
L - 1columns 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 incategoricalfor one carryingReducedRank. The coefficient names stay readable:catalyst[S.A],T:catalyst[S.A].- Parameters:
- Return type:
- class process_improve.experiments.designs_mixture_process.MixtureProcessModel(name, terms)[source]#
Bases:
objectA model in mixture components and process factors, as its list of terms.
- Parameters:
- property n_parameters: int#
The number of coefficients, one per term and
L - 1per 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:
- 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).
- 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 inlevelsis categorical, so it has no square.
- process_improve.experiments.designs_mixture_process.mixture_process_model(mixture, process, model_type, levels=None)[source]#
Build the mixture-process model
model_typenames, for these components and process factors.- Parameters:
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:
- Raises:
ValueError – If
model_typenames 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:
- process_improve.experiments.designs_mixture_process.category_levels(factors)[source]#
Return the levels of each categorical factor, keyed by its name.
- 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 (
lowandhightheir bounds as proportions), and continuous and categorical process factors, in any order.budget (int or None) – Number of runs.
Nonemeans the model’s number of terms plus 3. A budget below the number of terms raises aValueError. 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 (seemixture_process_model(); default"interactions"), andcriterion:"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.formulais the model’s right-hand side, to giveevaluate_design()oranalyze_experiment().- Return type:
- 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), orNoneif 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.
- process_improve.experiments.designs_supersaturated.e_s2(design)[source]#
E(s^2): the mean of(x_i' x_j)^2over all pairs of distinct columns.
- 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 withn_factors >= n_runs.The bound is Nguyen’s (1996) and Tang and Wu’s (1997)
n^2 (m - n + 1) / ((m - 1)(n - 1))formfactors innruns, raised to 4 whenn = 2 mod 4: two balanced columns then haves_ij = 2 mod 4, so every|s_ij| >= 2, a floor the general bound falls below for smallm. 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. Form < n(not supersaturated) the bound is 0.
- 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.
- process_improve.experiments.designs_supersaturated.supersaturated_runs(k, budget)[source]#
Return the largest run count up to
budgetwith an unaliased supersaturated design forkfactors.The run count is even, at most
k(fewer thank + 1runs),2 * nis a Hadamard order built here, and Lin’s half-fraction has no fully aliased pair of factors.Nonewhen there is no such count, or whenbudgetreachesk + 1and so is not supersaturated.
- process_improve.experiments.designs_supersaturated.supersaturated_available(k, budget)[source]#
Whether an unaliased supersaturated design for
kfactors fits inbudgetruns.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.
- process_improve.experiments.designs_supersaturated.dispatch_supersaturated(factors, budget=None)[source]#
Generate a supersaturated design:
ktwo-level factors in fewer thank + 1runs.- 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, and2 * budgetmust be a Hadamard order built here (every multiple of 4 up to 232).Nonepicks the smallest orderNwithN - 2 >= kandN / 2 <= kwhose half-fraction has no fully aliased pair of factors, givingN / 2runs. A budget that forces fully aliased pairs is honoured, with aUserWarningnaming the run count that avoids them.
- Returns:
The coded design and metadata:
e_s2, its lower bound (seee_s2_lower_bound()) and their ratioe_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:
- 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 order2 * 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 intonequal 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.
- 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.
Noneuses10 * 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 discrepancyCD^2of the points mapped to[0, 1]^k(lower is more uniform), asscipy.stats.qmc.discrepancy()computes it.- Return type:
- 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 first2**22points 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 takendistinct 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 -
hand-hcancel, so every main-effect column sums to zero;main effects clear of the two-factor interactions -
x_i x_a x_bis an odd function, so its contributions fromhand-hcancel;main effects clear of the pure quadratics -
x_i x_j^2is odd inx_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:
objectDiagnostics 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_restarts#
Number of randomized-objective ILP solves the multistart was allowed (the actual count can be lower when early-stopping ends it).
- Type:
- ilp_iterations#
Number of ILP solves (the outer search iterations): the minimize-size probe, the baseline feasibility solve, and every randomized-objective restart.
- Type:
- feasible_designs#
Number of distinct verified OMARS designs found (enumerated, on the exhaustive path). The
rank_deficient_designsamong them are set aside; the rest are ranked.- Type:
- 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:
- enumerated_designs#
Number of feasible designs enumerated on the exhaustive path (0 on the multistart path).
- Type:
- node_limit#
Branch-and-bound node budget of each randomized-objective solve (
Nonemeans unlimited).- Type:
int or None
- node_limited_solves#
Number of ILP solves that the node budget stopped before optimality was proven. These are expected and deterministic.
- Type:
- 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_statemay not reproduce the design.- Type:
- 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:
- size_proven_minimal#
Truewhen the returned run size was proven to be the smallest feasible one in the window.Falsewhen 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.Nonewhen the run size was pinned byn_runsor lies beyond the distinct half-runs (see n_runs_range ingenerate_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:
- 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(thegenerate_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 has2 * h + 1runs forhselected half-runs. The integer program is solved with HiGHS, throughscipy.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
Truethe 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 minimisessum_r objective[r] * s_rinstead of running a pure feasibility (or minimise-size) search. A random objective drives the solver towards a different feasible OMARS design, which is howgenerate_omars()samples diverse, high-quality designs. Takes precedence over minimize_size. Only solves with an objective are subject to thenode_limitsolver 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, defaultFalse): print the HiGHS log to standard output."time_limit"(float, default60.0): wall-clock cap in seconds on the solve;math.infremoves it. A solve stopped by this cap depends on machine speed."node_limit"(int or None, default100): 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;Nonesolves to proven optimality.
- Returns:
(design or None, solver_status, chosen_half_indices).Nonemeans 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:
- 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, for2*h + center_runsruns 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:
n_runs (int, optional) – Exact total run size of the returned design, centre runs included.
n_runs - center_runsmust 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)/2for"full_second_order",1 + 2kfor"main_quadratic"). IfNonea size is chosen automatically.n_runs_range (tuple[int, int], optional) – Inclusive
(min, max)total-run-size window to search when n_runs isNone. 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 raisesValueError. Sizes needing more half-runs than the(3**k - 1) / 2distinct 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 variancetrace((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) scoresinfon 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}. AValueErroris 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=3returns 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;
0runs 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 hitssolver_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. DefaultNone: 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 exceed1 + 2k + k(k-1)/2runs."main_quadratic"sizes for only the main effects and pure quadratics (1 + 2kparameters), 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, defaultFalse) prints the HiGHS log to standard output;"time_limit"(float seconds, default60.0) caps each solve’s wall-clock time;"node_limit"(int or None, default100) caps the branch-and-bound nodes of each randomized-objective solve, andNonesolves each one to proven optimality. Unknown keys raiseValueError. Seesolve_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 withis_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_limitinstead depends on machine speed;metadata["omars_search"].time_limited_solvescounts 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_searchreport, …).- Return type:
- 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:
- Returns:
One
Columnper factor, each of length2**k, holding the coded settings -1 and +1 in standard (Yates) order.- Return type:
- 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 holdnfactorsdistinct 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:designordesign_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 areinner_runandouter_run(the position of the run in its array, from 1), then the inner array’s factors and the outer array’s. TheRunOrdercolumn 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, usecrossed.pivot(index="inner_run", columns="outer_run", values="y").- Return type:
- Raises:
ValueError – If an array has no runs or no factors, has a
BlockorWholePlotcolumn, or shares a factor name with the other array, or ifunitsis neither"coded"nor"actual".
Notes
A
DesignResultis crossed in the order of its table, which is its randomised run order. For the standard order, passresult.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:
- Raises:
ValueError – If
kindis not one of the three, orreadingsis 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) andaliases(each main effect’s alias chain). A relation of more than 1023 components (more than six generators) is not listed:defining_relation_notesays so, and more than ten generators leave the resolution unknown as well.- Return type:
- 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:
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
- 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) andsum_of_squares.- Return type:
- 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:
OLSJust a thin wrapper around the OLS class from Statsmodels.
- 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-levelsummary()wrapper handles screen output via its ownshowflag. Thealphaandprint_to_screenarguments are unused and kept for backwards compatibility.
- get_parameters(drop_intercept=True)[source]#
Get the parameter values; return them in a pandas Series indexed by term.
- 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
- get_response_name()[source]#
Get the name of the response variable from the model specification.
- Return type:
- get_title()[source]#
Get the model’s title, if it has one. Always returns a string.
The title is the data’s
pi_title.lmalso accepts a plain DataFrame, which has none, so a missing attribute is an empty title rather than an error.- Return type:
- 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.
- exception process_improve.experiments.models.UnsafeFormulaError[source]#
Bases:
ValueErrorRaised 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_modelMCP 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
xwithprecsignificant/decimal digits, switching to thegformat for very large or very small magnitudes. Any positiveprecis supported;prec=3andprec=4reproduce the original widths.
- 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, asIinI(P ** 2), is fitted under a stand-in name and reported under its own.data (Expt) – The data; a plain
Exptor 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.
Nonedrops nothing.
- Returns:
The fitted model.
- Return type:
- process_improve.experiments.models.predict(model, **kwargs)[source]#
Make predictions from the model.
- 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.
- process_improve.experiments.models.validate_formula_is_safe(formula, allowed_names, *, allow_transforms=False, allow_numpy=False)[source]#
Reject a model
formulathat is not a safe Wilkinson formula over allowed_names.This is the guard for untrusted callers (e.g. the
fit_linear_modeltool). Patsy evaluates every formula term as a Python expression with builtins and numpy in scope, so a string such asy ~ 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- permitI(...)/Q(...)wrapping arithmetic over data columns (e.g. thequadraticshorthand’sI(A ** 2)).allow_numpy- additionally permit a curated allowlist of element-wise numpy calls such asnp.log(A)ornp.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 asIinI(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_matrixdict 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,dfandmsof the difference;fandp_value; andss_error,df_errorandms_error, the denominator’s.- Return type:
- 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 leavingtermsout ofmodeland 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:
- Returns:
What
compare_models()returns, with thereducedformula it built and thetermstested.- Return type:
- Raises:
TypeError – If the model was not fitted from a formula.
ValueError – If
termsis empty, names a term the model does not have, or names several terms in one string; and ascompare_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:
Code each factor to the
[-1, +1]range and centre the response.Estimate the error variance from the residuals of the full second-order model (intercept + main effects + all quadratics and interactions).
Test the main effects with t-tests at
alpha_mainand split them into active and inactive sets.Pool the inactive main effects back into the error estimate, increasing the error degrees of freedom and (usually) sharpening the variance.
Gate the second-order space with an overall F-test at
alpha_second_overall.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:
objectContainer returned by
analyze_omars().- Parameters:
- success#
Falsewhen the full second-order model leaves no error degrees of freedom (the design cannot support the analysis). All other fields are only meaningful whensuccessisTrue.- Type:
- active_main_effects#
Factor names declared active by the main-effect t-tests (step 3), including any forced in via
force_main_effects.
- forced_main_effects#
Factor names that were forced active despite being statistically inactive.
- second_order_overall_p_value#
p-value of the overall second-order F-gate (step 5).
nanwhen the analysis was main-effects only.- Type:
- final_p_value#
p-value of the final (failed) F-test that terminated the subset search.
nanwhen the second-order gate did not open.- Type:
- subset_limit#
Maximum number of second-order terms the subset search was allowed to consider.
- Type:
- 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
Falsethe analysis stops after the main effects (step 3). DefaultTrue.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:
- 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
Tplus, for each factor, the mean of the runs at its level lessT; each pair named in interactions adds the mean of the runs at its two levels, less the two factors’ means, plusT. For one 2-level factor A and an interaction with B this isT + (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
UserWarningsays 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:
objectHow the levels of a factor are compared, and at what confidence.
- 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:
- reference_rule#
How
reference_scaleis 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 aComparisonsthat 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) itsmean,std_errorand interval on the residual degrees of freedom, andby_level, the same keyed by level;comparisons, one row per pair (levellessversus) with thedifference,std_error,t, adjustedp_valueand simultaneous interval (and itshalf_width);pairs, the same keyed"level - versus"; the residualdf; themethod; thecritical_valuethat multiplies each difference’s standard error;individual_confidence, the confidence each interval has alone, andfamily_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 thestudentized_rangepoint, the critical value times the square root of 2, and Tukey’s and the unadjusted giveletters, each level’s connecting letters at the significance level 1 - the confidence.reference_scaleis the scale of Box, Hunter and Hunter’s reference t distribution for the means, ondfdegrees of freedom, by thereference_rulethe comparisons name (see the module’s text).- Return type:
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, andkept, 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; andresidual.attrs["sum_of_squares"]holds the sum of squares ofkeptand of each column, each twice the one before, then offitted(the crude sum of squares of the fitted values) and ofresidual.- Return type:
pd.DataFrame
- Raises:
ValueError – If
tableis not a table fromyates_table(), orkeepnames 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
designbut 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", …); itsestimate, the last column divided by N/2 (by N for the mean); and itssum_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_experimentgives 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; andapproximate_anova, the sequential analysis of the completed table, one row per term and the residual, withdf,sum_sq,mean_sq,fandp_value.- Return type:
- 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:
ssondf(1), itsfandp_value;ss_error,df_errorandms_error, the denominator’s;remainder_ssandremainder_df, the interaction left oncessis taken out (the denominator itself, with one run per cell);coefficient, the regressor’s (Box, Hunter and Hunter’s theta); andpower.- Return type:
- Raises:
ValueError – If
erroris 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"isaanda:b,"a + b"is both.
- 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 itsdf, itsunits(the number of groups its error term makes, the runs for"Within"), thetermsestimated in it, each with itsdf(and with a responsess,ms,fandp_value), and itsresidual;df, each stratum’s terms and"Residual"mapped to their degrees of freedom; andtable, 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:
- 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 ofleast_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 inevaluate_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
DesignRegionor itsto_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:
- 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
Modelreturned bylm(), 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
Modelis supplied.factors_to_plot (list of str, optional) – Subset of factors to display. By default all main-effect factors are shown - for a
Modelthese 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_doeGeneral 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_codinginevaluate_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
DesignResultis 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 aUserWarning. Categorical factors are label columns. A run with a missing factor setting raisesValueError.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 ingenerate_design()("interactions"and"quadratic"to"scheffe_quadratic","special_cubic"to"scheffe_special_cubic","cubic"to"scheffe_cubic"), andNonemeans 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", …; seemixture_process_model()) becomes that model, andNonemeans the model the design was generated for, else"interactions". OtherwiseNonemeans"interactions". A formula may name aBlockcolumn ("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"is100 * 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 * sigmato3 * sigmais 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 radiussqrt(k). ADesignRegionrestricts 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 aDesignResult). The average and the maximum are then taken over exactly those points, each counted once, with no sampling.None(default) uses the region aDesignResultrecorded 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**kcube vertices of the quantitative factors, crossed with every combination of categorical levels; for aDesignRegion, 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
fdsmetric returns only the coarse 11-pointquantilessummary. When set (e.g. 200), acurvesub-dict with length-fds_resolutionfraction/prediction_variance/scaled_prediction_variancearrays 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 withL > 2levels getsL - 1columns, 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. ABlockcolumn named in the formula is sum-coded under"effect", asanalyze_experimentcodes 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)withWthe 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 aDesignRegionit 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
WholePlotof 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 informationX' V^-1 X, withV = I + sum_k eta_k Z_k Z_k'andZ_kthe indicators of effectk’s groups, in place ofX'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 freedomN - p.None(default) treats the runs as independent.alias_terms ({"interactions", "quadratic"}) – The terms outside the model that
alias_matrixworks 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:
- 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()withmetric="all"so callers need not enumerate the metric list. All parameters have the same meaning as inevaluate_design().- Returns:
Results keyed by metric name (the union of every registered metric).
- Return type:
- Parameters:
See also
evaluate_designCompute 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_quadraticis 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:
objectOne row of a
DesignComparison.- key#
The key the measure has in
DesignComparison.values.- Type:
- direction#
"higher"or"lower", whichever is preferred; empty for the feasibility rows, which are facts about the design rather than a scale.- Type:
- group#
"Feasibility","Averages","Worst cases","Neither"or"Power": the book’s grouping of the measures.- Type:
- 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:
objectDesigns scored for one model on the measures in
MEASURES.- Parameters:
model (str)
effect_size (float)
significance_level (float)
g_points (str)
region (DesignRegion | None)
_model (_Model | None)
- terms#
The model’s terms, named as
analyze_experiment()names them ("Intercept","A","A:B","I(A ** 2)").
- effect_size#
The coefficient size, in noise standard deviations, at which power is computed.
- Type:
- designs#
The designs’ labels, each with its size (
"DSD [n=9]"), in the order given.
- values#
For each design label, each measure’s value by its
Measure.key. A measure the design cannot provide isNone: 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.
- correlations#
For each design label, the absolute Pearson correlation between its main effects, squares and two-factor interactions, in that order;
NaNfor 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:
- region#
The region I and G were taken over, as it was given;
Nonefor 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.
- 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.valuesandlonghold the numbers.- Return type:
- property long: DataFrame#
Return one row per design and measure.
- Returns:
Columns
design,key,measure,group,directionandvalue.- Return type:
- 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
designslists them ("DSD [n=9]") or as they were given ("DSD").reference (str) – The designs’ labels, as
designslists 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_designseeds 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_efficiencyandg_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), andfds_lower_share, the share of the fractions at which the design’s curve is the lower;design,referenceandn_points.
- Return type:
- Raises:
ValueError – If a label names no design of the comparison, either design cannot fit the model, or
n_pointsis below 2.
- 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_pointsabove 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,
regionholds 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, asg_pointsthen 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
DesignResultkeeps it inmetadata["whole_plot_variance_ratio"]; a DataFrame can carry one inattrs["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 theWholePlotcolumn, asevaluate_design()does withrandom_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 codeddesign), a DataFrame of factor columns (RunOrderandBlockare ignored), or an array with one column per factor. A mapping’s keys label the designs; in a sequence each design is labelled with itsdesign_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:
- Raises:
ValueError – If
modelis not one ofMODELS,effect_sizeis not positive,significance_levelis not between 0 and 1, no design is given, the designs differ in their factors, orregionis a mixture region, has limits that name a categorical level (compare over eachregion.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
regionis neither aDesignRegionnor 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) andn_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\) andcombined_effects\(\tau_i^*\); by block label:block_totals\(y_{.j}\) andadjusted_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_errorandsigma2_blocks(\(\hat\sigma_\beta^2\)).- Return type:
- 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
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
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.
RunOrderandBlockcolumns are set aside, asevaluate_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,meanandmaximum: 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 withinROTATABLE_TOLERANCEof the mean on every sphere;design_radius: the largest distance of a run from the centre;n_runsandn_terms: \(N\), and the model’s columns.
- Return type:
- 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_directionsis 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:
objectA 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 nameanalyze_experimentuses. 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.
- Raises:
ValueError – If a term cannot be read, a coefficient is not a finite number, or
sigmais 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 codeddesign) or a DataFrame with a column per factor;RunOrderandBlockare ignored.- Returns:
One value per run.
- Return type:
- 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:
design (DesignResult or pandas.DataFrame) – The runs in coded units, as for
mean().random_state (int, numpy.random.Generator or None) – Seed or generator for the noise; the same integer gives the same responses.
Nonedraws fresh noise on every call.
- Returns:
One response per run, named
"y", on the design’s index.- Return type:
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.
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:
- class process_improve.experiments.analysis.AnalysisResult(ols_result=None, formula='', results=<factory>)[source]#
Bases:
objectInternal, unused container.
Retained for backwards compatibility with any external caller that imports the name from this module.
analyze_experiment()returns a plaindict(see itsReturnssection), not an instance of this class, so downstream code that expects the return type ofanalyze_experimentshould key into that dict rather than access attributes here.- ols_result: RegressionResultsWrapper = None#
- 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
RunOrdercolumn, 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. ABlockcolumn with two or more blocks is added to the named non-mixture models as a fixed effect (sum-coded contrast columnsBlock1, …; 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 namesBlockas 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 writtenC(Block),C(Block, Treatment(1))and so on. AWholePlotcolumn, whichgenerate_designadds to a split-plot design, labels each run’s whole plot: it is used byanalysis_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, asIin 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_columnmust 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. Useresponse_columnto pick a specific column explicitly. An unnamed Series takes the nameresponse_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 andmodel_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 mostsettings.max_formula_termsterms.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 anI()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 inanova_note, naming the strata formultistratum_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 undereffects_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 asI(A**2). A model with anI()term that is not a product of whole powers of the factors has no coded scale, so its effects and Lenth’s method raiseValueErrorunless its numeric factors are already coded. The curvature test uses pure error from the replicated runs.Each of the
"coefficients"carries itsstd_error,t_value,p_value, its intervalci_lowtoci_highat theconfidence_levelof 1 -significance_level, and itsstandardised_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 oncodingwhen the model has interactions or squares. The"coefficients"analysis also givesestimate_correlations, the correlation of each pair of estimates, from \((X'X)^{-1}\) alone, andjoint_confidence_region: itssum_of_squaresbounds the values of all the coefficients together at1 - significance_level, \(S_R (1 + p F / (n - p))\) with \(S_R\) theresidual_sum_of_squares(Box, Hunter and Hunter, eq. 14.45); with asigma, \(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. Itsdurbin_watsonis worked in the rows’ order. With aRunOrdercolumn, itsrun_orderentry holds the residuals’ lag-1 autocorrelation in run order, with its standard error 1/sqrt(N), and the Durbin-Watson statistic in run order; anotesays 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"]isFalse."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 thevariance_components(whole_plot,residualand their ratioeta, which a split-plot design is built for), theerror_dfof the two strata,coefficientsand per-termtests(each with itsdf,stratumand p-value), and thesignificant_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, andinformation="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. Withnew_points, the result also haspredictionsfrom the generalized least-squares fit: thepredictedmean with itsstd_error,dfand interval (ci_low,ci_high), and the interval for one new run there (pi_low,pi_high, withpi_std_errorandpi_df), which falls in a new whole plot, so its variance adds every variance component. The result carriesresponse_name,factor_namesandcodingas"coefficients"does, so it can be passed tooptimize_responsesas 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 thefamilygiven, for a response that is a count of defects ("poisson"), a number of successes out of a number oftrials("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 thecoefficients(withexp_coefficient, an odds ratio or a rate ratio, under a logit or log link), thelog_likelihood,devianceandpearson_chi2, thegoodness_of_fittests (Hosmer and Lemeshow’s for the binomial family), and thefittedmean of each run with its interval; withnew_points,predictionsthere as well. Seerun_glm().significance_level (float) – Default 0.05. Used by every test’s
significantflag and every interval.transform (str or None) –
"log"(natural),"log10","sqrt","inverse","box_cox", orNone. 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 asprediction_scale/scale), and confirmation runs are transformed the same way before they are compared. Box-Cox chooseslambdaby the profile likelihood of the model. A response outside the transform’s domain raisesValueError.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 anI()term that is not a product of whole powers of the factors, such asI(x ** 0.5); these keep"actual", and asking for another scale raisesValueError.Nonemeans 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 asoptimize_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_responsesevaluates models at coded settings, so pass it coded coefficients. The result reports the scale ascoding, and, asfactor_coding, thelowandhighthat 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 itsBlockcolumn names, one of the runs’ blocks; a point that names no block (noBlockcolumn, 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 theWholePlotcolumn 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’sglmdoes;"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: eacht_valueis 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, aftertransform. 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 tooptimize_responsesas 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, orNone; withbox_cox_lambdafor 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 ofaverage_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_notesays 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 asigma): 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 thann_termsimplies aliasing / rank deficiency.rank_deficient-Trueifmodel_rank < n_terms.ill_conditioned-Trueif, 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- thesigmagiven, when one is: the standard errors,adequate_precisionandaverage_fitted_varianceuse it, not the square root ofmse_residual.f_statisticandf_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,aicandbic- the Gaussian log-likelihood and the criteria that count the error variance as a parameter, as R’sAICdoes;aic_step, n ln(RSS/n) + 2p, as R’sstepprints it.ss_total,ss_meanandss_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 ofr_squaredandf_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. %”;Nonewhen the mean is zero.With blocks in the model,
ss_blocks(the blocks’ sum of squares, fitted first) andr_squared_without_blocks,r_squared_adj_without_blocksandr_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 (seemodel), and thatadequate_precisionis below 4.
Notes from individual analyses are kept apart, as
anova_note,effects_note,lenth_note,significance_noteandsplit_plot_note. Themodel_summaryis of the least-squares fit, also for"split_plot".- Return type:
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:
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,
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:
objectA Gaussian process model fitted by
fit_gaussian_process().- Parameters:
- theta#
The correlation parameter of each input, in the inputs’ own units.
- Type:
- weights#
\(R^{-1}(y - 1\hat\mu)\), one per run: the prediction is \(\hat\mu + r(x)^T w\).
- Type:
- runs#
The inputs of the runs, one row each.
- Type:
- 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:
predictedandstd_error, one row per point; at a run the prediction is the run’s output and the standard error is 0.- Return type:
- 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_effectandtotal_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_effectis the share of their interaction.- Return type:
- 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
predictandsensitivity.- Return type:
- 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:
objectA model fitted across blocks by
fit_across_blocks(): the chosen form, its fit, and how it was chosen.- Parameters:
- 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, andruns_alone(runs that alone carry a term or a block, so cannot be left out).- Type:
- coefficients#
The chosen form’s coefficients, a row per term (
Interceptis the average block), a column per response.- Type:
- standard_errors#
Their standard errors, shaped as
coefficients.- Type:
- block_shift#
Each block’s shift from the average block, a row per block, a column per response; the shifts sum to 0.
- Type:
- block_shift_se#
Their standard errors, shaped as
block_shift.- Type:
- residual_sd#
Each response’s residual standard deviation.
- Type:
- df#
Each response’s residual degrees of freedom.
- Type:
- 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:
Notes
With neither
blocknoranchors, 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 writtenC(F, Sum)for effects coding; a square root asnp.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:
- 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:
objectCurves from a designed experiment, fitted by
analyze_functional_experiment().- Parameters:
- 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 thereasonit is kept:"variance"up to 99% of the variance,"factor test"after that.- Type:
- 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 aWholePlotcolumn.
- 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:
- 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
WholePlotcolumn 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
levelthere: 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_lowandsimultaneous_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.levelandn_bootstrap, as used.- Return type:
- 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 ismean + x @ mapand replicateb’s isreplicate_means[b] + x @ replicate_maps[b]. The bootstrap is the onepredict()describes, andpredict()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_meansandreplicate_maps, the same for each replicate; anddf_residual.- Return type:
- 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_startsrandom 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;
NaNwhere 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),scoresandtarget_scores(the kept components’ scores, predicted and of the target).- Return type:
- 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 withanalyze_experiment(), under the samemodel.FunctionalFit.predict()predicts whole curves at new factor settings, with bands.- Parameters:
design_matrix (pandas.DataFrame) – Factor settings, one row per run. A
WholePlotcolumn 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 (seeFPCA);NaNwhere 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_componentsis 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:
- 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 ofNonebrings 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 addslen(existing_design)runs); the default ofNonebecomes 1 complete copy.fold_on (str or None) – For
"semifold"only: which factor to fold on. IfNone, 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, andNonedraws fresh starts.factor_names (list[str] or None) – The factor columns.
Nonetakes every column exceptRunOrderandBlock, 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 asmetadata["region"]of the designgenerate_designmade. The region’s factors give the coding: theirlowandhighare 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 aUserWarning.
- 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:
- 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.
- 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:
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 ofanalyze_experiment(..., analysis_type="coefficients", coding="coded")as it is. One fitted withcoding="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 byanalyze_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, asanalyze_experimentreports them. A model without them is taken to be coded.
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 aValueError. 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 betweenlowandhigh: 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 whengoal="target")."low"(float) - lower acceptable bound."high"(float) - upper acceptable bound."weight"(float, default 1) - the exponent shaping this response’s desirability ramp betweenlowandhigh. 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. Unlikeweight, 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 fromanalyze_experimentsupplies 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 byanalyze_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 withinside_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 carrieswithin_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 matrixBat 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, whichflat_tolerance=0.05reads as a stationary ridge; their rising ridge, with a slope of 2.97, still rises. Below 5% of the largest, the result’sclassification_notesays so.
- Returns:
Results keyed by method. Always includes
"method"and"factor_names".- Return type:
- 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:
ValueErrorThe 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
basealong 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: itscoefficientson the coded factors, itsfactor_names, and optionally itsfactor_coding(each factor’slowandhigh), which gives each step in actual units too.How far each step goes.
A number: the distance between successive points, in coded units (default 0.5).
{lead: amount}: the lead factor movesamounta 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 + 1rows: 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, anddirection:"ascent"or"descent".lead: the factor that sets each step, orNonefor 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,codedand, with the coding,actual.factor_ranges: the ranges the coding gives, whichnext_design()reads.steps: one row per step, with itsstepnumber, itscodedsettings, itsactualsettings when the model has its coding, and itspredicted_response.
The intervals on the predictions need the fit itself: pass the coded settings of the steps as
new_pointstoanalyze_experiment(..., analysis_type="prediction").- Return type:
- Raises:
NoSteepestDirectionError – If the gradient of the model is zero at the base.
ValueError – If
step,n_steps,baseorgoalis 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, timesscale[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_actualin the factors’ units about the new centre.- Return type:
- Raises:
ValueError – If
at_stepis not a step of the path, orscalenames 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
Blockcolumn.
- Returns:
contrasts: for each factor, itsaxial_slope,factorial_slope, their differencecontrast, and thestd_errorof that difference.sigmais the standard deviation used, andsigma_dfits degrees of freedom when it comes from the runs (Nonefor a givensigma).- Return type:
- 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:
- Returns:
ss_pure_error,df_pure_error,ms_pure_error(the pooled variance) andstd_dev, its square root, withper_design, each design’sssanddf.- Return type:
- 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_levelconfidence 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
Blockcolumn, 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_errorgives 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
Blockcolumn.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 (Nonewhen the fitted surface has none: a rising ridge),classification("maximum","minimum","saddle_point","stationary_ridge"or"rising_ridge", asoptimize_responses()reads the eigenvalues of B), andstd_error, each coordinate’s delta-method standard error (Nonefor a ridge, along which the point is not determined).statisticis a function of settings returning the F statistic of the gradient there, andcontainsone returning whether each setting is in the region, where the statistic is at mostcritical_value. Both take a DataFrame or a mapping of the factors’ names to values or arrays (a grid fromnumpy.meshgrid, say), or an array whose last axis holds the factors in order, and return an array of the shape of the settings.confidence_levelis 1 -significance_level;sigmais the standard deviation used, andsigma_dfits degrees of freedom when it comes from the runs (Nonefor a givensigma).- Return type:
- 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, orsigmaorsignificance_levelis 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:
objectWhat you get, and what you give up, at a given (runs, factors) pair.
- Parameters:
- n_generators#
The
pin2^(k-p): how many factors are added on top of thek - pbase factors. Zero for a full factorial.- Type:
- resolution#
Design resolution as an integer (3, 4, 5, …), or
Nonefor 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 on2^(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=-ABgives the complementary fraction, which is equally valid.
- defining_relation#
The full defining relation, e.g.
["I=ABD", "I=ACE", "I=BCDE"]. Empty for a full factorial.
- aliases#
Alias chains for the main effects and the two-factor interactions, e.g.
"A = BD + CE + ...". Empty for a full factorial.
- n_replicates#
How many times the full factorial fits into the run budget.
1in the usual case;2means the budget pays for the full factorial twice over, and so on.- Type:
- exception process_improve.experiments.trade_off.SearchLimitError[source]#
Bases:
ValueErrorThe 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
pextra 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:
- Returns:
Generators such as
("D=AB", "E=AC").- Return type:
- 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:
- Returns:
Resolution, generators, defining relation and alias chains. See the class for the full field list.
- Return type:
- 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
Falseto 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
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:
FullN >= k^2 + k + 1, ork^2 + k + 2for an evenN. Main effects, pure quadratics and every two-factor interaction are jointly estimable, so a response surface can be fitted without a follow-up design.QuadN >= 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 + 2runs, is a definitive screening design with a second centre run.SatdN = 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:
objectWhat one run budget buys for one factor count.
- Parameters:
- exists#
Whether any foldover OMARS design has this run count for this many factors.
Falsefor a run count below2k + 1.- Type:
- 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 + 1for"full",2k + 2for"quad",2k + 1for"satd". The"quad"size is even: a foldover with two centre runs, one more than the saturated design.- Return type:
- 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 + 1for a conference matrix of orderm:2k + 1for an even n_factors and2k + 3for an odd one, except where no conference matrix of that order exists.- Return type:
Notes
The construction folds a conference matrix of order
k, available for an evenk. For an oddkit uses one of orderk + 1and 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 - 1must 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
Nonewhen 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:
Nonewhere 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 + 1runs; 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:
- 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 thatgenerate_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
DSDrow above the budgets and aBBDrow 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 anintto make a call reproducible.
- Returns:
The number of popped kernels, with random noise added for realism. Never negative.
- Return type:
- Raises:
ValueError – If t is not a single finite number (a string or
Noneincluded), 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 anintto make a call reproducible.
- Returns:
Profit made per hour [dollars/hour], with random noise added.
- Return type:
- Raises:
ValueError – If either input is a vector, is not a finite number (a string or
Noneincluded), 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 anintto make a call reproducible.
- Returns:
Profit made per hour [dollars/hour], with random noise added.
- Return type:
- Raises:
ValueError – If either input is a vector, is not a finite number (a string or
Noneincluded), 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#
- Return type:
- 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:
- 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:
- 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:
- 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:
- 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:
- 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 responseResistance.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.datain the BHH2 R package.- Return type:
- 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 percentagesfat,sucrose,dry_flourandwater, 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
cookiein 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 theNOTICEbeside the file.- Return type:
- process_improve.experiments.datasets.data(dataset)[source]#
Return the
datasetgiven 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 tooildoe().- 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.typesays 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", orNone) andford_field(that field’s name, orNone).- Return type:
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 applywork,volume,pages,publisher,doi, theversionandcommitthat was read,read: when and from what the numbers were taken, andrights: the terms of the source’s numbers and quotations, which the MIT licence of this package does not cover.- Return type:
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 whenNone.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 whenNone.
- Returns:
One dict per experiment, with keys
name,title,topics,source,kind,where,factors,responses,design,runs(Nonewhen the source does not print them, anddata_sourcethen says where they come from),expectedandnotes. Each item ofexpectedhaswhat,termandvalue(the printed text),where(a file and line, or a page, with a quotation) andcheck. A response measured as a curve, not as one number, also hascurve: itscolumnsin the run table, in order along the curve, and itsargument(nameandunits, and eithervalues, the points every run shares, orcolumns, where each run records its own).- Return type:
- 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:
- Returns:
One dict per design, as for
reference_experiments()but without responses.- Return type:
- 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
causerecords 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
causerecords 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 ofyates_table()andreverse_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 byFPCA, 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(Nonewhere not checked),reason(why a number is not recomputed, or the error raised recomputing it),cause(why a number is not recomputed, one ofUNCHECKED_CAUSES, or why it differs, one ofDISAGREEMENT_CAUSES),disagreement(for a number known not to reproduce, the explanation of why) andwhere.- 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:
objectA family of designs: its name on the page, and the year and work that first published it.
- 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.
yearandreferencename 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:
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 thanGLYPH_ROWSruns 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:
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(seeestimable_models()),glyph(seeglyph()); and, for the largest model the design estimates,run_loss_model,survives_run_loss,critical_runs,worst_d_ratioandleverage(seerun_loss()); andquadratic_projectionandprojectivity(seedesign_measures;Nonefor a design with a categorical factor of three or more labels, whose coding makes it arbitrary).- Return type:
- 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-plotis “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
figuresof an entry’swhererecord it.- Returns:
The figure’s name, with a capital first letter and no full stop.
- Return type:
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
tablesof an entry’swhererecord it.- Returns:
The table’s name, with a capital first letter and no full stop;
""for a note of no table.- Return type:
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;Nonefor 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 byfigure_name(); the notes kept for the catalogue’s maintainers (the source file, its lines and commit) are left out.
- process_improve.experiments.design_catalog.published_runs(entry)[source]#
Return an entry’s runs in coded units, one column per factor;
Nonewhen 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
arrayinstead ofk.- 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 (itsdisagreement). So, on a commit whose tests pass, the counts here are what that report finds, without the minutes it takes to run.
- 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
GENERATEDlists, or both.- Returns:
One record per design.
kindsays what a published entry holds (see_kind()): anexperiment(a design and its results), adesign, asample(results with no factor varied) orfactsabout designs (neither factors nor runs); a generated record is adesign. 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 inruns_from; it isNonefor every other record. The fields inINDEX_FIELDSdescribe it for searching; a published record’scheckscounts its printed numbers (seeprinted_checks()) and a generated record’s isNone. Records with the samedesign_classhold one design, printed in different sources or made by the library (seedesign_classes()).factors,responses,coded_runs,response_values,details,notesandwhere_fullhold what a reader sees on opening a design, andwhere_urlthe address of the place its source prints it (seewhere_url()).blocksholds each run’s block as its source prints it, where the run table has them, and isNoneotherwise: a block is not one of thefactors, nor a column ofcoded_runs.topicslists a published entry’s applications (seecatalog_topics()); a generated design has none.listedsays whether a published entry is one ofreference_experiments()or ofreference_designs(), andspecnames a generated design asGENERATEDdoes.- Return type:
- 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'
- 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
workit 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". SeeSOURCE_FORMS.Examples
>>> source_form(reference_sources()["box-hunter-hunter-1978"]), source_form(reference_sources()["lenth-1989"]) ('book', 'paper')
- 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.
shortnames the source as the list of designs does: its authors’ surnames (seeshort_citation()).
- 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 namedGENERAL_LABEL.Examples
>>> catalog_topics()["food-beverages"]["label"] 'Food and beverages'
- 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.
descriptionsays what a design of the family is in plain words, as the knowledge base of design types describes it to a newcomer (itsdescription["novice"]), or""where it has none.nextlists the families a design of this one can be augmented to, each with how (how), as the knowledge base records them (itscan_augment_to).
- 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,whatandterm(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) androunding(whether the reason mentions rounding).- Return type:
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 ofDISAGREEMENT_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 ofreproduce_reference_catalogue(). A case falls in the best ofREPRODUCTION_TIERSthat 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(Nonewhen none is),worst(the largest relative difference of the numbers the tier holds,Nonewhen one has no relative size, 0 when none differs),differs,uncheckedandmissing.differslists each number that differs, withwhat,term,printed,found(the library’s value, as text),error(its relative difference, seeworst),where(its place in the source, in a few words),reason(why it differs) andcause.uncheckedcounts the printed numbers not recomputed by their cause, one ofUNCHECKED_CAUSES, andmissinglists those the library has no function for yet ("library_gap"), each withreason.- Return type:
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:
- 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 thanCOMPARED_FACTORSfactors or more thanCOMPARED_RUNSruns are not scored.- Parameters:
records (Sequence[dict]) – Records of the catalog (see
catalog_records()).- Returns:
For each scored design’s
id, for each model inMODELS, the valuescompare_designsreports (the keys ofMEASURES, and the model matrix’srank), to four significant figures, andfds, the design’sfds_curve()for the model (Nonewhen the design cannot fit it).- Return type:
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:
objectThe 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)
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
- 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.
- collection#
Published designs, those the library makes, or both.
- Type:
{“both”, “published”, “generated”}
- survives_run_loss#
Keep only designs that still estimate their largest model after losing any one run.
- Type:
- min_quadratic_projection#
Keep only designs whose every projection onto this many factors estimates the full quadratic.
- 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:
- 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 asmin_factors=6: they replace the query’s.
- Returns:
Each design’s facets (the fields of
INDEX_FIELDS), withfamily_label,source_short(its source’s authors and year),where_url(the address of the place its source prints it, seewhere_url()),same_design(how many other designs of the catalog are this one) andurl(its page in the catalog).- Return type:
- Raises:
ValueError – If a family is not one of
FAMILIES, an application not one ofcatalog_topics(), or the model not one ofMODELS.TypeError – If a filter is not a field of
CatalogQuery.
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()andprojectivity(): 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()andsame_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
Nonewhen 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;
Nonefor 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.
- 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 isNonefor a mixture design, whose models are Scheffé’s, or when a setting is missing. The interactions areNonefor a single factor; the quadratic isNonewhen no factor is numeric, andFalsewhen a numeric factor has fewer than three levels.- Return type:
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), andleverage(each run’s).Nonewhen the design does not estimate the model, or the model does not apply (seemodel_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, ascompare_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_SAMPLESpoints 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;
Nonewhen a factor is not numeric, a setting is missing, or the design does not estimate the model.- Return type:
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.
Nonefor 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;
Nonewhen 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.
- process_improve.experiments.design_measures.relabelling(first, second, *, signs=True, most=200000)[source]#
Return, for each column of
first, the column ofsecondand 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
firstin turn, and drops an assignment as soon as the runs on the columns assigned so far differ.firstmay use only some of the columns ofsecond, 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 ofsecondand the sign (1 or -1);Nonewhen no relabelling makes the runs one, or none was found withinmoststeps.- Return type:
- 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(seerelabelling()) gives the runs offirst, with the same number of factors.- Return type:
- 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, andsame_design()says for sure.- Parameters:
coded (np.ndarray) – The runs in coded units, one column per factor.
- Returns:
Sixteen hexadecimal digits;
Nonewhen 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.
- 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 (
n0MEandn0IE). 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, andlevel_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, andinteractions, each product of two three-level factors’ zeros, both over the runs that are not centre runs, withn0MEandn0IE, the value they all share (Nonewhen they differ), andzeros_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, andfoldover, 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) andblocks_balanced.
- Return type:
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,.xlsxor.xlsfile, 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 anXlabel (x1isX01).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, andattrs["columns"]maps each factor to the column it was read from.- Return type:
- 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
unitsis 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(itsdesign_actual, in its randomised run order), or a table with a column per factor, such as therunsthatrecommend_next_step()returns. ABlockcolumn 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 offactors("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, …),Blockwhen the runs have one, a column per factor and a column per response. Save it withto_csvorto_excel.attrs["columns"]maps each factor to its column.- Return type:
- 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
unitsis 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
ExperimentalStrategyfields.- Return type:
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
ExperimentalStrategyfields.- Return type:
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:
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).
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.
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.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.
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).
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.
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.
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
lowandhighdefine 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;Nonejudges 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.
Falsekeeps 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
runsthat 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 ofACTIONS),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), orpath_predictionsanddirection, each moving factor’s step in coded units per unit of the factor that moves most (move_region).- Return type:
- 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
historynot amongACTIONS, ablockthat 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_simulationstimes ontruth, with fresh noise each time, and each simulated study is judged asrecommend_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
DforA:Bin 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: aDesignResult, a DataFrame of factor columns (RunOrderandBlockare 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 andsigma),goal,n_simulations,alpha,precision(the widest 95% margin of a share),true_optimumtoward a goal (the coded settings and the true response there), anddesigns: for each design label (with its size, as"DSD [n=21]"), a dict withn_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, andfalse_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, asmean_in_sigma, in noise standard deviations;summary: one sentence with the main numbers.
- Return type:
- 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,
goalis unknown or a target goal has no target,n_simulationsis below 1, oralphais not between 0 and 1.TypeError – If
truthis not aTrueProcess.
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_designdesign types, and what each one means.
- class process_improve.experiments.strategy.models.DomainType(value)[source]#
-
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:
BaseModelParsed 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.
- 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:
BaseModelRule governing the transition between consecutive experimental stages.
- Parameters:
- 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:
BaseModelOne 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_designdesign type, e.g."plackett_burman","ccd","box_behnken", sogenerate_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 thatoptimize_responsesfinds, not a design built from the ranges.design_params (dict) –
generate_designkeyword arguments (resolution, n_center_points, alpha, etc.).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.
- transition_rules: list[TransitionRule]#
- model_config = {}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class process_improve.experiments.strategy.models.ExperimentalStrategy(*, strategy_id='', stages=<factory>, total_estimated_runs=0, minimum_total_runs=0, budget_allocation=<factory>, assumptions=<factory>, risks=<factory>, alternative_strategies=<factory>, domain='general', detail_level='intermediate', reasoning=<factory>)[source]#
Bases:
BaseModelComplete 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.
- stages: list[ExperimentalStage]#
- 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:
BaseModelValidated 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".
- constraints: list[Constraint] | None#
- prior_knowledge: PriorKnowledge | None#
- domain: DomainType#
- detail_level: Literal['novice', 'intermediate']#
- 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.
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:
- Returns:
Estimated run count including center points.
- Return type:
- 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:
- Returns:
Estimated run count. For a CCD it is the size
generate_designbuilds withcube="full"up to five factors andcube="fractional"(a half-fraction cube) from six, which is whatrecommend_strategypasses.- Return type:
- process_improve.experiments.strategy.budget.estimate_confirmation_runs(min_runs=3)[source]#
Return the number of confirmation runs.
- 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: