¿Por qué visualizar? El Datasaurus Dozen

El cálculo de estadísticas no nos permite contar la historia completa de los datos con los que trabajamos.

¿Por qué visualizar?

La visualización de datos es más que la representación gráfica de información. Es una forma de comunicación visual orientada a generar conocimiento acerca de los datos.


Las herramientas de visualización ayudan a poner en evidencia información relevante sobre un dataset, facilitando la detección de tendencias, patrones, valores atípicos y correlaciones entre variables.

Visualización de datos en Python

MATPLOTLIB. Es una extensa biblioteca de funciones para generar figuras 2D y 3D adecuadas para su publicación. Destaca su módulo pyplot.

Visualización de datos en Python

SEABORN. Es una librería de gráficos de alto nivel construida sobre matplotlib, que simplifica la creación de diversas visualizaciones de uso común.

Visualización de datos en Python

PLOTNINE. Es una implementación de Grammar of Graphics (libro escrito por Leland Wilkinson en 1999) en Python, basada en ggplot2 de R. La gramática de los gráficos de Wilkinson provee un sistema para combinar elementos gráficos que den como resultado figuras para mostrar datos de manera visualmente significativa.

Clasificación según Claus Wilke

Claus Wilke propone una clasificación de las herramientas de visualización según el objetivo que se persigue. Por ejemplo:

  • Gráficos para visualizar distribuciones

  • Gráficos para visualizar cantidades y proporciones

  • Gráficos para visualizar relaciones entre dos variables cuantitativas

  • Gráficos para datos geoespaciales

Visualizar distribuciones

Alternativas para visualizar una única distribución:

Visualizar distribuciones

Alternativas para visualizar varias distribuciones al mismo tiempo:

Visualizar proporciones

Relaciones entre variables cuantitativas

Relaciones entre variables cuantitativas

Visualizar datos geoespaciales

GRÁFICOS PARA VISUALIZAR DISTRIBUCIONES
(vars. cuantitativas)

GRÁFICOS PARA VISUALIZAR DISTRIBUCIONES

Recuperemos el dataset data_ejercicio.csv con el que trabajamos en la Unidad anterior.

Acá va un vistazo de la información que contiene:

df = pd.read_csv('datasets/data_ejercicio.csv')
df.head(4)
id duracion_min distancia_km actividad calorias_quemadas peso_kg edad_anios
0 1 10.18 4.2 correr 241.3 70.1 32
1 2 5.36 3.4 caminar 201.5 54.4 28
2 3 22.08 2.4 trotar 258.7 76.0 27
3 4 14.02 6.9 trotar 242.9 66.8 27

GRÁFICOS PARA VISUALIZAR DISTRIBUCIONES

Supongamos, en primer lugar, que deseamos construir un gráfico para visualizar la distribución de las observaciones de la variable edad_anios.

¿De qué hablamos cuando hablamos de DISTRIBUCIÓN?

Cuando hablamos de la distribución de las observaciones de una variable, nos referimos a cómo están “organizados” o “repartidos” los valores de esa variable, es decir, cuántas veces aparece cada valor o cada rango de valores.

GRÁFICO DE BASTONES

Cuando el número de observaciones es grande pero hay pocos valores diferentes, como ocurre generalmente cuando la variable es discreta, el gráfico o diagrama de bastones es la representación gráfica adecuada en este caso.

En el eje de abscisas se representan los valores observados de la variable y en el de ordenadas, las correspondientes frecuencias (absolutas o relativas). Luego, para cada valor observado se levanta un segmento o bastón de altura igual a su frecuencia.

GRÁFICO DE BASTONES

Es decir que este tipo de representación toma como punto de partida la tabla de frecuencias correspondiente para las observaciones de la variable, que constituye una forma de resumir la distribución de dichos valores en el dataset.

Acá, las primeras filas de dicha tabla de frecuencias:

