Objetivos de esta Sesión

Al finalizar esta actividad, usted será capaz de:

  • Aplicar el proceso completo de EDA paso a paso con datos reales
  • Evaluar la calidad de datos químicos de laboratorio
  • Tomar decisiones inteligentes sobre limpieza de datos
  • Clasificar y analizar diferentes tipos de variables
  • Crear visualizaciones efectivas para datos químicos
  • Interpretar medidas estadísticas en el contexto de química
  • Implementar estructuras de control para automatizar análisis

📚 Contexto del Dataset: Trabajaremos con datos reales de experimentos de laboratorio de química, incluyendo mediciones de pH, concentraciones, temperaturas y características físico-químicas de diferentes tipos de soluciones.

1 Paso 1: Carga y Exploración Inicial de Datos

1.1 Carga del Dataset

# Cargar el dataset de experimentos químicos
# NOTA: Ajustar la ruta según tu sistema
ruta_archivo <- "/Users/jordyab00/Downloads/dataset_quimica_eda.txt"

# Verificar que el archivo existe
if (file.exists(ruta_archivo)) {
    # Leer el archivo CSV
    datos_quimica <- read.csv(ruta_archivo, stringsAsFactors = FALSE)
    cat("Archivo cargado exitosamente\n")
    cat("Dimensiones:", nrow(datos_quimica), "filas x", ncol(datos_quimica), "columnas\n")
} else {
    cat("Error: No se encontró el archivo en la ruta especificada\n")
    cat("Ruta buscada:", ruta_archivo, "\n")
    cat("Verifica la ruta y ajusta según tu sistema\n")
}
#> Archivo cargado exitosamente
#> Dimensiones: 100 filas x 16 columnas

1.2 Inspección Inicial - ¿Qué tenemos?

# Estructura del dataset
cat("=== ESTRUCTURA DEL DATASET ===\n")
#> === ESTRUCTURA DEL DATASET ===
str(datos_quimica)
#> 'data.frame':    100 obs. of  16 variables:
#>  $ experimento_id          : chr  "E001" "E002" "E003" "E004" ...
#>  $ estudiante_id           : chr  "EST_001" "EST_002" "EST_003" "EST_004" ...
#>  $ laboratorio             : chr  "Lab_A" "Lab_A" "Lab_B" "Lab_C" ...
#>  $ tipo_solucion           : chr  "acido_base" "acido_base" "salina" "azucarada" ...
#>  $ fecha_experimento       : chr  "2025-03-10" "2025-03-10" "2025-03-10" "2025-03-10" ...
#>  $ ph                      : num  3.2 3.1 7 6.8 3 7.1 6.9 2.9 7.2 7 ...
#>  $ temperatura_C           : num  22.5 22.8 21.2 23.1 22.9 21.8 23.5 23.2 21.5 22.8 ...
#>  $ concentracion_sal_mg_L  : int  150 145 850 50 148 820 45 152 875 52 ...
#>  $ concentracion_azucar_g_L: int  0 0 0 125 0 0 130 0 0 120 ...
#>  $ volumen_muestra_mL      : int  100 100 250 200 100 250 200 100 250 200 ...
#>  $ masa_soluto_g           : num  2.5 2.4 8.5 25 2.6 8.2 26 2.7 8.8 24 ...
#>  $ densidad_g_mL           : num  1.02 1.02 1.04 1.06 1.02 ...
#>  $ tiempo_reaccion_min     : num  5.2 5.5 0 0 5.8 0 0 6.1 0 0 ...
#>  $ color                   : chr  "incoloro" "incoloro" "incoloro" "incoloro" ...
#>  $ estado_fisico           : chr  "liquido" "liquido" "liquido" "liquido" ...
#>  $ precipitado_formado     : chr  "no" "no" "no" "no" ...
cat("\n=== PRIMERAS 6 FILAS ===\n")
#> 
#> === PRIMERAS 6 FILAS ===
head(datos_quimica)
cat("\n=== ÚLTIMAS 6 FILAS ===\n")
#> 
#> === ÚLTIMAS 6 FILAS ===
tail(datos_quimica)
cat("\n=== NOMBRES DE LAS VARIABLES ===\n")
#> 
#> === NOMBRES DE LAS VARIABLES ===
colnames(datos_quimica)
#>  [1] "experimento_id"           "estudiante_id"           
#>  [3] "laboratorio"              "tipo_solucion"           
#>  [5] "fecha_experimento"        "ph"                      
#>  [7] "temperatura_C"            "concentracion_sal_mg_L"  
#>  [9] "concentracion_azucar_g_L" "volumen_muestra_mL"      
#> [11] "masa_soluto_g"            "densidad_g_mL"           
#> [13] "tiempo_reaccion_min"      "color"                   
#> [15] "estado_fisico"            "precipitado_formado"

