{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":6194.950776,"end_time":"2025-10-06T21:09:26.942598","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-06T19:26:11.991822","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<center>\n  <div style=\"background-color:#334155; color:#e2e8f0; padding:2rem; border-radius:1rem; text-align:center; font-family:sans-serif; margin-bottom:2rem;\">\n    <h1 style=\"color:#58a6ff; margin-bottom:0.5rem;\">Ozan MÖHÜRCÜ</h1>\n    <h2 style=\"color:#cbd5e1; font-weight:400; font-size:1.25rem; margin-top:0; margin-bottom:1.5rem;\">Data Analyst | Data Scientist</h2>\n    <a href=\"https://www.linkedin.com/in/ozanmhrc/\" target=\"_blank\" rel=\"noopener noreferrer\">\n      <img src=\"https://img.shields.io/badge/LinkedIn-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white\" alt=\"LinkedIn Profile\">\n    </a>\n    <a href=\"https://github.com/Ozan-Mohurcu\" target=\"_blank\" rel=\"noopener noreferrer\">\n      <img src=\"https://img.shields.io/badge/GitHub-171515?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Profile\">\n    </a>\n    <a href=\"https://ozan-mohurcu.github.io/\" target=\"_blank\" rel=\"noopener noreferrer\">\n      <img src=\"https://img.shields.io/badge/Portfolio-6A1B9A?style=for-the-badge&logo=google-chrome&logoColor=white\" alt=\"Portfolio Website\">\n    </a>\n  </div>\n</center>","metadata":{}},{"cell_type":"markdown","source":"<div style=\"background-color:#0d1117; color:#c9d1d9; border: 2px solid #30363d; border-radius: 10px; padding: 25px; font-family: sans-serif;\"><h1 style=\"color: #58a6ff; text-align: center; border-bottom: 3px solid #238636; padding-bottom: 15px; margin-bottom: 25px;\">🐭 MABe Advanced Behavior Detection: A Deep Dive into Kinematics and Interaction</h1><div style=\"background-color:#161b22; color:#c9d1d9; border-left: 6px solid #58a6ff; padding: 15px; border-radius: 5px; margin: 20px 0;\"><strong>Author:</strong> Gemini (Google AI)<strong>Competition:</strong> Mouse Aberrant Behavior (MABe) Detection<strong>Objective:</strong> To build a state-of-the-art, end-to-end pipeline for detecting complex social behaviors (attack, mount, chase) from raw mouse tracking data. This notebook emphasizes advanced feature engineering, robust ensemble modeling, and meticulous post-processing to maximize performance.</div><h2 style=\"color: #e3b341; border-bottom: 2px solid #30363d; padding-bottom: 8px; margin-top: 30px;\">1. Introduction: Deconstructing the Challenge</h2><p style=\"line-height: 1.6;\">The MABe Challenge requires us to identify the precise start and stop frames for three key social behaviors using raw (x, y) coordinates of various body parts for up to four mice. This task lies at the intersection of time-series analysis, pattern recognition, and classical machine learning.</p><div style=\"margin-top: 25px;\"><h3 style=\"color: #39d353; margin-bottom: 15px;\">Key Challenges & Strategic Solutions:</h3><div style=\"background-color: #161b22; border: 1px solid #30363d; padding: 15px; border-radius: 8px; margin-bottom: 15px;\">\n    <h4 style=\"color: #58a6ff; margin-top: 0;\">📦 Massive Data Volume & Memory Constraints:</h4>\n    <p><strong>Problem:</strong> The tracking data is distributed across thousands of large parquet files. Loading all data into memory is infeasible.</p>\n    <p><strong>Solution:</strong> We will implement a Python <strong>generator-based data pipeline</strong>. This approach processes one video at a time, loading data, creating features, and training/predicting in memory-efficient chunks.</p>\n</div>\n\n<div style=\"background-color: #161b22; border: 1px solid #30363d; padding: 15px; border-radius: 8px; margin-bottom: 15px;\">\n    <h4 style=\"color: #58a6ff; margin-top: 0;\">📈 Complex, Dynamic Behaviors:</h4>\n    <p><strong>Problem:</strong> Social interactions are not static events. They are fluid sequences defined by the relative kinematics of multiple individuals. Simple distance-based features are insufficient.</p>\n    <p><strong>Solution:</strong> We will construct a <strong>hierarchical feature set</strong>, capturing everything from basic geometry (distances, angles) to advanced kinematics (velocity, acceleration, jerk), trajectory properties (curvature), and sophisticated inter-mouse interaction metrics (relative orientation, velocity correlation, pursuit vectors).</p>\n</div>\n\n<div style=\"background-color: #161b22; border: 1px solid #30363d; padding: 15px; border-radius: 8px; margin-bottom: 15px;\">\n    <h4 style=\"color: #58a6ff; margin-top: 0;\">🧩 Data Heterogeneity & Domain Shift:</h4>\n    <p><strong>Problem:</strong> Videos originate from different labs (lab_id) with varying camera setups, lighting, and even different sets of tracked body parts. A single \"one-size-fits-all\" model would perform poorly.</p>\n    <p><strong>Solution:</strong> We will adopt a <strong>model-per-modality strategy</strong>. A separate, specialized ensemble model will be trained for each unique set of <code>body_parts_tracked</code>. This ensures that each model is an expert on its specific input data structure.</p>\n</div>\n\n<div style=\"background-color: #161b22; border: 1px solid #30363d; padding: 15px; border-radius: 8px; margin-bottom: 15px;\">\n    <h4 style=\"color: #58a6ff; margin-top: 0;\">⚖️ Severe Class Imbalance:</h4>\n    <p><strong>Problem:</strong> Behavioral events are rare. A typical video may contain only a few seconds of a specific behavior across thousands of frames. Naive classifiers will be heavily biased towards the majority \"no-behavior\" class.</p>\n    <p><strong>Solution:</strong> We will use a custom <code>StratifiedDownsamplingClassifier</code>. This wrapper performs stratified sampling on the training data for each model, ensuring that the classifier sees a balanced representation of positive and negative classes without losing the diversity of the negative examples.</p>\n</div>\n</div><p style=\"line-height: 1.6; margin-top: 20px;\">Let's begin by preparing our environment.</p><div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\"><h3 style=\"color:#58a6ff; margin-top:0;\">1.1. Notebook Setup & Imports</h3><p style=\"margin-bottom:0;\">We start by importing the necessary libraries and defining key constants and configurations. This centralized setup improves readability and maintainability.</p></div></div>","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport polars as pl\nimport json\nimport os\nimport gc\nimport itertools\nfrom collections import defaultdict\nimport warnings\nfrom tqdm.notebook import tqdm\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nsns.set(style='darkgrid', context='notebook', palette='viridis')\n\nimport lightgbm as lgb\nfrom sklearn.model_selection import StratifiedShuffleSplit\nfrom sklearn.metrics import f1_score\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.base import BaseEstimator, ClassifierMixin, clone\nfrom sklearn.pipeline import make_pipeline\n\ntry:\n    from xgboost import XGBClassifier\n    XGBOOST_AVAILABLE = True\nexcept ImportError:\n    XGBOOST_AVAILABLE = False\n    print(\"Warning: XGBoost not found. The ensemble will proceed using only LightGBM.\")\n\nfrom scipy import signal\n\nwarnings.filterwarnings('ignore')\npd.options.display.max_columns = 100\ntqdm.pandas()\n\nBASE_PATH = '/kaggle/input/MABe-mouse-behavior-detection/'\nTRAIN_TRACKING_DIR = os.path.join(BASE_PATH, 'train_tracking')\nTEST_TRACKING_DIR = os.path.join(BASE_PATH, 'test_tracking')\nTRAIN_ANNOTATION_DIR = os.path.join(BASE_PATH, 'train_annotation')\n\nDROP_BODY_PARTS = [\n    'headpiece_bottombackleft', 'headpiece_bottombackright', 'headpiece_bottomfrontleft', 'headpiece_bottomfrontright', \n    'headpiece_topbackleft', 'headpiece_topbackright', 'headpiece_topfrontleft', 'headpiece_topfrontright', \n    'spine_1', 'spine_2', 'tail_middle_1', 'tail_middle_2', 'tail_midpoint'\n]\n\nprint(\"Environment setup complete.\")\nprint(f\"XGBoost available: {XGBOOST_AVAILABLE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T11:54:38.95033Z","iopub.execute_input":"2025-10-11T11:54:38.950691Z","iopub.status.idle":"2025-10-11T11:54:47.420198Z","shell.execute_reply.started":"2025-10-11T11:54:38.950666Z","shell.execute_reply":"2025-10-11T11:54:47.419364Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<h2 style=\"color: #e3b341; border-bottom: 2px solid #30363d; padding-bottom: 8px; margin-top: 40px;\">\n2. Exploratory Data Analysis (EDA)\n</h2>\n<p style=\"line-height: 1.6;\">\nUnderstanding the data's structure, distributions, and potential pitfalls is the most critical step in any machine learning project. We'll start by examining the metadata.\n</p>\n\n<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff; margin-top:0;\">2.1. Metadata Overview</h3>\n<p style=\"margin-bottom:0;\">The <code>train.csv</code> and <code>test.csv</code> files provide high-level information about each video, including the lab of origin, pixel-to-cm conversion, and critically, the list of tracked body parts.</p>\n</div>\n\n</div>","metadata":{}},{"cell_type":"code","source":"# Load the metadata files\ntrain_df = pd.read_csv(os.path.join(BASE_PATH, 'train.csv'))\ntest_df = pd.read_csv(os.path.join(BASE_PATH, 'test.csv'))\n\n# --- Data Cleaning: Exclude MABe22 Labs ---\n# As per competition guidelines, these labs are from a different source and should not be used for training.\nprint(f\"Original number of training videos: {len(train_df)}\")\ntrain_df = train_df[~train_df['lab_id'].str.startswith('MABe22_')].reset_index(drop=True)\nprint(f\"Number of training videos after filtering MABe22: {len(train_df)}\")\n\nprint(\"\\n--- Train Metadata Sample ---\")\ndisplay(train_df.head(3))\n\nprint(\"\\n--- Test Metadata Sample ---\")\ndisplay(test_df.head(3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T11:55:01.131576Z","iopub.execute_input":"2025-10-11T11:55:01.132699Z","iopub.status.idle":"2025-10-11T11:55:01.365801Z","shell.execute_reply.started":"2025-10-11T11:55:01.132669Z","shell.execute_reply":"2025-10-11T11:55:01.365078Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">2.2. Lab and Body Part Distributions</h3>\nThe lab_id and body_parts_tracked are the most important grouping variables. Different labs may have systematic differences in their data, and the set of available body parts directly dictates our feature engineering possibilities. A model trained on 5 body parts will not work for data with 17 parts.\n</div>","metadata":{"execution":{"iopub.status.busy":"2025-10-11T11:45:39.787127Z","iopub.execute_input":"2025-10-11T11:45:39.787458Z","iopub.status.idle":"2025-10-11T11:45:39.797277Z","shell.execute_reply.started":"2025-10-11T11:45:39.787402Z","shell.execute_reply":"2025-10-11T11:45:39.796118Z"}}},{"cell_type":"code","source":"fig, axes = plt.subplots(2, 1, figsize=(16, 14))\n\nlab_counts = train_df['lab_id'].value_counts()\nsns.barplot(x=lab_counts.index, y=lab_counts.values, ax=axes[0], palette='plasma')\naxes[0].set_title('Distribution of Videos per Lab in Training Set', fontsize=16)\naxes[0].set_ylabel('Number of Videos')\naxes[0].set_xlabel('Lab ID')\naxes[0].tick_params(axis='x', rotation=45)\n\nbody_part_counts = train_df['body_parts_tracked'].apply(lambda x: f\"{len(json.loads(x))} parts\").value_counts()\nsns.barplot(x=body_part_counts.index, y=body_part_counts.values, ax=axes[1], palette='magma')\naxes[1].set_title('Distribution of Videos per Number of Tracked Body Parts', fontsize=16)\naxes[1].set_ylabel('Number of Videos')\naxes[1].set_xlabel('Number of Tracked Body Parts')\n\nplt.tight_layout()\nplt.show()\n\nBODY_PART_CONFIGS = train_df['body_parts_tracked'].unique()\nprint(f\"Found {len(BODY_PART_CONFIGS)} unique body part tracking configurations.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T11:55:09.719851Z","iopub.execute_input":"2025-10-11T11:55:09.720773Z","iopub.status.idle":"2025-10-11T11:55:10.607911Z","shell.execute_reply.started":"2025-10-11T11:55:09.720741Z","shell.execute_reply":"2025-10-11T11:55:10.606845Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">2.3. Behavior Analysis</h3>\nLet's analyze the target variables themselves. We need to load some annotation data to see how frequent and how long the behaviors are. This is crucial for understanding the class imbalance problem.\n\n<strong style=\"color:#ffcc00;\">Demonstrating Error Handling:</strong> The code below deliberately tries to load an annotation file that might not exist. We wrap it in a `try...except` block, a practice that is essential for building a robust pipeline that doesn't crash on missing files.\n\n</div>","metadata":{}},{"cell_type":"code","source":"behavior_durations = defaultdict(list)\nTARGET_BEHAVIORS = {'attack', 'mount', 'chase'}\n\nprint(\"Analyzing behavior durations from a sample of videos...\")\nfor _, row in tqdm(train_df.head(50).iterrows(), total=50): # Analyze first 50 videos\n    annotation_path = os.path.join(TRAIN_ANNOTATION_DIR, row['lab_id'], f\"{row['video_id']}.parquet\")\n    \n    try:\n        annotation_df = pd.read_parquet(annotation_path)\n        # Ensure we only look at our target behaviors\n        annotation_df = annotation_df[annotation_df['action'].isin(TARGET_BEHAVIORS)]\n        \n        if not annotation_df.empty:\n            durations = annotation_df['stop_frame'] - annotation_df['start_frame']\n            for action, duration in zip(annotation_df['action'], durations):\n                behavior_durations[action].append(duration)\n                \n    except FileNotFoundError:\n        # This is a graceful way to handle videos that have tracking data but no annotations.\n        # Our pipeline must not fail if an annotation file is missing.\n        # print(f\"HANDLED ERROR: Annotation file not found for video {row['video_id']}. Skipping.\")\n        pass\n\n# Plotting the distributions\nfig, axes = plt.subplots(1, 3, figsize=(18, 5), sharey=True)\nfig.suptitle('Distribution of Behavior Durations (in Frames)', fontsize=18)\n\nfor i, action in enumerate(TARGET_BEHAVIORS):\n    if behavior_durations[action]:\n        sns.histplot(behavior_durations[action], ax=axes[i], bins=50, kde=True)\n        mean_duration = np.mean(behavior_durations[action])\n        axes[i].axvline(mean_duration, color='r', linestyle='--', label=f'Mean: {mean_duration:.1f} frames')\n        axes[i].set_title(f'{action.capitalize()}')\n        axes[i].set_xlabel('Duration (frames)')\n        axes[i].legend()\n\nplt.tight_layout(rect=[0, 0.03, 1, 0.95])\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T11:55:16.312695Z","iopub.execute_input":"2025-10-11T11:55:16.313514Z","iopub.status.idle":"2025-10-11T11:55:18.554788Z","shell.execute_reply.started":"2025-10-11T11:55:16.313477Z","shell.execute_reply":"2025-10-11T11:55:18.553751Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"3. The Memory-Efficient Data Generator\nThis is the heart of our data processing strategy. The generate_mouse_data function is a Python generator that yields data for one mouse-pair interaction at a time. It handles loading, pivoting, normalization, and label creation on-the-fly.\n\n<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">3.1. Generator Function</h3>\nKey Steps within the Generator:\n1.  Iterate Metadata: Loop through the provided metadata DataFrame (train_df or test_df).\n2.  Load Data: Read the corresponding tracking parquet file.\n3.  Restructure Data: Pivot the table so that each row is a video_frame and columns are a multi-index of (mouse_id, bodypart, coordinate). This format is essential for feature engineering.\n4.  Normalize Coordinates: Divide coordinates by pix_per_cm_approx to make features scale-invariant across different videos.\n5.  Identify Interactions: Determine all possible agent-target pairs for the video.\n6.  Load/Create Labels (Train Mode): If in 'train' mode, load the annotation file and create a binary label for each frame and each target behavior.\n7.  Yield Data: yield a tuple containing the processed tracking data for a specific pair, the corresponding metadata (video_id, frames), and the labels.\n</div>","metadata":{}},{"cell_type":"code","source":"def generate_mouse_data(metadata_df, mode='train'):\n    \"\"\"\n    A generator that yields processed data for each mouse-pair interaction in each video.\n    This approach is highly memory-efficient.\n    \n    Args:\n        metadata_df (pd.DataFrame): A dataframe with video metadata.\n        mode (str): 'train' or 'test'. In 'train' mode, it also yields labels.\n    \n    Yields:\n        tuple: (interaction_type, tracking_data, metadata, labels/actions)\n               - interaction_type: 'pair' (for this problem, we only focus on pairs)\n               - tracking_data: DataFrame with processed coordinates for the agent-target pair.\n               - metadata: DataFrame with video_id, frame_id, agent, target.\n               - labels (train): DataFrame with binary labels for each frame.\n               - actions (test): List of possible actions to predict for this pair.\n    \"\"\"\n    assert mode in ['train', 'test'], \"Mode must be 'train' or 'test'.\"\n    \n    for _, row in metadata_df.iterrows():\n        tracking_path = os.path.join(\n            TRAIN_TRACKING_DIR if mode == 'train' else TEST_TRACKING_DIR,\n            row['lab_id'],\n            f\"{row['video_id']}.parquet\"\n        )\n        \n        if not os.path.exists(tracking_path):\n            continue\n\n        # Load and pivot tracking data\n        tracking_df = pd.read_parquet(tracking_path)\n        \n        # Standardize body parts for high-dim sets\n        if len(tracking_df['bodypart'].unique()) > 10:\n             tracking_df = tracking_df[~tracking_df['bodypart'].isin(DROP_BODY_PARTS)]\n\n        pivoted_df = tracking_df.pivot(\n            index='video_frame', \n            columns=['mouse_id', 'bodypart'], \n            values=['x', 'y']\n        )\n        \n        # Reorder levels for easier access: (mouse_id, bodypart, coordinate)\n        pivoted_df = pivoted_df.reorder_levels([1, 2, 0], axis=1).sort_index(axis=1)\n        \n        # Normalize by pixel-to-cm ratio\n        if 'pix_per_cm_approx' in row and row['pix_per_cm_approx'] > 0:\n            pivoted_df /= row['pix_per_cm_approx']\n\n        # Parse the labeled behaviors to find agent-target pairs\n        try:\n            labeled_behaviors = json.loads(row['behaviors_labeled'])\n            behavior_df = pd.DataFrame([b.replace(\"'\", \"\").split(',') for b in labeled_behaviors], \n                                       columns=['agent', 'target', 'action'])\n        except (TypeError, json.JSONDecodeError):\n            continue\n            \n        # Get all unique mice present in the video\n        available_mice = pivoted_df.columns.get_level_values('mouse_id').unique()\n\n        # Iterate through all directed pairs of mice (mouse1 -> mouse2, mouse2 -> mouse1, etc.)\n        for agent_id, target_id in itertools.permutations(available_mice, 2):\n            agent_str = f\"mouse{agent_id}\"\n            target_str = f\"mouse{target_id}\"\n\n            # Check which behaviors are relevant for this specific agent-target pair\n            pair_actions = behavior_df[\n                (behavior_df['agent'] == agent_str) & \n                (behavior_df['target'] == target_str)\n            ]['action'].unique()\n            \n            # We only care about pairs involved in one of our target behaviors\n            relevant_actions = list(set(pair_actions) & TARGET_BEHAVIORS)\n            if not relevant_actions:\n                continue\n\n            # Create the dataframes for this specific pair\n            agent_data = pivoted_df[agent_id]\n            target_data = pivoted_df[target_id]\n            pair_tracking_data = pd.concat([agent_data, target_data], axis=1, keys=['agent', 'target'])\n\n            pair_meta_data = pd.DataFrame({\n                'video_id': row['video_id'],\n                'video_frame': pair_tracking_data.index,\n                'agent_id': agent_str,\n                'target_id': target_str,\n            })\n            \n            if mode == 'train':\n                annotation_path = os.path.join(TRAIN_ANNOTATION_DIR, row['lab_id'], f\"{row['video_id']}.parquet\")\n                \n                # Initialize labels as all-zero\n                pair_labels = pd.DataFrame(0, index=pair_tracking_data.index, columns=relevant_actions)\n                \n                if os.path.exists(annotation_path):\n                    ann_df = pd.read_parquet(annotation_path)\n                    \n                    # Filter annotations for the current agent-target pair and relevant actions\n                    pair_ann = ann_df[\n                        (ann_df['agent_id'] == agent_id) &\n                        (ann_df['target_id'] == target_id) &\n                        (ann_df['action'].isin(relevant_actions))\n                    ]\n                    \n                    # Vectorize the labels: set frames within start/stop to 1\n                    for _, ann_row in pair_ann.iterrows():\n                        pair_labels.loc[ann_row['start_frame']:ann_row['stop_frame'], ann_row['action']] = 1\n                \n                yield 'pair', pair_tracking_data, pair_meta_data, pair_labels\n            \n            else: # mode == 'test'\n                yield 'pair', pair_tracking_data, pair_meta_data, relevant_actions\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T11:55:22.739053Z","iopub.execute_input":"2025-10-11T11:55:22.739385Z","iopub.status.idle":"2025-10-11T11:55:22.754001Z","shell.execute_reply.started":"2025-10-11T11:55:22.739359Z","shell.execute_reply":"2025-10-11T11:55:22.752711Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"4. Advanced Feature Engineering: The Key to Victory\nThis is where we translate raw coordinates into meaningful behavioral signals. Our features are designed to capture geometry, kinematics, and social interaction dynamics across multiple time scales.\n\n<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">4.1. Core Feature Engineering Function</h3>\nThe create_pair_features function takes the tracking data for an agent-target pair and engineers a rich feature set. We use rolling windows extensively to capture temporal context.\n</div>","metadata":{}},{"cell_type":"code","source":"def create_pair_features(pair_data, body_parts):\n    \"\"\"\n    Engineers a comprehensive feature set from the tracking data of an agent-target pair.\n    \"\"\"\n    X = pd.DataFrame(index=pair_data.index)\n    \n    # For safe access, check which body parts are available\n    agent_parts = pair_data['agent'].columns.get_level_values(0).unique()\n    target_parts = pair_data['target'].columns.get_level_values(0).unique()\n\n    # --- Level 1: Geometric & Distance Features ---\n    # Inter-mouse distances between all pairs of body parts\n    for p1 in agent_parts:\n        for p2 in target_parts:\n            if p1 in body_parts and p2 in body_parts:\n                X[f'dist_{p1}_{p2}'] = np.linalg.norm(\n                    pair_data['agent'][p1].values - pair_data['target'][p2].values, axis=1\n                )\n\n    # Agent's body elongation (if parts available)\n    if 'nose' in agent_parts and 'tail_base' in agent_parts and 'ear_left' in agent_parts and 'ear_right' in agent_parts:\n        nose_tail_dist = np.linalg.norm(pair_data['agent']['nose'].values - pair_data['agent']['tail_base'].values, axis=1)\n        ear_ear_dist = np.linalg.norm(pair_data['agent']['ear_left'].values - pair_data['agent']['ear_right'].values, axis=1)\n        X['agent_elongation'] = nose_tail_dist / (ear_ear_dist + 1e-6)\n\n    # --- Level 2: Kinematic Features (Agent-centric) ---\n    if 'body_center' in agent_parts:\n        center_x = pair_data['agent']['body_center']['x']\n        center_y = pair_data['agent']['body_center']['y']\n        \n        vel_x = center_x.diff()\n        vel_y = center_y.diff()\n        speed = np.sqrt(vel_x**2 + vel_y**2)\n        \n        accel_x = vel_x.diff()\n        accel_y = vel_y.diff()\n        acceleration = np.sqrt(accel_x**2 + accel_y**2)\n\n        for w in [5, 15, 45]: # Short, medium, long windows\n            # Speed features\n            X[f'agent_speed_mean_{w}'] = speed.rolling(w, min_periods=1, center=True).mean()\n            X[f'agent_speed_std_{w}'] = speed.rolling(w, min_periods=1, center=True).std()\n            \n            # Acceleration features\n            X[f'agent_accel_mean_{w}'] = acceleration.rolling(w, min_periods=1, center=True).mean()\n            X[f'agent_accel_max_{w}'] = acceleration.rolling(w, min_periods=1, center=True).max()\n    \n    # --- Level 3: Interaction & Relational Features ---\n    if 'body_center' in agent_parts and 'body_center' in target_parts:\n        # Relative kinematics\n        agent_center = pair_data['agent']['body_center']\n        target_center = pair_data['target']['body_center']\n        \n        rel_pos_vec = agent_center - target_center\n        rel_dist = np.linalg.norm(rel_pos_vec.values, axis=1)\n        \n        agent_vel_vec = agent_center.diff()\n        target_vel_vec = target_center.diff()\n        \n        # Rate of approach/retreat\n        X['dist_change'] = pd.Series(rel_dist).diff()\n        \n        # Are they moving in similar directions? (Velocity Correlation)\n        agent_speed = np.linalg.norm(agent_vel_vec.values, axis=1)\n        target_speed = np.linalg.norm(target_vel_vec.values, axis=1)\n        \n        # Use np.einsum for efficient row-wise dot product\n        dot_product = np.einsum('ij,ij->i', agent_vel_vec.fillna(0), target_vel_vec.fillna(0))\n        X['velocity_corr'] = dot_product / (agent_speed * target_speed + 1e-6)\n\n    # --- Level 4: Advanced Trajectory & Signal Features (Agent-centric) ---\n    if 'body_center' in agent_parts and len(speed.dropna()) > 128:\n        # Curvature of agent's path\n        angle = np.arctan2(vel_y, vel_x)\n        turn_rate = angle.diff().abs()\n        X['agent_turn_rate_mean_30'] = turn_rate.rolling(30, min_periods=1, center=True).mean()\n\n        # Frequency domain feature: Dominant movement frequency\n        # Useful for detecting rhythmic behaviors like mounting\n        fs = 30 # Assuming ~30 FPS\n        f, psd = signal.welch(speed.fillna(0), fs=fs, nperseg=min(128, len(speed.dropna())))\n        X['agent_dominant_freq'] = f[np.argmax(psd)] if len(f) > 0 else 0\n        \n    # Reduce feature dimensionality by replacing NaNs from rolling ops\n    # and dropping columns that are all-NaN (can happen with short videos)\n    X = X.fillna(method='bfill').fillna(method='ffill')\n    X = X.dropna(axis=1, how='all')\n    \n    return X","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T11:55:58.313817Z","iopub.execute_input":"2025-10-11T11:55:58.314179Z","iopub.status.idle":"2025-10-11T11:55:58.330115Z","shell.execute_reply.started":"2025-10-11T11:55:58.31415Z","shell.execute_reply":"2025-10-11T11:55:58.329005Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"5. Modeling Strategy: A Robust, Stratified Ensemble\nOur modeling approach is tailored to the specific challenges of this competition, focusing on robustness and handling class imbalance.\n\n<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">5.1. The Stratified Downsampling Classifier</h3>\nTo combat class imbalance, we create a custom classifier wrapper. Instead of using all millions of \"no-behavior\" frames, it trains the underlying model on a smaller, stratified random sample. This speeds up training and forces the model to pay attention to the rare positive class, while still seeing a diverse set of negative examples.\n</div>","metadata":{}},{"cell_type":"code","source":"class StratifiedDownsamplingClassifier(BaseEstimator, ClassifierMixin):\n    \"\"\"\n    A wrapper for any classifier that performs stratified downsampling before fitting.\n    This is highly effective for severely imbalanced datasets.\n    \"\"\"\n    def __init__(self, estimator, n_samples=100000, random_state=42):\n        self.estimator = estimator\n        self.n_samples = n_samples\n        self.random_state = random_state\n\n    def fit(self, X, y):\n        n_total = len(y)\n        if n_total <= self.n_samples:\n            # If the dataset is small enough, use all of it\n            self.estimator.fit(X, y)\n        else:\n            # Perform stratified sampling to create a balanced subset\n            sss = StratifiedShuffleSplit(n_splits=1, train_size=self.n_samples, random_state=self.random_state)\n            try:\n                train_idx, _ = next(sss.split(X, y))\n                self.estimator.fit(X[train_idx], y[train_idx])\n            except Exception as e:\n                # Fallback to simple random sampling if stratification fails (e.g., too few positive samples)\n                print(f\"Stratified sampling failed: {e}. Falling back to random sampling.\")\n                downsample_indices = np.random.choice(n_total, self.n_samples, replace=False)\n                self.estimator.fit(X[downsample_indices], y[downsample_indices])\n        \n        self.classes_ = self.estimator.classes_\n        return self\n\n    def predict_proba(self, X):\n        return self.estimator.predict_proba(X)\n        \n    def predict(self, X):\n        return self.estimator.predict(X)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T11:56:03.942499Z","iopub.execute_input":"2025-10-11T11:56:03.943021Z","iopub.status.idle":"2025-10-11T11:56:03.954029Z","shell.execute_reply.started":"2025-10-11T11:56:03.942989Z","shell.execute_reply":"2025-10-11T11:56:03.953074Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">5.2. The Main Training Loop</h3>\nThis is where everything comes together. We will:\n1.  Iterate through each unique body_parts_tracked configuration.\n2.  For each configuration, use our generate_mouse_data to load all relevant training data and create features.\n3.  Train a separate ensemble of models for each target behavior (attack, mount, chase).\n4.  Our ensemble will consist of LightGBM and XGBoost (if available), each wrapped in our StratifiedDownsamplingClassifier.\n5.  Store all trained models in a dictionary for the prediction phase.\n</div>","metadata":{}},{"cell_type":"code","source":"models_dict = {}\n\nprint(\"--- Starting Model Training ---\")\n\nfor config_str in BODY_PART_CONFIGS:\n    body_parts = json.loads(config_str)\n    if len(body_parts) > 10:\n        body_parts = [bp for bp in body_parts if bp not in DROP_BODY_PARTS]\n    \n    print(f\"\\nTraining models for configuration with {len(body_parts)} body parts...\")\n    \n    # --- 1. Data Collection for this config ---\n    config_train_df = train_df[train_df['body_parts_tracked'] == config_str]\n    \n    all_X = []\n    all_y = []\n    \n    # Assuming a realistic number of pairs per video for tqdm\n    data_generator = generate_mouse_data(config_train_df, mode='train')\n    for _, tracking_data, _, labels in tqdm(data_generator, total=len(config_train_df) * 6):\n        if labels.empty or tracking_data.empty:\n            continue\n            \n        features = create_pair_features(tracking_data, body_parts)\n        \n        common_index = features.index.intersection(labels.index)\n        if not common_index.empty:\n            all_X.append(features.loc[common_index])\n            all_y.append(labels.loc[common_index])\n            \n    if not all_X:\n        print(f\"  No valid training data found for this configuration. Skipping.\")\n        continue\n        \n    X_train = pd.concat(all_X)\n    y_train = pd.concat(all_y)\n    del all_X, all_y\n    gc.collect()\n\n    X_train_np = X_train.values\n    \n    # --- 2. Model Training for each behavior ---\n    config_models = {}\n    for behavior in y_train.columns:\n        print(f\"  Training for behavior: '{behavior}'...\")\n        \n        # ==================================================================\n        # CORE FIX: Handle NaNs by filtering data before training\n        # ==================================================================\n        \n        # 1. Get the target series for the current behavior\n        y_series = y_train[behavior]\n        \n        # 2. Create a boolean mask to identify rows with valid (non-NaN) labels\n        valid_mask = y_series.notna()\n        \n        # 3. Apply the mask to both the features and the labels\n        X_train_behavior = X_train_np[valid_mask]\n        y_behavior_clean = y_series[valid_mask].values.astype(int)\n        \n        # Skip if there are not enough positive samples to train a meaningful model\n        if np.sum(y_behavior_clean) < 20:\n            print(f\"    Not enough positive samples ({np.sum(y_behavior_clean)}). Skipping behavior.\")\n            continue\n            \n        # Define the ensemble\n        ensemble = []\n        \n        # Model 1: LightGBM\n        lgbm = lgb.LGBMClassifier(objective='binary', metric='logloss', n_estimators=300,\n                                  learning_rate=0.05, num_leaves=31, random_state=42, n_jobs=-1, colsample_bytree=0.8)\n        \n        pipeline_lgbm = make_pipeline(\n            SimpleImputer(strategy='mean'),\n            StratifiedDownsamplingClassifier(estimator=lgbm, n_samples=150000)\n        )\n        # Fit on the cleaned, filtered data\n        pipeline_lgbm.fit(X_train_behavior, y_behavior_clean)\n        ensemble.append(pipeline_lgbm)\n\n        # Model 2: XGBoost (if available)\n        if XGBOOST_AVAILABLE:\n            xgb = XGBClassifier(objective='binary:logistic', eval_metric='logloss', n_estimators=250,\n                                learning_rate=0.05, max_depth=5, use_label_encoder=False, \n                                random_state=42, n_jobs=-1, tree_method='hist')\n            \n            pipeline_xgb = make_pipeline(\n                SimpleImputer(strategy='mean'),\n                StratifiedDownsamplingClassifier(estimator=xgb, n_samples=150000)\n            )\n            # Fit on the cleaned, filtered data\n            pipeline_xgb.fit(X_train_behavior, y_behavior_clean)\n            ensemble.append(pipeline_xgb)\n\n        config_models[behavior] = ensemble\n        \n    models_dict[config_str] = {\n        'models': config_models,\n        'feature_columns': X_train.columns\n    }\n    print(f\"  Finished training for this configuration.\")\n    del X_train, y_train, X_train_np, X_train_behavior, y_behavior_clean\n    gc.collect()\n\nprint(\"\\n--- Model Training Complete ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T12:00:42.378368Z","iopub.execute_input":"2025-10-11T12:00:42.379095Z","iopub.status.idle":"2025-10-11T12:13:16.468996Z","shell.execute_reply.started":"2025-10-11T12:00:42.37907Z","shell.execute_reply":"2025-10-11T12:13:16.468056Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"6. Prediction & Intelligent Post-Processing\nWith our models trained, we can now generate predictions on the test set. Getting raw probabilities is only half the battle; we must convert them into clean, valid submission events.\n\n<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">6.1. Post-Processing Pipeline</h3>\nThis function takes the frame-wise probabilities and applies several heuristic steps to refine them:\n1.  Temporal Smoothing: A rolling average is applied to the probabilities to reduce prediction jitter and create smoother signals.\n2.  Thresholding: A simple probability threshold (e.g., 0.5) is used to convert probabilities into binary predictions.\n3.  Event Coalescing: Consecutive frames with positive predictions are grouped together to form [start_frame, stop_frame] events.\n4.  Noise Filtering: Very short events (e.g., lasting fewer than 4 frames) are removed, as they are likely to be false positives.\n</div>","metadata":{}},{"cell_type":"code","source":"def post_process_predictions(probs_df, metadata_df, threshold=0.5, min_duration=4):\n    \"\"\"\n    Converts frame-wise probabilities into a submission-ready dataframe of events.\n    \"\"\"\n    submission_events = []\n    \n    if probs_df.empty:\n        return pd.DataFrame()\n        \n \n    smoothed_probs = probs_df.rolling(window=5, min_periods=1, center=True).mean()\n    \n    for behavior in smoothed_probs.columns:\n        \n        predictions = (smoothed_probs[behavior] > threshold).astype(int)\n        \n        \n        diffs = predictions.diff()\n        start_frames = metadata_df.loc[diffs == 1, 'video_frame']\n        stop_frames = metadata_df.loc[diffs == -1, 'video_frame']\n        \n        \n        if len(start_frames) > len(stop_frames):\n            stop_frames = stop_frames.tolist() + [metadata_df['video_frame'].max() + 1]\n            stop_frames = pd.Series(stop_frames, index=start_frames.index[:len(stop_frames)])\n        \n        if len(stop_frames) > len(start_frames):\n            start_frames = [metadata_df['video_frame'].min()] + start_frames.tolist()\n            start_frames = pd.Series(start_frames, index=stop_frames.index[:len(start_frames)])\n\n        \n        for start, stop in zip(start_frames, stop_frames):\n            if stop - start >= min_duration:\n                event = {\n                    'video_id': metadata_df['video_id'].iloc[0],\n                    'agent_id': metadata_df['agent_id'].iloc[0],\n                    'target_id': metadata_df['target_id'].iloc[0],\n                    'action': behavior,\n                    'start_frame': start,\n                    'stop_frame': stop\n                }\n                submission_events.append(event)\n                \n    return pd.DataFrame(submission_events)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T12:14:49.586176Z","iopub.execute_input":"2025-10-11T12:14:49.586664Z","iopub.status.idle":"2025-10-11T12:14:49.59552Z","shell.execute_reply.started":"2025-10-11T12:14:49.586637Z","shell.execute_reply":"2025-10-11T12:14:49.594477Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"background-color:#161b22; color:#c9d1d9; border: 1px solid #30363d; padding: 15px; border-radius: 5px; margin: 20px 0;\">\n<h3 style=\"color:#58a6ff;\">6.2. The Main Prediction Loop</h3>\nThis loop mirrors the training process but applies the trained models to the test data.\n</div>","metadata":{}},{"cell_type":"code","source":"all_submissions = []\n\nprint(\"--- Starting Prediction on Test Set ---\")\n\nfor config_str in models_dict.keys():\n    \n    body_parts = json.loads(config_str)\n    if len(body_parts) > 10:\n        body_parts = [bp for bp in body_parts if bp not in DROP_BODY_PARTS]\n    \n    print(f\"\\nPredicting for configuration with {len(body_parts)} body parts...\")\n    \n    config_test_df = test_df[test_df['body_parts_tracked'] == config_str]\n    \n    if config_test_df.empty:\n        continue\n        \n    trained_models = models_dict[config_str]['models']\n    feature_cols = models_dict[config_str]['feature_columns']\n    \n    test_generator = generate_mouse_data(config_test_df, mode='test')\n    \n    for _, tracking_data, metadata, actions in tqdm(test_generator, total=len(config_test_df)*6):\n        \n     \n        features = create_pair_features(tracking_data, body_parts)\n \n        features = features.reindex(columns=feature_cols).fillna(0)\n        \n        X_test_np = features.values\n        \n        probs_df = pd.DataFrame(index=features.index)\n        \n        for behavior, ensemble in trained_models.items():\n            if behavior in actions: \n                behavior_probs = [model.predict_proba(X_test_np)[:, 1] for model in ensemble]\n                probs_df[behavior] = np.mean(behavior_probs, axis=0)\n\n        sub_part = post_process_predictions(probs_df, metadata)\n        if not sub_part.empty:\n            all_submissions.append(sub_part)\n\nprint(\"\\n--- Prediction Complete ---\")\n\n\nif all_submissions:\n    submission_df = pd.concat(all_submissions, ignore_index=True)\n    submission_df = submission_df.sort_values(by=['video_id', 'agent_id', 'target_id', 'start_frame'])\n    submission_df = submission_df.drop_duplicates(subset=['video_id', 'agent_id', 'target_id', 'action', 'start_frame'])\nelse:\n    submission_df = pd.DataFrame(columns=['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame'])\n\nsubmission_df.to_csv('submission.csv', index_label='row_id')\nprint(f\"\\nSubmission file created with {len(submission_df)} events.\")\ndisplay(submission_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-11T12:14:52.778143Z","iopub.execute_input":"2025-10-11T12:14:52.778487Z","iopub.status.idle":"2025-10-11T12:15:01.640776Z","shell.execute_reply.started":"2025-10-11T12:14:52.77846Z","shell.execute_reply":"2025-10-11T12:15:01.639968Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"background-color:#0d1117; color:#c9d1d9; border: 2px solid #30363d; border-radius: 10px; padding: 25px; font-family: sans-serif;\"><h2 style=\"color: #58a6ff; border-bottom: 3px solid #238636; padding-bottom: 10px; margin-bottom: 20px;\">🏁 7. Conclusion and Future Directions</h2><div style=\"background-color:#161b22; padding: 15px; border-left: 5px solid #58a6ff; border-radius: 5px;\">This notebook presented a comprehensive, robust, and memory-efficient pipeline for the MABe challenge. We tackled the core challenges of high data volume, complex dynamic behaviors, data heterogeneity, and class imbalance with a series of targeted strategies.</div><div style=\"margin-top: 25px;\"><h3 style=\"color: #39d353; margin-bottom: 15px;\">Key Pillars of Our Approach:</h3><ul style=\"list-style: none; padding-left: 0;\"><li style=\"background-color: #161b22; border: 1px solid #30363d; padding: 12px; border-radius: 5px; margin-bottom: 8px;\">📦 A <strong>generator-based pipeline</strong> to handle massive data.</li><li style=\"background-color: #161b22; border: 1px solid #30363d; padding: 12px; border-radius: 5px; margin-bottom: 8px;\">🧩 A <strong>model-per-modality</strong> strategy to adapt to heterogeneous data sources.</li><li style=\"background-color: #161b22; border: 1px solid #30363d; padding: 12px; border-radius: 5px; margin-bottom: 8px;\">📈 <strong>Advanced feature engineering</strong> focusing on kinematics and social interaction dynamics.</li><li style=\"background-color: #161b22; border: 1px solid #30363d; padding: 12px; border-radius: 5px; margin-bottom: 8px;\">⚖️ A <strong>stratified, ensemble model</strong> to handle class imbalance and improve generalization.</li><li style=\"background-color: #161b22; border: 1px solid #30363d; padding: 12px; border-radius: 5px;\">🧼 <strong>Intelligent post-processing</strong> to clean and refine predictions into valid events.</li></ul></div><div style=\"margin-top: 30px;\"><h3 style=\"color: #e3b341; border-bottom: 2px solid #30363d; padding-bottom: 8px; margin-bottom: 20px;\">🚀 Potential Future Improvements</h3><div style=\"background-color:#161b22; padding: 15px; border-radius: 5px; border: 1px solid #30363d;\">While this notebook provides a strong baseline, several avenues could be explored for even higher performance:<ol style=\"padding-left: 20px; margin-top: 15px;\"><li style=\"margin-bottom: 10px;\"><strong>Sequence Models:</strong> The current frame-by-frame approach with rolling windows captures local context. True sequence models like LSTMs, GRUs, or Transformers could learn longer-term temporal dependencies more effectively.</li><li style=\"margin-bottom: 10px;\"><strong>Hyperparameter Optimization:</strong> We used robust default hyperparameters. A systematic optimization process using a library like Optuna or Hyperopt could fine-tune the models for each behavior and data modality, likely yielding significant gains.</li><li style=\"margin-bottom: 10px;\"><strong>Advanced Post-Processing:</strong> The post-processing could be made more sophisticated. For example, using a Hidden Markov Model (HMM) to smooth transitions between \"behavior\" and \"no-behavior\" states could produce more realistic event boundaries.</li><li style=\"margin-bottom: 10px;\"><strong>Cross-Validation Strategy:</strong> For more robust evaluation and model selection, a GroupKFold cross-validation strategy (grouping by video_id) should be implemented to prevent data leakage between frames of the same video.</li></ol></div></div><div style=\"margin-top: 30px; padding: 15px; background-color: #0c1a1f; border-top: 3px solid #39d353; border-radius: 5px; text-align: center;\">Thank you for following along! This challenging competition offers a fantastic opportunity to apply and refine a wide range of machine learning techniques.</div><div style=\"text-align: right; margin-top: 25px; font-size: 0.9em; color: #8b949e;\"><em>Created by Ozan M.</em></div></div>","metadata":{}}]}