Indice / Visualizacion con Matplotlib / 8.1
Anatomía de una figura
Matplotlib tiene dos formas de dibujar lo mismo y solo una de las dos escala. Esta lección es sobre las dos piezas que hay que tener en la cabeza: la figura y el eje.
Para qué sirve
Hasta acá el resultado de todo lo que hiciste fue una tabla. Una tabla es exacta y es imposible de mirar: nadie lee 500 filas y ve que hay una tendencia. Un gráfico no agrega información, agrega la posibilidad de ver la que ya está.
El precio es que un gráfico también puede mentir sin que se note, y ahí es donde empieza el trabajo real de esta unidad. Antes de eso hay que poder dibujar uno y saber qué parte es cuál, porque casi todos los errores de más adelante son «le pedí esto al objeto equivocado».
| Pieza | Qué es | Qué se le pide |
|---|---|---|
Figure | El lienzo completo. Puede contener uno o varios ejes. | El título general, el reparto del espacio, guardar el archivo. |
Axes | Un área de dibujo con sus dos escalas. No es «un eje x»: es el par completo. | Los datos, el título de ese gráfico, los rótulos, la leyenda, la grilla. |
ELI5Explicado facil
Pensá en una hoja de papel y en los recuadros que dibujás encima.
- La hoja es la figura. Es una sola, tiene un tamaño y es lo que después imprimís o mandás por mail.
- Cada recuadro es un
Axes. Adentro de un recuadro dibujás una cosa, con sus números al costado y abajo.
Podés poner un recuadro grande o cuatro chiquitos. Lo que no podés es dibujar «en la hoja» sin decir en qué recuadro: siempre estás dibujando adentro de uno.
La idea: «el gráfico actual» es una variable escondida
Vas a ver por todos lados código así:
plt.plot(x, y)
plt.title("Ventas")
plt.show()
Funciona, y es la razón por la que tanta gente se traba después. plt.title
no le pone el título a tu gráfico: se lo pone al gráfico actual,
que es una variable global que Matplotlib mantiene por vos. Mientras hay uno solo, no
se nota la diferencia. En cuanto hay dos, el título aparece en el que no era, y no hay
mensaje de error que te lo diga.
La alternativa es pedir las dos piezas por su nombre y no depender nunca de cuál es «la actual»:
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("Ventas")
Es una línea más larga y es la única que sigue funcionando cuando la figura tiene cuatro gráficos. En este curso siempre se usa esta.
El ejemplo mínimo
import pandas as pd
import matplotlib.pyplot as plt
ventas = pd.DataFrame({"mes": ["Ene", "Feb", "Mar", "Abr", "May", "Jun"],
"ventas": [82, 88, 95, 93, 105, 117]})
# subplots() devuelve las dos piezas de una vez: la figura, que es el lienzo
# completo, y el eje, que es el area donde se dibuja. Casi todo se le pide al
# eje; a la figura solo se le pide lo que afecta al conjunto.
fig, ax = plt.subplots()
ax.plot(ventas["mes"], ventas["ventas"], marker="o")
ax.set_title("Ventas mensuales del primer semestre")
ax.set_xlabel("Mes")
ax.set_ylabel("Ventas (miles de pesos)")
fig.tight_layout()
plt.show()
print(ventas.to_string(index=False))
print()
cambio = ventas["ventas"].diff()
print("variacion mes a mes:", [int(v) for v in cambio.dropna()])
print("unico mes con retroceso:",
ventas.loc[cambio.idxmin() if cambio.min() < 0 else 0, "mes"])
mes ventas
Ene 82
Feb 88
Mar 95
Abr 93
May 105
Jun 117
variacion mes a mes: [6, 7, -2, 12, 12]
unico mes con retroceso: Abr
Fijate en dos detalles que van a repetirse en toda la unidad. Primero: el ejemplo
imprime los números que dibuja. La imagen es un resumen; los números
son la prueba. Segundo: fig.tight_layout() va al final, después de poner
todos los rótulos, porque lo que hace es acomodar los márgenes al texto que ya
existe.
Título, nombre del eje horizontal, nombre del eje vertical y unidad. Un gráfico sin unidad no se puede discutir: «117» puede ser pesos, miles de pesos o cantidad de cajas, y la conclusión cambia con cada uno. En una entrega, la unidad faltante es el error más frecuente y el más barato de evitar.
Sobre datos de verdad
Una fila del archivo, veinticuatro puntos del gráfico
import pandas as pd
import matplotlib.pyplot as plt
sensores = pd.read_csv("numpy_sensores.csv")
horas = [c for c in sensores.columns if c.startswith("h")]
# Una fila del archivo son 24 columnas. Para graficar hace falta lo contrario:
# 24 filas, una por hora. Eso es lo que hace el transpuesto de la fila.
deposito = sensores.loc[sensores["sensor_id"] == "S01", horas].iloc[0]
etiquetas = [int(h[1:]) for h in horas]
fig, ax = plt.subplots()
ax.plot(etiquetas, deposito.to_numpy(), marker="o", markersize=4)
ax.set_title("Temperatura del deposito, sensor S01")
ax.set_xlabel("Hora del dia")
ax.set_ylabel("Grados Celsius")
ax.set_xticks(range(0, 24, 3))
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
plt.show()
print(deposito.to_frame(name="grados").T.to_string())
print()
print("minimo:", deposito.min(), "a las", deposito.idxmin())
print("maximo:", deposito.max(), "a las", deposito.idxmax())
print("amplitud del dia:", round(deposito.max() - deposito.min(), 1))
h00 h01 h02 h03 h04 h05 h06 h07 h08 h09 h10 h11 h12 h13 h14 h15 h16 h17 h18 h19 h20 h21 h22 h23
grados 20.8 19.8 19.2 19.0 19.6 20.3 20.0 21.0 21.1 21.7 21.9 22.9 22.6 23.7 23.9 24.3 24.4 23.3 23.2 22.3 22.4 21.9 21.2 21.1
minimo: 19.0 a las h03
maximo: 24.4 a las h16
amplitud del dia: 5.4
El archivo tiene una fila por sensor y una columna por hora. El gráfico necesita lo contrario: una posición por hora. Casi siempre hay un paso de forma antes del paso de dibujo, y es donde se cae la mitad de los intentos: el error dice algo sobre dimensiones y parece un problema de Matplotlib cuando es un problema de tabla.
Dos ejes en la misma figura
import pandas as pd
import matplotlib.pyplot as plt
sensores = pd.read_csv("numpy_sensores.csv")
horas = [c for c in sensores.columns if c.startswith("h")]
deposito = sensores.loc[sensores["sensor_id"] == "S01", horas].iloc[0]
completas = sensores[horas].notna().sum(axis=1)
# Un solo lienzo, dos areas de dibujo. axes es un arreglo: axes[0] y axes[1].
fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.4))
axes[0].plot([int(h[1:]) for h in horas], deposito.to_numpy())
axes[0].set_title("Temperatura de S01")
axes[0].set_xlabel("Hora")
axes[0].set_ylabel("Grados Celsius")
axes[1].bar(sensores["sensor_id"], completas)
axes[1].set_title("Lecturas validas por sensor")
axes[1].set_xlabel("Sensor")
axes[1].set_ylabel("Horas con dato")
axes[1].tick_params(axis="x", labelrotation=90)
fig.suptitle("Dos preguntas distintas sobre el mismo archivo")
fig.tight_layout()
plt.show()
print("grados de S01: de", deposito.min(), "a", deposito.max())
print()
print(pd.DataFrame({"sensor": sensores["sensor_id"],
"lecturas": completas}).to_string(index=False))
print()
print("Las alturas de los dos ejes NO se comparan: uno mide grados y el otro,")
print("cantidad de lecturas. Comparten la figura, no la escala.")
grados de S01: de 19.0 a 24.4
sensor lecturas
S01 24
S02 24
S03 22
S04 24
S05 24
S06 23
S07 24
S08 24
S09 23
S10 24
Las alturas de los dos ejes NO se comparan: uno mide grados y el otro,
cantidad de lecturas. Comparten la figura, no la escala.
Están al lado y tienen la misma altura en pantalla, así que el ojo los compara igual. Pero el de la izquierda llega hasta 24 grados y el de la derecha hasta 24 lecturas: la barra más alta y el punto más alto no significan nada parecido.
Cuando las dos cosas sí son comparables, pedile a Matplotlib que use la
misma escala con sharey=True. Cuando no lo son, el título de cada eje
tiene que dejar claro que no lo son.
Dónde se rompe
Causa: falta plt.show(), o el script corre en un entorno sin
pantalla. La figura se construyó igual: existe en memoria, nadie la mostró.
Arreglo: plt.show() al final. Si es un script que corre solo,
fig.savefig("archivo.png"), que es lo que se ve en la lección 8.8.
Causa: mezcla de las dos interfaces. plt.title() apunta al
último eje que se tocó, no al que vos tenías en mente.
Arreglo: axes[0].set_title(...). Nombrar el eje elimina la
clase entera de error.
Causa: las dos listas no miden lo mismo. Es clásico después de una media móvil, que devuelve menos puntos que la serie original.
Arreglo: imprimí len(x) y len(y) antes de
dibujar. El problema es de datos, no de gráfico.
Causa: hay más categorías que lugar. Matplotlib no las achica ni las saltea solo.
Arreglo: ax.tick_params(axis="x", labelrotation=90), o menos
marcas con ax.set_xticks(...), o barras horizontales.
Causa: se reutilizó el mismo ax sin limpiarlo, o se llamó a
plt.plot dos veces sin crear una figura nueva.
Arreglo: una llamada a plt.subplots() por figura. Si querés
dos series en el mismo eje, eso sí es ax.plot(...) dos veces, y va con
label= y ax.legend().
Recetario
| Expresión | Qué hace | Cuándo |
|---|---|---|
fig, ax = plt.subplots() | Una figura con un eje | Siempre que sea un gráfico solo |
plt.subplots(1, 2, figsize=(7.2, 3.4)) | axes es un arreglo | Dos preguntas en una lámina |
plt.subplots(2, 2) | axes[fila, columna] | Panel; ver la lección 8.8 |
ax.plot(x, y) | Línea | Orden continuo |
ax.bar(x, y) | Barras | Categorías |
ax.hist(v, bins=10) | Histograma | Una variable continua |
ax.scatter(x, y) | Dispersión | Dos variables continuas |
ax.set_title(...) | Título de ese eje | Uno por gráfico |
ax.set_xlabel(...), ax.set_ylabel(...) | Rótulos con unidad | Siempre |
ax.set(title=..., xlabel=..., ylabel=...) | Los tres de una vez | Cuando son cuatro ejes |
ax.legend() | Leyenda | Solo si hay label= |
ax.grid(axis="y", alpha=0.3) | Grilla tenue | Cuando ayuda a leer valores |
fig.suptitle(...) | Título de la figura entera | Con varios ejes |
fig.tight_layout() | Acomoda márgenes | Último, después de los rótulos |
plt.show() | Muestra | Al final, una sola vez |