{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# CMI BFRB Detection - LightGBM with World Acceleration Feature\n# A simple LightGBM implementation for BFRB classification with world coordinate transformation\n# \n# Key Innovation: Converting device acceleration to world coordinates using quaternion rotations\n# This helps normalize hand orientation differences across subjects and positions\n# Thanks https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data/discussion/583080 @tatamikenn for your idea!\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport joblib\nfrom typing import Tuple, List, Optional\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ML utilities\nfrom sklearn.model_selection import StratifiedGroupKFold, PredefinedSplit\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.metrics import f1_score, accuracy_score\nfrom lightgbm import LGBMClassifier, log_evaluation, early_stopping\n\n# World coordinate transformation\nfrom scipy.spatial.transform import Rotation as R\n\n# Competition specific\nimport kaggle_evaluation.cmi_inference_server\n\nprint(\"✓ All imports loaded successfully\")\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\n\nclass Config: \n    \"\"\"Central configuration class for training and data parameters\"\"\"\n    \n    # Paths for Kaggle environment\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    \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    # LightGBM parameters\n    LGBM_PARAMS = {\n        'objective': 'multiclass',\n        'n_estimators': 1024,\n        'max_depth': 8,\n        'learning_rate': 0.025,\n        'colsample_bytree': 0.5,\n        'n_jobs': -1,\n        'num_leaves': 20,\n        'random_state': 42,\n        'reg_alpha': 0.1,\n        'reg_lambda': 0.1,\n        'subsample': 0.5,\n        'verbosity': -1,\n        # 'device': 'gpu',  # Will be set automatically based on availability\n    }\n\n# Set reproducibility\nnp.random.seed(Config.SEED)\n\ndef 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})\")\n\n# =============================================================================\n# GESTURE MAPPING\n# =============================================================================\n\n# 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()}\n\n# =============================================================================\n# COMPETITION METRIC\n# =============================================================================\n\ndef 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\n\n# =============================================================================\n# CORE FEATURE ENGINEERING: WORLD ACCELERATION\n# =============================================================================\n\ndef 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\n\ndef 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 wrist\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\n\n# =============================================================================\n# SIMPLIFIED FEATURE EXTRACTION\n# =============================================================================\n\ndef 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    # Convert to pandas for processing\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    # 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\n\ndef 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\n\n# =============================================================================\n# DATA LOADING AND PREPROCESSING\n# =============================================================================\n\ndef load_and_prepare_data():\n    \"\"\"Load and prepare training data with comprehensive features\"\"\"\n    \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    # 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    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 = 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        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    y_train = np.array(train_labels)\n    subjects = np.array(train_subjects)\n    \n    print(\"Extracting features for test sequences...\")\n    test_features_list = []\n    test_sequence_ids = []\n    \n    # Group by sequence_id for test data  \n    test_sequences = test_df.select(pl.col(imu_cols)).group_by('sequence_id', maintain_order=True)\n    \n    for sequence_id, sequence_data in test_sequences:\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 = test_demographics.filter(pl.col('subject') == subject_id)\n        \n        # Extract features using the same function as training\n        features = extract_comprehensive_features(sequence_data, subject_demographics)\n        features['sequence_id'] = sequence_id_val\n        \n        test_features_list.append(features)\n        test_sequence_ids.append(sequence_id_val)\n    \n    # Combine all test features\n    X_test = pd.concat(test_features_list, ignore_index=True)\n    \n    print(f\"✓ Training features shape: {X_train.shape}\")\n    print(f\"✓ Training labels shape: {y_train.shape}\")\n    print(f\"✓ Test features shape: {X_test.shape}\")\n    print(f\"✓ Number of features: {X_train.shape[1] - 1}\")  # -1 for sequence_id\n    \n    return X_train, y_train, subjects, X_test, test_sequence_ids, imu_cols\n\n# =============================================================================\n# MODEL TRAINING WITH CROSS-VALIDATION\n# =============================================================================\n\ndef train_lightgbm_models(X_train, y_train, subjects):\n    \"\"\"Train LightGBM models using stratified group k-fold cross-validation\"\"\"\n    \n    print(f\"Training LightGBM models with {Config.N_FOLDS}-fold cross-validation...\")\n    \n    # Prepare features (remove sequence_id)\n    feature_cols = [col for col in X_train.columns if col != 'sequence_id']\n    X_features = X_train[feature_cols]\n    \n    # Setup cross-validation\n    cv = StratifiedGroupKFold(n_splits=Config.N_FOLDS, shuffle=True, random_state=Config.SEED)\n    \n    models = []\n    oof_predictions = np.zeros(len(y_train))\n    cv_scores = []\n    \n    print(f\"Feature columns: {len(feature_cols)}\")\n    print(\"Starting cross-validation...\")\n    \n    for fold, (train_idx, val_idx) in enumerate(cv.split(X_features, y_train, subjects)):\n        print(f\"\\n--- Fold {fold + 1}/{Config.N_FOLDS} ---\")\n        \n        # Split data\n        X_fold_train = X_features.iloc[train_idx]\n        X_fold_val = X_features.iloc[val_idx]\n        y_fold_train = y_train[train_idx]\n        y_fold_val = y_train[val_idx]\n        \n        print(f\"Train size: {len(X_fold_train)}, Val size: {len(X_fold_val)}\")\n        \n        # Train model with monitoring and device detection\n        lgbm_params = Config.LGBM_PARAMS.copy()\n        lgbm_params['device'] = DEVICE  # Use detected device\n        \n        model = LGBMClassifier(**lgbm_params)\n        \n        print(f\"Training fold {fold + 1} with monitoring every 5 rounds (Device: {DEVICE})...\")\n        model.fit(\n            X_fold_train, y_fold_train,\n            eval_set=[(X_fold_train, y_fold_train), (X_fold_val, y_fold_val)],\n            eval_names=['train', 'valid'],\n            eval_metric=['multi_logloss', 'multi_error'],\n            callbacks=[\n                log_evaluation(period=5),  # Output every 5 rounds\n                early_stopping(stopping_rounds=100, verbose=True)\n            ]\n        )\n        \n        # Predictions\n        val_preds = model.predict(X_fold_val)\n        oof_predictions[val_idx] = val_preds\n        \n        # Calculate metrics\n        score, binary_f1, macro_f1 = competition_metric(y_fold_val, val_preds)\n        cv_scores.append(score)\n        \n        print(f\"Fold {fold + 1} - Competition Score: {score:.4f} (Binary F1: {binary_f1:.4f}, Macro F1: {macro_f1:.4f})\")\n        \n        models.append(model)\n    \n    # Overall CV performance\n    overall_score, overall_binary_f1, overall_macro_f1 = competition_metric(y_train, oof_predictions)\n    \n    print(f\"\\n{'='*60}\")\n    print(\"CROSS-VALIDATION RESULTS\")\n    print(f\"{'='*60}\")\n    print(f\"Overall Competition Score: {overall_score:.4f} ± {np.std(cv_scores):.4f}\")\n    print(f\"Overall Binary F1: {overall_binary_f1:.4f}\")\n    print(f\"Overall Macro F1: {overall_macro_f1:.4f}\")\n    print(f\"Fold scores: {[f'{score:.4f}' for score in cv_scores]}\")\n    print(f\"{'='*60}\")\n    \n    return models, cv_scores, overall_score\n\n# =============================================================================\n# INFERENCE FUNCTION\n# =============================================================================\n\ndef create_prediction_function(models, feature_cols, imu_cols):\n    \"\"\"Create prediction function for Kaggle evaluation\"\"\"\n    \n    def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n        \"\"\"\n        Prediction function for Kaggle evaluation\n        Uses ensemble of trained LightGBM 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            # 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            # Get predictions from all models\n            predictions = []\n            for model in models:\n                pred_probs = model.predict_proba(X_pred)\n                pred_class = np.argmax(pred_probs, axis=1)[0]\n                predictions.append(pred_class)\n            \n            # Ensemble prediction (majority vote)\n            final_prediction = max(set(predictions), key=predictions.count)\n            \n            # Convert back to gesture name\n            gesture_name = REVERSE_GESTURE_MAPPER[final_prediction]\n            \n            print(f\"Predicted: {gesture_name} (class {final_prediction})\")\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    \n    return predict\n\n# =============================================================================\n# MAIN EXECUTION\n# =============================================================================\n\ndef main():\n    \"\"\"Main execution pipeline\"\"\"\n    \n    print(\"=\"*60)\n    print(\"CMI BFRB Detection - LightGBM with World Acceleration Feature\")\n    print(\"=\"*60)\n    print(\"🚀 Key Innovation: World Coordinate Transformation\")\n    print(\"   Converting device acceleration to world coordinates\")\n    print(\"   This normalizes for different wrist orientations!\")\n    print(\"=\"*60)\n    \n    # Load and prepare data\n    X_train, y_train, subjects, X_test, test_sequence_ids, imu_cols = load_and_prepare_data()\n    \n    # Train models\n    models, cv_scores, overall_score = train_lightgbm_models(X_train, y_train, subjects)\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(models, 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, models, cv_scores\n\n# Execute main pipeline\nif __name__ == \"__main__\":\n    predict_function, trained_models, cv_scores = 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":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-24T13:47:12.992498Z","iopub.execute_input":"2025-06-24T13:47:12.993193Z","iopub.status.idle":"2025-06-24T13:55:22.160115Z","shell.execute_reply.started":"2025-06-24T13:47:12.993167Z","shell.execute_reply":"2025-06-24T13:55:22.159527Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import inspect\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import f1_score, accuracy_score, log_loss\n\n# === STEP 1: Chạy hàm main() trong local scope và thu biến lại ===\ndef extract_main_variables():\n    # Extract body of `main()` into a wrapper function\n    main_source = inspect.getsource(main)\n    main_body = \"\\n\".join([\"    \" + line for line in main_source.splitlines()[1:]])\n    wrapped_code = f\"def __main_wrapper__():\\n{main_body}\"\n    \n    # Execute the wrapper and capture locals\n    exec_scope = {}\n    exec(wrapped_code, globals(), exec_scope)\n    exec_scope['__main_wrapper__']()\n    \n    # Filter all non-builtin variables\n    return {k: v for k, v in exec_scope.items() if not k.startswith(\"__\")}\n\n# Gọi và lấy biến\nmain_vars = extract_main_variables()\nX_train = main_vars['X_train']\ny_train = main_vars['y_train']\nsubjects = main_vars['subjects']\ntrained_models = main_vars['trained_models']\n\n# === STEP 2: Tính chỉ số bổ sung ===\nprint(\"\\n🔍 Additional Evaluation Metrics (per fold):\")\n\naccuracies = []\nf1_scores_macro = []\nf1_scores_weighted = []\nlog_losses = []\n\nfor fold, (train_idx, val_idx) in enumerate(StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42).split(X_train.drop(columns=['sequence_id']), y_train, subjects)):\n    X_fold_val = X_train.drop(columns=['sequence_id']).iloc[val_idx]\n    y_fold_val = y_train[val_idx]\n    \n    model = trained_models[fold]\n    val_preds = model.predict(X_fold_val)\n    val_probs = model.predict_proba(X_fold_val)\n    \n    acc = accuracy_score(y_fold_val, val_preds)\n    f1_macro = f1_score(y_fold_val, val_preds, average='macro')\n    f1_weighted = f1_score(y_fold_val, val_preds, average='weighted')\n    loss = log_loss(y_fold_val, val_probs, labels=np.arange(18))  # 18 classes\n    \n    accuracies.append(acc)\n    f1_scores_macro.append(f1_macro)\n    f1_scores_weighted.append(f1_weighted)\n    log_losses.append(loss)\n    \n    print(f\"\\nFold {fold+1}\")\n    print(f\"  Accuracy      : {acc:.4f}\")\n    print(f\"  F1 Macro      : {f1_macro:.4f}\")\n    print(f\"  F1 Weighted   : {f1_weighted:.4f}\")\n    print(f\"  Log Loss      : {loss:.4f}\")\n\n# Tổng kết\nprint(\"\\n📊 Overall Evaluation Metrics:\")\nprint(f\"  Avg Accuracy    : {np.mean(accuracies):.4f}\")\nprint(f\"  Avg F1 Macro    : {np.mean(f1_scores_macro):.4f}\")\nprint(f\"  Avg F1 Weighted : {np.mean(f1_scores_weighted):.4f}\")\nprint(f\"  Avg Log Loss    : {np.mean(log_losses):.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-24T13:58:48.099329Z","iopub.execute_input":"2025-06-24T13:58:48.100012Z","iopub.status.idle":"2025-06-24T14:08:44.012435Z","shell.execute_reply.started":"2025-06-24T13:58:48.099987Z","shell.execute_reply":"2025-06-24T14:08:44.011522Z"}},"outputs":[],"execution_count":null}]}