1 1. Objetivo del análisis

El experimento corresponde a un diseño factorial 3 × 2 × 2, con tres factores experimentales:

T.Susp: Tipo de suspensión.

A.Malla: Abertura de la malla.

T.Cicla: Temperatura de ciclaje.


La variable de respuesta es Vol.Sedimentacion. El objetivo práctico del experimento es minimizar el volumen de sedimentación.

2 2. Carga y preparación de los datos

# ============================================================
# Carga del archivo
# Coloca el archivo Excel en la misma carpeta de este RMarkdown.
# El script busca ambos nombres posibles.
# ============================================================
opciones_archivo <- c("FFD_con_respuestas.xlsx", "FFD_Con_Respuestas.xlsx")
archivo <- opciones_archivo[file.exists(opciones_archivo)][1]

if (is.na(archivo)) {
  message("No encontré el archivo Excel en la carpeta del RMarkdown. Selecciónalo manualmente.")
  archivo <- file.choose()
}

datos_original <- readxl::read_excel(archivo, sheet = "FFD")

# Renombramiento limpio de columnas.
# Se conservan las convenciones originales en las etiquetas de análisis.
datos <- datos_original %>%
  rename(
    TSusp = `T.Susp.`,
    AMalla = `A.Malla`,
    TCicla = `T.Cicla.`,
    Bloque = `Blocks`,
    Corrida = `run.no`,
    VolSed = `Vol.Sedimentacion`
  ) %>%
  mutate(
    TSusp = factor(TSusp, levels = c("A1", "A2", "A3")),
    AMalla = factor(AMalla, levels = c("B1", "B2")),
    TCicla = factor(TCicla, levels = c("C1", "C2")),
    Bloque = factor(Bloque),
    VolSed = as.numeric(VolSed),
    Grupo = interaction(TSusp, AMalla, TCicla, sep = ":"),
    Orden = row_number()
  )

# Vista interactiva de la matriz experimental con respuestas.
DT::datatable(
  datos %>% select(Orden, Corrida, TSusp, AMalla, TCicla, Bloque, VolSed),
  filter = "top",
  rownames = FALSE,
  options = list(
    pageLength = 12,
    scrollX = TRUE,
    dom = "Bfrtip"
  ),
  caption = "Matriz del diseño factorial con la variable de respuesta Volumen de Sedimentación"
)

3 3. Estadística descriptiva del diseño

