RF OFICIAL

1 CURVAS SIMULADAS

1.1 ESIG

1.1.1 RANDOM FOREST - SIGNATURE

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada. Si algo sigue fallando, vuelve a ejecutar esta celda.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Catalina/path_signature_esig_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

# split holdout 80/20 reproducible
data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out


def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print("\n" + "═" * 80)
    print(f"{nombre} #{idx} | params = {params}")
    print("─" * 80)

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

    print("═" * 80)

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1510, 1023)
X_test : (377, 1023)
y_train: (1510,)
y_test : (377,)
Ver código
import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("TOP 5 (según AUC CV)")
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    80)
    print(name)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("EVALUACIÓN TOP-5: CV (AUC) en TRAIN")

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )

    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean

    rf.fit(X_train, y_train_encoded)

    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

110)
print("RESUMEN")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

#matrices de confusión

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_  # AGN, Blazar, QSO

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF1_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF1_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Revisar X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

# Si parte en 0, lo pasamos a 1..N
if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1)) - 2  

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]


print("RESULTADOS POR NIVEL — criterio AUC_test")

print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))


print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test")

print(best_simple.to_string())


print("FILAS LaTeX (sin AUC)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")


print("FILAS LaTeX")


for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                      params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0  {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}         0.604712        0.026122          0.944088    0.339375                1
1     {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}         0.603181        0.032051          0.934251    0.331070                2
2  {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}         0.603108        0.024413          0.925421    0.322313                3
3   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}         0.603025        0.032304          0.942723    0.339698                4
4  {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6, 'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}         0.602976        0.026780          0.881134    0.278158                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     343      20   59
Blazar    4     144   41
QSO      24      34  841

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      69       7   25
Blazar    2      22    6
QSO       8      19  219

MÉTRICAS (train)
  Accuracy : 0.879
  Precision: 0.882
  Recall   : 0.879
  F1-score : 0.879
  AUC      : 0.946

MÉTRICAS (test)
  Accuracy : 0.822
  Precision: 0.842
  Recall   : 0.822
  F1-score : 0.826
  AUC      : 0.878
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      69       7   25
Blazar    2      22    6
QSO       8      19  219

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.87      0.68      0.77       101
      Blazar       0.46      0.73      0.56        30
         QSO       0.88      0.89      0.88       246

    accuracy                           0.82       377
   macro avg       0.74      0.77      0.74       377
weighted avg       0.84      0.82      0.83       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     307      18   97
Blazar   15     118   56
QSO      25      34  840

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      61       7   33
Blazar    6      21    3
QSO      11      19  216

MÉTRICAS (train)
  Accuracy : 0.838
  Precision: 0.838
  Recall   : 0.838
  F1-score : 0.834
  AUC      : 0.935

MÉTRICAS (test)
  Accuracy : 0.790
  Precision: 0.804
  Recall   : 0.790
  F1-score : 0.792
  AUC      : 0.861
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      61       7   33
Blazar    6      21    3
QSO      11      19  216

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.78      0.60      0.68       101
      Blazar       0.45      0.70      0.55        30
         QSO       0.86      0.88      0.87       246

    accuracy                           0.79       377
   macro avg       0.70      0.73      0.70       377
weighted avg       0.80      0.79      0.79       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     299      20  103
Blazar    7     128   54
QSO      22      33  844

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      62       7   32
Blazar    5      21    4
QSO       7      19  220

MÉTRICAS (train)
  Accuracy : 0.842
  Precision: 0.845
  Recall   : 0.842
  F1-score : 0.838
  AUC      : 0.928

MÉTRICAS (test)
  Accuracy : 0.804
  Precision: 0.821
  Recall   : 0.804
  F1-score : 0.805
  AUC      : 0.862
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      62       7   32
Blazar    5      21    4
QSO       7      19  220

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.84      0.61      0.71       101
      Blazar       0.45      0.70      0.55        30
         QSO       0.86      0.89      0.88       246

    accuracy                           0.80       377
   macro avg       0.71      0.74      0.71       377
weighted avg       0.82      0.80      0.81       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     311      16   95
Blazar   14     122   53
QSO      24      29  846

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      62       7   32
Blazar    6      21    3
QSO      10      17  219

MÉTRICAS (train)
  Accuracy : 0.847
  Precision: 0.847
  Recall   : 0.847
  F1-score : 0.843
  AUC      : 0.943

MÉTRICAS (test)
  Accuracy : 0.801
  Precision: 0.813
  Recall   : 0.801
  F1-score : 0.802
  AUC      : 0.867
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      62       7   32
Blazar    6      21    3
QSO      10      17  219

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.79      0.61      0.69       101
      Blazar       0.47      0.70      0.56        30
         QSO       0.86      0.89      0.88       246

    accuracy                           0.80       377
   macro avg       0.71      0.73      0.71       377
weighted avg       0.81      0.80      0.80       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6, 'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     273      24  125
Blazar   10     123   56
QSO      46      39  814

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      54       8   39
Blazar    5      21    4
QSO       7      22  217

MÉTRICAS (train)
  Accuracy : 0.801
  Precision: 0.802
  Recall   : 0.801
  F1-score : 0.797
  AUC      : 0.885

MÉTRICAS (test)
  Accuracy : 0.775
  Precision: 0.797
  Recall   : 0.775
  F1-score : 0.774
  AUC      : 0.835
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6, 'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      54       8   39
Blazar    5      21    4
QSO       7      22  217

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.82      0.53      0.65       101
      Blazar       0.41      0.70      0.52        30
         QSO       0.83      0.88      0.86       246

    accuracy                           0.77       377
   macro avg       0.69      0.71      0.67       377
weighted avg       0.80      0.77      0.77       377


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                    params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top1 {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}        0.604712       0.026122       0.339375           0.943674         0.602452        0.022391    0.341222   0.945606  0.878162  0.822281          0.826499
RF_rob_top4  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}        0.603025       0.032304       0.339698           0.941899         0.601817        0.024236    0.340083   0.943140  0.867113  0.801061          0.801757
RF_rob_top3 {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}        0.603108       0.024413       0.322313           0.924656         0.601931        0.022635    0.322724   0.928358  0.862410  0.803714          0.805164
RF_rob_top2    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}        0.603181       0.032051       0.331070           0.933575         0.601879        0.024322    0.331695   0.935360  0.860887  0.790451          0.792040
RF_rob_top5 {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6, 'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}        0.602976       0.026780       0.278158           0.879719         0.599641        0.020737    0.280078   0.885222  0.834958  0.774536          0.774188

Tiempo total: 19:07:55
✅ Guardado: /home/felorrieta/Descargas/RF1_1.png
✅ Guardado: /home/felorrieta/Descargas/RF1_2.png
✅ Guardado: /home/felorrieta/Descargas/RF1_3.png
✅ Guardado: /home/felorrieta/Descargas/RF1_4.png
✅ Guardado: /home/felorrieta/Descargas/RF1_5.png

Tiempo total guardando figuras: 00:20:03
Rango original columnas: 0 .. 1022
Detectado 0-based -> usando idx_1based = idx + 1
Rango idx_1based: 1 .. 1023

IMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)
nivel  n_features  importancia_sum  importancia_pct  importancia_prom
   N0           1         0.000000         0.000000          0.000000
   N1           2         0.000236         0.023620          0.000118
   N2           4         0.002477         0.247744          0.000619
   N3           8         0.006087         0.608676          0.000761
   N4          16         0.009974         0.997441          0.000623
   N5          32         0.019125         1.912526          0.000598
   N6          64         0.033415         3.341497          0.000522
   N7         128         0.107770        10.777009          0.000842
   N8         256         0.214030        21.402962          0.000836
   N9         512         0.606885        60.688526          0.001185


Tiempo total: 00:08:08

===============================================================================================
RESULTADOS POR NIVEL — criterio AUC_test
===============================================================================================
 NivelFirma  N_features  AccTrain  F1Train  AUCTrain  AccTest  F1Test  AUCTest
          1           2     0.580    0.570     0.670    0.597   0.588    0.635
          2           6     0.636    0.635     0.749    0.589   0.594    0.689
          3          14     0.725    0.721     0.824    0.660   0.658    0.746
          4          30     0.766    0.763     0.859    0.690   0.695    0.772
          5          62     0.797    0.795     0.879    0.727   0.733    0.808
          6         126     0.825    0.823     0.903    0.753   0.758    0.829
          7         254     0.838    0.837     0.917    0.769   0.772    0.846
          8         510     0.864    0.863     0.933    0.801   0.805    0.864
          9        1022     0.879    0.879     0.946    0.820   0.823    0.877

===============================================================================================
NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test (con tolerancia)
===============================================================================================
NivelFirma       9.000000
N_features    1022.000000
AccTrain         0.879470
F1Train          0.879133
AUCTrain         0.945956
AccTest          0.819629
F1Test           0.823494
AUCTest          0.876826

===============================================================================================
FILAS LaTeX (tabla ORIGINAL: sin AUC)
===============================================================================================
1 & 2 & 0.580 & 0.570 & 0.597 & 0.588 \\
2 & 6 & 0.636 & 0.635 & 0.589 & 0.594 \\
3 & 14 & 0.725 & 0.721 & 0.660 & 0.658 \\
4 & 30 & 0.766 & 0.763 & 0.690 & 0.695 \\
5 & 62 & 0.797 & 0.795 & 0.727 & 0.733 \\
6 & 126 & 0.825 & 0.823 & 0.753 & 0.758 \\
7 & 254 & 0.838 & 0.837 & 0.769 & 0.772 \\
8 & 510 & 0.864 & 0.863 & 0.801 & 0.805 \\
9 & 1022 & 0.879 & 0.879 & 0.820 & 0.823 \\

===============================================================================================
FILAS LaTeX (tabla EXTENDIDA: incluye AUC_test)
===============================================================================================
1 & 2 & 0.580 & 0.570 & 0.597 & 0.588 & 0.635 \\
2 & 6 & 0.636 & 0.635 & 0.589 & 0.594 & 0.689 \\
3 & 14 & 0.725 & 0.721 & 0.660 & 0.658 & 0.746 \\
4 & 30 & 0.766 & 0.763 & 0.690 & 0.695 & 0.772 \\
5 & 62 & 0.797 & 0.795 & 0.727 & 0.733 & 0.808 \\
6 & 126 & 0.825 & 0.823 & 0.753 & 0.758 & 0.829 \\
7 & 254 & 0.838 & 0.837 & 0.769 & 0.772 & 0.846 \\
8 & 510 & 0.864 & 0.863 & 0.801 & 0.805 & 0.864 \\
9 & 1022 & 0.879 & 0.879 & 0.820 & 0.823 & 0.877 \\

1.1.2 RANDOM FOREST - LOG SIGNATURE

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Catalina/logsignature_esig_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out


def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print("\n" + "═" * 80)
    print(f"{nombre} #{idx} | params = {params}")
    print("─" * 80)

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

    print("═" * 80)

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0


def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1510, 127)
X_test : (377, 127)
y_train: (1510,)
y_test : (377,)
Ver código
import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("\n" + "#" * 80)
print("TOP 5 (según AUC CV)")
print("#" * 80)
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    80)
    print(name)
    print("=" * 80)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("\n" + "#" * 80)
print("EVALUACIÓN TOP-5: CV (AUC) en TRAIN")
print("#" * 80)

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )

    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean

    # Fit final
    rf.fit(X_train, y_train_encoded)

    # Métricas holdout con tu función
    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    # AUC holdout explícita
    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

110)
print("RESUMEN (ordenado por AUC_test, luego CV val AUC)")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

#matrices de confusión

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_ 

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF1_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF1_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Revisar X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

# Si parte en 0, lo pasamos a 1..N
if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  # N0..N9

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1))

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]


print("RESULTADOS POR NIVEL — criterio AUC test")

print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))


print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC test")

print(best_simple.to_string())


print("FILAS LaTeXsin AUC)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")


print("FILAS LaTeX")


for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                     params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}         0.579481        0.017433          0.920948    0.341467                1
1  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}         0.578393        0.015500          0.901905    0.323511                2
2     {'criterion': 'gini', 'max_depth': 10, 'max_features': 0.4, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 72, 'n_estimators': 801}         0.578309        0.017571          0.832686    0.254377                3
3    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}         0.578086        0.017740          0.892229    0.314144                4
4    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}         0.578074        0.016370          0.871922    0.293848                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     312      15   95
Blazar    5     117   67
QSO      35      38  826

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      59       7   35
Blazar    2      19    9
QSO      17      13  216

MÉTRICAS (train)
  Accuracy : 0.831
  Precision: 0.832
  Recall   : 0.831
  F1-score : 0.828
  AUC      : 0.923

MÉTRICAS (test)
  Accuracy : 0.780
  Precision: 0.784
  Recall   : 0.780
  F1-score : 0.778
  AUC      : 0.852
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      59       7   35
Blazar    2      19    9
QSO      17      13  216

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.76      0.58      0.66       101
      Blazar       0.49      0.63      0.55        30
         QSO       0.83      0.88      0.85       246

    accuracy                           0.78       377
   macro avg       0.69      0.70      0.69       377
weighted avg       0.78      0.78      0.78       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     300      19  103
Blazar    7     106   76
QSO      40      42  817

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      54       7   40
Blazar    2      18   10
QSO      17      18  211

MÉTRICAS (train)
  Accuracy : 0.810
  Precision: 0.809
  Recall   : 0.810
  F1-score : 0.806
  AUC      : 0.903

MÉTRICAS (test)
  Accuracy : 0.751
  Precision: 0.759
  Recall   : 0.751
  F1-score : 0.749
  AUC      : 0.831
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      54       7   40
Blazar    2      18   10
QSO      17      18  211

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.74      0.53      0.62       101
      Blazar       0.42      0.60      0.49        30
         QSO       0.81      0.86      0.83       246

    accuracy                           0.75       377
   macro avg       0.66      0.66      0.65       377
