{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":27989,"databundleVersionId":2281517},{"sourceType":"datasetVersion","sourceId":2215934,"datasetId":1330735,"databundleVersionId":2257579}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"m000","cell_type":"markdown","source":"# DDoS FL Detection — Binary Classification (Combined Dataset)\n\n---\n\n**Labels**: BENIGN (0) | DDoS (1) — all attack classes from both datasets mapped to DDoS\n**Data**: CICIDS2017 CSVs + Competition CSVs (combined, deduplicated)\n**Pipeline**: Load → Label normalise → Combine → EDA (14 sections) → Scale+SMOTE → 6-model benchmark → Tune → Threshold","metadata":{}},{"id":"m001","cell_type":"markdown","source":"---\n## 0. Environment Setup","metadata":{}},{"id":"c002","cell_type":"code","source":"import os, glob, warnings, time, subprocess, gc\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nimport seaborn as sns\nfrom matplotlib.colors import Normalize\nfrom matplotlib.cm import ScalarMappable\n\nfrom sklearn.model_selection import train_test_split, StratifiedKFold, RandomizedSearchCV\nfrom sklearn.preprocessing import RobustScaler\nfrom sklearn.neighbors import KNeighborsClassifier\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.naive_bayes import GaussianNB\nfrom sklearn.metrics import (\n    classification_report, confusion_matrix, roc_auc_score, f1_score,\n    roc_curve, average_precision_score, precision_score, recall_score,\n    ConfusionMatrixDisplay\n)\nfrom imblearn.over_sampling import SMOTE\nfrom imblearn.under_sampling import RandomUnderSampler\nfrom imblearn.pipeline import Pipeline as ImbPipeline\nfrom scipy.stats import randint, uniform, ks_2samp, f_oneway\nfrom collections import Counter\n\nimport lightgbm as lgb\nfrom catboost import CatBoostClassifier\nimport xgboost as xgb\n\nwarnings.filterwarnings('ignore')\npd.set_option('display.max_columns', 90)\npd.set_option('display.float_format', '{:.4f}'.format)\n\nSEED = 42\nnp.random.seed(SEED)\nPALETTE = sns.color_palette('Set2', 12)\nsns.set_theme(style='darkgrid', palette=PALETTE)\nplt.rcParams.update({'figure.dpi': 120, 'font.size': 11})\n\nMAX_ROWS_PER_SOURCE = 500_000   # lower to 250_000 if OOM persists\n\n# ── GPU detection ─────────────────────────────────────────────────────────────\ndef _has_gpu():\n    try:\n        r = subprocess.run(['nvidia-smi'], capture_output=True, timeout=5)\n        return r.returncode == 0\n    except Exception:\n        return False\n\nHAS_GPU = _has_gpu()\nprint(f'GPU available        : {HAS_GPU}')\n\nUSE_CUML = False\nif HAS_GPU:\n    try:\n        from cuml.neighbors  import KNeighborsClassifier  as cuKNN\n        from cuml.ensemble   import RandomForestClassifier as cuRF\n        from cuml.naive_bayes import GaussianNB            as cuGNB\n        USE_CUML = True\n        print('cuML loaded          : KNN / RF / NB → GPU')\n    except ImportError:\n        print('cuML absent          : KNN / RF / NB → CPU sklearn')\nelse:\n    print('No GPU               : all sklearn models → CPU')\n\nlgbm_gpu = {'device_type': 'gpu'} if HAS_GPU else {}\nif HAS_GPU: print('LightGBM             : device_type=gpu')\n\nxgb_gpu = {}\nif HAS_GPU:\n    try:\n        xgb_gpu = {'device':'cuda'} if int(xgb.__version__.split('.')[0])>=2 else {'tree_method':'gpu_hist'}\n        print(f'XGBoost              : {xgb_gpu}')\n    except Exception: pass\n\ncat_gpu = {'task_type': 'GPU'} if HAS_GPU else {}\nif HAS_GPU: print('CatBoost             : task_type=GPU')\n\nprint('\\nEnvironment ready.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:02:24.692736Z","iopub.execute_input":"2026-04-30T22:02:24.693516Z","iopub.status.idle":"2026-04-30T22:02:38.136455Z","shell.execute_reply.started":"2026-04-30T22:02:24.693483Z","shell.execute_reply":"2026-04-30T22:02:38.135851Z"}},"outputs":[],"execution_count":null},{"id":"m003","cell_type":"markdown","source":"---\n## 1. Data Ingestion","metadata":{}},{"id":"c004","cell_type":"code","source":"CICIDS_DIR = '/kaggle/input/datasets/mdalamintalukder/cicids2017/MachineLearningCVE/'      # adjust slug if needed\nTRAIN_DIR  = '/kaggle/input/competitions/ddosfldetection/train/train/'\n\ncicids_files = sorted(glob.glob(os.path.join(CICIDS_DIR, '**/*.csv'), recursive=True))\nprint(f'CICIDS2017 files : {len(cicids_files)}')\n\ndfs = []\nfor f in cicids_files:\n    try:\n        tmp = pd.read_csv(f, low_memory=False)\n        tmp.columns = tmp.columns.str.strip()\n        tmp['_src'] = 'cicids'\n        dfs.append(tmp)\n    except Exception as e:\n        print(f'  [WARN] {os.path.basename(f)}: {e}')\n\ndf_cicids = pd.concat(dfs, ignore_index=True); del dfs\ndf_cicids.columns = df_cicids.columns.str.strip()\n\n_num = df_cicids.select_dtypes(include=[np.number]).columns\ndf_cicids[_num] = df_cicids[_num].astype(np.float32)\n\nprint(f'Raw shape        : {df_cicids.shape}')\nprint(f'Labels:\\n{df_cicids[\"Label\"].value_counts().to_string()}')\n\nif MAX_ROWS_PER_SOURCE and len(df_cicids) > MAX_ROWS_PER_SOURCE:\n    df_cicids = (df_cicids.groupby('Label', group_keys=False)\n                 .apply(lambda g: g.sample(min(len(g), max(1, int(MAX_ROWS_PER_SOURCE*len(g)/len(df_cicids)))), random_state=SEED))\n                 .reset_index(drop=True))\n    print(f'Subsampled to    : {len(df_cicids):,}')\ngc.collect()\nprint(f'Memory           : {df_cicids.memory_usage(deep=True).sum()/1e6:.0f} MB')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:02:38.137755Z","iopub.execute_input":"2026-04-30T22:02:38.138402Z","iopub.status.idle":"2026-04-30T22:03:19.224390Z","shell.execute_reply.started":"2026-04-30T22:02:38.138376Z","shell.execute_reply":"2026-04-30T22:03:19.223599Z"}},"outputs":[],"execution_count":null},{"id":"c005","cell_type":"code","source":"comp_files = sorted(glob.glob(os.path.join(TRAIN_DIR, '*.csv')))\nprint(f'Competition files : {len(comp_files)}')\n\ndfs = []\nfor f in comp_files:\n    try:\n        tmp = pd.read_csv(f, low_memory=False)\n        tmp.columns = tmp.columns.str.strip()\n        tmp['_src'] = 'comp'\n        dfs.append(tmp)\n    except Exception as e:\n        print(f'  [WARN] {os.path.basename(f)}: {e}')\n\ndf_comp = pd.concat(dfs, ignore_index=True); del dfs\ndf_comp.columns = df_comp.columns.str.strip()\n\n_num = df_comp.select_dtypes(include=[np.number]).columns\ndf_comp[_num] = df_comp[_num].astype(np.float32)\n\nprint(f'Raw shape         : {df_comp.shape}')\nprint(f'Labels:\\n{df_comp[\"Label\"].value_counts().to_string()}')\n\nif MAX_ROWS_PER_SOURCE and len(df_comp) > MAX_ROWS_PER_SOURCE:\n    df_comp = (df_comp.groupby('Label', group_keys=False)\n               .apply(lambda g: g.sample(min(len(g), max(1, int(MAX_ROWS_PER_SOURCE*len(g)/len(df_comp)))), random_state=SEED))\n               .reset_index(drop=True))\n    print(f'Subsampled to     : {len(df_comp):,}')\ngc.collect()\nprint(f'Memory            : {df_comp.memory_usage(deep=True).sum()/1e6:.0f} MB')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:03:19.225527Z","iopub.execute_input":"2026-04-30T22:03:19.225855Z","iopub.status.idle":"2026-04-30T22:04:12.808197Z","shell.execute_reply.started":"2026-04-30T22:03:19.225830Z","shell.execute_reply":"2026-04-30T22:04:12.807521Z"}},"outputs":[],"execution_count":null},{"id":"c006","cell_type":"code","source":"def normalize_label(df):\n    df = df.copy()\n    df['Label'] = df['Label'].astype(str).str.strip()\n    df['Label'] = df['Label'].apply(lambda x: 'BENIGN' if x.upper()=='BENIGN' else 'DDoS')\n    return df\n\ndf_cicids = normalize_label(df_cicids)\ndf_comp   = normalize_label(df_comp)\nprint('After normalisation:')\nprint('  CICIDS2017  :', df_cicids['Label'].value_counts().to_dict())\nprint('  Competition :', df_comp['Label'].value_counts().to_dict())\n\nEXCLUDE = {'Label','_src','source_file','Unnamed: 0','Unnamed: 0.1',\n           'Flow ID','Source IP','Source Port','Destination IP',\n           'Protocol','Timestamp','SimillarHTTP','Inbound'}\ncommon_cols = sorted((set(df_cicids.columns)-EXCLUDE) & (set(df_comp.columns)-EXCLUDE))\nprint(f'\\nCommon feature columns : {len(common_cols)}')\n\nkeep = common_cols + ['Label','_src']\ndf = pd.concat([df_cicids[keep], df_comp[keep]], ignore_index=True)\ndel df_cicids, df_comp; gc.collect()\n\nLABEL_COL = 'Label'\nprint(f'\\nCombined shape : {df.shape}')\nprint(f'Attack rate    : {(df[LABEL_COL]==\"DDoS\").mean():.2%}')\nprint(f'Memory         : {df.memory_usage(deep=True).sum()/1e6:.0f} MB')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:12.809845Z","iopub.execute_input":"2026-04-30T22:04:12.810482Z","iopub.status.idle":"2026-04-30T22:04:14.204801Z","shell.execute_reply.started":"2026-04-30T22:04:12.810458Z","shell.execute_reply":"2026-04-30T22:04:14.204094Z"}},"outputs":[],"execution_count":null},{"id":"c007","cell_type":"code","source":"def audit(df, name='df'):\n    print(f'[{name}]  rows={len(df):,}  cols={df.shape[1]}'\n          f'  NaN={df.isnull().sum().sum():,}'\n          f'  Inf={np.isinf(df.select_dtypes(include=[np.number])).sum().sum():,}'\n          f'  Dups={df.duplicated().sum():,}')\naudit(df, 'Combined')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:14.206084Z","iopub.execute_input":"2026-04-30T22:04:14.206435Z","iopub.status.idle":"2026-04-30T22:04:16.913674Z","shell.execute_reply.started":"2026-04-30T22:04:14.206409Z","shell.execute_reply":"2026-04-30T22:04:16.912925Z"}},"outputs":[],"execution_count":null},{"id":"m008","cell_type":"markdown","source":"---\n## 2. Data Cleaning","metadata":{}},{"id":"c009","cell_type":"code","source":"num_cols = df.select_dtypes(include=[np.number]).columns\ndf[num_cols] = df[num_cols].replace([np.inf,-np.inf], np.nan)\ndf[num_cols] = df[num_cols].fillna(df[num_cols].median())\n\nrows_before = len(df)\ndf = df.drop_duplicates(subset=[c for c in df.columns if c != '_src'])\nprint(f'Dups removed : {rows_before-len(df):,}')\n\nNON_FEAT = {LABEL_COL, '_src'}\nfeature_cols = [c for c in common_cols\n                if c not in NON_FEAT\n                and df[c].dtype in [np.float64, np.int64, np.float32, np.int32]]\nconst_cols = [c for c in feature_cols if df[c].nunique()<=1]\nfeature_cols = [c for c in feature_cols if c not in const_cols]\nif const_cols: df = df.drop(columns=const_cols)\n\nprint(f'Const cols dropped : {len(const_cols)}')\nprint(f'Feature cols       : {len(feature_cols)}')\nprint(f'Final shape        : {df.shape}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:16.914534Z","iopub.execute_input":"2026-04-30T22:04:16.914872Z","iopub.status.idle":"2026-04-30T22:04:22.781600Z","shell.execute_reply.started":"2026-04-30T22:04:16.914833Z","shell.execute_reply":"2026-04-30T22:04:22.780647Z"}},"outputs":[],"execution_count":null},{"id":"m010","cell_type":"markdown","source":"---\n## 3. Exploratory Data Analysis","metadata":{}},{"id":"c011","cell_type":"code","source":"# EDA subsample to avoid OOM on large combined datasets\nN_EDA = min(30_000, len(df))\ndf_e  = df.sample(N_EDA, random_state=SEED)\nprint(f'EDA sample : {N_EDA:,} rows')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:22.783140Z","iopub.execute_input":"2026-04-30T22:04:22.783394Z","iopub.status.idle":"2026-04-30T22:04:22.853788Z","shell.execute_reply.started":"2026-04-30T22:04:22.783361Z","shell.execute_reply":"2026-04-30T22:04:22.853018Z"}},"outputs":[],"execution_count":null},{"id":"m012","cell_type":"markdown","source":"### 3.1 Label Distribution","metadata":{}},{"id":"c013","cell_type":"code","source":"fig, axes = plt.subplots(1, 3, figsize=(20, 5))\n\nlc = df[LABEL_COL].value_counts()\nclrs = ['#2ecc71' if l=='BENIGN' else '#e74c3c' for l in lc.index]\nbars = axes[0].barh(lc.index, lc.values, color=clrs)\naxes[0].set_xlabel('Flow Count')\naxes[0].set_title('Label Distribution', fontweight='bold')\nfor bar, val in zip(bars, lc.values):\n    axes[0].text(val*1.01, bar.get_y()+bar.get_height()/2, f'{val:,}', va='center', fontsize=9)\n\nwedges, texts, ats = axes[1].pie(lc.values, labels=lc.index, autopct='%1.1f%%',\n    colors=['#2ecc71','#e74c3c'], startangle=90,\n    wedgeprops={'edgecolor':'white','linewidth':2})\naxes[1].set_title('Binary Class Balance', fontweight='bold')\nfor t in ats: t.set_fontsize(13); t.set_fontweight('bold')\n\nsl = df.groupby(['_src', LABEL_COL]).size().unstack(fill_value=0)\nsl = sl.reindex(columns=['BENIGN','DDoS'], fill_value=0)\nsl.plot(kind='bar', ax=axes[2], color=['#2ecc71','#e74c3c'], edgecolor='white', width=0.5)\naxes[2].set_title('Label Count by Source', fontweight='bold')\naxes[2].set_xticklabels(['Competition','CICIDS2017'], rotation=0)\naxes[2].legend(['BENIGN','DDoS'])\n\nplt.suptitle('Combined Dataset — Class Balance Overview', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('eda_01_labels.png', bbox_inches='tight', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:22.854778Z","iopub.execute_input":"2026-04-30T22:04:22.855124Z","iopub.status.idle":"2026-04-30T22:04:23.826496Z","shell.execute_reply.started":"2026-04-30T22:04:22.855089Z","shell.execute_reply":"2026-04-30T22:04:23.825534Z"}},"outputs":[],"execution_count":null},{"id":"m014","cell_type":"markdown","source":"### 3.2 Correlation Matrix\n\nHigh inter-feature correlations suggest redundancy. Gradient boosters handle it; KNN and NB suffer.","metadata":{}},{"id":"c015","cell_type":"code","source":"corr = df_e[feature_cols].corr()\n\nupper = corr.where(np.triu(np.ones(corr.shape),k=1).astype(bool))\nhi_pairs = [(c,r,upper.loc[r,c]) for c in upper.columns for r in upper.index\n            if pd.notna(upper.loc[r,c]) and abs(upper.loc[r,c])>0.95]\nprint(f'Pairs |r|>0.95 : {len(hi_pairs)}')\nfor c1,c2,r in hi_pairs[:8]:\n    print(f'  {c1:38s} <-> {c2:38s}  r={r:.3f}')\n\nfig, ax = plt.subplots(figsize=(18, 14))\nsns.heatmap(corr, mask=np.triu(np.ones_like(corr,dtype=bool)),\n            annot=False, cmap='RdBu_r', vmin=-1, vmax=1, center=0,\n            linewidths=0.2, ax=ax, cbar_kws={'shrink':0.6})\nax.set_title('Feature Correlation Matrix (lower triangle)', fontsize=13, fontweight='bold')\nax.tick_params(axis='x', rotation=90, labelsize=6)\nax.tick_params(axis='y', labelsize=6)\nplt.tight_layout()\nplt.savefig('eda_02_correlation.png', bbox_inches='tight', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:23.827653Z","iopub.execute_input":"2026-04-30T22:04:23.827984Z","iopub.status.idle":"2026-04-30T22:04:26.187630Z","shell.execute_reply.started":"2026-04-30T22:04:23.827961Z","shell.execute_reply":"2026-04-30T22:04:26.186724Z"}},"outputs":[],"execution_count":null},{"id":"m016","cell_type":"markdown","source":"### 3.3 Feature Distributions — BENIGN vs DDoS","metadata":{}},{"id":"c017","cell_type":"code","source":"KEY = ['Flow Duration','Total Fwd Packets','Total Backward Packets',\n       'Flow Bytes/s','Flow Packets/s','Flow IAT Mean']\nKEY = [f for f in KEY if f in df.columns]\n\nfig, axes = plt.subplots(2, 3, figsize=(20, 10))\naxes = axes.flatten()\ns_b = df[df[LABEL_COL]=='BENIGN'][KEY].sample(min(5000,(df[LABEL_COL]=='BENIGN').sum()),random_state=SEED)\ns_d = df[df[LABEL_COL]=='DDoS'][KEY].sample(min(5000,(df[LABEL_COL]=='DDoS').sum()),random_state=SEED)\n\nfor i, feat in enumerate(KEY):\n    ax = axes[i]\n    p1,p99 = np.percentile(df[feat].dropna(),[1,99])\n    ax.hist(np.clip(s_b[feat].dropna(),p1,p99), bins=50, alpha=0.6, color='#2ecc71', label='BENIGN', density=True)\n    ax.hist(np.clip(s_d[feat].dropna(),p1,p99), bins=50, alpha=0.6, color='#e74c3c', label='DDoS',   density=True)\n    ax.set_title(feat, fontweight='bold', fontsize=10)\n    ax.legend(fontsize=8)\n\nplt.suptitle('Key Feature Distributions — BENIGN vs DDoS (1–99th pct clipped)', fontsize=13, fontweight='bold')\nplt.tight_layout()\nplt.savefig('eda_03_distributions.png', bbox_inches='tight', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:26.190316Z","iopub.execute_input":"2026-04-30T22:04:26.190624Z","iopub.status.idle":"2026-04-30T22:04:29.804148Z","shell.execute_reply.started":"2026-04-30T22:04:26.190601Z","shell.execute_reply":"2026-04-30T22:04:29.803324Z"}},"outputs":[],"execution_count":null},{"id":"m018","cell_type":"markdown","source":"### 3.4 TCP Flag Forensics\n\n> High SYN / Low ACK → SYN Flood  |  High RST → Port Scan  |  Zero FIN → Slowloris","metadata":{}},{"id":"c019","cell_type":"code","source":"FLAG_COLS = [c for c in ['FIN Flag Count','SYN Flag Count','RST Flag Count',\n             'PSH Flag Count','ACK Flag Count','URG Flag Count',\n             'CWE Flag Count','ECE Flag Count'] if c in df.columns]\n\nflag_means = df.groupby(LABEL_COL)[FLAG_COLS].mean()\nflag_pct   = flag_means.div(flag_means.sum(axis=1)+1e-9, axis=0)*100\n\nfig, axes = plt.subplots(1, 2, figsize=(18, 6))\nsns.heatmap(flag_means, annot=True, fmt='.2f', cmap='YlOrRd',\n            linewidths=0.5, ax=axes[0], cbar_kws={'label':'Mean count/flow'})\naxes[0].set_title('Mean TCP Flag Counts (BENIGN vs DDoS)', fontweight='bold')\n\nflag_pct.plot(kind='bar', stacked=True, ax=axes[1], colormap='tab10', edgecolor='white', linewidth=0.4)\naxes[1].set_title('TCP Flag Composition (%)', fontweight='bold')\naxes[1].tick_params(axis='x', rotation=0)\naxes[1].legend(title='Flag', bbox_to_anchor=(1.01,1), loc='upper left', fontsize=8)\n\nif 'SYN Flag Count' in df.columns and 'ACK Flag Count' in df.columns:\n    df['SYN_ACK_ratio'] = (df['SYN Flag Count']/(df['ACK Flag Count']+1)).clip(upper=10)\n    print('SYN/ACK ratio:')\n    print(df.groupby(LABEL_COL)['SYN_ACK_ratio'].describe().round(3))\n\nplt.suptitle('TCP Flag Forensics', fontsize=13, fontweight='bold')\nplt.tight_layout()\nplt.savefig('eda_04_tcp_flags.png', bbox_inches='tight', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:29.805165Z","iopub.execute_input":"2026-04-30T22:04:29.805509Z","iopub.status.idle":"2026-04-30T22:04:30.980481Z","shell.execute_reply.started":"2026-04-30T22:04:29.805482Z","shell.execute_reply":"2026-04-30T22:04:30.979667Z"}},"outputs":[],"execution_count":null},{"id":"m020","cell_type":"markdown","source":"### 3.5 Traffic Asymmetry & Amplification Factor\n\n> High Bwd/Fwd bytes ratio → reflective amplification (NTP/DNS/SSDP). High Fwd, near-zero Bwd → SYN flood.","metadata":{}},{"id":"c021","cell_type":"code","source":"fwd_b = next((c for c in df.columns if 'Total Length of Fwd' in c), None)\nbwd_b = next((c for c in df.columns if 'Total Length of Bwd' in c), None)\nfwd_p = next((c for c in df.columns if 'Total Fwd Packets'   in c), None)\nbwd_p = next((c for c in df.columns if 'Total Backward Packets' in c), None)\n\nif all([fwd_b, bwd_b, fwd_p, bwd_p]):\n    df['amplification_factor'] = (df[bwd_b] / (df[fwd_b] + 1)).clip(upper=100)\n    df['byte_asymmetry']       = ((df[bwd_b] - df[fwd_b]) / (df[bwd_b] + df[fwd_b] + 1))\n\n    # Refresh EDA sample now that engineered cols exist\n    df_e = df.sample(N_EDA, random_state=SEED)\n\n    fig, axes = plt.subplots(1, 2, figsize=(16, 5))\n    order_af = df.groupby(LABEL_COL)['amplification_factor'].median().sort_values(ascending=False).index\n    sns.boxplot(data=df_e, x=LABEL_COL, y='amplification_factor', order=order_af,\n                palette='Reds', ax=axes[0],\n                flierprops={'marker': '.', 'markersize': 2, 'alpha': 0.2})\n    axes[0].axhline(10, color='red', linestyle=':', linewidth=1.5, label='AF=10')\n    axes[0].set_yscale('log')\n    axes[0].set_title('Amplification Factor (log)', fontweight='bold')\n    axes[0].tick_params(axis='x', rotation=0)\n    axes[0].legend()\n\n    order_ba = df.groupby(LABEL_COL)['byte_asymmetry'].median().sort_values(ascending=False).index\n    sns.violinplot(data=df_e, x=LABEL_COL, y='byte_asymmetry', order=order_ba,\n                   palette='coolwarm', cut=0, inner='box', ax=axes[1])\n    axes[1].axhline(0, color='black', linestyle='--', linewidth=1.2, label='Symmetric')\n    axes[1].set_title('Byte Asymmetry (+1=all bwd / -1=all fwd)', fontweight='bold')\n    axes[1].tick_params(axis='x', rotation=0)\n    axes[1].legend()\n\n    plt.suptitle('Traffic Asymmetry & Amplification', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig('eda_05_amplification.png', bbox_inches='tight', dpi=150)\n    plt.show()\nelse:\n    print('[SKIP] Fwd/Bwd byte columns not found')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:30.981613Z","iopub.execute_input":"2026-04-30T22:04:30.982451Z","iopub.status.idle":"2026-04-30T22:04:32.796796Z","shell.execute_reply.started":"2026-04-30T22:04:30.982425Z","shell.execute_reply":"2026-04-30T22:04:32.795984Z"}},"outputs":[],"execution_count":null},{"id":"m022","cell_type":"markdown","source":"### 3.6 Inter-Arrival Time (IAT) Analysis\n\n> Low IAT CV (std/mean) = machine-clock precision = automated flood tool. Human traffic has high CV.","metadata":{}},{"id":"c023","cell_type":"code","source":"IAT_PAIRS = {k: v for k, v in {\n    'Flow IAT Mean': 'Flow IAT Std',\n    'Fwd IAT Mean':  'Fwd IAT Std',\n    'Bwd IAT Mean':  'Bwd IAT Std'\n}.items() if k in df.columns and v in df.columns}\n\nfor mc, sc in IAT_PAIRS.items():\n    cv = mc.replace('Mean', 'CV')\n    df[cv] = (df[sc] / (df[mc] + 1e-6)).clip(upper=50)\n\ncv_cols = [c for c in df.columns if 'IAT CV' in c]\nprint(f'IAT CV features: {cv_cols}')\n\nif cv_cols:\n    df_e = df.sample(N_EDA, random_state=SEED)\n\n    primary_cv  = cv_cols[0]\n    primary_iat = list(IAT_PAIRS.keys())[0]\n\n    fig, axes = plt.subplots(1, 2, figsize=(16, 5))\n    order_cv = df.groupby(LABEL_COL)[primary_cv].median().sort_values().index\n    sns.violinplot(data=df_e, x=LABEL_COL, y=primary_cv, order=order_cv,\n                   palette='Set2', cut=0, inner='quartile', ax=axes[0])\n    axes[0].axhline(0.5, color='orange', linestyle='--', linewidth=1.5, label='Low CV threshold')\n    axes[0].set_title('IAT Coefficient of Variation\\nLow = machine/automated attack', fontweight='bold')\n    axes[0].tick_params(axis='x', rotation=0)\n    axes[0].legend()\n\n    from scipy.stats import gaussian_kde\n    for lbl, col in zip(['BENIGN', 'DDoS'], ['#2ecc71', '#e74c3c']):\n        raw = df[df[LABEL_COL] == lbl][primary_iat].replace([np.inf, -np.inf], np.nan).dropna()\n        if len(raw) < 200:\n            continue\n        p99 = raw.quantile(0.99)\n        dl  = np.log1p(raw.clip(upper=p99))\n        dl  = dl.replace([np.inf, -np.inf], np.nan).dropna()\n        dl  = dl[np.isfinite(dl)]\n        if len(dl) < 200 or dl.std() < 1e-6:\n            # all values identical — skip KDE, draw a vertical line instead\n            axes[1].axvline(dl.mean(), color=col, linewidth=2.5,\n                            linestyle='--', label=f'{lbl} (constant IAT)')\n            continue\n        samp = dl.sample(min(5000, len(dl)), random_state=SEED).values\n        try:\n            kde    = gaussian_kde(samp)\n            xr     = np.linspace(samp.min(), samp.max(), 300)\n            kvals  = kde(xr)\n            if np.all(np.isfinite(kvals)):\n                axes[1].plot(xr, kvals, label=lbl, color=col, linewidth=2.5)\n                axes[1].fill_between(xr, kvals, alpha=0.1, color=col)\n        except Exception as e:\n            print(f'  [KDE skip {lbl}]: {e}')\n\n    axes[1].set_xlabel('log(1 + IAT Mean µs)')\n    axes[1].set_ylabel('Density')\n    axes[1].set_title('IAT Mean KDE — BENIGN vs DDoS', fontweight='bold')\n    axes[1].legend()\n\n    plt.suptitle('IAT Timing Forensics', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig('eda_06_iat.png', bbox_inches='tight', dpi=150)\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:32.797899Z","iopub.execute_input":"2026-04-30T22:04:32.798218Z","iopub.status.idle":"2026-04-30T22:04:34.636529Z","shell.execute_reply.started":"2026-04-30T22:04:32.798185Z","shell.execute_reply":"2026-04-30T22:04:34.635814Z"}},"outputs":[],"execution_count":null},{"id":"m024","cell_type":"markdown","source":"### 3.7 Packet Size Forensics\n\n> Near-zero Packet Length Variance = fixed-size flood. SYN flood ~40 B, UDP flood ~1400 B.","metadata":{}},{"id":"c025","cell_type":"code","source":"if 'Packet Length Std' in df.columns and 'Packet Length Mean' in df.columns:\n    df['pkt_size_cv'] = (df['Packet Length Std'] / (df['Packet Length Mean'] + 1)).clip(upper=5)\n\nif 'Packet Length Mean' in df.columns:\n    # Refresh EDA sample now that pkt_size_cv exists\n    df_e = df.sample(N_EDA, random_state=SEED)\n\n    fig, axes = plt.subplots(1, 2, figsize=(16, 5))\n    order_p = df.groupby(LABEL_COL)['Packet Length Mean'].median().sort_values(ascending=False).index\n    sns.boxplot(data=df_e, x=LABEL_COL, y='Packet Length Mean', order=order_p,\n                palette='Set2', ax=axes[0],\n                flierprops={'marker': '.', 'markersize': 2, 'alpha': 0.2})\n    axes[0].axhline(40,   color='red',    linestyle=':', linewidth=1.5, label='SYN flood (~40 B)')\n    axes[0].axhline(1400, color='orange', linestyle=':', linewidth=1.5, label='MTU flood (~1400 B)')\n    axes[0].set_title('Packet Length Mean by Label', fontweight='bold')\n    axes[0].tick_params(axis='x', rotation=0)\n    axes[0].legend(fontsize=8)\n\n    if 'pkt_size_cv' in df.columns:\n        order_cv2 = df.groupby(LABEL_COL)['pkt_size_cv'].median().sort_values().index\n        sns.violinplot(data=df_e, x=LABEL_COL, y='pkt_size_cv', order=order_cv2,\n                       palette='coolwarm', cut=0, inner='box', ax=axes[1])\n        axes[1].axhline(0.3, color='red', linestyle='--', linewidth=1.5, label='Low CV = fixed-size flood')\n        axes[1].set_title('Packet Size CV (low = templated flood)', fontweight='bold')\n        axes[1].tick_params(axis='x', rotation=0)\n        axes[1].legend(fontsize=9)\n\n    plt.suptitle('Packet Size Forensics', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig('eda_07_pkt_size.png', bbox_inches='tight', dpi=150)\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:34.637444Z","iopub.execute_input":"2026-04-30T22:04:34.637782Z","iopub.status.idle":"2026-04-30T22:04:35.827999Z","shell.execute_reply.started":"2026-04-30T22:04:34.637758Z","shell.execute_reply":"2026-04-30T22:04:35.827281Z"}},"outputs":[],"execution_count":null},{"id":"m026","cell_type":"markdown","source":"### 3.8 Flow Duration & Connection Lifecycle\n\n> Near-zero → UDP/ICMP/SYN flood. Very long → Slowloris keep-alive abuse.","metadata":{}},{"id":"c027","cell_type":"code","source":"DUR = next((c for c in df.columns if 'Flow Duration'==c), None)\nif DUR:\n    df['flow_ms']     = df[DUR]/1000.0\n    df['log_dur']     = np.log1p(df['flow_ms'])\n\n    fig, axes = plt.subplots(1, 2, figsize=(16, 5))\n    for lbl, col in zip(['BENIGN','DDoS'],['#2ecc71','#e74c3c']):\n        data = df[df[LABEL_COL]==lbl]['log_dur'].dropna().sort_values()\n        axes[0].plot(data.values, np.linspace(0,1,len(data)), label=lbl, color=col, linewidth=2)\n    axes[0].set_xlabel('log(1 + Duration ms)'); axes[0].set_ylabel('CDF')\n    axes[0].set_title('Flow Duration CDF', fontweight='bold'); axes[0].legend()\n\n    dp = df.groupby(LABEL_COL)['flow_ms'].agg(P10=lambda x:x.quantile(.10),\n         P50=lambda x:x.quantile(.50), P90=lambda x:x.quantile(.90), P99=lambda x:x.quantile(.99))\n    dl = np.log1p(dp); x_pos = np.arange(len(dl))\n    for pct, col in zip(['P10','P50','P90','P99'],['#3498db','#2ecc71','#e67e22','#e74c3c']):\n        axes[1].scatter(x_pos, dl[pct], label=pct, color=col, s=120, zorder=5)\n    for i in range(len(dl)):\n        axes[1].plot([x_pos[i]]*4,\n                     [dl['P10'].iloc[i],dl['P50'].iloc[i],dl['P90'].iloc[i],dl['P99'].iloc[i]],\n                     'gray', linewidth=1, alpha=0.5)\n    axes[1].set_xticks(x_pos); axes[1].set_xticklabels(dl.index, rotation=0)\n    axes[1].set_ylabel('log(1 + Duration ms)')\n    axes[1].set_title('Duration Percentile Spread', fontweight='bold'); axes[1].legend(title='Pct', fontsize=9)\n\n    plt.suptitle('Flow Duration Analysis', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig('eda_08_duration.png', bbox_inches='tight', dpi=150)\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:35.828874Z","iopub.execute_input":"2026-04-30T22:04:35.829275Z","iopub.status.idle":"2026-04-30T22:04:38.855450Z","shell.execute_reply.started":"2026-04-30T22:04:35.829241Z","shell.execute_reply":"2026-04-30T22:04:38.854768Z"}},"outputs":[],"execution_count":null},{"id":"m028","cell_type":"markdown","source":"### 3.9 Destination Port Attack Surface\n\n> Low Shannon entropy = concentrated targeting = DDoS signal. Well-known ports (80/443/53) are prime targets.","metadata":{}},{"id":"c029","cell_type":"code","source":"from scipy.stats import entropy as scipy_entropy\nDST = next((c for c in df.columns if 'Destination Port'==c.strip()), None)\nif DST:\n    def port_cls(p):\n        try: p=int(p)\n        except: return 'Unknown'\n        known={80:'HTTP(80)',443:'HTTPS(443)',22:'SSH(22)',53:'DNS(53)',\n               21:'FTP(21)',23:'Telnet(23)',8080:'HTTP-Alt(8080)',3389:'RDP(3389)',445:'SMB(445)'}\n        if p in known: return known[p]\n        elif 0<=p<=1023: return 'Well-Known'\n        elif p>=49152:   return 'Ephemeral'\n        return 'Registered'\n\n    df['port_class'] = df[DST].apply(port_cls)\n\n    ent_df = df.groupby(LABEL_COL)[DST].apply(\n        lambda s: scipy_entropy(s.value_counts(normalize=True), base=2)).reset_index()\n    ent_df.columns = [LABEL_COL,'Port Entropy (bits)']\n\n    fig, axes = plt.subplots(1, 2, figsize=(16, 5))\n    clrs_e = ['#e74c3c' if v<5 else '#3498db' for v in ent_df['Port Entropy (bits)']]\n    axes[0].barh(ent_df[LABEL_COL], ent_df['Port Entropy (bits)'], color=clrs_e)\n    axes[0].axvline(5, color='red', linestyle='--', linewidth=1.5, label='Low entropy<5 bits')\n    axes[0].set_xlabel('Shannon Entropy (bits)')\n    axes[0].set_title('Destination Port Entropy\\nLow = targeted attack', fontweight='bold')\n    axes[0].legend()\n\n    pc = df.groupby([LABEL_COL,'port_class']).size().unstack(fill_value=0)\n    pc.div(pc.sum(axis=1),axis=0).mul(100).plot(kind='bar', stacked=True, ax=axes[1],\n        colormap='tab10', edgecolor='white', linewidth=0.4)\n    axes[1].set_title('Port Class Composition (%)', fontweight='bold')\n    axes[1].tick_params(axis='x', rotation=0)\n    axes[1].legend(title='Port Class', bbox_to_anchor=(1.01,1), loc='upper left', fontsize=8)\n\n    plt.suptitle('Destination Port Attack Surface', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig('eda_09_ports.png', bbox_inches='tight', dpi=150)\n    plt.show()\n    print('\\nTop 10 targeted ports:')\n    print(df[DST].value_counts().head(10).to_string())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:38.856600Z","iopub.execute_input":"2026-04-30T22:04:38.856909Z","iopub.status.idle":"2026-04-30T22:04:40.463747Z","shell.execute_reply.started":"2026-04-30T22:04:38.856886Z","shell.execute_reply":"2026-04-30T22:04:40.463099Z"}},"outputs":[],"execution_count":null},{"id":"m030","cell_type":"markdown","source":"### 3.10 KS-Test Feature Discriminability\n\n> KS statistic ∈ [0,1]: 0 = identical distributions, 1 = perfect separation. Model-free feature ranking.","metadata":{}},{"id":"c031","cell_type":"code","source":"n_ks = min(5000, (df[LABEL_COL]=='BENIGN').sum(), (df[LABEL_COL]=='DDoS').sum())\nbenign_ks = df[df[LABEL_COL]=='BENIGN'][feature_cols].sample(n_ks, random_state=SEED)\nddos_ks   = df[df[LABEL_COL]=='DDoS'][feature_cols].sample(n_ks, random_state=SEED)\n\nks_stats = {}\nfor feat in feature_cols:\n    try:\n        stat, _ = ks_2samp(benign_ks[feat].dropna().values, ddos_ks[feat].dropna().values)\n        if np.isfinite(stat): ks_stats[feat] = stat\n    except: pass\n\nks_series = pd.Series(ks_stats).sort_values(ascending=False)\ntop20_ks  = ks_series.head(20)\n\nfig, ax = plt.subplots(figsize=(10, 8))\nclrs_k = plt.cm.Reds(np.linspace(0.35, 0.9, len(top20_ks)))\nax.barh(top20_ks.index[::-1], top20_ks.values[::-1], color=clrs_k[::-1])\nax.axvline(0.5, color='red', linestyle='--', linewidth=1.5, label='KS=0.5')\nax.set_xlabel('KS Statistic (BENIGN vs DDoS)')\nax.set_title('Top 20 Discriminative Features (KS-Test)\\nHigher = better class separation', fontweight='bold')\nax.legend(); ax.tick_params(axis='y', labelsize=9)\nplt.tight_layout()\nplt.savefig('eda_10_ks_test.png', bbox_inches='tight', dpi=150)\nplt.show()\n\nprint('Top 10 features by KS statistic:')\nfor feat, val in ks_series.head(10).items():\n    print(f'  {feat:45s}  KS={val:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:40.464979Z","iopub.execute_input":"2026-04-30T22:04:40.465414Z","iopub.status.idle":"2026-04-30T22:04:42.079704Z","shell.execute_reply.started":"2026-04-30T22:04:40.465386Z","shell.execute_reply":"2026-04-30T22:04:42.078738Z"}},"outputs":[],"execution_count":null},{"id":"m032","cell_type":"markdown","source":"### 3.11 Z-Score Anomaly — DDoS vs BENIGN Baseline\n\n> Z = (μ_DDoS − μ_BENIGN) / σ_BENIGN. Red = DDoS unusually HIGH, Blue = DDoS unusually LOW.","metadata":{}},{"id":"c033","cell_type":"code","source":"top_feats_z = ks_series.head(25).index.tolist()\nb_mean = df[df[LABEL_COL]=='BENIGN'][top_feats_z].mean()\nb_std  = df[df[LABEL_COL]=='BENIGN'][top_feats_z].std().replace(0, 1e-9)\nd_mean = df[df[LABEL_COL]=='DDoS'][top_feats_z].mean()\nz_vals = ((d_mean - b_mean) / b_std).clip(-5, 5).sort_values()\n\nfig, ax = plt.subplots(figsize=(10, 10))\nclrs_z = ['#e74c3c' if v>0 else '#3498db' for v in z_vals.values]\nax.barh(z_vals.index, z_vals.values, color=clrs_z)\nax.axvline(0,  color='black', linewidth=1)\nax.axvline( 2, color='red',  linestyle='--', linewidth=1, label='Z=+2')\nax.axvline(-2, color='blue', linestyle='--', linewidth=1, label='Z=-2')\nax.set_xlabel('Z-Score (DDoS vs BENIGN baseline)')\nax.set_title('Feature Anomaly Z-Score\\nRed = DDoS HIGH  |  Blue = DDoS LOW', fontweight='bold')\nax.legend(); ax.tick_params(axis='y', labelsize=9)\nplt.tight_layout()\nplt.savefig('eda_11_zscore.png', bbox_inches='tight', dpi=150)\nplt.show()\n\nprint('Top 5 features where DDoS is anomalously HIGH:')\nfor f,v in z_vals.nlargest(5).items(): print(f'  {f:45s}  Z=+{v:.2f}')\nprint('\\nTop 5 features where DDoS is anomalously LOW:')\nfor f,v in z_vals.nsmallest(5).items(): print(f'  {f:45s}  Z={v:.2f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:42.080898Z","iopub.execute_input":"2026-04-30T22:04:42.081580Z","iopub.status.idle":"2026-04-30T22:04:43.682858Z","shell.execute_reply.started":"2026-04-30T22:04:42.081554Z","shell.execute_reply":"2026-04-30T22:04:43.682112Z"}},"outputs":[],"execution_count":null},{"id":"m034","cell_type":"markdown","source":"### 3.12 t-SNE Cluster Visualisation\n\n> Tight, separated clusters = high classifier confidence. Overlap = hard boundary where false positives/negatives occur.","metadata":{}},{"id":"c035","cell_type":"code","source":"from sklearn.manifold import TSNE\nfrom sklearn.decomposition import PCA\n\ntop_feats_tsne = ks_series.head(30).index.tolist()\nTSNE_N = min(3000, len(df))\ndf_t   = df.sample(TSNE_N, random_state=SEED).copy()\n\nX_raw = df_t[top_feats_tsne].values\nX_pca = PCA(n_components=min(30,X_raw.shape[1]), random_state=SEED).fit_transform(\n            RobustScaler().fit_transform(X_raw))\n\nprint(f'Running t-SNE on {TSNE_N} samples...')\nt0 = time.time()\nX2d = TSNE(n_components=2, perplexity=40, learning_rate=200,\n           n_iter=1000, random_state=SEED).fit_transform(X_pca)\nprint(f'Done in {time.time()-t0:.1f}s')\n\ndf_t['tsne_x'], df_t['tsne_y'] = X2d[:,0], X2d[:,1]\n\nfig, ax = plt.subplots(figsize=(10, 8))\nclr_map = {'BENIGN':'#2ecc71','DDoS':'#e74c3c'}\nfor lbl, grp in df_t.groupby(LABEL_COL):\n    ax.scatter(grp['tsne_x'], grp['tsne_y'], c=clr_map[lbl], label=lbl,\n               alpha=0.4 if lbl=='BENIGN' else 0.6, s=12)\nax.set_title(f't-SNE — BENIGN vs DDoS  (n={TSNE_N}, top-{len(top_feats_tsne)} KS features)',\n             fontweight='bold')\nax.legend(fontsize=12, markerscale=3)\nax.tick_params(labelbottom=False, labelleft=False)\nplt.tight_layout()\nplt.savefig('eda_12_tsne.png', bbox_inches='tight', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:43.684070Z","iopub.execute_input":"2026-04-30T22:04:43.684390Z","iopub.status.idle":"2026-04-30T22:04:58.245823Z","shell.execute_reply.started":"2026-04-30T22:04:43.684364Z","shell.execute_reply":"2026-04-30T22:04:58.245194Z"}},"outputs":[],"execution_count":null},{"id":"m036","cell_type":"markdown","source":"### 3.13 TCP Window Size & Connection Initialisation\n\n> `Init_Win_bytes_backward = 0` → server never completed handshake → SYN flood fingerprint.","metadata":{}},{"id":"c037","cell_type":"code","source":"WIN_F = next((c for c in df.columns if 'Init_Win_bytes_forward'  in c), None)\nWIN_B = next((c for c in df.columns if 'Init_Win_bytes_backward' in c), None)\nif WIN_F and WIN_B:\n    df['zero_win_fwd'] = (df[WIN_F]<=0).astype(int)\n    df['zero_win_bwd'] = (df[WIN_B]<=0).astype(int)\n\n    zw = df.groupby(LABEL_COL)[['zero_win_fwd','zero_win_bwd']].mean()*100\n    zw.columns = ['Zero Fwd (%)','Zero Bwd (%)']\n    print('TCP Zero-Window Rate (high bwd = SYN flood):')\n    print(zw.to_string())\n\n    fig, axes = plt.subplots(1, 2, figsize=(16, 5))\n    x_zw = np.arange(len(zw)); w = 0.35\n    axes[0].barh(x_zw+w/2, zw['Zero Bwd (%)'], w, label='Bwd=0 (no handshake)', color='#e74c3c', alpha=0.85)\n    axes[0].barh(x_zw-w/2, zw['Zero Fwd (%)'], w, label='Fwd=0 (no data intent)', color='#3498db', alpha=0.85)\n    axes[0].set_yticks(x_zw); axes[0].set_yticklabels(zw.index, fontsize=9)\n    axes[0].set_xlabel('% flows with zero window')\n    axes[0].set_title('TCP Zero-Window Rate\\n(High bwd = SYN Flood)', fontweight='bold'); axes[0].legend(fontsize=8)\n\n    tv = df[WIN_F].value_counts().head(20)\n    axes[1].barh(tv.index.astype(str), tv.values, color='#9b59b6')\n    axes[1].set_xlabel('Number of Flows')\n    axes[1].set_title('Top 20 Init Fwd Window Sizes\\n(Spikes = OS/tool fingerprint)', fontweight='bold')\n    axes[1].tick_params(axis='y', labelsize=8)\n\n    plt.suptitle('TCP Window Size & Connection Initialisation Forensics', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig('eda_13_tcp_window.png', bbox_inches='tight', dpi=150)\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:58.246909Z","iopub.execute_input":"2026-04-30T22:04:58.247263Z","iopub.status.idle":"2026-04-30T22:04:59.036752Z","shell.execute_reply.started":"2026-04-30T22:04:58.247239Z","shell.execute_reply":"2026-04-30T22:04:59.036017Z"}},"outputs":[],"execution_count":null},{"id":"m038","cell_type":"markdown","source":"### 3.14 EDA Summary — Threat Intelligence Card","metadata":{}},{"id":"c039","cell_type":"code","source":"ti = {\n    'Indicator':   ['TCP Flags','IAT pattern','Packet size','Flow duration',\n                    'Byte asymmetry','Port entropy','Init window'],\n    'BENIGN':      ['Balanced ACK/PSH/FIN','High CV (human randomness)',\n                    'Variable (multi-modal)','Bimodal (short XHR + long page)',\n                    'Near symmetric','High (many services)','Normal OS values'],\n    'DDoS':        ['High SYN / low ACK / zero FIN','Near-zero CV (machine-clock)',\n                    'Fixed small OR MTU-fill','Near-zero (flood) or very long (slow)',\n                    'High Fwd heavy OR Bwd heavy (amplification)',\n                    'Concentrated (1-2 ports)','Bwd=0 (server never responded)'],\n}\nti_df = pd.DataFrame(ti).set_index('Indicator')\ndisplay(ti_df.style\n    .set_properties(**{'text-align':'left','font-size':'10pt'})\n    .set_table_styles([{'selector':'th',\n                        'props':[('font-weight','bold'),('background-color','#2c3e50'),\n                                 ('color','white'),('font-size','10pt')]}])\n    .set_caption('<b>EDA-derived Behavioral Fingerprints — BENIGN vs DDoS</b>'))\nprint('\\nNew engineered features in df:')\neng = [c for c in ['SYN_ACK_ratio','byte_asymmetry','amplification_factor',\n                   'pkt_size_cv','flow_ms','log_dur','port_class',\n                   'zero_win_fwd','zero_win_bwd'] + cv_cols if c in df.columns]\nprint('  ' + ', '.join(eng))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:59.037830Z","iopub.execute_input":"2026-04-30T22:04:59.038168Z","iopub.status.idle":"2026-04-30T22:04:59.115062Z","shell.execute_reply.started":"2026-04-30T22:04:59.038144Z","shell.execute_reply":"2026-04-30T22:04:59.114163Z"}},"outputs":[],"execution_count":null},{"id":"m040","cell_type":"markdown","source":"---\n## 4. Feature Preparation","metadata":{}},{"id":"c041","cell_type":"code","source":"y = (df[LABEL_COL]=='DDoS').astype(np.int8).values\nX = df[feature_cols].values.astype(np.float32)\n\nscaler   = RobustScaler()\nX_scaled = scaler.fit_transform(X).astype(np.float32)\ndel X; gc.collect()\n\nprint(f'Feature matrix : {X_scaled.shape}  dtype={X_scaled.dtype}')\nprint(f'Memory         : {X_scaled.nbytes/1e6:.0f} MB')\nprint('Class balance  :')\nfor cls, cnt in sorted(Counter(y).items()):\n    print(f'  {\"BENIGN\" if cls==0 else \"DDoS\"}: {cnt:,} ({cnt/len(y):.2%})')\n\nX_train_raw, X_test, y_train_raw, y_test = train_test_split(\n    X_scaled, y, test_size=0.20, random_state=SEED, stratify=y)\ndel X_scaled; gc.collect()\n\nattack_rate = y_train_raw.mean()\nif attack_rate < 0.25:\n    resample = ImbPipeline([\n        ('smote', SMOTE(sampling_strategy=0.33, random_state=SEED, k_neighbors=5)),\n        ('under', RandomUnderSampler(sampling_strategy=0.50, random_state=SEED))\n    ])\n    X_train, y_train = resample.fit_resample(X_train_raw, y_train_raw)\n    X_train = X_train.astype(np.float32)\n    print(f'\\nSMOTE applied (was {attack_rate:.1%} attack rate)')\nelse:\n    X_train, y_train = X_train_raw, y_train_raw\n    print(f'\\nSMOTE skipped — balanced ({attack_rate:.1%} attack rate)')\ndel X_train_raw; gc.collect()\n\nfor cls, cnt in sorted(Counter(y_train).items()):\n    print(f'  {\"BENIGN\" if cls==0 else \"DDoS\"}: {cnt:,} ({cnt/len(y_train):.2%})')\nprint(f'\\nTest set : {X_test.shape[0]:,} samples (held-out)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:04:59.116307Z","iopub.execute_input":"2026-04-30T22:04:59.116668Z","iopub.status.idle":"2026-04-30T22:05:02.873213Z","shell.execute_reply.started":"2026-04-30T22:04:59.116627Z","shell.execute_reply":"2026-04-30T22:05:02.872544Z"}},"outputs":[],"execution_count":null},{"id":"m042","cell_type":"markdown","source":"---\n## 5. Model Training — All 6 Models (GPU-Aware)","metadata":{}},{"id":"c043","cell_type":"code","source":"# ── Gradient-booster GPU params ───────────────────────────────────────────────\nlgbm_params = dict(n_estimators=300, learning_rate=0.05, num_leaves=63,\n                   n_jobs=-1, random_state=SEED, verbose=-1, **lgbm_gpu)\nxgb_params  = dict(n_estimators=300, learning_rate=0.05, max_depth=8,\n                   eval_metric='logloss', random_state=SEED,\n                   n_jobs=1 if HAS_GPU else -1, **xgb_gpu)\ncat_params  = dict(iterations=300, learning_rate=0.05, depth=8,\n                   random_seed=SEED, verbose=0, **cat_gpu)\n\n# ── KNN subsample (O(n²) cost) ────────────────────────────────────────────────\nKNN_N   = min(50_000, len(X_train))\nknn_idx = np.random.choice(len(X_train), KNN_N, replace=False)\nX_knn, y_knn = X_train[knn_idx], y_train[knn_idx]\nprint(f'KNN subsample : {KNN_N:,} rows')\n\n# ── GPU-aware sklearn equivalents ────────────────────────────────────────────\nif USE_CUML:\n    knn_model = cuKNN(n_neighbors=7)\n    rf_model  = cuRF(n_estimators=200, max_depth=15, random_state=SEED)\n    nb_model  = cuGNB()\n    print('KNN / RF / NB → cuML (GPU)')\nelse:\n    knn_model = KNeighborsClassifier(n_neighbors=7, n_jobs=-1, metric='euclidean')\n    rf_model  = RandomForestClassifier(n_estimators=200, max_depth=15, n_jobs=-1, random_state=SEED)\n    nb_model  = GaussianNB()\n    print('KNN / RF / NB → sklearn (CPU)')\n\nprint(f'GPU={HAS_GPU}  cuML={USE_CUML}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:05:02.874155Z","iopub.execute_input":"2026-04-30T22:05:02.874412Z","iopub.status.idle":"2026-04-30T22:05:02.906330Z","shell.execute_reply.started":"2026-04-30T22:05:02.874391Z","shell.execute_reply":"2026-04-30T22:05:02.905334Z"}},"outputs":[],"execution_count":null},{"id":"c044","cell_type":"code","source":"# name → (model, X_tr, y_tr)\nALL_MODELS = {\n    'KNN (k=7)':     (knn_model, X_knn,   y_knn),\n    'Naive Bayes':   (nb_model,  X_train,  y_train),\n    'Random Forest': (rf_model,  X_train,  y_train),\n    'LightGBM':      (lgb.LGBMClassifier(**lgbm_params),  X_train, y_train),\n    'XGBoost':       (xgb.XGBClassifier(**xgb_params),    X_train, y_train),\n    'CatBoost':      (CatBoostClassifier(**cat_params),    X_train, y_train),\n}\n\nprint(f'Training {len(ALL_MODELS)} models  (GPU={HAS_GPU})\\n')\nresults = {}\n\nfor name, (model, X_tr, y_tr) in ALL_MODELS.items():\n    t0 = time.time()\n    model.fit(X_tr, y_tr)\n    elapsed = time.time() - t0\n\n    y_prob = model.predict_proba(X_test)[:, 1]\n    y_pred = model.predict(X_test)\n\n    results[name] = {\n        'model':   model,\n        'y_prob':  y_prob,\n        'y_pred':  y_pred,\n        'roc_auc': roc_auc_score(y_test, y_prob),\n        'pr_auc':  average_precision_score(y_test, y_prob),\n        'f1':      f1_score(y_test, y_pred, zero_division=0),\n        'prec':    precision_score(y_test, y_pred, zero_division=0),\n        'rec':     recall_score(y_test, y_pred, zero_division=0),\n        'elapsed': elapsed,\n    }\n    r = results[name]\n    print(f'{name:15s}  ROC-AUC={r[\"roc_auc\"]:.4f}  PR-AUC={r[\"pr_auc\"]:.4f}'\n          f'  F1={r[\"f1\"]:.4f}  Prec={r[\"prec\"]:.4f}  Rec={r[\"rec\"]:.4f}  ({elapsed:.1f}s)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:05:02.907675Z","iopub.execute_input":"2026-04-30T22:05:02.907976Z","iopub.status.idle":"2026-04-30T22:06:08.267571Z","shell.execute_reply.started":"2026-04-30T22:05:02.907947Z","shell.execute_reply":"2026-04-30T22:06:08.266179Z"}},"outputs":[],"execution_count":null},{"id":"c045","cell_type":"code","source":"# ── Build metrics DataFrame ───────────────────────────────────────────────────\nmetrics_df = pd.DataFrame({\n    k: {'ROC-AUC':  results[k]['roc_auc'],\n        'PR-AUC':   results[k]['pr_auc'],\n        'F1-Score': results[k]['f1'],\n        'Precision':results[k]['prec'],\n        'Recall':   results[k]['rec'],\n        'Time (s)': results[k]['elapsed']}\n    for k in results\n}).T.sort_values('ROC-AUC', ascending=False)\n\nprint('\\n=== Model Comparison ===')\nprint(metrics_df.to_string())\nprint()\n\n# ── Pandas Styler — colored cells ────────────────────────────────────────────\nperf_cols  = ['ROC-AUC','PR-AUC','F1-Score','Precision','Recall']\ncost_cols  = ['Time (s)']\n\nstyled = (metrics_df.style\n    .background_gradient(cmap='RdYlGn',   subset=perf_cols)\n    .background_gradient(cmap='RdYlGn_r', subset=cost_cols)\n    .highlight_max(subset=['ROC-AUC'], props='font-weight:bold; border: 2px solid #27ae60')\n    .highlight_min(subset=['ROC-AUC'], props='font-weight:bold; border: 2px solid #e74c3c')\n    .format({'ROC-AUC':'{:.4f}','PR-AUC':'{:.4f}','F1-Score':'{:.4f}',\n             'Precision':'{:.4f}','Recall':'{:.4f}','Time (s)':'{:.1f}'})\n    .set_caption('<b style=\"font-size:13px\">All 6 Models — Performance Comparison'\n                 ' (green=high / red=low)</b>'))\ndisplay(styled)\n\n# ── Matplotlib bar chart ──────────────────────────────────────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(18, 6))\n\n# Bar chart: ROC-AUC / PR-AUC / F1\nx      = np.arange(len(metrics_df))\nwidth  = 0.25\nbclrs  = ['#2980b9','#e67e22','#27ae60']\nfor i, (metric, bc) in enumerate(zip(['ROC-AUC','PR-AUC','F1-Score'], bclrs)):\n    bars = axes[0].bar(x + i*width, metrics_df[metric], width,\n                       label=metric, color=bc, edgecolor='white', alpha=0.85)\n    for bar, val in zip(bars, metrics_df[metric]):\n        axes[0].text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.005,\n                     f'{val:.3f}', ha='center', va='bottom', fontsize=8, fontweight='bold')\n\nMODEL_COLORS = {'KNN (k=7)':'#7f8c8d','Naive Bayes':'#95a5a6',\n                'Random Forest':'#16a085','LightGBM':'#8e44ad',\n                'XGBoost':'#e74c3c','CatBoost':'#e67e22'}\naxes[0].set_xticks(x + width)\ntklbls = axes[0].set_xticklabels(metrics_df.index, rotation=20, ha='right', fontsize=10)\nfor tk in tklbls:\n    tk.set_color(MODEL_COLORS.get(tk.get_text(), 'black'))\n    tk.set_fontweight('bold')\naxes[0].set_ylim(0, 1.15)\naxes[0].set_ylabel('Score', fontsize=11)\naxes[0].set_title('ROC-AUC / PR-AUC / F1  — All 6 Models', fontsize=13, fontweight='bold')\naxes[0].legend(fontsize=10); axes[0].grid(axis='y', alpha=0.4)\naxes[0].axhline(0.5, color='gray', linestyle=':', linewidth=1)\n\n# Horizontal ranking by ROC-AUC\nroc_sorted = metrics_df['ROC-AUC'].sort_values()\nbar_clrs   = [MODEL_COLORS.get(n,'#bdc3c7') for n in roc_sorted.index]\nbars2      = axes[1].barh(roc_sorted.index, roc_sorted.values, color=bar_clrs, edgecolor='white')\naxes[1].axvline(0.5, color='gray', linestyle=':', linewidth=1)\nfor bar, val in zip(bars2, roc_sorted.values):\n    axes[1].text(val+0.002, bar.get_y()+bar.get_height()/2,\n                 f'{val:.4f}', va='center', fontsize=10, fontweight='bold')\naxes[1].set_xlim(0, 1.08)\naxes[1].set_xlabel('ROC-AUC', fontsize=11)\naxes[1].set_title('Model Ranking by ROC-AUC', fontsize=13, fontweight='bold')\naxes[1].tick_params(axis='y', labelsize=10)\n\nplt.suptitle('Model Comparison — BENIGN vs DDoS Classification', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('model_comparison.png', bbox_inches='tight', dpi=150)\nplt.show()\n\nbest_name = metrics_df['ROC-AUC'].idxmax()\nprint(f'\\nBest model for tuning: {best_name}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:06:08.268932Z","iopub.execute_input":"2026-04-30T22:06:08.269256Z","iopub.status.idle":"2026-04-30T22:06:09.278641Z","shell.execute_reply.started":"2026-04-30T22:06:08.269225Z","shell.execute_reply":"2026-04-30T22:06:09.277988Z"}},"outputs":[],"execution_count":null},{"id":"m046","cell_type":"markdown","source":"---\n## 6. Hyperparameter Tuning","metadata":{}},{"id":"c047","cell_type":"code","source":"param_grids = {\n    'LightGBM': {\n        'n_estimators':      randint(200, 500),\n        'learning_rate':     uniform(0.02, 0.12),\n        'num_leaves':        randint(31, 127),\n        'min_child_samples': randint(10, 40),\n    },\n    'XGBoost': {\n        'n_estimators':  randint(200, 500),\n        'learning_rate': uniform(0.02, 0.12),\n        'max_depth':     randint(4, 10),\n        'subsample':     uniform(0.6, 0.4),\n    },\n    'CatBoost': {\n        'iterations':    randint(200, 500),\n        'learning_rate': uniform(0.02, 0.12),\n        'depth':         randint(4, 9),\n    },\n    'Random Forest': {\n        'n_estimators': randint(100, 400),\n        'max_depth':    [10, 15, 20, None],\n        'max_features': ['sqrt', 'log2', 0.3],\n    },\n    'KNN (k=7)': {\n        'n_neighbors': randint(3, 15),\n        'metric':      ['euclidean', 'manhattan', 'chebyshev'],\n    },\n}\n\nbase_est = {\n    'LightGBM':     lgb.LGBMClassifier(**lgbm_params),\n    'XGBoost':      xgb.XGBClassifier(**xgb_params),\n    'CatBoost':     CatBoostClassifier(**cat_params),\n    'Random Forest': (cuRF(random_state=SEED) if USE_CUML else\n                      RandomForestClassifier(random_state=SEED, n_jobs=-1)),\n    'KNN (k=7)':    (cuKNN() if USE_CUML else KNeighborsClassifier(n_jobs=-1)),\n}\n\ntune_name = best_name if best_name in param_grids else 'LightGBM'\ntune_X    = X_knn   if tune_name == 'KNN (k=7)' else X_train\ntune_y    = y_knn   if tune_name == 'KNN (k=7)' else y_train\n\ncv = StratifiedKFold(n_splits=3, shuffle=True, random_state=SEED)\nsearch = RandomizedSearchCV(\n    estimator=base_est[tune_name],\n    param_distributions=param_grids[tune_name],\n    n_iter=10, scoring='roc_auc', cv=cv,\n    n_jobs=1 if HAS_GPU else -1,\n    random_state=SEED, verbose=1, refit=True\n)\n\nprint(f'Tuning {tune_name}: 10 iter x 3-fold CV  (GPU={HAS_GPU})')\nt0 = time.time()\nsearch.fit(tune_X, tune_y)\nprint(f'Done in {time.time()-t0:.1f}s')\nprint(f'Best CV ROC-AUC : {search.best_score_:.4f}')\nprint(f'Best params     : {search.best_params_}')\n\nbest_model  = search.best_estimator_\ny_prob_tune = best_model.predict_proba(X_test)[:, 1]\ny_pred_tune = best_model.predict(X_test)\n\nprint(f'\\n=== Tuned {tune_name} ===')\nprint(classification_report(y_test, y_pred_tune, target_names=['BENIGN','DDoS']))\nprint(f'ROC-AUC : {roc_auc_score(y_test, y_prob_tune):.4f}')\nprint(f'PR-AUC  : {average_precision_score(y_test, y_prob_tune):.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:06:09.279753Z","iopub.execute_input":"2026-04-30T22:06:09.280146Z","iopub.status.idle":"2026-04-30T22:17:02.916481Z","shell.execute_reply.started":"2026-04-30T22:06:09.280120Z","shell.execute_reply":"2026-04-30T22:17:02.915714Z"}},"outputs":[],"execution_count":null},{"id":"m048","cell_type":"markdown","source":"---\n## 7. Threshold Optimisation","metadata":{}},{"id":"c049","cell_type":"code","source":"thresholds = np.arange(0.05, 0.96, 0.01)\nf1s, precs, recs = [], [], []\nfor thr in thresholds:\n    yp = (y_prob_tune >= thr).astype(int)\n    f1s.append(f1_score(y_test, yp, pos_label=1, zero_division=0))\n    precs.append(precision_score(y_test, yp, pos_label=1, zero_division=0))\n    recs.append(recall_score(y_test, yp, pos_label=1, zero_division=0))\n\nbest_idx = int(np.argmax(f1s))\nOPT_THR  = thresholds[best_idx]\nprint(f'Optimal threshold : {OPT_THR:.2f}')\nprint(f'F1  (DDoS)        : {f1s[best_idx]:.4f}')\nprint(f'Precision         : {precs[best_idx]:.4f}')\nprint(f'Recall            : {recs[best_idx]:.4f}')\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\naxes[0].plot(thresholds, f1s,   label='F1',       color='#e74c3c', linewidth=2)\naxes[0].plot(thresholds, precs, label='Precision', color='#3498db', linewidth=2)\naxes[0].plot(thresholds, recs,  label='Recall',    color='#2ecc71', linewidth=2)\naxes[0].axvline(OPT_THR, color='black', linestyle='--', linewidth=1.5, label=f'Optimal={OPT_THR:.2f}')\naxes[0].set_xlabel('Decision Threshold'); axes[0].set_ylabel('Score')\naxes[0].set_title('Threshold Optimisation', fontweight='bold'); axes[0].legend(); axes[0].grid(alpha=0.4)\n\nfpr, tpr, _ = roc_curve(y_test, y_prob_tune)\naxes[1].plot(fpr, tpr, color='#9b59b6', linewidth=2,\n             label=f'AUC={roc_auc_score(y_test, y_prob_tune):.4f}')\naxes[1].plot([0,1],[0,1],'k--', linewidth=1)\naxes[1].set_xlabel('False Positive Rate'); axes[1].set_ylabel('True Positive Rate')\naxes[1].set_title('ROC Curve', fontweight='bold'); axes[1].legend(); axes[1].grid(alpha=0.4)\n\nplt.suptitle(f'{tune_name} — Threshold & ROC', fontsize=12, fontweight='bold')\nplt.tight_layout()\nplt.savefig('threshold_roc.png', bbox_inches='tight', dpi=150)\nplt.show()\n\ny_final = (y_prob_tune >= OPT_THR).astype(int)\ncm = confusion_matrix(y_test, y_final)\ntn, fp, fn, tp = cm.ravel()\nprint(f'\\nFalse Alarm Rate  (FP/(FP+TN)) : {fp/(fp+tn):.2%}')\nprint(f'Detection Rate    (TP/(TP+FN)) : {tp/(tp+fn):.2%}')\n\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['BENIGN','DDoS'])\nfig2, ax2 = plt.subplots(figsize=(6, 5))\ndisp.plot(ax=ax2, cmap='Blues', colorbar=False)\nax2.set_title(f'Confusion Matrix — {tune_name} @ thr={OPT_THR:.2f}', fontweight='bold')\nplt.tight_layout()\nplt.savefig('confusion_matrix.png', bbox_inches='tight', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:17:02.917538Z","iopub.execute_input":"2026-04-30T22:17:02.917843Z","iopub.status.idle":"2026-04-30T22:17:08.084716Z","shell.execute_reply.started":"2026-04-30T22:17:02.917820Z","shell.execute_reply":"2026-04-30T22:17:08.084056Z"}},"outputs":[],"execution_count":null},{"id":"c050","cell_type":"code","source":"try:\n    importances = best_model.feature_importances_\nexcept AttributeError:\n    importances = np.zeros(len(feature_cols))\n\nfi = pd.DataFrame({'Feature':feature_cols,'Importance':importances})\nfi = fi.sort_values('Importance', ascending=False).head(20)\n\nfig, ax = plt.subplots(figsize=(10, 7))\nclrs_fi = plt.cm.RdYlGn(np.linspace(0.85, 0.15, len(fi)))\nax.barh(fi['Feature'][::-1], fi['Importance'][::-1], color=clrs_fi)\nax.set_xlabel(f'{tune_name} Feature Importance')\nax.set_title(f'Top 20 Features — {tune_name}', fontweight='bold')\nax.tick_params(axis='y', labelsize=9)\nplt.tight_layout()\nplt.savefig('feature_importance.png', bbox_inches='tight', dpi=150)\nplt.show()\n\nprint('Top 10 features:')\nfor _, row in fi.head(10).iterrows():\n    print(f'  {row[\"Feature\"]:45s}  {row[\"Importance\"]:.0f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:17:08.087979Z","iopub.execute_input":"2026-04-30T22:17:08.088903Z","iopub.status.idle":"2026-04-30T22:17:08.616095Z","shell.execute_reply.started":"2026-04-30T22:17:08.088875Z","shell.execute_reply":"2026-04-30T22:17:08.615226Z"}},"outputs":[],"execution_count":null},{"id":"m051","cell_type":"markdown","source":"---\n## 8. Summary","metadata":{}},{"id":"c052","cell_type":"code","source":"import joblib\n\nprint('=== FINAL SUMMARY ===')\nprint(f'Dataset          : CICIDS2017 + Competition (combined, binary labels)')\nprint(f'Total samples    : {len(df):,}  (after dedup)')\nprint(f'Feature count    : {len(feature_cols)}')\nprint(f'Train / Test     : {len(X_train):,} / {X_test.shape[0]:,}')\nprint(f'GPU              : {HAS_GPU}  cuML={USE_CUML}')\nprint()\nprint('=== Baseline Comparison (all 6 models) ===')\nprint(metrics_df.to_string())\nprint()\nprint(f'=== Tuned {tune_name} ===')\nprint(f'Best CV ROC-AUC  : {search.best_score_:.4f}')\nprint(f'Test ROC-AUC     : {roc_auc_score(y_test, y_prob_tune):.4f}')\nprint(f'Test PR-AUC      : {average_precision_score(y_test, y_prob_tune):.4f}')\nprint(f'Optimal Threshold: {OPT_THR:.2f}')\nprint(f'Detection Rate   : {tp/(tp+fn):.2%}')\nprint(f'False Alarm Rate : {fp/(fp+tn):.2%}')\n\njoblib.dump(best_model,   'best_model.pkl')\njoblib.dump(scaler,       'best_scaler.pkl')\njoblib.dump(feature_cols, 'feature_cols.pkl')\nprint('\\nArtifacts : best_model.pkl  best_scaler.pkl  feature_cols.pkl')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T22:17:08.617359Z","iopub.execute_input":"2026-04-30T22:17:08.618140Z","iopub.status.idle":"2026-04-30T22:17:08.813178Z","shell.execute_reply.started":"2026-04-30T22:17:08.618115Z","shell.execute_reply":"2026-04-30T22:17:08.812502Z"}},"outputs":[],"execution_count":null}]}