La lección anterior terminó con las respuestas en tablas: sábado 130 unidades, abril 168, Fausto con un 11.9 % de conversión. Correctas, pero Ana no piensa en tablas — y su cerebro, como el de todos, procesa una barra más alta que otra en milisegundos y una columna de números en... bastante más. Matplotlib es la librería de visualización de referencia en Python: la más antigua, la más flexible y la base sobre la que se construyen casi todas las demás. Esta lección enseña a elegir el gráfico según la pregunta, a montarlo pieza a pieza y a no mentir por el camino, para acabar colgando en la trastienda de Papyrus un panel con los números del semestre.

Contenido

  1. Por qué visualizar: la lección de Anscombe
  2. Anatomía de una figura
  3. plt.plot: la evolución mensual (y Sant Jordi)
  4. bar y barh: comparar títulos
  5. hist: la distribución de importes
  6. scatter: precio contra unidades
  7. pie y cuándo no usarlo
  8. subplots: el panel 2×2 de la tienda
  9. Estilo y legibilidad
  10. El atajo df.plot y savefig
  11. Qué gráfico para qué pregunta

Por qué visualizar: la lección de Anscombe

En 1973 el estadístico Francis Anscombe construyó cuatro conjuntos de datos con las mismas estadísticas — misma media de x e y, misma varianza, misma recta de regresión — que al dibujarlos resultan ser cuatro historias completamente distintas: una nube lineal, una curva, una recta perfecta con un punto rebelde y un caso degenerado donde un único punto fabrica toda la correlación. La moraleja es doble y gobierna esta lección:

  • Los resúmenes numéricos ocultan la forma. describe() de 11-03 no distingue esos cuatro mundos; un vistazo, sí.
  • Dibuja siempre antes de concluir. Especialmente antes de ajustar el modelo de 11-05: la regresión de Anscombe era "correcta" en los cuatro casos y solo tenía sentido en uno.
pip install matplotlib

Anatomía de una figura

Matplotlib distingue el lienzo (Figure) de cada zona de dibujo (Axes — unos ejes con sus datos, título y leyenda; una Figure puede contener varios). Casi todos los problemas de "¿dónde pongo el título?" se resuelven sabiendo a cuál de los dos pertenece cada pieza.

flowchart TB
    F["Figure (el lienzo)"] --> A["Axes (la zona de dibujo)"]
    A --> T["título — set_title"]
    A --> X["eje X — set_xlabel, ticks"]
    A --> Y["eje Y — set_ylabel"]
    A --> D["los datos — plot, bar, scatter..."]
    A --> L["leyenda — legend (usa los label=)"]

La forma recomendada de trabajar es la interfaz orientada a objetos: plt.subplots() te da la pareja fig, ax y a partir de ahí todo son métodos de ax — nada de estado global escondido:

import matplotlib.pyplot as plt   # el alias plt es convención, como np y pd

fig, ax = plt.subplots()          # 1 Figure con 1 Axes
ax.set_title("Mi primer gráfico")
plt.show()                        # en Jupyter aparece solo; en un script .py, show() abre la ventana

plt.plot: la evolución mensual (y Sant Jordi)

Las líneas cuentan evolución en el tiempo. Los datos: las unidades mensuales que groupby nos dio en 11-03.

meses = ["ene", "feb", "mar", "abr", "may", "jun"]
unidades = [62, 58, 71, 168, 84, 77]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(meses, unidades, marker="o", label="unidades/mes")
ax.annotate("Sant Jordi (23 abr)", xy=("abr", 168), xytext=("feb", 150),
            arrowprops=dict(arrowstyle="->"))
ax.set_title("Papyrus — unidades vendidas por mes (2026)")
ax.set_xlabel("Mes")
ax.set_ylabel("Unidades")
ax.legend()
plt.show()

El marker="o" señala los datos reales (solo hay 6 puntos; la línea entre ellos es una guía visual, no medición), y annotate clava la flecha en el pico: abril, 168 unidades, más del doble de un mes normal. Es el mismo dato del axis=0 de 11-02 y del groupby de 11-03 — pero ahora se ve, y Ana lo entiende sin saber Python.

bar y barh: comparar títulos

Las barras comparan categorías. Unidades por título (11-03):

titulos = ["El Quijote", "La Odisea", "Hamlet", "Fausto"]
unidades_titulo = [182, 154, 121, 63]

fig, ax = plt.subplots()
ax.bar(titulos, unidades_titulo)
ax.set_ylabel("Unidades (ene-jun)")
ax.set_title("Ventas por título")
plt.show()

Dos consejos que valen oro: ordena las barras por valor (ya lo están: es el sort_values de 11-03 trabajando para el gráfico) y, con nombres largos o muchas categorías, usa barh (barras horizontales): las etiquetas se leen del tirón sin girar la cabeza — ax.barh(titulos[::-1], unidades_titulo[::-1]) (invertidos para que el mayor quede arriba).

Y una regla de honestidad: el eje de una barra empieza en 0. Una barra que arranca en 100 convierte una diferencia del 5 % en un abismo visual. Matplotlib lo hace bien por defecto; no lo "arregles".

hist: la distribución de importes

El histograma trocea un rango en intervalos (bins) y cuenta cuántos valores caen en cada uno: es la forma de los datos, lo que describe() resume a ciegas.

df = pd.read_csv("datos/ventas_2026.csv", parse_dates=["fecha"])

fig, ax = plt.subplots()
ax.hist(df["importe"], bins=20, edgecolor="white")
ax.set_xlabel("Importe de la venta (€)")
ax.set_ylabel("Nº de ventas")
ax.set_title("Distribución de importes")
plt.show()

El dibujo confirma la sospecha de 11-01: una montaña concentrada entre 9 y 16 € (las ventas de un ejemplar) y una cola larga a la derecha — los packs de Sant Jordi hasta 63.60 € — que es la que separa la media (15.03) de la mediana (12.50). Prueba bins=5 y bins=50: pocos bins esconden la forma, demasiados la hacen ruido. No hay número mágico; se tantea.

scatter: precio contra unidades

El diagrama de dispersión busca relación entre dos variables numéricas:

precios = [15.90, 12.50, 9.95, 21.00]
unidades_titulo = [182, 154, 121, 63]

fig, ax = plt.subplots()
ax.scatter(precios, unidades_titulo, s=80)
for p, u, t in zip(precios, unidades_titulo, ["El Quijote", "La Odisea", "Hamlet", "Fausto"]):
    ax.annotate(t, (p, u), xytext=(5, 5), textcoords="offset points")
ax.set_xlabel("Precio (€)")
ax.set_ylabel("Unidades vendidas")
ax.set_title("¿Vende menos lo que cuesta más?")
plt.show()

Cuatro puntos y una historia: no hay una recta limpia (El Quijote, a 15.90, es el superventas), pero Fausto está solo en su esquina — el más caro y el que menos vende, coherente con su 11.9 % de conversión de 11-03. Con solo 4 puntos esto es una pista, no una ley: exactamente la prudencia que necesitaremos al ajustar la regresión de 11-05 sobre este mismo par de variables.

pie y cuándo no usarlo

fig, ax = plt.subplots()
ax.pie(unidades_titulo, labels=["El Quijote", "La Odisea", "Hamlet", "Fausto"],
       autopct="%1.0f%%")
ax.set_title("Cuota de unidades por título")
plt.show()

La advertencia honesta: el ojo humano compara longitudes mucho mejor que ángulos. ¿Es mayor la porción de La Odisea (30 %) o la de Hamlet (23 %)? En la tarta cuesta; en la barra es evidente. El pie solo funciona con pocas categorías (2-4), partes de un todo y diferencias grandes — el reparto tienda/web (60/40) pasa el corte; casi todo lo demás está mejor en barras. Si dudas, barras.

subplots: el panel 2×2 de la tienda

Todo junto: el panel que Ana quiere imprimir y colgar en la trastienda.

fig, axs = plt.subplots(2, 2, figsize=(11, 7))   # axs es un array 2x2 de Axes (¡11-02!)

axs[0, 0].plot(meses, unidades, marker="o")
axs[0, 0].set_title("Unidades por mes")

axs[0, 1].barh(titulos[::-1], unidades_titulo[::-1])
axs[0, 1].set_title("Unidades por título")

