Introducción al Análisis de Datos para Otras Carreras
Tamaño, estructura, tipos de variables, primeras 10 filas
Valores faltantes, duplicados, outliers, inconsistencias
Decisiones sobre missing data, duplicados, transformaciones
Distribuciones, estadísticas descriptivas por variable
Correlaciones, relaciones entre variables
Patrones identificados, hipótesis generadas
"Siempre visualiza antes de calcular, siempre cuestiona antes de asumir"
# Evaluación inicial de los datos str(datos) # Estructura del dataframe head(datos) # Primeras 6 filas tail(datos) # Últimas 6 filas dim(datos) # Dimensiones (filas x columnas) # Resumen estadístico summary(datos) # Estadísticas descriptivas # Evaluación de calidad sum(is.na(datos)) # Total de valores faltantes colSums(is.na(datos)) # Valores faltantes por columna sum(duplicated(datos)) # Filas completamente duplicadas # Valores únicos sapply(datos, function(x) length(unique(x))) # Variables categóricas table(datos$variable_categorica) # Frecuencias
Cuándo usarlo:
Media: Para datos numéricos simétricos
Mediana: Para datos asimétricos
Moda: Para datos categóricos
CUIDADO: Los outliers pueden ser:
Discretos (Contables):
Continuos (Medibles):
Nominales (Sin orden):
Ordinales (Con orden):
Escala | Características | Operaciones Válidas | Estadísticas Apropiadas | Ejemplos |
---|---|---|---|---|
Nominal | Solo clasificación Sin orden natural |
= ≠ Conteo Frecuencias |
Moda Proporciones Chi-cuadrado |
Color, género, marca, tipo de reacción |
Ordinal | Clasificación + orden Distancias no iguales |
= ≠ < > Ranking Percentiles |
Mediana Cuartiles Correlación de Spearman |
Satisfacción, nivel educativo, grado de dolor |
Intervalo | Orden + distancias iguales Sin cero absoluto |
= ≠ < > + - Diferencias Promedios |
Media Desviación estándar Correlación de Pearson |
Temperatura en °C, IQ, fechas |
Razón | Intervalo + cero absoluto Todas las propiedades |
= ≠ < > + - × ÷ Ratios Proporciones |
Media geométrica Coeficiente de variación Todas las estadísticas |
Altura, peso, ingresos, temperatura en Kelvin |
Error Común: Aplicar estadísticas inapropiadas para el tipo de escala puede llevar a conclusiones incorrectas. Por ejemplo, calcular la media de satisfacción en escala Likert (ordinal) puede ser problemático.
Cuándo usar:
Ejemplo: Distribución de edades, alturas, tiempos de respuesta
Buena práctica: Experimenta con diferentes números de bins (5-30 dependiendo del tamaño de datos)
Cuándo usar:
Ejemplo: Ventas por región, número de estudiantes por carrera
Evitar: Gráficos 3D innecesarios que distorsionan la percepción
Cuándo usar:
Ejemplo: Población representada con figuras humanas
Cuidado: Pueden distorsionar proporciones si no se escalan correctamente
Cuándo usar:
Ejemplo: Comparar distribuciones de calificaciones entre diferentes cursos
Ventaja: Más claro que múltiples histogramas superpuestos
# Paletas accesibles recomendadas en R library(RColorBrewer) library(viridis) # Paletas para datos categóricos RColorBrewer::display.brewer.all(colorblindFriendly = TRUE) brewer.pal(8, "Set2") # Colores suaves y distinguibles brewer.pal(8, "Dark2") # Colores más intensos # Paletas para datos continuos viridis(10) # Paleta viridis (amarillo-verde-azul) plasma(10) # Paleta plasma (rosa-púrpura) cividis(10) # Diseñada específicamente para daltonismo # Ejemplo de uso en ggplot2 ggplot(data) + geom_point(aes(x = var1, y = var2, color = categoria)) + scale_color_brewer(type = "qual", palette = "Set2") # EVITAR: combinaciones problemáticas # - Rojo y verde juntos (daltonismo más común) # - Colores muy similares en tonalidad # - Más de 8-10 colores categóricos diferentes
Usar cuando:
Evitar cuando:
Ejemplo problemático: Ingresos familiares con algunos millonarios en la muestra → la media será engañosa
Mediana - El valor del medio:
Moda - El valor más frecuente:
Regla práctica:
Interpretación:
Ventajas del IQR:
# Cálculo de cuartiles y percentiles Q1 <- quantile(datos$variable, 0.25) # Primer cuartil Q2 <- quantile(datos$variable, 0.50) # Mediana Q3 <- quantile(datos$variable, 0.75) # Tercer cuartil # Percentiles específicos P10 <- quantile(datos$variable, 0.10) # Percentil 10 P90 <- quantile(datos$variable, 0.90) # Percentil 90 # Resumen completo de cuartiles summary(datos$variable) # Min, Q1, Median, Q3, Max # Rango intercuartílico IQR_valor <- IQR(datos$variable) # Función directa # O manualmente: IQR_manual <- Q3 - Q1
# Detección de outliers usando IQR Q1 <- quantile(datos$variable, 0.25) Q3 <- quantile(datos$variable, 0.75) IQR_val <- Q3 - Q1 # Definir límites limite_inferior <- Q1 - 1.5 * IQR_val limite_superior <- Q3 + 1.5 * IQR_val # Identificar outliers outliers <- datos[datos$variable < limite_inferior | datos$variable > limite_superior, ] # Contar outliers num_outliers <- nrow(outliers) cat("Outliers encontrados:", num_outliers) # Boxplot automático (muestra outliers) boxplot(datos$variable, main = "Detección de Outliers")
¿Por qué n-1 en la muestra?
Regla Empírica (distribución normal):
Interpretación del CV:
¿Cuándo usar CV?
Limitación: CV no es apropiado cuando la media es cercana a cero o puede ser negativa
if/else/elif: Tomar decisiones basadas en condiciones
for/while: Repetir operaciones múltiples veces
# Estructura básica if (condicion) { accion1 } else if (otra_condicion) { accion2 } else { accion3 } # Función ifelse() para vectores resultado <- ifelse(condicion, valor_si_verdadero, valor_si_falso) # ifelse() anidado resultado <- ifelse(condicion1, "opcion1", ifelse(condicion2, "opcion2", "opcion3"))
# Función personalizada clasificar_edad <- function(edad) { if (edad < 18) { return("Menor de edad") } else if (edad < 65) { return("Adulto") } else { return("Adulto mayor") } } # Aplicar a todo el dataset datos$categoria_edad <- sapply(datos$edad, clasificar_edad) # Alternativa con ifelse (más eficiente) datos$categoria_edad <- ifelse(datos$edad < 18, "Menor de edad", ifelse(datos$edad < 65, "Adulto", "Adulto mayor"))
# Calcular límites IQR Q1 <- quantile(datos$variable, 0.25) Q3 <- quantile(datos$variable, 0.75) IQR_val <- Q3 - Q1 # Función para clasificar valores clasificar_outlier <- function(valor) { if (valor < Q1 - 1.5 * IQR_val) { return("Outlier inferior") } else if (valor > Q3 + 1.5 * IQR_val) { return("Outlier superior") } else { return("Normal") } } # Aplicar clasificación datos$tipo_valor <- sapply(datos$variable, clasificar_outlier)
# Validar concentraciones químicas (ppm) validar_concentracion <- function(conc) { if (is.na(conc)) { return("Dato faltante") } else if (conc < 0) { return("Valor imposible") } else if (conc == 0) { return("Sin detectar") } else if (conc > 1000) { return("Concentración alta") } else { return("Normal") } } # Aplicar validación datos$validez_conc <- sapply(datos$concentracion, validar_concentracion) # Contar por categoría table(datos$validez_conc)
# Imputación basada en condiciones imputar_ph <- function(row) { if (is.na(row["ph"])) { if (row["tipo_muestra"] == "agua_potable") { return(7.0) # pH neutro típico } else if (row["tipo_muestra"] == "suelo_acido") { return(5.5) # pH ácido típico } else { return(6.8) # pH promedio general } } else { return(row["ph"]) } } # Aplicar usando apply por filas datos$ph_imputado <- apply(datos, 1, imputar_ph) # Verificar imputación sum(is.na(datos$ph)) # Antes sum(is.na(datos$ph_imputado)) # Después
# Procesar múltiples archivos de laboratorio archivos <- list.files(pattern = "analisis_.*\\.csv") lista_datos <- list() for (i in 1:length(archivos)) { # Leer cada archivo datos_lab <- read.csv(archivos[i]) # Agregar información de fuente datos_lab$archivo_origen <- archivos[i] datos_lab$fecha_procesado <- Sys.Date() # Guardar en lista lista_datos[[i]] <- datos_lab } # Combinar todos los datos datos_completos <- do.call(rbind, lista_datos) cat("Procesados", length(archivos), "archivos") cat("Total de filas:", nrow(datos_completos))
# Estadísticas por tipo de muestra química tipos_muestra <- unique(datos$tipo_muestra) resultados <- data.frame() for (tipo in tipos_muestra) { # Filtrar por tipo subconjunto <- datos[datos$tipo_muestra == tipo, ] # Calcular estadísticas stats <- data.frame( tipo_muestra = tipo, n_muestras = nrow(subconjunto), ph_promedio = mean(subconjunto$ph, na.rm = TRUE), ph_desv_std = sd(subconjunto$ph, na.rm = TRUE), concentracion_max = max(subconjunto$concentracion, na.rm = TRUE) ) # Agregar a resultados resultados <- rbind(resultados, stats) } # Ver resultados print(resultados)
# Crear histogramas para múltiples variables químicas library(ggplot2) variables_quimicas <- c("ph", "concentracion", "turbidez", "conductividad") for (var in variables_quimicas) { # Crear gráfico p <- ggplot(datos, aes_string(x = var)) + geom_histogram(bins = 20, fill = "steelblue", alpha = 0.7) + labs( title = paste("Distribución de", var), x = var, y = "Frecuencia" ) + theme_minimal() # Guardar gráfico nombre_archivo <- paste0("histograma_", var, ".png") ggsave(nombre_archivo, p, width = 8, height = 6) cat("Gráfico guardado:", nombre_archivo, "\n") } # Crear boxplots por grupo for (var in variables_quimicas) { p <- ggplot(datos, aes_string(x = "tipo_muestra", y = var)) + geom_boxplot(fill = "lightblue") + labs(title = paste("Boxplot de", var, "por tipo de muestra")) ggsave(paste0("boxplot_", var, ".png"), p) }
# Limpieza iterativa de outliers extremos datos_limpios <- datos outliers_encontrados <- TRUE iteracion <- 0 max_iteraciones <- 5 while (outliers_encontrados && iteracion < max_iteraciones) { # Calcular límites IQR Q1 <- quantile(datos_limpios$concentracion, 0.25, na.rm = TRUE) Q3 <- quantile(datos_limpios$concentracion, 0.75, na.rm = TRUE) IQR_val <- Q3 - Q1 # Detectar outliers extremos limite_inf <- Q1 - 3 * IQR_val # Más estricto (3 x IQR) limite_sup <- Q3 + 3 * IQR_val outliers <- which(datos_limpios$concentracion < limite_inf | datos_limpios$concentracion > limite_sup) if (length(outliers) == 0) { outliers_encontrados <- FALSE cat("No se encontraron más outliers extremos\n") } else { # Remover outliers datos_limpios <- datos_limpios[-outliers, ] iteracion <- iteracion + 1 cat("Iteración", iteracion, "- Removidos", length(outliers), "outliers\n") } } cat("Proceso completado en", iteracion, "iteraciones") cat("Datos finales:", nrow(datos_limpios), "de", nrow(datos), "originales")
# Ineficiente - Bucle innecesario resultado_lento <- c() for (i in 1:nrow(datos)) { if (datos$ph[i] > 7) { resultado_lento <- c(resultado_lento, datos$muestra_id[i]) } } # Eficiente - Vectorización resultado_rapido <- datos$muestra_id[datos$ph > 7] # Muy lento - Concatenar vectores en bucle concentraciones_altas <- c() for (i in 1:nrow(datos)) { if (datos$concentracion[i] > 100) { # ¡Esto recrea el vector cada vez! concentraciones_altas <- c(concentraciones_altas, datos$concentracion[i]) } } # Más eficiente - Pre-asignar espacio indices_altas <- which(datos$concentracion > 100) concentraciones_altas <- datos$concentracion[indices_altas]
# Bucle para calcular medias por grupo grupos <- unique(datos$tipo_muestra) medias_por_grupo <- numeric() for (grupo in grupos) { subset_datos <- datos[datos$tipo_muestra == grupo, ] media_grupo <- mean(subset_datos$ph, na.rm = TRUE) medias_por_grupo <- c(medias_por_grupo, media_grupo) } # Más eficiente con aggregate() medias_por_grupo <- aggregate(ph ~ tipo_muestra, datos, mean, na.rm = TRUE) # O con tapply() medias_tapply <- tapply(datos$ph, datos$tipo_muestra, mean, na.rm = TRUE)
# Validación robusta al procesar archivos archivos_laboratorio <- list.files(pattern = "\\.csv$") for (archivo in archivos_laboratorio) { # Verificar que el archivo existe if (file.exists(archivo)) { # Manejo de errores con tryCatch datos_archivo <- tryCatch({ read.csv(archivo, stringsAsFactors = FALSE) }, error = function(e) { cat("Error leyendo", archivo, ":", e$message, "\n") return(NULL) }) # Continuar solo si la lectura fue exitosa if (!is.null(datos_archivo)) { # Validar estructura esperada if (all(c("muestra_id", "ph", "concentracion") %in% names(datos_archivo))) { # Procesar datos... cat("Procesado exitosamente:", archivo, "\n") } else { cat("Columnas faltantes en", archivo, "\n") } } } else { cat("Archivo no encontrado:", archivo, "\n") } }
"El EDA no es solo una fase previa al análisis, es la conversación inicial con nuestros datos. Entre mejor sea esta conversación, más reveladores serán los insights que obtengamos."
¡Gracias por su atención!
¿Preguntas sobre el proceso EDA o estructuras de control?