1.3 Resumen Estadístico Inicial

# Resumen estadístico básico
cat("=== RESUMEN ESTADÍSTICO INICIAL ===\n")
#> === RESUMEN ESTADÍSTICO INICIAL ===
summary(datos_quimica)
#>  experimento_id     estudiante_id      laboratorio        tipo_solucion     
#>  Length:100         Length:100         Length:100         Length:100        
#>  Class :character   Class :character   Class :character   Class :character  
#>  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
#>                                                                             
#>                                                                             
#>                                                                             
#>                                                                             
#>  fecha_experimento        ph         temperatura_C   concentracion_sal_mg_L
#>  Length:100         Min.   : 1.200   Min.   :21.20   Min.   :  45.0        
#>  Class :character   1st Qu.: 4.225   1st Qu.:22.88   1st Qu.: 150.0        
#>  Mode  :character   Median : 6.500   Median :23.60   Median : 325.0        
#>                     Mean   : 5.909   Mean   :24.06   Mean   : 759.4        
#>                     3rd Qu.: 7.000   3rd Qu.:25.02   3rd Qu.:1190.0        
#>                     Max.   :11.800   Max.   :27.60   Max.   :2185.0        
#>                     NA's   :2                        NA's   :1             
#>  concentracion_azucar_g_L volumen_muestra_mL masa_soluto_g    densidad_g_mL  
#>  Min.   :  0.00           Min.   : 50        Min.   : 2.400   Min.   :1.012  
#>  1st Qu.:  0.00           1st Qu.:100        1st Qu.: 3.275   1st Qu.:1.021  
#>  Median :  0.00           Median :150        Median : 8.800   Median :1.068  
#>  Mean   : 20.87           Mean   :156        Mean   :10.631   Mean   :1.063  
#>  3rd Qu.:  0.00           3rd Qu.:200        3rd Qu.:15.225   3rd Qu.:1.093  
#>  Max.   :140.00           Max.   :300        Max.   :28.000   Max.   :1.128  
#>                                                               NA's   :1      
#>  tiempo_reaccion_min    color           estado_fisico      precipitado_formado
#>  Min.   : 0.00       Length:100         Length:100         Length:100         
#>  1st Qu.: 0.00       Class :character   Class :character   Class :character   
#>  Median : 3.90       Mode  :character   Mode  :character   Mode  :character   
#>  Mean   :10.55                                                                
#>  3rd Qu.:10.30                                                                
#>  Max.   :48.80                                                                
#> 
# Información adicional sobre el dataset
cat("\n=== INFORMACIÓN ADICIONAL ===\n")
#> 
#> === INFORMACIÓN ADICIONAL ===
cat("Número total de observaciones:", nrow(datos_quimica), "\n")
#> Número total de observaciones: 100
cat("Número de variables:", ncol(datos_quimica), "\n")
#> Número de variables: 16
cat("Rango de fechas:", min(datos_quimica$fecha_experimento, na.rm = TRUE), "a", 
    max(datos_quimica$fecha_experimento, na.rm = TRUE), "\n")
#> Rango de fechas: 2025-03-10 a 2025-04-11
cat("Laboratorios incluidos:", paste(unique(datos_quimica$laboratorio), collapse = ", "), "\n")
#> Laboratorios incluidos: Lab_A, Lab_B, Lab_C
cat("Tipos de solución:", paste(unique(datos_quimica$tipo_solucion), collapse = ", "), "\n")
#> Tipos de solución: acido_base, salina, azucarada, precipitacion, neutralizacion, concentracion, dilucion, cristalizacion, indicadores

2 Paso 2: Evaluación de Calidad de Datos

2.1 Análisis de Valores Faltantes

# Detectar valores faltantes por columna
valores_faltantes <- colSums(is.na(datos_quimica))
porcentaje_faltantes <- round((valores_faltantes / nrow(datos_quimica)) * 100, 2)

# Crear tabla de valores faltantes
tabla_faltantes <- data.frame(
    Variable = names(valores_faltantes),
    Valores_Faltantes = valores_faltantes,
    Porcentaje = porcentaje_faltantes,
    Evaluacion = ifelse(porcentaje_faltantes == 0, "Completo",
                       ifelse(porcentaje_faltantes < 5, "Pocos faltantes",
                             ifelse(porcentaje_faltantes < 15, "Moderados faltantes",
                                   "Muchos faltantes")))
)

# Mostrar tabla
kable(tabla_faltantes, 
      caption = "Análisis de Valores Faltantes por Variable",
      align = "lccc")
Análisis de Valores Faltantes por Variable
Variable Valores_Faltantes Porcentaje Evaluacion
experimento_id experimento_id 0 0 Completo
estudiante_id estudiante_id 0 0 Completo
laboratorio laboratorio 0 0 Completo
tipo_solucion tipo_solucion 0 0 Completo
fecha_experimento fecha_experimento 0 0 Completo
ph ph 2 2 Pocos faltantes
temperatura_C temperatura_C 0 0 Completo
concentracion_sal_mg_L concentracion_sal_mg_L 1 1 Pocos faltantes
concentracion_azucar_g_L concentracion_azucar_g_L 0 0 Completo
volumen_muestra_mL volumen_muestra_mL 0 0 Completo
masa_soluto_g masa_soluto_g 0 0 Completo
densidad_g_mL densidad_g_mL 1 1 Pocos faltantes
tiempo_reaccion_min tiempo_reaccion_min 0 0 Completo
color color 0 0 Completo
estado_fisico estado_fisico 0 0 Completo
precipitado_formado precipitado_formado 0 0 Completo
# Total de valores faltantes
cat("\n=== RESUMEN DE VALORES FALTANTES ===\n")
#> 
#> === RESUMEN DE VALORES FALTANTES ===
cat("Total de valores faltantes:", sum(valores_faltantes), "\n")
#> Total de valores faltantes: 4
cat("Porcentaje del dataset:", round((sum(valores_faltantes) / (nrow(datos_quimica) * ncol(datos_quimica))) * 100, 2), "%\n")
#> Porcentaje del dataset: 0.25 %
# Filas con valores faltantes
filas_con_na <- sum(apply(datos_quimica, 1, function(x) any(is.na(x))))
cat("Filas que contienen al menos un NA:", filas_con_na, 
    "(", round((filas_con_na/nrow(datos_quimica))*100, 2), "%)\n")
#> Filas que contienen al menos un NA: 2 ( 2 %)

2.2 Detección de Duplicados

# Buscar duplicados completos
duplicados_completos <- sum(duplicated(datos_quimica))
cat("=== ANÁLISIS DE DUPLICADOS ===\n")
#> === ANÁLISIS DE DUPLICADOS ===
cat("Filas completamente duplicadas:", duplicados_completos, "\n")
#> Filas completamente duplicadas: 0
# Buscar duplicados por ID de experimento
if("experimento_id" %in% names(datos_quimica)) {
    duplicados_id <- sum(duplicated(datos_quimica$experimento_id))
    cat("IDs de experimento duplicados:", duplicados_id, "\n")
}
#> IDs de experimento duplicados: 0
# Buscar duplicados por estudiante y fecha
if(all(c("estudiante_id", "fecha_experimento") %in% names(datos_quimica))) {
    duplicados_estudiante_fecha <- sum(duplicated(datos_quimica[, c("estudiante_id", "fecha_experimento")]))
    cat("Estudiante+fecha duplicados:", duplicados_estudiante_fecha, "\n")
}
#> Estudiante+fecha duplicados: 0
if (duplicados_completos == 0 && duplicados_id == 0) {
    cat("No se encontraron duplicados problemáticos\n")
} else {
    cat("Se encontraron duplicados que requieren atención\n")
}
#> No se encontraron duplicados problemáticos

2.3 Detección de Valores Atípicos (Outliers)

# Función para detectar outliers usando el método IQR
detectar_outliers_IQR <- function(x, factor = 1.5) {
    if (!is.numeric(x)) return(rep(FALSE, length(x)))
    
    Q1 <- quantile(x, 0.25, na.rm = TRUE)
    Q3 <- quantile(x, 0.75, na.rm = TRUE)
    IQR <- Q3 - Q1
    
    limite_inferior <- Q1 - factor * IQR
    limite_superior <- Q3 + factor * IQR
    
    return(x < limite_inferior | x > limite_superior)
}

