{"metadata":{"kernelspec":{"name":"ir","display_name":"R","language":"R"},"language_info":{"name":"R","codemirror_mode":"r","pygments_lexer":"r","mimetype":"text/x-r-source","file_extension":".r","version":"4.4.0"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"}],"dockerImageVersionId":30749,"isInternetEnabled":true,"language":"r","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<h3>Findings in this notebook (after data update patch):</h3>\n<div style=\"line-height:24px; font-size:16px\">\n    <ul style=\"list-style:circle\">\n<li>70 videos have ≤2.1% frames missing in tracking files overall, 49 of which show up to 80% missing frames within annotated spans, driven mostly by very short annotations (mice can hide from view/motion blur, the amount is negligible)</li>\n<li>fps×duration in train.csv vs tracking frames span (<strong>index range</strong> present in the tracking files, <strong>invariant to dropped frames</strong>) mismatches:\n    <ul style=\"list-style-type:disc; margin-top:4px;\">\n        <li style=\"list-style-type:'- ';\">shorter tracking - possibly tracking starts late/ends early, or the tracking file is a trimmed subset</li>\n        <li style=\"list-style-type:'- ';\">longer tracking (the tracker’s indices exceed the nominal window): i have no explanation, except fps metadata mismatch/rounding or VFR (Variable Frame Rate)</li>\n    </ul>\n</li>\n<li>153 videos have negative or out-of-video coordinates (a known property of DeepLabCut - the model is extrapolating to predict locations of body parts. Same for MARS and SLEAP?)</li>\n<li>4 labs have arena &gt; video size (metadata inconsistency or cropped field of view?)</li>\n<li>244 videos have annotated actions with duration <= 0.1 seconds (confirmed by the host as correct)</li>\n<li>No annotations: MABe22_keypoints &amp; MABe22_movies (100% videos); PleasantMeerkat missing for 1 video (not an issue, just keep in mind)\n</li>\n<li><strong>(New):</strong> Counterintuitive reciprocal actions (e.g. attack ↔ sniffgenital)</li>\n    </ul>\n     <div style=\"margin-top:20px;\">\n        Data: <a href=\"https://www.kaggle.com/competitions/MABe-mouse-behavior-detection/overview\">MABe Challenge - Social Action Recognition in Mice</a>\n    </div>\n</div>\n","metadata":{}},{"cell_type":"code","source":"library(data.table)\nlibrary(duckdb)\nlibrary(arrow)\nlibrary(DBI)\nlibrary(fs)\nlibrary(tictoc)","metadata":{"_uuid":"051d70d956493feee0c6d64651c6a088724dca2a","_execution_state":"idle","trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:32:38.141408Z","iopub.execute_input":"2025-10-03T20:32:38.143863Z","iopub.status.idle":"2025-10-03T20:32:39.444010Z","shell.execute_reply":"2025-10-03T20:32:39.442137Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"report_issues <- function(dt, label) {\n  cat(label, dt[, uniqueN(video_id)], \"videos from\",\n      dt[, uniqueN(lab_id)], \"labs:\",\n      paste(sort(unique(dt$lab_id)), collapse = \", \"),\n      \"\\n\")\n  invisible(dt)\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:32:39.448196Z","iopub.execute_input":"2025-10-03T20:32:39.519871Z","iopub.status.idle":"2025-10-03T20:32:39.626962Z","shell.execute_reply":"2025-10-03T20:32:39.624813Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Load the data</p>","metadata":{}},{"cell_type":"markdown","source":"**Load train csv with files description:**","metadata":{}},{"cell_type":"code","source":"dir <- \"/kaggle/input/MABe-mouse-behavior-detection/\"\ntrain_meta <- fread(file.path(dir, \"train.csv\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:32:39.629789Z","iopub.execute_input":"2025-10-03T20:32:39.631268Z","iopub.status.idle":"2025-10-03T20:32:39.726785Z","shell.execute_reply":"2025-10-03T20:32:39.724933Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Calculate summary per each file for all parquet files in the train_tracking folder:**\n- min/max x and y coordinates\n- first and last frame\n- number of distinct frames\n- total number of frames\n- number of mice\n- number of body parts","metadata":{}},{"cell_type":"code","source":"con <- dbConnect(duckdb())\ndbExecute(con, \"PRAGMA threads=4;\")\n\nbase <- file.path(dir, \"train_tracking\")\n\nquery <- sprintf(\"\nSELECT\n  filename,\n  MIN(video_frame) AS first_frame,\n  MAX(video_frame) AS last_frame,\n  COUNT(DISTINCT video_frame) AS num_distinct_frames,\n  COUNT(DISTINCT mouse_id) AS num_mice,\n  COUNT(DISTINCT bodypart) AS num_bodyparts,\n  MIN(x) AS min_x, MAX(x) AS max_x,\n  MIN(y) AS min_y, MAX(y) AS max_y\nFROM read_parquet('%s/*/*.parquet', filename=true)\nGROUP BY filename\n\", base)\n\ntic()\ntrack_summary <- as.data.table(dbGetQuery(con, query))\ntoc()\ntrack_summary[, video_id := sub(\"\\\\.parquet$\", \"\", basename(filename))]\ntrack_summary[, filename := NULL]\ntrack_summary[, span_frames := last_frame - first_frame + 1L]\nsetcolorder(track_summary, \"video_id\", before = \"first_frame\")\n\ncat(nrow(track_summary), \"rows,\", ncol(track_summary), \"columns\\n\")\nhead(track_summary)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:32:39.729510Z","iopub.execute_input":"2025-10-03T20:32:39.731011Z","iopub.status.idle":"2025-10-03T20:34:41.235650Z","shell.execute_reply":"2025-10-03T20:34:41.233094Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Load train annotations","metadata":{}},{"cell_type":"code","source":"ann_files <- dir_ls(\n  file.path(dir, \"train_annotation\"),\n  recurse = TRUE, glob = \"*.parquet\"\n)\ntrain_ann <- rbindlist(\n  lapply(ann_files, \\(f) {\n      dt <- as.data.table(read_parquet(f))\n      fn <- gsub(\"\\\\.parquet\", \"\", basename(f))\n      dt[, video_file := fn]\n  }),\n  use.names = TRUE, fill = TRUE\n)\nsetnames(train_ann, \"video_file\",\"video_id\")\ncat(\"Number of files:\", length(ann_files))\nhead(train_ann)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:41.239744Z","iopub.execute_input":"2025-10-03T20:34:41.241528Z","iopub.status.idle":"2025-10-03T20:34:50.467641Z","shell.execute_reply":"2025-10-03T20:34:50.465722Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Missing annotations</p>","metadata":{}},{"cell_type":"markdown","source":"Сheck whether all videos listed in train.csv have corresponding annotation files and tracking files and vice versa.","metadata":{}},{"cell_type":"code","source":"ann_files_dt <- data.table(file = ann_files)[, .(\n    lab_id   = basename(dirname(file)),\n    video_id = basename(gsub(\".parquet\", \"\", file))\n    )\n]\ntrack_files <- dir_ls(\n  file.path(dir, \"train_tracking\"),\n  recurse = TRUE, glob = \"*.parquet\"\n)\ntrack_files_dt <- data.table(file = track_files)[, .(\n    lab_id   = basename(dirname(file)),\n    video_id = basename(gsub(\".parquet\", \"\", file))\n    )\n]\nmeta_files_dt <- train_meta[, .(video_id = as.character(video_id), lab_id)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.470431Z","iopub.execute_input":"2025-10-03T20:34:50.471899Z","iopub.status.idle":"2025-10-03T20:34:50.609063Z","shell.execute_reply":"2025-10-03T20:34:50.607130Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Calculate number of videos with metadata in train.csv, annotations and tracking:","metadata":{}},{"cell_type":"code","source":"meta_files_dt[, num_in_csv := uniqueN(video_id), by = lab_id]\nmeta_files_dt[, `:=` (\n    has_ann      = video_id %in% ann_files_dt$video_id,\n    has_tracking = video_id %in% track_files_dt$video_id\n)]\nmeta_files_dt[, `:=` (\n    num_annotated = sum(has_ann),\n    num_tracking  = sum(has_tracking)\n), by = lab_id]\nsummary <- meta_files_dt[, .(lab_id, num_in_csv, num_tracking, num_annotated)][\n    order(-num_in_csv), unique(.SD)]\nsummary","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.611821Z","iopub.execute_input":"2025-10-03T20:34:50.613317Z","iopub.status.idle":"2025-10-03T20:34:50.665324Z","shell.execute_reply":"2025-10-03T20:34:50.663423Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"summary[num_in_csv != num_tracking | num_in_csv != num_annotated]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.667961Z","iopub.execute_input":"2025-10-03T20:34:50.669436Z","iopub.status.idle":"2025-10-03T20:34:50.691832Z","shell.execute_reply":"2025-10-03T20:34:50.690054Z"},"_kg_hide-input":false},"outputs":[],"execution_count":null},{"cell_type":"code","source":"no_ann <- meta_files_dt[has_ann == FALSE]\nno_ann[, .(\n    miss_videos = uniqueN(video_id),\n    `%`         = round(uniqueN(video_id) / num_in_csv[1] * 100, 1)\n), by = lab_id]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.694486Z","iopub.execute_input":"2025-10-03T20:34:50.696038Z","iopub.status.idle":"2025-10-03T20:34:50.736100Z","shell.execute_reply":"2025-10-03T20:34:50.734243Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: \n    <ul style=\"list-style:circle\">\n        <li>Labs MABe22_keypoints and MABe22_movies have no annotations at all (100% of their videos are missing).</li>\n        <li>Lab PleasantMeerkat has annotations for most videos, but for 1 video annotations file is missing.</li>\n        <li>Lab SparklingTapir has missing annotations for 15 (22%) videos.</li>\n        <li>These videos still can be used e.g. for unsupervised clustering or feature learning.</li>\n    </ul>\n</div>","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Missing frames</p>","metadata":{}},{"cell_type":"markdown","source":"Check for gaps in the tracking frame sequence, to detect potential frame drops or incomplete tracking data.","metadata":{}},{"cell_type":"code","source":"check_frames <- track_summary[\n    , .(video_id, first_frame, last_frame, span_frames, num_distinct_frames)\n]\ncheck_frames[meta_files_dt, on = .(video_id), lab_id := i.lab_id]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.738797Z","iopub.execute_input":"2025-10-03T20:34:50.740285Z","iopub.status.idle":"2025-10-03T20:34:50.778513Z","shell.execute_reply":"2025-10-03T20:34:50.775908Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"First I compare if calculated `span_frames` (last_frame - first_frame + 1) is equal to number of distince frames - meaning there are no gaps:","metadata":{}},{"cell_type":"code","source":"check_frames[, miss_frames := span_frames - num_distinct_frames]\ncheck_frames[, miss_frames_pct := round(miss_frames / span_frames * 100, 1)]\nissues <- check_frames[abs(miss_frames) > 0]\n\nreport_issues(issues, \"Videos with time gaps:\")\nhead(issues[order(-abs(miss_frames_pct))], 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.781390Z","iopub.execute_input":"2025-10-03T20:34:50.782965Z","iopub.status.idle":"2025-10-03T20:34:50.828657Z","shell.execute_reply":"2025-10-03T20:34:50.826681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"quantile(issues$miss_frames_pct)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.831425Z","iopub.execute_input":"2025-10-03T20:34:50.833015Z","iopub.status.idle":"2025-10-03T20:34:50.850558Z","shell.execute_reply":"2025-10-03T20:34:50.848736Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: There are 70 videos with up to 1884 (2.1%) frames missing (not tracked time gaps?) (top 10 shown). It seems negligible.\n</div> ","metadata":{}},{"cell_type":"markdown","source":"Still, let's check if these missing frames fall into any annotated ranges","metadata":{}},{"cell_type":"code","source":"find_gaps <- function(tracks) {\n    frames <- sort(unique(tracks[[\"video_frame\"]]))\n    next_frame <- data.table::shift(frames, type = \"lead\")\n    gaps <- data.table(\n        gap_start = frames + 1L,\n        gap_end   = next_frame - 1L\n    )[(next_frame - frames) > 1L]\n    gaps\n}\n\nfind_gap_overlaps <- function(vid, gaps, train_ann) {\n    ann_vid <- train_ann[video_id == vid]\n  \n    setkey(ann_vid, start_frame, stop_frame)\n    setkey(gaps, gap_start, gap_end)\n    \n    foverlaps(\n        ann_vid, gaps,\n        by.x = c(\"start_frame\", \"stop_frame\"),\n        by.y = c(\"gap_start\", \"gap_end\"),\n        type = \"any\", nomatch = 0L\n    )\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.853318Z","iopub.execute_input":"2025-10-03T20:34:50.854816Z","iopub.status.idle":"2025-10-03T20:34:50.869201Z","shell.execute_reply":"2025-10-03T20:34:50.867274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tic()\ndt <- lapply(issues$video_id, function(vid) {\n    tracks <- as.data.table(read_parquet(track_files[grepl(vid, track_files)]))\n    gaps   <- find_gaps(tracks)\n    find_gap_overlaps(vid, gaps, train_ann)\n}) |> rbindlist()\ntoc()\n\n# count number of frames in each annotated interval\ndt[, num_frames := stop_frame - start_frame + 1L]\ndt[meta_files_dt, on = .(video_id), lab_id := i.lab_id]\n\n# count missing frames inside annotated intervals\ndt[, `:=`(\n      overlap_start = pmax(start_frame, gap_start),\n      overlap_end   = pmin(stop_frame, gap_end)\n)]\ndt[, num_miss_frames := pmax(0L, overlap_end - overlap_start + 1L)]\ndt[, num_miss_frames_pct := round(num_miss_frames / num_frames * 100, 1)]\ndt[, c(\"overlap_start\", \"overlap_end\") := NULL]\n\nreport_issues(dt, \"Videos with annotated time gaps:\")\nhead(dt[order(-num_miss_frames_pct)], 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:50.871975Z","iopub.execute_input":"2025-10-03T20:34:50.873516Z","iopub.status.idle":"2025-10-03T20:34:58.612433Z","shell.execute_reply":"2025-10-03T20:34:58.610633Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's calcuate percent of missing frames across all intervals for each action in each lab:","metadata":{}},{"cell_type":"code","source":"issues <- dt[, .(\n    sum_frames = sum(num_frames),\n    sum_miss_frames = sum(num_miss_frames),\n    sum_miss_frames_pct = round(sum(num_miss_frames) / sum(num_frames) * 100, 1)\n), by = .(video_id, action)]\nissues[sum_miss_frames_pct > 10][order(-sum_miss_frames_pct)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.615127Z","iopub.execute_input":"2025-10-03T20:34:58.616552Z","iopub.status.idle":"2025-10-03T20:34:58.646108Z","shell.execute_reply":"2025-10-03T20:34:58.644290Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: There are 49 videos with up to 80.0% missing frames inside specific annotated intervals (top 10 shown in the first table). Given that these percentages mostly come from very short annotations (second table), this also seems negligible.\n</div>","metadata":{}},{"cell_type":"markdown","source":"[The hosts addition](https://www.kaggle.com/competitions/MABe-mouse-behavior-detection/discussion/609092#3293523): *\"Sometimes there are frames where no body parts are detected. In AdaptableSnail this happens because there are some covered spots where mice can hide from view. In some other datasets, eg JovialSwallow, there's so much motion blur during attack that the mice aren't really discernible to the tracker. I think you are right that the overall amount of this is fairly small.\"*","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Number of frames missmatch</p>","metadata":{}},{"cell_type":"markdown","source":"Compare the number of frames derived from train.csv (fps × duration) with the span observed in tracking files, to detect inconsistencies in metadata versus actual tracking.","metadata":{}},{"cell_type":"code","source":"check_frames <- track_summary[\n    , .(video_id, first_frame, last_frame, span_frames, num_distinct_frames)\n]\ncheck_frames[meta_files_dt, on = .(video_id), lab_id := i.lab_id]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.648849Z","iopub.execute_input":"2025-10-03T20:34:58.650407Z","iopub.status.idle":"2025-10-03T20:34:58.680426Z","shell.execute_reply":"2025-10-03T20:34:58.678494Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Add video characteristics from train.csv**\n- frames_per_second as `fps`\n- video_duration_sec\n- total frames (frames_per_second * video_duration_sec) as `csv_total_frames` (number of frames in video according to train.csv metadata)","metadata":{}},{"cell_type":"code","source":"train_info <- train_meta[, .(\n    video_id           = as.character(video_id),\n    fps                = frames_per_second,\n    video_duration_sec = video_duration_sec,\n    csv_total_frames   = round(frames_per_second * video_duration_sec)\n)]\ncheck_frames <- check_frames[train_info, on = .(video_id)]\nhead(check_frames)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.684837Z","iopub.execute_input":"2025-10-03T20:34:58.686640Z","iopub.status.idle":"2025-10-03T20:34:58.740288Z","shell.execute_reply":"2025-10-03T20:34:58.738290Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can already see that the number of frames we can get from the video duration and FPS indicated in the train_csv (`csv_total_frames`) is different from that extracted form train_tracking files (`span_frames` = last_frame - first_frame + 1) \n\nLet's examine this difference:","metadata":{}},{"cell_type":"code","source":"check_frames[, diff_frames := span_frames - csv_total_frames]\ncheck_frames[, diff_time_sec := (span_frames / fps) - video_duration_sec]\n\nallowed_diff <- 100\nissues <- check_frames[abs(diff_frames) > allowed_diff]\n\nreport_issues(issues, \"Videos with number of frames missmatch (>100 frames):\")\nhead(issues[order(-abs(diff_frames))], 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.743210Z","iopub.execute_input":"2025-10-03T20:34:58.744817Z","iopub.status.idle":"2025-10-03T20:34:58.794248Z","shell.execute_reply":"2025-10-03T20:34:58.792186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"quantile(issues[diff_frames > 0]$diff_frames)\nquantile(issues[diff_frames < 0]$diff_frames)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.797411Z","iopub.execute_input":"2025-10-03T20:34:58.798985Z","iopub.status.idle":"2025-10-03T20:34:58.828176Z","shell.execute_reply":"2025-10-03T20:34:58.826197Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Check videos having annotations:","metadata":{}},{"cell_type":"code","source":"issues[lab_id %in% c(\"GroovyShrew\", \"PleasantMeerkat\")]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.830928Z","iopub.execute_input":"2025-10-03T20:34:58.832461Z","iopub.status.idle":"2025-10-03T20:34:58.860751Z","shell.execute_reply":"2025-10-03T20:34:58.858931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues[diff_frames > 0, .N]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.863337Z","iopub.execute_input":"2025-10-03T20:34:58.864867Z","iopub.status.idle":"2025-10-03T20:34:58.881159Z","shell.execute_reply":"2025-10-03T20:34:58.879221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues[diff_frames > 0, unique(lab_id)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.883819Z","iopub.execute_input":"2025-10-03T20:34:58.885345Z","iopub.status.idle":"2025-10-03T20:34:58.901389Z","shell.execute_reply":"2025-10-03T20:34:58.899540Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: \n    <ul style=\"list-style:circle\">\n        <li>deviations between <code>csv_total_frames</code> and <code>span_frames</code> are not necessarily an error, e.g. it's expected if duration in train.csv is from real video metadata</li>\n        <li>small deviations could arise from duration rounding? I examined only >100 frames differences</li>\n        <li>large negative mismatches (e.g., missing 58511 frames) can indicate that tracking covers only part of the video</li>\n        <li>positive mismatches in CalMS21* and CRIM13 labs with >100 extra frames in 57 videos I cannot explain</li>\n        <li>seems if you want a reliable video duration, compute span_frames / fps. But keep in mind this assumes the train.csv fps is close to the real one (hope so)</li>\n        <li>seems there are no changes after the datasets update</li>\n    </ul>\n</div>","metadata":{}},{"cell_type":"markdown","source":"[The hosts addition](https://www.kaggle.com/competitions/MABe-mouse-behavior-detection/discussion/609092#3293523): *\"The larger discrepancies you report are more unusual and I will check them- we used cv2.VideoCapture to get video frame counts, and this is known to sometimes return incorrect values. My instinct would be to trust the fps over the video duration when there is disagreement.\"*","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Сoordinates and Arena size</p>","metadata":{}},{"cell_type":"markdown","source":"Ensure that min_x, max_x, min_y, and max_y from tracking and arena size do not exceed the video dimensions given in train.csv (video_width_pix, video_height_pix) (assuming the origin (0,0) is at the top-left corner of the video).","metadata":{}},{"cell_type":"code","source":"train_meta[, unique(arena_shape)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.904250Z","iopub.execute_input":"2025-10-03T20:34:58.905750Z","iopub.status.idle":"2025-10-03T20:34:58.921834Z","shell.execute_reply":"2025-10-03T20:34:58.920049Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_coords <- track_summary[, .(video_id, min_x, max_x, min_y, max_y)]\ntmp <- train_meta[, .(\n  video_id = as.character(video_id),\n  lab_id,\n  pix_per_cm_approx,\n  video_width_pix,\n  video_height_pix,\n  arena_width_cm,\n  arena_height_cm,\n  arena_shape,\n  tracking_method\n)]\ncheck_coords <- check_coords[tmp, on = .(video_id), nomatch = 0]\nhead(check_coords)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.924522Z","iopub.execute_input":"2025-10-03T20:34:58.926072Z","iopub.status.idle":"2025-10-03T20:34:58.983031Z","shell.execute_reply":"2025-10-03T20:34:58.981078Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Coordinates out of video","metadata":{}},{"cell_type":"code","source":"issues1 <- check_coords[(min_x < 0 | max_x > video_width_pix)]\nreport_issues(issues1, \"Videos with out of video dimentions x coordinate:\")\nhead(issues1[order(min_x), !c(\"min_y\", \"max_y\")], 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:58.985997Z","iopub.execute_input":"2025-10-03T20:34:58.987534Z","iopub.status.idle":"2025-10-03T20:34:59.027353Z","shell.execute_reply":"2025-10-03T20:34:59.024953Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues1[, unique(tracking_method)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.030064Z","iopub.execute_input":"2025-10-03T20:34:59.031513Z","iopub.status.idle":"2025-10-03T20:34:59.053766Z","shell.execute_reply":"2025-10-03T20:34:59.051938Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues2 <- check_coords[(min_y < 0 | max_y > video_height_pix)]\nreport_issues(issues2, \"Videos with out of video dimentions y coordinate:\")\nhead(issues2[order(min_y), !c(\"min_x\", \"max_x\")], 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.056409Z","iopub.execute_input":"2025-10-03T20:34:59.057854Z","iopub.status.idle":"2025-10-03T20:34:59.098882Z","shell.execute_reply":"2025-10-03T20:34:59.097080Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues2[, unique(tracking_method)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.101469Z","iopub.execute_input":"2025-10-03T20:34:59.102931Z","iopub.status.idle":"2025-10-03T20:34:59.119333Z","shell.execute_reply":"2025-10-03T20:34:59.117174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues <- rbind(issues1, issues2)[, unique(.SD)]\nreport_issues(issues, \"Total videos with out of video dimentions coordinates:\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.122077Z","iopub.execute_input":"2025-10-03T20:34:59.123561Z","iopub.status.idle":"2025-10-03T20:34:59.142399Z","shell.execute_reply":"2025-10-03T20:34:59.140557Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues[!tracking_method %in% c(\"DeepLabCut\")]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.145154Z","iopub.execute_input":"2025-10-03T20:34:59.146690Z","iopub.status.idle":"2025-10-03T20:34:59.200106Z","shell.execute_reply":"2025-10-03T20:34:59.198215Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: 153 videos that used 'DeepLabCut', 'MARS', and 'SLEAP' tracking methods have out of video x or y coordinates\n</div>","metadata":{}},{"cell_type":"markdown","source":"[The hosts addition](https://www.kaggle.com/competitions/MABe-mouse-behavior-detection/discussion/609092#3293523): *This is a known property of DeepLabCut (https://forum.image.sc/t/negative-y-coordinates/42499 ): the model is extrapolating to predict locations of body parts that are outside the video bounds, based on what parts are visible.*","metadata":{}},{"cell_type":"markdown","source":"## Arena shape","metadata":{}},{"cell_type":"code","source":"check_coords[arena_shape == \"square\" & arena_width_cm != arena_height_cm]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.202855Z","iopub.execute_input":"2025-10-03T20:34:59.204399Z","iopub.status.idle":"2025-10-03T20:34:59.231517Z","shell.execute_reply":"2025-10-03T20:34:59.229598Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues <- check_coords[\n    !arena_shape %in% c(\"square\", \"circular\") & arena_width_cm == arena_height_cm\n]\nreport_issues(issues, \"Square arena and different label for shape:\")\nhead(issues, 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.234298Z","iopub.execute_input":"2025-10-03T20:34:59.236356Z","iopub.status.idle":"2025-10-03T20:34:59.278124Z","shell.execute_reply":"2025-10-03T20:34:59.276305Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: a nominal square arena divided into two rectangular sections by an internal partition?\n</div>","metadata":{}},{"cell_type":"markdown","source":"## Arena dimensions","metadata":{}},{"cell_type":"markdown","source":"Check whether the arena dimensions converted to pixels (arena_width_cm × pix_per_cm_approx, arena_height_cm × pix_per_cm_approx) exceed the actual video resolution (video_width_pix, video_height_pix):","metadata":{}},{"cell_type":"code","source":"check_coords[, `:=`(\n  arena_width_pix  = arena_width_cm  * pix_per_cm_approx,\n  arena_height_pix = arena_height_cm * pix_per_cm_approx\n)]\ncheck_coords[, `:=`(\n  arena_EXTRA_width_pix  = arena_width_pix  - video_width_pix,\n  arena_EXTRA_height_pix = arena_height_pix - video_height_pix\n)]\ncheck_coords[arena_EXTRA_width_pix < 0,  arena_EXTRA_width_pix := 0]\ncheck_coords[arena_EXTRA_height_pix < 0, arena_EXTRA_height_pix := 0]\nissues <- check_coords[\n    (arena_width_pix > video_width_pix) | (arena_height_pix > video_height_pix),\n][order(-arena_EXTRA_width_pix, -arena_EXTRA_height_pix)]\nreport_issues(issues, \"Videos with arena size out of video size:\")\ncols <- names(issues)[names(issues) %like% \"lab_|video_|_cm|_pix\"]\ncols <- setdiff(cols, \"video_id\")\nunique(issues[, ..cols])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:34:59.280805Z","iopub.execute_input":"2025-10-03T20:34:59.282272Z","iopub.status.idle":"2025-10-03T20:34:59.332262Z","shell.execute_reply":"2025-10-03T20:34:59.329713Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: 4 labs in total have arena size out of video dimensions. Such cases can indicate inconsistent metadata (e.g., incorrect scaling factor or arena size, rounding approximation), but they could also occur if the video only captures a cropped region of the full arena rather than the entire field.\n</div>","metadata":{}},{"cell_type":"markdown","source":"[The hosts addition](https://www.kaggle.com/competitions/MABe-mouse-behavior-detection/discussion/609092#3293523): *\"The datasets update is coming soon\"*","metadata":{}},{"cell_type":"markdown","source":"## Arena configuration","metadata":{}},{"cell_type":"code","source":"check_sizes <- train_meta[\n    , .(lab_id, arena_shape, arena_type, arena_width_cm, arena_height_cm, \n       pix_per_cm_approx, video_width_pix, video_height_pix)\n][, unique(.SD)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:42:07.386292Z","iopub.execute_input":"2025-10-03T20:42:07.387893Z","iopub.status.idle":"2025-10-03T20:42:07.404098Z","shell.execute_reply":"2025-10-03T20:42:07.402158Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_sizes[, .N, by = .(lab_id, arena_shape, arena_type)][N!=1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:42:07.408180Z","iopub.execute_input":"2025-10-03T20:42:07.409837Z","iopub.status.idle":"2025-10-03T20:42:07.439011Z","shell.execute_reply":"2025-10-03T20:42:07.436510Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"AdaptableSnail\")][order(video_width_pix)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:46:09.570339Z","iopub.execute_input":"2025-10-03T20:46:09.571956Z","iopub.status.idle":"2025-10-03T20:46:09.603703Z","shell.execute_reply":"2025-10-03T20:46:09.601209Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"CRIM13\")][order(arena_height_cm)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:47:56.012127Z","iopub.execute_input":"2025-10-03T20:47:56.013859Z","iopub.status.idle":"2025-10-03T20:47:56.045010Z","shell.execute_reply":"2025-10-03T20:47:56.042414Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"GroovyShrew\")][order(video_width_pix)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:55:31.771447Z","iopub.execute_input":"2025-10-03T20:55:31.773160Z","iopub.status.idle":"2025-10-03T20:55:31.813329Z","shell.execute_reply":"2025-10-03T20:55:31.811491Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"UppityFerret\")][order(video_width_pix)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T21:06:34.374470Z","iopub.execute_input":"2025-10-03T21:06:34.376112Z","iopub.status.idle":"2025-10-03T21:06:34.404013Z","shell.execute_reply":"2025-10-03T21:06:34.402183Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"These above seems MOSTLY (BUT NOT ALL) ok, with the same physical arena size, more pixels in width/height mean more pixels per centimeter","metadata":{}},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"InvincibleJellyfish\")][order(video_width_pix)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:57:47.187339Z","iopub.execute_input":"2025-10-03T20:57:47.189399Z","iopub.status.idle":"2025-10-03T20:57:47.220120Z","shell.execute_reply":"2025-10-03T20:57:47.217549Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"If they are two genuine recordings at different resolutions, then pix_per_cm_approx should differ, shouldn't it?","metadata":{}},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"PleasantMeerkat\")][order(video_width_pix)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T21:00:24.302279Z","iopub.execute_input":"2025-10-03T21:00:24.303992Z","iopub.status.idle":"2025-10-03T21:00:24.337249Z","shell.execute_reply":"2025-10-03T21:00:24.335368Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Looks like pix_per_cm_approx here is a mean across all videos, but ok, the rounding?","metadata":{}},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"TranquilPanther\")][order(video_width_pix)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T21:04:39.157358Z","iopub.execute_input":"2025-10-03T21:04:39.159048Z","iopub.status.idle":"2025-10-03T21:04:39.193737Z","shell.execute_reply":"2025-10-03T21:04:39.191866Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"For the same 640×480 resolution, pix_per_cm_approx ranges widely (9.0–13.7)","metadata":{}},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"ElegantMink\")][order(arena_height_cm)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T20:48:32.128605Z","iopub.execute_input":"2025-10-03T20:48:32.130312Z","iopub.status.idle":"2025-10-03T20:48:32.162784Z","shell.execute_reply":"2025-10-03T20:48:32.160346Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Not consistent: At the same resolution (720×480) pix_per_cm_approx ranges ~17–20, two arena sizes are reported (33×16 cm and 33×22 cm), and one video is 640×480 but still arena is 33×22 cm with pix_per_cm_approx ≈18.2, the same as for entries with same arena size and video 720×480.","metadata":{}},{"cell_type":"code","source":"check_sizes[lab_id %in% c(\"SparklingTapir\")][order(arena_type, arena_height_cm, video_width_pix)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T21:12:24.149329Z","iopub.execute_input":"2025-10-03T21:12:24.157383Z","iopub.status.idle":"2025-10-03T21:12:24.232239Z","shell.execute_reply":"2025-10-03T21:12:24.229418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues <- check_sizes[lab_id %in% c(\"SparklingTapir\")]\nissues[, .(\n    min_pix_per_cm = min(pix_per_cm_approx),\n    max_pix_per_cm = max(pix_per_cm_approx)\n), by = .(arena_shape, arena_type, arena_width_cm, arena_height_cm, video_width_pix,video_height_pix)\n][min_pix_per_cm != max_pix_per_cm]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T21:15:36.642863Z","iopub.execute_input":"2025-10-03T21:15:36.644583Z","iopub.status.idle":"2025-10-03T21:15:36.678124Z","shell.execute_reply":"2025-10-03T21:15:36.675552Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"SparklingTapir is the most inconsistent case: \n- 19.0 × 28.5 cm (rectangular, sometimes blank type, sometimes resident-intruder)\n- 45.7 × 12.0 cm (rectangular, CSDS)\n- 12.7 × 46.0 cm (rectangular, CSDS)\n\npix_per_cm_approx swings widely within each arenasize/video size, e.g. in videos for which the arena_type is missing, the sizes of arena and video are the same but pix_per_cm_approx vary from 45 to 51.","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Body parts count</p>","metadata":{}},{"cell_type":"markdown","source":"Ensures consistency between the body parts listed in the train.csv and the number of body parts in the tracking files.","metadata":{}},{"cell_type":"code","source":"split_body_parts <- function(x){\n  x <- gsub('\"\"','', x, fixed = TRUE)\n  x <- gsub(\"^\\\\[|\\\\]$\", \"\", x)\n  trimws(strsplit(x, \", \", fixed = TRUE)[[1L]])\n}\nbp_check <- train_meta[, .(\n    body_part = split_body_parts(body_parts_tracked)\n), by = .(lab_id, video_id)]\nbp_check[, video_id := as.character(video_id)]\n    \nbp_check <- bp_check[\n    , .(csv_num_bodyparts = uniqueN(body_part)),\n    by = .(lab_id, video_id)\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:14:38.610446Z","iopub.execute_input":"2025-09-28T19:14:38.612385Z","iopub.status.idle":"2025-09-28T19:14:39.494693Z","shell.execute_reply":"2025-09-28T19:14:39.492812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"bp_check <- bp_check[\n    track_summary[, .(video_id, num_bodyparts)],\n    on = .(video_id)\n] \nbp_check[csv_num_bodyparts != num_bodyparts]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:14:40.465318Z","iopub.execute_input":"2025-09-28T19:14:40.466871Z","iopub.status.idle":"2025-09-28T19:14:40.510506Z","shell.execute_reply":"2025-09-28T19:14:40.508821Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: numbers of train.csv body parts are consistent with the tracking files.\n</div>","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Behaviors labeled</p>","metadata":{}},{"cell_type":"markdown","source":"Ensures consistency between the agent-target-action triplets listed in the train.csv (whitelist) and the annotated files.","metadata":{}},{"cell_type":"code","source":"clean_tokens <- function(x) {\n  x <- gsub(\"\\\\\\\\\", \"\", as.character(x))\n  x <- gsub(\"^['\\\"]+|['\\\"]+$\", \"\", x)\n  x <- trimws(tolower(x))\n  x[nchar(x) == 0] <- NA_character_\n  x\n}\nnormalize_actor <- function(x) {\n  x <- clean_tokens(x)\n  is_mouse <- grepl(\"^mouse\\\\d+$\", x)\n  out <- rep(NA_integer_, length(x))\n  out[is_mouse] <- as.integer(sub(\"^mouse\", \"\", x[is_mouse]))\n  out\n}\nparse_behaviors <- function(s) {\n  s <- gsub('\"\"', '', s, fixed = TRUE)\n  s <- gsub(\"^\\\\[|\\\\]$\", \"\", s)\n  tokens <- trimws(unlist(strsplit(s, \",\", fixed = TRUE)))\n  tokens <- tokens[nzchar(tokens)]\n  stopifnot(length(tokens) %% 3 == 0)\n  m <- matrix(tokens, ncol = 3, byrow = TRUE)\n  dt <- as.data.table(m)\n  setnames(dt, c(\"agent_raw\", \"target_raw\", \"action_raw\"))\n  dt\n}","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-09-28T19:14:43.291929Z","iopub.execute_input":"2025-09-28T19:14:43.293418Z","iopub.status.idle":"2025-09-28T19:14:43.309130Z","shell.execute_reply":"2025-09-28T19:14:43.307285Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"actions_check <- train_ann[, .(video_id, agent_id, target_id, action)][, unique(.SD)]\nhead(actions_check)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:14:45.801196Z","iopub.execute_input":"2025-09-28T19:14:45.802813Z","iopub.status.idle":"2025-09-28T19:14:45.837075Z","shell.execute_reply":"2025-09-28T19:14:45.834657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"actions_check[, .(agent_id, target_id)][order(agent_id, target_id), unique(.SD)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:14:48.322111Z","iopub.execute_input":"2025-09-28T19:14:48.323731Z","iopub.status.idle":"2025-09-28T19:14:48.353169Z","shell.execute_reply":"2025-09-28T19:14:48.350768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"whitelist <- train_meta[\n    behaviors_labeled != \"\",\n    .(video_id = as.character(video_id), behaviors_labeled)\n]\nwhitelist <- whitelist[, {\n    tmp <- parse_behaviors(behaviors_labeled)\n    tmp[, `:=`(\n      agent_id  = normalize_actor(agent_raw),\n      target_id = normalize_actor(target_raw),\n      action    = clean_tokens(action_raw)\n    )]\n    tmp[, target_id := ifelse(\n      is.na(target_id) & clean_tokens(target_raw) == \"self\",\n      agent_id, target_id\n    )]\n}, by = video_id]\nhead(whitelist)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:14:51.002512Z","iopub.execute_input":"2025-09-28T19:14:51.004117Z","iopub.status.idle":"2025-09-28T19:14:52.666562Z","shell.execute_reply":"2025-09-28T19:14:52.664804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fsetdiff(\n    unique(actions_check[, .(video_id, agent_id, target_id, action)]),\n    unique(whitelist[,  .(video_id, agent_id, target_id, action)])\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:20.417856Z","iopub.execute_input":"2025-09-28T19:16:20.419643Z","iopub.status.idle":"2025-09-28T19:16:20.449580Z","shell.execute_reply":"2025-09-28T19:16:20.447155Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: annotations are consistent with the whitelist in train.csv\n</div>","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Actions duration</p>","metadata":{}},{"cell_type":"markdown","source":"Сhecks the consistency of annotated actions intervals:","metadata":{}},{"cell_type":"code","source":"actions_check <- copy(train_ann)\nby_cols <- c(\"agent_id\",\"target_id\", \"action\",\"video_id\")\nsetorderv(actions_check, c(by_cols, \"start_frame\", \"stop_frame\"))\nactions_check[, prev_stop := shift(stop_frame), by = by_cols]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:23.268797Z","iopub.execute_input":"2025-09-28T19:16:23.270492Z","iopub.status.idle":"2025-09-28T19:16:23.309720Z","shell.execute_reply":"2025-09-28T19:16:23.307846Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Back-to-back intervals where one interval ends exactly when the next one starts:","metadata":{}},{"cell_type":"code","source":"actions_check[start_frame == prev_stop]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:25.704727Z","iopub.execute_input":"2025-09-28T19:16:25.706759Z","iopub.status.idle":"2025-09-28T19:16:25.732860Z","shell.execute_reply":"2025-09-28T19:16:25.731030Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Overlapping intervals where the next interval starts before the previous one ended:","metadata":{}},{"cell_type":"code","source":"actions_check[start_frame < prev_stop]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:29.384788Z","iopub.execute_input":"2025-09-28T19:16:29.386397Z","iopub.status.idle":"2025-09-28T19:16:29.410496Z","shell.execute_reply":"2025-09-28T19:16:29.408680Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Check actions duration:","metadata":{}},{"cell_type":"code","source":"actions_check[ , prev_stop := NULL]\nactions_check[, num_frames := stop_frame - start_frame + 1L]\nfps_dt <- train_meta[, .(lab_id, video_id = as.character(video_id), fps = frames_per_second)]\nactions_check[fps_dt, on = .(video_id), `:=` (fps = i.fps, lab_id = i.lab_id)]\n\nactions_check[, duration_sec := num_frames / fps]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:34.396284Z","iopub.execute_input":"2025-09-28T19:16:34.397902Z","iopub.status.idle":"2025-09-28T19:16:34.439604Z","shell.execute_reply":"2025-09-28T19:16:34.437723Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues <- actions_check[duration_sec <= 0.1][order(duration_sec)]\nreport_issues(issues, \"Actions with duration <= 0.1 seconds:\")\nhead(issues, 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:37.322474Z","iopub.execute_input":"2025-09-28T19:16:37.324107Z","iopub.status.idle":"2025-09-28T19:16:37.363888Z","shell.execute_reply":"2025-09-28T19:16:37.362104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"actions_check[num_frames == 1][, unique(lab_id)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:46.388047Z","iopub.execute_input":"2025-09-28T19:16:46.389668Z","iopub.status.idle":"2025-09-28T19:16:46.409485Z","shell.execute_reply":"2025-09-28T19:16:46.407162Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: There are 244 videos with annotated actions with duration <= 0.1 seconds\n</div>","metadata":{}},{"cell_type":"markdown","source":"[The hosts addition](https://www.kaggle.com/competitions/MABe-mouse-behavior-detection/discussion/609092#3293523): *\"Some annotations we received really were quite short, but they are real. We removed single-frame annotations from the Adjective-Animal labs (both training and test sets), but left the single-frame labels in CalMS21, CRIM13, and MABe22 since these are previously published datasets and we didn't want to create version discrepancies.\"*","metadata":{}},{"cell_type":"code","source":"actions_check[duration_sec > 300][order(-duration_sec), unique(action)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:16:59.618597Z","iopub.execute_input":"2025-09-28T19:16:59.620351Z","iopub.status.idle":"2025-09-28T19:16:59.639581Z","shell.execute_reply":"2025-09-28T19:16:59.637156Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"actions_check[duration_sec > 120][order(-duration_sec), unique(action)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T19:17:01.634743Z","iopub.execute_input":"2025-09-28T19:17:01.636399Z","iopub.status.idle":"2025-09-28T19:17:01.655149Z","shell.execute_reply":"2025-09-28T19:17:01.652883Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: Durations longer than 120 s are only found in state-like behaviors (rest, sniff, sniffgenital, mount, intromit) - seems ok\n</div>","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"background-color:#d9f2d9; color:black; font-family:Verdana; font-size:100%; text-align:left; border: 3px solid #7bb661; border-radius:15px; padding:20px 20px;\">Reciprocal interaction consistency</p>","metadata":{}},{"cell_type":"markdown","source":"In multi-agent behavior annotations, some actions are reciprocal by definition (e.g. “chase”: if mouse 1 chases mouse 2, then from mouse 2’s perspective the same event can be marked as “being chased”). Let's check if mirror records exists in this dataset and are the reciprocal actions and their span consistent.","metadata":{}},{"cell_type":"code","source":"actions_check <- train_ann[agent_id != target_id]\nactions_check[meta_files_dt, on = .(video_id), lab_id := i.lab_id]\nactions_check[, row_id := .I]\nhead(actions_check)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:01:52.170037Z","iopub.execute_input":"2025-09-28T20:01:52.171867Z","iopub.status.idle":"2025-09-28T20:01:52.223769Z","shell.execute_reply":"2025-09-28T20:01:52.221883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tolerance <- 20\n\ntic()\nres_list <- list()\nfor (idx in seq_len(nrow(actions_check))) {\n  row_dt <- actions_check[row_id == idx]\n  if (!nrow(row_dt)) next\n  rec_dt <- actions_check[\n    video_id    == row_dt$video_id &\n      agent_id    == row_dt$target_id &\n      target_id   == row_dt$agent_id &\n      start_frame >= row_dt$start_frame - tolerance &\n      start_frame <  row_dt$stop_frame &\n      stop_frame  <= row_dt$stop_frame  + tolerance &\n      stop_frame  >  row_dt$start_frame,\n    .(video_id,\n      row_id_rec      = row_id,\n      agent_id        = target_id,\n      target_id       = agent_id,\n      start_frame_rec = start_frame,\n      stop_frame_rec  = stop_frame,\n      action_rec      = action\n    )\n  ]\n  if (nrow(rec_dt)) {\n    actions_check <- actions_check[!row_id %in% rec_dt$row_id_rec]\n    res_list[[idx]] <- merge(\n      row_dt, rec_dt,\n      by = c(\"video_id\", \"agent_id\",\"target_id\"),\n      all = TRUE\n    )\n  }\n}\ntoc()\nreciprocal_actions <- rbindlist(res_list)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:01:54.392600Z","iopub.execute_input":"2025-09-28T20:01:54.394299Z","iopub.status.idle":"2025-09-28T20:08:37.384489Z","shell.execute_reply":"2025-09-28T20:08:37.382598Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"(Yes, a data.table join/antijoin would be 100 times faster. I just haven’t cracked it yet, until then: enjoy the brute-force loop.)","metadata":{}},{"cell_type":"code","source":"reciprocal_actions[, num_intersecting_frames := pmax(\n    0L,\n    pmin(stop_frame, stop_frame_rec) -\n      pmax(start_frame, start_frame_rec) + 1L\n  )\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:10:39.710217Z","iopub.execute_input":"2025-09-28T20:10:39.711909Z","iopub.status.idle":"2025-09-28T20:10:39.727802Z","shell.execute_reply":"2025-09-28T20:10:39.725931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cat(\"Rows:\", reciprocal_actions[, .N])\nhead(reciprocal_actions)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:10:43.714342Z","iopub.execute_input":"2025-09-28T20:10:43.715928Z","iopub.status.idle":"2025-09-28T20:10:43.750880Z","shell.execute_reply":"2025-09-28T20:10:43.748747Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: \n    <ul style=\"list-style:circle\">\n<li>Indeed, there are reciprocal actions. For instance, in the above table, we see that, from frames 5220 to 5262 (last row), mouse 1 attacks mouse 3. Meanwhile, starting from frame 5230, mouse 3 attack back. Let's check if the reciprocal actions are consistent.\n    </ul>\n</div>","metadata":{}},{"cell_type":"code","source":"reciprocal_actions[\n    action != action_rec, .(\n        action1 = pmin(action, action_rec),\n        action2 = pmax(action, action_rec)\n    )][order(action1), unique(.SD)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:11:05.379806Z","iopub.execute_input":"2025-09-28T20:11:05.381541Z","iopub.status.idle":"2025-09-28T20:11:05.426801Z","shell.execute_reply":"2025-09-28T20:11:05.425010Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"line-height:24px; font-size:16px;border-left: 5px solid silver; padding-left: 26px;\"> \n    💡 Note: Not all combinations seem correct. For example, it's difficult to imagine a mouse approaching while the target mouse sniffs its genital area...\n</div>","metadata":{}},{"cell_type":"code","source":"a1 <- c(\"approach\", \"attack\"); a2 <- c(\"sniffgenital\", \"sniffface\", \"sniff\")\n\nissues <- reciprocal_actions[\n    (action %in% a1 & action_rec %in% a2) | \n    (action %in% a2 & action_rec %in% a1)\n]\nreport_issues(issues, \"approach/attack ↔ sniff:\")\nhead(issues, 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:15:30.041231Z","iopub.execute_input":"2025-09-28T20:15:30.042889Z","iopub.status.idle":"2025-09-28T20:15:30.087072Z","shell.execute_reply":"2025-09-28T20:15:30.085237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues[, range(num_intersecting_frames)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:12:47.055204Z","iopub.execute_input":"2025-09-28T20:12:47.056915Z","iopub.status.idle":"2025-09-28T20:12:47.073937Z","shell.execute_reply":"2025-09-28T20:12:47.071907Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"These are just funny:","metadata":{}},{"cell_type":"code","source":"a1 <- c(\"defend\", \"escape\"); a2 <- c(\"sniffgenital\", \"sniffface\", \"sniff\")\n\nissues <- reciprocal_actions[\n    (action %in% a1 & action_rec %in% a2) | \n    (action %in% a2 & action_rec %in% a1)\n]\nreport_issues(issues, \"defend/escape ↔ sniff:\")\nhead(issues, 10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:14:18.676867Z","iopub.execute_input":"2025-09-28T20:14:18.678518Z","iopub.status.idle":"2025-09-28T20:14:18.721057Z","shell.execute_reply":"2025-09-28T20:14:18.719159Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues[, range(num_intersecting_frames)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:14:21.115382Z","iopub.execute_input":"2025-09-28T20:14:21.117073Z","iopub.status.idle":"2025-09-28T20:14:21.133076Z","shell.execute_reply":"2025-09-28T20:14:21.131209Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Counterintuitive:","metadata":{}},{"cell_type":"code","source":"a1 <- c(\"defend\"); a2 <- c(\"escape\", \"follow\")\n\nissues <- reciprocal_actions[\n    (action %in% a1 & action_rec %in% a2) | \n    (action %in% a2 & action_rec %in% a1)\n]\nreport_issues(issues, \"defend ↔ escape/follow:\")\nhead(issues)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:14:41.510533Z","iopub.execute_input":"2025-09-28T20:14:41.512108Z","iopub.status.idle":"2025-09-28T20:14:41.551844Z","shell.execute_reply":"2025-09-28T20:14:41.549861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"issues[, range(num_intersecting_frames)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:14:49.594050Z","iopub.execute_input":"2025-09-28T20:14:49.595669Z","iopub.status.idle":"2025-09-28T20:14:49.629017Z","shell.execute_reply":"2025-09-28T20:14:49.626438Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Defend is a protective counter-posture to attack. Being sniffed is not the symmetric counterpart of defending, as well as escape/follow.","metadata":{}},{"cell_type":"markdown","source":"Just to enshure my filtrations and joins above are correct, let's find these strange reciprocal actions in the raw annotations dt:","metadata":{}},{"cell_type":"code","source":"train_ann[video_id == \"984382096\" & start_frame >= 34442 & stop_frame <= 34458]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:14:56.312176Z","iopub.execute_input":"2025-09-28T20:14:56.313952Z","iopub.status.idle":"2025-09-28T20:14:56.339826Z","shell.execute_reply":"2025-09-28T20:14:56.337866Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ann[video_id == \"1057262087\" & start_frame >= 3429 & stop_frame <= 3455]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:15:01.141089Z","iopub.execute_input":"2025-09-28T20:15:01.142725Z","iopub.status.idle":"2025-09-28T20:15:01.167825Z","shell.execute_reply":"2025-09-28T20:15:01.165972Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ann[video_id == \"1864237378\" & start_frame >= 11491 & stop_frame <= 11539]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-28T20:17:45.102243Z","iopub.execute_input":"2025-09-28T20:17:45.103955Z","iopub.status.idle":"2025-09-28T20:17:45.131608Z","shell.execute_reply":"2025-09-28T20:17:45.129134Z"}},"outputs":[],"execution_count":null}]}