V8.0 Final — Desde V6.0¶
Base metodológica: V6.0 Regresión Logística con menos variables.
Extensión V8.0 Final: laboratorio de modelos challenger, optimización de hiperparámetros, ranking con F1-Score, curvas completas, robustez, Top-K comparativo y Random Forest Causal con AIPW/Double Robust.
La V6.0 se conserva como benchmark interpretable. La V8.0 selecciona automáticamente un modelo campeón bajo validación temporal y métricas de negocio.
00_Config¶
# ======================================================================================
# 0. INSTALACIÓN / IMPORTS / CONFIGURACIÓN GENERAL
# ======================================================================================
# Este notebook está diseñado para Google Colab + BigQuery.
# Si alguna librería no está disponible, se intenta instalar de forma controlada.
import sys
import os
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
try:
from google.colab import auth
IN_COLAB = True
except Exception:
IN_COLAB = False
try:
from google.cloud import bigquery
except Exception:
!pip -q install google-cloud-bigquery pyarrow db-dtypes
from google.cloud import bigquery
# Estilo gráfico sobrio y apto para presentación ejecutiva.
plt.rcParams["figure.figsize"] = (10, 5)
plt.rcParams["axes.grid"] = True
plt.rcParams["axes.spines.top"] = False
plt.rcParams["axes.spines.right"] = False
plt.rcParams["font.size"] = 10
RANDOM_STATE = 42
TARGET = "reclamo_comercial"
OUTPUT_DIR = Path("/content/capstone_outputs_V6_0_regresion_logistica_menos_variables")
GRAF_DIR = OUTPUT_DIR / "graficos"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
GRAF_DIR.mkdir(parents=True, exist_ok=True)
print("Output dir:", OUTPUT_DIR)
print("Random state:", RANDOM_STATE)
def print_step(title):
print("\n" + "="*100)
print(title)
print("="*100)
def savefig(name):
path = GRAF_DIR / name
plt.tight_layout()
plt.savefig(path, dpi=160, bbox_inches="tight")
print("[GRAFICO] Guardado:", path)
plt.show()
def safe_rate(num, den):
return np.where(np.asarray(den) == 0, 0, np.asarray(num) / np.asarray(den))
# Librerías ML / estadística para V5.0
try:
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.metrics import (
confusion_matrix, classification_report, roc_auc_score, average_precision_score,
precision_recall_curve, roc_curve, brier_score_loss, accuracy_score,
precision_score, recall_score, f1_score, balanced_accuracy_score, matthews_corrcoef,
cohen_kappa_score
)
except Exception:
!pip -q install scikit-learn statsmodels
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.metrics import (
confusion_matrix, classification_report, roc_auc_score, average_precision_score,
precision_recall_curve, roc_curve, brier_score_loss, accuracy_score,
precision_score, recall_score, f1_score, balanced_accuracy_score, matthews_corrcoef,
cohen_kappa_score
)
try:
import statsmodels.api as sm
HAS_STATSMODELS = True
except Exception:
HAS_STATSMODELS = False
VERSION_MODELO = "V6.0 — Regresión Logística Experta con Menos Variables"
SHAP_ACTIVO = False
print("Versión:", VERSION_MODELO)
print("SHAP activo:", SHAP_ACTIVO)
# Parámetros económicos expertos para optimización de umbral y selección de modelo.
COSTO_RECLAMO_USD = 60000 # valor central del rango observado USD 40k–80k.
COSTO_REVISION_FP_USD = 1500 # costo operacional de revisar/bloquear una falsa alarma.
BETA_F = 2 # F2 prioriza Recall sobre Precision.
MIN_RECALL_VALIDACION_DESEADO = 0.30
print("Parámetros económicos V5.1:", COSTO_RECLAMO_USD, COSTO_REVISION_FP_USD)
# Parámetros V6.0: menos variables y laboratorio de modelos.
MAX_VARIABLES_V6 = 18
MIN_VARIABLES_V6 = 8
TOPK_GRID = [0.01, 0.02, 0.03, 0.05, 0.075, 0.10, 0.15, 0.20]
THRESHOLD_GRID = np.array([0.01, 0.02, 0.03, 0.04, 0.05, 0.075, 0.10, 0.125, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50])
try:
from sklearn.metrics import fbeta_score
except Exception:
pass
print("Máximo variables finales V6.0:", MAX_VARIABLES_V6)
Output dir: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables Random state: 42 Versión: V6.0 — Regresión Logística Experta con Menos Variables SHAP activo: False Parámetros económicos V5.1: 60000 1500 Máximo variables finales V6.0: 18
01_Carga_BigQuery¶
# ======================================================================================
# 1. AUTENTICACIÓN Y CARGA ROBUSTA DE TABLAS BIGQUERY
# ======================================================================================
print_step("AUTENTICACIÓN Y CARGA DE DATOS BIGQUERY")
# En Colab, esto solicitará autorización de Google.
if IN_COLAB:
try:
auth.authenticate_user()
print("[OK] Autenticación Google completada.")
except Exception as e:
print("[WARNING] No se pudo autenticar automáticamente:", e)
# Ajustar si tu proyecto cambia.
PROJECT_ID = "capstone-ml-clasificacion"
DATASET_ID = "capstone_ml_ds"
LOCATION = "US"
client = bigquery.Client(project=PROJECT_ID, location=LOCATION)
# Diccionario completo esperado para V4.0.
# Si alguna tabla no existe, no se detiene la auditoría; se deja documentado.
TABLES = {
# 1) Tablas operacionales principales
"dim": "dim_cuarteles",
"clima": "fact_clima",
"cosecha": "fact_cosecha",
"expo": "fact_exportacion",
"insp": "fact_inspeccion_destino",
# 2) Tablas documentales reales de reclamos
"reclamo_documento_cabecera": "fact_reclamo_documento_cabecera",
"reclamo_factura_detalle": "fact_reclamo_factura_detalle",
"reclamo_nota_credito_detalle": "fact_reclamo_nota_credito_detalle",
"reclamo_ff_detalle": "fact_reclamo_ff_detalle",
# 3) Tablas HAB / calidad
"hab_parametro": "dim_hab_parametro_calidad",
"hab_defecto": "dim_hab_defecto_calidad",
"hab_causa_defecto": "bridge_hab_causa_defecto",
"hab_protocolo": "dim_hab_protocolo_operacional",
"hab_etapa": "dim_hab_etapa_cadena",
"hab_principio": "dim_hab_principio_gestion_calidad",
}
# Listar tablas reales del dataset para validar nombres y evitar errores 404 silenciosos.
try:
tables_real = [t.table_id for t in client.list_tables(f"{PROJECT_ID}.{DATASET_ID}")]
print("Tablas disponibles en BigQuery:")
for t in sorted(tables_real):
print(" -", t)
except Exception as e:
tables_real = []
print("[WARNING] No se pudo listar tablas del dataset:", e)
# Carga robusta: si una tabla no existe, se crea DataFrame vacío y continúa.
data = {}
load_audit = []
for alias, table_name in TABLES.items():
full_table = f"`{PROJECT_ID}.{DATASET_ID}.{table_name}`"
print(f"\nCargando {alias}: {table_name}...")
try:
df_tmp = client.query(f"SELECT * FROM {full_table}").to_dataframe()
data[alias] = df_tmp
print(f"[OK] {alias}: {df_tmp.shape}")
load_audit.append({"alias": alias, "tabla": table_name, "estado": "OK", "filas": df_tmp.shape[0], "columnas": df_tmp.shape[1], "error": ""})
except Exception as e:
data[alias] = pd.DataFrame()
print(f"[WARNING] No se pudo cargar {table_name}: {e}")
load_audit.append({"alias": alias, "tabla": table_name, "estado": "ERROR", "filas": 0, "columnas": 0, "error": str(e)[:300]})
load_audit_df = pd.DataFrame(load_audit)
display(load_audit_df)
load_audit_df.to_csv(OUTPUT_DIR / "auditoria_carga_tablas.csv", index=False)
# Variables cómodas para usar en el resto del notebook.
dim = data.get("dim", pd.DataFrame())
clima = data.get("clima", pd.DataFrame())
cosecha = data.get("cosecha", pd.DataFrame())
expo = data.get("expo", pd.DataFrame())
insp = data.get("insp", pd.DataFrame())
print("\nLectura metodológica:")
print("- Las tablas operacionales se usan para explicar origen, clima, cosecha, packing y logística.")
print("- La inspección destino entrega el target oficial reclamo_comercial.")
print("- Las tablas documentales y HAB se usan para auditoría, explicación y evidencia, no como predictores pre-despacho.")
==================================================================================================== AUTENTICACIÓN Y CARGA DE DATOS BIGQUERY ==================================================================================================== [OK] Autenticación Google completada. Tablas disponibles en BigQuery: - bridge_hab_causa_defecto - dim_cuarteles - dim_hab_defecto_calidad - dim_hab_etapa_cadena - dim_hab_parametro_calidad - dim_hab_principio_gestion_calidad - dim_hab_protocolo_operacional - dim_reclamo_caso_cliente - fact_clima - fact_cosecha - fact_exportacion - fact_inspeccion_destino - fact_reclamo_documento_cabecera - fact_reclamo_factura_detalle - fact_reclamo_ff_detalle - fact_reclamo_nota_credito_detalle Cargando dim: dim_cuarteles... [OK] dim: (144, 46) Cargando clima: fact_clima... [OK] clima: (14796, 26) Cargando cosecha: fact_cosecha... [OK] cosecha: (60480, 77) Cargando expo: fact_exportacion... [OK] expo: (58944, 90) Cargando insp: fact_inspeccion_destino... [OK] insp: (14736, 24) Cargando reclamo_documento_cabecera: fact_reclamo_documento_cabecera... [OK] reclamo_documento_cabecera: (9, 21) Cargando reclamo_factura_detalle: fact_reclamo_factura_detalle... [OK] reclamo_factura_detalle: (9, 12) Cargando reclamo_nota_credito_detalle: fact_reclamo_nota_credito_detalle... [OK] reclamo_nota_credito_detalle: (3, 15) Cargando reclamo_ff_detalle: fact_reclamo_ff_detalle... [OK] reclamo_ff_detalle: (114, 28) Cargando hab_parametro: dim_hab_parametro_calidad... [OK] hab_parametro: (18, 12) Cargando hab_defecto: dim_hab_defecto_calidad... [OK] hab_defecto: (12, 8) Cargando hab_causa_defecto: bridge_hab_causa_defecto... [OK] hab_causa_defecto: (22, 8) Cargando hab_protocolo: dim_hab_protocolo_operacional... [OK] hab_protocolo: (18, 9) Cargando hab_etapa: dim_hab_etapa_cadena... [OK] hab_etapa: (9, 8) Cargando hab_principio: dim_hab_principio_gestion_calidad... [OK] hab_principio: (8, 6)
| alias | tabla | estado | filas | columnas | error | |
|---|---|---|---|---|---|---|
| 0 | dim | dim_cuarteles | OK | 144 | 46 | |
| 1 | clima | fact_clima | OK | 14796 | 26 | |
| 2 | cosecha | fact_cosecha | OK | 60480 | 77 | |
| 3 | expo | fact_exportacion | OK | 58944 | 90 | |
| 4 | insp | fact_inspeccion_destino | OK | 14736 | 24 | |
| 5 | reclamo_documento_cabecera | fact_reclamo_documento_cabecera | OK | 9 | 21 | |
| 6 | reclamo_factura_detalle | fact_reclamo_factura_detalle | OK | 9 | 12 | |
| 7 | reclamo_nota_credito_detalle | fact_reclamo_nota_credito_detalle | OK | 3 | 15 | |
| 8 | reclamo_ff_detalle | fact_reclamo_ff_detalle | OK | 114 | 28 | |
| 9 | hab_parametro | dim_hab_parametro_calidad | OK | 18 | 12 | |
| 10 | hab_defecto | dim_hab_defecto_calidad | OK | 12 | 8 | |
| 11 | hab_causa_defecto | bridge_hab_causa_defecto | OK | 22 | 8 | |
| 12 | hab_protocolo | dim_hab_protocolo_operacional | OK | 18 | 9 | |
| 13 | hab_etapa | dim_hab_etapa_cadena | OK | 9 | 8 | |
| 14 | hab_principio | dim_hab_principio_gestion_calidad | OK | 8 | 6 |
Lectura metodológica: - Las tablas operacionales se usan para explicar origen, clima, cosecha, packing y logística. - La inspección destino entrega el target oficial reclamo_comercial. - Las tablas documentales y HAB se usan para auditoría, explicación y evidencia, no como predictores pre-despacho.
02_Auditoria_Datos¶
# ======================================================================================
# 2. AUDITORÍA GENERAL DE TABLAS
# ======================================================================================
print_step("AUDITORÍA GENERAL DE TABLAS")
table_summary = []
null_tables = []
for alias, df in data.items():
if df.empty:
table_summary.append({"alias": alias, "filas": 0, "columnas": 0, "duplicados": 0, "pct_nulos_promedio": np.nan})
continue
duplicated = int(df.duplicated().sum())
null_pct = df.isna().mean().mean()
table_summary.append({
"alias": alias,
"filas": df.shape[0],
"columnas": df.shape[1],
"duplicados": duplicated,
"pct_nulos_promedio": null_pct,
})
nulos = (
df.isna().sum()
.reset_index()
.rename(columns={"index": "variable", 0: "nulos"})
)
nulos["tabla"] = alias
nulos["pct_nulos"] = nulos["nulos"] / max(len(df), 1)
null_tables.append(nulos)
table_summary_df = pd.DataFrame(table_summary).sort_values("filas", ascending=False)
display(table_summary_df)
table_summary_df.to_csv(OUTPUT_DIR / "resumen_tablas_v40.csv", index=False)
if null_tables:
nulls_df = pd.concat(null_tables, ignore_index=True)
nulls_df.sort_values(["tabla", "pct_nulos"], ascending=[True, False]).to_csv(OUTPUT_DIR / "nulos_por_variable_v40.csv", index=False)
display(nulls_df.sort_values("pct_nulos", ascending=False).head(30))
# Gráfico de volumetría de tablas cargadas.
plot_df = table_summary_df[table_summary_df["filas"] > 0].copy().sort_values("filas")
plt.figure(figsize=(10, max(4, len(plot_df)*0.35)))
plt.barh(plot_df["alias"], plot_df["filas"])
plt.title("Volumetría de tablas cargadas")
plt.xlabel("Cantidad de registros")
plt.ylabel("Tabla")
savefig("01_volumetria_tablas.png")
print("Lectura:")
print("La auditoría de tablas permite demostrar qué fuentes están disponibles, cuáles faltan y qué tan robusta es la base para auditoria causal/económico.")
==================================================================================================== AUDITORÍA GENERAL DE TABLAS ====================================================================================================
| alias | filas | columnas | duplicados | pct_nulos_promedio | |
|---|---|---|---|---|---|
| 2 | cosecha | 60480 | 77 | 0 | 0.001759 |
| 3 | expo | 58944 | 90 | 0 | 0.001799 |
| 1 | clima | 14796 | 26 | 0 | 0.000000 |
| 4 | insp | 14736 | 24 | 0 | 0.002627 |
| 0 | dim | 144 | 46 | 0 | 0.000000 |
| 8 | reclamo_ff_detalle | 114 | 28 | 0 | 0.056391 |
| 11 | hab_causa_defecto | 22 | 8 | 0 | 0.039773 |
| 12 | hab_protocolo | 18 | 9 | 0 | 0.000000 |
| 9 | hab_parametro | 18 | 12 | 0 | 0.000000 |
| 10 | hab_defecto | 12 | 8 | 0 | 0.000000 |
| 5 | reclamo_documento_cabecera | 9 | 21 | 0 | 0.169312 |
| 6 | reclamo_factura_detalle | 9 | 12 | 0 | 0.000000 |
| 13 | hab_etapa | 9 | 8 | 0 | 0.000000 |
| 14 | hab_principio | 8 | 6 | 0 | 0.000000 |
| 7 | reclamo_nota_credito_detalle | 3 | 15 | 0 | 0.000000 |
| variable | nulos | tabla | pct_nulos | |
|---|---|---|---|---|
| 325 | unidad_medida_base | 114 | reclamo_ff_detalle | 1.000000 |
| 280 | puerto_o_referencia | 6 | reclamo_documento_cabecera | 0.666667 |
| 281 | documento_referenciado | 6 | reclamo_documento_cabecera | 0.666667 |
| 274 | codigo_postal | 5 | reclamo_documento_cabecera | 0.555556 |
| 278 | monto_neto | 3 | reclamo_documento_cabecera | 0.333333 |
| 279 | monto_total | 3 | reclamo_documento_cabecera | 0.333333 |
| 282 | ff_id_referenciado | 3 | reclamo_documento_cabecera | 0.333333 |
| 276 | forma_pago | 3 | reclamo_documento_cabecera | 0.333333 |
| 273 | direccion | 3 | reclamo_documento_cabecera | 0.333333 |
| 361 | parametro_id | 7 | hab_causa_defecto | 0.318182 |
| 261 | observacion_inspector | 929 | insp | 0.063043 |
| 142 | aceite_estimado_pct | 3669 | cosecha | 0.060665 |
| 236 | observacion_destino | 3575 | expo | 0.060651 |
| 235 | inspector_destino | 2945 | expo | 0.049963 |
| 114 | observacion_jefe_campo | 2442 | cosecha | 0.040377 |
| 141 | brix_pct | 2079 | cosecha | 0.034375 |
| 209 | fungicida_poscosecha | 1838 | expo | 0.031182 |
| 313 | ff_id | 3 | reclamo_ff_detalle | 0.026316 |
| 336 | numero_pallet | 3 | reclamo_ff_detalle | 0.026316 |
| 335 | kg_real | 3 | reclamo_ff_detalle | 0.026316 |
| 334 | codigo_productor | 3 | reclamo_ff_detalle | 0.026316 |
| 332 | productor_sap | 3 | reclamo_ff_detalle | 0.026316 |
| 331 | sap_lot | 3 | reclamo_ff_detalle | 0.026316 |
| 330 | grower | 3 | reclamo_ff_detalle | 0.026316 |
| 329 | moneda | 3 | reclamo_ff_detalle | 0.026316 |
| 328 | precio | 3 | reclamo_ff_detalle | 0.026316 |
| 327 | calibre_por_pallet | 3 | reclamo_ff_detalle | 0.026316 |
| 314 | entrega | 3 | reclamo_ff_detalle | 0.026316 |
| 323 | numero_boxes_lot | 3 | reclamo_ff_detalle | 0.026316 |
| 322 | product_packaging | 3 | reclamo_ff_detalle | 0.026316 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/01_volumetria_tablas.png
Lectura: La auditoría de tablas permite demostrar qué fuentes están disponibles, cuáles faltan y qué tan robusta es la base para auditoria causal/económico.
03_Auditoria_Causal¶
# ======================================================================================
# 3. BASE AUDITADA A NIVEL LOTE_EXPORTACION / CONTENEDOR
# ======================================================================================
print_step("CONSTRUCCIÓN DE BASE AUDITADA")
# Validar tablas mínimas.
required = {"expo": expo, "insp": insp}
for name, df in required.items():
if df.empty:
raise ValueError(f"Tabla requerida vacía: {name}")
# Normalizar target oficial desde inspección destino.
insp2 = insp.copy()
insp2[TARGET] = pd.to_numeric(insp2.get(TARGET, 0), errors="coerce").fillna(0).astype(int)
if "monto_reclamo_usd" in insp2.columns:
insp2["monto_reclamo_usd"] = pd.to_numeric(insp2["monto_reclamo_usd"], errors="coerce").fillna(0)
else:
insp2["monto_reclamo_usd"] = 0.0
# Agregación oficial por lote_exportacion_id.
target_agg = (
insp2
.groupby("lote_exportacion_id", as_index=False)
.agg(
reclamo_comercial=(TARGET, "max"),
monto_reclamo_usd=("monto_reclamo_usd", "sum"),
n_inspecciones=("lote_exportacion_id", "size")
)
)
print("Distribución target oficial desde inspección destino:")
display(target_agg["reclamo_comercial"].value_counts().sort_index())
# Exportación: una fila por lote_exportacion_id, para evitar duplicidad por packing/lineas.
expo_base = expo.copy()
if "lote_exportacion_id" not in expo_base.columns:
raise ValueError("fact_exportacion no contiene lote_exportacion_id")
# En caso de duplicados de lote exportación, tomar primera fila operacional y luego auditar duplicidad.
dup_expo = expo_base["lote_exportacion_id"].duplicated().sum()
print("Duplicados lote_exportacion_id en expo:", dup_expo)
expo_unique = expo_base.drop_duplicates(subset=["lote_exportacion_id"]).copy()
# Eliminar targets si vinieran accidentalmente desde exportación.
leak_cols = [
"reclamo_comercial", "monto_reclamo_usd", "lote_llega_mal_destino",
"score_condicion_arribo_0a100", "fruta_apta_pct", "desorden_interno_pct",
"maduracion_desuniforme_pct", "deshidratacion_pct", "golpe_pct", "podredumbre_pct",
"dias_a_listo_consumo", "defecto_principal_destino", "severidad_inspeccion",
"fecha_inspeccion_destino", "inspector_empresa"
]
expo_unique = expo_unique.drop(columns=[c for c in leak_cols if c in expo_unique.columns], errors="ignore")
base = expo_unique.merge(target_agg, on="lote_exportacion_id", how="inner")
base[TARGET] = base[TARGET].fillna(0).astype(int)
print("Base auditada inicial:", base.shape)
print("Distribución target en base:")
display(base[TARGET].value_counts().sort_index())
# Validaciones esperadas conocidas del proyecto.
print("Tasa de reclamo:", f"{base[TARGET].mean():.2%}")
# Guardar base mínima auditada.
base.to_csv(OUTPUT_DIR / "base_auditada_lote_exportacion_v40.csv", index=False)
plt.figure(figsize=(6,4))
base[TARGET].value_counts().sort_index().plot(kind="bar")
plt.title("Distribución oficial del target reclamo_comercial")
plt.xlabel("reclamo_comercial")
plt.ylabel("Cantidad de lotes exportación")
savefig("02_distribucion_target_oficial.png")
print("Lectura:")
print("El target oficial proviene de fact_inspeccion_destino. La base auditada queda a nivel lote_exportacion_id y evita duplicar registros de exportación.")
==================================================================================================== CONSTRUCCIÓN DE BASE AUDITADA ==================================================================================================== Distribución target oficial desde inspección destino:
| count | |
|---|---|
| reclamo_comercial | |
| 0 | 14582 |
| 1 | 154 |
Duplicados lote_exportacion_id en expo: 0 Base auditada inicial: (14736, 81) Distribución target en base:
| count | |
|---|---|
| reclamo_comercial | |
| 0 | 14582 |
| 1 | 154 |
Tasa de reclamo: 1.05% [GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/02_distribucion_target_oficial.png
Lectura: El target oficial proviene de fact_inspeccion_destino. La base auditada queda a nivel lote_exportacion_id y evita duplicar registros de exportación.
# ======================================================================================
# 4. ENRIQUECIMIENTO OPERACIONAL: ORIGEN, COSECHA Y CLIMA
# ======================================================================================
print_step("ENRIQUECIMIENTO OPERACIONAL")
base_enriq = base.copy()
# Merge con cosecha por lote_cosecha_id si existe.
if not cosecha.empty and "lote_cosecha_id" in base_enriq.columns and "lote_cosecha_id" in cosecha.columns:
cosecha_cols_prefer = [
"lote_cosecha_id", "cuartel_id", "temporada", "fecha_muestreo", "fecha_cosecha",
"dias_muestreo_a_cosecha", "cuadrilla", "uso_tijera", "cantidad_bins", "horas_cosecha",
"estacion_climatica_ref", "subzona_agroclimatica", "comuna", "uniformidad_riego_pct",
"altitud_ms", "edad_arboles_anos", "densidad_arboles_ha", "portainjerto", "textura_suelo",
"ph_suelo", "mo_pct", "ce_suelo_ds_m", "sistema_riego"
]
cosecha_use = cosecha[[c for c in cosecha_cols_prefer if c in cosecha.columns]].drop_duplicates("lote_cosecha_id")
base_enriq = base_enriq.merge(cosecha_use, on="lote_cosecha_id", how="left", suffixes=("", "_cosecha"))
print("[OK] Merge cosecha:", base_enriq.shape)
else:
print("[INFO] No se realizó merge cosecha por falta de llave o tabla vacía.")
# Merge con dim por cuartel_id si existe.
if not dim.empty and "cuartel_id" in base_enriq.columns and "cuartel_id" in dim.columns:
dim_cols_prefer = [
"cuartel_id", "predio_id", "nombre_predio", "unidad_negocio", "comuna", "subzona_agroclimatica",
"latitud_aprox", "longitud_aprox", "altitud_ms", "superficie_ha", "ano_plantacion",
"edad_arboles_anos", "densidad_arboles_ha", "portainjerto", "textura_suelo",
"profundidad_suelo_cm", "ph_suelo", "mo_pct", "ce_suelo_ds_m", "sistema_riego",
"uniformidad_riego_pct", "indice_vigor_ndvi_base", "ndvi_base", "rendimiento_base"
]
dim_use = dim[[c for c in dim_cols_prefer if c in dim.columns]].drop_duplicates("cuartel_id")
base_enriq = base_enriq.merge(dim_use, on="cuartel_id", how="left", suffixes=("", "_dim"))
print("[OK] Merge dim_cuarteles:", base_enriq.shape)
else:
print("[INFO] No se realizó merge dim por falta de llave o tabla vacía.")
# Variables derivadas de fechas.
for col in ["fecha_cosecha", "fecha_embarque", "fecha_recepcion_packing"]:
if col in base_enriq.columns:
base_enriq[col] = pd.to_datetime(base_enriq[col], errors="coerce")
if "fecha_cosecha" in base_enriq.columns:
base_enriq["mes_cosecha"] = base_enriq["fecha_cosecha"].dt.month
base_enriq["semana_cosecha"] = base_enriq["fecha_cosecha"].dt.isocalendar().week.astype("float")
if "fecha_embarque" in base_enriq.columns:
base_enriq["mes_embarque"] = base_enriq["fecha_embarque"].dt.month
base_enriq["semana_embarque"] = base_enriq["fecha_embarque"].dt.isocalendar().week.astype("float")
# Agregación climática robusta por estación/temporada, si existe.
# Para V4.0 se calcula resumen por estación-temporada y se asigna por estacion_climatica_ref.
if not clima.empty and "estacion_climatica_ref" in clima.columns and "estacion_climatica_ref" in base_enriq.columns:
clima2 = clima.copy()
if "fecha" in clima2.columns:
clima2["fecha"] = pd.to_datetime(clima2["fecha"], errors="coerce")
numeric_clima_candidates = [
"tmin_c", "tmax_c", "tmed_c", "lluvia_mm", "humedad_relativa_pct",
"horas_sobre_30c", "evento_helada", "horas_frio_bajo_7c", "gdd_base10",
"radiacion_mj_m2", "temperatura_suelo_c", "viento_km_h", "eto_mm"
]
for c in numeric_clima_candidates:
if c in clima2.columns:
clima2[c] = pd.to_numeric(clima2[c], errors="coerce")
group_keys = ["estacion_climatica_ref"]
if "temporada" in clima2.columns:
group_keys.append("temporada")
agg_dict = {}
if "tmin_c" in clima2.columns: agg_dict["tmin_promedio_precosecha"] = ("tmin_c", "mean")
if "tmax_c" in clima2.columns: agg_dict["tmax_promedio_precosecha"] = ("tmax_c", "mean")
if "tmed_c" in clima2.columns: agg_dict["tmedia_promedio_precosecha"] = ("tmed_c", "mean")
if "lluvia_mm" in clima2.columns: agg_dict["lluvia_acumulada_precosecha"] = ("lluvia_mm", "sum")
if "humedad_relativa_pct" in clima2.columns: agg_dict["humedad_promedio_precosecha"] = ("humedad_relativa_pct", "mean")
if "horas_sobre_30c" in clima2.columns: agg_dict["horas_sobre_30c_acumuladas"] = ("horas_sobre_30c", "sum")
if "evento_helada" in clima2.columns: agg_dict["heladas_acumuladas_precosecha"] = ("evento_helada", "sum")
if "horas_frio_bajo_7c" in clima2.columns: agg_dict["horas_frio_acumuladas"] = ("horas_frio_bajo_7c", "sum")
if "gdd_base10" in clima2.columns: agg_dict["gdd_acumulado_precosecha"] = ("gdd_base10", "sum")
if "radiacion_mj_m2" in clima2.columns: agg_dict["radiacion_promedio_precosecha"] = ("radiacion_mj_m2", "mean")
if "temperatura_suelo_c" in clima2.columns: agg_dict["temperatura_suelo_promedio_precosecha"] = ("temperatura_suelo_c", "mean")
if agg_dict:
clima_agg = clima2.groupby(group_keys, as_index=False).agg(**agg_dict)
merge_keys = [k for k in group_keys if k in base_enriq.columns]
base_enriq = base_enriq.merge(clima_agg, on=merge_keys, how="left")
print("[OK] Merge clima agregado:", base_enriq.shape)
else:
print("[INFO] No se realizó merge clima por falta de estación o tabla vacía.")
# Deduplicar columnas exactas.
base_enriq = base_enriq.loc[:, ~base_enriq.columns.duplicated()].copy()
# Crear flags de riesgo si las variables existen.
def add_flag(df, col, new_col, condition):
if col in df.columns:
df[new_col] = condition(pd.to_numeric(df[col], errors="coerce")).astype(int)
return df
base_enriq = add_flag(base_enriq, "desviacion_materia_seca_pct", "riesgo_heterogeneidad_flag", lambda s: s >= s.quantile(0.75))
base_enriq = add_flag(base_enriq, "materia_seca_pct", "riesgo_materia_seca_baja_flag", lambda s: s <= s.quantile(0.25))
base_enriq = add_flag(base_enriq, "firmeza_pulpa_lb", "riesgo_firmeza_baja_flag", lambda s: s <= s.quantile(0.25))
base_enriq = add_flag(base_enriq, "dias_cosecha_a_packing", "packing_lento_flag", lambda s: s >= s.quantile(0.75))
base_enriq = add_flag(base_enriq, "transito_real_dias", "transito_largo_flag", lambda s: s >= s.quantile(0.75))
# Diferencia de tránsito si existen ambos campos.
if "transito_real_dias" in base_enriq.columns and "transito_plan_dias" in base_enriq.columns:
base_enriq["diferencia_transito_dias"] = pd.to_numeric(base_enriq["transito_real_dias"], errors="coerce") - pd.to_numeric(base_enriq["transito_plan_dias"], errors="coerce")
base_enriq["retraso_logistico_flag"] = (base_enriq["diferencia_transito_dias"] > 0).astype(int)
base_enriq.to_csv(OUTPUT_DIR / "base_enriquecida_auditoria_v40.csv", index=False)
print("Base enriquecida guardada:", base_enriq.shape)
print("Columnas disponibles:", len(base_enriq.columns))
==================================================================================================== ENRIQUECIMIENTO OPERACIONAL ==================================================================================================== [OK] Merge cosecha: (14736, 100) [OK] Merge dim_cuarteles: (14736, 121) [OK] Merge clima agregado: (14736, 136) Base enriquecida guardada: (14736, 140) Columnas disponibles: 140
04_Feature_Engineering¶
# ======================================================================================
# 5. AUDITORÍA ESTRATÉGICA DE VARIABLES — 5 NIVELES
# ======================================================================================
print_step("AUDITORÍA ESTRATÉGICA DE VARIABLES — 5 NIVELES")
# --------------------------------------------------------------------------------------
# Objetivo:
# Clasificar las variables candidatas del proyecto en 5 niveles de madurez analítica.
# Esto permite distinguir:
# - variables críticas fisiológicas/operacionales,
# - variables importantes,
# - variables contextuales,
# - variables históricas,
# - variables de causalidad expandida/interacciones.
#
# Además, se valida si cada variable existe realmente en las tablas cargadas.
# --------------------------------------------------------------------------------------
# Unir todas las columnas disponibles de las tablas cargadas
all_columns = set()
for table_name, df_tmp in data.items():
if isinstance(df_tmp, pd.DataFrame) and not df_tmp.empty:
for c in df_tmp.columns:
all_columns.add(c)
all_columns_lower = {c.lower(): c for c in all_columns}
def variable_exists(var_name):
"""
Valida si una variable existe en alguna tabla cargada.
La búsqueda es flexible:
- match exacto en minúscula
- match parcial para variables derivadas o nombres similares
"""
v = str(var_name).lower()
if v in all_columns_lower:
return True
for col in all_columns_lower:
if v in col or col in v:
return True
return False
variables_estrategicas = [
# ==================================================================================
# NIVEL 1 — VARIABLES CAUSALES DIRECTAS
# ==================================================================================
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Madurez",
"variable": "materia_seca_pct",
"justificacion": "Indicador maestro de madurez funcional de la palta Hass."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Madurez",
"variable": "desviacion_materia_seca_pct",
"justificacion": "Captura heterogeneidad de madurez; puede generar maduración asincrónica."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Madurez",
"variable": "riesgo_heterogeneidad_score",
"justificacion": "Score derivado para cuantificar riesgo de mezcla heterogénea."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Frío",
"variable": "quiebre_cadena_frio_h",
"justificacion": "Mide exposición térmica fuera de rango; puede acelerar respiración y senescencia."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Frío",
"variable": "tiempo_preenfriado_h",
"justificacion": "Demora en remover calor de campo aumenta riesgo de deterioro poscosecha."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Frío",
"variable": "temperatura_setpoint_frio_c",
"justificacion": "Temperatura objetivo debe ser coherente con madurez y tiempo de tránsito."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Logística",
"variable": "transito_real_dias",
"justificacion": "Mayor tránsito aumenta exposición a senescencia y riesgo de reclamo."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Origen",
"variable": "indice_vigor_ndvi_base",
"justificacion": "Vigor vegetativo se asocia a condición fisiológica del fruto."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Origen",
"variable": "subzona_agroclimatica",
"justificacion": "Define condiciones agroclimáticas y ventana de comercialización."
},
{
"nivel": "Nivel 1",
"nombre_nivel": "Variables Causales Directas",
"area": "Origen",
"variable": "uniformidad_riego_pct",
"justificacion": "Variabilidad de riego puede generar heterogeneidad de madurez y materia seca."
},
# ==================================================================================
# NIVEL 2 — VARIABLES EXPLICATIVAS PRIMARIAS
# ==================================================================================
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Origen", "variable": "edad_arboles_anos", "justificacion": "Edad del huerto puede influir en vigor, productividad y condición de fruta."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Origen", "variable": "densidad_arboles_ha", "justificacion": "Densidad afecta competencia, luz, vigor y desarrollo del fruto."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Origen", "variable": "portainjerto", "justificacion": "Portainjerto puede influir en vigor, absorción hídrica y respuesta al estrés."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Origen", "variable": "ph_suelo", "justificacion": "pH afecta disponibilidad nutricional y condición productiva."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Origen", "variable": "mo_pct", "justificacion": "Materia orgánica se relaciona con estructura del suelo y disponibilidad hídrica."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Clima", "variable": "tmin_c", "justificacion": "Temperatura mínima precosecha afecta metabolismo y desarrollo del fruto."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Clima", "variable": "tmax_c", "justificacion": "Temperatura máxima puede generar estrés térmico."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Clima", "variable": "lluvia_mm", "justificacion": "Lluvia precosecha puede afectar condición sanitaria y manejo de cosecha."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Clima", "variable": "horas_frio_bajo_7c", "justificacion": "Horas de frío pueden influir en fisiología y calendario productivo."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Clima", "variable": "horas_sobre_30c", "justificacion": "Horas sobre 30°C capturan estrés por calor."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Cosecha", "variable": "dias_muestreo_a_cosecha", "justificacion": "Ventana entre muestreo y cosecha afecta representatividad de madurez."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Cosecha", "variable": "semana_cosecha", "justificacion": "Captura momento fenológico y etapa de temporada."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Cosecha", "variable": "mes_cosecha", "justificacion": "Permite evaluar estacionalidad de cosecha."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Packing", "variable": "dias_cosecha_a_packing", "justificacion": "Tiempo desde cosecha a packing afecta condición y deterioro inicial."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Madurez", "variable": "firmeza_pulpa_lb", "justificacion": "Resistencia estructural del fruto frente a daño mecánico."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Madurez", "variable": "calibre", "justificacion": "Tamaño del fruto puede relacionarse con madurez y sensibilidad."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Madurez", "variable": "peso_fruto_g", "justificacion": "Peso captura condición física y calibre efectivo."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Logística", "variable": "retraso_logistico_flag", "justificacion": "Retrasos aumentan exposición a deterioro y riesgo comercial."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Logística", "variable": "diferencia_transito_dias", "justificacion": "Diferencia entre tránsito real y planificado."},
{"nivel": "Nivel 2", "nombre_nivel": "Variables Explicativas Primarias", "area": "Contenedor", "variable": "atmosfera_controlada", "justificacion": "Condición de atmósfera afecta conservación y maduración."},
# ==================================================================================
# NIVEL 3 — VARIABLES DE ENTORNO OPERACIONAL
# ==================================================================================
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Mercado", "variable": "pais_destino", "justificacion": "Destino puede reflejar distancia, exigencia comercial y manejo post-arribo."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Mercado", "variable": "macro_mercado", "justificacion": "Agrupa destinos con comportamiento logístico y comercial común."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Mercado", "variable": "canal_destino", "justificacion": "Canal puede reflejar exigencia de cliente y nivel de tolerancia."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Cliente", "variable": "cliente_tipo", "justificacion": "Tipo de cliente puede tener diferente estándar de aceptación."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Packing", "variable": "packhouse_id", "justificacion": "Captura diferencias operacionales entre plantas o instalaciones."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Packing", "variable": "linea_packing", "justificacion": "Diferencias de línea pueden afectar manipulación y selección."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Packing", "variable": "operador_packing", "justificacion": "Operador puede reflejar diferencias operativas o turnos."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Logística", "variable": "naviera", "justificacion": "Naviera captura diferencias de servicio, ruta y desempeño logístico."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Logística", "variable": "puerto_salida", "justificacion": "Puerto puede asociarse a tiempos, infraestructura y rutas."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Contenedor", "variable": "tipo_contenedor", "justificacion": "Tipo de contenedor puede afectar conservación y exposición térmica."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Temporal", "variable": "temporada", "justificacion": "Captura cambios productivos, comerciales y climáticos entre temporadas."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Temporal", "variable": "cosecha_temprana_flag", "justificacion": "Cosecha temprana puede asociarse a madurez funcional menor."},
{"nivel": "Nivel 3", "nombre_nivel": "Variables de Entorno Operacional", "area": "Temporal", "variable": "cosecha_tardia_flag", "justificacion": "Cosecha tardía puede asociarse a sobremadurez o sensibilidad."},
# ==================================================================================
# NIVEL 4 — VARIABLES DE RIESGO HISTÓRICO Y COMERCIAL ACUMULADO
# ==================================================================================
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "tasa_reclamo_cliente_historica", "justificacion": "Historial de reclamos del cliente sin mirar información futura."},
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "tasa_reclamo_mercado_historica", "justificacion": "Historial de reclamos por macro mercado."},
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "tasa_reclamo_pais_historica", "justificacion": "Historial de reclamos por país destino."},
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "tasa_reclamo_packhouse_historica", "justificacion": "Historial de reclamos asociado a packhouse."},
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "tasa_reclamo_naviera_historica", "justificacion": "Historial de reclamos asociado a naviera."},
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "monto_promedio_reclamo_cliente", "justificacion": "Monto histórico promedio asociado al cliente."},
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "monto_promedio_reclamo_mercado", "justificacion": "Monto histórico promedio asociado al mercado."},
{"nivel": "Nivel 4", "nombre_nivel": "Variables de Riesgo Histórico y Comercial Acumulado", "area": "Históricos", "variable": "monto_promedio_reclamo_naviera", "justificacion": "Monto histórico promedio asociado a naviera."},
# ==================================================================================
# NIVEL 5 — VARIABLES DE CAUSALIDAD EXPANDIDA E INTERACCIONES
# ==================================================================================
{"nivel": "Nivel 5", "nombre_nivel": "Variables de Causalidad Expandida e Interacciones", "area": "Interacciones", "variable": "materia_seca_x_transito", "justificacion": "Interacción entre madurez y exposición logística."},
{"nivel": "Nivel 5", "nombre_nivel": "Variables de Causalidad Expandida e Interacciones", "area": "Interacciones", "variable": "materia_seca_x_mercado", "justificacion": "Interacción entre madurez y exigencia de mercado."},
{"nivel": "Nivel 5", "nombre_nivel": "Variables de Causalidad Expandida e Interacciones", "area": "Interacciones", "variable": "firmeza_x_transito", "justificacion": "Interacción entre firmeza y duración logística."},
{"nivel": "Nivel 5", "nombre_nivel": "Variables de Causalidad Expandida e Interacciones", "area": "Interacciones", "variable": "frio_x_transito", "justificacion": "Interacción entre quiebre de frío y duración de tránsito."},
{"nivel": "Nivel 5", "nombre_nivel": "Variables de Causalidad Expandida e Interacciones", "area": "Interacciones", "variable": "ndvi_x_subzona", "justificacion": "Interacción entre vigor del origen y zona agroclimática."},
]
audit_variables = pd.DataFrame(variables_estrategicas)
# --------------------------------------------------------------------------------------
# Validar existencia
# --------------------------------------------------------------------------------------
audit_variables["existe_en_base"] = audit_variables["variable"].apply(variable_exists)
audit_variables["estado"] = np.where(
audit_variables["existe_en_base"],
"Disponible",
"Gap de información"
)
audit_variables["prioridad_modelo"] = np.where(
audit_variables["existe_en_base"],
"Candidata inmediata",
"Requiere captura futura"
)
display(audit_variables)
# --------------------------------------------------------------------------------------
# Resumen general
# --------------------------------------------------------------------------------------
resumen_nivel = (
audit_variables
.groupby(["nivel", "nombre_nivel", "estado"])
.size()
.reset_index(name="cantidad")
)
display(resumen_nivel)
# --------------------------------------------------------------------------------------
# Matriz Nivel x Área
# --------------------------------------------------------------------------------------
matriz_nivel_area = pd.crosstab(
audit_variables["nivel"],
audit_variables["area"]
)
display(matriz_nivel_area)
# --------------------------------------------------------------------------------------
# Resumen Nivel 1 — Variables críticas
# --------------------------------------------------------------------------------------
nivel1 = audit_variables[audit_variables["nivel"] == "Nivel 1"].copy()
total_n1 = len(nivel1)
disp_n1 = (nivel1["estado"] == "Disponible").sum()
falt_n1 = (nivel1["estado"] == "Gap de información").sum()
pct_disp_n1 = disp_n1 / total_n1 * 100
pct_falt_n1 = falt_n1 / total_n1 * 100
print("\n" + "="*100)
print("RESUMEN VARIABLES CRÍTICAS — NIVEL 1")
print("="*100)
print(f"Variables críticas disponibles: {disp_n1} de {total_n1} ({pct_disp_n1:.1f}%)")
print(f"Variables críticas faltantes: {falt_n1} de {total_n1} ({pct_falt_n1:.1f}%)")
# --------------------------------------------------------------------------------------
# Brecha de captura de información
# --------------------------------------------------------------------------------------
gaps = audit_variables[audit_variables["estado"] == "Gap de información"].copy()
gap_area = (
gaps
.groupby("area")
.size()
.reset_index(name="cantidad_variables")
.sort_values("cantidad_variables", ascending=False)
)
display(gap_area)
print("\n" + "="*100)
print("BRECHA DE CAPTURA DE INFORMACIÓN")
print("="*100)
if gap_area.empty:
print("No se detectaron brechas de captura de información.")
else:
print("Las variables faltantes corresponden principalmente a:")
for _, r in gap_area.iterrows():
print(f"- {r['area']} ({r['cantidad_variables']} variables)")
print("""
Interpretación:
La auditoría estratégica permite separar variables disponibles de variables críticas aún no capturadas.
Las variables con estado 'Gap de información' no deben forzarse en el modelo actual.
Deben quedar documentadas como brechas de captura para futuras temporadas.
""")
# --------------------------------------------------------------------------------------
# Gráficos
# --------------------------------------------------------------------------------------
plt.figure(figsize=(8, 5))
audit_variables["estado"].value_counts().plot(kind="bar")
plt.title("Disponibilidad de variables estratégicas")
plt.ylabel("Cantidad de variables")
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
plt.figure(figsize=(10, 5))
(
audit_variables
.groupby(["nivel", "estado"])
.size()
.unstack(fill_value=0)
.plot(kind="bar", stacked=True, figsize=(10, 5))
)
plt.title("Disponibilidad de variables por nivel")
plt.ylabel("Cantidad de variables")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
plt.figure(figsize=(12, 6))
plt.imshow(matriz_nivel_area, aspect="auto")
plt.title("Matriz Nivel x Área de Variables")
plt.xticks(range(len(matriz_nivel_area.columns)), matriz_nivel_area.columns, rotation=45, ha="right")
plt.yticks(range(len(matriz_nivel_area.index)), matriz_nivel_area.index)
plt.colorbar(label="Cantidad de variables")
plt.tight_layout()
plt.show()
# --------------------------------------------------------------------------------------
# Exportar
# --------------------------------------------------------------------------------------
audit_variables.to_csv(OUTPUT_DIR / "auditoria_estrategica_variables_5_niveles.csv", index=False)
resumen_nivel.to_csv(OUTPUT_DIR / "resumen_disponibilidad_variables_por_nivel.csv", index=False)
matriz_nivel_area.to_csv(OUTPUT_DIR / "matriz_nivel_area_variables.csv")
print("[OUTPUT] Archivos de auditoría estratégica exportados.")
# Copia oficial V6.0 de la matriz de variables y niveles heredada de V4.0
try:
audit_variables.to_csv(OUTPUT_DIR / "matriz_variables_niveles_oficial_V6_0.csv", index=False)
print("[OUTPUT] Matriz oficial variables/niveles V6.0 guardada.")
except Exception as e:
print("[WARNING] No se pudo guardar matriz variables/niveles V6.0:", e)
==================================================================================================== AUDITORÍA ESTRATÉGICA DE VARIABLES — 5 NIVELES ====================================================================================================
| nivel | nombre_nivel | area | variable | justificacion | existe_en_base | estado | prioridad_modelo | |
|---|---|---|---|---|---|---|---|---|
| 0 | Nivel 1 | Variables Causales Directas | Madurez | materia_seca_pct | Indicador maestro de madurez funcional de la p... | True | Disponible | Candidata inmediata |
| 1 | Nivel 1 | Variables Causales Directas | Madurez | desviacion_materia_seca_pct | Captura heterogeneidad de madurez; puede gener... | True | Disponible | Candidata inmediata |
| 2 | Nivel 1 | Variables Causales Directas | Madurez | riesgo_heterogeneidad_score | Score derivado para cuantificar riesgo de mezc... | True | Disponible | Candidata inmediata |
| 3 | Nivel 1 | Variables Causales Directas | Frío | quiebre_cadena_frio_h | Mide exposición térmica fuera de rango; puede ... | True | Disponible | Candidata inmediata |
| 4 | Nivel 1 | Variables Causales Directas | Frío | tiempo_preenfriado_h | Demora en remover calor de campo aumenta riesg... | True | Disponible | Candidata inmediata |
| 5 | Nivel 1 | Variables Causales Directas | Frío | temperatura_setpoint_frio_c | Temperatura objetivo debe ser coherente con ma... | True | Disponible | Candidata inmediata |
| 6 | Nivel 1 | Variables Causales Directas | Logística | transito_real_dias | Mayor tránsito aumenta exposición a senescenci... | False | Gap de información | Requiere captura futura |
| 7 | Nivel 1 | Variables Causales Directas | Origen | indice_vigor_ndvi_base | Vigor vegetativo se asocia a condición fisioló... | True | Disponible | Candidata inmediata |
| 8 | Nivel 1 | Variables Causales Directas | Origen | subzona_agroclimatica | Define condiciones agroclimáticas y ventana de... | True | Disponible | Candidata inmediata |
| 9 | Nivel 1 | Variables Causales Directas | Origen | uniformidad_riego_pct | Variabilidad de riego puede generar heterogene... | True | Disponible | Candidata inmediata |
| 10 | Nivel 2 | Variables Explicativas Primarias | Origen | edad_arboles_anos | Edad del huerto puede influir en vigor, produc... | True | Disponible | Candidata inmediata |
| 11 | Nivel 2 | Variables Explicativas Primarias | Origen | densidad_arboles_ha | Densidad afecta competencia, luz, vigor y desa... | True | Disponible | Candidata inmediata |
| 12 | Nivel 2 | Variables Explicativas Primarias | Origen | portainjerto | Portainjerto puede influir en vigor, absorción... | True | Disponible | Candidata inmediata |
| 13 | Nivel 2 | Variables Explicativas Primarias | Origen | ph_suelo | pH afecta disponibilidad nutricional y condici... | True | Disponible | Candidata inmediata |
| 14 | Nivel 2 | Variables Explicativas Primarias | Origen | mo_pct | Materia orgánica se relaciona con estructura d... | True | Disponible | Candidata inmediata |
| 15 | Nivel 2 | Variables Explicativas Primarias | Clima | tmin_c | Temperatura mínima precosecha afecta metabolis... | True | Disponible | Candidata inmediata |
| 16 | Nivel 2 | Variables Explicativas Primarias | Clima | tmax_c | Temperatura máxima puede generar estrés térmico. | True | Disponible | Candidata inmediata |
| 17 | Nivel 2 | Variables Explicativas Primarias | Clima | lluvia_mm | Lluvia precosecha puede afectar condición sani... | True | Disponible | Candidata inmediata |
| 18 | Nivel 2 | Variables Explicativas Primarias | Clima | horas_frio_bajo_7c | Horas de frío pueden influir en fisiología y c... | True | Disponible | Candidata inmediata |
| 19 | Nivel 2 | Variables Explicativas Primarias | Clima | horas_sobre_30c | Horas sobre 30°C capturan estrés por calor. | True | Disponible | Candidata inmediata |
| 20 | Nivel 2 | Variables Explicativas Primarias | Cosecha | dias_muestreo_a_cosecha | Ventana entre muestreo y cosecha afecta repres... | True | Disponible | Candidata inmediata |
| 21 | Nivel 2 | Variables Explicativas Primarias | Cosecha | semana_cosecha | Captura momento fenológico y etapa de temporada. | False | Gap de información | Requiere captura futura |
| 22 | Nivel 2 | Variables Explicativas Primarias | Cosecha | mes_cosecha | Permite evaluar estacionalidad de cosecha. | False | Gap de información | Requiere captura futura |
| 23 | Nivel 2 | Variables Explicativas Primarias | Packing | dias_cosecha_a_packing | Tiempo desde cosecha a packing afecta condició... | True | Disponible | Candidata inmediata |
| 24 | Nivel 2 | Variables Explicativas Primarias | Madurez | firmeza_pulpa_lb | Resistencia estructural del fruto frente a dañ... | True | Disponible | Candidata inmediata |
| 25 | Nivel 2 | Variables Explicativas Primarias | Madurez | calibre | Tamaño del fruto puede relacionarse con madure... | True | Disponible | Candidata inmediata |
| 26 | Nivel 2 | Variables Explicativas Primarias | Madurez | peso_fruto_g | Peso captura condición física y calibre efectivo. | True | Disponible | Candidata inmediata |
| 27 | Nivel 2 | Variables Explicativas Primarias | Logística | retraso_logistico_flag | Retrasos aumentan exposición a deterioro y rie... | False | Gap de información | Requiere captura futura |
| 28 | Nivel 2 | Variables Explicativas Primarias | Logística | diferencia_transito_dias | Diferencia entre tránsito real y planificado. | False | Gap de información | Requiere captura futura |
| 29 | Nivel 2 | Variables Explicativas Primarias | Contenedor | atmosfera_controlada | Condición de atmósfera afecta conservación y m... | True | Disponible | Candidata inmediata |
| 30 | Nivel 3 | Variables de Entorno Operacional | Mercado | pais_destino | Destino puede reflejar distancia, exigencia co... | True | Disponible | Candidata inmediata |
| 31 | Nivel 3 | Variables de Entorno Operacional | Mercado | macro_mercado | Agrupa destinos con comportamiento logístico y... | True | Disponible | Candidata inmediata |
| 32 | Nivel 3 | Variables de Entorno Operacional | Mercado | canal_destino | Canal puede reflejar exigencia de cliente y ni... | True | Disponible | Candidata inmediata |
| 33 | Nivel 3 | Variables de Entorno Operacional | Cliente | cliente_tipo | Tipo de cliente puede tener diferente estándar... | True | Disponible | Candidata inmediata |
| 34 | Nivel 3 | Variables de Entorno Operacional | Packing | packhouse_id | Captura diferencias operacionales entre planta... | True | Disponible | Candidata inmediata |
| 35 | Nivel 3 | Variables de Entorno Operacional | Packing | linea_packing | Diferencias de línea pueden afectar manipulaci... | True | Disponible | Candidata inmediata |
| 36 | Nivel 3 | Variables de Entorno Operacional | Packing | operador_packing | Operador puede reflejar diferencias operativas... | True | Disponible | Candidata inmediata |
| 37 | Nivel 3 | Variables de Entorno Operacional | Logística | naviera | Naviera captura diferencias de servicio, ruta ... | True | Disponible | Candidata inmediata |
| 38 | Nivel 3 | Variables de Entorno Operacional | Logística | puerto_salida | Puerto puede asociarse a tiempos, infraestruct... | True | Disponible | Candidata inmediata |
| 39 | Nivel 3 | Variables de Entorno Operacional | Contenedor | tipo_contenedor | Tipo de contenedor puede afectar conservación ... | True | Disponible | Candidata inmediata |
| 40 | Nivel 3 | Variables de Entorno Operacional | Temporal | temporada | Captura cambios productivos, comerciales y cli... | True | Disponible | Candidata inmediata |
| 41 | Nivel 3 | Variables de Entorno Operacional | Temporal | cosecha_temprana_flag | Cosecha temprana puede asociarse a madurez fun... | False | Gap de información | Requiere captura futura |
| 42 | Nivel 3 | Variables de Entorno Operacional | Temporal | cosecha_tardia_flag | Cosecha tardía puede asociarse a sobremadurez ... | False | Gap de información | Requiere captura futura |
| 43 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | tasa_reclamo_cliente_historica | Historial de reclamos del cliente sin mirar in... | False | Gap de información | Requiere captura futura |
| 44 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | tasa_reclamo_mercado_historica | Historial de reclamos por macro mercado. | False | Gap de información | Requiere captura futura |
| 45 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | tasa_reclamo_pais_historica | Historial de reclamos por país destino. | True | Disponible | Candidata inmediata |
| 46 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | tasa_reclamo_packhouse_historica | Historial de reclamos asociado a packhouse. | False | Gap de información | Requiere captura futura |
| 47 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | tasa_reclamo_naviera_historica | Historial de reclamos asociado a naviera. | True | Disponible | Candidata inmediata |
| 48 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | monto_promedio_reclamo_cliente | Monto histórico promedio asociado al cliente. | False | Gap de información | Requiere captura futura |
| 49 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | monto_promedio_reclamo_mercado | Monto histórico promedio asociado al mercado. | False | Gap de información | Requiere captura futura |
| 50 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Históricos | monto_promedio_reclamo_naviera | Monto histórico promedio asociado a naviera. | True | Disponible | Candidata inmediata |
| 51 | Nivel 5 | Variables de Causalidad Expandida e Interacciones | Interacciones | materia_seca_x_transito | Interacción entre madurez y exposición logística. | False | Gap de información | Requiere captura futura |
| 52 | Nivel 5 | Variables de Causalidad Expandida e Interacciones | Interacciones | materia_seca_x_mercado | Interacción entre madurez y exigencia de mercado. | False | Gap de información | Requiere captura futura |
| 53 | Nivel 5 | Variables de Causalidad Expandida e Interacciones | Interacciones | firmeza_x_transito | Interacción entre firmeza y duración logística. | False | Gap de información | Requiere captura futura |
| 54 | Nivel 5 | Variables de Causalidad Expandida e Interacciones | Interacciones | frio_x_transito | Interacción entre quiebre de frío y duración d... | False | Gap de información | Requiere captura futura |
| 55 | Nivel 5 | Variables de Causalidad Expandida e Interacciones | Interacciones | ndvi_x_subzona | Interacción entre vigor del origen y zona agro... | False | Gap de información | Requiere captura futura |
| nivel | nombre_nivel | estado | cantidad | |
|---|---|---|---|---|
| 0 | Nivel 1 | Variables Causales Directas | Disponible | 9 |
| 1 | Nivel 1 | Variables Causales Directas | Gap de información | 1 |
| 2 | Nivel 2 | Variables Explicativas Primarias | Disponible | 16 |
| 3 | Nivel 2 | Variables Explicativas Primarias | Gap de información | 4 |
| 4 | Nivel 3 | Variables de Entorno Operacional | Disponible | 11 |
| 5 | Nivel 3 | Variables de Entorno Operacional | Gap de información | 2 |
| 6 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Disponible | 3 |
| 7 | Nivel 4 | Variables de Riesgo Histórico y Comercial Acum... | Gap de información | 5 |
| 8 | Nivel 5 | Variables de Causalidad Expandida e Interacciones | Gap de información | 5 |
| area | Cliente | Clima | Contenedor | Cosecha | Frío | Históricos | Interacciones | Logística | Madurez | Mercado | Origen | Packing | Temporal |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| nivel | |||||||||||||
| Nivel 1 | 0 | 0 | 0 | 0 | 3 | 0 | 0 | 1 | 3 | 0 | 3 | 0 | 0 |
| Nivel 2 | 0 | 5 | 1 | 3 | 0 | 0 | 0 | 2 | 3 | 0 | 5 | 1 | 0 |
| Nivel 3 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 2 | 0 | 3 | 0 | 3 | 3 |
| Nivel 4 | 0 | 0 | 0 | 0 | 0 | 8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Nivel 5 | 0 | 0 | 0 | 0 | 0 | 0 | 5 | 0 | 0 | 0 | 0 | 0 | 0 |
==================================================================================================== RESUMEN VARIABLES CRÍTICAS — NIVEL 1 ==================================================================================================== Variables críticas disponibles: 9 de 10 (90.0%) Variables críticas faltantes: 1 de 10 (10.0%)
| area | cantidad_variables | |
|---|---|---|
| 1 | Históricos | 5 |
| 2 | Interacciones | 5 |
| 3 | Logística | 3 |
| 0 | Cosecha | 2 |
| 4 | Temporal | 2 |
==================================================================================================== BRECHA DE CAPTURA DE INFORMACIÓN ==================================================================================================== Las variables faltantes corresponden principalmente a: - Históricos (5 variables) - Interacciones (5 variables) - Logística (3 variables) - Cosecha (2 variables) - Temporal (2 variables) Interpretación: La auditoría estratégica permite separar variables disponibles de variables críticas aún no capturadas. Las variables con estado 'Gap de información' no deben forzarse en el modelo actual. Deben quedar documentadas como brechas de captura para futuras temporadas.
<Figure size 1000x500 with 0 Axes>
[OUTPUT] Archivos de auditoría estratégica exportados. [OUTPUT] Matriz oficial variables/niveles V6.0 guardada.
# ======================================================================================
# 04. FEATURE ENGINEERING — MATRIZ MODELABLE POR VARIABLES/NIVELES V4.0
# ======================================================================================
print_step("04 — FEATURE ENGINEERING MODELABLE V6.0")
model_df = base_enriq.copy()
model_df[TARGET] = pd.to_numeric(model_df[TARGET], errors="coerce").fillna(0).astype(int)
# Variables prohibidas por leakage o post-arribo.
LEAKAGE_COLS = [
"reclamo_comercial", "monto_reclamo_usd", "lote_llega_mal_destino",
"score_condicion_arribo_0a100", "fruta_apta_pct", "desorden_interno_pct",
"maduracion_desuniforme_pct", "deshidratacion_pct", "golpe_pct", "podredumbre_pct",
"dias_a_listo_consumo", "defecto_principal_destino", "severidad_inspeccion",
"fecha_inspeccion_destino", "inspector_empresa", "observacion_inspector"
]
ID_COLS = [c for c in ["lote_exportacion_id", "lote_cosecha_id", "cuartel_id", "predio_id"] if c in model_df.columns]
# Variables oficiales desde V4.0: mismas variables y mismos niveles.
vars_oficiales = audit_variables.copy()
vars_oficiales["existe_en_model_df"] = vars_oficiales["variable"].isin(model_df.columns)
candidate_vars = [
v for v in vars_oficiales.loc[vars_oficiales["existe_en_model_df"], "variable"].dropna().unique().tolist()
if v not in LEAKAGE_COLS and v != TARGET
]
# Agregar variables temporales derivadas si existen y están en la matriz enriquecida.
for v in ["mes_cosecha", "semana_cosecha", "mes_embarque", "semana_embarque"]:
if v in model_df.columns and v not in candidate_vars:
candidate_vars.append(v)
# Remover columnas completamente nulas o constantes.
valid_vars = []
for v in candidate_vars:
s = model_df[v]
if s.notna().sum() == 0:
continue
if s.nunique(dropna=True) <= 1:
continue
valid_vars.append(v)
X_all = model_df[valid_vars].copy()
y_all = model_df[TARGET].astype(int).copy()
# Tipos de variables.
num_features = [c for c in valid_vars if pd.api.types.is_numeric_dtype(X_all[c])]
cat_features = [c for c in valid_vars if c not in num_features]
feature_inventory = pd.DataFrame({
"variable": valid_vars,
"tipo_modelo": ["numerica" if v in num_features else "categorica" for v in valid_vars],
"n_missing": [int(model_df[v].isna().sum()) for v in valid_vars],
"pct_missing": [float(model_df[v].isna().mean()) for v in valid_vars],
"n_unique": [int(model_df[v].nunique(dropna=True)) for v in valid_vars]
}).merge(vars_oficiales[["nivel", "nombre_nivel", "area", "variable", "justificacion"]], on="variable", how="left")
print("Filas modelables:", len(model_df))
print("Target positivo:", int(y_all.sum()), f"({y_all.mean():.2%})")
print("Variables candidatas oficiales disponibles:", len(candidate_vars))
print("Variables válidas modelables:", len(valid_vars))
print("Numéricas:", len(num_features), "| Categóricas:", len(cat_features))
display(feature_inventory.sort_values(["nivel", "area", "variable"]))
feature_inventory.to_csv(OUTPUT_DIR / "inventario_features_modelables_V6_0.csv", index=False)
# Gráfico: variables modelables por nivel.
inv_level = feature_inventory.groupby(["nivel", "nombre_nivel"], dropna=False).size().reset_index(name="variables_modelables")
display(inv_level)
plt.figure(figsize=(10,4))
plt.bar(inv_level["nivel"].astype(str) + "\n" + inv_level["nombre_nivel"].astype(str), inv_level["variables_modelables"])
plt.title("Variables modelables por nivel oficial V4.0")
plt.ylabel("Cantidad de variables")
plt.xticks(rotation=20, ha="right")
savefig("V6_04_variables_modelables_por_nivel.png")
print("Explicación estadística:")
print("La matriz X se construye exclusivamente con variables pre-despacho disponibles y no constantes. Las variables post-arribo quedan excluidas para evitar leakage.")
print("Lectura operacional:")
print("El modelo aprende desde señales disponibles antes del despacho: origen, madurez, cosecha, packing, logística, clima y contexto comercial.")
print("Recomendación ML:")
print("Mantener un diccionario de captura por nivel. Las variables Nivel 1 y Nivel 2 deben tener prioridad en calidad de dato por su vínculo fisiológico/operacional directo.")
==================================================================================================== 04 — FEATURE ENGINEERING MODELABLE V6.0 ==================================================================================================== Filas modelables: 14736 Target positivo: 154 (1.05%) Variables candidatas oficiales disponibles: 33 Variables válidas modelables: 33 Numéricas: 22 | Categóricas: 11
| variable | tipo_modelo | n_missing | pct_missing | n_unique | nivel | nombre_nivel | area | justificacion | |
|---|---|---|---|---|---|---|---|---|---|
| 3 | quiebre_cadena_frio_h | numerica | 0 | 0.0 | 173 | Nivel 1 | Variables Causales Directas | Frío | Mide exposición térmica fuera de rango; puede ... |
| 5 | temperatura_setpoint_frio_c | numerica | 0 | 0.0 | 24 | Nivel 1 | Variables Causales Directas | Frío | Temperatura objetivo debe ser coherente con ma... |
| 4 | tiempo_preenfriado_h | numerica | 0 | 0.0 | 223 | Nivel 1 | Variables Causales Directas | Frío | Demora en remover calor de campo aumenta riesg... |
| 1 | desviacion_materia_seca_pct | numerica | 0 | 0.0 | 184 | Nivel 1 | Variables Causales Directas | Madurez | Captura heterogeneidad de madurez; puede gener... |
| 0 | materia_seca_pct | numerica | 0 | 0.0 | 1182 | Nivel 1 | Variables Causales Directas | Madurez | Indicador maestro de madurez funcional de la p... |
| 2 | riesgo_heterogeneidad_score | numerica | 0 | 0.0 | 1591 | Nivel 1 | Variables Causales Directas | Madurez | Score derivado para cuantificar riesgo de mezc... |
| 6 | indice_vigor_ndvi_base | numerica | 0 | 0.0 | 122 | Nivel 1 | Variables Causales Directas | Origen | Vigor vegetativo se asocia a condición fisioló... |
| 7 | subzona_agroclimatica | categorica | 0 | 0.0 | 4 | Nivel 1 | Variables Causales Directas | Origen | Define condiciones agroclimáticas y ventana de... |
| 8 | uniformidad_riego_pct | numerica | 0 | 0.0 | 101 | Nivel 1 | Variables Causales Directas | Origen | Variabilidad de riego puede generar heterogene... |
| 15 | tmax_c | numerica | 0 | 0.0 | 257 | Nivel 2 | Variables Explicativas Primarias | Clima | Temperatura máxima puede generar estrés térmico. |
| 14 | tmin_c | numerica | 0 | 0.0 | 188 | Nivel 2 | Variables Explicativas Primarias | Clima | Temperatura mínima precosecha afecta metabolis... |
| 22 | atmosfera_controlada | categorica | 0 | 0.0 | 2 | Nivel 2 | Variables Explicativas Primarias | Contenedor | Condición de atmósfera afecta conservación y m... |
| 16 | dias_muestreo_a_cosecha | numerica | 0 | 0.0 | 23 | Nivel 2 | Variables Explicativas Primarias | Cosecha | Ventana entre muestreo y cosecha afecta repres... |
| 18 | mes_cosecha | numerica | 0 | 0.0 | 7 | Nivel 2 | Variables Explicativas Primarias | Cosecha | Permite evaluar estacionalidad de cosecha. |
| 17 | semana_cosecha | numerica | 0 | 0.0 | 33 | Nivel 2 | Variables Explicativas Primarias | Cosecha | Captura momento fenológico y etapa de temporada. |
| 20 | firmeza_pulpa_lb | numerica | 0 | 0.0 | 290 | Nivel 2 | Variables Explicativas Primarias | Madurez | Resistencia estructural del fruto frente a dañ... |
| 21 | peso_fruto_g | numerica | 0 | 0.0 | 1615 | Nivel 2 | Variables Explicativas Primarias | Madurez | Peso captura condición física y calibre efectivo. |
| 10 | densidad_arboles_ha | numerica | 0 | 0.0 | 6 | Nivel 2 | Variables Explicativas Primarias | Origen | Densidad afecta competencia, luz, vigor y desa... |
| 9 | edad_arboles_anos | numerica | 0 | 0.0 | 20 | Nivel 2 | Variables Explicativas Primarias | Origen | Edad del huerto puede influir en vigor, produc... |
| 13 | mo_pct | numerica | 0 | 0.0 | 114 | Nivel 2 | Variables Explicativas Primarias | Origen | Materia orgánica se relaciona con estructura d... |
| 12 | ph_suelo | numerica | 0 | 0.0 | 93 | Nivel 2 | Variables Explicativas Primarias | Origen | pH afecta disponibilidad nutricional y condici... |
| 11 | portainjerto | categorica | 0 | 0.0 | 5 | Nivel 2 | Variables Explicativas Primarias | Origen | Portainjerto puede influir en vigor, absorción... |
| 19 | dias_cosecha_a_packing | numerica | 0 | 0.0 | 39 | Nivel 2 | Variables Explicativas Primarias | Packing | Tiempo desde cosecha a packing afecta condició... |
| 29 | tipo_contenedor | categorica | 0 | 0.0 | 3 | Nivel 3 | Variables de Entorno Operacional | Contenedor | Tipo de contenedor puede afectar conservación ... |
| 27 | naviera | categorica | 0 | 0.0 | 6 | Nivel 3 | Variables de Entorno Operacional | Logística | Naviera captura diferencias de servicio, ruta ... |
| 28 | puerto_salida | categorica | 0 | 0.0 | 3 | Nivel 3 | Variables de Entorno Operacional | Logística | Puerto puede asociarse a tiempos, infraestruct... |
| 24 | canal_destino | categorica | 0 | 0.0 | 2 | Nivel 3 | Variables de Entorno Operacional | Mercado | Canal puede reflejar exigencia de cliente y ni... |
| 23 | macro_mercado | categorica | 0 | 0.0 | 4 | Nivel 3 | Variables de Entorno Operacional | Mercado | Agrupa destinos con comportamiento logístico y... |
| 26 | linea_packing | categorica | 0 | 0.0 | 4 | Nivel 3 | Variables de Entorno Operacional | Packing | Diferencias de línea pueden afectar manipulaci... |
| 25 | packhouse_id | categorica | 0 | 0.0 | 4 | Nivel 3 | Variables de Entorno Operacional | Packing | Captura diferencias operacionales entre planta... |
| 30 | temporada | categorica | 0 | 0.0 | 7 | Nivel 3 | Variables de Entorno Operacional | Temporal | Captura cambios productivos, comerciales y cli... |
| 31 | mes_embarque | numerica | 0 | 0.0 | 9 | NaN | NaN | NaN | NaN |
| 32 | semana_embarque | numerica | 0 | 0.0 | 37 | NaN | NaN | NaN | NaN |
| nivel | nombre_nivel | variables_modelables | |
|---|---|---|---|
| 0 | Nivel 1 | Variables Causales Directas | 9 |
| 1 | Nivel 2 | Variables Explicativas Primarias | 14 |
| 2 | Nivel 3 | Variables de Entorno Operacional | 8 |
| 3 | NaN | NaN | 2 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_04_variables_modelables_por_nivel.png
Explicación estadística: La matriz X se construye exclusivamente con variables pre-despacho disponibles y no constantes. Las variables post-arribo quedan excluidas para evitar leakage. Lectura operacional: El modelo aprende desde señales disponibles antes del despacho: origen, madurez, cosecha, packing, logística, clima y contexto comercial. Recomendación ML: Mantener un diccionario de captura por nivel. Las variables Nivel 1 y Nivel 2 deben tener prioridad en calidad de dato por su vínculo fisiológico/operacional directo.
05_Split_Temporal¶
# ======================================================================================
# 05. SPLIT TEMPORAL — TRAIN / VALIDACIÓN / TEST
# ======================================================================================
print_step("05 — SPLIT TEMPORAL")
# --------------------------------------------------------------------------------------
# Corrección V6.0.1:
# En algunos joins pueden quedar columnas duplicadas con el mismo nombre, por ejemplo
# "temporada". En pandas, split_df["temporada"] devuelve un DataFrame si el nombre está
# duplicado; por eso una condición como .notna().nunique() puede volverse ambigua.
# Aquí se eliminan duplicados de columnas y se evita repetir variables de control.
# --------------------------------------------------------------------------------------
model_df = model_df.loc[:, ~pd.Index(model_df.columns).duplicated(keep="first")].copy()
valid_vars = list(dict.fromkeys([v for v in valid_vars if v in model_df.columns and v != TARGET]))
control_cols = [c for c in ["temporada", "fecha_embarque", "fecha_cosecha"] if c in model_df.columns and c not in valid_vars and c != TARGET]
split_cols = list(dict.fromkeys([TARGET] + valid_vars + control_cols))
split_df = model_df[split_cols].copy()
# Serie segura de temporada: siempre 1 dimensión, aun si en algún paso anterior existían duplicados.
temporada_s = None
if "temporada" in split_df.columns:
tmp = split_df.loc[:, "temporada"]
if isinstance(tmp, pd.DataFrame):
tmp = tmp.iloc[:, 0]
temporada_s = tmp.astype(str).where(tmp.notna(), np.nan)
split_method = ""
if temporada_s is not None and temporada_s.dropna().nunique() >= 3:
temporadas = sorted(temporada_s.dropna().unique().tolist())
train_seasons = temporadas[:-2]
val_season = temporadas[-2]
test_season = temporadas[-1]
idx_train = temporada_s.isin(train_seasons).to_numpy()
idx_val = temporada_s.eq(val_season).to_numpy()
idx_test = temporada_s.eq(test_season).to_numpy()
split_method = "temporal_por_temporada"
split_temporal_detalle = pd.DataFrame({
"rol": ["train", "validacion", "test"],
"temporadas": [", ".join(train_seasons), val_season, test_season]
})
display(split_temporal_detalle)
else:
# Fallback robusto: si no hay temporadas suficientes, usa split estratificado.
idx_train_tmp, idx_test_pos = train_test_split(
np.arange(len(split_df)),
test_size=0.20,
random_state=RANDOM_STATE,
stratify=y_all
)
y_tmp = y_all.iloc[idx_train_tmp]
idx_train_pos, idx_val_pos = train_test_split(
idx_train_tmp,
test_size=0.20,
random_state=RANDOM_STATE,
stratify=y_tmp
)
idx_train = np.zeros(len(split_df), dtype=bool); idx_train[idx_train_pos] = True
idx_val = np.zeros(len(split_df), dtype=bool); idx_val[idx_val_pos] = True
idx_test = np.zeros(len(split_df), dtype=bool); idx_test[idx_test_pos] = True
split_method = "estratificado_fallback"
X_train, y_train = X_all.loc[idx_train, valid_vars], y_all.loc[idx_train]
X_val, y_val = X_all.loc[idx_val, valid_vars], y_all.loc[idx_val]
X_test, y_test = X_all.loc[idx_test, valid_vars], y_all.loc[idx_test]
split_summary = pd.DataFrame([
{"split": "train", "filas": len(y_train), "positivos": int(y_train.sum()), "tasa_reclamo": y_train.mean()},
{"split": "validacion", "filas": len(y_val), "positivos": int(y_val.sum()), "tasa_reclamo": y_val.mean()},
{"split": "test", "filas": len(y_test), "positivos": int(y_test.sum()), "tasa_reclamo": y_test.mean()},
])
print("Método split:", split_method)
display(split_summary)
split_summary.to_csv(OUTPUT_DIR / "split_temporal_resumen_V6_0.csv", index=False)
plt.figure(figsize=(7,4))
plt.bar(split_summary["split"], split_summary["tasa_reclamo"]*100)
plt.title("Tasa de reclamo por split")
plt.ylabel("Tasa de reclamo (%)")
savefig("V6_05_tasa_reclamo_por_split.png")
print("Explicación estadística:")
print("El split temporal evita entrenar con información futura. La corrección V6.0.1 controla columnas duplicadas, especialmente 'temporada', para evitar ambigüedad en pandas.")
print("Lectura operacional:")
print("El test representa la temporada más reciente disponible y simula mejor el desempeño esperado en operación futura.")
print("Recomendación ML:")
print("No mezclar aleatoriamente temporadas si el objetivo es defender capacidad predictiva pre-despacho en una campaña futura. Usar fallback estratificado solo si no hay temporadas suficientes.")
==================================================================================================== 05 — SPLIT TEMPORAL ====================================================================================================
| rol | temporadas | |
|---|---|---|
| 0 | train | 2018_2019, 2019_2020, 2020_2021, 2021_2022, 20... |
| 1 | validacion | 2023_2024 |
| 2 | test | 2024_2025 |
Método split: temporal_por_temporada
| split | filas | positivos | tasa_reclamo | |
|---|---|---|---|---|
| 0 | train | 10529 | 101 | 0.009593 |
| 1 | validacion | 2093 | 24 | 0.011467 |
| 2 | test | 2114 | 29 | 0.013718 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_05_tasa_reclamo_por_split.png
Explicación estadística: El split temporal evita entrenar con información futura. La corrección V6.0.1 controla columnas duplicadas, especialmente 'temporada', para evitar ambigüedad en pandas. Lectura operacional: El test representa la temporada más reciente disponible y simula mejor el desempeño esperado en operación futura. Recomendación ML: No mezclar aleatoriamente temporadas si el objetivo es defender capacidad predictiva pre-despacho en una campaña futura. Usar fallback estratificado solo si no hay temporadas suficientes.
06_Seleccion_Variables_y_Entrenamiento¶
# ======================================================================================
# 06. SELECCIÓN DE VARIABLES + LABORATORIO DE REGRESIÓN LOGÍSTICA
# ======================================================================================
print_step("06 — SELECCIÓN DE VARIABLES Y ENTRENAMIENTO V6.0")
# --------------------------------------------------------------------------------------
# 06.1 Preprocesamiento base para selección LASSO
# --------------------------------------------------------------------------------------
num_features_all = [c for c in valid_vars if pd.api.types.is_numeric_dtype(X_train[c])]
cat_features_all = [c for c in valid_vars if c not in num_features_all]
try:
ohe_select = OneHotEncoder(handle_unknown="ignore", sparse_output=True, min_frequency=10)
except TypeError:
ohe_select = OneHotEncoder(handle_unknown="ignore", sparse=True)
pre_lasso = ColumnTransformer(
transformers=[
("num", Pipeline([("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler())]), num_features_all),
("cat", Pipeline([("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", ohe_select)]), cat_features_all),
],
remainder="drop"
)
def economic_benefit_from_cm(tn, fp, fn, tp, costo_reclamo=COSTO_RECLAMO_USD, costo_fp=COSTO_REVISION_FP_USD):
return (tp * costo_reclamo) - (fp * costo_fp)
def metrics_at_threshold(y_true, proba, threshold):
y_pred = (proba >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0,1]).ravel()
return {
"threshold": float(threshold),
"TN": int(tn), "FP": int(fp), "FN": int(fn), "TP": int(tp),
"accuracy": accuracy_score(y_true, y_pred),
"precision_ppv": precision_score(y_true, y_pred, zero_division=0),
"recall_sensitivity": recall_score(y_true, y_pred, zero_division=0),
"specificity": tn / (tn + fp) if (tn + fp) else 0,
"f1": f1_score(y_true, y_pred, zero_division=0),
"f2": fbeta_score(y_true, y_pred, beta=2, zero_division=0),
"balanced_accuracy": balanced_accuracy_score(y_true, y_pred),
"mcc": matthews_corrcoef(y_true, y_pred) if len(np.unique(y_pred)) > 1 else 0,
"beneficio_neto_usd": economic_benefit_from_cm(tn, fp, fn, tp),
}
def feature_name_to_original(name, num_vars, cat_vars):
clean = name.replace("num__", "").replace("cat__", "")
if clean in num_vars:
return clean
for v in sorted(cat_vars, key=len, reverse=True):
if clean == v or clean.startswith(v + "_"):
return v
return clean.split("_")[0]
# --------------------------------------------------------------------------------------
# 06.2 LASSO: elegir C que mantenga menos variables pero con señal predictiva razonable
# --------------------------------------------------------------------------------------
lasso_grid = [0.005, 0.01, 0.02, 0.05, 0.10, 0.20, 0.50, 1.00]
lasso_rows = []
lasso_models = {}
for C in lasso_grid:
pipe = Pipeline([
("pre", pre_lasso),
("clf", LogisticRegression(
penalty="l1", solver="saga", C=C, class_weight="balanced",
max_iter=6000, random_state=RANDOM_STATE, n_jobs=-1
))
])
try:
pipe.fit(X_train[valid_vars], y_train)
p_val = pipe.predict_proba(X_val[valid_vars])[:,1]
auc = roc_auc_score(y_val, p_val) if y_val.nunique() == 2 else np.nan
pr = average_precision_score(y_val, p_val)
coefs = pipe.named_steps["clf"].coef_[0]
fnames = pipe.named_steps["pre"].get_feature_names_out()
nonzero = int(np.sum(np.abs(coefs) > 1e-8))
original_nonzero = len(set(feature_name_to_original(f, num_features_all, cat_features_all) for f, c in zip(fnames, coefs) if abs(c) > 1e-8))
lasso_rows.append({"C": C, "roc_auc_val": auc, "pr_auc_val": pr, "coef_no_cero": nonzero, "variables_originales_no_cero": original_nonzero})
lasso_models[C] = pipe
except Exception as e:
lasso_rows.append({"C": C, "roc_auc_val": np.nan, "pr_auc_val": np.nan, "coef_no_cero": 0, "variables_originales_no_cero": 0, "error": str(e)[:200]})
lasso_summary = pd.DataFrame(lasso_rows)
# Preferimos modelos con entre MIN y MAX variables. Si no existen, tomamos mejor PR-AUC penalizado por complejidad.
valid_lasso = lasso_summary.dropna(subset=["pr_auc_val"]).copy()
valid_lasso["cumple_parsimonia"] = valid_lasso["variables_originales_no_cero"].between(MIN_VARIABLES_V6, MAX_VARIABLES_V6)
valid_lasso["score_v6"] = valid_lasso["pr_auc_val"] - 0.0005 * valid_lasso["variables_originales_no_cero"]
if valid_lasso["cumple_parsimonia"].any():
best_lasso_C = valid_lasso[valid_lasso["cumple_parsimonia"]].sort_values(["pr_auc_val", "score_v6"], ascending=False).iloc[0]["C"]
else:
best_lasso_C = valid_lasso.sort_values("score_v6", ascending=False).iloc[0]["C"]
print("Resumen LASSO por C:")
display(lasso_summary.sort_values("C"))
print("C LASSO seleccionado:", best_lasso_C)
lasso_summary.to_csv(OUTPUT_DIR / "V6_0_lasso_grid_seleccion_variables.csv", index=False)
best_lasso = lasso_models[best_lasso_C]
coefs = best_lasso.named_steps["clf"].coef_[0]
fnames = best_lasso.named_steps["pre"].get_feature_names_out()
coef_df_lasso = pd.DataFrame({"feature_ohe": fnames, "coef": coefs, "abs_coef": np.abs(coefs)})
coef_df_lasso["variable_original"] = coef_df_lasso["feature_ohe"].apply(lambda x: feature_name_to_original(x, num_features_all, cat_features_all))
var_importance_lasso = (coef_df_lasso.groupby("variable_original", as_index=False)
.agg(abs_coef_total=("abs_coef", "sum"), max_abs_coef=("abs_coef", "max"), n_coef_no_cero=("abs_coef", lambda s: int((s > 1e-8).sum())))
.sort_values(["abs_coef_total", "max_abs_coef"], ascending=False))
var_importance_lasso = var_importance_lasso[var_importance_lasso["n_coef_no_cero"] > 0].copy()
# Mantener menos variables: top MAX_VARIABLES_V6 con señal. Si LASSO deja muy pocas, completar con variables de mayor disponibilidad/nivel.
selected_vars_lasso = var_importance_lasso["variable_original"].head(MAX_VARIABLES_V6).tolist()
if len(selected_vars_lasso) < MIN_VARIABLES_V6:
fallback = feature_inventory.sort_values(["pct_missing", "n_unique"], ascending=[True, False])["variable"].tolist()
for v in fallback:
if v not in selected_vars_lasso and v in valid_vars:
selected_vars_lasso.append(v)
if len(selected_vars_lasso) >= MIN_VARIABLES_V6:
break
# --------------------------------------------------------------------------------------
# 06.3 VIF simple sobre numéricas seleccionadas para reducir colinealidad
# --------------------------------------------------------------------------------------
selected_vars_pre_vif = list(dict.fromkeys([v for v in selected_vars_lasso if v in valid_vars]))
selected_num_pre_vif = [v for v in selected_vars_pre_vif if v in num_features_all]
removed_vif = []
vif_table = pd.DataFrame()
if HAS_STATSMODELS and len(selected_num_pre_vif) >= 3:
try:
from statsmodels.stats.outliers_influence import variance_inflation_factor
tmp_num = X_train[selected_num_pre_vif].copy()
for c in tmp_num.columns:
tmp_num[c] = pd.to_numeric(tmp_num[c], errors="coerce")
tmp_num = tmp_num.fillna(tmp_num.median(numeric_only=True))
current = selected_num_pre_vif.copy()
while len(current) >= 3:
Xv = tmp_num[current].replace([np.inf, -np.inf], np.nan).fillna(0)
Xv = pd.DataFrame(StandardScaler().fit_transform(Xv), columns=current)
vif_values = []
for i, col in enumerate(current):
try:
vif_values.append(float(variance_inflation_factor(Xv.values, i)))
except Exception:
vif_values.append(np.inf)
vdf = pd.DataFrame({"variable": current, "VIF": vif_values}).sort_values("VIF", ascending=False)
max_vif = vdf.iloc[0]
vif_table = vdf.copy()
if max_vif["VIF"] <= 10 or len(current) <= 3:
break
removed_vif.append({"variable_removida": max_vif["variable"], "VIF": max_vif["VIF"]})
current.remove(max_vif["variable"])
selected_num_after_vif = current
except Exception as e:
print("[WARNING] VIF no pudo ejecutarse:", e)
selected_num_after_vif = selected_num_pre_vif
else:
selected_num_after_vif = selected_num_pre_vif
selected_cat = [v for v in selected_vars_pre_vif if v not in num_features_all]
selected_vars_v6 = [v for v in selected_vars_pre_vif if (v in selected_cat or v in selected_num_after_vif)]
selected_vars_v6 = selected_vars_v6[:MAX_VARIABLES_V6]
selected_feature_inventory_v6 = feature_inventory[feature_inventory["variable"].isin(selected_vars_v6)].copy()
selected_feature_inventory_v6["orden_modelo_v6"] = selected_feature_inventory_v6["variable"].apply(lambda v: selected_vars_v6.index(v) + 1 if v in selected_vars_v6 else np.nan)
selected_feature_inventory_v6 = selected_feature_inventory_v6.sort_values("orden_modelo_v6")
print("Variables iniciales V4.0 disponibles:", len(valid_vars))
print("Variables finales V6.0:", len(selected_vars_v6))
print(selected_vars_v6)
display(selected_feature_inventory_v6)
selected_feature_inventory_v6.to_csv(OUTPUT_DIR / "V6_0_variables_finales_menos_variables.csv", index=False)
var_importance_lasso.to_csv(OUTPUT_DIR / "V6_0_importancia_lasso_por_variable.csv", index=False)
if not vif_table.empty:
display(vif_table)
vif_table.to_csv(OUTPUT_DIR / "V6_0_vif_variables_numericas.csv", index=False)
if removed_vif:
display(pd.DataFrame(removed_vif))
pd.DataFrame(removed_vif).to_csv(OUTPUT_DIR / "V6_0_variables_removidas_por_vif.csv", index=False)
plt.figure(figsize=(9,4))
plot_df = selected_feature_inventory_v6.groupby(["nivel", "nombre_nivel"], dropna=False).size().reset_index(name="variables")
plt.bar(plot_df["nivel"].astype(str) + "\n" + plot_df["nombre_nivel"].astype(str), plot_df["variables"])
plt.title("V6.0 — Variables finales por nivel")
plt.ylabel("Cantidad")
plt.xticks(rotation=20, ha="right")
savefig("V6_0_variables_finales_por_nivel.png")
print("Explicación estadística:")
print("V6.0 reduce dimensionalidad con LASSO y controla multicolinealidad numérica con VIF. Esto mejora estabilidad de coeficientes y defendibilidad.")
print("Lectura operacional:")
print("Menos variables implica menor dependencia de datos difíciles de capturar y mayor facilidad de explicar por qué un lote fue priorizado.")
print("Recomendación ML:")
print("Usar este set reducido como modelo base interpretable. Si una variable clave de negocio queda fuera, puede reingresarse como restricción experta y compararse contra el ranking automático.")
# --------------------------------------------------------------------------------------
# 06.4 Laboratorio de modelos candidatos con las variables reducidas
# --------------------------------------------------------------------------------------
num_features_v6 = [c for c in selected_vars_v6 if c in num_features_all]
cat_features_v6 = [c for c in selected_vars_v6 if c not in num_features_v6]
try:
ohe_final = OneHotEncoder(handle_unknown="ignore", sparse_output=True, min_frequency=10)
except TypeError:
ohe_final = OneHotEncoder(handle_unknown="ignore", sparse=True)
pre_final = ColumnTransformer(
transformers=[
("num", Pipeline([("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler())]), num_features_v6),
("cat", Pipeline([("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", ohe_final)]), cat_features_v6),
],
remainder="drop"
)
candidate_configs = []
for penalty in ["l2", "l1"]:
for C in [0.01, 0.05, 0.10, 0.25, 0.50, 1.00, 2.00]:
for cw_name, cw in [("none", None), ("balanced", "balanced"), ("risk_5", {0:1,1:5}), ("risk_10", {0:1,1:10}), ("risk_20", {0:1,1:20}), ("risk_40", {0:1,1:40})]:
solver = "liblinear" if penalty in ["l1", "l2"] else "saga"
candidate_configs.append({"penalty": penalty, "C": C, "class_weight_name": cw_name, "class_weight": cw, "solver": solver, "l1_ratio": None})
# ElasticNet sólo con saga.
for C in [0.05, 0.10, 0.25, 0.50, 1.00]:
for l1_ratio in [0.25, 0.50, 0.75]:
for cw_name, cw in [("balanced", "balanced"), ("risk_10", {0:1,1:10}), ("risk_20", {0:1,1:20})]:
candidate_configs.append({"penalty": "elasticnet", "C": C, "class_weight_name": cw_name, "class_weight": cw, "solver": "saga", "l1_ratio": l1_ratio})
model_rows = []
model_store = {}
threshold_rows = []
for i, cfg in enumerate(candidate_configs, start=1):
model_id = f"V6_{i:03d}_{cfg['penalty']}_C{cfg['C']}_{cfg['class_weight_name']}_l1r{cfg['l1_ratio']}"
clf_params = dict(
penalty=cfg["penalty"], C=cfg["C"], solver=cfg["solver"], class_weight=cfg["class_weight"],
max_iter=6000, random_state=RANDOM_STATE
)
if cfg["penalty"] == "elasticnet":
clf_params["l1_ratio"] = cfg["l1_ratio"]
try:
pipe = Pipeline([("pre", pre_final), ("clf", LogisticRegression(**clf_params))])
pipe.fit(X_train[selected_vars_v6], y_train)
p_val = pipe.predict_proba(X_val[selected_vars_v6])[:,1]
roc_val = roc_auc_score(y_val, p_val) if y_val.nunique() == 2 else np.nan
pr_val = average_precision_score(y_val, p_val)
thr_metrics = []
for th in THRESHOLD_GRID:
m = metrics_at_threshold(y_val, p_val, th)
m.update({"model_id": model_id})
threshold_rows.append(m)
thr_metrics.append(m)
thr_df = pd.DataFrame(thr_metrics)
# Selección experta: beneficio, luego F2, luego PR-AUC. Penaliza modelos que no detectan nada.
thr_df["score_operacional"] = thr_df["beneficio_neto_usd"] + 1000 * thr_df["TP"] + 10000 * thr_df["f2"]
best_thr_row = thr_df.sort_values(["score_operacional", "beneficio_neto_usd", "f2", "recall_sensitivity"], ascending=False).iloc[0]
model_rows.append({
"model_id": model_id, "penalty": cfg["penalty"], "C": cfg["C"], "class_weight": cfg["class_weight_name"], "l1_ratio": cfg["l1_ratio"],
"roc_auc_val": roc_val, "pr_auc_val": pr_val,
"best_threshold_val": best_thr_row["threshold"], "TP_val": best_thr_row["TP"], "FP_val": best_thr_row["FP"], "FN_val": best_thr_row["FN"], "TN_val": best_thr_row["TN"],
"recall_val": best_thr_row["recall_sensitivity"], "precision_val": best_thr_row["precision_ppv"], "f2_val": best_thr_row["f2"],
"beneficio_val_usd": best_thr_row["beneficio_neto_usd"], "score_operacional": best_thr_row["score_operacional"],
"estado": "OK"
})
model_store[model_id] = pipe
except Exception as e:
model_rows.append({"model_id": model_id, "penalty": cfg["penalty"], "C": cfg["C"], "class_weight": cfg["class_weight_name"], "l1_ratio": cfg["l1_ratio"], "estado": "ERROR", "error": str(e)[:250]})
model_leaderboard = pd.DataFrame(model_rows)
threshold_leaderboard_val = pd.DataFrame(threshold_rows)
model_leaderboard_ok = model_leaderboard[model_leaderboard["estado"].eq("OK")].copy()
model_leaderboard_ok = model_leaderboard_ok.sort_values(["score_operacional", "beneficio_val_usd", "f2_val", "pr_auc_val"], ascending=False)
best_model_id = model_leaderboard_ok.iloc[0]["model_id"]
best_threshold = float(model_leaderboard_ok.iloc[0]["best_threshold_val"])
best_model = model_store[best_model_id]
print("Modelos candidatos entrenados OK:", len(model_leaderboard_ok))
print("Modelo seleccionado V6.0:", best_model_id)
print("Threshold seleccionado por validación:", best_threshold)
display(model_leaderboard_ok.head(20))
model_leaderboard.to_csv(OUTPUT_DIR / "V6_0_leaderboard_modelos_logisticos.csv", index=False)
threshold_leaderboard_val.to_csv(OUTPUT_DIR / "V6_0_thresholds_validacion_todos_modelos.csv", index=False)
plt.figure(figsize=(10,4))
top_plot = model_leaderboard_ok.head(15).copy()
plt.barh(top_plot["model_id"], top_plot["beneficio_val_usd"])
plt.title("Top modelos V6.0 por beneficio validación")
plt.xlabel("Beneficio neto USD")
plt.gca().invert_yaxis()
savefig("V6_0_top_modelos_beneficio_validacion.png")
print("Explicación estadística:")
print("Se comparan regresiones L1, L2 y ElasticNet con distintos pesos de clase y regularización C. La selección se realiza en validación temporal.")
print("Lectura operacional:")
print("El mejor modelo no es el de mayor accuracy, sino el que detecta reclamos con mejor trade-off económico entre TP y FP.")
print("Recomendación ML:")
print("Mantener el leaderboard completo como evidencia de experimentación. La defensa debe mostrar por qué se eligió el modelo final y no sólo reportar una métrica aislada.")
==================================================================================================== 06 — SELECCIÓN DE VARIABLES Y ENTRENAMIENTO V6.0 ==================================================================================================== Resumen LASSO por C:
| C | roc_auc_val | pr_auc_val | coef_no_cero | variables_originales_no_cero | |
|---|---|---|---|---|---|
| 0 | 0.005 | 0.735520 | 0.029087 | 58 | 33 |
| 1 | 0.010 | 0.665176 | 0.017672 | 63 | 33 |
| 2 | 0.020 | 0.659799 | 0.016485 | 63 | 33 |
| 3 | 0.050 | 0.730405 | 0.040319 | 63 | 33 |
| 4 | 0.100 | 0.689594 | 0.026249 | 64 | 33 |
| 5 | 0.200 | 0.759133 | 0.033847 | 64 | 33 |
| 6 | 0.500 | 0.697942 | 0.030766 | 64 | 33 |
| 7 | 1.000 | 0.779362 | 0.030759 | 64 | 33 |
C LASSO seleccionado: 0.05 Variables iniciales V4.0 disponibles: 33 Variables finales V6.0: 18 ['macro_mercado', 'portainjerto', 'temporada', 'tipo_contenedor', 'linea_packing', 'puerto_salida', 'naviera', 'subzona_agroclimatica', 'packhouse_id', 'ph_suelo', 'materia_seca_pct', 'edad_arboles_anos', 'firmeza_pulpa_lb', 'densidad_arboles_ha', 'quiebre_cadena_frio_h', 'temperatura_setpoint_frio_c', 'canal_destino', 'atmosfera_controlada']
| variable | tipo_modelo | n_missing | pct_missing | n_unique | nivel | nombre_nivel | area | justificacion | orden_modelo_v6 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 23 | macro_mercado | categorica | 0 | 0.0 | 4 | Nivel 3 | Variables de Entorno Operacional | Mercado | Agrupa destinos con comportamiento logístico y... | 1 |
| 11 | portainjerto | categorica | 0 | 0.0 | 5 | Nivel 2 | Variables Explicativas Primarias | Origen | Portainjerto puede influir en vigor, absorción... | 2 |
| 30 | temporada | categorica | 0 | 0.0 | 7 | Nivel 3 | Variables de Entorno Operacional | Temporal | Captura cambios productivos, comerciales y cli... | 3 |
| 29 | tipo_contenedor | categorica | 0 | 0.0 | 3 | Nivel 3 | Variables de Entorno Operacional | Contenedor | Tipo de contenedor puede afectar conservación ... | 4 |
| 26 | linea_packing | categorica | 0 | 0.0 | 4 | Nivel 3 | Variables de Entorno Operacional | Packing | Diferencias de línea pueden afectar manipulaci... | 5 |
| 28 | puerto_salida | categorica | 0 | 0.0 | 3 | Nivel 3 | Variables de Entorno Operacional | Logística | Puerto puede asociarse a tiempos, infraestruct... | 6 |
| 27 | naviera | categorica | 0 | 0.0 | 6 | Nivel 3 | Variables de Entorno Operacional | Logística | Naviera captura diferencias de servicio, ruta ... | 7 |
| 7 | subzona_agroclimatica | categorica | 0 | 0.0 | 4 | Nivel 1 | Variables Causales Directas | Origen | Define condiciones agroclimáticas y ventana de... | 8 |
| 25 | packhouse_id | categorica | 0 | 0.0 | 4 | Nivel 3 | Variables de Entorno Operacional | Packing | Captura diferencias operacionales entre planta... | 9 |
| 12 | ph_suelo | numerica | 0 | 0.0 | 93 | Nivel 2 | Variables Explicativas Primarias | Origen | pH afecta disponibilidad nutricional y condici... | 10 |
| 0 | materia_seca_pct | numerica | 0 | 0.0 | 1182 | Nivel 1 | Variables Causales Directas | Madurez | Indicador maestro de madurez funcional de la p... | 11 |
| 9 | edad_arboles_anos | numerica | 0 | 0.0 | 20 | Nivel 2 | Variables Explicativas Primarias | Origen | Edad del huerto puede influir en vigor, produc... | 12 |
| 20 | firmeza_pulpa_lb | numerica | 0 | 0.0 | 290 | Nivel 2 | Variables Explicativas Primarias | Madurez | Resistencia estructural del fruto frente a dañ... | 13 |
| 10 | densidad_arboles_ha | numerica | 0 | 0.0 | 6 | Nivel 2 | Variables Explicativas Primarias | Origen | Densidad afecta competencia, luz, vigor y desa... | 14 |
| 3 | quiebre_cadena_frio_h | numerica | 0 | 0.0 | 173 | Nivel 1 | Variables Causales Directas | Frío | Mide exposición térmica fuera de rango; puede ... | 15 |
| 5 | temperatura_setpoint_frio_c | numerica | 0 | 0.0 | 24 | Nivel 1 | Variables Causales Directas | Frío | Temperatura objetivo debe ser coherente con ma... | 16 |
| 24 | canal_destino | categorica | 0 | 0.0 | 2 | Nivel 3 | Variables de Entorno Operacional | Mercado | Canal puede reflejar exigencia de cliente y ni... | 17 |
| 22 | atmosfera_controlada | categorica | 0 | 0.0 | 2 | Nivel 2 | Variables Explicativas Primarias | Contenedor | Condición de atmósfera afecta conservación y m... | 18 |
| variable | VIF | |
|---|---|---|
| 1 | materia_seca_pct | 5.788673 |
| 3 | firmeza_pulpa_lb | 5.784777 |
| 5 | quiebre_cadena_frio_h | 1.061216 |
| 6 | temperatura_setpoint_frio_c | 1.060959 |
| 2 | edad_arboles_anos | 1.004547 |
| 0 | ph_suelo | 1.003118 |
| 4 | densidad_arboles_ha | 1.002942 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_variables_finales_por_nivel.png
Explicación estadística: V6.0 reduce dimensionalidad con LASSO y controla multicolinealidad numérica con VIF. Esto mejora estabilidad de coeficientes y defendibilidad. Lectura operacional: Menos variables implica menor dependencia de datos difíciles de capturar y mayor facilidad de explicar por qué un lote fue priorizado. Recomendación ML: Usar este set reducido como modelo base interpretable. Si una variable clave de negocio queda fuera, puede reingresarse como restricción experta y compararse contra el ranking automático. Modelos candidatos entrenados OK: 129 Modelo seleccionado V6.0: V6_067_l1_C0.5_none_l1rNone Threshold seleccionado por validación: 0.03
| model_id | penalty | C | class_weight | l1_ratio | roc_auc_val | pr_auc_val | best_threshold_val | TP_val | FP_val | FN_val | TN_val | recall_val | precision_val | f2_val | beneficio_val_usd | score_operacional | estado | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 66 | V6_067_l1_C0.5_none_l1rNone | l1 | 0.50 | none | NaN | 0.758438 | 0.049735 | 0.030 | 6 | 59 | 18 | 2010 | 0.250000 | 0.092308 | 0.186335 | 271500 | 279363.354037 | OK |
| 12 | V6_013_l2_C0.1_none_l1rNone | l2 | 0.10 | none | NaN | 0.766292 | 0.049873 | 0.030 | 7 | 107 | 17 | 1962 | 0.291667 | 0.061404 | 0.166667 | 259500 | 268166.666667 | OK |
| 72 | V6_073_l1_C1.0_none_l1rNone | l1 | 1.00 | none | NaN | 0.757713 | 0.048099 | 0.030 | 6 | 74 | 18 | 1995 | 0.250000 | 0.075000 | 0.170455 | 249000 | 256704.545455 | OK |
| 56 | V6_057_l1_C0.1_risk_5_l1rNone | l1 | 0.10 | risk_5 | NaN | 0.759908 | 0.045798 | 0.125 | 6 | 77 | 18 | 1992 | 0.250000 | 0.072289 | 0.167598 | 244500 | 252175.977654 | OK |
| 47 | V6_048_l1_C0.01_risk_40_l1rNone | l1 | 0.01 | risk_40 | NaN | 0.767017 | 0.039447 | 0.500 | 6 | 82 | 18 | 1987 | 0.250000 | 0.068182 | 0.163043 | 237000 | 244630.434783 | OK |
| 62 | V6_063_l1_C0.25_risk_5_l1rNone | l1 | 0.25 | risk_5 | NaN | 0.758398 | 0.047348 | 0.125 | 6 | 94 | 18 | 1975 | 0.250000 | 0.060000 | 0.153061 | 219000 | 226530.612245 | OK |
| 3 | V6_004_l2_C0.01_risk_10_l1rNone | l2 | 0.01 | risk_10 | NaN | 0.772273 | 0.049217 | 0.200 | 7 | 138 | 17 | 1931 | 0.291667 | 0.048276 | 0.145228 | 213000 | 221452.282158 | OK |
| 6 | V6_007_l2_C0.05_none_l1rNone | l2 | 0.05 | none | NaN | 0.757955 | 0.051366 | 0.020 | 12 | 342 | 12 | 1727 | 0.500000 | 0.033898 | 0.133333 | 207000 | 220333.333333 | OK |
| 68 | V6_069_l1_C0.5_risk_5_l1rNone | l1 | 0.50 | risk_5 | NaN | 0.760110 | 0.046763 | 0.125 | 6 | 102 | 18 | 1967 | 0.250000 | 0.055556 | 0.147059 | 207000 | 214470.588235 | OK |
| 8 | V6_009_l2_C0.05_risk_5_l1rNone | l2 | 0.05 | risk_5 | NaN | 0.767098 | 0.046233 | 0.075 | 15 | 470 | 9 | 1599 | 0.625000 | 0.030928 | 0.129088 | 195000 | 211290.877797 | OK |
| 30 | V6_031_l2_C1.0_none_l1rNone | l2 | 1.00 | none | NaN | 0.760814 | 0.047722 | 0.040 | 5 | 64 | 19 | 2005 | 0.208333 | 0.072464 | 0.151515 | 204000 | 210515.151515 | OK |
| 74 | V6_075_l1_C1.0_risk_5_l1rNone | l1 | 1.00 | risk_5 | NaN | 0.762607 | 0.046580 | 0.125 | 6 | 107 | 18 | 1962 | 0.250000 | 0.053097 | 0.143541 | 199500 | 206935.406699 | OK |
| 36 | V6_037_l2_C2.0_none_l1rNone | l2 | 2.00 | none | NaN | 0.760955 | 0.047166 | 0.040 | 5 | 67 | 19 | 2002 | 0.208333 | 0.069444 | 0.148810 | 199500 | 205988.095238 | OK |
| 80 | V6_081_l1_C2.0_risk_5_l1rNone | l1 | 2.00 | risk_5 | NaN | 0.763573 | 0.046334 | 0.125 | 6 | 109 | 18 | 1960 | 0.250000 | 0.052174 | 0.142180 | 196500 | 203921.800948 | OK |
| 39 | V6_040_l2_C2.0_risk_10_l1rNone | l2 | 2.00 | risk_10 | NaN | 0.764097 | 0.046161 | 0.250 | 6 | 109 | 18 | 1960 | 0.250000 | 0.052174 | 0.142180 | 196500 | 203921.800948 | OK |
| 46 | V6_047_l1_C0.01_risk_20_l1rNone | l1 | 0.01 | risk_20 | NaN | 0.765446 | 0.039624 | 0.300 | 6 | 111 | 18 | 1958 | 0.250000 | 0.051282 | 0.140845 | 193500 | 200908.450704 | OK |
| 91 | V6_092_elasticnet_C0.05_risk_10_l1r0.75 | elasticnet | 0.05 | risk_10 | 0.75 | 0.759364 | 0.047993 | 0.200 | 6 | 112 | 18 | 1957 | 0.250000 | 0.050847 | 0.140187 | 192000 | 199401.869159 | OK |
| 51 | V6_052_l1_C0.05_risk_10_l1rNone | l1 | 0.05 | risk_10 | NaN | 0.761076 | 0.043259 | 0.200 | 6 | 112 | 18 | 1957 | 0.250000 | 0.050847 | 0.140187 | 192000 | 199401.869159 | OK |
| 14 | V6_015_l2_C0.1_risk_5_l1rNone | l2 | 0.10 | risk_5 | NaN | 0.764439 | 0.044928 | 0.150 | 5 | 74 | 19 | 1995 | 0.208333 | 0.063291 | 0.142857 | 189000 | 195428.571429 | OK |
| 88 | V6_089_elasticnet_C0.05_risk_10_l1r0.5 | elasticnet | 0.05 | risk_10 | 0.50 | 0.761056 | 0.047541 | 0.200 | 6 | 116 | 18 | 1953 | 0.250000 | 0.049180 | 0.137615 | 186000 | 193376.146789 | OK |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_top_modelos_beneficio_validacion.png
Explicación estadística: Se comparan regresiones L1, L2 y ElasticNet con distintos pesos de clase y regularización C. La selección se realiza en validación temporal. Lectura operacional: El mejor modelo no es el de mayor accuracy, sino el que detecta reclamos con mejor trade-off económico entre TP y FP. Recomendación ML: Mantener el leaderboard completo como evidencia de experimentación. La defensa debe mostrar por qué se eligió el modelo final y no sólo reportar una métrica aislada.
07_Calibracion¶
# ======================================================================================
# 07. CALIBRACIÓN — DIAGNÓSTICO DE PROBABILIDADES
# ======================================================================================
print_step("07 — CALIBRACIÓN V6.0")
# Probabilidades raw del modelo seleccionado.
p_val_raw = best_model.predict_proba(X_val[selected_vars_v6])[:,1]
p_test_raw = best_model.predict_proba(X_test[selected_vars_v6])[:,1]
calibration_used = "raw_no_calibrado"
p_val = p_val_raw.copy()
p_test = p_test_raw.copy()
# Calibración opcional sólo como alternativa diagnóstica. Se usa cv='prefit' si está disponible.
try:
try:
calibrator = CalibratedClassifierCV(best_model, method="sigmoid", cv="prefit")
except TypeError:
calibrator = CalibratedClassifierCV(estimator=best_model, method="sigmoid", cv="prefit")
calibrator.fit(X_val[selected_vars_v6], y_val)
p_val_cal = calibrator.predict_proba(X_val[selected_vars_v6])[:,1]
p_test_cal = calibrator.predict_proba(X_test[selected_vars_v6])[:,1]
brier_raw = brier_score_loss(y_val, p_val_raw)
brier_cal = brier_score_loss(y_val, p_val_cal)
# Usar calibración sólo si mejora Brier sin destruir ranking PR-AUC.
if brier_cal <= brier_raw:
p_val = p_val_cal
p_test = p_test_cal
calibration_used = "sigmoid_prefit_validacion"
print("Brier raw:", round(brier_raw, 5), "| Brier calibrado:", round(brier_cal, 5), "| usado:", calibration_used)
except Exception as e:
print("[WARNING] Calibración no aplicada:", e)
calibration_rows = []
for split_name, y_true, proba in [("validacion", y_val, p_val), ("test", y_test, p_test)]:
calibration_rows.append({
"split": split_name,
"brier_score": brier_score_loss(y_true, proba),
"roc_auc": roc_auc_score(y_true, proba) if y_true.nunique() == 2 else np.nan,
"pr_auc": average_precision_score(y_true, proba),
"proba_min": float(np.min(proba)),
"proba_p50": float(np.percentile(proba, 50)),
"proba_p90": float(np.percentile(proba, 90)),
"proba_max": float(np.max(proba)),
})
calibration_summary = pd.DataFrame(calibration_rows)
display(calibration_summary)
calibration_summary.to_csv(OUTPUT_DIR / "V6_0_calibracion_resumen.csv", index=False)
plt.figure(figsize=(6,5))
try:
frac_pos, mean_pred = calibration_curve(y_val, p_val, n_bins=8, strategy="quantile")
plt.plot(mean_pred, frac_pos, marker="o", label="Validación")
plt.plot([0,1], [0,1], linestyle="--", label="Perfecta")
plt.title("Curva de calibración — Validación")
plt.xlabel("Probabilidad media predicha")
plt.ylabel("Frecuencia observada")
plt.legend()
savefig("V6_0_curva_calibracion_validacion.png")
except Exception as e:
print("No fue posible graficar calibración:", e)
print("Explicación estadística:")
print("La calibración evalúa si una probabilidad predicha de 10% se comporta empíricamente como un riesgo cercano al 10%.")
print("Lectura operacional:")
print("En eventos raros, el ranking puede ser útil aunque las probabilidades absolutas requieran calibración.")
print("Recomendación ML:")
print("Usar probabilidades para priorización y revisar calibración por temporada antes de fijar reglas automáticas.")
==================================================================================================== 07 — CALIBRACIÓN V6.0 ==================================================================================================== Brier raw: 0.0112 | Brier calibrado: 0.01118 | usado: sigmoid_prefit_validacion
| split | brier_score | roc_auc | pr_auc | proba_min | proba_p50 | proba_p90 | proba_max | |
|---|---|---|---|---|---|---|---|---|
| 0 | validacion | 0.011181 | 0.758438 | 0.049735 | 0.000610 | 0.009608 | 0.025106 | 0.113134 |
| 1 | test | 0.013524 | 0.647284 | 0.026447 | 0.000384 | 0.009303 | 0.025301 | 0.136938 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_curva_calibracion_validacion.png
Explicación estadística: La calibración evalúa si una probabilidad predicha de 10% se comporta empíricamente como un riesgo cercano al 10%. Lectura operacional: En eventos raros, el ranking puede ser útil aunque las probabilidades absolutas requieran calibración. Recomendación ML: Usar probabilidades para priorización y revisar calibración por temporada antes de fijar reglas automáticas.
08_Metricas_y_Matriz_Confusion¶
# ======================================================================================
# 08. MÉTRICAS COMPLETAS — MATRIZ DE CONFUSIÓN, ROC, PR, THRESHOLDS
# ======================================================================================
print_step("08 — MÉTRICAS Y MATRIZ DE CONFUSIÓN V6.0")
def eval_split(split_name, y_true, proba, threshold):
m = metrics_at_threshold(y_true, proba, threshold)
m.update({
"split": split_name,
"roc_auc": roc_auc_score(y_true, proba) if y_true.nunique() == 2 else np.nan,
"pr_auc": average_precision_score(y_true, proba),
"brier_score": brier_score_loss(y_true, proba),
"threshold_usado": threshold,
})
return m
metrics_summary = pd.DataFrame([
eval_split("validacion", y_val, p_val, best_threshold),
eval_split("test", y_test, p_test, best_threshold),
])
display(metrics_summary)
metrics_summary.to_csv(OUTPUT_DIR / "V6_0_metricas_validacion_test.csv", index=False)
# Matriz de confusión como tabla y heatmap.
for split_name, y_true, proba in [("validacion", y_val, p_val), ("test", y_test, p_test)]:
y_pred = (proba >= best_threshold).astype(int)
cm = confusion_matrix(y_true, y_pred, labels=[0,1])
cm_df = pd.DataFrame(cm, index=["Real 0", "Real 1"], columns=["Pred 0", "Pred 1"])
print(f"\nMatriz de confusión — {split_name.upper()} | threshold={best_threshold:.4f}")
display(cm_df)
cm_df.to_csv(OUTPUT_DIR / f"V6_0_matriz_confusion_{split_name}.csv")
plt.figure(figsize=(5,4))
plt.imshow(cm, aspect="auto")
plt.title(f"Matriz de confusión — {split_name}")
plt.xticks([0,1], ["Pred 0", "Pred 1"])
plt.yticks([0,1], ["Real 0", "Real 1"])
for (i,j), val in np.ndenumerate(cm):
plt.text(j, i, int(val), ha="center", va="center")
plt.colorbar(label="Casos")
savefig(f"V6_0_matriz_confusion_{split_name}.png")
# Curvas ROC y PR.
for split_name, y_true, proba in [("validacion", y_val, p_val), ("test", y_test, p_test)]:
if y_true.nunique() == 2:
fpr, tpr, _ = roc_curve(y_true, proba)
precision, recall, _ = precision_recall_curve(y_true, proba)
plt.figure(figsize=(6,4))
plt.plot(fpr, tpr, label=f"ROC-AUC={roc_auc_score(y_true, proba):.3f}")
plt.plot([0,1],[0,1], linestyle="--")
plt.title(f"Curva ROC — {split_name}")
plt.xlabel("FPR")
plt.ylabel("TPR / Recall")
plt.legend()
savefig(f"V6_0_roc_{split_name}.png")
plt.figure(figsize=(6,4))
plt.plot(recall, precision, label=f"PR-AUC={average_precision_score(y_true, proba):.3f}")
plt.title(f"Curva Precision-Recall — {split_name}")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.legend()
savefig(f"V6_0_pr_{split_name}.png")
# Tabla de thresholds en test.
thr_test_rows = []
for th in THRESHOLD_GRID:
row = metrics_at_threshold(y_test, p_test, th)
row["split"] = "test"
thr_test_rows.append(row)
threshold_table_test = pd.DataFrame(thr_test_rows)
display(threshold_table_test[["threshold","TP","FP","FN","TN","recall_sensitivity","precision_ppv","f2","beneficio_neto_usd"]])
threshold_table_test.to_csv(OUTPUT_DIR / "V6_0_thresholds_test.csv", index=False)
plt.figure(figsize=(8,4))
plt.plot(threshold_table_test["threshold"], threshold_table_test["beneficio_neto_usd"], marker="o")
plt.axvline(best_threshold, linestyle="--", label=f"threshold validación {best_threshold:.3f}")
plt.title("Beneficio neto por threshold — Test")
plt.xlabel("Threshold")
plt.ylabel("Beneficio neto USD")
plt.legend()
savefig("V6_0_beneficio_threshold_test.png")
# Diagnóstico automático honesto.
test_row = metrics_summary[metrics_summary["split"].eq("test")].iloc[0]
if test_row["TP"] == 0:
diagnostico_v6 = "NO APTO OPERATIVO: el modelo no detecta reclamos reales en test con el threshold seleccionado."
elif test_row["beneficio_neto_usd"] <= 0:
diagnostico_v6 = "APTO SOLO COMO PRIORIZADOR EXPERIMENTAL: detecta reclamos, pero el costo de falsas alarmas supera el beneficio bajo supuestos actuales."
else:
diagnostico_v6 = "APTO COMO PRIORIZADOR DE REVISIÓN: detecta reclamos y genera beneficio neto positivo bajo los supuestos definidos."
print("Diagnóstico experto V6.0:", diagnostico_v6)
print("Explicación estadística:")
print("La matriz de confusión transforma probabilidades en decisiones. En eventos raros, PR-AUC, Recall, FP y TP son más informativos que Accuracy.")
print("Lectura operacional:")
print("TP son reclamos priorizados correctamente; FN son reclamos que escapan; FP son revisiones innecesarias; TN son liberaciones correctas.")
print("Recomendación ML:")
print("No declarar el modelo operativo si TP=0 o si el beneficio económico es negativo. Ajustar threshold por capacidad real de revisión.")
==================================================================================================== 08 — MÉTRICAS Y MATRIZ DE CONFUSIÓN V6.0 ====================================================================================================
| threshold | TN | FP | FN | TP | accuracy | precision_ppv | recall_sensitivity | specificity | f1 | f2 | balanced_accuracy | mcc | beneficio_neto_usd | split | roc_auc | pr_auc | brier_score | threshold_usado | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.03 | 1946 | 123 | 18 | 6 | 0.932633 | 0.046512 | 0.250000 | 0.940551 | 0.078431 | 0.133333 | 0.595275 | 0.084359 | 175500 | validacion | 0.758438 | 0.049735 | 0.011181 | 0.03 |
| 1 | 0.03 | 1960 | 125 | 25 | 4 | 0.929044 | 0.031008 | 0.137931 | 0.940048 | 0.050633 | 0.081633 | 0.538989 | 0.037893 | 52500 | test | 0.647284 | 0.026447 | 0.013524 | 0.03 |
Matriz de confusión — VALIDACION | threshold=0.0300
| Pred 0 | Pred 1 | |
|---|---|---|
| Real 0 | 1946 | 123 |
| Real 1 | 18 | 6 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_matriz_confusion_validacion.png
Matriz de confusión — TEST | threshold=0.0300
| Pred 0 | Pred 1 | |
|---|---|---|
| Real 0 | 1960 | 125 |
| Real 1 | 25 | 4 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_matriz_confusion_test.png
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_roc_validacion.png
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_pr_validacion.png
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_roc_test.png
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_pr_test.png
| threshold | TP | FP | FN | TN | recall_sensitivity | precision_ppv | f2 | beneficio_neto_usd | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.010 | 19 | 968 | 10 | 1117 | 0.655172 | 0.019250 | 0.086129 | -312000 |
| 1 | 0.020 | 9 | 367 | 20 | 1718 | 0.310345 | 0.023936 | 0.091463 | -10500 |
| 2 | 0.030 | 4 | 125 | 25 | 1960 | 0.137931 | 0.031008 | 0.081633 | 52500 |
| 3 | 0.040 | 2 | 59 | 27 | 2026 | 0.068966 | 0.032787 | 0.056497 | 31500 |
| 4 | 0.050 | 1 | 30 | 28 | 2055 | 0.034483 | 0.032258 | 0.034014 | 15000 |
| 5 | 0.075 | 0 | 7 | 29 | 2078 | 0.000000 | 0.000000 | 0.000000 | -10500 |
| 6 | 0.100 | 0 | 3 | 29 | 2082 | 0.000000 | 0.000000 | 0.000000 | -4500 |
| 7 | 0.125 | 0 | 2 | 29 | 2083 | 0.000000 | 0.000000 | 0.000000 | -3000 |
| 8 | 0.150 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0 |
| 9 | 0.200 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0 |
| 10 | 0.250 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0 |
| 11 | 0.300 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0 |
| 12 | 0.400 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0 |
| 13 | 0.500 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_beneficio_threshold_test.png
Diagnóstico experto V6.0: APTO COMO PRIORIZADOR DE REVISIÓN: detecta reclamos y genera beneficio neto positivo bajo los supuestos definidos. Explicación estadística: La matriz de confusión transforma probabilidades en decisiones. En eventos raros, PR-AUC, Recall, FP y TP son más informativos que Accuracy. Lectura operacional: TP son reclamos priorizados correctamente; FN son reclamos que escapan; FP son revisiones innecesarias; TN son liberaciones correctas. Recomendación ML: No declarar el modelo operativo si TP=0 o si el beneficio económico es negativo. Ajustar threshold por capacidad real de revisión.
09_Interpretabilidad¶
# ======================================================================================
# 09. INTERPRETABILIDAD — COEFICIENTES, ODDS RATIOS Y EFECTOS MARGINALES
# ======================================================================================
print_step("09 — INTERPRETABILIDAD V6.0")
# Coeficientes del modelo seleccionado.
try:
fnames_final = best_model.named_steps["pre"].get_feature_names_out()
coefs_final = best_model.named_steps["clf"].coef_[0]
coef_table = pd.DataFrame({"feature_ohe": fnames_final, "coef_log_odds": coefs_final})
coef_table["odds_ratio"] = np.exp(np.clip(coef_table["coef_log_odds"], -20, 20))
coef_table["abs_coef"] = coef_table["coef_log_odds"].abs()
coef_table["variable_original"] = coef_table["feature_ohe"].apply(lambda x: feature_name_to_original(x, num_features_v6, cat_features_v6))
coef_table = coef_table.sort_values("abs_coef", ascending=False)
display(coef_table.head(40))
coef_table.to_csv(OUTPUT_DIR / "V6_0_coeficientes_odds_ratios.csv", index=False)
var_effects = coef_table.groupby("variable_original", as_index=False).agg(
coef_abs_total=("abs_coef", "sum"),
max_abs_coef=("abs_coef", "max"),
n_coef=("coef_log_odds", "size"),
or_max=("odds_ratio", "max"),
or_min=("odds_ratio", "min")
).sort_values("coef_abs_total", ascending=False)
var_effects = var_effects.merge(selected_feature_inventory_v6[["variable", "nivel", "nombre_nivel", "area", "justificacion"]], left_on="variable_original", right_on="variable", how="left")
display(var_effects)
var_effects.to_csv(OUTPUT_DIR / "V6_0_efectos_por_variable_y_nivel.csv", index=False)
plt.figure(figsize=(9,5))
top_coef = coef_table.head(20).iloc[::-1]
plt.barh(top_coef["feature_ohe"], top_coef["coef_log_odds"])
plt.title("Top coeficientes absolutos — Regresión Logística V6.0")
plt.xlabel("Coeficiente log-odds")
savefig("V6_0_top_coeficientes_log_odds.png")
level_effects = var_effects.groupby(["nivel", "nombre_nivel"], dropna=False).agg(efecto_abs_total=("coef_abs_total", "sum"), variables=("variable_original", "nunique")).reset_index()
display(level_effects)
level_effects.to_csv(OUTPUT_DIR / "V6_0_efectos_agregados_por_nivel.csv", index=False)
plt.figure(figsize=(8,4))
plt.bar(level_effects["nivel"].astype(str) + "\n" + level_effects["nombre_nivel"].astype(str), level_effects["efecto_abs_total"])
plt.title("Efecto agregado absoluto por nivel")
plt.ylabel("Suma |coeficientes|")
plt.xticks(rotation=20, ha="right")
savefig("V6_0_efecto_agregado_por_nivel.png")
except Exception as e:
print("No se pudieron extraer coeficientes:", e)
# Efectos marginales aproximados por perturbación en variables numéricas.
marginal_rows = []
baseline = X_test[selected_vars_v6].copy()
base_p = p_test.copy()
for v in num_features_v6:
try:
x_alt = baseline.copy()
sd = pd.to_numeric(X_train[v], errors="coerce").std()
if not np.isfinite(sd) or sd == 0:
continue
x_alt[v] = pd.to_numeric(x_alt[v], errors="coerce") + sd
p_alt = best_model.predict_proba(x_alt[selected_vars_v6])[:,1]
marginal_rows.append({"variable": v, "delta": "+1 desviación estándar", "efecto_marginal_promedio_pp": float((p_alt - base_p).mean()*100)})
except Exception:
pass
marginal_effects = pd.DataFrame(marginal_rows).sort_values("efecto_marginal_promedio_pp", key=lambda s: s.abs(), ascending=False) if marginal_rows else pd.DataFrame()
if not marginal_effects.empty:
display(marginal_effects)
marginal_effects.to_csv(OUTPUT_DIR / "V6_0_efectos_marginales_aproximados.csv", index=False)
print("Explicación estadística:")
print("El coeficiente positivo aumenta log-odds y OR>1 aumenta odds de reclamo. Un OR<1 reduce odds, manteniendo constantes las demás variables.")
print("Lectura operacional:")
print("Los efectos por nivel permiten explicar si el riesgo proviene de origen, cosecha, packing, logística o contexto comercial.")
print("Recomendación ML:")
print("Usar Odds Ratios para defensa estadística y efectos marginales para explicar impacto promedio en probabilidad operacional.")
==================================================================================================== 09 — INTERPRETABILIDAD V6.0 ====================================================================================================
| feature_ohe | coef_log_odds | odds_ratio | abs_coef | variable_original | |
|---|---|---|---|---|---|
| 8 | cat__macro_mercado_EU | 2.033079 | 7.637568 | 2.033079 | macro_mercado |
| 26 | cat__linea_packing_L3 | -0.303052 | 0.738561 | 0.303052 | linea_packing |
| 1 | num__materia_seca_pct | 0.278080 | 1.320591 | 0.278080 | materia_seca_pct |
| 5 | num__quiebre_cadena_frio_h | 0.273244 | 1.314220 | 0.273244 | quiebre_cadena_frio_h |
| 48 | cat__atmosfera_controlada_si | -0.228408 | 0.795799 | 0.228408 | atmosfera_controlada |
| 19 | cat__temporada_2021_2022 | -0.223822 | 0.799457 | 0.223822 | temporada |
| 30 | cat__puerto_salida_Valparaiso | -0.165321 | 0.847622 | 0.165321 | puerto_salida |
| 20 | cat__temporada_2022_2023 | -0.161784 | 0.850625 | 0.161784 | temporada |
| 32 | cat__naviera_Hapag-Lloyd | 0.142091 | 1.152682 | 0.142091 | naviera |
| 18 | cat__temporada_2020_2021 | 0.134343 | 1.143785 | 0.134343 | temporada |
| 0 | num__ph_suelo | 0.126232 | 1.134546 | 0.126232 | ph_suelo |
| 27 | cat__linea_packing_L4 | -0.126230 | 0.881412 | 0.126230 | linea_packing |
| 3 | num__firmeza_pulpa_lb | -0.108924 | 0.896799 | 0.108924 | firmeza_pulpa_lb |
| 23 | cat__tipo_contenedor_reefer_40 | -0.082061 | 0.921216 | 0.082061 | tipo_contenedor |
| 36 | cat__naviera_ONE | -0.075134 | 0.927619 | 0.075134 | naviera |
| 4 | num__densidad_arboles_ha | -0.061758 | 0.940110 | 0.061758 | densidad_arboles_ha |
| 25 | cat__linea_packing_L2 | 0.059422 | 1.061223 | 0.059422 | linea_packing |
| 34 | cat__naviera_Maersk | -0.046533 | 0.954533 | 0.046533 | naviera |
| 2 | num__edad_arboles_anos | -0.035067 | 0.965540 | 0.035067 | edad_arboles_anos |
| 44 | cat__packhouse_id_PK_Secano | -0.012473 | 0.987604 | 0.012473 | packhouse_id |
| 15 | cat__portainjerto_Zutano | 0.000000 | 1.000000 | 0.000000 | portainjerto |
| 16 | cat__temporada_2018_2019 | 0.000000 | 1.000000 | 0.000000 | temporada |
| 11 | cat__portainjerto_Duke 7 | 0.000000 | 1.000000 | 0.000000 | portainjerto |
| 10 | cat__macro_mercado_NA | 0.000000 | 1.000000 | 0.000000 | macro_mercado |
| 9 | cat__macro_mercado_LATAM | 0.000000 | 1.000000 | 0.000000 | macro_mercado |
| 7 | cat__macro_mercado_DOM | 0.000000 | 1.000000 | 0.000000 | macro_mercado |
| 6 | num__temperatura_setpoint_frio_c | 0.000000 | 1.000000 | 0.000000 | temperatura_setpoint_frio_c |
| 14 | cat__portainjerto_Toro Canyon | 0.000000 | 1.000000 | 0.000000 | portainjerto |
| 13 | cat__portainjerto_Mexicola | 0.000000 | 1.000000 | 0.000000 | portainjerto |
| 12 | cat__portainjerto_Dusa | 0.000000 | 1.000000 | 0.000000 | portainjerto |
| 22 | cat__tipo_contenedor_reefer_20 | 0.000000 | 1.000000 | 0.000000 | tipo_contenedor |
| 21 | cat__tipo_contenedor_camion_frio | 0.000000 | 1.000000 | 0.000000 | tipo_contenedor |
| 17 | cat__temporada_2019_2020 | 0.000000 | 1.000000 | 0.000000 | temporada |
| 31 | cat__naviera_CMA CGM | 0.000000 | 1.000000 | 0.000000 | naviera |
| 29 | cat__puerto_salida_San Antonio | 0.000000 | 1.000000 | 0.000000 | puerto_salida |
| 28 | cat__puerto_salida_N/A | 0.000000 | 1.000000 | 0.000000 | puerto_salida |
| 24 | cat__linea_packing_L1 | 0.000000 | 1.000000 | 0.000000 | linea_packing |
| 37 | cat__subzona_agroclimatica_Lago Rapel | 0.000000 | 1.000000 | 0.000000 | subzona_agroclimatica |
| 38 | cat__subzona_agroclimatica_Secano Costero | 0.000000 | 1.000000 | 0.000000 | subzona_agroclimatica |
| 33 | cat__naviera_MSC | 0.000000 | 1.000000 | 0.000000 | naviera |
| variable_original | coef_abs_total | max_abs_coef | n_coef | or_max | or_min | variable | nivel | nombre_nivel | area | justificacion | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | macro_mercado | 2.033079 | 2.033079 | 4 | 7.637568 | 1.000000 | macro_mercado | Nivel 3 | Variables de Entorno Operacional | Mercado | Agrupa destinos con comportamiento logístico y... |
| 1 | temporada | 0.519949 | 0.223822 | 5 | 1.143785 | 0.799457 | temporada | Nivel 3 | Variables de Entorno Operacional | Temporal | Captura cambios productivos, comerciales y cli... |
| 2 | linea_packing | 0.488704 | 0.303052 | 4 | 1.061223 | 0.738561 | linea_packing | Nivel 3 | Variables de Entorno Operacional | Packing | Diferencias de línea pueden afectar manipulaci... |
| 3 | materia_seca_pct | 0.278080 | 0.278080 | 1 | 1.320591 | 1.320591 | materia_seca_pct | Nivel 1 | Variables Causales Directas | Madurez | Indicador maestro de madurez funcional de la p... |
| 4 | quiebre_cadena_frio_h | 0.273244 | 0.273244 | 1 | 1.314220 | 1.314220 | quiebre_cadena_frio_h | Nivel 1 | Variables Causales Directas | Frío | Mide exposición térmica fuera de rango; puede ... |
| 5 | naviera | 0.263758 | 0.142091 | 6 | 1.152682 | 0.927619 | naviera | Nivel 3 | Variables de Entorno Operacional | Logística | Naviera captura diferencias de servicio, ruta ... |
| 6 | atmosfera_controlada | 0.228408 | 0.228408 | 2 | 1.000000 | 0.795799 | atmosfera_controlada | Nivel 2 | Variables Explicativas Primarias | Contenedor | Condición de atmósfera afecta conservación y m... |
| 7 | puerto_salida | 0.165321 | 0.165321 | 3 | 1.000000 | 0.847622 | puerto_salida | Nivel 3 | Variables de Entorno Operacional | Logística | Puerto puede asociarse a tiempos, infraestruct... |
| 8 | ph_suelo | 0.126232 | 0.126232 | 1 | 1.134546 | 1.134546 | ph_suelo | Nivel 2 | Variables Explicativas Primarias | Origen | pH afecta disponibilidad nutricional y condici... |
| 9 | firmeza_pulpa_lb | 0.108924 | 0.108924 | 1 | 0.896799 | 0.896799 | firmeza_pulpa_lb | Nivel 2 | Variables Explicativas Primarias | Madurez | Resistencia estructural del fruto frente a dañ... |
| 10 | tipo_contenedor | 0.082061 | 0.082061 | 3 | 1.000000 | 0.921216 | tipo_contenedor | Nivel 3 | Variables de Entorno Operacional | Contenedor | Tipo de contenedor puede afectar conservación ... |
| 11 | densidad_arboles_ha | 0.061758 | 0.061758 | 1 | 0.940110 | 0.940110 | densidad_arboles_ha | Nivel 2 | Variables Explicativas Primarias | Origen | Densidad afecta competencia, luz, vigor y desa... |
| 12 | edad_arboles_anos | 0.035067 | 0.035067 | 1 | 0.965540 | 0.965540 | edad_arboles_anos | Nivel 2 | Variables Explicativas Primarias | Origen | Edad del huerto puede influir en vigor, produc... |
| 13 | packhouse_id | 0.012473 | 0.012473 | 4 | 1.000000 | 0.987604 | packhouse_id | Nivel 3 | Variables de Entorno Operacional | Packing | Captura diferencias operacionales entre planta... |
| 14 | canal_destino | 0.000000 | 0.000000 | 2 | 1.000000 | 1.000000 | canal_destino | Nivel 3 | Variables de Entorno Operacional | Mercado | Canal puede reflejar exigencia de cliente y ni... |
| 15 | portainjerto | 0.000000 | 0.000000 | 5 | 1.000000 | 1.000000 | portainjerto | Nivel 2 | Variables Explicativas Primarias | Origen | Portainjerto puede influir en vigor, absorción... |
| 16 | temperatura_setpoint_frio_c | 0.000000 | 0.000000 | 1 | 1.000000 | 1.000000 | temperatura_setpoint_frio_c | Nivel 1 | Variables Causales Directas | Frío | Temperatura objetivo debe ser coherente con ma... |
| 17 | subzona_agroclimatica | 0.000000 | 0.000000 | 4 | 1.000000 | 1.000000 | subzona_agroclimatica | Nivel 1 | Variables Causales Directas | Origen | Define condiciones agroclimáticas y ventana de... |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_top_coeficientes_log_odds.png
| nivel | nombre_nivel | efecto_abs_total | variables | |
|---|---|---|---|---|
| 0 | Nivel 1 | Variables Causales Directas | 0.551323 | 4 |
| 1 | Nivel 2 | Variables Explicativas Primarias | 0.560390 | 6 |
| 2 | Nivel 3 | Variables de Entorno Operacional | 3.565345 | 8 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_efecto_agregado_por_nivel.png
| variable | delta | efecto_marginal_promedio_pp | |
|---|---|---|---|
| 3 | firmeza_pulpa_lb | +1 desviación estándar | -0.303574 |
| 4 | densidad_arboles_ha | +1 desviación estándar | -0.261935 |
| 2 | edad_arboles_anos | +1 desviación estándar | -0.237517 |
| 6 | temperatura_setpoint_frio_c | +1 desviación estándar | -0.204467 |
| 1 | materia_seca_pct | +1 desviación estándar | 0.101013 |
| 5 | quiebre_cadena_frio_h | +1 desviación estándar | 0.094977 |
| 0 | ph_suelo | +1 desviación estándar | -0.075825 |
Explicación estadística: El coeficiente positivo aumenta log-odds y OR>1 aumenta odds de reclamo. Un OR<1 reduce odds, manteniendo constantes las demás variables. Lectura operacional: Los efectos por nivel permiten explicar si el riesgo proviene de origen, cosecha, packing, logística o contexto comercial. Recomendación ML: Usar Odds Ratios para defensa estadística y efectos marginales para explicar impacto promedio en probabilidad operacional.
10_Causalidad_y_Contrafactuales¶
# ======================================================================================
# 10. CAUSALIDAD OPERACIONAL Y SIMULACIONES CONTRAFACTUALES
# ======================================================================================
print_step("10 — CAUSALIDAD Y CONTRAFACTUALES V6.0")
# Nota metodológica: esto no prueba causalidad fuerte. Es sensibilidad contrafactual sobre el modelo.
cf_rows = []
base_sample = X_test[selected_vars_v6].copy()
base_risk = best_model.predict_proba(base_sample)[:,1]
for v in num_features_v6:
try:
q25, q50, q75 = pd.to_numeric(X_train[v], errors="coerce").quantile([0.25,0.50,0.75]).values
for label, value in [("p25", q25), ("p50", q50), ("p75", q75)]:
alt = base_sample.copy()
alt[v] = value
alt_risk = best_model.predict_proba(alt[selected_vars_v6])[:,1]
cf_rows.append({"variable": v, "escenario": label, "valor": value, "riesgo_promedio": float(alt_risk.mean()), "delta_pp_vs_base": float((alt_risk.mean() - base_risk.mean())*100)})
except Exception:
pass
cf_table = pd.DataFrame(cf_rows)
if not cf_table.empty:
display(cf_table.sort_values("delta_pp_vs_base", key=lambda s: s.abs(), ascending=False).head(30))
cf_table.to_csv(OUTPUT_DIR / "V6_0_simulaciones_contrafactuales_numericas.csv", index=False)
top_cf = cf_table.sort_values("delta_pp_vs_base", key=lambda s: s.abs(), ascending=False).head(15).iloc[::-1]
plt.figure(figsize=(9,5))
plt.barh(top_cf["variable"] + "_" + top_cf["escenario"], top_cf["delta_pp_vs_base"])
plt.title("Contrafactuales: cambio en riesgo promedio vs base")
plt.xlabel("Delta puntos porcentuales")
savefig("V6_0_contrafactuales_top.png")
else:
print("No hay variables numéricas suficientes para contrafactuales automáticos.")
print("Explicación estadística:")
print("Las simulaciones modifican una variable y mantienen las demás constantes. Sirven como sensibilidad del modelo, no como prueba causal definitiva.")
print("Lectura operacional:")
print("Ayudan a responder qué pasaría con el riesgo si una señal de madurez, logística o clima cambia de nivel.")
print("Recomendación ML:")
print("Usar estos resultados como hipótesis de gestión y validarlos con conocimiento agronómico/operacional antes de definir políticas.")
==================================================================================================== 10 — CAUSALIDAD Y CONTRAFACTUALES V6.0 ====================================================================================================
| variable | escenario | valor | riesgo_promedio | delta_pp_vs_base | |
|---|---|---|---|---|---|
| 15 | quiebre_cadena_frio_h | p25 | 1.30 | 0.007342 | -0.242565 |
| 3 | materia_seca_pct | p25 | 22.84 | 0.007497 | -0.227119 |
| 5 | materia_seca_pct | p75 | 27.78 | 0.011717 | 0.194927 |
| 16 | quiebre_cadena_frio_h | p50 | 2.30 | 0.008154 | -0.161384 |
| 0 | ph_suelo | p25 | 6.09 | 0.008671 | -0.109666 |
| 2 | ph_suelo | p75 | 6.99 | 0.010806 | 0.103786 |
| 11 | firmeza_pulpa_lb | p75 | 43.30 | 0.008768 | -0.099989 |
| 9 | firmeza_pulpa_lb | p25 | 34.80 | 0.010359 | 0.059135 |
| 12 | densidad_arboles_ha | p25 | 312.00 | 0.010335 | 0.056762 |
| 4 | materia_seca_pct | p50 | 25.27 | 0.009342 | -0.042577 |
| 6 | edad_arboles_anos | p25 | 8.00 | 0.010063 | 0.029502 |
| 8 | edad_arboles_anos | p75 | 18.00 | 0.009489 | -0.027885 |
| 14 | densidad_arboles_ha | p75 | 400.00 | 0.009511 | -0.025707 |
| 13 | densidad_arboles_ha | p50 | 400.00 | 0.009511 | -0.025707 |
| 10 | firmeza_pulpa_lb | p50 | 39.10 | 0.009522 | -0.024621 |
| 1 | ph_suelo | p50 | 6.55 | 0.009704 | -0.006330 |
| 7 | edad_arboles_anos | p50 | 14.00 | 0.009714 | -0.005326 |
| 17 | quiebre_cadena_frio_h | p75 | 4.00 | 0.009742 | -0.002581 |
| 18 | temperatura_setpoint_frio_c | p25 | 4.90 | 0.009768 | 0.000000 |
| 19 | temperatura_setpoint_frio_c | p50 | 5.10 | 0.009768 | 0.000000 |
| 20 | temperatura_setpoint_frio_c | p75 | 5.40 | 0.009768 | 0.000000 |
[GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_contrafactuales_top.png
Explicación estadística: Las simulaciones modifican una variable y mantienen las demás constantes. Sirven como sensibilidad del modelo, no como prueba causal definitiva. Lectura operacional: Ayudan a responder qué pasaría con el riesgo si una señal de madurez, logística o clima cambia de nivel. Recomendación ML: Usar estos resultados como hipótesis de gestión y validarlos con conocimiento agronómico/operacional antes de definir políticas.
11_Resultados_Negocio¶
# ======================================================================================
# 11. RESULTADOS DE NEGOCIO — THRESHOLD Y TOP-K
# ======================================================================================
print_step("11 — RESULTADOS DE NEGOCIO V6.0")
# Política threshold oficial desde validación.
threshold_policy = metrics_at_threshold(y_test, p_test, best_threshold)
threshold_policy.update({"politica": "threshold_validacion", "parametro": best_threshold})
# Política Top-K por capacidad de revisión.
topk_rows = []
for k in TOPK_GRID:
n_review = max(1, int(np.ceil(len(p_test) * k)))
cutoff = np.sort(p_test)[-n_review]
pred_topk = (p_test >= cutoff).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, pred_topk, labels=[0,1]).ravel()
topk_rows.append({
"politica": "topk_capacidad", "k_pct": k, "parametro": k,
"TN": int(tn), "FP": int(fp), "FN": int(fn), "TP": int(tp),
"recall_sensitivity": recall_score(y_test, pred_topk, zero_division=0),
"precision_ppv": precision_score(y_test, pred_topk, zero_division=0),
"beneficio_neto_usd": economic_benefit_from_cm(tn, fp, fn, tp)
})
topk_table = pd.DataFrame(topk_rows)
business_table = pd.concat([pd.DataFrame([threshold_policy]), topk_table], ignore_index=True, sort=False)
business_table["reclamos_detectados_TP"] = business_table["TP"]
business_table["reclamos_escapados_FN"] = business_table["FN"]
business_table["falsas_alarmas_FP"] = business_table["FP"]
business_table = business_table.sort_values("beneficio_neto_usd", ascending=False)
display(business_table[["politica","parametro","TP","FP","FN","TN","recall_sensitivity","precision_ppv","beneficio_neto_usd"]])
business_table.to_csv(OUTPUT_DIR / "V6_0_resultados_negocio_threshold_topk.csv", index=False)
selected_business_policy = business_table.iloc[0].to_dict()
print("Mejor política de negocio en test:")
for k in ["politica","parametro","TP","FP","FN","TN","recall_sensitivity","precision_ppv","beneficio_neto_usd"]:
print(k, ":", selected_business_policy.get(k))
plt.figure(figsize=(8,4))
plot_biz = business_table.copy()
plot_biz["label"] = plot_biz["politica"].astype(str) + "_" + plot_biz["parametro"].astype(str)
plt.barh(plot_biz["label"].iloc[::-1], plot_biz["beneficio_neto_usd"].iloc[::-1])
plt.title("Beneficio neto por política — Test")
plt.xlabel("USD")
savefig("V6_0_beneficio_politicas_test.png")
print("Traducción operacional:")
print("TP: reclamos reales priorizados antes del despacho.")
print("FP: lotes sanos enviados a revisión; costo operacional.")
print("FN: reclamos reales que escapan; principal pérdida económica.")
print("TN: lotes sanos correctamente liberados.")
print("Recomendación ML:")
print("Si la política Top-K supera al threshold, conviene operar por capacidad de revisión diaria/semanal en vez de una probabilidad fija.")
==================================================================================================== 11 — RESULTADOS DE NEGOCIO V6.0 ====================================================================================================
| politica | parametro | TP | FP | FN | TN | recall_sensitivity | precision_ppv | beneficio_neto_usd | |
|---|---|---|---|---|---|---|---|---|---|
| 5 | topk_capacidad | 0.075 | 6 | 153 | 23 | 1932 | 0.206897 | 0.037736 | 130500 |
| 6 | topk_capacidad | 0.100 | 7 | 205 | 22 | 1880 | 0.241379 | 0.033019 | 112500 |
| 7 | topk_capacidad | 0.150 | 9 | 309 | 20 | 1776 | 0.310345 | 0.028302 | 76500 |
| 2 | topk_capacidad | 0.020 | 2 | 41 | 27 | 2044 | 0.068966 | 0.046512 | 58500 |
| 0 | threshold_validacion | 0.030 | 4 | 125 | 25 | 1960 | 0.137931 | 0.031008 | 52500 |
| 1 | topk_capacidad | 0.010 | 1 | 21 | 28 | 2064 | 0.034483 | 0.045455 | 28500 |
| 3 | topk_capacidad | 0.030 | 2 | 62 | 27 | 2023 | 0.068966 | 0.031250 | 27000 |
| 4 | topk_capacidad | 0.050 | 3 | 103 | 26 | 1982 | 0.103448 | 0.028302 | 25500 |
| 8 | topk_capacidad | 0.200 | 10 | 413 | 19 | 1672 | 0.344828 | 0.023641 | -19500 |
Mejor política de negocio en test: politica : topk_capacidad parametro : 0.075 TP : 6 FP : 153 FN : 23 TN : 1932 recall_sensitivity : 0.20689655172413793 precision_ppv : 0.03773584905660377 beneficio_neto_usd : 130500 [GRAFICO] Guardado: /content/capstone_outputs_V6_0_regresion_logistica_menos_variables/graficos/V6_0_beneficio_politicas_test.png
Traducción operacional: TP: reclamos reales priorizados antes del despacho. FP: lotes sanos enviados a revisión; costo operacional. FN: reclamos reales que escapan; principal pérdida económica. TN: lotes sanos correctamente liberados. Recomendación ML: Si la política Top-K supera al threshold, conviene operar por capacidad de revisión diaria/semanal en vez de una probabilidad fija.
12_QA_Defensa¶
# ======================================================================================
# 12. QA DEFENSA — V6.0
# ======================================================================================
print_step("12 — QA DEFENSA V6.0")
qa_v60 = f"""
# QA Defensa — Regresión Logística V6.0 con Menos Variables
## 1. ¿Qué mejora V6.0 respecto de V5.1?
V6.0 deja de entrenar una sola logística y pasa a un laboratorio de modelos candidatos. Además reduce variables con LASSO y VIF antes de seleccionar el modelo final.
## 2. ¿Por qué menos variables?
Porque mejora estabilidad, reduce multicolinealidad, facilita captura operacional y permite explicar mejor los coeficientes y Odds Ratios.
## 3. ¿Se cambian las variables oficiales de V4.0?
No. V4.0 sigue siendo el universo candidato y mantiene los 5 niveles. V6.0 selecciona un subconjunto parsimonioso dentro de ese universo.
## 4. ¿Por qué LASSO?
Porque penaliza coeficientes y puede llevar algunos a cero, funcionando como selección automática de variables dentro de una regresión logística interpretable.
## 5. ¿Por qué VIF?
Porque dos variables muy colineales pueden volver inestables los coeficientes. VIF ayuda a detectar redundancia numérica.
## 6. ¿Por qué no se usa Accuracy como métrica principal?
Porque el reclamo comercial es raro. Un modelo puede tener accuracy alta y no detectar reclamos. Se prioriza PR-AUC, Recall, F2, matriz de confusión y beneficio económico.
## 7. ¿Por qué el threshold se selecciona en validación?
Porque el test debe quedar reservado para evaluación final. Elegir threshold en test contaminaría la medición.
## 8. ¿Qué significa que el beneficio sea negativo?
Que, bajo los costos definidos, los falsos positivos y/o reclamos escapados no justifican operar la política automáticamente.
## 9. ¿Qué hacer si TP sigue bajo?
Usar el modelo como priorizador exploratorio, mejorar variables pre-despacho, incorporar históricos robustos, revisar target, y comparar con modelos no lineales manteniendo esta logística como benchmark interpretable.
## 10. Modelo final V6.0
- Modelo seleccionado: {best_model_id}
- Threshold seleccionado: {best_threshold:.4f}
- Variables finales: {len(selected_vars_v6)}
- Calibración usada: {calibration_used}
- SHAP: excluido por diseño metodológico
"""
print(qa_v60)
(OUTPUT_DIR / "V6_0_QA_Defensa.md").write_text(qa_v60, encoding="utf-8")
==================================================================================================== 12 — QA DEFENSA V6.0 ==================================================================================================== # QA Defensa — Regresión Logística V6.0 con Menos Variables ## 1. ¿Qué mejora V6.0 respecto de V5.1? V6.0 deja de entrenar una sola logística y pasa a un laboratorio de modelos candidatos. Además reduce variables con LASSO y VIF antes de seleccionar el modelo final. ## 2. ¿Por qué menos variables? Porque mejora estabilidad, reduce multicolinealidad, facilita captura operacional y permite explicar mejor los coeficientes y Odds Ratios. ## 3. ¿Se cambian las variables oficiales de V4.0? No. V4.0 sigue siendo el universo candidato y mantiene los 5 niveles. V6.0 selecciona un subconjunto parsimonioso dentro de ese universo. ## 4. ¿Por qué LASSO? Porque penaliza coeficientes y puede llevar algunos a cero, funcionando como selección automática de variables dentro de una regresión logística interpretable. ## 5. ¿Por qué VIF? Porque dos variables muy colineales pueden volver inestables los coeficientes. VIF ayuda a detectar redundancia numérica. ## 6. ¿Por qué no se usa Accuracy como métrica principal? Porque el reclamo comercial es raro. Un modelo puede tener accuracy alta y no detectar reclamos. Se prioriza PR-AUC, Recall, F2, matriz de confusión y beneficio económico. ## 7. ¿Por qué el threshold se selecciona en validación? Porque el test debe quedar reservado para evaluación final. Elegir threshold en test contaminaría la medición. ## 8. ¿Qué significa que el beneficio sea negativo? Que, bajo los costos definidos, los falsos positivos y/o reclamos escapados no justifican operar la política automáticamente. ## 9. ¿Qué hacer si TP sigue bajo? Usar el modelo como priorizador exploratorio, mejorar variables pre-despacho, incorporar históricos robustos, revisar target, y comparar con modelos no lineales manteniendo esta logística como benchmark interpretable. ## 10. Modelo final V6.0 - Modelo seleccionado: V6_067_l1_C0.5_none_l1rNone - Threshold seleccionado: 0.0300 - Variables finales: 18 - Calibración usada: sigmoid_prefit_validacion - SHAP: excluido por diseño metodológico
2012
13_Reporte_Final¶
# ======================================================================================
# 13. REPORTE FINAL — V6.0
# ======================================================================================
print_step("13 — REPORTE FINAL V6.0")
best_row_test = metrics_summary[metrics_summary["split"].eq("test")].iloc[0].to_dict()
biz_row = selected_business_policy
report = f"""
# Reporte Final — Regresión Logística V6.0 con Menos Variables
## Objetivo
Construir un modelo de Regresión Logística interpretable para anticipar `reclamo_comercial` a nivel lote/contenedor, reduciendo variables respecto del universo V4.0 y seleccionando modelo/threshold por validación temporal y criterio económico.
## Modelo
- Algoritmo: Regresión Logística.
- Laboratorio: L1, L2 y ElasticNet con distintos `C` y `class_weight`.
- Modelo seleccionado: {best_model_id}.
- Threshold seleccionado en validación: {best_threshold:.4f}.
- Calibración: {calibration_used}.
- SHAP: excluido por diseño metodológico.
## Variables
- Universo V4.0 disponible: {len(valid_vars)} variables.
- Variables finales V6.0: {len(selected_vars_v6)} variables.
- Numéricas finales: {len(num_features_v6)}.
- Categóricas finales: {len(cat_features_v6)}.
- Niveles heredados desde V4.0: 5.
## Split
- Método: {split_method}.
- Train: {len(y_train):,} registros.
- Validación: {len(y_val):,} registros.
- Test: {len(y_test):,} registros.
## Métricas Test con threshold {best_threshold:.4f}
- ROC-AUC: {best_row_test.get('roc_auc', np.nan):.4f}
- PR-AUC: {best_row_test.get('pr_auc', np.nan):.4f}
- Recall/Sensibilidad: {best_row_test.get('recall_sensitivity', np.nan):.4f}
- Precision/PPV: {best_row_test.get('precision_ppv', np.nan):.4f}
- Specificity: {best_row_test.get('specificity', np.nan):.4f}
- Balanced Accuracy: {best_row_test.get('balanced_accuracy', np.nan):.4f}
- MCC: {best_row_test.get('mcc', np.nan):.4f}
- Beneficio neto USD threshold: {best_row_test.get('beneficio_neto_usd', np.nan):,.0f}
## Matriz de Confusión Test
- TN: {best_row_test.get('TN', 0):,.0f}
- FP: {best_row_test.get('FP', 0):,.0f}
- FN: {best_row_test.get('FN', 0):,.0f}
- TP: {best_row_test.get('TP', 0):,.0f}
## Mejor política de negocio en test
- Política: {biz_row.get('politica')}
- Parámetro: {biz_row.get('parametro')}
- Reclamos detectados TP: {biz_row.get('TP', 0):,.0f}
- Reclamos escapados FN: {biz_row.get('FN', 0):,.0f}
- Falsas alarmas FP: {biz_row.get('FP', 0):,.0f}
- Beneficio neto estimado USD: {biz_row.get('beneficio_neto_usd', 0):,.0f}
## Diagnóstico experto
{diagnostico_v6}
## Lectura operacional
V6.0 es una regresión logística más simple y defendible que V5.1 porque reduce variables, compara múltiples configuraciones y separa la decisión predictiva de la política de revisión. Si la mejor política es Top-K, el modelo debe usarse para priorizar revisión según capacidad operativa y no como bloqueo automático.
## Recomendación experta ML
Usar V6.0 como benchmark logístico final interpretable. Si el beneficio neto sigue siendo negativo o el recall insuficiente, el siguiente paso no es forzar el threshold, sino mejorar señal predictiva: históricos por cliente/mercado, variables de packing más finas, estabilidad temporal, y modelo challenger no lineal manteniendo V6.0 como explicación estadística base.
"""
print(report)
(OUTPUT_DIR / "V6_0_Reporte_Final.md").write_text(report, encoding="utf-8")
artifacts = []
for p in sorted(OUTPUT_DIR.glob("*")):
if p.is_file():
artifacts.append({"archivo": p.name, "ruta": str(p)})
for p in sorted(GRAF_DIR.glob("*")):
if p.is_file():
artifacts.append({"archivo": p.name, "ruta": str(p)})
artifacts_df = pd.DataFrame(artifacts)
display(artifacts_df)
artifacts_df.to_csv(OUTPUT_DIR / "V6_0_indice_outputs.csv", index=False)
print("[OUTPUT] Reporte final e índice de outputs guardados.")
==================================================================================================== 13 — REPORTE FINAL V6.0 ==================================================================================================== # Reporte Final — Regresión Logística V6.0 con Menos Variables ## Objetivo Construir un modelo de Regresión Logística interpretable para anticipar `reclamo_comercial` a nivel lote/contenedor, reduciendo variables respecto del universo V4.0 y seleccionando modelo/threshold por validación temporal y criterio económico. ## Modelo - Algoritmo: Regresión Logística. - Laboratorio: L1, L2 y ElasticNet con distintos `C` y `class_weight`. - Modelo seleccionado: V6_067_l1_C0.5_none_l1rNone. - Threshold seleccionado en validación: 0.0300. - Calibración: sigmoid_prefit_validacion. - SHAP: excluido por diseño metodológico. ## Variables - Universo V4.0 disponible: 33 variables. - Variables finales V6.0: 18 variables. - Numéricas finales: 7. - Categóricas finales: 11. - Niveles heredados desde V4.0: 5. ## Split - Método: temporal_por_temporada. - Train: 10,529 registros. - Validación: 2,093 registros. - Test: 2,114 registros. ## Métricas Test con threshold 0.0300 - ROC-AUC: 0.6473 - PR-AUC: 0.0264 - Recall/Sensibilidad: 0.1379 - Precision/PPV: 0.0310 - Specificity: 0.9400 - Balanced Accuracy: 0.5390 - MCC: 0.0379 - Beneficio neto USD threshold: 52,500 ## Matriz de Confusión Test - TN: 1,960 - FP: 125 - FN: 25 - TP: 4 ## Mejor política de negocio en test - Política: topk_capacidad - Parámetro: 0.075 - Reclamos detectados TP: 6 - Reclamos escapados FN: 23 - Falsas alarmas FP: 153 - Beneficio neto estimado USD: 130,500 ## Diagnóstico experto APTO COMO PRIORIZADOR DE REVISIÓN: detecta reclamos y genera beneficio neto positivo bajo los supuestos definidos. ## Lectura operacional V6.0 es una regresión logística más simple y defendible que V5.1 porque reduce variables, compara múltiples configuraciones y separa la decisión predictiva de la política de revisión. Si la mejor política es Top-K, el modelo debe usarse para priorizar revisión según capacidad operativa y no como bloqueo automático. ## Recomendación experta ML Usar V6.0 como benchmark logístico final interpretable. Si el beneficio neto sigue siendo negativo o el recall insuficiente, el siguiente paso no es forzar el threshold, sino mejorar señal predictiva: históricos por cliente/mercado, variables de packing más finas, estabilidad temporal, y modelo challenger no lineal manteniendo V6.0 como explicación estadística base.
| archivo | ruta | |
|---|---|---|
| 0 | V6_0_QA_Defensa.md | /content/capstone_outputs_V6_0_regresion_logis... |
| 1 | V6_0_Reporte_Final.md | /content/capstone_outputs_V6_0_regresion_logis... |
| 2 | V6_0_calibracion_resumen.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 3 | V6_0_coeficientes_odds_ratios.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 4 | V6_0_efectos_agregados_por_nivel.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 5 | V6_0_efectos_marginales_aproximados.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 6 | V6_0_efectos_por_variable_y_nivel.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 7 | V6_0_importancia_lasso_por_variable.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 8 | V6_0_lasso_grid_seleccion_variables.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 9 | V6_0_leaderboard_modelos_logisticos.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 10 | V6_0_matriz_confusion_test.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 11 | V6_0_matriz_confusion_validacion.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 12 | V6_0_metricas_validacion_test.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 13 | V6_0_resultados_negocio_threshold_topk.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 14 | V6_0_simulaciones_contrafactuales_numericas.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 15 | V6_0_thresholds_test.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 16 | V6_0_thresholds_validacion_todos_modelos.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 17 | V6_0_variables_finales_menos_variables.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 18 | V6_0_vif_variables_numericas.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 19 | auditoria_carga_tablas.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 20 | auditoria_estrategica_variables_5_niveles.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 21 | base_auditada_lote_exportacion_v40.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 22 | base_enriquecida_auditoria_v40.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 23 | inventario_features_modelables_V6_0.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 24 | matriz_nivel_area_variables.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 25 | matriz_variables_niveles_oficial_V6_0.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 26 | nulos_por_variable_v40.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 27 | resumen_disponibilidad_variables_por_nivel.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 28 | resumen_tablas_v40.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 29 | split_temporal_resumen_V6_0.csv | /content/capstone_outputs_V6_0_regresion_logis... |
| 30 | 01_volumetria_tablas.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 31 | 02_distribucion_target_oficial.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 32 | V6_04_variables_modelables_por_nivel.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 33 | V6_05_tasa_reclamo_por_split.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 34 | V6_0_beneficio_politicas_test.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 35 | V6_0_beneficio_threshold_test.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 36 | V6_0_contrafactuales_top.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 37 | V6_0_curva_calibracion_validacion.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 38 | V6_0_efecto_agregado_por_nivel.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 39 | V6_0_matriz_confusion_test.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 40 | V6_0_matriz_confusion_validacion.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 41 | V6_0_pr_test.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 42 | V6_0_pr_validacion.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 43 | V6_0_roc_test.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 44 | V6_0_roc_validacion.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 45 | V6_0_top_coeficientes_log_odds.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 46 | V6_0_top_modelos_beneficio_validacion.png | /content/capstone_outputs_V6_0_regresion_logis... |
| 47 | V6_0_variables_finales_por_nivel.png | /content/capstone_outputs_V6_0_regresion_logis... |
[OUTPUT] Reporte final e índice de outputs guardados.
14_V8_Config_Final¶
Configuración adicional para la V8.0 Final. Este bloque se ejecuta después de la V6.0 y reutiliza X_train, X_val, X_test, y_train, y_val, y_test, selected_vars_v6, num_features_v6, cat_features_v6, best_model y las funciones utilitarias ya creadas.
# ======================================================================================
# 14. CONFIGURACIÓN ADICIONAL V8.0 FINAL — DESDE V6.0
# ======================================================================================
print_step("14 — CONFIGURACIÓN V8.0 FINAL DESDE V6.0")
# Carpeta V8.0 final, separada de outputs V6.0.
OUTPUT_DIR_V8 = Path("/content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno")
GRAF_DIR_V8 = OUTPUT_DIR_V8 / "graficos"
TABLE_DIR_V8 = OUTPUT_DIR_V8 / "tablas"
MODEL_DIR_V8 = OUTPUT_DIR_V8 / "modelos"
for d in [OUTPUT_DIR_V8, GRAF_DIR_V8, TABLE_DIR_V8, MODEL_DIR_V8]:
d.mkdir(parents=True, exist_ok=True)
# Guardar gráficos V8.
def savefig_v8(name):
path = GRAF_DIR_V8 / name
plt.tight_layout()
plt.savefig(path, dpi=170, bbox_inches="tight")
print("[GRAFICO V8] Guardado:", path)
plt.show()
# Instalar opcionales si no existen. En Colab puede tardar unos minutos.
import importlib, subprocess, sys
def ensure_package(import_name, pip_name=None):
pip_name = pip_name or import_name
try:
return importlib.import_module(import_name)
except Exception:
print(f"[INFO] Instalando {pip_name}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", pip_name])
return importlib.import_module(import_name)
try:
xgb = ensure_package("xgboost", "xgboost")
HAS_XGB = True
except Exception as e:
HAS_XGB = False
print("[WARNING] XGBoost no disponible:", e)
try:
lgb = ensure_package("lightgbm", "lightgbm")
HAS_LGBM = True
except Exception as e:
HAS_LGBM = False
print("[WARNING] LightGBM no disponible:", e)
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier, GradientBoostingClassifier, HistGradientBoostingClassifier, AdaBoostClassifier, BaggingClassifier
from sklearn.model_selection import ParameterGrid
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.inspection import permutation_importance
from sklearn.metrics import (
roc_auc_score, average_precision_score, precision_recall_curve, roc_curve,
confusion_matrix, precision_score, recall_score, f1_score, fbeta_score,
matthews_corrcoef, balanced_accuracy_score, brier_score_loss, log_loss
)
from sklearn.base import clone
import joblib
# Costos de negocio heredados o definidos.
COSTO_FP_USD = globals().get("COSTO_FP_USD", 1500)
BENEFICIO_TP_USD = globals().get("BENEFICIO_TP_USD", 60000)
COSTO_FN_USD = globals().get("COSTO_FN_USD", 0)
THRESHOLD_GRID_V8 = np.array([0.003,0.005,0.0075,0.01,0.015,0.02,0.025,0.03,0.04,0.05,0.075,0.10,0.125,0.15,0.20,0.25,0.30,0.40,0.50])
TOPK_ABS_GRID_V8 = [25, 50, 75, 100, 150, 200]
N_BOOTSTRAP_V8 = 100
# La V8.0 parte desde variables finales de V6.0.
FEATURES_V8_BASE = list(dict.fromkeys(selected_vars_v6))
X_train_v8 = X_train[FEATURES_V8_BASE].copy()
X_val_v8 = X_val[FEATURES_V8_BASE].copy()
X_test_v8 = X_test[FEATURES_V8_BASE].copy()
num_features_v8 = [c for c in num_features_v6 if c in FEATURES_V8_BASE]
cat_features_v8 = [c for c in cat_features_v6 if c in FEATURES_V8_BASE]
print("Variables base V8 desde V6:", len(FEATURES_V8_BASE))
print("Numéricas:", len(num_features_v8), num_features_v8)
print("Categóricas:", len(cat_features_v8), cat_features_v8)
print("Output V8:", OUTPUT_DIR_V8)
==================================================================================================== 14 — CONFIGURACIÓN V8.0 FINAL DESDE V6.0 ==================================================================================================== Variables base V8 desde V6: 18 Numéricas: 7 ['ph_suelo', 'materia_seca_pct', 'edad_arboles_anos', 'firmeza_pulpa_lb', 'densidad_arboles_ha', 'quiebre_cadena_frio_h', 'temperatura_setpoint_frio_c'] Categóricas: 11 ['macro_mercado', 'portainjerto', 'temporada', 'tipo_contenedor', 'linea_packing', 'puerto_salida', 'naviera', 'subzona_agroclimatica', 'packhouse_id', 'canal_destino', 'atmosfera_controlada'] Output V8: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno
15_V8_Laboratorio_Modelos_Optimizacion¶
Entrena candidatos de alto desempeño: benchmark Logístico V6, Random Forest, ExtraTrees, Gradient Boosting, HistGradientBoosting, XGBoost y LightGBM. Cada familia usa una grilla controlada de hiperparámetros, no parámetros por defecto.
# ======================================================================================
# 15. LABORATORIO V8.0 — MODELOS + OPTIMIZACIÓN AUTOMÁTICA DE HIPERPARÁMETROS
# ======================================================================================
print_step("15 — LABORATORIO DE MODELOS V8.0 CON HIPERPARÁMETROS")
# Preprocesadores:
# - Modelos sklearn: OneHotEncoder para categóricas + StandardScaler para numéricas si aplica.
# - XGBoost/LightGBM también funcionan con OHE para mantener consistencia y evitar dependencias de categorical handling.
def make_preprocessor(num_cols, cat_cols, scale_numeric=False):
num_pipe = Pipeline([("imputer", SimpleImputer(strategy="median"))])
if scale_numeric:
num_pipe = Pipeline([("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler())])
cat_pipe = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore", min_frequency=0.01))
])
return ColumnTransformer(
transformers=[
("num", num_pipe, num_cols),
("cat", cat_pipe, cat_cols)
],
remainder="drop"
)
pre_tree = make_preprocessor(num_features_v8, cat_features_v8, scale_numeric=False)
pre_log = make_preprocessor(num_features_v8, cat_features_v8, scale_numeric=True)
# Funciones de evaluación.
def economic_benefit(tp, fp, fn=0):
return float(tp * BENEFICIO_TP_USD - fp * COSTO_FP_USD - fn * COSTO_FN_USD)
def metrics_from_scores(y_true, p, threshold):
y_pred = (np.asarray(p) >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0,1]).ravel()
out = {
"threshold": float(threshold),
"TN": int(tn), "FP": int(fp), "FN": int(fn), "TP": int(tp),
"accuracy": float((tp+tn)/max(1,(tn+fp+fn+tp))),
"precision_ppv": float(precision_score(y_true, y_pred, zero_division=0)),
"recall_sensitivity": float(recall_score(y_true, y_pred, zero_division=0)),
"specificity": float(tn/max(1,(tn+fp))),
"f1_score": float(f1_score(y_true, y_pred, zero_division=0)),
"f2_score": float(fbeta_score(y_true, y_pred, beta=2, zero_division=0)),
"balanced_accuracy": float(balanced_accuracy_score(y_true, y_pred)),
"mcc": float(matthews_corrcoef(y_true, y_pred)) if len(np.unique(y_pred)) > 1 else 0.0,
"beneficio_neto_usd": economic_benefit(tp, fp, fn),
"brier_score": float(brier_score_loss(y_true, p))
}
try: out["roc_auc"] = float(roc_auc_score(y_true, p))
except Exception: out["roc_auc"] = np.nan
try: out["pr_auc"] = float(average_precision_score(y_true, p))
except Exception: out["pr_auc"] = np.nan
return out
def best_threshold_by_validation(y_true, p, grid=THRESHOLD_GRID_V8):
rows = [metrics_from_scores(y_true, p, t) for t in grid]
df = pd.DataFrame(rows)
# Orden profesional: beneficio, recall, F2, PR-AUC. Evita elegir por accuracy.
df = df.sort_values(["beneficio_neto_usd", "recall_sensitivity", "f2_score", "precision_ppv"], ascending=False)
return float(df.iloc[0]["threshold"]), df
def evaluate_topk_abs(y_true, p, k_values=TOPK_ABS_GRID_V8):
y_true = np.asarray(y_true)
p = np.asarray(p)
rows = []
order = np.argsort(-p)
for k in k_values:
k_eff = min(int(k), len(p))
pred = np.zeros(len(p), dtype=int)
pred[order[:k_eff]] = 1
tn, fp, fn, tp = confusion_matrix(y_true, pred, labels=[0,1]).ravel()
rows.append({
"top_k": k_eff,
"TN": int(tn), "FP": int(fp), "FN": int(fn), "TP": int(tp),
"recall_sensitivity": float(recall_score(y_true, pred, zero_division=0)),
"precision_ppv": float(precision_score(y_true, pred, zero_division=0)),
"f1_score": float(f1_score(y_true, pred, zero_division=0)),
"f2_score": float(fbeta_score(y_true, pred, beta=2, zero_division=0)),
"beneficio_neto_usd": economic_benefit(tp, fp, fn)
})
return pd.DataFrame(rows)
# Grillas controladas. Se mantienen razonables para que Colab ejecute en tiempos realistas.
model_specs = []
# Benchmark logístico V6.0: se reusa el mejor modelo ya entrenado.
model_specs.append({"family":"Logistic_V6_Benchmark", "base_estimator": best_model, "param_grid":[{}], "prebuilt": True})
rf_grid = list(ParameterGrid({
"clf__n_estimators": [300, 600],
"clf__max_depth": [None, 6, 10],
"clf__min_samples_leaf": [1, 5, 10],
"clf__min_samples_split": [2, 10],
"clf__max_features": ["sqrt", 0.6],
"clf__class_weight": [None, "balanced"]
}))
model_specs.append({"family":"RandomForest", "base_estimator": Pipeline([("pre", pre_tree), ("clf", RandomForestClassifier(random_state=RANDOM_STATE, n_jobs=-1))]), "param_grid":rf_grid[:48], "prebuilt": False})
et_grid = list(ParameterGrid({
"clf__n_estimators": [300, 600],
"clf__max_depth": [None, 8, 12],
"clf__min_samples_leaf": [1, 5, 10],
"clf__min_samples_split": [2, 10],
"clf__max_features": ["sqrt", 0.6],
"clf__class_weight": [None, "balanced"]
}))
model_specs.append({"family":"ExtraTrees", "base_estimator": Pipeline([("pre", pre_tree), ("clf", ExtraTreesClassifier(random_state=RANDOM_STATE, n_jobs=-1))]), "param_grid":et_grid[:48], "prebuilt": False})
gb_grid = list(ParameterGrid({
"clf__learning_rate": [0.03, 0.05, 0.10],
"clf__n_estimators": [100, 250, 400],
"clf__max_depth": [2, 3],
"clf__subsample": [0.7, 1.0]
}))
model_specs.append({"family":"GradientBoosting", "base_estimator": Pipeline([("pre", pre_tree), ("clf", GradientBoostingClassifier(random_state=RANDOM_STATE))]), "param_grid":gb_grid, "prebuilt": False})
hgb_grid = list(ParameterGrid({
"clf__learning_rate": [0.03, 0.05, 0.10],
"clf__max_iter": [100, 250, 400],
"clf__max_leaf_nodes": [15, 31],
"clf__l2_regularization": [0.0, 0.1, 1.0]
}))
model_specs.append({"family":"HistGradientBoosting", "base_estimator": Pipeline([("pre", pre_tree), ("clf", HistGradientBoostingClassifier(random_state=RANDOM_STATE))]), "param_grid":hgb_grid, "prebuilt": False})
if HAS_XGB:
from xgboost import XGBClassifier
xgb_grid = list(ParameterGrid({
"clf__n_estimators": [200, 400],
"clf__max_depth": [2, 3, 4],
"clf__learning_rate": [0.03, 0.05, 0.10],
"clf__subsample": [0.7, 1.0],
"clf__colsample_bytree": [0.7, 1.0],
"clf__scale_pos_weight": [1, max(1, int((y_train==0).sum()/max(1,(y_train==1).sum())))]
}))
xgb_base = Pipeline([("pre", pre_tree), ("clf", XGBClassifier(random_state=RANDOM_STATE, eval_metric="logloss", n_jobs=-1, tree_method="hist"))])
model_specs.append({"family":"XGBoost", "base_estimator": xgb_base, "param_grid":xgb_grid[:48], "prebuilt": False})
if HAS_LGBM:
from lightgbm import LGBMClassifier
lgb_grid = list(ParameterGrid({
"clf__n_estimators": [200, 400, 700],
"clf__max_depth": [-1, 3, 5],
"clf__learning_rate": [0.03, 0.05, 0.10],
"clf__subsample": [0.7, 1.0],
"clf__colsample_bytree": [0.7, 1.0],
"clf__class_weight": [None, "balanced"]
}))
lgb_base = Pipeline([("pre", pre_tree), ("clf", LGBMClassifier(random_state=RANDOM_STATE, n_jobs=-1, verbose=-1))])
model_specs.append({"family":"LightGBM", "base_estimator": lgb_base, "param_grid":lgb_grid[:48], "prebuilt": False})
results = []
fitted_candidates = {}
threshold_tables = {}
def fit_candidate(estimator, params):
est = clone(estimator)
if params:
est.set_params(**params)
est.fit(X_train_v8, y_train)
return est
candidate_counter = 0
for spec in model_specs:
family = spec["family"]
print(f"\n[LAB] Familia: {family} | candidatos: {len(spec['param_grid'])}")
for params in spec["param_grid"]:
candidate_counter += 1
model_id = f"V8_{candidate_counter:04d}_{family}"
try:
if spec.get("prebuilt", False):
model = spec["base_estimator"]
else:
model = fit_candidate(spec["base_estimator"], params)
p_val = model.predict_proba(X_val_v8)[:,1]
best_t, th_df = best_threshold_by_validation(y_val, p_val, THRESHOLD_GRID_V8)
val_metrics = metrics_from_scores(y_val, p_val, best_t)
val_metrics.update({"model_id": model_id, "modelo": family, "params": str(params), "threshold_validacion": best_t})
results.append(val_metrics)
fitted_candidates[model_id] = model
threshold_tables[model_id] = th_df.assign(model_id=model_id, modelo=family)
except Exception as e:
print(f"[WARNING] Falló {model_id}: {e}")
ranking_val = pd.DataFrame(results)
ranking_val = ranking_val.sort_values(["beneficio_neto_usd","pr_auc","recall_sensitivity","f2_score","f1_score"], ascending=False).reset_index(drop=True)
ranking_val.insert(0, "ranking", np.arange(1, len(ranking_val)+1))
display(ranking_val[["ranking","modelo","model_id","roc_auc","pr_auc","recall_sensitivity","precision_ppv","f1_score","f2_score","beneficio_neto_usd","threshold_validacion"]].head(30))
ranking_val.to_csv(TABLE_DIR_V8 / "V8_0_ranking_validacion_modelos.csv", index=False)
champion_id = ranking_val.iloc[0]["model_id"]
champion_model = fitted_candidates[champion_id]
champion_family = ranking_val.iloc[0]["modelo"]
champion_threshold = float(ranking_val.iloc[0]["threshold_validacion"])
print("\nCAMPEÓN V8.0 por validación temporal:")
print("Modelo:", champion_family)
print("ID:", champion_id)
print("Threshold:", champion_threshold)
print("Beneficio validación:", ranking_val.iloc[0]["beneficio_neto_usd"])
joblib.dump(champion_model, MODEL_DIR_V8 / "V8_0_champion_model.joblib")
==================================================================================================== 15 — LABORATORIO DE MODELOS V8.0 CON HIPERPARÁMETROS ==================================================================================================== [LAB] Familia: Logistic_V6_Benchmark | candidatos: 1 [LAB] Familia: RandomForest | candidatos: 48 [LAB] Familia: ExtraTrees | candidatos: 48 [LAB] Familia: GradientBoosting | candidatos: 36 [LAB] Familia: HistGradientBoosting | candidatos: 54 [LAB] Familia: XGBoost | candidatos: 48 [LAB] Familia: LightGBM | candidatos: 48
| ranking | modelo | model_id | roc_auc | pr_auc | recall_sensitivity | precision_ppv | f1_score | f2_score | beneficio_neto_usd | threshold_validacion | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | XGBoost | V8_0225_XGBoost | 0.809711 | 0.052742 | 0.666667 | 0.049536 | 0.092219 | 0.190931 | 499500.0 | 0.0150 |
| 1 | 2 | LightGBM | V8_0262_LightGBM | 0.808140 | 0.057279 | 0.750000 | 0.040909 | 0.077586 | 0.167910 | 447000.0 | 0.0100 |
| 2 | 3 | LightGBM | V8_0263_LightGBM | 0.808140 | 0.057279 | 0.750000 | 0.040909 | 0.077586 | 0.167910 | 447000.0 | 0.0100 |
| 3 | 4 | GradientBoosting | V8_0116_GradientBoosting | 0.824231 | 0.043337 | 0.791667 | 0.039256 | 0.074803 | 0.163793 | 442500.0 | 0.0100 |
| 4 | 5 | LightGBM | V8_0268_LightGBM | 0.772495 | 0.037180 | 0.666667 | 0.043956 | 0.082474 | 0.173913 | 438000.0 | 0.0030 |
| 5 | 6 | LightGBM | V8_0269_LightGBM | 0.772495 | 0.037180 | 0.666667 | 0.043956 | 0.082474 | 0.173913 | 438000.0 | 0.0030 |
| 6 | 7 | GradientBoosting | V8_0103_GradientBoosting | 0.814443 | 0.049571 | 0.916667 | 0.035599 | 0.068536 | 0.154062 | 426000.0 | 0.0100 |
| 7 | 8 | LightGBM | V8_0278_LightGBM | 0.805502 | 0.065054 | 0.708333 | 0.040865 | 0.077273 | 0.166016 | 421500.0 | 0.0100 |
| 8 | 9 | LightGBM | V8_0279_LightGBM | 0.805502 | 0.065054 | 0.708333 | 0.040865 | 0.077273 | 0.166016 | 421500.0 | 0.0100 |
| 9 | 10 | XGBoost | V8_0200_XGBoost | 0.801676 | 0.049757 | 0.375000 | 0.101124 | 0.159292 | 0.243243 | 420000.0 | 0.0300 |
| 10 | 11 | LightGBM | V8_0266_LightGBM | 0.773723 | 0.038075 | 0.708333 | 0.040284 | 0.076233 | 0.164093 | 412500.0 | 0.0075 |
| 11 | 12 | LightGBM | V8_0267_LightGBM | 0.773723 | 0.038075 | 0.708333 | 0.040284 | 0.076233 | 0.164093 | 412500.0 | 0.0075 |
| 12 | 13 | XGBoost | V8_0224_XGBoost | 0.784739 | 0.043848 | 0.375000 | 0.092784 | 0.148760 | 0.233161 | 408000.0 | 0.0300 |
| 13 | 14 | XGBoost | V8_0232_XGBoost | 0.770582 | 0.037156 | 0.583333 | 0.045752 | 0.084848 | 0.174129 | 402000.0 | 0.0100 |
| 14 | 15 | XGBoost | V8_0229_XGBoost | 0.792794 | 0.052020 | 0.416667 | 0.069930 | 0.119760 | 0.209205 | 400500.0 | 0.0250 |
| 15 | 16 | GradientBoosting | V8_0114_GradientBoosting | 0.799944 | 0.040120 | 0.791667 | 0.036822 | 0.070370 | 0.155229 | 394500.0 | 0.0100 |
| 16 | 17 | XGBoost | V8_0233_XGBoost | 0.779745 | 0.042459 | 0.666667 | 0.040404 | 0.076190 | 0.162602 | 390000.0 | 0.0100 |
| 17 | 18 | GradientBoosting | V8_0112_GradientBoosting | 0.811906 | 0.041111 | 0.833333 | 0.035714 | 0.068493 | 0.152439 | 390000.0 | 0.0100 |
| 18 | 19 | XGBoost | V8_0228_XGBoost | 0.792935 | 0.050010 | 0.458333 | 0.057292 | 0.101852 | 0.190972 | 388500.0 | 0.0200 |
| 19 | 20 | LightGBM | V8_0246_LightGBM | 0.792150 | 0.052640 | 0.708333 | 0.038462 | 0.072961 | 0.157993 | 382500.0 | 0.0100 |
| 20 | 21 | LightGBM | V8_0247_LightGBM | 0.792150 | 0.052640 | 0.708333 | 0.038462 | 0.072961 | 0.157993 | 382500.0 | 0.0100 |
| 21 | 22 | XGBoost | V8_0194_XGBoost | 0.796137 | 0.062230 | 0.791667 | 0.036190 | 0.069217 | 0.152979 | 381000.0 | 0.5000 |
| 22 | 23 | XGBoost | V8_0217_XGBoost | 0.816739 | 0.075676 | 0.500000 | 0.049587 | 0.090226 | 0.177515 | 375000.0 | 0.0200 |
| 23 | 24 | XGBoost | V8_0204_XGBoost | 0.784578 | 0.054682 | 0.416667 | 0.062500 | 0.108696 | 0.195312 | 375000.0 | 0.0200 |
| 24 | 25 | GradientBoosting | V8_0113_GradientBoosting | 0.812419 | 0.048078 | 0.875000 | 0.034314 | 0.066038 | 0.148305 | 373500.0 | 0.0100 |
| 25 | 26 | GradientBoosting | V8_0105_GradientBoosting | 0.780963 | 0.052726 | 0.500000 | 0.048980 | 0.089219 | 0.175953 | 370500.0 | 0.0150 |
| 26 | 27 | LightGBM | V8_0252_LightGBM | 0.777852 | 0.046520 | 0.625000 | 0.040650 | 0.076336 | 0.161290 | 369000.0 | 0.0030 |
| 27 | 28 | LightGBM | V8_0253_LightGBM | 0.777852 | 0.046520 | 0.625000 | 0.040650 | 0.076336 | 0.161290 | 369000.0 | 0.0030 |
| 28 | 29 | XGBoost | V8_0211_XGBoost | 0.786491 | 0.047544 | 0.416667 | 0.059172 | 0.103627 | 0.188679 | 361500.0 | 0.5000 |
| 29 | 30 | LightGBM | V8_0260_LightGBM | 0.793580 | 0.064031 | 0.458333 | 0.052133 | 0.093617 | 0.179153 | 360000.0 | 0.0200 |
CAMPEÓN V8.0 por validación temporal: Modelo: XGBoost ID: V8_0225_XGBoost Threshold: 0.015 Beneficio validación: 499500.0
['/content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/modelos/V8_0_champion_model.joblib']
Este resultado sí es claramente mejor que la regresión V6.0 en validación.
El campeón V8.0 fue:
Modelo: XGBoost ID: V8_0225_XGBoost ROC-AUC: 0.8097 PR-AUC: 0.0527 Recall: 66.7% Precision: 4.95% F1: 0.0922 F2: 0.1909 Beneficio validación: USD 499.500 Threshold: 0.015
Comparado con la mejor regresión V6.0 en validación:
Modelo: Logistic L1 ROC-AUC: 0.7584 PR-AUC: 0.0497 Recall: 25.0% Precision: 9.23% F1: 0.0784 F2: 0.1333 Beneficio validación: USD 271.500 Threshold: 0.030 Lectura experta
V8.0 mejora especialmente en:
Recall: 25.0% → 66.7% F2: 0.1333 → 0.1909 Beneficio: 271.500 → 499.500 ROC-AUC: 0.7584 → 0.8097
Pero baja en Precision:
Precision: 9.23% → 4.95%
Eso significa que XGBoost detecta muchos más reclamos, pero genera más falsas alarmas. Como el beneficio sube, el trade-off parece conveniente en validación.
Conclusión: V8.0 supera a V6.0 como modelo de desempeño; V6.0 queda como benchmark interpretable.
16_V8_Evaluacion_Test_Ranking_Campeon¶
Evalúa el campeón en test, genera ranking consolidado y mantiene V6 como benchmark. Se incluyen ROC, PR, Recall, Precision, F1-Score, F2 y beneficio económico.
# ======================================================================================
# 16. EVALUACIÓN TEST — RANKING FINAL Y MODELO CAMPEÓN
# ======================================================================================
print_step("16 — EVALUACIÓN TEST V8.0 Y RANKING FINAL")
test_rows = []
for _, row in ranking_val.iterrows():
model_id = row["model_id"]
model = fitted_candidates[model_id]
t = float(row["threshold_validacion"])
try:
p_te = model.predict_proba(X_test_v8)[:,1]
met = metrics_from_scores(y_test, p_te, t)
met.update({"model_id": model_id, "modelo": row["modelo"], "threshold_validacion": t, "params": row["params"]})
test_rows.append(met)
except Exception as e:
print("[WARNING] Test falló para", model_id, e)
ranking_test = pd.DataFrame(test_rows)
ranking_test = ranking_test.sort_values(["beneficio_neto_usd","pr_auc","recall_sensitivity","f2_score","f1_score"], ascending=False).reset_index(drop=True)
ranking_test.insert(0, "ranking_test", np.arange(1, len(ranking_test)+1))
display(ranking_test[["ranking_test","modelo","model_id","roc_auc","pr_auc","recall_sensitivity","precision_ppv","f1_score","f2_score","beneficio_neto_usd","threshold_validacion","TP","FP","FN","TN"]].head(30))
ranking_test.to_csv(TABLE_DIR_V8 / "V8_0_ranking_test_modelos.csv", index=False)
# Métricas oficiales del campeón seleccionado SOLO por validación.
p_champion_val = champion_model.predict_proba(X_val_v8)[:,1]
p_champion_test = champion_model.predict_proba(X_test_v8)[:,1]
champion_val_metrics = metrics_from_scores(y_val, p_champion_val, champion_threshold)
champion_test_metrics = metrics_from_scores(y_test, p_champion_test, champion_threshold)
champion_val_metrics.update({"split":"validacion", "modelo":champion_family, "model_id":champion_id})
champion_test_metrics.update({"split":"test", "modelo":champion_family, "model_id":champion_id})
champion_metrics_summary = pd.DataFrame([champion_val_metrics, champion_test_metrics])
display(champion_metrics_summary[["split","modelo","threshold","TN","FP","FN","TP","roc_auc","pr_auc","recall_sensitivity","precision_ppv","f1_score","f2_score","beneficio_neto_usd","brier_score"]])
champion_metrics_summary.to_csv(TABLE_DIR_V8 / "V8_0_champion_metricas_validacion_test.csv", index=False)
# Matrices de confusión del campeón.
for split_name, y_true, p_score in [("validacion", y_val, p_champion_val), ("test", y_test, p_champion_test)]:
pred = (p_score >= champion_threshold).astype(int)
cm = confusion_matrix(y_true, pred, labels=[0,1])
cm_df = pd.DataFrame(cm, index=["Real 0", "Real 1"], columns=["Pred 0", "Pred 1"])
print(f"\nMatriz de confusión campeón — {split_name.upper()} | threshold={champion_threshold:.4f}")
display(cm_df)
plt.figure(figsize=(5,4))
plt.imshow(cm, aspect="auto")
plt.title(f"Matriz de confusión V8 campeón — {split_name}")
plt.xticks([0,1], ["Pred 0","Pred 1"])
plt.yticks([0,1], ["Real 0","Real 1"])
for i in range(2):
for j in range(2):
plt.text(j, i, int(cm[i,j]), ha="center", va="center")
savefig_v8(f"V8_0_matriz_confusion_campeon_{split_name}.png")
==================================================================================================== 16 — EVALUACIÓN TEST V8.0 Y RANKING FINAL ====================================================================================================
| ranking_test | modelo | model_id | roc_auc | pr_auc | recall_sensitivity | precision_ppv | f1_score | f2_score | beneficio_neto_usd | threshold_validacion | TP | FP | FN | TN | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | LightGBM | V8_0280_LightGBM | 0.747854 | 0.040487 | 0.586207 | 0.036638 | 0.068966 | 0.146552 | 349500.0 | 0.0050 | 17 | 447 | 12 | 1638 |
| 1 | 2 | LightGBM | V8_0281_LightGBM | 0.747854 | 0.040487 | 0.586207 | 0.036638 | 0.068966 | 0.146552 | 349500.0 | 0.0050 | 17 | 447 | 12 | 1638 |
| 2 | 3 | LightGBM | V8_0278_LightGBM | 0.720814 | 0.039496 | 0.551724 | 0.037296 | 0.069869 | 0.146789 | 340500.0 | 0.0100 | 16 | 413 | 13 | 1672 |
| 3 | 4 | LightGBM | V8_0279_LightGBM | 0.720814 | 0.039496 | 0.551724 | 0.037296 | 0.069869 | 0.146789 | 340500.0 | 0.0100 | 16 | 413 | 13 | 1672 |
| 4 | 5 | HistGradientBoosting | V8_0177_HistGradientBoosting | 0.690796 | 0.032982 | 0.413793 | 0.043478 | 0.078689 | 0.153061 | 324000.0 | 0.0150 | 12 | 264 | 17 | 1821 |
| 5 | 6 | HistGradientBoosting | V8_0179_HistGradientBoosting | 0.690796 | 0.032982 | 0.413793 | 0.043478 | 0.078689 | 0.153061 | 324000.0 | 0.0150 | 12 | 264 | 17 | 1821 |
| 6 | 7 | HistGradientBoosting | V8_0181_HistGradientBoosting | 0.690796 | 0.032982 | 0.413793 | 0.043478 | 0.078689 | 0.153061 | 324000.0 | 0.0150 | 12 | 264 | 17 | 1821 |
| 7 | 8 | XGBoost | V8_0211_XGBoost | 0.682014 | 0.036436 | 0.310345 | 0.055215 | 0.093750 | 0.161290 | 309000.0 | 0.5000 | 9 | 154 | 20 | 1931 |
| 8 | 9 | XGBoost | V8_0229_XGBoost | 0.673993 | 0.036577 | 0.275862 | 0.062016 | 0.101266 | 0.163265 | 298500.0 | 0.0250 | 8 | 121 | 21 | 1964 |
| 9 | 10 | LightGBM | V8_0238_LightGBM | 0.707848 | 0.036435 | 0.310345 | 0.052941 | 0.090452 | 0.157343 | 298500.0 | 0.0030 | 9 | 161 | 20 | 1924 |
| 10 | 11 | LightGBM | V8_0239_LightGBM | 0.707848 | 0.036435 | 0.310345 | 0.052941 | 0.090452 | 0.157343 | 298500.0 | 0.0030 | 9 | 161 | 20 | 1924 |
| 11 | 12 | GradientBoosting | V8_0127_GradientBoosting | 0.671347 | 0.033687 | 0.413793 | 0.040816 | 0.074303 | 0.146341 | 297000.0 | 0.0150 | 12 | 282 | 17 | 1803 |
| 12 | 13 | LightGBM | V8_0236_LightGBM | 0.709534 | 0.033567 | 0.482759 | 0.037135 | 0.068966 | 0.141988 | 295500.0 | 0.0050 | 14 | 363 | 15 | 1722 |
| 13 | 14 | LightGBM | V8_0237_LightGBM | 0.709534 | 0.033567 | 0.482759 | 0.037135 | 0.068966 | 0.141988 | 295500.0 | 0.0050 | 14 | 363 | 15 | 1722 |
| 14 | 15 | HistGradientBoosting | V8_0176_HistGradientBoosting | 0.672935 | 0.032424 | 0.310345 | 0.051724 | 0.088670 | 0.155172 | 292500.0 | 0.0200 | 9 | 165 | 20 | 1920 |
| 15 | 16 | HistGradientBoosting | V8_0178_HistGradientBoosting | 0.672935 | 0.032424 | 0.310345 | 0.051724 | 0.088670 | 0.155172 | 292500.0 | 0.0200 | 9 | 165 | 20 | 1920 |
| 16 | 17 | HistGradientBoosting | V8_0180_HistGradientBoosting | 0.672935 | 0.032424 | 0.310345 | 0.051724 | 0.088670 | 0.155172 | 292500.0 | 0.0200 | 9 | 165 | 20 | 1920 |
| 17 | 18 | LightGBM | V8_0250_LightGBM | 0.702737 | 0.030894 | 0.448276 | 0.036932 | 0.068241 | 0.138889 | 271500.0 | 0.0075 | 13 | 339 | 16 | 1746 |
| 18 | 19 | LightGBM | V8_0251_LightGBM | 0.702737 | 0.030894 | 0.448276 | 0.036932 | 0.068241 | 0.138889 | 271500.0 | 0.0075 | 13 | 339 | 16 | 1746 |
| 19 | 20 | XGBoost | V8_0226_XGBoost | 0.689159 | 0.035076 | 0.379310 | 0.040000 | 0.072368 | 0.140665 | 264000.0 | 0.4000 | 11 | 264 | 18 | 1821 |
| 20 | 21 | XGBoost | V8_0204_XGBoost | 0.661110 | 0.031982 | 0.275862 | 0.050314 | 0.085106 | 0.145455 | 253500.0 | 0.0200 | 8 | 151 | 21 | 1934 |
| 21 | 22 | XGBoost | V8_0203_XGBoost | 0.681469 | 0.042362 | 0.413793 | 0.036923 | 0.067797 | 0.136054 | 250500.0 | 0.5000 | 12 | 313 | 17 | 1772 |
| 22 | 23 | XGBoost | V8_0208_XGBoost | 0.688564 | 0.033576 | 0.344828 | 0.041152 | 0.073529 | 0.139276 | 250500.0 | 0.0150 | 10 | 233 | 19 | 1852 |
| 23 | 24 | HistGradientBoosting | V8_0140_HistGradientBoosting | 0.674828 | 0.039727 | 0.275862 | 0.049383 | 0.083770 | 0.143885 | 249000.0 | 0.0200 | 8 | 154 | 21 | 1931 |
| 24 | 25 | HistGradientBoosting | V8_0142_HistGradientBoosting | 0.674828 | 0.039727 | 0.275862 | 0.049383 | 0.083770 | 0.143885 | 249000.0 | 0.0200 | 8 | 154 | 21 | 1931 |
| 25 | 26 | HistGradientBoosting | V8_0144_HistGradientBoosting | 0.674828 | 0.039727 | 0.275862 | 0.049383 | 0.083770 | 0.143885 | 249000.0 | 0.0200 | 8 | 154 | 21 | 1931 |
| 26 | 27 | HistGradientBoosting | V8_0159_HistGradientBoosting | 0.657471 | 0.029513 | 0.344828 | 0.040816 | 0.072993 | 0.138504 | 247500.0 | 0.0150 | 10 | 235 | 19 | 1850 |
| 27 | 28 | HistGradientBoosting | V8_0161_HistGradientBoosting | 0.657471 | 0.029513 | 0.344828 | 0.040816 | 0.072993 | 0.138504 | 247500.0 | 0.0150 | 10 | 235 | 19 | 1850 |
| 28 | 29 | HistGradientBoosting | V8_0163_HistGradientBoosting | 0.657471 | 0.029513 | 0.344828 | 0.040816 | 0.072993 | 0.138504 | 247500.0 | 0.0150 | 10 | 235 | 19 | 1850 |
| 29 | 30 | HistGradientBoosting | V8_0134_HistGradientBoosting | 0.647647 | 0.035545 | 0.275862 | 0.047619 | 0.081218 | 0.140845 | 240000.0 | 0.0200 | 8 | 160 | 21 | 1925 |
| split | modelo | threshold | TN | FP | FN | TP | roc_auc | pr_auc | recall_sensitivity | precision_ppv | f1_score | f2_score | beneficio_neto_usd | brier_score | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | validacion | XGBoost | 0.015 | 1762 | 307 | 8 | 16 | 0.809711 | 0.052742 | 0.666667 | 0.049536 | 0.092219 | 0.190931 | 499500.0 | 0.011179 |
| 1 | test | XGBoost | 0.015 | 1795 | 290 | 18 | 11 | 0.681948 | 0.034352 | 0.379310 | 0.036545 | 0.066667 | 0.131894 | 225000.0 | 0.013536 |
Matriz de confusión campeón — VALIDACION | threshold=0.0150
| Pred 0 | Pred 1 | |
|---|---|---|
| Real 0 | 1762 | 307 |
| Real 1 | 8 | 16 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_matriz_confusion_campeon_validacion.png
Matriz de confusión campeón — TEST | threshold=0.0150
| Pred 0 | Pred 1 | |
|---|---|---|
| Real 0 | 1795 | 290 |
| Real 1 | 18 | 11 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_matriz_confusion_campeon_test.png
Estos resultados ya muestran una mejora real versus V6.0, pero con una observación importante: el campeón elegido por validación fue XGBoost, pero en test el mejor modelo del ranking fue LightGBM.
Campeón por validación: XGBoost Validación: TP = 16 FP = 307 FN = 8 TN = 1762 ROC-AUC = 0.8097 PR-AUC = 0.0527 Recall = 66.7% Precision = 4.95% F1 = 0.0922 F2 = 0.1909 Beneficio = USD 499.500
En test:
TP = 11 FP = 290 FN = 18 TN = 1795 ROC-AUC = 0.6819 PR-AUC = 0.0344 Recall = 37.9% Precision = 3.65% F1 = 0.0667 F2 = 0.1319 Beneficio = USD 225.000 Mejor modelo observado en test: LightGBM TP = 17 FP = 447 FN = 12 TN = 1638 ROC-AUC = 0.7479 PR-AUC = 0.0405 Recall = 58.6% Precision = 3.66% F1 = 0.0690 F2 = 0.1466 Beneficio = USD 349.500 Comparación clave contra V6.0 test Modelo TP FP FN Recall Precision F1 F2 Beneficio V6.0 Logística 4 125 25 13.8% 3.10% 0.0506 0.0816 USD 52.500 V8 XGBoost campeón validación 11 290 18 37.9% 3.65% 0.0667 0.1319 USD 225.000 V8 LightGBM mejor test 17 447 12 58.6% 3.66% 0.0690 0.1466 USD 349.500
Conclusión: V8.0 sí supera claramente a V6.0 como modelo de alto desempeño. Para defensa, yo diría:
XGBoost fue el campeón seleccionado correctamente en validación temporal. Sin embargo, al observar test, LightGBM mostró mejor desempeño final. Esto no debe usarse para re-elegir retroactivamente el modelo, pero sí como evidencia de que la familia boosting es superior al benchmark logístico y debería ser la línea principal de mejora.
17_V8_Curvas_Todos_Modelos¶
Genera curvas ROC, Precision-Recall, Lift, Gain, calibración, Recall vs Threshold, Precision vs Threshold, Beneficio vs Threshold y Recall vs Top-K. También compara campeón contra logística V6.
# ======================================================================================
# 17. CURVAS V8.0 — TODOS LOS MODELOS Y COMPARACIÓN CAMPEÓN VS LOGÍSTICA V6
# ======================================================================================
print_step("17 — CURVAS TODOS LOS MODELOS V8.0")
# Seleccionar top modelos por validación para curvas legibles.
top_model_ids = ranking_val.head(8)["model_id"].tolist()
if champion_id not in top_model_ids:
top_model_ids = [champion_id] + top_model_ids
score_test_by_model = {}
for mid in top_model_ids:
try:
score_test_by_model[mid] = fitted_candidates[mid].predict_proba(X_test_v8)[:,1]
except Exception:
pass
# ROC comparativo.
plt.figure(figsize=(8,6))
for mid, p in score_test_by_model.items():
fpr, tpr, _ = roc_curve(y_test, p)
auc_val = roc_auc_score(y_test, p)
label = f"{ranking_val.loc[ranking_val.model_id.eq(mid),'modelo'].iloc[0]} ({auc_val:.3f})"
plt.plot(fpr, tpr, label=label)
plt.plot([0,1],[0,1], linestyle="--")
plt.title("Comparación ROC — Top modelos V8.0 en test")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate / Recall")
plt.legend(fontsize=8)
savefig_v8("V8_0_comparacion_ROC_top_modelos.png")
# PR comparativo.
plt.figure(figsize=(8,6))
base_rate = y_test.mean()
for mid, p in score_test_by_model.items():
prec, rec, _ = precision_recall_curve(y_test, p)
ap = average_precision_score(y_test, p)
label = f"{ranking_val.loc[ranking_val.model_id.eq(mid),'modelo'].iloc[0]} ({ap:.3f})"
plt.plot(rec, prec, label=label)
plt.axhline(base_rate, linestyle="--", label=f"Base rate={base_rate:.3f}")
plt.title("Comparación Precision-Recall — Top modelos V8.0 en test")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.legend(fontsize=8)
savefig_v8("V8_0_comparacion_PR_top_modelos.png")
# Lift y Gain para campeón.
def lift_gain_table(y_true, p, n_bins=10):
df = pd.DataFrame({"y": np.asarray(y_true), "p": np.asarray(p)})
df = df.sort_values("p", ascending=False).reset_index(drop=True)
df["decil"] = pd.qcut(df.index + 1, q=n_bins, labels=False, duplicates="drop") + 1
agg = df.groupby("decil", as_index=False).agg(n=("y","size"), positives=("y","sum"), p_min=("p","min"), p_max=("p","max"))
agg["cum_n"] = agg["n"].cumsum()
agg["cum_positives"] = agg["positives"].cumsum()
total_pos = max(1, df["y"].sum())
total_n = len(df)
agg["gain"] = agg["cum_positives"] / total_pos
agg["population_pct"] = agg["cum_n"] / total_n
agg["lift"] = (agg["cum_positives"] / agg["cum_n"]) / (total_pos / total_n)
return agg
lg_champion = lift_gain_table(y_test, p_champion_test, 10)
display(lg_champion)
lg_champion.to_csv(TABLE_DIR_V8 / "V8_0_lift_gain_campeon_test.csv", index=False)
plt.figure(figsize=(8,5))
plt.plot(lg_champion["population_pct"], lg_champion["gain"], marker="o")
plt.plot([0,1],[0,1], linestyle="--")
plt.title("Gain acumulado — Campeón V8.0")
plt.xlabel("% población revisada")
plt.ylabel("% reclamos capturados")
savefig_v8("V8_0_gain_campeon.png")
plt.figure(figsize=(8,5))
plt.plot(lg_champion["population_pct"], lg_champion["lift"], marker="o")
plt.title("Lift acumulado — Campeón V8.0")
plt.xlabel("% población revisada")
plt.ylabel("Lift")
savefig_v8("V8_0_lift_campeon.png")
# Calibración campeón.
prob_true, prob_pred = calibration_curve(y_test, p_champion_test, n_bins=8, strategy="quantile")
plt.figure(figsize=(6,5))
plt.plot(prob_pred, prob_true, marker="o")
plt.plot([0,1],[0,1], linestyle="--")
plt.title("Curva de calibración — Campeón V8.0")
plt.xlabel("Probabilidad predicha promedio")
plt.ylabel("Frecuencia observada")
savefig_v8("V8_0_calibracion_campeon.png")
# Threshold curves campeón.
th_champ = pd.DataFrame([metrics_from_scores(y_test, p_champion_test, t) for t in THRESHOLD_GRID_V8])
th_champ.to_csv(TABLE_DIR_V8 / "V8_0_curvas_threshold_campeon_test.csv", index=False)
display(th_champ[["threshold","TP","FP","FN","TN","recall_sensitivity","precision_ppv","f1_score","f2_score","beneficio_neto_usd"]])
for metric, title, fname in [
("recall_sensitivity", "Recall vs Threshold — Campeón V8.0", "V8_0_recall_vs_threshold_campeon.png"),
("precision_ppv", "Precision vs Threshold — Campeón V8.0", "V8_0_precision_vs_threshold_campeon.png"),
("beneficio_neto_usd", "Beneficio vs Threshold — Campeón V8.0", "V8_0_beneficio_vs_threshold_campeon.png")
]:
plt.figure(figsize=(8,5))
plt.plot(th_champ["threshold"], th_champ[metric], marker="o")
plt.title(title)
plt.xlabel("Threshold")
plt.ylabel(metric)
savefig_v8(fname)
# Top-K campeón vs logística V6.
topk_champion = evaluate_topk_abs(y_test, p_champion_test, TOPK_ABS_GRID_V8)
topk_champion["modelo"] = f"Campeón V8 — {champion_family}"
try:
p_log_v6_test = best_model.predict_proba(X_test_v8)[:,1]
except Exception:
p_log_v6_test = best_model.predict_proba(X_test[selected_vars_v6])[:,1]
topk_log = evaluate_topk_abs(y_test, p_log_v6_test, TOPK_ABS_GRID_V8)
topk_log["modelo"] = "Benchmark Logístico V6"
topk_comp = pd.concat([topk_champion, topk_log], ignore_index=True)
display(topk_comp[["modelo","top_k","TP","FP","FN","TN","recall_sensitivity","precision_ppv","f1_score","f2_score","beneficio_neto_usd"]])
topk_comp.to_csv(TABLE_DIR_V8 / "V8_0_topk_campeon_vs_logistica.csv", index=False)
plt.figure(figsize=(8,5))
for modelo, grp in topk_comp.groupby("modelo"):
plt.plot(grp["top_k"], grp["recall_sensitivity"], marker="o", label=modelo)
plt.title("Recall vs Capacidad Operacional Top-K — Campeón vs Logística")
plt.xlabel("Top-K lotes revisados")
plt.ylabel("Recall / reclamos capturados")
plt.legend()
savefig_v8("V8_0_recall_vs_topk_campeon_vs_logistica.png")
plt.figure(figsize=(8,5))
for modelo, grp in topk_comp.groupby("modelo"):
plt.plot(grp["top_k"], grp["beneficio_neto_usd"], marker="o", label=modelo)
plt.title("Beneficio vs Top-K — Campeón vs Logística")
plt.xlabel("Top-K lotes revisados")
plt.ylabel("Beneficio neto USD")
plt.legend()
savefig_v8("V8_0_beneficio_vs_topk_campeon_vs_logistica.png")
==================================================================================================== 17 — CURVAS TODOS LOS MODELOS V8.0 ==================================================================================================== [GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_comparacion_ROC_top_modelos.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_comparacion_PR_top_modelos.png
| decil | n | positives | p_min | p_max | cum_n | cum_positives | gain | population_pct | lift | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 212 | 9 | 0.019541 | 0.216337 | 212 | 9 | 0.310345 | 0.100284 | 3.094665 |
| 1 | 2 | 211 | 3 | 0.011599 | 0.019517 | 423 | 12 | 0.413793 | 0.200095 | 2.067987 |
| 2 | 3 | 211 | 4 | 0.007937 | 0.011597 | 634 | 16 | 0.551724 | 0.299905 | 1.839661 |
| 3 | 4 | 212 | 2 | 0.005428 | 0.007931 | 846 | 18 | 0.620690 | 0.400189 | 1.550990 |
| 4 | 5 | 211 | 4 | 0.003828 | 0.005418 | 1057 | 22 | 0.758621 | 0.500000 | 1.517241 |
| 5 | 6 | 211 | 1 | 0.002429 | 0.003820 | 1268 | 23 | 0.793103 | 0.599811 | 1.322256 |
| 6 | 7 | 212 | 3 | 0.001223 | 0.002428 | 1480 | 26 | 0.896552 | 0.700095 | 1.280615 |
| 7 | 8 | 211 | 2 | 0.000476 | 0.001208 | 1691 | 28 | 0.965517 | 0.799905 | 1.207039 |
| 8 | 9 | 211 | 0 | 0.000223 | 0.000472 | 1902 | 28 | 0.965517 | 0.899716 | 1.073135 |
| 9 | 10 | 212 | 1 | 0.000021 | 0.000223 | 2114 | 29 | 1.000000 | 1.000000 | 1.000000 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_gain_campeon.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_lift_campeon.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_calibracion_campeon.png
| threshold | TP | FP | FN | TN | recall_sensitivity | precision_ppv | f1_score | f2_score | beneficio_neto_usd | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.0030 | 22 | 1145 | 7 | 940 | 0.758621 | 0.018852 | 0.036789 | 0.085737 | -397500.0 |
| 1 | 0.0050 | 18 | 873 | 11 | 1212 | 0.620690 | 0.020202 | 0.039130 | 0.089374 | -229500.0 |
| 2 | 0.0075 | 16 | 649 | 13 | 1436 | 0.551724 | 0.024060 | 0.046110 | 0.102433 | -13500.0 |
| 3 | 0.0100 | 13 | 488 | 16 | 1597 | 0.448276 | 0.025948 | 0.049057 | 0.105348 | 48000.0 |
| 4 | 0.0150 | 11 | 290 | 18 | 1795 | 0.379310 | 0.036545 | 0.066667 | 0.131894 | 225000.0 |
| 5 | 0.0200 | 8 | 195 | 21 | 1890 | 0.275862 | 0.039409 | 0.068966 | 0.125392 | 187500.0 |
| 6 | 0.0250 | 7 | 144 | 22 | 1941 | 0.241379 | 0.046358 | 0.077778 | 0.131086 | 204000.0 |
| 7 | 0.0300 | 5 | 106 | 24 | 1979 | 0.172414 | 0.045045 | 0.071429 | 0.110132 | 141000.0 |
| 8 | 0.0400 | 3 | 60 | 26 | 2025 | 0.103448 | 0.047619 | 0.065217 | 0.083799 | 90000.0 |
| 9 | 0.0500 | 3 | 39 | 26 | 2046 | 0.103448 | 0.071429 | 0.084507 | 0.094937 | 121500.0 |
| 10 | 0.0750 | 0 | 19 | 29 | 2066 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | -28500.0 |
| 11 | 0.1000 | 0 | 7 | 29 | 2078 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | -10500.0 |
| 12 | 0.1250 | 0 | 2 | 29 | 2083 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | -3000.0 |
| 13 | 0.1500 | 0 | 2 | 29 | 2083 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | -3000.0 |
| 14 | 0.2000 | 0 | 1 | 29 | 2084 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | -1500.0 |
| 15 | 0.2500 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.0 |
| 16 | 0.3000 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.0 |
| 17 | 0.4000 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.0 |
| 18 | 0.5000 | 0 | 0 | 29 | 2085 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.0 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_recall_vs_threshold_campeon.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_precision_vs_threshold_campeon.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_beneficio_vs_threshold_campeon.png
| modelo | top_k | TP | FP | FN | TN | recall_sensitivity | precision_ppv | f1_score | f2_score | beneficio_neto_usd | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Campeón V8 — XGBoost | 25 | 1 | 24 | 28 | 2061 | 0.034483 | 0.040000 | 0.037037 | 0.035461 | 24000.0 |
| 1 | Campeón V8 — XGBoost | 50 | 3 | 47 | 26 | 2038 | 0.103448 | 0.060000 | 0.075949 | 0.090361 | 109500.0 |
| 2 | Campeón V8 — XGBoost | 75 | 4 | 71 | 25 | 2014 | 0.137931 | 0.053333 | 0.076923 | 0.104712 | 133500.0 |
| 3 | Campeón V8 — XGBoost | 100 | 5 | 95 | 24 | 1990 | 0.172414 | 0.050000 | 0.077519 | 0.115741 | 157500.0 |
| 4 | Campeón V8 — XGBoost | 150 | 7 | 143 | 22 | 1942 | 0.241379 | 0.046667 | 0.078212 | 0.131579 | 205500.0 |
| 5 | Campeón V8 — XGBoost | 200 | 8 | 192 | 21 | 1893 | 0.275862 | 0.040000 | 0.069869 | 0.126582 | 192000.0 |
| 6 | Benchmark Logístico V6 | 25 | 1 | 24 | 28 | 2061 | 0.034483 | 0.040000 | 0.037037 | 0.035461 | 24000.0 |
| 7 | Benchmark Logístico V6 | 50 | 2 | 48 | 27 | 2037 | 0.068966 | 0.040000 | 0.050633 | 0.060241 | 48000.0 |
| 8 | Benchmark Logístico V6 | 75 | 2 | 73 | 27 | 2012 | 0.068966 | 0.026667 | 0.038462 | 0.052356 | 10500.0 |
| 9 | Benchmark Logístico V6 | 100 | 3 | 97 | 26 | 1988 | 0.103448 | 0.030000 | 0.046512 | 0.069444 | 34500.0 |
| 10 | Benchmark Logístico V6 | 150 | 6 | 144 | 23 | 1941 | 0.206897 | 0.040000 | 0.067039 | 0.112782 | 144000.0 |
| 11 | Benchmark Logístico V6 | 200 | 6 | 194 | 23 | 1891 | 0.206897 | 0.030000 | 0.052402 | 0.094937 | 69000.0 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_recall_vs_topk_campeon_vs_logistica.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_beneficio_vs_topk_campeon_vs_logistica.png
Estos resultados confirman que V8.0 mejora a V6.0, pero también muestran que el uso óptimo depende de la política operacional.
- Threshold económico del campeón XGBoost
El mejor punto para XGBoost en test es:
threshold = 0.015 TP = 11 FP = 290 FN = 18 TN = 1795 Recall = 37.9% Precision = 3.65% Beneficio = USD 225.000
Ese es un resultado bastante mejor que V6.0, porque V6.0 tenía:
TP = 4 Recall = 13.8% Beneficio = USD 52.500 2. Curva Gain / Lift
El primer decil concentra:
9 de 29 reclamos Gain = 31.0% Lift = 3.09
Esto es importante: el modelo ordena bien el riesgo. El 10% de mayor riesgo contiene más de 3 veces la concentración esperada de reclamos versus seleccionar al azar.
- Top-K
Comparación clave:
Política Top-K TP Recall Beneficio XGBoost 150 7 24.1% USD 205.500 XGBoost 200 8 27.6% USD 192.000 Logística V6 150 6 20.7% USD 144.000 Logística V6 200 6 20.7% USD 69.000
XGBoost también gana en Top-K.
Conclusión experta
V8.0 no es perfecto, pero sí es un modelo de mejor desempeño que V6.0.
La mejor forma de defenderlo es:
V6.0 se conserva como benchmark interpretable. V8.0, usando XGBoost, mejora la capacidad de ranking, captura más reclamos y genera mayor beneficio económico. El uso recomendado no es bloqueo automático, sino priorización operacional mediante threshold económico o Top-K según capacidad de revisión.
18_V8_Importancia_Variables¶
Calcula importancia de variables para el campeón con permutation importance y, si existe, importancia nativa del modelo. Se resume además por nivel V4.0/V6.0.
# ======================================================================================
# 18. IMPORTANCIA DE VARIABLES — CAMPEÓN V8.0
# ======================================================================================
print_step("18 — IMPORTANCIA DE VARIABLES V8.0")
# Permutation importance sobre variables originales, usando PR-AUC como scoring por evento raro.
try:
perm = permutation_importance(
champion_model, X_test_v8, y_test,
n_repeats=20, random_state=RANDOM_STATE,
scoring="average_precision", n_jobs=-1
)
imp = pd.DataFrame({
"variable": FEATURES_V8_BASE,
"importance_mean_pr_auc": perm.importances_mean,
"importance_std": perm.importances_std
}).sort_values("importance_mean_pr_auc", ascending=False)
except Exception as e:
print("[WARNING] permutation_importance falló:", e)
imp = pd.DataFrame({"variable": FEATURES_V8_BASE, "importance_mean_pr_auc": np.nan, "importance_std": np.nan})
# Enriquecer con niveles V4.0/V6.0 si está disponible.
try:
inv_cols = [c for c in ["variable","nivel","nombre_nivel","area","justificacion"] if c in selected_feature_inventory_v6.columns]
imp = imp.merge(selected_feature_inventory_v6[inv_cols].drop_duplicates("variable"), on="variable", how="left")
except Exception:
pass
display(imp.head(30))
imp.to_csv(TABLE_DIR_V8 / "V8_0_importancia_variables_campeon.csv", index=False)
plt.figure(figsize=(9, max(4, min(20, len(imp))*0.35)))
top_imp = imp.head(20).iloc[::-1]
plt.barh(top_imp["variable"], top_imp["importance_mean_pr_auc"])
plt.title("Importancia de variables — Permutation PR-AUC — Campeón V8.0")
plt.xlabel("Disminución promedio PR-AUC al permutar")
savefig_v8("V8_0_importancia_variables_campeon.png")
# Importancia por nivel.
if "nivel" in imp.columns:
level_imp = imp.groupby(["nivel","nombre_nivel"], dropna=False, as_index=False).agg(
importancia_promedio=("importance_mean_pr_auc","mean"),
importancia_total=("importance_mean_pr_auc","sum"),
n_variables=("variable","nunique")
).sort_values("importancia_total", ascending=False)
display(level_imp)
level_imp.to_csv(TABLE_DIR_V8 / "V8_0_importancia_por_nivel.csv", index=False)
plt.figure(figsize=(8,5))
li = level_imp.iloc[::-1]
plt.barh(li["nivel"].astype(str) + " — " + li["nombre_nivel"].fillna(""), li["importancia_total"])
plt.title("Importancia total por nivel — V8.0")
plt.xlabel("Importancia total")
savefig_v8("V8_0_importancia_por_nivel.png")
==================================================================================================== 18 — IMPORTANCIA DE VARIABLES V8.0 ====================================================================================================
| variable | importance_mean_pr_auc | importance_std | nivel | nombre_nivel | area | justificacion | |
|---|---|---|---|---|---|---|---|
| 0 | macro_mercado | 0.012012 | 0.003769 | Nivel 3 | Variables de Entorno Operacional | Mercado | Agrupa destinos con comportamiento logístico y... |
| 1 | firmeza_pulpa_lb | 0.008153 | 0.003352 | Nivel 2 | Variables Explicativas Primarias | Madurez | Resistencia estructural del fruto frente a dañ... |
| 2 | materia_seca_pct | 0.007476 | 0.004242 | Nivel 1 | Variables Causales Directas | Madurez | Indicador maestro de madurez funcional de la p... |
| 3 | quiebre_cadena_frio_h | 0.004681 | 0.003590 | Nivel 1 | Variables Causales Directas | Frío | Mide exposición térmica fuera de rango; puede ... |
| 4 | naviera | 0.003447 | 0.001549 | Nivel 3 | Variables de Entorno Operacional | Logística | Naviera captura diferencias de servicio, ruta ... |
| 5 | atmosfera_controlada | 0.003067 | 0.001246 | Nivel 2 | Variables Explicativas Primarias | Contenedor | Condición de atmósfera afecta conservación y m... |
| 6 | edad_arboles_anos | 0.001800 | 0.004982 | Nivel 2 | Variables Explicativas Primarias | Origen | Edad del huerto puede influir en vigor, produc... |
| 7 | densidad_arboles_ha | 0.001682 | 0.001021 | Nivel 2 | Variables Explicativas Primarias | Origen | Densidad afecta competencia, luz, vigor y desa... |
| 8 | tipo_contenedor | 0.001520 | 0.000641 | Nivel 3 | Variables de Entorno Operacional | Contenedor | Tipo de contenedor puede afectar conservación ... |
| 9 | linea_packing | 0.000318 | 0.001577 | Nivel 3 | Variables de Entorno Operacional | Packing | Diferencias de línea pueden afectar manipulaci... |
| 10 | packhouse_id | 0.000310 | 0.000570 | Nivel 3 | Variables de Entorno Operacional | Packing | Captura diferencias operacionales entre planta... |
| 11 | canal_destino | 0.000000 | 0.000000 | Nivel 3 | Variables de Entorno Operacional | Mercado | Canal puede reflejar exigencia de cliente y ni... |
| 12 | temporada | 0.000000 | 0.000000 | Nivel 3 | Variables de Entorno Operacional | Temporal | Captura cambios productivos, comerciales y cli... |
| 13 | subzona_agroclimatica | -0.000380 | 0.000688 | Nivel 1 | Variables Causales Directas | Origen | Define condiciones agroclimáticas y ventana de... |
| 14 | portainjerto | -0.000597 | 0.001222 | Nivel 2 | Variables Explicativas Primarias | Origen | Portainjerto puede influir en vigor, absorción... |
| 15 | ph_suelo | -0.000850 | 0.005522 | Nivel 2 | Variables Explicativas Primarias | Origen | pH afecta disponibilidad nutricional y condici... |
| 16 | puerto_salida | -0.001499 | 0.003483 | Nivel 3 | Variables de Entorno Operacional | Logística | Puerto puede asociarse a tiempos, infraestruct... |
| 17 | temperatura_setpoint_frio_c | -0.006175 | 0.004756 | Nivel 1 | Variables Causales Directas | Frío | Temperatura objetivo debe ser coherente con ma... |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_importancia_variables_campeon.png
| nivel | nombre_nivel | importancia_promedio | importancia_total | n_variables | |
|---|---|---|---|---|---|
| 2 | Nivel 3 | Variables de Entorno Operacional | 0.002013 | 0.016108 | 8 |
| 1 | Nivel 2 | Variables Explicativas Primarias | 0.002209 | 0.013255 | 6 |
| 0 | Nivel 1 | Variables Causales Directas | 0.001400 | 0.005601 | 4 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_importancia_por_nivel.png
19_V8_Robustez¶
Incluye bootstrap, estabilidad temporal, sensibilidad al threshold, sensibilidad a temporadas y sensibilidad a selección de variables. Se evalúa el campeón y se reportan intervalos de confianza empíricos.
# ======================================================================================
# 19. ROBUSTEZ V8.0 — BOOTSTRAP, TEMPORADAS, THRESHOLD Y VARIABLES
# ======================================================================================
print_step("19 — ROBUSTEZ V8.0")
rng = np.random.default_rng(RANDOM_STATE)
boot_rows = []
n_test = len(y_test)
y_test_arr = np.asarray(y_test)
p_test_arr = np.asarray(p_champion_test)
for b in range(N_BOOTSTRAP_V8):
idx = rng.integers(0, n_test, n_test)
if len(np.unique(y_test_arr[idx])) < 2:
continue
met = metrics_from_scores(y_test_arr[idx], p_test_arr[idx], champion_threshold)
met["iteracion"] = b + 1
boot_rows.append(met)
bootstrap_v8 = pd.DataFrame(boot_rows)
summary_cols = ["roc_auc","pr_auc","recall_sensitivity","precision_ppv","f1_score","f2_score","mcc","brier_score","TP","FP","FN","TN","beneficio_neto_usd"]
bootstrap_summary_v8 = bootstrap_v8[summary_cols].agg(["mean","std",lambda s: s.quantile(0.025),"median",lambda s: s.quantile(0.975)]).T.reset_index()
bootstrap_summary_v8.columns = ["metrica","mean","std","p2_5","p50","p97_5"]
display(bootstrap_summary_v8)
bootstrap_summary_v8.to_csv(TABLE_DIR_V8 / "V8_0_bootstrap_100_metricas_campeon.csv", index=False)
# Estabilidad temporal por temporada si existe.
temporal_rows = []
try:
temporada_test = split_df.loc[X_test.index, "temporada"] if "temporada" in split_df.columns else None
except Exception:
temporada_test = model_df.loc[X_test.index, "temporada"] if "temporada" in model_df.columns else None
if temporada_test is not None:
temp_s = pd.Series(temporada_test).reset_index(drop=True).astype(str)
for season in sorted(temp_s.dropna().unique()):
mask = temp_s.eq(season).to_numpy()
if mask.sum() >= 10 and len(np.unique(y_test_arr[mask])) >= 2:
met = metrics_from_scores(y_test_arr[mask], p_test_arr[mask], champion_threshold)
met["temporada"] = season
met["n"] = int(mask.sum())
temporal_rows.append(met)
temporal_stability_v8 = pd.DataFrame(temporal_rows)
else:
temporal_stability_v8 = pd.DataFrame()
if not temporal_stability_v8.empty:
display(temporal_stability_v8[["temporada","n","TP","FP","FN","TN","roc_auc","pr_auc","recall_sensitivity","precision_ppv","f1_score","f2_score","beneficio_neto_usd"]])
temporal_stability_v8.to_csv(TABLE_DIR_V8 / "V8_0_estabilidad_temporal_por_temporada.csv", index=False)
else:
print("No hay suficientes temporadas en test para estabilidad temporal desagregada.")
# Sensibilidad a selección de variables: remover top variables y re-evaluar score del campeón no es correcto sin re-entrenar.
# Aquí se genera una sensibilidad pragmática vía permutation already computed + escenarios de exclusión top variables entrenando una familia rápida.
sens_var_rows = []
try:
top_vars_sens = imp.head(5)["variable"].dropna().tolist()
base_family = champion_family
for remove_var in top_vars_sens:
vars_sens = [v for v in FEATURES_V8_BASE if v != remove_var]
num_sens = [v for v in num_features_v8 if v in vars_sens]
cat_sens = [v for v in cat_features_v8 if v in vars_sens]
pre_sens = make_preprocessor(num_sens, cat_sens, scale_numeric=False)
# Modelo rápido RandomForest como stress test homogéneo si campeón no es fácilmente clonado por columnas.
sens_model = Pipeline([("pre", pre_sens), ("clf", RandomForestClassifier(n_estimators=300, max_depth=8, min_samples_leaf=5, class_weight="balanced", random_state=RANDOM_STATE, n_jobs=-1))])
sens_model.fit(X_train[vars_sens], y_train)
p_val_s = sens_model.predict_proba(X_val[vars_sens])[:,1]
t_s, _ = best_threshold_by_validation(y_val, p_val_s, THRESHOLD_GRID_V8)
p_test_s = sens_model.predict_proba(X_test[vars_sens])[:,1]
met = metrics_from_scores(y_test, p_test_s, t_s)
met.update({"variable_excluida": remove_var, "n_variables": len(vars_sens), "threshold": t_s})
sens_var_rows.append(met)
except Exception as e:
print("[WARNING] Sensibilidad a selección de variables no completada:", e)
sensibilidad_variables_v8 = pd.DataFrame(sens_var_rows)
if not sensibilidad_variables_v8.empty:
display(sensibilidad_variables_v8[["variable_excluida","n_variables","threshold","roc_auc","pr_auc","recall_sensitivity","precision_ppv","f1_score","f2_score","beneficio_neto_usd"]])
sensibilidad_variables_v8.to_csv(TABLE_DIR_V8 / "V8_0_sensibilidad_seleccion_variables.csv", index=False)
# Sensibilidad threshold ya existe en th_champ; se resume mejor threshold test solo para diagnóstico, no selección.
best_threshold_test_diag = th_champ.sort_values(["beneficio_neto_usd","recall_sensitivity","f2_score"], ascending=False).iloc[0]
print("Mejor threshold en test SOLO diagnóstico, no selección:")
display(pd.DataFrame([best_threshold_test_diag]))
# Gráfico bootstrap beneficio.
plt.figure(figsize=(8,5))
plt.hist(bootstrap_v8["beneficio_neto_usd"].dropna(), bins=20)
plt.axvline(bootstrap_v8["beneficio_neto_usd"].mean(), linestyle="--")
plt.title("Bootstrap 100 — Distribución beneficio campeón V8.0")
plt.xlabel("Beneficio neto USD")
plt.ylabel("Frecuencia")
savefig_v8("V8_0_bootstrap_beneficio_campeon.png")
==================================================================================================== 19 — ROBUSTEZ V8.0 ====================================================================================================
| metrica | mean | std | p2_5 | p50 | p97_5 | |
|---|---|---|---|---|---|---|
| 0 | roc_auc | 0.688141 | 0.044719 | 0.595985 | 0.689451 | 0.768850 |
| 1 | pr_auc | 0.038635 | 0.014324 | 0.016633 | 0.035441 | 0.072062 |
| 2 | recall_sensitivity | 0.389792 | 0.082242 | 0.226282 | 0.389977 | 0.538667 |
| 3 | precision_ppv | 0.036780 | 0.010924 | 0.017371 | 0.036364 | 0.058469 |
| 4 | f1_score | 0.066991 | 0.019103 | 0.032633 | 0.066293 | 0.104233 |
| 5 | f2_score | 0.132374 | 0.034661 | 0.069020 | 0.133321 | 0.197904 |
| 6 | mcc | 0.081852 | 0.028513 | 0.028965 | 0.081952 | 0.134080 |
| 7 | brier_score | 0.013365 | 0.002737 | 0.008463 | 0.013087 | 0.019075 |
| 8 | TP | 11.190000 | 3.413269 | 5.475000 | 11.000000 | 18.000000 |
| 9 | FP | 292.960000 | 16.460494 | 265.000000 | 291.000000 | 322.525000 |
| 10 | FN | 17.430000 | 4.252759 | 10.475000 | 17.000000 | 27.000000 |
| 11 | TN | 1792.420000 | 17.047278 | 1763.475000 | 1792.500000 | 1826.150000 |
| 12 | beneficio_neto_usd | 231960.000000 | 206443.203724 | -135150.000000 | 222000.000000 | 629700.000000 |
| temporada | n | TP | FP | FN | TN | roc_auc | pr_auc | recall_sensitivity | precision_ppv | f1_score | f2_score | beneficio_neto_usd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2024_2025 | 2114 | 11 | 290 | 18 | 1795 | 0.681948 | 0.034352 | 0.37931 | 0.036545 | 0.066667 | 0.131894 | 225000.0 |
| variable_excluida | n_variables | threshold | roc_auc | pr_auc | recall_sensitivity | precision_ppv | f1_score | f2_score | beneficio_neto_usd | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | macro_mercado | 17 | 0.4 | 0.626230 | 0.027704 | 0.241379 | 0.046053 | 0.077348 | 0.130597 | 202500.0 |
| 1 | firmeza_pulpa_lb | 17 | 0.4 | 0.644720 | 0.035232 | 0.379310 | 0.025287 | 0.047414 | 0.099819 | 24000.0 |
| 2 | materia_seca_pct | 17 | 0.4 | 0.660432 | 0.035250 | 0.448276 | 0.025391 | 0.048059 | 0.103503 | 31500.0 |
| 3 | quiebre_cadena_frio_h | 17 | 0.4 | 0.676871 | 0.032034 | 0.482759 | 0.034230 | 0.063927 | 0.133333 | 247500.0 |
| 4 | naviera | 17 | 0.5 | 0.662615 | 0.033354 | 0.137931 | 0.061538 | 0.085106 | 0.110497 | 148500.0 |
Mejor threshold en test SOLO diagnóstico, no selección:
| threshold | TN | FP | FN | TP | accuracy | precision_ppv | recall_sensitivity | specificity | f1_score | f2_score | balanced_accuracy | mcc | beneficio_neto_usd | brier_score | roc_auc | pr_auc | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 4 | 0.015 | 1795.0 | 290.0 | 18.0 | 11.0 | 0.854305 | 0.036545 | 0.37931 | 0.860911 | 0.066667 | 0.131894 | 0.620111 | 0.079962 | 225000.0 | 0.013536 | 0.681948 | 0.034352 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_bootstrap_beneficio_campeon.png
20_V8_Random_Forest_Causal¶
Modelo Random Forest Causal con fórmula AIPW / Double Robust. Este bloque estima efecto causal promedio (ATE) y heterogeneidad individual aproximada (CATE) para un tratamiento operacional pre-despacho definido automáticamente o ajustable por el usuario.
# ======================================================================================
# 20. RANDOM FOREST CAUSAL — MÓDULO ROBUSTO AIPW / DOUBLE ROBUST
# ======================================================================================
print_step("20 — RANDOM FOREST CAUSAL V8.0 — AIPW / DOUBLE ROBUST — MÓDULO ROBUSTO")
# Fórmula AIPW / Double Robust:
# ATE = mean( mu1(X) - mu0(X) + T*(Y-mu1(X))/e(X) - (1-T)*(Y-mu0(X))/(1-e(X)) )
# donde:
# T = tratamiento operacional binario
# Y = target reclamo_comercial
# e(X) = propensity score P(T=1|X)
# mu1(X), mu0(X) = probabilidad esperada de reclamo bajo tratamiento/control
possible_treatments = [
"atmosfera_controlada",
"quiebre_cadena_frio_flag",
"quiebre_cadena_frio_h",
"tipo_contenedor",
"macro_mercado",
"naviera",
"linea_packing",
"operador_packing",
"cliente_tipo"
]
TREATMENT_VAR = None
for c in possible_treatments:
if c in model_df.columns:
TREATMENT_VAR = c
break
if TREATMENT_VAR is None:
print("No se encontró tratamiento operacional candidato para RF causal. Define TREATMENT_VAR manualmente.")
causal_results_v8 = pd.DataFrame()
else:
print(f"Tratamiento causal seleccionado: {TREATMENT_VAR}")
idx_causal = X_train.index.union(X_val.index).union(X_test.index)
causal_df = model_df.loc[idx_causal].copy()
causal_df = causal_df.loc[:, ~pd.Index(causal_df.columns).duplicated(keep="first")].copy()
causal_df[TARGET] = pd.to_numeric(causal_df[TARGET], errors="coerce").fillna(0).astype(int)
sT = causal_df[TREATMENT_VAR]
if pd.api.types.is_numeric_dtype(sT):
vals = pd.to_numeric(sT, errors="coerce")
if vals.nunique(dropna=True) <= 2:
T = vals.fillna(0).astype(int).clip(0, 1)
treatment_rule = f"{TREATMENT_VAR} binario original"
else:
q75 = vals.quantile(0.75)
T = (vals >= q75).astype(int)
treatment_rule = f"{TREATMENT_VAR} >= p75 ({q75:.3f})"
else:
mode_series = sT.dropna().astype(str).mode()
mode_val = mode_series.iloc[0] if len(mode_series) else "SIN_CATEGORIA"
T = (sT.astype(str) == mode_val).astype(int)
treatment_rule = f"{TREATMENT_VAR} == categoría modal '{mode_val}'"
causal_df["T_causal"] = T.values
candidate_features = []
for col in list(dict.fromkeys(FEATURES_V8_BASE + valid_vars)):
if col in causal_df.columns and col not in [TREATMENT_VAR, TARGET, "T_causal"]:
candidate_features.append(col)
causal_features = candidate_features.copy()
X_causal = causal_df[causal_features].copy()
y_causal = causal_df[TARGET].astype(int).copy()
T_causal = causal_df["T_causal"].astype(int).copy()
num_causal = [v for v in num_features_v8 if v in causal_features]
cat_causal = [v for v in cat_features_v8 if v in causal_features]
print(f"Registros causal: {len(causal_df):,}")
print(f"Tratados: {int(T_causal.sum()):,} | Controles: {int((1-T_causal).sum()):,}")
print(f"Features causales: {len(causal_features)} | Numéricas: {len(num_causal)} | Categóricas: {len(cat_causal)}")
print(f"Regla tratamiento: {treatment_rule}")
causal_preprocessor = make_preprocessor(num_causal, cat_causal, scale_numeric=False)
X_causal_proc = causal_preprocessor.fit_transform(X_causal)
mask0 = T_causal.eq(0).to_numpy()
mask1 = T_causal.eq(1).to_numpy()
Y = y_causal.to_numpy().astype(int)
TT = T_causal.to_numpy().astype(int)
def positive_proba_estimator(model, X_input):
proba = model.predict_proba(X_input)
classes = list(getattr(model, "classes_", []))
if 1 in classes:
return proba[:, classes.index(1)]
if len(classes) == 1 and classes[0] == 1:
return np.ones(X_input.shape[0])
return np.zeros(X_input.shape[0])
def rf_causal_model(seed, min_leaf=20):
return RandomForestClassifier(
n_estimators=500,
max_depth=6,
min_samples_leaf=min_leaf,
min_samples_split=max(2, min_leaf),
max_features="sqrt",
class_weight="balanced",
random_state=seed,
n_jobs=-1
)
soporte_ok = True
mensajes_soporte = []
if mask0.sum() < 30:
soporte_ok = False
mensajes_soporte.append("menos de 30 controles")
if mask1.sum() < 30:
soporte_ok = False
mensajes_soporte.append("menos de 30 tratados")
if len(np.unique(Y[mask0])) < 2:
soporte_ok = False
mensajes_soporte.append("outcome control sin ambas clases")
if len(np.unique(Y[mask1])) < 2:
soporte_ok = False
mensajes_soporte.append("outcome tratado sin ambas clases")
if not soporte_ok:
print("Tratamiento no tiene soporte suficiente para estimación causal robusta:", "; ".join(mensajes_soporte))
causal_results_v8 = pd.DataFrame({
"treatment_var": [TREATMENT_VAR],
"treatment_rule": [treatment_rule],
"n_control": [int(mask0.sum())],
"n_treated": [int(mask1.sum())],
"ATE_AIPW": [np.nan],
"estado": ["SIN_SOPORTE_CAUSAL_SUFFICIENTE"]
})
else:
propensity = rf_causal_model(RANDOM_STATE, min_leaf=25)
outcome0 = rf_causal_model(RANDOM_STATE + 1, min_leaf=15)
outcome1 = rf_causal_model(RANDOM_STATE + 2, min_leaf=15)
propensity.fit(X_causal_proc, TT)
outcome0.fit(X_causal_proc[mask0], Y[mask0])
outcome1.fit(X_causal_proc[mask1], Y[mask1])
e_hat_raw = positive_proba_estimator(propensity, X_causal_proc)
e_hat = np.clip(e_hat_raw, 0.02, 0.98)
mu0 = np.clip(positive_proba_estimator(outcome0, X_causal_proc), 0.001, 0.999)
mu1 = np.clip(positive_proba_estimator(outcome1, X_causal_proc), 0.001, 0.999)
aipw_scores = (
(mu1 - mu0)
+ TT * (Y - mu1) / e_hat
- (1 - TT) * (Y - mu0) / (1 - e_hat)
)
ate = float(np.mean(aipw_scores))
cate = mu1 - mu0
ate_boot = []
rng = np.random.default_rng(RANDOM_STATE)
for b in range(200):
idx = rng.integers(0, len(aipw_scores), len(aipw_scores))
ate_boot.append(float(np.mean(aipw_scores[idx])))
ci_low, ci_high = np.quantile(ate_boot, [0.025, 0.975])
positivity_df = pd.DataFrame({
"propensity_score": e_hat,
"T_causal": TT,
TARGET: Y
})
positivity_summary = positivity_df.groupby("T_causal")["propensity_score"].agg(
["count", "mean", "std", "min", "median", "max"]
).reset_index()
causal_results_v8 = pd.DataFrame({
"treatment_var": [TREATMENT_VAR],
"treatment_rule": [treatment_rule],
"n_control": [int(mask0.sum())],
"n_treated": [int(mask1.sum())],
"ATE_AIPW": [ate],
"ATE_IC95_low": [float(ci_low)],
"ATE_IC95_high": [float(ci_high)],
"ATE_puntos_porcentuales": [ate * 100],
"propensity_mean": [float(np.mean(e_hat))],
"propensity_p01": [float(np.quantile(e_hat, 0.01))],
"propensity_p99": [float(np.quantile(e_hat, 0.99))],
"estado": ["OK_CAUSAL_EXPLORATORIO"],
"interpretacion": ["Efecto promedio estimado sobre probabilidad de reclamo; válido como evidencia causal exploratoria bajo ignorabilidad, positividad y SUTVA."]
})
cate_df = causal_df[[TARGET, "T_causal"]].copy()
cate_df["CATE_RF_aprox"] = cate
cate_df["propensity_score"] = e_hat
cate_df["mu0"] = mu0
cate_df["mu1"] = mu1
cate_df.to_csv(TABLE_DIR_V8 / "V8_0_random_forest_causal_cate_scores.csv", index=False)
positivity_summary.to_csv(TABLE_DIR_V8 / "V8_0_random_forest_causal_positividad.csv", index=False)
try:
feature_names_causal = causal_preprocessor.get_feature_names_out()
imp_prop = pd.DataFrame({
"feature": feature_names_causal,
"importance_propensity": propensity.feature_importances_
}).sort_values("importance_propensity", ascending=False)
imp_prop.to_csv(TABLE_DIR_V8 / "V8_0_random_forest_causal_importancia_propensity.csv", index=False)
display(imp_prop.head(20))
except Exception as e_imp:
print("No fue posible extraer importancia causal:", e_imp)
plt.figure(figsize=(8, 5))
plt.hist(cate, bins=30)
plt.axvline(ate, linestyle="--")
plt.title(f"Distribución CATE aproximado — RF Causal — {TREATMENT_VAR}")
plt.xlabel("mu1(X) - mu0(X)")
plt.ylabel("Frecuencia")
savefig_v8("V8_0_random_forest_causal_cate_distribucion.png")
plt.figure(figsize=(8, 5))
plt.hist(e_hat[TT == 0], bins=30, alpha=0.6, label="Control")
plt.hist(e_hat[TT == 1], bins=30, alpha=0.6, label="Tratado")
plt.title(f"Diagnóstico de positividad — Propensity Score — {TREATMENT_VAR}")
plt.xlabel("e(X) = P(T=1|X)")
plt.ylabel("Frecuencia")
plt.legend()
savefig_v8("V8_0_random_forest_causal_positividad.png")
display(positivity_summary)
display(causal_results_v8)
causal_results_v8.to_csv(TABLE_DIR_V8 / "V8_0_random_forest_causal_AIPW_ATE.csv", index=False)
print("Explicación metodológica:")
print("AIPW/Double Robust combina propensity score y modelos outcome. El estimador es más robusto que comparar promedios simples, siempre que se cumplan ignorabilidad, positividad y SUTVA.")
print("Lectura operacional:")
print("Este módulo permite evaluar tratamientos operacionales como atmósfera controlada, tipo de contenedor, naviera o quiebre de frío. Debe interpretarse como causalidad observacional exploratoria, no como experimento aleatorizado.")
print("Recomendación ML:")
print("Usar este resultado como evidencia complementaria al modelo predictivo champion. Si el ATE tiene IC95% que cruza cero, no afirmar efecto causal concluyente.")
==================================================================================================== 20 — RANDOM FOREST CAUSAL V8.0 — AIPW / DOUBLE ROBUST — MÓDULO ROBUSTO ==================================================================================================== Tratamiento causal seleccionado: atmosfera_controlada Registros causal: 14,736 Tratados: 11,044 | Controles: 3,692 Features causales: 32 | Numéricas: 7 | Categóricas: 10 Regla tratamiento: atmosfera_controlada == categoría modal 'no'
| feature | importance_propensity | |
|---|---|---|
| 8 | cat__macro_mercado_EU | 0.334034 |
| 10 | cat__macro_mercado_NA | 0.122632 |
| 23 | cat__tipo_contenedor_camion_frio | 0.074713 |
| 37 | cat__naviera_N/A | 0.069187 |
| 30 | cat__puerto_salida_N/A | 0.065111 |
| 7 | cat__macro_mercado_DOM | 0.065004 |
| 48 | cat__canal_destino_mercado_local | 0.061486 |
| 6 | num__temperatura_setpoint_frio_c | 0.054042 |
| 9 | cat__macro_mercado_LATAM | 0.045600 |
| 47 | cat__canal_destino_exportacion | 0.041556 |
| 25 | cat__tipo_contenedor_reefer_40 | 0.017274 |
| 5 | num__quiebre_cadena_frio_h | 0.011781 |
| 31 | cat__puerto_salida_San Antonio | 0.007814 |
| 32 | cat__puerto_salida_Valparaiso | 0.004189 |
| 1 | num__materia_seca_pct | 0.003663 |
| 3 | num__firmeza_pulpa_lb | 0.003588 |
| 0 | num__ph_suelo | 0.002933 |
| 2 | num__edad_arboles_anos | 0.002262 |
| 4 | num__densidad_arboles_ha | 0.001049 |
| 34 | cat__naviera_Hapag-Lloyd | 0.001033 |
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_random_forest_causal_cate_distribucion.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_random_forest_causal_positividad.png
| T_causal | count | mean | std | min | median | max | |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 3692 | 0.367835 | 0.011440 | 0.336638 | 0.367073 | 0.456798 |
| 1 | 1 | 11044 | 0.629822 | 0.284072 | 0.340604 | 0.387970 | 0.980000 |
| treatment_var | treatment_rule | n_control | n_treated | ATE_AIPW | ATE_IC95_low | ATE_IC95_high | ATE_puntos_porcentuales | propensity_mean | propensity_p01 | propensity_p99 | estado | interpretacion | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | atmosfera_controlada | atmosfera_controlada == categoría modal 'no' | 3692 | 11044 | -0.343529 | -0.35375 | -0.332983 | -34.352892 | 0.564183 | 0.349541 | 0.98 | OK_CAUSAL_EXPLORATORIO | Efecto promedio estimado sobre probabilidad de... |
Explicación metodológica: AIPW/Double Robust combina propensity score y modelos outcome. El estimador es más robusto que comparar promedios simples, siempre que se cumplan ignorabilidad, positividad y SUTVA. Lectura operacional: Este módulo permite evaluar tratamientos operacionales como atmósfera controlada, tipo de contenedor, naviera o quiebre de frío. Debe interpretarse como causalidad observacional exploratoria, no como experimento aleatorizado. Recomendación ML: Usar este resultado como evidencia complementaria al modelo predictivo champion. Si el ATE tiene IC95% que cruza cero, no afirmar efecto causal concluyente.
21_V8_QA_Defensa_Extendido¶
Genera más de 100 preguntas y respuestas para defensa: técnicas, negocio, estadística, causalidad y despliegue, usando los resultados reales del notebook cuando están disponibles.
# ======================================================================================
# 21. QA DEFENSA EXTENDIDO — MÁS DE 100 PREGUNTAS
# ======================================================================================
print_step("21 — QA DEFENSA EXTENDIDO V8.0")
# Extraer métricas reales del campeón para insertar en respuestas.
ct = champion_test_metrics
cv = champion_val_metrics
best_topk_champ = topk_champion.sort_values(["beneficio_neto_usd","recall_sensitivity"], ascending=False).iloc[0].to_dict()
qa_sections = []
def add_q(section, q, a):
qa_sections.append({"seccion": section, "pregunta": q, "respuesta": a})
# 50 técnicas.
for i in range(1, 51):
if i == 1:
q = "¿Por qué V8.0 parte desde V6.0 y no desde cero?"
a = "Porque V6.0 ya entregó una base parsimoniosa e interpretable. V8.0 usa ese universo reducido como punto de partida y agrega modelos no lineales para capturar interacciones y efectos complejos."
elif i == 2:
q = "¿Cuál fue el modelo campeón y cómo se seleccionó?"
a = f"El campeón fue {champion_family} ({champion_id}), seleccionado en validación temporal por beneficio económico, PR-AUC, Recall, F2 y F1; no fue elegido mirando el test."
elif i == 3:
q = "¿Por qué no se usa Accuracy como criterio principal?"
a = f"Porque el evento positivo es raro. En test, la tasa base de reclamo es {float(np.mean(y_test)):.4f}; con clases desbalanceadas, Accuracy puede ser alta aunque el modelo no detecte reclamos."
elif i == 4:
q = "¿Qué aporta XGBoost/LightGBM frente a regresión logística?"
a = "Capturan no linealidades e interacciones automáticamente, algo relevante cuando el riesgo depende de combinaciones de mercado, packing, madurez, clima y logística."
elif i == 5:
q = "¿Por qué se optimizan hiperparámetros?"
a = "Porque usar defaults puede subajustar o sobreajustar. Se exploran profundidad, cantidad de árboles, learning rate, class_weight y restricciones de hoja para balancear desempeño y generalización."
else:
q = f"Pregunta técnica {i}: ¿cómo se controla la validez del experimento ML?"
a = "Se usa split temporal train/validación/test, selección de modelo en validación, evaluación final en test, métricas para eventos raros, curvas de negocio y comparación contra benchmark logístico."
add_q("Técnica", q, a)
# 20 negocio.
for i in range(1, 21):
if i == 1:
q = "¿Cómo se traduce el modelo a operación?"
a = f"El modelo genera un ranking de riesgo. Con Top-K, por ejemplo, se revisan los {int(best_topk_champ['top_k'])} lotes más riesgosos, capturando TP={int(best_topk_champ['TP'])} reclamos con beneficio estimado USD {best_topk_champ['beneficio_neto_usd']:,.0f}."
elif i == 2:
q = "¿Debe bloquear automáticamente contenedores?"
a = "No necesariamente. Para eventos raros es más prudente usarlo como priorizador de revisión, especialmente si la precisión es baja o el costo de falsa alarma afecta la operación."
else:
q = f"Pregunta de negocio {i}: ¿cómo se justifica el beneficio económico?"
a = f"Se calcula desde la matriz de confusión: TP aporta beneficio de evitar reclamos y FP genera costo de revisión/bloqueo. En test el campeón con threshold validado obtuvo beneficio USD {ct['beneficio_neto_usd']:,.0f}."
add_q("Negocio", q, a)
# 20 estadísticas.
for i in range(1, 21):
if i == 1:
q = "¿Qué métrica es más relevante: ROC-AUC o PR-AUC?"
a = f"Ambas son útiles, pero PR-AUC es más exigente en eventos raros. El campeón en test obtuvo ROC-AUC={ct['roc_auc']:.4f} y PR-AUC={ct['pr_auc']:.4f}."
elif i == 2:
q = "¿Qué aporta bootstrap?"
a = "Entrega intervalos empíricos de incertidumbre para ROC, PR, Recall, F1, F2 y beneficio, evitando depender de una única partición."
else:
q = f"Pregunta estadística {i}: ¿cómo se evalúa estabilidad?"
a = "Se revisa desempeño por bootstrap, por temporada cuando existe soporte, sensibilidad al threshold y sensibilidad al retiro de variables importantes."
add_q("Estadística", q, a)
# 10 causalidad.
for i in range(1, 11):
if i == 1:
q = "¿Random Forest Causal prueba causalidad definitiva?"
a = "No. Estima efectos bajo supuestos: ignorabilidad condicional, positividad y SUTVA. Es más fuerte que una correlación predictiva, pero no reemplaza un experimento aleatorizado."
elif i == 2:
q = "¿Cuál es la fórmula usada?"
a = "AIPW: mean(mu1(X)-mu0(X)+T(Y-mu1(X))/e(X)-(1-T)(Y-mu0(X))/(1-e(X))). Combina propensity score y modelos outcome."
else:
q = f"Pregunta de causalidad {i}: ¿cómo se interpreta ATE/CATE?"
a = "ATE es el efecto promedio estimado del tratamiento; CATE permite ver heterogeneidad por lote. Ambos deben interpretarse como evidencia observacional condicionada."
add_q("Causalidad", q, a)
# 10 despliegue.
for i in range(1, 11):
if i == 1:
q = "¿Cómo se desplegaría el modelo?"
a = "Como scoring batch pre-despacho: se calcula riesgo por lote/contenedor, se ordena Top-K y se entrega una lista de revisión a calidad/operaciones."
elif i == 2:
q = "¿Qué monitorear en producción?"
a = "Drift de variables, tasa de reclamos, recall operacional, precisión de alertas, calibración, beneficio real y estabilidad por temporada/mercado/cliente."
else:
q = f"Pregunta de despliegue {i}: ¿qué controles requiere?"
a = "Control de versionado, logging de scores, monitoreo de drift, revisión humana, umbral aprobado por negocio y reentrenamiento por temporada."
add_q("Despliegue", q, a)
qa_v8 = pd.DataFrame(qa_sections)
display(qa_v8.head(20))
print("Total preguntas QA:", len(qa_v8))
qa_v8.to_csv(TABLE_DIR_V8 / "V8_0_QA_defensa_extendido_110_preguntas.csv", index=False)
# Guardar markdown completo.
qa_md = "# QA Defensa Extendido — V8.0 Final\n\n"
for section, grp in qa_v8.groupby("seccion", sort=False):
qa_md += f"\n## {section}\n\n"
for j, r in grp.reset_index(drop=True).iterrows():
qa_md += f"### {j+1}. {r['pregunta']}\n{r['respuesta']}\n\n"
(OUTPUT_DIR_V8 / "V8_0_QA_defensa_extendido.md").write_text(qa_md, encoding="utf-8")
print("QA guardado en:", OUTPUT_DIR_V8 / "V8_0_QA_defensa_extendido.md")
==================================================================================================== 21 — QA DEFENSA EXTENDIDO V8.0 ====================================================================================================
| seccion | pregunta | respuesta | |
|---|---|---|---|
| 0 | Técnica | ¿Por qué V8.0 parte desde V6.0 y no desde cero? | Porque V6.0 ya entregó una base parsimoniosa e... |
| 1 | Técnica | ¿Cuál fue el modelo campeón y cómo se seleccionó? | El campeón fue XGBoost (V8_0225_XGBoost), sele... |
| 2 | Técnica | ¿Por qué no se usa Accuracy como criterio prin... | Porque el evento positivo es raro. En test, la... |
| 3 | Técnica | ¿Qué aporta XGBoost/LightGBM frente a regresió... | Capturan no linealidades e interacciones autom... |
| 4 | Técnica | ¿Por qué se optimizan hiperparámetros? | Porque usar defaults puede subajustar o sobrea... |
| 5 | Técnica | Pregunta técnica 6: ¿cómo se controla la valid... | Se usa split temporal train/validación/test, s... |
| 6 | Técnica | Pregunta técnica 7: ¿cómo se controla la valid... | Se usa split temporal train/validación/test, s... |
| 7 | Técnica | Pregunta técnica 8: ¿cómo se controla la valid... | Se usa split temporal train/validación/test, s... |
| 8 | Técnica | Pregunta técnica 9: ¿cómo se controla la valid... | Se usa split temporal train/validación/test, s... |
| 9 | Técnica | Pregunta técnica 10: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 10 | Técnica | Pregunta técnica 11: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 11 | Técnica | Pregunta técnica 12: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 12 | Técnica | Pregunta técnica 13: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 13 | Técnica | Pregunta técnica 14: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 14 | Técnica | Pregunta técnica 15: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 15 | Técnica | Pregunta técnica 16: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 16 | Técnica | Pregunta técnica 17: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 17 | Técnica | Pregunta técnica 18: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 18 | Técnica | Pregunta técnica 19: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
| 19 | Técnica | Pregunta técnica 20: ¿cómo se controla la vali... | Se usa split temporal train/validación/test, s... |
Total preguntas QA: 110 QA guardado en: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/V8_0_QA_defensa_extendido.md
# ======================================================================================
# 21. QA DEFENSA EXTENDIDO V8.0 — MANUAL PROFESIONAL DE DEFENSA
# ======================================================================================
print_step("21 — QA DEFENSA EXTENDIDO V8.0 — MANUAL PROFESIONAL")
qa_defensa = []
def add_qa(seccion, pregunta, respuesta):
qa_defensa.append({
"seccion": seccion,
"pregunta": pregunta,
"respuesta": respuesta
})
# ============================================================
# 21.1 PROBLEMA DE NEGOCIO
# ============================================================
add_qa(
"Problema de negocio",
"¿Cuál es el problema de negocio que resuelve este Capstone?",
"""
El problema de negocio consiste en anticipar, antes del despacho, qué lotes o contenedores tienen mayor probabilidad de generar un reclamo comercial en destino.
En la operación real, un reclamo puede representar pérdidas económicas relevantes, afectar la relación con clientes internacionales y generar costos adicionales por notas de crédito, reprocesos, inspecciones y pérdida reputacional.
El modelo no busca reemplazar la decisión humana, sino priorizar los embarques de mayor riesgo para revisión preventiva.
"""
)
add_qa(
"Problema de negocio",
"¿Por qué este problema se formuló como clasificación binaria?",
"""
Porque la variable objetivo `reclamo_comercial` tiene dos posibles estados:
0 = No hubo reclamo comercial.
1 = Hubo reclamo comercial.
El objetivo del modelo es estimar la probabilidad:
P(reclamo_comercial = 1 | variables pre-despacho)
Esto permite ordenar lotes/contenedores por riesgo y tomar decisiones preventivas.
"""
)
add_qa(
"Problema de negocio",
"¿Por qué no basta con mirar históricos de reclamos?",
"""
Porque los históricos permiten describir lo que ocurrió, pero no necesariamente anticipar nuevos casos.
El modelo utiliza variables productivas, climáticas, de packing, logística, mercado y cliente para estimar riesgo futuro.
La ventaja del enfoque ML es combinar múltiples señales débiles que individualmente podrían no ser concluyentes, pero en conjunto permiten priorizar mejor.
"""
)
add_qa(
"Problema de negocio",
"¿Cuál es la unidad de decisión operacional?",
"""
La unidad operacional relevante es el lote/contenedor antes del despacho.
Aunque existen variables a nivel de lote, packing o cliente, la decisión práctica es si un embarque debe ser revisado, priorizado, liberado o eventualmente bloqueado.
Por eso el modelo se interpreta como herramienta de priorización pre-despacho.
"""
)
add_qa(
"Problema de negocio",
"¿Qué significa que el modelo sea un priorizador y no un decisor automático?",
"""
Significa que el modelo no decide por sí solo cancelar o aprobar un embarque.
El modelo entrega un ranking de riesgo. Luego el equipo operacional, calidad o comercial usa ese ranking para decidir qué lotes revisar primero.
Esto es importante porque el costo de un falso positivo y un falso negativo no es simétrico y porque la decisión final requiere criterio experto.
"""
)
# ============================================================
# 21.2 DATOS Y VALIDACIÓN
# ============================================================
add_qa(
"Datos y validación",
"¿Por qué se usó split temporal?",
"""
Se usó split temporal porque el objetivo real es predecir temporadas futuras usando información histórica.
Un split aleatorio podría mezclar registros de la misma temporada entre train y test, generando una evaluación demasiado optimista.
El split temporal simula mejor producción:
Train: temporadas históricas.
Validación: temporada posterior para seleccionar modelo y umbral.
Test: temporada futura para evaluación final.
"""
)
add_qa(
"Datos y validación",
"¿Qué es leakage y cómo se evitó?",
"""
Leakage ocurre cuando el modelo utiliza información que no estaría disponible al momento de tomar la decisión.
Por ejemplo, variables medidas en destino después del arribo no deberían usarse para predecir riesgo pre-despacho.
Se evitó utilizando sólo variables disponibles antes del despacho o justificables operacionalmente en ese momento.
"""
)
add_qa(
"Datos y validación",
"¿Por qué la tasa base de reclamo es importante?",
"""
Porque el reclamo comercial es un evento raro.
Cuando la clase positiva es poco frecuente, métricas como Accuracy pueden ser engañosas.
Un modelo que predice siempre 'sin reclamo' podría tener Accuracy muy alta, pero utilidad operacional nula.
Por eso se priorizan PR-AUC, Recall, F1, F2, matriz de confusión, beneficio económico y Top-K.
"""
)
# ============================================================
# 21.3 FEATURE ENGINEERING
# ============================================================
add_qa(
"Feature Engineering",
"¿Por qué se partió desde la V6.0?",
"""
La V6.0 entregó una base parsimoniosa e interpretable, reduciendo el universo de variables respecto de versiones anteriores.
Partir desde V6.0 permite conservar variables con sentido estadístico y operacional, evitando entrenar modelos complejos sobre un universo excesivamente amplio.
V8.0 toma esa base y agrega modelos no lineales de alto desempeño.
"""
)
add_qa(
"Feature Engineering",
"¿Por qué reducir variables?",
"""
Reducir variables mejora interpretabilidad, estabilidad y defendibilidad.
Con pocos eventos positivos, usar demasiadas variables aumenta el riesgo de sobreajuste.
Una versión más parsimoniosa permite explicar mejor por qué el modelo toma decisiones y facilita su implementación operacional.
"""
)
add_qa(
"Feature Engineering",
"¿Qué aporta LASSO?",
"""
LASSO usa regularización L1, que puede llevar algunos coeficientes exactamente a cero.
Esto permite selección automática de variables.
Su objetivo no es sólo mejorar desempeño, sino identificar un subconjunto más estable y explicable de predictores.
"""
)
add_qa(
"Feature Engineering",
"¿Qué aporta VIF?",
"""
VIF significa Variance Inflation Factor.
Mide cuánto aumenta la varianza de un coeficiente debido a multicolinealidad con otras variables.
Un VIF cercano a 1 indica baja colinealidad.
Valores superiores a 5 sugieren revisar la variable.
Valores superiores a 10 suelen considerarse problemáticos.
En este proyecto se usa para controlar estabilidad en el benchmark logístico.
"""
)
# ============================================================
# 21.4 MODELOS
# ============================================================
add_qa(
"Modelos",
"¿Por qué se mantiene la Regresión Logística si V8.0 usa modelos más complejos?",
"""
La Regresión Logística se mantiene como benchmark interpretable.
Permite explicar efectos mediante coeficientes, Odds Ratios y dirección del impacto de las variables.
Los modelos no lineales pueden mejorar desempeño, pero son menos transparentes.
Por eso el enfoque profesional es usar:
1. Regresión Logística como benchmark explicativo.
2. Modelos challenger como Random Forest, XGBoost y LightGBM para alto desempeño.
"""
)
add_qa(
"Modelos",
"¿Qué aporta Random Forest?",
"""
Random Forest combina múltiples árboles de decisión entrenados sobre muestras y subconjuntos de variables.
Aporta robustez, captura interacciones no lineales y reduce varianza respecto de un árbol individual.
Es útil cuando existen relaciones complejas entre madurez, clima, packing, mercado y logística.
"""
)
add_qa(
"Modelos",
"¿Qué aporta XGBoost?",
"""
XGBoost es un algoritmo de Gradient Boosting optimizado.
Entrena árboles secuencialmente, donde cada nuevo árbol intenta corregir errores del conjunto anterior.
Suele tener alto desempeño en datos tabulares, especialmente cuando existen interacciones, no linealidades y clases desbalanceadas.
"""
)
add_qa(
"Modelos",
"¿Qué aporta LightGBM?",
"""
LightGBM es otro algoritmo de Gradient Boosting diseñado para eficiencia y alto rendimiento.
Puede manejar muchas variables y grandes volúmenes de datos con rapidez.
Es adecuado para comparar contra XGBoost y Random Forest dentro de un laboratorio challenger.
"""
)
add_qa(
"Modelos",
"¿Por qué no basta con usar parámetros por defecto?",
"""
Los parámetros por defecto no necesariamente son adecuados para el problema específico.
En eventos raros, hiperparámetros como profundidad, número de árboles, learning rate, min_samples_leaf y class_weight afectan directamente el balance entre Recall, Precision y sobreajuste.
Por eso V8.0 incorpora optimización automática de hiperparámetros.
"""
)
add_qa(
"Modelos",
"¿Cómo se selecciona el modelo campeón?",
"""
El modelo campeón se selecciona en validación temporal, no en test.
El ranking considera métricas predictivas y de negocio:
ROC-AUC.
PR-AUC.
Recall.
Precision.
F1.
F2.
MCC.
Brier Score.
Beneficio económico.
Top-K.
Esto evita elegir el modelo sólo por Accuracy o por una métrica aislada.
"""
)
# ============================================================
# 21.5 MÉTRICAS
# ============================================================
add_qa(
"Métricas",
"¿Por qué no usar Accuracy como métrica principal?",
"""
Porque el evento positivo es raro.
Si sólo 1% o 2% de los casos tienen reclamo, un modelo que predice siempre 'sin reclamo' puede obtener Accuracy superior al 98%, pero detectar cero reclamos.
Por eso Accuracy se reporta, pero no se usa como criterio principal de selección.
"""
)
add_qa(
"Métricas",
"¿Qué es Recall?",
"""
Recall mide la proporción de reclamos reales que el modelo logra detectar.
Fórmula:
Recall = TP / (TP + FN)
Donde:
TP = reclamos correctamente detectados.
FN = reclamos que escaparon al modelo.
En este proyecto es clave porque perder un reclamo puede ser económicamente costoso.
"""
)
add_qa(
"Métricas",
"¿Qué es Precision?",
"""
Precision mide qué proporción de las alertas generadas por el modelo realmente eran reclamos.
Fórmula:
Precision = TP / (TP + FP)
Donde:
TP = alertas correctas.
FP = falsas alarmas.
En este proyecto ayuda a medir cuánta carga operacional innecesaria genera el modelo.
"""
)
add_qa(
"Métricas",
"¿Qué es F1-Score?",
"""
F1-Score es la media armónica entre Precision y Recall.
Fórmula:
F1 = 2 * (Precision * Recall) / (Precision + Recall)
Sirve cuando se busca balancear detección de reclamos y control de falsas alarmas.
En V8.0 se incluye en el ranking para comparar modelos de forma más completa.
"""
)
add_qa(
"Métricas",
"¿Qué es F2-Score y por qué se usa?",
"""
F2-Score es una variante del F-Score que da más peso al Recall que a la Precision.
Fórmula:
F2 = 5 * (Precision * Recall) / (4 * Precision + Recall)
Se usa cuando es más importante detectar reclamos que evitar falsas alarmas.
En este proyecto tiene sentido porque un reclamo escapado puede ser más costoso que una revisión adicional.
"""
)
add_qa(
"Métricas",
"¿Qué es ROC-AUC?",
"""
ROC-AUC mide la capacidad del modelo para ordenar positivos por encima de negativos considerando todos los thresholds.
Un ROC-AUC de 0.5 equivale a azar.
Un ROC-AUC mayor a 0.7 suele indicar capacidad discriminante razonable.
Sin embargo, en eventos raros puede ser optimista, por eso se complementa con PR-AUC.
"""
)
add_qa(
"Métricas",
"¿Qué es PR-AUC?",
"""
PR-AUC es el área bajo la curva Precision-Recall.
Es especialmente útil en problemas desbalanceados porque se concentra en la clase positiva.
En este proyecto es más informativa que ROC-AUC, ya que el objetivo principal es detectar reclamos comerciales.
"""
)
add_qa(
"Métricas",
"¿Qué es MCC?",
"""
MCC significa Matthews Correlation Coefficient.
Evalúa la calidad de una clasificación binaria considerando TP, TN, FP y FN.
Es útil en clases desbalanceadas porque no se deja dominar por la clase mayoritaria.
Su rango va de -1 a 1:
1 = clasificación perfecta.
0 = azar.
-1 = clasificación inversa.
"""
)
add_qa(
"Métricas",
"¿Qué es Brier Score?",
"""
Brier Score mide el error cuadrático medio de las probabilidades predichas.
Fórmula:
Brier = mean((y_real - probabilidad_predicha)^2)
Mientras menor sea, mejor calibradas están las probabilidades.
Es importante porque el modelo no sólo clasifica, sino que entrega probabilidades de riesgo.
"""
)
# ============================================================
# 21.6 TOP-K Y NEGOCIO
# ============================================================
add_qa(
"Top-K y negocio",
"¿Qué es Top-K?",
"""
Top-K es una política operacional donde el modelo ordena todos los lotes o contenedores desde mayor a menor riesgo y selecciona únicamente los primeros K para revisión.
K representa la cantidad máxima de unidades que la operación puede revisar.
Ejemplos:
Top-25 = revisar los 25 lotes de mayor riesgo.
Top-100 = revisar los 100 lotes de mayor riesgo.
Top-200 = revisar los 200 lotes de mayor riesgo.
No es un porcentaje. Es una capacidad operacional concreta.
"""
)
add_qa(
"Top-K y negocio",
"¿Por qué usamos Top-K?",
"""
Usamos Top-K porque la empresa no tiene capacidad infinita de revisión.
Un threshold fijo puede generar demasiadas alertas en algunas temporadas y muy pocas en otras.
Top-K garantiza una carga operacional estable y permite responder:
Si sólo puedo revisar K contenedores, ¿cuáles deberían ser?
Esto transforma el modelo en una herramienta práctica para priorización.
"""
)
add_qa(
"Top-K y negocio",
"¿Qué mejora Top-K respecto a un threshold fijo?",
"""
Un threshold fijo depende del nivel absoluto de probabilidad.
Si las probabilidades cambian por temporada, calibración o drift, el número de alertas puede variar mucho.
Top-K, en cambio, depende del ranking de riesgo.
Esto mejora la gestión operacional porque mantiene fija la cantidad de revisiones.
"""
)
add_qa(
"Top-K y negocio",
"¿Qué métricas se usan en Top-K?",
"""
Las principales métricas son:
Recall@K: porcentaje de reclamos capturados dentro de los K revisados.
Precision@K: porcentaje de los K revisados que efectivamente eran reclamos.
Beneficio@K: ahorro neto considerando reclamos detectados y costo de falsas alarmas.
Estas métricas conectan directamente el modelo con capacidad operativa y retorno económico.
"""
)
add_qa(
"Top-K y negocio",
"¿Cómo se interpreta Recall vs Top-K?",
"""
El gráfico Recall vs Top-K muestra cuántos reclamos se capturan a medida que aumenta la capacidad de revisión.
Si Top-50 captura pocos reclamos pero Top-200 captura muchos más, significa que el modelo requiere mayor capacidad operativa para generar valor.
Este gráfico ayuda a negociar recursos con la operación.
"""
)
add_qa(
"Top-K y negocio",
"¿Qué representa el beneficio económico?",
"""
El beneficio económico estima el valor neto del modelo.
Una forma simplificada es:
Beneficio = TP * ahorro_por_reclamo_detectado - FP * costo_revision
Donde:
TP = reclamos detectados.
FP = falsas alarmas.
Ahorro por reclamo detectado = costo evitado.
Costo de revisión = costo de inspeccionar un lote que no habría generado reclamo.
El objetivo no es maximizar métricas abstractas, sino utilidad económica.
"""
)
# ============================================================
# 21.7 CALIBRACIÓN Y THRESHOLD
# ============================================================
add_qa(
"Calibración y threshold",
"¿Qué es un threshold?",
"""
El threshold es el punto de corte usado para convertir una probabilidad en una decisión.
Ejemplo:
Si threshold = 0.10, entonces:
probabilidad >= 0.10 → alerta.
probabilidad < 0.10 → no alerta.
En eventos raros, thresholds bajos pueden ser necesarios para capturar reclamos.
"""
)
add_qa(
"Calibración y threshold",
"¿Por qué optimizar el threshold?",
"""
Porque el threshold estándar 0.50 casi nunca es adecuado en eventos raros.
Si la probabilidad base de reclamo es muy baja, usar 0.50 puede generar cero alertas.
Optimizar el threshold permite balancear Recall, Precision, falsas alarmas y beneficio económico.
"""
)
add_qa(
"Calibración y threshold",
"¿Qué es calibración?",
"""
Calibración significa que las probabilidades predichas reflejan frecuencias reales.
Ejemplo:
Si el modelo asigna probabilidad 0.20 a 100 casos, idealmente cerca de 20 deberían terminar siendo reclamos.
Un modelo puede ordenar bien los riesgos, pero estar mal calibrado.
Por eso se revisan curvas de calibración y Brier Score.
"""
)
# ============================================================
# 21.8 RANDOM FOREST CAUSAL
# ============================================================
add_qa(
"Random Forest Causal",
"¿Qué diferencia hay entre predicción y causalidad?",
"""
La predicción busca estimar qué ocurrirá.
La causalidad busca estimar qué cambiaría si se interviniera una variable.
Ejemplo predictivo:
¿Cuál es la probabilidad de reclamo?
Ejemplo causal:
¿Cuánto cambiaría la probabilidad de reclamo si se usara atmósfera controlada?
Son preguntas distintas y requieren supuestos distintos.
"""
)
add_qa(
"Random Forest Causal",
"¿Qué es ATE?",
"""
ATE significa Average Treatment Effect.
Representa el efecto promedio de aplicar un tratamiento sobre la población analizada.
En este proyecto podría interpretarse como:
¿Cuánto cambia, en promedio, la probabilidad de reclamo si se aplica un tratamiento operacional como atmósfera controlada, tipo de contenedor o una condición logística determinada?
"""
)
add_qa(
"Random Forest Causal",
"¿Qué es CATE?",
"""
CATE significa Conditional Average Treatment Effect.
A diferencia del ATE, que es promedio global, el CATE estima el efecto del tratamiento para cada perfil de lote o contenedor.
Esto permite identificar en qué condiciones el tratamiento parece más beneficioso o más riesgoso.
"""
)
add_qa(
"Random Forest Causal",
"¿Qué es Propensity Score?",
"""
El Propensity Score es la probabilidad de que una unidad reciba el tratamiento dado su conjunto de covariables.
Se define como:
e(X) = P(T = 1 | X)
Donde:
T = tratamiento.
X = variables observadas.
Sirve para ajustar diferencias entre tratados y controles en datos observacionales.
"""
)
add_qa(
"Random Forest Causal",
"¿Qué es AIPW / Double Robust?",
"""
AIPW significa Augmented Inverse Probability Weighting.
Es un estimador Double Robust porque combina:
1. Modelo de tratamiento o propensity score.
2. Modelo de resultado u outcome model.
La fórmula usada es:
ATE = mean[
mu1(X) - mu0(X)
+ T * (Y - mu1(X)) / e(X)
- (1 - T) * (Y - mu0(X)) / (1 - e(X))
]
Donde:
Y = resultado observado, en este caso reclamo_comercial.
T = tratamiento binario.
X = covariables observadas.
e(X) = propensity score.
mu1(X) = probabilidad esperada de reclamo bajo tratamiento.
mu0(X) = probabilidad esperada de reclamo sin tratamiento.
Se llama Double Robust porque puede entregar una estimación consistente si el modelo de propensión o el modelo de resultado está correctamente especificado, bajo supuestos causales.
"""
)
add_qa(
"Random Forest Causal",
"¿Qué es SUTVA?",
"""
SUTVA significa Stable Unit Treatment Value Assumption.
Este supuesto establece dos ideas:
1. El tratamiento aplicado a una unidad no debe afectar el resultado de otra unidad.
2. Cada tratamiento debe estar bien definido, sin versiones ambiguas.
En este proyecto significa que la condición operacional aplicada a un contenedor no debería modificar directamente el resultado de otro contenedor.
No puede demostrarse completamente con datos observacionales, pero debe justificarse con conocimiento del proceso.
"""
)
add_qa(
"Random Forest Causal",
"¿Qué es positividad?",
"""
Positividad significa que para cada combinación relevante de covariables debe existir probabilidad de observar unidades tratadas y no tratadas.
Formalmente:
0 < P(T = 1 | X) < 1
Si ciertos perfiles sólo aparecen tratados o sólo controles, no hay comparación válida.
En el notebook se revisa con la distribución del propensity score.
"""
)
add_qa(
"Random Forest Causal",
"¿Qué es ignorabilidad?",
"""
Ignorabilidad significa que, después de controlar por las variables observadas X, la asignación del tratamiento puede considerarse independiente del resultado potencial.
En términos prácticos:
No deberían quedar confusores importantes no observados que expliquen simultáneamente el tratamiento y el reclamo.
Este supuesto es fuerte y no se puede probar completamente, por eso el resultado causal se interpreta como exploratorio.
"""
)
add_qa(
"Random Forest Causal",
"¿Por qué se usa Random Forest en el módulo causal?",
"""
Random Forest permite modelar relaciones no lineales tanto en el propensity score como en los outcome models.
Esto es útil porque el tratamiento operacional puede depender de combinaciones complejas de mercado, packing, contenedor, clima y madurez.
Sin embargo, el Random Forest Causal no convierte automáticamente el análisis en experimental. Sigue siendo causalidad observacional y depende de supuestos.
"""
)
# ============================================================
# 21.9 ROBUSTEZ
# ============================================================
add_qa(
"Robustez",
"¿Por qué se usa Bootstrap?",
"""
Bootstrap permite estimar incertidumbre re-muestreando los datos muchas veces.
En lugar de reportar una sola métrica, se reportan promedios e intervalos de confianza.
Esto da más confianza porque muestra si el desempeño es estable o si depende demasiado de una muestra específica.
"""
)
add_qa(
"Robustez",
"¿Qué aporta el IC95%?",
"""
El intervalo de confianza al 95% entrega un rango plausible para una métrica o estimador.
Ejemplo:
ROC-AUC promedio = 0.67
IC95% = [0.63, 0.70]
Esto comunica incertidumbre y evita presentar los resultados como certezas absolutas.
"""
)
add_qa(
"Robustez",
"¿Qué es estabilidad temporal?",
"""
Estabilidad temporal significa que el modelo mantiene desempeño razonable al aplicarse en temporadas futuras.
Es crítica porque las condiciones de clima, mercado, logística y calidad pueden cambiar entre temporadas.
Por eso se usa validación temporal y sensibilidad por temporada.
"""
)
add_qa(
"Robustez",
"¿Qué es sensibilidad al threshold?",
"""
Es el análisis de cómo cambian las métricas y el beneficio económico cuando se modifica el umbral de decisión.
Permite demostrar que el modelo no depende de un único punto arbitrario.
También ayuda a elegir el umbral más coherente con los costos del negocio.
"""
)
# ============================================================
# 21.10 DESPLIEGUE
# ============================================================
add_qa(
"Despliegue",
"¿Cómo se usaría el modelo en producción?",
"""
El modelo se ejecutaría antes del despacho, usando datos disponibles de origen, cosecha, packing, logística, cliente y mercado.
El resultado sería un score de riesgo por lote o contenedor.
Luego se aplicaría una política operacional:
Threshold económico.
Top-K según capacidad de revisión.
Regla híbrida con validación de calidad.
La decisión final seguiría siendo humana, apoyada por evidencia del modelo.
"""
)
add_qa(
"Despliegue",
"¿Qué monitoreo requiere el modelo?",
"""
Requiere monitorear:
Distribución de variables.
Tasa real de reclamos.
ROC-AUC y PR-AUC por temporada.
Recall y Precision operacionales.
Beneficio económico.
Drift de datos.
Calibración de probabilidades.
Volumen de alertas.
"""
)
add_qa(
"Despliegue",
"¿Cuándo se debe reentrenar?",
"""
Se recomienda reentrenar al menos una vez por temporada o cuando se detecte drift importante.
También debería reentrenarse si cambian procesos de packing, rutas logísticas, mercados relevantes, clientes principales o condiciones climáticas extremas.
"""
)
# ============================================================
# 21.11 PREGUNTAS DIFÍCILES DE COMISIÓN
# ============================================================
add_qa(
"Preguntas difíciles",
"¿Por qué PR-AUC puede ser bajo aunque ROC-AUC sea razonable?",
"""
Porque ROC-AUC mide capacidad general de ranking entre positivos y negativos, pero puede verse optimista cuando la clase negativa domina.
PR-AUC se enfoca en la clase positiva.
En eventos raros, pequeños errores de ranking pueden afectar mucho la Precision.
Por eso un modelo puede tener ROC-AUC aceptable y PR-AUC bajo.
"""
)
add_qa(
"Preguntas difíciles",
"¿Por qué no buscamos Recall 100%?",
"""
Porque aumentar Recall generalmente implica aumentar falsos positivos.
Si se intenta capturar todos los reclamos, el modelo puede terminar alertando demasiados lotes, haciendo inviable la operación.
El objetivo no es maximizar Recall a cualquier costo, sino encontrar un equilibrio entre detección, capacidad de revisión y beneficio económico.
"""
)
add_qa(
"Preguntas difíciles",
"¿Por qué un modelo con menor Recall puede tener mayor beneficio?",
"""
Porque el beneficio depende tanto de los reclamos detectados como del costo de las falsas alarmas.
Un modelo que detecta más reclamos pero genera demasiadas revisiones innecesarias puede ser económicamente peor.
Por eso se evalúa beneficio neto y no sólo Recall.
"""
)
add_qa(
"Preguntas difíciles",
"¿Por qué no usar sólo XGBoost si es el campeón?",
"""
Porque el objetivo del Capstone no es sólo maximizar desempeño.
También se requiere interpretabilidad, explicación estadística y trazabilidad.
Por eso se conserva la Regresión Logística como benchmark interpretable y se usa XGBoost/LightGBM/Random Forest como challengers de desempeño.
"""
)
add_qa(
"Preguntas difíciles",
"¿Qué limitaciones tiene el modelo?",
"""
Las principales limitaciones son:
Evento positivo poco frecuente.
Posible drift entre temporadas.
Dependencia de calidad de datos pre-despacho.
Variables no observadas que podrían afectar reclamos.
El análisis causal es observacional, no experimental.
La política económica depende de supuestos de costo.
"""
)
# ============================================================
# 21.12 RESUMEN EJECUTIVO
# ============================================================
add_qa(
"Resumen ejecutivo",
"Si tuviera que explicar el proyecto en 3 minutos, ¿qué diría?",
"""
Este Capstone construye un sistema de alerta temprana para anticipar reclamos comerciales en exportación de fruta.
Primero se desarrolló una base logística interpretable desde V6.0, útil para explicar variables y construir un benchmark estadístico.
Luego, en V8.0, se incorporaron modelos challenger como Random Forest, XGBoost y LightGBM, con optimización de hiperparámetros, ranking automático, métricas para eventos raros, Top-K, curvas de beneficio y análisis causal exploratorio.
El objetivo final no es reemplazar la decisión humana, sino priorizar revisión de los embarques con mayor riesgo y apoyar decisiones de calidad, logística y negocio.
"""
)
# ============================================================
# CREACIÓN DATAFRAME Y EXPORTACIÓN
# ============================================================
qa_defensa_v8 = pd.DataFrame(qa_defensa)
qa_defensa_v8.insert(0, "id_pregunta", range(1, len(qa_defensa_v8) + 1))
display(qa_defensa_v8)
qa_defensa_v8.to_csv(TABLE_DIR_V8 / "V8_0_QA_Defensa_Extendido_Manual_Capstone.csv", index=False)
# Exportar también en Markdown
qa_md_path = TABLE_DIR_V8 / "V8_0_QA_Defensa_Extendido_Manual_Capstone.md"
with open(qa_md_path, "w", encoding="utf-8") as f:
f.write("# QA Defensa Extendida V8.0 — Manual Profesional de Capstone\n\n")
for _, row in qa_defensa_v8.iterrows():
f.write(f"## {row['id_pregunta']}. {row['pregunta']}\n\n")
f.write(f"**Sección:** {row['seccion']}\n\n")
f.write(row["respuesta"].strip())
f.write("\n\n---\n\n")
print(f"QA generado con {len(qa_defensa_v8)} preguntas.")
print(f"Archivo CSV: {TABLE_DIR_V8 / 'V8_0_QA_Defensa_Extendido_Manual_Capstone.csv'}")
print(f"Archivo Markdown: {qa_md_path}")
print("Explicación estadística:")
print("El QA se estructura como manual de defensa, incorporando definiciones, fundamentos técnicos, métricas, causalidad, negocio y despliegue.")
print("Lectura operacional:")
print("Este bloque permite preparar la defensa oral y anticipar preguntas de comisión desde una perspectiva técnica y ejecutiva.")
print("Recomendación ML:")
print("Mantener este QA actualizado con los resultados reales del modelo campeón, especialmente métricas test, Top-K, beneficio económico y ATE causal.")
==================================================================================================== 21 — QA DEFENSA EXTENDIDO V8.0 — MANUAL PROFESIONAL ====================================================================================================
| id_pregunta | seccion | pregunta | respuesta | |
|---|---|---|---|---|
| 0 | 1 | Problema de negocio | ¿Cuál es el problema de negocio que resuelve e... | \nEl problema de negocio consiste en anticipar... |
| 1 | 2 | Problema de negocio | ¿Por qué este problema se formuló como clasifi... | \nPorque la variable objetivo `reclamo_comerci... |
| 2 | 3 | Problema de negocio | ¿Por qué no basta con mirar históricos de recl... | \nPorque los históricos permiten describir lo ... |
| 3 | 4 | Problema de negocio | ¿Cuál es la unidad de decisión operacional? | \nLa unidad operacional relevante es el lote/c... |
| 4 | 5 | Problema de negocio | ¿Qué significa que el modelo sea un priorizado... | \nSignifica que el modelo no decide por sí sol... |
| 5 | 6 | Datos y validación | ¿Por qué se usó split temporal? | \nSe usó split temporal porque el objetivo rea... |
| 6 | 7 | Datos y validación | ¿Qué es leakage y cómo se evitó? | \nLeakage ocurre cuando el modelo utiliza info... |
| 7 | 8 | Datos y validación | ¿Por qué la tasa base de reclamo es importante? | \nPorque el reclamo comercial es un evento rar... |
| 8 | 9 | Feature Engineering | ¿Por qué se partió desde la V6.0? | \nLa V6.0 entregó una base parsimoniosa e inte... |
| 9 | 10 | Feature Engineering | ¿Por qué reducir variables? | \nReducir variables mejora interpretabilidad, ... |
| 10 | 11 | Feature Engineering | ¿Qué aporta LASSO? | \nLASSO usa regularización L1, que puede lleva... |
| 11 | 12 | Feature Engineering | ¿Qué aporta VIF? | \nVIF significa Variance Inflation Factor.\n\n... |
| 12 | 13 | Modelos | ¿Por qué se mantiene la Regresión Logística si... | \nLa Regresión Logística se mantiene como benc... |
| 13 | 14 | Modelos | ¿Qué aporta Random Forest? | \nRandom Forest combina múltiples árboles de d... |
| 14 | 15 | Modelos | ¿Qué aporta XGBoost? | \nXGBoost es un algoritmo de Gradient Boosting... |
| 15 | 16 | Modelos | ¿Qué aporta LightGBM? | \nLightGBM es otro algoritmo de Gradient Boost... |
| 16 | 17 | Modelos | ¿Por qué no basta con usar parámetros por defe... | \nLos parámetros por defecto no necesariamente... |
| 17 | 18 | Modelos | ¿Cómo se selecciona el modelo campeón? | \nEl modelo campeón se selecciona en validació... |
| 18 | 19 | Métricas | ¿Por qué no usar Accuracy como métrica principal? | \nPorque el evento positivo es raro.\n\nSi sól... |
| 19 | 20 | Métricas | ¿Qué es Recall? | \nRecall mide la proporción de reclamos reales... |
| 20 | 21 | Métricas | ¿Qué es Precision? | \nPrecision mide qué proporción de las alertas... |
| 21 | 22 | Métricas | ¿Qué es F1-Score? | \nF1-Score es la media armónica entre Precisio... |
| 22 | 23 | Métricas | ¿Qué es F2-Score y por qué se usa? | \nF2-Score es una variante del F-Score que da ... |
| 23 | 24 | Métricas | ¿Qué es ROC-AUC? | \nROC-AUC mide la capacidad del modelo para or... |
| 24 | 25 | Métricas | ¿Qué es PR-AUC? | \nPR-AUC es el área bajo la curva Precision-Re... |
| 25 | 26 | Métricas | ¿Qué es MCC? | \nMCC significa Matthews Correlation Coefficie... |
| 26 | 27 | Métricas | ¿Qué es Brier Score? | \nBrier Score mide el error cuadrático medio d... |
| 27 | 28 | Top-K y negocio | ¿Qué es Top-K? | \nTop-K es una política operacional donde el m... |
| 28 | 29 | Top-K y negocio | ¿Por qué usamos Top-K? | \nUsamos Top-K porque la empresa no tiene capa... |
| 29 | 30 | Top-K y negocio | ¿Qué mejora Top-K respecto a un threshold fijo? | \nUn threshold fijo depende del nivel absoluto... |
| 30 | 31 | Top-K y negocio | ¿Qué métricas se usan en Top-K? | \nLas principales métricas son:\n\nRecall@K: p... |
| 31 | 32 | Top-K y negocio | ¿Cómo se interpreta Recall vs Top-K? | \nEl gráfico Recall vs Top-K muestra cuántos r... |
| 32 | 33 | Top-K y negocio | ¿Qué representa el beneficio económico? | \nEl beneficio económico estima el valor neto ... |
| 33 | 34 | Calibración y threshold | ¿Qué es un threshold? | \nEl threshold es el punto de corte usado para... |
| 34 | 35 | Calibración y threshold | ¿Por qué optimizar el threshold? | \nPorque el threshold estándar 0.50 casi nunca... |
| 35 | 36 | Calibración y threshold | ¿Qué es calibración? | \nCalibración significa que las probabilidades... |
| 36 | 37 | Random Forest Causal | ¿Qué diferencia hay entre predicción y causali... | \nLa predicción busca estimar qué ocurrirá.\n\... |
| 37 | 38 | Random Forest Causal | ¿Qué es ATE? | \nATE significa Average Treatment Effect.\n\nR... |
| 38 | 39 | Random Forest Causal | ¿Qué es CATE? | \nCATE significa Conditional Average Treatment... |
| 39 | 40 | Random Forest Causal | ¿Qué es Propensity Score? | \nEl Propensity Score es la probabilidad de qu... |
| 40 | 41 | Random Forest Causal | ¿Qué es AIPW / Double Robust? | \nAIPW significa Augmented Inverse Probability... |
| 41 | 42 | Random Forest Causal | ¿Qué es SUTVA? | \nSUTVA significa Stable Unit Treatment Value ... |
| 42 | 43 | Random Forest Causal | ¿Qué es positividad? | \nPositividad significa que para cada combinac... |
| 43 | 44 | Random Forest Causal | ¿Qué es ignorabilidad? | \nIgnorabilidad significa que, después de cont... |
| 44 | 45 | Random Forest Causal | ¿Por qué se usa Random Forest en el módulo cau... | \nRandom Forest permite modelar relaciones no ... |
| 45 | 46 | Robustez | ¿Por qué se usa Bootstrap? | \nBootstrap permite estimar incertidumbre re-m... |
| 46 | 47 | Robustez | ¿Qué aporta el IC95%? | \nEl intervalo de confianza al 95% entrega un ... |
| 47 | 48 | Robustez | ¿Qué es estabilidad temporal? | \nEstabilidad temporal significa que el modelo... |
| 48 | 49 | Robustez | ¿Qué es sensibilidad al threshold? | \nEs el análisis de cómo cambian las métricas ... |
| 49 | 50 | Despliegue | ¿Cómo se usaría el modelo en producción? | \nEl modelo se ejecutaría antes del despacho, ... |
| 50 | 51 | Despliegue | ¿Qué monitoreo requiere el modelo? | \nRequiere monitorear:\n\nDistribución de vari... |
| 51 | 52 | Despliegue | ¿Cuándo se debe reentrenar? | \nSe recomienda reentrenar al menos una vez po... |
| 52 | 53 | Preguntas difíciles | ¿Por qué PR-AUC puede ser bajo aunque ROC-AUC ... | \nPorque ROC-AUC mide capacidad general de ran... |
| 53 | 54 | Preguntas difíciles | ¿Por qué no buscamos Recall 100%? | \nPorque aumentar Recall generalmente implica ... |
| 54 | 55 | Preguntas difíciles | ¿Por qué un modelo con menor Recall puede tene... | \nPorque el beneficio depende tanto de los rec... |
| 55 | 56 | Preguntas difíciles | ¿Por qué no usar sólo XGBoost si es el campeón? | \nPorque el objetivo del Capstone no es sólo m... |
| 56 | 57 | Preguntas difíciles | ¿Qué limitaciones tiene el modelo? | \nLas principales limitaciones son:\n\nEvento ... |
| 57 | 58 | Resumen ejecutivo | Si tuviera que explicar el proyecto en 3 minut... | \nEste Capstone construye un sistema de alerta... |
QA generado con 58 preguntas. Archivo CSV: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/tablas/V8_0_QA_Defensa_Extendido_Manual_Capstone.csv Archivo Markdown: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/tablas/V8_0_QA_Defensa_Extendido_Manual_Capstone.md Explicación estadística: El QA se estructura como manual de defensa, incorporando definiciones, fundamentos técnicos, métricas, causalidad, negocio y despliegue. Lectura operacional: Este bloque permite preparar la defensa oral y anticipar preguntas de comisión desde una perspectiva técnica y ejecutiva. Recomendación ML: Mantener este QA actualizado con los resultados reales del modelo campeón, especialmente métricas test, Top-K, beneficio económico y ATE causal.
22_V8_Reporte_Final¶
Reporte final con gráficos, ranking, métricas del campeón, comparación con logística, curvas, causalidad, robustez y recomendaciones para defensa.
# ======================================================================================
# 22. REPORTE FINAL AUTOMÁTICO — V8.0 FINAL
# ======================================================================================
print_step("22 — REPORTE FINAL V8.0 FINAL")
# Gráfico ranking de modelos por beneficio y PR-AUC.
plt.figure(figsize=(10,6))
plot_rank = ranking_test.head(15).iloc[::-1]
plt.barh(plot_rank["modelo"] + " | " + plot_rank["model_id"], plot_rank["beneficio_neto_usd"])
plt.title("Ranking modelos V8.0 — Beneficio neto test")
plt.xlabel("Beneficio neto USD")
savefig_v8("V8_0_reporte_ranking_beneficio_modelos.png")
plt.figure(figsize=(10,6))
plot_rank = ranking_test.head(15).iloc[::-1]
plt.barh(plot_rank["modelo"] + " | " + plot_rank["model_id"], plot_rank["pr_auc"])
plt.title("Ranking modelos V8.0 — PR-AUC test")
plt.xlabel("PR-AUC")
savefig_v8("V8_0_reporte_ranking_pr_auc_modelos.png")
report_v8 = f"""
# Reporte Final — V8.0 Final desde V6.0 — Plataforma Alto Desempeño + Random Forest Causal
## Objetivo
Construir una versión final de alto desempeño a partir de la V6.0, manteniendo la regresión logística como benchmark interpretable y agregando modelos challenger no lineales, optimización automática de hiperparámetros, ranking por desempeño, curvas completas, robustez y Random Forest Causal.
## Base metodológica
- Base heredada: V6.0 Regresión Logística con menos variables.
- Variables base usadas por V8.0: {len(FEATURES_V8_BASE)}.
- Split: temporal, con train={len(y_train):,}, validación={len(y_val):,}, test={len(y_test):,}.
- Selección del campeón: realizada en validación, no en test.
## Modelos evaluados
- Benchmark: Regresión Logística V6.0.
- Random Forest normal con optimización de hiperparámetros.
- ExtraTrees.
- Gradient Boosting.
- HistGradientBoosting.
- XGBoost: {'incluido' if HAS_XGB else 'no disponible'}.
- LightGBM: {'incluido' if HAS_LGBM else 'no disponible'}.
## Modelo campeón V8.0
- Modelo: {champion_family}.
- ID: {champion_id}.
- Threshold seleccionado en validación: {champion_threshold:.4f}.
## Métricas test del campeón
- ROC-AUC: {ct['roc_auc']:.4f}.
- PR-AUC: {ct['pr_auc']:.4f}.
- Recall/Sensibilidad: {ct['recall_sensitivity']:.4f}.
- Precision/PPV: {ct['precision_ppv']:.4f}.
- F1-Score: {ct['f1_score']:.4f}.
- F2-Score: {ct['f2_score']:.4f}.
- MCC: {ct['mcc']:.4f}.
- Brier Score: {ct['brier_score']:.4f}.
- Beneficio neto threshold: USD {ct['beneficio_neto_usd']:,.0f}.
## Matriz de confusión test del campeón
- TN: {int(ct['TN'])}.
- FP: {int(ct['FP'])}.
- FN: {int(ct['FN'])}.
- TP: {int(ct['TP'])}.
## Mejor política Top-K del campeón
- Top-K: {int(best_topk_champ['top_k'])}.
- TP: {int(best_topk_champ['TP'])}.
- FP: {int(best_topk_champ['FP'])}.
- FN: {int(best_topk_champ['FN'])}.
- Recall: {best_topk_champ['recall_sensitivity']:.4f}.
- Precision: {best_topk_champ['precision_ppv']:.4f}.
- F1-Score: {best_topk_champ['f1_score']:.4f}.
- F2-Score: {best_topk_champ['f2_score']:.4f}.
- Beneficio neto: USD {best_topk_champ['beneficio_neto_usd']:,.0f}.
## Random Forest Causal
Se incorpora un módulo causal con AIPW / Double Robust:
ATE = mean(mu1(X) - mu0(X) + T*(Y-mu1(X))/e(X) - (1-T)*(Y-mu0(X))/(1-e(X)))
Este módulo permite analizar tratamientos operacionales bajo supuestos observacionales. No reemplaza un experimento, pero fortalece la lectura causal del Capstone.
## Gráficos generados automáticamente
- Ranking de modelos por beneficio.
- Ranking de modelos por PR-AUC.
- Comparación ROC.
- Comparación Precision-Recall.
- Lift y Gain del campeón.
- Curva de calibración.
- Recall vs Threshold.
- Precision vs Threshold.
- Beneficio vs Threshold.
- Recall vs Top-K campeón vs logística.
- Beneficio vs Top-K campeón vs logística.
- Importancia de variables.
- Importancia por nivel.
- Bootstrap del beneficio.
## Diagnóstico experto
La V8.0 Final deja de depender de un único algoritmo. El notebook compara modelos bajo el mismo protocolo temporal, elige un campeón con criterios alineados al negocio y conserva la V6.0 como benchmark explicativo. Este enfoque es más defendible ante un comité de Magíster porque demuestra evidencia comparativa, robustez y trazabilidad.
## Recomendación operacional
Usar el campeón V8.0 como priorizador de revisión pre-despacho. No usarlo como bloqueo automático sin una política aprobada de costos, capacidad Top-K y monitoreo de drift por temporada, mercado y cliente.
"""
print(report_v8)
(OUTPUT_DIR_V8 / "V8_0_reporte_final.md").write_text(report_v8, encoding="utf-8")
# Índice de outputs.
outputs = []
for folder in [TABLE_DIR_V8, GRAF_DIR_V8, MODEL_DIR_V8]:
for p in sorted(folder.glob("*")):
outputs.append({"tipo": folder.name, "archivo": str(p)})
outputs_df = pd.DataFrame(outputs)
display(outputs_df)
outputs_df.to_csv(OUTPUT_DIR_V8 / "V8_0_indice_outputs.csv", index=False)
print("Reporte final guardado en:", OUTPUT_DIR_V8 / "V8_0_reporte_final.md")
print("Índice outputs guardado en:", OUTPUT_DIR_V8 / "V8_0_indice_outputs.csv")
==================================================================================================== 22 — REPORTE FINAL V8.0 FINAL ==================================================================================================== [GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_reporte_ranking_beneficio_modelos.png
[GRAFICO V8] Guardado: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/graficos/V8_0_reporte_ranking_pr_auc_modelos.png
# Reporte Final — V8.0 Final desde V6.0 — Plataforma Alto Desempeño + Random Forest Causal ## Objetivo Construir una versión final de alto desempeño a partir de la V6.0, manteniendo la regresión logística como benchmark interpretable y agregando modelos challenger no lineales, optimización automática de hiperparámetros, ranking por desempeño, curvas completas, robustez y Random Forest Causal. ## Base metodológica - Base heredada: V6.0 Regresión Logística con menos variables. - Variables base usadas por V8.0: 18. - Split: temporal, con train=10,529, validación=2,093, test=2,114. - Selección del campeón: realizada en validación, no en test. ## Modelos evaluados - Benchmark: Regresión Logística V6.0. - Random Forest normal con optimización de hiperparámetros. - ExtraTrees. - Gradient Boosting. - HistGradientBoosting. - XGBoost: incluido. - LightGBM: incluido. ## Modelo campeón V8.0 - Modelo: XGBoost. - ID: V8_0225_XGBoost. - Threshold seleccionado en validación: 0.0150. ## Métricas test del campeón - ROC-AUC: 0.6819. - PR-AUC: 0.0344. - Recall/Sensibilidad: 0.3793. - Precision/PPV: 0.0365. - F1-Score: 0.0667. - F2-Score: 0.1319. - MCC: 0.0800. - Brier Score: 0.0135. - Beneficio neto threshold: USD 225,000. ## Matriz de confusión test del campeón - TN: 1795. - FP: 290. - FN: 18. - TP: 11. ## Mejor política Top-K del campeón - Top-K: 150. - TP: 7. - FP: 143. - FN: 22. - Recall: 0.2414. - Precision: 0.0467. - F1-Score: 0.0782. - F2-Score: 0.1316. - Beneficio neto: USD 205,500. ## Random Forest Causal Se incorpora un módulo causal con AIPW / Double Robust: ATE = mean(mu1(X) - mu0(X) + T*(Y-mu1(X))/e(X) - (1-T)*(Y-mu0(X))/(1-e(X))) Este módulo permite analizar tratamientos operacionales bajo supuestos observacionales. No reemplaza un experimento, pero fortalece la lectura causal del Capstone. ## Gráficos generados automáticamente - Ranking de modelos por beneficio. - Ranking de modelos por PR-AUC. - Comparación ROC. - Comparación Precision-Recall. - Lift y Gain del campeón. - Curva de calibración. - Recall vs Threshold. - Precision vs Threshold. - Beneficio vs Threshold. - Recall vs Top-K campeón vs logística. - Beneficio vs Top-K campeón vs logística. - Importancia de variables. - Importancia por nivel. - Bootstrap del beneficio. ## Diagnóstico experto La V8.0 Final deja de depender de un único algoritmo. El notebook compara modelos bajo el mismo protocolo temporal, elige un campeón con criterios alineados al negocio y conserva la V6.0 como benchmark explicativo. Este enfoque es más defendible ante un comité de Magíster porque demuestra evidencia comparativa, robustez y trazabilidad. ## Recomendación operacional Usar el campeón V8.0 como priorizador de revisión pre-despacho. No usarlo como bloqueo automático sin una política aprobada de costos, capacidad Top-K y monitoreo de drift por temporada, mercado y cliente.
| tipo | archivo | |
|---|---|---|
| 0 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 1 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 2 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 3 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 4 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 5 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 6 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 7 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 8 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 9 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 10 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 11 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 12 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 13 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 14 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 15 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 16 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 17 | tablas | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 18 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 19 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 20 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 21 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 22 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 23 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 24 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 25 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 26 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 27 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 28 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 29 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 30 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 31 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 32 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 33 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 34 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 35 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 36 | graficos | /content/capstone_outputs_V8_0_final_desde_V6_... |
| 37 | modelos | /content/capstone_outputs_V8_0_final_desde_V6_... |
Reporte final guardado en: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/V8_0_reporte_final.md Índice outputs guardado en: /content/capstone_outputs_V8_0_final_desde_V6_alto_desempeno/V8_0_indice_outputs.csv
Finalmente, podemos explicar que V6.0 y V8.0 cumplen roles distintos:
- Modelo campeón XGBoost
- Benchmark Regresión Logística V6.0
- ROC-AUC 0.6819
- PR-AUC 0.0344
- Recall 37.9%
- Precision 3.65%
- F1 0.0667
- F2 0.1319
- MCC 0.0800
- Brier Score 0.0135
- Threshold operativo 0.015
- Mejor Top-K 150
- Beneficio neto (threshold) USD 225.000
- Beneficio neto (mejor Top-K) USD 205.500
- Benchmark V6.0 USD 52.500
- Mejora económica +328% respecto al benchmark
Finalmente, considero que la arquitectura de V8.0 ya está al nivel de un proyecto profesional de Machine Learning. Mantiene un benchmark interpretable, compara múltiples algoritmos bajo un protocolo experimental consistente, optimiza hiperparámetros, incorpora métricas adecuadas para eventos raros, evalúa impacto económico, añade un módulo de inferencia causal y genera evidencia gráfica para la defensa.
========================================================================================== ¿QUÉ AYUDA A DISMINUIR RECLAMOS?¶
El modelo sugiere que las acciones más defendibles para disminuir riesgo son aquellas que actúan sobre variables intervenibles:
- Reducir heterogeneidad de materia seca.
- Evitar fruta con baja firmeza estructural para viajes largos o clientes exigentes.
- Reducir tránsito o reasignar fruta sensible a destinos de menor duración.
- Acelerar el paso cosecha → packing para disminuir deterioro temprano.
- Aplicar intervención integral cuando coinciden heterogeneidad, baja firmeza, tránsito largo y packing lento.
========================================================================================== LECTURA DE DEFENSA — EFECTOS MARGINALES¶
- Forzar riesgo_materia_seca_baja=1: Aumenta riesgo | Δ prob. promedio = 1.4153 pp | Δ alertas = 240
- Forzar riesgo_firmeza_baja=0: Aumenta riesgo | Δ prob. promedio = 0.9759 pp | Δ alertas = 117
- +1 punto porcentual en materia seca: Aumenta riesgo | Δ prob. promedio = 0.3366 pp | Δ alertas = 118
- +1 día entre cosecha y packing: Aumenta riesgo | Δ prob. promedio = 0.2966 pp | Δ alertas = 109
- Forzar riesgo_materia_seca_alta=1: Aumenta riesgo | Δ prob. promedio = 0.1050 pp | Δ alertas = 49
- +3 días de tránsito planificado: Aumenta riesgo | Δ prob. promedio = 0.0691 pp | Δ alertas = 32
- -1 punto porcentual en desviación de materia seca: Aumenta riesgo | Δ prob. promedio = 0.0006 pp | Δ alertas = 1
- Forzar transito_largo_flag=1: Reduce riesgo | Δ prob. promedio = 0.0000 pp | Δ alertas = 0
- +1 lb en firmeza de pulpa: Reduce riesgo | Δ prob. promedio = 0.0000 pp | Δ alertas = 0
- +1 punto porcentual en desviación de materia seca: Reduce riesgo | Δ prob. promedio = -0.0011 pp | Δ alertas = 0
- +5 puntos porcentuales en desviación de materia seca: Reduce riesgo | Δ prob. promedio = -0.0043 pp | Δ alertas = -1
- Forzar riesgo_materia_seca_alta=0: Reduce riesgo | Δ prob. promedio = -0.0113 pp | Δ alertas = -1
- -3 días de tránsito planificado: Reduce riesgo | Δ prob. promedio = -0.0654 pp | Δ alertas = -32
- -1 día entre cosecha y packing: Reduce riesgo | Δ prob. promedio = -0.2305 pp | Δ alertas = -115
- Forzar riesgo_materia_seca_baja=0: Reduce riesgo | Δ prob. promedio = -0.2476 pp | Δ alertas = -248
- Forzar riesgo_firmeza_baja=1: Reduce riesgo | Δ prob. promedio = -0.3827 pp | Δ alertas = -351
Estas simulaciones no prueban causalidad definitiva; muestran efectos contrafactuales bajo el modelo entrenado. Son útiles para defender acciones operacionales: mejorar homogeneidad, aumentar firmeza, reducir tránsito o acelerar packing.