df['edad_anios'].value_counts().reset_index().rename(columns = {'edad_anios' : 'Edad', 'count' : 'Frec. abs.'}).sort_values('Edad').head(4)
Edad Frec. abs.
19 14 1
18 15 2
15 17 4
17 18 4

GRÁFICO DE BASTONES

Podemos construir un gráfico de este tipo para la variable edad utilizando la función countplot() de la librería seaborn, seteando el parámetro width en un valor pequeño de manera que lo que se muestren sean bastones y no barras.

La variable que se muestra en el eje y puede modificarse a través del parámetro stat, pudiendo ser: count (default), percent o proportion.

GRÁFICO DE BASTONES

edad_min = df['edad_anios'].min() # Edad mínima
edad_max = df['edad_anios'].max() # Edad máxima

sns.countplot(x = 'edad_anios', width = 0.25, order = np.arange(edad_min, edad_max+1, 1), data = df);

GRÁFICO DE BASTONES

⚠️IMPORTANTE: en este caso, es fundamental agregar la especificación correspondiente en el parámetro order, ya que de lo contrario obtenemos un gráfico que omite, en la escala del eje x, la edad de 16 años. Esto vuelve la escala incorrecta.

El problema es que, como no hay personas de 16 años en los datos, la frecuencia para esa edad es cero. Si no se indica manualmente el orden, Seaborn no deja un espacio vacío en el gráfico para representar ese valor ausente. Esto puede inducir a error al interpretar la distribución.

HISTOGRAMA DE FRECUENCIAS

Un gráfico muy utilizado para representar datos de una variable continua (especialmente cuando el número de observaciones es grande) es el histograma de frecuencias.

En el eje de las abscisas se representan intervalos en que se agrupan los valores de la variable (recordar clases pasadas) y en el eje de las ordenadas, la frecuencia absoluta o relativa. Luego, sobre cada uno de los subintervalos se grafica un rectángulo cuya área representa la frecuencia (absoluta o relativa) del mismo.

HISTOGRAMA DE FRECUENCIAS

Podemos construir este tipo de visualización utilizando la función histplot() de la librería seaborn. Veamos cómo luce el histograma de las observaciones de calorias_quemadas:

sns.histplot(x = 'calorias_quemadas', data = df);

HISTOGRAMA DE FRECUENCIAS

Por default, el número de subintervalos en los que se segmenta la variable (bins) es calculado por la función de seaborn utilizando algún método predefinido que toma en cuenta la cantidad de observaciones que se quieren representar.

¿Qué efecto produce la modificación del número de bins sobre el gráfico resultante?

HISTOGRAMA DE FRECUENCIAS

HISTOGRAMA DE FRECUENCIAS

Otro ejemplo con las edades de los pasajeros del Titanic:

HISTOGRAMA DE FRECUENCIAS

El parámetro bins de sns.histplot() puede estar dado por:

  • el nombre de algún método o regla de referencia: por ejemplo, bins = 'sqrt', para que utilice el criterio de la raíz cuadrada de n para el número de bins.

  • el número de bins: por ejemplo, bins = 10.

  • una lista con los breaks que definen los subintervalos: por ejemplo, bins = [0, 5, 10, 15, 20].

Otros parámetros que puede ser útil setear son binwidth y binrange.

HISTOGRAMA DE FRECUENCIAS

Para ver otro tipo de distribución, representemos gráficamente la distribución de las observaciones de la variable peso_kg:

sns.histplot(x = 'peso_kg', data = df);

HISTOGRAMA DE FRECUENCIAS

🤔 ¿Qué forma tiene la distribución de los pesos de las personas del dataset?

“EMPROLIJAR EL GRÁFICO”

Siempre que presentemos un gráfico, es importante que los ejes estén correctamente etiquetados:

sns.histplot(x = 'peso_kg', data = df)
plt.xlabel('Peso (kg)', fontweight = 'bold')
plt.ylabel('N° de personas', fontweight = 'bold');

“EMPROLIJAR EL GRÁFICO”

Podemos modificar la escala del eje x utilizando xticks y definiendo la secuencia con la ayuda de np.arange():

