import pandas as pd
from lets_plot import *
from lets_plot.mapping import as_discrete
from pandas.api.types import CategoricalDtype
from skimpy import skim
LetsPlot.setup_html()Análisis exploratorio de datos
Introducción
Este capítulo te mostrará cómo usar la visualización y la transformación para explorar tus datos de forma sistemática, una tarea que los científicos de datos llaman análisis exploratorio de datos, o EDA (por sus siglas en inglés). El EDA es un ciclo iterativo en el que:
Generas preguntas sobre tus datos.
Buscas respuestas visualizando, transformando y modelando tus datos.
Usas lo que aprendes para refinar tus preguntas y/o generar preguntas nuevas.
El EDA no es un proceso formal con un conjunto estricto de reglas y, durante sus fases iniciales, debes sentirte libre de investigar cada idea que se te ocurra. Algunas de estas ideas darán frutos y otras serán callejones sin salida. A medida que avance tu exploración, te irás centrando en unas pocas áreas especialmente productivas que, con el tiempo, pondrás por escrito y comunicarás a otras personas. Mientras exploras tus datos, recuerda que hay algunas trampas: siempre debes pensar en cómo se recopilaron los datos, qué podría faltar, si hay problemas de calidad, y ser muy estricto con las diferencias entre correlación y causalidad (¡esto es un tema enorme en sí mismo!).
Requisitos previos
Para hacer EDA, usaremos los paquetes pandas, skimpy y pandas-profiling. También necesitaremos lets-plot para la visualización de datos. Todos ellos se pueden instalar con uv add <packagename>.
Como siempre, empezamos cargando los paquetes que vamos a usar:
Preguntas
“No hay preguntas estadísticas rutinarias, solo rutinas estadísticas cuestionables.” — Sir David Cox
“Es mucho mejor una respuesta aproximada a la pregunta correcta, que a menudo es vaga, que una respuesta exacta a la pregunta equivocada, que siempre puede hacerse precisa.” — John Tukey
Tu objetivo durante el EDA es llegar a comprender tus datos. La forma más sencilla de hacerlo es usar preguntas como herramientas para guiar tu investigación. Cuando haces una pregunta, esta centra tu atención en una parte concreta de tu dataset y te ayuda a decidir qué gráficos, modelos o transformaciones hacer.
El EDA es, en esencia, un proceso creativo. Y, como en la mayoría de los procesos creativos, la clave para hacer preguntas de calidad es generar una gran cantidad de preguntas. Es difícil hacer preguntas reveladoras al comienzo del análisis porque no sabes qué hallazgos contiene tu dataset. Por otro lado, cada nueva pregunta que hagas te mostrará un nuevo aspecto de tus datos y aumentará tus posibilidades de hacer un descubrimiento. Puedes profundizar rápidamente en las partes más interesantes de tus datos —y desarrollar un conjunto de preguntas sugerentes— si a cada pregunta le sigues con otra nueva basada en lo que encuentres.
No hay ninguna regla sobre qué preguntas debes hacer para guiar tu investigación. Sin embargo, dos tipos de preguntas siempre serán útiles para hacer descubrimientos en tus datos. Puedes formularlas, a grandes rasgos, así:
¿Qué tipo de variación se produce dentro de mis variables?
¿Qué tipo de covariación se produce entre mis variables?
El resto de este capítulo examinará estas dos preguntas. Explicaremos qué son la variación y la covariación, y te mostraremos varias formas de responder a cada pregunta.
Variación
La variación es la tendencia de los valores de una variable a cambiar de una medición a otra. Puedes ver la variación fácilmente en la vida real: si mides dos veces cualquier variable continua, obtendrás dos resultados diferentes. Esto ocurre incluso si mides magnitudes constantes, como la velocidad de la luz, debido a las limitaciones de los equipos. Cada una de tus mediciones incluye una pequeña cantidad de error que varía de una medición a otra. Las variables también pueden variar si mides en distintos sujetos (p. ej., el color de ojos de diferentes personas) o en distintos momentos (p. ej., los niveles de energía de un electrón en diferentes instantes). La mejor forma de entender ese patrón es visualizar la distribución de los valores de la variable.
Empezaremos nuestra exploración visualizando la distribución de los pesos ("carat") de ~54.000 diamantes del dataset diamonds.
diamonds = pd.read_csv(
"https://github.com/mwaskom/seaborn-data/raw/master/diamonds.csv"
)
diamonds["cut"] = diamonds["cut"].astype(
CategoricalDtype(
categories=["Fair", "Good", "Very Good", "Premium", "Ideal"], ordered=True
)
)
diamonds["color"] = diamonds["color"].astype(
CategoricalDtype(categories=["D", "E", "F", "G", "H", "I", "J"], ordered=True)
)
diamonds.head()| carat | cut | color | clarity | depth | table | price | x | y | z | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.23 | Ideal | E | SI2 | 61.5 | 55.0 | 326 | 3.95 | 3.98 | 2.43 |
| 1 | 0.21 | Premium | E | SI1 | 59.8 | 61.0 | 326 | 3.89 | 3.84 | 2.31 |
| 2 | 0.23 | Good | E | VS1 | 56.9 | 65.0 | 327 | 4.05 | 4.07 | 2.31 |
| 3 | 0.29 | Premium | I | VS2 | 62.4 | 58.0 | 334 | 4.20 | 4.23 | 2.63 |
| 4 | 0.31 | Good | J | SI2 | 63.3 | 58.0 | 335 | 4.34 | 4.35 | 2.75 |
Como "carat" es una variable numérica, podemos usar un histograma:
(ggplot(diamonds, aes(x="carat")) + geom_histogram(binwidth=0.5))Ahora que sabes visualizar la variación, ¿qué deberías buscar en tus gráficos? ¿Y qué tipo de preguntas de seguimiento deberías hacer? A continuación hemos reunido una lista de los tipos de información más útiles que encontrarás en tus gráficos, junto con algunas preguntas de seguimiento para cada tipo de información. La clave para hacer buenas preguntas de seguimiento será apoyarte en tu curiosidad (¿sobre qué quieres saber más?) y en tu escepticismo (¿cómo podría esto ser engañoso?).
Valores típicos
Tanto en los gráficos de barras como en los histogramas, las barras altas muestran los valores comunes de una variable y las barras más bajas muestran los valores menos comunes. Los lugares sin barras revelan valores que no aparecieron en tus datos. Para convertir esta información en preguntas útiles, busca cualquier cosa inesperada:
¿Qué valores son los más comunes? ¿Por qué?
¿Qué valores son raros? ¿Por qué? ¿Coincide eso con lo que esperabas?
¿Ves algún patrón inusual? ¿Qué podría explicarlo?
Veamos la distribución de "carat" para los diamantes más pequeños.
Ten en cuenta que, al crear smaller_diamonds, lo hacemos creando una copia. De lo contrario, cualquier cambio que hiciéramos en smaller_diamonds también afectaría a diamonds (ambos apuntarían a los mismos datos subyacentes en la memoria de tu computadora). A veces querrás que un subconjunto siga conectado al dataset original y otras veces no; en este caso, queremos que sean distintos, así que usamos copy().
smaller_diamonds = diamonds.query("carat < 3").copy()
(ggplot(smaller_diamonds, aes(x="carat")) + geom_histogram(binwidth=0.01))Este histograma sugiere varias preguntas interesantes:
¿Por qué hay más diamantes en quilates enteros y en fracciones comunes de quilate?
¿Por qué hay más diamantes ligeramente a la derecha de cada pico que ligeramente a la izquierda?
Las visualizaciones también pueden revelar agrupaciones (clusters), que sugieren que existen subgrupos en tus datos. Para entender los subgrupos, pregúntate:
¿En qué se parecen entre sí las observaciones dentro de cada subgrupo?
¿En qué se diferencian entre sí las observaciones de agrupaciones distintas?
¿Cómo puedes explicar o describir las agrupaciones?
¿Por qué podría ser engañosa la aparición de agrupaciones?
Algunas de estas preguntas pueden responderse con los datos, mientras que otras requerirán conocimiento experto del dominio de los datos. Muchas de ellas te llevarán a explorar una relación entre variables, por ejemplo, para ver si los valores de una variable pueden explicar el comportamiento de otra. Llegaremos a eso en breve.
Valores inusuales
Los valores atípicos (outliers) son observaciones inusuales; puntos de datos que no parecen encajar en el patrón. A veces los valores atípicos son errores de introducción de datos, a veces son simplemente valores extremos que se dio la casualidad de observar en esta recopilación de datos, y otras veces sugieren descubrimientos nuevos e importantes. Cuando tienes muchos datos, a veces es difícil ver los valores atípicos en un histograma. Por ejemplo, toma la distribución de la variable "y" del dataset diamonds. La única evidencia de valores atípicos son los límites inusualmente amplios del eje x.
(ggplot(diamonds, aes(x="y")) + geom_histogram(binwidth=0.5))Hay tantas observaciones en los intervalos comunes que los intervalos raros son muy bajos, lo que hace muy difícil verlos (aunque quizá, si miras fijamente el 0, descubras algo). Para que sea fácil ver los valores inusuales, necesitamos hacer zoom en los valores pequeños del eje y con coord_cartesian():
(
ggplot(diamonds, aes(x="y"))
+ geom_histogram(binwidth=0.5)
+ coord_cartesian(ylim=[0, 50])
)coord_cartesian() también tiene un argumento xlim() para cuando necesites hacer zoom en el eje x. Lets-Plot también tiene las funciones xlim() e ylim(), que funcionan de forma ligeramente distinta: descartan los datos fuera de los límites.
Esto nos permite ver que hay tres valores inusuales: 0, ~30 y ~60. Los extraemos con pandas:
unusual = diamonds.query("y < 3 or y > 20").loc[:, ["x", "y", "z", "price"]]
unusual| x | y | z | price | |
|---|---|---|---|---|
| 11963 | 0.00 | 0.0 | 0.00 | 5139 |
| 15951 | 0.00 | 0.0 | 0.00 | 6381 |
| 24067 | 8.09 | 58.9 | 8.06 | 12210 |
| 24520 | 0.00 | 0.0 | 0.00 | 12800 |
| 26243 | 0.00 | 0.0 | 0.00 | 15686 |
| 27429 | 0.00 | 0.0 | 0.00 | 18034 |
| 49189 | 5.15 | 31.8 | 5.12 | 2075 |
| 49556 | 0.00 | 0.0 | 0.00 | 2130 |
| 49557 | 0.00 | 0.0 | 0.00 | 2130 |
La variable "y" mide una de las tres dimensiones de estos diamantes, en mm. Sabemos que los diamantes no pueden tener una anchura de 0 mm, así que estos valores deben ser incorrectos. Al hacer EDA, hemos descubierto datos faltantes codificados como 0, algo que nunca habríamos encontrado simplemente buscando NAs. De aquí en adelante, podríamos optar por recodificar estos valores como NAs para evitar cálculos engañosos. También podríamos sospechar que las medidas de 32 mm y 59 mm son inverosímiles: ¡esos diamantes miden más de una pulgada de largo, pero no cuestan cientos de miles de dólares!
Es buena práctica repetir tu análisis con y sin los valores atípicos. Si tienen un efecto mínimo en los resultados y no logras averiguar por qué están ahí, es razonable omitirlos y seguir adelante. Sin embargo, si tienen un efecto sustancial en tus resultados, no deberías eliminarlos sin justificación. Tendrás que averiguar qué los causó (p. ej., un error de introducción de datos) e indicar en tu informe que los eliminaste.
Ejercicios
Explora la distribución de cada una de las variables
x,yyzdediamonds. ¿Qué aprendes? Piensa en un diamante y en cómo podrías decidir qué dimensión es la longitud, la anchura y la profundidad.Explora la distribución de
"price". ¿Descubres algo inusual o sorprendente? (Pista: piensa con cuidado en el valor del argumentobinwidth=y asegúrate de probar un rango amplio de valores).¿Cuántos diamantes tienen 0.99 quilates? ¿Cuántos tienen 1 quilate? ¿Cuál crees que es la causa de la diferencia?
Compara y contrasta
coord_cartesian()frente axlim()oylim()al hacer zoom en un histograma. ¿Qué pasa si no defines binwidth? ¿Qué pasa si intentas hacer zoom de forma que solo se vea media barra?
Valores inusuales
Si encontraste valores inusuales en tu dataset y simplemente quieres continuar con el resto de tu análisis, tienes dos opciones.
Eliminar la fila completa con los valores extraños:
condition = ((diamonds["y"] < 3) | (diamonds["y"] > 20)) diamonds2 = diamonds.loc[~condition, :]No recomendamos esta opción, porque un valor no válido no implica que todos los demás valores de esa observación también lo sean. Además, si tienes datos de baja calidad, para cuando hayas aplicado este enfoque a cada variable, ¡podrías descubrir que no te queda ningún dato!
En su lugar, recomendamos reemplazar los valores inusuales por valores faltantes. Una forma de hacerlo, que distingue entre los dataframes a los que se les reemplazaron los valores inusuales y los datos originales, es hacer una copia y luego asignar a los valores problemáticos
pd.NA, el valor NA especial de pandas.
diamonds2 = diamonds.copy()
condition = (diamonds2["y"] < 3) | (diamonds2["y"] > 20)
diamonds2.loc[condition, "y"] = pd.NANo es obvio dónde deberías graficar los valores faltantes, así que lets-plot no los incluye en el gráfico:
(ggplot(diamonds2, aes(x="x", y="y")) + geom_point())Otras veces quieres entender qué diferencia a las observaciones con valores faltantes de las observaciones con valores registrados. Por ejemplo, en los datos de nycflights13, los valores faltantes en la variable "dep_time" indican que el vuelo fue cancelado. Así que podrías querer comparar las horas de salida programadas de los vuelos cancelados y no cancelados. Puedes hacerlo creando una nueva variable y usando is.na() para comprobar si falta "dep_time".
url = "https://raw.githubusercontent.com/byuidatascience/data4python4ds/master/data-raw/flights/flights.csv"
flights = pd.read_csv(url)
flights.head()| year | month | day | dep_time | sched_dep_time | dep_delay | arr_time | sched_arr_time | arr_delay | carrier | flight | tailnum | origin | dest | air_time | distance | hour | minute | time_hour | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2013 | 1 | 1 | 517.0 | 515 | 2.0 | 830.0 | 819 | 11.0 | UA | 1545 | N14228 | EWR | IAH | 227.0 | 1400 | 5 | 15 | 2013-01-01T10:00:00Z |
| 1 | 2013 | 1 | 1 | 533.0 | 529 | 4.0 | 850.0 | 830 | 20.0 | UA | 1714 | N24211 | LGA | IAH | 227.0 | 1416 | 5 | 29 | 2013-01-01T10:00:00Z |
| 2 | 2013 | 1 | 1 | 542.0 | 540 | 2.0 | 923.0 | 850 | 33.0 | AA | 1141 | N619AA | JFK | MIA | 160.0 | 1089 | 5 | 40 | 2013-01-01T10:00:00Z |
| 3 | 2013 | 1 | 1 | 544.0 | 545 | -1.0 | 1004.0 | 1022 | -18.0 | B6 | 725 | N804JB | JFK | BQN | 183.0 | 1576 | 5 | 45 | 2013-01-01T10:00:00Z |
| 4 | 2013 | 1 | 1 | 554.0 | 600 | -6.0 | 812.0 | 837 | -25.0 | DL | 461 | N668DN | LGA | ATL | 116.0 | 762 | 6 | 0 | 2013-01-01T11:00:00Z |
flights2 = flights.assign(
cancelled=lambda x: pd.isna(x["dep_time"]),
sched_hour=lambda x: x["sched_dep_time"] // 100,
sched_min=lambda x: x["sched_dep_time"] % 100,
sched_dep_time=lambda x: x["sched_hour"] + x["sched_min"] / 60,
)
(
ggplot(flights2, aes(x="sched_dep_time"))
+ geom_freqpoly(aes(color="cancelled"), binwidth=1 / 4)
)Sin embargo, este gráfico no es muy bueno porque hay muchos más vuelos no cancelados que cancelados. En la siguiente sección exploraremos algunas técnicas para mejorar esta comparación.
Ejercicios
¿Qué pasa con los valores faltantes en un histograma? ¿Qué pasa con los valores faltantes en un gráfico de barras? ¿Por qué hay una diferencia en cómo se manejan los valores faltantes en los histogramas y en los gráficos de barras?
Recrea el gráfico de frecuencias de
scheduled_dep_timecoloreado según si el vuelo fue cancelado o no. Crea también facetas según la variablecancelled. Experimenta con distintos valores de la variablescalesen la función de facetas para mitigar el efecto de que haya más vuelos no cancelados que cancelados.
Covariación
Si la variación describe el comportamiento dentro de una variable, la covariación describe el comportamiento entre variables. La covariación es la tendencia de los valores de dos o más variables a variar juntos de forma relacionada. La mejor manera de detectar la covariación es visualizar la relación entre dos o más variables, pero ten en cuenta que la covariación no implica una relación causal entre las variables.
Una variable categórica y una numérica
Por ejemplo, exploremos cómo varía el precio de un diamante según su calidad (medida por "cut") usando geom_freqpoly():
(
ggplot(diamonds, aes(x="price"))
+ geom_freqpoly(aes(color="cut"), binwidth=500, linewidth=0.75)
)La apariencia predeterminada de geom_freqpoly() no es muy útil aquí, porque la altura, determinada por el conteo total, difiere mucho entre los cortes, lo que dificulta ver las diferencias en la forma de sus distribuciones.
Para facilitar la comparación, necesitamos cambiar lo que se muestra en el eje y. En lugar de mostrar el conteo, mostraremos la densidad, que es el conteo estandarizado de forma que el área bajo cada polígono de frecuencias sea uno.
(
ggplot(diamonds, aes(x="price"))
+ geom_density(aes(color="cut", fill="cut"), size=1, alpha=0.2)
)Hay algo bastante sorprendente en este gráfico: ¡parece que los diamantes fair (los de menor calidad) tienen el precio promedio más alto! Pero quizá eso se deba a que los gráficos de densidad son algo difíciles de interpretar: en este gráfico pasan muchas cosas.
Un gráfico visualmente más sencillo para explorar esta relación son los boxplots uno al lado del otro.
(ggplot(diamonds, aes(x="cut", y="price")) + geom_boxplot())Vemos mucha menos información sobre la distribución, pero los boxplots son mucho más compactos, así que podemos compararlos con más facilidad (y caben más en un gráfico). Esto respalda el hallazgo contraintuitivo de que los diamantes de mejor calidad suelen ser más baratos. En los ejercicios, tendrás el reto de averiguar por qué.
"cut" es una variable categórica ordenada: fair es peor que good, que es peor que very good, y así sucesivamente. Muchas variables categóricas no tienen un orden intrínseco, así que podrías querer reordenarlas para obtener una visualización más informativa. Una forma de hacerlo es según el valor de la mediana, aunque hay otras opciones.
Como ejemplo, con el dataset mpg, podríamos querer ver cómo varía el rendimiento en carretera entre clases:
mpg = pd.read_csv(
"https://vincentarelbundock.github.io/Rdatasets/csv/ggplot2/mpg.csv", index_col=0
)
mpg["class"] = mpg["class"].astype("category")
(ggplot(mpg, aes(x="class", y="hwy")) + geom_boxplot())Para que la tendencia se vea más fácilmente, podemos reordenar class según el valor de la mediana de "hwy":
(ggplot(mpg) + geom_boxplot(aes(as_discrete("class", order_by="..middle.."), "hwy")))Si tienes nombres de variables largos, geom_boxplot() funcionará mejor si lo giras 90°. Puedes hacerlo añadiendo coord_flip().
(
ggplot(mpg)
+ geom_boxplot(aes(as_discrete("class", order_by="..middle.."), "hwy"))
+ coord_flip()
)Ejercicios
Usa lo que aprendiste para mejorar la visualización de las horas de salida de los vuelos cancelados frente a los no cancelados.
Según el EDA, ¿qué variable del dataset diamonds parece ser la más importante para predecir el precio de un diamante? ¿Cómo se correlaciona esa variable con cut? ¿Por qué la combinación de esas dos relaciones hace que los diamantes de menor calidad sean más caros?
Crea una visualización de los precios de los diamantes frente a una variable categórica del dataset
diamondsusandogeom_violin(), luego ungeom_histogram()con facetas, luego ungeom_freqpoly()coloreado y luego ungeom_density()coloreado. Compara y contrasta los cuatro gráficos. ¿Cuáles son las ventajas y desventajas de cada método para visualizar la distribución de una variable numérica según los niveles de una variable categórica?Si tienes un dataset pequeño, a veces es útil usar
geom_jitter()para evitar la superposición de puntos y ver más fácilmente la relación entre una variable continua y una categórica. El paquete ggbeeswarm ofrece varios métodos similares ageom_jitter(). Enuméralos y describe brevemente qué hace cada uno.
Dos variables categóricas
Para visualizar la covariación entre variables categóricas, necesitarás contar el número de observaciones para cada combinación de niveles de estas variables categóricas. Puedes hacerlo con pd.crosstab(), cuyo resultado luego transformamos con melt para ponerlo en formato “tidy”.
ct_cut_color = pd.melt(
pd.crosstab(diamonds["cut"], diamonds["color"]).reset_index(),
id_vars=["cut"],
value_vars=diamonds["color"].unique(),
)Y después lo visualizamos con geom_tile():
(ggplot(ct_cut_color, aes(x="color", y="cut")) + geom_tile(aes(fill="value")))Ejercicios
¿Cómo podrías reescalar el dataset de conteos anterior para mostrar con más claridad la distribución de cut dentro de color, o de color dentro de cut?
¿Qué información distinta sobre los datos obtienes con un gráfico de barras segmentado si color se asigna a la estética
xy cut a la estética fill? Calcula los conteos que caen en cada uno de los segmentos.Usa
geom_tile()junto con pandas para explorar cómo varían los retrasos promedio de salida de los vuelos según el destino y el mes del año. ¿Qué hace que el gráfico sea difícil de leer? ¿Cómo podrías mejorarlo?
Dos variables numéricas
Ya viste una excelente forma de visualizar la covariación entre dos variables numéricas: dibujar un scatter plot con geom_point(). Puedes ver la covariación como un patrón en los puntos. Por ejemplo, puedes ver una asociación positiva entre los quilates y el precio de un diamante: los diamantes con más quilates tienen un precio más alto. La relación es exponencial.
(ggplot(smaller_diamonds, aes(x="carat", y="price")) + geom_point())(En esta sección usaremos el dataset smaller_diamonds para centrarnos en la mayoría de los diamantes, que tienen menos de 3 quilates)
Los scatter plots se vuelven menos útiles a medida que crece el tamaño de tu dataset, porque los puntos empiezan a superponerse y se acumulan en zonas de negro uniforme, lo que dificulta juzgar las diferencias en la densidad de los datos en el espacio bidimensional y también detectar la tendencia. Ya viste una forma de solucionar el problema: usar la estética alpha para añadir transparencia.
(ggplot(smaller_diamonds, aes(x="carat", y="price")) + geom_point(alpha=1 / 20))Pero usar transparencia puede ser complicado con datasets muy grandes. En ese caso, recomendamos un binscatter, o scatter plot agrupado en intervalos. Un binscatter divide la variable condicionante, "carat" en nuestro ejemplo, en intervalos (bins) o cuantiles del mismo tamaño, y luego grafica la media condicional de la variable dependiente, "price" en nuestro ejemplo, dentro de cada intervalo. Los binscatters suelen incluir también intervalos de confianza. Un buen paquete de binscatter en Python es binsreg. Sin embargo, los binscatters son un tema avanzado y no los cubriremos aquí.
Herramientas integradas de pandas para EDA
pandas tiene algunas opciones excelentes integradas para EDA; de hecho, ya vimos una de ellas, df.info(), que además de informar los tipos de datos y el uso de memoria, también nos dice cuántas observaciones de cada columna son ‘truthy’ en lugar de ‘falsy’, es decir, cuántas tienen valores no nulos.
Tablas exploratorias y estadística descriptiva
Un pequeño paso más allá de .info() para obtener tablas es usar .describe(), que, si tienes tipos de datos mixtos que incluyen floats, mostrará algunas estadísticas descriptivas básicas:
diamonds.describe()| carat | depth | table | price | x | y | z | |
|---|---|---|---|---|---|---|---|
| count | 53940.000000 | 53940.000000 | 53940.000000 | 53940.000000 | 53940.000000 | 53940.000000 | 53940.000000 |
| mean | 0.797940 | 61.749405 | 57.457184 | 3932.799722 | 5.731157 | 5.734526 | 3.538734 |
| std | 0.474011 | 1.432621 | 2.234491 | 3989.439738 | 1.121761 | 1.142135 | 0.705699 |
| min | 0.200000 | 43.000000 | 43.000000 | 326.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.400000 | 61.000000 | 56.000000 | 950.000000 | 4.710000 | 4.720000 | 2.910000 |
| 50% | 0.700000 | 61.800000 | 57.000000 | 2401.000000 | 5.700000 | 5.710000 | 3.530000 |
| 75% | 1.040000 | 62.500000 | 59.000000 | 5324.250000 | 6.540000 | 6.540000 | 4.040000 |
| max | 5.010000 | 79.000000 | 95.000000 | 18823.000000 | 10.740000 | 58.900000 | 31.800000 |
Aunque es útil, ¡sin duda cuesta leerlo! Podemos mejorarlo usando también el método round():
sum_table = diamonds.describe().round(1)
sum_table| carat | depth | table | price | x | y | z | |
|---|---|---|---|---|---|---|---|
| count | 53940.0 | 53940.0 | 53940.0 | 53940.0 | 53940.0 | 53940.0 | 53940.0 |
| mean | 0.8 | 61.7 | 57.5 | 3932.8 | 5.7 | 5.7 | 3.5 |
| std | 0.5 | 1.4 | 2.2 | 3989.4 | 1.1 | 1.1 | 0.7 |
| min | 0.2 | 43.0 | 43.0 | 326.0 | 0.0 | 0.0 | 0.0 |
| 25% | 0.4 | 61.0 | 56.0 | 950.0 | 4.7 | 4.7 | 2.9 |
| 50% | 0.7 | 61.8 | 57.0 | 2401.0 | 5.7 | 5.7 | 3.5 |
| 75% | 1.0 | 62.5 | 59.0 | 5324.2 | 6.5 | 6.5 | 4.0 |
| max | 5.0 | 79.0 | 95.0 | 18823.0 | 10.7 | 58.9 | 31.8 |
Las tablas de estadísticas descriptivas publicadas suelen mostrar una variable por fila y, si tu dataframe tiene muchas variables, describe() puede volverse rápidamente demasiado ancho para leerlo con facilidad. Puedes trasponerlo usando la propiedad T (o el método transpose()):
sum_table = sum_table.T
sum_table| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| carat | 53940.0 | 0.8 | 0.5 | 0.2 | 0.4 | 0.7 | 1.0 | 5.0 |
| depth | 53940.0 | 61.7 | 1.4 | 43.0 | 61.0 | 61.8 | 62.5 | 79.0 |
| table | 53940.0 | 57.5 | 2.2 | 43.0 | 56.0 | 57.0 | 59.0 | 95.0 |
| price | 53940.0 | 3932.8 | 3989.4 | 326.0 | 950.0 | 2401.0 | 5324.2 | 18823.0 |
| x | 53940.0 | 5.7 | 1.1 | 0.0 | 4.7 | 5.7 | 6.5 | 10.7 |
| y | 53940.0 | 5.7 | 1.1 | 0.0 | 4.7 | 5.7 | 6.5 | 58.9 |
| z | 53940.0 | 3.5 | 0.7 | 0.0 | 2.9 | 3.5 | 4.0 | 31.8 |
Por supuesto, las estadísticas de esta tabla predefinida no están muy personalizadas. Entonces, ¿qué hacemos para obtener la tabla que realmente queremos? Pues la respuesta es recurrir al contenido de los capítulos anteriores sobre datos, en particular la introducción al análisis de datos. Groupbys, merges, agregaciones: úsalos todos para producir la tabla de EDA que quieras.
Si estás explorando datos, quizá también quieras poder leerlo todo con claridad y detectar rápidamente cualquier desviación respecto a lo que esperas. pandas tiene funcionalidad integrada para dar estilo a los dataframes que te ayuda con esto. Estos estilos se conservan también cuando exportas el dataframe, por ejemplo, a Excel.
Aquí tienes un ejemplo que muestra algunas formas de dar estilo a los dataframes, aprovechando varias funcionalidades como: desapilar a un formato más ancho (unstack), cambiar las unidades (función lambda; ten en cuenta que 1e3 es la abreviatura de 1000 en los ordenadores), rellenar los NaN con strings discretos (.fillna('-')), eliminar los decimales (.style.format(precision=0)) y añadir un título (.style.set_caption).
(
diamonds.groupby(["cut", "color"])["price"]
.mean()
.unstack()
.apply(lambda x: x / 1e3)
.fillna("-")
.style.format(precision=2)
.set_caption("Sale price (thousands)")
)/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75706/1534868389.py:2: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
diamonds.groupby(["cut", "color"])["price"]
| color | D | E | F | G | H | I | J |
|---|---|---|---|---|---|---|---|
| cut | |||||||
| Fair | 4.29 | 3.68 | 3.83 | 4.24 | 5.14 | 4.69 | 4.98 |
| Good | 3.41 | 3.42 | 3.50 | 4.12 | 4.28 | 5.08 | 4.57 |
| Very Good | 3.47 | 3.21 | 3.78 | 3.87 | 4.54 | 5.26 | 5.10 |
| Premium | 3.63 | 3.54 | 4.32 | 4.50 | 5.22 | 5.95 | 6.29 |
| Ideal | 2.63 | 2.60 | 3.37 | 3.72 | 3.89 | 4.45 | 4.92 |
Aunque es más ordenada que las que hemos visto, sigue siendo una tabla de números bastante sosa. ¡No atrae la mirada de inmediato!
Para remediarlo, veamos otra técnica de estilo: el uso del color. Supongamos que queremos crear una tabla que muestre una tabulación cruzada entre cut y color; es decir, el recuento de objetos que aparecen en ambos campos según sus categorías.
Para hacer una tabulación cruzada usaremos la función integrada pd.crosstab(), pero además pediremos que los valores de la tabla (recuentos) se resalten con un mapa de calor usando style.background_gradient():
pd.crosstab(diamonds["color"], diamonds["cut"]).style.background_gradient(cmap="plasma")| cut | Fair | Good | Very Good | Premium | Ideal |
|---|---|---|---|---|---|
| color | |||||
| D | 163 | 662 | 1513 | 1603 | 2834 |
| E | 224 | 933 | 2400 | 2337 | 3903 |
| F | 312 | 909 | 2164 | 2331 | 3826 |
| G | 314 | 871 | 2299 | 2924 | 4884 |
| H | 303 | 702 | 1824 | 2360 | 3115 |
| I | 175 | 522 | 1204 | 1428 | 2093 |
| J | 119 | 307 | 678 | 808 | 896 |
Por defecto, background_gradient() resalta cada número en relación con los demás de su columna; puedes resaltar por fila usando axis=1 o en relación con todos los valores de la tabla usando axis=0. ¡Y, por supuesto, plasma es solo uno de los muchos mapas de colores disponibles!
Haz una nueva tabulación cruzada usando un mapa de colores diferente.
Aquí tienes un par de consejos más para dar estilo a los dataframes.
Primero, usa barras para mostrar el orden:
(
pd.crosstab(diamonds["color"], diamonds["cut"])
.style.format(precision=0)
.bar(color="#d65f5f")
)| cut | Fair | Good | Very Good | Premium | Ideal |
|---|---|---|---|---|---|
| color | |||||
| D | 163 | 662 | 1513 | 1603 | 2834 |
| E | 224 | 933 | 2400 | 2337 | 3903 |
| F | 312 | 909 | 2164 | 2331 | 3826 |
| G | 314 | 871 | 2299 | 2924 | 4884 |
| H | 303 | 702 | 1824 | 2360 | 3115 |
| I | 175 | 522 | 1204 | 1428 | 2093 |
| J | 119 | 307 | 678 | 808 | 896 |
Usa .hightlight_max() y comandos similares para mostrar las entradas importantes:
pd.crosstab(diamonds["color"], diamonds["cut"]).style.highlight_max().format("{:.0f}")| cut | Fair | Good | Very Good | Premium | Ideal |
|---|---|---|---|---|---|
| color | |||||
| D | 163 | 662 | 1513 | 1603 | 2834 |
| E | 224 | 933 | 2400 | 2337 | 3903 |
| F | 312 | 909 | 2164 | 2331 | 3826 |
| G | 314 | 871 | 2299 | 2924 | 4884 |
| H | 303 | 702 | 1824 | 2360 | 3115 |
| I | 175 | 522 | 1204 | 1428 | 2093 |
| J | 119 | 307 | 678 | 808 | 896 |
Puedes encontrar el conjunto completo de comandos de estilo aquí.
Gráficos exploratorios con pandas
pandas tiene algunas opciones de gráficos integradas para ayudarte a examinar los datos rápidamente. Se accede a ellas mediante .plot.* o .plot(), según el contexto. Hagamos un .plot() rápido usando un dataset sobre taxis.
taxis = pd.read_csv("https://github.com/mwaskom/seaborn-data/raw/master/taxis.csv")
# turn the pickup time column into a datetime
taxis["pickup"] = pd.to_datetime(taxis["pickup"])
# set some other columns types
taxis = taxis.astype(
{
"dropoff": "datetime64[ns]",
"pickup": "datetime64[ns]",
"color": "category",
"payment": "category",
"pickup_zone": "string",
"dropoff_zone": "string",
"pickup_borough": "category",
"dropoff_borough": "category",
}
)
taxis.head()| pickup | dropoff | passengers | distance | fare | tip | tolls | total | color | payment | pickup_zone | dropoff_zone | pickup_borough | dropoff_borough | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2019-03-23 20:21:09 | 2019-03-23 20:27:24 | 1 | 1.60 | 7.0 | 2.15 | 0.0 | 12.95 | yellow | credit card | Lenox Hill West | UN/Turtle Bay South | Manhattan | Manhattan |
| 1 | 2019-03-04 16:11:55 | 2019-03-04 16:19:00 | 1 | 0.79 | 5.0 | 0.00 | 0.0 | 9.30 | yellow | cash | Upper West Side South | Upper West Side South | Manhattan | Manhattan |
| 2 | 2019-03-27 17:53:01 | 2019-03-27 18:00:25 | 1 | 1.37 | 7.5 | 2.36 | 0.0 | 14.16 | yellow | credit card | Alphabet City | West Village | Manhattan | Manhattan |
| 3 | 2019-03-10 01:23:59 | 2019-03-10 01:49:51 | 1 | 7.70 | 27.0 | 6.15 | 0.0 | 36.95 | yellow | credit card | Hudson Sq | Yorkville West | Manhattan | Manhattan |
| 4 | 2019-03-30 13:27:42 | 2019-03-30 13:37:14 | 3 | 2.16 | 9.0 | 1.10 | 0.0 | 13.40 | yellow | credit card | Midtown East | Yorkville West | Manhattan | Manhattan |
taxis.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6433 entries, 0 to 6432
Data columns (total 14 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 pickup 6433 non-null datetime64[ns]
1 dropoff 6433 non-null datetime64[ns]
2 passengers 6433 non-null int64
3 distance 6433 non-null float64
4 fare 6433 non-null float64
5 tip 6433 non-null float64
6 tolls 6433 non-null float64
7 total 6433 non-null float64
8 color 6433 non-null category
9 payment 6389 non-null category
10 pickup_zone 6407 non-null string
11 dropoff_zone 6388 non-null string
12 pickup_borough 6407 non-null category
13 dropoff_borough 6388 non-null category
dtypes: category(4), datetime64[ns](2), float64(5), int64(1), string(2)
memory usage: 528.5 KB
(
taxis.set_index("pickup")
.groupby(pd.Grouper(freq="D"))["total"]
.mean()
.plot(
title="Tarifas medias de taxi",
xlabel="",
ylabel="Tarifa (USD)",
)
);De nuevo, si consigues darle a los datos la forma adecuada, puedes graficarlos. La misma función funciona con varias líneas
(
taxis.set_index("pickup")
.groupby(pd.Grouper(freq="D"))[["fare", "tip", "tolls"]]
.mean()
.plot(
style=["-", ":", "-."],
title="Componentes de las tarifas de taxi",
xlabel="",
ylabel="USD",
)
);Ahora veamos algunas de las otras opciones rápidas de .plot.*.
Un gráfico de barras (usa barh para la orientación horizontal; rot establece la rotación de las etiquetas):
taxis.value_counts("payment").sort_index().plot.bar(title="Conteos", rot=0);El siguiente usa .plot.hist() para crear un histograma.
taxis["tip"].plot.hist(bins=30, title="Propina");Boxplot:
(taxis[["fare", "tolls", "tip"]].plot.box());Scatter plot:
taxis.plot.scatter(x="fare", y="tip", alpha=0.7, ylim=(0, None));Otras herramientas para EDA
Entre pandas y los paquetes de visualización, tienes gran parte de lo que necesitas para el EDA. Pero existen algunas herramientas dedicadas exclusivamente a facilitar el EDA que vale la pena conocer.
skimpy para estadísticas descriptivas
El paquete skimpy es una herramienta ligera que proporciona estadísticas descriptivas sobre las variables de los dataframes en la consola (en lugar de en un gran informe HTML, que es lo que hacen los demás paquetes de EDA del resto de este capítulo). A veces ejecutar .summary() sobre un dataframe no es suficiente, y skimpy cubre ese hueco. También incluye la función clean_columns() para limpiar los nombres de las columnas, que vimos en un capítulo anterior. Para instalar skimpy, ejecuta uv add skimpy en la terminal.
Veamos skimpy en acción.
skim(taxis)╭──────────────────────────────────────────────── skimpy summary ─────────────────────────────────────────────────╮ │ Data Summary Data Types Categories │ │ ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓ ┏━━━━━━━━━━━━━┳━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━┓ │ │ ┃ Dataframe ┃ Values ┃ ┃ Column Type ┃ Count ┃ ┃ Categorical Variables ┃ │ │ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩ ┡━━━━━━━━━━━━━╇━━━━━━━┩ ┡━━━━━━━━━━━━━━━━━━━━━━━┩ │ │ │ Number of rows │ 6433 │ │ float64 │ 5 │ │ color │ │ │ │ Number of columns │ 14 │ │ category │ 4 │ │ payment │ │ │ └───────────────────┴────────┘ │ datetime64 │ 2 │ │ pickup_borough │ │ │ │ string │ 2 │ │ dropoff_borough │ │ │ │ int64 │ 1 │ └───────────────────────┘ │ │ └─────────────┴───────┘ │ │ number │ │ ┏━━━━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━┓ │ │ ┃ column ┃ NA ┃ NA % ┃ mean ┃ sd ┃ p0 ┃ p25 ┃ p50 ┃ p75 ┃ p100 ┃ hist ┃ │ │ ┡━━━━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━┩ │ │ │ passengers │ 0 │ 0 │ 1.539 │ 1.204 │ 0 │ 1 │ 1 │ 2 │ 6 │ █▁ ▁ │ │ │ │ distance │ 0 │ 0 │ 3.025 │ 3.828 │ 0 │ 0.98 │ 1.64 │ 3.21 │ 36.7 │ █▁ │ │ │ │ fare │ 0 │ 0 │ 13.09 │ 11.55 │ 1 │ 6.5 │ 9.5 │ 15 │ 150 │ █▁ │ │ │ │ tip │ 0 │ 0 │ 1.979 │ 2.449 │ 0 │ 0 │ 1.7 │ 2.8 │ 33.2 │ █ │ │ │ │ tolls │ 0 │ 0 │ 0.3253 │ 1.415 │ 0 │ 0 │ 0 │ 0 │ 24.02 │ █ │ │ │ │ total │ 0 │ 0 │ 18.52 │ 13.82 │ 1.3 │ 10.8 │ 14.16 │ 20.3 │ 174.8 │ █▁ │ │ │ └────────────────┴──────┴────────┴───────────┴─────────┴───────┴────────┴─────────┴───────┴────────┴─────────┘ │ │ category │ │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓ │ │ ┃ column ┃ NA ┃ NA % ┃ ordered ┃ unique ┃ │ │ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩ │ │ │ color │ 0 │ 0 │ False │ 2 │ │ │ │ payment │ 44 │ 0.6839732628633608 │ False │ 3 │ │ │ │ pickup_borough │ 26 │ 0.4041660189647132 │ False │ 5 │ │ │ │ dropoff_borough │ 45 │ 0.699518109746619 │ False │ 6 │ │ │ └───────────────────────────────┴────────┴────────────────────────────────────┴────────────────┴─────────────┘ │ │ datetime │ │ ┏━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ │ │ ┃ column ┃ NA ┃ NA % ┃ first ┃ last ┃ frequency ┃ │ │ ┡━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ │ │ pickup │ 0 │ 0 │ 2019-02-28 23:29:03 │ 2019-03-31 23:43:45 │ None │ │ │ │ dropoff │ 0 │ 0 │ 2019-02-28 23:32:35 │ 2019-04-01 00:13:58 │ None │ │ │ └─────────────┴──────┴─────────┴──────────────────────────────┴──────────────────────────────┴───────────────┘ │ │ string │ │ ┏━━━━━━━━━━━┳━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┓ │ │ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ chars ┃ words per ┃ total ┃ │ │ ┃ column ┃ NA ┃ NA % ┃ shortest ┃ longest ┃ min ┃ max ┃ per row ┃ row ┃ words ┃ │ │ ┡━━━━━━━━━━━╇━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━┩ │ │ │ pickup_zo │ 26 │ 0.4041660 │ SoHo │ Riverdale │ Allerton │ Yorkville │ 16.2 │ 2.5 │ 15791 │ │ │ │ ne │ │ 189647132 │ │ /North │ /Pelham │ West │ │ │ │ │ │ │ │ │ │ │ Riverdale │ Gardens │ │ │ │ │ │ │ │ │ │ │ │ /Fieldsto │ │ │ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │ │ │ │ │ dropoff_z │ 45 │ 0.6995181 │ SoHo │ Riverdale │ Allerton │ Yorkville │ 16.3 │ 2.5 │ 15851 │ │ │ │ one │ │ 09746619 │ │ /North │ /Pelham │ West │ │ │ │ │ │ │ │ │ │ │ Riverdale │ Gardens │ │ │ │ │ │ │ │ │ │ │ │ /Fieldsto │ │ │ │ │ │ │ │ │ │ │ │ │ n │ │ │ │ │ │ │ │ └───────────┴────┴───────────┴──────────┴───────────┴──────────┴───────────┴──────────┴───────────┴──────────┘ │ ╰────────────────────────────────────────────────────── End ──────────────────────────────────────────────────────╯
Resumen
En este capítulo has aprendido diversas herramientas que te ayudan a entender la variación dentro de tus datos. Has visto técnicas que funcionan con una sola variable a la vez y con un par de variables. Esto puede parecer muy restrictivo si tienes decenas o cientos de variables en tus datos, pero son la base sobre la que se construyen todas las demás técnicas.
En el próximo capítulo nos centraremos en las herramientas que podemos usar para comunicar nuestros resultados.