{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:00:57.527855Z","iopub.execute_input":"2025-08-24T07:00:57.528149Z","iopub.status.idle":"2025-08-24T07:00:57.915507Z","shell.execute_reply.started":"2025-08-24T07:00:57.528115Z","shell.execute_reply":"2025-08-24T07:00:57.914650Z"},"_kg_hide-input":true,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Libraries","metadata":{}},{"cell_type":"code","source":"%%time\nimport os\n\nimport pandas as pd\nimport polars as pl\nimport xgboost as xgb\nimport joblib\nimport numpy as np\nfrom sklearn.preprocessing import LabelEncoder\nfrom scipy import stats   \nimport warnings\nwarnings.filterwarnings('ignore')\nimport kaggle_evaluation.cmi_inference_server","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:00:57.916509Z","iopub.execute_input":"2025-08-24T07:00:57.917017Z","iopub.status.idle":"2025-08-24T07:00:58.709664Z","shell.execute_reply.started":"2025-08-24T07:00:57.916967Z","shell.execute_reply":"2025-08-24T07:00:58.708829Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define path","metadata":{}},{"cell_type":"code","source":"train_path = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\"\ndemo_path = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\"\ntest_path = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\"\ntest_demo_path = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\"\n\n# Global model state\nmodel_ges = None\nle_ges = None\nfeature_columns = None\n_model_trained = False\n\n# Paths (for local testing)\nTRAIN_PATH = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\"\nDEMO_PATH = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:00:58.711374Z","iopub.execute_input":"2025-08-24T07:00:58.711740Z","iopub.status.idle":"2025-08-24T07:00:58.716153Z","shell.execute_reply.started":"2025-08-24T07:00:58.711710Z","shell.execute_reply":"2025-08-24T07:00:58.715372Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time\n\n# Load training and test data\ntrain = pd.read_csv(train_path)\ntrain_demo = pd.read_csv(demo_path)\ntest = pd.read_csv(test_path)\ntest_demo = pd.read_csv(test_demo_path)\n\n# Confirm loaded successfully\nprint(train.shape, train_demo.shape)\nprint(test.shape, test_demo.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:00:58.716994Z","iopub.execute_input":"2025-08-24T07:00:58.717397Z","iopub.status.idle":"2025-08-24T07:01:20.651537Z","shell.execute_reply.started":"2025-08-24T07:00:58.717374Z","shell.execute_reply":"2025-08-24T07:01:20.650814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test.columns","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:20.652355Z","iopub.execute_input":"2025-08-24T07:01:20.652584Z","iopub.status.idle":"2025-08-24T07:01:20.658392Z","shell.execute_reply.started":"2025-08-24T07:01:20.652558Z","shell.execute_reply":"2025-08-24T07:01:20.657745Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:20.659407Z","iopub.execute_input":"2025-08-24T07:01:20.659659Z","iopub.status.idle":"2025-08-24T07:01:20.696522Z","shell.execute_reply.started":"2025-08-24T07:01:20.659637Z","shell.execute_reply":"2025-08-24T07:01:20.695811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_demo.columns","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:20.697198Z","iopub.execute_input":"2025-08-24T07:01:20.697456Z","iopub.status.idle":"2025-08-24T07:01:20.702191Z","shell.execute_reply.started":"2025-08-24T07:01:20.697439Z","shell.execute_reply":"2025-08-24T07:01:20.701503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_demo.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:20.704442Z","iopub.execute_input":"2025-08-24T07:01:20.705863Z","iopub.status.idle":"2025-08-24T07:01:20.722242Z","shell.execute_reply.started":"2025-08-24T07:01:20.705844Z","shell.execute_reply":"2025-08-24T07:01:20.721641Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize","metadata":{}},{"cell_type":"markdown","source":"1. Explore Target Distribution (Gestures)\nUnderstand class imbalance:","metadata":{}},{"cell_type":"code","source":"%%time\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\nplt.figure(figsize=(14,6))\nsns.countplot(data=train, x='gesture', order=train['gesture'].value_counts().index)\nplt.xticks(rotation=90)\nplt.title(\"Distribution of Gesture Labels\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:20.722919Z","iopub.execute_input":"2025-08-24T07:01:20.723144Z","iopub.status.idle":"2025-08-24T07:01:21.313036Z","shell.execute_reply.started":"2025-08-24T07:01:20.723129Z","shell.execute_reply":"2025-08-24T07:01:21.311979Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"2. Behavior & Gesture Distributions","metadata":{}},{"cell_type":"code","source":"%%time\n# Behavior\nplt.figure(figsize=(10, 4))\nsns.countplot(data=train, y='behavior', order=train['behavior'].value_counts().index)\nplt.title('Behavior Distribution')\nplt.show()\n\n# Gesture\nplt.figure(figsize=(10, 6))\nsns.countplot(data=train, y='gesture', order=train['gesture'].value_counts().index)\nplt.title('Gesture Distribution')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:21.314093Z","iopub.execute_input":"2025-08-24T07:01:21.314636Z","iopub.status.idle":"2025-08-24T07:01:22.227473Z","shell.execute_reply.started":"2025-08-24T07:01:21.314602Z","shell.execute_reply":"2025-08-24T07:01:22.226836Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Encode Target Labels\nMap gesture strings to integers for modeling:","metadata":{}},{"cell_type":"code","source":"%%time\nfrom sklearn.preprocessing import LabelEncoder\n\nlabel_encoder = LabelEncoder()\ntrain['gesture_label'] = label_encoder.fit_transform(train['gesture'])\ngesture_mapping = dict(zip(label_encoder.classes_, label_encoder.transform(label_encoder.classes_)))\nprint(\"Label mapping:\\n\", gesture_mapping)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:22.228567Z","iopub.execute_input":"2025-08-24T07:01:22.228897Z","iopub.status.idle":"2025-08-24T07:01:22.325204Z","shell.execute_reply.started":"2025-08-24T07:01:22.228873Z","shell.execute_reply":"2025-08-24T07:01:22.324605Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Merge Demographics\nUseful for adding features like age, handedness, etc.","metadata":{}},{"cell_type":"code","source":"%%time\ntrain = train.merge(train_demo, on='subject', how='left')\ntest = test.merge(test_demo, on='subject', how='left')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:22.326001Z","iopub.execute_input":"2025-08-24T07:01:22.326207Z","iopub.status.idle":"2025-08-24T07:01:22.966963Z","shell.execute_reply.started":"2025-08-24T07:01:22.326189Z","shell.execute_reply":"2025-08-24T07:01:22.966204Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature Selection (Drop Unnecessary Columns)","metadata":{}},{"cell_type":"code","source":"print(train.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:22.967857Z","iopub.execute_input":"2025-08-24T07:01:22.968133Z","iopub.status.idle":"2025-08-24T07:01:22.972327Z","shell.execute_reply.started":"2025-08-24T07:01:22.968106Z","shell.execute_reply":"2025-08-24T07:01:22.971570Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time\ndrop_cols = ['row_id', 'sequence_id', 'gesture', 'subject']\nexisting_drop_cols = [col for col in drop_cols if col in train.columns]\n\nX = train.drop(columns=existing_drop_cols + ['gesture_label'])\ny = train['gesture_label']\n\nexisting_test_drop_cols = [col for col in drop_cols if col in test.columns]\nX_test = test.drop(columns=existing_test_drop_cols)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:22.973149Z","iopub.execute_input":"2025-08-24T07:01:22.973333Z","iopub.status.idle":"2025-08-24T07:01:23.448290Z","shell.execute_reply.started":"2025-08-24T07:01:22.973319Z","shell.execute_reply":"2025-08-24T07:01:23.447506Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Step 5: Model Training","metadata":{}},{"cell_type":"markdown","source":"## Model Training (XGBoost - Multiclass)\n✅ Prerequisites (make sure you have these from earlier steps):\nX_train, X_valid, y_train, y_valid → split training data\n\nlabel_encoder → for decoding prediction later\n\nCategorical columns handled or dropped","metadata":{}},{"cell_type":"markdown","source":"Train XGBoost Classifier","metadata":{}},{"cell_type":"code","source":"%%time\nfrom sklearn.model_selection import train_test_split\n\n# Drop non-numeric and label columns\nfeatures_to_drop = ['row_id', 'sequence_id', 'gesture', 'subject']\nX = train.drop(columns=features_to_drop, errors='ignore')\ny = train['gesture']\n\n# Encode labels\nfrom sklearn.preprocessing import LabelEncoder\nle = LabelEncoder()\ny_encoded = le.fit_transform(y)\n\n# Split data\nX_train, X_valid, y_train, y_valid = train_test_split(\n    X.select_dtypes(include=['int', 'float', 'bool']),\n    y_encoded,\n    test_size=0.2,\n    random_state=42,\n    stratify=y_encoded\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:23.449255Z","iopub.execute_input":"2025-08-24T07:01:23.449567Z","iopub.status.idle":"2025-08-24T07:01:27.283154Z","shell.execute_reply.started":"2025-08-24T07:01:23.449537Z","shell.execute_reply":"2025-08-24T07:01:27.282443Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train with GPU","metadata":{}},{"cell_type":"code","source":"%%time\nfrom xgboost import XGBClassifier\n\n# Define the model with GPU support\nmodel = XGBClassifier(\n    objective='multi:softprob',\n    num_class=len(le.classes_),\n    eval_metric='mlogloss',\n    use_label_encoder=False,\n    tree_method='auto', # Specify the \n    device ='cuda',\n    n_estimators=300,\n    learning_rate=0.1,\n    max_depth=6,\n    random_state=42,\n    n_jobs=2,  # or 1 if you want to be gentler on CPU,\n    #enable_categorical=True\n)\n\n# Fit the model\nmodel.fit(\n    X_train, y_train,\n    eval_set=[(X_valid, y_valid)],\n    early_stopping_rounds=15,\n    verbose=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:01:27.283893Z","iopub.execute_input":"2025-08-24T07:01:27.284087Z","iopub.status.idle":"2025-08-24T07:04:50.774562Z","shell.execute_reply.started":"2025-08-24T07:01:27.284071Z","shell.execute_reply":"2025-08-24T07:04:50.773753Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Template to Follow for XGBoost Model","metadata":{}},{"cell_type":"code","source":"%%time\nimport pandas as pd\nimport numpy as np\n\n# Load or use already trained model\n# model = ... (your trained model)\n\n# Preprocessing steps you applied during training\ndef preprocess(sample_df):\n    # Example: drop unused columns, encode categorical variables, fill missing values\n    X = sample_df.copy()\n\n    # Drop columns not used during training\n    drop_cols = ['row_id', 'sequence_id']  # adjust based on your training\n    X = X.drop(columns=[col for col in drop_cols if col in X.columns], errors='ignore')\n\n    # Encode categoricals if needed (must match training)\n    # For example: X['subject'] = subject_encoder.transform(X['subject'])\n\n    # Fill missing values or use other preprocessing\n    X = X.fillna(0)\n\n    return X\n\n# Final prediction function\n\ndef predict(sample: pd.DataFrame, sample_metadata: pd.DataFrame) -> np.ndarray:\n    # Combine or use just sample if you don't need metadata\n    X = preprocess(sample)  # your preprocessing function\n    pred = model.predict(X)\n    return np.array(pred)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:04:50.775424Z","iopub.execute_input":"2025-08-24T07:04:50.775663Z","iopub.status.idle":"2025-08-24T07:04:50.782282Z","shell.execute_reply.started":"2025-08-24T07:04:50.775645Z","shell.execute_reply":"2025-08-24T07:04:50.781492Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Save and Evaluate the Model","metadata":{}},{"cell_type":"code","source":"%%time\nfrom sklearn.metrics import classification_report, confusion_matrix\n\ny_val_pred = model.predict(X_valid)\nprint(confusion_matrix(y_valid, y_val_pred))\nprint(classification_report(y_valid, y_val_pred))","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-08-24T07:04:50.783118Z","iopub.execute_input":"2025-08-24T07:04:50.783378Z","iopub.status.idle":"2025-08-24T07:04:52.684196Z","shell.execute_reply.started":"2025-08-24T07:04:50.783341Z","shell.execute_reply":"2025-08-24T07:04:52.683450Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import joblib\njoblib.dump(model, \"model.pkl\")   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:04:52.684904Z","iopub.execute_input":"2025-08-24T07:04:52.685080Z","iopub.status.idle":"2025-08-24T07:04:52.748978Z","shell.execute_reply.started":"2025-08-24T07:04:52.685066Z","shell.execute_reply":"2025-08-24T07:04:52.748057Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Feature Extraction Function","metadata":{}},{"cell_type":"code","source":"%%time\ndef extract_features_polars(sequence: pl.DataFrame) -> pl.DataFrame:\n    \"\"\"\n    Extract features from one sequence (filtered to gesture phase)\n    Returns a 1-row Polars DataFrame\n    \"\"\"\n    feats = {}\n\n    # Filter to gesture phase using behavior or phase\n    if 'behavior' in sequence.columns:\n        valid_rows = sequence.filter(pl.col('behavior') == 'Active')\n        if len(valid_rows) == 0:\n            valid_rows = sequence.filter(pl.col('behavior').str.to_lowercase() == 'active')\n    elif 'phase' in sequence.columns:\n        valid_rows = sequence.filter(pl.col('phase') == 2)\n    else:\n        valid_rows = sequence  # fallback\n\n    if len(valid_rows) == 0:\n        valid_rows = sequence\n\n    df = valid_rows.to_pandas()\n\n    # IMU: acc_x, acc_y, acc_z\n    for ax in 'xyz':\n        col = f'acc_{ax}'\n        if col in df:\n            x = df[col].dropna().values\n            if len(x) == 0: x = [0]\n            feats[f'{col}_mean'] = np.mean(x)\n            feats[f'{col}_std'] = np.std(x)\n            feats[f'{col}_max'] = np.max(x)\n            feats[f'{col}_min'] = np.min(x)\n            feats[f'{col}_kurt'] = stats.kurtosis(x) if len(x) > 2 else 0\n            feats[f'{col}_skew'] = stats.skew(x) if len(x) > 2 else 0\n            feats[f'{col}_amp'] = np.max(x) - np.min(x)\n            feats[f'{col}_jerk'] = np.std(np.diff(x)) if len(x) > 1 else 0\n\n    # Rotation\n    for ax in 'wxyz':\n        col = f'rot_{ax}'\n        if col in df:\n            x = df[col].dropna().values\n            if len(x) == 0: x = [0]\n            feats[f'{col}_mean'] = np.mean(x)\n            feats[f'{col}_std'] = np.std(x)\n\n    # Thermopiles\n    for i in range(1, 6):\n        col = f'thm_{i}'\n        if col in df:\n            x = df[col].dropna().values\n            feats[f'thm_{i}_mean'] = np.mean(x) if len(x) > 0 else 0\n            feats[f'thm_{i}_std'] = np.std(x) if len(x) > 0 else 0\n\n    # ToF\n    for i in range(1, 6):\n        tof_cols = [f'tof_{i}_v{j}' for j in range(64) if f'tof_{i}_v{j}' in df.columns]\n        if tof_cols:\n            vals = df[tof_cols].values.flatten()\n            valid = vals[(vals != -1) & (~np.isnan(vals))]\n            feats[f'tof_{i}_mean'] = np.mean(valid) if len(valid) > 0 else 0\n            feats[f'tof_{i}_std'] = np.std(valid) if len(valid) > 0 else 0\n\n    return pl.DataFrame([feats])  ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:04:52.750095Z","iopub.execute_input":"2025-08-24T07:04:52.750302Z","iopub.status.idle":"2025-08-24T07:04:52.758968Z","shell.execute_reply.started":"2025-08-24T07:04:52.750286Z","shell.execute_reply":"2025-08-24T07:04:52.758143Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Retrain Model on First Call","metadata":{}},{"cell_type":"code","source":"%%time\ndef train_model_on_first_call(train_path: str, demo_path: str):\n    global model_ges, le_ges, feature_columns, _model_trained\n\n    if _model_trained:\n        return\n\n    print(\"🔍 Starting model training on first call...\")\n\n    # Load data\n    try:\n        train = pl.read_csv(train_path)\n        demo = pl.read_csv(demo_path)\n    except Exception as e:\n        print(f\"❌ Failed to load data: {e}\")\n        return\n\n    print(f\"Total train rows: {len(train)}\")\n    if 'behavior' in train.columns:\n        print(\"Unique behavior values:\", train['behavior'].unique().to_list())\n    if 'phase' in train.columns:\n        print(\"Unique phase values:\", train['phase'].unique().to_list())\n\n    # Filter to gesture phase\n    if 'behavior' in train.columns:\n        train_gesture = train.filter(\n            (pl.col('behavior') == 'Active') | \n            (pl.col('behavior').str.to_lowercase() == 'active')\n        )\n    elif 'phase' in train.columns:\n        train_gesture = train.filter(pl.col('phase') == 2)\n    else:\n        print(\"❌ No behavior or phase column — using all data as fallback\")\n        train_gesture = train\n\n    print(f\"Rows after gesture filtering: {len(train_gesture)}\")\n\n    if len(train_gesture) == 0:\n        print(\"❌ No gesture-phase data found. Cannot train.\")\n        # Fallback: predict constant\n        le_ges = LabelEncoder()\n        le_ges.fit(['non_target'])\n        model_ges = lambda x: 0\n        feature_columns = ['acc_x_mean']\n        _model_trained = True\n        return\n\n    # Merge with demographics\n    train_gesture = train_gesture.join(demo, on='subject', how='left')\n\n    # Get unique sequence IDs\n    sequence_ids = train_gesture['sequence_id'].unique().to_list()\n    print(f\"Number of sequences to process: {len(sequence_ids)}\")\n\n    feature_list = []\n    labels = []\n\n    for seq_id in sequence_ids:\n        seq_data = train_gesture.filter(pl.col('sequence_id') == seq_id)\n        if len(seq_data) == 0:\n            continue\n\n        feats = extract_features_polars(seq_data)\n        if len(feats) == 0 or len(feats.columns) == 0:\n            continue\n        feature_list.append(feats)\n\n        # Extract label\n        gesture_series = seq_data['gesture'].drop_nulls()\n        if len(gesture_series) > 0:\n            labels.append(gesture_series[0])\n        else:\n            labels.append('non_target')\n\n    # Check if we have any features\n    if len(feature_list) == 0:\n        print(\"❌ No features extracted. Using fallback model.\")\n        le_ges = LabelEncoder()\n        le_ges.fit(['non_target'])\n        model_ges = lambda x: 0\n        feature_columns = ['dummy_feature']\n        _model_trained = True\n        return\n\n    # Combine all features\n    X = pl.concat(feature_list).fill_null(0).fill_nan(0)\n    y = pl.Series(\"gesture\", labels).drop_nulls()\n\n    if len(X) == 0 or len(y) == 0:\n        print(\"❌ No valid labels or features. Using fallback.\")\n        le_ges = LabelEncoder()\n        le_ges.fit(['non_target'])\n        model_ges = lambda x: 0\n        feature_columns = ['dummy']\n        _model_trained = True\n        return\n\n    # Align X and y\n    min_len = min(len(X), len(y))\n    X_pd = X[:min_len].to_pandas()\n    y_pd = y[:min_len].to_list()\n\n    # Encode\n    le_ges = LabelEncoder()\n    y_enc = le_ges.fit_transform(y_pd)\n\n    # Train XGBoost\n    print(f\"✅ Training on {len(X_pd)} sequences with {len(le_ges.classes_)} classes\")\n    model_ges = xgb.XGBClassifier(\n        n_estimators=100,\n        max_depth=6,\n        learning_rate=0.1,\n        subsample=0.8,\n        colsample_bytree=0.8,\n        tree_method='hist',\n        random_state=42,\n        eval_metric='mlogloss'\n    )\n    model_ges.fit(X_pd, y_enc)\n\n    feature_columns = X_pd.columns.tolist()\n    _model_trained = True\n    print(\"🎉 Model training complete!\")   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:04:52.759819Z","iopub.execute_input":"2025-08-24T07:04:52.760079Z","iopub.status.idle":"2025-08-24T07:04:52.785965Z","shell.execute_reply.started":"2025-08-24T07:04:52.760055Z","shell.execute_reply":"2025-08-24T07:04:52.785331Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Prediction","metadata":{}},{"cell_type":"code","source":"%%time\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    global model_ges, le_ges, feature_columns, _model_trained\n\n    # Train model on first call if not already trained\n    if not _model_trained:\n        print(\"🎯 First prediction: training model...\")\n        train_model_on_first_call(TRAIN_PATH, DEMO_PATH)\n\n    # Extract features from current sequence\n    try:\n        feats = extract_features_polars(sequence)\n    except Exception as e:\n        print(f\"❌ Feature extraction failed: {e}\")\n        feats = pl.DataFrame([{}])  # empty fallback\n\n    # Merge with demographics\n    try:\n        subject = sequence['subject'].unique()[0]\n        demo_row = demographics.filter(pl.col('subject') == subject)\n        if len(demo_row) == 0:\n            # Create empty demo row with zeros\n            demo_data = {\n                'subject': [subject],\n                'adult_child': [0],\n                'age': [30],\n                'sex': [0],\n                'handedness': [1],\n                'height_cm': [170],\n                'shoulder_to_wrist_cm': [60],\n                'elbow_to_wrist_cm': [35]\n            }\n            demo_row = pl.DataFrame(demo_data)\n        feats = feats.join(demo_row, on='subject', how='left')\n    except Exception as e:\n        print(f\"⚠️ Demographics merge failed: {e}\")\n        # Add subject columns as zero\n        for col in ['adult_child', 'age', 'sex', 'handedness', \n                   'height_cm', 'shoulder_to_wrist_cm', 'elbow_to_wrist_cm']:\n            if col not in feats.columns:\n                feats = feats.with_columns(pl.lit(0).cast(pl.Float64).alias(col))\n\n    # Fill missing values\n    feats = feats.fill_null(0).fill_nan(0)\n\n    # Ensure all expected features are present\n    for col in feature_columns:\n        if col not in feats.columns:\n            feats = feats.with_columns(pl.lit(0).cast(pl.Float64).alias(col))\n    \n    # Select and sort columns to match training\n    feats = feats.select(sorted(feature_columns))\n\n    # Convert to pandas for prediction\n    try:\n        X = feats.to_pandas()\n        pred_class = model_ges.predict(X)[0]\n        predicted_gesture = le_ges.inverse_transform([pred_class])[0]\n    except Exception as e:\n        print(f\"❌ Prediction failed: {e}\")\n        predicted_gesture = 'Text on phone'  # safe fallback\n\n    return predicted_gesture   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-24T07:04:52.786659Z","iopub.execute_input":"2025-08-24T07:04:52.786894Z","iopub.status.idle":"2025-08-24T07:04:52.809747Z","shell.execute_reply.started":"2025-08-24T07:04:52.786877Z","shell.execute_reply":"2025-08-24T07:04:52.809094Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time\nimport kaggle_evaluation.cmi_inference_server\n\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\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,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-08-24T07:04:52.812372Z","iopub.execute_input":"2025-08-24T07:04:52.812988Z","iopub.status.idle":"2025-08-24T07:04:58.266553Z","shell.execute_reply.started":"2025-08-24T07:04:52.812966Z","shell.execute_reply":"2025-08-24T07:04:58.265947Z"}},"outputs":[],"execution_count":null}]}