Capítulo 15 de 17 13 secciones 19 min

Compartir

Gráficos que se entienden

matplotlib sin adornos, y la regla que vale más que todas las opciones de color juntas.

matplotlib es la librería base para graficar en Python. Y lo que decide si un gráfico se entiende no son los colores, esto lo aprendí presentando: son tres cosas, elegir el tipo según la pregunta que respondes, ordenar las barras por valor y poner un título que diga la conclusión en vez del nombre de las columnas.

Hola! Vamos a hacer que se vea

Ya sabes cargar, limpiar y agrupar. Ahora falta la parte que decide si a alguien le importa: que se entienda de un vistazo 📊

Y te lo digo de entrada, porque es la lección del capítulo: lo que hace que un gráfico funcione no son los colores. Son tres decisiones que se toman antes de escribir una línea.

Las tres reglas, antes que el código

  • 🎯 El tipo sale de la pregunta, no del gusto. Comparar categorías es barras. Ver evolución en el tiempo es líneas. Ver cómo se reparte una variable es histograma. Ver si dos cosas se mueven juntas es dispersión. Ya está, con esas cuatro haces el 95%.
  • 📐 Las barras van ordenadas por valor, no alfabéticas. Nadie va a leer seis nombres para descubrir cuál es la más alta.
  • ✍️ El título dice la conclusión, no el nombre de las columnas. "Ventas por ciudad" no dice nada. "Arequipa vende 17% más que Lima" sí.

Si haces esas tres, ya vas mejor que la mayoría de los informes que he visto. Y ninguna necesita saber matplotlib 💜

Y llévate esta pregunta a tu próxima reunión: ¿el último gráfico que presentaste decía la conclusión en el título? Si decía "Ventas por ciudad", ya sabes por dónde empezar 📊

El primer gráfico

import matplotlib
matplotlib.use('Agg')          # sin ventana, para guardar en archivo
import matplotlib.pyplot as plt
import pandas as pd
from pathlib import Path
import tempfile

df = pd.read_csv('ventas-miss-yera.csv').drop_duplicates(subset='id_venta')
df['ciudad'] = (df['ciudad'].str.strip().str.lower()
                .str.normalize('NFKD')
                .str.encode('ascii', 'ignore').str.decode('utf-8'))
df['monto'] = pd.to_numeric(df['monto'].str.replace(',', '.'), errors='coerce')

por_ciudad = df.groupby('ciudad')['monto'].sum().sort_values()
print(por_ciudad.round(0))
ciudad
lima        373379.0
cusco       387225.0
trujillo    396906.0
piura       400833.0
chiclayo    415093.0
arequipa    437847.0
Name: monto, dtype: float64

Fíjate en el sort_values() del final: ordené antes de graficar. Esa es la regla 2 y se aplica aquí, no en el gráfico.

carpeta = Path(tempfile.mkdtemp())

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.barh(por_ciudad.index, por_ciudad.values, color='#F092C7')

ax.set_title('Arequipa vende 17% más que Lima', fontsize=13, loc='left')
ax.set_xlabel('Ventas del periodo (S/)')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

fig.tight_layout()
ruta = carpeta / 'ventas-por-ciudad.png'
fig.savefig(ruta, dpi=120)
plt.close(fig)

print(ruta.name, 'creado:', ruta.exists())
print('pesa', round(ruta.stat().st_size / 1024), 'KB')
ventas-por-ciudad.png creado: True
pesa ...

Vamos por partes, que ahí hay varias cosas 🌸

  • 🖼️ fig, ax = plt.subplots() es la forma seria de empezar. Verás mucho plt.bar() suelto por internet; funciona para una prueba rápida y se vuelve un lío en cuanto hay dos gráficos.
  • 📏 barh y no bar: barras horizontales, porque los nombres de ciudad se leen sin torcer la cabeza.
  • ✂️ Esas dos líneas de spines quitan el marco de arriba y de la derecha. Es el cambio más barato que existe para que un gráfico se vea profesional.
  • 💾 tight_layout() antes de guardar, o los textos salen cortados.