weighted avg       0.76      0.75      0.75       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'gini', 'max_depth': 10, 'max_features': 0.4, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 72, 'n_estimators': 801}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     246      40  136
Blazar    8     106   75
QSO      84      68  747

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      47      12   42
Blazar    3      18    9
QSO      28      23  195

MÉTRICAS (train)
  Accuracy : 0.728
  Precision: 0.730
  Recall   : 0.728
  F1-score : 0.726
  AUC      : 0.832

MÉTRICAS (test)
  Accuracy : 0.690
  Precision: 0.706
  Recall   : 0.690
  F1-score : 0.692
  AUC      : 0.782
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'gini', 'max_depth': 10, 'max_features': 0.4, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 72, 'n_estimators': 801}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      47      12   42
Blazar    3      18    9
QSO      28      23  195

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.60      0.47      0.53       101
      Blazar       0.34      0.60      0.43        30
         QSO       0.79      0.79      0.79       246

    accuracy                           0.69       377
   macro avg       0.58      0.62      0.58       377
weighted avg       0.71      0.69      0.69       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     287      21  114
Blazar    6     108   75
QSO      47      47  805

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      54       8   39
Blazar    2      17   11
QSO      19      18  209

MÉTRICAS (train)
  Accuracy : 0.795
  Precision: 0.795
  Recall   : 0.795
  F1-score : 0.791
  AUC      : 0.896

MÉTRICAS (test)
  Accuracy : 0.743
  Precision: 0.751
  Recall   : 0.743
  F1-score : 0.742
  AUC      : 0.825
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      54       8   39
Blazar    2      17   11
QSO      19      18  209

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.72      0.53      0.61       101
      Blazar       0.40      0.57      0.47        30
         QSO       0.81      0.85      0.83       246

    accuracy                           0.74       377
   macro avg       0.64      0.65      0.64       377
weighted avg       0.75      0.74      0.74       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     289      31  102
Blazar    7     109   73
QSO      66      61  772

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      54      10   37
Blazar    3      19    8
QSO      22      22  202

MÉTRICAS (train)
  Accuracy : 0.775
  Precision: 0.776
  Recall   : 0.775
  F1-score : 0.774
  AUC      : 0.875

MÉTRICAS (test)
  Accuracy : 0.729
  Precision: 0.746
  Recall   : 0.729
  F1-score : 0.733
  AUC      : 0.812
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      54      10   37
Blazar    3      19    8
QSO      22      22  202

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.68      0.53      0.60       101
      Blazar       0.37      0.63      0.47        30
         QSO       0.82      0.82      0.82       246

    accuracy                           0.73       377
   macro avg       0.62      0.66      0.63       377
weighted avg       0.75      0.73      0.73       377


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                   params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top1  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}        0.579481       0.017433       0.341467           0.920691         0.577086        0.019963    0.343604   0.922847  0.851783  0.779841          0.777524
RF_rob_top2 {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}        0.578393       0.015500       0.323511           0.901716         0.576501        0.018724    0.325215   0.903391  0.831009  0.750663          0.748651
RF_rob_top4   {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}        0.578086       0.017740       0.314144           0.892344         0.576562        0.019781    0.315782   0.895611  0.825366  0.742706          0.741564
RF_rob_top5   {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}        0.578074       0.016370       0.293848           0.872191         0.576538        0.018167    0.295653   0.875301  0.812163  0.729443          0.732797
RF_rob_top3    {'criterion': 'gini', 'max_depth': 10, 'max_features': 0.4, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 72, 'n_estimators': 801}        0.578309       0.017571       0.254377           0.832074         0.574618        0.017425    0.257456   0.831554  0.782260  0.689655          0.692443

Tiempo total: 02:18:26
✅ Guardado: /home/felorrieta/Descargas/RF1_1.png
✅ Guardado: /home/felorrieta/Descargas/RF1_2.png
✅ Guardado: /home/felorrieta/Descargas/RF1_3.png
✅ Guardado: /home/felorrieta/Descargas/RF1_4.png
✅ Guardado: /home/felorrieta/Descargas/RF1_5.png

Tiempo total guardando figuras: 00:02:33
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 382
    380 feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
    381 if feat_idx.isna().any():
--> 382     raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")
    384 feat_idx = feat_idx.astype(int)
    385 min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())

ValueError: Tus columnas no se pudieron convertir a números. Revisa X_train.columns.

1.2 IISIGNATURE

1.2.1 RANDOM FOREST - PATH SIGNATURE

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Catalina/path_signature_iisig_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out


def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print("\n" + "═" * 80)
    print(f"{nombre} #{idx} | params = {params}")
    print("─" * 80)

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

    print("═" * 80)

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1510, 1022)
X_test : (377, 1022)
y_train: (1510,)
y_test : (377,)
Ver código
import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("\n" + "#" * 80)
print("TOP 5 (según AUC CV)")
print("#" * 80)
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    80)
    print(name)
    print("=" * 80)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("\n" + "#" * 80)
print("EVALUACIÓN TOP-5: CV (AUC) en TRAIN")
print("#" * 80)

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )

    # CV repetida en TRAIN
    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean

    # Fit final
    rf.fit(X_train, y_train_encoded)

    # Métricas holdout con tu función
    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    # AUC holdout explícita
    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

110)
print("RESUMEN (ordenado por AUC_test, luego CV val AUC)")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_ 

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF1_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF1_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Revisar X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  # N0..N9

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1)) - 2  # 2,6,14,...,1022

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]


print("RESULTADOS POR NIVEL — criterio AUC_test")

print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))


print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test (con tolerancia)")

print(best_simple.to_string())


print("FILAS LaTeX (tabla ORIGINAL: sin AUC)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")


print("FILAS LaTeX (tabla EXTENDIDA: incluye AUC_test)")


for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                      params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0  {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}         0.601896        0.019595          0.947298    0.345402                1
1    {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}         0.599491        0.025986          0.957978    0.358487                2
2  {'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}         0.599145        0.022419          0.893454    0.294309                3
3   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}         0.598769        0.025092          0.946359    0.347590                4
4  {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}         0.598361        0.019640          0.928591    0.330231                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     349      19   54
Blazar    5     140   44
QSO      17      30  852

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      64       8   29
Blazar    2      23    5
QSO      10      18  218

MÉTRICAS (train)
  Accuracy : 0.888
  Precision: 0.890
  Recall   : 0.888
  F1-score : 0.887
  AUC      : 0.950

MÉTRICAS (test)
  Accuracy : 0.809
  Precision: 0.827
  Recall   : 0.809
  F1-score : 0.811
  AUC      : 0.872
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      64       8   29
Blazar    2      23    5
QSO      10      18  218

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.84      0.63      0.72       101
      Blazar       0.47      0.77      0.58        30
         QSO       0.87      0.89      0.88       246

    accuracy                           0.81       377
   macro avg       0.73      0.76      0.73       377
weighted avg       0.83      0.81      0.81       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     334      12   76
Blazar    9     130   50
QSO      13      21  865

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      65       7   29
Blazar    3      23    4
QSO       9      13  224

MÉTRICAS (train)
  Accuracy : 0.880
  Precision: 0.882
  Recall   : 0.880
  F1-score : 0.877
  AUC      : 0.960

MÉTRICAS (test)
  Accuracy : 0.828
  Precision: 0.837
  Recall   : 0.828
  F1-score : 0.827
  AUC      : 0.880
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      65       7   29
Blazar    3      23    4
QSO       9      13  224

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.84      0.64      0.73       101
      Blazar       0.53      0.77      0.63        30
         QSO       0.87      0.91      0.89       246

    accuracy                           0.83       377
   macro avg       0.75      0.77      0.75       377
weighted avg       0.84      0.83      0.83       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     292      24  106
Blazar    9     122   58
QSO      29      40  830

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      57       8   36
Blazar    4      21    5
QSO       9      23  214

MÉTRICAS (train)
  Accuracy : 0.824
  Precision: 0.827
  Recall   : 0.824
  F1-score : 0.821
  AUC      : 0.898

MÉTRICAS (test)
  Accuracy : 0.775
  Precision: 0.798
  Recall   : 0.775
  F1-score : 0.777
  AUC      : 0.830
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      57       8   36
Blazar    4      21    5
QSO       9      23  214

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.81      0.56      0.67       101
      Blazar       0.40      0.70      0.51        30
         QSO       0.84      0.87      0.85       246

    accuracy                           0.77       377
   macro avg       0.69      0.71      0.68       377
weighted avg       0.80      0.77      0.78       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     319      16   87
Blazar   14     119   56
QSO      21      22  856

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      61       8   32
Blazar    5      21    4
QSO      13      14  219

MÉTRICAS (train)
  Accuracy : 0.857
  Precision: 0.857
  Recall   : 0.857
  F1-score : 0.853
  AUC      : 0.948

MÉTRICAS (test)
  Accuracy : 0.798
  Precision: 0.806
  Recall   : 0.798
  F1-score : 0.798
  AUC      : 0.863
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      61       8   32
Blazar    5      21    4
QSO      13      14  219

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.77      0.60      0.68       101
      Blazar       0.49      0.70      0.58        30
         QSO       0.86      0.89      0.87       246

    accuracy                           0.80       377
   macro avg       0.71      0.73      0.71       377
weighted avg       0.81      0.80      0.80       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     309      22   91
Blazar    5     133   51
QSO      16      31  852

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      64       8   29
Blazar    2      23    5
QSO       9      20  217

MÉTRICAS (train)
  Accuracy : 0.857
  Precision: 0.861
  Recall   : 0.857
  F1-score : 0.854
  AUC      : 0.933

MÉTRICAS (test)
  Accuracy : 0.806
  Precision: 0.829
  Recall   : 0.806
  F1-score : 0.810
  AUC      : 0.859
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      64       8   29
Blazar    2      23    5
QSO       9      20  217

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.85      0.63      0.73       101
      Blazar       0.45      0.77      0.57        30
         QSO       0.86      0.88      0.87       246

    accuracy                           0.81       377
   macro avg       0.72      0.76      0.72       377
weighted avg       0.83      0.81      0.81       377


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                    params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top2   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}        0.599491       0.025986       0.358487           0.957538         0.596461        0.024224    0.361077   0.960134  0.879779  0.827586          0.826975
RF_rob_top1 {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}        0.601896       0.019595       0.345402           0.947036         0.597011        0.021125    0.350026   0.950267  0.871648  0.809019          0.811356
RF_rob_top4  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}        0.598769       0.025092       0.347590           0.945641         0.595155        0.023775    0.350486   0.948052  0.863273  0.798408          0.797829
RF_rob_top5 {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}        0.598361       0.019640       0.330231           0.927529         0.595604        0.021060    0.331925   0.932678  0.859231  0.806366          0.809837
RF_rob_top3 {'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}        0.599145       0.022419       0.294309           0.892269         0.594925        0.020142    0.297344   0.898257  0.829610  0.774536          0.776803

Tiempo total: 19:21:10
✅ Guardado: /home/felorrieta/Downloads/RF1_1.png
✅ Guardado: /home/felorrieta/Downloads/RF1_2.png
✅ Guardado: /home/felorrieta/Downloads/RF1_3.png
✅ Guardado: /home/felorrieta/Downloads/RF1_4.png
✅ Guardado: /home/felorrieta/Downloads/RF1_5.png

Tiempo total guardando figuras: 00:20:00
Rango original columnas: 0 .. 1021
Detectado 0-based -> usando idx_1based = idx + 1
Rango idx_1based: 1 .. 1022

IMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)
nivel  n_features  importancia_sum  importancia_pct  importancia_prom
   N0           1         0.000148         0.014757          0.000148
   N1           2         0.000434         0.043384          0.000217
   N2           4         0.002589         0.258878          0.000647
   N3           8         0.006506         0.650560          0.000813
   N4          16         0.008798         0.879780          0.000550
   N5          32         0.019152         1.915228          0.000599
   N6          64         0.035305         3.530521          0.000552
   N7         128         0.104954        10.495411          0.000820
   N8         256         0.212819        21.281920          0.000831
   N9         511         0.609296        60.929561          0.001192


Tiempo total: 00:08:04

===============================================================================================
RESULTADOS POR NIVEL — criterio AUC_test
===============================================================================================
 NivelFirma  N_features  AccTrain  F1Train  AUCTrain  AccTest  F1Test  AUCTest
          1           2     0.579    0.571     0.670    0.594   0.585    0.636
          2           6     0.632    0.631     0.749    0.584   0.591    0.689
          3          14     0.715    0.710     0.816    0.655   0.653    0.741
          4          30     0.752    0.747     0.855    0.682   0.686    0.773
          5          62     0.797    0.794     0.883    0.719   0.726    0.802
          6         126     0.825    0.823     0.908    0.748   0.752    0.823
          7         254     0.846    0.845     0.921    0.761   0.768    0.840
          8         510     0.868    0.867     0.939    0.785   0.791    0.857
          9        1021     0.887    0.886     0.950    0.814   0.817    0.873

===============================================================================================
NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test (con tolerancia)
===============================================================================================
NivelFirma       9.000000
N_features    1021.000000
AccTrain         0.886755
F1Train          0.886097
AUCTrain         0.950164
AccTest          0.814324
F1Test           0.817482
AUCTest          0.873239

