{"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":"gpu","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, GridSearchCV\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\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau, CosineAnnealingLR\nimport warnings\nfrom sklearn.preprocessing import StandardScaler\nimport xgboost as xgb\nimport time\nimport joblib\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:22:58.607538Z","iopub.execute_input":"2025-08-20T14:22:58.607793Z","iopub.status.idle":"2025-08-20T14:23:04.545898Z","shell.execute_reply.started":"2025-08-20T14:22:58.607758Z","shell.execute_reply":"2025-08-20T14:23:04.545321Z"}},"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-08-20T14:23:04.547353Z","iopub.execute_input":"2025-08-20T14:23:04.547687Z","iopub.status.idle":"2025-08-20T14:23:04.556568Z","shell.execute_reply.started":"2025-08-20T14:23:04.547670Z","shell.execute_reply":"2025-08-20T14:23:04.555773Z"}},"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-08-20T14:23:04.557259Z","iopub.execute_input":"2025-08-20T14:23:04.557456Z","iopub.status.idle":"2025-08-20T14:23:34.096032Z","shell.execute_reply.started":"2025-08-20T14:23:04.557438Z","shell.execute_reply":"2025-08-20T14:23:34.095452Z"}},"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):\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    # 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    # Final fill for any remaining NaNs\n    df[columns_to_fill] = df[columns_to_fill].fillna(final_fill_value)\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.096806Z","iopub.execute_input":"2025-08-20T14:23:34.097091Z","iopub.status.idle":"2025-08-20T14:23:34.102046Z","shell.execute_reply.started":"2025-08-20T14:23:34.097050Z","shell.execute_reply":"2025-08-20T14:23:34.101196Z"}},"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    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    ).fillna(0)  # Ensures the added stats columns don't contain NaN\n\n    return df_with_stats\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.102790Z","iopub.execute_input":"2025-08-20T14:23:34.102956Z","iopub.status.idle":"2025-08-20T14:23:34.138882Z","shell.execute_reply.started":"2025-08-20T14:23:34.102943Z","shell.execute_reply":"2025-08-20T14:23:34.138102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_mag_features_fast(df):\n    \"\"\"Optimized magnitude feature calculation.\"\"\"\n    acc_vectors = df[['acc_x', 'acc_y', 'acc_z']].values\n    rot_vectors = df[['rot_x', 'rot_y', 'rot_z']].values\n    df['acc_mag'] = np.linalg.norm(acc_vectors, axis=1)\n    df['rot_mag'] = np.linalg.norm(rot_vectors, axis=1)\n    df[['acc_mag', 'rot_mag']] = df[['acc_mag', 'rot_mag']].fillna(0)\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.139748Z","iopub.execute_input":"2025-08-20T14:23:34.139973Z","iopub.status.idle":"2025-08-20T14:23:34.153497Z","shell.execute_reply.started":"2025-08-20T14:23:34.139947Z","shell.execute_reply":"2025-08-20T14:23:34.152881Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_jerk_features(acc_mag, rot_mag):\n    \"\"\"Calculates jerk features from magnitude arrays using NumPy.\"\"\"\n    acc_jerk = np.insert(np.diff(acc_mag), 0, 0)\n    rot_jerk = np.insert(np.diff(rot_mag), 0, 0)\n    return acc_jerk, rot_jerk","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.155523Z","iopub.execute_input":"2025-08-20T14:23:34.156135Z","iopub.status.idle":"2025-08-20T14:23:34.165776Z","shell.execute_reply.started":"2025-08-20T14:23:34.156110Z","shell.execute_reply":"2025-08-20T14:23:34.165286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def remove_gravity_from_acc(acc_data, rot_data):\n    gravity_world = np.array([0, 0, 9.81])\n    # Handle NaNs or zero quaternions by masking\n    valid_mask = ~(np.isnan(rot_data).any(axis=1) | np.all(np.isclose(rot_data, 0), axis=1))\n    \n    linear_accel = acc_data.copy()\n    \n    if valid_mask.any():\n        # Create Rotation object for all valid quaternions\n        rotations = R.from_quat(rot_data[valid_mask])\n        # Apply inverse rotation to gravity vector in batch\n        gravity_sensor = rotations.apply(gravity_world, inverse=True)\n        # Subtract gravity from acc_data only for valid indices\n        linear_accel[valid_mask] = acc_data[valid_mask] - gravity_sensor\n    \n    return linear_accel","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.166363Z","iopub.execute_input":"2025-08-20T14:23:34.166616Z","iopub.status.idle":"2025-08-20T14:23:34.178206Z","shell.execute_reply.started":"2025-08-20T14:23:34.166593Z","shell.execute_reply":"2025-08-20T14:23:34.177621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_tof_statistics_np_no_loop(df):\n    \"\"\"\n    Fully vectorized ToF feature extractor with no Python loops.\n    Assumes ToF columns are named 'tof_[1-5]_v[0-63]' and ordered consistently.\n    \"\"\"\n    df = df.copy()\n    tof_cols = [col for col in df.columns if col.startswith('tof_')]\n    tof_cols_sorted = sorted(tof_cols, key=lambda x: (int(x.split('_')[1]), int(x.split('_')[2][1:])))\n    \n    tof_data = df[tof_cols_sorted].replace(-1, np.nan).values  # shape: (N, 320)\n\n    N = tof_data.shape[0]\n    num_sensors = 5\n    num_pixels_per_sensor = 64\n\n    if tof_data.shape[1] != num_sensors * num_pixels_per_sensor:\n        raise ValueError(\"Unexpected number of ToF columns\")\n\n    # Reshape: (N, 5, 64)\n    tof_reshaped = tof_data.reshape(N, num_sensors, num_pixels_per_sensor)\n\n    # Compute stats along the 64-pixel axis\n    with warnings.catch_warnings():\n        warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n        tof_mean = np.nanmean(tof_reshaped, axis=2)\n        tof_std = np.nanstd(tof_reshaped, axis=2)\n        tof_min = np.nanmin(tof_reshaped, axis=2)\n        tof_max = np.nanmax(tof_reshaped, axis=2)\n\n    # Assign new columns without a loop\n    sensor_indices = np.arange(1, num_sensors + 1)\n    stat_names = ['mean', 'std', 'min', 'max']\n    stats = [tof_mean, tof_std, tof_min, tof_max]\n\n    for stat_array, stat_name in zip(stats, stat_names):\n        for i, sensor_idx in enumerate(sensor_indices):\n            df[f'tof_{sensor_idx}_{stat_name}'] = np.nan_to_num(stat_array[:, i])\n\n    created_cols = [f'tof_{i}_{stat}' for i in sensor_indices for stat in stat_names]\n    return df, created_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.178966Z","iopub.execute_input":"2025-08-20T14:23:34.179523Z","iopub.status.idle":"2025-08-20T14:23:34.193444Z","shell.execute_reply.started":"2025-08-20T14:23:34.179498Z","shell.execute_reply":"2025-08-20T14:23:34.192755Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_rolling_features(array, window):\n    \"\"\"Calculates rolling mean and std for a 1D NumPy array efficiently.\"\"\"\n    padded_array = np.pad(array, (window - 1, 0), mode='edge')\n    \n    cumsum = np.cumsum(padded_array)\n    rolling_sum = cumsum[window - 1:] - np.concatenate(([0], cumsum[:-window]))\n    rolling_mean = rolling_sum / window\n    \n    cumsum_sq = np.cumsum(padded_array**2)\n    rolling_sum_sq = cumsum_sq[window - 1:] - np.concatenate(([0], cumsum_sq[:-window]))\n    rolling_var = (rolling_sum_sq / window) - (rolling_mean**2)\n    rolling_std = np.sqrt(np.maximum(rolling_var, 0))\n\n    for i in range(window - 1):\n        rolling_mean[i] = np.mean(array[:i+1])\n        rolling_std[i] = np.std(array[:i+1])\n        \n    return rolling_mean, rolling_std","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.194210Z","iopub.execute_input":"2025-08-20T14:23:34.194438Z","iopub.status.idle":"2025-08-20T14:23:34.208358Z","shell.execute_reply.started":"2025-08-20T14:23:34.194414Z","shell.execute_reply":"2025-08-20T14:23:34.207631Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_feature_engineering_fast(df):\n    \"\"\"A new, fast feature engineering function that avoids all slow pandas operations.\"\"\"\n    # We work on a copy to avoid modifying the original DataFrame in the loop\n    df = df.copy() \n    tof_cols = [col for col in df.columns if col.startswith('tof_')]\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_') and not col.startswith('acc_lin_')]\n    initial_cols = set(df.columns)\n    acc_data = df[acc_cols].values\n    rot_data = df[rot_cols].values\n    acc_lin = remove_gravity_from_acc(acc_data, rot_data)\n\n    lin_acc_cols = [col.replace('acc_', 'acc_lin_') for col in acc_cols]\n    acc_lin_df = pd.DataFrame(acc_lin, columns=lin_acc_cols, index=df.index)\n    df.drop(columns=lin_acc_cols, inplace=True, errors='ignore')  # optional safety\n    df = pd.concat([df, acc_lin_df], axis=1)\n    # === FAST VECTORIZED OPERATIONS ===\n    # These functions are already fast\n    df = add_mag_features_fast(df)\n    df = create_thm_statistics(df)\n    df, tof_created_list = create_tof_statistics_np_no_loop(df)\n\n    # === USE NUMPY HELPERS FOR SLOW PARTS ===\n    # Jerk Features\n    acc_jerk, rot_jerk = add_jerk_features(df['acc_mag'].values, df['rot_mag'].values)\n    df['acc_jerk'] = acc_jerk\n    df['rot_jerk'] = rot_jerk\n\n    # Rolling Features\n    cols_to_roll = ['acc_mag', 'rot_mag', 'acc_jerk']\n    for col in cols_to_roll:\n        rolling_mean, rolling_std = add_rolling_features(df[col].values, window=10)\n        df[f'{col}_mean_10'] = rolling_mean\n        df[f'{col}_std_10'] = rolling_std\n\n    # The bfill/ffill logic is slow with groupby. \n    # For a single sequence, it's simpler and faster to do this.\n    df.ffill(inplace=True)\n    df.bfill(inplace=True)\n    df.fillna(0, inplace=True)\n    cols_to_drop = ['orientation', 'behavior', 'phase', 'subject', 'sequence_type', 'row_id']\n    df = df.drop(columns=[col for col in cols_to_drop if col in df.columns])\n    \n    new_cols = list(set(df.columns) - initial_cols)\n    return df, new_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.209196Z","iopub.execute_input":"2025-08-20T14:23:34.209588Z","iopub.status.idle":"2025-08-20T14:23:34.224380Z","shell.execute_reply.started":"2025-08-20T14:23:34.209571Z","shell.execute_reply":"2025-08-20T14:23:34.223732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def pad_and_truncate_sequence(data, max_len):\n    \"\"\"\n    Pads/truncates along the first dimension (time).\n    Works for 2D (seq_len, feat_dim) or higher dims like (seq_len, 5, 8, 8).\n    \"\"\"\n    if data.shape[0] == 0:\n        # If empty, return all zeros with the right shape\n        return np.zeros((max_len, *data.shape[1:]), dtype=data.dtype)\n\n    seq_len = data.shape[0]\n\n    if seq_len > max_len:\n        return data[-max_len:]   # truncate along time\n    elif seq_len < max_len:\n        pad_shape = (max_len - seq_len, *data.shape[1:])\n        pad = np.zeros(pad_shape, dtype=data.dtype)\n        return np.concatenate([pad, data], axis=0)\n    else:\n        return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.225167Z","iopub.execute_input":"2025-08-20T14:23:34.225747Z","iopub.status.idle":"2025-08-20T14:23:34.238489Z","shell.execute_reply.started":"2025-08-20T14:23:34.225725Z","shell.execute_reply":"2025-08-20T14:23:34.237919Z"}},"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-08-20T14:23:34.239254Z","iopub.execute_input":"2025-08-20T14:23:34.239455Z","iopub.status.idle":"2025-08-20T14:23:34.249817Z","shell.execute_reply.started":"2025-08-20T14:23:34.239440Z","shell.execute_reply":"2025-08-20T14:23:34.249228Z"}},"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-08-20T14:23:34.250414Z","iopub.execute_input":"2025-08-20T14:23:34.250604Z","iopub.status.idle":"2025-08-20T14:23:34.262355Z","shell.execute_reply.started":"2025-08-20T14:23:34.250590Z","shell.execute_reply":"2025-08-20T14:23:34.261705Z"}},"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-08-20T14:23:34.262924Z","iopub.execute_input":"2025-08-20T14:23:34.263179Z","iopub.status.idle":"2025-08-20T14:23:34.277715Z","shell.execute_reply.started":"2025-08-20T14:23:34.263163Z","shell.execute_reply":"2025-08-20T14:23:34.277113Z"}},"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-08-20T14:23:34.278678Z","iopub.execute_input":"2025-08-20T14:23:34.278970Z","iopub.status.idle":"2025-08-20T14:23:34.291099Z","shell.execute_reply.started":"2025-08-20T14:23:34.278941Z","shell.execute_reply":"2025-08-20T14:23:34.290458Z"}},"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-08-20T14:23:34.291676Z","iopub.execute_input":"2025-08-20T14:23:34.291883Z","iopub.status.idle":"2025-08-20T14:23:34.303800Z","shell.execute_reply.started":"2025-08-20T14:23:34.291861Z","shell.execute_reply":"2025-08-20T14:23:34.303257Z"}},"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-08-20T14:23:34.304375Z","iopub.execute_input":"2025-08-20T14:23:34.304560Z","iopub.status.idle":"2025-08-20T14:23:34.319816Z","shell.execute_reply.started":"2025-08-20T14:23:34.304547Z","shell.execute_reply":"2025-08-20T14:23:34.319103Z"}},"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-08-20T14:23:34.320551Z","iopub.execute_input":"2025-08-20T14:23:34.320776Z","iopub.status.idle":"2025-08-20T14:23:34.332231Z","shell.execute_reply.started":"2025-08-20T14:23:34.320752Z","shell.execute_reply":"2025-08-20T14:23:34.331536Z"}},"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-08-20T14:23:34.332915Z","iopub.execute_input":"2025-08-20T14:23:34.333146Z","iopub.status.idle":"2025-08-20T14:23:34.345544Z","shell.execute_reply.started":"2025-08-20T14:23:34.333131Z","shell.execute_reply":"2025-08-20T14:23:34.344851Z"}},"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-08-20T14:23:34.346223Z","iopub.execute_input":"2025-08-20T14:23:34.346921Z","iopub.status.idle":"2025-08-20T14:23:34.358234Z","shell.execute_reply.started":"2025-08-20T14:23:34.346904Z","shell.execute_reply":"2025-08-20T14:23:34.357616Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def augment_tof_batch(tof_batch, max_val=254.0, missing_prob=0.05, block_prob=0.3, rng=None):\n    if rng is None:\n        rng = np.random.default_rng()\n\n    aug = tof_batch.copy()\n\n    # --- Step 1: Missing pixel simulation ---\n    random_mask = rng.random(size=aug.shape) < missing_prob\n    aug[random_mask] = -1\n\n    # --- Step 1b: Contiguous block occlusion ---\n    B = aug.shape[0]\n    for i in range(B):\n        if rng.random() < block_prob:\n            sensor_id = rng.integers(0, 5)\n            h = rng.integers(2, 5)\n            w = rng.integers(2, 5)\n            y = rng.integers(0, 8 - h + 1)\n            x = rng.integers(0, 8 - w + 1)\n            aug[i, sensor_id, y:y+h, x:x+w] = -1\n\n    # --- Step 2: Sensor bias ---\n    bias = rng.normal(0, 2.0, size=(B, 5, 1, 1))\n    gain = 1.0 + rng.normal(0, 0.01, size=(B, 5, 1, 1))\n\n    valid_mask = (aug != -1)\n\n    # Broadcast gain & bias\n    aug = aug * gain + bias\n    aug[~valid_mask] = -1  # restore missing pixels\n\n    # --- Step 3: Noise injection ---\n    noise_std = (0.01 + 0.02 * np.clip(aug, 0, max_val) / max_val)\n    noise = rng.normal(0, noise_std)\n    aug[valid_mask] += noise[valid_mask]\n\n    # --- Step 4: Clamp to sensor range ---\n    aug[valid_mask] = np.clip(aug[valid_mask], 0.0, max_val)\n\n    return aug\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.361377Z","iopub.execute_input":"2025-08-20T14:23:34.361580Z","iopub.status.idle":"2025-08-20T14:23:34.372197Z","shell.execute_reply.started":"2025-08-20T14:23:34.361565Z","shell.execute_reply":"2025-08-20T14:23:34.371455Z"}},"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 = []\n\n    with torch.no_grad():\n        for imu_data, thm_data, other_data, tof_data, tof_stats, labels, gate_target, ids in val_loader:\n            imu_data = imu_data.to(device)\n            thm_data = thm_data.to(device)\n            other_data = other_data.to(device)\n            tof_data = tof_data.to(device)\n            \n            if tof_stats is not None:\n                tof_stats = tof_stats.to(device)\n\n            labels = labels.to(device)\n            val_outputs = model(imu_data, thm_data, other_data, tof_data, tof_stats)\n            \n            val_loss += criterion(val_outputs, labels).item() * labels.size(0)\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)\n\n    avg_val_loss = val_loss / total\n    val_acc = correct / total\n\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\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.372837Z","iopub.execute_input":"2025-08-20T14:23:34.373099Z","iopub.status.idle":"2025-08-20T14:23:34.388020Z","shell.execute_reply.started":"2025-08-20T14:23:34.373079Z","shell.execute_reply":"2025-08-20T14:23:34.387383Z"}},"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.4):\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-08-20T14:23:34.388763Z","iopub.execute_input":"2025-08-20T14:23:34.388998Z","iopub.status.idle":"2025-08-20T14:23:34.405216Z","shell.execute_reply.started":"2025-08-20T14:23:34.388977Z","shell.execute_reply":"2025-08-20T14:23:34.404563Z"}},"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.5):\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-08-20T14:23:34.405854Z","iopub.execute_input":"2025-08-20T14:23:34.406109Z","iopub.status.idle":"2025-08-20T14:23:34.420785Z","shell.execute_reply.started":"2025-08-20T14:23:34.406089Z","shell.execute_reply":"2025-08-20T14:23:34.420098Z"}},"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-08-20T14:23:34.421357Z","iopub.execute_input":"2025-08-20T14:23:34.421567Z","iopub.status.idle":"2025-08-20T14:23:34.437493Z","shell.execute_reply.started":"2025-08-20T14:23:34.421553Z","shell.execute_reply":"2025-08-20T14:23:34.436921Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CombinedGRUModel(nn.Module):\n    def __init__(self, imu_input, thm_input, other_feat_input, num_classes, gru_hidden_size=128, gru_num_layers=1, tof_stats_dim=None):\n        super(CombinedGRUModel, self).__init__()\n        \n        total_input_features = imu_input + thm_input + other_feat_input\n        self.feature_branch = Conv1DBranch(input_features=total_input_features, num_filters=128)\n\n        # ToF branch stays separate\n        self.tof_branch = Conv2DBranch(in_channels=5, num_filters=64)\n\n        # Output sizes from CNN branches\n        feature_cnn_features = 256\n        tof_cnn_features = 512\n        cnn_output_features = feature_cnn_features + tof_cnn_features\n\n        # BiLSTM\n        self.bilstm = nn.LSTM(\n            input_size=cnn_output_features,\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        # GRU on top of BiLSTM\n        self.gru = nn.GRU(\n            input_size=gru_hidden_size * 2,  # BiLSTM is bidirectional\n            hidden_size=gru_hidden_size,\n            num_layers=gru_num_layers,\n            batch_first=True,\n            dropout=0.5,\n            bidirectional=False\n        )\n\n        # Attention now on GRU output\n        self.attention = Attention(hidden_size=gru_hidden_size)\n\n        self.use_tof_stats = tof_stats_dim is not None\n\n        # Classifier input\n        classifier_input_dim = gru_hidden_size\n        if self.use_tof_stats:\n            self.tof_stats_proj = nn.Sequential(\n                nn.Linear(tof_stats_dim, 64),\n                nn.LeakyReLU(0.1),\n                nn.Dropout(0.4)\n            )\n            classifier_input_dim += 64\n\n        self.classifier = nn.Sequential(\n            nn.Linear(classifier_input_dim, 128),\n            nn.LeakyReLU(0.1),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes)\n        )\n        self.gru_dropout = nn.Dropout(0.3)\n        self.gru_norm = nn.LayerNorm(gru_hidden_size)\n        self.attn_dropout = nn.Dropout(0.3)\n\n    def forward(self, imu_x, thm_x, other_x, tof_x, tof_stats=None):\n        feature_seq = self.feature_branch(torch.cat((imu_x, thm_x, other_x), dim=2))\n        tof_features_seq = self.tof_branch(tof_x)\n\n        # Align sequence lengths\n        min_seq_len = min(feature_seq.size(1), tof_features_seq.size(1))\n        feature_seq = feature_seq[:, :min_seq_len, :]\n        tof_features_seq = tof_features_seq[:, :min_seq_len, :]\n\n        merged_seq = torch.cat((feature_seq, tof_features_seq), dim=2)\n        \n        bilstm_out, _ = self.bilstm(merged_seq)\n        gru_out, _ = self.gru(bilstm_out)  # feed BiLSTM output into GRU\n        gru_out = self.gru_norm(gru_out)\n        gru_out = self.gru_dropout(gru_out)\n                \n        context_vector, _ = self.attention(gru_out)\n        context_vector = self.attn_dropout(context_vector)  \n\n        if self.use_tof_stats and tof_stats is not None:\n            tof_stats_embed = self.tof_stats_proj(tof_stats)\n            context_vector = torch.cat([context_vector, tof_stats_embed], dim=1)\n\n        return self.classifier(context_vector)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.438187Z","iopub.execute_input":"2025-08-20T14:23:34.438471Z","iopub.status.idle":"2025-08-20T14:23:34.511020Z","shell.execute_reply.started":"2025-08-20T14:23:34.438450Z","shell.execute_reply":"2025-08-20T14:23:34.510360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GestureDatasetNumpy(Dataset):\n    def __init__(self, df, sequence_ids, \n                 imu_scaler, thm_scaler, other_scaler, tof_scaler, tof_stats_scaler,\n                 imu_cols, thm_cols, tof_cols, other_feat_cols, tof_stats_cols, imu_cols_raw, thm_cols_raw, tof_cols_raw,\n                 labels=None, seq_slices=None, max_seq_len=128, \n                 is_train=False, use_mixup=False, mixup_alpha=0.4, masking_prob=0.15, noise_level=0.01,\n                 run_feature_engineering=None):\n        \"\"\"\n        Dataset that applies augmentation -> feature engineering -> scaling on the fly.\n        \"\"\"\n        self.df = df\n        self.sequence_ids = sequence_ids\n        self.imu_scaler = imu_scaler\n        self.thm_scaler = thm_scaler\n        self.other_scaler = other_scaler\n        self.tof_scaler = tof_scaler\n        self.tof_stats_scaler = tof_stats_scaler\n        self.imu_cols = imu_cols\n        self.thm_cols = thm_cols\n        self.tof_cols = tof_cols\n        self.other_feat_cols = other_feat_cols\n        self.tof_stats_cols = tof_stats_cols\n        self.imu_cols_raw = imu_cols_raw\n        self.thm_cols_raw = thm_cols_raw\n        self.tof_cols_raw = tof_cols_raw\n        self.labels = labels\n        self.seq_slices = seq_slices\n        self.max_seq_len = max_seq_len\n        self.is_train = is_train\n        self.has_labels = labels is not None\n        self.use_mixup = use_mixup\n        self.mixup_alpha = mixup_alpha\n        self.masking_prob = masking_prob\n        self.noise_level = noise_level\n        self.run_feature_engineering = run_feature_engineering\n\n    def __len__(self):\n        return len(self.sequence_ids)\n\n    def __getitem__(self, idx):\n        seq_id = self.sequence_ids.iloc[idx]\n        start, end = self.seq_slices[seq_id]\n        df_seq = self.df.iloc[start:end].copy()\n\n        # ---- Augmentation first ----\n        if self.is_train:\n            df_seq[self.imu_cols_raw] = apply_imu_augmentations(df_seq[self.imu_cols_raw].values)\n            df_seq[self.thm_cols_raw] = apply_thermopile_augmentations(df_seq[self.thm_cols_raw].values)\n            tof_arr = df_seq[self.tof_cols_raw].values\n            B = tof_arr.shape[0]  # sequence length\n            tof_arr = tof_arr.reshape(B, 5, 8, 8)  # make sure this matches your sensors and spatial dims\n            augmented = augment_tof_batch(tof_arr)\n            # flatten back if you need to assign to dataframe\n            df_seq[self.tof_cols_raw] = augmented.reshape(B, -1)\n\n        # ---- Feature engineering ----\n        if self.run_feature_engineering is not None:\n            df_seq, _ = self.run_feature_engineering(df_seq)\n\n        # ---- Scaling ----\n        imu_feat   = self.imu_scaler.transform(df_seq[self.imu_cols])\n        thm_feat   = self.thm_scaler.transform(df_seq[self.thm_cols])\n        other_feat = self.other_scaler.transform(df_seq[self.other_feat_cols])\n        tof_feat   = self.tof_scaler.transform(df_seq[self.tof_cols])\n\n        # Aggregate ToF stats per sequence\n        tof_stats_feat = df_seq[self.tof_stats_cols].mean().to_numpy().reshape(1, -1)\n        tof_stats_feat = self.tof_stats_scaler.transform(tof_stats_feat).squeeze(0)\n\n        # Reshape ToF if needed\n        if tof_feat.ndim in (2, 3):\n            seq_len = tof_feat.shape[0]\n            tof_feat = tof_feat.reshape(seq_len, 5, 8, 8)\n\n        # ---- Pad/truncate ----\n        imu_feat   = pad_and_truncate_sequence(imu_feat, self.max_seq_len)\n        thm_feat   = pad_and_truncate_sequence(thm_feat, self.max_seq_len)\n        other_feat = pad_and_truncate_sequence(other_feat, self.max_seq_len)\n        tof_feat   = pad_and_truncate_sequence(tof_feat, self.max_seq_len)\n\n        # ---- Convert to tensors ----\n        imu_tensor   = torch.tensor(imu_feat, dtype=torch.float32)\n        thm_tensor   = torch.tensor(thm_feat, dtype=torch.float32)\n        other_tensor = torch.tensor(other_feat, dtype=torch.float32)\n        tof_tensor   = torch.tensor(tof_feat, dtype=torch.float32)\n        tof_stats_tensor = torch.tensor(tof_stats_feat, dtype=torch.float32)\n\n        # ---- Mixup / masking ----\n        gate_target = torch.ones(1, dtype=torch.float32)\n        if self.is_train and self.use_mixup:\n            mask_tof = np.random.rand() < self.masking_prob\n            if mask_tof:\n                tof_tensor = torch.zeros_like(tof_tensor)\n                gate_target = torch.zeros(1, dtype=torch.float32)\n\n            mix_idx = np.random.randint(0, len(self))\n            if mix_idx != idx:\n                seq_id2 = self.sequence_ids.iloc[mix_idx]\n                start2, end2 = self.seq_slices[seq_id2]\n                df_seq2 = self.df.iloc[start2:end2].copy()\n                df_seq2, _ = self.run_feature_engineering(df_seq2)\n\n                imu2   = torch.tensor(pad_and_truncate_sequence(self.imu_scaler.transform(df_seq2[self.imu_cols]), self.max_seq_len), dtype=torch.float32)\n                thm2   = torch.tensor(pad_and_truncate_sequence(self.thm_scaler.transform(df_seq2[self.thm_cols]), self.max_seq_len), dtype=torch.float32)\n                other2 = torch.tensor(pad_and_truncate_sequence(self.other_scaler.transform(df_seq2[self.other_feat_cols]), self.max_seq_len), dtype=torch.float32)\n                tof2   = torch.tensor(pad_and_truncate_sequence(self.tof_scaler.transform(df_seq2[self.tof_cols]), self.max_seq_len), dtype=torch.float32)\n\n                lam = float(np.clip(np.random.beta(self.mixup_alpha, self.mixup_alpha), 0.2, 0.8))\n                imu_tensor   = lam * imu_tensor + (1 - lam) * imu2\n                thm_tensor   = lam * thm_tensor + (1 - lam) * thm2\n                other_tensor = lam * other_tensor + (1 - lam) * other2\n                if not mask_tof:\n                    tof_tensor = lam * tof_tensor + (1 - lam) * tof2\n\n        if self.has_labels:\n            label_tensor = torch.tensor(self.labels[seq_id], dtype=torch.long)\n            return imu_tensor, thm_tensor, other_tensor, tof_tensor, tof_stats_tensor, label_tensor, gate_target, seq_id\n        else:\n            return imu_tensor, thm_tensor, other_tensor, tof_tensor, tof_stats_tensor, gate_target, seq_id\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:31:54.350089Z","iopub.execute_input":"2025-08-20T14:31:54.350427Z","iopub.status.idle":"2025-08-20T14:31:54.368692Z","shell.execute_reply.started":"2025-08-20T14:31:54.350395Z","shell.execute_reply":"2025-08-20T14:31:54.367859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def preprocess_and_scale(df, run_feature_engineering, fold, MAX_LEN, le, fit=True,\n                         scaler_imu=None, scaler_thermopile=None, scaler_other=None, \n                         scaler_tof_array=None, scaler_tof_stats=None):\n    \"\"\"\n    Preprocess features and fit/return scalers + column names.\n    No transformation here – scaling happens inside the Dataset.\n    \"\"\"\n    # Feature engineering\n    rot_cols = [c for c in df.columns if c.startswith('rot_')]\n    acc_cols = [c for c in df.columns if c.startswith('acc_') and not c.startswith('acc_lin_')]\n    imu_cols_raw = rot_cols + acc_cols\n    thermopile_cols_raw = [c for c in df.columns if c.startswith('thm_') \n                           and not any(feat in c for feat in ['eng', 'feat'])]\n    tof_cols_raw = [c for c in df.columns if c.startswith('tof_')]\n    df_processed, new_feature_names = run_feature_engineering(df)\n\n    # Identify columns\n    rot_cols = [c for c in df_processed.columns if c.startswith('rot_')]\n    acc_cols = [c for c in df_processed.columns if c.startswith('acc_') and not c.startswith('acc_lin_')]\n    imu_cols = rot_cols + acc_cols\n\n    thermopile_cols = [c for c in df_processed.columns if c.startswith('thm_') \n                           and not any(feat in c for feat in ['eng', 'feat'])]\n\n    stat_names = ['mean', 'std', 'min', 'max']\n    tof_cols = [c for c in df_processed.columns if c.startswith('tof_') and not any(stat in c for stat in stat_names)]\n    tof_stats = [c for c in df_processed.columns if c.startswith('tof_') and any(stat in c for stat in stat_names)]\n\n    other_feat_cols = [c for c in new_feature_names if c not in tof_stats]\n\n    # ===== Fit scalers only =====\n    if fit:\n        scaler_imu = StandardScaler().fit(df_processed[imu_cols])\n        scaler_thermopile = StandardScaler().fit(df_processed[thermopile_cols])\n        scaler_other = StandardScaler().fit(df_processed[other_feat_cols])\n        scaler_tof_array = StandardScaler().fit(df_processed[tof_cols])\n        tof_stats_per_seq = df_processed.groupby('sequence_id')[tof_stats].mean()\n        scaler_tof_stats = StandardScaler().fit(tof_stats_per_seq)\n\n        # Save scalers\n        for name, scaler in zip(\n            ['imu', 'thermopile', 'other', 'tof_array', 'tof_stats'],\n            [scaler_imu, scaler_thermopile, scaler_other, scaler_tof_array, scaler_tof_stats]\n        ):\n            with open(f'scaler_{name}_{fold}.pkl', 'wb') as f:\n                pickle.dump(scaler, f)\n\n        # Save inference-time metadata only once\n        if fold == 0:\n            columns_for_inference = {\n                'imu_cols_raw': imu_cols_raw,\n                'thm_cols_raw': thermopile_cols_raw,\n                'tof_cols_raw': tof_cols_raw,\n                'imu_cols': imu_cols,\n                'thermopile_cols': thermopile_cols,\n                'tof_cols': tof_cols,\n                'tof_stats': tof_stats,\n                'other_feat_cols': other_feat_cols,\n                'max_seq_len': MAX_LEN,\n                'label_classes': le.classes_.tolist()\n            }\n            with open('columns_for_inference.pkl', 'wb') as f:\n                pickle.dump(columns_for_inference, f)\n\n    return imu_cols_raw, thermopile_cols_raw, tof_cols_raw, imu_cols, thermopile_cols, tof_cols, tof_stats, other_feat_cols, \\\n           scaler_imu, scaler_thermopile, scaler_other, scaler_tof_array, scaler_tof_stats\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.539405Z","iopub.execute_input":"2025-08-20T14:23:34.539643Z","iopub.status.idle":"2025-08-20T14:23:34.556437Z","shell.execute_reply.started":"2025-08-20T14:23:34.539628Z","shell.execute_reply":"2025-08-20T14:23:34.555888Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.optim.lr_scheduler import LambdaLR, ReduceLROnPlateau\n\ndef get_warmup_scheduler(optimizer, warmup_epochs, base_lr):\n    def lr_lambda(epoch):\n        if epoch < warmup_epochs:\n            # linearly increase from small LR to base_lr\n            return float(epoch + 1) / float(warmup_epochs)\n        else:\n            return 1.0\n    return LambdaLR(optimizer, lr_lambda=lr_lambda)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.557403Z","iopub.execute_input":"2025-08-20T14:23:34.557648Z","iopub.status.idle":"2025-08-20T14:23:34.571988Z","shell.execute_reply.started":"2025-08-20T14:23:34.557622Z","shell.execute_reply":"2025-08-20T14:23:34.571379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_one_fold(model, train_loader, val_loader, device, le, y_train_fold,\n                     num_epochs=100, patience=7, fold=0):\n    \n    y_train_fold_encoded = le.transform(y_train_fold)\n    class_weights = compute_class_weight(\n        'balanced', classes=np.unique(y_train_fold_encoded), y=y_train_fold_encoded\n    )\n    criterion = nn.CrossEntropyLoss(\n        weight=torch.tensor(class_weights, dtype=torch.float32)\n    ).to(device)\n    base_lr = 1e-3\n    optimizer = torch.optim.AdamW(model.parameters(), lr=base_lr, weight_decay=1e-4)\n    warmup_epochs = 3\n    warmup_scheduler = get_warmup_scheduler(optimizer, warmup_epochs, base_lr)\n    main_scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs - warmup_epochs)\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        loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs}\", leave=False)\n        \n        for imu, thm, other, tof, tof_stats, labels, gate_target, seq_id in loop:\n            imu, thm, other, tof, tof_stats, labels = (\n                imu.to(device), thm.to(device), other.to(device),\n                tof.to(device), tof_stats.to(device), labels.to(device)\n            )\n            optimizer.zero_grad()\n\n            outputs = model(imu, thm, other, tof, tof_stats=tof_stats)\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            loop.set_postfix(train_loss=total_train_loss / len(train_loader.dataset))\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        current_lr = optimizer.param_groups[0]['lr']\n        print(f\"Batch LR: {current_lr:.6f}\")\n        print(f\"Epoch {epoch+1}/{num_epochs} | Train Loss: {avg_train_loss:.4f} | \"\n              f\"Val Loss: {avg_val_loss:.4f} | Val Acc: {val_acc:.4f} | Val F1: {val_f1:.4f}\")\n\n        if epoch < warmup_epochs:\n            warmup_scheduler.step()\n        else:\n            main_scheduler.step()\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    # Load best model\n    model.load_state_dict(torch.load(f'best_model_{fold}.pth'))\n    model.eval()\n    val_preds_all, val_labels_all, seq_ids_all = [], [], []\n\n    with torch.no_grad():\n        for imu, thm, other, tof, tof_stats, labels, gate_target, seq_ids in val_loader:\n            imu, thm, other, tof, tof_stats = (\n                imu.to(device), thm.to(device), other.to(device),\n                tof.to(device), tof_stats.to(device)\n            )\n            outputs = model(imu, thm, other, tof, tof_stats=tof_stats)\n            val_preds_all.append(outputs.cpu().numpy())\n            val_labels_all.append(labels.cpu().numpy())\n            seq_ids_all.extend(seq_ids)\n\n    val_preds_all = np.concatenate(val_preds_all)\n    val_labels_all = np.concatenate(val_labels_all)\n    print(f\"--- Training Finished. Best Val F1: {best_val_f1:.4f} ---\")\n\n    return best_val_f1, val_preds_all, val_labels_all, seq_ids_all\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.572716Z","iopub.execute_input":"2025-08-20T14:23:34.572911Z","iopub.status.idle":"2025-08-20T14:23:34.585881Z","shell.execute_reply.started":"2025-08-20T14:23:34.572896Z","shell.execute_reply":"2025-08-20T14:23:34.585355Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_seq_slices(df, sequence_ids):\n    seq_lengths = df['sequence_id'].value_counts(sort=False).to_dict()\n    seq_slices = {}\n    start_idx = 0\n    for seq_id in sequence_ids:  # preserves order of provided IDs\n        seq_len = seq_lengths[seq_id]\n        end_idx = start_idx + seq_len\n        seq_slices[seq_id] = (start_idx, end_idx)\n        start_idx = end_idx\n    return seq_slices","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.586644Z","iopub.execute_input":"2025-08-20T14:23:34.586841Z","iopub.status.idle":"2025-08-20T14:23:34.601125Z","shell.execute_reply.started":"2025-08-20T14:23:34.586826Z","shell.execute_reply":"2025-08-20T14:23:34.600449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_fold(train_idx, val_idx, df, X, y, fold, le, labels_dict):\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    MAX_LEN = 128\n\n    # Split sequences\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    train_seq_slices = build_seq_slices(df_train, train_ids)\n    val_seq_slices   = build_seq_slices(df_val, val_ids)\n\n    # === Fit scalers on train ===\n    imu_cols_raw, thm_cols_raw, tof_cols_raw, imu_cols, thm_cols, tof_cols, tof_stats_cols, other_cols, \\\n    scaler_imu, scaler_thm, scaler_other, scaler_tof, scaler_tof_stats = \\\n        preprocess_and_scale(\n            df_train,\n            run_feature_engineering_fast,\n            fold,\n            MAX_LEN,\n            le,\n            fit=True\n        )\n\n    # === For validation we don’t fit again, just ensure col alignment ===\n    preprocess_and_scale(\n        df_val,\n        run_feature_engineering_fast,\n        fold,\n        MAX_LEN,\n        le,\n        fit=False,\n        scaler_imu=scaler_imu,\n        scaler_thermopile=scaler_thm,\n        scaler_other=scaler_other,\n        scaler_tof_array=scaler_tof,\n        scaler_tof_stats=scaler_tof_stats,\n\n    )\n\n    # === Dataset objects (datasets handle scaling internally) ===\n    train_dataset = GestureDatasetNumpy(\n        df=df_train,\n        sequence_ids=train_ids,\n        imu_scaler=scaler_imu,\n        thm_scaler=scaler_thm,\n        other_scaler=scaler_other,\n        tof_scaler=scaler_tof,\n        tof_stats_scaler=scaler_tof_stats,\n        imu_cols=imu_cols,\n        thm_cols=thm_cols,\n        tof_cols=tof_cols,\n        imu_cols_raw = imu_cols_raw, \n        thm_cols_raw = thm_cols_raw,\n        tof_cols_raw = tof_cols_raw,\n        other_feat_cols=other_cols,\n        tof_stats_cols=tof_stats_cols,\n        labels=labels_dict,\n        seq_slices=train_seq_slices,\n        max_seq_len=MAX_LEN,\n        is_train=True,\n        run_feature_engineering=run_feature_engineering_fast\n    )\n\n    val_dataset = GestureDatasetNumpy(\n        sequence_ids=val_ids,\n        df=df_val,\n        imu_scaler=scaler_imu,\n        thm_scaler=scaler_thm,\n        other_scaler=scaler_other,\n        tof_scaler=scaler_tof,\n        tof_stats_scaler=scaler_tof_stats,\n        imu_cols=imu_cols,\n        thm_cols=thm_cols,\n        tof_cols=tof_cols,\n        imu_cols_raw = imu_cols_raw, \n        thm_cols_raw = thm_cols_raw,\n        tof_cols_raw = tof_cols_raw,\n        other_feat_cols=other_cols,\n        tof_stats_cols=tof_stats_cols,\n        labels=labels_dict,\n        seq_slices=val_seq_slices,\n        max_seq_len=MAX_LEN,\n        is_train=False,\n        run_feature_engineering=run_feature_engineering_fast\n    )\n\n    train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)\n    val_loader   = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)\n\n    # === Build model input sizes dynamically from col lengths ===\n    model = CombinedGRUModel(\n        imu_input=len(imu_cols),\n        thm_input=len(thm_cols),\n        other_feat_input=len(other_cols),\n        num_classes=len(le.classes_),\n        tof_stats_dim=len(tof_stats_cols)\n    )\n\n    # === Train & evaluate fold ===\n    y_train_fold = y.iloc[train_idx].values\n    fold_score, val_preds, val_labels, val_seq_ids = process_one_fold(\n        model, train_loader, val_loader, device, le,\n        y_train_fold, fold=fold\n    )\n\n    return fold_score, val_preds, val_labels, val_seq_ids\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:28:52.356303Z","iopub.execute_input":"2025-08-20T14:28:52.357093Z","iopub.status.idle":"2025-08-20T14:28:52.366432Z","shell.execute_reply.started":"2025-08-20T14:28:52.357043Z","shell.execute_reply":"2025-08-20T14:28:52.365629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sgkf = StratifiedGroupKFold(n_splits=10, 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'])\ny_encoded = le.transform(sequence_level_df['gesture'])\nlabels_dict = {seq_id: int_label for seq_id, int_label in zip(sequence_level_df['sequence_id'], y_encoded)}\nwith open('/kaggle/working/label_encoder.pkl', 'wb') as f:\n    pickle.dump(le, f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T14:23:34.617259Z","iopub.execute_input":"2025-08-20T14:23:34.617483Z","iopub.status.idle":"2025-08-20T14:23:36.309725Z","shell.execute_reply.started":"2025-08-20T14:23:34.617463Z","shell.execute_reply":"2025-08-20T14:23:36.308853Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"start_fold = 0\nall_oof_preds = []\nall_oof_labels = []\nall_oof_seq_ids = []\nfold_f1_scores = []\n\nfor fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y, groups)):\n\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    print(f\"\\n{'='*25}\")\n    print(f\"====== FOLD {fold+1} / 10 ======\")\n    print(f\"{'='*25}\")\n    fold_score, val_preds, val_labels, val_seq_ids = process_fold(train_idx, val_idx, df, X, y, fold, le, labels_dict)\n    \n    fold_f1_scores.append(fold_score)\n    all_oof_preds.append(val_preds)\n    all_oof_labels.append(val_labels)\n    all_oof_seq_ids.extend(val_seq_ids)\n    print(f\"\\nBest F1 Score for Fold {fold+1}: {fold_score:.4f}\")\n\nall_oof_preds = np.concatenate(all_oof_preds)\nall_oof_labels = np.concatenate(all_oof_labels)\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-08-20T14:31:59.622411Z","iopub.execute_input":"2025-08-20T14:31:59.622900Z","execution_failed":"2025-08-20T14:33:07.877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}