Y ese matplotlib.use('Agg') de la primera línea es para guardar sin abrir ventana. En Colab no hace falta, ahí los gráficos salen solos debajo de la celda.

Y el 17% de dónde salió

De calcularlo, no de inventarlo. Si vas a poner un número en un título, compruébalo:

diferencia = (por_ciudad['arequipa'] / por_ciudad['lima'] - 1) * 100
print(f'{diferencia:.1f}%')
17.3%

Parece obvio y no lo es: los títulos con números inventados son la forma más rápida de perder la confianza de una reunión 🙃

Los cuatro tipos que necesitas

Barras: comparar categorías

por_canal = df.groupby('canal')['monto'].sum().sort_values()

fig, ax = plt.subplots(figsize=(7, 3.5))
ax.barh(por_canal.index, por_canal.values, color='#4DB8E8')
ax.set_title('Los cuatro canales venden casi lo mismo', loc='left')
fig.tight_layout()
fig.savefig(carpeta / 'canales.png', dpi=120)
plt.close(fig)

print(por_canal.round(0))
print('diferencia entre el mejor y el peor:',
      round((por_canal.max() / por_canal.min() - 1) * 100, 1), '%')
canal
WhatsApp       567392.0
Tienda         591519.0
Marketplace    619761.0
Web            632612.0
Name: monto, dtype: float64
diferencia entre el mejor y el peor: 11.5 %

Y aquí una decisión de honestidad: once por ciento de diferencia entre el mejor y el peor canal es poco. El título lo dice y no lo esconde.

La tentación sería cortar el eje para que la diferencia se vea enorme. Eso se hace todo el tiempo y es engañar con un gráfico 😖 En barras, el eje empieza en cero, siempre.

Cuatro gráficos del archivo de ventas: una línea con la venta por mes, barras horizontales por categoría, un histograma de montos con la media y la mediana marcadas, y cajas del monto por segmento de cliente.
Los cuatro, sobre el archivo de la guía. En el histograma se ve de un vistazo lo que cuenta la guía de estadística: la línea continua es la media, la punteada la mediana, y están separadas porque hay cola. Y en las cajas se ve que un segmento no solo vende más, sino que además varía muchísimo más.

El error que da todo el mundo la primera vez

Y este te va a pasar, así que mejor aquí:

fig, ax = plt.subplots()
ax.barh(por_canal.index, por_canal.values, colour='#F092C7')
AttributeError: Rectangle.set() got an unexpected keyword argument 'colour'

Escribí colour a la inglesa en vez de color. Y matplotlib no te dice "no conozco ese parámetro" a secas: te dice qué objeto lo rechazó, que es Rectangle, o sea la barra.

Ese patrón se repite con casi todos los errores de matplotlib: el mensaje nombra el objeto interno y no la función que llamaste. Cuando veas nombres raros tipo Rectangle, Line2D o Text, no te asustes: son las piezas del gráfico, y el problema está en el parámetro que les pasaste 🙂

plt.close(fig)
print('cerrada')
cerrada

Líneas: evolución en el tiempo

fecha = pd.to_datetime(df['fecha'], format='%Y-%m-%d', errors='coerce')
faltan = fecha.isna()
fecha[faltan] = pd.to_datetime(df.loc[faltan, 'fecha'],
                               format='%d/%m/%Y', errors='coerce')
df['fecha'] = fecha

por_mes = df.groupby(df['fecha'].dt.to_period('M'))['monto'].sum()
print(por_mes.head(4).round(0))
print('meses:', len(por_mes))
fecha
2025-01    128800.0
2025-02    129470.0
2025-03    139931.0
2025-04    115464.0
Freq: M, Name: monto, dtype: float64
meses: 18
fig, ax = plt.subplots(figsize=(9, 3.5))
ax.plot(por_mes.index.to_timestamp(), por_mes.values,
        marker='o', color='#F092C7')
