{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# MABe Challenge - Advanced Ensemble with Comprehensive Improvements\n# Complete working code - optimized for performance\n# Enhanced version with additional features\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\nimport gc\nimport lightgbm\nfrom collections import defaultdict\nimport polars as pl\nfrom scipy import signal, stats\n\nfrom sklearn.base import ClassifierMixin, BaseEstimator, clone\nfrom sklearn.model_selection import cross_val_predict, GroupKFold, StratifiedKFold\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.metrics import f1_score\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# ==================== IMPROVED CLASSIFIERS ====================\n\nclass StratifiedSubsetClassifier(ClassifierMixin, BaseEstimator):\n    \"\"\"Fit estimator with stratified sampling to maintain class balance\"\"\"\n    def __init__(self, estimator, n_samples):\n        self.estimator = estimator\n        self.n_samples = n_samples\n\n    def fit(self, X, y):\n        if len(X) <= self.n_samples:\n            self.estimator.fit(np.array(X, copy=False), np.array(y, copy=False))\n        else:\n            # Stratified sampling\n            from sklearn.model_selection import StratifiedShuffleSplit\n            sss = StratifiedShuffleSplit(n_splits=1, train_size=min(self.n_samples, len(X)), random_state=42)\n            try:\n                for train_idx, _ in sss.split(X, y):\n                    self.estimator.fit(np.array(X, copy=False)[train_idx], np.array(y, copy=False)[train_idx])\n            except:\n                # Fallback to regular downsampling if stratified fails\n                downsample = len(X) // self.n_samples\n                downsample = max(downsample, 1)\n                self.estimator.fit(np.array(X, copy=False)[::downsample], np.array(y, copy=False)[::downsample])\n        \n        self.classes_ = self.estimator.classes_\n        return self\n\n    def predict_proba(self, X):\n        if len(self.classes_) == 1:\n            return np.full((len(X), 1), 1.0)\n        probs = self.estimator.predict_proba(np.array(X))\n        return probs\n        \n    def predict(self, X):\n        return self.estimator.predict(np.array(X))\n\n# ==================== 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)\ntrain_without_mabe22 = train.query(\"~ lab_id.str.startswith('MABe22_')\")\n\ntest = pd.read_csv('/kaggle/input/MABe-mouse-behavior-detection/test.csv')\nbody_parts_tracked_list = list(np.unique(train.body_parts_tracked))\n\ndrop_body_parts = ['headpiece_bottombackleft', 'headpiece_bottombackright', 'headpiece_bottomfrontleft', '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/MABe-mouse-behavior-detection/{traintest}_tracking\"\n    for _, row in dataset.iterrows():\n        \n        lab_id = row.lab_id\n        if lab_id.startswith('MABe22'): continue\n        video_id = row.video_id\n\n        if type(row.behaviors_labeled) != str:\n            if verbose: print('No labeled behaviors:', lab_id, video_id)\n            continue\n\n        path = f\"{traintest_directory}/{lab_id}/{video_id}.parquet\"\n        vid = pd.read_parquet(path)\n        if len(np.unique(vid.bodypart)) > 5:\n            vid = vid.query(\"~ bodypart.isin(@drop_body_parts)\")\n        pvid = vid.pivot(columns=['mouse_id', 'bodypart'], index='video_frame', values=['x', 'y'])\n        if pvid.isna().any().any():\n            if verbose and traintest == 'test': print('video with missing values', video_id, 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                    })\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                    })\n                    if traintest == 'train':\n                        mouse_pair_label = pd.DataFrame(0.0, columns=vid_agent_actions, index=mouse_pair.index)\n                        annot_subset = annot.query(\"(agent_id == @agent) & (target_id == @target)\")\n                        for i in range(len(annot_subset)):\n                            annot_row = annot_subset.iloc[i]\n                            mouse_pair_label.loc[annot_row['start_frame']:annot_row['stop_frame'], annot_row.action] = 1.0\n                        yield 'pair', mouse_pair, mouse_pair_meta, mouse_pair_label\n                    else:\n                        if verbose: print('- test pair', video_id, agent, target)\n                        yield 'pair', mouse_pair, mouse_pair_meta, vid_agent_actions\n\n# ==================== ADAPTIVE THRESHOLDING ====================\n\naction_thresholds = defaultdict(lambda: 0.27)  # Default threshold\n\ndef predict_multiclass_adaptive(pred, meta, action_thresholds):\n    \"\"\"Adaptive thresholding per action + temporal smoothing\"\"\"\n    # Apply temporal smoothing\n    pred_smoothed = pred.rolling(window=5, min_periods=1, center=True).mean()\n    \n    ama = np.argmax(pred_smoothed, axis=1)\n    \n    # Adaptive thresholding per action\n    max_probs = pred_smoothed.max(axis=1)\n    threshold_mask = np.zeros(len(pred_smoothed), dtype=bool)\n    for i, action in enumerate(pred_smoothed.columns):\n        action_mask = (ama == i)\n        threshold = action_thresholds.get(action, 0.27)\n        threshold_mask |= (action_mask & (max_probs >= threshold))\n    \n    ama = np.where(threshold_mask, ama, -1)\n    ama = pd.Series(ama, index=meta.video_frame)\n    \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    # Filter out very short events (likely noise)\n    duration = submission_part.stop_frame - submission_part.start_frame\n    submission_part = submission_part[duration >= 3].reset_index(drop=True)\n    \n    if len(submission_part) > 0:\n        assert (submission_part.stop_frame > submission_part.start_frame).all(), 'stop <= start'\n    \n    if verbose: print(f'  actions found: {len(submission_part)}')\n    return submission_part\n\n# ==================== ADVANCED 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 add_curvature_features(X, center_x, center_y):\n    \"\"\"Trajectory curvature\"\"\"\n    vel_x = center_x.diff()\n    vel_y = center_y.diff()\n    acc_x = vel_x.diff()\n    acc_y = vel_y.diff()\n    \n    cross_prod = vel_x * acc_y - vel_y * acc_x\n    vel_mag = np.sqrt(vel_x**2 + vel_y**2)\n    curvature = np.abs(cross_prod) / (vel_mag**3 + 1e-6)\n    \n    for window in [30, 60]:\n        X[f'curv_mean_{window}'] = curvature.rolling(window, min_periods=5).mean()\n    \n    angle = np.arctan2(vel_y, vel_x)\n    angle_change = np.abs(angle.diff())\n    X['turn_rate_30'] = angle_change.rolling(30, min_periods=5).sum()\n    \n    return X\n\ndef add_multiscale_features(X, center_x, center_y):\n    \"\"\"Multi-scale temporal features\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2)\n    \n    scales = [10, 40, 160]\n    for scale in scales:\n        if len(speed) >= scale:\n            X[f'sp_m{scale}'] = speed.rolling(scale, min_periods=max(1, scale//4)).mean()\n            X[f'sp_s{scale}'] = speed.rolling(scale, min_periods=max(1, scale//4)).std()\n    \n    if len(scales) >= 2 and f'sp_m{scales[0]}' in X.columns and f'sp_m{scales[-1]}' in X.columns:\n        X['sp_ratio'] = X[f'sp_m{scales[0]}'] / (X[f'sp_m{scales[-1]}'] + 1e-6)\n    \n    return X\n\ndef add_state_features(X, center_x, center_y):\n    \"\"\"Behavioral state transitions\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2)\n    speed_ma = speed.rolling(15, min_periods=5).mean()\n    \n    try:\n        speed_states = pd.cut(speed_ma, bins=[-np.inf, 0.5, 2.0, 5.0, np.inf], labels=[0, 1, 2, 3]).astype(float)\n        \n        for window in [60, 120]:\n            if len(speed_states) >= window:\n                for state in [0, 1, 2, 3]:\n                    X[f's{state}_{window}'] = (speed_states == state).astype(float).rolling(window, min_periods=10).mean()\n                \n                state_changes = (speed_states != speed_states.shift(1)).astype(float)\n                X[f'trans_{window}'] = state_changes.rolling(window, min_periods=10).sum()\n    except:\n        pass\n    \n    return X\n\ndef add_longrange_features(X, center_x, center_y):\n    \"\"\"Long-range temporal features\"\"\"\n    for window in [120, 240]:\n        if len(center_x) >= window:\n            X[f'x_ml{window}'] = center_x.rolling(window, min_periods=20).mean()\n            X[f'y_ml{window}'] = center_y.rolling(window, min_periods=20).mean()\n    \n    for span in [60, 120]:\n        X[f'x_e{span}'] = center_x.ewm(span=span, min_periods=1).mean()\n        X[f'y_e{span}'] = center_y.ewm(span=span, min_periods=1).mean()\n    \n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2)\n    for window in [60, 120]:\n        if len(speed) >= window:\n            X[f'sp_pct{window}'] = speed.rolling(window, min_periods=20).rank(pct=True)\n    \n    return X\n\ndef add_interaction_features(X, mouse_pair, avail_A, avail_B):\n    \"\"\"Social interaction features\"\"\"\n    if 'body_center' not in avail_A or 'body_center' not in avail_B:\n        return X\n    \n    rel_x = mouse_pair['A']['body_center']['x'] - mouse_pair['B']['body_center']['x']\n    rel_y = mouse_pair['A']['body_center']['y'] - mouse_pair['B']['body_center']['y']\n    rel_dist = np.sqrt(rel_x**2 + rel_y**2)\n    \n    A_vx = mouse_pair['A']['body_center']['x'].diff()\n    A_vy = mouse_pair['A']['body_center']['y'].diff()\n    B_vx = mouse_pair['B']['body_center']['x'].diff()\n    B_vy = mouse_pair['B']['body_center']['y'].diff()\n    \n    A_lead = (A_vx * rel_x + A_vy * rel_y) / (np.sqrt(A_vx**2 + A_vy**2) * rel_dist + 1e-6)\n    B_lead = (B_vx * (-rel_x) + B_vy * (-rel_y)) / (np.sqrt(B_vx**2 + B_vy**2) * rel_dist + 1e-6)\n    \n    for window in [30, 60]:\n        X[f'A_ld{window}'] = A_lead.rolling(window, min_periods=5).mean()\n        X[f'B_ld{window}'] = B_lead.rolling(window, min_periods=5).mean()\n    \n    approach = -rel_dist.diff()\n    chase = approach * B_lead\n    X['chase_30'] = chase.rolling(30, min_periods=5).mean()\n    \n    for window in [60, 120]:\n        A_sp = np.sqrt(A_vx**2 + A_vy**2)\n        B_sp = np.sqrt(B_vx**2 + B_vy**2)\n        X[f'sp_cor{window}'] = A_sp.rolling(window, min_periods=10).corr(B_sp)\n    \n    return X\n\n# NEW: Additional feature functions for enhancement\n\ndef add_acceleration_features(X, center_x, center_y):\n    \"\"\"Add acceleration-based features\"\"\"\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    acc_mag = np.sqrt(acc_x**2 + acc_y**2)\n    \n    for window in [15, 30, 60]:\n        X[f'acc_mean_{window}'] = acc_mag.rolling(window, min_periods=max(1, window//4)).mean()\n        X[f'acc_std_{window}'] = acc_mag.rolling(window, min_periods=max(1, window//4)).std()\n        X[f'acc_max_{window}'] = acc_mag.rolling(window, min_periods=max(1, window//4)).max()\n    \n    X['jerk_mean_30'] = acc_mag.diff().abs().rolling(30, min_periods=5).mean()\n    \n    return X\n\ndef add_frequency_features(X, center_x, center_y):\n    \"\"\"Add frequency domain features using scipy.signal\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2).fillna(0)\n    \n    # Compute power spectral density\n    if len(speed) > 128:  # Minimum length for meaningful FFT\n        f, psd = signal.welch(speed, fs=1.0, nperseg=min(128, len(speed)), noverlap=64)\n        dominant_freq = f[np.argmax(psd)]\n        spectral_entropy = -np.sum(psd * np.log2(psd + 1e-12)) / np.log2(len(psd))\n    else:\n        dominant_freq = 0\n        spectral_entropy = 0\n    \n    X['dominant_freq'] = dominant_freq\n    X['spectral_entropy'] = spectral_entropy\n    \n    # Add rolling spectral features if length allows\n    if len(speed) >= 256:\n        rolling_psd = []\n        for i in range(0, len(speed) - 128, 64):\n            f, psd = signal.welch(speed[i:i+128], fs=1.0, nperseg=128)\n            rolling_psd.append(np.mean(psd))\n        # Pad to match length\n        rolling_psd = np.interp(np.arange(len(speed)), np.arange(0, len(rolling_psd)*64, 64), rolling_psd)\n        X['rolling_psd_mean'] = pd.Series(rolling_psd).rolling(10, min_periods=1).mean()\n    \n    return X\n\ndef add_statistical_moments(X, center_x, center_y):\n    \"\"\"Add higher-order statistical moments\"\"\"\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2)\n    \n    for window in [30, 60]:\n        X[f'sp_skew_{window}'] = speed.rolling(window, min_periods=5).skew()\n        X[f'sp_kurt_{window}'] = speed.rolling(window, min_periods=5).kurt()\n    \n    return X\n\ndef add_relative_acceleration_features(X, mouse_pair, avail_A, avail_B):\n    \"\"\"Add relative acceleration features for pairs\"\"\"\n    if 'body_center' not in avail_A or 'body_center' not in avail_B:\n        return X\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_ax = A_vx.diff()\n    A_ay = A_vy.diff()\n    B_ax = B_vx.diff()\n    B_ay = B_vy.diff()\n    \n    rel_acc_mag = np.sqrt((A_ax - B_ax)**2 + (A_ay - B_ay)**2)\n    \n    for window in [30, 60]:\n        X[f'rel_acc_mean_{window}'] = rel_acc_mag.rolling(window, min_periods=5).mean()\n        X[f'rel_acc_std_{window}'] = rel_acc_mag.rolling(window, min_periods=5).std()\n    \n    return X\n\n# Updated transform_single with new features\ndef transform_single(single_mouse, body_parts_tracked):\n    \"\"\"Enhanced single mouse transform with additional features\"\"\"\n    available_body_parts = single_mouse.columns.get_level_values(0)\n    \n    # Base distance features\n    X = pd.DataFrame({\n        f\"{p1}+{p2}\": np.square(single_mouse[p1] - single_mouse[p2]).sum(axis=1, skipna=False)\n        for p1, p2 in itertools.combinations(body_parts_tracked, 2) \n        if p1 in available_body_parts and p2 in available_body_parts\n    })\n    X = X.reindex(columns=[f\"{p1}+{p2}\" for p1, p2 in itertools.combinations(body_parts_tracked, 2)], copy=False)\n\n    # Speed features\n    if all(p in single_mouse.columns for p in ['ear_left', 'ear_right', 'tail_base']):\n        shifted = single_mouse[['ear_left', 'ear_right', 'tail_base']].shift(10)\n        speeds = pd.DataFrame({\n            'sp_lf': np.square(single_mouse['ear_left'] - shifted['ear_left']).sum(axis=1, skipna=False),\n            'sp_rt': np.square(single_mouse['ear_right'] - shifted['ear_right']).sum(axis=1, skipna=False),\n            'sp_lf2': np.square(single_mouse['ear_left'] - shifted['tail_base']).sum(axis=1, skipna=False),\n            'sp_rt2': np.square(single_mouse['ear_right'] - shifted['tail_base']).sum(axis=1, skipna=False),\n        })\n        X = pd.concat([X, speeds], axis=1)\n    \n    if 'nose+tail_base' in X.columns and 'ear_left+ear_right' in X.columns:\n        X['elong'] = X['nose+tail_base'] / (X['ear_left+ear_right'] + 1e-6)\n    \n    # Body angle\n    if all(p in available_body_parts for p in ['nose', 'body_center', 'tail_base']):\n        v1 = single_mouse['nose'] - single_mouse['body_center']\n        v2 = single_mouse['tail_base'] - single_mouse['body_center']\n        X['body_ang'] = (v1['x'] * v2['x'] + v1['y'] * v2['y']) / (\n            np.sqrt(v1['x']**2 + v1['y']**2) * np.sqrt(v2['x']**2 + v2['y']**2) + 1e-6)\n    \n    # Core temporal features\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']\n        cy = single_mouse['body_center']['y']\n        \n        for w in [5, 15, 30, 60]:\n            X[f'cx_m{w}'] = cx.rolling(w, min_periods=1, center=True).mean()\n            X[f'cy_m{w}'] = cy.rolling(w, min_periods=1, center=True).mean()\n            X[f'cx_s{w}'] = cx.rolling(w, min_periods=1, center=True).std()\n            X[f'cy_s{w}'] = cy.rolling(w, min_periods=1, center=True).std()\n            X[f'x_rng{w}'] = cx.rolling(w, min_periods=1, center=True).max() - cx.rolling(w, min_periods=1, center=True).min()\n            X[f'y_rng{w}'] = cy.rolling(w, min_periods=1, center=True).max() - cy.rolling(w, min_periods=1, center=True).min()\n            X[f'disp{w}'] = np.sqrt(cx.diff().rolling(w, min_periods=1).sum()**2 + cy.diff().rolling(w, min_periods=1).sum()**2)\n            X[f'act{w}'] = np.sqrt(cx.diff().rolling(w, min_periods=1).var() + cy.diff().rolling(w, min_periods=1).var())\n        \n        # Advanced features\n        X = add_curvature_features(X, cx, cy)\n        X = add_multiscale_features(X, cx, cy)\n        X = add_state_features(X, cx, cy)\n        X = add_longrange_features(X, cx, cy)\n        \n        # NEW: Additional features\n        X = add_acceleration_features(X, cx, cy)\n        X = add_frequency_features(X, cx, cy)\n        X = add_statistical_moments(X, cx, cy)\n    \n    # Nose-tail features\n    if all(p in available_body_parts for p in ['nose', 'tail_base']):\n        nt_dist = np.sqrt((single_mouse['nose']['x'] - single_mouse['tail_base']['x'])**2 + \n                         (single_mouse['nose']['y'] - single_mouse['tail_base']['y'])**2)\n        for lag in [10, 20, 40]:\n            X[f'nt_lg{lag}'] = nt_dist.shift(lag)\n            X[f'nt_df{lag}'] = nt_dist - nt_dist.shift(lag)\n    \n    # Ear features\n    if all(p in available_body_parts for p in ['ear_left', 'ear_right']):\n        ear_d = np.sqrt((single_mouse['ear_left']['x'] - single_mouse['ear_right']['x'])**2 + \n                       (single_mouse['ear_left']['y'] - single_mouse['ear_right']['y'])**2)\n        for off in [-20, -10, 10, 20]:\n            X[f'ear_o{off}'] = ear_d.shift(-off)\n        X['ear_con'] = ear_d.rolling(30, min_periods=1, center=True).std() / (ear_d.rolling(30, min_periods=1, center=True).mean() + 1e-6)\n    \n    return X\n\n# Updated transform_pair with new features\ndef transform_pair(mouse_pair, body_parts_tracked):\n    \"\"\"Enhanced pair transform with additional features\"\"\"\n    avail_A = mouse_pair['A'].columns.get_level_values(0)\n    avail_B = mouse_pair['B'].columns.get_level_values(0)\n    \n    # Inter-mouse distances\n    X = pd.DataFrame({\n        f\"12+{p1}+{p2}\": np.square(mouse_pair['A'][p1] - mouse_pair['B'][p2]).sum(axis=1, skipna=False)\n        for p1, p2 in itertools.product(body_parts_tracked, repeat=2) \n        if p1 in avail_A and p2 in avail_B\n    })\n    X = X.reindex(columns=[f\"12+{p1}+{p2}\" for p1, p2 in itertools.product(body_parts_tracked, repeat=2)], copy=False)\n\n    # Speed features\n    if ('A', 'ear_left') in mouse_pair.columns and ('B', 'ear_left') in mouse_pair.columns:\n        shA = mouse_pair['A']['ear_left'].shift(10)\n        shB = mouse_pair['B']['ear_left'].shift(10)\n        speeds = pd.DataFrame({\n            'sp_A': np.square(mouse_pair['A']['ear_left'] - shA).sum(axis=1, skipna=False),\n            'sp_AB': np.square(mouse_pair['A']['ear_left'] - shB).sum(axis=1, skipna=False),\n            'sp_B': np.square(mouse_pair['B']['ear_left'] - shB).sum(axis=1, skipna=False),\n        })\n        X = pd.concat([X, speeds], axis=1)\n    \n    if 'nose+tail_base' in X.columns and 'ear_left+ear_right' in X.columns:\n        X['elong'] = X['nose+tail_base'] / (X['ear_left+ear_right'] + 1e-6)\n    \n    # Relative orientation\n    if all(p in avail_A for p in ['nose', 'tail_base']) and all(p in avail_B for p in ['nose', 'tail_base']):\n        dir_A = mouse_pair['A']['nose'] - mouse_pair['A']['tail_base']\n        dir_B = mouse_pair['B']['nose'] - mouse_pair['B']['tail_base']\n        X['rel_ori'] = (dir_A['x'] * dir_B['x'] + dir_A['y'] * dir_B['y']) / (\n            np.sqrt(dir_A['x']**2 + dir_A['y']**2) * np.sqrt(dir_B['x']**2 + dir_B['y']**2) + 1e-6)\n    \n    # Approach rate\n    if all(p in avail_A for p in ['nose']) and all(p in avail_B for p in ['nose']):\n        cur = np.square(mouse_pair['A']['nose'] - mouse_pair['B']['nose']).sum(axis=1, skipna=False)\n        shA_n = mouse_pair['A']['nose'].shift(10)\n        shB_n = mouse_pair['B']['nose'].shift(10)\n        past = np.square(shA_n - shB_n).sum(axis=1, skipna=False)\n        X['appr'] = cur - past\n    \n    # Distance bins\n    if 'body_center' in avail_A and 'body_center' in avail_B:\n        cd = np.sqrt((mouse_pair['A']['body_center']['x'] - mouse_pair['B']['body_center']['x'])**2 +\n                    (mouse_pair['A']['body_center']['y'] - mouse_pair['B']['body_center']['y'])**2)\n        X['v_cls'] = (cd < 5.0).astype(float)\n        X['cls'] = ((cd >= 5.0) & (cd < 15.0)).astype(float)\n        X['med'] = ((cd >= 15.0) & (cd < 30.0)).astype(float)\n        X['far'] = (cd >= 30.0).astype(float)\n    \n    # Temporal interaction\n    if 'body_center' in avail_A and 'body_center' in avail_B:\n        cd_full = np.square(mouse_pair['A']['body_center'] - mouse_pair['B']['body_center']).sum(axis=1, skipna=False)\n        \n        for w in [5, 15, 30, 60]:\n            X[f'd_m{w}'] = cd_full.rolling(w, min_periods=1, center=True).mean()\n            X[f'd_s{w}'] = cd_full.rolling(w, min_periods=1, center=True).std()\n            X[f'd_mn{w}'] = cd_full.rolling(w, min_periods=1, center=True).min()\n            X[f'd_mx{w}'] = cd_full.rolling(w, min_periods=1, center=True).max()\n            \n            d_var = cd_full.rolling(w, min_periods=1, center=True).var()\n            X[f'int{w}'] = 1 / (1 + d_var)\n            \n            Axd = mouse_pair['A']['body_center']['x'].diff()\n            Ayd = mouse_pair['A']['body_center']['y'].diff()\n            Bxd = mouse_pair['B']['body_center']['x'].diff()\n            Byd = mouse_pair['B']['body_center']['y'].diff()\n            coord = Axd * Bxd + Ayd * Byd\n            X[f'co_m{w}'] = coord.rolling(w, min_periods=1, center=True).mean()\n            X[f'co_s{w}'] = coord.rolling(w, min_periods=1, center=True).std()\n    \n    # Nose-nose\n    if 'nose' in avail_A and 'nose' in avail_B:\n        nn = np.sqrt((mouse_pair['A']['nose']['x'] - mouse_pair['B']['nose']['x'])**2 +\n                    (mouse_pair['A']['nose']['y'] - mouse_pair['B']['nose']['y'])**2)\n        for lag in [10, 20, 40]:\n            X[f'nn_lg{lag}'] = nn.shift(lag)\n            X[f'nn_ch{lag}'] = nn - nn.shift(lag)\n            is_cl = (nn < 10.0).astype(float)\n            X[f'cl_ps{lag}'] = is_cl.rolling(lag, min_periods=1).mean()\n    \n    # Velocity alignment\n    if 'body_center' in avail_A and 'body_center' in avail_B:\n        Avx = mouse_pair['A']['body_center']['x'].diff()\n        Avy = mouse_pair['A']['body_center']['y'].diff()\n        Bvx = mouse_pair['B']['body_center']['x'].diff()\n        Bvy = mouse_pair['B']['body_center']['y'].diff()\n        val = (Avx * Bvx + Avy * Bvy) / (np.sqrt(Avx**2 + Avy**2) * np.sqrt(Bvx**2 + Bvy**2) + 1e-6)\n        \n        for off in [-20, -10, 0, 10, 20]:\n            X[f'va_{off}'] = val.shift(-off)\n        \n        X['int_con'] = cd_full.rolling(30, min_periods=1, center=True).std() / (cd_full.rolling(30, min_periods=1, center=True).mean() + 1e-6)\n        \n        # Advanced interaction\n        X = add_interaction_features(X, mouse_pair, avail_A, avail_B)\n        \n        # NEW: Additional features for pairs\n        X = add_relative_acceleration_features(X, mouse_pair, avail_A, avail_B)\n        cx_a = mouse_pair['A']['body_center']['x']\n        cy_a = mouse_pair['A']['body_center']['y']\n        cx_b = mouse_pair['B']['body_center']['x']\n        cy_b = mouse_pair['B']['body_center']['y']\n        X = add_frequency_features(X, cx_a, cy_a)  # Apply to agent A\n        X = add_statistical_moments(X, cx_a, cy_a)  # Apply to agent A\n    \n    return X\n\n# ==================== ENSEMBLE TRAINING ====================\n\ndef submit_ensemble(body_parts_tracked_str, switch_tr, X_tr, label, meta):\n    \"\"\"Advanced ensemble with 5 models\"\"\"\n    \n    # Build model list\n    models = []\n    \n    # Model 1: LightGBM shallow\n    models.append(make_pipeline(\n        SimpleImputer(),\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            100000)\n    ))\n    \n    # Model 2: LightGBM deep\n    models.append(make_pipeline(\n        SimpleImputer(),\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            80000)\n    ))\n    \n    # Model 3: LightGBM very deep\n    models.append(make_pipeline(\n        SimpleImputer(),\n        StratifiedSubsetClassifier(\n            lightgbm.LGBMClassifier(\n                n_estimators=100, learning_rate=0.05, min_child_samples=30,\n                num_leaves=127, max_depth=10, subsample=0.75, verbose=-1),\n            60000)\n    ))\n    \n    # Model 4: XGBoost\n    if XGBOOST_AVAILABLE:\n        models.append(make_pipeline(\n            SimpleImputer(),\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                85000)\n        ))\n    \n    # Model 5: CatBoost\n    if CATBOOST_AVAILABLE:\n        models.append(make_pipeline(\n            SimpleImputer(),\n            StratifiedSubsetClassifier(\n                CatBoostClassifier(\n                    iterations=120, learning_rate=0.1, depth=6,\n                    verbose=False, allow_writing_files=False),\n                70000)\n        ))\n    \n    model_list = []\n    for action in label.columns:\n        action_mask = ~ label[action].isna().values\n        y_action = label[action][action_mask].values.astype(int)\n\n        if not (y_action == 0).all() and y_action.sum() >= 5:\n            trained = []\n            for m in models:\n                m_clone = clone(m)\n                m_clone.fit(X_tr[action_mask], y_action)\n                trained.append(m_clone)\n            model_list.append((action, trained))\n    \n    del X_tr\n    gc.collect()\n\n    body_parts_tracked = json.loads(body_parts_tracked_str)\n    if len(body_parts_tracked) > 5:\n        body_parts_tracked = [b for b in body_parts_tracked if b not in drop_body_parts]\n    \n    test_subset = test[test.body_parts_tracked == body_parts_tracked_str]\n    generator = generate_mouse_data(test_subset, 'test',\n                                    generate_single=(switch_tr == 'single'), \n                                    generate_pair=(switch_tr == 'pair'))\n    \n    if verbose: print(f\"n_videos: {len(test_subset)}, n_models: {len(models)}\")\n    \n    for switch_te, data_te, meta_te, actions_te in generator:\n        assert switch_te == switch_tr\n        try:\n            if switch_te == 'single':\n                X_te = transform_single(data_te, body_parts_tracked)\n            else:\n                X_te = transform_pair(data_te, body_parts_tracked)\n            \n            if verbose and len(X_te) == 0: print(\"ERROR: X_te empty\")\n            del data_te\n    \n            pred = pd.DataFrame(index=meta_te.video_frame)\n            for action, trained in model_list:\n                if action in actions_te:\n                    probs = [m.predict_proba(X_te)[:, 1] for m in trained]\n                    pred[action] = np.mean(probs, axis=0)\n            \n            del X_te\n            gc.collect()\n            \n            if pred.shape[1] != 0:\n                sub_part = predict_multiclass_adaptive(pred, meta_te, action_thresholds)\n                submission_list.append(sub_part)\n            else:\n                if verbose: print(f\"  ERROR: no training data\")\n        except Exception as e:\n            if verbose: print(f'  ERROR: {str(e)[:50]}')\n            try:\n                del data_te\n            except:\n                pass\n            gc.collect()\n\ndef robustify(submission, dataset, traintest, traintest_directory=None):\n    \"\"\"Robustness post-processing\"\"\"\n    if traintest_directory is None:\n        traintest_directory = f\"/kaggle/input/MABe-mouse-behavior-detection/{traintest}_tracking\"\n\n    # Remove invalid frames\n    submission = submission[submission.start_frame < submission.stop_frame]\n    \n    # Remove overlaps\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    # Fill empty videos\n    s_list = []\n    for idx, row in dataset.iterrows():\n        lab_id = row['lab_id']\n        if lab_id.startswith('MABe22'):\n            continue\n        video_id = row['video_id']\n        if (submission.video_id == video_id).any():\n            continue\n\n        if verbose: print(f\"Video {video_id} has no predictions\")\n        \n        path = f\"{traintest_directory}/{lab_id}/{video_id}.parquet\"\n        vid = pd.read_parquet(path)\n    \n        vid_behaviors = eval(row['behaviors_labeled'])\n        vid_behaviors = sorted(list({b.replace(\"'\", \"\") for b in vid_behaviors}))\n        vid_behaviors = [b.split(',') for b in vid_behaviors]\n        vid_behaviors = pd.DataFrame(vid_behaviors, columns=['agent', 'target', 'action'])\n    \n        start_frame = vid.video_frame.min()\n        stop_frame = vid.video_frame.max() + 1\n    \n        for (agent, target), actions in vid_behaviors.groupby(['agent', 'target']):\n            batch_len = int(np.ceil((stop_frame - start_frame) / len(actions)))\n            for i, (_, action_row) in enumerate(actions.iterrows()):\n                batch_start = start_frame + i * batch_len\n                batch_stop = min(batch_start + batch_len, stop_frame)\n                s_list.append((video_id, agent, target, action_row['action'], batch_start, batch_stop))\n\n    if len(s_list) > 0:\n        submission = pd.concat([\n            submission,\n            pd.DataFrame(s_list, columns=['video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame'])\n        ])\n\n    submission = submission.reset_index(drop=True)\n    return submission\n\n# ==================== MAIN LOOP ====================\n\nsubmission_list = []\n\nprint(f\"XGBoost: {XGBOOST_AVAILABLE}, CatBoost: {CATBOOST_AVAILABLE}\\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        single_list, single_label_list, single_meta_list = [], [], []\n        pair_list, pair_label_list, pair_meta_list = [], [], []\n    \n        for switch, data, meta, label in generate_mouse_data(train_subset, 'train'):\n            if switch == 'single':\n                single_list.append(data)\n                single_meta_list.append(meta)\n                single_label_list.append(label)\n            else:\n                pair_list.append(data)\n                pair_meta_list.append(meta)\n                pair_label_list.append(label)\n    \n        if len(single_list) > 0:\n            single_mouse = pd.concat(single_list)\n            single_label = pd.concat(single_label_list)\n            single_meta = pd.concat(single_meta_list)\n            del single_list, single_label_list, single_meta_list\n            gc.collect()\n            \n            X_tr = transform_single(single_mouse, body_parts_tracked)\n            del single_mouse\n            print(f\"  Single: {X_tr.shape}\")\n            submit_ensemble(body_parts_tracked_str, 'single', X_tr, single_label, single_meta)\n                \n        if len(pair_list) > 0:\n            mouse_pair = pd.concat(pair_list)\n            pair_label = pd.concat(pair_label_list)\n            pair_meta = pd.concat(pair_meta_list)\n            del pair_list, pair_label_list, pair_meta_list\n            gc.collect()\n        \n            X_tr = transform_pair(mouse_pair, body_parts_tracked)\n            del mouse_pair\n            print(f\"  Pair: {X_tr.shape}\")\n            submit_ensemble(body_parts_tracked_str, 'pair', X_tr, pair_label, pair_meta)\n                \n    except Exception as e:\n        print(f'***Exception*** {str(e)[:100]}')\n    \n    gc.collect()\n    print()\n\n# Final submission\nif len(submission_list) > 0:\n    submission = pd.concat(submission_list)\nelse:\n    submission = pd.DataFrame({\n        'video_id': [438887472],\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\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T17:36:33.053085Z","iopub.execute_input":"2025-10-06T17:36:33.053432Z","iopub.status.idle":"2025-10-06T19:22:55.000781Z","shell.execute_reply.started":"2025-10-06T17:36:33.053404Z","shell.execute_reply":"2025-10-06T19:22:54.999567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#upvote","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T19:25:45.153271Z","iopub.execute_input":"2025-10-06T19:25:45.153613Z","iopub.status.idle":"2025-10-06T19:25:45.158422Z","shell.execute_reply.started":"2025-10-06T19:25:45.153585Z","shell.execute_reply":"2025-10-06T19:25:45.157515Z"}},"outputs":[],"execution_count":null}]}