Más allá de la precisión: cuantificación de la fragilidad de la producción causada por características excesivas, redundantes y de baja señal en regresión

A primera vista, agregar más funciones a un modelo parece una forma obvia de mejorar el rendimiento. Si un modelo puede aprender de más información, debería poder hacer mejores predicciones. En la práctica, sin embargo, este instinto a menudo introduce riesgos estructurales ocultos. Cada característica adicional crea otra dependencia de las canalizaciones de datos ascendentes, los sistemas externos y los controles de calidad de los datos. Un solo campo faltante, un cambio de esquema o un conjunto de datos retrasado pueden degradar silenciosamente las predicciones en producción.

El problema más profundo no es el costo computacional o la complejidad del sistema: es la inestabilidad del peso. En los modelos de regresión, especialmente cuando las características están correlacionadas o son poco informativas, el optimizador tiene dificultades para asignar crédito de manera significativa. Los coeficientes pueden cambiar de manera impredecible a medida que el modelo intenta distribuir la influencia entre señales superpuestas, y las variables de señal baja pueden parecer importantes simplemente debido al ruido en los datos. Con el tiempo, esto lleva a modelos que parecen sofisticados sobre el papel pero que se comportan de manera inconsistente cuando se implementan.

En este artículo, examinaremos por qué agregar más características puede hacer que los modelos de regresión sean menos confiables en lugar de más precisos. Exploraremos cómo las características correlacionadas distorsionan las estimaciones de los coeficientes, cómo las señales débiles se confunden con patrones reales y por qué cada característica adicional aumenta la fragilidad de la producción. Para concretar estas ideas, analizaremos ejemplos utilizando un conjunto de datos de precios de propiedades y compararemos el comportamiento de grandes modelos de “fregadero de cocina” con alternativas más eficientes y estables.

Importando las dependencias

pip instala seaborn scikit-learn pandas numpy matplotlib
importar numpy como np importar pandas como pd importar matplotlib.pyplot como plt importar matplotlib.gridspec como gridspec importar seaborn como sns desde sklearn.linear_model importar Ridge desde sklearn.preprocessing importar StandardScaler desde sklearn.metrics importar mean_squared_error desde sklearn.model_selection importar train_test_split importar advertencias advertencias.filterwarnings("ignore") plt.rcParams.update({ "figure.facecolor": "#FAFAFA", "axes.facecolor": "#FAFAFA", "axes.spines.top": False, "axes.spines.right":False, "axes.grid": True, "grid.color": "#E5E5E5", "grid.linewidth": 0.8, "font.family": "monoespacio", }) SEMILLA = 42 np.random.seed(SEMILLA)

Este código establece un estilo Matplotlib limpio y consistente ajustando los colores de fondo, la apariencia de la cuadrícula y eliminando lomos de eje innecesarios para visualizaciones más claras. También establece una semilla aleatoria NumPy fija (42) para garantizar que cualquier dato generado aleatoriamente siga siendo reproducible en todas las ejecuciones.

Conjunto de datos de propiedades sintéticas