ax.set_title('Ventas mensuales, enero 2025 a junio 2026', loc='left')
ax.set_ylim(0, None)
ax.grid(axis='y', alpha=0.3)
fig.tight_layout()
fig.savefig(carpeta / 'mensual.png', dpi=120)
plt.close(fig)

print('grafico de', len(por_mes), 'meses guardado')
grafico de 18 meses guardado

Ese set_ylim(0, None) hace lo mismo que en barras: obliga a que el eje empiece en cero. Sin él, matplotlib ajusta el eje al rango de los datos y una variación del 3% parece un terremoto.

Histograma: cómo se reparte una variable

fig, ax = plt.subplots(figsize=(8, 3.5))
ax.hist(df['monto'].dropna(), bins=40, color='#F08A8A')
ax.axvline(df['monto'].median(), color='#4A4A4A', linestyle='--')
ax.set_title('La mayoría de ventas está por debajo de S/1.000', loc='left')
fig.tight_layout()
fig.savefig(carpeta / 'histograma.png', dpi=120)
plt.close(fig)

print('mediana  S/', round(df['monto'].median(), 2))
print('promedio S/', round(df['monto'].mean(), 2))
print('por debajo de 1000:',
      round((df['monto'] < 1000).mean() * 100, 1), '%')
mediana  S/ 533.63
promedio S/ 803.76
por debajo de 1000: 73.0 %

El histograma es el gráfico que más me gusta y el que menos se usa. Con una tabla de promedios nunca ves la forma; aquí ves que el 73% de las ventas está bajo los mil soles y que hay una cola larga de ventas grandes que estira el promedio 🌟

Esa línea vertical de la mediana no es adorno: es lo que hace que se vea la distancia entre la mediana y el promedio sin explicarla.

Y ojo con bins. Con pocas cajas escondes la forma, con demasiadas todo parece ruido. Entre 20 y 50 suele funcionar, y vale la pena probar dos o tres.

Dispersión: si dos cosas se mueven juntas

fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(df['unidades'], df['monto'], s=8, alpha=0.25, color='#4DB8E8')
ax.set_xlabel('Unidades')
ax.set_ylabel('Monto (S/)')
ax.set_title('Más unidades no significa más monto', loc='left')
fig.tight_layout()
fig.savefig(carpeta / 'dispersion.png', dpi=120)
plt.close(fig)

print('correlación:', round(df['unidades'].corr(df['monto']), 3))
correlación: 0.018

Correlación de 0,018, o sea prácticamente nada. Y ahí está el valor del gráfico: cuando la respuesta es "no hay relación", eso también es un hallazgo y hay que reportarlo.

Ese alpha=0.25 hace los puntos semitransparentes. Con tres mil puntos encimados es la diferencia entre ver una mancha negra y ver dónde se concentran.

Y el aviso de siempre, que desarrollo en el libro de estadística: aunque la correlación hubiera salido alta, correlación no es causa.

Varios gráficos juntos

fig, axes = plt.subplots(1, 2, figsize=(11, 3.5))

por_segmento = df.groupby('segmento')['monto'].sum().sort_values()
axes[0].barh(por_segmento.index, por_segmento.values, color='#F092C7')
axes[0].set_title('Por segmento', loc='left')

axes[1].hist(df['satisfaccion'].dropna(), bins=5, color='#4DB8E8')
axes[1].set_title('Satisfacción', loc='left')

fig.tight_layout()
fig.savefig(carpeta / 'panel.png', dpi=120)
plt.close(fig)

print('archivos generados:', len(list(carpeta.glob('*.png'))))
archivos generados: 6

Ese subplots(1, 2) te da una fila y dos columnas, y axes es una lista. Con subplots(2, 2) serían cuatro y axes sería una matriz, así que se accede con axes[0][1].