resumen_combinaciones <- datos %>%
  group_by(TSusp, AMalla, TCicla) %>%
  summarise(
    n = n(),
    media = mean(VolSed, na.rm = TRUE),
    sd = sd(VolSed, na.rm = TRUE),
    minimo = min(VolSed, na.rm = TRUE),
    maximo = max(VolSed, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(media)

DT::datatable(
  resumen_combinaciones,
  rownames = FALSE,
  options = list(pageLength = 12, scrollX = TRUE),
  caption = "Resumen por combinación factorial, ordenado de menor a mayor volumen promedio"
) %>%
  DT::formatRound(columns = c("media", "sd", "minimo", "maximo"), digits = 3)
mejor <- resumen_combinaciones %>% slice_min(media, n = 1)
peor <- resumen_combinaciones %>% slice_max(media, n = 1)

cat(
  '<div class="good-box">',
  '<b>Mejor combinación observada para minimizar la respuesta:</b> ',
  'T.Susp = ', as.character(mejor$TSusp), ', ',
  'A.Malla = ', as.character(mejor$AMalla), ', ',
  'T.Cicla = ', as.character(mejor$TCicla), '. ',
  'Promedio observado = ', round(mejor$media, 3), '.',
  '<br><br>',
  '<b>Combinación con mayor volumen promedio:</b> ',
  'T.Susp = ', as.character(peor$TSusp), ', ',
  'A.Malla = ', as.character(peor$AMalla), ', ',
  'T.Cicla = ', as.character(peor$TCicla), '. ',
  'Promedio observado = ', round(peor$media, 3), '.',
  '</div>',
  sep = ""
)
Mejor combinación observada para minimizar la respuesta: T.Susp = A2, A.Malla = B1, T.Cicla = C2. Promedio observado = 45.25.

Combinación con mayor volumen promedio: T.Susp = A2, A.Malla = B2, T.Cicla = C1. Promedio observado = 77.75.

3.1 3.1 Ranking interactivo de combinaciones

resumen_combinaciones <- resumen_combinaciones %>%
  mutate(
    combinacion = paste("T.Susp", TSusp, "| A.Malla", AMalla, "| T.Cicla", TCicla),
    combinacion = factor(combinacion, levels = combinacion),
    hover = paste0(
      "Combinación: ", combinacion,
      "<br>Media: ", round(media, 3),
      "<br>Desv. estándar: ", round(sd, 3),
      "<br>n: ", n
    )
  )

p_ranking <- ggplot(
  resumen_combinaciones,
  aes(x = combinacion, y = media, fill = media, text = hover)
) +
  geom_col(width = 0.72, color = "white") +
  coord_flip() +
  scale_fill_viridis_c(option = "C", direction = -1) +
  labs(
    title = "Ranking de combinaciones factoriales",
    subtitle = "La barra más corta es la condición más conveniente porque se busca minimizar Vol.Sedimentacion",
    x = "Combinación experimental",
    y = "Volumen promedio de sedimentación",
    fill = "Media"
  ) +
  tema_grafico

plotly::ggplotly(p_ranking, tooltip = "text") %>%
  plotly::layout(legend = list(orientation = "h", x = 0.1, y = -0.2))

3.2 3.2 Mapa de calor de medias

resumen_combinaciones <- resumen_combinaciones %>%
  mutate(
    hover_heat = paste0(
      "T.Susp: ", TSusp,
      "<br>A.Malla: ", AMalla,
      "<br>T.Cicla: ", TCicla,
      "<br>Media: ", round(media, 3),
      "<br>SD: ", round(sd, 3)
    )
  )

p_heat <- ggplot(
  resumen_combinaciones,
  aes(x = AMalla, y = TSusp, fill = media, text = hover_heat)
) +
  geom_tile(color = "white", linewidth = 1.3) +
  geom_text(aes(label = round(media, 1)), color = "white", fontface = "bold", size = 5) +
  facet_wrap(~ TCicla) +
  scale_fill_viridis_c(option = "C", direction = -1) +
  labs(
    title = "Mapa de calor del volumen promedio",
    subtitle = "Valores menores son mejores para el objetivo del experimento",
    x = "Abertura de malla (A.Malla)",
    y = "Tipo de suspensión (T.Susp)",
    fill = "Media"
  ) +
  tema_grafico

plotly::ggplotly(p_heat, tooltip = "text")

4 4. Modelo lineal ampliado

El modelo ampliado utilizado es el factorial completo: incluye efectos principales, interacciones de dos factores e interacción triple.

\[ Y = \mu + T.Susp + A.Malla + T.Cicla + T.Susp:A.Malla + T.Susp:T.Cicla + A.Malla:T.Cicla + T.Susp:A.Malla:T.Cicla + \varepsilon \]

modelo_ampliado <- lm(VolSed ~ TSusp * AMalla * TCicla, data = datos)

# Se agregan residuos y ajustados para los diagnósticos.
datos <- datos %>%
  mutate(
    Ajustado = fitted(modelo_ampliado),
    Residuo = residuals(modelo_ampliado),
    Residuo_estandar = rstandard(modelo_ampliado),
    Residuo_student = rstudent(modelo_ampliado),
    Abs_sqrt_resid = sqrt(abs(Residuo_estandar))
  )

resumen_modelo <- broom::glance(modelo_ampliado)

DT::datatable(
  resumen_modelo,
  rownames = FALSE,
  options = list(dom = "t", scrollX = TRUE),
  caption = "Resumen global del modelo lineal ampliado"
) %>%
  DT::formatRound(columns = names(resumen_modelo)[sapply(resumen_modelo, is.numeric)], digits = 4)
coeficientes_modelo <- broom::tidy(modelo_ampliado, conf.int = TRUE) %>%
  mutate(
    termino = limpiar_termino(term),
    p.value = p.value,
    decision = ifelse(!is.na(p.value) & p.value < alfa, "Significativo", "No significativo")
  ) %>%
  select(termino, estimate, std.error, statistic, p.value, conf.low, conf.high, decision)

DT::datatable(
  coeficientes_modelo,
  rownames = FALSE,
  options = list(pageLength = 12, scrollX = TRUE),
  caption = "Coeficientes del modelo ampliado"
) %>%
  DT::formatRound(columns = c("estimate", "std.error", "statistic", "conf.low", "conf.high"), digits = 4) %>%
  DT::formatSignif(columns = "p.value", digits = 4)

5 5. Validación de supuestos del modelo

6 5.1 Normalidad de residuos

6.1 Método gráfico: histograma y densidad

p_hist <- ggplot(datos, aes(x = Residuo, text = paste0("Residuo: ", round(Residuo, 3)))) +
  geom_histogram(aes(y = after_stat(density)), bins = 12, fill = "#1565C0", color = "white", alpha = 0.82) +
  geom_density(color = "#F28E2B", linewidth = 1.2) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray35") +
  labs(
    title = "Distribución de residuos del modelo ampliado",
    subtitle = "La forma debería aproximarse a una campana centrada en cero",
    x = "Residuo",
    y = "Densidad"
  ) +
  tema_grafico

plotly::ggplotly(p_hist, tooltip = "text")

6.2 Método gráfico: QQ-plot

qq <- qqnorm(datos$Residuo, plot.it = FALSE)
qq_df <- tibble(
  teorico = qq$x,
  muestra = qq$y,
  hover = paste0("Teórico: ", round(qq$x, 3), "<br>Residuo: ", round(qq$y, 3))
)

p_qq <- ggplot(qq_df, aes(x = teorico, y = muestra, text = hover)) +
  geom_point(size = 3, color = "#1565C0", alpha = 0.86) +
  geom_smooth(method = "lm", se = FALSE, color = "#F28E2B", linewidth = 1) +
  labs(
    title = "QQ-plot interactivo de residuos",
    subtitle = "Puntos cerca de la línea sugieren normalidad de los residuos",
    x = "Cuantiles teóricos normales",
    y = "Cuantiles muestrales de residuos"
  ) +
  tema_grafico

plotly::ggplotly(p_qq, tooltip = "text")

6.3 Test de Shapiro-Wilk

shapiro_res <- shapiro.test(residuals(modelo_ampliado))
shapiro_tbl <- broom::tidy(shapiro_res)

DT::datatable(
  shapiro_tbl,
  rownames = FALSE,
  options = list(dom = "t"),
  caption = "Prueba de normalidad de Shapiro-Wilk para los residuos"
) %>%
  DT::formatRound(columns = c("statistic", "p.value"), digits = 5)
texto_shapiro <- if (shapiro_res$p.value >= alfa) {
  paste0("No se rechaza la normalidad de los residuos, porque p = ", fmt_p(shapiro_res$p.value), " es mayor o igual que ", alfa, ".")
} else {
  paste0("Se rechaza la normalidad de los residuos, porque p = ", fmt_p(shapiro_res$p.value), " es menor que ", alfa, ". Esto sugiere revisar posibles valores atípicos, transformaciones o confirmar si la conclusión del ANOVA se mantiene por la robustez del diseño balanceado.")
}

cat('<div class="warn-box"><b>Interpretación:</b> ', texto_shapiro, '</div>', sep = "")
Interpretación: Se rechaza la normalidad de los residuos, porque p = < 0.001 es menor que 0.05. Esto sugiere revisar posibles valores atípicos, transformaciones o confirmar si la conclusión del ANOVA se mantiene por la robustez del diseño balanceado.

7 5.2 Homocedasticidad

7.1 Test de Levene

levene_res <- car::leveneTest(VolSed ~ Grupo, data = datos)
levene_tbl <- as.data.frame(levene_res) %>%
  tibble::rownames_to_column("termino")

DT::datatable(
  levene_tbl,
  rownames = FALSE,
  options = list(dom = "t", scrollX = TRUE),
  caption = "Test de Levene para igualdad de varianzas entre combinaciones factoriales"
) %>%
  DT::formatRound(columns = names(levene_tbl)[sapply(levene_tbl, is.numeric)], digits = 5)

7.2 Test de Cochran

cochran_res <- tryCatch(
  DescTools::CochranTest(VolSed ~ Grupo, data = datos),
  error = function(e1) {
    tryCatch(
      DescTools::CochranTest(datos$VolSed, datos$Grupo),
      error = function(e2) NULL
    )
  }
)

if (!is.null(cochran_res)) {
  cochran_tbl <- broom::tidy(cochran_res)
  DT::datatable(
    cochran_tbl,
    rownames = FALSE,
    options = list(dom = "t"),
    caption = "Test de Cochran para homogeneidad de varianzas"
  ) %>%
    DT::formatRound(columns = c("statistic", "p.value"), digits = 5)
} else {
  cat("No fue posible ejecutar CochranTest con la versión instalada de DescTools. Revisa la instalación del paquete DescTools.")
}
## No fue posible ejecutar CochranTest con la versión instalada de DescTools. Revisa la instalación del paquete DescTools.

7.3 Gráfico de residuos vs ajustados

p_res_fit <- ggplot(
  datos,
  aes(
    x = Ajustado,
    y = Residuo,
    color = Grupo,
    text = paste0(
      "Corrida: ", Corrida,
      "<br>T.Susp: ", TSusp,
      "<br>A.Malla: ", AMalla,
      "<br>T.Cicla: ", TCicla,
      "<br>Ajustado: ", round(Ajustado, 3),
      "<br>Residuo: ", round(Residuo, 3)
    )
  )
) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray35") +
  geom_point(size = 3, alpha = 0.88) +
  labs(
    title = "Residuos vs valores ajustados",
    subtitle = "Una nube sin patrón fuerte favorece el supuesto de varianza constante",
    x = "Valor ajustado",
    y = "Residuo",
    color = "Grupo"
  ) +
  tema_grafico

plotly::ggplotly(p_res_fit, tooltip = "text") %>%
  plotly::layout(showlegend = FALSE)
# En levene_tbl, la primera fila suele contener el p-valor del factor Grupo.
p_levene <- suppressWarnings(as.numeric(levene_tbl$`Pr(>F)`[1]))

texto_levene <- if (!is.na(p_levene) && p_levene >= alfa) {
  paste0("Levene no detecta diferencias significativas de varianza entre grupos, porque p = ", fmt_p(p_levene), ".")
} else if (!is.na(p_levene) && p_levene < alfa) {
  paste0("Levene detecta evidencia de varianzas diferentes entre grupos, porque p = ", fmt_p(p_levene), ".")
} else {
  "No fue posible leer automáticamente el p-valor de Levene."
}

texto_cochran <- ""
if (!is.null(cochran_res)) {
  texto_cochran <- if (cochran_res$p.value >= alfa) {
    paste0(" Cochran tampoco detecta una varianza dominante problemática, porque p = ", fmt_p(cochran_res$p.value), ".")
  } else {
    paste0(" Cochran sugiere que una de las combinaciones podría tener una varianza dominante, porque p = ", fmt_p(cochran_res$p.value), ".")
  }
}

cat('<div class="note-box"><b>Interpretación:</b> ', texto_levene, texto_cochran, '</div>', sep = "")
Interpretación: Levene no detecta diferencias significativas de varianza entre grupos, porque p = 0.2972.

8 5.3 Independencia de residuos

8.1 Test de Durbin-Watson

dw_res <- lmtest::dwtest(modelo_ampliado)
dw_tbl <- broom::tidy(dw_res)

DT::datatable(
  dw_tbl,
  rownames = FALSE,
  options = list(dom = "t"),
  caption = "Test de independencia de Durbin-Watson"
) %>%
  DT::formatRound(columns = c("statistic", "p.value"), digits = 5)

8.2 Gráfico de residuos según el orden de corrida

p_orden <- ggplot(
  datos,
  aes(
    x = Orden,
    y = Residuo,
    text = paste0(
      "Orden: ", Orden,
      "<br>Corrida: ", Corrida,
      "<br>Residuo: ", round(Residuo, 3),
      "<br>Grupo: ", Grupo
    )
  )
) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray35") +
  geom_line(color = "#1565C0", linewidth = 0.8, alpha = 0.75) +
  geom_point(size = 3, color = "#F28E2B") +
  labs(
    title = "Residuos en el orden de corrida",
    subtitle = "Patrones ondulados o secuenciales pueden indicar dependencia",
    x = "Orden de observación",
    y = "Residuo"
  ) +
  tema_grafico

plotly::ggplotly(p_orden, tooltip = "text")
texto_dw <- if (dw_res$p.value >= alfa) {
  paste0("No se detecta evidencia estadística de autocorrelación, porque p = ", fmt_p(dw_res$p.value), ".")
} else {
  paste0("Se detecta evidencia de autocorrelación, porque p = ", fmt_p(dw_res$p.value), ". Esta conclusión depende de que el orden usado represente el orden real de ejecución experimental.")
}

cat('<div class="warn-box"><b>Interpretación:</b> ', texto_dw, '</div>', sep = "")
Interpretación: Se detecta evidencia de autocorrelación, porque p = 0.0046. Esta conclusión depende de que el orden usado represente el orden real de ejecución experimental.

9 6. Tabla ANOVA del modelo ampliado

anova_tbl <- broom::tidy(stats::anova(modelo_ampliado)) %>%
  mutate(
    termino = limpiar_termino(term),
    porcentaje_SS = ifelse(term != "Residuals", 100 * sumsq / sum(sumsq[term != "Residuals"], na.rm = TRUE), NA_real_),
    decision = ifelse(!is.na(p.value) & p.value < alfa, "Significativo", "No significativo")
  ) %>%
  select(termino, df, sumsq, meansq, statistic, p.value, porcentaje_SS, decision)

DT::datatable(
  anova_tbl,
  rownames = FALSE,
  options = list(pageLength = 10, scrollX = TRUE),
  caption = "Tabla ANOVA del modelo ampliado factorial completo"
) %>%
  DT::formatRound(columns = c("sumsq", "meansq", "statistic", "porcentaje_SS"), digits = 4) %>%
  DT::formatSignif(columns = "p.value", digits = 4)
# ANOVA tipo III con contrastes de suma. Es útil para modelos con interacciones.
anova_tipo3 <- as.data.frame(car::Anova(modelo_ampliado, type = 3)) %>%
  tibble::rownames_to_column("termino") %>%
  mutate(termino = limpiar_termino(termino))

DT::datatable(
  anova_tipo3,
  rownames = FALSE,
  options = list(pageLength = 12, scrollX = TRUE),
  caption = "ANOVA tipo III del modelo ampliado"
) %>%
  DT::formatRound(columns = names(anova_tipo3)[sapply(anova_tipo3, is.numeric)], digits = 5)
sig_terms <- anova_tbl %>%
  filter(termino != "Error", !is.na(p.value), p.value < alfa) %>%
  arrange(p.value)

if (nrow(sig_terms) == 0) {
  texto_anova <- "Ningún término del modelo resultó significativo al 5 %. En ese caso, la decisión práctica debe apoyarse más en las medias observadas y en criterios técnicos del proceso."
} else {
  texto_anova <- paste0(
    "Los términos significativos al 5 % son: ",
    paste0(sig_terms$termino, " (p = ", fmt_p(sig_terms$p.value), ")", collapse = "; "),
    ". Estos términos explican diferencias relevantes en el volumen de sedimentación."
  )
}

cat('<div class="good-box"><b>Interpretación ANOVA:</b> ', texto_anova, '</div>', sep = "")
Interpretación ANOVA: Los términos significativos al 5 % son: T.Cicla (p = < 0.001); T.Susp:A.Malla (p = < 0.001); A.Malla (p = < 0.001). Estos términos explican diferencias relevantes en el volumen de sedimentación.

10 7. Coeficientes estandarizados

Como los predictores son categóricos, la estandarización se realiza sobre la variable de respuesta. Los coeficientes indican cambios relativos en desviaciones estándar del volumen de sedimentación respecto a los contrastes del modelo.

datos_std <- datos %>%
  mutate(VolSed_z = as.numeric(scale(VolSed)))

modelo_std <- lm(VolSed_z ~ TSusp * AMalla * TCicla, data = datos_std)

coef_std <- broom::tidy(modelo_std, conf.int = TRUE) %>%
  filter(term != "(Intercept)") %>%
  mutate(
    termino = limpiar_termino(term),
    abs_beta = abs(estimate),
    decision = ifelse(!is.na(p.value) & p.value < alfa, "Significativo", "No significativo")
  ) %>%
  arrange(desc(abs_beta)) %>%
  select(termino, beta_estandarizado = estimate, abs_beta, std.error, statistic, p.value, conf.low, conf.high, decision)

DT::datatable(
  coef_std,
  rownames = FALSE,
  options = list(pageLength = 12, scrollX = TRUE),
  caption = "Coeficientes estandarizados del modelo ampliado"
) %>%
  DT::formatRound(columns = c("beta_estandarizado", "abs_beta", "std.error", "statistic", "conf.low", "conf.high"), digits = 4) %>%
  DT::formatSignif(columns = "p.value", digits = 4)
coef_std_plot <- coef_std %>%
  mutate(
    termino = factor(termino, levels = rev(termino)),
    hover = paste0(
      "Término: ", termino,
      "<br>Beta estandarizado: ", round(beta_estandarizado, 4),
      "<br>|Beta|: ", round(abs_beta, 4),
      "<br>p: ", fmt_p(p.value),
      "<br>Decisión: ", decision
    )
  )

p_beta <- ggplot(coef_std_plot, aes(x = termino, y = abs_beta, fill = decision, text = hover)) +
  geom_col(width = 0.72, color = "white") +
  coord_flip() +
  scale_fill_manual(values = c("Significativo" = "#00A676", "No significativo" = "#B0BEC5")) +
  labs(
    title = "Magnitud de coeficientes estandarizados",
    subtitle = "Ordenados por importancia relativa absoluta",
    x = "Término del modelo",
    y = "|Beta estandarizado|",
    fill = "Decisión"
  ) +
  tema_grafico

plotly::ggplotly(p_beta, tooltip = "text")

11 8. Diagrama de Pareto de efectos

11.1 8.1 Pareto por contribución a la suma de cuadrados

pareto_anova <- anova_tbl %>%
  filter(termino != "Error") %>%
  arrange(desc(porcentaje_SS)) %>%
  mutate(
    acumulado = cumsum(porcentaje_SS),
    termino = factor(termino, levels = termino),
    hover = paste0(
      "Término: ", termino,
      "<br>% SS: ", round(porcentaje_SS, 2), "%",
      "<br>% acumulado: ", round(acumulado, 2), "%",
      "<br>F: ", round(statistic, 3),
      "<br>p: ", fmt_p(p.value)
    )
  )

plotly::plot_ly(
  data = pareto_anova,
  x = ~termino,
  y = ~porcentaje_SS,
  type = "bar",
  name = "% suma de cuadrados",
  text = ~hover,
  hoverinfo = "text",
  marker = list(color = "#1565C0")
) %>%
  plotly::add_trace(
    y = ~acumulado,
    type = "scatter",
    mode = "lines+markers",
    name = "% acumulado",
    yaxis = "y2",
    text = ~hover,
    hoverinfo = "text",
    line = list(color = "#F28E2B", width = 3),
    marker = list(size = 8, color = "#F28E2B")
  ) %>%
  plotly::layout(
    title = list(text = "Pareto interactivo de efectos por contribución ANOVA"),
    xaxis = list(title = "Término", tickangle = -35),
    yaxis = list(title = "% de suma de cuadrados", rangemode = "tozero"),
    yaxis2 = list(
      title = "% acumulado",
      overlaying = "y",
      side = "right",
      rangemode = "tozero"
    ),
    legend = list(orientation = "h", x = 0.05, y = -0.25),
    margin = list(b = 120)
  )
top_pareto <- pareto_anova %>% slice(1)
cat(
  '<div class="good-box">',
  '<b>Interpretación del Pareto:</b> El término con mayor contribución a la variabilidad explicada es <b>',
  as.character(top_pareto$termino),
  '</b>, con aproximadamente <b>', round(top_pareto$porcentaje_SS, 2),
  '%</b> de la suma de cuadrados explicada por los tratamientos. ',
  'Los primeros términos del Pareto deben ser priorizados al elegir las condiciones de operación para reducir el volumen de sedimentación.',
  '</div>',
  sep = ""
)
Interpretación del Pareto: El término con mayor contribución a la variabilidad explicada es T.Cicla, con aproximadamente 83.55% de la suma de cuadrados explicada por los tratamientos. Los primeros términos del Pareto deben ser priorizados al elegir las condiciones de operación para reducir el volumen de sedimentación.

12 9. Gráficas de interacción

medias_interaccion <- datos %>%
  group_by(TSusp, AMalla, TCicla) %>%
  summarise(
    n = n(),
    media = mean(VolSed, na.rm = TRUE),
    sd = sd(VolSed, na.rm = TRUE),
    se = sd / sqrt(n),
    ic95 = qt(0.975, df = n - 1) * se,
    .groups = "drop"
  ) %>%
  mutate(
    hover = paste0(
      "T.Susp: ", TSusp,
      "<br>A.Malla: ", AMalla,
      "<br>T.Cicla: ", TCicla,
      "<br>Media: ", round(media, 3),
      "<br>IC95: ±", round(ic95, 3),
      "<br>SD: ", round(sd, 3),
      "<br>n: ", n
    )
  )

12.1 9.1 Interacción T.Susp × A.Malla, separada por T.Cicla

p_int_1 <- ggplot(
  medias_interaccion,
  aes(x = TSusp, y = media, color = AMalla, group = AMalla, text = hover)
) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 4) +
  geom_errorbar(aes(ymin = media - ic95, ymax = media + ic95), width = 0.08, linewidth = 0.8) +
  facet_wrap(~ TCicla) +
  labs(
    title = "Interacción T.Susp × A.Malla",
    subtitle = "Líneas no paralelas sugieren interacción entre el tipo de suspensión y la abertura de malla",
    x = "Tipo de suspensión (T.Susp)",
    y = "Volumen promedio de sedimentación",
    color = "A.Malla"
  ) +
  tema_grafico

plotly::ggplotly(p_int_1, tooltip = "text")

12.2 9.2 Interacción T.Susp × T.Cicla, separada por A.Malla

p_int_2 <- ggplot(
  medias_interaccion,
  aes(x = TSusp, y = media, color = TCicla, group = TCicla, text = hover)
) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 4) +
  geom_errorbar(aes(ymin = media - ic95, ymax = media + ic95), width = 0.08, linewidth = 0.8) +
  facet_wrap(~ AMalla) +
  labs(
    title = "Interacción T.Susp × T.Cicla",
    subtitle = "Permite observar si el efecto de la temperatura de ciclaje cambia según el tipo de suspensión",
    x = "Tipo de suspensión (T.Susp)",
    y = "Volumen promedio de sedimentación",
    color = "T.Cicla"
  ) +
  tema_grafico