axs[1, 0].hist(df["importe"], bins=20, edgecolor="white")
axs[1, 0].set_title("Distribución de importes (€)")

dias = ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"]
axs[1, 1].bar(dias, [48, 52, 55, 60, 78, 130, 97])
axs[1, 1].set_title("Unidades por día de la semana")

fig.suptitle("Papyrus — panel del primer semestre 2026", fontsize=14)
fig.tight_layout()
plt.show()

subplots(2, 2) devuelve la Figure y una matriz 2×2 de Axes que se indexa como los arrays de 11-02 (axs[1, 1] = abajo derecha). suptitle titula el lienzo entero y tight_layout() recoloca todo para que nada se pise — sin él, los títulos de arriba se comen los ejes de abajo.

Estilo y legibilidad

  • Etiquetas SIEMPRE. Un gráfico sin xlabel/ylabel/title es una adivinanza. Si el eje son euros, que lo diga; el "yo ya sé lo que es" caduca en una semana.
  • Colores con criterio. El azul por defecto está bien; el arcoíris, no. Usa un color neutro para todo y uno solo de acento para el dato que importa (Fausto en el scatter, abril en la línea). El color debe significar algo o no estar. Y recuerda que ~8 % de los hombres confunde rojo y verde: no apoyes en esa pareja la única distinción.
  • Fechas rotadas. Con muchas fechas en el eje X se amontonan: ax.tick_params(axis="x", rotation=45) o fig.autofmt_xdate().
  • Menos es más. Cada elemento (rejilla, borde, leyenda de una sola serie) debe justificar su tinta. ax.grid(alpha=0.3) como mucho.

El atajo df.plot y savefig

pandas envuelve Matplotlib: toda Series o DataFrame tiene .plot, ideal para mirar rápido mientras exploras:

df.groupby(df["fecha"].dt.month)["unidades"].sum().plot(kind="bar", title="Unidades por mes")
df["importe"].plot(kind="hist", bins=20)

Una línea desde el groupby hasta el dibujo. Devuelve el ax de siempre, así que puedes seguir retocando (ax.set_ylabel(...)). Para explorar, df.plot; para la figura definitiva y a medida, la interfaz fig, ax.

Y el paso final — Ana no ejecuta Python, Ana recibe un PNG:

fig.savefig("informes/panel_papyrus.png", dpi=150, bbox_inches="tight")

Al directorio informes/ de M6, como los CSV de 11-03. dpi=150 da calidad de impresión razonable y bbox_inches="tight" recorta márgenes muertos. Importante: llama a savefig antes de show() — tras mostrar, la figura puede quedar en blanco.

Qué gráfico para qué pregunta

Tipo de pregunta Gráfico Ejemplo Papyrus
¿Cómo evoluciona en el tiempo? plot (líneas) Unidades por mes, pico de Sant Jordi
¿Cómo se comparan categorías? bar / barh Ventas por título, por día de semana
¿Qué forma tienen mis datos? hist Distribución de importes
¿Se relacionan dos variables? scatter Precio vs unidades
¿Qué parte del todo? (pocas partes) pie (con cautela) Reparto tienda/web
Varias preguntas a la vez subplots El panel 2×2

Cuando esta tabla se te quede corta: seaborn (gráficos estadísticos elegantes sobre Matplotlib, en una línea lo que aquí son diez) y plotly (gráficos interactivos con zoom y tooltips, perfectos para la web de M10) son las siguientes paradas naturales. Todo lo aprendido aquí — figuras, ejes, tipos de gráfico, honestidad — se transfiere tal cual.

Errores Comunes y Consejos

  • Olvidar plt.show() en scripts. En Jupyter la figura aparece sola; en un .py sin show() el programa termina sin mostrar nada. (Y savefig va antes de show.)
  • Reutilizar la figura sin querer. Dibujar dos gráficos seguidos sobre el mismo ax los superpone. Cada figura nueva, su fig, ax = plt.subplots().
  • Truncar el eje Y en barras. El clásico de los gráficos mentirosos: diferencias minúsculas parecen enormes. En barras, desde 0; en líneas se admite recortar si se señaliza.
  • Demasiadas series en un gráfico. Cinco líneas cruzándose son espagueti. Mejor subplots con una por panel, o resalta una y apaga el resto en gris.
  • Ejes sin unidades. "¿168 qué? ¿euros, unidades, visitas?" Cada informe que sale de informes/ debe sobrevivir sin ti al lado explicándolo.

