XGBoost -empresas en Reorganización#
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
classification_report, confusion_matrix,
accuracy_score, f1_score, roc_auc_score,
RocCurveDisplay, ConfusionMatrixDisplay
)
import xgboost.callback as xgb_callback
from xgboost import XGBClassifier, plot_importance
import warnings
warnings.filterwarnings("ignore")
path = "BD empresas en re organización.xlsx"
xls = pd.ExcelFile(path)
df = pd.read_excel(path, sheet_name=xls.sheet_names[0])
df.head()
| Razón Social | Margen EBIT | Carga financiera | Margen neto | CxC | CxP | Solvencia | Apalancamiento | En Reorganización | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | AACER SAS | 0.071690 | 0.000000 | 0.042876 | 0.104095 | 0.153192 | 1.877078 | 1.642505 | 0 |
| 1 | ABARROTES EL ROMPOY SAS | 0.017816 | 0.000000 | 0.010767 | 0.018414 | 0.000000 | 0.000000 | 0.865044 | 0 |
| 2 | ABASTECIMIENTOS INDUSTRIALES SAS | 0.144646 | 0.054226 | 0.059784 | 0.227215 | 0.025591 | 1.077412 | 1.272299 | 0 |
| 3 | ACME LEON PLASTICOS SAS | 0.004465 | 0.000000 | -0.013995 | 0.073186 | 0.127866 | 0.000000 | 1.391645 | 0 |
| 4 | ADVANCED PRODUCTS COLOMBIA SAS | 0.141829 | 0.050810 | 0.053776 | 0.398755 | 0.147678 | 0.675073 | 2.118774 | 0 |
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 629 entries, 0 to 628
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Razón Social 629 non-null object
1 Margen EBIT 629 non-null float64
2 Carga financiera 629 non-null float64
3 Margen neto 629 non-null float64
4 CxC 629 non-null float64
5 CxP 629 non-null float64
6 Solvencia 629 non-null float64
7 Apalancamiento 629 non-null float64
8 En Reorganización 629 non-null int64
dtypes: float64(7), int64(1), object(1)
memory usage: 44.4+ KB
Ajuste con todas las variables#
# ------------------------
# Selección de variables
# ------------------------
variables_seleccionadas = ['Margen EBIT',
'Carga financiera',
'Margen neto',
'CxC',
'CxP',
'Solvencia',
'Apalancamiento']
# Variable objetivo
target = 'En Reorganización'
# ------------------------
# Preparar datos
# ------------------------
X = df[variables_seleccionadas]
y = df[target]
# Estandarizar variables
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Dividir en entrenamiento y prueba (70%-30%)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.3, random_state=35, stratify=y)
XGBoost#
Definición del Modelo:
model = XGBClassifier(
# --- Tasa de aprendizaje e iteraciones ---
learning_rate=0.1, # η: contracción de cada árbol
n_estimators=500, # techo de árboles (early stopping decide el real)
early_stopping_rounds=50, # Early Stopping
# --- Estructura del árbol ---
max_depth=5, # profundidad máxima por árbol
min_child_weight=3, # suma mínima de hessianos en nodo hijo
gamma=0.1, # ganancia mínima para aceptar un split
# --- Regularización directa ---
reg_lambda=1, # λ: penalización L2 sobre pesos de hojas
reg_alpha=0.1, # α: penalización L1 sobre pesos de hojas (sparsity)
# --- Muestreo (stochastic boosting) ---
subsample=0.8, # fracción de filas por árbol
colsample_bytree=0.8, # fracción de columnas por árbol
# --- Configuración general ---
objective='binary:logistic',
eval_metric='logloss',
random_state=36
)
Entrenamiento con Early Stopping:
¿Por qué se hace un segundo split?
Early stopping necesita dos conjuntos con roles distintos:
Datos de entrenamiento → el modelo aprende de ellos (ajusta pesos, construye árboles).
Datos de validación → el modelo no aprende de ellos, solo los usa como “alarma” para saber cuándo parar.
Si monitoreas la pérdida sobre los mismos datos con los que entrenas, la pérdida casi siempre seguirá bajando (porque el modelo puede memorizar), y early stopping nunca se activaría.
Es por esto que X_train se divide otra vez.
# Separar una porción del train para monitoreo
X_tr, X_val, y_tr, y_val = train_test_split(
X_train, y_train, test_size=0.2, random_state=35, stratify=y_train
)
model.fit(
X_tr, y_tr,
eval_set=[(X_val, y_val)],
verbose=False,
)
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=0.8, device=None, early_stopping_rounds=50,
enable_categorical=False, eval_metric='logloss',
feature_types=None, feature_weights=None, gamma=0.1,
grow_policy=None, importance_type=None,
interaction_constraints=None, learning_rate=0.1, max_bin=None,
max_cat_threshold=None, max_cat_to_onehot=None,
max_delta_step=None, max_depth=5, max_leaves=None,
min_child_weight=3, missing=nan, monotone_constraints=None,
multi_strategy=None, n_estimators=500, n_jobs=None,
num_parallel_tree=None, ...)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=0.8, device=None, early_stopping_rounds=50,
enable_categorical=False, eval_metric='logloss',
feature_types=None, feature_weights=None, gamma=0.1,
grow_policy=None, importance_type=None,
interaction_constraints=None, learning_rate=0.1, max_bin=None,
max_cat_threshold=None, max_cat_to_onehot=None,
max_delta_step=None, max_depth=5, max_leaves=None,
min_child_weight=3, missing=nan, monotone_constraints=None,
multi_strategy=None, n_estimators=500, n_jobs=None,
num_parallel_tree=None, ...)Resultados#
print(f"Árboles solicitados (n_estimators): {model.n_estimators}")
print(f"Árboles construidos (best_iteration): {model.best_iteration}")
print(f"Mejor logloss en validación: {model.best_score:.4f}")
Árboles solicitados (n_estimators): 500
Árboles construidos (best_iteration): 21
Mejor logloss en validación: 0.4752
resultados = model.evals_result()
plt.figure(figsize=(10, 5))
plt.plot(resultados['validation_0']['logloss'], label='Validación', color='steelblue')
plt.axvline(x=model.best_iteration, color='red', linestyle='--', label=f'Mejor iteración ({model.best_iteration})')
plt.xlabel('Número de árboles')
plt.ylabel('Log Loss')
plt.title('Curva de Aprendizaje')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
importancia = pd.DataFrame({
'Feature': variables_seleccionadas,
'Gain': [model.get_booster().get_score(importance_type='gain').get(f'f{i}', 0) for i in range(len(variables_seleccionadas))],
'Weight': [model.get_booster().get_score(importance_type='weight').get(f'f{i}', 0) for i in range(len(variables_seleccionadas))],
'Cover': [model.get_booster().get_score(importance_type='cover').get(f'f{i}', 0) for i in range(len(variables_seleccionadas))],
})
importancia = importancia.sort_values('Gain', ascending=False).reset_index(drop=True)
print(importancia.to_string(index=False))
Feature Gain Weight Cover
Apalancamiento 4.085507 86.0 27.509918
CxC 3.897206 87.0 24.689163
CxP 3.717442 88.0 23.958036
Margen neto 3.180511 83.0 25.785898
Solvencia 3.067891 73.0 20.580566
Carga financiera 1.666710 44.0 19.381701
Margen EBIT 1.559393 56.0 17.179787
La tabla muestra qué tan importante fue cada variable para el modelo, medida de tres formas distintas. Las tres cuentan historias complementarias.
Gain es la más informativa. Cada vez que un árbol usa una variable para hacer un split, ese split produce una reducción en la función de pérdida (la “ganancia” que discutimos con gamma). El Gain de la tabla es el promedio de esas ganancias a lo largo de todos los árboles. Apalancamiento tiene el Gain más alto (4.08), lo que significa que cada vez que el modelo usó Apalancamiento para partir un nodo, en promedio mejoró la predicción más que con cualquier otra variable. Carga financiera y Margen EBIT tienen los Gain más bajos (1.66 y 1.55), lo que indica que cuando se usaron, su contribución a reducir el error fue menor.
Weight es simplemente cuántas veces apareció cada variable como criterio de split en todos los árboles. CxP fue la más usada (88 veces), Carga financiera la menos usada (44 veces). Pero esta métrica puede ser engañosa: una variable ruidosa podría aparecer muchas veces haciendo splits pequeños e inútiles, inflando su Weight sin realmente contribuir. Por eso CxP tiene el Weight más alto pero no el Gain más alto: se usó mucho pero cada uso individual aportó un poco menos que Apalancamiento.
Cover es el número promedio de muestras que pasaron por los nodos donde se usó esa variable. Apalancamiento tiene el Cover más alto (27.5), lo que significa que cuando el modelo la usó, afectó a más muestras en promedio. Margen EBIT tiene el más bajo (17.1): sus splits tendieron a ocurrir en nodos más pequeños, afectando a menos observaciones. Si Apalancamiento tiene Cover de 27.5, significa que en promedio, cada vez que el modelo usó Apalancamiento para partir un nodo, ese nodo contenía unas 27-28 muestras. Los splits con Apalancamiento ocurrieron en nodos “grandes”, cerca de la raíz, donde todavía había muchas muestras juntas. La decisión afectó a muchas observaciones de un solo golpe.
Evaluación del ajuste#
# Probabilidades:
y_prob_train = model.predict(X_train)
y_prob = model.predict(X_test)
# Definición de las clases con umbral:
y_pred_train = np.where(y_prob_train > 0.5, 1, 0)
y_pred = np.where(y_prob > 0.5, 1, 0)
# ------------------------
# Evaluación del modelo
# ------------------------
# =========================================================
# 1. Matrices de confusión
# =========================================================
cm_train = confusion_matrix(y_train, y_pred_train, labels=[0, 1])
cm_test = confusion_matrix(y_test, y_pred, labels=[0, 1])
cm_df_train = pd.DataFrame(
cm_train,
index=["Real: No Reorg.", "Real: Reorg."],
columns=["Pred: No Reorg.", "Pred: Reorg."]
)
cm_df_test = pd.DataFrame(
cm_test,
index=["Real: No Reorg.", "Real: Reorg."],
columns=["Pred: No Reorg.", "Pred: Reorg."]
)
# =========================================================
# 2. Estilo visual
# =========================================================
cmap = mpl.colormaps["viridis"]
BG_FIG = "#f7f7f7"
BG_AX = "#ffffff"
GRID_COL = "#d9d9d9"
TEXT_COL = "#1f1f1f"
SUB_COL = "#4d4d4d"
TITLE_FS = 20
SUBTITLE_FS = 12
LABEL_FS = 12
TICK_FS = 11
ANNOT_FS = 16
sns.set_theme(style="white")
# =========================================================
# 3. Figura con dos paneles
# =========================================================
fig, axes = plt.subplots(1, 2, figsize=(12, 5.5), facecolor=BG_FIG)
fig.suptitle(
"Matrices de confusión",
fontsize=TITLE_FS,
fontweight="bold",
color=TEXT_COL,
y=0.98
)
# =========================================================
# 4. Función para dibujar cada heatmap
# =========================================================
def plot_conf_matrix(ax, cm_df, title):
ax.set_facecolor(BG_AX)
hm = sns.heatmap(
cm_df,
annot=True,
fmt="d",
cmap=cmap,
cbar=True,
linewidths=0.8,
linecolor=GRID_COL,
square=True,
annot_kws={
"fontsize": ANNOT_FS,
"fontweight": "bold",
"color": TEXT_COL
},
cbar_kws={"shrink": 0.85},
ax=ax
)
ax.set_title(
title,
fontsize=15,
fontweight="bold",
color=TEXT_COL,
pad=10
)
ax.set_xlabel(
"Clase predicha",
fontsize=LABEL_FS,
fontweight="bold",
color=SUB_COL
)
ax.set_ylabel(
"Clase real",
fontsize=LABEL_FS,
fontweight="bold",
color=SUB_COL
)
ax.tick_params(axis='x', labelsize=TICK_FS, colors=TEXT_COL, rotation=0)
ax.tick_params(axis='y', labelsize=TICK_FS, colors=TEXT_COL, rotation=0)
for lbl in ax.get_xticklabels() + ax.get_yticklabels():
lbl.set_fontweight("bold")
# Estilo del colorbar
cbar = hm.collections[0].colorbar
cbar.ax.tick_params(labelsize=10, colors=TEXT_COL)
for t in cbar.ax.get_yticklabels():
t.set_fontweight("bold")
for spine in ax.spines.values():
spine.set_edgecolor(GRID_COL)
spine.set_linewidth(0.8)
# =========================================================
# 5. Dibujar train y test
# =========================================================
plot_conf_matrix(axes[0], cm_df_train, "Train")
plot_conf_matrix(axes[1], cm_df_test, "Test")
plt.tight_layout(rect=[0.03, 0.08, 0.98, 0.92])
plt.show()
print("\n=== Reporte de Clasificación - train ===")
print(classification_report(y_train, y_pred_train))
print("\n=== Reporte de Clasificación - test ===")
print(classification_report(y_test, y_pred))
=== Reporte de Clasificación - train ===
precision recall f1-score support
0 0.83 0.91 0.87 201
1 0.91 0.85 0.88 239
accuracy 0.87 440
macro avg 0.87 0.88 0.87 440
weighted avg 0.88 0.87 0.87 440
=== Reporte de Clasificación - test ===
precision recall f1-score support
0 0.76 0.84 0.80 86
1 0.85 0.78 0.81 103
accuracy 0.80 189
macro avg 0.80 0.81 0.80 189
weighted avg 0.81 0.80 0.80 189