{"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":"# Rotating IMU Accel. & Accounting for Gravity\n\nFor this competition, the IMU data is more consistently available than the other sensors (missing for some sequences), and tells us about the acceleration and orientation of the wrist-worn device.\n\nIf you haven't already, you might want to try [💬rotating your acceleration data points](https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data/discussion/583080), because:\n- The orientation ```rot_[w/x/y/z]``` data from the device should be relative to the real world as it uses a [gyroscope](https://fcon_1000.projects.nitrc.org/indi/cmi_healthy_brain_network/Competitions/Helios2025/IMU_Sensor.pdf).\n- The acceleration ```acc_[x/y/z]``` data from the device appears to be [💬recorded relative to itself, while including gravity](https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data/discussion/582696#3215558)\n\nWe'll look into how to perform this rotation, as well as how to account for gravity, in this notebook.","metadata":{}},{"cell_type":"markdown","source":"## Loading a Random Sequence\n\nHere, we import some libraries and pick a random sequence from the dataset to work with for the rest of the notebook.","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport random\nimport matplotlib.pyplot as plt\n\nDATASET_PATH = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\"\n\ndf = pd.read_csv(DATASET_PATH)\n\nsequence_ids = df[\"sequence_id\"].unique()\n\nchosen_sequence_id = random.choice(sequence_ids)\nprint(f\"randomly chose sequence: {chosen_sequence_id}\")\n\nfiltered_df = df[df['sequence_id'] == chosen_sequence_id]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:07.001594Z","iopub.execute_input":"2025-06-07T07:32:07.001952Z","iopub.status.idle":"2025-06-07T07:32:31.012922Z","shell.execute_reply.started":"2025-06-07T07:32:07.001917Z","shell.execute_reply":"2025-06-07T07:32:31.011647Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Isolating Relevant Columns\n\nHere we isolate the columns we care about:\n- ```sequence_counter``` is our time variable\n- ```acc_[x/y/z]``` gives us acceleration relative to the device\n- ```rot_[w/x/y/z]``` gives us orientation relative to the planet","metadata":{}},{"cell_type":"code","source":"RELEVANT_COLUMNS = [\"sequence_id\", \"sequence_counter\", \"acc_x\", \"acc_y\", \"acc_z\", \"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\nfiltered_df = filtered_df[RELEVANT_COLUMNS]\nfiltered_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:31.014539Z","iopub.execute_input":"2025-06-07T07:32:31.014890Z","iopub.status.idle":"2025-06-07T07:32:31.035296Z","shell.execute_reply.started":"2025-06-07T07:32:31.014858Z","shell.execute_reply":"2025-06-07T07:32:31.034085Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Plotting Acceleration & Orientation as Provided\n\nHere, we plot the acceleration & orientation values over time, as provided.\n\nDepending on which sequence has been selected at random, these plots may look noisy or quite interpretable.\nHopefully though, the plots illustrate how the change of orientation influences the reported acceleration values in a strange way. If you run the notebook a few times, you should be able to see this effect across different sequences (hence the random selection).\n\nAs provided, the features appear to be interacting, but we have the opportunity to isolate them.","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\n\nplt.plot(filtered_df['sequence_counter'], filtered_df['acc_x'], label='acc_x')\nplt.plot(filtered_df['sequence_counter'], filtered_df['acc_y'], label='acc_y')\nplt.plot(filtered_df['sequence_counter'], filtered_df['acc_z'], label='acc_z')\n\nplt.xlabel('Sequence Counter')\nplt.ylabel('Device-Relative Acceleration')\nplt.title(f'Device-Relative Acceleration Over Time\\n{chosen_sequence_id}')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:31.036866Z","iopub.execute_input":"2025-06-07T07:32:31.037215Z","iopub.status.idle":"2025-06-07T07:32:31.341927Z","shell.execute_reply.started":"2025-06-07T07:32:31.037184Z","shell.execute_reply":"2025-06-07T07:32:31.340010Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\n\nplt.plot(filtered_df['sequence_counter'], filtered_df['rot_w'], label='rot_w')\nplt.plot(filtered_df['sequence_counter'], filtered_df['rot_x'], label='rot_x')\nplt.plot(filtered_df['sequence_counter'], filtered_df['rot_y'], label='rot_y')\nplt.plot(filtered_df['sequence_counter'], filtered_df['rot_z'], label='rot_z')\n\nplt.xlabel('Sequence Counter')\nplt.ylabel('World-Relative Orientation')\nplt.ylim([-1, 1])\nplt.title(f'World-Relative Orientation Over Time\\n{chosen_sequence_id}')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:31.345111Z","iopub.execute_input":"2025-06-07T07:32:31.345448Z","iopub.status.idle":"2025-06-07T07:32:31.691370Z","shell.execute_reply.started":"2025-06-07T07:32:31.345425Z","shell.execute_reply":"2025-06-07T07:32:31.690147Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Rotating Acceleration Data Points\n\nHere we have use some utility functions to rotate the acceleration vectors by the orientation quarternion provided - ```rot_[w/x/y/z]```. \n\nThis is not something I have a great handle on but there is a [nice 3B1B video on precisely this topic](https://www.youtube.com/watch?v=zjMuIxRvygQ).\n\nAlternatively, you can use ```scipy.spatial.transform``` as detailed in [💬this post](https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data/discussion/583080).","metadata":{}},{"cell_type":"code","source":"def quaternion_conjugate(q):\n    \"\"\"Conjugate of a quaternion (w, x, y, z).\"\"\"\n    w, x, y, z = q\n    return np.array([w, -x, -y, -z])\n\ndef quaternion_multiply(q1, q2):\n    \"\"\"Multiply two quaternions.\"\"\"\n    w1, x1, y1, z1 = q1\n    w2, x2, y2, z2 = q2\n    return np.array([\n        w1*w2 - x1*x2 - y1*y2 - z1*z2,\n        w1*x2 + x1*w2 + y1*z2 - z1*y2,\n        w1*y2 - x1*z2 + y1*w2 + z1*x2,\n        w1*z2 + x1*y2 - y1*x2 + z1*w2\n    ])\n\ndef rotate_vector_by_quaternion(v, q):\n    \"\"\"Rotate 3D vector v by quaternion q.\"\"\"\n    v_quat = np.array([0, *v])\n    q_conj = quaternion_conjugate(q)\n    rotated = quaternion_multiply(quaternion_multiply(q, v_quat), q_conj)\n    return rotated[1:]  # return only x, y, z","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:31.692295Z","iopub.execute_input":"2025-06-07T07:32:31.692551Z","iopub.status.idle":"2025-06-07T07:32:31.699717Z","shell.execute_reply.started":"2025-06-07T07:32:31.692527Z","shell.execute_reply":"2025-06-07T07:32:31.698765Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Apply to all rows\nworld_acc = filtered_df.apply(\n    lambda row: rotate_vector_by_quaternion(\n        [row['acc_x'], row['acc_y'], row['acc_z']],\n        [row['rot_w'], row['rot_x'], row['rot_y'], row['rot_z']]\n    ),\n    axis=1, result_type='expand'\n)\n\n# Assign to new columns\nfiltered_df[['world_acc_x', 'world_acc_y', 'world_acc_z']] = world_acc\nfiltered_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:31.700934Z","iopub.execute_input":"2025-06-07T07:32:31.701199Z","iopub.status.idle":"2025-06-07T07:32:31.733949Z","shell.execute_reply.started":"2025-06-07T07:32:31.701177Z","shell.execute_reply":"2025-06-07T07:32:31.732772Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Plotting World-Relative Acceleration\n\nNow that we've applied this rotation to our acceleration vectors, we can plot acceleration along each of the real world [x/y/z] axis used by the gyroscope.\n\nI find these plots to be much cleaner and easier to interpret now that the data is decoupled from the device orientation.\n\nYou'll also notice that the world_acc_z line is influenced by gravity, which gives us (or at least me) some confidence that we're on the right track.","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\n\nplt.plot(filtered_df['sequence_counter'], filtered_df['world_acc_x'], label='world_acc_x')\nplt.plot(filtered_df['sequence_counter'], filtered_df['world_acc_y'], label='world_acc_y')\nplt.plot(filtered_df['sequence_counter'], filtered_df['world_acc_z'], label='world_acc_z')\n\nplt.xlabel('Sequence Counter')\nplt.ylabel('World-Relative Acceleration')\nplt.title(f'World-Relative Acceleration Over Time\\n{chosen_sequence_id}')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:31.734947Z","iopub.execute_input":"2025-06-07T07:32:31.735224Z","iopub.status.idle":"2025-06-07T07:32:32.044295Z","shell.execute_reply.started":"2025-06-07T07:32:31.735202Z","shell.execute_reply":"2025-06-07T07:32:32.043194Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Accounting for Gravity\n\nNow that we can identify the influence of gravity on a single variable, we have the option of accounting for it to align our acc_z with x and z. I think that the acceleration values should then reflect those introduced by the participant relative to the real-world.\n\nI've had gravity both increase and decrease the acc_z values, so I'm accounting for both below. I think that this is related to the device being upside-down somehow for some sequences, but it's also very possible I've made a mistake somewhere - please let me know if you spot something!","metadata":{}},{"cell_type":"code","source":"ACCOUNT_FOR_GRAVITY = True\nALWAYS_SUBTRACT = False and ACCOUNT_FOR_GRAVITY\nALWAYS_ADD = False and ACCOUNT_FOR_GRAVITY\n\nz_axis_avg_accel = filtered_df['world_acc_z'].mean()\nprint(f\"avg. z_axis accel: {z_axis_avg_accel:.3f}\")\n\nif ACCOUNT_FOR_GRAVITY and (ALWAYS_SUBTRACT or (z_axis_avg_accel > 6)):\n    filtered_df['world_acc_z'] -= 9.81 # subtracting the magnitude of gravity from z axis accel\n    \nif ACCOUNT_FOR_GRAVITY and (ALWAYS_ADD or (z_axis_avg_accel < -6)):\n    filtered_df['world_acc_z'] += 9.81 # adding the magnitude of gravity to z axis accel","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:32.045042Z","iopub.execute_input":"2025-06-07T07:32:32.045309Z","iopub.status.idle":"2025-06-07T07:32:32.053337Z","shell.execute_reply.started":"2025-06-07T07:32:32.045289Z","shell.execute_reply":"2025-06-07T07:32:32.052073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\n\nplt.plot(filtered_df['sequence_counter'], filtered_df['world_acc_x'], label='world_acc_x')\nplt.plot(filtered_df['sequence_counter'], filtered_df['world_acc_y'], label='world_acc_y')\nplt.plot(filtered_df['sequence_counter'], filtered_df['world_acc_z'], label='world_acc_z')\n\nplt.xlabel('Sequence Counter')\nplt.ylabel('World-Relative Acceleration')\nplt.title(f'World-Relative Acceleration Over Time (No Gravity) \\n{chosen_sequence_id}')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:32.054791Z","iopub.execute_input":"2025-06-07T07:32:32.055064Z","iopub.status.idle":"2025-06-07T07:32:32.353194Z","shell.execute_reply.started":"2025-06-07T07:32:32.055041Z","shell.execute_reply":"2025-06-07T07:32:32.352023Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Reconstructing a Tangled Signal\n\nNow that we've accounted for gravity, we have the option to entwine the sensors again to create something new. \n\nI think there's a stronger argument for using the real-world acceleration values, but device-relative acceleration without gravity sounds interesting too. \n\nRotating the acceleration vectors back onto the original device axes, the plot looks very similar (for me) to the world-relative plot.","metadata":{}},{"cell_type":"code","source":"def rotate_vector_back_to_device(v_world, q):\n    \"\"\"Rotate a world-frame vector back into the device frame using the inverse quaternion.\"\"\"\n    q_conj = quaternion_conjugate(q)\n    v_quat = np.array([0, *v_world])\n    rotated = quaternion_multiply(quaternion_multiply(q_conj, v_quat), q)\n    return rotated[1:]  # return x, y, z","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:32.355489Z","iopub.execute_input":"2025-06-07T07:32:32.355744Z","iopub.status.idle":"2025-06-07T07:32:32.361900Z","shell.execute_reply.started":"2025-06-07T07:32:32.355726Z","shell.execute_reply":"2025-06-07T07:32:32.360766Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Apply to each row\ndevice_acc = filtered_df.apply(\n    lambda row: rotate_vector_back_to_device(\n        [row['world_acc_x'], row['world_acc_y'], row['world_acc_z']],\n        [row['rot_w'], row['rot_x'], row['rot_y'], row['rot_z']]\n    ),\n    axis=1, result_type='expand'\n)\n\n# Save to new columns\nfiltered_df[['device_acc_x', 'device_acc_y', 'device_acc_z']] = device_acc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:32.362904Z","iopub.execute_input":"2025-06-07T07:32:32.363174Z","iopub.status.idle":"2025-06-07T07:32:32.390161Z","shell.execute_reply.started":"2025-06-07T07:32:32.363151Z","shell.execute_reply":"2025-06-07T07:32:32.389230Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\n\nplt.plot(filtered_df['sequence_counter'], filtered_df['device_acc_x'], label='device_acc_x')\nplt.plot(filtered_df['sequence_counter'], filtered_df['device_acc_y'], label='device_acc_y')\nplt.plot(filtered_df['sequence_counter'], filtered_df['device_acc_z'], label='device_acc_z')\n\nplt.xlabel('Sequence Counter')\nplt.ylabel('Device-Relative Acceleration')\nplt.title(f'Device-Relative Acceleration Over Time (No Gravity) \\n{chosen_sequence_id}')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T07:32:32.391401Z","iopub.execute_input":"2025-06-07T07:32:32.391734Z","iopub.status.idle":"2025-06-07T07:32:32.702180Z","shell.execute_reply.started":"2025-06-07T07:32:32.391706Z","shell.execute_reply":"2025-06-07T07:32:32.701028Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Conclusion\n\nI think it can be argued that the combination of device orientation changing and gravity not being accounted for disrupts the ```acc_[x/y/z]``` data as originally provided. When we account for gravity, both the world-specific and device-specific alignments look clean and interpretable. If the 'flipping' of gravity influence isn't a mistake of mine and is actually indicative of the participants pose in some way, it may be beneficial to retain gravity rather than remove it, in which case I would think plainly rotating the vectors onto the world-relative axes makes the most sense.\n\nIf you found this notebook interesting, I'd thoroughly recommend taking the time to look through @[tatamikenn](https://www.kaggle.com/tatamikenn)'s [📓analysis of the dataset](https://www.kaggle.com/code/tatamikenn/cmi-eda-visualize-sample-gestures), which is full of beautiful plots of the sequences. I realised their [💬post on rotating IMU accel](https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data/discussion/583080) predates this notebook while covering much of the same ground. As a result, I tried to focus instead on how gravity might be influencing the data.","metadata":{}}]}