{"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"},{"sourceId":242895646,"sourceType":"kernelVersion"}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **FOREWORD**\n\nThis kernel is a baseline experiment for the usage of IMU sensor data with ML models. I start with a simple multiclass model and check the CV-LB sync here. \n\nI use a manual mapper for the 18 targets with the below convention- \n- Target categories from 0-7 are the target classes\n- Classes 8-17 are for non-target kinesiology\n- I design my own competition metric equivalent using this numeric data and convention\n\n## **VERSION DETAILS**\n\n|Version|Details|\n|---- | ----- |\n|2 | Submitted the model for the leaderboard and scored 0.69 |\n|3 | Corrected a small issue with the competiion metric and re-ran the same model to obtain the correct CV score|\n|4 | Added some documentation and foreword and presented this for public usage |\n|5 | Used LightGBM as a single model option |\n\nThe single catboost model here provides a **CV - LB sync with CV = 0.6904 and LB = 0.69!** <br>\nI shall build on this and release a detailed baseline kernel shortly! Stay tuned!\n\nThis model works well with CPU and GPU, but prefer the GPU version to stay compliant with the timing requirements of the API! <br>\nIn this version, I try a simple LGBM classifier and elicit the CV-LB score.","metadata":{}},{"cell_type":"markdown","source":"# **IMPORTS**","metadata":{}},{"cell_type":"code","source":"!pip install -q polars==1.29.0      --no-index --find-links=/kaggle/input/cmi2025-public-imports-v2/packages\n!pip install -q lightgbm==4.6.0     --no-index --find-links=/kaggle/input/cmi2025-public-imports-v2/packages\n!pip install -q scikit-learn==1.6.1 --no-index --find-links=/kaggle/input/cmi2025-public-imports-v2/packages\n\nimport os\nimport numpy as np\nimport polars as pl\nimport pandas as pd\nimport joblib\nfrom tqdm.notebook import tqdm\nfrom gc import collect\nfrom pprint import pprint\n\nimport torch\n\nfrom sklearn.base import clone\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import StratifiedGroupKFold, PredefinedSplit\nfrom sklearn.ensemble import BaggingClassifier as BC\nfrom sklearn.metrics import *\nfrom lightgbm import LGBMClassifier as LGBMC, log_evaluation, early_stopping\nfrom catboost import CatBoostClassifier as CBC\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T21:14:45.907944Z","iopub.execute_input":"2025-06-06T21:14:45.908114Z","iopub.status.idle":"2025-06-06T21:15:12.868770Z","shell.execute_reply.started":"2025-06-06T21:14:45.908098Z","shell.execute_reply":"2025-06-06T21:15:12.867945Z"},"_kg_hide-output":true,"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target  = \"gesture\"\ngrouper = \"subject\"\n\nmapper = \\\n{\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}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T21:15:26.995167Z","iopub.execute_input":"2025-06-06T21:15:26.995887Z","iopub.status.idle":"2025-06-06T21:15:27.000642Z","shell.execute_reply.started":"2025-06-06T21:15:26.995838Z","shell.execute_reply":"2025-06-06T21:15:26.999941Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **PREPROCESSING**","metadata":{}},{"cell_type":"code","source":"%%time \n\ndef make_ftre(sequence : pl.DataFrame, demographics : pl.DataFrame) :\n    \"\"\"\n    Makes aggregate columns for the model\n    Source - https://www.kaggle.com/code/farisalahmdi/lgbm-inference\n    \"\"\"\n    \n    agg_exprs = []\n    stat_cols = ['acc_x','acc_y','acc_z','rot_w','rot_x','rot_y','rot_z']\n    \n    for c in stat_cols:\n        agg_exprs += [\n            pl.col(c).mean().alias(f\"{c}_mean\"),\n            pl.col(c).std().alias(f\"{c}_std\"),\n            pl.col(c).var().alias(f\"{c}_var\"),\n            pl.col(c).quantile(0.25).alias(f\"{c}_q25\"),\n            pl.col(c).median().alias(f\"{c}_q50\"),\n            pl.col(c).quantile(0.75).alias(f\"{c}_q75\"),\n            pl.col(c).max().alias(f\"{c}_max\"),\n            pl.col(c).min().alias(f\"{c}_min\"),\n            pl.col(c).first().alias(f\"{c}_first\"),\n            pl.col(c).last().alias(f\"{c}_last\"),\n            pl.col(c).quantile(0.25, \"nearest\").alias(f\"{c}_t25\"),\n            pl.col(c).quantile(0.75, \"nearest\").alias(f\"{c}_t75\"),\n            (pl.col(c).last() - pl.col(c).first()).alias(f\"{c}_delta\"),\n            pl.corr(\"sequence_counter\", c).alias(f\"{c}_corr_time\"),\n            pl.col(c).diff().mean().alias(f\"{c}_diff_mean\"),\n            pl.col(c).diff().std().alias(f\"{c}_diff_std\"),\n            pl.col(c).skew().alias(f\"{c}_skew\"),\n            pl.col(c).kurtosis().alias(f\"{c}_kurt\"),\n            pl.col(c).diff().abs().gt(0).sum().alias(f\"{c}_n_changes\")\n        ]\n        \n        agg_exprs += [\n                pl.when(pl.col(\"sequence_counter\") < 0.1 * pl.max(\"sequence_counter\"))\n                  .then(pl.col(c)).otherwise(None).mean().alias(f\"{c}_seg1_mean\"),\n                pl.when(pl.col(\"sequence_counter\") > 0.9 * pl.max(\"sequence_counter\"))\n                  .then(pl.col(c)).otherwise(None).mean().alias(f\"{c}_seg3_mean\"),\n        ]\n    \n    return \\\n    (\n        sequence.\n        group_by(pl.col([\"sequence_id\", \"subject\"]), maintain_order=True).\n        agg(agg_exprs).\n        select(pl.all().shrink_dtype()).\n        join(\n            demographics.select(pl.all().shrink_dtype()),\n            how = \"left\",\n            on = [\"subject\"],\n        ).\n        drop([\"subject\"], strict = False).\n        to_pandas()\n    )\n    \n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T21:15:30.311467Z","iopub.execute_input":"2025-06-06T21:15:30.311964Z","iopub.status.idle":"2025-06-06T21:15:30.321088Z","shell.execute_reply.started":"2025-06-06T21:15:30.311938Z","shell.execute_reply":"2025-06-06T21:15:30.320338Z"},"_kg_hide-input":true,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time \n \ntrain    = pl.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntraind   = pl.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\ntest     = pl.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\")\ntestd    = pl.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")\n\nsel_cols = [ c for c in test.collect_schema().names() if \"thm\" not in c and \"tof\" not in c ]\n\nXtrain         = make_ftre(train.select(pl.col(sel_cols)), traind)\nXtest          = make_ftre(test.select(pl.col(sel_cols)),  testd)\n\nYtrain         = train.select(pl.col([\"sequence_id\", \"subject\", target])).unique().to_pandas()\nYtrain[target] = Ytrain[target].map(mapper).astype(np.int8)\nYtrain         = Xtrain[[\"sequence_id\"]].merge(Ytrain, how = \"inner\", on = [\"sequence_id\"])\nytrain         = Ytrain[target]\n\nygrp = np.zeros(len(Xtrain))\ncv   = StratifiedGroupKFold(n_splits = 5, shuffle = True, random_state = 42)\n\nfor fold_nb, (train_idx, dev_idx) in tqdm( \n    enumerate(cv.split(Xtrain, Ytrain[target], Ytrain[grouper] )) \n) :\n    ygrp[dev_idx] = fold_nb\n\ncv = PredefinedSplit(ygrp)\nprint(\n    f\"\\n\\n---> Shape = {Xtrain.shape} {ytrain.shape} {ygrp.shape} {Xtest.shape}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T21:15:33.745483Z","iopub.execute_input":"2025-06-06T21:15:33.745774Z","iopub.status.idle":"2025-06-06T21:15:47.118418Z","shell.execute_reply.started":"2025-06-06T21:15:33.745753Z","shell.execute_reply":"2025-06-06T21:15:47.117802Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **MODEL TRAINING**","metadata":{}},{"cell_type":"code","source":"def ScoreMetric(ytrue, ypreds)-> tuple:\n    \"Defines the score metric for the competition using the true and predicted labels\"\n\n    bscore = f1_score(\n        np.where(ytrue  <= 7, 1, 0),\n        np.where(ypreds <= 7, 1, 0),\n        zero_division = 0.0,\n    )\n\n    mscore = f1_score(\n        np.where(ytrue   <= 7, ytrue, 99),\n        np.where(ypreds  <= 7, ypreds, 99),\n        average = \"macro\", \n        zero_division = 0.0,\n    )\n\n    return (0.5 * (bscore + mscore), bscore, mscore)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T21:15:51.222636Z","iopub.execute_input":"2025-06-06T21:15:51.223319Z","iopub.status.idle":"2025-06-06T21:15:51.227728Z","shell.execute_reply.started":"2025-06-06T21:15:51.223293Z","shell.execute_reply":"2025-06-06T21:15:51.226998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time \n\nmethod = \"LGBM1C\"\nmymodel = \\\nLGBMC(**{'device'             : \"gpu\" if torch.cuda.is_available() else \"cpu\",\n         'objective'          : \"multiclass\",\n         \"n_estimators\"       : 10000, \n         \"max_depth\"          : 8,\n         \"learning_rate\"      : 0.025,\n         \"colsample_bytree\"   : 0.55,               \n         \"n_jobs\"             : -1,\n         \"num_leaves\"         : 75,\n         \"random_state\"       : 42,\n         \"reg_alpha\"          : 0.001,\n         \"reg_lambda\"         : 0.001,\n         \"subsample\"          : 0.40,\n         \"verbosity\"          : -1,\n      }\n)\n\noof_preds     = []\nfitted_models = []\nOOF_Preds     = {}\ndrop_cols     = [\"sequence_id\", target, grouper]\n\nfor fold_nb, (train_idx, dev_idx) in tqdm( \n    enumerate(cv.split(Xtrain, ytrain) , start = 1)\n):\n    \n    Xtr  = Xtrain.iloc[train_idx].drop(drop_cols, axis = 1, errors = \"ignore\")\n    Xdev = Xtrain.iloc[dev_idx].drop(drop_cols, axis = 1, errors = \"ignore\")\n    ytr  = ytrain.iloc[train_idx]\n    ydev = ytrain.iloc[dev_idx]\n\n    model = clone(mymodel)\n    model.fit(\n        Xtr, ytr, \n        eval_set = [(Xdev, ydev)], \n        **{\"callbacks\" : [log_evaluation(0), early_stopping(100, verbose = False)]}\n    )\n    dev_preds = pd.DataFrame(model.predict_proba(Xdev), index = dev_idx)\n    oof_preds.append(dev_preds)\n    fitted_models.append(model)\n    \n    score, bscore, mscore = ScoreMetric(\n        ydev.values.flatten(), \n        dev_preds.idxmax(axis=1).values.flatten()\n    )\n    print(\n        f\"---> OOF = {score:,.8f} | {bscore:,.8f} | {mscore:,.8f} -- Fold {fold_nb}\"\n    )\n\noof_preds = pd.concat(oof_preds, axis=0).sort_index(ascending = True)\nOOF_Preds[method] = oof_preds\n\nscore, bscore, mscore = \\\nScoreMetric(\n    ytrain.values.flatten(), \n    oof_preds.idxmax(axis=1).values.flatten()\n)\nprint(\n    f\"\\n\\n---> Overall OOF = {score:,.8f} | {bscore:,.8f} | {mscore:,.8f}\"\n)\n\ncollect()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T21:15:56.637719Z","iopub.execute_input":"2025-06-06T21:15:56.638344Z","iopub.status.idle":"2025-06-06T21:20:38.142722Z","shell.execute_reply.started":"2025-06-06T21:15:56.638321Z","shell.execute_reply":"2025-06-06T21:20:38.141939Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **INFERENCING**","metadata":{}},{"cell_type":"code","source":"%%time \n\nimport kaggle_evaluation.cmi_inference_server\n\ndef predict(\n    sequence     : pl.DataFrame, \n    demographics : pl.DataFrame,\n) -> str:\n    \"\"\"\n    Prediction function for Kaggle evaluation\n    \"\"\"\n\n    drop_cols  = [\"sequence_id\", target, grouper]\n    Xtest      = make_ftre(sequence, demographics).drop(drop_cols, axis=1, errors = \"ignore\")\n\n    test_preds = []\n    for model in fitted_models :\n        test_preds.append( \n            pd.DataFrame(model.predict_proba(Xtest) , index = Xtest.index) \n        )\n\n    test_preds = pd.concat(test_preds, axis=0).groupby(level = 0).mean()\n    test_preds = test_preds.sort_index(ascending = True)\n    pred = test_preds.idxmax(axis=1).map({v: k for k, v in mapper.items()}).values.flatten()[0]\n    \n    print(f\"Final prediction = {pred}\")\n    return pred","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T21:20:51.234740Z","iopub.execute_input":"2025-06-06T21:20:51.235024Z","iopub.status.idle":"2025-06-06T21:20:51.534879Z","shell.execute_reply.started":"2025-06-06T21:20:51.235002Z","shell.execute_reply":"2025-06-06T21:20:51.534317Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_server = \\\nkaggle_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-06-06T21:20:54.378627Z","iopub.execute_input":"2025-06-06T21:20:54.379229Z","iopub.status.idle":"2025-06-06T21:20:55.445222Z","shell.execute_reply.started":"2025-06-06T21:20:54.379202Z","shell.execute_reply":"2025-06-06T21:20:55.444623Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null}]}