{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"none","dataSources":[{"sourceType":"competition","sourceId":50160,"databundleVersionId":7921029}],"dockerImageVersionId":31328,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Submission 5 — Improved LightGBM (Best Params from Optuna)\n\nSame features as Sub 2 (depth-0 + 4 depth-1 aggregations + stability filter).\n\nUses best params found by Optuna locally:\n- walk-forward CV (3 folds)\n- custom Gini stability feval\n- Optuna best: learning_rate=0.0228, num_leaves=55, min_child_samples=38\n\nOutputs `lgbm_test_preds.npy` for ensemble and `submission_05_lgbm_improved.csv` for direct submission.","metadata":{}},{"cell_type":"code","source":"import glob\nimport polars as pl\nimport numpy as np\nimport lightgbm as lgb\nimport optuna\nfrom sklearn.metrics import roc_auc_score\nfrom scipy.stats import linregress\nimport matplotlib.pyplot as plt\nimport gc\nimport os\nimport warnings\nwarnings.filterwarnings('ignore')\noptuna.logging.set_verbosity(optuna.logging.WARNING)\n\nDATA   = '/kaggle/input/competitions/home-credit-credit-risk-model-stability/parquet_files'\nOUTPUT = '/kaggle/working/'\nTRAIN_CUTOFF = 60","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Aggregate depth-1 tables","metadata":{}},{"cell_type":"code","source":"def aggregate_lazy(pattern: str, name: str) -> pl.DataFrame:\n    files = sorted(glob.glob(pattern))\n    if not files:\n        raise FileNotFoundError(f'No files found: {pattern}')\n    print(f'{name}: {len(files)} file(s)')\n\n    ref_schema = None\n    for f in files:\n        s = pl.read_parquet(f, n_rows=10).schema\n        if any(v != pl.Null for v in s.values()):\n            ref_schema = s\n            break\n\n    parts = []\n    for f in files:\n        part = pl.read_parquet(f)\n        exprs = []\n        for c in part.columns:\n            if c == 'case_id':\n                continue\n            dtype = part[c].dtype\n            if dtype == pl.Null and ref_schema and c in ref_schema:\n                target = ref_schema[c]\n                exprs.append(pl.col(c).cast(target if target != pl.Null else pl.Utf8))\n            elif dtype in [pl.Float64, pl.Int64, pl.Int32, pl.Int16, pl.Int8]:\n                exprs.append(pl.col(c).cast(pl.Float32))\n        if exprs:\n            part = part.with_columns(exprs)\n        parts.append(part)\n\n    df = pl.concat(parts, how='diagonal_relaxed')\n    del parts\n    gc.collect()\n\n    schema   = df.schema\n    num_cols = [c for c, dtype in schema.items() if c != 'case_id' and dtype == pl.Float32]\n    cat_cols = [c for c, dtype in schema.items() if c != 'case_id' and dtype == pl.Utf8]\n\n    aggs = [pl.len().alias(f'{name}_count').cast(pl.Int32)]\n    for col in num_cols:\n        aggs += [\n            pl.col(col).mean().alias(f'{name}_{col}_mean'),\n            pl.col(col).max().alias(f'{name}_{col}_max'),\n            pl.col(col).min().alias(f'{name}_{col}_min'),\n            pl.col(col).std().alias(f'{name}_{col}_std'),\n        ]\n    for col in cat_cols:\n        aggs.append(pl.col(col).n_unique().cast(pl.Int16).alias(f'{name}_{col}_nunique'))\n\n    result = df.group_by('case_id').agg(aggs)\n    del df\n    gc.collect()\n    print(f'  -> {result.shape}')\n    return result","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_applprev      = aggregate_lazy(f'{DATA}/train/train_applprev_1_*.parquet',        'applprev')\nagg_cb_a          = aggregate_lazy(f'{DATA}/train/train_credit_bureau_a_1_*.parquet', 'cb_a')\nagg_cb_b          = aggregate_lazy(f'{DATA}/train/train_credit_bureau_b_1.parquet',   'cb_b')\nagg_person        = aggregate_lazy(f'{DATA}/train/train_person_1.parquet',            'person')\n\nagg_applprev_test = aggregate_lazy(f'{DATA}/test/test_applprev_1_*.parquet',          'applprev')\nagg_cb_a_test     = aggregate_lazy(f'{DATA}/test/test_credit_bureau_a_1_*.parquet',   'cb_a')\nagg_cb_b_test     = aggregate_lazy(f'{DATA}/test/test_credit_bureau_b_1.parquet',     'cb_b')\nagg_person_test   = aggregate_lazy(f'{DATA}/test/test_person_1.parquet',              'person')","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load depth-0 tables and join","metadata":{}},{"cell_type":"code","source":"def cast_static(df: pl.DataFrame) -> pl.DataFrame:\n    return df.with_columns([\n        pl.col(c).cast(pl.Float32)\n        for c in df.columns\n        if df[c].dtype in [pl.Float64, pl.Int64, pl.Int32]\n        and c not in ('case_id', 'target', 'WEEK_NUM', 'MONTH')\n    ])\n\nbase      = pl.read_parquet(f'{DATA}/train/train_base.parquet')\nstatic0   = pl.scan_parquet(sorted(glob.glob(f'{DATA}/train/train_static_0_*.parquet'))).collect().pipe(cast_static)\nstatic_cb = pl.read_parquet(f'{DATA}/train/train_static_cb_0.parquet').pipe(cast_static)\n\ntest_base      = pl.read_parquet(f'{DATA}/test/test_base.parquet')\ntest_static0   = pl.scan_parquet(sorted(glob.glob(f'{DATA}/test/test_static_0_*.parquet'))).collect().pipe(cast_static)\ntest_static_cb = pl.read_parquet(f'{DATA}/test/test_static_cb_0.parquet').pipe(cast_static)\n\ndef build(base_df, s0, scb, aggs):\n    df = base_df.join(s0, on='case_id', how='left').join(scb, on='case_id', how='left')\n    for agg in aggs:\n        df = df.join(agg, on='case_id', how='left')\n    return df\n\ndf   = build(base, static0, static_cb, [agg_applprev, agg_cb_a, agg_cb_b, agg_person])\ntest = build(test_base, test_static0, test_static_cb, [agg_applprev_test, agg_cb_a_test, agg_cb_b_test, agg_person_test])\n\ndel static0, static_cb, test_static0, test_static_cb\ndel agg_applprev, agg_cb_a, agg_cb_b, agg_person\ndel agg_applprev_test, agg_cb_a_test, agg_cb_b_test, agg_person_test\ngc.collect()\n\nprint(f'Train: {df.shape}')\nprint(f'Test:  {test.shape}')","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Preprocessing","metadata":{}},{"cell_type":"code","source":"DROP_COLS = ['case_id', 'date_decision', 'MONTH', 'WEEK_NUM', 'target']\nTEST_DROP = ['case_id', 'date_decision', 'MONTH', 'WEEK_NUM']\n\ndef preprocess(df: pl.DataFrame, drop_cols: list) -> pl.DataFrame:\n    date_cols = [c for c in df.columns if c.endswith('_D') and c not in drop_cols]\n    for col in date_cols:\n        df = df.with_columns(\n            pl.col(col).str.to_date(strict=False).cast(pl.Int32).alias(col)\n        )\n    cat_cols = [c for c in df.columns if df[c].dtype == pl.Utf8 and c not in drop_cols]\n    for col in cat_cols:\n        df = df.with_columns(\n            pl.col(col).cast(pl.Categorical).to_physical().cast(pl.Int16).alias(col)\n        )\n    return df.drop([c for c in drop_cols if c in df.columns])\n\nweek_num = df['WEEK_NUM'].to_numpy()\ny        = df['target'].to_numpy()\n\nX_df      = preprocess(df, DROP_COLS)\nX_test_df = preprocess(test, TEST_DROP)\ndel df, test\ngc.collect()\n\nfeature_cols = [c for c in X_df.columns if c in X_test_df.columns]\nX_df         = X_df.select(feature_cols)\nX_test_df    = X_test_df.select(feature_cols)\n\nprint(f'Features before stability filter: {len(feature_cols)}')","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Stability-first feature filtering","metadata":{}},{"cell_type":"code","source":"train_mask = week_num <= TRAIN_CUTOFF\n\nnumeric_cols = [\n    c for c in X_df.columns\n    if X_df[c].dtype in [pl.Float32, pl.Float64, pl.Int32, pl.Int16, pl.Int8]\n]\n\ndf_wk = X_df.filter(pl.Series(train_mask)).select(numeric_cols).with_columns(\n    pl.Series('WEEK_NUM', week_num[train_mask])\n)\nweekly_means = df_wk.group_by('WEEK_NUM').agg([pl.col(c).mean() for c in numeric_cols]).sort('WEEK_NUM')\n\ndrift = {}\nfor col in numeric_cols:\n    vals = weekly_means[col].drop_nulls().to_numpy()\n    if len(vals) < 5 or np.abs(vals.mean()) < 1e-9:\n        drift[col] = 0.0\n    else:\n        drift[col] = vals.std() / np.abs(vals.mean())\n\ndel df_wk, weekly_means\ngc.collect()\n\ndrift_df   = pl.DataFrame({'feature': list(drift.keys()), 'drift': list(drift.values())}).sort('drift', descending=True)\nn_drop     = max(1, int(len(drift_df) * 0.10))\nunstable   = set(drift_df.head(n_drop)['feature'].to_list())\nstable_features = [c for c in feature_cols if c not in unstable]\n\nprint(f'Dropped {len(unstable)} unstable features, {len(stable_features)} remaining')","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X      = X_df.select(stable_features).to_numpy()\nX_test = X_test_df.select(stable_features).to_numpy()\ndel X_df, X_test_df\ngc.collect()\nprint(f'Feature matrix: {X.shape}, Test: {X_test.shape}')\n\nscale_pos = float((y == 0).sum() / (y == 1).sum())\n","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Gini stability metric","metadata":{}},{"cell_type":"code","source":"def gini_stability(week_nums, targets, preds):\n    weeks = np.unique(week_nums)\n    weekly_gini = []\n    for w in weeks:\n        mask = week_nums == w\n        if targets[mask].sum() == 0:\n            continue\n        weekly_gini.append(2 * roc_auc_score(targets[mask], preds[mask]) - 1)\n    weekly_gini = np.array(weekly_gini)\n    mean_gini   = weekly_gini.mean()\n    slope, intercept, *_ = linregress(np.arange(len(weekly_gini)), weekly_gini)\n    residuals   = weekly_gini - (intercept + slope * np.arange(len(weekly_gini)))\n    return mean_gini + 88.0 * min(0, slope) - 0.5 * residuals.std(), mean_gini, slope, residuals.std(), weekly_gini","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Walk-forward CV helpers\n\nThree expanding-window folds — each fold trains on all weeks up to `train_end`,\nvalidates on the immediately following window. This gives a more honest stability\nestimate than a single split and is required for Optuna to tune on.","metadata":{}},{"cell_type":"code","source":"FOLDS = [\n    (0, 40,  41, 60),   # fold 1: train 0-40, val 41-60\n    (0, 60,  61, 75),   # fold 2: train 0-60, val 61-75\n    (0, 75,  76, 91),   # fold 3: train 0-75, val 76-91\n]\n\n# Module-level variable used by the custom feval closure (set before each fold's fit call).\ncurrent_val_weeks = None\n\ndef gini_stability_feval(y_true, y_pred):\n    \"\"\"LightGBM custom eval metric. Signature: (y_true, y_pred) -> (name, value, higher_is_better)\"\"\"\n    score, *_ = gini_stability(current_val_weeks, y_true, y_pred)\n    return 'gini_stability', score, True\n\ndef run_cv(params, verbose=True):\n    \"\"\"Walk-forward CV. Returns mean Gini stability across folds.\"\"\"\n    global current_val_weeks\n    scores = []\n    for i, (tr_start, tr_end, va_start, va_end) in enumerate(FOLDS):\n        tr_mask = (week_num >= tr_start) & (week_num <= tr_end)\n        va_mask = (week_num >= va_start) & (week_num <= va_end)\n\n        X_tr, y_tr = X[tr_mask], y[tr_mask]\n        X_va, y_va = X[va_mask], y[va_mask]\n        current_val_weeks = week_num[va_mask]\n\n        model = lgb.LGBMClassifier(**params)\n        model.fit(\n            X_tr, y_tr,\n            eval_set=[(X_va, y_va)],\n            eval_metric=gini_stability_feval,\n            callbacks=[\n                lgb.early_stopping(50, verbose=False),\n                lgb.log_evaluation(0),\n            ],\n        )\n\n        preds = model.predict_proba(X_va)[:, 1]\n        score, mean_g, slope, resid_std, _ = gini_stability(current_val_weeks, y_va, preds)\n        scores.append(score)\n        if verbose:\n            print(f'  Fold {i+1} | best_iter={model.best_iteration_:4d} | '\n                  f'gini_stab={score:.4f}  (mean_gini={mean_g:.4f}, slope={slope:.5f}, resid_std={resid_std:.4f})')\n\n    mean_score = float(np.mean(scores))\n    if verbose:\n        print(f'Mean Gini stability across folds: {mean_score:.4f}')\n    return mean_score","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Final training with best params\n\nPaste Optuna's `best_params` into `tuned_params` below after the search finishes.\nThe defaults here are reasonable placeholders until then.","metadata":{}},{"cell_type":"code","source":"# Best params from Optuna (found locally on M2 Max, 10 trials)\ntuned_params = {\n    'objective': 'binary', 'metric': 'custom',\n    'n_estimators': 1000,\n    'learning_rate':     0.022812809566861304,\n    'num_leaves':        55,\n    'min_child_samples': 38,\n    'feature_fraction':  0.774816395214887,\n    'bagging_fraction':  0.5187196516747448,\n    'bagging_freq':      1,\n    'reg_alpha':         0.7931156231178,\n    'reg_lambda':        0.3782056934716548,\n    'scale_pos_weight':  scale_pos,\n    'n_jobs': -1, 'verbose': -1, 'random_state': 42,\n}\n\n","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Evaluate on last fold (val weeks 76–91)","metadata":{}},{"cell_type":"code","source":"tr_mask = week_num <= 75\nva_mask = week_num > 75\n\nX_tr, y_tr = X[tr_mask], y[tr_mask]\nX_va, y_va = X[va_mask], y[va_mask]\ncurrent_val_weeks = week_num[va_mask]\n\neval_model = lgb.LGBMClassifier(**tuned_params)\neval_model.fit(\n    X_tr, y_tr,\n    eval_set=[(X_va, y_va)],\n    eval_metric=gini_stability_feval,\n    callbacks=[\n        lgb.early_stopping(50, verbose=False),\n        lgb.log_evaluation(100),\n    ],\n)\n\nval_preds = eval_model.predict_proba(X_va)[:, 1]\nauc = roc_auc_score(y_va, val_preds)\nscore, mean_gini, slope, resid_std, weekly_gini = gini_stability(current_val_weeks, y_va, val_preds)\n\nprint(f'Validation AUC:        {auc:.4f}')\nprint(f'Gini stability score:  {score:.4f}')\nprint(f'  Mean weekly Gini:    {mean_gini:.4f}')\nprint(f'  Trend slope:         {slope:.6f}')\nprint(f'  Residual std:        {resid_std:.4f}')\n\nvalid_weeks = [w for w in np.unique(current_val_weeks) if y_va[current_val_weeks == w].sum() > 0]\nplt.figure(figsize=(10, 4))\nplt.plot(valid_weeks, weekly_gini, marker='o', linewidth=1.5)\nplt.axhline(mean_gini, color='red', linestyle='--', label=f'Mean={mean_gini:.3f}')\nplt.xlabel('WEEK_NUM'); plt.ylabel('Gini')\nplt.title('Per-week Gini — improved LightGBM (last fold)')\nplt.legend(); plt.tight_layout(); plt.show()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Feature importance","metadata":{}},{"cell_type":"code","source":"importance = pl.DataFrame({\n    'feature':    stable_features,\n    'importance': eval_model.feature_importances_\n}).sort('importance', descending=True)\nprint(importance.head(20))\n\ntop20 = importance.head(20)\nplt.figure(figsize=(10, 8))\nplt.barh(top20['feature'].to_list()[::-1], top20['importance'].to_list()[::-1])\nplt.xlabel('Importance'); plt.title('Top 20 features — improved LightGBM')\nplt.tight_layout(); plt.show()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. Retrain on full data & save predictions","metadata":{}},{"cell_type":"code","source":"best_n_estimators = eval_model.best_iteration_\nfinal_params = {**tuned_params, 'n_estimators': best_n_estimators}\n\nfinal_model = lgb.LGBMClassifier(**final_params)\nfinal_model.fit(X, y)\n\ntest_preds = final_model.predict_proba(X_test)[:, 1]\n\n# Save raw probabilities for the ensemble (05_ensemble_kaggle.ipynb)\nos.makedirs(OUTPUT, exist_ok=True)\nnp.save(f'{OUTPUT}/lgbm_test_preds.npy', test_preds)\nprint(f'Saved lgbm_test_preds.npy — shape: {test_preds.shape}')\n\n# Standalone submission (Sub 5 solo, optional)\nsubmission = pl.DataFrame({\n    'case_id': test_base['case_id'],\n    'score':   test_preds\n})\nsubmission.write_csv(f'{OUTPUT}/submission.csv')\nprint(f'Saved submission.csv')\nprint(submission.describe())","metadata":{},"outputs":[],"execution_count":null}]}