{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Do mice multitask?\n\nBased on wafaa alayoubi's excellent notebook [1.Full EDA]('https://www.kaggle.com/code/wafaaalayoubi/1-full-eda'), I decided to look a bit further at one aspect of the labelling:  are there any labels where one mouse is labelled with two behavors at the same time.  The short answer is yes, but quite infrequently; less than 0.2% of the behaviors and 0.2% of the frames.\n\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport pyarrow as pa\nimport pyarrow.parquet as pq\nimport seaborn as sns\nfrom tqdm.auto import tqdm\nimport os","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:04.852927Z","iopub.execute_input":"2025-10-16T11:49:04.853629Z","iopub.status.idle":"2025-10-16T11:49:04.860043Z","shell.execute_reply.started":"2025-10-16T11:49:04.853592Z","shell.execute_reply":"2025-10-16T11:49:04.858988Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the path to your data\nDATA_PATH = '/kaggle/input/MABe-mouse-behavior-detection/'\n\n# Load the metadata files\nprint(\"Loading train.csv (metadata)...\")\ndf_train_meta = pd.read_csv(DATA_PATH + 'train.csv')\n\nprint(\"\\n--- Train Metadata ---\")\nprint(f\"Shape: {df_train_meta.shape}\")\ndisplay(df_train_meta.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:04.861736Z","iopub.execute_input":"2025-10-16T11:49:04.862248Z","iopub.status.idle":"2025-10-16T11:49:04.993697Z","shell.execute_reply.started":"2025-10-16T11:49:04.862211Z","shell.execute_reply":"2025-10-16T11:49:04.992564Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# This cell is designed to be self-contained. It will build the full annotation\n# dataframe if it doesn't already exist in memory.  (Copied directly from [1.Full EDA]('https://www.kaggle.com/code/wafaaalayoubi/1-full-eda'))\nif 'df_annotations_full' not in locals():\n    print(\"Building the full annotations dataframe by combining all individual annotation files...\")\n\n    all_annotations_list = []\n    for index, row in tqdm(df_train_meta.iterrows(), total=df_train_meta.shape[0]):\n        lab_id = row['lab_id']\n        video_id = row['video_id']\n        annotation_path = os.path.join(DATA_PATH, 'train_annotation', lab_id, f'{video_id}.parquet')\n        \n        if os.path.exists(annotation_path):\n            temp_df = pd.read_parquet(annotation_path)\n            temp_df['video_id'] = video_id\n            all_annotations_list.append(temp_df)\n\n    df_annotations_full = pd.concat(all_annotations_list, ignore_index=True)\n    print(f\"\\nSuccessfully created full annotation dataframe with shape: {df_annotations_full.shape}\")\nelse:\n    print(\"Full annotation dataframe already exists in memory. Proceeding with analysis.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:04.994602Z","iopub.execute_input":"2025-10-16T11:49:04.994835Z","iopub.status.idle":"2025-10-16T11:49:14.507012Z","shell.execute_reply.started":"2025-10-16T11:49:04.994815Z","shell.execute_reply":"2025-10-16T11:49:14.506177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_annotations_full.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:14.508708Z","iopub.execute_input":"2025-10-16T11:49:14.509076Z","iopub.status.idle":"2025-10-16T11:49:14.519429Z","shell.execute_reply.started":"2025-10-16T11:49:14.509052Z","shell.execute_reply":"2025-10-16T11:49:14.518497Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check if, for each video and agent, the start/stop frames of actions overlap with different actions\ndef check_action_frame_overlap(df):\n    overlap_found = False\n    grouped = df.groupby(['video_id', 'agent_id'])\n    for (video_id, agent_id), group in grouped:\n        # Sort by start_frame and include action information\n        sorted_group = group.sort_values('start_frame')\n        intervals = sorted_group[['start_frame', 'stop_frame', 'action']].values\n        \n        for i in range(1, len(intervals)):\n            prev_start, prev_stop, prev_action = intervals[i-1]\n            curr_start, curr_stop, curr_action = intervals[i]\n            \n            # Check for overlap AND different actions\n            if curr_start < prev_stop and prev_action != curr_action:\n                print(f\"Overlap found in video {video_id}, agent {agent_id}: \"\n                      f\"'{prev_action}' (frames {prev_start}-{prev_stop}) overlaps with \"\n                      f\"'{curr_action}' (frames {curr_start}-{curr_stop})\")\n                overlap_found = True\n    \n    if not overlap_found:\n        print(\"✅ No overlapping frames found between different actions for any agent in any video.\")\n    return not overlap_found\n\ncheck_action_frame_overlap(df_annotations_full)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:14.520522Z","iopub.execute_input":"2025-10-16T11:49:14.520802Z","iopub.status.idle":"2025-10-16T11:49:15.488147Z","shell.execute_reply.started":"2025-10-16T11:49:14.520773Z","shell.execute_reply":"2025-10-16T11:49:15.487227Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Examine one 'multitasking event'","metadata":{}},{"cell_type":"code","source":"# Filter for video 44566106 and agent 1, and check for the specified frame intervals\nvideo_id = 44566106\nagent_id = 1\n\nmask = (\n    (df_annotations_full['video_id'] == video_id) &\n    (df_annotations_full['agent_id'] == agent_id) &\n    (\n        ((df_annotations_full['start_frame'] == 15907) & (df_annotations_full['stop_frame'] == 15957)) |\n        ((df_annotations_full['start_frame'] == 15930) & (df_annotations_full['stop_frame'] == 15953))\n    )\n)\n\ndf_annotations_full[mask]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:15.489130Z","iopub.execute_input":"2025-10-16T11:49:15.489424Z","iopub.status.idle":"2025-10-16T11:49:15.504603Z","shell.execute_reply.started":"2025-10-16T11:49:15.489379Z","shell.execute_reply":"2025-10-16T11:49:15.503431Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"what's happening is that this mouse is avoiding mouse 3 while approaching mouse 2.  Which I guess is logical.\n","metadata":{}},{"cell_type":"markdown","source":"## Count of number of multitasking events","metadata":{}},{"cell_type":"code","source":"# Count the number of apparent conflicts (overlapping intervals with different actions)\nconflict_count = 0\ntotal_behaviors = 0\nconflict_frames = 0\n\ngrouped = df_annotations_full.groupby(['video_id', 'agent_id'])\nfor (video_id, agent_id), group in grouped:\n    sorted_group = group.sort_values('start_frame')\n    intervals = sorted_group[['start_frame', 'stop_frame', 'action']].values\n    for i in range(1, len(intervals)):\n        prev_start, prev_stop, prev_action = intervals[i-1]\n        curr_start, curr_stop, curr_action = intervals[i]\n        if curr_start < prev_stop and prev_action != curr_action:\n            conflict_count += 1\n            # Calculate overlapping frames\n            overlap_start = curr_start\n            overlap_stop = min(prev_stop, curr_stop)\n            conflict_frames += overlap_stop - overlap_start + 1\n        total_behaviors += 1\n\ntotal_frames = (df_annotations_full['stop_frame'] - df_annotations_full['start_frame'] + 1).sum()\n\nprint(f\"Total apparent conflicts: {conflict_count}, total behaviors: {total_behaviors}, conflict fraction: {conflict_count / total_behaviors:.6f}  \")\nprint(f\"Total frames in conflicts: {conflict_frames}, total annotated frames: {total_frames} conflict fraction: {conflict_frames / total_frames:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:15.505566Z","iopub.execute_input":"2025-10-16T11:49:15.505817Z","iopub.status.idle":"2025-10-16T11:49:16.440196Z","shell.execute_reply.started":"2025-10-16T11:49:15.505795Z","shell.execute_reply":"2025-10-16T11:49:16.439289Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"So while mice can multitask (and labellers can label mice as multitasking), it's really quite rare.","metadata":{}},{"cell_type":"markdown","source":"## What are mice doing when they multitask?","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\n# Create a matrix of action conflicts (rows: previous action, columns: current action)\nactions = df_annotations_full['action'].unique()\nconflict_matrix = pd.DataFrame(0, index=actions, columns=actions)\n\ngrouped = df_annotations_full.groupby(['video_id', 'agent_id'])\nfor (video_id, agent_id), group in grouped:\n    sorted_group = group.sort_values('start_frame')\n    intervals = sorted_group[['start_frame', 'stop_frame', 'action']].values\n    for i in range(1, len(intervals)):\n        prev_start, prev_stop, prev_action = intervals[i-1]\n        curr_start, curr_stop, curr_action = intervals[i]\n        if curr_start < prev_stop and prev_action != curr_action:\n            conflict_matrix.loc[prev_action, curr_action] += 1\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:16.441063Z","iopub.execute_input":"2025-10-16T11:49:16.441322Z","iopub.status.idle":"2025-10-16T11:49:17.384025Z","shell.execute_reply.started":"2025-10-16T11:49:16.441300Z","shell.execute_reply":"2025-10-16T11:49:17.383000Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Drop any action (row or column) with less than 5 total conflicts from filtered_conflict_matrix\nconflict_sums =conflict_matrix.sum(axis=1) + conflict_matrix.sum(axis=0)\nactions_to_keep = conflict_sums[conflict_sums >= 5].index\nfiltered_conflict_matrix =  conflict_matrix.loc[actions_to_keep, actions_to_keep]\nfiltered_conflict_matrix\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:17.384945Z","iopub.execute_input":"2025-10-16T11:49:17.385230Z","iopub.status.idle":"2025-10-16T11:49:17.399159Z","shell.execute_reply.started":"2025-10-16T11:49:17.385202Z","shell.execute_reply":"2025-10-16T11:49:17.398274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nsns.heatmap(filtered_conflict_matrix, cmap='Blues', annot=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-16T11:49:17.401305Z","iopub.execute_input":"2025-10-16T11:49:17.401645Z","iopub.status.idle":"2025-10-16T11:49:17.881415Z","shell.execute_reply.started":"2025-10-16T11:49:17.401616Z","shell.execute_reply":"2025-10-16T11:49:17.880382Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"So the main form of multitasking is 'huddle' while 'reciprocall sniff'; followed by 'approach' while 'avoid'.","metadata":{}}]}