{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"nvidiaTeslaT4","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"},{"sourceId":13607639,"sourceType":"datasetVersion","datasetId":8619229}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":1494.813501,"end_time":"2025-10-25T19:51:35.870094","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-25T19:26:41.056593","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"00962fe9010e42b7a5b123af50d2a026":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"0b098fa23a3b4031bf88b32b20044978":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"108c3e5db1154f47a5aca68386ed4f24":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"38c36db3be3843b19da108d4cd1f9421":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_3e9806911d1d489d963f76955d8468c4","IPY_MODEL_e6ad60bc760e4b51885b15335c048a65","IPY_MODEL_6f4723180bf14101b60deeac9fd59e50"],"layout":"IPY_MODEL_00962fe9010e42b7a5b123af50d2a026","tabbable":null,"tooltip":null}},"3e9806911d1d489d963f76955d8468c4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_aeb2544f9ad9413b8b71b5f5643b1019","placeholder":"​","style":"IPY_MODEL_ec012e9ac53d477daee0988a35117f0b","tabbable":null,"tooltip":null,"value":"100%"}},"3edd91e1eafb4f6bbf55275ea60b6c97":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"6f4723180bf14101b60deeac9fd59e50":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_e8f95c8096294ce6b6fb2b65c08af536","placeholder":"​","style":"IPY_MODEL_0b098fa23a3b4031bf88b32b20044978","tabbable":null,"tooltip":null,"value":" 863/863 [00:08&lt;00:00, 96.46it/s]"}},"aeb2544f9ad9413b8b71b5f5643b1019":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"e6ad60bc760e4b51885b15335c048a65":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_3edd91e1eafb4f6bbf55275ea60b6c97","max":863,"min":0,"orientation":"horizontal","style":"IPY_MODEL_108c3e5db1154f47a5aca68386ed4f24","tabbable":null,"tooltip":null,"value":863}},"e8f95c8096294ce6b6fb2b65c08af536":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"ec012e9ac53d477daee0988a35117f0b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## credit goes tp below notebook and author\n\nhttps://www.kaggle.com/code/ravaghi/social-action-recognition-in-mice-xgboost\n\n## new changes included \n- randomsearchcv for best hyperparameter estimator finding automatically\n- mabapipeline model code for ease of usage","metadata":{}},{"cell_type":"markdown","source":"**References**\n- [MABe Nearest Neighbors: The Original ⭐️⭐️⭐️⭐️⭐️](https://www.kaggle.com/code/ambrosm/mabe-nearest-neighbors-the-original)\n- [MABe EDA which makes sense ⭐️⭐️⭐️⭐️⭐️](https://www.kaggle.com/code/ambrosm/mabe-eda-which-makes-sense)\n- [MABe Validated baseline without machine learning](https://www.kaggle.com/code/ambrosm/mabe-validated-baseline-without-machine-learning)\n- [Squeeze GBT](https://www.kaggle.com/code/cody11null/squeeze-gbt)","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":0.007766,"end_time":"2025-10-25T19:26:46.411629","exception":false,"start_time":"2025-10-25T19:26:46.403863","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import json\nimport warnings\nimport gc\nimport itertools\nimport numpy as np\nimport pandas as pd\nimport joblib\nimport optuna\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple, Optional\nfrom dataclasses import dataclass\nfrom tqdm.notebook import tqdm\n\nfrom sklearn.model_selection import StratifiedGroupKFold, RandomizedSearchCV\nfrom sklearn.metrics import f1_score\nfrom xgboost import XGBClassifier\nfrom scipy.stats import uniform, randint\n\noptuna.logging.set_verbosity(optuna.logging.WARNING)\nwarnings.filterwarnings('ignore')\n\n@dataclass\nclass PipelineConfig:\n    train_path: str\n    test_path: str\n    train_annotation_path: str\n    train_tracking_path: str\n    test_tracking_path: str\n    model_path: str\n    model_name: str\n    mode: str = \"submit\"\n    n_splits: int = 3\n    param_dist: Dict = None\n    n_iter_search: int = 50\n    optuna_trials: int = 1000\n    \n    def __post_init__(self):\n        if self.param_dist is None:\n            self.param_dist = {\n                'n_estimators': randint(100, 500),\n                'learning_rate': uniform(0.01, 0.2),\n                'max_depth': randint(3, 10),\n                'min_child_weight': randint(1, 10),\n                'subsample': uniform(0.5, 1.0),\n                'colsample_bytree': uniform(0.5, 1.0),\n                'gamma': uniform(0, 5),\n                'reg_alpha': uniform(0, 1),\n                'reg_lambda': uniform(0.5, 2)\n            }\n\nclass BehaviorFeatureTransformer:\n    DROP_BODY_PARTS = [\n        'headpiece_bottombackleft', 'headpiece_bottombackright', 'headpiece_bottomfrontleft',\n        'headpiece_bottomfrontright', 'headpiece_topbackleft', 'headpiece_topbackright',\n        'headpiece_topfrontleft', 'headpiece_topfrontright', 'spine_1', 'spine_2',\n        'tail_middle_1', 'tail_middle_2', 'tail_midpoint'\n    ]\n    \n    @staticmethod\n    def _scale(n_frames_at_30fps: int, fps: float, ref: float = 30.0) -> int:\n        return max(1, int(round(n_frames_at_30fps * float(fps) / ref)))\n    \n    @staticmethod\n    def _fps_from_meta(meta_df: pd.DataFrame, fallback_lookup: Dict, default_fps: float = 30.0) -> float:\n        if 'frames_per_second' in meta_df.columns and pd.notnull(meta_df['frames_per_second']).any():\n            return float(meta_df['frames_per_second'].iloc[0])\n        vid = meta_df['video_id'].iloc[0]\n        return float(fallback_lookup.get(vid, default_fps))\n        \n    @classmethod\n    def add_curvature_features(cls, X, cx, cy, fps):\n        vel_x, vel_y = cx.diff(), cy.diff()\n        acc_x, acc_y = vel_x.diff(), vel_y.diff()\n        cross = vel_x * acc_y - vel_y * acc_x\n        vel_mag = np.sqrt(vel_x**2 + vel_y**2)\n        curvature = np.abs(cross) / (vel_mag**3 + 1e-6)\n        for w in [25, 50, 75]:\n            ws = cls._scale(w, fps)\n            X[f'curv_mean_{w}'] = curvature.rolling(ws, min_periods=max(1, ws // 5)).mean()\n        angle = np.arctan2(vel_y, vel_x)\n        ws = cls._scale(30, fps)\n        angle_change = np.abs(angle.diff())\n        X[f'turn_rate_30'] = angle_change.rolling(ws, min_periods=max(1, ws // 5)).sum()\n        return X\n\n    @classmethod\n    def add_multiscale_features(cls, X, cx, cy, fps):\n        speed = np.sqrt(cx.diff()**2 + cy.diff()**2) * float(fps)\n        scales = [20, 40, 60, 80]\n        for scale in scales:\n            ws = cls._scale(scale, fps)\n            if len(speed) >= ws:\n                X[f'sp_m{scale}'] = speed.rolling(ws, min_periods=max(1, ws // 4)).mean()\n                X[f'sp_s{scale}'] = speed.rolling(ws, min_periods=max(1, ws // 4)).std()\n        if len(scales) >= 2 and f'sp_m{scales[0]}' in X.columns and f'sp_m{scales[-1]}' in X.columns:\n            X['sp_ratio'] = X[f'sp_m{scales[0]}'] / (X[f'sp_m{scales[-1]}'] + 1e-6)\n        return X\n    \n    @classmethod\n    def add_state_features(cls, X, cx, cy, fps):\n        speed = np.sqrt(cx.diff()**2 + cy.diff()**2) * float(fps)\n        w_ma = cls._scale(15, fps)\n        speed_ma = speed.rolling(w_ma, min_periods=max(1, w_ma // 3)).mean()\n        try:\n            bins = [-np.inf, 0.5 * fps, 2.0 * fps, 5.0 * fps, np.inf]\n            speed_states = pd.cut(speed_ma, bins=bins, labels=[0, 1, 2, 3]).astype(float)\n            for window in [20, 40, 60, 80]:\n                ws = cls._scale(window, fps)\n                if len(speed_states) >= ws:\n                    for state in [0, 1, 2, 3]:\n                        X[f's{state}_{window}'] = ((speed_states == state).astype(float)\n                            .rolling(ws, min_periods=max(1, ws // 5)).mean())\n                    state_changes = (speed_states != speed_states.shift(1)).astype(float)\n                    X[f'trans_{window}'] = state_changes.rolling(ws, min_periods=max(1, ws // 5)).sum()\n        except Exception: pass\n        return X\n\n    @classmethod\n    def add_longrange_features(cls, X, cx, cy, fps):\n        for window in [30, 60, 120]:\n            ws = cls._scale(window, fps)\n            if len(cx) >= ws:\n                X[f'x_ml{window}'] = cx.rolling(ws, min_periods=max(5, ws // 6)).mean()\n                X[f'y_ml{window}'] = cy.rolling(ws, min_periods=max(5, ws // 6)).mean()\n        for span in [30, 60, 120]:\n            s = cls._scale(span, fps)\n            X[f'x_e{span}'] = cx.ewm(span=s, min_periods=1).mean()\n            X[f'y_e{span}'] = cy.ewm(span=s, min_periods=1).mean()\n        speed = np.sqrt(cx.diff()**2 + cy.diff()**2) * float(fps)\n        for window in [30, 60, 120]:\n            ws = cls._scale(window, fps)\n            if len(speed) >= ws:\n                X[f'sp_pct{window}'] = speed.rolling(ws, min_periods=max(5, ws // 6)).rank(pct=True)\n        return X\n\n    @classmethod\n    def add_interaction_features(cls, X, mouse_pair, avail_A, avail_B, fps):\n        if 'body_center' not in avail_A or 'body_center' not in avail_B:\n            return X\n        rel_x = mouse_pair['A']['body_center']['x'] - mouse_pair['B']['body_center']['x']\n        rel_y = mouse_pair['A']['body_center']['y'] - mouse_pair['B']['body_center']['y']\n        rel_dist = np.sqrt(rel_x**2 + rel_y**2)\n        A_vx = mouse_pair['A']['body_center']['x'].diff()\n        A_vy = mouse_pair['A']['body_center']['y'].diff()\n        B_vx = mouse_pair['B']['body_center']['x'].diff()\n        B_vy = mouse_pair['B']['body_center']['y'].diff()\n        A_lead = (A_vx * rel_x + A_vy * rel_y) / (np.sqrt(A_vx**2 + A_vy**2) * rel_dist + 1e-6)\n        B_lead = (B_vx * (-rel_x) + B_vy * (-rel_y)) / (np.sqrt(B_vx**2 + B_vy**2) * rel_dist + 1e-6)\n        for window in [30, 60]:\n            ws = cls._scale(window, fps)\n            X[f'A_ld{window}'] = A_lead.rolling(ws, min_periods=max(1, ws // 6)).mean()\n            X[f'B_ld{window}'] = B_lead.rolling(ws, min_periods=max(1, ws // 6)).mean()\n        approach = -rel_dist.diff()\n        chase = approach * B_lead\n        ws = cls._scale(30, fps)\n        X[f'chase_30'] = chase.rolling(ws, min_periods=max(1, ws // 6)).mean()\n        for window in [60, 120]:\n            ws = cls._scale(window, fps)\n            A_sp = np.sqrt(A_vx**2 + A_vy**2)\n            B_sp = np.sqrt(B_vx**2 + B_vy**2)\n            X[f'sp_cor{window}'] = A_sp.rolling(ws, min_periods=max(1, ws // 6)).corr(B_sp)\n        return X\n\nclass BehaviorDataGenerator:\n    def __init__(self, config: PipelineConfig):\n        self.config = config\n        self.transformer = BehaviorFeatureTransformer()\n    def generate_mouse_data(self, dataset: pd.DataFrame, traintest: str,\n                           traintest_directory: Optional[str] = None,\n                           generate_single: bool = True,\n                           generate_pair: bool = True):\n        if traintest_directory is None:\n            traintest_directory = self.config.train_tracking_path if traintest == 'train' else self.config.test_tracking_path\n        for _, row in tqdm(dataset.iterrows(), desc=\"Processing videos\", total=len(dataset)):\n            lab_id = row.lab_id\n            if lab_id.startswith('MABe22') or (traintest == 'train' and type(row.behaviors_labeled) != str):\n                continue\n            video_id = row.video_id\n            path = f\"{traintest_directory}/{lab_id}/{video_id}.parquet\"\n            try: vid = pd.read_parquet(path)\n            except FileNotFoundError: continue\n            if len(np.unique(vid.bodypart)) > 5:\n                vid = vid.query(f\"~ bodypart.isin(@self.transformer.DROP_BODY_PARTS)\")\n            pvid = vid.pivot(columns=['mouse_id', 'bodypart'], index='video_frame', values=['x', 'y'])\n            del vid; gc.collect()\n            pvid = pvid.reorder_levels([1, 2, 0], axis=1).T.sort_index().T\n            pvid /= row.pix_per_cm_approx\n            vid_behaviors = json.loads(row.behaviors_labeled)\n            vid_behaviors = sorted(list({b.replace(\"'\", \"\") for b in vid_behaviors}))\n            vid_behaviors = [b.split(',') for b in vid_behaviors]\n            vid_behaviors = pd.DataFrame(vid_behaviors, columns=['agent', 'target', 'action'])\n            annot = None\n            if traintest == 'train':\n                try: annot = pd.read_parquet(path.replace('train_tracking', 'train_annotation'))\n                except FileNotFoundError: continue\n            if generate_single:\n                yield from self._generate_single(pvid, vid_behaviors, video_id, path, traintest, annot)\n            if generate_pair:\n                yield from self._generate_pair(pvid, vid_behaviors, video_id, path, traintest, annot)\n    def _generate_single(self, pvid, vid_behaviors, video_id, path, traintest, annot):\n        vid_behaviors_subset = vid_behaviors.query(\"target == 'self'\")\n        for mouse_id_str in np.unique(vid_behaviors_subset.agent):\n            try:\n                mouse_id = int(mouse_id_str[-1])\n                vid_agent_actions = np.unique(vid_behaviors_subset.query(f\"agent == '{mouse_id_str}'\").action)\n                single_mouse = pvid.loc[:, mouse_id]\n                single_mouse_meta = pd.DataFrame({\n                    'video_id': video_id,\n                    'agent_id': mouse_id_str,\n                    'target_id': 'self',\n                    'video_frame': single_mouse.index\n                })\n                if traintest == 'train':\n                    single_mouse_label = pd.DataFrame(0.0, columns=vid_agent_actions, index=single_mouse.index)\n                    annot_subset = annot.query(f\"(agent_id == {mouse_id}) & (target_id == {mouse_id})\")\n                    for i in range(len(annot_subset)):\n                        annot_row = annot_subset.iloc[i]\n                        single_mouse_label.loc[annot_row['start_frame']:annot_row['stop_frame'], annot_row.action] = 1.0\n                    yield 'single', single_mouse, single_mouse_meta, single_mouse_label\n                else:\n                    yield 'single', single_mouse, single_mouse_meta, vid_agent_actions\n            except KeyError: pass\n    def _generate_pair(self, pvid, vid_behaviors, video_id, path, traintest, annot):\n        vid_behaviors_subset = vid_behaviors.query(\"target != 'self'\")\n        if len(vid_behaviors_subset) > 0:\n            for agent, target in itertools.permutations(np.unique(pvid.columns.get_level_values('mouse_id')), 2):\n                agent_str = f\"mouse{agent}\"\n                target_str = f\"mouse{target}\"\n                vid_agent_actions = np.unique(\n                    vid_behaviors_subset.query(f\"(agent == '{agent_str}') & (target == '{target_str}')\").action\n                )\n                mouse_pair = pd.concat([pvid[agent], pvid[target]], axis=1, keys=['A', 'B'])\n                mouse_pair_meta = pd.DataFrame({\n                    'video_id': video_id,\n                    'agent_id': agent_str,\n                    'target_id': target_str,\n                    'video_frame': mouse_pair.index\n                })\n                if traintest == 'train':\n                    mouse_pair_label = pd.DataFrame(0.0, columns=vid_agent_actions, index=mouse_pair.index)\n                    annot_subset = annot.query(f\"(agent_id == {agent}) & (target_id == {target})\")\n                    for i in range(len(annot_subset)):\n                        annot_row = annot_subset.iloc[i]\n                        mouse_pair_label.loc[annot_row['start_frame']:annot_row['stop_frame'], annot_row.action] = 1.0\n                    yield 'pair', mouse_pair, mouse_pair_meta, mouse_pair_label\n                else:\n                    yield 'pair', mouse_pair, mouse_pair_meta, vid_agent_actions\n\nclass BehaviorClassifier:\n    def __init__(self, config: PipelineConfig):\n        self.config = config\n        self.transformer = BehaviorFeatureTransformer()\n    def tune_threshold(self, oof_action: np.ndarray, y_action: np.ndarray) -> float:\n        def objective(trial):\n            threshold = trial.suggest_float(\"threshold\", 0, 1, step=0.01)\n            return f1_score(y_action, (oof_action >= threshold), zero_division=0)\n        study = optuna.create_study(direction=\"maximize\")\n        study.optimize(objective, n_trials=self.config.optuna_trials, n_jobs=-1, show_progress_bar=False)\n        return study.best_params[\"threshold\"]\n    def cross_validate_with_randomsearch(self, X: pd.DataFrame, label: pd.DataFrame,\n                                         meta: pd.DataFrame, body_parts_str: str,\n                                         section: int) -> Tuple[List, List, Dict]:\n        oof = pd.DataFrame(index=meta.video_frame)\n        f1_list = []\n        submission_list = []\n        thresholds = {}\n        cv = StratifiedGroupKFold(n_splits=self.config.n_splits)\n        for action in tqdm(label.columns, desc=f\"[Section {section}] Tuning actions\", leave=True):\n            action_mask = ~label[action].isna().values\n            y_action = label[action][action_mask].values.astype(int)\n            X_action = X[action_mask]\n            groups_action = meta.video_id[action_mask].values\n            if len(np.unique(groups_action)) < self.config.n_splits:\n                continue\n            if not (y_action == 0).all():\n                try:\n                    with warnings.catch_warnings():\n                        warnings.filterwarnings('ignore', category=RuntimeWarning)\n                        base_model = XGBClassifier(\n                            verbosity=0, random_state=42, tree_method='hist', n_jobs=-1\n                        )\n                        random_search = RandomizedSearchCV(\n                            estimator=base_model,\n                            param_distributions=self.config.param_dist,\n                            n_iter=self.config.n_iter_search,\n                            cv=cv,\n                            scoring='f1_weighted',\n                            n_jobs=-1,\n                            verbose=0,\n                            random_state=42,\n                            refit=True,\n                            error_score=0\n                        )\n                        random_search.fit(X_action, y_action, groups=groups_action)\n                        best_model = random_search.best_estimator_\n                        oof_action = best_model.predict_proba(X_action)[:, 1]\n                        threshold = self.tune_threshold(oof_action, y_action)\n                        thresholds[action] = threshold\n                        f1 = f1_score(y_action, (oof_action >= threshold), zero_division=0)\n                        f1_list.append((body_parts_str, action, f1))\n                        self._save_results(section, action, best_model, \n                                          random_search.best_params_, threshold, oof_action)\n                        print(f\"    ✓ {action:20s} | F1: {f1:.4f} | Threshold: {threshold:.3f}\")\n                        del random_search, best_model\n                        gc.collect()\n                except Exception as e:\n                    oof_action = np.zeros(len(y_action))\n                    print(f\"    ✗ {action:20s} | Error: {str(e)[:50]}\")\n            else:\n                oof_action = np.zeros(len(y_action))\n                print(f\"    ✗ {action:20s} | No positive samples\")\n            oof_column = np.zeros(len(label))\n            oof_column[action_mask] = oof_action\n            oof[action] = oof_column\n            del oof_action, action_mask, X_action, y_action, groups_action\n            gc.collect()\n        submission_part = self.predict_multiclass(oof, meta, thresholds)\n        submission_list.append(submission_part)\n        return submission_list, f1_list, thresholds\n    def _save_results(self, section: int, action: str, model, best_params: Dict,\n                      threshold: float, oof_proba: np.ndarray):\n        save_dir = Path(f\"xgboost/{section}/{action}\")\n        save_dir.mkdir(parents=True, exist_ok=True)\n        joblib.dump(model, save_dir / \"best_model.pkl\")\n        joblib.dump(best_params, save_dir / \"best_params.pkl\")\n        joblib.dump(threshold, save_dir / \"threshold.pkl\")\n        joblib.dump(oof_proba, save_dir / \"oof_pred_probs.pkl\")\n    def predict_multiclass(self, pred: pd.DataFrame, meta: pd.DataFrame,\n                          thresholds: Dict) -> pd.DataFrame:\n        ama = np.argmax(pred.values, axis=1)\n        max_proba = pred.max(axis=1).values\n        threshold_array = np.array([thresholds.get(col, 0.27) for col in pred.columns])\n        action_thresholds = threshold_array[ama]\n        ama = np.where(max_proba >= action_thresholds, ama, -1)\n        ama = pd.Series(ama, index=meta.video_frame)\n        changes_mask = (ama != ama.shift(1)).values\n        ama_changes = ama[changes_mask]\n        meta_changes = meta[changes_mask]\n        mask = ama_changes.values >= 0\n        mask[-1] = False\n        submission_part = pd.DataFrame({\n            'video_id': meta_changes['video_id'][mask].values,\n            'agent_id': meta_changes['agent_id'][mask].values,\n            'target_id': meta_changes['target_id'][mask].values,\n            'action': pred.columns[ama_changes[mask].values],\n            'start_frame': ama_changes.index[mask],\n            'stop_frame': ama_changes.index[1:][mask[:-1]]\n        })\n        stop_video_id = meta_changes['video_id'][1:][mask[:-1]].values\n        stop_agent_id = meta_changes['agent_id'][1:][mask[:-1]].values\n        stop_target_id = meta_changes['target_id'][1:][mask[:-1]].values\n        for i in range(len(submission_part)):\n            video_id = submission_part.video_id.iloc[i]\n            agent_id = submission_part.agent_id.iloc[i]\n            target_id = submission_part.target_id.iloc[i]\n            if i < len(stop_video_id) and (stop_video_id[i] != video_id or stop_agent_id[i] != agent_id or stop_target_id[i] != target_id):\n                new_stop_frame = meta.query(\"(video_id == @video_id)\").video_frame.max() + 1\n                submission_part.iat[i, submission_part.columns.get_loc('stop_frame')] = new_stop_frame\n        return submission_part\n\nclass MABePipeline:\n    def __init__(self, config: PipelineConfig):\n        self.config = config\n        self.data_generator = BehaviorDataGenerator(config)\n        self.classifier = BehaviorClassifier(config)\n        self.transformer = BehaviorFeatureTransformer()\n        self.train_df = None\n        self.test_df = None\n        self.body_parts_list = None\n    def load_data(self):\n        print(\"📂 Loading data...\")\n        self.train_df = pd.read_csv(self.config.train_path)\n        self.test_df = pd.read_csv(self.config.test_path)\n        self.train_df['n_mice'] = 4 - self.train_df[\n            ['mouse1_strain', 'mouse2_strain', 'mouse3_strain', 'mouse4_strain']\n        ].isna().sum(axis=1)\n        self.train_df_filtered = self.train_df.query(\"~lab_id.str.startswith('MABe22_')\")\n        self.body_parts_list = list(np.unique(self.train_df.body_parts_tracked))\n        print(f\"✓ Loaded {len(self.train_df)} training videos\")\n        print(f\"✓ Loaded {len(self.test_df)} test videos\")\n        print(f\"✓ Found {len(self.body_parts_list)} unique body part configurations\\n\")\n    def transform_single(self, single_mouse: pd.DataFrame, body_parts: List[str], fps: float) -> pd.DataFrame:\n        available_body_parts = single_mouse.columns.get_level_values(0)\n        X = pd.DataFrame({\n            f\"{p1}+{p2}\": np.square(single_mouse[p1] - single_mouse[p2]).sum(axis=1, skipna=False)\n            for p1, p2 in itertools.combinations(body_parts, 2)\n            if p1 in available_body_parts and p2 in available_body_parts\n        })\n        if 'body_center' in available_body_parts:\n            cx = single_mouse['body_center']['x']\n            cy = single_mouse['body_center']['y']\n            for w in [5, 15, 30, 60]:\n                ws = self.transformer._scale(w, fps)\n                roll = dict(min_periods=1, center=True)\n                X[f'cx_m{w}'] = cx.rolling(ws, **roll).mean()\n                X[f'cy_m{w}'] = cy.rolling(ws, **roll).mean()\n                X[f'cx_s{w}'] = cx.rolling(ws, **roll).std()\n                X[f'cy_s{w}'] = cy.rolling(ws, **roll).std()\n                X[f'x_rng{w}'] = cx.rolling(ws, **roll).max() - cx.rolling(ws, **roll).min()\n                X[f'y_rng{w}'] = cy.rolling(ws, **roll).max() - cy.rolling(ws, **roll).min()\n                X[f'disp{w}'] = np.sqrt(cx.diff().rolling(ws, min_periods=1).sum()**2 +\n                                         cy.diff().rolling(ws, min_periods=1).sum()**2)\n                X[f'act{w}'] = np.sqrt(cx.diff().rolling(ws, min_periods=1).var() +\n                                       cy.diff().rolling(ws, min_periods=1).var())\n            X = self.transformer.add_curvature_features(X, cx, cy, fps)\n            X = self.transformer.add_multiscale_features(X, cx, cy, fps)\n            X = self.transformer.add_state_features(X, cx, cy, fps)\n            X = self.transformer.add_longrange_features(X, cx, cy, fps)\n        return X.astype(np.float32, copy=False)\n    def transform_pair(self, mouse_pair: Dict, body_parts: List[str], fps: float) -> pd.DataFrame:\n        avail_A = mouse_pair['A'].columns.get_level_values(0)\n        avail_B = mouse_pair['B'].columns.get_level_values(0)\n        X = pd.DataFrame({\n            f\"12+{p1}+{p2}\": np.square(mouse_pair['A'][p1] - mouse_pair['B'][p2]).sum(axis=1, skipna=False)\n            for p1, p2 in itertools.product(body_parts, repeat=2)\n            if p1 in avail_A and p2 in avail_B\n        })\n        if 'body_center' in avail_A and 'body_center' in avail_B:\n            cd_full = np.square(mouse_pair['A']['body_center'] - mouse_pair['B']['body_center']).sum(axis=1, skipna=False)\n            for w in [5, 15, 30, 60]:\n                ws = self.transformer._scale(w, fps)\n                roll = dict(min_periods=1, center=True)\n                X[f'd_m{w}'] = cd_full.rolling(ws, **roll).mean()\n                X[f'd_s{w}'] = cd_full.rolling(ws, **roll).std()\n            X = self.transformer.add_interaction_features(X, mouse_pair, avail_A, avail_B, fps)\n        return X.astype(np.float32, copy=False)\n    def process_section(self, section: int, body_parts_str: str):\n        print(f\"\\n{'='*70}\")\n        print(f\"Section {section}/{len(self.body_parts_list)-1}\")\n        print(f\"{'='*70}\\n\")\n        body_parts = json.loads(body_parts_str)\n        if len(body_parts) > 5:\n            body_parts = [b for b in body_parts if b not in self.transformer.DROP_BODY_PARTS]\n        train_subset = self.train_df[self.train_df.body_parts_tracked == body_parts_str]\n        fps_lookup = (\n            train_subset[['video_id', 'frames_per_second']]\n            .drop_duplicates('video_id')\n            .set_index('video_id')['frames_per_second']\n            .to_dict()\n        )\n        submission_list = []\n        f1_list = []\n        for subset_type in ['single', 'pair']:\n            generate_single = (subset_type == 'single')\n            generate_pair = (subset_type == 'pair')\n            data_list = []\n            meta_list = []\n            label_list = []\n            for switch, data, meta, label in tqdm(\n                list(self.data_generator.generate_mouse_data(\n                train_subset, 'train', generate_single=generate_single, generate_pair=generate_pair)),\n                desc=f\"Data gen [{subset_type}]\", leave=False):\n                if switch == subset_type:\n                    data_list.append(data)\n                    meta_list.append(meta)\n                    label_list.append(label)\n            gc.collect()\n            if len(data_list) > 0:\n                feats_list = []\n                for data_i, meta_i in tqdm(zip(data_list, meta_list), total=len(data_list),\n                                           desc=f\"Transform {subset_type} features\", leave=False):\n                    fps_i = self.transformer._fps_from_meta(meta_i, fps_lookup, default_fps=30.0)\n                    if subset_type == 'single':\n                        X_i = self.transform_single(data_i, body_parts, fps_i)\n                    else:\n                        X_i = self.transform_pair(data_i, body_parts, fps_i)\n                    feats_list.append(X_i)\n                    del X_i, fps_i\n                gc.collect()\n                X_tr = pd.concat(feats_list, axis=0, ignore_index=True)\n                label_tr = pd.concat(label_list, axis=0, ignore_index=True)\n                meta_tr = pd.concat(meta_list, axis=0, ignore_index=True)\n                del feats_list, data_list, label_list, meta_list\n                gc.collect()\n                print(f\"\\n🎯 {subset_type.upper()} MICE:\")\n                temp_submission, temp_f1, _ = self.classifier.cross_validate_with_randomsearch(\n                    X_tr, label_tr, meta_tr, body_parts_str, section\n                )\n                submission_list.extend(temp_submission)\n                f1_list.extend(temp_f1)\n                del X_tr, label_tr, meta_tr\n                gc.collect()\n        return submission_list, f1_list\n    def run(self):\n        print('Starting pipeline run...')\n        self.load_data()\n        all_submissions = []\n        all_f1_scores = []\n        for section in tqdm(range(1, len(self.body_parts_list)), desc=\"Pipeline section\", leave=True):\n            body_parts_str = self.body_parts_list[section]\n            try:\n                submissions, f1_scores = self.process_section(section, body_parts_str)\n                all_submissions.extend(submissions)\n                all_f1_scores.extend(f1_scores)\n            except Exception as e:\n                print(f\"\\n⚠️  Error in section {section}: {str(e)}\\n\")\n        if len(all_submissions) > 0:\n            final_submission = pd.concat(all_submissions, ignore_index=True)\n        else:\n            final_submission = pd.DataFrame()\n        print('Pipeline run completed!')\n        return final_submission, all_f1_scores\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:18:12.260823Z","iopub.execute_input":"2025-11-10T07:18:12.261165Z","iopub.status.idle":"2025-11-10T07:18:14.319095Z","shell.execute_reply.started":"2025-11-10T07:18:12.26114Z","shell.execute_reply":"2025-11-10T07:18:14.318089Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ============= USAGE IN NOTEBOOK =============\n\n# Initialize configuration\nconfig = PipelineConfig(\n    train_path=\"/kaggle/input/MABe-mouse-behavior-detection/train.csv\",\n    test_path=\"/kaggle/input/MABe-mouse-behavior-detection/test.csv\",\n    train_annotation_path=\"/kaggle/input/MABe-mouse-behavior-detection/train_annotation\",\n    train_tracking_path=\"/kaggle/input/MABe-mouse-behavior-detection/train_tracking\",\n    test_tracking_path=\"/kaggle/input/MABe-mouse-behavior-detection/test_tracking\",\n    model_path=\"/kaggle/input/social-action-recognition-in-mice-xgb-catboost\",\n    model_name=\"xgboost\",\n    mode=\"submit\",  # Change to \"validate\" for validation mode\n    n_splits=3,\n    n_iter_search=50,\n    optuna_trials=1000\n)\n\n# Create and run pipeline\npipeline = MABePipeline(config)\nfinal_submission, f1_scores = pipeline.run()\n\n# Save results\nif len(final_submission) > 0:\n    final_submission.index.name = 'row_id'\n    final_submission.to_csv('submission.csv')\n    print(f\"\\n✓ Submission saved with {len(final_submission)} predictions\")\n\nif len(f1_scores) > 0:\n    f1_df = pd.DataFrame(f1_scores, columns=['body_parts_tracked_str', 'action', 'binary_f1_score'])\n    print(f\"\\n✓ Mean F1 Score: {f1_df['binary_f1_score'].mean():.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:18:14.32088Z","iopub.execute_input":"2025-11-10T07:18:14.321297Z","execution_failed":"2025-11-10T14:14:50.46Z"}},"outputs":[],"execution_count":null}]}