D
Nucleo tecnico M5 intermedio 20 min

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:

ConceptoDescripcion
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 titulo es la conclusion

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.

python Matplotlib OOP: de caotico a presentable
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

ObjetivoOpcion #1Alternativa
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().

python Tidy data para Seaborn
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

8% de los hombres tienen daltonismo

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).

ConceptoDescripcion
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.
python Paletas accesibles
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

Grafico de torta con mas de 5 categorias
✗ plt.pie([10, 20, 30, 25, 15]) # 5 slices, dificil comparar angulos
✓ Ordena y usa barras horizontales. Las personas comparan longitudes mucho mejor que angulos.
Doble eje Y para forzar correlacion visual
✗ ax1.plot(t, ventas); ax2 = ax1.twinx(); ax2.plot(t, temperatura) # engaña al lector
✓ Si necesitas dos escalas, normaliza o usa un small multiples (panel por variable).
No rotular los ejes o la unidad
✗ plt.plot(x, y); plt.title('Datos') # ¿que son x, y, en que unidad?
✓ plt.xlabel('Fecha'); plt.ylabel('Ventas (miles de $)'); plt.title('Ventas diarias 2024')
Usar rojo y verde para distinguir 2 categorias
✗ colors = ['red' if c=='A' else 'green' for c in cats] # 8% no distingue
✓ Usa azul + naranja (colorblind-safe) o marker + color (redundancia visual).
Grafico 3D
✗ plt.bar3d(...) # el 3D distorsiona la percepcion de altura, no aporta informacion
✓ Si quieres 3 variables, usa small multiples (panel por categoria) o color como tercera dimension.
Libro: "Storytelling with Data" - Cole Nussbaumer Knaflic. La biblia del data storytelling. Cap 1-6 son lectura obligatoria.
Libro: "Fundamentals of Data Visualization" - Claus O. Wilke. Gratuito online. Decide cuando usar cada grafico.
Video: StatQuest: "Plotting with Seaborn" (YouTube)
Video: Corey Schafer: "Matplotlib Tutorial" (YouTube, ~3h, exhaustivo)
Articulo: Edward Tufte: "The Visual Display of Quantitative Information" (clasico, leer)
Herramienta: Plotly / Bokeh: graficos interactivos (hover, zoom) para dashboards web
Herramienta: Datawrapper / Flourish: herramientas no-code para visualizaciones editoriales
Mini-proyecto 5: dashboard de 4 visualizaciones intermedio
  1. 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)
  2. Cada grafico debe tener: titulo narrativo (que diga la conclusion), ejes rotulados con unidad, fuente de datos, paleta accesible.
  3. Aplica sns.despine() y elimina el chartjunk (cuadriculas gruesas, texturas 3D).
  4. 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.
  5. Bonus: converti uno a Plotly para que sea interactivo (hover muestra valores).