Visualización de datos

Ejecutar este documento en forma dinámica: Binder

El propósito de este notebook es mostrar diferentes posibilidades de visualización utilizando las herramientas combinadas de Python, Pandas y Seaborn, dentro de un notebook de Jupyter.

Carga de módulos

Primero es necesario incoporar los módulos que vamos a utilizar:

In [ ]:
import numpy as np
import pandas as pd
import seaborn as sns

Gráficos de puntos

Los gráficos de puntos son una herramienta poderosa para mostrar la relación entre dos variables ya que se puede ver directamente la distribución cruda de los datos. Por otra parte, utilizando diferentes semánticas (color, tamaño, estilo) es posible visualizar información de más de dos dimensiones.

Primero levantamos datos desde un archivo tipo csv (comma-separated values), y los consolidamos dentro de un dataframe de Pandas. El archivo contiene el conjunto de datos Iris dataset que lista mediciones de ancho y largo (en centímetros) de pétalos y sépalos de tres especies:

In [ ]:
iris = pd.read_csv('iris.data', names=['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'])
print(iris.head())

Exploramos información general sobre el dataframe:

In [ ]:
iris.info()
In [ ]:
iris.describe()

Podemos entonces explorar la relación entre la longitud y ancho de los pétalos con un gráfico de puntos:

In [ ]:
ax = sns.relplot(x='petal_length', y='petal_width', data=iris)

Cuando se grafica puntos en dos dimensiones, puede agregarse otra dimensión coloreando los puntos de acuerdo con una tercera variable. Esto se denomina hue semantic (hue = tono, matiz o tonalidad) debido a que el color de los puntos adquiere significado. Podemos agregar al gráfico anterior color según la especie:

In [ ]:
ax = sns.relplot(x='petal_length', y='petal_width', hue='species', data=iris)

Para enfatizar la diferencia entre clases, y mejorar la accesibilidad, se puede utilizar un símbolo diferente (marker) para cada clase:

In [ ]:
ax = sns.relplot(x='petal_length', y='petal_width', hue='species', style='species', data=iris)

Es posible visualizar relaciones multidimensionales entre muestras agrupando las variables de a pares:

In [ ]:
ax = sns.pairplot(iris, hue='species', height=2.5)

Podemos explorar diferentes relaciones en datos más complejos, como el que levantamos a continuación:

In [ ]:
tips = pd.read_csv('tips.csv')
print(tips.head())
In [ ]:
tips.info()

Es posible representar cuatro variables cambiando el hue y el estilo de cada punto independientemente, pero esto debe hacerse con cuidado pues el ojo es menos sensible a la forma que al color:

In [ ]:
ax = sns.relplot(x='total_bill', y='tip', hue='smoker', style='time', data=tips)

En los ejemplos anteriores, la semántica de tonos fue categórica, por lo que se aplicó una paleta cualitativa. Si la semántica de tonos es numérica (específicamente, puede transformarse en un flotante), el coloreado por defecto cambia a una paleta secuencial.

In [ ]:
sns.relplot(x="total_bill", y="tip", hue="size", data=tips);

El tercer tipo de variable semántica cambia el tamaño de cada punto:

In [ ]:
sns.relplot(x="total_bill", y="tip", size="size", data=tips);

Gráfico de líneas

Si bien los gráficos de puntos son muy efectivos, no hay un tipo de visualización que sea universalmente óptimo. En muchos casos se quiere enfatizar la continuidad entre valores para comprender los cambios en una variable como función de otra (por ejemplo el tiempo). Esto se obtiene en forma sencilla en Seaborn utilizando la función lineplot() directamente o con relplot() estableciendo la opción kind="line":

In [ ]:
sns.set(style="darkgrid")
df = pd.DataFrame(dict(time=np.arange(500),
                       value=np.random.randn(500).cumsum()))
g = sns.relplot(x="time", y="value", kind="line", data=df)
g.fig.autofmt_xdate()

Dado que lineplot() asume que estamos tratando de graficar y en función de x, el comportamiento por defecto es el de ordenar los datos por la variable x antes de graficar. Sin embargo en algunos gráficos este puede no ser el comportamiento deseado, y por lo tanto puede desactivarse:

In [ ]:
df = pd.DataFrame(np.random.randn(100, 2).cumsum(axis=0), columns=["x", "y"])
sns.relplot(x="x", y="y", sort=False, kind="line", data=df);

Agregación y representación de incertezas

Los conjuntos de datos más complejos pueden tener múltiples mediciones para el mismo valor de la variable $x$. El comportamiento por defecto de Seaborn es agregar múltiples mediciones para cada $x$ graficando la media y el intervalo de confianza del 95% alrededor de la media.

En el siguiente ejemplo usamos los datos del archivo fmri.csv que contiene mediciones de ensayos clínicos de respuestas a estímulos en dos regiones cerebrales.

In [ ]:
fmri = pd.read_csv('fmri.csv')
print(fmri.head())
fmri.info()
sns.relplot(x="timepoint", y="signal", kind="line", data=fmri);

Otra buena opción, especialmente con grandes volúmenes de datos, es representar la dispersión de la distribución en cada punto temporal ploteando la desviación estándar en vez del intervalo de confianza:

In [ ]:
sns.relplot(x="timepoint", y="signal", kind="line", ci="sd", data=fmri);

Es posible desactivar completamente la agregación de datos, estableciendo el parámetro estimator a None. Esto suele producir efectos extraños cuando tenemos múltiples observaciones para cada punto:

In [ ]:
sns.relplot(x="timepoint", y="signal", estimator=None, kind="line", data=fmri);

Graficando subconjuntos de datos utilizando mapeos semánticos

Los gráficos de línea tienen la misma flexibilidad que los de puntos: pueden mostrar hasta tres variables adicionales modificando hue, size y style. Además, utilizando semántica en los gráficos de línea podemos determinar cómo se agregan los datos. Por ejemplo, agregando la semántica de tonos con dos niveles dividimos el gráfico en dos líneas y barras de error, coloreando cada subconjunto a los datos que les corresponden:

In [ ]:
sns.relplot(x="timepoint", y="signal", hue="event", kind="line", data=fmri);

Agregando la semántica de estilo cambia el patrón de líneas:

In [ ]:
sns.relplot(x="timepoint", y="signal", hue="region", style="event",
            kind="line", data=fmri);

Podemos también identificar subconjuntos utilizando símbolos en vez del estilo de línea:

In [ ]:
sns.relplot(x="timepoint", y="signal", hue="region", style="event",
            dashes=False, markers=True, kind="line", data=fmri);

Igual que con los gráficos de puntos, hay que ser cuidadosos con usar múltiples semánticas. Mientras que muchas veces es informativo, también pueden generar gráficos difíciles de interpretar. De todos modos, aún cuando estamos examinando solo una variable adicional, puede ser útil alterar el color y el estilo de las líneas ya que puede mejorar la accesibilidad de personas daltónicas o cuando se imprime en blanco y negro.

In [ ]:
sns.relplot(x="timepoint", y="signal", hue="event", style="event",
            kind="line", data=fmri);

El mapa de colores y las leyendas por defecto en lineplot() dependen del carácter numérico o categórico de la semántica:

In [ ]:
dots = pd.read_csv("dots.csv").query("align == 'dots'")
print(dots.head())
dots.info()
In [ ]:
sns.relplot(x="time", y="firing_rate",
            hue="coherence", style="choice",
            kind="line", data=dots);

Puede pasar que aunque la variable hue es numérica, quede representada en forma muy pobre usando una escala lineal cuando el valor que representa tiene escala logarítmica. Esto puede modificarse especificando el color para cada línea mediante un diccionario.

Por otra parte, es posible mapear una variable categórica con el ancho de las líneas. Hay que ser cuidadoso con esto porque a veces no es simple determinar líneas anchas de finas. Sin embargo también es complicado percibir líneas punteadas cuando las líneas tienen variaciones de alta frecuencia, por lo que usar anchos diferentes puede ser más efectivo en este caso:

In [ ]:
from matplotlib.colors import LogNorm
palette = sns.cubehelix_palette(light=.8, n_colors=6)
sns.relplot(x="time", y="firing_rate",
            hue="coherence", size="choice",
            hue_norm=LogNorm(),
            kind="line", data=dots);

Gráfico de múltiples relaciones con facetas (o subplots)

Si bien las variables semánticas pueden mostrar múltiples relaciones en un gráfico, no siempre es la forma más efectiva de hacerlo. Muchas veces es mejor hacer más de un gráfico. Esto significa que se pueden hacer múltiples ejes y plotear subconjuntos de datos en cada uno de ellos:

In [ ]:
sns.relplot(x="total_bill", y="tip", hue="smoker",
            col="time", data=tips);

También puede mostrarse el efecto de dos variables faceteando una variable en columnas y la otra variable en filas:

In [ ]:
sns.relplot(x="timepoint", y="signal", hue="subject",
            col="region", row="event", height=5,
            kind="line", estimator=None, data=fmri);

Si queremos examinar efectos a través de varios niveles de una variable, puede ser buena idea facetear la variable en columnas y luego "envolver" las facetas en filas:

In [ ]:
sns.relplot(x="timepoint", y="signal", hue="event", style="event",
            col="subject", col_wrap=5,
            height=3, aspect=.75, linewidth=2.5,
            kind="line", data=fmri.query("region == 'frontal'"));

Estas visualizaciones, llamadas frecuentemente "malla", son muy efectivas ya que permiten presentar datos en un formato que hace fácil para la vista detectar patrones y desviaciones de esos patrones. Hay que tener en cuenta que suele ser más efectivo usar varios gráficos simples que uno complejo.

Gráficos categóricos especializados

Los gráficos de puntos o líneas visualizan relaciones entre variables numéricas, pero muchos análisis de datos involucran variables categóricas. En Seaborn existen diferentes tipos de gráficos especializados que están optimizados para este tipo de datos, a los que pueden accederse a través de catplot().

Del mismo modo que relplot(), la idea de catplot() es ofrecer diferentes representaciones de relaciones entre una variable numérica y una (o varias) categóricas. Estas representaciones ofrecen diferentes niveles de agregación en la presentación de los datos subyacentes. Al nivel más fino, puede graficarse cada observación a través de un gráfico de puntos que ajusta la posición de los mismos sobre un eje categórico de modo que no se superpongan:

In [ ]:
sns.catplot(x="day", y="total_bill",
            kind="swarm", data=tips);

Del mismo modo que en los gráficos relacionales, es posible agregar otra dimensión a un gráfico categórico utilizando una semántica de tonos (los gráficos categóricos no tienen actualmente semántica de tamaño o estilo). Por ejemplo:

In [ ]:
sns.catplot(x="day", y="total_bill", hue="sex", kind="swarm", data=tips);

A diferencia de los datos numéricos, no es obvio cómo ordenar una variable categórica sobre su eje. En general, Seaborn intenta inferir el orden de las categorías a partir de los datos, pero puede indicarse como se muestra en el próximo ejemplo.

In [ ]:
sns.catplot(x="day", y="total_bill", hue="sex", kind="swarm", 
            order=['Thur','Fri','Sat','Sun'], data=tips);

Distribuciones de observaciones dentro de las categorías

A medida que el tamaño del dataset crece, los gráficos de puntos categóricos ofrecen una información limitada sobre la distribución de valores en cada categoría. Cuando esto ocurre existen varios abordajes para sintetizar esta información de modo de poder facilitar las comparaciones entre categorías.

Gráfico de barras

Los gráficos de barras se utilizan cuando queremos visualizar el número de observaciones dentro de cada categoría:

In [ ]:
titanic = pd.read_csv('titanic.csv')
print(titanic.head())
print(titanic.info())
In [ ]:
sns.catplot(x="deck", kind="count", order=['A','B','C','D','E','F','G'], data=titanic);

Es posible mostrar una estimación de la tendencia central de los valores. El tipo bar opera sobre todos los datos para obtener la estimación (por defecto la media) y un intervalo de confianza:

In [ ]:
sns.catplot(x="sex", y="survived", hue="class", kind="bar",data=titanic);

Histogramas

Un histograma representa la distribución de datos mediante la formación de bins sobre un rango de los datos, y dibujando barras para mostrar el número de observaciones que cae en cada bin. Por defecto, la función distplot() dibuja un histograma y ajusta un KDE (kernel density estimation):

In [ ]:
x = np.random.normal(size=100)
sns.distplot(x);

Es posible graficar un histograma sin la curva de densidad, y agregarle un rug plot que consiste en poner una marca vertical en cada observación:

In [ ]:
sns.distplot(x, kde=False, rug=True);

También es posible graficar solo la KDE, que resulta útil para mostrar la forma de la distribución. Igual que el histograma, el KDE sintentiza la densidad de observaciones en un eje con la altura en el otro:

In [ ]:
sns.distplot(x, hist=False, rug=True);

Distribuciones bivariadas

En muchos casos puede ser útil visualizar la distribución bivariada de dos variables, utilizando la función jointplot(), que crea una figura con tres paneles mostrando la relación bivariada junto con las distribuciones univariadas de cada variable en ejes separados:

In [ ]:
mean, cov = [0, 1], [(1, .5), (.5, 1)]
data = np.random.multivariate_normal(mean, cov, 2000)
df = pd.DataFrame(data, columns=["x", "y"])
sns.jointplot(x="x", y="y", data=df);

Gráficos hexbin

El análogo bivariado de un histograma es el gráfico hexbin, debido a que muestra la cuenta de observaciones que caen dentro de bins hexagonales. Suelen verse mejor sobre un fondo blanco:

In [ ]:
x, y = np.random.multivariate_normal(mean, cov, 1000).T
with sns.axes_style("white"):
    sns.jointplot(x=x, y=y, kind="hex", color="k");

También es posible usar el procedimiento KDE para visualizar una distribución vibariada:

In [ ]:
sns.jointplot(x="x", y="y", data=df, kind="kde");

Boxplots

Este tipo de gráfico muestra los tres valores de los cuartilos junto con los valores extremos. Los "bigotes" se extienden hasta puntos dentro de 1.5 IQR del cuartilo inferior y superior, y todas las observaciones que caen fuera de ese rango se muestran independientemente:

In [ ]:
sns.catplot(x="day", y="total_bill", kind="box",
            order=['Thur','Fri','Sat','Sun'], data=tips);

Si incluimos la opción notch=True se muestra la muesca con el intervalo de confianza de la media:

In [ ]:
sns.catplot(x="day", y="total_bill", kind="box", notch=True,
            order=['Thur','Fri','Sat','Sun'], data=tips);

Cuando le agregamos una semántica de tonos, la caja para cada nivel de la variable semántica se mueve a lo largo del eje categórico para evitar la superposición:

In [ ]:
sns.catplot(x="day", y="total_bill", kind="box", hue='sex',
            order=['Thur','Fri','Sat','Sun'], data=tips);

Este comportamiento se llama dodging y se activa de manera predeterminada porque se supone que la variable semántica está anidada dentro de la variable categórica principal. Si ese no es el caso, puede deshabilitar:

In [ ]:
tips["weekend"] = tips["day"].isin(["Sat", "Sun"])
sns.catplot(x="day", y="total_bill", hue="weekend",
            order=['Thur','Fri','Sat','Sun'],
            kind="box", dodge=False, data=tips);

Violinplots

Un abordaje diferente es un violinplot(), que combina un boxplot con el procedimiento de KDE (kernel density estimation):

In [ ]:
sns.catplot(x="day", y="total_bill", hue="sex", order=['Thur','Fri','Sat','Sun'],
            kind="violin", split=True, data=tips);

Es posible combinar un swarmplot() con un boxplot() o violinplot() para mostrar cada observación junto con la descripción de la distribución:

In [ ]:
g = sns.catplot(x="day", y="total_bill", kind="violin", inner=None, data=tips)
sns.swarmplot(x="day", y="total_bill", order=['Thur','Fri','Sat','Sun'], 
              color="k", size=3, data=tips, ax=g.ax);