Capitulo 05
Visualizacion: contar historias con datos
El 80% de tu audiencia vera UN grafico. Hacelo bien.
Matplotlib OOP (Figure, Axes, GridSpec), Seaborn para estadistica, formato tidy data, principios de diseno editorial y accesibilidad cromatica para daltonismo.
5.1 La jerarquia de visualizacion: del analisis al storytelling
No todos los graficos cumplen la misma funcion. Antes de elegir un grafico, define el objetivo:
| Concepto | Descripcion |
|---|---|
| Exploracion | Para ti. Velocidad > belleza. Pandas .plot() o Seaborn con defaults funciona. Iterar rapido. No publicar. Juguetes para ti. |
| Reporte | Para tu equipo. Claro, rotulado, con la conclusion destacada. Matplotlib con estilo, sns.despine(). Para slack, email, dashboard. |
| Storytelling | Para un CEO o un board. Un grafico, una conclusion, una accion. Colores con proposito. Titulo narrativo. Para presentacion o memoria anual. |
El error #1 de los Data Scientists: titulos descriptivos. “Ventas por mes” es descriptivo. “Las ventas cayeron 18% en Q3, la peor caida del anio” es narrativo. Si tu titulo no dice la conclusion, obligas al lector a descifrarla. En el ambito ejecutivo, el titulo ES la conclusion.
5.2 Matplotlib OOP: la API que no muere
Hay dos APIs en Matplotlib. La pyplot (plt.plot, plt.title) es la historica y rapida para EDA. La OOP (fig, ax = plt.subplots(); ax.plot(…)) es la que necesitas para figuras de calidad publicacion porque te da control sobre cada elemento.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
# Dataset: 4 senales con estadisticas distintas
np.random.seed(42)
fig, axes = plt.subplots(2, 2, figsize=(12, 9))
(ax1, ax2), (ax3, ax4) = axes
# 1) Distribucion normal: histograma + KDE
data = np.random.normal(0, 1, 1000)
ax1.hist(data, bins=40, density=True, alpha=0.6, color='#06B6D4', edgecolor='white')
from scipy.stats import norm
x = np.linspace(-4, 4, 100)
ax1.plot(x, norm.pdf(x), 'r-', lw=2, label='N(0,1)')
ax1.set_title('Distribucion Normal Estandar', fontsize=14, fontweight='bold')
ax1.set_xlabel('Valor')
ax1.set_ylabel('Densidad')
ax1.legend()
ax1.grid(alpha=0.3, axis='y')
# 2) Boxplot: comparativa entre grupos
df_groups = pd.DataFrame({
'grupo': np.repeat(['A', 'B', 'C', 'D'], 100),
'valor': np.concatenate([
np.random.normal(10, 2, 100),
np.random.normal(15, 3, 100),
np.random.normal(12, 4, 100),
np.random.normal(18, 2.5, 100),
])
})
sns.boxplot(data=df_groups, x='grupo', y='valor', ax=ax2, palette='viridis')
ax2.set_title('Distribucion por grupo', fontsize=14, fontweight='bold')
# 3) Heatmap de correlacion
df_corr = pd.DataFrame(np.random.randn(200, 6), columns=list('ABCDEF'))
corr = df_corr.corr()
sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdBu_r', center=0, ax=ax3,
square=True, cbar_kws={'shrink': 0.8})
ax3.set_title('Matriz de correlacion', fontsize=14, fontweight='bold')
# 4) Series temporales con multiples series
fechas = pd.date_range('2024-01-01', periods=100, freq='D')
ax4.plot(fechas, np.cumsum(np.random.randn(100)), label='A', linewidth=2)
ax4.plot(fechas, np.cumsum(np.random.randn(100)) + 5, label='B', linewidth=2)
ax4.plot(fechas, np.cumsum(np.random.randn(100)) - 3, label='C', linewidth=2)
ax4.set_title('Series temporales acumuladas', fontsize=14, fontweight='bold')
ax4.set_xlabel('Fecha')
ax4.set_ylabel('Valor acumulado')
ax4.legend()
ax4.grid(alpha=0.3)
# Limpiar las "spines" (bordes) para todas las subplots
for ax in axes.flatten():
sns.despine(ax=ax)
ax.grid(axis='y', alpha=0.3, linestyle='--')
plt.tight_layout()
plt.savefig('dashboard_ejemplo.png', dpi=150, bbox_inches='tight')
plt.show() 5.3 Elegir el grafico correcto
| Objetivo | Opcion #1 | Alternativa |
|---|---|---|
| comp | Barras horizontales (ordenadas de mayor a menor) | |
| dist | Histograma + KDE o violin | |
| rel | Scatter con recta de regresion | |
| corr | Heatmap con anotaciones | |
| compT | Barras apiladas (100% stacked) | |
| tend | Line plot con banda de confianza | |
| geo | Choropleth map |
5.4 Tidy Data: prerequisito para Seaborn
Seaborn esta optimizado para tidy data: una columna por variable, una fila por observacion. Si tus datos estan en wide, primero pd.melt().
import seaborn as sns
import pandas as pd
# Wide: 1 fila por categoria, columna por mes
df_wide = pd.DataFrame({
'categoria': ['A', 'B', 'C'],
'enero': [100, 150, 80],
'febrero': [110, 160, 90],
'marzo': [120, 170, 95],
})
# Tidy: 1 fila por (categoria, mes, valor) - lo que Seaborn quiere
df_tidy = pd.melt(df_wide, id_vars='categoria', var_name='mes', value_name='ventas')
# sns.lineplot requiere tidy data
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 5))
sns.lineplot(data=df_tidy, x='mes', y='ventas', hue='categoria', marker='o', ax=ax)
ax.set_title('Ventas por categoria a lo largo del tiempo')
sns.despine(ax=ax)
plt.tight_layout()
plt.show() 5.5 Accesibilidad cromatica
No es un caso de uso de nicho. Si tu presentacion va a una audiencia diversa, las paletas que elijas importan. Las paletas seguras para daltonicos varian LUMINOSIDAD, no solo tono. Las que NO son seguras dependen mucho del rojo-verde (las formas mas comunes de daltonismo).
| Concepto | Descripcion |
|---|---|
| viridis | Default en Seaborn y Matplotlib moderno. Segura, perceptualmente uniforme, funciona en grayscale. |
| cividis | Alternativa a viridis, mejor para imprimir en B/N. Apta para daltonicos. |
| magma | Esteticas, perceptualmente uniformes. Buenas para heatmaps. |
| Set2 | 5-8 colores distinguibles, segura. Util para categoricas. |
| tab10 | 10 colores de matplotlib, NO es del todo segura. Mejor: tab10 + modificacion de luminosidad. |
import seaborn as sns
import matplotlib.pyplot as plt
# Verificar como se ve la paleta en distintos tipos de daltonismo
sns.set_palette('viridis')
# O explicitamente:
# sns.color_palette('colorblind', n_colors=8) # 8 colores accesibles
# En Matplotlib:
import matplotlib as mpl
# mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=sns.color_palette('viridis', 10)) 5.6 Errores comunes
- Tomá un dataset publico y construi 4 graficos de calidad publicacion:
- 1 de distribucion (hist + KDE o violin)
- 1 de comparativa (barras horizontales ordenadas)
- 1 de tendencia temporal (line plot con media movil)
- 1 de correlacion (heatmap con anotaciones)
- Cada grafico debe tener: titulo narrativo (que diga la conclusion), ejes rotulados con unidad, fuente de datos, paleta accesible.
- Aplica
sns.despine()y elimina el chartjunk (cuadriculas gruesas, texturas 3D). - Pidele a alguien (un amigo, un compañero de cursada) que mire SOLO los titulos y te diga cual es la conclusion de cada uno. Si no puede, el titulo no esta haciendo su trabajo.
- Bonus: converti uno a Plotly para que sea interactivo (hover muestra valores).