{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59156,"databundleVersionId":13874099,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nfrom tqdm.auto import tqdm\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import accuracy_score, classification_report\nfrom sklearn.utils import resample\n\n# --- 1. CẤU HÌNH ---\nDATA_PATH = '/kaggle/input/MABe-mouse-behavior-detection/'\nNUM_VIDEOS_TO_TRAIN = 20 # Số lượng video dùng để train (Càng nhiều càng tốt)\n\n# Đọc metadata\ntry:\n    df_train_meta = pd.read_csv(os.path.join(DATA_PATH, 'train.csv'))\nexcept FileNotFoundError:\n    print(\"⚠️ Lỗi: Không tìm thấy file train.csv\")\n\n# --- 2. HÀM TẠO ĐẶC TRƯNG (FEATURE ENGINEERING) ---\ndef calculate_features_with_memory(df):\n    # a. Tính Vật lý (Distance & Velocity)\n    try:\n        dx = df['mouse1_body_center_x'] - df['mouse2_body_center_x']\n        dy = df['mouse1_body_center_y'] - df['mouse2_body_center_y']\n        df['distance'] = np.sqrt(dx**2 + dy**2)\n    except KeyError:\n        df['distance'] = 0 \n        \n    vx = df['mouse1_body_center_x'].diff().fillna(0)\n    vy = df['mouse1_body_center_y'].diff().fillna(0)\n    df['velocity_m1'] = np.sqrt(vx**2 + vy**2)\n    \n    try:\n        vx2 = df['mouse2_body_center_x'].diff().fillna(0)\n        vy2 = df['mouse2_body_center_y'].diff().fillna(0)\n        df['velocity_m2'] = np.sqrt(vx2**2 + vy2**2)\n    except KeyError:\n        df['velocity_m2'] = 0\n        \n    # b. Tạo Ký ức (Rolling Window - 10 frames)\n    w = 10\n    df['dist_mean_10'] = df['distance'].rolling(window=w).mean().fillna(0)\n    df['dist_std_10'] = df['distance'].rolling(window=w).std().fillna(0)\n    df['vel1_mean_10'] = df['velocity_m1'].rolling(window=w).mean().fillna(0)\n    df['vel2_mean_10'] = df['velocity_m2'].rolling(window=w).mean().fillna(0)\n    \n    return df\n\n# --- 3. HÀM LOAD & LỌC DỮ LIỆU (CHỈ LẤY CẶP 1-2 ĐỂ TRAIN) ---\ndef get_train_data(idx):\n    row = df_train_meta.iloc[idx]\n    lab_id, video_id = row['lab_id'], row['video_id']\n    pix_per_cm = row['pix_per_cm_approx'] if row['pix_per_cm_approx'] > 0 else 1.0\n    \n    # Load Tracking\n    t_path = os.path.join(DATA_PATH, 'train_tracking', lab_id, f'{video_id}.parquet')\n    a_path = os.path.join(DATA_PATH, 'train_annotation', lab_id, f'{video_id}.parquet')\n    \n    try:\n        df_track = pd.read_parquet(t_path)\n    except FileNotFoundError: return None\n\n    # Pivot & Chuẩn hóa CM\n    px = df_track.pivot(index='video_frame', columns=['mouse_id', 'bodypart'], values='x')\n    px.columns = [f\"mouse{m}_{bp}_x\" for m, bp in px.columns]\n    py = df_track.pivot(index='video_frame', columns=['mouse_id', 'bodypart'], values='y')\n    py.columns = [f\"mouse{m}_{bp}_y\" for m, bp in py.columns]\n    df_wide = pd.concat([px, py], axis=1).sort_index(axis=1)\n    df_wide = df_wide / pix_per_cm \n\n    # Load Annotation & Lọc chỉ lấy tương tác cặp 1-2\n    try:\n        df_annot = pd.read_parquet(a_path)\n        df_wide['label'] = 'other'\n        # Chỉ lấy dòng có agent=1, target=2 HOẶC agent=2, target=1\n        mask = ((df_annot['agent_id'] == 1) & (df_annot['target_id'] == 2)) | \\\n               ((df_annot['agent_id'] == 2) & (df_annot['target_id'] == 1))\n        pair_annot = df_annot[mask]\n        \n        for _, r in pair_annot.iterrows():\n            if r['stop_frame'] <= df_wide.index.max():\n                df_wide.loc[r['start_frame']:r['stop_frame'], 'label'] = r['action']\n    except:\n        return None # Bỏ qua video lỗi nhãn\n\n    return df_wide.fillna(0)\n\n# --- 4. PIPELINE CHÍNH: GỘP DATA -> CÂN BẰNG -> TRAIN ---\nfeatures = ['distance', 'velocity_m1', 'velocity_m2', \n            'dist_mean_10', 'dist_std_10', 'vel1_mean_10', 'vel2_mean_10']\n\n# A. Gộp dữ liệu nhiều video\nall_data = []\nprint(f\"⏳ Đang xử lý {NUM_VIDEOS_TO_TRAIN} video...\")\nfor i in tqdm(range(NUM_VIDEOS_TO_TRAIN)):\n    df = get_train_data(i)\n    if df is not None and len(df) > 0:\n        df = calculate_features_with_memory(df)\n        all_data.append(df[features + ['label']])\n\ndf_big_train = pd.concat(all_data, ignore_index=True)\nprint(f\"✅ Kích thước tập Train thô: {df_big_train.shape}\")\n\n# B. Cân bằng dữ liệu (Undersampling an toàn)\nprint(\"⚖️ Đang cân bằng dữ liệu...\")\nothers = df_big_train[df_big_train['label'] == 'other']\nactions = df_big_train[df_big_train['label'] != 'other']\n\nmin_sample = min(len(others), len(actions))\ndf_bal = pd.concat([\n    resample(others, replace=False, n_samples=min_sample, random_state=42),\n    resample(actions, replace=False, n_samples=min_sample, random_state=42)\n])\nprint(f\"✅ Dữ liệu sau cân bằng: {df_bal.shape} (Mỗi phe {min_sample} dòng)\")\n\n# C. Train Model\nprint(\"🚀 Đang huấn luyện Random Forest...\")\nle = LabelEncoder()\ny_train = le.fit_transform(df_bal['label'])\nX_train = df_bal[features]\n\nmodel_big = RandomForestClassifier(n_estimators=50, random_state=42, n_jobs=-1)\nmodel_big.fit(X_train, y_train)\nprint(\"🎉 Đã huấn luyện xong model_big!\")\nprint(f\"Các nhãn đã học: {le.classes_}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-26T08:43:32.198990Z","iopub.execute_input":"2025-11-26T08:43:32.200064Z","iopub.status.idle":"2025-11-26T08:43:57.733312Z","shell.execute_reply.started":"2025-11-26T08:43:32.200026Z","shell.execute_reply":"2025-11-26T08:43:57.732380Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nimport gc\nimport lightgbm as lgb\nfrom tqdm.auto import tqdm\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.utils import resample\nimport itertools\n\n# --- 1. CẤU HÌNH ---\nDATA_PATH = '/kaggle/input/MABe-mouse-behavior-detection/'\nNUM_VIDEOS_TRAIN = 50 \n\n# --- 2. HÀM FEATURE ENGINEERING AN TOÀN (FIX LỖI KEYERROR) ---\ndef calculate_features_safe(df):\n    # Khởi tạo mặc định bằng 0.0 để tránh lỗi thiếu cột\n    df['distance'] = 0.0\n    df['velocity_m1'] = 0.0\n    df['velocity_m2'] = 0.0\n    \n    # 1. Tính Khoảng cách (Chỉ khi có đủ cả 2 chuột)\n    if 'mouse1_body_center_x' in df.columns and 'mouse2_body_center_x' in df.columns:\n        dx = df['mouse1_body_center_x'] - df['mouse2_body_center_x']\n        dy = df['mouse1_body_center_y'] - df['mouse2_body_center_y']\n        df['distance'] = np.sqrt(dx**2 + dy**2)\n    \n    # 2. Tính Vận tốc M1\n    if 'mouse1_body_center_x' in df.columns:\n        vx1 = df['mouse1_body_center_x'].diff().fillna(0)\n        vy1 = df['mouse1_body_center_y'].diff().fillna(0)\n        df['velocity_m1'] = np.sqrt(vx1**2 + vy1**2)\n        \n    # 3. Tính Vận tốc M2\n    if 'mouse2_body_center_x' in df.columns:\n        vx2 = df['mouse2_body_center_x'].diff().fillna(0)\n        vy2 = df['mouse2_body_center_y'].diff().fillna(0)\n        df['velocity_m2'] = np.sqrt(vx2**2 + vy2**2)\n        \n    # 4. Tính Ký ức (Rolling Window)\n    # Vì các cột trên đã được khởi tạo (dù là 0), nên đoạn này luôn an toàn\n    w = 10\n    df['dist_mean_10'] = df['distance'].rolling(window=w).mean().fillna(0)\n    df['vel1_mean_10'] = df['velocity_m1'].rolling(window=w).mean().fillna(0)\n    df['vel2_mean_10'] = df['velocity_m2'].rolling(window=w).mean().fillna(0)\n    \n    return df\n\nfeatures = ['distance', 'velocity_m1', 'velocity_m2', 'dist_mean_10', 'vel1_mean_10', 'vel2_mean_10']\n\n# --- 3. HÀM LOAD DATA \"ALL-PAIRS\" (ĐÃ THÊM TRY-EXCEPT) ---\ndef load_train_data_all_pairs(meta_row):\n    try:\n        video_id = meta_row['video_id']\n        lab_id = meta_row['lab_id']\n        pix_per_cm = meta_row['pix_per_cm_approx'] if meta_row['pix_per_cm_approx'] > 0 else 1.0\n        \n        t_path = os.path.join(DATA_PATH, 'train_tracking', lab_id, f'{video_id}.parquet')\n        a_path = os.path.join(DATA_PATH, 'train_annotation', lab_id, f'{video_id}.parquet')\n        \n        if not os.path.exists(t_path) or not os.path.exists(a_path): return None\n        \n        # Load & Pivot\n        df_track = pd.read_parquet(t_path)\n        px = df_track.pivot(index='video_frame', columns=['mouse_id', 'bodypart'], values='x')\n        px.columns = [f\"mouse{m}_{bp}_x\" for m, bp in px.columns]\n        py = df_track.pivot(index='video_frame', columns=['mouse_id', 'bodypart'], values='y')\n        py.columns = [f\"mouse{m}_{bp}_y\" for m, bp in py.columns]\n        df_wide = pd.concat([px, py], axis=1).sort_index(axis=1)\n        \n        # Interpolate & Normalize\n        df_wide = df_wide.interpolate(limit=5).fillna(0)\n        df_wide = df_wide / pix_per_cm \n        \n        # Load Annotation\n        df_annot = pd.read_parquet(a_path)\n        \n        mouse_ids = sorted(list(set([int(c.split('_')[0].replace('mouse', '')) for c in df_wide.columns if 'mouse' in c])))\n        pairs_data = []\n        \n        for m1, m2 in itertools.combinations(mouse_ids, 2):\n            cols1 = [c for c in df_wide.columns if f'mouse{m1}_' in c]\n            cols2 = [c for c in df_wide.columns if f'mouse{m2}_' in c]\n            if not cols1 or not cols2: continue\n            \n            df_pair = df_wide[cols1 + cols2].copy()\n            rename_dict = {}\n            for c in cols1: rename_dict[c] = c.replace(f'mouse{m1}_', 'mouse1_')\n            for c in cols2: rename_dict[c] = c.replace(f'mouse{m2}_', 'mouse2_')\n            df_pair.rename(columns=rename_dict, inplace=True)\n            \n            # Check xem sau khi rename có đủ cột body_center không, nếu không thì hàm safe sẽ xử lý\n            \n            df_pair['label'] = 'other'\n            mask = ((df_annot['agent_id'] == m1) & (df_annot['target_id'] == m2)) | \\\n                   ((df_annot['agent_id'] == m2) & (df_annot['target_id'] == m1))\n            \n            pair_annot = df_annot[mask]\n            for _, r in pair_annot.iterrows():\n                if r['stop_frame'] <= df_pair.index.max():\n                    df_pair.loc[r['start_frame']:r['stop_frame'], 'label'] = r['action']\n                    \n            pairs_data.append(df_pair)\n            \n        if pairs_data:\n            return pd.concat(pairs_data, ignore_index=True)\n    except Exception as e:\n        # print(f\"Skipping video {meta_row['video_id']} due to error: {e}\")\n        return None\n        \n    return None\n\n# --- 4. CHUẨN BỊ DỮ LIỆU TRAIN ---\nprint(\"⏳ Đang tạo dữ liệu 'All-Pairs' từ 50 video (Safe Mode)...\")\ntry:\n    df_meta = pd.read_csv(os.path.join(DATA_PATH, 'train.csv'))\nexcept:\n    df_meta = pd.DataFrame()\n\nall_train_dfs = []\n# Dùng tqdm để theo dõi tiến độ\nfor i in tqdm(range(min(NUM_VIDEOS_TRAIN, len(df_meta)))):\n    df = load_train_data_all_pairs(df_meta.iloc[i])\n    if df is not None:\n        # Dùng hàm SAFE thay vì hàm thường\n        df = calculate_features_safe(df) \n        all_train_dfs.append(df[features + ['label']])\n\nif len(all_train_dfs) > 0:\n    df_train_big = pd.concat(all_train_dfs, ignore_index=True)\n    del all_train_dfs\n    gc.collect()\n\n    print(f\"✅ Dữ liệu thô: {df_train_big.shape}\")\n\n    # --- 5. CÂN BẰNG DỮ LIỆU ---\n    print(\"⚖️ Đang cân bằng dữ liệu...\")\n    others = df_train_big[df_train_big['label'] == 'other']\n    actions = df_train_big[df_train_big['label'] != 'other']\n\n    n_sample = min(len(others), len(actions))\n    # Nếu video quá ít action, ta lấy toàn bộ action và một phần other\n    if n_sample == 0: n_sample = 1000 # Fallback\n    \n    df_train_bal = pd.concat([\n        resample(others, replace=False, n_samples=n_sample, random_state=42),\n        actions # Lấy hết action (vì thường action ít hơn other)\n    ])\n    \n    # Nếu action nhiều hơn other (hiếm), ta cũng có thể resample action. \n    # Nhưng code trên ưu tiên giữ lại toàn bộ hành vi hiếm.\n    \n    print(f\"✅ Dữ liệu train cuối cùng: {df_train_bal.shape}\")\n\n    # Label Encoding\n    le = LabelEncoder()\n    y_train = le.fit_transform(df_train_bal['label'])\n    X_train = df_train_bal[features]\n\n    # --- 6. HUẤN LUYỆN LIGHTGBM ---\n    print(\"🚀 Đang huấn luyện LightGBM...\")\n    params = {\n        'objective': 'multiclass',\n        'num_class': len(le.classes_),\n        'metric': 'multi_logloss',\n        'boosting_type': 'gbdt',\n        'n_jobs': -1,\n        'random_state': 42,\n        'learning_rate': 0.05,\n        'n_estimators': 500,\n        'verbosity': -1\n    }\n\n    model = lgb.LGBMClassifier(**params)\n    model.fit(X_train, y_train)\n    print(\"🎉 Huấn luyện xong!\")\n\nelse:\n    print(\"❌ Không load được dữ liệu nào. Kiểm tra lại đường dẫn hoặc dataset.\")\n\n# --- 7. HÀM POST-PROCESSING ---\ndef run_length_encoding_pro(predictions, agent_id, target_id):\n    events = []\n    if len(predictions) == 0: return events\n    \n    current_label = predictions[0]\n    start_frame = 0\n    \n    for i in range(1, len(predictions)):\n        if predictions[i] != current_label:\n            if current_label != 'other':\n                events.append({\n                    'agent_id': agent_id,\n                    'target_id': target_id,\n                    'action': current_label,\n                    'start_frame': start_frame,\n                    'stop_frame': i - 1\n                })\n            current_label = predictions[i]\n            start_frame = i\n            \n    if current_label != 'other':\n        events.append({\n            'agent_id': agent_id,\n            'target_id': target_id,\n            'action': current_label,\n            'start_frame': start_frame,\n            'stop_frame': len(predictions) - 1\n        })\n    return events\n\n# --- 8. TẠO SUBMISSION (SAFE MODE) ---\nif 'model' in locals(): # Chỉ chạy nếu train thành công\n    print(\"📝 Đang tạo submission...\")\n    try:\n        df_test_meta = pd.read_csv(os.path.join(DATA_PATH, 'test.csv'))\n    except: df_test_meta = pd.DataFrame()\n\n    submission_rows = []\n    row_id_counter = 0\n\n    for idx, row in tqdm(df_test_meta.iterrows(), total=len(df_test_meta)):\n        try:\n            video_id = row['video_id']\n            lab_id = row['lab_id']\n            pix_per_cm = row['pix_per_cm_approx'] if row['pix_per_cm_approx'] > 0 else 1.0\n            \n            t_path = os.path.join(DATA_PATH, 'test_tracking', lab_id, f'{video_id}.parquet')\n            if not os.path.exists(t_path): continue\n            \n            df_track = pd.read_parquet(t_path)\n            px = df_track.pivot(index='video_frame', columns=['mouse_id', 'bodypart'], values='x')\n            px.columns = [f\"mouse{m}_{bp}_x\" for m, bp in px.columns]\n            py = df_track.pivot(index='video_frame', columns=['mouse_id', 'bodypart'], values='y')\n            py.columns = [f\"mouse{m}_{bp}_y\" for m, bp in py.columns]\n            df_wide = pd.concat([px, py], axis=1).sort_index(axis=1)\n            \n            df_wide = df_wide.interpolate(limit=5).fillna(0)\n            df_wide = df_wide / pix_per_cm\n            \n            mouse_ids = sorted(list(set([int(c.split('_')[0].replace('mouse', '')) for c in df_wide.columns if 'mouse' in c])))\n            \n            for m1, m2 in itertools.permutations(mouse_ids, 2):\n                cols1 = [c for c in df_wide.columns if f'mouse{m1}_' in c]\n                cols2 = [c for c in df_wide.columns if f'mouse{m2}_' in c]\n                if not cols1 or not cols2: continue\n                \n                df_pair = df_wide[cols1 + cols2].copy()\n                rename_dict = {}\n                for c in cols1: rename_dict[c] = c.replace(f'mouse{m1}_', 'mouse1_')\n                for c in cols2: rename_dict[c] = c.replace(f'mouse{m2}_', 'mouse2_')\n                df_pair.rename(columns=rename_dict, inplace=True)\n                \n                # Dùng hàm SAFE features\n                df_pair = calculate_features_safe(df_pair)\n                \n                X_test = pd.DataFrame(0.0, index=df_pair.index, columns=features)\n                for c in features:\n                    if c in df_pair.columns: X_test[c] = df_pair[c]\n                \n                # Dự đoán\n                y_pred_idx = model.predict(X_test)\n                y_pred_lbl = le.inverse_transform(y_pred_idx)\n                \n                events = run_length_encoding_pro(y_pred_lbl, m1, m2)\n                \n                for event in events:\n                    submission_rows.append({\n                        'row_id': row_id_counter,\n                        'video_id': video_id,\n                        'agent_id': event['agent_id'],\n                        'target_id': event['target_id'],\n                        'action': event['action'],\n                        'start_frame': event['start_frame'],\n                        'stop_frame': event['stop_frame']\n                    })\n                    row_id_counter += 1\n                    \n            del df_wide\n            gc.collect()\n        except Exception as e:\n            print(f\"Error processing test video {video_id}: {e}\")\n            continue\n\n    # Lưu file\n    sub = pd.DataFrame(submission_rows)\n    if len(sub) == 0:\n        sub = pd.DataFrame(columns=['row_id', 'video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame'])\n\n    sub.to_csv('submission.csv', index=False)\n    print(f\"✅ Đã tạo submission.csv với {len(sub)} dòng.\")\n    display(sub.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-27T03:20:11.321629Z","iopub.execute_input":"2025-11-27T03:20:11.321949Z","iopub.status.idle":"2025-11-27T03:23:15.244301Z","shell.execute_reply.started":"2025-11-27T03:20:11.321921Z","shell.execute_reply":"2025-11-27T03:23:15.243386Z"}},"outputs":[],"execution_count":null}]}