{"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":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":2187279,"sourceType":"datasetVersion","datasetId":767955}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-04T01:06:13.602754Z","iopub.execute_input":"2025-12-04T01:06:13.603033Z","iopub.status.idle":"2025-12-04T01:06:16.680028Z","shell.execute_reply.started":"2025-12-04T01:06:13.603005Z","shell.execute_reply":"2025-12-04T01:06:16.678630Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport cv2\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\nimport timm  # Library for EfficientNet\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# --- CONFIGURATION ---\nCONF = {\n    'input_dir': '/kaggle/input/csiro-biomass',\n    'train_img_dir': '/kaggle/input/csiro-biomass/train',\n    'test_img_dir': '/kaggle/input/csiro-biomass/test',\n    'batch_size': 16,\n    'num_workers': 2,  # Lowered to prevent shared memory errors in some envs\n    'lr': 1e-4,\n    'epochs': 5,\n    'model_name': 'tf_efficientnet_b3_ns',\n    'target_cols': ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n}\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\n# --- 1. DATA PREPROCESSING (LONG -> WIDE FIX) ---\ndef load_and_pivot_csv(csv_path, is_test=False):\n    \"\"\"\n    Reads CSV. If in Long format (ID__Target), pivots to Wide format (ID, Targ1, Targ2...).\n    \"\"\"\n    df = pd.read_csv(csv_path)\n    \n    # Check if pivot is needed (look for Wide format columns)\n    if 'Dry_Green_g' in df.columns:\n        print(f\"Loaded {csv_path}: Already in Wide Format.\")\n        return df\n\n    print(f\"Loaded {csv_path}: Detected Long Format. Pivoting...\")\n    \n    # Logic: Split \"ID123__Dry_Green_g\" -> \"ID123\", \"Dry_Green_g\"\n    try:\n        split_data = df['sample_id'].str.split('__', n=1, expand=True)\n        df['clean_id'] = split_data[0]\n        df['variable'] = split_data[1]\n    except Exception as e:\n        print(f\"Error parsing IDs: {e}\")\n        return df\n    \n    if is_test:\n        # For submission file, we just need unique image IDs\n        unique_ids = df['clean_id'].unique()\n        return pd.DataFrame({'sample_id': unique_ids})\n    \n    # Pivot logic\n    df_wide = df.pivot(index='clean_id', columns='variable', values='target').reset_index()\n    df_wide = df_wide.rename(columns={'clean_id': 'sample_id'})\n    \n    # Ensure float32\n    for col in CONF['target_cols']:\n        if col in df_wide.columns:\n            df_wide[col] = df_wide[col].astype(np.float32)\n        else:\n            # Handle missing columns gracefully\n            df_wide[col] = 0.0\n            \n    print(f\"Pivoted Data Shape: {df_wide.shape}\")\n    return df_wide\n\n# --- 2. DATASET CLASS (FILE NOT FOUND FIX) ---\nclass BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None, mode='train'):\n        self.df = df\n        self.img_dir = img_dir\n        self.transform = transform\n        self.mode = mode\n        self.targets = CONF['target_cols']\n        \n        # --- ROBUST FILENAME MAPPING ---\n        # Scans directory once to map \"ID123\" -> \"ID123.jpg\" or \"ID123.png\"\n        print(f\"Scanning directory: {img_dir} ...\")\n        if not os.path.exists(img_dir):\n             raise RuntimeError(f\"Directory not found: {img_dir}\")\n             \n        all_files = os.listdir(img_dir)\n        # Create map: { \"ID123\": \"ID123.png\", \"ID456\": \"ID456.jpg\" }\n        self.id_to_filename = {\n            os.path.splitext(f)[0]: f \n            for f in all_files \n            if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tif'))\n        }\n        print(f\"Mapped {len(self.id_to_filename)} images.\")\n\n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = row['sample_id']\n        \n        # Retrieve actual filename from map\n        filename = self.id_to_filename.get(image_id)\n        if filename is None:\n            # Skip or Error? For training, raising error is safer to debug data issues.\n            raise FileNotFoundError(f\"Image ID '{image_id}' not found in {self.img_dir}\")\n            \n        img_path = os.path.join(self.img_dir, filename)\n        \n        # Load\n        image = cv2.imread(img_path)\n        if image is None:\n             raise FileNotFoundError(f\"cv2 failed to load {img_path}\")\n             \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # Augment\n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n            \n        # Return\n        if self.mode == 'train' or self.mode == 'val':\n            targs = row[self.targets].values.astype(np.float32)\n            return image, torch.tensor(targs)\n        else:\n            return image, image_id\n\n# --- 3. AUGMENTATIONS ---\ndef get_transforms(data):\n    if data == 'train':\n        return A.Compose([\n            A.Resize(320, 320), # EfficientNet B3 works well ~300-320\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.RandomRotate90(p=0.5),\n            A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, p=0.5),\n            A.Normalize(),\n            ToTensorV2()\n        ])\n    else:\n        return A.Compose([\n            A.Resize(320, 320),\n            A.Normalize(),\n            ToTensorV2()\n        ])\n\n# --- 4. MODEL & LOSS ---\nclass BiomassNet(nn.Module):\n    def __init__(self, model_name=CONF['model_name']):\n        super().__init__()\n        # Try loading pretrained, handle offline case\n        try:\n            self.backbone = timm.create_model(model_name, pretrained=True, num_classes=0)\n        except Exception:\n            print(\"Warning: Internet unreachable. Loading non-pretrained weights.\")\n            self.backbone = timm.create_model(model_name, pretrained=False, num_classes=0)\n            \n        self.in_features = self.backbone.num_features\n        \n        self.head = nn.Sequential(\n            nn.Linear(self.in_features, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 5) # 5 targets\n        )\n        \n    def forward(self, x):\n        f = self.backbone(x)\n        return self.head(f)\n\nclass WeightedMSELoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        # Weights: Green, Dead, Clover, GDM, Total\n        # Penalize Total heavily (0.5)\n        self.weights = torch.tensor([0.2, 0.2, 0.1, 0.2, 1.0]).to(device)\n        \n    def forward(self, inputs, targets):\n        # inputs: [Batch, 5], targets: [Batch, 5]\n        return torch.mean(self.weights * (inputs - targets) ** 2)\n\n# --- 5. SUBMISSION GENERATOR ---\ndef generate_submission(model, loader):\n    model.eval()\n    results = []\n    print(\"Generating predictions...\")\n    \n    with torch.no_grad():\n        for images, ids in tqdm(loader):\n            images = images.to(device)\n            preds = model(images).cpu().numpy()\n            \n            # Clip negatives\n            preds = np.maximum(preds, 0)\n            \n            for i, sample_id in enumerate(ids):\n                # Melt back to Long format\n                for j, target_name in enumerate(CONF['target_cols']):\n                    row_id = f\"{sample_id}__{target_name}\"\n                    results.append({\n                        'sample_id': row_id,\n                        'target': preds[i][j]\n                    })\n                    \n    return pd.DataFrame(results)\n\n# --- MAIN EXECUTION ---\nif __name__ == '__main__':\n    # 1. Prepare Data\n    train_csv = os.path.join(CONF['input_dir'], 'train.csv')\n    df_full = load_and_pivot_csv(train_csv)\n    \n    # Split\n    df_train, df_val = train_test_split(df_full, test_size=0.1, random_state=42)\n    \n    # Datasets\n    train_ds = BiomassDataset(df_train, CONF['train_img_dir'], transform=get_transforms('train'), mode='train')\n    val_ds = BiomassDataset(df_val, CONF['train_img_dir'], transform=get_transforms('valid'), mode='val')\n    \n    train_loader = DataLoader(train_ds, batch_size=CONF['batch_size'], shuffle=True, num_workers=CONF['num_workers'])\n    val_loader = DataLoader(val_ds, batch_size=CONF['batch_size'], shuffle=False, num_workers=CONF['num_workers'])\n    \n    # 2. Setup Model\n    model = BiomassNet().to(device)\n    optimizer = torch.optim.AdamW(model.parameters(), lr=CONF['lr'])\n    criterion = WeightedMSELoss()\n    scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=CONF['lr'], \n                                                    steps_per_epoch=len(train_loader), epochs=CONF['epochs'])\n    \n    # 3. Train Loop\n    print(f\"Starting training for {CONF['epochs']} epochs...\")\n    for epoch in range(CONF['epochs']):\n        model.train()\n        train_loss = 0\n        \n        pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{CONF['epochs']}\")\n        for images, targets in pbar:\n            images, targets = images.to(device), targets.to(device)\n            \n            optimizer.zero_grad()\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n            loss.backward()\n            optimizer.step()\n            scheduler.step()\n            \n            train_loss += loss.item()\n            pbar.set_postfix({'loss': loss.item()})\n            \n        print(f\"Epoch {epoch+1} Avg Loss: {train_loss/len(train_loader):.4f}\")\n        \n    # 4. Inference\n    sample_sub_csv = os.path.join(CONF['input_dir'], 'sample_submission.csv')\n    # Use robust pivot loader for test file too (extracts IDs)\n    df_test = load_and_pivot_csv(sample_sub_csv, is_test=True)\n    \n    test_ds = BiomassDataset(df_test, CONF['test_img_dir'], transform=get_transforms('valid'), mode='test')\n    test_loader = DataLoader(test_ds, batch_size=CONF['batch_size'], shuffle=False, num_workers=CONF['num_workers'])\n    \n    sub_df = generate_submission(model, test_loader)\n    sub_df.to_csv('submission.csv', index=False)\n    print(f\"Success! Submission shape: {sub_df.shape}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport cv2\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import KFold\nfrom tqdm import tqdm\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport gc\n\n# --- CONFIGURATION ---\nCONF = {\n    'input_dir': '/kaggle/input/csiro-biomass',\n    'train_img_dir': '/kaggle/input/csiro-biomass/train',\n    'test_img_dir': '/kaggle/input/csiro-biomass/test',\n    'image_size': 384,     # Increased from 320 for better texture detail\n    'batch_size': 12,      # Slightly smaller batch for larger images\n    'num_workers': 2,\n    'lr': 2e-4,\n    'epochs': 12,          # Increased epochs\n    'n_folds': 5,          # 5-Fold CV\n    'model_name': 'tf_efficientnet_b4_ns', # Upgraded from B3 to B4\n    'target_cols': ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n}\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# --- 1. DATA PROCESSING ---\ndef load_and_pivot_csv(csv_path, is_test=False):\n    df = pd.read_csv(csv_path)\n    if 'Dry_Green_g' in df.columns: return df\n    \n    # Pivot logic\n    try:\n        split_data = df['sample_id'].str.split('__', n=1, expand=True)\n        df['clean_id'] = split_data[0]\n        df['variable'] = split_data[1]\n    except:\n        return df # Fallback\n    \n    if is_test:\n        return pd.DataFrame({'sample_id': df['clean_id'].unique()})\n    \n    df_wide = df.pivot(index='clean_id', columns='variable', values='target').reset_index()\n    df_wide = df_wide.rename(columns={'clean_id': 'sample_id'})\n    for col in CONF['target_cols']:\n        if col in df_wide.columns: df_wide[col] = df_wide[col].astype(np.float32)\n        else: df_wide[col] = 0.0\n    return df_wide\n\n# --- 2. DATASET (With Robust File Mapping) ---\nclass BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None, mode='train', file_map=None):\n        self.df = df\n        self.img_dir = img_dir\n        self.transform = transform\n        self.mode = mode\n        self.targets = CONF['target_cols']\n        self.file_map = file_map # Pass pre-computed map to save time\n\n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = row['sample_id']\n        \n        filename = self.file_map.get(image_id)\n        if filename is None:\n             # Just return a black image in worst case to not crash training\n             image = np.zeros((CONF['image_size'], CONF['image_size'], 3), dtype=np.uint8)\n        else:\n            img_path = os.path.join(self.img_dir, filename)\n            image = cv2.imread(img_path)\n            if image is not None:\n                image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            else:\n                image = np.zeros((CONF['image_size'], CONF['image_size'], 3), dtype=np.uint8)\n        \n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n            \n        if self.mode != 'test':\n            targs = row[self.targets].values.astype(np.float32)\n            return image, torch.tensor(targs)\n        else:\n            return image, image_id\n\ndef get_file_map(img_dir):\n    print(f\"Mapping files in {img_dir}...\")\n    if not os.path.exists(img_dir): return {}\n    return {os.path.splitext(f)[0]: f for f in os.listdir(img_dir) \n            if f.lower().endswith(('.png', '.jpg', '.jpeg'))}\n\n# --- 3. AUGMENTATIONS ---\ndef get_transforms(data):\n    if data == 'train':\n        return A.Compose([\n            A.Resize(CONF['image_size'], CONF['image_size']),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.RandomRotate90(p=0.5),\n            # Distortions to mimic sensor noise / heat shimmer\n            A.GridDistortion(num_steps=5, distort_limit=0.05, p=0.3),\n            A.ColorJitter(brightness=0.1, contrast=0.1, p=0.5),\n            A.Normalize(),\n            ToTensorV2()\n        ])\n    else:\n        return A.Compose([\n            A.Resize(CONF['image_size'], CONF['image_size']),\n            A.Normalize(),\n            ToTensorV2()\n        ])\n\n# --- 4. MODEL & PHYSICS LOSS ---\nclass BiomassNet(nn.Module):\n    def __init__(self, model_name=CONF['model_name']):\n        super().__init__()\n        try:\n            self.backbone = timm.create_model(model_name, pretrained=True, num_classes=0)\n        except:\n            self.backbone = timm.create_model(model_name, pretrained=False, num_classes=0)\n        \n        self.head = nn.Sequential(\n            nn.Linear(self.backbone.num_features, 768),\n            nn.SiLU(), # Swish activation is modern standard\n            nn.Dropout(0.2),\n            nn.Linear(768, 5)\n        )\n        \n    def forward(self, x):\n        return self.head(self.backbone(x))\n\nclass BiomassLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        # Standard weights\n        self.weights = torch.tensor([0.2, 0.2, 0.1, 0.2, 1.0]).to(device)\n        self.mse = nn.MSELoss(reduction='none')\n\n    def forward(self, pred, target):\n        # 1. Weighted MSE\n        loss_mse = torch.mean(self.mse(pred, target) * self.weights)\n        \n        # 2. Physics Constraint: Total (idx 4) should equal Green(0)+Dead(1)+Clover(2)\n        # Note: Depending on dataset definition, GDM might be involved. \n        # Usually Total = Green + Dead + Clover + (others not listed)\n        # We add a soft penalty if the sum of parts deviates wildly from the predicted total\n        pred_sum = pred[:, 0] + pred[:, 1] + pred[:, 2] # Green + Dead + Clover\n        pred_total = pred[:, 4]\n        \n        loss_physics = torch.mean((pred_total - pred_sum) ** 2)\n        \n        return loss_mse + (0.1 * loss_physics)\n\n# --- 5. TRAINING HELPERS ---\ndef train_one_epoch(model, loader, optimizer, criterion, scheduler):\n    model.train()\n    running_loss = 0\n    scaler = torch.cuda.amp.GradScaler() # Mixed Precision for speed/memory\n    \n    for images, targets in loader:\n        images, targets = images.to(device), targets.to(device)\n        \n        with torch.cuda.amp.autocast():\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n            \n        optimizer.zero_grad()\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        scheduler.step()\n        \n        running_loss += loss.item()\n    return running_loss / len(loader)\n\ndef validate(model, loader, criterion):\n    model.eval()\n    running_loss = 0\n    with torch.no_grad():\n        for images, targets in loader:\n            images, targets = images.to(device), targets.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n            running_loss += loss.item()\n    return running_loss / len(loader)\n\n# --- 6. MAIN LOOP (5-FOLD CV) ---\nif __name__ == '__main__':\n    # Prepare Data\n    train_csv = os.path.join(CONF['input_dir'], 'train.csv')\n    df_full = load_and_pivot_csv(train_csv)\n    file_map = get_file_map(CONF['train_img_dir'])\n    \n    # K-Fold Setup\n    kf = KFold(n_splits=CONF['n_folds'], shuffle=True, random_state=42)\n    \n    # Store models for ensemble inference later\n    fold_models = []\n    \n    for fold, (train_idx, val_idx) in enumerate(kf.split(df_full)):\n        print(f\"\\n=== Starting Fold {fold+1}/{CONF['n_folds']} ===\")\n        \n        train_sub = df_full.iloc[train_idx]\n        val_sub = df_full.iloc[val_idx]\n        \n        train_ds = BiomassDataset(train_sub, CONF['train_img_dir'], transform=get_transforms('train'), file_map=file_map)\n        val_ds = BiomassDataset(val_sub, CONF['train_img_dir'], transform=get_transforms('valid'), file_map=file_map)\n        \n        train_loader = DataLoader(train_ds, batch_size=CONF['batch_size'], shuffle=True, num_workers=CONF['num_workers'], pin_memory=True)\n        val_loader = DataLoader(val_ds, batch_size=CONF['batch_size'], shuffle=False, num_workers=CONF['num_workers'], pin_memory=True)\n        \n        model = BiomassNet().to(device)\n        optimizer = torch.optim.AdamW(model.parameters(), lr=CONF['lr'], weight_decay=1e-5)\n        criterion = BiomassLoss()\n        \n        # Cosine Schedule with Warmup\n        scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(\n            optimizer, T_0=CONF['epochs'], T_mult=1, eta_min=1e-6\n        )\n        \n        best_loss = float('inf')\n        best_model_path = f\"model_fold_{fold}.pth\"\n        \n        for epoch in range(CONF['epochs']):\n            train_loss = train_one_epoch(model, train_loader, optimizer, criterion, scheduler)\n            val_loss = validate(model, val_loader, criterion)\n            \n            print(f\"Fold {fold+1} | Epoch {epoch+1} | Train: {train_loss:.4f} | Val: {val_loss:.4f}\")\n            \n            if val_loss < best_loss:\n                best_loss = val_loss\n                torch.save(model.state_dict(), best_model_path)\n                \n        # Reload best model for this fold and clear memory\n        model.load_state_dict(torch.load(best_model_path))\n        fold_models.append(model)\n        \n        del train_ds, val_ds, train_loader, val_loader\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    # --- 7. INFERENCE WITH TTA (TEST TIME AUGMENTATION) ---\n    print(\"\\n=== Starting Inference with TTA ===\")\n    sample_sub = os.path.join(CONF['input_dir'], 'sample_submission.csv')\n    df_test = load_and_pivot_csv(sample_sub, is_test=True)\n    test_file_map = get_file_map(CONF['test_img_dir'])\n    \n    test_ds = BiomassDataset(df_test, CONF['test_img_dir'], transform=get_transforms('valid'), mode='test', file_map=test_file_map)\n    test_loader = DataLoader(test_ds, batch_size=CONF['batch_size'], shuffle=False, num_workers=CONF['num_workers'])\n    \n    final_preds = []\n    sample_ids = []\n    \n    with torch.no_grad():\n        for images, ids in tqdm(test_loader):\n            images = images.to(device)\n            batch_preds = torch.zeros((images.shape[0], 5)).to(device)\n            \n            # Simple TTA: Original + Horizontal Flip\n            # Average predictions from all 5 fold models\n            for model in fold_models:\n                model.eval()\n                \n                # 1. Predict Original\n                p1 = model(images)\n                \n                # 2. Predict Horizontal Flip\n                p2 = model(torch.flip(images, [3])) \n                \n                # Average TTA\n                batch_preds += (p1 + p2) / 2\n            \n            # Average over Folds\n            batch_preds /= CONF['n_folds']\n            \n            # Post-process\n            batch_preds = torch.relu(batch_preds).cpu().numpy() # Clip negative values\n            \n            final_preds.append(batch_preds)\n            sample_ids.extend(ids)\n            \n    # --- 8. SUBMISSION FORMATTING ---\n    final_preds = np.concatenate(final_preds, axis=0)\n    submission_rows = []\n    \n    for i, sid in enumerate(sample_ids):\n        for j, col in enumerate(CONF['target_cols']):\n            submission_rows.append({\n                'sample_id': f\"{sid}__{col}\",\n                'target': final_preds[i][j]\n            })\n            \n    sub_df = pd.DataFrame(submission_rows)\n    sub_df.to_csv('submission.csv', index=False)\n    print(\"Ensemble Submission Saved!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T01:07:10.977575Z","iopub.execute_input":"2025-12-04T01:07:10.978002Z","iopub.status.idle":"2025-12-04T08:28:36.554409Z","shell.execute_reply.started":"2025-12-04T01:07:10.977969Z","shell.execute_reply":"2025-12-04T08:28:36.552585Z"}},"outputs":[],"execution_count":null}]}