Saltar al contenido

Indice / Visualizacion con Seaborn / 9.4

Varias variables a la vez

Dos variables entran en una dispersión. Tres o cuatro también, si cada una usa un canal visual distinto. Diez no entran en ningún gráfico, y conviene saber dónde está el límite antes de cruzarlo.

Para qué sirve

Un análisis real empieza con una tabla de ocho o doce columnas y la pregunta «¿qué hay acá?». El mapa de calor de la lección anterior contesta parte: qué se mueve junto con qué, en un número por par. Lo que no contesta es cómo: si la relación es recta o curva, si hay grupos separados, si un par de puntos sostiene todo.

El pairplot dibuja todas las dispersiones de a pares en una sola figura y pone la distribución de cada variable en la diagonal. Es la versión visual de la matriz de correlación.

El ejemplo mínimo

EjemploPairplot: todas las dispersiones de una vez, media matriz alcanza
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

estudio = pd.DataFrame({
    "horas": [2, 3, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12],
    "nota": [52, 55, 60, 62, 68, 70, 74, 78, 83, 85, 89, 92],
    "proyectos": [1, 1, 2, 2, 3, 2, 3, 4, 3, 4, 5, 5],
    "curso": ["Datos", "Software", "IA"] * 4})

variables = ["horas", "nota", "proyectos"]
print("correlaciones equivalentes a lo que dibuja la matriz:")
print(estudio[variables].corr().round(3).to_string())
print("\nobservaciones por curso:")
print(estudio["curso"].value_counts().sort_index().to_string())

g = sns.pairplot(estudio, vars=variables, hue="curso",
                 corner=True, diag_kind="hist", height=2.0)
# pairplot es figure-level: crea su figura y no acepta ax=. El ancho se fija
# despues, sobre g.figure, para que todas las paginas midan lo mismo.
g.figure.set_size_inches(7.2, 5.6)
g.figure.suptitle("Relaciones por pares, por curso", y=1.0)
g.figure.tight_layout()
plt.show()
matriz triangular de tres por tres con dispersiones por pares e histogramas en la diagonal, coloreada en tres cursos
correlaciones equivalentes a lo que dibuja la matriz:
           horas   nota  proyectos
horas      1.000  0.992      0.940
nota       0.992  1.000      0.942
proyectos  0.940  0.942      1.000

observaciones por curso:
curso
Datos       4
IA          4
Software    4

Tres cosas del ejemplo merecen nombre propio:

pairplot es figure-level, y eso cambia cómo se lo trata

No acepta ax= y no devuelve Axes: crea su propia figura y devuelve un PairGrid. Por eso el título se pone con g.figure.suptitle(...) y no con ax.set_title(...).

Y por eso también hay que fijarle el tamaño a mano: height controla cada celda, no el total. El ejemplo cierra con g.figure.set_size_inches(7.2, 5.6), que es lo que mantiene todas las figuras del curso en el mismo ancho.

Sobre datos de verdad

EjemploEl mismo pairplot sobre el data warehouse, despues de limpiar
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

hechos = pd.read_csv("dw_hecho_ventas.csv")
productos = pd.read_csv("dw_dim_producto.csv")

# Mismo criterio que en 9.2: el dominio del ingreso llega a 100000.
valido = hechos["ingreso"].between(0, 100000) & hechos["cantidad"].notna()
print("filas descartadas por dominio o ausencia:", int((~valido).sum()))

ventas = (hechos[valido]
          .merge(productos[["producto_id", "categoria"]],
                 on="producto_id", how="left"))
print("ventas sin categoria:", int(ventas["categoria"].isna().sum()))

ventas = ventas.dropna(subset=["categoria", "costo"])
print("filas que entran al grafico:", len(ventas))
print("\nobservaciones por categoria:")
print(ventas["categoria"].value_counts().sort_index().to_string())

variables = ["cantidad", "ingreso", "costo"]
print("\ncorrelaciones sobre lo que queda:")
print(ventas[variables].corr().round(3).to_string())

g = sns.pairplot(ventas, vars=variables, hue="categoria",
                 corner=True, diag_kind="hist", height=2.0,
                 plot_kws={"alpha": 0.45, "s": 18})
g.figure.set_size_inches(7.2, 5.6)
g.figure.suptitle("Medidas de venta por categoria de producto", y=1.0)
g.figure.tight_layout()
plt.show()
matriz triangular de tres por tres sobre cantidad, ingreso y costo coloreada por categoria de producto, con las nubes de ingreso y costo alineadas
filas descartadas por dominio o ausencia: 3
ventas sin categoria: 25
filas que entran al grafico: 470

observaciones por categoria:
categoria
Almacenamiento    138
Audio              17
Equipos           108
Pantallas          57
Perifericos       121
Redes              29

correlaciones sobre lo que queda:
          cantidad  ingreso  costo
cantidad     1.000    0.645  0.617
ingreso      0.645    1.000  0.969
costo        0.617    0.969  1.000

Acá se ve por qué la limpieza va antes del gráfico y no después. Tres filas salen por dominio, veinticinco ventas apuntan a un producto que no existe en la dimensión, y las 470 que quedan muestran lo que la lección 9.2 ya había encontrado: ingreso contra costo en 0,969, una banda casi perfecta.

Fijate en las 25 huérfanas. Un merge con how="left" las deja con la categoría en NaN, y pairplot con hue simplemente no las dibuja. Sin ese isna().sum() impreso, 25 ventas desaparecen del análisis y nadie se entera. Es el mismo modo de falla de la unidad 6.

El pairplot no escala

Con n variables dibuja n por n celdas. Con 5 son 25, con 10 son 100 y con 30 son 900: la figura tarda, pesa y no se puede leer. No es una limitación de la biblioteca, es del ojo.

El orden útil es al revés del intuitivo: primero la matriz de correlación, que sí soporta muchas variables, y con eso se eligen las tres o cuatro que valen un pairplot.

La idea: una variable por canal visual

La otra manera de meter más variables en un gráfico es usar los canales que una nube de puntos tiene libres. La posición ya está ocupada por dos. Quedan el color, el tamaño y la forma.

EjemploCuatro variables en un solo grafico: posicion, color, tamano y forma
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

estudio = pd.DataFrame({
    "horas": [2, 3, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12],
    "nota": [52, 55, 60, 62, 68, 70, 74, 78, 83, 85, 89, 92],
    "proyectos": [1, 1, 2, 2, 3, 2, 3, 4, 3, 4, 5, 5],
    "curso": ["Datos", "Software", "IA"] * 4})

print("lo que codifica cada canal visual:")
print("  posicion x -> horas   ", list(estudio["horas"]))
print("  posicion y -> nota    ", list(estudio["nota"]))
print("  tamano     -> proyectos", list(estudio["proyectos"]))
print("  color/forma-> curso   ", sorted(estudio["curso"].unique()))

extremos = estudio.nlargest(3, "nota")[["curso", "horas", "nota", "proyectos"]]
print("\nlos tres de nota mas alta:")
print(extremos.to_string(index=False))

fig, ax = plt.subplots()
sns.scatterplot(data=estudio, x="horas", y="nota", hue="curso",
                size="proyectos", style="curso", sizes=(40, 220),
                alpha=0.78, ax=ax)
ax.set_title("Horas, nota, curso y proyectos en un grafico")
ax.set_xlabel("Horas por semana")
ax.set_ylabel("Nota sobre 100")
ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left", borderaxespad=0, fontsize=8)
fig.tight_layout()
plt.show()
nube de doce puntos donde la posicion da horas y nota, el color y la forma dan el curso y el tamano da la cantidad de proyectos
lo que codifica cada canal visual:
  posicion x -> horas    [2, 3, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12]
  posicion y -> nota     [52, 55, 60, 62, 68, 70, 74, 78, 83, 85, 89, 92]
  tamano     -> proyectos [1, 1, 2, 2, 3, 2, 3, 4, 3, 4, 5, 5]
  color/forma-> curso    ['Datos', 'IA', 'Software']

los tres de nota mas alta:
   curso  horas  nota  proyectos
      IA     12    92          5
Software     11    89          5
   Datos     10    85          4

Cuatro variables en un gráfico que se sigue leyendo. La regla que lo hace posible es que cada canal lleva exactamente una variable, y que cada variable va al canal que le corresponde:

Cuando se cruza el límite

EjemploEl mismo dato con seis canales visuales y con tres
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

ventas = pd.DataFrame({
    "visitas": [120, 150, 180, 210, 240, 270, 300, 330, 360, 390, 420, 450],
    "conversion": [2.1, 2.4, 2.6, 3.0, 3.1, 3.6, 3.8, 4.2, 4.4, 4.9, 5.1, 5.5],
    "region": ["Norte", "Sur", "Centro"] * 4,
    "canal": ["Web", "App"] * 6,
    "equipo": list("ABCDEFGHIJKL")})

print("niveles por variable categorica:")
for col in ["region", "canal", "equipo"]:
    print("  %-8s %2d niveles" % (col, ventas[col].nunique()))
print("\nEl panel de la izquierda mapea las tres a la vez mas el tamano:")
print("  12 niveles de equipo en el color es mas de lo que el ojo separa.")
print("\nconversion media por region:")
print(ventas.groupby("region")["conversion"].mean().round(2).to_string())

fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.4), sharey=True)

sns.scatterplot(data=ventas, x="visitas", y="conversion", hue="equipo",
                style="canal", size="visitas", sizes=(30, 260),
                legend=False, ax=axes[0])
axes[0].set_title("Seis canales: ilegible")
axes[0].set_xlabel("Visitas")
axes[0].set_ylabel("Conversion (%)")

sns.scatterplot(data=ventas, x="visitas", y="conversion", hue="region",
                s=80, alpha=0.85, ax=axes[1])
axes[1].set_title("Uno por variable: se lee")
axes[1].set_xlabel("Visitas")
axes[1].legend(title="region", fontsize=8)

fig.tight_layout()
plt.show()
dos paneles con la misma nube, el de la izquierda saturado de colores formas y tamanos distintos y el de la derecha limpio con un solo canal por variable
niveles por variable categorica:
  region    3 niveles
  canal     2 niveles
  equipo   12 niveles

El panel de la izquierda mapea las tres a la vez mas el tamano:
  12 niveles de equipo en el color es mas de lo que el ojo separa.

conversion media por region:
region
Centro    4.03
Norte     3.45
Sur       3.70

El panel de la izquierda tiene los mismos doce puntos que el de la derecha. Le mapea doce niveles de equipo al color, dos de canal a la forma y las visitas al tamaño, además de a la posición x. No hay nada roto: hay demasiado.

El de la derecha usa un canal para una variable y contesta una pregunta. Esa es toda la diferencia, y el número que la explica está impreso arriba del gráfico: doce niveles en el color es más de lo que el ojo separa.

Acá hay que elegir

Canales, facetas o varios gráficos

Cuando hay más variables que canales razonables, hay tres salidas:

  • Facetas (sns.relplot(..., col="region")): un panel por nivel de la categoría, todos con los mismos ejes. Es lo mejor cuando la categoría tiene entre tres y ocho niveles y querés comparar la misma relación entre grupos.
  • Más canales: sirve hasta cuatro variables, y con la disciplina de una por canal. Más allá, el gráfico deja de responder preguntas.
  • Varios gráficos separados: cuando las preguntas son distintas. Un panel de cuatro gráficos que contestan cuatro preguntas se lee mejor que un gráfico que intenta contestar las cuatro.

La pregunta que ordena la elección no es «¿cuántas variables tengo?» sino «¿cuántas preguntas estoy haciendo?». Una por gráfico.

Dónde se rompe

AttributeError: 'PairGrid' object has no attribute 'set_title'

Causa: pairplot es figure-level y no devuelve Axes.

Arreglo: g.figure.suptitle("..."). Lo mismo vale para relplot, catplot y displot.

TypeError: pairplot() got an unexpected keyword argument 'ax'

Causa: intentaste meterlo en un panel armado con plt.subplots.

Arreglo: no se puede, y no es un olvido de la biblioteca. Si necesitás el panel, armalo con funciones axes-level.

(sin error) el pairplot tarda muchísimo y sale ilegible

Causa: demasiadas columnas. Sin vars toma todas las numéricas, identificadores incluidos.

Arreglo: vars=[...] con tres o cuatro columnas, elegidas a partir de la matriz de correlación.

(sin error) faltan puntos en el gráfico y el total no cierra

Causa: la columna de hue tiene NaN, casi siempre por un merge con filas huérfanas. Seaborn no dibuja esas filas.

Arreglo: contá los NaN antes, decidí si van a una categoría «sin dato» o se descartan, e informá cuántos fueron.

(sin error) la leyenda tapa la mitad del gráfico

Causa: hue más size generan dos bloques de leyenda, y por defecto van adentro de los ejes.

Arreglo: ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left") para sacarla afuera, y fig.tight_layout() después.

Recetario

Varias variables en una figura
ExpresiónQué haceCuándo
sns.pairplot(df, vars=[...])Todas las dispersiones por paresTres o cuatro variables
corner=TrueSolo el triángulo inferiorCasi siempre: la otra mitad repite
diag_kind="hist"Histograma en la diagonalVer la forma de cada variable
hue="cat"Colorea por categoríaPocos niveles
plot_kws={"alpha": 0.45, "s": 18}Pasa opciones a los puntosCientos de observaciones
g.figure.set_size_inches(7.2, 5.6)Fija el tamaño totalObligatorio en figure-level
g.figure.suptitle("...")Título de la figura enteraÍdem
sns.scatterplot(..., hue=, size=, style=)Hasta cuatro variablesUna por canal, sin excepción
sizes=(40, 220)Rango de área de los marcadoresJunto con size
sns.relplot(data=df, x=, y=, col="cat")Un panel por nivelComparar la misma relación entre grupos
col_wrap=3Corta las facetas en filas de tresMuchos niveles
ax.legend(bbox_to_anchor=(1.02, 1))Saca la leyenda afueraCuando tapa los datos

Ponete a prueba

1. ¿Qué hay en la diagonal de un pairplot?Nivel 1 · Recordar
2. ¿Qué hace corner=True?Nivel 1 · Recordar
3. Llamás a sns.pairplot(df) sobre una tabla de ventas y aparecen dispersiones de producto_id contra cliente_id. ¿Qué faltó?Nivel 2 · Predecir la salida
4. Querés meter cuatro variables en una sola dispersión. ¿Cuál es la regla?Nivel 2 · Predecir la salida
5. Hacés un merge how="left" y 25 filas quedan con la categoría en NaN. Después graficás con hue="categoria". ¿Qué pasa?Nivel 3 · Elegir entre dos casi correctas
6. Tenés una tabla de 30 columnas numéricas y querés explorar relaciones. ¿Por dónde empezás?Nivel 3 · Elegir entre dos casi correctas
7. Tenés que comparar la misma relación entre visitas y conversión en seis regiones. ¿Qué conviene?Nivel 4 · Encontrar el defecto

Prácticas de esta lección