Matplotlib básico: visualización de datos con Python

📅 Actualizado en marzo 2026 📊 Nivel: Principiante ⏱️ 17 min de lectura

Hasta ahora hemos aprendido a crear y transformar datos con NumPy y Pandas. Pero los datos en bruto, por bien estructurados que estén, son esencialmente invisibles: un DataFrame de diez mil filas no comunica nada a simple vista. Matplotlib cierra esa brecha. Es la librería que convierte números en imágenes: curvas que revelan tendencias, barras que hacen comparaciones obvias, scatter plots que muestran correlaciones que ningún coeficiente estadístico transmite tan directamente. Si NumPy es el músculo y Pandas el cerebro, Matplotlib es la voz.

Medallón de Aristóteles de Estagira, filósofo griego y padre del empirismo y la lógica formal
Nihil est in intellectu quod non prius fuerit in sensu
«Nada hay en el intelecto que no haya pasado antes por los sentidos»
Aristóteles de Estagira · Filósofo griego · 384 a.C. – 322 a.C.
Aristóteles fundó el empirismo occidental: el conocimiento verdadero no nace de la especulación abstracta, sino de la observación directa del mundo. Trescientos años antes de Cristo ya entendía lo que tardamos tanto en aplicar al análisis de datos: un número, por sí solo, no genera conocimiento. La media de ventas de un dataset de un millón de filas es un dato; su representación gráfica a lo largo del tiempo es conocimiento. Matplotlib es la herramienta que cierra ese circuito entre los datos crudos y la comprensión humana, haciendo pasar la información por el único canal que el cerebro procesa de forma instantánea: la vista.

¿Qué es Matplotlib y cuándo usarlo?

Matplotlib nació en 2003 de la frustración de John D. Hunter: necesitaba reproducir en Python los gráficos que MATLAB generaba para análisis de señales neuronales, sin pagar la licencia de MATLAB. El resultado fue una librería que hoy tiene más de 20 años, más de tres millones de descargas semanales y que sirve de base a prácticamente todas las librerías de visualización del ecosistema Python (Seaborn, Pandas .plot(), Plotly estático).

Matplotlib es la elección correcta cuando necesitas control total sobre el aspecto del gráfico, cuando generas figuras para publicaciones científicas o informes técnicos, cuando integras visualizaciones en aplicaciones Python (Flask, Django, scripts de análisis automatizados) o cuando combinas tipos de gráficos no estándar en una misma figura. Para exploración rápida y gráficos estadísticos comunes con mejor aspecto por defecto, Seaborn (que usa Matplotlib internamente) puede ser más conveniente.

pip install matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

print(plt.matplotlib.__version__)  # 3.x es la versión actual

# El módulo principal es pyplot, que se importa siempre como plt
# matplotlib.pyplot implementa una interfaz de estado al estilo MATLAB
Carta náutica de navegación extendida sobre una mesa con regla paralela y compás de puntas, instrumentos clásicos de medición de rumbos y distancias marítimas
Una carta náutica no inventa el mar: traduce sondas de profundidad, corrientes y distancias medidas en el campo a un lenguaje visual que el navegante lee en segundos con la regla paralela y el compás. Matplotlib hace lo mismo con los datos: no añade información, la hace perceptible. Un DataFrame con cientos de registros es opaco; un scatter plot o una serie temporal revela tendencias, clusters y anomalías de forma instantánea. Fuente: Pexels, Lea Guerette (licencia comercial libre).

Anatomía de una figura: Figure, Axes y Artists

Antes de dibujar nada, conviene entender la jerarquía de objetos de Matplotlib. Todo gráfico es un árbol de objetos llamados Artists, organizados en tres niveles principales: la Figure es el lienzo completo (la ventana o la imagen final), los Axes son los sistemas de coordenadas donde se dibujan los datos (lo que normalmente llamamos "un gráfico"), y los Axis son los ejes individuales X e Y con sus escalas y etiquetas.

Matplotlib tiene dos interfaces para crear figuras. La interfaz pyplot (estilo MATLAB) mantiene un estado global y es cómoda para exploración. La interfaz orientada a objetos trabaja explícitamente con variables fig y ax, y es la recomendada para código de producción.

import matplotlib.pyplot as plt
import numpy as np

# ── Interfaz pyplot (estado global) ───────────────────────────
plt.figure(figsize=(8, 4))     # crea una Figure
plt.plot([1, 2, 3], [4, 5, 3]) # dibuja en el Axes activo
plt.title('Mi primer gráfico')
plt.xlabel('Eje X')
plt.ylabel('Eje Y')
plt.show()

# ── Interfaz OO (recomendada para producción) ──────────────────
fig, ax = plt.subplots(figsize=(8, 4))   # Figure + 1 Axes
ax.plot([1, 2, 3], [4, 5, 3])
ax.set_title('Mi primer gráfico')
ax.set_xlabel('Eje X')
ax.set_ylabel('Eje Y')
plt.tight_layout()
plt.show()

# ── La jerarquía de objetos ────────────────────────────────────
fig = plt.figure(figsize=(8, 4))   # el lienzo completo
ax  = fig.add_subplot(111)         # Axes: sistema de coordenadas

# Un Axes contiene:
# ax.xaxis / ax.yaxis  → objetos Axis con escala y ticks
# ax.lines             → lista de Line2D dibujadas
# ax.patches           → rectángulos, círculos, etc.
# ax.texts             → anotaciones de texto

# Obtener referencia al Axes activo (interfaz pyplot)
ax_actual = plt.gca()   # get current axes
fig_actual = plt.gcf()  # get current figure
📋 Axes ≠ Axis: Es una confusión frecuente incluso entre usuarios con experiencia. Axes (plural de Axis en inglés, pero en Matplotlib es un objeto distinto) representa un gráfico completo con sus dos ejes. Axis (singular) es uno de los dos ejes numéricos de ese gráfico. Una Figure puede tener múltiples Axes (subplots); cada Axes tiene exactamente dos Axis (xaxis e yaxis).

Gráficos de líneas y áreas

El gráfico de líneas es el tipo más común en análisis temporal. ax.plot() acepta arrays de X e Y y decenas de parámetros de estilo. Cuando no se especifica X, Matplotlib usa el índice como eje horizontal.

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(10, 5))

x = np.linspace(0, 4 * np.pi, 200)
y1 = np.sin(x)
y2 = np.cos(x)

# ── Parámetros principales de plot() ──────────────────────────
ax.plot(x, y1,
        color='#306998',      # azul Python
        linewidth=2,
        linestyle='-',        # '-', '--', ':', '-.'
        marker='o',           # marcador: 'o', 's', '^', 'D', 'x', '+'
        markersize=4,
        markevery=20,         # marcador cada 20 puntos
        label='sin(x)')

ax.plot(x, y2,
        color='#FFD43B',      # amarillo Python
        linewidth=2,
        linestyle='--',
        label='cos(x)')

# ── Anotaciones básicas ────────────────────────────────────────
ax.set_title('Funciones seno y coseno', fontsize=14, fontweight='bold')
ax.set_xlabel('Ángulo (radianes)')
ax.set_ylabel('Amplitud')
ax.legend()                   # muestra la leyenda con los labels
ax.grid(True, alpha=0.3)      # rejilla semitransparente
ax.set_xlim(0, 4 * np.pi)     # límites del eje X

plt.tight_layout()
plt.show()

# ── Múltiples líneas con datos reales (Pandas) ────────────────
import pandas as pd

df = pd.DataFrame({
    'mes':    range(1, 13),
    'ventas': [120, 145, 132, 178, 190, 210, 195, 220, 205, 240, 260, 310],
    'costes': [ 80,  90,  85, 100, 115, 120, 110, 125, 115, 130, 140, 160],
})

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(df['mes'], df['ventas'], label='Ventas',  color='#2ecc71', linewidth=2)
ax.plot(df['mes'], df['costes'], label='Costes',  color='#e74c3c', linewidth=2, linestyle='--')
ax.fill_between(df['mes'], df['ventas'], df['costes'],
                alpha=0.15, color='#2ecc71')  # área entre curvas
ax.set_xticks(range(1, 13))
ax.set_xticklabels(['Ene','Feb','Mar','Abr','May','Jun',
                     'Jul','Ago','Sep','Oct','Nov','Dic'])
ax.set_title('Ventas vs costes — 2025')
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
💡 fill_between para áreas: ax.fill_between(x, y1, y2) rellena el área entre dos curvas. Es especialmente útil para visualizar márgenes (ventas menos costes), intervalos de confianza o rangos mín/máx. El parámetro alpha controla la transparencia (0 = invisible, 1 = opaco). Un valor de 0.1–0.3 suele dar un aspecto elegante sin ocultar las líneas.

Gráficos de barras: verticales y horizontales

Los gráficos de barras son el formato correcto para comparar categorías. ax.bar() dibuja barras verticales; ax.barh() dibuja barras horizontales. Las barras horizontales son preferibles cuando las etiquetas de categoría son largas (no caben bien en el eje X rotadas) o cuando el número de categorías es alto y quieres priorizar la lectura de la lista.

import matplotlib.pyplot as plt
import numpy as np

# ── Barras verticales simples ──────────────────────────────────
categorias = ['Python', 'JavaScript', 'Java', 'C#', 'Go', 'Rust']
popularidad = [32.5, 22.1, 15.3, 10.8, 8.2, 5.4]

fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(categorias, popularidad,
              color=['#306998', '#f7df1e', '#b07219', '#178600', '#00acd7', '#ce3d28'],
              edgecolor='white', linewidth=0.5,
              width=0.6)

# Etiquetas encima de cada barra
for bar, val in zip(bars, popularidad):
    ax.text(bar.get_x() + bar.get_width() / 2,
            bar.get_height() + 0.3,
            f'{val}%',
            ha='center', va='bottom', fontsize=9, fontweight='bold')

ax.set_title('Lenguajes más populares en 2025', fontsize=13)
ax.set_ylabel('Índice de popularidad (%)')
ax.set_ylim(0, max(popularidad) * 1.15)
ax.spines[['top', 'right']].set_visible(False)  # quitar ejes superior y derecho
plt.tight_layout()
plt.show()

# ── Barras horizontales (categorías con nombres largos) ────────
departamentos = ['Ingeniería', 'Marketing', 'Ventas', 'Operaciones', 'RRHH']
presupuesto   = [450000, 120000, 280000, 195000, 85000]

fig, ax = plt.subplots(figsize=(8, 4))
bars_h = ax.barh(departamentos, presupuesto,
                 color='#306998', alpha=0.85)

for bar, val in zip(bars_h, presupuesto):
    ax.text(bar.get_width() + 5000,
            bar.get_y() + bar.get_height() / 2,
            f'{val:,} €',
            va='center', fontsize=9)

ax.set_title('Presupuesto por departamento')
ax.set_xlabel('Presupuesto (€)')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()

# ── Barras agrupadas ───────────────────────────────────────────
trimestres = ['Q1', 'Q2', 'Q3', 'Q4']
ventas_2024 = [120, 145, 165, 200]
ventas_2025 = [135, 160, 185, 230]

x     = np.arange(len(trimestres))
width = 0.35

fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(x - width/2, ventas_2024, width, label='2024', color='#a8c8e8')
ax.bar(x + width/2, ventas_2025, width, label='2025', color='#306998')

ax.set_xticks(x)
ax.set_xticklabels(trimestres)
ax.set_title('Ventas por trimestre: 2024 vs 2025')
ax.set_ylabel('Ventas (miles €)')
ax.legend()
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()
⚠️ ax.spines[['top', 'right']].set_visible(False): Esta es probablemente la personalización más rentable de Matplotlib. Los ejes superior y derecho no aportan información y añaden ruido visual. Eliminarlos (o hacerlos invisibles) hace que cualquier gráfico parezca más moderno y profesional al instante. Esta técnica viene del trabajo de Edward Tufte sobre el ratio dato/tinta.

Scatter plots e histogramas

El scatter plot muestra la relación entre dos variables cuantitativas: cada punto es una observación, su posición en X e Y codifica los valores. Es el gráfico para detectar correlaciones, clusters, outliers. El histograma muestra la distribución de una variable: cómo se reparten los valores entre rangos (bins). Es el primer gráfico que deberías hacer con cualquier variable numérica nueva.

import matplotlib.pyplot as plt
import numpy as np

# ── Scatter plot básico ────────────────────────────────────────
np.random.seed(42)
n = 150
experiencia = np.random.uniform(1, 15, n)                       # años
salario     = experiencia * 3500 + np.random.normal(0, 8000, n) # euros
depto_id    = np.random.choice([0, 1, 2], n)                    # 3 departamentos
colores     = ['#306998', '#FFD43B', '#2ecc71']

fig, ax = plt.subplots(figsize=(9, 6))
for dept, color, nombre in zip([0, 1, 2], colores,
                                ['Ingeniería', 'Marketing', 'Ventas']):
    mask = depto_id == dept
    ax.scatter(experiencia[mask], salario[mask],
               c=color, label=nombre,
               alpha=0.7,        # transparencia para ver solapamiento
               s=60,             # tamaño del marcador en puntos²
               edgecolors='white', linewidths=0.5)

ax.set_title('Experiencia vs Salario por departamento')
ax.set_xlabel('Años de experiencia')
ax.set_ylabel('Salario anual (€)')
ax.legend()
ax.grid(True, alpha=0.3, linestyle='--')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()

# ── Scatter con color continuo (colormap) ─────────────────────
fig, ax = plt.subplots(figsize=(8, 5))
scatter = ax.scatter(experiencia, salario,
                     c=salario,          # color según valor de salario
                     cmap='viridis',     # colormap: viridis, plasma, coolwarm, RdYlGn
                     alpha=0.8, s=50)
plt.colorbar(scatter, ax=ax, label='Salario (€)')
ax.set_title('Scatter con escala de color continua')
ax.set_xlabel('Años de experiencia')
ax.set_ylabel('Salario (€)')
plt.tight_layout()
plt.show()

# ── Histograma ─────────────────────────────────────────────────
datos = np.random.normal(loc=50000, scale=12000, size=1000)  # salarios simulados

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Histograma de frecuencias absolutas
axes[0].hist(datos, bins=30, color='#306998', edgecolor='white', linewidth=0.5)
axes[0].set_title('Distribución de salarios (frecuencia)')
axes[0].set_xlabel('Salario (€)')
axes[0].set_ylabel('Nº de empleados')

# Histograma de densidad
axes[1].hist(datos, bins=30, density=True,    # density=True → área total = 1
             color='#306998', alpha=0.7, edgecolor='white')

# Superponer curva de densidad teórica
from scipy.stats import norm
mu, std = norm.fit(datos)
x_range = np.linspace(datos.min(), datos.max(), 200)
axes[1].plot(x_range, norm.pdf(x_range, mu, std),
             color='#e74c3c', linewidth=2, label=f'Normal (μ={mu:.0f}, σ={std:.0f})')
axes[1].set_title('Distribución de salarios (densidad)')
axes[1].set_xlabel('Salario (€)')
axes[1].set_ylabel('Densidad')
axes[1].legend()

for ax_i in axes:
    ax_i.spines[['top', 'right']].set_visible(False)

