Diseño factorial completo \(2^k\) en R

Fecha de publicación

15 de agosto de 2026

Librerías

Al inicio de todas las clase se colocarán las librerías que se van a utilizar para el desarrollo de los casos.

library(tidyverse)
library(FrF2)
library(broom)
library(flextable)

Introducción

Diseñar un experimento factorial es bastante simple, y hacerlo en R no es la excepción. En particular, a mi me gusta usar el tidyverse para casi todo.

Por lo general se parte de una matriz de diseño y con base en ella se trabaja con variables codificadas.

Supongamos una situación en la que interesa diseñar un experimento factorial \(2^4\)$2^4$.

A <- B <- C <- D <- c(-1, 1)

diseno <- tidyr::expand_grid(A, B, C, D)

diseno
# A tibble: 16 × 4
       A     B     C     D
   <dbl> <dbl> <dbl> <dbl>
 1    -1    -1    -1    -1
 2    -1    -1    -1     1
 3    -1    -1     1    -1
 4    -1    -1     1     1
 5    -1     1    -1    -1
 6    -1     1    -1     1
 7    -1     1     1    -1
 8    -1     1     1     1
 9     1    -1    -1    -1
10     1    -1    -1     1
11     1    -1     1    -1
12     1    -1     1     1
13     1     1    -1    -1
14     1     1    -1     1
15     1     1     1    -1
16     1     1     1     1

Lo anterior corresponde a un diseño factorial \(2^4\), pero sin réplicas. Para replicarlo, se puede usar rbind() o alguna función compuesta que lo genere.

En este caso, vamos a replicarlo 2 veces.

diseno <- rbind(diseno, diseno)

diseno
# A tibble: 32 × 4
       A     B     C     D
   <dbl> <dbl> <dbl> <dbl>
 1    -1    -1    -1    -1
 2    -1    -1    -1     1
 3    -1    -1     1    -1
 4    -1    -1     1     1
 5    -1     1    -1    -1
 6    -1     1    -1     1
 7    -1     1     1    -1
 8    -1     1     1     1
 9     1    -1    -1    -1
10     1    -1    -1     1
# ℹ 22 more rows

Finalmente, solo nos falta aleatorizarlo

diseno <- diseno[order(sample(1:32)), ]

Con esto, tenemos un diseño factorial generado \(2^4\), con lo cual se puede expandir a otras cantidades de factores y niveles.

Este diseño también se puede relizar con liberías, por ejemplo usando FrF2.

FrF2::FrF2(nruns = 2^4, 
           nfactors = 4, 
           replications = 2, 
           blocks = 1,
           randomize = TRUE)
   run.no run.no.std.rp  A  B  C  D Blocks
1       1           7.1 -1  1  1 -1     .1
2       2          11.1 -1  1 -1  1     .1
3       3           5.1 -1 -1  1 -1     .1
4       4          14.1  1 -1  1  1     .1
5       5          15.1 -1  1  1  1     .1
6       6           8.1  1  1  1 -1     .1
7       7           1.1 -1 -1 -1 -1     .1
8       8           2.1  1 -1 -1 -1     .1
9       9          10.1  1 -1 -1  1     .1
10     10          16.1  1  1  1  1     .1
11     11           4.1  1  1 -1 -1     .1
12     12           3.1 -1  1 -1 -1     .1
13     13           9.1 -1 -1 -1  1     .1
14     14           6.1  1 -1  1 -1     .1
15     15          13.1 -1 -1  1  1     .1
16     16          12.1  1  1 -1  1     .1
17     17           8.2  1  1  1 -1     .2
18     18           2.2  1 -1 -1 -1     .2
19     19          14.2  1 -1  1  1     .2
20     20          11.2 -1  1 -1  1     .2
21     21          16.2  1  1  1  1     .2
22     22          10.2  1 -1 -1  1     .2
23     23          15.2 -1  1  1  1     .2
24     24           5.2 -1 -1  1 -1     .2
25     25           1.2 -1 -1 -1 -1     .2
26     26           6.2  1 -1  1 -1     .2
27     27          12.2  1  1 -1  1     .2
28     28          13.2 -1 -1  1  1     .2
29     29           4.2  1  1 -1 -1     .2
30     30           7.2 -1  1  1 -1     .2
31     31           3.2 -1  1 -1 -1     .2
32     32           9.2 -1 -1 -1  1     .2
class=design, type= full factorial 
NOTE: columns run.no and run.no.std.rp  are annotation, 
 not part of the data frame