plotly::ggplotly(p_int_2, tooltip = "text")

12.3 9.3 Interacción A.Malla × T.Cicla, separada por T.Susp

p_int_3 <- ggplot(
  medias_interaccion,
  aes(x = AMalla, y = media, color = TCicla, group = TCicla, text = hover)
) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 4) +
  geom_errorbar(aes(ymin = media - ic95, ymax = media + ic95), width = 0.08, linewidth = 0.8) +
  facet_wrap(~ TSusp) +
  labs(
    title = "Interacción A.Malla × T.Cicla",
    subtitle = "Ayuda a decidir la abertura de malla considerando cada nivel de temperatura de ciclaje",
    x = "Abertura de malla (A.Malla)",
    y = "Volumen promedio de sedimentación",
    color = "T.Cicla"
  ) +
  tema_grafico

plotly::ggplotly(p_int_3, tooltip = "text")
interacciones_sig <- anova_tbl %>%
  filter(grepl(":", termino), !is.na(p.value), p.value < alfa) %>%
  arrange(p.value)

if (nrow(interacciones_sig) > 0) {
  texto_interacciones <- paste0(
    "Las interacciones significativas son: ",
    paste0(interacciones_sig$termino, " (p = ", fmt_p(interacciones_sig$p.value), ")", collapse = "; "),
    ". Cuando una interacción es significativa, no conviene interpretar los factores de forma aislada; se debe analizar la combinación de niveles."
  )
} else {
  texto_interacciones <- "No se detectaron interacciones significativas al 5 %. En ese caso, los efectos principales pueden interpretarse con mayor claridad."
}