plt.tight_layout()
plt.show()
Paleta de pintor con manchas de pintura de múltiples colores sobre fondo de madera en un taller de arte
La paleta del pintor es la interfaz entre la visión y el material: el artista no mezcla colores por capricho, sino para comunicar temperatura, profundidad, emoción. Matplotlib funciona igual: elegir el colormap correcto no es estética, es semántica. viridis es perceptualmente uniforme y apto para daltónicos; RdYlGn (rojo-amarillo-verde) codifica bien valores positivos y negativos; coolwarm es ideal para divergencias respecto a un punto central. El color incorrecto puede hacer que los datos "mietan" sin que el autor lo pretenda. Fuente: Pexels (licencia libre).
Diagrama de Matplotlib: jerarquía de objetos Figure-Axes-Axis, interfaz pyplot vs orientada a objetos, tipos principales de gráficos (líneas, barras, scatter, histograma), opciones de personalización y subplots
El mapa completo de Matplotlib: cómo se organiza la jerarquía de objetos, la diferencia entre las dos interfaces, los tipos de gráficos más comunes con sus casos de uso, y las opciones de personalización que más impacto tienen en el resultado final. Infografía: Ciberaula.

Personalización: colores, estilos y anotaciones

Matplotlib da acceso completo a cada elemento visual. Desde los colores hasta el grosor de cada línea de los ejes, todo es configurable. La clave es conocer los mecanismos principales sin intentar memorizar todos los parámetros: los estilos predefinidos, los colormaps y el sistema de anotaciones cubren el 90% de los casos.

import matplotlib.pyplot as plt
import numpy as np

# ── Estilos predefinidos ───────────────────────────────────────
print(plt.style.available)    # lista todos los estilos disponibles
plt.style.use('seaborn-v0_8-whitegrid')  # aplicar estilo globalmente

# Estilos más útiles:
# 'seaborn-v0_8-whitegrid'  → fondo blanco con rejilla gris suave
# 'ggplot'                  → estilo R/ggplot2
# 'bmh'                     → Bayesian Methods for Hackers
# 'dark_background'         → fondo negro, bueno para presentaciones
# 'fivethirtyeight'         → estilo del medio de datos FiveThirtyEight

# ── Colores ────────────────────────────────────────────────────
# Por nombre:  'blue', 'red', 'green', 'orange', 'purple', 'gray'
# Por código:  'C0', 'C1', 'C2'... (ciclo de color actual)
# Por hex:     '#306998', '#FFD43B'
# Por RGBA:    (0.19, 0.41, 0.59, 0.8)  ← cuarto valor = alpha
# Por nombre abreviado: 'b','g','r','c','m','y','k','w'

# ── Anotaciones con ax.annotate() ─────────────────────────────
fig, ax = plt.subplots(figsize=(9, 5))
x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x * 0.15)
ax.plot(x, y, color='#306998', linewidth=2)

# Anotar el máximo
idx_max = np.argmax(y)
ax.annotate(f'Máximo ({x[idx_max]:.2f}, {y[idx_max]:.2f})',
            xy=(x[idx_max], y[idx_max]),          # punto a anotar
            xytext=(x[idx_max] + 1.5, y[idx_max] + 0.1),  # posición del texto
            arrowprops=dict(arrowstyle='->', color='#e74c3c', lw=1.5),
            fontsize=10, color='#e74c3c')

ax.axhline(y=0, color='gray', linewidth=0.8, linestyle='--')  # línea horizontal en y=0
ax.set_title('Onda amortiguada con anotación')
ax.set_xlabel('Tiempo (s)')
ax.set_ylabel('Amplitud')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()

# ── Personalización de ticks ───────────────────────────────────
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
        [3.2, 3.8, 5.1, 7.4, 8.9, 9.2, 8.7, 8.1, 6.5, 5.0, 3.9, 3.1])
ax.set_xticks(range(1, 13))
ax.set_xticklabels(['E','F','M','A','M','J','J','A','S','O','N','D'],
                   fontsize=9)
ax.set_yticks([0, 2, 4, 6, 8, 10])
ax.tick_params(axis='both', which='major', length=4, width=1)
ax.set_title('Temperatura media mensual (°C)')
plt.tight_layout()
plt.show()

# ── rcParams para configuración global ────────────────────────
plt.rcParams.update({
    'font.family':         'DejaVu Sans',
    'font.size':           11,
    'axes.titlesize':      13,
    'axes.labelsize':      11,
    'axes.spines.top':     False,
    'axes.spines.right':   False,
    'figure.figsize':      (9, 5),
    'figure.dpi':          100,
    'savefig.dpi':         150,
    'savefig.bbox':        'tight',
})

Subplots: múltiples gráficos en una figura

Con frecuencia necesitas comparar varios gráficos en paralelo o mostrar diferentes perspectivas del mismo dataset. Matplotlib permite crear cuadrículas de subplots con plt.subplots(nrows, ncols). Para layouts más complejos (subplots de distinto tamaño), usa GridSpec.

import matplotlib.pyplot as plt
import numpy as np

# ── Cuadrícula simple de subplots ──────────────────────────────
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
fig.suptitle('Dashboard: análisis de ventas', fontsize=15, fontweight='bold', y=1.02)

np.random.seed(0)
meses = range(1, 13)
ventas  = [120, 135, 128, 175, 190, 210, 195, 225, 205, 240, 260, 305]
costes  = [ 80,  90,  85, 100, 110, 125, 115, 130, 120, 135, 145, 165]
retorno = [(v - c) / c * 100 for v, c in zip(ventas, costes)]
categorias_v = {'Online': 45, 'Tienda': 30, 'Distribuidor': 25}

# Subplot (0,0): línea temporal
ax = axes[0, 0]
ax.plot(meses, ventas, 'o-', color='#306998', label='Ventas', linewidth=2)
ax.plot(meses, costes, 's--', color='#e74c3c', label='Costes', linewidth=2)
ax.set_title('Ventas vs Costes')
ax.set_xlabel('Mes')
ax.legend(fontsize=9)
ax.grid(axis='y', alpha=0.3)

# Subplot (0,1): barras
ax = axes[0, 1]
ax.bar(meses, ventas, color='#306998', alpha=0.8)
ax.set_title('Ventas mensuales')
ax.set_xlabel('Mes')
ax.set_ylabel('Miles €')

# Subplot (1,0): scatter retorno vs ventas
ax = axes[1, 0]
ax.scatter(ventas, retorno, c=retorno, cmap='RdYlGn', s=80, edgecolors='white')
ax.set_title('Volumen vs Rentabilidad')
ax.set_xlabel('Ventas (miles €)')
ax.set_ylabel('Retorno (%)')

# Subplot (1,1): histograma de ventas
ax = axes[1, 1]
ax.hist(ventas, bins=8, color='#306998', edgecolor='white')
ax.set_title('Distribución de ventas')
ax.set_xlabel('Miles €')
ax.set_ylabel('Frecuencia')

for row in axes:
    for ax_i in row:
        ax_i.spines[['top', 'right']].set_visible(False)

plt.tight_layout()
plt.show()

# ── GridSpec para layouts asimétricos ─────────────────────────
from matplotlib.gridspec import GridSpec

fig = plt.figure(figsize=(12, 7))
gs  = GridSpec(2, 3, figure=fig, hspace=0.4, wspace=0.3)

ax_grande  = fig.add_subplot(gs[0, :2])   # fila 0, columnas 0-1 (ocupa 2 columnas)
ax_derecha = fig.add_subplot(gs[0, 2])    # fila 0, columna 2
ax_inf_izq = fig.add_subplot(gs[1, 0])    # fila 1, columna 0
ax_inf_mid = fig.add_subplot(gs[1, 1])    # fila 1, columna 1
ax_inf_der = fig.add_subplot(gs[1, 2])    # fila 1, columna 2

ax_grande.plot(meses, ventas, color='#306998', linewidth=2)
ax_grande.set_title('Serie temporal — gráfico principal')