===============================================================================================
FILAS LaTeX (tabla ORIGINAL: sin AUC)
===============================================================================================
1 & 2 & 0.579 & 0.571 & 0.594 & 0.585 \\
2 & 6 & 0.632 & 0.631 & 0.584 & 0.591 \\
3 & 14 & 0.715 & 0.710 & 0.655 & 0.653 \\
4 & 30 & 0.752 & 0.747 & 0.682 & 0.686 \\
5 & 62 & 0.797 & 0.794 & 0.719 & 0.726 \\
6 & 126 & 0.825 & 0.823 & 0.748 & 0.752 \\
7 & 254 & 0.846 & 0.845 & 0.761 & 0.768 \\
8 & 510 & 0.868 & 0.867 & 0.785 & 0.791 \\
9 & 1021 & 0.887 & 0.886 & 0.814 & 0.817 \\

===============================================================================================
FILAS LaTeX (tabla EXTENDIDA: incluye AUC_test)
===============================================================================================
1 & 2 & 0.579 & 0.571 & 0.594 & 0.585 & 0.636 \\
2 & 6 & 0.632 & 0.631 & 0.584 & 0.591 & 0.689 \\
3 & 14 & 0.715 & 0.710 & 0.655 & 0.653 & 0.741 \\
4 & 30 & 0.752 & 0.747 & 0.682 & 0.686 & 0.773 \\
5 & 62 & 0.797 & 0.794 & 0.719 & 0.726 & 0.802 \\
6 & 126 & 0.825 & 0.823 & 0.748 & 0.752 & 0.823 \\
7 & 254 & 0.846 & 0.845 & 0.761 & 0.768 & 0.840 \\
8 & 510 & 0.868 & 0.867 & 0.785 & 0.791 & 0.857 \\
9 & 1021 & 0.887 & 0.886 & 0.814 & 0.817 & 0.873 \\

1.2.2 RANDOM FOREST - LOG SIGNATURE

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada. Si algo sigue fallando, vuelve a ejecutar esta celda.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Catalina/logsignature_iisig_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out

def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print("\n" + "═" * 80)
    print(f"{nombre} #{idx} | params = {params}")
    print("─" * 80)

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

    print("═" * 80)

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0


def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1032, 127)
X_test : (258, 127)
y_train: (1032,)
y_test : (258,)
Ver código
import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("TOP 5 (según AUC CV)")
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    print(name)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("EVALUACIÓN TOP-5: CV (AUC) en TRAIN")

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )

    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean

    rf.fit(X_train, y_train_encoded)

    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

110)
print("RESUMEN (ordenado por AUC_test, luego CV val AUC)")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

#matrices de confusión

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_ 

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF1_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF1_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  # N0..N9

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1)) - 2 

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]


print("RESULTADOS POR NIVEL — AUC test")

print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))


print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test")

print(best_simple.to_string())


print("FILAS LaTeX (sin AUC)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")


print("FILAS LaTeX")


for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                       params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0     {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}         0.555453        0.011409          0.935690    0.380237                1
1   {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}         0.554111        0.006018          0.884536    0.330425                2
2   {'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}         0.554006        0.006747          0.838837    0.284832                3
3  {'criterion': 'entropy', 'max_depth': 18, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 14, 'min_samples_split': 177, 'n_estimators': 1520}         0.553503        0.015575          0.678337    0.124833                4
4    {'criterion': 'entropy', 'max_depth': 7, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 21, 'min_samples_split': 105, 'n_estimators': 725}         0.553313        0.011126          0.671496    0.118182                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     213       5   66
Blazar    4      75   47
QSO      15      14  593

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      45       3   26
Blazar    2      24   14
QSO       9       9  126

MÉTRICAS (train)
  Accuracy : 0.854
  Precision: 0.856
  Recall   : 0.854
  F1-score : 0.849
  AUC      : 0.940

MÉTRICAS (test)
  Accuracy : 0.756
  Precision: 0.757
  Recall   : 0.756
  F1-score : 0.750
  AUC      : 0.849
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      45       3   26
Blazar    2      24   14
QSO       9       9  126

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.80      0.61      0.69        74
      Blazar       0.67      0.60      0.63        40
         QSO       0.76      0.88      0.81       144

    accuracy                           0.76       258
   macro avg       0.74      0.69      0.71       258
weighted avg       0.76      0.76      0.75       258


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     194       7   83
Blazar    3      75   48
QSO      25      21  576

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      42       3   29
Blazar    1      25   14
QSO       9      11  124

MÉTRICAS (train)
  Accuracy : 0.819
  Precision: 0.820
  Recall   : 0.819
  F1-score : 0.813
  AUC      : 0.891

MÉTRICAS (test)
  Accuracy : 0.740
  Precision: 0.745
  Recall   : 0.740
  F1-score : 0.734
  AUC      : 0.814
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      42       3   29
Blazar    1      25   14
QSO       9      11  124

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.81      0.57      0.67        74
      Blazar       0.64      0.62      0.63        40
         QSO       0.74      0.86      0.80       144

    accuracy                           0.74       258
   macro avg       0.73      0.68      0.70       258
weighted avg       0.75      0.74      0.73       258


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     167      14  103
Blazar    2      73   51
QSO      33      25  564

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      32       6   36
Blazar    1      24   15
QSO       9      15  120

MÉTRICAS (train)
  Accuracy : 0.779
  Precision: 0.781
  Recall   : 0.779
  F1-score : 0.771
  AUC      : 0.848

MÉTRICAS (test)
  Accuracy : 0.682
  Precision: 0.693
  Recall   : 0.682
  F1-score : 0.671
  AUC      : 0.782
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      32       6   36
Blazar    1      24   15
QSO       9      15  120

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.76      0.43      0.55        74
      Blazar       0.53      0.60      0.56        40
         QSO       0.70      0.83      0.76       144

    accuracy                           0.68       258
   macro avg       0.67      0.62      0.63       258
weighted avg       0.69      0.68      0.67       258


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'entropy', 'max_depth': 18, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 14, 'min_samples_split': 177, 'n_estimators': 1520}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN      72      28  184
Blazar    0      53   73
QSO      37      34  551

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      16      12   46
Blazar    0      22   18
QSO       9      18  117

MÉTRICAS (train)
  Accuracy : 0.655
  Precision: 0.649
  Recall   : 0.655
  F1-score : 0.619
  AUC      : 0.685

MÉTRICAS (test)
  Accuracy : 0.601
  Precision: 0.610
  Recall   : 0.601
  F1-score : 0.569
  AUC      : 0.696
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'entropy', 'max_depth': 18, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 14, 'min_samples_split': 177, 'n_estimators': 1520}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      16      12   46
Blazar    0      22   18
QSO       9      18  117

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.64      0.22      0.32        74
      Blazar       0.42      0.55      0.48        40
         QSO       0.65      0.81      0.72       144

    accuracy                           0.60       258
   macro avg       0.57      0.53      0.51       258
weighted avg       0.61      0.60      0.57       258


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'entropy', 'max_depth': 7, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 21, 'min_samples_split': 105, 'n_estimators': 725}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN      66      18  200
Blazar    1      38   87
QSO      36      23  563

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      15       6   53
Blazar    1      17   22
QSO       9      14  121

MÉTRICAS (train)
  Accuracy : 0.646
  Precision: 0.634
  Recall   : 0.646
  F1-score : 0.600
  AUC      : 0.678

MÉTRICAS (test)
  Accuracy : 0.593
  Precision: 0.588
  Recall   : 0.593
  F1-score : 0.553
  AUC      : 0.684
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'entropy', 'max_depth': 7, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 21, 'min_samples_split': 105, 'n_estimators': 725}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      15       6   53
Blazar    1      17   22
QSO       9      14  121

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.60      0.20      0.30        74
      Blazar       0.46      0.42      0.44        40
         QSO       0.62      0.84      0.71       144

    accuracy                           0.59       258
   macro avg       0.56      0.49      0.49       258
weighted avg       0.59      0.59      0.55       258


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                     params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top1    {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}        0.555453       0.011409       0.380237           0.935911         0.553256        0.018436    0.382655   0.939622  0.849138  0.755814          0.750202
RF_rob_top2  {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}        0.554111       0.006018       0.330425           0.885213         0.550939        0.015032    0.334274   0.891183  0.813645  0.740310          0.734416
RF_rob_top3  {'criterion': 'entropy', 'max_depth': 17, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 18, 'min_samples_split': 47, 'n_estimators': 2725}        0.554006       0.006747       0.284832           0.841488         0.551483        0.014031    0.290004   0.848256  0.781770  0.682171          0.671047
RF_rob_top4 {'criterion': 'entropy', 'max_depth': 18, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 14, 'min_samples_split': 177, 'n_estimators': 1520}        0.553503       0.015575       0.124833           0.678265         0.554163        0.015123    0.124102   0.684580  0.696405  0.600775          0.568719
RF_rob_top5   {'criterion': 'entropy', 'max_depth': 7, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 21, 'min_samples_split': 105, 'n_estimators': 725}        0.553313       0.011126       0.118182           0.671651         0.551989        0.014795    0.119661   0.677814  0.683639  0.593023          0.552638

Tiempo total: 01:18:35
✅ Guardado: /home/felorrieta/Downloads/RF1_1.png
✅ Guardado: /home/felorrieta/Downloads/RF1_2.png
✅ Guardado: /home/felorrieta/Downloads/RF1_3.png
✅ Guardado: /home/felorrieta/Downloads/RF1_4.png
✅ Guardado: /home/felorrieta/Downloads/RF1_5.png

Tiempo total guardando figuras: 00:01:35
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[9], line 382
    380 feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
    381 if feat_idx.isna().any():
--> 382     raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")
    384 feat_idx = feat_idx.astype(int)
    385 min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())

ValueError: Tus columnas no se pudieron convertir a números. Revisa X_train.columns.

2 CURVAS REALES

2.1 ESIG

2.1.1 RANDOM FOREST - SIGNATURE

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Catalina/path_signature_esig_REALES_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out

def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print("\n" + "═" * 80)
    print(f"{nombre} #{idx} | params = {params}")
    print("─" * 80)

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

    print("═" * 80)

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1438, 1023)
X_test : (359, 1023)
y_train: (1438,)
y_test : (359,)
Ver código
import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("TOP 5 (según AUC CV)")
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    80)
    print(name)
    print("=" * 80)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("\n" + "#" * 80)
print("EVALUACIÓN TOP-5: CV (AUC) en TRAIN")
print("#" * 80)

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )

    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean

    rf.fit(X_train, y_train_encoded)

    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

110)
print("RESUMEN (ordenado por AUC_test, luego CV val AUC)")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

#matrices de confusión

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_ 

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF1_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF1_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Revisar X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1)) - 2 

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]


print("RESULTADOS POR NIVEL — criterio AUC test")

print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))


print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC test")

print(best_simple.to_string())


print("FILAS LaTeX (sin AUC)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")

print("FILAS LaTeX")


for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                     params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}         0.754485        0.024814          0.920010    0.165525                1
1   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}         0.753650        0.024988          0.933676    0.180026                2
2    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}         0.752972        0.023791          0.905882    0.152910                3
3    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}         0.752820        0.025653          0.913352    0.160532                4
4    {'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}         0.752344        0.025595          0.886468    0.134124                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     290      19   91
Blazar   27     127   32
QSO      83      32  737

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      58       5   32
Blazar    6      19    4
QSO      24       8  203

MÉTRICAS (train)
  Accuracy : 0.803
  Precision: 0.802
  Recall   : 0.803
  F1-score : 0.802
  AUC      : 0.922

MÉTRICAS (test)
  Accuracy : 0.780
  Precision: 0.778
  Recall   : 0.780
  F1-score : 0.779
  AUC      : 0.878
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      58       5   32
Blazar    6      19    4
QSO      24       8  203

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.66      0.61      0.63        95
      Blazar       0.59      0.66      0.62        29
         QSO       0.85      0.86      0.86       235

    accuracy                           0.78       359
   macro avg       0.70      0.71      0.70       359
weighted avg       0.78      0.78      0.78       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     304      18   78
Blazar   18     137   31
QSO      71      33  748

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      59       6   30
Blazar    5      20    4
QSO      23       6  206

MÉTRICAS (train)
  Accuracy : 0.827
  Precision: 0.827
  Recall   : 0.827
  F1-score : 0.827
  AUC      : 0.937

MÉTRICAS (test)
  Accuracy : 0.794
  Precision: 0.792
  Recall   : 0.794
  F1-score : 0.792
  AUC      : 0.891
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      59       6   30
Blazar    5      20    4
QSO      23       6  206

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.68      0.62      0.65        95
      Blazar       0.62      0.69      0.66        29
         QSO       0.86      0.88      0.87       235

    accuracy                           0.79       359
   macro avg       0.72      0.73      0.72       359
weighted avg       0.79      0.79      0.79       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     289      20   91
Blazar   31     125   30
QSO      93      38  721

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      60       6   29
Blazar    6      19    4
QSO      27       8  200

MÉTRICAS (train)
  Accuracy : 0.789
  Precision: 0.790
  Recall   : 0.789
  F1-score : 0.790
  AUC      : 0.910

MÉTRICAS (test)
  Accuracy : 0.777
  Precision: 0.779
  Recall   : 0.777
  F1-score : 0.778
  AUC      : 0.867
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      60       6   29
Blazar    6      19    4
QSO      27       8  200

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.65      0.63      0.64        95
      Blazar       0.58      0.66      0.61        29
         QSO       0.86      0.85      0.85       235

    accuracy                           0.78       359
   macro avg       0.69      0.71      0.70       359
weighted avg       0.78      0.78      0.78       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     287      21   92
Blazar   27     129   30
QSO      85      37  730

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      55       7   33
Blazar    6      19    4
QSO      25       8  202

MÉTRICAS (train)
  Accuracy : 0.797
  Precision: 0.797
  Recall   : 0.797
  F1-score : 0.797
  AUC      : 0.917

MÉTRICAS (test)
  Accuracy : 0.769
  Precision: 0.768
  Recall   : 0.769
  F1-score : 0.767
  AUC      : 0.871
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      55       7   33
Blazar    6      19    4
QSO      25       8  202

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.64      0.58      0.61        95
      Blazar       0.56      0.66      0.60        29
         QSO       0.85      0.86      0.85       235

    accuracy                           0.77       359
   macro avg       0.68      0.70      0.69       359
weighted avg       0.77      0.77      0.77       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     259      25  116
Blazar   39     115   32
QSO      93      37  722

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      53       8   34
Blazar    6      19    4
QSO      28       8  199

MÉTRICAS (train)
  Accuracy : 0.762
  Precision: 0.760
  Recall   : 0.762
  F1-score : 0.761
  AUC      : 0.890

MÉTRICAS (test)
  Accuracy : 0.755
  Precision: 0.755
  Recall   : 0.755
  F1-score : 0.754
  AUC      : 0.844
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      53       8   34
Blazar    6      19    4
QSO      28       8  199

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.61      0.56      0.58        95
      Blazar       0.54      0.66      0.59        29
         QSO       0.84      0.85      0.84       235

    accuracy                           0.75       359
   macro avg       0.66      0.69      0.67       359
weighted avg       0.75      0.75      0.75       359


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                   params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top2  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}        0.753650       0.024988       0.180026           0.933639         0.757046        0.022806    0.176593   0.936610  0.890796  0.793872          0.792316
RF_rob_top1 {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}        0.754485       0.024814       0.165525           0.919750         0.758382        0.022996    0.161369   0.922367  0.877516  0.779944          0.778750
RF_rob_top4   {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}        0.752820       0.025653       0.160532           0.912921         0.756613        0.023760    0.156307   0.917316  0.870754  0.768802          0.767471
RF_rob_top3   {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}        0.752972       0.023791       0.152910           0.905523         0.757107        0.023075    0.148416   0.909778  0.867421  0.777159          0.777903
RF_rob_top5   {'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}        0.752344       0.025595       0.134124           0.885998         0.755828        0.023918    0.130170   0.890305  0.844131  0.754875          0.754053

Tiempo total: 23:37:58
✅ Guardado: /home/felorrieta/Downloads/RF1_1.png
✅ Guardado: /home/felorrieta/Downloads/RF1_2.png
✅ Guardado: /home/felorrieta/Downloads/RF1_3.png
✅ Guardado: /home/felorrieta/Downloads/RF1_4.png
✅ Guardado: /home/felorrieta/Downloads/RF1_5.png

Tiempo total guardando figuras: 00:16:54
Rango original columnas: 0 .. 1022
Detectado 0-based -> usando idx_1based = idx + 1
Rango idx_1based: 1 .. 1023

IMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)
nivel  n_features  importancia_sum  importancia_pct  importancia_prom
   N0           1         0.000000         0.000000          0.000000
   N1           2         0.000113         0.011285          0.000056
   N2           4         0.002418         0.241844          0.000605
   N3           8         0.002910         0.291003          0.000364
   N4          16         0.006203         0.620324          0.000388
   N5          32         0.015166         1.516604          0.000474
   N6          64         0.032592         3.259169          0.000509
   N7         128         0.076305         7.630456          0.000596
   N8         256         0.232266        23.226616          0.000907
   N9         512         0.632027        63.202698          0.001234


Tiempo total: 00:06:51

===============================================================================================
RESULTADOS POR NIVEL — criterio AUC_test
===============================================================================================
 NivelFirma  N_features  AccTrain  F1Train  AUCTrain  AccTest  F1Test  AUCTest
          1           2     0.590    0.481     0.525    0.646   0.533    0.507
          2           6     0.586    0.585     0.691    0.599   0.596    0.628
          3          14     0.668    0.673     0.819    0.638   0.650    0.758
          4          30     0.710    0.715     0.851    0.657   0.669    0.776
          5          62     0.732    0.734     0.875    0.688   0.694    0.809
          6         126     0.771    0.772     0.896    0.755   0.757    0.843
          7         254     0.794    0.796     0.911    0.758   0.759    0.864
          8         510     0.813    0.814     0.922    0.791   0.792    0.876
          9        1022     0.823    0.823     0.932    0.797   0.797    0.889

===============================================================================================
NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test (con tolerancia)
===============================================================================================
NivelFirma       9.000000
N_features    1022.000000
AccTrain         0.822670
F1Train          0.823288
AUCTrain         0.932414
AccTest          0.796657
F1Test           0.796580
AUCTest          0.888666

===============================================================================================
FILAS LaTeX (tabla ORIGINAL: sin AUC)
===============================================================================================
1 & 2 & 0.590 & 0.481 & 0.646 & 0.533 \\
2 & 6 & 0.586 & 0.585 & 0.599 & 0.596 \\
3 & 14 & 0.668 & 0.673 & 0.638 & 0.650 \\
4 & 30 & 0.710 & 0.715 & 0.657 & 0.669 \\
5 & 62 & 0.732 & 0.734 & 0.688 & 0.694 \\
6 & 126 & 0.771 & 0.772 & 0.755 & 0.757 \\
7 & 254 & 0.794 & 0.796 & 0.758 & 0.759 \\
8 & 510 & 0.813 & 0.814 & 0.791 & 0.792 \\
9 & 1022 & 0.823 & 0.823 & 0.797 & 0.797 \\

===============================================================================================
FILAS LaTeX (tabla EXTENDIDA: incluye AUC_test)
===============================================================================================
1 & 2 & 0.590 & 0.481 & 0.646 & 0.533 & 0.507 \\
2 & 6 & 0.586 & 0.585 & 0.599 & 0.596 & 0.628 \\
3 & 14 & 0.668 & 0.673 & 0.638 & 0.650 & 0.758 \\
4 & 30 & 0.710 & 0.715 & 0.657 & 0.669 & 0.776 \\
5 & 62 & 0.732 & 0.734 & 0.688 & 0.694 & 0.809 \\
6 & 126 & 0.771 & 0.772 & 0.755 & 0.757 & 0.843 \\
7 & 254 & 0.794 & 0.796 & 0.758 & 0.759 & 0.864 \\
8 & 510 & 0.813 & 0.814 & 0.791 & 0.792 & 0.876 \\
9 & 1022 & 0.823 & 0.823 & 0.797 & 0.797 & 0.889 \\

2.1.2 RANDOM FOREST - LOG SIGNATURE

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Catalina/logsignature_esig_REALES_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out
    
def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print("\n" + "═" * 80)
    print(f"{nombre} #{idx} | params = {params}")
    print("─" * 80)

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1438, 127)
X_test : (359, 127)
y_train: (1438,)
y_test : (359,)
Ver código
import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("TOP 5 (según AUC CV)")
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    print(name)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("EVALUACIÓN TOP 5: CV (AUC) en TRAIN")

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )

    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean

    rf.fit(X_train, y_train_encoded)

    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

print("\n" + "=" * 110)
print("RESUMEN (ordenado por AUC_test, luego CV val AUC)")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

#matrices de confusión

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_ 

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF1_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF1_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Revisar X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  # N0..N9

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

# Línea = importancia promedio por feature del nivel
ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1)) - 2  # 2,6,14,...,1022

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    # predicciones
    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]

print("RESULTADOS POR NIVEL — AUC test")
print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))

print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC test")
print(best_simple.to_string())

print("\n" + "="*95)
print("FILAS LaTeX (tabla ORIGINAL: sin AUC)")
print("="*95)
for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")

print("FILAS LaTeX (tabla EXTENDIDA: incluye AUC_test)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                      params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}         0.721860        0.032956          0.882840    0.160981                1
1  {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}         0.720965        0.034688          0.887773    0.166808                2
2    {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}         0.720730        0.033739          0.897614    0.176883                3
3  {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}         0.720658        0.034824          0.867674    0.147017                4
4     {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}         0.720252        0.033464          0.862809    0.142556                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     261      38  101
Blazar   28     125   33
QSO      93      60  699

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      50      11   34
Blazar    7      16    6
QSO      28      13  194

MÉTRICAS (train)
  Accuracy : 0.755
  Precision: 0.760
  Recall   : 0.755
  F1-score : 0.756
  AUC      : 0.886

MÉTRICAS (test)
  Accuracy : 0.724
  Precision: 0.731
  Recall   : 0.724
  F1-score : 0.726
  AUC      : 0.823
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      50      11   34
Blazar    7      16    6
QSO      28      13  194

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.59      0.53      0.56        95
      Blazar       0.40      0.55      0.46        29
         QSO       0.83      0.83      0.83       235

    accuracy                           0.72       359
   macro avg       0.61      0.63      0.62       359
weighted avg       0.73      0.72      0.73       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     278      33   89
Blazar   19     141   26
QSO     110      63  679

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      53       9   33
Blazar    5      22    2
QSO      32      14  189

MÉTRICAS (train)
  Accuracy : 0.764
  Precision: 0.774
  Recall   : 0.764
  F1-score : 0.767
  AUC      : 0.890

MÉTRICAS (test)
  Accuracy : 0.735
  Precision: 0.748
  Recall   : 0.735
  F1-score : 0.739
  AUC      : 0.826
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      53       9   33
Blazar    5      22    2
QSO      32      14  189

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.59      0.56      0.57        95
      Blazar       0.49      0.76      0.59        29
         QSO       0.84      0.80      0.82       235

    accuracy                           0.74       359
   macro avg       0.64      0.71      0.66       359
weighted avg       0.75      0.74      0.74       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     277      33   90
Blazar   22     133   31
QSO      82      59  711

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      50      10   35
Blazar    7      17    5
QSO      25      13  197

MÉTRICAS (train)
  Accuracy : 0.780
  Precision: 0.785
  Recall   : 0.780
  F1-score : 0.781
  AUC      : 0.901

MÉTRICAS (test)
  Accuracy : 0.735
  Precision: 0.740
  Recall   : 0.735
  F1-score : 0.736
  AUC      : 0.835
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      50      10   35
Blazar    7      17    5
QSO      25      13  197

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.61      0.53      0.56        95
      Blazar       0.42      0.59      0.49        29
         QSO       0.83      0.84      0.83       235

    accuracy                           0.74       359
   macro avg       0.62      0.65      0.63       359
weighted avg       0.74      0.74      0.74       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     256      40  104
Blazar   30     129   27
QSO     122      63  667

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      49      12   34
Blazar    9      17    3
QSO      36      14  185

MÉTRICAS (train)
  Accuracy : 0.732
  Precision: 0.742
  Recall   : 0.732
  F1-score : 0.735
  AUC      : 0.871

MÉTRICAS (test)
  Accuracy : 0.699
  Precision: 0.715
  Recall   : 0.699
  F1-score : 0.705
  AUC      : 0.806
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      49      12   34
Blazar    9      17    3
QSO      36      14  185

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.52      0.52      0.52        95
      Blazar       0.40      0.59      0.47        29
         QSO       0.83      0.79      0.81       235

    accuracy                           0.70       359
   macro avg       0.58      0.63      0.60       359
weighted avg       0.72      0.70      0.71       359


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     259      42   99
Blazar   34     121   31
QSO     116      65  671

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      49      11   35
Blazar    9      17    3
QSO      33      15  187

MÉTRICAS (train)
  Accuracy : 0.731
  Precision: 0.741
  Recall   : 0.731
  F1-score : 0.735
  AUC      : 0.867

MÉTRICAS (test)
  Accuracy : 0.705
  Precision: 0.718
  Recall   : 0.705
  F1-score : 0.710
  AUC      : 0.805
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      49      11   35
Blazar    9      17    3
QSO      33      15  187

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.54      0.52      0.53        95
      Blazar       0.40      0.59      0.47        29
         QSO       0.83      0.80      0.81       235

    accuracy                           0.70       359
   macro avg       0.59      0.63      0.60       359
weighted avg       0.72      0.70      0.71       359


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                    params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top3   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}        0.720730       0.033739       0.176883           0.897539         0.719485        0.020853    0.178054   0.901333  0.835356  0.735376          0.735731
RF_rob_top2 {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}        0.720965       0.034688       0.166808           0.887348         0.717665        0.021007    0.169683   0.890379  0.825712  0.735376          0.738733
RF_rob_top1  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}        0.721860       0.032956       0.160981           0.882597         0.720167        0.021003    0.162429   0.886092  0.822933  0.724234          0.726019
RF_rob_top4 {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}        0.720658       0.034824       0.147017           0.867466         0.717045        0.021736    0.150421   0.870812  0.806125  0.699164          0.705338
RF_rob_top5    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}        0.720252       0.033464       0.142556           0.862434         0.717498        0.020806    0.144936   0.867195  0.804726  0.704735          0.709787

Tiempo total: 02:17:42
✅ Guardado: /home/felorrieta/Downloads/RF1_1.png
✅ Guardado: /home/felorrieta/Downloads/RF1_2.png
✅ Guardado: /home/felorrieta/Downloads/RF1_3.png
✅ Guardado: /home/felorrieta/Downloads/RF1_4.png
✅ Guardado: /home/felorrieta/Downloads/RF1_5.png

Tiempo total guardando figuras: 00:02:17
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[15], line 382
    380 feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
    381 if feat_idx.isna().any():
--> 382     raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")
    384 feat_idx = feat_idx.astype(int)
    385 min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())

ValueError: Tus columnas no se pudieron convertir a números. Revisa X_train.columns.

2.2 IISIGNATURE

2.2.1 RANDOM FOREST - FIRMA

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Downloads/path_signature_iisignature_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out


def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print(f"{nombre} #{idx} | params = {params}")

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0


def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)

import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("\n" + "#" * 80)
print("TOP 5 (según AUC CV)")
print("#" * 80)
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    80)
    print(name)
    print("=" * 80)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("\n" + "#" * 80)
