{"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":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<h2>This is the code for training models. The running time exceeded 15 minutes, so I separated the training and inference. \nThe inference code is here</h2>\n<a href='https://www.kaggle.com/code/jiaoyouzhang/cmi-2025-only-lightgbm'>\n  https://www.kaggle.com/code/jiaoyouzhang/cmi-2025-only-lightgbm\n</a>","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport polars as pl\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import StratifiedKFold, StratifiedShuffleSplit\nfrom sklearn.metrics import f1_score, classification_report\nimport lightgbm as lgb\nimport os\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import accuracy_score, f1_score\nimport joblib\nfrom scipy.spatial.transform import Rotation as R\n\n# Import evaluation API\nimport kaggle_evaluation.cmi_inference_server\npd.set_option('display.max_rows', 500)\nrandom_seed = 42","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:17:21.206321Z","iopub.execute_input":"2025-07-09T05:17:21.206553Z","iopub.status.idle":"2025-07-09T05:17:27.074014Z","shell.execute_reply.started":"2025-07-09T05:17:21.206516Z","shell.execute_reply":"2025-07-09T05:17:27.073412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pl.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntrain_demo = pl.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:17:27.075353Z","iopub.execute_input":"2025-07-09T05:17:27.075752Z","iopub.status.idle":"2025-07-09T05:17:42.874900Z","shell.execute_reply.started":"2025-07-09T05:17:27.075735Z","shell.execute_reply":"2025-07-09T05:17:42.874128Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = train.drop([\"sequence_type\",\"orientation\",\"behavior\",\"phase\"])\n\n#data = train.join(train_demo,on=\"subject\",how=\"left\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:17:42.875750Z","iopub.execute_input":"2025-07-09T05:17:42.876005Z","iopub.status.idle":"2025-07-09T05:17:42.912796Z","shell.execute_reply.started":"2025-07-09T05:17:42.875987Z","shell.execute_reply":"2025-07-09T05:17:42.912312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def remove_gravity_from_acc(acc_data, rot_data):\n\n    if isinstance(acc_data, pd.DataFrame):\n        acc_values = acc_data[['acc_x', 'acc_y', 'acc_z']].values\n    else:\n        acc_values = acc_data\n\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = acc_values.shape[0]\n    linear_accel = np.zeros_like(acc_values)\n    \n    gravity_world = np.array([0, 0, 9.81])\n\n    for i in range(num_samples):\n        if np.all(np.isnan(quat_values[i])) or np.all(np.isclose(quat_values[i], 0)):\n            linear_accel[i, :] = acc_values[i, :] \n            continue\n\n        try:\n            rotation = R.from_quat(quat_values[i])\n            gravity_sensor_frame = rotation.apply(gravity_world, inverse=True)\n            linear_accel[i, :] = acc_values[i, :] - gravity_sensor_frame\n        except ValueError:\n             linear_accel[i, :] = acc_values[i, :]\n             \n    return linear_accel\n\ndef calculate_angular_velocity_from_quat(rot_data, time_delta=1/200): # Assuming 200Hz sampling rate\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = quat_values.shape[0]\n    angular_vel = np.zeros((num_samples, 3))\n\n    for i in range(num_samples - 1):\n        q_t = quat_values[i]\n        q_t_plus_dt = quat_values[i+1]\n\n        if np.all(np.isnan(q_t)) or np.all(np.isclose(q_t, 0)) or \\\n           np.all(np.isnan(q_t_plus_dt)) or np.all(np.isclose(q_t_plus_dt, 0)):\n            continue\n\n        try:\n            rot_t = R.from_quat(q_t)\n            rot_t_plus_dt = R.from_quat(q_t_plus_dt)\n\n            # Calculate the relative rotation\n            delta_rot = rot_t.inv() * rot_t_plus_dt\n            \n            # Convert delta rotation to angular velocity vector\n            # The rotation vector (Euler axis * angle) scaled by 1/dt\n            # is a good approximation for small delta_rot\n            angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n        except ValueError:\n            # If quaternion is invalid, angular velocity remains zero\n            pass\n            \n    return angular_vel\n\ndef calculate_angular_distance(rot_data):\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = quat_values.shape[0]\n    angular_dist = np.zeros(num_samples)\n\n    for i in range(num_samples - 1):\n        q1 = quat_values[i]\n        q2 = quat_values[i+1]\n\n        if np.all(np.isnan(q1)) or np.all(np.isclose(q1, 0)) or \\\n           np.all(np.isnan(q2)) or np.all(np.isclose(q2, 0)):\n            angular_dist[i] = 0 # Или np.nan, в зависимости от желаемого поведения\n            continue\n        try:\n            # Преобразование кватернионов в объекты Rotation\n            r1 = R.from_quat(q1)\n            r2 = R.from_quat(q2)\n\n            # Вычисление углового расстояния: 2 * arccos(|real(p * q*)|)\n            # где p* - сопряженный кватернион q\n            # В scipy.spatial.transform.Rotation, r1.inv() * r2 дает относительное вращение.\n            # Угол этого относительного вращения - это и есть угловое расстояние.\n            relative_rotation = r1.inv() * r2\n            \n            # Угол rotation vector соответствует угловому расстоянию\n            # Норма rotation vector - это угол в радианах\n            angle = np.linalg.norm(relative_rotation.as_rotvec())\n            angular_dist[i] = angle\n        except ValueError:\n            angular_dist[i] = 0 # В случае недействительных кватернионов\n            pass\n            \n    return angular_dist","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:17:42.913716Z","iopub.execute_input":"2025-07-09T05:17:42.913884Z","iopub.status.idle":"2025-07-09T05:17:43.033375Z","shell.execute_reply.started":"2025-07-09T05:17:42.913870Z","shell.execute_reply":"2025-07-09T05:17:43.032645Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def feature_engineering_imu(data:pl.DataFrame):\n    data = data.to_pandas()\n    data['acc_mag'] = np.sqrt(data['acc_x']**2 + data['acc_y']**2 + data['acc_z']**2)\n    data['rot_angle'] = 2 * np.arccos(data['rot_w'].clip(-1, 1))\n    data['acc_mag_jerk'] = data.groupby('sequence_id')['acc_mag'].diff().fillna(0)\n    data['rot_angle_vel'] = data.groupby('sequence_id')['rot_angle'].diff().fillna(0)\n\n    linear_accel_list = []\n    for _, group in data.groupby('sequence_id'):\n        acc_data_group = group[['acc_x', 'acc_y', 'acc_z']]\n        rot_data_group = group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        linear_accel_group = remove_gravity_from_acc(acc_data_group, rot_data_group)\n        linear_accel_list.append(pd.DataFrame(linear_accel_group, columns=['linear_acc_x', 'linear_acc_y', 'linear_acc_z'], index=group.index))\n    \n    df_linear_accel = pd.concat(linear_accel_list)\n    data = pd.concat([data, df_linear_accel], axis=1)\n    data['linear_acc_mag'] = np.sqrt(data['linear_acc_x']**2 + data['linear_acc_y']**2 + data['linear_acc_z']**2)\n    data['linear_acc_mag_jerk'] = data.groupby('sequence_id')['linear_acc_mag'].diff().fillna(0)\n    angular_vel_list = []\n    for _, group in data.groupby('sequence_id'):\n        rot_data_group = group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        angular_vel_group = calculate_angular_velocity_from_quat(rot_data_group)\n        angular_vel_list.append(pd.DataFrame(angular_vel_group, columns=['angular_vel_x', 'angular_vel_y', 'angular_vel_z'], index=group.index))\n    \n    df_angular_vel = pd.concat(angular_vel_list)\n    data = pd.concat([data, df_angular_vel], axis=1)\n    \n    print(\"  Calculating angular distance between successive quaternions...\")\n    angular_distance_list = []\n    for _, group in data.groupby('sequence_id'):\n        rot_data_group = group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        angular_dist_group = calculate_angular_distance(rot_data_group)\n        angular_distance_list.append(pd.DataFrame(angular_dist_group, columns=['angular_distance'], index=group.index))\n    \n    df_angular_distance = pd.concat(angular_distance_list)\n    data = pd.concat([data, df_angular_distance], axis=1)\n    data = pl.from_pandas(data)\n    return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:17:43.034153Z","iopub.execute_input":"2025-07-09T05:17:43.034348Z","iopub.status.idle":"2025-07-09T05:17:43.052259Z","shell.execute_reply.started":"2025-07-09T05:17:43.034332Z","shell.execute_reply":"2025-07-09T05:17:43.051659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = feature_engineering_imu(train)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:17:43.052948Z","iopub.execute_input":"2025-07-09T05:17:43.053686Z","iopub.status.idle":"2025-07-09T05:20:54.829171Z","shell.execute_reply.started":"2025-07-09T05:17:43.053661Z","shell.execute_reply":"2025-07-09T05:20:54.828359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def feature_engineering(data:pl.DataFrame):\n    target_col = \"gesture\"\n    \n    # All numeric sensor columns (everything except id, demo, target)\n    stat_cols = [\n        c for c in data.columns\n        if c not in [target_col, \"sequence_id\", \"row_id\",\"sequence_counter\",\"subject\"]\n    ]\n    \n    # Build aggregation expressions\n    agg_exprs = []\n    \n    # full-stats bundle for sensor columns\n    for c in stat_cols:\n        agg_exprs += [\n            pl.col(c).mean().alias(f\"{c}_mean\"),\n            pl.col(c).std().alias(f\"{c}_std\"),\n            pl.col(c).var().alias(f\"{c}_var\"),\n            # pl.col(c).mode().list.first().alias(f\"{c}_mode\"),\n            #pl.col(c).quantile(0.05).alias(f\"{c}_q5\"),\n            pl.col(c).quantile(0.25).alias(f\"{c}_q25\"),\n            pl.col(c).median().alias(f\"{c}_q50\"),\n            pl.col(c).quantile(0.75).alias(f\"{c}_q75\"),\n            #pl.col(c).quantile(0.95).alias(f\"{c}_q95\"),\n            pl.col(c).max().alias(f\"{c}_max\"),\n            pl.col(c).min().alias(f\"{c}_min\"),\n        ]\n    \n    # first() for demographics and target\n    agg_exprs += [\n        pl.col(c).first().alias(c) for c in [target_col]\n    ]\n    \n    # Group-by and aggregate\n    cleaned_data = (\n        data\n        .group_by(\"sequence_id\", maintain_order=True)\n        .agg(agg_exprs)\n    )\n    return cleaned_data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:20:54.831797Z","iopub.execute_input":"2025-07-09T05:20:54.832039Z","iopub.status.idle":"2025-07-09T05:20:54.838533Z","shell.execute_reply.started":"2025-07-09T05:20:54.832022Z","shell.execute_reply":"2025-07-09T05:20:54.837698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = feature_engineering(train)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:20:54.839248Z","iopub.execute_input":"2025-07-09T05:20:54.839449Z","iopub.status.idle":"2025-07-09T05:20:59.076722Z","shell.execute_reply.started":"2025-07-09T05:20:54.839435Z","shell.execute_reply":"2025-07-09T05:20:59.076134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_col = \"gesture\"\npdf = train.to_pandas()  # keeps nullable dtypes\n\nle = LabelEncoder()\ny = le.fit_transform(pdf[target_col])\nX = pdf.drop(columns=[target_col, \"sequence_id\"])         # drop id + label\n\njoblib.dump(le, 'le.joblib')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:20:59.077442Z","iopub.execute_input":"2025-07-09T05:20:59.077661Z","iopub.status.idle":"2025-07-09T05:20:59.288689Z","shell.execute_reply.started":"2025-07-09T05:20:59.077638Z","shell.execute_reply":"2025-07-09T05:20:59.288141Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def competition_metric(y_true, y_pred, le_instance, all_original_gestures):\n    \"\"\"\n    Competition metric calculation\n    \"\"\"\n    bfrb_gestures = [g for g in all_original_gestures if g in le_instance.classes_]\n    \n    # Binary F1: All are Target in this filtered dataset\n    y_true_binary = np.ones_like(y_true, dtype=int)\n    y_pred_binary = np.ones_like(y_pred, dtype=int)\n    binary_f1 = f1_score(y_true_binary, y_pred_binary, average='binary', pos_label=1, zero_division=0)\n    \n    # Macro F1: specific gesture classification\n    macro_f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)\n    \n    final_score = (binary_f1 + macro_f1) / 2\n    return final_score, binary_f1, macro_f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T05:20:59.289352Z","iopub.execute_input":"2025-07-09T05:20:59.290013Z","iopub.status.idle":"2025-07-09T05:20:59.294776Z","shell.execute_reply.started":"2025-07-09T05:20:59.289987Z","shell.execute_reply":"2025-07-09T05:20:59.294074Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)\ncv_scores = []\nmodels = []\n\nall_original_gestures_in_train = pdf['gesture'].unique()\n\ncallbacks = [lgb.early_stopping(stopping_rounds=100, verbose=100)]\n\n# LightGBM model with cross-validation\nprint(\"\\nTraining LightGBM models with cross-validation...\")\nfor fold, (train_idx, val_idx) in enumerate(cv.split(X, y)):\n    print(f\"\\nFold {fold + 1}/5\")\n    \n    X_train_fold, X_val_fold = X.iloc[train_idx], X.iloc[val_idx]\n    y_train_fold, y_val_fold = y[train_idx], y[val_idx]\n    \n    # LightGBM model with GPU acceleration\n    model = lgb.LGBMClassifier(\n        objective='multiclass',\n        n_estimators= 1000,\n        learning_rate= 0.06,\n        max_depth= 5,\n        reg_alpha= 0.8,\n        lambda_l2= 4.0,  \n        #num_leaves=512, \n        min_child_samples= 32,\n        colsample_bytree= 0.8,\n        subsample= 0.8,\n        subsample_freq=0,\n        cat_smooth=20.0,\n        is_unbalance=True,\n        max_bin=127,\n        verbose=-1,  \n        metric='multi_logloss',   \n        device='gpu',  \n    )\n    \n    # Train model with verbose output\n    model.fit(\n        X_train_fold, y_train_fold,\n        eval_set=[(X_val_fold, y_val_fold)],  \n        eval_metric='multi_logloss',  \n        callbacks=callbacks\n    )\n    \n    # Predict\n    y_pred_fold = model.predict(X_val_fold)\n    \n    # Calculate score\n    score, binary_f1, macro_f1 = competition_metric(\n        y_val_fold, y_pred_fold, le, all_original_gestures_in_train\n    )\n    \n    cv_scores.append(score)\n    models.append(model)\n    joblib.dump(model, f'model_lgb{fold}.joblib')\n    \n    print(f\"Fold {fold + 1} - Competition Score: {score:.4f} (Binary F1: {binary_f1:.4f}, Macro F1: {macro_f1:.4f})\")\n\n    # feature importances\n    feature_importances = model.feature_importances_    \n    feature_names = X.columns    \n    feature_importance_df = pd.DataFrame({\n        'Feature': feature_names,\n        'Importance': feature_importances\n    })    \n    feature_importance_df = feature_importance_df.sort_values(by='Importance', ascending=False)    \n    print(feature_importance_df.head(20))\n\nprint(f\"\\nCross-validation results:\")\nprint(f\"Mean CV Score: {np.mean(cv_scores):.4f} (+/- {np.std(cv_scores) * 2:.4f})\")\nprint(f\"Individual fold scores: {cv_scores}\")\n\n# Train final model on all data with GPU acceleration\nprint(\"\\nTraining final model on all training data...\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T06:07:31.687822Z","iopub.execute_input":"2025-07-09T06:07:31.688352Z","iopub.status.idle":"2025-07-09T06:13:48.124171Z","shell.execute_reply.started":"2025-07-09T06:07:31.688327Z","shell.execute_reply":"2025-07-09T06:13:48.123209Z"}},"outputs":[],"execution_count":null}]}