{"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"},{"sourceId":255777770,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<h2>Versions 1 to 11 contain the complete code. Starting from Version 12, since the model training time exceeded 15 minutes, I have separated the model training and inference. </h2>\n<h2>The training code is here</h2>\n<a href='https://www.kaggle.com/code/jiaoyouzhang/cmi-2025-only-lightgbm-training'>\n  https://www.kaggle.com/code/jiaoyouzhang/cmi-2025-only-lightgbm-training\n</a>","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport polars as pl\nimport pandas as pd\nimport joblib\nfrom scipy.spatial.transform import Rotation as R\n\nmodels=[]\nfor fold in range(3):\n    model = joblib.load(f'/kaggle/input/cmi-2025-only-lightgbm-training/model_lgb{fold}.joblib')\n    models.append(model)\nle = joblib.load('/kaggle/input/cmi-2025-only-lightgbm-training/le.joblib')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T04:33:58.726711Z","iopub.execute_input":"2025-07-09T04:33:58.726997Z","iopub.status.idle":"2025-07-09T04:33:59.013969Z","shell.execute_reply.started":"2025-07-09T04:33:58.726976Z","shell.execute_reply":"2025-07-09T04:33:59.013445Z"}},"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-09T04:33:59.014899Z","iopub.execute_input":"2025-07-09T04:33:59.015088Z","iopub.status.idle":"2025-07-09T04:33:59.026746Z","shell.execute_reply.started":"2025-07-09T04:33:59.015071Z","shell.execute_reply":"2025-07-09T04:33:59.026013Z"}},"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-09T04:33:59.027401Z","iopub.execute_input":"2025-07-09T04:33:59.027576Z","iopub.status.idle":"2025-07-09T04:33:59.050850Z","shell.execute_reply.started":"2025-07-09T04:33:59.027563Z","shell.execute_reply":"2025-07-09T04:33:59.050092Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kaggle_evaluation.cmi_inference_server\nimport os\n\ndef feature_engineering_inf(data:pl.DataFrame):\n\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 [\"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.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).max().alias(f\"{c}_max\"),\n            pl.col(c).min().alias(f\"{c}_min\"),\n        ]\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-09T04:33:59.052168Z","iopub.execute_input":"2025-07-09T04:33:59.052402Z","iopub.status.idle":"2025-07-09T04:33:59.072416Z","shell.execute_reply.started":"2025-07-09T04:33:59.052386Z","shell.execute_reply":"2025-07-09T04:33:59.071604Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    data = sequence\n    data = feature_engineering_imu(data)\n    data = feature_engineering_inf(data)\n    \n    pdf = data.to_pandas().drop(columns=[\"sequence_id\"])\n    # Predict using ensemble of CV models\n    predictions = []\n    for model in models:\n        pred = model.predict(pdf)\n        # Ensure we get a scalar value\n        if isinstance(pred, np.ndarray):\n            pred = pred[0]\n        predictions.append(int(pred))\n    \n    # Use majority vote or most confident prediction\n    predicted_label_id = max(set(predictions), key=predictions.count)\n    \n    # Convert back to gesture string\n    predicted_gesture_str = le.inverse_transform([predicted_label_id])[0]\n    \n    return predicted_gesture_str","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T04:33:59.073254Z","iopub.execute_input":"2025-07-09T04:33:59.073495Z","iopub.status.idle":"2025-07-09T04:33:59.087600Z","shell.execute_reply.started":"2025-07-09T04:33:59.073470Z","shell.execute_reply":"2025-07-09T04:33:59.086941Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T04:33:59.088242Z","iopub.execute_input":"2025-07-09T04:33:59.088545Z","iopub.status.idle":"2025-07-09T04:34:00.264691Z","shell.execute_reply.started":"2025-07-09T04:33:59.088520Z","shell.execute_reply":"2025-07-09T04:34:00.263970Z"}},"outputs":[],"execution_count":null}]}