Capítulo 8 de 21 9 secciones 7 min

Compartir

Rango e independencia: por qué dos columnas parecidas rompen tu modelo

Qué es el rango de una matriz, qué es la independencia lineal, y cómo detectar las columnas que sobran antes de que revienten algo.

El rango de una matriz es cuántas columnas aportan información propia. Si tienes 8 columnas y rango 6, dos se pueden fabricar con las otras. Eso rompe una regresión sin dar error: los pesos salen enormes y contradictorios, y la interpretación deja de valer.

El bug que no da error 🌸

En el capítulo 6 rompimos la inversa a propósito pegando una columna que eran las unidades por dos. Salió Singular matrix.

Ese caso es el amable, porque avisa. El que hace daño de verdad es el hermano callado: dos columnas que casi son la misma. Ahí no hay error, hay resultados que parecen bien y no lo están.

Este capítulo es para detectarlo antes.

Volvemos a abrir el archivo

Cada capítulo arranca de cero, así que estas líneas se repiten. Son las mismas del capítulo 1 y las vas a ver en todos.

import pandas as pd
import numpy as np

df = pd.read_csv('ventas-miss-yera.csv')
for c in ('unidades', 'monto', 'satisfaccion'):
    df[c] = pd.to_numeric(df[c], errors='coerce')
X = df[['unidades', 'monto', 'satisfaccion']].dropna().to_numpy()
print('X.shape:', X.shape)
X.shape: (2238, 3)

Cuántas columnas aportan de verdad

print('columnas:', X.shape[1], '| rango:', np.linalg.matrix_rank(X))
columnas: 3 | rango: 3

Tres columnas, rango tres. Todo bien: ninguna se puede fabricar con las otras. Se dice que son linealmente independientes.

Ahora le pego una que sí se puede fabricar:

Xd = np.column_stack([X, X[:, 0] * 2])
Xs = np.column_stack([X, X[:, 0] + X[:, 2]])
print('con unidades por 2       :', Xd.shape, 'rango', np.linalg.matrix_rank(Xd))
print('con unidades+satisfaccion:', Xs.shape, 'rango', np.linalg.matrix_rank(Xs))
con unidades por 2       : (2238, 4) rango 3
con unidades+satisfaccion: (2238, 4) rango 3

Cuatro columnas y rango tres, las dos veces. La tabla creció y la información no.

Y mira el segundo caso, que es el interesante: la columna nueva no es un múltiplo de ninguna, es una suma de dos. Y aun así sobra.

𝐯3=α𝐯1+β𝐯2

una columna es combinación lineal de otras si se puede fabricar sumándolas y escalándolas, y entonces no trae información nueva

SímboloQué es
v3la columna que sobra
α, βdos números cualquiera, las letras alfa y beta
v1, v2las columnas con las que se puede fabricar

Esa es la definición entera de dependencia lineal: se puede armar con las otras sumando y escalando, que son las dos operaciones del capítulo 3. El rango es simplemente cuántas quedan cuando quitas las que sobran.

Dónde te va a pasar de verdad

Nadie pega a mano una columna por dos. Pero esto sí pasa, y pasa mucho:

  • El mismo dato en dos unidades. Precio en soles y precio en dólares con tipo de cambio fijo. Peso en kilos y en libras.
  • Un total y sus partes. Si tienes ventas de Lima, de provincia y el total, el total sobra.
  • Dummies sin quitar una. Si conviertes "canal" en tres columnas de ceros y unos, las tres suman siempre uno, y con eso ya sobra una. Por eso pd.get_dummies tiene drop_first. Es el error más común de los tres.
  • Porcentajes que suman cien. Misma historia.

El daño, y por qué es callado

Xd = np.column_stack([X, X[:, 0] * 2])
print('det de X.T @ X   :', round(float(np.linalg.det(X.T @ X)), 2))
print('det con la de mas:', round(float(np.linalg.det(Xd.T @ Xd)), 6))
det de X.T @ X   : 4.710773414834139e+18
det con la de mas: 0.0
np.linalg.inv(Xd.T @ Xd)
LinAlgError: Singular matrix

Determinante cero, no hay inversa, y las ecuaciones normales del capítulo 6 no se pueden resolver. Ese es el caso limpio.

El sucio es cuando el determinante no es cero pero está cerca. Ahí inv funciona, te devuelve pesos, y esos pesos son basura: enormes, con signos que no tienen sentido, y que cambian del todo si añades una fila. Eso se llama multicolinealidad, y es la razón número uno por la que alguien mira los coeficientes de su regresión y dice "esto no puede ser".

