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.
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, center_points=3, replicates=1, blocks=None, resolution=None, generators=None, alpha=None, cube='full', constraints=None, hard_to_change=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.
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.
replicates (int) – Number of full replicates of the design (default 1 = no replication).
blocks (int or None) – Number of blocks.
resolution (int or None) – Desired minimum resolution for fractional factorials (III=3, IV=4, V=5).
generators (list[str] or None) – Explicit generators for fractional factorials, e.g.
["D=ABC", "E=AC"].alpha (str, float, or None) – Axial distance for CCD designs:
"rotatable","face_centered","orthogonal", or a numeric value.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).
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)[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:
- 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 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]with2*h + 1runs (an odd run count). 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 (odd) run size. 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)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 found.
"dominance"(default) keeps the Pareto front on D-efficiency and the maximum second-order correlation, then prefers the smallest, most efficient design."a_optimal"minimises the 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.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). 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 even, 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.
- 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, 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", 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".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", 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:
objectContainer returned by
analyze_experiment().Holds the fitted OLS result and all requested analysis outputs.
- 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.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) –
"coded"or"actual".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 R², adj-R², pred-R², and adequate precision.- 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 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)
>>> 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 replicates, or number of blocks).
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).
Stubs (not yet implemented)#
ridge_analysis - Trace the optimum along increasing radii.
pareto_front - Multi-objective Pareto frontier (NSGA-II).
- 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, desirability_weights=None)[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 (must match a model)."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) - desirability shape parameter."importance"(float, default 1) - relative importance for composite desirability.
method (str) – Optimisation method:
"desirability","steepest_ascent","steepest_descent","stationary_point","canonical_analysis","ridge_analysis"(stub),"pareto_front"(stub).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 for steepest ascent/descent.
desirability_weights (list[float] or None) – Importance weights for composite desirability (overrides per-goal
"importance"values).
- Returns:
Results keyed by method. Always includes
"method"and"factor_names".- Return type:
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'
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, 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, 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, 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).
center_points (int) – Center points for RSM design.
- Returns:
Keys:
"screening","optimization","confirmation","total","ideal_total","is_tight","warnings".- Return type: