Skip to content

Fractional Factorial

fractional_factorial

Fractional Factorial classical Design of Experiments (DoE) matrices.

This module provides the FractionalFactorialDesign class, which constructs design matrices containing a fraction of the full factorial combination (denoted \(2^{k-p}\)). It leverages design generators to allocate trials efficiently, presenting resolution characteristics and confounding/alias structures.

CLASS DESCRIPTION
FractionalFactorialDesign

Generates fractional factorial experimental designs, showing resolution and aliasing.

FractionalFactorialDesign

FractionalFactorialDesign(
    factors: dict, generator_string: str
)

Bases: DesignMatrix

Generates fractional factorial experimental designs, showing resolution and aliasing.

In a fractional factorial design (represented as \(2^{k-p}\)), only a subset of size \(2^{k-p}\) of the \(2^k\) full combinations is run, where \(p\) is the number of "generators" used to define columns for fractional factors. This is highly efficient for screening a large number of factors using a small number of runs, under the Sparsity of Effects principle (which states that system response is primarily governed by main effects and low-order interactions).

Mathematical Background and Sizing
  • Coded Notation: Let there be \(k\) factors. We select a subset of \(k - p\) "base factors" which are run as a standard \(2^{k-p}\) full factorial design.
  • Generators: The remaining \(p\) factors are generated by multiplying columns of the base factors. For example, in a \(2^{5-1}\) design (\(k=5, p=1\)), the fifth factor \(E\) can be generated as: $$ E = A \times B \times C \times D \quad (\text{or } E = ABCD) $$ The defining relation of the design is then: $$ I = ABCDE $$
  • Resolution:
  • Resolution III: Main effects are confounded with 2-way interactions. (e.g., \(I = ABD\)).
  • Resolution IV: Main effects are confounded with 3-way interactions, but 2-way interactions are confounded with other 2-way interactions. (e.g., \(I = ABCD\)).
  • Resolution V: Main effects are confounded with 4-way interactions, and 2-way interactions are confounded with 3-way interactions. No main effect or 2-way interaction is confounded with any other main effect or 2-way interaction. (e.g., \(I = ABCDE\)).

Confounding/Alias Structure Algorithm: To calculate the alias structure, we multiply every effect column by the defining relation. Since \(A \times A = I\) (identity): $$ A \times I = A \times ABCDE \implies A = BCDE $$ Thus, the estimate of factor \(A\)'s effect is confounded with the 4-way interaction \(BCDE\).

Pseudocode for the Algorithm
function generate_fractional_factorial(factors, generator_string):
    1. Parse generator_string (e.g., "E = ABCD, F = BCD").
    2. Identify base factors (e.g., A, B, C, D).
    3. Generate full factorial matrix of size 2^(k-p) for base factors.
       Map levels to coded values [-1, +1].
    4. For each generator "X = Y1*Y2*...":
         Create column X by taking the row-wise product of columns Y1, Y2, ...
    5. Re-scale coded [-1, +1] columns back to the actual factor level physical units.
    6. Return DataFrame.
ATTRIBUTE DESCRIPTION
generator_string

Defining relation generator string, such as "E=ABCD" or "E=AB, F=AC".

TYPE: str

Examples:

Example
>>> # Planning a 2^{5-1} resolution V design
>>> # Base factors are A, B, C, D. E is generated as ABCD.
>>> factors = {
...     "A": [10, 20], "B": [0, 1], "C": [-1, 1], "D": [5, 10], "E": [100, 200]
... }
>>> design = FractionalFactorialDesign(factors, generator_string="E = ABCD")
>>> # In the generated DataFrame, we will have 2^(5-1) = 16 runs instead of 32.
PARAMETER DESCRIPTION
factors

Mapping of factor labels to their designated low/high levels.

TYPE: dict

generator_string

Design generator equations, e.g. "E = AB, F = BC".

TYPE: str

METHOD DESCRIPTION
generate

Generates the fractional factorial design matrix.

Source code in src\xpyrment\design\doe\fractional_factorial.py
def __init__(self, factors: dict, generator_string: str):
    """Initializes a FractionalFactorialDesign.

    Args:
        factors (dict): Mapping of factor labels to their designated low/high levels.
        generator_string (str): Design generator equations, e.g. `"E = AB, F = BC"`.
    """
    super().__init__(factors)
    self.generator_string = generator_string

    for factor_name, levels in self.factors.items():
        if len(levels) != 2:
            raise ValueError(
                f"Fractional Factorial designs strictly require exactly 2 levels per factor. "
                f"Factor '{factor_name}' has {len(levels)} levels."
            )

generate

generate() -> DataFrame

Generates the fractional factorial design matrix.

Processes the generator string to construct base columns and applies multiplicative transformations to compute fractional factors.

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: A pandas DataFrame containing the fractional factorial design matrix, using physical factor level units.

Source code in src\xpyrment\design\doe\fractional_factorial.py
def generate(self) -> pd.DataFrame:
    """Generates the fractional factorial design matrix.

    Processes the generator string to construct base columns and applies multiplicative
    transformations to compute fractional factors.

    Returns:
        pd.DataFrame: A pandas DataFrame containing the fractional factorial design matrix,
            using physical factor level units.
    """
    import itertools

    # Parse generator equations (e.g. "E = ABCD, F = BCD" or "E = A * B * C * D")
    generators = {}
    for equation in self.generator_string.split(","):
        if "=" not in equation:
            continue
        lhs, rhs = equation.split("=")
        lhs = lhs.strip()
        rhs_cleaned = rhs.replace(" ", "").replace("*", "")

        # Find which of the defined factors are components of this generator
        components = []
        for char in rhs_cleaned:
            if char in self.factors:
                components.append(char)
        generators[lhs] = components

    # Identify base factors (factors that are not generated)
    base_keys = [k for k in self.factors.keys() if k not in generators]
    if not base_keys:
        raise ValueError("All factors cannot be generated; some base factors are required.")

    # Create base full factorial in coded space [-1.0, 1.0]
    combinations = list(itertools.product([-1.0, 1.0], repeat=len(base_keys)))
    coded_df = pd.DataFrame(combinations, columns=base_keys)

    # Compute generated columns row-wise
    for lhs, components in generators.items():
        if not components:
            raise ValueError(f"No valid factors found in generator expression for '{lhs}'")

        # Start with the first component
        col_val = coded_df[components[0]].copy()
        for comp in components[1:]:
            col_val *= coded_df[comp]
        coded_df[lhs] = col_val

    # Align columns to the original factors ordering
    coded_df = coded_df[list(self.factors.keys())]

    # Map coded space [-1.0, 1.0] coordinates to actual physical bounds
    physical_df = pd.DataFrame()
    for col in self.factors.keys():
        low, high = self.factors[col]
        mid = (low + high) / 2.0
        half_range = (high - low) / 2.0
        physical_df[col] = mid + coded_df[col] * half_range

    return physical_df