cat('<div class="note-box"><b>Interpretación de interacciones:</b> ', texto_interacciones, '</div>', sep = "")
Interpretación de interacciones: Las interacciones significativas son: T.Susp:A.Malla (p = < 0.001). Cuando una interacción es significativa, no conviene interpretar los factores de forma aislada; se debe analizar la combinación de niveles.

13 10. Visualización 3D interactiva

datos_3d <- datos %>%
  mutate(
    TSusp_num = as.numeric(TSusp),
    AMalla_num = as.numeric(AMalla),
    TCicla_num = as.numeric(TCicla),
    hover_3d = paste0(
      "Corrida: ", Corrida,
      "<br>T.Susp: ", TSusp,
      "<br>A.Malla: ", AMalla,
      "<br>T.Cicla: ", TCicla,
      "<br>Vol.Sedimentacion: ", VolSed
    )
  )

plotly::plot_ly(
  data = datos_3d,
  x = ~TSusp_num,
  y = ~AMalla_num,
  z = ~VolSed,
  color = ~TCicla,
  colors = c("#1565C0", "#F28E2B"),
  type = "scatter3d",
  mode = "markers",
  marker = list(size = 5, opacity = 0.86),
  text = ~hover_3d,
  hoverinfo = "text"
) %>%
  plotly::layout(
    title = "Nube 3D interactiva del diseño factorial",
    scene = list(
      xaxis = list(title = "T.Susp", tickvals = c(1, 2, 3), ticktext = c("A1", "A2", "A3")),
      yaxis = list(title = "A.Malla", tickvals = c(1, 2), ticktext = c("B1", "B2")),
      zaxis = list(title = "Vol.Sedimentacion")
    ),
    legend = list(orientation = "h", x = 0.05, y = -0.05)
  )