print("EVALUACIÓN TOP-5: CV (AUC) en TRAIN")
print("#" * 80)

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )

    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean
    
    rf.fit(X_train, y_train_encoded)

    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

110)
print("RESUMEN (ordenado por AUC_test, luego CV val AUC)")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

#matrices de confusión

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_ 

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF_IISIG_FIRMA_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF_IISIG_FIRMA_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

# Si parte en 0, lo pasamos a 1..N
if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1)) - 2  

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]

print("RESULTADOS POR NIVEL — criterio AUC test")

print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))


print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC test")

print(best_simple.to_string())


print("FILAS LaTeX (sin AUC)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")


print("FILAS LaTeX (tabla EXTENDIDA")


for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1510, 1022)
X_test : (377, 1022)
y_train: (1510,)
y_test : (377,)

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                      params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0  {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}         0.776300        0.014817          0.932933    0.156632                1
1  {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}         0.775676        0.015800          0.915855    0.140179                2
2   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}         0.775384        0.016302          0.922434    0.147050                3
3    {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}         0.775097        0.017671          0.936421    0.161324                4
4     {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}         0.773837        0.014971          0.910114    0.136277                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     344      14   64
Blazar   31     133   25
QSO     102      30  767

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      66       7   28
Blazar    8      19    3
QSO      34       5  207

MÉTRICAS (train)
  Accuracy : 0.824
  Precision: 0.829
  Recall   : 0.824
  F1-score : 0.825
  AUC      : 0.936

MÉTRICAS (test)
  Accuracy : 0.775
  Precision: 0.780
  Recall   : 0.775
  F1-score : 0.777
  AUC      : 0.876
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      66       7   28
Blazar    8      19    3
QSO      34       5  207

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.61      0.65      0.63       101
      Blazar       0.61      0.63      0.62        30
         QSO       0.87      0.84      0.86       246

    accuracy                           0.77       377
   macro avg       0.70      0.71      0.70       377
weighted avg       0.78      0.77      0.78       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     317      17   88
Blazar   35     127   27
QSO     100      31  768

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      63       7   31
Blazar    8      18    4
QSO      33       6  207

MÉTRICAS (train)
  Accuracy : 0.803
  Precision: 0.805
  Recall   : 0.803
  F1-score : 0.803
  AUC      : 0.919

MÉTRICAS (test)
  Accuracy : 0.764
  Precision: 0.767
  Recall   : 0.764
  F1-score : 0.765
  AUC      : 0.858
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      63       7   31
Blazar    8      18    4
QSO      33       6  207

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.61      0.62      0.61       101
      Blazar       0.58      0.60      0.59        30
         QSO       0.86      0.84      0.85       246

    accuracy                           0.76       377
   macro avg       0.68      0.69      0.68       377
weighted avg       0.77      0.76      0.77       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     326      17   79
Blazar   41     118   30
QSO      90      35  774

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      64       7   30
Blazar    8      18    4
QSO      30       7  209

MÉTRICAS (train)
  Accuracy : 0.807
  Precision: 0.808
  Recall   : 0.807
  F1-score : 0.807
  AUC      : 0.924

MÉTRICAS (test)
  Accuracy : 0.772
  Precision: 0.774
  Recall   : 0.772
  F1-score : 0.773
  AUC      : 0.864
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      64       7   30
Blazar    8      18    4
QSO      30       7  209

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.63      0.63      0.63       101
      Blazar       0.56      0.60      0.58        30
         QSO       0.86      0.85      0.85       246

    accuracy                           0.77       377
   macro avg       0.68      0.69      0.69       377
weighted avg       0.77      0.77      0.77       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     331      17   74
Blazar   36     125   28
QSO      79      33  787

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      65       7   29
Blazar    8      18    4
QSO      27       7  212

MÉTRICAS (train)
  Accuracy : 0.823
  Precision: 0.824
  Recall   : 0.823
  F1-score : 0.823
  AUC      : 0.939

MÉTRICAS (test)
  Accuracy : 0.782
  Precision: 0.784
  Recall   : 0.782
  F1-score : 0.783
  AUC      : 0.875
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      65       7   29
Blazar    8      18    4
QSO      27       7  212

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.65      0.64      0.65       101
      Blazar       0.56      0.60      0.58        30
         QSO       0.87      0.86      0.86       246

    accuracy                           0.78       377
   macro avg       0.69      0.70      0.70       377
weighted avg       0.78      0.78      0.78       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     323      19   80
Blazar   44     115   30
QSO     109      37  753

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      64       7   30
Blazar   10      16    4
QSO      36       8  202

MÉTRICAS (train)
  Accuracy : 0.789
  Precision: 0.793
  Recall   : 0.789
  F1-score : 0.790
  AUC      : 0.913

MÉTRICAS (test)
  Accuracy : 0.748
  Precision: 0.755
  Recall   : 0.748
  F1-score : 0.751
  AUC      : 0.851
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      64       7   30
Blazar   10      16    4
QSO      36       8  202

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.58      0.63      0.61       101
      Blazar       0.52      0.53      0.52        30
         QSO       0.86      0.82      0.84       246

    accuracy                           0.75       377
   macro avg       0.65      0.66      0.66       377
weighted avg       0.76      0.75      0.75       377


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                    params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top1 {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8, 'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}        0.776300       0.014817       0.156632           0.932912         0.777879        0.018616    0.155033   0.935709  0.875557  0.774536          0.776922
RF_rob_top4   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}        0.775097       0.017671       0.161324           0.936232         0.775800        0.018576    0.160432   0.938966  0.875049  0.782493          0.782956
RF_rob_top3  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}        0.775384       0.016302       0.147050           0.922395         0.776716        0.018325    0.145679   0.924492  0.863614  0.771883          0.772908
RF_rob_top2 {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}        0.775676       0.015800       0.140179           0.915383         0.775928        0.018684    0.139455   0.919422  0.858378  0.763926          0.765198
RF_rob_top5    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}        0.773837       0.014971       0.136277           0.909691         0.776399        0.018362    0.133292   0.912615  0.850747  0.748011          0.751190

Tiempo total: 17:53:13
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_1.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_2.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_3.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_4.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_5.png

Tiempo total guardando figuras: 00:18:21
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[17], line 625
    623 feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
    624 if feat_idx.isna().any():
--> 625     raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")
    627 feat_idx = feat_idx.astype(int)
    628 min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())

ValueError: Tus columnas no se pudieron convertir a números. Revisa X_train.columns.

2.2.2 RANDOM FOREST - LOGIFRMA

Ver código
import sys
import subprocess
import importlib.util

required = {
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
    "tqdm": "tqdm",
}

missing = [pip_name for mod_name, pip_name in required.items()
           if importlib.util.find_spec(mod_name) is None]

if missing:
    print("Instalando paquetes faltantes:", ", ".join(missing))
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + missing)
    print("Instalación terminada.")

import time
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

from scipy.stats import randint

from sklearn.base import clone
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

x = pd.read_csv('/home/felorrieta/Downloads/log_signature_iisignature_M9.csv')
y = pd.read_csv('/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv')

y["id"] = y["oid"]
data = pd.merge(x, y, on="id")

data_modelado = data.sample(frac=0.8, random_state=42).reset_index(drop=True)
data_test = data.drop(data_modelado.index).reset_index(drop=True)

X_train = data_modelado.drop(
    columns=['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat', 'id']
)
y_train = data_modelado['survey_class_mapped']

X_test = data_test[X_train.columns].copy()
y_test = data_test['survey_class_mapped']

le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

labels = le.classes_

print("Clases codificadas:")
print(dict(enumerate(labels)))

pesos_por_nombre = {
    'AGN': 2.0,
    'Blazar': 3.0,
    'QSO': 1.0
}

class_weight_dict = {}
for cls_name, peso in pesos_por_nombre.items():
    if cls_name in le.classes_:
        class_weight_dict[le.transform([cls_name])[0]] = peso

print("\nclass_weight_dict usado:")
print(class_weight_dict)

def evaluar_modelo(clf, X_tr, y_tr, X_te, y_te):
    y_pred_tr = clf.predict(X_tr)
    y_pred_te = clf.predict(X_te)

    out = {
        'cm_train': confusion_matrix(y_tr, y_pred_tr),
        'cm_test':  confusion_matrix(y_te, y_pred_te),

        'acc_train': accuracy_score(y_tr, y_pred_tr),
        'prec_train': precision_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'rec_train': recall_score(y_tr, y_pred_tr, average='weighted', zero_division=0),
        'f1_train': f1_score(y_tr, y_pred_tr, average='weighted', zero_division=0),

        'acc_test': accuracy_score(y_te, y_pred_te),
        'prec_test': precision_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'rec_test': recall_score(y_te, y_pred_te, average='weighted', zero_division=0),
        'f1_test': f1_score(y_te, y_pred_te, average='weighted', zero_division=0),
    }

    if hasattr(clf, "predict_proba"):
        try:
            proba_tr = clf.predict_proba(X_tr)
            proba_te = clf.predict_proba(X_te)

            out['auc_train'] = roc_auc_score(
                y_tr, proba_tr,
                multi_class="ovr",
                average="weighted"
            )
            out['auc_test'] = roc_auc_score(
                y_te, proba_te,
                multi_class="ovr",
                average="weighted"
            )
        except Exception:
            out['auc_train'] = np.nan
            out['auc_test'] = np.nan
    else:
        out['auc_train'] = np.nan
        out['auc_test'] = np.nan

    return out

def print_bloque_modelo(nombre, idx, params, metrics, labels):
    print("\n" + "═" * 80)
    print(f"{nombre} #{idx} | params = {params}")
    print("─" * 80)

    print("Matriz de confusión — Entrenamiento")
    df_cm_tr = pd.DataFrame(metrics['cm_train'], index=labels, columns=labels)
    print(df_cm_tr)

    print("\nMatriz de confusión — Test")
    df_cm_te = pd.DataFrame(metrics['cm_test'], index=labels, columns=labels)
    print(df_cm_te)

    print("\nMÉTRICAS (train)")
    print(f"  Accuracy : {metrics['acc_train']:.3f}")
    print(f"  Precision: {metrics['prec_train']:.3f}")
    print(f"  Recall   : {metrics['rec_train']:.3f}")
    print(f"  F1-score : {metrics['f1_train']:.3f}")
    print(f"  AUC      : {metrics.get('auc_train', np.nan):.3f}")

    print("\nMÉTRICAS (test)")
    print(f"  Accuracy : {metrics['acc_test']:.3f}")
    print(f"  Precision: {metrics['prec_test']:.3f}")
    print(f"  Recall   : {metrics['rec_test']:.3f}")
    print(f"  F1-score : {metrics['f1_test']:.3f}")
    print(f"  AUC      : {metrics.get('auc_test', np.nan):.3f}")

    print("═" * 80)


def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0


def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test, "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, title in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(title, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

print("\nShapes:")
print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)

import time
import numpy as np
import pandas as pd

from scipy.stats import randint

from sklearn.base import clone
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    RepeatedStratifiedKFold,
    cross_validate,
    ParameterSampler
)

from tqdm.notebook import tqdm

start_time = time.time()

cv_strategy = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
scoring_metric = "roc_auc_ovr_weighted"

rf_base = RandomForestClassifier(
    random_state=42,
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True
)

max_features_opts = ["sqrt", "log2"] + [round(v, 2) for v in np.linspace(0.2, 0.8, 7)]

param_dist_robusto = {
    "n_estimators": randint(600, 5001),
    "max_depth": [None] + list(range(4, 21)),
    "max_features": max_features_opts,
    "min_samples_split": randint(20, 201),
    "min_samples_leaf": randint(5, 51),
    "max_samples": [0.4, 0.6, 0.8],
    "criterion": ["gini", "entropy"],
}

n_iter = 200
param_list = list(ParameterSampler(param_dist_robusto, n_iter=n_iter, random_state=42))

search_rows = []

with tqdm(
    total=n_iter,
    desc="RF RandomizedSearch",
    leave=True,
    dynamic_ncols=True
) as pbar:

    for i, params_i in enumerate(param_list, start=1):
        rf_i = clone(rf_base)
        rf_i.set_params(**params_i)

        cv_out = cross_validate(
            rf_i,
            X_train,
            y_train_encoded,
            cv=cv_strategy,
            scoring=scoring_metric,
            return_train_score=True,
            n_jobs=-1
        )

        mean_test_score = float(np.mean(cv_out["test_score"]))
        std_test_score = float(np.std(cv_out["test_score"]))
        mean_train_score = float(np.mean(cv_out["train_score"]))
        gap_cv_auc = mean_train_score - mean_test_score

        search_rows.append({
            "params": params_i,
            "mean_test_score": mean_test_score,
            "std_test_score": std_test_score,
            "mean_train_score": mean_train_score,
            "gap_cv_auc": gap_cv_auc,
        })

        pbar.update(1)
        pbar.set_postfix({
            "iter": i,
            "best_auc": f"{max(r['mean_test_score'] for r in search_rows):.4f}"
        })

results_df = pd.DataFrame(search_rows).copy()
results_df["rank_test_score"] = results_df["mean_test_score"].rank(
    ascending=False, method="min"
).astype(int)

top5 = results_df.nlargest(5, "mean_test_score")[
    ["params", "mean_test_score", "std_test_score", "mean_train_score", "gap_cv_auc", "rank_test_score"]
].reset_index(drop=True)

print("\n" + "#" * 80)
print("TOP 5 (según AUC CV)")
print("#" * 80)
print(top5.to_string(index=True))

rskf = RepeatedStratifiedKFold(n_splits=4, n_repeats=5, random_state=42)
labels = le.classes_
resumen = []

