{"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"},{"sourceId":12736664,"sourceType":"datasetVersion","datasetId":8050951},{"sourceId":12860513,"sourceType":"datasetVersion","datasetId":8134486},{"sourceId":12882361,"sourceType":"datasetVersion","datasetId":8150170},{"sourceId":12903918,"sourceType":"datasetVersion","datasetId":8164662},{"sourceId":12937288,"sourceType":"datasetVersion","datasetId":8186685},{"sourceId":12938280,"sourceType":"datasetVersion","datasetId":8187378},{"sourceId":12940799,"sourceType":"datasetVersion","datasetId":8189129}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install --no-index --no-deps /kaggle/input/autogluon-new/wheelhouse/autogluon-1.4.0-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/autogluon-new/wheelhouse/autogluon.common-1.4.0-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/autogluon-new/wheelhouse/autogluon.core-1.4.0-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/autogluon-new/wheelhouse/autogluon.features-1.4.0-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/autogluon-new/wheelhouse/autogluon.multimodal-1.4.0-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/autogluon-new/wheelhouse/autogluon.tabular-1.4.0-py3-none-any.whl\n!pip install --no-index --no-deps /kaggle/input/autogluon-new/wheelhouse/autogluon.timeseries-1.4.0-py3-none-any.whl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:47.769179Z","iopub.execute_input":"2025-08-25T06:52:47.769674Z","iopub.status.idle":"2025-08-25T06:52:58.205327Z","shell.execute_reply.started":"2025-08-25T06:52:47.769652Z","shell.execute_reply":"2025-08-25T06:52:58.204363Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport polars.selectors as cs\nimport joblib\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport matplotlib.pyplot as plt\nfrom autogluon.tabular import TabularDataset, TabularPredictor\n\n# World coordinate transformation\nfrom scipy.spatial.transform import Rotation as R\n\n\nimport kaggle_evaluation.cmi_inference_server\n\n# model\nfrom lightgbm import LGBMClassifier, log_evaluation, early_stopping\nfrom sklearn.model_selection import StratifiedGroupKFold, PredefinedSplit\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.metrics import f1_score, accuracy_score\n\nprint(\" * All imports loaded successfully!\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.206893Z","iopub.execute_input":"2025-08-25T06:52:58.207208Z","iopub.status.idle":"2025-08-25T06:52:58.213388Z","shell.execute_reply.started":"2025-08-25T06:52:58.207177Z","shell.execute_reply":"2025-08-25T06:52:58.212643Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Config:\n    # path\n    TRAIN_PATH = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\"\n    TRAIN_DEMOGRAPHICS_PATH = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\"\n    TEST_PATH = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\"\n    TEST_DEMOGRAPHICS_PATH = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\"\n\n    # Training parameters\n    SEED = 42\n    N_FOLDS = 5\n    SEQ_WINDOW = 60\n\n    # Feature columns\n    ACC_COLS = ['acc_x', 'acc_y', 'acc_z']\n    ROT_COLS = ['rot_w', 'rot_x', 'rot_y', 'rot_z']\n    \n# Set reproducibility\nnp.random.seed(Config.SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.214239Z","iopub.execute_input":"2025-08-25T06:52:58.214961Z","iopub.status.idle":"2025-08-25T06:52:58.234504Z","shell.execute_reply.started":"2025-08-25T06:52:58.214939Z","shell.execute_reply":"2025-08-25T06:52:58.233884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Gesture mapping (targets 0-7 are BFRB, 8-17 are non-BFRB)\nGESTURE_MAPPER = {\n    \"Above ear - pull hair\": 0,\n    \"Cheek - pinch skin\": 1,\n    \"Eyebrow - pull hair\": 2,\n    \"Eyelash - pull hair\": 3, \n    \"Forehead - pull hairline\": 4,\n    \"Forehead - scratch\": 5,\n    \"Neck - pinch skin\": 6, \n    \"Neck - scratch\": 7,\n    \n    \"Drink from bottle/cup\": 8,\n    \"Feel around in tray and pull out an object\": 9,\n    \"Glasses on/off\": 10,\n    \"Pinch knee/leg skin\": 11, \n    \"Pull air toward your face\": 12,\n    \"Scratch knee/leg skin\": 13,\n    \"Text on phone\": 14,\n    \"Wave hello\": 15,\n    \"Write name in air\": 16,\n    \"Write name on leg\": 17,\n}\n\nREVERSE_GESTURE_MAPPER = {v: k for k, v in GESTURE_MAPPER.items()}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.236223Z","iopub.execute_input":"2025-08-25T06:52:58.236844Z","iopub.status.idle":"2025-08-25T06:52:58.253732Z","shell.execute_reply.started":"2025-08-25T06:52:58.236781Z","shell.execute_reply":"2025-08-25T06:52:58.253013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def check_gpu_availability():\n    \"\"\"Check if GPU is available for LightGBM\"\"\"\n    try:\n        # Try to create a simple LightGBM model with GPU\n        from lightgbm import LGBMClassifier\n        import numpy as np\n        \n        # Create dummy data\n        X_dummy = np.random.rand(100, 10)\n        y_dummy = np.random.randint(0, 2, 100)\n        \n        # Try GPU\n        model = LGBMClassifier(n_estimators=1, device='gpu', verbosity=-1)\n        model.fit(X_dummy, y_dummy)\n        print(\"✓ GPU available for LightGBM\")\n        return 'gpu'\n    except:\n        print(\"⚠️  GPU not available, using CPU for LightGBM\")\n        return 'cpu'\n\n# Check GPU availability\nDEVICE = check_gpu_availability()\n\nprint(f\"✓ Configuration loaded for Kaggle environment (Device: {DEVICE})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.254673Z","iopub.execute_input":"2025-08-25T06:52:58.254918Z","iopub.status.idle":"2025-08-25T06:52:58.401277Z","shell.execute_reply.started":"2025-08-25T06:52:58.254896Z","shell.execute_reply":"2025-08-25T06:52:58.400497Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# COMPETITION METRIC","metadata":{}},{"cell_type":"code","source":"def competition_metric(y_true, y_pred) -> tuple:\n    \"\"\"Calculate the competition metric (Binary F1 + Macro F1) / 2\"\"\"\n    \n    # Binary F1: BFRB vs non-BFRB\n    binary_f1 = f1_score(\n        np.where(y_true <= 7, 1, 0),\n        np.where(y_pred <= 7, 1, 0),\n        zero_division=0.0,\n    )\n    \n    # Macro F1: specific gesture classification (only for BFRB gestures)\n    macro_f1 = f1_score(\n        np.where(y_true <= 7, y_true, 99),  # Map non-BFRB to 99\n        np.where(y_pred <= 7, y_pred, 99),  # Map non-BFRB to 99\n        average=\"macro\", \n        zero_division=0.0,\n    )\n    \n    # Final competition score\n    final_score = 0.5 * (binary_f1 + macro_f1)\n    \n    return final_score, binary_f1, macro_f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.402164Z","iopub.execute_input":"2025-08-25T06:52:58.402422Z","iopub.status.idle":"2025-08-25T06:52:58.407608Z","shell.execute_reply.started":"2025-08-25T06:52:58.402404Z","shell.execute_reply":"2025-08-25T06:52:58.406887Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# CORE FEATURE ENGINEERING: WORLD ACCELERATION","metadata":{}},{"cell_type":"code","source":"def handle_quaternion_missing_values(rot_data: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Handle missing values in quaternion data intelligently\n    \n    Key insight: Quaternions must have unit length |q| = 1\n    If one component is missing, we can reconstruct it from the others\n    \"\"\"\n    rot_cleaned = rot_data.copy()\n    \n    for i in range(len(rot_data)):\n        row = rot_data[i]\n        missing_count = np.isnan(row).sum()\n        \n        if missing_count == 0:\n            # No missing values, normalize to unit quaternion\n            norm = np.linalg.norm(row)\n            if norm > 1e-8:\n                rot_cleaned[i] = row / norm\n            else:\n                rot_cleaned[i] = [1.0, 0.0, 0.0, 0.0]  # Identity quaternion\n                \n        elif missing_count == 1:\n            # One missing value, reconstruct using unit quaternion constraint\n            # |w|² + |x|² + |y|² + |z|² = 1\n            missing_idx = np.where(np.isnan(row))[0][0]\n            valid_values = row[~np.isnan(row)]\n            \n            sum_squares = np.sum(valid_values**2)\n            if sum_squares <= 1.0:\n                missing_value = np.sqrt(max(0, 1.0 - sum_squares))\n                # Choose sign for continuity with previous quaternion\n                if i > 0 and not np.isnan(rot_cleaned[i-1, missing_idx]):\n                    if rot_cleaned[i-1, missing_idx] < 0:\n                        missing_value = -missing_value\n                rot_cleaned[i, missing_idx] = missing_value\n                rot_cleaned[i, ~np.isnan(row)] = valid_values\n            else:\n                rot_cleaned[i] = [1.0, 0.0, 0.0, 0.0]\n        else:\n            # More than one missing value, use identity quaternion\n            rot_cleaned[i] = [1.0, 0.0, 0.0, 0.0]\n    \n    return rot_cleaned","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.408323Z","iopub.execute_input":"2025-08-25T06:52:58.408658Z","iopub.status.idle":"2025-08-25T06:52:58.425958Z","shell.execute_reply.started":"2025-08-25T06:52:58.408641Z","shell.execute_reply":"2025-08-25T06:52:58.425326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compute_world_acceleration(acc: np.ndarray, rot: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Convert acceleration from device coordinates to world coordinates\n    \n    This is the key innovation: normalizing for device orientation\n    \n    Args:\n        acc: acceleration in device coordinates, shape (time_steps, 3) [x, y, z]\n        rot: rotation quaternion, shape (time_steps, 4) [w, x, y, z] (normalized)\n    \n    Returns:\n        acc_world: acceleration in world coordinates, shape (time_steps, 3)\n        \n    Why this matters:\n    - Device acceleration depends on how the watch is oriented on the wri/st\n    - World acceleration is independent of device orientation\n    - This helps the model focus on actual hand motion rather than wrist rotation\n    \"\"\"\n    try:\n        # Convert quaternion format from [w, x, y, z] to [x, y, z, w] for scipy\n        rot_scipy = rot[:, [1, 2, 3, 0]]\n        \n        # Verify quaternions are valid (non-zero norm)\n        norms = np.linalg.norm(rot_scipy, axis=1)\n        if np.any(norms < 1e-8):\n            # Replace problematic quaternions with identity\n            mask = norms < 1e-8\n            rot_scipy[mask] = [0.0, 0.0, 0.0, 1.0]  # Identity quaternion in scipy format\n        \n        # Create rotation object and apply transformation\n        r = R.from_quat(rot_scipy)\n        acc_world = r.apply(acc)\n        \n    except Exception:\n        # Fallback to original acceleration if transformation fails\n        print(\"Warning: World coordinate transformation failed, using device coordinates\")\n        acc_world = acc.copy()\n    \n    return acc_world","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.426775Z","iopub.execute_input":"2025-08-25T06:52:58.427383Z","iopub.status.idle":"2025-08-25T06:52:58.447228Z","shell.execute_reply.started":"2025-08-25T06:52:58.427359Z","shell.execute_reply":"2025-08-25T06:52:58.446659Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# SIMPLIFIED FEATURE EXTRACTION","metadata":{}},{"cell_type":"code","source":"def extract_comprehensive_features(sequence: pl.DataFrame, demographics: pl.DataFrame) -> pd.DataFrame:\n    \"\"\"\n    Extract features from IMU data with world acceleration transformation\n    \n    Feature Groups:\n    1. Device Acceleration (acc_x, acc_y, acc_z) - raw sensor data\n    2. Rotation Quaternion (rot_w, rot_x, rot_y, rot_z) - device orientation  \n    3. World Acceleration (NEW) - orientation-normalized acceleration\n    4. Demographics - subject characteristics\n    5. Sequence metadata - length, etc.\n    \"\"\"\n    \n    # to pandas\n    seq_df = sequence.to_pandas()\n    demo_df = demographics.to_pandas()\n    \n    # Handle missing values in basic sensor data\n    acc_data = seq_df[Config.ACC_COLS].copy()\n    acc_data = acc_data.ffill().bfill().fillna(0)\n    \n    rot_data = seq_df[Config.ROT_COLS].copy()\n    rot_data = rot_data.ffill().bfill()\n    \n    # Handle quaternion missing values and normalize\n    rot_data_clean = handle_quaternion_missing_values(rot_data.values)\n    \n    # CORE INNOVATION: Compute world acceleration\n    try:\n        world_acc_data = compute_world_acceleration(acc_data.values, rot_data_clean)\n        # print(\"✓ World acceleration computed successfully\")  # Reduced verbosity\n    except Exception as e:\n        print(f\"Warning: World acceleration computation failed: {e}\")\n        world_acc_data = acc_data.values.copy()  # Fallback to device coordinates\n    \n    # Initialize feature dictionary\n    features = {}\n    \n    # Add sequence metadata\n    features['sequence_length'] = len(seq_df)\n    \n    # Add demographics features\n    if len(demo_df) > 0:\n        demo_row = demo_df.iloc[0]\n        features['age'] = demo_row.get('age', 0)\n        features['adult_child'] = demo_row.get('adult_child', 0)\n        features['sex'] = demo_row.get('sex', 0)\n        features['handedness'] = demo_row.get('handedness', 0)\n        features['height_cm'] = demo_row.get('height_cm', 0)\n        features['shoulder_to_wrist_cm'] = demo_row.get('shoulder_to_wrist_cm', 0)\n        features['elbow_to_wrist_cm'] = demo_row.get('elbow_to_wrist_cm', 0)\n    \n    # Define feature arrays for statistical extraction\n    feature_arrays = {\n        'acc': acc_data.values,           # Device acceleration (3D)\n        'rot': rot_data_clean,            # Rotation quaternion (4D) \n        'world_acc': world_acc_data,      # World acceleration (3D) - KEY INNOVATION\n    }\n    \n    # Extract statistical features for each data source\n    for source_name, array in feature_arrays.items():\n        if array.ndim == 1:\n            array = array.reshape(-1, 1)\n        \n        n_features = array.shape[1]\n        \n        for feat_idx in range(n_features):\n            feat_data = array[:, feat_idx]\n            \n            # Create feature name\n            if source_name == 'acc':\n                axis_names = ['x', 'y', 'z']\n                prefix = f\"acc_{axis_names[feat_idx]}\"\n            elif source_name == 'rot':\n                comp_names = ['w', 'x', 'y', 'z']\n                prefix = f\"rot_{comp_names[feat_idx]}\"\n            elif source_name == 'world_acc':\n                axis_names = ['x', 'y', 'z']  \n                prefix = f\"world_acc_{axis_names[feat_idx]}\"\n            else:\n                prefix = f\"{source_name}_{feat_idx}\" if n_features > 1 else source_name\n            \n            # Extract comprehensive statistical features\n            features.update(extract_statistical_features(feat_data, prefix))\n    \n    # Compute magnitude features (important for motion intensity)\n    acc_magnitude = np.linalg.norm(acc_data.values, axis=1)\n    world_acc_magnitude = np.linalg.norm(world_acc_data, axis=1)\n    \n    features.update(extract_statistical_features(acc_magnitude, 'acc_magnitude'))\n    features.update(extract_statistical_features(world_acc_magnitude, 'world_acc_magnitude'))\n    \n    # Cross-feature: difference between device and world acceleration magnitudes\n    # This captures how much device orientation affects motion measurement\n    acc_world_diff = acc_magnitude - world_acc_magnitude\n    features.update(extract_statistical_features(acc_world_diff, 'acc_world_diff'))\n\n# 쿼터니언에서 회전각 추출\n    rot_angle = 2 * np.arccos(np.clip(rot_data_clean[:, 0], -1, 1))\n    features.update(extract_statistical_features(rot_angle, 'rot_angle'))\n    \n    # 수평면 가속도 크기\n    acc_xy = np.sqrt(acc_data.values[:, 0]**2 + acc_data.values[:, 1]**2)\n    features.update(extract_statistical_features(acc_xy, 'acc_xy'))\n    \n    # 오일러 각 변환 (roll, pitch, yaw)\n    rot_w, rot_x, rot_y, rot_z = (\n        rot_data_clean[:, 0],\n        rot_data_clean[:, 1],\n        rot_data_clean[:, 2],\n        rot_data_clean[:, 3],\n    )\n    \n    roll = np.arctan2(\n        2 * (rot_w * rot_x + rot_y * rot_z),\n        1 - 2 * (rot_x**2 + rot_y**2)\n    )\n    features.update(extract_statistical_features(roll, 'roll'))\n    \n    pitch = np.arcsin(\n        np.clip(2 * (rot_w * rot_y - rot_z * rot_x), -1, 1)\n    )\n    features.update(extract_statistical_features(pitch, 'pitch'))\n    \n    yaw = np.arctan2(\n        2 * (rot_w * rot_z + rot_x * rot_y),\n        1 - 2 * (rot_y**2 + rot_z**2)\n    )\n    features.update(extract_statistical_features(yaw, 'yaw'))\n    \n    # Convert to DataFrame\n    result_df = pd.DataFrame([features])\n    \n    # Handle any remaining NaN values\n    result_df = result_df.fillna(0)\n    \n    return result_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.447899Z","iopub.execute_input":"2025-08-25T06:52:58.448068Z","iopub.status.idle":"2025-08-25T06:52:58.466146Z","shell.execute_reply.started":"2025-08-25T06:52:58.448051Z","shell.execute_reply":"2025-08-25T06:52:58.465447Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_statistical_features(data: np.ndarray, prefix: str) -> dict:\n    \"\"\"\n    Extract comprehensive statistical features from a 1D time series\n    \n    Returns features that capture:\n    - Central tendency: mean, median, mode region\n    - Spread: std, variance, range, IQR  \n    - Shape: skewness, kurtosis\n    - Dynamics: differences, trends, changes\n    - Segments: beginning vs middle vs end behavior\n    \"\"\"\n    \n    features = {}\n    \n    # Basic statistics\n    features[f'{prefix}_mean'] = np.mean(data)\n    features[f'{prefix}_std'] = np.std(data)\n    features[f'{prefix}_var'] = np.var(data)\n    features[f'{prefix}_min'] = np.min(data)\n    features[f'{prefix}_max'] = np.max(data)\n    features[f'{prefix}_median'] = np.median(data)\n    features[f'{prefix}_q25'] = np.percentile(data, 25)\n    features[f'{prefix}_q75'] = np.percentile(data, 75)\n    features[f'{prefix}_iqr'] = np.percentile(data, 75) - np.percentile(data, 25)\n    \n    # Range and boundary features\n    features[f'{prefix}_range'] = np.max(data) - np.min(data)\n    features[f'{prefix}_first'] = data[0] if len(data) > 0 else 0\n    features[f'{prefix}_last'] = data[-1] if len(data) > 0 else 0\n    features[f'{prefix}_delta'] = data[-1] - data[0] if len(data) > 0 else 0\n    \n    # Higher order moments (shape of distribution)\n    if len(data) > 1 and np.std(data) > 1e-8:\n        features[f'{prefix}_skew'] = pd.Series(data).skew()\n        features[f'{prefix}_kurt'] = pd.Series(data).kurtosis()\n    else:\n        features[f'{prefix}_skew'] = 0\n        features[f'{prefix}_kurt'] = 0\n    \n    # Differential features (capture dynamics)\n    if len(data) > 1:\n        diff_data = np.diff(data)\n        features[f'{prefix}_diff_mean'] = np.mean(diff_data)\n        features[f'{prefix}_diff_std'] = np.std(diff_data)\n        features[f'{prefix}_n_changes'] = np.sum(np.abs(diff_data) > np.std(data) * 0.1)  # Significant changes\n    else:\n        features[f'{prefix}_diff_mean'] = 0\n        features[f'{prefix}_diff_std'] = 0\n        features[f'{prefix}_n_changes'] = 0\n    \n    # Correlation with time (trend detection)\n    if len(data) > 2:\n        time_indices = np.arange(len(data))\n        try:\n            corr_coef = np.corrcoef(time_indices, data)[0, 1]\n            features[f'{prefix}_time_corr'] = corr_coef if not np.isnan(corr_coef) else 0\n        except:\n            features[f'{prefix}_time_corr'] = 0\n    else:\n        features[f'{prefix}_time_corr'] = 0\n    \n    # Segment features (beginning, middle, end patterns)\n    seq_len = len(data)\n    if seq_len >= 9:  # Need sufficient data for meaningful segments\n        seg_size = seq_len // 3\n        seg1 = data[:seg_size]           # Beginning (Transition phase)\n        seg2 = data[seg_size:2*seg_size] # Middle (Pause phase)  \n        seg3 = data[2*seg_size:]         # End (Gesture phase)\n        \n        features[f'{prefix}_seg1_mean'] = np.mean(seg1)\n        features[f'{prefix}_seg2_mean'] = np.mean(seg2)\n        features[f'{prefix}_seg3_mean'] = np.mean(seg3)\n        \n        features[f'{prefix}_seg1_std'] = np.std(seg1)\n        features[f'{prefix}_seg2_std'] = np.std(seg2)\n        features[f'{prefix}_seg3_std'] = np.std(seg3)\n        \n        # Segment transitions (important for distinguishing gesture types)\n        features[f'{prefix}_seg1_to_seg2'] = np.mean(seg2) - np.mean(seg1)\n        features[f'{prefix}_seg2_to_seg3'] = np.mean(seg3) - np.mean(seg2)\n    else:\n        # Not enough data for meaningful segments\n        for seg in [1, 2, 3]:\n            features[f'{prefix}_seg{seg}_mean'] = features[f'{prefix}_mean']\n            features[f'{prefix}_seg{seg}_std'] = features[f'{prefix}_std']\n        features[f'{prefix}_seg1_to_seg2'] = 0\n        features[f'{prefix}_seg2_to_seg3'] = 0\n    \n    return features","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.468096Z","iopub.execute_input":"2025-08-25T06:52:58.468696Z","iopub.status.idle":"2025-08-25T06:52:58.486094Z","shell.execute_reply.started":"2025-08-25T06:52:58.468679Z","shell.execute_reply":"2025-08-25T06:52:58.485427Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# DATA LOADING","metadata":{}},{"cell_type":"markdown","source":"## IMU ONLY","metadata":{}},{"cell_type":"code","source":"def sequence_window(df: pl.DataFrame, needed_cols: list[str]) -> pl.DataFrame:\n    \"\"\"\n    주어진 컬럼만 유지하면서, 각 sequence_id 별 row 개수를 100으로 맞춤.\n    - row 수 >= 100 → 뒤에서 200개만 유지\n    - row 수 < 100 → 앞을 padding\n        * 수치형 컬럼 → 0\n        * 문자열 컬럼 → 첫 번째 값\n    \"\"\"\n\n    # 필요한 컬럼만 추출 (단, sequence_counter는 나중에 다시 만들 거니까 제외)\n    needed_cols_no_counter = [c for c in needed_cols if ((c != \"sequence_counter\") & (c != 'row_id'))]\n    df = df.select(needed_cols_no_counter)\n\n    out = []\n    for seq_id, subdf in df.group_by(\"sequence_id\"):\n        n = subdf.height\n\n        if n >= Config.SEQ_WINDOW:\n            trimmed = subdf.tail(Config.SEQ_WINDOW)\n        else:\n            pad_len = Config.SEQ_WINDOW - n\n            pad_dict = {}\n\n            for col in subdf.columns:\n                dtype = subdf[col].dtype\n                first_val = subdf[col][0]\n\n                if dtype.is_numeric():\n                    pad_dict[col] = [0] * pad_len\n                else:  # 문자열 컬럼\n                    pad_dict[col] = [first_val] * pad_len\n\n            pad = pl.DataFrame(pad_dict, schema=subdf.schema)\n            trimmed = pl.concat([pad, subdf], how=\"vertical_relaxed\")\n\n        # 시퀀스 내 row 번호 (0 ~ 199) 다시 생성\n        trimmed = trimmed.with_columns(\n            pl.arange(0, Config.SEQ_WINDOW).alias(\"sequence_counter\")\n        )\n\n        # 최종 필요한 컬럼만 유지\n        trimmed = trimmed.select(needed_cols_no_counter + [\"sequence_counter\"])\n        out.append(trimmed)\n\n    final_df = pl.concat(out, how=\"vertical_relaxed\")\n    return final_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:52:58.486700Z","iopub.execute_input":"2025-08-25T06:52:58.486937Z","iopub.status.idle":"2025-08-25T06:52:58.505722Z","shell.execute_reply.started":"2025-08-25T06:52:58.486917Z","shell.execute_reply":"2025-08-25T06:52:58.505193Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_and_prepare_data():\n    print(\"Loading training data...\")\n    train_df = pl.read_csv(Config.TRAIN_PATH)\n    train_demographics = pl.read_csv(Config.TRAIN_DEMOGRAPHICS_PATH)\n    \n    print(\"Loading test data...\")\n    test_df = pl.read_csv(Config.TEST_PATH)\n    test_demographics = pl.read_csv(Config.TEST_DEMOGRAPHICS_PATH)\n\n    # remove row_id\n    train_df = train_df.drop(\"row_id\")\n    test_df = test_df.drop(\"row_id\")\n    \n    # Get common columns between train and test (exclude thermal and ToF sensors)\n    train_cols = set(train_df.columns)\n    test_cols = set(test_df.columns)\n    common_cols = train_cols.intersection(test_cols)\n    \n    # Filter to IMU-only columns (remove thermal and ToF sensors)\n    imu_cols = [col for col in common_cols if not (col.startswith('thm_') or col.startswith('tof_'))]\n\n     # 각 row를 window 크기로 자르기 (변경 부분)\n    imu_train_df = sequence_window(train_df, imu_cols + ['gesture'])\n    \n    print(f\"✓ Using {len(imu_cols)} common IMU columns\")\n    print(f\"✓ Train-only columns: {train_cols - test_cols}\")\n    print(f\"✓ Test-only columns: {test_cols - train_cols}\")\n\n    print(\"Extracting features for training sequences...\")\n    train_features_list = []\n    train_labels = []\n    train_subjects = []\n    train_sequence_ids = []\n    \n    # Group by sequence_id for training data - need to include gesture column for labels\n    train_imu_cols = imu_cols + ['gesture'] if 'gesture' not in imu_cols else imu_cols\n    train_sequences = imu_train_df.select(pl.col(train_imu_cols)).group_by('sequence_id', maintain_order=True)\n    \n    for sequence_id, sequence_data in train_sequences:\n        # Get sequence features\n        sequence_id_val = sequence_id[0] if isinstance(sequence_id, tuple) else sequence_id\n        \n        # Get demographics for this sequence\n        subject_id = sequence_data['subject'][0]\n        subject_demographics = train_demographics.filter(pl.col('subject') == subject_id)\n        \n        # Extract features (only IMU columns for feature extraction)\n        imu_only_data = sequence_data.select(pl.col(imu_cols))\n        \n        # features : dataframe\n        features = extract_comprehensive_features(imu_only_data, subject_demographics)\n        features['sequence_id'] = sequence_id_val\n        \n        train_features_list.append(features)\n        \n        # Get label (gesture) for this sequence\n        gesture = sequence_data['gesture'][0]\n        label = GESTURE_MAPPER[gesture]\n        train_labels.append(label)\n        train_subjects.append(subject_id)\n        train_sequence_ids.append(sequence_id_val)\n    \n    # Combine all training features\n    X_train = pd.concat(train_features_list, ignore_index=True)\n    \n    return X_train, imu_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T06:57:37.407672Z","iopub.execute_input":"2025-08-25T06:57:37.408194Z","iopub.status.idle":"2025-08-25T06:57:37.416055Z","shell.execute_reply.started":"2025-08-25T06:57:37.408170Z","shell.execute_reply":"2025-08-25T06:57:37.415443Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# INFERENCE FUNCTION","metadata":{}},{"cell_type":"code","source":"def create_prediction_function(feature_cols, imu_cols):\n\n    def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n        \"\"\"\n        Prediction function for Kaggle evaluation\n        Uses AutoGluon predictor (internally an ensemble of models)\n        \"\"\"\n        try:\n            # Filter sequence to only include IMU columns that we trained with\n            available_cols = sequence.columns\n            sequence_imu_cols = [col for col in imu_cols if col in available_cols]\n            sequence_filtered = sequence.select(pl.col(sequence_imu_cols))\n\n            #sequence_filtered_df = sequence_window(sequence_filtered, imu_cols)\n            \n            # Extract features using the same method as training\n            features = extract_comprehensive_features(sequence_filtered, demographics)\n            \n            # Ensure we have the same features as training\n            missing_features = [col for col in feature_cols if col not in features.columns]\n            if missing_features:\n                print(f\"Warning: Missing features {missing_features}, filling with zeros\")\n                for col in missing_features:\n                    features[col] = 0\n            \n            X_pred = features[feature_cols]\n    \n            predictor = TabularPredictor.load(\"/kaggle/input/ag-0902/ag-20250902_082410\")\n            \n            # AutoGluon predictor 직접 사용\n            pred_class = predictor.predict(X_pred)[0]\n            \n            # predictor.predict()는 class label 그대로 반환할 수 있음\n            # 만약 숫자로 반환되면 REVERSE_GESTURE_MAPPER 적용\n            if isinstance(pred_class, (int, np.integer)):\n                gesture_name = REVERSE_GESTURE_MAPPER[pred_class]\n            else:\n                gesture_name = pred_class\n            \n            print(f\"Predicted: {gesture_name}\")\n            return gesture_name\n            \n        except Exception as e:\n            print(f\"Prediction error: {e}\")\n            import traceback\n            traceback.print_exc()\n            return 'Text on phone'  # Fallback prediction\n    return predict","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T07:08:03.540499Z","iopub.execute_input":"2025-08-25T07:08:03.540846Z","iopub.status.idle":"2025-08-25T07:08:03.548176Z","shell.execute_reply.started":"2025-08-25T07:08:03.540824Z","shell.execute_reply":"2025-08-25T07:08:03.547416Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# MAIN","metadata":{}},{"cell_type":"code","source":"def main():\n    \"\"\"Main execution pipeline\"\"\"\n    \n    # Load and prepare data\n    X_train, imu_cols = load_and_prepare_data()\n    \n    # Prepare feature columns for inference\n    feature_cols = [col for col in X_train.columns if col != 'sequence_id']\n    \n    # Create prediction function\n    predict_func = create_prediction_function(feature_cols, imu_cols)\n    \n    print(f\"\\n✓ Training completed successfully!\")\n    #print(f\"✓ Final CV Score: {overall_score:.4f}\")\n    print(f\"✓ Core innovation: World acceleration transformation\")\n    print(f\"✓ Models ready for inference\")\n    \n    return predict_func\n    \nif __name__ == \"__main__\":\n    predict_function = main()\n    \n    # Setup inference server\n    print(\"\\nSetting up inference server...\")\n    inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict_function)\n    \n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        inference_server.serve()\n    else:\n        # Local testing\n        inference_server.run_local_gateway(\n            data_paths=(\n                Config.TEST_PATH,\n                Config.TEST_DEMOGRAPHICS_PATH,\n            )\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-25T07:08:07.748344Z","iopub.execute_input":"2025-08-25T07:08:07.748655Z","iopub.status.idle":"2025-08-25T07:11:37.392916Z","shell.execute_reply.started":"2025-08-25T07:08:07.748631Z","shell.execute_reply":"2025-08-25T07:11:37.392230Z"}},"outputs":[],"execution_count":null}]}