{"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":"gpu","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"},{"sourceId":13908609,"sourceType":"datasetVersion","datasetId":8625437},{"sourceId":13990963,"sourceType":"datasetVersion","datasetId":8917133},{"sourceId":14154447,"sourceType":"datasetVersion","datasetId":8790069}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":1494.813501,"end_time":"2025-10-25T19:51:35.870094","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-25T19:26:41.056593","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"00962fe9010e42b7a5b123af50d2a026":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"0b098fa23a3b4031bf88b32b20044978":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"108c3e5db1154f47a5aca68386ed4f24":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"38c36db3be3843b19da108d4cd1f9421":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_3e9806911d1d489d963f76955d8468c4","IPY_MODEL_e6ad60bc760e4b51885b15335c048a65","IPY_MODEL_6f4723180bf14101b60deeac9fd59e50"],"layout":"IPY_MODEL_00962fe9010e42b7a5b123af50d2a026","tabbable":null,"tooltip":null}},"3e9806911d1d489d963f76955d8468c4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_aeb2544f9ad9413b8b71b5f5643b1019","placeholder":"​","style":"IPY_MODEL_ec012e9ac53d477daee0988a35117f0b","tabbable":null,"tooltip":null,"value":"100%"}},"3edd91e1eafb4f6bbf55275ea60b6c97":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"6f4723180bf14101b60deeac9fd59e50":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_e8f95c8096294ce6b6fb2b65c08af536","placeholder":"​","style":"IPY_MODEL_0b098fa23a3b4031bf88b32b20044978","tabbable":null,"tooltip":null,"value":" 863/863 [00:08&lt;00:00, 96.46it/s]"}},"aeb2544f9ad9413b8b71b5f5643b1019":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"e6ad60bc760e4b51885b15335c048a65":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_3edd91e1eafb4f6bbf55275ea60b6c97","max":863,"min":0,"orientation":"horizontal","style":"IPY_MODEL_108c3e5db1154f47a5aca68386ed4f24","tabbable":null,"tooltip":null,"value":863}},"e8f95c8096294ce6b6fb2b65c08af536":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"ec012e9ac53d477daee0988a35117f0b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# First, do NN prediction. Save it in var \"yp\", delete all but that and \"df3\"","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport polars as pl\nimport matplotlib.pyplot as plt\nimport os, warnings, gc, random, sys, math, joblib\nfrom scipy.signal import savgol_filter\nfrom pathlib import Path\nfrom time import time\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.preprocessing import QuantileTransformer\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras.callbacks import Callback, LearningRateScheduler, EarlyStopping\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.optimizers.schedules import ExponentialDecay, CosineDecayRestarts\nfrom tensorflow.keras.regularizers import L2\nfrom tensorflow.keras.losses import SparseCategoricalCrossentropy\nfrom tensorflow.keras import Layer\nfrom tensorflow.keras.layers import Input, Dense, Softmax, Dropout, LSTM, Bidirectional, Concatenate, Embedding\nfrom tensorflow.keras.utils import Sequence\n\n\nwarnings.filterwarnings('ignore')\nt0 = time()\n\ndata_dir = '/kaggle/input/MABe-mouse-behavior-detection'\nseed = 13\n\nmodel_dir = '/kaggle/input/nn-data-mabe/nn/'\nnn_mode = 'submit' # 'train'/'validate'/'submit'\n\nfolds = 5","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T15:55:27.422462Z","iopub.execute_input":"2025-12-15T15:55:27.423319Z","iopub.status.idle":"2025-12-15T15:55:41.519992Z","shell.execute_reply.started":"2025-12-15T15:55:27.423281Z","shell.execute_reply":"2025-12-15T15:55:41.519410Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"NN functions","metadata":{}},{"cell_type":"code","source":"# hardcoded di of all actions\n# single actions are: 0, 10, 21-24, 26, 29, 30, 32 (11 out of 34)\nN_ACT = 33\nN_LAB = 17 # number of labs; last 3 are combined\n\n# the 'ejaculate' label was supposed to be removed; It does not appear in the test set so can be safely ignored. - done. Was 23.\n# drop 'dominancemount'=15, 'genitalgroom'=18 - confirmed by LB30 - incr of 2 points.\n# drop 'disengage'=10 - it is only in CRIM13 - confirm by LB38 - decr of 3 points!\naction_di = {'rear': 0, 'avoid': 1, 'attack': 2, 'approach': 3, 'chase': 4, 'submit': 5, 'chaseattack': 6, 'shepherd': 7, 'sniff': 8, 'mount': 9\n    , 'disengage': -1, 'selfgroom': 10, 'sniffgenital': 11, 'sniffface': 12, 'sniffbody': 13, 'dominancemount': -1, 'attemptmount': 14, 'intromit': 15\n    , 'genitalgroom': -1, 'reciprocalsniff': 16, 'escape': 17, 'dominance': 18, 'allogroom': 19, 'ejaculate': -1, 'defend': 20, 'climb': 21, 'dig': 22\n    , 'rest': 23, 'run': 24, 'dominancegroom': 25, 'freeze': 26, 'flinch': 27, 'follow': 28, 'exploreobject': 29, 'biteobject': 30, 'tussle': 31\n    , 'huddle': 32}\naction_di2 = {0: 'rear', 1: 'avoid', 2: 'attack', 3: 'approach', 4: 'chase', 5: 'submit', 6: 'chaseattack', 7: 'shepherd', 8: 'sniff', 9: 'mount'\n    , 10: 'selfgroom', 11: 'sniffgenital', 12: 'sniffface', 13: 'sniffbody', 14: 'attemptmount', 15: 'intromit'\n    , 16: 'reciprocalsniff', 17: 'escape', 18: 'dominance', 19: 'allogroom', 20: 'defend', 21: 'climb', 22: 'dig'\n    , 23: 'rest', 24: 'run', 25: 'dominancegroom', 26: 'freeze', 27: 'flinch', 28: 'follow', 29: 'exploreobject', 30: 'biteobject', 31: 'tussle'\n    , 32: 'huddle'}\n\n# body_parts_tracked\n# remap 'head' to 'nose' - they never occur together, and are close on the body. Head is only for lab 'GroovyShrew'=5, id=[555-571]\n# drop all past 5. Always use 0123, then either 4 or (5+6)/2, or fill-in with average of all of them. Then add 7=f\nbp_di = {'tail_base':0,'ear_right':1,'ear_left':2,'nose':3,'head':3,'body_center':4,'hip_right':5,'hip_left':6,'tail_tip':7,\n    'neck':8,'lateral_right':9,'lateral_left':10,'tail_midpoint':11,'tail_middle_1':13,'spine_2':14,'spine_1':15,'tail_middle_2':16,'headpiece_topfrontright':17,\n    'headpiece_topbackright':18,'headpiece_topfrontleft':19,'headpiece_topbackleft':20,'headpiece_bottomfrontright':21,'headpiece_bottombackright':22,\n    'headpiece_bottombackleft':23,'headpiece_bottomfrontleft':24}\n\n# lab text to numeric mapping\nlab_di = {'AdaptableSnail': 0, 'BoisterousParrot': 1, 'CautiousGiraffe': 2, 'DeliriousFly': 3, 'ElegantMink': 4, 'GroovyShrew': 5\n          , 'InvincibleJellyfish': 6, 'JovialSwallow': 7, 'LyricalHare': 8, 'NiftyGoldfinch': 9, 'PleasantMeerkat': 10\n          , 'ReflectiveManatee': 11, 'SparklingTapir': 12, 'TranquilPanther': 13, 'UppityFerret': 14 # real labs: 0-14\n          , 'CRIM13': 15, 'CalMS21_supplemental': 16, 'CalMS21_task1': 17, 'CalMS21_task2': 18} # additional training labs only\n\nnorm_di = {} # put values here if training is not run\n\n\n\n\n\n# combine all 5 body parts ***********************************************************************************\ndef body_parts(det_df):\n    # combine first 4 normally\n    det_df2 = (det_df\n          .filter(pl.col('bodypart') == 0)\n          .select(['video_frame', 'mouse_id', 'x', 'y'])\n          .rename({'x': 'x_a', 'y': 'y_a'})\n          .join(\n              det_df\n              .filter(pl.col('bodypart') == 1)\n              .select(['video_frame', 'mouse_id', 'x', 'y'])\n              .rename({'x': 'x_b', 'y': 'y_b'})\n              , how='full', on=['mouse_id', 'video_frame'], coalesce=True\n              )\n          .join(\n              det_df\n              .filter(pl.col('bodypart') == 2)\n              .select(['video_frame', 'mouse_id', 'x', 'y'])\n              .rename({'x': 'x_c', 'y': 'y_c'})\n              , how='full', on=['mouse_id', 'video_frame'], coalesce=True\n              )\n          .join(\n              det_df\n              .filter(pl.col('bodypart') == 3)\n              .select(['video_frame', 'mouse_id', 'x', 'y'])\n              .rename({'x': 'x_d', 'y': 'y_d'})\n              , how='full', on=['mouse_id', 'video_frame'], coalesce=True\n              )\n          )\n    \n    # add 5th one, one of 3 ways\n    if det_df.filter(pl.col('bodypart') == 4).shape[0] > 0: # use body center 'as is' - most labs\n        det_df2 = (det_df2\n            .join(\n                det_df\n                .filter(pl.col('bodypart') == 4)\n                .select(['video_frame', 'mouse_id', 'x', 'y'])\n                .rename({'x': 'x_e', 'y': 'y_e'})\n                , how='full', on=['mouse_id', 'video_frame'], coalesce=True\n                )\n            )\n    elif det_df.filter(pl.col('bodypart') == 5).shape[0] > 0: # use middle of hips (5+6) - labs 2,4,6,7,13\n        det_df3 = (det_df\n            .filter(pl.col('bodypart') == 5)\n            .select(['video_frame', 'mouse_id', 'x', 'y'])\n            .rename({'x': 'x_1', 'y': 'y_1'})\n            .join(\n                det_df\n                .filter(pl.col('bodypart') == 6)\n                .select(['video_frame', 'mouse_id', 'x', 'y'])\n                .rename({'x': 'x_2', 'y': 'y_2'})\n                , how='full', on=['mouse_id', 'video_frame'], coalesce=True\n                )\n            )\n        det_df3 = (det_df3\n            .with_columns(x_e = ((pl.col('x_1') + pl.col('x_2')) / 2).cast(pl.Float32))\n            .with_columns(y_e = ((pl.col('y_1') + pl.col('y_2')) / 2).cast(pl.Float32))\n            .select(['video_frame', 'mouse_id', 'x_e', 'y_e'])\n            )\n        det_df2 = (det_df2\n            .join(\n                det_df3\n                , how='full', on=['mouse_id', 'video_frame'], coalesce=True\n                )\n            )\n    else: # use average* of first 4 body parts - labs 5, 8\n        det_df2 = (det_df2\n            .with_columns(x_e = ((pl.col('x_a') + pl.col('x_b') + pl.col('x_c') + pl.col('x_d')) / 4).cast(pl.Float32))\n            .with_columns(y_e = ((pl.col('y_a') + pl.col('y_b') + pl.col('y_c') + pl.col('y_d')) / 4).cast(pl.Float32))\n            )\n        \n    # add tail tip\n    if det_df.filter(pl.col('bodypart') == 7).shape[0] > 0: # use tail tip 'as is' - 15% of frames, 5 labs\n        det_df2 = (det_df2\n            .join(\n                det_df\n                .filter(pl.col('bodypart') == 7)\n                .select(['video_frame', 'mouse_id', 'x', 'y'])\n                .rename({'x': 'x_f', 'y': 'y_f'})\n                , how='full', on=['mouse_id', 'video_frame'], coalesce=True\n                )\n            )\n    else: # use -1 to indicate missing tail. Handle it later.\n        det_df2 = (det_df2\n            .with_columns(x_f = (pl.col('x_a') * 0 - 1).cast(pl.Float32))\n            .with_columns(y_f = (pl.col('y_a') * 0 - 1).cast(pl.Float32))\n            )\n    return det_df2\n\n\n\n\n\n# read data ***********************************************************************************\ndef read_data(train = 0):\n    # meta\n    if train:\n        dfm = pd.read_csv(data_dir + '/train.csv')\n    else:\n        dfm = pd.read_csv(data_dir + '/test.csv')\n\n    # details and annotations\n    dfa = pd.DataFrame()\n    df = pl.DataFrame()\n    i, j = 0, 0\n    lab_id0 = ''\n    id_di = {}\n    id_di2 = {}\n    for _, row in dfm.iterrows():\n        lab_id = row['lab_id'] # 19 labs\n        video_id = row['video_id'] # many vids\n        if train:\n            ann_path = data_dir + '/train_annotation/' + lab_id + '/' + str(video_id) + '.parquet'\n            det_path = data_dir + '/train_tracking/' + lab_id + '/' + str(video_id) + '.parquet'\n        else:\n            det_path = data_dir + '/test_tracking/' + lab_id + '/' + str(video_id) + '.parquet'\n        # skip training data without annotations\n        if os.path.exists(det_path) and (train == 0 or os.path.exists(ann_path)):\n            # count videos from the same lab\n            if lab_id0 == lab_id:\n                j += 1\n            else:\n                j = 0\n            lab_id0 = lab_id\n\n            if train:\n                ann_df = pd.read_parquet(ann_path)\n                ann_df['agent_id'] -= 1 # now starts from 0\n                ann_df['target_id'] -= 1 # now starts from 0\n                ann_df['id'] = i # add id\n                ann_df['id'] = ann_df['id'].astype('int16') # reduce size\n                dfa = pd.concat([dfa, ann_df], axis=0, ignore_index=True)\n                \n            # polars code for details\n            multx = np.maximum(.42, np.minimum(4.5, 30 * row['arena_width_cm'] / row['video_width_pix']))\n            multy = np.maximum(.42, np.minimum(4.5, 30 * row['arena_height_cm'] / row['video_height_pix']))\n            det_df = (pl.read_parquet(det_path)\n                      .with_columns(bodypart = pl.col('bodypart').map_elements(lambda x: bp_di[x], return_dtype=pl.Int8)) # map bodypart\n                      .filter(pl.col('bodypart') < 8) # drop all but 8 - incl tail_tip=7\n                      .with_columns(x = (pl.col('x') * multx).cast(pl.Float32)) # scale coordinates by lab [lab_scale[lab_id]]\n                      .with_columns(y = (pl.col('y') * multy).cast(pl.Float32))\n                      )\n            det_df = body_parts(det_df) # combine all body parts - one video at a time. Before adding 'id'\n            det_df = (det_df\n                      .with_columns(id = i) # add id\n                      .with_columns(video_frame = pl.col('video_frame').cast(pl.Int32), id = pl.col('id').cast(pl.Int16)) # cast\n                      )\n            df = pl.concat([df, det_df], how='vertical') # concat\n\n            print('    ', i, j, lab_id + '/' + str(video_id), det_df.shape[0])\n            id_di[i] = [lab_id, video_id, lab_di[lab_id]] # decode: lab[text], video, lab[numeric]\n            id_di2[lab_id + '/' + str(video_id)] = i\n            i += 1 # incr id\n\n    print('    finished reading files', int(time() - t0), 'sec')\n    df = df.to_pandas()\n\n    # drop bad videos\n    vid = set(dfm['video_id'].loc[(dfm['frames_per_second']==25) & (dfm['lab_id']=='AdaptableSnail')]) # list of 'bad' video_id values\n    vid = [id_di2['AdaptableSnail/' + str(video_id)] for video_id in vid if 'AdaptableSnail/' + str(video_id) in id_di2] # list of 'bad' id values. In train it is [1, 8, 9, 13]\n    df = df.loc[df['id'].map(lambda x: x not in vid)].reset_index(drop=True) # drop them\n    \n    df['mouse_id'] -= 1 # now starts from 0\n    df = df.sort_values(by=['mouse_id', 'id', 'video_frame']).reset_index(drop=True) # sort, JIC\n\n    # determine the average size of each mouse - before interpolation. From a/b/c points only\n    df['s'] = np.sqrt((df['x_a'] - df['x_b'] / 2 - df['x_c'] / 2)**2+(df['y_a'] - df['y_b'] / 2 - df['y_c'] / 2)**2) # use a+b+c only\n    a = df.groupby(['mouse_id', 'id'])['s'].agg(['median', 'mean', 'std', 'max', 'min', 'size']).reset_index()\n    df.drop('s', axis=1, inplace=True)\n    a['median'] = np.maximum(30, a['median'].fillna(a['median'].mean())) # use mean when missing, floor at 30\n    m_size_di = dict(zip(list(a['id'] * 4 + a['mouse_id']), list(np.round(a['median'], 1)))) # key: id * 4 + mouse_id\n\n    # fill missing values\n    print('    interpolating missing values...', int(time() - t0), 'sec')\n    df.interpolate(method='polynomial', order=1, inplace=True, axis=0) # slow\n    df = df.fillna(method='ffill') # after interpolation some values are still missing\n    df = df.fillna(method='bfill') # after ffill some values are still missing\n    \n    if train:\n        dfa0 = dfa.copy() # save for CV\n        # format solution\n        dfa0['id2'] = dfa0['id'].map(id_di)\n        dfa0['lab_id'] = dfa0['id2'].str[0]\n        dfa0['video_id'] = dfa0['id2'].str[1]\n        dfa0.drop('id2', axis=1, inplace=True)\n        dfa0['agent_id'] = dfa0['agent_id'].apply(lambda x: 'mouse' + str(x+1))\n        dfa0['target_id'] = dfa0['target_id'].apply(lambda x: 'mouse' + str(x+1))\n        dfa0 = dfa0.merge(dfm[['lab_id', 'video_id', 'behaviors_labeled']], how='left', on=['lab_id', 'video_id'])\n        # relabel self-actions\n        dfa0['target_id'].loc[dfa0['target_id'] == dfa0['agent_id']] = 'self'\n        # drop exlcuded actions from official solution\n        for a in ['dominancemount', 'genitalgroom', 'ejaculate', 'disengage']:\n            dfa0 = dfa0.loc[dfa0['action'] != a]\n        return df, dfm, dfa, dfa0, id_di, id_di2, m_size_di\n    else:\n        return df, dfm, id_di, id_di2, m_size_di\n\n\n\n\n\n# format into mouse pairs ***********************************************************************************\ndef prepare_pairs():\n    global df\n    df = pl.from_pandas(df)\n    df3 = pl.DataFrame()\n    for m1, m2 in zip([0, 0, 0, 1, 1, 2, 1, 2, 3, 2, 3, 3, 0, 1, 2, 3], [1, 2, 3, 2, 3, 3, 0, 0, 0, 1, 1, 2, 0, 1, 2, 3]):\n        dft = (df.filter(pl.col('mouse_id') == m1)\n              .select(['id', 'video_frame', 'x_a', 'y_a', 'x_b', 'y_b', 'x_c', 'y_c', 'x_d', 'y_d', 'x_e', 'y_e', 'x_f', 'y_f'])\n              .rename({'x_a': 'x_1_a', 'y_a': 'y_1_a', 'x_b': 'x_1_b', 'y_b': 'y_1_b', 'x_c': 'x_1_c', 'y_c': 'y_1_c', 'x_d': 'x_1_d', 'y_d': 'y_1_d', 'x_e': 'x_1_e', 'y_e': 'y_1_e', 'x_f': 'x_1_f', 'y_f': 'y_1_f'})\n              .with_columns(mid = m1 * 10 + m2) # add mid\n              .with_columns(mid = pl.col('mid').cast(pl.Int8)) # cast\n              .join(\n                  df\n                  .filter(pl.col('mouse_id') == m2)\n                  .select(['id', 'video_frame', 'x_a', 'y_a', 'x_b', 'y_b', 'x_c', 'y_c', 'x_d', 'y_d', 'x_e', 'y_e', 'x_f', 'y_f'])\n                  .rename({'x_a': 'x_2_a', 'y_a': 'y_2_a', 'x_b': 'x_2_b', 'y_b': 'y_2_b', 'x_c': 'x_2_c', 'y_c': 'y_2_c', 'x_d': 'x_2_d', 'y_d': 'y_2_d', 'x_e': 'x_2_e', 'y_e': 'y_2_e', 'x_f': 'x_2_f', 'y_f': 'y_2_f'})\n                  , how='inner', on=['id', 'video_frame']\n                  )\n               )\n        df3 = pl.concat([df3, dft], how='vertical') # concat\n    del df, dft\n    gc.collect()\n    df3 = df3.to_pandas()\n    df3 = df3[['id', 'mid', 'video_frame', 'x_1_a', 'x_1_b', 'x_1_c', 'x_1_d', 'x_1_e', 'x_1_f', 'x_2_a', 'x_2_b', 'x_2_c', 'x_2_d', 'x_2_e', 'x_2_f',\n               'y_1_a', 'y_1_b', 'y_1_c', 'y_1_d', 'y_1_e', 'y_1_f', 'y_2_a', 'y_2_b', 'y_2_c', 'y_2_d', 'y_2_e', 'y_2_f']] # reorder\n    df3 = df3.sort_values(by=['id', 'mid', 'video_frame']).reset_index(drop=True) # need this order - frame last!\n    return df3\n\n\n\n\n\n# construct 'behaviors_labeled' mask ***********************************************************************************\ndef prepare_mask(df3):\n    global dfm\n    # format dfm\n    dfm['id'] = dfm['lab_id'] + '/' + dfm['video_id'].astype('str')\n    dfm['id'] = dfm['id'].map(id_di2)\n    dfm = dfm.loc[~dfm['behaviors_labeled'].isna()].reset_index(drop=True) # drop empty\n    # replace actions with their num codes\n    dfm['behaviors_labeled'] = dfm['behaviors_labeled'].apply(lambda x: x.replace(\"'\",\"\")) # remove \"'\"\n    a = list(action_di.keys())\n    a.sort(key=lambda s: -len(s)) # sort actions by decr length, to enforce exact match\n    for k in a:\n        dfm['behaviors_labeled'] = dfm['behaviors_labeled'].apply(lambda x: x.replace(k, str(action_di[k])))\n    # replace mice combos with their codes\n    for m1 in [1,2,3,4]:\n        t = str(m1 * 10 + m1 - 11)\n        dfm['behaviors_labeled'] = dfm['behaviors_labeled'].apply(lambda x: x.replace('mouse'+str(m1)+',self', t))\n        for m2 in [1,2,3,4]:\n            t = str(m1 * 10 + m2 - 11) # is this in correct order? Confirmed, it is.\n            dfm['behaviors_labeled'] = dfm['behaviors_labeled'].apply(lambda x: x.replace('mouse'+str(m1)+',mouse'+str(m2), t))\n\n    # first, construct mask for id/mid keys only\n    dfm = dfm.loc[~dfm['id'].isna()].reset_index(drop=True) # drop missing id\n    res = []\n    a = list(dfm['behaviors_labeled'])\n    for i in range(dfm.shape[0]):\n        iid = int(dfm['id'].iloc[i])\n        b = a[i].split()\n        b = [x.replace('[', '').replace(']', '').replace('\"', '') for x in b]\n        di = {}\n        for bi in b:\n            c = bi.split(',')\n            if int(c[0]) not in di:\n                di[int(c[0])] = c[1]\n            else:\n                di[int(c[0])] = di[int(c[0])] + ',' + c[1]\n        for k in di.keys():\n            res.append([iid, k, di[k]])\n    dfm2 = pd.DataFrame(res)\n    dfm2.columns = ['id', 'mid', 'actions']\n\n    # second, expand all actions\n    for i in range(N_ACT):\n        dfm2['a'+str(i)] = dfm2['actions'].apply(lambda x: str(i) in x.split(',')).astype('int8')\n    dfm2.drop('actions', axis=1, inplace=True)\n    dfm2 = dfm2.loc[dfm2.iloc[:,2:].values.sum(-1) > 0].reset_index(drop=True) # drop records with no action: 1440->1423; happens when the only action is excluded\n\n    # third, merge with full data. 50% are NAs, but all of them are for records with action=-1, so they can be dropped\n    df_mask = df3[['id', 'mid']].merge(dfm2[['id', 'mid', 'a0']], how='left', on=['id', 'mid'])\n    idx = df_mask['a0'].isna()\n    print('    proportion without behaviors_labeled, drop', np.round(df_mask['a0'].isna().mean(), 3))\n\n    # drop data for not labeled actions\n    df3 = df3.loc[~idx].reset_index(drop=True)\n    df_mask = df_mask.loc[~idx].reset_index(drop=True)\n\n    # now merge with full data; since nothing is missing, int8 stays int8 and greatly reduced RAM usage\n    del df_mask\n    gc.collect()\n    df_mask = df3[['id', 'mid']].merge(dfm2, how='left', on=['id', 'mid'])\n    mask = df_mask.iloc[:,2:].values.astype(np.int8) # skip id, mid\n    del idx, a, dfm2, res, di, df_mask\n    gc.collect()\n    # add leading 'no action'\n    mask = np.concatenate((mask[:,0].reshape(-1,1), mask), axis=1)\n    mask[:,0] = 1 # 'no action' is always labeled\n    return df3, mask\n\n\n\n\n\ndef fit_ellipse(df, cols=['x_a','x_b','x_c','x_d','x_e','y_a','y_b','y_c','y_d','y_e']): # returns major/minor axis based on 5 points\n    data = df[cols].values.reshape(-1,2,5)\n    data = data - data.mean(-1).reshape(-1,2,1) # center\n    covariance = np.zeros([data.shape[0], 2, 2], dtype=np.float32)\n    covariance[:,0,0] = data[:,0,:].var(-1)\n    covariance[:,1,1] = data[:,1,:].var(-1)\n    covariance[:,0,1] = (data[:,0,:] * data[:,1,:]).mean(-1)\n    covariance[:,1,0] = covariance[:,0,1]\n    D = np.linalg.svd(covariance, full_matrices=True, compute_uv=False, hermitian=True)\n    a, b = np.sqrt(D[:,0]), np.sqrt(D[:,1])\n    # clip\n    a = np.clip(a, 3, 500)\n    b = np.clip(b, 1, 100)\n    return a, b\n\n\n\n\n\n# FE\ndef FE(df3):\n    global mask, dfm\n\n    # single\n    df3['single'] = (((df3['mid']//10) == (df3['mid']%10)) * 1).astype('int8')\n\n    # mid: 0/11/22/33=self; inverse: 10/20/21/30/31/32  1/2/3/12/13/23/10/20/21/30/31/32\n    df3['inverse'] = (((df3['mid']//10) > (df3['mid']%10)) * 1).astype('int8')\n\n    # lab code. Split into 4. Use dict to make sure it works on sub!!!\n    l = np.minimum(N_LAB -1, df3['id'].map(id_di).str[2].astype(np.int8)) # 0-16 - collapse last 3 labs\n    df3['l1'] = (l%2).astype(np.int8) # 0-1\n    df3['l2'] = ((l//2)%2).astype(np.int8) # 0-1\n    df3['l3'] = ((l//4)%2).astype(np.int8) # 0-1\n    df3['l4'] = ((l//8)%2).astype(np.int8) # 0-1\n    df3['l5'] = (l//16).astype(np.int8) # 0-1. 1 only for excluded labs in training\n    del l\n    gc.collect()\n\n    # add sex of both mice; missing sex = 80% male. So values are 0/1/.2 - pretty much binary, leave it 'as is'\n    # first, create mapping of id + mouse_id to sex\n    dfm['id'] = dfm['id'].astype('int16')\n    m_sex_di = dict(zip(list(dfm['id'] * 4 + 0), list(dfm['mouse1_sex'].map({'male':0, 'female':1}).fillna(.2)))) # key: id * 4 + mouse_id\n    m_sex_di = m_sex_di | dict(zip(list(dfm['id'] * 4 + 1), list(dfm['mouse2_sex'].map({'male':0, 'female':1}).fillna(.2)))) # key: id * 4 + mouse_id\n    m_sex_di = m_sex_di | dict(zip(list(dfm['id'] * 4 + 2), list(dfm['mouse3_sex'].map({'male':0, 'female':1}).fillna(.2)))) # key: id * 4 + mouse_id\n    m_sex_di = m_sex_di | dict(zip(list(dfm['id'] * 4 + 3), list(dfm['mouse4_sex'].map({'male':0, 'female':1}).fillna(.2)))) # key: id * 4 + mouse_id\n    df3['sex1'] = (df3['id'] * 4 + df3['mid'] // 10).map(m_sex_di).astype('float32')\n    df3['sex2'] = (df3['id'] * 4 + df3['mid'] % 10).map(m_sex_di).astype('float32')\n\n    # add color of both mice; missing(=brown) color = .5. So values are 0/1/.5 - pretty much binary, leave it 'as is'\n    # first, create mapping of id + mouse_id to color\n    m_c_di = dict(zip(list(dfm['id'] * 4 + 0), list(dfm['mouse1_color'].map({'black':0, 'white':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    m_c_di = m_c_di | dict(zip(list(dfm['id'] * 4 + 1), list(dfm['mouse2_color'].map({'black':0, 'white':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    m_c_di = m_c_di | dict(zip(list(dfm['id'] * 4 + 2), list(dfm['mouse3_color'].map({'black':0, 'white':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    m_c_di = m_c_di | dict(zip(list(dfm['id'] * 4 + 3), list(dfm['mouse4_color'].map({'black':0, 'white':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    df3['color1'] = (df3['id'] * 4 + df3['mid'] // 10).map(m_c_di).astype('float32')\n    df3['color2'] = (df3['id'] * 4 + df3['mid'] % 10).map(m_c_di).astype('float32')\n\n    # add strain of both mice; missing = .5. So values are 0/1/.5 - pretty much binary, leave it 'as is'\n    # first, create mapping of id + mouse_id to strain\n    m_s_di = dict(zip(list(dfm['id'] * 4 + 0), list(dfm['mouse1_strain'].map({'C57Bl/6J':0, 'BALB/c':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    m_s_di = m_s_di | dict(zip(list(dfm['id'] * 4 + 1), list(dfm['mouse2_strain'].map({'C57Bl/6J':0, 'BALB/c':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    m_s_di = m_s_di | dict(zip(list(dfm['id'] * 4 + 2), list(dfm['mouse3_strain'].map({'C57Bl/6J':0, 'BALB/c':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    m_s_di = m_s_di | dict(zip(list(dfm['id'] * 4 + 3), list(dfm['mouse4_strain'].map({'C57Bl/6J':0, 'BALB/c':1}).fillna(.5)))) # key: id * 4 + mouse_id\n    df3['strain1'] = (df3['id'] * 4 + df3['mid'] // 10).map(m_s_di).astype('float32')\n    df3['strain2'] = (df3['id'] * 4 + df3['mid'] % 10).map(m_s_di).astype('float32')\n\n    # tracking method - no missing values in train. Split in 2 binaries\n    tmp_di = dict(zip(list(dfm['id']), list(dfm['tracking_method'].map({'MARS':0, 'DeepLabCut':1, 'SLEAP':2}).fillna(0))))\n    df3['tracking_method'] = df3['id'].map(tmp_di).fillna(0).astype('int8')\n    df3['tracking_DeepLabCut'] = (df3['tracking_method'] == 1).astype('int8')\n    df3['tracking_SLEAP'] = (df3['tracking_method'] == 2).astype('int8')\n    df3.drop('tracking_method', axis=1, inplace=True)\n\n    # arena shape - no missing values in train. Split in 3 binaries\n    tmp_di = dict(zip(list(dfm['id']), list(dfm['arena_shape'].map({'rectangular':0, 'circular':1, 'split rectangluar':2, 'square':3}).fillna(0))))\n    df3['arena_shape'] = df3['id'].map(tmp_di).fillna(0).astype('int8')\n    df3['arena_shape_circular'] = (df3['arena_shape'] == 1).astype('int8')\n    df3['arena_shape_split_rectangluar'] = (df3['arena_shape'] == 2).astype('int8')\n    df3['arena_shape_square'] = (df3['arena_shape'] == 3).astype('int8')\n    df3.drop('arena_shape', axis=1, inplace=True)\n    \n    # arena type - missing values. Split in 3 binaries\n    tmp_di = dict(zip(list(dfm['id']), list(dfm['arena_type'].map({'resident-intruder':0, 'neutral':1, 'divided territories':2}).fillna(3)))) # others = 3\n    df3['arena_type'] = df3['id'].map(tmp_di).fillna(0).astype('int8')\n    df3['arena_type_neutral'] = (df3['arena_type'] == 1).astype('int8')\n    df3['arena_type_divided'] = (df3['arena_type'] == 2).astype('int8')\n    df3['arena_type_other'] = (df3['arena_type'] == 3).astype('int8')\n    df3.drop('arena_type', axis=1, inplace=True)\n    \n    # frame rate: 13 values. Mostly correlates with lab, so let it be non-cat.\n    tmp_di = dict(zip(list(dfm['id']), list(dfm['frames_per_second'].fillna(30))))\n    df3['frames_per_second'] = df3['id'].map(tmp_di).fillna(30).astype('float32')\n\n    # duration, log, rounded\n    tmp_di = dict(zip(list(dfm['id']), list(dfm['video_duration_sec'].fillna(900))))\n    df3['video_duration_sec'] = np.round(np.log(1 + df3['id'].map(tmp_di).fillna(6.8)), 1).astype('float32')\n\n    # arena w/h\n    tmp_di = dict(zip(list(dfm['id']), list(dfm['arena_width_cm'].fillna(35))))\n    df3['arena_width_cm'] = df3['id'].map(tmp_di).fillna(35).astype('float32')\n    tmp_di = dict(zip(list(dfm['id']), list(dfm['arena_height_cm'].fillna(30))))\n    df3['arena_height_cm'] = df3['id'].map(tmp_di).fillna(30).astype('float32')\n\n    # add number of possible actions. Log.\n    df3['num_act'] = np.log(np.maximum(.5, mask.sum(-1) - 1)).astype(np.float32) # 1-11. Mostly 1.\n\n    # bring in the frame rate\n    fr_di = dict(zip(list(dfm['id']), list(dfm['frames_per_second'])))\n    fr = np.round(df3['id'].map(fr_di).values, 0).astype(np.float32) # mostly 30, some are 25, 120 and 10\n    fd = df3['video_frame'].values\n    fd = (fd[1:] - fd[:-1]) * 30 / fr[1:]\n    fd = np.maximum(0.25, fd).astype(np.float32) # to avoid /0; use .25 as a floor - for fr=120\n    # feature: number of frames between steps\n    df3['fd'] = 0\n    df3['fd'].iloc[1:] = fd # apply qt to this\n    df3['fd'] = df3['fd'].astype('float32')\n    del fr\n\n    # features for single action: 'tail_base':a,'ear_right':b,'ear_left':c,'nose':d,'body_center':e\n    # dist mid-ear to tail base of mouse 1\n    df3['dist_ear_tail_11'] = np.clip(np.sqrt((df3['x_1_b']/2 + df3['x_1_c']/2 - df3['x_1_a'])**2 + (df3['y_1_b']/2 + df3['y_1_c']/2 - df3['y_1_a'])**2).fillna(120).astype('float32').values, 5, 1000)\n    # dist mid-ear to tail base\n    df3['dist_ear_tail_12'] = np.clip(np.sqrt((df3['x_1_b']/2 + df3['x_1_c']/2 - df3['x_2_a'])**2 + (df3['y_1_b']/2 + df3['y_1_c']/2 - df3['y_2_a'])**2).fillna(230).astype('float32').values, 5, 3000)\n    df3['dist_ear_tail_21'] = np.clip(np.sqrt((df3['x_2_b']/2 + df3['x_2_c']/2 - df3['x_1_a'])**2 + (df3['y_2_b']/2 + df3['y_2_c']/2 - df3['y_1_a'])**2).fillna(230).astype('float32').values, 5, 3000)\n    # dist mid-ear to tail base [shifted by 10] of mouse 1\n    df3['dist_ear_tail_s10_11'] = np.clip(np.sqrt((df3['x_1_b']/2 + df3['x_1_c']/2 - df3['x_1_a'].shift(10))**2 + (df3['y_1_b']/2 + df3['y_1_c']/2 - df3['y_1_a'].shift(10))**2).fillna(130).astype('float32').values, 5, 1000)\n    # dist mid-ear to tail base [shifted by 10]\n    df3['dist_ear_tail_s10_12'] = np.clip(np.sqrt((df3['x_1_b']/2 + df3['x_1_c']/2 - df3['x_2_a'].shift(10))**2 + (df3['y_1_b']/2 + df3['y_1_c']/2 - df3['y_2_a'].shift(10))**2).fillna(240).astype('float32').values, 5, 3000)\n    df3['dist_ear_tail_s10_21'] = np.clip(np.sqrt((df3['x_2_b']/2 + df3['x_2_c']/2 - df3['x_1_a'].shift(10))**2 + (df3['y_2_b']/2 + df3['y_2_c']/2 - df3['y_1_a'].shift(10))**2).fillna(240).astype('float32').values, 5, 3000)\n\n    # average mouse size - for feature scaling only\n    size1 = np.maximum(.1, (df3['id'] * 4 + df3['mid'] // 10).map(m_size_di).values).astype(np.float32) # m_size_di; key: id * 4 + mouse_id\n    size2 = np.maximum(.1, (df3['id'] * 4 + df3['mid'] % 10).map(m_size_di).values).astype(np.float32)\n  \n    # orientation of each mouse: best(based on vector length vs size) of ad or bc-perp\n    # 1\n    # ad\n    ox = (df3['x_1_d'] - df3['x_1_a']).values / 3.2 # orientation vector; scale to the same mean length\n    oy = (df3['y_1_d'] - df3['y_1_a']).values / 3.2 # orientation vector\n    # bc perp\n    oyt = -(df3['x_1_b'] - df3['x_1_c']).values # orientation vector\n    oxt = (df3['y_1_b'] - df3['y_1_c']).values # orientation vector\n    # blend\n    osize1 = np.sqrt(ox**2 + oy**2)\n    osizet = np.sqrt(oxt**2 + oyt**2)\n    \n    w = (np.abs(osize1 - size1 / 2.4) < np.abs(osizet - size1 / 2.4)) * 1 # pick the better one\n    # w/wt\n    ox = (ox * w + oxt * (1 - w)).astype(np.float32)\n    oy = (oy * w + oyt * (1 - w)).astype(np.float32)\n\n    # 2\n    # ad\n    ox2 = (df3['x_2_d'] - df3['x_2_a']).values / 3.2 # orientation vector; scale to the same mean length\n    oy2 = (df3['y_2_d'] - df3['y_2_a']).values / 3.2 # orientation vector\n    # bc perp\n    oyt = -(df3['x_2_b'] - df3['x_2_c']).values # orientation vector\n    oxt = (df3['y_2_b'] - df3['y_2_c']).values # orientation vector\n    # blend\n    osize2 = np.sqrt(ox2**2 + oy2**2)\n    osizet = np.sqrt(oxt**2 + oyt**2)\n    \n    w = (np.abs(osize2 - size2 / 2.4) < np.abs(osizet - size2 / 2.4)) * 1 # pick the better one\n    # w/wt\n    ox2 = (ox2 * w + oxt * (1 - w)).astype(np.float32)\n    oy2 = (oy2 * w + oyt * (1 - w)).astype(np.float32)\n    del oxt, oyt, osizet, w, size1, size2\n    gc.collect()\n\n    osize1 = np.maximum(.1, np.sqrt(ox**2 + oy**2)).astype(np.float32) # length of orientation vector - use for all angles\n    osize2 = np.maximum(.1, np.sqrt(ox2**2 + oy2**2)).astype(np.float32) # length of orientation vector - use for all angles\n\n    # 'size' of each mouse\n    a, b = fit_ellipse(df3, cols=['x_1_a','x_1_b','x_1_c','x_1_d','x_1_e','y_1_a','y_1_b','y_1_c','y_1_d','y_1_e']) # m1 axis\n    df3['m1_maj'] = a\n    df3['m1_min'] = b\n    df3['m1_r'] = np.maximum(.5, a / b).astype(np.float32)\n    a, b = fit_ellipse(df3, cols=['x_2_a','x_2_b','x_2_c','x_2_d','x_2_e','y_2_a','y_2_b','y_2_c','y_2_d','y_2_e']) # m2 axis\n    df3['m2_maj'] = a\n    df3['m2_min'] = b\n    df3['m2_r'] = np.maximum(.5, a / b).astype(np.float32)\n    del a, b\n    gc.collect()\n\n\n    # cos/sin of angle between mice\n    df3['cos12'] = np.round(((ox * ox2 + oy * oy2) / osize1 / osize2), 4).astype(np.float32) # orientation ************************\n    df3['sin12'] = np.round(((ox * oy2 - oy * ox2) / osize1 / osize2), 4).astype(np.float32) # orientation ************************\n\n\n    # distance between noses\n    df3['dist_nn_abs'] = np.maximum(2, np.sqrt((df3['x_1_d'] - df3['x_2_d'])**2 + (df3['y_1_d'] - df3['y_2_d'])**2)) # floor to make single=0 stand out less\n    # distance nose to tail\n    df3['dist_nt_abs'] = np.sqrt((df3['x_1_d'] - df3['x_2_a'])**2 + (df3['y_1_d'] - df3['y_2_a'])**2)\n    # distance tail to nose\n    df3['dist_tn_abs'] = np.sqrt((df3['x_2_d'] - df3['x_1_a'])**2 + (df3['y_2_d'] - df3['y_1_a'])**2)\n    \n                                                    \n    # calculate absolute speed based on 3 best points\n    w = np.ones([df3.shape[0]-1, 4], dtype=np.int32)\n    sx1 = df3[['x_1_a', 'x_1_b', 'x_1_c', 'x_1_d']].values[1:] - df3[['x_1_a', 'x_1_b', 'x_1_c', 'x_1_d']].values[:-1]\n    idx = np.abs(sx1 - sx1.mean(-1).reshape(-1,1)).argmax(-1)\n    w[np.arange(w.shape[0]),idx] = 0\n    sx1 = ((sx1 * w).sum(1) / w.sum(1)).astype(np.float32)\n\n    w = np.ones([df3.shape[0]-1, 4], dtype=np.int32)\n    sy1 = df3[['y_1_a', 'y_1_b', 'y_1_c', 'y_1_d']].values[1:] - df3[['y_1_a', 'y_1_b', 'y_1_c', 'y_1_d']].values[:-1]\n    idx = np.abs(sy1 - sy1.mean(-1).reshape(-1,1)).argmax(-1)\n    w[np.arange(w.shape[0]),idx] = 0\n    sy1 = ((sy1 * w).sum(1) / w.sum(1)).astype(np.float32)\n\n    w = np.ones([df3.shape[0]-1, 4], dtype=np.int32)\n    sx2 = df3[['x_2_a', 'x_2_b', 'x_2_c', 'x_2_d']].values[1:] - df3[['x_2_a', 'x_2_b', 'x_2_c', 'x_2_d']].values[:-1]\n    idx = np.abs(sx2 - sx2.mean(-1).reshape(-1,1)).argmax(-1)\n    w[np.arange(w.shape[0]),idx] = 0\n    sx2 = ((sx2 * w).sum(1) / w.sum(1)).astype(np.float32)\n\n    w = np.ones([df3.shape[0]-1, 4], dtype=np.int32)\n    sy2 = df3[['y_2_a', 'y_2_b', 'y_2_c', 'y_2_d']].values[1:] - df3[['y_2_a', 'y_2_b', 'y_2_c', 'y_2_d']].values[:-1]\n    idx = np.abs(sy2 - sy2.mean(-1).reshape(-1,1)).argmax(-1)\n    w[np.arange(w.shape[0]),idx] = 0\n    sy2 = ((sy2 * w).sum(1) / w.sum(1)).astype(np.float32)\n    del w, idx\n    gc.collect()\n\n\n    # distance nose to real tail and vv ********************************************\n    df3['dist_nrt'] = np.sqrt((df3['x_1_d'] - df3['x_2_f'])**2 + (df3['y_1_d'] - df3['y_2_f'])**2)\n    df3['dist_rtn'] = np.sqrt((df3['x_2_d'] - df3['x_1_f'])**2 + (df3['y_2_d'] - df3['y_1_f'])**2)\n    # dist nt of mouse 1 only\n    df3['dist_nrt_1'] = np.sqrt((df3['x_1_d'] - df3['x_1_f'])**2 + (df3['y_1_d'] - df3['y_1_f'])**2)\n    idx = df3['x_2_f'] < -.99 # missing tail - set dist to xxx\n    df3['dist_nrt'].loc[idx] = 150 # replace missing with mean values\n    df3['dist_rtn'].loc[idx] = 150 # replace missing with mean values\n    df3['dist_nrt_1'].loc[idx] = 100 # replace missing with mean values\n    \n\n    # distance between centers, absolute\n    x1 = df3['x_1_e'].values # center: mean of all points\n    df3.drop(['x_1_a', 'x_1_b', 'x_1_c', 'x_1_d', 'x_1_f'], axis=1, inplace=True)\n    gc.collect()\n    y1 = df3['y_1_e'].values # center\n    df3.drop(['y_1_a', 'y_1_b', 'y_1_c', 'y_1_d', 'y_1_f'], axis=1, inplace=True)\n    gc.collect()\n    x2 = df3['x_2_e'].values # center\n    df3.drop(['x_2_a', 'x_2_b', 'x_2_c', 'x_2_d', 'x_2_f'], axis=1, inplace=True)\n    gc.collect()\n    y2 = df3['y_2_e'].values # center\n    df3.drop(['y_2_a', 'y_2_b', 'y_2_c', 'y_2_d', 'y_2_f'], axis=1, inplace=True)\n    gc.collect()\n    print('    dropped all x/y', int(time() - t0), 'sec')\n    df3['dist_abs'] = np.maximum(2, np.sqrt((x1 - x2)**2 + (y1 - y2)**2)) # floor to make single=0 stand out less\n\n   \n    # features: angle of each mouse to vector between them\n    dx = x1 - x2\n    dy = y1 - y2\n    df3['cos1_dist'] = ((ox * dx + oy * dy) / osize1 / np.maximum(.1, np.sqrt(dx**2 + dy**2))).astype(np.float32)  # orientation ************************\n    df3['cos2_dist'] = ((ox2 * dx + oy2 * dy) / osize2 / np.maximum(.1, np.sqrt(dx**2 + dy**2))).astype(np.float32)  # orientation ************************\n    del dx, dy, x1, x2, y1, y2\n    gc.collect()\n\n    # speed, absolute\n    df3['speed1'] = 0\n    df3['speed1'].iloc[1:] = np.sqrt(sx1**2 + sy1**2) / fd\n    df3['speed1'] = df3['speed1'].astype('float32')\n    \n    df3['speed2'] = 0\n    df3['speed2'].iloc[1:] = np.sqrt(sx2**2 + sy2**2) / fd\n    df3['speed2'] = df3['speed2'].astype('float32')\n    \n    # angle between speed and orientation - to catch 'rear' action\n    df3['cos_o_speed_1'] = 1\n    df3['cos_o_speed_1'].iloc[1:] = (sx1 * ox[1:] + sy1 * oy[1:]) / np.sqrt(sx1**2 + sy1**2) / osize1[1:] # orientation ************************\n    df3['cos_o_speed_1'] = df3['cos_o_speed_1'].fillna(1).astype('float32')\n    df3['cos_o_speed_2'] = 1\n    df3['cos_o_speed_2'].iloc[1:] = (sx2 * ox2[1:] + sy2 * oy2[1:]) / np.sqrt(sx2**2 + sy2**2) / osize2[1:] # orientation ************************\n    df3['cos_o_speed_2'] = df3['cos_o_speed_2'].fillna(1).astype('float32')\n    del osize1, osize2\n    gc.collect()\n\n    # cos s1 s2\n    df3['cos_s1_s2'] = 1\n    df3['cos_s1_s2'].iloc[1:] = (sx2 * sx1 + sy2 * sy1) / np.sqrt(sx2**2 + sy2**2) / np.sqrt(sx1**2 + sy1**2)\n    df3['cos_s1_s2'] = np.round(df3['cos_s1_s2'].fillna(1), 4).astype('float32')\n    del ox, oy, ox2, oy2\n    gc.collect()\n\n    # relative speed, absolute\n    df3['rel_speed'] = 0\n    df3['rel_speed'].iloc[1:] = np.sqrt((sx1 - sx2)**2 + (sy1 - sy2)**2) / fd\n    df3['rel_speed'] = np.maximum(.005, df3['rel_speed']).astype('float32') # floor to make single=0 stand out less\n    del fd, sx1, sx2, sy1, sy2\n    gc.collect()\n    \n    # 'rear' - min cos b/w orientation and speed\n    for dd in [3, 5, 10, 20]:\n        for c in ['cos_o_speed_1']:\n            df3[c+'_min_'+str(dd)] = df3[c].rolling(dd, center=True, min_periods=1).min().fillna(0).astype('float32')\n\n    # speed: break between segments\n    idx = (df3['id'] * 4 + df3['mid']).values # unique index of a series of frames\n    idx = (idx[1:] != idx[:-1]).astype(np.int8)\n    idx = np.concatenate((np.ones(1, dtype=np.int8), idx)) # now this is index of first frame of new series - use that for speed reset\n    for c in ['speed1_abs', 'speed2_abs', 'speed1_sca', 'speed2_sca', 'rel_speed_abs', 'rel_speed_sca']:\n        if c in df3.columns:\n            df3[c].loc[idx] = 0\n\n    # combine x12 and y12\n    df3['x_e'] = (df3['x_1_e'] + df3['x_2_e']) / 2\n    df3['y_e'] = (df3['y_1_e'] + df3['y_2_e']) / 2\n    df3.drop(['x_1_e', 'x_2_e', 'y_1_e', 'y_2_e'], axis=1, inplace=True)\n    gc.collect()\n\n    if df3.isna().sum().sum() > 0: # check for NA\n        print(df3.isna().sum())\n        df3 = df3.fillna(method='ffill') # better than fill with 0\n        df3 = df3.fillna(method='bfill') # JIC\n    return df3\n\n\n\n\n\n# x\nN = 256 # length of data series ********************\ndef construct_x(df3, mask, train=0):\n    global norm_di\n    cols = list(df3.columns[3:]) # skip index(3)\n    x = df3[cols].values.astype(np.float32)\n    normo = cols.index('frames_per_second') # norm offset - skip binaries, start at 'frames_per_second'. 21. So norm 5 vars only\n    qto = cols.index('fd') # qt offset - start at true* float, 'fd'. 26\n    df3 = df3[df3.columns[:3]] # reduce size\n    gc.collect()\n\n    # skip QT. And skip binaries.\n    for i in range(normo, qto): # skip binaries, skip qt\n        if train:\n            x_m = x[:,i].mean()\n            x_s = x[:,i].std()\n            norm_di['x_m'+str(i)] = x_m\n            norm_di['x_s'+str(i)] = x_s\n        else:\n            x_m = norm_di['x_m'+str(i)]\n            x_s = norm_di['x_s'+str(i)]\n        x[:,i] -= x_m\n        x[:,i] /= np.maximum(.00001, x_s)\n\n    # QT x/y into uniform[0,1]/normal: first 'qto' vars have low cardinality\n    # sometimes QT does not work - single actions have 0 for rel_speed, dist, ...\n    if nn_mode == 'train':\n        qt = QuantileTransformer(n_quantiles=1000, output_distribution='normal', subsample=None, random_state=0, copy=False)\n        qt = qt.fit(x[x[:,0]<.5, qto:]) # fit on data excluding single (single is the first var)\n        joblib.dump(qt, model_dir + 'qt.pkl')\n    else:\n        qt = joblib.load(model_dir + 'qt.pkl')\n    x[:,qto:] = qt.transform(x[:,qto:])\n\n    x = x.astype(np.float32) # JIC\n    l = np.minimum(N_LAB - 1, df3['id'].map(id_di).str[2].astype('int8').values)\n   \n    # trim/reshape everything\n    df3 = df3.iloc[:N * (df3.shape[0]//N)] # trim df3\n    x = x[:N * (x.shape[0]//N), :].reshape(-1, N, x.shape[-1])\n    mask = mask[:N * (mask.shape[0]//N), :].reshape(-1, N, mask.shape[-1])\n    l = l[:df3.shape[0]].reshape(-1, N)\n    return x, df3, mask, l\n\n\n\n\n\n# real submission\ndef prepare_sub(yp, df3, mp, mp3, sm=0, sm1=0):\n    # calc yp2\n    lab = df3['id'].map(id_di).str[2].values[:yp.shape[0]].astype(np.int8)\n    if sm1 == 0:\n        yp2 = ((yp > mp[lab, :]) * 1).astype(np.int8)\n    else:\n        yp2 = ((savgol_filter(yp, window_length=sm1, polyorder=1, axis=0) > mp[lab, :]) * 1).astype(np.int8)\n\n    # round of smoothing, to eliminate runs of 1: reduces size of sub, does not really improve the score.\n    if sm > 1:\n        yp2 = np.round(savgol_filter(yp2, window_length=sm, polyorder=1, axis=0), 0).astype(np.int8) # 10/1 seems best\n\n    # reset prediction on id+mid change - need this to avoid submission errors!\n    d2 = df3[['id', 'video_frame', 'mid', 'video_frame']].values\n    d2[:,3] = d2[:,0] * (d2[:,2].max() + 1) + d2[:,2] # combine id and mid\n    d2 = np.concatenate((d2[-1:,:], d2), axis=0) # make it longer - append 1 to the front; this shifts it forward by 1!!!!!!\n    idx = d2[1:yp2.shape[0]+1,3] != d2[:yp2.shape[0],3] # this is the index of the first frame in a series\n    yp2[idx,:] = 0\n    \n    # remove duplicate actions\n    idx = yp2.sum(-1) > 1\n    if idx.sum() > 0:\n        idx2 = (yp[idx,:] / mp3[lab[idx], :]).argmax(-1) # highest adjusted probability\n        yp2[idx,:] *= 0 # for now, just remove all of them\n        yp2[idx,idx2] = 1 # assign action to ...\n    \n    # loop over actions\n    res = []\n    for j in range(yp2.shape[1]):\n        i1 = 0\n        a = yp2[0,j] # init to starting status\n        idx = yp2[1:,j] != yp2[:-1,j]\n        ii = 1 + np.arange(yp2.shape[0] - 1)[idx] # these are indices of prediction change\n        for i in ii:\n            if a == 0: # start action\n                a, i1 = 1, i\n            else: # stop action and record it\n                a = 0\n                res.append([d2[i1+1,0], d2[i1+1,2], j, d2[i1+1,1], 1 + d2[i,1]]) # id, mid, action, start, stop; shift by 1 + lengthen by 1!!!\n    sub = pd.DataFrame(res)\n    del res, yp2\n    gc.collect()\n    sub.columns = ['id', 'mid', 'action', 'start_frame', 'stop_frame']\n    sub['id'] = sub['id'].map(id_di)\n    sub['video_id'] = sub['id'].str[1]\n    sub['action'] = sub['action'].map(action_di2)\n    sub['agent_id']  = sub['mid'].map({1:'mouse1', 2:'mouse1', 3:'mouse1', 12:'mouse2', 13:'mouse2', 23:'mouse3', 0:'mouse1', 10:'mouse2', 11:'mouse2', 20:'mouse3', 21:'mouse3', 22:'mouse3', 30:'mouse4', 31:'mouse4', 32:'mouse4', 33:'mouse4'})\n    sub['target_id'] = sub['mid'].map({1:'mouse2', 2:'mouse3', 3:'mouse4', 12:'mouse3', 13:'mouse4', 23:'mouse4', 0:'mouse1', 10:'mouse1', 11:'mouse2', 20:'mouse1', 21:'mouse2', 22:'mouse3', 30:'mouse1', 31:'mouse2', 32:'mouse3', 33:'mouse4'})\n    # relabel self-actions\n    sub['target_id'].loc[sub['target_id'] == sub['agent_id']] = 'self'\n    sub['row_id'] = sub.index\n    sub = sub[['row_id','video_id','agent_id','target_id','action','start_frame','stop_frame']] # drop and reorder, JIC\n    return sub\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T15:55:41.521197Z","iopub.execute_input":"2025-12-15T15:55:41.521702Z","iopub.status.idle":"2025-12-15T15:55:41.881504Z","shell.execute_reply.started":"2025-12-15T15:55:41.521678Z","shell.execute_reply":"2025-12-15T15:55:41.880675Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"NN execution","metadata":{}},{"cell_type":"code","source":"# code *******************************************************************************************************************************************\nif nn_mode == 'submit': # submit part *********************************************************************\n    verbose = 1\n    norm_di = joblib.load(model_dir + 'norm_di.pkl')\n    \n    df, dfm, id_di, id_di2, m_size_di = read_data(train = 0) # read test data\n    df3 = prepare_pairs() # format into mouse pairs\n    df3, mask = prepare_mask(df3) # construct 'behaviors_labeled' mask\n    df3 = FE(df3) # FE\n    x, df3, mask, l0 = construct_x(df3, mask, train=0) # x\n\n    # get the test prediction\n    S = 1024 # batch size\n    yp = np.zeros([x.shape[0], x.shape[1], N_ACT], dtype=np.float32) # predicted - skip 'no action'\n    run_mult = {3:0.15, 4:0.08, 9:0.03, 16:0.17} # blend 4 NN models with these weights\n    with tf.device('/GPU:0'):\n        for run in [3,4,9,16]:\n          for fold in range(folds):\n            model = tf.keras.models.load_model(model_dir+'model'+str(fold)+'_'+str(run)+'.keras')\n            for i in range(1 + x.shape[0]//S):\n                i1 = S * i\n                i2 = np.minimum(S * (i+1), x.shape[0])\n                if run == 16: # N=512, use special logic\n                    if (i2 - i1)%2 == 1: # make it even\n                        i2 -= 1\n                    yp[i1:i2,:,:] = (yp[i1:i2,:,:].ravel() + run_mult[run] * model.predict((x[i1:i2,:,:].reshape(-1,N*2,x.shape[-1]), mask[i1:i2,:,:].reshape(-1,N*2,mask.shape[-1]), l0[i1:i2,:].reshape(-1,N*2)), verbose=0, batch_size=S)[:,:,1:].ravel() / folds).reshape(-1,N,yp.shape[-1])\n                else:\n                    yp[i1:i2,:,:] += run_mult[run] * model.predict((x[i1:i2,:,:], mask[i1:i2,:,:], l0[i1:i2,:]), verbose=0, batch_size=S)[:,:,1:] / folds # skip 'no action'\n\n    # TTA - shift by N/2\n    x = x.reshape(-1, x.shape[-1])[N//2:-N//2].reshape(-1, N, x.shape[-1])\n    mask = mask.reshape(-1, mask.shape[-1])[N//2:-N//2].reshape(-1, N, mask.shape[-1])\n    l0 = l0.ravel()[N//2:-N//2].reshape(-1, N)\n    yp2 = np.zeros([x.shape[0], x.shape[1], N_ACT], dtype=np.float32) # predicted - skip 'no action'\n    with tf.device('/GPU:0'):\n        for run in [3,4,9,16]:\n          for fold in range(folds):\n            model = tf.keras.models.load_model(model_dir+'model'+str(fold)+'_'+str(run)+'.keras')\n            for i in range(1 + x.shape[0]//S):\n                i1 = S * i\n                i2 = np.minimum(S * (i+1), x.shape[0])\n                if run == 16: # N=512, use special logic\n                    if (i2 - i1)%2 == 1: # make it even\n                        i2 -= 1\n                    yp2[i1:i2,:,:] = (yp2[i1:i2,:,:].ravel() + run_mult[run] * model.predict((x[i1:i2,:,:].reshape(-1,N*2,x.shape[-1]), mask[i1:i2,:,:].reshape(-1,N*2,mask.shape[-1]), l0[i1:i2,:].reshape(-1,N*2)), verbose=0, batch_size=S)[:,:,1:].ravel() / folds).reshape(-1,N,yp.shape[-1])\n                else:\n                    yp2[i1:i2,:,:] += run_mult[run] * model.predict((x[i1:i2,:,:], mask[i1:i2,:,:], l0[i1:i2,:]), verbose=0, batch_size=S)[:,:,1:] / folds # skip 'no action'\n\n    # reduce memory in submission; still need df3\n    del x, mask, l0\n    gc.collect()\n    \n    # unroll\n    yp = yp.reshape(-1, N_ACT)\n    yp2 = yp2.reshape(-1, N_ACT)\n    yp[N//2:-N//2,:] = (yp[N//2:-N//2,:] + yp2) / 2\n\n\n    yp = yp / 0.43 # undo blending weights ********************************************************************************\n\n    # reduce memory in submission; still need df3\n    del yp2\n    gc.collect()\n\n    # do not submit anything here, blend predictions in the end *******************************************************\n    print('done', int(time() - t0), 'sec')\n\n\n\n\n\nif nn_mode != 'submit': # train/validate part *****************************************************************************************************\n    df, dfm, dfa, dfa0, id_di, id_di2, m_size_di = read_data(train = 1) # read train data and combine all body parts\n    print('read', df.shape, dfa.shape, dfm.shape, int(time() - t0), 'sec')\n\n    df3 = prepare_pairs() # format into mouse pairs\n    print('format into mouse pairs', df3.shape, int(time() - t0), 'sec') # exacly double the individual count.\n    \n    df3, mask = prepare_mask(df3) # construct 'behaviors_labeled' mask.\n    print('masks', mask.shape, int(time() - t0), 'sec') # drop over 1/2 unlabeled ones (mostly self-actions)\n\n    df3 = FE(df3) # FE\n    print('FE', df3.shape, int(time() - t0), 'sec')\n    print(df3.columns)\n    print(df3.dtypes.value_counts())\n\n    # target: add it as the last column in the data\n    dfa['action'] = dfa['action'].map(action_di).astype('int8')\n    dfa['mid'] = dfa['target_id'] + dfa['agent_id'] * 10 # 1, 2, 3, 12, 13, 23, etc - same as df3\n    dfa.drop(['agent_id', 'target_id'], axis=1, inplace=True)\n    dfa = dfa.sort_values(by=['id', 'mid', 'start_frame']).reset_index(drop=True)\n\n    # expand all frames\n    res = np.zeros([(dfa['stop_frame']-dfa['start_frame']).sum(), 4], dtype=np.int32) # 5 Mil\n    i1 = 0\n    for i in range(dfa.shape[0]):\n        f1 = dfa['start_frame'].iloc[i]\n        f2 = dfa['stop_frame'].iloc[i]\n        i2 = i1 + f2 - f1\n        res[i1:i2,0] = dfa['id'].iloc[i]\n        res[i1:i2,1] = np.arange(f1, f2)\n        res[i1:i2,2] = dfa['mid'].iloc[i]\n        res[i1:i2,3] = dfa['action'].iloc[i]\n        i1 = i2\n    dfa2 = pd.DataFrame(res)\n    dfa2.columns =['id', 'video_frame', 'mid', 'action']\n\n    # merge with data\n    df3 = df3.merge(dfa2, on=['id', 'video_frame', 'mid'], how='left')\n    df3['action'] = df3['action'].fillna(-1).astype('int8')\n    del dfa2, res\n    gc.collect()\n    print('target', df3.shape, 'num records with action', (df3['action']>=0).sum(), int(time() - t0), 'sec')\n\n    # model: prep data\n    # y, never shaped into N\n    y0 = 1 + df3['action'].values # 0 to N_ACT (N_ACT+1 total, incl 'no action', which is 0)\n    y0 = y0[:N * (y0.shape[0]//N)] # trim\n    df3.drop('action', axis=1, inplace=True)\n    \n    x, df3, mask, l0 = construct_x(df3, mask, train=1) # x\n    if nn_mode == 'train': # save scaling data in train only\n        joblib.dump(norm_di, model_dir + 'norm_di.pkl')\n    print('model: prep data', x.shape, int(time() - t0), 'sec')\n\n   \n    # model: training\n    DR1         = 0.3\n    LL2         = 1e-5\n    filters     = 256\n    BS          = 32\n    D1          = 256\n    act         = 'selu'\n    lr0         = 5e-3\n    lrd         = 25\n\n    verbose = 1\n    np.random.seed(seed)\n    random.seed(seed)\n    yp = np.zeros([x.shape[0], N, N_ACT], dtype=np.float32) # predicted - skip 'no action', so N_ACT and not N_ACT+1\n    with tf.device('/GPU:0'):\n        class data_gen(Sequence): # data generator\n            def __init__(self, idx, batch_size, train): # constructor: save all data locally\n                self.idx, self.batch_size, self.train = idx.astype(np.int32), batch_size, train\n                super().__init__(max_queue_size=4000)\n                return\n\n            def __len__(self): # returns number of batches\n                return math.ceil(len(self.idx) / self.batch_size)\n\n            def __getitem__(self, b): # returns one batch\n                idx2 = self.idx[self.batch_size * b:self.batch_size * (b + 1)]\n                \n                # train-time augmentation: shift all by random amount from 0 to 128\n                if self.train:\n                    idx2 = np.minimum(idx2, x.shape[0] - 2) # cap the index at ... - 1\n                    o = np.random.randint(N)\n                    xb = x.reshape(-1, x.shape[-1])[o:o-N,:].reshape(-1, N, x.shape[-1])[idx2,:,:]\n                    mb = mask.reshape(-1, mask.shape[-1])[o:o-N,:].reshape(-1, N, mask.shape[-1])[idx2,:,:]\n                    lb = l0.ravel()[o:o-N].reshape(-1, N)[idx2,:]\n                    yb = y0[o:o-N].reshape(-1, N)[idx2,:]\n                else: # do not augment validation data; best w/b to average several augmentations, but never to pick a random one!\n                    xb, mb, lb, yb = x[idx2,:,:], mask[idx2,:,:], l0[idx2,:], y0.reshape(-1, N)[idx2,:]\n                return ((xb, mb, lb), yb)\n\n            def on_epoch_end(self): # this shuffle greatly improves convergence\n                if self.train: # do not shuffle va!\n                    np.random.shuffle(self.idx)\n\n        kf = StratifiedGroupKFold(n_splits=folds, shuffle=False, random_state=None) # groups: videos\n        l1 = np.median(df3['id'].map(id_di).str[2].values[:N * x.shape[0]].reshape(-1, N), axis=-1).astype(np.int16) # stratified by lab\n        for fold, (tr_ind, va_ind) in enumerate(kf.split(x, l1, np.median(df3['id'].values[:N * x.shape[0]].reshape(-1, N), axis=-1).astype(np.int16))):\n            if nn_mode == 'train': # train the model *****************************************************************************************\n                i1 = Input(shape=(N, x.shape[2],), dtype='float32', name='x')\n                i2 = Input(shape=(N, N_ACT + 1,), dtype='int8', name='mask')\n                i3 = Input(shape=(N, ), dtype='int8', name='lab')\n\n                i3b= Embedding(input_dim=N_LAB, output_dim=8, input_length=1, name='embed1')(i3)\n                i1a= Concatenate(name='x_l1')([i1, i3b])\n\n                l1 = Bidirectional(LSTM(filters//2, return_sequences=True, kernel_regularizer=L2(LL2), recurrent_regularizer=L2(LL2)), name='LSTM1')(sequences=i1a)\n                l1 = Dropout(DR1, name='DR1')(l1)\n\n                d1 = Dense(D1, activation=act, kernel_initializer='lecun_normal', name='D1')(l1)\n                \n                i3a= Embedding(input_dim=N_LAB, output_dim=4, input_length=1, name='embed2')(i3)\n                do = Concatenate(name='x_l2')([d1, i3a])\n                \n                o  = Dense(N_ACT + 1, activation=None, name='Do')(do) # N_ACT+1 actions, including 'no action'\n                \n                o  = o * i2 - (1 - i2) * 6.0 # apply mask and set excluded actions to -6 (1/400)\n                o  = Softmax(name='o')(o)\n                \n                model = tf.keras.Model(inputs=(i1, i2, i3), outputs=o)\n                if verbose and fold == 0: print(model.summary())\n                sc = CosineDecayRestarts(initial_learning_rate=lr0*0.6, first_decay_steps=4) # reduce lr0\n                es = EarlyStopping(monitor='val_loss', start_from_epoch=5, patience=69, mode='min', verbose=1*verbose, restore_best_weights=True)\n                \n                class print_lr(Callback):\n                    def on_epoch_end(self, epoch, logs=None):\n                        print('epoch', epoch + 1, 'lr', np.round(np.array(self.model.optimizer.learning_rate), 6), ' ', end='')\n                \n                model.compile(optimizer=Adam(learning_rate=sc), loss=SparseCategoricalCrossentropy())\n                tr = data_gen(tr_ind, BS, 1)\n                va = data_gen(va_ind, BS, 0)\n                h = model.fit(x=tr, validation_data=va, epochs=74, callbacks=[es, print_lr()], verbose=2*verbose)\n                model.save(model_dir+'model'+str(fold)+'.keras')\n                del tr, va\n                gc.collect()\n                \n            if nn_mode == 'validate': # load saved model ****************************************************************************\n                model = tf.keras.models.load_model(model_dir+'model'+str(fold)+'.keras')\n                \n            S = 1024 # prediction batch size\n            for i in range(1 + len(va_ind)//S):\n                i1, i2 = S * i, np.minimum(S * (i + 1), len(va_ind))\n                idx = va_ind[i1:i2]\n                yp[idx,:,:] = model.predict((x[idx,:,:], mask[idx,:,:], l0[idx,:]), verbose=0, batch_size=S)[:,:,1:] # skip 'no action'\n            if verbose: print('    finished fold', fold, int(time() - t0), 'sec')\n            del tr_ind, va_ind, model\n            gc.collect()\n\n    # reduce ram usage\n    del x, mask\n    gc.collect()\n    \n    # unroll\n    yp = yp.reshape(-1, N_ACT)\n    l0 = l0.ravel()\n    \n    # drop all labs past 15 - don't need them after training! **********************\n    idx = l0 < 15\n    y0 = y0[idx]\n    yp = yp[idx,:]\n    df3 = df3.loc[idx].reset_index(drop=True)\n    dfa0 = dfa0.loc[(dfa0['lab_id'].str[:8] != 'CalMS21_') & (dfa0['lab_id'].str[:6] != 'CRIM13')]\n\n    # score the prediction\n    dft = pd.DataFrame(y0)\n    dft.columns = ['y']\n    dft['id'] = df3['id']\n    dft['l'] = dft['id'].map(id_di).str[2].astype('int8')\n    dft['video_frame'] = df3['video_frame']\n    dft['w'] = df3['video_frame']\n    dft['w'].iloc[1:] = np.maximum(1, df3['video_frame'].values[1:dft.shape[0]] - df3['video_frame'].values[:dft.shape[0]-1])\n\n    # reset prediction on id+mid change\n    d2 = df3[['id', 'video_frame', 'mid', 'video_frame']].values\n    d2[:,3] = d2[:,0] * (d2[:,2].max() + 1) + d2[:,2] # combine id and mid\n    d2 = np.concatenate((d2[-1:,:], d2), axis=0) # make it longer\n    idx = d2[1:yp.shape[0]+1,3] != d2[:yp.shape[0],3] # this is the index of the first frame in a series\n    yp[idx,:] = 0\n    dft['w'].loc[idx] = 0\n    del d2\n\n    # find threshold to optimize F1. By action + lab.\n    mp = np.ones([15, N_ACT], dtype=np.float32) # 15 labs by N_ACT actions\n    li = []\n    for l1 in range(1 + dft['l'].max()):\n        idx = dft['l'] == l1\n        dft1 = dft.loc[idx].reset_index(drop=True)\n        yp1 = yp[idx,:]\n        for i in range(N_ACT):\n            if (dft1['y'] == 1 + i).sum() > 0:\n                a = -yp1[:,i].ravel() # predicted probability\n                b = (dft1['y'] == 1 + i).values.astype(np.int8) # y\n                w = dft1['w'].values.astype(np.int8) # w = frames per observation\n                b = b * w # y, with weight\n                idx = np.argsort(a, order=None) # this is where all the runtime goes\n                a = -a[idx]\n                b = b[idx]\n                w = w[idx]\n                b = b.cumsum()\n                w = w.cumsum() # predicted count, with weight\n                f = 2 * b / (b[-1] + w) # here b[-1] id yc(constant), w is ypc(cumulative by threshold)\n                j = f.argmax()\n                mp[l1, i] = (a[j] + a[j-1]) / 2\n                li.append([l1, f[j], i, b[j], b[-1], w[j]]) # save expected F1\n    li2 = pd.DataFrame(li).groupby(0)[1].mean() # score by lab. lab/score/action/tp/yc/ypc\n    if verbose: print('mp: best score:', np.round(li2.values.mean(), 3), int(time() - t0), 'sec')\n    if nn_mode == 'train': # save mp\n        joblib.dump(mp, model_dir + 'mp.pkl') # save this for training only!!!\n\n    # construct yp2 and my score ********\n    lab = dft['l'].values[:yp.shape[0]]\n    yp2 = ((yp > mp[lab, :]) * 1).astype(np.int8)\n\n    # remove duplicate actions\n    idx = yp2.sum(-1) > 1\n    if idx.sum() > 0:\n        idx2 = (yp[idx,:] / mp[lab[idx],:]).argmax(-1) # highest adjusted probability\n        yp2[idx,:] *= 0 # for now, just remove all of them\n        yp2[idx,idx2] = 1 # assign action to ...\n\n    # compute my version of score by lab; it does not match exactly because of missing frames\n    dft['yp'] = (yp2 * np.array(np.arange(1, N_ACT+1)).reshape(1, -1)).sum(-1).astype('int8')\n    dft['match'] = (dft['yp'] != 0 ) * (dft['y'] == dft['yp']) * 1\n    dft['mid'] = df3['mid']\n    dft['m2'] = dft['y'] * dft['match']\n\n    # start with TP\n    dft2 = dft.groupby(['m2', 'l'])['w'].sum().reset_index()\n    dft2.columns = ['y','l','TP']\n\n    # add FP(ypc)\n    dft3 = dft.groupby(['yp','l'])['w'].sum().reset_index()\n    dft3.columns = ['y', 'l','FP']\n    dft2 = dft2.merge(dft3, on=['y', 'l'], how='outer')\n\n    # add FN(yc)\n    dft3 = dft.groupby(['y','l'])['w'].sum().reset_index()\n    dft3.columns = ['y', 'l','FN']\n    dft2 = dft2.merge(dft3, on=['y', 'l'], how='outer')\n    \n    # F1\n    dft2['TP'] = dft2['TP'].fillna(0)\n    dft2['FN'] = dft2['FN'].fillna(0) - dft2['TP']\n    dft2['FP'] = dft2['FP'].fillna(0) - dft2['TP']\n    dft2['F1'] = dft2['TP'] * 2 / (dft2['TP'] * 2 + dft2['FP'] + dft2['FN']) # only now calc F1\n    dft2 = dft2.loc[dft2['y'] > 0].reset_index(drop=True) # drop 0\n    dft3 = dft2.groupby('l')['F1'].mean().reset_index() # F1 by lab\n    if verbose: print('my score by lab', np.round(dft3['F1'].mean(), 3), int(time() - t0), 'sec')\n\n    # score\n    sub = prepare_sub(yp, df3) # real submission\n    sc = score(dfa0, sub, row_id_column_name='row_id')\n    if verbose: print('score_predicted', np.round(sc, 3), int(time() - t0), 'sec***********')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T15:55:41.882475Z","iopub.execute_input":"2025-12-15T15:55:41.882865Z","iopub.status.idle":"2025-12-15T15:56:51.897400Z","shell.execute_reply.started":"2025-12-15T15:55:41.882840Z","shell.execute_reply":"2025-12-15T15:56:51.896640Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Second, do XGB prediction. Save result in \"df\", which combined metadata and the predictions","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import f1_score\nfrom xgboost import XGBClassifier\nimport numpy as np\nimport os, json, gc, glob, joblib, warnings, itertools\nfrom scipy.signal import savgol_filter\nfrom time import time\nt0 = time()\nwarnings.filterwarnings('ignore')\n\n\nfolds = 5\nverbose = 1\nseed = 13\n\nclass CFG:\n    train_path = \"/kaggle/input/MABe-mouse-behavior-detection/train.csv\"\n    test_path = \"/kaggle/input/MABe-mouse-behavior-detection/test.csv\"\n    train_annotation_path = \"/kaggle/input/MABe-mouse-behavior-detection/train_annotation\"\n    train_tracking_path = \"/kaggle/input/MABe-mouse-behavior-detection/train_tracking\"\n    test_tracking_path = \"/kaggle/input/MABe-mouse-behavior-detection/test_tracking\"\n\n    model_name  = \"/kaggle/input/xgb-data-v1/lgb\"\n    model_name2 = \"/kaggle/input/xgb-data-v2/lgb\"\n    \n    mode = 'submit' # 'validate'/'check'/'submit'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T15:56:51.899178Z","iopub.execute_input":"2025-12-15T15:56:51.899598Z","iopub.status.idle":"2025-12-15T15:56:52.154075Z","shell.execute_reply.started":"2025-12-15T15:56:51.899578Z","shell.execute_reply":"2025-12-15T15:56:52.153505Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"XGB function definitions (mostly)","metadata":{}},{"cell_type":"code","source":"## Data loading and preprocessing\ntrain = pd.read_csv(CFG.train_path)\ntrain['n_mice'] = 4 - train[['mouse1_strain', 'mouse2_strain', 'mouse3_strain', 'mouse4_strain']].isna().sum(axis=1)\ntrain_without_mabe22 = train.query(\"~lab_id.str.startswith('MABe22_')\")\ntest = pd.read_csv(CFG.test_path)\nbody_parts_tracked_list = list(np.unique(train.body_parts_tracked))\n\nif CFG.mode != 'submit':\n    dfm = train.copy()\nelse:\n    dfm = test.copy()\n\n\n\n## Creating solution data\ndef create_solution_df(dataset):\n    solution = []\n    for _, row in dataset.iterrows():\n    \n        lab_id = row['lab_id']\n        if lab_id.startswith('MABe22'): \n            continue\n        \n        video_id = row['video_id']\n        path = f\"{CFG.train_annotation_path}/{lab_id}/{video_id}.parquet\"\n        try:\n            annot = pd.read_parquet(path)\n        except FileNotFoundError:\n            continue\n    \n        annot['lab_id'] = lab_id\n        annot['video_id'] = video_id\n        annot['behaviors_labeled'] = row['behaviors_labeled']\n        annot['target_id'] = np.where(annot.target_id != annot.agent_id, annot['target_id'].apply(lambda s: f\"mouse{s}\"), 'self')\n        annot['agent_id'] = annot['agent_id'].apply(lambda s: f\"mouse{s}\")\n        solution.append(annot)\n    \n    solution = pd.concat(solution)\n\n    # drop excluded actions\n    solution = solution.loc[solution['action'].map(lambda x: x not in ['ejaculate', 'dominancemount', 'genitalgroom', 'disengage'])]\n    return solution\n\nif CFG.mode != 'submit':\n    solution = create_solution_df(train_without_mabe22)\n\n\n\n\n\n## Data generator\ndrop_body_parts =  [\n    'headpiece_bottombackleft', 'headpiece_bottombackright', 'headpiece_bottomfrontleft', 'headpiece_bottomfrontright', \n    'headpiece_topbackleft', 'headpiece_topbackright', 'headpiece_topfrontleft', 'headpiece_topfrontright', \n    'spine_1', 'spine_2', 'tail_middle_1', 'tail_middle_2', 'tail_midpoint'\n]\n\n\ndef generate_mouse_data(dataset, traintest, traintest_directory, generate_single=True, generate_pair=True):\n    vid_count = 0\n    for _, row in dataset.iterrows():\n        lab_id = row.lab_id\n        if lab_id.startswith('MABe22') or type(row.behaviors_labeled) != str: \n            continue\n        if traintest == 'test' and lab_id in ['CRIM13', 'CalMS21_supplemental', 'CalMS21_task1', 'CalMS21_task2']: # skip excluded labs in test\n            continue\n        \n        video_id = row.video_id\n        path = f\"{traintest_directory}/{lab_id}/{video_id}.parquet\"\n        vid = pd.read_parquet(path) # this reads the data *************************************************************************************\n        if len(np.unique(vid.bodypart)) > 5:\n            vid = vid.query(\"~ bodypart.isin(@drop_body_parts)\")\n\n        pvid = vid.pivot(columns=['mouse_id', 'bodypart'], index='video_frame', values=['x', 'y'])\n\n        del vid\n        gc.collect()\n        \n        pvid = pvid.reorder_levels([1, 2, 0], axis=1).T.sort_index().T\n        pvid /= row.pix_per_cm_approx\n        \n        vid_behaviors = json.loads(row.behaviors_labeled)\n        vid_behaviors = sorted(list({b.replace(\"'\", \"\") for b in vid_behaviors}))\n        vid_behaviors = [b.split(',') for b in vid_behaviors]\n        vid_behaviors = pd.DataFrame(vid_behaviors, columns=['agent', 'target', 'action'])\n        \n        if traintest == 'train':\n            try:\n                annot = pd.read_parquet(path.replace('train_tracking', 'train_annotation'))\n            except FileNotFoundError:\n                continue\n\n        vid_count += 1\n        if generate_single:\n            vid_behaviors_subset = vid_behaviors.query(\"target == 'self'\")\n            skip = 0\n            if lab_id == 'AdaptableSnail' and row.frames_per_second == 25:\n                skip = 1\n                print('skipping bad singles') # print ****************************************************************\n            if len(vid_behaviors_subset) > 0 and skip == 0: # skip bad singles\n                for mouse_id_str in np.unique(vid_behaviors_subset.agent):\n                    try:\n                        mouse_id = int(mouse_id_str[-1])\n                        vid_agent_actions = np.unique(vid_behaviors_subset.query(\"agent == @mouse_id_str\").action)\n                        single_mouse = pvid.loc[:, mouse_id]\n                        assert len(single_mouse) == len(pvid)\n                        single_mouse_meta = pd.DataFrame({\n                            'lab_id': lab_id,\n                            'video_id': video_id,\n                            'agent_id': mouse_id_str,\n                            'target_id': 'self',\n                            'video_frame': single_mouse.index\n                        })\n                        if traintest == 'train':\n                            single_mouse_label = pd.DataFrame(0.0, columns=vid_agent_actions, index=single_mouse.index)\n                            annot_subset = annot.query(\"(agent_id == @mouse_id) & (target_id == @mouse_id)\")\n                            for i in range(len(annot_subset)):\n                                annot_row = annot_subset.iloc[i]\n                                single_mouse_label.loc[annot_row['start_frame']:annot_row['stop_frame'] - 1, annot_row.action] = 1.0\n                            yield 'single', single_mouse, single_mouse_meta, single_mouse_label\n                        else:\n                            yield 'single', single_mouse, single_mouse_meta, vid_agent_actions\n                    except KeyError:\n                        pass\n\n        if generate_pair:\n            vid_behaviors_subset = vid_behaviors.query(\"target != 'self'\")\n            skip = 0\n            if lab_id == 'AdaptableSnail' and row.frames_per_second == 25:\n                skip = 1\n                print('skipping bad pairs') # print ****************************************************************\n            if len(vid_behaviors_subset) > 0 and skip == 0: # skip bad pairs\n                for agent, target in itertools.permutations(np.unique(pvid.columns.get_level_values('mouse_id')), 2):\n                    agent_str = f\"mouse{agent}\"\n                    target_str = f\"mouse{target}\"\n                    vid_agent_actions = np.unique(vid_behaviors_subset.query(\"(agent == @agent_str) & (target == @target_str)\").action)\n                    mouse_pair = pd.concat([pvid[agent], pvid[target]], axis=1, keys=['A', 'B'])\n                    assert len(mouse_pair) == len(pvid)\n                    mouse_pair_meta = pd.DataFrame({\n                        'lab_id': lab_id,\n                        'video_id': video_id,\n                        'agent_id': agent_str,\n                        'target_id': target_str,\n                        'video_frame': mouse_pair.index\n                    })\n                    if traintest == 'train':\n                        mouse_pair_label = pd.DataFrame(0.0, columns=vid_agent_actions, index=mouse_pair.index)\n                        annot_subset = annot.query(\"(agent_id == @agent) & (target_id == @target)\")\n                        for i in range(len(annot_subset)):\n                            annot_row = annot_subset.iloc[i]\n                            mouse_pair_label.loc[annot_row['start_frame']:annot_row['stop_frame'] - 1, annot_row.action] = 1.0\n                        yield 'pair', mouse_pair, mouse_pair_meta, mouse_pair_label\n                    else:\n                        yield 'pair', mouse_pair, mouse_pair_meta, vid_agent_actions\n\n\n## Transforming coordinates\ndef safe_rolling(series, window, func, min_periods=None):\n    if min_periods is None:\n        min_periods = max(1, window // 4)\n    return series.rolling(window, min_periods=min_periods, center=True).apply(func, raw=True)\n\ndef _scale(n_frames_at_30fps, fps, ref=30.0):\n    return max(1, int(round(n_frames_at_30fps * float(fps) / ref)))\n\ndef _scale_signed(n_frames_at_30fps, fps, ref=30.0):\n    if n_frames_at_30fps == 0:\n        return 0\n    s = 1 if n_frames_at_30fps > 0 else -1\n    mag = max(1, int(round(abs(n_frames_at_30fps) * float(fps) / ref)))\n    return s * mag\n\ndef _fps_from_meta(meta_df, fallback_lookup, default_fps=30.0):\n    if 'frames_per_second' in meta_df.columns and pd.notnull(meta_df['frames_per_second']).any():\n        return float(meta_df['frames_per_second'].iloc[0])\n    vid = meta_df['video_id'].iloc[0]\n    return float(fallback_lookup.get(vid, default_fps))\n\ndef add_curvature_features(X, center_x, center_y, fps):\n    vel_x = center_x.diff()\n    vel_y = center_y.diff()\n    acc_x = vel_x.diff()\n    acc_y = vel_y.diff()\n\n    cross_prod = vel_x * acc_y - vel_y * acc_x\n    vel_mag = np.sqrt(vel_x**2 + vel_y**2)\n    curvature = np.abs(cross_prod) / (vel_mag**3 + 1e-6)\n\n    for w in [25, 50, 75]:\n        ws = _scale(w, fps)\n        X[f'curv_mean_{w}'] = curvature.rolling(ws, min_periods=max(1, ws // 5)).mean()\n\n    angle = np.arctan2(vel_y, vel_x)\n    angle_change = np.abs(angle.diff())\n    w = 30\n    ws = _scale(w, fps)\n    X[f'turn_rate_{w}'] = angle_change.rolling(ws, min_periods=max(1, ws // 5)).sum()\n\n    return X\n\ndef add_multiscale_features(X, center_x, center_y, fps):\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2) * float(fps)\n\n    scales = [20, 40, 60, 80]\n    for scale in scales:\n        ws = _scale(scale, fps)\n        if len(speed) >= ws:\n            X[f'sp_m{scale}'] = speed.rolling(ws, min_periods=max(1, ws // 4)).mean()\n            X[f'sp_s{scale}'] = speed.rolling(ws, min_periods=max(1, ws // 4)).std()\n\n    if len(scales) >= 2 and f'sp_m{scales[0]}' in X.columns and f'sp_m{scales[-1]}' in X.columns:\n        X['sp_ratio'] = X[f'sp_m{scales[0]}'] / (X[f'sp_m{scales[-1]}'] + 1e-6)\n\n    return X\n\ndef add_state_features(X, center_x, center_y, fps):\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2) * float(fps)\n    w_ma = _scale(15, fps)\n    speed_ma = speed.rolling(w_ma, min_periods=max(1, w_ma // 3)).mean()\n\n    try:\n        bins = [-np.inf, 0.5 * fps, 2.0 * fps, 5.0 * fps, np.inf]\n        speed_states = pd.cut(speed_ma, bins=bins, labels=[0, 1, 2, 3]).astype(float)\n\n        for window in [20, 40, 60, 80]:\n            ws = _scale(window, fps)\n            if len(speed_states) >= ws:\n                for state in [0, 1, 2, 3]:\n                    X[f's{state}_{window}'] = (\n                        (speed_states == state).astype(float)\n                        .rolling(ws, min_periods=max(1, ws // 5)).mean()\n                    )\n                state_changes = (speed_states != speed_states.shift(1)).astype(float)\n                X[f'trans_{window}'] = state_changes.rolling(ws, min_periods=max(1, ws // 5)).sum()\n    except Exception:\n        pass\n\n    return X\n\ndef add_longrange_features(X, center_x, center_y, fps):\n    for window in [30, 60, 120]:\n        ws = _scale(window, fps)\n        if len(center_x) >= ws:\n            X[f'x_ml{window}'] = center_x.rolling(ws, min_periods=max(5, ws // 6)).mean()\n            X[f'y_ml{window}'] = center_y.rolling(ws, min_periods=max(5, ws // 6)).mean()\n\n    for span in [30, 60, 120]:\n        s = _scale(span, fps)\n        X[f'x_e{span}'] = center_x.ewm(span=s, min_periods=1).mean()\n        X[f'y_e{span}'] = center_y.ewm(span=s, min_periods=1).mean()\n\n    speed = np.sqrt(center_x.diff()**2 + center_y.diff()**2) * float(fps)  # cm/s\n    for window in [30, 60, 120]:\n        ws = _scale(window, fps)\n        if len(speed) >= ws:\n            X[f'sp_pct{window}'] = speed.rolling(ws, min_periods=max(5, ws // 6)).rank(pct=True)\n\n    return X\n\ndef add_interaction_features(X, mouse_pair, fps, bp):\n    rel_x = mouse_pair['A'][bp]['x'] - mouse_pair['B'][bp]['x']\n    rel_y = mouse_pair['A'][bp]['y'] - mouse_pair['B'][bp]['y']\n    rel_dist = np.sqrt(rel_x**2 + rel_y**2)\n\n    A_vx = mouse_pair['A'][bp]['x'].diff()\n    A_vy = mouse_pair['A'][bp]['y'].diff()\n    B_vx = mouse_pair['B'][bp]['x'].diff()\n    B_vy = mouse_pair['B'][bp]['y'].diff()\n\n    A_lead = (A_vx * rel_x + A_vy * rel_y) / (np.sqrt(A_vx**2 + A_vy**2) * rel_dist + 1e-6)\n    B_lead = (B_vx * (-rel_x) + B_vy * (-rel_y)) / (np.sqrt(B_vx**2 + B_vy**2) * rel_dist + 1e-6)\n\n    for window in [30, 60]:\n        ws = _scale(window, fps)\n        X[f'A_ld{window}'] = A_lead.rolling(ws, min_periods=max(1, ws // 6)).mean()\n        X[f'B_ld{window}'] = B_lead.rolling(ws, min_periods=max(1, ws // 6)).mean()\n\n    approach = -rel_dist.diff()\n    chase = approach * B_lead\n    w = 30\n    ws = _scale(w, fps)\n    X[f'chase_{w}'] = chase.rolling(ws, min_periods=max(1, ws // 6)).mean()\n\n    for window in [60, 120]:\n        ws = _scale(window, fps)\n        A_sp = np.sqrt(A_vx**2 + A_vy**2)\n        B_sp = np.sqrt(B_vx**2 + B_vy**2)\n        X[f'sp_cor{window}'] = A_sp.rolling(ws, min_periods=max(1, ws // 6)).corr(B_sp)\n\n    return X\n\n\ndef roll_f(X, fps): # add rolled features\n    for dd in [3, 5, 10, 20]:\n        for c in ['1speed1', 'speed1', 'speed2', '12+nose+tail_base', '12+nose+body_center', '12+nose+nose', '12+neck+neck', '12+body_center+nose', 'lateral_left+lateral_right', 'ear_left+ear_right']:\n            if c in X.columns:\n                win = int(dd * fps / 30 + 0.5) # rolling window, adjusted for fps\n                X['roll_'+c+'_mi_'+str(dd)] = X[c].rolling(win, center=True, min_periods=1).min().astype('float32')\n                X['roll_'+c+'_ma_'+str(dd)] = X[c].rolling(win, center=True, min_periods=1).max().astype('float32')\n                X['roll_'+c+'_me_'+str(dd)] = X[c].rolling(win, center=True, min_periods=1).mean().astype('float32')\n    return X\n\n\n\n\n\ndef transform_single(single_mouse, body_parts_tracked, fps, section, meta): #************************************************************************\n    available_body_parts = single_mouse.columns.get_level_values(0)\n    \n    X = pd.DataFrame({\n        f\"{p1}+{p2}\": np.square(single_mouse[p1] - single_mouse[p2]).sum(axis=1, skipna=False)\n        for p1, p2 in itertools.combinations(body_parts_tracked, 2)\n        if p1 in available_body_parts and p2 in available_body_parts\n    })\n    X = X.reindex(columns=[f\"{p1}+{p2}\" for p1, p2 in itertools.combinations(body_parts_tracked, 2)], copy=False)\n\n    if all(p in single_mouse.columns for p in ['ear_left', 'ear_right', 'tail_base']):\n        lag = _scale(10, fps)\n        shifted = single_mouse[['ear_left', 'ear_right', 'tail_base']].shift(lag)\n        speeds = pd.DataFrame({\n            'sp_lf': np.square(single_mouse['ear_left'] - shifted['ear_left']).sum(axis=1, skipna=False),\n            'sp_rt': np.square(single_mouse['ear_right'] - shifted['ear_right']).sum(axis=1, skipna=False),\n            'sp_lf2': np.square(single_mouse['ear_left'] - shifted['tail_base']).sum(axis=1, skipna=False),\n            'sp_rt2': np.square(single_mouse['ear_right'] - shifted['tail_base']).sum(axis=1, skipna=False),\n        })\n        X = pd.concat([X, speeds], axis=1)\n\n    if 'nose+tail_base' in X.columns and 'ear_left+ear_right' in X.columns:\n        X['elong'] = X['nose+tail_base'] / (X['ear_left+ear_right'] + 1e-6)\n\n    if all(p in available_body_parts for p in ['nose', 'body_center', 'tail_base']):\n        v1 = single_mouse['nose'] - single_mouse['body_center']\n        v2 = single_mouse['tail_base'] - single_mouse['body_center']\n        X['body_ang'] = (v1['x'] * v2['x'] + v1['y'] * v2['y']) / (\n            np.sqrt(v1['x']**2 + v1['y']**2) * np.sqrt(v2['x']**2 + v2['y']**2) + 1e-6)\n\n    if 'body_center' in available_body_parts:\n        cx = single_mouse['body_center']['x']\n        cy = single_mouse['body_center']['y']\n\n        for w in [5, 15, 30, 60]:\n            ws = _scale(w, fps)\n            roll = dict(min_periods=1, center=True)\n            X[f'cx_m{w}'] = cx.rolling(ws, **roll).mean()\n            X[f'cy_m{w}'] = cy.rolling(ws, **roll).mean()\n            X[f'cx_s{w}'] = cx.rolling(ws, **roll).std()\n            X[f'cy_s{w}'] = cy.rolling(ws, **roll).std()\n            X[f'x_rng{w}'] = cx.rolling(ws, **roll).max() - cx.rolling(ws, **roll).min()\n            X[f'y_rng{w}'] = cy.rolling(ws, **roll).max() - cy.rolling(ws, **roll).min()\n            X[f'disp{w}'] = np.sqrt(cx.diff().rolling(ws, min_periods=1).sum()**2 +\n                                     cy.diff().rolling(ws, min_periods=1).sum()**2)\n            X[f'act{w}'] = np.sqrt(cx.diff().rolling(ws, min_periods=1).var() +\n                                   cy.diff().rolling(ws, min_periods=1).var())\n\n        X = add_curvature_features(X, cx, cy, fps)\n        X = add_multiscale_features(X, cx, cy, fps)\n        X = add_state_features(X, cx, cy, fps)\n        X = add_longrange_features(X, cx, cy, fps)\n\n    if all(p in available_body_parts for p in ['nose', 'tail_base']):\n        nt_dist = np.sqrt((single_mouse['nose']['x'] - single_mouse['tail_base']['x'])**2 +\n                          (single_mouse['nose']['y'] - single_mouse['tail_base']['y'])**2)\n        for lag in [10, 20, 40]:\n            l = _scale(lag, fps)\n            X[f'nt_lg{lag}'] = nt_dist.shift(l)\n            X[f'nt_df{lag}'] = nt_dist - nt_dist.shift(l)\n\n    if all(p in available_body_parts for p in ['ear_left', 'ear_right']):\n        ear_d = np.sqrt((single_mouse['ear_left']['x'] - single_mouse['ear_right']['x'])**2 +\n                        (single_mouse['ear_left']['y'] - single_mouse['ear_right']['y'])**2)\n        for off in [-30, -20, -10, 10, 20, 30]:\n            o = _scale_signed(off, fps)\n            X[f'ear_o{off}'] = ear_d.shift(-o)\n        w = _scale(30, fps)\n        X['ear_con'] = ear_d.rolling(w, min_periods=1, center=True).std() / \\\n                       (ear_d.rolling(w, min_periods=1, center=True).mean() + 1e-6)\n\n\n    # angle between speed and orientation\n    sx1 = single_mouse['tail_base']['x'].values[1:] - single_mouse['tail_base']['x'].values[:-1]\n    sy1 = single_mouse['tail_base']['y'].values[1:] - single_mouse['tail_base']['y'].values[:-1]\n    \n    # speed: assume already included? Add it anyway, JIC\n    X['1speed1'] = 0\n    X['1speed1'].iloc[1:] = np.sqrt(sx1**2 + sy1**2) * fps\n\n    # mix 'ear_left+nose' and ear_right+nose'\n    if 'ear_left+nose' in X.columns:\n        X['ear+nose_sum'] = X['ear_left+nose'] + X['ear_right+nose']\n        X['ear+nose_diff'] = X['ear_left+nose'] - X['ear_right+nose']\n        X.drop(['ear_left+nose','ear_right+nose'], axis=1, inplace=True)\n\n    # drop some features based on FI\n    drops = ['s2_20','s2_40','s2_60','s2_80','s3_20','s3_40','s3_60','s3_80']\n    drops = list(set(drops).intersection(set(X.columns)))\n    X.drop(drops, axis=1, inplace=True)\n\n    X = roll_f(X, fps) # add rolled features      \n    return X.astype(np.float32, copy=False)\n\n\n\n\n\ndef transform_pair(mouse_pair, body_parts_tracked, fps, section, meta): # ***************************************************************************\n    avail_A = mouse_pair['A'].columns.get_level_values(0)\n    avail_B = mouse_pair['B'].columns.get_level_values(0)\n\n    # drop both ears: [1]=111, -32\n    bp = body_parts_tracked.copy()\n    if len(bp) > 4: # do not drop ears if only 4 body parts - impacts section 7 only\n        bp.remove('ear_left')\n        bp.remove('ear_right')\n    X = pd.DataFrame({\n        f\"12+{p1}+{p2}\": np.square(mouse_pair['A'][p1] - mouse_pair['B'][p2]).sum(axis=1, skipna=False)\n        for p1, p2 in itertools.product(bp, repeat=2)\n        if p1 in avail_A and p2 in avail_B\n    })\n    X = X.reindex(columns=[f\"12+{p1}+{p2}\" for p1, p2 in itertools.product(bp, repeat=2)], copy=False)\n\n\n    if ('A', 'ear_left') in mouse_pair.columns and ('B', 'ear_left') in mouse_pair.columns:\n        lag = _scale(10, fps)\n        shA = mouse_pair['A']['ear_left'].shift(lag)\n        shB = mouse_pair['B']['ear_left'].shift(lag)\n        speeds = pd.DataFrame({\n            'sp_A': np.square(mouse_pair['A']['ear_left'] - shA).sum(axis=1, skipna=False),\n            'sp_AB': np.square(mouse_pair['A']['ear_left'] - shB).sum(axis=1, skipna=False),\n            'sp_B': np.square(mouse_pair['B']['ear_left'] - shB).sum(axis=1, skipna=False),\n        })\n        X = pd.concat([X, speeds], axis=1)\n\n    if all(p in avail_A for p in ['nose', 'tail_base']) and all(p in avail_B for p in ['nose', 'tail_base']):\n        dir_A = mouse_pair['A']['nose'] - mouse_pair['A']['tail_base']\n        dir_B = mouse_pair['B']['nose'] - mouse_pair['B']['tail_base']\n        X['rel_ori'] = (dir_A['x'] * dir_B['x'] + dir_A['y'] * dir_B['y']) / (\n            np.sqrt(dir_A['x']**2 + dir_A['y']**2) * np.sqrt(dir_B['x']**2 + dir_B['y']**2) + 1e-6)\n\n    if all(p in avail_A for p in ['nose']) and all(p in avail_B for p in ['nose']):\n        cur = np.square(mouse_pair['A']['nose'] - mouse_pair['B']['nose']).sum(axis=1, skipna=False)\n        lag = _scale(10, fps)\n        shA_n = mouse_pair['A']['nose'].shift(lag)\n        shB_n = mouse_pair['B']['nose'].shift(lag)\n        past = np.square(shA_n - shB_n).sum(axis=1, skipna=False)\n        X['appr'] = cur - past\n\n    if 'body_center' in avail_A and 'body_center' in avail_B: # body centers **********************************************************\n        bp = 'body_center'\n        cd = np.sqrt((mouse_pair['A'][bp]['x'] - mouse_pair['B'][bp]['x'])**2 +\n                     (mouse_pair['A'][bp]['y'] - mouse_pair['B'][bp]['y'])**2)\n        X['cd']   = cd.astype(float)\n        cd_full = np.square(mouse_pair['A'][bp] - mouse_pair['B'][bp]).sum(axis=1, skipna=False) # no sqrt here, dist**2\n        Axd = mouse_pair['A'][bp]['x'].diff()\n        Ayd = mouse_pair['A'][bp]['y'].diff()\n        Bxd = mouse_pair['B'][bp]['x'].diff()\n        Byd = mouse_pair['B'][bp]['y'].diff()\n        coord = Axd * Bxd + Ayd * Byd\n        for w in [5, 15, 30, 60]:\n            ws = _scale(w, fps)\n            roll = dict(min_periods=1, center=True)\n            X[f'd_m{w}']  = cd_full.rolling(ws, **roll).mean()\n            X[f'd_s{w}']  = cd_full.rolling(ws, **roll).std()\n            X[f'd_mn{w}'] = cd_full.rolling(ws, **roll).min()\n            X[f'd_mx{w}'] = cd_full.rolling(ws, **roll).max()\n\n            d_var = cd_full.rolling(ws, **roll).var()\n            X[f'int{w}'] = 1 / (1 + d_var)\n\n            X[f'co_m{w}'] = coord.rolling(ws, **roll).mean()\n            X[f'co_s{w}'] = coord.rolling(ws, **roll).std()\n\n\n    if 'nose' in avail_A and 'nose' in avail_B: # noses **********************************************************\n        nn = np.sqrt((mouse_pair['A']['nose']['x'] - mouse_pair['B']['nose']['x'])**2 +\n                     (mouse_pair['A']['nose']['y'] - mouse_pair['B']['nose']['y'])**2)\n        for lag in [10, 20, 40]:\n            l = _scale(lag, fps)\n            X[f'nn_lg{lag}']  = nn.shift(l)\n            X[f'nn_ch{lag}']  = nn - nn.shift(l)\n            is_cl = (nn < 10.0).astype(float)\n            X[f'cl_ps{lag}']  = is_cl.rolling(l, min_periods=1).mean()\n\n\n    if 'body_center' in avail_A and 'body_center' in avail_B: # body centers **********************************************************\n        val = (Axd * Bxd + Ayd * Byd) / (np.sqrt(Axd**2 + Ayd**2) * np.sqrt(Bxd**2 + Byd**2) + 1e-6) # cos of angle between speeds\n        for off in [-30, -20, -10, 0, 10, 20, 30]:\n            o = _scale_signed(off, fps)\n            X[f'va_{off}'] = val.shift(-o)\n        w = _scale(30, fps)\n        X['int_con'] = cd_full.rolling(w, min_periods=1, center=True).std() / \\\n                       (cd_full.rolling(w, min_periods=1, center=True).mean() + 1e-6)\n        X = add_interaction_features(X, mouse_pair, fps, bp)\n\n\n    # all have both ears and tail base\n    # almost all have nose\n    # cos/sin of angle between mice, based on ear-to-ear vectors\n    ox1 = mouse_pair['A']['ear_left']['x'].values - mouse_pair['A']['ear_right']['x'].values\n    ox2 = mouse_pair['B']['ear_left']['x'].values - mouse_pair['B']['ear_right']['x'].values\n    oy1 = mouse_pair['A']['ear_left']['y'].values - mouse_pair['A']['ear_right']['y'].values\n    oy2 = mouse_pair['B']['ear_left']['y'].values - mouse_pair['B']['ear_right']['y'].values\n    osize1 = np.sqrt(ox1**2 + oy1**2)\n    osize2 = np.sqrt(ox2**2 + oy2**2)\n\n    # features: angle of each mouse to vector between them\n    dx = mouse_pair['A']['tail_base']['x'].values - mouse_pair['B']['tail_base']['x'].values\n    dy = mouse_pair['A']['tail_base']['y'].values - mouse_pair['B']['tail_base']['y'].values\n\n    # speed\n    # base it on all BODY parts, not just tail_base\n    avail = set(mouse_pair['A'].columns.get_level_values(0)).intersection(set(mouse_pair['B'].columns.get_level_values(0)))\n    avail = list(avail.intersection(set(['hip_left','hip_right','lateral_left','lateral_right','tail_base','neck','body_center']))) # only use some body parts - 'body' only. No head or tail\n    v = np.array([mouse_pair['A'][p]['x'].values for p in avail]).mean(0)\n    sx1 = v[1:] - v[:-1]\n    v = np.array([mouse_pair['B'][p]['x'].values for p in avail]).mean(0)\n    sx2 = v[1:] - v[:-1]\n    v = np.array([mouse_pair['A'][p]['y'].values for p in avail]).mean(0)\n    sy1 = v[1:] - v[:-1]\n    v = np.array([mouse_pair['B'][p]['y'].values for p in avail]).mean(0)\n    sy2 = v[1:] - v[:-1]\n\n    # speed\n    X['speed1'] = 0\n    X['speed1'].iloc[1:] = np.sqrt(sx1**2 + sy1**2) * fps\n    X['speed2'] = 0\n    X['speed2'].iloc[1:] = np.sqrt(sx2**2 + sy2**2) * fps\n\n    # relative speed\n    X['rel_speed'] = 0\n    X['rel_speed'].iloc[1:] = np.sqrt((sx1 - sx2)**2 + (sy1 - sy2)**2) * fps\n\n    # cos s1 s2\n    X['cos_s1_s2'] = 1\n    X['cos_s1_s2'].iloc[1:] = (sx2 * sx1 + sy2 * sy1) / np.sqrt(sx2**2 + sy2**2) / np.sqrt(sx1**2 + sy1**2)\n\n    X = roll_f(X, fps) # add rolled features\n    return X.astype(np.float32, copy=False)\n\n\n## Training, validation and submission\ndef predict_multiclass(pred, meta, thresholds): # for 1 section only    \n    threshold_array = np.array([thresholds.get(col, 0.27) for col in pred.columns]) # for 1 section only, single vs pair. Turn into array by action.\n    \n    # v3: highest prob/th among actions with p>th\n    pred1 = np.concatenate((np.zeros([pred.shape[0], 1], dtype=np.int8), pred.values), axis=1)\n    th1 = np.concatenate((np.ones([1], dtype=np.int8), threshold_array))\n    ama = np.argmax(pred1 / th1.reshape(1, -1), axis=1)\n    max_proba = (pred1 / th1.reshape(1, -1)).max(axis=1)\n    ama = np.where(max_proba >= 1, ama - 1, -1) # only keep where it is over threshold; set to -1 otherwise\n\n    # use selected actions to form submission *********************\n    ama = pd.Series(ama, index=meta.video_frame) # make 'frame' the final output\n    changes_mask = (ama != ama.shift(1)).values\n    ama_changes = ama[changes_mask]\n    meta_changes = meta[changes_mask]\n    \n    mask = ama_changes.values >= 0 # pos means start of new action, ends are dropped\n    mask[-1] = False\n    \n    submission_part = pd.DataFrame({\n        'video_id': meta_changes['video_id'][mask].values,\n        'agent_id': meta_changes['agent_id'][mask].values,\n        'target_id': meta_changes['target_id'][mask].values,\n        'action': pred.columns[ama_changes[mask].values],\n        'start_frame': ama_changes.index[mask],\n        'stop_frame': ama_changes.index[1:][mask[:-1]]\n    })\n    \n    stop_video_id = meta_changes['video_id'][1:][mask[:-1]].values\n    stop_agent_id = meta_changes['agent_id'][1:][mask[:-1]].values\n    stop_target_id = meta_changes['target_id'][1:][mask[:-1]].values\n    for i in range(len(submission_part)):\n        video_id = submission_part.video_id.iloc[i]\n        agent_id = submission_part.agent_id.iloc[i]\n        target_id = submission_part.target_id.iloc[i]\n        if stop_video_id[i] != video_id or stop_agent_id[i] != agent_id or stop_target_id[i] != target_id:\n            new_stop_frame = meta.query(\"(video_id == @video_id)\").video_frame.max() + 1\n            submission_part.iat[i, submission_part.columns.get_loc('stop_frame')] = new_stop_frame\n\n    return submission_part\n\n\n# only used to create cat lab feature\nlab_di = {'AdaptableSnail': 0, 'BoisterousParrot': 1, 'CautiousGiraffe': 2, 'DeliriousFly': 3, 'ElegantMink': 4, 'GroovyShrew': 5\n          , 'InvincibleJellyfish': 6, 'JovialSwallow': 7, 'LyricalHare': 8, 'NiftyGoldfinch': 9, 'PleasantMeerkat': 10\n          , 'ReflectiveManatee': 11, 'SparklingTapir': 12, 'TranquilPanther': 13, 'UppityFerret': 14 # real labs: 0-14\n          , 'CRIM13': 15, 'CalMS21_supplemental': 16, 'CalMS21_task1': 16, 'CalMS21_task2': 16} # additional training labs only\n\n\ndef find_th(oof_action, y_action):# find best threshold\n    a = -oof_action.copy()  # predicted probability\n    b = y_action.copy()     # y\n    idx = np.argsort(a, order=None)\n    a = -a[idx]\n    b = b[idx]\n    b = b.cumsum()\n    f = 2 * b / (b[-1] + np.arange(b.shape[0]))\n    j = f.argmax()\n    th = np.round(a[j], 3)\n    f = f1_score(y_action, (oof_action >= th), zero_division=0)\n    return th, f, b[j], b[-1], j # threshold, F1, TP, YC, YPC\n\n\ndef FE(X, meta, sp, section):\n    m_di = {'mouse1':0, 'mouse2':1, 'mouse3':2, 'mouse4':3}\n\n    # round to 5 digits\n    X = np.round(X, 5)\n\n    # add lab. make it cat. Helps a lot.\n    if section in [4, 5, 6, 8]: # only these sections have >1 lab\n        X['l'] = meta['lab_id'].map(lab_di).astype('int8').values\n        if section == 8: # only section 8 has > 2 labs\n            X['l'] = X['l'].astype('category')\n\n    return X\n\n\n\ndef f1f(labels, preds): # early stopping functon\n    preds = preds.astype(np.float32)\n    labels = labels.astype(np.int8)\n    f1, yc = 0, labels.sum()\n    for i in range(1, 50): # test all th in .01 incr from .01 to .49. Best s/b 0.28\n        p = (preds > i / 100).astype(np.int8)\n        f1 = max(f1, 2 * (p * labels).sum() / (p.sum() + yc))\n    return -f1\n\n# XGB paramters ************************************************\nn_estimators        = 250 * 4   # make it high enough for ES\nlearning_rate       = 0.08\nmax_depth           = 8\nmin_child_weight    = 15\ncolsample_bytree    = 0.6\nsubsample           = 0.8\nmax_bin             = 32\nscale_pos_weight    = 1.1\n\nfi_sum, fi_cnt = {}, {}\nnames = ['a','b','c','d','e','f','g','h','i','j']\ndef cross_validate_classifier(sp, X, label, meta, section):\n    submission_list, ths, ths2 = [], {}, {}\n    \n    # drop actions with no targets: happens for section 5 (sniffgenital), 7 (intromit, mount), 2 (sniff, intromit, mount, rear, selfgroom)\n    # for section 8, drop approach and sniffface - they only appear in excluded labs!\n    for c in label.columns:\n        if label[c].max() < 1 or c in ['ejaculate', 'dominancemount', 'genitalgroom', 'disengage'] or (section == 8 and c in ['approach', 'sniffface']):\n            label.drop(c, axis=1, inplace=True)\n            \n    # drop records with no mask: section 5 (18K records), 2 (314K records)\n    keep = (label.reset_index(drop=True).fillna(-1).max(1) > -.5).values\n    if keep.sum() < len(keep):\n        meta  = meta.loc[keep].reset_index(drop=True)\n        label = label.loc[keep]\n        X     = X.loc[keep]\n\n    oof = pd.DataFrame(index=meta.video_frame)\n    X = FE(X, meta, sp, section)\n    actions = ['attack','sniff','escape','shepherd','chase','sniffgenital','rear','mount','defend','reciprocalsniff','selfgroom','approach','intromit'\n               ,'dominance','huddle','follow','allogroom','dig','attemptmount','sniffbody','avoid','submit','chaseattack','freeze','climb'\n               ,'dominancegroom','rest','run','sniffface','flinch','exploreobject','biteobject','tussle']\n    for action in [a for a in actions if a in label.columns]: # order by decreasing importance ****\n        action_mask = ~ label[action].isna().values\n        y_action = label[action][action_mask].values.astype(np.int8)\n        X_action = X[action_mask]\n        groups_action = meta.video_id[action_mask]\n        oof_action = np.zeros(X_action.shape[0], dtype=np.float32)\n        X_action = X_action.reset_index(drop=True)\n        groups_action = groups_action.values\n        y1 = y_action + 2 * meta['lab_id'].map(lab_di)[action_mask]\n        if CFG.mode == 'validate': # training code ******************************************************************************************\n            if str(section) + action in best_features: # drop non-selected features\n                cols = best_features[str(section) + action]\n            else:\n                cols = X_action.columns\n            X_action = X_action[cols].copy() # break the link and select columns\n            kf = StratifiedGroupKFold(n_splits=folds, shuffle=False)\n            for fold, (tr_ind, va_ind) in enumerate(kf.split(X_action, y1, groups_action)): # groups by video\n                model = XGBClassifier(objective='binary:logistic', verbosity=0, random_state=seed\n                    , n_estimators=n_estimators, learning_rate=learning_rate, max_depth=max_depth\n                    , min_child_weight=min_child_weight, subsample=subsample, colsample_bytree=colsample_bytree\n                    , max_bin=max_bin, scale_pos_weight=scale_pos_weight\n                    , n_jobs=16, device='cuda', tree_method='hist', enable_categorical=True\n                    , early_stopping_rounds=10, eval_metric=f1f)\n                model = model.fit(X_action.loc[tr_ind], y_action[tr_ind], eval_set=[(X_action.loc[va_ind], y_action[va_ind])], verbose=False)\n                oof_action[va_ind] = model.predict_proba(X_action.loc[va_ind])[:,1]\n                model.save_model(CFG.model_name + '/' + str(section) + action + 'xgbmodel' + names[fold] + '.json')  # Saves in JSON format\n                del model\n                gc.collect()\n            # tune smoothing per section/action\n            f1b = 0\n            for wl in [0, 5, 10, 20, 30, 35, 40, 50, 70, 100, 150, 250]:\n                if wl > 0:\n                    oa = savgol_filter(oof_action, window_length=wl, polyorder=1, axis=0) # smooth prediction ******************************\n                else:\n                    oa = oof_action\n                threshold, f1, yp, yc, ypc = find_th(oa, y_action) # only place where this is called\n                if f1 > f1b:\n                    f1b, thresholdb, wlb, ypb, ycb, ypcb = f1, threshold, wl, yp, yc, ypc\n                if wl >= 70 and wlb < wl: # break if score stops increasing\n                    break\n            ths[action], ths2[action] = thresholdb, wlb # save th, wl\n            if wlb > 0:\n                oof_action = savgol_filter(oof_action, window_length=wlb, polyorder=1, axis=0) # save pred\n            if verbose: print('    endfit', int(time()-t0), section, action, np.round(f1b, 3), np.round(thresholdb, 3), wlb, y_action.sum(), X_action.shape[0], X_action.shape[1])\n        else: # prediction code - check only, submit is done elsewhere ***********************************************************************\n            kf = StratifiedGroupKFold(n_splits=folds, shuffle=False)\n            for fold, (tr_ind, va_ind) in enumerate(kf.split(X_action, y1, groups_action)): # groups by video\n                model = XGBClassifier()\n                model.load_model(CFG.model_name + '/' + str(section) + action + 'xgbmodel' + names[fold] + '.json')\n                oof_action[va_ind] = model.predict_proba(X_action.loc[va_ind])[:,1]\n                del model\n                gc.collect()\n            wl = thresholds2[sp][str(section)][action]\n            threshold = thresholds[sp][str(section)][action]\n            if wl > 0:\n                oof_action = savgol_filter(oof_action, window_length=wl, polyorder=1, axis=0) # smooth prediction ************************\n            f1 = f1_score(y_action, (oof_action >= threshold), zero_division=0)\n            print('    end predict', int(time()-t0), 'sec, section/action/Fa', section, action, np.round(f1, 3), X_action.shape[0], X_action.shape[1])\n        \n        oof_column = np.zeros(len(label))\n        oof_column[action_mask] = oof_action # use this for final pred (smoothed)\n        oof[action] = oof_column\n        del oof_action, action_mask, X_action, y_action, groups_action\n        gc.collect()\n\n    if CFG.mode != 'validate':\n        ths = thresholds[sp][str(section)] # need this - it is used in constructing the final prediction!\n    submission_part = predict_multiclass(oof, meta, ths) # this creates the submission ***************\n    submission_list.append(submission_part)\n    return submission_list, ths, ths2, oof, meta, label # return oof+meta+label, JIC\n\n\n\nif CFG.mode == \"validate\":\n    thresholds = {\"single\": {}, \"pair\": {}}\n    thresholds2 = {\"single\": {}, \"pair\": {}}\n    best_features = joblib.load(f\"{CFG.model_name}/best_features.pkl\")\nelse:\n    thresholds = joblib.load(f\"{CFG.model_name}/thresholds.pkl\") # th- not used for blended submission\n    best_features = joblib.load(f\"{CFG.model_name}/best_features.pkl\")\n    mp = joblib.load(f\"{CFG.model_name}/mp_di2.pkl\") # by lab and action - not used for blended submission\n    thresholds2  = joblib.load(f\"{CFG.model_name}/thresholds2.pkl\") # smoothing period - use different ones for different models\n    thresholds2b = joblib.load(f\"{CFG.model_name2}/thresholds2.pkl\")\n\n\n\ndef submit(body_parts_tracked_str, switch_tr, section, thresholds, thresholds2, thresholds2b):\n    body_parts_tracked = json.loads(body_parts_tracked_str)\n    if len(body_parts_tracked) > 5:\n        body_parts_tracked = [b for b in body_parts_tracked if b not in drop_body_parts]\n\n    test_subset = test[test.body_parts_tracked == body_parts_tracked_str]\n    generator = generate_mouse_data(\n        test_subset, \n        'test',\n        CFG.test_tracking_path,\n        generate_single=(switch_tr == 'single'), \n        generate_pair=(switch_tr == 'pair')\n    )\n\n    fps_lookup = (\n        test_subset[['video_id', 'frames_per_second']]\n        .drop_duplicates('video_id')\n        .set_index('video_id')['frames_per_second']\n        .to_dict()\n    )\n\n    submission_list, ooft, metat = [], [], []\n    for switch, data, meta, actions in generator:\n        if len(actions) == 0: # nothing to predict, skip. Need this with new FE logic\n            continue\n        fps = _fps_from_meta(meta, fps_lookup, default_fps=30.0)\n        if switch == 'single':\n            X = transform_single(data, body_parts_tracked, fps, section, meta)\n        else:\n            X = transform_pair(data, body_parts_tracked, fps, section, meta)\n\n        X = FE(X, meta, switch, section)\n        pred = pd.DataFrame(index=meta.video_frame)\n        for action in actions:\n            # drop excluded actions - only need to do this for train submission, since these are not in real test data (confirmed)\n            if action in ['ejaculate', 'dominancemount', 'genitalgroom', 'disengage']:\n                continue\n            if str(section) + action in best_features: # drop non-selected features\n                cols = best_features[str(section) + action]\n            else:\n                cols = X.columns\n            pred[action] = np.zeros(X.shape[0], dtype=np.float32)\n\n            # first XGB prediction\n            if os.path.isfile(CFG.model_name + '/' + str(section) + action + 'xgbmodela.json'):\n                for fold in range(folds):\n                    model = XGBClassifier(device='cuda') # does this work? Don't think so.\n                    model.load_model(CFG.model_name + '/' + str(section) + action + 'xgbmodel' + names[fold] + '.json')\n                    model.set_params(device='cuda') # or this?\n                    pred[action] += model.predict_proba(X[cols])[:,1] / folds # average\n                    del model\n                    gc.collect()\n                wl = thresholds2[action]\n                if wl > 0:\n                    pred[action] = savgol_filter(pred[action], window_length=wl, polyorder=1, axis=0) # smooth prediction ********************************\n            \n            # second XGB prediction\n            #if os.path.isfile(CFG.model_name2 + '/' + str(section) + action + 'xgbmodela.json'):\n            #    p2 = np.zeros(X.shape[0], dtype=np.float32)\n            #    for fold in range(folds):\n            #        model = XGBClassifier(device='cuda') # does this work? Don't think so.\n            #        model.load_model(CFG.model_name2 + '/' + str(section) + action + 'xgbmodel' + names[fold] + '.json')\n            #        model.set_params(device='cuda') # or this?\n            #        p2 += model.predict_proba(X[cols])[:,1] / folds # average\n            #        del model\n            #        gc.collect()\n            #    wl = thresholds2b[action]\n            #    if wl > 0:\n            #        p2 = savgol_filter(p2, window_length=wl, polyorder=1, axis=0) # smooth prediction ********************************\n            #    # blend the 2 XGB preds\n            #    pred[action] = pred[action].values * 0.5 + p2 * 0.5 # 50/50 should do it\n                    \n        if pred.shape[1] != 0:\n            mp2 = mp[meta['lab_id'].iloc[0]]\n            submission_part = predict_multiclass(pred, meta, mp2)\n            submission_list.append(submission_part)\n            ooft.append(pred)\n            metat.append(meta)\n    return submission_list, ooft, metat\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T15:56:52.154958Z","iopub.execute_input":"2025-12-15T15:56:52.155174Z","iopub.status.idle":"2025-12-15T15:56:52.406226Z","shell.execute_reply.started":"2025-12-15T15:56:52.155156Z","shell.execute_reply":"2025-12-15T15:56:52.405658Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"XGB code","metadata":{}},{"cell_type":"code","source":"\nsubmission_list, oofs, metas, labels = [], [], [], []\nfor section in [8, 6, 3, 4, 1, 9, 5, 7, 2]: # order sections by decreasing importance\n        body_parts_tracked_str = body_parts_tracked_list[section]\n        body_parts_tracked = json.loads(body_parts_tracked_str)\n        if len(body_parts_tracked) > 5:\n            body_parts_tracked = [b for b in body_parts_tracked if b not in drop_body_parts]\n        if verbose: print('section', section)\n\n        if CFG.mode == \"submit\": # submit logic ***************************************************************************************\n            print(int(time()-t0), 'sec')\n            if f\"{section}\" in thresholds[\"single\"].keys(): # skip single actions for some sections\n                temp_submission_list, oof, meta = submit(body_parts_tracked_str, 'single', section, thresholds[\"single\"][f\"{section}\"], thresholds2[\"single\"][f\"{section}\"], thresholds2b[\"single\"][f\"{section}\"])\n                submission_list.extend(temp_submission_list)\n                oofs.extend(oof)\n                metas.extend(meta)\n            temp_submission_list, oof, meta = submit(body_parts_tracked_str, 'pair', section, thresholds[\"pair\"][f\"{section}\"], thresholds2[\"pair\"][f\"{section}\"], thresholds2b[\"pair\"][f\"{section}\"])\n            submission_list.extend(temp_submission_list)\n            oofs.extend(oof)\n            metas.extend(meta)\n            continue\n\n        # the rest is validate/check logic only *****************\n        single_mouse_list, single_mouse_label_list, single_mouse_meta_list = [], [], []\n        mouse_pair_list, mouse_pair_label_list, mouse_pair_meta_list = [], [], []\n        \n        train_subset = train[train.body_parts_tracked == body_parts_tracked_str]\n        genmode = 'train'\n            \n        _fps_lookup = (\n            train_subset[['video_id', 'frames_per_second']]\n            .drop_duplicates('video_id')\n            .set_index('video_id')['frames_per_second']\n            .to_dict()\n        )\n        for switch, data, meta, label in generate_mouse_data(train_subset, genmode, CFG.train_tracking_path):\n            # drop excluded actions; keep videos with no targets, drop them only later!!\n            for c in label.columns:\n                if c in ['ejaculate', 'dominancemount', 'genitalgroom', 'disengage']:\n                    label.drop(c, axis=1, inplace=True)\n                    \n            # drop records with no mask\n            keep = (label.reset_index(drop=True).fillna(-1).max(1) > -.5).values\n            if keep.sum() < len(keep):\n                if keep.sum() == 0: # skip empty outputs\n                    continue\n                meta  = meta.loc[keep].reset_index(drop=True)\n                data  = data.loc[keep]\n                label = label.loc[keep]\n            \n            if switch == 'single':\n                single_mouse_list.append(data)\n                single_mouse_meta_list.append(meta)\n                single_mouse_label_list.append(label)\n            else:\n                mouse_pair_list.append(data)\n                mouse_pair_meta_list.append(meta)\n                mouse_pair_label_list.append(label)\n        del data, meta, label\n        gc.collect()\n\n        if len(mouse_pair_list) > 0: # pair *********************************\n            pair_feats_parts = []\n            for mouse_pair, meta in zip(mouse_pair_list, mouse_pair_meta_list):\n                fps = _fps_from_meta(meta, _fps_lookup, default_fps=30.0)\n                X = transform_pair(mouse_pair, body_parts_tracked, fps, section, meta).astype(np.float32)\n                pair_feats_parts.append(X)\n           \n            X = pd.concat(pair_feats_parts, axis=0, ignore_index=True)\n            label = pd.concat(mouse_pair_label_list, axis=0, ignore_index=True)\n            meta = pd.concat(mouse_pair_meta_list, axis=0, ignore_index=True)\n            del pair_feats_parts, mouse_pair_list, mouse_pair_label_list, mouse_pair_meta_list\n            gc.collect()\n\n            temp_submission_list, temp_thresholds, temp_thresholds2, oof, meta, label = cross_validate_classifier('pair', X, label, meta, section)\n                                    \n            if CFG.mode == 'validate':\n                if f\"{section}\" not in thresholds[\"pair\"].keys():\n                    thresholds[\"pair\"][f\"{section}\"] = {}\n                if f\"{section}\" not in thresholds2[\"pair\"].keys():\n                    thresholds2[\"pair\"][f\"{section}\"] = {}\n                for k, v in temp_thresholds.items():\n                    thresholds[\"pair\"][f\"{section}\"][k] = v\n                for k, v in temp_thresholds2.items():\n                    thresholds2[\"pair\"][f\"{section}\"][k] = v\n                \n            submission_list.extend(temp_submission_list)\n            oofs.append(oof)\n            metas.append(meta)\n            labels.append(label)\n            del temp_submission_list, temp_thresholds, temp_thresholds2, X, oof, meta, label\n            gc.collect()\n\n        if len(single_mouse_list) > 0: # single *****************************************************\n            single_feats_parts = []\n            for single_mouse, meta in zip(single_mouse_list, single_mouse_meta_list):\n                fps = _fps_from_meta(meta, _fps_lookup, default_fps=30.0)\n                X = transform_single(single_mouse, body_parts_tracked, fps, section, meta).astype(np.float32)\n                single_feats_parts.append(X)\n            \n            X = pd.concat(single_feats_parts, axis=0, ignore_index=True)\n            label = pd.concat(single_mouse_label_list, axis=0, ignore_index=True)\n            meta = pd.concat(single_mouse_meta_list, axis=0, ignore_index=True)\n            del single_feats_parts, single_mouse_list, single_mouse_label_list, single_mouse_meta_list\n            gc.collect()\n            \n            temp_submission_list, temp_thresholds, temp_thresholds2, oof, meta, label = cross_validate_classifier('single', X, label, meta, section)\n            \n            if CFG.mode == 'validate':\n                if f\"{section}\" not in thresholds[\"single\"].keys():\n                    thresholds[\"single\"][f\"{section}\"] = {}\n                if f\"{section}\" not in thresholds2[\"single\"].keys():\n                    thresholds2[\"single\"][f\"{section}\"] = {}\n                for k, v in temp_thresholds.items():\n                    thresholds[\"single\"][f\"{section}\"][k] = v\n                for k, v in temp_thresholds2.items():\n                    thresholds2[\"single\"][f\"{section}\"][k] = v                 \n            \n            submission_list.extend(temp_submission_list)\n            oofs.append(oof)\n            metas.append(meta)\n            labels.append(label)\n            del temp_submission_list, temp_thresholds, temp_thresholds2, X, oof, meta, label\n            gc.collect()\n\n# Validate/check\nif CFG.mode != 'submit':\n    if CFG.mode == 'validate':\n        joblib.dump(thresholds, f\"{CFG.model_name}/thresholds.pkl\") # these are used for prediction\n        joblib.dump(thresholds2, f\"{CFG.model_name}/thresholds2.pkl\") # these are used for prediction\n    submission = pd.concat(submission_list)\n\n    s = np.round(score(solution.loc[(solution['lab_id'].str[:8] != 'CalMS21_') & (solution['lab_id'].str[:6] != 'CRIM13')], submission, ''), 4)    \n    if verbose: print('score for selected labs:', s)\n    if verbose: print(submission.shape, solution.shape)\n\n\n# Submission\nif CFG.mode == 'submit':\n    # construct prediction from totals\n    oofs = pd.concat(oofs, axis=0, ignore_index=True).astype('float32').reset_index(drop=True)\n    metas = pd.concat(metas, axis=0, ignore_index=True).reset_index(drop=True)\n    metas['lab_id'] = metas['lab_id'].map(lab_di).astype('int8')\n    idx = metas['target_id'] == 'self'\n    metas['target_id'].loc[idx] = metas['agent_id'].loc[idx]\n    metas['target_id'] = metas['target_id'].map({'mouse1':0, 'mouse2':1, 'mouse3':2, 'mouse4':3}).astype('int8')\n    metas['agent_id']  =  metas['agent_id'].map({'mouse1':0, 'mouse2':1, 'mouse3':2, 'mouse4':3}).astype('int8')\n    \n    df = pd.concat([metas, oofs], axis=1).reset_index(drop=True)\n    del metas, oofs\n    gc.collect()\n    \n    print(df.columns)\n    print(df.shape)\n    print(df.dtypes)\n    print('finished XGB pred')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T15:56:52.406970Z","iopub.execute_input":"2025-12-15T15:56:52.407158Z","iopub.status.idle":"2025-12-15T15:58:53.206410Z","shell.execute_reply.started":"2025-12-15T15:56:52.407143Z","shell.execute_reply":"2025-12-15T15:58:53.205804Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Now, blend it","metadata":{}},{"cell_type":"code","source":"mp = joblib.load(model_dir + 'mp_blend_tuned.pkl') # use blended tuned mp - no adjustment needed!\nmp3 = joblib.load(model_dir + 'mp3.pkl')\n\n\n\n\n\n# format xgb prediciton, blend it with NN ************************************************************\nid_di3 = {}\nfor k in id_di.keys(): # id\n    id_di3[id_di[k][1]] = k\ndf['id'] = df['video_id'].map(id_di3).fillna(-1).astype('int16')\ndf['mid'] = df['agent_id'] * 10 + df['target_id']\ndf.drop(['lab_id', 'video_id','agent_id','target_id'], axis=1, inplace=True)\ncols_yp = [action_di2[i] for i in range(33) if action_di2[i] in df.columns]\n\ndf3 = df3.iloc[:yp.shape[0]] # trim\ndf3['l'] = df3['id'].map(id_di).str[2].values.astype(np.int8) # need this!\nlab = df3['l']\n\n# merge, left\ndf = df3.merge(df, how='left', on=['id', 'mid', 'video_frame'])\n\n# fill-in with yp\n# ypx0 is missing 300K records, for which actual target is always zero.\n# ypx0 has 300K extra records - mostly frames with some missing body parts\nypx0 = yp.copy() # same shape now\nfor c in cols_yp:\n    i = action_di[c]\n    idx = df[c].isna()\n    df[c].loc[idx] = yp[idx,i] # complete df - fill missing values with yp\n    ypx0[:,i] = df[c].values.astype(np.float32) # put df into ypx0\ndel df\ngc.collect()\nprint('ypx0', int(time() - t0), 'sec')\n\n\n\n\n\n# blend NN and XGB *****************\nw = 0.57\nyp_blended = yp * (1 - w) + ypx0 * w # blend\n\n\n\n\n\n# transform yp_blended to make it in blocks of n for lab=1.\nn = 5 # Good results for n=5, 13, 21. Use 5 here.\ndft = df3.loc[df3['l']==1].reset_index(drop=True) # select lab 1 only\nif dft.shape[0] > 0:\n    dft['block'] = dft['video_frame']//n\n    dft['p'] = yp_blended[df3['l']==1, 7] # only action=7 for lab=1\n    b = dft.groupby(['id','mid','block'])['p'].max().reset_index() # max is better than mean\n    dft = dft.merge(b[['id', 'mid', 'block', 'p']], on=['id','mid','block'], how='left') # adds 'p_y'\n    yp_blended[df3['l']==1, 7] = dft['p_y'].fillna(0).values.astype(np.float32)\n    mp[1,7] = 0.2942 # need to change the threashold\n\n\n\n\n\n# construct the submission **************\ndel yp, ypx0\ngc.collect()\nsub = prepare_sub(yp_blended, df3, mp, mp3, sm=3, sm1=3)\n\n\n\n\n\n# save submission\nsub.to_csv('submission.csv', index=False)\nprint('done', sub.shape, int(time() - t0), 'sec')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T15:58:53.207212Z","iopub.execute_input":"2025-12-15T15:58:53.207872Z","iopub.status.idle":"2025-12-15T15:58:55.230776Z","shell.execute_reply.started":"2025-12-15T15:58:53.207841Z","shell.execute_reply":"2025-12-15T15:58:55.230036Z"}},"outputs":[],"execution_count":null}]}