14 11. Medias marginales estimadas y selección de la condición óptima

emm_combinaciones <- emmeans::emmeans(modelo_ampliado, ~ TSusp * AMalla * TCicla)
emm_tbl <- as.data.frame(emm_combinaciones) %>%
  arrange(emmean) %>%
  mutate(
    ranking = row_number(),
    combinacion = paste("T.Susp", TSusp, "| A.Malla", AMalla, "| T.Cicla", TCicla)
  ) %>%
  select(ranking, combinacion, TSusp, AMalla, TCicla, emmean, SE, df, lower.CL, upper.CL)

DT::datatable(
  emm_tbl,
  rownames = FALSE,
  options = list(pageLength = 12, scrollX = TRUE),
  caption = "Medias marginales estimadas por combinación, ordenadas para minimizar Vol.Sedimentacion"
) %>%
  DT::formatRound(columns = c("emmean", "SE", "df", "lower.CL", "upper.CL"), digits = 4)
emm_plot <- emm_tbl %>%
  mutate(
    combinacion = factor(combinacion, levels = combinacion),
    hover = paste0(
      "Ranking: ", ranking,
      "<br>", combinacion,
      "<br>Media estimada: ", round(emmean, 3),
      "<br>IC95: [", round(lower.CL, 3), ", ", round(upper.CL, 3), "]"
    )
  )

