{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"},{"sourceId":262477103,"sourceType":"kernelVersion"},{"sourceId":279806245,"sourceType":"kernelVersion"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# MABe Challenge - XGBoost training Notebook","metadata":{}},{"cell_type":"markdown","source":"📝 **Note:** Please note that comments and explanations are in Japanese. However, I've made an effort to write clear, self-explanatory code that should be accessible to non-Japanese speakers as well.","metadata":{}},{"cell_type":"markdown","source":"## inference and submission notebook: \nhttps://www.kaggle.com/code/hutch1221/mabe-starter-inference-ja/notebook","metadata":{}},{"cell_type":"code","source":"!pip install -q --no-index --find-links=/kaggle/input/mabe-package xgboost==3.1.1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:26:36.200045Z","iopub.execute_input":"2025-11-24T19:26:36.200368Z","iopub.status.idle":"2025-11-24T19:26:49.005363Z","shell.execute_reply.started":"2025-11-24T19:26:36.200343Z","shell.execute_reply":"2025-11-24T19:26:49.004177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import datetime\nimport gc\nimport itertools\nimport json\nimport re\nimport sys\nimport time\nimport traceback\nfrom collections import defaultdict\nfrom pathlib import Path\n\nimport joblib\nimport lightgbm as lgb\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport xgboost as xgb\nfrom sklearn.metrics import f1_score\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom tqdm.auto import tqdm\n\nsys.path.append(\"/kaggle/usr/lib/mabe-f-beta\")\nfrom metric import score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:26:49.007027Z","iopub.execute_input":"2025-11-24T19:26:49.007365Z","iopub.status.idle":"2025-11-24T19:26:56.746570Z","shell.execute_reply.started":"2025-11-24T19:26:49.007326Z","shell.execute_reply":"2025-11-24T19:26:56.745751Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# const\nINPUT_DIR = Path(\"/kaggle/input/MABe-mouse-behavior-detection\")\nTRAIN_TRACKING_DIR = INPUT_DIR / \"train_tracking\"\nTRAIN_ANNOTATION_DIR = INPUT_DIR / \"train_annotation\"\nTEST_TRACKING_DIR = INPUT_DIR / \"test_tracking\"\n\nWORKING_DIR = Path(\"/kaggle/working\")\n\nINDEX_COLS = [\n    \"video_id\",\n    \"agent_mouse_id\",\n    \"target_mouse_id\",\n    \"video_frame\",\n]\n\nBODY_PARTS = [\n    \"ear_left\",\n    \"ear_right\",\n    \"nose\",\n    \"neck\",\n    \"body_center\",\n    \"lateral_left\",\n    \"lateral_right\",\n    \"hip_left\",\n    \"hip_right\",\n    \"tail_base\",\n    \"tail_tip\",\n]\n\nSELF_BEHAVIORS = [\n    \"biteobject\",\n    \"climb\",\n    \"dig\",\n    \"exploreobject\",\n    \"freeze\",\n    \"genitalgroom\",\n    \"huddle\",\n    \"rear\",\n    \"rest\",\n    \"run\",\n    \"selfgroom\",\n]\n\nPAIR_BEHAVIORS = [\n    \"allogroom\",\n    \"approach\",\n    \"attack\",\n    \"attemptmount\",\n    \"avoid\",\n    \"chase\",\n    \"chaseattack\",\n    \"defend\",\n    \"disengage\",\n    \"dominance\",\n    \"dominancegroom\",\n    \"dominancemount\",\n    \"ejaculate\",\n    \"escape\",\n    \"flinch\",\n    \"follow\",\n    \"intromit\",\n    \"mount\",\n    \"reciprocalsniff\",\n    \"shepherd\",\n    \"sniff\",\n    \"sniffbody\",\n    \"sniffface\",\n    \"sniffgenital\",\n    \"submit\",\n    \"tussle\",\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:26:56.747517Z","iopub.execute_input":"2025-11-24T19:26:56.748251Z","iopub.status.idle":"2025-11-24T19:26:56.754829Z","shell.execute_reply.started":"2025-11-24T19:26:56.748222Z","shell.execute_reply":"2025-11-24T19:26:56.754087Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# read data\ntrain_dataframe = pl.read_csv(INPUT_DIR / \"train.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:26:56.756839Z","iopub.execute_input":"2025-11-24T19:26:56.757143Z","iopub.status.idle":"2025-11-24T19:26:56.914494Z","shell.execute_reply.started":"2025-11-24T19:26:56.757120Z","shell.execute_reply":"2025-11-24T19:26:56.913534Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# preprocess behavior labels\ntrain_behavior_dataframe = (\n    train_dataframe.filter(pl.col(\"behaviors_labeled\").is_not_null())\n    .select(\n        pl.col(\"lab_id\"),\n        pl.col(\"video_id\"),\n        pl.col(\"behaviors_labeled\").map_elements(eval, return_dtype=pl.List(pl.Utf8)).alias(\"behaviors_labeled_list\"),\n    )\n    .explode(\"behaviors_labeled_list\")\n    .rename({\"behaviors_labeled_list\": \"behaviors_labeled_element\"})\n    .select(\n        pl.col(\"lab_id\"),\n        pl.col(\"video_id\"),\n        pl.col(\"behaviors_labeled_element\").str.split(\",\").list[0].str.replace_all(\"'\", \"\").alias(\"agent\"),\n        pl.col(\"behaviors_labeled_element\").str.split(\",\").list[1].str.replace_all(\"'\", \"\").alias(\"target\"),\n        pl.col(\"behaviors_labeled_element\").str.split(\",\").list[2].str.replace_all(\"'\", \"\").alias(\"behavior\"),\n    )\n)\n\ntrain_self_behavior_dataframe = train_behavior_dataframe.filter(pl.col(\"behavior\").is_in(SELF_BEHAVIORS))\ntrain_pair_behavior_dataframe = train_behavior_dataframe.filter(pl.col(\"behavior\").is_in(PAIR_BEHAVIORS))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:26:56.915664Z","iopub.execute_input":"2025-11-24T19:26:56.915935Z","iopub.status.idle":"2025-11-24T19:26:57.170881Z","shell.execute_reply.started":"2025-11-24T19:26:56.915912Z","shell.execute_reply":"2025-11-24T19:26:57.169830Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 特徴量加工(self)\n- agentの体の各部位間距離(cm)\n\n  主要な11部位(定数: BODY_PARTS)の組み合わせの距離\n  \n- 部位の推定速度(cm/s)\n\n  ear_left, ear_right, tail_base の 500, 1000, 2000, 3000ms間の推定速度\n  \n- 伸長度\n\n  (nose-tail_baseの距離) / (ear_left-ear_rightの距離)\n\n- 体角度(deg)\n  \n  nose-body_center, body_center-tail_base　がなすベクトルの角度","metadata":{}},{"cell_type":"code","source":"%%writefile self_features.py\n\ndef make_self_features(\n    metadata: dict,\n    tracking: pl.DataFrame,\n) -> pl.DataFrame:\n    def body_parts_distance(body_part_1, body_part_2):\n        # agentの体の各部位間距離(cm)\n        assert body_part_1 in BODY_PARTS\n        assert body_part_2 in BODY_PARTS\n        return (\n            (pl.col(f\"agent_x_{body_part_1}\") - pl.col(f\"agent_x_{body_part_2}\")).pow(2)\n            + (pl.col(f\"agent_y_{body_part_1}\") - pl.col(f\"agent_y_{body_part_2}\")).pow(2)\n        ).sqrt() / metadata[\"pix_per_cm_approx\"]\n\n    def body_part_speed(body_part, period_ms):\n        # 部位の推定速度(cm/s)\n        assert body_part in BODY_PARTS\n        window_frames = max(1, int(round(period_ms * metadata[\"frames_per_second\"] / 1000.0)))\n        return (\n            ((pl.col(f\"agent_x_{body_part}\").diff()).pow(2) + (pl.col(f\"agent_y_{body_part}\").diff()).pow(2)).sqrt()\n            / metadata[\"pix_per_cm_approx\"]\n            * metadata[\"frames_per_second\"]\n        ).rolling_mean(window_size=window_frames, center=True, min_samples=1)\n\n    def elongation():\n        # 伸長度\n        d1 = body_parts_distance(\"nose\", \"tail_base\")\n        d2 = body_parts_distance(\"ear_left\", \"ear_right\")\n        return d1 / (d2 + 1e-06)\n\n    def body_angle():\n        # 体角度(deg)\n        v1x = pl.col(\"agent_x_nose\") - pl.col(\"agent_x_body_center\")\n        v1y = pl.col(\"agent_y_nose\") - pl.col(\"agent_y_body_center\")\n        v2x = pl.col(\"agent_x_tail_base\") - pl.col(\"agent_x_body_center\")\n        v2y = pl.col(\"agent_y_tail_base\") - pl.col(\"agent_y_body_center\")\n        return (v1x * v2x + v1y * v2y) / ((v1x.pow(2) + v1y.pow(2)).sqrt() * (v2x.pow(2) + v2y.pow(2)).sqrt() + 1e-06)\n\n    n_mice = (\n        (metadata[\"mouse1_strain\"] is not None)\n        + (metadata[\"mouse2_strain\"] is not None)\n        + (metadata[\"mouse3_strain\"] is not None)\n        + (metadata[\"mouse4_strain\"] is not None)\n    )\n    start_frame = tracking.select(pl.col(\"video_frame\").min()).item()\n    end_frame = tracking.select(pl.col(\"video_frame\").max()).item()\n\n    result = []\n\n    pivot = tracking.pivot(\n        on=[\"bodypart\"],\n        index=[\"video_frame\", \"mouse_id\"],\n        values=[\"x\", \"y\"],\n    ).sort([\"mouse_id\", \"video_frame\"])\n    pivot_trackings = {mouse_id: pivot.filter(pl.col(\"mouse_id\") == mouse_id) for mouse_id in range(1, n_mice + 1)}\n\n    for agent_mouse_id in range(1, n_mice + 1):\n        result_element = pl.DataFrame(\n            {\n                \"video_id\": metadata[\"video_id\"],\n                \"agent_mouse_id\": agent_mouse_id,\n                \"target_mouse_id\": -1,\n                \"video_frame\": pl.arange(start_frame, end_frame + 1, eager=True),\n            },\n            schema={\n                \"video_id\": pl.Int32,\n                \"agent_mouse_id\": pl.Int8,\n                \"target_mouse_id\": pl.Int8,\n                \"video_frame\": pl.Int32,\n            },\n        )\n\n        pivot = pivot_trackings[agent_mouse_id].select(\n            pl.col(\"video_frame\"),\n            pl.exclude(\"video_frame\").name.prefix(\"agent_\"),\n        )\n        columns = pivot.columns\n        pivot = pivot.with_columns(\n            *[pl.lit(None).cast(pl.Float32).alias(f\"agent_x_{bp}\") for bp in BODY_PARTS if f\"agent_x_{bp}\" not in columns],\n            *[pl.lit(None).cast(pl.Float32).alias(f\"agent_y_{bp}\") for bp in BODY_PARTS if f\"agent_y_{bp}\" not in columns],\n        )\n\n        features = pivot.with_columns(\n            pl.lit(agent_mouse_id).alias(\"agent_mouse_id\"),\n            pl.lit(-1).alias(\"target_mouse_id\"),\n        ).select(\n            pl.col(\"video_frame\"),\n            pl.col(\"agent_mouse_id\"),\n            pl.col(\"target_mouse_id\"),\n            *[\n                body_parts_distance(body_part_1, body_part_2).alias(f\"aa__{body_part_1}__{body_part_2}__distance\")\n                for body_part_1, body_part_2 in itertools.combinations(BODY_PARTS, 2)\n            ],\n            *[\n                body_part_speed(body_part, period_ms).alias(f\"agent__{body_part}__speed_{period_ms}ms\")\n                for body_part, period_ms in itertools.product([\"ear_left\", \"ear_right\", \"tail_base\"], [500, 1000, 2000, 3000])\n            ],\n            elongation().alias(\"agent__elongation\"),\n            body_angle().alias(\"agent__body_angle\"),\n        )\n\n        result_element = result_element.join(\n            features,\n            on=[\"video_frame\", \"agent_mouse_id\", \"target_mouse_id\"],\n            how=\"left\",\n        )\n        result.append(result_element)\n\n    return pl.concat(result, how=\"vertical\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:39:31.522158Z","iopub.execute_input":"2025-11-24T19:39:31.522567Z","iopub.status.idle":"2025-11-24T19:39:31.533069Z","shell.execute_reply.started":"2025-11-24T19:39:31.522544Z","shell.execute_reply":"2025-11-24T19:39:31.532077Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 特徴量加工(pair)\n- agent-target の体の各部位間距離(cm)\n\n  agent-target の主要な11部位(定数: BODY_PARTS)の組み合わせの距離\n  \n- agent, target の部位の推定速度(cm/s)\n\n  ear_left, ear_right, tail_base の 500, 1000, 2000, 3000ms間の推定速度\n  \n- agent, target の伸長度\n\n  (nose-tail_baseの距離) / (ear_left-ear_rightの距離)\n\n- agent, target の体角度(deg)\n  \n  nose-body_center, body_center-tail_base　がなすベクトルの角度","metadata":{}},{"cell_type":"code","source":"%%writefile pair_features.py\n\ndef make_pair_features(\n    metadata: dict,\n    tracking: pl.DataFrame,\n) -> pl.DataFrame:\n    def body_parts_distance(agent_or_target_1, body_part_1, agent_or_target_2, body_part_2):\n        # agent-targetの体の各部位間距離(cm)\n        assert agent_or_target_1 == \"agent\" or agent_or_target_1 == \"target\"\n        assert agent_or_target_2 == \"agent\" or agent_or_target_2 == \"target\"\n        assert body_part_1 in BODY_PARTS\n        assert body_part_2 in BODY_PARTS\n        return (\n            (pl.col(f\"{agent_or_target_1}_x_{body_part_1}\") - pl.col(f\"{agent_or_target_2}_x_{body_part_2}\")).pow(2)\n            + (pl.col(f\"{agent_or_target_1}_y_{body_part_1}\") - pl.col(f\"{agent_or_target_2}_y_{body_part_2}\")).pow(2)\n        ).sqrt() / metadata[\"pix_per_cm_approx\"]\n\n    def body_part_speed(agent_or_target, body_part, period_ms):\n        # 部位の推定速度(cm/s)\n        assert agent_or_target == \"agent\" or agent_or_target == \"target\"\n        assert body_part in BODY_PARTS\n        window_frames = max(1, int(round(period_ms * metadata[\"frames_per_second\"] / 1000.0)))\n        return (\n            (\n                (pl.col(f\"{agent_or_target}_x_{body_part}\").diff()).pow(2)\n                + (pl.col(f\"{agent_or_target}_y_{body_part}\").diff()).pow(2)\n            ).sqrt()\n            / metadata[\"pix_per_cm_approx\"]\n            * metadata[\"frames_per_second\"]\n        ).rolling_mean(window_size=window_frames, center=True)\n\n    def elongation(agent_or_target):\n        # 伸長度(cm)\n        assert agent_or_target == \"agent\" or agent_or_target == \"target\"\n        d1 = body_parts_distance(agent_or_target, \"nose\", agent_or_target, \"tail_base\")\n        d2 = body_parts_distance(agent_or_target, \"ear_left\", agent_or_target, \"ear_right\")\n        return d1 / (d2 + 1e-06)\n\n    def body_angle(agent_or_target):\n        # 体角度(deg)\n        assert agent_or_target == \"agent\" or agent_or_target == \"target\"\n        v1x = pl.col(f\"{agent_or_target}_x_nose\") - pl.col(f\"{agent_or_target}_x_body_center\")\n        v1y = pl.col(f\"{agent_or_target}_y_nose\") - pl.col(f\"{agent_or_target}_y_body_center\")\n        v2x = pl.col(f\"{agent_or_target}_x_tail_base\") - pl.col(f\"{agent_or_target}_x_body_center\")\n        v2y = pl.col(f\"{agent_or_target}_y_tail_base\") - pl.col(f\"{agent_or_target}_y_body_center\")\n        return (v1x * v2x + v1y * v2y) / ((v1x.pow(2) + v1y.pow(2)).sqrt() * (v2x.pow(2) + v2y.pow(2)).sqrt() + 1e-06)\n\n    def body_center_distance_rolling_agg(agg, period_ms):\n        # 距離の移動集計特徴量\n        assert agg in [\"mean\", \"std\", \"var\", \"min\", \"max\"] # 集計関数\n        expr = body_parts_distance(\"agent\", \"body_center\", \"target\", \"body_center\")\n        window_frames = max(1, int(round(period_ms * metadata[\"frames_per_second\"] / 1000.0)))\n\n        if agg == \"mean\":\n            return expr.rolling_mean(window_size=window_frames, center=True, min_samples=1)\n        elif agg == \"std\":\n            return expr.rolling_std(window_size=window_frames, center=True, min_samples=1)\n        elif agg == \"var\":\n            return expr.rolling_var(window_size=window_frames, center=True, min_samples=1)\n        elif agg == \"min\":\n            return expr.rolling_min(window_size=window_frames, center=True, min_samples=1)\n        elif agg == \"max\":\n            return expr.rolling_max(window_size=window_frames, center=True, min_samples=1)\n        else:\n            raise ValueError()\n\n    n_mice = (\n        (metadata[\"mouse1_strain\"] is not None)\n        + (metadata[\"mouse2_strain\"] is not None)\n        + (metadata[\"mouse3_strain\"] is not None)\n        + (metadata[\"mouse4_strain\"] is not None)\n    )\n    start_frame = tracking.select(pl.col(\"video_frame\").min()).item()\n    end_frame = tracking.select(pl.col(\"video_frame\").max()).item()\n\n    result = []\n\n    pivot = tracking.pivot(\n        on=[\"bodypart\"],\n        index=[\"video_frame\", \"mouse_id\"],\n        values=[\"x\", \"y\"],\n    ).sort([\"mouse_id\", \"video_frame\"])\n    pivot_trackings = {mouse_id: pivot.filter(pl.col(\"mouse_id\") == mouse_id) for mouse_id in range(1, n_mice + 1)}\n\n    for agent_mouse_id, target_mouse_id in itertools.permutations(range(1, n_mice + 1), 2):\n        result_element = pl.DataFrame(\n            {\n                \"video_id\": metadata[\"video_id\"],\n                \"agent_mouse_id\": agent_mouse_id,\n                \"target_mouse_id\": target_mouse_id,\n                \"video_frame\": pl.arange(start_frame, end_frame + 1, eager=True),\n            },\n            schema={\n                \"video_id\": pl.Int32,\n                \"agent_mouse_id\": pl.Int8,\n                \"target_mouse_id\": pl.Int8,\n                \"video_frame\": pl.Int32,\n            },\n        )\n\n        merged_pivot = (\n            pivot_trackings[agent_mouse_id]\n            .select(\n                pl.col(\"video_frame\"),\n                pl.exclude(\"video_frame\").name.prefix(\"agent_\"),\n            )\n            .join(\n                pivot_trackings[target_mouse_id].select(\n                    pl.col(\"video_frame\"),\n                    pl.exclude(\"video_frame\").name.prefix(\"target_\"),\n                ),\n                on=\"video_frame\",\n                how=\"inner\",\n            )\n        )\n        columns = merged_pivot.columns\n        merged_pivot = merged_pivot.with_columns(\n            *[pl.lit(None).cast(pl.Float32).alias(f\"agent_x_{bp}\") for bp in BODY_PARTS if f\"agent_x_{bp}\" not in columns],\n            *[pl.lit(None).cast(pl.Float32).alias(f\"agent_y_{bp}\") for bp in BODY_PARTS if f\"agent_y_{bp}\" not in columns],\n            *[pl.lit(None).cast(pl.Float32).alias(f\"target_x_{bp}\") for bp in BODY_PARTS if f\"target_x_{bp}\" not in columns],\n            *[pl.lit(None).cast(pl.Float32).alias(f\"target_y_{bp}\") for bp in BODY_PARTS if f\"target_y_{bp}\" not in columns],\n        )\n\n        features = merged_pivot.with_columns(\n            pl.lit(agent_mouse_id).alias(\"agent_mouse_id\"),\n            pl.lit(target_mouse_id).alias(\"target_mouse_id\"),\n        ).select(\n            pl.col(\"video_frame\"),\n            pl.col(\"agent_mouse_id\"),\n            pl.col(\"target_mouse_id\"),\n            *[\n                body_parts_distance(\"agent\", agent_body_part, \"target\", target_body_part).alias(\n                    f\"at__{agent_body_part}__{target_body_part}__distance\"\n                )\n                for agent_body_part, target_body_part in itertools.product(BODY_PARTS, repeat=2)\n            ],\n            *[\n                body_part_speed(\"agent\", body_part, period_ms).alias(f\"agent__{body_part}__speed_{period_ms}ms\")\n                for body_part, period_ms in itertools.product([\"ear_left\", \"ear_right\", \"tail_base\"], [500, 1000, 2000, 3000])\n            ],\n            *[\n                body_part_speed(\"target\", body_part, period_ms).alias(f\"target__{body_part}__speed_{period_ms}ms\")\n                for body_part, period_ms in itertools.product([\"ear_left\", \"ear_right\", \"tail_base\"], [500, 1000, 2000, 3000])\n            ],\n            elongation(\"agent\").alias(\"agent__elongation\"),\n            elongation(\"target\").alias(\"target__elongation\"),\n            body_angle(\"agent\").alias(\"agent__body_angle\"),\n            body_angle(\"target\").alias(\"target__body_angle\"),\n        )\n\n        result_element = result_element.join(\n            features,\n            on=[\"video_frame\", \"agent_mouse_id\", \"target_mouse_id\"],\n            how=\"left\",\n        )\n        result.append(result_element)\n\n    return pl.concat(result, how=\"vertical\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:39:31.902767Z","iopub.execute_input":"2025-11-24T19:39:31.903131Z","iopub.status.idle":"2025-11-24T19:39:31.914672Z","shell.execute_reply.started":"2025-11-24T19:39:31.903105Z","shell.execute_reply":"2025-11-24T19:39:31.913641Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%run -i self_features.py\n%run -i pair_features.py\n\ndef process_video(row):\n    \"\"\"Process a single video to extract self and pair features.\"\"\"\n    lab_id = row[\"lab_id\"]\n    video_id = row[\"video_id\"]\n\n    tracking_path = TRAIN_TRACKING_DIR / f\"{lab_id}/{video_id}.parquet\"\n    tracking = pl.read_parquet(tracking_path)\n\n    self_features = make_self_features(metadata=row, tracking=tracking)\n    pair_features = make_pair_features(metadata=row, tracking=tracking)\n\n    self_features.write_parquet(WORKING_DIR / \"self_features\" / f\"{video_id}.parquet\")\n    pair_features.write_parquet(WORKING_DIR / \"pair_features\" / f\"{video_id}.parquet\")\n\n    return video_id\n\n\n# make data\n(WORKING_DIR / \"self_features\").mkdir(exist_ok=True, parents=True)\n(WORKING_DIR / \"pair_features\").mkdir(exist_ok=True, parents=True)\n\nrows = list(train_dataframe.filter(pl.col(\"behaviors_labeled\").is_not_null()).rows(named=True))\nresults = joblib.Parallel(n_jobs=-1, verbose=5)(joblib.delayed(process_video)(row) for row in rows)\n\nprint(f\"Processed {len(results)} videos successfully\")\n\ndel rows, results\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:39:36.471041Z","iopub.execute_input":"2025-11-24T19:39:36.471396Z","iopub.status.idle":"2025-11-24T19:45:33.703419Z","shell.execute_reply.started":"2025-11-24T19:39:36.471373Z","shell.execute_reply":"2025-11-24T19:45:33.702330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tune_threshold(oof_action, y_action):\n    thresholds = np.arange(0, 1.005, 0.005)\n    scores = [f1_score(y_action, (oof_action >= th), zero_division=0) for th in thresholds]\n    best_idx = np.argmax(scores)\n    return thresholds[best_idx]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:47:43.305255Z","iopub.execute_input":"2025-11-24T19:47:43.305631Z","iopub.status.idle":"2025-11-24T19:47:43.311602Z","shell.execute_reply.started":"2025-11-24T19:47:43.305609Z","shell.execute_reply":"2025-11-24T19:47:43.310376Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 学習・検証\n\nlab, behavior 毎にモデル(XGBoost)を作る\n\nクロスバリデーション(3fold)でf1スコアを計算しながら学習する","metadata":{}},{"cell_type":"code","source":"def train_validate(lab_id: str, behavior: str, indices: pl.DataFrame, features: pl.DataFrame, labels: pl.Series):\n    # 結果を保存するディレクトリのパスを作成\n    result_dir = WORKING_DIR / \"results\" / lab_id / behavior\n    # ディレクトリが存在しない場合は作成（親ディレクトリも含めて）\n    result_dir.mkdir(exist_ok=True, parents=True)\n\n    # ラベルの合計が0の場合（正例が1つもない場合）の処理\n    if labels.sum() == 0:\n        # F1スコアを0として保存\n        with open(result_dir / \"f1.txt\", \"w\") as f:\n            f.write(\"0.0\\n\")\n        # すべての予測値を0とした結果データフレームを作成\n        oof_prediction_dataframe = indices.with_columns(\n            pl.Series(\"fold\", [-1] * len(labels), dtype=pl.Int8),  # フォールド番号（-1は未使用を意味）\n            pl.Series(\"prediction\", [0.0] * len(labels), dtype=pl.Float32),  # 予測確率\n            pl.Series(\"predicted_label\", [0] * len(labels), dtype=pl.Int8),  # 予測ラベル\n        )\n        # 結果をparquet形式で保存\n        oof_prediction_dataframe.write_parquet(result_dir / \"oof_predictions.parquet\")\n        return 0.0\n\n    # Out-of-Fold予測結果を保存するための配列を初期化\n    folds = np.ones(len(labels), dtype=np.int8) * -1  # 各サンプルが属するフォールド番号\n    oof_predictions = np.zeros(len(labels), dtype=np.float32)  # 予測確率\n    oof_prediction_labels = np.zeros(len(labels), dtype=np.int8)  # 予測ラベル（0または1）\n\n    # 3分割の層化グループ交差検証を実行\n    # StratifiedGroupKFoldは、ラベルの分布を保ちつつ、同じグループ（video_id）が複数のフォールドに分かれないようにする\n    for fold, (train_idx, valid_idx) in enumerate(\n        StratifiedGroupKFold(n_splits=3, shuffle=True, random_state=42).split(\n            X=features,  # 特徴量\n            y=labels,  # ラベル\n            groups=indices.get_column(\"video_id\"),  # グループ化の基準（同じ動画IDは同じフォールドに）\n        )\n    ):\n        # 各フォールドの結果を保存するディレクトリを作成\n        result_dir_fold = result_dir / f\"fold_{fold}\"\n        result_dir_fold.mkdir(exist_ok=True, parents=True)\n\n        # 訓練データと検証データに分割\n        X_train = features[train_idx]  # 訓練用特徴量\n        y_train = labels[train_idx]  # 訓練用ラベル\n        X_valid = features[valid_idx]  # 検証用特徴量\n        y_valid = labels[valid_idx]  # 検証用ラベル\n\n        # クラス不均衡に対処するための重みを計算\n        # 負例の数 / 正例の数 = 正例にかける重み\n        scale_pos_weight = (len(y_train) - y_train.sum()) / y_train.sum()\n\n        # XGBoostのハイパーパラメータを設定\n        params = {\n            \"objective\": \"binary:logistic\",  # 二値分類問題\n            \"eval_metric\": \"logloss\",  # 評価指標：対数損失\n            \"device\": \"cpu\",  # 使用デバイス\n            \"tree_method\": \"hist\",  # ヒストグラムベースの高速なアルゴリズム\n            \"learning_rate\": 0.05,  # 学習率\n            \"max_depth\": 6,  # 木の最大深さ\n            \"min_child_weight\": 5,  # 子ノードの最小重み\n            \"subsample\": 0.8,  # 各木で使用するサンプルの割合\n            \"colsample_bytree\": 0.8,  # 各木で使用する特徴量の割合\n            \"scale_pos_weight\": scale_pos_weight,  # 正例の重み\n            \"max_bin\": 64,  # ヒストグラムのビン数\n            \"seed\": 42,  # 乱数シード\n        }\n        \n        # XGBoost用のデータ行列を作成（訓練データは量子化行列、検証データは通常の行列）\n        dtrain = xgb.QuantileDMatrix(X_train, label=y_train, feature_names=features.columns, max_bin=64)\n        dvalid = xgb.DMatrix(X_valid, label=y_valid, feature_names=features.columns)\n\n        # 評価結果を保存する辞書\n        evals_result = {}\n        \n        # 早期終了のコールバックを設定\n        # 検証データの対数損失が10ラウンド改善しない場合、学習を停止\n        early_stopping_callback = xgb.callback.EarlyStopping(\n            rounds=10,  # 改善が見られない連続ラウンド数\n            metric_name=\"logloss\",  # 監視する指標\n            data_name=\"valid\",  # 監視するデータセット\n            maximize=False,  # 小さい方が良い指標\n            save_best=True,  # 最良のモデルを保存\n        )\n        \n        # モデルの学習を実行\n        model = xgb.train(\n            params,  # ハイパーパラメータ\n            dtrain=dtrain,  # 訓練データ\n            num_boost_round=250,  # 最大ブースティングラウンド数\n            evals=[(dtrain, \"train\"), (dvalid, \"valid\")],  # 評価するデータセット\n            callbacks=[early_stopping_callback],  # コールバック\n            evals_result=evals_result,  # 評価結果の保存先\n            verbose_eval=0,  # ログ出力の頻度（0は出力なし）\n        )\n\n        # 検証データに対して予測を実行（確率値を取得）\n        fold_predictions = model.predict(dvalid)\n\n        # F1スコアを最大化する最適な閾値を調整\n        threshold = tune_threshold(fold_predictions, y_valid)\n        \n        # Out-of-Fold予測結果を保存\n        folds[valid_idx] = fold  # フォールド番号\n        oof_predictions[valid_idx] = fold_predictions  # 予測確率\n        oof_prediction_labels[valid_idx] = (fold_predictions >= threshold).astype(np.int8)  # 閾値で二値化\n\n        # このフォールドの結果を保存\n        # 学習済みモデルを保存\n        model.save_model(result_dir_fold / \"model.json\")\n        # 最適な閾値を保存\n        with open(result_dir_fold / \"threshold.txt\", \"w\") as f:\n            f.write(f\"{threshold}\\n\")\n\n        # 特徴量の重要度をプロット（上位20個、ゲイン基準）\n        xgb.plot_importance(model, max_num_features=20, importance_type=\"gain\", values_format=\"{v:.2f}\")\n        plt.tight_layout()\n        plt.savefig(result_dir_fold / \"feature_importance.png\")\n        plt.close()\n\n        # 学習曲線（対数損失の推移）をプロット\n        lgb.plot_metric(evals_result, metric=\"logloss\")\n        plt.tight_layout()\n        plt.savefig(result_dir_fold / \"metric.png\")\n        plt.close()\n\n        # メモリを解放\n        gc.collect()\n\n    # すべてのフォールドの予測結果をデータフレームにまとめる\n    oof_prediction_dataframe = indices.with_columns(\n        pl.Series(\"fold\", folds, dtype=pl.Int8),  # フォールド番号\n        pl.Series(\"prediction\", oof_predictions, dtype=pl.Float32),  # 予測確率\n        pl.Series(\"predicted_label\", oof_prediction_labels, dtype=pl.Int8),  # 予測ラベル\n    )\n    \n    # 全体のF1スコアを計算\n    f1 = f1_score(labels, oof_prediction_labels, zero_division=0)\n    # F1スコアをファイルに保存\n    with open(result_dir / \"f1.txt\", \"w\") as f:\n        f.write(f\"{f1}\\n\")\n\n    # 予測結果データフレームを保存\n    oof_prediction_dataframe.write_parquet(result_dir / \"oof_predictions.parquet\")\n\n    # F1スコアを返す\n    return f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:47:43.660229Z","iopub.execute_input":"2025-11-24T19:47:43.660554Z","iopub.status.idle":"2025-11-24T19:47:43.679919Z","shell.execute_reply.started":"2025-11-24T19:47:43.660530Z","shell.execute_reply":"2025-11-24T19:47:43.678647Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## (self) lab-behavior毎に学習-検証を行う","metadata":{}},{"cell_type":"code","source":"groups = train_self_behavior_dataframe.group_by(\"lab_id\", \"behavior\", maintain_order=True)\ntotal_groups = len(list(groups))\nstart_time = time.perf_counter()\n\nfor idx, ((lab_id, behavior), group) in tqdm(enumerate(groups), total=total_groups):\n    if idx == 0:\n        tqdm.write(\n            f\"|{'LAB':^25}|{'BEHAVIOR':^15}|{'SAMPLES':^10}|{'POSITIVE':^10}|{'FEATURES':^10}|{'F1':^10}|{'ELAPSED TIME':^15}|\",\n            end=\"\\n\",\n        )\n\n    tqdm.write(f\"|{lab_id:^25}|{behavior:^15}|\", end=\"\")\n    index_list = []\n    feature_list = []\n    label_list = []\n\n    for row in group.rows(named=True):\n        video_id = row[\"video_id\"]\n        agent = row[\"agent\"]\n\n        agent_mouse_id = int(re.search(r\"mouse(\\d+)\", agent).group(1))\n\n        data = pl.scan_parquet(WORKING_DIR / \"self_features\" / f\"{video_id}.parquet\").filter(\n            (pl.col(\"agent_mouse_id\") == agent_mouse_id)\n        )\n        index = data.select(INDEX_COLS).collect(engine=\"streaming\")\n        feature = data.select(pl.exclude(INDEX_COLS)).collect(engine=\"streaming\")\n\n        # read annotation\n        annotation_path = TRAIN_ANNOTATION_DIR / lab_id / f\"{video_id}.parquet\"\n        if annotation_path.exists():\n            annotation = (\n                pl.scan_parquet(annotation_path)\n                .filter((pl.col(\"action\") == behavior) & (pl.col(\"agent_id\") == agent_mouse_id))\n                .collect()\n            )\n        else:\n            annotation = pl.DataFrame(\n                schema={\n                    \"agent_id\": pl.Int8,\n                    \"target_id\": pl.Int8,\n                    \"action\": str,\n                    \"start_frame\": pl.Int16,\n                    \"stop_frame\": pl.Int16,\n                }\n            )\n\n        label_frames = set()\n        for annotation_row in annotation.rows(named=True):\n            label_frames.update(range(annotation_row[\"start_frame\"], annotation_row[\"stop_frame\"]))\n        label = index.select(pl.col(\"video_frame\").is_in(label_frames).cast(pl.Int8).alias(\"label\"))\n\n        if label.get_column(\"label\").sum() == 0:\n            continue\n\n        index_list.append(index)\n        feature_list.append(feature)\n        label_list.append(label.get_column(\"label\"))\n\n    if not index_list:\n        elapsed_time = datetime.timedelta(seconds=int(time.perf_counter() - start_time))\n        tqdm.write(f\"{0:>10,}|{0:>10,}|{0:>10,}|{'-':>10}|{str(elapsed_time):>15}|\", end=\"\\n\")\n        continue\n\n    indices = pl.concat(index_list, how=\"vertical\")\n    features = pl.concat(feature_list, how=\"vertical\")\n    labels = pl.concat(label_list, how=\"vertical\")\n\n    del index_list, feature_list, label_list\n    gc.collect()\n\n    tqdm.write(f\"{len(indices):>10,}|{labels.sum():>10,}|{len(features.columns):>10,}|\", end=\"\")\n\n    f1 = train_validate(lab_id, behavior, indices, features, labels)\n    tqdm.write(f\"{f1:>10.2f}|\", end=\"\")\n\n    elapsed_time = datetime.timedelta(seconds=int(time.perf_counter() - start_time))\n    tqdm.write(f\"{str(elapsed_time):>15}|\", end=\"\\n\")\n\n    gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:47:44.758803Z","iopub.execute_input":"2025-11-24T19:47:44.759150Z","iopub.status.idle":"2025-11-24T19:51:45.990977Z","shell.execute_reply.started":"2025-11-24T19:47:44.759126Z","shell.execute_reply":"2025-11-24T19:51:45.989568Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## (pair) lab-behavior毎に学習-検証を行う","metadata":{}},{"cell_type":"code","source":"groups = train_pair_behavior_dataframe.group_by(\"lab_id\", \"behavior\", maintain_order=True)\ntotal_groups = len(list(groups))\nstart_time = time.perf_counter()\n\nfor idx, ((lab_id, behavior), group) in tqdm(enumerate(groups), total=total_groups):\n    if idx == 0:\n        tqdm.write(\n            f\"|{'LAB':^25}|{'BEHAVIOR':^15}|{'SAMPLES':^10}|{'POSITIVE':^10}|{'FEATURES':^10}|{'F1':^10}|{'ELAPSED TIME':^15}|\",\n            end=\"\\n\",\n        )\n\n    tqdm.write(f\"|{lab_id:^25}|{behavior:^15}|\", end=\"\")\n    index_list = []\n    feature_list = []\n    label_list = []\n\n    for row in group.rows(named=True):\n        video_id = row[\"video_id\"]\n        agent = row[\"agent\"]\n        target = row[\"target\"]\n\n        agent_mouse_id = int(re.search(r\"mouse(\\d+)\", agent).group(1))\n        target_mouse_id = int(re.search(r\"mouse(\\d+)\", target).group(1))\n\n        data = pl.scan_parquet(WORKING_DIR / \"pair_features\" / f\"{video_id}.parquet\").filter(\n            (pl.col(\"agent_mouse_id\") == agent_mouse_id) & (pl.col(\"target_mouse_id\") == target_mouse_id)\n        )\n        index = data.select(INDEX_COLS).collect(engine=\"streaming\")\n        feature = data.select(pl.exclude(INDEX_COLS)).collect(engine=\"streaming\")\n\n        # read annotation\n        annotation_path = TRAIN_ANNOTATION_DIR / lab_id / f\"{video_id}.parquet\"\n        if annotation_path.exists():\n            annotation = (\n                pl.scan_parquet(annotation_path)\n                .filter(\n                    (pl.col(\"action\") == behavior)\n                    & (pl.col(\"agent_id\") == agent_mouse_id)\n                    & (pl.col(\"target_id\") == target_mouse_id)\n                )\n                .collect()\n            )\n        else:\n            annotation = pl.DataFrame(\n                schema={\n                    \"agent_id\": pl.Int8,\n                    \"target_id\": pl.Int8,\n                    \"action\": str,\n                    \"start_frame\": pl.Int16,\n                    \"stop_frame\": pl.Int16,\n                }\n            )\n\n        label_frames = set()\n        for annotation_row in annotation.rows(named=True):\n            label_frames.update(range(annotation_row[\"start_frame\"], annotation_row[\"stop_frame\"]))\n        label = index.select(pl.col(\"video_frame\").is_in(label_frames).cast(pl.Int8).alias(\"label\"))\n\n        if label.get_column(\"label\").sum() == 0:\n            continue\n\n        index_list.append(index)\n        feature_list.append(feature)\n        label_list.append(label.get_column(\"label\"))\n\n    if not index_list:\n        elapsed_time = datetime.timedelta(seconds=int(time.perf_counter() - start_time))\n        tqdm.write(f\"{0:>10,}|{0:>10,}|{0:>10,}|{'-':>10}|{str(elapsed_time):>15}|\", end=\"\\n\")\n        continue\n\n    indices = pl.concat(index_list, how=\"vertical\")\n    features = pl.concat(feature_list, how=\"vertical\")\n    labels = pl.concat(label_list, how=\"vertical\")\n\n    del index_list, feature_list, label_list\n    gc.collect()\n\n    tqdm.write(f\"{len(indices):>10,}|{labels.sum():>10,}|{len(features.columns):>10,}|\", end=\"\")\n\n    f1 = train_validate(lab_id, behavior, indices, features, labels)\n    tqdm.write(f\"{f1:>10.2f}|\", end=\"\")\n\n    elapsed_time = datetime.timedelta(seconds=int(time.perf_counter() - start_time))\n    tqdm.write(f\"{str(elapsed_time):>15}|\", end=\"\\n\")\n\n    gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:51:48.588932Z","iopub.execute_input":"2025-11-24T19:51:48.589306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile robustify.py\n\ndef robustify(submission: pl.DataFrame, dataset: pl.DataFrame, train_test: str = \"train\"):\n    traintest_directory = INPUT_DIR / f\"{train_test}_tracking\"\n\n    old_submission = submission.clone()\n    submission = submission.filter(pl.col(\"start_frame\") < pl.col(\"stop_frame\"))\n    if len(submission) != len(old_submission):\n        print(\"ERROR: Dropped frames with start >= stop\")\n\n    old_submission = submission.clone()\n    group_list = []\n    for _, group in submission.group_by(\"video_id\", \"agent_id\", \"target_id\"):\n        group = group.sort(\"start_frame\")\n        mask = np.ones(len(group), dtype=bool)\n        last_stop_frame = 0\n        for i, row in enumerate(group.rows(named=True)):\n            if row[\"start_frame\"] < last_stop_frame:\n                mask[i] = False\n            else:\n                last_stop_frame = row[\"stop_frame\"]\n        group_list.append(group.filter(pl.Series(\"mask\", mask)))\n\n    submission = pl.concat(group_list)\n\n    if len(submission) != len(old_submission):\n        print(\"ERROR: Dropped duplicate frames\")\n\n    s_list = []\n    for row in dataset.rows(named=True):\n        lab_id = row[\"lab_id\"]\n        video_id = row[\"video_id\"]\n        if row[\"behaviors_labeled\"] is None:\n            continue\n\n        if video_id in submission.get_column(\"video_id\").to_list():\n            continue\n\n        if isinstance(row[\"behaviors_labeled\"], str):\n            continue\n\n        print(f\"Video {video_id} has no predictions.\")\n\n        path = traintest_directory / f\"/{lab_id}/{video_id}.parquet\"\n        vid = pd.read_parquet(path)\n\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\n        start_frame = vid.video_frame.min()\n        stop_frame = vid.video_frame.max() + 1\n\n        for (agent, target), actions in vid_behaviors.groupby([\"agent\", \"target\"]):\n            batch_length = int(np.ceil((stop_frame - start_frame) / len(actions)))\n            for i, action_row in enumerate(actions.itertuples(index=False)):\n                batch_start = start_frame + i * batch_length\n                batch_stop = min(batch_start + batch_length, stop_frame)\n                s_list.append((video_id, agent, target, action_row[\"action\"], batch_start, batch_stop))\n\n    if len(s_list) > 0:\n        submission = pd.concat(\n            [\n                submission,\n                pd.DataFrame(s_list, columns=[\"video_id\", \"agent_id\", \"target_id\", \"action\", \"start_frame\", \"stop_frame\"]),\n            ]\n        )\n        print(\"ERROR: Filled empty videos\")\n\n    return submission","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-18T09:47:18.239683Z","iopub.execute_input":"2025-11-18T09:47:18.240013Z","iopub.status.idle":"2025-11-18T09:47:18.249055Z","shell.execute_reply.started":"2025-11-18T09:47:18.239978Z","shell.execute_reply":"2025-11-18T09:47:18.247832Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 検証データに対する予測値を集計","metadata":{}},{"cell_type":"code","source":"# グループごとのOut-of-Fold予測結果を保存するリスト\ngroup_oof_predictions = []\n\n# データを lab_id, video_id, agent, target でグループ化\n# maintain_order=True で元の順序を保持\ngroups = train_behavior_dataframe.group_by(\"lab_id\", \"video_id\", \"agent\", \"target\", maintain_order=True)\n\n# 各グループに対して処理を実行（進捗バーを表示）\nfor (lab_id, video_id, agent, target), group in tqdm(groups, total=len(list(groups))):\n    # agent（行動主体）からマウスIDを抽出\n    # 例: \"mouse1\" → 1\n    agent_mouse_id = int(re.search(r\"mouse(\\d+)\", agent).group(1))\n    \n    # target（行動対象）からマウスIDを抽出\n    # \"self\"（自分自身）の場合は -1、それ以外はマウスIDを取得\n    target_mouse_id = -1 if target == \"self\" else int(re.search(r\"mouse(\\d+)\", target).group(1))\n\n    # このグループの各行動の予測結果を保存するリスト\n    prediction_dataframe_list = []\n\n    # グループ内の各行（各行動）を処理\n    for row in group.rows(named=True):\n        behavior = row[\"behavior\"]  # 行動の種類（例: \"grooming\", \"sniffing\"など）\n\n        # この行動のOOF予測結果ファイルのパスを構築\n        oof_path = WORKING_DIR / \"results\" / lab_id / behavior / \"oof_predictions.parquet\"\n        \n        # ファイルが存在しない場合はスキップ\n        if not oof_path.exists():\n            continue\n\n        # 予測結果を読み込み、該当するvideo_id、agent、targetでフィルタリング\n        prediction = (\n            pl.scan_parquet(oof_path)  # 遅延読み込み（メモリ効率的）\n            .filter(\n                (pl.col(\"video_id\") == video_id)  # 動画IDが一致\n                & (pl.col(\"agent_mouse_id\") == agent_mouse_id)  # 行動主体が一致\n                & (pl.col(\"target_mouse_id\") == target_mouse_id)  # 行動対象が一致\n            )\n            .select(\n                *INDEX_COLS,  # インデックス列を選択\n                # 予測確率と予測ラベルを掛け合わせて、この行動のスコアを計算\n                # 予測ラベルが0の場合はスコアも0になる\n                (pl.col(\"prediction\") * pl.col(\"predicted_label\")).alias(behavior)\n            )\n            .collect()  # 実際にデータを読み込んで実行\n        )\n\n        # フィルタ後に行がない場合（該当データがない場合）はスキップ\n        if len(prediction) == 0:\n            continue\n\n        # この行動の予測結果をリストに追加\n        prediction_dataframe_list.append(prediction)\n\n    # このグループで予測結果が1つもない場合はスキップ\n    if not prediction_dataframe_list:\n        continue\n\n    # 複数の行動の予測結果を横方向に結合\n    # how=\"align\"で、インデックス列を基準に整列して結合\n    prediction_dataframe = pl.concat(prediction_dataframe_list, how=\"align\")\n\n    # インデックス列以外の列名（各行動名）を取得\n    cols = prediction_dataframe.select(pl.exclude(INDEX_COLS)).columns\n    \n    # 各フレームで最も確信度の高い行動を選択\n    prediction_labels_dataframe = prediction_dataframe.with_columns(\n        pl.struct(pl.exclude(INDEX_COLS))  # 全行動のスコアを構造体にまとめる\n        .map_elements(\n            # 各行に対して実行する関数\n            lambda row: \"none\" if sum(row.values()) == 0  # 全スコアが0なら\"none\"\n                       else (cols[np.argmax(list(row.values()))]),  # 最大スコアの行動を選択\n            return_dtype=pl.String,\n        )\n        .alias(\"prediction\")  # 新しい列名を\"prediction\"とする\n    ).select(INDEX_COLS + [\"prediction\"])  # インデックス列と予測列のみを選択\n\n    # 連続する同じ行動をまとめて、行動の開始と終了フレームを特定\n    group_oof_prediction = (\n        prediction_labels_dataframe\n        .filter((pl.col(\"prediction\") != pl.col(\"prediction\").shift(1)))  # 前の行と異なる行動のみを抽出（境界点）\n        .with_columns(pl.col(\"video_frame\").shift(-1).alias(\"stop_frame\"))  # 次の境界点を終了フレームとする\n        .filter(pl.col(\"prediction\") != \"none\")  # \"none\"（行動なし）を除外\n        .select(\n            pl.col(\"video_id\"),  # 動画ID\n            (\"mouse\" + pl.col(\"agent_mouse_id\").cast(str)).alias(\"agent_id\"),  # \"mouse1\"形式に変換\n            # target_mouse_idが-1なら\"self\"、それ以外は\"mouse2\"形式に変換\n            pl.when(pl.col(\"target_mouse_id\") == -1)\n            .then(pl.lit(\"self\"))\n            .otherwise(\"mouse\" + pl.col(\"target_mouse_id\").cast(str))\n            .alias(\"target_id\"),\n            pl.col(\"prediction\").alias(\"action\"),  # 行動名\n            pl.col(\"video_frame\").alias(\"start_frame\"),  # 開始フレーム\n            pl.col(\"stop_frame\"),  # 終了フレーム\n        )\n    )\n\n    # このグループの予測結果をリストに追加\n    group_oof_predictions.append(group_oof_prediction)\n\n%run -i robustify.py\n\noof_predictions = pl.concat(group_oof_predictions, how=\"vertical\")\noof_predictions = robustify(oof_predictions, train_dataframe, train_test=\"train\")\noof_predictions.with_row_index(\"row_id\").write_csv(WORKING_DIR / \"oof_predictions.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-18T09:47:18.250261Z","iopub.execute_input":"2025-11-18T09:47:18.250613Z","iopub.status.idle":"2025-11-18T09:47:23.076158Z","shell.execute_reply.started":"2025-11-18T09:47:18.250591Z","shell.execute_reply":"2025-11-18T09:47:23.075202Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 検証データによるスコアを計算する","metadata":{}},{"cell_type":"code","source":"\ndef compute_validation_metrics(submission, verbose=True):\n    \"\"\"Compute and display validation metrics for single vs pair behaviors.\"\"\"\n    # solution_df\n    dataset = pl.read_csv(INPUT_DIR / \"train.csv\").to_pandas()\n\n    solution = []\n    for _, row in dataset.iterrows():\n        lab_id = row[\"lab_id\"]\n        if lab_id.startswith(\"MABe22\"):\n            continue\n\n        video_id = row[\"video_id\"]\n        path = TRAIN_ANNOTATION_DIR / lab_id / f\"{video_id}.parquet\"\n        try:\n            annot = pd.read_parquet(path)\n        except FileNotFoundError:\n            continue\n\n        annot[\"lab_id\"] = lab_id\n        annot[\"video_id\"] = video_id\n        annot[\"behaviors_labeled\"] = row[\"behaviors_labeled\"]\n        annot[\"target_id\"] = np.where(\n            annot.target_id != annot.agent_id, annot[\"target_id\"].apply(lambda s: f\"mouse{s}\"), \"self\"\n        )\n        annot[\"agent_id\"] = annot[\"agent_id\"].apply(lambda s: f\"mouse{s}\")\n        solution.append(annot)\n\n    solution = pd.concat(solution)\n\n    try:\n        # Separate single and pair behaviors\n        submission_single = submission[submission[\"target_id\"] == \"self\"].copy()\n        submission_pair = submission[submission[\"target_id\"] != \"self\"].copy()\n\n        # Filter solution to match submission videos\n        solution_videos = set(submission[\"video_id\"].unique())\n        solution = solution[solution[\"video_id\"].isin(solution_videos)]\n\n        if len(solution) == 0:\n            return\n\n        # Compute overall F1 score\n        overall_f1 = score(solution, submission, \"row_id\", beta=1.0)\n        print(f\"\\n{'=' * 60}\")\n        print(\"PERFORMANCE METRICS\")\n        print(f\"{'=' * 60}\")\n        print(f\"Overall F1 Score: {overall_f1:.4f}\")\n        print(f\"Total predictions: {len(submission)}\")\n        print(f\"  - Single behaviors: {len(submission_single)}\")\n        print(f\"  - Pair behaviors: {len(submission_pair)}\")\n\n        # Compute per-action F1 scores using existing scoring function\n        solution_pl = pl.DataFrame(solution)\n        submission_pl = pl.DataFrame(submission)\n\n        # Add label_key and prediction_key\n        solution_pl = solution_pl.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_pl = submission_pl.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        # Group by action and compute metrics\n        action_stats = defaultdict(lambda: {\"single\": {\"count\": 0, \"f1\": 0.0}, \"pair\": {\"count\": 0, \"f1\": 0.0}})\n\n        for lab in solution_pl[\"lab_id\"].unique():\n            lab_solution = solution_pl.filter(pl.col(\"lab_id\") == lab).clone()\n            lab_videos = set(lab_solution[\"video_id\"].unique())\n            lab_submission = submission_pl.filter(pl.col(\"video_id\").is_in(lab_videos)).clone()\n\n            # Compute per-action F1 using same logic as single_lab_f1\n            label_frames = defaultdict(set)\n            prediction_frames = 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 row in lab_submission.to_dicts():\n                key = row[\"prediction_key\"]\n                prediction_frames[key].update(range(row[\"start_frame\"], row[\"stop_frame\"]))\n\n            for key in set(list(label_frames.keys()) + list(prediction_frames.keys())):\n                action = key.split(\"_\")[-1]\n                mode = \"single\" if \"self\" in key else \"pair\"\n\n                pred_frames = prediction_frames.get(key, set())\n                label_frames_set = label_frames.get(key, set())\n\n                tp = len(pred_frames & label_frames_set)\n                fn = len(label_frames_set - pred_frames)\n                fp = len(pred_frames - label_frames_set)\n\n                if tp + fn + fp > 0:\n                    f1 = (1 + 1**2) * tp / ((1 + 1**2) * tp + 1**2 * fn + fp)\n                    action_stats[action][mode][\"count\"] += 1\n                    action_stats[action][mode][\"f1\"] += f1\n\n        # Print per-action summary\n        print(\"\\nPer-Action Performance Summary:\")\n        print(f\"{'-' * 60}\")\n        print(f\"{'Action':<20} {'Mode':<10} {'Count':<10} {'Avg F1':<10}\")\n        print(f\"{'-' * 60}\")\n\n        for action in sorted(action_stats.keys()):\n            for mode in [\"single\", \"pair\"]:\n                stats = action_stats[action][mode]\n                if stats[\"count\"] > 0:\n                    avg_f1 = stats[\"f1\"] / stats[\"count\"]\n                    print(f\"{action:<20} {mode:<10} {stats['count']:<10} {avg_f1:<10.4f}\")\n\n        # Summary by mode\n        single_actions = [a for a in action_stats.keys() if action_stats[a][\"single\"][\"count\"] > 0]\n        pair_actions = [a for a in action_stats.keys() if action_stats[a][\"pair\"][\"count\"] > 0]\n\n        if single_actions:\n            single_avg_f1 = np.mean(\n                [\n                    action_stats[a][\"single\"][\"f1\"] / action_stats[a][\"single\"][\"count\"]\n                    for a in single_actions\n                    if action_stats[a][\"single\"][\"count\"] > 0\n                ]\n            )\n            print(f\"\\nSingle behaviors: {len(single_actions)} actions, Avg F1: {single_avg_f1:.4f}\")\n\n        if pair_actions:\n            pair_avg_f1 = np.mean(\n                [\n                    action_stats[a][\"pair\"][\"f1\"] / action_stats[a][\"pair\"][\"count\"]\n                    for a in pair_actions\n                    if action_stats[a][\"pair\"][\"count\"] > 0\n                ]\n            )\n            print(f\"Pair behaviors: {len(pair_actions)} actions, Avg F1: {pair_avg_f1:.4f}\")\n\n        print(f\"{'=' * 60}\\n\")\n\n    except Exception as e:\n        if verbose:\n            error_msg = str(e)\n            if len(error_msg) > 200:\n                error_msg = error_msg[:200] + \"...\"\n            print(f\"\\nWarning: Could not compute validation metrics: {error_msg}\")\n            if verbose:\n                print(f\"Traceback: {traceback.format_exc()[:300]}\")\n\ncompute_validation_metrics(submission=pd.read_csv(WORKING_DIR / \"oof_predictions.csv\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T18:32:13.340830Z","iopub.execute_input":"2025-11-24T18:32:13.341728Z","iopub.status.idle":"2025-11-24T18:32:13.445868Z","shell.execute_reply.started":"2025-11-24T18:32:13.341690Z","shell.execute_reply":"2025-11-24T18:32:13.444274Z"}},"outputs":[],"execution_count":null}]}