def eval_test(model, name):
    y_pred = model.predict(X_test)
    80)
    print(name)
    print("=" * 80)
    print("Matriz de confusión (TEST)")
    print(pd.DataFrame(confusion_matrix(y_test_encoded, y_pred), index=labels, columns=labels))
    print("\nReporte (TEST)")
    print(classification_report(y_test_encoded, y_pred, target_names=labels, zero_division=0))

print("\n" + "#" * 80)
print("EVALUACIÓN TOP-5: CV (AUC) en TRAIN")
print("#" * 80)

for i, row in top5.iterrows():
    params_i = row["params"]

    rf = RandomForestClassifier(
        random_state=100 + i,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    
    cv_out = cross_validate(
        rf,
        X_train,
        y_train_encoded,
        cv=rskf,
        scoring=scoring_metric,
        return_train_score=True,
        n_jobs=-1
    )

    cv_train_mean = float(np.mean(cv_out["train_score"]))
    cv_val_mean = float(np.mean(cv_out["test_score"]))
    cv_val_std = float(np.std(cv_out["test_score"]))
    gap = cv_train_mean - cv_val_mean

    rf.fit(X_train, y_train_encoded)

    metrics_holdout = evaluar_modelo(rf, X_train, y_train_encoded, X_test, y_test_encoded)

    proba_train = rf.predict_proba(X_train)
    proba_test = rf.predict_proba(X_test)

    auc_train_holdout = roc_auc_score(
        y_train_encoded, proba_train,
        multi_class="ovr", average="weighted"
    )
    auc_test_holdout = roc_auc_score(
        y_test_encoded, proba_test,
        multi_class="ovr", average="weighted"
    )

    print_bloque_modelo("RF ROBUSTO TOP-5", i + 1, params_i, metrics_holdout, labels)
    eval_test(rf, f"RF_rob_top{i+1} | params={params_i}")

    resumen.append({
        "modelo": f"RF_rob_top{i+1}",
        "params": params_i,
        "rs_mean_auc_cv": row["mean_test_score"],
        "rs_std_auc_cv": row["std_test_score"],
        "rs_gap_auc_cv": row["gap_cv_auc"],
        "cv_train_mean_auc": cv_train_mean,
        "cv_val_mean_auc": cv_val_mean,
        "cv_val_std_auc": cv_val_std,
        "cv_gap_auc": gap,
        "train_auc": auc_train_holdout,
        "test_auc": auc_test_holdout,
        "test_acc": metrics_holdout["acc_test"],
        "test_f1_weighted": metrics_holdout["f1_test"],
    })

df_res = pd.DataFrame(resumen).sort_values(
    ["test_auc", "cv_val_mean_auc"], ascending=False
).reset_index(drop=True)

print("RESUMEN (ordenado por AUC_test, luego CV val AUC)")
print("=" * 110)
print(df_res[[
    "modelo", "params",
    "rs_mean_auc_cv", "rs_std_auc_cv", "rs_gap_auc_cv",
    "cv_train_mean_auc", "cv_val_mean_auc", "cv_val_std_auc", "cv_gap_auc",
    "train_auc", "test_auc",
    "test_acc", "test_f1_weighted"
]].to_string(index=False))

elapsed_seconds = int(time.time() - start_time)
hours, rem = divmod(elapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print(f"\nTiempo total: {hours:02d}:{minutes:02d}:{seconds:02d}")

#matrices de confusión

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score

start_time_save = time.time()

downloads = Path.home() / "Downloads"
if not downloads.exists():
    downloads = Path.home() / "Descargas"
downloads.mkdir(parents=True, exist_ok=True)

labels = le.classes_  

top5_params_in_order = [
    ("RF_rob_top1", {'criterion': 'entropy', 'max_depth': 15, 'max_features': 0.8, 'max_samples': 0.8,
                     'min_samples_leaf': 13, 'min_samples_split': 41, 'n_estimators': 3141}),
    ("RF_rob_top2", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.7, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 42, 'n_estimators': 1414}),
    ("RF_rob_top3", {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}),
    ("RF_rob_top4", {'criterion': 'entropy', 'max_depth': 13, 'max_features': 0.8, 'max_samples': 0.6,
                     'min_samples_leaf': 16, 'min_samples_split': 54, 'n_estimators': 3160}),
    ("RF_rob_top5", {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6,
                     'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}),
]

def _row_normalize(cm):
    cm = cm.astype(float)
    row_sums = cm.sum(axis=1, keepdims=True)
    row_sums[row_sums == 0] = 1.0
    return (cm / row_sums) * 100.0

def save_confusion_train_test(cm_train, cm_test, labels, outpath,
                              title_prefix="", subtitle="",
                              gap_width=0.28, wspace=0.15,
                              label_fontsize=13, tick_fontsize=13, title_fontsize=14):
    """
    Guarda TRAIN y TEST lado a lado:
    - % por fila grande
    - (conteo) pequeño debajo
    """
    cm_tr_pct = _row_normalize(cm_train)
    cm_te_pct = _row_normalize(cm_test)

    fig = plt.figure(figsize=(10.8, 4.8))
    gs = gridspec.GridSpec(
        1, 4,
        width_ratios=[1, gap_width, 1, 0.08],
        wspace=wspace
    )

    ax1 = fig.add_subplot(gs[0, 0])
    ax_gap = fig.add_subplot(gs[0, 1])
    ax2 = fig.add_subplot(gs[0, 2])
    ax_cbar = fig.add_subplot(gs[0, 3])
    ax_gap.axis("off")

    panels = [
        (ax1, cm_tr_pct, cm_train, "Train"),
        (ax2, cm_te_pct, cm_test,  "Test"),
    ]

    vmin, vmax = 0, 100

    for ax, cm_pct, cm_cnt, t in panels:
        im = ax.imshow(cm_pct, cmap="BuPu", vmin=vmin, vmax=vmax)
        ax.set_title(t, fontsize=title_fontsize)

        ax.set_xticks(np.arange(len(labels)))
        ax.set_yticks(np.arange(len(labels)))
        ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=tick_fontsize)
        ax.set_yticklabels(labels, fontsize=tick_fontsize)

        ax.set_xlabel("Predicho", fontsize=label_fontsize)
        ax.set_ylabel("Real", fontsize=label_fontsize)

        thr = 50
        for i in range(cm_pct.shape[0]):
            for j in range(cm_pct.shape[1]):
                pct = cm_pct[i, j]
                cnt = int(cm_cnt[i, j])
                color_txt = "white" if pct > thr else "black"

                ax.text(j, i - 0.10, f"{pct:.1f}%",
                        ha="center", va="center",
                        color=color_txt, fontsize=10, fontweight="bold")
                ax.text(j, i + 0.22, f"({cnt})",
                        ha="center", va="center",
                        color=color_txt, fontsize=7)

    fig.colorbar(im, cax=ax_cbar, label="% por fila (clase real)")
    fig.suptitle(f"{title_prefix}\n{subtitle}", fontsize=13, y=0.98)

    fig.subplots_adjust(left=0.08, right=0.92, bottom=0.22, top=0.82)

    fig.savefig(outpath, dpi=300, bbox_inches="tight")
    plt.close(fig)

for i, (name, params_i) in enumerate(top5_params_in_order):  
    rf = RandomForestClassifier(
        random_state=100 + i,      
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **params_i
    )
    rf.fit(X_train, y_train_encoded)

    y_pred_tr = rf.predict(X_train)
    y_pred_te = rf.predict(X_test)

    cm_train = confusion_matrix(y_train_encoded, y_pred_tr)
    cm_test  = confusion_matrix(y_test_encoded,  y_pred_te)

    acc_tr = accuracy_score(y_train_encoded, y_pred_tr)
    acc_te = accuracy_score(y_test_encoded,  y_pred_te)
    f1_tr  = f1_score(y_train_encoded, y_pred_tr, average="weighted", zero_division=0)
    f1_te  = f1_score(y_test_encoded,  y_pred_te, average="weighted", zero_division=0)

    subtitle = f"Acc train={acc_tr:.3f} | Acc test={acc_te:.3f} | F1w train={f1_tr:.3f} | F1w test={f1_te:.3f}"

    outpath = downloads / f"RF_IISIG_FIRMA_{i+1}.png"
    save_confusion_train_test(
        cm_train, cm_test, labels,
        outpath=outpath,
        title_prefix=f"{name}  (RF_IISIG_FIRMA_{i+1})",
        subtitle=subtitle,
        gap_width=0.28,
        wspace=0.15
    )

    print(f"Guardado: {outpath}")

elapsed = int(time.time() - start_time_save)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total guardando figuras: {h:02d}:{m:02d}:{s:02d}")

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier

start_time_imp = time.time()

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

rf_best = RandomForestClassifier(
    random_state=100,         
    n_jobs=-1,
    class_weight=class_weight_dict,
    bootstrap=True,
    **best_params_sig
)
rf_best.fit(X_train, y_train_encoded)

feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if feat_idx.isna().any():
    raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")

feat_idx = feat_idx.astype(int)
min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())
print(f"Rango original columnas: {min_idx} .. {max_idx}")

if min_idx == 0:
    feat_idx_1based = feat_idx + 1
    print("Detectado 0-based -> usando idx_1based = idx + 1")
else:
    feat_idx_1based = feat_idx

print(f"Rango idx_1based: {int(feat_idx_1based.min())} .. {int(feat_idx_1based.max())}")

edges = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
lvl_labels = [f"N{i}" for i in range(0, 10)]  

niveles = pd.cut(feat_idx_1based, bins=edges, labels=lvl_labels, right=False, include_lowest=True)
if niveles.isna().any():
    bad = np.array(X_train.columns)[niveles.isna()][:10]
    raise ValueError(f"Aún hay features fuera de rango de bins. Ejemplos: {bad}")

importances = rf_best.feature_importances_

df = pd.DataFrame({
    "feature": X_train.columns.astype(str),
    "nivel": niveles.astype(str),
    "importance": importances
})

res = df.groupby("nivel", as_index=False).agg(
    n_features=("importance", "size"),
    importancia_sum=("importance", "sum")
)
res["importancia_pct"] = 100 * res["importancia_sum"] / res["importancia_sum"].sum()
res["importancia_prom"] = res["importancia_sum"] / res["n_features"]

res["nivel_num"] = res["nivel"].str.replace("N", "", regex=False).astype(int)
res = res.sort_values("nivel_num").drop(columns="nivel_num").reset_index(drop=True)

print("\nIMPORTANCIA POR NIVEL — RF_rob_top1 (FIRMA NORMAL)")
print(res[["nivel","n_features","importancia_sum","importancia_pct","importancia_prom"]].to_string(index=False))

res = res[res["nivel"] != "N0"].reset_index(drop=True)

xpos = np.arange(len(res))
xticks_labels = [f"Nivel {n}" for n in res["nivel"].str.replace("N", "", regex=False)]

fig, ax1 = plt.subplots(figsize=(10.5, 5.2))

ax1.bar(xpos, res["importancia_pct"], edgecolor="black", alpha=0.9, color="plum")
ax1.set_ylabel("Porcentaje de importancia")
ax1.set_xlabel("Nivel")
ax1.set_title("Primer modelo — Importancia por nivel")
ax1.grid(axis="y", linestyle="--", alpha=0.5)

ax1.set_xticks(xpos)
ax1.set_xticklabels(xticks_labels, rotation=0)

ax2 = ax1.twinx()
ax2.plot(xpos, res["importancia_prom"], marker="o", color="purple")
ax2.set_ylabel("Importancia promedio")

plt.tight_layout()
plt.show()

elapsed = int(time.time() - start_time_imp)
h, r = divmod(elapsed, 3600)
m, s = divmod(r, 60)
print(f"\nTiempo total: {h:02d}:{m:02d}:{s:02d}")

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

best_params_sig = {
    'criterion': 'entropy',
    'max_depth': 15,
    'max_features': 0.8,
    'max_samples': 0.8,
    'min_samples_leaf': 13,
    'min_samples_split': 41,
    'n_estimators': 3141
}

col_nums = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
if col_nums.isna().any():
    raise ValueError("X_train.columns no son numéricas (0..1022).")
col_nums = col_nums.astype(int)

cols_sorted = [c for _, c in sorted(zip(col_nums.values, X_train.columns), key=lambda t: t[0])]
nums_sorted = sorted(col_nums.values)

EXCLUDE_N0 = True

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    end_idx = (2 ** (m + 1)) - 2 

    if EXCLUDE_N0:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (1 <= n <= end_idx)]
    else:
        selected = [c for c, n in zip(cols_sorted, nums_sorted) if (0 <= n <= end_idx)]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=100,        
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )
    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")
    auc_te = roc_auc_score(y_test_encoded,  proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

best_auc = df_levels["AUCTest"].max()
tol = 1e-4 
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values(["NivelFirma"]).iloc[0]


print("RESULTADOS POR NIVEL — criterio AUC test")

print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))


print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC test")

print(best_simple.to_string())


print("FILAS LaTeX (sin AUC)")

for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\")


print("FILAS LaTeX")


