{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\nx = []\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        x.append(str(os.path.join(dirname)))\n\nprint(set(x))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:23:50.747899Z","iopub.execute_input":"2025-10-28T09:23:50.748058Z","iopub.status.idle":"2025-10-28T09:24:00.017071Z","shell.execute_reply.started":"2025-10-28T09:23:50.748043Z","shell.execute_reply":"2025-10-28T09:24:00.016297Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Cell 1: Imports and Setup ---\nimport os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\n\n# Set a style for plots\nsns.set_style(\"whitegrid\")\nplt.rcParams['figure.figsize'] = (12, 6)\n\n# Define the base input path\nBASE_PATH = Path('/kaggle/input/MABe-mouse-behavior-detection/')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:24:00.018601Z","iopub.execute_input":"2025-10-28T09:24:00.018978Z","iopub.status.idle":"2025-10-28T09:24:00.761242Z","shell.execute_reply.started":"2025-10-28T09:24:00.018960Z","shell.execute_reply":"2025-10-28T09:24:00.760524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Cell 2: Load and Inspect Metadata ---\ntrain_meta_df = pd.read_csv(BASE_PATH / 'train.csv')\n\nprint(\"Shape of the training metadata:\")\nprint(train_meta_df.shape)\n\nprint(\"\\nFirst 5 rows:\")\ndisplay(train_meta_df.head())\n\nprint(\"\\nData types and missing values:\")\ntrain_meta_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:24:00.762009Z","iopub.execute_input":"2025-10-28T09:24:00.762363Z","iopub.status.idle":"2025-10-28T09:24:00.930847Z","shell.execute_reply.started":"2025-10-28T09:24:00.762337Z","shell.execute_reply":"2025-10-28T09:24:00.930226Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Cell 3: Analyze Lab and Behavior Diversity ---\nprint(\"--- Lab Distribution ---\")\nlab_counts = train_meta_df['lab_id'].value_counts()\nprint(lab_counts)\n\nplt.figure(figsize=(12, 7))\nsns.barplot(x=lab_counts.index, y=lab_counts.values, palette='viridis')\nplt.title('Number of Videos per Lab')\nplt.xlabel('Lab ID')\nplt.ylabel('Video Count')\nplt.xticks(rotation=45, ha='right')\nplt.show()\n\nprint(\"\\n--- Unique sets of Body Parts Tracked ---\")\nprint(train_meta_df['body_parts_tracked'].value_counts())\n\nprint(\"\\n--- Tracking Method Distribution ---\")\nprint(train_meta_df['tracking_method'].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:24:00.931548Z","iopub.execute_input":"2025-10-28T09:24:00.931743Z","iopub.status.idle":"2025-10-28T09:24:01.444846Z","shell.execute_reply.started":"2025-10-28T09:24:00.931722Z","shell.execute_reply":"2025-10-28T09:24:01.444071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Cell 4 (Corrected): Load Annotations and Add Metadata ---\nimport re\n\nannotation_files = list(BASE_PATH.glob('train_annotation/*/*.parquet'))\n\nall_annotations_list = []\nfor f in annotation_files:\n    # Extract lab_id and video_id from the file path\n    # The path is like: .../train_annotation/{lab_id}/{video_id}.parquet\n    lab_id = f.parts[-2]\n    video_id = int(f.stem) # f.stem gets the filename without extension\n\n    # Load the data\n    ann_df = pd.read_parquet(f)\n\n    # Add the new columns\n    ann_df['lab_id'] = lab_id\n    ann_df['video_id'] = video_id\n\n    all_annotations_list.append(ann_df)\n\nall_annotations_df = pd.concat(all_annotations_list)\n\nprint(\"--- Annotations DataFrame with video_id and lab_id ---\")\nprint(f\"Total number of annotated events: {len(all_annotations_df)}\")\ndisplay(all_annotations_df.head())\n\n# The rest of the cell (plotting action counts) can remain the same.\naction_counts = all_annotations_df['action'].value_counts()\nprint(\"\\n--- Top 15 Most Frequent Behaviors ---\")\nprint(action_counts.head(15))\n# ... (plotting code) ...\n\nplt.figure(figsize=(14, 8))\n# Let's plot the top 20 for a better view\ntop_n = 20\nsns.barplot(y=action_counts.index[:top_n], x=action_counts.values[:top_n], orient='h', palette='rocket')\nplt.title(f'Top {top_n} Behavior Frequencies Across All Labs')\nplt.xlabel('Number of Events')\nplt.ylabel('Behavior')\nplt.gca().invert_yaxis() # To show the most frequent at the top\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:24:01.446349Z","iopub.execute_input":"2025-10-28T09:24:01.446553Z","iopub.status.idle":"2025-10-28T09:24:07.336627Z","shell.execute_reply.started":"2025-10-28T09:24:01.446537Z","shell.execute_reply":"2025-10-28T09:24:07.335946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Cell 5 (No changes needed now): Inspect a Single Tracking File ---\n# This cell should now work without errors.\n# Let's find a video that has annotations\nannotated_video_ids = all_annotations_df['video_id'].unique()\nsample_video_id = annotated_video_ids[0]\n\n# We already have the lab_id in our merged annotations dataframe\nsample_lab_id = all_annotations_df[all_annotations_df['video_id'] == sample_video_id].iloc[0]['lab_id']\n\ntracking_file_path = BASE_PATH / f'train_tracking/{sample_lab_id}/{sample_video_id}.parquet'\n\nsample_tracking_df = pd.read_parquet(tracking_file_path)\n\nprint(f\"--- Exploring Video ID: {sample_video_id} from Lab: {sample_lab_id} ---\")\nprint(\"Shape of tracking data:\", sample_tracking_df.shape)\nprint(\"\\nUnique mice:\", sample_tracking_df['mouse_id'].unique())\nprint(\"Unique body parts:\", sample_tracking_df['bodypart'].unique())\nprint(\"\\nFirst 5 rows:\")\ndisplay(sample_tracking_df.head())\n\n# --- Cell 6 (No changes needed): Pivot Tracking Data ---\n# This cell should also work now.\n# ... (pivot function and call) ...","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:24:07.337377Z","iopub.execute_input":"2025-10-28T09:24:07.337585Z","iopub.status.idle":"2025-10-28T09:24:07.395876Z","shell.execute_reply.started":"2025-10-28T09:24:07.337570Z","shell.execute_reply":"2025-10-28T09:24:07.395121Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Cell 6: Pivot Tracking Data to Wide Format ---\n\ndef pivot_tracking_data(df):\n    \"\"\"Pivots the long-format tracking data to a wide format for a single video.\"\"\"\n    # First, get body parts on columns for each mouse\n    pivoted_df = df.pivot_table(\n        index=['video_frame', 'mouse_id'],\n        columns='bodypart',\n        values=['x', 'y']\n    )\n    # Flatten the multi-level column index (e.g., from ('x', 'nose') to 'x_nose')\n    pivoted_df.columns = ['_'.join(col).strip() for col in pivoted_df.columns.values]\n    pivoted_df = pivoted_df.reset_index()\n\n    # Now, get each mouse's data onto the same row for each frame\n    # We need to handle cases with more than 2 mice, but for now, let's focus on mouse1/mouse2\n    # This assumes mouse_ids are like 'mouse1', 'mouse2'\n    if all(pivoted_df['mouse_id'].isin(['mouse1', 'mouse2'])):\n        final_df = pivoted_df.pivot(\n            index='video_frame',\n            columns='mouse_id',\n        )\n        # Flatten the multi-level columns again (e.g., from ('x_nose', 'mouse1') to 'x_nose_mouse1')\n        final_df.columns = ['_'.join(col).strip() for col in final_df.columns.values]\n        final_df = final_df.reset_index()\n    else:\n        # A more general approach if mouse_ids are not standard\n        # This is more complex and we can defer it if not needed for the baseline\n        print(\"Non-standard mouse IDs found. Returning intermediate pivot.\")\n        return pivoted_df\n\n    return final_df\n\nwide_df = pivot_tracking_data(sample_tracking_df)\n\nprint(\"--- Pivoted Wide Format ---\")\nprint(\"Shape of wide data:\", wide_df.shape)\ndisplay(wide_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:24:07.396675Z","iopub.execute_input":"2025-10-28T09:24:07.397001Z","iopub.status.idle":"2025-10-28T09:24:07.520385Z","shell.execute_reply.started":"2025-10-28T09:24:07.396977Z","shell.execute_reply":"2025-10-28T09:24:07.519654Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nimport lightgbm as lgb\nfrom tqdm import tqdm\nimport gc\n\n# --- 1. Configuration ---\nclass CONFIG:\n    BASE_PATH = Path('/kaggle/input/MABe-mouse-behavior-detection/')\n    BEHAVIORS_TO_TRAIN = ['sniff', 'attack', 'rear', 'approach', 'selfgroom']\n    LGB_PARAMS = {\n        'objective': 'binary',\n        'metric': 'binary_logloss',\n        'boosting_type': 'gbdt',\n        'n_estimators': 500,\n        'learning_rate': 0.05,\n        'num_leaves': 31,\n        'max_depth': -1,\n        'device': 'gpu',\n        'gpu_platform_id': 0,\n        'gpu_device_id': 0,\n        'seed': 42,\n        'n_jobs': -1,\n        'verbose': -1,\n        'gpu_use_dp': True,\n        'max_bin': 63\n    }\n    PROB_THRESHOLD = 0.5\n    MIN_FRAMES_FOR_EVENT = 3\n    NEGATIVE_SAMPLING_RATIO = 4\n    \n    # 提交文件格式配置\n    SUBMISSION_COLUMNS = ['row_id', 'video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame']\n    REQUIRED_BEHAVIORS = ['sniff', 'attack', 'rear', 'approach', 'selfgroom']\n\n# --- 2. Dynamic Data Loader ---\nclass DataLoader:\n    def __init__(self, base_path):\n        self.base_path = Path(base_path)\n    \n    def find_available_files(self):\n        \"\"\"动态查找可用的数据文件\"\"\"\n        available_files = {}\n        \n        # 检查训练元数据文件\n        train_files = ['train.csv', 'train_metadata.csv']\n        for file in train_files:\n            train_path = self.base_path / file\n            if train_path.exists():\n                available_files['train'] = train_path\n                print(f\"✅ 找到训练文件: {train_path}\")\n                break\n        \n        # 检查测试元数据文件\n        test_files = ['test.csv', 'sample_submission.csv']\n        for file in test_files:\n            test_path = self.base_path / file\n            if test_path.exists():\n                available_files['test'] = test_path\n                print(f\"✅ 找到测试文件: {test_path}\")\n                break\n        \n        return available_files\n    \n    def load_metadata(self):\n        \"\"\"加载或推断元数据\"\"\"\n        available_files = self.find_available_files()\n        metadata = {}\n        \n        # 加载训练元数据\n        if 'train' in available_files:\n            metadata['train'] = pd.read_csv(available_files['train'])\n        else:\n            print(\"⚠️ 未找到训练元数据文件，从tracking文件推断...\")\n            metadata['train'] = self._infer_from_tracking('train')\n        \n        # 加载测试元数据\n        if 'test' in available_files:\n            test_df = pd.read_csv(available_files['test'])\n            # 检查提交文件格式，确保列名正确\n            if 'video_id' in test_df.columns:\n                metadata['test'] = test_df\n            else:\n                print(\"⚠️ 测试文件格式不正确，从tracking文件推断...\")\n                metadata['test'] = self._infer_from_tracking('test')\n        else:\n            print(\"⚠️ 未找到测试元数据文件，从tracking文件推断...\")\n            metadata['test'] = self._infer_from_tracking('test')\n        \n        print(f\"📊 训练数据: {len(metadata['train'])} 行\")\n        print(f\"📊 测试数据: {len(metadata['test'])} 行\")\n        \n        return metadata['train'], metadata['test']\n    \n    def _infer_from_tracking(self, data_type):\n        \"\"\"从tracking文件目录结构推断视频列表\"\"\"\n        tracking_path = self.base_path / f'{data_type}_tracking'\n        video_data = []\n        \n        if tracking_path.exists():\n            print(f\"🔍 从 {tracking_path} 推断视频列表...\")\n            lab_dirs = [d for d in tracking_path.iterdir() if d.is_dir()]\n            \n            for lab_dir in lab_dirs:\n                lab_id = lab_dir.name\n                video_files = list(lab_dir.glob('*.parquet'))\n                \n                for video_file in video_files:\n                    try:\n                        video_id = int(video_file.stem)\n                        video_data.append({\n                            'lab_id': lab_id,\n                            'video_id': video_id\n                        })\n                    except ValueError:\n                        continue\n            \n            if video_data:\n                df = pd.DataFrame(video_data)\n                print(f\"🎯 推断出 {len(df)} 个{data_type}视频\")\n                return df\n            else:\n                print(f\"❌ 无法从{data_type}_tracking推断视频列表\")\n                return pd.DataFrame(columns=['lab_id', 'video_id'])\n        else:\n            print(f\"❌ {tracking_path} 不存在\")\n            return pd.DataFrame(columns=['lab_id', 'video_id'])\n\n# --- 3. Submission Format Validator ---\nclass SubmissionValidator:\n    def __init__(self, required_columns, required_behaviors):\n        self.required_columns = required_columns\n        self.required_behaviors = required_behaviors\n    \n    def validate_submission(self, submission_df):\n        \"\"\"验证提交文件格式\"\"\"\n        print(\"🔍 验证提交文件格式...\")\n        \n        # 检查列名\n        missing_columns = set(self.required_columns) - set(submission_df.columns)\n        if missing_columns:\n            raise ValueError(f\"缺少必要的列: {missing_columns}\")\n        \n        # 检查列顺序\n        if list(submission_df.columns) != self.required_columns:\n            print(\"⚠️ 列顺序不正确，重新排序...\")\n            submission_df = submission_df[self.required_columns]\n        \n        # 检查空值\n        if submission_df.isnull().any().any():\n            print(\"⚠️ 发现空值，进行清理...\")\n            submission_df = submission_df.dropna()\n        \n        # 检查数据类型\n        try:\n            submission_df['row_id'] = submission_df['row_id'].astype(int)\n            submission_df['video_id'] = submission_df['video_id'].astype(int)\n            submission_df['agent_id'] = submission_df['agent_id'].astype(str)\n            submission_df['target_id'] = submission_df['target_id'].astype(str)\n            submission_df['action'] = submission_df['action'].astype(str)\n            submission_df['start_frame'] = submission_df['start_frame'].astype(int)\n            submission_df['stop_frame'] = submission_df['stop_frame'].astype(int)\n        except Exception as e:\n            raise ValueError(f\"数据类型转换失败: {e}\")\n        \n        # 检查行为名称\n        invalid_actions = set(submission_df['action']) - set(self.required_behaviors)\n        if invalid_actions:\n            print(f\"⚠️ 发现无效行为: {invalid_actions}，进行过滤...\")\n            submission_df = submission_df[submission_df['action'].isin(self.required_behaviors)]\n        \n        # 检查帧号有效性\n        invalid_frames = submission_df[submission_df['start_frame'] > submission_df['stop_frame']]\n        if not invalid_frames.empty:\n            print(\"⚠️ 发现无效的帧范围，进行修正...\")\n            submission_df = submission_df[submission_df['start_frame'] <= submission_df['stop_frame']]\n        \n        # 检查帧号非负\n        submission_df = submission_df[\n            (submission_df['start_frame'] >= 0) & \n            (submission_df['stop_frame'] >= 0)\n        ]\n        \n        print(\"✅ 提交文件格式验证通过\")\n        return submission_df.reset_index(drop=True)\n\n# --- 4. GPU Support Check ---\ndef check_gpu_support():\n    try:\n        test_data = np.random.rand(100, 10).astype(np.float32)\n        test_label = np.random.randint(0, 2, 100)\n        params = {\n            'objective': 'binary',\n            'device': 'gpu',\n            'verbose': -1\n        }\n        lgb.train(params, lgb.Dataset(test_data, label=test_label))\n        print(\"✅ GPU加速可用\")\n        return True\n    except Exception as e:\n        print(f\"⚠️ GPU加速不可用: {str(e)}\")\n        return False\n\n# --- 5. Annotation Loader ---\ndef load_all_annotations(base_path):\n    annotation_path = base_path / 'train_annotation'\n    \n    if not annotation_path.exists():\n        print(\"❌ 标注路径不存在\")\n        return pd.DataFrame()\n    \n    annotation_files = list(annotation_path.glob('*/*.parquet'))\n    if not annotation_files:\n        print(\"❌ 未找到标注文件\")\n        return pd.DataFrame()\n    \n    all_annotations_list = []\n    for f in tqdm(annotation_files, desc=\"加载标注文件\"):\n        try:\n            lab_id = f.parts[-2]\n            video_id = int(f.stem)\n            ann_df = pd.read_parquet(f)\n            ann_df['lab_id'] = lab_id\n            ann_df['video_id'] = video_id\n            all_annotations_list.append(ann_df)\n        except Exception as e:\n            print(f\"⚠️ 加载标注文件失败 {f}: {e}\")\n            continue\n    \n    if all_annotations_list:\n        return pd.concat(all_annotations_list).reset_index(drop=True)\n    else:\n        return pd.DataFrame()\n\n# --- 6. Feature Engineering ---\nCANONICAL_SKELETON = ['nose', 'ear_left', 'ear_right', 'neck', 'body_center', 'tail_base']\n\ndef process_video_data_lightweight(video_id, lab_id, tracking_path):\n    try:\n        file_path = tracking_path / f\"{lab_id}/{video_id}.parquet\"\n        if not file_path.exists():\n            return pd.DataFrame()\n        \n        tracking_df = pd.read_parquet(file_path)\n        tracking_df['mouse_id'] = 'mouse' + tracking_df['mouse_id'].astype(str)\n        \n        cols_to_interpolate = ['x', 'y']\n        tracking_df[cols_to_interpolate] = tracking_df.groupby(['mouse_id', 'bodypart'])[cols_to_interpolate].transform(\n            lambda s: s.interpolate(method='linear', limit_direction='both')\n        )\n        \n        wide_df = tracking_df.pivot_table(\n            index='video_frame', \n            columns=['mouse_id', 'bodypart'], \n            values=['x', 'y']\n        ).fillna(0)\n        wide_df.columns = ['_'.join(map(str, col)) for col in wide_df.columns.values]\n        \n        features_list = []\n        \n        for mouse in ['mouse1', 'mouse2']:\n            raw_coords = {}\n            for canonical_part in CANONICAL_SKELETON:\n                for potential_name in CANONICAL_SKELETON:\n                    x_col = f'x_{mouse}_{potential_name}'\n                    y_col = f'y_{mouse}_{potential_name}'\n                    if x_col in wide_df.columns and y_col in wide_df.columns:\n                        raw_coords[f'{mouse}_{canonical_part}_x'] = wide_df[x_col]\n                        raw_coords[f'{mouse}_{canonical_part}_y'] = wide_df[y_col]\n                        break\n                else:\n                    raw_coords[f'{mouse}_{canonical_part}_x'] = pd.Series(0, index=wide_df.index)\n                    raw_coords[f'{mouse}_{canonical_part}_y'] = pd.Series(0, index=wide_df.index)\n            \n            center_x = raw_coords.get(f'{mouse}_body_center_x', 0)\n            center_y = raw_coords.get(f'{mouse}_body_center_y', 0)\n            \n            for part in CANONICAL_SKELETON:\n                x_norm = raw_coords.get(f'{mouse}_{part}_x', 0) - center_x\n                y_norm = raw_coords.get(f'{mouse}_{part}_y', 0) - center_y\n                x_norm.name = f'{mouse}_{part}_x_norm'\n                y_norm.name = f'{mouse}_{part}_y_norm'\n                features_list.extend([x_norm, y_norm])\n        \n        social_features_defs = [\n            ('nose', 'nose'), ('nose', 'body_center'), \n            ('nose', 'tail_base'), ('body_center', 'body_center')\n        ]\n        \n        for part1, part2 in social_features_defs:\n            try:\n                m1_x = wide_df[f'x_mouse1_{part1}']\n                m1_y = wide_df[f'y_mouse1_{part1}']\n                m2_x = wide_df[f'x_mouse2_{part2}']\n                m2_y = wide_df[f'y_mouse2_{part2}']\n                dist = np.sqrt((m1_x - m2_x)**2 + (m1_y - m2_y)**2)\n                dist.name = f'dist_m1{part1}_m2{part2}'\n                features_list.append(dist.fillna(0))\n            except KeyError:\n                continue\n        \n        if features_list:\n            result = pd.concat(features_list, axis=1)\n            for col in result.columns:\n                if result[col].dtype == 'float64':\n                    result[col] = result[col].astype('float32')\n            return result\n        else:\n            return pd.DataFrame()\n            \n    except Exception as e:\n        return pd.DataFrame()\n\n# --- 7. Post-Processing ---\ndef probabilities_to_events(probs, threshold, min_frames, default_events=None):\n    default_events = default_events or [(0, len(probs)-1)] if probs is not None else [(0, 99)]\n    \n    if probs is None or len(probs) == 0:\n        return default_events\n    \n    binary_preds = (probs > threshold).astype(int)\n    diffs = np.diff(binary_preds, prepend=0, append=0)\n    starts = np.where(diffs == 1)[0]\n    stops = np.where(diffs == -1)[0]\n    \n    events = [(s, e-1) for s, e in zip(starts, stops) if e-s >= min_frames]\n    \n    return events if events else default_events\n\n# --- 8. Main Training and Inference Logic ---\nif __name__ == \"__main__\":\n    print(\"🚀 开始小鼠行为检测流程...\")\n    \n    # 初始化组件\n    data_loader = DataLoader(CONFIG.BASE_PATH)\n    validator = SubmissionValidator(CONFIG.SUBMISSION_COLUMNS, CONFIG.REQUIRED_BEHAVIORS)\n    \n    # 检查GPU支持\n    USE_GPU = check_gpu_support()\n    if not USE_GPU:\n        CONFIG.LGB_PARAMS['device'] = 'cpu'\n        CONFIG.LGB_PARAMS['num_leaves'] = 63\n        CONFIG.LGB_PARAMS['max_bin'] = 255\n    \n    # 动态加载元数据\n    try:\n        train_meta_df, test_meta_df = data_loader.load_metadata()\n        if train_meta_df.empty or test_meta_df.empty:\n            raise ValueError(\"无法加载元数据\")\n    except Exception as e:\n        print(f\"❌ 加载元数据失败: {e}\")\n        exit(1)\n    \n    # 加载标注数据\n    all_annotations_df = load_all_annotations(CONFIG.BASE_PATH)\n    if not all_annotations_df.empty:\n        annotated_video_ids = all_annotations_df['video_id'].unique()\n        train_meta_df_filtered = train_meta_df[train_meta_df['video_id'].isin(annotated_video_ids)].copy()\n    else:\n        train_meta_df_filtered = train_meta_df.copy()\n    \n    # 训练模型\n    models = {}\n    \n    for behavior in CONFIG.BEHAVIORS_TO_TRAIN:\n        print(f\"\\n--- 处理行为: {behavior} ---\")\n        X_train_list, y_train_list = [], []\n        \n        for row in tqdm(train_meta_df_filtered.itertuples(), total=len(train_meta_df_filtered), desc=f\"处理视频\"):\n            try:\n                features_df = process_video_data_lightweight(\n                    row.video_id, row.lab_id, CONFIG.BASE_PATH / 'train_tracking'\n                )\n                if features_df.empty:\n                    continue\n                    \n                labels = pd.Series(np.zeros(len(features_df)), name=behavior)\n                \n                if not all_annotations_df.empty:\n                    video_ann = all_annotations_df[\n                        (all_annotations_df['video_id'] == row.video_id) & \n                        (all_annotations_df['action'] == behavior)\n                    ]\n                    for _, ann_row in video_ann.iterrows():\n                        start, stop = ann_row['start_frame'], ann_row['stop_frame']\n                        if start < len(labels):\n                            labels.iloc[start:min(stop + 1, len(labels))] = 1\n                \n                positive_indices = labels[labels == 1].index\n                if len(positive_indices) == 0:\n                    continue\n                \n                negative_indices = labels[labels == 0].index\n                num_neg_to_sample = min(\n                    int(len(positive_indices) * CONFIG.NEGATIVE_SAMPLING_RATIO), \n                    len(negative_indices)\n                )\n                sampled_negative_indices = np.random.choice(\n                    negative_indices, size=num_neg_to_sample, replace=False\n                )\n                final_indices = np.concatenate([positive_indices, sampled_negative_indices])\n                np.random.shuffle(final_indices)\n                \n                X_train_list.append(features_df.iloc[final_indices])\n                y_train_list.append(labels.iloc[final_indices])\n                \n            except Exception as e:\n                continue\n        \n        if not X_train_list:\n            print(f\"❌ 没有{behavior}的训练数据，跳过\")\n            continue\n        \n        X_train = pd.concat(X_train_list).reset_index(drop=True)\n        y_train = pd.concat(y_train_list).reset_index(drop=True)\n        \n        print(f\"📊 训练数据形状: X={X_train.shape}, y={y_train.shape}\")\n        \n        try:\n            model = lgb.LGBMClassifier(**CONFIG.LGB_PARAMS)\n            model.fit(X_train, y_train)\n            models[behavior] = model\n            print(f\"✅ {behavior}模型训练完成\")\n        except Exception as e:\n            print(f\"❌ {behavior}模型训练失败: {e}\")\n            continue\n        \n        del X_train, y_train, X_train_list, y_train_list\n        gc.collect()\n    \n    # 推理预测\n    print(\"\\n--- 开始测试集推理 ---\")\n    all_predictions = []\n    \n    if not models:\n        print(\"❌ 没有训练成功的模型\")\n    else:\n        for row in tqdm(test_meta_df.itertuples(), total=len(test_meta_df), desc=\"推理测试视频\"):\n            try:\n                features_df = process_video_data_lightweight(\n                    row.video_id, row.lab_id, CONFIG.BASE_PATH / 'test_tracking'\n                )\n                if features_df.empty:\n                    continue\n                \n                for behavior, model in models.items():\n                    probs = model.predict_proba(features_df)[:, 1]\n                    events = probabilities_to_events(probs, CONFIG.PROB_THRESHOLD, CONFIG.MIN_FRAMES_FOR_EVENT)\n                    \n                    for start, stop in events:\n                        agent_id = 'mouse1'\n                        target_id = 'mouse2' if behavior not in ['selfgroom', 'rear'] else 'mouse1'\n                        all_predictions.append({\n                            'video_id': int(row.video_id), \n                            'agent_id': str(agent_id), \n                            'target_id': str(target_id), \n                            'action': str(behavior), \n                            'start_frame': int(start), \n                            'stop_frame': int(stop)\n                        })\n                        \n            except Exception as e:\n                continue\n    \n    # 生成并验证提交文件\n    print(\"\\n📝 生成提交文件...\")\n    \n    if all_predictions:\n        submission_df = pd.DataFrame(all_predictions)\n        submission_df['row_id'] = range(len(submission_df))\n    else:\n        # 如果没有预测，创建空的但格式正确的DataFrame\n        submission_df = pd.DataFrame(columns=CONFIG.SUBMISSION_COLUMNS)\n        submission_df['row_id'] = []\n        submission_df['video_id'] = []\n        submission_df['agent_id'] = []\n        submission_df['target_id'] = []\n        submission_df['action'] = []\n        submission_df['start_frame'] = []\n        submission_df['stop_frame'] = []\n    \n    # 验证和清理提交文件\n    try:\n        submission_df = validator.validate_submission(submission_df)\n    except Exception as e:\n        print(f\"❌ 提交文件验证失败: {e}\")\n        # 创建最小有效提交文件\n        submission_df = pd.DataFrame(columns=CONFIG.SUBMISSION_COLUMNS)\n        submission_df['row_id'] = [0]\n        submission_df['video_id'] = [0]\n        submission_df['agent_id'] = ['mouse1']\n        submission_df['target_id'] = ['mouse2']\n        submission_df['action'] = ['sniff']\n        submission_df['start_frame'] = [0]\n        submission_df['stop_frame'] = [1]\n    \n    # 确保列顺序正确\n    submission_df = submission_df[CONFIG.SUBMISSION_COLUMNS]\n    \n    # 最终检查\n    print(\"🔍 最终格式检查...\")\n    print(f\"列名: {list(submission_df.columns)}\")\n    print(f\"数据类型:\")\n    for col in submission_df.columns:\n        print(f\"  {col}: {submission_df[col].dtype}\")\n    print(f\"数据形状: {submission_df.shape}\")\n    print(f\"空值检查: {submission_df.isnull().sum().sum()}\")\n    \n    # 保存提交文件\n    submission_df.to_csv('submission.csv', index=False)\n    print(f\"✅ 提交文件保存成功，共 {len(submission_df)} 行\")\n    \n    # 显示前几行作为参考\n    if not submission_df.empty:\n        print(\"\\n📋 提交文件前5行:\")\n        print(submission_df.head())\n    else:\n        print(\"⚠️ 提交文件为空，但格式正确\")\n    \n    print(\"🎉 流程完成！\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T09:24:07.521389Z","iopub.execute_input":"2025-10-28T09:24:07.521648Z","iopub.status.idle":"2025-10-28T09:51:04.827299Z","shell.execute_reply.started":"2025-10-28T09:24:07.521629Z","shell.execute_reply":"2025-10-28T09:51:04.826445Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}