{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","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"},{"sourceId":4537,"sourceType":"modelInstanceVersion","modelInstanceId":3329,"modelId":986}],"dockerImageVersionId":31234,"isInternetEnabled":false,"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":"2026-01-15T13:47:53.891963Z","iopub.execute_input":"2026-01-15T13:47:53.892606Z","iopub.status.idle":"2026-01-15T13:47:55.478645Z","shell.execute_reply.started":"2026-01-15T13:47:53.892578Z","shell.execute_reply":"2026-01-15T13:47:55.477821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#!pip install transformers==4.57.1\n\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom sklearn.model_selection import GroupKFold\nfrom tqdm import tqdm\nimport torchvision\nimport os\nfrom torchvision.datasets import ImageFolder\nfrom torch.utils.data import DataLoader, Subset, Dataset\nimport torchvision.transforms as transforms\nfrom PIL import Image\n\ntry:\n    from transformers import AutoModel, AutoConfig\nexcept ImportError:\n    print(\"Error: Transformers library not found. Please install it or check your environment.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:47:55.479706Z","iopub.execute_input":"2026-01-15T13:47:55.481199Z","iopub.status.idle":"2026-01-15T13:48:08.837332Z","shell.execute_reply.started":"2026-01-15T13:47:55.481170Z","shell.execute_reply":"2026-01-15T13:48:08.836688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mean = [0.485, 0.456, 0.406]\nstd = [0.229, 0.224, 0.225]\n\n# --- CONFIGURATION ---\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nCACHE_DIR = \"/kaggle/working/features\"\nIMAGE_DIR = \"/kaggle/input/csiro-biomass/train\"\nTEST_IMAGE_DIR = \"/kaggle/input/csiro-biomass/test\"\nMODEL_PATH = \"/kaggle/input/dinov2/pytorch/giant/1\"\n\n\nMODEL_FILE = \"best_biomass_model.pth\"\nSUBMISSION_FILE = \"/kaggle/working/submission.csv\"\n\n# Create cache directory if it doesn't exist\nos.makedirs(CACHE_DIR, exist_ok=True)\n\nEPOCHS = 500\nBATCH_SIZE = 32\nLR = 1e-3\n\n\n# ---- VALIDATION STRATEGY CONFIGURATION ----\n# Set to 'random' for train_test_split or 'logo' for Leave-One-Group-Out\nVALIDATION_STRATEGY = 'random'  # Options: 'random', 'logo'\nLOGO_EPOCHS = 40  # Fewer epochs per fold for LOGO (many folds)\n\n\n# Weights from your description\nTARGET_WEIGHTS = {\n    \"Dry_Green_g\": 0.1,\n    \"Dry_Dead_g\": 0.1,\n    \"Dry_Clover_g\": 0.01,\n    \"GDM_g\": 0.2,\n    \"Dry_Total_g\": 0.5\n}\nTARGET_COLS = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:48:08.839248Z","iopub.execute_input":"2026-01-15T13:48:08.839711Z","iopub.status.idle":"2026-01-15T13:48:08.932745Z","shell.execute_reply.started":"2026-01-15T13:48:08.839682Z","shell.execute_reply":"2026-01-15T13:48:08.932101Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Loading model from {MODEL_PATH}...\")\n\n# Load configuration first to ensure it's valid\ntry:\n    config = AutoConfig.from_pretrained(MODEL_PATH)\n    backbone = AutoModel.from_pretrained(MODEL_PATH, config=config)\nexcept OSError:\n    print(\"Could not load local model. Trying internet download (will fail if offline)...\")\n    backbone = AutoModel.from_pretrained(\"facebookresearch/dinov2-giant\")\nexcept NameError:\n    # If AutoModel fails, fallback to torch hub (alternative method)\n    print(\"Transformers failed. Trying Torch Hub...\")\n    backbone = torch.hub.load('facebookresearch/dinov2', 'dinov2_vitg14')\n    \nbackbone.to(DEVICE)\nbackbone.eval()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:48:08.933812Z","iopub.execute_input":"2026-01-15T13:48:08.934213Z","iopub.status.idle":"2026-01-15T13:48:53.418634Z","shell.execute_reply.started":"2026-01-15T13:48:08.934177Z","shell.execute_reply":"2026-01-15T13:48:53.418042Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preprocess = transforms.Compose([\n    transforms.RandomResizedCrop(224),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),\n    transforms.RandomRotation(degrees=15),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:48:53.419529Z","iopub.execute_input":"2026-01-15T13:48:53.420048Z","iopub.status.idle":"2026-01-15T13:48:53.424470Z","shell.execute_reply.started":"2026-01-15T13:48:53.420022Z","shell.execute_reply":"2026-01-15T13:48:53.423732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ImageOnlyDataset(Dataset):\n    def __init__(self, image_dir, transform=None):\n        self.image_ids = [f.split('.')[0] for f in os.listdir(image_dir) if f.endswith('.jpg')]\n        self.image_dir = image_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, idx):\n        img_id = self.image_ids[idx]\n        img_path = os.path.join(self.image_dir, f\"{img_id}.jpg\")\n        image = Image.open(img_path).convert('RGB')\n        if self.transform:\n            image = self.transform(image)\n        return image, img_id\n\n# --- 3. EXTRACTION LOOP ---\ndef extract_and_cache(directory, desc=\"Extracting\"):\n    # Filter out images that are already cached\n    # We check if ID123.pt exists in CACHE_DIR\n    all_files = [f for f in os.listdir(directory) if f.endswith('.jpg')]\n    to_process = [f for f in all_files if not os.path.exists(f\"{CACHE_DIR}/{f.split('.')[0]}.pt\")]\n    \n    if len(to_process) == 0:\n        print(f\"All images in {desc} are already cached!\")\n        return\n\n    # Create a temporary dataset for just the missing files\n    # (Simplified for this snippet: We just loop the subset)\n    \n    print(f\"Processing {len(to_process)} new images for {desc}...\")\n    \n    # Process in batches for speed\n    batch_size = 16\n    \n    # Helper to chunk the list\n    chunks = [to_process[i:i + batch_size] for i in range(0, len(to_process), batch_size)]\n    \n    with torch.no_grad():\n        for chunk in tqdm(chunks, desc=desc):\n            images = []\n            ids = []\n            \n            # Load batch\n            for filename in chunk:\n                img_id = filename.split('.')[0]\n                img_path = os.path.join(directory, filename)\n                img = Image.open(img_path).convert('RGB')\n                images.append(preprocess(img))\n                ids.append(img_id)\n            \n            # Stack and Move to GPU\n            batch_tensor = torch.stack(images).to(DEVICE)\n            \n            # Forward Pass\n            outputs = backbone(batch_tensor)\n            \n            # Get Embeddings\n            # method A: CLS Token (outputs.last_hidden_state[:, 0, :])\n            # method B: Average Pooling (Better for Biomass)\n            embeddings = outputs.last_hidden_state.mean(dim=1) # Shape: (Batch, 1536)\n            \n            # Save to Disk\n            for i, img_id in enumerate(ids):\n                # Save as a CPU tensor to save GPU memory\n                torch.save(embeddings[i].cpu(), f\"{CACHE_DIR}/{img_id}.pt\")\n\n# Run Extraction for Train and Test\nextract_and_cache(IMAGE_DIR, desc=\"Train Images\")\nextract_and_cache(TEST_IMAGE_DIR, desc=\"Test Images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:48:53.425386Z","iopub.execute_input":"2026-01-15T13:48:53.425674Z","iopub.status.idle":"2026-01-15T13:50:17.932915Z","shell.execute_reply.started":"2026-01-15T13:48:53.425636Z","shell.execute_reply":"2026-01-15T13:50:17.932205Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassFeatureDataset(Dataset):\n    def __init__(self, df, feature_dict, mode='train'):\n        self.df = df\n        self.feature_dict = feature_dict\n        self.mode = mode\n        \n        if self.mode == 'train':\n            self.pivot_df = self.df.pivot(index='image_path', columns='target_name', values='target').reset_index()\n            for col in TARGET_COLS:\n                if col not in self.pivot_df.columns:\n                    self.pivot_df[col] = 0.0\n\n    def __len__(self):\n        return len(self.pivot_df)\n\n    def __getitem__(self, idx):\n        row = self.pivot_df.iloc[idx]\n        img_path = row['image_path']\n        \n        # 1. Get the feature (This is already a Tensor)\n        feature = self.feature_dict[img_path]\n        \n        if self.mode == 'train':\n            # 2. Get targets (This is a Numpy array from Pandas)\n            targets = row[TARGET_COLS].values.astype(np.float32)\n            \n            # CORRECTED RETURN:\n            # - feature: Use .clone().detach() or just .float() if you don't need a copy\n            # - targets: Use torch.from_numpy() for efficiency with arrays\n            return feature.float(), torch.from_numpy(targets)\n        else:\n            return feature.float(), img_path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:17.933921Z","iopub.execute_input":"2026-01-15T13:50:17.934238Z","iopub.status.idle":"2026-01-15T13:50:17.940872Z","shell.execute_reply.started":"2026-01-15T13:50:17.934211Z","shell.execute_reply":"2026-01-15T13:50:17.940139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassMLP(nn.Module):\n    def __init__(self, input_dim=1536, hidden_dim=512, dropout=0.6):\n        super().__init__()\n        self.network = nn.Sequential(\n            nn.Linear(input_dim, hidden_dim),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, hidden_dim // 2),\n            nn.ReLU(),\n            nn.Linear(hidden_dim // 2, 5) # Output 5 values at once!\n        )\n        \n    def forward(self, x):\n        # x shape: (Batch, 1536) -> (Batch, 5)\n        return self.network(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:17.942034Z","iopub.execute_input":"2026-01-15T13:50:17.942603Z","iopub.status.idle":"2026-01-15T13:50:17.959651Z","shell.execute_reply.started":"2026-01-15T13:50:17.942576Z","shell.execute_reply":"2026-01-15T13:50:17.958979Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass BiomassMLPAlgebra(nn.Module):\n    def __init__(self, input_dim=1536, hidden_dim=512, dropout=0.7):\n        super().__init__()\n        self.backbone = nn.Sequential(\n            nn.Linear(input_dim, hidden_dim),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, hidden_dim // 2),\n            nn.ReLU(),\n            nn.Linear(hidden_dim // 2, 3)  # 3 DOF: green, clover, dead\n        )\n\n    def forward(self, x):\n        # x: (B, 1536)\n        base = self.backbone(x)          # (B, 3)\n        green  = base[:, 0:1]            # (B, 1)\n        clover = base[:, 1:2]\n        dead   = base[:, 2:3]\n        gdm    = green + clover\n        total  = gdm + dead\n        # return in the usual order: [Dry_Green, Dry_Dead, Dry_Clover, GDM, Dry_Total]\n        out = torch.cat([green, dead, clover, gdm, total], dim=1)  # (B, 5)\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:17.962211Z","iopub.execute_input":"2026-01-15T13:50:17.962502Z","iopub.status.idle":"2026-01-15T13:50:17.974013Z","shell.execute_reply.started":"2026-01-15T13:50:17.962478Z","shell.execute_reply":"2026-01-15T13:50:17.973397Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass BiomassMLPAlgebraClover(nn.Module):\n    def __init__(self, input_dim=1536, hidden_dim=512, dropout=0.7):\n        super().__init__()\n        self.backbone = nn.Sequential(\n            nn.Linear(input_dim, hidden_dim),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, hidden_dim // 2),\n            nn.ReLU(),\n            nn.Linear(hidden_dim // 2, 3)  # 3 DOF: green, clover, dead\n        )\n\n    def forward(self, x):\n        # x: (B, 1536)\n        base = self.backbone(x)          # (B, 3)\n        green  = base[:, 0:1]            # (B, 1)\n        gdm = base[:, 1:2]\n        total   = base[:, 2:3]\n        clover    = gdm - green\n        dead  = total - gdm \n        # return in the usual order: [Dry_Green, Dry_Dead, Dry_Clover, GDM, Dry_Total]\n        out = torch.cat([green, dead, clover, gdm, total], dim=1)  # (B, 5)\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:17.974775Z","iopub.execute_input":"2026-01-15T13:50:17.975028Z","iopub.status.idle":"2026-01-15T13:50:17.991087Z","shell.execute_reply.started":"2026-01-15T13:50:17.975005Z","shell.execute_reply":"2026-01-15T13:50:17.990493Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassMultiHead(nn.Module):\n    def __init__(self, input_dim=1536, shared_dim=512, head_hidden_dim=128, dropout=0.6):\n        super().__init__()\n        \n        # 1. Shared Trunk\n        self.shared_block = nn.Sequential(\n            nn.Linear(input_dim, shared_dim),\n            nn.BatchNorm1d(shared_dim),\n            nn.ReLU(),\n            nn.Dropout(0.4)\n        )\n        \n        # 2. Independent Heads\n        self.heads = nn.ModuleList([\n            nn.Sequential(\n                # Layer 1\n                nn.Linear(shared_dim, head_hidden_dim),\n                nn.BatchNorm1d(head_hidden_dim),       # BN before Activation\n                nn.ReLU(),\n                nn.Dropout(0.7),                   # Dropout last\n                \n                # Layer 2 (The added depth)\n                nn.Linear(head_hidden_dim, head_hidden_dim),\n                nn.ReLU(),\n                nn.Dropout(0.8),                   # BN stats are safe now\n                \n                # Output\n                nn.Linear(head_hidden_dim, 1)\n            ) for _ in range(5)\n        ])\n        \n    def forward(self, x):\n        shared_features = self.shared_block(x)\n        outputs = [head(shared_features) for head in self.heads]\n        return torch.cat(outputs, dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:17.991986Z","iopub.execute_input":"2026-01-15T13:50:17.992265Z","iopub.status.idle":"2026-01-15T13:50:18.004568Z","shell.execute_reply.started":"2026-01-15T13:50:17.992240Z","shell.execute_reply":"2026-01-15T13:50:18.003943Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassMultiMLP(nn.Module):\n    def __init__(self, input_dim=1536, head_hidden_dim=128, dropout_rate=0.7):\n        super().__init__()\n        \n        # 1. No shared trunk. \n        # 2. Each head now takes input_dim directly.\n        self.heads = nn.ModuleList([\n            nn.Sequential(\n                # Layer 1: Directly from raw input\n                nn.Linear(input_dim, head_hidden_dim),\n                nn.BatchNorm1d(head_hidden_dim),\n                nn.ReLU(),\n                nn.Dropout(dropout_rate),\n                \n                # Layer 2\n                nn.Linear(head_hidden_dim, head_hidden_dim),\n                nn.ReLU(),\n                nn.Dropout(dropout_rate + 0.1), # Increased dropout for deeper layers\n                \n                # Output\n                nn.Linear(head_hidden_dim, 1)\n            ) for _ in range(5)\n        ])\n        \n    def forward(self, x):\n        # Pass the raw input 'x' to every independent head\n        outputs = [head(x) for head in self.heads]\n        return torch.cat(outputs, dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.005415Z","iopub.execute_input":"2026-01-15T13:50:18.005661Z","iopub.status.idle":"2026-01-15T13:50:18.022846Z","shell.execute_reply.started":"2026-01-15T13:50:18.005640Z","shell.execute_reply":"2026-01-15T13:50:18.022005Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassMultiMLPAlgebra(nn.Module):\n    def __init__(self, input_dim=1536, head_hidden_dim=128, dropout_rate=0.7):\n        super().__init__()\n        \n        # 1. No shared trunk. \n        # 2. Each head now takes input_dim directly.\n        self.heads = nn.ModuleList([\n            nn.Sequential(\n                # Layer 1: Directly from raw input\n                nn.Linear(input_dim, head_hidden_dim),\n                nn.BatchNorm1d(head_hidden_dim),\n                nn.ReLU(),\n                nn.Dropout(dropout_rate),\n                \n                # Layer 2\n                nn.Linear(head_hidden_dim, head_hidden_dim),\n                nn.ReLU(),\n                nn.Dropout(dropout_rate + 0.1), # Increased dropout for deeper layers\n                \n                # Output\n                nn.Linear(head_hidden_dim, 1)\n            ) for _ in range(5)\n        ])\n        \n    def forward(self, x):\n        # 1. Iterate through the list to get the 3 base outputs\n        # This creates a list of 3 tensors, each (B, 1)\n        base_outputs = [head(x) for head in self.heads]\n        \n        # 2. Extract the individual base components\n        green  = base_outputs[0]  # (B, 1)\n        clover = base_outputs[1]  # (B, 1)\n        dead   = base_outputs[2]  # (B, 1)\n        \n        # 3. Perform your Biomass Algebra\n        gdm    = green + clover\n        total  = gdm + dead\n        \n        # 4. Return in your specific order: [Green, Dead, Clover, GDM, Total]\n        out = torch.cat([green, dead, clover, gdm, total], dim=1)\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.023682Z","iopub.execute_input":"2026-01-15T13:50:18.023972Z","iopub.status.idle":"2026-01-15T13:50:18.038676Z","shell.execute_reply.started":"2026-01-15T13:50:18.023931Z","shell.execute_reply":"2026-01-15T13:50:18.038033Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_weighted_r2(y_true, y_pred, verbose=False):\n    \"\"\"\n    Calculates the Global Weighted R2 as defined in the image.\n    y_true, y_pred: numpy arrays of shape (N_samples, 5)\n    \"\"\"\n    # Weights for the 5 columns\n    weights_per_col = np.array([TARGET_WEIGHTS[col] for col in TARGET_COLS])\n    \n    # Flatten the data to (N_samples * 5) for the global calculation\n    # We repeat the row-weights for every sample\n    y_true_flat = y_true.flatten()\n    y_pred_flat = y_pred.flatten()\n    \n    # Create a weight vector matching the flattened data\n    # (Rows * 5_targets)\n    weights_flat = np.tile(weights_per_col, (y_true.shape[0], 1)).flatten()\n    \n    # 1. Calculate Global Weighted Mean (y_bar_w)\n    # Sum(w * y) / Sum(w)\n    weighted_mean = np.sum(weights_flat * y_true_flat) / np.sum(weights_flat)\n    \n    # 2. Residual Sum of Squares (SS_res)\n    # Sum(w * (y - y_hat)^2)\n    ss_res = np.sum(weights_flat * (y_true_flat - y_pred_flat)**2)\n    \n    # 3. Total Sum of Squares (SS_tot)\n    # Sum(w * (y - y_bar_w)^2)\n    ss_tot = np.sum(weights_flat * (y_true_flat - weighted_mean)**2)\n    \n    # 4. R2\n    r2_w = 1 - (ss_res / (ss_tot + 1e-8)) # epsilon for stability\n    \n    if verbose:\n        print(f\"Weighted Mean: {weighted_mean:.4f}\")\n        print(f\"SS_res: {ss_res:.4f}, SS_tot: {ss_tot:.4f}\")\n        \n    return r2_w","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.039366Z","iopub.execute_input":"2026-01-15T13:50:18.039594Z","iopub.status.idle":"2026-01-15T13:50:18.058413Z","shell.execute_reply.started":"2026-01-15T13:50:18.039573Z","shell.execute_reply":"2026-01-15T13:50:18.057816Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 5. REAL EXECUTION SETUP ---\n\n# 1. Load the CSV\nreal_train_df = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\n\n# 2. Build the Feature Cache Dictionary from Disk\nprint(\"Loading features from disk into memory...\")\nreal_features_cache = {}\n\n# We only load features for images that are in our CSV\nunique_images = real_train_df['image_path'].apply(lambda x: x.split('/')[-1].split('.')[0]).unique()\n\nfor img_id in tqdm(unique_images):\n    cache_path = f\"/kaggle/working/features/{img_id}.pt\"\n    if os.path.exists(cache_path):\n        # Load tensor\n        real_features_cache[img_id + \".jpg\"] = torch.load(cache_path) # Keys must match CSV image_path format if strictly required, or adjust logic\n    else:\n        # Fallback (should not happen if Phase 1 ran)\n        print(f\"Warning: Feature missing for {img_id}\")\n\n# NOTE: The CSV 'image_path' is like 'train/ID123.jpg'.\n# Our cache keys need to match. Let's fix the dataframe to match our simple keys or update the dict.\n# Simplest approach: Update the dataframe to just be the filename\nreal_train_df['image_path'] = real_train_df['image_path'].apply(lambda x: os.path.basename(x))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.059177Z","iopub.execute_input":"2026-01-15T13:50:18.059364Z","iopub.status.idle":"2026-01-15T13:50:18.240666Z","shell.execute_reply.started":"2026-01-15T13:50:18.059344Z","shell.execute_reply":"2026-01-15T13:50:18.240010Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport torch\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom IPython.display import clear_output\nimport os\nimport re\n\ndef plot_training_progress(history, val_preds, val_trues, final=False):\n    if not final:\n        clear_output(wait=True)\n    \n    # Calculate grid requirements\n    num_targets = len(TARGET_COLS)\n    # 3 standard plots + N target plots\n    total_plots = 3 + num_targets\n    cols = 3\n    rows = (total_plots + cols - 1) // cols  # Ceiling division\n    \n    fig = plt.figure(figsize=(18, 5 * rows))\n    \n    # 1. Training curves\n    ax1 = plt.subplot(rows, cols, 1)\n    epochs = range(len(history['train_r2']))\n    ax1.plot(epochs, history['train_r2'], 'b-', label='Train R²', linewidth=2)\n    ax1.plot(epochs, history['val_r2'], 'r-', label='Val R²', linewidth=2)\n    ax1.axvline(history['best_epoch'], color='g', linestyle='--', alpha=0.5, label=f'Best')\n    ax1.set_title('Training Progress', fontsize=14, fontweight='bold')\n    ax1.legend()\n    ax1.grid(True, alpha=0.3)\n    \n    # 2. Loss curve\n    ax2 = plt.subplot(rows, cols, 2)\n    ax2.plot(epochs, history['train_loss'], 'purple', linewidth=2)\n    ax2.set_title('Training Loss', fontsize=14, fontweight='bold')\n    ax2.grid(True, alpha=0.3)\n    \n    # 3. Overfitting indicator\n    ax3 = plt.subplot(rows, cols, 3)\n    gap = np.array(history['train_r2']) - np.array(history['val_r2'])\n    ax3.plot(epochs, gap, 'orange', linewidth=2)\n    ax3.fill_between(epochs, 0, gap, alpha=0.3, color='orange')\n    ax3.set_title('Overfitting Gap', fontsize=14, fontweight='bold')\n    ax3.grid(True, alpha=0.3)\n    \n    # 4+. Per-target predictions (Dynamic positioning)\n    for i, target_name in enumerate(TARGET_COLS):\n        ax = plt.subplot(rows, cols, i + 4) # This now works for any number of targets\n        \n        true_vals = val_trues[:, i]\n        pred_vals = val_preds[:, i]\n        \n        ax.scatter(true_vals, pred_vals, alpha=0.6, s=30, edgecolors='black', linewidth=0.5)\n        \n        # Add diagonal line\n        min_val = min(true_vals.min(), pred_vals.min())\n        max_val = max(true_vals.max(), pred_vals.max())\n        ax.plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2)\n        \n        # Calculate individual R2\n        ss_res = np.sum((true_vals - pred_vals) ** 2)\n        ss_tot = np.sum((true_vals - true_vals.mean()) ** 2)\n        r2 = 1 - (ss_res / (ss_tot + 1e-8))\n        \n        ax.set_title(f'{target_name}\\nR² = {r2:.4f}', fontsize=11, fontweight='bold')\n        ax.grid(True, alpha=0.3)\n    \n    plt.tight_layout()\n    plt.show()\n    \n    if final:\n    \n        print(\"\\n\" + \"=\"*60)\n    \n        print(\"FINAL VALIDATION RESULTS\")\n    \n        print(\"=\"*60)\n    \n        for i, target_name in enumerate(TARGET_COLS):\n    \n            true_vals = val_trues[:, i]\n            pred_vals = val_preds[:, i]\n    \n            ss_res = np.sum((true_vals - pred_vals) ** 2)\n            ss_tot = np.sum((true_vals - true_vals.mean()) ** 2)\n            r2 = 1 - (ss_res / (ss_tot + 1e-8))\n    \n            mae = np.mean(np.abs(true_vals - pred_vals))\n            rmse = np.sqrt(np.mean((true_vals - pred_vals) ** 2))\n    \n            print(f\"{target_name:15s} | R²: {r2:6.4f} | MAE: {mae:7.2f} | RMSE: {rmse:7.2f}\")\n    \n        print(\"=\"*60)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.241518Z","iopub.execute_input":"2026-01-15T13:50:18.241829Z","iopub.status.idle":"2026-01-15T13:50:18.254743Z","shell.execute_reply.started":"2026-01-15T13:50:18.241796Z","shell.execute_reply":"2026-01-15T13:50:18.254075Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\nimport torch.optim as optim\n\ndef train_model(df,model, features_cache):\n    # 1. Split Data (80% Train / 20% Unseen Validation)\n    unique_imgs = df['image_path'].unique()\n    train_imgs, val_imgs = train_test_split(unique_imgs, test_size=0.15, random_state=42)\n    \n    # Filter Dataframes\n    train_df = df[df['image_path'].isin(train_imgs)]\n    val_df = df[df['image_path'].isin(val_imgs)]\n    \n    print(f\"Training on {len(train_imgs)} images, Validating on {len(val_imgs)} images.\")\n\n    # 2. Create Loaders\n    train_ds = BiomassFeatureDataset(train_df, features_cache, mode='train')\n    val_ds = BiomassFeatureDataset(val_df, features_cache, mode='train')\n    \n    # Validation shuffle=False is standard\n    train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)\n    val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False)\n    \n    # 3. Setup Model & Optimizer\n    model = model.to(DEVICE)\n    optimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-3)\n    \n    # --- FIX 1: Removed 'verbose=True' (it is deprecated in new PyTorch) ---\n    # scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=5)\n    \n    # Weights for the loss function\n    loss_weights = torch.tensor([TARGET_WEIGHTS[c] for c in TARGET_COLS]).to(DEVICE)\n    \n    best_val_r2 = -float('inf')\n    \n    print(\"\\nEpoch | Train R2 | Val R2 | Train Loss\")\n    print(\"-\" * 45)\n\n    history = {\n        'train_r2': [],\n        'val_r2': [],\n        'train_loss': [],\n        'best_epoch': 0\n    }\n    \n    for epoch in range(EPOCHS):\n        # --- TRAINING PHASE ---\n        model.train()\n        train_loss = 0\n        \n        train_preds_all = []\n        train_trues_all = []\n        \n        for X, y in train_loader:\n            X, y = X.to(DEVICE), y.to(DEVICE)\n            \n            optimizer.zero_grad()\n            preds = model(X)\n            \n            # Weighted MSE Loss\n            loss = torch.mean(torch.sum(loss_weights * (preds - y)**2, dim=1))\n            loss.backward()\n            optimizer.step()\n            \n            train_loss += loss.item()\n            \n            train_preds_all.append(preds.detach().cpu().numpy())\n            train_trues_all.append(y.detach().cpu().numpy())\n            \n        train_preds_all = np.concatenate(train_preds_all)\n        train_trues_all = np.concatenate(train_trues_all)\n        train_r2 = calculate_weighted_r2(train_trues_all, train_preds_all)\n            \n        # --- VALIDATION PHASE ---\n        model.eval()\n        val_preds_all = []\n        val_trues_all = []\n        \n        with torch.no_grad():\n            for X, y in val_loader:\n                X, y = X.to(DEVICE), y.to(DEVICE)\n                preds = model(X)\n                val_preds_all.append(preds.cpu().numpy())\n                val_trues_all.append(y.cpu().numpy())\n        \n        val_preds_all = np.concatenate(val_preds_all)\n        val_trues_all = np.concatenate(val_trues_all)\n        val_r2 = calculate_weighted_r2(val_trues_all, val_preds_all)\n        \n        history['train_r2'].append(train_r2)\n        history['val_r2'].append(val_r2)\n        history['train_loss'].append(train_loss / len(train_loader))\n        # --- LOGGING & SAVING ---\n        \n        # Check LR before stepping to see if it changes\n        prev_lr = optimizer.param_groups[0]['lr']\n        # scheduler.step(val_r2)\n        new_lr = optimizer.param_groups[0]['lr']\n        \n        if val_r2 > best_val_r2:\n            best_val_r2 = val_r2\n            torch.save(model.state_dict(), \"best_biomass_model.pth\")\n            marker = \"*\"\n        else:\n            marker = \"\"\n\n        # --- FIX 2: Manually print if LR changes (replaces verbose=True) ---\n        if new_lr != prev_lr:\n            print(f\"  >>> Scheduler: Learning Rate reduced to {new_lr}\")\n            \n        if epoch % 1 == 0:\n            print(f\"{epoch:3d}   | {train_r2:6.4f}   | {val_r2:6.4f} | {train_loss/len(train_loader):.1f} {marker}\")\n\n        if epoch % 10 == 0 and epoch > 0:\n            plot_training_progress(history, val_preds_all, val_trues_all)\n\n    print(f\"\\nBest Validation R2: {best_val_r2:.4f} (Epoch {history['best_epoch']})\")\n\n    # Final visualization\n    plot_training_progress(history, val_preds_all, val_trues_all, final=True)\n    # --- FIX 3: Corrected variable name from 'best_r2' to 'best_val_r2' ---\n    print(f\"\\nBest Validation R2: {best_val_r2:.4f}\")\n    \n    # (Optional) Load the best model weights before returning\n    model.load_state_dict(torch.load(\"best_biomass_model.pth\"))\n    \n    return model\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.256339Z","iopub.execute_input":"2026-01-15T13:50:18.256838Z","iopub.status.idle":"2026-01-15T13:50:18.276128Z","shell.execute_reply.started":"2026-01-15T13:50:18.256805Z","shell.execute_reply":"2026-01-15T13:50:18.275477Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import GroupKFold, LeaveOneGroupOut\nimport copy\nclass BiomassFeatureDatasetLOGO(Dataset):\n    \"\"\"Dataset for LOGO cross-validation that works with pre-pivoted DataFrames.\"\"\"\n    def __init__(self, df, feature_dict, target_cols):\n        self.df = df.reset_index(drop=True)\n        self.feature_dict = feature_dict\n        self.target_cols = target_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row['image_path']\n\n        # Get the feature tensor\n        feature = self.feature_dict[img_path]\n\n        # Get targets\n        targets = row[self.target_cols].values.astype(np.float32)\n\n        return feature.float(), torch.from_numpy(targets)\n\n# ----\n# LOGO TRAINING FUNCTION\n# Leave-One-Group-Out Cross-Validation using Sampling_Date as groups\n# ----\ndef train_model_logo(df, model_class, features_cache, \n                      epochs=None, batch_size=None, lr=None):\n    \"\"\"\n    Train model using Leave-One-Group-Out cross-validation.\n    Groups are defined by Sampling_Date to prevent data leakage.\n    \n    Args:\n        df: Raw DataFrame with 'target_name' column (unpivoted format)\n        model_class: Model class (e.g., BiomassMLP)\n        features_cache: Dictionary mapping image_path to feature tensors\n        epochs: Training epochs per fold (default: LOGO_EPOCHS global)\n        batch_size: Batch size (default: BATCH_SIZE global)\n        lr: Learning rate (default: LR global)\n    \n    Returns:\n        tuple: (trained_model, oof_predictions, oof_targets, global_r2)\n    \"\"\"\n    # Use global values if parameters not provided\n    epochs = epochs if epochs is not None else LOGO_EPOCHS\n    batch_size = batch_size if batch_size is not None else BATCH_SIZE\n    lr = lr if lr is not None else LR\n    # 1. Pivot the data while preserving Sampling_Date\n    print(\"Preparing data for LOGO cross-validation...\")\n    \n    # Get metadata columns we want to preserve (before pivot)\n    meta_cols = ['image_path', 'Sampling_Date']\n    meta_cols = [c for c in meta_cols if c in df.columns]\n    \n    # Pivot targets into columns\n    pivot_df = df.pivot_table(\n        index=meta_cols,\n        columns='target_name',\n        values='target'\n    ).reset_index()\n    pivot_df.columns.name = None\n    \n    for col in TARGET_COLS:\n        if col not in pivot_df.columns:\n            pivot_df[col] = 0.0\n    \n    print(f\"Pivoted DataFrame: {len(pivot_df)} samples\")\n    \n    # 2. Setup LOGO\n    logo = LeaveOneGroupOut()\n    groups = pivot_df['Sampling_Date']\n    N_GROUPS = len(groups.unique())\n    \n    # Containers for out-of-fold predictions\n    oof_preds_final = np.zeros((len(pivot_df), 5))\n    oof_targets_final = np.zeros((len(pivot_df), 5))\n    scores_logo = []\n    \n    # Weights for loss function\n    loss_weights = torch.tensor([TARGET_WEIGHTS[c] for c in TARGET_COLS]).to(DEVICE)\n    \n    print(f\"\\n🚀 INICIANDO LEAVE-ONE-GROUP-OUT CROSS-VALIDATION\")\n    print(f\"   Total de rondas: {N_GROUPS} (Una por fecha de muestreo)\")\n    print(\"=\"*60)\n    \n    # 3. LOGO Training Loop\n    for i, (train_idx, val_idx) in enumerate(logo.split(pivot_df, groups=groups)):\n        # Subset DataFrames\n        train_sub = pivot_df.iloc[train_idx]\n        val_sub = pivot_df.iloc[val_idx]\n        \n        # Create datasets\n        ds_train = BiomassFeatureDatasetLOGO(train_sub, features_cache, TARGET_COLS)\n        ds_val = BiomassFeatureDatasetLOGO(val_sub, features_cache, TARGET_COLS)\n        \n        dl_train = DataLoader(ds_train, batch_size=batch_size, shuffle=True, drop_last=True)\n        dl_val = DataLoader(ds_val, batch_size=batch_size, shuffle=False)\n        \n        # Fresh model for each fold\n        model = model_class().to(DEVICE)\n        optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-3)\n        \n        # Training loop for this fold\n        best_loss = float('inf')\n        best_wts = None\n        \n        for epoch in range(epochs):\n            # Training phase\n            model.train()\n            for X, y in dl_train:\n                X, y = X.to(DEVICE), y.to(DEVICE)\n                optimizer.zero_grad()\n                preds = model(X)\n                loss = torch.mean(torch.sum(loss_weights * (preds - y)**2, dim=1))\n                loss.backward()\n                optimizer.step()\n            \n            # Validation phase (for early stopping / best model)\n            model.eval()\n            val_loss = 0\n            with torch.no_grad():\n                for X, y in dl_val:\n                    X, y = X.to(DEVICE), y.to(DEVICE)\n                    preds = model(X)\n                    l = torch.mean(torch.sum(loss_weights * (preds - y)**2, dim=1))\n                    val_loss += l.item()\n            \n            if val_loss < best_loss:\n                best_loss = val_loss\n                best_wts = copy.deepcopy(model.state_dict())\n        \n        # Load best weights for this fold\n        model.load_state_dict(best_wts)\n        \n        # Get predictions for the validation fold\n        preds_list, targets_list = [], []\n        model.eval()\n        with torch.no_grad():\n            for X, y in dl_val:\n                X, y = X.to(DEVICE), y.to(DEVICE)\n                preds_list.append(model(X).cpu().numpy())\n                targets_list.append(y.cpu().numpy())\n        \n        # Store OOF predictions\n        fold_preds = np.concatenate(preds_list)\n        fold_targets = np.concatenate(targets_list)\n        \n        oof_preds_final[val_idx] = fold_preds\n        oof_targets_final[val_idx] = fold_targets\n        \n        # Calculate R2 for this group\n        r2_group = calculate_weighted_r2(fold_targets, fold_preds)\n        scores_logo.append(r2_group)\n        \n        print(f\"✅ Grupo {i+1}/{N_GROUPS} | R2: {r2_group:.4f} | Samples: {len(val_idx)}\")\n    \n    # 4. Final Results\n    final_r2_logo = calculate_weighted_r2(oof_targets_final, oof_preds_final)\n    \n    print(\"=\"*60)\n    print(f\"🌟 R2 GLOBAL (LOGO): {final_r2_logo:.4f}\")\n    print(f\"   (Promedio de los grupos individuales: {np.mean(scores_logo):.4f})\")\n    print(\"=\"*60)\n    \n    # Return the last trained model (or you could ensemble them)\n    return model, oof_preds_final, oof_targets_final, final_r2_logo\n\n# ----\n# TRAINING DISPATCHER\n# Wrapper function that routes to the appropriate training strategy\n# ----\ndef train(df, model_class, features_cache, strategy=None, **kwargs):\n    \"\"\"\n    Unified training interface that dispatches to the appropriate strategy.\n    \n    Args:\n        df: Training DataFrame\n        model_class: Model class to train\n        features_cache: Feature dictionary\n        strategy: 'random' or 'logo' (default: uses VALIDATION_STRATEGY global)\n        **kwargs: Additional arguments passed to the training function\n            - For 'logo': epochs, batch_size, lr\n    \n    Returns:\n        Depends on strategy:\n        - 'random': trained model\n        - 'logo': (model, oof_preds, oof_targets, global_r2)\n    \"\"\"\n    strategy = strategy if strategy is not None else VALIDATION_STRATEGY\n    \n    if strategy == 'random':\n        print(f\"🎲 Training with RANDOM split strategy\")\n        print(\"=\"*60)\n        return train_model(df, model_class(), features_cache)\n    \n    elif strategy == 'logo':\n        print(f\"📅 Training with LOGO strategy (Leave-One-Group-Out by Sampling_Date)\")\n        print(\"=\"*60)\n        # Ensure we have Sampling_Date column\n        if 'Sampling_Date' not in df.columns:\n            print(\"Reloading DataFrame to get Sampling_Date column...\")\n            df = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\n            df['image_path'] = df['image_path'].apply(lambda x: os.path.basename(x))\n        return train_model_logo(df, model_class, features_cache, **kwargs)\n    \n    else:\n        raise ValueError(f\"Unknown strategy: '{strategy}'. Use 'random' or 'logo'.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:55.054447Z","iopub.execute_input":"2026-01-15T13:50:55.054959Z","iopub.status.idle":"2026-01-15T13:50:55.074442Z","shell.execute_reply.started":"2026-01-15T13:50:55.054927Z","shell.execute_reply":"2026-01-15T13:50:55.073671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train(real_train_df, BiomassMLP, real_features_cache, strategy='logo')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:59.483901Z","iopub.execute_input":"2026-01-15T13:50:59.484920Z","iopub.status.idle":"2026-01-15T13:53:32.060056Z","shell.execute_reply.started":"2026-01-15T13:50:59.484882Z","shell.execute_reply":"2026-01-15T13:53:32.059193Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- EXECUTE ---\n# Reload model with honest validation\nmodel_type = BiomassMLP\nmodel = train_model(real_train_df, model_type(), real_features_cache)\n# model = train_with_multi_head(real_train_df,real_features_cache)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.350423Z","iopub.status.idle":"2026-01-15T13:50:18.350653Z","shell.execute_reply.started":"2026-01-15T13:50:18.350538Z","shell.execute_reply":"2026-01-15T13:50:18.350553Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_type = BiomassMLPAlgebra\nmodel = train_model(real_train_df, model_type(), real_features_cache)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.351916Z","iopub.status.idle":"2026-01-15T13:50:18.352387Z","shell.execute_reply.started":"2026-01-15T13:50:18.352243Z","shell.execute_reply":"2026-01-15T13:50:18.352263Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_submission(model):\n    print(\"Loading resources...\")\n    \n    # 1. Load Test Metadata\n    test_df = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\n    \n    # 2. Load Your Trained Model\n    \n    model = model(input_dim=1536).to(DEVICE)\n\n        \n    if os.path.exists(MODEL_FILE):\n        model.load_state_dict(torch.load(MODEL_FILE, map_location=DEVICE))\n        print(f\"Loaded weights from {MODEL_FILE}\")\n    else:\n        print(\"Warning: Model file not found! Using random weights (Submissions will be garbage).\")\n    \n    model.eval()\n\n    # 3. Predict Image-by-Image\n    # We group by image_path so we only run the model ONCE per image\n    # instead of running it 5 times for the 5 rows.\n    submission_rows = []\n    \n    # Get unique images from test_df\n    unique_test_images = test_df['image_path'].unique()\n    \n    print(f\"Generating predictions for {len(unique_test_images)} unique images...\")\n    \n    with torch.no_grad():\n        for image_path in tqdm(unique_test_images):\n            # Construct feature filename (e.g., test/ID123.jpg -> ID123.pt)\n            filename = os.path.basename(image_path)\n            img_id = filename.split('.')[0]\n            feature_path = os.path.join(CACHE_DIR, f\"{img_id}.pt\")\n            \n            # Load Feature\n            if os.path.exists(feature_path):\n                # Load and add batch dimension (1, 1536)\n                feature = torch.load(feature_path, map_location=DEVICE).unsqueeze(0)\n                \n                # Run Inference\n                # Output shape: (1, 5) corresponding to [Green, Dead, Clover, GDM, Total]\n                preds = model(feature).cpu().numpy()[0]\n                \n                # Enforce non-negativity (Biomass can't be < 0)\n                preds = [max(0.0, p) for p in preds]\n                \n            else:\n                # Fallback if feature extraction failed for some image\n                print(f\"Warning: Feature missing for {img_id}, predicting 0.\")\n                preds = [0.0] * 5\n\n            # 4. Create Rows for Submission\n            # We need to map the model outputs [0..4] to the correct sample_id\n            \n            # Current image ID\n            current_id_str = img_id\n            \n            # Append the 5 required rows for this image\n            # Format: ID1001187975__Dry_Green_g\n            submission_rows.append({'sample_id': f\"{current_id_str}__Dry_Green_g\", 'target': preds[0]})\n            submission_rows.append({'sample_id': f\"{current_id_str}__Dry_Dead_g\",  'target': preds[1]})\n            submission_rows.append({'sample_id': f\"{current_id_str}__Dry_Clover_g\",'target': preds[2]})\n            submission_rows.append({'sample_id': f\"{current_id_str}__GDM_g\",       'target': preds[3]})\n            submission_rows.append({'sample_id': f\"{current_id_str}__Dry_Total_g\", 'target': preds[4]})\n\n    # 5. Save to CSV\n    submission_df = pd.DataFrame(submission_rows)\n    \n    # Ensure columns are in exact order required\n    submission_df = submission_df[['sample_id', 'target']]\n    \n    submission_df.to_csv(SUBMISSION_FILE, index=False)\n    print(f\"\\nSaved {len(submission_df)} rows to {SUBMISSION_FILE}\")\n    print(submission_df.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.353224Z","iopub.status.idle":"2026-01-15T13:50:18.353522Z","shell.execute_reply.started":"2026-01-15T13:50:18.353354Z","shell.execute_reply":"2026-01-15T13:50:18.353378Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = model_type\ngenerate_submission(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T13:50:18.354881Z","iopub.status.idle":"2026-01-15T13:50:18.355157Z","shell.execute_reply.started":"2026-01-15T13:50:18.355014Z","shell.execute_reply":"2026-01-15T13:50:18.355028Z"}},"outputs":[],"execution_count":null}]}