{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":5357,"databundleVersionId":38056,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-09T09:10:17.614508Z","iopub.execute_input":"2025-09-09T09:10:17.614673Z","iopub.status.idle":"2025-09-09T09:10:17.875788Z","shell.execute_reply.started":"2025-09-09T09:10:17.614656Z","shell.execute_reply":"2025-09-09T09:10:17.875064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Bosch: Cost-Sensitive Gradient Boosting + Feature Hashing\n# Models: XGBoost, LightGBM, (optional) CatBoost + Ensemble\n# Adds: PR/ROC plots, Threshold–Cost curve, Confusion Matrix,\n#       Model metric bar charts, Warnings silenced\n# Kaggle-ready (reads zipped CSVs from /kaggle/input/)\n# ============================================================\n\nimport os, gc, math, json, time, warnings\nfrom typing import Dict, Any\nimport numpy as np\nimport pandas as pd\nfrom scipy import sparse as sp\n\n# ML / metrics\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import (\n    average_precision_score, roc_auc_score, confusion_matrix,\n    precision_recall_curve, roc_curve\n)\nfrom sklearn.feature_extraction import FeatureHasher\nfrom joblib import dump\n\n# Plots\nimport matplotlib.pyplot as plt\n\n# Silencing noisy warnings (optional)\nwarnings.filterwarnings(\"ignore\")\n\n# ---- Boosted tree libs ----\nHAVE_XGB = True\ntry:\n    from xgboost import XGBClassifier\nexcept Exception:\n    HAVE_XGB = False\n\nHAVE_LGBM = True\ntry:\n    import lightgbm as lgb\nexcept Exception:\n    HAVE_LGBM = False\n\nHAVE_CAT = True\ntry:\n    from catboost import CatBoostClassifier\nexcept Exception:\n    HAVE_CAT = False\n\n\n# =========================\n# Config\n# =========================\nDATA_DIR = \"/kaggle/input/bosch-production-line-performance\"\nWORK_DIR = \"/kaggle/working\"\nARTIFACT_DIR = os.path.join(WORK_DIR, \"artifacts\")\nPLOTS_DIR = os.path.join(WORK_DIR, \"plots\")\nos.makedirs(ARTIFACT_DIR, exist_ok=True)\nos.makedirs(PLOTS_DIR, exist_ok=True)\n\nTRAIN_NUM_ZIP = os.path.join(DATA_DIR, \"train_numeric.csv.zip\")\nTRAIN_CAT_ZIP = os.path.join(DATA_DIR, \"train_categorical.csv.zip\")\nTEST_NUM_ZIP  = os.path.join(DATA_DIR, \"test_numeric.csv.zip\")\nTEST_CAT_ZIP  = os.path.join(DATA_DIR, \"test_categorical.csv.zip\")\n\n# ---- Speed toggles ----\nFAST_PREVIEW = True      # দ্রুত টেস্টিং; ফুল রান চাইলে False দিন\nUSE_GPU_XGB  = True      # Kaggle Accelerator: GPU দিলে True রাখুন\n\nif FAST_PREVIEW:\n    N_ROWS_TRAIN = 100_000\n    N_ROWS_TEST  = None\n    N_HASHED_FEATURES = 2**17     # 131,072\n    CHUNKSIZE_CAT     = 100_000\n    N_SPLITS          = 3\n    EARLY_STOP        = 100\n    XGB_N_ESTIMATORS  = 1500\n    LGBM_N_ESTIMATORS = 1500\n    CAT_ITERATIONS    = 2000\n    SPARSE_COL_MIN_NN_RATE = 0.001  # 0.1% non-null না হলে কলাম ড্রপ\nelse:\n    N_ROWS_TRAIN = None\n    N_ROWS_TEST  = None\n    N_HASHED_FEATURES = 2**18     # 262,144\n    CHUNKSIZE_CAT     = 200_000\n    N_SPLITS          = 5\n    EARLY_STOP        = 200\n    XGB_N_ESTIMATORS  = 5000\n    LGBM_N_ESTIMATORS = 5000\n    CAT_ITERATIONS    = 5000\n    SPARSE_COL_MIN_NN_RATE = 0.0\n\n# কোন মডেল চালাবেন\nENABLE_XGB        = True\nENABLE_LGBM_GBDT  = True\nENABLE_LGBM_DART  = False if FAST_PREVIEW else True\nENABLE_CATBOOST   = False if FAST_PREVIEW else True\n\n# Cost matrix (FN >> FP)\nCOST_FP = 1.0\nCOST_FN = 20.0\nRANDOM_STATE = 42\n\ndef log(msg: str):\n    print(f\"[{time.strftime('%H:%M:%S')}] {msg}\")\n\n# =========================\n# Threshold tuner (cost)\n# =========================\ndef tune_threshold_for_cost(y_true: np.ndarray, p: np.ndarray,\n                            cost_fp: float, cost_fn: float):\n    thresholds = np.linspace(0.01, 0.99, 99)\n    best_t, best_cost = 0.5, float(\"inf\")\n    curve = []\n    for t in thresholds:\n        yhat = (p >= t).astype(int)\n        tn, fp, fn, tp = confusion_matrix(y_true, yhat, labels=[0,1]).ravel()\n        c = cost_fn*fn + cost_fp*fp\n        curve.append({\"t\": float(t), \"cost\": int(c), \"tp\": int(tp), \"fp\": int(fp), \"fn\": int(fn), \"tn\": int(tn)})\n        if c < best_cost:\n            best_cost, best_t = c, t\n    return best_t, best_cost, {\"curve\": curve}\n\n# =========================\n# Data loading (zips OK)\n# =========================\ndef read_train_numeric(n_rows=None):\n    log(f\"Reading numeric (train): {TRAIN_NUM_ZIP}\")\n    df = pd.read_csv(TRAIN_NUM_ZIP, nrows=n_rows, low_memory=False)\n    assert \"Id\" in df.columns and \"Response\" in df.columns\n    df.sort_values(\"Id\", inplace=True)\n    ids = df[\"Id\"].astype(np.int64).values\n    y   = df[\"Response\"].astype(np.int8).values\n    Xn  = df.drop(columns=[\"Id\", \"Response\"])\n    for c in Xn.columns:\n        Xn[c] = pd.to_numeric(Xn[c], errors=\"coerce\", downcast=\"float\")\n    X_num = sp.csr_matrix(Xn.values)\n    log(f\"Numeric loaded: rows={X_num.shape[0]}, cols={X_num.shape[1]}\")\n    del df, Xn; gc.collect()\n    return ids, y, X_num\n\ndef read_test_numeric(n_rows=None):\n    log(f\"Reading numeric (test): {TEST_NUM_ZIP}\")\n    df = pd.read_csv(TEST_NUM_ZIP, nrows=n_rows, low_memory=False)\n    assert \"Id\" in df.columns\n    df.sort_values(\"Id\", inplace=True)\n    ids = df[\"Id\"].astype(np.int64).values\n    Xn  = df.drop(columns=[\"Id\"])\n    for c in Xn.columns:\n        Xn[c] = pd.to_numeric(Xn[c], errors=\"coerce\", downcast=\"float\")\n    X_num = sp.csr_matrix(Xn.values)\n    log(f\"Numeric test loaded: rows={X_num.shape[0]}, cols={X_num.shape[1]}\")\n    del df, Xn; gc.collect()\n    return ids, X_num\n\ndef hashed_cat_matrix(cat_zip_path: str,\n                      id_to_row_index: Dict[int, int],\n                      n_rows_total: int,\n                      n_features: int = N_HASHED_FEATURES,\n                      chunksize: int = CHUNKSIZE_CAT) -> sp.csr_matrix:\n    \"\"\"\n    Subset-aware categorical hashing:\n    - Only rows whose Id is in id_to_row_index are processed (no KeyError)\n    - Optional rare-column pruning by non-null rate\n    \"\"\"\n    log(f\"Hashing categorical (chunked, subset-aware): {cat_zip_path}\")\n    hasher = FeatureHasher(n_features=n_features, input_type=\"string\", alternate_sign=False)\n\n    known_ids = set(id_to_row_index.keys())\n    data_all, rows_all, cols_all = [], [], []\n\n    for chunk in pd.read_csv(cat_zip_path, chunksize=chunksize, low_memory=False):\n        if \"Id\" not in chunk.columns:\n            raise ValueError(\"Expected 'Id' column in categorical file.\")\n        chunk[\"Id\"] = chunk[\"Id\"].astype(np.int64)\n\n        # keep only ids that exist in numeric subset\n        mask = chunk[\"Id\"].map(known_ids.__contains__).values\n        if not np.any(mask):\n            continue\n        chunk = chunk.loc[mask]\n\n        # drop all-NaN columns\n        chunk = chunk.dropna(axis=1, how=\"all\")\n\n        cat_cols = [c for c in chunk.columns if c != \"Id\"]\n        if not cat_cols:\n            continue\n\n        # (optional) rare column pruning by non-null rate\n        if SPARSE_COL_MIN_NN_RATE > 0:\n            nn_rate = chunk[cat_cols].notna().mean()\n            keep = nn_rate[nn_rate > SPARSE_COL_MIN_NN_RATE].index.tolist()\n            if keep:\n                cat_cols = keep\n            if not cat_cols:\n                continue\n\n        ids_chunk = chunk[\"Id\"].values\n\n        # Build tokens\n        tokens_per_row = []\n        for row in chunk[cat_cols].itertuples(index=False, name=None):\n            toks = [f\"{c}={v}\" for c, v in zip(cat_cols, row) if pd.notna(v)]\n            tokens_per_row.append(toks)\n\n        Xh = hasher.transform(tokens_per_row).tocoo()\n        if Xh.nnz == 0:\n            continue\n\n        global_rows_for_chunk = np.fromiter((id_to_row_index[int(i)] for i in ids_chunk),\n                                            dtype=np.int64, count=len(ids_chunk))\n        global_rows = global_rows_for_chunk[Xh.row]\n\n        data_all.append(Xh.data); rows_all.append(global_rows); cols_all.append(Xh.col)\n\n        del chunk, Xh, tokens_per_row, global_rows_for_chunk, global_rows\n        gc.collect()\n\n    if not data_all:\n        return sp.csr_matrix((n_rows_total, n_features))\n\n    data_all = np.concatenate(data_all)\n    rows_all = np.concatenate(rows_all)\n    cols_all = np.concatenate(cols_all)\n\n    X_cat = sp.coo_matrix((data_all, (rows_all, cols_all)),\n                          shape=(n_rows_total, n_features)).tocsr()\n    log(f\"Categorical hashed: rows={X_cat.shape[0]}, hashed_bins={X_cat.shape[1]}\")\n    return X_cat\n\ndef load_design_matrix_train(n_rows=N_ROWS_TRAIN):\n    ids, y, X_num = read_train_numeric(n_rows=n_rows)\n    id_to_row = {int(i): r for r, i in enumerate(ids)}\n    X_cat = hashed_cat_matrix(TRAIN_CAT_ZIP, id_to_row, len(ids),\n                              n_features=N_HASHED_FEATURES, chunksize=CHUNKSIZE_CAT)\n    X = sp.hstack([X_num, X_cat], format=\"csr\")\n    log(f\"TRAIN matrix: rows={X.shape[0]}, cols={X.shape[1]}\")\n    return ids, y, X\n\ndef load_design_matrix_test(n_rows=N_ROWS_TEST):\n    ids, X_num = read_test_numeric(n_rows=n_rows)\n    id_to_row = {int(i): r for r, i in enumerate(ids)}\n    X_cat = hashed_cat_matrix(TEST_CAT_ZIP, id_to_row, len(ids),\n                              n_features=N_HASHED_FEATURES, chunksize=CHUNKSIZE_CAT)\n    X = sp.hstack([X_num, X_cat], format=\"csr\")\n    log(f\"TEST matrix: rows={X.shape[0]}, cols={X.shape[1]}\")\n    return ids, X\n\n# =========================\n# Model builders\n# =========================\ndef build_xgb():\n    if not HAVE_XGB:\n        raise ImportError(\"xgboost not installed.\")\n    # XGBoost 2.x: GPU-এর জন্য tree_method নয়, device=\"cuda\" ব্যবহার করুন\n    params = dict(\n        n_estimators=XGB_N_ESTIMATORS,\n        learning_rate=0.03,\n        max_depth=6,\n        subsample=0.8,\n        colsample_bytree=0.8,\n        eval_metric=\"aucpr\",\n        reg_alpha=0.0,\n        reg_lambda=1.0,\n        random_state=RANDOM_STATE,\n        n_jobs=-1,\n        early_stopping_rounds=EARLY_STOP,\n        device=(\"cuda\" if USE_GPU_XGB else \"cpu\")\n    )\n    return XGBClassifier(**params)\n\ndef build_lgbm(boosting_type=\"gbdt\"):\n    if not HAVE_LGBM:\n        raise ImportError(\"lightgbm not installed.\")\n    # নোট: GPU চাইলে Kaggle ইমেজে সাপোর্ট সাধারণত থাকে; verbosity কমানো হলো\n    return lgb.LGBMClassifier(\n        objective=\"binary\",\n        boosting_type=boosting_type,\n        n_estimators=LGBM_N_ESTIMATORS,\n        learning_rate=0.03,\n        num_leaves=63,\n        subsample=0.8,\n        colsample_bytree=0.8,\n        reg_alpha=0.0,\n        reg_lambda=1.0,\n        random_state=RANDOM_STATE,\n        n_jobs=-1,\n        verbosity=-1\n    )\n\ndef build_cat():\n    if not HAVE_CAT:\n        raise ImportError(\"catboost not installed.\")\n    return CatBoostClassifier(\n        task_type=\"GPU\" if USE_GPU_XGB else \"CPU\",\n        devices=\"0\" if USE_GPU_XGB else None,\n        loss_function=\"Logloss\",\n        eval_metric=\"AUC\",\n        iterations=CAT_ITERATIONS,\n        learning_rate=0.03,\n        depth=6,\n        l2_leaf_reg=3.0,\n        random_seed=RANDOM_STATE,\n        od_type=\"Iter\",\n        od_wait=EARLY_STOP,\n        verbose=False,\n        allow_writing_files=False\n    )\n\n# =========================\n# CV runner\n# =========================\ndef run_cv(model_name: str, model_builder, X: sp.csr_matrix, y: np.ndarray,\n           cost_fp: float, cost_fn: float, early_stop_rounds: int = EARLY_STOP) -> Dict[str, Any]:\n    skf = StratifiedKFold(n_splits=N_SPLITS, shuffle=True, random_state=RANDOM_STATE)\n    oof_pred = np.zeros_like(y, dtype=float)\n    pr_aucs, roc_aucs, costs, best_ts, best_iters = [], [], [], [], []\n    fold = 0\n\n    for tr_idx, va_idx in skf.split(X, y):\n        fold += 1\n        log(f\"[{model_name}] Fold {fold}/{N_SPLITS}\")\n        X_tr, X_va = X[tr_idx], X[va_idx]\n        y_tr, y_va = y[tr_idx], y[va_idx]\n        w_tr = np.where(y_tr == 1, cost_fn, cost_fp)\n        w_va = np.where(y_va == 1, cost_fn, cost_fp)\n\n        model = model_builder()\n\n        if model_name.startswith(\"lgbm\"):\n            model.fit(\n                X_tr, y_tr,\n                sample_weight=w_tr,\n                eval_set=[(X_va, y_va)],\n                callbacks=[lgb.early_stopping(stopping_rounds=early_stop_rounds, verbose=False)]\n            )\n            bi = getattr(model, \"best_iteration_\", None)\n        elif model_name.startswith(\"xgb\"):\n            model.fit(\n                X_tr, y_tr,\n                sample_weight=w_tr,\n                eval_set=[(X_va, y_va)],\n                sample_weight_eval_set=[w_va],\n                verbose=False\n            )\n            bi = getattr(model, \"best_iteration\", None)\n        elif model_name.startswith(\"cat\"):\n            model.fit(\n                X_tr, y_tr,\n                sample_weight=w_tr,\n                eval_set=[(X_va, y_va)],\n                use_best_model=True\n            )\n            bi = model.get_best_iteration()\n        else:\n            model.fit(X_tr, y_tr, sample_weight=w_tr); bi = None\n\n        p = model.predict_proba(X_va)[:, 1]\n        oof_pred[va_idx] = p\n\n        pr = average_precision_score(y_va, p)\n        ra = roc_auc_score(y_va, p)\n        t_best, cost_best, _ = tune_threshold_for_cost(y_va, p, cost_fp, cost_fn)\n\n        pr_aucs.append(pr); roc_aucs.append(ra)\n        costs.append(cost_best); best_ts.append(float(t_best))\n        if bi is not None:\n            best_iters.append(int(bi))\n\n        dump(model, os.path.join(ARTIFACT_DIR, f\"{model_name}_fold{fold}.joblib\"))\n        log(f\"[{model_name}] Fold {fold}: PR-AUC={pr:.5f}, ROC-AUC={ra:.5f}, t*={t_best:.2f}, cost={cost_best}\"\n            + (f\", best_iter={bi}\" if bi is not None else \"\"))\n\n        del model, X_tr, X_va, y_tr, y_va, w_tr, w_va, p; gc.collect()\n\n    t_global, cost_global, details = tune_threshold_for_cost(y, oof_pred, cost_fp, cost_fn)\n\n    result = {\n        \"model\": model_name,\n        \"pr_auc_mean\": float(np.mean(pr_aucs)),\n        \"pr_auc_std\": float(np.std(pr_aucs)),\n        \"roc_auc_mean\": float(np.mean(roc_aucs)),\n        \"roc_auc_std\": float(np.std(roc_aucs)),\n        \"cv_cost_mean\": float(np.mean(costs)),\n        \"cv_cost_std\": float(np.std(costs)),\n        \"t_cv_mean\": float(np.mean(best_ts)),\n        \"t_global\": float(t_global),\n        \"oof_pred_path\": os.path.join(ARTIFACT_DIR, f\"oof_{model_name}.npy\"),\n        \"threshold_cost_curve\": os.path.join(ARTIFACT_DIR, f\"threshold_cost_curve_{model_name}.json\"),\n        \"best_iterations\": best_iters,\n        \"recommended_n_estimators\": int(np.mean(best_iters)) if best_iters else None\n    }\n    np.save(result[\"oof_pred_path\"], oof_pred)\n    with open(result[\"threshold_cost_curve\"], \"w\") as f:\n        json.dump(details, f)\n\n    log(f\"[{model_name}] OOF: PR-AUC={result['pr_auc_mean']:.5f}±{result['pr_auc_std']:.5f} | \"\n        f\"ROC-AUC={result['roc_auc_mean']:.5f}±{result['roc_auc_std']:.5f} | \"\n        f\"t_global={result['t_global']:.2f} | mean_cost={result['cv_cost_mean']:.1f}\")\n    return result\n\n\n# =========================\n# Plot helpers\n# =========================\ndef plot_pr_roc(y_true, p, prefix):\n    pr, rc, th = precision_recall_curve(y_true, p)\n    fpr, tpr, _ = roc_curve(y_true, p)\n\n    # PR\n    plt.figure(figsize=(6,4))\n    plt.plot(rc, pr, lw=2)\n    plt.xlabel(\"Recall\"); plt.ylabel(\"Precision\"); plt.title(f\"PR Curve ({prefix})\")\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout(); plt.savefig(os.path.join(PLOTS_DIR, f\"{prefix}_pr_curve.png\")); plt.close()\n\n    # ROC\n    plt.figure(figsize=(6,4))\n    plt.plot(fpr, tpr, lw=2)\n    plt.xlabel(\"FPR\"); plt.ylabel(\"TPR\"); plt.title(f\"ROC Curve ({prefix})\")\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout(); plt.savefig(os.path.join(PLOTS_DIR, f\"{prefix}_roc_curve.png\")); plt.close()\n\ndef plot_threshold_cost(details_json_path, prefix):\n    with open(details_json_path, \"r\") as f:\n        data = json.load(f)[\"curve\"]\n    ts = [d[\"t\"] for d in data]; cs = [d[\"cost\"] for d in data]\n    plt.figure(figsize=(6,4))\n    plt.plot(ts, cs, lw=2)\n    plt.xlabel(\"Threshold\"); plt.ylabel(\"Expected Cost\")\n    plt.title(f\"Threshold–Cost Curve ({prefix})\"); plt.grid(True, alpha=0.3)\n    plt.tight_layout(); plt.savefig(os.path.join(PLOTS_DIR, f\"{prefix}_threshold_cost.png\")); plt.close()\n\ndef plot_confmat(cm, prefix):\n    tn, fp, fn, tp = cm.ravel()\n    fig, ax = plt.subplots(figsize=(4.5,4))\n    im = ax.imshow([[tn, fp],[fn, tp]], cmap=\"Blues\")\n    for (i,j), val in np.ndenumerate([[tn, fp],[fn, tp]]):\n        ax.text(j, i, int(val), ha=\"center\", va=\"center\", fontsize=12)\n    ax.set_xticks([0,1]); ax.set_xticklabels([\"Pred 0\",\"Pred 1\"])\n    ax.set_yticks([0,1]); ax.set_yticklabels([\"True 0\",\"True 1\"])\n    ax.set_title(f\"Confusion Matrix ({prefix})\")\n    fig.colorbar(im, ax=ax, shrink=0.8)\n    plt.tight_layout(); plt.savefig(os.path.join(PLOTS_DIR, f\"{prefix}_confusion.png\")); plt.close()\n\ndef plot_model_bars(results):\n    if not results: return\n    df = pd.DataFrame([{\n        \"model\": r[\"model\"], \"PR-AUC\": r[\"pr_auc_mean\"], \"ROC-AUC\": r[\"roc_auc_mean\"]\n    } for r in results])\n    # PR-AUC\n    plt.figure(figsize=(6,4))\n    order = df.sort_values(\"PR-AUC\", ascending=False)\n    plt.bar(order[\"model\"], order[\"PR-AUC\"])\n    plt.xticks(rotation=20); plt.ylabel(\"PR-AUC\"); plt.title(\"Model PR-AUC (OOF mean)\")\n    plt.tight_layout(); plt.savefig(os.path.join(PLOTS_DIR, \"models_pr_auc_bar.png\")); plt.close()\n    # ROC-AUC\n    plt.figure(figsize=(6,4))\n    order = df.sort_values(\"ROC-AUC\", ascending=False)\n    plt.bar(order[\"model\"], order[\"ROC-AUC\"])\n    plt.xticks(rotation=20); plt.ylabel(\"ROC-AUC\"); plt.title(\"Model ROC-AUC (OOF mean)\")\n    plt.tight_layout(); plt.savefig(os.path.join(PLOTS_DIR, \"models_roc_auc_bar.png\")); plt.close()\n\n\n# =========================\n# Final refit for test (optional)\n# =========================\ndef refit_full(model_name: str, model_builder, X: sp.csr_matrix, y: np.ndarray, n_estimators_hint: int = None):\n    model = model_builder()\n    if n_estimators_hint is not None:\n        if model_name.startswith(\"lgbm\"):\n            model.set_params(n_estimators=int(n_estimators_hint))\n        elif model_name.startswith(\"xgb\"):\n            model.set_params(n_estimators=int(n_estimators_hint))\n        elif model_name.startswith(\"cat\"):\n            model.set_params(iterations=int(n_estimators_hint))\n    w = np.where(y == 1, COST_FN, COST_FP)\n    model.fit(X, y, sample_weight=w)\n    dump(model, os.path.join(ARTIFACT_DIR, f\"{model_name}_FULL.joblib\"))\n    return model\n\n\n# =========================\n# Main\n# =========================\nif __name__ == \"__main__\":\n    log(\"Loading TRAIN matrix ...\")\n    ids_tr, y, X = load_design_matrix_train(n_rows=N_ROWS_TRAIN)\n\n    with open(os.path.join(ARTIFACT_DIR, \"run_meta.json\"), \"w\") as f:\n        json.dump({\n            \"rows_train\": int(X.shape[0]),\n            \"cols_total\": int(X.shape[1]),\n            \"hashed_bins\": int(N_HASHED_FEATURES),\n            \"splits\": int(N_SPLITS),\n            \"cost_fp\": float(COST_FP),\n            \"cost_fn\": float(COST_FN),\n            \"random_state\": int(RANDOM_STATE),\n            \"fast_preview\": FAST_PREVIEW,\n            \"gpu_xgb\": USE_GPU_XGB\n        }, f, indent=2)\n\n    results = []\n\n    if ENABLE_XGB and HAVE_XGB:\n        results.append(run_cv(\"xgb_gbdt\", build_xgb, X, y, COST_FP, COST_FN))\n    if ENABLE_LGBM_GBDT and HAVE_LGBM:\n        results.append(run_cv(\"lgbm_gbdt\", lambda: build_lgbm(\"gbdt\"), X, y, COST_FP, COST_FN))\n    if ENABLE_LGBM_DART and HAVE_LGBM:\n        results.append(run_cv(\"lgbm_dart\", lambda: build_lgbm(\"dart\"), X, y, COST_FP, COST_FN))\n    if ENABLE_CATBOOST and HAVE_CAT:\n        results.append(run_cv(\"cat_gbdt\", build_cat, X, y, COST_FP, COST_FN))\n\n    # Save CV summary\n    with open(os.path.join(ARTIFACT_DIR, \"cv_summary.json\"), \"w\") as f:\n        json.dump(results, f, indent=2)\n\n    # ---- Create visualizations ----\n    plot_model_bars(results)  # bar charts for PR/ROC\n\n    # For each model: threshold–cost curve plot\n    for r in results:\n        plot_threshold_cost(r[\"threshold_cost_curve\"], r[\"model\"])\n\n    # If at least two models: ensemble OOF\n    oof_paths = [r[\"oof_pred_path\"] for r in results if os.path.exists(r[\"oof_pred_path\"])]\n    if len(oof_paths) >= 2:\n        oofs = [np.load(p) for p in oof_paths]\n        ens_oof = np.mean(np.vstack(oofs), axis=0)\n        ens_pr  = average_precision_score(y, ens_oof)\n        ens_roc = roc_auc_score(y, ens_oof)\n        ens_t, ens_cost, ens_details = tune_threshold_for_cost(y, ens_oof, COST_FP, COST_FN)\n        np.save(os.path.join(ARTIFACT_DIR, \"oof_ensemble_mean.npy\"), ens_oof)\n        with open(os.path.join(ARTIFACT_DIR, \"threshold_cost_curve_ensemble.json\"), \"w\") as f:\n            json.dump(ens_details, f, indent=2)\n        with open(os.path.join(ARTIFACT_DIR, \"ensemble_summary.json\"), \"w\") as f:\n            json.dump({\n                \"model\": \"ensemble_mean\",\n                \"pr_auc\": float(ens_pr),\n                \"roc_auc\": float(ens_roc),\n                \"t_global\": float(ens_t),\n                \"cost\": float(ens_cost),\n                \"members\": [os.path.basename(p)[4:-4] for p in oof_paths]\n            }, f, indent=2)\n\n        # Ensemble PR/ROC + Confusion at t*\n        plot_pr_roc(y, ens_oof, \"ensemble\")\n        yhat = (ens_oof >= ens_t).astype(int)\n        cm = confusion_matrix(y, yhat, labels=[0,1])\n        plot_confmat(cm, \"ensemble\")\n        # Ensemble threshold–cost\n        plot_threshold_cost(os.path.join(ARTIFACT_DIR, \"threshold_cost_curve_ensemble.json\"), \"ensemble\")\n    else:\n        # Single best model plots from its OOF\n        best = sorted(results, key=lambda d: d[\"pr_auc_mean\"], reverse=True)[0]\n        p = np.load(best[\"oof_pred_path\"])\n        plot_pr_roc(y, p, best[\"model\"])\n        yhat = (p >= best[\"t_global\"]).astype(int)\n        cm = confusion_matrix(y, yhat, labels=[0,1])\n        plot_confmat(cm, best[\"model\"])\n\n    log(\"Done. Artifacts in /kaggle/working/artifacts/ and plots in /kaggle/working/plots/\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-09T09:10:17.877345Z","iopub.execute_input":"2025-09-09T09:10:17.877701Z","iopub.status.idle":"2025-09-09T09:21:00.042514Z","shell.execute_reply.started":"2025-09-09T09:10:17.877683Z","shell.execute_reply":"2025-09-09T09:21:00.041827Z"}},"outputs":[],"execution_count":null}]}