{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":59156,"databundleVersionId":13719397,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\"\"\"\nMABe Challenge 2025 - Starter Code with CV and Competition Metric\nMulti-Agent Behavior Recognition in Mice\n\nKey features:\n1. Proper cross-validation strategy\n2. Competition F-beta metric implementation\n3. Local CV score tracking for LB correlation\n4. Stratified sampling to ensure behavior diversity\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nfrom pathlib import Path\nimport warnings\nimport gc\nimport json\nfrom typing import Dict, List, Optional, Tuple, Any\nfrom dataclasses import dataclass, field\nfrom tqdm import tqdm\nimport pickle\nimport random\nfrom scipy import signal, stats\nfrom collections import defaultdict\nfrom sklearn.model_selection import StratifiedKFold, GroupKFold, train_test_split\nfrom sklearn.preprocessing import StandardScaler, RobustScaler\nfrom sklearn.metrics import f1_score\nimport lightgbm as lgb\nimport xgboost as xgb\n\nwarnings.filterwarnings('ignore')\n\n# ============================================================================\n# CONFIGURATION\n# ============================================================================\n\n@dataclass\nclass Config:\n    \"\"\"Configuration with CV strategy\"\"\"\n    # Paths\n    data_path: Path = Path('/kaggle/input/MABe-mouse-behavior-detection')\n    output_path: Path = Path('/kaggle/working')\n    \n    # Data processing\n    max_train_videos: int = 100  # Balanced for accuracy vs speed\n    max_test_videos: Optional[int] = None\n    sample_rate: float = 0.5  # Sample 50% of windows\n    \n    # Feature extraction\n    window_size: int = 45\n    window_stride: int = 15  # Balance between coverage and speed\n    min_window_fill: float = 0.4\n    use_advanced_features: bool = True\n    \n    # Cross-validation strategy\n    cv_strategy: str = 'group'  # 'group' for video-based, 'stratified' for behavior-based\n    n_folds: int = 5  # 5-fold CV for robust validation\n    validation_metric: str = 'f_beta'  # Use competition metric\n    beta: float = 1.0  # F1 score\n    \n    # Model parameters\n    use_ensemble: bool = True\n    n_models: int = 2  # LightGBM + XGBoost\n    random_state: int = 42\n    early_stopping_rounds: int = 50\n    \n    # Behavior classes\n    behavior_classes: List[str] = field(default_factory=lambda: [\n        'grooming', 'sniff', 'chase', 'attack', \n        'mount', 'investigate', 'escape', 'approach'\n    ])\n    \n    # Thresholds (will be optimized during CV)\n    confidence_thresholds: Dict[str, float] = field(default_factory=lambda: {\n        'grooming': 0.3,\n        'sniff': 0.25,\n        'chase': 0.35,\n        'attack': 0.4,\n        'mount': 0.35,\n        'investigate': 0.25,\n        'escape': 0.35,\n        'approach': 0.3\n    })\n    \n    # Processing\n    batch_size: int = 100\n    max_features: int = 100  # Reduced to prevent overfitting\n\nconfig = Config()\n\n# ============================================================================\n# COMPETITION METRIC IMPLEMENTATION\n# ============================================================================\n\nclass MABeMetric:\n    \"\"\"Competition F-beta metric implementation\"\"\"\n    \n    @staticmethod\n    def prepare_solution_data(annotations: pd.DataFrame, video_id: str, lab_id: str = 'lab1') -> pd.DataFrame:\n        \"\"\"Convert annotations to solution format\"\"\"\n        if annotations.empty:\n            return pd.DataFrame()\n        \n        solution_rows = []\n        for _, ann in annotations.iterrows():\n            # Create solution row\n            row = {\n                'video_id': video_id,\n                'agent_id': ann.get('agent_id', 'mouse1'),\n                'target_id': ann.get('target_id', 'mouse2'),\n                'action': ann.get('action', 'unknown'),\n                'start_frame': int(ann['start_frame']),\n                'stop_frame': int(ann.get('end_frame', ann.get('stop_frame', ann['start_frame'] + 30))),\n                'lab_id': lab_id\n            }\n            solution_rows.append(row)\n        \n        if solution_rows:\n            solution_df = pd.DataFrame(solution_rows)\n            \n            # Add behaviors_labeled column (required by metric)\n            unique_behaviors = solution_df.apply(\n                lambda x: f\"{x['agent_id']},{x['target_id']},{x['action']}\", axis=1\n            ).unique().tolist()\n            solution_df['behaviors_labeled'] = json.dumps(unique_behaviors)\n            \n            return solution_df\n        \n        return pd.DataFrame()\n    \n    @staticmethod\n    def single_lab_f1(lab_solution: pl.DataFrame, lab_submission: pl.DataFrame, beta: float = 1) -> float:\n        \"\"\"Calculate F1 for a single lab\"\"\"\n        if lab_solution.is_empty() or lab_submission.is_empty():\n            return 0.0\n        \n        label_frames = defaultdict(set)\n        prediction_frames = defaultdict(set)\n        \n        # Build label frames\n        for row in lab_solution.to_dicts():\n            key = f\"{row['video_id']}_{row['agent_id']}_{row['target_id']}_{row['action']}\"\n            label_frames[key].update(range(row['start_frame'], row['stop_frame']))\n        \n        # Build prediction frames\n        for row in lab_submission.to_dicts():\n            key = f\"{row['video_id']}_{row['agent_id']}_{row['target_id']}_{row['action']}\"\n            prediction_frames[key].update(range(row['start_frame'], row['stop_frame']))\n        \n        # Calculate metrics per action\n        tps = defaultdict(int)\n        fns = defaultdict(int)\n        fps = defaultdict(int)\n        \n        all_keys = set(label_frames.keys()) | set(prediction_frames.keys())\n        actions = set()\n        \n        for key in all_keys:\n            action = key.split('_')[-1]\n            actions.add(action)\n            \n            label_set = label_frames.get(key, set())\n            pred_set = prediction_frames.get(key, set())\n            \n            tps[action] += len(label_set & pred_set)\n            fns[action] += len(label_set - pred_set)\n            fps[action] += len(pred_set - label_set)\n        \n        # Calculate F-beta per action\n        action_scores = []\n        for action in actions:\n            tp = tps[action]\n            fn = fns[action]\n            fp = fps[action]\n            \n            if tp + fn + fp == 0:\n                continue\n            \n            precision = tp / (tp + fp) if (tp + fp) > 0 else 0\n            recall = tp / (tp + fn) if (tp + fn) > 0 else 0\n            \n            if precision + recall > 0:\n                f_score = (1 + beta**2) * (precision * recall) / (beta**2 * precision + recall)\n            else:\n                f_score = 0\n            \n            action_scores.append(f_score)\n        \n        return np.mean(action_scores) if action_scores else 0.0\n    \n    @staticmethod\n    def calculate_metric(solution: pd.DataFrame, submission: pd.DataFrame, beta: float = 1) -> float:\n        \"\"\"Calculate competition F-beta metric\"\"\"\n        if solution.empty or submission.empty:\n            return 0.0\n        \n        # Convert to polars for efficiency\n        solution_pl = pl.DataFrame(solution)\n        submission_pl = pl.DataFrame(submission)\n        \n        # Calculate per lab\n        lab_scores = []\n        for lab_id in solution['lab_id'].unique():\n            lab_solution = solution_pl.filter(pl.col('lab_id') == lab_id)\n            lab_videos = lab_solution['video_id'].unique().to_list()\n            lab_submission = submission_pl.filter(pl.col('video_id').is_in(lab_videos))\n            \n            score = MABeMetric.single_lab_f1(lab_solution, lab_submission, beta)\n            lab_scores.append(score)\n        \n        return np.mean(lab_scores) if lab_scores else 0.0\n\n# ============================================================================\n# FEATURE EXTRACTION\n# ============================================================================\n\nclass FeatureExtractor:\n    \"\"\"Feature extraction with focus on reducing overfitting\"\"\"\n    \n    def __init__(self, window_size: int = 45, stride: int = 15):\n        self.window_size = window_size\n        self.stride = stride\n    \n    def extract_movement_features(self, trajectory: pd.DataFrame) -> Dict[str, float]:\n        \"\"\"Extract core movement features\"\"\"\n        features = {}\n        \n        if 'x' not in trajectory.columns or len(trajectory) < 3:\n            return features\n        \n        x = trajectory['x'].values\n        y = trajectory['y'].values\n        \n        # Velocity\n        dx = np.diff(x)\n        dy = np.diff(y)\n        velocity = np.sqrt(dx**2 + dy**2)\n        \n        # Core statistics\n        features['vel_mean'] = np.mean(velocity)\n        features['vel_std'] = np.std(velocity)\n        features['vel_max'] = np.max(velocity)\n        features['vel_median'] = np.median(velocity)\n        features['vel_q25'] = np.percentile(velocity, 25)\n        features['vel_q75'] = np.percentile(velocity, 75)\n        \n        # Acceleration\n        if len(velocity) > 1:\n            acceleration = np.diff(velocity)\n            features['acc_mean'] = np.mean(np.abs(acceleration))\n            features['acc_std'] = np.std(acceleration)\n        \n        # Path metrics\n        features['total_dist'] = np.sum(velocity)\n        features['net_disp'] = np.sqrt((x[-1] - x[0])**2 + (y[-1] - y[0])**2)\n        features['path_efficiency'] = features['net_disp'] / (features['total_dist'] + 1e-6)\n        \n        # Spatial\n        features['x_range'] = np.max(x) - np.min(x)\n        features['y_range'] = np.max(y) - np.min(y)\n        features['area'] = features['x_range'] * features['y_range']\n        \n        # Angular features\n        if len(dx) > 1:\n            angles = np.arctan2(dy, dx)\n            angle_changes = np.diff(angles)\n            angle_changes = np.arctan2(np.sin(angle_changes), np.cos(angle_changes))\n            \n            features['turn_mean'] = np.mean(np.abs(angle_changes))\n            features['turn_std'] = np.std(angle_changes)\n            features['turn_max'] = np.max(np.abs(angle_changes))\n        \n        # Motion patterns\n        features['stationary_ratio'] = np.mean(velocity < 2.0)\n        features['slow_ratio'] = np.mean((velocity >= 2.0) & (velocity < 10.0))\n        features['fast_ratio'] = np.mean(velocity >= 10.0)\n        \n        return features\n    \n    def extract_temporal_features(self, trajectory: pd.DataFrame) -> Dict[str, float]:\n        \"\"\"Extract temporal dynamics\"\"\"\n        features = {}\n        \n        if 'x' not in trajectory.columns or len(trajectory) < 10:\n            return features\n        \n        x = trajectory['x'].values\n        y = trajectory['y'].values\n        \n        # Autocorrelation\n        if len(x) > 15:\n            x_centered = x - np.mean(x)\n            y_centered = y - np.mean(y)\n            \n            # Lag-1 autocorrelation\n            features['x_autocorr'] = np.corrcoef(x_centered[:-1], x_centered[1:])[0, 1]\n            features['y_autocorr'] = np.corrcoef(y_centered[:-1], y_centered[1:])[0, 1]\n        \n        # Trend\n        time = np.arange(len(x))\n        x_trend = np.polyfit(time, x, 1)[0]\n        y_trend = np.polyfit(time, y, 1)[0]\n        features['x_trend'] = x_trend\n        features['y_trend'] = y_trend\n        \n        return features\n    \n    def extract_windows(self, data: pd.DataFrame, video_id: str, \n                       sample_rate: float = 1.0) -> pd.DataFrame:\n        \"\"\"Extract feature windows from tracking data\"\"\"\n        if data.empty:\n            return pd.DataFrame()\n        \n        # Get trajectory\n        if 'frame' not in data.columns:\n            data['frame'] = data.index\n        \n        trajectory = data.groupby('frame').agg({\n            'x': 'mean',\n            'y': 'mean'\n        }).reset_index()\n        \n        if len(trajectory) < self.window_size:\n            return pd.DataFrame()\n        \n        all_features = []\n        min_frame = trajectory['frame'].min()\n        max_frame = trajectory['frame'].max()\n        \n        # Generate windows\n        window_starts = list(range(int(min_frame), \n                                  int(max_frame) - self.window_size + 1, \n                                  self.stride))\n        \n        # Sample if needed\n        if sample_rate < 1.0:\n            n_samples = max(1, int(len(window_starts) * sample_rate))\n            window_starts = random.sample(window_starts, min(n_samples, len(window_starts)))\n        \n        for start_frame in window_starts:\n            end_frame = start_frame + self.window_size\n            \n            window = trajectory[(trajectory['frame'] >= start_frame) & \n                               (trajectory['frame'] < end_frame)]\n            \n            if len(window) < self.window_size * config.min_window_fill:\n                continue\n            \n            # Extract features\n            features = {\n                'video_id': video_id,\n                'start_frame': start_frame,\n                'end_frame': end_frame,\n            }\n            \n            # Movement features\n            move_feats = self.extract_movement_features(window)\n            features.update(move_feats)\n            \n            # Temporal features\n            if config.use_advanced_features:\n                temp_feats = self.extract_temporal_features(window)\n                features.update(temp_feats)\n            \n            all_features.append(features)\n        \n        return pd.DataFrame(all_features)\n\n# ============================================================================\n# CROSS-VALIDATION STRATEGY\n# ============================================================================\n\nclass CVStrategy:\n    \"\"\"Cross-validation with competition metric\"\"\"\n    \n    def __init__(self, n_folds: int = 5, strategy: str = 'group'):\n        self.n_folds = n_folds\n        self.strategy = strategy\n        self.cv_scores = []\n        self.best_thresholds = {}\n    \n    def create_folds(self, features_df: pd.DataFrame, labels: np.ndarray) -> List[Tuple]:\n        \"\"\"Create CV folds based on strategy\"\"\"\n        if self.strategy == 'group':\n            # Group by video to prevent leakage\n            groups = features_df['video_id'].values\n            gkf = GroupKFold(n_splits=self.n_folds)\n            folds = list(gkf.split(features_df, labels, groups))\n        else:\n            # Stratified by behavior presence\n            skf = StratifiedKFold(n_splits=self.n_folds, shuffle=True, random_state=42)\n            # Create binary labels for stratification\n            binary_labels = (labels > 0).astype(int) if labels.ndim > 1 else labels\n            folds = list(skf.split(features_df, binary_labels))\n        \n        return folds\n    \n    def optimize_thresholds(self, y_true: np.ndarray, y_pred: np.ndarray, \n                           features_df: pd.DataFrame) -> Dict[str, float]:\n        \"\"\"Optimize thresholds using validation data\"\"\"\n        best_thresholds = {}\n        \n        # Simple threshold optimization\n        for threshold in np.arange(0.1, 0.6, 0.05):\n            # Convert predictions to submission format\n            predictions = []\n            for idx, prob in enumerate(y_pred):\n                if prob > threshold:\n                    predictions.append({\n                        'video_id': features_df.iloc[idx]['video_id'],\n                        'agent_id': 'mouse1',\n                        'target_id': 'mouse2',\n                        'action': 'unknown',\n                        'start_frame': int(features_df.iloc[idx]['start_frame']),\n                        'stop_frame': int(features_df.iloc[idx]['end_frame'])\n                    })\n            \n            if predictions:\n                # Calculate score (simplified)\n                pred_binary = (y_pred > threshold).astype(int)\n                score = f1_score(y_true, pred_binary)\n                \n                if 'best_score' not in locals() or score > best_score:\n                    best_score = score\n                    best_threshold = threshold\n        \n        return {'global': best_threshold if 'best_threshold' in locals() else 0.3}\n\n# ============================================================================\n# MODEL WITH CV\n# ============================================================================\n\nclass BehaviorDetectorCV:\n    \"\"\"Model with cross-validation\"\"\"\n    \n    def __init__(self):\n        self.models = []\n        self.scalers = []\n        self.feature_columns = None\n        self.cv_strategy = CVStrategy(n_folds=config.n_folds, strategy=config.cv_strategy)\n        self.cv_scores = []\n        self.feature_importance = None\n    \n    def prepare_features(self, features_df: pd.DataFrame) -> np.ndarray:\n        \"\"\"Prepare features\"\"\"\n        numeric_cols = features_df.select_dtypes(include=[np.number]).columns\n        exclude = ['start_frame', 'end_frame']\n        self.feature_columns = [c for c in numeric_cols if c not in exclude]\n        \n        if not self.feature_columns:\n            return np.zeros((len(features_df), 1))\n        \n        X = features_df[self.feature_columns].fillna(0).values\n        \n        # Remove constant features\n        std = np.std(X, axis=0)\n        valid_features = std > 1e-10\n        X = X[:, valid_features]\n        self.feature_columns = [col for col, valid in zip(self.feature_columns, valid_features) if valid]\n        \n        return X\n    \n    def train_with_cv(self, features_df: pd.DataFrame, labels: np.ndarray, \n                     annotations_df: pd.DataFrame = None):\n        \"\"\"Train with cross-validation\"\"\"\n        print(\"\\n\" + \"=\"*60)\n        print(\"TRAINING WITH CROSS-VALIDATION\")\n        print(\"=\"*60)\n        \n        X = self.prepare_features(features_df)\n        \n        if X.shape[0] < 100 or X.shape[1] == 0:\n            print(\"Insufficient data for training\")\n            return\n        \n        # Binary classification for simplicity\n        y = (labels > 0).astype(int) if labels.ndim > 1 else labels\n        \n        print(f\"Data shape: {X.shape}\")\n        print(f\"Positive rate: {100*np.mean(y):.1f}%\")\n        \n        # Create CV folds\n        folds = self.cv_strategy.create_folds(features_df, y)\n        print(f\"Created {len(folds)} CV folds\")\n        \n        # Track feature importance\n        feature_importance_sum = np.zeros(X.shape[1])\n        \n        # Train on each fold\n        for fold_idx, (train_idx, val_idx) in enumerate(folds):\n            print(f\"\\nFold {fold_idx + 1}/{len(folds)}\")\n            \n            X_train, X_val = X[train_idx], X[val_idx]\n            y_train, y_val = y[train_idx], y[val_idx]\n            \n            # Check for minimum positive samples\n            if np.sum(y_train) < 10 or np.sum(y_val) < 5:\n                print(f\"  Skipping fold - insufficient positive samples\")\n                continue\n            \n            # Scale\n            scaler = RobustScaler()\n            X_train_scaled = scaler.fit_transform(X_train)\n            X_val_scaled = scaler.transform(X_val)\n            \n            # Train LightGBM\n            lgb_model = lgb.LGBMClassifier(\n                n_estimators=200,\n                max_depth=6,\n                num_leaves=40,\n                learning_rate=0.05,\n                min_child_samples=30,\n                subsample=0.7,\n                colsample_bytree=0.7,\n                reg_alpha=0.3,\n                reg_lambda=0.3,\n                class_weight='balanced' if np.mean(y_train) < 0.3 else None,\n                random_state=config.random_state + fold_idx,\n                verbosity=-1\n            )\n            \n            lgb_model.fit(\n                X_train_scaled, y_train,\n                eval_set=[(X_val_scaled, y_val)],\n                callbacks=[lgb.early_stopping(config.early_stopping_rounds), \n                          lgb.log_evaluation(0)]\n            )\n            \n            # Track feature importance\n            if hasattr(lgb_model, 'feature_importances_'):\n                feature_importance_sum += lgb_model.feature_importances_\n            \n            # Validate\n            y_pred_proba = lgb_model.predict_proba(X_val_scaled)[:, 1]\n            y_pred = (y_pred_proba > 0.3).astype(int)\n            \n            # Calculate metrics\n            val_f1 = f1_score(y_val, y_pred)\n            print(f\"  Validation F1: {val_f1:.3f}\")\n            \n            # If we have annotations, calculate competition metric\n            if annotations_df is not None:\n                val_features = features_df.iloc[val_idx]\n                val_predictions = []\n                \n                for idx, prob in enumerate(y_pred_proba):\n                    if prob > 0.3:\n                        val_predictions.append({\n                            'video_id': val_features.iloc[idx]['video_id'],\n                            'agent_id': 'mouse1',\n                            'target_id': 'mouse2',\n                            'action': 'unknown',\n                            'start_frame': int(val_features.iloc[idx]['start_frame']),\n                            'stop_frame': int(val_features.iloc[idx]['end_frame'])\n                        })\n                \n                if val_predictions:\n                    submission_df = pd.DataFrame(val_predictions)\n                    \n                    # Get corresponding annotations\n                    val_videos = val_features['video_id'].unique()\n                    val_annotations = annotations_df[annotations_df['video_id'].isin(val_videos)]\n                    \n                    if not val_annotations.empty:\n                        # Convert to solution format\n                        solution_df = MABeMetric.prepare_solution_data(\n                            val_annotations, \n                            val_videos[0] if len(val_videos) > 0 else 'unknown'\n                        )\n                        \n                        if not solution_df.empty:\n                            competition_score = MABeMetric.calculate_metric(\n                                solution_df, submission_df, beta=config.beta\n                            )\n                            print(f\"  Competition F-beta: {competition_score:.3f}\")\n                            self.cv_scores.append(competition_score)\n            \n            # Store model\n            self.models.append(lgb_model)\n            self.scalers.append(scaler)\n            \n            # Train XGBoost if ensemble enabled\n            if config.use_ensemble and config.n_models > 1:\n                xgb_model = xgb.XGBClassifier(\n                    n_estimators=150,\n                    max_depth=5,\n                    learning_rate=0.05,\n                    subsample=0.7,\n                    colsample_bytree=0.7,\n                    reg_alpha=0.3,\n                    reg_lambda=0.3,\n                    random_state=config.random_state + fold_idx + 100,\n                    use_label_encoder=False,\n                    eval_metric='logloss'\n                )\n                \n                xgb_model.fit(\n                    X_train_scaled, y_train,\n                    eval_set=[(X_val_scaled, y_val)],\n                    early_stopping_rounds=config.early_stopping_rounds,\n                    verbose=False\n                )\n                \n                self.models.append(xgb_model)\n        \n        # Store average feature importance\n        if feature_importance_sum.any():\n            self.feature_importance = feature_importance_sum / len(folds)\n            \n            # Print top features\n            top_features_idx = np.argsort(self.feature_importance)[-10:][::-1]\n            print(\"\\nTop 10 important features:\")\n            for idx in top_features_idx:\n                if idx < len(self.feature_columns):\n                    print(f\"  {self.feature_columns[idx]}: {self.feature_importance[idx]:.2f}\")\n        \n        # Print CV summary\n        if self.cv_scores:\n            print(f\"\\nCV Summary:\")\n            print(f\"  Mean F-beta: {np.mean(self.cv_scores):.3f} (+/- {np.std(self.cv_scores):.3f})\")\n            print(f\"  Min: {np.min(self.cv_scores):.3f}, Max: {np.max(self.cv_scores):.3f}\")\n    \n    def predict(self, X: np.ndarray) -> np.ndarray:\n        \"\"\"Predict using ensemble\"\"\"\n        if not self.models:\n            return np.zeros(X.shape[0])\n        \n        predictions = []\n        \n        for i, model in enumerate(self.models):\n            scaler_idx = min(i // (config.n_models if config.use_ensemble else 1), len(self.scalers) - 1)\n            X_scaled = self.scalers[scaler_idx].transform(X)\n            \n            if hasattr(model, 'predict_proba'):\n                pred = model.predict_proba(X_scaled)[:, 1]\n            else:\n                pred = model.predict(X_scaled)\n            \n            predictions.append(pred)\n        \n        # Average predictions\n        return np.mean(predictions, axis=0)\n\n# ============================================================================\n# MAIN PIPELINE WITH CV\n# ============================================================================\n\nclass MABePipelineCV:\n    \"\"\"Main pipeline with proper CV\"\"\"\n    \n    def __init__(self):\n        self.feature_extractor = FeatureExtractor(\n            window_size=config.window_size,\n            stride=config.window_stride\n        )\n        self.detector = BehaviorDetectorCV()\n        self.all_annotations = []\n    \n    def load_and_process_file(self, tracking_path: Path, annotation_path: Path = None) -> Tuple[pd.DataFrame, np.ndarray, pd.DataFrame]:\n        \"\"\"Load and process a single file\"\"\"\n        try:\n            # Load tracking\n            data = pd.read_parquet(tracking_path)\n            video_id = tracking_path.stem\n            \n            # Extract features\n            features = self.feature_extractor.extract_windows(\n                data, video_id, sample_rate=config.sample_rate\n            )\n            \n            if features.empty:\n                return pd.DataFrame(), np.array([]), pd.DataFrame()\n            \n            # Load annotations if available\n            annotations = pd.DataFrame()\n            labels = np.zeros(len(features))\n            \n            if annotation_path and annotation_path.exists():\n                annotations = pd.read_parquet(annotation_path)\n                \n                # Map column names\n                col_mapping = {\n                    'start': 'start_frame',\n                    'end': 'end_frame',\n                    'stop': 'end_frame',\n                    'stop_frame': 'end_frame'\n                }\n                \n                for old_col, new_col in col_mapping.items():\n                    if old_col in annotations.columns and new_col not in annotations.columns:\n                        annotations[new_col] = annotations[old_col]\n                \n                # Create labels\n                if 'start_frame' in annotations.columns and 'end_frame' in annotations.columns:\n                    for idx, window in features.iterrows():\n                        overlaps = ((annotations['start_frame'] <= window['end_frame']) & \n                                  (annotations['end_frame'] >= window['start_frame']))\n                        labels[idx] = 1 if overlaps.any() else 0\n                    \n                    # Add video_id to annotations\n                    annotations['video_id'] = video_id\n            \n            return features, labels, annotations\n            \n        except Exception as e:\n            print(f\"Error processing {tracking_path.name}: {e}\")\n            return pd.DataFrame(), np.array([]), pd.DataFrame()\n    \n    def process_training_data(self):\n        \"\"\"Process training data with CV in mind\"\"\"\n        print(\"\\n\" + \"=\"*60)\n        print(\"PROCESSING TRAINING DATA FOR CV\")\n        print(\"=\"*60)\n        \n        # Find files\n        tracking_files = []\n        train_path = config.data_path / 'train_tracking'\n        \n        if train_path.exists():\n            for lab_dir in train_path.iterdir():\n                if lab_dir.is_dir():\n                    files = list(lab_dir.glob('*.parquet'))\n                    tracking_files.extend(files)\n        \n        # Sample files\n        if config.max_train_videos and len(tracking_files) > config.max_train_videos:\n            # Stratified sampling by lab\n            lab_files = defaultdict(list)\n            for f in tracking_files:\n                lab_files[f.parent.name].append(f)\n            \n            sampled_files = []\n            files_per_lab = config.max_train_videos // len(lab_files)\n            \n            for lab, files in lab_files.items():\n                n_sample = min(files_per_lab, len(files))\n                sampled_files.extend(random.sample(files, n_sample))\n            \n            tracking_files = sampled_files[:config.max_train_videos]\n        \n        print(f\"Processing {len(tracking_files)} files\")\n        \n        all_features = []\n        all_labels = []\n        all_annotations = []\n        \n        # Process files\n        for i in range(0, len(tracking_files), config.batch_size):\n            batch = tracking_files[i:i+config.batch_size]\n            print(f\"\\nBatch {i//config.batch_size + 1}/{(len(tracking_files)-1)//config.batch_size + 1}\")\n            \n            for file_path in tqdm(batch, desc=\"Processing\"):\n                # Get annotation path\n                ann_path = config.data_path / 'train_annotation' / file_path.parent.name / f\"{file_path.stem}.parquet\"\n                \n                # Process file\n                features, labels, annotations = self.load_and_process_file(file_path, ann_path)\n                \n                if not features.empty and len(labels) > 0:\n                    all_features.append(features)\n                    all_labels.append(labels)\n                    if not annotations.empty:\n                        all_annotations.append(annotations)\n            \n            gc.collect()\n        \n        # Combine\n        if all_features:\n            features_df = pd.concat(all_features, ignore_index=True)\n            labels = np.concatenate(all_labels)\n            \n            annotations_df = pd.concat(all_annotations, ignore_index=True) if all_annotations else pd.DataFrame()\n            \n            print(f\"\\nTotal samples: {len(features_df)}\")\n            print(f\"Positive rate: {100*np.mean(labels):.1f}%\")\n            print(f\"Number of videos: {features_df['video_id'].nunique()}\")\n            \n            return features_df, labels, annotations_df\n        \n        return None, None, None\n    \n    def train_model(self, features_df: pd.DataFrame, labels: np.ndarray, annotations_df: pd.DataFrame = None):\n        \"\"\"Train model with CV\"\"\"\n        self.detector.train_with_cv(features_df, labels, annotations_df)\n    \n    def generate_predictions(self) -> pd.DataFrame:\n        \"\"\"Generate test predictions\"\"\"\n        print(\"\\n\" + \"=\"*60)\n        print(\"GENERATING PREDICTIONS\")\n        print(\"=\"*60)\n        \n        test_path = config.data_path / 'test_tracking'\n        test_files = []\n        \n        if test_path.exists():\n            for lab_dir in test_path.iterdir():\n                if lab_dir.is_dir():\n                    test_files.extend(list(lab_dir.glob('*.parquet')))\n        \n        print(f\"Found {len(test_files)} test files\")\n        \n        predictions = []\n        \n        for test_file in tqdm(test_files, desc=\"Predicting\"):\n            # Process file\n            features, _, _ = self.load_and_process_file(test_file)\n            \n            if features.empty:\n                continue\n            \n            # Prepare features\n            X = self.detector.prepare_features(features)\n            \n            # Predict\n            proba = self.detector.predict(X)\n            \n            # Use optimized threshold or default\n            threshold = config.confidence_thresholds.get('global', 0.3)\n            \n            # Convert to predictions\n            for idx, prob in enumerate(proba):\n                if prob > threshold:\n                    predictions.append({\n                        'video_id': features.iloc[idx]['video_id'],\n                        'agent_id': 'mouse1',\n                        'target_id': 'mouse2',\n                        'action': 'sniff',  # Default action\n                        'start_frame': int(features.iloc[idx]['start_frame']),\n                        'stop_frame': int(features.iloc[idx]['end_frame'])\n                    })\n        \n        if predictions:\n            return pd.DataFrame(predictions)\n        else:\n            # Minimal valid submission\n            return pd.DataFrame({\n                'video_id': ['test_video'],\n                'agent_id': ['mouse1'],\n                'target_id': ['mouse2'],\n                'action': ['grooming'],\n                'start_frame': [0],\n                'stop_frame': [30]\n            })\n    \n    def create_submission(self, predictions: pd.DataFrame) -> pd.DataFrame:\n        \"\"\"Create submission file\"\"\"\n        predictions['row_id'] = range(len(predictions))\n        submission = predictions[['row_id', 'video_id', 'agent_id', 'target_id', \n                                 'action', 'start_frame', 'stop_frame']]\n        \n        submission.to_csv('submission.csv', index=False)\n        print(f\"\\nCreated submission.csv with {len(submission)} predictions\")\n        return submission\n\n# ============================================================================\n# MAIN EXECUTION\n# ============================================================================\n\ndef main():\n    \"\"\"Main execution with CV\"\"\"\n    import time\n    start_time = time.time()\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"MABe CHALLENGE 2025 - WITH PROPER CV\")\n    print(\"=\"*80)\n    \n    print(f\"\\nConfiguration:\")\n    print(f\"  CV Strategy: {config.cv_strategy}\")\n    print(f\"  CV Folds: {config.n_folds}\")\n    print(f\"  Max training videos: {config.max_train_videos}\")\n    print(f\"  Ensemble models: {config.n_models}\")\n    \n    # Initialize pipeline\n    pipeline = MABePipelineCV()\n    \n    # Process training data\n    features_df, labels, annotations_df = pipeline.process_training_data()\n    \n    if features_df is not None and len(features_df) > 500:\n        # Train with CV\n        pipeline.train_model(features_df, labels, annotations_df)\n        \n        # Print CV results\n        if pipeline.detector.cv_scores:\n            print(\"\\n\" + \"=\"*60)\n            print(\"CROSS-VALIDATION RESULTS\")\n            print(\"=\"*60)\n            print(f\"Mean CV F-beta: {np.mean(pipeline.detector.cv_scores):.4f}\")\n            print(f\"Std CV F-beta: {np.std(pipeline.detector.cv_scores):.4f}\")\n            print(f\"Individual fold scores: {pipeline.detector.cv_scores}\")\n            print(\"\\nExpected LB correlation:\")\n            print(f\"  Optimistic (mean + std): {np.mean(pipeline.detector.cv_scores) + np.std(pipeline.detector.cv_scores):.4f}\")\n            print(f\"  Expected (mean): {np.mean(pipeline.detector.cv_scores):.4f}\")\n            print(f\"  Conservative (mean - std): {np.mean(pipeline.detector.cv_scores) - np.std(pipeline.detector.cv_scores):.4f}\")\n        \n        # Save model\n        with open('cv_model.pkl', 'wb') as f:\n            pickle.dump(pipeline.detector, f)\n        print(\"\\nModel saved to cv_model.pkl\")\n        \n        # Generate predictions\n        predictions = pipeline.generate_predictions()\n        \n        # Create submission\n        submission = pipeline.create_submission(predictions)\n        \n        # Report runtime\n        elapsed = time.time() - start_time\n        print(f\"\\nTotal runtime: {elapsed/3600:.2f} hours\")\n        \n        print(\"\\n\" + \"=\"*80)\n        print(\"PIPELINE COMPLETE!\")\n        print(\"=\"*80)\n        \n        return submission\n    else:\n        print(\"\\nInsufficient training data!\")\n        return None\n\nif __name__ == \"__main__\":\n    submission = main()\n    if submission is not None:\n        print(\"\\n✓ Execution completed successfully!\")\n        print(f\"Submission shape: {submission.shape}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null}]}