Designed Experiments#
- class process_improve.experiments.structures.Column(data=None, index=None, dtype=None, name=None, copy=None, fastpath=<no_default>)[source]#
Bases:
SeriesCreate a column. Can be used as a factor, or a response vector.
- 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.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=3, n_replicates=1, n_blocks=None, resolution=None, generators=None, alpha=None, cube='full', constraints=None, hard_to_change=None, model_type='interactions', fixed_runs=None, random_seed=42)[source]#
Generate an experimental design matrix.
- Parameters:
factors (list[Factor]) – Factor specifications. Each
Factorhas a name, type ("continuous","categorical","mixture"), low/high bounds (for continuous), levels (for categorical), and optional units.design_type (str or None) – One of
"full_factorial","fractional_factorial","plackett_burman","box_behnken","ccd","dsd","omars","omars_ilp","d_optimal","i_optimal","a_optimal","mixture","taguchi". IfNone, the design type is chosen automatically based on the factor count, budget, and constraints.budget (int or None) – Maximum number of runs the experimenter can afford.
n_center_points (int) – Number of center-point replicates (default 3). For designs that embed their own center points (CCD, Box-Behnken), this parameter controls the count within the design structure.
n_replicates (int) – Number of full replicates of the design (default 1 = no replication).
n_blocks (int or None) – Number of blocks.
resolution (int or None) – Desired minimum resolution for fractional factorials (III=3, IV=4, V=5).
generators (list[str] or None) – Explicit generators for fractional factorials, e.g.
["D=ABC", "E=AC"].Axial distance for CCD designs:
"rotatable","face_centered","orthogonal", or a numeric value.Note
A numeric
alphais only honored whencube="fractional". Forcube="full"(the default) the underlying pyDOE3ccdesigncall does not accept an arbitrary axial distance, so a numeric value is silently treated as"orthogonal".cube (str) – For CCD designs, how to build the cube (factorial) portion:
"full"(default) uses the complete 2^k factorial;"fractional"uses a resolution-V (or higher) fractional factorial, keeping the run count practical for k >= 5. When"fractional"and generators is given, those generators define the cube; otherwise a minimum-aberration half-fraction is chosen automatically.constraints (list[Constraint] or None) – Constraints on the factor space.
hard_to_change (list[str] or None) – Names of hard-to-change factors (triggers split-plot structure).
model_type (str) – Model the optimal designs (
"d_optimal","i_optimal","a_optimal") are built for:"main_effects","interactions"(default), or"quadratic". With a categorical factor present,"quadratic"builds a partial response-surface model (quadratics on the continuous factors only; the categorical enters as a main effect plus its interactions), since a categorical factor has no square. Ignored by the classical (non-optimal) design families.fixed_runs (pandas.DataFrame or None) – Runs to hold fixed while the optimizer fills the rest (design augmentation), for the optimal families only (
"d_optimal","i_optimal","a_optimal", which use pyoptex). One row per fixed run, one column per factor, in the same coding as the returned design: continuous factors in coded[-1, 1]units, categorical factors as level labels. The fixed runs occupy the first rows of the result andbudgetcounts them, sobudgetmust exceedlen(fixed_runs). A common use is to seed a centre point. RaisesValueErrorif given for a non-optimaldesign_type.random_seed (int) – Seed for reproducible randomization (default 42).
- Returns:
Contains
design(codedExpt),design_actual(actual-unitsExpt),run_order, and design metadata (generators, defining relation, resolution, etc.).- Return type:
DesignResult
- Raises:
ValueError – If design_type is unknown, or if factor/budget constraints cannot be satisfied.
Examples
>>> from process_improve.experiments import generate_design, Factor >>> factors = [ ... Factor(name="T", low=150, high=200, units="degC"), ... Factor(name="P", low=1, high=5, units="bar"), ... ] >>> result = generate_design(factors, design_type="full_factorial") >>> result.design_actual
Integer-programming generator for OMARS designs.
The constructive generator in process_improve.experiments.designs_omars
(dispatch_omars) only builds the minimal conference-foldover member of the
OMARS family (2k + 1 / 2k + 3 runs). That design is saturated for a
full second-order model, so process_improve.experiments.analyze_omars()
has no error degrees of freedom to work with. This module builds larger
OMARS designs that leave error degrees of freedom, by selecting runs with an
integer linear program (ILP).
Method#
Every design here is a foldover [H; -H; 0]: a half-design H, its
mirror image -H, and a single centre run. The foldover structure makes
three of the four OMARS-defining conditions hold automatically:
balance -
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 makes every pure quadratic estimable (each x_i^2 column
takes the value 0 there). The only condition that is not automatic is the
mutual orthogonality of the main effects, which is linear in the binary
“include this half-run” variables s_r: for each pair i < j,
sum_r (x[r,i] x[r,j]) s_r = 0. The run count is 2 * sum_r s_r + 1.
So the ILP selects a half-design from the (3**k - 1) / 2 distinct non-mirror
three-level runs subject to a handful of linear equalities - only k(k-1)/2
of them - which keeps it tractable up to seven factors. Because the
coefficients are integers, the equalities are exact; the floating-point
is_omars() re-check only guards against mistakes. A pure feasibility
solve, however, returns an arbitrary OMARS design that is usually far from the
most efficient member. To search for a high-quality design the solve is
repeated with random linear objectives (a multistart): each random objective
sends the solver to a different vertex of the feasibility polytope, so the
retained designs span the high-D-efficiency / low-A members. The best is then
chosen by a satisficing-and-dominance rule over D-efficiency and the maximum
second-order correlation, following the selection philosophy of Nunez Ares and
Goos (2020). This makes the generator competitive with their enumerated
catalogue without consulting it.
This realises, for OMARS designs, the integer-programming construction of Nunez Ares and Goos (2020); the ILP-over-design-points framing is shared with their trend-robust run-order work (Nunez Ares and Goos, 2019). An exhaustively enumerated OMARS catalogue exists but is unlicensed and is not redistributed here. Only the (dominant) foldover OMARS family is generated; the rarer non-foldover members are a documented future extension.
References
Nunez Ares, J. and Goos, P. (2020). “Enumeration and multicriteria selection of orthogonal minimally aliased response surface designs.” Technometrics, 62(1):21-36.
Nunez Ares, J. and Goos, P. (2019). “An integer linear programming approach to find trend-robust run orders of experimental designs.” Journal of Quality Technology.
- class process_improve.experiments.designs_omars_ilp.OmarsSearchReport(n_factors=0, half_pool_size=0, n_restarts=0, ilp_iterations=0, feasible_designs=0, run_size=0, total_solve_seconds=0.0, search_mode='', enumerated_designs=0)[source]#
Bases:
objectDiagnostics from the ILP search, recorded on
DesignResult.metadata.- Parameters:
- 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:
- 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:
- process_improve.experiments.designs_omars_ilp.solve_omars_ilp(half_pool, *, n_half=None, half_bounds=None, minimize_size=False, objective=None, exclude_solutions=None, solver_options=None)[source]#
Select a half-design from half_pool and return the foldover OMARS design.
Exactly one of n_half (exact half count) or half_bounds (inclusive
(min, max)half count) sets the size constraint. The returned design has2 * n_half + 1runs.- 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 to a different vertex of the OMARS-feasibility polytope, which is howgenerate_omars()samples diverse, high-quality designs. Takes precedence over minimize_size.exclude_solutions (list[list[int]], optional) – Previously found half-index sets to forbid via no-good cuts.
solver_options (dict, optional) –
{"msg": bool, "time_limit": int seconds}.
- Returns:
(design or None, solver_status, chosen_half_indices).Nonemeans the solver returned no feasible selection.- Return type:
- Raises:
ImportError – If PuLP (the
ilpextra) is not installed.
- process_improve.experiments.designs_omars_ilp.generate_omars(factors, *, n_runs=None, n_runs_range=None, selection_criterion='dominance', satisfice=None, center_runs=1, n_restarts=50, max_candidates=6, model='full_second_order', solver_options=None, tol=1e-09, random_seed=42, verify=True)[source]#
Generate a foldover OMARS design by exhaustive or integer-programming run selection.
Builds a three-level OMARS design large enough to leave error degrees of freedom for the chosen analysis model, so it can be analysed with
process_improve.experiments.analyze_omars(). The design is a foldover[H; -H; 0](half-runs, their mirrors, and a centre run) plus any further centre runs, 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:
factors (list[Factor]) – At least three continuous factors.
n_runs (int, optional) – Exact total run size of the returned design, centre runs included.
n_runs - center_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 smallest feasible size is used.selection_criterion ({"dominance", "d_efficiency", "min_second_order_correlation", "a_optimal"}) – How to choose among the feasible designs. When the design class at the chosen size is small enough (currently up to four factors at moderate sizes), the search enumerates it exhaustively and the selection is exact for the stated objective; the metadata reports
search_mode="exhaustive". Otherwise the criterion selects the best design among those found by the randomized multistart (search_mode="multistart"), which may miss the optimum."dominance"(default) keeps the Pareto front on D-efficiency and the maximum second-order correlation, then prefers the smallest, most efficient design."a_optimal"selects the design with the lowest summed coefficient 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, so it is never selected by"min_second_order_correlation"when an alternative with every term present exists.satisfice (dict, optional) – Acceptability thresholds applied before selection: a design is kept only if it clears every threshold. Supported keys are
"d_efficiency"(a minimum, higher is better) and"max_second_order_correlation"(a maximum, lower is better), for example{"d_efficiency": 5.0, "max_second_order_correlation": 0.7}. 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 drives the solver to a different feasible OMARS design; the best one (by selection_criterion) is kept. Higher values explore more of the feasible set and approach the catalogue-optimal designs more closely, at a roughly linear cost in runtime. The search early-stops once the feasible set stops yielding new designs, so small factor counts finish quickly regardless. Default 50, which reaches catalogue-competitive D-efficiency for up to seven factors. Deterministic for a fixed random_seed.
max_candidates (int, optional) – Legacy alias retained for backward compatibility. It now sets a floor on n_restarts (the effective restart budget is
max(n_restarts, max_candidates)), so calls that raised it to enumerate more designs still explore at least that many. Default 6.model ({"full_second_order", "main_quadratic"}, optional) – The analysis model the design is sized for.
"full_second_order"(default) leaves room for every two-factor interaction, so the smallest feasible design must 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) – Passed to the solver:
{"msg": bool, "time_limit": int seconds}.tol (float, optional) – Tolerance for the floating-point
is_omars()re-check.random_seed (int, optional) – Seed for both the randomized-objective search (which design is found) and the run-order randomisation of the returned design. A fixed seed makes the whole call reproducible.
verify (bool, optional) – When
True(default) every selected design is re-checked withis_omars()before it is accepted.
- Returns:
The OMARS design, with ILP provenance and search diagnostics under
metadata(family,sparsity,omars_searchreport, …).- Return type:
DesignResult
- Raises:
ValueError – If fewer than three factors are given, the factor count exceeds the combinatorial cap, model is not recognised, n_runs is too small or incompatible with center_runs, or no feasible design is found.
ImportError – If PuLP (the
ilpextra) is not installed.
Examples
>>> from process_improve.experiments import Factor, generate_omars, analyze_omars >>> factors = [Factor(name=n, low=-1, high=1) for n in "ABCDE"] >>> result = generate_omars(factors) >>> result.metadata["omars_verified"] True
Various factorial designs.
- process_improve.experiments.designs_factorial.full_factorial(nfactors, names=None)[source]#
Create a full factorial (2^k) design for the case when there are nfactors [integer] number of factors.
The optional list of names can be provided. The entries in the list should be strings. If not provided, the names will be created.
- Raises:
ValueError – If
nfactors < 1(the empty design is undefined) ornfactors > settings.max_factors_combinatorial(SEC-19 #268: a request for 2**40 rows is a memory-exhaustion attack, not a legitimate design).- Parameters:
- Return type:
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. 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 dataframe.
- Parameters:
drop_intercept (bool)
- Return type:
DataFrame
- get_factor_names(level=1)[source]#
Get the factors in a model which correspond to a certain level.
1 : pure factors 2 : 2-factor interactions and quadratic terms 3 : 3-factor interactions and cubic terms 4 : etc
- get_response_name()[source]#
Get the name of the response variable from the model specification.
- 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.995)[source]#
Create a linear model.
- 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.- 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
Design evaluation: quality metrics for experimental designs.
Provides evaluate_design(), which computes properties and quality metrics
of an existing design matrix. Supported metrics include efficiency values
(D/I/G), prediction variance, VIF, condition number, power analysis, alias
structure, confounding, resolution, defining relation, clear effects, minimum
aberration, moment aberration, and degrees of freedom.
Example
>>> from process_improve.experiments import evaluate_design, generate_design, Factor
>>> factors = [Factor(name="A", low=0, high=10), Factor(name="B", low=0, high=10)]
>>> result = generate_design(factors, design_type="full_factorial", n_center_points=0)
>>> metrics = evaluate_design(result, model="interactions", metric=["d_efficiency", "vif"])
- process_improve.experiments.evaluate.evaluate_design(design_matrix, model=None, metric='d_efficiency', effect_size=None, alpha=0.05, sigma=None, region='cuboidal', n_samples=100000, include_vertices=True, random_seed=42, fds_resolution=None)[source]#
Compute quality metrics for an experimental design.
- Parameters:
design_matrix (DataFrame or DesignResult) – The design to evaluate. If a
DesignResultis passed, the coded design matrix and any generator / defining-relation metadata are extracted automatically.model (str or None) – Model type:
"main_effects","interactions","quadratic", or an explicit patsy formula.Nonedefaults to"interactions".metric (str or list[str]) – One or more metric names to compute, or the special value
"all"to compute every metric. Valid names:"d_efficiency","i_efficiency","g_efficiency","a_optimality","e_optimality","correlation","alias_matrix","fds","prediction_variance","vif","condition_number","power","degrees_of_freedom","alias_structure","confounding","resolution","defining_relation","clear_effects","minimum_aberration","moment_aberration". The optimality-criterion metrics also accept the opposite suffix as an alias (e.g."d_optimality"for"d_efficiency","a_efficiency"for"a_optimality"); the result is keyed under the canonical name.effect_size (float or None) – Expected effect size for power calculation. When None, a power curve over a range of effect sizes is returned instead.
alpha (float) – Significance level for power calculation (default 0.05).
sigma (float or None) – Estimated noise standard deviation. Defaults to 1.0 when needed but not provided.
region ({"cuboidal", "spherical"}) – Design region over which the region-based metrics (
i_efficiency,g_efficiency,fds) integrate the prediction variance."cuboidal"(default) is[-1, 1]^k;"spherical"is the ball of radiussqrt(k).n_samples (int) – Number of random samples drawn over the region (default 100,000).
include_vertices (bool) – When True (default), all
2**kcube vertices are added to the region sample so the worst-case (G) value at a corner is represented.random_seed (int) – Seed for the region sampler (full reproducibility).
fds_resolution (int or None) – Resolution of the dense FDS curve. When None (default) the
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.
- Returns:
Results keyed by metric name. The structure of each value depends on the metric - see individual metric documentation.
- Return type:
Examples
>>> from process_improve.experiments import evaluate_design, generate_design, Factor >>> factors = [Factor(name="A", low=0, high=10), Factor(name="B", low=0, high=10)] >>> result = generate_design(factors, design_type="full_factorial", n_center_points=0) >>> metrics = evaluate_design(result, model="main_effects", metric="d_efficiency") >>> metrics["d_efficiency"] 100.0
- process_improve.experiments.evaluate.evaluate_all(design_matrix, model=None, effect_size=None, alpha=0.05, sigma=None, region='cuboidal', n_samples=100000, include_vertices=True, random_seed=42, fds_resolution=None)[source]#
Compute every available metric for a design in one call.
Thin convenience wrapper around
evaluate_design()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.
Experiment analysis: fit models, ANOVA, diagnostics, residuals.
Provides analyze_experiment(), the main analytical workhorse for
designed experiments (Tool 3 in the DOE tool architecture).
Uses statsmodels and scipy for the heavy lifting, with thin custom code for lack-of-fit, curvature test, Lenth’s method, pred-R², adequate precision, and confirmation run testing.
- process_improve.experiments.analysis.build_formula(response, factors, model=None)[source]#
Build a patsy/statsmodels formula string.
- 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='coded', new_points=None, observed_at_new=None, response_column=None)[source]#
Fit models, run ANOVA, compute effects, diagnose residuals.
- Parameters:
design_matrix (DataFrame) – Factor settings per run. May also contain the response column(s).
responses (DataFrame, Series, or None) – Response column(s). If None,
response_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.model (str or None) –
"main_effects","interactions","quadratic", an explicit formula, or None (defaults to"interactions").analysis_type (str or list[str]) – One or more of:
"anova","effects","coefficients","significance","residual_diagnostics","lack_of_fit","curvature_test","model_selection","box_cox","lenth_method","confidence_intervals","prediction","confirmation_test".significance_level (float) – Default 0.05.
transform (str or None) –
"log","sqrt","inverse","box_cox", orNone.coding (str, default
"coded") – Reserved for a future coded/actual factor-scale switch. Currently accepted for API stability but not consumed by the analysis.new_points (DataFrame or None) – For prediction or confirmation testing.
observed_at_new (list[float] or None) – Observed values at new_points (for confirmation testing).
response_column (str or None) – Name of the response column when it lives inside design_matrix.
- Returns:
Results keyed by analysis type. Always includes
"model_summary"with the keys:formula- the resolved patsy formula that was fitted.r_squared- R^2 of the fit.r_squared_adj- adjusted R^2.r_squared_pred- prediction R^2 (leave-one-out style).adequate_precision- signal-to-noise ratio (>= 4 is considered adequate).n_obs- number of observations used to fit.n_terms- number of columns in the model matrix.model_rank- numerical rank of the model matrix; less thann_termsimplies aliasing / rank deficiency.rank_deficient-Trueifmodel_rank < n_terms.df_model- model degrees of freedom.df_residual- residual degrees of freedom.mse_residual- mean squared error of the residuals.
- Return type:
Examples
>>> import pandas as pd >>> from process_improve.experiments.analysis import analyze_experiment >>> df = pd.DataFrame({ ... "A": [-1, 1, -1, 1], "B": [-1, -1, 1, 1], ... "y": [28, 36, 18, 31], ... }) >>> result = analyze_experiment(df, response_column="y", analysis_type="coefficients") >>> result["coefficients"][0]["term"] 'Intercept'
Design augmentation: extend or modify an existing experimental design.
Provides augment_design(), which takes an existing design matrix and
augments it by adding runs (foldover, semifold, center points, axial points,
D-optimal runs), upgrading to a response surface design, adding n_blocks, or
replicating.
Example
>>> import pandas as pd
>>> from process_improve.experiments.augment import augment_design
>>> design = pd.DataFrame({"A": [-1, 1, -1, 1], "B": [-1, -1, 1, 1]})
>>> result = augment_design(design, augmentation_type="add_center_points", n_additional_runs=3)
>>> pd.DataFrame(result["augmented_design"]).shape
(7, 2)
- process_improve.experiments.augment.augment_design(existing_design, augmentation_type, target_model=None, n_additional_runs=None, fold_on=None, alpha=None, generators=None)[source]#
Extend or modify an existing experimental design.
- Parameters:
existing_design (DataFrame) – The current design matrix with factor columns in coded units (-1/+1).
augmentation_type (str) – One of
"foldover","semifold","add_center_points","add_axial_points","add_runs_optimal","upgrade_to_rsm","add_blocks","replicate".target_model (str or None) – Desired model after augmentation:
"main_effects","interactions","quadratic". Used by"add_runs_optimal"and"upgrade_to_rsm".n_additional_runs (int or None) – Budget for additional runs. Interpretation depends on the augmentation type (number of center points, number of D-optimal runs, number of blocks, …). For
"replicate", this is the number of complete copies of the existing design that are appended (each copy 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.
- Returns:
Keys include
"augmented_design"(list of dicts),"new_runs"(list of dicts),"n_runs_before","n_runs_after","explanation"(narrative),"before_metrics","after_metrics", and augmentation-specific keys.- Return type:
- Raises:
ValueError – If augmentation_type is unknown, or if required parameters are missing for the requested augmentation.
Examples
>>> import pandas as pd >>> from process_improve.experiments.augment import augment_design >>> design = pd.DataFrame({ ... "A": [-1, 1, -1, 1, -1, 1, -1, 1], ... "B": [-1, -1, 1, 1, -1, -1, 1, 1], ... "C": [-1, -1, -1, -1, 1, 1, 1, 1], ... }) >>> result = augment_design(design, "add_center_points", n_additional_runs=3) >>> result["n_runs_after"] 11
Response optimization for designed experiments (Tool 4).
Find optimal factor settings for one or multiple responses after fitting
a model with analyze_experiment() (Tool 3).
Implemented methods#
desirability - Derringer-Suich desirability functions (single and multi-response) with
scipy.optimize.minimize(SLSQP).steepest_ascent / steepest_descent - Move along the gradient of a first-order model from the design centre.
stationary_point - Locate the stationary point of a second-order model via
numpy.linalg.solve.canonical_analysis - Eigenvalue decomposition of the B matrix to classify the stationary point (max / min / saddle).
ridge_analysis - Trace the constrained optimum along spheres of increasing radius, by solving Draper’s secular equation for the Lagrange multiplier.
pareto_front - The non-dominated set over several responses, via augmented Chebyshev scalarisation on a Das-Dennis weight lattice.
- process_improve.experiments.optimization.evaluate_model(coefficients, factor_names, point)[source]#
Evaluate predicted response at an arbitrary coded point.
- process_improve.experiments.optimization.optimize_responses(fitted_models, goals=None, method='desirability', factor_ranges=None, step_size=0.5, n_steps=10, response_importance=None, fitted_results=None, significance_level=0.05, search_bounds=None, desirability_weights=None, ridge_direction='maximize', n_pareto_points=21)[source]#
Find optimal factor settings for one or multiple responses.
- Parameters:
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.
Per-response optimisation goals. Each dict has keys:
"response"(str) - response name. Matched against each model’s"response_name"; when both sides name their responses the goals are reordered to match, so the two lists need not be in the same order. When either side omits a name, goals are taken in list order."goal"(str) -"maximize","minimize", or"target"."target"(float, optional) - target value (required 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.step_size (float) – Step magnitude for steepest ascent/descent (coded units).
n_steps (int) – Number of steps along a path: the steepest ascent/descent steps, or the radii reported by ridge analysis over and above the centre.
response_importance (list[float] or None) – Relative importance per response, overriding the per-goal
"importance"values. Aligned with fitted_models.fitted_results (list or None) – Optional statsmodels results objects, one per entry in fitted_models and in the same order, as returned by
lm()or 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.That default suits a two-level design but understates a central composite design, whose axial runs sit at plus or minus alpha: leaving it at the cube would refuse to consider settings the experiment actually covered. Pass
(-1.41, 1.41)for a two-factor rotatable central composite design, or a mapping such as{"T": (-1.41, 1.41)}to widen one factor only. Factors left out of a mapping keep the (-1, 1) default.desirability_weights (list[float] or None) – Deprecated alias for response_importance. The name was misleading: these values are importances, not the
weightthat shapes an individual ramp.ridge_direction ({"maximize", "minimize"}) – Which ridge
method="ridge_analysis"traces.n_pareto_points (int) – Target number of weight vectors for
method="pareto_front". The front returned is usually smaller, since dominated and duplicate solutions are dropped.
- Returns:
Results keyed by method. Always includes
"method"and"factor_names".- Return type:
- Raises:
ValueError – If method is unknown, if fitted_models is empty, if a method that needs goals is called without them, or if both response_importance and desirability_weights are given.
Examples
>>> from process_improve.experiments.optimization import optimize_responses >>> model = { ... "response_name": "yield", ... "coefficients": [ ... {"term": "Intercept", "coefficient": 40.0}, ... {"term": "A", "coefficient": 5.25}, ... {"term": "B", "coefficient": -2.0}, ... {"term": "I(A ** 2)", "coefficient": -3.0}, ... {"term": "I(B ** 2)", "coefficient": -1.5}, ... {"term": "A:B", "coefficient": 1.5}, ... ], ... "factor_names": ["A", "B"], ... } >>> result = optimize_responses( ... fitted_models=[model], ... method="stationary_point", ... ) >>> result["stationary_point"]["classification"] 'maximum'
The fractional-factorial trade-off: n_runs against n_factors.
The central question when screening many n_factors is how few n_runs you can get
away with, and what you pay for that saving. This module answers it in two
ways, mirroring the R pid package:
get_trade_off_table_entry()reports, for one (runs, factors) pair, the design’s resolution, its generators, its defining relation, and which effects end up aliased with which.trade_off_table()prints the whole grid at once, the Python counterpart of the trade-off table figure in the course notes.
Unlike the R version, which looks designs up in the FrF2 catalogue, the
generators here are derived by a minimum-aberration search: for a given
number of n_runs and n_factors, every admissible set of generators is scored on
its word-length pattern and the best is kept. The search reproduces the table
in the course notes exactly, and extends past its printed edge.
Also see#
process_improve.experiments.designs.generate_design : builds the design matrix. process_improve.experiments.evaluate.evaluate_design : evaluates one you have.
- class process_improve.experiments.trade_off.TradeOffTableEntry(n_runs, n_factors, n_generators, resolution, roman, generators=<factory>, defining_relation=<factory>, aliases=<factory>, n_replicates=1, label='')[source]#
Bases:
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 n_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:
- 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 n_factors to interaction columns of the base factorial is enumerated, scored by its word-length pattern, and the best-scoring one is returned. Ties are broken in favour of the generator set that comes first by word length and then alphabetically.- Parameters:
- 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 n_factors, or if the search space is too large to enumerate.
Examples
>>> minimum_aberration_generators(8, 5) ('D=AB', 'E=AC') >>> minimum_aberration_generators(16, 5) ('E=ABCD',)
Notes
A minimum-aberration design is unique only up to relabelling the factors, so a textbook may print a different but equivalent set of generators.
- process_improve.experiments.trade_off.get_trade_off_table_entry(n_runs=8, n_factors=7, display=True)[source]#
Report the resolution, generators and aliasing at a (runs, factors) pair.
Answers the screening question “if I can afford n_runs experiments and I want to study n_factors n_factors, what do I lose?”. The loss is aliasing: effects that the design cannot tell apart.
- Parameters:
- 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 n_factors cannot fit into the run budget.
Examples
>>> result = get_trade_off_table_entry(n_runs=8, n_factors=5, display=False) >>> result.label '2^(5-2) III' >>> result.generators ['D=AB', 'E=AC']
A run budget larger than the full factorial needs is reported as replication rather than as an error:
>>> get_trade_off_table_entry(n_runs=16, n_factors=3, display=False).label '2^3 (twice)'
Also see#
trade_off_table : the same information for a whole grid of designs.
- process_improve.experiments.trade_off.trade_off_table(runs=(4, 8, 16, 32, 64), factors=(3, 4, 5, 6, 7, 8, 9), display=True)[source]#
Return the runs-against-factors trade-off table.
The Python counterpart of R’s
tradeOffTable(), which displays the table as a static image. Here it is computed, so it can be widened past the printed edge and the cells can be read programmatically.Reading the table: going down a column costs more experiments but buys resolution; going across a row studies more factors for the same money, at the cost of heavier aliasing. Blank cells are designs that do not exist (too many factors for that many runs).
- Parameters:
runs (Sequence[int], default (4, 8, 16, 32, 64)) – Run budgets, one per row. Each must be a power of two.
factors (Sequence[int], default (3, 4, 5, 6, 7, 8, 9)) – Factor counts, one per column.
display (bool, default True) – Print the table as well as returning it. Set to
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.- Return type:
pd.DataFrame
Examples
>>> table = trade_off_table() >>> table.loc[8, 5] '2^(5-2) III' >>> table.loc[16, 4] '2^4 (full)'
Also see#
get_trade_off_table_entry : generators and alias chains for a single cell of this table.
The OMARS trade-off: which model does a given run budget buy.
The two-level trade-off table in process_improve.experiments.trade_off
answers “I can afford N runs and want k factors, what do I give up?” with a
resolution and an alias structure. That currency does not transfer to OMARS
designs, because an OMARS design always has its main effects orthogonal to
each other and to every second-order term. Resolution is constant, so it cannot
be what the table reports.
What varies instead is which model is estimable at all, and that is set by
the foldover structure. A foldover is [H; -H; 0], and every second-order
term is an even function, so the quadratic and interaction columns of H
and -H are identical. The even block therefore has at most h + 1
distinct rows, against 1 + k(k+1)/2 columns for the full second-order model.
The main effects live in the odd block and contribute at most k more, so for
every foldover
with equality for half-designs in general position. Three capability classes follow, and they are the OMARS analogue of resolution:
FullN >= k^2 + k + 1. Main effects, pure quadratics and every two-factor interaction are jointly estimable, so a response surface can be fitted without a follow-up design.QuadN >= 2k + 3. Main effects and the pure quadratics, with degrees of freedom left to test them, so curvature can be judged factor by factor. The two-factor interactions are present in the design but not in the model.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 number here is closed-form, so the table is instant and exact: no integer
program, no solver, and no dependence on a search budget. The quality of a
particular design at a given size (its D-efficiency, its second-order
correlations) does need the ILP, and lives on
generate_omars() instead.
Also see#
process_improve.experiments.trade_off : the two-level counterpart. process_improve.experiments.designs_omars_ilp.generate_omars : build the design. process_improve.experiments.omars.analyze_omars : the staged analysis it feeds.
- process_improve.experiments.omars_trade_off.CAPABILITIES: tuple[str, ...] = ('full', 'quad', 'satd')#
Capability classes, best first. The four-character tags line up in a table, and sort alphabetically in decreasing order of capability.
- process_improve.experiments.omars_trade_off.DEFAULT_RUNS: tuple[int, ...] = (9, 13, 17, 21, 25, 31, 37, 43, 57)#
odd run counts spanning the useful band for three to seven factors. The last one is the smallest budget at which every one of those factor counts reaches
Full.- Type:
Default rows
- process_improve.experiments.omars_trade_off.DEFAULT_FACTORS: tuple[int, ...] = (3, 4, 5, 6, 7)#
Default columns.
- class process_improve.experiments.omars_trade_off.OmarsTradeOffTableEntry(n_runs, n_factors, exists, capability, tag, model, model_params, error_df, label, min_runs_full, min_runs_quad, min_runs_satd, reason='')[source]#
Bases:
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 an even n_runs or one below2k + 1.- Type:
- process_improve.experiments.omars_trade_off.omars_minimum_runs(n_factors, capability='full')[source]#
Return the smallest run count reaching capability for n_factors.
- Parameters:
n_factors (int) – Number of factors,
k, between 3 and 25.capability ({"full", "quad", "satd"}, default "full") – Which class to reach. See the module docstring.
- Returns:
The (odd) run count:
k^2 + k + 1for"full",2k + 3for"quad",2k + 1for"satd".- 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") 13 >>> [omars_minimum_runs(k) for k in (3, 4, 5, 6, 7)] [13, 21, 31, 43, 57]
- process_improve.experiments.omars_trade_off.get_omars_trade_off_table_entry(n_runs, n_factors, display=True)[source]#
Report which model a run budget buys, for a foldover OMARS design.
The OMARS counterpart of
get_trade_off_table_entry(). Because OMARS main effects are always clear of the second-order terms, the answer is not a resolution: it is which model is estimable, and how much is left over to test it with.- Parameters:
- 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'
Also see#
omars_trade_off_table : the same answer across a grid of budgets.
- process_improve.experiments.omars_trade_off.omars_trade_off_table(runs=(9, 13, 17, 21, 25, 31, 37, 43, 57), factors=(3, 4, 5, 6, 7), display=True)[source]#
Return the run-budget against factor-count table for OMARS designs.
Each cell says which model that budget supports and how much error is left to test it with, for example
"Full df=11". Blank cells are budgets that are not a foldover design at all.- Parameters:
runs (sequence of int, default
DEFAULT_RUNS) – Run budgets, one per row. Even values are always blank.factors (sequence of int, default
DEFAULT_FACTORS) – Factor counts, one per column.display (bool, default True) – Print the table left-aligned, with the
df=label written once per column rather than in every cell.
- Returns:
Rows indexed by run count, columns by factor count. Cells are self-contained labels; the once-per-column compression applies only to the printed view.
- Return type:
pd.DataFrame
Examples
>>> table = omars_trade_off_table(display=False) >>> table.loc[21, 4] 'Full df=6' >>> table.loc[9, 3] 'Quad df=2'
Also see#
get_omars_trade_off_table_entry : the detail behind one cell.
Teaching simulators used in the Process Improvement using Data course.
Each function here is a small, deliberately opaque process that a student
drives with a designed experiment. They are ports of the simulators in the
companion R package (pid): popcorn(), grocery() and
manufacture().
All three add random noise to the response, so that repeated runs at the same
settings do not give an identical answer. Pass random_state to make a run
reproducible; leave it at None (the default) for fresh noise on every call,
which is what the classroom exercise expects.
Every simulator refuses vector input on purpose: the point of the exercise is sequential experimentation, one run at a time, with the fewest number of runs.
- process_improve.experiments.simulations.popcorn(t=120, T=None, *, random_state=None)[source]#
Simulate stovetop popcorn cooking.
Returns the number of popped kernels after cooking a fixed set of kernels, at the same heat setting on the stove, for t seconds. There is only one factor: the cooking time.
- Parameters:
t (float, default 120) – Number of seconds the pot is left on the stove. Cooking times less than 77 seconds are not supported: nothing has popped yet. A vector (list or array) of time values is not permitted, since the goal is to perform sequential experimentation to determine the optimum time, with the fewest number of function calls.
T (float or None, default None) – Alias for t, matching the
popcorn(T=...)spelling used in the R package. When given, it overrides t.random_state (int, np.random.Generator, or None, default None) – Seed or generator for the noise term.
None(the default) draws fresh noise on every call, which is the intended behaviour for the classroom exercise; pass 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, or is below 77 seconds.
Examples
>>> popcorn(t=135, random_state=13) 94
Source#
Kevin Dunn, Process Improvement using Data, Chapter 5, 2010 to 2026, https://learnche.org/pid
Also see#
grocery manufacture
- process_improve.experiments.simulations.grocery(p=3.46, h=150, P=None, H=None, *, random_state=None)[source]#
Simulate grocery store profits for a single product.
The hourly profit made when selling the product at price p and the product is displayed at height h [cm up from the ground] on the shelf.
Simulates a grocery store profit function where there are 2 factors:
p = selling price of the product, measured in dollars and cents
h = height of the product on the shelf, measured in centimeters above the ground.
Typical values are p = $3.50 and h = 150cm. The outcome is the profit made per hour [dollars/hour], with random noise added, for realism.
- Parameters:
p (float, default 3.46) – Selling price of the product [dollars].
h (float, default 150) – Height of the product on the shelf [cm above the ground].
P (float or None, default None) – Alias for p, matching the
grocery(P=...)spelling used in the R package. When given, it overrides p.H (float or None, default None) – Alias for h. When given, it overrides h.
random_state (int, np.random.Generator, or None, default None) – Seed or generator for the noise term.
None(the default) draws fresh noise on every call; pass 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 finite, or is negative.
Source –
------ –
Kevin Dunn, Process Improvement using Data, Chapter 5, 2010 to 2026, –
https://learnche.org/pid –
Also see –
-------- –
popcorn –
manufacture –
- process_improve.experiments.simulations.manufacture(p=0.75, t=325, P=None, T=None, *, random_state=None)[source]#
Simulate the hourly profit of a manufacturing facility.
Two factors affect the outcome:
p = selling price of the product, measured in dollars and cents
t = throughput (production rate) of the process, in parts per hour
Typical values are p = $0.75 and t = 325 parts per hour. The outcome is the profit made per hour [dollars/hour], with random noise added for realism. The aim of the exercise is to maximize that profit.
- Parameters:
p (float, default 0.75) – Selling price of the product [dollars].
t (float, default 325) – Throughput (production rate) of the process [parts per hour].
P (float or None, default None) – Alias for p, matching the
manufacture(P=...)spelling used in the R package. When given, it overrides p.T (float or None, default None) – Alias for t. When given, it overrides t.
random_state (int, np.random.Generator, or None, default None) – Seed or generator for the noise term.
None(the default) draws fresh noise on every call; pass 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 finite, or is negative.
Examples
>>> manufacture(p=1.5, t=320, random_state=42) 601
Source#
Kevin Dunn, Process Improvement using Data, Chapter 5, 2010 to 2026, https://learnche.org/pid
Also see#
grocery popcorn
- process_improve.experiments.datasets.distillateflow()[source]#
Return the flow rate of distillate from the top of a distillation column.
These are actual data, taken 1 minute apart in time, of the flow rate leaving the top of a continuous distillation column (data are from a 31 day period in time). The data are fetched from the canonical hosted location on openmv.net rather than bundled with the package.
Dimensions#
A data frame containing 44640 observations of 1 variable.
Source#
- Return type:
DataFrame
- process_improve.experiments.datasets.pollutant()[source]#
Return water treatment example data from BHH2, Ch 5, Question 19.
Description#
The data are from the first 8 rows of the pollutant water treatment example n the book by Box, Hunter and Hunter, 2nd edition, Chapter 5, Question 19.
The 3 factors (C, T, and S) are in coded units where: C = -1 is chemical brand A; C = +1 is chemical brand B T = -1 is 72F for treatment temperature; T = +1 is 100F for the temperature S = -1 is No stirring; S = +1 is with fast stirring
The outcome variable is: y = the pollutant amount in the discharge [lb/day].
The aim is to find treatment conditions that MINIMIZE the amount of pollutant discharged each day, where the limit is 10 lb/day.
Dimensions#
A data frame containing 8 observations of 4 variables (C, S, T and y).
Source#
Box, G. E. P. and Hunter, J. S. and Hunter, W. G.r, Statistics for Experimenters, Wiley, 2nd edition, Chapter 5, Question 19, page 232.
- Return type:
DataFrame
- process_improve.experiments.datasets.oildoe()[source]#
Return industrial designed experiment data to improve the volumetric heat capacity of a product.
Description#
Four materials: A, B, C and D are added in a blend to achieve a desired heat capacity, the response variable, y.
The amounts were varied in a factorial manner for the 4 materials.
The data are scaled and coded for confidentiality. All that may be disclosed is that variable C is either added (“Yes”) or not added not added (“No”). The data are fetched from the canonical hosted location on openmv.net rather than bundled with the package.
Dimensions#
A data frame containing 19 observations of 5 variables (A, B, C, D, and the response, y).
Source#
http://openmv.net/info/oil-company-doe Data from a confidential industrial source.
- Return type:
DataFrame
- process_improve.experiments.datasets.golf()[source]#
Return full factorial experiment data to maximize a golfer’s driving distance.
A full factorial experiment with four factors run by a golf enthusiast. The objective of the experiments was for the golfer to maximize her driving distance at a specific tee off location on her local golf course. The golfer considered the following factors:
H = Tee height (cm) N = Holes: number of golf balls played for prior to experimental tee shot C = Club type T = Time of day (on the 24 hour clock)
The data are in standard order, however the actual experiments were run in random order.
Coded values for H, N, C and T should be used in the linear regression model analysis, with -1 representing the low value and +1 the high value.
Dimensions#
A data frame containing 16 observations of 4 variables (H, N, C, T) and a column y, as a response variable. C and T are stored as text labels (
"Callaway"/"Titleist"and"9:00"/"14:00"); code them to -1 / +1 before fitting a linear model.Source#
A MOOC on Design of Experiments, “Experimentation for Improvement”, https://learnche.org
- Return type:
DataFrame
- process_improve.experiments.datasets.boilingpot()[source]#
Return full factorial experiment data for stove-top boiling of water.
Description#
The data are from boiling water in a pot under various conditions. The response variable, y, is the time taken, in minutes to reach 90 degrees Celsius. Accurately measuring the time to actual boiling is hard, hence the 90 degrees Celsius point is used instead.
Three factors are varied in a full factorial manner (the first 8 observations). The data are in standard order, however the actual experiments were run in random order. The last 3 rows are runs close to, or interior to the factorial.
Factors varied were:
A = Amount of water: low level was 500 mL, and high level was 600 mL B = Lid off (low level) or lid on (high level) C = Size of pot used: low level was 2 L, and high level was 3 L.
Coded values for A, B and C should be used in the linear regression model analysis, with -1 representing the low value and +1 the high value.
Dimensions#
A data frame containing 11 observations of 4 variables (A, B, C, with y as a response variable.
Source#
MOOC on Design of Experiments, “Experimentation for Improvement”, https://learnche.org
- Return type:
DataFrame
- process_improve.experiments.datasets.solar()[source]#
Return solar panel example data from Box, Hunter and Hunter, 2nd edition, Chapter 5, page 230.
Description#
The data are from a solar panel simulation case study.
The original source that Box, Hunter and Hunter used is https://www.sciencedirect.com/science/article/abs/pii/0038092X67900515
A theoretical model for a commercial system was made. A 2^4 factorial design was used (center point is not included in this dataset).
The factors are dimensionless groups (https://en.wikipedia.org/wiki/Dimensionless_quantity), related to:
A = total daily insolation, B = the tank capacity, C = the water flow through the absorber, D = solar intermittency coming in.
All 4 factors are coded as -1 for the low level, and +1 for the high lever.
The responses variables are y1: collection efficiency, and y2: the energy delivery efficiency.
Dimensions#
A data frame containing 16 observations of 6 variables (A, B, C, D, with y1 and y2 as responses.)
Source#
Box, G. E. P. and Hunter, J. S. and Hunter, W. G., Statistics for Experimenters, 2nd edition, Wiley, Chapter 5, page 230.
- Return type:
DataFrame
- process_improve.experiments.datasets.data(dataset)[source]#
Return the
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
"boilingpot","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.
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
Pydantic models for the DOE strategy recommender.
Defines the input specification (DOEProblemSpec), the output
(ExperimentalStrategy, ExperimentalStage, TransitionRule),
and supporting types (DomainType, PriorKnowledge).
- class process_improve.experiments.strategy.models.DomainType(value)[source]#
-
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>)[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) – Design type key, e.g.
"plackett_burman","ccd","bbd".design_params (dict) – Design-specific parameters (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.
- transition_rules: list[TransitionRule]#
- model_config = {}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class process_improve.experiments.strategy.models.ExperimentalStrategy(*, strategy_id='', stages=<factory>, total_estimated_runs=0, budget_allocation=<factory>, assumptions=<factory>, risks=<factory>, alternative_strategies=<factory>, domain='general', detail_level='intermediate', reasoning=<factory>)[source]#
Bases:
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.
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".
- 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.
- 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.
- 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: