{"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":13719397,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# <span style=\"color:#1E3A8A;\">MABe Challenge 2025</span>: <span style=\"color:#374151;\">Social Action Recognition in Mice</span>  \n*<span style=\"color:#0D9488;\">Multi-Laboratory Behavioral Classification from Pose Estimation Data</span>*\n\n---\n\n## <span style=\"color:#0D9488;\">Summary</span>\n\n**Challenge**  \nAutomated temporal detection of 30+ mouse social behaviors from pose trajectories across heterogeneous laboratory environments.  \n\n**Dataset**  \n400+ hours of markerless motion capture data from 20+ research facilities with frame-level behavioral annotations.  \n\n**Key Difficulty**  \nExtreme domain shift between laboratories caused by different tracking systems, experimental protocols, and annotation standards.  \n\n---\n\n## <span style=\"color:#0D9488;\">Problem Definition</span>\n\n| **Input** | **Output** | **Evaluation** |\n|-----------|------------|----------------|\n| Time-series pose data `(x_t, y_t)` for multiple bodyparts across multiple mice per frame | Temporal action segments `(agent_id, target_id, behavior_class, t_start, t_stop)` with boundary detection | Per-laboratory averaged **F1-score**, requiring robust cross-domain generalization |\n\n**Primary Technical Obstacles**\n- Laboratory-specific coordinate systems and tracking methodologies  \n- Severe class imbalance: rare behaviors <0.1% of annotations  \n- Variable temporal dynamics: instantaneous vs. extended actions  \n- Multi-agent interaction modeling with complex spatial dependencies  \n\n---\n\n## <span style=\"color:#0D9488;\">Dataset Architecture</span>\n\n- `train_tracking/` # Pose coordinates per frame per video\n- `train_annotation/` # Behavioral labels with temporal boundaries\n- `test_tracking/` # Unlabeled pose data for inference\n- `train.csv` # Video metadata and experimental parameters\n\n**Data Format**: Apache Parquet (efficient columnar storage for large-scale behavioral datasets).  \n**Annotation Schema**: Expert-validated ontology with standardized agent–target relationship encoding.  \n\n---\n\n## <span style=\"color:#0D9488;\">Analysis Framework</span>\n\n### Section 1: Data Inventory and Quality Assessment\n\n**Research Questions**\n1. What proportion of tracking data includes behavioral annotations suitable for supervised learning?  \n2. Which laboratories contribute sufficient data volume for reliable cross-validation?  \n3. How should annotation-sparse laboratories be incorporated into training strategies?  \n\n**Analytical Approach**\n- *File System Audit*: Verify parquet availability across labs with integrity checks.  \n- *Coverage Analysis*: Measure annotation completeness relative to available tracking data.  \n- *Laboratory Profiling*: Characterize per-lab contributions and annotation practices.  ","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2025-09-19T07:16:04.646368Z","iopub.execute_input":"2025-09-19T07:16:04.646673Z","iopub.status.idle":"2025-09-19T07:16:04.657022Z","shell.execute_reply.started":"2025-09-19T07:16:04.646654Z","shell.execute_reply":"2025-09-19T07:16:04.653066Z"}}},{"cell_type":"code","source":"from pathlib import Path\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# consistent color palette\nPALETTE = {\n    \"deep_blue\": \"#1E3A8A\",\n    \"slate_gray\": \"#374151\",\n    \"teal\": \"#0D9488\",\n    \"muted\": \"#94A3B8\",\n    \"bg\": \"#FFFFFF\"\n}\n\n# seaborn/matplotlib defaults\nsns.set_theme(\n    context=\"talk\",\n    style=\"whitegrid\",\n    rc={\n        \"figure.dpi\": 120,\n        \"axes.facecolor\": PALETTE[\"bg\"],\n        \"figure.facecolor\": PALETTE[\"bg\"],\n        \"font.family\": \"sans-serif\",\n        \"font.size\": 12,\n        \"axes.titlesize\": 16,\n        \"axes.labelsize\": 13,\n        \"legend.fontsize\": 11,\n        \"xtick.labelsize\": 11,\n        \"ytick.labelsize\": 11,\n        \"grid.color\": \"#E6EEF2\",\n        \"grid.linewidth\": 0.8\n    }\n)\n\n# dataset root\nDATA = Path(\"/kaggle/input/MABe-mouse-behavior-detection\")\npd.set_option(\"display.max_columns\", None)\npd.set_option(\"display.width\", 240)\n\n# read train.csv\ntrain_csv_path = DATA / \"train.csv\"\nif not train_csv_path.exists():\n    raise FileNotFoundError(f\"train.csv not found at: {train_csv_path}\")\ntrain_csv = pd.read_csv(train_csv_path)\ntrain_csv[\"video_id\"] = train_csv[\"video_id\"].astype(str)\n\n# folder structure\nfolders = {\n    \"train_tracking\": DATA / \"train_tracking\",\n    \"train_annotation\": DATA / \"train_annotation\",\n    \"test_tracking\": DATA / \"test_tracking\"\n}\n\n# collect summary\nsummary_data = []\nfor folder_name, folder_path in folders.items():\n    if not folder_path.exists():\n        summary_data.append((folder_name, 0, 0))\n        continue\n    total_files = 0\n    lab_dirs = [p for p in folder_path.iterdir() if p.is_dir()]\n    for lab_dir in lab_dirs:\n        files_in_lab = len(list(lab_dir.glob(\"*.parquet\")))\n        total_files += files_in_lab\n    summary_data.append((folder_name, total_files, len(lab_dirs)))\n\nsummary_df = pd.DataFrame(summary_data, columns=[\"Directory\", \"Total Files\", \"Lab Count\"]).sort_values(\"Total Files\", ascending=True)\n\n# plot\nfig, ax = plt.subplots(figsize=(8, 4.5))\nsns.barplot(data=summary_df, x=\"Total Files\", y=\"Directory\", palette=[PALETTE[\"deep_blue\"]], ax=ax)\n\nfor p in ax.patches:\n    ax.annotate(f\"{int(p.get_width()):,}\", \n                (p.get_width(), p.get_y() + p.get_height() / 2),\n                xytext=(6, 0), textcoords=\"offset points\", \n                va=\"center\", color=PALETTE[\"slate_gray\"], fontsize=11)\n\nax.set_title(\"Top-level file counts by directory\", color=PALETTE[\"slate_gray\"], fontsize=14)\nax.set_xlabel(\"Total parquet files\")\nax.set_ylabel(\"\")\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.grid(axis=\"x\", linestyle=\"--\", alpha=0.35)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:14:40.142347Z","iopub.execute_input":"2025-09-19T12:14:40.142696Z","iopub.status.idle":"2025-09-19T12:14:41.675833Z","shell.execute_reply.started":"2025-09-19T12:14:40.142667Z","shell.execute_reply":"2025-09-19T12:14:41.674704Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"lab_inventory = []\nfor lab_id in sorted(train_csv[\"lab_id\"].unique()):\n    tracking_dir = folders[\"train_tracking\"] / lab_id\n    annotation_dir = folders[\"train_annotation\"] / lab_id\n    tracking_count = len(list(tracking_dir.glob(\"*.parquet\"))) if tracking_dir.exists() else 0\n    annotation_count = len(list(annotation_dir.glob(\"*.parquet\"))) if annotation_dir.exists() else 0\n    difference = tracking_count - annotation_count\n    lab_inventory.append((lab_id, tracking_count, annotation_count, difference))\n\ninventory_df = pd.DataFrame(\n    lab_inventory, \n    columns=[\"Lab ID\", \"Tracking Files\", \"Annotation Files\", \"Difference (T-A)\"]\n).sort_values([\"Annotation Files\", \"Tracking Files\"], ascending=[False, False]).reset_index(drop=True)\n\nfig, ax = plt.subplots(figsize=(10, 6))\nsns.barplot(\n    data=inventory_df, \n    x=\"Lab ID\", \n    y=\"Tracking Files\", \n    color=PALETTE[\"deep_blue\"], \n    ax=ax, \n    label=\"Tracking Files\"\n)\nsns.barplot(\n    data=inventory_df, \n    x=\"Lab ID\", \n    y=\"Annotation Files\", \n    color=PALETTE[\"teal\"], \n    ax=ax, \n    label=\"Annotation Files\"\n)\nax.set_title(\"Per-lab tracking vs annotation files\", color=PALETTE[\"slate_gray\"], fontsize=15)\nax.set_xlabel(\"Lab ID\")\nax.set_ylabel(\"Number of files\")\nax.legend(frameon=False)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.grid(axis=\"y\", linestyle=\"--\", alpha=0.35)\nplt.xticks(rotation=45)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:14:42.910735Z","iopub.execute_input":"2025-09-19T12:14:42.911095Z","iopub.status.idle":"2025-09-19T12:14:43.632298Z","shell.execute_reply.started":"2025-09-19T12:14:42.911069Z","shell.execute_reply":"2025-09-19T12:14:43.631205Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"viz_df = inventory_df.copy()\nviz_df[\"Lab Short\"] = viz_df[\"Lab ID\"].str.replace(\"_\", \" \").str.slice(0, 30)\ntop_labs = viz_df.nlargest(25, \"Tracking Files\").reset_index(drop=True)\n\nfig, ax = plt.subplots(figsize=(12, 10), dpi=120)\ny = np.arange(len(top_labs))\nbar_h = 0.35\n\nax.barh(y - bar_h/2, top_labs[\"Tracking Files\"], height=bar_h, label=\"Tracking Files\", color=PALETTE[\"deep_blue\"], alpha=0.85)\nax.barh(y + bar_h/2, top_labs[\"Annotation Files\"], height=bar_h, label=\"Annotation Files\", color=PALETTE[\"teal\"], alpha=0.95)\n\nfor i, (t, a) in enumerate(zip(top_labs[\"Tracking Files\"], top_labs[\"Annotation Files\"])):\n    ax.text(t + max(top_labs[\"Tracking Files\"]) * 0.003, i - bar_h/2, f\"{int(t):,}\", va=\"center\", ha=\"left\", color=PALETTE[\"slate_gray\"], fontsize=10)\n    ax.text(a + max(top_labs[\"Tracking Files\"]) * 0.003, i + bar_h/2, f\"{int(a):,}\", va=\"center\", ha=\"left\", color=PALETTE[\"slate_gray\"], fontsize=10)\n\nax.set_yticks(y)\nax.set_yticklabels(top_labs[\"Lab Short\"], color=PALETTE[\"slate_gray\"])\nax.invert_yaxis()\nax.set_xlabel(\"Number of Parquet Files\", color=PALETTE[\"slate_gray\"])\nax.set_title(\"File Availability by Lab — Tracking vs Annotation\", color=PALETTE[\"slate_gray\"], fontsize=16, pad=12)\nax.legend(frameon=False, loc=\"lower right\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:30.739725Z","iopub.execute_input":"2025-09-19T12:15:30.740079Z","iopub.status.idle":"2025-09-19T12:15:31.329693Z","shell.execute_reply.started":"2025-09-19T12:15:30.740053Z","shell.execute_reply":"2025-09-19T12:15:31.328742Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import HTML, display\n\nanno_root = folders[\"train_annotation\"]\nvalid_annos = set()\nif anno_root.exists():\n    for lab_dir in sorted(anno_root.iterdir()):\n        if not lab_dir.is_dir():\n            continue\n        for f in lab_dir.glob(\"*.parquet\"):\n            valid_annos.add((lab_dir.name, f.stem))\n\nlabs_no_annotations = inventory_df.loc[inventory_df[\"Annotation Files\"] == 0, \"Lab ID\"].tolist()\nusable_mask = train_csv.apply(lambda r: (r[\"lab_id\"], r[\"video_id\"]) in valid_annos, axis=1)\nusable_train_rows = train_csv[usable_mask]\n\ntotal_rows = len(train_csv)\nusable_rows = len(usable_train_rows)\nusable_pct = (usable_rows / total_rows * 100) if total_rows > 0 else 0.0\n\nsummary_html = f\"\"\"\n<div style=\"border-left:4px solid {PALETTE['teal']}; padding:12px; font-family:sans-serif; background:#ffffff;\">\n  <h3 style=\"margin:0; color:{PALETTE['deep_blue']};\">Missing Annotation Analysis</h3>\n  <div style=\"color:{PALETTE['slate_gray']}; margin-top:6px;\">\n    <div><strong>Labs with zero annotation files</strong>: {', '.join(labs_no_annotations) if labs_no_annotations else 'None'}</div>\n    <div style=\"margin-top:6px;\"><strong>Total rows in train.csv</strong>: {total_rows:,}</div>\n    <div><strong>Usable rows with annotation parquet</strong>: {usable_rows:,}</div>\n    <div><strong>Usable percentage</strong>: {usable_pct:.1f}%</div>\n  </div>\n</div>\n\"\"\"\ndisplay(HTML(summary_html))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:33.786834Z","iopub.execute_input":"2025-09-19T12:15:33.787146Z","iopub.status.idle":"2025-09-19T12:15:33.888964Z","shell.execute_reply.started":"2025-09-19T12:15:33.787124Z","shell.execute_reply":"2025-09-19T12:15:33.887995Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"top_labs = inventory_df.head(15)\nbottom_labs = inventory_df.tail(15)\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 6), dpi=120, sharey=True)\n\nsns.barplot(data=top_labs, x=\"Tracking Files\", y=\"Lab ID\", color=PALETTE[\"deep_blue\"], ax=axes[0], label=\"Tracking\")\nsns.barplot(data=top_labs, x=\"Annotation Files\", y=\"Lab ID\", color=PALETTE[\"teal\"], ax=axes[0], label=\"Annotation\")\naxes[0].set_title(\"Top 15 Labs by File Count\", color=PALETTE[\"slate_gray\"], fontsize=14)\naxes[0].set_xlabel(\"Number of Files\")\naxes[0].set_ylabel(\"\")\naxes[0].legend(frameon=False)\n\nsns.barplot(data=bottom_labs, x=\"Tracking Files\", y=\"Lab ID\", color=PALETTE[\"deep_blue\"], ax=axes[1], label=\"Tracking\")\nsns.barplot(data=bottom_labs, x=\"Annotation Files\", y=\"Lab ID\", color=PALETTE[\"teal\"], ax=axes[1], label=\"Annotation\")\naxes[1].set_title(\"Bottom 15 Labs by File Count\", color=PALETTE[\"slate_gray\"], fontsize=14)\naxes[1].set_xlabel(\"Number of Files\")\naxes[1].set_ylabel(\"\")\naxes[1].legend(frameon=False)\n\nfor ax in axes:\n    ax.spines[\"top\"].set_visible(False)\n    ax.spines[\"right\"].set_visible(False)\n    ax.grid(axis=\"x\", linestyle=\"--\", alpha=0.35)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:35.955538Z","iopub.execute_input":"2025-09-19T12:15:35.955944Z","iopub.status.idle":"2025-09-19T12:15:36.926186Z","shell.execute_reply.started":"2025-09-19T12:15:35.955916Z","shell.execute_reply":"2025-09-19T12:15:36.924858Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## <span style=\"color:#1E3A8A;\">Results and Findings</span>\n\n### <span style=\"color:#0D9488;\">Dataset Composition Analysis</span>\n\n- **Data Availability**: Only **863 / 8,790 tracking files (9.8%)** contain behavioral annotations for supervised learning.  \n- **Laboratory Participation**: 21 laboratories contributed tracking data, but only 19 provide annotated sequences.  \n- **Test Set Limitation**: The test set contains a single file → evaluation requires carefully designed validation strategies.  \n\n---\n\n### <span style=\"color:#0D9488;\">Annotation Coverage Assessment</span>\n\n- **Legacy Data**: *MABe22_keypoints* (5,320 files) and *MABe22_movies* (2,606 files) lack annotations → together **91% of tracking data**.  \n- **Pretraining Potential**: **7,926 unlabeled sequences** can be leveraged for self-supervised representation learning.  \n- **Annotation Integrity**: Perfect alignment between tracking and annotation across active labs confirms robust protocols.  \n\n---\n\n### <span style=\"color:#0D9488;\">Laboratory Contribution Analysis</span>\n\n- **Primary Contributors**:  \n  - *CalMS21 family*: **474 annotated videos (55% of supervised dataset)**  \n  - *SparklingTapir*: 70 videos  \n  - *JovialSwallow*: 52 videos  \n  - Remaining 14 laboratories: each <40 videos  \n\n- **Domain Bias Risk**: Heavy dominance of *CalMS21* may bias models toward its protocols and behaviors.  \n- **Validation Implication**: **Leave-one-laboratory-out CV** is critical for unbiased generalization.  \n\n---\n\n## <span style=\"color:#1E3A8A;\">Section 2: Behavioral Annotation Characteristics & Distribution</span>\n\n### <span style=\"color:#0D9488;\">Research Objectives</span>\n\n- **Label Distribution Profiling**: Quantify behavior frequencies and highlight extreme imbalance.  \n- **Temporal Dynamics**: Characterize durations, variance, and sequence patterns.  \n- **Cross-Laboratory Consistency**: Assess ontology alignment across labs.  \n\n---\n\n### <span style=\"color:#0D9488;\">Analytical Framework</span>\n\n- *Frequency Analysis*: Statistical breakdown of class distributions and rare behaviors.  \n- *Duration Modeling*: Duration signatures (mean, variance, percentiles) per class.  \n- *Social Structure*: Agent–target decomposition (self-directed vs interactive).  \n- *Cross-Domain Validation*: Laboratory-wise availability check for protocol inconsistencies.  \n\n---\n\n### <span style=\"color:#0D9488;\">Expected Outcomes</span>\n\n- **Class Balancing**: Sampling strategies, weighted losses, and augmentation for rare classes.  \n- **Temporal Modeling**: Window sizing and multi-scale design informed by behavior durations.  \n- **Domain Adaptation**: Lab-specific subsets identified for transfer learning and generalization.  \n\n---\n\n### <span style=\"color:#0D9488;\">Critical Success Metrics</span>\n\n- **Imbalance Quantification**: Ratio between most and least common classes + significance tests.  \n- **Temporal Consistency**: Cross-lab duration similarity via statistical distance measures.  \n- **Annotation Protocol Alignment**: Ontology overlap and standardization recommendations.  \n","metadata":{}},{"cell_type":"code","source":"all_annotations = []\nannotation_stats = []\n\nfor _, row in usable_train_rows.iterrows():\n    lab_id = row[\"lab_id\"]\n    video_id = row[\"video_id\"]\n    anno_path = DATA / \"train_annotation\" / lab_id / f\"{video_id}.parquet\"\n    try:\n        df_anno = pd.read_parquet(anno_path)\n        df_anno[\"lab_id\"] = lab_id\n        df_anno[\"video_id\"] = video_id\n        df_anno[\"duration\"] = df_anno[\"stop_frame\"] - df_anno[\"start_frame\"] + 1\n        df_anno[\"is_self_directed\"] = df_anno[\"agent_id\"] == df_anno[\"target_id\"]\n        all_annotations.append(df_anno)\n        annotation_stats.append((lab_id, video_id, len(df_anno)))\n    except Exception:\n        continue\n\ncombined_annotations = pd.concat(all_annotations, ignore_index=True)\nstats_df = pd.DataFrame(annotation_stats, columns=[\"lab_id\", \"video_id\", \"annotation_count\"])\n\nannotation_overview = {\n    \"Behavioral Instances\": len(combined_annotations),\n    \"Unique Actions\": combined_annotations[\"action\"].nunique(),\n    \"Videos with Annotations\": len(stats_df),\n    \"Labs with Annotations\": combined_annotations[\"lab_id\"].nunique()\n}\n\nfig, ax = plt.subplots(figsize=(7, 7), dpi=120)\nsizes = list(annotation_overview.values())\nlabels = [f\"{k}\\n{v:,}\" for k, v in annotation_overview.items()]\ncolors = [PALETTE[\"deep_blue\"], PALETTE[\"teal\"], \"#60A5FA\", \"#F59E0B\"]\n\nwedges, _ = ax.pie(\n    sizes,\n    startangle=90,\n    counterclock=False,\n    wedgeprops=dict(width=0.45),\n    colors=colors\n)\nax.set(aspect=\"equal\")\nax.set_title(\"Behavioral Annotation Overview\", color=PALETTE[\"slate_gray\"], fontsize=15, pad=15)\nax.legend(wedges, labels, loc=\"center left\", bbox_to_anchor=(1, 0.5), frameon=False)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:39.288402Z","iopub.execute_input":"2025-09-19T12:15:39.288786Z","iopub.status.idle":"2025-09-19T12:15:45.201125Z","shell.execute_reply.started":"2025-09-19T12:15:39.288761Z","shell.execute_reply":"2025-09-19T12:15:45.200143Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"action_counts = combined_annotations[\"action\"].value_counts()\naction_stats = (\n    combined_annotations.groupby(\"action\")[\"duration\"]\n    .agg([\"count\", \"mean\", \"std\", \"min\", \"max\"])\n    .round(2)\n)\naction_stats[\"percentage\"] = (action_stats[\"count\"] / len(combined_annotations) * 100).round(2)\naction_stats = action_stats.sort_values(\"count\", ascending=False).reset_index()\n\ntop20 = action_stats.head(20)\n\nfig, ax = plt.subplots(figsize=(10, 7), dpi=120)\nsns.barplot(\n    data=top20,\n    x=\"count\",\n    y=\"action\",\n    palette=sns.color_palette([PALETTE[\"deep_blue\"], PALETTE[\"teal\"]]),\n    ax=ax\n)\nfor i, (c, pct) in enumerate(zip(top20[\"count\"], top20[\"percentage\"])):\n    ax.text(c + max(top20[\"count\"]) * 0.003, i, f\"{c:,} ({pct:.1f}%)\",\n            va=\"center\", ha=\"left\", fontsize=10, color=PALETTE[\"slate_gray\"])\n\nax.set_title(\"Top 20 Most Frequent Actions\", color=PALETTE[\"slate_gray\"], fontsize=15, pad=12)\nax.set_xlabel(\"Number of Instances\")\nax.set_ylabel(\"\")\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.grid(axis=\"x\", linestyle=\"--\", alpha=0.35)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:45.202503Z","iopub.execute_input":"2025-09-19T12:15:45.202815Z","iopub.status.idle":"2025-09-19T12:15:45.708897Z","shell.execute_reply.started":"2025-09-19T12:15:45.202790Z","shell.execute_reply":"2025-09-19T12:15:45.707819Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nfrom IPython.display import HTML, display\n\nif \"combined_annotations\" not in globals():\n    if \"usable_train_rows\" in globals():\n        all_annotations = []\n        for _, row in usable_train_rows.iterrows():\n            lab_id = row[\"lab_id\"]\n            video_id = row[\"video_id\"]\n            anno_path = DATA / \"train_annotation\" / lab_id / f\"{video_id}.parquet\"\n            try:\n                df_anno = pd.read_parquet(anno_path)\n                df_anno[\"lab_id\"] = lab_id\n                df_anno[\"video_id\"] = video_id\n                df_anno[\"duration\"] = df_anno[\"stop_frame\"] - df_anno[\"start_frame\"] + 1\n                df_anno[\"is_self_directed\"] = df_anno[\"agent_id\"] == df_anno[\"target_id\"]\n                all_annotations.append(df_anno)\n            except Exception:\n                continue\n        if len(all_annotations) == 0:\n            raise NameError(\"No annotations found when attempting to rebuild combined_annotations.\")\n        combined_annotations = pd.concat(all_annotations, ignore_index=True)\n    else:\n        raise NameError(\"combined_annotations not found in the session. Re-run the annotation-loading cell first.\")\n\nsd_counts = combined_annotations[\"is_self_directed\"].value_counts()\nsocial = int(sd_counts.get(False, 0))\nself_dir = int(sd_counts.get(True, 0))\ntotal = social + self_dir\nsocial_pct = social / total * 100 if total > 0 else 0.0\nself_pct = self_dir / total * 100 if total > 0 else 0.0\n\nsummary_html = f\"\"\"\n<div style=\"border-left:4px solid {PALETTE['teal']}; padding:12px; font-family:sans-serif; background:#ffffff; max-width:680px;\">\n  <h3 style=\"margin:0; color:{PALETTE['deep_blue']};\">Self-directed vs Social Behaviors</h3>\n  <div style=\"color:{PALETTE['slate_gray']}; margin-top:8px;\">\n    <div><strong>Social behaviors (agent != target)</strong>: {social:,} ({social_pct:.1f}%)</div>\n    <div style=\"margin-top:6px;\"><strong>Self-directed behaviors (agent == target)</strong>: {self_dir:,} ({self_pct:.1f}%)</div>\n    <div style=\"margin-top:8px; color:{PALETTE['muted']}; font-size:90%;\">Total behavioral instances: {total:,}</div>\n  </div>\n</div>\n\"\"\"\ndisplay(HTML(summary_html))\n\nfig, ax = plt.subplots(figsize=(6, 6), dpi=120)\nsizes = [social, self_dir]\nlabels = [f\"Social\\n{social:,}\\n{social_pct:.1f}%\", f\"Self-directed\\n{self_dir:,}\\n{self_pct:.1f}%\"]\ncolors = [PALETTE[\"deep_blue\"], PALETTE[\"teal\"]]\n\nwedges, texts = ax.pie(sizes, startangle=90, counterclock=False, wedgeprops=dict(width=0.45), colors=colors)\nax.set(aspect=\"equal\")\nax.set_title(\"Social vs Self-directed Behavioral Instances\", color=PALETTE[\"slate_gray\"], fontsize=14, pad=12)\nax.legend(wedges, labels, title=\"\", loc=\"center left\", bbox_to_anchor=(1, 0.5), frameon=False)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:45.709863Z","iopub.execute_input":"2025-09-19T12:15:45.710095Z","iopub.status.idle":"2025-09-19T12:15:45.890553Z","shell.execute_reply.started":"2025-09-19T12:15:45.710077Z","shell.execute_reply":"2025-09-19T12:15:45.889602Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Compute duration statistics per action\nduration_stats = (\n    combined_annotations.groupby(\"action\")[\"duration\"]\n    .agg([\"mean\", \"median\", \"std\"])\n    .round(1)\n    .sort_values(\"mean\", ascending=False)\n)\n\n# Prepare data for top 15 actions\ntop_actions = duration_stats.head(15).index.tolist()\nplot_df = combined_annotations[combined_annotations[\"action\"].isin(top_actions)].copy()\nplot_df = plot_df[plot_df[\"duration\"] > 0]\n\n# Plot distributions\nplt.figure(figsize=(12, 8), dpi=120)\nax = sns.boxplot(\n    data=plot_df,\n    x=\"duration\",\n    y=\"action\",\n    order=top_actions,\n    showfliers=False,\n    width=0.6,\n    palette=[PALETTE[\"teal\"] if i % 2 == 0 else PALETTE[\"deep_blue\"] for i in range(len(top_actions))]\n)\nax.set_xscale(\"log\")\nax.set_xlabel(\"Duration (frames) — log scale\", color=PALETTE[\"slate_gray\"])\nax.set_ylabel(\"\")\nax.set_title(\"Action Duration Distributions — Top 15 by Mean Duration\", color=PALETTE[\"slate_gray\"], fontsize=15, pad=12)\n\n# annotate medians\nmedians = plot_df.groupby(\"action\")[\"duration\"].median().reindex(top_actions)\nx_off = plot_df[\"duration\"].median() * 0.03\nfor i, (act, med) in enumerate(medians.items()):\n    ax.text(med + x_off, i, f\"{int(med):,}\", va=\"center\", ha=\"left\", color=PALETTE[\"slate_gray\"], fontsize=9)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:52.717410Z","iopub.execute_input":"2025-09-19T12:15:52.717742Z","iopub.status.idle":"2025-09-19T12:15:53.589052Z","shell.execute_reply.started":"2025-09-19T12:15:52.717719Z","shell.execute_reply":"2025-09-19T12:15:53.588113Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build lab-action availability matrix\nlab_action_matrix = combined_annotations.groupby([\"lab_id\", \"action\"]).size().unstack(fill_value=0)\nlab_action_presence = (lab_action_matrix > 0).astype(int)\n\ntop_actions = combined_annotations[\"action\"].value_counts().head(30).index.tolist()\nsubset = lab_action_presence[top_actions]\n\nplt.figure(figsize=(16, 10), dpi=120)\nax = sns.heatmap(\n    subset,\n    cmap=sns.color_palette([PALETTE[\"muted\"], PALETTE[\"teal\"]]),\n    linewidths=0.3,\n    linecolor=\"#E5E7EB\",\n    cbar_kws={\"label\": \"Action Presence\"}\n)\n\nax.set_title(\"Cross-Lab Action Availability Heatmap (Top 30 Actions)\", color=PALETTE[\"slate_gray\"], fontsize=15, pad=16)\nax.set_xlabel(\"Actions\", color=PALETTE[\"slate_gray\"])\nax.set_ylabel(\"Labs\", color=PALETTE[\"slate_gray\"])\nax.tick_params(axis=\"x\", rotation=65)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:54.893767Z","iopub.execute_input":"2025-09-19T12:15:54.894113Z","iopub.status.idle":"2025-09-19T12:15:55.820810Z","shell.execute_reply.started":"2025-09-19T12:15:54.894088Z","shell.execute_reply":"2025-09-19T12:15:55.819709Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_imbalance = action_counts / action_counts.sum()\nrare_actions = action_counts[action_counts <= 10]\ncommon_actions = action_counts[action_counts >= 1000]\n\nrare_count = len(rare_actions)\ncommon_count = len(common_actions)\nmost_common = action_counts.iloc[0]\nleast_common = action_counts.iloc[-1]\nimbalance_ratio = most_common / least_common if least_common > 0 else np.nan\n\nfig, ax = plt.subplots(1, 2, figsize=(14, 6), dpi=120)\n\n# Histogram of action frequencies (log scale)\nsns.histplot(action_counts, bins=40, log_scale=(True, False), color=PALETTE[\"deep_blue\"], ax=ax[0])\nax[0].set_title(\"Distribution of Action Frequencies\", color=PALETTE[\"slate_gray\"], fontsize=14, pad=12)\nax[0].set_xlabel(\"Instances per Action (log scale)\")\nax[0].set_ylabel(\"Number of Actions\")\nax[0].spines[\"top\"].set_visible(False)\nax[0].spines[\"right\"].set_visible(False)\nax[0].grid(axis=\"y\", linestyle=\"--\", alpha=0.35)\n\n# Bar plot for rare vs common classes\nax[1].bar([\"Rare (≤10)\", \"Common (≥1000)\"], [rare_count, common_count], color=[PALETTE[\"teal\"], PALETTE[\"deep_blue\"]])\nfor i, v in enumerate([rare_count, common_count]):\n    ax[1].text(i, v + 1, f\"{v:,}\", ha=\"center\", va=\"bottom\", color=PALETTE[\"slate_gray\"], fontsize=11)\n\nax[1].set_title(\"Counts of Rare vs Common Actions\", color=PALETTE[\"slate_gray\"], fontsize=14, pad=12)\nax[1].set_ylabel(\"Number of Actions\")\nax[1].spines[\"top\"].set_visible(False)\nax[1].spines[\"right\"].set_visible(False)\nax[1].grid(axis=\"y\", linestyle=\"--\", alpha=0.35)\n\nplt.suptitle(\n    f\"Class Imbalance Analysis\\nMost common: {most_common:,} • Least common: {least_common:,} • Ratio: {imbalance_ratio:.1f}x\",\n    color=PALETTE[\"slate_gray\"], fontsize=13, y=1.02\n)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:15:57.461865Z","iopub.execute_input":"2025-09-19T12:15:57.462229Z","iopub.status.idle":"2025-09-19T12:15:58.430105Z","shell.execute_reply.started":"2025-09-19T12:15:57.462205Z","shell.execute_reply":"2025-09-19T12:15:58.429110Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## <span style=\"color:#1E3A8A;\">Behavioral Annotation Analysis Results</span>\n\n### <span style=\"color:#0D9488;\">Annotation Scale and Density</span>\n- **Dataset Scope**: 44,665 behavioral instances across **863 videos** from **19 labs**, providing substantial supervision volume.  \n- **Ontology Coverage**: **37 categories** spanning locomotion, social interactions, and reproductive behaviors.  \n- **Annotation Density**: Mean **52 events per video** with strong inter-lab variance → heterogeneous protocols.  \n\n---\n\n### <span style=\"color:#0D9488;\">Class Distribution Characteristics</span>\n- **Extreme Imbalance**: Long-tail with **37,837 : 1 ratio** between `sniff` (45% of data) and `ejaculate` (1 instance).  \n- **Rare Class Challenge**: Ultra-rare categories require **special handling strategies** for training.  \n- **Common Behavior Dominance**: 15 behaviors have **≥1,000 instances**, supporting robust classification.  \n- **Implication**: Sampling techniques, weighted loss functions, and metric design are essential.  \n\n---\n\n### <span style=\"color:#0D9488;\">Agent Interaction Patterns</span>\n- **Social vs Self-directed**: Social behaviors dominate → models must capture **multi-agent spatial–temporal relations**.  \n- **Architectural Impact**:  \n  - *Self-directed*: single-agent feature extraction.  \n  - *Social*: relational reasoning across multiple trajectories.  \n\n---\n\n### <span style=\"color:#0D9488;\">Temporal Signature Analysis</span>\n- **Duration Heterogeneity**: Ranges from brief (`sniff`: 58 frames) to extended (`rest`: 534 frames).  \n- **Variance Patterns**: Durations vary by **2–3 orders of magnitude** across classes.  \n- **Implication**: Outliers and heterogeneity demand **multi-scale modeling** and preprocessing.  \n\n---\n\n### <span style=\"color:#0D9488;\">Cross-Laboratory Protocol Assessment</span>\n- **Universal Behaviors**: Core categories (`sniff`, `approach`, `attack`) appear across labs → reliable foundation.  \n- **Lab-Specific Annotations**: Many behaviors appear in limited labs → domain generalization challenge.  \n- **CalMS21 Bias**: Broader coverage by CalMS21 may dominate model learning.  \n\n---\n\n## <span style=\"color:#1E3A8A;\">Section 3A: Pose Estimation Data Quality & Tracking Consistency</span>\n\n### <span style=\"color:#0D9488;\">Research Framework</span>\n- **Data Quality**: Evaluate pose reliability, coordinate standardization, tracking consistency.  \n- **Feature Engineering**: Profile bodypart availability and coordinate distributions.  \n- **Cross-Domain**: Identify tracking methodology differences requiring normalization.  \n\n---\n\n### <span style=\"color:#0D9488;\">Analytical Methodology</span>\n- *Sampling*: Extract representative pose data across labs.  \n- *Bodypart Inventory*: Enumerate anatomical landmarks + naming conventions.  \n- *Coordinate Analysis*: Study pixel ranges, scaling factors, transformations.  \n- *Quality Control*: Quantify missing data, tracking consistency, smoothness.  \n\n---\n\n### <span style=\"color:#0D9488;\">Expected Outcomes</span>\n- **Preprocessing Pipeline**: Normalization strategies from coordinate distributions.  \n- **Feature Selection**: Bodypart reliability ranking for robust features.  \n- **Domain Adaptation**: Quantify heterogeneity → scaling and normalization requirements.  \n\n---\n\n### <span style=\"color:#0D9488;\">Critical Analysis Questions</span>\n- **Standardization**: How consistent are bodypart labels across labs?  \n- **Completeness**: How frequent are missing coordinates?  \n- **Normalization**: Do coordinate ranges require rescaling for cross-lab generalization?  \n- **Consistency**: How smooth are trajectories over time → artifact detection?  \n","metadata":{}},{"cell_type":"code","source":"if \"usable_train_rows\" not in globals():\n    anno_root = folders[\"train_annotation\"]\n    valid_annos = set()\n    if anno_root.exists():\n        for lab_dir in sorted(anno_root.iterdir()):\n            if not lab_dir.is_dir():\n                continue\n            for f in lab_dir.glob(\"*.parquet\"):\n                valid_annos.add((lab_dir.name, f.stem))\n    usable_mask = train_csv.apply(lambda r: (r[\"lab_id\"], r[\"video_id\"]) in valid_annos, axis=1)\n    usable_train_rows = train_csv[usable_mask]\n\nsample_labs = list(pd.unique(usable_train_rows[\"lab_id\"]))[:8]\nsample_videos = []\nfor lab in sample_labs:\n    lab_videos = usable_train_rows[usable_train_rows[\"lab_id\"] == lab].head(3)\n    sample_videos.extend(lab_videos[[\"lab_id\", \"video_id\"]].values.tolist())\n\ntracking_samples = []\nbodypart_inventory = {}\ncoordinate_stats = []\n\nfor lab_id, video_id in sample_videos:\n    track_path = DATA / \"train_tracking\" / lab_id / f\"{video_id}.parquet\"\n    if not track_path.exists():\n        continue\n    try:\n        df_track = pd.read_parquet(track_path)\n        df_track = df_track.head(1000).copy()\n        df_track[\"lab_id\"] = lab_id\n        df_track[\"video_id\"] = video_id\n        tracking_samples.append(df_track)\n        unique_bodyparts = df_track[\"bodypart\"].unique()\n        bodypart_inventory[lab_id] = sorted(unique_bodyparts.tolist())\n        coord_stats = df_track[[\"x\", \"y\"]].describe()\n        coordinate_stats.append({\n            \"lab_id\": lab_id,\n            \"video_id\": video_id,\n            \"x_min\": coord_stats.loc[\"min\", \"x\"],\n            \"x_max\": coord_stats.loc[\"max\", \"x\"],\n            \"y_min\": coord_stats.loc[\"min\", \"y\"],\n            \"y_max\": coord_stats.loc[\"max\", \"y\"],\n            \"x_mean\": coord_stats.loc[\"mean\", \"x\"],\n            \"y_mean\": coord_stats.loc[\"mean\", \"y\"],\n            \"sampled_frames\": len(df_track),\n            \"unique_bodyparts\": len(unique_bodyparts)\n        })\n    except Exception:\n        continue\n\nif len(tracking_samples) == 0:\n    display(HTML(f\"<div style='color:{PALETTE['slate_gray']}'>No tracking samples found for the selected labs/videos.</div>\"))\nelse:\n    combined_tracking = pd.concat(tracking_samples, ignore_index=True)\n    coord_df = pd.DataFrame(coordinate_stats).sort_values([\"lab_id\", \"video_id\"]).reset_index(drop=True)\n\n    coord_df_display = coord_df.copy()\n    coord_df_display[\"x_range\"] = (coord_df_display[\"x_max\"] - coord_df_display[\"x_min\"]).round(1)\n    coord_df_display[\"y_range\"] = (coord_df_display[\"y_max\"] - coord_df_display[\"y_min\"]).round(1)\n    coord_df_display = coord_df_display[[\n        \"lab_id\", \"video_id\", \"sampled_frames\", \"unique_bodyparts\",\n        \"x_min\", \"x_max\", \"x_range\", \"x_mean\",\n        \"y_min\", \"y_max\", \"y_range\", \"y_mean\"\n    ]]\n\n    display(HTML(f\"\"\"\n    <div style=\"border-left:4px solid {PALETTE['teal']}; padding:10px; margin-bottom:6px;\">\n      <h3 style=\"margin:0; color:{PALETTE['deep_blue']}; font-family:sans-serif;\">Pose Data Sample — Coordinate Summary</h3>\n      <div style=\"color:{PALETTE['slate_gray']}; margin-top:6px;\">\n        <strong>Total sampled frames</strong>: {len(combined_tracking):,} &nbsp;•&nbsp;\n        <strong>Labs sampled</strong>: {combined_tracking['lab_id'].nunique()} &nbsp;•&nbsp;\n        <strong>Videos sampled</strong>: {combined_tracking['video_id'].nunique()} &nbsp;•&nbsp;\n        <strong>Unique mice</strong>: {combined_tracking['mouse_id'].nunique()}\n      </div>\n    </div>\n    \"\"\"))\n\n    display(\n        coord_df_display.style.format({\n            \"sampled_frames\":\"{:,}\",\n            \"unique_bodyparts\":\"{:,}\",\n            \"x_min\":\"{:.1f}\", \"x_max\":\"{:.1f}\", \"x_range\":\"{:.1f}\", \"x_mean\":\"{:.1f}\",\n            \"y_min\":\"{:.1f}\", \"y_max\":\"{:.1f}\", \"y_range\":\"{:.1f}\", \"y_mean\":\"{:.1f}\"\n        }).set_table_styles([{\"selector\":\"th\",\"props\":[(\"background-color\",PALETTE[\"muted\"]),(\"color\",\"white\")]}])\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:16:13.325227Z","iopub.execute_input":"2025-09-19T12:16:13.326287Z","iopub.status.idle":"2025-09-19T12:16:15.361549Z","shell.execute_reply.started":"2025-09-19T12:16:13.326243Z","shell.execute_reply":"2025-09-19T12:16:15.360675Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"all_bodyparts = set()\nfor bodyparts in bodypart_inventory.values():\n    all_bodyparts.update(bodyparts)\n\nbodypart_matrix = pd.DataFrame(index=sorted(bodypart_inventory.keys()), columns=sorted(all_bodyparts))\nfor lab, bodyparts in bodypart_inventory.items():\n    for bp in all_bodyparts:\n        bodypart_matrix.loc[lab, bp] = 1 if bp in bodyparts else 0\nbodypart_matrix = bodypart_matrix.astype(int)\n\ntotal_bodyparts = len(all_bodyparts)\nper_lab_counts = bodypart_matrix.sum(axis=1)\nmin_bp, max_bp = int(per_lab_counts.min()), int(per_lab_counts.max())\n\nplt.figure(figsize=(14, max(4, 0.25 * bodypart_matrix.shape[0])), dpi=120)\ncmap = sns.color_palette([PALETTE[\"muted\"], PALETTE[\"teal\"]])\nax = sns.heatmap(\n    bodypart_matrix,\n    cmap=cmap,\n    cbar=False,\n    linewidths=0.25,\n    linecolor=\"#E6EEF2\"\n)\nax.set_title(\n    f\"Bodypart Tracking Overview — {total_bodyparts} unique bodyparts \"\n    f\"(per lab range: {min_bp}–{max_bp})\",\n    color=PALETTE[\"slate_gray\"], fontsize=14, pad=12\n)\nax.set_xlabel(\"Bodyparts\", color=PALETTE[\"slate_gray\"])\nax.set_ylabel(\"Labs\", color=PALETTE[\"slate_gray\"])\nax.tick_params(axis=\"x\", rotation=65)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:16:15.689803Z","iopub.execute_input":"2025-09-19T12:16:15.690298Z","iopub.status.idle":"2025-09-19T12:16:16.078690Z","shell.execute_reply.started":"2025-09-19T12:16:15.690270Z","shell.execute_reply":"2025-09-19T12:16:16.077727Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"common_bodyparts = bodypart_matrix.sum(axis=0).sort_values(ascending=False)\ntop15_bodyparts = common_bodyparts.head(15).reset_index()\ntop15_bodyparts.columns = [\"Bodypart\", \"Lab Count\"]\n\nplt.figure(figsize=(12, 6), dpi=120)\nax = sns.barplot(\n    data=top15_bodyparts,\n    x=\"Lab Count\", y=\"Bodypart\",\n    palette=[PALETTE[\"teal\"]] * len(top15_bodyparts)\n)\n\nfor i, v in enumerate(top15_bodyparts[\"Lab Count\"]):\n    ax.text(v + 0.3, i, f\"{v}\", va=\"center\", ha=\"left\", color=PALETTE[\"slate_gray\"], fontsize=10)\n\nax.set_title(\"Most Commonly Tracked Bodyparts (Top 15)\", color=PALETTE[\"slate_gray\"], fontsize=14, pad=12)\nax.set_xlabel(\"Number of Labs Tracking\")\nax.set_ylabel(\"\")\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.grid(axis=\"x\", linestyle=\"--\", alpha=0.35)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:16:18.359058Z","iopub.execute_input":"2025-09-19T12:16:18.359388Z","iopub.status.idle":"2025-09-19T12:16:18.775467Z","shell.execute_reply.started":"2025-09-19T12:16:18.359365Z","shell.execute_reply":"2025-09-19T12:16:18.774449Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(12, 7), dpi=120)\n\nfor i, row in coord_df.iterrows():\n    plt.plot([row[\"x_min\"], row[\"x_max\"]], [i, i], color=PALETTE[\"deep_blue\"], linewidth=2)\n    plt.scatter(row[\"x_min\"], i, color=PALETTE[\"teal\"], s=50, zorder=3)\n    plt.scatter(row[\"x_max\"], i, color=PALETTE[\"teal\"], s=50, zorder=3)\n\nplt.yticks(range(len(coord_df)), coord_df[\"lab_id\"])\nplt.xlabel(\"X-coordinate Range (pixels)\", color=PALETTE[\"slate_gray\"])\nplt.title(\"Coordinate System Analysis — X-axis Ranges by Lab\", color=PALETTE[\"slate_gray\"], fontsize=14, pad=12)\nplt.grid(axis=\"x\", linestyle=\"--\", alpha=0.35)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:16:20.202062Z","iopub.execute_input":"2025-09-19T12:16:20.202875Z","iopub.status.idle":"2025-09-19T12:16:21.071351Z","shell.execute_reply.started":"2025-09-19T12:16:20.202845Z","shell.execute_reply":"2025-09-19T12:16:21.070185Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 3, figsize=(18, 8), dpi=120, constrained_layout=True)\n\n# 1) Arena centers by lab (mean x, mean y)\nax = axes[0]\npalette_lab = sns.color_palette(\"tab20\", n_colors=len(coord_df))\nsc = ax.scatter(coord_df[\"x_mean\"], coord_df[\"y_mean\"], c=range(len(coord_df)), cmap=\"tab20\", s=90, alpha=0.85)\nfor i, row in coord_df.reset_index().iterrows():\n    ax.annotate(row[\"lab_id\"][:12], (row[\"x_mean\"], row[\"y_mean\"]), xytext=(6, 4), textcoords=\"offset points\",\n                fontsize=8, color=PALETTE[\"slate_gray\"])\nax.set_xlabel(\"Mean X Coordinate\", color=PALETTE[\"slate_gray\"])\nax.set_ylabel(\"Mean Y Coordinate\", color=PALETTE[\"slate_gray\"])\nax.set_title(\"Arena Centers by Lab\", color=PALETTE[\"slate_gray\"], fontsize=13, pad=8)\n\n# 2) Arena dimensions by lab (range x, range y)\nax = axes[1]\nx_range = coord_df[\"x_max\"] - coord_df[\"x_min\"]\ny_range = coord_df[\"y_max\"] - coord_df[\"y_min\"]\nax.scatter(x_range, y_range, c=range(len(coord_df)), cmap=\"tab20\", s=90, alpha=0.85)\nfor i, row in coord_df.reset_index().iterrows():\n    xr = row[\"x_max\"] - row[\"x_min\"]\n    yr = row[\"y_max\"] - row[\"y_min\"]\n    ax.annotate(row[\"lab_id\"][:10], (xr, yr), xytext=(6, 4), textcoords=\"offset points\", fontsize=8, color=PALETTE[\"slate_gray\"])\nax.set_xlabel(\"X Range (px)\", color=PALETTE[\"slate_gray\"])\nax.set_ylabel(\"Y Range (px)\", color=PALETTE[\"slate_gray\"])\nax.set_title(\"Arena Dimensions by Lab\", color=PALETTE[\"slate_gray\"], fontsize=13, pad=8)\n\n# 3) Coordinate distribution sample (pose points colored by lab)\nax = axes[2]\nsample_n = min(12000, len(combined_tracking))\nsample_df = combined_tracking.sample(sample_n, random_state=42)\nlabs = sample_df[\"lab_id\"].unique()\npal = sns.color_palette(\"tab20\", n_colors=len(labs))\nlab_to_color = {lab: pal[i % len(pal)] for i, lab in enumerate(labs)}\nax.scatter(sample_df[\"x\"], sample_df[\"y\"], s=6,\n           c=[lab_to_color[l] for l in sample_df[\"lab_id\"].values], alpha=0.6)\nax.invert_yaxis()\nax.set_xlabel(\"x (pixels)\", color=PALETTE[\"slate_gray\"])\nax.set_ylabel(\"y (pixels)\", color=PALETTE[\"slate_gray\"])\nax.set_title(\"Coordinate Distribution Sample (colored by lab)\", color=PALETTE[\"slate_gray\"], fontsize=13, pad=8)\n# compact legend: show only top 8 labs to avoid clutter\ntop_labs_for_legend = list(pd.Series(sample_df[\"lab_id\"]).value_counts().head(8).index)\nhandles = [plt.Line2D([], [], marker=\"o\", linestyle=\"\", color=lab_to_color[l], markersize=6) for l in top_labs_for_legend]\nax.legend(handles, top_labs_for_legend, title=\"Top labs\", bbox_to_anchor=(1.02, 1), loc=\"upper left\", frameon=False)\n\nplt.suptitle(\"Coordinate System Summary — centers, dimensions, and sampled point cloud\", color=PALETTE[\"slate_gray\"], fontsize=14, y=1.02)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:16:37.987213Z","iopub.execute_input":"2025-09-19T12:16:37.987569Z","iopub.status.idle":"2025-09-19T12:16:39.460796Z","shell.execute_reply.started":"2025-09-19T12:16:37.987546Z","shell.execute_reply":"2025-09-19T12:16:39.459751Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if \"sample_videos\" not in globals():\n    sample_labs = list(pd.unique(usable_train_rows[\"lab_id\"]))[:8]\n    sample_videos = []\n    for lab in sample_labs:\n        lab_videos = usable_train_rows[usable_train_rows[\"lab_id\"] == lab].head(3)\n        sample_videos.extend(lab_videos[[\"lab_id\", \"video_id\"]].values.tolist())\n\ntemporal_analysis = []\nfor lab_id, video_id in sample_videos[:10]:\n    track_path = DATA / \"train_tracking\" / lab_id / f\"{video_id}.parquet\"\n    if not track_path.exists():\n        continue\n    try:\n        df = pd.read_parquet(track_path)\n        df_pivot = df.pivot_table(index=\"video_frame\", columns=[\"mouse_id\", \"bodypart\"], values=[\"x\", \"y\"])\n        if len(df_pivot) > 100:\n            sample_coords = df_pivot.iloc[:100, :4].values.astype(float)\n            velocities = np.diff(sample_coords, axis=0)\n            vel_magnitude = np.sqrt(np.nansum(velocities**2, axis=1))\n            temporal_analysis.append({\n                \"lab_id\": lab_id,\n                \"video_id\": video_id,\n                \"mean_velocity\": float(np.nanmean(vel_magnitude)),\n                \"max_velocity\": float(np.nanmax(vel_magnitude)),\n                \"velocity_std\": float(np.nanstd(vel_magnitude))\n            })\n    except Exception:\n        continue\n\ntemporal_df = pd.DataFrame(temporal_analysis)\nif temporal_df.empty:\n    raise ValueError(\"No temporal samples found for the chosen videos. Re-run sampling or choose different videos.\")\n\ntemporal_df = temporal_df.round({\n    \"mean_velocity\": 2,\n    \"max_velocity\": 2,\n    \"velocity_std\": 2\n}).sort_values([\"mean_velocity\", \"max_velocity\"], ascending=[False, False]).reset_index(drop=True)\n\ndisplay(\n    temporal_df.style.background_gradient(\n        subset=[\"mean_velocity\", \"max_velocity\", \"velocity_std\"], cmap=\"YlGnBu\"\n    ).set_caption(\"Temporal Consistency Analysis — Velocity Statistics\")\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:16:45.910977Z","iopub.execute_input":"2025-09-19T12:16:45.911297Z","iopub.status.idle":"2025-09-19T12:16:58.096240Z","shell.execute_reply.started":"2025-09-19T12:16:45.911276Z","shell.execute_reply":"2025-09-19T12:16:58.095193Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if \"missing_summary\" not in globals():\n    # ensure usable_train_rows exists\n    if \"usable_train_rows\" not in globals():\n        anno_root = folders[\"train_annotation\"]\n        valid_annos = set()\n        if anno_root.exists():\n            for lab_dir in sorted(anno_root.iterdir()):\n                if not lab_dir.is_dir():\n                    continue\n                for f in lab_dir.glob(\"*.parquet\"):\n                    valid_annos.add((lab_dir.name, f.stem))\n        usable_mask = train_csv.apply(lambda r: (r[\"lab_id\"], r[\"video_id\"]) in valid_annos, axis=1)\n        usable_train_rows = train_csv[usable_mask]\n\n    # ensure combined_tracking exists (sample if necessary)\n    if \"combined_tracking\" not in globals():\n        sample_labs = list(pd.unique(usable_train_rows[\"lab_id\"]))[:8]\n        sample_videos = []\n        for lab in sample_labs:\n            lab_videos = usable_train_rows[usable_train_rows[\"lab_id\"] == lab].head(3)\n            sample_videos.extend(lab_videos[[\"lab_id\", \"video_id\"]].values.tolist())\n\n        tracking_samples = []\n        for lab_id, video_id in sample_videos:\n            track_path = DATA / \"train_tracking\" / lab_id / f\"{video_id}.parquet\"\n            if not track_path.exists():\n                continue\n            try:\n                df_track = pd.read_parquet(track_path)\n                df_track = df_track.head(1000).copy()\n                df_track[\"lab_id\"] = lab_id\n                df_track[\"video_id\"] = video_id\n                tracking_samples.append(df_track)\n            except Exception:\n                continue\n\n        if len(tracking_samples) == 0:\n            raise RuntimeError(\"No tracking samples found to compute missing_summary. Ensure tracking files are available.\")\n        combined_tracking = pd.concat(tracking_samples, ignore_index=True)\n\n    # groupby + apply with include_groups=False to silence deprecation warning\n    missing_analysis = combined_tracking.groupby(['lab_id', 'mouse_id', 'bodypart']).apply(\n        lambda x: pd.Series({\n            'total_frames': len(x),\n            'missing_x': int(x['x'].isnull().sum()),\n            'missing_y': int(x['y'].isnull().sum()),\n            'missing_either': int((x['x'].isnull() | x['y'].isnull()).sum())\n        }),\n        include_groups=False\n    ).reset_index()\n\n    # aggregate to lab-level\n    missing_summary = missing_analysis.groupby('lab_id').agg({\n        'missing_either': ['sum', 'mean'],\n        'total_frames': 'sum'\n    }).round(3)\n    missing_summary.columns = ['total_missing', 'avg_missing_rate', 'total_frames']\n    missing_summary['missing_percentage'] = (missing_summary['total_missing'] / missing_summary['total_frames'] * 100).round(2)\n    missing_summary = missing_summary.reset_index()\n\n# compute KPIs using coord_df and missing_summary (assumes coord_df exists)\ncoord_range_variation = coord_df[\"x_max\"].max() - coord_df[\"x_min\"].min()\narena_cv = ((coord_df[\"x_max\"] - coord_df[\"x_min\"]).std() /\n            (coord_df[\"x_max\"] - coord_df[\"x_min\"]).mean()) if (coord_df[\"x_max\"] - coord_df[\"x_min\"]).mean() != 0 else np.nan\nmissing_min, missing_max = missing_summary[\"missing_percentage\"].min(), missing_summary[\"missing_percentage\"].max()\n\nfrom IPython.display import HTML, display\ndisplay(HTML(f\"\"\"\n<div style=\"border-left:4px solid {PALETTE['teal']}; padding:12px; background:#ffffff; font-family:sans-serif; max-width:720px;\">\n  <h3 style=\"margin:0; color:{PALETTE['deep_blue']};\">Summary Statistics</h3>\n  <ul style=\"margin-top:8px; color:{PALETTE['slate_gray']}; line-height:1.6;\">\n    <li><strong>Coordinate range variation</strong>: {coord_range_variation:.0f} px (X-axis span across labs)</li>\n    <li><strong>Arena size consistency</strong>: CV = {arena_cv:.2f}</li>\n    <li><strong>Missing data rate</strong>: {missing_min:.1f}% – {missing_max:.1f}% across labs</li>\n  </ul>\n</div>\n\"\"\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-19T12:20:56.848035Z","iopub.execute_input":"2025-09-19T12:20:56.848401Z","iopub.status.idle":"2025-09-19T12:20:56.867634Z","shell.execute_reply.started":"2025-09-19T12:20:56.848376Z","shell.execute_reply":"2025-09-19T12:20:56.866717Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}