Análisis
exploratorio de datos by
Christian
Ballejo - Instituto Nacional de Epidemiología is licensed under
CC
BY-NC
4.0
El análisis exploratorio de datos (conocido como EDA, su sigla en inglés) es un enfoque de análisis de datos para resumir y visualizar las características importantes de un conjunto de datos.
John Tukey, estadístico estadounidense, fue el principal propulsor contribuyendo de manera significativa al desarrollo del análisis exploratorio de datos al publicar, en 1977, su libro que lleva ese nombre donde entre otras cosas introdujo el gráfico boxplot (diagrama de caja y bigotes).
En términos simples, antes de avanzar con la etapa analítica y de construir modelos estadísticos, es relevante explorar, conocer y describir las variables de interés en nuestra tabla de datos.
Los principales objetivos perseguidos por EDA son:
Aclaración: En este documento mostraremos funciones del lenguaje R que se pueden aplicar en este proceso basadas en la filosofía tidyverse. También aplicaremos otros paquetes diseñados para tareas específicas que le serán de mucha utilidad. Esto no quiere decir que no se pueda hacer la misma exploración con funciones del R base pero el ecosistema facilita el entendimiento de lo que estamos haciendo.
Presentaremos estas diferentes funciones de distintos paquetes que pueden servir en cada etapa de un EDA. Los paquetes con los que trabajaremos son:
Para instalarlos puede copiar y ejecutar el siguiente código:
install.packages(c("tidyverse", "skimr", "janitor", "dlookr"))
Nota: Algunos paquetes, entre estos
dlookr, pueden ocasionar un falso positivo en la detección
del antivirus durante el proceso de instalación. Sugerimos que desactive
momentáneamente su antivirus para instalarlo sin inconvenientes.
Una vez instalados los podemos activar:
library(tidyverse)
library(skimr)
library(janitor)
library(dlookr)
Cabe aclarar que no existe un solo camino y/o función del lenguaje para obtener la información requerida y que esta selección de paquetes puede cambiarse y ampliarse según la conveniencia del usuario. Es decir, aquellxs estudiantes que ya utilicen R y estén familiarizados con funciones y/o paquetes que realicen la misma tarea pueden seguir usándolos.
Con el fin de ejemplificar este análisis exploratorio vamos a utilizar un archivo con datos ficticios y variables de distinto tipo.
El primer paso en la exploración de un conjunto de datos es conocer su estructura y tamaño.
El tamaño está definido por la cantidad de observaciones (filas) y la cantidad de variables (columnas).
Llamamos estructura a la forma en se organizan sus variables, sus tipos de datos y sus categorías/valores.
La función glimpse() del tidyverse le da un vistazo a
los datos:
glimpse(datos)
## Rows: 74
## Columns: 7
## $ id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,…
## $ sexo <chr> "M", "M", "M", "M", "M", "M", "M", "M", NA, "F", "F", "M", "F"…
## $ edad <dbl> 76, 68, 50, 49, 51, 68, 70, 64, 60, 57, 83, 76, 27, 34, 17, 45…
## $ peso <dbl> 71.0, 71.0, 79.0, 71.0, 87.0, 75.0, 80.0, 83.0, 69.0, 73.0, 60…
## $ talla <dbl> 167.0, 164.0, 164.0, 164.0, 167.5, 170.0, 166.0, 160.0, 160.0,…
## $ trabaja <lgl> FALSE, FALSE, FALSE, TRUE, TRUE, FALSE, NA, TRUE, TRUE, TRUE, …
## $ fecha <date> 2020-10-20, 2020-10-20, 2020-10-20, 2020-11-05, 2020-11-05, 2…
Nos informa que la tabla tiene 74 observaciones, 7 variables con su tipo de dato y los primeros valores de cada una al lado.
Los tipos de datos que nos podemos encontrar son:
Esta exploración inicial de la estructura generalmente viene acompañada por el “diccionario de datos” asociado a la tabla de datos, ya sea que esta tabla provenga de un proyecto de investigación propio (fuente primaria) o producto de una fuente secundaria.
En algunas situaciones el tipo de dato del dato coincidirá con la clasificación de la variable (por ejemplo, que sea numérica -dbl- para variables cuantitativas continuas) pero en otros casos podemos tener variables codificadas donde el dato es numérico pero representa una categoría de una variable cualitativa (por ejemplo, si a una variable de respuesta Si - No, la codificamos como 1 y 0).
Sabemos que los valores perdidos o faltantes (conocidos en inglés como missing), que se gestionan en R mediante el valor especial reservado NA, constituyen un serio problema en nuestras variables de análisis.
Existen numerosos libros sobre como tratarlos y sobre diversos algoritmos de imputación que no vamos a incluir en este curso.
Sólo vamos a enfocarnos en como podemos utilizar algunas funciones de R para detectarlos, contabilizarlos y en algunas situaciones excluirlos.
Cada vez que ejecutemos un count() a una variable nos
informará, al final de la tabla de salida, la cantidad de valores
NA.
datos %>%
count(trabaja)
## trabaja n
## 1 FALSE 26
## 2 TRUE 39
## 3 NA 9
Mucho mejor es la función find_na() que proviene del
paquete dlookr:
find_na(datos, rate = T)
## id sexo edad peso talla trabaja fecha
## 0.000 4.054 0.000 0.000 0.000 12.162 0.000
Podemos aplicarla a todo el dataframe y nos dice que porcentaje de valores NA hay en cada variable. En este ejemplo la variable sexo tiene alrededor de un 4 % de valores faltantes y trabaja un poco más de 12 %.
Estos porcentajes, siempre y cuando los NA sean el
resultado de la falta de dato, nos hará decidir si una variable es
apropiada para incluirla en un análisis y/o si conviene excluir
observaciones con esa situación.
El mismo paquete trae una función gráfica llamada
plot_na_pareto()
plot_na_pareto(datos, only_na = T) # solo mostramos las variables con NA
Algo mas completo se logra con diagnose() (también de
dlookr) que diagnostica la calidad de las variables.
diagnose(datos)
## # A tibble: 7 × 6
## variables types missing_count missing_percent unique_count unique_rate
## <chr> <chr> <int> <dbl> <int> <dbl>
## 1 id integer 0 0 74 1
## 2 sexo character 3 4.05 3 0.0405
## 3 edad numeric 0 0 45 0.608
## 4 peso numeric 0 0 56 0.757
## 5 talla numeric 0 0 38 0.514
## 6 trabaja logical 9 12.2 3 0.0405
## 7 fecha Date 0 0 11 0.149
La instalación básica de R tiene incorporadas muchas funciones estadísticas con las cuales calcular medidas resumen de variables cuantitativas que podemos incluir dentro de los resúmenes de tidyverse.
Medidas de tendencia central
Estas medidas son parte de las medidas de posición o localización, pero tiene la intención de resumir la información en torno a un valor central, respecto al cual parece agruparse de un modo más o menos concentrado la distribución de los demás valores.
datos %>%
summarise(media = mean(edad),
mediana = median(edad)) # mediana
## media mediana
## 1 48.06757 52.5
En cambio, no hay ninguna función base que calcule la moda. Tendremos que escribir una forma de cálculo o bien buscar y activar algún paquete extra de las numerosas librerías que tiene el lenguaje R que la tenga implementada.
Medidas de posición
Las medidas de posición dividen un conjunto de datos en grupos con el mismo número de individuos. Entre los más utilizados tenemos los cuartiles y percentiles.
Por ejemplo, con la función quantile(), del paquete
stats, calculamos los cuartiles Q1 y Q3.
Indicamos como argumento los valores 0.25 y 0.75.
datos %>%
summarise(cuartil1 = quantile(edad, probs = 0.25),
cuartil3 = quantile(edad, probs = 0.75))
## cuartil1 cuartil3
## 1 28 64
Si queremos obtener el mínimo y máximo de este conjunto de valores numéricos podemos hacerlo con:
datos %>%
summarise(minimo = min(edad),
maximo = max(edad))
## minimo maximo
## 1 13 86
Medidas de dispersión
Cuando intentamos saber que tan dispersos están los valores o que tan variables son los datos dentro del conjunto de datos, usamos estadísticos de dispersión.
Los clásicos conocidos como la varianza - var() y el
desvío estándar – sd() se pueden aplicar directamente al
summarise().
datos %>%
summarise(varianza = var(edad),
desvio = sd(edad))
## varianza desvio
## 1 404.8036 20.11973
También se puede calcular el rango y el rango intercuartílico.
Para el primero debemos hacer la diferencia entre el máximo y el
mínimo, en cambio para el RIC se usa la función IQR() que
devuelve el rango entre el tercer y el primer cuartil de una
distribución.
datos %>%
summarise(rango = max(edad)-min(edad),
ric = IQR(edad))
## rango ric
## 1 73 36
En dlookr tenemos la función describe()
dedicada a describir variables numéricas.
describe(datos, -id)
| described_variables | n | na | mean | sd | se_mean | IQR | skewness | kurtosis | p00 | p01 | p05 | p10 | p20 | p25 | p30 | p40 | p50 | p60 | p70 | p75 | p80 | p90 | p95 | p99 | p100 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| edad | 74 | 0 | 48.07 | 20.12 | 2.34 | 36.00 | -0.21 | -1.11 | 13 | 14.46 | 17.00 | 19.00 | 24.6 | 28.0 | 34.9 | 46.00 | 52.50 | 56.00 | 61.2 | 64.00 | 65.2 | 73.10 | 76.35 | 83.81 | 86.0 |
| peso | 74 | 0 | 72.49 | 13.23 | 1.54 | 17.65 | 0.14 | 0.21 | 42 | 44.77 | 51.56 | 53.18 | 61.0 | 63.1 | 67.7 | 70.68 | 72.35 | 74.64 | 79.0 | 80.75 | 83.0 | 88.70 | 93.35 | 102.83 | 110.5 |
| talla | 74 | 0 | 164.85 | 8.12 | 0.94 | 9.75 | 0.26 | 0.04 | 148 | 148.73 | 150.32 | 155.00 | 158.9 | 160.0 | 161.0 | 163.00 | 164.00 | 166.00 | 168.1 | 169.75 | 171.4 | 176.25 | 178.35 | 184.27 | 185.0 |
Observemos que se puede aplicar sobre la tabla de datos completa. La propia función seleccionará las variables numéricas, pero con -id le estamos indicando que no tome en cuenta la variable id (el identificador es una variable que no tiene sentido analizar estadísticamente).
Los resultados comprenden: observaciones con datos, observaciones con valores NA, media, desvío estandar, error de la media, intervalo intercuartílico, medidas de forma como la simetría (skewness) y la curtosis (kurtosis) y los quintiles (incluye la mediana p50 y los cuartiles p25-p75).
Las variables cualitativas o categóricas pueden encontrarse bajo el tipo de dato character o factor. Ocasionalmente vamos a necesitar que las variables se encuentren bajo este último formato que el lenguaje R reserva para efectuar algunos procedimientos con estas variables.
Frecuencias y tablas de contingencia
Podemos resumir individualmente variables de tipo cualitativo mediante las frecuencias absolutas y relativas de sus categorías.
count() nos muestra el conteo absoluto de las diferentes
categorías de la variable.
datos %>%
count(sexo)
## sexo n
## 1 F 27
## 2 M 44
## 3 <NA> 3
En la salida, además de las categorías explícitas, aparece el valor
NA con un conteo de 3 observaciones.
Tener en cuenta o no los valores faltantes es una decisión propia del que conduce el análisis y puede cambiar dependiendo de los objetivos buscados.
Una forma de evitarlos con count() es utilizar luego la
función drop_na().
datos %>%
count(sexo) %>%
drop_na() # saltea los valores NA
## sexo n
## 1 F 27
## 2 M 44
Obtenemos frecuencias relativas porcentuales así:
datos %>%
count(sexo) %>%
drop_na() %>%
mutate(porc = 100*n/sum(n))
## sexo n porc
## 1 F 27 38.02817
## 2 M 44 61.97183
Redondeamos el valor del porcentaje con round().
datos %>%
count(sexo) %>%
drop_na() %>%
mutate(porc = 100*n/sum(n),
porc = round(porc, digits = 2))
## sexo n porc
## 1 F 27 38.03
## 2 M 44 61.97
Una opción más completa es utilizar funciones del paquete
janitor como tabyl():
datos %>%
tabyl(sexo)
## sexo n percent valid_percent
## F 27 0.36486486 0.3802817
## M 44 0.59459459 0.6197183
## <NA> 3 0.04054054 NA
Calcula las frecuencias relativas incluyendo y no incluyendo los valores NA (porcentaje de valores válidos).
Podemos modificar sus argumentos y asociar otras funciones del paquete mediante tuberías para obtener mejores resultados (es compatible con tidyverse).
datos %>%
tabyl(sexo, show_na = F) %>% # anulamos valores na
adorn_totals(where = "row") %>% # agregamos totales
adorn_pct_formatting(digits = 2) # porcentaje con dos decimales
## sexo n percent
## F 27 38.03%
## M 44 61.97%
## Total 71 100.00%
Tablas de contingencia
La forma más adecuada de describir la relación entre dos variables categóricas es a partir de la construcción de una tabla de contingencia. Para ello se introduce en cada fila de la tabla las categorías de una de las variables y las categorías de la otra variable se asocian a cada una de las columnas de la tabla, en cada celda de la tabla aparecerá el número de observaciones correspondientes a la combinación oportuna de ambas variables.
Con la misma función tabyl() se puede realizar una tabla
de contingencia, incluyendo a la variable trabaja (aunque tenga
formato lógico puede utilizarse igual si conceptualmente la variable es
categórica):
datos %>%
tabyl(sexo, trabaja)
## sexo FALSE TRUE NA_
## F 8 15 4
## M 17 22 5
## <NA> 1 2 0
Recordemos que en orden dentro de los paréntesis de la función es igual al de los índices, el primer argumento es la variable que aparecerá en las filas y el segundo la variable de las columnas. Por ese motivo, en la tabla de contingencia absoluta tenemos el sexo en las filas y a trabaja en las columnas.
Se puede mejorar sin valores NA y agregando totales:
datos %>%
tabyl(sexo, trabaja, show_na = F) %>%
adorn_totals(where = "row")
## sexo FALSE TRUE
## F 8 15
## M 17 22
## Total 25 37
Calculamos frecuencias relativas porcentuales por columna:
datos %>%
tabyl(sexo, trabaja, show_na = F) %>%
adorn_totals(where = "row") %>%
adorn_percentages(denominator = "col") %>% # % por columna
adorn_pct_formatting(digits = 2) # redondea con 2 decimales
## sexo FALSE TRUE
## F 32.00% 40.54%
## M 68.00% 59.46%
## Total 100.00% 100.00%
Calculamos frecuencias relativas porcentuales por fila:
datos %>%
tabyl(sexo, trabaja, show_na = F) %>%
adorn_totals(where = "col") %>%
adorn_percentages(denominator = "row") %>% # % por fila
adorn_pct_formatting(digits = 2) # redondea con 2 decimales
## sexo FALSE TRUE Total
## F 34.78% 65.22% 100.00%
## M 43.59% 56.41% 100.00%
Cambiando el argumento denominator de
adorn_percentages() a “all” se calculan relativas
al total.
datos %>%
tabyl(sexo, trabaja, show_na = F) %>%
adorn_totals(where = "col") %>%
adorn_percentages(denominator = "all") %>% # % por total
adorn_pct_formatting(digits = 2) # redondea con 2 decimales
## sexo FALSE TRUE Total
## F 12.90% 24.19% 37.10%
## M 27.42% 35.48% 62.90%
Uno de los aportes más relevantes de Tukey respecto al análisis es el uso de los gráficos cómo método exploratorio.
Los gráficos básicos más útiles que muestran la distribución univariada de variables que podemos hacer con R son:
Cuando interviene más de una variable aparecen comúnmente los puntos, las líneas y los gráficos de mosaico.
El lenguaje R soporta una serie de sistemas gráficos asociados a paquetes como graphics, lattice, ggplot2, etc. que sirven de base incluso para otros paquetes con funciones más específicas. Actualmente el estándar gráfico en R es ggplot2.
En el documento referido a tidyverse explicamos el funcionamiento de ggplot2 y sus capas gráficas.
Ahora sólo ejecutaremos los distintos elementos geométricos para representar los diferentes gráficos mencionados.
Barras en univariado
datos %>% drop_na(sexo) %>% # omitimos los NA de sexo
ggplot(aes(x = sexo, fill = sexo)) +
geom_bar() +
scale_fill_manual(values = c("palevioletred4", "orange"))
Barras en bivariado - posición stack
datos %>% drop_na(sexo, trabaja) %>% # omitimos los NA de sexo y trabaja
ggplot(aes(x = sexo, fill = trabaja)) +
geom_bar(position = "stack") +
scale_fill_brewer(palette = "Set1")
Barras en bivariado - posición dodge
datos %>% drop_na(sexo, trabaja) %>% # omitimos los NA de sexo y trabaja
ggplot(aes(x = sexo, fill = trabaja)) +
geom_bar(position = "dodge") +
scale_fill_brewer(palette = "Set3")
Barras en bivariado - posición fill
datos %>% drop_na(sexo, trabaja) %>% # omitimos los NA de sexo y trabaja
ggplot(aes(x = sexo, fill = trabaja)) +
geom_bar(position = "fill") +
scale_fill_brewer(palette = "Accent")
Histograma
datos %>%
ggplot(aes(x = edad)) +
geom_histogram(binwidth = 10, # intervalos de 10 años
fill = "royalblue1",
color = "white")
Densidad
datos %>%
ggplot(aes(x = edad)) +
geom_density(fill = "thistle1")
Boxplot
datos %>%
ggplot(aes(x = edad)) +
geom_boxplot(fill = "seagreen4")
Violinplot
datos %>% drop_na(sexo) %>% # omitimos los NA de sexo
ggplot(aes(x = edad, y = sexo, fill = sexo)) +
geom_violin() +
scale_fill_brewer(palette = "Set2")
Q-Q Plot
Los Q-Q Plot (Cuantil-Cuantil) son gráficos especiales que permiten observar cuan cerca está la distribución de un conjunto de datos a alguna distribución ideal o comparar la distribución de dos conjuntos de datos.
Suele usarse como método gráfico para analizar “normalidad”, es decir cuanto se asemeja la distribución de la variable a la distribución normal (“gaussiana”).
La función plot_normality() de dlookr muestra un
diagnóstico gráfico de normalidad de una variable usando histogramas y
Q-Q plot. Además muestra otros histogramas con conversiones de datos
(logarítmico y raíz cuadrada por defecto, pero también “Box-Cox” y
otras)
Sobre la variable edad:
datos %>%
plot_normality(edad)
Sobre la variable peso:
datos %>%
plot_normality(peso)
En la comparación podemos decir que los puntos se desarrollan mas cercanos a la línea teórica normal para la variable peso que para la variable edad.
Estos gráficos siempre se analizan acompañados por test de bondad de ajuste específicos de normalidad que veremos en la unidad 2.
Un valor atípico (en inglés outlier) es una observación que está numéricamente distante del resto de los datos.
Los outliers pueden deberse a:
Estos valores desproporcionados pueden conducir a interpretar erróneamente algunos estadísticos como la media, ya que los distorsionan.
Una forma habitual de detección es mediante los gráficos boxplot.
Veamos el ejemplo con la variable peso donde existe un valor considerado atípico en el extremo superior de la distribución (punto rojo):
datos %>%
ggplot(aes(x = peso)) +
geom_boxplot(fill = "darkkhaki", outlier.color = "red")
Coincide con el valor máximo de 110.5 kgrs.
Dentro de los paquetes incluidos en este documento la función
diagnose_outlier() de dlookr esta pensada
para la detección de datos atípicos.
diagnose_outlier(datos)
## variables outliers_cnt outliers_ratio outliers_mean with_mean without_mean
## 1 id 0 0.000000 NaN 37.50000 37.50000
## 2 edad 0 0.000000 NaN 48.06757 48.06757
## 3 peso 1 1.351351 110.5 72.48649 71.96575
## 4 talla 0 0.000000 NaN 164.85135 164.85135
Se puede aplicar a todo el conjunto de datos y nos devuelve una tabla con la cantidad de outliers detectados por variable, la proporción, la media considerando estos valores y la media sin considerarlos.
En función de estos dos estadísticos se puede comparar el efecto de los valores atípicos en la media.
Finalmente una función interesante por lo sencillo de su uso es
skim() de skimr.
skim(datos)
| skim_type | skim_variable | n_missing | complete_rate | Date.min | Date.max | Date.median | Date.n_unique | character.min | character.max | character.empty | character.n_unique | character.whitespace | logical.mean | logical.count | numeric.mean | numeric.sd | numeric.p0 | numeric.p25 | numeric.p50 | numeric.p75 | numeric.p100 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | fecha | 0 | 1.0000000 | 2020-10-20 | 2020-12-15 | 2020-11-11 | 11 | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA |
| character | sexo | 3 | 0.9594595 | NA | NA | NA | NA | 1 | 1 | 0 | 2 | 0 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
| logical | trabaja | 9 | 0.8783784 | NA | NA | NA | NA | NA | NA | NA | NA | NA | 0.6 | TRU: 39, FAL: 26 | NA | NA | NA | NA | NA | NA | NA |
| numeric | id | 0 | 1.0000000 | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | 37.50000 | 21.505813 | 1 | 19.25 | 37.50 | 55.75 | 74.0 |
| numeric | edad | 0 | 1.0000000 | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | 48.06757 | 20.119731 | 13 | 28.00 | 52.50 | 64.00 | 86.0 |
| numeric | peso | 0 | 1.0000000 | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | 72.48649 | 13.231918 | 42 | 63.10 | 72.35 | 80.75 | 110.5 |
| numeric | talla | 0 | 1.0000000 | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | 164.85135 | 8.120551 | 148 | 160.00 | 164.00 | 169.75 | 185.0 |
Muestra en una tabla características y estadísticos descriptivos de las variables del dataframe y se puede combinar dentro de estructuras tidyverse con tuberías.
datos %>%
drop_na(sexo) %>%
group_by(sexo) %>%
select(where(is.numeric), -id) %>% # solo variables numéricas - id
skim()
## Adding missing grouping variables: `sexo`
| skim_type | skim_variable | sexo | n_missing | complete_rate | numeric.mean | numeric.sd | numeric.p0 | numeric.p25 | numeric.p50 | numeric.p75 | numeric.p100 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| numeric | edad | F | 0 | 1 | 41.88889 | 19.641662 | 15.0 | 26.00 | 39.0 | 54.50 | 83.0 |
| numeric | edad | M | 0 | 1 | 51.59091 | 19.558283 | 13.0 | 40.75 | 55.5 | 64.75 | 86.0 |
| numeric | peso | F | 0 | 1 | 63.87778 | 12.016154 | 42.0 | 53.30 | 61.0 | 72.00 | 85.6 |
| numeric | peso | M | 0 | 1 | 77.96591 | 11.492443 | 51.8 | 71.00 | 77.2 | 83.75 | 110.5 |
| numeric | talla | F | 0 | 1 | 158.57407 | 6.077550 | 148.0 | 155.00 | 159.5 | 162.25 | 172.0 |
| numeric | talla | M | 0 | 1 | 168.85227 | 6.577427 | 158.0 | 164.00 | 167.0 | 173.00 | 185.0 |
En este ejemplo, mostramos resultados de variables numéricas menos de
id agrupados por sexo (sin considerar valores NA en las
categorías de sexo)