{"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":"gpu","dataSources":[{"sourceId":114135,"databundleVersionId":14604229,"sourceType":"competition"},{"sourceId":289224187,"sourceType":"kernelVersion"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport logging\nimport warnings\nimport os\nfrom functools import partial\n\nimport jax\nimport jax.numpy as jnp\nfrom jax import random, jit, vmap\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import f1_score, accuracy_score\nfrom sklearn.utils.class_weight import compute_class_weight\nimport lightgbm as lgb\n\n# -------------------------\n# CONFIG & LOGGING\n# -------------------------\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s: %(message)s\")\nlogger = logging.getLogger(\"reversal_pipeline\")\n\nTRAIN_PATH = \"/kaggle/input/detecting-reversal-points-in-us-equities/new_comptetition_data/train.csv\"\nTEST_PATH = \"/kaggle/input/detecting-reversal-points-in-us-equities/new_comptetition_data/test.csv\"\nSAMPLE_SUB_PATH = \"/kaggle/input/detecting-reversal-points-in-us-equities/new_comptetition_data/sample_submission.csv\"\nGA_FEATURE_FILE = \"/kaggle/input/genetic-algo-jax-feature-selection/final_selected_features.csv\"\n\n# -------------------------\n# JAX SUBSPACE SOLVER (Kept but not required for LGBM)\n# -------------------------\n@partial(jit, static_argnums=(4, 5))\ndef solve_subspace_jit(XtX_full, Xty_full, mask, alpha, num_classes, max_cap):\n    sort_idx = jnp.argsort(mask, descending=True)\n    active_indices = sort_idx[:max_cap]\n    is_active = mask[active_indices]\n\n    XtX_sub = XtX_full[active_indices][:, active_indices]\n    Xty_sub = Xty_full[active_indices]\n\n    sub_mask_mat = is_active[:, None] * is_active[None, :]\n    XtX_active = XtX_sub * sub_mask_mat\n\n    diag_reg = jnp.where(is_active > 0, alpha, 1.0)\n    XtX_reg = XtX_active + jnp.diag(diag_reg) + (jnp.eye(max_cap) * (1.0 - is_active[:, None]))\n\n    Xty_active = Xty_sub * is_active[:, None]\n    W_sub = jnp.linalg.solve(XtX_reg, Xty_active)\n\n    return W_sub, active_indices\n\n# -------------------------\n# DATA PREPARATION & ROBUST MAPPING\n# -------------------------\ndef load_and_prepare_data():\n    logger.info(\"Loading data...\")\n    train = pd.read_csv(TRAIN_PATH, low_memory=False)\n    test = pd.read_csv(TEST_PATH, low_memory=False)\n\n    # Robust mapping function\n    def robust_map(x):\n        if pd.isna(x):\n            return 'None'\n        val = str(x).strip().upper()\n        # Map explicit patterns to H or L; keep unknowns as 'None'\n        if val in ('H', 'HH', 'LH', 'HIGHLIGHT', 'HIGH'):\n            return 'H'\n        if val in ('L', 'LL', 'HL', 'LOW'):\n            return 'L'\n        if val in ('NONE', 'N', ''):\n            return 'None'\n        # fallback: if contains H or L prefer those\n        if 'H' in val and 'L' not in val:\n            return 'H'\n        if 'L' in val and 'H' not in val:\n            return 'L'\n        return 'None'\n\n    logger.info(\"Mapping class labels...\")\n    y_raw = train['class_label'].apply(robust_map)\n\n    # Diagnostics\n    logger.info(f\"Unique classes identified: {y_raw.unique().tolist()}\")\n    logger.info(f\"Class distribution:\\n{y_raw.value_counts(dropna=False)}\")\n\n    if y_raw.nunique() < 2:\n        raise ValueError(f\"Error: Only one class found: {y_raw.unique().tolist()}\")\n\n    le = LabelEncoder()\n    y_encoded = le.fit_transform(y_raw)\n\n    logger.info(f\"LabelEncoder classes (order): {list(le.classes_)}\")\n\n    # Identify feature columns (excluding metadata)\n    meta_cols = ['id', 'train_id', 'Unnamed: 0', 'ticker_id', 't', 'class_label']\n    all_numeric_features = [col for col in train.columns if col not in meta_cols]\n\n    # Basic sanity\n    logger.info(f\"Found {len(all_numeric_features)} candidate features.\")\n\n    return train, test, all_numeric_features, y_encoded, le\n\n# Execute preparation\ntrain_df, test_df, all_feats, y_encoded, le = load_and_prepare_data()\nn_classes = len(le.classes_)\nlogger.info(f\"n_classes = {n_classes}\")\n\n# -------------------------\n# FEATURE SELECTION: USE ALL AVAILABLE FEATURES (no GA)\n# -------------------------\nlogger.info(\"Using all available features (no GA selection).\")\nselected_features = all_feats.copy()\n\n# Ensure consistent column order and existence in both train/test\nselected_features = [f for f in selected_features if f in train_df.columns and f in test_df.columns]\nlogger.info(f\"Using {len(selected_features)} features.\")\n\n\n# Ensure consistent column order and existence\nmissing_in_train = [f for f in selected_features if f not in train_df.columns]\nmissing_in_test = [f for f in selected_features if f not in test_df.columns]\nif missing_in_train or missing_in_test:\n    logger.warning(f\"Missing features in train: {missing_in_train}\")\n    logger.warning(f\"Missing features in test: {missing_in_test}\")\n    # fallback to intersection\n    selected_features = [f for f in selected_features if f in train_df.columns and f in test_df.columns]\n    logger.info(f\"Using {len(selected_features)} features after intersection.\")\n\n# Imputation strategy: mean + missingness indicator\ndef prepare_matrix(df, features):\n    X = df[features].copy()\n    X_missing = X.isna().astype(np.uint8).add_suffix(\"_missing\")\n    X_imputed = X.fillna(X.mean())\n    X_final = pd.concat([X_imputed, X_missing], axis=1)\n    return X_final\n\nX = prepare_matrix(train_df, selected_features)\nX_test = prepare_matrix(test_df, selected_features)\n\nlogger.info(f\"X shape: {X.shape}, X_test shape: {X_test.shape}\")\n\n# Optional scaling (commented out; enable if helpful)\n# scaler = StandardScaler()\n# X[X.columns] = scaler.fit_transform(X)\n# X_test[X_test.columns] = scaler.transform(X_test)\n\n# -------------------------\n# LIGHTGBM TRAINING (Stratified 5-Fold) with diagnostics\n# -------------------------\nskf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\noof_preds = np.zeros((X.shape[0], n_classes))\ntest_oof_preds = np.zeros((X_test.shape[0], n_classes))\n\n# Compute class weights for balanced learning\ncw = compute_class_weight(class_weight='balanced', classes=np.unique(y_encoded), y=y_encoded)\nclass_weight_dict = {int(cls): float(w) for cls, w in zip(np.unique(y_encoded), cw)}\nlogger.info(f\"class_weight_dict: {class_weight_dict}\")\n\ncompute_fold_f1s = []\n\nfor fold, (train_idx, val_idx) in enumerate(skf.split(X, y_encoded)):\n    logger.info(f\"--- Training Fold {fold+1} ---\")\n    X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx]\n    y_tr, y_val = y_encoded[train_idx], y_encoded[val_idx]\n\n    # Optional simple upsampling of minority classes in training fold (disabled by default)\n    UPSAMPLE_MINORITY = False\n    if UPSAMPLE_MINORITY:\n        df_tr = pd.concat([X_tr.reset_index(drop=True), pd.Series(y_tr, name='target')], axis=1)\n        counts = df_tr['target'].value_counts()\n        max_count = counts.max()\n        parts = []\n        for cls, cnt in counts.items():\n            part = df_tr[df_tr['target'] == cls]\n            if cnt < max_count:\n                part = part.sample(max_count, replace=True, random_state=42)\n            parts.append(part)\n        df_tr_bal = pd.concat(parts).sample(frac=1, random_state=42).reset_index(drop=True)\n        y_tr = df_tr_bal['target'].values\n        X_tr = df_tr_bal.drop(columns=['target'])\n\n    objective_type = 'multiclass' if n_classes > 2 else 'binary'\n\n    # For multiclass, do not pass num_class for binary; let LGBM infer\n    model = lgb.LGBMClassifier(\n        objective=objective_type,\n        num_class=n_classes if n_classes > 2 else None,\n        learning_rate=0.03,\n        n_estimators=1200,\n        class_weight=class_weight_dict,\n        random_state=42,\n        importance_type='gain',\n        verbosity=-1\n    )\n\n    model.fit(\n        X_tr, y_tr,\n        eval_set=[(X_val, y_val)],\n        eval_metric='multi_logloss' if n_classes > 2 else 'binary_logloss',\n        callbacks=[\n            lgb.early_stopping(stopping_rounds=100),\n            lgb.log_evaluation(period=200)\n        ],\n    )\n\n    val_proba = model.predict_proba(X_val)\n    logger.info(f\"val_proba shape: {val_proba.shape}\")\n\n    # Ensure shape matches\n    if val_proba.ndim == 1:\n        # binary case returning single column of proba for positive class\n        val_proba = np.vstack([1 - val_proba, val_proba]).T\n\n    oof_preds[val_idx] = val_proba\n    test_proba = model.predict_proba(X_test)\n    if test_proba.ndim == 1:\n        test_proba = np.vstack([1 - test_proba, test_proba]).T\n    test_oof_preds += test_proba / skf.n_splits\n\n    val_preds = np.argmax(val_proba, axis=1)\n    fold_f1 = f1_score(y_val, val_preds, average='macro')\n    compute_fold_f1s.append(fold_f1)\n    logger.info(f\"Fold {fold+1} macro-F1: {fold_f1:.6f}\")\n\n# Overall OOF F1\noof_preds_labels = np.argmax(oof_preds, axis=1)\noof_macro_f1 = f1_score(y_encoded, oof_preds_labels, average='macro')\nlogger.info(f\"OOF macro-F1: {oof_macro_f1:.6f}\")\nlogger.info(f\"Per-fold macro-F1s: {compute_fold_f1s}\")\n\n# -------------------------\n# SUBMISSION & MANUAL OVERRIDES (by sample ID)\n# -------------------------\nfinal_labels = le.inverse_transform(np.argmax(test_oof_preds, axis=1))\n\nsubmission = pd.read_csv(SAMPLE_SUB_PATH)\n\n# Validate sample submission has expected id column\nif 'id' not in submission.columns:\n    logger.warning(\"sample_submission has no 'id' column; manual overrides by id won't be applied.\")\nsubmission['class_label'] = final_labels\n\n# If you need manual overrides, use sample IDs here (example keys must be sample ids, not indices)\n# Example: overrides_by_id = {'sample_0001': 'L', 'sample_0758': 'H'}\noverrides_by_id = {}  # populate with actual sample ids if needed\nif overrides_by_id:\n    for sample_id, lbl in overrides_by_id.items():\n        submission.loc[submission['id'] == sample_id, 'class_label'] = lbl\n    logger.info(f\"Applied {len(overrides_by_id)} manual overrides by sample id.\")\n\nsubmission.to_csv(\"submission.csv\", index=False)\nlogger.info(\"Pipeline complete. 'submission.csv' generated.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T12:09:16.078701Z","iopub.execute_input":"2025-12-30T12:09:16.079189Z","iopub.status.idle":"2025-12-30T12:14:21.321575Z","shell.execute_reply.started":"2025-12-30T12:09:16.079166Z","shell.execute_reply":"2025-12-30T12:14:21.320966Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T12:03:33.215481Z","iopub.execute_input":"2025-12-30T12:03:33.216388Z","iopub.status.idle":"2025-12-30T12:03:33.223200Z","shell.execute_reply.started":"2025-12-30T12:03:33.216357Z","shell.execute_reply":"2025-12-30T12:03:33.222436Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.shape, submission.isna().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-30T12:03:41.888519Z","iopub.execute_input":"2025-12-30T12:03:41.889236Z","iopub.status.idle":"2025-12-30T12:03:41.894879Z","shell.execute_reply.started":"2025-12-30T12:03:41.889208Z","shell.execute_reply":"2025-12-30T12:03:41.894105Z"}},"outputs":[],"execution_count":null}]}