N = 800 # muestras de entrenamiento # ── Características de señal verdadera ──────────────────────────────────── pies cuadrados = np.random.normal(1800, 400, N) # dormitorios con señal fuerte = np.round(sqft / 550 + np.random.normal(0, 0.4, N)).clip(1, 6) vecindario = np.random.choice([0, 1, 2], N, p=[0.3, 0.5, 0.2]) # categórico # ── Características derivadas/correlacionadas (multicolinealidad) ─────── total_rooms = dormitorios + np.random.normal(2, 0.3, N) # ≈ dormitorios piso_area_m2 = pies cuadrados * 0.0929 + np.random.normal(0, 1, N) # ≈ pies cuadrados en m² lot_sqft = sqft * 1.4 + np.random.normal(0, 50, N) # ≈ sqft escalado # ── Características débiles/espurias ──────────────────────────────── código_color_puerta = np.random.randint(0, 10, N).astype(float) bus_stop_age_yrs = np.random.normal(15, 5, N) near_mcdonalds_m = np.random.normal(800, 200, N) # ── Funciones de ruido puro (simula 90 columnas aleatorias) ──────── noise_features = np.random.randn(N, 90) noise_df = pd.DataFrame( noise_features, columns=[f"noise_{i:03d}" for i in range(90)] ) # ── Objetivo: precio de la vivienda ───────────────────────────────────── precio = ( 120 * pies cuadrados + 8_000 * habitaciones + 30_000 * vecindario – 15 * bus_stop_age_yrs # pequeño efecto real + np.random.normal(0, 15_000, N) # ruido irreducible) # ── Ensamblar marcos de datos ────────────────────────────────────── signal_cols = ["sqft", "dormitorios", "vecindario", "total_rooms", "floor_area_m2", "lot_sqft", "door_color_code", "bus_stop_age_yrs", "nearest_mcdonalds_m"] df_base = pd.DataFrame({ "sqft": pies cuadrados, "dormitorios": dormitorios, "neighborhood": vecindario, "total_rooms": total_rooms, "floor_area_m2": piso_area_m2, "lot_sqft": lote_sqft, "door_color_code": puerta_color_code, "bus_stop_age_yrs": bus_stop_age_yrs, "nearest_mcdonalds_m": mcdonalds_m más cercano, "precio": precio, }) df_full = pd.concat([df_base.drop("precio", eje=1), noise_df, df_base[["precio"]]], eje=1) LEAN_FEATURES = ["sqft", "dormitorios", "vecindario"] NOISY_FEATURES = [c for c in df_full.columns if c != "price"] print(f"Características del modelo Lean: {len(LEAN_FEATURES)}") print(f"Características del modelo ruidoso: {len(NOISY_FEATURES)}") print(f"Forma del conjunto de datos: {df_full.shape}")

Este código construye un conjunto de datos sintéticos diseñado para imitar un escenario de precios de propiedades del mundo real, donde solo una pequeña cantidad de variables realmente influyen en el objetivo, mientras que muchas otras introducen redundancia o ruido. El conjunto de datos contiene 800 muestras de entrenamiento. Las características principales de la señal, como los pies cuadrados (sqft), el número de dormitorios y la categoría del vecindario, representan los principales impulsores de los precios de la vivienda. Además de estas, se crean intencionalmente varias características derivadas para que estén altamente correlacionadas con las variables principales, como piso_area_m2 (una conversión de unidades de pies cuadrados), lote_pies cuadrados y total_habitaciones. Estas variables simulan la multicolinealidad, un problema común en conjuntos de datos reales donde múltiples características transportan información superpuesta.

El conjunto de datos también incluye características débiles o espurias, como door_color_code, bus_stop_age_yrs y near_mcdonalds_m, que tienen poca o ninguna relación significativa con el precio de la propiedad. Para replicar aún más el problema del “modelo de fregadero de cocina”, el script genera 90 características de ruido completamente aleatorias, que representan columnas irrelevantes que a menudo aparecen en grandes conjuntos de datos. El precio variable objetivo se construye utilizando una fórmula conocida en la que los metros cuadrados, las habitaciones y el vecindario tienen la mayor influencia, mientras que la edad de la parada de autobús tiene un efecto muy pequeño y el ruido aleatorio introduce una variabilidad natural.

Finalmente, se definen dos conjuntos de características: un modelo eficiente que contiene solo las tres características de señal verdadera (pies cuadrados, dormitorios, vecindario) y un modelo ruidoso que contiene todas las columnas disponibles excepto el objetivo. Esta configuración nos permite comparar directamente el rendimiento de un conjunto mínimo de características de alta señal con un modelo grande con muchas características lleno de variables redundantes e irrelevantes.

Dilución de peso mediante multicolinealidad

