{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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-10-31T06:07:25.313702Z","iopub.execute_input":"2025-10-31T06:07:25.314483Z","iopub.status.idle":"2025-10-31T06:07:26.065632Z","shell.execute_reply.started":"2025-10-31T06:07:25.314445Z","shell.execute_reply":"2025-10-31T06:07:26.064872Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport random\nimport warnings\nfrom typing import Optional, List, Dict, Any, Tuple\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nimport timm\nfrom timm.data import resolve_data_config\nfrom timm.data.transforms_factory import create_transform\n\nimport pytorch_lightning as pl\nfrom pytorch_lightning.callbacks import EarlyStopping, LearningRateMonitor, ModelCheckpoint\nfrom pytorch_lightning.loggers import CSVLogger\nimport torchmetrics\n\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder\nfrom sklearn.feature_extraction.text import CountVectorizer\nfrom sklearn.compose import ColumnTransformer\nfrom sklearn.pipeline import Pipeline\n\n# Suppress warnings\nwarnings.filterwarnings(\"ignore\")\n\nprint(f\"PyTorch: {torch.__version__}\")\nprint(f\"Lightning: {pl.__version__}\")\nprint(f\"TIMM: {timm.__version__}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T06:07:26.066679Z","iopub.execute_input":"2025-10-31T06:07:26.067047Z","iopub.status.idle":"2025-10-31T06:07:49.762612Z","shell.execute_reply.started":"2025-10-31T06:07:26.067027Z","shell.execute_reply":"2025-10-31T06:07:49.761927Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"PATH_DATA = '/kaggle/input/csiro-biomass'\nPATH_TRAIN_CSV = os.path.join(PATH_DATA, 'train.csv')\nPATH_TEST_CSV = os.path.join(PATH_DATA, 'test.csv')\n\n# --- Competition-Specific Constants ---\n# CRITICAL: This mapping must be consistent.\nTARGET_MAP = {\n    'Dry_Total_g': 0,\n    'GDM_g': 1,\n    'Dry_Green_g': 2,\n    'Dry_Dead_g': 3,\n    'Dry_Clover_g': 4\n}\n# CRITICAL: These are the competition weights.\nTARGET_WEIGHTS = torch.tensor([0.5, 0.2, 0.1, 0.1, 0.1])\n\n# --- Model & Training Hyperparameters ---\nCONFIG = {\n    \"n_splits\": 5,\n    \"max_epochs\": 20, # Increase for better score (e.g., 30-50)\n    \"batch_size\": 32,\n    \"img_size\": (256, 256),\n    \"model_name\": \"efficientnet_b4\", # A strong, balanced backbone\n    \"learning_rate\": 1e-4,\n    \"precision\": \"16-mixed\", # Use \"32-true\" if 16-mixed is unstable\n    \"seed\": 42\n}\n\n# --- Set Seed for Reproducibility ---\npl.seed_everything(CONFIG[\"seed\"], workers=True)\n\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T06:07:49.763253Z","iopub.execute_input":"2025-10-31T06:07:49.763673Z","iopub.status.idle":"2025-10-31T06:07:49.780777Z","shell.execute_reply.started":"2025-10-31T06:07:49.763645Z","shell.execute_reply":"2025-10-31T06:07:49.780179Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ### 3. Custom Loss Function (Part 3) ###\n\nclass WeightedMSELoss(nn.Module):\n    \"\"\"\n    Implements a Weighted Mean Squared Error loss.\n    The weights are applied based on the target_index.\n    \"\"\"\n    def __init__(self, weights: torch.Tensor):\n        super(WeightedMSELoss, self).__init__()\n        # Ensure weights are a buffer, so they move to the correct device\n        self.register_buffer('weights', weights)\n\n    def forward(self, y_pred: torch.Tensor, y_true: torch.Tensor, target_index: torch.Tensor) -> torch.Tensor:\n        # y_pred, y_true, target_index all have shape (batch_size,)\n        \n        # 1. Calculate Squared Error\n        se = (y_pred - y_true) ** 2\n        \n        # 2. Get the specific weight for each sample in the batch\n        # self.weights[target_index] uses advanced indexing to create\n        # a tensor of shape (batch_size,) containing the correct weight for each sample.\n        sample_weights = self.weights[target_index]\n        \n        # 3. Calculate Weighted Squared Error\n        weighted_se = se * sample_weights\n        \n        # 4. Return the mean\n        loss = torch.mean(weighted_se)\n        return loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T06:07:49.782281Z","iopub.execute_input":"2025-10-31T06:07:49.782480Z","iopub.status.idle":"2025-10-31T06:07:49.798013Z","shell.execute_reply.started":"2025-10-31T06:07:49.782464Z","shell.execute_reply":"2025-10-31T06:07:49.797283Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ### 4. Multi-Modal Dataset (Part 1) ###\n\nclass BiomassDataset(Dataset):\n    \"\"\"\n    Serves (Image, Tabular Features, Target, Target Index) for training/validation.\n    Serves (Image, Tabular Features, Sample ID) for testing.\n    \"\"\"\n    def __init__(self, df: pd.DataFrame, path_img: str, tabular_cols: List[str], \n                 transforms=None, mode: str = 'train'):\n        self.df = df.reset_index(drop=True)\n        self.path_img = path_img\n        self.tabular_cols = tabular_cols\n        self.transforms = transforms\n        self.mode = mode\n\n    def __len__(self) -> int:\n        return len(self.df)\n\n    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor, Any, ...]:\n        row = self.df.iloc[idx]\n        \n        # 1. Load Image\n        img_path = os.path.join(self.path_img, row['image_path'])\n        img = Image.open(img_path).convert('RGB')\n        if self.transforms:\n            img = self.transforms(img)\n\n        # 2. Get Tabular Features\n        tab_features = torch.tensor(row[self.tabular_cols].values.astype(np.float32))\n\n        if self.mode == 'test':\n            return img, tab_features, row['sample_id'] # Return sample_id for submission\n\n        # 3. Get Target\n        target = torch.tensor(row['target'], dtype=torch.float32)\n        \n        # 4. Get Target Index (for loss)\n        target_index = torch.tensor(row['target_index'], dtype=torch.long)\n        \n        return img, tab_features, target, target_index","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T06:07:49.798769Z","iopub.execute_input":"2025-10-31T06:07:49.799527Z","iopub.status.idle":"2025-10-31T06:07:49.821458Z","shell.execute_reply.started":"2025-10-31T06:07:49.799508Z","shell.execute_reply":"2025-10-31T06:07:49.820899Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ### 5. Multi-Modal DataModule ###\n\nclass BiomassDataModule(pl.LightningDataModule):\n    def __init__(self, \n                 data_path: str, \n                 batch_size: int = 32, \n                 img_size: Tuple[int, int] = (224, 224), \n                 fold: int = 0, \n                 n_splits: int = 5):\n        super().__init__()\n        self.data_path = data_path\n        self.batch_size = batch_size\n        self.img_size = img_size\n        self.fold = fold\n        self.n_splits = n_splits\n        self._num_workers = os.cpu_count() or 0\n        \n        # ImageNet mean/std\n        IMAGENET_MEAN = [0.485, 0.456, 0.406]\n        IMAGENET_STD = [0.229, 0.224, 0.225]\n\n        # Define train/val transforms\n        self.train_transforms = transforms.Compose([\n            transforms.RandomResizedCrop(self.img_size, scale=(0.8, 1.0)),\n            transforms.RandomHorizontalFlip(),\n            transforms.RandomVerticalFlip(),\n            transforms.RandomRotation(degrees=30),\n            transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n        ])\n        \n        self.val_transforms = transforms.Compose([\n            transforms.Resize(self.img_size),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n        ])\n        \n        # Placeholders\n        self.df = None\n        self.test_df = None\n        self.train_df = None\n        self.val_df = None\n        self.preprocessor = None\n        self.tabular_cols = None\n        self.tabular_dim = None\n\n    # **ALL FIXES ARE HERE**\n    def _feature_engineer(self, df: pd.DataFrame) -> pd.DataFrame:\n        \"\"\"Applies feature engineering to the dataframe.\"\"\"\n        \n        # Fix 1: Handle missing 'Sampling_Date'\n        if 'Sampling_Date' in df.columns:\n            df['Sampling_Date'] = pd.to_datetime(df['Sampling_Date'])\n            df['dayofyear'] = df['Sampling_Date'].dt.dayofyear\n            df['month'] = df['Sampling_Date'].dt.month\n        else:\n            df['dayofyear'] = 0\n            df['month'] = 0\n            \n        # Fix 2: Handle missing numerical features\n        if 'Pre_GSHH_NDVI' not in df.columns:\n            df['Pre_GSHH_NDVI'] = 0\n        else:\n            df['Pre_GSHH_NDVI'] = df['Pre_GSHH_NDVI'].fillna(0)\n            \n        if 'Height_Ave_cm' not in df.columns:\n            df['Height_Ave_cm'] = 0\n        else:\n            df['Height_Ave_cm'] = df['Height_Ave_cm'].fillna(0)\n\n        # Create the target index for the loss function\n        if 'target_name' in df.columns:\n            df['target_index'] = df['target_name'].map(TARGET_MAP)\n        \n        # Fix 3: Handle missing 'Species'\n        if 'Species' not in df.columns:\n            df['Species'] = 'unknown' # Create placeholder column\n        else:\n            df['Species'] = df['Species'].fillna('unknown') # Fill NaNs\n            \n        # Fix 4: Handle missing 'State'\n        if 'State' not in df.columns:\n            df['State'] = 'unknown' # Create placeholder column\n        else:\n            df['State'] = df['State'].fillna('unknown') # Fill NaNs\n            \n        return df\n\n    def setup(self, stage: Optional[str] = None):\n        if self.df is None:\n            # --- Load and Process Train Data ---\n            self.df = pd.read_csv(os.path.join(self.data_path, 'train.csv'))\n            self.df = self._feature_engineer(self.df)\n            \n            # --- Define features ---\n            # These must match what _feature_engineer creates\n            numerical_features = ['Pre_GSHH_NDVI', 'Height_Ave_cm', 'dayofyear', 'month']\n            categorical_features = ['State', 'target_name'] # target_name is a feature!\n            text_features = 'Species' # Handled separately\n\n            # --- Build Preprocessing Pipeline ---\n            numeric_transformer = Pipeline(steps=[('scaler', StandardScaler())])\n            categorical_transformer = Pipeline(steps=[('onehot', OneHotEncoder(handle_unknown='ignore'))])\n            species_transformer = Pipeline(steps=[\n                ('count', CountVectorizer(tokenizer=lambda x: x.split('_'), max_features=50))\n            ])\n            \n            # Combine all transformers\n            self.preprocessor = ColumnTransformer(\n                transformers=[\n                    ('num', numeric_transformer, numerical_features),\n                    ('cat', categorical_transformer, categorical_features),\n                    ('species', species_transformer, text_features)\n                ],\n                remainder='drop'\n            )\n            \n            # Fit the preprocessor on the *entire* training data\n            all_tabular_features = self.preprocessor.fit_transform(self.df)\n            \n            # Get feature names after transformation\n            cat_names = self.preprocessor.named_transformers_['cat'].named_steps['onehot'].get_feature_names_out(categorical_features)\n            spec_names = self.preprocessor.named_transformers_['species'].named_steps['count'].get_feature_names_out()\n            self.tabular_cols = numerical_features + list(cat_names) + list(spec_names)\n            self.tabular_dim = len(self.tabular_cols) # Save dimension for model\n            \n            # Create a new DataFrame with processed features\n            processed_tab_df = pd.DataFrame(all_tabular_features.toarray(), columns=self.tabular_cols)\n            \n            # Re-combine with essential columns\n            essential_cols_df = self.df[['image_path', 'target', 'target_index']].reset_index(drop=True)\n            \n            self.df = pd.concat([\n                essential_cols_df,\n                processed_tab_df.reset_index(drop=True)\n            ], axis=1)\n\n        # --- Create Cross-Validation Splits (Part 3) ---\n        gkf = GroupKFold(n_splits=self.n_splits)\n        groups = self.df['image_path'] # Group by image path\n        splits = list(gkf.split(X=self.df, y=self.df['target'], groups=groups))\n        \n        train_idx, val_idx = splits[self.fold]\n        self.train_df = self.df.iloc[train_idx]\n        self.val_df = self.df.iloc[val_idx]\n\n        # --- Load and Process Test Data ---\n        if stage == 'test' or self.test_df is None:\n            self.test_df = pd.read_csv(os.path.join(self.data_path, 'test.csv'))\n            # This will now work, as _feature_engineer handles all missing columns\n            self.test_df = self._feature_engineer(self.test_df) \n            \n            # Transform test data *using the fitted preprocessor*\n            processed_test_tab = self.preprocessor.transform(self.test_df)\n            processed_test_df = pd.DataFrame(processed_test_tab.toarray(), columns=self.tabular_cols)\n            \n            # **FIX 5:** Use self.test_df, not self.df\n            self.test_df = pd.concat([\n                self.test_df[['image_path', 'sample_id']].reset_index(drop=True),\n                processed_test_df.reset_index(drop=True)\n            ], axis=1)\n\n    def train_dataloader(self) -> DataLoader:\n        train_dataset = BiomassDataset(\n            self.train_df, self.data_path, self.tabular_cols, \n            transforms=self.train_transforms, mode='train'\n        )\n        return DataLoader(\n            train_dataset, batch_size=self.batch_size, shuffle=True, \n            num_workers=self._num_workers, pin_memory=True\n        )\n\n    def val_dataloader(self) -> DataLoader:\n        val_dataset = BiomassDataset(\n            self.val_df, self.data_path, self.tabular_cols, \n            transforms=self.val_transforms, mode='train'\n        )\n        return DataLoader(\n            val_dataset, batch_size=self.batch_size, shuffle=False, \n            num_workers=self._num_workers, pin_memory=True\n        )\n\n    def test_dataloader(self) -> DataLoader:\n        if self.test_df is None:\n            self.setup(stage='test')\n            \n        test_dataset = BiomassDataset(\n            self.test_df, self.data_path, self.tabular_cols, \n            transforms=self.val_transforms, mode='test'\n        )\n        return DataLoader(\n            test_dataset, batch_size=self.batch_size, shuffle=False, \n            num_workers=self._num_workers, pin_memory=True\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T06:07:49.822233Z","iopub.execute_input":"2025-10-31T06:07:49.822476Z","iopub.status.idle":"2025-10-31T06:07:49.850240Z","shell.execute_reply.started":"2025-10-31T06:07:49.822452Z","shell.execute_reply":"2025-10-31T06:07:49.849596Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassRegressionModel(pl.LightningModule):\n    def __init__(self, \n                 model_name: str = \"efficientnet_b4\", \n                 pretrained: bool = True, \n                 tabular_dim: int = 0, \n                 learning_rate: float = 1e-4,\n                 scheduler_T_max: int = 10):\n        \n        super().__init__()\n        # This logs all hparams (model_name, learning_rate, etc.)\n        self.save_hyperparameters()\n\n        # 1. Image Tower\n        self.backbone = timm.create_model(\n            self.hparams.model_name, \n            pretrained=self.hparams.pretrained, \n            num_classes=0 # Remove classifier head\n        )\n        num_image_features = self.backbone.num_features\n\n        # 2. Tabular Tower\n        self.tabular_tower = nn.Sequential(\n            nn.Linear(self.hparams.tabular_dim, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 256),\n        )\n\n        # 3. Fusion & Final Head\n        fused_dim = num_image_features + 256\n        self.final_head = nn.Sequential(\n            nn.Linear(fused_dim, 128),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, 1), # Single output for regression\n        )\n        \n        # 4. Custom Loss Function\n        self.criterion = WeightedMSELoss(weights=TARGET_WEIGHTS)\n        \n        # 5. Metrics\n        self.val_rmse = torchmetrics.MeanSquaredError(squared=False)\n\n    def forward(self, image: torch.Tensor, tabular: torch.Tensor) -> torch.Tensor:\n        # 1. Process Image\n        image_features = self.backbone(image)\n        \n        # 2. Process Tabular\n        tabular_features = self.tabular_tower(tabular)\n        \n        # 3. Fuse\n        fused_features = torch.cat([image_features, tabular_features], dim=1)\n        \n        # 4. Predict\n        output = self.final_head(fused_features)\n        return output.squeeze(-1) # Squeeze last dim to get (batch_size,)\n\n    def training_step(self, batch: Tuple[torch.Tensor, ...], batch_idx: int) -> torch.Tensor:\n        img, tab, target, target_index = batch\n        \n        pred = self(img, tab)\n        loss = self.criterion(pred, target, target_index)\n        \n        self.log('train_loss', loss, on_step=True, on_epoch=True, prog_bar=True, \n                 batch_size=img.size(0))\n        return loss\n\n    def validation_step(self, batch: Tuple[torch.Tensor, ...], batch_idx: int):\n        img, tab, target, target_index = batch\n        \n        pred = self(img, tab)\n        \n        loss = self.criterion(pred, target, target_index)\n        self.val_rmse(pred, target)\n        \n        self.log('val_loss', loss, on_epoch=True, prog_bar=True, \n                 batch_size=img.size(0))\n        self.log('val_rmse', self.val_rmse, on_epoch=True, prog_bar=True, \n                 batch_size=img.size(0))\n\n    def predict_step(self, batch: Tuple[torch.Tensor, ...], batch_idx: int, \n                     dataloader_idx: int = 0) -> Tuple[List[str], torch.Tensor]:\n        \"\"\"\n        Implements 4-way Test Time Augmentation (TTA)\n        \"\"\"\n        img, tab, sample_id = batch\n        \n        # 1. Standard Prediction\n        pred_orig = self(img, tab)\n        \n        # 2. Horizontal Flip Prediction\n        img_hf = torch.flip(img, dims=[3])\n        pred_hf = self(img_hf, tab)\n        \n        # 3. Vertical Flip Prediction\n        img_vf = torch.flip(img, dims=[2])\n        pred_vf = self(img_vf, tab)\n        \n        # 4. Horizontal + Vertical Flip Prediction\n        img_hvf = torch.flip(img, dims=[2, 3])\n        pred_hvf = self(img_hvf, tab)\n        \n        # 5. Average Predictions\n        final_pred = (pred_orig + pred_hf + pred_vf + pred_hvf) / 4.0\n        \n        return sample_id, final_pred\n\n    def configure_optimizers(self) -> Dict[str, Any]:\n        optimizer = torch.optim.AdamW(self.parameters(), lr=self.hparams.learning_rate)\n        \n        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n            optimizer, \n            T_max=self.hparams.scheduler_T_max, \n            eta_min=1e-6\n        )\n        \n        return {\n            \"optimizer\": optimizer,\n            \"lr_scheduler\": {\n                \"scheduler\": scheduler,\n                \"interval\": \"epoch\",\n                \"monitor\": \"val_loss\",\n            }\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T06:07:49.850912Z","iopub.execute_input":"2025-10-31T06:07:49.851424Z","iopub.status.idle":"2025-10-31T06:07:49.876543Z","shell.execute_reply.started":"2025-10-31T06:07:49.851398Z","shell.execute_reply":"2025-10-31T06:07:49.876011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_predictions = []\n\nprint(\"--- 🚀 Starting {CONFIG['n_splits']}-Fold Cross-Validation ---\")\n\nfor fold in range(CONFIG['n_splits']):\n    print(f\"\\n--- Fold {fold+1}/{CONFIG['n_splits']} ---\")\n    \n    # 1. Initialize DataModule for the current fold\n    data_module = BiomassDataModule(\n        data_path=PATH_DATA, \n        batch_size=CONFIG['batch_size'], \n        img_size=CONFIG['img_size'],\n        fold=fold, \n        n_splits=CONFIG['n_splits']\n    )\n    \n    # CRITICAL: setup() must be called to fit preprocessors and get tabular_dim\n    data_module.setup() \n    \n    # 2. Initialize Model\n    model = BiomassRegressionModel(\n        model_name=CONFIG['model_name'],\n        tabular_dim=data_module.tabular_dim, # Get dim from datamodule\n        learning_rate=CONFIG['learning_rate'],\n        scheduler_T_max=CONFIG['max_epochs'] \n    )\n\n    # 3. Callbacks\n    checkpoint_callback = ModelCheckpoint(\n        monitor='val_loss',\n        dirpath='checkpoints/',\n        filename=f\"model_fold_{fold}\",\n        save_top_k=1,\n        mode='min'\n    )\n    early_stop_callback = EarlyStopping(\n        monitor='val_loss', \n        patience=5, \n        mode='min'\n    )\n    \n    # 4. Logger\n    logger = CSVLogger(\"logs\", name=f\"fold_{fold}\")\n\n    # 5. Trainer\n    trainer = pl.Trainer(\n        max_epochs=CONFIG['max_epochs'],\n        accelerator='auto',\n        devices=1,\n        logger=logger,\n        callbacks=[checkpoint_callback, early_stop_callback],\n        precision=CONFIG['precision'],\n        deterministic=True # For reproducibility\n    )\n    \n    # 6. Fit\n    trainer.fit(model, data_module)\n    \n    # 7. Predict on Test Set\n    print(f\"--- Predicting on Test Set for Fold {fold+1} (with TTA) ---\")\n    test_preds = trainer.predict(\n        model, \n        data_module.test_dataloader(), \n        ckpt_path='best' # Load best checkpoint\n    )\n    \n    # Aggregate predictions from all test batches\n    all_sample_ids = []\n    all_preds = []\n    for batch_sample_ids, batch_preds in test_preds:\n        all_sample_ids.extend(batch_sample_ids)\n        all_preds.append(batch_preds.cpu().numpy())\n        \n    all_preds = np.concatenate(all_preds)\n    \n    fold_pred_df = pd.DataFrame(\n        {'sample_id': all_sample_ids, f'target_fold_{fold}': all_preds}\n    )\n    test_predictions.append(fold_pred_df.set_index('sample_id'))\n    \n    print(f\"--- ✅ Fold {fold+1} Complete ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T06:07:49.877373Z","iopub.execute_input":"2025-10-31T06:07:49.877549Z","iopub.status.idle":"2025-10-31T07:25:24.411782Z","shell.execute_reply.started":"2025-10-31T06:07:49.877529Z","shell.execute_reply":"2025-10-31T07:25:24.410820Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ### 8. Ensembling & Submission (Part 4) ###\n\nprint(\"---  Ensembling Predictions ---\")\n\n# Concatenate all fold predictions along the column axis\nall_fold_preds = pd.concat(test_predictions, axis=1)\n\n# Calculate the mean prediction across all folds\nall_fold_preds['target'] = all_fold_preds.mean(axis=1)\n\n# Reset index to get sample_id back as a column\nsubmission_df = all_fold_preds.reset_index()[['sample_id', 'target']]\n\n# IMPORTANT: Clip negative predictions, as biomass cannot be negative\nsubmission_df['target'] = submission_df['target'].clip(lower=0)\n\n# Save to submission file\nsubmission_df.to_csv('submission.csv', index=False)\n\nprint(\"Submission file created successfully!\")\ndisplay(submission_df.head())\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T07:25:24.413068Z","iopub.execute_input":"2025-10-31T07:25:24.413452Z","iopub.status.idle":"2025-10-31T07:25:24.456422Z","shell.execute_reply.started":"2025-10-31T07:25:24.413428Z","shell.execute_reply":"2025-10-31T07:25:24.455910Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!head submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T07:25:24.458302Z","iopub.execute_input":"2025-10-31T07:25:24.458678Z","iopub.status.idle":"2025-10-31T07:25:24.643321Z","shell.execute_reply.started":"2025-10-31T07:25:24.458660Z","shell.execute_reply":"2025-10-31T07:25:24.642614Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}