Guardar para lo que sea

FormatoCuándo
.png con dpi=150Presentaciones, correo, informes
.svgWeb y documentos que se van a imprimir: no pierde nitidez
.pdfCuando va dentro de un documento formal
fig, ax = plt.subplots(figsize=(6, 3))
ax.barh(por_canal.index, por_canal.values, color='#F092C7')
fig.tight_layout()

for extension in ['png', 'svg', 'pdf']:
    fig.savefig(carpeta / f'canales.{extension}')
plt.close(fig)

for archivo in sorted(carpeta.glob('canales.*')):
    print(archivo.name, round(archivo.stat().st_size / 1024), 'KB')
canales.pdf ...

Y siempre plt.close(fig) después de guardar. Si generas cincuenta gráficos en un bucle sin cerrarlos, se te van acumulando en memoria y matplotlib te avisa con una advertencia que nadie entiende 😄

El número encima de la barra

Una cosa que cambia un gráfico de "está bien" a "se entiende solo": poner el valor sobre cada barra y borrar el eje 🔢

fig, ax = plt.subplots(figsize=(8, 4))
barras = ax.barh(por_ciudad.index, por_ciudad.values, color='#F092C7')

ax.bar_label(barras, labels=[f'{v/1000:,.0f}k' for v in por_ciudad.values],
             padding=4, fontsize=9)
ax.set_xlim(0, por_ciudad.max() * 1.12)
ax.get_xaxis().set_visible(False)
for lado in ('top', 'right', 'bottom'):
    ax.spines[lado].set_visible(False)

fig.tight_layout()
fig.savefig(carpeta / 'con-numeros.png', dpi=120)
plt.close(fig)

print('etiquetas puestas:', [f'{v/1000:,.0f}k' for v in por_ciudad.values])
etiquetas puestas: ['373k', '387k', '397k', '401k', '415k', '438k']

Piénsalo un segundo: si el número está escrito, el eje de abajo ya no hace falta. Estaba ahí solo para que el ojo estimara el valor, y ahora el valor está puesto 🙂

Por eso ese get_xaxis().set_visible(False) no es dejadez, es quitar lo que sobra. Y el set_xlim con ese 1.12 es para que la etiqueta de la barra más larga no se salga de la figura.

El padding=4 son los píxeles entre la punta de la barra y el texto. Sin él quedan pegados y se lee peor.

Ese labels= es opcional. Si te vale el número tal cual, con ax.bar_label(barras, fmt='S/ %.0f') ya lo tienes. Lo uso cuando quiero acortar a miles, que es lo que se hace cuando las cifras son largas.

La torta, y por qué casi nunca

Te la van a pedir. Y yo te voy a dar el argumento para decir que no, con un número, que es como se ganan estas discusiones 🥧

Los datos de arriba, repartidos en porcentaje:

reparto = (por_ciudad / por_ciudad.sum() * 100).sort_values(ascending=False)
grados = reparto / 100 * 360

print(grados.round(1))
print('la mayor y la menor se llevan',
      round(grados.iloc[0] - grados.iloc[-1], 1), 'grados de diferencia')
print('en barras, la diferencia es de',
      round(reparto.iloc[0] / reparto.iloc[-1] * 100 - 100, 1), '% de largo')
ciudad
arequipa    65.4
chiclayo    62.0
piura       59.8
trujillo    59.3
cusco       57.8
lima        55.7
Name: monto, dtype: float64
la mayor y la menor se llevan 9.6 grados de diferencia
en barras, la diferencia es de 17.3 % de largo

Ahí está todo 👀

Arequipa vende un 17% más que Lima. En un gráfico de barras esa diferencia se ve: una barra es visiblemente más larga que la otra. En una torta, esa misma diferencia son nueve grados y medio de abertura, y nueve grados en una circunferencia no los distingue nadie.