import numpy as np

sns.histplot(x = 'peso_kg', data = df)
plt.xticks(np.arange(30, 81, 5));

GRÁFICO DE DENSIDAD

Un gráfico de densidad intenta visualizar la distribución de probabilidad subyacente de los datos mediante el dibujo de una curva continua apropiada, la cual debe ser estimada a partir de los datos.

El método de estimación más comúnmente utilizado se conoce como estimación de densidad por kernel.

ESTIMACIÓN DE DENSIDAD POR KERNEL

En la estimación de densidad por kernel, dibujamos una curva continua llamada kernel con un ancho pequeño (controlado por un parámetro llamado ancho de banda) en la ubicación de cada punto de datos, y luego sumamos todas estas curvas para obtener la estimación final de densidad.

El kernel más utilizado es un kernel gaussiano, pero hay muchas otras opciones.

GRÁFICO DE DENSIDAD

Para hacer un gráfico de densidad a partir de las observaciones de la variable calorias_quemadas podemos utilizar la función kdeplot() de seaborn:

sns.kdeplot(x = 'calorias_quemadas', fill = True, data = df);

GRÁFICO DE DENSIDAD

La apariencia visual exacta de un gráfico de densidad dependerá tanto de la elección del ancho de banda como del tipo de kernel. La primera característica puede modificarse dentro del parámetro bw_method de sns.kdeplot(), el cual puede ser:

  • el nombre de un método utilizado para estimar el bandwidth según ciertos criterios: por ejemplo, bw_method = 'scott' (default).

  • un valor de ancho de banda específico: por ejemplo: bw_method = 0.5.

¿Qué efecto produce la modificación del ancho de banda (bandwidth) sobre el gráfico resultante?

GRÁFICO DE DENSIDAD

GRÁFICO DE DENSIDAD

La trampa de los gráficos de densidad.

😱 Las estimaciones de densidad por kernel tienen una trampa de la que debemos ser conscientes: tienden a producir la apariencia de que hay datos donde no existen, en particular en las colas.


🤓 Para evitar que el gráfico se extienda hacia valores que resultan inconsistentes para el tipo de variable con el que trabajamos (por ejemplo, valores negativos de calorías quemadas), podemos setear el parámetro clip de manera de “cortar” el gráfico de densidad en puntos específicos del eje.

GRÁFICO DE DENSIDAD

Sobre la escala del eje “Densidad”.

Como estimaciones de las distribuciones de probabilidad de variables continuas, las curvas de densidad suelen escalarse de manera que el área bajo la curva sea igual a uno. Esta convención puede hacer que la escala del eje y sea confusa, ya que depende de las unidades del eje x.

HISTOGRAMA + GRÁFICO DE DENSIDAD

Seteando el parámetro kde = True de sns.histplot() podemos superponer la curva de densidad por Kernel al histograma:

sns.histplot(x = 'peso_kg', kde = True, data = df);

GRÁFICOS PARA COMPARAR DISTRIBUCIONES
(vars. cuantitativas)

GRÁFICOS DE DENSIDAD MÚLTIPLES

Si quisiéramos visualizar la distribución de calorias_quemadas ya no en forma general sino segmentada según actividad podemos setear los siguientes parámetros dentro de sns.kdeplot():

  • hue = 'actividad', para representar con un color distinto la distribución de la variable para cada actividad física (trotar/caminar/correr).

  • multiple = 'layer', para superponer las distintas curvas.

GRÁFICOS DE DENSIDAD MÚLTIPLES

sns.kdeplot(x = 'calorias_quemadas', hue = 'actividad', multiple = 'layer', fill = True, data = df);

GRÁFICOS DE DENSIDAD MÚLTIPLES

Superponer el gráfico de densidad “general”, ayuda a visualizar qué proporción representa, sobre el total, cada una de las actividades:

sns.kdeplot(x = 'calorias_quemadas', hue = 'actividad', multiple = 'layer', fill = True, data = df)
sns.kdeplot(x = 'calorias_quemadas', fill = True, data = df);

