{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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"},"colab":{"provenance":[],"gpuType":"T4"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# $$📡HeliosTrack – Behavior\\ Detection\\ with\\ CMI\\ Data | v1.1$$","metadata":{}},{"cell_type":"markdown","source":"# 💻 Setup & Overview","metadata":{}},{"cell_type":"code","source":"import os\nimport random\nimport tensorflow as tf\nfrom tqdm import tqdm\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import accuracy_score, f1_score\n\ndef set_seed(seed=42):\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n\nSEED = 42\nset_seed(SEED)","metadata":{"id":"33MUu1HU7cJT","outputId":"53030e9b-2b91-48a3-df0f-18578494cd34","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:11:01.185559Z","iopub.execute_input":"2025-08-20T16:11:01.186291Z","iopub.status.idle":"2025-08-20T16:11:17.957606Z","shell.execute_reply.started":"2025-08-20T16:11:01.186262Z","shell.execute_reply":"2025-08-20T16:11:17.956838Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the datasets\nTRAIN_DF = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\",index_col=0)\nTEST_DF = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\",index_col=0)\nTRAIN_DEM_DF = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\nTEST_DEM_DF = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")\n\n# Load datasets (on Colab)\n# import os\n# TRAIN_DF = pd.read_csv(os.path.join(cmi_detect_behavior_with_sensor_data_path, 'train.csv'))\n# TEST_DF = pd.read_csv(os.path.join(cmi_detect_behavior_with_sensor_data_path, 'test.csv'))\n# TRAIN_DEM_DF = pd.read_csv(os.path.join(cmi_detect_behavior_with_sensor_data_path, 'train_demographics.csv'))\n# TEST_DEM_DF = pd.read_csv(os.path.join(cmi_detect_behavior_with_sensor_data_path, 'test_demographics.csv'))","metadata":{"id":"1zQ-ZWQJ8JwL","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:11:17.958774Z","iopub.execute_input":"2025-08-20T16:11:17.959434Z","iopub.status.idle":"2025-08-20T16:11:45.364801Z","shell.execute_reply.started":"2025-08-20T16:11:17.959413Z","shell.execute_reply":"2025-08-20T16:11:45.364058Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## IMU only","metadata":{"id":"lsBrEmpB_oyx"}},{"cell_type":"code","source":"# Keep only IMU features (drop thm_ and tof_ features from TRAIN and TEST)\nNON_IMU_FEATURES = [col for col in TRAIN_DF.columns if col.startswith('thm_') or col.startswith('tof_')]\nTRAIN_DF = TRAIN_DF.drop(NON_IMU_FEATURES, axis=1)\nTEST_DF = TEST_DF.drop(NON_IMU_FEATURES, axis=1)","metadata":{"id":"2HH0dVG_DN3o","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:11:45.365481Z","iopub.execute_input":"2025-08-20T16:11:45.365675Z","iopub.status.idle":"2025-08-20T16:11:45.416760Z","shell.execute_reply.started":"2025-08-20T16:11:45.365658Z","shell.execute_reply":"2025-08-20T16:11:45.415905Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Check","metadata":{"id":"2UoZd8tY814t"}},{"cell_type":"code","source":"# Print helper function\ndef print_with_sep(text,sep=\"=\",n=20):\n  print(\"\\n\")\n  print(sep*n)\n  print(text)\n  print(sep*n)\n\n# Check shapes of all 4 datasets\ndatasets = {'TRAIN_DF': TRAIN_DF, 'TEST_DF': TEST_DF, 'TRAIN_DEM_DF': TRAIN_DEM_DF, 'TEST_DEM_DF': TEST_DEM_DF}\n\n# Check shapes\nprint_with_sep(\"Shapes\")\nfor name, df in datasets.items():\n  print(f\"{name} shape: {df.shape}\")\n\n# Check duplicates\nprint_with_sep(\"Duplicates\")\nfor name, df in datasets.items():\n  print(f\"{name} duplicates: {df.duplicated().sum()}\")\n\n# Check nans\nprint_with_sep(\"NaNs\")\nfor name, df in datasets.items():\n  print(f\"{name} NaNs: {df.isnull().sum().sum()}\")\n\n# Check col difference\nprint_with_sep(\"Columns not in test\")\nprint(set(TRAIN_DF.columns).difference(set(TEST_DF.columns)))","metadata":{"id":"DC_FOx-u83dE","outputId":"7563952a-8562-4ead-c27e-61915057b352","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:11:45.418753Z","iopub.execute_input":"2025-08-20T16:11:45.418984Z","iopub.status.idle":"2025-08-20T16:11:45.970743Z","shell.execute_reply.started":"2025-08-20T16:11:45.418965Z","shell.execute_reply":"2025-08-20T16:11:45.970149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check descriptive stats\nprint_with_sep(\"Descriptive Statistics\")\nfor name, df in datasets.items():\n  print(f\"{name} Description:\")\n  display(df.describe(include='all').T.replace(np.nan,'-').style.background_gradient(cmap='Blues',subset='count'))\n  print(\"\\n\")","metadata":{"id":"zTZ0iCjG_qCo","outputId":"e13f8458-bf9d-47f5-daee-cfd80140ebd6","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:11:45.971572Z","iopub.execute_input":"2025-08-20T16:11:45.971862Z","iopub.status.idle":"2025-08-20T16:11:46.676665Z","shell.execute_reply.started":"2025-08-20T16:11:45.971835Z","shell.execute_reply":"2025-08-20T16:11:46.675905Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🎯 Target Analysis","metadata":{"id":"cE5kaHL99rpA"}},{"cell_type":"code","source":"# Identify target\ntarget = 'gesture'\n\n# Distribution of the target variable\nplt.figure(figsize=(12, 5))\nsns.countplot(data=TRAIN_DF, y=target, order = TRAIN_DF[target].value_counts().index, palette='viridis')\nplt.title(f'Distribution of {target.capitalize()} (Target Variable)')\nplt.xlabel('Count')\nplt.ylabel(f'{target.capitalize()}')\nplt.show()","metadata":{"id":"i8AaI2Gg9srP","outputId":"7f12ce39-48e5-4309-e84a-7be38d24ca71","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:11:46.677465Z","iopub.execute_input":"2025-08-20T16:11:46.677883Z","iopub.status.idle":"2025-08-20T16:11:47.297439Z","shell.execute_reply.started":"2025-08-20T16:11:46.677857Z","shell.execute_reply":"2025-08-20T16:11:47.296645Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Relationship between numerical features and the target variable (using boxplots)\n\n# Get the numerical features excluding the target and the index\nnumerical_features = TRAIN_DF.select_dtypes(include=np.number).columns.tolist()\n\n# Set up the subplot grid\nfig, axes = plt.subplots(4, 2, figsize=(18, 3 * 6))\naxes = axes.flatten()\n\n# Iterate through the numerical features and create boxplots\nfor i, feature in enumerate(numerical_features):\n    sns.boxplot(y=target, x=feature, data=TRAIN_DF, ax=axes[i], hue = target, palette='viridis')\n    axes[i].set_title(f'Target vs {feature}')\n    axes[i].set_ylabel('Target')\n    axes[i].set_xlabel(feature)\n    axes[i].legend_.remove()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:11:47.298307Z","iopub.execute_input":"2025-08-20T16:11:47.298578Z","iopub.status.idle":"2025-08-20T16:12:02.375389Z","shell.execute_reply.started":"2025-08-20T16:11:47.298559Z","shell.execute_reply":"2025-08-20T16:12:02.374747Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📊 Visualize Data Distribution (Univariate Analysis)\n\n[commented out to not exceed 15 mins]","metadata":{"id":"A2_uBxzmAKjt"}},{"cell_type":"markdown","source":"## Numeric Features","metadata":{"id":"wmK2VoAtEqLE"}},{"cell_type":"code","source":"# # Perform univariate analysis for each numerical variable\n# for variable in TRAIN_DF.select_dtypes(include='number'):\n#     # Create subplots\n#     fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n#     annot_kws = {'xy': (0.70, 0.75), 'xycoords': 'axes fraction', 'fontsize': 10}\n\n#     # Box plot\n#     sns.boxplot(data=TRAIN_DF, x=variable, ax=axes[0])\n#     axes[0].set_xlabel(variable)\n#     axes[0].set_title(f\"Box Plot of {variable}\")\n\n#     # Histogram\n#     sns.histplot(data=TRAIN_DF, x=variable, kde=True, bins=30, ax=axes[1])\n#     axes[1].set_xlabel(variable)\n#     axes[1].set_ylabel(\"Frequency\")\n#     axes[1].set_title(f\"Histogram of {variable} [Train]\")\n#     # axes[1].legend()\n#     axes[1].annotate(f\"Skewness (TRAIN): {TRAIN_DF[variable].skew():.2f}\\nKurtosis (TRAIN): {TRAIN_DF[variable].kurt():.2f}\",\n#                      xy=annot_kws['xy'], xycoords=annot_kws['xycoords'], fontsize=annot_kws['fontsize'])\n\n\n#     # Adjust spacing and show\n#     plt.tight_layout()\n#     plt.show()","metadata":{"id":"InGpbAM3ASDO","outputId":"047dfdd8-5bc4-4ff1-ff2d-75464fc1a15b","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T17:07:59.209786Z","iopub.execute_input":"2025-08-20T17:07:59.209982Z","iopub.status.idle":"2025-08-20T17:07:59.214510Z","shell.execute_reply.started":"2025-08-20T17:07:59.209966Z","shell.execute_reply":"2025-08-20T17:07:59.213772Z"},"_kg_hide-input":true,"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Categorical features","metadata":{"id":"9H6cAm1TEtDp"}},{"cell_type":"code","source":"# TRAIN_DF.select_dtypes(exclude='number').columns.difference([target]+['row_id', 'sequence_id'])","metadata":{"id":"BdW5eFDvE2-S","outputId":"e76769c6-0eec-44d5-c9ee-d6c4cbb7cb11","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T17:07:59.215995Z","iopub.execute_input":"2025-08-20T17:07:59.216521Z","iopub.status.idle":"2025-08-20T17:07:59.302302Z","shell.execute_reply.started":"2025-08-20T17:07:59.216490Z","shell.execute_reply":"2025-08-20T17:07:59.301463Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # Define features to investigate\n# cat_cols = TRAIN_DF.select_dtypes(exclude='number').columns.difference([target]+['row_id', 'sequence_id','subject'])\n\n# # Visualise categorical variables\n# fig, axes = plt.subplots(2,2,figsize=(15, 5))\n# ax = axes.flatten()\n\n# for i, col in enumerate(TRAIN_DF[cat_cols]):\n#     sns.countplot(data=TRAIN_DF, y=col, order = TRAIN_DF[col].value_counts().index, palette='viridis', ax=ax[i])\n\n# plt.tight_layout()","metadata":{"id":"8i_lI5SKEuts","outputId":"52539cc5-da3e-4933-8f59-93eed67e296b","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T17:07:59.302670Z","iopub.status.idle":"2025-08-20T17:07:59.302870Z","shell.execute_reply.started":"2025-08-20T17:07:59.302772Z","shell.execute_reply":"2025-08-20T17:07:59.302782Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🛠 Feature Engineer","metadata":{"id":"9T6Ge8amH42b"}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom scipy.spatial.transform import Rotation as R\n\ndef compute_angular_features(df, seq_col='sequence_id', sampling_rate=200):\n    dt = 1.0 / sampling_rate\n    results = []\n\n    for _, group in df.groupby(seq_col):\n        quats = group[[\"rot_x\", \"rot_y\", \"rot_z\", \"rot_w\"]].to_numpy()\n\n        omega = np.zeros((len(quats), 3))  # fill with zeros first\n\n        for i in range(len(quats)-1):\n            # Start a try loop to avoid invalid (zero norm) quaternions\n            try:\n                q1 = quats[i]\n                q2 = quats[i+1]\n\n                # Convert to Rotation object\n                r1 = R.from_quat(q1)\n                r2 = R.from_quat(q2)\n\n                # Compute relative rotations (t -> t+1)\n                delta = r2 * r1.inv()\n\n                # Convert to rotation vectors\n                omega[i+1] = delta.as_rotvec() / dt\n\n            except Exception:\n                # if anything goes wrong, keep zeros\n                pass\n\n        # angular acceleration = diff of omega\n        alpha = np.zeros_like(omega)\n        alpha[1:] = np.diff(omega, axis=0) / dt\n\n        # Append\n        group = group.copy()\n        group[[\"omega_x\", \"omega_y\", \"omega_z\"]] = omega\n        group[[\"alpha_x\", \"alpha_y\", \"alpha_z\"]] = alpha\n        results.append(group)\n        \n    return pd.concat(results, ignore_index=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:12:25.892587Z","iopub.execute_input":"2025-08-20T16:12:25.892803Z","iopub.status.idle":"2025-08-20T16:12:25.910312Z","shell.execute_reply.started":"2025-08-20T16:12:25.892788Z","shell.execute_reply":"2025-08-20T16:12:25.909575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_physics_features(\n    data,\n    seq_col = \"sequence_id\",\n    time_col = \"sequence_counter\",\n    use_bfill = True,\n    add_cross_terms = False,\n    add_stats_features = False\n):\n\n    ε = 1e-8\n    df = data.copy()\n\n    # # sort within each sequence to ensure correct differencing\n    # df = df.sort_values([seq_col, time_col])\n\n    # Compute angular features\n    df = compute_angular_features(df, seq_col=seq_col)\n\n    # numeric columns except the counter\n    num_cols = df.select_dtypes(include=np.number).columns.difference([time_col])\n    \n    # per-sequence imputation: forward-fill, optionally backward-fill (beware of leakage)\n    if use_bfill:\n        df[num_cols] = df.groupby(seq_col)[num_cols].apply(lambda x:x.ffill().bfill()).reset_index(level=0, drop=True)\n        df[num_cols] = df.groupby(seq_col)[num_cols].apply(lambda x:x.fillna(x.min()).fillna(0)).reset_index(level=0, drop=True) # fill remaining nans\n    else:\n        df[num_cols] = df.groupby(seq_col)[num_cols].apply(lambda x:x.ffill()).reset_index(level=0, drop=True)\n        df[num_cols] = df.groupby(seq_col)[num_cols].apply(lambda x:x.fillna(x.min()).fillna(0)).reset_index(level=0, drop=True) # fill remaining nans\n\n    # --- ACCELERATION FEATURES ---\n    acc_cols = [c for c in df.columns if c.startswith(\"acc\")]\n    for col in acc_cols:\n        df[f\"diff1_{col}\"] = df.groupby(seq_col)[col].diff() # jerk\n        \n    # --- ROTATION FEATURES ---\n    rot_cols = [c for c in df.columns if c.startswith(\"alpha\")]\n    for col in rot_cols:\n        df[f\"diff1_{col}\"] = df.groupby(seq_col)[col].diff() # angular jerk \n\n    # --- ACCELERATION AND ROTATION MAGNITUDES ---\n    df[\"acc_mag\"] = np.sqrt(df[\"acc_x\"]**2 + df[\"acc_y\"]**2 + df[\"acc_z\"]**2)\n    df[\"rot_mag\"] = np.sqrt(df[\"alpha_x\"]**2 + df[\"alpha_y\"]**2 + df[\"alpha_z\"]**2)\n    \n    # fill NaNs introduced by diff with 0\n    diff_mask = [c for c in df.columns if c.startswith(\"diff\")]\n    df[diff_mask] = df[diff_mask].fillna(0.0)\n\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:12:25.910956Z","iopub.execute_input":"2025-08-20T16:12:25.911237Z","iopub.status.idle":"2025-08-20T16:12:25.925709Z","shell.execute_reply.started":"2025-08-20T16:12:25.911222Z","shell.execute_reply":"2025-08-20T16:12:25.925158Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Test function on TEST_DF\ndisplay(add_physics_features(TEST_DF))","metadata":{"id":"U9cA5mXuHIiQ","outputId":"d453aadd-906c-401e-e063-180a50dd2ece","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:12:25.926618Z","iopub.execute_input":"2025-08-20T16:12:25.926851Z","iopub.status.idle":"2025-08-20T16:12:25.996083Z","shell.execute_reply.started":"2025-08-20T16:12:25.926829Z","shell.execute_reply":"2025-08-20T16:12:25.995505Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_DF = add_physics_features(TRAIN_DF)\nTEST_DF = add_physics_features(TEST_DF)","metadata":{"id":"fda3c4a1","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:12:25.996750Z","iopub.execute_input":"2025-08-20T16:12:25.996951Z","iopub.status.idle":"2025-08-20T16:13:24.458152Z","shell.execute_reply.started":"2025-08-20T16:12:25.996936Z","shell.execute_reply":"2025-08-20T16:13:24.457153Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🧠 Keras","metadata":{"id":"Y8Uo8iKJQmJy"}},{"cell_type":"markdown","source":"## Data preparation for kfold\nPrepare the data for StratifiedGroupKFold by creating sequences and identifying the groups and targets.\n","metadata":{"id":"4d5c6ed5"}},{"cell_type":"code","source":"type_to_num = {\n    \"Target\": 1,\n    \"Non-Target\":0\n    }\n\nlabel_to_num = {\n    'Drink from bottle/cup': 0,  # < ------- NON-TARGETS START\n    'Feel around in tray and pull out an object': 0,\n    'Glasses on/off': 0,\n    'Pinch knee/leg skin': 0,\n    'Pull air toward your face': 0,\n    'Scratch knee/leg skin': 0,\n    'Text on phone': 0,\n    'Wave hello': 0,\n    'Write name in air': 0,\n    'Write name on leg': 0,  # < ------- NON-TARGETS END\n    'Above ear - pull hair': 1,  # < ------- TARGETS START\n    'Cheek - pinch skin': 2,\n    'Eyebrow - pull hair': 3,\n    'Eyelash - pull hair': 4,\n    'Forehead - pull hairline': 5,\n    'Forehead - scratch': 6,\n    'Neck - pinch skin': 7,\n    'Neck - scratch': 8,  # < ------- TARGETS END\n}\n\n# Map target (label_to_num)\nTRAIN_DF['gesture'].map(label_to_num)","metadata":{"id":"Xc3X5k4XNqUh","outputId":"cbcd6e60-d8d5-448e-f137-ba7ec4b2481e","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:24.459286Z","iopub.execute_input":"2025-08-20T16:13:24.459558Z","iopub.status.idle":"2025-08-20T16:13:24.500784Z","shell.execute_reply.started":"2025-08-20T16:13:24.459534Z","shell.execute_reply":"2025-08-20T16:13:24.499766Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define reverse dictionaries\nnum_to_label = {v: k for k, v in label_to_num.items()}\nnum_to_type = {v: k for k, v in type_to_num.items()}\n\n# Leave 0 value in num_to_label as a random non-target type (I don't need to trace back to specific non-target gesture. See challenge overview)\n# num_to_label[0] = num_to_type[0]\nnum_to_label","metadata":{"id":"l8xksDGs76lN","outputId":"b31c8ab6-bf88-4572-fa77-5aa803948ec8","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:24.501689Z","iopub.execute_input":"2025-08-20T16:13:24.501943Z","iopub.status.idle":"2025-08-20T16:13:24.513455Z","shell.execute_reply.started":"2025-08-20T16:13:24.501917Z","shell.execute_reply":"2025-08-20T16:13:24.512781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Encode target with map\nTRAIN_DF['gesture'] = TRAIN_DF['gesture'].map(label_to_num)\nTRAIN_DF['gesture'].unique()","metadata":{"id":"f-GFKXCw9AJy","outputId":"b3fb5380-8bc5-449a-bd6b-a54ed57d159a","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:24.514399Z","iopub.execute_input":"2025-08-20T16:13:24.514632Z","iopub.status.idle":"2025-08-20T16:13:24.560925Z","shell.execute_reply.started":"2025-08-20T16:13:24.514610Z","shell.execute_reply":"2025-08-20T16:13:24.560228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Segment data by sequence_id (TRAIN_DF)\nfeatures = TRAIN_DF.select_dtypes(include=np.number).columns.difference(['sequence_counter','gesture']).tolist()\ntrain_sequences = []\ntrain_groups = []\ntargets = []\n\nfor sequence_id, group_df in TRAIN_DF.groupby('sequence_id'):\n    train_sequences.append(group_df[features].values)\n    train_groups.append(group_df['subject'].iloc[0])\n    targets.append(group_df['gesture'].iloc[0])\n\nlen(train_groups)","metadata":{"id":"c995834a","outputId":"58eb0b7e-fefd-483a-c36d-36203fe97bc9","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:24.561851Z","iopub.execute_input":"2025-08-20T16:13:24.562071Z","iopub.status.idle":"2025-08-20T16:13:30.797093Z","shell.execute_reply.started":"2025-08-20T16:13:24.562055Z","shell.execute_reply":"2025-08-20T16:13:30.796520Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Segment data by sequence_id (TEST_DF)\ntest_sequences = []\ntest_groups = []\n\nfor sequence_id, group_df in TEST_DF.groupby('sequence_id'):\n    test_sequences.append(group_df[features].values)\n    test_groups.append(group_df['subject'].iloc[0])\n\nlen(test_groups)","metadata":{"id":"hri5V28GSRMw","outputId":"00d4f114-8b60-4e84-da36-6a913fc03d5f","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:30.797778Z","iopub.execute_input":"2025-08-20T16:13:30.798019Z","iopub.status.idle":"2025-08-20T16:13:30.814006Z","shell.execute_reply.started":"2025-08-20T16:13:30.797994Z","shell.execute_reply":"2025-08-20T16:13:30.813382Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Determine Sequence Length","metadata":{"id":"2iP7RWJRVn3n"}},{"cell_type":"code","source":"seq_lengths = [s.shape[0] for s in train_sequences]\n\nplt.figure(figsize=(12, 5))\nsns.histplot(seq_lengths, bins=100)\nplt.title(\"Sequence Lengths\")\nplt.show()","metadata":{"id":"58owjv2mVqfN","outputId":"3e0a84c4-7687-461f-94df-1b332c6605eb","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:30.814620Z","iopub.execute_input":"2025-08-20T16:13:30.814809Z","iopub.status.idle":"2025-08-20T16:13:31.345355Z","shell.execute_reply.started":"2025-08-20T16:13:30.814795Z","shell.execute_reply":"2025-08-20T16:13:31.344607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert to pandas Series to use quantile\nseq_lengths_series = pd.Series(seq_lengths)\n\n# Calculate the 95th percentile and assign it to MAXLEN\nMAXLEN = int(seq_lengths_series.quantile(0.95))\nprint(f\"95th percentile of sequence lengths: {MAXLEN}\")","metadata":{"id":"PQG4bvSYWPY-","outputId":"168d283c-958c-4d2b-b6a3-fbe11f923eb7","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:31.346430Z","iopub.execute_input":"2025-08-20T16:13:31.346712Z","iopub.status.idle":"2025-08-20T16:13:31.353483Z","shell.execute_reply.started":"2025-08-20T16:13:31.346687Z","shell.execute_reply":"2025-08-20T16:13:31.352635Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Pad Sequences","metadata":{"id":"di8nKDf4WZjS"}},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.sequence import pad_sequences\n\n# pad_sequences (train)\nX_padded = pad_sequences(\n    train_sequences,\n    maxlen=MAXLEN,\n    dtype='float32',\n    padding='post',\n    truncating='post',\n    value=0\n)","metadata":{"id":"tQxnssRIWcDN","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:31.354243Z","iopub.execute_input":"2025-08-20T16:13:31.354474Z","iopub.status.idle":"2025-08-20T16:13:31.469830Z","shell.execute_reply.started":"2025-08-20T16:13:31.354450Z","shell.execute_reply":"2025-08-20T16:13:31.469310Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# pad_sequences (test)\nX_test_padded = pad_sequences(\n    test_sequences,\n    maxlen=MAXLEN,\n    dtype='float32',\n    padding='post',\n    truncating='post',\n    value=0\n)","metadata":{"id":"4SQOLKbHmWZR","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:31.470461Z","iopub.execute_input":"2025-08-20T16:13:31.470620Z","iopub.status.idle":"2025-08-20T16:13:31.474386Z","shell.execute_reply.started":"2025-08-20T16:13:31.470607Z","shell.execute_reply":"2025-08-20T16:13:31.473535Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Stratified Group K-Fold","metadata":{"id":"ZWOxghN8YN6p"}},{"cell_type":"code","source":"# Initialise sgkf\nsgkf = StratifiedGroupKFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\n# Define y\ny = np.array(targets)\n\n# Define folds\nfolds = list(sgkf.split(X_padded, y, train_groups))","metadata":{"id":"ZxMUBiFFYLxk","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:31.475347Z","iopub.execute_input":"2025-08-20T16:13:31.475858Z","iopub.status.idle":"2025-08-20T16:13:31.530472Z","shell.execute_reply.started":"2025-08-20T16:13:31.475834Z","shell.execute_reply":"2025-08-20T16:13:31.529935Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Function to Build Model","metadata":{"id":"7a56e4b0"}},{"cell_type":"code","source":"def build_model(input_shape, n_classes):\n    inputs = keras.Input(shape=input_shape)\n\n    # Mask padded timesteps\n    x = layers.Masking(mask_value=0)(inputs)\n\n    # Bidirectional LSTM\n    x = layers.Bidirectional(layers.LSTM(128, return_sequences=True))(x); x = layers.Dropout(0.1)(x)\n    x = layers.Bidirectional(layers.LSTM(128, return_sequences=True))(x); x = layers.Dropout(0.1)(x)\n    x = layers.Bidirectional(layers.LSTM(64, return_sequences=True))(x); x = layers.Dropout(0.1)(x)\n    x = layers.Bidirectional(layers.LSTM(64, return_sequences=True))(x); x = layers.Dropout(0.1)(x)\n\n    # Global pooling → converts (batch, time, features) → (batch, features)\n    x = layers.GlobalAveragePooling1D()(x)\n\n    x = layers.Dense(64, activation='relu')(x)\n    outputs = layers.Dense(n_classes, activation='softmax')(x)\n\n    model = keras.Model(inputs, outputs)\n    model.compile(\n        optimizer='adam',\n        loss='sparse_categorical_crossentropy',\n        metrics=['accuracy']\n    )\n    return model","metadata":{"id":"b6fe1999","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:31.531349Z","iopub.execute_input":"2025-08-20T16:13:31.531612Z","iopub.status.idle":"2025-08-20T16:13:31.537156Z","shell.execute_reply.started":"2025-08-20T16:13:31.531589Z","shell.execute_reply":"2025-08-20T16:13:31.536538Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training Loop (Cross-validation)","metadata":{"id":"c380820b"}},{"cell_type":"code","source":"histories = []\noof_preds = np.zeros((len(y), len(np.unique(y))))\ntest_preds = np.zeros((len(X_test_padded), len(np.unique(y))))\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.utils.class_weight import compute_class_weight\nimport tensorflow as tf\nfrom tensorflow import keras\nimport tensorflow.keras.layers as layers\n\nn_classes = len(np.unique(y))\n\nfor fold, (train_idx, val_idx) in enumerate(folds):\n    print(f\"\\n=== Fold {fold+1} ===\")\n\n    X_train, X_val = X_padded[train_idx], X_padded[val_idx]\n    y_train, y_val = y[train_idx], y[val_idx]\n\n    # Fit scaler on train only\n    n_features = X_train.shape[2]\n    scaler = StandardScaler()\n\n    # Flatten time axis for fitting scaler\n    scaler.fit(\n        X_train.reshape(-1, n_features)\n    )\n\n    # Compute class weights\n    weights = compute_class_weight(\n        class_weight='balanced',\n        classes=np.unique(y_train),\n        y=y_train\n    )\n    # Convert class weights to a dictionary\n    class_weight_dict = dict(zip(np.unique(y_train), weights))\n    print(class_weight_dict)\n\n    # transform and reshape back\n    X_train_scaled = scaler.transform(X_train.reshape(-1, n_features)).reshape(X_train.shape)\n    X_val_scaled = scaler.transform(X_val.reshape(-1, n_features)).reshape(X_val.shape)\n    X_test_scaled = scaler.transform(X_test_padded.reshape(-1, n_features)).reshape(X_test_padded.shape)\n\n    model = build_model(\n        input_shape=(MAXLEN, n_features),\n        n_classes=n_classes\n    )\n\n    callbacks = [\n        keras.callbacks.EarlyStopping(\n            patience=5,\n            restore_best_weights=True\n        )\n    ]\n\n    history = model.fit(\n        X_train_scaled, y_train,\n        validation_data=(X_val_scaled, y_val),\n        epochs=50,\n        batch_size=32,\n        callbacks=callbacks,\n        class_weight=class_weight_dict,\n        verbose=1\n    )\n\n    histories.append(history)\n\n    # Predict OOF and test\n    oof_preds[val_idx] = model.predict(X_val_scaled, verbose=0)\n    test_preds += model.predict(X_test_scaled)","metadata":{"id":"0d64a589","outputId":"6e0a978a-affb-437a-d3b5-2cd1daf9e383","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T16:13:31.537744Z","iopub.execute_input":"2025-08-20T16:13:31.537969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# histories[0].model.predict(X_test_scaled[0])\nhistories[0].model.predict(X_test_scaled).argmax(axis=1)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize Training Curves","metadata":{"id":"LhbzNsLkeU8m"}},{"cell_type":"code","source":"# Plot accuracy per fold\n\n# subplots\nfig, ax = plt.subplots(1, 2, figsize=(15, 5))\naxes = ax.flatten()\n\nfor i, h in enumerate(histories):\n    sns.lineplot(h.history['val_accuracy'], label=f'Fold {i}', ax=axes[0])\n\nplt.title(\"Validation Accuracy per Fold\")\nplt.xlabel(\"Epochs\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\n\n\n# Plot val_loss per fold\nfor i, h in enumerate(histories):\n    sns.lineplot(h.history['val_loss'], label=f'Fold {i}', ax=axes[1])\n\nplt.title(\"Validation Loss per Fold\")\nplt.xlabel(\"Epochs\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.show()","metadata":{"id":"8E95cmCnRfF3","outputId":"0f2c2f3d-b2fd-4db2-c975-6b3106200870","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\ny_oof_pred = oof_preds.argmax(axis=1)\n\n# Get the original gesture class names from the label encoder\ngesture_classes = num_to_label.values()\n\nprint(\n    classification_report(y, y_oof_pred, target_names=gesture_classes)\n)","metadata":{"id":"iiftDgFCoyLs","outputId":"8f2bbb52-d02a-4a06-cfee-d0efd2cfc43b","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# ✅ Submission","metadata":{"id":"6jdEkcehKLc9"}},{"cell_type":"code","source":"test_preds = np.zeros((len(X_test_padded), len(np.unique(y))))\n\nfor i,h in enumerate(histories):\n    partial_predictions = h.model.predict(X_test_padded)\n    test_preds += partial_predictions\n\ntest_preds.argmax(axis=1)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import polars as pl\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n\n    TEST_DF = sequence.to_pandas()\n    TEST_DF = add_physics_features(TEST_DF)\n\n    # Segment data by sequence_id (TEST_DF)\n    test_sequences = []\n    test_groups = []\n\n    for sequence_id, group_df in TEST_DF.groupby('sequence_id'):\n        test_sequences.append(group_df[features].values)\n        test_groups.append(group_df['subject'].iloc[0])\n\n    # pad_sequences (test)\n    X_test_padded = pad_sequences(\n        test_sequences,\n        maxlen=MAXLEN,\n        dtype='float32',\n        padding='post',\n        truncating='post',\n        value=0\n    )\n\n    # transform and reshape back\n    X_test_scaled = scaler.transform(X_test_padded.reshape(-1, n_features)).reshape(X_test_padded.shape)\n\n    # Initialize test_preds\n    test_preds = np.zeros((len(X_test_scaled), len(np.unique(y))))\n\n    # Loop through partial models\n    for i,h in enumerate(histories):\n        partial_predictions = h.model.predict(X_test_scaled)\n        test_preds += partial_predictions\n    \n        # Predict test\n        test_preds += model.predict(X_test_scaled)\n    \n    prediction = pd.Series(test_preds.argmax(axis=1)).map(num_to_label)\n    print(prediction)\n\n    return prediction[0]","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kaggle_evaluation.cmi_inference_server\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":{"id":"X6hIp-9HKSRN","outputId":"d6d12c6b-7def-4f3e-e372-0a4fb83f866a","trusted":true},"outputs":[],"execution_count":null}]}