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
- Por qué visualizar: la lección de Anscombe
- Anatomía de una figura
plt.plot: la evolución mensual (y Sant Jordi)barybarh: comparar títuloshist: la distribución de importesscatter: precio contra unidadespiey cuándo no usarlosubplots: el panel 2×2 de la tienda- Estilo y legibilidad
- El atajo
df.plotysavefig - 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.
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 ventanaplt.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/titlees 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)ofig.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:
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.pysinshow()el programa termina sin mostrar nada. (Ysavefigva antes deshow.) - Reutilizar la figura sin querer. Dibujar dos gráficos seguidos sobre el mismo
axlos superpone. Cada figura nueva, sufig, 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
subplotscon 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
- Dibuja la evolución mensual de la facturación (suma de
importepor mes) como línea con marcadores, anotando Sant Jordi, partiendo delgroupbycorrespondiente de 11-03. Etiqueta ejes con unidades (€) y guárdala eninformes/facturacion_mensual.png. - 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. - 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:
baracepta una lista de colores, y puedes superponer unax.plotde un único punto.
Soluciones
-
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.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 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.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() -
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.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)
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
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
