{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"},{"sourceId":14058704,"sourceType":"datasetVersion","datasetId":8948162,"isSourceIdPinned":true},{"sourceId":14058722,"sourceType":"datasetVersion","datasetId":8948171,"isSourceIdPinned":true},{"sourceId":14072951,"sourceType":"datasetVersion","datasetId":8958143,"isSourceIdPinned":true},{"sourceId":14079463,"sourceType":"datasetVersion","datasetId":8871157,"isSourceIdPinned":true},{"sourceId":14079598,"sourceType":"datasetVersion","datasetId":8871021,"isSourceIdPinned":true},{"sourceId":14126554,"sourceType":"datasetVersion","datasetId":9000666},{"sourceId":14151731,"sourceType":"datasetVersion","datasetId":9019614},{"sourceId":14152035,"sourceType":"datasetVersion","datasetId":9019839},{"sourceId":14153661,"sourceType":"datasetVersion","datasetId":9021038}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"xgb_labs = {\n            # \"JovialSwallow\": 0.477, #CNN: 474 XGB: 477\n            \"UppityFerret\": 0.607, #CNN: 531 XGB: 607\n            \"ReflectiveManatee\": 0.869, #CNN: 831 XGB: 869\n            \"SparklingTapir\": 0.732, #CNN: 692 XGB: 732\n            \"DeliriousFly\": 0.569, #CNN: 492 XGB: 569\n            \"NiftyGoldfinch\": 0.429, #CNN: 400 XGB: 429\n            \"LyricalHare\": 0.612, #CNN: 550 XGB: 612\n            \"PleasantMeerkat\": 0.305, #CNN: 293 XGB: 305\n            \"AdaptableSnail\": 0.494\n}\ncnn_labs = {\n    # \"ElegantMink\": 0.553, #CNN: 553 XGB: 556\n    \"TranquilPanther\": 0.541, #CNN: 541 XGB: 445\n    \"InvincibleJellyfish\": 0.367, #CNN: 367 XGB: 337\n    # \"CautiousGiraffe\": 0.626, #CNN: 626 XGB: 615\n    # \"GroovyShrew\": 0.473, #CNN: 473 XGB: 465\n}\n\nxgb_labs2 = {\n    \"BoisterousParrot\": 0.649 # 0.612\n}\n\nedu_xgb_labs = {\n    \"CautiousGiraffe\": 0.648,\n    \"ElegantMink\": 0.584,\n    \"GroovyShrew\": 0.512,\n    \"JovialSwallow\": 0.490,                                                                       \n}\n\nensemble_labs = {\n    \"ElegantMink\": \"edu\",\n    \"InvincibleJellyfish\": \"cnn\",\n    \"LyricalHare\": \"xgb\",\n    \"JovialSwallow\": \"edu\",\n    \"CautiousGiraffe\": \"edu\"\n}\n\nprint(\"CV:\", round((sum(xgb_labs.values()) + sum(cnn_labs.values()) + sum(xgb_labs2.values()) + sum(edu_xgb_labs.values())) / 15, 3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:12:02.737651Z","iopub.execute_input":"2025-12-15T05:12:02.737988Z","iopub.status.idle":"2025-12-15T05:12:02.748581Z","shell.execute_reply.started":"2025-12-15T05:12:02.737957Z","shell.execute_reply":"2025-12-15T05:12:02.747565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import f1_score\nfrom sklearn.base import clone\nfrom xgboost import XGBClassifier\nfrom tqdm.notebook import tqdm\nimport itertools\nimport warnings\nimport optuna\nimport joblib\nimport glob\nimport gc\nimport numpy as np\nimport pandas as pd\nimport os\n\noptuna.logging.set_verbosity(optuna.logging.WARNING)\nwarnings.filterwarnings('ignore')\n\n\nRUN_MODE = \"test\"\n\ndf = pd.read_csv(\"/kaggle/input/MABe-mouse-behavior-detection/test.csv\")\n\nif df.shape[0] == 1:\n    RUN_MODE = \"train\"\n    \n    df = pd.read_csv(\"/kaggle/input/MABe-mouse-behavior-detection/train.csv\")\n    df = df[~df[\"behaviors_labeled\"].isna()]\n    df = df.groupby(\"lab_id\").first().reset_index()\n    print(df.shape)\n\nensemble_df = df[df[\"lab_id\"].isin(ensemble_labs)].reset_index(drop=True)\nxgb_df = df[df[\"lab_id\"].isin(xgb_labs) | df[\"lab_id\"].isin(ensemble_labs)].reset_index(drop=True)\nedu_xgb_df = df[df[\"lab_id\"].isin(edu_xgb_labs) | df[\"lab_id\"].isin(ensemble_labs)].reset_index(drop=True)\nxgb_df2 = df[df[\"lab_id\"].isin(xgb_labs2)].reset_index(drop=True)\ndf = df[df[\"lab_id\"].isin(cnn_labs) | df[\"lab_id\"].isin(ensemble_labs)].reset_index(drop=True)\n\nos.environ['RUN_MODE'] = RUN_MODE\n\ndf.shape, xgb_df.shape, xgb_df2.shape, edu_xgb_df.shape, ensemble_df.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:12:02.750141Z","iopub.execute_input":"2025-12-15T05:12:02.750569Z","iopub.status.idle":"2025-12-15T05:12:04.602657Z","shell.execute_reply.started":"2025-12-15T05:12:02.750542Z","shell.execute_reply":"2025-12-15T05:12:04.601644Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"xgb_df.to_csv(\"xgb_test.csv\", index=False)\ndf.to_csv(\"cnn_test.csv\", index=False)\nxgb_df2.to_csv(\"xgb_test2.csv\", index=False)\nedu_xgb_df.to_csv(\"edu_xgb_test.csv\", index=False)\nensemble_df.to_csv(\"ensemble_test.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:12:04.603945Z","iopub.execute_input":"2025-12-15T05:12:04.604301Z","iopub.status.idle":"2025-12-15T05:12:04.620717Z","shell.execute_reply.started":"2025-12-15T05:12:04.604274Z","shell.execute_reply":"2025-12-15T05:12:04.619681Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# SUB MATHEUS","metadata":{}},{"cell_type":"code","source":"!cp -r /kaggle/input/mabexgbsrc/src/ /kaggle/working","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:12:04.622593Z","iopub.execute_input":"2025-12-15T05:12:04.623547Z","iopub.status.idle":"2025-12-15T05:12:04.866040Z","shell.execute_reply.started":"2025-12-15T05:12:04.623513Z","shell.execute_reply":"2025-12-15T05:12:04.864548Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile submission.py\n\"\"\"Prediction and post-processing utilities.\"\"\"\n\nimport json\nfrom typing import Optional\n\nimport numpy as np\nimport pandas as pd\nimport os\n\nRUN_MODE = os.environ.get('RUN_MODE')\n\ndef predict_multiclass(\n    pred: pd.DataFrame,\n    meta: pd.DataFrame,\n    thresholds: dict,\n    train: pd.DataFrame,\n    use_windowing: bool = False,\n    base_window_frames: int = 5,\n    target_fps: float = 30.0,\n    smooth_window: int = 5,\n    min_duration: int = 3,\n    max_gap: int = 5,\n) -> pd.DataFrame:\n    \"\"\"\n    Convert frame-level predictions to interval predictions with temporal smoothing.\n\n    Args:\n        pred: DataFrame with prediction probabilities for each action\n        meta: Metadata DataFrame with video_id, agent_id, target_id, video_frame, fps, lab_id\n        thresholds: Dictionary of action-specific thresholds\n        train: Training DataFrame with 'actions' column for filtering\n        use_windowing: If True, predictions are at window-level\n        base_window_frames: Base window size at target_fps\n        target_fps: Target FPS for window size calculation\n        smooth_window: Window size for temporal smoothing (rolling mode)\n        min_duration: Minimum interval duration in frames\n        max_gap: Maximum gap to merge adjacent intervals of same action\n\n    Returns:\n        DataFrame with interval predictions (start_frame, stop_frame)\n    \"\"\"\n    threshold_matrix = np.zeros((len(pred), len(pred.columns)))\n\n    for col_idx, action in enumerate(pred.columns):\n        action_thresholds = thresholds.get(action, 0.27)\n\n        if isinstance(action_thresholds, dict):\n            for lab_id, threshold in action_thresholds.items():\n                lab_mask = meta.lab_id == lab_id\n                threshold_matrix[lab_mask, col_idx] = threshold\n\n            default_threshold = np.mean(list(action_thresholds.values()))\n            unset_mask = threshold_matrix[:, col_idx] == 0\n            threshold_matrix[unset_mask, col_idx] = default_threshold\n        else:\n            threshold_matrix[:, col_idx] = action_thresholds\n\n    delta_proba = pred.values - threshold_matrix\n    ama = np.argmax(delta_proba, axis=1)\n    max_proba = np.max(delta_proba, axis=1)\n    ama = np.where(max_proba >= 0, ama, -1)\n\n    group_keys = [\"video_id\", \"agent_id\", \"target_id\"]\n    meta_reset = meta.reset_index(drop=True)\n    ama_series = pd.Series(ama, index=meta_reset.index)\n\n    smoothed_ama = np.full_like(ama, -1)\n    for _, group_idx in meta_reset.groupby(group_keys).groups.items():\n        group_ama = ama_series.iloc[group_idx].values\n        if smooth_window > 1 and len(group_ama) >= smooth_window:\n            smoothed_ama[group_idx] = _rolling_mode(group_ama, smooth_window)\n        else:\n            smoothed_ama[group_idx] = group_ama\n\n    records = []\n    action_labels = np.array(pred.columns)\n\n    for _, group_idx in meta_reset.groupby(group_keys).groups.items():\n        group_meta = meta_reset.loc[group_idx]\n        group_actions = smoothed_ama[group_idx]\n        frames = group_meta[\"video_frame\"].to_numpy()\n        if len(frames) == 0:\n            continue\n\n        order = np.argsort(frames)\n        frames = frames[order]\n        group_actions = group_actions[order]\n\n        frame_step = int(np.median(np.diff(frames))) if len(frames) > 1 else 1\n        if frame_step <= 0:\n            frame_step = 1\n        if use_windowing:\n            fps_col = \"fps\" if \"fps\" in group_meta else (\"frames_per_second\" if \"frames_per_second\" in group_meta else None)\n            fps_val = group_meta[fps_col].iloc[0] if fps_col else target_fps\n            window_size = max(1, int(round(base_window_frames * fps_val / target_fps)))\n            frame_step = max(frame_step, window_size)\n\n        change_mask = np.ones_like(group_actions, dtype=bool)\n        if len(group_actions) > 1:\n            change_mask[1:] = group_actions[1:] != group_actions[:-1]\n\n        change_frames = frames[change_mask]\n        change_actions = group_actions[change_mask]\n        if len(change_frames) == 0:\n            continue\n\n        stop_frames = np.concatenate((change_frames[1:], [frames[-1] + frame_step]))\n        keep = change_actions >= 0\n        if not keep.any():\n            continue\n\n        video_id = group_meta[\"video_id\"].iloc[0]\n        agent_id = group_meta[\"agent_id\"].iloc[0]\n        target_id = group_meta[\"target_id\"].iloc[0]\n\n        records.append(\n            pd.DataFrame(\n                {\n                    \"video_id\": video_id,\n                    \"agent_id\": agent_id,\n                    \"target_id\": target_id,\n                    \"action\": action_labels[change_actions[keep]],\n                    \"start_frame\": change_frames[keep].astype(np.int64),\n                    \"stop_frame\": stop_frames[keep].astype(np.int64),\n                }\n            )\n        )\n\n    if len(records) == 0:\n        return pd.DataFrame(\n            columns=[\"video_id\", \"agent_id\", \"target_id\", \"action\", \"start_frame\", \"stop_frame\"]\n        )\n\n    submission_part = pd.concat(records, ignore_index=True)\n\n    durations = submission_part[\"stop_frame\"] - submission_part[\"start_frame\"]\n    submission_part = submission_part[durations >= min_duration].reset_index(drop=True)\n\n    if max_gap > 0 and len(submission_part) > 1:\n        submission_part = _merge_intervals(submission_part, max_gap)\n\n    return submission_part\n\n\ndef _rolling_mode(arr: np.ndarray, window: int) -> np.ndarray:\n    \"\"\"Apply rolling mode filter for temporal smoothing.\"\"\"\n    result = np.copy(arr)\n    half = window // 2\n\n    for i in range(len(arr)):\n        start = max(0, i - half)\n        end = min(len(arr), i + half + 1)\n        window_vals = arr[start:end]\n\n        # Find mode (most common value)\n        unique, counts = np.unique(window_vals, return_counts=True)\n        mode_idx = np.argmax(counts)\n        result[i] = unique[mode_idx]\n\n    return result\n\n\ndef _merge_intervals(df: pd.DataFrame, max_gap: int) -> pd.DataFrame:\n    \"\"\"Merge nearby intervals of the same action within same video/agent/target.\"\"\"\n    merged = []\n    group_keys = [\"video_id\", \"agent_id\", \"target_id\"]\n\n    for _, group in df.groupby(group_keys, sort=False):\n        group = group.sort_values(\"start_frame\").reset_index(drop=True)\n\n        if len(group) == 0:\n            continue\n\n        current = group.iloc[0].to_dict()\n\n        for i in range(1, len(group)):\n            row = group.iloc[i]\n            same_action = row[\"action\"] == current[\"action\"]\n            gap = row[\"start_frame\"] - current[\"stop_frame\"]\n\n            if same_action and gap <= max_gap:\n                current[\"stop_frame\"] = row[\"stop_frame\"]\n            else:\n                merged.append(current)\n                current = row.to_dict()\n\n        merged.append(current)\n\n    if not merged:\n        return pd.DataFrame(\n            columns=[\"video_id\", \"agent_id\", \"target_id\", \"action\", \"start_frame\", \"stop_frame\"]\n        )\n\n    return pd.DataFrame(merged)\n\n\ndef robustify(\n    submission: pd.DataFrame,\n    dataset: pd.DataFrame,\n    traintest: str,\n    traintest_directory: Optional[str] = None,\n) -> pd.DataFrame:\n    \"\"\"\n    Post-process predictions to ensure validity.\n\n    Args:\n        submission: Raw predictions\n        dataset: Metadata DataFrame\n        traintest: 'train' or 'test'\n        traintest_directory: Path to tracking data\n\n    Returns:\n        Cleaned and validated submission DataFrame\n    \"\"\"\n    if traintest_directory is None:\n        traintest_directory = (\n            f\"/kaggle/input/MABe-mouse-behavior-detection/{traintest}_tracking\"\n        )\n\n    # Remove invalid intervals\n    old_submission = submission.copy()\n    submission = submission[submission.start_frame < submission.stop_frame]\n    if len(submission) != len(old_submission):\n        print(\"ERROR: Dropped frames with start >= stop\")\n\n    # Remove overlapping predictions\n    old_submission = submission.copy()\n    group_list = []\n    for _, group in submission.groupby([\"video_id\", \"agent_id\", \"target_id\"]):\n        group = group.sort_values(\"start_frame\")\n        mask = np.ones(len(group), dtype=bool)\n        last_stop_frame = 0\n        for i, (_, row) in enumerate(group.iterrows()):\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[mask])\n\n    submission = pd.concat(group_list)\n\n    if len(submission) != len(old_submission):\n        print(\"ERROR: Dropped duplicate frames\")\n\n    # Fill in missing videos\n    s_list = []\n    for idx, 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        if (submission.video_id == video_id).any():\n            continue\n\n        if type(row.behaviors_labeled) != str:\n            continue\n\n        print(f\"Video {video_id} has no predictions.\")\n\n        path = f\"{traintest_directory}/{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(\n            vid_behaviors, columns=[\"agent\", \"target\", \"action\"]\n        )\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.iterrows()):\n                batch_start = start_frame + i * batch_length\n                batch_stop = min(batch_start + batch_length, stop_frame)\n                s_list.append(\n                    (\n                        video_id,\n                        agent,\n                        target,\n                        action_row[\"action\"],\n                        batch_start,\n                        batch_stop,\n                    )\n                )\n\n    if len(s_list) > 0:\n        submission = pd.concat(\n            [\n                submission,\n                pd.DataFrame(\n                    s_list,\n                    columns=[\n                        \"video_id\",\n                        \"agent_id\",\n                        \"target_id\",\n                        \"action\",\n                        \"start_frame\",\n                        \"stop_frame\",\n                    ],\n                ),\n            ]\n        )\n        print(\"ERROR: Filled empty videos\")\n\n    submission = submission.reset_index(drop=True)\n\n    return submission\n\n\n\nCOUNTER = []\n\n\n# In[3]:\n\n\nimport gc\nimport glob\nimport warnings\nfrom pathlib import Path\nimport warnings\nwarnings.filterwarnings('ignore', message='invalid value encountered')\n\nimport joblib\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nfrom src.config import EXCLUDE_ACTIONS, EXCLUDE_LABS, STANDARD_BODY_PARTS, Config\nfrom src.data_processing import (\n    create_solution_df,\n    generate_mouse_data_unified,\n    preprocess_train_data,\n)\nfrom src.features import _fps_from_meta, transform_pair, transform_single\nfrom src.features_windowed import transform_pair_windowed, transform_single_windowed\nfrom src.temporal import add_temporal_context\nfrom src.metrics import score\n# from src.prediction import predict_multiclass, robustify\nfrom src.utils import postprocess_fps\n\nwarnings.filterwarnings(\"ignore\", category=pd.errors.PerformanceWarning)\n\n\n# In[4]:\n\n\nfrom sklearn.model_selection import StratifiedGroupKFold\n\n\nclass Config:\n    \"\"\"Configuration class for model training and evaluation.\"\"\"\n\n    # Paths\n    train_path = \"/kaggle/input/MABe-mouse-behavior-detection/train.csv\"\n    test_path = \"xgb_test.csv\"\n    train_annotation_path = \"/kaggle/input/MABe-mouse-behavior-detection/train_annotation\"\n    train_tracking_path = \"/kaggle/input/MABe-mouse-behavior-detection/train_tracking\"\n    test_tracking_path = f\"/kaggle/input/MABe-mouse-behavior-detection/{RUN_MODE}_tracking\"\n\n    model_path = \"/kaggle/input\"\n    model_name_prev = \"rats-model-weights\"\n    model_name_baseline = \"mabexgboostv1/models_xgboost\"\n    model_name = \"mabexgboostv2\"\n\n    # Training mode\n    mode = \"validate\"  # or \"submit\"\n    no_fit = True  # Load existing models instead of retraining\n    use_per_lab_thresholds = True  # Optimize thresholds per (action, lab_id) pair\n    use_per_lab_models = (\n        True  # Train separate model per (lab, action) instead of unified\n    )\n    enable_fallback_model = False\n\n    # Windowing settings\n    use_windowing = True  # Aggregate frames into windows\n    base_window_frames = 1  # Window size at 30fps (e.g., 5 frames @ 30fps = ~167ms)\n    target_fps = 30.0  # Target FPS for normalization\n    label_threshold = 0.5  # Fraction of frames in window for positive label\n\n    # Multiprocessing\n    n_jobs = 2  # Number of parallel workers (-1 = all CPUs)\n    n_models = 3  # Number of models to train and average (1 = no ensemble)\n    base_random_seed = 42  # Base seed (will use 42, 43, 44, ... for n_models)    \n\n    # Advanced features\n    use_advanced_features = (\n        True  # Enable Phase 1 advanced features (acceleration, heading, etc.)\n    )\n    temporal_window = 4  # Frames on each side for temporal context (0 = disabled)\n    temporal_stride = 5  # Step between frames (effective span = window * stride)\n    use_xgboost = True  # Use XGBoost instead of CatBoost\n\n    # Cross-validation\n    n_splits = 4\n    cv = StratifiedGroupKFold(n_splits)\n\n    prev_best_labs = [\"PleasantMeerkat\"]\n    baseline_labs = [\"AdaptableSnail\", \"BoisterousParrot\"]\n\n\n# In[5]:\n\n\ndef align_features(X: pd.DataFrame, expected_features: list[str]) -> pd.DataFrame:\n    \"\"\"\n    Align features to expected features. If a feature is not in the DataFrame, it is set to NaN.\n    \"\"\"\n    X = X.copy()\n    return X.reindex(columns=expected_features, fill_value=np.nan)\n\n\ndef generate_submission(dryrun=True, model_name=None):\n    \"\"\"\n    Generate submission.\n\n    Args:\n        dryrun: If True, use training data for validation\n        model_name: Model name to use (default from config)\n    \"\"\"\n    global COUNTER\n    # Load configuration\n    cfg = Config()\n    if model_name:\n        cfg.model_name = model_name\n    cfg.mode = \"submit\"\n    cfg.no_fit = True  # Always load existing models\n\n    print(\"=\" * 80)\n    print(\"SUBMISSION GENERATION\")\n    print(\"=\" * 80)\n    print(f\"Mode: {'DRYRUN (validation)' if dryrun else 'SUBMISSION (test)'}\")\n    print(f\"Model: {cfg.model_name}\")\n    print(f\"Configuration:\")\n    print(f\"  - Windowing: {cfg.use_windowing}\")\n    print(f\"  - Base window frames: {cfg.base_window_frames}\")\n    print(f\"  - Advanced features: {cfg.use_advanced_features}\")\n    print(f\"  - Temporal window: {cfg.temporal_window} (stride={cfg.temporal_stride})\")\n    print(f\"  - Per-lab models: {cfg.use_per_lab_models}\")\n    print(\"=\" * 80)\n\n    # Load data\n    print(\"\\nLoading data...\")\n    train = pd.read_csv(cfg.train_path)\n    train = preprocess_train_data(train)\n    train_without_mabe22 = train.query(\"~lab_id.str.startswith('MABe22_')\")\n\n    # Apply exclusions to training data (for metadata lookups)\n    if EXCLUDE_LABS:\n        print(f\"\\nExcluding labs from train: {EXCLUDE_LABS}\")\n        train_without_mabe22 = train_without_mabe22[\n            ~train_without_mabe22[\"lab_id\"].isin(EXCLUDE_LABS)\n        ]\n\n    # Exclude AdaptableSnail 25fps videos\n    adaptable_25fps_mask = (train_without_mabe22[\"lab_id\"] == \"AdaptableSnail\") & (\n        train_without_mabe22[\"frames_per_second\"] == 25\n    )\n    num_excluded = adaptable_25fps_mask.sum()\n    if num_excluded > 0:\n        train_without_mabe22 = train_without_mabe22[~adaptable_25fps_mask]\n        print(f\"Excluded {num_excluded} AdaptableSnail 25fps videos from train\")\n\n    if dryrun:\n        # Use training data for validation\n        test = train_without_mabe22.copy()\n        test_tracking_path = cfg.train_tracking_path\n        print(f\"DRYRUN: Using {len(test)} training videos as 'test' data\")\n\n        # Load solution for evaluation\n        solution = create_solution_df(train_without_mabe22, cfg.train_annotation_path)\n        print(f\"Solution size: {len(solution)}\")\n    else:\n        # Use actual test data\n        test = pd.read_csv(cfg.test_path)\n        test_tracking_path = cfg.test_tracking_path\n        print(f\"Test videos: {len(test)}\")\n\n\n\n        # Exclude AdaptableSnail 25fps videos\n        adaptable_25fps_mask = (test[\"lab_id\"] == \"AdaptableSnail\") & (test[\"frames_per_second\"] == 25)\n        num_excluded = adaptable_25fps_mask.sum()\n        if num_excluded > 0:\n            test = test[~adaptable_25fps_mask]\n            print(f\"Excluded {num_excluded} AdaptableSnail 25fps videos from train\")\n\n    \n        # test = pd.read_csv(cfg.train_path).query(\"lab_id.str.startswith('PleasantMeerkat')\")\n        # test_tracking_path = cfg.train_tracking_path\n        # print(f\"Test videos: {len(test)}\")\n\n        \n    # Build FPS lookup\n    _fps_lookup = (\n        test[[\"video_id\", \"frames_per_second\"]]\n        .drop_duplicates(\"video_id\")\n        .set_index(\"video_id\")[\"frames_per_second\"]\n        .to_dict()\n    )\n\n    # Generate test data\n    print(\"\\n\" + \"=\" * 80)\n    print(\"GENERATING TEST DATA\")\n    print(\"=\" * 80)\n\n    single_results, pair_results, _ = generate_mouse_data_unified(\n        test,\n        \"test\" if not dryrun else \"train\",\n        traintest_directory=test_tracking_path,\n        track_missing=False,\n        use_windowing=cfg.use_windowing,\n        base_window_frames=cfg.base_window_frames,\n        target_fps=cfg.target_fps,\n        label_threshold=cfg.label_threshold,\n        n_jobs=1, #cfg.n_jobs,\n    )\n\n    print(f\"\\nSingle mouse samples: {len(single_results)}\")\n    print(f\"Mouse pair samples: {len(pair_results)}\")\n\n    submission_list = []\n\n    # Process single mouse data\n    if len(single_results) > 0:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"PROCESSING SINGLE MOUSE DATA\")\n        print(\"=\" * 80)\n\n        for switch, data, meta, actions in tqdm(single_results, desc=\"Single mouse\"):\n            try:\n                # body_parts_tracked = meta[\"body_parts\"].iloc[0]\n                body_parts_tracked = STANDARD_BODY_PARTS\n                # Transform features\n                lab_id = meta[\"lab_id\"].iloc[0]\n                use_prev = lab_id in cfg.prev_best_labs\n                use_baseline = lab_id in cfg.baseline_labs\n\n                if use_prev:\n                    model_name = cfg.model_name_prev\n                elif use_baseline:\n                    model_name = cfg.model_name_baseline\n                else:\n                    model_name = cfg.model_name\n\n                if cfg.use_windowing:\n                    X = transform_single_windowed(\n                        data,\n                        body_parts_tracked,\n                        base_window_frames=cfg.base_window_frames,\n                        use_advanced_features=cfg.use_advanced_features,\n                        meta=meta,\n                    )\n                else:\n                    fps_i = _fps_from_meta(meta, _fps_lookup, default_fps=30.0)\n                    X = transform_single(data, body_parts_tracked, fps_i)\n                \n                if not use_prev:\n                    window = 2 if use_baseline else 4\n                    X = add_temporal_context(X, window, cfg.temporal_stride)\n\n                del data\n                gc.collect()\n\n                pred = pd.DataFrame(index=meta.video_frame)\n\n                for action in actions:\n                    if action in EXCLUDE_ACTIONS:\n                        continue                    \n                    try:\n                        if cfg.use_per_lab_models:\n                            base_model_path = f\"{cfg.model_path}/{model_name}/per_lab_single/{lab_id}/{action}\"\n                        else:\n                            base_model_path = (\n                                f\"{cfg.model_path}/{model_name}/unified_single/{action}\"\n                            )\n\n                        # Check for ensemble models (model_0, model_1, ...) or single model\n                        ensemble_predictions = []\n\n                        for model_idx in range(cfg.n_models):\n                            if cfg.n_models > 1:\n                                model_pattern = (\n                                    f\"{base_model_path}/model_{model_idx}/*_trainer_*.pkl\"\n                                )\n                            else:\n                                model_pattern = f\"{base_model_path}/*_trainer_*.pkl\"                        \n\n                            files = glob.glob(model_pattern)\n\n                            if len(files) == 1:\n                                trainer = joblib.load(files[0])\n                                est = trainer.estimators[0]\n                                feat_names = getattr(est, 'feature_names_in_', None)\n                                if feat_names is None:\n                                    feat_names = est.feature_names_\n                                pred_i = trainer.predict(align_features(X, list(feat_names)))\n                                ensemble_predictions.append(pred_i)\n                                del trainer\n                            else:\n                                print(f\"Couldn't find model for {model_pattern}\")\n\n                        if len(ensemble_predictions) > 0:\n                            pred[action] = np.mean(ensemble_predictions, axis=0)                        \n\n                    except Exception as e:\n                        print(f\"Error processing single mouse action {action}: {e}\")\n                        continue\n\n                del X\n                gc.collect()\n\n                thresholds = {}\n                for action in pred.columns:\n                    if cfg.use_per_lab_models:\n                        threshold_path = f\"{cfg.model_path}/{model_name}/per_lab_single/{lab_id}/{action}/threshold.pkl\"\n                    else:\n                        threshold_path = f\"{cfg.model_path}/{model_name}/unified_single/{action}/thresholds_per_lab.pkl\"\n\n                    if Path(threshold_path).exists():\n                        loaded_threshold = joblib.load(threshold_path)\n                        if isinstance(loaded_threshold, dict):\n                            thresholds[action] = loaded_threshold\n                        else:\n                            thresholds[action] = {lab_id: loaded_threshold}\n                    else:\n                        thresholds[action] = {lab_id: 0.27}\n\n                if pred.shape[1] > 0:\n                    submission_part = predict_multiclass(\n                        pred,\n                        meta,\n                        thresholds,\n                        test,\n                        use_windowing=cfg.use_windowing,\n                        base_window_frames=cfg.base_window_frames,\n                        target_fps=cfg.target_fps,\n                    )\n                    submission_list.append(submission_part)\n\n            except Exception as e:\n                print(f\"Error processing single mouse: {e}\")\n                continue\n\n    # Process mouse pair data\n    if len(pair_results) > 0:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"PROCESSING MOUSE PAIR DATA\")\n        print(\"=\" * 80)\n\n        for switch, data, meta, actions in tqdm(pair_results, desc=\"Mouse pairs\"):\n            try:\n                body_parts_tracked = STANDARD_BODY_PARTS\n                lab_id = meta[\"lab_id\"].iloc[0]\n                use_prev = lab_id in cfg.prev_best_labs\n                use_baseline = lab_id in cfg.baseline_labs\n\n                if use_prev:\n                    model_name = cfg.model_name_prev\n                elif use_baseline:\n                    model_name = cfg.model_name_baseline\n                else:\n                    model_name = cfg.model_name\n\n                if cfg.use_windowing:\n                    X = transform_pair_windowed(\n                        data,\n                        body_parts_tracked,\n                        base_window_frames=cfg.base_window_frames,\n                        use_advanced_features=cfg.use_advanced_features,\n                        meta=meta,\n                    )\n                else:\n                    fps_i = _fps_from_meta(meta, _fps_lookup, default_fps=30.0)\n                    X = transform_pair(data, body_parts_tracked, fps_i)\n                \n                if not use_prev:\n                    window = 2 if use_baseline else 4\n                    X = add_temporal_context(X, window, cfg.temporal_stride)\n\n                del data\n                gc.collect()\n\n                pred = pd.DataFrame(index=meta.video_frame)\n\n                for action in actions:\n                    if action in EXCLUDE_ACTIONS:\n                        continue                    \n                    try:\n                        if cfg.use_per_lab_models:\n                            base_model_path = f\"{cfg.model_path}/{model_name}/per_lab_pair/{lab_id}/{action}\"\n                        else:\n                            base_model_path = (\n                                f\"{cfg.model_path}/{model_name}/per_lab_pair/{action}\"\n                            )\n\n                        # Check for ensemble models (model_0, model_1, ...) or single model\n                        ensemble_predictions = []\n\n                        for model_idx in range(cfg.n_models):\n                            if cfg.n_models > 1:\n                                model_pattern = (\n                                    f\"{base_model_path}/model_{model_idx}/*_trainer_*.pkl\"\n                                )\n                            else:\n                                model_pattern = f\"{base_model_path}/*_trainer_*.pkl\"                        \n\n                            files = glob.glob(model_pattern)\n\n                            if len(files) == 1:\n                                trainer = joblib.load(files[0])\n                                est = trainer.estimators[0]\n                                feat_names = getattr(est, 'feature_names_in_', None)\n                                if feat_names is None:\n                                    feat_names = est.feature_names_\n                                pred_i = trainer.predict(align_features(X, list(feat_names)))\n                                ensemble_predictions.append(pred_i)\n                                del trainer\n                            else:\n                                print(f\"Couldn't find model for {model_pattern}\")\n\n                        if len(ensemble_predictions) > 0:\n                            pred[action] = np.mean(ensemble_predictions, axis=0)                        \n\n                    except Exception as e:\n                        print(f\"Error processing single mouse action {action}: {e}\")\n                        continue\n\n                del X\n                gc.collect()\n\n                thresholds = {}\n                for action in pred.columns:\n                    if cfg.use_per_lab_models:\n                        threshold_path = f\"{cfg.model_path}/{model_name}/per_lab_pair/{lab_id}/{action}/threshold.pkl\"\n                    else:\n                        threshold_path = f\"{cfg.model_path}/{model_name}/unified_pair/{action}/thresholds_per_lab.pkl\"\n\n                    if Path(threshold_path).exists():\n                        loaded_threshold = joblib.load(threshold_path)\n                        if isinstance(loaded_threshold, dict):\n                            thresholds[action] = loaded_threshold\n                        else:\n                            thresholds[action] = {lab_id: loaded_threshold}\n                    else:\n                        thresholds[action] = {lab_id: 0.27}\n\n                if pred.shape[1] > 0:\n                    submission_part = predict_multiclass(\n                        pred,\n                        meta,\n                        thresholds,\n                        test,\n                        use_windowing=cfg.use_windowing,\n                        base_window_frames=cfg.base_window_frames,\n                        target_fps=cfg.target_fps,\n                    )\n                    submission_list.append(submission_part)\n\n            except Exception as e:\n                print(f\"Error processing mouse pair: {e}\")\n                continue\n\n    # Combine and post-process\n    print(\"\\n\" + \"=\" * 80)\n    print(\"POST-PROCESSING\")\n    print(\"=\" * 80)\n\n    if len(submission_list) > 0:\n        print(\"Combining predictions...\")\n        submission = pd.concat(submission_list, ignore_index=True)\n        print(f\"Raw submission size: {len(submission)}\")\n\n        # Robustify\n        print(\"Robustifying...\")\n        submission_robust = robustify(\n            submission,\n            test,\n            \"test\" if not dryrun else \"train\",\n            traintest_directory=test_tracking_path,\n        )\n        print(f\"Robust submission size: {len(submission_robust)}\")\n\n        # Additional post-processing\n        # print(\"Final post-processing...\")\n        # submission_robust = postprocess_fps(submission_robust, test)\n        print(f\"Final submission size: {len(submission_robust)}\")\n    else:\n        print(\"ERROR: No predictions generated!\")\n        return None\n\n    # Evaluate (dryrun only)\n    if dryrun:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"EVALUATION (DRYRUN MODE)\")\n        print(\"=\" * 80)\n\n        comp_score, action_metrics, video_action_metrics, lab_action_metrics = score(\n            solution, submission_robust, \"\", return_details=True\n        )\n\n        print(f\"\\nCompetition metric: {comp_score:.4f}\")\n\n        # Per-action metrics\n        print(\"\\nTop 10 actions by F1:\")\n        action_f1 = [\n            (action, metrics[\"avg_f1\"]) for action, metrics in action_metrics.items()\n        ]\n        action_f1.sort(key=lambda x: x[1], reverse=True)\n        for action, f1 in action_f1[:10]:\n            print(f\"  {action}: {f1:.4f}\")\n\n        print(\"\\nBottom 10 actions by F1:\")\n        for action, f1 in action_f1[-10:]:\n            print(f\"  {action}: {f1:.4f}\")\n\n        # Per-lab metrics\n        print(\"\\nPer-lab average F1:\")\n        for lab, lab_metrics in sorted(lab_action_metrics.items()):\n            lab_f1_scores = [metrics[\"f1\"] for metrics in lab_metrics.values()]\n            lab_avg_f1 = (\n                sum(lab_f1_scores) / len(lab_f1_scores) if lab_f1_scores else 0.0\n            )\n            print(f\"  {lab}: {lab_avg_f1:.4f}\")\n\n    # Save submission\n    print(\"\\n\" + \"=\" * 80)\n    print(\"SAVING SUBMISSION\")\n    print(\"=\" * 80)\n\n    if dryrun:\n        output_file = f\"{cfg.model_name}_dryrun_submission.csv\"\n    else:\n        output_file = \"submission.csv\"\n\n    submission_robust.index.name = \"row_id\"\n    submission_robust.to_csv(output_file)\n\n    print(f\"\\nSubmission saved to: {output_file}\")\n    print(f\"Rows: {len(submission_robust)}\")\n\n    # Validation checks\n    print(\"\\n\" + \"=\" * 80)\n    print(\"VALIDATION CHECKS\")\n    print(\"=\" * 80)\n\n    # Check 1: No overlapping intervals\n    overlaps = 0\n    for (vid, agent, target), group in submission_robust.groupby(\n        [\"video_id\", \"agent_id\", \"target_id\"]\n    ):\n        group = group.sort_values(\"start_frame\")\n        for i in range(len(group) - 1):\n            if group.iloc[i][\"stop_frame\"] > group.iloc[i + 1][\"start_frame\"]:\n                overlaps += 1\n\n    print(f\"  Overlapping intervals: {overlaps} {'✓' if overlaps == 0 else '✗'}\")\n\n    # Check 2: Valid frame ranges\n    invalid_ranges = (\n        submission_robust[\"start_frame\"] >= submission_robust[\"stop_frame\"]\n    ).sum()\n    print(\n        f\"  Invalid frame ranges: {invalid_ranges} {'✓' if invalid_ranges == 0 else '✗'}\"\n    )\n\n    # Check 3: Required columns\n    required_cols = [\n        \"video_id\",\n        \"agent_id\",\n        \"target_id\",\n        \"action\",\n        \"start_frame\",\n        \"stop_frame\",\n    ]\n    missing_cols = [\n        col for col in required_cols if col not in submission_robust.columns\n    ]\n    print(\n        f\"  Required columns: {'✓' if not missing_cols else f'✗ Missing: {missing_cols}'}\"\n    )\n\n    # Check 4: Action distribution\n    print(f\"\\nAction distribution:\")\n    action_counts = submission_robust[\"action\"].value_counts()\n    for action, count in action_counts.head(10).items():\n        print(f\"  {action}: {count}\")\n\n    if dryrun:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"DRYRUN COMPLETE\")\n        print(f\"Competition metric: {comp_score:.4f}\")\n        print(\"If metrics look good, run with DRYRUN=False for actual submission\")\n        print(\"=\" * 80)\n    else:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"SUBMISSION READY FOR KAGGLE!\")\n        print(f\"Upload: {output_file}\")\n        print(\"=\" * 80)\n\n    return submission_robust\n\n\n# In[6]:\n\n\nif __name__ == '__main__':\n    generate_submission(dryrun=False)\n\n\n# In[7]:\n\n\n#### COUNTER\n\n\n# In[8]:\n\n\n# if len(set(COUNTER)) >= 10:\n#     import os\n#     os.remove(\"submission.csv\")\n#     asdasd\n#     raise ValueError","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:12:04.868493Z","iopub.execute_input":"2025-12-15T05:12:04.868854Z","iopub.status.idle":"2025-12-15T05:12:04.893457Z","shell.execute_reply.started":"2025-12-15T05:12:04.868821Z","shell.execute_reply":"2025-12-15T05:12:04.892368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python submission.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:12:04.894711Z","iopub.execute_input":"2025-12-15T05:12:04.895198Z","iopub.status.idle":"2025-12-15T05:19:18.621846Z","shell.execute_reply.started":"2025-12-15T05:12:04.895168Z","shell.execute_reply":"2025-12-15T05:19:18.620443Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!rm -rf train_tracking_filled test_tracking_filled src interpolate_tracking_data.py submission.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:19:18.623357Z","iopub.execute_input":"2025-12-15T05:19:18.623704Z","iopub.status.idle":"2025-12-15T05:19:18.752545Z","shell.execute_reply.started":"2025-12-15T05:19:18.623672Z","shell.execute_reply":"2025-12-15T05:19:18.751240Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# XGB EDU","metadata":{}},{"cell_type":"code","source":"!cp -r /kaggle/input/rats-mouse-source-behavior2/src/ /kaggle/working\n!cp /kaggle/input/rats-mouse-source-behavior2/interpolate_tracking_data.py /kaggle/working","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:19:18.754144Z","iopub.execute_input":"2025-12-15T05:19:18.754540Z","iopub.status.idle":"2025-12-15T05:19:19.056329Z","shell.execute_reply.started":"2025-12-15T05:19:18.754501Z","shell.execute_reply":"2025-12-15T05:19:19.055199Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python interpolate_tracking_data.py --input_dir=\"/kaggle/input/MABe-mouse-behavior-detection/train_tracking\" --output_dir=\"/kaggle/working/train_tracking_filled\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:19:19.057871Z","iopub.execute_input":"2025-12-15T05:19:19.058301Z","iopub.status.idle":"2025-12-15T05:19:52.621451Z","shell.execute_reply.started":"2025-12-15T05:19:19.058261Z","shell.execute_reply":"2025-12-15T05:19:52.619925Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python interpolate_tracking_data.py --input_dir=\"/kaggle/input/MABe-mouse-behavior-detection/test_tracking\" --output_dir=\"/kaggle/working/test_tracking_filled\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:19:52.626040Z","iopub.execute_input":"2025-12-15T05:19:52.626650Z","iopub.status.idle":"2025-12-15T05:19:53.526285Z","shell.execute_reply.started":"2025-12-15T05:19:52.626602Z","shell.execute_reply":"2025-12-15T05:19:53.525183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile submission.py\nimport gc\nimport glob\nimport warnings\nfrom pathlib import Path\nfrom typing import List\n\nimport joblib\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nfrom src.config import EXCLUDE_ACTIONS, STANDARD_BODY_PARTS, Config\nfrom src.data_processing import (\n    create_solution_df,\n    generate_mouse_data_unified,\n    preprocess_train_data,\n)\nfrom src.features import _fps_from_meta, transform_pair, transform_single\nfrom src.features_windowed import transform_pair_windowed, transform_single_windowed\nfrom src.metrics import score\nfrom src.prediction import predict_multiclass, robustify\nfrom src.utils import postprocess_fps\nfrom src.features_temporal_context import add_selective_temporal_context\nimport os\n\nRUN_MODE = os.environ.get('RUN_MODE')\n\nwarnings.filterwarnings(\"ignore\", category=pd.errors.PerformanceWarning)\nwarnings.filterwarnings(\n    \"ignore\", message=\".*No visible GPU is found.*\"\n)  # XGBoost GPU warnings\nwarnings.filterwarnings(\n    \"ignore\", message=\".*Device is changed from GPU to CPU.*\"\n)  # XGBoost GPU warnings\nnp.seterr(all=\"ignore\")  # Suppress numpy warnings\n\nfrom sklearn.model_selection import StratifiedGroupKFold\n\n\nEXCLUDE_LABS = [\"CRIM13\", \"CalMS21_supplemental\", \"CalMS21_task1\", \"CalMS21_task2\"]\n\nclass Config:\n    \"\"\"Configuration class for model training and evaluation.\"\"\"\n\n    # Paths\n    train_path = \"/kaggle/input/MABe-mouse-behavior-detection/train.csv\"\n    test_path = \"edu_xgb_test.csv\"\n    train_annotation_path = \"/kaggle/input/MABe-mouse-behavior-detection/train_annotation\"\n    train_tracking_path_raw = \"/kaggle/input/MABe-mouse-behavior-detection/train_tracking\"\n    train_tracking_path_filled = \"/kaggle/working/train_tracking_filled\"\n    # Default tracking path used when no lab-specific override is applied\n    train_tracking_path = train_tracking_path_raw\n    test_tracking_path = f\"/kaggle/input/MABe-mouse-behavior-detection/{RUN_MODE}_tracking\"\n    test_tracking_path_filled = f\"/kaggle/working/{RUN_MODE}_tracking_filled\"\n    # Labs that benefit from interpolated tracking data\n    labs_use_interpolated = [\n        \"CautiousGiraffe\",\n        \"DeliriousFly\",\n        \"GroovyShrew\",\n        \"TranquilPanther\",\n    ]\n\n    # Interpolation\n    use_interpolated_tracking = True  # Use interpolated tracking data\n    add_interpolation_features = (\n        True  # Add features indicating if keypoints were interpolated\n    )\n\n    # Caching\n    use_data_cache = False  # Cache processed data to disk for faster retraining\n    cache_dir = \"/kaggle/working/.cache\"  # Directory for cache files\n\n    model_path = \"/kaggle/input\"\n    model_name = \"rats-model-weights2\"\n\n    # Training mode\n    mode = \"validate\"  # or \"submit\"\n    no_fit = True  # Load existing models instead of retraining\n    use_per_lab_thresholds = True  # Optimize thresholds per (action, lab_id) pair\n    use_per_lab_models = (\n        True  # Train separate model per (lab, action) instead of unified\n    )\n\n    # Windowing settings\n    use_windowing = True  # Aggregate frames into windows\n    base_window_frames = 1  # Window size at 30fps (e.g., 5 frames @ 30fps = ~167ms)\n    target_fps = 30.0  # Target FPS for normalization\n    label_threshold = 0.5  # Fraction of frames in window for positive label\n\n    # Multiprocessing\n    n_jobs = 12  # Number of parallel workers (-1 = all CPUs)\n\n    # Advanced features\n    use_advanced_features = (\n        True  # Enable Phase 1 advanced features (acceleration, heading, etc.)\n    )\n\n    # Temporal context (lagged features)\n    use_temporal_context = False  # Add lagged versions of important features\n    temporal_context_top_n = 25  # Number of top features to add temporal context for\n    temporal_context_window = (\n        2  # Frames before/after (e.g., 2 = offsets -6,-3,+3,+6 with stride=3)\n    )\n    temporal_context_stride = 15  # Stride between offsets\n\n    # Cross-validation\n    n_splits = 4\n    n_models = 3  # Number of models to train and average (1 = no ensemble)\n    base_random_seed = 42  # Base seed (will use 42, 43, 44, ... for n_models)\n    cv = StratifiedGroupKFold(n_splits)\n\n    # Special lab handling\n    use_tranquil_panther_merge = (\n        False  # Merge mouse1 and mouse2 features for TranquilPanther\n    )\n\n    # Filtering\n    # Set to None to use all, or provide list to filter\n    filter_labs = None  # [\"GroovyShrew\"]  # [\"SparklingTapir\", \"DeliriousFly\"]\n    # [\n    #     \"ElegantMink\",\n    #     \"GroovyShrew\",\n    # ]  # e.g., [\"PleasantMeerkat\", \"AdaptableSnail\"]\n    filter_actions = None\n    # [\n    #     \"allogroom\",\n    #     \"selfgroom\",\n    #     \"dominancegroom\",\n    #     \"genitalgroom\",\n    # ]  # e.g., [\"attack\", \"chase\", \"escape\"]\n    use_lagged_features = True\n    single_pair_lagged_feats = None\n    # single_pair_lagged_feats = [\n    #     \"nose+body_center\",\n    #     \"ear_left+ear_right\",\n    #     \"elong\",\n    #     \"cx_s12\",\n    #     \"sp_m4\",\n    #     \"x_rng12\",\n    #     \"sp_rt2\",\n    #     \"tail_base_sp_m3\",\n    #     \"nose_tail_min_3\",\n    #     \"edge_dist_min_6\",\n    #     \"body_center+tail_base\",\n    #     \"y_rng12\",\n    # ]\n    pair_lagged_feats = None\n    # pair_lagged_feats = [\n    #     \"12+nose+tail_base\",\n    #     \"12+nose+body_center\",\n    #     \"A_nose_B_body_proximity\",\n    #     \"A_nose_B_dist_min_6\",\n    #     \"A_nose_speed_x_proximity_6\",\n    #     \"d_mn6\",\n    #     \"co_s12\",\n    #     \"A_nose_B_dist_mean_6\",\n    #     # \"A_nose_speed_x_proximity_3\",\n    #     \"12+nose+nose\",\n    #     # \"co_m12\",\n    #     # \"d_m6\",\n    #     # \"d_mx12\",\n    #     # \"d_m12\",\n    # ]\n\n\n# Standardized body parts used across all labs\nSTANDARD_BODY_PARTS = [\"ear_left\", \"ear_right\", \"nose\", \"body_center\", \"tail_base\"]\n\n# Body parts to drop from videos with many tracked points\nDROP_BODY_PARTS = [\n    \"headpiece_bottombackleft\",\n    \"headpiece_bottombackright\",\n    \"headpiece_bottomfrontleft\",\n    \"headpiece_bottomfrontright\",\n    \"headpiece_topbackleft\",\n    \"headpiece_topbackright\",\n    \"headpiece_topfrontleft\",\n    \"headpiece_topfrontright\",\n    \"spine_1\",\n    \"spine_2\",\n    \"tail_middle_1\",\n    \"tail_middle_2\",\n    \"tail_midpoint\",\n]\n\nEXCLUDE_ACTIONS = [\"ejaculate\"]\n\ndef add_temporal_context_stats(\n    X: pd.DataFrame,\n    window: int = 30,\n    stride: int = 30,\n    n_offsets: int = 2,\n    feats: List[str] = None,\n    stats: List[str] = [\"median\", \"std\", \"min\", \"max\"],\n) -> pd.DataFrame:\n    \"\"\"\n    Add temporal context by computing statistics over windows at different offsets.\n\n    Instead of just shifting features, this computes aggregated statistics (median, std, min, max)\n    over small windows at different temporal positions.\n\n    Args:\n        X: Feature DataFrame\n        window: Size of window (in frames) to compute stats over at each offset position\n        stride: Spacing between offset positions (e.g., 30 frames = 1 second at 30fps)\n        n_offsets: Number of offset positions in each direction (past and future)\n        feats: List of features to compute temporal stats for (if None, uses all)\n        stats: List of statistics to compute [\"median\", \"std\", \"min\", \"max\", \"mean\"]\n\n    Returns:\n        DataFrame with original features plus temporal statistic features\n\n    Example:\n        With window=3, stride=30, n_offsets=2:\n        - At frame i, we compute stats over windows centered at:\n          i-60 (frames i-61.5 to i-58.5)\n          i-30 (frames i-31.5 to i-28.5)\n          i+30 (frames i+28.5 to i+31.5)\n          i+60 (frames i+58.5 to i+61.5)\n    \"\"\"\n    if n_offsets <= 0:\n        return X\n\n    n_samples = X.shape[0]\n    original_n_feats = len(X.columns)\n\n    # Determine which features to use\n    if feats is None:\n        cols_to_use = X.columns.tolist()\n    else:\n        cols_to_use = [col for col in feats if col in X.columns]\n        if len(cols_to_use) == 0:\n            print(f\"  WARNING: None of the specified features found in X\")\n            return X\n\n    # Start with original dataframe\n    result_dfs = [X]\n\n    # Extract subset of features for efficiency\n    X_subset = X[cols_to_use]\n\n    # Compute stats at different temporal offsets\n    for i in range(-n_offsets, n_offsets + 1):\n        if i == 0:\n            continue  # Skip center position (that's the original data)\n\n        offset = i * stride\n\n        # Shift the data by the offset\n        if offset > 0:\n            # Shift down (looking into past from each frame's perspective)\n            shifted_data = X_subset.shift(offset)\n        else:\n            # Shift up (looking into future)\n            shifted_data = X_subset.shift(offset)\n\n        # Compute rolling statistics over the window at the shifted position\n        # min_periods controls how many non-NaN values required:\n        # - min_periods=1: compute stat if at least 1 non-NaN value (lenient, less NaNs)\n        # - min_periods=window//2+1: require majority of window (moderate)\n        # - min_periods=window: require all values (strict, more NaNs)\n        min_periods = max(\n            1, window // 2\n        )  # Require at least half the window to be valid\n\n        for stat_name in stats:\n            if stat_name == \"median\":\n                stat_values = shifted_data.rolling(\n                    window=window, min_periods=min_periods, center=True\n                ).median()\n            elif stat_name == \"mean\":\n                stat_values = shifted_data.rolling(\n                    window=window, min_periods=min_periods, center=True\n                ).mean()\n            elif stat_name == \"std\":\n                stat_values = shifted_data.rolling(\n                    window=window, min_periods=min_periods, center=True\n                ).std()\n            elif stat_name == \"min\":\n                stat_values = shifted_data.rolling(\n                    window=window, min_periods=min_periods, center=True\n                ).min()\n            elif stat_name == \"max\":\n                stat_values = shifted_data.rolling(\n                    window=window, min_periods=min_periods, center=True\n                ).max()\n            else:\n                continue  # Skip unknown stat types\n\n            # Rename columns with offset and stat suffix\n            stat_values.columns = [f\"{c}_t{offset:+d}_{stat_name}\" for c in cols_to_use]\n            result_dfs.append(stat_values)\n\n    # Concatenate all dataframes\n    result = pd.concat(result_dfs, axis=1)\n    print(\n        f\"  Added {len(result.columns) - original_n_feats} temporal statistic features\"\n    )\n\n    return result\n    \n\ndef align_features(X: pd.DataFrame, expected_features: list[str]) -> pd.DataFrame:\n    \"\"\"\n    Align features to expected features. If a feature is not in the DataFrame, it is set to NaN.\n    \"\"\"\n    X = X.copy()\n    return X.reindex(columns=expected_features, fill_value=np.nan)\n\ndef load_models_and_predict(\n    X: pd.DataFrame,\n    base_model_path: str,\n    lab_id: str,\n    action: str,\n    cfg: Config,\n    switch: str,\n) -> tuple[np.ndarray, float]:\n    \"\"\"\n    Load models and generate predictions, with support for temporal features.\n\n    Returns:\n        predictions: Average predictions from ensemble\n        threshold: Threshold for this action\n    \"\"\"\n    # Check if temporal models exist\n    temporal_model_path = f\"{base_model_path}_temporal\"\n    use_temporal = Path(f\"{temporal_model_path}/feature_importance.pkl\").exists()\n\n    if use_temporal:\n        # Load feature importance and apply temporal context\n        importance_dict = joblib.load(f\"{temporal_model_path}/feature_importance.pkl\")\n        X_orig_cols = X.shape[1]\n        X = add_selective_temporal_context(\n            X,\n            importance_dict,\n            top_n=cfg.temporal_context_top_n,\n            window=cfg.temporal_context_window,\n            stride=cfg.temporal_context_stride,\n        )\n        # if X.shape[1] > X_orig_cols:\n        #     print(\n        #         f\"    [Temporal] {action}: Added {X.shape[1] - X_orig_cols} temporal features\"\n        #     )\n        model_path = temporal_model_path\n        threshold_path = f\"{temporal_model_path}/threshold.pkl\"\n    else:\n        model_path = base_model_path\n        threshold_path = f\"{base_model_path}/threshold.pkl\"\n\n    # Load ensemble models\n    ensemble_predictions = []\n\n    for model_idx in range(cfg.n_models):\n        if cfg.n_models > 1:\n            model_pattern = f\"{model_path}/model_{model_idx}/*_trainer_*.pkl\"\n        else:\n            model_pattern = f\"{model_path}/*_trainer_*.pkl\"\n\n        files = glob.glob(model_pattern)\n\n        if model_idx == 0 and cfg.use_lagged_features:\n            if \"pair\" in switch:\n                lagged_feats = cfg.pair_lagged_feats\n            else:\n                lagged_feats = cfg.single_pair_lagged_feats\n            X = add_temporal_context_stats(\n                X,\n                window=cfg.temporal_context_stride,\n                n_offsets=cfg.temporal_context_window,\n                stride=cfg.temporal_context_stride,\n                feats=lagged_feats,\n            )        \n\n        if len(files) == 1:\n            trainer = joblib.load(files[0])\n            pred_i = trainer.predict(\n                align_features(X, trainer.estimators[0].feature_names_in_)\n            )\n            ensemble_predictions.append(pred_i)\n            del trainer\n        elif model_idx == 0:\n            # If first model not found, break\n            break\n\n    if len(ensemble_predictions) == 0:\n        return None, None\n\n    # Average predictions from ensemble\n    predictions = np.mean(ensemble_predictions, axis=0)\n\n    # Load threshold\n    if Path(threshold_path).exists():\n        threshold = joblib.load(threshold_path)\n        if isinstance(threshold, dict):\n            threshold = threshold.get(lab_id, 0.27)\n    else:\n        threshold = 0.27\n\n    return predictions, threshold\n\ndef generate_submission(dryrun=True, model_name=None):\n    \"\"\"\n    Generate submission.\n\n    Args:\n        dryrun: If True, use training data for validation\n        model_name: Model name to use (default from config)\n    \"\"\"\n    global COUNTER\n    # Load configuration\n    cfg = Config()\n    if model_name:\n        cfg.model_name = model_name\n    cfg.mode = \"submit\"\n    cfg.no_fit = True  # Always load existing models\n\n    print(\"=\" * 80)\n    print(\"SUBMISSION GENERATION\")\n    print(\"=\" * 80)\n    print(f\"Mode: {'DRYRUN (validation)' if dryrun else 'SUBMISSION (test)'}\")\n    print(f\"Model: {cfg.model_name}\")\n    print(f\"Configuration:\")\n    print(f\"  - Windowing: {cfg.use_windowing}\")\n    print(f\"  - Base window frames: {cfg.base_window_frames}\")\n    print(f\"  - Advanced features: {cfg.use_advanced_features}\")\n    print(f\"  - Per-lab models: {cfg.use_per_lab_models}\")\n    print(\"=\" * 80)\n\n    # Load data\n    print(\"\\nLoading data...\")\n    train = pd.read_csv(cfg.train_path)\n    train = preprocess_train_data(train)\n    train_without_mabe22 = train.query(\"~lab_id.str.startswith('MABe22_')\")\n    test = pd.read_csv(cfg.test_path)\n    \n    # Apply exclusions to training data (for metadata lookups)\n    if EXCLUDE_LABS:\n        print(f\"\\nExcluding labs from train: {EXCLUDE_LABS}\")\n        train_without_mabe22 = train_without_mabe22[\n            ~train_without_mabe22[\"lab_id\"].isin(EXCLUDE_LABS)\n        ]\n        test = test[\n            ~test[\"lab_id\"].isin(EXCLUDE_LABS)\n        ]        \n        \n\n    # Exclude AdaptableSnail 25fps videos\n    adaptable_25fps_mask = (train_without_mabe22[\"lab_id\"] == \"AdaptableSnail\") & (\n        train_without_mabe22[\"frames_per_second\"] == 25\n    )\n    num_excluded = adaptable_25fps_mask.sum()\n    if num_excluded > 0:\n        train_without_mabe22 = train_without_mabe22[~adaptable_25fps_mask]\n        print(f\"Excluded {num_excluded} AdaptableSnail 25fps videos from train\")\n\n    if dryrun:\n        # Use training data for validation\n        train_without_mabe22 = train_without_mabe22[~train_without_mabe22[\"behaviors_labeled\"].isna()]\n        train_without_mabe22 = train_without_mabe22.groupby(\"lab_id\").sample(1)\n        test = train_without_mabe22.copy()\n        test_tracking_path = cfg.train_tracking_path_raw\n        lab_dir_map = {\n            lab_id: cfg.train_tracking_path_filled\n            for lab_id in cfg.labs_use_interpolated\n        }\n        print(f\"DRYRUN: Using {len(test)} training videos as 'test' data\")\n\n        # Load solution for evaluation\n        solution = create_solution_df(train_without_mabe22, cfg.train_annotation_path)\n        print(f\"Solution size: {len(solution)}\")\n    else:\n        # Use actual test data\n        test_tracking_path = cfg.test_tracking_path\n        lab_dir_map = None\n        # if not Path(cfg.test_tracking_path_filled).exists() and len(test) > 1:\n        #     raise ValueError\n        if Path(cfg.test_tracking_path_filled).exists():\n            lab_dir_map = {\n                lab_id: cfg.test_tracking_path_filled\n                for lab_id in cfg.labs_use_interpolated\n            }        \n        print(f\"Test videos: {len(test)}\")\n\n    # Build FPS lookup\n    _fps_lookup = (\n        test[[\"video_id\", \"frames_per_second\"]]\n        .drop_duplicates(\"video_id\")\n        .set_index(\"video_id\")[\"frames_per_second\"]\n        .to_dict()\n    )\n\n    # Generate test data\n    print(\"\\n\" + \"=\" * 80)\n    print(\"GENERATING TEST DATA\")\n    print(\"=\" * 80)\n\n    single_results, pair_results, _ = generate_mouse_data_unified(\n        test,\n        \"test\" if not dryrun else \"train\",\n        traintest_directory=test_tracking_path,\n        lab_directory_map=lab_dir_map,\n        track_missing=False,\n        use_windowing=cfg.use_windowing,\n        base_window_frames=cfg.base_window_frames,\n        target_fps=cfg.target_fps,\n        label_threshold=cfg.label_threshold,\n        n_jobs=1, #cfg.n_jobs,\n        use_tranquil_panther_merge=cfg.use_tranquil_panther_merge,\n    )\n\n    print(f\"\\nSingle mouse samples: {len(single_results)}\")\n    print(f\"Mouse pair samples: {len(pair_results)}\")\n\n    submission_list = []\n\n    # Process single mouse data\n    if len(single_results) > 0:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"PROCESSING SINGLE MOUSE DATA\")\n        print(\"=\" * 80)\n\n        for switch, data, meta, actions in tqdm(single_results, desc=\"Single mouse\"):\n            try:\n                # body_parts_tracked = meta[\"body_parts\"].iloc[0]\n                body_parts_tracked = STANDARD_BODY_PARTS\n                # Transform features\n                if cfg.use_windowing:\n                    X = transform_single_windowed(\n                        data,\n                        body_parts_tracked,\n                        base_window_frames=cfg.base_window_frames,\n                        use_advanced_features=cfg.use_advanced_features,\n                        meta=meta,\n                    )\n                else:\n                    fps_i = _fps_from_meta(meta, _fps_lookup, default_fps=30.0)\n                    X = transform_single(data, body_parts_tracked, fps_i)\n\n                del data\n                gc.collect()\n\n                # Load models and predict\n                pred = pd.DataFrame(index=meta.video_frame)\n                thresholds = {}\n                lab_id = meta[\"lab_id\"].iloc[0]\n\n                for action in actions:\n                    if action in EXCLUDE_ACTIONS:\n                        continue                    \n                    try:\n                        # Try per-lab model first, fall back to unified\n                        if cfg.use_per_lab_models:\n                            base_model_path = f\"{cfg.model_path}/{cfg.model_name}/per_lab_single/{lab_id}/{action}\"\n                        else:\n                            base_model_path = (\n                                f\"{cfg.model_path}/{cfg.model_name}/unified_single/{action}\"\n                            )\n\n                        # Load models and predict (handles temporal features automatically)\n                        predictions, threshold = load_models_and_predict(\n                            X, base_model_path, lab_id, action, cfg, switch=\"per_lab_single\"\n                        )\n    \n                        if predictions is not None:\n                            pred[action] = predictions\n                            thresholds[action] = {lab_id: threshold}                       \n   \n                    except Exception as e:\n                        print(f\"Error processing single mouse action {action}: {e}\")\n                        continue\n\n                del X\n                gc.collect()\n\n                if pred.shape[1] > 0:\n                    submission_part = predict_multiclass(\n                        pred,\n                        meta,\n                        thresholds,\n                        test,\n                        use_windowing=cfg.use_windowing,\n                        base_window_frames=cfg.base_window_frames,\n                        target_fps=cfg.target_fps,\n                    )\n                    submission_list.append(submission_part)\n\n            except Exception as e:\n                print(f\"Error processing single mouse: {e}\")\n                continue\n\n    # Process mouse pair data\n    if len(pair_results) > 0:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"PROCESSING MOUSE PAIR DATA\")\n        print(\"=\" * 80)\n\n        for switch, data, meta, actions in tqdm(pair_results, desc=\"Mouse pairs\"):\n            try:\n                # body_parts_tracked = meta[\"body_parts\"].iloc[0]\n                body_parts_tracked = STANDARD_BODY_PARTS\n                # Transform features\n                if cfg.use_windowing:\n                    X = transform_pair_windowed(\n                        data,\n                        body_parts_tracked,\n                        base_window_frames=cfg.base_window_frames,\n                        use_advanced_features=cfg.use_advanced_features,\n                        meta=meta,\n                    )\n                else:\n                    fps_i = _fps_from_meta(meta, _fps_lookup, default_fps=30.0)\n                    X = transform_pair(data, body_parts_tracked, fps_i)\n\n                del data\n                gc.collect()\n\n                # Load models and predict\n                pred = pd.DataFrame(index=meta.video_frame)\n                thresholds = {}\n                lab_id = meta[\"lab_id\"].iloc[0]\n\n                for action in actions:\n                    if action in EXCLUDE_ACTIONS:\n                        continue                    \n                    try:\n                        # Try per-lab model first, fall back to unified\n                        if cfg.use_per_lab_models:\n                            base_model_path = f\"{cfg.model_path}/{cfg.model_name}/per_lab_pair/{lab_id}/{action}\"\n                        else:\n                            base_model_path = (\n                                f\"{cfg.model_path}/{cfg.model_name}/per_lab_pair/{action}\"\n                            )\n\n                        # Load models and predict (handles temporal features automatically)\n                        predictions, threshold = load_models_and_predict(\n                            X, base_model_path, lab_id, action, cfg, switch=\"per_lab_pair\"\n                        )\n                        \n                        if predictions is not None:\n                            pred[action] = predictions\n                            thresholds[action] = {lab_id: threshold}                        \n   \n                    except Exception as e:\n                        print(f\"Error processing single mouse action {action}: {e}\")\n                        continue\n\n                del X\n                gc.collect()\n\n                if pred.shape[1] > 0:\n                    submission_part = predict_multiclass(\n                        pred,\n                        meta,\n                        thresholds,\n                        test,\n                        use_windowing=cfg.use_windowing,\n                        base_window_frames=cfg.base_window_frames,\n                        target_fps=cfg.target_fps,\n                    )\n                    submission_list.append(submission_part)\n\n            except Exception as e:\n                print(f\"Error processing mouse pair: {e}\")\n                continue\n\n    # Combine and post-process\n    print(\"\\n\" + \"=\" * 80)\n    print(\"POST-PROCESSING\")\n    print(\"=\" * 80)\n\n    if len(submission_list) > 0:\n        print(\"Combining predictions...\")\n        submission = pd.concat(submission_list, ignore_index=True)\n        print(f\"Raw submission size: {len(submission)}\")\n\n        # Robustify\n        print(\"Robustifying...\")\n        submission_robust = robustify(\n            submission,\n            test,\n            \"test\" if not dryrun else \"train\",\n            traintest_directory=test_tracking_path,\n        )\n        print(f\"Robust submission size: {len(submission_robust)}\")\n\n        # Additional post-processing\n        # print(\"Final post-processing...\")\n        # submission_robust = postprocess_fps(submission_robust, test)\n        print(f\"Final submission size: {len(submission_robust)}\")\n    else:\n        print(\"ERROR: No predictions generated!\")\n        return None\n\n    # Evaluate (dryrun only)\n    if dryrun:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"EVALUATION (DRYRUN MODE)\")\n        print(\"=\" * 80)\n\n        comp_score, action_metrics, video_action_metrics, lab_action_metrics = score(\n            solution, submission_robust, \"\", return_details=True\n        )\n\n        print(f\"\\nCompetition metric: {comp_score:.4f}\")\n\n        # Per-action metrics\n        print(\"\\nTop 10 actions by F1:\")\n        action_f1 = [\n            (action, metrics[\"avg_f1\"]) for action, metrics in action_metrics.items()\n        ]\n        action_f1.sort(key=lambda x: x[1], reverse=True)\n        for action, f1 in action_f1[:10]:\n            print(f\"  {action}: {f1:.4f}\")\n\n        print(\"\\nBottom 10 actions by F1:\")\n        for action, f1 in action_f1[-10:]:\n            print(f\"  {action}: {f1:.4f}\")\n\n        # Per-lab metrics\n        print(\"\\nPer-lab average F1:\")\n        for lab, lab_metrics in sorted(lab_action_metrics.items()):\n            lab_f1_scores = [metrics[\"f1\"] for metrics in lab_metrics.values()]\n            lab_avg_f1 = (\n                sum(lab_f1_scores) / len(lab_f1_scores) if lab_f1_scores else 0.0\n            )\n            print(f\"  {lab}: {lab_avg_f1:.4f}\")\n\n    # Save submission\n    print(\"\\n\" + \"=\" * 80)\n    print(\"SAVING SUBMISSION\")\n    print(\"=\" * 80)\n\n    output_file = \"submission2.csv\"\n\n    submission_robust.index.name = \"row_id\"\n    submission_robust.to_csv(output_file)\n\n    print(f\"\\nSubmission saved to: {output_file}\")\n    print(f\"Rows: {len(submission_robust)}\")\n\n    # Validation checks\n    print(\"\\n\" + \"=\" * 80)\n    print(\"VALIDATION CHECKS\")\n    print(\"=\" * 80)\n\n    # Check 1: No overlapping intervals\n    overlaps = 0\n    for (vid, agent, target), group in submission_robust.groupby(\n        [\"video_id\", \"agent_id\", \"target_id\"]\n    ):\n        group = group.sort_values(\"start_frame\")\n        for i in range(len(group) - 1):\n            if group.iloc[i][\"stop_frame\"] > group.iloc[i + 1][\"start_frame\"]:\n                overlaps += 1\n\n    print(f\"  Overlapping intervals: {overlaps} {'✓' if overlaps == 0 else '✗'}\")\n\n    # Check 2: Valid frame ranges\n    invalid_ranges = (\n        submission_robust[\"start_frame\"] >= submission_robust[\"stop_frame\"]\n    ).sum()\n    print(\n        f\"  Invalid frame ranges: {invalid_ranges} {'✓' if invalid_ranges == 0 else '✗'}\"\n    )\n\n    # Check 3: Required columns\n    required_cols = [\n        \"video_id\",\n        \"agent_id\",\n        \"target_id\",\n        \"action\",\n        \"start_frame\",\n        \"stop_frame\",\n    ]\n    missing_cols = [\n        col for col in required_cols if col not in submission_robust.columns\n    ]\n    print(\n        f\"  Required columns: {'✓' if not missing_cols else f'✗ Missing: {missing_cols}'}\"\n    )\n\n    # Check 4: Action distribution\n    print(f\"\\nAction distribution:\")\n    action_counts = submission_robust[\"action\"].value_counts()\n    for action, count in action_counts.head(10).items():\n        print(f\"  {action}: {count}\")\n\n    if dryrun:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"DRYRUN COMPLETE\")\n        print(f\"Competition metric: {comp_score:.4f}\")\n        print(\"If metrics look good, run with DRYRUN=False for actual submission\")\n        print(\"=\" * 80)\n    else:\n        print(\"\\n\" + \"=\" * 80)\n        print(\"SUBMISSION READY FOR KAGGLE!\")\n        print(f\"Upload: {output_file}\")\n        print(\"=\" * 80)\n\n    return submission_robust\n\nif __name__ == \"__main__\":\n    generate_submission(dryrun=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:19:53.527862Z","iopub.execute_input":"2025-12-15T05:19:53.528180Z","iopub.status.idle":"2025-12-15T05:19:53.550185Z","shell.execute_reply.started":"2025-12-15T05:19:53.528147Z","shell.execute_reply":"2025-12-15T05:19:53.549178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python submission.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:19:53.551093Z","iopub.execute_input":"2025-12-15T05:19:53.551402Z","iopub.status.idle":"2025-12-15T05:33:47.353283Z","shell.execute_reply.started":"2025-12-15T05:19:53.551382Z","shell.execute_reply":"2025-12-15T05:33:47.351469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!rm -rf train_tracking_filled test_tracking_filled src interpolate_tracking_data.py submission.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:33:47.355937Z","iopub.execute_input":"2025-12-15T05:33:47.356380Z","iopub.status.idle":"2025-12-15T05:33:47.500319Z","shell.execute_reply.started":"2025-12-15T05:33:47.356344Z","shell.execute_reply":"2025-12-15T05:33:47.499059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# CNN","metadata":{}},{"cell_type":"code","source":"!mkdir cache\n!mkdir cache/input","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:33:47.501723Z","iopub.execute_input":"2025-12-15T05:33:47.502011Z","iopub.status.idle":"2025-12-15T05:33:47.752002Z","shell.execute_reply.started":"2025-12-15T05:33:47.501983Z","shell.execute_reply":"2025-12-15T05:33:47.750641Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv(\"cnn_test.csv\")\ndf.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:04:37.217931Z","iopub.execute_input":"2025-12-15T06:04:37.218453Z","iopub.status.idle":"2025-12-15T06:04:37.235761Z","shell.execute_reply.started":"2025-12-15T06:04:37.218407Z","shell.execute_reply":"2025-12-15T06:04:37.234317Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\nbody_dict = {'body_center': 0,\n 'ear_left': 1,\n 'ear_right': 2,\n 'hip_left': 3,\n 'hip_right': 4,\n 'neck': 5,\n 'nose': 6,\n 'tail_base': 7,\n 'tail_midpoint': 8,\n 'tail_tip': 9}\n\ndef keep_bodypart(x):\n    x = str(x)\n    if x.startswith(\"headpiece_\"):\n        return False\n    if x.endswith(\"_1\") or x.endswith(\"_2\"):\n        return False\n    \n    return True\n\ndef rename_bodypart(x):\n    if x == \"lateral_left\":\n        return \"hip_left\"\n    if x == \"lateral_right\":\n        return \"hip_right\"\n    if x == \"head\":\n        return \"nose\"\n    return x\n\ndef masked_std(x, mask, dim):\n    \"\"\"\n    Computes std over values where mask=True.\n    x: tensor [..., T, ...]\n    mask: boolean tensor same shape as x\n    dim: dimension to reduce\n    \"\"\"\n    # Number of valid items\n    count = mask.sum(dim=dim).clamp(min=1)\n\n    # Mask invalid entries\n    xm = x * mask\n\n    # Mean of valid entries\n    mean = xm.sum(dim=dim) / count\n\n    # Variance\n    var = ((xm - mean.unsqueeze(dim)) ** 2 * mask).sum(dim=dim) / count\n\n    return torch.sqrt(var)\n\ndef interpolate_seq(feat_x, feat_notnull):\n    \"\"\"\n    feat_x:        (T, M, N)\n    feat_notnull:  (T, M, N)  binary mask\n    \"\"\"\n    T, M, N = feat_x.shape\n    t = torch.arange(T, device=feat_x.device).float()\n\n    # Output container\n    out = feat_x.clone()\n\n    # iterate across category dims M, N\n    for i in range(M):\n        for j in range(N):\n            mask = feat_notnull[:, i, j].bool()\n            if mask.sum() == 0:\n                continue\n            \n            y = feat_x[:, i, j]\n\n            # if all missing or all present → skip\n            if mask.sum() == 0 or mask.sum() == T:\n                continue\n\n            # valid points\n            t_valid = t[mask]\n            y_valid = y[mask]\n\n            # interpolate\n            y_interp = np.interp(t.numpy(), t_valid.numpy(), y_valid.numpy())\n\n            out[:, i, j] = torch.FloatTensor(y_interp)\n\n    return out\n\n\ndef downsample_to_quarter_seconds(inp: torch.Tensor, fps: float, ppc: float):\n    seq_len, n_m, n_bp, _ = inp.shape\n    \n    bin_size = int(fps / 4)\n\n    usable_len = (seq_len // bin_size) * bin_size\n    inp = inp[:usable_len]\n\n    bins = inp.reshape(-1, bin_size, n_m, n_bp, 3)\n\n    notnull = bins[..., 0]\n    x = bins[..., 1] / ppc\n    y = bins[..., 2] / ppc\n\n    mask = notnull.bool()\n\n    # 1) max(notnull)\n    feat_notnull = notnull.max(dim=1).values\n\n    # 2–3) avg x,y where notnull\n    valid_count = mask.sum(dim=1).clamp(min=1)\n    feat_x = (x * mask).sum(dim=1) / valid_count\n    feat_y = (y * mask).sum(dim=1) / valid_count\n\n    # 4) mean(notnull)\n    feat_notnull_mean = notnull.float().mean(dim=1)\n\n    # 5–6) std(x), std(y) where notnull\n    feat_x_std = masked_std(x, mask, dim=1)\n    feat_y_std = masked_std(y, mask, dim=1)\n    \n    feat_x = interpolate_seq(feat_x, feat_notnull)\n    feat_y = interpolate_seq(feat_y, feat_notnull)\n    \n    feat_notnull = feat_notnull.amax(dim=0, keepdim=True).repeat(feat_x.shape[0], 1, 1)\n\n    # stack: [max_notnull, avg_x, avg_y, mean_notnull, std_x, std_y]\n    out = torch.stack(\n        [feat_notnull,\n         feat_x,\n         feat_y,\n         feat_notnull_mean,\n         feat_x_std,\n         feat_y_std],\n        dim=-1\n    )\n\n    return out\n\ndef process_data(df, fps24=False):\n    target_lens = []\n    \n    for i, row in tqdm(df.iterrows(), total=df.shape[0]):\n        key = f\"{row.lab_id}/{row.video_id}\"\n        \n        path = f\"/kaggle/input/MABe-mouse-behavior-detection/{RUN_MODE}_tracking/{row.lab_id}/{row.video_id}.parquet\"\n        track_df = pd.read_parquet(path)\n        \n        track_df = track_df[track_df.bodypart.apply(keep_bodypart)].reset_index(drop=True)\n        track_df[\"bodypart\"] = track_df[\"bodypart\"].apply(rename_bodypart)\n        \n        track_df = track_df.groupby([\"bodypart\", \"video_frame\", \"mouse_id\"])[[\"x\", \"y\"]].mean().reset_index()\n        \n        input_len = track_df[\"video_frame\"].max() + 1\n        \n        X = torch.zeros(size=(input_len, 4, len(body_dict), 3), dtype=torch.float32)\n        \n        track_df[\"bp_id\"] = track_df[\"bodypart\"].map(body_dict)\n        \n        for bp_id in range(len(body_dict)):\n            for mid in range(4):\n                sub_track_df = track_df[(track_df[\"bp_id\"] == bp_id) & (track_df[\"mouse_id\"] - 1 == mid)]\n                \n                frames = sub_track_df[\"video_frame\"].values\n                x = sub_track_df[\"x\"].values\n                y = sub_track_df[\"y\"].values\n                \n                X[frames, mid, bp_id, 0] = 1\n                X[frames, mid, bp_id, 1] = torch.FloatTensor(x)\n                X[frames, mid, bp_id, 2] = torch.FloatTensor(y)\n    \n        fps = row.frames_per_second\n        if fps24:\n            fps = 24\n        X = downsample_to_quarter_seconds(X, fps, row.pix_per_cm_approx).numpy()\n                \n        new_key = key.replace(\"/\", \"_\")\n        \n        np.save(f\"cache/input/{new_key}.npy\", X)\n        target_lens.append(input_len)\n    \n    df[\"target_len\"] = target_lens\n\nprocess_data(df, fps24=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:04:37.969998Z","iopub.execute_input":"2025-12-15T06:04:37.970511Z","iopub.status.idle":"2025-12-15T06:04:41.213335Z","shell.execute_reply.started":"2025-12-15T06:04:37.970474Z","shell.execute_reply":"2025-12-15T06:04:41.212082Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nimport torch.nn.functional as F\n\n\nNW = 1\nBS = 1\n\nclass MouseDataset(Dataset):\n    def __init__(self, df, is_train=False, n_mice=4):\n        self.df = df.reset_index(drop=True)\n        self.is_train = is_train\n        self.n_mice = n_mice\n\n    def __len__(self):\n        return self.df.shape[0]\n\n    def __getitem__(self, index):\n        row = self.df.iloc[index]\n        key = f\"{row.lab_id}/{row.video_id}\".replace(\"/\", \"_\")\n        \n        X = np.load(f\"cache/input/{key}.npy\")[:, :self.n_mice]\n        \n        label_map = torch.zeros((self.n_mice, self.n_mice, len(actions_dict)))\n        for label in eval(row.behaviors_labeled):\n            label = label.split(\",\")\n            \n            if row.lab_id == \"UppityFerret\":\n                if label[2] not in {\"huddle\", \"reciprocalsniff\", \"sniffgenital\"}:\n                    continue\n            elif row.lab_id == \"CautiousGiraffe\":\n                if label[2] not in {'chase', 'escape', 'reciprocalsniff', 'sniff', 'sniffbody', 'sniffgenital'}:\n                    continue\n            elif row.lab_id == \"JovialSwallow\":\n                if label[2] not in {'attack', 'sniff'}:\n                    continue\n            elif row.lab_id == \"SparklingTapir\":\n                if label[2] not in {'attack', 'defend', 'escape', 'mount'}:\n                    continue\n            elif row.lab_id == \"ElegantMink\":\n                if label[2] not in {'allogroom','attack','attemptmount','intromit','mount','sniff'}:\n                    continue\n            elif row.lab_id == \"GroovyShrew\":\n                if label[2] not in {'approach','attemptmount','climb','defend','dig','escape','rear','rest','run','selfgroom','sniff','sniffgenital'}:\n                    continue\n            \n            try:\n                m_i = int(label[0][5]) - 1\n\n                m_j = int(m_i)\n                if label[1] != \"self\":\n                    m_j = int(label[1][5]) - 1\n\n                label_map[m_i, m_j, actions_dict[label[2]]] = 1\n            except:\n                pass\n                \n        cm1 = row.video_width_pix / row.pix_per_cm_approx\n        cm2 = row.video_height_pix / row.pix_per_cm_approx\n        \n        x_coo = X[:, :, :, 1]\n        y_coo = X[:, :, :, 2]\n\n        x_coo = np.minimum(np.abs(x_coo), np.abs(cm1 - x_coo))\n        y_coo = np.minimum(np.abs(y_coo), np.abs(cm2 - y_coo))\n\n        min_dist_to_edge = np.minimum(x_coo, y_coo)\n            \n        X = np.concatenate([X, min_dist_to_edge[:, :, :, None]], axis=-1)\n                \n        X = torch.FloatTensor(X)\n\n        return X, label_map\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:04:42.942864Z","iopub.execute_input":"2025-12-15T06:04:42.943249Z","iopub.status.idle":"2025-12-15T06:04:42.959907Z","shell.execute_reply.started":"2025-12-15T06:04:42.943223Z","shell.execute_reply":"2025-12-15T06:04:42.958524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn as nn\n\ndef pairwise_mouse_distances(data):\n    \"\"\"\n    data: (bs, 3, 25, 4, seq)\n        loc = [is_notnull, x, y]\n    Returns:\n        (bs, 25*25 + 2, 4, 4, seq)\n    \"\"\"\n    bs, _, n_bp, n_mouse, seq = data.shape\n    \n    is_notnull = data[:, 0]   # (bs, 25, 4, seq)\n    xy = data[:, 1:3]         # (bs, 2, 25, 4, seq)\n    \n    # -- Pairwise distances between bodyparts (25×25) and mice (4×4)\n    xy_i = xy.unsqueeze(3).unsqueeze(5)  # (bs, 2, 25, 1, 4, 1, seq)\n    xy_j = xy.unsqueeze(2).unsqueeze(4)  # (bs, 2, 1, 25, 1, 4, seq)\n    \n    # Compute Euclidean distance\n    dist = torch.sqrt(((xy_i - xy_j) ** 2).sum(dim=1))  # (bs, 25, 25, 4, 4, seq)\n    \n    \n    # -- Compute is_notnull pair summaries (2 extra channels)\n    mask_i = is_notnull.unsqueeze(2).unsqueeze(4)  # (bs, 25, 1, 4, seq)\n    mask_j = is_notnull.unsqueeze(1).unsqueeze(3)  # (bs, 1, 25, 4, seq)\n    both_valid = (mask_i * mask_j).float()  # (bs, 25, 25, 4, 4, seq)\n    \n    dist = both_valid*dist + 10*(1 - both_valid)\n    \n    dist = dist.flatten(1, 2)  # (bs, 625, 4, 4, seq)\n    \n    dist = torch.cat([torch.log(1 + dist),\n                      torch.diff(dist, dim=-1, prepend=torch.zeros_like(dist[:, :, :, :, :1]))], axis=1)\n    \n    loc = (xy*data[:, :1]).sum(axis=2) / (data[:, :1].sum(axis=2) + 1e-4)\n    heading = F.normalize(xy[:, :, 6] - xy[:, :, 7], dim=1)\n    \n    flist = []\n    for gap in [2, 4, 8, 16]:\n        v = loc[:, :, :, gap:] - loc[:, :, :, :-gap]\n        # bs, 2, 4, seq-4\n\n        v_i = v.unsqueeze(3)  # (bs, 2, 4, 1, seq)\n        v_j = v.unsqueeze(2)  # (bs, 2, 1, 4, seq)\n        \n        mdir = F.normalize(loc[:, :, :, gap:].unsqueeze(3) - loc[:, :, :, :-gap].unsqueeze(2), dim=1)\n\n        cos_sim = (F.normalize(v_i, dim=1) * F.normalize(v_j, dim=1)).sum(axis=1)\n        cos_sim2 = (heading[:, :, :, gap:].unsqueeze(3) * F.normalize(v_j, dim=1)).sum(axis=1)\n        cos_sim3 = (F.normalize(v_i, dim=1) * heading[:, :, :, gap:].unsqueeze(2)).sum(axis=1)\n        cos_sim4 = (heading[:, :, :, gap:].unsqueeze(3) * mdir).sum(axis=1)\n        cos_sim5 = (F.normalize(v_i, dim=1) * mdir).sum(axis=1)\n\n        v_i = torch.log(1 + ((v_i ** 2).sum(axis=1)**0.5).repeat(1, 1, 2, 1))\n        v_j = torch.log(1 + ((v_j ** 2).sum(axis=1)**0.5).repeat(1, 2, 1, 1))\n        # bs 4, 4, seq-4\n\n        f = torch.cat([cos_sim.unsqueeze(1), cos_sim2.unsqueeze(1), cos_sim3.unsqueeze(1),\n                       cos_sim4.unsqueeze(1), cos_sim5.unsqueeze(1),\n                       v_i.unsqueeze(1), (v_i - v_j).unsqueeze(1),\n                      ], axis=1)\n        features = F.pad(f, (gap, 0, 0, 0, 0, 0, 0, 0, 0, 0), \"constant\", 0)\n        flist.append(features)\n        features = F.pad(f, (0, gap, 0, 0, 0, 0, 0, 0, 0, 0), \"constant\", 0)\n        flist.append(features)\n\n    return dist, torch.cat(flist, axis=1)\n\n\n\nclass MouseModel2(nn.Module):\n    def __init__(self, num_mice=4, num_classes=38, nhead=4, num_layers=2):\n        super(MouseModel2, self).__init__()\n        \n        self.blen = len(body_dict)\n        \n        self.init_norm = nn.InstanceNorm3d((self.blen**2)*2)\n        self.init_norm2 = nn.InstanceNorm3d(7*8 + 4*self.blen)\n        self.conv1 = nn.Sequential(nn.Conv3d((self.blen**2)*2 + 7*8 + 4*self.blen, 128, kernel_size=(1, 1, 3), padding=(0, 0, 1), padding_mode=\"replicate\"),\n                                   nn.ELU(), nn.GroupNorm(8, 128, affine=False))\n        self.conv2 = nn.Sequential(nn.Conv3d(128 + 38, 64, kernel_size=(1, 1, 3),  dilation=(1, 1, 3),\n                                             padding=(0, 0, 3), padding_mode=\"replicate\"),\n                                   nn.ELU())\n        self.conv2b = nn.Sequential(nn.GroupNorm(4*5, 64*5, affine=False),\n                                    nn.Conv3d(64*5, 128, kernel_size=(1, 1, 1))\n        )\n        self.conv3 = nn.Sequential(nn.Conv3d(128, 128, kernel_size=(1, 1, 3),  dilation=(1, 1, 5),\n                                             padding=(0, 0, 10), padding_mode=\"replicate\"),\n                                   nn.ELU())\n        self.conv4 = nn.Sequential(nn.GroupNorm(8, 128, affine=False),\n                                   nn.Conv3d(128, 128, kernel_size=(1, 1, 3), dilation=(1, 1, 7),\n                                             padding=(0, 0, 14), padding_mode=\"replicate\"),\n                                   nn.ELU())\n        self.top = nn.Conv3d(128, num_classes, kernel_size=(1, 1, 1))\n        self.top_gate = nn.Sequential(nn.Conv3d(128, num_classes, kernel_size=(1, 1, 1)), nn.Sigmoid())\n        \n        self.avg_pool = nn.AvgPool3d(kernel_size=(1, 1, 17), stride=(1, 1, 1), padding=(0, 0, 8))\n        self.max_pool = nn.MaxPool3d(kernel_size=(1, 1, 17), stride=(1, 1, 1), padding=(0, 0, 8))\n        \n\n    def forward(self, x, lm, target_len=None):\n        # x: (bs, seq, mid(4), bid(25), loc(3))\n        n_mice = 2\n        \n        x = x.permute(0, 4, 3, 2, 1)\n        # -> (bs, 6, bid(25), mid(4), seq)\n        \n        x2 = x[:, 3:].reshape(x.shape[0], 4*self.blen, n_mice, 1, x.shape[-1]).repeat(1, 1, 1, n_mice, 1)\n        x = x[:, :3]\n        \n        x, f = pairwise_mouse_distances(x)\n        # (bs, 625, 4, 4, seq)\n        \n        x = torch.cat([self.init_norm(x),\n                       self.init_norm2(torch.cat([f, x2], axis=1))\n                      ], axis=1)\n        \n        x1 = self.conv1(x)\n        # -> (bs, 64, mid(4), mid(4), seq)\n        \n        lm = lm.permute(0, 3, 1, 2).reshape(1, 38, n_mice, n_mice, 1).repeat(1, 1, 1, 1, x.shape[-1])\n        \n        x = torch.cat([x1, lm], axis=1)\n        \n        x = self.conv2(x)\n        x = torch.cat([x[:, :4].softmax(dim=2),\n                       x[:, 4:8].softmax(dim=3),\n                       x[:, 8:]], axis=1)\n        \n        x = torch.cat([x, self.avg_pool(x) - x, self.max_pool(x) - x, \n                       x.amax(dim=-1, keepdim=True) - x, x.mean(dim=-1, keepdim=True) - x], axis=1)\n        x = self.conv2b(x)\n        \n        x2 = (x + x1) / 2\n        \n        x = self.conv3(x2)[:, :, :, :, :x2.shape[-1]]\n        \n        x = (x + x2) / 2\n        \n        x = x + self.conv4(x)[:, :, :, :, :x.shape[-1]]\n        \n        g = self.top_gate(x)\n        x = self.top(x)\n        \n        x = g * x + (1 - g) * x.amax(dim=(2, 3), keepdim=True)\n        # -> (bs, 38, mid(4), mid(4), seq)\n        \n        if target_len is not None:\n            scale = target_len // x.shape[-1]\n\n            x = F.interpolate(x, scale_factor=(1, 1, scale), mode=\"trilinear\")\n\n            rest = target_len - x.shape[-1]\n\n            if rest > 0:\n                x = F.pad(x, (0, rest, 0, 0, 0, 0), mode=\"replicate\")\n            \n        x = x.transpose(4, 1)\n\n        return x\n\n    def save(self, path):\n        torch.save(self.state_dict(), path)\n\n    def load(self, path):\n        self.load_state_dict(torch.load(path, map_location='cpu'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:04:45.247552Z","iopub.execute_input":"2025-12-15T06:04:45.247909Z","iopub.status.idle":"2025-12-15T06:04:45.283596Z","shell.execute_reply.started":"2025-12-15T06:04:45.247884Z","shell.execute_reply":"2025-12-15T06:04:45.282338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_preds(ds, models, n_mice):\n    loader = DataLoader(ds, batch_size=BS, shuffle=False, num_workers=NW,\n                                 pin_memory=False, drop_last=False)   \n    \n    tbar = tqdm(loader, file=sys.stdout)\n    \n    pred_dfs = []\n    \n    with torch.no_grad():\n        for idx, (x, lm) in enumerate(tbar):\n            \n            video_id = ds.df[\"video_id\"].values[idx]\n            lab_id = ds.df[\"lab_id\"].values[idx]\n            fps = ds.df[\"frames_per_second\"].values[idx]\n            tl = ds.df[\"target_len\"].values[idx]\n    \n            preds = models[0](x, lm, target_len=tl) / len(models)\n            for m in range(1, len(models)):\n                preds += models[m](x, lm, target_len=tl) / len(models)\n                \n            preds = adjust_preds(preds, lm)\n    \n            if lab_id == \"InvincibleJellyfish\":\n                preds[:, :, :, :, 1] *= 1.5\n                preds[:, :, :, :, 3] *= 1.5\n                preds[:, :, :, :, 11] *= 1.5\n                preds[:, :, :, :, 14] *= 1.5\n                preds[:, :, :, :, 17] *= 1.5\n            elif lab_id == \"LyricalHare\":\n                preds[:, :, :, :, 2] *= 1.5\n                preds[:, :, :, :, 21] *= 1.5\n                preds[:, :, :, :, 26] *= 1.5\n            elif lab_id == \"ElegantMink\":\n                preds[:, :, :, :, 1] *= 1.5\n            elif lab_id == \"GroovyShrew\":\n                preds[:, :, :, :, 4] *= 1.5\n            elif lab_id == \"NiftyGoldfinch\":\n                preds[:, :, :, :, 9] *= 1.5\n                preds[:, :, :, :, 18] *= 1.5\n                preds[:, :, :, :, 19] *= 1.5\n                preds[:, :, :, :, 26] *= 1.5\n                preds[:, :, :, :, 37] *= 1.5\n            \n            y_pred = preds.argmax(axis=-1)[0].detach().numpy()\n    \n            for i in range(n_mice):\n                for j in range(n_mice):\n                    if lm[0, i, j].sum() > 0:\n                        pred_df = consecutive_classes(y_pred[:, i, j])\n                        pred_df[\"lab_id\"] = lab_id\n                        pred_df[\"video_id\"] = video_id\n                        pred_df[\"agent_id\"] = f\"mouse{i+1}\"\n                        pred_df[\"target_id\"] = f\"mouse{j+1}\"\n                        \n                        pred_dfs.append(pred_df)\n    \n    pred_df = pd.concat(pred_dfs, axis=0).reset_index(drop=True)\n    pred_df.loc[pred_df[\"agent_id\"] == pred_df[\"target_id\"], \"target_id\"] = \"self\"\n    pred_df[\"action\"] = pred_df[\"class_id\"].map(reverse_dict)\n    return pred_df\n    \n\nimport sys\n\nactions_dict = {'allogroom': 1,\n 'approach': 2,\n 'attack': 3,\n 'attemptmount': 4,\n 'avoid': 5,\n 'biteobject': 6,\n 'chase': 7,\n 'chaseattack': 8,\n 'climb': 9,\n 'defend': 10,\n 'dig': 11,\n 'disengage': 12,\n 'dominance': 13,\n 'dominancegroom': 14,\n 'dominancemount': 15,\n 'ejaculate': 16,\n 'escape': 17,\n 'exploreobject': 18,\n 'flinch': 19,\n 'follow': 20,\n 'freeze': 21,\n 'genitalgroom': 22,\n 'huddle': 23,\n 'intromit': 24,\n 'mount': 25,\n 'rear': 26,\n 'reciprocalsniff': 27,\n 'rest': 28,\n 'run': 29,\n 'selfgroom': 30,\n 'shepherd': 31,\n 'sniff': 32,\n 'sniffbody': 33,\n 'sniffface': 34,\n 'sniffgenital': 35,\n 'submit': 36,\n 'tussle': 37,\n 'none': 0}\n\nreverse_dict = {v: k for k, v in actions_dict.items()}\n\n\ndef adjust_preds(preds, lm):\n    label_map = lm.reshape(1, 1, preds.shape[2], preds.shape[3], 38).repeat(preds.shape[0], preds.shape[1], 1, 1, 1)\n    preds = preds.sigmoid()*label_map\n    preds[:, :, :, :, 0] = 0.6\n    return preds\n\ndef consecutive_classes(arr):\n    # Identify where the class changes\n    change = np.diff(arr, prepend=arr[0]-1) != 0\n    idx = np.where(change)[0]\n\n    # Build segments\n    segments = []\n    for start, end in zip(idx, np.append(idx[1:], len(arr))):\n        class_id = arr[start]\n        if class_id != 0:  # ignore zeros\n            segments.append([class_id, start, end])\n\n    return pd.DataFrame(segments, columns=[\"class_id\", \"start_frame\", \"stop_frame\"])\n\npred_dfs = []\n\nN_FOLDS = 6\n\nall_labs = set(cnn_labs.keys()).union(ensemble_labs.keys())\n\nfor lab in all_labs:\n    ds = MouseDataset(df[df[\"lab_id\"] == lab].reset_index(drop=True), n_mice=2)\n    \n    models = []\n    \n    for fold in range(N_FOLDS):\n        model = MouseModel2()\n        model.load(f\"/kaggle/input/mabe-v76/v76_{lab}_{fold}_0.pth\")\n        model.eval()\n        models.append(model)\n        \n    pred_dfs.append(get_preds(ds, models, n_mice=2))\n\npred_df2 = pd.concat(pred_dfs, axis=0).reset_index(drop=True)\npred_df2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:04:48.913295Z","iopub.execute_input":"2025-12-15T06:04:48.914283Z","iopub.status.idle":"2025-12-15T06:05:15.251946Z","shell.execute_reply.started":"2025-12-15T06:04:48.914251Z","shell.execute_reply":"2025-12-15T06:05:15.250850Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# XGB 2","metadata":{}},{"cell_type":"code","source":"df = pd.read_csv(\"xgb_test2.csv\")\n\nprocess_data(df, fps24=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:11.389661Z","iopub.execute_input":"2025-12-15T05:34:11.390034Z","iopub.status.idle":"2025-12-15T05:34:23.442135Z","shell.execute_reply.started":"2025-12-15T05:34:11.389996Z","shell.execute_reply":"2025-12-15T05:34:23.440795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import xgboost as xgb\n\nxgb_model = xgb.Booster()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:23.443366Z","iopub.execute_input":"2025-12-15T05:34:23.443988Z","iopub.status.idle":"2025-12-15T05:34:23.449872Z","shell.execute_reply.started":"2025-12-15T05:34:23.443936Z","shell.execute_reply":"2025-12-15T05:34:23.448823Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"n_mice = 2\nblen = 5\n\n\ndef get_mask(n_features):\n    idx = np.arange(n_features)\n    rows = idx // (n_features ** 0.5)\n    cols = idx % (n_features ** 0.5)\n\n    mask = cols > rows\n    \n    return mask\n\n\nmask = get_mask(blen ** 2)\nmask = np.concatenate([mask, mask])\n\nactions = ['none', 'shepherd']\n\nself_actions = {'biteobject', 'climb', 'dig', 'exploreobject', 'freeze', \n                'genitalgroom', 'huddle', 'rear', 'rest', 'run', 'selfgroom'}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:23.450972Z","iopub.execute_input":"2025-12-15T05:34:23.451344Z","iopub.status.idle":"2025-12-15T05:34:23.471249Z","shell.execute_reply.started":"2025-12-15T05:34:23.451315Z","shell.execute_reply":"2025-12-15T05:34:23.470055Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def pairwise_mouse_distances(data):\n    \"\"\"\n    data: (bs, 3, 25, 4, seq)\n        loc = [is_notnull, x, y]\n    Returns:\n        (bs, 25*25 + 2, 4, 4, seq)\n    \"\"\"\n    bs, _, n_bp, n_mouse, seq = data.shape\n    \n    is_notnull = data[:, 0]   # (bs, 25, 4, seq)\n    xy = data[:, 1:3]         # (bs, 2, 25, 4, seq)\n    \n    # -- Pairwise distances between bodyparts (25×25) and mice (4×4)\n    xy_i = xy.unsqueeze(3).unsqueeze(5)  # (bs, 2, 25, 1, 4, 1, seq)\n    xy_j = xy.unsqueeze(2).unsqueeze(4)  # (bs, 2, 1, 25, 1, 4, seq)\n    \n    # Compute Euclidean distance\n    dist = torch.sqrt(((xy_i - xy_j) ** 2).sum(dim=1))  # (bs, 25, 25, 4, 4, seq)\n    \n    \n    # -- Compute is_notnull pair summaries (2 extra channels)\n    mask_i = is_notnull.unsqueeze(2).unsqueeze(4)  # (bs, 25, 1, 4, seq)\n    mask_j = is_notnull.unsqueeze(1).unsqueeze(3)  # (bs, 1, 25, 4, seq)\n    both_valid = (mask_i * mask_j).float()  # (bs, 25, 25, 4, 4, seq)\n    \n    dist = both_valid*dist + 1000*(1 - both_valid)\n    \n    dist = dist.flatten(1, 2)  # (bs, 625, 4, 4, seq)\n    \n    dist = torch.cat([torch.log(1 + dist),\n                      torch.diff(dist, dim=-1, prepend=torch.zeros_like(dist[:, :, :, :, :1]))], axis=1)\n    \n    loc = (xy*data[:, :1]).sum(axis=2) / (data[:, :1].sum(axis=2) + 1e-4)\n    \n    flist = []\n    for gap in [2, 4, 8, 16]:\n        v = loc[:, :, :, gap:] - loc[:, :, :, :-gap]\n        a = F.pad(v[:, :, :, gap:] - v[:, :, :, :-gap], (gap, 0, 0, 0, 0, 0, 0, 0), \"constant\", 0)\n        \n        # bs, 2, 4, seq-4\n\n        v_i = v.unsqueeze(3)  # (bs, 2, 4, 1, seq)\n        v_j = v.unsqueeze(2)  # (bs, 2, 1, 4, seq)\n        \n        a_i = v.unsqueeze(3)  # (bs, 2, 4, 1, seq)\n        a_j = v.unsqueeze(2)  # (bs, 2, 1, 4, seq)\n\n        cos_sim = (F.normalize(v_i, dim=1) * F.normalize(v_j, dim=1)).sum(axis=1)\n        cos_sim2 = (F.normalize(a_i, dim=1) * F.normalize(a_j, dim=1)).sum(axis=1)\n        # bs 4, 4, seq-4\n\n        v_i = torch.log(1 + ((v_i ** 2).sum(axis=1)**0.5).repeat(1, 1, 2, 1))\n        v_j = torch.log(1 + ((v_j ** 2).sum(axis=1)**0.5).repeat(1, 2, 1, 1))\n        \n        a_i = torch.log(1 + ((a_i ** 2).sum(axis=1)**0.5).repeat(1, 1, 2, 1))\n        a_j = torch.log(1 + ((a_j ** 2).sum(axis=1)**0.5).repeat(1, 2, 1, 1))\n        # bs 4, 4, seq-4\n\n        f = torch.cat([cos_sim.unsqueeze(1), v_i.unsqueeze(1), v_j.unsqueeze(1),\n                       cos_sim2.unsqueeze(1), a_i.unsqueeze(1), a_j.unsqueeze(1)], axis=1)\n        features = F.pad(f, (gap, 0, 0, 0, 0, 0, 0, 0, 0, 0), \"constant\", 0)\n        flist.append(features)\n        features = F.pad(f, (0, gap, 0, 0, 0, 0, 0, 0, 0, 0), \"constant\", 0)\n        flist.append(features)\n\n    return dist, torch.cat(flist, axis=1)\n\nxgb_pred_dfs = []\nfor idx, row in tqdm(df.iterrows(), total=df.shape[0]):\n    key = f\"{row.lab_id}/{row.video_id}\".replace(\"/\", \"_\")\n    \n    X = np.load(f\"cache/input/{key}.npy\")[:, :n_mice, [0, 1, 2, 6, 7]]\n    \n    cm1 = row.video_width_pix / row.pix_per_cm_approx\n    cm2 = row.video_height_pix / row.pix_per_cm_approx\n    \n    x = torch.FloatTensor(X).unsqueeze(0).permute(0, 4, 3, 2, 1)\n    \n    x2 = x[:, 3:].reshape(x.shape[0], 3*blen, n_mice, x.shape[-1]).cpu().numpy()[0]\n    x = x[:, :3]\n        \n    x, f = pairwise_mouse_distances(x)\n    \n    f = f.numpy()[0]\n    \n    x = x.permute(0, 2, 3, 4, 1)[0].numpy()\n    \n    n_features = x.shape[-1]\n    \n    self_f_cols = [f\"self_f{c}\" for c in range(sum(mask))]\n    opp_f_cols = [f\"opp_f{c}\" for c in range(sum(mask))] \n    pair_f_cols = [f\"pair_f{c}\" for c in range(n_features)]\n    \n    bin_features = [f\"bin_stats_f{c}\" for c in range(x2.shape[0])]\n    self_vf_cols = [f\"self_vf{c}\" for c in range(48)] \n    pair_vf_cols = [f\"pair_vf{c}\" for c in range(48)]\n    \n    video_df1 = pd.DataFrame()\n    video_df1[self_f_cols] = x[0, 0][:, mask]\n    video_df1[pair_f_cols] = x[0, 1]\n    video_df1[bin_features] = x2[:, 0].T\n    video_df1[self_vf_cols] = f[:, 0, 0].T\n    video_df1[pair_vf_cols] = f[:, 0, 1].T\n    video_df1[\"mouse\"] = 1\n    \n    video_df2 = pd.DataFrame()\n    video_df2[self_f_cols] = x[1, 1][:, mask]\n    video_df2[pair_f_cols] = x[1, 0]\n    video_df2[bin_features] = x2[:, 1].T\n    video_df2[self_vf_cols] = f[:, 1, 1].T\n    video_df2[pair_vf_cols] = f[:, 1, 0].T\n    video_df2[\"mouse\"] = 2\n    \n    \n    video_df1[opp_f_cols] = video_df2[self_f_cols].values\n    video_df2[opp_f_cols] = video_df1[self_f_cols].values\n\n    predict_list = []\n    for label in eval(row.behaviors_labeled):\n        label = label.split(\",\")\n\n        if row.lab_id == \"UppityFerret\":\n            if label[2] not in {\"huddle\", \"reciprocalsniff\", \"sniffgenital\"}:\n                continue\n        elif row.lab_id == \"CautiousGiraffe\":\n            if label[2] not in {'chase', 'escape', 'reciprocalsniff', 'sniff', 'sniffbody', 'sniffgenital'}:\n                continue\n        elif row.lab_id == \"JovialSwallow\":\n            if label[2] not in {'attack', 'sniff'}:\n                continue\n        elif row.lab_id == \"SparklingTapir\":\n            if label[2] not in {'attack', 'defend', 'escape', 'mount'}:\n                continue\n        elif row.lab_id == \"ElegantMink\":\n            if label[2] not in {'allogroom','attack','attemptmount','intromit','mount','sniff'}:\n                continue\n        elif row.lab_id == \"GroovyShrew\":\n            if label[2] not in {'approach','attemptmount','climb','defend','dig','escape','rear','rest','run','selfgroom','sniff','sniffgenital'}:\n                continue\n                \n        \n\n        try:\n            m_i = int(label[0][5]) - 1\n            \n            m_j = int(m_i)\n            if label[1] != \"self\":\n                m_j = int(label[1][5]) - 1\n                \n            if m_i ==0:\n                predict_list.append((m_i, label[2]))\n            else:\n                predict_list.append((m_i, label[2]))\n\n        except:\n            pass\n        \n    move_features = self_f_cols + pair_f_cols + bin_features + self_vf_cols + pair_vf_cols + opp_f_cols\n\n    features = move_features + [f\"prev_{col}\" for col in move_features] + [\"mouse\"]\n\n    datasets = []\n    video_dfs = []\n        \n    for video_df in [video_df1, video_df2]:\n        video_df[\"lab_id\"] = row.lab_id\n        video_df[\"video_id\"] = row.video_id\n        video_df[\"timeframe\"] = np.arange(video_df.shape[0])\n        video_df[[f\"prev_{col}\" for col in move_features]] = video_df[move_features].shift(6, axis=0)\n        video_df[actions] = None\n        datasets.append(xgb.DMatrix(video_df[features]))\n        video_dfs.append(video_df)\n\n\n    for m, action in predict_list:\n        if action not in actions:\n            continue\n        if m >= 2:\n            continue\n\n        xgb_preds = []\n\n        for fold in range(4):\n            xgb_model.load_model(f\"/kaggle/input/mabe-xgb-parrot/xgb_{row.lab_id}_{action}_fold{fold}.json\")\n            xgb_preds.append(xgb_model.predict(datasets[m]))\n\n        video_dfs[m][action] = sum(xgb_preds) / len(xgb_preds)\n\n    timeframe_count = 24 // 4\n    for m in range(2):\n        video_df = video_dfs[m]\n        video_df[\"none\"] = 0.42\n\n        xgb_pred_df = consecutive_classes(np.argmax(video_df[actions].fillna(-1).values, axis=1))\n\n        xgb_pred_df[\"start_frame\"] *= timeframe_count\n        xgb_pred_df[\"stop_frame\"] *= timeframe_count\n        xgb_pred_df[\"video_id\"] = row.video_id\n\n        xgb_pred_df[\"action\"] = xgb_pred_df[\"class_id\"].apply(lambda x: actions[x])\n\n        xgb_pred_df[\"agent_id\"] = f\"mouse{m + 1}\"\n        xgb_pred_df[\"target_id\"] = f\"mouse{2 - m}\"\n        xgb_pred_df.loc[xgb_pred_df[\"action\"].isin(self_actions), \"target_id\"] = \"self\"\n\n        xgb_pred_dfs.append(xgb_pred_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:23.472537Z","iopub.execute_input":"2025-12-15T05:34:23.472827Z","iopub.status.idle":"2025-12-15T05:34:30.799431Z","shell.execute_reply.started":"2025-12-15T05:34:23.472805Z","shell.execute_reply":"2025-12-15T05:34:30.797877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"xgb_pred_df = pd.concat(xgb_pred_dfs, axis=0).reset_index(drop=True)\nxgb_pred_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:30.800849Z","iopub.execute_input":"2025-12-15T05:34:30.801277Z","iopub.status.idle":"2025-12-15T05:34:30.819636Z","shell.execute_reply.started":"2025-12-15T05:34:30.801243Z","shell.execute_reply":"2025-12-15T05:34:30.818108Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Submission","metadata":{}},{"cell_type":"code","source":"# xgb\npred_df = pd.read_csv(\"submission.csv\")\npred_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:30.820913Z","iopub.execute_input":"2025-12-15T05:34:30.821324Z","iopub.status.idle":"2025-12-15T05:34:30.864082Z","shell.execute_reply.started":"2025-12-15T05:34:30.821293Z","shell.execute_reply":"2025-12-15T05:34:30.862957Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# xgb edu\nedu_pred_df = pd.read_csv(\"submission2.csv\")\nedu_pred_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:30.865449Z","iopub.execute_input":"2025-12-15T05:34:30.865856Z","iopub.status.idle":"2025-12-15T05:34:30.883176Z","shell.execute_reply.started":"2025-12-15T05:34:30.865809Z","shell.execute_reply":"2025-12-15T05:34:30.882065Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ensemble_df = pd.read_csv(\"ensemble_test.csv\")\nensemble_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T05:34:30.884574Z","iopub.execute_input":"2025-12-15T05:34:30.885003Z","iopub.status.idle":"2025-12-15T05:34:30.911059Z","shell.execute_reply.started":"2025-12-15T05:34:30.884973Z","shell.execute_reply":"2025-12-15T05:34:30.910016Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def ensemble_videos(videos, n_mice=2):\n    max_stop = 0\n    for video_df, weight in videos:\n        if video_df.shape[0] > 0:\n            max_stop = max(video_df[\"stop_frame\"].max(), max_stop)\n\n    if max_stop == 0:\n        return None\n\n    y = np.zeros((2, 2, max_stop + 1, len(actions_dict)))\n    y[:, :, :, 0] = 1.0\n\n    for video_df, weight in videos:\n        if video_df.shape[0] > 0:\n            for idx, row in video_df.iterrows():\n                m_i = int(row.agent_id[5]) - 1\n                m_j = int(m_i)\n                if row.target_id != \"self\":\n                    m_j = int(row.target_id[5]) - 1\n                y[m_i, m_j, row.start_frame:row.stop_frame, actions_dict[row.action]] += weight\n                y[m_i, m_j, row.start_frame:row.stop_frame, 0] -= weight\n\n    y = np.argmax(y, axis=-1)\n    \n    print(row.video_id)\n    print(np.unique(y[0, 0]))\n    print(np.unique(y[0, 1]))\n    print(np.unique(y[1, 0]))\n    print(np.unique(y[1, 1]))\n    print()\n\n    pred_dfs = []\n    for i in range(n_mice):\n        for j in range(n_mice):\n            if y[i, j, :].max() > 0:\n                pred_df = consecutive_classes(y[i, j, :])\n                pred_df[\"agent_id\"] = f\"mouse{i+1}\"\n                pred_df[\"target_id\"] = f\"mouse{j+1}\"\n                pred_dfs.append(pred_df)\n\n    if len(pred_dfs) == 0:\n        return None\n\n    pred_df = pd.concat(pred_dfs, axis=0).reset_index(drop=True)\n    pred_df.loc[pred_df[\"agent_id\"] == pred_df[\"target_id\"], \"target_id\"] = \"self\"\n    pred_df[\"action\"] = pred_df[\"class_id\"].map(reverse_dict)\n\n    return pred_df\n\n    \n        \n\nvideo_dfs = []\n\nfor idx, row in ensemble_df.iterrows():\n    main_lab = ensemble_labs[row.lab_id]\n\n    video_df = ensemble_videos([\n        (pred_df[pred_df[\"video_id\"] == row.video_id], 0.3 + 0.1*(main_lab == \"xgb\")),\n        (pred_df2[pred_df2[\"video_id\"] == row.video_id], 0.3 + 0.1*(main_lab == \"cnn\")),\n        (edu_pred_df[edu_pred_df[\"video_id\"] == row.video_id], 0.3 + 0.1*(main_lab == \"edu\")),\n    ])\n\n    if video_df is not None:\n        video_df[\"video_id\"] = row.video_id\n        video_dfs.append(video_df)\n\ngrand_ensemble_df = pd.concat(video_dfs, axis=0)\ngrand_ensemble_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:05:30.755762Z","iopub.execute_input":"2025-12-15T06:05:30.756206Z","iopub.status.idle":"2025-12-15T06:05:30.915975Z","shell.execute_reply.started":"2025-12-15T06:05:30.756170Z","shell.execute_reply":"2025-12-15T06:05:30.915150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df = pd.concat([pred_df[~pred_df[\"video_id\"].isin(ensemble_df[\"video_id\"])], \n                    pred_df2[~pred_df2[\"video_id\"].isin(ensemble_df[\"video_id\"])], \n                    xgb_pred_df[~xgb_pred_df[\"video_id\"].isin(ensemble_df[\"video_id\"])], \n                    edu_pred_df[~edu_pred_df[\"video_id\"].isin(ensemble_df[\"video_id\"])],\n                    grand_ensemble_df\n                   ], axis=0).reset_index(drop=True)\n\nsub_df[\"row_id\"] = np.arange(sub_df.shape[0])\n\nsub_df.to_csv(\"submission.csv\", index=False, columns=[\"row_id\",\"video_id\",\"agent_id\",\"target_id\",\"action\",\"start_frame\",\"stop_frame\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:06:28.000539Z","iopub.execute_input":"2025-12-15T06:06:28.000953Z","iopub.status.idle":"2025-12-15T06:06:28.031496Z","shell.execute_reply.started":"2025-12-15T06:06:28.000930Z","shell.execute_reply":"2025-12-15T06:06:28.030466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.read_csv(\"submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:06:31.663868Z","iopub.execute_input":"2025-12-15T06:06:31.664258Z","iopub.status.idle":"2025-12-15T06:06:31.684856Z","shell.execute_reply.started":"2025-12-15T06:06:31.664234Z","shell.execute_reply":"2025-12-15T06:06:31.683872Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df[\"action\"].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:06:34.641567Z","iopub.execute_input":"2025-12-15T06:06:34.641953Z","iopub.status.idle":"2025-12-15T06:06:34.653536Z","shell.execute_reply.started":"2025-12-15T06:06:34.641927Z","shell.execute_reply":"2025-12-15T06:06:34.652361Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df[\"target_id\"].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:06:35.313559Z","iopub.execute_input":"2025-12-15T06:06:35.313938Z","iopub.status.idle":"2025-12-15T06:06:35.322710Z","shell.execute_reply.started":"2025-12-15T06:06:35.313914Z","shell.execute_reply":"2025-12-15T06:06:35.321616Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df[\"video_id\"].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T06:06:35.921793Z","iopub.execute_input":"2025-12-15T06:06:35.922144Z","iopub.status.idle":"2025-12-15T06:06:35.931197Z","shell.execute_reply.started":"2025-12-15T06:06:35.922102Z","shell.execute_reply":"2025-12-15T06:06:35.930060Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}