# Gráficos que se entienden

URL canónica: https://missyera.com/guias/python-desde-cero/graficos/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> 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.

Capítulo 15 de 17 del libro [Python desde cero](https://missyera.com/guias/python-desde-cero.md).

## 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))
```

```text
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')
```

```text
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}%')
```

```text
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), '%')
```

```text
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.](https://missyera.com/static/diagramas/webp/g-py-galeria.webp)

*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')
```

```text
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))
```

```text
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')
```

```text
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), '%')
```

```text
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))
```

```text
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](https://missyera.com/guias/estadistica-desde-cero/): 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'))))
```

```text
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

| Formato | Cuándo |
|---|---|
| `.png` con `dpi=150` | Presentaciones, correo, informes |
| `.svg` | Web y documentos que se van a imprimir: no pierde nitidez |
| `.pdf` | Cuando 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')
```

```text
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])
```

```text
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')
```

```text
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')
```

```text
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'])
```

```text
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**

Gráfica 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))
```

```text
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}')
```

```text
Abarrotes vende 18% más que Cuidado personal
```

**3. Histograma de unidades**

Gráfica 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())
```

```text
mediana: 12.0
máximo : 30
```

**4. El eje que engaña**

Gráfica 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')
```

```text
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**

Gráfica 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))
```

```text
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')
```

```text
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])
```

```text
<?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'))))
```

```text
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**

Gráfica 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))
```

```text
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')
```

```text
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'])
```

```text
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()))
```

```text
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! 🌟

## Seguir leyendo

- Anterior: [Cuando los datos no vienen en un archivo](https://missyera.com/guias/python-desde-cero/traer-datos-de-fuera.md)
- Siguiente: [De datos sucios a una conclusión](https://missyera.com/guias/python-desde-cero/proyecto-final.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/python-desde-cero/graficos/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
