Feature Engineering Automatizado e Interpretable con Programación Genética#
Contexto del Problema#
En la industria del Machine Learning, existe un dilema clásico: Interpretabilidad vs. Rendimiento.
Los modelos lineales (Regresión Logística, Ridge) son rápidos, auditables y explicables (ideales para medicina o finanzas), pero fallan si las variables tienen relaciones no lineales complejas.
Los ensambles (Random Forest, XGBoost) capturan estas relaciones no lineales a la perfección, pero son “cajas negras” difíciles de auditar.
Objetivos y Restricciones#
Objetivo principal: Utilizar Programación Genética (GP) para descubrir automáticamente transformaciones matemáticas no lineales (ej. \(x_1 / \sqrt{x_2 + x_3}\)). Estas nuevas variables actuarán como “atajos matemáticos” que potenciarán el rendimiento de los modelos predictivos, manteniendo la trazabilidad exacta de cómo se calculó la variable.
Restricciones:
El problema del Bloat: Las ecuaciones generadas no pueden crecer infinitamente; deben ser legibles y auditables.
Redundancia: Debemos minimizar la generación de variables que sean matemáticamente equivalentes entre sí.
Costo Computacional: La búsqueda debe ser lo suficientemente eficiente para ejecutarse como un paso de preprocesamiento (EDA avanzado).
Prerrequisitos#
Haber completado el capítulo de machine learning clásico.
Haber completado el notebook Programación Genética (GP): Evolucionando Estructuras y Ecuaciones
Configuración del Entorno y Arquitectura del Framework#
Para mantener este notebook enfocado en la aplicación, encapsulamos el motor de Programación Genética en la siguiente celda. Este framework implementa la estrategia HFSR (Hall of Fame Secuencial con Penalización de Redundancia), control de bloat y paralelización.
Diseño de la Arquitectura: El Framework HFSR#
Si le pedimos a un Algoritmo Genético que genere 5 características a la vez, la población entera convergerá hacia la misma fórmula matemática (la que dé el mejor fitness). Terminaríamos con 5 variables idénticas.
Para evitar esto, nuestro framework implementa HFSR (Hall of Fame Secuencial con Penalización de Redundancia).
Evolucionamos 1 característica y la guardamos en el Hall of Fame (HoF).
Evolucionamos la 2da característica, pero penalizamos su correlación con la 1ra.
Repetimos el proceso.
La función de fitness maestra que evalúa cada árbol \(T\) es: $\( \text{Fitness}(T) = MI(\varphi_T(X), y) - \beta \cdot \max_j \text{Corr}(\varphi_T(X), \text{HoF}_j(X)) - \gamma \cdot |T| \)$
\(MI\) (Información Mutua): Captura qué tanto explica la nueva variable a la variable objetivo \(y\) (incluso relaciones no lineales).
\(\beta\) (Redundancia): Penaliza si la nueva variable se parece mucho a las que ya descubrimos.
\(\gamma\) (Parsimonia): Penaliza el tamaño del árbol \(|T|\) para evitar el Bloat (ecuaciones gigantes e incomprensibles).
Caso de Estudio 1: Prueba de con Friedman #1#
Antes de confiar en el algoritmo con datos reales, realizamos una “Prueba de Cordura” (Sanity Check). Usamos el dataset sintético Friedman #1, cuya función generadora es conocida:
Solo \(x_0, x_1, x_2, x_3, x_4\) son relevantes. El resto (\(x_5 \dots x_9\)) son ruido puro.
Existen interacciones multiplicativas (\(x_0 \cdot x_1\)) y cuadráticas (\(x_2^2\)).
Hipótesis: El GP debería descubrir exactamente estas interacciones matemáticas, obteniendo nuevas variables más informativas.
# 1. Generamos el dataset de Friedman
X_fried, y_fried = make_friedman1(n_samples=500, n_features=10, noise=0.5, random_state=SEED)
feat_names_fried = [f'x{i}' for i in range(10)]
# 2. Baseline: Regresión Lineal (Ridge) con los features originales
cv5 = KFold(5, shuffle=True, random_state=SEED)
baseline_fried = cross_val_score(Ridge(), X_fried, y_fried, scoring='r2', cv=cv5)
print(f"Baseline R² (Ridge, features originales): {np.mean(baseline_fried):.4f}")
# 3. Ejecutamos el GP Feature Engineer
config_fried = GPConfig(
population_size=350,
n_generations=60,
n_features_to_generate=3,
function_set='extended', # Incluimos multiplicaciones y cuadrados
fitness_metric='mutual_info',
redundancy_beta=0.30,
parsimony_coeff=0.003,
max_tree_height=7,
task_type='regression',
verbose=True
)
print("\nEvolucionando nuevas características con GP... (Esto tomará unos segundos)")
engineer_fried = GPFeatureEngineer(config=config_fried)
engineer_fried.fit(X_fried, y_fried)
r_fried = engineer_fried.result_
Baseline R² (Ridge, features originales): 0.7531
Evolucionando nuevas características con GP... (Esto tomará unos segundos)
═════════════════════════════════════════════════════════════════
GP Feature Engineering | n_features_in=10
Generando 3 features nuevos
Primitivas: extended | Métrica: mutual_info
β-redundancia: 0.3 | Parsimonia: 0.003
─────────────────────────────────────────────────────────────────
Baseline CV Score (r2): 0.7531
─────────────────────────────────────────────────────────────────
[Feature 1/3]
Gen 0 | BestFit=0.5486 | MeanFit=0.0885 | MeanNodes=5.3
Gen 10 | BestFit=0.8090 | MeanFit=0.4930 | MeanNodes=11.7
Gen 20 | BestFit=0.8951 | MeanFit=0.5541 | MeanNodes=15.3
Gen 30 | BestFit=0.8951 | MeanFit=0.6368 | MeanNodes=15.8
→ Early stopping en gen 38 (estancamiento)
Expr: mul(x0, mul(x1, add(add(x3, x4), sq(add(x3, sq(add(x3, x4)))))))
MI=0.9401 | Redund=0.000 | Nodes=15 | Gen=18
[Feature 2/3]
Gen 0 | BestFit=0.5951 | MeanFit=0.3762 | MeanNodes=15.7
Gen 10 | BestFit=0.6108 | MeanFit=0.3511 | MeanNodes=14.9
Gen 20 | BestFit=0.6108 | MeanFit=0.3605 | MeanNodes=14.4
→ Early stopping en gen 23 (estancamiento)
✗ Descartado: redundancia=0.999 ≥ 0.95
→ Incluyendo de todas formas (sin alternativa viable)
Expr: mul(x0, mul(x1, add(sqrt(x3), sq(add(x3, sq(add(x3, x4)))))))
MI=0.9526 | Redund=0.999 | Nodes=14 | Gen=3
[Feature 3/3]
Gen 0 | BestFit=0.6106 | MeanFit=0.3538 | MeanNodes=14.5
Gen 10 | BestFit=0.6106 | MeanFit=0.3772 | MeanNodes=14.9
→ Early stopping en gen 19 (estancamiento)
✗ Descartado: redundancia=1.000 ≥ 0.95
→ Incluyendo de todas formas (sin alternativa viable)
Expr: mul(x0, mul(x1, add(sqrt(x3), sq(add(x3, sq(add(x3, x4)))))))
MI=0.9526 | Redund=1.000 | Nodes=14 | Gen=0
═════════════════════════════════════════════════════════════════
GP Feature Engineering — Resultado
═════════════════════════════════════════════════════════════════
Dataset original : (500, 10)
Dataset transformado : (500, 13)
Features generados : 3
Scoring : r2
Baseline CV Score : 0.7531
Augmented CV Score : 0.7482
Mejora : -0.0048 (-0.6%)
Evaluaciones totales : 12280
Tiempo total : 166.28s
─────────────────────────────────────────────────────────────────
Expresiones simbólicas generadas:
[0] MI=0.9401 | Nodes=15 | mul(x0, mul(x1, add(add(x3, x4), sq(add(x3, sq(add(x3, x4)))))))
[1] MI=0.9526 | Nodes=14 | mul(x0, mul(x1, add(sqrt(x3), sq(add(x3, sq(add(x3, x4)))))))
[2] MI=0.9526 | Nodes=14 | mul(x0, mul(x1, add(sqrt(x3), sq(add(x3, sq(add(x3, x4)))))))
═════════════════════════════════════════════════════════════════
# 4. Análisis de las Ecuaciones Descubiertas
print("\n--- Ecuaciones Descubiertas por el GP ---")
for gf in r_fried.generated_features:
# Analizamos si el GP descubrió la verdad subyacente
uses_x0x1 = 'x0' in gf.expression and 'x1' in gf.expression
uses_x2 = 'x2' in gf.expression
uses_noise = any(f'x{i}' in gf.expression for i in range(5, 10))
tags = []
if uses_x0x1: tags.append("✓ Interacción x0·x1 descubierta")
if uses_x2: tags.append("✓ Relación de x2 descubierta")
if uses_noise: tags.append("CUIDADO: Incluye ruido")
print(f"Feature [{gf.feature_index}]: {gf.expression[:60]}")
if tags: print(f" -> {' | '.join(tags)}")
--- Ecuaciones Descubiertas por el GP ---
Feature [0]: mul(x0, mul(x1, add(add(x3, x4), sq(add(x3, sq(add(x3, x4)))
-> ✓ Interacción x0·x1 descubierta
Feature [1]: mul(x0, mul(x1, add(sqrt(x3), sq(add(x3, sq(add(x3, x4))))))
-> ✓ Interacción x0·x1 descubierta
Feature [2]: mul(x0, mul(x1, add(sqrt(x3), sq(add(x3, sq(add(x3, x4))))))
-> ✓ Interacción x0·x1 descubierta
Comparativa de Modelos#
Veamos si nuestras nuevas características lograron mejorar el rendimiento predictivo. Evaluamos tanto un modelo lineal (Ridge) como un ensamble no lineal (RandomForest).
print("\n--- Comparación de Estrategias en Friedman #1 ---")
print(f"{'Estrategia':35s} | {'R² (5-CV)':>10}")
print("─" * 50)
# 1. Ridge baseline
print(f"{'Ridge (features originales)':35s} | {np.mean(baseline_fried):10.4f}")
# 2. Ridge + GP features
X_aug_fried = engineer_fried.transform(X_fried)
s_aug_ridge = cross_val_score(Ridge(), X_aug_fried, y_fried, scoring='r2', cv=cv5)
print(f"{'Ridge + GP features':35s} | {np.mean(s_aug_ridge):10.4f}")
# 3. Random Forest Baseline
rf = RandomForestRegressor(100, random_state=SEED)
s_rf_base = cross_val_score(rf, X_fried, y_fried, scoring='r2', cv=cv5)
print(f"{'RandomForest (features orig)':35s} | {np.mean(s_rf_base):10.4f}")
# 4. Random Forest + GP features
s_rf_aug = cross_val_score(rf, X_aug_fried, y_fried, scoring='r2', cv=cv5)
print(f"{'RandomForest + GP features':35s} | {np.mean(s_rf_aug):10.4f}")
--- Comparación de Estrategias en Friedman #1 ---
Estrategia | R² (5-CV)
──────────────────────────────────────────────────
Ridge (features originales) | 0.7531
Ridge + GP features | 0.7482
RandomForest (features orig) | 0.8289
RandomForest + GP features | 0.8807
Diagnóstico de Modelos:
Observar los resultados empíricos con atención. El modelo Ridge no mejoró (incluso bajó ligeramente de 0,753 a 0,748). ¿Por qué? Porque el GP generó características altamente correlacionadas entre sí (redundancia \(\ge 0,95\)). Los modelos lineales asumen independencia entre variables; al inyectarles multicolinealidad, su rendimiento se degrada.
Sin embargo, mirar el RandomForest. Por sí solo, alcanzó un \(R^2\) de 0,828. Pero cuando lo alimentamos con las ecuaciones descubiertas por el GP, ¡salta a 0,881!. Los árboles de decisión son pésimos aproximando funciones matemáticas suaves (como \(x_0 \cdot x_1\)), necesitan cientos de divisiones para simular una curva. El GP le dio al Random Forest el “atajo matemático” exacto que necesitaba, demostrando una sinergia espectacular entre la computación evolutiva y el Machine Learning tradicional.
Caso de Estudio 2: Datos Reales (Diabetes)#
Aplicamos el framework a un problema médico real (Dataset de Diabetes), donde la interpretabilidad no es un lujo, sino un requisito legal.
ds_diabetes = load_diabetes(as_frame=True)
X_diab, y_diab = ds_diabetes.data, ds_diabetes.target
config_diab = GPConfig(
population_size=300,
n_generations=50,
n_features_to_generate=5,
function_set='extended',
fitness_metric='mutual_info',
redundancy_beta=0.35,
parsimony_coeff=0.002,
max_tree_height=6,
task_type='regression',
verbose=False
)
engineer_diab = GPFeatureEngineer(config=config_diab, estimator=Ridge(alpha=1.0))
print("Evolucionando features para Diabetes...")
engineer_diab.fit(X_diab, y_diab)
r_diab = engineer_diab.result_
print(f"\nBaseline R² (Ridge): {r_diab.baseline_cv_score:.4f}")
print(f"Augmented R² (Ridge): {r_diab.augmented_cv_score:.4f}")
print(f"Mejora absoluta: {r_diab.score_improvement:+.4f}")
Evolucionando features para Diabetes...
Baseline R² (Ridge): 0.4093
Augmented R² (Ridge): 0.4129
Mejora absoluta: +0.0036
Visualización: Información Mutua (MI)#
¿Realmente los features inventados por el GP son mejores que los que midieron los médicos?
# Usamos el plotter integrado en el framework
GPPlotter.plot_mi_comparison(r_diab, X_diab.values, y_diab.values)
La gráfica demuestra que el GP es capaz de sintetizar variables (ej. gp_0) que tienen una correlación no lineal (Información Mutua) con la progresión de la diabetes muy superior a cualquier variable original aislada.
Interpretación Matemática con SymPy#
Afirmamos que en el sector médico la interpretabilidad es un requisito legal. Un médico jamás confiará en un modelo de “Caja Negra” que simplemente escupe un diagnóstico. Pero, ¿confiará en un árbol Lisp que dice mul(x2, add(x8, log(x2)))? Tampoco.
Un último paso importante es traducir la salida del algoritmo al lenguaje del negocio. Tomamos la mejor característica descubierta por el GP, y usamos la librería de álgebra computacional SymPy para simplificar la ecuación y presentarla en un formato matemático tradicional.
import sympy as sp
# 1. Tomamos el mejor feature generado por el GP para Diabetes
mejor_feature_diab = r_diab.generated_features[0]
expr_cruda = mejor_feature_diab.expression
print(f"1. Salida Cruda del GP (Formato Lisp):")
print(f" {expr_cruda}\n")
# 2. Mapeamos las primitivas de DEAP a funciones matemáticas de SymPy
mapeo_sympy = {
'add': lambda a, b: a + b,
'sub': lambda a, b: a - b,
'mul': lambda a, b: a * b,
'div': lambda a, b: a / b, # Asumimos división ideal para la interpretación humana
'log': lambda a: sp.log(a),
'sqrt': lambda a: sp.sqrt(a),
'sq': lambda a: a**2,
'cube': lambda a: a**3,
'neg': lambda a: -a,
'abs': lambda a: sp.Abs(a),
'sigmoid': lambda a: 1 / (1 + sp.exp(-a)),
'relu': lambda a: sp.Max(0, a)
}
# 3. Mapeamos los genes (x0, x1...) a los nombres reales del dataset
nombres_clinicos = ds_diabetes.feature_names
# Para cuando se optimiza utilizando una matriz de numpy, los features originales se nombran (x0, x1...)
# Lo dejo a mano por si se experimenta con otros datos
# simbolos_reales = {f'x{i}': sp.Symbol(nombre) for i, nombre in enumerate(nombres_clinicos)}
# En este caso, ingresamos con un dataframe, por lo tanto, se adoptan los nombre reales en los genes
simbolos_reales = {nombre: sp.Symbol(nombre) for nombre in nombres_clinicos}
# Unimos ambos diccionarios para el entorno de evaluación
entorno_evaluacion = {**mapeo_sympy, **simbolos_reales}
try:
# 4. Evaluamos el string Lisp convirtiéndolo en un objeto SymPy
expr_sympy = eval(expr_cruda, {"__builtins__": None}, entorno_evaluacion)
# 5. Simplificamos algebraicamente la ecuación
expr_limpia = sp.simplify(expr_sympy)
print(f"2. Ecuación Traducida y Simplificada para el Negocio:")
print(f" Nueva_Variable = {expr_limpia}")
except Exception as e:
print(f"No se pudo simplificar la expresión: {e}")
1. Salida Cruda del GP (Formato Lisp):
neg(add(div(div(div(add(s5, bmi), -0.7354444220376792), log(1.4774699372826094)), log(add(log(1.4774699372826094), add(1.4774699372826094, s2)))), bmi))
2. Ecuación Traducida y Simplificada para el Negocio:
Nueva_Variable = (-bmi*log(s2 + 1.86780106035688) + 3.48350918857291*bmi + 3.48350918857291*s5)/log(s2 + 1.86780106035688)
expr_limpia
Observar la salida simplificada. Aunque la ecuación resultante (ej. (-bmi*log(s2 + 1.86) + 3.48*bmi + 3.48*s5)/log(s2 + 1.86)) pueda parecer compleja, es estrictamente determinista y auditable.
En lugar de decirle a un auditor médico “el modelo usa una red neuronal de caja negra”, podemos presentar la fórmula matemática exacta que relaciona el Índice de Masa Corporal (bmi) con los marcadores séricos (s2, s5).
El médico puede tomar esta fórmula, graficarla, someterla a pruebas de estrés, contrastarla con la literatura médica existente, validarla clínicamente y aprobar su uso en producción cumpliendo con las normativas legales. Acabamos de convertir un problema de Machine Learning en un descubrimiento científico auditable.
Ablation Study: ¿Cuántos features necesitamos?#
Generamos 5 features, pero ¿necesitamos los 5? En ingeniería, menos es más.
print("Ablation study: R² vs número de features GP agregados")
print(f"{'N features GP':>15} | {'R² (5-CV)':>12} | {'Δ vs baseline':>15}")
print("─" * 50)
baseline_r2 = cross_val_score(Ridge(), X_diab, y_diab, scoring='r2', cv=cv5)
print(f"{'0 (baseline)':>15} | {np.mean(baseline_r2):12.4f} | {'—':>15}")
for n_gp in range(1, len(r_diab.generated_features)+1):
gp_vals = np.column_stack([r_diab.generated_features[i].values for i in range(n_gp)])
X_aug = np.hstack([X_diab.values, gp_vals])
scores = cross_val_score(Ridge(), X_aug, y_diab, scoring='r2', cv=cv5)
delta = np.mean(scores) - np.mean(baseline_r2)
print(f"{n_gp:>15} | {np.mean(scores):12.4f} | {delta:+15.4f}")
Ablation study: R² vs número de features GP agregados
N features GP | R² (5-CV) | Δ vs baseline
──────────────────────────────────────────────────
0 (baseline) | 0.4093 | —
1 | 0.4625 | +0.0532
2 | 0.4180 | +0.0087
3 | 0.4174 | +0.0081
4 | 0.4161 | +0.0068
5 | 0.4129 | +0.0036
Conclusión: Generalmente, el primer y segundo feature aportan el 90% del valor. A partir del tercero, entramos en la ley de rendimientos decrecientes.
Discusión de Ingeniería: Peligro de la Multicolinealidad
Observar detenidamente la tabla de resultados del Ablation Study.
Con 0 features GP (Baseline), el \(R^2\) es \(0,4093\).
Al agregar 1 feature GP, el rendimiento se dispara a \(0,4625\) (mejora del +0,05).
Pero al agregar el 2do, 3ro, 4to y 5to feature, el rendimiento cae progresivamente hasta volver a \(0,4129\).
¿Qué pasó acá? Acabamos de presenciar el problema clásico de la Multicolinealidad. Los modelos lineales (como Ridge simple que estamos usando) son extremadamente sensibles a variables que comparten información. Aunque usamos el parámetro \(\beta\) para forzar diversidad, los features generados por el GP inevitablemente tienen cierta correlación entre sí y con las variables originales. Al inyectar 5 variables complejas de golpe, asfixiamos al modelo lineal con información redundante.
Criterio Profesional: Esta es la prueba empírica irrefutable de por qué el GP Feature Engineer no debe usarse solo. Necesitamos el paso de Feature Selection (como el del notebook GA como Wrapper para Machine Learning (Feature Selection y HPO)) para que evalúe este dataset aumentado y elimine la basura y la redundancia, quedándose solo con ese “Feature 1” que realmente aportaba valor.
Análisis de Sensibilidad: El Parámetro \(\beta\) (redundancia)#
El parámetro \(\beta\) es el corazón de nuestra estrategia HFSR. Controla cuánto penalizamos a un árbol si se parece a los que ya descubrimos. Vemos empíricamente cómo afecta la diversidad de las ecuaciones generadas:
betas = [0.0, 0.30, 0.70]
print(f"{'β':>6} | {'Expresiones Únicas':>20} | {'R² Augmented':>15}")
print("─" * 45)
for beta_val in betas:
cfg_b = GPConfig(population_size=100, n_generations=20, n_features_to_generate=3,
redundancy_beta=beta_val, verbose=False)
eng_b = GPFeatureEngineer(config=cfg_b)
eng_b.fit(X_diab.values, y_diab.values)
exprs = [gf.expression for gf in eng_b.result_.generated_features]
n_unique = len(set(exprs))
print(f"{beta_val:6.2f} | {n_unique:20d} | {eng_b.result_.augmented_cv_score:15.4f}")
β | Expresiones Únicas | R² Augmented
─────────────────────────────────────────────
0.00 | 1 | 0.4693
0.30 | 3 | 0.4688
0.70 | 3 | 0.4495
¿Qué dicen estos valores?:
\(\beta = 0.0\) (R² \(\approx 0.469\), Únicas = 1): Sin penalización, el GP es perezoso. Encontró una buena ecuación y generó 3 copias exactas de la misma. No hay diversidad.
\(\beta = 0.3\) (R² \(\approx 0.468\), Únicas = 3): El Sweet Spot. El GP se vio forzado a inventar 3 ecuaciones matemáticamente distintas, manteniendo prácticamente el mismo poder predictivo.
\(\beta = 0.7\) (R² \(\approx 0.449\), Únicas = 3): Alta penalización. Forzamos tanta diversidad que el GP tuvo que recurrir a ecuaciones subóptimas o ruidosas solo para no parecerse a las anteriores, degradando el rendimiento final del modelo.
Conclusión: Un valor de \(\beta \approx 0.3\) garantiza un banco de características diverso y rico en información, ideal para pasarlo a la siguiente etapa de un buen pipeline (Selección de Features).
El Pipeline Definitivo#
Para llevar esto a producción, nuestro GPFeatureEngineer fue diseñado heredando de TransformerMixin de scikit-learn. Esto significa que podemos insertarlo directamente en un Pipeline estándar, combinando la creación de features (GP) con la selección de features y el modelo final.
from sklearn.pipeline import Pipeline
# 1. Configuramos el GP (Creador de Features), 1 solamente para velocidad y asentar el ejemplo
config_pipe = GPConfig(n_features_to_generate=1, verbose=False)
# 2. Ensamblamos el Pipeline Híbrido
pipe = Pipeline([
('scaler', StandardScaler()),
('gp_engineer', GPFeatureEngineer(config=config_pipe)),
# Acá podríamos insertar un Selector de Features, debe ser compatible con pipelines...
# Eliminar colinealidad si queremos usar Rigde, etc etc. según el tipo de modelo
('ridge', Ridge(alpha=1.0)),
])
# 3. Entrenamiento y Evaluación End-to-End
print("Entrenando Pipeline completo (Scaler -> GP -> Ridge)...")
cv_pipe = cross_val_score(pipe, X_diab, y_diab, scoring='r2', cv=KFold(3, shuffle=True, random_state=SEED))
print(f"Pipeline CV R²: {np.mean(cv_pipe):.4f} ± {np.std(cv_pipe):.4f}")
Entrenando Pipeline completo (Scaler -> GP -> Ridge)...
Pipeline CV R²: 0.4784 ± 0.0388
Conseguimos un pequeño plus, el \(R^2\) ahora es 0,478. Al integrar el GP dentro de un pipeline formal con StandardScaler, estabilizamos numéricamente las entradas, permitiendo que el modelo Ridge aproveche al máximo las nuevas características. Resta mucho por hacer, pero de los capítulos anteriores deberían de tener las bases para un pipeline robusto y efectivo, incorporando “lo nuevo” que es delegar la parte dura del FE a un algoritmo genético.
NOTA: una implementación avanzada (y compatible con Pipeline) del Selector de Features con AG implementado de manera minimalista en el notebook GA como Wrapper para Machine Learning (Feature Selection y HPO), se encuentra en: evo-suite
Lecciones de Ingeniería y Limitaciones#
Costo Computacional: La Programación Genética es costosa. Su complejidad es \(O(\text{features} \times \text{generaciones} \times \text{población} \times \text{muestras})\). No se ejecuta en cada iteración de entrenamiento, sino una sola vez durante la fase de descubrimiento de datos (EDA avanzado).
Overfitting de Features: Al generar miles de árboles, es posible que el GP encuentre una fórmula que se ajuste perfectamente al ruido del set de entrenamiento. Es importante seleccionar los features útiles, o incorporar un modelo que lo haga de manera nativa. Incluso con todo esto, es vital evaluar el pipeline final en un conjunto de Test estrictamente separado (lo que venimos mencionando desde siempre).
El Poder de la Caja Blanca: Demostramos que no siempre necesitamos sacrificar la interpretabilidad usando Redes Neuronales o XGBoost. Con GP, podemos dotar a modelos lineales simples de la capacidad de entender el mundo no lineal, manteniendo las ecuaciones a la vista de todos.
Entorno de Ejecución#
from utils.environment import environment_table
environment_table()
| Package | Version |
|---|---|
| Python | 3.12.13 |
| Platform | Linux-6.6.122+-x86_64-with-glibc2.35 |
| IPython | 7.34.0 |
| deap | 1.4 |
| ipywidgets | 7.7.1 |
| joblib | 1.5.3 |
| matplotlib | 3.10.0 |
| numpy | 2.0.2 |
| pandas | 2.2.2 |
| scipy | 1.16.3 |
| seaborn | 0.13.2 |
| sklearn | 1.5.3 |
| statsmodels | 0.14.6 |