Saltar al contenido

Indice / Visualizacion con Seaborn / 9.6

La imagen que sale del script

El gráfico se va a ver sin vos al lado. Esta lección es sobre la mitad que casi nunca se enseña: qué archivo producir, con qué resolución, y qué tiene que estar adentro de la imagen para que se entienda sola.

Para qué sirve

La lección 8.8 ya cerró la unidad de Matplotlib guardando un panel. Acá se retoma con lo que agregan Seaborn y el trabajo real: una figura que va a un informe, a una presentación o a un repositorio, y que alguien va a abrir dentro de seis meses sin poder preguntarte nada.

Son tres decisiones: qué formato, con qué resolución y qué contiene la figura. Las tres se toman antes de escribir savefig.

El ejemplo mínimo

EjemploLa figura que se guarda para un informe
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

ventas = pd.DataFrame({
    "mes": ["Ene", "Feb", "Mar", "Abr", "May", "Jun"],
    "ventas": [82, 88, 95, 93, 105, 117]})

print("ventas mes a mes:")
print(ventas.to_string(index=False))
variacion = ventas["ventas"].diff()
print("\nvariacion contra el mes anterior:")
for mes, v in zip(ventas["mes"][1:], variacion[1:]):
    print("  %s %+.0f" % (mes, v))
print("\nunico retroceso:", ventas.loc[variacion.idxmin(), "mes"])

sns.set_theme(style="whitegrid")
fig, ax = plt.subplots()
sns.lineplot(data=ventas, x="mes", y="ventas", marker="o",
             errorbar=None, ax=ax)
ax.set(title="Ventas mensuales del primer semestre",
       xlabel="Mes", ylabel="Ventas (unidades)")
fig.tight_layout()
# tight_layout acomoda los margenes ANTES de guardar, y a diferencia del
# recorte automatico al contenido no cambia el tamano del lienzo: el
# archivo mide lo mismo en cualquier maquina. La leccion lo explica.
fig.savefig("informe_ventas.png", dpi=300)
plt.show()
linea de ventas mensuales con marcadores que sube de enero a junio con un retroceso en abril
ventas mes a mes:
mes  ventas
Ene      82
Feb      88
Mar      95
Abr      93
May     105
Jun     117

variacion contra el mes anterior:
  Feb +6
  Mar +7
  Abr -2
  May +12
  Jun +12

unico retroceso: Abr

sns.lineplot con errorbar=None es el caso más simple: una serie, un punto por mes. El errorbar=None conviene explicarlo, porque es una diferencia real con Matplotlib.

Seaborn agrega cosas que Matplotlib no agrega

Si hay más de un valor de y para el mismo x, lineplot no dibuja todos los puntos: calcula la media y le pone alrededor una banda de confianza estimada por bootstrap. Es útil y es una decisión estadística que tomó la biblioteca por vos.

errorbar=None apaga la banda. Y si no esperabas una media, el problema no es la banda: es que tu tabla tiene un grano distinto del que creías. Comprobalo con df.groupby("x").size().max() antes de graficar.

Acá hay que elegir: el formato

PNG, PDF o SVG
  • PNG es una grilla de píxeles. Va bien en Word, en PowerPoint y en la web, y se ve pixelado si alguien lo agranda. Para impresión, 300 dpi.
  • PDF es vectorial: se agranda sin perder nitidez y conserva el texto como texto. Es lo que corresponde en un informe académico o una publicación.
  • SVG también es vectorial y además es editable: se abre en Inkscape o Illustrator para retocar una etiqueta sin volver a correr el script. Es el formato para la web moderna y para cuando un diseñador va a meter mano.

Con un vectorial y decenas de miles de puntos, el archivo puede quedar enorme porque guarda cada punto. Ahí el PNG a 300 dpi suele ser mejor idea.

EjemploQue tamano de archivo produce cada combinacion de figsize y dpi
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

ANCHO_PULGADAS = 7.2
ALTO_PULGADAS = 4.0

tabla = pd.DataFrame({"dpi": [72, 100, 150, 300]})
tabla["ancho_px"] = (tabla["dpi"] * ANCHO_PULGADAS).astype(int)
tabla["alto_px"] = (tabla["dpi"] * ALTO_PULGADAS).astype(int)
tabla["destino"] = ["Borrador en pantalla", "Pantalla y web",
                    "Presentacion proyectada", "Informe impreso"]

print("lienzo de %.1f por %.1f pulgadas" % (ANCHO_PULGADAS, ALTO_PULGADAS))
print(tabla.to_string(index=False))
print("\nEl dpi NO cambia el tamano del texto respecto del grafico:")
print("una figura a 300 dpi tiene las mismas proporciones que a 100,")
print("con cuatro veces mas pixeles. Para agrandar la letra se cambia")
print("font.size o figsize, no el dpi.")

sns.set_theme(style="whitegrid")
fig, ax = plt.subplots()
sns.barplot(data=tabla, x="dpi", y="ancho_px", hue="dpi",
            palette="viridis", legend=False, ax=ax)
ax.bar_label(ax.containers[0], fmt="%d px", padding=2, fontsize=9)
ax.set(title="Pixeles de ancho segun el dpi, con figsize fijo",
       xlabel="dpi", ylabel="Ancho en pixeles")
ax.margins(y=0.15)
fig.tight_layout()
plt.show()
barras con los pixeles de ancho resultantes para 100, 150 y 300 dpi sobre un lienzo de 7,2 pulgadas
lienzo de 7.2 por 4.0 pulgadas
 dpi  ancho_px  alto_px                 destino
  72       518      288    Borrador en pantalla
 100       720      400          Pantalla y web
 150      1080      600 Presentacion proyectada
 300      2160     1200         Informe impreso

El dpi NO cambia el tamano del texto respecto del grafico:
una figura a 300 dpi tiene las mismas proporciones que a 100,
con cuatro veces mas pixeles. Para agrandar la letra se cambia
font.size o figsize, no el dpi.

El dpi multiplica el lienzo: 7,2 pulgadas a 300 dpi son 2160 píxeles de ancho. Lo que no hace es agrandar la letra respecto del gráfico: las proporciones son idénticas a 100 y a 300 dpi, solo que con cuatro veces más píxeles. Si el texto se ve chico, lo que hay que cambiar es font.size o figsize, no el dpi.

El caso bbox_inches="tight"

La presentación de la cátedra guarda así:

plt.savefig("informe_notas.png", dpi=300, bbox_inches="tight", facecolor="white")

Vas a ver esa línea en el material y en medio internet, así que conviene saber exactamente qué hace. bbox_inches="tight" recorta el lienzo hasta el contenido: mide dónde termina el texto más externo y corta ahí. Resuelve de un saque el clásico «se me cortó la etiqueta del eje».

Por qué en este curso no se usa

Recorta según el texto medido. El ancho del archivo pasa a depender de la tipografía instalada, de su versión y del motor de fuentes del sistema. El mismo script produce un PNG de 2160 por 1200 en tu máquina y de 2143 por 1187 en la de al lado, y el sitio no puede prometer que la figura publicada sea la que vos generás.

Por eso verificar_sitio.py lo rechaza, junto con plt.style.use, datetime.now y default_rng() sin semilla. No es una opinión sobre el recorte: es que el tamaño del archivo deje de ser determinístico.

Cuándo sí usarlo: en una entrega de una vez, donde el archivo se genera, se mira y se manda. Ahí el recorte automático es cómodo y el determinismo no importa. Cuándo no: en cualquier proceso que se corra más de una vez y cuya salida se compare, se versione o se publique.

La alternativa determinística es la que usan todos los ejemplos de esta unidad: fig.tight_layout() antes de guardar. Acomoda los márgenes dentro del lienzo sin cambiar su tamaño.

Lo que tiene que estar adentro de la imagen

EjemploEl checklist de cierre aplicado: el mismo dato, antes y despues
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

entregas = pd.DataFrame({
    "zona": ["Norte", "Sur", "Centro", "Oeste"],
    "demora": [22.4, 41.8, 19.1, 25.6]})

print("demora promedio por zona, en minutos:")
print(entregas.to_string(index=False))
peor = entregas.loc[entregas["demora"].idxmax()]
print("\npeor zona:", peor["zona"], "con", peor["demora"], "minutos")
print("diferencia contra la mejor: %.1f minutos"
      % (entregas["demora"].max() - entregas["demora"].min()))
print("\nLos numeros son identicos en los dos paneles. Lo que cambia es")
print("cuanto trabajo tiene que hacer quien mira para entenderlos.")

sns.set_theme(style="whitegrid")
fig, axes = plt.subplots(2, 1, figsize=(7.2, 5.2))

sns.barplot(data=entregas, x="zona", y="demora", hue="zona",
            palette="Set2", legend=False, ax=axes[0])
axes[0].set_xlabel("")
axes[0].set_ylabel("")

# Gris para todo menos la zona que el grafico viene a senalar.
destacado = ["#c9ccd1" if z != peor["zona"] else "#d7263d"
             for z in entregas["zona"]]
sns.barplot(data=entregas, x="zona", y="demora", hue="zona",
            palette=destacado, legend=False, ax=axes[1])
axes[1].bar_label(axes[1].containers[0], fmt="%.1f", padding=2, fontsize=9)
axes[1].set(title="El Sur demora el doble que el Centro (agosto 2026)",
            xlabel="Zona de reparto", ylabel="Demora promedio (minutos)")
axes[1].margins(y=0.15)

fig.tight_layout()
plt.show()
dos paneles con los mismos cuatro valores, arriba sin titulo ni rotulos y con colores arbitrarios, abajo con titulo que concluye, ejes rotulados con unidad y una sola serie destacada
demora promedio por zona, en minutos:
  zona  demora
 Norte    22.4
   Sur    41.8
Centro    19.1
 Oeste    25.6

peor zona: Sur con 41.8 minutos
diferencia contra la mejor: 22.7 minutos

Los numeros son identicos en los dos paneles. Lo que cambia es
cuanto trabajo tiene que hacer quien mira para entenderlos.

Los dos paneles tienen los mismos cuatro números. El de arriba obliga a preguntar qué mide el eje, en qué unidad, de cuándo son los datos y qué habría que mirar primero. El de abajo contesta esas cuatro preguntas sin que nadie las haga.

Fijate en el color: todas las barras en gris menos una. El gráfico vino a decir que el Sur demora el doble que el Centro, así que el único color fuerte es el del Sur. Es la regla de la lección anterior aplicada al cierre: reservar la intensidad para lo importante.

El checklist antes de entregar
  • ¿El gráfico responde una pregunta concreta?
  • ¿El título dice qué, quién y cuándo? Mejor todavía: ¿dice la conclusión?
  • ¿Los ejes tienen rótulo y unidad?
  • ¿La leyenda hace falta, o el color ya está explicado en el título?
  • ¿Los colores son accesibles, consistentes con el resto del informe, y funcionan en escala de grises?
  • ¿La interpretación evita confundir correlación con causalidad?
  • ¿El formato y la resolución corresponden al destino?
  • ¿Se declaró qué filas quedaron afuera y por qué?

El último punto no estaba en la diapositiva de la cátedra y es el que más veces salvó un análisis en esta unidad: en 9.1 eran nueve notas imposibles, en 9.2 una venta de siete dígitos, en 9.3 seis importes ilegibles y en 9.4 veinticinco ventas huérfanas.

Dónde se rompe

(sin error) el archivo guardado sale en blanco

Causa: savefig después de show. En algunos backends show libera la figura y lo que se guarda es un lienzo vacío.

Arreglo: guardar siempre antes de mostrar. En un script que corre solo, show directamente no hace falta.

(sin error) la imagen tiene fondo transparente y en el informe se ve negra

Causa: el PNG se guardó con fondo transparente y se pegó sobre una diapositiva oscura.

Arreglo: facecolor="white" al guardar, o dejar el fondo por defecto de la figura, que ya es blanco.

(sin error) el tamaño del PNG cambia entre dos máquinas

Causa: bbox_inches="tight". El recorte depende del texto medido y por lo tanto de la tipografía instalada.

Arreglo: figsize explícito más fig.tight_layout() antes de guardar.

(sin error) el texto se ve diminuto en la imagen exportada

Causa: subiste el dpi esperando agrandar la letra. El dpi multiplica todo por igual.

Arreglo: agrandá font.size o achicá figsize. La letra se mide en puntos respecto del lienzo, no en píxeles.

(sin error) lineplot dibuja una banda que no pediste

Causa: hay más de un valor de y por cada x, y Seaborn promedia y estima un intervalo por bootstrap.

Arreglo: errorbar=None si querés solo la media. Y revisá el grano de la tabla con df.groupby("x").size().max(): puede que estuvieras graficando otra cosa de la que creías.

(sin error) el PDF pesa 40 MB

Causa: es vectorial y guardó cada uno de los cien mil puntos de la dispersión.

Arreglo: PNG a 300 dpi para nubes densas, o reducí los puntos con una muestra o un hexbin.

Recetario

Exportar una figura
ExpresiónQué haceCuándo
fig.tight_layout()Acomoda márgenes dentro del lienzoSiempre, antes de guardar
fig.savefig("g.png", dpi=300)PNG de alta resoluciónInforme impreso
fig.savefig("g.png", dpi=150)PNG intermedioPresentación proyectada
fig.savefig("g.pdf")Vectorial, texto como textoInforme académico
fig.savefig("g.svg")Vectorial y editableWeb, o retoque posterior
facecolor="white"Fondo blanco explícitoSi el destino es oscuro
bbox_inches="tight"Recorta al contenido medidoEntrega de una vez; nunca en un pipeline
plt.subplots(figsize=(7.2, 4.0))Lienzo explícitoSiempre, para que el tamaño sea tuyo
sns.lineplot(..., errorbar=None)Sin banda de confianzaUna sola observación por x
marker="o"Marca cada observaciónSeries cortas
ax.bar_label(ax.containers[0], fmt="%.1f")Valor sobre cada barraCuando el número exacto importa
ax.margins(y=0.15)Aire arriba para las etiquetasJunto con bar_label
df.groupby("x").size().max()Cuántas filas hay por valor de xAntes de un lineplot

Ponete a prueba

1. ¿Cuál es la diferencia principal entre guardar en PNG y en PDF?Nivel 1 · Recordar
2. Guardás a 300 dpi esperando que la letra se vea más grande, y se ve igual de chica pero con más píxeles. ¿Por qué?Nivel 2 · Predecir la salida
3. Llamás a sns.lineplot y aparece una banda sombreada alrededor de la línea que no pediste. ¿Qué significa?Nivel 2 · Predecir la salida
4. ¿Qué hace exactamente bbox_inches="tight" y por qué este curso no lo usa?Nivel 3 · Elegir entre dos casi correctas
5. ¿Cuál es la alternativa determinística a bbox_inches="tight"?Nivel 3 · Elegir entre dos casi correctas
6. Guardás la figura y el archivo sale en blanco. ¿Cuál es la causa más probable?Nivel 4 · Encontrar el defecto
7. De todo el checklist de cierre, ¿cuál es el punto que más veces salvó un análisis en esta unidad?Nivel 4 · Encontrar el defecto

Prácticas de esta lección