ax_derecha.pie(list(categorias_v.values()),
               labels=list(categorias_v.keys()),
               autopct='%1.0f%%',
               colors=['#306998', '#FFD43B', '#2ecc71'])
ax_derecha.set_title('Canal')

for ax_i in [ax_inf_izq, ax_inf_mid, ax_inf_der]:
    ax_i.bar(range(4), np.random.randint(50, 200, 4), color='#306998', alpha=0.7)
    ax_i.spines[['top', 'right']].set_visible(False)

plt.show()
💡 plt.tight_layout() vs constrained_layout: plt.tight_layout() ajusta automáticamente los márgenes para que los títulos y etiquetas no se solapen. Es el método clásico y funciona bien en la mayoría de casos. fig, axes = plt.subplots(..., layout='constrained') es la alternativa moderna, más precisa pero menos compatible con versiones antiguas. Siempre llama a uno de los dos antes de guardar la figura; sin ajuste de márgenes, las etiquetas exteriores quedan cortadas.

Guardar figuras en alta calidad

En producción, los gráficos no se muestran en pantalla: se guardan como archivos y se insertan en informes, dashboards o documentos. fig.savefig() admite múltiples formatos y resoluciones. Elegir el formato correcto evita pérdida de calidad o archivos innecesariamente pesados.

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(9, 5))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), color='#306998', linewidth=2)
ax.set_title('Ejemplo de guardado')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()

# ── Formatos de guardado ───────────────────────────────────────
# PNG — mapa de bits, ideal para web y documentos
fig.savefig('grafico.png',
            dpi=150,              # 150 dpi: buen equilibrio calidad/tamaño
            bbox_inches='tight',  # recortar márgenes en blanco extra
            facecolor='white')    # fondo blanco explícito (a veces es transparente)

# PDF — vectorial, ideal para publicaciones e impresión
fig.savefig('grafico.pdf',
            dpi=300,
            bbox_inches='tight')

# SVG — vectorial, editable en Inkscape/Illustrator
fig.savefig('grafico.svg', bbox_inches='tight')

# JPG — con compresión con pérdida, no recomendado para gráficos
fig.savefig('grafico.jpg', dpi=150, quality=95, bbox_inches='tight')

# ── No mostrar ventana al guardar (para scripts de producción) ─
import matplotlib
matplotlib.use('Agg')         # backend sin pantalla, antes de importar pyplot
import matplotlib.pyplot as plt
# ahora plt.show() no hace nada, solo savefig() guarda el archivo

# ── Guardar múltiples páginas en un PDF ────────────────────────
from matplotlib.backends.backend_pdf import PdfPages

with PdfPages('informe_completo.pdf') as pdf:
    for mes, dato in zip(['Enero', 'Febrero', 'Marzo'],
                         [[1,2,3], [4,5,6], [7,8,9]]):
        fig, ax = plt.subplots(figsize=(8, 4))
        ax.bar(range(len(dato)), dato, color='#306998')
        ax.set_title(f'Datos — {mes}')
        plt.tight_layout()
        pdf.savefig(fig)     # añade esta figura como una página del PDF
        plt.close(fig)       # importante: liberar memoria

# ── Configuración global de guardado con rcParams ─────────────
plt.rcParams.update({
    'savefig.dpi':    200,
    'savefig.bbox':   'tight',
    'savefig.format': 'png',     # formato por defecto
})

# ── Resolución según el uso final ─────────────────────────────
# Pantalla web / Jupyter:     dpi = 96–100
# Documentos / informes PDF:  dpi = 150–200
# Publicaciones científicas:  dpi = 300–600
# Carteles / impresión grande: dpi = 600+
Ficha de referencia rápida de Matplotlib en cuatro columnas: crear figura y ejes con figure y subplots, tipos de gráficos con plot bar scatter hist pie y boxplot, personalización con títulos etiquetas leyendas estilos y colores, guardar y exportar con savefig y rcParams
La referencia completa de Matplotlib en una página: desde la creación de figuras hasta el guardado en producción, pasando por los tipos de gráficos más habituales y las opciones de personalización de mayor impacto. Ficha de referencia: Ciberaula.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Matplotlib básico: visualización de datos con Python