GRÁFICOS DE DENSIDAD MÚLTIPLES

Porcentaje que representa cada actividad sobre el total:

round(df['actividad'].value_counts()*100/df['actividad'].value_counts().sum(),2)
actividad
caminar    42.04
trotar     36.93
correr     21.02
Name: count, dtype: float64

BOXPLOT MÚLTIPLE

Un boxplot múltiple es otro gráfico que permite visualizar si existen diferencias en las calorías quemadas según la actividad:

sns.boxplot(x = 'calorias_quemadas', y = 'actividad', hue = 'actividad', data = df);

BOXPLOT MÚLTIPLE “ORDENADO”

Podemos ordenar las categorías del eje y según la mediana de calorias_quemadas. Primero necesitamos ver cuál es ese orden:

tabla = (df.groupby('actividad'))['calorias_quemadas'].median().sort_values()
print(tabla)
actividad
caminar    248.05
trotar     263.80
correr     277.35
Name: calorias_quemadas, dtype: float64
orden_segun_mediana = tabla.index
print(orden_segun_mediana)
Index(['caminar', 'trotar', 'correr'], dtype='object', name='actividad')

BOXPLOT MÚLTIPLE “ORDENADO”

Luego lo incluimos como parte del parámetro order:

sns.boxplot(x = 'calorias_quemadas', y = 'actividad', hue = 'actividad', order = orden_segun_mediana, data = df);

🤓 ¡Manos a la obra!

En visualizaciones con boxplots múltiples, es común superponer también la media aritmética de cada grupo para complementar la información que aporta la mediana.

📌 Sobre el boxplot múltiple de calorías quemadas según el tipo de actividad física, agregar al gráfico la media de cada grupo, representándola con un punto negro relleno.

STRIP PLOTS o JITTER PLOTS

Los jitter plots son gráficos en los que se representan directamente todos los datos individuales en la forma de puntos.

Cuando hay muchos datos, para no graficar demasiados puntos uno encima del otro, los mismos se dispersan un poco añadiendo algo de ruido aleatorio en la dimensión en la que se plotean (técnica llamada “jittering”).

Constituyen una buena herramienta para visualizar múltiples distribuciones.

STRIP PLOTS o JITTER PLOTS

Podemos construir uno con los datos de calorias_quemadas según actividad utilizando la función stripplot() de seaborn. El grado de “jittering” se setea a través del parámetro jitter.

sns.stripplot(x = 'calorias_quemadas', y = 'actividad', hue = 'actividad', order = orden_segun_mediana, jitter = 0.25, data = df);

VIOLIN PLOT

Los gráficos de violín (violin plots) son gráficos de densidad por Kernel espejados.

Se pueden usar en los mismos casos en los que podríamos optar por un boxplot, y a diferencia de este tipo de gráfico ofrecen una representación mucho más matizada de los datos que posibilita la detección de distribuciones bi/multimodales.

VIOLIN PLOT

Podemos construir uno con los datos de calorias_quemadas según actividad utilizando la función violinplot() de seaborn.

sns.violinplot(x = 'calorias_quemadas', y = 'actividad', hue = 'actividad', order = orden_segun_mediana, gap = 1.7, data = df);

VIOLIN PLOT

Por default, la visualización incorpora un boxplot interno que puede ocultarse seteando el parámetro inner = None o “tunearse” introduciendo modificaciones en la forma de un diccionario dentro de inner_kws.

Por otra parte, otro parámetro que permite modificar la apariencia de la visualización es gap, para introducir cambios en el grosor del gráfico de violín.

STRIP PLOT + VIOLIN PLOT

Los dos últimos gráficos pueden combinarse para crear una visualización muy interesante:

sns.violinplot(x = 'calorias_quemadas', y = 'actividad', hue = 'actividad', data = df, order = orden_segun_mediana, inner = None)
sns.stripplot(x = 'calorias_quemadas', y = 'actividad', data = df, color = 'black', size = 3, dodge = True, linewidth = 1, edgecolor = 'gray', jitter = True, alpha = 0.7);

