{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"0.41 score\n\nChanges made from Taylor S. Amarel notebook,\n\nAdded Optuna model tuning for xgboost and lightgbm.","metadata":{}},{"cell_type":"code","source":"# MABe Challenge - Complete Code with Full Features + Optuna Integration\n# Combines comprehensive feature engineering with hyperparameter optimization\n\nvalidate_or_submit = 'submit'\nverbose = True\nUSE_OPTUNA = True # Set to True to run hyperparameter optimization (adds ~30min)\nOPTUNA_TRIALS = 200  # Number of optimization trials\n\nimport pandas as pd\nimport numpy as np\nfrom tqdm import tqdm\nimport itertools\nimport warnings\nimport json\nimport os\nimport gc\nimport lightgbm\nfrom collections import defaultdict\nimport polars as pl\nfrom scipy import signal, stats\n\nfrom sklearn.base import ClassifierMixin, BaseEstimator, clone\nfrom sklearn.model_selection import cross_val_predict, GroupKFold, StratifiedKFold\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.metrics import f1_score, log_loss\n\nwarnings.filterwarnings('ignore')\n\n# Optional imports\ntry:\n    from xgboost import XGBClassifier\n    XGBOOST_AVAILABLE = True\nexcept:\n    XGBOOST_AVAILABLE = False\n    print(\"XGBoost not available\")\n    \ntry:\n    from catboost import CatBoostClassifier\n    CATBOOST_AVAILABLE = True\nexcept:\n    CATBOOST_AVAILABLE = False\n    print(\"CatBoost not available\")\n\ntry:\n    import optuna\n    OPTUNA_AVAILABLE = True\n    optuna.logging.set_verbosity(optuna.logging.WARNING)\nexcept:\n    OPTUNA_AVAILABLE = False\n    print(\"Optuna not available\")\n\n# ==================== IMPROVED CLASSIFIERS ====================\n\nclass StratifiedSubsetClassifier(ClassifierMixin, BaseEstimator):\n    \"\"\"Fit estimator with stratified sampling to maintain class balance\"\"\"\n    def __init__(self, estimator, n_samples):\n        self.estimator = estimator\n        self.n_samples = n_samples\n\n    def fit(self, X, y):\n        if len(X) <= self.n_samples:\n            self.estimator.fit(np.array(X, copy=False), np.array(y, copy=False))\n        else:\n            from sklearn.model_selection import StratifiedShuffleSplit\n            sss = StratifiedShuffleSplit(n_splits=1, train_size=min(self.n_samples, len(X)), random_state=42)\n            try:\n                for train_idx, _ in sss.split(X, y):\n                    self.estimator.fit(np.array(X, copy=False)[train_idx], np.array(y, copy=False)[train_idx])\n            except:\n                downsample = len(X) // self.n_samples\n                downsample = max(downsample, 1)\n                self.estimator.fit(np.array(X, copy=False)[::downsample], np.array(y, copy=False)[::downsample])\n        \n        self.classes_ = self.estimator.classes_\n        return self\n\n    def predict_proba(self, X):\n        if len(self.classes_) == 1:\n            return np.full((len(X), 1), 1.0)\n        probs = self.estimator.predict_proba(np.array(X))\n        return probs\n        \n    def predict(self, X):\n        return self.estimator.predict(np.array(X))\n\n# ==================== OPTUNA OPTIMIZATION ====================\n\ndef objective_lgbm(trial, X, y, groups):\n    \"\"\"Optuna objective for LightGBM hyperparameter tuning\"\"\"\n    params = {\n        'n_estimators': trial.suggest_int('n_estimators', 100, 300),\n        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.15, log=True),\n        'max_depth': trial.suggest_int('max_depth', 3, 12),\n        'num_leaves': trial.suggest_int('num_leaves', 20, 150),\n        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),\n        'subsample': trial.suggest_float('subsample', 0.5, 1.0),\n        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),\n        'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),\n        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True),\n        'verbose': -1\n    }\n    \n    model = make_pipeline(SimpleImputer(), lightgbm.LGBMClassifier(**params))\n    cv = GroupKFold(n_splits=min(5, len(np.unique(groups))))\n    cv_scores = []\n    \n    for train_idx, val_idx in cv.split(X, y, groups=groups):\n        X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]\n        y_train, y_val = y[train_idx], y[val_idx]\n        \n        if len(np.unique(y_train)) < 2 or len(y_val) < 10:\n            continue\n        \n        model.fit(X_train, y_train)\n        preds = model.predict_proba(X_val)[:, 1]\n        score = log_loss(y_val, preds)\n        cv_scores.append(score)\n    \n    return np.mean(cv_scores) if cv_scores else float('inf')\n\ndef objective_xgb(trial, X, y, groups):\n    \"\"\"Optuna objective for XGBoost hyperparameter tuning\"\"\"\n    params = {\n        'n_estimators': trial.suggest_int('n_estimators', 100, 300),\n        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.15, log=True),\n        'max_depth': trial.suggest_int('max_depth', 3, 10),\n        'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),\n        'subsample': trial.suggest_float('subsample', 0.5, 1.0),\n        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),\n        'gamma': trial.suggest_float('gamma', 1e-8, 1.0, log=True),\n        'tree_method': 'hist',\n        'verbosity': 0\n    }\n    \n    model = make_pipeline(SimpleImputer(), XGBClassifier(**params))\n    cv = GroupKFold(n_splits=min(5, len(np.unique(groups))))\n    cv_scores = []\n    \n    for train_idx, val_idx in cv.split(X, y, groups=groups):\n        X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]\n        y_train, y_val = y[train_idx], y[val_idx]\n        \n        if len(np.unique(y_train)) < 2 or len(y_val) < 10:\n            continue\n        \n        model.fit(X_train, y_train)\n        preds = model.predict_proba(X_val)[:, 1]\n        score = log_loss(y_val, preds)\n        cv_scores.append(score)\n    \n    return np.mean(cv_scores) if cv_scores else float('inf')\n\ndef run_optuna_optimization(X_sample, y_sample, groups_sample):\n    \"\"\"Run Optuna optimization and return best parameters\"\"\"\n    best_params = {}\n    \n    if OPTUNA_AVAILABLE and USE_OPTUNA:\n        print(\"Running Optuna hyperparameter optimization...\")\n        \n        # Optimize LightGBM\n        print(\"  Optimizing LightGBM...\")\n        study_lgbm = optuna.create_study(direction='minimize', sampler=optuna.samplers.TPESampler(seed=42))\n        study_lgbm.optimize(lambda trial: objective_lgbm(trial, X_sample, y_sample, groups_sample), \n                           n_trials=OPTUNA_TRIALS, show_progress_bar=True)\n        best_params['lgbm'] = study_lgbm.best_params\n        print(f\"    Best LightGBM log_loss: {study_lgbm.best_value:.4f}\")\n        \n        # Optimize XGBoost if available\n        if XGBOOST_AVAILABLE:\n            print(\"  Optimizing XGBoost...\")\n            study_xgb = optuna.create_study(direction='minimize', sampler=optuna.samplers.TPESampler(seed=42))\n            study_xgb.optimize(lambda trial: objective_xgb(trial, X_sample, y_sample, groups_sample), \n                              n_trials=OPTUNA_TRIALS, show_progress_bar=True)\n            best_params['xgb'] = study_xgb.best_params\n            print(f\"    Best XGBoost log_loss: {study_xgb.best_value:.4f}\")\n    else:\n        # Default parameters\n        best_params['lgbm'] = {\n            'n_estimators': 225, 'learning_rate': 0.07, 'max_depth': -1,\n            'num_leaves': 31, 'min_child_samples': 40, 'subsample': 0.8,\n            'colsample_bytree': 0.8, 'reg_alpha': 0.0, 'reg_lambda': 0.0, 'verbose': -1\n        }\n        if XGBOOST_AVAILABLE:\n            best_params['xgb'] = {\n                'n_estimators': 180, 'learning_rate': 0.08, 'max_depth': 6,\n                'min_child_weight': 5, 'subsample': 0.8, 'colsample_bytree': 0.8,\n                'gamma': 0.0, 'tree_method': 'hist', 'verbosity': 0\n            }\n    \n    return best_params\n\n# ==================== SCORING FUNCTIONS ====================\n\nclass HostVisibleError(Exception):\n    pass\n\ndef single_lab_f1(lab_solution: pl.DataFrame, lab_submission: pl.DataFrame, beta: float = 1) -> float:\n    label_frames: defaultdict[str, set[int]] = defaultdict(set)\n    prediction_frames: defaultdict[str, set[int]] = defaultdict(set)\n\n    for row in lab_solution.to_dicts():\n        label_frames[row['label_key']].update(range(row['start_frame'], row['stop_frame']))\n\n    for video in lab_solution['video_id'].unique():\n        active_labels: str = lab_solution.filter(pl.col('video_id') == video)['behaviors_labeled'].first()\n        active_labels: set[str] = set(json.loads(active_labels))\n        predicted_mouse_pairs: defaultdict[str, set[int]] = defaultdict(set)\n\n        for row in lab_submission.filter(pl.col('video_id') == video).to_dicts():\n            if ','.join([str(row['agent_id']), str(row['target_id']), row['action']]) not in active_labels:\n                continue\n           \n            new_frames = set(range(row['start_frame'], row['stop_frame']))\n            new_frames = new_frames.difference(prediction_frames[row['prediction_key']])\n            prediction_pair = ','.join([str(row['agent_id']), str(row['target_id'])])\n            if predicted_mouse_pairs[prediction_pair].intersection(new_frames):\n                raise HostVisibleError('Multiple predictions for the same frame from one agent/target pair')\n            prediction_frames[row['prediction_key']].update(new_frames)\n            predicted_mouse_pairs[prediction_pair].update(new_frames)\n\n    tps = defaultdict(int)\n    fns = defaultdict(int)\n    fps = defaultdict(int)\n    for key, pred_frames in prediction_frames.items():\n        action = key.split('_')[-1]\n        matched_label_frames = label_frames[key]\n        tps[action] += len(pred_frames.intersection(matched_label_frames))\n        fns[action] += len(matched_label_frames.difference(pred_frames))\n        fps[action] += len(pred_frames.difference(matched_label_frames))\n\n    distinct_actions = set()\n    for key, frames in label_frames.items():\n        action = key.split('_')[-1]\n        distinct_actions.add(action)\n        if key not in prediction_frames:\n            fns[action] += len(frames)\n\n    action_f1s = []\n    for action in distinct_actions:\n        if tps[action] + fns[action] + fps[action] == 0:\n            action_f1s.append(0)\n        else:\n            action_f1s.append((1 + beta**2) * tps[action] / ((1 + beta**2) * tps[action] + beta**2 * fns[action] + fps[action]))\n    return sum(action_f1s) / len(action_f1s)\n\ndef mouse_fbeta(solution: pd.DataFrame, submission: pd.DataFrame, beta: float = 1) -> float:\n    if len(solution) == 0 or len(submission) == 0:\n        raise ValueError('Missing solution or submission data')\n\n    expected_cols = ['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame']\n    for col in expected_cols:\n        if col not in solution.columns:\n            raise ValueError(f'Solution is missing column {col}')\n        if col not in submission.columns:\n            raise ValueError(f'Submission is missing column {col}')\n\n    solution: pl.DataFrame = pl.DataFrame(solution)\n    submission: pl.DataFrame = pl.DataFrame(submission)\n    assert (solution['start_frame'] <= solution['stop_frame']).all()\n    assert (submission['start_frame'] <= submission['stop_frame']).all()\n    solution_videos = set(solution['video_id'].unique())\n    submission = submission.filter(pl.col('video_id').is_in(solution_videos))\n\n    solution = solution.with_columns(\n        pl.concat_str([pl.col('video_id').cast(pl.Utf8), pl.col('agent_id').cast(pl.Utf8),\n                      pl.col('target_id').cast(pl.Utf8), pl.col('action')], separator='_').alias('label_key'))\n    submission = submission.with_columns(\n        pl.concat_str([pl.col('video_id').cast(pl.Utf8), pl.col('agent_id').cast(pl.Utf8),\n                      pl.col('target_id').cast(pl.Utf8), pl.col('action')], separator='_').alias('prediction_key'))\n\n    lab_scores = []\n    for lab in solution['lab_id'].unique():\n        lab_solution = solution.filter(pl.col('lab_id') == lab).clone()\n        lab_videos = set(lab_solution['video_id'].unique())\n        lab_submission = submission.filter(pl.col('video_id').is_in(lab_videos)).clone()\n        lab_scores.append(single_lab_f1(lab_solution, lab_submission, beta=beta))\n\n    return sum(lab_scores) / len(lab_scores)\n\ndef score(solution: pd.DataFrame, submission: pd.DataFrame, row_id_column_name: str, beta: float = 1) -> float:\n    solution = solution.drop(row_id_column_name, axis='columns', errors='ignore')\n    submission = submission.drop(row_id_column_name, axis='columns', errors='ignore')\n    return mouse_fbeta(solution, submission, beta=beta)\n\n# ==================== DATA LOADING ====================\n\ntrain = pd.read_csv('/kaggle/input/MABe-mouse-behavior-detection/train.csv')\ntrain['n_mice'] = 4 - train[['mouse1_strain', 'mouse2_strain', 'mouse3_strain', 'mouse4_strain']].isna().sum(axis=1)\ntrain_without_mabe22 = train.query(\"~ lab_id.str.startswith('MABe22_')\")\n\ntest = pd.read_csv('/kaggle/input/MABe-mouse-behavior-detection/test.csv')\nbody_parts_tracked_list = list(np.unique(train.body_parts_tracked))\n\ndrop_body_parts = ['headpiece_bottombackleft', 'headpiece_bottombackright', 'headpiece_bottomfrontleft', \n                   'headpiece_bottomfrontright', 'headpiece_topbackleft', 'headpiece_topbackright', \n                   'headpiece_topfrontleft', 'headpiece_topfrontright', 'spine_1', 'spine_2', \n                   'tail_middle_1', 'tail_middle_2', 'tail_midpoint']\n\ndef generate_mouse_data(dataset, traintest, traintest_directory=None, generate_single=True, generate_pair=True):\n    assert traintest in ['train', 'test']\n    if traintest_directory is None:\n        traintest_directory = f\"/kaggle/input/MABe-mouse-behavior-detection/{traintest}_tracking\"\n    for _, row in dataset.iterrows():\n        \n        lab_id = row.lab_id\n        if lab_id.startswith('MABe22'): continue\n        video_id = row.video_id\n\n        if type(row.behaviors_labeled) != str:\n            if verbose: print('No labeled behaviors:', lab_id, video_id)\n            continue\n\n        path = f\"{traintest_directory}/{lab_id}/{video_id}.parquet\"\n        vid = pd.read_parquet(path)\n        if len(np.unique(vid.bodypart)) > 5:\n            vid = vid.query(\"~ bodypart.isin(@drop_body_parts)\")\n        pvid = vid.pivot(columns=['mouse_id', 'bodypart'], index='video_frame', values=['x', 'y'])\n        if pvid.isna().any().any():\n            if verbose and traintest == 'test': print('video with missing values', video_id)\n        del vid\n        pvid = pvid.reorder_levels([1, 2, 0], axis=1).T.sort_index().T\n        pvid /= row.pix_per_cm_approx\n\n        vid_behaviors = json.loads(row.behaviors_labeled)\n        vid_behaviors = sorted(list({b.replace(\"'\", \"\") for b in vid_behaviors}))\n        vid_behaviors = [b.split(',') for b in vid_behaviors]\n        vid_behaviors = pd.DataFrame(vid_behaviors, columns=['agent', 'target', 'action'])\n        \n        if traintest == 'train':\n            try:\n                annot = pd.read_parquet(path.replace('train_tracking', 'train_annotation'))\n            except FileNotFoundError:\n                continue\n\n        if generate_single:\n            vid_behaviors_subset = vid_behaviors.query(\"target == 'self'\")\n            for mouse_id_str in np.unique(vid_behaviors_subset.agent):\n                try:\n                    mouse_id = int(mouse_id_str[-1])\n                    vid_agent_actions = np.unique(vid_behaviors_subset.query(\"agent == @mouse_id_str\").action)\n                    single_mouse = pvid.loc[:, mouse_id]\n                    single_mouse_meta = pd.DataFrame({\n                        'video_id': video_id, 'agent_id': mouse_id_str,\n                        'target_id': 'self', 'video_frame': single_mouse.index\n                    })\n                    if traintest == 'train':\n                        single_mouse_label = pd.DataFrame(0.0, columns=vid_agent_actions, index=single_mouse.index)\n                        annot_subset = annot.query(\"(agent_id == @mouse_id) & (target_id == @mouse_id)\")\n                        for i in range(len(annot_subset)):\n                            annot_row = annot_subset.iloc[i]\n                            single_mouse_label.loc[annot_row['start_frame']:annot_row['stop_frame'], annot_row.action] = 1.0\n                        yield 'single', single_mouse, single_mouse_meta, single_mouse_label\n                    else:\n                        if verbose: print('- test single', video_id, mouse_id)\n                        yield 'single', single_mouse, single_mouse_meta, vid_agent_actions\n                except KeyError:\n                    pass\n\n        if generate_pair:\n            vid_behaviors_subset = vid_behaviors.query(\"target != 'self'\")\n            if len(vid_behaviors_subset) > 0:\n                for agent, target in itertools.permutations(np.unique(pvid.columns.get_level_values('mouse_id')), 2):\n                    agent_str = f\"mouse{agent}\"\n                    target_str = f\"mouse{target}\"\n                    vid_agent_actions = np.unique(vid_behaviors_subset.query(\"(agent == @agent_str) & (target == @target_str)\").action)\n                    mouse_pair = pd.concat([pvid[agent], pvid[target]], axis=1, keys=['A', 'B'])\n                    mouse_pair_meta = pd.DataFrame({\n                        'video_id': video_id, 'agent_id': agent_str,\n                        'target_id': target_str, 'video_frame': mouse_pair.index\n                    })\n                    if traintest == 'train':\n                        mouse_pair_label = pd.DataFrame(0.0, columns=vid_agent_actions, index=mouse_pair.index)\n                        annot_subset = annot.query(\"(agent_id == @agent) & (target_id == @target)\")\n                        for i in range(len(annot_subset)):\n                            annot_row = annot_subset.iloc[i]\n                            mouse_pair_label.loc[annot_row['start_frame']:annot_row['stop_frame'], annot_row.action] = 1.0\n                        yield 'pair', mouse_pair, mouse_pair_meta, mouse_pair_label\n                    else:\n                        if verbose: print('- test pair', video_id, agent, target)\n                        yield 'pair', mouse_pair, mouse_pair_meta, vid_agent_actions\n\n# ==================== ADAPTIVE THRESHOLDING ====================\n\naction_thresholds = defaultdict(lambda: 0.27)\n\ndef predict_multiclass_adaptive(pred, meta, action_thresholds):\n    \"\"\"Adaptive thresholding per action + temporal smoothing\"\"\"\n    pred_smoothed = pred.rolling(window=5, min_periods=1, center=True).mean()\n    ama = np.argmax(pred_smoothed, axis=1)\n    max_probs = pred_smoothed.max(axis=1)\n    threshold_mask = np.zeros(len(pred_smoothed), dtype=bool)\n    \n    for i, action in enumerate(pred_smoothed.columns):\n        action_mask = (ama == i)\n        threshold = action_thresholds.get(action, 0.27)\n        threshold_mask |= (action_mask & (max_probs >= threshold))\n    \n    ama = np.where(threshold_mask, ama, -1)\n    ama = pd.Series(ama, index=meta.video_frame)\n    changes_mask = (ama != ama.shift(1)).values\n    ama_changes = ama[changes_mask]\n    meta_changes = meta[changes_mask]\n    mask = ama_changes.values >= 0\n    mask[-1] = False\n    \n    submission_part = pd.DataFrame({\n        'video_id': meta_changes['video_id'][mask].values,\n        'agent_id': meta_changes['agent_id'][mask].values,\n        'target_id': meta_changes['target_id'][mask].values,\n        'action': pred.columns[ama_changes[mask].values],\n        'start_frame': ama_changes.index[mask],\n        'stop_frame': ama_changes.index[1:][mask[:-1]]\n    })\n    \n    stop_video_id = meta_changes['video_id'][1:][mask[:-1]].values\n    stop_agent_id = meta_changes['agent_id'][1:][mask[:-1]].values\n    stop_target_id = meta_changes['target_id'][1:][mask[:-1]].values\n    \n    for i in range(len(submission_part)):\n        video_id = submission_part.video_id.iloc[i]\n        if i < len(stop_video_id):\n            if stop_video_id[i] != video_id or stop_agent_id[i] != submission_part.agent_id.iloc[i] or stop_target_id[i] != submission_part.target_id.iloc[i]:\n                new_stop_frame = meta.query(\"(video_id == @video_id)\").video_frame.max() + 1\n                submission_part.iat[i, submission_part.columns.get_loc('stop_frame')] = new_stop_frame\n        else:\n            new_stop_frame = meta.query(\"(video_id == @video_id)\").video_frame.max() + 1\n            submission_part.iat[i, submission_part.columns.get_loc('stop_frame')] = new_stop_frame\n    \n    duration = submission_part.stop_frame - submission_part.start_frame\n    submission_part = submission_part[duration >= 3].reset_index(drop=True)\n    \n    if len(submission_part) > 0:\n        assert (submission_part.stop_frame > submission_part.start_frame).all()\n    \n    if verbose: print(f'  actions found: {len(submission_part)}')\n    return submission_part\n\n# ==================== COMPLETE FEATURE ENGINEERING ====================\n\ndef add_curvature_features(X, center_x, center_y):\n    \"\"\"Trajectory curvature\"\"\"\n    vel_x = center_x.diff()\n    vel_y = center_y.diff()\n    acc_x = vel_x.diff()\n    acc_y = vel_y.diff()\n    \n    cross_prod = vel_x * acc_y - vel_y * acc_x\n    vel_mag = np.sqrt(vel_x**2 + vel_y**2)\n    curvature = np.abs(cross_prod) / (vel_mag**3 + 1e-6)\n    \n    for window in [30, 60]:\n        X[f'curv_mean_{window}'] = curvature.rolling(window, min_periods=5).mean()\n    \n    angle = np.arctan2(vel_y, vel_x)\n    angle_change = np.abs(angle.diff())\n    X['turn_rate_30'] = angle_change.rolling(30, min_periods=5).sum()\n    \n    return X\n\ndef add_multiscale_features(X, center_x, center_y):\n    \"\"\"Multi-scale temporal features\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2)\n    \n    scales = [10, 40, 160]\n    for scale in scales:\n        if len(speed) >= scale:\n            X[f'sp_m{scale}'] = speed.rolling(scale, min_periods=max(1, scale//4)).mean()\n            X[f'sp_s{scale}'] = speed.rolling(scale, min_periods=max(1, scale//4)).std()\n    \n    if len(scales) >= 2 and f'sp_m{scales[0]}' in X.columns and f'sp_m{scales[-1]}' in X.columns:\n        X['sp_ratio'] = X[f'sp_m{scales[0]}'] / (X[f'sp_m{scales[-1]}'] + 1e-6)\n    \n    return X\n\ndef add_state_features(X, center_x, center_y):\n    \"\"\"Behavioral state transitions\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2)\n    speed_ma = speed.rolling(15, min_periods=5).mean()\n    \n    try:\n        speed_states = pd.cut(speed_ma, bins=[-np.inf, 0.5, 2.0, 5.0, np.inf], labels=[0, 1, 2, 3]).astype(float)\n        \n        for window in [60, 120]:\n            if len(speed_states) >= window:\n                for state in [0, 1, 2, 3]:\n                    X[f's{state}_{window}'] = (speed_states == state).astype(float).rolling(window, min_periods=10).mean()\n                \n                state_changes = (speed_states != speed_states.shift(1)).astype(float)\n                X[f'trans_{window}'] = state_changes.rolling(window, min_periods=10).sum()\n    except:\n        pass\n    \n    return X\n\ndef add_longrange_features(X, center_x, center_y):\n    \"\"\"Long-range temporal features\"\"\"\n    for window in [120, 240]:\n        if len(center_x) >= window:\n            X[f'x_ml{window}'] = center_x.rolling(window, min_periods=20).mean()\n            X[f'y_ml{window}'] = center_y.rolling(window, min_periods=20).mean()\n    \n    for span in [60, 120]:\n        X[f'x_e{span}'] = center_x.ewm(span=span, min_periods=1).mean()\n        X[f'y_e{span}'] = center_y.ewm(span=span, min_periods=1).mean()\n    \n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2)\n    for window in [60, 120]:\n        if len(speed) >= window:\n            X[f'sp_pct{window}'] = speed.rolling(window, min_periods=20).rank(pct=True)\n    \n    return X\n\ndef add_interaction_features(X, mouse_pair, avail_A, avail_B):\n    \"\"\"Social interaction features\"\"\"\n    if 'body_center' not in avail_A or 'body_center' not in avail_B:\n        return X\n    \n    rel_x = mouse_pair['A']['body_center']['x'] - mouse_pair['B']['body_center']['x']\n    rel_y = mouse_pair['A']['body_center']['y'] - mouse_pair['B']['body_center']['y']\n    rel_dist = np.sqrt(rel_x**2 + rel_y**2)\n    \n    A_vx = mouse_pair['A']['body_center']['x'].diff()\n    A_vy = mouse_pair['A']['body_center']['y'].diff()\n    B_vx = mouse_pair['B']['body_center']['x'].diff()\n    B_vy = mouse_pair['B']['body_center']['y'].diff()\n    \n    A_lead = (A_vx * rel_x + A_vy * rel_y) / (np.sqrt(A_vx**2 + A_vy**2) * rel_dist + 1e-6)\n    B_lead = (B_vx * (-rel_x) + B_vy * (-rel_y)) / (np.sqrt(B_vx**2 + B_vy**2) * rel_dist + 1e-6)\n    \n    for window in [30, 60]:\n        X[f'A_ld{window}'] = A_lead.rolling(window, min_periods=5).mean()\n        X[f'B_ld{window}'] = B_lead.rolling(window, min_periods=5).mean()\n    \n    approach = -rel_dist.diff()\n    chase = approach * B_lead\n    X['chase_30'] = chase.rolling(30, min_periods=5).mean()\n    \n    for window in [60, 120]:\n        A_sp = np.sqrt(A_vx**2 + A_vy**2)\n        B_sp = np.sqrt(B_vx**2 + B_vy**2)\n        X[f'sp_cor{window}'] = A_sp.rolling(window, min_periods=10).corr(B_sp)\n    \n    return X\n\ndef transform_single(single_mouse, body_parts_tracked):\n    \"\"\"Enhanced single mouse transform with FULL features\"\"\"\n    available_body_parts = single_mouse.columns.get_level_values(0)\n    \n    # Base distance features\n    X = pd.DataFrame({\n        f\"{p1}+{p2}\": np.square(single_mouse[p1] - single_mouse[p2]).sum(axis=1, skipna=False)\n        for p1, p2 in itertools.combinations(body_parts_tracked, 2) \n        if p1 in available_body_parts and p2 in available_body_parts\n    })\n    X = X.reindex(columns=[f\"{p1}+{p2}\" for p1, p2 in itertools.combinations(body_parts_tracked, 2)], copy=False)\n\n    # Speed features\n    if all(p in single_mouse.columns for p in ['ear_left', 'ear_right', 'tail_base']):\n        shifted = single_mouse[['ear_left', 'ear_right', 'tail_base']].shift(10)\n        speeds = pd.DataFrame({\n            'sp_lf': np.square(single_mouse['ear_left'] - shifted['ear_left']).sum(axis=1, skipna=False),\n            'sp_rt': np.square(single_mouse['ear_right'] - shifted['ear_right']).sum(axis=1, skipna=False),\n            'sp_lf2': np.square(single_mouse['ear_left'] - shifted['tail_base']).sum(axis=1, skipna=False),\n            'sp_rt2': np.square(single_mouse['ear_right'] - shifted['tail_base']).sum(axis=1, skipna=False),\n        })\n        X = pd.concat([X, speeds], axis=1)\n    \n    if 'nose+tail_base' in X.columns and 'ear_left+ear_right' in X.columns:\n        X['elong'] = X['nose+tail_base'] / (X['ear_left+ear_right'] + 1e-6)\n    \n    # Body angle\n    if all(p in available_body_parts for p in ['nose', 'body_center', 'tail_base']):\n        v1 = single_mouse['nose'] - single_mouse['body_center']\n        v2 = single_mouse['tail_base'] - single_mouse['body_center']\n        X['body_ang'] = (v1['x'] * v2['x'] + v1['y'] * v2['y']) / (\n            np.sqrt(v1['x']**2 + v1['y']**2) * np.sqrt(v2['x']**2 + v2['y']**2) + 1e-6)\n    \n    # Core temporal features\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']\n        cy = single_mouse['body_center']['y']\n        \n        for w in [5, 15, 30, 60]:\n            X[f'cx_m{w}'] = cx.rolling(w, min_periods=1, center=True).mean()\n            X[f'cy_m{w}'] = cy.rolling(w, min_periods=1, center=True).mean()\n            X[f'cx_s{w}'] = cx.rolling(w, min_periods=1, center=True).std()\n            X[f'cy_s{w}'] = cy.rolling(w, min_periods=1, center=True).std()\n            X[f'x_rng{w}'] = cx.rolling(w, min_periods=1, center=True).max() - cx.rolling(w, min_periods=1, center=True).min()\n            X[f'y_rng{w}'] = cy.rolling(w, min_periods=1, center=True).max() - cy.rolling(w, min_periods=1, center=True).min()\n            X[f'disp{w}'] = np.sqrt(cx.diff().rolling(w, min_periods=1).sum()**2 + cy.diff().rolling(w, min_periods=1).sum()**2)\n            X[f'act{w}'] = np.sqrt(cx.diff().rolling(w, min_periods=1).var() + cy.diff().rolling(w, min_periods=1).var())\n        \n        # Advanced features\n        X = add_curvature_features(X, cx, cy)\n        X = add_multiscale_features(X, cx, cy)\n        X = add_state_features(X, cx, cy)\n        X = add_longrange_features(X, cx, cy)\n    \n    # Nose-tail features\n    if all(p in available_body_parts for p in ['nose', 'tail_base']):\n        nt_dist = np.sqrt((single_mouse['nose']['x'] - single_mouse['tail_base']['x'])**2 + \n                         (single_mouse['nose']['y'] - single_mouse['tail_base']['y'])**2)\n        for lag in [10, 20, 40]:\n            X[f'nt_lg{lag}'] = nt_dist.shift(lag)\n            X[f'nt_df{lag}'] = nt_dist - nt_dist.shift(lag)\n    \n    # Ear features\n    if all(p in available_body_parts for p in ['ear_left', 'ear_right']):\n        ear_d = np.sqrt((single_mouse['ear_left']['x'] - single_mouse['ear_right']['x'])**2 + \n                       (single_mouse['ear_left']['y'] - single_mouse['ear_right']['y'])**2)\n        for off in [-20, -10, 10, 20]:\n            X[f'ear_o{off}'] = ear_d.shift(-off)\n        X['ear_con'] = ear_d.rolling(30, min_periods=1, center=True).std() / (ear_d.rolling(30, min_periods=1, center=True).mean() + 1e-6)\n    \n    return X\n\ndef transform_pair(mouse_pair, body_parts_tracked):\n    \"\"\"Enhanced pair transform with FULL features\"\"\"\n    avail_A = mouse_pair['A'].columns.get_level_values(0)\n    avail_B = mouse_pair['B'].columns.get_level_values(0)\n    \n    # Inter-mouse distances\n    X = pd.DataFrame({\n        f\"12+{p1}+{p2}\": np.square(mouse_pair['A'][p1] - mouse_pair['B'][p2]).sum(axis=1, skipna=False)\n        for p1, p2 in itertools.product(body_parts_tracked, repeat=2) \n        if p1 in avail_A and p2 in avail_B\n    })\n    X = X.reindex(columns=[f\"12+{p1}+{p2}\" for p1, p2 in itertools.product(body_parts_tracked, repeat=2)], copy=False)\n\n    # Speed features\n    if ('A', 'ear_left') in mouse_pair.columns and ('B', 'ear_left') in mouse_pair.columns:\n        shA = mouse_pair['A']['ear_left'].shift(10)\n        shB = mouse_pair['B']['ear_left'].shift(10)\n        speeds = pd.DataFrame({\n            'sp_A': np.square(mouse_pair['A']['ear_left'] - shA).sum(axis=1, skipna=False),\n            'sp_AB': np.square(mouse_pair['A']['ear_left'] - shB).sum(axis=1, skipna=False),\n            'sp_B': np.square(mouse_pair['B']['ear_left'] - shB).sum(axis=1, skipna=False),\n        })\n        X = pd.concat([X, speeds], axis=1)\n    \n    if 'nose+tail_base' in X.columns and 'ear_left+ear_right' in X.columns:\n        X['elong'] = X['nose+tail_base'] / (X['ear_left+ear_right'] + 1e-6)\n    \n    # Relative orientation\n    if all(p in avail_A for p in ['nose', 'tail_base']) and all(p in avail_B for p in ['nose', 'tail_base']):\n        dir_A = mouse_pair['A']['nose'] - mouse_pair['A']['tail_base']\n        dir_B = mouse_pair['B']['nose'] - mouse_pair['B']['tail_base']\n        X['rel_ori'] = (dir_A['x'] * dir_B['x'] + dir_A['y'] * dir_B['y']) / (\n            np.sqrt(dir_A['x']**2 + dir_A['y']**2) * np.sqrt(dir_B['x']**2 + dir_B['y']**2) + 1e-6)\n    \n    # Approach rate\n    if all(p in avail_A for p in ['nose']) and all(p in avail_B for p in ['nose']):\n        cur = np.square(mouse_pair['A']['nose'] - mouse_pair['B']['nose']).sum(axis=1, skipna=False)\n        shA_n = mouse_pair['A']['nose'].shift(10)\n        shB_n = mouse_pair['B']['nose'].shift(10)\n        past = np.square(shA_n - shB_n).sum(axis=1, skipna=False)\n        X['appr'] = cur - past\n    \n    # Distance bins\n    if 'body_center' in avail_A and 'body_center' in avail_B:\n        cd = np.sqrt((mouse_pair['A']['body_center']['x'] - mouse_pair['B']['body_center']['x'])**2 +\n                    (mouse_pair['A']['body_center']['y'] - mouse_pair['B']['body_center']['y'])**2)\n        X['v_cls'] = (cd < 5.0).astype(float)\n        X['cls'] = ((cd >= 5.0) & (cd < 15.0)).astype(float)\n        X['med'] = ((cd >= 15.0) & (cd < 30.0)).astype(float)\n        X['far'] = (cd >= 30.0).astype(float)\n    \n    # Temporal interaction\n    if 'body_center' in avail_A and 'body_center' in avail_B:\n        cd_full = np.square(mouse_pair['A']['body_center'] - mouse_pair['B']['body_center']).sum(axis=1, skipna=False)\n        \n        for w in [5, 15, 30, 60]:\n            X[f'd_m{w}'] = cd_full.rolling(w, min_periods=1, center=True).mean()\n            X[f'd_s{w}'] = cd_full.rolling(w, min_periods=1, center=True).std()\n            X[f'd_mn{w}'] = cd_full.rolling(w, min_periods=1, center=True).min()\n            X[f'd_mx{w}'] = cd_full.rolling(w, min_periods=1, center=True).max()\n            \n            d_var = cd_full.rolling(w, min_periods=1, center=True).var()\n            X[f'int{w}'] = 1 / (1 + d_var)\n            \n            Axd = mouse_pair['A']['body_center']['x'].diff()\n            Ayd = mouse_pair['A']['body_center']['y'].diff()\n            Bxd = mouse_pair['B']['body_center']['x'].diff()\n            Byd = mouse_pair['B']['body_center']['y'].diff()\n            coord = Axd * Bxd + Ayd * Byd\n            X[f'co_m{w}'] = coord.rolling(w, min_periods=1, center=True).mean()\n            X[f'co_s{w}'] = coord.rolling(w, min_periods=1, center=True).std()\n    \n    # Nose-nose\n    if 'nose' in avail_A and 'nose' in avail_B:\n        nn = np.sqrt((mouse_pair['A']['nose']['x'] - mouse_pair['B']['nose']['x'])**2 +\n                    (mouse_pair['A']['nose']['y'] - mouse_pair['B']['nose']['y'])**2)\n        for lag in [10, 20, 40]:\n            X[f'nn_lg{lag}'] = nn.shift(lag)\n            X[f'nn_ch{lag}'] = nn - nn.shift(lag)\n            is_cl = (nn < 10.0).astype(float)\n            X[f'cl_ps{lag}'] = is_cl.rolling(lag, min_periods=1).mean()\n    \n    # Velocity alignment\n    if 'body_center' in avail_A and 'body_center' in avail_B:\n        Avx = mouse_pair['A']['body_center']['x'].diff()\n        Avy = mouse_pair['A']['body_center']['y'].diff()\n        Bvx = mouse_pair['B']['body_center']['x'].diff()\n        Bvy = mouse_pair['B']['body_center']['y'].diff()\n        val = (Avx * Bvx + Avy * Bvy) / (np.sqrt(Avx**2 + Avy**2) * np.sqrt(Bvx**2 + Bvy**2) + 1e-6)\n        \n        for off in [-20, -10, 0, 10, 20]:\n            X[f'va_{off}'] = val.shift(-off)\n        \n        X['int_con'] = cd_full.rolling(30, min_periods=1, center=True).std() / (cd_full.rolling(30, min_periods=1, center=True).mean() + 1e-6)\n        \n        # Advanced interaction\n        X = add_interaction_features(X, mouse_pair, avail_A, avail_B)\n    \n    return X\n\n# ==================== ENSEMBLE TRAINING ====================\n\ndef submit_ensemble(body_parts_tracked_str, switch_tr, X_tr, label, meta, best_params):\n    \"\"\"Advanced ensemble with optimized hyperparameters\"\"\"\n    \n    models = []\n    \n    # Model 1: Optimized LightGBM (or default)\n    lgbm_params = best_params.get('lgbm', {\n        'n_estimators': 225, 'learning_rate': 0.07, 'num_leaves': 31,\n        'min_child_samples': 40, 'subsample': 0.8, 'colsample_bytree': 0.8, 'verbose': -1\n    })\n    models.append(make_pipeline(\n        SimpleImputer(),\n        StratifiedSubsetClassifier(lightgbm.LGBMClassifier(**lgbm_params), 100000)\n    ))\n    \n    # Model 2: Diverse LightGBM\n    models.append(make_pipeline(\n        SimpleImputer(),\n        StratifiedSubsetClassifier(\n            lightgbm.LGBMClassifier(\n                n_estimators=150, learning_rate=0.1, num_leaves=63, max_depth=8,\n                min_child_samples=20, subsample=0.7, colsample_bytree=0.9,\n                reg_alpha=0.1, reg_lambda=0.1, verbose=-1),\n            80000)\n    ))\n    \n    # Model 3: Deep LightGBM\n    models.append(make_pipeline(\n        SimpleImputer(),\n        StratifiedSubsetClassifier(\n            lightgbm.LGBMClassifier(\n                n_estimators=100, learning_rate=0.05, min_child_samples=30,\n                num_leaves=127, max_depth=10, subsample=0.75, verbose=-1),\n            60000)\n    ))\n    \n    # Model 4: Optimized XGBoost (if available)\n    if XGBOOST_AVAILABLE:\n        xgb_params = best_params.get('xgb', {\n            'n_estimators': 180, 'learning_rate': 0.08, 'max_depth': 6,\n            'min_child_weight': 5, 'subsample': 0.8, 'colsample_bytree': 0.8,\n            'tree_method': 'hist', 'verbosity': 0\n        })\n        models.append(make_pipeline(\n            SimpleImputer(),\n            StratifiedSubsetClassifier(XGBClassifier(**xgb_params), 85000)\n        ))\n    \n    # Model 5: CatBoost (if available)\n    if CATBOOST_AVAILABLE:\n        models.append(make_pipeline(\n            SimpleImputer(),\n            StratifiedSubsetClassifier(\n                CatBoostClassifier(\n                    iterations=120, learning_rate=0.1, depth=6,\n                    verbose=False, allow_writing_files=False),\n                70000)\n        ))\n    \n    model_list = []\n    for action in label.columns:\n        action_mask = ~label[action].isna().values\n        y_action = label[action][action_mask].values.astype(int)\n\n        if not (y_action == 0).all() and y_action.sum() >= 5:\n            trained = []\n            for m in models:\n                m_clone = clone(m)\n                m_clone.fit(X_tr[action_mask], y_action)\n                trained.append(m_clone)\n            model_list.append((action, trained))\n    \n    del X_tr\n    gc.collect()\n\n    body_parts_tracked = json.loads(body_parts_tracked_str)\n    if len(body_parts_tracked) > 5:\n        body_parts_tracked = [b for b in body_parts_tracked if b not in drop_body_parts]\n    \n    test_subset = test[test.body_parts_tracked == body_parts_tracked_str]\n    generator = generate_mouse_data(test_subset, 'test',\n                                    generate_single=(switch_tr == 'single'), \n                                    generate_pair=(switch_tr == 'pair'))\n    \n    if verbose: print(f\"n_videos: {len(test_subset)}, n_models: {len(models)}\")\n    \n    for switch_te, data_te, meta_te, actions_te in generator:\n        assert switch_te == switch_tr\n        try:\n            if switch_te == 'single':\n                X_te = transform_single(data_te, body_parts_tracked)\n            else:\n                X_te = transform_pair(data_te, body_parts_tracked)\n            \n            if verbose and len(X_te) == 0: print(\"ERROR: X_te empty\")\n            del data_te\n    \n            pred = pd.DataFrame(index=meta_te.video_frame)\n            for action, trained in model_list:\n                if action in actions_te:\n                    probs = [m.predict_proba(X_te)[:, 1] for m in trained]\n                    pred[action] = np.mean(probs, axis=0)\n            \n            del X_te\n            gc.collect()\n            \n            if pred.shape[1] != 0:\n                sub_part = predict_multiclass_adaptive(pred, meta_te, action_thresholds)\n                submission_list.append(sub_part)\n            else:\n                if verbose: print(f\"  ERROR: no training data\")\n        except Exception as e:\n            if verbose: print(f'  ERROR: {str(e)[:50]}')\n            try:\n                del data_te\n            except:\n                pass\n            gc.collect()\n\ndef robustify(submission, dataset, traintest, traintest_directory=None):\n    \"\"\"Robustness post-processing\"\"\"\n    if traintest_directory is None:\n        traintest_directory = f\"/kaggle/input/MABe-mouse-behavior-detection/{traintest}_tracking\"\n\n    submission = submission[submission.start_frame < submission.stop_frame]\n    \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 = 0\n        for i, (_, row) in enumerate(group.iterrows()):\n            if row['start_frame'] < last_stop:\n                mask[i] = False\n            else:\n                last_stop = row['stop_frame']\n        group_list.append(group[mask])\n    submission = pd.concat(group_list) if group_list else submission\n\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        video_id = row['video_id']\n        if (submission.video_id == video_id).any():\n            continue\n\n        if verbose: 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 = eval(row['behaviors_labeled'])\n        vid_behaviors = sorted(list({b.replace(\"'\", \"\") for b in vid_behaviors}))\n        vid_behaviors = [b.split(',') for b in vid_behaviors]\n        vid_behaviors = pd.DataFrame(vid_behaviors, columns=['agent', 'target', 'action'])\n    \n        start_frame = vid.video_frame.min()\n        stop_frame = vid.video_frame.max() + 1\n    \n        for (agent, target), actions in vid_behaviors.groupby(['agent', 'target']):\n            batch_len = 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_len\n                batch_stop = min(batch_start + batch_len, stop_frame)\n                s_list.append((video_id, agent, target, action_row['action'], batch_start, batch_stop))\n\n    if len(s_list) > 0:\n        submission = pd.concat([\n            submission,\n            pd.DataFrame(s_list, columns=['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame'])\n        ])\n\n    submission = submission.reset_index(drop=True)\n    return submission\n\n# ==================== MAIN LOOP ====================\n\nsubmission_list = []\nbest_params = {}\n\nprint(f\"XGBoost: {XGBOOST_AVAILABLE}, CatBoost: {CATBOOST_AVAILABLE}, Optuna: {OPTUNA_AVAILABLE}\")\nprint(f\"USE_OPTUNA: {USE_OPTUNA}\\n\")\n\n# Run Optuna optimization once on a sample if enabled\nif USE_OPTUNA and OPTUNA_AVAILABLE:\n    print(\"=\"*60)\n    print(\"PHASE 1: HYPERPARAMETER OPTIMIZATION\")\n    print(\"=\"*60)\n    \n    body_parts_tracked_str = body_parts_tracked_list[1]\n    body_parts_tracked = json.loads(body_parts_tracked_str)\n    if len(body_parts_tracked) > 5:\n        body_parts_tracked = [b for b in body_parts_tracked if b not in drop_body_parts]\n    \n    train_subset = train[train.body_parts_tracked == body_parts_tracked_str].head(3)\n    \n    single_list, single_label_list, single_meta_list = [], [], []\n    for switch, data, meta, label in generate_mouse_data(train_subset, 'train'):\n        if switch == 'single':\n            single_list.append(data)\n            single_meta_list.append(meta)\n            single_label_list.append(label)\n            if len(single_list) >= 2:\n                break\n    \n    if len(single_list) > 0:\n        single_mouse = pd.concat(single_list)\n        single_label = pd.concat(single_label_list)\n        single_meta = pd.concat(single_meta_list)\n        \n        X_sample = transform_single(single_mouse, body_parts_tracked)\n        \n        for action in single_label.columns:\n            action_mask = ~single_label[action].isna().values\n            y_sample = single_label[action][action_mask].values.astype(int)\n            groups_sample = single_meta.video_id[action_mask].values\n            \n            if y_sample.sum() >= 10 and len(np.unique(groups_sample)) >= 3:\n                X_opt = X_sample[action_mask]\n                best_params = run_optuna_optimization(X_opt, y_sample, groups_sample)\n                break\n        \n        del single_mouse, single_label, single_meta, X_sample\n        gc.collect()\n    \n    print(\"\\n\" + \"=\"*60)\n    print(\"PHASE 2: TRAINING WITH OPTIMIZED PARAMETERS\")\n    print(\"=\"*60 + \"\\n\")\n\n# Main training loop\nfor section in range(1, len(body_parts_tracked_list)):\n    body_parts_tracked_str = body_parts_tracked_list[section]\n    try:\n        body_parts_tracked = json.loads(body_parts_tracked_str)\n        print(f\"{section}. Processing: {len(body_parts_tracked)} body parts\")\n        if len(body_parts_tracked) > 5:\n            body_parts_tracked = [b for b in body_parts_tracked if b not in drop_body_parts]\n    \n        train_subset = train[train.body_parts_tracked == body_parts_tracked_str]\n        single_list, single_label_list, single_meta_list = [], [], []\n        pair_list, pair_label_list, pair_meta_list = [], [], []\n    \n        for switch, data, meta, label in generate_mouse_data(train_subset, 'train'):\n            if switch == 'single':\n                single_list.append(data)\n                single_meta_list.append(meta)\n                single_label_list.append(label)\n            else:\n                pair_list.append(data)\n                pair_meta_list.append(meta)\n                pair_label_list.append(label)\n    \n        if len(single_list) > 0:\n            single_mouse = pd.concat(single_list)\n            single_label = pd.concat(single_label_list)\n            single_meta = pd.concat(single_meta_list)\n            del single_list, single_label_list, single_meta_list\n            gc.collect()\n            \n            X_tr = transform_single(single_mouse, body_parts_tracked)\n            del single_mouse\n            print(f\"  Single: {X_tr.shape}\")\n            submit_ensemble(body_parts_tracked_str, 'single', X_tr, single_label, single_meta, best_params)\n                \n        if len(pair_list) > 0:\n            mouse_pair = pd.concat(pair_list)\n            pair_label = pd.concat(pair_label_list)\n            pair_meta = pd.concat(pair_meta_list)\n            del pair_list, pair_label_list, pair_meta_list\n            gc.collect()\n        \n            X_tr = transform_pair(mouse_pair, body_parts_tracked)\n            del mouse_pair\n            print(f\"  Pair: {X_tr.shape}\")\n            submit_ensemble(body_parts_tracked_str, 'pair', X_tr, pair_label, pair_meta, best_params)\n                \n    except Exception as e:\n        print(f'***Exception*** {str(e)[:100]}')\n    \n    gc.collect()\n    print()\n\n# Final submission\nif len(submission_list) > 0:\n    submission = pd.concat(submission_list)\nelse:\n    submission = pd.DataFrame({\n        'video_id': [438887472], 'agent_id': ['mouse1'], 'target_id': ['self'],\n        'action': ['rear'], 'start_frame': [278], 'stop_frame': [500]\n    })\n\nsubmission_robust = robustify(submission, test, 'test')\nsubmission_robust.index.name = 'row_id'\nsubmission_robust.to_csv('submission.csv')\nprint(f\"\\nSubmission created: {len(submission_robust)} predictions\")\n\nif USE_OPTUNA and best_params:\n    print(\"\\n\" + \"=\"*60)\n    print(\"OPTIMIZED HYPERPARAMETERS USED:\")\n    print(\"=\"*60)\n    for model_name, params in best_params.items():\n        print(f\"\\n{model_name.upper()}:\")\n        for param, value in params.items():\n            print(f\"  {param}: {value}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}