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.
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ímbolo | Qué es |
|---|---|
| v3 | la columna que sobra |
| α, β | dos números cualquiera, las letras alfa y beta |
| v1, v2 | las 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_dummiestienedrop_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.