{
  "id": 663082,
  "title": "26th place solution",
  "url": "/competitions/MABe-mouse-behavior-detection/discussion/663082",
  "author_name": "",
  "post_date": "2025-12-16T09:50:42.224000",
  "votes": 7,
  "comment_count": 0,
  "views": 0,
  "content": "<h1>kaggle-MABe</h1>\n<p><a href=\"https://github.com/Jayzen-Tech/kaggle-MABe\" target=\"_blank\">github kaggle-MABe</a></p>\n<h2>Table of Contents</h2>\n<ul>\n<li><a href=\"#directory-layout--required-data\">Directory Layout &amp; Required Data</a></li>\n<li><a href=\"#how-to-run--switches\">How to Run &amp; Switches</a></li>\n<li><a href=\"#feature-engineering-upgrades-overview\">Feature Engineering Upgrades Overview</a></li>\n<li><a href=\"#1-tracking-cleanup--pixel-normalization\">Tracking Cleanup &amp; Pixel Normalization</a></li>\n<li><a href=\"#2-meta-encoding--feature-augmentation\">Meta Encoding &amp; Feature Augmentation</a></li>\n<li><a href=\"#3-third-mouse-context-triadic-relations\">Third-Mouse Context</a></li>\n<li><a href=\"#4-single-mouse-advanced-motion-long-horizon--pastfuture\">Single-Mouse Advanced Motion</a></li>\n<li><a href=\"#5-pair-interaction-enhancements--context-ratios\">Pair Interaction Enhancements</a></li>\n<li><a href=\"#6-traintest-consistency--non-finite-logging\">Train/Test Consistency</a></li>\n<li><a href=\"#7-adaptive-threshold-tuning--inference-strategy\">Adaptive Threshold Strategy</a></li>\n</ul>\n<h3>Directory Layout &amp; Required Data</h3>\n<p>The project uses <code>/kaggle/kaggle-MABe</code> as the working directory and depends on external Kaggle inputs. Core folders:</p>\n<pre><code>/kaggle/kaggle-MABe/\n├── mabe-final.py                # Final solution script\n├── baseline/mabe-baseline.py    # Initial baseline\n├── README.md, requirements.txt\n└── __pycache__/\n</code></pre>\n<p>External data and pretrained artifacts live under <code>/kaggle/input</code>, primarily:</p>\n<pre><code>/kaggle/input/\n└── MABe-mouse-behavior-detection/        # Official train/test CSVs and tracking parquet files\n</code></pre>\n<p>If you plan to reuse pretrained models/thresholds through <code>LOAD_MODELS=True</code> or <code>LOAD_THRESHOLDS=True</code>, keep the default folder structure or update the <code>MODEL_LOAD_DIR</code> / <code>THRESHOLD_LOAD_DIR</code> constants inside the script before running.</p>\n<h3>How to Run &amp; Switches</h3>\n<h4>Basic Commands</h4>\n<ol>\n<li>(Optional on first run) Install dependencies: <code>pip install -r requirements.txt</code></li>\n<li>Execute the pipeline: <code>python mabe-final.py</code></li>\n</ol>\n<p>Runtime switches at the top of <code>mabe-final.py</code> control training/inference behavior:</p>\n<pre><code># ---- runtime switches ----\nONLY_TUNE_THRESHOLDS = False    # True: tune thresholds only and exit (skip training/inference/submission)\nUSE_ADAPTIVE_THRESHOLDS = True  # False: fallback to a fixed 0.27 threshold for every action\nLOAD_THRESHOLDS = False         # True: read thresholds from THRESHOLD_DIR instead of tuning\nLOAD_MODELS = False             # True: load pretrained models from MODEL_DIR\nCHECK_LOAD = False              # True: after loading, drop actions without positives in the current folds\nTHRESHOLD_DIR = \"./models/threshold\"\nMODEL_DIR = \"./models\"\nTHRESHOLD_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13/threshold\"\nMODEL_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13\"\nNON_FINITE_LOG = \"non_finite_features.log\"\n</code></pre>\n<p><strong>Recommended usage:</strong></p>\n<ul>\n<li><strong>Full training + inference</strong>: <code>ONLY_TUNE_THRESHOLDS=False</code>, <code>LOAD_MODELS=False</code>. The script trains, predicts, and saves models/thresholds under <code>MODEL_DIR</code> / <code>THRESHOLD_DIR</code>.</li>\n<li><strong>Threshold tuning only</strong>: When models already exist, set <code>ONLY_TUNE_THRESHOLDS=True</code>. The run stops after tuning and writes fresh pickles into <code>THRESHOLD_DIR</code>.</li>\n<li><strong>Reuse trained models</strong>: Set <code>LOAD_MODELS=True</code> (optionally <code>CHECK_LOAD=True</code> to verify label availability). The script reads Joblib caches from <code>MODEL_LOAD_DIR</code>; update the path if your models live elsewhere.</li>\n<li><strong>Reuse tuned thresholds</strong>: Flip <code>LOAD_THRESHOLDS=True</code> to skip tuning and read <code>.pkl</code> files from <code>THRESHOLD_LOAD_DIR</code>.</li>\n<li>Disable adaptive thresholds (<code>USE_ADAPTIVE_THRESHOLDS=False</code>) to fall back to the global 0.27 cutoff for quick sanity checks.</li>\n<li><code>NON_FINITE_LOG</code> records any NaN/Inf columns encountered during training or inference. Point it to another path or truncate it as needed.</li>\n</ul>\n<h3>Feature Engineering Upgrades Overview</h3>\n<p><code>mabe-final.py</code> delivers a full feature-engineering refresh covering tracking cleanup, meta encoding, third-mouse context, single/pair motion modeling, and consistent train/test handling. Each subsection below highlights the additions versus the baseline and quotes the relevant code (all snippets originate from <code>mabe-final.py</code>).</p>\n<h4>1. Tracking Cleanup &amp; Pixel Normalization</h4>\n<ul>\n<li>Reorder the multi-level columns and convert pixels to centimeters using <code>pix_per_cm_approx</code>.</li>\n<li>Interpolate every body part, fill in both directions, and drop joints that remain all NaN.</li>\n<li>Run all downstream matrices through <code>sanitize_features</code> to replace NaN/Inf with interpolations and per-column medians.</li>\n</ul>\n<pre><code># mabe-final.py (generate_mouse_data)\npvid = vid.pivot(columns=['mouse_id','bodypart'], index='video_frame', values=['x','y'])\npvid = pvid.reorder_levels([1,2,0], axis=1).T.sort_index().T\npvid = (pvid / float(row.pix_per_cm_approx)).astype('float32', copy=False)\npvid = pvid.sort_index(axis=1)\npvid = pvid.interpolate(limit_direction='both')\npvid = pvid.fillna(method='bfill').fillna(method='ffill')\nall_nan_cols = pvid.columns[pvid.isna().all()]\nif len(all_nan_cols) &gt; 0:\n    pvid = pvid.drop(columns=all_nan_cols)\n\n# send every feature matrix through sanitize_features\ndef sanitize_features(X: Optional[pd.DataFrame]) -&gt; pd.DataFrame:\n    X = X.replace([np.inf, -np.inf], np.nan)\n    X = X.interpolate(axis=0, limit_direction='both')\n    if X.isna().any().any():\n        med = X.median(axis=0)\n        X = X.fillna(med)\n    if X.isna().any().any():\n        X = X.fillna(0.0)\n    return X\n</code></pre>\n<h4>2. Meta Encoding &amp; Feature Augmentation</h4>\n<ul>\n<li><code>_mk_meta</code> parses agent/target sex and strain, builds integer codes and same-sex/strain indicators, computes arena area/aspect/shape, and aligns meta rows with frame indices.</li>\n<li><code>augment_with_meta_features</code> injects these meta columns into single/pair feature matrices and uses identical sanitization/logging during inference.</li>\n</ul>\n<pre><code># mabe-final.py (_mk_meta excerpt)\nm['agent_sex_code'] = np.float32(agent_sex_code)\nm['target_sex_code'] = np.float32(target_sex_code if target_key is not None else agent_sex_code)\nm['agent_strain_code'] = np.float32(agent_strain_code)\nm['target_strain_code'] = np.float32(target_strain_code if target_key is not None else agent_strain_code)\nm['same_sex'] = np.float32(1.0 if (...) else 0.0)\nm['same_strain'] = np.float32(1.0 if (...) else 0.0)\nm['arena_shape_code'] = np.float32(arena_shape_code)\nm['arena_area_cm2'] = np.float32(arena_area)\nm['arena_aspect_ratio'] = np.float32(arena_aspect)\nm = m.set_index('video_frame')\nm['video_frame'] = m.index\n\ndef augment_with_meta_features(X: pd.DataFrame, meta: Optional[pd.DataFrame]) -&gt; pd.DataFrame:\n    meta_aligned = meta.reindex(X.index)\n    meta_features = pd.DataFrame(index=X.index)\n    for raw, name in [...]:\n        _copy(raw, name)\n    meta_features = sanitize_features(meta_features)\n    return pd.concat([X, meta_features], axis=1)\n</code></pre>\n<h4>3. Third-Mouse Context (Triadic Relations)</h4>\n<ul>\n<li><code>_compute_third_party_context</code> converts every non agent/target mouse into distance stats against agent/target/midpoint, contact counts, weaving ratios, and cosine directionality.</li>\n<li><code>transform_pair</code> flattens the CTX MultiIndex and adds ratios (for example <code>ctx_third_min_dist_*_ratio</code>) so the model learns how third-party interference modulates behaviors.</li>\n</ul>\n<pre><code>def _compute_third_party_context(pvid: pd.DataFrame, agent_label, target_label) -&gt; Optional[pd.DataFrame]:\n    ctx = pd.DataFrame(index=agent_bc.index)\n    ctx['third_min_dist_agent'] = df_a.min(axis=1)\n    ctx['third_mean_dist_agent'] = df_a.mean(axis=1)\n    ctx['third_close_agent_cnt'] = (df_a &lt; close_thresh).sum(axis=1)\n    ctx['third_close_any'] = (close_any &gt; 0).astype(float)\n    ctx['third_between_ratio'] = (df_a + df_b).min(axis=1) / (dist_at + 1e-6)\n    ctx['third_cos_agent_closest'] = closest\n    ctx.columns = pd.MultiIndex.from_arrays([['CTX'] * len(ctx_cols), ctx_cols, ['value'] * len(ctx_cols)])\n    return ctx\n\n# transform_pair excerpt\nif 'CTX' in mouse_pair.columns.get_level_values(0):\n    ctx_flat = mouse_pair['CTX'].copy()\n    ...\n    X[col] = ctx_flat[col]\nif cd is not None:\n    denom = (cd + 1e-6)\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / denom\n</code></pre>\n<h4>4. Single-Mouse Advanced Motion (Long Horizon + Past/Future)</h4>\n<ul>\n<li>When <code>body_center</code> is available, <code>transform_single</code> composes <code>add_longrange_features</code>, curvature, future-vs-past speed asymmetry, Gaussian-shift comparisons, and segment-level metrics.</li>\n<li><code>add_longrange_features</code> mixes configurable windows/EWM/percentiles, and the added operators cover future/past velocity contrasts, KL-style speed shifts, and segment-wise acceleration/jerk/pause ratios.</li>\n</ul>\n<pre><code>def add_longrange_features(X, center_x, center_y, fps,\n                          long_windows=None, ewm_spans=None, pct_windows=None):\n    if long_windows is None:\n        long_windows = [30, 60, 120, 240, 480]\n    if ewm_spans is None:\n        ewm_spans = [15, 30, 60, 120, 240]\n    for window in long_windows:\n        ws = _scale(window, fps)\n        X[f'x_ml{window}'] = center_x.rolling(ws, min_periods=max(5, ws // 6)).mean()\n    for span in ewm_spans:\n        s = _scale(span, fps)\n        X[f'x_e{span}'] = center_x.ewm(span=s, min_periods=1).mean()\n\ndef transform_single(...):\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']; cy = single_mouse['body_center']['y']\n        ...\n        X = add_longrange_features(X, cx, cy, fps)\n        X = add_cumulative_distance_single(X, cx, cy, fps, horizon_frames_base=180)\n        X = add_groom_microfeatures(X, single_mouse, fps)\n        X = add_speed_asymmetry_future_past_single(X, cx, cy, fps, horizon_base=30)\n        X = add_gauss_shift_speed_future_past_single(X, cx, cy, fps, window_base=30)\n        X = add_segmental_features_single(X, cx, cy, fps, horizons_base=(30, 90))\n</code></pre>\n<ul>\n<li><strong>Multi-scale speed &amp; lags</strong>: <code>transform_single</code> now iterates <code>speed_lags = [1,2,3,4,5,10,20,30,40,50,60]</code>, computing squared displacements for <code>ear_left/right</code> and <code>tail_base</code> (see <code>mabe-final.py:1194-1225</code>). <code>_scale(lag_base, fps)</code> converts each lag to a real time span, covering both 1–2 frame micro stops and &gt;2 s runs. To maintain backward compatibility, the 10-frame outputs remain exposed as <code>sp_lf</code>, <code>sp_rt</code>, etc.</li>\n<li><strong>Nose–tail lagged deltas</strong>: <code>nt_lg{10,20,40}</code> and <code>nt_df{10,20,40}</code> reuse the FPS-aware shifts (<code>mabe-final.py:1266-1274</code>), so stretch/contract cues stay stable across frame rates—coverage the baseline lacked.</li>\n</ul>\n<h4>5. Pair Interaction Enhancements + Context Ratios</h4>\n<ul>\n<li><code>transform_pair</code> enriches lagged inter-mouse speeds, head orientation bins, nose/ear wedge angles, segmented distance stats, radial/tangential nose velocities, rolling contact ratios, and velocity alignment offsets.</li>\n<li>CTX metrics are normalized by pairwise distance to remove raw-scale bias.</li>\n</ul>\n<pre><code>def transform_pair(mouse_pair, body_parts_tracked, fps):\n    # head alignment &amp; facing bins\n    X['head_parallel'] = (head_cos &gt; 0.5).astype(float)\n    X['A_face_front'] = (X['A_face_cos'] &gt; 0.7).astype(float)\n    ...\n    # nose dynamics\n    X['nn_rad_sp'] = (rv * float(fps)).fillna(0)\n    X['nn_lat_sp'] = (lv * float(fps)).fillna(0)\n    X[f'nn_ct{int(thr)}_{w}'] = (nn &lt; thr).rolling(ws, **roll_opts).mean()\n    # velocity alignment offsets\n    for off in [-20, -10, 10, 20]:\n        o = _scale_signed(off, fps)\n        X[f'va_{off}'] = val.shift(-o)\n    # CTX ratios\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / (cd + 1e-6)\n</code></pre>\n<ul>\n<li><strong>Multi-lag motion &amp; approach cues</strong>: Pair features reuse <code>speed_lags = [1,2,3,4,5,10,20,30,40,50,60]</code> (<code>mabe-final.py:1302-1325</code>) to output <code>sp_A_l{lag}</code>, <code>sp_AB_l{lag}</code>, <code>sp_B_l{lag}</code>, separating agent, target, and cross-body displacement intensity over short/long horizons. Nose-to-nose distance now spans lag=1–100 via <code>nn_lg*</code>, <code>nn_ch*</code>, <code>cl_ps*</code>, and <code>nose_spd*_lg*</code> (<code>mabe-final.py:1430-1466</code>), capturing “approach → brief contact → disengage” rhythms that the 10/20/40-frame baseline missed.</li>\n</ul>\n<h4>6. Train/Test Consistency &amp; Non-finite Logging</h4>\n<ul>\n<li>Training and inference both apply <code>augment_with_meta_features</code> + <code>sanitize_features</code>, and <code>log_non_finite_features</code> records any NaN/Inf columns together with their runtime context.</li>\n<li>Sampling keeps meta/labels aligned with feature slices; inference reindexes by <code>feature_columns</code> and zero-fills unseen feature names.</li>\n</ul>\n<pre><code># training stage\nXi_full = transform_single(data_i, body_parts_tracked, fps_i).astype(np.float32)\nXi_full = augment_with_meta_features(Xi_full, meta_i).astype(np.float32, copy=False)\nlog_non_finite_features(\n    Xi_full,\n    f\"train|single|section={section}|n_parts={len(body_parts_tracked)}|{_meta_summary(meta_i)}\"\n)\n\n# inference stage\nX_te = transform_pair(data_te, body_parts_tracked, fps_i)\nX_te = augment_with_meta_features(X_te, meta_te)\nif feature_columns:\n    X_te = (X_te.reindex(columns=feature_columns, fill_value=0.0)\n                .astype(np.float32, copy=False))\nlog_non_finite_features(X_te, f\"test|{switch_te}|...\")\n</code></pre>\n<h4>7. Adaptive Threshold Tuning &amp; Inference Strategy</h4>\n<ul>\n<li><code>submit_ensemble</code> searches per-action thresholds on GroupKFold OOF predictions via Optuna and persists the Joblib dumps into <code>THRESHOLD_DIR</code> for reuse.</li>\n<li><code>predict_multiclass_adaptive</code> smooths predictions, masks them by tuned thresholds, argmaxes valid classes, and smooths segments again—more robust to class imbalance than the fixed 0.27 baseline.</li>\n</ul>\n<pre><code># submit_ensemble excerpt\nfor action in label.columns:\n    ...\n    oof_pred = cross_val_predict(\n        base_model, X_tune, y_tune, cv=splits,\n        groups=groups_tune, method=\"predict_proba\"\n    )[:, 1]\n    tuned_thr = tune_threshold(oof_pred, y_tune)\n    action_thresholds[action] = tuned_thr\njoblib.dump(dict(action_thresholds), thr_path)\n\n# predict_multiclass_adaptive\npred_smoothed = pred.rolling(window=7, min_periods=1, center=True).mean()\nthresholds = np.array([action_thresholds.get(action, 0.27) for action in pred_smoothed.columns])\nvalid = pred_smoothed.values &gt;= thresholds\nmasked = np.where(valid, pred_smoothed.values, -np.inf)\nama = masked.argmax(axis=1)\nall_invalid = ~valid.any(axis=1)\nama[all_invalid] = -1\n</code></pre>\n<p>Each upgrade is either missing or heavily simplified in the baseline, and together they explain most of the final solution’s lift. Clear snippets and descriptions make it easier to audit, reproduce, or extend the approach.</p>",
  "messages": [
    {
      "id": 3377425,
      "postDate": "2025-12-16T09:50:42.223Z",
      "content": "<h1>kaggle-MABe</h1>\n<p><a href=\"https://github.com/Jayzen-Tech/kaggle-MABe\" target=\"_blank\">github kaggle-MABe</a></p>\n<h2>Table of Contents</h2>\n<ul>\n<li><a href=\"#directory-layout--required-data\">Directory Layout &amp; Required Data</a></li>\n<li><a href=\"#how-to-run--switches\">How to Run &amp; Switches</a></li>\n<li><a href=\"#feature-engineering-upgrades-overview\">Feature Engineering Upgrades Overview</a></li>\n<li><a href=\"#1-tracking-cleanup--pixel-normalization\">Tracking Cleanup &amp; Pixel Normalization</a></li>\n<li><a href=\"#2-meta-encoding--feature-augmentation\">Meta Encoding &amp; Feature Augmentation</a></li>\n<li><a href=\"#3-third-mouse-context-triadic-relations\">Third-Mouse Context</a></li>\n<li><a href=\"#4-single-mouse-advanced-motion-long-horizon--pastfuture\">Single-Mouse Advanced Motion</a></li>\n<li><a href=\"#5-pair-interaction-enhancements--context-ratios\">Pair Interaction Enhancements</a></li>\n<li><a href=\"#6-traintest-consistency--non-finite-logging\">Train/Test Consistency</a></li>\n<li><a href=\"#7-adaptive-threshold-tuning--inference-strategy\">Adaptive Threshold Strategy</a></li>\n</ul>\n<h3>Directory Layout &amp; Required Data</h3>\n<p>The project uses <code>/kaggle/kaggle-MABe</code> as the working directory and depends on external Kaggle inputs. Core folders:</p>\n<pre><code>/kaggle/kaggle-MABe/\n├── mabe-final.py                # Final solution script\n├── baseline/mabe-baseline.py    # Initial baseline\n├── README.md, requirements.txt\n└── __pycache__/\n</code></pre>\n<p>External data and pretrained artifacts live under <code>/kaggle/input</code>, primarily:</p>\n<pre><code>/kaggle/input/\n└── MABe-mouse-behavior-detection/        # Official train/test CSVs and tracking parquet files\n</code></pre>\n<p>If you plan to reuse pretrained models/thresholds through <code>LOAD_MODELS=True</code> or <code>LOAD_THRESHOLDS=True</code>, keep the default folder structure or update the <code>MODEL_LOAD_DIR</code> / <code>THRESHOLD_LOAD_DIR</code> constants inside the script before running.</p>\n<h3>How to Run &amp; Switches</h3>\n<h4>Basic Commands</h4>\n<ol>\n<li>(Optional on first run) Install dependencies: <code>pip install -r requirements.txt</code></li>\n<li>Execute the pipeline: <code>python mabe-final.py</code></li>\n</ol>\n<p>Runtime switches at the top of <code>mabe-final.py</code> control training/inference behavior:</p>\n<pre><code># ---- runtime switches ----\nONLY_TUNE_THRESHOLDS = False    # True: tune thresholds only and exit (skip training/inference/submission)\nUSE_ADAPTIVE_THRESHOLDS = True  # False: fallback to a fixed 0.27 threshold for every action\nLOAD_THRESHOLDS = False         # True: read thresholds from THRESHOLD_DIR instead of tuning\nLOAD_MODELS = False             # True: load pretrained models from MODEL_DIR\nCHECK_LOAD = False              # True: after loading, drop actions without positives in the current folds\nTHRESHOLD_DIR = \"./models/threshold\"\nMODEL_DIR = \"./models\"\nTHRESHOLD_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13/threshold\"\nMODEL_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13\"\nNON_FINITE_LOG = \"non_finite_features.log\"\n</code></pre>\n<p><strong>Recommended usage:</strong></p>\n<ul>\n<li><strong>Full training + inference</strong>: <code>ONLY_TUNE_THRESHOLDS=False</code>, <code>LOAD_MODELS=False</code>. The script trains, predicts, and saves models/thresholds under <code>MODEL_DIR</code> / <code>THRESHOLD_DIR</code>.</li>\n<li><strong>Threshold tuning only</strong>: When models already exist, set <code>ONLY_TUNE_THRESHOLDS=True</code>. The run stops after tuning and writes fresh pickles into <code>THRESHOLD_DIR</code>.</li>\n<li><strong>Reuse trained models</strong>: Set <code>LOAD_MODELS=True</code> (optionally <code>CHECK_LOAD=True</code> to verify label availability). The script reads Joblib caches from <code>MODEL_LOAD_DIR</code>; update the path if your models live elsewhere.</li>\n<li><strong>Reuse tuned thresholds</strong>: Flip <code>LOAD_THRESHOLDS=True</code> to skip tuning and read <code>.pkl</code> files from <code>THRESHOLD_LOAD_DIR</code>.</li>\n<li>Disable adaptive thresholds (<code>USE_ADAPTIVE_THRESHOLDS=False</code>) to fall back to the global 0.27 cutoff for quick sanity checks.</li>\n<li><code>NON_FINITE_LOG</code> records any NaN/Inf columns encountered during training or inference. Point it to another path or truncate it as needed.</li>\n</ul>\n<h3>Feature Engineering Upgrades Overview</h3>\n<p><code>mabe-final.py</code> delivers a full feature-engineering refresh covering tracking cleanup, meta encoding, third-mouse context, single/pair motion modeling, and consistent train/test handling. Each subsection below highlights the additions versus the baseline and quotes the relevant code (all snippets originate from <code>mabe-final.py</code>).</p>\n<h4>1. Tracking Cleanup &amp; Pixel Normalization</h4>\n<ul>\n<li>Reorder the multi-level columns and convert pixels to centimeters using <code>pix_per_cm_approx</code>.</li>\n<li>Interpolate every body part, fill in both directions, and drop joints that remain all NaN.</li>\n<li>Run all downstream matrices through <code>sanitize_features</code> to replace NaN/Inf with interpolations and per-column medians.</li>\n</ul>\n<pre><code># mabe-final.py (generate_mouse_data)\npvid = vid.pivot(columns=['mouse_id','bodypart'], index='video_frame', values=['x','y'])\npvid = pvid.reorder_levels([1,2,0], axis=1).T.sort_index().T\npvid = (pvid / float(row.pix_per_cm_approx)).astype('float32', copy=False)\npvid = pvid.sort_index(axis=1)\npvid = pvid.interpolate(limit_direction='both')\npvid = pvid.fillna(method='bfill').fillna(method='ffill')\nall_nan_cols = pvid.columns[pvid.isna().all()]\nif len(all_nan_cols) &gt; 0:\n    pvid = pvid.drop(columns=all_nan_cols)\n\n# send every feature matrix through sanitize_features\ndef sanitize_features(X: Optional[pd.DataFrame]) -&gt; pd.DataFrame:\n    X = X.replace([np.inf, -np.inf], np.nan)\n    X = X.interpolate(axis=0, limit_direction='both')\n    if X.isna().any().any():\n        med = X.median(axis=0)\n        X = X.fillna(med)\n    if X.isna().any().any():\n        X = X.fillna(0.0)\n    return X\n</code></pre>\n<h4>2. Meta Encoding &amp; Feature Augmentation</h4>\n<ul>\n<li><code>_mk_meta</code> parses agent/target sex and strain, builds integer codes and same-sex/strain indicators, computes arena area/aspect/shape, and aligns meta rows with frame indices.</li>\n<li><code>augment_with_meta_features</code> injects these meta columns into single/pair feature matrices and uses identical sanitization/logging during inference.</li>\n</ul>\n<pre><code># mabe-final.py (_mk_meta excerpt)\nm['agent_sex_code'] = np.float32(agent_sex_code)\nm['target_sex_code'] = np.float32(target_sex_code if target_key is not None else agent_sex_code)\nm['agent_strain_code'] = np.float32(agent_strain_code)\nm['target_strain_code'] = np.float32(target_strain_code if target_key is not None else agent_strain_code)\nm['same_sex'] = np.float32(1.0 if (...) else 0.0)\nm['same_strain'] = np.float32(1.0 if (...) else 0.0)\nm['arena_shape_code'] = np.float32(arena_shape_code)\nm['arena_area_cm2'] = np.float32(arena_area)\nm['arena_aspect_ratio'] = np.float32(arena_aspect)\nm = m.set_index('video_frame')\nm['video_frame'] = m.index\n\ndef augment_with_meta_features(X: pd.DataFrame, meta: Optional[pd.DataFrame]) -&gt; pd.DataFrame:\n    meta_aligned = meta.reindex(X.index)\n    meta_features = pd.DataFrame(index=X.index)\n    for raw, name in [...]:\n        _copy(raw, name)\n    meta_features = sanitize_features(meta_features)\n    return pd.concat([X, meta_features], axis=1)\n</code></pre>\n<h4>3. Third-Mouse Context (Triadic Relations)</h4>\n<ul>\n<li><code>_compute_third_party_context</code> converts every non agent/target mouse into distance stats against agent/target/midpoint, contact counts, weaving ratios, and cosine directionality.</li>\n<li><code>transform_pair</code> flattens the CTX MultiIndex and adds ratios (for example <code>ctx_third_min_dist_*_ratio</code>) so the model learns how third-party interference modulates behaviors.</li>\n</ul>\n<pre><code>def _compute_third_party_context(pvid: pd.DataFrame, agent_label, target_label) -&gt; Optional[pd.DataFrame]:\n    ctx = pd.DataFrame(index=agent_bc.index)\n    ctx['third_min_dist_agent'] = df_a.min(axis=1)\n    ctx['third_mean_dist_agent'] = df_a.mean(axis=1)\n    ctx['third_close_agent_cnt'] = (df_a &lt; close_thresh).sum(axis=1)\n    ctx['third_close_any'] = (close_any &gt; 0).astype(float)\n    ctx['third_between_ratio'] = (df_a + df_b).min(axis=1) / (dist_at + 1e-6)\n    ctx['third_cos_agent_closest'] = closest\n    ctx.columns = pd.MultiIndex.from_arrays([['CTX'] * len(ctx_cols), ctx_cols, ['value'] * len(ctx_cols)])\n    return ctx\n\n# transform_pair excerpt\nif 'CTX' in mouse_pair.columns.get_level_values(0):\n    ctx_flat = mouse_pair['CTX'].copy()\n    ...\n    X[col] = ctx_flat[col]\nif cd is not None:\n    denom = (cd + 1e-6)\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / denom\n</code></pre>\n<h4>4. Single-Mouse Advanced Motion (Long Horizon + Past/Future)</h4>\n<ul>\n<li>When <code>body_center</code> is available, <code>transform_single</code> composes <code>add_longrange_features</code>, curvature, future-vs-past speed asymmetry, Gaussian-shift comparisons, and segment-level metrics.</li>\n<li><code>add_longrange_features</code> mixes configurable windows/EWM/percentiles, and the added operators cover future/past velocity contrasts, KL-style speed shifts, and segment-wise acceleration/jerk/pause ratios.</li>\n</ul>\n<pre><code>def add_longrange_features(X, center_x, center_y, fps,\n                          long_windows=None, ewm_spans=None, pct_windows=None):\n    if long_windows is None:\n        long_windows = [30, 60, 120, 240, 480]\n    if ewm_spans is None:\n        ewm_spans = [15, 30, 60, 120, 240]\n    for window in long_windows:\n        ws = _scale(window, fps)\n        X[f'x_ml{window}'] = center_x.rolling(ws, min_periods=max(5, ws // 6)).mean()\n    for span in ewm_spans:\n        s = _scale(span, fps)\n        X[f'x_e{span}'] = center_x.ewm(span=s, min_periods=1).mean()\n\ndef transform_single(...):\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']; cy = single_mouse['body_center']['y']\n        ...\n        X = add_longrange_features(X, cx, cy, fps)\n        X = add_cumulative_distance_single(X, cx, cy, fps, horizon_frames_base=180)\n        X = add_groom_microfeatures(X, single_mouse, fps)\n        X = add_speed_asymmetry_future_past_single(X, cx, cy, fps, horizon_base=30)\n        X = add_gauss_shift_speed_future_past_single(X, cx, cy, fps, window_base=30)\n        X = add_segmental_features_single(X, cx, cy, fps, horizons_base=(30, 90))\n</code></pre>\n<ul>\n<li><strong>Multi-scale speed &amp; lags</strong>: <code>transform_single</code> now iterates <code>speed_lags = [1,2,3,4,5,10,20,30,40,50,60]</code>, computing squared displacements for <code>ear_left/right</code> and <code>tail_base</code> (see <code>mabe-final.py:1194-1225</code>). <code>_scale(lag_base, fps)</code> converts each lag to a real time span, covering both 1–2 frame micro stops and &gt;2 s runs. To maintain backward compatibility, the 10-frame outputs remain exposed as <code>sp_lf</code>, <code>sp_rt</code>, etc.</li>\n<li><strong>Nose–tail lagged deltas</strong>: <code>nt_lg{10,20,40}</code> and <code>nt_df{10,20,40}</code> reuse the FPS-aware shifts (<code>mabe-final.py:1266-1274</code>), so stretch/contract cues stay stable across frame rates—coverage the baseline lacked.</li>\n</ul>\n<h4>5. Pair Interaction Enhancements + Context Ratios</h4>\n<ul>\n<li><code>transform_pair</code> enriches lagged inter-mouse speeds, head orientation bins, nose/ear wedge angles, segmented distance stats, radial/tangential nose velocities, rolling contact ratios, and velocity alignment offsets.</li>\n<li>CTX metrics are normalized by pairwise distance to remove raw-scale bias.</li>\n</ul>\n<pre><code>def transform_pair(mouse_pair, body_parts_tracked, fps):\n    # head alignment &amp; facing bins\n    X['head_parallel'] = (head_cos &gt; 0.5).astype(float)\n    X['A_face_front'] = (X['A_face_cos'] &gt; 0.7).astype(float)\n    ...\n    # nose dynamics\n    X['nn_rad_sp'] = (rv * float(fps)).fillna(0)\n    X['nn_lat_sp'] = (lv * float(fps)).fillna(0)\n    X[f'nn_ct{int(thr)}_{w}'] = (nn &lt; thr).rolling(ws, **roll_opts).mean()\n    # velocity alignment offsets\n    for off in [-20, -10, 10, 20]:\n        o = _scale_signed(off, fps)\n        X[f'va_{off}'] = val.shift(-o)\n    # CTX ratios\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / (cd + 1e-6)\n</code></pre>\n<ul>\n<li><strong>Multi-lag motion &amp; approach cues</strong>: Pair features reuse <code>speed_lags = [1,2,3,4,5,10,20,30,40,50,60]</code> (<code>mabe-final.py:1302-1325</code>) to output <code>sp_A_l{lag}</code>, <code>sp_AB_l{lag}</code>, <code>sp_B_l{lag}</code>, separating agent, target, and cross-body displacement intensity over short/long horizons. Nose-to-nose distance now spans lag=1–100 via <code>nn_lg*</code>, <code>nn_ch*</code>, <code>cl_ps*</code>, and <code>nose_spd*_lg*</code> (<code>mabe-final.py:1430-1466</code>), capturing “approach → brief contact → disengage” rhythms that the 10/20/40-frame baseline missed.</li>\n</ul>\n<h4>6. Train/Test Consistency &amp; Non-finite Logging</h4>\n<ul>\n<li>Training and inference both apply <code>augment_with_meta_features</code> + <code>sanitize_features</code>, and <code>log_non_finite_features</code> records any NaN/Inf columns together with their runtime context.</li>\n<li>Sampling keeps meta/labels aligned with feature slices; inference reindexes by <code>feature_columns</code> and zero-fills unseen feature names.</li>\n</ul>\n<pre><code># training stage\nXi_full = transform_single(data_i, body_parts_tracked, fps_i).astype(np.float32)\nXi_full = augment_with_meta_features(Xi_full, meta_i).astype(np.float32, copy=False)\nlog_non_finite_features(\n    Xi_full,\n    f\"train|single|section={section}|n_parts={len(body_parts_tracked)}|{_meta_summary(meta_i)}\"\n)\n\n# inference stage\nX_te = transform_pair(data_te, body_parts_tracked, fps_i)\nX_te = augment_with_meta_features(X_te, meta_te)\nif feature_columns:\n    X_te = (X_te.reindex(columns=feature_columns, fill_value=0.0)\n                .astype(np.float32, copy=False))\nlog_non_finite_features(X_te, f\"test|{switch_te}|...\")\n</code></pre>\n<h4>7. Adaptive Threshold Tuning &amp; Inference Strategy</h4>\n<ul>\n<li><code>submit_ensemble</code> searches per-action thresholds on GroupKFold OOF predictions via Optuna and persists the Joblib dumps into <code>THRESHOLD_DIR</code> for reuse.</li>\n<li><code>predict_multiclass_adaptive</code> smooths predictions, masks them by tuned thresholds, argmaxes valid classes, and smooths segments again—more robust to class imbalance than the fixed 0.27 baseline.</li>\n</ul>\n<pre><code># submit_ensemble excerpt\nfor action in label.columns:\n    ...\n    oof_pred = cross_val_predict(\n        base_model, X_tune, y_tune, cv=splits,\n        groups=groups_tune, method=\"predict_proba\"\n    )[:, 1]\n    tuned_thr = tune_threshold(oof_pred, y_tune)\n    action_thresholds[action] = tuned_thr\njoblib.dump(dict(action_thresholds), thr_path)\n\n# predict_multiclass_adaptive\npred_smoothed = pred.rolling(window=7, min_periods=1, center=True).mean()\nthresholds = np.array([action_thresholds.get(action, 0.27) for action in pred_smoothed.columns])\nvalid = pred_smoothed.values &gt;= thresholds\nmasked = np.where(valid, pred_smoothed.values, -np.inf)\nama = masked.argmax(axis=1)\nall_invalid = ~valid.any(axis=1)\nama[all_invalid] = -1\n</code></pre>\n<p>Each upgrade is either missing or heavily simplified in the baseline, and together they explain most of the final solution’s lift. Clear snippets and descriptions make it easier to audit, reproduce, or extend the approach.</p>",
      "rawMarkdown": "# kaggle-MABe\n\n[github kaggle-MABe](https://github.com/Jayzen-Tech/kaggle-MABe)\n\n## Table of Contents\n- [Directory Layout & Required Data](#directory-layout--required-data)\n- [How to Run & Switches](#how-to-run--switches)\n- [Feature Engineering Upgrades Overview](#feature-engineering-upgrades-overview)\n- [Tracking Cleanup & Pixel Normalization](#1-tracking-cleanup--pixel-normalization)\n- [Meta Encoding & Feature Augmentation](#2-meta-encoding--feature-augmentation)\n- [Third-Mouse Context](#3-third-mouse-context-triadic-relations)\n- [Single-Mouse Advanced Motion](#4-single-mouse-advanced-motion-long-horizon--pastfuture)\n- [Pair Interaction Enhancements](#5-pair-interaction-enhancements--context-ratios)\n- [Train/Test Consistency](#6-traintest-consistency--non-finite-logging)\n- [Adaptive Threshold Strategy](#7-adaptive-threshold-tuning--inference-strategy)\n\n### Directory Layout & Required Data\nThe project uses `/kaggle/kaggle-MABe` as the working directory and depends on external Kaggle inputs. Core folders:\n\n```\n/kaggle/kaggle-MABe/\n├── mabe-final.py                # Final solution script\n├── baseline/mabe-baseline.py    # Initial baseline\n├── README.md, requirements.txt\n└── __pycache__/\n```\n\nExternal data and pretrained artifacts live under `/kaggle/input`, primarily:\n\n```\n/kaggle/input/\n└── MABe-mouse-behavior-detection/        # Official train/test CSVs and tracking parquet files\n```\n\nIf you plan to reuse pretrained models/thresholds through `LOAD_MODELS=True` or `LOAD_THRESHOLDS=True`, keep the default folder structure or update the `MODEL_LOAD_DIR` / `THRESHOLD_LOAD_DIR` constants inside the script before running.\n\n### How to Run & Switches\n\n#### Basic Commands\n1. (Optional on first run) Install dependencies: `pip install -r requirements.txt`\n2. Execute the pipeline: `python mabe-final.py`\n\nRuntime switches at the top of `mabe-final.py` control training/inference behavior:\n\n```python\n# ---- runtime switches ----\nONLY_TUNE_THRESHOLDS = False    # True: tune thresholds only and exit (skip training/inference/submission)\nUSE_ADAPTIVE_THRESHOLDS = True  # False: fallback to a fixed 0.27 threshold for every action\nLOAD_THRESHOLDS = False         # True: read thresholds from THRESHOLD_DIR instead of tuning\nLOAD_MODELS = False             # True: load pretrained models from MODEL_DIR\nCHECK_LOAD = False              # True: after loading, drop actions without positives in the current folds\nTHRESHOLD_DIR = \"./models/threshold\"\nMODEL_DIR = \"./models\"\nTHRESHOLD_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13/threshold\"\nMODEL_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13\"\nNON_FINITE_LOG = \"non_finite_features.log\"\n```\n\n**Recommended usage:**\n- **Full training + inference**: `ONLY_TUNE_THRESHOLDS=False`, `LOAD_MODELS=False`. The script trains, predicts, and saves models/thresholds under `MODEL_DIR` / `THRESHOLD_DIR`.\n- **Threshold tuning only**: When models already exist, set `ONLY_TUNE_THRESHOLDS=True`. The run stops after tuning and writes fresh pickles into `THRESHOLD_DIR`.\n- **Reuse trained models**: Set `LOAD_MODELS=True` (optionally `CHECK_LOAD=True` to verify label availability). The script reads Joblib caches from `MODEL_LOAD_DIR`; update the path if your models live elsewhere.\n- **Reuse tuned thresholds**: Flip `LOAD_THRESHOLDS=True` to skip tuning and read `.pkl` files from `THRESHOLD_LOAD_DIR`.\n- Disable adaptive thresholds (`USE_ADAPTIVE_THRESHOLDS=False`) to fall back to the global 0.27 cutoff for quick sanity checks.\n- `NON_FINITE_LOG` records any NaN/Inf columns encountered during training or inference. Point it to another path or truncate it as needed.\n\n### Feature Engineering Upgrades Overview\n`mabe-final.py` delivers a full feature-engineering refresh covering tracking cleanup, meta encoding, third-mouse context, single/pair motion modeling, and consistent train/test handling. Each subsection below highlights the additions versus the baseline and quotes the relevant code (all snippets originate from `mabe-final.py`).\n\n#### 1. Tracking Cleanup & Pixel Normalization\n- Reorder the multi-level columns and convert pixels to centimeters using `pix_per_cm_approx`.\n- Interpolate every body part, fill in both directions, and drop joints that remain all NaN.\n- Run all downstream matrices through `sanitize_features` to replace NaN/Inf with interpolations and per-column medians.\n\n```python\n# mabe-final.py (generate_mouse_data)\npvid = vid.pivot(columns=['mouse_id','bodypart'], index='video_frame', values=['x','y'])\npvid = pvid.reorder_levels([1,2,0], axis=1).T.sort_index().T\npvid = (pvid / float(row.pix_per_cm_approx)).astype('float32', copy=False)\npvid = pvid.sort_index(axis=1)\npvid = pvid.interpolate(limit_direction='both')\npvid = pvid.fillna(method='bfill').fillna(method='ffill')\nall_nan_cols = pvid.columns[pvid.isna().all()]\nif len(all_nan_cols) > 0:\n    pvid = pvid.drop(columns=all_nan_cols)\n\n# send every feature matrix through sanitize_features\ndef sanitize_features(X: Optional[pd.DataFrame]) -> pd.DataFrame:\n    X = X.replace([np.inf, -np.inf], np.nan)\n    X = X.interpolate(axis=0, limit_direction='both')\n    if X.isna().any().any():\n        med = X.median(axis=0)\n        X = X.fillna(med)\n    if X.isna().any().any():\n        X = X.fillna(0.0)\n    return X\n```\n\n#### 2. Meta Encoding & Feature Augmentation\n- `_mk_meta` parses agent/target sex and strain, builds integer codes and same-sex/strain indicators, computes arena area/aspect/shape, and aligns meta rows with frame indices.\n- `augment_with_meta_features` injects these meta columns into single/pair feature matrices and uses identical sanitization/logging during inference.\n\n```python\n# mabe-final.py (_mk_meta excerpt)\nm['agent_sex_code'] = np.float32(agent_sex_code)\nm['target_sex_code'] = np.float32(target_sex_code if target_key is not None else agent_sex_code)\nm['agent_strain_code'] = np.float32(agent_strain_code)\nm['target_strain_code'] = np.float32(target_strain_code if target_key is not None else agent_strain_code)\nm['same_sex'] = np.float32(1.0 if (...) else 0.0)\nm['same_strain'] = np.float32(1.0 if (...) else 0.0)\nm['arena_shape_code'] = np.float32(arena_shape_code)\nm['arena_area_cm2'] = np.float32(arena_area)\nm['arena_aspect_ratio'] = np.float32(arena_aspect)\nm = m.set_index('video_frame')\nm['video_frame'] = m.index\n\ndef augment_with_meta_features(X: pd.DataFrame, meta: Optional[pd.DataFrame]) -> pd.DataFrame:\n    meta_aligned = meta.reindex(X.index)\n    meta_features = pd.DataFrame(index=X.index)\n    for raw, name in [...]:\n        _copy(raw, name)\n    meta_features = sanitize_features(meta_features)\n    return pd.concat([X, meta_features], axis=1)\n```\n\n#### 3. Third-Mouse Context (Triadic Relations)\n- `_compute_third_party_context` converts every non agent/target mouse into distance stats against agent/target/midpoint, contact counts, weaving ratios, and cosine directionality.\n- `transform_pair` flattens the CTX MultiIndex and adds ratios (for example `ctx_third_min_dist_*_ratio`) so the model learns how third-party interference modulates behaviors.\n\n```python\ndef _compute_third_party_context(pvid: pd.DataFrame, agent_label, target_label) -> Optional[pd.DataFrame]:\n    ctx = pd.DataFrame(index=agent_bc.index)\n    ctx['third_min_dist_agent'] = df_a.min(axis=1)\n    ctx['third_mean_dist_agent'] = df_a.mean(axis=1)\n    ctx['third_close_agent_cnt'] = (df_a < close_thresh).sum(axis=1)\n    ctx['third_close_any'] = (close_any > 0).astype(float)\n    ctx['third_between_ratio'] = (df_a + df_b).min(axis=1) / (dist_at + 1e-6)\n    ctx['third_cos_agent_closest'] = closest\n    ctx.columns = pd.MultiIndex.from_arrays([['CTX'] * len(ctx_cols), ctx_cols, ['value'] * len(ctx_cols)])\n    return ctx\n\n# transform_pair excerpt\nif 'CTX' in mouse_pair.columns.get_level_values(0):\n    ctx_flat = mouse_pair['CTX'].copy()\n    ...\n    X[col] = ctx_flat[col]\nif cd is not None:\n    denom = (cd + 1e-6)\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / denom\n```\n\n#### 4. Single-Mouse Advanced Motion (Long Horizon + Past/Future)\n- When `body_center` is available, `transform_single` composes `add_longrange_features`, curvature, future-vs-past speed asymmetry, Gaussian-shift comparisons, and segment-level metrics.\n- `add_longrange_features` mixes configurable windows/EWM/percentiles, and the added operators cover future/past velocity contrasts, KL-style speed shifts, and segment-wise acceleration/jerk/pause ratios.\n\n```python\ndef add_longrange_features(X, center_x, center_y, fps,\n                          long_windows=None, ewm_spans=None, pct_windows=None):\n    if long_windows is None:\n        long_windows = [30, 60, 120, 240, 480]\n    if ewm_spans is None:\n        ewm_spans = [15, 30, 60, 120, 240]\n    for window in long_windows:\n        ws = _scale(window, fps)\n        X[f'x_ml{window}'] = center_x.rolling(ws, min_periods=max(5, ws // 6)).mean()\n    for span in ewm_spans:\n        s = _scale(span, fps)\n        X[f'x_e{span}'] = center_x.ewm(span=s, min_periods=1).mean()\n\ndef transform_single(...):\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']; cy = single_mouse['body_center']['y']\n        ...\n        X = add_longrange_features(X, cx, cy, fps)\n        X = add_cumulative_distance_single(X, cx, cy, fps, horizon_frames_base=180)\n        X = add_groom_microfeatures(X, single_mouse, fps)\n        X = add_speed_asymmetry_future_past_single(X, cx, cy, fps, horizon_base=30)\n        X = add_gauss_shift_speed_future_past_single(X, cx, cy, fps, window_base=30)\n        X = add_segmental_features_single(X, cx, cy, fps, horizons_base=(30, 90))\n```\n\n- **Multi-scale speed & lags**: `transform_single` now iterates `speed_lags = [1,2,3,4,5,10,20,30,40,50,60]`, computing squared displacements for `ear_left/right` and `tail_base` (see `mabe-final.py:1194-1225`). `_scale(lag_base, fps)` converts each lag to a real time span, covering both 1–2 frame micro stops and >2 s runs. To maintain backward compatibility, the 10-frame outputs remain exposed as `sp_lf`, `sp_rt`, etc.\n- **Nose–tail lagged deltas**: `nt_lg{10,20,40}` and `nt_df{10,20,40}` reuse the FPS-aware shifts (`mabe-final.py:1266-1274`), so stretch/contract cues stay stable across frame rates—coverage the baseline lacked.\n\n#### 5. Pair Interaction Enhancements + Context Ratios\n- `transform_pair` enriches lagged inter-mouse speeds, head orientation bins, nose/ear wedge angles, segmented distance stats, radial/tangential nose velocities, rolling contact ratios, and velocity alignment offsets.\n- CTX metrics are normalized by pairwise distance to remove raw-scale bias.\n\n```python\ndef transform_pair(mouse_pair, body_parts_tracked, fps):\n    # head alignment & facing bins\n    X['head_parallel'] = (head_cos > 0.5).astype(float)\n    X['A_face_front'] = (X['A_face_cos'] > 0.7).astype(float)\n    ...\n    # nose dynamics\n    X['nn_rad_sp'] = (rv * float(fps)).fillna(0)\n    X['nn_lat_sp'] = (lv * float(fps)).fillna(0)\n    X[f'nn_ct{int(thr)}_{w}'] = (nn < thr).rolling(ws, **roll_opts).mean()\n    # velocity alignment offsets\n    for off in [-20, -10, 10, 20]:\n        o = _scale_signed(off, fps)\n        X[f'va_{off}'] = val.shift(-o)\n    # CTX ratios\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / (cd + 1e-6)\n```\n\n- **Multi-lag motion & approach cues**: Pair features reuse `speed_lags = [1,2,3,4,5,10,20,30,40,50,60]` (`mabe-final.py:1302-1325`) to output `sp_A_l{lag}`, `sp_AB_l{lag}`, `sp_B_l{lag}`, separating agent, target, and cross-body displacement intensity over short/long horizons. Nose-to-nose distance now spans lag=1–100 via `nn_lg*`, `nn_ch*`, `cl_ps*`, and `nose_spd*_lg*` (`mabe-final.py:1430-1466`), capturing “approach → brief contact → disengage” rhythms that the 10/20/40-frame baseline missed.\n\n#### 6. Train/Test Consistency & Non-finite Logging\n- Training and inference both apply `augment_with_meta_features` + `sanitize_features`, and `log_non_finite_features` records any NaN/Inf columns together with their runtime context.\n- Sampling keeps meta/labels aligned with feature slices; inference reindexes by `feature_columns` and zero-fills unseen feature names.\n\n```python\n# training stage\nXi_full = transform_single(data_i, body_parts_tracked, fps_i).astype(np.float32)\nXi_full = augment_with_meta_features(Xi_full, meta_i).astype(np.float32, copy=False)\nlog_non_finite_features(\n    Xi_full,\n    f\"train|single|section={section}|n_parts={len(body_parts_tracked)}|{_meta_summary(meta_i)}\"\n)\n\n# inference stage\nX_te = transform_pair(data_te, body_parts_tracked, fps_i)\nX_te = augment_with_meta_features(X_te, meta_te)\nif feature_columns:\n    X_te = (X_te.reindex(columns=feature_columns, fill_value=0.0)\n                .astype(np.float32, copy=False))\nlog_non_finite_features(X_te, f\"test|{switch_te}|...\")\n```\n\n#### 7. Adaptive Threshold Tuning & Inference Strategy\n- `submit_ensemble` searches per-action thresholds on GroupKFold OOF predictions via Optuna and persists the Joblib dumps into `THRESHOLD_DIR` for reuse.\n- `predict_multiclass_adaptive` smooths predictions, masks them by tuned thresholds, argmaxes valid classes, and smooths segments again—more robust to class imbalance than the fixed 0.27 baseline.\n\n```python\n# submit_ensemble excerpt\nfor action in label.columns:\n    ...\n    oof_pred = cross_val_predict(\n        base_model, X_tune, y_tune, cv=splits,\n        groups=groups_tune, method=\"predict_proba\"\n    )[:, 1]\n    tuned_thr = tune_threshold(oof_pred, y_tune)\n    action_thresholds[action] = tuned_thr\njoblib.dump(dict(action_thresholds), thr_path)\n\n# predict_multiclass_adaptive\npred_smoothed = pred.rolling(window=7, min_periods=1, center=True).mean()\nthresholds = np.array([action_thresholds.get(action, 0.27) for action in pred_smoothed.columns])\nvalid = pred_smoothed.values >= thresholds\nmasked = np.where(valid, pred_smoothed.values, -np.inf)\nama = masked.argmax(axis=1)\nall_invalid = ~valid.any(axis=1)\nama[all_invalid] = -1\n```\n\nEach upgrade is either missing or heavily simplified in the baseline, and together they explain most of the final solution’s lift. Clear snippets and descriptions make it easier to audit, reproduce, or extend the approach.",
      "votes": 7
    }
  ],
  "comments": [],
  "raw_markdown_by_id": {
    "3377425": "# kaggle-MABe\n\n[github kaggle-MABe](https://github.com/Jayzen-Tech/kaggle-MABe)\n\n## Table of Contents\n- [Directory Layout & Required Data](#directory-layout--required-data)\n- [How to Run & Switches](#how-to-run--switches)\n- [Feature Engineering Upgrades Overview](#feature-engineering-upgrades-overview)\n- [Tracking Cleanup & Pixel Normalization](#1-tracking-cleanup--pixel-normalization)\n- [Meta Encoding & Feature Augmentation](#2-meta-encoding--feature-augmentation)\n- [Third-Mouse Context](#3-third-mouse-context-triadic-relations)\n- [Single-Mouse Advanced Motion](#4-single-mouse-advanced-motion-long-horizon--pastfuture)\n- [Pair Interaction Enhancements](#5-pair-interaction-enhancements--context-ratios)\n- [Train/Test Consistency](#6-traintest-consistency--non-finite-logging)\n- [Adaptive Threshold Strategy](#7-adaptive-threshold-tuning--inference-strategy)\n\n### Directory Layout & Required Data\nThe project uses `/kaggle/kaggle-MABe` as the working directory and depends on external Kaggle inputs. Core folders:\n\n```\n/kaggle/kaggle-MABe/\n├── mabe-final.py                # Final solution script\n├── baseline/mabe-baseline.py    # Initial baseline\n├── README.md, requirements.txt\n└── __pycache__/\n```\n\nExternal data and pretrained artifacts live under `/kaggle/input`, primarily:\n\n```\n/kaggle/input/\n└── MABe-mouse-behavior-detection/        # Official train/test CSVs and tracking parquet files\n```\n\nIf you plan to reuse pretrained models/thresholds through `LOAD_MODELS=True` or `LOAD_THRESHOLDS=True`, keep the default folder structure or update the `MODEL_LOAD_DIR` / `THRESHOLD_LOAD_DIR` constants inside the script before running.\n\n### How to Run & Switches\n\n#### Basic Commands\n1. (Optional on first run) Install dependencies: `pip install -r requirements.txt`\n2. Execute the pipeline: `python mabe-final.py`\n\nRuntime switches at the top of `mabe-final.py` control training/inference behavior:\n\n```python\n# ---- runtime switches ----\nONLY_TUNE_THRESHOLDS = False    # True: tune thresholds only and exit (skip training/inference/submission)\nUSE_ADAPTIVE_THRESHOLDS = True  # False: fallback to a fixed 0.27 threshold for every action\nLOAD_THRESHOLDS = False         # True: read thresholds from THRESHOLD_DIR instead of tuning\nLOAD_MODELS = False             # True: load pretrained models from MODEL_DIR\nCHECK_LOAD = False              # True: after loading, drop actions without positives in the current folds\nTHRESHOLD_DIR = \"./models/threshold\"\nMODEL_DIR = \"./models\"\nTHRESHOLD_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13/threshold\"\nMODEL_LOAD_DIR = \"/kaggle/input/xgb-models-new/other/xgb-models-new/13\"\nNON_FINITE_LOG = \"non_finite_features.log\"\n```\n\n**Recommended usage:**\n- **Full training + inference**: `ONLY_TUNE_THRESHOLDS=False`, `LOAD_MODELS=False`. The script trains, predicts, and saves models/thresholds under `MODEL_DIR` / `THRESHOLD_DIR`.\n- **Threshold tuning only**: When models already exist, set `ONLY_TUNE_THRESHOLDS=True`. The run stops after tuning and writes fresh pickles into `THRESHOLD_DIR`.\n- **Reuse trained models**: Set `LOAD_MODELS=True` (optionally `CHECK_LOAD=True` to verify label availability). The script reads Joblib caches from `MODEL_LOAD_DIR`; update the path if your models live elsewhere.\n- **Reuse tuned thresholds**: Flip `LOAD_THRESHOLDS=True` to skip tuning and read `.pkl` files from `THRESHOLD_LOAD_DIR`.\n- Disable adaptive thresholds (`USE_ADAPTIVE_THRESHOLDS=False`) to fall back to the global 0.27 cutoff for quick sanity checks.\n- `NON_FINITE_LOG` records any NaN/Inf columns encountered during training or inference. Point it to another path or truncate it as needed.\n\n### Feature Engineering Upgrades Overview\n`mabe-final.py` delivers a full feature-engineering refresh covering tracking cleanup, meta encoding, third-mouse context, single/pair motion modeling, and consistent train/test handling. Each subsection below highlights the additions versus the baseline and quotes the relevant code (all snippets originate from `mabe-final.py`).\n\n#### 1. Tracking Cleanup & Pixel Normalization\n- Reorder the multi-level columns and convert pixels to centimeters using `pix_per_cm_approx`.\n- Interpolate every body part, fill in both directions, and drop joints that remain all NaN.\n- Run all downstream matrices through `sanitize_features` to replace NaN/Inf with interpolations and per-column medians.\n\n```python\n# mabe-final.py (generate_mouse_data)\npvid = vid.pivot(columns=['mouse_id','bodypart'], index='video_frame', values=['x','y'])\npvid = pvid.reorder_levels([1,2,0], axis=1).T.sort_index().T\npvid = (pvid / float(row.pix_per_cm_approx)).astype('float32', copy=False)\npvid = pvid.sort_index(axis=1)\npvid = pvid.interpolate(limit_direction='both')\npvid = pvid.fillna(method='bfill').fillna(method='ffill')\nall_nan_cols = pvid.columns[pvid.isna().all()]\nif len(all_nan_cols) > 0:\n    pvid = pvid.drop(columns=all_nan_cols)\n\n# send every feature matrix through sanitize_features\ndef sanitize_features(X: Optional[pd.DataFrame]) -> pd.DataFrame:\n    X = X.replace([np.inf, -np.inf], np.nan)\n    X = X.interpolate(axis=0, limit_direction='both')\n    if X.isna().any().any():\n        med = X.median(axis=0)\n        X = X.fillna(med)\n    if X.isna().any().any():\n        X = X.fillna(0.0)\n    return X\n```\n\n#### 2. Meta Encoding & Feature Augmentation\n- `_mk_meta` parses agent/target sex and strain, builds integer codes and same-sex/strain indicators, computes arena area/aspect/shape, and aligns meta rows with frame indices.\n- `augment_with_meta_features` injects these meta columns into single/pair feature matrices and uses identical sanitization/logging during inference.\n\n```python\n# mabe-final.py (_mk_meta excerpt)\nm['agent_sex_code'] = np.float32(agent_sex_code)\nm['target_sex_code'] = np.float32(target_sex_code if target_key is not None else agent_sex_code)\nm['agent_strain_code'] = np.float32(agent_strain_code)\nm['target_strain_code'] = np.float32(target_strain_code if target_key is not None else agent_strain_code)\nm['same_sex'] = np.float32(1.0 if (...) else 0.0)\nm['same_strain'] = np.float32(1.0 if (...) else 0.0)\nm['arena_shape_code'] = np.float32(arena_shape_code)\nm['arena_area_cm2'] = np.float32(arena_area)\nm['arena_aspect_ratio'] = np.float32(arena_aspect)\nm = m.set_index('video_frame')\nm['video_frame'] = m.index\n\ndef augment_with_meta_features(X: pd.DataFrame, meta: Optional[pd.DataFrame]) -> pd.DataFrame:\n    meta_aligned = meta.reindex(X.index)\n    meta_features = pd.DataFrame(index=X.index)\n    for raw, name in [...]:\n        _copy(raw, name)\n    meta_features = sanitize_features(meta_features)\n    return pd.concat([X, meta_features], axis=1)\n```\n\n#### 3. Third-Mouse Context (Triadic Relations)\n- `_compute_third_party_context` converts every non agent/target mouse into distance stats against agent/target/midpoint, contact counts, weaving ratios, and cosine directionality.\n- `transform_pair` flattens the CTX MultiIndex and adds ratios (for example `ctx_third_min_dist_*_ratio`) so the model learns how third-party interference modulates behaviors.\n\n```python\ndef _compute_third_party_context(pvid: pd.DataFrame, agent_label, target_label) -> Optional[pd.DataFrame]:\n    ctx = pd.DataFrame(index=agent_bc.index)\n    ctx['third_min_dist_agent'] = df_a.min(axis=1)\n    ctx['third_mean_dist_agent'] = df_a.mean(axis=1)\n    ctx['third_close_agent_cnt'] = (df_a < close_thresh).sum(axis=1)\n    ctx['third_close_any'] = (close_any > 0).astype(float)\n    ctx['third_between_ratio'] = (df_a + df_b).min(axis=1) / (dist_at + 1e-6)\n    ctx['third_cos_agent_closest'] = closest\n    ctx.columns = pd.MultiIndex.from_arrays([['CTX'] * len(ctx_cols), ctx_cols, ['value'] * len(ctx_cols)])\n    return ctx\n\n# transform_pair excerpt\nif 'CTX' in mouse_pair.columns.get_level_values(0):\n    ctx_flat = mouse_pair['CTX'].copy()\n    ...\n    X[col] = ctx_flat[col]\nif cd is not None:\n    denom = (cd + 1e-6)\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / denom\n```\n\n#### 4. Single-Mouse Advanced Motion (Long Horizon + Past/Future)\n- When `body_center` is available, `transform_single` composes `add_longrange_features`, curvature, future-vs-past speed asymmetry, Gaussian-shift comparisons, and segment-level metrics.\n- `add_longrange_features` mixes configurable windows/EWM/percentiles, and the added operators cover future/past velocity contrasts, KL-style speed shifts, and segment-wise acceleration/jerk/pause ratios.\n\n```python\ndef add_longrange_features(X, center_x, center_y, fps,\n                          long_windows=None, ewm_spans=None, pct_windows=None):\n    if long_windows is None:\n        long_windows = [30, 60, 120, 240, 480]\n    if ewm_spans is None:\n        ewm_spans = [15, 30, 60, 120, 240]\n    for window in long_windows:\n        ws = _scale(window, fps)\n        X[f'x_ml{window}'] = center_x.rolling(ws, min_periods=max(5, ws // 6)).mean()\n    for span in ewm_spans:\n        s = _scale(span, fps)\n        X[f'x_e{span}'] = center_x.ewm(span=s, min_periods=1).mean()\n\ndef transform_single(...):\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']; cy = single_mouse['body_center']['y']\n        ...\n        X = add_longrange_features(X, cx, cy, fps)\n        X = add_cumulative_distance_single(X, cx, cy, fps, horizon_frames_base=180)\n        X = add_groom_microfeatures(X, single_mouse, fps)\n        X = add_speed_asymmetry_future_past_single(X, cx, cy, fps, horizon_base=30)\n        X = add_gauss_shift_speed_future_past_single(X, cx, cy, fps, window_base=30)\n        X = add_segmental_features_single(X, cx, cy, fps, horizons_base=(30, 90))\n```\n\n- **Multi-scale speed & lags**: `transform_single` now iterates `speed_lags = [1,2,3,4,5,10,20,30,40,50,60]`, computing squared displacements for `ear_left/right` and `tail_base` (see `mabe-final.py:1194-1225`). `_scale(lag_base, fps)` converts each lag to a real time span, covering both 1–2 frame micro stops and >2 s runs. To maintain backward compatibility, the 10-frame outputs remain exposed as `sp_lf`, `sp_rt`, etc.\n- **Nose–tail lagged deltas**: `nt_lg{10,20,40}` and `nt_df{10,20,40}` reuse the FPS-aware shifts (`mabe-final.py:1266-1274`), so stretch/contract cues stay stable across frame rates—coverage the baseline lacked.\n\n#### 5. Pair Interaction Enhancements + Context Ratios\n- `transform_pair` enriches lagged inter-mouse speeds, head orientation bins, nose/ear wedge angles, segmented distance stats, radial/tangential nose velocities, rolling contact ratios, and velocity alignment offsets.\n- CTX metrics are normalized by pairwise distance to remove raw-scale bias.\n\n```python\ndef transform_pair(mouse_pair, body_parts_tracked, fps):\n    # head alignment & facing bins\n    X['head_parallel'] = (head_cos > 0.5).astype(float)\n    X['A_face_front'] = (X['A_face_cos'] > 0.7).astype(float)\n    ...\n    # nose dynamics\n    X['nn_rad_sp'] = (rv * float(fps)).fillna(0)\n    X['nn_lat_sp'] = (lv * float(fps)).fillna(0)\n    X[f'nn_ct{int(thr)}_{w}'] = (nn < thr).rolling(ws, **roll_opts).mean()\n    # velocity alignment offsets\n    for off in [-20, -10, 10, 20]:\n        o = _scale_signed(off, fps)\n        X[f'va_{off}'] = val.shift(-o)\n    # CTX ratios\n    X['ctx_third_min_dist_agent_ratio'] = X['ctx_third_min_dist_agent'] / (cd + 1e-6)\n```\n\n- **Multi-lag motion & approach cues**: Pair features reuse `speed_lags = [1,2,3,4,5,10,20,30,40,50,60]` (`mabe-final.py:1302-1325`) to output `sp_A_l{lag}`, `sp_AB_l{lag}`, `sp_B_l{lag}`, separating agent, target, and cross-body displacement intensity over short/long horizons. Nose-to-nose distance now spans lag=1–100 via `nn_lg*`, `nn_ch*`, `cl_ps*`, and `nose_spd*_lg*` (`mabe-final.py:1430-1466`), capturing “approach → brief contact → disengage” rhythms that the 10/20/40-frame baseline missed.\n\n#### 6. Train/Test Consistency & Non-finite Logging\n- Training and inference both apply `augment_with_meta_features` + `sanitize_features`, and `log_non_finite_features` records any NaN/Inf columns together with their runtime context.\n- Sampling keeps meta/labels aligned with feature slices; inference reindexes by `feature_columns` and zero-fills unseen feature names.\n\n```python\n# training stage\nXi_full = transform_single(data_i, body_parts_tracked, fps_i).astype(np.float32)\nXi_full = augment_with_meta_features(Xi_full, meta_i).astype(np.float32, copy=False)\nlog_non_finite_features(\n    Xi_full,\n    f\"train|single|section={section}|n_parts={len(body_parts_tracked)}|{_meta_summary(meta_i)}\"\n)\n\n# inference stage\nX_te = transform_pair(data_te, body_parts_tracked, fps_i)\nX_te = augment_with_meta_features(X_te, meta_te)\nif feature_columns:\n    X_te = (X_te.reindex(columns=feature_columns, fill_value=0.0)\n                .astype(np.float32, copy=False))\nlog_non_finite_features(X_te, f\"test|{switch_te}|...\")\n```\n\n#### 7. Adaptive Threshold Tuning & Inference Strategy\n- `submit_ensemble` searches per-action thresholds on GroupKFold OOF predictions via Optuna and persists the Joblib dumps into `THRESHOLD_DIR` for reuse.\n- `predict_multiclass_adaptive` smooths predictions, masks them by tuned thresholds, argmaxes valid classes, and smooths segments again—more robust to class imbalance than the fixed 0.27 baseline.\n\n```python\n# submit_ensemble excerpt\nfor action in label.columns:\n    ...\n    oof_pred = cross_val_predict(\n        base_model, X_tune, y_tune, cv=splits,\n        groups=groups_tune, method=\"predict_proba\"\n    )[:, 1]\n    tuned_thr = tune_threshold(oof_pred, y_tune)\n    action_thresholds[action] = tuned_thr\njoblib.dump(dict(action_thresholds), thr_path)\n\n# predict_multiclass_adaptive\npred_smoothed = pred.rolling(window=7, min_periods=1, center=True).mean()\nthresholds = np.array([action_thresholds.get(action, 0.27) for action in pred_smoothed.columns])\nvalid = pred_smoothed.values >= thresholds\nmasked = np.where(valid, pred_smoothed.values, -np.inf)\nama = masked.argmax(axis=1)\nall_invalid = ~valid.any(axis=1)\nama[all_invalid] = -1\n```\n\nEach upgrade is either missing or heavily simplified in the baseline, and together they explain most of the final solution’s lift. Clear snippets and descriptions make it easier to audit, reproduce, or extend the approach."
  }
}