{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":14051109,"sourceType":"datasetVersion","datasetId":8944024},{"sourceId":14053926,"sourceType":"datasetVersion","datasetId":8945893},{"sourceId":676130,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":512611,"modelId":527255}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CMI: Behavior Detection Model 1D-CNN 🌌🧠\nThis is my submission notebook for the Child Mind Institute \"Detect Behavior with Sensor Data\" Kaggle competition. The model training notebook [can be found on GitHub](https://github.com/tomragus/CMI-Behavior-Detection-1D-CNN/blob/main/CMI%20Behavior%20Detection%20Model%201D-CNN.ipynb)","metadata":{}},{"cell_type":"code","source":"# install protobuf 3.20.3 to resolve library conflict in tensorflow\n!pip install \"/kaggle/input/protobuf-3-20-3/protobuf-3.20.3-py2.py3-none-any.whl\"\n\nimport site\nimport importlib\nimportlib.reload(site)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T20:31:07.811082Z","iopub.execute_input":"2025-12-08T20:31:07.811465Z","iopub.status.idle":"2025-12-08T20:31:11.114497Z","shell.execute_reply.started":"2025-12-08T20:31:07.811439Z","shell.execute_reply":"2025-12-08T20:31:11.113574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import libraries\nimport os\nos.environ[\"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION\"] = \"python\"\n\nimport pandas as pd\nimport numpy as np\nimport polars as pl\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport sklearn\nimport joblib\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.preprocessing import RobustScaler\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences\nfrom scipy.spatial.transform import Rotation as R\nimport random\nimport warnings\nwarnings.simplefilter(action='ignore', category=pd.errors.DtypeWarning)\nwarnings.simplefilter(action='ignore', category=FutureWarning)\nwarnings.simplefilter(action='ignore', category=RuntimeWarning)\nimport kaggle_evaluation.cmi_inference_server\n\n# load model\nmodel_ALL = joblib.load('/kaggle/input/cmi-behavior-detection-cnn-1/tensorflow2/default/1/Behavior_Detection_CNN.pkl')\n\n# load train data\ntrain_placeholder = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntrain = train_placeholder.copy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T20:31:11.116086Z","iopub.execute_input":"2025-12-08T20:31:11.116383Z","iopub.status.idle":"2025-12-08T20:31:28.869904Z","shell.execute_reply.started":"2025-12-08T20:31:11.116359Z","shell.execute_reply":"2025-12-08T20:31:28.869196Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prepare the train data for Scaler Fitting","metadata":{}},{"cell_type":"code","source":"# prepare train dataset for scaler fitting\ntrain = train[train['subject'] != 'SUBJ_019262']\ntrain = train[train['subject'] != 'SUBJ_045235']\ntrain.drop_duplicates(inplace=True)\ntof_cols = [c for c in train.columns if c.startswith('tof_')]\ntrain[tof_cols] = train[tof_cols].replace([-1, 0], np.nan)\nrots = [\"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\ntrain[rots] = train[rots].fillna(0)\nthms = [\"thm_1\", \"thm_2\", \"thm_3\", \"thm_4\", \"thm_5\"]\nimputer = SimpleImputer(strategy='constant', fill_value=0)\ntrain[thms] = imputer.fit_transform(train[thms])\ntof_train = train.loc[:, \"tof_1_v0\" : \"tof_5_v63\"].columns\ntrain[tof_train] = train[tof_train].fillna(400)\ngesture_counts = ((train['phase'] == 'Gesture').groupby(train['sequence_id']).transform('sum'))\ntrain = train[gesture_counts >= 15].reset_index(drop=True)\nlefties = [\"SUBJ_002923\", \"SUBJ_013623\", \"SUBJ_019756\", \"SUBJ_028998\", \"SUBJ_032233\", \"SUBJ_032585\", \"SUBJ_039234\", \"SUBJ_041243\", \"SUBJ_055211\", \"SUBJ_061552\"]\nlefty_mask = train.subject.isin(lefties)\ntrain.loc[lefty_mask, 'acc_y'] = -train.loc[lefty_mask, 'acc_y']\ntrain.loc[lefty_mask, 'acc_z'] = -train.loc[lefty_mask, 'acc_z']\nq_original = train.loc[lefty_mask, ['rot_w', 'rot_x', 'rot_y', 'rot_z']].copy()\ntrain.loc[lefty_mask, 'rot_w'] = -q_original['rot_x']\ntrain.loc[lefty_mask, 'rot_x'] =  q_original['rot_w']\ntrain.loc[lefty_mask, 'rot_y'] = -q_original['rot_z']\ntrain.loc[lefty_mask, 'rot_z'] =  q_original['rot_y']\nacc_cols = [\"acc_x\", \"acc_y\", \"acc_z\"]\ntrain[acc_cols] = train[acc_cols].interpolate(method='linear', limit_direction='both')\nsequences = train['sequence_id'].unique().tolist()\nval_sequences = random.sample(sequences, int(len(sequences) * 0.2))\nvalidation = train[train['sequence_id'].isin(val_sequences)].copy()\ntrain = train[~train['sequence_id'].isin(val_sequences)].reset_index(drop=True)\ncolumns_to_drop = ['row_id', 'sequence_type', 'sequence_counter', 'subject', 'orientation', 'behavior', 'phase']\ntrain = train.drop(columns=[col for col in columns_to_drop if col in train.columns])\ntrain = train.copy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T20:31:28.870670Z","iopub.execute_input":"2025-12-08T20:31:28.870921Z","iopub.status.idle":"2025-12-08T20:31:48.984624Z","shell.execute_reply.started":"2025-12-08T20:31:28.870900Z","shell.execute_reply":"2025-12-08T20:31:48.984001Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define Feature Calculation Function","metadata":{}},{"cell_type":"code","source":"# consolidated calculation of features function\ndef calculate_sequence_features(group):\n    acc = group[['acc_x', 'acc_y', 'acc_z']].values\n    rot = group[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    time_step = 0.1\n    gravity_world = np.array([0, 0, 9.81])\n    new_feats = {}\n\n    # -> SIMPLE FEATURES\n    # linear acceleration (acceleration w/ gravity removed)\n    norms = np.linalg.norm(rot, axis=1)\n    valid_mask = norms > 1e-6\n    lin_acc = acc.copy()\n    \n    if np.any(valid_mask):\n        q_norm = rot[valid_mask] / norms[valid_mask][:, None]\n        r = R.from_quat(q_norm)\n        g_sensor = r.apply(gravity_world, inverse=False)\n        lin_acc[valid_mask] = acc[valid_mask] - g_sensor\n\n    new_feats['lin_acc_x'] = lin_acc[:, 0]\n    new_feats['lin_acc_y'] = lin_acc[:, 1]\n    new_feats['lin_acc_z'] = lin_acc[:, 2]\n\n    # magnitudes of acceleration\n    acc_mag = np.linalg.norm(acc, axis=1)\n    lin_acc_mag = np.linalg.norm(lin_acc, axis=1)\n    new_feats['acc_mag'] = acc_mag\n    new_feats['lin_acc_mag'] = lin_acc_mag\n\n    # jerk features (derivatives [gradient] of magnitudes)\n    new_feats['acc_mag_jerk'] = np.gradient(acc_mag, time_step)\n    new_feats['lin_acc_mag_jerk'] = np.gradient(lin_acc_mag, time_step)\n\n    # rotation angle\n    rot_w_clipped = np.clip(group['rot_w'].values, -1.0, 1.0)\n    new_feats['rot_angle'] = 2 * np.arccos(rot_w_clipped)\n\n    # -> COMPLEX FEATURES\n    q_curr = rot\n    q_prev = np.roll(rot, 1, axis=0)\n    q_prev[0] = [0, 0, 0, 1]\n    q_prev_conj = q_prev.copy()\n    q_prev_conj[:, :3] = -q_prev_conj[:, :3]\n    w1, x1, y1, z1 = q_curr[:, 3], q_curr[:, 0], q_curr[:, 1], q_curr[:, 2]\n    w2, x2, y2, z2 = q_prev_conj[:, 3], q_prev_conj[:, 0], q_prev_conj[:, 1], q_prev_conj[:, 2]\n    delta_w = w1*w2 - x1*x2 - y1*y2 - z1*z2\n    \n    # angular distance\n    delta_w_clip = np.clip(delta_w, -1.0, 1.0)\n    ang_dist = 2 * np.arccos(delta_w_clip)\n    ang_dist[0] = 0\n    new_feats['angular_distance'] = ang_dist\n    \n    # rotation angle velocity\n    new_feats['rot_angle_vel'] = ang_dist * 0.1\n\n    # angular velocity\n    delta_x = w1*x2 + x1*w2 + y1*z2 - z1*y2\n    delta_y = w1*y2 - x1*z2 + y1*w2 + z1*x2\n    delta_z = w1*z2 + x1*y2 - y1*x2 + z1*w2\n    \n    # normalize delta\n    delta_vec = np.stack([delta_x, delta_y, delta_z], axis=1)\n    dnorm = np.linalg.norm(delta_vec, axis=1)\n    angle_full = 2.0 * np.arctan2(dnorm, delta_w)\n    with np.errstate(divide='ignore', invalid='ignore'):\n        scale = angle_full / (time_step * dnorm)\n        scale[dnorm < 1e-12] = 2.0 / time_step\n    omega = delta_vec * scale[:, None]\n    omega[0] = [0, 0, 0]\n    new_feats['angular_vel_x'] = omega[:, 0]\n    new_feats['angular_vel_y'] = omega[:, 1]\n    new_feats['angular_vel_z'] = omega[:, 2]\n    return pd.DataFrame(new_feats, index=group.index)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T20:31:48.986467Z","iopub.execute_input":"2025-12-08T20:31:48.986706Z","iopub.status.idle":"2025-12-08T20:31:58.740709Z","shell.execute_reply.started":"2025-12-08T20:31:48.986689Z","shell.execute_reply":"2025-12-08T20:31:58.740142Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Fit the RobustScaler","metadata":{}},{"cell_type":"code","source":"# consolidate and add new features to train\nsets = [train]\nnames = ['train']\nNEW_IMU_FEATURES = ['lin_acc_x', 'lin_acc_y', 'lin_acc_z','acc_mag', 'lin_acc_mag', 'acc_mag_jerk', 'lin_acc_mag_jerk', 'rot_angle', 'angular_distance', 'rot_angle_vel', 'angular_vel_x', 'angular_vel_y', 'angular_vel_z']\n\nfor i, dataset in enumerate(sets):\n    dataset = dataset.copy()\n    results = [calculate_sequence_features(group) for _, group in dataset.groupby('sequence_id')]\n    all_new_features = pd.concat(results)\n    dataset = pd.concat([dataset, all_new_features], axis=1)\n    id_cols = ['sequence_id']\n    \n    if 'gesture' in dataset.columns:\n        id_cols.append('gesture')\n    original_cols = [col for col in dataset.columns if col not in id_cols and col not in NEW_IMU_FEATURES]\n    final_order = id_cols + NEW_IMU_FEATURES + original_cols\n    dataset = dataset[final_order]\n    \n    if names[i] == 'train':\n        train = dataset\n\n# define robust scaler\nscaler = RobustScaler() \n\n# fit scaler on train dataset\ntrain_cols_to_exclude = ['sequence_id', 'gesture']\ntrain_cols_to_scale = train.drop(columns=train_cols_to_exclude).columns\nscaler.fit(train[train_cols_to_scale])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Evaluate using the Inference Server","metadata":{}},{"cell_type":"code","source":"# define list of labels\nLABELS = [\n    'Above ear - pull hair',\n    'Cheek - pinch skin',\n    'Drink from bottle/cup',\n    'Eyebrow - pull hair',\n    'Eyelash - pull hair',\n    'Feel around in tray and pull out an object',\n    'Forehead - pull hairline',\n    'Forehead - scratch',\n    'Glasses on/off',\n    'Neck - pinch skin',\n    'Neck - scratch',\n    'Pinch knee/leg skin',\n    'Pull air toward your face',\n    'Scratch knee/leg skin',\n    'Text on phone',\n    'Wave hello',\n    'Write name in air',\n    'Write name on leg'\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T20:32:03.554652Z","iopub.execute_input":"2025-12-08T20:32:03.554890Z","iopub.status.idle":"2025-12-08T20:32:03.558890Z","shell.execute_reply.started":"2025-12-08T20:32:03.554872Z","shell.execute_reply":"2025-12-08T20:32:03.558184Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_single_sequence(df_seq: pd.DataFrame) -> np.ndarray:\n    df_seq = df_seq.copy()\n\n    # drop duplicate rows\n    df_seq.drop_duplicates(inplace=True)\n\n    # replace -1 and 0 with NaN in TOF columns\n    tof_cols = [c for c in df_seq.columns if c.startswith('tof_')]\n    if tof_cols:\n        df_seq[tof_cols] = df_seq[tof_cols].replace([-1, 0], np.nan)\n\n    # impute missing rot values with 0\n    rots = [\"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\n    existing_rots = [c for c in rots if c in df_seq.columns]\n    if existing_rots:\n        df_seq[existing_rots] = df_seq[existing_rots].fillna(0)\n\n    # fill missing temperature data with average by column (Constant 0 strategy)\n    thms = [\"thm_1\", \"thm_2\", \"thm_3\", \"thm_4\", \"thm_5\"]\n    existing_thms = [c for c in thms if c in df_seq.columns]\n    if existing_thms:\n        df_seq[existing_thms] = df_seq[existing_thms].fillna(0)\n\n    # fill all NaNs in time-of-flight (ToF) columns with 400\n    if tof_cols:\n        df_seq[tof_cols] = df_seq[tof_cols].fillna(400)\n\n    # linear interpolation for missing acceleration data\n    acc_cols = [\"acc_x\", \"acc_y\", \"acc_z\"]\n    if all(c in df_seq.columns for c in acc_cols):\n        df_seq[acc_cols] = df_seq[acc_cols].interpolate(method='linear', limit_direction='both')\n\n    # drop unnecessary columns\n    columns_to_drop = ['row_id', 'sequence_type', 'sequence_counter', 'subject', 'orientation', 'behavior', 'phase']\n    df_seq = df_seq.drop(columns=[col for col in columns_to_drop if col in df_seq.columns])\n\n    # calculate new features and concatenate\n    new_features_df = calculate_sequence_features(df_seq)\n    df_processed = pd.concat([df_seq, new_features_df], axis=1)\n    \n    # reorder columns\n    NEW_IMU_FEATURES = [\n        'lin_acc_x', 'lin_acc_y', 'lin_acc_z',\n        'acc_mag', 'lin_acc_mag', \n        'acc_mag_jerk', 'lin_acc_mag_jerk', \n        'rot_angle', 'angular_distance', 'rot_angle_vel', \n        'angular_vel_x', 'angular_vel_y', 'angular_vel_z'\n    ]\n    \n    id_cols = ['sequence_id']\n    if 'gesture' in df_processed.columns:\n        id_cols.append('gesture')\n    \n    original_cols = [\n        col for col in df_processed.columns \n        if col not in id_cols and col not in NEW_IMU_FEATURES\n    ]\n    \n    final_order = id_cols + NEW_IMU_FEATURES + original_cols\n    final_order = [c for c in final_order if c in df_processed.columns]\n    df_processed = df_processed[final_order]\n\n    # scale features\n    cols_to_exclude = ['sequence_id', 'gesture']\n    cols_to_scale = [c for c in df_processed.columns if c not in cols_to_exclude]\n    df_processed[cols_to_scale] = scaler.transform(df_processed[cols_to_scale])\n    \n    # pad sequence\n    if 'sequence_id' in df_processed.columns:\n        df_processed = df_processed.drop(columns=['sequence_id'])\n    if 'gesture' in df_processed.columns:\n        df_processed = df_processed.drop(columns=['gesture'])\n    padded_sequences = pad_sequences([df_processed.values], padding='post', dtype='float32', maxlen=700)\n\n    return padded_sequences","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T20:32:03.559665Z","iopub.execute_input":"2025-12-08T20:32:03.560004Z","iopub.status.idle":"2025-12-08T20:32:03.587337Z","shell.execute_reply.started":"2025-12-08T20:32:03.559962Z","shell.execute_reply":"2025-12-08T20:32:03.586526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# define prediction function for submission API\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    if isinstance(sequence, pl.DataFrame):\n        sequence = sequence.to_pandas()\n    input_data = process_single_sequence(sequence)\n    prediction_probs = model_ALL.predict(input_data, verbose=0) \n    predicted_index = np.argmax(prediction_probs, axis=1)[0]\n    predicted_label = LABELS[predicted_index]\n    return predicted_label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T20:32:03.587945Z","iopub.execute_input":"2025-12-08T20:32:03.588163Z","iopub.status.idle":"2025-12-08T20:32:03.608988Z","shell.execute_reply.started":"2025-12-08T20:32:03.588147Z","shell.execute_reply":"2025-12-08T20:32:03.608292Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# inference server API\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-12-08T20:32:03.609741Z","iopub.execute_input":"2025-12-08T20:32:03.609968Z","iopub.status.idle":"2025-12-08T20:32:08.213362Z","shell.execute_reply.started":"2025-12-08T20:32:03.609953Z","shell.execute_reply":"2025-12-08T20:32:08.212447Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Woohoo!","metadata":{}}]}