Capítulo 10 de 21 11 secciones 7 min

Compartir

Autovalores y autovectores, sin empezar por el determinante

Qué es una dirección que una matriz no gira, qué mide el número que la acompaña, y por qué eso es lo que hay debajo de PCA.

Un autovector es una dirección que la matriz no gira: solo la estira o la encoge. Cuánto la estira es el autovalor. En datos, los autovectores de la matriz de covarianza son las direcciones donde tus datos más varían, y los autovalores dicen cuánto varían en cada una. Eso es PCA por dentro.

Te van a enseñar esto empezando por el determinante. Yo no 🌸

La forma clásica de enseñar autovalores es plantear una ecuación con un determinante, resolver un polinomio y sacar unas raíces. Se puede hacer y no sirve para entender nada.

Vamos por el otro lado: qué pregunta contesta.

La pregunta

Una matriz, cuando multiplica a un vector, normalmente lo gira y lo estira. Le cambia la dirección y el largo.

La pregunta es: ¿hay alguna dirección que esta matriz no gire? ¿Alguna donde el vector salga apuntando exactamente igual, solo más largo o más corto?

Casi siempre las hay. Esas direcciones son los autovectores, y cuánto se estira cada una es su autovalor.

A𝐯=λ𝐯

un autovector es una dirección que la matriz no gira: la deja apuntando igual y solo la estira o la encoge, y cuánto la estira es el autovalor

SímboloQué es
Ala matriz
vel autovector: la dirección que no gira
λel autovalor, la letra lambda: cuánto se estira esa dirección

Léela así: "multiplicar por la matriz hace lo mismo que multiplicar por un número". Eso es todo lo que dice, y es rarísimo que pase. Por eso las direcciones donde pasa son especiales.

Abrimos el archivo con más columnas

Este bloque necesita una tabla más ancha. Con las tres de siempre no hay nada que descomponer, porque están casi sin relación entre ellas. Así que meto dos más: el descuento y el monto final facturado.

import pandas as pd
import numpy as np

COLS = ['unidades', 'monto', 'descuento', 'satisfaccion',
        'monto_final_facturado']

df = pd.read_csv('ventas-miss-yera.csv')
for c in COLS:
    df[c] = pd.to_numeric(df[c], errors='coerce')
M = df[COLS].dropna().to_numpy()

Z = (M - M.mean(axis=0)) / M.std(axis=0)
print('M.shape:', M.shape)
M.shape: (1793, 5)

Esa Z es la tabla escalada: a cada columna le resté su promedio y la dividí por su desviación. Después de eso todas tienen media cero y desviación uno, o sea que ninguna manda por tener números más grandes. En el capítulo 7 viste por qué eso hace falta.

La matriz que nos importa

C = np.cov(Z, rowvar=False)
print(np.round(C, 4))
[[ 1.0006e+00  5.0000e-04 -1.2200e-02 -3.5600e-02  1.0000e-03]
 [ 5.0000e-04  1.0006e+00 -4.0000e-02 -1.4600e-02  7.3770e-01]
 [-1.2200e-02 -4.0000e-02  1.0006e+00 -8.4000e-03 -6.2200e-02]
 [-3.5600e-02 -1.4600e-02 -8.4000e-03  1.0006e+00  8.0200e-02]
 [ 1.0000e-03  7.3770e-01 -6.2200e-02  8.0200e-02  1.0006e+00]]

Es la matriz de covarianza: en la diagonal está cuánto varía cada columna, y fuera cuánto se mueven juntas dos columnas. Como escalamos, la diagonal sale toda cerca de uno.

Y hay un número que salta: 0.7377, entre el monto y el monto final facturado. Esas dos columnas se mueven juntas, y tiene todo el sentido del mundo, porque una es casi la otra con el descuento aplicado.

Es simétrica, como te adelanté en el capítulo 6. Guárdate eso porque es lo que hace que todo lo que viene funcione.

Sacamos las direcciones

vals, vecs = np.linalg.eigh(C)
orden = np.argsort(vals)[::-1]
vals, vecs = vals[orden], vecs[:, orden]

print('autovalores:', np.round(vals, 4))
print('autovectores, uno por columna:')
print(np.round(vecs, 4))
autovalores: [1.7482 1.0382 0.9982 0.9617 0.2565]
autovectores, uno por columna:
[[-0.      0.7011  0.1877 -0.6879  0.0045]
 [ 0.6996  0.065  -0.113   0.0399  0.7014]
 [-0.0969 -0.112  -0.9189 -0.365  -0.0203]
 [ 0.0631 -0.701   0.3252 -0.6251  0.09  ]
 [ 0.7051 -0.0171 -0.0432 -0.0338 -0.7068]]

Uso eigh y no eig porque la matriz es simétrica. La versión con hache lo aprovecha, va más rápido y devuelve números reales garantizados. Con eig a veces salen complejos por redondeo y empieza el circo.

eigh te los da de menor a mayor, así que hay que darles la vuelta. Eso hace el [::-1].

Comprobamos que la fórmula se cumple

v0 = vecs[:, 0]
print('C @ v0      :', np.round(C @ v0, 6))
print('lambda * v0 :', np.round(vals[0] * v0, 6))
print('son iguales :', np.allclose(C @ v0, vals[0] * v0))
C @ v0      : [-8.300000e-05  1.223104e+00 -1.693240e-01  1.102990e-01  1.232683e+00]
lambda * v0 : [-8.300000e-05  1.223104e+00 -1.693240e-01  1.102990e-01  1.232683e+00]
son iguales : True

