{"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":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# <div style=\"padding:20px;color:white;margin:0;font-size:30px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#0077b6;overflow:hidden\">Competition Overview</div>\n\n## <span style=\"color: #0077b6;\">Definitions</span>\n\n***1. What are body-focused repetitive behavior (BFRB) disorders?*** \n\nBody-focused repetitive behavior (BFRB) disorders are a group of self-grooming actions that may harm your body. These include skin picking, hair pulling and nail biting, among others. BFRBs are hard to stop and you may not even know you do them. Therapy and medications may help. Body-focused repetitive behavior disorders, also known as BFRBs, are a group of conditions that cause you to perform self-grooming actions that unintentionally harm your body.\n\nMany people recognize BFRBs as nervous habits, like picking the skin around your nails or cracking your knuckles when you’re bored. But these behaviors are more than just something to do to pass the time. Research suggests your genetics, brain structure and emotions play a role in why these behaviors happen.\n\nYou may try to reduce how often you perform these behaviors but find it difficult to stop them. BFRBs come on with an intense, uncontrollable urge and may bring feelings of pleasure or satisfaction when you do them. Treatment options are available to help you if you need it. Body-focused repetitive behaviors (BFRBs), such as hair pulling, skin picking, and nail biting, are self-directed habits involving repetitive actions that, when frequent or intense, can cause physical harm and psychosocial challenges. These behaviors are commonly seen in anxiety disorders and obsessive-compulsive disorder (OCD), thus representing key indicators of mental health challenges.\n\n***2. What is a Wrist-Worn Wearable?***\n\nA wristband or fitness tracker is a wrist-worn device tailored to activity-tracking and healthmonitoring functions. Designs can vary from a simple pedometer for step counting to more\nsophisticated use cases such as sleep monitoring, location tracking and access control. **Wrist-worn devices** include smartwatches with a touchscreen display, while wristbands are mainly used for fitness tracking but do not contain a touchscreen display.\n\n***3. What is a proximity sensor?***\n\nProximity Sensors detect an object without touching it, and they therefore do not cause abrasion or damage to the object. Devices such as limit switches detect an object by contacting it, but Proximity Sensors are able to detect the presence of the object electrically, without having to touch it.\n\n## <span style=\"color: #0077b6;\">Competition Objective</span>\n\nThe goal of this competition is to **develop a predictive model** that distinguishes ***BFRB-like*** and ***non-BFRB-like*** activity using data from a variety of sensors collected via a wrist-worn device. Successfully disentangling these behaviors will improve the design and accuracy of wearable BFRB-detection devices, which are relevant to a wide range of mental illnesses, ultimately strengthening the tools available to support their treatment.\n\nWe will use *movement*, *temperature*, and *proximity sensor data* to differentiate between body-focused repetitive behaviors (BFRBs), like hair pulling, from non-BFRB everyday gestures, like adjusting glasses. \n\nThis competition challenges you to develop a predictive model capable of:\n\n1. **(1 - Binary):** distinguishing BFRB-like gestures from non-BFRB-like gestures\n2. **(2):** and distinguishing the specific type of BFRB-like gesture.\n\nCritically, when your model is evaluated, half of the test set will include only data from the IMU, while the other half will include all of the sensors on the Helios device (IMU, thermopiles, and time-of-flight sensors).\n\nYour solutions will have direct real-world impact, as the insights gained will inform design decisions about sensor selection — specifically whether the added expense and complexity of thermopile and time-of-flight sensors is justified by significant improvements in BFRB detection accuracy compared to an IMU alone. By helping us determine the added value of these thermopiles and time-of-flight sensors, your work will guide the development of better tools for detection and treatment of BFRBs.\n\n\n## <span style=\"color: #0077b6;\">Data Acquisition</span>\n\nTo investigate BFRBs, the Child Mind Institute has developed a wrist-worn device, Helios, designed to detect these behaviors. While many commercially available devices contain \n\n- Inertial Measurement Units (**IMUs**) to measure rotation and motion, the Helios watch integrates additional sensors,\n- including **5 thermopiles** (for detecting body heat) and\n- **5 time-of-flight sensors** (for detecting proximity). See the figure down for the placement of these sensors on the Helios device.\n\n<img width=\"290\" height=\"300\" alt=\"Image\" src=\"https://lh7-rt.googleusercontent.com/docsz/AD_4nXd38TabNLM493zfhsiCEqG0DFvonbT9Hn3QR6RO3Zqkt2HycgFVUWprQpGO9tPDvLWKUmJg4M50oSLtJ7aUGcj2cFAZpCTG8gsJ6liJFhmiWIh4H3U0GK-u0CvbJGMH2_3eMcyF?key=URtGEbMbDsqEdcEk8Y_O2PMu\" />\n\n\nWe conducted a research study to test the added value of these additional sensors for detecting BFRB-like movements. In the study, participants performed series of repeated gestures while wearing the Helios device:\n\n1. They began a transition from “rest” position and moved their hand to the appropriate location (Transition);\n\n1. They followed this with a short pause wherein they did nothing (Pause); and\n\n1. Finally they performed a gesture from either the BFRB-like or non-BFRB-like category of movements (Gesture; see Table below).\n\nEach participant performed 18 unique gestures (8 BFRB-like gestures and 10 non-BFRB-like gestures) in at least 1 of 4 different body-positions:\n\n- sitting\n- sitting leaning forward with their non-dominant arm resting on their leg\n- lying on their back\n- and lying on their side","metadata":{"_kg_hide-input":false}},{"cell_type":"markdown","source":"# <div style=\"padding:20px;color:white;margin:0;font-size:30px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#0077b6;overflow:hidden\">Load the Datasets</div>","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\nimport os\n#for dirname, _, filenames in os.walk('/kaggle/input'):\n    #for filename in filenames:\n        #print(os.path.join(dirname, filename))\n\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\nimport plotly.express as px\nfrom scipy.stats import gaussian_kde\nimport plotly.io as pio\nimport pandas as pd\nimport numpy as np\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\npio.renderers.default = 'notebook'\npio.renderers.default = 'iframe_connected'\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:18:06.380841Z","iopub.execute_input":"2025-08-12T11:18:06.381243Z","iopub.status.idle":"2025-08-12T11:18:06.396921Z","shell.execute_reply.started":"2025-08-12T11:18:06.381189Z","shell.execute_reply":"2025-08-12T11:18:06.396062Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_demographics_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv')\ntrain_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:18:06.765820Z","iopub.execute_input":"2025-08-12T11:18:06.766836Z","iopub.status.idle":"2025-08-12T11:18:38.856030Z","shell.execute_reply.started":"2025-08-12T11:18:06.766801Z","shell.execute_reply":"2025-08-12T11:18:38.854935Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_demographics_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:30.137380Z","iopub.execute_input":"2025-08-12T11:20:30.138246Z","iopub.status.idle":"2025-08-12T11:20:30.156324Z","shell.execute_reply.started":"2025-08-12T11:20:30.138218Z","shell.execute_reply":"2025-08-12T11:20:30.155446Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_demographics_df.describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:30.186034Z","iopub.execute_input":"2025-08-12T11:20:30.186604Z","iopub.status.idle":"2025-08-12T11:20:30.216207Z","shell.execute_reply.started":"2025-08-12T11:20:30.186559Z","shell.execute_reply":"2025-08-12T11:20:30.215414Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:30.347939Z","iopub.execute_input":"2025-08-12T11:20:30.348505Z","iopub.status.idle":"2025-08-12T11:20:30.370351Z","shell.execute_reply.started":"2025-08-12T11:20:30.348481Z","shell.execute_reply":"2025-08-12T11:20:30.369440Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:30.518436Z","iopub.execute_input":"2025-08-12T11:20:30.518771Z","iopub.status.idle":"2025-08-12T11:20:30.525085Z","shell.execute_reply.started":"2025-08-12T11:20:30.518740Z","shell.execute_reply":"2025-08-12T11:20:30.524123Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <div style=\"padding:20px;color:white;margin:0;font-size:30px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#0077b6;overflow:hidden\">Exploratory Data Analysis</div>","metadata":{}},{"cell_type":"markdown","source":"## <span style=\"color: #0077b6;\">Participant Demographics Overview</span>","metadata":{}},{"cell_type":"code","source":"label_maps = {\n    'adult_child': {0: 'Child', 1: 'Adult'},\n    'sex': {0: 'Female', 1: 'Male'},\n    'handedness': {0: 'Left-handed', 1: 'Right-handed'}\n}\n\ncolors = ['#0077b6', '#d3d3d3']\n\nfig = make_subplots(\n    rows=1, cols=3,\n    subplot_titles=[\"Adult vs Child\", \"Sex Assigned at Birth\", \"Dominant Hand\"]\n)\n\n\nfor i, var in enumerate(['adult_child', 'sex', 'handedness']):\n    counts = train_demographics_df[var].value_counts().sort_index()\n    total = counts.sum()\n    labels = [label_maps[var][k] for k in counts.index]\n    values = counts.values\n    percentages = [f\"{v} ({v/total:.1%})\" for v in values]\n\n    fig.add_trace(\n        go.Bar(\n            x=labels,\n            y=values,\n            marker_color=colors[:len(values)],\n            text=percentages,\n            textposition='outside'\n        ),\n        row=1, col=i+1\n    )\n\n    fig.update_xaxes(title_text=\"\", showgrid=False, row=1, col=i+1)\n    fig.update_yaxes(title_text=\"\", showgrid=False, row=1, col=i+1)\n\n\nfig.update_layout(\n    title_text=\"Distributions of Binary Demographic Variables\",\n    title_x=0.5,\n    showlegend=False,\n    height=500,\n    width=1000,\n    margin=dict(t=100),\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    yaxis_domain=[0.0, 0.85],  # shift plots downward\n    yaxis2_domain=[0.0, 0.85],\n    yaxis3_domain=[0.0, 0.85]\n)\n\nfig.update_yaxes(range=[0, 1.15 * train_demographics_df['adult_child'].value_counts().max()], row=1, col=1)\nfig.update_yaxes(range=[0, 1.15 * train_demographics_df['sex'].value_counts().max()], row=1, col=2)\nfig.update_yaxes(range=[0, 1.15 * train_demographics_df['handedness'].value_counts().max()], row=1, col=3)\n\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:30.953883Z","iopub.execute_input":"2025-08-12T11:20:30.954665Z","iopub.status.idle":"2025-08-12T11:20:31.039542Z","shell.execute_reply.started":"2025-08-12T11:20:30.954639Z","shell.execute_reply":"2025-08-12T11:20:31.038678Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"age_data = train_demographics_df['age'].dropna()\n\nnbins = 9\ncounts, bins = np.histogram(age_data, bins=nbins)\nbin_centers = 0.5 * (bins[1:] + bins[:-1])\ndensities = counts / sum(counts) / np.diff(bins)\n\nhist = go.Bar(\n    x=bin_centers,\n    y=densities,\n    width=np.diff(bins),\n    name='Age Histogram',\n    marker_color='#0077b6',\n    opacity=0.6,\n    hovertemplate=(\n        'Age bin: %{x:.1f}<br>' +\n        'Density: %{y:.4f}<br>' +\n        'Count: %{customdata}<extra></extra>'\n    ),\n    customdata=counts\n)\n\nkde = gaussian_kde(age_data)\nx_vals = np.linspace(age_data.min(), age_data.max(), 200)\nkde_line = go.Scatter(\n    x=x_vals,\n    y=kde(x_vals),\n    mode='lines',\n    name='KDE',\n    line=dict(color='black')\n)\n\nfig = go.Figure(data=[hist, kde_line])\nfig.update_layout(\n    title='Age Distribution of Participants',\n    xaxis_title='Age',\n    yaxis_title='Density',\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    bargap=0.1\n)\n\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:31.098372Z","iopub.execute_input":"2025-08-12T11:20:31.098787Z","iopub.status.idle":"2025-08-12T11:20:31.146545Z","shell.execute_reply.started":"2025-08-12T11:20:31.098760Z","shell.execute_reply":"2025-08-12T11:20:31.145835Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nheight_data = train_demographics_df['height_cm'].dropna()\n\n\nhist = go.Histogram(\n    x=height_data,\n    histnorm='probability density',\n    name='Height Histogram',\n    marker_color='#0077b6',\n    opacity=0.6,\n    hovertemplate=(\n        'Height: %{x:.1f} cm<br>' +\n        'Density: %{y:.4f}<extra></extra>'\n    )\n)\n\n\nkde = gaussian_kde(height_data)\nx_vals = np.linspace(height_data.min(), height_data.max(), 200)\nkde_line = go.Scatter(\n    x=x_vals,\n    y=kde(x_vals),\n    mode='lines',\n    name='KDE',\n    line=dict(color='black')\n)\n\n\nfig = go.Figure(data=[hist, kde_line])\nfig.update_layout(\n    title='Height Distribution of Participants',\n    xaxis_title='Height (cm)',\n    yaxis_title='Density',\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    bargap=0.1\n)\n\nfig.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:31.245466Z","iopub.execute_input":"2025-08-12T11:20:31.245831Z","iopub.status.idle":"2025-08-12T11:20:31.287926Z","shell.execute_reply.started":"2025-08-12T11:20:31.245805Z","shell.execute_reply":"2025-08-12T11:20:31.287268Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## <span style=\"color: #0077b6;\">Target Variable Analysis: BFRB vs. Non-BFRB Gestures</span>","metadata":{}},{"cell_type":"code","source":"sequence_type_summary = (\n    train_df.groupby('sequence_id')['sequence_type']\n    .agg(\n        num_unique='nunique',\n        unique_values=lambda x: x.unique().tolist()\n    )\n    .reset_index()\n)\n\nsequence_type_summary['single_value'] = sequence_type_summary['unique_values'].apply(\n    lambda x: x[0] if len(x) == 1 else None\n)\n\nsequence_type_summary = sequence_type_summary.rename(columns={\n    'sequence_id': 'Sequence ID',\n    'num_unique': 'Number of Unique Sequence Types',\n    'unique_values': 'List of Unique Sequence Types',\n    'single_value': 'Single Sequence Type (if only one)'\n})\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:32.505701Z","iopub.execute_input":"2025-08-12T11:20:32.506367Z","iopub.status.idle":"2025-08-12T11:20:32.945406Z","shell.execute_reply.started":"2025-08-12T11:20:32.506339Z","shell.execute_reply":"2025-08-12T11:20:32.944660Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"single_type_df = sequence_type_summary.dropna(subset=['Single Sequence Type (if only one)'])\n\ncounts = single_type_df['Single Sequence Type (if only one)'].value_counts().sort_index()\nlabels = counts.index\nvalues = counts.values\n\npercentages = values / values.sum() * 100\ntext_labels = [f\"{v} ({p:.1f}%)\" for v, p in zip(values, percentages)]\n\ncolors = ['#0077b6', '#d3d3d3']  # blue and light grey\n\nfig = go.Figure()\nfig.add_trace(go.Bar(\n    x=labels,\n    y=values,\n    text=text_labels,\n    textposition='outside',\n    marker_color=colors[:len(labels)],\n))\n\nfig.update_layout(\n    title='Distribution of Sequence Types: BFRB-like vs. Non-BFRB-like',\n    xaxis_title='Sequence Type',\n    yaxis_title='Count',\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    showlegend=False,\n    margin=dict(t=80),\n)\n\nfig.update_xaxes(showgrid=False, title_standoff=10)\nfig.update_yaxes(showgrid=True, gridcolor='white')\nfig.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:35.246717Z","iopub.execute_input":"2025-08-12T11:20:35.247250Z","iopub.status.idle":"2025-08-12T11:20:35.276148Z","shell.execute_reply.started":"2025-08-12T11:20:35.247225Z","shell.execute_reply":"2025-08-12T11:20:35.275306Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"gesture_type_summary = (\n    train_df.groupby('sequence_id')['gesture']\n    .agg(\n        num_unique='nunique',\n        unique_values=lambda x: x.unique().tolist()\n    )\n    .reset_index()\n)\n\ngesture_type_summary['single_value'] = gesture_type_summary['unique_values'].apply(\n    lambda x: x[0] if len(x) == 1 else None\n)\n\ngesture_type_summary = gesture_type_summary.rename(columns={\n    'sequence_id': 'Sequence ID',\n    'num_unique': 'Number of Unique Gestures',\n    'unique_values': 'List of Unique Gesture Types',\n    'single_value': 'Gesture Type (if only one)'\n})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:40.519493Z","iopub.execute_input":"2025-08-12T11:20:40.520120Z","iopub.status.idle":"2025-08-12T11:20:40.955784Z","shell.execute_reply.started":"2025-08-12T11:20:40.520096Z","shell.execute_reply":"2025-08-12T11:20:40.955096Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_df = pd.merge(sequence_type_summary, gesture_type_summary, on='Sequence ID', how='inner')\n\ncolumns_to_keep = ['Sequence ID', 'Single Sequence Type (if only one)', 'Gesture Type (if only one)']\ntarget_df = target_df[columns_to_keep]\n\ntarget_df = target_df.rename(columns={\n    'Sequence ID': 'Sequence ID',\n    'Single Sequence Type (if only one)': 'Sequence Type',\n    'Gesture Type (if only one)': 'Gesture'\n})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:40.956958Z","iopub.execute_input":"2025-08-12T11:20:40.957207Z","iopub.status.idle":"2025-08-12T11:20:40.972786Z","shell.execute_reply.started":"2025-08-12T11:20:40.957187Z","shell.execute_reply":"2025-08-12T11:20:40.971826Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ngesture_target_counts = (\n    target_df.groupby(['Gesture', 'Sequence Type'])\n    .size()\n    .reset_index(name='Count')\n)\n\nfig = px.bar(\n    gesture_target_counts,\n    x='Gesture',\n    y='Count',\n    color='Sequence Type',\n    barmode='group',\n    title='Gesture Distribution by Sequence Type (BFRB-like vs. Non-BFRB-like)',\n    color_discrete_map={'Target': '#0077b6', 'Non-Target': '#d3d3d3'}\n)\n\n\nfig.update_layout(\n    xaxis_tickangle=45,\n    plot_bgcolor='white',\n    paper_bgcolor='white'\n)\n\nfig.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:40.973670Z","iopub.execute_input":"2025-08-12T11:20:40.973933Z","iopub.status.idle":"2025-08-12T11:20:41.046130Z","shell.execute_reply.started":"2025-08-12T11:20:40.973914Z","shell.execute_reply":"2025-08-12T11:20:41.045066Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## <span style=\"color: #0077b6;\">Exploring Proximity Sensor Signals - ToF</span>","metadata":{}},{"cell_type":"code","source":"def plot_tof_sensor_animation(train_df, sequence_id, phase):\n    \"\"\"\n    Generate an animated heatmap for all 5 Time-of-Flight (ToF) sensors \n    during a specified phase of a specific sequence.\n\n    Parameters\n    ----------\n    train_df : pd.DataFrame\n        The full training dataframe containing sensor data.\n    sequence_id : str\n        The ID of the sequence to visualize (e.g., 'SEQ_000091').\n    phase : str\n        The phase of the sequence to visualize. Must be one of \n        ['Gesture', 'Pause', 'Transition'].\n\n    Returns\n    -------\n    fig : plotly.graph_objects.Figure\n        A Plotly figure object showing the animated heatmaps for each ToF sensor.\n    \"\"\"\n    seq_df = train_df[(train_df['sequence_id'] == sequence_id) & \n                      (train_df['phase'] == phase)].reset_index(drop=True)\n\n    for sensor_id in range(1, 6):\n        tof_cols = [f\"tof_{sensor_id}_v{i}\" for i in range(64)]\n        seq_df[tof_cols] = seq_df[tof_cols].replace(-1, np.nan)\n\n    fig = make_subplots(rows=1, cols=5, subplot_titles=[f\"ToF Sensor {i}\" for i in range(1, 6)])\n\n    for sensor_id in range(1, 6):\n        tof_cols = [f\"tof_{sensor_id}_v{i}\" for i in range(64)]\n        data_0 = seq_df.iloc[0][tof_cols].values.reshape(8, 8)\n        fig.add_trace(\n            go.Heatmap(\n                z=data_0,\n                zmin=0, zmax=254,\n                colorscale='Viridis',\n                showscale=False\n            ),\n            row=1, col=sensor_id\n        )\n\n    frames = []\n    for i in range(len(seq_df)):\n        frame_data = []\n        for sensor_id in range(1, 6):\n            tof_cols = [f\"tof_{sensor_id}_v{j}\" for j in range(64)]\n            heatmap = go.Heatmap(\n                z=seq_df.iloc[i][tof_cols].values.reshape(8, 8),\n                zmin=0, zmax=254,\n                colorscale='Viridis',\n                showscale=False\n            )\n            frame_data.append(heatmap)\n        frames.append(go.Frame(data=frame_data, name=str(i)))\n\n    fig.update_layout(\n        title=f\"All 5 ToF Sensors – Sequence {sequence_id} – Phase: {phase}\",\n        height=500,\n        width=1300,\n        updatemenus=[dict(\n            type='buttons',\n            showactive=False,\n            buttons=[dict(label='Play', method='animate', args=[None])]\n        )],\n        sliders=[dict(\n            steps=[dict(method='animate', args=[[f.name]], label=f.name) for f in frames],\n            active=0\n        )],\n        xaxis_showticklabels=False,\n        yaxis_showticklabels=False\n    )\n\n    fig.frames = frames\n    fig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:41.242102Z","iopub.execute_input":"2025-08-12T11:20:41.242410Z","iopub.status.idle":"2025-08-12T11:20:41.255424Z","shell.execute_reply.started":"2025-08-12T11:20:41.242386Z","shell.execute_reply":"2025-08-12T11:20:41.254338Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_tof_sensor_animation(train_df, 'SEQ_000091', 'Gesture')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:41.369553Z","iopub.execute_input":"2025-08-12T11:20:41.370491Z","iopub.status.idle":"2025-08-12T11:20:41.869253Z","shell.execute_reply.started":"2025-08-12T11:20:41.370459Z","shell.execute_reply":"2025-08-12T11:20:41.868264Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_tof_sensor_animation(train_df, 'SEQ_000091', 'Transition')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:45.913260Z","iopub.execute_input":"2025-08-12T11:20:45.913571Z","iopub.status.idle":"2025-08-12T11:20:46.418617Z","shell.execute_reply.started":"2025-08-12T11:20:45.913547Z","shell.execute_reply":"2025-08-12T11:20:46.417761Z"},"_kg_hide-input":false},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## <span style=\"color: #0077b6;\">Distribution of Sequence Lengths by Phase and Type</span>","metadata":{}},{"cell_type":"code","source":"sequence_lengths = train_df.groupby('sequence_id').size().reset_index(name='length')\n\nfig = px.histogram(\n    sequence_lengths,\n    x='length',\n    nbins=30,\n    title='Distribution of Sequence Lengths',\n    color_discrete_sequence=['#0077b6']\n)\n\nfig.update_layout(\n    xaxis_title='Sequence Length (number of rows)',\n    yaxis_title='Number of Sequences',\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    bargap=0.1\n)\n\nfig.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:53.953931Z","iopub.execute_input":"2025-08-12T11:20:53.954231Z","iopub.status.idle":"2025-08-12T11:20:54.046513Z","shell.execute_reply.started":"2025-08-12T11:20:53.954208Z","shell.execute_reply":"2025-08-12T11:20:54.045554Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nlengths_df = train_df.groupby(['sequence_id', 'sequence_type']).size().reset_index(name='length')\n\nfig = go.Figure()\n\n\ncolors = {'Target': '#0077b6', 'Non-Target': '#adb5bd'}\n\nfor seq_type in lengths_df['sequence_type'].unique():\n    subset = lengths_df[lengths_df['sequence_type'] == seq_type]['length']\n    \n    hist = go.Histogram(\n        x=subset,\n        name=f\"{seq_type} (hist)\",\n        histnorm='probability density',\n        opacity=0.5,\n        nbinsx=30,\n        marker_color=colors[seq_type],\n        showlegend=False\n    )\n    \n    kde = gaussian_kde(subset)\n    x_vals = np.linspace(subset.min(), subset.max(), 200)\n    kde_line = go.Scatter(\n        x=x_vals,\n        y=kde(x_vals),\n        mode='lines',\n        name=f\"{seq_type} (KDE)\",\n        line=dict(color=colors[seq_type])\n    )\n    \n    fig.add_trace(hist)\n    fig.add_trace(kde_line)\n\nfig.update_layout(\n    title='Sequence Length Distribution by Sequence Type',\n    xaxis_title='Sequence Length (number of rows)',\n    yaxis_title='Density',\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    bargap=0.1\n)\n\nfig.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:20:56.843850Z","iopub.execute_input":"2025-08-12T11:20:56.844158Z","iopub.status.idle":"2025-08-12T11:20:57.010042Z","shell.execute_reply.started":"2025-08-12T11:20:56.844134Z","shell.execute_reply":"2025-08-12T11:20:57.008254Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Filter to only Transition phase\ntransition_df = train_df[train_df['phase'] == 'Transition']\n\n# Compute sequence lengths with sequence_type\ntransition_lengths = transition_df.groupby(['sequence_id', 'sequence_type']).size().reset_index(name='length')\n\n# Plot\nfig = go.Figure()\ncolors = {'Target': '#0077b6', 'Non-Target': '#adb5bd'}\n\nfor seq_type in transition_lengths['sequence_type'].unique():\n    subset = transition_lengths[transition_lengths['sequence_type'] == seq_type]['length']\n    \n    hist = go.Histogram(\n        x=subset,\n        name=f\"{seq_type} (hist)\",\n        histnorm='probability density',\n        opacity=0.5,\n        nbinsx=30,\n        marker_color=colors[seq_type],\n        showlegend=False\n    )\n    \n    kde = gaussian_kde(subset)\n    x_vals = np.linspace(subset.min(), subset.max(), 200)\n    kde_line = go.Scatter(\n        x=x_vals,\n        y=kde(x_vals),\n        mode='lines',\n        name=f\"{seq_type} (KDE)\",\n        line=dict(color=colors[seq_type])\n    )\n    \n    fig.add_trace(hist)\n    fig.add_trace(kde_line)\n\nfig.update_layout(\n    title='Sequence Length Distribution by Sequence Type (Transition Phase)',\n    xaxis_title='Sequence Length',\n    yaxis_title='Density',\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    bargap=0.1\n)\n\nfig.show()\n","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-08-12T11:21:00.821858Z","iopub.execute_input":"2025-08-12T11:21:00.822185Z","iopub.status.idle":"2025-08-12T11:21:03.404040Z","shell.execute_reply.started":"2025-08-12T11:21:00.822158Z","shell.execute_reply":"2025-08-12T11:21:03.403191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Filter to only Gesture phase\ngesture_df = train_df[train_df['phase'] == 'Gesture']\n\n# Compute sequence lengths with sequence_type\ngesture_lengths = gesture_df.groupby(['sequence_id', 'sequence_type']).size().reset_index(name='length')\n\n# Plot\nfig = go.Figure()\n\nfor seq_type in gesture_lengths['sequence_type'].unique():\n    subset = gesture_lengths[gesture_lengths['sequence_type'] == seq_type]['length']\n    \n    hist = go.Histogram(\n        x=subset,\n        name=f\"{seq_type} (hist)\",\n        histnorm='probability density',\n        opacity=0.5,\n        nbinsx=30,\n        marker_color=colors[seq_type],\n        showlegend=False\n    )\n    \n    kde = gaussian_kde(subset)\n    x_vals = np.linspace(subset.min(), subset.max(), 200)\n    kde_line = go.Scatter(\n        x=x_vals,\n        y=kde(x_vals),\n        mode='lines',\n        name=f\"{seq_type} (KDE)\",\n        line=dict(color=colors[seq_type])\n    )\n    \n    fig.add_trace(hist)\n    fig.add_trace(kde_line)\n\nfig.update_layout(\n    title='Sequence Length Distribution by Sequence Type (Gesture Phase)',\n    xaxis_title='Sequence Length',\n    yaxis_title='Density',\n    plot_bgcolor='white',\n    paper_bgcolor='white',\n    bargap=0.1\n)\n\nfig.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T11:21:03.405012Z","iopub.execute_input":"2025-08-12T11:21:03.405267Z","iopub.status.idle":"2025-08-12T11:21:06.418246Z","shell.execute_reply.started":"2025-08-12T11:21:03.405246Z","shell.execute_reply":"2025-08-12T11:21:06.417289Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 1: imports & config\nimport numpy as np\nimport pandas as pd\nfrom scipy.stats import iqr\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import f1_score\nfrom sklearn.utils.class_weight import compute_class_weight\nimport lightgbm as lgb\n\nimport os\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server\n\nACC_AXES = [\"acc_x\", \"acc_y\", \"acc_z\"]\nROT_COLS = [\"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\nROT_AXES = [\"rot_x\", \"rot_y\", \"rot_z\"]  # correlations\nDEMOGRAPHIC_COLS = [\n    \"adult_child\", \"age\", \"sex\", \"handedness\",\n    \"height_cm\", \"shoulder_to_wrist_cm\", \"elbow_to_wrist_cm\"\n]\n\n# Globals filled after training\nMODEL = None\nFEATURE_COLS = None\nGLOBAL_MEDIANS = None\nCLASSES = None\nNON_TARGET_SET = None  # set of gesture names that are non-target (from train)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T12:44:50.443714Z","iopub.execute_input":"2025-08-12T12:44:50.448430Z","iopub.status.idle":"2025-08-12T12:44:50.556165Z","shell.execute_reply.started":"2025-08-12T12:44:50.448351Z","shell.execute_reply":"2025-08-12T12:44:50.555178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 2: features\n\ndef _time_aggs(arr: np.ndarray):\n    arr = np.asarray(arr, dtype=float)\n    if arr.size == 0:\n        return dict(mean=np.nan, std=np.nan, min=np.nan, max=np.nan, median=np.nan, iqr=np.nan)\n    return dict(\n        mean=float(np.nanmean(arr)),\n        std=float(np.nanstd(arr)),\n        min=float(np.nanmin(arr)),\n        max=float(np.nanmax(arr)),\n        median=float(np.nanmedian(arr)),\n        iqr=float(iqr(arr, nan_policy=\"omit\")),\n    )\n\ndef _pairwise_corr(a, b):\n    a = np.asarray(a, dtype=float); b = np.asarray(b, dtype=float)\n    if np.sum(~np.isnan(a)) < 2 or np.sum(~np.isnan(b)) < 2:\n        return 0.0\n    a = np.nan_to_num(a, nan=np.nanmean(a))\n    b = np.nan_to_num(b, nan=np.nanmean(b))\n    try:\n        return float(np.corrcoef(a, b)[0, 1])\n    except Exception:\n        return 0.0\n\ndef features_one_sequence(sdf: pd.DataFrame) -> dict:\n    out = {}\n    # Acc\n    if all(c in sdf.columns for c in ACC_AXES):\n        ax, ay, az = sdf[\"acc_x\"].values, sdf[\"acc_y\"].values, sdf[\"acc_z\"].values\n        for name, arr in zip(ACC_AXES, [ax, ay, az]):\n            for k, v in _time_aggs(arr).items():\n                out[f\"{name}_{k}\"] = v\n        res = np.sqrt(ax**2 + ay**2 + az**2)\n        for k, v in _time_aggs(res).items():\n            out[f\"acc_res_{k}\"] = v\n        out[\"acc_sma\"] = float(np.nanmean(np.abs(ax) + np.abs(ay) + np.abs(az)))\n        out[\"acc_corr_xy\"] = _pairwise_corr(ax, ay)\n        out[\"acc_corr_xz\"] = _pairwise_corr(ax, az)\n        out[\"acc_corr_yz\"] = _pairwise_corr(ay, az)\n\n    # Rot (quaternions)\n    if all(c in sdf.columns for c in ROT_COLS):\n        rw, rx, ry, rz = [sdf[c].values for c in ROT_COLS]\n        for name, arr in zip(ROT_COLS, [rw, rx, ry, rz]):\n            for k, v in _time_aggs(arr).items():\n                out[f\"{name}_{k}\"] = v\n        out[\"rot_corr_xy\"] = _pairwise_corr(rx, ry)\n        out[\"rot_corr_xz\"] = _pairwise_corr(rx, rz)\n        out[\"rot_corr_yz\"] = _pairwise_corr(ry, rz)\n\n    return out\n\ndef build_feature_table(sensor_df: pd.DataFrame, demo_df: pd.DataFrame) -> pd.DataFrame:\n    rows = []\n    for seq_id, sdf in sensor_df.groupby(\"sequence_id\"):\n        row = {\"sequence_id\": seq_id, \"subject\": sdf[\"subject\"].iloc[0]}\n        row.update(features_one_sequence(sdf))\n        if \"gesture\" in sdf.columns:\n            row[\"gesture\"] = str(sdf[\"gesture\"].iloc[-1])\n        rows.append(row)\n\n    feat = pd.DataFrame(rows)\n\n    merged = feat.merge(demo_df[[\"subject\"] + DEMOGRAPHIC_COLS], on=\"subject\", how=\"left\")\n    for col in DEMOGRAPHIC_COLS:\n        if col in merged.columns:\n            merged[col] = pd.to_numeric(merged[col], errors=\"coerce\")\n    return merged\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T12:45:00.538812Z","iopub.execute_input":"2025-08-12T12:45:00.539505Z","iopub.status.idle":"2025-08-12T12:45:00.576996Z","shell.execute_reply.started":"2025-08-12T12:45:00.539475Z","shell.execute_reply":"2025-08-12T12:45:00.575987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 3: metric helpers\n\ndef compute_non_target_set(train_df: pd.DataFrame) -> set:\n    \"\"\"\n    Use train metadata to determine which gestures are non-target.\n    sequence_type == 'non-target' -> those gestures are non-target.\n    \"\"\"\n    assert \"sequence_type\" in train_df.columns and \"gesture\" in train_df.columns\n    tmp = train_df[[\"sequence_id\", \"sequence_type\", \"gesture\"]].drop_duplicates()\n    non_targets = set(tmp.loc[tmp.sequence_type == \"non-target\", \"gesture\"].astype(str).unique())\n    return non_targets\n\ndef collapse_to_non_target(arr: np.ndarray, non_target_set: set, non_target_label=\"non_target\"):\n    out = []\n    for a in arr:\n        out.append(non_target_label if a in non_target_set else a)\n    return np.array(out, dtype=object)\n\ndef competition_score_fulllabels(y_true_labels, y_pred_labels, non_target_set: set, non_target_label=\"non_target\"):\n    \"\"\"\n    Binary F1 on target vs non-target, and Macro F1 on gesture where all non-target classes are collapsed into one.\n    \"\"\"\n    y_true_labels = np.asarray(y_true_labels, dtype=object)\n    y_pred_labels = np.asarray(y_pred_labels, dtype=object)\n\n    # Binary via set membership\n    to_bin = lambda arr: np.array([0 if a in non_target_set else 1 for a in arr], dtype=int)\n    y_true_bin = to_bin(y_true_labels)\n    y_pred_bin = to_bin(y_pred_labels)\n    binary_f1 = f1_score(y_true_bin, y_pred_bin, average=\"binary\")\n\n    # Macro-F1 on collapsed labels\n    y_true_c = collapse_to_non_target(y_true_labels, non_target_set, non_target_label)\n    y_pred_c = collapse_to_non_target(y_pred_labels, non_target_set, non_target_label)\n    macro_f1 = f1_score(y_true_c, y_pred_c, average=\"macro\")\n\n    return float(binary_f1), float(macro_f1), float((binary_f1 + macro_f1) / 2.0)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T12:45:10.706282Z","iopub.execute_input":"2025-08-12T12:45:10.706644Z","iopub.status.idle":"2025-08-12T12:45:10.718609Z","shell.execute_reply.started":"2025-08-12T12:45:10.706576Z","shell.execute_reply":"2025-08-12T12:45:10.717462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 4: load, features, CV train\n\n# 1) Load\ntrain_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ndemo_df  = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\n\n# 2) Determine non-target gesture set from train metadata\nNON_TARGET_SET = compute_non_target_set(train_df)\n\n# 3) Build features (no label collapsing)\nfeats = build_feature_table(train_df, demo_df)\n\n# 4) Prepare matrices\ndrop_cols = [\"sequence_id\",\"subject\",\"gesture\"]\nFEATURE_COLS = [c for c in feats.columns if c not in drop_cols]\nX = feats[FEATURE_COLS].replace([np.inf, -np.inf], np.nan)\ny = feats[\"gesture\"].astype(str).values\ngroups = feats[\"subject\"].values\n\n# 5) Encode all original gesture classes (BFRB + non-BFRB individually)\nCLASSES = np.unique(y)\nlabel2id = {c:i for i,c in enumerate(CLASSES)}\nid2label = {i:c for c,i in label2id.items()}\ny_int = np.array([label2id[v] for v in y], dtype=int)\n\n# 6) Class-balanced weights\ncw = compute_class_weight(\"balanced\", classes=np.arange(len(CLASSES)), y=y_int)\ncw_map = {i:w for i,w in enumerate(cw)}\n\n# 7) GroupKFold CV with per-fold median imputation\ngkf = GroupKFold(n_splits=5)\noof_proba = np.zeros((len(X), len(CLASSES)))\nfold_scores = []\n\nfor f, (tr, va) in enumerate(gkf.split(X, y_int, groups), 1):\n    X_tr, X_va = X.iloc[tr].copy(), X.iloc[va].copy()\n    y_tr, y_va = y_int[tr], y_int[va]\n\n    # per-fold medians (no leakage)\n    med = X_tr.median(numeric_only=True)\n    X_tr = X_tr.fillna(med); X_va = X_va.fillna(med)\n\n    sw = np.array([cw_map[i] for i in y_tr], dtype=float)\n\n    model = lgb.LGBMClassifier(\n        objective=\"multiclass\",\n        num_class=len(CLASSES),\n        n_estimators=1200,\n        learning_rate=0.03,\n        max_depth=-1,\n        subsample=0.9,\n        colsample_bytree=0.9,\n        reg_lambda=1.0,\n        random_state=42+f,\n        verbose=-1  # suppress logs in recent LightGBM versions\n    )\n    model.fit(\n        X_tr, y_tr,\n        sample_weight=sw,\n        eval_set=[(X_va, y_va)],\n        eval_metric=\"multi_logloss\"\n    )\n\n    proba = model.predict_proba(X_va)\n    oof_proba[va] = proba\n\n    y_va_pred = proba.argmax(axis=1)\n    y_va_pred_labels = np.array([id2label[i] for i in y_va_pred], dtype=object)\n    y_va_true_labels = np.array([id2label[i] for i in y_va], dtype=object)\n\n    b, m, s = competition_score_fulllabels(y_va_true_labels, y_va_pred_labels, NON_TARGET_SET, \"non_target\")\n    fold_scores.append((b, m, s))\n    print(f\"[Fold {f}] Binary-F1={b:.4f}  Macro-F1(collapse non-target)={m:.4f}  Mean={s:.4f}\")\n\n# OOF score\ny_oof = oof_proba.argmax(axis=1)\ny_oof_labels = np.array([id2label[i] for i in y_oof], dtype=object)\nb, m, s = competition_score_fulllabels(y, y_oof_labels, NON_TARGET_SET, \"non_target\")\nprint(f\"[OOF] Binary-F1={b:.4f}  Macro-F1(collapse non-target)={m:.4f}  Mean={s:.4f}\")\n\n# 8) Final model on all data with global medians (for inference)\nGLOBAL_MEDIANS = X.median(numeric_only=True).to_dict()\nX_full = X.fillna(GLOBAL_MEDIANS)\ny_full = y_int\nsw_full = np.array([cw_map[i] for i in y_full], dtype=float)\n\nMODEL = lgb.LGBMClassifier(\n    objective=\"multiclass\",\n    num_class=len(CLASSES),\n    n_estimators=1200,\n    learning_rate=0.03,\n    max_depth=-1,\n    subsample=0.9,\n    colsample_bytree=0.9,\n    reg_lambda=1.0,\n    random_state=123,\n    verbose=-1\n)\nMODEL.fit(X_full, y_full, sample_weight=sw_full)\n\nprint(\"Final model trained with full label set (BFRB + non-BFRB individually).\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T12:45:29.376021Z","iopub.execute_input":"2025-08-12T12:45:29.376444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 5: predict() + local gateway\n\ndef _time_aggs_infer(arr: np.ndarray):\n    # simple iqr without scipy to keep predict light\n    arr = np.asarray(arr, dtype=float)\n    if arr.size == 0:\n        return dict(mean=np.nan, std=np.nan, min=np.nan, max=np.nan, median=np.nan, iqr=np.nan)\n    q75, q25 = np.nanpercentile(arr, 75), np.nanpercentile(arr, 25)\n    return dict(\n        mean=float(np.nanmean(arr)),\n        std=float(np.nanstd(arr)),\n        min=float(np.nanmin(arr)),\n        max=float(np.nanmax(arr)),\n        median=float(np.nanmedian(arr)),\n        iqr=float(q75 - q25),\n    )\n\ndef _pairwise_corr_infer(a, b):\n    a = np.asarray(a, dtype=float); b = np.asarray(b, dtype=float)\n    if np.sum(~np.isnan(a)) < 2 or np.sum(~np.isnan(b)) < 2:\n        return 0.0\n    a = np.nan_to_num(a, nan=np.nanmean(a))\n    b = np.nan_to_num(b, nan=np.nanmean(b))\n    try:\n        return float(np.corrcoef(a, b)[0, 1])\n    except Exception:\n        return 0.0\n\ndef _features_one_sequence_infer(sdf: pd.DataFrame) -> dict:\n    out = {}\n    if all(c in sdf.columns for c in ACC_AXES):\n        ax, ay, az = sdf[\"acc_x\"].values, sdf[\"acc_y\"].values, sdf[\"acc_z\"].values\n        for name, arr in zip(ACC_AXES, [ax, ay, az]):\n            for k, v in _time_aggs_infer(arr).items():\n                out[f\"{name}_{k}\"] = v\n        res = np.sqrt(ax**2 + ay**2 + az**2)\n        for k, v in _time_aggs_infer(res).items():\n            out[f\"acc_res_{k}\"] = v\n        out[\"acc_sma\"] = float(np.nanmean(np.abs(ax) + np.abs(ay) + np.abs(az)))\n        out[\"acc_corr_xy\"] = _pairwise_corr_infer(ax, ay)\n        out[\"acc_corr_xz\"] = _pairwise_corr_infer(ax, az)\n        out[\"acc_corr_yz\"] = _pairwise_corr_infer(ay, az)\n\n    if all(c in sdf.columns for c in ROT_COLS):\n        rw, rx, ry, rz = [sdf[c].values for c in ROT_COLS]\n        for name, arr in zip(ROT_COLS, [rw, rx, ry, rz]):\n            for k, v in _time_aggs_infer(arr).items():\n                out[f\"{name}_{k}\"] = v\n        out[\"rot_corr_xy\"] = _pairwise_corr_infer(rx, ry)\n        out[\"rot_corr_xz\"] = _pairwise_corr_infer(rx, rz)\n        out[\"rot_corr_yz\"] = _pairwise_corr_infer(ry, rz)\n    return out\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Return a gesture string exactly from the full set of training gestures (BFRB + non-BFRB),\n    not collapsed. The competition's scoring will collapse non-targets for Macro-F1.\n    \"\"\"\n    seq_pd = sequence.to_pandas()\n    subject = seq_pd[\"subject\"].iloc[0]\n\n    feat = _features_one_sequence_infer(seq_pd)\n    row = pd.DataFrame([feat])\n\n    # Merge demographics\n    demo_pd = demographics.filter(pl.col(\"subject\") == subject).to_pandas()\n    if demo_pd.empty:\n        demo_vals = {k: np.nan for k in DEMOGRAPHIC_COLS}\n    else:\n        demo_vals = {k: pd.to_numeric(demo_pd.iloc[0].get(k, np.nan), errors=\"coerce\") for k in DEMOGRAPHIC_COLS}\n    for k, v in demo_vals.items():\n        row[k] = v\n\n    # Align\n    for c in FEATURE_COLS:\n        if c not in row.columns:\n            row[c] = np.nan\n    row = row[FEATURE_COLS].fillna(value={c: GLOBAL_MEDIANS.get(c, 0.0) for c in FEATURE_COLS})\n\n    # Predict a label from CLASSES\n    proba = MODEL.predict_proba(row.values)[0]\n    pred_idx = int(np.argmax(proba))\n    return CLASSES[pred_idx]\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Run the local gateway in notebook for sanity\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    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-12T09:50:22.599884Z","iopub.execute_input":"2025-08-12T09:50:22.600443Z","iopub.status.idle":"2025-08-12T09:50:23.700419Z","shell.execute_reply.started":"2025-08-12T09:50:22.600420Z","shell.execute_reply":"2025-08-12T09:50:23.699650Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}