for _, r in df_levels.iterrows():
    print(f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
          f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & {r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\")
Clases codificadas:
{0: 'AGN', 1: 'Blazar', 2: 'QSO'}

class_weight_dict usado:
{0: 2.0, 1: 3.0, 2: 1.0}

Shapes:
X_train: (1510, 127)
X_test : (377, 127)
y_train: (1510,)
y_test : (377,)

################################################################################
TOP 5 (según AUC CV)
################################################################################
                                                                                                                                                     params  mean_test_score  std_test_score  mean_train_score  gap_cv_auc  rank_test_score
0  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}         0.732185        0.026969          0.890593    0.158408                1
1    {'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}         0.729792        0.028591          0.858887    0.129095                2
2   {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}         0.729765        0.027618          0.905818    0.176053                3
3    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}         0.729449        0.028214          0.884245    0.154796                4
4    {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}         0.729017        0.028157          0.871757    0.142740                5

################################################################################
EVALUACIÓN TOP-5: CV (AUC) en TRAIN
################################################################################

════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #1 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     302      33   87
Blazar   41     123   25
QSO     118      51  730

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      60      11   30
Blazar   10      18    2
QSO      45      12  189

MÉTRICAS (train)
  Accuracy : 0.765
  Precision: 0.774
  Recall   : 0.765
  F1-score : 0.768
  AUC      : 0.893

MÉTRICAS (test)
  Accuracy : 0.708
  Precision: 0.733
  Recall   : 0.708
  F1-score : 0.717
  AUC      : 0.821
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top1 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      60      11   30
Blazar   10      18    2
QSO      45      12  189

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.52      0.59      0.56       101
      Blazar       0.44      0.60      0.51        30
         QSO       0.86      0.77      0.81       246

    accuracy                           0.71       377
   macro avg       0.61      0.65      0.62       377
weighted avg       0.73      0.71      0.72       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #2 | params = {'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     272      37  113
Blazar   50     114   25
QSO     133      55  711

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      53      13   35
Blazar   12      16    2
QSO      48      14  184

MÉTRICAS (train)
  Accuracy : 0.726
  Precision: 0.735
  Recall   : 0.726
  F1-score : 0.730
  AUC      : 0.862

MÉTRICAS (test)
  Accuracy : 0.671
  Precision: 0.699
  Recall   : 0.671
  F1-score : 0.682
  AUC      : 0.793
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top2 | params={'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      53      13   35
Blazar   12      16    2
QSO      48      14  184

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.47      0.52      0.50       101
      Blazar       0.37      0.53      0.44        30
         QSO       0.83      0.75      0.79       246

    accuracy                           0.67       377
   macro avg       0.56      0.60      0.57       377
weighted avg       0.70      0.67      0.68       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #3 | params = {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     312      30   80
Blazar   40     124   25
QSO      95      51  753

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      64      11   26
Blazar   11      17    2
QSO      39      12  195

MÉTRICAS (train)
  Accuracy : 0.787
  Precision: 0.793
  Recall   : 0.787
  F1-score : 0.790
  AUC      : 0.907

MÉTRICAS (test)
  Accuracy : 0.732
  Precision: 0.755
  Recall   : 0.732
  F1-score : 0.741
  AUC      : 0.837
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top3 | params={'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      64      11   26
Blazar   11      17    2
QSO      39      12  195

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.56      0.63      0.60       101
      Blazar       0.42      0.57      0.49        30
         QSO       0.87      0.79      0.83       246

    accuracy                           0.73       377
   macro avg       0.62      0.66      0.64       377
weighted avg       0.75      0.73      0.74       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #4 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     303      32   87
Blazar   46     119   24
QSO     123      52  724

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      62      11   28
Blazar   11      17    2
QSO      48      12  186

MÉTRICAS (train)
  Accuracy : 0.759
  Precision: 0.769
  Recall   : 0.759
  F1-score : 0.763
  AUC      : 0.886

MÉTRICAS (test)
  Accuracy : 0.703
  Precision: 0.733
  Recall   : 0.703
  F1-score : 0.714
  AUC      : 0.816
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top4 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      62      11   28
Blazar   11      17    2
QSO      48      12  186

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.51      0.61      0.56       101
      Blazar       0.42      0.57      0.49        30
         QSO       0.86      0.76      0.81       246

    accuracy                           0.70       377
   macro avg       0.60      0.65      0.62       377
weighted avg       0.73      0.70      0.71       377


════════════════════════════════════════════════════════════════════════════════
RF ROBUSTO TOP-5 #5 | params = {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
────────────────────────────────────────────────────────────────────────────────
Matriz de confusión — Entrenamiento
        AGN  Blazar  QSO
AGN     298      35   89
Blazar   47     120   22
QSO     140      54  705

Matriz de confusión — Test
        AGN  Blazar  QSO
AGN      61      12   28
Blazar   11      17    2
QSO      52      13  181

MÉTRICAS (train)
  Accuracy : 0.744
  Precision: 0.758
  Recall   : 0.744
  F1-score : 0.749
  AUC      : 0.876

MÉTRICAS (test)
  Accuracy : 0.687
  Precision: 0.724
  Recall   : 0.687
  F1-score : 0.700
  AUC      : 0.805
════════════════════════════════════════════════════════════════════════════════

================================================================================
RF_rob_top5 | params={'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}
================================================================================
Matriz de confusión (TEST)
        AGN  Blazar  QSO
AGN      61      12   28
Blazar   11      17    2
QSO      52      13  181

Reporte (TEST)
              precision    recall  f1-score   support

         AGN       0.49      0.60      0.54       101
      Blazar       0.40      0.57      0.47        30
         QSO       0.86      0.74      0.79       246

    accuracy                           0.69       377
   macro avg       0.58      0.64      0.60       377
weighted avg       0.72      0.69      0.70       377


==============================================================================================================
RESUMEN (ordenado por AUC_test, luego CV val AUC)
==============================================================================================================
     modelo                                                                                                                                                   params  rs_mean_auc_cv  rs_std_auc_cv  rs_gap_auc_cv  cv_train_mean_auc  cv_val_mean_auc  cv_val_std_auc  cv_gap_auc  train_auc  test_auc  test_acc  test_f1_weighted
RF_rob_top3  {'criterion': 'gini', 'max_depth': None, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 6, 'min_samples_split': 29, 'n_estimators': 2465}        0.729765       0.027618       0.176053           0.904797         0.731719        0.022077    0.173078   0.907234  0.836617  0.732095          0.740755
RF_rob_top1 {'criterion': 'gini', 'max_depth': None, 'max_features': 0.5, 'max_samples': 0.6, 'min_samples_leaf': 14, 'min_samples_split': 24, 'n_estimators': 2273}        0.732185       0.026969       0.158408           0.890043         0.732816        0.022062    0.157227   0.892732  0.820725  0.708223          0.717348
RF_rob_top4   {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.3, 'max_samples': 0.6, 'min_samples_leaf': 10, 'min_samples_split': 34, 'n_estimators': 3091}        0.729449       0.028214       0.154796           0.883001         0.730700        0.022043    0.152301   0.886293  0.816212  0.702918          0.713697
RF_rob_top5   {'criterion': 'gini', 'max_depth': 19, 'max_features': 0.7, 'max_samples': 0.8, 'min_samples_leaf': 22, 'min_samples_split': 20, 'n_estimators': 3657}        0.729017       0.028157       0.142740           0.870971         0.730614        0.022013    0.140358   0.875505  0.804637  0.687003          0.699717
RF_rob_top2   {'criterion': 'gini', 'max_depth': 11, 'max_features': 0.6, 'max_samples': 0.4, 'min_samples_leaf': 12, 'min_samples_split': 31, 'n_estimators': 3704}        0.729792       0.028591       0.129095           0.857912         0.730437        0.022136    0.127474   0.861872  0.793280  0.671088          0.681774

Tiempo total: 02:15:49
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_1.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_2.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_3.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_4.png
✅ Guardado: /home/felorrieta/Downloads/RF_IISIG_FIRMA_5.png

Tiempo total guardando figuras: 00:02:22
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[18], line 625
    623 feat_idx = pd.to_numeric(pd.Index(X_train.columns).astype(str), errors="coerce")
    624 if feat_idx.isna().any():
--> 625     raise ValueError("Tus columnas no se pudieron convertir a números. Revisa X_train.columns.")
    627 feat_idx = feat_idx.astype(int)
    628 min_idx, max_idx = int(feat_idx.min()), int(feat_idx.max())

ValueError: Tus columnas no se pudieron convertir a números. Revisa X_train.columns.

2.2.3 GRÁFICO DE IMPORTANCIAS - DATOS REALES

Ver código
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

csv_path = ('/home/felorrieta/Downloads/path_signature_iisignature_M9.csv')
output_dir = ('/home/felorrieta/Catalina/resultados_path_signature_iisignature_M9.csv')
os.makedirs(output_dir, exist_ok=True)

target_col = "class"   
drop_cols = ["id"]     

depth_M = 9
path_dimension = 2   # tiempo y magnitud

test_size = 0.20
random_state = 42

def signature_feature_counts_per_level(d, M):
    """
    Cantidad de features por nivel para firma estándar truncada.
    Ejemplo: d=2, M=3 -> [2, 4, 8]
    """
    return [d**k for k in range(1, M + 1)]


def build_level_vector(d, M):
    """
    Crea un vector que asigna a cada feature su nivel de firma.
    """
    counts = signature_feature_counts_per_level(d, M)
    levels = []
    for level, count in enumerate(counts, start=1):
        levels.extend([level] * count)
    return np.array(levels)

df = pd.read_csv(csv_path)

if target_col not in df.columns:
    raise ValueError(f"No se encontró la columna objetivo '{target_col}' en el archivo.")

drop_cols_existing = [c for c in drop_cols if c in df.columns]

feature_cols = [c for c in df.columns if c not in drop_cols_existing + [target_col]]

X = df[feature_cols].select_dtypes(include=[np.number]).copy()
y = df[target_col].copy()

if X.shape[1] == 0:
    raise ValueError("No se encontraron features numéricas.")

expected_n_features = sum(signature_feature_counts_per_level(path_dimension, depth_M))
if X.shape[1] != expected_n_features:
    raise ValueError(
        f"Cantidad de features inesperada.\n"
        f"Esperadas: {expected_n_features}\n"
        f"Encontradas: {X.shape[1]}\n"
        f"Revisa si hay columnas extra o si el archivo no corresponde a firma estándar M={depth_M}."
    )

le = LabelEncoder()
y_encoded = le.fit_transform(y)

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y_encoded,
    test_size=test_size,
    stratify=y_encoded,
    random_state=random_state
)

rf = RandomForestClassifier(
    n_estimators=3141,
    criterion="entropy",
    max_depth=15,
    max_features=0.8,
    max_samples=0.8,
    min_samples_leaf=13,
    min_samples_split=41,
    bootstrap=True,
    random_state=random_state,
    n_jobs=-1
)

rf.fit(X_train, y_train)

y_pred_train = rf.predict(X_train)
y_pred_test = rf.predict(X_test)

metrics_summary = pd.DataFrame({
    "Split": ["Train", "Test"],
    "Accuracy": [
        accuracy_score(y_train, y_pred_train),
        accuracy_score(y_test, y_pred_test)
    ],
    "Precision_weighted": [
        precision_score(y_train, y_pred_train, average="weighted", zero_division=0),
        precision_score(y_test, y_pred_test, average="weighted", zero_division=0)
    ],
    "Recall_weighted": [
        recall_score(y_train, y_pred_train, average="weighted", zero_division=0),
        recall_score(y_test, y_pred_test, average="weighted", zero_division=0)
    ],
    "F1_weighted": [
        f1_score(y_train, y_pred_train, average="weighted", zero_division=0),
        f1_score(y_test, y_pred_test, average="weighted", zero_division=0)
    ]
})

print("\nMÉTRICAS DEL MODELO")
print(metrics_summary.round(4))

metrics_summary.to_csv(
    os.path.join(output_dir, "metricas_rf_iisig_firma_reales.csv"),
    index=False
)

importances = rf.feature_importances_
levels = build_level_vector(path_dimension, depth_M)

importance_df = pd.DataFrame({
    "feature": X.columns,
    "level": levels,
    "importance": importances
})

importance_df.to_csv(
    os.path.join(output_dir, "importancias_individuales_rf_iisig_firma_reales.csv"),
    index=False
)

importance_by_level = (
    importance_df
    .groupby("level", as_index=False)
    .agg(
        mean_importance=("importance", "mean"),
        median_importance=("importance", "median"),
        total_importance=("importance", "sum"),
        n_features=("importance", "size")
    )
)

importance_by_level["pct_total_importance"] = (
    100 * importance_by_level["total_importance"] / importance_by_level["total_importance"].sum()
)

print("\n=== IMPORTANCIA POR NIVEL ===")
print(importance_by_level.round(6))

importance_by_level.to_csv(
    os.path.join(output_dir, "importancia_por_nivel_rf_iisig_firma_reales.csv"),
    index=False
)

x = importance_by_level["level"].values
mean_vals = importance_by_level["mean_importance"].values
median_vals = importance_by_level["median_importance"].values

bar_width = 0.38

plt.figure(figsize=(10, 6))
plt.bar(x - bar_width/2, mean_vals, width=bar_width, label="Promedio")
plt.bar(x + bar_width/2, median_vals, width=bar_width, label="Mediana")

plt.xlabel("Nivel de firma")
plt.ylabel("Importancia")
plt.title("Importancia de características por nivel\nRandom Forest - iisignature firma estándar (datos reales)")
plt.xticks(x)
plt.legend()
plt.grid(axis="y", alpha=0.3)
plt.tight_layout()

plt.savefig(
    os.path.join(output_dir, "grafica_importancia_media_mediana_rf_iisig_firma_reales.png"),
    dpi=300,
    bbox_inches="tight"
)
plt.show()

plt.figure(figsize=(10, 6))
plt.bar(
    importance_by_level["level"],
    importance_by_level["pct_total_importance"]
)

plt.xlabel("Nivel de firma")
plt.ylabel("% de importancia total")
plt.title("Porcentaje de importancia total por nivel\nRandom Forest - iisignature firma estándar (datos reales)")
plt.xticks(importance_by_level["level"])
plt.grid(axis="y", alpha=0.3)
plt.tight_layout()

plt.savefig(
    os.path.join(output_dir, "grafica_importancia_total_rf_iisig_firma_reales.png"),
    dpi=300,
    bbox_inches="tight"
)
plt.show()

print("\nArchivos guardados en:")
print(output_dir)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[19], line 69
     66 df = pd.read_csv(csv_path)
     68 if target_col not in df.columns:
---> 69     raise ValueError(f"No se encontró la columna objetivo '{target_col}' en el archivo.")
     71 drop_cols_existing = [c for c in drop_cols if c in df.columns]
     73 feature_cols = [c for c in df.columns if c not in drop_cols_existing + [target_col]]

ValueError: No se encontró la columna objetivo 'class' en el archivo.

2.2.4 PROFUNDIDAD

Ver código
import os
import numpy as np
import pandas as pd

from tqdm import tqdm

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
from sklearn.utils.class_weight import compute_class_weight

signatures_path = "/home/felorrieta/Downloads/path_signature_iisignature_M9.csv"
xmatch_path = "/home/felorrieta/Catalina/ts_v9.0.1_SMBH_ZTF_xmatch.csv"

id_col_sig = "id"

possible_id_cols_xmatch = [
    "id", "ID", "oid", "OID", "objectId", "object_id",
    "ztf_id", "ZTFID", "source_id"
]

possible_label_cols = [
    "survey_class_mapped", "class", "Class", "clase", "Clase",
    "label", "Label", "target", "Target"
]

valid_classes = ["AGN", "Blazar", "QSO"]

output_dir = "/home/felorrieta/Downloads/resultados_rf_por_nivel_reales"
os.makedirs(output_dir, exist_ok=True)

random_state_split = 42
random_state_model = 100

best_params_sig = {
    "criterion": "entropy",
    "max_depth": 15,
    "max_features": 0.8,
    "max_samples": 0.8,
    "min_samples_leaf": 13,
    "min_samples_split": 41,
    "n_estimators": 3141
}

df_sig = pd.read_csv(signatures_path)
df_xmatch = pd.read_csv(xmatch_path)

print("Shape firmas:", df_sig.shape)
print("Shape xmatch :", df_xmatch.shape)

print("\nColumnas firmas:")
print(df_sig.columns.tolist()[:15], "...")

print("\nColumnas xmatch:")
print(df_xmatch.columns.tolist())

if id_col_sig not in df_sig.columns:
    raise ValueError(f"No existe la columna '{id_col_sig}' en el archivo de firmas.")

id_candidates = [c for c in possible_id_cols_xmatch if c in df_xmatch.columns]
if len(id_candidates) == 0:
    raise ValueError(
        "No pude detectar automáticamente la columna ID en el xmatch.\n"
        "Revisa las columnas impresas arriba y define manualmente id_col_xmatch."
    )
id_col_xmatch = id_candidates[0]
print(f"\nColumna ID detectada en xmatch: {id_col_xmatch}")

label_candidates = [c for c in possible_label_cols if c in df_xmatch.columns]
if len(label_candidates) == 0:
    raise ValueError(
        "No pude detectar automáticamente la columna de clase en el xmatch.\n"
        "Revisa las columnas impresas arriba y define manualmente label_col."
    )
label_col = label_candidates[0]
print(f"Columna de clase detectada en xmatch: {label_col}")

df_full = df_sig.merge(
    df_xmatch[[id_col_xmatch, label_col]].drop_duplicates(),
    left_on=id_col_sig,
    right_on=id_col_xmatch,
    how="inner"
)

if id_col_xmatch != id_col_sig and id_col_xmatch in df_full.columns:
    df_full = df_full.drop(columns=[id_col_xmatch])

df_full = df_full.dropna(subset=[label_col]).copy()
df_full = df_full[df_full[label_col].isin(valid_classes)].copy()

print("\nShape después del merge y filtrado:", df_full.shape)
print("Clases presentes:", sorted(df_full[label_col].unique().tolist()))

if df_full.empty:
    raise ValueError("El dataframe final quedó vacío. Revisa el merge o las clases.")

feature_cols = [c for c in df_full.columns if str(c).startswith("sig_")]
if len(feature_cols) == 0:
    raise ValueError("No se encontraron columnas tipo sig_k.")

X = df_full[feature_cols].copy()
y = df_full[label_col].copy()

print("\nShape X completo:", X.shape)
print("Primeras columnas:", X.columns[:5].tolist())
print("Últimas columnas :", X.columns[-5:].tolist())

le = LabelEncoder()
y_encoded = le.fit_transform(y)

print("\nMapeo de clases:")
for i, cls in enumerate(le.classes_):
    print(f"{i} -> {cls}")

X_train, X_test, y_train_encoded, y_test_encoded = train_test_split(
    X,
    y_encoded,
    test_size=0.20,
    stratify=y_encoded,
    random_state=random_state_split
)

print("\nShape X_train:", X_train.shape)
print("Shape X_test :", X_test.shape)

classes_int = np.unique(y_train_encoded)
weights = compute_class_weight(
    class_weight="balanced",
    classes=classes_int,
    y=y_train_encoded
)
class_weight_dict = {cls: w for cls, w in zip(classes_int, weights)}

print("\nclass_weight_dict =", class_weight_dict)

feat_idx = (
    pd.Index(X_train.columns.astype(str))
    .str.extract(r"sig_(\d+)", expand=False)
)

if feat_idx.isna().any():
    bad = X_train.columns[feat_idx.isna()].tolist()[:10]
    raise ValueError(f"No se pudieron interpretar algunas columnas como sig_k. Ejemplos: {bad}")

feat_idx = feat_idx.astype(int)

pairs_sorted = sorted(zip(feat_idx.values, X_train.columns), key=lambda t: t[0])
nums_sorted = np.array([p[0] for p in pairs_sorted])
cols_sorted = [p[1] for p in pairs_sorted]

print("\nRango detectado de índices:", nums_sorted.min(), "..", nums_sorted.max())

rows = []

for m in tqdm(range(1, 10), desc="Evaluando niveles firma (AUC)", unit="nivel"):
    n_features_m = (2 ** (m + 1)) - 2

    selected = [c for n, c in zip(nums_sorted, cols_sorted) if 0 <= n < n_features_m]

    Xtr_m = X_train[selected]
    Xte_m = X_test[selected]

    rf = RandomForestClassifier(
        random_state=random_state_model,
        n_jobs=-1,
        class_weight=class_weight_dict,
        bootstrap=True,
        **best_params_sig
    )

    rf.fit(Xtr_m, y_train_encoded)

    pred_tr = rf.predict(Xtr_m)
    pred_te = rf.predict(Xte_m)

    proba_tr = rf.predict_proba(Xtr_m)
    proba_te = rf.predict_proba(Xte_m)

    acc_tr = accuracy_score(y_train_encoded, pred_tr)
    f1_tr  = f1_score(y_train_encoded, pred_tr, average="weighted", zero_division=0)
    auc_tr = roc_auc_score(y_train_encoded, proba_tr, multi_class="ovr", average="weighted")

    acc_te = accuracy_score(y_test_encoded, pred_te)
    f1_te  = f1_score(y_test_encoded, pred_te, average="weighted", zero_division=0)
    auc_te = roc_auc_score(y_test_encoded, proba_te, multi_class="ovr", average="weighted")

    rows.append({
        "NivelFirma": m,
        "N_features": Xtr_m.shape[1],
        "AccTrain": acc_tr,
        "F1Train": f1_tr,
        "AUCTrain": auc_tr,
        "AccTest": acc_te,
        "F1Test": f1_te,
        "AUCTest": auc_te
    })

df_levels = pd.DataFrame(rows)

expected_features = [2, 6, 14, 30, 62, 126, 254, 510, 1022]
obtained_features = df_levels["N_features"].tolist()

print("\nEsperado :", expected_features)
print("Obtenido :", obtained_features)

if obtained_features != expected_features:
    print("\nAdvertencia: el número de features por nivel no coincide con lo esperado.")

best_auc = df_levels["AUCTest"].max()
tol = 1e-4
best_candidates = df_levels[df_levels["AUCTest"] >= best_auc - tol]
best_simple = best_candidates.sort_values("NivelFirma").iloc[0]

print("RESULTADOS POR NIVEL — criterio AUC_test")
print(df_levels.to_string(index=False, float_format=lambda x: f"{x:.3f}"))

print("NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test (con tolerancia)")
print(best_simple.to_string())

csv_out = os.path.join(output_dir, "rf_reales_por_nivel.csv")
df_levels.to_csv(csv_out, index=False)
print(f"\nResultados guardados en: {csv_out}")

print("FILAS LaTeX (sin AUC)")
print("=" * 95)
for _, r in df_levels.iterrows():
    print(
        f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
        f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & "
        f"{r['AccTest']:.3f} & {r['F1Test']:.3f} \\\\"
    )

print("FILAS LaTeX")
for _, r in df_levels.iterrows():
    print(
        f"{int(r['NivelFirma'])} & {int(r['N_features'])} & "
        f"{r['AccTrain']:.3f} & {r['F1Train']:.3f} & "
        f"{r['AccTest']:.3f} & {r['F1Test']:.3f} & {r['AUCTest']:.3f} \\\\"
    )
Shape firmas: (1887, 1023)
Shape xmatch : (110951, 4)

Columnas firmas:
['id', 'sig_0', 'sig_1', 'sig_2', 'sig_3', 'sig_4', 'sig_5', 'sig_6', 'sig_7', 'sig_8', 'sig_9', 'sig_10', 'sig_11', 'sig_12', 'sig_13'] ...

Columnas xmatch:
['oid', 'survey_class_mapped', 'survey_class', 'survey_class_cat']

Columna ID detectada en xmatch: oid
Columna de clase detectada en xmatch: survey_class_mapped

Shape después del merge y filtrado: (1887, 1024)
Clases presentes: ['AGN', 'Blazar', 'QSO']

Shape X completo: (1887, 1022)
Primeras columnas: ['sig_0', 'sig_1', 'sig_2', 'sig_3', 'sig_4']
Últimas columnas : ['sig_1017', 'sig_1018', 'sig_1019', 'sig_1020', 'sig_1021']

Mapeo de clases:
0 -> AGN
1 -> Blazar
2 -> QSO

Shape X_train: (1509, 1022)
Shape X_test : (378, 1022)

class_weight_dict = {0: 1.2091346153846154, 1: 2.6613756613756614, 2: 0.5564159292035398}

Rango detectado de índices: 0 .. 1021
Evaluando niveles firma (AUC): 100%|███████████| 9/9 [14:40<00:00, 97.78s/nivel]

Esperado : [2, 6, 14, 30, 62, 126, 254, 510, 1022]
Obtenido : [2, 6, 14, 30, 62, 126, 254, 510, 1022]

===============================================================================================
RESULTADOS POR NIVEL — criterio AUC_test
===============================================================================================
 NivelFirma  N_features  AccTrain  F1Train  AUCTrain  AccTest  F1Test  AUCTest
          1           2     0.587    0.477     0.523    0.590   0.470    0.512
          2           6     0.536    0.551     0.687    0.402   0.415    0.533
          3          14     0.662    0.670     0.815    0.545   0.547    0.654
          4          30     0.706    0.713     0.848    0.566   0.573    0.662
          5          62     0.742    0.747     0.874    0.577   0.586    0.683
          6         126     0.775    0.778     0.897    0.593   0.602    0.704
          7         254     0.795    0.798     0.910    0.595   0.604    0.723
          8         510     0.820    0.822     0.922    0.601   0.606    0.737
          9        1022     0.837    0.839     0.932    0.611   0.616    0.747

===============================================================================================
NIVEL MÁS SIMPLE QUE MAXIMIZA AUC_test (con tolerancia)
===============================================================================================
NivelFirma       9.000000
N_features    1022.000000
AccTrain         0.836978
F1Train          0.838869
AUCTrain         0.931841
AccTest          0.611111
F1Test           0.615648
AUCTest          0.747273

Resultados guardados en: /home/felorrieta/Downloads/resultados_rf_por_nivel_reales/rf_reales_por_nivel.csv

===============================================================================================
FILAS LaTeX (tabla ORIGINAL: sin AUC)
===============================================================================================
1 & 2 & 0.587 & 0.477 & 0.590 & 0.470 \\
2 & 6 & 0.536 & 0.551 & 0.402 & 0.415 \\
3 & 14 & 0.662 & 0.670 & 0.545 & 0.547 \\
4 & 30 & 0.706 & 0.713 & 0.566 & 0.573 \\
5 & 62 & 0.742 & 0.747 & 0.577 & 0.586 \\
6 & 126 & 0.775 & 0.778 & 0.593 & 0.602 \\
7 & 254 & 0.795 & 0.798 & 0.595 & 0.604 \\
8 & 510 & 0.820 & 0.822 & 0.601 & 0.606 \\
9 & 1022 & 0.837 & 0.839 & 0.611 & 0.616 \\

===============================================================================================
FILAS LaTeX (tabla EXTENDIDA: incluye AUC_test)
===============================================================================================
1 & 2 & 0.587 & 0.477 & 0.590 & 0.470 & 0.512 \\
2 & 6 & 0.536 & 0.551 & 0.402 & 0.415 & 0.533 \\
3 & 14 & 0.662 & 0.670 & 0.545 & 0.547 & 0.654 \\
4 & 30 & 0.706 & 0.713 & 0.566 & 0.573 & 0.662 \\
5 & 62 & 0.742 & 0.747 & 0.577 & 0.586 & 0.683 \\
6 & 126 & 0.775 & 0.778 & 0.593 & 0.602 & 0.704 \\
7 & 254 & 0.795 & 0.798 & 0.595 & 0.604 & 0.723 \\
8 & 510 & 0.820 & 0.822 & 0.601 & 0.606 & 0.737 \\
9 & 1022 & 0.837 & 0.839 & 0.611 & 0.616 & 0.747 \\