{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "a1586457-0f5d-4325-894f-255dc3dcffcc",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os, warnings, glob\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "from sklearn.model_selection import train_test_split, StratifiedKFold, GridSearchCV\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score,\n",
    "                             roc_auc_score, classification_report, confusion_matrix, RocCurveDisplay)\n",
    "from sklearn.feature_selection import SelectKBest, f_classif, SelectFromModel\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier\n",
    "from sklearn.svm import SVC\n",
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "import joblib\n",
    "\n",
    "RANDOM_STATE = 42\n",
    "np.random.seed(RANDOM_STATE)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "c97bcafa-7aed-44f9-9f08-a07c49312d1a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(569, 32)\n",
      "target\n",
      "0    357\n",
      "1    212\n",
      "Name: count, dtype: int64\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>diagnosis</th>\n",
       "      <th>radius_mean</th>\n",
       "      <th>texture_mean</th>\n",
       "      <th>perimeter_mean</th>\n",
       "      <th>area_mean</th>\n",
       "      <th>smoothness_mean</th>\n",
       "      <th>compactness_mean</th>\n",
       "      <th>concavity_mean</th>\n",
       "      <th>concave points_mean</th>\n",
       "      <th>symmetry_mean</th>\n",
       "      <th>...</th>\n",
       "      <th>texture_worst</th>\n",
       "      <th>perimeter_worst</th>\n",
       "      <th>area_worst</th>\n",
       "      <th>smoothness_worst</th>\n",
       "      <th>compactness_worst</th>\n",
       "      <th>concavity_worst</th>\n",
       "      <th>concave points_worst</th>\n",
       "      <th>symmetry_worst</th>\n",
       "      <th>fractal_dimension_worst</th>\n",
       "      <th>target</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>M</td>\n",
       "      <td>17.99</td>\n",
       "      <td>10.38</td>\n",
       "      <td>122.80</td>\n",
       "      <td>1001.0</td>\n",
       "      <td>0.11840</td>\n",
       "      <td>0.27760</td>\n",
       "      <td>0.3001</td>\n",
       "      <td>0.14710</td>\n",
       "      <td>0.2419</td>\n",
       "      <td>...</td>\n",
       "      <td>17.33</td>\n",
       "      <td>184.60</td>\n",
       "      <td>2019.0</td>\n",
       "      <td>0.1622</td>\n",
       "      <td>0.6656</td>\n",
       "      <td>0.7119</td>\n",
       "      <td>0.2654</td>\n",
       "      <td>0.4601</td>\n",
       "      <td>0.11890</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>M</td>\n",
       "      <td>20.57</td>\n",
       "      <td>17.77</td>\n",
       "      <td>132.90</td>\n",
       "      <td>1326.0</td>\n",
       "      <td>0.08474</td>\n",
       "      <td>0.07864</td>\n",
       "      <td>0.0869</td>\n",
       "      <td>0.07017</td>\n",
       "      <td>0.1812</td>\n",
       "      <td>...</td>\n",
       "      <td>23.41</td>\n",
       "      <td>158.80</td>\n",
       "      <td>1956.0</td>\n",
       "      <td>0.1238</td>\n",
       "      <td>0.1866</td>\n",
       "      <td>0.2416</td>\n",
       "      <td>0.1860</td>\n",
       "      <td>0.2750</td>\n",
       "      <td>0.08902</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>M</td>\n",
       "      <td>19.69</td>\n",
       "      <td>21.25</td>\n",
       "      <td>130.00</td>\n",
       "      <td>1203.0</td>\n",
       "      <td>0.10960</td>\n",
       "      <td>0.15990</td>\n",
       "      <td>0.1974</td>\n",
       "      <td>0.12790</td>\n",
       "      <td>0.2069</td>\n",
       "      <td>...</td>\n",
       "      <td>25.53</td>\n",
       "      <td>152.50</td>\n",
       "      <td>1709.0</td>\n",
       "      <td>0.1444</td>\n",
       "      <td>0.4245</td>\n",
       "      <td>0.4504</td>\n",
       "      <td>0.2430</td>\n",
       "      <td>0.3613</td>\n",
       "      <td>0.08758</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>M</td>\n",
       "      <td>11.42</td>\n",
       "      <td>20.38</td>\n",
       "      <td>77.58</td>\n",
       "      <td>386.1</td>\n",
       "      <td>0.14250</td>\n",
       "      <td>0.28390</td>\n",
       "      <td>0.2414</td>\n",
       "      <td>0.10520</td>\n",
       "      <td>0.2597</td>\n",
       "      <td>...</td>\n",
       "      <td>26.50</td>\n",
       "      <td>98.87</td>\n",
       "      <td>567.7</td>\n",
       "      <td>0.2098</td>\n",
       "      <td>0.8663</td>\n",
       "      <td>0.6869</td>\n",
       "      <td>0.2575</td>\n",
       "      <td>0.6638</td>\n",
       "      <td>0.17300</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>M</td>\n",
       "      <td>20.29</td>\n",
       "      <td>14.34</td>\n",
       "      <td>135.10</td>\n",
       "      <td>1297.0</td>\n",
       "      <td>0.10030</td>\n",
       "      <td>0.13280</td>\n",
       "      <td>0.1980</td>\n",
       "      <td>0.10430</td>\n",
       "      <td>0.1809</td>\n",
       "      <td>...</td>\n",
       "      <td>16.67</td>\n",
       "      <td>152.20</td>\n",
       "      <td>1575.0</td>\n",
       "      <td>0.1374</td>\n",
       "      <td>0.2050</td>\n",
       "      <td>0.4000</td>\n",
       "      <td>0.1625</td>\n",
       "      <td>0.2364</td>\n",
       "      <td>0.07678</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 32 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "  diagnosis  radius_mean  texture_mean  perimeter_mean  area_mean  \\\n",
       "0         M        17.99         10.38          122.80     1001.0   \n",
       "1         M        20.57         17.77          132.90     1326.0   \n",
       "2         M        19.69         21.25          130.00     1203.0   \n",
       "3         M        11.42         20.38           77.58      386.1   \n",
       "4         M        20.29         14.34          135.10     1297.0   \n",
       "\n",
       "   smoothness_mean  compactness_mean  concavity_mean  concave points_mean  \\\n",
       "0          0.11840           0.27760          0.3001              0.14710   \n",
       "1          0.08474           0.07864          0.0869              0.07017   \n",
       "2          0.10960           0.15990          0.1974              0.12790   \n",
       "3          0.14250           0.28390          0.2414              0.10520   \n",
       "4          0.10030           0.13280          0.1980              0.10430   \n",
       "\n",
       "   symmetry_mean  ...  texture_worst  perimeter_worst  area_worst  \\\n",
       "0         0.2419  ...          17.33           184.60      2019.0   \n",
       "1         0.1812  ...          23.41           158.80      1956.0   \n",
       "2         0.2069  ...          25.53           152.50      1709.0   \n",
       "3         0.2597  ...          26.50            98.87       567.7   \n",
       "4         0.1809  ...          16.67           152.20      1575.0   \n",
       "\n",
       "   smoothness_worst  compactness_worst  concavity_worst  concave points_worst  \\\n",
       "0            0.1622             0.6656           0.7119                0.2654   \n",
       "1            0.1238             0.1866           0.2416                0.1860   \n",
       "2            0.1444             0.4245           0.4504                0.2430   \n",
       "3            0.2098             0.8663           0.6869                0.2575   \n",
       "4            0.1374             0.2050           0.4000                0.1625   \n",
       "\n",
       "   symmetry_worst  fractal_dimension_worst  target  \n",
       "0          0.4601                  0.11890       1  \n",
       "1          0.2750                  0.08902       1  \n",
       "2          0.3613                  0.08758       1  \n",
       "3          0.6638                  0.17300       1  \n",
       "4          0.2364                  0.07678       1  \n",
       "\n",
       "[5 rows x 32 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>missing</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>diagnosis</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>radius_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>texture_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>perimeter_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>area_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>smoothness_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>compactness_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>concavity_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>concave points_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>symmetry_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>fractal_dimension_mean</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>radius_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>texture_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>perimeter_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>area_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>smoothness_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>compactness_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>concavity_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>concave points_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>symmetry_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>fractal_dimension_se</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>radius_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>texture_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>perimeter_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>area_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>smoothness_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>compactness_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>concavity_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>concave points_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>symmetry_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>fractal_dimension_worst</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>target</th>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                         missing\n",
       "diagnosis                      0\n",
       "radius_mean                    0\n",
       "texture_mean                   0\n",
       "perimeter_mean                 0\n",
       "area_mean                      0\n",
       "smoothness_mean                0\n",
       "compactness_mean               0\n",
       "concavity_mean                 0\n",
       "concave points_mean            0\n",
       "symmetry_mean                  0\n",
       "fractal_dimension_mean         0\n",
       "radius_se                      0\n",
       "texture_se                     0\n",
       "perimeter_se                   0\n",
       "area_se                        0\n",
       "smoothness_se                  0\n",
       "compactness_se                 0\n",
       "concavity_se                   0\n",
       "concave points_se              0\n",
       "symmetry_se                    0\n",
       "fractal_dimension_se           0\n",
       "radius_worst                   0\n",
       "texture_worst                  0\n",
       "perimeter_worst                0\n",
       "area_worst                     0\n",
       "smoothness_worst               0\n",
       "compactness_worst              0\n",
       "concavity_worst                0\n",
       "concave points_worst           0\n",
       "symmetry_worst                 0\n",
       "fractal_dimension_worst        0\n",
       "target                         0"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "\n",
    "CSV_PATH = \"Breast Cancer Wisconsin.csv\"\n",
    "assert os.path.exists(CSV_PATH), f\"{CSV_PATH} .\"\n",
    "\n",
    "df = pd.read_csv(CSV_PATH)\n",
    "\n",
    "for c in [\"id\", \"ID\", \"Unnamed: 32\", \"Unnamed: 0\"]:\n",
    "    if c in df.columns:\n",
    "        df.drop(columns=[c], inplace=True)\n",
    "\n",
    "\n",
    "assert \"diagnosis\" in df.columns, \" diagnosis  .\"\n",
    "df[\"target\"] = df[\"diagnosis\"].map({\"M\":1, \"B\":0})\n",
    "assert df[\"target\"].isin([0,1]).all(), \" (M/B).\"\n",
    "\n",
    "print(df.shape)\n",
    "print(df[\"target\"].value_counts())\n",
    "display(df.head())\n",
    "display(df.isna().sum().to_frame(\"missing\"))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "7bd238ff-f01e-448f-8a1e-70c522be9354",
   "metadata": {},
   "outputs": [],
   "source": [
    "X = df.drop(columns=[\"diagnosis\", \"target\"])\n",
    "y = df[\"target\"]\n",
    "\n",
    "num_cols = X.select_dtypes(include=[np.number]).columns.tolist()\n",
    "cat_cols = X.select_dtypes(exclude=[np.number]).columns.tolist()\n",
    "\n",
    "numeric_pipe = Pipeline([\n",
    "    (\"imputer\", SimpleImputer(strategy=\"median\")),\n",
    "    (\"scaler\", StandardScaler())\n",
    "])\n",
    "categorical_pipe = Pipeline([\n",
    "    (\"imputer\", SimpleImputer(strategy=\"most_frequent\")),\n",
    "    (\"oh\", OneHotEncoder(handle_unknown=\"ignore\"))\n",
    "])\n",
    "\n",
    "preprocessor = ColumnTransformer(\n",
    "    transformers=[\n",
    "        (\"num\", numeric_pipe, num_cols),\n",
    "        (\"cat\", categorical_pipe, cat_cols)\n",
    "    ],\n",
    "    remainder=\"drop\"\n",
    ")\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    X, y, test_size=0.2, stratify=y, random_state=RANDOM_STATE\n",
    ")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "3bc0359b-d471-49d5-b3b3-947a79f0465b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "[logreg] best params: {'model__C': 0.1}\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.99      1.00      0.99        72\n",
      "           1       1.00      0.98      0.99        42\n",
      "\n",
      "    accuracy                           0.99       114\n",
      "   macro avg       0.99      0.99      0.99       114\n",
      "weighted avg       0.99      0.99      0.99       114\n",
      "\n",
      "\n",
      "[rf] best params: {'model__max_depth': None, 'model__n_estimators': 200}\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.95      1.00      0.97        72\n",
      "           1       1.00      0.90      0.95        42\n",
      "\n",
      "    accuracy                           0.96       114\n",
      "   macro avg       0.97      0.95      0.96       114\n",
      "weighted avg       0.97      0.96      0.96       114\n",
      "\n",
      "\n",
      "[gb] best params: {'model__learning_rate': 0.05, 'model__n_estimators': 250}\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.95      1.00      0.97        72\n",
      "           1       1.00      0.90      0.95        42\n",
      "\n",
      "    accuracy                           0.96       114\n",
      "   macro avg       0.97      0.95      0.96       114\n",
      "weighted avg       0.97      0.96      0.96       114\n",
      "\n",
      "\n",
      "[svc] best params: {'model__C': 10, 'model__gamma': 'scale'}\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.96      1.00      0.98        72\n",
      "           1       1.00      0.93      0.96        42\n",
      "\n",
      "    accuracy                           0.97       114\n",
      "   macro avg       0.98      0.96      0.97       114\n",
      "weighted avg       0.97      0.97      0.97       114\n",
      "\n",
      "\n",
      "[knn] best params: {'model__n_neighbors': 3}\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.92      0.99      0.95        72\n",
      "           1       0.97      0.86      0.91        42\n",
      "\n",
      "    accuracy                           0.94       114\n",
      "   macro avg       0.95      0.92      0.93       114\n",
      "weighted avg       0.94      0.94      0.94       114\n",
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>model</th>\n",
       "      <th>cv_best_f1</th>\n",
       "      <th>test_acc</th>\n",
       "      <th>test_prec</th>\n",
       "      <th>test_rec</th>\n",
       "      <th>test_f1</th>\n",
       "      <th>test_auc</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>logreg</td>\n",
       "      <td>0.967146</td>\n",
       "      <td>0.991228</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.976190</td>\n",
       "      <td>0.987952</td>\n",
       "      <td>0.997685</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>svc</td>\n",
       "      <td>0.961858</td>\n",
       "      <td>0.973684</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.928571</td>\n",
       "      <td>0.962963</td>\n",
       "      <td>0.992725</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>rf</td>\n",
       "      <td>0.948876</td>\n",
       "      <td>0.964912</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.904762</td>\n",
       "      <td>0.950000</td>\n",
       "      <td>0.994213</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>gb</td>\n",
       "      <td>0.960994</td>\n",
       "      <td>0.964912</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.904762</td>\n",
       "      <td>0.950000</td>\n",
       "      <td>0.993056</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>knn</td>\n",
       "      <td>0.963263</td>\n",
       "      <td>0.938596</td>\n",
       "      <td>0.972973</td>\n",
       "      <td>0.857143</td>\n",
       "      <td>0.911392</td>\n",
       "      <td>0.982474</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    model  cv_best_f1  test_acc  test_prec  test_rec   test_f1  test_auc\n",
       "0  logreg    0.967146  0.991228   1.000000  0.976190  0.987952  0.997685\n",
       "3     svc    0.961858  0.973684   1.000000  0.928571  0.962963  0.992725\n",
       "1      rf    0.948876  0.964912   1.000000  0.904762  0.950000  0.994213\n",
       "2      gb    0.960994  0.964912   1.000000  0.904762  0.950000  0.993056\n",
       "4     knn    0.963263  0.938596   0.972973  0.857143  0.911392  0.982474"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE)\n",
    "\n",
    "models = {\n",
    "    \"logreg\": (LogisticRegression(max_iter=500, class_weight=\"balanced\", random_state=RANDOM_STATE),\n",
    "               {\"model__C\": [0.01, 0.1, 1, 10]}),\n",
    "    \"rf\":     (RandomForestClassifier(random_state=RANDOM_STATE),\n",
    "               {\"model__n_estimators\": [200, 400],\n",
    "                \"model__max_depth\": [None, 5, 10]}),\n",
    "    \"gb\":     (GradientBoostingClassifier(random_state=RANDOM_STATE),\n",
    "               {\"model__n_estimators\": [150, 250],\n",
    "                \"model__learning_rate\": [0.05, 0.1]}),\n",
    "    \"svc\":    (SVC(probability=True, class_weight=\"balanced\", random_state=RANDOM_STATE),\n",
    "               {\"model__C\": [0.1, 1, 10],\n",
    "                \"model__gamma\": [\"scale\", \"auto\"]}),\n",
    "    \"knn\":    (KNeighborsClassifier(),\n",
    "               {\"model__n_neighbors\": [3,5,7,9]})\n",
    "}\n",
    "\n",
    "results = []\n",
    "best_estimators = {}\n",
    "\n",
    "for name, (estimator, grid) in models.items():\n",
    "    pipe = Pipeline([(\"prep\", preprocessor), (\"model\", estimator)])\n",
    "    gs = GridSearchCV(pipe, param_grid=grid, scoring=\"f1\", cv=cv, n_jobs=-1, refit=True)\n",
    "    gs.fit(X_train, y_train)\n",
    "    best_estimators[name] = gs.best_estimator_\n",
    "    \n",
    "    y_pred = gs.best_estimator_.predict(X_test)\n",
    "    y_prob = gs.best_estimator_.predict_proba(X_test)[:,1]\n",
    "    results.append({\n",
    "        \"model\": name,\n",
    "        \"cv_best_f1\": gs.best_score_,\n",
    "        \"test_acc\": accuracy_score(y_test, y_pred),\n",
    "        \"test_prec\": precision_score(y_test, y_pred),\n",
    "        \"test_rec\": recall_score(y_test, y_pred),\n",
    "        \"test_f1\": f1_score(y_test, y_pred),\n",
    "        \"test_auc\": roc_auc_score(y_test, y_prob)\n",
    "    })\n",
    "    print(f\"\\n[{name}] best params:\", gs.best_params_)\n",
    "    print(classification_report(y_test, y_pred))\n",
    "\n",
    "results_df = pd.DataFrame(results).sort_values(\"test_f1\", ascending=False)\n",
    "display(results_df)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "d26970d4-5a1b-4fb4-bc0c-48008f5583cd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['logreg', 'svc']"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "top2 = results_df.head(2)[\"model\"].tolist()\n",
    "top2\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "6b1d3226-f2a8-423f-b4c6-e7ac53ba10fe",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "KBest best params: {'select__k': 'all'}\n",
      "SFM best params: {'select__max_features': 10, 'select__threshold': '1.0*mean'}\n"
     ]
    }
   ],
   "source": [
    "# (أ) SelectKBest\n",
    "kbest_pipe = Pipeline([\n",
    "    (\"prep\", preprocessor),\n",
    "    (\"select\", SelectKBest(score_func=f_classif)),\n",
    "    (\"clf\", LogisticRegression(max_iter=500, class_weight=\"balanced\", random_state=RANDOM_STATE))\n",
    "])\n",
    "param_kbest = {\"select__k\": [5, 10, 15, \"all\"]}\n",
    "gs_kbest = GridSearchCV(kbest_pipe, param_kbest, scoring=\"f1\", cv=cv, n_jobs=-1)\n",
    "gs_kbest.fit(X_train, y_train)\n",
    "best_kbest = gs_kbest.best_estimator_\n",
    "print(\"KBest best params:\", gs_kbest.best_params_)\n",
    "\n",
    "# (ب) SelectFromModel بـ Logistic L1\n",
    "base_selector = LogisticRegression(\n",
    "    penalty=\"l1\", solver=\"liblinear\", class_weight=\"balanced\",\n",
    "    max_iter=1000, random_state=RANDOM_STATE\n",
    ")\n",
    "sfm_pipe = Pipeline([\n",
    "    (\"prep\", preprocessor),\n",
    "    (\"select\", SelectFromModel(base_selector, prefit=False)),\n",
    "    (\"clf\", LogisticRegression(max_iter=500, class_weight=\"balanced\", random_state=RANDOM_STATE))\n",
    "])\n",
    "param_sfm = {\n",
    "    \"select__max_features\": [10, 15, 20, None],\n",
    "    \"select__threshold\": [\"median\", \"1.0*mean\", \"-inf\"]\n",
    "}\n",
    "gs_sfm = GridSearchCV(sfm_pipe, param_sfm, scoring=\"f1\", cv=cv, n_jobs=-1)\n",
    "gs_sfm.fit(X_train, y_train)\n",
    "best_sfm = gs_sfm.best_estimator_\n",
    "print(\"SFM best params:\", gs_sfm.best_params_)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "53dfcf68-6bef-40c5-a53e-66e69888a5c2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== logreg+KBest ===\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.99      1.00      0.99        72\n",
      "           1       1.00      0.98      0.99        42\n",
      "\n",
      "    accuracy                           0.99       114\n",
      "   macro avg       0.99      0.99      0.99       114\n",
      "weighted avg       0.99      0.99      0.99       114\n",
      "\n",
      "\n",
      "=== svc+KBest ===\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.96      1.00      0.98        72\n",
      "           1       1.00      0.93      0.96        42\n",
      "\n",
      "    accuracy                           0.97       114\n",
      "   macro avg       0.98      0.96      0.97       114\n",
      "weighted avg       0.97      0.97      0.97       114\n",
      "\n",
      "\n",
      "=== logreg+SFM ===\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.97      0.97      0.97        72\n",
      "           1       0.95      0.95      0.95        42\n",
      "\n",
      "    accuracy                           0.96       114\n",
      "   macro avg       0.96      0.96      0.96       114\n",
      "weighted avg       0.96      0.96      0.96       114\n",
      "\n",
      "\n",
      "=== svc+SFM ===\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.97      0.97      0.97        72\n",
      "           1       0.95      0.95      0.95        42\n",
      "\n",
      "    accuracy                           0.96       114\n",
      "   macro avg       0.96      0.96      0.96       114\n",
      "weighted avg       0.96      0.96      0.96       114\n",
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>setting</th>\n",
       "      <th>acc</th>\n",
       "      <th>prec</th>\n",
       "      <th>rec</th>\n",
       "      <th>f1</th>\n",
       "      <th>auc</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>logreg+KBest</td>\n",
       "      <td>0.991228</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.976190</td>\n",
       "      <td>0.987952</td>\n",
       "      <td>0.997685</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>svc+KBest</td>\n",
       "      <td>0.973684</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.928571</td>\n",
       "      <td>0.962963</td>\n",
       "      <td>0.992725</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>logreg+SFM</td>\n",
       "      <td>0.964912</td>\n",
       "      <td>0.952381</td>\n",
       "      <td>0.952381</td>\n",
       "      <td>0.952381</td>\n",
       "      <td>0.995701</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>svc+SFM</td>\n",
       "      <td>0.964912</td>\n",
       "      <td>0.952381</td>\n",
       "      <td>0.952381</td>\n",
       "      <td>0.952381</td>\n",
       "      <td>0.994048</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        setting       acc      prec       rec        f1       auc\n",
       "0  logreg+KBest  0.991228  1.000000  0.976190  0.987952  0.997685\n",
       "1     svc+KBest  0.973684  1.000000  0.928571  0.962963  0.992725\n",
       "2    logreg+SFM  0.964912  0.952381  0.952381  0.952381  0.995701\n",
       "3       svc+SFM  0.964912  0.952381  0.952381  0.952381  0.994048"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from copy import deepcopy\n",
    "\n",
    "def eval_pipe(pipe, Xtr, ytr, Xte, yte, tag):\n",
    "    pipe.fit(Xtr, ytr)\n",
    "    y_pred = pipe.predict(Xte)\n",
    "    y_prob = pipe.predict_proba(Xte)[:,1] if hasattr(pipe, \"predict_proba\") else None\n",
    "    row = {\n",
    "        \"setting\": tag,\n",
    "        \"acc\": accuracy_score(yte, y_pred),\n",
    "        \"prec\": precision_score(yte, y_pred),\n",
    "        \"rec\": recall_score(yte, y_pred),\n",
    "        \"f1\": f1_score(yte, y_pred),\n",
    "        \"auc\": roc_auc_score(yte, y_prob) if y_prob is not None else np.nan\n",
    "    }\n",
    "    print(f\"\\n=== {tag} ===\")\n",
    "    print(classification_report(yte, y_pred))\n",
    "    return row\n",
    "\n",
    "final_rows = []\n",
    "for sel_name, selector in [(\"KBest\", best_kbest.named_steps[\"select\"]),\n",
    "                           (\"SFM\",   best_sfm.named_steps[\"select\"])]:\n",
    "    for m in top2:\n",
    "        base = best_estimators[m]\n",
    "        new_model = deepcopy(base.named_steps[\"model\"])\n",
    "        final = Pipeline([\n",
    "            (\"prep\", preprocessor),\n",
    "            (\"select\", selector),\n",
    "            (\"model\", new_model)\n",
    "        ])\n",
    "        final_rows.append(eval_pipe(final, X_train, y_train, X_test, y_test, f\"{m}+{sel_name}\"))\n",
    "\n",
    "final_df = pd.DataFrame(final_rows).sort_values(\"f1\", ascending=False)\n",
    "display(final_df)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "cea73f27-683e-4f21-9077-b71306077fd4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Confusion Matrix:\n",
      " [[72  0]\n",
      " [ 1 41]]\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "best_row = final_df.iloc[0]\n",
    "best_name = best_row[\"setting\"]\n",
    "best_tag  = \"KBest\" if \"KBest\" in best_name else \"SFM\"\n",
    "best_model_key = best_name.split(\"+\")[0]\n",
    "\n",
    "selector = best_kbest.named_steps[\"select\"] if best_tag==\"KBest\" else best_sfm.named_steps[\"select\"]\n",
    "from copy import deepcopy\n",
    "new_model = deepcopy(best_estimators[best_model_key].named_steps[\"model\"])\n",
    "\n",
    "best_pipe = Pipeline([\n",
    "    (\"prep\", preprocessor),\n",
    "    (\"select\", selector),\n",
    "    (\"model\", new_model)\n",
    "])\n",
    "best_pipe.fit(X_train, y_train)\n",
    "\n",
    "y_pred = best_pipe.predict(X_test)\n",
    "y_prob = best_pipe.predict_proba(X_test)[:,1]\n",
    "\n",
    "cm = confusion_matrix(y_test, y_pred)\n",
    "print(\"Confusion Matrix:\\n\", cm)\n",
    "\n",
    "RocCurveDisplay.from_predictions(y_test, y_prob)\n",
    "plt.title(f\"ROC — {best_name}\")\n",
    "plt.show()\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