p_emm <- ggplot(emm_plot, aes(x = combinacion, y = emmean, fill = emmean, text = hover)) +
  geom_col(color = "white", width = 0.72) +
  geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = 0.18, color = "gray20") +
  coord_flip() +
  scale_fill_viridis_c(option = "C", direction = -1) +
  labs(
    title = "Medias marginales estimadas por combinación",
    subtitle = "La primera barra corresponde a la condición estimada más favorable para minimizar la respuesta",
    x = "Combinación",
    y = "Media marginal estimada",
    fill = "Media"
  ) +
  tema_grafico

plotly::ggplotly(p_emm, tooltip = "text")

15 12. Conclusión ejecutiva

mejor_emm <- emm_tbl %>% slice(1)
segundo_emm <- emm_tbl %>% slice(2)

factor_top <- pareto_anova %>% slice(1)

texto_normalidad <- if (shapiro_res$p.value >= alfa) {
  "El supuesto de normalidad no muestra evidencia de incumplimiento con Shapiro-Wilk."
} else {
  "El supuesto de normalidad muestra evidencia de incumplimiento con Shapiro-Wilk; por tanto, conviene revisar residuos atípicos o considerar una transformación si el curso lo exige."
}

texto_varianzas <- if (!is.na(p_levene) && p_levene >= alfa) {
  "El supuesto de homocedasticidad es razonable según Levene."
} else {
  "La homocedasticidad debe revisarse, porque Levene sugiere diferencias de varianza."
}