Ejercicios

  1. Dibuja la evolución mensual de la facturación (suma de importe por mes) como línea con marcadores, anotando Sant Jordi, partiendo del groupby correspondiente de 11-03. Etiqueta ejes con unidades (€) y guárdala en informes/facturacion_mensual.png.
  2. Haz un gráfico de barras agrupadas con las unidades por título y canal (la tabla unstack() de 11-03: tienda 121/38/74/79, web 61/25/47/75). Pista: tabla.T.plot(kind="bar") lo hace en una línea; interpreta qué historia cuenta.
  3. El panel 2×2 de la lección usa el mismo color para todo. Modifícalo para que solo la barra del sábado (en el gráfico de días) y el punto de abril (en la línea mensual) queden en un color de acento, y el resto en gris. Pista: bar acepta una lista de colores, y puedes superponer un ax.plot de un único punto.

Soluciones

  1. fact = df.groupby(df["fecha"].dt.month)["importe"].sum()
    fig, ax = plt.subplots(figsize=(8, 4))
    ax.plot(["ene", "feb", "mar", "abr", "may", "jun"], fact.values, marker="o")
    ax.annotate("Sant Jordi", xy=("abr", fact[4]), xytext=("feb", fact[4] * 0.9),
                arrowprops=dict(arrowstyle="->"))
    ax.set_title("Papyrus — facturación mensual 2026")
    ax.set_xlabel("Mes"); ax.set_ylabel("Facturación (€)")
    fig.savefig("informes/facturacion_mensual.png", dpi=150, bbox_inches="tight")
    plt.show()
    
    La curva calca la de unidades — pico en abril — porque el ticket medio apenas varía entre meses. Comprobar que dos métricas cuentan la misma historia también es un hallazgo.
  2. tabla = df.groupby(["canal", "titulo"])["unidades"].sum().unstack()
    ax = tabla.T.plot(kind="bar", figsize=(8, 4))   # .T: títulos al eje X, una barra por canal
    ax.set_ylabel("Unidades"); ax.set_title("Ventas por título y canal")
    ax.tick_params(axis="x", rotation=0)
    plt.show()
    
    La historia: en casi todos los títulos la barra de tienda gana... salvo en La Odisea, donde la web casi la iguala a la tienda (75 vs 79) y supera con claridad al Quijote web. El hallazgo de 11-03, ahora visible de un golpe.
  3. colores = ["#9e9e9e"] * 7
    colores[5] = "#d62728"                     # sábado, índice 5
    axs[1, 1].bar(dias, unidades_dia, color=colores)
    
    axs[0, 0].plot(meses, unidades, marker="o", color="#9e9e9e")
    axs[0, 0].plot(["abr"], [168], marker="o", color="#d62728", markersize=10)
    
    El gris apaga lo secundario y el acento dirige el ojo exactamente a los dos datos que sostienen la decisión de Ana: el sábado y Sant Jordi. Ese "apagar para resaltar" es probablemente el truco de comunicación con más rendimiento por línea de código de toda la lección.

Conclusión

Ya sabes convertir los números de Papyrus en historias: líneas para la evolución (y la flecha clavada en Sant Jordi), barras para comparar títulos y días, histogramas para ver la forma que describe() esconde, scatter para sospechar relaciones — con Fausto solo en su esquina cara —, la tarta bajo fianza, y el panel 2×2 guardado como PNG en informes/ para la trastienda. Y el principio que lo gobierna todo, herencia de Anscombe: dibuja antes de concluir, porque los resúmenes ocultan la forma. Ese consejo va a ser inmediatamente práctico: la pregunta que queda viva — ¿cuánto stock pedir para el próximo Sant Jordi? — no se responde describiendo el pasado, sino proyectándolo. Aprender patrones del histórico para predecir lo que viene es aprendizaje automático, y la última lección del módulo lo hace de verdad, con scikit-learn, dos casos completos y las advertencias honestas que el marketing suele saltarse.

Curso de Programación en Python

Módulo 1: Introducción a Python

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados