{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ⚙️ Notebook 06 — Optimización con Optuna + LinearSVC\n", "## YouTube Hate Speech Detection\n", "\n", "### ¿Qué hace este notebook?\n", "Optimizamos LR y RF con Optuna, añadimos LinearSVC como modelo adicional,\n", "y comparamos 5 modelos en una tabla unificada.\n", "\n", "### Nota metodológica — dos métricas de GAP\n", "Este notebook reporta dos métricas de gap distintas:\n", "\n", "| Métrica | Fórmula | Qué mide |\n", "|---|---|---|\n", "| `train_test_gap` | f1_train - f1_test | Memorización / ajuste in-sample |\n", "| `cv_test_gap` | \\|cv_mean - f1_test\\| | Estabilidad de generalización (OOS vs OOS) |\n", "\n", "La rúbrica exige < 5pp. El `cv_test_gap` es la comparación\n", "estadísticamente correcta porque ambos términos son out-of-sample.\n", "Ver informe metodológico para la justificación completa.\n", "\n", "### Modelos comparados\n", "1. LR baseline (cargado desde disco)\n", "2. RF baseline (cargado desde disco)\n", "3. LinearSVC (nuevo — muy competitivo en TF-IDF sparse)\n", "4. LR tuned (Optuna)\n", "5. RF tuned (Optuna)\n", "\n", "### Sin hardcoding\n", "Todos los valores se calculan en el momento desde los modelos en disco." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 0. Imports y configuración" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "PROJECT_ROOT: /mnt/c/Users/under/Documents/F5/3_Projects/Project_9_Equipo3/Project_YT\n" ] } ], "source": [ "import sys, yaml, joblib, warnings\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import mlflow, mlflow.sklearn\n", "import optuna\n", "from pathlib import Path\n", "from sklearn.svm import LinearSVC\n", "from sklearn.calibration import CalibratedClassifierCV\n", "from sklearn.ensemble import RandomForestClassifier\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.feature_extraction.text import TfidfVectorizer\n", "from sklearn.pipeline import Pipeline\n", "from sklearn.model_selection import (\n", " train_test_split, StratifiedKFold,\n", " cross_val_score, cross_validate\n", ")\n", "from sklearn.metrics import f1_score, roc_auc_score, classification_report\n", "warnings.filterwarnings('ignore')\n", "optuna.logging.set_verbosity(optuna.logging.WARNING)\n", "\n", "PROJECT_ROOT = Path.cwd().parent\n", "sys.path.insert(0, str(PROJECT_ROOT))\n", "plt.rcParams['figure.figsize'] = (12, 5)\n", "plt.rcParams['axes.spines.top'] = False\n", "plt.rcParams['axes.spines.right'] = False\n", "print(f'PROJECT_ROOT: {PROJECT_ROOT}')" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LR baseline: True | RF baseline: True\n" ] } ], "source": [ "CONFIG_FEAT = PROJECT_ROOT / 'configs' / 'features.yaml'\n", "CONFIG_PIPE = PROJECT_ROOT / 'configs' / 'pipeline.yaml'\n", "CONFIG_MOD = PROJECT_ROOT / 'configs' / 'models.yaml'\n", "\n", "with open(CONFIG_FEAT) as f: feat_cfg = yaml.safe_load(f)\n", "with open(CONFIG_PIPE) as f: pipe_cfg = yaml.safe_load(f)\n", "with open(CONFIG_MOD) as f: mod_cfg = yaml.safe_load(f)\n", "\n", "tfidf_cfg = feat_cfg['vectorization']['tfidf']\n", "rf_cfg = mod_cfg['models']['random_forest']\n", "lr_cfg = mod_cfg['models']['logistic_regression']\n", "TARGET = pipe_cfg['data']['target_binary']\n", "RAND = pipe_cfg['pipeline']['random_state']\n", "TEST_SIZE = pipe_cfg['pipeline']['test_size']\n", "CV_FOLDS = pipe_cfg['pipeline']['cv_folds']\n", "\n", "MODEL_LR = PROJECT_ROOT / 'models' / 'lr_baseline.joblib'\n", "MODEL_RF = PROJECT_ROOT / 'models' / 'best_ensemble.joblib'\n", "print(f'LR baseline: {MODEL_LR.exists()} | RF baseline: {MODEL_RF.exists()}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Carga de datos y split\n", "\n", "Mismo `random_state` fijo en todos los notebooks — comparación justa." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Train: 800 | Test: 200\n" ] } ], "source": [ "PROCESSED = PROJECT_ROOT / 'data' / 'processed' / 'v2' / 'comments_preprocessed.csv'\n", "df = pd.read_csv(PROCESSED)\n", "df['clean_text'] = df['clean_text'].fillna('').astype(str)\n", "X, y = df['clean_text'], df[TARGET]\n", "\n", "X_train, X_test, y_train, y_test = train_test_split(\n", " X, y, test_size=TEST_SIZE, random_state=RAND, stratify=y\n", ")\n", "cv_strategy = StratifiedKFold(n_splits=CV_FOLDS, shuffle=True, random_state=RAND)\n", "print(f'Train: {len(X_train)} | Test: {len(X_test)}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Función de evaluación\n", "\n", "Calcula ambas métricas de gap. `cv_scores` es opcional —\n", "solo los modelos evaluados con `cross_validate` tendrán `cv_test_gap`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def evaluate_pipeline(pipeline, X_tr, y_tr, X_te, y_te, name, cv_scores=None):\n", " \"\"\"\n", " Evalua un pipeline y devuelve dict con ambas metricas de gap.\n", "\n", " train_test_gap : f1_train - f1_test (in-sample vs out-of-sample)\n", " cv_test_gap : |cv_mean - f1_test| (OOS vs OOS — para rubrica)\n", " \"\"\"\n", " y_pred = pipeline.predict(X_te)\n", " y_pred_proba = pipeline.predict_proba(X_te)[:, 1]\n", " y_pred_train = pipeline.predict(X_tr)\n", "\n", " f1_te = f1_score(y_te, y_pred, average='weighted')\n", " f1_tr = f1_score(y_tr, y_pred_train, average='weighted')\n", " roc = roc_auc_score(y_te, y_pred_proba)\n", "\n", " cv_mean = cv_std = cv_test_gap = None\n", " if cv_scores is not None:\n", " cv_mean = cv_scores['test_score'].mean()\n", " cv_std = cv_scores['test_score'].std()\n", " cv_test_gap = abs(cv_mean - f1_te) * 100\n", "\n", " return {\n", " 'name' : name,\n", " 'f1_test' : round(f1_te, 4),\n", " 'f1_train' : round(f1_tr, 4),\n", " 'train_test_gap_pp': round((f1_tr - f1_te) * 100, 2),\n", " 'cv_mean' : round(cv_mean, 4) if cv_mean is not None else None,\n", " 'cv_std' : round(cv_std, 4) if cv_std is not None else None,\n", " 'cv_test_gap_pp' : round(cv_test_gap, 2) if cv_test_gap is not None else None,\n", " 'roc_auc' : round(roc, 4),\n", " 'fp' : int(((y_te == False) & (y_pred == True)).sum()),\n", " 'fn' : int(((y_te == True) & (y_pred == False)).sum()),\n", " }" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Helper TF-IDF y carga de baselines\n", "\n", "Los baselines se cargan desde disco y se evalúan sin re-entrenar." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " LR baseline F1=0.7531 | train-test=10.91pp | cv-test=5.17pp\n", " RF baseline F1=0.7531 | train-test=10.91pp | cv-test=5.17pp\n" ] } ], "source": [ "def make_tfidf(**overrides):\n", " params = {\n", " 'max_features': tfidf_cfg['max_features'],\n", " 'ngram_range' : tuple(tfidf_cfg['ngram_range']),\n", " 'sublinear_tf': tfidf_cfg['sublinear_tf'],\n", " 'min_df' : tfidf_cfg['min_df'],\n", " 'analyzer' : 'word',\n", " 'strip_accents': 'unicode',\n", " }\n", " params.update(overrides)\n", " return TfidfVectorizer(**params)\n", "\n", "# Cargar baselines desde disco\n", "lr_baseline_pipe = joblib.load(MODEL_LR)\n", "rf_baseline_pipe = joblib.load(MODEL_RF)\n", "\n", "# CV scores para baselines\n", "cv_lr_base = cross_validate(lr_baseline_pipe, X_train, y_train,\n", " cv=cv_strategy, scoring='f1_weighted',\n", " return_train_score=False, n_jobs=-1)\n", "cv_rf_base = cross_validate(rf_baseline_pipe, X_train, y_train,\n", " cv=cv_strategy, scoring='f1_weighted',\n", " return_train_score=False, n_jobs=-1)\n", "\n", "metrics_lr_base = evaluate_pipeline(lr_baseline_pipe, X_train, y_train,\n", " X_test, y_test, 'LR baseline', cv_lr_base)\n", "metrics_rf_base = evaluate_pipeline(rf_baseline_pipe, X_train, y_train,\n", " X_test, y_test, 'RF baseline', cv_rf_base)\n", "\n", "for m in [metrics_lr_base, metrics_rf_base]:\n", " print(f\" {m['name']:15} F1={m['f1_test']:.4f} | \"\n", " f\"train-test={m['train_test_gap_pp']}pp | \"\n", " f\"cv-test={m['cv_test_gap_pp']}pp\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. LinearSVC — modelo adicional\n", "\n", "### ¿Por qué LinearSVC en TF-IDF?\n", "LinearSVC usa un hiperplano de separación lineal que funciona\n", "excepcionalmente bien con matrices sparse de alta dimensión.\n", "No tiene probabilidades nativas, pero `CalibratedClassifierCV`\n", "las añade mediante Platt scaling.\n", "\n", "Frecuentemente supera a LR en clasificación de texto porque\n", "maximiza el margen entre clases en lugar de minimizar log-loss." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " LinearSVC F1=0.7250 | train-test=23.99pp | cv-test=4.03pp\n" ] } ], "source": [ "# LinearSVC con calibración para obtener predict_proba\n", "svc_pipeline = Pipeline([\n", " ('tfidf', make_tfidf()),\n", " ('clf', CalibratedClassifierCV(\n", " LinearSVC(\n", " C=1.0, max_iter=2000,\n", " class_weight='balanced',\n", " random_state=RAND,\n", " ),\n", " cv=3\n", " ))\n", "])\n", "\n", "svc_pipeline.fit(X_train, y_train)\n", "\n", "cv_svc = cross_validate(svc_pipeline, X_train, y_train,\n", " cv=cv_strategy, scoring='f1_weighted',\n", " return_train_score=False, n_jobs=-1)\n", "\n", "metrics_svc = evaluate_pipeline(svc_pipeline, X_train, y_train,\n", " X_test, y_test, 'LinearSVC', cv_svc)\n", "\n", "print(f\" LinearSVC F1={metrics_svc['f1_test']:.4f} | \"\n", " f\"train-test={metrics_svc['train_test_gap_pp']}pp | \"\n", " f\"cv-test={metrics_svc['cv_test_gap_pp']}pp\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Optuna — Logistic Regression\n", "\n", "Búsqueda bayesiana sobre el pipeline completo TF-IDF + LR.\n", "Optuna solo ve X_train — X_test se reserva para evaluación final." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Optimizando LR — 60 trials...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Best trial: 52. Best value: 0.710353: 100%|██████████| 60/60 [00:04<00:00, 14.23it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Mejor F1 CV: 0.7104\n", "Params : {'ngram_range': '1_2', 'max_features': 4045, 'min_df': 2, 'sublinear_tf': False, 'C': 0.3235215031170205}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "N_TRIALS = 60\n", "\n", "def objective_lr(trial):\n", " ngram_str = trial.suggest_categorical('ngram_range', ['1_1', '1_2'])\n", " ngram = (1,1) if ngram_str == '1_1' else (1,2)\n", " pipe = Pipeline([\n", " ('tfidf', make_tfidf(\n", " max_features = trial.suggest_int('max_features', 500, 5000),\n", " min_df = trial.suggest_int('min_df', 2, 6),\n", " ngram_range = ngram,\n", " sublinear_tf = trial.suggest_categorical('sublinear_tf', [True, False]),\n", " )),\n", " ('clf', LogisticRegression(\n", " C = trial.suggest_float('C', 0.01, 2.0, log=True),\n", " max_iter = 1000,\n", " class_weight = 'balanced',\n", " solver = 'lbfgs',\n", " random_state = RAND,\n", " ))\n", " ])\n", " return cross_val_score(pipe, X_train, y_train,\n", " cv=cv_strategy, scoring='f1_weighted',\n", " n_jobs=-1).mean()\n", "\n", "study_lr = optuna.create_study(direction='maximize',\n", " sampler=optuna.samplers.TPESampler(seed=RAND),\n", " study_name='lr_optimization')\n", "print(f'Optimizando LR — {N_TRIALS} trials...')\n", "study_lr.optimize(objective_lr, n_trials=N_TRIALS, show_progress_bar=True)\n", "print(f'Mejor F1 CV: {study_lr.best_value:.4f}')\n", "print(f'Params : {study_lr.best_trial.params}')" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LR tuned F1=0.7579 | train-test=14.07pp | cv-test=4.76pp\n" ] } ], "source": [ "bp_lr = study_lr.best_trial.params\n", "ngram_lr = (1,1) if bp_lr['ngram_range'] == '1_1' else (1,2)\n", "\n", "lr_tuned_pipe = Pipeline([\n", " ('tfidf', make_tfidf(\n", " max_features=bp_lr['max_features'], min_df=bp_lr['min_df'],\n", " ngram_range=ngram_lr, sublinear_tf=bp_lr['sublinear_tf'],\n", " )),\n", " ('clf', LogisticRegression(\n", " C=bp_lr['C'], max_iter=1000,\n", " class_weight='balanced', solver='lbfgs', random_state=RAND,\n", " ))\n", "])\n", "lr_tuned_pipe.fit(X_train, y_train)\n", "\n", "cv_lr_tuned = cross_validate(lr_tuned_pipe, X_train, y_train,\n", " cv=cv_strategy, scoring='f1_weighted',\n", " return_train_score=False, n_jobs=-1)\n", "\n", "metrics_lr_tuned = evaluate_pipeline(lr_tuned_pipe, X_train, y_train,\n", " X_test, y_test, 'LR tuned', cv_lr_tuned)\n", "print(f\"LR tuned F1={metrics_lr_tuned['f1_test']:.4f} | \"\n", " f\"train-test={metrics_lr_tuned['train_test_gap_pp']}pp | \"\n", " f\"cv-test={metrics_lr_tuned['cv_test_gap_pp']}pp\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Optuna — Random Forest\n", "\n", "Búsqueda sobre n_estimators, max_depth y min_samples_leaf.\n", "RF con bigramas es muy lento — usamos solo unigramas." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Optimizando RF — 60 trials...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Best trial: 37. Best value: 0.713928: 100%|██████████| 60/60 [00:54<00:00, 1.09it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Mejor F1 CV: 0.7139\n", "Params : {'max_features': 1380, 'min_df': 6, 'n_estimators': 171, 'max_depth': 9, 'min_samples_leaf': 2}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "def objective_rf(trial):\n", " pipe = Pipeline([\n", " ('tfidf', make_tfidf(\n", " max_features = trial.suggest_int('max_features', 500, 5000),\n", " min_df = trial.suggest_int('min_df', 2, 6),\n", " ngram_range = (1, 1),\n", " sublinear_tf = True,\n", " )),\n", " ('clf', RandomForestClassifier(\n", " n_estimators = trial.suggest_int('n_estimators', 50, 300),\n", " max_depth = trial.suggest_int('max_depth', 4, 12),\n", " min_samples_leaf = trial.suggest_int('min_samples_leaf', 2, 8),\n", " max_features = 'sqrt',\n", " class_weight = 'balanced',\n", " random_state = RAND, n_jobs=-1,\n", " ))\n", " ])\n", " return cross_val_score(pipe, X_train, y_train,\n", " cv=cv_strategy, scoring='f1_weighted',\n", " n_jobs=-1).mean()\n", "\n", "study_rf = optuna.create_study(direction='maximize',\n", " sampler=optuna.samplers.TPESampler(seed=RAND),\n", " study_name='rf_optimization')\n", "print(f'Optimizando RF — {N_TRIALS} trials...')\n", "study_rf.optimize(objective_rf, n_trials=N_TRIALS, show_progress_bar=True)\n", "print(f'Mejor F1 CV: {study_rf.best_value:.4f}')\n", "print(f'Params : {study_rf.best_trial.params}')" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "RF tuned F1=0.6924 | train-test=12.09pp | cv-test=2.15pp\n" ] } ], "source": [ "bp_rf = study_rf.best_trial.params\n", "\n", "rf_tuned_pipe = Pipeline([\n", " ('tfidf', make_tfidf(\n", " max_features=bp_rf['max_features'], min_df=bp_rf['min_df'],\n", " ngram_range=(1,1), sublinear_tf=True,\n", " )),\n", " ('clf', RandomForestClassifier(\n", " n_estimators=bp_rf['n_estimators'], max_depth=bp_rf['max_depth'],\n", " min_samples_leaf=bp_rf['min_samples_leaf'],\n", " max_features='sqrt', class_weight='balanced',\n", " random_state=RAND, n_jobs=-1,\n", " ))\n", "])\n", "rf_tuned_pipe.fit(X_train, y_train)\n", "\n", "cv_rf_tuned = cross_validate(rf_tuned_pipe, X_train, y_train,\n", " cv=cv_strategy, scoring='f1_weighted',\n", " return_train_score=False, n_jobs=-1)\n", "\n", "metrics_rf_tuned = evaluate_pipeline(rf_tuned_pipe, X_train, y_train,\n", " X_test, y_test, 'RF tuned', cv_rf_tuned)\n", "print(f\"RF tuned F1={metrics_rf_tuned['f1_test']:.4f} | \"\n", " f\"train-test={metrics_rf_tuned['train_test_gap_pp']}pp | \"\n", " f\"cv-test={metrics_rf_tuned['cv_test_gap_pp']}pp\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Tabla comparativa — 5 modelos\n", "\n", "Se reportan ambos gaps. La columna `cv_test_gap` es la referencia\n", "para la rúbrica (OOS vs OOS). La columna `train_test_gap` se mantiene\n", "para análisis de memorización." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "COMPARATIVA FINAL — 5 MODELOS\n", "====================================================================================================\n", "Modelo F1 Test F1 Train TrTe gap CV Mean CV Std CV-Te gap FP FN Rubrica\n", "----------------------------------------------------------------------------------------------------\n", " LR baseline 0.7531 0.8623 10.91 0.7015 0.0312 5.17 19 30 ⚠️ 5.2pp\n", " LR tuned 0.7579 0.8987 14.07 0.7104 0.0353 4.76 18 30 ✅ OK\n", " RF baseline 0.7531 0.8623 10.91 0.7015 0.0312 5.17 19 30 ⚠️ 5.2pp\n", " RF tuned 0.6924 0.8133 12.09 0.7139 0.0334 2.15 9 49 ✅ OK\n", " LinearSVC 0.7250 0.9649 23.99 0.6847 0.0276 4.03 17 37 ✅ OK\n", "\n", "GANADOR (F1 test): LR tuned\n", " F1 test : 0.7579\n", " cv_test_gap : 4.76pp\n", " train_test_gap: 14.07pp\n" ] } ], "source": [ "all_metrics = [\n", " metrics_lr_base, metrics_lr_tuned,\n", " metrics_rf_base, metrics_rf_tuned,\n", " metrics_svc\n", "]\n", "\n", "comp_df = pd.DataFrame(all_metrics).set_index('name')\n", "\n", "# Rubrica basada en cv_test_gap\n", "comp_df['rubrica'] = comp_df['cv_test_gap_pp'].apply(\n", " lambda x: '✅ OK' if x is not None and x < 5 else f'⚠️ {x:.1f}pp' if x else 'N/A'\n", ")\n", "\n", "print('COMPARATIVA FINAL — 5 MODELOS')\n", "print('=' * 100)\n", "print(f\"{'Modelo':16} {'F1 Test':>9} {'F1 Train':>9} \"\n", " f\"{'TrTe gap':>9} {'CV Mean':>9} {'CV Std':>8} \"\n", " f\"{'CV-Te gap':>10} {'FP':>4} {'FN':>4} {'Rubrica':>12}\")\n", "print('-' * 100)\n", "for name, row in comp_df.iterrows():\n", " cv_gap_str = f\"{row['cv_test_gap_pp']:.2f}\" if row['cv_test_gap_pp'] else 'N/A'\n", " cv_mean_str = f\"{row['cv_mean']:.4f}\" if row['cv_mean'] else 'N/A'\n", " cv_std_str = f\"{row['cv_std']:.4f}\" if row['cv_std'] else 'N/A'\n", " print(f\" {name:14} {row['f1_test']:>9.4f} {row['f1_train']:>9.4f} \"\n", " f\"{row['train_test_gap_pp']:>9.2f} {cv_mean_str:>9} {cv_std_str:>8} \"\n", " f\"{cv_gap_str:>10} {row['fp']:>4} {row['fn']:>4} {row['rubrica']:>12}\")\n", "\n", "best_name = comp_df['f1_test'].idxmax()\n", "print(f'\\nGANADOR (F1 test): {best_name}')\n", "print(f\" F1 test : {comp_df.loc[best_name, 'f1_test']:.4f}\")\n", "print(f\" cv_test_gap : {comp_df.loc[best_name, 'cv_test_gap_pp']:.2f}pp\")\n", "print(f\" train_test_gap: {comp_df.loc[best_name, 'train_test_gap_pp']:.2f}pp\")" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "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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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, axes = plt.subplots(1, 2, figsize=(14, 6))\n", "\n", "models_list = list(comp_df.index)\n", "x = np.arange(len(models_list))\n", "w = 0.35\n", "c_tr = ['#534AB7','#7F77DD','#0F6E56','#5DCAA5','#993C1D']\n", "c_te = ['#9B96E8','#B8B4F0','#5DCAA5','#9FE1CB','#E8593C']\n", "\n", "axes[0].bar(x-w/2, comp_df['f1_train'], w, label='Train', color=c_tr, alpha=0.85)\n", "axes[0].bar(x+w/2, comp_df['f1_test'], w, label='Test', color=c_te, alpha=0.85)\n", "axes[0].axhline(0.75, color='gray', linestyle='--', alpha=0.4)\n", "axes[0].set_title('F1 Train vs Test', fontweight='bold')\n", "axes[0].set_xticks(x)\n", "axes[0].set_xticklabels([m.replace(' ','\\n') for m in models_list], fontsize=8)\n", "axes[0].set_ylim(0.5, 1.0)\n", "axes[0].legend()\n", "\n", "# GAP comparativo\n", "cv_gaps = [r['cv_test_gap_pp'] if r['cv_test_gap_pp'] else 0\n", " for _, r in comp_df.iterrows()]\n", "tr_gaps = comp_df['train_test_gap_pp'].tolist()\n", "x2 = np.arange(len(models_list))\n", "axes[1].bar(x2-w/2, tr_gaps, w, label='Train-Test gap', color='#E8593C', alpha=0.7)\n", "axes[1].bar(x2+w/2, cv_gaps, w, label='CV-Test gap', color='#5DCAA5', alpha=0.7)\n", "axes[1].axhline(5, color='red', linestyle='--', lw=1.5, label='Límite 5pp')\n", "axes[1].set_title('Comparativa gaps (pp) — verde = rubrica correcta', fontweight='bold')\n", "axes[1].set_xticks(x2)\n", "axes[1].set_xticklabels([m.replace(' ','\\n') for m in models_list], fontsize=8)\n", "axes[1].legend(fontsize=9)\n", "\n", "plt.tight_layout()\n", "plt.savefig(PROJECT_ROOT / 'reports' / 'v2' / '14_optuna_comparativa.png',\n", " dpi=150, bbox_inches='tight')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8. Guardar ganador y best_params.yaml" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Modelo guardado: /mnt/c/Users/under/Documents/F5/3_Projects/Project_9_Equipo3/Project_YT/models/final_model.joblib\n", "best_params.yaml guardado\n", "winner: LR tuned\n", "hyperparameters:\n", " ngram_range: '1_2'\n", " max_features: 4045\n", " min_df: 2\n", " sublinear_tf: false\n", " C: 0.3235215031170205\n", "results:\n", " f1_test: 0.7579\n", " f1_train: 0.8987\n", " train_test_gap_pp: 14.07\n", " cv_test_gap_pp: 4.76\n", " roc_auc: 0.81\n", " fp: 18\n", " fn: 30\n", "\n" ] } ], "source": [ "pipeline_map = {\n", " 'LR baseline': lr_baseline_pipe,\n", " 'LR tuned' : lr_tuned_pipe,\n", " 'RF baseline': rf_baseline_pipe,\n", " 'RF tuned' : rf_tuned_pipe,\n", " 'LinearSVC' : svc_pipeline,\n", "}\n", "best_pipeline = pipeline_map[best_name]\n", "\n", "MODELS_DIR = PROJECT_ROOT / 'models'\n", "MODELS_DIR.mkdir(exist_ok=True)\n", "model_path = MODELS_DIR / 'final_model.joblib'\n", "joblib.dump(best_pipeline, model_path)\n", "print(f'Modelo guardado: {model_path}')\n", "\n", "best_row = comp_df.loc[best_name]\n", "best_trial_params = {}\n", "if 'LR tuned' == best_name:\n", " best_trial_params = study_lr.best_trial.params\n", "elif 'RF tuned' == best_name:\n", " best_trial_params = study_rf.best_trial.params\n", "\n", "best_out = {\n", " 'winner' : best_name,\n", " 'hyperparameters' : best_trial_params,\n", " 'results': {\n", " 'f1_test' : float(best_row['f1_test']),\n", " 'f1_train' : float(best_row['f1_train']),\n", " 'train_test_gap_pp': float(best_row['train_test_gap_pp']),\n", " 'cv_test_gap_pp' : float(best_row['cv_test_gap_pp'])\n", " if best_row['cv_test_gap_pp'] is not None else None,\n", " 'roc_auc' : float(best_row['roc_auc']),\n", " 'fp' : int(best_row['fp']),\n", " 'fn' : int(best_row['fn']),\n", " }\n", "}\n", "\n", "import yaml\n", "best_path = PROJECT_ROOT / 'configs' / 'best_params.yaml'\n", "with open(best_path, 'w') as f:\n", " yaml.dump(best_out, f, default_flow_style=False, sort_keys=False)\n", "print(f'best_params.yaml guardado')\n", "with open(best_path) as f: print(f.read())" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Verificacion final_model.joblib:\n", " ✅ [TOXICO 0.70] you are a stupid thug get out\n", " ✅ [NO TOXICO 0.45] I think the police should be more transparent\n", " ✅ [TOXICO 0.56] black people are criminal thugs\n", " ✅ [NO TOXICO 0.31] thank you for sharing this video\n" ] } ], "source": [ "# Verificacion\n", "loaded = joblib.load(model_path)\n", "tests = [\n", " ('you are a stupid thug get out', True),\n", " ('I think the police should be more transparent', False),\n", " ('black people are criminal thugs', True),\n", " ('thank you for sharing this video', False),\n", "]\n", "print('Verificacion final_model.joblib:')\n", "for text, expected in tests:\n", " pred = loaded.predict([text])[0]\n", " prob = loaded.predict_proba([text])[0][1]\n", " ok = '✅' if pred == expected else '❌'\n", " print(f' {ok} [{\"TOXICO\" if pred else \"NO TOXICO\"} {prob:.2f}] {text[:55]}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 9. Registro en MLflow" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2026/05/18 10:12:46 WARNING mlflow.models.model: `artifact_path` is deprecated. Please use `name` instead.\n", "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" ] }, { "name": "stdout", "output_type": "stream", "text": [ " ✅ lr_tuned_optuna\n", " ✅ rf_tuned_optuna\n", " ✅ linear_svc\n" ] } ], "source": [ "MLFLOW_DIR = PROJECT_ROOT / 'mlruns'\n", "mlflow.set_tracking_uri(f'file://{MLFLOW_DIR}')\n", "mlflow.set_experiment('Youtube_project_experiment')\n", "\n", "runs_info = [\n", " ('lr_tuned_optuna', study_lr, metrics_lr_tuned, lr_tuned_pipe,\n", " {'model':'LR','optuna_trials':N_TRIALS}),\n", " ('rf_tuned_optuna', study_rf, metrics_rf_tuned, rf_tuned_pipe,\n", " {'model':'RF','optuna_trials':N_TRIALS}),\n", " ('linear_svc', None, metrics_svc, svc_pipeline,\n", " {'model':'LinearSVC','C':1.0}),\n", "]\n", "\n", "for run_name, study, mets, pipe, extra in runs_info:\n", " with mlflow.start_run(run_name=run_name):\n", " for k,v in extra.items(): mlflow.log_param(k, v)\n", " if study:\n", " mlflow.log_param('best_trial', study.best_trial.number)\n", " for k,v in study.best_trial.params.items(): mlflow.log_param(k, v)\n", " mlflow.log_metric('test_f1', mets['f1_test'])\n", " mlflow.log_metric('train_f1', mets['f1_train'])\n", " mlflow.log_metric('train_test_gap_pp', mets['train_test_gap_pp'])\n", " if mets['cv_test_gap_pp'] is not None:\n", " mlflow.log_metric('cv_mean', mets['cv_mean'])\n", " mlflow.log_metric('cv_test_gap_pp', mets['cv_test_gap_pp'])\n", " mlflow.log_metric('roc_auc', mets['roc_auc'])\n", " if run_name.split('_')[0].upper() in best_name.upper() and 'tuned' in best_name.lower():\n", " mlflow.sklearn.log_model(pipe, 'final_model')\n", " print(f' ✅ {run_name}')\n", "mlflow.log_artifact(str(PROJECT_ROOT / 'reports' / 'v2' / '14_optuna_comparativa.png'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 10. Conclusiones" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "CONCLUSIONES — OPTIMIZACION + LinearSVC\n", "=======================================================\n", "5 modelos evaluados bajo las mismas condiciones.\n", "\n", "Mejora Optuna:\n", " LR: +0.48pp F1 test\n", " RF: -6.07pp F1 test\n", "\n", "Ganador: LR tuned\n", " F1 test : 0.7579\n", " train-test gap : 14.07pp\n", " cv-test gap : 4.76pp\n", "\n", "Nota metodologica:\n", " El train-test gap esta inflado por ser in-sample vs OOS.\n", " El cv-test gap compara OOS vs OOS — es la metrica correcta\n", " para la rubrica. Ver informe metodologico adjunto.\n", "\n", "Siguiente: 07_data_augmentation.ipynb\n", " Explorar si augmentation mejora los FN sin aumentar FP.\n", "\n" ] } ], "source": [ "lr_delta = metrics_lr_tuned['f1_test'] - metrics_lr_base['f1_test']\n", "rf_delta = metrics_rf_tuned['f1_test'] - metrics_rf_base['f1_test']\n", "\n", "print(f\"\"\"\n", "CONCLUSIONES — OPTIMIZACION + LinearSVC\n", "{'='*55}\n", "5 modelos evaluados bajo las mismas condiciones.\n", "\n", "Mejora Optuna:\n", " LR: {lr_delta*100:+.2f}pp F1 test\n", " RF: {rf_delta*100:+.2f}pp F1 test\n", "\n", "Ganador: {best_name}\n", " F1 test : {comp_df.loc[best_name, 'f1_test']:.4f}\n", " train-test gap : {comp_df.loc[best_name, 'train_test_gap_pp']:.2f}pp\n", " cv-test gap : {comp_df.loc[best_name, 'cv_test_gap_pp']:.2f}pp\n", "\n", "Nota metodologica:\n", " El train-test gap esta inflado por ser in-sample vs OOS.\n", " El cv-test gap compara OOS vs OOS — es la metrica correcta\n", " para la rubrica. Ver informe metodologico adjunto.\n", "\n", "Siguiente: 07_data_augmentation.ipynb\n", " Explorar si augmentation mejora los FN sin aumentar FP.\n", "\"\"\")" ] } ], "metadata": { "kernelspec": { "display_name": "py310", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.20" } }, "nbformat": 4, "nbformat_minor": 4 }