GRÁFICOS PARA VISUALIZAR PROPORCIONES
(vars. cualitativas)

REPRESENTAR VARIABLES CUALITATIVAS

Uno de los gráficos más utilizados para representar datos de variables cualitativas son los gráficos de barras.

En este tipo de gráficos, se representa una barra (por lo general, horizontal) para cada categoría. La longitud de cada una de ellas es proporcional al porcentaje de unidades que pertenecen a la categoría y el ancho es el mismo para todas.

DATASET TITANIC

Para ver algunos ejemplos de gráficos de barras, vamos a trabajar con el dataset titanic. El mismo viene incorporado en seaborn y contiene información relacionada con los pasajeros del barco.

Podemos cargar el dataset en el entorno de trabajo utilizando el siguiente comando:

titanic = sns.load_dataset('titanic')

🤓 ¡Manos a la obra!

1. Importar el dataset al entorno de trabajo utilizando la función load_dataset() de Seaborn.

2. Generar una tabla de frecuencias con la distribución de pasajeros según la clase. La misma debe contener tanto las frecuencias absolutas de las diferentes categorías como los porcentajes correspondientes.

3. Generar una tabla con la distribución conjunta de pasajeros según clase y supervivencia. La tabla debe contener sólo los porcentajes correspondientes.

PASAJEROS DE CADA CLASE

Podemos representar la distribución de pasajeros que viajaron en cada clase (class) utilizando un gráfico de barras:

sns.countplot(y = 'class', stat = 'percent', order = ['First', 'Second', 'Third'], data = titanic);

BARRAS AGRUPADAS o PARALELAS

A menudo estamos interesados en visualizar dos variables categóricas al mismo tiempo. Esto puede hacerse a través de un gráfico de barras paralelas o agrupadas.

En este tipo de gráficos, representamos un grupo de barras en cada posición a lo largo del eje y, de acuerdo a los distintos niveles de una primera variable categórica, y luego relacionamos cada una de las barras del grupo a un nivel diferente de una segunda variable categórica.

BARRAS AGRUPADAS o PARALELAS

Para construir un gráfico de barras paralelas que muestre los porcentajes de pasajeros según la clase en la que viajaron y si sobrevivieron o no (alive), podemos setear, en el gráfico anterior, el parámetro hue = 'alive'.

sns.countplot(y = 'class', hue = 'alive', stat = 'percent', order = ['First', 'Second', 'Third'], data = titanic);

BARRAS AGRUPADAS o PARALELAS

🤔 ¿Qué tipo de información nos brinda este gráfico?

BARRAS AGRUPADAS o PARALELAS

Los porcentajes de cada combinación de categorías están calculados sobre el total. Podríamos acceder a esa información realizando una operación previa de groupby() por las columnas class y alive, y luego calcular los porcentajes correspondientes:

round(titanic.groupby(['class', 'alive']).size()*100/titanic.groupby(['class', 'alive']).size().sum(),2)
class   alive
First   no        8.98
        yes      15.26
Second  no       10.89
        yes       9.76
Third   no       41.75
        yes      13.36
dtype: float64

GRÁFICO DE BARRAS APILADAS

Otra forma de mostrar la información anterior podría ser a través de un gráfico de barras apiladas o stackeadas.

Para construir este tipo de visualización, necesitamos generar previamente de una tabla de doble entrada, lo que podemos hacer con la combinación de los métodos size() y unstack() de la librería Pandas, previo agrupamiento las columnas class y alive:

tabla_freq = titanic.groupby(['class', 'alive']).size().unstack()
print(tabla_freq)
alive    no  yes
class           
First    80  136
Second   97   87
Third   372  119

GRÁFICO DE BARRAS APILADAS

Luego podemos usar el método plot.barh() de Pandas para construir el gráfico deseado, con el argumento stacked = True para mostrar barras apiladas:

tabla_freq.plot.barh(stacked = True)

