{"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":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 1. Setup and Imports","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom tqdm import trange, tqdm\nimport itertools\nimport warnings\nimport json\nimport os\nimport lightgbm\nfrom collections import defaultdict\nimport polars as pl\nimport gc\nimport joblib\nfrom pathlib import Path\n\n# Scikit-learn imports\nfrom sklearn.base import ClassifierMixin, BaseEstimator, clone\nfrom sklearn.model_selection import cross_val_predict, GroupKFold, train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import f1_score, precision_recall_curve\nfrom sklearn.exceptions import UndefinedMetricWarning\n\n# --- CONFIGURATION ---\nvalidate_or_submit = 'submit' \nverbose = True\nSEED = 42\n\n# --- MODEL AND FEATURE CONSTANTS ---\nN_SAMPLES_SUBSET = 100000\nN_ESTIMATORS_LGBM = 500\nMIN_BEHAVIOR_SAMPLES = 100\nMIN_GROUPS_FOR_CV = 5\nMIN_SEGMENT_LENGTH = 4\nMERGE_MAX_GAP = 2\n\n# --- INITIALIZATION ---\nnp.random.seed(SEED)\nos.environ['PYTHONHASHSEED'] = str(SEED)\n\nwarnings.filterwarnings('ignore', category=UndefinedMetricWarning)\nwarnings.filterwarnings('ignore', category=RuntimeWarning, message='invalid value encountered')\n\nPath('./models').mkdir(exist_ok=True)\nPath('./results').mkdir(exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T15:59:57.291441Z","iopub.execute_input":"2025-10-03T15:59:57.291831Z","iopub.status.idle":"2025-10-03T15:59:57.302840Z","shell.execute_reply.started":"2025-10-03T15:59:57.291806Z","shell.execute_reply":"2025-10-03T15:59:57.301476Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 2. Core Modules and Helper Functions","metadata":{}},{"cell_type":"code","source":"class TrainOnSubsetClassifier(ClassifierMixin, BaseEstimator):\n    \"\"\"\n    Fit estimator to a stratified subset of the training data to speed up training\n    while preserving class distribution.\n    \"\"\"\n    def __init__(self, estimator, n_samples):\n        self.estimator = estimator\n        self.n_samples = n_samples\n\n    def fit(self, X, y):\n        if len(X) <= self.n_samples:\n            self.estimator.fit(X, y)\n        else:\n            try:\n                X_sample, _, y_sample, _ = train_test_split(\n                    X, y,\n                    train_size=self.n_samples,\n                    stratify=y,\n                    random_state=SEED,\n                    shuffle=True\n                )\n                self.estimator.fit(X_sample, y_sample)\n            except (ValueError, KeyError): # Fallback for non-stratifiable cases\n                indices = np.random.choice(len(X), self.n_samples, replace=False)\n                self.estimator.fit(X.iloc[indices], y.iloc[indices])\n\n        self.classes_ = self.estimator.classes_\n        return self\n\n    def predict_proba(self, X):\n        if len(self.classes_) == 1:\n            prob_array = np.zeros((len(X), 2))\n            class_idx = self.classes_[0]\n            prob_array[:, class_idx] = 1.0\n            return prob_array\n        return self.estimator.predict_proba(X)\n\ndef find_best_threshold(y_true, y_pred_proba):\n    \"\"\"Find the best F1 threshold from precision-recall curve.\"\"\"\n    precision, recall, thresholds = precision_recall_curve(y_true, y_pred_proba)\n    thresholds = np.append(thresholds, 1.0)\n    f1_scores = (2 * precision * recall) / (precision + recall + 1e-6)\n    best_idx = np.argmax(f1_scores)\n    return thresholds[best_idx]\n\ndef predict_and_segment(pred_df, meta, thresholds):\n    \"\"\"Convert model probabilities to action segments using per-action thresholds.\"\"\"\n    submission_parts = []\n    for action, threshold in thresholds.items():\n        if action not in pred_df.columns:\n            continue\n        \n        preds = (pred_df[action] >= threshold).astype(int)\n        changes_mask = (preds != preds.shift(1))\n        segment_boundaries = meta[changes_mask]\n        action_values = preds[changes_mask]\n\n        starts = segment_boundaries[action_values == 1]\n        ends = segment_boundaries[action_values == 0]\n        \n        segs = []\n        for _, start_row in starts.iterrows():\n            end_frame_candidates = ends[ends.index > start_row.name]\n            stop_frame = end_frame_candidates.index[0] if not end_frame_candidates.empty else meta.video_frame.max() + 1\n            \n            segs.append({\n                'video_id': start_row['video_id'], 'agent_id': start_row['agent_id'],\n                'target_id': start_row['target_id'], 'action': action,\n                'start_frame': start_row.name, 'stop_frame': stop_frame,\n            })\n        \n        if segs:\n            submission_parts.append(pd.DataFrame(segs))\n\n    if not submission_parts:\n        return pd.DataFrame()\n\n    full_submission = pd.concat(submission_parts).sort_values('start_frame').reset_index(drop=True)\n    return refine_segments(full_submission)\n\ndef refine_segments(df):\n    \"\"\"Apply post-processing heuristics to clean up segments.\"\"\"\n    if df.empty:\n        return df\n\n    df['duration'] = df['stop_frame'] - df['start_frame']\n    df = df[df['duration'] >= MIN_SEGMENT_LENGTH]\n    \n    df = df.sort_values(['video_id', 'agent_id', 'target_id', 'action', 'start_frame'])\n    \n    merged_rows = []\n    for _, group in df.groupby(['video_id', 'agent_id', 'target_id', 'action']):\n        if len(group) == 1:\n            merged_rows.append(group)\n            continue\n        \n        current_row = group.iloc[0].copy()\n        for i in range(1, len(group)):\n            next_row = group.iloc[i]\n            if (next_row['start_frame'] - current_row['stop_frame']) <= MERGE_MAX_GAP:\n                current_row['stop_frame'] = next_row['stop_frame']\n            else:\n                merged_rows.append(pd.DataFrame([current_row]))\n                current_row = next_row.copy()\n        merged_rows.append(pd.DataFrame([current_row]))\n        \n    if not merged_rows:\n        return pd.DataFrame(columns=df.columns)\n\n    return pd.concat(merged_rows).drop(columns=['duration']).reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T15:59:57.304829Z","iopub.execute_input":"2025-10-03T15:59:57.305340Z","iopub.status.idle":"2025-10-03T15:59:57.328684Z","shell.execute_reply.started":"2025-10-03T15:59:57.305178Z","shell.execute_reply":"2025-10-03T15:59:57.327181Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3. Data Loading and Preparation","metadata":{}},{"cell_type":"code","source":"train = pd.read_csv('/kaggle/input/MABe-mouse-behavior-detection/train.csv')\ntest = pd.read_csv('/kaggle/input/MABe-mouse-behavior-detection/test.csv')\n\n# --- Standardize Body Part Schemas ---\nCANONICAL_SCHEMA_1 = ['body_center', 'ear_left', 'ear_right', 'lateral_left', 'lateral_right', 'neck', 'nose', 'tail_base', 'tail_midpoint', 'tail_tip']\nCANONICAL_SCHEMA_2 = ['body_center', 'ear_left', 'ear_right', 'nose', 'tail_base']\nBODY_PARTS_WHITELIST = [\n    'body_center', 'ear_left', 'ear_right', 'hip_left', 'hip_right',\n    'lateral_left', 'lateral_right', 'neck', 'nose', 'tail_base',\n    'tail_midpoint', 'tail_tip'\n]\n\ndef map_to_canonical_schema(parts_str):\n    \"\"\"Maps a list of body parts to the best-matching canonical schema.\"\"\"\n    parts = json.loads(parts_str)\n    if all(p in parts for p in CANONICAL_SCHEMA_1):\n        return json.dumps(CANONICAL_SCHEMA_1)\n    if all(p in parts for p in CANONICAL_SCHEMA_2):\n        return json.dumps(CANONICAL_SCHEMA_2)\n    return parts_str\n\ntrain['canonical_schema'] = train['body_parts_tracked'].apply(map_to_canonical_schema)\ntest['canonical_schema'] = test['body_parts_tracked'].apply(map_to_canonical_schema)\n\nbody_parts_schemas_to_process = train['canonical_schema'].unique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T15:59:57.329766Z","iopub.execute_input":"2025-10-03T15:59:57.330101Z","iopub.status.idle":"2025-10-03T15:59:57.495838Z","shell.execute_reply.started":"2025-10-03T15:59:57.330072Z","shell.execute_reply":"2025-10-03T15:59:57.494872Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 4. Data Generation with Pre-Imputation","metadata":{}},{"cell_type":"code","source":"def generate_mouse_data(dataset, traintest, traintest_directory=None, generate_single=True, generate_pair=True):\n    assert traintest in ['train', 'test']\n    if traintest_directory is None:\n        traintest_directory = f\"/kaggle/input/MABe-mouse-behavior-detection/{traintest}_tracking\"\n    \n    for _, row in dataset.iterrows():\n        lab_id, video_id = row.lab_id, row.video_id\n        if lab_id.startswith('MABe22'): continue\n        if not isinstance(row.behaviors_labeled, str):\n            if verbose: print(f'No labeled behaviors for {lab_id}/{video_id}, skipping.')\n            continue\n\n        path = f\"{traintest_directory}/{lab_id}/{video_id}.parquet\"\n        try:\n            vid = pd.read_parquet(path)\n        except FileNotFoundError:\n            if verbose: print(f\"Tracking file not found: {path}\")\n            continue\n\n        # Filter to whitelisted body parts for consistency\n        vid = vid[vid.bodypart.isin(BODY_PARTS_WHITELIST)]\n        \n        pvid = vid.pivot(columns=['mouse_id', 'bodypart'], index='video_frame', values=['x', 'y'])\n        del vid; gc.collect()\n\n        # Proactively interpolate missing coordinates\n        pvid = pvid.interpolate(method='linear', limit_direction='both', axis=0)\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 = pd.DataFrame([b.split(',') for b in sorted(list(set(json.loads(row.behaviors_labeled))))], columns=['agent', 'target', 'action'])\n        \n        if traintest == 'train':\n            try:\n                annot = pd.read_parquet(path.replace(f'{traintest}_tracking', f'{traintest}_annotation'))\n            except FileNotFoundError:\n                continue\n\n        # Generate single mouse data\n        if generate_single:\n            vid_behaviors_subset = vid_behaviors.query(\"target == 'self'\")\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                    single_mouse_meta = pd.DataFrame({'video_id': video_id, 'agent_id': mouse_id_str, 'target_id': 'self', '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 _, annot_row in annot_subset.iterrows():\n                            single_mouse_label.loc[annot_row['start_frame']:annot_row['stop_frame'], annot_row.action] = 1.0\n                        # This 'yield' is crucial\n                        yield 'single', single_mouse, single_mouse_meta, single_mouse_label\n                    else:\n                        if verbose: print(f'- test single {video_id} {mouse_id}')\n                        # This 'yield' is crucial\n                        yield 'single', single_mouse, single_mouse_meta, vid_agent_actions\n                except KeyError:\n                    pass\n        \n        # Generate mouse pair data\n        if generate_pair:\n            vid_behaviors_subset = vid_behaviors.query(\"target != 'self'\")\n            if len(vid_behaviors_subset) > 0:\n                for agent, target in itertools.permutations(np.unique(pvid.columns.get_level_values('mouse_id')), 2):\n                    agent_str, target_str = f\"mouse{agent}\", f\"mouse{target}\"\n                    vid_agent_actions = np.unique(vid_behaviors_subset.query(\"(agent == @agent_str) & (target == @target_str)\").action)\n                    if len(vid_agent_actions) == 0: continue\n                    \n                    mouse_pair = pd.concat([pvid[agent], pvid[target]], axis=1, keys=['A', 'B'])\n                    mouse_pair_meta = pd.DataFrame({'video_id': video_id, 'agent_id': agent_str, 'target_id': target_str, '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 _, annot_row in annot_subset.iterrows():\n                             mouse_pair_label.loc[annot_row['start_frame']:annot_row['stop_frame'], annot_row.action] = 1.0\n                        # This 'yield' is crucial\n                        yield 'pair', mouse_pair, mouse_pair_meta, mouse_pair_label\n                    else:\n                        if verbose: print(f'- test pair {video_id} {agent} {target}')\n                        # This 'yield' is crucial\n                        yield 'pair', mouse_pair, mouse_pair_meta, vid_agent_actions\n\n# --- Configurable Temporal Feature Parameters ---\nTIME_WINDOWS = [5, 15, 30, 60]\nLAG_OFFSETS = [10, 20, 40]\nCONTEXT_OFFSETS = [-20, -10, 10, 20]\n\ndef transform_single(single_mouse, body_parts_tracked):\n    \"\"\"Generates features for a single mouse's tracking data.\"\"\"\n    available_parts = set(single_mouse.columns.get_level_values(0))\n    X = pd.DataFrame(index=single_mouse.index)\n    \n    # Pairwise distances\n    for p1, p2 in itertools.combinations(body_parts_tracked, 2):\n        if p1 in available_parts and p2 in available_parts:\n            X[f\"{p1}+{p2}\"] = np.sum(np.square(single_mouse[p1].values - single_mouse[p2].values), axis=1)\n\n    # Long-range temporal features (guarded)\n    if 'body_center' in available_parts:\n        center_x, center_y = single_mouse['body_center']['x'], single_mouse['body_center']['y']\n        for window in TIME_WINDOWS:\n            # Calculate activity level (movement variance) over the window\n            activity_sq = center_x.diff().rolling(window).std()**2 + center_y.diff().rolling(window).std()**2\n            X[f'activity_level_{window}'] = np.sqrt(activity_sq)\n    \n    # Drop columns that are mostly empty to save memory\n    X = X.dropna(axis=1, thresh=len(X) * 0.5)\n    return X\n\ndef transform_pair(mouse_pair, body_parts_tracked):\n    \"\"\"Generates features for a pair of mice's tracking data.\"\"\"\n    available_A = set(mouse_pair['A'].columns.get_level_values(0))\n    available_B = set(mouse_pair['B'].columns.get_level_values(0))\n    X = pd.DataFrame(index=mouse_pair.index)\n\n    # Inter-mouse distances\n    for p1 in body_parts_tracked:\n        for p2 in body_parts_tracked:\n            if p1 in available_A and p2 in available_B:\n                X[f\"12+{p1}+{p2}\"] = np.sum(np.square(mouse_pair['A'][p1].values - mouse_pair['B'][p2].values), axis=1)\n    \n    # Social zone indicators and temporal stats (guarded)\n    if 'body_center' in available_A and 'body_center' in available_B:\n        center_dist = np.linalg.norm(mouse_pair['A']['body_center'].values - mouse_pair['B']['body_center'].values, axis=1)\n        \n        for window in TIME_WINDOWS:\n            X[f'dist_mean_{window}'] = pd.Series(center_dist, index=X.index).rolling(window, min_periods=1, center=True).mean()\n            X[f'dist_std_{window}'] = pd.Series(center_dist, index=X.index).rolling(window, min_periods=1, center=True).std()\n\n    # Drop columns that are mostly empty\n    X = X.dropna(axis=1, thresh=len(X) * 0.5)\n    return X","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T15:59:57.504058Z","iopub.execute_input":"2025-10-03T15:59:57.504369Z","iopub.status.idle":"2025-10-03T15:59:57.533072Z","shell.execute_reply.started":"2025-10-03T15:59:57.504348Z","shell.execute_reply":"2025-10-03T15:59:57.531847Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training ","metadata":{}},{"cell_type":"code","source":"def cross_validate_and_train(schema_str, switch, X_tr, label, meta):\n    \"\"\"\n    Performs GroupKFold CV, tunes thresholds, evaluates, and trains final models.\n    \"\"\"\n    tuned_thresholds = {}\n    print(f\"  Starting cross-validation for {len(label.columns)} actions...\")\n    \n    for action in tqdm(label.columns, desc=\"  Actions\"):\n        action_mask = label[action].notna()\n        y_action = label.loc[action_mask, action].astype(int)\n        \n        if y_action.sum() < MIN_BEHAVIOR_SAMPLES:\n            continue\n            \n        X_action = X_tr[action_mask]\n        groups_action = meta.loc[action_mask, 'video_id']\n        \n        if len(groups_action.unique()) < MIN_GROUPS_FOR_CV:\n            continue\n\n        binary_classifier = make_pipeline(\n            SimpleImputer(strategy='mean'),\n            StandardScaler(),\n            lightgbm.LGBMClassifier(\n                n_estimators=N_ESTIMATORS_LGBM,\n                class_weight='balanced',\n                random_state=SEED,\n                n_jobs=-1,\n                verbose=-1\n            )\n        )\n        \n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\")\n            oof_action_preds = cross_val_predict(\n                binary_classifier, X_action, y_action, \n                groups=groups_action, cv=GroupKFold(n_splits=MIN_GROUPS_FOR_CV), \n                method='predict_proba'\n            )[:, 1]\n        \n        best_thresh = find_best_threshold(y_action, oof_action_preds)\n        tuned_thresholds[action] = best_thresh\n        f1 = f1_score(y_action, oof_action_preds >= best_thresh)\n        print(f\"  OOF F1 for '{action}': {f1:.4f} | Thresh: {best_thresh:.3f}\")\n        \n        print(f\"  Training final model for '{action}'...\")\n        final_model = clone(binary_classifier).fit(X_action, y_action)\n        model_path = f'./models/{schema_str.replace(\" \", \"\")}_{switch}_{action}.joblib'\n        joblib.dump(final_model, model_path)\n    \n    joblib.dump(tuned_thresholds, f'./models/{schema_str.replace(\" \", \"\")}_{switch}_thresholds.joblib')\n\ndef submit(schema_str, switch, test_dataset):\n    \"\"\"\n    Loads trained models and thresholds to generate predictions on the test set.\n    \"\"\"\n    submission_list = []\n    \n    try:\n        thresholds = joblib.load(f'./models/{schema_str.replace(\" \", \"\")}_{switch}_thresholds.joblib')\n        models = {\n            action: joblib.load(f'./models/{schema_str.replace(\" \", \"\")}_{switch}_{action}.joblib')\n            for action in thresholds.keys()\n        }\n    except FileNotFoundError:\n        return pd.DataFrame()\n\n    body_parts_tracked = json.loads(schema_str)\n    \n    generator = generate_mouse_data(\n        test_dataset.query(\"canonical_schema == @schema_str\"), 'test',\n        generate_single=(switch == 'single'), \n        generate_pair=(switch == 'pair')\n    )\n    \n    for switch_te, data_te, meta_te, actions_te in generator:\n        if verbose: print(f\"  Predicting for video {meta_te.video_id.iloc[0]}...\")\n        \n        X_te = transform_single(data_te, body_parts_tracked) if switch_te == 'single' else transform_pair(data_te, body_parts_tracked)\n        \n        pred_df = pd.DataFrame(index=meta_te.index)\n        for action, model in models.items():\n            if action in actions_te:\n                # --- FIX APPLIED HERE ---\n                # Use the pipeline's `feature_names_in_` attribute to get the correct\n                # feature names that the first step (imputer) was trained on.\n                model_features = model.feature_names_in_\n                X_te_aligned = X_te.reindex(columns=model_features, fill_value=0)\n                pred_df[action] = model.predict_proba(X_te_aligned)[:, 1]\n\n        if not pred_df.empty:\n            submission_part = predict_and_segment(pred_df, meta_te, thresholds)\n            submission_list.append(submission_part)\n            \n    return pd.concat(submission_list) if submission_list else pd.DataFrame()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T15:59:57.534885Z","iopub.execute_input":"2025-10-03T15:59:57.535178Z","iopub.status.idle":"2025-10-03T15:59:57.562095Z","shell.execute_reply.started":"2025-10-03T15:59:57.535155Z","shell.execute_reply":"2025-10-03T15:59:57.560977Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 6. Main Execution ","metadata":{}},{"cell_type":"code","source":"submission_list = []\n\nfor i, schema_str in enumerate(body_parts_schemas_to_process):\n    body_parts_tracked = json.loads(schema_str)\n    print(f\"\\n--- Processing Schema {i+1}/{len(body_parts_schemas_to_process)}: {len(body_parts_tracked)} parts ---\")\n    \n    train_subset = train[train.canonical_schema == schema_str]\n    if len(train_subset) == 0:\n        continue\n\n    # Generate data for the current schema\n    data_lists = {'single': [], 'pair': []}\n    meta_lists = {'single': [], 'pair': []}\n    label_lists = {'single': [], 'pair': []}\n    for switch, data, meta, label in generate_mouse_data(train_subset, 'train'):\n        data_lists[switch].append(data)\n        meta_lists[switch].append(meta)\n        label_lists[switch].append(label)\n\n    # Process single-mouse and pair-mouse data separately\n    for switch in ['single', 'pair']:\n        if not data_lists[switch]:\n            continue\n            \n        print(f\"\\nProcessing '{switch}' mouse data...\")\n        \n        full_data = pd.concat(data_lists[switch])\n        full_meta = pd.concat(meta_lists[switch])\n        full_label = pd.concat(label_lists[switch])\n        del data_lists[switch], meta_lists[switch], label_lists[switch]; gc.collect()\n\n        print(\"  Transforming features...\")\n        X_tr = transform_single(full_data, body_parts_tracked) if switch == 'single' else transform_pair(full_data, body_parts_tracked)\n        del full_data; gc.collect()\n        \n        print(f\"  Feature matrix shape: {X_tr.shape}\")\n\n        # Align indices\n        X_tr.reset_index(drop=True, inplace=True)\n        full_label.reset_index(drop=True, inplace=True)\n        full_meta.reset_index(drop=True, inplace=True)\n      \n        print(\"  Training and saving models for submission...\")\n        cross_validate_and_train(schema_str, switch, X_tr, full_label, full_meta)\n        \n        print(\"  Running inference on test set...\")\n        submission_part = submit(schema_str, switch, test)\n        submission_list.append(submission_part)\n\n        del X_tr, full_label, full_meta; gc.collect()\n\n# Create final submission file\nfinal_submission = pd.concat(submission_list).reset_index(drop=True)\nfinal_submission.to_csv('submission.csv', index_label='row_id')\nprint(\"\\nSubmission file created successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-03T15:59:57.563327Z","iopub.execute_input":"2025-10-03T15:59:57.563857Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}