feat: add data augmentation and new optuna tuning. #6 , #9
Browse files
notebooks/06_Optuna_v2_final.ipynb
DELETED
|
@@ -1,1040 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"cells": [
|
| 3 |
-
{
|
| 4 |
-
"cell_type": "markdown",
|
| 5 |
-
"metadata": {
|
| 6 |
-
"id": "d8d7a65f"
|
| 7 |
-
},
|
| 8 |
-
"source": [
|
| 9 |
-
"# βοΈ Notebook 06 β OptimizaciΓ³n con Optuna + LinearSVC\n",
|
| 10 |
-
"## YouTube Hate Speech Detection\n",
|
| 11 |
-
"\n",
|
| 12 |
-
"### ΒΏQuΓ© hace este notebook?\n",
|
| 13 |
-
"Optimizamos LR y RF con Optuna, aΓ±adimos LinearSVC como modelo adicional,\n",
|
| 14 |
-
"y comparamos 5 modelos en una tabla unificada.\n",
|
| 15 |
-
"\n",
|
| 16 |
-
"### Nota metodolΓ³gica β dos mΓ©tricas de GAP\n",
|
| 17 |
-
"Este notebook reporta dos mΓ©tricas de gap distintas:\n",
|
| 18 |
-
"\n",
|
| 19 |
-
"| MΓ©trica | FΓ³rmula | QuΓ© mide |\n",
|
| 20 |
-
"|---|---|---|\n",
|
| 21 |
-
"| `train_test_gap` | f1_train - f1_test | MemorizaciΓ³n / ajuste in-sample |\n",
|
| 22 |
-
"| `cv_test_gap` | \\|cv_mean - f1_test\\| | Estabilidad de generalizaciΓ³n (OOS vs OOS) |\n",
|
| 23 |
-
"\n",
|
| 24 |
-
"La rΓΊbrica exige < 5pp. El `cv_test_gap` es la comparaciΓ³n\n",
|
| 25 |
-
"estadΓsticamente correcta porque ambos tΓ©rminos son out-of-sample.\n",
|
| 26 |
-
"Ver informe metodolΓ³gico para la justificaciΓ³n completa.\n",
|
| 27 |
-
"\n",
|
| 28 |
-
"### Modelos comparados\n",
|
| 29 |
-
"1. LR baseline (cargado desde disco)\n",
|
| 30 |
-
"2. RF baseline (cargado desde disco)\n",
|
| 31 |
-
"3. LinearSVC (nuevo β muy competitivo en TF-IDF sparse)\n",
|
| 32 |
-
"4. LR tuned (Optuna)\n",
|
| 33 |
-
"5. RF tuned (Optuna)\n",
|
| 34 |
-
"\n",
|
| 35 |
-
"### Sin hardcoding\n",
|
| 36 |
-
"Todos los valores se calculan en el momento desde los modelos en disco."
|
| 37 |
-
]
|
| 38 |
-
},
|
| 39 |
-
{
|
| 40 |
-
"cell_type": "markdown",
|
| 41 |
-
"metadata": {
|
| 42 |
-
"id": "26ed562f"
|
| 43 |
-
},
|
| 44 |
-
"source": [
|
| 45 |
-
"## 0. Imports y configuraciΓ³n"
|
| 46 |
-
]
|
| 47 |
-
},
|
| 48 |
-
{
|
| 49 |
-
"cell_type": "code",
|
| 50 |
-
"execution_count": 2,
|
| 51 |
-
"metadata": {},
|
| 52 |
-
"outputs": [
|
| 53 |
-
{
|
| 54 |
-
"name": "stdout",
|
| 55 |
-
"output_type": "stream",
|
| 56 |
-
"text": [
|
| 57 |
-
"kernel\n"
|
| 58 |
-
]
|
| 59 |
-
}
|
| 60 |
-
],
|
| 61 |
-
"source": [
|
| 62 |
-
"print(\"kernel\")"
|
| 63 |
-
]
|
| 64 |
-
},
|
| 65 |
-
{
|
| 66 |
-
"cell_type": "code",
|
| 67 |
-
"execution_count": 3,
|
| 68 |
-
"metadata": {
|
| 69 |
-
"id": "d57a4942"
|
| 70 |
-
},
|
| 71 |
-
"outputs": [
|
| 72 |
-
{
|
| 73 |
-
"name": "stderr",
|
| 74 |
-
"output_type": "stream",
|
| 75 |
-
"text": [
|
| 76 |
-
"/home/under/miniconda3/envs/py310/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
|
| 77 |
-
" from .autonotebook import tqdm as notebook_tqdm\n"
|
| 78 |
-
]
|
| 79 |
-
},
|
| 80 |
-
{
|
| 81 |
-
"name": "stdout",
|
| 82 |
-
"output_type": "stream",
|
| 83 |
-
"text": [
|
| 84 |
-
"PROJECT_ROOT: /mnt/c/Users/under/Documents/F5/3_Projects/Project_9_Equipo3/Project_YT\n"
|
| 85 |
-
]
|
| 86 |
-
}
|
| 87 |
-
],
|
| 88 |
-
"source": [
|
| 89 |
-
"import sys, yaml, joblib, warnings\n",
|
| 90 |
-
"import numpy as np\n",
|
| 91 |
-
"import pandas as pd\n",
|
| 92 |
-
"import matplotlib.pyplot as plt\n",
|
| 93 |
-
"import mlflow, mlflow.sklearn\n",
|
| 94 |
-
"import optuna\n",
|
| 95 |
-
"from pathlib import Path\n",
|
| 96 |
-
"from sklearn.svm import LinearSVC\n",
|
| 97 |
-
"from sklearn.calibration import CalibratedClassifierCV\n",
|
| 98 |
-
"from sklearn.ensemble import RandomForestClassifier\n",
|
| 99 |
-
"from sklearn.linear_model import LogisticRegression\n",
|
| 100 |
-
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
|
| 101 |
-
"from sklearn.pipeline import Pipeline\n",
|
| 102 |
-
"from sklearn.model_selection import (\n",
|
| 103 |
-
" train_test_split, StratifiedKFold,\n",
|
| 104 |
-
" cross_val_score, cross_validate\n",
|
| 105 |
-
")\n",
|
| 106 |
-
"from sklearn.metrics import f1_score, roc_auc_score, classification_report\n",
|
| 107 |
-
"warnings.filterwarnings('ignore')\n",
|
| 108 |
-
"optuna.logging.set_verbosity(optuna.logging.WARNING)\n",
|
| 109 |
-
"\n",
|
| 110 |
-
"PROJECT_ROOT = Path.cwd().parent\n",
|
| 111 |
-
"sys.path.insert(0, str(PROJECT_ROOT))\n",
|
| 112 |
-
"plt.rcParams['figure.figsize'] = (12, 5)\n",
|
| 113 |
-
"plt.rcParams['axes.spines.top'] = False\n",
|
| 114 |
-
"plt.rcParams['axes.spines.right'] = False\n",
|
| 115 |
-
"print(f'PROJECT_ROOT: {PROJECT_ROOT}')"
|
| 116 |
-
]
|
| 117 |
-
},
|
| 118 |
-
{
|
| 119 |
-
"cell_type": "code",
|
| 120 |
-
"execution_count": 4,
|
| 121 |
-
"metadata": {
|
| 122 |
-
"id": "44ca1814"
|
| 123 |
-
},
|
| 124 |
-
"outputs": [
|
| 125 |
-
{
|
| 126 |
-
"name": "stdout",
|
| 127 |
-
"output_type": "stream",
|
| 128 |
-
"text": [
|
| 129 |
-
"LR baseline: True | RF baseline: True\n"
|
| 130 |
-
]
|
| 131 |
-
}
|
| 132 |
-
],
|
| 133 |
-
"source": [
|
| 134 |
-
"CONFIG_FEAT = PROJECT_ROOT / 'configs' / 'features.yaml'\n",
|
| 135 |
-
"CONFIG_PIPE = PROJECT_ROOT / 'configs' / 'pipeline.yaml'\n",
|
| 136 |
-
"CONFIG_MOD = PROJECT_ROOT / 'configs' / 'models.yaml'\n",
|
| 137 |
-
"\n",
|
| 138 |
-
"with open(CONFIG_FEAT) as f: feat_cfg = yaml.safe_load(f)\n",
|
| 139 |
-
"with open(CONFIG_PIPE) as f: pipe_cfg = yaml.safe_load(f)\n",
|
| 140 |
-
"with open(CONFIG_MOD) as f: mod_cfg = yaml.safe_load(f)\n",
|
| 141 |
-
"\n",
|
| 142 |
-
"tfidf_cfg = feat_cfg['vectorization']['tfidf']\n",
|
| 143 |
-
"rf_cfg = mod_cfg['models']['random_forest']\n",
|
| 144 |
-
"lr_cfg = mod_cfg['models']['logistic_regression']\n",
|
| 145 |
-
"TARGET = pipe_cfg['data']['target_binary']\n",
|
| 146 |
-
"RAND = pipe_cfg['pipeline']['random_state']\n",
|
| 147 |
-
"TEST_SIZE = pipe_cfg['pipeline']['test_size']\n",
|
| 148 |
-
"CV_FOLDS = pipe_cfg['pipeline']['cv_folds']\n",
|
| 149 |
-
"\n",
|
| 150 |
-
"MODEL_LR = PROJECT_ROOT / 'models' / 'lr_baseline.joblib'\n",
|
| 151 |
-
"MODEL_RF = PROJECT_ROOT / 'models' / 'best_ensemble.joblib'\n",
|
| 152 |
-
"print(f'LR baseline: {MODEL_LR.exists()} | RF baseline: {MODEL_RF.exists()}')"
|
| 153 |
-
]
|
| 154 |
-
},
|
| 155 |
-
{
|
| 156 |
-
"cell_type": "markdown",
|
| 157 |
-
"metadata": {
|
| 158 |
-
"id": "e02c2668"
|
| 159 |
-
},
|
| 160 |
-
"source": [
|
| 161 |
-
"## 1. Carga de datos y split\n",
|
| 162 |
-
"\n",
|
| 163 |
-
"Mismo `random_state` fijo en todos los notebooks β comparaciΓ³n justa."
|
| 164 |
-
]
|
| 165 |
-
},
|
| 166 |
-
{
|
| 167 |
-
"cell_type": "code",
|
| 168 |
-
"execution_count": 5,
|
| 169 |
-
"metadata": {
|
| 170 |
-
"id": "c2af1236"
|
| 171 |
-
},
|
| 172 |
-
"outputs": [
|
| 173 |
-
{
|
| 174 |
-
"name": "stdout",
|
| 175 |
-
"output_type": "stream",
|
| 176 |
-
"text": [
|
| 177 |
-
"Train: 800 | Test: 200\n"
|
| 178 |
-
]
|
| 179 |
-
}
|
| 180 |
-
],
|
| 181 |
-
"source": [
|
| 182 |
-
"PROCESSED = PROJECT_ROOT / 'data' / 'processed' / 'v2' / 'comments_preprocessed.csv'\n",
|
| 183 |
-
"df = pd.read_csv(PROCESSED)\n",
|
| 184 |
-
"df['clean_text'] = df['clean_text'].fillna('').astype(str)\n",
|
| 185 |
-
"X, y = df['clean_text'], df[TARGET]\n",
|
| 186 |
-
"\n",
|
| 187 |
-
"X_train, X_test, y_train, y_test = train_test_split(\n",
|
| 188 |
-
" X, y, test_size=TEST_SIZE, random_state=RAND, stratify=y\n",
|
| 189 |
-
")\n",
|
| 190 |
-
"cv_strategy = StratifiedKFold(n_splits=CV_FOLDS, shuffle=True, random_state=RAND)\n",
|
| 191 |
-
"print(f'Train: {len(X_train)} | Test: {len(X_test)}')"
|
| 192 |
-
]
|
| 193 |
-
},
|
| 194 |
-
{
|
| 195 |
-
"cell_type": "markdown",
|
| 196 |
-
"metadata": {
|
| 197 |
-
"id": "9c263c3b"
|
| 198 |
-
},
|
| 199 |
-
"source": [
|
| 200 |
-
"## 2. FunciΓ³n de evaluaciΓ³n\n",
|
| 201 |
-
"\n",
|
| 202 |
-
"Calcula ambas mΓ©tricas de gap. `cv_scores` es opcional β\n",
|
| 203 |
-
"solo los modelos evaluados con `cross_validate` tendrΓ‘n `cv_test_gap`."
|
| 204 |
-
]
|
| 205 |
-
},
|
| 206 |
-
{
|
| 207 |
-
"cell_type": "code",
|
| 208 |
-
"execution_count": 6,
|
| 209 |
-
"metadata": {
|
| 210 |
-
"id": "8bc1d30a"
|
| 211 |
-
},
|
| 212 |
-
"outputs": [],
|
| 213 |
-
"source": [
|
| 214 |
-
"def evaluate_pipeline(pipeline, X_tr, y_tr, X_te, y_te, name, cv_scores=None):\n",
|
| 215 |
-
" \"\"\"\n",
|
| 216 |
-
" Evalua un pipeline y devuelve dict con ambas metricas de gap.\n",
|
| 217 |
-
"\n",
|
| 218 |
-
" train_test_gap : f1_train - f1_test (in-sample vs out-of-sample)\n",
|
| 219 |
-
" cv_test_gap : |cv_mean - f1_test| (OOS vs OOS β para rubrica)\n",
|
| 220 |
-
" \"\"\"\n",
|
| 221 |
-
" y_pred = pipeline.predict(X_te)\n",
|
| 222 |
-
" y_pred_proba = pipeline.predict_proba(X_te)[:, 1]\n",
|
| 223 |
-
" y_pred_train = pipeline.predict(X_tr)\n",
|
| 224 |
-
"\n",
|
| 225 |
-
" f1_te = f1_score(y_te, y_pred, average='weighted')\n",
|
| 226 |
-
" f1_tr = f1_score(y_tr, y_pred_train, average='weighted')\n",
|
| 227 |
-
" roc = roc_auc_score(y_te, y_pred_proba)\n",
|
| 228 |
-
"\n",
|
| 229 |
-
" cv_mean = cv_std = cv_test_gap = None\n",
|
| 230 |
-
" if cv_scores is not None:\n",
|
| 231 |
-
" cv_mean = cv_scores['test_score'].mean()\n",
|
| 232 |
-
" cv_std = cv_scores['test_score'].std()\n",
|
| 233 |
-
" cv_test_gap = abs(cv_mean - f1_te) * 100\n",
|
| 234 |
-
"\n",
|
| 235 |
-
" return {\n",
|
| 236 |
-
" 'name' : name,\n",
|
| 237 |
-
" 'f1_test' : round(f1_te, 4),\n",
|
| 238 |
-
" 'f1_train' : round(f1_tr, 4),\n",
|
| 239 |
-
" 'train_test_gap_pp': round((f1_tr - f1_te) * 100, 2),\n",
|
| 240 |
-
" 'cv_mean' : round(cv_mean, 4) if cv_mean is not None else None,\n",
|
| 241 |
-
" 'cv_std' : round(cv_std, 4) if cv_std is not None else None,\n",
|
| 242 |
-
" 'cv_test_gap_pp' : round(cv_test_gap, 2) if cv_test_gap is not None else None,\n",
|
| 243 |
-
" 'roc_auc' : round(roc, 4),\n",
|
| 244 |
-
" 'fp' : int(((y_te == False) & (y_pred == True)).sum()),\n",
|
| 245 |
-
" 'fn' : int(((y_te == True) & (y_pred == False)).sum()),\n",
|
| 246 |
-
" }"
|
| 247 |
-
]
|
| 248 |
-
},
|
| 249 |
-
{
|
| 250 |
-
"cell_type": "markdown",
|
| 251 |
-
"metadata": {
|
| 252 |
-
"id": "b7042f8a"
|
| 253 |
-
},
|
| 254 |
-
"source": [
|
| 255 |
-
"## 3. Helper TF-IDF y carga de baselines\n",
|
| 256 |
-
"\n",
|
| 257 |
-
"Los baselines se cargan desde disco y se evalΓΊan sin re-entrenar."
|
| 258 |
-
]
|
| 259 |
-
},
|
| 260 |
-
{
|
| 261 |
-
"cell_type": "code",
|
| 262 |
-
"execution_count": 7,
|
| 263 |
-
"metadata": {
|
| 264 |
-
"id": "891692e9"
|
| 265 |
-
},
|
| 266 |
-
"outputs": [
|
| 267 |
-
{
|
| 268 |
-
"name": "stdout",
|
| 269 |
-
"output_type": "stream",
|
| 270 |
-
"text": [
|
| 271 |
-
" LR baseline F1=0.7531 | train-test=10.91pp | cv-test=5.17pp\n",
|
| 272 |
-
" RF baseline F1=0.7531 | train-test=10.91pp | cv-test=5.17pp\n"
|
| 273 |
-
]
|
| 274 |
-
}
|
| 275 |
-
],
|
| 276 |
-
"source": [
|
| 277 |
-
"def make_tfidf(**overrides):\n",
|
| 278 |
-
" params = {\n",
|
| 279 |
-
" 'max_features': tfidf_cfg['max_features'],\n",
|
| 280 |
-
" 'ngram_range' : tuple(tfidf_cfg['ngram_range']),\n",
|
| 281 |
-
" 'sublinear_tf': tfidf_cfg['sublinear_tf'],\n",
|
| 282 |
-
" 'min_df' : tfidf_cfg['min_df'],\n",
|
| 283 |
-
" 'analyzer' : 'word',\n",
|
| 284 |
-
" 'strip_accents': 'unicode',\n",
|
| 285 |
-
" }\n",
|
| 286 |
-
" params.update(overrides)\n",
|
| 287 |
-
" return TfidfVectorizer(**params)\n",
|
| 288 |
-
"\n",
|
| 289 |
-
"# Cargar baselines desde disco\n",
|
| 290 |
-
"lr_baseline_pipe = joblib.load(MODEL_LR)\n",
|
| 291 |
-
"rf_baseline_pipe = joblib.load(MODEL_RF)\n",
|
| 292 |
-
"\n",
|
| 293 |
-
"# CV scores para baselines\n",
|
| 294 |
-
"cv_lr_base = cross_validate(lr_baseline_pipe, X_train, y_train,\n",
|
| 295 |
-
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 296 |
-
" return_train_score=False, n_jobs=-1)\n",
|
| 297 |
-
"cv_rf_base = cross_validate(rf_baseline_pipe, X_train, y_train,\n",
|
| 298 |
-
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 299 |
-
" return_train_score=False, n_jobs=-1)\n",
|
| 300 |
-
"\n",
|
| 301 |
-
"metrics_lr_base = evaluate_pipeline(lr_baseline_pipe, X_train, y_train,\n",
|
| 302 |
-
" X_test, y_test, 'LR baseline', cv_lr_base)\n",
|
| 303 |
-
"metrics_rf_base = evaluate_pipeline(rf_baseline_pipe, X_train, y_train,\n",
|
| 304 |
-
" X_test, y_test, 'RF baseline', cv_rf_base)\n",
|
| 305 |
-
"\n",
|
| 306 |
-
"for m in [metrics_lr_base, metrics_rf_base]:\n",
|
| 307 |
-
" print(f\" {m['name']:15} F1={m['f1_test']:.4f} | \"\n",
|
| 308 |
-
" f\"train-test={m['train_test_gap_pp']}pp | \"\n",
|
| 309 |
-
" f\"cv-test={m['cv_test_gap_pp']}pp\")"
|
| 310 |
-
]
|
| 311 |
-
},
|
| 312 |
-
{
|
| 313 |
-
"cell_type": "markdown",
|
| 314 |
-
"metadata": {
|
| 315 |
-
"id": "27e76bb8"
|
| 316 |
-
},
|
| 317 |
-
"source": [
|
| 318 |
-
"## 4. LinearSVC β modelo adicional\n",
|
| 319 |
-
"\n",
|
| 320 |
-
"### ΒΏPor quΓ© LinearSVC en TF-IDF?\n",
|
| 321 |
-
"LinearSVC usa un hiperplano de separaciΓ³n lineal que funciona\n",
|
| 322 |
-
"excepcionalmente bien con matrices sparse de alta dimensiΓ³n.\n",
|
| 323 |
-
"No tiene probabilidades nativas, pero `CalibratedClassifierCV`\n",
|
| 324 |
-
"las aΓ±ade mediante Platt scaling.\n",
|
| 325 |
-
"\n",
|
| 326 |
-
"Frecuentemente supera a LR en clasificaciΓ³n de texto porque\n",
|
| 327 |
-
"maximiza el margen entre clases en lugar de minimizar log-loss."
|
| 328 |
-
]
|
| 329 |
-
},
|
| 330 |
-
{
|
| 331 |
-
"cell_type": "code",
|
| 332 |
-
"execution_count": 8,
|
| 333 |
-
"metadata": {
|
| 334 |
-
"id": "6c750ae7"
|
| 335 |
-
},
|
| 336 |
-
"outputs": [
|
| 337 |
-
{
|
| 338 |
-
"name": "stdout",
|
| 339 |
-
"output_type": "stream",
|
| 340 |
-
"text": [
|
| 341 |
-
" LinearSVC F1=0.7250 | train-test=23.99pp | cv-test=4.03pp\n"
|
| 342 |
-
]
|
| 343 |
-
}
|
| 344 |
-
],
|
| 345 |
-
"source": [
|
| 346 |
-
"# LinearSVC con calibraciΓ³n para obtener predict_proba\n",
|
| 347 |
-
"svc_pipeline = Pipeline([\n",
|
| 348 |
-
" ('tfidf', make_tfidf()),\n",
|
| 349 |
-
" ('clf', CalibratedClassifierCV(\n",
|
| 350 |
-
" LinearSVC(\n",
|
| 351 |
-
" C=1.0, max_iter=2000,\n",
|
| 352 |
-
" class_weight='balanced',\n",
|
| 353 |
-
" random_state=RAND,\n",
|
| 354 |
-
" ),\n",
|
| 355 |
-
" cv=3\n",
|
| 356 |
-
" ))\n",
|
| 357 |
-
"])\n",
|
| 358 |
-
"\n",
|
| 359 |
-
"svc_pipeline.fit(X_train, y_train)\n",
|
| 360 |
-
"\n",
|
| 361 |
-
"cv_svc = cross_validate(svc_pipeline, X_train, y_train,\n",
|
| 362 |
-
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 363 |
-
" return_train_score=False, n_jobs=-1)\n",
|
| 364 |
-
"\n",
|
| 365 |
-
"metrics_svc = evaluate_pipeline(svc_pipeline, X_train, y_train,\n",
|
| 366 |
-
" X_test, y_test, 'LinearSVC', cv_svc)\n",
|
| 367 |
-
"\n",
|
| 368 |
-
"print(f\" LinearSVC F1={metrics_svc['f1_test']:.4f} | \"\n",
|
| 369 |
-
" f\"train-test={metrics_svc['train_test_gap_pp']}pp | \"\n",
|
| 370 |
-
" f\"cv-test={metrics_svc['cv_test_gap_pp']}pp\")"
|
| 371 |
-
]
|
| 372 |
-
},
|
| 373 |
-
{
|
| 374 |
-
"cell_type": "markdown",
|
| 375 |
-
"metadata": {
|
| 376 |
-
"id": "a3660dce"
|
| 377 |
-
},
|
| 378 |
-
"source": [
|
| 379 |
-
"## 5. Optuna β Logistic Regression\n",
|
| 380 |
-
"\n",
|
| 381 |
-
"BΓΊsqueda bayesiana sobre el pipeline completo TF-IDF + LR.\n",
|
| 382 |
-
"Optuna solo ve X_train β X_test se reserva para evaluaciΓ³n final."
|
| 383 |
-
]
|
| 384 |
-
},
|
| 385 |
-
{
|
| 386 |
-
"cell_type": "code",
|
| 387 |
-
"execution_count": 9,
|
| 388 |
-
"metadata": {
|
| 389 |
-
"id": "852e0804"
|
| 390 |
-
},
|
| 391 |
-
"outputs": [
|
| 392 |
-
{
|
| 393 |
-
"name": "stdout",
|
| 394 |
-
"output_type": "stream",
|
| 395 |
-
"text": [
|
| 396 |
-
"Optimizando LR β 60 trials...\n"
|
| 397 |
-
]
|
| 398 |
-
},
|
| 399 |
-
{
|
| 400 |
-
"name": "stderr",
|
| 401 |
-
"output_type": "stream",
|
| 402 |
-
"text": [
|
| 403 |
-
"Best trial: 52. Best value: 0.710353: 100%|ββββββββββ| 60/60 [00:05<00:00, 10.25it/s]"
|
| 404 |
-
]
|
| 405 |
-
},
|
| 406 |
-
{
|
| 407 |
-
"name": "stdout",
|
| 408 |
-
"output_type": "stream",
|
| 409 |
-
"text": [
|
| 410 |
-
"Mejor F1 CV: 0.7104\n",
|
| 411 |
-
"Params : {'ngram_range': '1_2', 'max_features': 4045, 'min_df': 2, 'sublinear_tf': False, 'C': 0.3235215031170205}\n"
|
| 412 |
-
]
|
| 413 |
-
},
|
| 414 |
-
{
|
| 415 |
-
"name": "stderr",
|
| 416 |
-
"output_type": "stream",
|
| 417 |
-
"text": [
|
| 418 |
-
"\n"
|
| 419 |
-
]
|
| 420 |
-
}
|
| 421 |
-
],
|
| 422 |
-
"source": [
|
| 423 |
-
"N_TRIALS = 60\n",
|
| 424 |
-
"\n",
|
| 425 |
-
"def objective_lr(trial):\n",
|
| 426 |
-
" ngram_str = trial.suggest_categorical('ngram_range', ['1_1', '1_2'])\n",
|
| 427 |
-
" ngram = (1,1) if ngram_str == '1_1' else (1,2)\n",
|
| 428 |
-
" pipe = Pipeline([\n",
|
| 429 |
-
" ('tfidf', make_tfidf(\n",
|
| 430 |
-
" max_features = trial.suggest_int('max_features', 500, 5000),\n",
|
| 431 |
-
" min_df = trial.suggest_int('min_df', 2, 6),\n",
|
| 432 |
-
" ngram_range = ngram,\n",
|
| 433 |
-
" sublinear_tf = trial.suggest_categorical('sublinear_tf', [True, False]),\n",
|
| 434 |
-
" )),\n",
|
| 435 |
-
" ('clf', LogisticRegression(\n",
|
| 436 |
-
" C = trial.suggest_float('C', 0.01, 2.0, log=True),\n",
|
| 437 |
-
" max_iter = 1000,\n",
|
| 438 |
-
" class_weight = 'balanced',\n",
|
| 439 |
-
" solver = 'lbfgs',\n",
|
| 440 |
-
" random_state = RAND,\n",
|
| 441 |
-
" ))\n",
|
| 442 |
-
" ])\n",
|
| 443 |
-
" return cross_val_score(pipe, X_train, y_train,\n",
|
| 444 |
-
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 445 |
-
" n_jobs=-1).mean()\n",
|
| 446 |
-
"\n",
|
| 447 |
-
"study_lr = optuna.create_study(direction='maximize',\n",
|
| 448 |
-
" sampler=optuna.samplers.TPESampler(seed=RAND),\n",
|
| 449 |
-
" study_name='lr_optimization')\n",
|
| 450 |
-
"print(f'Optimizando LR β {N_TRIALS} trials...')\n",
|
| 451 |
-
"study_lr.optimize(objective_lr, n_trials=N_TRIALS, show_progress_bar=True)\n",
|
| 452 |
-
"print(f'Mejor F1 CV: {study_lr.best_value:.4f}')\n",
|
| 453 |
-
"print(f'Params : {study_lr.best_trial.params}')"
|
| 454 |
-
]
|
| 455 |
-
},
|
| 456 |
-
{
|
| 457 |
-
"cell_type": "code",
|
| 458 |
-
"execution_count": 10,
|
| 459 |
-
"metadata": {
|
| 460 |
-
"id": "27c75e2a"
|
| 461 |
-
},
|
| 462 |
-
"outputs": [
|
| 463 |
-
{
|
| 464 |
-
"name": "stdout",
|
| 465 |
-
"output_type": "stream",
|
| 466 |
-
"text": [
|
| 467 |
-
"LR tuned F1=0.7579 | train-test=14.07pp | cv-test=4.76pp\n"
|
| 468 |
-
]
|
| 469 |
-
}
|
| 470 |
-
],
|
| 471 |
-
"source": [
|
| 472 |
-
"bp_lr = study_lr.best_trial.params\n",
|
| 473 |
-
"ngram_lr = (1,1) if bp_lr['ngram_range'] == '1_1' else (1,2)\n",
|
| 474 |
-
"\n",
|
| 475 |
-
"lr_tuned_pipe = Pipeline([\n",
|
| 476 |
-
" ('tfidf', make_tfidf(\n",
|
| 477 |
-
" max_features=bp_lr['max_features'], min_df=bp_lr['min_df'],\n",
|
| 478 |
-
" ngram_range=ngram_lr, sublinear_tf=bp_lr['sublinear_tf'],\n",
|
| 479 |
-
" )),\n",
|
| 480 |
-
" ('clf', LogisticRegression(\n",
|
| 481 |
-
" C=bp_lr['C'], max_iter=1000,\n",
|
| 482 |
-
" class_weight='balanced', solver='lbfgs', random_state=RAND,\n",
|
| 483 |
-
" ))\n",
|
| 484 |
-
"])\n",
|
| 485 |
-
"lr_tuned_pipe.fit(X_train, y_train)\n",
|
| 486 |
-
"\n",
|
| 487 |
-
"cv_lr_tuned = cross_validate(lr_tuned_pipe, X_train, y_train,\n",
|
| 488 |
-
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 489 |
-
" return_train_score=False, n_jobs=-1)\n",
|
| 490 |
-
"\n",
|
| 491 |
-
"metrics_lr_tuned = evaluate_pipeline(lr_tuned_pipe, X_train, y_train,\n",
|
| 492 |
-
" X_test, y_test, 'LR tuned', cv_lr_tuned)\n",
|
| 493 |
-
"print(f\"LR tuned F1={metrics_lr_tuned['f1_test']:.4f} | \"\n",
|
| 494 |
-
" f\"train-test={metrics_lr_tuned['train_test_gap_pp']}pp | \"\n",
|
| 495 |
-
" f\"cv-test={metrics_lr_tuned['cv_test_gap_pp']}pp\")"
|
| 496 |
-
]
|
| 497 |
-
},
|
| 498 |
-
{
|
| 499 |
-
"cell_type": "markdown",
|
| 500 |
-
"metadata": {
|
| 501 |
-
"id": "927c7687"
|
| 502 |
-
},
|
| 503 |
-
"source": [
|
| 504 |
-
"## 6. Optuna β Random Forest\n",
|
| 505 |
-
"\n",
|
| 506 |
-
"BΓΊsqueda sobre n_estimators, max_depth y min_samples_leaf.\n",
|
| 507 |
-
"RF con bigramas es muy lento β usamos solo unigramas."
|
| 508 |
-
]
|
| 509 |
-
},
|
| 510 |
-
{
|
| 511 |
-
"cell_type": "code",
|
| 512 |
-
"execution_count": 11,
|
| 513 |
-
"metadata": {
|
| 514 |
-
"id": "f37dc04c"
|
| 515 |
-
},
|
| 516 |
-
"outputs": [
|
| 517 |
-
{
|
| 518 |
-
"name": "stdout",
|
| 519 |
-
"output_type": "stream",
|
| 520 |
-
"text": [
|
| 521 |
-
"Optimizando RF β 60 trials...\n"
|
| 522 |
-
]
|
| 523 |
-
},
|
| 524 |
-
{
|
| 525 |
-
"name": "stderr",
|
| 526 |
-
"output_type": "stream",
|
| 527 |
-
"text": [
|
| 528 |
-
"Best trial: 37. Best value: 0.713928: 100%|ββββββββββ| 60/60 [00:55<00:00, 1.09it/s]"
|
| 529 |
-
]
|
| 530 |
-
},
|
| 531 |
-
{
|
| 532 |
-
"name": "stdout",
|
| 533 |
-
"output_type": "stream",
|
| 534 |
-
"text": [
|
| 535 |
-
"Mejor F1 CV: 0.7139\n",
|
| 536 |
-
"Params : {'max_features': 1380, 'min_df': 6, 'n_estimators': 171, 'max_depth': 9, 'min_samples_leaf': 2}\n"
|
| 537 |
-
]
|
| 538 |
-
},
|
| 539 |
-
{
|
| 540 |
-
"name": "stderr",
|
| 541 |
-
"output_type": "stream",
|
| 542 |
-
"text": [
|
| 543 |
-
"\n"
|
| 544 |
-
]
|
| 545 |
-
}
|
| 546 |
-
],
|
| 547 |
-
"source": [
|
| 548 |
-
"def objective_rf(trial):\n",
|
| 549 |
-
" pipe = Pipeline([\n",
|
| 550 |
-
" ('tfidf', make_tfidf(\n",
|
| 551 |
-
" max_features = trial.suggest_int('max_features', 500, 5000),\n",
|
| 552 |
-
" min_df = trial.suggest_int('min_df', 2, 6),\n",
|
| 553 |
-
" ngram_range = (1, 1),\n",
|
| 554 |
-
" sublinear_tf = True,\n",
|
| 555 |
-
" )),\n",
|
| 556 |
-
" ('clf', RandomForestClassifier(\n",
|
| 557 |
-
" n_estimators = trial.suggest_int('n_estimators', 50, 300),\n",
|
| 558 |
-
" max_depth = trial.suggest_int('max_depth', 4, 12),\n",
|
| 559 |
-
" min_samples_leaf = trial.suggest_int('min_samples_leaf', 2, 8),\n",
|
| 560 |
-
" max_features = 'sqrt',\n",
|
| 561 |
-
" class_weight = 'balanced',\n",
|
| 562 |
-
" random_state = RAND, n_jobs=-1,\n",
|
| 563 |
-
" ))\n",
|
| 564 |
-
" ])\n",
|
| 565 |
-
" return cross_val_score(pipe, X_train, y_train,\n",
|
| 566 |
-
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 567 |
-
" n_jobs=-1).mean()\n",
|
| 568 |
-
"\n",
|
| 569 |
-
"study_rf = optuna.create_study(direction='maximize',\n",
|
| 570 |
-
" sampler=optuna.samplers.TPESampler(seed=RAND),\n",
|
| 571 |
-
" study_name='rf_optimization')\n",
|
| 572 |
-
"print(f'Optimizando RF β {N_TRIALS} trials...')\n",
|
| 573 |
-
"study_rf.optimize(objective_rf, n_trials=N_TRIALS, show_progress_bar=True)\n",
|
| 574 |
-
"print(f'Mejor F1 CV: {study_rf.best_value:.4f}')\n",
|
| 575 |
-
"print(f'Params : {study_rf.best_trial.params}')"
|
| 576 |
-
]
|
| 577 |
-
},
|
| 578 |
-
{
|
| 579 |
-
"cell_type": "code",
|
| 580 |
-
"execution_count": 12,
|
| 581 |
-
"metadata": {
|
| 582 |
-
"id": "b5e6782e"
|
| 583 |
-
},
|
| 584 |
-
"outputs": [
|
| 585 |
-
{
|
| 586 |
-
"name": "stdout",
|
| 587 |
-
"output_type": "stream",
|
| 588 |
-
"text": [
|
| 589 |
-
"RF tuned F1=0.6924 | train-test=12.09pp | cv-test=2.15pp\n"
|
| 590 |
-
]
|
| 591 |
-
}
|
| 592 |
-
],
|
| 593 |
-
"source": [
|
| 594 |
-
"bp_rf = study_rf.best_trial.params\n",
|
| 595 |
-
"\n",
|
| 596 |
-
"rf_tuned_pipe = Pipeline([\n",
|
| 597 |
-
" ('tfidf', make_tfidf(\n",
|
| 598 |
-
" max_features=bp_rf['max_features'], min_df=bp_rf['min_df'],\n",
|
| 599 |
-
" ngram_range=(1,1), sublinear_tf=True,\n",
|
| 600 |
-
" )),\n",
|
| 601 |
-
" ('clf', RandomForestClassifier(\n",
|
| 602 |
-
" n_estimators=bp_rf['n_estimators'], max_depth=bp_rf['max_depth'],\n",
|
| 603 |
-
" min_samples_leaf=bp_rf['min_samples_leaf'],\n",
|
| 604 |
-
" max_features='sqrt', class_weight='balanced',\n",
|
| 605 |
-
" random_state=RAND, n_jobs=-1,\n",
|
| 606 |
-
" ))\n",
|
| 607 |
-
"])\n",
|
| 608 |
-
"rf_tuned_pipe.fit(X_train, y_train)\n",
|
| 609 |
-
"\n",
|
| 610 |
-
"cv_rf_tuned = cross_validate(rf_tuned_pipe, X_train, y_train,\n",
|
| 611 |
-
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 612 |
-
" return_train_score=False, n_jobs=-1)\n",
|
| 613 |
-
"\n",
|
| 614 |
-
"metrics_rf_tuned = evaluate_pipeline(rf_tuned_pipe, X_train, y_train,\n",
|
| 615 |
-
" X_test, y_test, 'RF tuned', cv_rf_tuned)\n",
|
| 616 |
-
"print(f\"RF tuned F1={metrics_rf_tuned['f1_test']:.4f} | \"\n",
|
| 617 |
-
" f\"train-test={metrics_rf_tuned['train_test_gap_pp']}pp | \"\n",
|
| 618 |
-
" f\"cv-test={metrics_rf_tuned['cv_test_gap_pp']}pp\")"
|
| 619 |
-
]
|
| 620 |
-
},
|
| 621 |
-
{
|
| 622 |
-
"cell_type": "markdown",
|
| 623 |
-
"metadata": {
|
| 624 |
-
"id": "dae94e0e"
|
| 625 |
-
},
|
| 626 |
-
"source": [
|
| 627 |
-
"## 7. Tabla comparativa β 5 modelos\n",
|
| 628 |
-
"\n",
|
| 629 |
-
"Se reportan ambos gaps. La columna `cv_test_gap` es la referencia\n",
|
| 630 |
-
"para la rΓΊbrica (OOS vs OOS). La columna `train_test_gap` se mantiene\n",
|
| 631 |
-
"para anΓ‘lisis de memorizaciΓ³n."
|
| 632 |
-
]
|
| 633 |
-
},
|
| 634 |
-
{
|
| 635 |
-
"cell_type": "code",
|
| 636 |
-
"execution_count": 13,
|
| 637 |
-
"metadata": {
|
| 638 |
-
"id": "c8f54e1c"
|
| 639 |
-
},
|
| 640 |
-
"outputs": [
|
| 641 |
-
{
|
| 642 |
-
"name": "stdout",
|
| 643 |
-
"output_type": "stream",
|
| 644 |
-
"text": [
|
| 645 |
-
"COMPARATIVA FINAL β 5 MODELOS\n",
|
| 646 |
-
"====================================================================================================\n",
|
| 647 |
-
"Modelo F1 Test F1 Train TrTe gap CV Mean CV Std CV-Te gap FP FN Rubrica\n",
|
| 648 |
-
"----------------------------------------------------------------------------------------------------\n",
|
| 649 |
-
" LR baseline 0.7531 0.8623 10.91 0.7015 0.0312 5.17 19 30 β οΈ 5.2pp\n",
|
| 650 |
-
" LR tuned 0.7579 0.8987 14.07 0.7104 0.0353 4.76 18 30 β
OK\n",
|
| 651 |
-
" RF baseline 0.7531 0.8623 10.91 0.7015 0.0312 5.17 19 30 β οΈ 5.2pp\n",
|
| 652 |
-
" RF tuned 0.6924 0.8133 12.09 0.7139 0.0334 2.15 9 49 β
OK\n",
|
| 653 |
-
" LinearSVC 0.7250 0.9649 23.99 0.6847 0.0276 4.03 17 37 β
OK\n",
|
| 654 |
-
"\n",
|
| 655 |
-
"GANADOR (F1 test): LR tuned\n",
|
| 656 |
-
" F1 test : 0.7579\n",
|
| 657 |
-
" cv_test_gap : 4.76pp\n",
|
| 658 |
-
" train_test_gap: 14.07pp\n"
|
| 659 |
-
]
|
| 660 |
-
}
|
| 661 |
-
],
|
| 662 |
-
"source": [
|
| 663 |
-
"all_metrics = [\n",
|
| 664 |
-
" metrics_lr_base, metrics_lr_tuned,\n",
|
| 665 |
-
" metrics_rf_base, metrics_rf_tuned,\n",
|
| 666 |
-
" metrics_svc\n",
|
| 667 |
-
"]\n",
|
| 668 |
-
"\n",
|
| 669 |
-
"comp_df = pd.DataFrame(all_metrics).set_index('name')\n",
|
| 670 |
-
"\n",
|
| 671 |
-
"# Rubrica basada en cv_test_gap\n",
|
| 672 |
-
"comp_df['rubrica'] = comp_df['cv_test_gap_pp'].apply(\n",
|
| 673 |
-
" lambda x: 'β
OK' if x is not None and x < 5 else f'β οΈ {x:.1f}pp' if x else 'N/A'\n",
|
| 674 |
-
")\n",
|
| 675 |
-
"\n",
|
| 676 |
-
"print('COMPARATIVA FINAL β 5 MODELOS')\n",
|
| 677 |
-
"print('=' * 100)\n",
|
| 678 |
-
"print(f\"{'Modelo':16} {'F1 Test':>9} {'F1 Train':>9} \"\n",
|
| 679 |
-
" f\"{'TrTe gap':>9} {'CV Mean':>9} {'CV Std':>8} \"\n",
|
| 680 |
-
" f\"{'CV-Te gap':>10} {'FP':>4} {'FN':>4} {'Rubrica':>12}\")\n",
|
| 681 |
-
"print('-' * 100)\n",
|
| 682 |
-
"for name, row in comp_df.iterrows():\n",
|
| 683 |
-
" cv_gap_str = f\"{row['cv_test_gap_pp']:.2f}\" if row['cv_test_gap_pp'] else 'N/A'\n",
|
| 684 |
-
" cv_mean_str = f\"{row['cv_mean']:.4f}\" if row['cv_mean'] else 'N/A'\n",
|
| 685 |
-
" cv_std_str = f\"{row['cv_std']:.4f}\" if row['cv_std'] else 'N/A'\n",
|
| 686 |
-
" print(f\" {name:14} {row['f1_test']:>9.4f} {row['f1_train']:>9.4f} \"\n",
|
| 687 |
-
" f\"{row['train_test_gap_pp']:>9.2f} {cv_mean_str:>9} {cv_std_str:>8} \"\n",
|
| 688 |
-
" f\"{cv_gap_str:>10} {row['fp']:>4} {row['fn']:>4} {row['rubrica']:>12}\")\n",
|
| 689 |
-
"\n",
|
| 690 |
-
"best_name = comp_df['f1_test'].idxmax()\n",
|
| 691 |
-
"print(f'\\nGANADOR (F1 test): {best_name}')\n",
|
| 692 |
-
"print(f\" F1 test : {comp_df.loc[best_name, 'f1_test']:.4f}\")\n",
|
| 693 |
-
"print(f\" cv_test_gap : {comp_df.loc[best_name, 'cv_test_gap_pp']:.2f}pp\")\n",
|
| 694 |
-
"print(f\" train_test_gap: {comp_df.loc[best_name, 'train_test_gap_pp']:.2f}pp\")"
|
| 695 |
-
]
|
| 696 |
-
},
|
| 697 |
-
{
|
| 698 |
-
"cell_type": "code",
|
| 699 |
-
"execution_count": 14,
|
| 700 |
-
"metadata": {
|
| 701 |
-
"id": "243374d0"
|
| 702 |
-
},
|
| 703 |
-
"outputs": [
|
| 704 |
-
{
|
| 705 |
-
"data": {
|
| 706 |
-
"image/png": "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Yr788kulpqZKkqZMmaJ169bpk08+0fPPP6/atWtn+4ZumXnttde0bt06SVLz5s21bNkyffbZZ6pZs6Ykafr06frpp58kSWvXrtWVK1ckSU8++aTWrVunzz77TC+++KIaN24sm81mDmmRfkzCJUuWaNOmTdq0aZPKlSt3wzkDAAqOuLg4u3FsW7duneU6s2fP1saNGyVJ4eHhWrRokT7++GNVqlRJkrRx40bNmjXL4bqenp5atmyZeQMnSZo2bZqaNGmiL7/8Ug888ICktKEJ3n33XYfb2L9/vyZPnqyvvvpKHTp0kCQlJydrxIgRZkzTpk01Z84crVixQuvXr9fatWv18ssvS0r72a2z/G6kjZLT79hy5crp9ttvlyRt2bJFJ06ckJT28981a9ZIkurWrat69erd0LFdk5KSohEjRsgwDEnSgw8+qJUrV2r48OFO73PQo0cPzZ8/XytXrlRUVJRWrlypPn36SJL27Nmjzz//3OF618abXbJkiapWrSpJOn78uKZMmWIe79mzZyVJPXv21Nq1a7V8+XK9+uqratu2rTw9PTM9liNHjig+Pl6SdNNNN2Uam1Uu10tOTtbixYu1cOFClS5dWpK0e/du/ec//7GLS7/fP/74I9Mc8oqvr6+6d+8uSTp27Jh+/PFHc9nSpUslSaVKlTKH33ruuef066+/SpI6d+5svt/DwsIkSSNHjnR4Y7UTJ06oYsWK+u9//6tVq1apa9euunjxosaPH2/GPPHEE1q5cqV69OihPXv2ZNjG8ePHNWzYMKWmpiowMFBz5szR119/rf79+0uS9u7dm2F4t/x08OBBNW7cWEuWLNHy5cuzdU3MzJ49e8zP2MiRI835EydO1Llz5zLEHzhwQB06dNDy5cv16aef6pZbbnG67eeee84cXq1kyZKaNWuW1qxZozlz5qhWrVpm3I1eMwzDUJ8+fXThwgVJUp06dfThhx9q5cqVGj9+vEqVKiUp7Zo1YMAAJScnS5IGDx6sVatWaciQIZLSPlMDBgwwP7OuHLej997ly5fVt29fJSYmysvLSy+99JK++eYbPfPMM5KkU6dOmfvO7us1Z84cvf766+Y6nTp1Mq/hzz33XK68nshH7q4aA8D1svqvs7Pet/nZ07Zly5YO1//yyy+NO++80yhdurTh6emZIfdrPUyy09P2vvvuM+d/8skn5vwR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81NCQoK2bt2qESNGqEOHDvrkk08cDl/QvXt3jRkzRomJiZo2bZpsNptatGihy5cv6+DBg1q/fr2uXLlil78jEydO1MSJE7P92tyIkJAQ87O5f/9+DRo0SDVr1tSrr76a5bqHDx9W37591atXL61bt878NYOvr686duyY45w8PDzUq1cvzZ07V1La3ynjxo1TrVq1dODAAa1YsUKrVq3KlWtGZGSk6tSpo99++00xMTG644479Mwzz6hkyZLavn27Ll68qBkzZuiuu+4yX6dt27Zp2LBhuvvuu7Vq1SqzgF+6dGm7XynciDvvvFNlypTR2bNn9cEHH6hkyZK68847lZKSokOHDun777/Xrl279Mcff2T79ZLsr+GbN2/W6tWrFRgYqBo1auTK64l8lEdj5QJAjqW/mYazm46ll/4GDY6m6wfbz0x2b0SW/sYW1yxZsiTDvv38/OwGrb92A4Ps3Igs/T6yGkzekRMnTmS40cju3bvtYl544QWnr5uHh4exefPmbO3LMJzfiCwxMdG44447Mj1H1+I//PDDTOM+/vhjc7tz5szJsPz6cwYAKDp+//13o2rVqpl+j+zYscMwDMNISEgwWrdu7TSuTZs2RmJiorltRzfqMQzD4fdPdr7j098I7drk5eVlthMMwzDuvvvuDDHpbwCUfp+52UYxjMy/YzO7oZFhGMbgwYPt1nvggQcyxLhybM4sXbrUsNlsGbZTt27dDK9FbGysUa5cuUz3mb59lb5N4+hclStXzryR0aZNmzJ9z02dOjXLY0n/em/bts1umSu5GIb9e9VRfJ06dYwrV644PB9hYWF2N5xyl5dfftku54YNG2aIuXjxot25djRl1e5Ob9q0aRnW9/DwsLumpP+MvPPOO4aHh4fTfbvy90duyM7fCmPHjnX4/gkJCclwfUu/vapVqzo81hdffNGMT39DrgkTJmTYt7NzEB0d7fB9ev11IDeuGdu3b7c71vRT+tds+fLlhre3t8M4b29v44svvrjh405v5cqVDm+O6OjYsvt6JScnG2FhYRlirl0Tc+P1RP5geAQAyCXdu3fX22+/rerVq8vPz09NmjTRmjVrVKdOHbfkU65cOd1+++3m83r16qlu3bp2MZ07d9bjjz+uOnXqqESJEvL09FTJkiV111136euvv86V3hY+Pj5as2aNXn/9dTVt2lSBgYHy8/NTlSpVdPfdd+u9997T/fffLynt5iFPPvmkGjZsqNKlS8vT01PBwcFq3bq1Fi9erIcfftjc7uOPP67Ro0erUqVK2b7BBwCg8Kpdu7Z2796tmTNnqlWrVipZsqR8fHwUHh6uDh066P333zd/+u3r66u1a9dq2rRpqlevnooVKyY/Pz/VrVtXU6dO1TfffCMfH588y3XkyJF64403VK1aNfn4+KhBgwb66quv7HoFfvjhh+rbt69Kly6tkJAQPfroo/ryyy9ztD9X2yg38h2bvkeko+dS7hzbAw88oE8//VS1a9eWj4+Pbr75Zi1atEh33HGHGePv7y8pbbzdtWvX6vbbb1dAQIAqVKigyZMna/LkyVnu5+OPP9bw4cNVpkwZFStWTJ06ddLGjRvNGxnVqFFDo0ePVvPmzRUaGiovLy8FBASoSZMmmjt3rkaPHp3lPnr16mX+NDmz4RSyyuV669at06OPPqrg4GAFBgbq4Ycf1rfffis/Pz8z5tKlS+bPrB955BG7Xt7u0qtXL7v3naP3UEhIiLZs2aIXXnhBkZGRKlasmPz9/VW9enV1795dH3/8sZo3b57tfY4ePVqvvfaaIiIi5Ovrq/r16+uLL75Q69atHcY/9thj2rhxox544AHzvIeGhqpp06YaN26c3nzzTdcPPI+NHz9e//73vxUSEqLixYvrvvvu0/fff5/hZo7Xa926tVasWKEGDRrI19dXlStX1owZM+x6b+dUcHCww/N48803q0+fPmZcblwzGjZsqF27dmnw4MGqWrWqfHx8FBISoubNm6tTp05m3H333actW7aoe/fuKlu2rLy8vFSmTBk98MAD+uGHH8yxu3NL586dtW3bNj366KOqWLGivL29Vbp0adWvX1+jRo0yh7mTsv96eXl5acWKFWrVqpUCAwMz7DM3v1+Qt2yG4cKtIgEAAAAALps4caImTZokKe2O8/369XNvQgWcYRgOx4hs3ry5OebmL7/8ogYNGri87Xbt2mnDhg2SpIMHDyoiIuKGcs2OwYMHa968eapQoYIOHjwob2/vHOUSERGhw4cPS1K2boT35ptvaujQofL19dVff/1l3jAOAOB+dE0CAAAAABQomzZtUs+ePfX111/r8OHD2rVrl4YOHWoWbGvWrKnIyEg3Z5l9zz33nHx9fXX8+HF9+umn+bJPwzD02muvSUrrOUrBFgCshRuRAQAAAAAKlNTUVH3yySf65JNPMiwLDAzUwoULC9TwSRUrVlRCQkK+7tNms2nv3r35uk8AQPYVnG8xAAAAAAAkVa1aVY888oiqVasmf39/+fr66qabbtLgwYO1a9cul8YzBQDAilwe03bjxo165ZVXtH37dp08eVLLli1T165dM10nKipKo0aN0u+//67w8HA9//zzjOEEAAAAAAAAAA643NM2Pj5ekZGRmjt3brbiDx48qLvvvlu33Xabdu7cqREjRuixxx7T119/7XKyAAAAAAAAAFDYudzT1m5lmy3LnrajR4/WypUr9dtvv5nzHn74YUVHR2vNmjU53TUAAAAAAAAAFEp5Pqbtli1b1L59e7t5HTp00JYtW5yuk5iYqNjYWHOKiYnR2bNndQP1ZQAAACDfGYah2NhY2rEAAABwSZ4XbU+dOqXQ0FC7eaGhoYqNjdWVK1ccrjN16lQFBwebU0hIiMqWLatLly7ldboAAAAoIqZOnaomTZooMDBQZcuWVdeuXTPcSb1du3ay2Wx206BBg7K9j0uXLik4OJh2LAAAAFyS50XbnBg7dqxiYmLM6ejRo+5OCQAAAIXMhg0bNHToUP34449au3atkpOTdddddyk+Pt4ubuDAgTp58qQ5TZ8+3U0ZAwAAoKjwyusdhIWF6fTp03bzTp8+raCgIBUrVszhOr6+vvL19c3r1AAAAFCEXX9/hYULF6ps2bLavn272rRpY8739/dXWFhYfqcHAACAIizPe9q2aNFC69ats5u3du1atWjRIq93DQAAAGRbTEyMJKlkyZJ28z/66COVLl1aderU0dixY3X58mWn27j+3gyxsbF5mjMAAAAKJ5d72sbFxWn//v3m84MHD2rnzp0qWbKkKlWqpLFjx+r48eP64IMPJEmDBg3SG2+8oWeeeUb/+te/9N133+nTTz/VypUrc+8oAAAAgBuQmpqqESNGqGXLlqpTp445v1evXqpcubLKly+v3bt3a/To0dq7d68+//xzh9uZOnWqJk2alF9pAwAAoJCyGS7eyjYqKkq33XZbhvl9+/bVwoUL1a9fPx06dEhRUVF264wcOVJ//PGHKlasqHHjxqlfv37Z3mdsbKyCg4MVExOjoKAgV9IFAAAAsjR48GCtXr1amzdvVsWKFZ3Gfffdd7rjjju0f/9+VatWLcPyxMRE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+/bttWDBAg0ZMkSNGzeWlDY8WK9evbRo0SJ5enpq9OjReuihh/Tnn3+a7eI1a9Zo7ty5evvtt7V69Wo9+OCD+v3331WtWrXsvpQAAAAuKxRFWymt6HhtTCwAAAAAN+bs2bOSpAoVKmQaN2DAAM2ePdss2s6fP18DBgzI9n7efvtt9evXT7fffrskqVWrVrrnnnv06aefaty4cfL29tb+/fvN4dBuvfXWbG978eLFuuOOO9SxY0dJ0sCBAzV79mxzeVBQkHr06GE+nzRpkl5//XWdOHHCPO4aNWro8ccflyR16dJFt912mz7++GM9//zz2c4DAADAVfwOHgAAAEAGpUuXliQdP34807hevXrp6NGj2rx5s3799Vft2rVLffr0kSTdcsstCggIUEBAgD766COH6x86dEjz5s1TSEiIOX3xxRc6ceKEJGnZsmX67bffVLNmTTVo0ECffvppto/hxIkTCg8Pt5tXqVIl8/GVK1c0ZMgQRUREKCgoSBEREZKkc+fOmTGVK1e2W79y5cpZviYAAAA3qtD0tAUAAACQe2rUqKGIiAh98skneu6555zGhYSEqFu3bpo/f76Cg4N1zz33KDQ0VJL0+++/Z7mf8PBwPfnkk5o2bZrD5Q0bNtTSpUuVmpqq5cuX66GHHlLbtm2zdR+G8uXL66effrKbd+TIETVr1kySNGPGDG3fvl2bN29WxYoVFR0drRIlSsgwDDP+8OHDGdZ3pbcvAABATtDTFgAAAEAGNptNc+bM0bRp0zRnzhydP39eUto4sQMGDLArZg4YMEBLlizRf//7X5eGRpCkxx9/XAsWLND69euVkpKixMREbdmyRXv27FFSUpI+/PBDXbx4UR4eHubYtV5eXipTpow8PDz0999/O932Qw89pG+//VbffPONrl69qvnz52vfvn3m8tjYWPn5+alEiRKKi4vTs88+m2Eb+/bt0zvvvKOrV69q5cqV+u677+yGVAAAAMgLFG0BAAAAOHTPPfdo9erVWrlypapVq6aQkBB1795dtWrVUrly5cy4du3aKTQ0VD4+PurUqZNL+2jQoIE5RmyZMmVUoUIFjRs3TomJiZLSbmx20003KTAwUE888YQWLVqkUqVKqVixYpowYYI6deqkkJAQLVq0KMO2a9asqffff1+DBw9WqVKltGXLFt1+++3y9fWVJI0aNUqenp4K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yRozZoyaNm2q+vXr66GHHtLFixclSYsWLVKzZs3UoEEDRUZG6ssvv7Tb/vDhw9WiRQvdddddOnbsmAIDAxUYGChJstlsatiwoSTp3nvv1dGjR7V7925z/fnz56tPnz7y9vbW3Llz1bp1aw0cONBcXqJECQ0aNChPXyPAFbRpiibOe+FGm6bo4ZwXDLRjrY+ibSEUExOjqKgodevWzd2pIJ9wzgu/P//8U+fPn1e7du0kSZcuXbL7idHRo0fdmyByHee8YNq5c6e+/PJL7dmzR6dPn9bSpUslSa+88oqKFy+urVu3aufOnapbt66ef/55SVKHDh30448/aseOHfriiy80cOBAJSYmmtvct2+fNm7cqO+++0533XWXAgMDValSJfXo0UNvvPGG2Wj29vbWo48+qvnz50uSEhIS9PHHH2vAgAGSpO3bt6tFixb5+XIALqNNUzRx3gs32jRFD+e8YKIdaz1e7k4AueeVV17R/PnztW/fPt1999267bbb3J0S8hjnvPDr0aOHPDw8tHfvXs2aNUtlypRRfHy8AgMDM4xJhMKBc16w3X///fL395ckNW3aVH///bckafny5YqJiTEbv0lJSYqIiJAkHTx4UL1799axY8fk5eWlCxcu6ODBg6pVq5Yk6ZFHHpG3t7ckyd/fX5s2bdLOnTu1adMmff7553r55Ze1a9culSxZUgMGDFDbtm01ffp0ff7557r55pt188035/OrALiONk3RxHkv3GjTFD2c84KNdqz10NO2EHn66ae1e/du7du3T9u2bWNw7yKAc174LV68WHv27NE333yjMWPG6Ndff3V3SshjnPOCzc/Pz3zs6empq1evSpIMw9CcOXO0c+dO7dy5U3/88YdWrVolSXr44Yf12GOP6bffftPOnTsVEBCghIQEczsBAQF2+7DZbGrQoIGGDx+udevWKSAgQFFRUZKk2rVr66abbtKXX36p+fPnm70TJKlRo0basmVLXh06cENo0xRNnPfCjTZN0cM5L9hox1oPRdtCqFKlSpozZ44mT56sK1euuDsd5APOeeHXvn17DR482PwZCgo/znnh0rVrV82aNcu8M/rly5f1+++/S5IuXryoKlWqSJL++9//mj8Tc+TPP/+0G+vr6NGjOnv2rKpWrWrOGzBggKZMmaKtW7eqR48e5vwhQ4Zow4YNWrBggTkvOjpa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",
|
| 707 |
-
"text/plain": [
|
| 708 |
-
"<Figure size 1400x600 with 2 Axes>"
|
| 709 |
-
]
|
| 710 |
-
},
|
| 711 |
-
"metadata": {},
|
| 712 |
-
"output_type": "display_data"
|
| 713 |
-
}
|
| 714 |
-
],
|
| 715 |
-
"source": [
|
| 716 |
-
"fig, axes = plt.subplots(1, 2, figsize=(14, 6))\n",
|
| 717 |
-
"\n",
|
| 718 |
-
"models_list = list(comp_df.index)\n",
|
| 719 |
-
"x = np.arange(len(models_list))\n",
|
| 720 |
-
"w = 0.35\n",
|
| 721 |
-
"c_tr = ['#534AB7','#7F77DD','#0F6E56','#5DCAA5','#993C1D']\n",
|
| 722 |
-
"c_te = ['#9B96E8','#B8B4F0','#5DCAA5','#9FE1CB','#E8593C']\n",
|
| 723 |
-
"\n",
|
| 724 |
-
"axes[0].bar(x-w/2, comp_df['f1_train'], w, label='Train', color=c_tr, alpha=0.85)\n",
|
| 725 |
-
"axes[0].bar(x+w/2, comp_df['f1_test'], w, label='Test', color=c_te, alpha=0.85)\n",
|
| 726 |
-
"axes[0].axhline(0.75, color='gray', linestyle='--', alpha=0.4)\n",
|
| 727 |
-
"axes[0].set_title('F1 Train vs Test', fontweight='bold')\n",
|
| 728 |
-
"axes[0].set_xticks(x)\n",
|
| 729 |
-
"axes[0].set_xticklabels([m.replace(' ','\\n') for m in models_list], fontsize=8)\n",
|
| 730 |
-
"axes[0].set_ylim(0.5, 1.0)\n",
|
| 731 |
-
"axes[0].legend()\n",
|
| 732 |
-
"\n",
|
| 733 |
-
"# GAP comparativo\n",
|
| 734 |
-
"cv_gaps = [r['cv_test_gap_pp'] if r['cv_test_gap_pp'] else 0\n",
|
| 735 |
-
" for _, r in comp_df.iterrows()]\n",
|
| 736 |
-
"tr_gaps = comp_df['train_test_gap_pp'].tolist()\n",
|
| 737 |
-
"x2 = np.arange(len(models_list))\n",
|
| 738 |
-
"axes[1].bar(x2-w/2, tr_gaps, w, label='Train-Test gap', color='#E8593C', alpha=0.7)\n",
|
| 739 |
-
"axes[1].bar(x2+w/2, cv_gaps, w, label='CV-Test gap', color='#5DCAA5', alpha=0.7)\n",
|
| 740 |
-
"axes[1].axhline(5, color='red', linestyle='--', lw=1.5, label='LΓmite 5pp')\n",
|
| 741 |
-
"axes[1].set_title('Comparativa gaps (pp) β verde = rubrica correcta', fontweight='bold')\n",
|
| 742 |
-
"axes[1].set_xticks(x2)\n",
|
| 743 |
-
"axes[1].set_xticklabels([m.replace(' ','\\n') for m in models_list], fontsize=8)\n",
|
| 744 |
-
"axes[1].legend(fontsize=9)\n",
|
| 745 |
-
"\n",
|
| 746 |
-
"plt.tight_layout()\n",
|
| 747 |
-
"plt.savefig(PROJECT_ROOT / 'reports' / 'v2' / '14_optuna_comparativa.png',\n",
|
| 748 |
-
" dpi=150, bbox_inches='tight')\n",
|
| 749 |
-
"plt.show()"
|
| 750 |
-
]
|
| 751 |
-
},
|
| 752 |
-
{
|
| 753 |
-
"cell_type": "markdown",
|
| 754 |
-
"metadata": {
|
| 755 |
-
"id": "adb85f73"
|
| 756 |
-
},
|
| 757 |
-
"source": [
|
| 758 |
-
"## 8. Guardar ganador y best_params.yaml"
|
| 759 |
-
]
|
| 760 |
-
},
|
| 761 |
-
{
|
| 762 |
-
"cell_type": "code",
|
| 763 |
-
"execution_count": 15,
|
| 764 |
-
"metadata": {
|
| 765 |
-
"id": "0d178b01"
|
| 766 |
-
},
|
| 767 |
-
"outputs": [
|
| 768 |
-
{
|
| 769 |
-
"name": "stdout",
|
| 770 |
-
"output_type": "stream",
|
| 771 |
-
"text": [
|
| 772 |
-
"Modelo guardado: /mnt/c/Users/under/Documents/F5/3_Projects/Project_9_Equipo3/Project_YT/models/final_model.joblib\n",
|
| 773 |
-
"best_params.yaml guardado\n",
|
| 774 |
-
"winner: LR tuned\n",
|
| 775 |
-
"hyperparameters:\n",
|
| 776 |
-
" ngram_range: '1_2'\n",
|
| 777 |
-
" max_features: 4045\n",
|
| 778 |
-
" min_df: 2\n",
|
| 779 |
-
" sublinear_tf: false\n",
|
| 780 |
-
" C: 0.3235215031170205\n",
|
| 781 |
-
"results:\n",
|
| 782 |
-
" f1_test: 0.7579\n",
|
| 783 |
-
" f1_train: 0.8987\n",
|
| 784 |
-
" train_test_gap_pp: 14.07\n",
|
| 785 |
-
" cv_test_gap_pp: 4.76\n",
|
| 786 |
-
" roc_auc: 0.81\n",
|
| 787 |
-
" fp: 18\n",
|
| 788 |
-
" fn: 30\n",
|
| 789 |
-
"\n"
|
| 790 |
-
]
|
| 791 |
-
}
|
| 792 |
-
],
|
| 793 |
-
"source": [
|
| 794 |
-
"pipeline_map = {\n",
|
| 795 |
-
" 'LR baseline': lr_baseline_pipe,\n",
|
| 796 |
-
" 'LR tuned' : lr_tuned_pipe,\n",
|
| 797 |
-
" 'RF baseline': rf_baseline_pipe,\n",
|
| 798 |
-
" 'RF tuned' : rf_tuned_pipe,\n",
|
| 799 |
-
" 'LinearSVC' : svc_pipeline,\n",
|
| 800 |
-
"}\n",
|
| 801 |
-
"best_pipeline = pipeline_map[best_name]\n",
|
| 802 |
-
"\n",
|
| 803 |
-
"MODELS_DIR = PROJECT_ROOT / 'models'\n",
|
| 804 |
-
"MODELS_DIR.mkdir(exist_ok=True)\n",
|
| 805 |
-
"model_path = MODELS_DIR / 'final_model.joblib'\n",
|
| 806 |
-
"joblib.dump(best_pipeline, model_path)\n",
|
| 807 |
-
"print(f'Modelo guardado: {model_path}')\n",
|
| 808 |
-
"\n",
|
| 809 |
-
"best_row = comp_df.loc[best_name]\n",
|
| 810 |
-
"best_trial_params = {}\n",
|
| 811 |
-
"if 'LR tuned' == best_name:\n",
|
| 812 |
-
" best_trial_params = study_lr.best_trial.params\n",
|
| 813 |
-
"elif 'RF tuned' == best_name:\n",
|
| 814 |
-
" best_trial_params = study_rf.best_trial.params\n",
|
| 815 |
-
"\n",
|
| 816 |
-
"best_out = {\n",
|
| 817 |
-
" 'winner' : best_name,\n",
|
| 818 |
-
" 'hyperparameters' : best_trial_params,\n",
|
| 819 |
-
" 'results': {\n",
|
| 820 |
-
" 'f1_test' : float(best_row['f1_test']),\n",
|
| 821 |
-
" 'f1_train' : float(best_row['f1_train']),\n",
|
| 822 |
-
" 'train_test_gap_pp': float(best_row['train_test_gap_pp']),\n",
|
| 823 |
-
" 'cv_test_gap_pp' : float(best_row['cv_test_gap_pp'])\n",
|
| 824 |
-
" if best_row['cv_test_gap_pp'] is not None else None,\n",
|
| 825 |
-
" 'roc_auc' : float(best_row['roc_auc']),\n",
|
| 826 |
-
" 'fp' : int(best_row['fp']),\n",
|
| 827 |
-
" 'fn' : int(best_row['fn']),\n",
|
| 828 |
-
" }\n",
|
| 829 |
-
"}\n",
|
| 830 |
-
"\n",
|
| 831 |
-
"import yaml\n",
|
| 832 |
-
"best_path = PROJECT_ROOT / 'configs' / 'best_params.yaml'\n",
|
| 833 |
-
"with open(best_path, 'w') as f:\n",
|
| 834 |
-
" yaml.dump(best_out, f, default_flow_style=False, sort_keys=False)\n",
|
| 835 |
-
"print(f'best_params.yaml guardado')\n",
|
| 836 |
-
"with open(best_path) as f: print(f.read())"
|
| 837 |
-
]
|
| 838 |
-
},
|
| 839 |
-
{
|
| 840 |
-
"cell_type": "code",
|
| 841 |
-
"execution_count": 16,
|
| 842 |
-
"metadata": {
|
| 843 |
-
"id": "8e205676"
|
| 844 |
-
},
|
| 845 |
-
"outputs": [
|
| 846 |
-
{
|
| 847 |
-
"name": "stdout",
|
| 848 |
-
"output_type": "stream",
|
| 849 |
-
"text": [
|
| 850 |
-
"Verificacion final_model.joblib:\n",
|
| 851 |
-
" β
TOXICO 0.70] you are a stupid thug get out\n",
|
| 852 |
-
" β
NO TOXICO 0.45] I think the police should be more transparent\n",
|
| 853 |
-
" β
TOXICO 0.56] black people are criminal thugs\n",
|
| 854 |
-
" β
NO TOXICO 0.31] thank you for sharing this video\n"
|
| 855 |
-
]
|
| 856 |
-
}
|
| 857 |
-
],
|
| 858 |
-
"source": [
|
| 859 |
-
"# Verificacion\n",
|
| 860 |
-
"loaded = joblib.load(model_path)\n",
|
| 861 |
-
"tests = [\n",
|
| 862 |
-
" ('you are a stupid thug get out', True),\n",
|
| 863 |
-
" ('I think the police should be more transparent', False),\n",
|
| 864 |
-
" ('black people are criminal thugs', True),\n",
|
| 865 |
-
" ('thank you for sharing this video', False),\n",
|
| 866 |
-
"]\n",
|
| 867 |
-
"print('Verificacion final_model.joblib:')\n",
|
| 868 |
-
"for text, expected in tests:\n",
|
| 869 |
-
" pred = loaded.predict([text])[0]\n",
|
| 870 |
-
" prob = loaded.predict_proba([text])[0][1]\n",
|
| 871 |
-
" ok = 'β
' if pred == expected else 'β'\n",
|
| 872 |
-
" print(f' {ok} {\"TOXICO\" if pred else \"NO TOXICO\"} {prob:.2f}] {text[:55]}')"
|
| 873 |
-
]
|
| 874 |
-
},
|
| 875 |
-
{
|
| 876 |
-
"cell_type": "markdown",
|
| 877 |
-
"metadata": {
|
| 878 |
-
"id": "82980c46"
|
| 879 |
-
},
|
| 880 |
-
"source": [
|
| 881 |
-
"## 9. Registro en MLflow"
|
| 882 |
-
]
|
| 883 |
-
},
|
| 884 |
-
{
|
| 885 |
-
"cell_type": "code",
|
| 886 |
-
"execution_count": 17,
|
| 887 |
-
"metadata": {
|
| 888 |
-
"id": "6dc9f27a"
|
| 889 |
-
},
|
| 890 |
-
"outputs": [
|
| 891 |
-
{
|
| 892 |
-
"name": "stderr",
|
| 893 |
-
"output_type": "stream",
|
| 894 |
-
"text": [
|
| 895 |
-
"2026/05/14 16:29:24 WARNING mlflow.models.model: `artifact_path` is deprecated. Please use `name` instead.\n",
|
| 896 |
-
"2026/05/14 16:29:25 WARNING mlflow.sklearn: Saving scikit-learn models in the pickle or cloudpickle format requires exercising caution because these formats rely on Python's object serialization mechanism, which can execute arbitrary code during deserialization. The recommended safe alternative is the 'skops' format. For more information, see: https://scikit-learn.org/stable/model_persistence.html\n"
|
| 897 |
-
]
|
| 898 |
-
},
|
| 899 |
-
{
|
| 900 |
-
"name": "stdout",
|
| 901 |
-
"output_type": "stream",
|
| 902 |
-
"text": [
|
| 903 |
-
" β
lr_tuned_optuna\n",
|
| 904 |
-
" β
rf_tuned_optuna\n",
|
| 905 |
-
" β
linear_svc\n"
|
| 906 |
-
]
|
| 907 |
-
}
|
| 908 |
-
],
|
| 909 |
-
"source": [
|
| 910 |
-
"MLFLOW_DIR = PROJECT_ROOT / 'mlruns'\n",
|
| 911 |
-
"mlflow.set_tracking_uri(f'file://{MLFLOW_DIR}')\n",
|
| 912 |
-
"mlflow.set_experiment('Youtube_project_experiment')\n",
|
| 913 |
-
"\n",
|
| 914 |
-
"runs_info = [\n",
|
| 915 |
-
" ('lr_tuned_optuna', study_lr, metrics_lr_tuned, lr_tuned_pipe,\n",
|
| 916 |
-
" {'model':'LR','optuna_trials':N_TRIALS}),\n",
|
| 917 |
-
" ('rf_tuned_optuna', study_rf, metrics_rf_tuned, rf_tuned_pipe,\n",
|
| 918 |
-
" {'model':'RF','optuna_trials':N_TRIALS}),\n",
|
| 919 |
-
" ('linear_svc', None, metrics_svc, svc_pipeline,\n",
|
| 920 |
-
" {'model':'LinearSVC','C':1.0}),\n",
|
| 921 |
-
"]\n",
|
| 922 |
-
"\n",
|
| 923 |
-
"for run_name, study, mets, pipe, extra in runs_info:\n",
|
| 924 |
-
" with mlflow.start_run(run_name=run_name):\n",
|
| 925 |
-
" for k,v in extra.items(): mlflow.log_param(k, v)\n",
|
| 926 |
-
" if study:\n",
|
| 927 |
-
" mlflow.log_param('best_trial', study.best_trial.number)\n",
|
| 928 |
-
" for k,v in study.best_trial.params.items(): mlflow.log_param(k, v)\n",
|
| 929 |
-
" mlflow.log_metric('test_f1', mets['f1_test'])\n",
|
| 930 |
-
" mlflow.log_metric('train_f1', mets['f1_train'])\n",
|
| 931 |
-
" mlflow.log_metric('train_test_gap_pp', mets['train_test_gap_pp'])\n",
|
| 932 |
-
" if mets['cv_test_gap_pp'] is not None:\n",
|
| 933 |
-
" mlflow.log_metric('cv_mean', mets['cv_mean'])\n",
|
| 934 |
-
" mlflow.log_metric('cv_test_gap_pp', mets['cv_test_gap_pp'])\n",
|
| 935 |
-
" mlflow.log_metric('roc_auc', mets['roc_auc'])\n",
|
| 936 |
-
" if run_name.split('_')[0].upper() in best_name.upper() and 'tuned' in best_name.lower():\n",
|
| 937 |
-
" mlflow.sklearn.log_model(pipe, 'final_model')\n",
|
| 938 |
-
" print(f' β
{run_name}')\n",
|
| 939 |
-
"mlflow.log_artifact(str(PROJECT_ROOT / 'reports' / 'v2' / '14_optuna_comparativa.png'))"
|
| 940 |
-
]
|
| 941 |
-
},
|
| 942 |
-
{
|
| 943 |
-
"cell_type": "markdown",
|
| 944 |
-
"metadata": {
|
| 945 |
-
"id": "96714037"
|
| 946 |
-
},
|
| 947 |
-
"source": [
|
| 948 |
-
"## 10. Conclusiones"
|
| 949 |
-
]
|
| 950 |
-
},
|
| 951 |
-
{
|
| 952 |
-
"cell_type": "code",
|
| 953 |
-
"execution_count": 18,
|
| 954 |
-
"metadata": {
|
| 955 |
-
"id": "b77fe7cc"
|
| 956 |
-
},
|
| 957 |
-
"outputs": [
|
| 958 |
-
{
|
| 959 |
-
"name": "stdout",
|
| 960 |
-
"output_type": "stream",
|
| 961 |
-
"text": [
|
| 962 |
-
"\n",
|
| 963 |
-
"CONCLUSIONES β OPTIMIZACION + LinearSVC\n",
|
| 964 |
-
"=======================================================\n",
|
| 965 |
-
"5 modelos evaluados bajo las mismas condiciones.\n",
|
| 966 |
-
"\n",
|
| 967 |
-
"Mejora Optuna:\n",
|
| 968 |
-
" LR: +0.48pp F1 test\n",
|
| 969 |
-
" RF: -6.07pp F1 test\n",
|
| 970 |
-
"\n",
|
| 971 |
-
"Ganador: LR tuned\n",
|
| 972 |
-
" F1 test : 0.7579\n",
|
| 973 |
-
" train-test gap : 14.07pp\n",
|
| 974 |
-
" cv-test gap : 4.76pp\n",
|
| 975 |
-
"\n",
|
| 976 |
-
"Nota metodologica:\n",
|
| 977 |
-
" El train-test gap esta inflado por ser in-sample vs OOS.\n",
|
| 978 |
-
" El cv-test gap compara OOS vs OOS β es la metrica correcta\n",
|
| 979 |
-
" para la rubrica. Ver informe metodologico adjunto.\n",
|
| 980 |
-
"\n",
|
| 981 |
-
"Siguiente: 07_data_augmentation.ipynb\n",
|
| 982 |
-
" Explorar si augmentation mejora los FN sin aumentar FP.\n",
|
| 983 |
-
"\n"
|
| 984 |
-
]
|
| 985 |
-
}
|
| 986 |
-
],
|
| 987 |
-
"source": [
|
| 988 |
-
"lr_delta = metrics_lr_tuned['f1_test'] - metrics_lr_base['f1_test']\n",
|
| 989 |
-
"rf_delta = metrics_rf_tuned['f1_test'] - metrics_rf_base['f1_test']\n",
|
| 990 |
-
"\n",
|
| 991 |
-
"print(f\"\"\"\n",
|
| 992 |
-
"CONCLUSIONES β OPTIMIZACION + LinearSVC\n",
|
| 993 |
-
"{'='*55}\n",
|
| 994 |
-
"5 modelos evaluados bajo las mismas condiciones.\n",
|
| 995 |
-
"\n",
|
| 996 |
-
"Mejora Optuna:\n",
|
| 997 |
-
" LR: {lr_delta*100:+.2f}pp F1 test\n",
|
| 998 |
-
" RF: {rf_delta*100:+.2f}pp F1 test\n",
|
| 999 |
-
"\n",
|
| 1000 |
-
"Ganador: {best_name}\n",
|
| 1001 |
-
" F1 test : {comp_df.loc[best_name, 'f1_test']:.4f}\n",
|
| 1002 |
-
" train-test gap : {comp_df.loc[best_name, 'train_test_gap_pp']:.2f}pp\n",
|
| 1003 |
-
" cv-test gap : {comp_df.loc[best_name, 'cv_test_gap_pp']:.2f}pp\n",
|
| 1004 |
-
"\n",
|
| 1005 |
-
"Nota metodologica:\n",
|
| 1006 |
-
" El train-test gap esta inflado por ser in-sample vs OOS.\n",
|
| 1007 |
-
" El cv-test gap compara OOS vs OOS β es la metrica correcta\n",
|
| 1008 |
-
" para la rubrica. Ver informe metodologico adjunto.\n",
|
| 1009 |
-
"\n",
|
| 1010 |
-
"Siguiente: 07_data_augmentation.ipynb\n",
|
| 1011 |
-
" Explorar si augmentation mejora los FN sin aumentar FP.\n",
|
| 1012 |
-
"\"\"\")"
|
| 1013 |
-
]
|
| 1014 |
-
}
|
| 1015 |
-
],
|
| 1016 |
-
"metadata": {
|
| 1017 |
-
"colab": {
|
| 1018 |
-
"provenance": []
|
| 1019 |
-
},
|
| 1020 |
-
"kernelspec": {
|
| 1021 |
-
"display_name": "py310",
|
| 1022 |
-
"language": "python",
|
| 1023 |
-
"name": "python3"
|
| 1024 |
-
},
|
| 1025 |
-
"language_info": {
|
| 1026 |
-
"codemirror_mode": {
|
| 1027 |
-
"name": "ipython",
|
| 1028 |
-
"version": 3
|
| 1029 |
-
},
|
| 1030 |
-
"file_extension": ".py",
|
| 1031 |
-
"mimetype": "text/x-python",
|
| 1032 |
-
"name": "python",
|
| 1033 |
-
"nbconvert_exporter": "python",
|
| 1034 |
-
"pygments_lexer": "ipython3",
|
| 1035 |
-
"version": "3.10.20"
|
| 1036 |
-
}
|
| 1037 |
-
},
|
| 1038 |
-
"nbformat": 4,
|
| 1039 |
-
"nbformat_minor": 0
|
| 1040 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
notebooks/06_tuning_clean_v2.ipynb
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# βοΈ Notebook 06 β OptimizaciΓ³n con Optuna + LinearSVC\n",
|
| 8 |
+
"## YouTube Hate Speech Detection\n",
|
| 9 |
+
"\n",
|
| 10 |
+
"### ΒΏQuΓ© hace este notebook?\n",
|
| 11 |
+
"Optimizamos LR y RF con Optuna, aΓ±adimos LinearSVC como modelo adicional,\n",
|
| 12 |
+
"y comparamos 5 modelos en una tabla unificada.\n",
|
| 13 |
+
"\n",
|
| 14 |
+
"### Nota metodolΓ³gica β dos mΓ©tricas de GAP\n",
|
| 15 |
+
"Este notebook reporta dos mΓ©tricas de gap distintas:\n",
|
| 16 |
+
"\n",
|
| 17 |
+
"| MΓ©trica | FΓ³rmula | QuΓ© mide |\n",
|
| 18 |
+
"|---|---|---|\n",
|
| 19 |
+
"| `train_test_gap` | f1_train - f1_test | MemorizaciΓ³n / ajuste in-sample |\n",
|
| 20 |
+
"| `cv_test_gap` | \\|cv_mean - f1_test\\| | Estabilidad de generalizaciΓ³n (OOS vs OOS) |\n",
|
| 21 |
+
"\n",
|
| 22 |
+
"La rΓΊbrica exige < 5pp. El `cv_test_gap` es la comparaciΓ³n\n",
|
| 23 |
+
"estadΓsticamente correcta porque ambos tΓ©rminos son out-of-sample.\n",
|
| 24 |
+
"Ver informe metodolΓ³gico para la justificaciΓ³n completa.\n",
|
| 25 |
+
"\n",
|
| 26 |
+
"### Modelos comparados\n",
|
| 27 |
+
"1. LR baseline (cargado desde disco)\n",
|
| 28 |
+
"2. RF baseline (cargado desde disco)\n",
|
| 29 |
+
"3. LinearSVC (nuevo β muy competitivo en TF-IDF sparse)\n",
|
| 30 |
+
"4. LR tuned (Optuna)\n",
|
| 31 |
+
"5. RF tuned (Optuna)\n",
|
| 32 |
+
"\n",
|
| 33 |
+
"### Sin hardcoding\n",
|
| 34 |
+
"Todos los valores se calculan en el momento desde los modelos en disco."
|
| 35 |
+
]
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"cell_type": "markdown",
|
| 39 |
+
"metadata": {},
|
| 40 |
+
"source": [
|
| 41 |
+
"## 0. Imports y configuraciΓ³n"
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"cell_type": "code",
|
| 46 |
+
"execution_count": 2,
|
| 47 |
+
"metadata": {},
|
| 48 |
+
"outputs": [
|
| 49 |
+
{
|
| 50 |
+
"name": "stdout",
|
| 51 |
+
"output_type": "stream",
|
| 52 |
+
"text": [
|
| 53 |
+
"PROJECT_ROOT: /mnt/c/Users/under/Documents/F5/3_Projects/Project_9_Equipo3/Project_YT\n"
|
| 54 |
+
]
|
| 55 |
+
}
|
| 56 |
+
],
|
| 57 |
+
"source": [
|
| 58 |
+
"import sys, yaml, joblib, warnings\n",
|
| 59 |
+
"import numpy as np\n",
|
| 60 |
+
"import pandas as pd\n",
|
| 61 |
+
"import matplotlib.pyplot as plt\n",
|
| 62 |
+
"import mlflow, mlflow.sklearn\n",
|
| 63 |
+
"import optuna\n",
|
| 64 |
+
"from pathlib import Path\n",
|
| 65 |
+
"from sklearn.svm import LinearSVC\n",
|
| 66 |
+
"from sklearn.calibration import CalibratedClassifierCV\n",
|
| 67 |
+
"from sklearn.ensemble import RandomForestClassifier\n",
|
| 68 |
+
"from sklearn.linear_model import LogisticRegression\n",
|
| 69 |
+
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
|
| 70 |
+
"from sklearn.pipeline import Pipeline\n",
|
| 71 |
+
"from sklearn.model_selection import (\n",
|
| 72 |
+
" train_test_split, StratifiedKFold,\n",
|
| 73 |
+
" cross_val_score, cross_validate\n",
|
| 74 |
+
")\n",
|
| 75 |
+
"from sklearn.metrics import f1_score, roc_auc_score, classification_report\n",
|
| 76 |
+
"warnings.filterwarnings('ignore')\n",
|
| 77 |
+
"optuna.logging.set_verbosity(optuna.logging.WARNING)\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"PROJECT_ROOT = Path.cwd().parent\n",
|
| 80 |
+
"sys.path.insert(0, str(PROJECT_ROOT))\n",
|
| 81 |
+
"plt.rcParams['figure.figsize'] = (12, 5)\n",
|
| 82 |
+
"plt.rcParams['axes.spines.top'] = False\n",
|
| 83 |
+
"plt.rcParams['axes.spines.right'] = False\n",
|
| 84 |
+
"print(f'PROJECT_ROOT: {PROJECT_ROOT}')"
|
| 85 |
+
]
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"cell_type": "code",
|
| 89 |
+
"execution_count": 3,
|
| 90 |
+
"metadata": {},
|
| 91 |
+
"outputs": [
|
| 92 |
+
{
|
| 93 |
+
"name": "stdout",
|
| 94 |
+
"output_type": "stream",
|
| 95 |
+
"text": [
|
| 96 |
+
"LR baseline: True | RF baseline: True\n"
|
| 97 |
+
]
|
| 98 |
+
}
|
| 99 |
+
],
|
| 100 |
+
"source": [
|
| 101 |
+
"CONFIG_FEAT = PROJECT_ROOT / 'configs' / 'features.yaml'\n",
|
| 102 |
+
"CONFIG_PIPE = PROJECT_ROOT / 'configs' / 'pipeline.yaml'\n",
|
| 103 |
+
"CONFIG_MOD = PROJECT_ROOT / 'configs' / 'models.yaml'\n",
|
| 104 |
+
"\n",
|
| 105 |
+
"with open(CONFIG_FEAT) as f: feat_cfg = yaml.safe_load(f)\n",
|
| 106 |
+
"with open(CONFIG_PIPE) as f: pipe_cfg = yaml.safe_load(f)\n",
|
| 107 |
+
"with open(CONFIG_MOD) as f: mod_cfg = yaml.safe_load(f)\n",
|
| 108 |
+
"\n",
|
| 109 |
+
"tfidf_cfg = feat_cfg['vectorization']['tfidf']\n",
|
| 110 |
+
"rf_cfg = mod_cfg['models']['random_forest']\n",
|
| 111 |
+
"lr_cfg = mod_cfg['models']['logistic_regression']\n",
|
| 112 |
+
"TARGET = pipe_cfg['data']['target_binary']\n",
|
| 113 |
+
"RAND = pipe_cfg['pipeline']['random_state']\n",
|
| 114 |
+
"TEST_SIZE = pipe_cfg['pipeline']['test_size']\n",
|
| 115 |
+
"CV_FOLDS = pipe_cfg['pipeline']['cv_folds']\n",
|
| 116 |
+
"\n",
|
| 117 |
+
"MODEL_LR = PROJECT_ROOT / 'models' / 'lr_baseline.joblib'\n",
|
| 118 |
+
"MODEL_RF = PROJECT_ROOT / 'models' / 'best_ensemble.joblib'\n",
|
| 119 |
+
"print(f'LR baseline: {MODEL_LR.exists()} | RF baseline: {MODEL_RF.exists()}')"
|
| 120 |
+
]
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"cell_type": "markdown",
|
| 124 |
+
"metadata": {},
|
| 125 |
+
"source": [
|
| 126 |
+
"## 1. Carga de datos y split\n",
|
| 127 |
+
"\n",
|
| 128 |
+
"Mismo `random_state` fijo en todos los notebooks β comparaciΓ³n justa."
|
| 129 |
+
]
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"cell_type": "code",
|
| 133 |
+
"execution_count": 4,
|
| 134 |
+
"metadata": {},
|
| 135 |
+
"outputs": [
|
| 136 |
+
{
|
| 137 |
+
"name": "stdout",
|
| 138 |
+
"output_type": "stream",
|
| 139 |
+
"text": [
|
| 140 |
+
"Train: 800 | Test: 200\n"
|
| 141 |
+
]
|
| 142 |
+
}
|
| 143 |
+
],
|
| 144 |
+
"source": [
|
| 145 |
+
"PROCESSED = PROJECT_ROOT / 'data' / 'processed' / 'v2' / 'comments_preprocessed.csv'\n",
|
| 146 |
+
"df = pd.read_csv(PROCESSED)\n",
|
| 147 |
+
"df['clean_text'] = df['clean_text'].fillna('').astype(str)\n",
|
| 148 |
+
"X, y = df['clean_text'], df[TARGET]\n",
|
| 149 |
+
"\n",
|
| 150 |
+
"X_train, X_test, y_train, y_test = train_test_split(\n",
|
| 151 |
+
" X, y, test_size=TEST_SIZE, random_state=RAND, stratify=y\n",
|
| 152 |
+
")\n",
|
| 153 |
+
"cv_strategy = StratifiedKFold(n_splits=CV_FOLDS, shuffle=True, random_state=RAND)\n",
|
| 154 |
+
"print(f'Train: {len(X_train)} | Test: {len(X_test)}')"
|
| 155 |
+
]
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"cell_type": "markdown",
|
| 159 |
+
"metadata": {},
|
| 160 |
+
"source": [
|
| 161 |
+
"## 2. FunciΓ³n de evaluaciΓ³n\n",
|
| 162 |
+
"\n",
|
| 163 |
+
"Calcula ambas mΓ©tricas de gap. `cv_scores` es opcional β\n",
|
| 164 |
+
"solo los modelos evaluados con `cross_validate` tendrΓ‘n `cv_test_gap`."
|
| 165 |
+
]
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"cell_type": "code",
|
| 169 |
+
"execution_count": 5,
|
| 170 |
+
"metadata": {},
|
| 171 |
+
"outputs": [],
|
| 172 |
+
"source": [
|
| 173 |
+
"def evaluate_pipeline(pipeline, X_tr, y_tr, X_te, y_te, name, cv_scores=None):\n",
|
| 174 |
+
" \"\"\"\n",
|
| 175 |
+
" Evalua un pipeline y devuelve dict con ambas metricas de gap.\n",
|
| 176 |
+
"\n",
|
| 177 |
+
" train_test_gap : f1_train - f1_test (in-sample vs out-of-sample)\n",
|
| 178 |
+
" cv_test_gap : |cv_mean - f1_test| (OOS vs OOS β para rubrica)\n",
|
| 179 |
+
" \"\"\"\n",
|
| 180 |
+
" y_pred = pipeline.predict(X_te)\n",
|
| 181 |
+
" y_pred_proba = pipeline.predict_proba(X_te)[:, 1]\n",
|
| 182 |
+
" y_pred_train = pipeline.predict(X_tr)\n",
|
| 183 |
+
"\n",
|
| 184 |
+
" f1_te = f1_score(y_te, y_pred, average='weighted')\n",
|
| 185 |
+
" f1_tr = f1_score(y_tr, y_pred_train, average='weighted')\n",
|
| 186 |
+
" roc = roc_auc_score(y_te, y_pred_proba)\n",
|
| 187 |
+
"\n",
|
| 188 |
+
" cv_mean = cv_std = cv_test_gap = None\n",
|
| 189 |
+
" if cv_scores is not None:\n",
|
| 190 |
+
" cv_mean = cv_scores['test_score'].mean()\n",
|
| 191 |
+
" cv_std = cv_scores['test_score'].std()\n",
|
| 192 |
+
" cv_test_gap = abs(cv_mean - f1_te) * 100\n",
|
| 193 |
+
"\n",
|
| 194 |
+
" return {\n",
|
| 195 |
+
" 'name' : name,\n",
|
| 196 |
+
" 'f1_test' : round(f1_te, 4),\n",
|
| 197 |
+
" 'f1_train' : round(f1_tr, 4),\n",
|
| 198 |
+
" 'train_test_gap_pp': round((f1_tr - f1_te) * 100, 2),\n",
|
| 199 |
+
" 'cv_mean' : round(cv_mean, 4) if cv_mean is not None else None,\n",
|
| 200 |
+
" 'cv_std' : round(cv_std, 4) if cv_std is not None else None,\n",
|
| 201 |
+
" 'cv_test_gap_pp' : round(cv_test_gap, 2) if cv_test_gap is not None else None,\n",
|
| 202 |
+
" 'roc_auc' : round(roc, 4),\n",
|
| 203 |
+
" 'fp' : int(((y_te == False) & (y_pred == True)).sum()),\n",
|
| 204 |
+
" 'fn' : int(((y_te == True) & (y_pred == False)).sum()),\n",
|
| 205 |
+
" }"
|
| 206 |
+
]
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"cell_type": "markdown",
|
| 210 |
+
"metadata": {},
|
| 211 |
+
"source": [
|
| 212 |
+
"## 3. Helper TF-IDF y carga de baselines\n",
|
| 213 |
+
"\n",
|
| 214 |
+
"Los baselines se cargan desde disco y se evalΓΊan sin re-entrenar."
|
| 215 |
+
]
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"cell_type": "code",
|
| 219 |
+
"execution_count": 6,
|
| 220 |
+
"metadata": {},
|
| 221 |
+
"outputs": [
|
| 222 |
+
{
|
| 223 |
+
"name": "stdout",
|
| 224 |
+
"output_type": "stream",
|
| 225 |
+
"text": [
|
| 226 |
+
" LR baseline F1=0.7531 | train-test=10.91pp | cv-test=5.17pp\n",
|
| 227 |
+
" RF baseline F1=0.7531 | train-test=10.91pp | cv-test=5.17pp\n"
|
| 228 |
+
]
|
| 229 |
+
}
|
| 230 |
+
],
|
| 231 |
+
"source": [
|
| 232 |
+
"def make_tfidf(**overrides):\n",
|
| 233 |
+
" params = {\n",
|
| 234 |
+
" 'max_features': tfidf_cfg['max_features'],\n",
|
| 235 |
+
" 'ngram_range' : tuple(tfidf_cfg['ngram_range']),\n",
|
| 236 |
+
" 'sublinear_tf': tfidf_cfg['sublinear_tf'],\n",
|
| 237 |
+
" 'min_df' : tfidf_cfg['min_df'],\n",
|
| 238 |
+
" 'analyzer' : 'word',\n",
|
| 239 |
+
" 'strip_accents': 'unicode',\n",
|
| 240 |
+
" }\n",
|
| 241 |
+
" params.update(overrides)\n",
|
| 242 |
+
" return TfidfVectorizer(**params)\n",
|
| 243 |
+
"\n",
|
| 244 |
+
"# Cargar baselines desde disco\n",
|
| 245 |
+
"lr_baseline_pipe = joblib.load(MODEL_LR)\n",
|
| 246 |
+
"rf_baseline_pipe = joblib.load(MODEL_RF)\n",
|
| 247 |
+
"\n",
|
| 248 |
+
"# CV scores para baselines\n",
|
| 249 |
+
"cv_lr_base = cross_validate(lr_baseline_pipe, X_train, y_train,\n",
|
| 250 |
+
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 251 |
+
" return_train_score=False, n_jobs=-1)\n",
|
| 252 |
+
"cv_rf_base = cross_validate(rf_baseline_pipe, X_train, y_train,\n",
|
| 253 |
+
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 254 |
+
" return_train_score=False, n_jobs=-1)\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"metrics_lr_base = evaluate_pipeline(lr_baseline_pipe, X_train, y_train,\n",
|
| 257 |
+
" X_test, y_test, 'LR baseline', cv_lr_base)\n",
|
| 258 |
+
"metrics_rf_base = evaluate_pipeline(rf_baseline_pipe, X_train, y_train,\n",
|
| 259 |
+
" X_test, y_test, 'RF baseline', cv_rf_base)\n",
|
| 260 |
+
"\n",
|
| 261 |
+
"for m in [metrics_lr_base, metrics_rf_base]:\n",
|
| 262 |
+
" print(f\" {m['name']:15} F1={m['f1_test']:.4f} | \"\n",
|
| 263 |
+
" f\"train-test={m['train_test_gap_pp']}pp | \"\n",
|
| 264 |
+
" f\"cv-test={m['cv_test_gap_pp']}pp\")"
|
| 265 |
+
]
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"cell_type": "markdown",
|
| 269 |
+
"metadata": {},
|
| 270 |
+
"source": [
|
| 271 |
+
"## 4. LinearSVC β modelo adicional\n",
|
| 272 |
+
"\n",
|
| 273 |
+
"### ΒΏPor quΓ© LinearSVC en TF-IDF?\n",
|
| 274 |
+
"LinearSVC usa un hiperplano de separaciΓ³n lineal que funciona\n",
|
| 275 |
+
"excepcionalmente bien con matrices sparse de alta dimensiΓ³n.\n",
|
| 276 |
+
"No tiene probabilidades nativas, pero `CalibratedClassifierCV`\n",
|
| 277 |
+
"las aΓ±ade mediante Platt scaling.\n",
|
| 278 |
+
"\n",
|
| 279 |
+
"Frecuentemente supera a LR en clasificaciΓ³n de texto porque\n",
|
| 280 |
+
"maximiza el margen entre clases en lugar de minimizar log-loss."
|
| 281 |
+
]
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
"cell_type": "code",
|
| 285 |
+
"execution_count": 7,
|
| 286 |
+
"metadata": {},
|
| 287 |
+
"outputs": [
|
| 288 |
+
{
|
| 289 |
+
"name": "stdout",
|
| 290 |
+
"output_type": "stream",
|
| 291 |
+
"text": [
|
| 292 |
+
" LinearSVC F1=0.7250 | train-test=23.99pp | cv-test=4.03pp\n"
|
| 293 |
+
]
|
| 294 |
+
}
|
| 295 |
+
],
|
| 296 |
+
"source": [
|
| 297 |
+
"# LinearSVC con calibraciΓ³n para obtener predict_proba\n",
|
| 298 |
+
"svc_pipeline = Pipeline([\n",
|
| 299 |
+
" ('tfidf', make_tfidf()),\n",
|
| 300 |
+
" ('clf', CalibratedClassifierCV(\n",
|
| 301 |
+
" LinearSVC(\n",
|
| 302 |
+
" C=1.0, max_iter=2000,\n",
|
| 303 |
+
" class_weight='balanced',\n",
|
| 304 |
+
" random_state=RAND,\n",
|
| 305 |
+
" ),\n",
|
| 306 |
+
" cv=3\n",
|
| 307 |
+
" ))\n",
|
| 308 |
+
"])\n",
|
| 309 |
+
"\n",
|
| 310 |
+
"svc_pipeline.fit(X_train, y_train)\n",
|
| 311 |
+
"\n",
|
| 312 |
+
"cv_svc = cross_validate(svc_pipeline, X_train, y_train,\n",
|
| 313 |
+
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 314 |
+
" return_train_score=False, n_jobs=-1)\n",
|
| 315 |
+
"\n",
|
| 316 |
+
"metrics_svc = evaluate_pipeline(svc_pipeline, X_train, y_train,\n",
|
| 317 |
+
" X_test, y_test, 'LinearSVC', cv_svc)\n",
|
| 318 |
+
"\n",
|
| 319 |
+
"print(f\" LinearSVC F1={metrics_svc['f1_test']:.4f} | \"\n",
|
| 320 |
+
" f\"train-test={metrics_svc['train_test_gap_pp']}pp | \"\n",
|
| 321 |
+
" f\"cv-test={metrics_svc['cv_test_gap_pp']}pp\")"
|
| 322 |
+
]
|
| 323 |
+
},
|
| 324 |
+
{
|
| 325 |
+
"cell_type": "markdown",
|
| 326 |
+
"metadata": {},
|
| 327 |
+
"source": [
|
| 328 |
+
"## 5. Optuna β Logistic Regression\n",
|
| 329 |
+
"\n",
|
| 330 |
+
"BΓΊsqueda bayesiana sobre el pipeline completo TF-IDF + LR.\n",
|
| 331 |
+
"Optuna solo ve X_train β X_test se reserva para evaluaciΓ³n final."
|
| 332 |
+
]
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"cell_type": "code",
|
| 336 |
+
"execution_count": 8,
|
| 337 |
+
"metadata": {},
|
| 338 |
+
"outputs": [
|
| 339 |
+
{
|
| 340 |
+
"name": "stdout",
|
| 341 |
+
"output_type": "stream",
|
| 342 |
+
"text": [
|
| 343 |
+
"Optimizando LR β 60 trials...\n"
|
| 344 |
+
]
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"name": "stderr",
|
| 348 |
+
"output_type": "stream",
|
| 349 |
+
"text": [
|
| 350 |
+
"Best trial: 52. Best value: 0.710353: 100%|ββββββββββ| 60/60 [00:04<00:00, 14.23it/s]"
|
| 351 |
+
]
|
| 352 |
+
},
|
| 353 |
+
{
|
| 354 |
+
"name": "stdout",
|
| 355 |
+
"output_type": "stream",
|
| 356 |
+
"text": [
|
| 357 |
+
"Mejor F1 CV: 0.7104\n",
|
| 358 |
+
"Params : {'ngram_range': '1_2', 'max_features': 4045, 'min_df': 2, 'sublinear_tf': False, 'C': 0.3235215031170205}\n"
|
| 359 |
+
]
|
| 360 |
+
},
|
| 361 |
+
{
|
| 362 |
+
"name": "stderr",
|
| 363 |
+
"output_type": "stream",
|
| 364 |
+
"text": [
|
| 365 |
+
"\n"
|
| 366 |
+
]
|
| 367 |
+
}
|
| 368 |
+
],
|
| 369 |
+
"source": [
|
| 370 |
+
"N_TRIALS = 60\n",
|
| 371 |
+
"\n",
|
| 372 |
+
"def objective_lr(trial):\n",
|
| 373 |
+
" ngram_str = trial.suggest_categorical('ngram_range', ['1_1', '1_2'])\n",
|
| 374 |
+
" ngram = (1,1) if ngram_str == '1_1' else (1,2)\n",
|
| 375 |
+
" pipe = Pipeline([\n",
|
| 376 |
+
" ('tfidf', make_tfidf(\n",
|
| 377 |
+
" max_features = trial.suggest_int('max_features', 500, 5000),\n",
|
| 378 |
+
" min_df = trial.suggest_int('min_df', 2, 6),\n",
|
| 379 |
+
" ngram_range = ngram,\n",
|
| 380 |
+
" sublinear_tf = trial.suggest_categorical('sublinear_tf', [True, False]),\n",
|
| 381 |
+
" )),\n",
|
| 382 |
+
" ('clf', LogisticRegression(\n",
|
| 383 |
+
" C = trial.suggest_float('C', 0.01, 2.0, log=True),\n",
|
| 384 |
+
" max_iter = 1000,\n",
|
| 385 |
+
" class_weight = 'balanced',\n",
|
| 386 |
+
" solver = 'lbfgs',\n",
|
| 387 |
+
" random_state = RAND,\n",
|
| 388 |
+
" ))\n",
|
| 389 |
+
" ])\n",
|
| 390 |
+
" return cross_val_score(pipe, X_train, y_train,\n",
|
| 391 |
+
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 392 |
+
" n_jobs=-1).mean()\n",
|
| 393 |
+
"\n",
|
| 394 |
+
"study_lr = optuna.create_study(direction='maximize',\n",
|
| 395 |
+
" sampler=optuna.samplers.TPESampler(seed=RAND),\n",
|
| 396 |
+
" study_name='lr_optimization')\n",
|
| 397 |
+
"print(f'Optimizando LR β {N_TRIALS} trials...')\n",
|
| 398 |
+
"study_lr.optimize(objective_lr, n_trials=N_TRIALS, show_progress_bar=True)\n",
|
| 399 |
+
"print(f'Mejor F1 CV: {study_lr.best_value:.4f}')\n",
|
| 400 |
+
"print(f'Params : {study_lr.best_trial.params}')"
|
| 401 |
+
]
|
| 402 |
+
},
|
| 403 |
+
{
|
| 404 |
+
"cell_type": "code",
|
| 405 |
+
"execution_count": 9,
|
| 406 |
+
"metadata": {},
|
| 407 |
+
"outputs": [
|
| 408 |
+
{
|
| 409 |
+
"name": "stdout",
|
| 410 |
+
"output_type": "stream",
|
| 411 |
+
"text": [
|
| 412 |
+
"LR tuned F1=0.7579 | train-test=14.07pp | cv-test=4.76pp\n"
|
| 413 |
+
]
|
| 414 |
+
}
|
| 415 |
+
],
|
| 416 |
+
"source": [
|
| 417 |
+
"bp_lr = study_lr.best_trial.params\n",
|
| 418 |
+
"ngram_lr = (1,1) if bp_lr['ngram_range'] == '1_1' else (1,2)\n",
|
| 419 |
+
"\n",
|
| 420 |
+
"lr_tuned_pipe = Pipeline([\n",
|
| 421 |
+
" ('tfidf', make_tfidf(\n",
|
| 422 |
+
" max_features=bp_lr['max_features'], min_df=bp_lr['min_df'],\n",
|
| 423 |
+
" ngram_range=ngram_lr, sublinear_tf=bp_lr['sublinear_tf'],\n",
|
| 424 |
+
" )),\n",
|
| 425 |
+
" ('clf', LogisticRegression(\n",
|
| 426 |
+
" C=bp_lr['C'], max_iter=1000,\n",
|
| 427 |
+
" class_weight='balanced', solver='lbfgs', random_state=RAND,\n",
|
| 428 |
+
" ))\n",
|
| 429 |
+
"])\n",
|
| 430 |
+
"lr_tuned_pipe.fit(X_train, y_train)\n",
|
| 431 |
+
"\n",
|
| 432 |
+
"cv_lr_tuned = cross_validate(lr_tuned_pipe, X_train, y_train,\n",
|
| 433 |
+
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 434 |
+
" return_train_score=False, n_jobs=-1)\n",
|
| 435 |
+
"\n",
|
| 436 |
+
"metrics_lr_tuned = evaluate_pipeline(lr_tuned_pipe, X_train, y_train,\n",
|
| 437 |
+
" X_test, y_test, 'LR tuned', cv_lr_tuned)\n",
|
| 438 |
+
"print(f\"LR tuned F1={metrics_lr_tuned['f1_test']:.4f} | \"\n",
|
| 439 |
+
" f\"train-test={metrics_lr_tuned['train_test_gap_pp']}pp | \"\n",
|
| 440 |
+
" f\"cv-test={metrics_lr_tuned['cv_test_gap_pp']}pp\")"
|
| 441 |
+
]
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
"cell_type": "markdown",
|
| 445 |
+
"metadata": {},
|
| 446 |
+
"source": [
|
| 447 |
+
"## 6. Optuna β Random Forest\n",
|
| 448 |
+
"\n",
|
| 449 |
+
"BΓΊsqueda sobre n_estimators, max_depth y min_samples_leaf.\n",
|
| 450 |
+
"RF con bigramas es muy lento β usamos solo unigramas."
|
| 451 |
+
]
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"cell_type": "code",
|
| 455 |
+
"execution_count": 10,
|
| 456 |
+
"metadata": {},
|
| 457 |
+
"outputs": [
|
| 458 |
+
{
|
| 459 |
+
"name": "stdout",
|
| 460 |
+
"output_type": "stream",
|
| 461 |
+
"text": [
|
| 462 |
+
"Optimizando RF β 60 trials...\n"
|
| 463 |
+
]
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"name": "stderr",
|
| 467 |
+
"output_type": "stream",
|
| 468 |
+
"text": [
|
| 469 |
+
"Best trial: 37. Best value: 0.713928: 100%|ββββββββββ| 60/60 [00:54<00:00, 1.09it/s]"
|
| 470 |
+
]
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"name": "stdout",
|
| 474 |
+
"output_type": "stream",
|
| 475 |
+
"text": [
|
| 476 |
+
"Mejor F1 CV: 0.7139\n",
|
| 477 |
+
"Params : {'max_features': 1380, 'min_df': 6, 'n_estimators': 171, 'max_depth': 9, 'min_samples_leaf': 2}\n"
|
| 478 |
+
]
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"name": "stderr",
|
| 482 |
+
"output_type": "stream",
|
| 483 |
+
"text": [
|
| 484 |
+
"\n"
|
| 485 |
+
]
|
| 486 |
+
}
|
| 487 |
+
],
|
| 488 |
+
"source": [
|
| 489 |
+
"def objective_rf(trial):\n",
|
| 490 |
+
" pipe = Pipeline([\n",
|
| 491 |
+
" ('tfidf', make_tfidf(\n",
|
| 492 |
+
" max_features = trial.suggest_int('max_features', 500, 5000),\n",
|
| 493 |
+
" min_df = trial.suggest_int('min_df', 2, 6),\n",
|
| 494 |
+
" ngram_range = (1, 1),\n",
|
| 495 |
+
" sublinear_tf = True,\n",
|
| 496 |
+
" )),\n",
|
| 497 |
+
" ('clf', RandomForestClassifier(\n",
|
| 498 |
+
" n_estimators = trial.suggest_int('n_estimators', 50, 300),\n",
|
| 499 |
+
" max_depth = trial.suggest_int('max_depth', 4, 12),\n",
|
| 500 |
+
" min_samples_leaf = trial.suggest_int('min_samples_leaf', 2, 8),\n",
|
| 501 |
+
" max_features = 'sqrt',\n",
|
| 502 |
+
" class_weight = 'balanced',\n",
|
| 503 |
+
" random_state = RAND, n_jobs=-1,\n",
|
| 504 |
+
" ))\n",
|
| 505 |
+
" ])\n",
|
| 506 |
+
" return cross_val_score(pipe, X_train, y_train,\n",
|
| 507 |
+
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 508 |
+
" n_jobs=-1).mean()\n",
|
| 509 |
+
"\n",
|
| 510 |
+
"study_rf = optuna.create_study(direction='maximize',\n",
|
| 511 |
+
" sampler=optuna.samplers.TPESampler(seed=RAND),\n",
|
| 512 |
+
" study_name='rf_optimization')\n",
|
| 513 |
+
"print(f'Optimizando RF β {N_TRIALS} trials...')\n",
|
| 514 |
+
"study_rf.optimize(objective_rf, n_trials=N_TRIALS, show_progress_bar=True)\n",
|
| 515 |
+
"print(f'Mejor F1 CV: {study_rf.best_value:.4f}')\n",
|
| 516 |
+
"print(f'Params : {study_rf.best_trial.params}')"
|
| 517 |
+
]
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"cell_type": "code",
|
| 521 |
+
"execution_count": 11,
|
| 522 |
+
"metadata": {},
|
| 523 |
+
"outputs": [
|
| 524 |
+
{
|
| 525 |
+
"name": "stdout",
|
| 526 |
+
"output_type": "stream",
|
| 527 |
+
"text": [
|
| 528 |
+
"RF tuned F1=0.6924 | train-test=12.09pp | cv-test=2.15pp\n"
|
| 529 |
+
]
|
| 530 |
+
}
|
| 531 |
+
],
|
| 532 |
+
"source": [
|
| 533 |
+
"bp_rf = study_rf.best_trial.params\n",
|
| 534 |
+
"\n",
|
| 535 |
+
"rf_tuned_pipe = Pipeline([\n",
|
| 536 |
+
" ('tfidf', make_tfidf(\n",
|
| 537 |
+
" max_features=bp_rf['max_features'], min_df=bp_rf['min_df'],\n",
|
| 538 |
+
" ngram_range=(1,1), sublinear_tf=True,\n",
|
| 539 |
+
" )),\n",
|
| 540 |
+
" ('clf', RandomForestClassifier(\n",
|
| 541 |
+
" n_estimators=bp_rf['n_estimators'], max_depth=bp_rf['max_depth'],\n",
|
| 542 |
+
" min_samples_leaf=bp_rf['min_samples_leaf'],\n",
|
| 543 |
+
" max_features='sqrt', class_weight='balanced',\n",
|
| 544 |
+
" random_state=RAND, n_jobs=-1,\n",
|
| 545 |
+
" ))\n",
|
| 546 |
+
"])\n",
|
| 547 |
+
"rf_tuned_pipe.fit(X_train, y_train)\n",
|
| 548 |
+
"\n",
|
| 549 |
+
"cv_rf_tuned = cross_validate(rf_tuned_pipe, X_train, y_train,\n",
|
| 550 |
+
" cv=cv_strategy, scoring='f1_weighted',\n",
|
| 551 |
+
" return_train_score=False, n_jobs=-1)\n",
|
| 552 |
+
"\n",
|
| 553 |
+
"metrics_rf_tuned = evaluate_pipeline(rf_tuned_pipe, X_train, y_train,\n",
|
| 554 |
+
" X_test, y_test, 'RF tuned', cv_rf_tuned)\n",
|
| 555 |
+
"print(f\"RF tuned F1={metrics_rf_tuned['f1_test']:.4f} | \"\n",
|
| 556 |
+
" f\"train-test={metrics_rf_tuned['train_test_gap_pp']}pp | \"\n",
|
| 557 |
+
" f\"cv-test={metrics_rf_tuned['cv_test_gap_pp']}pp\")"
|
| 558 |
+
]
|
| 559 |
+
},
|
| 560 |
+
{
|
| 561 |
+
"cell_type": "markdown",
|
| 562 |
+
"metadata": {},
|
| 563 |
+
"source": [
|
| 564 |
+
"## 7. Tabla comparativa β 5 modelos\n",
|
| 565 |
+
"\n",
|
| 566 |
+
"Se reportan ambos gaps. La columna `cv_test_gap` es la referencia\n",
|
| 567 |
+
"para la rΓΊbrica (OOS vs OOS). La columna `train_test_gap` se mantiene\n",
|
| 568 |
+
"para anΓ‘lisis de memorizaciΓ³n."
|
| 569 |
+
]
|
| 570 |
+
},
|
| 571 |
+
{
|
| 572 |
+
"cell_type": "code",
|
| 573 |
+
"execution_count": 12,
|
| 574 |
+
"metadata": {},
|
| 575 |
+
"outputs": [
|
| 576 |
+
{
|
| 577 |
+
"name": "stdout",
|
| 578 |
+
"output_type": "stream",
|
| 579 |
+
"text": [
|
| 580 |
+
"COMPARATIVA FINAL β 5 MODELOS\n",
|
| 581 |
+
"====================================================================================================\n",
|
| 582 |
+
"Modelo F1 Test F1 Train TrTe gap CV Mean CV Std CV-Te gap FP FN Rubrica\n",
|
| 583 |
+
"----------------------------------------------------------------------------------------------------\n",
|
| 584 |
+
" LR baseline 0.7531 0.8623 10.91 0.7015 0.0312 5.17 19 30 β οΈ 5.2pp\n",
|
| 585 |
+
" LR tuned 0.7579 0.8987 14.07 0.7104 0.0353 4.76 18 30 β
OK\n",
|
| 586 |
+
" RF baseline 0.7531 0.8623 10.91 0.7015 0.0312 5.17 19 30 β οΈ 5.2pp\n",
|
| 587 |
+
" RF tuned 0.6924 0.8133 12.09 0.7139 0.0334 2.15 9 49 β
OK\n",
|
| 588 |
+
" LinearSVC 0.7250 0.9649 23.99 0.6847 0.0276 4.03 17 37 β
OK\n",
|
| 589 |
+
"\n",
|
| 590 |
+
"GANADOR (F1 test): LR tuned\n",
|
| 591 |
+
" F1 test : 0.7579\n",
|
| 592 |
+
" cv_test_gap : 4.76pp\n",
|
| 593 |
+
" train_test_gap: 14.07pp\n"
|
| 594 |
+
]
|
| 595 |
+
}
|
| 596 |
+
],
|
| 597 |
+
"source": [
|
| 598 |
+
"all_metrics = [\n",
|
| 599 |
+
" metrics_lr_base, metrics_lr_tuned,\n",
|
| 600 |
+
" metrics_rf_base, metrics_rf_tuned,\n",
|
| 601 |
+
" metrics_svc\n",
|
| 602 |
+
"]\n",
|
| 603 |
+
"\n",
|
| 604 |
+
"comp_df = pd.DataFrame(all_metrics).set_index('name')\n",
|
| 605 |
+
"\n",
|
| 606 |
+
"# Rubrica basada en cv_test_gap\n",
|
| 607 |
+
"comp_df['rubrica'] = comp_df['cv_test_gap_pp'].apply(\n",
|
| 608 |
+
" lambda x: 'β
OK' if x is not None and x < 5 else f'β οΈ {x:.1f}pp' if x else 'N/A'\n",
|
| 609 |
+
")\n",
|
| 610 |
+
"\n",
|
| 611 |
+
"print('COMPARATIVA FINAL β 5 MODELOS')\n",
|
| 612 |
+
"print('=' * 100)\n",
|
| 613 |
+
"print(f\"{'Modelo':16} {'F1 Test':>9} {'F1 Train':>9} \"\n",
|
| 614 |
+
" f\"{'TrTe gap':>9} {'CV Mean':>9} {'CV Std':>8} \"\n",
|
| 615 |
+
" f\"{'CV-Te gap':>10} {'FP':>4} {'FN':>4} {'Rubrica':>12}\")\n",
|
| 616 |
+
"print('-' * 100)\n",
|
| 617 |
+
"for name, row in comp_df.iterrows():\n",
|
| 618 |
+
" cv_gap_str = f\"{row['cv_test_gap_pp']:.2f}\" if row['cv_test_gap_pp'] else 'N/A'\n",
|
| 619 |
+
" cv_mean_str = f\"{row['cv_mean']:.4f}\" if row['cv_mean'] else 'N/A'\n",
|
| 620 |
+
" cv_std_str = f\"{row['cv_std']:.4f}\" if row['cv_std'] else 'N/A'\n",
|
| 621 |
+
" print(f\" {name:14} {row['f1_test']:>9.4f} {row['f1_train']:>9.4f} \"\n",
|
| 622 |
+
" f\"{row['train_test_gap_pp']:>9.2f} {cv_mean_str:>9} {cv_std_str:>8} \"\n",
|
| 623 |
+
" f\"{cv_gap_str:>10} {row['fp']:>4} {row['fn']:>4} {row['rubrica']:>12}\")\n",
|
| 624 |
+
"\n",
|
| 625 |
+
"best_name = comp_df['f1_test'].idxmax()\n",
|
| 626 |
+
"print(f'\\nGANADOR (F1 test): {best_name}')\n",
|
| 627 |
+
"print(f\" F1 test : {comp_df.loc[best_name, 'f1_test']:.4f}\")\n",
|
| 628 |
+
"print(f\" cv_test_gap : {comp_df.loc[best_name, 'cv_test_gap_pp']:.2f}pp\")\n",
|
| 629 |
+
"print(f\" train_test_gap: {comp_df.loc[best_name, 'train_test_gap_pp']:.2f}pp\")"
|
| 630 |
+
]
|
| 631 |
+
},
|
| 632 |
+
{
|
| 633 |
+
"cell_type": "code",
|
| 634 |
+
"execution_count": 13,
|
| 635 |
+
"metadata": {},
|
| 636 |
+
"outputs": [
|
| 637 |
+
{
|
| 638 |
+
"data": {
|
| 639 |
+
"image/png": "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Yr788kulpqZKkqZMmaJ169bpk08+0fPPP6/atWtn+4ZumXnttde0bt06SVLz5s21bNkyffbZZ6pZs6Ykafr06frpp58kSWvXrtWVK1ckSU8++aTWrVunzz77TC+++KIaN24sm81mDmmRfkzCJUuWaNOmTdq0aZPKlSt3wzkDAAqOuLg4u3FsW7duneU6s2fP1saNGyVJ4eHhWrRokT7++GNVqlRJkrRx40bNmjXL4bqenp5atmyZeQMnSZo2bZqaNGmiL7/8Ug888ICktKEJ3n33XYfb2L9/vyZPnqyvvvpKHTp0kCQlJydrxIgRZkzTpk01Z84crVixQuvXr9fatWv18ssvS0r72a2z/G6kjZLT79hy5crp9ttvlyRt2bJFJ06ckJT28981a9ZIkurWrat69erd0LFdk5KSohEjRsgwDEnSgw8+qJUrV2r48OFO73PQo0cPzZ8/XytXrlRUVJRWrlypPn36SJL27Nmjzz//3OF618abXbJkiapWrSpJOn78uKZMmWIe79mzZyVJPXv21Nq1a7V8+XK9+uqratu2rTw9PTM9liNHjig+Pl6SdNNNN2Uam1Uu10tOTtbixYu1cOFClS5dWpK0e/du/ec//7GLS7/fP/74I9Mc8oqvr6+6d+8uSTp27Jh+/PFHc9nSpUslSaVKlTKH33ruuef066+/SpI6d+5svt/DwsIkSSNHjnR4Y7UTJ06oYsWK+u9//6tVq1apa9euunjxosaPH2/GPPHEE1q5cqV69OihPXv2ZNjG8ePHNWzYMKWmpiowMFBz5szR119/rf79+0uS9u7dm2F4t/x08OBBNW7cWEuWLNHy5cuzdU3MzJ49e8zP2MiRI835EydO1Llz5zLEHzhwQB06dNDy5cv16aef6pZbbnG67eeee84cXq1kyZKaNWuW1qxZozlz5qhWrVpm3I1eMwzDUJ8+fXThwgVJUp06dfThhx9q5cqVGj9+vEqVKiUp7Zo1YMAAJScnS5IGDx6sVatWaciQIZLSPlMDBgwwP7OuHLej997ly5fVt29fJSYmysvLSy+99JK++eYbPfPMM5KkU6dOmfvO7us1Z84cvf766+Y6nTp1Mq/hzz33XK68nshH7q4aA8D1svqvs7Pet/nZ07Zly5YO1//yyy+NO++80yhdurTh6emZIfdrPUyy09P2vvvuM+d/8skn5vwR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81NCQoK2bt2qESNGqEOHDvrkk08cDl/QvXt3jRkzRomJiZo2bZpsNptatGihy5cv6+DBg1q/fr2uXLlil78jEydO1MSJE7P92tyIkJAQ87O5f/9+DRo0SDVr1tSrr76a5bqHDx9W37591atXL61bt878NYOvr686duyY45w8PDzUq1cvzZ07V1La3ynjxo1TrVq1dODAAa1YsUKrVq3KlWtGZGSk6tSpo99++00xMTG644479Mwzz6hkyZLavn27Ll68qBkzZuiuu+4yX6dt27Zp2LBhuvvuu7Vq1SqzgF+6dGm7XynciDvvvFNlypTR2bNn9cEHH6hkyZK68847lZKSokOHDun777/Xrl279Mcff2T79ZLsr+GbN2/W6tWrFRgYqBo1auTK64l8lEdj5QJAjqW/mYazm46ll/4GDY6m6wfbz0x2b0SW/sYW1yxZsiTDvv38/OwGrb92A4Ps3Igs/T6yGkzekRMnTmS40cju3bvtYl544QWnr5uHh4exefPmbO3LMJzfiCwxMdG44447Mj1H1+I//PDDTOM+/vhjc7tz5szJsPz6cwYAKDp+//13o2rVqpl+j+zYscMwDMNISEgwWrdu7TSuTZs2RmJiorltRzfqMQzD4fdPdr7j098I7drk5eVlthMMwzDuvvvuDDHpbwCUfp+52UYxjMy/YzO7oZFhGMbgwYPt1nvggQcyxLhybM4sXbrUsNlsGbZTt27dDK9FbGysUa5cuUz3mb59lb5N4+hclStXzryR0aZNmzJ9z02dOjXLY0n/em/bts1umSu5GIb9e9VRfJ06dYwrV644PB9hYWF2N5xyl5dfftku54YNG2aIuXjxot25djRl1e5Ob9q0aRnW9/DwsLumpP+MvPPOO4aHh4fTfbvy90duyM7fCmPHjnX4/gkJCclwfUu/vapVqzo81hdffNGMT39DrgkTJmTYt7NzEB0d7fB9ev11IDeuGdu3b7c71vRT+tds+fLlhre3t8M4b29v44svvrjh405v5cqVDm+O6OjYsvt6JScnG2FhYRlirl0Tc+P1RP5geAQAyCXdu3fX22+/rerVq8vPz09NmjTRmjVrVKdOHbfkU65cOd1+++3m83r16qlu3bp2MZ07d9bjjz+uOnXqqESJEvL09FTJkiV111136euvv86V3hY+Pj5as2aNXn/9dTVt2lSBgYHy8/NTlSpVdPfdd+u9997T/fffLynt5iFPPvmkGjZsqNKlS8vT01PBwcFq3bq1Fi9erIcfftjc7uOPP67Ro0erUqVK2b7BBwCg8Kpdu7Z2796tmTNnqlWrVipZsqR8fHwUHh6uDh066P333zd/+u3r66u1a9dq2rRpqlevnooVKyY/Pz/VrVtXU6dO1TfffCMfH588y3XkyJF64403VK1aNfn4+KhBgwb66quv7HoFfvjhh+rbt69Kly6tkJAQPfroo/ryyy9ztD9X2yg38h2bvkeko+dS7hzbAw88oE8//VS1a9eWj4+Pbr75Zi1atEh33HGHGePv7y8pbbzdtWvX6vbbb1dAQIAqVKigyZMna/LkyVnu5+OPP9bw4cNVpkwZFStWTJ06ddLGjRvNGxnVqFFDo0ePVvPmzRUaGiovLy8FBASoSZMmmjt3rkaPHp3lPnr16mX+NDmz4RSyyuV669at06OPPqrg4GAFBgbq4Ycf1rfffis/Pz8z5tKlS+bPrB955BG7Xt7u0qtXL7v3naP3UEhIiLZs2aIXXnhBkZGRKlasmPz9/VW9enV1795dH3/8sZo3b57tfY4ePVqvvfaaIiIi5Ovrq/r16+uLL75Q69atHcY/9thj2rhxox544AHzvIeGhqpp06YaN26c3nzzTdcPPI+NHz9e//73vxUSEqLixYvrvvvu0/fff5/hZo7Xa926tVasWKEGDRrI19dXlStX1owZM+x6b+dUcHCww/N48803q0+fPmZcblwzGjZsqF27dmnw4MGqWrWqfHx8FBISoubNm6tTp05m3H333actW7aoe/fuKlu2rLy8vFSmTBk98MAD+uGHH8yxu3NL586dtW3bNj366KOqWLGivL29Vbp0adWvX1+jRo0yh7mTsv96eXl5acWKFWrVqpUCAwMz7DM3v1+Qt2yG4cKtIgEAAAAALps4caImTZokKe2O8/369XNvQgWcYRgOx4hs3ry5OebmL7/8ogYNGri87Xbt2mnDhg2SpIMHDyoiIuKGcs2OwYMHa968eapQoYIOHjwob2/vHOUSERGhw4cPS1K2boT35ptvaujQofL19dVff/1l3jAOAOB+dE0CAAAAABQomzZtUs+ePfX111/r8OHD2rVrl4YOHWoWbGvWrKnIyEg3Z5l9zz33nHx9fXX8+HF9+umn+bJPwzD02muvSUrrOUrBFgCshRuRAQAAAAAKlNTUVH3yySf65JNPMiwLDAzUwoULC9TwSRUrVlRCQkK+7tNms2nv3r35uk8AQPYVnG8xAAAAAAAkVa1aVY888oiqVasmf39/+fr66qabbtLgwYO1a9cul8YzBQDAilwe03bjxo165ZVXtH37dp08eVLLli1T165dM10nKipKo0aN0u+//67w8HA9//zzjOEEAAAAAAAAAA643NM2Pj5ekZGRmjt3brbiDx48qLvvvlu33Xabdu7cqREjRuixxx7T119/7XKyAAAAAAAAAFDYudzT1m5lmy3LnrajR4/WypUr9dtvv5nzHn74YUVHR2vNmjU53TUAAAAAAAAAFEp5Pqbtli1b1L59e7t5HTp00JYtW5yuk5iYqNjYWHOKiYnR2bNndQP1ZQAAACDfGYah2NhY2rEAAABwSZ4XbU+dOqXQ0FC7eaGhoYqNjdWVK1ccrjN16lQFBwebU0hIiMqWLatLly7ldboAAAAoIqZOnaomTZooMDBQZcuWVdeuXTPcSb1du3ay2Wx206BBg7K9j0uXLik4OJh2LAAAAFyS50XbnBg7dqxiYmLM6ejRo+5OCQAAAIXMhg0bNHToUP34449au3atkpOTdddddyk+Pt4ubuDAgTp58qQ5TZ8+3U0ZAwAAoKjwyusdhIWF6fTp03bzTp8+raCgIBUrVszhOr6+vvL19c3r1AAAAFCEXX9/hYULF6ps2bLavn272rRpY8739/dXWFhYfqcHAACAIizPe9q2aNFC69ats5u3du1atWjRIq93DQAAAGRbTEyMJKlkyZJ28z/66COVLl1aderU0dixY3X58mWn27j+3gyxsbF5mjMAAAAKJ5d72sbFxWn//v3m84MHD2rnzp0qWbKkKlWqpLFjx+r48eP64IMPJEmDBg3SG2+8oWeeeUb/+te/9N133+nTTz/VypUrc+8oAAAAgBuQmpqqESNGqGXLlqpTp445v1evXqpcubLKly+v3bt3a/To0dq7d68+//xzh9uZOnWqJk2alF9pAwAAoJCyGS7eyjYqKkq33XZbhvl9+/bVwoUL1a9fPx06dEhRUVF264wcOVJ//PGHKlasqHHjxqlfv37Z3mdsbKyCg4MVExOjoKAgV9IFAAAAsjR48GCtXr1amzdvVsWKFZ3Gfffdd7rjjju0f/9+VatWLcPyxMREJSYmms9jY2MVHh5OOxYAAAAucblo6w7ZKdqmpqYqKSkpnzMrPLy9veXp6enuNAAAAPLdsGHD9MUXX2jjxo2qUqVKprHx8fEKCAjQmjVr1KFDhyy3nVU71jAMXb16VSkpKTnOHwUb7XAAAOBInt+ILD8kJSXp4MGDSk1NdXcqBVpISIjCwsJks9ncnQoAAECeMwxDTzzxhJYtW6aoqKgsC7aStHPnTklSuXLlbnj/SUlJOnnyZKZj5KLws9lsqlixogICAtydCgAAsJACX7Q1DEMnT56Up6enwsPD5eGR5/dWK3QMw9Dly5d15swZSbnzRwgAAIDVDR06VIsWLdIXX3yhwMBAnTp1SpIUHBysYsWK6e+//9aiRYvUuXNnlSpVSrt379bIkSPVpk0b1atX74b2nZqaqoMHD8rT01Ply5eXj48P/zgvggzD0NmzZ3Xs2DFVr16dHrcAAMBU4Iu2V69e1eXLl1W+fHn5+/u7O50Cq1ixYpKkM2fOqGzZsjQYAQBAoffWW29Jktq1a2c3f8GCBerXr598fHz07bffavbs2YqPj1d4eLi6deum559//ob3nZSUpNTUVIWHh9OGLeLKlCmjQ4cOKTk5mTY4AAAwFfii7bXxv3x8fNycScF37Q8GGowAAKAoyOrWDuHh4dqwYUOe5sCvxEAPawAA4EihaSXS2LlxvIYAAAAAAACA+xWaoi0AAACA/JOamqp27dpp3rx5Lq+7adMmVaxYMQ+yAgAAKBwo2hYiERERmj17trvTAAAAQCHRrl07p+3LV155RTVq1NCgQYNc3m7r1q117NixbO0nuyIiIlSsWDEFBAQoICBAISEhN7Q9AAAAdyrwY9o6M/zfG/N1f6//p022Y7MahmDChAmaOHGiyzn8/PPPKl68uMvrAQAAAK5ITk6Wl5eX3njjDXenYufjjz9W165d3Z0GAADADaOnrRucPHnSnGbPnq2goCC7eU899ZQZaxiGrl69mq3tlilThrsPAwAAFGTx8c6nhITsx1654jw2F3h7e2vOnDlatWqVJGnhwoWqX7++xo8fr9KlSyssLEyLFy/W999/rzp16ig4OFgDBgxQamqqJCkqKsrsCft///d/2rRpk0aPHq2AgAB16tRJkhQXF6dhw4apUqVKKlu2rPr06aOYmJgc5Xvo0CHZbDa98847ioiIUKlSpTRkyBAlJSXZ5TNnzhyVK1dOYWFhmjBhQpY3qwMAAMgrFG3dICwszJyCg4Nls9nM53/++acCAwO1evVqNWrUSL6+vtq8ebP+/vtv3XfffQoNDVVAQICaNGmib7/91m671w+PYLPZ9O677+r++++Xv7+/qlevrhUrVuTz0QIAACDbAgKcT9262ceWLes89n+FT1NExD/L8shvv/2m0qVL69SpU3rppZf073//W6+99po2bNigPXv26KuvvtLy5cszrDdjxgy1bt1aL7/8suLi4rR69WpJ0r/+9S9duHBBu3fv1sGDB5WcnKxhw4ZlmsPjjz+u0qVLq0WLFmZBOb1ly5Zp586d+vXXX/XDDz9o6tSp5rJLly7pl19+0d9//62oqCjNnz9fH3zwwY29KAAAADlE0daixowZo2nTpmnPnj2qV6+e4uLi1LlzZ61bt047duxQx44d1aVLFx05ciTT7UyaNEkPPfSQdu/erc6dO6t37966cOFCPh0FAAAAiooyZcpo+PDh8vLyUs+ePRUbG6sBAwaoVKlSKl++vNq2batffvklW9s6e/asli5dqrlz5yokJETFixfX5MmTtXjxYqWkpDhc58MPP9TBgwd1/PhxPfHEE+rWrZt+/vlnu5iJEycqJCRE5cuX19ixY/Xhhx+ay1JTU/Xyyy/L399ftWrV0rBhw+yWAwAA5KdCO6ZtQTd58mTdeeed5vOSJUsqMjLSfP7CCy9o2bJlWrFiRaY9Dvr166eePXtKkqZMmaLXX39dW7duVceOHfMueQAAAORMXJzzZZ6e9s/PnHEe63Fd34xDh3KcUnaFhoaaj68N2XX9vLjMji+dQ4cOKTU1VVWqVLGb7+HhoVOnTqlChQoZ1mndurX5uFevXlq+fLmWLl2qJk2amPMrV65s9/j48ePmcz8/P5UtW9bpcgAAgPxE0daiGjdubPc8Li5OEydO1MqVK3Xy5EldvXpVV65cybKnbb169czHxYsXV1BQkM5k1sAHAACA+7hyU9m8inUDj+uKzOHh4fLw8NCJEydyfM+G67cpSYcPHzYLyUeOHLEr/iYkJOjMmTNm4fb65QAAAPmJ4REsqvh1DeunnnpKy5Yt05QpU7Rp0ybt3LlTdevWNW+e4Iy3t7fdc5vNZt4AAgAAAMjK1atXlZCQYE6JiYm5vo/Q0FD9/fff5vOwsDB17dpVw4YN07lz5yRJp06d0rJlyxyuf+TIEW3cuFGJiYlKTk7Wp59+qi+++EJdu3a1i5s8ebKio6N14sQJTZ06Vb179zaXeXh4aOzYsbpy5Yr27t2ruXPn2i0HAADITxRtC4jvv/9e/fr10/3336+6desqLCxMh/LhZ24AAAAo2p5++mkVK1bMnGrWrJnr+xgxYoS+/fZbhYSE6J577pEkLVy4UCEhIWrSpImCgoLUunVrbd++3eH6cXFxGj58uEqVKqUyZcro1Vdf1aeffqrmzZvbxd13332qX7++6tSpo2bNmunZZ581lwUGBqp+/fqqWrWq2rRpoz59+qhv3765fqwAAADZwfAIBUT16tX1+eefq0uXLrLZbBo3bhw9ZgEAAJCnoqKinC5L34GgX79+6tevn91ywzDsni9cuNB83K5dO0VHR5vPmzVrpj179tjFBwYGaubMmZo5c2aWedauXVs7d+7MMu7BBx/UwIEDnS5/4okn9MQTT2S5HQAAgLxGT9sCYubMmSpRooRuvfVWdenSRR06dFDDhg3dnRYAAAAAAACAXFZoe9q+/p827k4hW67vldCuXbsMvRIkKSIiQt99953dvKFDh9o9v364BEfbSd+jAQAAAAAAAID1FNqiLQAAAABIaR0gHHVouOb64RoAALCSi6+MdncKLinx9MvuTqFQYHgEAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAgTx05ckQBAQGKiYlxdyoAAAAFgpe7EwAAAACQ5uIro/N8HyWefjlbcQEBAebjK1euyMvLS97e3pKk1q1ba/Xq1dneZ6VKlRQXF+daopKmTJmiKVOmSJJSUlKUkJCg4sWLm8tXr16t1q1bu7TNiRMnaufOnVq+fLnL+QAAAOQXetoCAAAAyCAuLs6cWrdurZdfftl8nr5ge/XqVRmGkSc5PPvss3b7DA4OzpAXAABAYUTRFgAAAIBLbDab3njjDdWpU0fFixdXXFycZs6cqerVqyswMFDVqlXTG2+8YcYfOnRINptN0dHRkqR+/fpp4MCBevjhhxUYGKiaNWsqKirKpRySk5M1fvx4VatWTaVKldK9996rEydOSJIMw9Do0aMVFhamoKAg1ahRQ1999ZWWL1+uKVOm6KuvvlJAQIBdb+L0EhMTNWjQIJUsWVJVqlTRe++9J5vNpkOHDkmSvvnmGzVu3FjBwcEqV66chgwZoitXrpjrR0RE6KWXXlLDhg0VFBSkDh06mLkBAABkR6EdHmHJonP5ur8He5XOdqzNZst0+YQJEzRx4sQc5WGz2bRs2TJ17do1R+sDAAAA2bFo0SJ98803KlWqlLy9vVW5cmV99913qlixoqKiotS5c2c1aNBALVu2dLj+4sWLtWLFCn300UeaOnWq+vXrZxZFs+O5557T9u3btXnzZpUqVUrPPvusHn74YW3cuFFr167VokWL9Msvv6h8+fI6cuSIEhISVKNGDT377LNZDo/w4osvatu2bfr999/l7++v3r172y0vVqyY3nnnHdWrV0+HDx/W3XffrZkzZ+q5554zY959912tXr1alSpV0uDBg/XII4/ou+++y/bxAQCAoo2etm5w8uRJc5o9e7aCgoLs5j311FPuThEAAADI1DPPPKPy5cvL19dXHh4e6tatm8LDw2Wz2XTbbbepQ4cOmfae7dy5s9q1aydPT0/1799fhw8f1vnz57O1b8Mw9Oabb2rmzJkqV66cfHx89OKLL+r777/X0aNH5e3trYSEBP3+++9KTk5WpUqVVKNGjWwf26JFizRmzBiVK1dOwcHBmjBhgt3y1q1bq0GDBvL09FTVqlX1+OOPZzjWwYMHq1atWvL399f06dO1fv16HTt2LNs5AACAoq3Q9rS1srCwMPNxcHCwbDab3bx3331XM2bM0MGDBxUREaHhw4dryJAhkqSkpCSNGjVKS5cu1cWLFxUaGqpBgwZp7NixioiIkCTdf//9kqTKlSu71FsBAIDCbHGvdu5OIdt6LIpydwpAlipVqmT3/KOPPtKMGTN06NAhpaam6vLly6pSpYrT9dO3f6/dXOzSpUv6448/1KlTJ3OZoxuYnTt3TvHx8WrTpo3dr9h8fHx09OhR3XbbbZo0aZLGjRunPXv2qH379nr11VczzSe9EydOKDw83Omx/vzzzxo7dqx+/fVXXblyRVevXlXNmjXtYipXrmw+Dg0Nla+vr44fP66KFStmKwcAAFC00dPWYj766CONHz9eL730kvbs2aMpU6Zo3Lhxev/99yVJr7/+ulasWKFPP/1Ue/fu1UcffWQWa3/++WdJ0oIFC3Ty5EnzOQAAAJDbPDz++VPiyJEj6tu3r6ZPn64zZ84oOjpanTt3ztENylq3bm13szFHSpUqJX9/f/3000+Kjo42pytXrujWW2+VJA0ZMkQ//vijjhw5Il9fXw0fPjxD3s6UL19eR48etTu+9Hr27KnbbrtNBw4cUGxsrKZMmZLhWA8fPmw+PnPmjBITE1WhQoXsvQgAAKDIo2hrMRMmTNCMGTP0wAMPqEqVKnrggQc0cuRIvf3225LSGozVq1dXq1atVLlyZbVq1Uo9e/aUJJUpU0aSFBISorCwMPM5AAAAkJfi4uJkGIbKli0rDw8PrVq1St98802e7c/Dw0ODBg3S//3f/5nF1fPnz2vx4sWS0joz/PDDD0pKSlKxYsVUvHhxeXml/cgwNDRUhw8f1tWrV51uv2fPnpo+fbpOnTqlmJgYvfDCC3bLY2NjFRISouLFi2vPnj166623Mmzj7bff1t69e3XlyhWNHj1abdq0oZctAADINoq2FhIfH6+///5bAwYMMO9mGxAQoBdffFF///23pLQ77e7cuVM1a9bU8OHD87QxDAAAAGRH7dq19dxzz+n2229XqVKltHjxYt177715us+pU6eqRYsWuv322xUYGKhGjRqZbePY2FgNGTJEpUqVUlhYmE6cOKHXXntNkvTggw8qKChIZcqUUUhIiMNtP//884qMjFTt2rVVv359de7cWZLk6+srKa0g++qrryogIECDBg3Sww8/nGEb//rXv9SzZ0+Fhobq+PHj+uijj/LgVQAAAIWVzcjJb5byWWxsrIKDgxUTE6OgoCC7ZQkJCTp48KCqVKkiPz8/c/6SRefyNccHe5XO0XoLFy7UiBEjFB0drdOnTyssLEz//e9/1axZM7s4T09Pcwyu2NhYrV69Wt9++62WLFmi9u3b67PPPpMk2Ww2LVu2TF27dnU5F2evJQAAhQFj2sIdnLVjaXcVLFu2bFG7du2UkJBgN4auMxEREZo9e3a22uS8FwAAWbn4ymh3p+CSEk+/7O4UCgVuRGYhoaGhKl++vA4cOKDevXs7jQsKClKPHj3Uo0cPde/eXR07dtSFCxdUsmRJeXt7KyUlJR+zBgAAAAqXM2fO6Pfff1ebNm10+vRpjR07Vt26dctWwRYAACA3ULS1mEmTJmn48OEKDg5Wx44dlZiYqG3btunixYsaNWqUZs6cqXLlyqlBgwby8PDQkiVLFBYWZv60KyIiQuvWrVPLli3l6+urEiVKuPeAAAAAgAImJSVFI0eO1P79++Xv768777xTr7/+urvTAgAARQhFW4t57LHH5O/vr1deeUVPP/20ihcvrrp162rEiBGSpMDAQE2fPl1//fWXPD091aRJE61atcq8C+6MGTM0atQovfPOO6pQoYIOHTrkvoMBAAAACqBy5cpp586dOV6fNjgAALhRhXZMW7iO1xIAUJgxpi3cgTFtkRXeCwCArDCmbdHk4e4EAAAAAAAAAAD/oGgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQL3cnkFsMw3B3CgVeamqqu1MAAAAo0mbui8rzfYyq0c6l+M2bN+ull17Sjz/+KMMwVLlyZfXu3VuDBg1SRESE/vOf/6h79+5266xdu1b333+/Tp48qcDAQB05ckS1a9c2l1++fFm+vr7y9PSUJD3yyCOaN2+eS3kdOnRIVapU0cWLFxUSEuLSugAAAFZX4Iu23t7estlsOnv2rMqUKSObzebulAocwzCUlJSks2fPysPDQz4+Pu5OCQAAABbw1VdfqWfPnnrhhRf04YcfqnTp0vrzzz81bdo0Xbx4Ub1799b8+fMzFG3nz5+vhx56SIGBgZKkSpUqKS4uzlweERGh2bNnq2vXrvl5OAAAAAVGgS/aenp6qmLFijp27JgOHTrk7nQKNH9/f1WqVEkeHoyaAQAAUNQZhqHhw4dr9OjRGjFihDm/Vq1aWrhwoSRpwIABaty4sU6cOKHy5ctLkqKjo7V8+XJ9++232d7Xt99+q2effVb79u1ThQoVNHXqVN17772S0nrt/t///Z8OHjwof39/PfDAA3rrrbfUtGlTSVLFihUlSW+//bZ69+6dYdufffaZxowZo7Nnz+qhhx7SyZMn1bhxY02cOFFxcXHq3bu3tmzZosTEREVGRmrOnDmKjIyUJE2cOFHbtm1TaGiolixZotDQUE2fPl3333+/y68nAACAKwp80VaSAgICVL16dSUnJ7s7lQLL09NTXl5e9FQGAACAJOmvv/7SwYMH1bNnT6cx9evXV2RkpN5//32NHTtWkrRo0SJVrlxZLVu2zNZ+du/erQcffFBLly5Vu3bt9MMPP+juu+/W1q1bVbNmTfXt21cvv/yyHn30UcXHx2vXrl2SpK1bt6pKlSo6duyY0+ER9u3bp0cffVTLli1T+/bttWDBAg0ZMkSNGzeWlDY8WK9evbRo0SJ5enpq9OjReuihh/Tnn3+a7eI1a9Zo7ty5evvtt7V69Wo9+OCD+v3331WtWrXsvpQAAAAuKxRFWymt6HhtTCwAAAAAN+bs2bOSpAoVKmQaN2DAAM2ePdss2s6fP18DBgzI9n7efvtt9evXT7fffrskqVWrVrrnnnv06aefaty4cfL29tb+/fvN4dBuvfXWbG978eLFuuOOO9SxY0dJ0sCBAzV79mxzeVBQkHr06GE+nzRpkl5//XWdOHHCPO4aNWro8ccflyR16dJFt912mz7++GM9//zz2c4DAADAVfwOHgAAAEAGpUuXliQdP34807hevXrp6NGj2rx5s3799Vft2rVLffr0kSTdcsstCggIUEBAgD766COH6x86dEjz5s1TSEiIOX3xxRc6ceKEJGnZsmX67bffVLNmTTVo0ECffvppto/hxIkTCg8Pt5tXqVIl8/GVK1c0ZMgQRUREKCgoSBEREZKkc+fOmTGVK1e2W79y5cpZviYAAAA3qtD0tAUAAACQe2rUqKGIiAh98skneu6555zGhYSEqFu3bpo/f76Cg4N1zz33KDQ0VJL0+++/Z7mf8PBwPfnkk5o2bZrD5Q0bNtTSpUuVmpqq5cuX66GHHlLbtm2zdR+G8uXL66effrKbd+TIETVr1kySNGPGDG3fvl2bN29WxYoVFR0drRIlSsgwDDP+8OHDGdZ3pbcvAABATtDTFgAAAEAGNptNc+bM0bRp0zRnzhydP39eUto4sQMGDLArZg4YMEBLlizRf//7X5eGRpCkxx9/XAsWLND69euVkpKixMREbdmyRXv27FFSUpI+/PBDXbx4UR4eHubYtV5eXipTpow8PDz0999/O932Qw89pG+//VbffPONrl69qvnz52vfvn3m8tjYWPn5+alEiRKKi4vTs88+m2Eb+/bt0zvvvKOrV69q5cqV+u677+yGVAAAAMgLFG0BAAAAOHTPPfdo9erVWrlypapVq6aQkBB1795dtWrVUrly5cy4du3aKTQ0VD4+PurUqZNL+2jQoIE5RmyZMmVUoUIFjRs3TomJiZLSbmx20003KTAwUE888YQWLVqkUqVKqVixYpowYYI6deqkkJAQLVq0KMO2a9asqffff1+DBw9WqVKltGXLFt1+++3y9fWVJI0aNUqenp4KDQ1VnTp11KJFiwzb6Nixo3788UeVLFlSTz75pP773/+qevXqLh0jAACAq2xG+t/+WFRsbKyCg4MVExOjoKAgd6cDAAAKoMW92rk7hWzrsSjK3SkglzhrxyYkJOjgwYOqUqWK/Pz83Jhh0VOzZk2NHz9evXv3zjJ24sSJ2rlzp5YvX55n+fBeAABk5eIro92dgktKPP2yu1MoFOhpCwAAAKDQ+vLLL3Xp0iUlJiZqxowZOnnypDp27OjutAAAADLFjcgAAAAAFFpff/21+vbtq+TkZNWsWVMrVqxQqVKl3J0WAABApijaAgAAACi03njjDb3xxhs5WnfixIm5mwwAAEA2MTwCAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAICbpKamujsFuJlhGO5OAQAAWBBj2gIAAAD5zMfHRx4eHjpx4oTKlCkjHx8f2Ww2d6eFfGYYhs6ePSubzSZvb293pwMAACyEoi0AAACQzzw8PFSlShWdPHlSJ06ccHc6cCObzaaKFSvK09PT3akAAAALoWgLAAAAuIGPj48qVaqkq1evKiUlxd3pwE28vb0p2AIAgAwo2gIAAABucu1n8fw0HgAAAOlxIzIAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIXkqGg7d+5cRUREyM/PT82aNdPWrVudxiYnJ2vy5MmqVq2a/Pz8FBkZqTVr1uQ4YQAAAAAAAAAozFwu2i5evFijRo3ShAkT9MsvvygyMlIdOnTQmTNnHMY///zzevvttzVnzhz98ccfGjRokO6//37t2LHjhpMHAAAAAAAAgMLG5aLtzJkzNXDgQPXv31+1a9fWvHnz5O/vr/nz5zuM//DDD/Xss8+qc+fOqlq1qgYPHqzOnTtrxowZN5w8AAAAAAAAABQ2LhVtk5KStH37drVv3/6fDXh4qH379tqyZYvDdRITE+Xn52c3r1ixYtq8eXMO0gUAAAByx9SpU9WkSRMFBgaqbNmy6tq1q/bu3WsXk5CQoKFDh6pUqVIKCAhQt27ddPr0aTdlDAAAgKLCpaLtuXPnlJKSotDQULv5oaGhOnXqlMN1OnTooJkzZ+qvv/5Samqq1q5dq88//1wnT550up/ExETFxsbaTQAAAEBu2rBhg4YOHaoff/xRa9euVXJysu666y7Fx8ebMSNHjtSXX36pJUuWaMOGDTpx4oQeeOABN2YNAACAosArr3fw2muvaeDAgapVq5ZsNpuqVaum/v37Ox1OQUrr9TBp0qS8Tg0AAABF2PU3x124cKHKli2r7du3q02bNoqJidF7772nRYsW6fbbb5ckLViwQDfffLN+/PFHNW/e3B1pAwAAoAhwqWhbunRpeXp6ZvhJ2OnTpxUWFuZwnTJlymj58uVKSEjQ+fPnVb58eY0ZM0ZVq1Z1up+xY8dq1KhR5vPY2FiFh4e7kiqQY3NePeruFLLtiaf4XAAAkFtiYmIkSSVLlpQkbd++XcnJyXZDg9WqVUuVKlXSli1bKNoCAAAgz7g0PIKPj48aNWqkdevWmfNSU1O1bt06tWjRItN1/fz8VKFCBV29elVLly7Vfffd5zTW19dXQUFBdhMAAACQV1JTUzVixAi1bNlSderUkSSdOnVKPj4+CgkJsYvNbGgwhvkCAABAbnCpaCtJo0aN0jvvvKP3339fe/bs0eDBgxUfH6/+/ftLkvr06aOxY8ea8T/99JM+//xzHThwQJs2bVLHjh2VmpqqZ555JveOAgAAALgBQ4cO1W+//aZPPvnkhrYzdepUBQcHmxO/FgMAAEBOuDymbY8ePXT27FmNHz9ep06dUv369bVmzRrz5mRHjhyRh8c/teCEhAQ9//zzOnDggAICAtS5c2d9+OGHGXosAAAAAO4wbNgwffXVV9q4caMqVqxozg8LC1NSUpKio6Pt2q6ZDQ3GMF8AAADIDTm6EdmwYcM0bNgwh8uioqLsnrdt21Z//PFHTnYDAAAA5BnDMPTEE09o2bJlioqKUpUqVeyWN2rUSN7e3lq3bp26desmSdq7d6+OHDnidGgwX19f+fr65nnuAAAAKNxyVLQFAAAACrqhQ4dq0aJF+uKLLxQYGGiOUxscHKxixYopODhYAwYM0KhRo1SyZEkFBQXpiSeeUIsWLbgJGQAAAPIURVsAAAAUSW+99ZYkqV27dnbzFyxYoH79+kmSZs2aJQ8PD3Xr1k2JiYnq0KGD3nzzzXzOFAAAAEUNRVsAAAAUSYZhZBnj5+enuXPnau7cufmQEQAAAJDGI+sQAAAAAAAAAEB+oWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCFe7k7Ayob/e6O7U3DJ6/9p4+4UgAKp8Usj3J2CS7Y9N9vdKQAAAAAAgDxET1sAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBAvdycAAEB+e+bXL92dgkum1+3i7hQAAAAAAPmInrYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsJAcFW3nzp2riIgI+fn5qVmzZtq6dWum8bNnz1bNmjVVrFgxhYeHa+TIkUpISMhRwgAAAAAAAABQmLlctF28eLFGjRqlCRMm6JdfflFkZKQ6dOigM2fOOIxftGiRxowZowkTJmjPnj167733tHjxYj377LM3nDwAAAAAAAAAFDYuF21nzpypgQMHqn///qpdu7bmzZsnf39/zZ8/32H8Dz/8oJYtW6pXr16KiIjQXXfdpZ49e2bZOxcAAAAAAAAAiiKXirZJSUnavn272rdv/88GPDzUvn17bdmyxeE6t956q7Zv324WaQ8cOKBVq1apc+fON5A2AAAAcGM2btyoLl26qHz58rLZbFq+fLnd8n79+slms9lNHTt2dE+yAAAAKFK8XAk+d+6cUlJSFBoaajc/NDRUf/75p8N1evXqpXPnzqlVq1YyDENXr17VoEGDMh0eITExUYmJiebz2NhYV9IEAAAAshQfH6/IyEj961//0gMPPOAwpmPHjlqwYIH53NfXN7/SAwAAQBHmUtE2J6KiojRlyhS9+eabatasmfbv368nn3xSL7zwgsaNG+dwnalTp2rSpEl5nRoAAACKsE6dOqlTp06Zxvj6+iosLCyfMgIAAADSuDQ8QunSpeXp6anTp0/bzT99+rTTxuy4ceP06KOP6rHHHlPdunV1//33a8qUKZo6dapSU1MdrjN27FjFxMSY09GjR11JEwAAAMgVUVFRKlu2rGrWrKnBgwfr/PnzmcYnJiYqNjbWbgIAAABc5VLR1sfHR40aNdK6devMeampqVq3bp1atGjhcJ3Lly/Lw8N+N56enpIkwzAcruPr66ugoCC7CQAAAMhPHTt21AcffKB169bp5Zdf1oYNG9SpUyelpKQ4XWfq1KkKDg42p/Dw8HzMGAAAAIWFy8MjjBo1Sn379lXjxo3VtGlTzZ49W/Hx8erfv78kqU+fPqpQoYKmTp0qSerSpYtmzpypBg0amMMjjBs3Tl26dDGLtwAAAIDVPPzww+bjunXrql69eqpWrZqioqJ0xx13OFxn7NixGjVqlPk8NjaWwi0AAABc5nLRtkePHjp79qzGjx+vU6dOqX79+lqzZo15c7IjR47Y9ax9/vnnZbPZ9Pzzz+v48eMqU6aMunTpopdeein3jgIAAADIY1WrVlXp0qW1f/9+p0VbX19fblYGAACAG5ajG5ENGzZMw4YNc7gsKirKfgdeXpowYYImTJiQk10BAAAAlnDs2DGdP39e5cqVc3cqAAAAKORyVLQFAAAACrq4uDjt37/ffH7w4EHt3LlTJUuWVMmSJTVp0iR169ZNYWFh+vvvv/XMM8/opptuUocOHdyYNQAAAIoCirYAAAAokrZt26bbbrvNfH5tLNq+ffvqrbfe0u7du/X+++8rOjpa5cuX11133aUXXniB4Q8AAACQ5yjaAgAAoEhq166dDMNwuvzrr7/Ox2wAAACAf3hkHQIAAAAAAAAAyC8UbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC/FydwIAAAAAgBt38ZXR7k7BJSWeftndKQAAYFn0tAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBAvdycAIOc2RV1ydwouad0u0N0pAAAAAAAAWB49bQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFsKNyAqRJYvOuTsFlzzYq7S7UwAKpGd+/dLdKbhket0u7k4BAAAAAIAChZ62AAAAAAAAAGAhBaqnbWpqqlJTUx0u8/DwsIvLTPZjU2Vf1858u+6ONYxU2Wweds8z4+7Y1NRU2Ww22Wy2/8UaMgwjk+3mV2xmx2cfKznfbn7FGpnE2iwRa/+eyL3PZ+7F2q5L37A5X3Y9t8SmXjfDw+Z82fXSxxpG5m+1XIq9/vV25bzl1edehiH9LzbLY0t7E7s11jAMC14rc+lcKP8+94ZsTiLtlziPS2NLd7LyKtaK10qrxwIAAACFSYEq2u7atUsBAQEZ5gcHB+umm26yi3PWyA8ICFDNmjXN57/++quuXr3qMNbH77ySEsr887zYWdlsKQ5jjVQvJSWUTbfuOdk8HG/XMDyVdCXUbj82j2QnsR5KuhJmPvf2uyAPjySHsdGxHioRXNt8Hn/5qJKvxjmMlaQSwbf8E3vluJKTY53GhgTdbBbeLl85qaTkaKexwYE15eGR9ta6knBKiUkXHcbt2BGgOnXqyNfXV5J0/PhxnT592ul2a9eurWLFikmSTp48qZMnTzqNrVWrlooXLy5JOn36tI4fP+40tkaNGgoMDJQknT17VolX9zmN9fasKE9b2nsw1YhVcorzHLw9y8vTFvS/2EtKTjmRSWw5edqC/xcbr+SUY05jvTxD5WUrIUlKSorXufMHnMYGBZVTYEDaezg5+YrOntvvNDYwMFRBgWnvy6tXE3XmrPPXISCgtIKDykuSUlKSdfrMn05jixcvpZDgCv/b7lXt2rXLaWypUqUUEREhKe0P9R07djiNLVGihKpWrWo+zyw2q2tEuOFjPk6UodO2fz6PFQwfpz9JSJKhU+liyxve8nRSlEmWoZPpYsMMb3k7iU2RoePpYkMNb/mki/U6fP6fYA+brlYuZT71PB0jW4Lja49s0tWI0uliY2W74vjaI0lXq/wT63H2kjziHV97JKXl8L/Crce5OHnEJZrLdly1PzeRkZHy8kq7Rhw7dkxnz551ut28ukbIuCr5eqflG3NFHhcvOw1NCQuSUSztPWK7lCDP8/HOY0ODZPj/LzYuUZ7nnF+DU8oGyiiedmy2y0nyPHPJaeyFihdUqlTaeY6NjdX+/c4/y+Hh4SpbNu37KC4uTvv2Of8sV6hQQWFhad8xly9f1p9/Ov8slytXTuXLp33uExIS9McffziNDQ0NVcWKFSVJSUlJ+u2335zGlilTRpUqVZKU99eImOAwh7FeyYkKuHzBfB4TFPpPQf06nleTFBj/z2cwNqisDJvjq4RnSrIC4/4Zuig2sIwMD0+HsR4pVxUU989nYc+ePUpISHAY6+Pjo7p165rP9+7dq8uXHb+Hvby8FBkZaT7/66+/FBfn+H3p4eGhBg0amM8PHDigmJgYh7GS1KhRI/PxoUOHdPGi4+97SWrQoIFZYD1y5IjOnz/vNNaVa0T6HAAAAIDChO4JAAAAAAAAAGAhNiOr3yxaQGxsrIKDg3Xx4kUFBQU5jMmLn92NGLRR7h7ywJXYVm1ru33IA1diu/csbYmf5l4fO+fVI05jrTY8QmSj4AI1PELrdoGW+Ant9bHNpoyye2714RFu63K7/QyLD48wre499qEWGB5hzG9fuX3IA1diX67XxXLXyoI4PMKnj9zhJNJ6wyM8+N/vMo212tAEVoi1qmvt2JiYGKftWCC3XHxltLtTcEmJp192dwoAUCBwfS+aCtTwCB4eHtlqnLvSgM889vplrvxhkP+xtut+nnn988y4I/b61z79H/lZbzcvY7N3fGnbzP528yrWVoBipdz8fOZerJFJ+pktc1usRyYrZ7bsejZbdt9qNxSb2blx5bzl6uc+/bJ8eh1uJDb9sVjnWpn7sVLefu5tmVbR/5HduLyMteK10uqxAAAAQGFCSxgAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAvxcncCAAAAAAAAyNrFV0a7OwWXlHj6ZXenABRY9LQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhXu5OAAAAAPbO/V8vd6fgktIzFrk7BQAAAKBQoactAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACzEy90JAAAAAMhdF18Z7e4UXFLi6ZfdnQJQIPFZB4DCi562AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAvJUdF27ty5ioiIkJ+fn5o1a6atW7c6jW3Xrp1sNluG6e67785x0gAAAAAAAABQWLlctF28eLFGjRqlCRMm6JdfflFkZKQ6dOigM2fOOIz//PPPdfLkSXP67bff5OnpqQcffPCGkwcAAAByauPGjerSpYvKly8vm82m5cuX2y03DEPjx49XuXLlVKxYMbVv315//fWXe5IFAABAkeJy0XbmzJkaOHCg+vfvr9q1a2vevHny9/fX/PnzHcaXLFlSYWFh5rR27Vr5+/tTtAUAAIBbxcfHKzIyUnPnznW4fPr06Xr99dc1b948/fTTTypevLg6dOighISEfM4UAAAARY2XK8FJSUnavn27xo4da87z8PBQ+/bttWXLlmxt47333tPDDz+s4sWLO41JTExUYmKi+Tw2NtaVNAEAAIAsderUSZ06dXK4zDAMzZ49W88//7zuu+8+SdIHH3yg0NBQLV++XA8//HB+pgoAAIAixqWetufOnVNKSopCQ0Pt5oeGhurUqVNZrr9161b99ttveuyxxzKNmzp1qoKDg80pPDzclTQBAACAG3Lw4EGdOnVK7du3N+cFBwerWbNm2e6sAAAAAORUjm5EllPvvfee6tatq6ZNm2YaN3bsWMXExJjT0aNH8ylDAAAAQGaHBFc7KyQmJio2NtZuAgAAAFzlUtG2dOnS8vT01OnTp+3mnz59WmFhYZmuGx8fr08++UQDBgzIcj++vr4KCgqymwAAAACr4xdjAAAAyA0uFW19fHzUqFEjrVu3zpyXmpqqdevWqUWLFpmuu2TJEiUmJuqRRx7JWaYAAABAPrnWIcHVzgr8YgwAAAC5weXhEUaNGqV33nlH77//vvbs2aPBgwcrPj5e/fv3lyT16dPH7kZl17z33nvq2rWrSpUqdeNZAwAAAHmoSpUqCgsLs+usEBsbq59++inTzgr8YgwAAAC5wcvVFXr06KGzZ89q/PjxOnXqlOrXr681a9aY430dOXJEHh72teC9e/dq8+bN+uabb3InawAAAOAGxcXFaf/+/ebzgwcPaufOnSpZsqQqVaqkESNG6MUXX1T16tVVpUoVjRs3TuXLl1fXrl3dlzQAAACKBJeLtpI0bNgwDRs2zOGyqKioDPNq1qwpwzBysisAAAAgT2zbtk233Xab+XzUqFGSpL59+2rhwoV65plnFB8fr3//+9+Kjo5Wq1attGbNGvn5+bkrZQAAABQROSraAgAAAAVdu3btMu1YYLPZNHnyZE2ePDkfswIAAAAo2gIAYHmfn/jV3Sm45IHydd2dAgAAAAAUaC7fiAwAAAAAAAAAkHco2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIXkqGg7d+5cRUREyM/PT82aNdPWrVszjY+OjtbQoUNVrlw5+fr6qkaNGlq1alWOEgYAAAAAAACAwszlou3ixYs1atQoTZgwQb/88osiIyPVoUMHnTlzxmF8UlKS7rzzTh06dEifffaZ9u7dq3feeUcVKlS44eQBAACAvDRx4kTZbDa7qVatWu5OCwAAAIWcl6srzJw5UwMHDlT//v0lSfPmzdPKlSs1f/58jRkzJkP8/PnzdeHCBf3www/y9vaWJEVERNxY1gAAAEA+ueWWW/Ttt9+az728XG5CIwsz90W5OwWXjKrRzt0pAAUSn3UAyD6XWpxJSUnavn27xo4da87z8PBQ+/bttWXLFofrrFixQi1atNDQoUP1xRdfqEyZMurVq5dGjx4tT09Ph+skJiYqMTHRfB4bG+tKmgAAAECu8fLyUlhY2I1tJD5ectT29fSU/Pzs45zx8JCKFctebHKy9L8OE5KkpGRJhpNgm+STw9jkZMlwFivJxydbsV6Xr+iq/z/H5pmYJFtKitPNuhRbzE+y2dJik5Jku5oLsfHxaefC438/XExKSjs+Z1yJ9fP7573iSmxyclq8M55ekuf/ckhJSZuyFZsqpVzNJNbznxxciU1Nzfw97O39z/snJUVKSMhebGqqdOVK7sR6eUm+vmmPDUO6fDl3Yl353GcVm/6c22zXfe4zeT9kiM2fa4TXZfvXO/1n2SshMe2cOOGWa8T1r7e/vxmrxETpaibv97y6RqSm/hN7NUVKzeSz7OX1T6xLn/tcvEak/z66ejXtdXPGx+efWFc+90XhGuHs82zzkLzTlfZc+tzn4TXC2XXNZkv7HF1z5Uqmn3sVL56z2ISEzN/DrsS68rl3JTZ9Dk64VLQ9d+6cUlJSFBoaajc/NDRUf/75p8N1Dhw4oO+++069e/fWqlWrtH//fg0ZMkTJycmaMGGCw3WmTp2qSZMmuZIaAAAAkCf++usvlS9fXn5+fmrRooWmTp2qSpUqOYx12vmgfHnHG+/cWVq58p/nZcs6/0OubVspKuqf5xER0rlzDkMDK4bp0vA+5vOgGe/J86LjjhApoaUU+38D/omd84E8T593HFsiSLFjB/2zn7c+ltexUw5jU4sXU8yEJ8znAe99Ju8DRx3GDn5pnubsXG0+7/LEeFXd8JPDWEmauXe9+bjT01NU4+sNTmNf37HKLOC0Hz9Ttyz72mnsW1uW6UrJEElS26lvqv6iL5zG6uDBtHMgSc89J736qvPY336Tbrkl7fGUKVJmf+ts3So1aZL2+LXXpGeecR67fr3Url3a4//8RyWen+009FL/brp6czVJks+OP1T809VOY+MeuVfJ9dKGAfH+fZ8C/rvCaWz8Q52U1LiuJMlr30EFLljqNPZy1/ZKvLVhWuzBY1JAgNNYTZ8uPf102uNffpGaNnUeO2GCNHFi2uM9e6Q6dZzHPvWU9MoraY+PHJGqVHEeO2SINHdu2uNz59I+n8707SstXJj2+PLlzI+te3dpyZJ/nmcWm8U1okS60OSq4Yob1NN8Hjz1bXnEOy44XXXTNWJ4uvfo5RLBmvfjcvP5/QNHK3zrLofbTS7mZ4lrhM6ckcqUSXs8apT05pvOY/PoGuH5xKNKCS8nSfLdvE3+q5wf26XHH9bVamnfV74/7ZL/8m+dx+bRNUIVIqV+/dIef/21dM89zmPfeEMaOjTt8aZN0m23OY8tYteIEk5Ck2tVVdy/upvPQybNlc1J0T8/rxF6fpbjhCtXlg4d+ud5mzbStm2OY0uXls6e/ed5p07SBifvd39/+0Jxt25SZvfSSv9P5EcflT77zHlsXNw/BdbHH5fef995rCvXiMz+6f0/OboRmStSU1NVtmxZ/ec//1GjRo3Uo0cPPffcc5o3b57TdcaOHauYmBhzOnrUceMOAAAAyEvNmjXTwoULtWbNGr311ls6ePCgWrdurUuXLjmMnzp1qoKDg80pPDw8nzMGAABAYWAzjGyUdv8nKSlJ/v7++uyzz9S1a1dzft++fRUdHa0vvsj4n+i2bdvK29vbbhyw1atXq3PnzkpMTJRP+p9MOREbG6vg4GDFxMQoKCgou+nesOH/3phv+8oNrdvVdncKLnmwV2l3p+DQnFcLzj8J6jcOcXcKLmndLtDdKTjU+KUR7k7BJbffe4e7U3DJ9Lpd3J1CBs/8+qW7U3BJ81IR7k7BJQ+Ur+vuFBxa3Kudu1PItjvKOemVaVGlZyxydwr5Jjo6WpUrV9bMmTM1YMCADMsd9bQNDw9XzIkTjtuxeTQ8wsXXxhWo4RE+vPuuAjU8wvDqbSw5PMLF6Zn0yrXg8Aglhk10HlvUfvqcw9iLs5//50kBGB7hw7vvsou2+vAIw6u3sQ+2wPAIF9+YWKCGRyjxzHSGR3A11sHn3u6znp5Fh0coMeIF59tleISMOTjh0vAIPj4+atSokdatW2cWbVNTU7Vu3ToNGzbM4TotW7bUokWLlJqaKo//XSz27duncuXKZatgCwAAAFhFSEiIatSoof379ztc7uvrK99rf5SlV7x4thrn2YrJTmz6P7Qk+z+msuJK7PX7yWFs+gKLJKX4Zv/vBJdifXykbIZnGnv9a+/jY1+gzkxexaYvNGQlffE0y1gPyTO723Uh1sMj++93T8/sx7qyXVdibba8iZVuLDazc+7K39v5dI24/rNut8zPwbXTiXy7RmR2bnx9/ynCZSU3P/ce6X4w7eUpKbufZVc+97l4jUj/HvDySpuymwPXiDTFi7v2/smuvLxGZPf4ijm/JtxQbPqid27GuvK5dyXWCZeHRxg1apTeeecdvf/++9qzZ48GDx6s+Ph49e/fX5LUp08fuxuVDR48WBcuXNCTTz6pffv2aeXKlZoyZYqGXhunBAAAACgg4uLi9Pfff6tcuXLuTgUAAACFmEs9bSWpR48eOnv2rMaPH69Tp06pfv36WrNmjXlzsiNHjpg9aiUpPDxcX3/9tUaOHKl69eqpQoUKevLJJzV69OjcOwoAAAAgDzz11FPq0qWLKleurBMnTmjChAny9PRUz549s14ZAAAAyCGXi7aSNGzYMKfDIUSlv6Pt/7Ro0UI//vhjTnYFAAAAuM2xY8fUs2dPnT9/XmXKlFGrVq30448/qsy1OwMDyLGZ+6LcnYJLRtVo5+4UAABFSI6KtgAAAEBR8Mknn7g7BQAAABRBLo9pCwAAAAAAAADIOxRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABbi5e4EAAAAAAAAABQOM/dFuTsFl4yq0c7dKThET1sAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABbi5e4EAAAAAAAAUPjM3Bfl7hRcMqpGO3enAJjoaQsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEIo2gIAAAAAAACAhVC0BQAAAAAAAAALoWgLAAAAAAAAABZC0RYAAAAAAAAALISiLQAAAAAAAABYCEVbAAAAAAAAALAQirYAAAAAAAAAYCEUbQEAAAAAAADAQijaAgAAAAAAAICFULQFAAAAAAAAAAuhaAsAAAAAAAAAFkLRFgAAAAAAAAAshKItAAAAAAAAAFgIRVsAAAAAAAAAsBCKtgAAAAAAAABgIRRtAQAAAAAAAMBCKNoCAAAAAAAAgIVQtAUAAAAAAAAAC6FoCwAAAAAAAAAWQtEWAAAAAAAAACyEoi0AAAAAAAAAWAhFWwAAAAAAAACwEIq2AAAAAAAAAGAhFG0BAAAAAAAAwEJyVLSdO3euIiIi5Ofnp2bNmmnr1q1OYxcuXCibzWY3+fn55ThhAAAAID+50vYFAAAAcoPLRdvFixdr1KhRmjBhgn755RdFRkaqQ4cOOnPmjNN1goKCdPLkSXM6fPjwDSUNAAAA5IectH0BAACAG+Vy0XbmzJkaOHCg+vfvr9q1a2vevHny9/fX/Pnzna5js9kUFhZmTqGhoTeUNAAAAJAfctL2BQAAAG6US0XbpKQkbd++Xe3bt/9nAx4eat++vbZs2eJ0vbi4OFWuXFnh4eG677779Pvvv+c8YwAAACAf5LTtCwAAANwoL1eCz507p5SUlAw9ZUNDQ/Xnn386XKdmzZqaP3++6tWrp5iYGL366qu69dZb9fvvv6tixYoO10lMTFRiYqL5PCYmRpIUGxvrSro3LCkpPl/3d6MuX77k7hRcEhvr4+4UHLqSUHBex/j4gnUvwdhYw90pOJSSkJh1kIUkxl12dwouye9rd3YUtNfwsk+cu1NwiRXPuSRdTr7q7hSy7VJisrtTcImPG895YGCgbDZbnmw7J21fq7RjYwvYd1tCXMFqd1v1Osd5z1tWPO+c87zFOb9xnPPcwXnPW+4671m2Yw0XHD9+3JBk/PDDD3bzn376aaNp06bZ2kZSUpJRrVo14/nnn3caM2HCBEMSExMTExMTExMTU6ZTTEyMK81Zl+Sk7Us7lomJiYmJiYmJKTtTVu1Yl3rali5dWp6enjp9+rTd/NOnTyssLCxb2/D29laDBg20f/9+pzFjx47VqFGjzOepqam6cOGCSpUqlWc9KQq62NhYhYeH6+jRowoKCnJ3OsgHnPOiifNe9HDOix7OuWsCAwPzbNs5afvSjnUd7/miifNe9HDOix7OedHEec++rNqxLhVtfXx81KhRI61bt05du3aVlNYQXbdunYYNG5atbaSkpOjXX39V586dncb4+vrK19fXbl5ISIgrqRZZQUFBfCiKGM550cR5L3o450UP59z9ctL2pR2bc7zniybOe9HDOS96OOdFE+f9xrlUtJWkUaNGqW/fvmrcuLGaNm2q2bNnKz4+Xv3795ck9enTRxUqVNDUqVMlSZMnT1bz5s110003KTo6Wq+88ooOHz6sxx57LHePBAAAAMhlWbV9AQAAgLzgctG2R48eOnv2rMaPH69Tp06pfv36WrNmjXmDhiNHjsjD45+bI128eFEDBw7UqVOnVKJECTVq1Eg//PCDateunXtHAQAAAOSBrNq+AAAAQF5wuWgrScOGDXP6k7CoqCi757NmzdKsWbNyshu4wNfXVxMmTMjwczwUXpzzoonzXvRwzosezrn1ZNb2xY3jPV80cd6LHs550cM5L5o477nHZhiG4e4kAAAAAAAAAABpPLIOAQAAAAAAAADkF4q2AAAAAAAAAGAhFG0LoIiICO3cudNuXr9+/VShQgXVr19ftWrV0qOPPqrLly+7J0HkOs550RQREaGaNWuqfv36qlmzpqZNmyZJOnTokDw9PVW/fn1zmjdvnpuzRW7gnFubo2vxY489pvXr1+drHvPmzVO9evXM63/v3r0lSZ07d9Ybb7yRIT4yMlKff/65JOnvv/9W9+7dVaVKFTVq1EhNmzbVu+++m6/5o2ijTVM0cd6LHto0RQ/n3NpoxxZQBgqcypUrGzt27LCb17dvX2PWrFmGYRhGQkKCceuttxovv/xy/ieHPME5L5rSn/djx44ZQUFBxk8//WQcPHjQCA4OdmtuyBucc2tzdC3OT8nJycbPP/9sVKlSxTh//rxhGIaRmppqbN++3TAMw/jss8+Mhg0b2q3z888/G2XKlDGSkpKMkydPGmFhYcZ//vMfc/mFCxeMt956K/8OAkUebZqiifNe9NCmKXo459ZGO7ZgoqdtIeTr66tWrVrp8OHD7k4F+YRzXvhVqFBBtWrV4hwXIZzzgqFdu3Zavny5pLReY48//rjuuOMO1ahRQw888ICSkpIkScnJyRozZoyaNm2q+vXr66GHHtLFixclSYsWLVKzZs3UoEEDRUZG6ssvv7Tb/vDhw9WiRQvdddddOnbsmAIDAxUYGChJstlsatiwoSTp3nvv1dGjR7V7925z/fnz56tPnz7y9vbW3Llz1bp1aw0cONBcXqJECQ0aNChPXyPAFbRpiibOe+FGm6bo4ZwXDLRjrY+ibSEUExOjqKgodevWzd2pIJ9wzgu/P//8U+fPn1e7du0kSZcuXbL7idHRo0fdmyByHee8YNq5c6e+/PJL7dmzR6dPn9bSpUslSa+88oqKFy+urVu3aufOnapbt66ef/55SVKHDh30448/aseOHfriiy80cOBAJSYmmtvct2+fNm7cqO+++0533XWXAgMDValSJfXo0UNvvPGG2Wj29vbWo48+qvnz50uSEhIS9PHHH2vAgAGSpO3bt6tFixb5+XIALqNNUzRx3gs32jRFD+e8YKIdaz1e7k4AueeVV17R/PnztW/fPt1999267bbb3J0S8hjnvPDr0aOHPDw8tHfvXs2aNUtlypRRfHy8AgMDM4xJhMKBc16w3X///fL395ckNW3aVH///bckafny5YqJiTEbv0lJSYqIiJAkHTx4UL1799axY8fk5eWlCxcu6ODBg6pVq5Yk6ZFHHpG3t7ckyd/fX5s2bdLOnTu1adMmff7553r55Ze1a9culSxZUgMGDFDbtm01ffp0ff7557r55pt188035/OrALiONk3RxHkv3GjTFD2c84KNdqz10NO2EHn66ae1e/du7du3T9u2bWNw7yKAc174LV68WHv27NE333yjMWPG6Ndff3V3SshjnPOCzc/Pz3zs6empq1evSpIMw9CcOXO0c+dO7dy5U3/88YdWrVolSXr44Yf12GOP6bffftPOnTsVEBCghIQEczsBAQF2+7DZbGrQoIGGDx+udevWKSAgQFFRUZKk2rVr66abbtKXX36p+fPnm70TJKlRo0basmVLXh06cENo0xRNnPfCjTZN0cM5L9hox1oPRdtCqFKlSpozZ44mT56sK1euuDsd5APOeeHXvn17DR482PwZCgo/znnh0rVrV82aNcu8M/rly5f1+++/S5IuXryoKlWqSJL++9//mj8Tc+TPP/+0G+vr6NGjOnv2rKpWrWrOGzBggKZMmaKtW7eqR48e5vwhQ4Zow4YNWrBggTkvOjpa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",
|
| 640 |
+
"text/plain": [
|
| 641 |
+
"<Figure size 1400x600 with 2 Axes>"
|
| 642 |
+
]
|
| 643 |
+
},
|
| 644 |
+
"metadata": {},
|
| 645 |
+
"output_type": "display_data"
|
| 646 |
+
}
|
| 647 |
+
],
|
| 648 |
+
"source": [
|
| 649 |
+
"fig, axes = plt.subplots(1, 2, figsize=(14, 6))\n",
|
| 650 |
+
"\n",
|
| 651 |
+
"models_list = list(comp_df.index)\n",
|
| 652 |
+
"x = np.arange(len(models_list))\n",
|
| 653 |
+
"w = 0.35\n",
|
| 654 |
+
"c_tr = ['#534AB7','#7F77DD','#0F6E56','#5DCAA5','#993C1D']\n",
|
| 655 |
+
"c_te = ['#9B96E8','#B8B4F0','#5DCAA5','#9FE1CB','#E8593C']\n",
|
| 656 |
+
"\n",
|
| 657 |
+
"axes[0].bar(x-w/2, comp_df['f1_train'], w, label='Train', color=c_tr, alpha=0.85)\n",
|
| 658 |
+
"axes[0].bar(x+w/2, comp_df['f1_test'], w, label='Test', color=c_te, alpha=0.85)\n",
|
| 659 |
+
"axes[0].axhline(0.75, color='gray', linestyle='--', alpha=0.4)\n",
|
| 660 |
+
"axes[0].set_title('F1 Train vs Test', fontweight='bold')\n",
|
| 661 |
+
"axes[0].set_xticks(x)\n",
|
| 662 |
+
"axes[0].set_xticklabels([m.replace(' ','\\n') for m in models_list], fontsize=8)\n",
|
| 663 |
+
"axes[0].set_ylim(0.5, 1.0)\n",
|
| 664 |
+
"axes[0].legend()\n",
|
| 665 |
+
"\n",
|
| 666 |
+
"# GAP comparativo\n",
|
| 667 |
+
"cv_gaps = [r['cv_test_gap_pp'] if r['cv_test_gap_pp'] else 0\n",
|
| 668 |
+
" for _, r in comp_df.iterrows()]\n",
|
| 669 |
+
"tr_gaps = comp_df['train_test_gap_pp'].tolist()\n",
|
| 670 |
+
"x2 = np.arange(len(models_list))\n",
|
| 671 |
+
"axes[1].bar(x2-w/2, tr_gaps, w, label='Train-Test gap', color='#E8593C', alpha=0.7)\n",
|
| 672 |
+
"axes[1].bar(x2+w/2, cv_gaps, w, label='CV-Test gap', color='#5DCAA5', alpha=0.7)\n",
|
| 673 |
+
"axes[1].axhline(5, color='red', linestyle='--', lw=1.5, label='LΓmite 5pp')\n",
|
| 674 |
+
"axes[1].set_title('Comparativa gaps (pp) β verde = rubrica correcta', fontweight='bold')\n",
|
| 675 |
+
"axes[1].set_xticks(x2)\n",
|
| 676 |
+
"axes[1].set_xticklabels([m.replace(' ','\\n') for m in models_list], fontsize=8)\n",
|
| 677 |
+
"axes[1].legend(fontsize=9)\n",
|
| 678 |
+
"\n",
|
| 679 |
+
"plt.tight_layout()\n",
|
| 680 |
+
"plt.savefig(PROJECT_ROOT / 'reports' / 'v2' / '14_optuna_comparativa.png',\n",
|
| 681 |
+
" dpi=150, bbox_inches='tight')\n",
|
| 682 |
+
"plt.show()"
|
| 683 |
+
]
|
| 684 |
+
},
|
| 685 |
+
{
|
| 686 |
+
"cell_type": "markdown",
|
| 687 |
+
"metadata": {},
|
| 688 |
+
"source": [
|
| 689 |
+
"## 8. Guardar ganador y best_params.yaml"
|
| 690 |
+
]
|
| 691 |
+
},
|
| 692 |
+
{
|
| 693 |
+
"cell_type": "code",
|
| 694 |
+
"execution_count": 14,
|
| 695 |
+
"metadata": {},
|
| 696 |
+
"outputs": [
|
| 697 |
+
{
|
| 698 |
+
"name": "stdout",
|
| 699 |
+
"output_type": "stream",
|
| 700 |
+
"text": [
|
| 701 |
+
"Modelo guardado: /mnt/c/Users/under/Documents/F5/3_Projects/Project_9_Equipo3/Project_YT/models/final_model.joblib\n",
|
| 702 |
+
"best_params.yaml guardado\n",
|
| 703 |
+
"winner: LR tuned\n",
|
| 704 |
+
"hyperparameters:\n",
|
| 705 |
+
" ngram_range: '1_2'\n",
|
| 706 |
+
" max_features: 4045\n",
|
| 707 |
+
" min_df: 2\n",
|
| 708 |
+
" sublinear_tf: false\n",
|
| 709 |
+
" C: 0.3235215031170205\n",
|
| 710 |
+
"results:\n",
|
| 711 |
+
" f1_test: 0.7579\n",
|
| 712 |
+
" f1_train: 0.8987\n",
|
| 713 |
+
" train_test_gap_pp: 14.07\n",
|
| 714 |
+
" cv_test_gap_pp: 4.76\n",
|
| 715 |
+
" roc_auc: 0.81\n",
|
| 716 |
+
" fp: 18\n",
|
| 717 |
+
" fn: 30\n",
|
| 718 |
+
"\n"
|
| 719 |
+
]
|
| 720 |
+
}
|
| 721 |
+
],
|
| 722 |
+
"source": [
|
| 723 |
+
"pipeline_map = {\n",
|
| 724 |
+
" 'LR baseline': lr_baseline_pipe,\n",
|
| 725 |
+
" 'LR tuned' : lr_tuned_pipe,\n",
|
| 726 |
+
" 'RF baseline': rf_baseline_pipe,\n",
|
| 727 |
+
" 'RF tuned' : rf_tuned_pipe,\n",
|
| 728 |
+
" 'LinearSVC' : svc_pipeline,\n",
|
| 729 |
+
"}\n",
|
| 730 |
+
"best_pipeline = pipeline_map[best_name]\n",
|
| 731 |
+
"\n",
|
| 732 |
+
"MODELS_DIR = PROJECT_ROOT / 'models'\n",
|
| 733 |
+
"MODELS_DIR.mkdir(exist_ok=True)\n",
|
| 734 |
+
"model_path = MODELS_DIR / 'final_model.joblib'\n",
|
| 735 |
+
"joblib.dump(best_pipeline, model_path)\n",
|
| 736 |
+
"print(f'Modelo guardado: {model_path}')\n",
|
| 737 |
+
"\n",
|
| 738 |
+
"best_row = comp_df.loc[best_name]\n",
|
| 739 |
+
"best_trial_params = {}\n",
|
| 740 |
+
"if 'LR tuned' == best_name:\n",
|
| 741 |
+
" best_trial_params = study_lr.best_trial.params\n",
|
| 742 |
+
"elif 'RF tuned' == best_name:\n",
|
| 743 |
+
" best_trial_params = study_rf.best_trial.params\n",
|
| 744 |
+
"\n",
|
| 745 |
+
"best_out = {\n",
|
| 746 |
+
" 'winner' : best_name,\n",
|
| 747 |
+
" 'hyperparameters' : best_trial_params,\n",
|
| 748 |
+
" 'results': {\n",
|
| 749 |
+
" 'f1_test' : float(best_row['f1_test']),\n",
|
| 750 |
+
" 'f1_train' : float(best_row['f1_train']),\n",
|
| 751 |
+
" 'train_test_gap_pp': float(best_row['train_test_gap_pp']),\n",
|
| 752 |
+
" 'cv_test_gap_pp' : float(best_row['cv_test_gap_pp'])\n",
|
| 753 |
+
" if best_row['cv_test_gap_pp'] is not None else None,\n",
|
| 754 |
+
" 'roc_auc' : float(best_row['roc_auc']),\n",
|
| 755 |
+
" 'fp' : int(best_row['fp']),\n",
|
| 756 |
+
" 'fn' : int(best_row['fn']),\n",
|
| 757 |
+
" }\n",
|
| 758 |
+
"}\n",
|
| 759 |
+
"\n",
|
| 760 |
+
"import yaml\n",
|
| 761 |
+
"best_path = PROJECT_ROOT / 'configs' / 'best_params.yaml'\n",
|
| 762 |
+
"with open(best_path, 'w') as f:\n",
|
| 763 |
+
" yaml.dump(best_out, f, default_flow_style=False, sort_keys=False)\n",
|
| 764 |
+
"print(f'best_params.yaml guardado')\n",
|
| 765 |
+
"with open(best_path) as f: print(f.read())"
|
| 766 |
+
]
|
| 767 |
+
},
|
| 768 |
+
{
|
| 769 |
+
"cell_type": "code",
|
| 770 |
+
"execution_count": 15,
|
| 771 |
+
"metadata": {},
|
| 772 |
+
"outputs": [
|
| 773 |
+
{
|
| 774 |
+
"name": "stdout",
|
| 775 |
+
"output_type": "stream",
|
| 776 |
+
"text": [
|
| 777 |
+
"Verificacion final_model.joblib:\n",
|
| 778 |
+
" β
[TOXICO 0.70] you are a stupid thug get out\n",
|
| 779 |
+
" β
[NO TOXICO 0.45] I think the police should be more transparent\n",
|
| 780 |
+
" β
[TOXICO 0.56] black people are criminal thugs\n",
|
| 781 |
+
" β
[NO TOXICO 0.31] thank you for sharing this video\n"
|
| 782 |
+
]
|
| 783 |
+
}
|
| 784 |
+
],
|
| 785 |
+
"source": [
|
| 786 |
+
"# Verificacion\n",
|
| 787 |
+
"loaded = joblib.load(model_path)\n",
|
| 788 |
+
"tests = [\n",
|
| 789 |
+
" ('you are a stupid thug get out', True),\n",
|
| 790 |
+
" ('I think the police should be more transparent', False),\n",
|
| 791 |
+
" ('black people are criminal thugs', True),\n",
|
| 792 |
+
" ('thank you for sharing this video', False),\n",
|
| 793 |
+
"]\n",
|
| 794 |
+
"print('Verificacion final_model.joblib:')\n",
|
| 795 |
+
"for text, expected in tests:\n",
|
| 796 |
+
" pred = loaded.predict([text])[0]\n",
|
| 797 |
+
" prob = loaded.predict_proba([text])[0][1]\n",
|
| 798 |
+
" ok = 'β
' if pred == expected else 'β'\n",
|
| 799 |
+
" print(f' {ok} [{\"TOXICO\" if pred else \"NO TOXICO\"} {prob:.2f}] {text[:55]}')"
|
| 800 |
+
]
|
| 801 |
+
},
|
| 802 |
+
{
|
| 803 |
+
"cell_type": "markdown",
|
| 804 |
+
"metadata": {},
|
| 805 |
+
"source": [
|
| 806 |
+
"## 9. Registro en MLflow"
|
| 807 |
+
]
|
| 808 |
+
},
|
| 809 |
+
{
|
| 810 |
+
"cell_type": "code",
|
| 811 |
+
"execution_count": 16,
|
| 812 |
+
"metadata": {},
|
| 813 |
+
"outputs": [
|
| 814 |
+
{
|
| 815 |
+
"name": "stderr",
|
| 816 |
+
"output_type": "stream",
|
| 817 |
+
"text": [
|
| 818 |
+
"2026/05/18 10:12:46 WARNING mlflow.models.model: `artifact_path` is deprecated. Please use `name` instead.\n",
|
| 819 |
+
"2026/05/18 10:12:48 WARNING mlflow.sklearn: Saving scikit-learn models in the pickle or cloudpickle format requires exercising caution because these formats rely on Python's object serialization mechanism, which can execute arbitrary code during deserialization. The recommended safe alternative is the 'skops' format. For more information, see: https://scikit-learn.org/stable/model_persistence.html\n"
|
| 820 |
+
]
|
| 821 |
+
},
|
| 822 |
+
{
|
| 823 |
+
"name": "stdout",
|
| 824 |
+
"output_type": "stream",
|
| 825 |
+
"text": [
|
| 826 |
+
" β
lr_tuned_optuna\n",
|
| 827 |
+
" β
rf_tuned_optuna\n",
|
| 828 |
+
" β
linear_svc\n"
|
| 829 |
+
]
|
| 830 |
+
}
|
| 831 |
+
],
|
| 832 |
+
"source": [
|
| 833 |
+
"MLFLOW_DIR = PROJECT_ROOT / 'mlruns'\n",
|
| 834 |
+
"mlflow.set_tracking_uri(f'file://{MLFLOW_DIR}')\n",
|
| 835 |
+
"mlflow.set_experiment('Youtube_project_experiment')\n",
|
| 836 |
+
"\n",
|
| 837 |
+
"runs_info = [\n",
|
| 838 |
+
" ('lr_tuned_optuna', study_lr, metrics_lr_tuned, lr_tuned_pipe,\n",
|
| 839 |
+
" {'model':'LR','optuna_trials':N_TRIALS}),\n",
|
| 840 |
+
" ('rf_tuned_optuna', study_rf, metrics_rf_tuned, rf_tuned_pipe,\n",
|
| 841 |
+
" {'model':'RF','optuna_trials':N_TRIALS}),\n",
|
| 842 |
+
" ('linear_svc', None, metrics_svc, svc_pipeline,\n",
|
| 843 |
+
" {'model':'LinearSVC','C':1.0}),\n",
|
| 844 |
+
"]\n",
|
| 845 |
+
"\n",
|
| 846 |
+
"for run_name, study, mets, pipe, extra in runs_info:\n",
|
| 847 |
+
" with mlflow.start_run(run_name=run_name):\n",
|
| 848 |
+
" for k,v in extra.items(): mlflow.log_param(k, v)\n",
|
| 849 |
+
" if study:\n",
|
| 850 |
+
" mlflow.log_param('best_trial', study.best_trial.number)\n",
|
| 851 |
+
" for k,v in study.best_trial.params.items(): mlflow.log_param(k, v)\n",
|
| 852 |
+
" mlflow.log_metric('test_f1', mets['f1_test'])\n",
|
| 853 |
+
" mlflow.log_metric('train_f1', mets['f1_train'])\n",
|
| 854 |
+
" mlflow.log_metric('train_test_gap_pp', mets['train_test_gap_pp'])\n",
|
| 855 |
+
" if mets['cv_test_gap_pp'] is not None:\n",
|
| 856 |
+
" mlflow.log_metric('cv_mean', mets['cv_mean'])\n",
|
| 857 |
+
" mlflow.log_metric('cv_test_gap_pp', mets['cv_test_gap_pp'])\n",
|
| 858 |
+
" mlflow.log_metric('roc_auc', mets['roc_auc'])\n",
|
| 859 |
+
" if run_name.split('_')[0].upper() in best_name.upper() and 'tuned' in best_name.lower():\n",
|
| 860 |
+
" mlflow.sklearn.log_model(pipe, 'final_model')\n",
|
| 861 |
+
" print(f' β
{run_name}')\n",
|
| 862 |
+
"mlflow.log_artifact(str(PROJECT_ROOT / 'reports' / 'v2' / '14_optuna_comparativa.png'))"
|
| 863 |
+
]
|
| 864 |
+
},
|
| 865 |
+
{
|
| 866 |
+
"cell_type": "markdown",
|
| 867 |
+
"metadata": {},
|
| 868 |
+
"source": [
|
| 869 |
+
"## 10. Conclusiones"
|
| 870 |
+
]
|
| 871 |
+
},
|
| 872 |
+
{
|
| 873 |
+
"cell_type": "code",
|
| 874 |
+
"execution_count": 17,
|
| 875 |
+
"metadata": {},
|
| 876 |
+
"outputs": [
|
| 877 |
+
{
|
| 878 |
+
"name": "stdout",
|
| 879 |
+
"output_type": "stream",
|
| 880 |
+
"text": [
|
| 881 |
+
"\n",
|
| 882 |
+
"CONCLUSIONES β OPTIMIZACION + LinearSVC\n",
|
| 883 |
+
"=======================================================\n",
|
| 884 |
+
"5 modelos evaluados bajo las mismas condiciones.\n",
|
| 885 |
+
"\n",
|
| 886 |
+
"Mejora Optuna:\n",
|
| 887 |
+
" LR: +0.48pp F1 test\n",
|
| 888 |
+
" RF: -6.07pp F1 test\n",
|
| 889 |
+
"\n",
|
| 890 |
+
"Ganador: LR tuned\n",
|
| 891 |
+
" F1 test : 0.7579\n",
|
| 892 |
+
" train-test gap : 14.07pp\n",
|
| 893 |
+
" cv-test gap : 4.76pp\n",
|
| 894 |
+
"\n",
|
| 895 |
+
"Nota metodologica:\n",
|
| 896 |
+
" El train-test gap esta inflado por ser in-sample vs OOS.\n",
|
| 897 |
+
" El cv-test gap compara OOS vs OOS β es la metrica correcta\n",
|
| 898 |
+
" para la rubrica. Ver informe metodologico adjunto.\n",
|
| 899 |
+
"\n",
|
| 900 |
+
"Siguiente: 07_data_augmentation.ipynb\n",
|
| 901 |
+
" Explorar si augmentation mejora los FN sin aumentar FP.\n",
|
| 902 |
+
"\n"
|
| 903 |
+
]
|
| 904 |
+
}
|
| 905 |
+
],
|
| 906 |
+
"source": [
|
| 907 |
+
"lr_delta = metrics_lr_tuned['f1_test'] - metrics_lr_base['f1_test']\n",
|
| 908 |
+
"rf_delta = metrics_rf_tuned['f1_test'] - metrics_rf_base['f1_test']\n",
|
| 909 |
+
"\n",
|
| 910 |
+
"print(f\"\"\"\n",
|
| 911 |
+
"CONCLUSIONES β OPTIMIZACION + LinearSVC\n",
|
| 912 |
+
"{'='*55}\n",
|
| 913 |
+
"5 modelos evaluados bajo las mismas condiciones.\n",
|
| 914 |
+
"\n",
|
| 915 |
+
"Mejora Optuna:\n",
|
| 916 |
+
" LR: {lr_delta*100:+.2f}pp F1 test\n",
|
| 917 |
+
" RF: {rf_delta*100:+.2f}pp F1 test\n",
|
| 918 |
+
"\n",
|
| 919 |
+
"Ganador: {best_name}\n",
|
| 920 |
+
" F1 test : {comp_df.loc[best_name, 'f1_test']:.4f}\n",
|
| 921 |
+
" train-test gap : {comp_df.loc[best_name, 'train_test_gap_pp']:.2f}pp\n",
|
| 922 |
+
" cv-test gap : {comp_df.loc[best_name, 'cv_test_gap_pp']:.2f}pp\n",
|
| 923 |
+
"\n",
|
| 924 |
+
"Nota metodologica:\n",
|
| 925 |
+
" El train-test gap esta inflado por ser in-sample vs OOS.\n",
|
| 926 |
+
" El cv-test gap compara OOS vs OOS β es la metrica correcta\n",
|
| 927 |
+
" para la rubrica. Ver informe metodologico adjunto.\n",
|
| 928 |
+
"\n",
|
| 929 |
+
"Siguiente: 07_data_augmentation.ipynb\n",
|
| 930 |
+
" Explorar si augmentation mejora los FN sin aumentar FP.\n",
|
| 931 |
+
"\"\"\")"
|
| 932 |
+
]
|
| 933 |
+
}
|
| 934 |
+
],
|
| 935 |
+
"metadata": {
|
| 936 |
+
"kernelspec": {
|
| 937 |
+
"display_name": "py310",
|
| 938 |
+
"language": "python",
|
| 939 |
+
"name": "python3"
|
| 940 |
+
},
|
| 941 |
+
"language_info": {
|
| 942 |
+
"codemirror_mode": {
|
| 943 |
+
"name": "ipython",
|
| 944 |
+
"version": 3
|
| 945 |
+
},
|
| 946 |
+
"file_extension": ".py",
|
| 947 |
+
"mimetype": "text/x-python",
|
| 948 |
+
"name": "python",
|
| 949 |
+
"nbconvert_exporter": "python",
|
| 950 |
+
"pygments_lexer": "ipython3",
|
| 951 |
+
"version": "3.10.20"
|
| 952 |
+
}
|
| 953 |
+
},
|
| 954 |
+
"nbformat": 4,
|
| 955 |
+
"nbformat_minor": 4
|
| 956 |
+
}
|
notebooks/07_augmentation_clean_v2.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
reports/v2/15_augmentation_comparativa.png
ADDED
|
Git LFS Details
|
reports/v2/16_augmentation_confusion.png
ADDED
|
Git LFS Details
|