GRÁFICO DE BARRAS APILADAS

Si quisiéramos ahora que los porcentajes estén calculados sobre el total de personas que viajaron en cada clase (y no sobre el total general de pasajeros), tendríamos que generar una tabla de doble entrada con dichos porcentajes.

Esto lo podemos hacer sobre la tabla de doble entrada generada anteriormente utilizando el método div() de Pandas:

tabla_porc = tabla_freq.div(tabla_freq.sum(axis = 1), axis = 0)*100
print(tabla_porc)
alive      no    yes
class               
First   37.04  62.96
Second  52.72  47.28
Third   75.76  24.24

GRÁFICO DE BARRAS APILADAS

En el código anterior, tabla_freq.sum(axis = 1) calcula la suma total de personas para cada clase.

tabla_freq.sum(axis = 1)
class
First     216
Second    184
Third     491
dtype: int64

tabla_freq.div(tabla_freq.sum(axis = 1), axis = 0) divide cada valor de la tabla tabla_freq entre el total de personas por clase. El argumento axis = 0 indica que la división se realiza por filas.

GRÁFICO DE BARRAS APILADAS

Finalmente, nos queda graficar la info contenida en la tabla de porcentajes que generamos:

tabla_porc.plot.barh(stacked = True)

GRÁFICO DE BARRAS APILADAS (+ prolijo)

tabla_porc.plot.barh(stacked = True)
plt.xlabel('Porcentaje (%)', fontweight = 'bold')
plt.ylabel('Clase', fontweight = 'bold')
plt.gca().set_yticklabels(['Primera', 'Segunda', 'Tercera'])
legend = plt.legend(title = '¿Sobrevivió?', fontsize = 15, labels = ['No', 'Sí'], bbox_to_anchor = (1,1))
legend.get_title().set_fontsize('15')

PARA CERRAR LA SECCIÓN

Más allá de colores y anchos de barras, ¿cuál es la principal diferencia entre estos dos gráficos que construimos?

PARA CERRAR LA SECCIÓN

El primer gráfico muestra la distribución conjunta de las variables class y alive en el total de los datos, es decir, en el total de pasajeros que viajaban en el Titanic. Para llegar al 100%, hay que sumar los porcentajes de todas las barras.

El título podría ser: “Distribución de pasajeros del Titanic según clase y supervivencia al naufragio”.

PARA CERRAR LA SECCIÓN

El segundo gráfico muestra la distribución condicional de la supervivencia al naufragio dada la clase en la que viajaba el pasajero. Los porcentajes suman 100% dentro de cada una de las clases.

El título podría ser: “Supervivencia al naufragio de los pasajeros según la clase en la que viajaban”.

GRÁFICO DE SECTORES

La construcción del gráfico de sectores consiste en diagramar un círculo que representa al 100% de las unidades. El mismo se divide en tantos sectores como categorías existan y el área de cada uno de los sectores es proporcional al porcentaje de unidades que pertenecen a la categoría que representa.

GRÁFICO DE SECTORES

Podemos construir un gráfico de sectores para las observaciones de class generando previamente una tabla que contenga los porcentajes correspondientes a cada una de las tres categorías:

tabla_clases = titanic['class'].value_counts().reset_index()
tabla_clases['porcentaje'] = tabla_clases['count']*100/sum(tabla_clases['count'])

print(tabla_clases)

GRÁFICO DE SECTORES

Luego, le damos forma al gráfico utilizando la función pie() de matplotlib:

plt.pie(data = tabla_clases, labels = 'class',  x = 'porcentaje', autopct = '%.0f%%', textprops = dict(color = "w"))
plt.legend(title = 'Clase', labels = ['Tercera', 'Primera', 'Segunda'], loc = 'center right', bbox_to_anchor=(1.3, 0.5));

GRÁFICO DE SECTORES vs. BARRAS

Cuando tenemos un gran número de categorías y varias de ellas presentan proporciones similares, el gráfico de sectores puede volverse muy engorroso.