El modelo sigue prediciendo parecido. Lo que se rompe es la interpretación, y si tú vendes la interpretación, se rompe tu trabajo 🙃

La correlación no es lo mismo, y hay que decirlo

print(np.round(np.corrcoef(X, rowvar=False), 4))
[[ 1.      0.0039 -0.0139]
 [ 0.0039  1.     -0.0224]
 [-0.0139 -0.0224  1.    ]]

Mis tres columnas están prácticamente sin correlación entre sí, y el rango es tres. Todo coherente.

Pero cuidado con la tentación de usar la correlación como detector: la correlación mira las columnas de dos en dos. Si una columna es la suma de otras tres, puede tener correlación bajita con cada una por separado y aun así sobrar. El rango lo ve y la correlación no.

Por eso: la correlación para mirar, el rango para decidir.

Lo que te llevas

  • El rango es cuántas columnas aportan información propia.
  • Columnas menos rango = columnas que sobran.
  • Dependencia lineal es "se puede fabricar sumando y escalando las otras".
  • Te va a pasar con dummies sin drop_first, con totales junto a sus partes y con el mismo dato en dos unidades.
  • Dependencia exacta revienta; casi dependencia no revienta y arruina la interpretación.
  • La correlación mira de dos en dos, el rango mira todo junto.

Comprueba que se entendió

Comprueba que lo tienes

Tu tabla tiene 8 columnas y np.linalg.matrix_rank te devuelve 6. ¿Qué quiere decir?

  • Que dos columnas se pueden fabricar con las otras seis
  • Que hay 6 filas útiles y las demás están repetidas
  • Que faltan datos en dos columnas
  • Que seis columnas están correlacionadas entre sí

Ejercicios

1. Cuántas sobran, en una línea

El chequeo que deberías correr antes de cada regresión.

def cuantas_sobran(M):
    return M.shape[1] - np.linalg.matrix_rank(M)

Xs = np.column_stack([X, X[:, 0] + X[:, 2]])
print('X   :', cuantas_sobran(X))
print('Xs  :', cuantas_sobran(Xs))
X   : 0
Xs  : 1

Dos líneas y te ahorras una tarde de mirar coeficientes raros. Ponlo en tu código de preparación de datos, antes de entrenar nada.

2. El error de las dummies, hecho a propósito

Convierte una columna de texto en dummies y mira el rango.

D = pd.get_dummies(df['canal'], dtype=float).to_numpy()
print('solo dummies   :', D.shape[1], 'columnas, rango', np.linalg.matrix_rank(D))
print('cada fila suma :', np.unique(D.sum(axis=1)))

unos = np.ones((D.shape[0], 1))
M = np.hstack([unos, D])
print('con intercepto :', M.shape[1], 'columnas, rango', np.linalg.matrix_rank(M))

Md = np.hstack([unos, D[:, 1:]])
print('con drop_first :', Md.shape[1], 'columnas, rango', np.linalg.matrix_rank(Md))
solo dummies   : 4 columnas, rango 4
cada fila suma : [1.]
con intercepto : 5 columnas, rango 4
con drop_first : 4 columnas, rango 4

Lee las tres con calma, porque acá hay un matiz que casi nadie cuenta bien.

Las cuatro dummies solas tienen rango 4: son independientes. El problema aparece en la segunda línea, cuando entra la columna de unos: las cuatro dummies suman uno en cada fila, así que juntas fabrican esa columna de unos y ahora sobra una. Cinco columnas, rango 4.

Y esa columna de unos no la pones tú: la pone el intercepto del modelo, sin avisar. Por eso la trampa de las variables dummy es tan escurridiza, y por eso pd.get_dummies trae drop_first. La tercera línea lo demuestra: quitando una, vuelve a cuadrar.

3. Casi dependiente, que es el caso feo

Fabrica una columna casi igual a otra y mira que el rango no se entera.

casi = X[:, 0] * 2 + np.linspace(0, 0.001, X.shape[0])
Xc = np.column_stack([X, casi])
print('rango           :', np.linalg.matrix_rank(Xc))
print('determinante    :', float(np.linalg.det(Xc.T @ Xc)))
print('numero de condicion:', round(float(np.linalg.cond(Xc.T @ Xc)), 1))
rango           : 4
determinante    : 1122514662638217.2
numero de condicion: 57014881775575.3

Rango 4, o sea que para el rango está todo bien. Y sin embargo el número de condición tiene doce cifras, que es la señal de alarma de verdad: mide cuánto se amplifica cualquier error de redondeo al resolver. Por encima de mil ya conviene mirar; con esto, los coeficientes no valen nada. El rango detecta lo exacto, np.linalg.cond detecta lo casi.

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?