El ojo compara largos bien y compara ángulos mal. No es opinión mía, es lo que hace que la misma diferencia se vea en un gráfico y desaparezca en el otro.

fig, axes = plt.subplots(1, 2, figsize=(11, 4))

axes[0].pie(reparto.values, labels=reparto.index, autopct='%1.0f%%')
axes[0].set_title('En torta')

axes[1].barh(reparto.index[::-1], reparto.values[::-1], color='#F092C7')
axes[1].set_title('En barras')

fig.tight_layout()
fig.savefig(carpeta / 'torta-vs-barras.png', dpi=120)
plt.close(fig)

print('las dos guardadas en la misma figura')
las dos guardadas en la misma figura

Genera ese archivo y míralo. En la torta tienes que leer los porcentajes para saber cuál gana; en las barras lo ves antes de leer nada. Y si tienes que leer los números, entonces el gráfico no está trabajando: mejor una tabla 📋

Cuándo sí uso torta: cuando hay dos o tres trozos y uno es claramente más grande, tipo "el 70% de las ventas viene de un solo canal". Ahí el mensaje es "uno se lleva casi todo" y la torta lo cuenta bien. Con seis trozos parecidos, nunca.

El estilo se decide una vez

Habrás notado que en cada gráfico repetí las dos líneas de spines, el loc='left' del título y el figsize. Eso en un cuaderno de verdad se hace una sola vez, arriba, y se olvida 🎨

plt.rcParams.update({
    'axes.spines.top': False,
    'axes.spines.right': False,
    'axes.titlelocation': 'left',
    'axes.titlesize': 12,
    'figure.figsize': (8, 4),
    'axes.grid': True,
    'grid.alpha': 0.25,
})

fig, ax = plt.subplots()
ax.barh(por_canal.index, por_canal.values, color='#F092C7')
ax.set_title('Sin repetir una sola linea de estilo')
fig.tight_layout()
fig.savefig(carpeta / 'con-estilo.png', dpi=120)
plt.close(fig)

print('marco de arriba visible:', plt.rcParams['axes.spines.top'])
print('tamano por defecto     :', plt.rcParams['figure.figsize'])
marco de arriba visible: False
tamano por defecto     : [8.0, 4.0]

Cuatro líneas menos en cada gráfico, y sobre todo: todos los gráficos del informe salen iguales. Que es la mitad de lo que hace que un documento se vea hecho por alguien que sabe 💜

rcParams es el diccionario de ajustes por defecto de matplotlib. Lo que pongas ahí vale para todo lo que dibujes después, en ese cuaderno. Al reiniciar el kernel vuelve a lo de fábrica, así que va arriba del todo, con los import.

Si prefieres no elegir nada, plt.style.use('seaborn-v0_8-whitegrid') te deja algo decente de una línea. Yo prefiero las siete de arriba porque sé exactamente qué cambia cada una.

Lo que hace que se entienda, resumido

  • 🎯 El tipo sale de la pregunta: barras para comparar, líneas para el tiempo, histograma para la forma, dispersión para la relación.
  • 📊 Barras ordenadas por valor y horizontales si los nombres son largos.
  • 0️⃣ El eje empieza en cero. Cortarlo es engañar.
  • ✍️ El título dice la conclusión, con el número comprobado.
  • ✂️ Fuera el marco de arriba y de la derecha, fuera la leyenda si hay una sola serie, fuera todo lo que no aporte.

Ejercicios

1. Barras ordenadas

Grafica el total por categoría, ordenado, y guárdalo.

por_cat = df.groupby('categoria')['monto'].sum().sort_values()

fig, ax = plt.subplots(figsize=(7, 3))
ax.barh(por_cat.index, por_cat.values, color='#F092C7')
fig.tight_layout()
fig.savefig(carpeta / 'categorias.png', dpi=120)
plt.close(fig)

