Saltar al contenido

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».

Las dos piezas, y qué se le pide a cada una
PiezaQué esQué se le pide
FigureEl lienzo completo. Puede contener uno o varios ejes.El título general, el reparto del espacio, guardar el archivo.
AxesUn á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

EjemploFigure y Axes: el lienzo y el area donde se dibuja
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"])
las ventas suben de 82 a 117 entre enero y junio, con una sola caida en abril
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.

Los cuatro rótulos no son decoración

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

EjemploLa misma anatomia sobre un archivo real: un sensor, 24 horas
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))
la temperatura del deposito baja hasta las 3 de la manana y sube hasta un maximo a las 16
         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

EjemploUna figura, dos ejes: dos preguntas que no comparten unidad
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.")
a la izquierda la temperatura por hora del sensor S01 en grados, a la derecha cuantas lecturas validas trajo cada uno de los diez sensores
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.
Dos gráficos en la misma figura no comparten 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

no se abre ninguna ventana y no hay error

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.

el título quedó en el gráfico equivocado

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.

ValueError: x and y must have same first dimension

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.

las etiquetas del eje x se pisan entre sí

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.

la segunda figura sale encima de la primera

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

Lo mínimo para una figura defendible
ExpresiónQué haceCuándo
fig, ax = plt.subplots()Una figura con un ejeSiempre que sea un gráfico solo
plt.subplots(1, 2, figsize=(7.2, 3.4))axes es un arregloDos preguntas en una lámina
plt.subplots(2, 2)axes[fila, columna]Panel; ver la lección 8.8
ax.plot(x, y)LíneaOrden continuo
ax.bar(x, y)BarrasCategorías
ax.hist(v, bins=10)HistogramaUna variable continua
ax.scatter(x, y)DispersiónDos variables continuas
ax.set_title(...)Título de ese ejeUno por gráfico
ax.set_xlabel(...), ax.set_ylabel(...)Rótulos con unidadSiempre
ax.set(title=..., xlabel=..., ylabel=...)Los tres de una vezCuando son cuatro ejes
ax.legend()LeyendaSolo si hay label=
ax.grid(axis="y", alpha=0.3)Grilla tenueCuando ayuda a leer valores
fig.suptitle(...)Título de la figura enteraCon varios ejes
fig.tight_layout()Acomoda márgenesÚltimo, después de los rótulos
plt.show()MuestraAl final, una sola vez

Ponete a prueba

1. ¿Qué devuelve fig, ax = plt.subplots()?Nivel 1 · Recordar
2. ¿Cuál es la diferencia entre ax.set_title() y fig.suptitle()?Nivel 1 · Recordar
3. Un script hace plt.subplots() dos veces, dibuja en cada eje y después llama a plt.title("Ventas"). ¿Dónde aparece el título?Nivel 2 · Predecir la salida
4. Una figura se arma con fig, axes = plt.subplots(1, 2). ¿Cómo se le pone título al gráfico de la derecha?Nivel 2 · Predecir la salida
5. Una figura tiene dos gráficos lado a lado: temperatura en grados y cantidad de lecturas. Las dos barras más altas llegan casi a la misma altura en pantalla. ¿Qué se puede concluir?Nivel 3 · Elegir entre dos casi correctas
6. ¿Por qué en esta unidad todo ejemplo que dibuja también imprime los números que grafica?Nivel 3 · Elegir entre dos casi correctas
7. Este código corre sin error y el gráfico sale con la línea de la media móvil desplazada tres días. ¿Cuál es el defecto?
movil = np.convolve(df["kwh"], np.ones(4) / 4, mode="same")
ax.plot(df["dia"], df["kwh"])
ax.plot(df["dia"], movil)
Nivel 4 · Encontrar el defecto

Prácticas de esta lección