JonnyBP commited on
Commit
ed334ca
Β·
1 Parent(s): e284b6a

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/8skn5vwRI0ZkegypqalGlSpVDElG2bJljatXrxqGYZg9BUJCQsweFmPGjDG3u2TJErueGq5y1tM2fY+XTz/91Pyp1+TJk835w4YNMwzDMObNm2fOmz17tnHy5Mls7Y/etQBQdB07dszuu3bPnj1ZrlOvXj0z/ssvvzTnf/nll+b8yMhIc376nmjXfsVy9uxZu/1e+5n6zz//bM7r2rWruY303/G9e/c250dHRxv+/v7msiNHjhiGYRjx8fHGxIkTjbp169otvzY1aNDA4bZvtI1iGJl/xzprw6Tvcfb6668bhmEYixcvNudNmzbNjHXl2Bz56aef7HqiJSUlmcuaN2/usD1y5MgRY+DAgUaVKlUc9mobOXKkw+N/5513zPlr166161VnGIaxZs0ac94zzzxjHDlyxKXhpdIfy5gxYzIsdyUXw7B/r6bv5fvOO++Y82+//Xa7faRvQ3fq1CnTfKOjo7P1k35H0759+zLddvp2+KhRozK8PoMGDTIMwzBSUlKMEiVKGJIMHx8f49tvvzX3MWTIEDP+1VdfNQzD/j17fQ94w7B/nzZp0sScf/XqVaNSpUrmsms9bWfNmmXO69+/v7nvjRs3mu/n4OBgIyUlJdPjzU3pX7uAgAC7oWAMw/nn1jCy7mlbqVIl8+8JwzCMli1bmss++OADwzDsP/+VK1fO8Gs9R/tPSUmx6/n69ddfOz2+G71m7Nixw4wNCgoyzpw54zDu888/N+MaNWpkt6xRo0bmsmXLlrl83I7ee8uWLbP77KX/vERERBiSDJvNZpw9e9al1+v64S1y+/VE/ik6I4wDKJAc3YgsNDTUTdn845577skw7/PPP8/y5hHR0dHZ3kfbtm3Nx9f++5udbdhsNvXq1UsvvfSSzpw5o40bNyo0NNTsKdC9e3f5+vpKknr37q1Zs2YpMTFRDz74oCSpbNmyatmypYYMGaL27dtnO19n9u3bZz5+6KGHHMZcy+2+++7Tc889p/Pnz2vEiBEaMWKESpQooWbNmulf//qXmSMAANdcfyfyEydO2PXQciT9d1OzZs3Mx02bNnUYk961mJIlS5rzSpQooWrVqkmS2aNRcv6dnX6fwcHBqlmzpnbs2CEprbdWeHi4evbsqRUrVjg9Bmfbzo82iiPdunXTkCFDdOXKFX322Wd64oknzF/5XGubXJPTY7smfc/qhg0bytvb23zeokULu16aUtovlW699VYdO3bM5X06e38cOnRIhmGodevWql69uv766y9Nnz5d06dPV2BgoBo2bKjevXtrwIAB8vDI3g9cjf/1HHYmq1yu/4WUs/j0r1929pvejh07dNttt2U7Pr2+fftq4cKFTpe3bdtW4eHhOnr0qJYuXaoZM2Y4/KXYuXPndPHiRUlSUlKS0/aqo16y1atXN3/tdU3616NJkybmY09PTzVq1Mi8weE16a8NCxYscHiTw5iYGLNnpTNHjhzJsG1H/Pz81Lhx4yzjrmnZsqXd9elGNW7c2K7HeNOmTfX9999LyvhekqSOHTtm62Z2586dM3u++vr6Zvp3x41eM66/5pcpUyZbcek1bdpU27dvzxB3TVbH7ei9l347q1ev1urVqzOsZxiG/vzzT9WoUSPbr1dWbvT1RP5heAQAlla2bFm1atXKbrr2Uz53clQ4fuONN8zH/fr10zfffKNNmzbpzjvvNOdfG4ogO0qUKGE+Tt8AyE7D+vohEtI3eHv37m0+rlOnjrZv367hw4erWbNmCg4O1pkzZ7Rs2TJ16NBBP/zwQ7bzvRHXfmIUFham7du3a/To0WrVqpVKlSqlixcvas2aNXrooYf0ySef5Es+AICCIyAgwPypuCSzmJAT2RkW6FqROH0hLigoyGFsdoth1+/3yJEj5h/UAQEBevPNNxUVFaWoqCgzxlmbIj/aKI4EBgbq3nvvlSRt3rxZBw8e1KpVqyRJbdq0UXh4+A0fmyPZOWfLli0zC7a1atXS4sWLtWnTJrufAGdnn4725e/vr++//16TJ0/W7bffrrCwMF26dEkbNmzQv//9b02fPj3TbaYv8l8rRGaHq0NYZRaffr/p88lv6Yv7hw8f1s8//2wOjVClShXdeuutLm3P0U/YXe38cSNDhTnaf3rz589X69ats5yuDRuRXY6OMf1xpKSk2C1zNMRBZrJ6TXLSwebaMHiO5PY1I6du9LhvpOPR9e+lzF6vrFjl9UT2ULQFgBxw9CV5/Phx8/GcOXN055136tZbb7Wbn19q1aqlhg0bSkrrXbNkyRJJaeP2pe/BaxiGbrnlFr322mv68ccfFR0dbRZ4U1NTtXz58hvOpUaNGubjAwcOyEi7CabddG3MW8MwVLlyZU2bNk2bNm3SuXPn9PPPP5vrpx9vLv0fyzQsAKBo69Gjh/l45syZ5piq6Z05c8bspZT+u2nr1q3m42tjrF8fk9vS7zMmJkZ79+41n1etWtWu7dChQwcNHjxYbdu2NX8pk5ncaKPk9Dv22j+GU1NT9fjjj5uFhvT/TL6RY7vmWq9mKa3nZ/oi1JYtWzLEp9/n0KFD9dBDD6lVq1ZKSEjIcl/O3h8RERGy2WwyDENlypTRuHHjtG7dOp08eVIHDhxQQECAJDkdK/eaSpUqqXjx4pLSxjq+kVyyG5/+nxzX77d27dqZ5tCuXTuHbbnsTJn1sr0mfeeC5557zuzJ2atXL/MYS5cubXZuCAgI0KVLlzLsKyUlxWEPWEevU/rXY9u2bebjlJQUu+fXpL82TJgwweGxxsfHZ+hVmV8cHWP6XyScOnXKfLx58+Ysi8vbt2+3uw5k9l5ytn9H0p/HhIQEffvttw7jcuOacf0131mh2tl3w/XPHX0/ZHXcjpan307fvn2dvpc6dOiQ7ddLyvwanhuvJ/IPwyMAKPA2bNigs2fPmgPGX3Ot+FimTBm7QmVeqVy5svkTl/Hjx6tDhw768MMP3XZDh0ceeUS//PKLTp06ZTbO0jd4pbSbgEVFRenuu+82/2j4+uuvzeWJiYk3nEfv3r3Nm4Lcc889euaZZ1SxYkWdPHlSf/75p7744gv93//9n/r166ePP/5Y8+bNU9euXVWlShUFBwfru+++c5hP+p7I77zzjjp37qxixYq59PMxAEDh8NRTT+mjjz7SkSNHFB0drWbNmumpp55S3bp1denSJUVFRWnBggWKiopSyZIl1atXL+3evVtSWhHv0qVLstlsGjNmjLnNnj175lm+H3/8sWrVqqUGDRrojTfeMIsmDRo0UHh4uN1Pkb/77jt9/PHH8vT0zPHNjVxto+T0O7Zjx44qVaqUzp8/r7Vr10qyv8HUtVxu9NgaNmxo/oz+xIkT6tOnj3r37q2vv/46w9AI1+9z/vz5qlq1qvbv368XX3wxy32NHTtWXl5eKl68uMaOHWvOv++++yRJP/zwg4YPH65u3bqpevXqKl26tHbv3q3Lly9Lyrot5eXlpaZNm2r9+vXmEBk5zeV6jz/+uKZOnaqEhATzBkSO4tPvt2XLlpnmkNeu3bBu9+7d5ntIsi/8e3h4qGfPnnrzzTcVFxenu+66S8OHD1fp0qV17Ngx/fbbb/r88881f/58tWvXLst93nnnnfLz81NCQoK2bt2qESNGqEOHDvrkk08cDl/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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",
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

  • SHA256: 58fc17e62b6c09ec7d95c5ac2f11d50430e6fc3acd66bd52b7b74222609c514a
  • Pointer size: 130 Bytes
  • Size of remote file: 56.9 kB
reports/v2/16_augmentation_confusion.png ADDED

Git LFS Details

  • SHA256: 2f3aeeae86185e63c6e822d70b1594835428cdb25da39275f059b792e6a989e8
  • Pointer size: 130 Bytes
  • Size of remote file: 67.9 kB