{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":540507,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":383852,"modelId":403270}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, json, joblib\n# Core data science libraries\nimport numpy as np\nimport pandas as pd\nfrom math import ceil\nfrom scipy import stats\n\n# Visualization libraries\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom pathlib import Path\nimport warnings \nimport random\nwarnings.filterwarnings(\"ignore\")\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.model_selection import StratifiedGroupKFold\n\nimport tensorflow as tf\nfrom tensorflow.keras.utils import Sequence, to_categorical, pad_sequences\nfrom tensorflow.keras.models import Model, load_model\nfrom tensorflow.keras.layers import (\n    Input, Conv1D, BatchNormalization, Activation, add, MaxPooling1D, Dropout,\n    Bidirectional, LSTM, GlobalAveragePooling1D, Dense, Multiply, Reshape,\n    Lambda, Concatenate, GRU, GaussianNoise, Add, GlobalMaxPooling1D,\n    MultiHeadAttention, LayerNormalization\n)\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, LearningRateScheduler\nfrom tensorflow.keras import backend as K\nfrom tensorflow.keras import mixed_precision\n\nimport polars as pl\nfrom scipy.spatial.transform import Rotation as R\nfrom joblib import Parallel, delayed\nimport multiprocessing\n\n# Progress bar\nfrom tqdm import tqdm","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:13.520116Z","iopub.execute_input":"2025-08-25T14:56:13.520877Z","iopub.status.idle":"2025-08-25T14:56:35.500111Z","shell.execute_reply.started":"2025-08-25T14:56:13.520844Z","shell.execute_reply":"2025-08-25T14:56:35.499345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def seed_everything(seed):\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n    tf.experimental.numpy.random.seed(seed)\n    os.environ['TF_CUDNN_DETERMINISTIC'] = '1'\n    os.environ['TF_DETERMINISTIC_OPS'] = '1'\nseed_everything(seed=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.501401Z","iopub.execute_input":"2025-08-25T14:56:35.502002Z","iopub.status.idle":"2025-08-25T14:56:35.506403Z","shell.execute_reply.started":"2025-08-25T14:56:35.501981Z","shell.execute_reply":"2025-08-25T14:56:35.505757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Configuration\ntrain = False                    \nraw_dir = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\npretrained_dir = Path(\"/kaggle/input/cmi-dbsd-trained-weights/tensorflow2/default/15\")\noutput_dir = Path(\"./\")                                    \nbatch_size = 64 \npad_percentile = 95\nlr_init = 4e-4\nwd = 3e-3\nmixup_alpha = 0.4\nepochs = 160  \npatience = 50 \nn_splits = 10 \n\nprint(\"Imports ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.507114Z","iopub.execute_input":"2025-08-25T14:56:35.507299Z","iopub.status.idle":"2025-08-25T14:56:35.546718Z","shell.execute_reply.started":"2025-08-25T14:56:35.507283Z","shell.execute_reply":"2025-08-25T14:56:35.546172Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train:\n    from tensorflow.keras import mixed_precision\n    policy = mixed_precision.Policy('mixed_float16')\n    mixed_precision.set_global_policy(policy)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.548214Z","iopub.execute_input":"2025-08-25T14:56:35.548400Z","iopub.status.idle":"2025-08-25T14:56:35.566491Z","shell.execute_reply.started":"2025-08-25T14:56:35.548385Z","shell.execute_reply":"2025-08-25T14:56:35.565826Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modeling Functions","metadata":{}},{"cell_type":"code","source":"#Tensor Manipulations\ndef time_sum(x):\n    return K.sum(x, axis=1)\n\ndef squeeze_last_axis(x):\n    return tf.squeeze(x, axis=-1)\n\ndef expand_last_axis(x):\n    return tf.expand_dims(x, axis=-1)\n\ndef se_block(x, reduction=8):\n    ch = x.shape[-1]\n    se = GlobalAveragePooling1D()(x)\n    se = Dense(ch // reduction, activation='relu')(se)\n    se = Dense(ch, activation='sigmoid')(se)\n    se = Reshape((1, ch))(se)\n    return Multiply()([x, se])\n\n# Residual CNN Block with SE\ndef residual_se_cnn_block(x, filters, kernel_size, pool_size=2, drop=0.3, wd=1e-4):\n    shortcut = x\n    for _ in range(2):\n        x = Conv1D(filters, kernel_size, padding='same', use_bias=False,\n                   kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x)\n        x = Activation('relu')(x)\n    x = se_block(x)\n    if shortcut.shape[-1] != filters:\n        shortcut = Conv1D(filters, 1, padding='same', use_bias=False,\n                          kernel_regularizer=l2(wd))(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n    x = add([x, shortcut])\n    x = Activation('relu')(x)\n    x = MaxPooling1D(pool_size)(x)\n    x = Dropout(drop)(x)\n    return x\n\ndef attention_layer(inputs):\n    score = Dense(1, activation='tanh')(inputs)\n    score = Lambda(squeeze_last_axis)(score)\n    weights = Activation('softmax')(score)\n    weights = Lambda(expand_last_axis)(weights)\n    context = Multiply()([inputs, weights])\n    context = Lambda(time_sum)(context)\n    return context","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.567307Z","iopub.execute_input":"2025-08-25T14:56:35.567896Z","iopub.status.idle":"2025-08-25T14:56:35.583005Z","shell.execute_reply.started":"2025-08-25T14:56:35.567872Z","shell.execute_reply":"2025-08-25T14:56:35.582233Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Optimized physics calculations using vectorization\ndef remove_gravity_from_acc_vectorized(acc_values, quat_values):\n    \"\"\"Vectorized gravity removal for better performance\"\"\"\n    num_samples = acc_values.shape[0]\n    linear_accel = acc_values.copy()\n    \n    # Filter valid quaternions\n    valid_mask = ~(np.any(np.isnan(quat_values), axis=1) | \n                   np.all(np.isclose(quat_values, 0), axis=1))\n    \n    if np.any(valid_mask):\n        # Process all valid quaternions at once\n        valid_quats = quat_values[valid_mask]\n        \n        # Batch rotation computation\n        try:\n            rotations = R.from_quat(valid_quats)\n            gravity_world = np.array([0, 0, 9.81])\n            \n            # Apply rotations in batch\n            gravity_sensor_frames = rotations.apply(gravity_world, inverse=True)\n            linear_accel[valid_mask] = acc_values[valid_mask] - gravity_sensor_frames\n        except:\n            pass\n            \n    return linear_accel","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.583729Z","iopub.execute_input":"2025-08-25T14:56:35.583950Z","iopub.status.idle":"2025-08-25T14:56:35.601685Z","shell.execute_reply.started":"2025-08-25T14:56:35.583927Z","shell.execute_reply":"2025-08-25T14:56:35.601200Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_angular_velocity_vectorized(quat_values, time_delta=1/200):\n    \"\"\"Vectorized angular velocity calculation\"\"\"\n    num_samples = quat_values.shape[0]\n    angular_vel = np.zeros((num_samples, 3))\n    \n    if num_samples < 2:\n        return angular_vel\n        \n    # Process in chunks for memory efficiency\n    chunk_size = 1000\n    for start in range(0, num_samples - 1, chunk_size):\n        end = min(start + chunk_size, num_samples - 1)\n        \n        q_t = quat_values[start:end]\n        q_t_plus_dt = quat_values[start+1:end+1]\n        \n        # Find valid pairs\n        valid_mask = ~(np.any(np.isnan(q_t), axis=1) | \n                      np.all(np.isclose(q_t, 0), axis=1) |\n                      np.any(np.isnan(q_t_plus_dt), axis=1) | \n                      np.all(np.isclose(q_t_plus_dt, 0), axis=1))\n        \n        if np.any(valid_mask):\n            try:\n                valid_indices = np.where(valid_mask)[0]\n                rot_t = R.from_quat(q_t[valid_mask])\n                rot_t_plus_dt = R.from_quat(q_t_plus_dt[valid_mask])\n                \n                # Batch computation\n                delta_rot = rot_t.inv() * rot_t_plus_dt\n                angular_vel[start + valid_indices] = delta_rot.as_rotvec() / time_delta\n            except:\n                pass\n                \n    return angular_vel","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.602328Z","iopub.execute_input":"2025-08-25T14:56:35.602578Z","iopub.status.idle":"2025-08-25T14:56:35.621400Z","shell.execute_reply.started":"2025-08-25T14:56:35.602562Z","shell.execute_reply":"2025-08-25T14:56:35.620915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_angular_distance(rot_data):\n    \"\"\"Calculate angular distance between consecutive rotations\"\"\"\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n    \n    num_samples = quat_values.shape[0]\n    angular_dist = np.zeros(num_samples)\n    \n    for i in range(num_samples - 1):\n        q1 = quat_values[i]\n        q2 = quat_values[i+1]\n        \n        if np.all(np.isnan(q1)) or np.all(np.isclose(q1, 0)) or \\\n           np.all(np.isnan(q2)) or np.all(np.isclose(q2, 0)):\n            angular_dist[i] = 0\n            continue\n        \n        try:\n            r1 = R.from_quat(q1)\n            r2 = R.from_quat(q2)\n            relative_rotation = r1.inv() * r2\n            angle = np.linalg.norm(relative_rotation.as_rotvec())\n            angular_dist[i] = angle\n        except ValueError:\n            angular_dist[i] = 0\n            \n    return angular_dist","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.622080Z","iopub.execute_input":"2025-08-25T14:56:35.622545Z","iopub.status.idle":"2025-08-25T14:56:35.642759Z","shell.execute_reply.started":"2025-08-25T14:56:35.622522Z","shell.execute_reply":"2025-08-25T14:56:35.642252Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GatedMixupGenerator(Sequence):\n    \"\"\"Enhanced Mixup generator with sensor masking and class weighting\"\"\"\n    def __init__(self, X, y, batch_size, imu_dim, class_weight=None, \n                 alpha=0.4, masking_prob=0.15, noise_level=0.01):\n        self.X, self.y = X, y\n        self.batch = batch_size\n        self.imu_dim = imu_dim\n        self.class_weight = class_weight\n        self.alpha = alpha\n        self.masking_prob = masking_prob\n        self.noise_level = noise_level\n        self.indices = np.arange(len(X))\n        \n    def __len__(self):\n        return int(np.ceil(len(self.X) / self.batch))\n    \n    @property\n    def num_batches(self):\n        return self.__len__()\n        \n    def __getitem__(self, i):\n        idx = self.indices[i*self.batch:(i+1)*self.batch]\n        Xb, yb = self.X[idx].copy(), self.y[idx].copy()\n        \n        # Calculate sample weights\n        sample_weights = np.ones(len(Xb), dtype='float32')\n        if self.class_weight is not None:\n            sample_weights = np.array([\n                self.class_weight.get(np.argmax(y_sample), 1.0) \n                for y_sample in yb\n            ])\n        \n        # Gate targets (1 = ToF/THM active, 0 = masked)\n        gate_target = np.ones(len(Xb), dtype='float32')\n        \n        # Apply sensor masking\n        if self.masking_prob > 0:\n            for j in range(len(Xb)):\n                if np.random.rand() < self.masking_prob:\n                    # Mask ToF/THM features (everything after IMU)\n                    Xb[j, :, self.imu_dim:] = 0\n                    gate_target[j] = 0.0\n        \n        # Apply Mixup\n        if self.alpha > 0:\n            lam = np.random.beta(self.alpha, self.alpha)\n            perm = np.random.permutation(len(Xb))\n            X_mix = lam * Xb + (1 - lam) * Xb[perm]\n            y_mix = lam * yb + (1 - lam) * yb[perm]\n            gate_target_mix = lam * gate_target + (1 - lam) * gate_target[perm]\n            sample_weights_mix = lam * sample_weights + (1 - lam) * sample_weights[perm]\n            \n            # Add noise\n            if self.noise_level > 0:\n                noise = np.random.normal(0, self.noise_level, X_mix.shape)\n                X_mix += noise\n            \n            return (X_mix.astype('float32'), \n                    {'main_output': y_mix.astype('float32'), \n                     'tof_gate': gate_target_mix.astype('float32')}, \n                    sample_weights_mix.astype('float32'))\n        \n        # Add noise to non-mixup data\n        if self.noise_level > 0:\n            noise = np.random.normal(0, self.noise_level, Xb.shape)\n            Xb += noise\n            \n        return (Xb.astype('float32'), \n                {'main_output': yb.astype('float32'), \n                 'tof_gate': gate_target.astype('float32')}, \n                sample_weights.astype('float32'))\n        \n    def on_epoch_end(self):\n        np.random.shuffle(self.indices)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.643378Z","iopub.execute_input":"2025-08-25T14:56:35.643570Z","iopub.status.idle":"2025-08-25T14:56:35.666131Z","shell.execute_reply.started":"2025-08-25T14:56:35.643555Z","shell.execute_reply":"2025-08-25T14:56:35.665657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_gated_two_branch_model(pad_len, imu_dim, tof_dim, n_classes, wd=1e-4):\n    \"\"\"\n    Gated Two-Branch model with proper broadcasting that can be serialized\n    \"\"\"\n    inp = Input(shape=(pad_len, imu_dim + tof_dim))\n    imu = Lambda(lambda t: t[:, :, :imu_dim])(inp)\n    tof = Lambda(lambda t: t[:, :, imu_dim:])(inp)\n    \n    # IMU Branch (Deep)\n    x1 = residual_se_cnn_block(imu, 64, 3, drop=0.1, wd=wd)\n    x1 = residual_se_cnn_block(x1, 128, 5, drop=0.1, wd=wd)\n    \n    # ToF/THM Branch\n    x2_base = Conv1D(64, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(tof)\n    x2_base = BatchNormalization()(x2_base)\n    x2_base = Activation('relu')(x2_base)\n    x2_base = MaxPooling1D(2)(x2_base)\n    x2_base = Dropout(0.2)(x2_base)\n    \n    x2_base = Conv1D(128, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x2_base)\n    x2_base = BatchNormalization()(x2_base)\n    x2_base = Activation('relu')(x2_base)\n    x2_base = MaxPooling1D(2)(x2_base)\n    x2_base = Dropout(0.2)(x2_base)\n    \n    # Gating Mechanism\n    gate_input = GlobalAveragePooling1D()(tof)\n    gate_input = Dense(16, activation='relu', kernel_regularizer=l2(wd))(gate_input)\n    gate = Dense(1, activation='sigmoid', name='tof_gate')(gate_input)\n    \n    # Apply gate with proper broadcasting using Keras layers\n    # Reshape gate from (batch, 1) to (batch, 1, 1)\n    gate_reshaped = Reshape((1, 1))(gate)\n    \n    # Use a Lambda layer with a self-contained function\n    def apply_gate(inputs):\n        x2_tensor, gate_tensor = inputs\n        # Get the temporal dimension dynamically\n        time_steps = tf.shape(x2_tensor)[1]\n        # Tile the gate to match temporal dimension\n        gate_tiled = tf.tile(gate_tensor, [1, time_steps, 1])\n        return x2_tensor * gate_tiled\n    \n    x2 = Lambda(apply_gate)([x2_base, gate_reshaped])\n    \n    # Merge branches\n    merged = Concatenate()([x1, x2])\n    \n    # Dual RNN layers\n    xa = Bidirectional(LSTM(128, return_sequences=True, kernel_regularizer=l2(wd)))(merged)\n    xb = Bidirectional(GRU(128, return_sequences=True, kernel_regularizer=l2(wd)))(merged)\n    xc = GaussianNoise(0.09)(merged)\n    xc = Dense(16, activation='elu')(xc)\n    \n    x = Concatenate()([xa, xb, xc])\n    x = Dropout(0.4)(x)\n    x = attention_layer(x)\n    \n    # Enhanced classifier\n    for units, drop in [(256, 0.5), (128, 0.3)]:\n        x = Dense(units, use_bias=False, kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x)\n        x = Activation('relu')(x)\n        x = Dropout(drop)(x)\n    \n    # Output with float32 for stability\n    x = Dense(n_classes, kernel_regularizer=l2(wd), dtype='float32')(x)\n    out = Activation('softmax', dtype='float32', name='main_output')(x)\n    \n    return Model(inputs=inp, outputs={'main_output': out, 'tof_gate': gate})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.668443Z","iopub.execute_input":"2025-08-25T14:56:35.668829Z","iopub.status.idle":"2025-08-25T14:56:35.690741Z","shell.execute_reply.started":"2025-08-25T14:56:35.668814Z","shell.execute_reply":"2025-08-25T14:56:35.690135Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def cosine_annealing_schedule(epoch, lr):\n    \"\"\"Cosine annealing schedule.\"\"\"\n    epochs = 120\n    lr_max = 5e-4\n    lr_min = 1e-6\n    \n    lr = lr_min + (lr_max - lr_min) * 0.5 * (1 + np.cos(np.pi * epoch / epochs))\n    return lr","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.691336Z","iopub.execute_input":"2025-08-25T14:56:35.691539Z","iopub.status.idle":"2025-08-25T14:56:35.712588Z","shell.execute_reply.started":"2025-08-25T14:56:35.691524Z","shell.execute_reply":"2025-08-25T14:56:35.712090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def reshape_tof_to_grid(tof_data, sensor_num):\n    if isinstance(tof_data, pd.DataFrame):\n        # Extract the 64 pixel values for this sensor\n        pixel_cols = [f\"tof_{sensor_num}_v{i}\" for i in range(64)]\n        pixel_data = tof_data[pixel_cols].values\n    else:\n        pixel_data = tof_data\n    \n    # Replace -1 (no signal) with 0 or NaN\n    pixel_data[pixel_data == -1] = 0\n    \n    # Reshape from (n_samples, 64) to (n_samples, 8, 8)\n    grid_data = pixel_data.reshape(-1, 8, 8)\n    \n    return grid_data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.713276Z","iopub.execute_input":"2025-08-25T14:56:35.713488Z","iopub.status.idle":"2025-08-25T14:56:35.729658Z","shell.execute_reply.started":"2025-08-25T14:56:35.713450Z","shell.execute_reply":"2025-08-25T14:56:35.729099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_tof_3d_matrix(df, sequence_id):\n    # Get sequence data\n    seq_data = df[df['sequence_id'] == sequence_id].copy()\n    seq_data = seq_data.sort_values('step')\n    \n    timesteps = len(seq_data)\n    tof_matrix = np.zeros((timesteps, 5, 8, 8))\n    \n    # Process each ToF sensor\n    for sensor_idx, sensor_num in enumerate(range(1, 6)):\n        grid_data = reshape_tof_to_grid(seq_data, sensor_num)\n        tof_matrix[:, sensor_idx, :, :] = grid_data\n    \n    return tof_matrix","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.730257Z","iopub.execute_input":"2025-08-25T14:56:35.730428Z","iopub.status.idle":"2025-08-25T14:56:35.745585Z","shell.execute_reply.started":"2025-08-25T14:56:35.730415Z","shell.execute_reply":"2025-08-25T14:56:35.744915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_tof_temporal_features(tof_matrix, window_size=5):\n    features = {}\n    \n    # Motion features - difference between consecutive frames\n    motion = np.diff(tof_matrix, axis=0)\n    features['tof_motion_mean'] = np.mean(np.abs(motion), axis=(1, 2, 3))\n    features['tof_motion_std'] = np.std(motion, axis=(1, 2, 3))\n    \n    # Spatial variance over time\n    spatial_var = np.var(tof_matrix, axis=(2, 3))\n    features['tof_spatial_var'] = spatial_var\n    \n    # Center of mass tracking (for each sensor)\n    com_features = []\n    for sensor_idx in range(5):\n        sensor_data = tof_matrix[:, sensor_idx, :, :]\n        com_x = []\n        com_y = []\n        \n        for t in range(len(sensor_data)):\n            frame = sensor_data[t]\n            if np.sum(frame) > 0:\n                # Calculate center of mass\n                y_coords, x_coords = np.meshgrid(range(8), range(8))\n                total_mass = np.sum(frame)\n                com_x.append(np.sum(x_coords * frame) / total_mass)\n                com_y.append(np.sum(y_coords * frame) / total_mass)\n            else:\n                com_x.append(4.0)  # Center position\n                com_y.append(4.0)\n        \n        # Track center of mass movement\n        com_x = np.array(com_x)\n        com_y = np.array(com_y)\n        com_velocity = np.sqrt(np.diff(com_x)**2 + np.diff(com_y)**2)\n        \n        features[f'tof_{sensor_idx+1}_com_velocity_mean'] = np.mean(com_velocity) if len(com_velocity) > 0 else 0\n        features[f'tof_{sensor_idx+1}_com_velocity_std'] = np.std(com_velocity) if len(com_velocity) > 0 else 0\n    \n    return features","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.746331Z","iopub.execute_input":"2025-08-25T14:56:35.746593Z","iopub.status.idle":"2025-08-25T14:56:35.762965Z","shell.execute_reply.started":"2025-08-25T14:56:35.746578Z","shell.execute_reply":"2025-08-25T14:56:35.762485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_cnn_tof_feature_extractor(input_shape=(None, 5, 8, 8)):\n    inputs = Input(shape=input_shape[1:])  # (5, 8, 8)\n    \n    # Process each sensor separately then combine\n    sensor_features = []\n    for i in range(5):\n        # Extract single sensor\n        sensor_input = Lambda(lambda x: x[:, i:i+1, :, :])(inputs)\n        \n        # 2D CNN for spatial features\n        x = Conv2D(16, (3, 3), padding='same', activation='relu')(sensor_input)\n        x = BatchNormalization()(x)\n        x = MaxPooling2D((2, 2))(x)\n        \n        x = Conv2D(32, (3, 3), padding='same', activation='relu')(x)\n        x = BatchNormalization()(x)\n        x = GlobalAveragePooling2D()(x)\n        \n        sensor_features.append(x)\n    \n    # Combine sensor features\n    combined = Concatenate()(sensor_features)\n    features = Dense(64, activation='relu')(combined)\n    features = Dropout(0.3)(features)\n    \n    model = Model(inputs, features)\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.763619Z","iopub.execute_input":"2025-08-25T14:56:35.763817Z","iopub.status.idle":"2025-08-25T14:56:35.783433Z","shell.execute_reply.started":"2025-08-25T14:56:35.763795Z","shell.execute_reply":"2025-08-25T14:56:35.782807Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_temporal_tof_model(sequence_length, n_classes):\n    # Input for ToF 3D matrices\n    tof_input = Input(shape=(sequence_length, 5, 8, 8))\n    \n    # Use TimeDistributed to apply CNN to each timestep\n    cnn_extractor = build_cnn_tof_feature_extractor()\n    tof_features = TimeDistributed(cnn_extractor)(tof_input)\n    \n    # LSTM for temporal modeling\n    x = LSTM(128, return_sequences=True)(tof_features)\n    x = LSTM(64)(x)\n    x = Dropout(0.4)(x)\n    \n    # Classification\n    x = Dense(128, activation='relu')(x)\n    x = Dropout(0.3)(x)\n    outputs = Dense(n_classes, activation='softmax')(x)\n    \n    model = Model(tof_input, outputs)\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.784151Z","iopub.execute_input":"2025-08-25T14:56:35.784332Z","iopub.status.idle":"2025-08-25T14:56:35.805817Z","shell.execute_reply.started":"2025-08-25T14:56:35.784311Z","shell.execute_reply":"2025-08-25T14:56:35.805307Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Process Sequence","metadata":{}},{"cell_type":"code","source":"# Parallel processing for sequence features\ndef process_sequence(seq_data):\n    \"\"\"Process a single sequence with all feature engineering\"\"\"\n    seq_data = seq_data.copy()\n    \n    # Dictionary to collect all new columns\n    new_columns = {}\n    \n    # Get numpy arrays for faster processing\n    acc_values = seq_data[['acc_x', 'acc_y', 'acc_z']].values\n    quat_values = seq_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n\n    try:\n        rotations = R.from_quat(quat_values)\n        euler_angles = rotations.as_euler('xyz', degrees=True)\n        new_columns['euler_x'] = euler_angles[:, 0]\n        new_columns['euler_y'] = euler_angles[:, 1]\n        new_columns['euler_z'] = euler_angles[:, 2]\n    except:\n        new_columns['euler_x'] = np.zeros(len(seq_data))\n        new_columns['euler_y'] = np.zeros(len(seq_data))\n        new_columns['euler_z'] = np.zeros(len(seq_data))\n    \n    # Linear acceleration\n    linear_accel = remove_gravity_from_acc_vectorized(acc_values, quat_values)\n    new_columns['linear_acc_x'] = linear_accel[:, 0]\n    new_columns['linear_acc_y'] = linear_accel[:, 1]\n    new_columns['linear_acc_z'] = linear_accel[:, 2]\n\n    # Acceleration features focusing on z and y axes\n    new_columns['acc_yz_mag'] = np.sqrt(seq_data['acc_y']**2 + seq_data['acc_z']**2)\n    new_columns['acc_y_z_ratio'] = seq_data['acc_y'] / (seq_data['acc_z'] + 1e-8)\n    \n    # Magnitudes\n    new_columns['acc_mag'] = np.linalg.norm(acc_values, axis=1)\n    new_columns['linear_acc_mag'] = np.linalg.norm(linear_accel, axis=1)\n    \n    # Jerk (simplified)\n    new_columns['linear_acc_mag_jerk'] = np.gradient(new_columns['linear_acc_mag']) * 200\n    \n    # Angular velocity\n    angular_vel = calculate_angular_velocity_vectorized(quat_values)\n    new_columns['angular_vel_x'] = angular_vel[:, 0]\n    new_columns['angular_vel_y'] = angular_vel[:, 1]\n    new_columns['angular_vel_z'] = angular_vel[:, 2]\n    \n    # ToF aggregations (vectorized)\n    for i in range(1, 6):\n        pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]\n        tof_data = seq_data[pixel_cols].values\n        tof_data[tof_data == -1] = np.nan\n        \n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n            new_columns[f'tof_{i}_mean'] = np.nanmean(tof_data, axis=1)\n            new_columns[f'tof_{i}_std'] = np.nanstd(tof_data, axis=1)\n            new_columns[f'tof_{i}_min'] = np.nanmin(tof_data, axis=1)\n            new_columns[f'tof_{i}_max'] = np.nanmax(tof_data, axis=1)\n\n    # Add rotation-specific features for rot_z and rot_w\n    new_columns['rot_z_w_product'] = seq_data['rot_z'] * seq_data['rot_w']\n    new_columns['rot_z_w_poly']  = seq_data['rot_z'] + (seq_data['rot_w'] ** 2)\n    new_columns['rot_w_z_poly'] = seq_data['rot_w'] + (seq_data['rot_z'] ** 2)\n    new_columns['rot_z_w_ratio'] = seq_data['rot_z'] / (seq_data['rot_w'] + 1e-8)\n\n    # Rolling statistics for important features (window of 10 samples = 50ms)\n    for col in ['rot_z', 'rot_w', 'acc_z', 'acc_y', 'thm_2']:\n        new_columns[f'{col}_rolling_mean'] = seq_data[col].rolling(10, center=True).mean()\n        new_columns[f'{col}_rolling_std'] = seq_data[col].rolling(10, center=True).std()\n        new_columns[f'{col}_diff'] = seq_data[col].diff()\n\n    new_columns['rot_w_rolling_mean_sqrd'] = new_columns['rot_w_rolling_mean'] ** 2\n    new_columns['acc_z_rolling_std_sqrd'] = new_columns['acc_z_rolling_std'] ** 2\n    \n    # Thermopile 2 specific features\n    thm_2_mean = seq_data['thm_2'].mean()\n    thm_2_std = seq_data['thm_2'].std()\n    new_columns['thm_2_normalized'] = (seq_data['thm_2'] - thm_2_mean) / (thm_2_std + 1e-8)\n    new_columns['thm_2_delta_from_mean'] = seq_data['thm_2'] - seq_data[['thm_1', 'thm_2', 'thm_3', 'thm_4', 'thm_5']].mean(axis=1)\n\n    # ToF band statistics for tof_1 and tof_2 (statistically significant bands)\n    bands_tof_1_2 = [\n        (3, 7),    # Band 1: v[3-7]\n        (11, 15),  # Band 2: v[11-15]\n        (19, 23),  # Band 3: v[19-23]\n        (27, 31),  # Band 4: v[27-31]\n        (35, 39),  # Band 5: v[35-39]\n        (43, 47),  # Band 6: v[43-47]\n        (51, 55),  # Band 7: v[51-55]\n        (59, 63),  # Band 8: v[59-63]\n    ]\n    \n    for tof_num in [1, 2]:  # Only process tof_1 and tof_2\n        for band_idx, (start, end) in enumerate(bands_tof_1_2, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = seq_data[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                new_columns[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1)\n                \n    # ToF band statistics for tof_3 (statistically significant bands)\n    bands_tof_3 = [\n        (0, 47),    \n        (50, 55)\n    ]\n    \n    for tof_num in [3]:  # Only process tof_3\n        for band_idx, (start, end) in enumerate(bands_tof_3, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = seq_data[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                new_columns[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1)\n                \n    # ToF band statistics for tof_4 (statistically significant bands)\n    bands_tof_4 = [\n        (0, 3),    \n        (7, 9),\n        (15, 16),\n        (21, 23),\n        (28, 31),\n        (35, 39),\n        (43, 47),\n        (50, 55),\n        (58, 63)\n    ]\n    \n    for tof_num in [4]:  # Only process tof_4\n        for band_idx, (start, end) in enumerate(bands_tof_4, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = seq_data[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                new_columns[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1)\n                \n    # ToF band statistics for tof_5 (statistically significant bands)\n    bands_tof_5 = [\n        (1, 7),    \n        (9, 15),\n        (18, 23),\n        (48, 49),\n        (56, 61)  \n    ]\n    \n    for tof_num in [5]:  # Only process tof_5\n        for band_idx, (start, end) in enumerate(bands_tof_5, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = seq_data[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                new_columns[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                new_columns[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1) \n\n    # added features\n    new_columns['angular_distance'] = calculate_angular_distance(quat_values)\n    new_columns['acc_mag_jerk'] = np.gradient(new_columns['acc_mag']) * 200\n    new_columns['rot_angle'] = 2 * np.arccos(np.clip(seq_data['rot_w'], -1, 1))\n    new_columns['rot_angle_vel'] = np.gradient(new_columns['rot_angle']) * 200\n\n    # new features\n    new_columns['linear_acc_mag_th2_delta_mean'] = new_columns['linear_acc_mag'] * new_columns['thm_2_delta_from_mean']\n    new_columns['rot_rolling_std_total'] = new_columns['rot_w_rolling_std'] + new_columns['rot_z_rolling_std']\n    new_columns['acc_rolling_std_total'] = new_columns['acc_y_rolling_std'] + new_columns['acc_z_rolling_std']\n    new_columns['rot_w_linear_acc_mag_product'] = new_columns['linear_acc_mag'] * seq_data['rot_w']\n    new_columns['thm2_linear_acc_mag_product'] = new_columns['linear_acc_mag'] * seq_data['thm_2']\n    new_columns['thm_1_2_sum'] = seq_data['thm_1'] + seq_data['thm_2']\n    new_columns['thm_1_2_product'] = seq_data['thm_1'] * seq_data['thm_2']\n    new_columns['tof_1_2_max_sum'] = new_columns['tof_1_max'] + new_columns['tof_2_max']\n    \n    # Create ToF 3D matrix features\n    timesteps = len(seq_data)\n    \n    # Initialize arrays for each feature\n    tof_active_pixels = {f'tof_{i}_active_pixels': np.zeros(timesteps) for i in range(1, 6)}\n    tof_com_x = {f'tof_{i}_com_x': np.zeros(timesteps) for i in range(1, 6)}\n    tof_com_y = {f'tof_{i}_com_y': np.zeros(timesteps) for i in range(1, 6)}\n    \n    for sensor_num in range(1, 6):\n        # Reshape each sensor's data into 8x8 grid\n        pixel_cols = [f\"tof_{sensor_num}_v{i}\" for i in range(64)]\n        pixel_data = seq_data[pixel_cols].values\n        pixel_data[pixel_data == -1] = 0  # Replace no-signal with 0\n        \n        # Calculate spatial features for each timestep\n        for t in range(timesteps):\n            frame = pixel_data[t].reshape(8, 8)\n            \n            # Active region size\n            active_pixels = np.sum(frame > 0)\n            tof_active_pixels[f'tof_{sensor_num}_active_pixels'][t] = active_pixels\n            \n            # Spatial center of mass\n            if np.sum(frame) > 0:\n                y_coords, x_coords = np.meshgrid(range(8), range(8))\n                total_mass = np.sum(frame)\n                com_x = np.sum(x_coords * frame) / total_mass\n                com_y = np.sum(y_coords * frame) / total_mass\n                tof_com_x[f'tof_{sensor_num}_com_x'][t] = com_x\n                tof_com_y[f'tof_{sensor_num}_com_y'][t] = com_y\n            else:\n                tof_com_x[f'tof_{sensor_num}_com_x'][t] = 4.0  # Center\n                tof_com_y[f'tof_{sensor_num}_com_y'][t] = 4.0  # Center\n    \n    # Add ToF 3D features to new_columns\n    new_columns.update(tof_active_pixels)\n    new_columns.update(tof_com_x)\n    new_columns.update(tof_com_y)\n    \n    # Calculate ToF motion features (temporal differences)\n    for sensor_num in range(1, 6):\n        # COM velocity\n        com_x_vals = tof_com_x[f'tof_{sensor_num}_com_x']\n        com_y_vals = tof_com_y[f'tof_{sensor_num}_com_y']\n        \n        if len(com_x_vals) > 1:\n            com_velocity = np.sqrt(np.diff(com_x_vals)**2 + np.diff(com_y_vals)**2)\n            # Pad with 0 at the beginning to maintain array length\n            com_velocity = np.concatenate([[0], com_velocity])\n            new_columns[f'tof_{sensor_num}_com_velocity'] = com_velocity\n        else:\n            new_columns[f'tof_{sensor_num}_com_velocity'] = np.zeros(timesteps)\n    \n    # Create DataFrame from new columns and concatenate with original\n    new_columns_df = pd.DataFrame(new_columns, index=seq_data.index)\n    result_df = pd.concat([seq_data, new_columns_df], axis=1)\n    \n    return result_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.806543Z","iopub.execute_input":"2025-08-25T14:56:35.806772Z","iopub.status.idle":"2025-08-25T14:56:35.835422Z","shell.execute_reply.started":"2025-08-25T14:56:35.806752Z","shell.execute_reply":"2025-08-25T14:56:35.834787Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Predict Function","metadata":{}},{"cell_type":"code","source":"# Prediction function for individual sequences\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    # Convert to pandas for compatibility\n    df_seq = sequence.to_pandas()\n    \n    # Get numpy arrays for faster processing\n    acc_values = df_seq[['acc_x', 'acc_y', 'acc_z']].values\n    quat_values = df_seq[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    \n    # Euler angles\n    try:\n        rotations = R.from_quat(quat_values)\n        euler_angles = rotations.as_euler('xyz', degrees=True)\n        df_seq['euler_x'] = euler_angles[:, 0]\n        df_seq['euler_y'] = euler_angles[:, 1]\n        df_seq['euler_z'] = euler_angles[:, 2]\n    except:\n        df_seq['euler_x'] = np.zeros(len(df_seq))\n        df_seq['euler_y'] = np.zeros(len(df_seq))\n        df_seq['euler_z'] = np.zeros(len(df_seq))\n    \n    # Linear acceleration\n    linear_accel = remove_gravity_from_acc_vectorized(acc_values, quat_values)\n    df_seq['linear_acc_x'] = linear_accel[:, 0]\n    df_seq['linear_acc_y'] = linear_accel[:, 1]\n    df_seq['linear_acc_z'] = linear_accel[:, 2]\n    \n    # Acceleration features focusing on z and y axes\n    df_seq['acc_yz_mag'] = np.sqrt(df_seq['acc_y']**2 + df_seq['acc_z']**2)\n    df_seq['acc_y_z_ratio'] = df_seq['acc_y'] / (df_seq['acc_z'] + 1e-8)\n    \n    # Magnitudes\n    df_seq['acc_mag'] = np.linalg.norm(acc_values, axis=1)\n    df_seq['linear_acc_mag'] = np.linalg.norm(linear_accel, axis=1)\n    \n    # Jerk (simplified)\n    df_seq['linear_acc_mag_jerk'] = np.gradient(df_seq['linear_acc_mag']) * 200\n    \n    # Angular velocity\n    angular_vel = calculate_angular_velocity_vectorized(quat_values)\n    df_seq['angular_vel_x'] = angular_vel[:, 0]\n    df_seq['angular_vel_y'] = angular_vel[:, 1]\n    df_seq['angular_vel_z'] = angular_vel[:, 2]\n    \n    # ToF aggregations (vectorized)\n    for i in range(1, 6):\n        pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]\n        tof_data = df_seq[pixel_cols].values\n        tof_data[tof_data == -1] = np.nan\n        \n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n            df_seq[f'tof_{i}_mean'] = np.nanmean(tof_data, axis=1)\n            df_seq[f'tof_{i}_std'] = np.nanstd(tof_data, axis=1)\n            df_seq[f'tof_{i}_min'] = np.nanmin(tof_data, axis=1)\n            df_seq[f'tof_{i}_max'] = np.nanmax(tof_data, axis=1)\n    \n    # Add rotation-specific features for rot_z and rot_w\n    df_seq['rot_z_w_product'] = df_seq['rot_z'] * df_seq['rot_w']\n    df_seq['rot_z_w_ratio'] = df_seq['rot_z'] / (df_seq['rot_w'] + 1e-8)\n    \n    # Rolling statistics for important features (window of 10 samples = 50ms)\n    for col in ['rot_z', 'rot_w', 'acc_z', 'acc_y', 'thm_2']:\n        df_seq[f'{col}_rolling_mean'] = df_seq[col].rolling(10, center=True).mean()\n        df_seq[f'{col}_rolling_std'] = df_seq[col].rolling(10, center=True).std()\n        df_seq[f'{col}_diff'] = df_seq[col].diff()\n    \n    # Thermopile 2 specific features\n    thm_2_mean = df_seq['thm_2'].mean()\n    thm_2_std = df_seq['thm_2'].std()\n    df_seq['thm_2_normalized'] = (df_seq['thm_2'] - thm_2_mean) / (thm_2_std + 1e-8)\n    df_seq['thm_2_delta_from_mean'] = df_seq['thm_2'] - df_seq[['thm_1', 'thm_2', 'thm_3', 'thm_4', 'thm_5']].mean(axis=1)\n\n    # added features\n    df_seq['angular_distance'] = calculate_angular_distance(quat_values)\n    df_seq['acc_mag_jerk'] = np.gradient(df_seq['acc_mag']) * 200\n    df_seq['rot_angle'] = 2 * np.arccos(np.clip(df_seq['rot_w'], -1, 1))\n    df_seq['rot_angle_vel'] = np.gradient(df_seq['rot_angle']) * 200\n    \n    # ToF band statistics for tof_1 and tof_2 (statistically significant bands)\n    bands_tof_1_2 = [\n        (3, 7),    # Band 1: v[3-7]\n        (11, 15),  # Band 2: v[11-15]\n        (19, 23),  # Band 3: v[19-23]\n        (27, 31),  # Band 4: v[27-31]\n        (35, 39),  # Band 5: v[35-39]\n        (43, 47),  # Band 6: v[43-47]\n        (51, 55),  # Band 7: v[51-55]\n        (59, 63),  # Band 8: v[59-63]\n    ]\n    \n    for tof_num in [1, 2]:  # Only process tof_1 and tof_2\n        for band_idx, (start, end) in enumerate(bands_tof_1_2, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = df_seq[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                df_seq[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1)\n    \n    # ToF band statistics for tof_3 (statistically significant bands)\n    bands_tof_3 = [\n        (0, 47),    \n        (50, 55)\n    ]\n    \n    for tof_num in [3]:  # Only process tof_3\n        for band_idx, (start, end) in enumerate(bands_tof_3, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = df_seq[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                df_seq[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1)\n    \n    # ToF band statistics for tof_4 (statistically significant bands)\n    bands_tof_4 = [\n        (0, 3),    \n        (7, 9),\n        (15, 16),\n        (21, 23),\n        (28, 31),\n        (35, 39),\n        (43, 47),\n        (50, 55),\n        (58, 63)\n    ]\n    \n    for tof_num in [4]:  # Only process tof_4\n        for band_idx, (start, end) in enumerate(bands_tof_4, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = df_seq[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                df_seq[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1)\n    \n    # ToF band statistics for tof_5 (statistically significant bands)\n    bands_tof_5 = [\n        (1, 7),    \n        (9, 15),\n        (18, 23),\n        (48, 49),\n        (56, 61)  \n    ]\n    \n    for tof_num in [5]:  # Only process tof_5\n        for band_idx, (start, end) in enumerate(bands_tof_5, 1):\n            # Get columns for this band (inclusive range)\n            band_cols = [f\"tof_{tof_num}_v{p}\" for p in range(start, end + 1)]\n            band_data = df_seq[band_cols].values\n            band_data[band_data == -1] = np.nan\n            \n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", category=RuntimeWarning)\n                df_seq[f'tof_{tof_num}_band{band_idx}_mean'] = np.nanmean(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_std'] = np.nanstd(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_var'] = np.nanvar(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_min'] = np.nanmin(band_data, axis=1)\n                df_seq[f'tof_{tof_num}_band{band_idx}_max'] = np.nanmax(band_data, axis=1)\n\n    # Add rotation-specific features for rot_z and rot_w (matching process_sequence)\n    df_seq['rot_z_w_poly'] = df_seq['rot_z'] + (df_seq['rot_w'] ** 2)\n    df_seq['rot_w_z_poly'] = df_seq['rot_w'] + (df_seq['rot_z'] ** 2)\n    df_seq['rot_w_rolling_mean_sqrd'] = df_seq['rot_w_rolling_mean'] ** 2\n    df_seq['acc_z_rolling_std_sqrd'] = df_seq['acc_z_rolling_std'] ** 2\n    \n    # New features matching process_sequence\n    df_seq['linear_acc_mag_th2_delta_mean'] = df_seq['linear_acc_mag'] * df_seq['thm_2_delta_from_mean']\n    df_seq['rot_rolling_std_total'] = df_seq['rot_w_rolling_std'] + df_seq['rot_z_rolling_std']\n    df_seq['acc_rolling_std_total'] = df_seq['acc_y_rolling_std'] + df_seq['acc_z_rolling_std']\n    df_seq['rot_w_linear_acc_mag_product'] = df_seq['linear_acc_mag'] * df_seq['rot_w']\n    df_seq['thm2_linear_acc_mag_product'] = df_seq['linear_acc_mag'] * df_seq['thm_2']\n    df_seq['thm_1_2_sum'] = df_seq['thm_1'] + df_seq['thm_2']\n    df_seq['thm_1_2_product'] = df_seq['thm_1'] * df_seq['thm_2']\n    df_seq['tof_1_2_max_sum'] = df_seq['tof_1_max'] + df_seq['tof_2_max']\n    \n    # Add missing jerk features\n    df_seq['acc_mag_jerk'] = np.gradient(df_seq['acc_mag']) * 200\n    df_seq['rot_angle_vel'] = np.gradient(df_seq['rot_angle']) * 200\n    \n    # Add ToF 3D matrix features (same as process_sequence)\n    timesteps = len(df_seq)\n    \n    for sensor_num in range(1, 6):\n        # Initialize columns\n        df_seq[f'tof_{sensor_num}_active_pixels'] = 0.0\n        df_seq[f'tof_{sensor_num}_com_x'] = 4.0\n        df_seq[f'tof_{sensor_num}_com_y'] = 4.0\n        df_seq[f'tof_{sensor_num}_com_velocity'] = 0.0\n        \n        # Reshape each sensor's data into 8x8 grid\n        pixel_cols = [f\"tof_{sensor_num}_v{i}\" for i in range(64)]\n        pixel_data = df_seq[pixel_cols].values\n        pixel_data[pixel_data == -1] = 0\n        \n        com_x_vals = np.zeros(timesteps)\n        com_y_vals = np.zeros(timesteps)\n        \n        # Calculate spatial features for each timestep\n        for t in range(timesteps):\n            frame = pixel_data[t].reshape(8, 8)\n            \n            # Active region size\n            active_pixels = np.sum(frame > 0)\n            df_seq.loc[df_seq.index[t], f'tof_{sensor_num}_active_pixels'] = active_pixels\n            \n            # Spatial center of mass\n            if np.sum(frame) > 0:\n                y_coords, x_coords = np.meshgrid(range(8), range(8))\n                total_mass = np.sum(frame)\n                com_x = np.sum(x_coords * frame) / total_mass\n                com_y = np.sum(y_coords * frame) / total_mass\n                df_seq.loc[df_seq.index[t], f'tof_{sensor_num}_com_x'] = com_x\n                df_seq.loc[df_seq.index[t], f'tof_{sensor_num}_com_y'] = com_y\n                com_x_vals[t] = com_x\n                com_y_vals[t] = com_y\n            else:\n                com_x_vals[t] = 4.0\n                com_y_vals[t] = 4.0\n        \n        # COM velocity\n        if timesteps > 1:\n            com_velocity = np.sqrt(np.diff(com_x_vals)**2 + np.diff(com_y_vals)**2)\n            com_velocity = np.concatenate([[0], com_velocity])\n            df_seq[f'tof_{sensor_num}_com_velocity'] = com_velocity\n\n    \n    # Extract features and scale\n    mat_unscaled = df_seq[final_feature_cols].fillna(0).values.astype('float32')\n    mat_scaled = scaler.transform(mat_unscaled)\n    \n    # Pad sequence\n    pad_input = pad_sequences([mat_scaled], maxlen=pad_len, padding='post', \n                             truncating='post', dtype='float32')\n    \n    # Ensemble prediction\n    all_preds = []\n    for model in models:\n        outputs = model.predict(pad_input, verbose=0)\n        # Handle both single and multi-output models\n        if isinstance(outputs, dict):\n            pred = outputs['main_output'][0]\n        elif isinstance(outputs, list):\n            pred = outputs[0][0]\n        else:\n            pred = outputs[0]\n        all_preds.append(pred)\n    \n    # Average predictions\n    avg_pred = np.mean(all_preds, axis=0)\n    predicted_class_idx = avg_pred.argmax()\n    \n    # Return gesture class name\n    return gesture_classes[predicted_class_idx]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.836213Z","iopub.execute_input":"2025-08-25T14:56:35.836393Z","iopub.status.idle":"2025-08-25T14:56:35.865299Z","shell.execute_reply.started":"2025-08-25T14:56:35.836379Z","shell.execute_reply":"2025-08-25T14:56:35.864803Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Adding Separate Data Load Function","metadata":{}},{"cell_type":"code","source":"# Load data\ntrain_df = pd.read_csv(raw_dir / \"train.csv\")\ntrain_dem_df = pd.read_csv(raw_dir / \"train_demographics.csv\")\ntrain_df = pd.merge(train_df, train_dem_df, on='subject', how='left')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:56:35.865944Z","iopub.execute_input":"2025-08-25T14:56:35.866409Z","iopub.status.idle":"2025-08-25T14:57:07.787368Z","shell.execute_reply.started":"2025-08-25T14:56:35.866388Z","shell.execute_reply":"2025-08-25T14:57:07.786812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def fill_missing_values(df):\n    # Fill categorical columns with 'N/A'\n    cat_cols = df.select_dtypes(include=['object']).columns\n    df[cat_cols] = df[cat_cols].fillna(\"N/A\")\n    \n    # Fill all numerical columns with 0 (int8, int16, int32, int64, uint8, uint16, uint32, uint64, float16, float32, float64, complex64, complex128)\n    num_cols = df.select_dtypes(include=[np.number]).columns  \n    df[num_cols] = df[num_cols].replace([np.inf, -np.inf, '', None], np.nan).fillna(0)\n    \n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:57:07.788184Z","iopub.execute_input":"2025-08-25T14:57:07.788478Z","iopub.status.idle":"2025-08-25T14:57:07.793345Z","shell.execute_reply.started":"2025-08-25T14:57:07.788426Z","shell.execute_reply":"2025-08-25T14:57:07.792504Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# fill missing values and NaN in train data set\ntrain_df = fill_missing_values(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:57:07.794182Z","iopub.execute_input":"2025-08-25T14:57:07.794487Z","iopub.status.idle":"2025-08-25T14:57:14.682108Z","shell.execute_reply.started":"2025-08-25T14:57:07.794439Z","shell.execute_reply":"2025-08-25T14:57:14.681567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_data(df):\n    df = df.copy()\n    \n    print(\"Processing sequences with parallel computation...\")\n    \n    # Group by sequence\n    sequences = [group for _, group in df.groupby('sequence_id')]\n    \n    # Process in parallel\n    n_cores = multiprocessing.cpu_count()\n    print(f\"Using {n_cores} cores for parallel processing\")\n    \n    processed_sequences = Parallel(n_jobs=n_cores)(\n        delayed(process_sequence)(seq) for seq in sequences\n    )\n\n    # Combine processed sequences\n    df = pd.concat(processed_sequences, ignore_index=True)\n    \n    return df\n\nprocessed_df = load_data(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T14:57:14.682777Z","iopub.execute_input":"2025-08-25T14:57:14.683130Z","iopub.status.idle":"2025-08-25T15:01:29.352623Z","shell.execute_reply.started":"2025-08-25T14:57:14.683108Z","shell.execute_reply":"2025-08-25T15:01:29.352005Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def feature_engineering(df):   \n    df = df.copy()\n\n    # checks N/A before combining\n    def combine_or_na(a, b):\n        if 'N/A' in str(a) or 'N/A' in str(b):\n            return 'N/A'\n        return f\"{a}_{b}\"\n\n    # combine orientation and gesture\n    df['orientation_gesture'] = df.apply(lambda x: combine_or_na(x['orientation'], x['gesture']), axis=1).astype('category') \n        \n    # behavioural boolean columns\n    df['performs_gesture'] = df['behavior'].str.contains('Performs gesture', case=False, na=False)\n    df['move_hand_to_target'] = df['behavior'].str.contains('Moves hand to target location', case=False, na=False)\n    df['hand_at_target'] = df['behavior'].str.contains('Hand at target location', case=False, na=False)\n    df['relaxes_moves_hand_to_target'] = df['behavior'].str.contains('Relaxes and moves hand to target location', case=False, na=False)\n    \n    return df\n\nfe_train_df = feature_engineering(processed_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:01:29.353723Z","iopub.execute_input":"2025-08-25T15:01:29.353993Z","iopub.status.idle":"2025-08-25T15:01:57.631965Z","shell.execute_reply.started":"2025-08-25T15:01:29.353974Z","shell.execute_reply":"2025-08-25T15:01:57.631386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fe_train_df = fill_missing_values(fe_train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:01:57.632616Z","iopub.execute_input":"2025-08-25T15:01:57.632788Z","iopub.status.idle":"2025-08-25T15:02:07.886081Z","shell.execute_reply.started":"2025-08-25T15:01:57.632775Z","shell.execute_reply":"2025-08-25T15:02:07.885521Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":" # Encode labels\nle = LabelEncoder()\nfe_train_df['gesture_int'] = le.fit_transform(fe_train_df['gesture'])\ngesture_classes = le.classes_\nnp.save(output_dir / \"gesture_classes.npy\", gesture_classes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:07.886903Z","iopub.execute_input":"2025-08-25T15:02:07.887244Z","iopub.status.idle":"2025-08-25T15:02:07.998525Z","shell.execute_reply.started":"2025-08-25T15:02:07.887224Z","shell.execute_reply":"2025-08-25T15:02:07.997806Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get categorical columns\ncat_features = fe_train_df.select_dtypes(include=['object', 'category', 'bool']).columns.tolist()\ncat_features","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:07.999315Z","iopub.execute_input":"2025-08-25T15:02:07.999566Z","iopub.status.idle":"2025-08-25T15:02:08.248590Z","shell.execute_reply.started":"2025-08-25T15:02:07.999540Z","shell.execute_reply":"2025-08-25T15:02:08.247867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Dictionary to store all encoder mappings\nencoder_mappings = {}\n\nprint(\"Label Encoding Categorical Features: \",end=\"\")\nfor c in cat_features:\n    print(f\"{c}, \",end=\"\")\n    fe_train_df[c] =  fe_train_df[c].astype('category')\n    \n    full_le = LabelEncoder()\n    fe_train_df[c] = full_le.fit_transform(fe_train_df[c].astype(str))\n\n    # Save the mapping of numerical values to string labels\n    encoder_mappings[c] = {\n        i: label for i, label in enumerate(full_le.classes_)\n    }\n\nwith open('new_categorical_encoder_mappings.json', 'w') as f:\n    json.dump(encoder_mappings, f, indent=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:08.249350Z","iopub.execute_input":"2025-08-25T15:02:08.249559Z","iopub.status.idle":"2025-08-25T15:02:14.368483Z","shell.execute_reply.started":"2025-08-25T15:02:08.249542Z","shell.execute_reply":"2025-08-25T15:02:14.367660Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the plot output path\nplot_output_path = Path('plots')\n\n# Create directory if it doesn't exist\nplot_output_path.mkdir(exist_ok=True)\nprint(f\"Directory '{plot_output_path}' is ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.371931Z","iopub.execute_input":"2025-08-25T15:02:14.372166Z","iopub.status.idle":"2025-08-25T15:02:14.376533Z","shell.execute_reply.started":"2025-08-25T15:02:14.372151Z","shell.execute_reply":"2025-08-25T15:02:14.375830Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Ttest analysis\ndef perform_ttest_analysis(df, group_col, group1_value, group2_value, output_path):  \n    # Filter data for the two groups\n    group1_data = df[df[group_col] == group1_value].copy()\n    group2_data = df[df[group_col] == group2_value].copy()\n    \n    print(f\"Group 1 ({group1_value}): {len(group1_data)} records\")\n    print(f\"Group 2 ({group2_value}): {len(group2_data)} records\")\n    \n    # Get numerical columns\n    numerical_cols = df.select_dtypes(include=['int64', 'float64', 'int32', 'float32']).columns.tolist()\n    \n    # Remove the group column if it's numerical\n    if group_col in numerical_cols:\n        numerical_cols.remove(group_col)\n    \n    print(f\"Testing {len(numerical_cols)} numerical variables\")\n    \n    # Initialize results list\n    results = []\n    \n    # Perform t-tests for each numerical column\n    for col in tqdm(numerical_cols, desc=\"Running t-tests and creating plots\"):\n        try:\n            # Get data for both groups for this column (remove NaN values)\n            group1_values = group1_data[col].dropna()\n            group2_values = group2_data[col].dropna()\n            \n            # Skip if insufficient data\n            if len(group1_values) < 2 or len(group2_values) < 2:\n                print(f\"Warning: Insufficient data for {col} (Group1: {len(group1_values)}, Group2: {len(group2_values)})\")\n                continue\n            \n            # Perform Levene's test for equal variances\n            levene_stat, levene_p = stats.levene(group1_values, group2_values)\n            equal_var = levene_p > 0.05\n            \n            # Perform independent t-test\n            t_stat, p_value = stats.ttest_ind(group1_values, group2_values, equal_var=equal_var)\n            \n            # Calculate effect size (Cohen's d)\n            if equal_var:\n                pooled_std = np.sqrt(((len(group1_values) - 1) * group1_values.var() + \n                                     (len(group2_values) - 1) * group2_values.var()) / \n                                    (len(group1_values) + len(group2_values) - 2))\n            else:\n                pooled_std = np.sqrt((group1_values.var() + group2_values.var()) / 2)\n            \n            cohens_d = (group1_values.mean() - group2_values.mean()) / pooled_std if pooled_std != 0 else 0\n            \n            # Calculate 95% confidence interval for the difference in means\n            diff_mean = group2_values.mean() - group1_values.mean()\n            se_diff = np.sqrt(group1_values.var()/len(group1_values) + group2_values.var()/len(group2_values))\n            t_critical = stats.t.ppf(0.975, len(group1_values) + len(group2_values) - 2)\n            ci_lower = diff_mean - t_critical * se_diff\n            ci_upper = diff_mean + t_critical * se_diff\n            \n            # Store results\n            results.append({\n                'variable': col,\n                'group1_mean': group1_values.mean(),\n                'group1_std': group1_values.std(),\n                'group1_median': group1_values.median(),\n                'group1_count': len(group1_values),\n                'group2_mean': group2_values.mean(),\n                'group2_std': group2_values.std(),\n                'group2_median': group2_values.median(),\n                'group2_count': len(group2_values),\n                'mean_difference': diff_mean,\n                'ci_lower': ci_lower,\n                'ci_upper': ci_upper,\n                't_statistic': t_stat,\n                'p_value': p_value,\n                'cohens_d': cohens_d,\n                'equal_variances': equal_var,\n                'levene_p_value': levene_p,\n                'significant_005': p_value < 0.05,\n                'significant_001': p_value < 0.01,\n                'significant_0001': p_value < 0.001\n            })\n            \n            # Create comparison plot\n            create_comparison_plot(group_col, group1_values, group2_values, col, \n                                 group1_value, group2_value, \n                                 t_stat, p_value, cohens_d, output_path)\n            \n        except Exception as e:\n            print(f\"Error processing {col}: {str(e)}\")\n            continue\n    \n    # Create results DataFrame\n    results_df = pd.DataFrame(results)\n    \n    if len(results_df) > 0:\n        # Sort by p-value\n        results_df = results_df.sort_values('p_value').reset_index(drop=True)\n        \n        # Add interpretation columns\n        results_df['effect_size_interpretation'] = results_df['cohens_d'].abs().apply(interpret_cohens_d)\n        results_df['significance_level'] = results_df.apply(get_significance_level, axis=1)\n    \n    return results_df\n\ndef interpret_cohens_d(d):\n    \"\"\"Interpret Cohen's d effect size\"\"\"\n    d_abs = abs(d)\n    if d_abs < 0.2:\n        return \"negligible\"\n    elif d_abs < 0.5:\n        return \"small\"\n    elif d_abs < 0.8:\n        return \"medium\"\n    else:\n        return \"large\"\n\ndef get_significance_level(row):\n    \"\"\"Get significance level string\"\"\"\n    if row['p_value'] < 0.001:\n        return \"*** (p < 0.001)\"\n    elif row['p_value'] < 0.01:\n        return \"** (p < 0.01)\"\n    elif row['p_value'] < 0.05:\n        return \"* (p < 0.05)\"\n    else:\n        return \"Not significant\"\n\ndef create_comparison_plot(group_col, group1_values, group2_values, col_name, \n                          group1_name, group2_name, t_stat, p_value, cohens_d, output_path):\n    \"\"\"Create comprehensive comparison plot for two groups\"\"\"\n    \n    # Set up the figure with subplots\n    fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n    fig.suptitle(f'Statistical Comparison: {col_name}', fontsize=16, fontweight='bold')\n    \n    # Color scheme\n    color1 = '#ff7f7f'  # Light red for group 1\n    color2 = '#7fbfff'  # Light blue for group 2\n    \n    # 1. Histogram comparison (top left)\n    ax1 = axes[0, 0]\n    \n    # Calculate bins for better visualization\n    all_values = np.concatenate([group1_values, group2_values])\n    bins = np.histogram_bin_edges(all_values, bins=30)\n    \n    ax1.hist(group1_values, bins=bins, alpha=0.7, label=f'{group1_name} (n={len(group1_values)})', \n             color=color1, density=True, edgecolor='black', linewidth=0.5)\n    ax1.hist(group2_values, bins=bins, alpha=0.7, label=f'{group2_name} (n={len(group2_values)})', \n             color=color2, density=True, edgecolor='black', linewidth=0.5)\n    \n    # Add mean lines\n    ax1.axvline(group1_values.mean(), color='darkred', linestyle='--', linewidth=2, \n                label=f'{group1_name} Mean: {group1_values.mean():.2f}')\n    ax1.axvline(group2_values.mean(), color='darkblue', linestyle='--', linewidth=2,\n                label=f'{group2_name} Mean: {group2_values.mean():.2f}')\n    \n    ax1.set_title('Distribution Comparison (Histogram)')\n    ax1.set_xlabel(col_name)\n    ax1.set_ylabel('Density')\n    ax1.legend(fontsize=9)\n    ax1.grid(True, alpha=0.3)\n    \n    # 2. Box plot comparison (top right)\n    ax2 = axes[0, 1]\n    \n    box_data = [group1_values, group2_values]\n    box_labels = [f'{group1_name}\\n(n={len(group1_values)})', f'{group2_name}\\n(n={len(group2_values)})']\n    \n    bp = ax2.boxplot(box_data, labels=box_labels, patch_artist=True, \n                     boxprops=dict(alpha=0.7), showfliers=True)\n    bp['boxes'][0].set_facecolor(color1)\n    bp['boxes'][1].set_facecolor(color2)\n    \n    ax2.set_title('Box Plot Comparison')\n    ax2.set_ylabel(col_name)\n    ax2.grid(True, alpha=0.3)\n    \n    # 3. KDE plot (bottom left)\n    ax3 = axes[1, 0]\n    \n    try:\n        # Only plot KDE if we have enough data points\n        if len(group1_values) > 3:\n            sns.kdeplot(data=group1_values, label=f'{group1_name}', \n                       color='darkred', ax=ax3, linewidth=2)\n        if len(group2_values) > 3:\n            sns.kdeplot(data=group2_values, label=f'{group2_name}', \n                       color='darkblue', ax=ax3, linewidth=2)\n    except:\n        # Fallback to simple line plot if KDE fails\n        ax3.hist(group1_values, alpha=0.5, density=True, color=color1, label=group1_name)\n        ax3.hist(group2_values, alpha=0.5, density=True, color=color2, label=group2_name)\n    \n    ax3.set_title('Density Comparison (KDE)')\n    ax3.set_xlabel(col_name)\n    ax3.set_ylabel('Density')\n    ax3.legend()\n    ax3.grid(True, alpha=0.3)\n    \n    # 4. Statistics summary (bottom right)\n    ax4 = axes[1, 1]\n    \n    # Calculate additional statistics\n    effect_size_interp = interpret_cohens_d(cohens_d)\n    sig_level = get_significance_level({'p_value': p_value})\n    \n    stats_text = f'T-Test Results:\\n'\n    stats_text += f'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n'\n    stats_text += f'T-statistic: {t_stat:.4f}\\n'\n    stats_text += f'P-value: {p_value:.2e}\\n'\n    stats_text += f'Cohen\\'s d: {cohens_d:.4f} ({effect_size_interp})\\n'\n    stats_text += f'Significance: {sig_level}\\n\\n'\n    \n    stats_text += f'{group1_name}:\\n'\n    stats_text += f'  Mean ± SD: {group1_values.mean():.2f} ± {group1_values.std():.2f}\\n'\n    stats_text += f'  Median: {group1_values.median():.2f}\\n'\n    stats_text += f'  Range: [{group1_values.min():.2f}, {group1_values.max():.2f}]\\n'\n    stats_text += f'  Count: {len(group1_values)}\\n\\n'\n    \n    stats_text += f'{group2_name}:\\n'\n    stats_text += f'  Mean ± SD: {group2_values.mean():.2f} ± {group2_values.std():.2f}\\n'\n    stats_text += f'  Median: {group2_values.median():.2f}\\n'\n    stats_text += f'  Range: [{group2_values.min():.2f}, {group2_values.max():.2f}]\\n'\n    stats_text += f'  Count: {len(group2_values)}\\n\\n'\n    \n    # Difference\n    mean_diff = group2_values.mean() - group1_values.mean()\n    stats_text += f'Mean Difference: {mean_diff:.2f}\\n'\n    stats_text += f'({group2_name} - {group1_name})\\n\\n'\n    \n    # Effect size interpretation\n    stats_text += f'Effect Size Interpretation:\\n'\n    if abs(cohens_d) < 0.2:\n        stats_text += f'Negligible practical difference'\n    elif abs(cohens_d) < 0.5:\n        stats_text += f'Small practical difference'\n    elif abs(cohens_d) < 0.8:\n        stats_text += f'Medium practical difference'\n    else:\n        stats_text += f'Large practical difference'\n    \n    ax4.text(0.05, 0.95, stats_text,\n             transform=ax4.transAxes,\n             verticalalignment='top',\n             horizontalalignment='left',\n             bbox=dict(boxstyle='round,pad=0.5', facecolor='lightgray', alpha=0.8),\n             fontsize=10, fontfamily='monospace')\n    ax4.set_xlim(0, 1)\n    ax4.set_ylim(0, 1)\n    ax4.axis('off')\n    \n    plt.tight_layout()\n    \n    # Save with significance indicator in filename\n    if p_value < 0.001:\n        sig_suffix = \"_highly_significant\"\n    elif p_value < 0.05:\n        sig_suffix = \"_significant\"\n    else:\n        sig_suffix = \"_not_significant\"\n    \n    plt.savefig(f'{output_path}/{group_col}_{col_name}_ttest_{group1_name}_vs_{group2_name}_{sig_suffix}_{effect_size_interp}.png', \n                dpi=300, bbox_inches='tight', facecolor='white')\n    plt.close()\n\n\n# Perform the analysis\n#ttest_results = perform_ttest_analysis(\n#    df=fe_train_df, \n#    group_col='performs_gesture',\n#    group1_value=True,\n#    group2_value=False,\n#    output_path=plot_output_path\n#)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.377317Z","iopub.execute_input":"2025-08-25T15:02:14.377566Z","iopub.status.idle":"2025-08-25T15:02:14.418942Z","shell.execute_reply.started":"2025-08-25T15:02:14.377550Z","shell.execute_reply":"2025-08-25T15:02:14.418278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_correlation_matrix(df, output_path):\n    df_copy = df.copy()   \n\n    # list of features to remove based on this analysis. This isn't used anywhere except to track which\n    # features to remove based on variance and correlation\n    features_to_remove = [\n        \n    ]\n\n    # features tested and slated to keep\n    features_to_keep = [\n        \n\n    ]\n    \n    # Features to test\n    key_features = ['acc_x', \n            'acc_y', \n            'acc_z', \n            'linear_acc_x', \n            'linear_acc_y',\n            'linear_acc_z',\n            'rot_x', \n            'rot_y', \n            'rot_z', \n            'rot_w',\n            'acc_mag', \n            'linear_acc_mag', \n            #'linear_acc_mag_jerk', low correlation\n            'angular_vel_x', \n            'angular_vel_y', \n            #'angular_vel_z',\n           # added columns\n            #'acc_y_rolling_mean',\n            'acc_y_rolling_std',\n            'acc_z_rolling_mean',\n            'acc_z_rolling_std',\n            'rot_w_rolling_mean',\n            #'rot_w_rolling_std', low correlation\n            #'rot_z_w_product',\n            #'euler_x',\n            #'euler_y',\n            #'euler_z',\n            'angular_distance',\n            'rot_angle',\n            'linear_acc_mag_th2_delta_mean',\n            'rot_z_w_poly',\n            'rot_w_z_poly',\n            'acc_z_rolling_std_sqrd',\n            'rot_w_linear_acc_mag_product',\n                    'thm_1',\n        #'thm_2_delta_from_mean',\n        'thm_2_rolling_std',\n        'thm_2',\n        'thm_3',\n        'thm_4',\n        'thm_5',\n        #added columns\n        'thm2_linear_acc_mag_product',\n        'thm_1_2_sum',\n        'thm_1_2_product',\n                    'tof_1_2_max_sum',\n        'tof_1_std',\n        'tof_2_std',\n        'tof_3_std',\n        'tof_4_std',\n        'tof_5_std',\n                    'tof_1_active_pixels',\n        'tof_1_com_x', \n        'tof_1_com_y',\n        'tof_1_com_velocity',\n        'tof_2_active_pixels',\n        'tof_2_com_y',\n        'tof_2_com_velocity',\n        'tof_3_active_pixels', \n        'tof_3_com_y',\n        'tof_3_com_velocity',\n        'tof_4_active_pixels',\n        'tof_4_com_x', \n        'tof_4_com_y',\n        'tof_4_com_velocity',\n        'tof_5_active_pixels',\n        'tof_5_com_x',\n                    'tof_5_band5_std',\n        'tof_5_band5_var',\n        'tof_3_band1_min',\n\n                    'orientation_gesture',\n 'performs_gesture',\n 'move_hand_to_target',\n 'hand_at_target',\n 'relaxes_moves_hand_to_target',\n    ]\n    \n    # Target variables\n    target_variables = [\n       'gesture_int',\n        'gesture'\n    ]\n    \n    # Combine features and targets\n    features_to_analyze = key_features + target_variables\n    \n    if len(features_to_analyze) > 1:\n        plt.figure(figsize=(80, 80))\n        \n        # Create correlation matrix\n        correlation_matrix = df_copy[features_to_analyze].corr()\n        \n        # Create mask to highlight target correlations\n        mask = np.zeros_like(correlation_matrix)\n        mask[:-2, :-2] = True  # Mask everything except target correlations\n        \n        # Plot heatmap\n        sns.heatmap(correlation_matrix, \n                   annot=True, \n                   cmap='coolwarm', \n                   center=0,\n                   fmt='.2f',\n                   mask=mask,\n                   vmin=-1, \n                   vmax=1)\n        \n        plt.title('Feature-Target Correlations')\n        plt.tight_layout()\n        plt.savefig(f'{output_path}/engineered_features_target_correlation_matrix.png', bbox_inches='tight')\n        plt.show()\n        plt.close()\n        \n        # Print sorted correlations with targets\n        print(\"\\nCorrelations with targets (sorted):\")\n        for target in target_variables:\n            print(f\"\\n{target} correlations:\")\n            target_corr = correlation_matrix[target].sort_values(ascending=False)\n            print(target_corr[target_corr.index != target])  # Exclude self-correlation\n\n    return features_to_keep, features_to_remove, key_features\n\n#features_to_keep, features_to_remove, key_features = plot_correlation_matrix(fe_train_df, plot_output_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.419577Z","iopub.execute_input":"2025-08-25T15:02:14.419766Z","iopub.status.idle":"2025-08-25T15:02:14.440711Z","shell.execute_reply.started":"2025-08-25T15:02:14.419748Z","shell.execute_reply":"2025-08-25T15:02:14.440131Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fe_train_df['linear_acc_mag_jerk'].head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.441486Z","iopub.execute_input":"2025-08-25T15:02:14.441722Z","iopub.status.idle":"2025-08-25T15:02:14.463768Z","shell.execute_reply.started":"2025-08-25T15:02:14.441700Z","shell.execute_reply":"2025-08-25T15:02:14.463195Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = fe_train_df.columns.tolist()\n# cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.464361Z","iopub.execute_input":"2025-08-25T15:02:14.464554Z","iopub.status.idle":"2025-08-25T15:02:14.477001Z","shell.execute_reply.started":"2025-08-25T15:02:14.464540Z","shell.execute_reply":"2025-08-25T15:02:14.476506Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define Cast layer for loading models trained with mixed precision\nclass Cast(tf.keras.layers.Layer):\n    def __init__(self, dtype='float32', **kwargs):\n        super().__init__(**kwargs)\n        self.target_dtype = dtype\n        \n    def call(self, inputs):\n        return tf.cast(inputs, self.target_dtype)\n    \n    def get_config(self):\n        config = super().get_config()\n        config.update({'dtype': self.target_dtype})\n        return config","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.477592Z","iopub.execute_input":"2025-08-25T15:02:14.477839Z","iopub.status.idle":"2025-08-25T15:02:14.493033Z","shell.execute_reply.started":"2025-08-25T15:02:14.477824Z","shell.execute_reply":"2025-08-25T15:02:14.492509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Additional helper functions\ndef apply_gate(inputs):\n    \"\"\"Apply gate to ToF branch with proper broadcasting\"\"\"\n    x2_tensor, gate_tensor = inputs\n    # Get the temporal dimension dynamically\n    time_steps = tf.shape(x2_tensor)[1]\n    # Tile the gate to match temporal dimension\n    gate_tiled = tf.tile(gate_tensor, [1, time_steps, 1])\n    return x2_tensor * gate_tiled\n\ndef split_imu(t):\n    \"\"\"Split IMU features from input\"\"\"\n    return t[:, :, :imu_dim]  # Note: imu_dim needs to be accessible\n\ndef split_tof(t):\n    \"\"\"Split ToF features from input\"\"\"\n    return t[:, :, imu_dim:]  # Note: imu_dim needs to be accessible","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.493750Z","iopub.execute_input":"2025-08-25T15:02:14.494317Z","iopub.status.idle":"2025-08-25T15:02:14.509275Z","shell.execute_reply.started":"2025-08-25T15:02:14.494292Z","shell.execute_reply":"2025-08-25T15:02:14.508746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define IMU columns outside of training function\nimu_cols = [\n        # main model imu features\n        'linear_acc_x',\n        'linear_acc_y',\n        'linear_acc_z',\n        'rot_w',\n        'rot_x',\n        'rot_y', \n        'rot_z',\n        'linear_acc_mag',\n        'linear_acc_mag_jerk',\n        'angular_vel_x',\n        'angular_vel_y',\n        'angular_vel_z',\n        'angular_distance',\n        # added imu features        \n        'acc_x', \n        'acc_y', \n        'acc_z', \n        'acc_mag', \n        'acc_y_rolling_mean',\n        'acc_y_rolling_std',\n        'acc_z_rolling_mean',\n        'acc_z_rolling_std',\n        'rot_w_rolling_mean',\n        #'rot_w_rolling_std', low correlation\n        #'rot_z_w_product',\n        #'euler_x',\n        #'euler_y',\n        #'euler_z',\n        'rot_angle',\n        'linear_acc_mag_th2_delta_mean',\n        'rot_z_w_poly',\n        'rot_w_z_poly',\n        'acc_z_rolling_std_sqrd',\n        'rot_w_linear_acc_mag_product',\n           ]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.509854Z","iopub.execute_input":"2025-08-25T15:02:14.510105Z","iopub.status.idle":"2025-08-25T15:02:14.529586Z","shell.execute_reply.started":"2025-08-25T15:02:14.510086Z","shell.execute_reply":"2025-08-25T15:02:14.529018Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train:\n    print(\"TRIANING MODE\")\n    \n    thm_cols = [\n        # main model features\n        'thm_1',\n        'thm_2',\n        'thm_3',\n        'thm_4',\n        'thm_5',\n        # added thm features\n        #'thm_2_delta_from_mean',\n        'thm_2_rolling_std',\n        'thm2_linear_acc_mag_product',\n        'thm_1_2_sum',\n        'thm_1_2_product',\n    ]\n    tof_aggregated_cols = [\n        # main model features\n        'tof_1_mean',\n        'tof_1_std',\n        'tof_1_min',\n        'tof_1_max',\n        'tof_2_mean',\n        'tof_2_std',\n        'tof_2_min',\n        'tof_2_max',\n        'tof_3_mean',\n        'tof_3_std',\n        'tof_3_min',\n        'tof_3_max',\n        'tof_4_mean',\n        'tof_4_std',\n        'tof_4_min',\n        'tof_4_max',\n        'tof_5_mean',\n        'tof_5_std',\n        'tof_5_min',\n        'tof_5_max',\n        # added tof features\n        'tof_1_2_max_sum',\n    ]\n\n   # Add ToF 3D matrix features\n    tof_3d_features = [\n        'tof_1_active_pixels',\n        #'tof_1_com_x', \n        #'tof_1_com_y',\n        'tof_1_com_velocity',\n        'tof_2_active_pixels',\n        #'tof_2_com_y',\n        'tof_2_com_velocity',\n        'tof_3_active_pixels', \n        #'tof_3_com_y',\n        'tof_3_com_velocity',\n        'tof_4_active_pixels',\n        #'tof_4_com_x', \n        #'tof_4_com_y',\n        'tof_4_com_velocity',\n        'tof_5_active_pixels',\n        #'tof_5_com_x', \n    ]\n    \n    \n    # Add ToF band features (matching process_sequence)\n    tof_band_features = [\n        #'tof_5_band5_std',\n        #'tof_5_band5_var',\n        #'tof_3_band1_min',\n    ]\n\n    # define final features\n    final_feature_cols = imu_cols + thm_cols + tof_aggregated_cols + tof_3d_features + tof_band_features\n    imu_dim = len(imu_cols)\n    tof_thm_dim = len(thm_cols) + len(tof_aggregated_cols) + len(tof_3d_features) + len(tof_band_features)\n    \n    print(f\"IMU features: {imu_dim} | All other features: {tof_thm_dim}\")\n    np.save(output_dir / \"feature_cols.npy\", np.array(final_feature_cols))\n    \n    # Build sequences efficiently\n    print(\"Building sequences...\")\n    seq_gp = fe_train_df.groupby('sequence_id')\n    X_list_unscaled = []\n    y_list_int = []\n    groups_list = []\n    lens = []\n    \n    for seq_id, seq_df in seq_gp:\n        X_list_unscaled.append(\n            seq_df[final_feature_cols].fillna(0).values.astype('float32')\n        )\n        y_list_int.append(seq_df['gesture_int'].iloc[0])\n        groups_list.append(seq_df['subject'].iloc[0])\n        lens.append(len(seq_df))\n    \n    # Scaling\n    print(\"Fitting StandardScaler...\")\n    all_steps_concatenated = np.concatenate(X_list_unscaled, axis=0)\n    scaler = StandardScaler().fit(all_steps_concatenated)\n    joblib.dump(scaler, output_dir / \"scaler.pkl\")\n    \n    # Scale and pad\n    X_scaled_list = [scaler.transform(x_seq) for x_seq in X_list_unscaled]\n    pad_len = int(np.percentile(lens, pad_percentile))\n    np.save(output_dir / \"sequence_maxlen.npy\", pad_len)\n    \n    X = pad_sequences(X_scaled_list, maxlen=pad_len, padding='post', \n                      truncating='post', dtype='float32')\n    y_stratify = np.array(y_list_int)\n    groups = np.array(groups_list)\n    y = to_categorical(y_list_int, num_classes=len(le.classes_))\n    \n    print(f\"Final data shape: X={X.shape}, y={y.shape}\")\n    \n    # Cross-validation with reduced folds\n    print(f\"\\nStarting training with {n_splits}-fold CV...\")\n    sgkf = StratifiedGroupKFold(n_splits=n_splits, shuffle=True, random_state=42)\n\n    # Calculate class weights (add this before the CV loop)\n    class_weights = compute_class_weight(\n        'balanced',\n        classes=np.unique(y_stratify),\n        y=y_stratify\n    )\n    class_weight_dict = {i: w for i, w in enumerate(class_weights)}\n    print(f\"Class weights: {class_weight_dict}\")\n    \n    for fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y_stratify, groups)):\n        print(f\"\\n{'='*20} FOLD {fold+1}/{n_splits} {'='*20}\")\n        \n        X_tr, X_val = X[train_idx], X[val_idx]\n        y_tr, y_val = y[train_idx], y[val_idx]\n        \n        # Build gated model\n        model = build_gated_two_branch_model(\n            pad_len=pad_len, \n            imu_dim=imu_dim, \n            tof_dim=tof_thm_dim, \n            n_classes=len(le.classes_), \n            wd=wd\n        )\n        \n        # Compile with multi-task loss\n        opt = Adam(learning_rate=lr_init)\n        if train:  # Only use mixed precision optimizer in training\n            opt = mixed_precision.LossScaleOptimizer(opt)\n        \n        model.compile(\n            optimizer=opt,\n            loss={\n                'main_output': tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n                'tof_gate': 'binary_crossentropy'\n            },\n            loss_weights={\n                'main_output': 1.0,\n                'tof_gate': 0.1  # Auxiliary task weight\n            },\n            metrics={\n                'main_output': 'accuracy',\n                'tof_gate': 'accuracy'\n            }\n        )\n        \n        # Fixed callbacks with proper mode specification\n        callbacks = [\n            EarlyStopping(\n                monitor='val_main_output_loss',\n                mode='min',  # Minimize loss\n                patience=patience, \n                restore_best_weights=True, \n                verbose=1\n            ),\n            tf.keras.callbacks.LearningRateScheduler(cosine_annealing_schedule),\n            tf.keras.callbacks.ReduceLROnPlateau(\n                monitor='val_main_output_loss',\n                mode='min',  # Minimize loss\n                factor=0.5, \n                patience=10, \n                min_lr=1e-6, \n                verbose=1\n            ),\n            tf.keras.callbacks.ModelCheckpoint(\n                str(output_dir / f'model_fold_{fold}_best.h5'),\n                save_best_only=True,\n                monitor='val_main_output_loss',\n                mode='min',  # Minimize loss\n                verbose=1\n            )\n        ]\n        \n        # Train with gated generator\n        train_gen = GatedMixupGenerator(\n            X_tr, y_tr, \n            batch_size=batch_size,\n            imu_dim=imu_dim,\n            class_weight=class_weight_dict,\n            alpha=mixup_alpha,\n            masking_prob=0.20,  # chance to mask ToF/THM\n            noise_level=0.01\n        )\n        \n        # Prepare validation data\n        val_gate_targets = np.ones(len(X_val), dtype='float32')  # All sensors active in validation\n        \n        history = model.fit(\n            train_gen,\n            epochs=epochs,\n            validation_data=(\n                X_val, \n                {'main_output': y_val, 'tof_gate': val_gate_targets}\n            ),\n            callbacks=callbacks,\n            verbose=1\n        )\n        \n        # Save model\n        model.save(output_dir / f\"model_fold_{fold}_final.h5\")\n        \n        # Optionally print best validation scores\n        best_val_loss = min(history.history['val_main_output_loss'])\n        best_val_acc = max(history.history['val_main_output_accuracy'])\n        print(f\"Fold {fold+1} - Best val_loss: {best_val_loss:.4f}, Best val_acc: {best_val_acc:.4f}\")\n        \n        # Clear session\n        tf.keras.backend.clear_session()\n    \n    print(\"\\nTraining completed!\")\nelse:\n    # INFERENCE MODE\n    print(\"INFERENCE MODE – Loading artifacts from\", pretrained_dir)\n    \n    # Load pretrained artifacts\n    final_feature_cols = np.load(pretrained_dir / \"feature_cols.npy\", allow_pickle=True).tolist()\n    pad_len = int(np.load(pretrained_dir / \"sequence_maxlen.npy\"))\n    scaler = joblib.load(pretrained_dir / \"scaler.pkl\")\n    gesture_classes = np.load(pretrained_dir / \"gesture_classes.npy\", allow_pickle=True)\n    \n    # Extract dimensions from loaded feature columns\n    #imu_cols = [col for col in final_feature_cols if col.startswith(('acc_', 'rot_', 'linear_', 'angular_', 'euler_'))]\n    imu_dim = len(imu_cols)\n    tof_thm_dim = len(final_feature_cols) - imu_dim\n    \n    print(f\"Loaded artifacts:\")\n    print(f\"  - Feature columns: {len(final_feature_cols)}\")\n    print(f\"  - Sequence max length: {pad_len}\")\n    print(f\"  - Gesture classes: {len(gesture_classes)}\")\n    print(f\"  - IMU dim: {imu_dim}, ToF+THM dim: {tof_thm_dim}\")\n    \n    # Disable mixed precision for inference to avoid Cast layer issues\n    from tensorflow.keras import mixed_precision\n    mixed_precision.set_global_policy('float32')\n    \n    # Custom objects for model loading\n    # Custom objects for model loading\n    custom_objs = {\n        'time_sum': time_sum,\n        'squeeze_last_axis': squeeze_last_axis,\n        'expand_last_axis': expand_last_axis,\n        'se_block': se_block,\n        'residual_se_cnn_block': residual_se_cnn_block,\n        'attention_layer': attention_layer,\n        'Cast': Cast,\n        'apply_gate': apply_gate,  # Add this\n        'split_imu': split_imu,    # Add this if using custom split\n        'split_tof': split_tof     # Add this if using custom split\n    }\n    \n    # Load models\n    models = []\n    print(f\"\\n▶ Loading {n_splits} models for ensemble inference...\")\n    for fold in range(n_splits):\n        model_path = pretrained_dir / f\"model_fold_{fold}_final.h5\"\n        print(f\"  Loading model: {model_path}\")\n        model = load_model(model_path, compile=False, custom_objects=custom_objs)\n        models.append(model)\n    print(f\"[INFO] Successfully loaded {len(models)} models\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:14.530383Z","iopub.execute_input":"2025-08-25T15:02:14.530676Z","iopub.status.idle":"2025-08-25T15:02:24.104723Z","shell.execute_reply.started":"2025-08-25T15:02:14.530653Z","shell.execute_reply":"2025-08-25T15:02:24.103935Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train:\n    # Custom objects for model loading\n    custom_objs = {\n        'time_sum': time_sum,\n        'squeeze_last_axis': squeeze_last_axis,\n        'expand_last_axis': expand_last_axis,\n        'se_block': se_block,\n        'residual_se_cnn_block': residual_se_cnn_block,\n        'attention_layer': attention_layer,\n        'Cast': Cast,\n        'apply_gate': apply_gate,  # Add this\n        'split_imu': split_imu,    # Add this if using custom split\n        'split_tof': split_tof     # Add this if using custom split\n    }\n    \n    # Load trained models\n    models = []\n    print(f\"\\n▶ Loading {n_splits} models for ensemble inference...\")\n    for fold in range(n_splits):\n        model_path = output_dir / f\"model_fold_{fold}_final.h5\"\n        print(f\"  Loading model: {model_path}\")\n        model = load_model(model_path, compile=False, custom_objects=custom_objs)\n        models.append(model)\n    print(f\"[INFO] Successfully loaded {len(models)} models\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:24.105578Z","iopub.execute_input":"2025-08-25T15:02:24.105808Z","iopub.status.idle":"2025-08-25T15:02:24.110876Z","shell.execute_reply.started":"2025-08-25T15:02:24.105783Z","shell.execute_reply":"2025-08-25T15:02:24.110127Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Kaggle evaluation server setup\nimport kaggle_evaluation.cmi_inference_server\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    print(\"\\n▶ Running local gateway for testing...\")\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T15:02:24.111614Z","iopub.execute_input":"2025-08-25T15:02:24.111934Z","iopub.status.idle":"2025-08-25T15:02:39.755076Z","shell.execute_reply.started":"2025-08-25T15:02:24.111906Z","shell.execute_reply":"2025-08-25T15:02:39.754409Z"}},"outputs":[],"execution_count":null}]}