{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","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":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ================= Step 1: 环境配置 =================\nimport pandas as pd\nimport numpy as np\nimport json\nimport gc\nimport os\nimport glob\nimport joblib\nimport itertools\nimport warnings\nfrom tqdm.auto import tqdm\nfrom lightgbm import LGBMClassifier\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import f1_score, roc_auc_score\n\n# 设置显示选项\npd.set_option('display.max_columns', None)\nwarnings.filterwarnings('ignore')\n\nclass CFG:\n    # 路径配置\n    train_path = '/kaggle/input/MABe-mouse-behavior-detection/train.csv'\n    test_path = '/kaggle/input/MABe-mouse-behavior-detection/test.csv'\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    train_annotation_path = '/kaggle/input/MABe-mouse-behavior-detection/train_annotation'\n    \n    model_name = 'model'  # 模型保存路径\n    seed = 42\n\nprint(\"Step 1: 环境配置完成。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:30.666061Z","iopub.execute_input":"2025-12-25T06:20:30.666776Z","iopub.status.idle":"2025-12-25T06:20:43.475410Z","shell.execute_reply.started":"2025-12-25T06:20:30.666735Z","shell.execute_reply":"2025-12-25T06:20:43.474791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Step 2: 读取元数据 =================\nprint(\"Step 2: 正在读取 CSV 文件...\")\ntrain = pd.read_csv(CFG.train_path)\ntest = pd.read_csv(CFG.test_path)\n\n# 过滤 'MABe22' 数据\ntrain = train.query(\"~lab_id.str.startswith('MABe22_')\").reset_index(drop=True)\n\n# 提取所有出现的“身体部位追踪配置”\nbody_parts_tracked_list = list(np.unique(train.body_parts_tracked))\n\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(f\"训练集: {train.shape}, 测试集: {test.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.476567Z","iopub.execute_input":"2025-12-25T06:20:43.477169Z","iopub.status.idle":"2025-12-25T06:20:43.612446Z","shell.execute_reply.started":"2025-12-25T06:20:43.477136Z","shell.execute_reply":"2025-12-25T06:20:43.611823Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= 后处理工具函数 =================\nimport pandas as pd\nimport numpy as np\nimport json\nimport os\n\ndef smooth_predictions(df, min_gap=30, min_duration=5):\n    \"\"\"\n    强力平滑函数：\n    1. Gap Filling: 如果两个相同动作间隔很短 (<30帧/1秒)，把它们连起来。\n    2. Remove Short: 如果动作持续时间太短 (<5帧)，视为噪声删掉。\n    \"\"\"\n    if df.empty: return df\n    \n    # 按视频、角色排序\n    df = df.sort_values(['video_id', 'agent_id', 'target_id', 'action', 'start_frame'])\n    refined_rows = []\n    \n    # 分组处理\n    for _, group in df.groupby(['video_id', 'agent_id', 'target_id', 'action']):\n        group = group.to_dict('records')\n        if not group: continue\n        \n        current = group[0]\n        \n        for i in range(1, len(group)):\n            next_event = group[i]\n            # 计算间隙\n            gap = next_event['start_frame'] - current['stop_frame']\n            \n            # 如果间隙很小，合并\n            if gap <= min_gap:\n                current['stop_frame'] = max(current['stop_frame'], next_event['stop_frame'])\n            else:\n                # 间隙太大，先保存当前的(如果够长)，然后开启新的\n                if (current['stop_frame'] - current['start_frame']) >= min_duration:\n                    refined_rows.append(current)\n                current = next_event\n        \n        # 保存最后一个\n        if (current['stop_frame'] - current['start_frame']) >= min_duration:\n            refined_rows.append(current)\n            \n    return pd.DataFrame(refined_rows)\n\ndef robustify(submission, dataset, traintest, traintest_directory=None):\n    \"\"\"\n    提交文件清洗与兜底函数 (防止提交失败的关键)\n    \"\"\"\n    if traintest_directory is None:\n        traintest_directory = f\"/kaggle/input/MABe-mouse-behavior-detection/{traintest}_tracking\"\n    \n    # 1. 基础清洗\n    submission['start_frame'] = pd.to_numeric(submission['start_frame'], errors='coerce').fillna(0).astype(int)\n    submission['stop_frame'] = pd.to_numeric(submission['stop_frame'], errors='coerce').fillna(0).astype(int)\n    submission = submission[submission.start_frame < submission.stop_frame]\n\n    # 2. 去重 (Resolve Overlaps)\n    # 简单策略：后来的动作覆盖前面的动作，或者保留较长的。\n    # 这里使用：按时间排序，截断重叠部分\n    group_list = []\n    if not submission.empty:\n        for _, group in submission.groupby(['video_id', 'agent_id', 'target_id']):\n            group = group.sort_values('start_frame')\n            keep_mask = np.ones(len(group), dtype=bool)\n            last_stop = -1\n            \n            for i, (_, row) in enumerate(group.iterrows()):\n                if row['start_frame'] < last_stop:\n                    # 发生重叠，简单丢弃后一个 (或者你可以写更复杂的逻辑)\n                    keep_mask[i] = False\n                else:\n                    last_stop = row['stop_frame']\n            group_list.append(group[keep_mask])\n        submission = pd.concat(group_list) if group_list else submission\n\n    # 3. 兜底填充 (Dummy Filling) - 最重要的一步！\n    # 检查是否有视频被遗漏了。如果有，必须填假数据，否则 Kaggle 会报错。\n    predicted_videos = set(submission.video_id.unique()) if not submission.empty else set()\n    dummy_rows = []\n    \n    for _, row in dataset.iterrows():\n        vid = row.video_id\n        # 如果这个视频已经有预测了，或者是不需要预测的 MABe22 数据，跳过\n        if vid in predicted_videos or str(row.lab_id).startswith('MABe22'):\n            continue\n            \n        # 如果是漏掉的视频，生成一个 \"other\" 动作占位\n        # 需要解析该视频有哪些老鼠\n        try:\n            b_labeled = json.loads(row.behaviors_labeled)\n            # 简单解析出第一对 agent-target\n            first_pair = b_labeled[0].replace(\"'\", \"\").split(',')[:2] # [agent, target]\n            agent, target = first_pair[0], first_pair[1]\n            \n            dummy_rows.append({\n                'video_id': vid,\n                'agent_id': agent,\n                'target_id': target,\n                'action': 'other', # 或者 'investigation' 等常见动作\n                'start_frame': 0,\n                'stop_frame': 100\n            })\n        except:\n            # 实在解析不了，硬填\n            dummy_rows.append({\n                'video_id': vid,\n                'agent_id': 'mouse1',\n                'target_id': 'mouse2',\n                'action': 'other',\n                'start_frame': 0,\n                'stop_frame': 100\n            })\n            \n    if dummy_rows:\n        print(f\"Robustify: Filled {len(dummy_rows)} missing videos with dummy predictions.\")\n        submission = pd.concat([submission, pd.DataFrame(dummy_rows)], ignore_index=True)\n        \n    return submission.reset_index(drop=True)\n\nprint(\"后处理工具 (Robustify & Smoothing) 定义完成。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.613319Z","iopub.execute_input":"2025-12-25T06:20:43.613588Z","iopub.status.idle":"2025-12-25T06:20:43.748476Z","shell.execute_reply.started":"2025-12-25T06:20:43.613569Z","shell.execute_reply":"2025-12-25T06:20:43.747804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Step 3: 工具函数 (含高级特征计算) =================\n\ndef _fps_from_meta(meta, fps_lookup=None, default_fps=30.0):\n    if fps_lookup is not None:\n        vid = meta.video_id.iloc[0] if isinstance(meta, pd.DataFrame) else meta['video_id']\n        return fps_lookup.get(vid, default_fps)\n    if 'frames_per_second' in meta.columns:\n        return meta.frames_per_second.iloc[0]\n    return default_fps\n\ndef _scale(window, fps):\n    return int(max(1, window * fps / 30.0))\n\ndef _triangle_area(p1, p2, p3):\n    return 0.5 * np.abs(p1['x']*(p2['y']-p3['y']) + p2['x']*(p3['y']-p1['y']) + p3['x']*(p1['y']-p2['y']))\n\ndef _angle_between(v1, v2):\n    dot = v1['x']*v2['x'] + v1['y']*v2['y']\n    norm = np.sqrt(v1['x']**2 + v1['y']**2) * np.sqrt(v2['x']**2 + v2['y']**2)\n    return np.arccos(np.clip(dot/(norm+1e-6), -1.0, 1.0))\n\n# --- 高级特征计算 ---\ndef add_frequency_features(X, cx, cy, fps):\n    \"\"\"震动特征 (Jerk)\"\"\"\n    vx = cx.diff().fillna(0); vy = cy.diff().fillna(0)\n    ax = vx.diff().fillna(0); ay = vy.diff().fillna(0)\n    jx = ax.diff().fillna(0); jy = ay.diff().fillna(0)\n    jerk_mag = np.sqrt(jx**2 + jy**2) * (fps**3) \n    for w in [10, 30]:\n        ws = _scale(w, fps)\n        X[f'jerk_mean_{w}'] = jerk_mag.rolling(ws, min_periods=1).mean()\n        X[f'jerk_std_{w}']  = jerk_mag.rolling(ws, min_periods=1).std()\n    return X\n\ndef add_tortuosity_features(X, cx, cy, fps):\n    \"\"\"曲折度特征\"\"\"\n    step_len = np.sqrt(cx.diff()**2 + cy.diff()**2).fillna(0)\n    for w in [30, 90]:\n        ws = _scale(w, fps)\n        path_len = step_len.rolling(ws, min_periods=1).sum()\n        dx = cx - cx.shift(ws).fillna(method='bfill')\n        dy = cy - cy.shift(ws).fillna(method='bfill')\n        disp = np.sqrt(dx**2 + dy**2)\n        X[f'tortuosity_{w}'] = path_len / (disp + 0.1)\n    return X\n\ndef add_wall_features(X, cx, cy):\n    \"\"\"贴墙特征\"\"\"\n    min_x, max_x = cx.min(), cx.max()\n    min_y, max_y = cy.min(), cy.max()\n    dist_l = cx - min_x; dist_r = max_x - cx\n    dist_t = cy - min_y; dist_b = max_y - cy\n    X['dist_to_wall'] = np.minimum.reduce([dist_l, dist_r, dist_t, dist_b])\n    X['is_corner'] = (((dist_l < 5) | (dist_r < 5)) & ((dist_t < 5) | (dist_b < 5))).astype(float)\n    return X\n\ndef add_relative_motion_state(X, mouse_pair, fps):\n    \"\"\"相对运动状态\"\"\"\n    vA = np.sqrt(mouse_pair['A']['body_center'].diff()['x']**2 + mouse_pair['A']['body_center'].diff()['y']**2) * fps\n    vB = np.sqrt(mouse_pair['B']['body_center'].diff()['x']**2 + mouse_pair['B']['body_center'].diff()['y']**2) * fps\n    move_th = 2.0\n    state = np.zeros(len(X))\n    state += 1 * ((vA > move_th) & (vB <= move_th))\n    state += 2 * ((vA <= move_th) & (vB > move_th))\n    state += 3 * ((vA > move_th) & (vB > move_th))\n    X['motion_state_code'] = state\n    return X\n\ndef add_arena_features(X, cx, cy, arena_dims=None):\n    if arena_dims is None or np.isnan(arena_dims).any(): return X\n    w, h = arena_dims\n    X['dist_center'] = np.sqrt((cx - w/2)**2 + (cy - h/2)**2)\n    X['dist_wall'] = np.minimum(np.minimum(cx, w - cx), np.minimum(cy, h - cy))\n    return X\n\n# 兼容旧函数\ndef add_curvature_features(X, cx, cy, fps):\n    vx = cx.diff().fillna(0); vy = cy.diff().fillna(0)\n    ax = vx.diff().fillna(0); ay = vy.diff().fillna(0)\n    curv = np.abs(vx*ay - vy*ax) / ((vx**2 + vy**2)**1.5 + 1e-6)\n    for w in [25, 50]:\n        ws = _scale(w, fps)\n        X[f'curv_mean_{w}'] = curv.rolling(ws, min_periods=1).mean()\n    return X\n\ndef add_multiscale_features(X, cx, cy, fps):\n    speed = np.sqrt(cx.diff()**2 + cy.diff()**2) * fps\n    for scale in [20, 60]:\n        ws = _scale(scale, fps)\n        X[f'sp_m{scale}'] = speed.rolling(ws, min_periods=1).mean()\n        X[f'sp_s{scale}'] = speed.rolling(ws, min_periods=1).std()\n    return X\n\n# Robustify\ndef robustify(submission, dataset, traintest, traintest_directory=None):\n    # (此处省略长代码，使用你之前提供的 Robustify 即可，或者直接用我之前给你的版本)\n    # 为了完整性建议直接复制之前的 robustify 函数定义\n    pass \n    \nprint(\"Step 3: 工具函数定义完成。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.749842Z","iopub.execute_input":"2025-12-25T06:20:43.750098Z","iopub.status.idle":"2025-12-25T06:20:43.774074Z","shell.execute_reply.started":"2025-12-25T06:20:43.750079Z","shell.execute_reply":"2025-12-25T06:20:43.773422Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Step 4: 数据生成器 (带监控版) =================\n# 在原有基础上增加了 verbose 参数，用于打印调试信息\n\ndef generate_mouse_data(dataset, traintest, traintest_directory=None, generate_single=True, generate_pair=True, verbose=False):\n    if traintest_directory is None:\n        traintest_directory = f\"/kaggle/input/MABe-mouse-behavior-detection/{traintest}_tracking\"\n        \n    for idx, row in dataset.iterrows():\n        if not isinstance(row.behaviors_labeled, str): continue\n            \n        lab_id = row.lab_id\n        video_id = row.video_id\n        path = f\"{traintest_directory}/{lab_id}/{video_id}.parquet\"\n        if not os.path.exists(path): continue\n            \n        try:\n            vid = pd.read_parquet(path)\n            # [监控] 打印原始数据形状\n            if verbose and idx == 0: \n                print(f\"    [Debug] 原始 Parquet 读取成功. Shape: {vid.shape}\")\n        except: continue\n\n        if 'drop_body_parts' in globals() and len(np.unique(vid.bodypart)) > 5:\n            vid = vid.query(\"~ bodypart.isin(@drop_body_parts)\")\n            \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        if hasattr(row, 'pix_per_cm_approx') and row.pix_per_cm_approx > 0:\n            pvid /= row.pix_per_cm_approx\n            \n        # [监控] 打印预处理后形状\n        if verbose and idx == 0:\n            print(f\"    [Debug] 预处理/归一化完成. Shape: {pvid.shape}\")\n\n        try:\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        except: continue\n        \n        annot = None\n        if traintest == 'train':\n            annot_path = path.replace(f'{traintest}_tracking', 'train_annotation')\n            if os.path.exists(annot_path):\n                try: annot = pd.read_parquet(annot_path)\n                except: continue\n            else: continue\n\n        if generate_single:\n            subset = vid_behaviors.query(\"target == 'self'\")\n            unique_agents = np.unique(subset.agent)\n            for agent_str in unique_agents:\n                try:\n                    mouse_id = int(agent_str.replace('mouse', '')) if 'mouse' in agent_str else int(agent_str)\n                    if mouse_id in pvid.columns.get_level_values(0):\n                        single_mouse = pvid.loc[:, mouse_id]\n                        meta = pd.DataFrame({'video_id': video_id, 'agent_id': agent_str, 'target_id': 'self', 'video_frame': single_mouse.index, 'frames_per_second': row.frames_per_second})\n                        possible_actions = np.unique(subset.query(\"agent == @agent_str\").action)\n                        if traintest == 'train':\n                            y = pd.DataFrame(0, index=single_mouse.index, columns=possible_actions)\n                            if annot is not None:\n                                agent_annot = annot.query(\"agent_id == @mouse_id and target_id == @mouse_id\")\n                                for _, r in agent_annot.iterrows():\n                                    if r['action'] in y.columns: y.loc[r['start_frame']:r['stop_frame'], r['action']] = 1\n                            yield 'single', single_mouse, meta, y\n                        else:\n                            yield 'single', single_mouse, meta, possible_actions\n                except: pass\n\n        if generate_pair:\n            subset = vid_behaviors.query(\"target != 'self'\")\n            if len(subset) > 0:\n                all_mice = np.unique(pvid.columns.get_level_values(0))\n                for m1, m2 in itertools.permutations(all_mice, 2):\n                    agent_str = f\"mouse{m1}\"; target_str = f\"mouse{m2}\"\n                    pair_actions = subset.query(\"agent == @agent_str and target == @target_str\")\n                    if len(pair_actions) == 0: continue\n                    mouse_pair = pd.concat([pvid[m1], pvid[m2]], axis=1, keys=['A', 'B'])\n                    meta = pd.DataFrame({'video_id': video_id, 'agent_id': agent_str, 'target_id': target_str, 'video_frame': mouse_pair.index, 'frames_per_second': row.frames_per_second})\n                    possible_actions = np.unique(pair_actions.action)\n                    if traintest == 'train':\n                        y = pd.DataFrame(0, index=mouse_pair.index, columns=possible_actions)\n                        if annot is not None:\n                            pair_annot = annot.query(\"agent_id == @m1 and target_id == @m2\")\n                            for _, r in pair_annot.iterrows():\n                                if r['action'] in y.columns: y.loc[r['start_frame']:r['stop_frame'], r['action']] = 1\n                        yield 'pair', mouse_pair, meta, y\n                    else:\n                        yield 'pair', mouse_pair, meta, possible_actions\n\nprint(\"Step 4: 数据生成器定义完成。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.774856Z","iopub.execute_input":"2025-12-25T06:20:43.775107Z","iopub.status.idle":"2025-12-25T06:20:43.797251Z","shell.execute_reply.started":"2025-12-25T06:20:43.775082Z","shell.execute_reply":"2025-12-25T06:20:43.796500Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Step 5: 特征工程 (维度对齐) =================\ndef transform_single_base(single_mouse, body_parts_tracked, fps, arena_dims=None):\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)\n        for p1, p2 in itertools.combinations(body_parts_tracked, 2)\n        if p1 in available_body_parts and p2 in available_body_parts\n    })\n    \n    if all(p in available_body_parts for p in ['nose', 'ear_left', 'ear_right']):\n        X['head_area'] = _triangle_area(single_mouse['nose'], single_mouse['ear_left'], single_mouse['ear_right'])\n    if all(p in available_body_parts for p in ['nose', 'body_center', 'tail_base']):\n        v1 = single_mouse['nose'] - single_mouse['body_center']\n        v2 = single_mouse['tail_base'] - single_mouse['body_center']\n        X['spine_angle'] = _angle_between(v1, v2)\n\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']; cy = single_mouse['body_center']['y']\n        X = add_arena_features(X, cx, cy, arena_dims)\n        for w in [5, 15, 30]:\n            ws = _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            vx = cx.diff().fillna(0); vy = cy.diff().fillna(0)\n            speed = np.sqrt(vx**2 + vy**2) * fps\n            X[f'speed_mean_{w}'] = speed.rolling(ws, **roll).mean()\n            acc_mag = np.sqrt(vx.diff()**2 + vy.diff()**2).fillna(0)\n            X[f'jitter_{w}'] = acc_mag.rolling(ws, **roll).mean()\n            X[f'rest_ratio_{w}'] = (speed < 1.0).astype(float).rolling(ws, **roll).mean()\n        X = add_curvature_features(X, cx, cy, fps)\n        X = add_multiscale_features(X, cx, cy, fps)\n    return X.fillna(0).replace([np.inf, -np.inf], 0)\n\ndef transform_single(single_mouse, body_parts_tracked, fps, arena_dims=None):\n    X = transform_single_base(single_mouse, body_parts_tracked, fps, arena_dims)\n    if 'body_center' in single_mouse.columns.get_level_values(0):\n        cx = single_mouse['body_center']['x']; cy = single_mouse['body_center']['y']\n        X = add_frequency_features(X, cx, cy, fps) # +4\n        X = add_tortuosity_features(X, cx, cy, fps)# +2\n        X = add_wall_features(X, cx, cy)           # +2\n    return X.fillna(0).replace([np.inf, -np.inf], 0)\n\ndef transform_pair(mouse_pair, body_parts_tracked, fps):\n    X_A = transform_single_base(mouse_pair['A'], body_parts_tracked, fps)\n    X_B = transform_single_base(mouse_pair['B'], body_parts_tracked, fps)\n    X_A.columns = [f\"A_{c}\" for c in X_A.columns]\n    X_B.columns = [f\"B_{c}\" for c in X_B.columns]\n    X = pd.concat([X_A, X_B], axis=1)\n    \n    avail_A = mouse_pair['A'].columns.get_level_values(0)\n    avail_B = mouse_pair['B'].columns.get_level_values(0)\n\n    for p1, p2 in itertools.product(body_parts_tracked, repeat=2):\n        if p1 in avail_A and p2 in avail_B:\n            X[f\"12+{p1}+{p2}\"] = np.square(mouse_pair['A'][p1] - mouse_pair['B'][p2]).sum(axis=1)\n\n    if all(p in avail_A for p in ['nose', 'body_center']) and all(p in avail_B for p in ['nose', 'body_center']):\n        cA = mouse_pair['A']['body_center']; cB = mouse_pair['B']['body_center']\n        vec_AB = cB - cA\n        dist_AB = np.sqrt(vec_AB['x']**2 + vec_AB['y']**2) + 1e-6\n        vec_head_A = mouse_pair['A']['nose'] - cA; vec_head_B = mouse_pair['B']['nose'] - cB\n        dot_A_AB = vec_head_A['x'] * vec_AB['x'] + vec_head_A['y'] * vec_AB['y']\n        norm_head_A = np.sqrt(vec_head_A['x']**2 + vec_head_A['y']**2) + 1e-6\n        X['A_facing_B'] = dot_A_AB / (norm_head_A * dist_AB)\n        dot_heads = vec_head_A['x'] * vec_head_B['x'] + vec_head_A['y'] * vec_head_B['y']\n        norm_head_B = np.sqrt(vec_head_B['x']**2 + vec_head_B['y']**2) + 1e-6\n        X['relative_orientation'] = dot_heads / (norm_head_A * norm_head_B)\n\n    if 'body_center' in avail_A and 'body_center' in avail_B:\n        dist_full = np.sqrt(np.square(mouse_pair['A']['body_center'] - mouse_pair['B']['body_center']).sum(axis=1))\n        for w in [10, 30]:\n            ws = _scale(w, fps)\n            roll = dict(min_periods=1, center=True)\n            X[f'approach_speed_{w}'] = (-dist_full.diff().fillna(0) * fps).rolling(ws, **roll).mean()\n            vA = np.sqrt(mouse_pair['A']['body_center'].diff()['x']**2 + mouse_pair['A']['body_center'].diff()['y']**2)\n            vB = np.sqrt(mouse_pair['B']['body_center'].diff()['x']**2 + mouse_pair['B']['body_center'].diff()['y']**2)\n            X[f'speed_corr_{w}'] = vA.rolling(ws, **roll).corr(vB)\n\n        X = add_relative_motion_state(X, mouse_pair, fps) # +1\n        cx = (mouse_pair['A']['body_center']['x'] + mouse_pair['B']['body_center']['x']) / 2\n        cy = (mouse_pair['A']['body_center']['y'] + mouse_pair['B']['body_center']['y']) / 2\n        X = add_frequency_features(X, cx, cy, fps) # +4\n        X = add_tortuosity_features(X, cx, cy, fps)# +2\n        X = add_wall_features(X, cx, cy)           # +2\n\n    return X.fillna(0).replace([np.inf, -np.inf], 0)\n\nprint(\"Step 5: 特征工程定义完成。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.798136Z","iopub.execute_input":"2025-12-25T06:20:43.798639Z","iopub.status.idle":"2025-12-25T06:20:43.822279Z","shell.execute_reply.started":"2025-12-25T06:20:43.798592Z","shell.execute_reply":"2025-12-25T06:20:43.821650Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Step 6: 定义训练函数 (终极底层静音版) =================\nfrom lightgbm import LGBMClassifier, log_evaluation, early_stopping\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import f1_score\nfrom tqdm.auto import tqdm\nimport joblib\nimport os\nimport sys\nimport gc\nimport numpy as np\nimport warnings\n\n# 1. 屏蔽 Python 警告\nwarnings.filterwarnings('ignore')\n\n# 2. 定义【底层】强力消音器 (使用 os.dup2 拦截 C++ 输出)\nclass SuppressAllOutput:\n    def __enter__(self):\n        # 刷新缓冲区，防止之前的日志没打印出来就被掐断\n        sys.stdout.flush()\n        sys.stderr.flush()\n        \n        # 1. 打开黑洞\n        self.null_fd = os.open(os.devnull, os.O_WRONLY)\n        \n        # 2. 备份当前的标准输出/错误的文件描述符 (File Descriptors)\n        self.save_stdout_fd = os.dup(1)\n        self.save_stderr_fd = os.dup(2)\n        \n        # 3. 将标准输出/错误 重定向到黑洞 (这是 OS 级别的操作)\n        os.dup2(self.null_fd, 1)\n        os.dup2(self.null_fd, 2)\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        # 4. 恢复原来的文件描述符\n        os.dup2(self.save_stdout_fd, 1)\n        os.dup2(self.save_stderr_fd, 2)\n        \n        # 5. 关闭临时句柄\n        os.close(self.null_fd)\n        os.close(self.save_stdout_fd)\n        os.close(self.save_stderr_fd)\n\ndef tune_threshold(y_prob, y_true):\n    best_f1 = 0; best_th = 0.3\n    for th in np.arange(0.1, 0.65, 0.05):\n        pred = (y_prob >= th).astype(int)\n        if pred.sum() == 0 and y_true.sum() == 0: f1 = 1.0\n        else: f1 = f1_score(y_true, pred, zero_division=0)\n        if f1 > best_f1: best_f1 = f1; best_th = th\n    return best_th\n\ndef train_one_stage_classifiers(X, label, meta, section_name):\n    print(f\"  [Training] Action Classifiers...\")\n    \n    save_dir_base = f\"{CFG.model_name}/{section_name}\"\n    os.makedirs(save_dir_base, exist_ok=True)\n    \n    thresholds = {}\n    pbar = tqdm(label.columns, desc=\"Progress\", leave=False)\n    \n    for action in pbar:\n        pbar.set_description(f\"Train: {action}\")\n        \n        # 1. 准备数据\n        action_mask = ~label[action].isna().values\n        if action_mask.sum() == 0: continue\n        y_action = label[action][action_mask].values.astype(int)\n        \n        if y_action.sum() < 20: \n            thresholds[action] = 1.0; continue\n            \n        X_action = X[action_mask]\n        groups_action = meta.video_id[action_mask]\n        \n        use_gpu = len(X_action) > 5000 \n        model_params = {\n            'objective': 'binary', 'metric': 'auc', 'n_estimators': 1000, \n            'learning_rate': 0.04, 'num_leaves': 31, 'max_depth': 8,\n            'class_weight': 'balanced', 'reg_alpha': 0.5, 'reg_lambda': 0.5,\n            'n_jobs': -1, 'random_state': 42,\n            'verbosity': -1,\n            'device': 'gpu' if use_gpu else 'cpu'\n        }\n        \n        cv = GroupKFold(n_splits=3)\n        oof_action = np.zeros(len(y_action))\n        models = []\n        \n        # 打印行头\n        msg = f\"    👉 {action:<12} [\"\n        print(msg, end=\"\", flush=True)\n        \n        for fold, (tr_idx, va_idx) in enumerate(cv.split(X_action, y_action, groups_action)):\n            print(f\"{fold+1}..\", end=\"\", flush=True)\n            \n            try:\n                # 【关键】使用底层消音器\n                with SuppressAllOutput():\n                    model = LGBMClassifier(**model_params)\n                    model.fit(\n                        X_action.iloc[tr_idx], y_action[tr_idx],\n                        eval_set=[(X_action.iloc[va_idx], y_action[va_idx])],\n                        eval_metric='auc',\n                        callbacks=[early_stopping(30, verbose=False), log_evaluation(0)]\n                    )\n            except:\n                model_params['device'] = 'cpu'\n                with SuppressAllOutput():\n                    model = LGBMClassifier(**model_params)\n                    model.fit(\n                        X_action.iloc[tr_idx], y_action[tr_idx],\n                        eval_set=[(X_action.iloc[va_idx], y_action[va_idx])],\n                        eval_metric='auc',\n                        callbacks=[early_stopping(30, verbose=False), log_evaluation(0)]\n                    )\n            \n            oof_action[va_idx] = model.predict_proba(X_action.iloc[va_idx])[:, 1]\n            models.append(model)\n            \n        best_th = tune_threshold(oof_action, y_action)\n        thresholds[action] = best_th\n        f1 = f1_score(y_action, (oof_action >= best_th).astype(int), zero_division=0)\n        \n        print(f\"] ✅ F1: {f1:.4f} (Pos: {y_action.sum()})\")\n        \n        action_dir = f\"{save_dir_base}/{action}\"\n        os.makedirs(action_dir, exist_ok=True)\n        joblib.dump(models, f\"{action_dir}/models.pkl\")\n        \n    joblib.dump(thresholds, f\"{save_dir_base}/thresholds.pkl\")\n    return thresholds\n\nprint(\"Step 6: 定义完成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.823297Z","iopub.execute_input":"2025-12-25T06:20:43.823660Z","iopub.status.idle":"2025-12-25T06:20:43.849821Z","shell.execute_reply.started":"2025-12-25T06:20:43.823632Z","shell.execute_reply":"2025-12-25T06:20:43.849320Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ================= Step 7: 训练主循环 (修复对齐Bug版) =================\n# import gc\n# import numpy as np\n# import pandas as pd\n\n# print(\"Step 7: 开始 One-Stage 训练 ...\")\n# total_sections = len(body_parts_tracked_list)\n\n# # 背景帧保留比例 (10%)\n# DOWNSAMPLE_RATIO = 0.1 \n# # 硬上限 (防止爆内存)\n# MAX_TRAIN_SAMPLES = 1500000 \n\n# for section in range(total_sections):\n#     bpt_str = body_parts_tracked_list[section]\n#     print(f\"\\n{'='*10} Section {section+1}/{total_sections} {'='*10}\")\n    \n#     train_subset = train[train.body_parts_tracked == bpt_str]\n#     if len(train_subset) == 0: continue\n    \n#     try:\n#         bpt = json.loads(bpt_str)\n#         if len(bpt) > 5: bpt = [b for b in bpt if b not in drop_body_parts]\n#     except: continue\n    \n#     _fps_lookup = (train_subset[['video_id', 'frames_per_second']]\n#                    .drop_duplicates('video_id').set_index('video_id')['frames_per_second'].to_dict())\n\n#     for mode in ['single', 'pair']:\n#         print(f\"  >>> Mode: {mode}\")\n        \n#         X_parts = []\n#         label_parts = []\n#         group_parts = []\n        \n#         gen = generate_mouse_data(\n#             train_subset, 'train', \n#             traintest_directory=CFG.train_tracking_path, \n#             generate_single=(mode=='single'), generate_pair=(mode=='pair')\n#         )\n        \n#         count = 0\n#         for switch, data, meta, label in gen:\n#             if switch != mode: continue\n            \n#             try:\n#                 # 1. 提取特征\n#                 fps = _fps_from_meta(meta, _fps_lookup)\n#                 if mode == 'single': Xi = transform_single(data, bpt, fps)\n#                 else: Xi = transform_pair(data, bpt, fps)\n                \n#                 # 2. 负样本降采样\n#                 has_action = label.sum(axis=1) > 0\n#                 keep_mask = has_action | (np.random.rand(len(has_action)) < DOWNSAMPLE_RATIO)\n#                 if keep_mask.sum() == 0: keep_mask.iloc[0] = True\n                \n#                 # 3. 【关键修复】先切片，再 Append (事务性操作)\n#                 # 这样如果任何一步报错，都不会污染 X_parts 列表\n#                 X_cut = Xi.loc[keep_mask]\n#                 label_cut = label.loc[keep_mask]\n                \n#                 # 安全获取 video_id\n#                 # 某些 Pandas 版本下 meta.video_id 可能是属性，用 ['video_id'] 更安全\n#                 group_cut = meta['video_id'].loc[keep_mask]\n                \n#                 # 双重检查长度是否一致\n#                 if len(X_cut) != len(group_cut):\n#                     raise ValueError(\"Length mismatch inside loop\")\n\n#                 # 一次性加入\n#                 X_parts.append(X_cut)\n#                 label_parts.append(label_cut)\n#                 group_parts.append(group_cut)\n                \n#                 count += 1\n#             except Exception as e:\n#                 # print(f\"Skipped video due to error: {e}\")\n#                 continue\n        \n#         if count == 0: continue\n        \n#         # 拼接数据\n#         X_all = pd.concat(X_parts, axis=0, ignore_index=True)\n#         label_all = pd.concat(label_parts, axis=0, ignore_index=True)\n#         groups_all = pd.concat(group_parts, axis=0, ignore_index=True)\n        \n#         # 构造 Meta\n#         meta_all = pd.DataFrame({'video_id': groups_all.values})\n        \n#         print(f\"  Raw Shape: {X_all.shape}\")\n        \n#         # 4. 硬上限截断 (防止 Section 3 这种超大数据集)\n#         if len(X_all) > MAX_TRAIN_SAMPLES:\n#             print(f\"  ⚠️ 数据量过大 ({len(X_all)}), 强制采样至 {MAX_TRAIN_SAMPLES}...\")\n#             indices = np.random.choice(len(X_all), MAX_TRAIN_SAMPLES, replace=False)\n#             X_all = X_all.iloc[indices].reset_index(drop=True)\n#             label_all = label_all.iloc[indices].reset_index(drop=True)\n#             meta_all = meta_all.iloc[indices].reset_index(drop=True)\n#             print(f\"  Capped Shape: {X_all.shape}\")\n\n#         # 内存清理\n#         del X_parts, label_parts, group_parts, groups_all\n#         gc.collect()\n        \n#         # 开始训练\n#         section_name = f\"{section}_{mode}\"\n#         train_one_stage_classifiers(X_all, label_all, meta_all, section_name)\n        \n#         del X_all, label_all, meta_all\n#         gc.collect()\n        \n# print(\"\\n🎉 训练完成！\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.850681Z","iopub.execute_input":"2025-12-25T06:20:43.850966Z","iopub.status.idle":"2025-12-25T06:20:43.874290Z","shell.execute_reply.started":"2025-12-25T06:20:43.850941Z","shell.execute_reply":"2025-12-25T06:20:43.873833Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ================= Step 8: 推理与提交 (One-Stage 最终修正版) =================\n# import joblib\n# import gc\n# import os\n# import numpy as np\n# import pandas as pd\n# import json\n# from tqdm.auto import tqdm\n\n# # 重新定义，防止变量丢失\n# drop_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\n# # --- 8.1 后处理工具函数 ---\n# def smooth_predictions(df, min_gap=30, min_duration=5):\n#     if df.empty: return df\n#     df = df.sort_values(['video_id', 'agent_id', 'target_id', 'action', 'start_frame'])\n#     refined_rows = []\n    \n#     for _, group in df.groupby(['video_id', 'agent_id', 'target_id', 'action']):\n#         group = group.to_dict('records')\n#         if not group: continue\n#         current = group[0]\n        \n#         for i in range(1, len(group)):\n#             next_event = group[i]\n#             gap = next_event['start_frame'] - current['stop_frame']\n            \n#             if gap <= min_gap:\n#                 current['stop_frame'] = max(current['stop_frame'], next_event['stop_frame'])\n#             else:\n#                 if (current['stop_frame'] - current['start_frame']) >= min_duration:\n#                     refined_rows.append(current)\n#                 current = next_event\n#         if (current['stop_frame'] - current['start_frame']) >= min_duration:\n#             refined_rows.append(current)\n            \n#     return pd.DataFrame(refined_rows)\n\n# def robustify(submission, dataset, traintest):\n#     submission['start_frame'] = pd.to_numeric(submission['start_frame'], errors='coerce').fillna(0).astype(int)\n#     submission['stop_frame'] = pd.to_numeric(submission['stop_frame'], errors='coerce').fillna(0).astype(int)\n#     submission = submission[submission.start_frame < submission.stop_frame]\n    \n#     predicted_videos = set(submission.video_id.unique()) if not submission.empty else set()\n#     dummy_rows = []\n    \n#     for _, row in dataset.iterrows():\n#         if row.video_id in predicted_videos or str(row.lab_id).startswith('MABe22'): continue\n#         dummy_rows.append({\n#             'video_id': row.video_id, 'agent_id': 'mouse1', 'target_id': 'mouse2',\n#             'action': 'other', 'start_frame': 0, 'stop_frame': 100\n#         })\n#     if dummy_rows:\n#         submission = pd.concat([submission, pd.DataFrame(dummy_rows)], ignore_index=True)\n#     return submission.reset_index(drop=True)\n\n# # --- 8.2 主推理循环 ---\n# print(\"Step 8: 开始全量推理 (One-Stage Inference)...\")\n# MODEL_ROOT = CFG.model_name\n# submission_list = []\n\n# print(f\"Loading models from: {MODEL_ROOT}\")\n# models_cache = {}\n\n# for section in range(len(body_parts_tracked_list)):\n#     models_cache[section] = {}\n#     for mode in ['single', 'pair']:\n#         section_name = f\"{section}_{mode}\"\n#         model_dir = f\"{MODEL_ROOT}/{section_name}\" # <--- 你的路径是对的，直接指向 section 文件夹\n        \n#         try:\n#             # 检查这里：直接找 thresholds.pkl\n#             if os.path.exists(f\"{model_dir}/thresholds.pkl\"):\n#                 thresholds = joblib.load(f\"{model_dir}/thresholds.pkl\")\n#                 models_cache[section][mode] = {'models': {}, 'ths': thresholds}\n                \n#                 for action in thresholds.keys():\n#                     # 检查这里：直接找 action 文件夹\n#                     m_path = f\"{model_dir}/{action}/models.pkl\"\n#                     if os.path.exists(m_path):\n#                         models_cache[section][mode]['models'][action] = joblib.load(m_path)\n#         except Exception as e:\n#             pass\n\n# print(f\"Loaded Sections: {[k for k in models_cache.keys() if models_cache[k]]}\")\n\n# # 遍历测试集\n# for section in range(len(body_parts_tracked_list)):\n#     bpt_str = body_parts_tracked_list[section]\n#     test_subset = test[test.body_parts_tracked == bpt_str]\n#     if len(test_subset) == 0: continue\n#     if section not in models_cache: continue\n    \n#     print(f\"Processing Section {section} ({len(test_subset)} videos)...\")\n#     try:\n#         bpt = json.loads(bpt_str)\n#         if len(bpt) > 5: bpt = [b for b in bpt if b not in drop_body_parts]\n#     except: continue\n\n#     _fps_lookup = (test_subset[['video_id', 'frames_per_second']]\n#                    .drop_duplicates('video_id').set_index('video_id')['frames_per_second'].to_dict())\n\n#     for mode in ['single', 'pair']:\n#         if mode not in models_cache[section] or not models_cache[section][mode]['models']:\n#             continue\n            \n#         cache = models_cache[section][mode]\n#         models_dict = cache['models']\n        \n#         gen = generate_mouse_data(test_subset, 'test', traintest_directory=CFG.test_tracking_path, \n#                                   generate_single=(mode=='single'), generate_pair=(mode=='pair'))\n        \n#         for switch, data, meta, _ in gen:\n#             if switch != mode: continue\n#             try:\n#                 fps = _fps_from_meta(meta, _fps_lookup)\n#                 if mode == 'single': X = transform_single(data, bpt, fps)\n#                 else: X = transform_pair(data, bpt, fps)\n                \n#                 # 全量打分\n#                 pred_df = pd.DataFrame(index=meta.video_frame)\n#                 for action, models in models_dict.items():\n#                     probs = np.mean([m.predict_proba(X)[:, 1] for m in models], axis=0)\n#                     pred_df[action] = probs\n                \n#                 if pred_df.empty:\n#                     del X, data, meta; continue\n\n#                 # 决策\n#                 best_actions = pred_df.idxmax(axis=1)\n#                 max_probs = pred_df.max(axis=1)\n#                 mask = max_probs > 0.15 \n#                 chosen_actions = best_actions[mask]\n                \n#                 if chosen_actions.empty:\n#                     del X, data, meta, pred_df; continue\n\n#                 # 转换格式\n#                 temp_res = pd.DataFrame({'action': chosen_actions.values, 'frame': chosen_actions.index.values})\n#                 temp_res['grp'] = ((temp_res['frame'].diff() > 1) | (temp_res['action'] != temp_res['action'].shift())).cumsum()\n                \n#                 intervals = temp_res.groupby('grp').agg(\n#                     action=('action', 'first'),\n#                     start_frame=('frame', 'min'),\n#                     stop_frame=('frame', 'max')\n#                 )\n#                 intervals['stop_frame'] += 1\n                \n#                 vid = meta.video_id.iloc[0]\n#                 aid = meta.agent_id.iloc[0]\n#                 tid = meta.target_id.iloc[0]\n                \n#                 for _, row in intervals.iterrows():\n#                     submission_list.append({\n#                         'video_id': vid, 'agent_id': aid, 'target_id': tid,\n#                         'action': row['action'], 'start_frame': row['start_frame'], 'stop_frame': row['stop_frame']\n#                     })\n#                 del X, data, meta, pred_df, temp_res, intervals\n                \n#             except Exception as e:\n#                 print(f\"Error: {e}\"); continue\n#     gc.collect()\n\n# # --- 8.3 生成 CSV ---\n# print(\"Raw inference done. Generating CSV...\")\n# if len(submission_list) > 0:\n#     submission_df = pd.DataFrame(submission_list)\n#     submission_df = smooth_predictions(submission_df, min_gap=30, min_duration=5)\n# else:\n#     submission_df = pd.DataFrame(columns=['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame'])\n\n# final_submission = robustify(submission_df, test, 'test')\n\n# # 【修改建议】: index=False 是最稳妥的\n# final_submission.to_csv('submission.csv', index=False)\n# print(f\"SUCCESS: submission.csv generated with {len(final_submission)} rows.\")\n# print(final_submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.875158Z","iopub.execute_input":"2025-12-25T06:20:43.875647Z","iopub.status.idle":"2025-12-25T06:20:43.897664Z","shell.execute_reply.started":"2025-12-25T06:20:43.875621Z","shell.execute_reply":"2025-12-25T06:20:43.897011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Step 7: 训练主循环 (全量 & 内存安全) =================\nimport gc\nimport numpy as np\nimport pandas as pd\nimport json\n\n# 设为 None 表示跑所有 Section (全量模式)\nTEST_SECTION_IDX = 1 \n\nprint(\"Step 7: 开始全量 One-Stage 训练...\")\n\n# 【核心配置】\n# 1. 丢弃 90% 的背景帧，保留动作帧\nDOWNSAMPLE_RATIO = 0.1 \n# 2. 硬上限：防止内存溢出，每个 Section 最多练 100 万条\nMAX_TRAIN_SAMPLES = 1000000 \n\ntotal_sections = len(body_parts_tracked_list)\n\nfor section in range(total_sections):\n    # 如果设置了测试索引，跳过其他\n    if TEST_SECTION_IDX is not None and section != TEST_SECTION_IDX: continue\n    \n    bpt_str = body_parts_tracked_list[section]\n    print(f\"\\n{'='*10} Section {section+1}/{total_sections} {'='*10}\")\n    \n    train_subset = train[train.body_parts_tracked == bpt_str]\n    if len(train_subset) == 0: continue\n    \n    try:\n        bpt = json.loads(bpt_str)\n        if len(bpt) > 5: bpt = [b for b in bpt if b not in drop_body_parts]\n    except: continue\n    \n    _fps_lookup = (train_subset[['video_id', 'frames_per_second']]\n                   .drop_duplicates('video_id').set_index('video_id')['frames_per_second'].to_dict())\n\n    for mode in ['single', 'pair']:\n        # 容器清空\n        X_parts, label_parts, group_parts = [], [], []\n        \n        gen = generate_mouse_data(\n            train_subset, 'train', \n            traintest_directory=CFG.train_tracking_path, \n            generate_single=(mode=='single'), generate_pair=(mode=='pair')\n        )\n        \n        count = 0\n        for switch, data, meta, label in gen:\n            if switch != mode: continue\n            try:\n                fps = _fps_from_meta(meta, _fps_lookup)\n                \n                # 特征提取\n                if mode == 'single': Xi = transform_single(data, bpt, fps)\n                else: Xi = transform_pair(data, bpt, fps)\n                \n                # 降采样 (保留动作 + 10%背景)\n                has_action = label.sum(axis=1) > 0\n                keep_mask = has_action | (np.random.rand(len(has_action)) < DOWNSAMPLE_RATIO)\n                if keep_mask.sum() == 0: keep_mask.iloc[0] = True\n                \n                # 截取与对齐检查\n                X_cut = Xi.loc[keep_mask]\n                label_cut = label.loc[keep_mask]\n                group_cut = meta['video_id'].loc[keep_mask]\n                \n                if len(X_cut) != len(group_cut): raise ValueError(\"Mismatch\")\n\n                X_parts.append(X_cut)\n                label_parts.append(label_cut)\n                group_parts.append(group_cut)\n                count += 1\n            except: continue\n            \n        if count == 0: continue\n        \n        # 数据拼接\n        X_all = pd.concat(X_parts, axis=0, ignore_index=True)\n        label_all = pd.concat(label_parts, axis=0, ignore_index=True)\n        groups_all = pd.concat(group_parts, axis=0, ignore_index=True)\n        meta_all = pd.DataFrame({'video_id': groups_all.values})\n        \n        print(f\"  Mode {mode}: Raw Shape {X_all.shape}\")\n        \n        # 【内存保护】强制截断\n        if len(X_all) > MAX_TRAIN_SAMPLES:\n            print(f\"  ⚠️ 数据量过大，随机采样至 {MAX_TRAIN_SAMPLES}...\")\n            indices = np.random.choice(len(X_all), MAX_TRAIN_SAMPLES, replace=False)\n            X_all = X_all.iloc[indices].reset_index(drop=True)\n            label_all = label_all.iloc[indices].reset_index(drop=True)\n            meta_all = meta_all.iloc[indices].reset_index(drop=True)\n            \n        del X_parts, label_parts, group_parts, groups_all; gc.collect()\n        \n        # 训练模型\n        section_name = f\"{section}_{mode}\"\n        train_one_stage_classifiers(X_all, label_all, meta_all, section_name)\n        \n        del X_all, label_all, meta_all; gc.collect()\n        \nprint(\"\\n🎉 Step 7 全量训练完成！\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:20:43.899283Z","iopub.execute_input":"2025-12-25T06:20:43.899555Z","iopub.status.idle":"2025-12-25T06:22:13.832133Z","shell.execute_reply.started":"2025-12-25T06:20:43.899537Z","shell.execute_reply":"2025-12-25T06:22:13.831378Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Step 8: 推理与提交 (One-Stage 最终全量版) =================\nimport joblib\nimport gc\nimport os\nimport numpy as np\nimport pandas as pd\nimport json\nfrom tqdm.auto import tqdm\n\n# 1. 重新定义配置 (防止重启后变量丢失)\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\n# --- 8.1 后处理工具函数 ---\ndef smooth_predictions(df, min_gap=30, min_duration=5):\n    if df.empty: return df\n    df = df.sort_values(['video_id', 'agent_id', 'target_id', 'action', 'start_frame'])\n    refined_rows = []\n    \n    for _, group in df.groupby(['video_id', 'agent_id', 'target_id', 'action']):\n        group = group.to_dict('records')\n        if not group: continue\n        current = group[0]\n        \n        for i in range(1, len(group)):\n            next_event = group[i]\n            gap = next_event['start_frame'] - current['stop_frame']\n            \n            if gap <= min_gap:\n                current['stop_frame'] = max(current['stop_frame'], next_event['stop_frame'])\n            else:\n                if (current['stop_frame'] - current['start_frame']) >= min_duration:\n                    refined_rows.append(current)\n                current = next_event\n        if (current['stop_frame'] - current['start_frame']) >= min_duration:\n            refined_rows.append(current)\n    return pd.DataFrame(refined_rows)\n\ndef robustify(submission, dataset, traintest):\n    submission['start_frame'] = pd.to_numeric(submission['start_frame'], errors='coerce').fillna(0).astype(int)\n    submission['stop_frame'] = pd.to_numeric(submission['stop_frame'], errors='coerce').fillna(0).astype(int)\n    submission = submission[submission.start_frame < submission.stop_frame]\n    \n    predicted_videos = set(submission.video_id.unique()) if not submission.empty else set()\n    dummy_rows = []\n    \n    for _, row in dataset.iterrows():\n        if row.video_id in predicted_videos or str(row.lab_id).startswith('MABe22'): continue\n        dummy_rows.append({\n            'video_id': row.video_id, 'agent_id': 'mouse1', 'target_id': 'mouse2',\n            'action': 'other', 'start_frame': 0, 'stop_frame': 100\n        })\n    if dummy_rows:\n        print(f\"Robustify: Filled {len(dummy_rows)} missing videos.\")\n        submission = pd.concat([submission, pd.DataFrame(dummy_rows)], ignore_index=True)\n    return submission.reset_index(drop=True)\n\n# --- 8.2 主推理程序 ---\nprint(\"Step 8: 开始全量推理 (正式提交模式)...\")\n\n# 自动寻找模型路径\nMODEL_ROOT = CFG.model_name\nif os.path.exists('/kaggle/working/model'): MODEL_ROOT = '/kaggle/working/model'\nelif not os.path.exists(MODEL_ROOT): MODEL_ROOT = '.'\nprint(f\"Model Root: {os.path.abspath(MODEL_ROOT)}\")\n\nsubmission_list = []\nmodels_cache = {}\n\n# 2. 加载所有可用模型 (智能路径兼容)\nprint(\"Loading all models...\")\nfor section in range(len(body_parts_tracked_list)):\n    models_cache[section] = {}\n    for mode in ['single', 'pair']:\n        section_name = f\"{section}_{mode}\"\n        model_dir = f\"{MODEL_ROOT}/{section_name}\"\n        try:\n            # 策略：自动寻找 thresholds.pkl，兼容 stage2 文件夹或根目录\n            th_path = f\"{model_dir}/thresholds.pkl\"\n            \n            if os.path.exists(th_path):\n                thresholds = joblib.load(th_path)\n                models_cache[section][mode] = {'models': {}, 'ths': thresholds}\n                base_dir = os.path.dirname(th_path) # 获取 thresholds 所在的目录\n                \n                for action in thresholds.keys():\n                    m_path = f\"{base_dir}/{action}/models.pkl\"\n                    if os.path.exists(m_path):\n                        models_cache[section][mode]['models'][action] = joblib.load(m_path)\n        except Exception as e: pass\n\nloaded = [k for k in models_cache.keys() if models_cache[k]]\nprint(f\"✅ Loaded Sections: {loaded}\")\n\n# 3. 遍历测试集 (全量)\nfor section in range(len(body_parts_tracked_list)):\n    bpt_str = body_parts_tracked_list[section]\n    test_subset = test[test.body_parts_tracked == bpt_str]\n    if len(test_subset) == 0: continue\n    \n    # 如果这个 Section 我们没训练过，就跳过，让 Robustify 填兜底\n    if section not in models_cache: continue \n    \n    print(f\"Processing Section {section} ({len(test_subset)} videos)...\")\n    try:\n        bpt = json.loads(bpt_str)\n        if len(bpt) > 5: bpt = [b for b in bpt if b not in drop_body_parts]\n    except: continue\n\n    _fps_lookup = (test_subset[['video_id', 'frames_per_second']]\n                   .drop_duplicates('video_id').set_index('video_id')['frames_per_second'].to_dict())\n\n    for mode in ['single', 'pair']:\n        if mode not in models_cache[section] or not models_cache[section][mode]['models']: continue\n        \n        cache = models_cache[section][mode]\n        models_dict = cache['models']\n        \n        gen = generate_mouse_data(test_subset, 'test', traintest_directory=CFG.test_tracking_path, \n                                  generate_single=(mode=='single'), generate_pair=(mode=='pair'))\n        \n        for switch, data, meta, _ in gen:\n            if switch != mode: continue\n            try:\n                fps = _fps_from_meta(meta, _fps_lookup)\n                if mode == 'single': X = transform_single(data, bpt, fps)\n                else: X = transform_pair(data, bpt, fps)\n                \n                # 打分\n                pred_df = pd.DataFrame(index=meta.video_frame)\n                for action, models in models_dict.items():\n                    probs = np.mean([m.predict_proba(X)[:, 1] for m in models], axis=0)\n                    pred_df[action] = probs\n                if pred_df.empty: del X, data, meta; continue\n\n                # 决策\n                best_actions = pred_df.idxmax(axis=1)\n                max_probs = pred_df.max(axis=1)\n                mask = max_probs > 0.15 \n                chosen_actions = best_actions[mask]\n                if chosen_actions.empty: del X, data, meta, pred_df; continue\n\n                # 格式化\n                temp_res = pd.DataFrame({'action': chosen_actions.values, 'frame': chosen_actions.index.values})\n                temp_res['grp'] = ((temp_res['frame'].diff() > 1) | (temp_res['action'] != temp_res['action'].shift())).cumsum()\n                intervals = temp_res.groupby('grp').agg(\n                    action=('action', 'first'), start_frame=('frame', 'min'), stop_frame=('frame', 'max')\n                )\n                intervals['stop_frame'] += 1\n                \n                vid = meta.video_id.iloc[0]; aid = meta.agent_id.iloc[0]; tid = meta.target_id.iloc[0]\n                for _, row in intervals.iterrows():\n                    submission_list.append({\n                        'video_id': vid, 'agent_id': aid, 'target_id': tid,\n                        'action': row['action'], 'start_frame': row['start_frame'], 'stop_frame': row['stop_frame']\n                    })\n                del X, data, meta, pred_df, temp_res, intervals\n            except: continue\n    gc.collect()\n\n# --- 8.3 生成 CSV ---\nprint(\"Generating final CSV...\")\nif len(submission_list) > 0:\n    submission_df = pd.DataFrame(submission_list)\n    submission_df = smooth_predictions(submission_df, min_gap=30, min_duration=5)\nelse:\n    submission_df = pd.DataFrame(columns=['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame'])\n\nfinal_submission = robustify(submission_df, test, 'test')\nfinal_submission.to_csv('submission.csv', index=False)\nprint(f\"🎉 DONE! submission.csv created with {len(final_submission)} rows.\")\nprint(final_submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T06:22:13.833206Z","iopub.execute_input":"2025-12-25T06:22:13.833427Z","iopub.status.idle":"2025-12-25T06:22:14.004424Z","shell.execute_reply.started":"2025-12-25T06:22:13.833408Z","shell.execute_reply":"2025-12-25T06:22:14.003860Z"}},"outputs":[],"execution_count":null}]}