{"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":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install pandas numpy scipy scikit-learn lightgbm xgboost catboost polars tqdm","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# MABe Mouse Behavior Detection - Enhanced with Validated Expert System\n# Complete working solution with optimized expert voting and adjustments\n\nvalidate_or_submit = 'submit'\nverbose = True\n\nimport pandas as pd\nimport numpy as np\nfrom tqdm import tqdm\nimport itertools\nimport warnings\nimport json\nimport os, random\nimport gc\nimport lightgbm\nfrom collections import defaultdict\nimport polars as pl\nfrom scipy import signal, stats\nfrom sklearn.base import ClassifierMixin, BaseEstimator, clone\nfrom sklearn.model_selection import cross_val_predict, GroupKFold, StratifiedKFold, cross_val_score\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.model_selection import StratifiedShuffleSplit\nfrom sklearn.metrics import f1_score, roc_auc_score\nfrom sklearn.calibration import CalibratedClassifierCV\nfrom sklearn.preprocessing import StandardScaler\n\nwarnings.filterwarnings('ignore')\n\n# Try importing additional models\ntry:\n    from xgboost import XGBClassifier\n    XGBOOST_AVAILABLE = True\nexcept:\n    XGBOOST_AVAILABLE = False\n    \ntry:\n    from catboost import CatBoostClassifier\n    CATBOOST_AVAILABLE = True\nexcept:\n    CATBOOST_AVAILABLE = False\n\n# --- SEED EVERYTHING -----\nSEED = 1234\nos.environ[\"PYTHONHASHSEED\"] = str(SEED)\nrnd = np.random.RandomState(SEED)\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\n# ==================== ENHANCED EXPERT SYSTEM WITH VALIDATION ====================\n\nclass ValidatedExpertSystem:\n    \"\"\"Advanced expert system with validation, voting, and optimization\"\"\"\n    \n    def __init__(self):\n        self.experts = {}\n        self.expert_weights = defaultdict(dict)\n        self.validation_scores = defaultdict(dict)\n        self.behavior_patterns = {}\n        self.temporal_adjustments = {}\n        self.confidence_thresholds = defaultdict(lambda: 0.3)\n        \n    def create_expert_models(self, action_name, n_samples, positive_rate):\n        \"\"\"Create multiple expert architectures based on behavior characteristics\"\"\"\n        experts = []\n        \n        # Expert 1: Calibrated model for probability refinement\n        if positive_rate < 0.1:\n            expert1 = CalibratedClassifierCV(\n                estimator=lightgbm.LGBMClassifier(\n                    n_estimators=150, learning_rate=0.05, num_leaves=20,\n                    min_child_samples=10, subsample=0.7, colsample_bytree=0.7,\n                    class_weight='balanced', verbose=-1, random_state=SEED\n                ),\n                method='isotonic', cv=3\n            )\n            experts.append(('calibrated', expert1))\n        \n        # Expert 2: Deep tree model for complex patterns\n        expert2 = lightgbm.LGBMClassifier(\n            n_estimators=100, learning_rate=0.08, num_leaves=50,\n            max_depth=8, min_child_samples=20, subsample=0.8,\n            colsample_bytree=0.8, verbose=-1, random_state=SEED+1\n        )\n        experts.append(('deep', expert2))\n        \n        # Expert 3: Shallow model for simple patterns\n        expert3 = lightgbm.LGBMClassifier(\n            n_estimators=80, learning_rate=0.15, num_leaves=15,\n            max_depth=4, min_child_samples=30, subsample=0.9,\n            verbose=-1, random_state=SEED+2\n        )\n        experts.append(('shallow', expert3))\n        \n        # Expert 4: XGBoost variant if available\n        if XGBOOST_AVAILABLE and n_samples > 100:\n            expert4 = XGBClassifier(\n                n_estimators=100, learning_rate=0.1, max_depth=5,\n                min_child_weight=10, subsample=0.8, colsample_bytree=0.8,\n                tree_method='hist', verbosity=0, random_state=SEED+3\n            )\n            experts.append(('xgboost', expert4))\n        \n        return experts\n    \n    def validate_expert(self, X, y, expert, expert_name):\n        \"\"\"Validate expert performance using cross-validation\"\"\"\n        if len(np.unique(y)) < 2 or np.sum(y) < 10:\n            return 0.0\n        \n        try:\n            # Use stratified k-fold for validation\n            n_splits = min(3, np.sum(y))\n            if n_splits < 2:\n                return 0.0\n                \n            skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=SEED)\n            scores = []\n            \n            for train_idx, val_idx in skf.split(X, y):\n                X_train, X_val = X[train_idx], X[val_idx]\n                y_train, y_val = y[train_idx], y[val_idx]\n                \n                # Clone and train the expert\n                expert_clone = clone(expert)\n                expert_clone.fit(X_train, y_train)\n                \n                # Predict and score\n                y_pred = expert_clone.predict_proba(X_val)[:, 1]\n                score = f1_score(y_val, y_pred > 0.5)\n                scores.append(score)\n            \n            return np.mean(scores)\n            \n        except Exception:\n            return 0.0\n    \n    def train_experts(self, X, y, action_name):\n        \"\"\"Train and validate multiple experts for an action\"\"\"\n        n_samples = len(y)\n        n_positive = np.sum(y)\n        positive_rate = n_positive / max(n_samples, 1)\n        \n        # Store behavior pattern information\n        self.behavior_patterns[action_name] = {\n            'positive_rate': positive_rate,\n            'n_samples': n_samples,\n            'n_positive': n_positive,\n            'is_rare': positive_rate < 0.05,\n            'is_common': positive_rate > 0.2,\n            'confidence_threshold': 0.25 if positive_rate < 0.05 else 0.35\n        }\n        \n        # Skip expert training if insufficient data\n        if n_positive < 5:\n            return\n        \n        # Create expert models\n        expert_models = self.create_expert_models(action_name, n_samples, positive_rate)\n        \n        # Train and validate each expert\n        trained_experts = []\n        for expert_name, expert_model in expert_models:\n            try:\n                # Train the expert\n                expert_model.fit(X, y)\n                \n                # Validate the expert\n                val_score = self.validate_expert(X, y, expert_model, expert_name)\n                \n                if val_score > 0.1:  # Only keep experts with reasonable performance\n                    trained_experts.append((expert_name, expert_model))\n                    self.validation_scores[action_name][expert_name] = val_score\n                    \n                    # Calculate expert weight based on validation score\n                    if val_score > 0.4:\n                        weight = min(0.3, val_score * 0.5)\n                    elif val_score > 0.2:\n                        weight = min(0.15, val_score * 0.3)\n                    else:\n                        weight = 0.05\n                    \n                    self.expert_weights[action_name][expert_name] = weight\n                    \n            except Exception:\n                continue\n        \n        if trained_experts:\n            self.experts[action_name] = trained_experts\n            \n            # Set temporal adjustment parameters\n            if action_name in ['grooming', 'rearing', 'investigating', 'following']:\n                self.temporal_adjustments[action_name] = {\n                    'min_duration': 5,\n                    'smoothing_window': 7,\n                    'consistency_weight': 0.8\n                }\n            elif positive_rate < 0.05:\n                self.temporal_adjustments[action_name] = {\n                    'min_duration': 3,\n                    'smoothing_window': 9,\n                    'consistency_weight': 0.9\n                }\n            else:\n                self.temporal_adjustments[action_name] = {\n                    'min_duration': 2,\n                    'smoothing_window': 5,\n                    'consistency_weight': 0.7\n                }\n    \n    def get_expert_predictions(self, X, action_name, base_predictions):\n        \"\"\"Get weighted expert predictions with voting\"\"\"\n        if action_name not in self.experts:\n            return base_predictions, {}\n        \n        expert_outputs = {}\n        expert_predictions = []\n        expert_weights = []\n        \n        # Get predictions from each expert\n        for expert_name, expert_model in self.experts[action_name]:\n            try:\n                pred = expert_model.predict_proba(X)[:, 1]\n                expert_predictions.append(pred)\n                expert_weights.append(self.expert_weights[action_name].get(expert_name, 0.05))\n                expert_outputs[expert_name] = pred\n            except Exception:\n                continue\n        \n        if not expert_predictions:\n            return base_predictions, {}\n        \n        # Weighted ensemble of expert predictions\n        expert_weights = np.array(expert_weights)\n        expert_weights = expert_weights / np.sum(expert_weights)\n        expert_ensemble = np.average(expert_predictions, axis=0, weights=expert_weights)\n        \n        # Determine combination strategy based on behavior pattern\n        pattern = self.behavior_patterns[action_name]\n        \n        if pattern['is_rare']:\n            # For rare behaviors, experts have more influence\n            alpha = 0.6  # Base weight\n            combined = alpha * base_predictions + (1 - alpha) * expert_ensemble\n            \n            # Additional boost for high-confidence expert predictions\n            high_conf_mask = expert_ensemble > 0.7\n            combined[high_conf_mask] = expert_ensemble[high_conf_mask] * 0.8 + base_predictions[high_conf_mask] * 0.2\n            \n        elif pattern['is_common']:\n            # For common behaviors, base models are more reliable\n            alpha = 0.85  # Base weight\n            combined = alpha * base_predictions + (1 - alpha) * expert_ensemble\n            \n        else:\n            # Balanced combination for medium-frequency behaviors\n            alpha = 0.75  # Base weight\n            combined = alpha * base_predictions + (1 - alpha) * expert_ensemble\n            \n            # Adjust for disagreement between base and experts\n            disagreement = np.abs(base_predictions - expert_ensemble)\n            high_disagreement = disagreement > 0.3\n            \n            # When there's high disagreement, use weighted voting\n            if np.any(high_disagreement):\n                # Count expert votes\n                expert_votes = np.mean([pred > 0.5 for pred in expert_predictions], axis=0)\n                \n                # If majority of experts agree and disagree with base\n                strong_expert_agreement = expert_votes > 0.7\n                combined[high_disagreement & strong_expert_agreement] = expert_ensemble[high_disagreement & strong_expert_agreement]\n        \n        return combined, expert_outputs\n    \n    def apply_temporal_adjustments(self, predictions, action_name):\n        \"\"\"Apply temporal consistency adjustments\"\"\"\n        if action_name not in self.temporal_adjustments:\n            return predictions\n        \n        params = self.temporal_adjustments[action_name]\n        \n        # Apply smoothing\n        window = params['smoothing_window']\n        predictions_smooth = pd.Series(predictions).rolling(\n            window=window, min_periods=1, center=True\n        ).mean().values\n        \n        # Weighted combination with original\n        weight = params['consistency_weight']\n        predictions = weight * predictions_smooth + (1 - weight) * predictions\n        \n        return predictions\n    \n    def post_process_predictions(self, pred_df, meta, expert_predictions_dict):\n        \"\"\"Apply post-processing adjustments to predictions\"\"\"\n        pred_processed = pred_df.copy()\n        \n        for action in pred_df.columns:\n            if action in self.behavior_patterns:\n                pattern = self.behavior_patterns[action]\n                \n                # Apply temporal adjustments\n                pred_processed[action] = self.apply_temporal_adjustments(\n                    pred_processed[action].values, action\n                )\n                \n                # Apply confidence threshold adjustments\n                threshold = pattern['confidence_threshold']\n                \n                # Boost predictions near threshold if experts agree\n                if action in expert_predictions_dict:\n                    expert_preds = expert_predictions_dict[action]\n                    if expert_preds:\n                        expert_mean = np.mean(list(expert_preds.values()), axis=0)\n                        \n                        # Find predictions near threshold\n                        near_threshold = (pred_processed[action] > threshold * 0.8) & (pred_processed[action] < threshold * 1.2)\n                        \n                        # Boost if experts strongly agree\n                        expert_confident = expert_mean > 0.6\n                        boost_mask = near_threshold & expert_confident\n                        pred_processed.loc[boost_mask, action] *= 1.15\n                        \n                        # Suppress if experts strongly disagree\n                        expert_disagree = expert_mean < 0.2\n                        suppress_mask = near_threshold & expert_disagree\n                        pred_processed.loc[suppress_mask, action] *= 0.85\n        \n        return pred_processed\n\n# Initialize global expert system\nexpert_system = ValidatedExpertSystem()\n\n# ==================== PERFORMANCE TRACKING ====================\n\nclass PerformanceTracker:\n    \"\"\"Track model and expert performance\"\"\"\n    def __init__(self):\n        self.behavior_stats = defaultdict(lambda: {\n            'positive_samples': 0,\n            'total_samples': 0,\n            'predictions_made': 0,\n            'experts_used': 0,\n            'positive_rate': 0.0,\n            'base_weight': 0.0,\n            'expert_weight': 0.0\n        })\n    \n    def record_training(self, behavior, y_action, experts_trained=0):\n        \"\"\"Record training statistics\"\"\"\n        n_pos = int(np.sum(y_action))\n        n_total = len(y_action)\n        rate = n_pos / max(n_total, 1)\n        \n        self.behavior_stats[behavior]['positive_samples'] = n_pos\n        self.behavior_stats[behavior]['total_samples'] = n_total\n        self.behavior_stats[behavior]['positive_rate'] = rate\n        self.behavior_stats[behavior]['experts_used'] = experts_trained\n        \n        # Determine weights\n        if rate < 0.05 and experts_trained > 0:\n            self.behavior_stats[behavior]['base_weight'] = 0.6\n            self.behavior_stats[behavior]['expert_weight'] = 0.4\n        elif rate < 0.1 and experts_trained > 0:\n            self.behavior_stats[behavior]['base_weight'] = 0.75\n            self.behavior_stats[behavior]['expert_weight'] = 0.25\n        else:\n            self.behavior_stats[behavior]['base_weight'] = 0.85\n            self.behavior_stats[behavior]['expert_weight'] = 0.15\n    \n    def record_predictions(self, behavior, n_predictions):\n        \"\"\"Record prediction statistics\"\"\"\n        self.behavior_stats[behavior]['predictions_made'] += n_predictions\n    \n    def print_summary(self):\n        \"\"\"Print performance summary\"\"\"\n        print(\"\\n\" + \"=\"*90)\n        print(\" \"*30 + \"PERFORMANCE SUMMARY\")\n        print(\"=\"*90)\n        \n        total_behaviors = len(self.behavior_stats)\n        total_predictions = sum(s['predictions_made'] for s in self.behavior_stats.values())\n        behaviors_with_experts = sum(1 for s in self.behavior_stats.values() if s['experts_used'] > 0)\n        \n        print(f\"Total Behaviors: {total_behaviors}\")\n        print(f\"Total Predictions: {total_predictions}\")\n        print(f\"Behaviors with Experts: {behaviors_with_experts}\")\n        \n        print(\"\\n\" + \"-\"*35 + \"TOP BEHAVIORS\" + \"-\"*35)\n        sorted_behaviors = sorted(\n            self.behavior_stats.items(),\n            key=lambda x: x[1]['predictions_made'],\n            reverse=True\n        )[:10]\n        \n        for behavior, stats in sorted_behaviors:\n            experts_str = f\"({stats['experts_used']} experts)\" if stats['experts_used'] > 0 else \"\"\n            print(f\"  {behavior:20} {stats['predictions_made']:8,} predictions {experts_str}\")\n        \n        print(\"=\"*90)\n\n# Initialize tracker\nperformance_tracker = PerformanceTracker()\n\n# ==================== BASE CLASSIFIER ====================\n\nclass StratifiedSubsetClassifier(ClassifierMixin, BaseEstimator):\n    def __init__(self, estimator, n_samples=None):\n        self.estimator = estimator\n        self.n_samples = n_samples\n\n    def _to_numpy(self, X):\n        try:\n            return X.to_numpy(np.float32, copy=False)\n        except AttributeError:\n            return np.asarray(X, dtype=np.float32)\n\n    def fit(self, X, y):\n        Xn = self._to_numpy(X)\n        y = np.asarray(y).ravel()\n\n        uniq = np.unique(y[~pd.isna(y)])\n        if set(uniq.tolist()) == {0, 2}:\n            y = (y > 0).astype(np.int8)\n\n        if self.n_samples is None or len(Xn) <= int(self.n_samples):\n            self.estimator.fit(Xn, y)\n        else:\n            from sklearn.model_selection import StratifiedShuffleSplit\n            sss = StratifiedShuffleSplit(n_splits=1, train_size=int(self.n_samples), random_state=42)\n            try:\n                idx, _ = next(sss.split(np.zeros_like(y), y))\n                self.estimator.fit(Xn[idx], y[idx])\n            except Exception:\n                step = max(len(Xn) // int(self.n_samples), 1)\n                self.estimator.fit(Xn[::step], y[::step])\n\n        try:\n            self.classes_ = np.asarray(self.estimator.classes_)\n        except Exception:\n            self.classes_ = np.unique(y)\n        return self\n\n    def predict_proba(self, X):\n        Xn = self._to_numpy(X)\n        try:\n            P = self.estimator.predict_proba(Xn)\n        except Exception:\n            if len(self.classes_) == 1:\n                n = len(Xn)\n                c = int(self.classes_[0])\n                if c == 1:\n                    return np.column_stack([np.zeros(n, dtype=np.float32), np.ones(n, dtype=np.float32)])\n                else:\n                    return np.column_stack([np.ones(n, dtype=np.float32), np.zeros(n, dtype=np.float32)])\n            return np.full((len(Xn), 2), 0.5, dtype=np.float32)\n\n        P = np.asarray(P)\n        if P.ndim == 1:\n            P1 = P.astype(np.float32)\n            return np.column_stack([1.0 - P1, P1])\n        if P.shape[1] == 1 and len(self.classes_) == 2:\n            P1 = P[:, 0].astype(np.float32)\n            return np.column_stack([1.0 - P1, P1])\n        return P\n\n    def predict(self, X):\n        Xn = self._to_numpy(X)\n        try:\n            return self.estimator.predict(Xn)\n        except Exception:\n            return np.argmax(self.predict_proba(Xn), axis=1)\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\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(\n            [\n                pl.col('video_id').cast(pl.Utf8),\n                pl.col('agent_id').cast(pl.Utf8),\n                pl.col('target_id').cast(pl.Utf8),\n                pl.col('action'),\n            ],\n            separator='_',\n        ).alias('label_key'),\n    )\n    submission = submission.with_columns(\n        pl.concat_str(\n            [\n                pl.col('video_id').cast(pl.Utf8),\n                pl.col('agent_id').cast(pl.Utf8),\n                pl.col('target_id').cast(pl.Utf8),\n                pl.col('action'),\n            ],\n            separator='_',\n        ).alias('prediction_key'),\n    )\n\n    lab_scores = []\n    for lab in solution['lab_id'].unique():\n        lab_solution = solution.filter(pl.col('lab_id') == lab).clone()\n        lab_videos = set(lab_solution['video_id'].unique())\n        lab_submission = submission.filter(pl.col('video_id').is_in(lab_videos)).clone()\n        lab_scores.append(single_lab_f1(lab_solution, lab_submission, beta=beta))\n\n    return sum(lab_scores) / len(lab_scores)\n\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)\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', 'headpiece_bottomfrontright', \n                   'headpiece_topbackleft', 'headpiece_topbackright', 'headpiece_topfrontleft', 'headpiece_topfrontright', \n                   'spine_1', 'spine_2', 'tail_middle_1', 'tail_middle_2', 'tail_midpoint']\n\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/mice-behavior-detection/{traintest}_tracking\"\n    for _, row in dataset.iterrows():\n        \n        lab_id = row.lab_id\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, traintest, len(vid), 'frames')\n        else:\n            if verbose and traintest == 'test': print('video with all values', video_id, traintest, len(vid), 'frames')\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                    assert len(single_mouse) == len(pvid)\n                    single_mouse_meta = pd.DataFrame({\n                        'video_id': video_id,\n                        'agent_id': mouse_id_str,\n                        'target_id': 'self',\n                        'video_frame': single_mouse.index,\n                        'frames_per_second': row.frames_per_second\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                    assert len(mouse_pair) == len(pvid)\n                    mouse_pair_meta = pd.DataFrame({\n                        'video_id': video_id,\n                        'agent_id': agent_str,\n                        'target_id': target_str,\n                        'video_frame': mouse_pair.index,\n                        'frames_per_second': row.frames_per_second\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 WITH EXPERT INTEGRATION ====================\n\naction_thresholds = defaultdict(lambda: 0.27)  # Default threshold\n\ndef predict_multiclass_adaptive_enhanced(pred, meta, action_thresholds, expert_predictions_dict=None):\n    \"\"\"Enhanced adaptive thresholding with expert adjustments\"\"\"\n    # Apply initial temporal smoothing\n    pred_smoothed = pred.rolling(window=5, min_periods=1, center=True).mean()\n    \n    # Apply expert post-processing if available\n    if expert_predictions_dict is not None:\n        pred_smoothed = expert_system.post_process_predictions(\n            pred_smoothed, meta, expert_predictions_dict\n        )\n    \n    ama = np.argmax(pred_smoothed, axis=1)\n    \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        \n        # Get action-specific threshold\n        threshold = action_thresholds.get(action, 0.27)\n        \n        # Adjust threshold based on behavior pattern if available\n        if action in expert_system.behavior_patterns:\n            pattern = expert_system.behavior_patterns[action]\n            if pattern['is_rare']:\n                threshold *= 0.9  # Lower threshold for rare behaviors\n            \n            # Use expert confidence threshold if available\n            if 'confidence_threshold' in pattern:\n                threshold = pattern['confidence_threshold']\n        \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    \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        agent_id = submission_part.agent_id.iloc[i]\n        target_id = submission_part.target_id.iloc[i]\n        if i < len(stop_video_id):\n            if stop_video_id[i] != video_id or stop_agent_id[i] != agent_id or stop_target_id[i] != target_id:\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    # Apply minimum duration filters based on behavior patterns\n    filtered_parts = []\n    for _, row in submission_part.iterrows():\n        action = row['action']\n        duration = row['stop_frame'] - row['start_frame']\n        \n        # Get minimum duration for this action\n        min_duration = 3  # Default\n        if action in expert_system.temporal_adjustments:\n            min_duration = expert_system.temporal_adjustments[action]['min_duration']\n        \n        if duration >= min_duration:\n            filtered_parts.append(row)\n    \n    submission_part = pd.DataFrame(filtered_parts) if filtered_parts else pd.DataFrame()\n    \n    if len(submission_part) > 0:\n        assert (submission_part.stop_frame > submission_part.start_frame).all(), 'stop <= start'\n    \n    # Track predictions\n    for action in submission_part['action'].unique() if len(submission_part) > 0 else []:\n        performance_tracker.record_predictions(\n            action, len(submission_part[submission_part['action'] == action])\n        )\n    \n    if verbose: print(f'  actions found: {len(submission_part)}')\n    return submission_part\n\n# ==================== FEATURE ENGINEERING ====================\n\ndef safe_rolling(series, window, func, min_periods=None):\n    \"\"\"Safe rolling operation with NaN handling\"\"\"\n    if min_periods is None:\n        min_periods = max(1, window // 4)\n    return series.rolling(window, min_periods=min_periods, center=True).apply(func, raw=True)\n\ndef _scale(n_frames_at_30fps, fps, ref=30.0):\n    \"\"\"Scale a frame count defined at 30 fps to the current video's fps.\"\"\"\n    return max(1, int(round(n_frames_at_30fps * float(fps) / ref)))\n\ndef _scale_signed(n_frames_at_30fps, fps, ref=30.0):\n    \"\"\"Signed version of _scale for forward/backward shifts\"\"\"\n    if n_frames_at_30fps == 0:\n        return 0\n    s = 1 if n_frames_at_30fps > 0 else -1\n    mag = max(1, int(round(abs(n_frames_at_30fps) * float(fps) / ref)))\n    return s * mag\n\ndef _fps_from_meta(meta_df, fallback_lookup, default_fps=30.0):\n    if 'frames_per_second' in meta_df.columns and pd.notnull(meta_df['frames_per_second']).any():\n        return float(meta_df['frames_per_second'].iloc[0])\n    vid = meta_df['video_id'].iloc[0]\n    return float(fallback_lookup.get(vid, default_fps))\n\ndef add_curvature_features(X, center_x, center_y, fps):\n    \"\"\"Trajectory curvature (window lengths scaled by fps).\"\"\"\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 w in [30, 60]:\n        ws = _scale(w, fps)\n        X[f'curv_mean_{w}'] = curvature.rolling(ws, min_periods=max(1, ws // 6)).mean()\n\n    angle = np.arctan2(vel_y, vel_x)\n    angle_change = np.abs(angle.diff())\n    w = 30\n    ws = _scale(w, fps)\n    X[f'turn_rate_{w}'] = angle_change.rolling(ws, min_periods=max(1, ws // 6)).sum()\n\n    return X\n\ndef add_multiscale_features(X, center_x, center_y, fps):\n    \"\"\"Multi-scale temporal features\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2) * float(fps)\n\n    scales = [10, 40, 160]\n    for scale in scales:\n        ws = _scale(scale, fps)\n        if len(speed) >= ws:\n            X[f'sp_m{scale}'] = speed.rolling(ws, min_periods=max(1, ws // 4)).mean()\n            X[f'sp_s{scale}'] = speed.rolling(ws, min_periods=max(1, ws // 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, fps):\n    \"\"\"Behavioral state transitions\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2) * float(fps)\n    w_ma = _scale(15, fps)\n    speed_ma = speed.rolling(w_ma, min_periods=max(1, w_ma // 3)).mean()\n\n    try:\n        bins = [-np.inf, 0.5 * fps, 2.0 * fps, 5.0 * fps, np.inf]\n        speed_states = pd.cut(speed_ma, bins=bins, labels=[0, 1, 2, 3]).astype(float)\n\n        for window in [60, 120]:\n            ws = _scale(window, fps)\n            if len(speed_states) >= ws:\n                for state in [0, 1, 2, 3]:\n                    X[f's{state}_{window}'] = (\n                        (speed_states == state).astype(float)\n                        .rolling(ws, min_periods=max(1, ws // 6)).mean()\n                    )\n                state_changes = (speed_states != speed_states.shift(1)).astype(float)\n                X[f'trans_{window}'] = state_changes.rolling(ws, min_periods=max(1, ws // 6)).sum()\n    except Exception:\n        pass\n\n    return X\n\ndef add_longrange_features(X, center_x, center_y, fps):\n    \"\"\"Long-range temporal features\"\"\"\n    for window in [120, 240]:\n        ws = _scale(window, fps)\n        if len(center_x) >= ws:\n            X[f'x_ml{window}'] = center_x.rolling(ws, min_periods=max(5, ws // 6)).mean()\n            X[f'y_ml{window}'] = center_y.rolling(ws, min_periods=max(5, ws // 6)).mean()\n\n    for span in [60, 120]:\n        s = _scale(span, fps)\n        X[f'x_e{span}'] = center_x.ewm(span=s, min_periods=1).mean()\n        X[f'y_e{span}'] = center_y.ewm(span=s, min_periods=1).mean()\n\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2) * float(fps)\n    for window in [60, 120]:\n        ws = _scale(window, fps)\n        if len(speed) >= ws:\n            X[f'sp_pct{window}'] = speed.rolling(ws, min_periods=max(5, ws // 6)).rank(pct=True)\n\n    return X\n\ndef add_interaction_features(X, mouse_pair, avail_A, avail_B, fps):\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        ws = _scale(window, fps)\n        X[f'A_ld{window}'] = A_lead.rolling(ws, min_periods=max(1, ws // 6)).mean()\n        X[f'B_ld{window}'] = B_lead.rolling(ws, min_periods=max(1, ws // 6)).mean()\n\n    approach = -rel_dist.diff()\n    chase = approach * B_lead\n    w = 30\n    ws = _scale(w, fps)\n    X[f'chase_{w}'] = chase.rolling(ws, min_periods=max(1, ws // 6)).mean()\n\n    for window in [60, 120]:\n        ws = _scale(window, fps)\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(ws, min_periods=max(1, ws // 6)).corr(B_sp)\n\n    return X\n\ndef transform_single(single_mouse, body_parts_tracked, fps):\n    \"\"\"Enhanced single mouse transform\"\"\"\n    available_body_parts = single_mouse.columns.get_level_values(0)\n\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    if all(p in single_mouse.columns for p in ['ear_left', 'ear_right', 'tail_base']):\n        lag = _scale(10, fps)\n        shifted = single_mouse[['ear_left', 'ear_right', 'tail_base']].shift(lag)\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    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    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            ws = _scale(w, fps)\n            roll = dict(min_periods=1, center=True)\n            X[f'cx_m{w}'] = cx.rolling(ws, **roll).mean()\n            X[f'cy_m{w}'] = cy.rolling(ws, **roll).mean()\n            X[f'cx_s{w}'] = cx.rolling(ws, **roll).std()\n            X[f'cy_s{w}'] = cy.rolling(ws, **roll).std()\n            X[f'x_rng{w}'] = cx.rolling(ws, **roll).max() - cx.rolling(ws, **roll).min()\n            X[f'y_rng{w}'] = cy.rolling(ws, **roll).max() - cy.rolling(ws, **roll).min()\n            X[f'disp{w}'] = np.sqrt(cx.diff().rolling(ws, min_periods=1).sum()**2 +\n                                     cy.diff().rolling(ws, min_periods=1).sum()**2)\n            X[f'act{w}'] = np.sqrt(cx.diff().rolling(ws, min_periods=1).var() +\n                                   cy.diff().rolling(ws, min_periods=1).var())\n\n        X = add_curvature_features(X, cx, cy, fps)\n        X = add_multiscale_features(X, cx, cy, fps)\n        X = add_state_features(X, cx, cy, fps)\n        X = add_longrange_features(X, cx, cy, fps)\n\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            l = _scale(lag, fps)\n            X[f'nt_lg{lag}'] = nt_dist.shift(l)\n            X[f'nt_df{lag}'] = nt_dist - nt_dist.shift(l)\n\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            o = _scale_signed(off, fps)\n            X[f'ear_o{off}'] = ear_d.shift(-o)\n        w = _scale(30, fps)\n        X['ear_con'] = ear_d.rolling(w, min_periods=1, center=True).std() / \\\n                       (ear_d.rolling(w, min_periods=1, center=True).mean() + 1e-6)\n\n    return X.astype(np.float32, copy=False)\n\ndef transform_pair(mouse_pair, body_parts_tracked, fps):\n    \"\"\"Enhanced pair transform\"\"\"\n    avail_A = mouse_pair['A'].columns.get_level_values(0)\n    avail_B = mouse_pair['B'].columns.get_level_values(0)\n\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    if ('A', 'ear_left') in mouse_pair.columns and ('B', 'ear_left') in mouse_pair.columns:\n        lag = _scale(10, fps)\n        shA = mouse_pair['A']['ear_left'].shift(lag)\n        shB = mouse_pair['B']['ear_left'].shift(lag)\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    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    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        lag = _scale(10, fps)\n        shA_n = mouse_pair['A']['nose'].shift(lag)\n        shB_n = mouse_pair['B']['nose'].shift(lag)\n        past = np.square(shA_n - shB_n).sum(axis=1, skipna=False)\n        X['appr'] = cur - past\n\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    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            ws = _scale(w, fps)\n            roll = dict(min_periods=1, center=True)\n            X[f'd_m{w}']  = cd_full.rolling(ws, **roll).mean()\n            X[f'd_s{w}']  = cd_full.rolling(ws, **roll).std()\n            X[f'd_mn{w}'] = cd_full.rolling(ws, **roll).min()\n            X[f'd_mx{w}'] = cd_full.rolling(ws, **roll).max()\n\n            d_var = cd_full.rolling(ws, **roll).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(ws, **roll).mean()\n            X[f'co_s{w}'] = coord.rolling(ws, **roll).std()\n\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            l = _scale(lag, fps)\n            X[f'nn_lg{lag}']  = nn.shift(l)\n            X[f'nn_ch{lag}']  = nn - nn.shift(l)\n            is_cl = (nn < 10.0).astype(float)\n            X[f'cl_ps{lag}']  = is_cl.rolling(l, min_periods=1).mean()\n\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            o = _scale_signed(off, fps)\n            X[f'va_{off}'] = val.shift(-o)\n\n        w = _scale(30, fps)\n        X['int_con'] = cd_full.rolling(w, min_periods=1, center=True).std() / \\\n                       (cd_full.rolling(w, min_periods=1, center=True).mean() + 1e-6)\n\n        X = add_interaction_features(X, mouse_pair, avail_A, avail_B, fps)\n\n    return X.astype(np.float32, copy=False)\n\n# ==================== ENSEMBLE TRAINING WITH VALIDATED EXPERTS ====================\n\ndef submit_ensemble_validated(body_parts_tracked_str, switch_tr, X_tr, label, meta):\n    \"\"\"Ensemble training with validated expert system\"\"\"\n    models = []\n\n    models.append(make_pipeline(\n        StratifiedSubsetClassifier(\n            lightgbm.LGBMClassifier(\n                n_estimators=225, learning_rate=0.07, min_child_samples=40,\n                num_leaves=31, subsample=0.8, colsample_bytree=0.8, verbose=-1,\n                random_state=SEED, bagging_seed=SEED, feature_fraction_seed=SEED, data_random_seed=SEED\n            ), 500_000,\n        )\n    ))\n    models.append(make_pipeline(\n        StratifiedSubsetClassifier(\n            lightgbm.LGBMClassifier(\n                n_estimators=150, learning_rate=0.1, min_child_samples=20,\n                num_leaves=63, max_depth=8, subsample=0.7, colsample_bytree=0.9,\n                reg_alpha=0.1, reg_lambda=0.1, verbose=-1,\n                random_state=SEED, bagging_seed=SEED, feature_fraction_seed=SEED, data_random_seed=SEED\n            ), 450_000,\n        )\n    ))\n    models.append(make_pipeline(\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                random_state=SEED, bagging_seed=SEED, feature_fraction_seed=SEED, data_random_seed=SEED\n            ), 400_000,\n        )\n    ))\n    if XGBOOST_AVAILABLE:\n        models.append(make_pipeline(\n            StratifiedSubsetClassifier(\n                XGBClassifier(\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                    random_state=SEED\n                ), 500_000,\n            )\n        ))\n    if CATBOOST_AVAILABLE:\n        models.append(make_pipeline(\n            StratifiedSubsetClassifier(\n                CatBoostClassifier(\n                    iterations=120, learning_rate=0.1, depth=6,\n                    verbose=False, allow_writing_files=False,\n                    random_seed=SEED\n                ), 500_000,\n            )\n        ))\n\n    X_tr_np = X_tr.to_numpy(np.float32, copy=False)\n    del X_tr; gc.collect()\n\n    model_list = []\n    \n    for action in label.columns:\n        y_raw = label[action].to_numpy()\n        mask = ~pd.isna(y_raw)\n        y_action = y_raw[mask].astype(int)\n        \n        if not (y_action == 0).all() and np.sum(y_action) >= 5:\n            # Train base models\n            trained = []\n            idx = np.flatnonzero(mask)\n            for m in models:\n                m_clone = clone(m)\n                m_clone.fit(X_tr_np[idx], y_action)\n                trained.append(m_clone)\n            model_list.append((action, trained))\n            \n            # Train experts for this action\n            expert_system.train_experts(X_tr_np[idx], y_action, action)\n            \n            # Track training\n            n_experts = len(expert_system.experts.get(action, []))\n            performance_tracker.record_training(action, y_action, n_experts)\n\n    del X_tr_np; 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(\n        test_subset, 'test',\n        generate_single=(switch_tr == 'single'),\n        generate_pair=(switch_tr == 'pair')\n    )\n\n    fps_lookup = (\n        test_subset[['video_id', 'frames_per_second']]\n        .drop_duplicates('video_id')\n        .set_index('video_id')['frames_per_second']\n        .to_dict()\n    )\n\n    if verbose:\n        n_experts_total = sum(len(v) for v in expert_system.experts.values())\n        print(f\"n_videos: {len(test_subset)}, n_models: {len(models)}, n_experts: {n_experts_total}\")\n\n    for switch_te, data_te, meta_te, actions_te in generator:\n        assert switch_te == switch_tr\n        try:\n            fps_i = _fps_from_meta(meta_te, fps_lookup, default_fps=30.0)\n\n            if switch_te == 'single':\n                X_te = transform_single(data_te, body_parts_tracked, fps_i).astype(np.float32)\n            else:\n                X_te = transform_pair(data_te, body_parts_tracked, fps_i).astype(np.float32)\n\n            X_te_np = X_te.to_numpy(np.float32, copy=False)\n            del X_te, data_te; gc.collect()\n\n            pred = pd.DataFrame(index=meta_te.video_frame)\n            expert_predictions_dict = {}\n            \n            for action, trained in model_list:\n                if action in actions_te:\n                    # Get base ensemble predictions\n                    probs = [m.predict_proba(X_te_np)[:, 1] for m in trained]\n                    base_pred = np.mean(probs, axis=0)\n                    \n                    # Get expert predictions and combine\n                    combined_pred, expert_outputs = expert_system.get_expert_predictions(\n                        X_te_np, action, base_pred\n                    )\n                    \n                    pred[action] = combined_pred\n                    \n                    if expert_outputs:\n                        expert_predictions_dict[action] = expert_outputs\n\n            del X_te_np; gc.collect()\n\n            if pred.shape[1] != 0:\n                sub_part = predict_multiclass_adaptive_enhanced(\n                    pred, meta_te, action_thresholds, expert_predictions_dict\n                )\n                submission_list.append(sub_part)\n            else:\n                if verbose:\n                    print(\"  ERROR: no training data\")\n\n        except Exception as e:\n            if verbose:\n                print(f\"  ERROR: {str(e)[:50]}\")\n            try:\n                del data_te\n            except Exception:\n                pass\n            gc.collect()\n\ndef robustify(submission, dataset, traintest, traintest_directory=None):\n    if traintest_directory is None:\n        traintest_directory = f\"/kaggle/input/mice-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 _, row in dataset.iterrows():\n        lab_id = row['lab_id']\n        video_id = row['video_id']\n        if (submission.video_id == video_id).any():\n            continue\n\n        if verbose:\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(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 = []\n\nprint(f\"XGBoost: {XGBOOST_AVAILABLE}, CatBoost: {CATBOOST_AVAILABLE}\")\nprint(\"Enhanced Architecture: Ensemble with Validated Expert System\\n\")\n\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\n        _fps_lookup = (\n            train_subset[['video_id', 'frames_per_second']]\n            .drop_duplicates('video_id')\n            .set_index('video_id')['frames_per_second']\n            .to_dict()\n        )\n\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_feats_parts = []\n            for data_i, meta_i in zip(single_list, single_meta_list):\n                fps_i = _fps_from_meta(meta_i, _fps_lookup, default_fps=30.0)\n                Xi = transform_single(data_i, body_parts_tracked, fps_i).astype(np.float32)\n                single_feats_parts.append(Xi)\n\n            X_tr = pd.concat(single_feats_parts, axis=0, ignore_index=True)\n            single_label = pd.concat(single_label_list, axis=0, ignore_index=True)\n            single_meta = pd.concat(single_meta_list, axis=0, ignore_index=True)\n\n            del single_list, single_label_list, single_meta_list, single_feats_parts\n            gc.collect()\n\n            print(f\"  Single: {X_tr.shape}\")\n            submit_ensemble_validated(body_parts_tracked_str, 'single', X_tr, single_label, single_meta)\n\n            del X_tr, single_label, single_meta\n            gc.collect()\n\n        if len(pair_list) > 0:\n            pair_feats_parts = []\n            for data_i, meta_i in zip(pair_list, pair_meta_list):\n                fps_i = _fps_from_meta(meta_i, _fps_lookup, default_fps=30.0)\n                Xi = transform_pair(data_i, body_parts_tracked, fps_i).astype(np.float32)\n                pair_feats_parts.append(Xi)\n\n            X_tr = pd.concat(pair_feats_parts, axis=0, ignore_index=True)\n            pair_label = pd.concat(pair_label_list, axis=0, ignore_index=True)\n            pair_meta = pd.concat(pair_meta_list, axis=0, ignore_index=True)\n\n            del pair_list, pair_label_list, pair_meta_list, pair_feats_parts\n            gc.collect()\n\n            print(f\"  Pair: {X_tr.shape}\")\n            submit_ensemble_validated(body_parts_tracked_str, 'pair', X_tr, pair_label, pair_meta)\n\n            del X_tr, pair_label, pair_meta\n            gc.collect()\n\n    except Exception as e:\n        print(f'***Exception*** {str(e)[:100]}')\n\n    gc.collect()\n    print()\n\nif len(submission_list) > 0:\n    submission = pd.concat(submission_list, ignore_index=True)\nelse:\n    submission = pd.DataFrame({\n        'video_id': [438887472],\n        'agent_id': ['mouse1'],\n        'target_id': ['self'],\n        'action': ['rear'],\n        'start_frame': [278],\n        '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\n# Print performance summary\nperformance_tracker.print_summary()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}