{"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":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport lightgbm as lgb\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import f1_score, make_scorer\nfrom sklearn.preprocessing import LabelEncoder\nimport joblib\n\n# --- 1. Load and Merge Data ---\n# Load training data and demographics\ntrain_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\ndemographics_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv')\n\n# Merge dataframes\ntrain_df = train_df.merge(demographics_df, on='subject')\n\n# --- 2. Preprocessing and Feature Engineering ---\nprint(\"Starting Feature Engineering...\")\n\n# Define feature engineering function\ndef create_features(df):\n    # Group by sequence and extract features\n    features = []\n    for seq_id, seq_data in df.groupby('sequence_id'):\n        row = {'sequence_id': seq_id}\n\n        # Demographics features\n        row.update(seq_data[['adult_child', 'age', 'sex', 'handedness', 'height_cm', 'shoulder_to_wrist_cm', 'elbow_to_wrist_cm']].iloc[0].to_dict())\n\n        # Target and other metadata\n        row['gesture'] = seq_data['gesture'].iloc[0]\n        row['is_full_data'] = 1 if not seq_data['thm_1'].isnull().all() else 0\n\n        # IMU Features\n        acc_cols = [f'acc_{axis}' for axis in 'xyz']\n        rot_cols = [f'rot_{axis}' for axis in 'wxyz']\n\n        for col in acc_cols + rot_cols:\n            row[f'{col}_mean'] = seq_data[col].mean()\n            row[f'{col}_std'] = seq_data[col].std()\n            row[f'{col}_min'] = seq_data[col].min()\n            row[f'{col}_max'] = seq_data[col].max()\n            row[f'{col}_skew'] = seq_data[col].skew()\n\n        # Thermopile (Thm) and Time-of-Flight (ToF) Features\n        thm_cols = [f'thm_{i}' for i in range(1, 6)]\n        tof_cols = [f'tof_{i}_v{j}' for i in range(1, 6) for j in range(64)]\n\n        # We will only create these features if it's a full-data sequence\n        if row['is_full_data'] == 1:\n            for col in thm_cols:\n                row[f'{col}_mean'] = seq_data[col].mean()\n                row[f'{col}_std'] = seq_data[col].std()\n                row[f'{col}_min'] = seq_data[col].min()\n                row[f'{col}_max'] = seq_data[col].max()\n            \n            # Inter-thermopile features\n            for i in range(1, 5):\n                row[f'thm_diff_{i}_{i+1}'] = seq_data[f'thm_{i}'].mean() - seq_data[f'thm_{i+1}'].mean()\n\n            # ToF features (treating each sensor as a unit)\n            for i in range(1, 6):\n                tof_sensor_cols = [f'tof_{i}_v{j}' for j in range(64)]\n                # Replace -1 with NaN for statistical calculations\n                tof_data = seq_data[tof_sensor_cols].replace(-1, np.nan)\n                row[f'tof_{i}_mean'] = tof_data.mean().mean()\n                row[f'tof_{i}_std'] = tof_data.std().mean()\n                row[f'tof_{i}_pixel_count'] = (~tof_data.isnull()).sum().mean()\n        \n        features.append(row)\n    \n    return pd.DataFrame(features)\n\n# Create the final feature dataframe\nfeatures_df = create_features(train_df)\nprint(\"Feature Engineering Complete.\")\n\n# --- 3. Model Training ---\n# Label encode the target variable 'gesture'\nle = LabelEncoder()\nfeatures_df['gesture_encoded'] = le.fit_transform(features_df['gesture'])\n\n# --- Custom Evaluation Metric ---\n# This is crucial for matching the competition's scoring.\ndef macro_f1_score(y_true, y_pred, labels):\n    y_pred_labels = np.argmax(y_pred, axis=1)\n    \n    # Binary F1\n    y_true_binary = (y_true != le.transform(['non_target'])[0]).astype(int)\n    y_pred_binary = (y_pred_labels != le.transform(['non_target'])[0]).astype(int)\n    binary_f1 = f1_score(y_true_binary, y_pred_binary)\n\n    # Macro F1\n    y_pred_macro = y_pred_labels.copy()\n    y_true_macro = y_true.copy()\n    non_target_idx = le.transform(['non_target'])[0]\n    \n    y_pred_macro[y_pred_macro == non_target_idx] = -1\n    y_true_macro[y_true_macro == non_target_idx] = -1\n    \n    macro_f1 = f1_score(y_true_macro[y_true_macro != -1], y_pred_macro[y_true_macro != -1], average='macro', zero_division=0)\n\n    return 'macro_f1', (binary_f1 + macro_f1) / 2, True\n\n# --- Train the Full-Data Model ---\nprint(\"\\nTraining Full-Data Model...\")\nfull_data_df = features_df[features_df['is_full_data'] == 1].drop(columns=['is_full_data', 'gesture'])\nX_full = full_data_df.drop(columns=['sequence_id', 'gesture_encoded'])\ny_full = full_data_df['gesture_encoded']\n\nlgb_full = lgb.LGBMClassifier(objective='multiclass', num_class=len(le.classes_), random_state=42, n_estimators=500, learning_rate=0.05)\nlgb_full.fit(X_full, y_full)\njoblib.dump(lgb_full, 'lgb_full_data_model.pkl')\njoblib.dump(le, 'label_encoder.pkl')\nprint(\"Full-Data Model Trained and Saved.\")\n\n# --- Train the IMU-Only Model ---\nprint(\"\\nTraining IMU-Only Model...\")\nimu_features = [col for col in X_full.columns if 'acc_' in col or 'rot_' in col or col in demographics_df.columns]\nX_imu = features_df[imu_features]\ny_imu = features_df['gesture_encoded']\n\nlgb_imu = lgb.LGBMClassifier(objective='multiclass', num_class=len(le.classes_), random_state=42, n_estimators=500, learning_rate=0.05)\nlgb_imu.fit(X_imu, y_imu)\njoblib.dump(lgb_imu, 'lgb_imu_only_model.pkl')\nprint(\"IMU-Only Model Trained and Saved.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-02T18:26:55.061553Z","iopub.execute_input":"2025-08-02T18:26:55.061911Z","iopub.status.idle":"2025-08-02T18:30:37.386479Z","shell.execute_reply.started":"2025-08-02T18:26:55.061879Z","shell.execute_reply":"2025-08-02T18:30:37.385380Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport polars as pl\nimport joblib\nimport numpy as np\n\n# --- 1. Global Variables and Model Loading ---\n# We will load the models and the label encoder into global variables.\n# This ensures that they are only loaded once when the notebook starts up,\n# and not on every single call to the `predict` function. This is critical for performance.\nlgb_full = None\nlgb_imu = None\nle = None\n\ndef load_models():\n    \"\"\"\n    Loads the pre-trained LightGBM models and the label encoder.\n    This function should be called once at the start of the notebook session.\n    \"\"\"\n    global lgb_full, lgb_imu, le\n    if lgb_full is None or lgb_imu is None or le is None:\n        try:\n            lgb_full = joblib.load('/kaggle/working/lgb_full_data_model.pkl')\n            lgb_imu = joblib.load('/kaggle/working/lgb_imu_only_model.pkl')\n            le = joblib.load('/kaggle/working/label_encoder.pkl')\n            demographics_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv') # Use train demographics for simplicity in this example\n            print(\"Models and Label Encoder loaded successfully.\")\n        except FileNotFoundError:\n            print(\"Error: Model files not found. Please ensure 'lgb_full_data_model.pkl', 'lgb_imu_only_model.pkl', and 'label_encoder.pkl' are in the working directory.\")\n            # In a real submission, this would likely cause a runtime error, which is desired.\n            raise\n\n# Call the model loading function once\nload_models()\n\n# --- 2. Feature Engineering Function ---\n# This function is designed to work on a single sequence at a time.\n# It replicates the feature engineering from the training notebook.\ndef create_inference_features(sequence: pd.DataFrame, demographics: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"\n    Creates the same features as the training script for a single sequence.\n    This function assumes the input is a Pandas DataFrame.\n    \"\"\"\n    features = {}\n\n    # Demographics features\n    features.update(demographics.iloc[0][['adult_child', 'age', 'sex', 'handedness', 'height_cm', 'shoulder_to_wrist_cm', 'elbow_to_wrist_cm']].to_dict())\n\n    # Check if it's a full-data sequence\n    is_full_data = 1 if not sequence['thm_1'].isnull().all() else 0\n    features['is_full_data'] = is_full_data\n\n    # IMU Features\n    acc_cols = [f'acc_{axis}' for axis in 'xyz']\n    rot_cols = [f'rot_{axis}' for axis in 'wxyz']\n\n    for col in acc_cols + rot_cols:\n        features[f'{col}_mean'] = sequence[col].mean()\n        features[f'{col}_std'] = sequence[col].std()\n        features[f'{col}_min'] = sequence[col].min()\n        features[f'{col}_max'] = sequence[col].max()\n        features[f'{col}_skew'] = sequence[col].skew()\n\n    # Thermopile (Thm) and Time-of-Flight (ToF) Features (only for full-data)\n    if is_full_data == 1:\n        thm_cols = [f'thm_{i}' for i in range(1, 6)]\n        for col in thm_cols:\n            features[f'{col}_mean'] = sequence[col].mean()\n            features[f'{col}_std'] = sequence[col].std()\n            features[f'{col}_min'] = sequence[col].min()\n            features[f'{col}_max'] = sequence[col].max()\n        \n        for i in range(1, 5):\n            features[f'thm_diff_{i}_{i+1}'] = sequence[f'thm_{i}'].mean() - sequence[f'thm_{i+1}'].mean()\n\n        for i in range(1, 6):\n            tof_sensor_cols = [f'tof_{i}_v{j}' for j in range(64)]\n            tof_data = sequence[tof_sensor_cols].replace(-1, np.nan)\n            features[f'tof_{i}_mean'] = tof_data.mean().mean()\n            features[f'tof_{i}_std'] = tof_data.std().mean()\n            features[f'tof_{i}_pixel_count'] = (~tof_data.isnull()).sum().mean()\n    \n    return pd.DataFrame([features])\n\n# --- 3. The Competition's `predict` function ---\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    This function is called by the Kaggle inference server for each test sequence.\n    It takes a Polars DataFrame and returns a single gesture string.\n    \"\"\"\n    # Convert Polars DataFrames to Pandas for easier feature engineering\n    sequence_pd = sequence.to_pandas()\n    demographics_pd = demographics.to_pandas()\n\n    # Create the feature vector for this single sequence\n    features_df = create_inference_features(sequence_pd, demographics_pd)\n\n    # Use the appropriate model based on data availability\n    is_full_data = features_df['is_full_data'].iloc[0]\n\n    if is_full_data == 1:\n        # Use the full-data model\n        X_full_test = features_df[lgb_full.feature_name_]\n        y_pred_encoded = lgb_full.predict(X_full_test)[0]\n    else:\n        # Use the IMU-only model\n        X_imu_test = features_df[lgb_imu.feature_name_]\n        y_pred_encoded = lgb_imu.predict(X_imu_test)[0]\n    \n    # Decode the prediction back to the original gesture string\n    predicted_gesture = le.inverse_transform([y_pred_encoded])[0]\n    \n    return predicted_gesture\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-02T18:30:37.387580Z","iopub.execute_input":"2025-08-02T18:30:37.387913Z","iopub.status.idle":"2025-08-02T18:30:37.852808Z","shell.execute_reply.started":"2025-08-02T18:30:37.387875Z","shell.execute_reply":"2025-08-02T18:30:37.851901Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nimport pandas as pd\nimport polars as pl\n\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    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-02T18:30:37.853746Z","iopub.execute_input":"2025-08-02T18:30:37.854019Z","iopub.status.idle":"2025-08-02T18:30:39.602966Z","shell.execute_reply.started":"2025-08-02T18:30:37.853997Z","shell.execute_reply":"2025-08-02T18:30:39.602179Z"}},"outputs":[],"execution_count":null}]}