print("n── Correlación entre pares de características correlacionadas ──") corr_pairs = [ ("sqft", "floor_area_m2"), ("sqft", "lot_sqft"), ("dormitorios", "total_rooms"), ] para a, b en corr_pairs: r = np.corrcoef(df_full[a], df_full[b])[0, 1] print(f" {a:20s} ↔ {b:20s} r = {r:.3f}") fig, axes = plt.subplots(1, 3, figsize=(14, 4)) fig.suptitle("Dilución de peso: pares de características correlacionadas", fontsize=13, fontweight="bold", y=1.02) para hacha, (a, b) en zip(axes, corr_pairs): ax.scatter(df_full[a], df_full[b], alpha=0.25, s=12, color="#3B6FD4") r = np.corrcoef(df_full[a], df_full[b])[0, 1] ax.set_title(f"r = {r:.3f}", tamaño de fuente=11) ax.set_xlabel(a); ax.set_ylabel(b) plt.tight_layout() plt.savefig("01_multicollinearity.png", dpi=150, bbox_inches="tight") plt.show() print("Guardado → 01_multicollinearity.png")

Esta sección demuestra la multicolinealidad, una situación en la que varias funciones contienen información casi idéntica. El código calcula coeficientes de correlación para tres pares de características intencionalmente correlacionadas: pies cuadrados frente a área_m2 de piso, pies cuadrados frente a pies cuadrados de lote y dormitorios frente a habitaciones_total.

Como muestran los resultados impresos, estas relaciones son extremadamente fuertes (r ≈ 1,0, 0,996 y 0,945), lo que significa que el modelo recibe múltiples señales que describen la misma característica de propiedad subyacente.

Los diagramas de dispersión visualizan esta superposición. Debido a que estas características se mueven juntas casi perfectamente, el optimizador de regresión lucha por determinar qué característica debe recibir crédito por predecir el objetivo. En lugar de asignar un peso claro a una variable, el modelo a menudo divide la influencia entre características correlacionadas de manera arbitraria, lo que genera coeficientes inestables y diluidos. Esta es una de las razones clave por las que agregar características redundantes puede hacer que un modelo sea menos interpretable y menos estable, incluso si el rendimiento predictivo inicialmente parece similar.

Inestabilidad del peso a lo largo de los ciclos de reentrenamiento

N_CYCLES = 30 SAMPLE_SZ = 300 # tamaño de cada segmento de reentrenamiento scaler_lean = StandardScaler() scaler_noisy = StandardScaler() # Ajustar los escaladores a los datos completos para que las unidades sean comparables X_lean_all = scaler_lean.fit_transform(df_full[LEAN_FEATURES]) X_noisy_all = escalar_noisy.fit_transform(df_full[NOISY_FEATURES]) y_all = df_full["precio"].valores lean_weights =[]# forma: (N_CYCLES, 3) pesos_ruidosos =[]# forma: (N_CYCLES, 3) – primeras 3 columnas solo para comparación del ciclo en el rango (N_CYCLES): idx = np.random.choice(N, SAMPLE_SZ, replace=False) X_l = X_lean_all[idx]; y_c = y_all[idx] X_n = X_noisy_all[idx] m_lean = Ridge(alpha=1.0).fit(X_l, y_c) m_noisy = Ridge(alpha=1.0).fit(X_n, y_c) lean_weights.append(m_lean.coef_) noise_weights.append(m_noisy.coef_[:3]) # pies cuadrados, dormitorios, vecindario lean_weights = np.array(lean_weights) noise_weights = np.array(noisy_weights) print("n── Coeficiente Std Dev en 30 ciclos de reentrenamiento ──") print(f"{'Característica':<18} {'Lean σ':>10} {'Ruidoso σ':>10} {'Amplificación':>14}") para i, hazaña en enumerate(LEAN_FEATURES): sl = lean_weights[:, i].std() sn = noise_weights[:, i].std() print(f" {feat:<16} {sl:>10.1f} {sn:>10.1f} ×{sn/sl:.1f}") fig, axes = plt.subplots(1, 3, figsize=(15, 4)) fig.suptitle("Inestabilidad de peso: modelo magro versus modelo ruidoso (30 ciclos de reentrenamiento)", fontsize=13, fontweight="bold", y=1.02) colores = {"lean": "#2DAA6E", "noisy": "#E05C3A"} para i, hazaña en enumerar(LEAN_FEATURES): ax = ejes[i] ax.plot(lean_weights[:, i], color=colors["lean"], linewidth=2, label="Lean (3 características)", alpha=0.9) ax.plot(noisy_weights[:, i], color=colors["noisy"], linewidth=2, label="Ruidoso (100+) características)", alpha=0.9, linestyle="–") ax.set_title(f'Coeficiente: "{feat}"', fontsize=11) ax.set_xlabel("Ciclo de reentrenamiento") ax.set_ylabel("Peso estandarizado") si i == 0: ax.legend(fontsize=9) plt.tight_layout() plt.savefig("02_weight_instability.png", dpi=150, bbox_inches="tight") plt.show() print("Guardado → 02_weight_instability.png")

Este experimento simula lo que sucede en sistemas de producción reales donde los modelos se vuelven a entrenar periódicamente con datos nuevos. A lo largo de 30 ciclos de reentrenamiento, el código muestrea aleatoriamente subconjuntos del conjunto de datos y se ajusta a dos modelos: un modelo ajustado que utiliza solo las tres características de señal principales y un modelo ruidoso que utiliza el conjunto completo de características que contiene variables aleatorias y correlacionadas. Al rastrear los coeficientes de las características clave en cada ciclo de reentrenamiento, podemos observar qué tan estables permanecen los pesos aprendidos a lo largo del tiempo.

Los resultados muestran un patrón claro: el modelo ruidoso exhibe una variabilidad de coeficientes significativamente mayor.

Por ejemplo, la desviación estándar del coeficiente de pies cuadrados aumenta 2,6 veces, mientras que los dormitorios se vuelven 2,2 veces más inestables en comparación con el modelo delgado. Las líneas trazadas hacen que este efecto sea visualmente obvio: los coeficientes del modelo eficiente permanecen relativamente suaves y consistentes a lo largo de los ciclos de reentrenamiento, mientras que los pesos del modelo ruidoso fluctúan mucho más. Esta inestabilidad surge porque características correlacionadas e irrelevantes obligan al optimizador a redistribuir el crédito de manera impredecible, haciendo que el comportamiento del modelo sea menos confiable incluso si la precisión general parece similar.

Degradación de la relación señal-ruido (SNR)

correlaciones = df_full[NOISY_FEATURES + ["precio"]].corr()["precio"].drop("precio") correlaciones = correlaciones.abs().sort_values(ascending=False) fig, ax = plt.subplots(figsize=(14, 5)) bar_colors = [ "#2DAA6E" if f in LEAN_FEATURES else "#E8A838" if f en ["total_rooms", "floor_area_m2", "lot_sqft", "bus_stop_age_yrs"] else "#CCCCCC" para f en correlaciones.index ] ax.bar(range(len(correlations)), correlaciones.values, color=bar_colors, width=0.85, edgecolor="none") # Parches de leyenda de matplotlib.patches import Parche legend_elements = [ Patch(facecolor="#2DAA6E", label="Señal alta (conjunto reducido)"), Patch(facecolor="#E8A838", label="Correlacionado/señal baja"), Patch(facecolor="#CCCCCC", label="Ruido puro"), ] ax.legend(handles=legend_elements, fontsize=10, loc="upper right") ax.set_title("Relación señal-ruido: |Correlación con precio| por característica", fontsize=13, fontweight="bold") ax.set_xlabel("Clasificación de característica (ordenada por |r|)") ax.set_ylabel("|Pearson r| con precio") ax.set_xticks([]) plt.tight_layout() plt.savefig("03_snr_degradation.png", dpi=150, bbox_inches="tight") plt.show() print("Guardado → 03_snr_degradation.png")

Esta sección mide la intensidad de la señal de cada característica calculando su correlación absoluta con la variable objetivo (precio). El gráfico de barras clasifica todas las características según su correlación, resaltando las características de señal alta verdadera en verde, las características correlacionadas o débiles en naranja y el gran conjunto de características de ruido puro en gris.

La visualización muestra que sólo un pequeño número de variables transmiten una señal predictiva significativa, mientras que la mayoría contribuye poco o nada. Cuando en un modelo se incluyen muchas características de baja señal o ruido, se diluye la relación señal-ruido general, lo que dificulta que el optimizador identifique consistentemente las características que realmente importan.

Simulación de deriva de características

def predict_with_drift(model, scaler, X_base, drift_col_idx, drift_magnitude, feature_cols): """Inyecta la deriva en una columna de características y mide el cambio de predicción.""" X_drifted = X_base.copy() X_drifted[:, drift_col_idx] += drift_magnitude return model.predict(scaler.transform(X_drifted)) # Vuelve a ajustar ambos modelos al máximo conjunto de datos sc_lean = StandardScaler().fit(df_full[LEAN_FEATURES]) sc_noisy = StandardScaler().fit(df_full[NOISY_FEATURES]) m_lean_full = Ridge(alpha=1.0).fit( sc_lean.transform(df_full[LEAN_FEATURES]), y_all) m_noisy_full = Ridge(alpha=1.0).fit( sc_noisy.transform(df_full[NOISY_FEATURES]), y_all) X_lean_raw = df_full[LEAN_FEATURES].values X_noisy_raw = df_full[NOISY_FEATURES].values base_lean = m_lean_full.predict(sc_lean.transform(X_lean_raw)) base_noisy = m_noisy_full.predict(sc_noisy.transform(X_noisy_raw)) # Deriva la característica "bus_stop_age_yrs" (señal baja, pero en modelo ruidoso) drift_col_noisy = NOISY_FEATURES.index("bus_stop_age_yrs") drift_range = np.linspace(0, 20, 40) # variación de hasta 20 años en la edad de las paradas de autobús rmse_lean_drift, rmse_noisy_drift =[],[]para d en drift_range: preds_noisy = predict_with_drift( m_noisy_full, sc_noisy, X_noisy_raw, drift_col_noisy, d, NOISY_FEATURES) # El modelo Lean ni siquiera tiene esta característica → no afectado rmse_lean_drift.append( np.sqrt(mean_squared_error(base_lean, base_lean))) # 0 por diseño rmse_noisy_drift.append( np.sqrt(mean_squared_error(base_noisy, preds_noisy))) fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(drift_range, rmse_lean_drift, color="#2DAA6E", linewidth=2.5, label="Modelo eficiente (función no presente)") ax.plot(drift_range, rmse_noisy_drift, color="#E05C3A", linewidth=2.5, linestyle="–", label="Modelo ruidoso ("bus_stop_age_yrs" deriva)") ax.fill_between(drift_range, rmse_noisy_drift, alpha=0.15, color="#E05C3A") ax.set_xlabel("Magnitud de deriva de característica (años)", fontsize=11) ax.set_ylabel("Cambio de predicción RMSE ($)", fontsize=11) ax.set_title("Sensibilidad de deriva de característica:nCada característica adicional = punto de falla adicional", fontsize=13, fontweight="bold") ax.legend(fontsize=10) plt.tight_layout() plt.savefig("05_drift_sensitivity.png", dpi=150, bbox_inches="tight") plt.show() print("Guardado → 05_drift_sensitivity.png")

Este experimento ilustra cómo la deriva de características puede afectar silenciosamente las predicciones del modelo en producción. El código introduce una deriva gradual en una característica débil (bus_stop_age_yrs) y mide cuánto cambian las predicciones del modelo. Dado que el modelo pobre no incluye esta característica, sus predicciones permanecen completamente estables, mientras que el modelo ruidoso se vuelve cada vez más sensible a medida que crece la magnitud de la deriva.

El gráfico resultante muestra un error de predicción que aumenta constantemente a medida que la característica cambia, lo que resalta una importante realidad de producción: cada característica adicional se convierte en otro posible punto de falla. Incluso las variables de baja señal pueden introducir inestabilidad si cambia su distribución de datos o si cambian las tuberías ascendentes.

Consulte los códigos completos aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 120.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.

Soy graduado en ingeniería civil (2022) de Jamia Millia Islamia, Nueva Delhi, y tengo un gran interés en la ciencia de datos, especialmente las redes neuronales y su aplicación en diversas áreas.