---
title: Full and Fractional Factorial Designs
jupyter: python3
---


BoFire can be used to setup full (two level) and fractional factorial designs (https://en.wikipedia.org/wiki/Fractional_factorial_design). This tutorial notebook shows how.


## Imports and helper functions

```{python}
#| papermill: {duration: 4.584683, end_time: '2024-10-10T20:36:14.685114', exception: false, start_time: '2024-10-10T20:36:10.100431', status: completed}
#| tags: []
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

import bofire.strategies.api as strategies
from bofire.data_models.domain.api import Domain
from bofire.data_models.features.api import CategoricalInput, ContinuousInput
from bofire.data_models.strategies.api import FractionalFactorialStrategy
from bofire.utils.doe import get_alias_structure, get_confounding_matrix, get_generator


def plot_design(design: pd.DataFrame):
    # we do a plot with three subplots in one row in which the three degrees of freedom (temperature, time and ph) are plotted
    _, axs = plt.subplots(1, 3, figsize=(15, 5))
    axs[0].scatter(design["temperature"], design["time"])
    axs[0].set_xlabel("Temperature")
    axs[0].set_ylabel("Time")
    axs[1].scatter(design["temperature"], design["ph"])
    axs[1].set_xlabel("Temperature")
    axs[1].set_ylabel("pH")
    axs[2].scatter(design["time"], design["ph"])
    axs[2].set_xlabel("Time")
    axs[2].set_ylabel("pH")
    plt.show()
```

## Setup the problem domain

The designs are generated for a simple three dimensional problem comprised of three continuous factors/features.

```{python}
#| papermill: {duration: 0.007239, end_time: '2024-10-10T20:36:14.698521', exception: false, start_time: '2024-10-10T20:36:14.691282', status: completed}
#| tags: []
domain = Domain(
    inputs=[
        ContinuousInput(key="temperature", bounds=(20, 80)),
        ContinuousInput(key="time", bounds=(60, 120)),
        ContinuousInput(key="ph", bounds=(7, 13)),
    ],
)
```

## Setup a full factorial design

Here we setup a full two-level factorial design including a center point and plot it.

```{python}
#| papermill: {duration: 0.193231, end_time: '2024-10-10T20:36:14.897979', exception: false, start_time: '2024-10-10T20:36:14.704748', status: completed}
#| tags: []
strategy_data = FractionalFactorialStrategy(
    domain=domain,
    n_center=1,  # number of center points
    n_repetitions=1,  # number of repetitions, we do only one round here
)
strategy = strategies.map(strategy_data)
design = strategy.ask()
display(design)

plot_design(design=design)
```

The confounding structure is shown below, as expected for a full factorial design, no confound is present.

```{python}
#| papermill: {duration: 0.107183, end_time: '2024-10-10T20:36:15.012084', exception: false, start_time: '2024-10-10T20:36:14.904901', status: completed}
#| tags: []
m = get_confounding_matrix(domain.inputs, design=design, interactions=[2])

sns.heatmap(m, annot=True, annot_kws={"fontsize": 7}, fmt="2.1f")
plt.show()
```

## Setup a full factorial design with blocking

Here we setup a blocked full two-level factorial design including a center point and plot it.

```{python}
blocked_domain = Domain(
    inputs=[
        ContinuousInput(key="temperature", bounds=(20, 80)),
        ContinuousInput(key="time", bounds=(60, 120)),
        ContinuousInput(key="ph", bounds=(7, 13)),
        CategoricalInput(key="operator", categories=["A", "B", "C", "D"]),
    ],
)


strategy_data = FractionalFactorialStrategy(
    domain=blocked_domain,
    n_center=1,  # number of center points per block
    n_repetitions=1,  # number of repetitions, we do only one round here
    block_feature_key="operator",
)
strategy = strategies.map(strategy_data)
design = strategy.ask()
display(design)

plot_design(design=design)
```

## Setup a fractional factorial design

Here a fractional factorial design of the form $2^{3-1}$ is setup by specifying the number of generators (here 1). In comparison to the full factorial design with 9 candidates, it features only 5 experiments.

```{python}
#| papermill: {duration: 0.012973, end_time: '2024-10-10T20:36:15.037263', exception: false, start_time: '2024-10-10T20:36:15.024290', status: completed}
#| tags: []
strategy_data = FractionalFactorialStrategy(
    domain=domain,
    n_center=1,  # number of center points
    n_repetitions=1,  # number of repetitions, we do only one round here
    n_generators=1,  # number of generators, ie number of reducing factors
)
strategy = strategies.map(strategy_data)
design = strategy.ask()
display(design)
```

The generator string is automatically generated by making use of the method `get_generator` and specifying the total number of factors (here 3) and the number of generators (here 1).

```{python}
#| papermill: {duration: 0.008137, end_time: '2024-10-10T20:36:15.052737', exception: false, start_time: '2024-10-10T20:36:15.044600', status: completed}
#| tags: []
get_generator(n_factors=3, n_generators=1)
```

As expected for a type III design the main effects are confounded with the two factor interactions:

```{python}
#| papermill: {duration: 0.107571, end_time: '2024-10-10T20:36:15.168255', exception: false, start_time: '2024-10-10T20:36:15.060684', status: completed}
#| tags: []
m = get_confounding_matrix(domain.inputs, design=design, interactions=[2])

sns.heatmap(m, annot=True, annot_kws={"fontsize": 7}, fmt="2.1f")
plt.show()
```

This can also be expressed by the so called alias structure that can be calculated as following:

```{python}
#| papermill: {duration: 0.006933, end_time: '2024-10-10T20:36:15.183147', exception: false, start_time: '2024-10-10T20:36:15.176214', status: completed}
#| tags: []
get_alias_structure("a b ab")
```

Here again a fractional factorial design of the form $2^{3-1}$ is setup by providing the complete generator string of the form `a b -ab` explicitly to the strategy.

```{python}
#| papermill: {duration: 0.012145, end_time: '2024-10-10T20:36:15.203578', exception: false, start_time: '2024-10-10T20:36:15.191433', status: completed}
#| tags: []
strategy_data = FractionalFactorialStrategy(
    domain=domain,
    n_center=1,  # number of center points
    n_repetitions=1,  # number of repetitions, we do only one round here
    generator="a b -ab",  # the exact generator
)
strategy = strategies.map(strategy_data)
design = strategy.ask()
display(design)
```

The last two designs differ only in the last feature `time`, since the generator strings are different. In the first one it holds `time=ph x temperature` whereas in the second it holds `time=-ph x temperature`, which is also reflected in the confounding structure.

```{python}
#| papermill: {duration: 0.112252, end_time: '2024-10-10T20:36:15.324464', exception: false, start_time: '2024-10-10T20:36:15.212212', status: completed}
#| tags: []
m = get_confounding_matrix(domain.inputs, design=design, interactions=[2])

sns.heatmap(m, annot=True, annot_kws={"fontsize": 7}, fmt="2.1f")
plt.show()
```