cat(
  '<div class="good-box">',
  '<b>Conclusión orientada a minimizar Vol.Sedimentacion:</b><br><br>',
  '1. La condición estimada con menor volumen de sedimentación es <b>', mejor_emm$combinacion, '</b>, con media estimada de <b>', round(mejor_emm$emmean, 3), '</b>.<br>',
  '2. La segunda mejor condición es <b>', segundo_emm$combinacion, '</b>, con media estimada de <b>', round(segundo_emm$emmean, 3), '</b>.<br>',
  '3. El término con mayor aporte en el Pareto es <b>', as.character(factor_top$termino), '</b>, con aproximadamente <b>', round(factor_top$porcentaje_SS, 2), '%</b> de la suma de cuadrados de tratamientos.<br>',
  '4. ', texto_normalidad, '<br>',
  '5. ', texto_varianzas, '<br><br>',
  '<b>Recomendación:</b> si la condición óptima es viable técnica y económicamente, debe priorizarse para reducir el volumen de sedimentación. Si existen restricciones operativas, compare la condición óptima con la segunda mejor usando los intervalos de confianza y el contexto del proceso.',
  '</div>',
  sep = ""
)
Conclusión orientada a minimizar Vol.Sedimentacion:

1. La condición estimada con menor volumen de sedimentación es T.Susp A2 | A.Malla B1 | T.Cicla C2, con media estimada de 45.25.
2. La segunda mejor condición es T.Susp A3 | A.Malla B1 | T.Cicla C2, con media estimada de 51.25.
3. El término con mayor aporte en el Pareto es T.Cicla, con aproximadamente 83.55% de la suma de cuadrados de tratamientos.
4. El supuesto de normalidad muestra evidencia de incumplimiento con Shapiro-Wilk; por tanto, conviene revisar residuos atípicos o considerar una transformación si el curso lo exige.
5. El supuesto de homocedasticidad es razonable según Levene.

Recomendación: si la condición óptima es viable técnica y económicamente, debe priorizarse para reducir el volumen de sedimentación. Si existen restricciones operativas, compare la condición óptima con la segunda mejor usando los intervalos de confianza y el contexto del proceso.

16 13. Nota para exposición en clase

Para explicar el diseño a estudiantes, la lectura sugerida es esta: primero se observa el ranking de combinaciones, luego el mapa de calor para ubicar visualmente dónde se minimiza la respuesta, después se valida el modelo con residuos y pruebas de supuestos, y finalmente se usa ANOVA + Pareto para justificar estadísticamente qué factores o interacciones gobiernan el comportamiento del volumen de sedimentación.