{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":4294,"databundleVersionId":33829}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Facebook Recruiting IV: Human or Robot — Data Preprocessing Pipeline\n=====================================================================\n## Covers: Data understanding · Cleaning· Visualization · Feature Engineering","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"# Install once if needed\n%pip install -q lightgbm tabm rtdl_num_embeddings","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:31:26.749025Z","iopub.execute_input":"2026-04-16T15:31:26.749327Z","iopub.status.idle":"2026-04-16T15:31:30.744844Z","shell.execute_reply.started":"2026-04-16T15:31:26.749303Z","shell.execute_reply":"2026-04-16T15:31:30.743930Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── 0. Imports ──────────────────────────────────────────────────────────────\nimport os, warnings, zipfile, glob\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nimport seaborn as sns\nfrom scipy import stats\nfrom sklearn.preprocessing import LabelEncoder\n \nwarnings.filterwarnings(\"ignore\")\npd.set_option(\"display.max_columns\", 50)\npd.set_option(\"display.float_format\", \"{:.4f}\".format)\n \n# ─── Aesthetic config ─────────────────────────────────────────────────────────\nPALETTE = {\"bot\": \"#E84545\", \"human\": \"#2B9EB3\", \"neutral\": \"#6C757D\"}\nsns.set_theme(style=\"darkgrid\", palette=\"muted\", font_scale=1.1)\nplt.rcParams.update({\"figure.dpi\": 140, \"figure.facecolor\": \"#0F1117\",\n                     \"axes.facecolor\": \"#1A1D27\", \"axes.labelcolor\": \"#E0E0E0\",\n                     \"xtick.color\": \"#A0A0A0\", \"ytick.color\": \"#A0A0A0\",\n                     \"text.color\": \"#E0E0E0\", \"grid.color\": \"#2A2D37\",\n                     \"axes.spines.top\": False, \"axes.spines.right\": False})\n\nLOG_DIR = \"logs\"\nos.makedirs(LOG_DIR, exist_ok=True)\nSAVE_DIR = \"plots\"\nos.makedirs(SAVE_DIR, exist_ok=True)\n \n# ─── Helper ───────────────────────────────────────────────────────────────────\ndef save(fig, name):\n    fig.savefig(f\"{SAVE_DIR}/{name}.png\", bbox_inches=\"tight\",\n                facecolor=fig.get_facecolor())\n    plt.close(fig)\n    print(f\"  ↳ Saved  {SAVE_DIR}/{name}.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:31:30.747503Z","iopub.execute_input":"2026-04-16T15:31:30.748083Z","iopub.status.idle":"2026-04-16T15:31:31.902424Z","shell.execute_reply.started":"2026-04-16T15:31:30.748045Z","shell.execute_reply":"2026-04-16T15:31:31.901809Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport zipfile\nimport pandas as pd\n\n# =============================================================================\n# 1. DATA LOADING\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"1. LOADING DATA\")\nprint(\"=\"*60)\n\nINPUT_DIR = \"/kaggle/input/competitions/facebook-recruiting-iv-human-or-bot\" \nWORK_DIR = \"/kaggle/working\" # ← Kaggle's writable directory\n\n# ── Auto-extract any zips in INPUT_DIR to WORK_DIR ───────────────────────────\nfor zpath in glob.glob(f\"{INPUT_DIR}/*.zip\"):\n    print(f\"Extracting {zpath} …\")\n    with zipfile.ZipFile(zpath, \"r\") as z:\n        members = z.namelist()\n        \n        # Only extract CSVs not already present in the WORKING directory\n        to_extract = [m for m in members \n                      if m.endswith(\".csv\") and not os.path.exists(os.path.join(WORK_DIR, m))]\n        \n        if to_extract:\n            # Extract to WORK_DIR instead of INPUT_DIR\n            z.extractall(WORK_DIR, members=to_extract)\n            print(f\"  ↳ extracted to {WORK_DIR}: {to_extract}\")\n        else:\n            print(f\"  ↳ CSVs already present, skipping.\")\n\n# Helper function to find the CSV whether it was extracted to working or already unzipped in input\ndef get_file_path(filename):\n    if os.path.exists(f\"{WORK_DIR}/{filename}\"):\n        return f\"{WORK_DIR}/{filename}\"\n    return f\"{INPUT_DIR}/{filename}\"\n\n# Read the CSVs\ntrain  = pd.read_csv(get_file_path(\"train.csv\"))\ntest   = pd.read_csv(get_file_path(\"test.csv\"))\nbids   = pd.read_csv(get_file_path(\"bids.csv\"))\n\nfor name, df in [(\"train\", train), (\"test\", test), (\"bids\", bids)]:\n    print(f\"\\n{name}.csv  shape={df.shape}\")\n    print(df.dtypes.to_string())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:31:31.903349Z","iopub.execute_input":"2026-04-16T15:31:31.904156Z","iopub.status.idle":"2026-04-16T15:31:52.380633Z","shell.execute_reply.started":"2026-04-16T15:31:31.904129Z","shell.execute_reply":"2026-04-16T15:31:52.379775Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 2. DATA UNDERSTANDING\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"2. DATA UNDERSTANDING\")\nprint(\"=\"*60)\n \n# ── 2a. Basic stats ──────────────────────────────────────────────────────────\nprint(\"\\n── TRAIN ──\")\nprint(train.describe(include=\"all\").T)\nprint(\"\\n── BIDS (numeric) ──\")\nprint(bids.describe().T)\n \n# ── 2b. Null audit ──────────────────────────────────────────────────────────\ndef null_report(df, label):\n    null = df.isnull().sum()\n    pct  = null / len(df) * 100\n    return pd.DataFrame({\"nulls\": null, \"pct\": pct}).query(\"nulls > 0\").assign(source=label)\n \nnull_df = pd.concat([null_report(train, \"train\"),\n                     null_report(test,  \"test\"),\n                     null_report(bids,  \"bids\")])\nprint(\"\\n── NULL REPORT ──\")\nprint(null_df.to_string() if not null_df.empty else \"No nulls found.\")\n \n# ── 2c. Class balance ────────────────────────────────────────────────────────\nprint(\"\\n── CLASS BALANCE (train) ──\")\nvc = train[\"outcome\"].value_counts()\nprint(vc.to_string())\nprint(f\"  Bot ratio: {vc[1]/len(train)*100:.1f}%\")\n \n# ── 2d. Overlap — how many bidders have bids? ─────────────────────────────────\ntrain_with_bids = set(train.bidder_id) & set(bids.bidder_id)\ntest_with_bids  = set(test.bidder_id)  & set(bids.bidder_id)\nprint(f\"\\nTrain bidders with bids : {len(train_with_bids)}/{len(train)} \"\n      f\"({len(train_with_bids)/len(train)*100:.1f}%)\")\nprint(f\"Test  bidders with bids : {len(test_with_bids)}/{len(test)} \"\n      f\"({len(test_with_bids)/len(test)*100:.1f}%)\")\n \n# ── 2e. Bids per bidder distribution (raw) ────────────────────────────────────\nbids_per_bidder_raw = bids.groupby(\"bidder_id\").size()\nprint(f\"\\nBids-per-bidder: min={bids_per_bidder_raw.min()} \"\n      f\"max={bids_per_bidder_raw.max()} \"\n      f\"median={bids_per_bidder_raw.median():.0f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:31:52.381663Z","iopub.execute_input":"2026-04-16T15:31:52.381995Z","iopub.status.idle":"2026-04-16T15:31:57.263917Z","shell.execute_reply.started":"2026-04-16T15:31:52.381968Z","shell.execute_reply":"2026-04-16T15:31:57.263197Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 3. VISUALISATION — RAW DATA\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"3. RAW DATA VISUALISATION\")\nprint(\"=\"*60)\n \nfig, axes = plt.subplots(2, 3, figsize=(18, 10))\nfig.suptitle(\"Raw Data Overview — Facebook Human vs Robot\", fontsize=16, y=1.01)\n \n# 3a. Class balance\nax = axes[0, 0]\nbars = ax.bar([\"Human (0)\", \"Bot (1)\"], vc.values,\n              color=[PALETTE[\"human\"], PALETTE[\"bot\"]], width=0.5, edgecolor=\"none\")\nax.bar_label(bars, fmt=\"%d\", padding=4, color=\"#E0E0E0\")\nax.set_title(\"Class Balance (train)\")\nax.set_ylabel(\"Count\")\n \n# 3b. Bids-per-bidder (log scale)\nax = axes[0, 1]\nbids_per_bidder_raw.clip(upper=2000).hist(bins=60, ax=ax, color=PALETTE[\"neutral\"],\n                                          edgecolor=\"none\", log=True)\nax.set_title(\"Bids per Bidder (raw, clipped@2k)\")\nax.set_xlabel(\"Bid count\"); ax.set_ylabel(\"Frequency (log)\")\n \n# 3c. Country distribution (top 15)\nax = axes[0, 2]\nbids[\"country\"].value_counts().head(15).plot.barh(ax=ax, color=PALETTE[\"neutral\"],\n                                                   edgecolor=\"none\")\nax.set_title(\"Top 15 Countries (bids)\")\nax.invert_yaxis()\n \n# 3d. Device diversity\nax = axes[1, 0]\nbids[\"device\"].value_counts().head(10).plot.bar(ax=ax, color=PALETTE[\"neutral\"],\n                                                 edgecolor=\"none\", rot=45)\nax.set_title(\"Top 10 Devices (bids)\")\n \n# 3e. Merchandise category\nax = axes[1, 1]\nbids[\"merchandise\"].value_counts().plot.pie(ax=ax, autopct=\"%1.1f%%\",\n    colors=sns.color_palette(\"Set2\"), startangle=140, labels=None)\nax.legend(bids[\"merchandise\"].value_counts().index, loc=\"center left\",\n          bbox_to_anchor=(1, 0.5), fontsize=8)\nax.set_title(\"Merchandise Distribution\")\nax.set_ylabel(\"\")\n \n# 3f. Time distribution (sample 200k rows for speed)\nax = axes[1, 2]\nsample_time = bids[\"time\"].sample(min(200_000, len(bids)), random_state=42)\nax.hist(sample_time, bins=100, color=PALETTE[\"human\"], edgecolor=\"none\", alpha=0.8)\nax.set_title(\"Bid Timestamp Distribution (sample)\")\nax.set_xlabel(\"Time\"); ax.set_ylabel(\"Count\")\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"01_raw_overview\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:31:57.265037Z","iopub.execute_input":"2026-04-16T15:31:57.265408Z","iopub.status.idle":"2026-04-16T15:32:02.123385Z","shell.execute_reply.started":"2026-04-16T15:31:57.265382Z","shell.execute_reply":"2026-04-16T15:32:02.122702Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 4. DATA CLEANING\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"4. DATA CLEANING\")\nprint(\"=\"*60)\n \nbids_clean = bids.copy()\ntrain_clean = train.copy()\ntest_clean  = test.copy()\n \n# ── 4a. Duplicates ───────────────────────────────────────────────────────────\ndup_bids = bids_clean.duplicated(subset=\"bid_id\").sum()\nprint(f\"Duplicate bid_ids in bids.csv: {dup_bids}\")\nbids_clean.drop_duplicates(subset=\"bid_id\", inplace=True)\n \n# ── 4b. Null handling ────────────────────────────────────────────────────────\n# country: fill missing with 'UNKNOWN'\nnull_country = bids_clean[\"country\"].isnull().sum()\nbids_clean[\"country\"] = bids_clean[\"country\"].fillna(\"UNKNOWN\")\nprint(f\"country nulls filled: {null_country}\")\n \n# url: fill with 'UNKNOWN', then extract domain prefix (first segment)\nnull_url = bids_clean[\"url\"].isnull().sum()\nbids_clean[\"url\"] = bids_clean[\"url\"].fillna(\"UNKNOWN\")\nprint(f\"url nulls filled: {null_url}\")\n \n# ip: fill with UNKNOWN\nnull_ip = bids_clean[\"ip\"].isnull().sum()\nbids_clean[\"ip\"] = bids_clean[\"ip\"].fillna(\"UNKNOWN\")\nprint(f\"ip nulls filled: {null_ip}\")\n \n# address (train/test): fill with UNKNOWN\nfor df in [train_clean, test_clean]:\n    df[\"address\"] = df[\"address\"].fillna(\"UNKNOWN\")\n    df[\"payment_account\"] = df[\"payment_account\"].fillna(\"UNKNOWN\")\n \n# ── 4c. Outlier audit — bids per bidder ─────────────────────────────────────\nbpb = bids_clean.groupby(\"bidder_id\").size()\nz_scores = np.abs(stats.zscore(bpb))\noutlier_bidders = bpb[z_scores > 4]\nprint(f\"\\nBidders with z>4 bid count: {len(outlier_bidders)}\")\nprint(outlier_bidders.sort_values(ascending=False).head(10))\n \n# ── 4d. Time sanity — detect zero-interval bursts ────────────────────────────\nbids_clean = bids_clean.sort_values([\"bidder_id\", \"time\"])\nbids_clean[\"time_diff\"] = bids_clean.groupby(\"bidder_id\")[\"time\"].diff()\n\nzero_interval = bids_clean[\"time_diff\"].eq(0).sum()\nprint(f\"\\nBids with zero time-diff to prev bid (same bidder): {zero_interval} \"\n      f\"({zero_interval/len(bids_clean)*100:.1f}%)\")\n \nprint(f\"\\nCleaned bids shape: {bids_clean.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:32:02.124349Z","iopub.execute_input":"2026-04-16T15:32:02.124643Z","iopub.status.idle":"2026-04-16T15:32:15.074182Z","shell.execute_reply.started":"2026-04-16T15:32:02.124619Z","shell.execute_reply":"2026-04-16T15:32:15.073430Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 5. FEATURE ENGINEERING\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"5. FEATURE ENGINEERING\")\nprint(\"=\"*60)\n \n# ── Helper: entropy ───────────────────────────────────────────────────────────\ndef entropy(series):\n    p = series.value_counts(normalize=True)\n    return -np.sum(p * np.log2(p + 1e-9))\n \n# ── 5a. Aggregate bids → per-bidder features ─────────────────────────────────\ng = bids_clean.groupby(\"bidder_id\")\n \nfeats = pd.DataFrame({\n    # Volume\n    \"bid_count\"             : g[\"bid_id\"].count(),\n    \"unique_auctions\"       : g[\"auction\"].nunique(),\n    \"unique_devices\"        : g[\"device\"].nunique(),\n    \"unique_countries\"      : g[\"country\"].nunique(),\n    \"unique_ips\"            : g[\"ip\"].nunique(),\n    \"unique_urls\"           : g[\"url\"].nunique(),\n    \"unique_merchandise\"    : g[\"merchandise\"].nunique(),\n \n    # Ratios (diversity / activity)\n    \"bids_per_auction\"      : g[\"bid_id\"].count() / g[\"auction\"].nunique(),\n    \"ips_per_bid\"           : g[\"ip\"].nunique() / g[\"bid_id\"].count(),\n    \"countries_per_bid\"     : g[\"country\"].nunique() / g[\"bid_id\"].count(),\n    \"devices_per_bid\"       : g[\"device\"].nunique() / g[\"bid_id\"].count(),\n \n    # Time-based\n    \"time_span\"             : g[\"time\"].max() - g[\"time\"].min(),\n    \"time_mean\"             : g[\"time\"].mean(),\n    \"time_std\"              : g[\"time\"].std().fillna(0),\n    \"time_diff_mean\"        : g[\"time_diff\"].mean(),\n    \"time_diff_std\"         : g[\"time_diff\"].std().fillna(0),\n    \"time_diff_min\"         : g[\"time_diff\"].min(),\n    \"zero_interval_count\"   : g[\"time_diff\"].apply(lambda x: x.dropna().eq(0).sum()),\n    \"zero_interval_ratio\"   : g[\"time_diff\"].apply(\n        lambda x: x.dropna().eq(0).mean() if x.dropna().shape[0] > 0 else 0\n    ),\n \n    # Entropy (information diversity)\n    \"country_entropy\"       : g[\"country\"].apply(entropy),\n    \"device_entropy\"        : g[\"device\"].apply(entropy),\n    \"url_entropy\"           : g[\"url\"].apply(entropy),\n    \"merchandise_entropy\"   : g[\"merchandise\"].apply(entropy),\n})\n \n# ── 5b. Merchandise one-hot pivots ────────────────────────────────────────────\nmerch_dummies = (bids_clean.groupby([\"bidder_id\", \"merchandise\"])\n                            .size()\n                            .unstack(fill_value=0))\nmerch_dummies.columns = [f\"merch_{c}\" for c in merch_dummies.columns]\nmerch_props = merch_dummies.div(merch_dummies.sum(axis=1), axis=0)\nmerch_props.columns = [f\"{c}_ratio\" for c in merch_dummies.columns]\n \n# ── 5c. Top-5 country flags ───────────────────────────────────────────────────\ntrain_bidder_ids = set(train_clean[\"bidder_id\"])\nbids_train_only = bids_clean[bids_clean[\"bidder_id\"].isin(train_bidder_ids)]\n\ntop_countries = bids_train_only[\"country\"].value_counts().head(5).index.tolist()\ncountry_flags = (bids_clean.assign(flag=bids_clean[\"country\"].isin(top_countries))\n                             .groupby(\"bidder_id\")[\"flag\"]\n                             .mean()\n                             .rename(\"top5_country_ratio\"))\n \n# ── 5d. IP subnet features ────────────────────────────────────────────────────\ndef ip_prefix(ip_series, parts=2):\n    \"\"\"Extract /16 subnet (first 2 octets) from IPv4, ignoring non-standard.\"\"\"\n    def extract(ip):\n        segs = str(ip).split(\".\")\n        return \".\".join(segs[:parts]) if len(segs) >= parts else \"OTHER\"\n    return ip_series.apply(extract)\n \nbids_clean[\"ip_subnet\"] = ip_prefix(bids_clean[\"ip\"])\nfeats[\"unique_subnets\"] = g[\"ip_subnet\"].nunique()\nfeats[\"subnets_per_bid\"] = feats[\"unique_subnets\"] / feats[\"bid_count\"]\n \n# ── 5e. Time-prefix burst features ────────────────────────────────────────────\n# Bots fire bids in tight clusters that share the same high-order timestamp\n# digits. For each prefix length we count the largest single-prefix burst\n# per bidder — a strong discriminator between scripted and human behaviour.\n#\n# time values are large integers (nanoseconds or ticks); stripping the last\n# N digits collapses bids into \"same-moment\" buckets at varying resolutions.\ntime_max_digits = len(str(int(bids_clean[\"time\"].max())))\n \nfor n_strip in [1, 2, 3, 4, 5]:               # coarse → fine granularity\n    divisor = 10 ** n_strip\n    col_name = f\"time_prefix_strip{n_strip}\"\n    bids_clean[col_name] = (bids_clean[\"time\"] // divisor).astype(np.int64)\n \n    # max burst size = largest number of bids sharing one prefix bucket\n    burst_max = (bids_clean\n                 .groupby([\"bidder_id\", col_name])\n                 .size()\n                 .groupby(\"bidder_id\").max()\n                 .rename(f\"burst_max_strip{n_strip}\"))\n \n    # mean burst size across all buckets (bidder-level burstiness)\n    burst_mean = (bids_clean\n                  .groupby([\"bidder_id\", col_name])\n                  .size()\n                  .groupby(\"bidder_id\").mean()\n                  .rename(f\"burst_mean_strip{n_strip}\"))\n \n    feats = feats.join(burst_max,  how=\"left\")\n    feats = feats.join(burst_mean, how=\"left\")\n \n# Ratio: max burst relative to total bids (normalised burstiness)\nfor n_strip in [1, 2, 3, 4, 5]:\n    feats[f\"burst_ratio_strip{n_strip}\"] = (\n        feats[f\"burst_max_strip{n_strip}\"] / feats[\"bid_count\"]\n    )\n \nprint(f\"  Time-prefix features added — strips 1–5 (max/mean/ratio each)\")\n \n# Drop temp prefix columns from bids_clean to save memory\nbids_clean.drop(columns=[c for c in bids_clean.columns\n                          if c.startswith(\"time_prefix_strip\")], inplace=True)\n \n# ── 5f. Combine all features ──────────────────────────────────────────────────\nall_feats = (feats\n             .join(merch_props, how=\"left\")\n             .join(country_flags, how=\"left\")\n             .fillna(0))\n \n# Log-transform heavy-tailed features\nfor col in [\"bid_count\", \"unique_auctions\", \"unique_ips\", \"unique_urls\",\n            \"time_span\", \"time_std\"] + \\\n           [f\"burst_max_strip{n}\" for n in [1,2,3,4,5]]:\n    all_feats[f\"log_{col}\"] = np.log1p(all_feats[col])\n \nprint(f\"Feature matrix shape: {all_feats.shape}\")\nprint(all_feats.dtypes.value_counts())\n \n# ── 5g. Merge with train/test ─────────────────────────────────────────────────\ntrain_feat = (train_clean\n              .merge(all_feats.reset_index(), on=\"bidder_id\", how=\"left\")\n              .fillna(0))\ntest_feat  = (test_clean\n              .merge(all_feats.reset_index(), on=\"bidder_id\", how=\"left\")\n              .fillna(0))\n \nprint(f\"\\ntrain_feat shape : {train_feat.shape}\")\nprint(f\"test_feat  shape : {test_feat.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:32:15.075314Z","iopub.execute_input":"2026-04-16T15:32:15.076181Z","iopub.status.idle":"2026-04-16T15:33:31.931336Z","shell.execute_reply.started":"2026-04-16T15:32:15.076153Z","shell.execute_reply":"2026-04-16T15:33:31.930594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 6. VISUALISATION — CLEANED + ENGINEERED DATA\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"6. CLEANED / ENGINEERED DATA VISUALISATION\")\nprint(\"=\"*60)\n \nlabel_map = {0: \"human\", 1: \"bot\"}\ntrain_feat[\"label\"] = train_feat[\"outcome\"].map(label_map)\ncolors = [PALETTE[\"human\"], PALETTE[\"bot\"]]\n \n# ── 6a. Key feature distributions — human vs bot ─────────────────────────────\nkey_feats = [\n    (\"log_bid_count\",           \"Log Bid Count\"),\n    (\"log_unique_ips\",          \"Log Unique IPs\"),\n    (\"zero_interval_ratio\",     \"Zero-interval Ratio\"),\n    (\"country_entropy\",         \"Country Entropy\"),\n    (\"burst_ratio_strip3\",      \"Burst Ratio (strip-3)\"),\n    (\"burst_max_strip4\",        \"Max Burst Size (strip-4)\"),\n]\n \nfig, axes = plt.subplots(2, 3, figsize=(18, 10))\nfig.suptitle(\"Feature Distributions: Human vs Bot\", fontsize=16)\n \nfor ax, (col, title) in zip(axes.flat, key_feats):\n    for i, (label, grp) in enumerate(train_feat.groupby(\"label\")):\n        vals = grp[col].dropna()\n        ax.hist(vals, bins=50, alpha=0.65, color=colors[i],\n                label=label, edgecolor=\"none\", density=True)\n    ax.set_title(title); ax.legend()\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"02_feature_distributions\")\n \n# ── 6b. Correlation heatmap (top numeric features) ───────────────────────────\nnum_cols = [c for c in train_feat.columns if pd.api.types.is_numeric_dtype(train_feat[c])]\ncorr = train_feat[num_cols].corr()\n# keep only top 20 most correlated with outcome\noutcome_corr = corr[\"outcome\"].abs().sort_values(ascending=False).head(21).index\nsub_corr = train_feat[list(outcome_corr)].corr()\n \nfig, ax = plt.subplots(figsize=(14, 12))\nsns.heatmap(sub_corr, cmap=\"coolwarm\", center=0, vmin=-1, vmax=1,\n            linewidths=0.3, ax=ax, cbar_kws={\"shrink\": 0.7},\n            annot=True, fmt=\".2f\", annot_kws={\"size\": 7})\nax.set_title(\"Feature Correlation Heatmap (top 20 + outcome)\")\nplt.tight_layout()\nplt.show()\nsave(fig, \"03_correlation_heatmap\")\n \n# ── 6c. Boxplots — raw vs log-transformed ─────────────────────────────────────\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\nfig.suptitle(\"Raw vs Log-Transformed: Bid Count & Unique IPs\", fontsize=14)\n \nfor i, (raw, log, title) in enumerate([\n    (\"bid_count\", \"log_bid_count\", \"Bid Count\"),\n    (\"unique_ips\", \"log_unique_ips\", \"Unique IPs\"),\n]):\n    for j, (col, label) in enumerate([(raw, \"Raw\"), (log, \"Log+1\")]):\n        ax = axes[i][j]\n        data_h = train_feat.loc[train_feat[\"label\"] == \"human\", col]\n        data_b = train_feat.loc[train_feat[\"label\"] == \"bot\",   col]\n        ax.boxplot([data_h, data_b], labels=[\"Human\", \"Bot\"],\n                   patch_artist=True,\n                   boxprops=dict(facecolor=\"none\", color=\"#A0A0A0\"),\n                   medianprops=dict(color=\"#FFD700\", linewidth=2),\n                   whiskerprops=dict(color=\"#A0A0A0\"),\n                   capprops=dict(color=\"#A0A0A0\"),\n                  flierprops=dict(\n                       marker=\"o\",                  # Use a full circle instead of a tiny dot\n                       markersize=4,                # Doubled the size for better visibility\n                       alpha=0.6,                   # Transparency helps if there is dense overlapping\n                       markerfacecolor=\"#FF6B6B\",   # Specifically set the fill color\n                       markeredgecolor=\"none\"       # Remove the edge so colors don't muddy together\n                   ))\n        ax.set_title(f\"{title} — {label}\")\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"04_raw_vs_log_boxplots\")\n \n# ── 6d. Zero-interval ratio vs bid count (scatter) ────────────────────────────\nfig, ax = plt.subplots(figsize=(10, 7))\nfor label, grp in train_feat.groupby(\"label\"):\n    ax.scatter(grp[\"log_bid_count\"], grp[\"zero_interval_ratio\"],\n               c=PALETTE[label], alpha=0.5, s=15, label=label, edgecolors=\"none\")\nax.set_xlabel(\"Log Bid Count\"); ax.set_ylabel(\"Zero-Interval Ratio\")\nax.set_title(\"Zero-Interval Ratio vs Log Bid Count\")\nax.legend()\nplt.tight_layout()\nplt.show()\nsave(fig, \"05_zero_interval_scatter\")\n \n# ── 6e. Merchandise ratio — bot vs human ──────────────────────────────────────\nmerch_cols = [c for c in train_feat.columns if c.startswith(\"merch_\") and \"ratio\" in c]\nif merch_cols:\n    means = train_feat.groupby(\"label\")[merch_cols].mean().T\n    means.index = [c.replace(\"merch_\",\"\").replace(\"_ratio\",\"\") for c in means.index]\n    fig, ax = plt.subplots(figsize=(12, 5))\n    means.plot.bar(ax=ax, color=[PALETTE[\"human\"], PALETTE[\"bot\"]], edgecolor=\"none\",\n                   rot=30)\n    ax.set_title(\"Merchandise Category Ratio — Human vs Bot\")\n    ax.set_ylabel(\"Mean ratio\"); ax.legend()\n    plt.tight_layout()\n    plt.show()\n    save(fig, \"06_merchandise_ratio\")\n \n# ── 6f. Country entropy comparison ───────────────────────────────────────────\nfig, ax = plt.subplots(figsize=(9, 5))\nfor i, (label, grp) in enumerate(train_feat.groupby(\"label\")):\n    ax.hist(grp[\"country_entropy\"], bins=40, alpha=0.7, color=colors[i],\n            label=label, edgecolor=\"none\", density=True)\nax.set_xlabel(\"Country Entropy\"); ax.set_ylabel(\"Density\")\nax.set_title(\"Country Entropy Distribution — Human vs Bot\")\nax.legend()\nplt.tight_layout()\nplt.show()\nsave(fig, \"07_country_entropy\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:31.932361Z","iopub.execute_input":"2026-04-16T15:33:31.932667Z","iopub.status.idle":"2026-04-16T15:33:39.527141Z","shell.execute_reply.started":"2026-04-16T15:33:31.932644Z","shell.execute_reply":"2026-04-16T15:33:39.526385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 7. EXPORT CLEANED FEATURES\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"7. EXPORTING\")\nprint(\"=\"*60)\n \ntrain_feat.drop(columns=[\"label\"], inplace=True)\ntrain_feat.to_csv(\"train_features.csv\", index=False)\ntest_feat.to_csv(\"test_features.csv\",   index=False)\nall_feats.to_csv(\"bidder_features.csv\")\n \nprint(f\"train_features.csv  → {train_feat.shape}\")\nprint(f\"test_features.csv   → {test_feat.shape}\")\nprint(f\"bidder_features.csv → {all_feats.shape}\")\n \n# ── 7a. Feature summary ──────────────────────────────────────────────────────\nfeat_cols = [c for c in train_feat.columns\n             if c not in [\"bidder_id\", \"payment_account\", \"address\", \"outcome\"]]\nprint(f\"\\nTotal engineered features : {len(feat_cols)}\")\n \nfeature_groups = {\n    \"Volume\"      : [c for c in feat_cols if any(x in c for x in [\"count\",\"unique\",\"log\"])],\n    \"Ratio\"       : [c for c in feat_cols if \"ratio\" in c or \"per_\" in c],\n    \"Time\"        : [c for c in feat_cols if \"time\" in c],\n    \"Burst\"       : [c for c in feat_cols if \"burst\" in c],\n    \"Entropy\"     : [c for c in feat_cols if \"entropy\" in c],\n    \"Merchandise\" : [c for c in feat_cols if \"merch\" in c],\n    \"Geography\"   : [c for c in feat_cols if \"country\" in c or \"subnet\" in c],\n}\n \nfor group, cols in feature_groups.items():\n    print(f\"  {group:15s}: {len(cols):3d} features\")\n \nprint(\"\\n✓ Preprocessing pipeline complete.\")\nprint(f\"  Plots saved to   ./{SAVE_DIR}/\")\nprint(f\"  Features saved to ./train_features.csv, test_features.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:39.528132Z","iopub.execute_input":"2026-04-16T15:33:39.528519Z","iopub.status.idle":"2026-04-16T15:33:40.306068Z","shell.execute_reply.started":"2026-04-16T15:33:39.528478Z","shell.execute_reply":"2026-04-16T15:33:40.305203Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cleaned_bids = pd.read_csv('/kaggle/working/bidder_features.csv')\ncleaned_train = pd.read_csv('/kaggle/working/train_features.csv')\ncleaned_test = pd.read_csv('/kaggle/working/test_features.csv')\n\ncleaned_bids.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:40.307136Z","iopub.execute_input":"2026-04-16T15:33:40.307500Z","iopub.status.idle":"2026-04-16T15:33:40.483838Z","shell.execute_reply.started":"2026-04-16T15:33:40.307449Z","shell.execute_reply":"2026-04-16T15:33:40.483150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cleaned_bids.tail()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:40.484920Z","iopub.execute_input":"2026-04-16T15:33:40.485221Z","iopub.status.idle":"2026-04-16T15:33:40.506779Z","shell.execute_reply.started":"2026-04-16T15:33:40.485189Z","shell.execute_reply":"2026-04-16T15:33:40.505918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cleaned_train.tail()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:40.508626Z","iopub.execute_input":"2026-04-16T15:33:40.509021Z","iopub.status.idle":"2026-04-16T15:33:40.534588Z","shell.execute_reply.started":"2026-04-16T15:33:40.508995Z","shell.execute_reply":"2026-04-16T15:33:40.533674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cleaned_train.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:40.538339Z","iopub.execute_input":"2026-04-16T15:33:40.538780Z","iopub.status.idle":"2026-04-16T15:33:40.561802Z","shell.execute_reply.started":"2026-04-16T15:33:40.538734Z","shell.execute_reply":"2026-04-16T15:33:40.560935Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cleaned_test.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:40.562850Z","iopub.execute_input":"2026-04-16T15:33:40.563141Z","iopub.status.idle":"2026-04-16T15:33:40.591537Z","shell.execute_reply.started":"2026-04-16T15:33:40.563120Z","shell.execute_reply":"2026-04-16T15:33:40.590918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cleaned_test.tail()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:40.592389Z","iopub.execute_input":"2026-04-16T15:33:40.592669Z","iopub.status.idle":"2026-04-16T15:33:40.616921Z","shell.execute_reply.started":"2026-04-16T15:33:40.592640Z","shell.execute_reply":"2026-04-16T15:33:40.616259Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Random Forest Classifier\n=================================================================\n## Requires: train_features.csv, test_features.csv  (output of preprocess.py)\n## Outputs : submission.csv, plots/rf_*.png","metadata":{}},{"cell_type":"code","source":"# ─── Imports ──────────────────────────────────────────────────────────────────\nimport os, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n \nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.model_selection import StratifiedKFold, cross_val_score\nfrom sklearn.metrics import (roc_auc_score, roc_curve, confusion_matrix,\n                              classification_report, precision_recall_curve,\n                              average_precision_score)\nfrom sklearn.inspection import permutation_importance\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.preprocessing import StandardScaler\nfrom imblearn.over_sampling import SMOTE\nfrom scipy.stats import randint, uniform\nfrom sklearn.model_selection import RandomizedSearchCV\n \nwarnings.filterwarnings(\"ignore\")\n \n# ─── Aesthetic config ─────────────────────────────────────────────────────────\nPALETTE = {\"bot\": \"#E84545\", \"human\": \"#2B9EB3\", \"neutral\": \"#6C757D\"}\nplt.rcParams.update({\"figure.dpi\": 140, \"figure.facecolor\": \"#0F1117\",\n                     \"axes.facecolor\": \"#1A1D27\", \"axes.labelcolor\": \"#E0E0E0\",\n                     \"xtick.color\": \"#A0A0A0\", \"ytick.color\": \"#A0A0A0\",\n                     \"text.color\": \"#E0E0E0\", \"grid.color\": \"#2A2D37\",\n                     \"axes.spines.top\": False, \"axes.spines.right\": False})\n \nSAVE_DIR = \"plots\"\nos.makedirs(SAVE_DIR, exist_ok=True)\nSEED = 42\n \ndef save(fig, name):\n    fig.savefig(f\"{SAVE_DIR}/{name}.png\", bbox_inches=\"tight\",\n                facecolor=fig.get_facecolor())\n    plt.close(fig)\n    print(f\"  ↳ Saved  {SAVE_DIR}/{name}.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:40.617817Z","iopub.execute_input":"2026-04-16T15:33:40.618137Z","iopub.status.idle":"2026-04-16T15:33:41.771411Z","shell.execute_reply.started":"2026-04-16T15:33:40.618115Z","shell.execute_reply":"2026-04-16T15:33:41.770765Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 1. LOAD FEATURES\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"1. LOADING FEATURES\")\nprint(\"=\"*60)\n\nMETA_COLS = [\"bidder_id\", \"payment_account\", \"address\", \"outcome\", \"label\"]\n\ntrain = pd.read_csv(\"train_features.csv\")\ntest  = pd.read_csv(\"test_features.csv\")\n\nFEAT_COLS = [c for c in train.columns if c not in META_COLS]\n\nX      = train[FEAT_COLS].values.astype(np.float32)\ny      = train[\"outcome\"].values.astype(np.int32)\nX_test = test[FEAT_COLS].values.astype(np.float32)\n\ntrain_prior = y.mean()\nscale_pos   = (1 - train_prior) / train_prior   # for LightGBM pos weighting\n\nN_SPLITS = 5\nfolds = list(StratifiedKFold(n_splits=N_SPLITS, shuffle=True, random_state=SEED).split(X, y))\n\nprint(f\"Train  : X={X.shape}  bot_rate={train_prior:.4f}\")\nprint(f\"Test   : X_test={X_test.shape}\")\nprint(f\"scale_pos_weight = {scale_pos:.1f}  (class imbalance correction)\")\nprint(f\"Features ({len(FEAT_COLS)}): {FEAT_COLS[:6]} …\")\nprint(f\"Shared folds prepared: {len(folds)}\")\nfor i, (tr_idx, val_idx) in enumerate(folds, 1):\n    print(f\"  Fold {i}: train={len(tr_idx)}, val={len(val_idx)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:41.772593Z","iopub.execute_input":"2026-04-16T15:33:41.773145Z","iopub.status.idle":"2026-04-16T15:33:41.865306Z","shell.execute_reply.started":"2026-04-16T15:33:41.773109Z","shell.execute_reply":"2026-04-16T15:33:41.864577Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 2. BASELINE RF (quick sanity check)\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"2. BASELINE RF — 5-FOLD CV\")\nprint(\"=\"*60)\n\nrf_base = RandomForestClassifier(\n    n_estimators=300,\n    max_depth=None,\n    min_samples_leaf=2,\n    class_weight=\"balanced\",   # handles imbalance without SMOTE\n    max_features=\"sqrt\",\n    n_jobs=-1,\n    random_state=SEED,\n)\n\nbase_scores = cross_val_score(rf_base, X, y, cv=folds,\n                               scoring=\"roc_auc\", n_jobs=-1)\nprint(f\"Mean AUC: {base_scores.mean():.4f} ± {base_scores.std():.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:41.866260Z","iopub.execute_input":"2026-04-16T15:33:41.866627Z","iopub.status.idle":"2026-04-16T15:33:46.664459Z","shell.execute_reply.started":"2026-04-16T15:33:41.866572Z","shell.execute_reply":"2026-04-16T15:33:46.663488Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 3. HYPERPARAMETER SEARCH\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"3. RANDOMIZED SEARCH (n_iter=30)\")\nprint(\"=\"*60)\n \nparam_dist = {\n    \"n_estimators\"    : randint(200, 800),\n    \"max_depth\"       : [None, 10, 20, 30, 40],\n    \"min_samples_leaf\": randint(1, 10),\n    \"min_samples_split\": randint(2, 20),\n    \"max_features\"    : [\"sqrt\", \"log2\", 0.3, 0.5],\n    \"class_weight\"    : [\"balanced\", \"balanced_subsample\"],\n}\n \nsearch = RandomizedSearchCV(\n    RandomForestClassifier(n_jobs=-1, random_state=SEED),\n    param_distributions=param_dist,\n    n_iter=30,\n    scoring=\"roc_auc\",\n    cv=folds,\n    refit=True,\n    verbose=1,\n    random_state=SEED,\n    n_jobs=-1,\n)\nsearch.fit(X, y)\n \nprint(f\"\\nBest params : {search.best_params_}\")\nprint(f\"Best CV AUC : {search.best_score_:.4f}\")\n \nbest_rf = search.best_estimator_","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:33:46.665597Z","iopub.execute_input":"2026-04-16T15:33:46.666022Z","iopub.status.idle":"2026-04-16T15:37:07.971448Z","shell.execute_reply.started":"2026-04-16T15:33:46.665996Z","shell.execute_reply":"2026-04-16T15:37:07.970593Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 4. FULL TRAIN + OOF PREDICTIONS (for metrics)\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"4. OUT-OF-FOLD PREDICTIONS\")\nprint(\"=\"*60)\n\noof_proba = np.zeros(len(y), dtype=np.float32)\n\nfor fold, (tr_idx, val_idx) in enumerate(folds, start=1):\n    X_tr, X_val = X[tr_idx], X[val_idx]\n    y_tr, y_val = y[tr_idx], y[val_idx]\n\n    fold_rf = RandomForestClassifier(**search.best_params_,\n                                     n_jobs=-1, random_state=SEED)\n    fold_rf.fit(X_tr, y_tr)\n    oof_proba[val_idx] = fold_rf.predict_proba(X_val)[:, 1]\n\n    fold_auc = roc_auc_score(y_val, oof_proba[val_idx])\n    print(f\"  Fold {fold}  AUC={fold_auc:.4f}\")\n\noof_auc = roc_auc_score(y, oof_proba)\nprint(f\"\\nOOF AUC (all folds) : {oof_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:07.972456Z","iopub.execute_input":"2026-04-16T15:37:07.972804Z","iopub.status.idle":"2026-04-16T15:37:13.193462Z","shell.execute_reply.started":"2026-04-16T15:37:07.972779Z","shell.execute_reply":"2026-04-16T15:37:13.192770Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 5. EVALUATION PLOTS\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"5. EVALUATION PLOTS\")\nprint(\"=\"*60)\n \n# ── 5a. ROC + PR curves ───────────────────────────────────────────────────────\nfpr, tpr, _ = roc_curve(y, oof_proba)\nprec, rec, _ = precision_recall_curve(y, oof_proba)\nap = average_precision_score(y, oof_proba)\n \nfig, axes = plt.subplots(1, 2, figsize=(14, 6))\nfig.suptitle(f\"Random Forest — OOF AUC={oof_auc:.4f}\", fontsize=14)\n \nax = axes[0]\nax.plot(fpr, tpr, color=PALETTE[\"bot\"], lw=2, label=f\"ROC (AUC={oof_auc:.4f})\")\nax.plot([0,1],[0,1], \"--\", color=\"#555\", lw=1)\nax.fill_between(fpr, tpr, alpha=0.12, color=PALETTE[\"bot\"])\nax.set_xlabel(\"FPR\"); ax.set_ylabel(\"TPR\"); ax.set_title(\"ROC Curve\"); ax.legend()\n \nax = axes[1]\nax.plot(rec, prec, color=PALETTE[\"human\"], lw=2, label=f\"PR (AP={ap:.4f})\")\nax.fill_between(rec, prec, alpha=0.12, color=PALETTE[\"human\"])\nax.axhline(y.mean(), color=\"#555\", lw=1, linestyle=\"--\", label=\"Chance\")\nax.set_xlabel(\"Recall\"); ax.set_ylabel(\"Precision\")\nax.set_title(\"Precision-Recall Curve\"); ax.legend()\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"rf_01_roc_pr\")\n \n# ── 5b. Confusion matrix at 0.5 threshold ────────────────────────────────────\noof_pred = (oof_proba >= 0.5).astype(int)\ncm = confusion_matrix(y, oof_pred)\nprint(\"\\nClassification Report:\\n\")\nprint(classification_report(y, oof_pred, target_names=[\"Human\", \"Bot\"]))\n \nfig, ax = plt.subplots(figsize=(6, 5))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\", ax=ax,\n            xticklabels=[\"Human\", \"Bot\"], yticklabels=[\"Human\", \"Bot\"],\n            linewidths=0.5, cbar=False)\nax.set_xlabel(\"Predicted\"); ax.set_ylabel(\"Actual\")\nax.set_title(\"Confusion Matrix (threshold=0.5)\")\nplt.tight_layout()\nplt.show()\nsave(fig, \"rf_02_confusion_matrix\")\n \n# ── 5c. Probability distribution ─────────────────────────────────────────────\nfig, ax = plt.subplots(figsize=(10, 5))\nax.hist(oof_proba[y == 0], bins=60, alpha=0.7, color=PALETTE[\"human\"],\n        label=\"Human\", density=True, edgecolor=\"none\")\nax.hist(oof_proba[y == 1], bins=60, alpha=0.7, color=PALETTE[\"bot\"],\n        label=\"Bot\", density=True, edgecolor=\"none\")\nax.axvline(0.5, color=\"#FFD700\", linestyle=\"--\", lw=1.5, label=\"threshold=0.5\")\nax.set_xlabel(\"Predicted Probability (Bot)\"); ax.set_ylabel(\"Density\")\nax.set_title(\"OOF Predicted Probability Distribution\")\nax.legend()\nplt.tight_layout()\nplt.show()\nsave(fig, \"rf_03_proba_dist\")\n \n# ── 5d. Feature importance (Gini + Permutation) ───────────────────────────────\n# Gini importance from best_rf (already fitted on full data by refit=True)\ngini_imp = pd.Series(best_rf.feature_importances_, index=FEAT_COLS)\ntop20_gini = gini_imp.nlargest(20)\n \n# Permutation importance (on OOF fold 0 for speed)\ntr0, va0 = folds[0]\nperm_rf = RandomForestClassifier(**search.best_params_, n_jobs=-1, random_state=SEED)\nperm_rf.fit(X[tr0], y[tr0])\nperm_res = permutation_importance(perm_rf, X[va0], y[va0],\n                                   scoring=\"roc_auc\", n_repeats=10,\n                                   n_jobs=-1, random_state=SEED)\nperm_imp = pd.Series(perm_res.importances_mean, index=FEAT_COLS)\ntop20_perm = perm_imp.nlargest(20)\n \nfig, axes = plt.subplots(1, 2, figsize=(18, 8))\nfig.suptitle(\"Feature Importance\", fontsize=14)\n \nfor ax, imp, title in zip(axes,\n                           [top20_gini, top20_perm],\n                           [\"Gini Importance (top 20)\", \"Permutation Importance (top 20)\"]):\n    bars = ax.barh(range(len(imp)), imp.values[::-1],\n                   color=PALETTE[\"bot\"], edgecolor=\"none\")\n    ax.set_yticks(range(len(imp)))\n    ax.set_yticklabels(imp.index[::-1], fontsize=9)\n    ax.set_title(title); ax.set_xlabel(\"Importance\")\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"rf_04_feature_importance\")\n \n# ── 5e. CV score distribution ─────────────────────────────────────────────────\ncv_results = pd.DataFrame(search.cv_results_)\nfig, ax = plt.subplots(figsize=(10, 4))\nax.plot(range(len(cv_results)), cv_results[\"mean_test_score\"],\n        color=PALETTE[\"human\"], lw=1.5, label=\"Mean CV AUC\")\nax.fill_between(range(len(cv_results)),\n                cv_results[\"mean_test_score\"] - cv_results[\"std_test_score\"],\n                cv_results[\"mean_test_score\"] + cv_results[\"std_test_score\"],\n                alpha=0.2, color=PALETTE[\"human\"])\nax.axhline(search.best_score_, color=PALETTE[\"bot\"], lw=1.5,\n           linestyle=\"--\", label=f\"Best={search.best_score_:.4f}\")\nax.set_xlabel(\"Search Iteration\"); ax.set_ylabel(\"CV AUC\")\nax.set_title(\"RandomizedSearch CV AUC Across Iterations\"); ax.legend()\nplt.tight_layout()\nplt.show()\nsave(fig, \"rf_05_search_scores\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:13.194431Z","iopub.execute_input":"2026-04-16T15:37:13.194751Z","iopub.status.idle":"2026-04-16T15:37:31.770077Z","shell.execute_reply.started":"2026-04-16T15:37:13.194727Z","shell.execute_reply":"2026-04-16T15:37:31.769421Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 6. FINAL PREDICTION + SUBMISSION\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"6. FINAL PREDICTION\")\nprint(\"=\"*60)\n \n# Retrain best RF on full training set\nbest_rf.fit(X, y)\ntest_proba = best_rf.predict_proba(X_test)[:, 1]\n \nsubmission = pd.DataFrame({\n    \"bidder_id\": test[\"bidder_id\"],\n    \"prediction\": test_proba,\n})\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(f\"submission.csv  shape={submission.shape}\")\nprint(submission.head(10).to_string())\nprint(f\"\\nPredicted bot rate (test): {test_proba.mean():.3f}\")\n \nprint(\"\\n✓ Random Forest pipeline complete.\")\nprint(f\"  OOF AUC   : {oof_auc:.4f}\")\nprint(f\"  Best CV   : {search.best_score_:.4f}\")\nprint(f\"  Plots     : ./{SAVE_DIR}/rf_*.png\")\nprint(f\"  Submission: ./submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:31.771161Z","iopub.execute_input":"2026-04-16T15:37:31.771576Z","iopub.status.idle":"2026-04-16T15:37:32.969770Z","shell.execute_reply.started":"2026-04-16T15:37:31.771550Z","shell.execute_reply":"2026-04-16T15:37:32.969005Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# RF SUMMARY LOG\n# =============================================================================\nrf_summary_df = pd.DataFrame([{\n    \"model\": \"RandomForest\",\n    \"oof_auc\": float(oof_auc),\n    \"oof_ap\": float(average_precision_score(y, oof_proba)),\n    \"oof_pred_bot_rate\": float(oof_proba.mean()),\n    \"test_pred_bot_rate\": float(test_proba.mean()),\n}])\n\nrf_summary_df.to_csv(\"logs/rf_summary.csv\", index=False)\nprint(\"Saved logs/rf_summary.csv\")\nprint(rf_summary_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:32.970802Z","iopub.execute_input":"2026-04-16T15:37:32.971593Z","iopub.status.idle":"2026-04-16T15:37:32.981009Z","shell.execute_reply.started":"2026-04-16T15:37:32.971567Z","shell.execute_reply":"2026-04-16T15:37:32.980105Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LightGBM + Calibration\n================================================================\n## Requires : train_features.csv, test_features.csv  (output of preprocess.py)\n## Outputs  : submission.csv, plots/lgbm_*.png\n## Install  : pip install lightgbm scikit-learn matplotlib seaborn","metadata":{}},{"cell_type":"code","source":"# ─── Imports ──────────────────────────────────────────────────────────────────\nimport os, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport lightgbm as lgb\n \nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.calibration import CalibratedClassifierCV, calibration_curve\nfrom sklearn.metrics import (roc_auc_score, roc_curve,\n                              precision_recall_curve, average_precision_score,\n                              confusion_matrix, classification_report)\n \nwarnings.filterwarnings(\"ignore\")\n \n# ─── Aesthetic config ─────────────────────────────────────────────────────────\nPALETTE = {\"bot\": \"#E84545\", \"human\": \"#2B9EB3\", \"neutral\": \"#6C757D\",\n           \"calib\": \"#F5A623\", \"uncalib\": \"#9B59B6\"}\nplt.rcParams.update({\"figure.dpi\": 140, \"figure.facecolor\": \"#0F1117\",\n                     \"axes.facecolor\": \"#1A1D27\", \"axes.labelcolor\": \"#E0E0E0\",\n                     \"xtick.color\": \"#A0A0A0\", \"ytick.color\": \"#A0A0A0\",\n                     \"text.color\": \"#E0E0E0\", \"grid.color\": \"#2A2D37\",\n                     \"axes.spines.top\": False, \"axes.spines.right\": False})\n \nSAVE_DIR = \"plots\"\nos.makedirs(SAVE_DIR, exist_ok=True)\nSEED = 42\n \ndef save(fig, name):\n    fig.savefig(f\"{SAVE_DIR}/{name}.png\", bbox_inches=\"tight\",\n                facecolor=fig.get_facecolor())\n    plt.close(fig)\n    print(f\"  ↳ Saved  {SAVE_DIR}/{name}.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:32.982167Z","iopub.execute_input":"2026-04-16T15:37:32.982530Z","iopub.status.idle":"2026-04-16T15:37:36.768524Z","shell.execute_reply.started":"2026-04-16T15:37:32.982505Z","shell.execute_reply":"2026-04-16T15:37:36.767915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 1. LOAD FEATURES\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"1. LOADING FEATURES\")\nprint(\"=\"*60)\n \nMETA_COLS = [\"bidder_id\", \"payment_account\", \"address\", \"outcome\", \"label\"]\n \ntrain = pd.read_csv(\"train_features.csv\")\ntest  = pd.read_csv(\"test_features.csv\")\n \nFEAT_COLS = [c for c in train.columns if c not in META_COLS]\n \nX      = train[FEAT_COLS].values.astype(np.float32)\ny      = train[\"outcome\"].values.astype(np.int32)\nX_test = test[FEAT_COLS].values.astype(np.float32)\n \ntrain_prior = y.mean()\nscale_pos   = (1 - train_prior) / train_prior   # for LightGBM pos weighting\n \nprint(f\"Train  : X={X.shape}  bot_rate={train_prior:.4f}\")\nprint(f\"Test   : X_test={X_test.shape}\")\nprint(f\"scale_pos_weight = {scale_pos:.1f}  (class imbalance correction)\")\nprint(f\"Features ({len(FEAT_COLS)}): {FEAT_COLS[:6]} …\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:36.769540Z","iopub.execute_input":"2026-04-16T15:37:36.770527Z","iopub.status.idle":"2026-04-16T15:37:36.853356Z","shell.execute_reply.started":"2026-04-16T15:37:36.770497Z","shell.execute_reply":"2026-04-16T15:37:36.852631Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 2. LIGHTGBM PARAMS\n# =============================================================================\n# scale_pos_weight replaces class_weight=\"balanced\" — it correctly reflects\n# the true 5% bot prior rather than flattening it to 50/50, which was the\n# root cause of the inflated predicted bot rate with RandomForest.\n \nLGBM_PARAMS = dict(\n    objective          = \"binary\",\n    metric             = \"auc\",\n    boosting_type      = \"gbdt\",\n    n_estimators       = 1000,          # early stopping will trim this\n    learning_rate      = 0.03,\n    num_leaves         = 63,\n    max_depth          = -1,\n    min_child_samples  = 10,\n    feature_fraction   = 0.7,\n    bagging_fraction   = 0.8,\n    bagging_freq       = 1,\n    lambda_l1          = 0.1,\n    lambda_l2          = 1.0,\n    scale_pos_weight   = scale_pos,     # ← key fix for calibration\n    n_jobs             = -1,\n    random_state       = SEED,\n    verbose            = -1,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:36.854267Z","iopub.execute_input":"2026-04-16T15:37:36.854574Z","iopub.status.idle":"2026-04-16T15:37:36.859162Z","shell.execute_reply.started":"2026-04-16T15:37:36.854539Z","shell.execute_reply":"2026-04-16T15:37:36.858537Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 3. OUT-OF-FOLD PREDICTIONS WITH EARLY STOPPING\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"3. OOF TRAINING — 5-FOLD STRATIFIED CV\")\nprint(\"=\"*60)\n\noof_proba    = np.zeros(len(y), dtype=np.float64)\ntest_probas  = np.zeros((len(X_test), len(folds)), dtype=np.float64)  # per-fold test preds\nbest_iters   = []\n\nfor fold, (tr_idx, val_idx) in enumerate(folds, start=1):\n    X_tr, X_val = X[tr_idx], X[val_idx]\n    y_tr, y_val = y[tr_idx], y[val_idx]\n\n    model = lgb.LGBMClassifier(**LGBM_PARAMS)\n    model.fit(\n        X_tr, y_tr,\n        eval_set        = [(X_val, y_val)],\n        eval_metric     = \"auc\",\n        callbacks       = [\n            lgb.early_stopping(100, verbose=False),\n            lgb.log_evaluation(-1),\n        ],\n    )\n\n    oof_proba[val_idx] = model.predict_proba(X_val)[:, 1]\n    test_probas[:, fold - 1] = model.predict_proba(X_test)[:, 1]\n    best_iters.append(model.best_iteration_)\n\n    fold_auc = roc_auc_score(y_val, oof_proba[val_idx])\n    print(f\"  Fold {fold}  best_iter={model.best_iteration_:4d}  AUC={fold_auc:.4f}\")\n\noof_auc = roc_auc_score(y, oof_proba)\navg_iter = int(np.mean(best_iters))\ntest_proba = test_probas.mean(axis=1)\n\nprint(f\"\\nOOF AUC (all folds) : {oof_auc:.4f}\")\nprint(f\"Mean best_iteration : {avg_iter}\")\nprint(f\"Predicted bot rate  : {test_proba.mean():.4f}  (train prior={train_prior:.4f})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:36.860256Z","iopub.execute_input":"2026-04-16T15:37:36.860564Z","iopub.status.idle":"2026-04-16T15:37:39.836443Z","shell.execute_reply.started":"2026-04-16T15:37:36.860542Z","shell.execute_reply":"2026-04-16T15:37:39.835784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 4. PROBABILITY CALIBRATION (proper OOF)\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"4. ISOTONIC CALIBRATION (proper OOF)\")\nprint(\"=\"*60)\n\noof_calib_proba = np.zeros(len(y), dtype=np.float64)\ntest_calib_probas = np.zeros((len(X_test), len(folds)), dtype=np.float64)\n\nfor fold, (tr_idx, val_idx) in enumerate(folds, start=1):\n    X_tr, X_val = X[tr_idx], X[val_idx]\n    y_tr, y_val = y[tr_idx], y[val_idx]\n\n    base_model = lgb.LGBMClassifier(\n        **{**LGBM_PARAMS, \"n_estimators\": avg_iter}\n    )\n\n    calib_model = CalibratedClassifierCV(\n        base_model,\n        method=\"isotonic\",\n        cv=3\n    )\n\n    calib_model.fit(X_tr, y_tr)\n    oof_calib_proba[val_idx] = calib_model.predict_proba(X_val)[:, 1]\n    test_calib_probas[:, fold - 1] = calib_model.predict_proba(X_test)[:, 1]\n\ntest_proba_cal = test_calib_probas.mean(axis=1)\n\nprint(f\"Bot rate raw OOF   : {oof_proba.mean():.4f}\")\nprint(f\"Bot rate calib OOF : {oof_calib_proba.mean():.4f}\")\nprint(f\"Bot rate test cal  : {test_proba_cal.mean():.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:39.837482Z","iopub.execute_input":"2026-04-16T15:37:39.837722Z","iopub.status.idle":"2026-04-16T15:37:44.074793Z","shell.execute_reply.started":"2026-04-16T15:37:39.837699Z","shell.execute_reply":"2026-04-16T15:37:44.074050Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 5. EVALUATION PLOTS\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"5. EVALUATION PLOTS\")\nprint(\"=\"*60)\n \n# ── 5a. ROC + PR curves ───────────────────────────────────────────────────────\nfpr, tpr, _ = roc_curve(y, oof_proba)\nprec, rec, _ = precision_recall_curve(y, oof_proba)\nap = average_precision_score(y, oof_proba)\n \nfig, axes = plt.subplots(1, 2, figsize=(14, 6))\nfig.suptitle(f\"LightGBM — OOF AUC={oof_auc:.4f}\", fontsize=14)\n \nax = axes[0]\nax.plot(fpr, tpr, color=PALETTE[\"bot\"], lw=2, label=f\"ROC (AUC={oof_auc:.4f})\")\nax.plot([0,1],[0,1], \"--\", color=\"#555\", lw=1)\nax.fill_between(fpr, tpr, alpha=0.12, color=PALETTE[\"bot\"])\nax.set_xlabel(\"FPR\"); ax.set_ylabel(\"TPR\")\nax.set_title(\"ROC Curve\"); ax.legend()\n \nax = axes[1]\nax.plot(rec, prec, color=PALETTE[\"human\"], lw=2, label=f\"PR (AP={ap:.4f})\")\nax.fill_between(rec, prec, alpha=0.12, color=PALETTE[\"human\"])\nax.axhline(train_prior, color=\"#555\", lw=1, linestyle=\"--\", label=\"Chance\")\nax.set_xlabel(\"Recall\"); ax.set_ylabel(\"Precision\")\nax.set_title(\"Precision-Recall Curve\"); ax.legend()\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"lgbm_01_roc_pr\")\n \n# ── 5b. Confusion matrix ──────────────────────────────────────────────────────\noof_pred = (oof_calib_proba >= 0.5).astype(int)\ncm = confusion_matrix(y, oof_pred)\nprint(\"\\nClassification Report (proper OOF calibrated, threshold=0.5):\")\nprint(classification_report(y, oof_pred, target_names=[\"Human\", \"Bot\"]))\n\nfig, ax = plt.subplots(figsize=(6, 5))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\", ax=ax,\n            xticklabels=[\"Human\", \"Bot\"], yticklabels=[\"Human\", \"Bot\"],\n            linewidths=0.5, cbar=False)\nax.set_xlabel(\"Predicted\"); ax.set_ylabel(\"Actual\")\nax.set_title(\"Confusion Matrix (proper OOF calibrated, threshold=0.5)\")\nplt.tight_layout()\nplt.show()\nsave(fig, \"lgbm_02_confusion_matrix\")\n \n# ── 5c. Calibration curve (reliability diagram) ───────────────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(14, 6))\nfig.suptitle(\"Probability Calibration — Before vs After\", fontsize=14)\n \nfor ax, proba, label, color in [\n    (axes[0], oof_proba,        \"Uncalibrated (LightGBM OOF)\", PALETTE[\"uncalib\"]),\n    (axes[1], oof_calib_proba,  \"Calibrated (Isotonic)\",        PALETTE[\"calib\"]),\n]:\n    frac_pos, mean_pred = calibration_curve(y, proba, n_bins=10, strategy=\"quantile\")\n    ax.plot([0,1],[0,1], \"--\", color=\"#555\", lw=1, label=\"Perfect calibration\")\n    ax.plot(mean_pred, frac_pos, \"o-\", color=color, lw=2, label=label)\n    ax.fill_between(mean_pred, frac_pos, mean_pred, alpha=0.15, color=color)\n    ax.set_xlabel(\"Mean predicted probability\")\n    ax.set_ylabel(\"Fraction of positives\")\n    ax.set_title(label); ax.legend(fontsize=8)\n    ax.set_xlim(0, 1); ax.set_ylim(0, 1)\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"lgbm_03_calibration_curve\")\n \n# ── 5d. Probability distribution — raw vs calibrated ─────────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\nfig.suptitle(\"Predicted Probability Distribution — Raw vs Calibrated\", fontsize=13)\n \nfor ax, proba, title in [\n    (axes[0], oof_proba,       \"Uncalibrated\"),\n    (axes[1], oof_calib_proba, \"Calibrated (Isotonic)\"),\n]:\n    ax.hist(proba[y == 0], bins=60, alpha=0.7, color=PALETTE[\"human\"],\n            label=\"Human\", density=True, edgecolor=\"none\")\n    ax.hist(proba[y == 1], bins=60, alpha=0.7, color=PALETTE[\"bot\"],\n            label=\"Bot\", density=True, edgecolor=\"none\")\n    ax.axvline(train_prior, color=\"#FFD700\", lw=1.5, linestyle=\"--\",\n               label=f\"True prior ({train_prior:.3f})\")\n    ax.set_xlabel(\"P(bot)\"); ax.set_ylabel(\"Density\")\n    ax.set_title(title); ax.legend(fontsize=8)\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"lgbm_04_proba_dist\")\n \n# ── 5e. Feature importance ────────────────────────────────────────────────────\n# Re-fit a single model on full data to extract importances\nfinal_model = lgb.LGBMClassifier(**{**LGBM_PARAMS, \"n_estimators\": avg_iter})\nfinal_model.fit(X, y)\n \nimportance_df = pd.DataFrame({\n    \"feature\"  : FEAT_COLS,\n    \"gain\"     : final_model.booster_.feature_importance(importance_type=\"gain\"),\n    \"split\"    : final_model.booster_.feature_importance(importance_type=\"split\"),\n}).sort_values(\"gain\", ascending=False)\n \ntop30 = importance_df.head(30)\n \nfig, axes = plt.subplots(1, 2, figsize=(18, 10))\nfig.suptitle(\"LightGBM Feature Importance (top 30)\", fontsize=14)\n \nfor ax, col, title, color in [\n    (axes[0], \"gain\",  \"Gain (total reduction in loss)\", PALETTE[\"bot\"]),\n    (axes[1], \"split\", \"Split count\",                    PALETTE[\"human\"]),\n]:\n    sub = importance_df.nlargest(30, col)\n    ax.barh(range(len(sub)), sub[col].values[::-1], color=color, edgecolor=\"none\")\n    ax.set_yticks(range(len(sub)))\n    ax.set_yticklabels(sub[\"feature\"].values[::-1], fontsize=8)\n    ax.set_title(title); ax.set_xlabel(col.capitalize())\n \nplt.tight_layout()\nplt.show()\nsave(fig, \"lgbm_05_feature_importance\")\n \n# ── 5f. Burst features importance spotlight ───────────────────────────────────\nburst_feats = importance_df[importance_df[\"feature\"].str.startswith(\"burst\")]\ntime_feats  = importance_df[importance_df[\"feature\"].str.contains(\"time\")]\nspotlight   = pd.concat([burst_feats, time_feats]).drop_duplicates()\n \nif not spotlight.empty:\n    fig, ax = plt.subplots(figsize=(10, 6))\n    bars = ax.barh(spotlight[\"feature\"], spotlight[\"gain\"],\n                   color=PALETTE[\"calib\"], edgecolor=\"none\")\n    ax.set_title(\"Burst & Time Feature Importance (Gain)\")\n    ax.set_xlabel(\"Gain\")\n    ax.invert_yaxis()\n    plt.tight_layout()\n    plt.show()\n    save(fig, \"lgbm_06_burst_time_importance\")\n \n# ── 5g. Learning curve — AUC per fold vs n_estimators (fold 0) ───────────────\nprint(\"\\n  Collecting learning curve for fold 0 …\")\nevals_result = {}\nfold0_tr, fold0_val = folds[0]\nprobe = lgb.LGBMClassifier(**{**LGBM_PARAMS, \"n_estimators\": 1000})\n\nprobe.fit(\n    X[fold0_tr], y[fold0_tr],\n    eval_set=[(X[fold0_tr], y[fold0_tr]), (X[fold0_val], y[fold0_val])],\n    eval_names=[\"train\", \"valid\"],\n    eval_metric=\"auc\",\n    callbacks=[lgb.record_evaluation(evals_result),\n               lgb.early_stopping(50, verbose=False),\n               lgb.log_evaluation(-1)],\n)\n \ntrain_auc_curve = evals_result[\"train\"][\"auc\"]\nvalid_auc_curve = evals_result[\"valid\"][\"auc\"]\niters = range(1, len(train_auc_curve) + 1)\n \nfig, ax = plt.subplots(figsize=(12, 5))\nax.plot(iters, train_auc_curve, color=PALETTE[\"human\"], lw=1.5, label=\"Train AUC\")\nax.plot(iters, valid_auc_curve, color=PALETTE[\"bot\"],   lw=1.5, label=\"Val AUC\")\nax.axvline(probe.best_iteration_, color=\"#FFD700\", lw=1.5, linestyle=\"--\",\n           label=f\"Best iter={probe.best_iteration_}\")\nax.set_xlabel(\"Iteration\"); ax.set_ylabel(\"AUC\")\nax.set_title(\"Learning Curve — Fold 0\"); ax.legend()\nplt.tight_layout()\nplt.show()\nsave(fig, \"lgbm_07_learning_curve\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:44.075826Z","iopub.execute_input":"2026-04-16T15:37:44.080337Z","iopub.status.idle":"2026-04-16T15:37:50.164796Z","shell.execute_reply.started":"2026-04-16T15:37:44.080302Z","shell.execute_reply":"2026-04-16T15:37:50.163828Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 6. FINAL SUBMISSION\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"6. FINAL SUBMISSION\")\nprint(\"=\"*60)\n \n# Use calibrated probabilities — they reflect the true ~5% bot prior\nsubmission = pd.DataFrame({\n    \"bidder_id\" : test[\"bidder_id\"],\n    \"prediction\": test_proba_cal,\n})\nsubmission.to_csv(\"submission_lgbm.csv\", index=False)\n \nprint(f\"submission_lgbm.csv  shape={submission.shape}\")\nprint(submission.head(10).to_string())\nprint(f\"\\nPredicted bot rate (calibrated): {test_proba_cal.mean():.4f}  \"\n      f\"(expected ~{train_prior:.4f})\")\n \nprint(\"\\n✓ LightGBM pipeline complete.\")\nprint(f\"  OOF AUC          : {oof_auc:.4f}\")\nprint(f\"  Avg best iter    : {avg_iter}\")\nprint(f\"  Plots            : ./{SAVE_DIR}/lgbm_*.png\")\nprint(f\"  Submission       : ./submission_lgbm.csv\")\nprint(f\"\\n  Feature importance top-5 (gain):\")\nprint(importance_df[[\"feature\",\"gain\"]].head(5).to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:50.166020Z","iopub.execute_input":"2026-04-16T15:37:50.166304Z","iopub.status.idle":"2026-04-16T15:37:50.191659Z","shell.execute_reply.started":"2026-04-16T15:37:50.166280Z","shell.execute_reply":"2026-04-16T15:37:50.190916Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# LIGHTGBM SUMMARY LOG\n# =============================================================================\nlgbm_summary_df = pd.DataFrame([{\n    \"model\": \"LightGBM\",\n    \"oof_auc\": float(oof_auc),\n    \"oof_ap\": float(average_precision_score(y, oof_proba)),\n    \"oof_pred_bot_rate\": float(oof_proba.mean()),\n    \"test_pred_bot_rate\": float(test_proba_cal.mean()),\n}])\n\nlgbm_summary_df.to_csv(\"logs/lgbm_summary.csv\", index=False)\nprint(\"Saved logs/lgbm_summary.csv\")\nprint(lgbm_summary_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:50.192619Z","iopub.execute_input":"2026-04-16T15:37:50.192944Z","iopub.status.idle":"2026-04-16T15:37:50.202499Z","shell.execute_reply.started":"2026-04-16T15:37:50.192911Z","shell.execute_reply":"2026-04-16T15:37:50.201728Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TabM","metadata":{}},{"cell_type":"code","source":"# =============================================================================\n# TABM - IMPORTS + SETUP\n# =============================================================================\nimport os, time, copy, random, json, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\ntry:\n    import tabm\nexcept ImportError:\n    !pip install -q tabm rtdl_num_embeddings\n    import tabm\n\ntry:\n    import rtdl_num_embeddings\nexcept ImportError:\n    !pip install -q rtdl_num_embeddings\n    import rtdl_num_embeddings\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import QuantileTransformer\nfrom sklearn.metrics import (\n    roc_auc_score, roc_curve, confusion_matrix, classification_report,\n    precision_recall_curve, average_precision_score\n)\n\nwarnings.filterwarnings(\"ignore\")\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"DEVICE:\", DEVICE)\n\nos.makedirs(\"plots\", exist_ok=True)\nos.makedirs(\"logs\", exist_ok=True)\n\nif \"SAVE_DIR\" not in globals():\n    SAVE_DIR = \"plots\"\nif \"LOG_DIR\" not in globals():\n    LOG_DIR = \"logs\"\n\nif \"SEED\" not in globals():\n    SEED = 42\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n\nset_seed(SEED)\n\n# use existing palette if already defined\nif \"PALETTE\" not in globals():\n    PALETTE = {\n        \"bot\": \"#E84545\",\n        \"human\": \"#2B9EB3\",\n        \"neutral\": \"#6C757D\",\n        \"tabm\": \"#8E44AD\",\n        \"tune\": \"#F39C12\",\n    }\n\n# use existing save() if already defined\nif \"save\" not in globals():\n    def save(fig, name):\n        fig.savefig(f\"{SAVE_DIR}/{name}.png\", bbox_inches=\"tight\", facecolor=fig.get_facecolor())\n        plt.close(fig)\n        print(f\"  ↳ Saved {SAVE_DIR}/{name}.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:50.203430Z","iopub.execute_input":"2026-04-16T15:37:50.203713Z","iopub.status.idle":"2026-04-16T15:37:52.156543Z","shell.execute_reply.started":"2026-04-16T15:37:50.203685Z","shell.execute_reply":"2026-04-16T15:37:52.155703Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## load data + helper functions","metadata":{}},{"cell_type":"code","source":"# =============================================================================\n# TABM - IMPORTS + SETUP\n# =============================================================================\nimport os, time, copy, random, json, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\ntry:\n    import tabm\nexcept ImportError:\n    !pip install -q tabm rtdl_num_embeddings\n    import tabm\n\ntry:\n    import rtdl_num_embeddings\nexcept ImportError:\n    !pip install -q rtdl_num_embeddings\n    import rtdl_num_embeddings\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import QuantileTransformer, RobustScaler\nfrom sklearn.metrics import (\n    roc_auc_score, roc_curve, confusion_matrix, classification_report,\n    precision_recall_curve, average_precision_score\n)\n\nwarnings.filterwarnings(\"ignore\")\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"DEVICE:\", DEVICE)\n\nos.makedirs(\"plots\", exist_ok=True)\nos.makedirs(\"logs\", exist_ok=True)\n\nif \"SAVE_DIR\" not in globals():\n    SAVE_DIR = \"plots\"\nif \"LOG_DIR\" not in globals():\n    LOG_DIR = \"logs\"\nif \"SEED\" not in globals():\n    SEED = 42\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n\nset_seed(SEED)\n\nif \"PALETTE\" not in globals():\n    PALETTE = {\n        \"bot\": \"#E84545\",\n        \"human\": \"#2B9EB3\",\n        \"neutral\": \"#6C757D\",\n        \"tabm\": \"#8E44AD\",\n        \"tune\": \"#F39C12\",\n    }\n\nif \"save\" not in globals():\n    def save(fig, name):\n        fig.savefig(f\"{SAVE_DIR}/{name}.png\", bbox_inches=\"tight\", facecolor=fig.get_facecolor())\n        plt.close(fig)\n        print(f\"  ↳ Saved {SAVE_DIR}/{name}.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:52.157742Z","iopub.execute_input":"2026-04-16T15:37:52.158605Z","iopub.status.idle":"2026-04-16T15:37:52.174023Z","shell.execute_reply.started":"2026-04-16T15:37:52.158578Z","shell.execute_reply":"2026-04-16T15:37:52.173224Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 1. TABM SEARCH SPACE\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"1. TABM SEARCH SPACE\")\nprint(\"=\"*60)\n\nif \"folds\" not in globals():\n    N_SPLITS = 5\n    folds = list(StratifiedKFold(n_splits=N_SPLITS, shuffle=True, random_state=SEED).split(X, y))\n    print(\"Shared folds were not found, so they were recreated.\")\nelse:\n    print(f\"Reusing shared folds: {len(folds)} folds\")\n\nMAX_EPOCHS = 80\nPATIENCE = 8\nMIN_DELTA = 5e-4\nGRAD_CLIP = 0.5\nBATCH_SIZE = 256 if DEVICE.type == \"cuda\" else 128\n\n# Tuning uses one seed for speed; final ensemble uses multiple seeds\nTUNING_SEED = 42\nFINAL_SEEDS = [42, 52, 62] if DEVICE.type == \"cuda\" else [42, 52]\nTOP_K_CONFIGS = 2\nAUC_TIE_GAP = 0.002\n\n# Balanced search space: not too aggressive, not too tiny\nif DEVICE.type == \"cuda\":\n    TABM_CONFIGS = [\n        {\n            \"arch_type\": \"tabm\",\n            \"k\": 12,\n            \"n_blocks\": 3,\n            \"d_block\": 192,\n            \"dropout\": 0.15,\n            \"lr\": 0.001,\n            \"weight_decay\": 0.001,\n            \"use_num_embeddings\": True,\n            \"pos_weight_scale\": 0.75,\n            \"preprocessor\": \"quantile\",\n        },\n        {\n            \"arch_type\": \"tabm\",\n            \"k\": 16,\n            \"n_blocks\": 3,\n            \"d_block\": 256,\n            \"dropout\": 0.20,\n            \"lr\": 0.001,\n            \"weight_decay\": 0.001,\n            \"use_num_embeddings\": True,\n            \"pos_weight_scale\": 0.50,\n            \"preprocessor\": \"quantile\",\n        },\n        {\n            \"arch_type\": \"tabm\",\n            \"k\": 8,\n            \"n_blocks\": 2,\n            \"d_block\": 128,\n            \"dropout\": 0.20,\n            \"lr\": 0.0007,\n            \"weight_decay\": 0.001,\n            \"use_num_embeddings\": False,\n            \"pos_weight_scale\": 0.75,\n            \"preprocessor\": \"robust\",\n        },\n        {\n            \"arch_type\": \"tabm\",\n            \"k\": 8,\n            \"n_blocks\": 2,\n            \"d_block\": 128,\n            \"dropout\": 0.25,\n            \"lr\": 0.0007,\n            \"weight_decay\": 0.003,\n            \"use_num_embeddings\": False,\n            \"pos_weight_scale\": 0.50,\n            \"preprocessor\": \"quantile\",\n        },\n        {\n            \"arch_type\": \"tabm\",\n            \"k\": 12,\n            \"n_blocks\": 2,\n            \"d_block\": 192,\n            \"dropout\": 0.15,\n            \"lr\": 0.001,\n            \"weight_decay\": 0.001,\n            \"use_num_embeddings\": True,\n            \"pos_weight_scale\": 0.75,\n            \"preprocessor\": \"robust\",\n        },\n    ]\nelse:\n    TABM_CONFIGS = [\n        {\n            \"arch_type\": \"tabm-mini\",\n            \"k\": 8,\n            \"n_blocks\": 2,\n            \"d_block\": 128,\n            \"dropout\": 0.20,\n            \"lr\": 0.001,\n            \"weight_decay\": 0.001,\n            \"use_num_embeddings\": True,\n            \"pos_weight_scale\": 0.75,\n            \"preprocessor\": \"quantile\",\n        },\n        {\n            \"arch_type\": \"tabm-mini\",\n            \"k\": 8,\n            \"n_blocks\": 2,\n            \"d_block\": 128,\n            \"dropout\": 0.25,\n            \"lr\": 0.0007,\n            \"weight_decay\": 0.003,\n            \"use_num_embeddings\": False,\n            \"pos_weight_scale\": 0.50,\n            \"preprocessor\": \"robust\",\n        },\n        {\n            \"arch_type\": \"tabm-mini\",\n            \"k\": 12,\n            \"n_blocks\": 2,\n            \"d_block\": 128,\n            \"dropout\": 0.20,\n            \"lr\": 0.001,\n            \"weight_decay\": 0.001,\n            \"use_num_embeddings\": True,\n            \"pos_weight_scale\": 0.75,\n            \"preprocessor\": \"robust\",\n        },\n    ]\n\nprint(\"TabM configs:\")\nfor i, cfg in enumerate(TABM_CONFIGS, 1):\n    print(f\"  Config {i}: {cfg}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:52.175107Z","iopub.execute_input":"2026-04-16T15:37:52.175432Z","iopub.status.idle":"2026-04-16T15:37:52.191279Z","shell.execute_reply.started":"2026-04-16T15:37:52.175399Z","shell.execute_reply":"2026-04-16T15:37:52.190454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 2. TABM HELPERS\n# =============================================================================\ndef make_preprocessor(X_train, seed, method=\"quantile\"):\n    if method == \"quantile\":\n        noise = np.random.default_rng(seed).normal(0.0, 1e-5, X_train.shape).astype(np.float32)\n        qt = QuantileTransformer(\n            n_quantiles=max(min(len(X_train) // 30, 1000), 10),\n            output_distribution=\"normal\",\n            subsample=10**9,\n            random_state=seed,\n        )\n        qt.fit(X_train + noise)\n        return qt\n\n    elif method == \"robust\":\n        rs = RobustScaler()\n        rs.fit(X_train)\n        return rs\n\n    else:\n        raise ValueError(f\"Unknown preprocessor: {method}\")\n\n\ndef build_tabm_model(n_num_features, cfg):\n    num_embeddings = None\n    if cfg.get(\"use_num_embeddings\", False):\n        num_embeddings = rtdl_num_embeddings.LinearReLUEmbeddings(n_num_features)\n\n    model = tabm.TabM.make(\n        n_num_features=n_num_features,\n        d_out=1,\n        num_embeddings=num_embeddings,\n        arch_type=cfg[\"arch_type\"],\n        k=int(cfg[\"k\"]),\n        n_blocks=int(cfg[\"n_blocks\"]),\n        d_block=int(cfg[\"d_block\"]),\n        dropout=float(cfg[\"dropout\"]),\n    ).to(DEVICE)\n    return model\n\n\ndef predict_tabm_proba(model, x_num_tensor, batch_size=1024):\n    model.eval()\n    probs = []\n\n    with torch.inference_mode():\n        for start in range(0, len(x_num_tensor), batch_size):\n            xb = x_num_tensor[start:start + batch_size]\n            logits = model(xb).squeeze(-1)          # (B, k)\n            proba = torch.sigmoid(logits).mean(dim=1)\n            probs.append(proba.detach().cpu().numpy())\n\n    return np.concatenate(probs)\n\n\ndef eval_tabm_loss(model, x_num_tensor, y_tensor, pos_weight, batch_size=1024):\n    model.eval()\n    losses = []\n\n    with torch.inference_mode():\n        for start in range(0, len(x_num_tensor), batch_size):\n            xb = x_num_tensor[start:start + batch_size]\n            yb = y_tensor[start:start + batch_size]\n\n            logits = model(xb).squeeze(-1)\n            target = yb[:, None].expand(-1, logits.shape[1]).float()\n\n            loss = F.binary_cross_entropy_with_logits(\n                logits,\n                target,\n                pos_weight=pos_weight\n            )\n            losses.append(loss.item())\n\n    return float(np.mean(losses))\n\n\ndef train_tabm_one_fold(X_tr_raw, y_tr, X_val_raw, y_val, X_te_raw, cfg, seed,\n                        collect_history=False, member_tag=None):\n    set_seed(seed)\n\n    pre = make_preprocessor(X_tr_raw, seed=seed, method=cfg[\"preprocessor\"])\n    X_tr = pre.transform(X_tr_raw).astype(np.float32)\n    X_val = pre.transform(X_val_raw).astype(np.float32)\n\n    X_te = None\n    if X_te_raw is not None:\n        X_te = pre.transform(X_te_raw).astype(np.float32)\n\n    X_tr_t = torch.tensor(X_tr, device=DEVICE)\n    y_tr_t = torch.tensor(y_tr.astype(np.float32), device=DEVICE)\n    X_val_t = torch.tensor(X_val, device=DEVICE)\n    y_val_t = torch.tensor(y_val.astype(np.float32), device=DEVICE)\n    X_te_t = torch.tensor(X_te, device=DEVICE) if X_te is not None else None\n\n    model = build_tabm_model(X_tr.shape[1], cfg)\n\n    optimizer = torch.optim.AdamW(\n        model.parameters(),\n        lr=cfg[\"lr\"],\n        weight_decay=cfg[\"weight_decay\"]\n    )\n\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer,\n        mode=\"max\",\n        factor=0.5,\n        patience=3,\n        min_lr=1e-5\n    )\n\n    n_pos = y_tr.sum()\n    n_neg = len(y_tr) - n_pos\n    base_pos_weight = n_neg / max(n_pos, 1)\n    pos_weight = torch.tensor(\n        base_pos_weight * cfg[\"pos_weight_scale\"],\n        dtype=torch.float32,\n        device=DEVICE\n    )\n\n    best_auc = -np.inf\n    best_epoch = 0\n    best_state = None\n    wait = 0\n    history_rows = []\n\n    for epoch in range(1, MAX_EPOCHS + 1):\n        model.train()\n        order = np.random.permutation(len(X_tr))\n        batch_losses = []\n\n        for start in range(0, len(order), BATCH_SIZE):\n            batch_idx = order[start:start + BATCH_SIZE]\n            xb = X_tr_t[batch_idx]\n            yb = y_tr_t[batch_idx]\n\n            optimizer.zero_grad(set_to_none=True)\n\n            logits = model(xb).squeeze(-1)\n            target = yb[:, None].expand(-1, logits.shape[1]).float()\n\n            loss = F.binary_cross_entropy_with_logits(\n                logits,\n                target,\n                pos_weight=pos_weight\n            )\n            loss.backward()\n            nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP)\n            optimizer.step()\n\n            batch_losses.append(loss.item())\n\n        train_loss = float(np.mean(batch_losses))\n        val_loss = eval_tabm_loss(model, X_val_t, y_val_t, pos_weight, batch_size=1024)\n        val_proba = predict_tabm_proba(model, X_val_t, batch_size=1024)\n        val_auc = roc_auc_score(y_val, val_proba)\n\n        if collect_history:\n            history_rows.append({\n                \"member_tag\": member_tag,\n                \"epoch\": epoch,\n                \"train_loss\": train_loss,\n                \"val_loss\": val_loss,\n                \"val_auc\": val_auc,\n            })\n\n        scheduler.step(val_auc)\n\n        if val_auc > best_auc + MIN_DELTA:\n            best_auc = val_auc\n            best_epoch = epoch\n            best_state = copy.deepcopy(model.state_dict())\n            wait = 0\n        else:\n            wait += 1\n            if wait >= PATIENCE:\n                break\n\n    model.load_state_dict(best_state)\n\n    val_proba = predict_tabm_proba(model, X_val_t, batch_size=1024)\n    test_proba = predict_tabm_proba(model, X_te_t, batch_size=1024) if X_te_t is not None else None\n\n    return {\n        \"model\": model,\n        \"val_proba\": val_proba,\n        \"test_proba\": test_proba,\n        \"best_auc\": float(best_auc),\n        \"best_epoch\": int(best_epoch),\n        \"history_rows\": history_rows,\n    }\n\nprint(\"TabM helper functions loaded.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:52.192500Z","iopub.execute_input":"2026-04-16T15:37:52.192736Z","iopub.status.idle":"2026-04-16T15:37:52.214018Z","shell.execute_reply.started":"2026-04-16T15:37:52.192716Z","shell.execute_reply":"2026-04-16T15:37:52.213185Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 3. TABM HYPERPARAMETER SEARCH\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"3. TABM HYPERPARAMETER SEARCH\")\nprint(\"=\"*60)\n\ntuning_rows = []\n\nfor config_id, cfg in enumerate(TABM_CONFIGS, start=1):\n    print(f\"\\n--- Config {config_id}/{len(TABM_CONFIGS)} ---\")\n    print(cfg)\n\n    fold_aucs = []\n    fold_aps = []\n    fold_epochs = []\n\n    for fold, (tr_idx, val_idx) in enumerate(folds, start=1):\n        res = train_tabm_one_fold(\n            X_tr_raw=X[tr_idx],\n            y_tr=y[tr_idx],\n            X_val_raw=X[val_idx],\n            y_val=y[val_idx],\n            X_te_raw=None,\n            cfg=cfg,\n            seed=TUNING_SEED + config_id * 100 + fold,\n            collect_history=False,\n        )\n\n        fold_auc = roc_auc_score(y[val_idx], res[\"val_proba\"])\n        fold_ap = average_precision_score(y[val_idx], res[\"val_proba\"])\n\n        fold_aucs.append(fold_auc)\n        fold_aps.append(fold_ap)\n        fold_epochs.append(res[\"best_epoch\"])\n\n        print(f\"  Fold {fold}  AUC={fold_auc:.4f}  AP={fold_ap:.4f}  best_epoch={res['best_epoch']}\")\n\n    tuning_rows.append({\n        \"config_id\": config_id,\n        \"arch_type\": cfg[\"arch_type\"],\n        \"k\": cfg[\"k\"],\n        \"n_blocks\": cfg[\"n_blocks\"],\n        \"d_block\": cfg[\"d_block\"],\n        \"dropout\": cfg[\"dropout\"],\n        \"lr\": cfg[\"lr\"],\n        \"weight_decay\": cfg[\"weight_decay\"],\n        \"use_num_embeddings\": cfg[\"use_num_embeddings\"],\n        \"pos_weight_scale\": cfg[\"pos_weight_scale\"],\n        \"preprocessor\": cfg[\"preprocessor\"],\n        \"mean_auc\": float(np.mean(fold_aucs)),\n        \"std_auc\": float(np.std(fold_aucs)),\n        \"mean_ap\": float(np.mean(fold_aps)),\n        \"mean_best_epoch\": float(np.mean(fold_epochs)),\n    })\n\ntabm_tuning_df = pd.DataFrame(tuning_rows).sort_values(\n    [\"mean_auc\", \"mean_ap\"], ascending=False\n).reset_index(drop=True)\n\nprint(\"\\nTabM tuning leaderboard:\")\nprint(tabm_tuning_df.to_string(index=False))\n\ntabm_tuning_df.to_csv(\"logs/tabm_tuning_results.csv\", index=False)\nprint(\"Saved logs/tabm_tuning_results.csv\")\n\nmax_auc = tabm_tuning_df[\"mean_auc\"].max()\ncandidates = tabm_tuning_df[tabm_tuning_df[\"mean_auc\"] >= max_auc - AUC_TIE_GAP].copy()\n\nselected_cfg_df = candidates.sort_values(\n    [\"std_auc\", \"mean_best_epoch\", \"mean_ap\"],\n    ascending=[True, True, False]\n).head(TOP_K_CONFIGS).reset_index(drop=True)\n\nprint(\"\\nSelected configs for final ensemble:\")\nprint(selected_cfg_df.to_string(index=False))\n\nselected_cfg_df.to_csv(\"logs/tabm_selected_configs.csv\", index=False)\nprint(\"Saved logs/tabm_selected_configs.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:37:52.215039Z","iopub.execute_input":"2026-04-16T15:37:52.215262Z","iopub.status.idle":"2026-04-16T15:38:21.968680Z","shell.execute_reply.started":"2026-04-16T15:37:52.215205Z","shell.execute_reply":"2026-04-16T15:38:21.967727Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 4. FINAL TABM MODEL\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"4. FINAL TABM MODEL\")\nprint(\"=\"*60)\n\nbest_row = selected_cfg_df.iloc[0]\n\ncfg = {\n    \"config_id\": int(best_row[\"config_id\"]),\n    \"arch_type\": best_row[\"arch_type\"],\n    \"k\": int(best_row[\"k\"]),\n    \"n_blocks\": int(best_row[\"n_blocks\"]),\n    \"d_block\": int(best_row[\"d_block\"]),\n    \"dropout\": float(best_row[\"dropout\"]),\n    \"lr\": float(best_row[\"lr\"]),\n    \"weight_decay\": float(best_row[\"weight_decay\"]),\n    \"use_num_embeddings\": bool(best_row[\"use_num_embeddings\"]),\n    \"pos_weight_scale\": float(best_row[\"pos_weight_scale\"]),\n    \"preprocessor\": best_row[\"preprocessor\"],\n}\n\nFINAL_SEED = 42\n\ntabm_oof_proba = np.zeros(len(y), dtype=np.float32)\ntabm_test_probas = np.zeros((len(X_test), len(folds)), dtype=np.float32)\ntabm_history_rows = []\ntabm_fold_rows = []\n\nfor fold, (tr_idx, val_idx) in enumerate(folds, start=1):\n    res = train_tabm_one_fold(\n        X_tr_raw=X[tr_idx],\n        y_tr=y[tr_idx],\n        X_val_raw=X[val_idx],\n        y_val=y[val_idx],\n        X_te_raw=X_test,\n        cfg=cfg,\n        seed=FINAL_SEED * 100 + fold,\n        collect_history=True,\n        member_tag=\"final_tabm\",\n    )\n\n    tabm_oof_proba[val_idx] = res[\"val_proba\"]\n    tabm_test_probas[:, fold - 1] = res[\"test_proba\"]\n\n    fold_auc = roc_auc_score(y[val_idx], res[\"val_proba\"])\n    fold_ap = average_precision_score(y[val_idx], res[\"val_proba\"])\n\n    tabm_fold_rows.append({\n        \"fold\": fold,\n        \"best_epoch\": res[\"best_epoch\"],\n        \"final_fold_auc\": float(fold_auc),\n        \"final_fold_ap\": float(fold_ap),\n    })\n\n    for row in res[\"history_rows\"]:\n        row[\"fold\"] = fold\n    tabm_history_rows.extend(res[\"history_rows\"])\n\n    torch.save(\n        res[\"model\"].state_dict(),\n        f\"logs/tabm_final_fold{fold}_best.pt\"\n    )\n\n    print(f\"Fold {fold}  AUC={fold_auc:.4f}  AP={fold_ap:.4f}  best_epoch={res['best_epoch']}\")\n\ntabm_test_proba = tabm_test_probas.mean(axis=1)\ntabm_oof_auc = roc_auc_score(y, tabm_oof_proba)\ntabm_oof_ap = average_precision_score(y, tabm_oof_proba)\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"TABM SUMMARY\")\nprint(\"=\"*60)\nprint(f\"OOF AUC : {tabm_oof_auc:.4f}\")\nprint(f\"OOF AP  : {tabm_oof_ap:.4f}\")\nprint(f\"OOF bot-rate mean  : {tabm_oof_proba.mean():.4f}\")\nprint(f\"Test bot-rate mean : {tabm_test_proba.mean():.4f}\")\n\npd.DataFrame(tabm_history_rows).to_csv(\"logs/tabm_history_all_folds.csv\", index=False)\npd.DataFrame(tabm_fold_rows).to_csv(\"logs/tabm_fold_metrics.csv\", index=False)\n\npd.DataFrame({\n    \"row_id\": np.arange(len(y)),\n    \"outcome\": y,\n    \"tabm_oof_proba\": tabm_oof_proba,\n}).to_csv(\"logs/tabm_oof_predictions.csv\", index=False)\n\ntabm_summary_df = pd.DataFrame([{\n    \"model\": \"TabM\",\n    \"oof_auc\": float(tabm_oof_auc),\n    \"oof_ap\": float(tabm_oof_ap),\n    \"oof_pred_bot_rate\": float(tabm_oof_proba.mean()),\n    \"test_pred_bot_rate\": float(tabm_test_proba.mean()),\n}])\ntabm_summary_df.to_csv(\"logs/tabm_summary.csv\", index=False)\n\nwith open(\"logs/tabm_best_config.json\", \"w\") as f:\n    json.dump(cfg, f, indent=2)\n\nprint(\"Saved logs/tabm_history_all_folds.csv\")\nprint(\"Saved logs/tabm_fold_metrics.csv\")\nprint(\"Saved logs/tabm_oof_predictions.csv\")\nprint(\"Saved logs/tabm_summary.csv\")\nprint(\"Saved logs/tabm_best_config.json\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:38:21.969913Z","iopub.execute_input":"2026-04-16T15:38:21.970246Z","iopub.status.idle":"2026-04-16T15:38:31.468299Z","shell.execute_reply.started":"2026-04-16T15:38:21.970211Z","shell.execute_reply":"2026-04-16T15:38:31.467426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 5. TABM EVALUATION PLOTS\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"5. TABM EVALUATION PLOTS\")\nprint(\"=\"*60)\n\n# -------------------------------------------------------------------------\n# Make sure PALETTE has the colors we need\n# -------------------------------------------------------------------------\nif \"PALETTE\" not in globals():\n    PALETTE = {}\n\ntabm_color = PALETTE.get(\"tabm\", \"#8E44AD\")\nhuman_color = PALETTE.get(\"human\", \"#2B9EB3\")\nneutral_color = PALETTE.get(\"neutral\", \"#6C757D\")\n\n# -------------------------------------------------------------------------\n# 5a. ROC + PR curves\n# -------------------------------------------------------------------------\nfpr, tpr, _ = roc_curve(y, tabm_oof_proba)\nprec, rec, _ = precision_recall_curve(y, tabm_oof_proba)\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 6))\nfig.suptitle(f\"TabM — OOF AUC={tabm_oof_auc:.4f}\", fontsize=14)\n\n# ROC\nax = axes[0]\nax.plot(fpr, tpr, color=tabm_color, lw=2, label=f\"ROC (AUC={tabm_oof_auc:.4f})\")\nax.plot([0, 1], [0, 1], \"--\", color=\"#555\", lw=1)\nax.fill_between(fpr, tpr, alpha=0.12, color=tabm_color)\nax.set_xlabel(\"False Positive Rate\")\nax.set_ylabel(\"True Positive Rate\")\nax.set_title(\"ROC Curve\")\nax.legend()\n\n# PR\nax = axes[1]\nax.plot(rec, prec, color=human_color, lw=2, label=f\"PR (AP={tabm_oof_ap:.4f})\")\nax.fill_between(rec, prec, alpha=0.12, color=human_color)\nax.axhline(y.mean(), color=\"#555\", lw=1, linestyle=\"--\", label=\"Chance\")\nax.set_xlabel(\"Recall\")\nax.set_ylabel(\"Precision\")\nax.set_title(\"Precision–Recall Curve\")\nax.legend()\n\nplt.tight_layout()\nplt.show()\nsave(fig, \"tabm_01_roc_pr\")\n\n# -------------------------------------------------------------------------\n# 5b. Confusion matrix\n# -------------------------------------------------------------------------\ntabm_pred = (tabm_oof_proba >= 0.5).astype(int)\ncm = confusion_matrix(y, tabm_pred)\n\nprint(\"\\nClassification Report (TabM, threshold=0.5):\")\nprint(classification_report(y, tabm_pred, target_names=[\"Human\", \"Bot\"]))\n\nfig, ax = plt.subplots(figsize=(6, 5))\nsns.heatmap(\n    cm,\n    annot=True,\n    fmt=\"d\",\n    cmap=\"Blues\",\n    ax=ax,\n    xticklabels=[\"Human\", \"Bot\"],\n    yticklabels=[\"Human\", \"Bot\"],\n    linewidths=0.5,\n    cbar=False\n)\nax.set_xlabel(\"Predicted\")\nax.set_ylabel(\"Actual\")\nax.set_title(\"TabM Confusion Matrix (threshold=0.5)\")\nplt.tight_layout()\nplt.show()\nsave(fig, \"tabm_02_confusion_matrix\")\n\n# -------------------------------------------------------------------------\n# 5c. Training history (mean over folds, common epochs only)\n# -------------------------------------------------------------------------\nhistory_path = \"logs/tabm_history_all_folds.csv\"\n\nif os.path.exists(history_path):\n    hist = pd.read_csv(history_path)\n\n    if not hist.empty and {\"fold\", \"epoch\", \"train_loss\", \"val_loss\", \"val_auc\"}.issubset(hist.columns):\n        # only keep epochs reached by all folds\n        max_common_epoch = hist.groupby(\"fold\")[\"epoch\"].max().min()\n        hist_common = hist[hist[\"epoch\"] <= max_common_epoch].copy()\n\n        mean_hist = (\n            hist_common.groupby(\"epoch\")[[\"train_loss\", \"val_loss\", \"val_auc\"]]\n            .mean()\n            .reset_index()\n        )\n\n        fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n        fig.suptitle(\"TabM Training History (mean over folds, common epochs only)\", fontsize=13)\n\n        # Loss curve\n        ax = axes[0]\n        ax.plot(mean_hist[\"epoch\"], mean_hist[\"train_loss\"], lw=2, label=\"Train Loss\")\n        ax.plot(mean_hist[\"epoch\"], mean_hist[\"val_loss\"], lw=2, label=\"Val Loss\")\n        ax.set_xlabel(\"Epoch\")\n        ax.set_ylabel(\"Loss\")\n        ax.set_title(\"Loss Curve\")\n        ax.legend()\n\n        # Validation AUC\n        ax = axes[1]\n        ax.plot(mean_hist[\"epoch\"], mean_hist[\"val_auc\"], color=tabm_color, lw=2)\n        ax.set_xlabel(\"Epoch\")\n        ax.set_ylabel(\"AUC\")\n        ax.set_title(\"Validation AUC Curve\")\n\n        plt.tight_layout()\n        plt.show()\n        save(fig, \"tabm_03_training_history\")\n    else:\n        print(f\"\\nNo valid training history found in {history_path}.\")\nelse:\n    print(f\"\\nFile not found: {history_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:38:31.469237Z","iopub.execute_input":"2026-04-16T15:38:31.469632Z","iopub.status.idle":"2026-04-16T15:38:33.183940Z","shell.execute_reply.started":"2026-04-16T15:38:31.469591Z","shell.execute_reply":"2026-04-16T15:38:33.183214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 6. FINAL PREDICTION + SUBMISSION\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"6. FINAL PREDICTION\")\nprint(\"=\"*60)\n\nsubmission_tabm = pd.DataFrame({\n    \"bidder_id\": test[\"bidder_id\"],\n    \"prediction\": tabm_test_proba,\n})\nsubmission_tabm.to_csv(\"submission_tabm.csv\", index=False)\n\nprint(f\"submission_tabm.csv  shape={submission_tabm.shape}\")\nprint(submission_tabm.head(10).to_string())\nprint(f\"\\nPredicted bot rate (test): {tabm_test_proba.mean():.4f}\")\n\nprint(\"\\n✓ TabM ensemble pipeline complete.\")\nprint(f\"  OOF AUC   : {tabm_oof_auc:.4f}\")\nprint(f\"  OOF AP    : {tabm_oof_ap:.4f}\")\nprint(f\"  Plots     : ./plots/tabm_*.png\")\nprint(f\"  Submission: ./submission_tabm.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:38:33.184950Z","iopub.execute_input":"2026-04-16T15:38:33.185264Z","iopub.status.idle":"2026-04-16T15:38:33.205850Z","shell.execute_reply.started":"2026-04-16T15:38:33.185239Z","shell.execute_reply":"2026-04-16T15:38:33.205006Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## XAI","metadata":{}},{"cell_type":"code","source":"# =============================================================================\n# 7. TABM XAI — INTEGRATED GRADIENTS (NO CAPTUM)\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"7. TABM XAI — INTEGRATED GRADIENTS (NO CAPTUM)\")\nprint(\"=\"*60)\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport torch\n\nos.makedirs(\"plots\", exist_ok=True)\nos.makedirs(\"logs\", exist_ok=True)\n\n# -------------------------------------------------------------------------\n# Safe colors\n# -------------------------------------------------------------------------\nif \"PALETTE\" not in globals():\n    PALETTE = {}\n\ntabm_color = PALETTE.get(\"tabm\", \"#8E44AD\")\nhuman_color = PALETTE.get(\"human\", \"#2B9EB3\")\n\n# -------------------------------------------------------------------------\n# Checks\n# -------------------------------------------------------------------------\nrequired_names = [\"folds\", \"X\", \"y\", \"FEAT_COLS\", \"cfg\", \"DEVICE\",\n                  \"make_preprocessor\", \"build_tabm_model\"]\nmissing = [name for name in required_names if name not in globals()]\nif missing:\n    raise ValueError(f\"Missing required variables/functions for XAI: {missing}\")\n\nif \"FINAL_SEED\" not in globals():\n    FINAL_SEED = 42\n\n# -------------------------------------------------------------------------\n# Helper: rebuild one fold model + transformed data\n# -------------------------------------------------------------------------\ndef load_tabm_fold_for_xai(fold_no, cfg, final_seed=42):\n    tr_idx, val_idx = folds[fold_no - 1]\n\n    X_tr_raw, X_val_raw = X[tr_idx], X[val_idx]\n    y_tr, y_val = y[tr_idx], y[val_idx]\n\n    pre = make_preprocessor(\n        X_tr_raw,\n        seed=final_seed * 100 + fold_no,\n        method=cfg[\"preprocessor\"]\n    )\n\n    X_tr = pre.transform(X_tr_raw).astype(np.float32)\n    X_val = pre.transform(X_val_raw).astype(np.float32)\n\n    model = build_tabm_model(X.shape[1], cfg)\n    ckpt_path = f\"logs/tabm_final_fold{fold_no}_best.pt\"\n\n    if not os.path.exists(ckpt_path):\n        raise FileNotFoundError(f\"Checkpoint not found: {ckpt_path}\")\n\n    state = torch.load(ckpt_path, map_location=DEVICE)\n    model.load_state_dict(state)\n    model.eval()\n\n    return {\n        \"model\": model,\n        \"preprocessor\": pre,\n        \"tr_idx\": tr_idx,\n        \"val_idx\": val_idx,\n        \"X_tr\": X_tr,\n        \"X_val\": X_val,\n        \"y_tr\": y_tr,\n        \"y_val\": y_val,\n    }\n\n# -------------------------------------------------------------------------\n# Wrapper: TabM -> one probability per row\n# -------------------------------------------------------------------------\nclass TabMProbWrapper(torch.nn.Module):\n    def __init__(self, base_model):\n        super().__init__()\n        self.base_model = base_model\n\n    def forward(self, x_num):\n        logits = self.base_model(x_num).squeeze(-1)   # (B, k)\n        proba = torch.sigmoid(logits).mean(dim=1)     # (B,)\n        return proba\n\n# -------------------------------------------------------------------------\n# Pure PyTorch Integrated Gradients\n# -------------------------------------------------------------------------\ndef integrated_gradients(model, x, baseline, n_steps=64):\n    \"\"\"\n    model: returns shape (B,)\n    x: shape (B, D)\n    baseline: shape (B, D) or (1, D)\n    \"\"\"\n    model.eval()\n\n    if baseline.shape[0] == 1 and x.shape[0] > 1:\n        baseline = baseline.repeat(x.shape[0], 1)\n\n    alphas = torch.linspace(0.0, 1.0, steps=n_steps + 1, device=x.device)[1:]\n    total_grads = torch.zeros_like(x)\n\n    for alpha in alphas:\n        x_step = baseline + alpha * (x - baseline)\n        x_step.requires_grad_(True)\n\n        preds = model(x_step)                # (B,)\n        grads = torch.autograd.grad(\n            outputs=preds.sum(),\n            inputs=x_step,\n            retain_graph=False,\n            create_graph=False\n        )[0]\n\n        total_grads += grads.detach()\n\n    avg_grads = total_grads / n_steps\n    attributions = (x - baseline) * avg_grads\n\n    with torch.no_grad():\n        pred_x = model(x)\n        pred_base = model(baseline)\n        completeness_delta = pred_x - pred_base - attributions.sum(dim=1)\n\n    return attributions, completeness_delta\n\n# -------------------------------------------------------------------------\n# 7a. LOCAL EXPLANATION\n# explain one case from the best fold\n# -------------------------------------------------------------------------\nif os.path.exists(\"logs/tabm_fold_metrics.csv\"):\n    fold_metrics = pd.read_csv(\"logs/tabm_fold_metrics.csv\")\n    FOLD_TO_EXPLAIN = int(fold_metrics.sort_values(\"final_fold_auc\", ascending=False).iloc[0][\"fold\"])\nelse:\n    FOLD_TO_EXPLAIN = 1\n\nprint(f\"Using fold {FOLD_TO_EXPLAIN} for local explanation.\")\n\nfold_pack = load_tabm_fold_for_xai(FOLD_TO_EXPLAIN, cfg, final_seed=FINAL_SEED)\nxai_model = fold_pack[\"model\"]\nX_tr_fold = fold_pack[\"X_tr\"]\nX_val_fold = fold_pack[\"X_val\"]\nval_idx_fold = fold_pack[\"val_idx\"]\n\nwrapped_model = TabMProbWrapper(xai_model).to(DEVICE)\nwrapped_model.eval()\n\nbaseline = torch.tensor(\n    np.median(X_tr_fold, axis=0, keepdims=True),\n    dtype=torch.float32,\n    device=DEVICE\n)\n\nwith torch.no_grad():\n    x_val_tensor = torch.tensor(X_val_fold, dtype=torch.float32, device=DEVICE)\n    val_proba_fold = wrapped_model(x_val_tensor).cpu().numpy()\n\nlocal_idx_in_fold = int(np.argmax(val_proba_fold))\nglobal_row_idx = int(val_idx_fold[local_idx_in_fold])\n\nx_local = torch.tensor(\n    X_val_fold[local_idx_in_fold:local_idx_in_fold+1],\n    dtype=torch.float32,\n    device=DEVICE\n)\n\nattr_local, delta_local = integrated_gradients(\n    wrapped_model,\n    x_local,\n    baseline,\n    n_steps=64\n)\n\nattr_local = attr_local.detach().cpu().numpy()[0]\n\nlocal_df = pd.DataFrame({\n    \"feature\": FEAT_COLS,\n    \"attribution\": attr_local,\n    \"abs_attribution\": np.abs(attr_local),\n    \"raw_feature_value\": X[global_row_idx],\n}).sort_values(\"abs_attribution\", ascending=False)\n\nlocal_df.to_csv(\"logs/tabm_xai_local_explanation.csv\", index=False)\nprint(\"Saved logs/tabm_xai_local_explanation.csv\")\nprint(f\"Explained global row index: {global_row_idx}\")\nprint(f\"Predicted bot probability (fold {FOLD_TO_EXPLAIN} model): {val_proba_fold[local_idx_in_fold]:.4f}\")\nprint(f\"Integrated Gradients completeness delta: {float(delta_local.cpu().numpy()[0]):.6f}\")\n\nprint(\"\\nTop local features:\")\nprint(local_df.head(15).to_string(index=False))\n\ntop_local = local_df.head(15).iloc[::-1]\n\nfig, ax = plt.subplots(figsize=(8, 6))\ncolors = [tabm_color if v > 0 else human_color for v in top_local[\"attribution\"]]\nax.barh(top_local[\"feature\"], top_local[\"attribution\"], color=colors)\nax.set_title(\"TabM Local Explanation (Integrated Gradients)\")\nax.set_xlabel(\"Attribution\")\nplt.tight_layout()\nplt.show()\nsave(fig, \"tabm_xai_local_ig\")\n\n# -------------------------------------------------------------------------\n# 7b. GLOBAL EXPLANATION\n# aggregate mean absolute IG across all folds\n# -------------------------------------------------------------------------\nMAX_ROWS_PER_FOLD = 128\nglobal_attr_sum = np.zeros(len(FEAT_COLS), dtype=np.float64)\nglobal_count = 0\n\nfor fold_no in range(1, len(folds) + 1):\n    fold_pack = load_tabm_fold_for_xai(fold_no, cfg, final_seed=FINAL_SEED)\n    fold_model = fold_pack[\"model\"]\n    X_tr_fold = fold_pack[\"X_tr\"]\n    X_val_fold = fold_pack[\"X_val\"]\n\n    wrapped_fold_model = TabMProbWrapper(fold_model).to(DEVICE)\n    wrapped_fold_model.eval()\n\n    baseline_fold = torch.tensor(\n        np.median(X_tr_fold, axis=0, keepdims=True),\n        dtype=torch.float32,\n        device=DEVICE\n    )\n\n    n_rows = min(MAX_ROWS_PER_FOLD, len(X_val_fold))\n    rng = np.random.default_rng(100 + fold_no)\n    sample_idx = rng.choice(len(X_val_fold), size=n_rows, replace=False)\n\n    x_sample = torch.tensor(\n        X_val_fold[sample_idx],\n        dtype=torch.float32,\n        device=DEVICE\n    )\n\n    attr_global, delta_global = integrated_gradients(\n        wrapped_fold_model,\n        x_sample,\n        baseline_fold,\n        n_steps=32\n    )\n\n    attr_global = attr_global.detach().cpu().numpy()\n    global_attr_sum += np.abs(attr_global).sum(axis=0)\n    global_count += attr_global.shape[0]\n\nglobal_mean_abs = global_attr_sum / max(global_count, 1)\n\nglobal_df = pd.DataFrame({\n    \"feature\": FEAT_COLS,\n    \"mean_abs_attribution\": global_mean_abs,\n}).sort_values(\"mean_abs_attribution\", ascending=False)\n\nglobal_df.to_csv(\"logs/tabm_xai_global_importance.csv\", index=False)\nprint(\"\\nSaved logs/tabm_xai_global_importance.csv\")\nprint(\"\\nTop global features:\")\nprint(global_df.head(20).to_string(index=False))\n\ntop_global = global_df.head(20).iloc[::-1]\n\nfig, ax = plt.subplots(figsize=(8, 7))\nax.barh(top_global[\"feature\"], top_global[\"mean_abs_attribution\"], color=tabm_color)\nax.set_title(\"TabM Global Feature Importance (mean |Integrated Gradients|)\")\nax.set_xlabel(\"Mean absolute attribution\")\nplt.tight_layout()\nplt.show()\nsave(fig, \"tabm_xai_global_ig\")\n\nprint(\"\\n✓ TabM XAI complete.\")\nprint(\"Artifacts:\")\nprint(\"  - logs/tabm_xai_local_explanation.csv\")\nprint(\"  - logs/tabm_xai_global_importance.csv\")\nprint(\"  - plots/tabm_xai_local_ig.png\")\nprint(\"  - plots/tabm_xai_global_ig.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:38:35.676601Z","iopub.execute_input":"2026-04-16T15:38:35.676990Z","iopub.status.idle":"2026-04-16T15:38:38.164222Z","shell.execute_reply.started":"2026-04-16T15:38:35.676966Z","shell.execute_reply":"2026-04-16T15:38:38.163554Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model Comparison","metadata":{}},{"cell_type":"code","source":"# =============================================================================\n# 1. LOAD MODEL SUMMARIES\n# =============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"1. MODEL COMPARISON\")\nprint(\"=\"*60)\n\nsummary_frames = []\nfor path in [\n    \"logs/rf_summary.csv\",\n    \"logs/lgbm_summary.csv\",\n    \"logs/tabm_summary.csv\",\n]:\n    if os.path.exists(path):\n        summary_frames.append(pd.read_csv(path))\n\ncomparison_df = pd.concat(summary_frames, ignore_index=True)\ncomparison_df.to_csv(\"logs/model_comparison_summary.csv\", index=False)\n\nprint(comparison_df.to_string(index=False))\nprint(\"Saved logs/model_comparison_summary.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:38:50.788751Z","iopub.execute_input":"2026-04-16T15:38:50.789059Z","iopub.status.idle":"2026-04-16T15:38:50.803628Z","shell.execute_reply.started":"2026-04-16T15:38:50.789034Z","shell.execute_reply":"2026-04-16T15:38:50.802729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 2. COMPARISON PLOTS\n# =============================================================================\nfig, axes = plt.subplots(1, 2, figsize=(12, 5))\nfig.suptitle(\"Model Comparison\", fontsize=14)\n\naxes[0].bar(comparison_df[\"model\"], comparison_df[\"oof_auc\"], color=[\"#2B9EB3\", \"#E84545\", \"#8E44AD\"][:len(comparison_df)])\naxes[0].set_title(\"OOF AUC\")\naxes[0].set_ylabel(\"AUC\")\n\nap_values = comparison_df[\"oof_ap\"] if \"oof_ap\" in comparison_df.columns else np.nan\naxes[1].bar(comparison_df[\"model\"], ap_values, color=[\"#2B9EB3\", \"#E84545\", \"#8E44AD\"][:len(comparison_df)])\naxes[1].set_title(\"OOF Average Precision\")\naxes[1].set_ylabel(\"AP\")\n\nplt.tight_layout()\nplt.show()\nsave(fig, \"model_comparison_auc_ap\")\n\nbest_model_name = comparison_df.sort_values(\"oof_auc\", ascending=False).iloc[0][\"model\"]\nprint(f\"\\nBest model by OOF AUC: {best_model_name}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T15:38:53.770019Z","iopub.execute_input":"2026-04-16T15:38:53.770319Z","iopub.status.idle":"2026-04-16T15:38:54.268422Z","shell.execute_reply.started":"2026-04-16T15:38:53.770296Z","shell.execute_reply":"2026-04-16T15:38:54.267462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}