{"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":"import polars as pl, pandas as pd, numpy as np\nimport lightgbm as lgb\n\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import f1_score\n\nimport joblib\nimport os\nimport warnings\nwarnings.filterwarnings('ignore')\n\n\n# Suppress warnings for cleaner output\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\ntrain=pl.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T11:34:54.615134Z","iopub.execute_input":"2025-06-09T11:34:54.615444Z","iopub.status.idle":"2025-06-09T11:35:18.078079Z","shell.execute_reply.started":"2025-06-09T11:34:54.615420Z","shell.execute_reply":"2025-06-09T11:35:18.077191Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Competition Metric","metadata":{}},{"cell_type":"code","source":"\"\"\"\nHierarchical macro F1 metric for the CMI 2025 Challenge.\n\nThis script defines a single entry point `score(solution, submission, row_id_column_name)`\nthat the Kaggle metrics orchestrator will call.\nIt performs validation on submission IDs and computes a combined binary & multiclass F1 score.\n\"\"\"\n\n\n\n\n\nclass ParticipantVisibleError(Exception):\n    \"\"\"Errors raised here will be shown directly to the competitor.\"\"\"\n    pass\n\n\nclass CompetitionMetric:\n    \"\"\"Hierarchical macro F1 for the CMI 2025 challenge.\"\"\"\n    def __init__(self):\n        self.target_gestures = [\n            'Above ear - pull hair',\n            'Cheek - pinch skin',\n            'Eyebrow - pull hair',\n            'Eyelash - pull hair',\n            'Forehead - pull hairline',\n            'Forehead - scratch',\n            'Neck - pinch skin',\n            'Neck - scratch',\n        ]\n        self.non_target_gestures = [\n            'Write name on leg',\n            'Wave hello',\n            'Glasses on/off',\n            'Text on phone',\n            'Write name in air',\n            'Feel around in tray and pull out an object',\n            'Scratch knee/leg skin',\n            'Pull air toward your face',\n            'Drink from bottle/cup',\n            'Pinch knee/leg skin'\n        ]\n        self.all_classes = self.target_gestures + self.non_target_gestures\n\n    def calculate_hierarchical_f1(\n        self,\n        sol: pd.DataFrame,\n        sub: pd.DataFrame\n    ) -> float:\n\n        # Validate gestures\n        invalid_types = {i for i in sub['gesture'].unique() if i not in self.all_classes}\n        if invalid_types:\n            raise ParticipantVisibleError(\n                f\"Invalid gesture values in submission: {invalid_types}\"\n            )\n\n        # Compute binary F1 (Target vs Non-Target)\n        y_true_bin = sol['gesture'].isin(self.target_gestures).values\n        y_pred_bin = sub['gesture'].isin(self.target_gestures).values\n        f1_binary = f1_score(\n            y_true_bin,\n            y_pred_bin,\n            pos_label=True,\n            zero_division=0,\n            average='binary'\n        )\n\n        # Build multi-class labels for gestures\n        y_true_mc = sol['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        y_pred_mc = sub['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n\n        # Compute macro F1 over all gesture classes\n        f1_macro = f1_score(\n            y_true_mc,\n            y_pred_mc,\n            average='macro',\n            zero_division=0\n        )\n\n        return 0.5 * f1_binary + 0.5 * f1_macro\n\n\ndef score(\n    solution: pd.DataFrame,\n    submission: pd.DataFrame,\n    row_id_column_name: str\n) -> float:\n    \"\"\"\n    Compute hierarchical macro F1 for the CMI 2025 challenge.\n\n    Expected input:\n      - solution and submission as pandas.DataFrame\n      - Column 'sequence_id': unique identifier for each sequence\n      - 'gesture': one of the eight target gestures or \"Non-Target\"\n\n    This metric averages:\n    1. Binary F1 on SequenceType (Target vs Non-Target)\n    2. Macro F1 on gesture (mapping non-targets to \"Non-Target\")\n\n    Raises ParticipantVisibleError for invalid submissions,\n    including invalid SequenceType or gesture values.\n\n\n    Examples\n    --------\n    >>> import pandas as pd\n    >>> row_id_column_name = \"id\"\n    >>> solution = pd.DataFrame({'id': range(4), 'gesture': ['Eyebrow - pull hair']*4})\n    >>> submission = pd.DataFrame({'id': range(4), 'gesture': ['Forehead - pull hairline']*4})\n    >>> score(solution, submission, row_id_column_name=row_id_column_name)\n    0.5\n    >>> submission = pd.DataFrame({'id': range(4), 'gesture': ['Text on phone']*4})\n    >>> score(solution, submission, row_id_column_name=row_id_column_name)\n    0.0\n    >>> score(solution, solution, row_id_column_name=row_id_column_name)\n    1.0\n    \"\"\"\n    # Validate required columns\n    for col in (row_id_column_name, 'gesture'):\n        if col not in solution.columns:\n            raise ParticipantVisibleError(f\"Solution file missing required column: '{col}'\")\n        if col not in submission.columns:\n            raise ParticipantVisibleError(f\"Submission file missing required column: '{col}'\")\n\n    metric = CompetitionMetric()\n    return metric.calculate_hierarchical_f1(solution, submission)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T11:36:44.570711Z","iopub.execute_input":"2025-06-09T11:36:44.571317Z","iopub.status.idle":"2025-06-09T11:36:44.581127Z","shell.execute_reply.started":"2025-06-09T11:36:44.571294Z","shell.execute_reply":"2025-06-09T11:36:44.580435Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Playing with data","metadata":{}},{"cell_type":"code","source":"train=pl.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntrain_demo=pl.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T11:36:51.518979Z","iopub.execute_input":"2025-06-09T11:36:51.519266Z","iopub.status.idle":"2025-06-09T11:36:55.838450Z","shell.execute_reply.started":"2025-06-09T11:36:51.519244Z","shell.execute_reply":"2025-06-09T11:36:55.837816Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Feature Engineering","metadata":{}},{"cell_type":"code","source":"def create_advanced_imu_features(df: pl.DataFrame) -> pl.DataFrame:\n    \"\"\"\n    Generates advanced statistical and phase-approximated features \n    from IMU data ONLY.\n    \"\"\"\n    imu_cols = [col for col in df.columns if 'acc_' in col or 'rot_' in col]\n    aggs = []\n    \n    # 1. Whole-sequence statistical features\n    for col in imu_cols:\n        aggs.extend([\n            pl.mean(col).alias(f'{col}_mean'),\n            pl.std(col).alias(f'{col}_std'),\n            pl.max(col).alias(f'{col}_max'),\n            pl.min(col).alias(f'{col}_min'),\n            pl.quantile(col, 0.25).alias(f'{col}_q25'),\n            pl.quantile(col, 0.75).alias(f'{col}_q75'),\n        ])\n\n    # 2. Difference features\n    for col in imu_cols:\n        aggs.extend([\n            (pl.col(col).diff().fill_null(0)).mean().alias(f'{col}_diff_mean'),\n            (pl.col(col).diff().fill_null(0)).std().alias(f'{col}_diff_std'),\n        ])\n        \n    # 3. Phase-approximated features\n    for part_name, part_expr in [\n        ('first_30pct', pl.col('sequence_counter') < pl.max('sequence_counter') * 0.3),\n        ('middle_40pct', (pl.col('sequence_counter') >= pl.max('sequence_counter') * 0.3) & (pl.col('sequence_counter') < pl.max('sequence_counter') * 0.7)),\n        ('last_30pct', pl.col('sequence_counter') >= pl.max('sequence_counter') * 0.7),\n    ]:\n        for col in imu_cols:\n            aggs.extend([\n                (pl.when(part_expr).then(pl.col(col))).mean().alias(f'{col}_mean_{part_name}'),\n                (pl.when(part_expr).then(pl.col(col))).std().alias(f'{col}_std_{part_name}'),\n            ])\n\n    feature_df = df.group_by('sequence_id').agg(aggs).fill_null(0)\n    return feature_df\n\n\ntrain_features=create_advanced_imu_features(train)\n\ntargets=train.group_by('sequence_id').agg(\n    pl.first('gesture'),\n    pl.first('subject'),\n    pl.first('sequence_type')\n)\n\ntrain_features_full = train_features.join(targets, on='sequence_id',how='left')\ntrain_features_full=train_features_full.join(train_demo, on='subject',how='left')\n\ntrain2 = train_features_full.to_pandas()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T11:35:31.929769Z","iopub.execute_input":"2025-06-09T11:35:31.930382Z","iopub.status.idle":"2025-06-09T11:35:34.013688Z","shell.execute_reply.started":"2025-06-09T11:35:31.930333Z","shell.execute_reply":"2025-06-09T11:35:34.012812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\n\n# plt.scatterplot(dat)\n# sns.countplot(data=train2,x='gesture')\nsns.histplot(data=train2, x='age',bins=7)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T11:25:38.961485Z","iopub.execute_input":"2025-06-09T11:25:38.961806Z","iopub.status.idle":"2025-06-09T11:25:39.132220Z","shell.execute_reply.started":"2025-06-09T11:25:38.961778Z","shell.execute_reply":"2025-06-09T11:25:39.131268Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Model Training","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm\n# lgb hyperparameters\nlgb_params = {\"device\":'gpu',                  # enables GPU\n    \"tree_method\":'gpu_hist',       # optional, for XGBoost-style syntax\n    'objective': 'multiclass',\n    'class_weight': 'balanced',\n    'n_estimators': 10_000,\n    'random_state': 42,\n    'n_jobs': -1,\n    'verbosity': -1,\n    'learning_rate': 0.0682511123410753,\n    'num_leaves': 285,\n    'max_depth': 11,\n    'reg_alpha': 0.015067299011315672,\n    'reg_lambda': 0.408041751307428,\n    'colsample_bytree': 0.4028382627354783,\n    'subsample': 0.6873886039232031\n}\n\n# Make X, y and group sets\nX = train2.drop(columns=['sequence_id', 'gesture', 'subject', 'sequence_type'])\ny = train2['gesture']\ngroups = train2['subject']\n\n# Model training setting\nk=5\ngkf = GroupKFold(n_splits=k)\n\n\noof_predictions = np.zeros(X.shape[0]).astype(int)\nmodels=[]\nscores=[]\n\nfolds = list(gkf.split(X, y, groups=groups))  # Ensure it's a fixed iterable\n\n# Start training (CV)\nt = tqdm(range(len(folds)), desc=\"Training folds\")\nfor i in t:\n\n    train_idx, valid_idx = folds[i]\n    X_train, y_train = X.loc[train_idx], y.loc[train_idx]\n    X_valid, y_valid = X.loc[valid_idx], y.loc[valid_idx]\n\n    le=LabelEncoder()\n    y_train_enc=le.fit_transform(y_train)\n    y_valid_enc=le.transform(y_valid)\n\n\n    model=lgb.LGBMClassifier(**lgb_params)\n    # Training\n    model.fit(X_train, y_train_enc,eval_set=[(X_valid, y_valid_enc)], callbacks=[lgb.early_stopping(100, verbose=False)])\n    # Prediction\n    y_pred=model.predict(X_valid)\n\n    # OOF prediction \n    oof_predictions[valid_idx]=y_pred\n    \n    # Validation scores \n    y_true=pd.DataFrame({'id':range(len(y.loc[valid_idx])),'gesture':y.loc[valid_idx]})\n    y_pred=pd.DataFrame({'id':range(len(y_pred)),'gesture':le.inverse_transform(oof_predictions[valid_idx])})\n\n    performance_score = score(y_true, y_pred,row_id_column_name='id')\n\n    # Append models and scores\n    models.append(model)\n    scores.append(performance_score)\n    \n    t.set_postfix({'Fold': i+1, 'Score': round(performance_score, 4)})\n\n    \n# create dataframes of true and predicted labels\ny_true_total=pd.DataFrame({'id':range(len(y)),'gesture':y})\ny_pred_total=pd.DataFrame({'id':range(len(y)),'gesture':le.inverse_transform(oof_predictions)})\n\n    # Calculate score\nperformance_score_overall = score(y_true, y_pred,row_id_column_name='id')\nprint(f\"OOF Score: {performance_score_overall} \")   \n ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-09T11:37:02.313500Z","iopub.execute_input":"2025-06-09T11:37:02.314623Z","iopub.status.idle":"2025-06-09T11:40:03.577507Z","shell.execute_reply.started":"2025-06-09T11:37:02.314588Z","shell.execute_reply":"2025-06-09T11:40:03.576839Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Submission","metadata":{}},{"cell_type":"code","source":"# for now choose the last trained model and label encoder in CV\nimport kaggle_evaluation.cmi_inference_server\nfeature_list=X.columns.tolist()\nle=LabelEncoder()\nle.fit(y)\n\n# --- 3. Define Predict Function ---\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Receives a single test sequence and predicts the gesture using ensemble of models.\n    \"\"\"\n    feature_df = create_advanced_imu_features(sequence)\n    \n    \n    subjects=sequence.group_by('sequence_id').agg(pl.first('subject'))\n    feature_df=feature_df.join(subjects, on='sequence_id',how='left')\n    feature_df_with_dem=feature_df.join(demographics, on='subject',how='left')\n   \n    X_test = feature_df_with_dem[feature_list]\n    \n    # Predict class probabilities with all models\n    probs = [model.predict_proba(X_test) for model in models]\n\n    # Average predictions\n    avg_probs = sum(probs) / len(probs)\n\n    # Get class index with highest average probability\n    pred_idx = avg_probs.argmax(axis=1)[0]\n\n    # Decode back to gesture string\n    pred_label = le.inverse_transform([pred_idx])[0]\n    return pred_label\n\n\n# --- 4. Start the Inference Server ---\nprint(\"Starting inference server...\")\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    # This branch runs when you submit the notebook\n    inference_server.serve()\nelse:\n    # This branch runs for local testing in the notebook editor\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-06-09T12:11:50.898108Z","iopub.execute_input":"2025-06-09T12:11:50.898883Z","iopub.status.idle":"2025-06-09T12:11:50.972344Z","shell.execute_reply.started":"2025-06-09T12:11:50.898857Z","shell.execute_reply":"2025-06-09T12:11:50.971183Z"}},"outputs":[],"execution_count":null}]}