print(por_cat.round(0))
categoria
Cuidado personal    453910.0
Bebidas             456361.0
Limpieza            471763.0
Snacks              493895.0
Abarrotes           535355.0
Name: monto, dtype: float64
2. El título con su número

Calcula cuánto más vende la categoría líder que la última y escribe el título con ese número.

lider, ultima = por_cat.index[-1], por_cat.index[0]
cuanto = (por_cat.iloc[-1] / por_cat.iloc[0] - 1) * 100

print(f'{lider} vende {cuanto:.0f}% más que {ultima}')
Abarrotes vende 18% más que Cuidado personal
3. Histograma de unidades

Grafica cómo se reparten las unidades por venta y di cuál es la más frecuente.

fig, ax = plt.subplots(figsize=(7, 3))
ax.hist(df['unidades'], bins=25, color='#F08A8A')
fig.tight_layout()
fig.savefig(carpeta / 'unidades.png', dpi=120)
plt.close(fig)

print('mediana:', df['unidades'].median())
print('máximo :', df['unidades'].max())
mediana: 12.0
máximo : 30
4. El eje que engaña

Grafica los canales dos veces, una con el eje desde cero y otra desde el mínimo, y compara los archivos.

fig, axes = plt.subplots(1, 2, figsize=(11, 3))

axes[0].bar(por_canal.index, por_canal.values, color='#4DB8E8')
axes[0].set_title('Eje desde cero (honesto)', loc='left')

axes[1].bar(por_canal.index, por_canal.values, color='#F08A8A')
axes[1].set_ylim(por_canal.min() * 0.98, por_canal.max() * 1.01)
axes[1].set_title('Eje cortado (engañoso)', loc='left')

fig.tight_layout()
fig.savefig(carpeta / 'comparacion-ejes.png', dpi=120)
plt.close(fig)

print('los mismos datos, dos historias distintas')
los mismos datos, dos historias distintas

Ábrelos y míralos al lado. En el segundo, once por ciento de diferencia parece que un canal triplica al otro. Es el truco más usado para mentir con datos, y ahora lo vas a reconocer 👀

5. Dispersión con transparencia

Grafica satisfacción contra monto y calcula la correlación.

sub = df.dropna(subset=['satisfaccion', 'monto'])

fig, ax = plt.subplots(figsize=(6, 3.5))
ax.scatter(sub['satisfaccion'], sub['monto'], s=8, alpha=0.2, color='#4DB8E8')
fig.tight_layout()
fig.savefig(carpeta / 'satisfaccion.png', dpi=120)
plt.close(fig)

print('correlación:', round(sub['satisfaccion'].corr(sub['monto']), 3))
correlación: -0.017

Prácticamente cero otra vez. Reportar que no hay relación es tan válido como reportar que sí la hay, y a veces más útil.

6. Un panel de cuatro

Arma una figura con cuatro gráficos en dos filas.

fig, axes = plt.subplots(2, 2, figsize=(10, 6))

axes[0][0].barh(por_ciudad.index, por_ciudad.values, color='#F092C7')
axes[0][1].barh(por_canal.index, por_canal.values, color='#4DB8E8')
axes[1][0].hist(df['monto'].dropna(), bins=30, color='#F08A8A')
axes[1][1].hist(df['unidades'], bins=25, color='#4A4A4A')

for fila in axes:
    for a in fila:
        a.spines['top'].set_visible(False)
        a.spines['right'].set_visible(False)

fig.tight_layout()
fig.savefig(carpeta / 'panel4.png', dpi=120)
plt.close(fig)

print('panel de', axes.size, 'gráficos')
panel de 4 gráficos
7. Guardar en SVG

Guarda un gráfico en SVG y comprueba que el archivo es texto, no una imagen.

fig, ax = plt.subplots(figsize=(5, 2.5))
ax.barh(por_canal.index, por_canal.values, color='#F092C7')
fig.tight_layout()
ruta_svg = carpeta / 'prueba.svg'
fig.savefig(ruta_svg)
plt.close(fig)

