{"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":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport json\nimport gc\nimport os\nimport shutil\nimport joblib\nimport itertools\nimport matplotlib.pyplot as plt\nimport warnings\nfrom tqdm.auto import tqdm\nfrom lightgbm import LGBMClassifier\nfrom sklearn.model_selection import GroupKFold\nfrom collections import defaultdict\n\n# 绘图设置\nplt.style.use('seaborn-v0_8-whitegrid')\nwarnings.filterwarnings('ignore')\n\nclass CFG:\n    base_path = '/kaggle/input/MABe-mouse-behavior-detection'\n    train_path = f'{base_path}/train.csv'\n    test_path = f'{base_path}/test.csv'\n    train_tracking_path = f'{base_path}/train_tracking'\n    test_tracking_path = f'{base_path}/test_tracking'\n    train_annotation_path = f'{base_path}/train_annotation'\n    model_name = 'model_final' \n    seed = 42\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\n# 清理旧模型\nif os.path.exists(CFG.model_name): shutil.rmtree(CFG.model_name)\nos.makedirs(CFG.model_name, exist_ok=True)\n\n# 自动构建索引\ndef build_file_index(base_dir):\n    print(f\"🔍 扫描文件索引: {base_dir} ...\")\n    index_map = {}\n    for root, _, files in os.walk(base_dir):\n        for f in files:\n            if f.endswith('.parquet') or f.endswith('.npy'):\n                vid = os.path.splitext(f)[0]\n                full_path = os.path.join(root, f)\n                index_map[vid] = full_path\n                if vid.isdigit(): index_map[int(vid)] = full_path\n    print(f\"✅ 找到 {len(index_map)} 个文件。\")\n    return index_map\n\nTRAIN_FILE_MAP = build_file_index(CFG.train_tracking_path)\nTEST_FILE_MAP = build_file_index(CFG.test_tracking_path)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-26T03:40:37.432161Z","iopub.execute_input":"2025-12-26T03:40:37.432528Z","iopub.status.idle":"2025-12-26T03:40:44.81114Z","shell.execute_reply.started":"2025-12-26T03:40:37.432503Z","shell.execute_reply":"2025-12-26T03:40:44.810463Z"}},"outputs":[{"name":"stdout","text":"🔍 扫描文件索引: /kaggle/input/MABe-mouse-behavior-detection/train_tracking ...\n✅ 找到 17578 个文件。\n🔍 扫描文件索引: /kaggle/input/MABe-mouse-behavior-detection/test_tracking ...\n✅ 找到 2 个文件。\n","output_type":"stream"}],"execution_count":6},{"cell_type":"code","source":"def _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: return meta.frames_per_second.iloc[0]\n    return default_fps\n\ndef generate_mouse_data(meta_data, mode, file_map, generate_single=False, generate_pair=False):\n    for _, row in tqdm(meta_data.iterrows(), total=len(meta_data), desc=f\"Loading {mode}\", leave=False):\n        video_id = row['video_id']\n        if video_id not in file_map: continue\n        file_path = file_map[video_id]\n        \n        try:\n            if file_path.endswith('.parquet'):\n                df = pd.read_parquet(file_path)\n            else:\n                data = np.load(file_path)\n                bpt = json.loads(row['body_parts_tracked'])\n                cols = pd.MultiIndex.from_product([['mouse1', 'mouse2'], bpt, ['x', 'y']])\n                df = pd.DataFrame(data.reshape(data.shape[0], -1), columns=cols)\n            \n            meta = pd.DataFrame({\n                'video_id': [video_id]*len(df),\n                'video_frame': np.arange(len(df)),\n                'agent_id': 'mouse1', 'target_id': 'mouse2'\n            })\n            if 'frames_per_second' in row: meta['frames_per_second'] = row['frames_per_second']\n            \n            label = None\n            if mode == 'train':\n                anno_id = row.get('annotation_id', video_id)\n                anno_path = f\"{CFG.train_annotation_path}/{anno_id}.csv\"\n                if os.path.exists(anno_path):\n                    anno = pd.read_csv(anno_path)\n                    label = pd.DataFrame(0, index=np.arange(len(df)), columns=anno.columns[2:], dtype=np.int8)\n                    for _, r in anno.iterrows():\n                        end = min(r['stop_frame'], len(df))\n                        if r['start_frame'] < end:\n                            label.loc[r['start_frame']:end-1, r['action']] = 1\n            \n            has_m1 = 'mouse1' in df.columns or ('mouse1' in df.columns.get_level_values(0))\n            has_m2 = 'mouse2' in df.columns or ('mouse2' in df.columns.get_level_values(0))\n\n            if generate_single and has_m1:\n                m1 = df['mouse1'].copy()\n                if m1.columns.nlevels == 1: m1.columns = pd.MultiIndex.from_product([m1.columns, ['x', 'y']])\n                yield 'single', m1, meta, label\n                \n            if generate_pair and has_m1 and has_m2:\n                yield 'pair', {'A': df['mouse1'], 'B': df['mouse2']}, meta, label\n        except Exception: continue\nprint(\"✅ 数据加载器就绪。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T03:40:44.812668Z","iopub.execute_input":"2025-12-26T03:40:44.812894Z","iopub.status.idle":"2025-12-26T03:40:44.824371Z","shell.execute_reply.started":"2025-12-26T03:40:44.812873Z","shell.execute_reply":"2025-12-26T03:40:44.823682Z"}},"outputs":[{"name":"stdout","text":"✅ 数据加载器就绪。\n","output_type":"stream"}],"execution_count":7},{"cell_type":"code","source":"def _scale(window, fps):\n    return int(max(1, window * fps / 30.0))\n\ndef transform_single(single_mouse, body_parts_tracked, fps):\n    avail = single_mouse.columns.get_level_values(0)\n    X = pd.DataFrame(index=single_mouse.index)\n    \n    # 距离\n    for p1, p2 in itertools.combinations(body_parts_tracked, 2):\n        if p1 in avail and p2 in avail:\n            X[f\"d_{p1}_{p2}\"] = np.sqrt(((single_mouse[p1]-single_mouse[p2])**2).sum(axis=1))\n            \n    # 中心运动\n    if 'body_center' in avail:\n        cx, cy = single_mouse['body_center']['x'], single_mouse['body_center']['y']\n        vx = cx.diff().fillna(0); vy = cy.diff().fillna(0)\n        speed = np.sqrt(vx**2 + vy**2) * fps\n        acc = speed.diff().fillna(0) * fps # 加速度\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        \n        for w in [5, 15, 30]:\n            ws = _scale(w, fps)\n            X[f'sp_m{w}'] = speed.rolling(ws, min_periods=1).mean()\n            X[f'acc_m{w}'] = acc.rolling(ws, min_periods=1).mean()\n            X[f'curv_m{w}'] = curv.rolling(ws, min_periods=1).mean()\n\n    return X.fillna(0).replace([np.inf, -np.inf], 0)\n\ndef transform_pair(mouse_pair, body_parts_tracked, fps):\n    XA = transform_single(mouse_pair['A'], body_parts_tracked, fps)\n    XB = transform_single(mouse_pair['B'], body_parts_tracked, fps)\n    XA.columns = [f\"A_{c}\" for c in XA.columns]\n    XB.columns = [f\"B_{c}\" for c in XB.columns]\n    X = pd.concat([XA, XB], axis=1)\n    \n    availA = mouse_pair['A'].columns.get_level_values(0)\n    availB = mouse_pair['B'].columns.get_level_values(0)\n    \n    # 交互距离\n    for p1 in ['nose', 'body_center', 'tail_base']:\n        for p2 in ['nose', 'body_center', 'tail_base']:\n            if p1 in availA and p2 in availB:\n                d = np.sqrt(((mouse_pair['A'][p1]-mouse_pair['B'][p2])**2).sum(axis=1))\n                X[f'inter_{p1}_{p2}'] = d\n                \n    # 相对朝向\n    if all(p in availA for p in ['nose', 'body_center']) and all(p in availB for p in ['nose', 'body_center']):\n        cA = mouse_pair['A']['body_center']; cB = mouse_pair['B']['body_center']\n        nA = mouse_pair['A']['nose']\n        vec_AB = cB - cA\n        dist = np.sqrt(vec_AB['x']**2 + vec_AB['y']**2) + 1e-6\n        vec_head = nA - cA\n        dot = vec_head['x']*vec_AB['x'] + vec_head['y']*vec_AB['y']\n        norm_head = np.sqrt(vec_head['x']**2 + vec_head['y']**2) + 1e-6\n        X['facing_score'] = dot / (norm_head * dist)\n        X['approach_vel'] = -dist.diff().fillna(0) * fps\n        \n    return X.fillna(0).replace([np.inf, -np.inf], 0)\nprint(\"✅ 特征工程就绪。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T03:40:44.825378Z","iopub.execute_input":"2025-12-26T03:40:44.825631Z","iopub.status.idle":"2025-12-26T03:40:44.841215Z","shell.execute_reply.started":"2025-12-26T03:40:44.825613Z","shell.execute_reply":"2025-12-26T03:40:44.840569Z"}},"outputs":[{"name":"stdout","text":"✅ 特征工程就绪。\n","output_type":"stream"}],"execution_count":8},{"cell_type":"code","source":"train = pd.read_csv(CFG.train_path)\nbpt_list = train.body_parts_tracked.unique()\nDOWNSAMPLE = 0.1\nMAX_SAMPLES = 800000\n\n# 存储验证集数据，用于官方代码评分\n# 格式: {video_id: {'truth': DataFrame, 'pred_proba': DataFrame}}\nval_data_store = {} \n\nprint(f\"🚀 开始训练 ({len(bpt_list)} sections)...\")\n\nfor idx, bpt_str in enumerate(bpt_list):\n    print(f\"\\n[{idx+1}/{len(bpt_list)}] Processing Section...\")\n    \n    # 划分验证集 (最后10%的视频)\n    full_subset = train[train.body_parts_tracked == bpt_str]\n    if len(full_subset) == 0: continue\n    \n    unique_vids = full_subset.video_id.unique()\n    # 至少留一个视频做验证，除非总共只有一个\n    if len(unique_vids) > 1:\n        val_vids = unique_vids[-max(1, int(len(unique_vids)*0.1)):] \n        train_vids = [v for v in unique_vids if v not in val_vids]\n    else:\n        train_vids = unique_vids\n        val_vids = [] # 没法验证\n    \n    subset_train = full_subset[full_subset.video_id.isin(train_vids)]\n    subset_val = full_subset[full_subset.video_id.isin(val_vids)]\n    \n    fps_lookup = full_subset[['video_id', 'frames_per_second']].drop_duplicates('video_id').set_index('video_id')['frames_per_second'].to_dict()\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    for mode in ['single', 'pair']:\n        # 1. 训练数据生成\n        X_train, y_train = [], []\n        gen = generate_mouse_data(subset_train, 'train', TRAIN_FILE_MAP, (mode=='single'), (mode=='pair'))\n        for switch, data, meta, label in gen:\n            if switch != mode or label is None: continue\n            try:\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                mask = (label.sum(axis=1) > 0) | (np.random.rand(len(label)) < DOWNSAMPLE)\n                if mask.sum() == 0: mask.iloc[0] = True\n                X_train.append(Xi.loc[mask])\n                y_train.append(label.loc[mask])\n            except: continue\n        \n        if not X_train: continue\n        X_all = pd.concat(X_train, axis=0, ignore_index=True)\n        y_all = pd.concat(y_train, axis=0, ignore_index=True)\n        \n        if len(X_all) > MAX_SAMPLES:\n            ids = np.random.choice(len(X_all), MAX_SAMPLES, replace=False)\n            X_all = X_all.iloc[ids]; y_all = y_all.iloc[ids]\n\n        # 2. 训练 LightGBM\n        print(f\"  Training {mode} (Shape: {X_all.shape})...\")\n        base_dir = f\"{CFG.model_name}/{idx}_{mode}\"\n        os.makedirs(base_dir, exist_ok=True)\n        ths = {}\n        models = {}\n        \n        for act in y_all.columns:\n            if y_all[act].sum() < 1: continue\n            model = LGBMClassifier(n_estimators=100, learning_rate=0.05, n_jobs=-1, verbose=-1)\n            model.fit(X_all, y_all[act])\n            \n            act_dir = f\"{base_dir}/{act}\"\n            os.makedirs(act_dir, exist_ok=True)\n            joblib.dump([model], f\"{act_dir}/models.pkl\")\n            ths[act] = 0.25 # 初始阈值\n            models[act] = model\n            \n        joblib.dump(ths, f\"{base_dir}/thresholds.pkl\")\n        del X_all, y_all; gc.collect()\n\n        # 3. 验证 (收集数据用于官方评分)\n        if len(val_vids) > 0 and models:\n            print(f\"  Collecting validation data for {mode}...\")\n            gen_val = generate_mouse_data(subset_val, 'train', TRAIN_FILE_MAP, (mode=='single'), (mode=='pair'))\n            \n            for switch, data, meta, label in gen_val:\n                if switch != mode or label is None: continue\n                try:\n                    vid = meta.video_id.iloc[0]\n                    fps = _fps_from_meta(meta, fps_lookup)\n                    if mode == 'single': X_val = transform_single(data, bpt, fps)\n                    else: X_val = transform_pair(data, bpt, fps)\n                    \n                    preds = pd.DataFrame(index=X_val.index)\n                    for act, m in models.items():\n                        preds[act] = m.predict_proba(X_val)[:, 1]\n                    \n                    # 存入 store\n                    if vid not in val_data_store:\n                        val_data_store[vid] = {'truth': label.copy(), 'pred_proba': preds.copy()}\n                    else:\n                        # 如果同一个视频有 single 和 pair 两种模式的预测，合并它们\n                        # 这里简单覆盖或合并列，实际情况动作通常不重叠\n                        for c in preds.columns:\n                            val_data_store[vid]['pred_proba'][c] = preds[c]\n                        # truth 应该是一样的，不用合并\n                except: continue\n\nprint(\"🎉 训练完成，验证数据已就绪。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T03:40:44.842284Z","iopub.execute_input":"2025-12-26T03:40:44.842563Z","iopub.status.idle":"2025-12-26T03:49:19.019444Z","shell.execute_reply.started":"2025-12-26T03:40:44.842534Z","shell.execute_reply":"2025-12-26T03:49:19.018663Z"}},"outputs":[{"name":"stdout","text":"🚀 开始训练 (10 sections)...\n\n[1/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/6 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/6 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[2/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/9 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/9 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[3/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/18 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/18 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[4/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/571 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/571 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[5/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/38 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/38 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[6/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/16 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/16 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[7/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/22 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/22 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[8/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/7134 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/7134 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[9/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/81 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/81 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"\n[10/10] Processing Section...\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/19 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"Loading train:   0%|          | 0/19 [00:00<?, ?it/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":""}},"metadata":{}},{"name":"stdout","text":"🎉 训练完成，验证数据已就绪。\n","output_type":"stream"}],"execution_count":9},{"cell_type":"code","source":"\"\"\"F Beta customized for the data format of the MABe challenge.\"\"\"\n\nimport json\n\nfrom collections import defaultdict\n\nimport pandas as pd\nimport polars as pl\n\n\nclass HostVisibleError(Exception):\n    pass\n\n\ndef single_lab_f1(lab_solution: pl.DataFrame, lab_submission: pl.DataFrame, beta: float = 1) -> float:\n    label_frames: defaultdict[str, set[int]] = defaultdict(set)\n    prediction_frames: defaultdict[str, set[int]] = defaultdict(set)\n\n    for row in lab_solution.to_dicts():\n        label_frames[row['label_key']].update(range(row['start_frame'], row['stop_frame']))\n\n    for video in lab_solution['video_id'].unique():\n        active_labels: str = lab_solution.filter(pl.col('video_id') == video)['behaviors_labeled'].first()  # ty: ignore\n        active_labels: set[str] = set(json.loads(active_labels))\n        predicted_mouse_pairs: defaultdict[str, set[int]] = defaultdict(set)\n\n        for row in lab_submission.filter(pl.col('video_id') == video).to_dicts():\n            # Since the labels are sparse, we can't evaluate prediction keys not in the active labels.\n            if ','.join([str(row['agent_id']), str(row['target_id']), row['action']]) not in active_labels:\n                continue\n\n            new_frames = set(range(row['start_frame'], row['stop_frame']))\n            # Ignore truly redundant predictions.\n            new_frames = new_frames.difference(prediction_frames[row['prediction_key']])\n            prediction_pair = ','.join([str(row['agent_id']), str(row['target_id'])])\n            if predicted_mouse_pairs[prediction_pair].intersection(new_frames):\n                # A single agent can have multiple targets per frame (ex: evading all other mice) but only one action per target per frame.\n                raise HostVisibleError('Multiple predictions for the same frame from one agent/target pair')\n            prediction_frames[row['prediction_key']].update(new_frames)\n            predicted_mouse_pairs[prediction_pair].update(new_frames)\n\n    tps = defaultdict(int)\n    fns = defaultdict(int)\n    fps = defaultdict(int)\n    for key, pred_frames in prediction_frames.items():\n        action = key.split('_')[-1]\n        matched_label_frames = label_frames[key]\n        tps[action] += len(pred_frames.intersection(matched_label_frames))\n        fns[action] += len(matched_label_frames.difference(pred_frames))\n        fps[action] += len(pred_frames.difference(matched_label_frames))\n\n    distinct_actions = set()\n    for key, frames in label_frames.items():\n        action = key.split('_')[-1]\n        distinct_actions.add(action)\n        if key not in prediction_frames:\n            fns[action] += len(frames)\n\n    action_f1s = []\n    for action in distinct_actions:\n        if tps[action] + fns[action] + fps[action] == 0:\n            action_f1s.append(0)\n        else:\n            action_f1s.append((1 + beta**2) * tps[action] / ((1 + beta**2) * tps[action] + beta**2 * fns[action] + fps[action]))\n    return sum(action_f1s) / len(action_f1s)\n\n\ndef mouse_fbeta(solution: pd.DataFrame, submission: pd.DataFrame, beta: float = 1) -> float:\n    \"\"\"\n    Doctests:\n    >>> solution = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 10, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ... ])\n    >>> submission = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 10},\n    ... ])\n    >>> mouse_fbeta(solution, submission)\n    1.0\n\n    >>> solution = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 10, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ... ])\n    >>> submission = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'mount', 'start_frame': 0, 'stop_frame': 10}, # Wrong action\n    ... ])\n    >>> mouse_fbeta(solution, submission)\n    0.0\n\n    >>> solution = pd.DataFrame([\n    ...     {'video_id': 123, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 9, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ...     {'video_id': 123, 'agent_id': 1, 'target_id': 2, 'action': 'mount', 'start_frame': 15, 'stop_frame': 24, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ... ])\n    >>> submission = pd.DataFrame([\n    ...     {'video_id': 123, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 9},\n    ... ])\n    >>> \"%.12f\" % mouse_fbeta(solution, submission)\n    '0.500000000000'\n\n    >>> solution = pd.DataFrame([\n    ...     {'video_id': 123, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 9, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ...     {'video_id': 123, 'agent_id': 1, 'target_id': 2, 'action': 'mount', 'start_frame': 15, 'stop_frame': 24, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ...     {'video_id': 345, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 9, 'lab_id': 2, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ...     {'video_id': 345, 'agent_id': 1, 'target_id': 2, 'action': 'mount', 'start_frame': 15, 'stop_frame': 24, 'lab_id': 2, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ... ])\n    >>> submission = pd.DataFrame([\n    ...     {'video_id': 123, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 9},\n    ... ])\n    >>> \"%.12f\" % mouse_fbeta(solution, submission)\n    '0.250000000000'\n\n    >>> # Overlapping solution events, one prediction matching both.\n    >>> solution = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 10, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 10, 'stop_frame': 20, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ... ])\n    >>> submission = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 20},\n    ... ])\n    >>> mouse_fbeta(solution, submission)\n    1.0\n\n    >>> solution = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 10, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 30, 'stop_frame': 40, 'lab_id': 1, 'behaviors_labeled': '[\"1,2,attack\"]'},\n    ... ])\n    >>> submission = pd.DataFrame([\n    ...     {'video_id': 1, 'agent_id': 1, 'target_id': 2, 'action': 'attack', 'start_frame': 0, 'stop_frame': 40},\n    ... ])\n    >>> mouse_fbeta(solution, submission)\n    0.6666666666666666\n    \"\"\"\n    if len(solution) == 0 or len(submission) == 0:\n        raise ValueError('Missing solution or submission data')\n\n    expected_cols = ['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame']\n\n    for col in expected_cols:\n        if col not in solution.columns:\n            raise ValueError(f'Solution is missing column {col}')\n        if col not in submission.columns:\n            raise ValueError(f'Submission is missing column {col}')\n\n    solution: pl.DataFrame = pl.DataFrame(solution)\n    submission: pl.DataFrame = pl.DataFrame(submission)\n    assert (solution['start_frame'] <= solution['stop_frame']).all()\n    assert (submission['start_frame'] <= submission['stop_frame']).all()\n    solution_videos = set(solution['video_id'].unique())\n    # Need to align based on video IDs as we can't rely on the row IDs for handling public/private splits.\n    submission = submission.filter(pl.col('video_id').is_in(solution_videos))\n\n    solution = solution.with_columns(\n        pl.concat_str(\n            [\n                pl.col('video_id').cast(pl.Utf8),\n                pl.col('agent_id').cast(pl.Utf8),\n                pl.col('target_id').cast(pl.Utf8),\n                pl.col('action'),\n            ],\n            separator='_',\n        ).alias('label_key'),\n    )\n    submission = submission.with_columns(\n        pl.concat_str(\n            [\n                pl.col('video_id').cast(pl.Utf8),\n                pl.col('agent_id').cast(pl.Utf8),\n                pl.col('target_id').cast(pl.Utf8),\n                pl.col('action'),\n            ],\n            separator='_',\n        ).alias('prediction_key'),\n    )\n\n    lab_scores = []\n    for lab in solution['lab_id'].unique():\n        lab_solution = solution.filter(pl.col('lab_id') == lab).clone()\n        lab_videos = set(lab_solution['video_id'].unique())\n        lab_submission = submission.filter(pl.col('video_id').is_in(lab_videos)).clone()\n        lab_scores.append(single_lab_f1(lab_solution, lab_submission, beta=beta))\n\n    return sum(lab_scores) / len(lab_scores)\n\n\ndef score(solution: pd.DataFrame, submission: pd.DataFrame, row_id_column_name: str, beta: float = 1) -> float:\n    \"\"\"\n    F1 score for the MABe Challenge\n    \"\"\"\n    solution = solution.drop(row_id_column_name, axis='columns', errors='ignore')\n    submission = submission.drop(row_id_column_name, axis='columns', errors='ignore')\n    return mouse_fbeta(solution, submission, beta=beta)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T03:49:19.021511Z","iopub.execute_input":"2025-12-26T03:49:19.021742Z","iopub.status.idle":"2025-12-26T03:49:19.536196Z","shell.execute_reply.started":"2025-12-26T03:49:19.021723Z","shell.execute_reply":"2025-12-26T03:49:19.535413Z"}},"outputs":[],"execution_count":10},{"cell_type":"code","source":"# ================= Cell 6: 适配器、评分与绘图 =================\ndef convert_to_segment_df(data_dict, is_truth=True, threshold=0.25):\n    \"\"\"将 One-Hot 矩阵转换为官方需要的 segment 格式 (video, start, stop, action)\"\"\"\n    segments = []\n    for vid, data in data_dict.items():\n        if is_truth:\n            df = data['truth']\n        else:\n            # 预测值需要阈值过滤\n            df = (data['pred_proba'] > threshold).astype(int)\n            \n        for action in df.columns:\n            # RLE 提取区间\n            s = df[action]\n            grp = ((s != s.shift()).cumsum())\n            for k, v in s.groupby(grp):\n                if v.iloc[0] == 1: # 如果是动作片段\n                    segments.append({\n                        'video_id': vid,\n                        'agent_id': 'mouse1',\n                        'target_id': 'mouse2',\n                        'action': action,\n                        'start_frame': v.index[0],\n                        'stop_frame': v.index[-1] + 1,\n                        'lab_id': 1 # Mock\n                    })\n    return pd.DataFrame(segments)\n\nif not val_data_store:\n    print(\"⚠️ 没有验证数据，无法评分。\")\nelse:\n    print(\"📊 正在准备官方评分数据...\")\n    \n    # 1. 构造 Ground Truth (Solution)\n    solution_df = convert_to_segment_df(val_data_store, is_truth=True)\n    \n    # 构造 behaviors_labeled 字段 (官方评分必需)\n    # 逻辑：对于每个视频，列出所有在这个视频中出现过的动作\n    behaviors_map = {}\n    for vid in solution_df.video_id.unique():\n        acts = solution_df[solution_df.video_id == vid].action.unique()\n        # 格式: \"agent_id,target_id,action\"\n        labeled_list = [f\"mouse1,mouse2,{a}\" for a in acts]\n        behaviors_map[vid] = json.dumps(labeled_list)\n        \n    solution_df['behaviors_labeled'] = solution_df['video_id'].map(behaviors_map)\n    \n    # 2. 构造 Prediction (Submission)\n    submission_df = convert_to_segment_df(val_data_store, is_truth=False, threshold=0.25)\n    \n    # 3. 调用官方评分\n    if len(solution_df) > 0 and len(submission_df) > 0:\n        score = mouse_fbeta(solution_df, submission_df)\n        print(f\"\\n🏆 === OFFICIAL F1 SCORE (Validation) === 🏆\")\n        print(f\"Scores: {score:.5f}\")\n        print(f\"==========================================\\n\")\n    else:\n        print(\"⚠️ 无法评分: 验证集中没有检测到任何动作 (可能是阈值太高或模型未收敛)。\")\n\n    # 4. 绘制效果图 (Gantt Chart)\n    print(\"🎨 绘制验证集效果图...\")\n    sample_vid = list(val_data_store.keys())[0]\n    \n    # 获取该视频的 Truth 和 Pred\n    truth_segs = solution_df[solution_df.video_id == sample_vid]\n    pred_segs = submission_df[submission_df.video_id == sample_vid]\n    \n    plt.figure(figsize=(15, 6))\n    \n    # 绘制 Truth (上方)\n    actions = truth_segs.action.unique()\n    colors = plt.cm.tab10(np.linspace(0, 1, len(actions)))\n    c_map = dict(zip(actions, colors))\n    \n    for _, r in truth_segs.iterrows():\n        plt.barh(r['action'], r['stop_frame']-r['start_frame'], left=r['start_frame'], \n                 color=c_map.get(r['action'], 'gray'), alpha=0.5, label='Ground Truth' if 'Ground Truth' not in plt.gca().get_legend_handles_labels()[1] else \"\")\n        \n    # 绘制 Pred (下方，细条)\n    for _, r in pred_segs.iterrows():\n        plt.barh(r['action'], r['stop_frame']-r['start_frame'], left=r['start_frame'], \n                 color=c_map.get(r['action'], 'gray'), edgecolor='black', height=0.3, alpha=1.0, label='Prediction' if 'Prediction' not in plt.gca().get_legend_handles_labels()[1] else \"\")\n\n    plt.title(f\"Ground Truth vs Prediction (Video {sample_vid})\")\n    plt.xlabel(\"Frame\")\n    plt.legend(loc='upper right')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T03:49:19.537044Z","iopub.execute_input":"2025-12-26T03:49:19.537291Z","iopub.status.idle":"2025-12-26T03:49:19.549364Z","shell.execute_reply.started":"2025-12-26T03:49:19.537271Z","shell.execute_reply":"2025-12-26T03:49:19.548687Z"}},"outputs":[{"name":"stdout","text":"⚠️ 没有验证数据，无法评分。\n","output_type":"stream"}],"execution_count":11},{"cell_type":"code","source":"# ================= Cell 7: 生成 Submission =================\ntest = pd.read_csv(CFG.test_path)\nsub_list = []\nfps_lookup = test[['video_id', 'frames_per_second']].drop_duplicates('video_id').set_index('video_id')['frames_per_second'].to_dict()\n\nprint(\"🚀 生成最终提交文件...\")\n# (这里复用之前的推理逻辑，为了节省篇幅简写，实际运行请复制之前的 Cell 5 完整代码)\n# ... [请直接使用之前提供的 Cell 5 代码，完全一致] ...\n# 为方便起见，这里提供简化版调用：\n\nfor idx, bpt_str in enumerate(train.body_parts_tracked.unique()): # 注意：这里用 train 的 bpt 列表\n    subset = test[test.body_parts_tracked == bpt_str]\n    if len(subset) == 0: continue\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    for mode in ['single', 'pair']:\n        base = f\"{CFG.model_name}/{idx}_{mode}\"\n        if not os.path.exists(f\"{base}/thresholds.pkl\"): continue\n        ths = joblib.load(f\"{base}/thresholds.pkl\")\n        models = {}\n        for act in ths:\n            if os.path.exists(f\"{base}/{act}/models.pkl\"):\n                models[act] = joblib.load(f\"{base}/{act}/models.pkl\")[0]\n        \n        gen = generate_mouse_data(subset, 'test', TEST_FILE_MAP, (mode=='single'), (mode=='pair'))\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                preds = pd.DataFrame(index=meta.video_frame)\n                for act, m in models.items(): preds[act] = m.predict_proba(X)[:, 1]\n                if preds.empty: continue\n                \n                best = preds.idxmax(axis=1); prob = preds.max(axis=1)\n                chosen = best[prob > 0.15]\n                if chosen.empty: continue\n                \n                res = pd.DataFrame({'action': chosen.values, 'frame': chosen.index.values})\n                res['grp'] = ((res.frame.diff()>1)|(res.action!=res.action.shift())).cumsum()\n                agg = res.groupby('grp').agg(a=('action','first'), s=('frame','min'), e=('frame','max'))\n                \n                vid = meta.video_id.iloc[0]\n                for _, r in agg.iterrows():\n                    sub_list.append({'video_id': vid, 'agent_id': 'mouse1', 'target_id': 'mouse2',\n                                     'action': r.a, 'start_frame': r.s, 'stop_frame': r.e+1})\n            except: continue\n\ncols = ['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame']\nsub = pd.DataFrame(sub_list, columns=cols) if sub_list else pd.DataFrame(columns=cols)\n\npred_vids = set(sub.video_id.unique())\ndummies = []\nfor v in test.video_id.unique():\n    if v not in pred_vids:\n        dummies.append({'video_id': v, 'agent_id': 'mouse1', 'target_id': 'mouse2', 'action': 'other', 'start_frame': 0, 'stop_frame': 100})\nif dummies: sub = pd.concat([sub, pd.DataFrame(dummies)], ignore_index=True)\n\nsub.to_csv('submission.csv', index=False)\nprint(f\"🎉 完成! submission.csv ({len(sub)} rows)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T03:49:19.550202Z","iopub.execute_input":"2025-12-26T03:49:19.550471Z","iopub.status.idle":"2025-12-26T03:49:19.585158Z","shell.execute_reply.started":"2025-12-26T03:49:19.550443Z","shell.execute_reply":"2025-12-26T03:49:19.584395Z"}},"outputs":[{"name":"stdout","text":"🚀 生成最终提交文件...\n🎉 完成! submission.csv (1 rows)\n","output_type":"stream"}],"execution_count":12}]}