No obstante, este tipo de librarias, a título personal, no me encantan. ¿Pueden deducir por qué?

Trabajemos con el diseño creado con R base, agreguemos una respuesta ficticia, solo a modo de ejemplo.

R <- rnorm(32, 30, 2)

resultados <- cbind(diseno, R)

Se procede con el análisis

modelo1 <- lm(R ~ A*B*C*D, 
              data = resultados)

broom::tidy(modelo1) %>% 
  dplyr::mutate(across(where(is.numeric), ~ round(., 2))) %>% 
  flextable::flextable()

term

estimate

std.error

statistic

p.value

(Intercept)

29.58

0.34

86.55

0.00

A

-0.35

0.34

-1.03

0.32

B

-0.03

0.34

-0.09

0.93

C

-0.43

0.34

-1.27

0.22

D

-0.73

0.34

-2.15

0.05

A:B

0.60

0.34

1.75

0.10

A:C

0.21

0.34

0.63

0.54

B:C

0.14

0.34

0.40

0.70

A:D

-0.14

0.34

-0.40

0.69

B:D

-0.23

0.34

-0.69

0.50

C:D

0.29

0.34

0.86

0.40

A:B:C

-0.32

0.34

-0.95

0.36

A:B:D

-0.39

0.34

-1.14

0.27

A:C:D

0.17

0.34

0.49

0.63

B:C:D

0.35

0.34

1.02

0.32

A:B:C:D

0.24

0.34

0.71

0.49

broom::tidy(aov(modelo1)) %>% 
  dplyr::mutate(across(where(is.numeric), ~ round(., 2))) %>% 
  flextable::flextable()

term

df

sumsq

meansq

statistic

p.value

A

1

3.99

3.99

1.07

0.32

B

1

0.03

0.03

0.01

0.93

C

1

6.04

6.04

1.61

0.22

D

1

17.20

17.20

4.60

0.05

A:B

1

11.45

11.45

3.06

0.10

A:C

1

1.46

1.46

0.39

0.54

B:C

1

0.59

0.59

0.16

0.70

A:D

1

0.61

0.61

0.16

0.69

B:D

1

1.76

1.76

0.47

0.50

C:D

1

2.76

2.76

0.74

0.40

A:B:C

1

3.36

3.36

0.90

0.36

A:B:D

1

4.87

4.87

1.30

0.27

A:C:D

1

0.88

0.88

0.24

0.63

B:C:D

1

3.90

3.90

1.04

0.32

A:B:C:D

1

1.90

1.90

0.51

0.49

Residuals

16

59.80

3.74

Algunos resultados gráficos:

FrF2::DanielPlot(modelo1) # gráfico de daniel

FrF2::MEPlot(modelo1) # Efectos principales

FrF2::IAPlot(modelo1) # Efectos de interacción

Para un gráfico de Pareto

efectos <- coef(modelo1)[-1]  # quitamos intercepto
df_efectos <- data.frame(
  factor = names(efectos),
  efecto = abs(efectos))

ggplot(df_efectos, aes(x = reorder(factor, efecto), y = efecto)) +
  geom_bar(stat = "identity", fill =  "darksalmon") +
  coord_flip() +
  labs(x = "Factor/Interacción", y = "Magnitud del efecto",
       title = "Pareto de efectos factoriales") +
  theme_bw()

¿Qué nos falta?

Pues eliminar todo aquello que no aporte al modelo, esta es una forma de hacerlo automático, pero en DdE puede ser más valioso hacerlo paso a paso.

modelo_final <- step(modelo1, direction  = "backward")
Start:  AIC=52.01
R ~ A * B * C * D

          Df Sum of Sq    RSS    AIC