print(ruta_svg.read_text()[:40])
<?xml version="1.0" encoding="utf-8" sta

Un SVG es texto XML que describe las formas, y por eso no se pixela por mucho que lo agrandes. Para la web es el formato correcto casi siempre.

8. Cerrar las figuras

Genera diez gráficos en un bucle cerrándolos, y comprueba que no queda ninguno abierto.

for i in range(10):
    f, a = plt.subplots(figsize=(3, 2))
    a.plot([1, 2, 3], [i, i + 1, i])
    f.savefig(carpeta / f'bucle-{i}.png')
    plt.close(f)

print('figuras abiertas:', len(plt.get_fignums()))
print('archivos:', len(list(carpeta.glob('ciudad-*.png'))))
figuras abiertas: 0
archivos: 0

Si quitas el plt.close(f), esa primera línea diría 10 y con cincuenta gráficos matplotlib te empieza a avisar. Cerrar es una línea y evita el problema entero.

9. El ticket promedio, con el número escrito

Grafica el monto promedio por categoría con el valor sobre cada barra.

por_cat_media = df.groupby('categoria')['monto'].mean().sort_values()

fig, ax = plt.subplots(figsize=(7, 3.5))
b = ax.barh(por_cat_media.index, por_cat_media.values, color='#4DB8E8')
ax.bar_label(b, fmt='S/ %.0f', padding=4)
ax.set_xlim(0, por_cat_media.max() * 1.15)
fig.tight_layout()
fig.savefig(carpeta / 'ticket-por-categoria.png', dpi=120)
plt.close(fig)

print(por_cat_media.round(2))
categoria
Cuidado personal    749.03
Limpieza            803.68
Bebidas             807.72
Snacks              820.42
Abarrotes           836.49
Name: monto, dtype: float64

Ochenta y siete soles separan la categoría de arriba de la de abajo, sobre tickets de ochocientos. O sea, un 11% 🤏

Y ahí viene la decisión honesta: con esa diferencia, el título no puede decir "Abarrotes es la categoría estrella". Puede decir "las cinco categorías tienen ticket parecido", que es lo que pasa de verdad.

10. Torta y barras, los mismos datos

Reparte las ventas por canal en porcentaje y dibújalo de las dos formas.

reparto_canal = (por_canal / por_canal.sum() * 100).sort_values(ascending=False)

fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].pie(reparto_canal.values, labels=reparto_canal.index, autopct='%1.0f%%')
axes[1].barh(reparto_canal.index[::-1], reparto_canal.values[::-1],
             color='#F092C7')
fig.tight_layout()
fig.savefig(carpeta / 'canales-dos-formas.png', dpi=120)
plt.close(fig)

print(reparto_canal.round(1))
print('el mayor y el menor se llevan',
      round((reparto_canal.iloc[0] - reparto_canal.iloc[-1]) / 100 * 360, 1),
      'grados')
canal
Web            26.2
Marketplace    25.7
Tienda         24.5
WhatsApp       23.5
Name: monto, dtype: float64
el mayor y el menor se llevan 9.7 grados

Aquí es todavía peor que con las ciudades: cuatro trozos casi idénticos. La torta dice "todos iguales" y es lo único que se puede leer de ella.

11. Los ajustes que te ahorran cuatro líneas por gráfico

Vuelve a los ajustes de fábrica, mira cómo vienen, y pon los tuyos.

plt.rcdefaults()

print('de fabrica:')
print('  marco arriba :', plt.rcParams['axes.spines.top'])
print('  tamano       :', plt.rcParams['figure.figsize'])
print('  titulo a la  :', plt.rcParams['axes.titlelocation'])

plt.rcParams.update({'axes.spines.top': False,
                     'figure.figsize': (9, 3.5),
                     'axes.titlelocation': 'left'})