# Variables numéricas para analizar outliers
variables_numericas <- c("ph", "temperatura_C", "concentracion_sal_mg_L", 
                        "concentracion_azucar_g_L", "volumen_muestra_mL", 
                        "masa_soluto_g", "densidad_g_mL", "tiempo_reaccion_min")

# Detectar outliers en cada variable
outliers_resumen <- data.frame()

for (var in variables_numericas) {
    if (var %in% names(datos_quimica)) {
        outliers <- detectar_outliers_IQR(datos_quimica[[var]])
        n_outliers <- sum(outliers, na.rm = TRUE)
        porcentaje <- round((n_outliers / nrow(datos_quimica)) * 100, 2)
        
        outliers_resumen <- rbind(outliers_resumen, data.frame(
            Variable = var,
            N_Outliers = n_outliers,
            Porcentaje = porcentaje,
            Min_Valor = min(datos_quimica[[var]], na.rm = TRUE),
            Max_Valor = max(datos_quimica[[var]], na.rm = TRUE),
            Evaluacion = ifelse(porcentaje < 5, "Normal", 
                               ifelse(porcentaje < 10, "Revisar", "Muchos outliers"))
        ))
    }
}

kable(outliers_resumen, 
      caption = "Detección de Outliers por Variable Numérica",
      align = "lccccl")
Detección de Outliers por Variable Numérica
Variable N_Outliers Porcentaje Min_Valor Max_Valor Evaluacion
ph 1 1 1.200 11.800 Normal
temperatura_C 0 0 21.200 27.600 Normal
concentracion_sal_mg_L 0 0 45.000 2185.000 Normal
concentracion_azucar_g_L 16 16 0.000 140.000 Muchos outliers
volumen_muestra_mL 0 0 50.000 300.000 Normal
masa_soluto_g 0 0 2.400 28.000 Normal
densidad_g_mL 0 0 1.012 1.128 Normal
tiempo_reaccion_min 17 17 0.000 48.800 Muchos outliers

3 Paso 3: Limpieza y Preparación de Datos

3.1 Decisiones sobre Valores Faltantes

# Crear copia para limpieza
datos_limpios <- datos_quimica

cat("=== ESTRATEGIAS DE LIMPIEZA IMPLEMENTADAS ===\n")
#> === ESTRATEGIAS DE LIMPIEZA IMPLEMENTADAS ===
# Estrategia 1: Valores faltantes en pH - imputar por tipo de solución
if (any(is.na(datos_limpios$ph))) {
    cat("Imputando valores faltantes de pH por tipo de solución...\n")
    
    # Calcular pH promedio por tipo de solución
    ph_por_tipo <- datos_limpios %>%
        group_by(tipo_solucion) %>%
        summarise(ph_promedio = mean(ph, na.rm = TRUE), .groups = 'drop')
    
    # Imputar valores faltantes
    for (i in 1:nrow(datos_limpios)) {
        if (is.na(datos_limpios$ph[i])) {
            tipo <- datos_limpios$tipo_solucion[i]
            ph_imputado <- ph_por_tipo$ph_promedio[ph_por_tipo$tipo_solucion == tipo]
            datos_limpios$ph[i] <- ph_imputado
            cat("   Fila", i, "- pH imputado:", round(ph_imputado, 2), "para tipo:", tipo, "\n")
        }
    }
}
#> Imputando valores faltantes de pH por tipo de solución...
#>    Fila 51 - pH imputado: 8.4 para tipo: precipitacion 
#>    Fila 100 - pH imputado: 11.8 para tipo: indicadores
# Estrategia 2: Valores faltantes en variables numéricas - usar mediana
variables_para_imputar <- c("temperatura_C", "concentracion_sal_mg_L", 
                           "concentracion_azucar_g_L", "densidad_g_mL")

for (var in variables_para_imputar) {
    if (var %in% names(datos_limpios) && any(is.na(datos_limpios[[var]]))) {
        mediana_var <- median(datos_limpios[[var]], na.rm = TRUE)
        n_imputados <- sum(is.na(datos_limpios[[var]]))
        datos_limpios[[var]][is.na(datos_limpios[[var]])] <- mediana_var
        cat(var, "- Imputados", n_imputados, "valores con mediana:", round(mediana_var, 2), "\n")
    }
}
#> concentracion_sal_mg_L - Imputados 1 valores con mediana: 325 
#> densidad_g_mL - Imputados 1 valores con mediana: 1.07
# Verificar limpieza
cat("\n=== VERIFICACIÓN POST-LIMPIEZA ===\n")
#> 
#> === VERIFICACIÓN POST-LIMPIEZA ===
cat("Valores faltantes restantes:", sum(is.na(datos_limpios)), "\n")
#> Valores faltantes restantes: 0
if (sum(is.na(datos_limpios)) == 0) {
    cat("Todos los valores faltantes han sido tratados\n")
}
#> Todos los valores faltantes han sido tratados

3.2 Validación de Rangos Químicos

# Función para validar rangos de variables químicas
validar_rangos_quimicos <- function(datos) {
    cat("=== VALIDACIÓN DE RANGOS QUÍMICOS ===\n")
    
    # pH debe estar entre 0 y 14
    ph_invalidos <- sum(datos$ph < 0 | datos$ph > 14, na.rm = TRUE)
    cat("pH fuera del rango 0-14:", ph_invalidos, "valores\n")
    
    # Temperatura debe ser razonable (asumiendo °C entre -10 y 100)
    temp_invalidos <- sum(datos$temperatura_C < -10 | datos$temperatura_C > 100, na.rm = TRUE)
    cat("Temperatura fuera del rango -10°C a 100°C:", temp_invalidos, "valores\n")
    
    # Concentraciones no pueden ser negativas
    conc_sal_negativas <- sum(datos$concentracion_sal_mg_L < 0, na.rm = TRUE)
    cat("Concentraciones de sal negativas:", conc_sal_negativas, "valores\n")
    
    conc_azucar_negativas <- sum(datos$concentracion_azucar_g_L < 0, na.rm = TRUE)
    cat("Concentraciones de azúcar negativas:", conc_azucar_negativas, "valores\n")
    
    # Densidad del agua debe estar cerca de 1.0 g/mL (entre 0.8 y 1.3 es razonable)
    densidad_invalida <- sum(datos$densidad_g_mL < 0.8 | datos$densidad_g_mL > 1.3, na.rm = TRUE)
    cat("Densidad fuera del rango razonable (0.8-1.3 g/mL):", densidad_invalida, "valores\n")
    
    # Volúmenes deben ser positivos
    volumen_invalido <- sum(datos$volumen_muestra_mL <= 0, na.rm = TRUE)
    cat("Volúmenes no positivos:", volumen_invalido, "valores\n")
    
    return(datos)
}

# Aplicar validación
datos_validados <- validar_rangos_quimicos(datos_limpios)
#> === VALIDACIÓN DE RANGOS QUÍMICOS ===
#> pH fuera del rango 0-14: 0 valores
#> Temperatura fuera del rango -10°C a 100°C: 0 valores
#> Concentraciones de sal negativas: 0 valores
#> Concentraciones de azúcar negativas: 0 valores
#> Densidad fuera del rango razonable (0.8-1.3 g/mL): 0 valores
#> Volúmenes no positivos: 0 valores

4 Paso 4: Análisis Exploratorio Univariado

4.1 Variables Categóricas

# Análisis de variables categóricas
cat("=== ANÁLISIS DE VARIABLES CATEGÓRICAS ===\n")
#> === ANÁLISIS DE VARIABLES CATEGÓRICAS ===
# Laboratorios
cat("\n DISTRIBUCIÓN POR LABORATORIO:\n")
#> 
#>  DISTRIBUCIÓN POR LABORATORIO:
tabla_lab <- table(datos_limpios$laboratorio)
prop_lab <- round(prop.table(tabla_lab) * 100, 1)
for (i in 1:length(tabla_lab)) {
    cat("  ", names(tabla_lab)[i], ":", tabla_lab[i], "muestras (", prop_lab[i], "%)\n")
}
#>    Lab_A : 34 muestras ( 34 %)
#>    Lab_B : 33 muestras ( 33 %)
#>    Lab_C : 33 muestras ( 33 %)
# Tipos de solución
cat("\n DISTRIBUCIÓN POR TIPO DE SOLUCIÓN:\n")
#> 
#>  DISTRIBUCIÓN POR TIPO DE SOLUCIÓN:
tabla_tipo <- table(datos_limpios$tipo_solucion)
prop_tipo <- round(prop.table(tabla_tipo) * 100, 1)
for (i in 1:length(tabla_tipo)) {
    cat("  ", names(tabla_tipo)[i], ":", tabla_tipo[i], "muestras (", prop_tipo[i], "%)\n")
}
#>    acido_base : 14 muestras ( 14 %)
#>    azucarada : 16 muestras ( 16 %)
#>    concentracion : 12 muestras ( 12 %)
#>    cristalizacion : 17 muestras ( 17 %)
#>    dilucion : 14 muestras ( 14 %)
#>    indicadores : 2 muestras ( 2 %)
#>    neutralizacion : 8 muestras ( 8 %)
#>    precipitacion : 11 muestras ( 11 %)
#>    salina : 6 muestras ( 6 %)
# Estados físicos
cat("\n DISTRIBUCIÓN POR ESTADO FÍSICO:\n")
#> 
#>  DISTRIBUCIÓN POR ESTADO FÍSICO:
tabla_estado <- table(datos_limpios$estado_fisico)
prop_estado <- round(prop.table(tabla_estado) * 100, 1)
for (i in 1:length(tabla_estado)) {
    cat("  ", names(tabla_estado)[i], ":", tabla_estado[i], "muestras (", prop_estado[i], "%)\n")
}
#>    liquido : 100 muestras ( 100 %)
# Colores
cat("\n DISTRIBUCIÓN POR COLOR:\n")
#> 
#>  DISTRIBUCIÓN POR COLOR:
tabla_color <- table(datos_limpios$color)
tabla_color_ordenada <- sort(tabla_color, decreasing = TRUE)
prop_color <- round(prop.table(tabla_color_ordenada) * 100, 1)
for (i in 1:min(length(tabla_color_ordenada), 8)) {  # Mostrar solo los 8 más frecuentes
    cat("  ", names(tabla_color_ordenada)[i], ":", tabla_color_ordenada[i], 
        "muestras (", prop_color[i], "%)\n")
}
#>    incoloro : 46 muestras ( 46 %)
#>    azul : 14 muestras ( 14 %)
#>    amarillo_claro : 8 muestras ( 8 %)
#>    azul_oscuro : 8 muestras ( 8 %)
#>    amarillo : 6 muestras ( 6 %)
#>    azul_claro : 6 muestras ( 6 %)
#>    verde_claro : 5 muestras ( 5 %)
#>    verde : 3 muestras ( 3 %)

4.2 Variables Numéricas - Estadísticas Descriptivas

# Crear función para estadísticas completas
estadisticas_completas <- function(x, nombre_var) {
    if (!is.numeric(x)) return(NULL)
    
    # Calcular estadísticas
    n <- length(x[!is.na(x)])
    media <- mean(x, na.rm = TRUE)
    mediana <- median(x, na.rm = TRUE)
    
    # Moda (valor más frecuente)
    tabla_freq <- table(x)
    moda <- as.numeric(names(tabla_freq)[which.max(tabla_freq)])
    
    # Medidas de dispersión
    desv_std <- sd(x, na.rm = TRUE)
    varianza <- var(x, na.rm = TRUE)
    rango <- max(x, na.rm = TRUE) - min(x, na.rm = TRUE)
    cv <- (desv_std / media) * 100
    
    # Cuartiles
    Q1 <- quantile(x, 0.25, na.rm = TRUE)
    Q3 <- quantile(x, 0.75, na.rm = TRUE)
    IQR <- Q3 - Q1
    
    return(data.frame(
        Variable = nombre_var,
        N = n,
        Media = round(media, 3),
        Mediana = round(mediana, 3),
        Moda = round(moda, 3),
        Desv_Std = round(desv_std, 3),
        CV_Pct = round(cv, 1),
        Min = round(min(x, na.rm = TRUE), 3),
        Q1 = round(Q1, 3),
        Q3 = round(Q3, 3),
        Max = round(max(x, na.rm = TRUE), 3),
        Rango = round(rango, 3),
        IQR = round(IQR, 3)
    ))
}

# Aplicar a todas las variables numéricas
resumen_completo <- data.frame()
for (var in variables_numericas) {
    if (var %in% names(datos_limpios)) {
        stats <- estadisticas_completas(datos_limpios[[var]], var)
        if (!is.null(stats)) {
            resumen_completo <- rbind(resumen_completo, stats)
        }
    }
}