- A:B:C:D  1    1.9048 61.702 51.011
<none>                 59.797 52.007

Step:  AIC=51.01
R ~ A + B + C + D + A:B + A:C + B:C + A:D + B:D + C:D + A:B:C + 
    A:B:D + A:C:D + B:C:D

        Df Sum of Sq    RSS    AIC
- A:C:D  1    0.8826 62.585 49.465
- A:B:C  1    3.3577 65.060 50.706
- B:C:D  1    3.9018 65.604 50.973
<none>               61.702 51.011
- A:B:D  1    4.8668 66.569 51.440

Step:  AIC=49.47
R ~ A + B + C + D + A:B + A:C + B:C + A:D + B:D + C:D + A:B:C + 
    A:B:D + B:C:D

        Df Sum of Sq    RSS    AIC
- A:B:C  1    3.3577 65.942 49.137
- B:C:D  1    3.9018 66.487 49.400
<none>               62.585 49.465
- A:B:D  1    4.8668 67.451 49.862

Step:  AIC=49.14
R ~ A + B + C + D + A:B + A:C + B:C + A:D + B:D + C:D + A:B:D + 
    B:C:D

        Df Sum of Sq    RSS    AIC
- A:C    1    1.4611 67.403 47.839
- B:C:D  1    3.9018 69.844 48.977
<none>               65.942 49.137
- A:B:D  1    4.8668 70.809 49.416

Step:  AIC=47.84
R ~ A + B + C + D + A:B + B:C + A:D + B:D + C:D + A:B:D + B:C:D

        Df Sum of Sq    RSS    AIC
- B:C:D  1    3.9018 71.305 47.640
<none>               67.403 47.839
- A:B:D  1    4.8668 72.270 48.070

Step:  AIC=47.64
R ~ A + B + C + D + A:B + B:C + A:D + B:D + C:D + A:B:D

        Df Sum of Sq    RSS    AIC
- B:C    1    0.5892 71.895 45.903
- C:D    1    2.7622 74.068 46.856
<none>               71.305 47.640
- A:B:D  1    4.8668 76.172 47.752

Step:  AIC=45.9
R ~ A + B + C + D + A:B + A:D + B:D + C:D + A:B:D

        Df Sum of Sq    RSS    AIC
- C:D    1    2.7622 74.657 45.109
<none>               71.895 45.903
- A:B:D  1    4.8668 76.761 45.999

Step:  AIC=45.11
R ~ A + B + C + D + A:B + A:D + B:D + A:B:D

        Df Sum of Sq    RSS    AIC
<none>               74.657 45.109
- A:B:D  1    4.8668 79.524 45.130
- C      1    6.0355 80.692 45.597
broom::tidy(modelo_final) %>% 
  dplyr::mutate(across(where(is.numeric), ~ round(., 2))) %>% 
  flextable::flextable()

term

estimate

std.error

statistic

p.value

(Intercept)

29.58

0.32

92.87

0.00

A

-0.35

0.32

-1.11

0.28

B

-0.03

0.32

-0.10

0.92

C

-0.43

0.32

-1.36

0.19

D

-0.73

0.32

-2.30

0.03

A:B

0.60

0.32

1.88

0.07

A:D

-0.14

0.32

-0.43

0.67

B:D

-0.23

0.32

-0.74

0.47

A:B:D

-0.39

0.32

-1.22

0.23

broom::tidy(aov(modelo_final)) %>% 
  dplyr::mutate(across(where(is.numeric), ~ round(., 2))) %>% 
  flextable::flextable()

term

df

sumsq

meansq

statistic

p.value

A

1

3.99

3.99

1.23

0.28

B

1

0.03

0.03

0.01

0.92

C

1

6.04

6.04

1.86

0.19

D

1

17.20

17.20

5.30

0.03

A:B

1

11.45

11.45

3.53

0.07

A:D

1

0.61

0.61

0.19

0.67

B:D

1

1.76

1.76

0.54

0.47

A:B:D

1

4.87

4.87

1.50

0.23

Residuals

23

74.66

3.25