print('con los mios:')
print('  marco arriba :', plt.rcParams['axes.spines.top'])
print('  tamano       :', plt.rcParams['figure.figsize'])
print('  titulo a la  :', plt.rcParams['axes.titlelocation'])
de fabrica:
  marco arriba : True
  tamano       : [6.4, 4.8]
  titulo a la  : center
con los mios:
  marco arriba : False
  tamano       : [9.0, 3.5]
  titulo a la  : left

Ese plt.rcdefaults() del principio es el botón de deshacer, y lo vas a querer el día que toquetees diez ajustes y ya no sepas cuál te dejó el gráfico raro 🔄

Fíjate en los de fábrica: el marco cerrado por los cuatro lados y el título centrado. Los dos son decisiones de matplotlib de hace veinte años, y los dos son lo primero que cambia cualquiera que publique gráficos hoy.

12. El error de los largos distintos

Intenta graficar seis ciudades contra cuatro canales.

fig, ax = plt.subplots()
ax.plot(por_ciudad.index, por_canal.values)
ValueError: x and y must have same first dimension, but have shapes (6,) and (4,)

El mensaje es bueno y por eso lo pongo: te dice las dos formas, (6,) y (4,), así que sabes exactamente cuál sobra 📏

Este error sale muchísimo cuando filtras uno de los dos lados y el otro no. Lo típico: quitas los nulos del monto y te olvidas de quitar las fechas correspondientes.

print('figuras abiertas tras el error:', len(plt.get_fignums()))
plt.close('all')
print('y despues de cerrar          :', len(plt.get_fignums()))
figuras abiertas tras el error: 1
y despues de cerrar          : 0

Ahí está la otra mitad de la lección 🧹

La figura que reventó se quedó abierta. Matplotlib la creó en el subplots(), y el error vino después, en el plot(). Nadie la cerró porque nunca llegamos a la línea que la cerraba.

Por eso plt.close('all') es lo que uso al final de cada cuaderno: recoge también las que quedaron a medias.

El gráfico correcto que engaña a toda la reunión

Antes de cerrar el capítulo, la trampa de los gráficos. Y esta es distinta a todas las anteriores, porque el código no tiene nada malo 📊

La trampa

Haces el gráfico de ventas por ciudad para la reunión. matplotlib elige los ejes solo y el resultado se ve clarísimo.

plt.bar(ciudades, ventas)
plt.ylim(480, 520)
plt.show()

# Lima 512, Cusco 495
# la barra de Lima se ve
# el triple de alta
Qué está mal

El eje no empieza en cero, así que una diferencia del 3% se ve como si fuera del 200% 📊 Nadie mintió: los números están bien y el gráfico es correcto. Lo que engaña es la forma, que es lo que la gente lee.

Y no hace falta mala fe. matplotlib ajusta los ejes al rango de los datos para que se aprecien las diferencias, así que este gráfico te sale solo si no miras.

En un gráfico de barras el eje empieza en cero, sin excepciones, porque la barra se compara por su longitud. Si la diferencia real es tan pequeña que no se ve, la respuesta no es estirar el eje: es que la diferencia no importa, y eso también es un hallazgo.

Comprueba que lo tienes

Quieres enseñar cómo se reparten los montos de 3.000 ventas. ¿Qué gráfico?

  • Un histograma, que enseña la forma entera
  • Un gráfico de barras con el promedio
  • Un gráfico de líneas ordenado por monto
  • Un gráfico de torta con tramos

Lo que te llevas

Que un gráfico bueno no es el más bonito, es el que se entiende sin que estés al lado explicándolo 💜

Y que las tres decisiones que lo deciden (el tipo, el orden y el título) se toman antes de escribir código.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

En un gráfico de barras el eje empieza en cero. Sin excepciones.

En el capítulo 16 juntamos todo el libro en un solo proyecto: del archivo sucio a una conclusión con su gráfico, de punta a punta.

Que tengas un hermoso día! 🌟

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?