Al finalizar esta actividad, usted será capaz de:
📚 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.
# 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
#> === ESTRUCTURA DEL DATASET ===
#> '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" ...
#>
#> === PRIMERAS 6 FILAS ===
#>
#> === ÚLTIMAS 6 FILAS ===
#>
#> === NOMBRES DE LAS VARIABLES ===
#> [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"
#> === RESUMEN ESTADÍSTICO INICIAL ===
#> 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 ===
#> Número total de observaciones: 100
#> 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
#> Laboratorios incluidos: Lab_A, Lab_B, Lab_C
#> Tipos de solución: acido_base, salina, azucarada, precipitacion, neutralizacion, concentracion, dilucion, cristalizacion, indicadores
# 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")
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 |
#>
#> === RESUMEN DE VALORES FALTANTES ===
#> 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 %)
# Buscar duplicados completos
duplicados_completos <- sum(duplicated(datos_quimica))
cat("=== ANÁLISIS DE DUPLICADOS ===\n")
#> === ANÁLISIS DE DUPLICADOS ===
#> 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
# 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")
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 |
# 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
#>
#> === VERIFICACIÓN POST-LIMPIEZA ===
#> Valores faltantes restantes: 0
#> Todos los valores faltantes han sido tratados
# 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
#> === ANÁLISIS DE VARIABLES CATEGÓRICAS ===
#>
#> 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 %)
#>
#> 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 %)
#>
#> 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 %)
#>
#> 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 %)
# 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")
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 ===
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)