{"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":50160,"databundleVersionId":7921029}],"dockerImageVersionId":31329,"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":"2026-05-02T14:42:15.532773Z","iopub.execute_input":"2026-05-02T14:42:15.533204Z","iopub.status.idle":"2026-05-02T14:42:17.655235Z","shell.execute_reply.started":"2026-05-02T14:42:15.533179Z","shell.execute_reply":"2026-05-02T14:42:17.65441Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\npath = kagglehub.competition_download('home-credit-credit-risk-model-stability')\n\nprint(\"Path to competition files:\", path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:42:21.861861Z","iopub.execute_input":"2026-05-02T14:42:21.862694Z","iopub.status.idle":"2026-05-02T14:42:23.370872Z","shell.execute_reply.started":"2026-05-02T14:42:21.86266Z","shell.execute_reply":"2026-05-02T14:42:23.370222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm import tqdm\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.impute import SimpleImputer\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import DataLoader, TensorDataset\n\n# Set seeds for reproducibility\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Using device: {device}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:42:26.561568Z","iopub.execute_input":"2026-05-02T14:42:26.561947Z","iopub.status.idle":"2026-05-02T14:42:35.483225Z","shell.execute_reply.started":"2026-05-02T14:42:26.561924Z","shell.execute_reply":"2026-05-02T14:42:35.482307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── SET YOUR DATA PATH HERE ───────────────────────────────────────────────────\nDATA_DIR = '/kaggle/input/competitions/home-credit-credit-risk-model-stability'   \n# ───────────────────────────────────────────────────────────────────────────────\n\ndef load_and_concat(file_patterns, data_dir=DATA_DIR):\n    \"\"\"Load multiple CSV shards and concatenate them.\"\"\"\n    dfs = []\n    for i in range(20):  # try up to 20 shards\n        path = os.path.join(data_dir, f'{file_patterns}_{i}.csv')\n        if os.path.exists(path):\n            dfs.append(pd.read_csv(path))\n    if not dfs:  # single file\n        single = os.path.join(data_dir, f'{file_patterns}.csv')\n        if os.path.exists(single):\n            return pd.read_csv(single)\n        return pd.DataFrame()\n    return pd.concat(dfs, ignore_index=True)\n\n# Load base tables\nprint('Loading train_base...')\ntrain_base = pd.read_csv(os.path.join(DATA_DIR, 'csv_files/train/train_base.csv'))\nprint(f'train_base shape: {train_base.shape}')\n\n# Load depth=0 static tables\nprint('Loading train_static_0...')\nstatic_0   = load_and_concat('csv_files/train/train_static_0')\nprint(f'train_static_0 shape: {static_0.shape}')\n\nprint('Loading train_static_cb_0...')\nstatic_cb  = pd.read_csv(os.path.join(DATA_DIR, 'csv_files/train/train_static_cb_0.csv'))\nprint(f'train_static_cb_0 shape: {static_cb.shape}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:42:41.966263Z","iopub.execute_input":"2026-05-02T14:42:41.967026Z","iopub.status.idle":"2026-05-02T14:43:28.259289Z","shell.execute_reply.started":"2026-05-02T14:42:41.966995Z","shell.execute_reply":"2026-05-02T14:43:28.258301Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# person_1: take first record (num_group1 == 0 = applicant)\nprint('Loading train_person_1...')\nperson_1 = pd.read_csv(os.path.join(DATA_DIR, 'csv_files/train/train_person_1.csv'))\n# Keep only applicant row\nperson_1_agg = person_1[person_1['num_group1'] == 0].drop(columns=['num_group1'])\nprint(f'person_1 (applicant only) shape: {person_1_agg.shape}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:43:31.608453Z","iopub.execute_input":"2026-05-02T14:43:31.609283Z","iopub.status.idle":"2026-05-02T14:43:50.99667Z","shell.execute_reply.started":"2026-05-02T14:43:31.609236Z","shell.execute_reply":"2026-05-02T14:43:50.995911Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Merge all tables on case_id\ndf = train_base.copy()\n\nif not static_0.empty:\n    df = df.merge(static_0,  on='case_id', how='left', suffixes=('', '_s0'))\nif not static_cb.empty:\n    df = df.merge(static_cb, on='case_id', how='left', suffixes=('', '_scb'))\nif not person_1_agg.empty:\n    df = df.merge(person_1_agg, on='case_id', how='left', suffixes=('', '_p1'))\n\nprint(f'Merged dataframe shape: {df.shape}')\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:43:56.284657Z","iopub.execute_input":"2026-05-02T14:43:56.285274Z","iopub.status.idle":"2026-05-02T14:44:14.527554Z","shell.execute_reply.started":"2026-05-02T14:43:56.285246Z","shell.execute_reply":"2026-05-02T14:44:14.526826Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Drop columns with more than 80% missing\nHIGH_MISSING_THRESH = 0.80\ncols_to_drop = missing_pct[missing_pct > HIGH_MISSING_THRESH].index.tolist()\ndf.drop(columns=cols_to_drop, inplace=True)\nprint(f'Dropped {len(cols_to_drop)} columns with >{HIGH_MISSING_THRESH*100:.0f}% missing.')\nprint(f'Remaining columns: {df.shape[1]}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:44:23.639725Z","iopub.execute_input":"2026-05-02T14:44:23.640002Z","iopub.status.idle":"2026-05-02T14:44:25.379662Z","shell.execute_reply.started":"2026-05-02T14:44:23.63998Z","shell.execute_reply":"2026-05-02T14:44:25.378781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Separate IDs / meta / target\nMETA_COLS = ['case_id', 'date_decision', 'WEEK_NUM', 'MONTH', 'target']\nmeta_cols_present = [c for c in META_COLS if c in df.columns]\n\nfeature_cols = [c for c in df.columns if c not in meta_cols_present]\n\nnum_cols = df[feature_cols].select_dtypes(include=[np.number]).columns.tolist()\ncat_cols = df[feature_cols].select_dtypes(include=['object', 'category']).columns.tolist()\n\nprint(f'Numerical features : {len(num_cols)}')\nprint(f'Categorical features: {len(cat_cols)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:44:27.256954Z","iopub.execute_input":"2026-05-02T14:44:27.257699Z","iopub.status.idle":"2026-05-02T14:44:36.516466Z","shell.execute_reply.started":"2026-05-02T14:44:27.257671Z","shell.execute_reply":"2026-05-02T14:44:36.515719Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Take up to 20 numerical features with highest variance for visibility\ntop_var_cols = df[num_cols].var().nlargest(20).index.tolist()\n\nplt.figure(figsize=(12, 10))\ncorr = df[top_var_cols].corr()\nmask = np.triu(np.ones_like(corr, dtype=bool))\nsns.heatmap(corr, mask=mask, annot=False, cmap='coolwarm', center=0,\n            linewidths=0.3, square=True)\nplt.title('Correlation Matrix (top-20 by variance)')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:45:16.847076Z","iopub.execute_input":"2026-05-02T14:45:16.847759Z","iopub.status.idle":"2026-05-02T14:45:20.187444Z","shell.execute_reply.started":"2026-05-02T14:45:16.847726Z","shell.execute_reply":"2026-05-02T14:45:20.18673Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Helper: safe ratio ──────────────────────────────────────────────────────────\ndef safe_ratio(a, b, fill=0.0):\n    \"\"\"Compute a/b safely, replacing division-by-zero with fill.\"\"\"\n    return np.where(b == 0, fill, a / b)\n\n# ── 1. Loan-to-Income Ratio ─────────────────────────────────────────────────────\nfor loan_col in ['credamount_770A', 'amtcredit_838A']:\n    for income_col in ['annualizedincomeapproved_831A', 'incomemonthlygross_1559A']:\n        if loan_col in df.columns and income_col in df.columns:\n            feat_name = f'ratio_{loan_col}__{income_col}'\n            df[feat_name] = safe_ratio(\n                pd.to_numeric(df[loan_col], errors='coerce'),\n                pd.to_numeric(df[income_col], errors='coerce')\n            )\n            print(f'Created: {feat_name}')\n\n# ── 2. Aggregation features ──────────────────────────────────────────────────────\ndpd_cols = [c for c in df.columns if 'dpd' in c.lower() or 'dpdtol' in c.lower()]\nif dpd_cols:\n    # تحويل كل الـ DPD columns لـ numeric أولاً\n    dpd_df = df[dpd_cols].apply(pd.to_numeric, errors='coerce')\n    df['feat_num_dpd_above0'] = (dpd_df > 0).sum(axis=1)\n    df['feat_max_dpd']        = dpd_df.max(axis=1)\n    df['feat_mean_dpd']       = dpd_df.mean(axis=1)\n    print(f'Created DPD aggregation features from {len(dpd_cols)} columns.')\n\n# ── 3. Amount aggregation ────────────────────────────────────────────────────────\namount_cols = [\n    c for c in df.columns\n    if c.endswith('A') and pd.to_numeric(df[c], errors='coerce').notna().any()\n]\nif amount_cols:\n    amt_df = df[amount_cols].apply(pd.to_numeric, errors='coerce')\n    df['feat_sum_amounts']  = amt_df.sum(axis=1)\n    df['feat_mean_amounts'] = amt_df.mean(axis=1)\n    print(f'Created amount aggregation features from {len(amount_cols)} columns.')\n\nprint('\\nFeature engineering done.')\nprint(f'Total columns now: {df.shape[1]}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:45:23.847084Z","iopub.execute_input":"2026-05-02T14:45:23.84778Z","iopub.status.idle":"2026-05-02T14:45:26.70701Z","shell.execute_reply.started":"2026-05-02T14:45:23.847751Z","shell.execute_reply":"2026-05-02T14:45:26.706281Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Refresh column lists after engineering\nfeature_cols = [c for c in df.columns if c not in meta_cols_present]\nnum_cols     = df[feature_cols].select_dtypes(include=[np.number]).columns.tolist()\ncat_cols     = df[feature_cols].select_dtypes(include=['object', 'category']).columns.tolist()\n\n# ── Encode categoricals with LabelEncoder ───────────────────────────────────────\nle = LabelEncoder()\nfor col in cat_cols:\n    df[col] = df[col].astype(str).fillna('MISSING')\n    df[col] = le.fit_transform(df[col])\n\nprint(f'Encoded {len(cat_cols)} categorical columns.')\n\n# ── Impute remaining missing numerics with median ────────────────────────────────\nall_feature_cols = num_cols + cat_cols\nimputer = SimpleImputer(strategy='median')\ndf[all_feature_cols] = imputer.fit_transform(df[all_feature_cols])\n\nprint(f'Imputation done. Remaining NaN: {df[all_feature_cols].isnull().sum().sum()}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:45:35.861172Z","iopub.execute_input":"2026-05-02T14:45:35.862044Z","iopub.status.idle":"2026-05-02T14:46:59.668032Z","shell.execute_reply.started":"2026-05-02T14:45:35.862014Z","shell.execute_reply":"2026-05-02T14:46:59.667132Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build X and y\nX = df[all_feature_cols].values.astype(np.float32)\ny = df['target'].values.astype(np.float32)\n\n# Train / Val / Test split (temporal split using WEEK_NUM if possible)\nif 'WEEK_NUM' in df.columns:\n    max_week = df['WEEK_NUM'].max()\n    val_week_start  = int(max_week * 0.70)\n    test_week_start = int(max_week * 0.85)\n\n    train_idx = df['WEEK_NUM'] <  val_week_start\n    val_idx   = (df['WEEK_NUM'] >= val_week_start)  & (df['WEEK_NUM'] < test_week_start)\n    test_idx  = df['WEEK_NUM'] >= test_week_start\n\n    X_train, y_train = X[train_idx.values], y[train_idx.values]\n    X_val,   y_val   = X[val_idx.values],   y[val_idx.values]\n    X_test,  y_test  = X[test_idx.values],  y[test_idx.values]\n    week_num_test    = df.loc[test_idx, 'WEEK_NUM'].values\n    print('Using temporal split based on WEEK_NUM')\nelse:\n    X_train, X_tmp, y_train, y_tmp = train_test_split(X, y, test_size=0.30, random_state=SEED, stratify=y)\n    X_val,   X_test, y_val, y_test = train_test_split(X_tmp, y_tmp, test_size=0.50, random_state=SEED, stratify=y_tmp)\n    week_num_test = None\n    print('Using random stratified split (WEEK_NUM not found)')\n\nprint(f'Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}')\n\n# Scale\nscaler = StandardScaler()\nX_train = scaler.fit_transform(X_train)\nX_val   = scaler.transform(X_val)\nX_test  = scaler.transform(X_test)\n\n# ── Compute class weight for imbalance ──────────────────────────────────────────\npos_weight = (y_train == 0).sum() / (y_train == 1).sum()\nprint(f'Positive class weight (for BCEWithLogitsLoss): {pos_weight:.2f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:46:59.669665Z","iopub.execute_input":"2026-05-02T14:46:59.670045Z","iopub.status.idle":"2026-05-02T14:47:07.136836Z","shell.execute_reply.started":"2026-05-02T14:46:59.670019Z","shell.execute_reply":"2026-05-02T14:47:07.136064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert to PyTorch tensors\ndef to_tensors(X, y):\n    return TensorDataset(torch.tensor(X, dtype=torch.float32),\n                         torch.tensor(y, dtype=torch.float32))\n\nBATCH_SIZE = 2048\n\ntrain_loader = DataLoader(to_tensors(X_train, y_train), batch_size=BATCH_SIZE, shuffle=True,  num_workers=0)\nval_loader   = DataLoader(to_tensors(X_val,   y_val),   batch_size=BATCH_SIZE, shuffle=False, num_workers=0)\ntest_loader  = DataLoader(to_tensors(X_test,  y_test),  batch_size=BATCH_SIZE, shuffle=False, num_workers=0)\n\nprint('DataLoaders ready.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:48:04.036962Z","iopub.execute_input":"2026-05-02T14:48:04.037815Z","iopub.status.idle":"2026-05-02T14:48:04.513705Z","shell.execute_reply.started":"2026-05-02T14:48:04.037781Z","shell.execute_reply":"2026-05-02T14:48:04.512869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CreditRiskNN(nn.Module):\n    \"\"\"\n    Feedforward Neural Network for binary classification (loan default).\n    - Input layer  : all numerical + encoded categorical features\n    - Hidden layers: configurable depth and width with BatchNorm + Dropout\n    - Output layer : single neuron (logit) → sigmoid for probability\n    \"\"\"\n    def __init__(self, input_dim, hidden_sizes=[512, 256, 128],\n                 dropout_rate=0.3, l2_reg=1e-4):\n        super().__init__()\n        layers = []\n        prev_size = input_dim\n        for h in hidden_sizes:\n            layers += [\n                nn.Linear(prev_size, h),\n                nn.BatchNorm1d(h),\n                nn.ReLU(),\n                nn.Dropout(p=dropout_rate),\n            ]\n            prev_size = h\n        layers.append(nn.Linear(prev_size, 1))   # output neuron (logit)\n        self.net = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return self.net(x).squeeze(1)  # shape: (batch,)\n\n\nINPUT_DIM = X_train.shape[1]\nmodel = CreditRiskNN(input_dim=INPUT_DIM, hidden_sizes=[512, 256, 128], dropout_rate=0.3)\nmodel = model.to(device)\nprint(model)\nprint(f'\\nTotal parameters: {sum(p.numel() for p in model.parameters()):,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:47:13.546788Z","iopub.execute_input":"2026-05-02T14:47:13.547359Z","iopub.status.idle":"2026-05-02T14:47:13.95379Z","shell.execute_reply.started":"2026-05-02T14:47:13.547329Z","shell.execute_reply":"2026-05-02T14:47:13.952961Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Evaluation helpers ──────────────────────────────────────────────────────────\n\ndef gini_score(y_true, y_prob):\n    \"\"\"Gini coefficient = 2*AUC - 1.\"\"\"\n    auc = roc_auc_score(y_true, y_prob)\n    return 2 * auc - 1\n\n\ndef predict_proba(model, loader):\n    \"\"\"Return numpy array of predicted probabilities.\"\"\"\n    model.eval()\n    preds, labels = [], []\n    with torch.no_grad():\n        for Xb, yb in loader:\n            Xb = Xb.to(device)\n            logits = model(Xb)\n            probs  = torch.sigmoid(logits).cpu().numpy()\n            preds.append(probs)\n            labels.append(yb.numpy())\n    return np.concatenate(preds), np.concatenate(labels)\n\n\ndef train_one_epoch(model, loader, criterion, optimizer):\n    model.train()\n    total_loss = 0\n    for Xb, yb in loader:\n        Xb, yb = Xb.to(device), yb.to(device)\n        optimizer.zero_grad()\n        logits = model(Xb)\n        loss   = criterion(logits, yb)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item() * len(yb)\n    return total_loss / len(loader.dataset)\n\n\ndef train_model(optimizer_name, lr=1e-3, epochs=30, patience=5):\n    \"\"\"\n    Train a fresh model with the chosen optimizer.\n    Returns history dict and the best model state.\n    \"\"\"\n    model = CreditRiskNN(input_dim=INPUT_DIM, hidden_sizes=[512, 256, 128], dropout_rate=0.3).to(device)\n\n    # Loss with class weighting for imbalance\n    criterion = nn.BCEWithLogitsLoss(pos_weight=torch.tensor(pos_weight, dtype=torch.float32).to(device))\n\n    if optimizer_name == 'Adam':\n        optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4)\n    elif optimizer_name == 'SGD':\n        optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=1e-4)\n    elif optimizer_name == 'RMSprop':\n        optimizer = optim.RMSprop(model.parameters(), lr=lr, weight_decay=1e-4)\n\n    # Learning rate scheduler: reduce on plateau\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=3)\n\n    history = {'train_loss': [], 'val_gini': [], 'val_auc': []}\n    best_gini      = -np.inf\n    best_state     = None\n    no_improve     = 0\n\n    for epoch in range(1, epochs + 1):\n        train_loss = train_one_epoch(model, train_loader, criterion, optimizer)\n        val_probs, val_labels = predict_proba(model, val_loader)\n        val_auc  = roc_auc_score(val_labels, val_probs)\n        val_gini = 2 * val_auc - 1\n\n        history['train_loss'].append(train_loss)\n        history['val_gini'].append(val_gini)\n        history['val_auc'].append(val_auc)\n\n        scheduler.step(val_gini)\n\n        if val_gini > best_gini:\n            best_gini  = val_gini\n            best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}\n            no_improve = 0\n        else:\n            no_improve += 1\n\n        print(f'[{optimizer_name}] Epoch {epoch:3d} | Loss: {train_loss:.4f} | Val AUC: {val_auc:.4f} | Val Gini: {val_gini:.4f}')\n\n        if no_improve >= patience:\n            print(f'Early stopping at epoch {epoch} (patience={patience})')\n            break\n\n    # Restore best model\n    model.load_state_dict(best_state)\n    return model, history, best_gini","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:48:11.485686Z","iopub.execute_input":"2026-05-02T14:48:11.485963Z","iopub.status.idle":"2026-05-02T14:48:11.498203Z","shell.execute_reply.started":"2026-05-02T14:48:11.485941Z","shell.execute_reply":"2026-05-02T14:48:11.497556Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Train with all three optimizers ────────────────────────────────────────────\nEPOCHS   = 30\nPATIENCE = 5\n\nresults = {}\n\nfor opt_name, lr in [('Adam', 1e-3), ('SGD', 1e-2), ('RMSprop', 1e-3)]:\n    print(f'\\n{\"=\"*60}')\n    print(f'  Training with optimizer: {opt_name}')\n    print(f'{\"=\"*60}')\n    model_trained, history, best_gini = train_model(opt_name, lr=lr, epochs=EPOCHS, patience=PATIENCE)\n    results[opt_name] = {'model': model_trained, 'history': history, 'best_gini': best_gini}\n    print(f'Best Val Gini [{opt_name}]: {best_gini:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:48:12.899295Z","iopub.execute_input":"2026-05-02T14:48:12.900037Z","iopub.status.idle":"2026-05-02T14:56:16.545761Z","shell.execute_reply.started":"2026-05-02T14:48:12.900004Z","shell.execute_reply":"2026-05-02T14:56:16.54481Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\ncolors = {'Adam': 'steelblue', 'SGD': 'tomato', 'RMSprop': 'seagreen'}\n\nfor opt_name, res in results.items():\n    hist = res['history']\n    axes[0].plot(hist['train_loss'], label=opt_name, color=colors[opt_name])\n    axes[1].plot(hist['val_gini'],   label=opt_name, color=colors[opt_name])\n\naxes[0].set_title('Training Loss per Epoch')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('BCE Loss')\naxes[0].legend()\n\naxes[1].set_title('Validation Gini per Epoch')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Gini Score')\naxes[1].legend()\n\nplt.tight_layout()\nplt.show()\n\n# Summary table\nsummary = pd.DataFrame({\n    'Optimizer': list(results.keys()),\n    'Best Val Gini': [v['best_gini'] for v in results.values()],\n    'Epochs until best': [np.argmax(v['history']['val_gini']) + 1 for v in results.values()]\n})\nprint('\\n--- Optimizer Comparison ---')\nprint(summary.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:57:11.386069Z","iopub.execute_input":"2026-05-02T14:57:11.386821Z","iopub.status.idle":"2026-05-02T14:57:11.730421Z","shell.execute_reply.started":"2026-05-02T14:57:11.386791Z","shell.execute_reply":"2026-05-02T14:57:11.729576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CreditRiskNN_L1(nn.Module):\n    \"\"\"NN variant that supports explicit L1 regularization.\"\"\"\n    def __init__(self, input_dim, hidden_sizes=[512, 256, 128],\n                 dropout_rate=0.0):\n        super().__init__()\n        layers = []\n        prev = input_dim\n        for h in hidden_sizes:\n            layers += [nn.Linear(prev, h), nn.BatchNorm1d(h), nn.ReLU(), nn.Dropout(dropout_rate)]\n            prev = h\n        layers.append(nn.Linear(prev, 1))\n        self.net = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return self.net(x).squeeze(1)\n\n    def l1_loss(self):\n        return sum(p.abs().sum() for p in self.parameters())\n\n\ndef train_regularization_variant(config_name, dropout=0.0, l2=0.0, l1_lambda=0.0, epochs=20, patience=5):\n    model = CreditRiskNN_L1(INPUT_DIM, dropout_rate=dropout).to(device)\n    criterion = nn.BCEWithLogitsLoss(pos_weight=torch.tensor(pos_weight).to(device))\n    optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=l2)\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=3)\n\n    history = {'train_loss': [], 'val_gini': []}\n    best_gini, best_state, no_improve = -np.inf, None, 0\n\n    for epoch in range(1, epochs + 1):\n        model.train()\n        total_loss = 0\n        for Xb, yb in train_loader:\n            Xb, yb = Xb.to(device), yb.to(device)\n            optimizer.zero_grad()\n            logits = model(Xb)\n            loss   = criterion(logits, yb)\n            if l1_lambda > 0:\n                loss = loss + l1_lambda * model.l1_loss()\n            loss.backward()\n            optimizer.step()\n            total_loss += loss.item() * len(yb)\n\n        val_probs, val_labels = predict_proba(model, val_loader)\n        val_gini = 2 * roc_auc_score(val_labels, val_probs) - 1\n        history['train_loss'].append(total_loss / len(train_loader.dataset))\n        history['val_gini'].append(val_gini)\n        scheduler.step(val_gini)\n\n        if val_gini > best_gini:\n            best_gini  = val_gini\n            best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}\n            no_improve = 0\n        else:\n            no_improve += 1\n        if no_improve >= patience:\n            print(f'[{config_name}] Early stop at epoch {epoch}')\n            break\n\n    model.load_state_dict(best_state)\n    return model, history, best_gini\n\n\n# ── Run ablation ────────────────────────────────────────────────────────────────\nablation_configs = [\n    {'name': 'No Regularization', 'dropout': 0.0, 'l2': 0.0,    'l1_lambda': 0.0},\n    {'name': 'L2 Only',           'dropout': 0.0, 'l2': 1e-4,   'l1_lambda': 0.0},\n    {'name': 'L1 Only',           'dropout': 0.0, 'l2': 0.0,    'l1_lambda': 1e-5},\n    {'name': 'Dropout Only',      'dropout': 0.3, 'l2': 0.0,    'l1_lambda': 0.0},\n    {'name': 'All Combined',      'dropout': 0.3, 'l2': 1e-4,   'l1_lambda': 1e-5},\n]\n\nablation_results = {}\nfor cfg in ablation_configs:\n    print(f'\\nTraining: {cfg[\"name\"]}')\n    m, h, g = train_regularization_variant(\n        cfg['name'], dropout=cfg['dropout'],\n        l2=cfg['l2'], l1_lambda=cfg['l1_lambda'], epochs=20, patience=5\n    )\n    ablation_results[cfg['name']] = {'model': m, 'history': h, 'best_gini': g}\n    print(f'  → Best Val Gini: {g:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T14:57:14.773791Z","iopub.execute_input":"2026-05-02T14:57:14.774418Z","iopub.status.idle":"2026-05-02T15:08:46.740325Z","shell.execute_reply.started":"2026-05-02T14:57:14.774388Z","shell.execute_reply":"2026-05-02T15:08:46.738819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Plot ablation results\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\ncmap = plt.cm.tab10\n\nfor i, (name, res) in enumerate(ablation_results.items()):\n    c = cmap(i)\n    axes[0].plot(res['history']['train_loss'], label=name, color=c)\n    axes[1].plot(res['history']['val_gini'],   label=name, color=c)\n\naxes[0].set_title('Training Loss — Regularization Ablation')\naxes[0].set_xlabel('Epoch')\naxes[0].legend(fontsize=8)\naxes[1].set_title('Validation Gini — Regularization Ablation')\naxes[1].set_xlabel('Epoch')\naxes[1].legend(fontsize=8)\nplt.tight_layout()\nplt.show()\n\nabl_summary = pd.DataFrame([\n    {'Config': k, 'Best Val Gini': v['best_gini']}\n    for k, v in ablation_results.items()\n]).sort_values('Best Val Gini', ascending=False)\nprint(abl_summary.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T15:08:52.379294Z","iopub.execute_input":"2026-05-02T15:08:52.380054Z","iopub.status.idle":"2026-05-02T15:08:52.946819Z","shell.execute_reply.started":"2026-05-02T15:08:52.380024Z","shell.execute_reply":"2026-05-02T15:08:52.94595Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Select best optimizer model\nbest_opt = max(results, key=lambda k: results[k]['best_gini'])\nbest_model = results[best_opt]['model']\nprint(f'Best optimizer: {best_opt} (Val Gini: {results[best_opt][\"best_gini\"]:.4f})')\n\n# Test set evaluation\ntest_probs, test_labels = predict_proba(best_model, test_loader)\ntest_auc  = roc_auc_score(test_labels, test_probs)\ntest_gini = 2 * test_auc - 1\n\nprint(f'\\n=== TEST SET RESULTS ===')\nprint(f'AUC  : {test_auc:.4f}')\nprint(f'Gini : {test_gini:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T15:08:55.96744Z","iopub.execute_input":"2026-05-02T15:08:55.968211Z","iopub.status.idle":"2026-05-02T15:08:57.168004Z","shell.execute_reply.started":"2026-05-02T15:08:55.968179Z","shell.execute_reply":"2026-05-02T15:08:57.167143Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if week_num_test is not None:\n    stability_df = pd.DataFrame({\n        'WEEK_NUM': week_num_test,\n        'pred':     test_probs,\n        'target':   test_labels\n    })\n\n    weekly_gini = (\n        stability_df.groupby('WEEK_NUM')\n        .apply(lambda g: 2 * roc_auc_score(g['target'], g['pred']) - 1\n               if g['target'].nunique() > 1 else np.nan)\n        .dropna()\n        .rename('Gini')\n    )\n\n    plt.figure(figsize=(12, 4))\n    weekly_gini.plot(marker='o', color='steelblue')\n    plt.axhline(weekly_gini.mean(), linestyle='--', color='tomato', label=f'Mean Gini = {weekly_gini.mean():.3f}')\n    plt.title('Temporal Stability: Gini per Week (Test Set)')\n    plt.xlabel('WEEK_NUM')\n    plt.ylabel('Gini Score')\n    plt.legend()\n    plt.tight_layout()\n    plt.show()\n\n    print(f'Mean weekly Gini : {weekly_gini.mean():.4f}')\n    print(f'Std  weekly Gini : {weekly_gini.std():.4f}  (lower = more stable)')\nelse:\n    print('WEEK_NUM not available — temporal stability plot skipped.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T15:09:00.01225Z","iopub.execute_input":"2026-05-02T15:09:00.01303Z","iopub.status.idle":"2026-05-02T15:09:00.294697Z","shell.execute_reply.started":"2026-05-02T15:09:00.012998Z","shell.execute_reply":"2026-05-02T15:09:00.293918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import roc_curve\n\nfpr, tpr, _ = roc_curve(test_labels, test_probs)\n\nplt.figure(figsize=(6, 5))\nplt.plot(fpr, tpr, color='steelblue', label=f'AUC = {test_auc:.4f}')\nplt.plot([0, 1], [0, 1], 'k--', alpha=0.4)\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC Curve — Test Set')\nplt.legend()\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T15:09:02.99887Z","iopub.execute_input":"2026-05-02T15:09:02.999633Z","iopub.status.idle":"2026-05-02T15:09:03.181534Z","shell.execute_reply.started":"2026-05-02T15:09:02.9996Z","shell.execute_reply":"2026-05-02T15:09:03.180723Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save the best model weights\ntorch.save(best_model.state_dict(), 'best_credit_risk_nn.pt')\nprint('Model saved to best_credit_risk_nn.pt')\nprint('Done! ✓')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T15:09:03.843384Z","iopub.execute_input":"2026-05-02T15:09:03.843715Z","iopub.status.idle":"2026-05-02T15:09:03.878223Z","shell.execute_reply.started":"2026-05-02T15:09:03.84369Z","shell.execute_reply":"2026-05-02T15:09:03.877569Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print('='*55)\nprint('            FINAL SUMMARY')\nprint('='*55)\nprint(f'Input features          : {INPUT_DIM}')\nprint(f'Architecture            : FC(512) → FC(256) → FC(128) → FC(1)')\nprint(f'Regularization          : BatchNorm + Dropout(0.3) + L2 + Early Stopping')\nprint(f'Imbalance handling      : BCEWithLogitsLoss(pos_weight={pos_weight:.1f})')\nprint('\\n--- Optimizer Comparison (Val Gini) ---')\nfor k, v in results.items():\n    print(f'  {k:10s}: {v[\"best_gini\"]:.4f}')\nprint(f'\\nBest optimizer          : {best_opt}')\nprint(f'Test AUC                : {test_auc:.4f}')\nprint(f'Test Gini               : {test_gini:.4f}')\nprint('='*55)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T15:09:05.841754Z","iopub.execute_input":"2026-05-02T15:09:05.84203Z","iopub.status.idle":"2026-05-02T15:09:05.847976Z","shell.execute_reply.started":"2026-05-02T15:09:05.842007Z","shell.execute_reply":"2026-05-02T15:09:05.84719Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}