{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CMI 2025: Single LightGBM Model [Train]\n\nThis notebook is **Part 1** of a two-notebook pipeline. Its sole purpose is to perform all the heavy data processing and model training, which takes longer than the submission environment's 15-minute startup limit.\n\nThe resulting artifacts from this notebook must be used as the input for the corresponding inference notebook.\n\n**Inference Notebook is here:**\n- [CMI 2025: Single LightGBM Model [Infer]](https://www.kaggle.com/code/masayakawamata/cmi2025-single-lgbm-infer/)\n\n## Workflow\n\n1.  **Load Data**: Reads the full `train.csv` file using Polars.\n2.  **Feature Engineering**: Creates statistical features (`mean`, `std`, `max`, `min`) for all sensor columns (`acc_*`, `rot_*`, `thm_*`, `tof_*`) across each sequence.\n3.  **Train Model**: Trains a single LightGBM classifier on 100% of the training data using a set of pre-tuned hyperparameters.\n4.  **Save Artifacts**: Saves the following essential files to be used for inference:\n    - `final_model.pkl`: The trained LightGBM model object.\n    - `label_encoder.pkl`: The fitted `LabelEncoder` for converting class predictions back to gesture names.\n    - `feature_list.txt`: The exact list and order of features the model was trained on.","metadata":{}},{"cell_type":"code","source":"import polars as pl, pandas as pd, numpy as np\nimport lightgbm as lgb\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import f1_score\nimport joblib\nimport warnings\n\n# Suppress warnings for cleaner output\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\n\n# --- 1. Define Constants and Helper Functions ---\n\n# The best hyperparameters found by Optuna\nBEST_PARAMS = {\n    'objective': 'multiclass',\n    'class_weight': 'balanced',\n    'n_estimators': 10_000,\n    'random_state': 42,\n    'n_jobs': -1,\n    'verbosity': -1,\n    'learning_rate': 0.0682511123410753,\n    'num_leaves': 285,\n    'max_depth': 11,\n    'reg_alpha': 0.015067299011315672,\n    'reg_lambda': 0.408041751307428,\n    'colsample_bytree': 0.4028382627354783,\n    'subsample': 0.6873886039232031\n}\n\ndef create_advanced_imu_features(df: pl.DataFrame) -> pl.DataFrame:\n    \"\"\"\n    Generates advanced statistical and phase-approximated features \n    from IMU data ONLY.\n    \"\"\"\n    imu_cols = [col for col in df.columns if 'acc_' in col or 'rot_' in col]\n    aggs = []\n    \n    # 1. Whole-sequence statistical features\n    for col in imu_cols:\n        aggs.extend([\n            pl.mean(col).alias(f'{col}_mean'),\n            pl.std(col).alias(f'{col}_std'),\n            pl.max(col).alias(f'{col}_max'),\n            pl.min(col).alias(f'{col}_min'),\n            pl.quantile(col, 0.25).alias(f'{col}_q25'),\n            pl.quantile(col, 0.75).alias(f'{col}_q75'),\n        ])\n\n    # 2. Difference features\n    for col in imu_cols:\n        aggs.extend([\n            (pl.col(col).diff().fill_null(0)).mean().alias(f'{col}_diff_mean'),\n            (pl.col(col).diff().fill_null(0)).std().alias(f'{col}_diff_std'),\n        ])\n        \n    # 3. Phase-approximated features\n    for part_name, part_expr in [\n        ('first_30pct', pl.col('sequence_counter') < pl.max('sequence_counter') * 0.3),\n        ('middle_40pct', (pl.col('sequence_counter') >= pl.max('sequence_counter') * 0.3) & (pl.col('sequence_counter') < pl.max('sequence_counter') * 0.7)),\n        ('last_30pct', pl.col('sequence_counter') >= pl.max('sequence_counter') * 0.7),\n    ]:\n        for col in imu_cols:\n            aggs.extend([\n                (pl.when(part_expr).then(pl.col(col))).mean().alias(f'{col}_mean_{part_name}'),\n                (pl.when(part_expr).then(pl.col(col))).std().alias(f'{col}_std_{part_name}'),\n            ])\n\n    feature_df = df.group_by('sequence_id').agg(aggs).fill_null(0)\n    return feature_df\n\ndef competition_metric(y_true, y_pred, sequence_type_map, le):\n    \"\"\"Calculates the official competition metric.\"\"\"\n    y_true_str = le.inverse_transform(y_true)\n    y_pred_str = le.inverse_transform(y_pred)\n    true_df = pd.DataFrame({'gesture': y_true_str})\n    true_df['sequence_type'] = true_df['gesture'].map(sequence_type_map)\n    pred_df = pd.DataFrame({'gesture': y_pred_str})\n    pred_df['sequence_type'] = pred_df['gesture'].map(sequence_type_map)\n    binary_f1 = f1_score(true_df['sequence_type'], pred_df['sequence_type'], pos_label='Target', zero_division=0)\n    true_df.loc[true_df['sequence_type'] == 'Non-Target', 'gesture'] = 'non_target'\n    pred_df.loc[pred_df['sequence_type'] == 'Non-Target', 'gesture'] = 'non_target'\n    all_labels = np.union1d(true_df['gesture'].unique(), pred_df['gesture'].unique())\n    macro_f1 = f1_score(true_df['gesture'], pred_df['gesture'], average='macro', labels=all_labels, zero_division=0)\n    return (binary_f1 + macro_f1) / 2\n\n# --- 2. Data Loading and Feature Creation ---\nprint(\"Loading training data and creating advanced IMU features...\")\ntrain_df = pl.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\ntrain_features = create_advanced_imu_features(train_df)\n\n# Attach gesture, subject, and sequence_type\ntargets = train_df.group_by(\"sequence_id\").agg(\n    pl.first('gesture'),\n    pl.first('subject'),\n    pl.first('sequence_type')\n)\ntrain_features_full = train_features.join(targets, on='sequence_id', how='left')\ntrain_features_pd = train_features_full.to_pandas()\n\n# --- 3. Final Cross-Validation with Best Parameters ---\nprint(f\"\\n--- Starting 5-Fold CV for Final IMU-Only Model ---\")\nprint(f\"Number of features: {len(train_features_pd.columns) - 4}\")\n\nX = train_features_pd.drop(columns=['sequence_id', 'gesture', 'subject', 'sequence_type'])\ny_str = train_features_pd['gesture']\ngroups = train_features_pd['subject']\nle = LabelEncoder()\ny = le.fit_transform(y_str)\ngesture_to_type_map = train_features_pd.drop_duplicates('gesture').set_index('gesture')['sequence_type'].to_dict()\n\ngkf = GroupKFold(n_splits=5)\nval_scores = []\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(X, y, groups=groups)):\n    print(f\"\\n--- Fold {fold+1}/5 ---\")\n    X_train, y_train = X.iloc[train_idx], y[train_idx]\n    X_val, y_val = X.iloc[val_idx], y[val_idx]\n\n    model = lgb.LGBMClassifier(**BEST_PARAMS)\n    model.fit(X_train, y_train, eval_set=[(X_val, y_val)], callbacks=[lgb.early_stopping(100, verbose=False)])\n    \n    preds = model.predict(X_val)\n    score = competition_metric(y_val, preds, gesture_to_type_map, le)\n    val_scores.append(score)\n    print(f\"Fold {fold+1} Score: {score:.4f}\")\n\nmean_score = np.mean(val_scores)\nprint(f\"\\n--- Average Final IMU-Only Validation Score: {mean_score:.4f} ---\")\n\n# --- 4. Train and Save Final Model on ALL Data ---\nprint(\"\\n--- Training and Saving Final IMU-Only Model ---\")\nfinal_model_imu_advanced = lgb.LGBMClassifier(**BEST_PARAMS)\nfinal_model_imu_advanced.fit(X, y)\n\nprint(\"Saving model, label encoder, and feature list...\")\njoblib.dump(final_model_imu_advanced, 'imu_advanced_model.pkl')\njoblib.dump(le, 'imu_advanced_label_encoder.pkl')\nwith open('imu_advanced_feature_list.txt', 'w') as f:\n    for feature in X.columns:\n        f.write(f\"{feature}\\n\")\n\nprint(\"\\nTraining notebook finished successfully. Artifacts are saved.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-08T01:13:55.251416Z","iopub.execute_input":"2025-06-08T01:13:55.251787Z","execution_failed":"2025-06-08T01:15:20.190Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}