Visualización de Datos con Matplotlib y Seaborn en Python
Matplotlib es el motor de renderizado 2D más utilizado en Python; Seaborn lo extiende con gráficos estadísticos de alto nivel. Juntos cubren desde plots exploratorios rápidos hasta figuras listas para publicación.
Instalación
pip install matplotlib seaborn
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
import numpy as np
import pandas as pd
Arquitectura de Matplotlib: Figure y Axes
# Forma recomendada: crear Figure y Axes explícitamente
fig, ax = plt.subplots(figsize=(8, 5))
x = np.linspace(0, 2 * np.pi, 300)
ax.plot(x, np.sin(x), label='sin(x)', color='steelblue', linewidth=2)
ax.plot(x, np.cos(x), label='cos(x)', color='tomato', linestyle='--')
ax.set_title('Funciones trigonométricas', fontsize=14)
ax.set_xlabel('x (radianes)')
ax.set_ylabel('Amplitud')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('trig.png', dpi=150)
plt.show()
Tipos de gráfico esenciales
Gráfico de línea con relleno de área
fig, ax = plt.subplots(figsize=(9, 4))
np.random.seed(42)
dias = pd.date_range('2024-01-01', periods=90)
ventas = 500 + np.cumsum(np.random.randn(90) * 20)
media_movil = pd.Series(ventas).rolling(7).mean()
ax.plot(dias, ventas, color='lightsteelblue', alpha=0.6, label='Ventas diarias')
ax.plot(dias, media_movil, color='steelblue', linewidth=2, label='Media móvil 7d')
ax.fill_between(dias, ventas, media_movil, alpha=0.15, color='steelblue')
ax.set_title('Ventas Q1 2024')
ax.legend()
ax.xaxis.set_major_locator(mpl.dates.WeekdayLocator(byweekday=0))
ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('%d %b'))
fig.autofmt_xdate()
plt.tight_layout()
Gráfico de barras agrupadas
categorias = ['IT', 'Marketing', 'Ventas', 'RR.HH.']
q1 = [420, 310, 580, 210]
q2 = [460, 285, 620, 230]
x = np.arange(len(categorias))
ancho = 0.35
fig, ax = plt.subplots(figsize=(8, 5))
barras1 = ax.bar(x - ancho/2, q1, ancho, label='Q1', color='steelblue')
barras2 = ax.bar(x + ancho/2, q2, ancho, label='Q2', color='tomato')
ax.set_xticks(x)
ax.set_xticklabels(categorias)
ax.set_ylabel('Miles de euros')
ax.set_title('Presupuesto por departamento')
ax.legend()
# Añadir etiquetas sobre las barras
ax.bar_label(barras1, fmt='%d', padding=3, fontsize=9)
ax.bar_label(barras2, fmt='%d', padding=3, fontsize=9)
plt.tight_layout()
Diagrama de dispersión con regresión
rng = np.random.default_rng(0)
n = 100
x = rng.uniform(0, 10, n)
y = 2.5 * x + rng.normal(0, 3, n)
fig, ax = plt.subplots(figsize=(7, 5))
ax.scatter(x, y, alpha=0.6, edgecolors='white', linewidths=0.5, color='steelblue', s=60)
# Línea de tendencia manual
m, b = np.polyfit(x, y, 1)
xr = np.array([x.min(), x.max()])
ax.plot(xr, m*xr + b, 'r--', linewidth=1.5, label=f'y = {m:.2f}x + {b:.2f}')
ax.set_title('Correlación precio-ventas')
ax.legend()
plt.tight_layout()
Histograma y distribución
rng = np.random.default_rng(1)
datos_a = rng.normal(70, 10, 500)
datos_b = rng.normal(80, 15, 500)
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
# Histograma comparativo
axes[0].hist(datos_a, bins=30, alpha=0.6, color='steelblue', label='Grupo A', density=True)
axes[0].hist(datos_b, bins=30, alpha=0.6, color='tomato', label='Grupo B', density=True)
axes[0].set_title('Distribución de puntuaciones')
axes[0].legend()
# Boxplot
axes[1].boxplot([datos_a, datos_b], labels=['Grupo A', 'Grupo B'],
patch_artist=True,
boxprops=dict(facecolor='lightsteelblue'))
axes[1].set_title('Comparación de grupos')
plt.tight_layout()
Subplots y layouts avanzados
# GridSpec para layouts irregulares
fig = plt.figure(figsize=(12, 8))
gs = fig.add_gridspec(2, 3, hspace=0.4, wspace=0.35)
ax_main = fig.add_subplot(gs[0, :2]) # ocupa 2 columnas
ax_side = fig.add_subplot(gs[0, 2]) # columna derecha
ax_bot1 = fig.add_subplot(gs[1, 0])
ax_bot2 = fig.add_subplot(gs[1, 1])
ax_bot3 = fig.add_subplot(gs[1, 2])
x = np.linspace(0, 4, 100)
ax_main.plot(x, np.sin(x) * np.exp(-x/3))
ax_main.set_title('Oscilación amortiguada')
for ax in [ax_side, ax_bot1, ax_bot2, ax_bot3]:
ax.plot(np.random.randn(50).cumsum())
ax.set_title(f'Serie {id(ax) % 100}')
fig.suptitle('Dashboard de análisis', fontsize=15, y=1.01)
plt.tight_layout()
Seaborn: visualización estadística
# Cargar dataset de ejemplo
tips = sns.load_dataset('tips') # DataFrame con datos de propinas
# Estilo global
sns.set_theme(style='whitegrid', palette='muted', font_scale=1.1)
Distribuciones
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
sns.histplot(tips['total_bill'], kde=True, ax=axes[0])
axes[0].set_title('Distribución de cuentas')
sns.boxplot(data=tips, x='day', y='total_bill', hue='sex', ax=axes[1])
axes[1].set_title('Cuentas por día y sexo')
sns.violinplot(data=tips, x='day', y='tip', inner='box', ax=axes[2])
axes[2].set_title('Propinas por día')
plt.tight_layout()
Regresión y scatter
# regplot — scatter + regresión con intervalo de confianza
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
sns.regplot(data=tips, x='total_bill', y='tip', ax=axes[0],
scatter_kws={'alpha': 0.4}, line_kws={'color': 'red'})
axes[0].set_title('Propina vs Cuenta total')
# scatterplot coloreado por categoría
sns.scatterplot(data=tips, x='total_bill', y='tip',
hue='smoker', size='size', ax=axes[1], alpha=0.7)
axes[1].set_title('Scatter multivariable')
plt.tight_layout()
Heatmap de correlación
numeric_cols = ['total_bill', 'tip', 'size']
corr_matrix = tips[numeric_cols].corr()
fig, ax = plt.subplots(figsize=(6, 5))
sns.heatmap(
corr_matrix,
annot=True,
fmt='.2f',
cmap='coolwarm',
vmin=-1, vmax=1,
linewidths=0.5,
ax=ax
)
ax.set_title('Correlación entre variables')
plt.tight_layout()
FacetGrid — múltiples paneles automáticos
g = sns.FacetGrid(tips, col='time', row='sex', height=3.5, aspect=1.2)
g.map_dataframe(sns.scatterplot, x='total_bill', y='tip', alpha=0.5)
g.set_axis_labels('Cuenta total', 'Propina')
g.add_legend()
g.figure.suptitle('Propinas por franja horaria y sexo', y=1.02)
Paletas de color y accesibilidad
# Paletas categóricas
sns.color_palette('tab10') # 10 colores distintos
sns.color_palette('Set2') # suaves, publicación
sns.color_palette('colorblind') # apto para daltónicos
# Paletas continuas
sns.color_palette('viridis', as_cmap=True) # perceptualmente uniforme
sns.color_palette('RdBu_r', as_cmap=True) # divergente
# Mostrar paleta
sns.palplot(sns.color_palette('colorblind'))
plt.show()
Exportar figuras de calidad editorial
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(np.sin(np.linspace(0, 10, 300)))
# PNG para web (150-200 dpi)
fig.savefig('figura_web.png', dpi=150, bbox_inches='tight')
# PDF vectorial para publicaciones científicas
fig.savefig('figura_publicacion.pdf', bbox_inches='tight')
# SVG para edición posterior en Inkscape/Illustrator
fig.savefig('figura_editable.svg', bbox_inches='tight')
# Configuración global para documentos (LaTeX-style)
mpl.rcParams.update({
'font.family': 'serif',
'font.size': 11,
'axes.titlesize': 13,
'figure.dpi': 150,
})
Buenas prácticas
- Usar siempre
fig, ax = plt.subplots()en lugar de la APIplt.plot()de estado global — facilita la reutilización y evita efectos secundarios en notebooks. tight_layout()oconstrained_layout=Trueal crear la figura previene el solapamiento de etiquetas.- Seaborn para exploración rápida; Matplotlib para control fino — combínalos: genera el plot con Seaborn y ajusta los detalles accediendo a
ax. - Paletas accesibles:
'colorblind'de Seaborn o'viridis'/'plasma'(uniformes en color y escala de grises). - Exportar en alta resolución (
dpi=300) o en vectorial (PDF/SVG) para materiales impresos;dpi=96-150es suficiente para web.
Conversiones relacionadas
Conversiones frecuentes del catálogo: