Selección de características
1 ¿Por qué seleccionar características?
Tener más datos no siempre es mejor. Un dataset con 500 variables puede ser peor que uno con 20 si las 480 restantes son ruido. La selección de características elimina las variables irrelevantes o redundantes, lo que produce:
- Modelos más rápidos: menos dimensiones = menos cálculos.
- Menor sobreajuste: el modelo no aprende patrones espurios del ruido.
- Mayor interpretabilidad: un modelo con 5 variables es más fácil de explicar que uno con 500.
Nota
Distinción importante: La selección de características no modifica las variables existentes — solo elige cuáles conservar y cuáles descartar. Esto la diferencia de la extracción de características (como PCA), que crea representaciones nuevas.
2 Métodos de filtrado
Los métodos de filtrado evalúan cada variable independientemente del modelo usando estadísticos como correlación, chi-cuadrado o información mutua. Son rápidos pero ignoran las interacciones entre variables.
2.1 Umbral de varianza
La técnica más simple: elimina variables cuya varianza esté por debajo de un umbral. La idea es que una variable con varianza casi cero (casi siempre el mismo valor) no aporta información.
Nota
¿Escalar antes de aplicar umbral de varianza? Depende. Si las variables están en rangos similares (por ejemplo, todas entre 0 y 1), puedes comparar varianzas directamente. Si están en rangos muy distintos (edad 0-100, ingresos 0-100000), la variable con mayor rango tendría mayor varianza por escalas, no por información. En ese caso, escala primero o usa covarianza en lugar de varianza.
from sklearn.feature_selection import VarianceThreshold
from sklearn.datasets import fetch_california_housing
import pandas as pd
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
print("Varianza original:")
print(X.var().round(2))
# AveOccup 3.62
# HouseAge 158.28
# AveRooms 6.12
# ...
# Eliminar variables con varianza < 1
selector = VarianceThreshold(threshold=1.0)
X_new = selector.fit_transform(X)
print(f"\nFeatures originales: {X.shape[1]}")
print(f"Features tras umbral: {X_new.shape[1]}")
print(f"Eliminadas: {X.columns[~selector.get_support()].tolist()}")
# Con datos escalados (comparar varianzas en mismo rango)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
selector = VarianceThreshold(threshold=0.5)
X_new = selector.fit_transform(X_scaled)
print(f"Features tras umbral en datos escalados: {X_new.shape[1]}")
2.2 Correlación de Pearson
Mide la relación lineal entre cada variable numérica y la objetivo. Para profundizar en la fórmula y propiedades de Pearson, ver el capítulo de Fundamentos Matemáticos.
from sklearn.datasets import fetch_california_housing
import pandas as pd
import matplotlib.pyplot as plt
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = housing.target
# Correlación con el target
corr_with_target = X.corrwith(pd.Series(y, name='target')).abs().sort_values(ascending=False)
print("Correlación con precio:")
print(corr_with_target.round(4))
# MedInc 0.6881
# AveRooms 0.1519
# HouseAge 0.1056
# ...
# Visualizar
plt.figure(figsize=(8, 5))
corr_with_target.plot(kind='barh')
plt.xlabel('Correlación absoluta con precio')
plt.title('Pearson: correlación de cada feature con el objetivo')
plt.tight_layout()
# Seleccionar variables con correlación > 0.1
selected = corr_with_target[corr_with_target > 0.1].index.tolist()
print(f"Features seleccionadas ({len(selected)}): {selected}")
Advertencia
La correlación de Pearson solo captura relaciones lineales. Una variable puede tener una relación fuerte no lineal con la objetivo y aparecer con correlación cercana a cero. Para detectar relaciones no lineales, usa información mutua.
2.3 Chi-cuadrado
Evalúa la independencia entre variables categóricas. Se usa para variables de entrada categóricas vs. objetivo categórico. Ver la sección de estadística en Fundamentos Matemáticos para la derivación de la prueba.
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectKBest, chi2
from sklearn.preprocessing import MinMaxScaler
# Breast cancer: 30 features numéricas, objetivo binario (maligno/benigno)
data = load_breast_cancer()
X, y = data.data, data.y
# Chi-cuadrado requiere valores no negativos → escalar a [0, 1]
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
# Seleccionar las 10 mejores features
selector = SelectKBest(score_func=chi2, k=10)
X_new = selector.fit_transform(X_scaled, y)
# Ver qué features se seleccionaron
selected_mask = selector.get_support()
selected_features = [data.feature_names[i] for i, v in enumerate(selected_mask) if v]
scores = selector.scores_[selected_mask]
print("Top 10 features (chi²):")
for feat, score in sorted(zip(selected_features, scores), key=lambda x: -x[1]):
print(f" {feat:30s} χ² = {score:.1f}")
2.4 ANOVA (F-test)
El Análisis de Varianza (ANOVA) verifica si la media de la variable objetivo varía significativamente entre los diferentes valores de cada feature numérica. Se usa para features numéricas vs. objetivo categórico.
La prueba F compara la varianza entre grupos con la varianza dentro de los grupos:
Un valor F alto significa que la feature separa bien las clases.
from sklearn.datasets import load_iris
from sklearn.feature_selection import SelectKBest, f_classif
import pandas as pd
# Iris: 4 features numéricas, 3 clases
iris = load_iris()
X, y = iris.data, iris.target
selector = SelectKBest(score_func=f_classif, k='all')
selector.fit(X, y)
# Resultados
df_anova = pd.DataFrame({
'Feature': iris.feature_names,
'F-score': selector.scores_,
'p-value': selector.pvalues_
}).sort_values('F-score', ascending=False)
print("ANOVA F-test (Iris):")
print(df_anova.to_string(index=False))
# Feature F-score p-value
# petal width (cm) 804.129148 1.334898e-95
# petal length (cm) 1180.161182 2.834570e-91
# sepal length (cm) 119.263683 1.669499e-31
# sepal width (cm) 11.834376 1.534840e-05
# Para regresión (objetivo numérico): usar f_regression en lugar de f_classif
from sklearn.feature_selection import f_regression
from sklearn.datasets import fetch_california_housing
housing = fetch_california_housing()
X, y = housing.data, housing.target
selector = SelectKBest(score_func=f_regression, k='all')
selector.fit(X, y)
df_anova = pd.DataFrame({
'Feature': housing.feature_names,
'F-score': selector.scores_,
'p-value': selector.pvalues_
}).sort_values('F-score', ascending=False)
print("F-test para regresión (California Housing):")
print(df_anova.round(6).to_string(index=False))
# MedInc 1.84e+04 0.0
# AveRooms 2.95e+02 0.0
# HouseAge 3.15e+02 0.0
# ...
2.5 Información mutua
Mide cualquier tipo de dependencia (lineal o no lineal) entre variables. Es el método de filtrado más flexible. Para la definición formal de información mutua, ver Fundamentos Matemáticos.
from sklearn.datasets import fetch_california_housing
from sklearn.feature_selection import SelectKBest, mutual_info_regression
import pandas as pd
import matplotlib.pyplot as plt
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = housing.target
selector = SelectKBest(score_func=mutual_info_regression, k='all')
selector.fit(X, y)
df_mi = pd.DataFrame({
'Feature': X.columns,
'MI Score': selector.scores_
}).sort_values('MI Score', ascending=False)
print("Información mutua (California Housing):")
print(df_mi.round(4).to_string(index=False))
plt.figure(figsize=(8, 5))
plt.barh(df_mi['Feature'], df_mi['MI Score'])
plt.xlabel('Información mutua')
plt.title('MI: dependencia de cada feature con el precio')
plt.gca().invert_yaxis()
plt.tight_layout()
2.6 Resumen de métodos de filtrado
| Método | Tipo de dato de entrada | Tipo de objetivo | Relación detectada |
|---|---|---|---|
| Umbral de varianza | Numérico | No necesita | Casi constante |
| Pearson | Numérico | Numérico | Lineal |
| Spearman | Numérico | Numérico | Monótona |
| Chi-cuadrado | Categórico | Categórico | Cualquier |
| ANOVA (F-value) | Numérico | Categórico | Lineal entre grupos |
| F-test (regresión) | Numérico | Numérico | Lineal |
| Información mutua | Cualquiera | Cualquiera | Cualquier |
3 Métodos integrados (embebidos)
Los métodos integrados usan el propio modelo para evaluar la importancia de cada variable durante el entrenamiento. Son más precisos que los filtros porque consideran las interacciones entre variables.
3.1 Regresión Lasso (L1)
La regresión Lasso añade una penalización proporcional al valor absoluto de los pesos:
La penalización \(L1\) fuerza a que los pesos de las variables irrelevantes se hagan exactamente cero, eliminándolas automáticamente del modelo.
from sklearn.linear_model import LassoCV
lasso = LassoCV(cv=5, random_state=42)
lasso.fit(X_train, y_train)
# Variables con peso distinto de cero
importancias = pd.Series(np.abs(lasso.coef_), index=feature_names)
seleccionadas = importancias[importancias > 0].sort_values(ascending=False)
print(seleccionadas)
3.2 Importancia en árboles de decisión
Los árboles de decisión y Random Forest calculan feature_importances_ midiendo cuánto reduce la impureza cada variable en los nodos del árbol:
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=200, random_state=42)
rf.fit(X_train, y_train)
importancias = pd.Series(rf.feature_importances_, index=feature_names)
top_10 = importancias.sort_values(ascending=False).head(10)
print(top_10)
Tip
Consejo práctico: Los árboles de decisión tienden a sobreestimar la importancia de variables con muchos valores únicos. Para un ranking más confiable, usa permutación en lugar de la importancia nativa:
from sklearn.inspection import permutation_importance
result = permutation_importance(rf, X_test, y_test, n_repeats=10, random_state=42)
importancias_perm = pd.Series(result.importances_mean, index=feature_names)
print(importancias_perm.sort_values(ascending=False).head(10))
4 Métodos de envoltura
Los métodos de envoltura entrenan múltiples modelos candidatos con diferentes subconjuntos de variables y seleccionan el que mejor rendimiento obtiene. Son los más precisos pero también los más costosos computacionalmente.
4.1 Eliminación Recursiva de Características (RFE)
RFE entrena un modelo, elimina la variable menos importante, y repite hasta quedarse con el número deseado de variables:
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rfe = RFE(estimator=rf, n_features_to_select=10, step=1)
X_selected = rfe.fit_transform(X_train, y_train)
print("Seleccionadas:", rfe.support_)
print("Ranking:", rfe.ranking_)
4.2 Selección secuencial hacia adelante (SFS)
Empieza con cero variables y va añadiendo las que más mejoran el modelo:
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, random_state=42)
sfs = SequentialFeatureSelector(rf, n_features_to_select=10, direction='forward', cv=5)
X_selected = sfs.fit_transform(X_train, y_train)
4.3 Comparación de métodos
| Método | Coste computacional | Precisión | Riesgo de overfitting |
|---|---|---|---|
| Filtrado | Bajo | Media | Bajo |
| Integrado | Medio | Alta | Medio |
| Envoltura | Alto | Muy alta | Alto |
5 Ejemplo práctico: comparación completa
Comparación de todos los métodos usando California Housing (regresión, 8 features reales):
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import (
VarianceThreshold, SelectKBest, chi2, mutual_info_classif,
f_classif, f_regression, RFE
)
from sklearn.linear_model import LassoCV
from sklearn.preprocessing import MinMaxScaler
import numpy as np
import pandas as pd
# Dataset real
housing = fetch_california_housing()
X, y = housing.data, housing.target
names = housing.feature_names
# Escalar para chi-cuadrado (requiere no negativos)
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
rf = RandomForestRegressor(n_estimators=100, random_state=42)
# 1. Umbral de varianza
variances = X.var(axis=0)
threshold = np.percentile(variances, 25) # Q1
mask_var = variances > threshold
score_var = cross_val_score(rf, X[:, mask_var], y, cv=5).mean()
# 2. Pearson
corr = np.abs(pd.DataFrame(X).corrwith(pd.Series(y)))
mask_corr = corr > corr.quantile(0.5)
score_corr = cross_val_score(rf, X[:, mask_corr], y, cv=5).mean()
# 3. Chi-cuadrado (con datos escalados)
sel_chi2 = SelectKBest(chi2, k=5).fit(X_scaled, y)
score_chi2 = cross_val_score(rf, X[:, sel_chi2.get_support()], y, cv=5).mean()
# 4. ANOVA (F-test)
sel_anova = SelectKBest(f_regression, k=5).fit(X, y)
score_anova = cross_val_score(rf, X[:, sel_anova.get_support()], y, cv=5).mean()
# 5. Información mutua
sel_mi = SelectKBest(mutual_info_classif, k=5).fit(X_scaled, y)
score_mi = cross_val_score(rf, X[:, sel_mi.get_support()], y, cv=5).mean()
# 6. Lasso
lasso = LassoCV(cv=5).fit(X, y)
mask_lasso = np.abs(lasso.coef_) > 0
score_lasso = cross_val_score(rf, X[:, mask_lasso], y, cv=5).mean()
# 7. RFE
rfe = RFE(rf, n_features_to_select=5).fit(X, y)
score_rfe = cross_val_score(rf, X[:, rfe.get_support()], y, cv=5).mean()
print(f"{'Método':25s} {'R²':>6s} {'Features':>8s} Seleccionadas")
print("-" * 75)
print(f"{'Umbral varianza':25s} {score_var:.4f} {mask_var.sum():>8d} {names[mask_var].tolist()}")
print(f"{'Pearson':25s} {score_corr:.4f} {mask_corr.sum():>8d} {names[mask_corr].tolist()}")
print(f"{'Chi-cuadrado':25s} {score_chi2:.4f} {sel_chi2.get_support().sum():>8d} {names[sel_chi2.get_support()].tolist()}")
print(f"{'ANOVA (F-test)':25s} {score_anova:.4f} {sel_anova.get_support().sum():>8d} {names[sel_anova.get_support()].tolist()}")
print(f"{'Información mutua':25s} {score_mi:.4f} {sel_mi.get_support().sum():>8d} {names[sel_mi.get_support()].tolist()}")
print(f"{'Lasso (L1)':25s} {score_lasso:.4f} {mask_lasso.sum():>8d} {names[mask_lasso].tolist()}")
print(f"{'RFE':25s} {score_rfe:.4f} {rfe.support_.sum():>8d} {names[rfe.get_support()].tolist()}")
Tip
Regla práctica: Empieza siempre por un método de filtrado o integrado (rápido y razonable). Si necesitas máxima precisión y tienes recursos computacionales, usa RFE o SFS como paso final. Y valida siempre con cross-validation: el mejor método es el que produce el mejor rendimiento en tu problema concreto.
6 Resumen
| Tipo | Método | Ideal para |
|---|---|---|
| Filtrado | Umbral de varianza | Eliminar features casi constantes |
| Filtrado | Pearson, Spearman | Primera exploración, relaciones lineales |
| Filtrado | Chi-cuadrado, ANOVA | Variables categóricas, clasificación |
| Filtrado | Información mutua | Cualquier tipo de relación |
| Integrado | Lasso, árboles, permutación | Modelos lineales o basados en árboles |
| Envoltura | RFE, SFS | Máxima precisión, pocos recursos |
La selección de características es un paso obligatorio antes de entrenar cualquier modelo con muchas variables. No hay un único método ganador: la mejor estrategia es combinar varios y validar con cross-validation.