# Mostrar tabla
kable(resumen_completo, 
      caption = "Estadísticas Descriptivas Completas - Variables Químicas",
      align = "lcccccccccccc")
Estadísticas Descriptivas Completas - Variables Químicas
Variable N Media Mediana Moda Desv_Std CV_Pct Min Q1 Q3 Max Rango IQR
25% ph 100 5.993 6.550 6.800 1.897 31.7 1.200 4.275 7.000 11.800 10.600 2.725
25%1 temperatura_C 100 24.059 23.600 22.800 1.617 6.7 21.200 22.875 25.025 27.600 6.400 2.150
25%2 concentracion_sal_mg_L 100 755.040 325.000 325.000 739.259 97.9 45.000 150.000 1187.500 2185.000 2140.000 1037.500
25%3 concentracion_azucar_g_L 100 20.870 0.000 0.000 48.113 230.5 0.000 0.000 0.000 140.000 140.000 0.000
25%4 volumen_muestra_mL 100 156.000 150.000 100.000 81.116 52.0 50.000 100.000 200.000 300.000 250.000 100.000
25%5 masa_soluto_g 100 10.631 8.800 2.500 8.041 75.6 2.400 3.275 15.225 28.000 25.600 11.950
25%6 densidad_g_mL 100 1.063 1.068 1.015 0.040 3.8 1.012 1.021 1.093 1.128 0.116 0.072
25%7 tiempo_reaccion_min 100 10.555 3.900 0.000 16.455 155.9 0.000 0.000 10.300 48.800 48.800 10.300
# Interpretaciones automáticas
cat("\n=== INTERPRETACIONES AUTOMÁTICAS ===\n")
#> 
#> === INTERPRETACIONES AUTOMÁTICAS ===
for (i in 1:nrow(resumen_completo)) {
    var_name <- resumen_completo$Variable[i]
    cv <- resumen_completo$CV_Pct[i]
    
    cat(var_name, ":\n")
    if (cv < 15) {
        cat("   Variabilidad baja (CV =", cv, "%) - Datos homogéneos\n")
    } else if (cv < 30) {
        cat("   Variabilidad moderada (CV =", cv, "%) - Datos moderadamente dispersos\n")
    } else {
        cat("   Variabilidad alta (CV =", cv, "%) - Datos muy dispersos\n")
    }
    
    # Comparar media vs mediana para detectar asimetría
    media <- resumen_completo$Media[i]
    mediana <- resumen_completo$Mediana[i]
    diff_pct <- abs((media - mediana) / mediana) * 100
    
    if (diff_pct < 5) {
        cat("   Distribución aproximadamente simétrica (media ≈ mediana)\n")
    } else if (media > mediana) {
        cat("   Distribución asimétrica positiva (media > mediana)\n")
    } else {
        cat("   Distribución asimétrica negativa (media < mediana)\n")
    }
    cat("\n")
}
#> ph :
#>    Variabilidad alta (CV = 31.7 %) - Datos muy dispersos
#>    Distribución asimétrica negativa (media < mediana)
#> 
#> temperatura_C :
#>    Variabilidad baja (CV = 6.7 %) - Datos homogéneos
#>    Distribución aproximadamente simétrica (media ≈ mediana)
#> 
#> concentracion_sal_mg_L :
#>    Variabilidad alta (CV = 97.9 %) - Datos muy dispersos
#>    Distribución asimétrica positiva (media > mediana)
#> 
#> concentracion_azucar_g_L :
#>    Variabilidad alta (CV = 230.5 %) - Datos muy dispersos
#>    Distribución asimétrica positiva (media > mediana)
#> 
#> volumen_muestra_mL :
#>    Variabilidad alta (CV = 52 %) - Datos muy dispersos
#>    Distribución aproximadamente simétrica (media ≈ mediana)
#> 
#> masa_soluto_g :
#>    Variabilidad alta (CV = 75.6 %) - Datos muy dispersos
#>    Distribución asimétrica positiva (media > mediana)
#> 
#> densidad_g_mL :
#>    Variabilidad baja (CV = 3.8 %) - Datos homogéneos
#>    Distribución aproximadamente simétrica (media ≈ mediana)
#> 
#> tiempo_reaccion_min :
#>    Variabilidad alta (CV = 155.9 %) - Datos muy dispersos
#>    Distribución asimétrica positiva (media > mediana)