{"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"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np \nimport pandas as pd \nfrom sklearn.compose import ColumnTransformer\nfrom sklearn.model_selection import train_test_split\nimport torch\nfrom torch.nn.utils.rnn import pad_sequence\nfrom torch.utils.data import Dataset, DataLoader\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom sklearn.preprocessing import LabelEncoder, OneHotEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom scipy.spatial.transform import Rotation as R\nfrom sklearn.metrics import classification_report, accuracy_score, recall_score, f1_score, precision_score, confusion_matrix\nfrom scipy.ndimage import gaussian_filter1d\nimport pickle\nfrom scipy.interpolate import interp1d","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:17.967627Z","iopub.execute_input":"2025-07-14T10:40:17.967901Z","iopub.status.idle":"2025-07-14T10:40:24.657520Z","shell.execute_reply.started":"2025-07-14T10:40:17.967876Z","shell.execute_reply":"2025-07-14T10:40:24.656730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\ndf_demo = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:24.658153Z","iopub.execute_input":"2025-07-14T10:40:24.658431Z","iopub.status.idle":"2025-07-14T10:40:55.845721Z","shell.execute_reply.started":"2025-07-14T10:40:24.658416Z","shell.execute_reply":"2025-07-14T10:40:55.844907Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def preprocess_data_bfill(\n    df, \n    sequence_col, \n    columns_to_fill, \n    value_to_replace=-1, \n    final_fill_value=0,\n    add_missingness_flags=True\n):\n    df = df.copy()\n\n    # Replace known missing indicator with NaN\n    df[columns_to_fill] = df[columns_to_fill].replace(value_to_replace, np.nan)\n\n    new_columns = {}\n\n    tof_columns = [col for col in columns_to_fill if col.startswith('tof_')]\n    if add_missingness_flags and tof_columns:\n        new_columns['is_tof_missing'] = df[tof_columns].isna().any(axis=1).astype(int)\n\n    # Then fill missing values forward/backward within sequence groups\n    df[columns_to_fill] = (\n        df.groupby(sequence_col)[columns_to_fill]\n          .ffill()\n          .bfill()\n    )\n\n    # Final fill for any remaining NaNs\n    df[columns_to_fill] = df[columns_to_fill].fillna(final_fill_value)\n\n    # Add all new columns at once\n    if new_columns:\n        df = pd.concat([df, pd.DataFrame(new_columns, index=df.index)], axis=1)\n\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.847389Z","iopub.execute_input":"2025-07-14T10:40:55.847588Z","iopub.status.idle":"2025-07-14T10:40:55.853208Z","shell.execute_reply.started":"2025-07-14T10:40:55.847573Z","shell.execute_reply":"2025-07-14T10:40:55.852506Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_thm_statistics(df):\n    \"\"\"\n    Calculates statistical features across the 5 thermopile sensors for each time step.\n    This should be run after nulls have been handled.\n    \"\"\"\n    thm_cols = [c for c in df.columns if c.startswith('thm_')]\n\n    if not thm_cols:\n        return df\n\n    # --- FIX ---\n    # Replace sequential assignment with the efficient .assign() method\n    df_with_stats = df.assign(\n        thm_mean = df[thm_cols].mean(axis=1),\n        thm_std = df[thm_cols].std(axis=1),\n        thm_range = df[thm_cols].max(axis=1) - df[thm_cols].min(axis=1)\n    )\n    \n    return df_with_stats","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.853998Z","iopub.execute_input":"2025-07-14T10:40:55.854650Z","iopub.status.idle":"2025-07-14T10:40:55.925885Z","shell.execute_reply.started":"2025-07-14T10:40:55.854626Z","shell.execute_reply":"2025-07-14T10:40:55.925186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_mag_features(df):\n    df_with_mag = df.assign(\n        acc_mag = np.sqrt((df['acc_x'] ** 2) + (df['acc_y'] ** 2) + (df['acc_z'] ** 2)),\n        rot_mag = np.sqrt((df['rot_x'] ** 2) + (df['rot_y'] ** 2) + (df['rot_z'] ** 2))\n    )\n    return df_with_mag","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.926525Z","iopub.execute_input":"2025-07-14T10:40:55.926759Z","iopub.status.idle":"2025-07-14T10:40:55.939395Z","shell.execute_reply.started":"2025-07-14T10:40:55.926742Z","shell.execute_reply":"2025-07-14T10:40:55.938629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_jerk_features(df):\n    df_with_jerk = df.assign(\n        acc_jerk = df.groupby('sequence_id')['acc_mag'].diff().fillna(0),\n        rot_jerk = df.groupby('sequence_id')['rot_mag'].diff().fillna(0)\n    )\n    return df_with_jerk","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.940145Z","iopub.execute_input":"2025-07-14T10:40:55.940314Z","iopub.status.idle":"2025-07-14T10:40:55.950794Z","shell.execute_reply.started":"2025-07-14T10:40:55.940292Z","shell.execute_reply":"2025-07-14T10:40:55.950202Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def separate_gravity_and_body_acc(df):\n    \"\"\"\n    Uses the orientation quaternion to separate the gravity component.\n    Includes a robust temporary fill for missing quaternion data.\n    \"\"\"\n    rot_cols = ['rot_x', 'rot_y', 'rot_z', 'rot_w']\n    \n    # Your NaN handling logic is fine\n    temp_rot_df = df[rot_cols].groupby(df['sequence_id']).ffill().bfill()\n    temp_rot_df = temp_rot_df.fillna(0)\n\n    # Your rotation logic is fine\n    rotations = R.from_quat(temp_rot_df.values)\n    gravity_in_device_frame = rotations.inv().apply([0, 0, -9.81])\n    \n    # --- FIX ---\n    # Replace sequential assignment with the efficient .assign() method\n    df_with_features = df.assign(\n        g_x = gravity_in_device_frame[:, 0],\n        g_y = gravity_in_device_frame[:, 1],\n        g_z = gravity_in_device_frame[:, 2]\n    ).assign(\n        body_acc_x = lambda x: x['acc_x'] - x['g_x'],\n        body_acc_y = lambda x: x['acc_y'] - x['g_y'],\n        body_acc_z = lambda x: x['acc_z'] - x['g_z']\n    )\n    \n    return df_with_features","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.951556Z","iopub.execute_input":"2025-07-14T10:40:55.951804Z","iopub.status.idle":"2025-07-14T10:40:55.963611Z","shell.execute_reply.started":"2025-07-14T10:40:55.951782Z","shell.execute_reply":"2025-07-14T10:40:55.962961Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_tof_statistics(df):\n    \"\"\"\n    Vectorized: Computes statistical features for each of the 5 ToF sensors,\n    treating -1 as missing, and drops raw ToF columns.\n    \"\"\"\n    df = df.copy()  # <== THIS FIXES THE WARNING\n\n    all_raw_tof_cols = []\n    cols_list = []\n    # Create a NaN-masked copy for stats calculation\n    tof_all_cols = [c for c in df.columns if c.startswith('tof_')]\n    tof_df = df[tof_all_cols].replace(-1, np.nan)\n\n    for i in range(1, 6):\n        sensor_cols = [col for col in tof_all_cols if col.startswith(f'tof_{i}_')]\n        if not sensor_cols:\n            continue\n\n        all_raw_tof_cols.extend(sensor_cols)\n\n        df.loc[:, f'tof_{i}_mean'] = tof_df[sensor_cols].mean(axis=1)\n        df.loc[:, f'tof_{i}_std']  = tof_df[sensor_cols].std(axis=1)\n        df.loc[:, f'tof_{i}_min']  = tof_df[sensor_cols].min(axis=1)\n        df.loc[:, f'tof_{i}_max']  = tof_df[sensor_cols].max(axis=1)\n        cols_list.append([f'tof_{i}_mean', f'tof_{i}_std', f'tof_{i}_min', f'tof_{i}_max'])\n    # Fill any NaNs in the new stat columns\n    stat_cols = [c for c in df.columns if c.startswith('tof_') and any(k in c for k in ['mean', 'std', 'min', 'max'])]\n    df.loc[:, stat_cols] = df[stat_cols].fillna(0)\n\n    return df, cols_list","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.964347Z","iopub.execute_input":"2025-07-14T10:40:55.964593Z","iopub.status.idle":"2025-07-14T10:40:55.977941Z","shell.execute_reply.started":"2025-07-14T10:40:55.964569Z","shell.execute_reply":"2025-07-14T10:40:55.977300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_orientation_angles(df):\n    rot_cols = ['rot_x', 'rot_y', 'rot_z', 'rot_w']\n    rot_quats = df[rot_cols].fillna(0).values\n    euler = R.from_quat(rot_quats).as_euler('xyz', degrees=False)\n    \n    df['rot_roll'] = euler[:, 0]\n    df['rot_pitch'] = euler[:, 1]\n    df['rot_yaw'] = euler[:, 2]\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.980366Z","iopub.execute_input":"2025-07-14T10:40:55.980903Z","iopub.status.idle":"2025-07-14T10:40:55.993598Z","shell.execute_reply.started":"2025-07-14T10:40:55.980873Z","shell.execute_reply":"2025-07-14T10:40:55.993073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ParticipantVisibleError(Exception):\n    \"\"\"Errors raised here will be shown directly to the competitor.\"\"\"\n    pass\n\n\nclass CompetitionMetric:\n    \"\"\"Hierarchical macro F1 for the CMI 2025 challenge.\"\"\"\n    def __init__(self):\n        self.target_gestures = [\n            'Above ear - pull hair',\n            'Cheek - pinch skin',\n            'Eyebrow - pull hair',\n            'Eyelash - pull hair',\n            'Forehead - pull hairline',\n            'Forehead - scratch',\n            'Neck - pinch skin',\n            'Neck - scratch',\n        ]\n        self.non_target_gestures = [\n            'Write name on leg',\n            'Wave hello',\n            'Glasses on/off',\n            'Text on phone',\n            'Write name in air',\n            'Feel around in tray and pull out an object',\n            'Scratch knee/leg skin',\n            'Pull air toward your face',\n            'Drink from bottle/cup',\n            'Pinch knee/leg skin'\n        ]\n        self.all_classes = self.target_gestures + self.non_target_gestures\n\n    def calculate_hierarchical_f1(\n        self,\n        sol: pd.DataFrame,\n        sub: pd.DataFrame\n    ) -> float:\n\n        # Validate gestures\n        invalid_types = {i for i in sub['gesture'].unique() if i not in self.all_classes}\n        if invalid_types:\n            raise ParticipantVisibleError(\n                f\"Invalid gesture values in submission: {invalid_types}\"\n            )\n\n        # Compute binary F1 (Target vs Non-Target)\n        y_true_bin = sol['gesture'].isin(self.target_gestures).values\n        y_pred_bin = sub['gesture'].isin(self.target_gestures).values\n        f1_binary = f1_score(\n            y_true_bin,\n            y_pred_bin,\n            pos_label=True,\n            zero_division=0,\n            average='binary'\n        )\n\n        # Build multi-class labels for gestures\n        y_true_mc = sol['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        y_pred_mc = sub['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n\n        # Compute macro F1 over all gesture classes\n        f1_macro = f1_score(\n            y_true_mc,\n            y_pred_mc,\n            average='macro',\n            zero_division=0\n        )\n\n        return 0.5 * f1_binary + 0.5 * f1_macro\n\n\ndef score(\n    solution: pd.DataFrame,\n    submission: pd.DataFrame,\n    row_id_column_name: str\n) -> float:\n    # Validate required columns\n    for col in (row_id_column_name, 'gesture'):\n        if col not in solution.columns:\n            raise ParticipantVisibleError(f\"Solution file missing required column: '{col}'\")\n        if col not in submission.columns:\n            raise ParticipantVisibleError(f\"Submission file missing required column: '{col}'\")\n\n    metric = CompetitionMetric()\n    return metric.calculate_hierarchical_f1(solution, submission)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:55.994271Z","iopub.execute_input":"2025-07-14T10:40:55.994555Z","iopub.status.idle":"2025-07-14T10:40:56.004711Z","shell.execute_reply.started":"2025-07-14T10:40:55.994530Z","shell.execute_reply":"2025-07-14T10:40:56.004102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_rolling_features(df, cols, window=10):\n    for col in cols:\n        df[f'{col}_mean_{window}'] = df.groupby('sequence_id')[col].transform(lambda x: x.rolling(window, min_periods=1).mean())\n        df[f'{col}_std_{window}']  = df.groupby('sequence_id')[col].transform(lambda x: x.rolling(window, min_periods=1).std().fillna(0))\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.005334Z","iopub.execute_input":"2025-07-14T10:40:56.005551Z","iopub.status.idle":"2025-07-14T10:40:56.018317Z","shell.execute_reply.started":"2025-07-14T10:40:56.005530Z","shell.execute_reply":"2025-07-14T10:40:56.017575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_feature_engineering(df):\n    tof_cols = [col for col in df.columns if col.startswith('tof_')]  # All ToF columns including grids\n    thm_cols = [col for col in df.columns if col.startswith('thm_')]\n    rot_cols = [col for col in df.columns if col.startswith('rot_')]\n    acc_cols = [col for col in df.columns if col.startswith('acc_')]\n    \n    imu_cols = thm_cols + rot_cols + acc_cols \n    \n    all_cols = rot_cols + acc_cols + thm_cols + tof_cols\n    initial_cols = set(df.columns)\n    df = preprocess_data_bfill(df, 'sequence_id', all_cols)\n    df = create_thm_statistics(df)\n    df = add_mag_features(df)\n    df = add_jerk_features(df)\n    df = separate_gravity_and_body_acc(df)\n    df, tof_created_list = create_tof_statistics(df)   # Adds mean, std, etc for ToF\n    \n    df = add_orientation_angles(df)\n    df = add_rolling_features(df, ['acc_mag', 'rot_mag', 'acc_jerk'], window=10)\n    \n    # Drop metadata columns only, keep raw ToF grids for Conv2D input!\n    df = df.drop(columns=['orientation', 'behavior', 'phase', 'subject', 'sequence_type', 'row_id'])\n    new_cols = list(set(df.columns) - initial_cols)\n    return df, new_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.019041Z","iopub.execute_input":"2025-07-14T10:40:56.019271Z","iopub.status.idle":"2025-07-14T10:40:56.030287Z","shell.execute_reply.started":"2025-07-14T10:40:56.019245Z","shell.execute_reply":"2025-07-14T10:40:56.029617Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"try_df, tof_created_cols = run_feature_engineering(df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.030970Z","iopub.execute_input":"2025-07-14T10:40:56.031263Z","iopub.status.idle":"2025-07-14T10:40:56.206430Z","shell.execute_reply.started":"2025-07-14T10:40:56.031241Z","shell.execute_reply":"2025-07-14T10:40:56.205874Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"try_df.columns","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.207277Z","iopub.execute_input":"2025-07-14T10:40:56.207568Z","iopub.status.idle":"2025-07-14T10:40:56.213250Z","shell.execute_reply.started":"2025-07-14T10:40:56.207541Z","shell.execute_reply":"2025-07-14T10:40:56.212738Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tof_created_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.214065Z","iopub.execute_input":"2025-07-14T10:40:56.214266Z","iopub.status.idle":"2025-07-14T10:40:56.224314Z","shell.execute_reply.started":"2025-07-14T10:40:56.214252Z","shell.execute_reply":"2025-07-14T10:40:56.223735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"thm_cols = [col for col in try_df.columns if col.startswith('thm_')]\nrot_cols = [col for col in try_df.columns if col.startswith('rot_')]\nacc_cols = [col for col in try_df.columns if col.startswith('acc_')]\nimu_cols = thm_cols + rot_cols + acc_cols \ntof_cols = [col for col in try_df.columns if col.startswith('tof_')]\ntof_created_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.225165Z","iopub.execute_input":"2025-07-14T10:40:56.225395Z","iopub.status.idle":"2025-07-14T10:40:56.236177Z","shell.execute_reply.started":"2025-07-14T10:40:56.225372Z","shell.execute_reply":"2025-07-14T10:40:56.235633Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"imu_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.236734Z","iopub.execute_input":"2025-07-14T10:40:56.236982Z","iopub.status.idle":"2025-07-14T10:40:56.246300Z","shell.execute_reply.started":"2025-07-14T10:40:56.236965Z","shell.execute_reply":"2025-07-14T10:40:56.245697Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def pad_and_truncate_sequence(data, max_len):\n    \"\"\"\n    Pads short sequences at the beginning (pre-padding) and truncates \n    long sequences from the beginning (pre-truncating).\n    \"\"\"\n    # Check for empty data to avoid errors with .shape\n    if data.shape[0] == 0:\n        return np.zeros((max_len, data.shape[1]), dtype=data.dtype)\n        \n    seq_len, num_features = data.shape\n    \n    if seq_len > max_len:\n        # Truncate from the beginning\n        return data[-max_len:, :]\n    elif seq_len < max_len:\n        # Pad at the beginning\n        pad_len = max_len - seq_len\n        pad = np.zeros((pad_len, num_features), dtype=data.dtype)\n        return np.concatenate([pad, data], axis=0)\n    else:\n        # If length is already correct\n        return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.246982Z","iopub.execute_input":"2025-07-14T10:40:56.247168Z","iopub.status.idle":"2025-07-14T10:40:56.258224Z","shell.execute_reply.started":"2025-07-14T10:40:56.247147Z","shell.execute_reply":"2025-07-14T10:40:56.257581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def simulate_motion_drift(data, max_drift = 0.2):\n    drift = np.linspace(0, np.random.uniform(-max_drift, max_drift), data.shape[0])\n    return data + drift[:, None]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.258929Z","iopub.execute_input":"2025-07-14T10:40:56.259148Z","iopub.status.idle":"2025-07-14T10:40:56.271750Z","shell.execute_reply.started":"2025-07-14T10:40:56.259129Z","shell.execute_reply":"2025-07-14T10:40:56.271079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def time_warp(data, sigma = 0.2):\n    orig_steps = np.arange(data.shape[0])\n    random_curve = np.cumsum(np.random.normal(0, sigma, data.shape[0]))\n    warped_steps = orig_steps + random_curve\n    warped_steps = np.clip(warped_steps, 0, data.shape[0] - 1)\n    interp_func = interp1d(orig_steps, data, axis = 0, fill_value = 'extrapolate')\n    return interp_func(warped_steps)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.272537Z","iopub.execute_input":"2025-07-14T10:40:56.272856Z","iopub.status.idle":"2025-07-14T10:40:56.281513Z","shell.execute_reply.started":"2025-07-14T10:40:56.272823Z","shell.execute_reply":"2025-07-14T10:40:56.280781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def temperature_drift(data):\n    drift = np.linspace(0, np.random.uniform(-1, 1), data.shape[0])\n    return data + drift[:, None]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.282181Z","iopub.execute_input":"2025-07-14T10:40:56.282956Z","iopub.status.idle":"2025-07-14T10:40:56.291405Z","shell.execute_reply.started":"2025-07-14T10:40:56.282935Z","shell.execute_reply":"2025-07-14T10:40:56.290796Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def temporal_smoothing(data, sigma = 1):\n    return gaussian_filter1d(data, sigma = sigma, axis = 0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.292211Z","iopub.execute_input":"2025-07-14T10:40:56.292425Z","iopub.status.idle":"2025-07-14T10:40:56.302627Z","shell.execute_reply.started":"2025-07-14T10:40:56.292410Z","shell.execute_reply":"2025-07-14T10:40:56.301873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def channel_dropout(data, drop_rate=0.3):\n    num_channels = data.shape[1]\n    drop_indices = np.random.choice(num_channels, size=int(num_channels * drop_rate), replace=False)\n    data[:, drop_indices] = 0  # or np.nan\n    return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.303451Z","iopub.execute_input":"2025-07-14T10:40:56.303731Z","iopub.status.idle":"2025-07-14T10:40:56.311824Z","shell.execute_reply.started":"2025-07-14T10:40:56.303706Z","shell.execute_reply":"2025-07-14T10:40:56.311310Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_noise(data, noise_level=0.01):\n    \"\"\"Adds Gaussian noise to the data.\"\"\"\n    noise = np.random.normal(0, noise_level, data.shape)\n    return data + noise","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.312494Z","iopub.execute_input":"2025-07-14T10:40:56.312693Z","iopub.status.idle":"2025-07-14T10:40:56.322511Z","shell.execute_reply.started":"2025-07-14T10:40:56.312673Z","shell.execute_reply":"2025-07-14T10:40:56.321801Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_imu_augmentations(data):\n    data = add_noise(data, 0.03)\n    data = random_axis_flip_swap(data)\n    data = simulate_motion_drift(data)\n    data = time_warp(data)\n    return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.323269Z","iopub.execute_input":"2025-07-14T10:40:56.323520Z","iopub.status.idle":"2025-07-14T10:40:56.333013Z","shell.execute_reply.started":"2025-07-14T10:40:56.323495Z","shell.execute_reply":"2025-07-14T10:40:56.332407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def random_axis_flip_swap(data):\n    if np.random.rand() <0.5:\n        data[:, [0, 1]] = data[:, [1, 0]]\n    if np.random.rand() < 0.5:\n        data[:, 2] *= -1\n    return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.333704Z","iopub.execute_input":"2025-07-14T10:40:56.333988Z","iopub.status.idle":"2025-07-14T10:40:56.345747Z","shell.execute_reply.started":"2025-07-14T10:40:56.333972Z","shell.execute_reply":"2025-07-14T10:40:56.345154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_thermopile_augmentations(data):\n    data = temperature_drift(data)\n    data = channel_dropout(data, drop_rate=0.3)\n    data = temporal_smoothing(data, sigma=1)\n    return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.349220Z","iopub.execute_input":"2025-07-14T10:40:56.349652Z","iopub.status.idle":"2025-07-14T10:40:56.355944Z","shell.execute_reply.started":"2025-07-14T10:40:56.349626Z","shell.execute_reply":"2025-07-14T10:40:56.355335Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GestureDataset(Dataset):\n    def __init__(self, df, imu_cols, feat_eng_cols, tof_cols, thermopile_cols,\n                 label_encoder=None, max_seq_len=128, is_train=False):\n        self.df = df\n        self.imu_cols = imu_cols\n        self.feat_eng_cols = feat_eng_cols\n        self.tof_cols = tof_cols\n        self.thermopile_cols = thermopile_cols\n        self.sequence_ids = df['sequence_id'].unique()\n        self.label_encoder = label_encoder\n        self.max_seq_len = max_seq_len\n        self.is_train = is_train\n\n        self.has_labels = 'gesture' in df.columns\n        self.label_indices = None\n        \n        if self.has_labels:\n            if self.label_encoder is None:\n                raise ValueError(\"A fitted label_encoder must be provided when labels are present.\")\n            labels = df.groupby('sequence_id')['gesture'].first().reindex(self.sequence_ids)\n            self.label_indices = self.label_encoder.transform(labels)\n\n    def __len__(self):\n        return len(self.sequence_ids)\n\n    def __getitem__(self, idx):\n        seq_id = self.sequence_ids[idx]\n        seq_df = self.df[self.df['sequence_id'] == seq_id]\n\n        imu_data = seq_df[self.imu_cols].values\n        feat_eng_data = seq_df[self.feat_eng_cols].values\n        tof_data = seq_df[self.tof_cols].values\n        thermopile_data = seq_df[self.thermopile_cols].values\n\n        if self.is_train:\n            # ✅ FIX: Call augmentation correctly\n            imu_data = apply_imu_augmentations(imu_data)\n            thermopile_data = apply_thermopile_augmentations(thermopile_data)\n\n        # Pad/truncate\n        imu_padded = pad_and_truncate_sequence(imu_data, self.max_seq_len)\n        feat_eng_padded = pad_and_truncate_sequence(feat_eng_data, self.max_seq_len)\n        tof_padded = pad_and_truncate_sequence(tof_data, self.max_seq_len)\n        thermopile_padded = pad_and_truncate_sequence(thermopile_data, self.max_seq_len)\n\n        tof_padded = tof_padded.reshape(self.max_seq_len, 5, 8, 8)\n        combined_features = np.concatenate([imu_padded, feat_eng_padded, thermopile_padded], axis=1)\n        combined_tensor = torch.tensor(combined_features, dtype=torch.float32)\n        tof_tensor = torch.tensor(tof_padded, dtype=torch.float32)\n\n        if self.has_labels:\n            label = torch.tensor(self.label_indices[idx], dtype=torch.long)\n            return combined_tensor, tof_tensor, label, seq_id\n        else:\n            return combined_tensor, tof_tensor, seq_id","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.356535Z","iopub.execute_input":"2025-07-14T10:40:56.356691Z","iopub.status.idle":"2025-07-14T10:40:56.370140Z","shell.execute_reply.started":"2025-07-14T10:40:56.356678Z","shell.execute_reply":"2025-07-14T10:40:56.369454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def evaluate_model(model, val_loader, idx_to_label, criterion, device):\n    model.eval()\n    val_loss = 0\n    correct = 0\n    total = 0\n    y_true = []\n    y_pred = []\n    seq_ids = [] # ✅ FIX: Initialize list to store sequence IDs\n\n    with torch.no_grad():\n        # ✅ FIX: Swap 'sensor_data' and 'tof_data' to match dataset output order\n        for sensor_data, tof_data, labels, ids in val_loader:\n            tof_data = tof_data.to(device)\n            sensor_data = sensor_data.to(device)\n            labels = labels.to(device)\n            \n            # ✅ FIX: Pass arguments in the correct order\n            val_outputs = model(sensor_data, tof_data)\n            \n            val_loss += criterion(val_outputs, labels).item() * labels.size(0)\n\n            preds = torch.argmax(val_outputs, dim=1)\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n\n            y_true.extend(labels.cpu().numpy())\n            y_pred.extend(preds.cpu().numpy())\n            seq_ids.extend(ids) # Collect the actual sequence IDs\n\n    avg_val_loss = val_loss / total\n    val_acc = correct / total\n\n    # ✅ FIX: Use the actual sequence IDs for scoring instead of a generated range\n    df_true = pd.DataFrame({'sequence_id': seq_ids, 'gesture': [idx_to_label[i] for i in y_true]})\n    df_pred = pd.DataFrame({'sequence_id': seq_ids, 'gesture': [idx_to_label[i] for i in y_pred]})\n\n    hierarchical_f1 = score(df_true, df_pred, row_id_column_name='sequence_id')\n\n    return avg_val_loss, val_acc, hierarchical_f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.371083Z","iopub.execute_input":"2025-07-14T10:40:56.371475Z","iopub.status.idle":"2025-07-14T10:40:56.385741Z","shell.execute_reply.started":"2025-07-14T10:40:56.371453Z","shell.execute_reply":"2025-07-14T10:40:56.384980Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Conv1DBranch(nn.Module):\n    \"\"\"\n    1D CNN branch for IMU data.\n    MODIFIED: This now outputs a sequence of features, not a flattened vector.\n    \"\"\"\n    def __init__(self, input_features, num_filters=64, dropout_rate=0.2):\n        super(Conv1DBranch, self).__init__()\n        self.block1 = nn.Sequential(\n            nn.Conv1d(in_channels=input_features, out_channels=num_filters, kernel_size=3, padding='same'),\n            nn.BatchNorm1d(num_filters),\n            nn.LeakyReLU(0.1)\n        )\n        self.pool1 = nn.MaxPool1d(kernel_size=2, stride=2)\n        \n        self.block2 = nn.Sequential(\n            nn.Conv1d(in_channels=num_filters, out_channels=num_filters * 2, kernel_size=3, padding='same'),\n            nn.BatchNorm1d(num_filters * 2),\n            nn.LeakyReLU(0.1)\n        )\n        self.pool2 = nn.MaxPool1d(kernel_size=2, stride=2)\n        self.dropout = nn.Dropout(dropout_rate)\n\n    def forward(self, x):\n        \"\"\"\n        Args:\n            x (Tensor): Input tensor of shape (batch_size, sequence_length, features).\n        Returns:\n            Tensor: Output sequence of shape (batch_size, new_sequence_length, features).\n        \"\"\"\n        # Conv1d expects (batch_size, features, sequence_length)\n        x = x.permute(0, 2, 1)\n        x = self.block1(x)\n        x = self.pool1(x)\n        x = self.block2(x)\n        x = self.pool2(x)\n        # Permute back to (batch_size, sequence_length, features) for the GRU\n        x = x.permute(0, 2, 1)\n        x = self.dropout(x)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.386337Z","iopub.execute_input":"2025-07-14T10:40:56.386519Z","iopub.status.idle":"2025-07-14T10:40:56.399771Z","shell.execute_reply.started":"2025-07-14T10:40:56.386505Z","shell.execute_reply":"2025-07-14T10:40:56.399119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Conv2DBranch(nn.Module):\n    \"\"\"\n    2D CNN branch for ToF data.\n    This architecture assumes the ToF data per time step is a (C, H, W) image.\n    MODIFIED: Applies 2D CNN and then 1D pooling to match sequence lengths.\n    \"\"\"\n    def __init__(self, in_channels=5, num_filters=64, dropout_rate=0.3):\n        super(Conv2DBranch, self).__init__()\n        self.block1 = nn.Sequential(\n            nn.Conv2d(in_channels=in_channels, out_channels=num_filters, kernel_size=3, padding=1),\n            nn.BatchNorm2d(num_filters),\n            nn.LeakyReLU(0.1),\n            nn.MaxPool2d(kernel_size=2, stride=2) # 8x8 -> 4x4\n        )\n        self.block2 = nn.Sequential(\n            nn.Conv2d(in_channels=num_filters, out_channels=num_filters * 2, kernel_size=3, padding=1),\n            nn.BatchNorm2d(num_filters * 2),\n            nn.LeakyReLU(0.1),\n            nn.MaxPool2d(kernel_size=2, stride=2) # 4x4 -> 2x2\n        )\n        # --- FIX: Add 1D pooling to reduce sequence length ---\n        self.pool_seq1 = nn.MaxPool1d(kernel_size=2, stride=2)\n        self.pool_seq2 = nn.MaxPool1d(kernel_size=2, stride=2)\n        self.dropout = nn.Dropout(dropout_rate)\n    def forward(self, x):\n        \"\"\"\n        Args:\n            x (Tensor): Input sequence of shape (batch_size, sequence_length, channels, height, width).\n        Returns:\n            Tensor: Output sequence of shape (batch_size, new_sequence_length, features).\n        \"\"\"\n        batch_size, seq_len, C, H, W = x.shape\n        # Combine batch and sequence dimensions to apply 2D CNN\n        x = x.view(batch_size * seq_len, C, H, W)\n        \n        x = self.block1(x)\n        x = self.block2(x)\n        \n        # Flatten the spatial dimensions but keep the sequence dimension\n        x = x.view(batch_size, seq_len, -1)\n        \n        # --- FIX: Apply temporal pooling to match IMU branch ---\n        # Permute to (batch_size, features, sequence_length) for MaxPool1d\n        x = x.permute(0, 2, 1)\n        x = self.pool_seq1(x)\n        x = self.pool_seq2(x)\n        # Permute back to (batch_size, sequence_length, features)\n        x = x.permute(0, 2, 1)\n        \n        x = self.dropout(x)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.400535Z","iopub.execute_input":"2025-07-14T10:40:56.400765Z","iopub.status.idle":"2025-07-14T10:40:56.415619Z","shell.execute_reply.started":"2025-07-14T10:40:56.400750Z","shell.execute_reply":"2025-07-14T10:40:56.415105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Attention(nn.Module):\n    def __init__(self, hidden_size):\n        super(Attention, self).__init__()\n        self.attention = nn.Linear(hidden_size, 1, bias=False)\n\n    def forward(self, gru_output):\n        energy = torch.tanh(self.attention(gru_output))  # (B, T, 1)\n        attention_weights = F.softmax(energy, dim=1)     # (B, T, 1)\n        context_vector = torch.sum(attention_weights * gru_output, dim=1)  # (B, H)\n        return context_vector, attention_weights","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.416347Z","iopub.execute_input":"2025-07-14T10:40:56.417099Z","iopub.status.idle":"2025-07-14T10:40:56.429268Z","shell.execute_reply.started":"2025-07-14T10:40:56.417075Z","shell.execute_reply":"2025-07-14T10:40:56.428530Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CombinedGRUModel(nn.Module):\n    \"\"\"\n    This model combines two CNN branches and feeds their output into \n    a Bidirectional GRU followed by a simple pooling (no attention).\n    \"\"\"\n    def __init__(self, imu_input, num_classes, gru_hidden_size=128, gru_num_layers=2):\n        super(CombinedGRUModel, self).__init__()\n        \n        # --- CNN Branches ---\n        self.imu_branch = Conv1DBranch(input_features=imu_input, num_filters=64)\n        self.tof_branch = Conv2DBranch(in_channels=5, num_filters=64)\n\n        imu_cnn_features = 64 * 2\n        tof_cnn_features = (64 * 2) * (2 * 2)  # Adjust according to CNN output shape\n        gru_input_size = imu_cnn_features + tof_cnn_features\n        \n        # --- BiGRU Layer ---\n        self.gru = nn.GRU(\n            input_size=gru_input_size, \n            hidden_size=gru_hidden_size, \n            num_layers=gru_num_layers,\n            batch_first=True, \n            dropout=0.5,\n            bidirectional=True\n        )\n        \n        # --- Classifier ---\n        self.classifier = nn.Sequential(\n            nn.Linear(gru_hidden_size * 2, 128),  # BiGRU output size\n            nn.LeakyReLU(0.1),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes)\n        )\n        self.attention = Attention(hidden_size=gru_hidden_size * 2)  # BiGRU is bidirectional\n\n\n    def forward(self, imu_x, tof_x):\n        imu_features_seq = self.imu_branch(imu_x)\n        tof_features_seq = self.tof_branch(tof_x)\n\n        min_seq_len = min(imu_features_seq.size(1), tof_features_seq.size(1))\n        imu_features_seq = imu_features_seq[:, :min_seq_len, :]\n        tof_features_seq = tof_features_seq[:, :min_seq_len, :]\n\n        merged_seq = torch.cat((imu_features_seq, tof_features_seq), dim=2)\n\n        gru_out, _ = self.gru(merged_seq)\n\n        context_vector, _ = self.attention(gru_out)\n        output = self.classifier(context_vector)\n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.429973Z","iopub.execute_input":"2025-07-14T10:40:56.430197Z","iopub.status.idle":"2025-07-14T10:40:56.443600Z","shell.execute_reply.started":"2025-07-14T10:40:56.430174Z","shell.execute_reply":"2025-07-14T10:40:56.443032Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_one_fold(model, train_loader, val_loader, device, le, y_train_fold, num_epochs=50, patience=5, fold=0, use_mixup=True):\n    y_train_fold_encoded = le.transform(y_train_fold)\n    class_weights = compute_class_weight('balanced', classes=np.unique(y_train_fold_encoded), y=y_train_fold_encoded)\n    criterion = nn.CrossEntropyLoss(weight=torch.tensor(class_weights, dtype=torch.float32)).to(device)\n    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)\n    best_val_f1 = -1\n    patience_counter = 0\n    model.to(device)\n\n    for epoch in range(num_epochs):\n        model.train()\n        total_train_loss = 0\n        \n        # ✅ FIX: Swap 'sensor' and 'tof' to match dataset output order.\n        # The dataloader returns (combined_tensor, tof_tensor, label)\n        # We also add '_' to ignore the sequence_id that the dataset now returns.\n        for batch_idx, (sensor, tof, labels, seq_id) in enumerate(train_loader):\n            if batch_idx % 100 == 0:\n                print(f\"Epoch {epoch+1}, Batch {batch_idx+1}/{len(train_loader)}\")\n            imu = sensor.to(device) # This is now the correct 3D tensor\n            tof = tof.to(device)    # This is now the correct 5D tensor\n            labels = labels.to(device)\n            optimizer.zero_grad()\n            \n            if use_mixup:\n                lam = np.random.beta(0.4, 0.4)\n                lam = np.clip(lam, 0.2, 0.8)\n                index = torch.randperm(labels.size(0)).to(device)\n                mixed_tof = lam * tof + (1 - lam) * tof[index]\n                mixed_imu = lam * imu + (1 - lam) * imu[index]\n                labels_a, labels_b = labels, labels[index]\n                \n                # ✅ FIX: Pass arguments to the model in the correct order.\n                outputs = model(mixed_imu, mixed_tof)\n                loss = lam * criterion(outputs, labels_a) + (1 - lam) * criterion(outputs, labels_b)\n            else:\n                outputs = model(imu, tof)\n                loss = criterion(outputs, labels)\n\n            if torch.isnan(loss):\n                print(\"NaN loss detected! Stopping training for this fold.\")\n                return 0\n\n            loss.backward()\n            optimizer.step()\n            total_train_loss += loss.item() * labels.size(0)\n\n        avg_train_loss = total_train_loss / len(train_loader.dataset)\n        idx_to_label = {i: c for i, c in enumerate(le.classes_)}\n        avg_val_loss, val_acc, val_f1 = evaluate_model(model, val_loader, idx_to_label, criterion, device)\n        print(f\"Epoch {epoch+1}/{num_epochs} | Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f} | Val Acc: {val_acc:.4f} | Val F1: {val_f1:.4f}\")\n\n        if val_f1 > best_val_f1:\n            best_val_f1 = val_f1\n            patience_counter = 0\n            torch.save(model.state_dict(), f'best_model_{fold}.pth')\n            print(f\"  -> New best model saved with F1: {best_val_f1:.4f}\")\n        else:\n            patience_counter += 1\n            if patience_counter >= patience:\n                print(\"Early stopping triggered.\")\n                break\n\n    print(f\"--- Training Finished. Best Val F1: {best_val_f1:.4f} ---\")\n    return best_val_f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.444283Z","iopub.execute_input":"2025-07-14T10:40:56.444517Z","iopub.status.idle":"2025-07-14T10:40:56.561411Z","shell.execute_reply.started":"2025-07-14T10:40:56.444497Z","shell.execute_reply":"2025-07-14T10:40:56.560637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_fold(train_idx, val_idx, df, X, y, fold, le):\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    MAX_LEN = 128\n    \n    # --- Create train/validation dataframes for the fold ---\n    train_ids, val_ids = X.iloc[train_idx], X.iloc[val_idx]\n    df_train = df[df['sequence_id'].isin(train_ids)].copy()\n    df_val = df[df['sequence_id'].isin(val_ids)].copy()\n\n    # --- Run Feature Engineering ---\n    print(\"Running Feature Engineering...\")\n    # Use the full df for column definitions to ensure consistency\n    df_processed_schema, new_feature_names = run_feature_engineering(df.head(1))\n    df_train_processed, _ = run_feature_engineering(df_train)\n    df_val_processed, _ = run_feature_engineering(df_val)\n    print(\"Feature Engineering complete.\")\n\n    # --- Define Column Groups for the Dataset ---\n    \n    # ✅ FIX: Define IMU columns (accel + gyro only) and Thermopile columns separately\n    # to prevent overlap.\n    rot_cols = [c for c in df_processed_schema.columns if c.startswith('rot_')]\n    acc_cols = [c for c in df_processed_schema.columns if c.startswith('acc_')]\n    imu_cols_raw = rot_cols + acc_cols\n\n    thermopile_cols_raw = [c for c in df_processed_schema.columns if c.startswith('thm_')]\n    \n    # ToF grid columns for the 2D CNN branch\n    tof_cols_grid = [c for c in df.columns if c.startswith('tof_') and '_' in c and 'mean' not in c and 'std' not in c]\n\n    # All other feature-engineered columns for the 1D CNN branch\n    # ✅ FIX: Exclude lists are now correct and not redundant.\n    exclude_cols = set(imu_cols_raw) | set(thermopile_cols_raw) | set(tof_cols_grid)\n    feat_eng_cols = [c for c in new_feature_names if c not in exclude_cols]\n    \n    # --- Create Datasets and DataLoaders ---\n    train_dataset = GestureDataset(\n        df_train_processed, \n        imu_cols=imu_cols_raw, \n        feat_eng_cols=feat_eng_cols, \n        tof_cols=tof_cols_grid, \n        thermopile_cols=thermopile_cols_raw,\n        label_encoder=le, \n        max_seq_len=MAX_LEN, \n        is_train=True\n    )\n    val_dataset = GestureDataset(\n        df_val_processed, \n        imu_cols=imu_cols_raw, \n        feat_eng_cols=feat_eng_cols, \n        tof_cols=tof_cols_grid, \n        thermopile_cols=thermopile_cols_raw,\n        label_encoder=le, \n        max_seq_len=MAX_LEN, \n        is_train=False\n    )\n    \n    train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2)\n    val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2)\n\n    num_1d_features = len(imu_cols_raw) + len(feat_eng_cols) + len(thermopile_cols_raw)\n    \n    model = CombinedGRUModel(\n        imu_input=num_1d_features,\n        num_classes=len(le.classes_)\n    )\n\n    # --- Train and Evaluate ---\n    y_train_fold = y.iloc[train_idx].values\n    best_f1 = process_one_fold(model, train_loader, val_loader, device, le, y_train_fold, fold = fold, use_mixup = True )\n    return best_f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.562096Z","iopub.execute_input":"2025-07-14T10:40:56.562291Z","iopub.status.idle":"2025-07-14T10:40:56.577370Z","shell.execute_reply.started":"2025-07-14T10:40:56.562278Z","shell.execute_reply":"2025-07-14T10:40:56.576594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42)\nsequence_level_df = df.groupby('sequence_id').first().reset_index()\nX = sequence_level_df['sequence_id']\ny = sequence_level_df['gesture']\ngroups = sequence_level_df['subject']\nle = LabelEncoder()\nle.fit(sequence_level_df['gesture'])\nwith open('/kaggle/working/label_encoder.pkl', 'wb') as f:\n    pickle.dump(le, f)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:56.578033Z","iopub.execute_input":"2025-07-14T10:40:56.578235Z","iopub.status.idle":"2025-07-14T10:40:58.219678Z","shell.execute_reply.started":"2025-07-14T10:40:56.578212Z","shell.execute_reply":"2025-07-14T10:40:58.218910Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"start_fold = 0\n\n# 2. (Optional but good practice) Pre-load scores for completed folds.\n#    The score for FOLD 1 (index 0) was 0.8247.\nfold_f1_scores = []\n\n# 3. Let the loop run from the beginning, but skip the completed folds.\nfor fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y, groups)):\n    \n    # This 'if' statement is the key.\n    # It checks if the current fold index is less than your desired start.\n    if fold < start_fold:\n        print(f\"--- Skipping Fold {fold+1} (already completed) ---\")\n        continue  # 'continue' jumps to the next iteration of the loop.\n    \n    # The rest of your code will only run for fold >= start_fold\n    print(f\"\\n{'='*25}\")\n    print(f\"====== FOLD {fold+1} / 5 ======\")\n    print(f\"{'='*25}\")\n    fold_score = process_fold(train_idx, val_idx, df, X, y, fold, le)\n    \n    fold_f1_scores.append(fold_score)\n    print(f\"\\nBest F1 Score for Fold {fold+1}: {fold_score:.4f}\")\n\nprint(f\"\\n{'='*40}\")\nprint(\"Cross-Validation Finished.\")\nprint(f\"Average F1 Score across all folds: {np.mean(fold_f1_scores):.4f}\")\nprint(f\"{'='*40}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T10:40:58.220493Z","iopub.execute_input":"2025-07-14T10:40:58.220753Z","execution_failed":"2025-07-14T10:41:17.998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}