Las dudas más comunes respondidas de forma clara y directa.

La interfaz pyplot (plt.plot(), plt.title()...) es cómoda para exploración rápida y scripts de un solo gráfico: menos código, resultados inmediatos. La interfaz orientada a objetos (fig, ax = plt.subplots(); ax.plot(); ax.set_title()) es la correcta para código de producción, dashboards, funciones reutilizables y subplots. La razón es que pyplot mantiene un estado global implícito: si tienes múltiples figuras abiertas o generas gráficos dentro de funciones, ese estado global puede mezclar los resultados de formas impredecibles. Con la API de objetos, cada figura y cada eje son variables explícitas que controlas tú. La regla práctica: usa pyplot para notebooks de exploración; usa la API de objetos para cualquier código que vayas a reutilizar o desplegar.
El estilo por defecto de Matplotlib está optimizado para publicaciones científicas (fondo blanco, líneas simples, sin ornamentos), no para impresto visual. Seaborn es una capa encima de Matplotlib que aplica por defecto paletas de colores más modernas y mejores proporciones. Pero con dos líneas puedes hacer que tus gráficos de Matplotlib tengan un aspecto comparable: plt.style.use("seaborn-v0_8-whitegrid") o import matplotlib.pyplot as plt; plt.rcParams.update({"figure.facecolor": "white", "axes.spines.top": False, "axes.spines.right": False}). Eliminar los dos ejes superiores (top y right) es el cambio más impactante: los gráficos parecen inmediatamente más modernos y limpios. También puedes instalar y usar el estilo "ggplot" (inspirado en R) o "bmh" (bayesiano) para variantes interesantes.
plt.show() abre una ventana interactiva y bloquea la ejecución hasta que la cierras; después de llamarla, la figura se vacía (el objeto Axes queda limpio). plt.savefig() escribe la figura a un archivo sin mostrarla en pantalla. El problema clásico: llamar plt.show() antes de plt.savefig() hace que el archivo guardado esté en blanco, porque show() limpió la figura. El orden correcto es siempre savefig() primero, show() después. En Jupyter Notebook, plt.show() es opcional porque el notebook renderiza la figura automáticamente en la celda de salida. Para scripts que solo guardan archivos (producción, pipelines), puedes suprimir cualquier ventana con matplotlib.use("Agg") antes de importar pyplot.
El tamaño se controla con figsize en pulgadas: fig, ax = plt.subplots(figsize=(10, 6)) crea una figura de 10 × 6 pulgadas. La resolución en píxeles = figsize × dpi. El dpi (dots per inch) por defecto es 100, así que una figura de 10×6 son 1000×600 píxeles. Para guardar en alta resolución (presentaciones, impresión): fig.savefig("figura.png", dpi=300) producirá 3000×1800 píxeles. Para uso en web (PNG para docs, logos): dpi=150 suele ser suficiente. Para publicaciones científicas: usa formato PDF o SVG (vectoriales, sin pérdida de calidad). El formato SVG es especialmente útil porque puedes editar el gráfico resultante en Inkscape o Illustrator.
Sí, y es una de las integraciones más convenientes del ecosistema Python. Los objetos DataFrame y Series de Pandas tienen un método .plot() que internamente llama a Matplotlib: df.plot(kind="bar"), df["col"].plot(kind="hist"), df.plot(x="fecha", y="ventas"). La ventaja es que Pandas infiere automáticamente los ejes, las etiquetas de las columnas y el índice como eje X. El resultado es un objeto Axes de Matplotlib, así que puedes seguir personalizando con ax.set_title() y compañía. Para gráficos simples de exploración, df.plot() es suficiente. Para control preciso del aspecto o para combinar múltiples DataFrames en un gráfico, usar Matplotlib directamente te da más flexibilidad.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Matplotlib básico: visualización de datos con Python? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!