Multiplicar por la matriz de cinco por cinco dio exactamente lo mismo que multiplicar por el número 1.7482. Eso es un autovector.

Y ahora léelo, que es lo bueno

Mira la primera columna de los autovectores, que es la dirección más importante:

ColumnaCuánto pesa en la dirección 1
unidades-0.0000
monto0.6996
descuento-0.0969
satisfaccion0.0631
monto_final_facturado0.7051

Dos números grandes y tres cerca de cero. La dirección donde más varían tus datos es plata: monto y monto final facturado, casi con el mismo peso, y lo demás no participa.

Eso no lo sabía nadie de antemano. Lo encontró la matriz sola 🌟

Tres propiedades que hacen falta después

print('largo del autovector :', round(float(np.linalg.norm(v0)), 6))
print('v0 . v1              :', round(float(vecs[:, 0] @ vecs[:, 1]), 10))
print('traza de C           :', round(float(np.trace(C)), 4))
print('suma de autovalores  :', round(float(vals.sum()), 4))
largo del autovector : 1.0
v0 . v1              : -0.0
traza de C           : 5.0028
suma de autovalores  : 5.0028

Uno. Los autovectores vienen con largo 1. Solo marcan dirección, no distancia.

Dos. El producto punto entre dos de ellos da cero, o sea que son perpendiculares entre sí, en el sentido del capítulo 9. Cada uno mira a un sitio que los otros no miran. Eso pasa porque la matriz es simétrica, y es la razón de que la simetría importara.

Tres. Los autovalores suman lo mismo que la diagonal. La varianza total no se creó ni se destruyó: se repartió entre direcciones. Y esa frase es PCA entero, que es el capítulo que viene.

Lo que te llevas

  • Un autovector es una dirección que la matriz no gira.
  • El autovalor es cuánto la estira.
  • Av = λv: multiplicar por la matriz hace lo mismo que multiplicar por un número.
  • Para matrices simétricas usa eigh, no eig.
  • Salen con largo 1 y perpendiculares entre sí.
  • Los autovalores suman la varianza total: la reparten, no la inventan.

Comprueba que se entendió

Comprueba que lo tienes

La matriz de covarianza de tus 5 columnas escaladas tiene autovalores [1.75, 1.04, 1.00, 0.96, 0.26]. ¿Qué te está diciendo el último?

  • Que hay una dirección donde tus datos casi no varían, o sea información repetida entre columnas
  • Que una columna tiene menos datos que las otras
  • Que la quinta columna es la menos importante
  • Que hay un error de escala en los datos

Ejercicios

1. Una dirección cualquiera SÍ gira

Para que veas que ser autovector es raro.

C = np.cov(Z, rowvar=False)
vals, vecs = np.linalg.eigh(C)
cualquiera = np.array([1.0, 0.0, 0.0, 0.0, 0.0])
salida = C @ cualquiera
print('entra :', cualquiera)
print('sale  :', np.round(salida, 4))
print('mismo sentido:',
      np.allclose(salida / np.linalg.norm(salida), cualquiera))
entra : [1. 0. 0. 0. 0.]
sale  : [ 1.0006e+00  5.0000e-04 -1.2200e-02 -3.5600e-02  1.0000e-03]
mismo sentido: False

Entró apuntando solo a las unidades y salió con un poco de todo. Eso es lo normal. Los autovectores son las pocas direcciones donde no pasa.

2. El autovalor más chico y qué columnas lo forman

La dirección donde no pasa nada es la más informativa.

COLS = ['unidades', 'monto', 'descuento', 'satisfaccion',
        'monto_final_facturado']
vals, vecs = np.linalg.eigh(np.cov(Z, rowvar=False))
orden = np.argsort(vals)[::-1]
vals, vecs = vals[orden], vecs[:, orden]

ultimo = vecs[:, -1]
print('autovalor mas chico:', round(float(vals[-1]), 4))
for nombre, peso in zip(COLS, ultimo):
    print(f'  {nombre:24} {peso:8.4f}')
autovalor mas chico: 0.2565
  unidades                   0.0045
  monto                      0.7014
  descuento                 -0.0203
  satisfaccion               0.0900
  monto_final_facturado     -0.7068

Léelo: monto menos monto final facturado. En esa dirección tus datos casi no se mueven, y eso quiere decir que esas dos columnas dicen casi lo mismo. Es el detector de redundancia del capítulo 8, pero fino: no te dice si sobra, te dice cuánto sobra.

3. Vuelve a armar la matriz con sus piezas

Autovalores y autovectores contienen la matriz entera.

C = np.cov(Z, rowvar=False)
vals, vecs = np.linalg.eigh(C)
rearmada = vecs @ np.diag(vals) @ vecs.T
print('vuelve a salir C:', np.allclose(rearmada, C))
vuelve a salir C: True

Girar a las direcciones buenas, estirar cada una por su autovalor, girar de vuelta. Eso se llama diagonalizar, y es la forma de la que salen PCA y SVD. Fíjate en que no se perdió nada: es la misma matriz escrita en otro idioma.

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?