{"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"},{"sourceId":3729,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":2656,"modelId":312},{"sourceId":3730,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":2657,"modelId":312},{"sourceId":3731,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":2658,"modelId":312},{"sourceId":3732,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":2659,"modelId":312},{"sourceId":3733,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":2660,"modelId":312}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\nimport os","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:12:13.877698Z","iopub.execute_input":"2025-12-03T10:12:13.877973Z","iopub.status.idle":"2025-12-03T10:12:14.556358Z","shell.execute_reply.started":"2025-12-03T10:12:13.877951Z","shell.execute_reply":"2025-12-03T10:12:14.555587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\ndisplay(train_df.head())\n\ntrain_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:12:14.557710Z","iopub.execute_input":"2025-12-03T10:12:14.558468Z","iopub.status.idle":"2025-12-03T10:12:14.585787Z","shell.execute_reply.started":"2025-12-03T10:12:14.558446Z","shell.execute_reply":"2025-12-03T10:12:14.585265Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\ndisplay(test_df.head())\n\ntest_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:12:14.586315Z","iopub.execute_input":"2025-12-03T10:12:14.586520Z","iopub.status.idle":"2025-12-03T10:12:14.599319Z","shell.execute_reply.started":"2025-12-03T10:12:14.586503Z","shell.execute_reply":"2025-12-03T10:12:14.598593Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Structuring","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\n\n# 1. Load Data\ndf = pd.read_csv('/kaggle/input/csiro-biomass/train.csv')\n\n# 2. Pivot the table\n# Index = Unique Image, Columns = The 5 Target Names, Values = Target Mass\ndf_pivot = df.pivot_table(index='image_path', columns='target_name', values='target').reset_index()\n\n# 3. Define the column order explicitly to ensure targets match weights later\ntarget_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\ndf_pivot = df_pivot[['image_path'] + target_cols]\n\n# Now df_pivot has 1 row per image and 6 columns.\nprint(df_pivot.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:12:14.601062Z","iopub.execute_input":"2025-12-03T10:12:14.601303Z","iopub.status.idle":"2025-12-03T10:12:14.623790Z","shell.execute_reply.started":"2025-12-03T10:12:14.601287Z","shell.execute_reply":"2025-12-03T10:12:14.623125Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset\nfrom torch.utils.data import DataLoader\nimport cv2\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, targets=None, transform=None):\n        self.df = df\n        self.img_dir = img_dir\n        self.targets = targets # List of column names\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        # Construct path\n        row = self.df.iloc[idx]\n        file_path = os.path.join(self.img_dir, row['image_path'])\n        \n        # Load Image (OpenCV loads BGR, convert to RGB)\n        image = cv2.imread(file_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # Apply Augmentations (Resize, Normalize, etc.)\n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n            \n        # Get Targets (if training)\n        if self.targets:\n            # Return tensor of shape (5,)\n            label = torch.tensor(row[self.targets].values.astype('float32'))\n            return image, label\n        else:\n            return image\n\n# Define Transforms (ImageNet Normalization is vital for EfficientNet)\ndata_transforms = {\n    \"train\": A.Compose([\n        A.Resize(256, 256), # EfficientNet-B0 likes 224 or 256\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n        ToTensorV2(),\n    ]),\n    \"val\": A.Compose([\n        A.Resize(256, 256),\n        A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n        ToTensorV2(),\n    ])\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:12:14.624489Z","iopub.execute_input":"2025-12-03T10:12:14.624824Z","iopub.status.idle":"2025-12-03T10:12:16.902694Z","shell.execute_reply.started":"2025-12-03T10:12:14.624801Z","shell.execute_reply":"2025-12-03T10:12:16.902124Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modeling","metadata":{}},{"cell_type":"code","source":"# Configuration for all EfficientNet variants\n# We match the resolution to what the specific model version expects for best performance.\nMODEL_CONFIGS = {\n    'b0': {\n        'name': 'tf_efficientnet_b0',\n        'size': 224,\n        'path': '/kaggle/input/tf-efficientnet/pytorch/tf-efficientnet-b0/1/tf_efficientnet_b0_aa-827b6e33.pth'\n    },\n    'b1': {\n        'name': 'tf_efficientnet_b1',\n        'size': 240,\n        'path': '/kaggle/input/tf-efficientnet/pytorch/tf-efficientnet-b1/1/tf_efficientnet_b1_aa-ea7a6ee0.pth'\n    },\n    'b2': {\n        'name': 'tf_efficientnet_b2',\n        'size': 260,\n        'path': '/kaggle/input/tf-efficientnet/pytorch/tf-efficientnet-b2/1/tf_efficientnet_b2_aa-60c94f97.pth'\n    },\n    'b3': {\n        'name': 'tf_efficientnet_b3',\n        'size': 300,\n        'path': '/kaggle/input/tf-efficientnet/pytorch/tf-efficientnet-b3/1/tf_efficientnet_b3_aa-84b4657e.pth'\n    },\n    'b4': {\n        'name': 'tf_efficientnet_b4',\n        'size': 380,\n        'path': '/kaggle/input/tf-efficientnet/pytorch/tf-efficientnet-b4/1/tf_efficientnet_b4_aa-818f208c.pth'\n    }\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:41:35.599203Z","iopub.execute_input":"2025-12-03T10:41:35.599564Z","iopub.status.idle":"2025-12-03T10:41:35.604955Z","shell.execute_reply.started":"2025-12-03T10:41:35.599533Z","shell.execute_reply":"2025-12-03T10:41:35.604276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\ndef get_transforms(img_size):\n    return {\n        \"train\": A.Compose([\n            A.Resize(img_size, img_size),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            # Standard ImageNet normalization\n            A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n            ToTensorV2(),\n        ]),\n        \"val\": A.Compose([\n            A.Resize(img_size, img_size),\n            A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n            ToTensorV2(),\n        ])\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:42:29.089900Z","iopub.execute_input":"2025-12-03T10:42:29.090213Z","iopub.status.idle":"2025-12-03T10:42:29.095555Z","shell.execute_reply.started":"2025-12-03T10:42:29.090191Z","shell.execute_reply":"2025-12-03T10:42:29.094813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport timm\n\nclass BiomassModel(nn.Module):\n    def __init__(self, model_name, weight_path, pretrained=False):\n        super().__init__()\n        \n        # 1. Create the empty backbone using the specific model name (e.g., 'tf_efficientnet_b3')\n        self.backbone = timm.create_model(model_name, pretrained=False, num_classes=0)\n        \n        # 2. Load the specific weights provided in arguments\n        try:\n            state_dict = torch.load(weight_path, map_location='cpu')\n            \n            # strict=False allows us to ignore the missing classifier head\n            self.backbone.load_state_dict(state_dict, strict=False)\n            print(f\"✅ Loaded weights for {model_name} from local file!\")\n            \n        except FileNotFoundError:\n            print(f\"❌ ERROR: File not found at {weight_path}\")\n        except Exception as e:\n            print(f\"❌ Error loading weights: {e}\")\n        \n        # 3. Dynamic input features\n        # B0/B1=1280, B2=1408, B3=1536, B4=1792. \n        # .num_features detects this automatically.\n        in_features = self.backbone.num_features\n        \n        self.head = nn.Sequential(\n            nn.Linear(in_features, 512),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(512, 5) \n        )\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        output = self.head(features)\n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:42:36.925297Z","iopub.execute_input":"2025-12-03T10:42:36.925558Z","iopub.status.idle":"2025-12-03T10:42:36.931950Z","shell.execute_reply.started":"2025-12-03T10:42:36.925540Z","shell.execute_reply":"2025-12-03T10:42:36.931239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nclass WeightedMSELoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        # Weights for: [Green, Dead, Clover, GDM, Total]\n        # This matches the column order in your df_pivot\n        self.weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5])\n        \n    def forward(self, inputs, targets):\n        # Move weights to the same device as inputs (CPU or GPU)\n        weights = self.weights.to(inputs.device)\n        \n        # Calculate Squared Error\n        loss = (inputs - targets) ** 2\n        \n        # Multiply by weights\n        weighted_loss = loss * weights\n        \n        # Return mean\n        return torch.mean(weighted_loss)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:54:22.659655Z","iopub.execute_input":"2025-12-03T10:54:22.660239Z","iopub.status.idle":"2025-12-03T10:54:22.665235Z","shell.execute_reply.started":"2025-12-03T10:54:22.660214Z","shell.execute_reply":"2025-12-03T10:54:22.664452Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## B0","metadata":{}},{"cell_type":"code","source":"# # --- SELECT YOUR MODEL HERE ---\n# SELECTED_VERSION = 'b0'  # Change to 'b1', 'b2', 'b3', or 'b4'\n# BATCH_SIZE = 16          # Lower this if you choose b4\n# # ------------------------------","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## B1","metadata":{}},{"cell_type":"code","source":"# # --- SELECT YOUR MODEL HERE ---\n# SELECTED_VERSION = 'b1'  # Change to 'b1', 'b2', 'b3', or 'b4'\n# BATCH_SIZE = 16          # Lower this if you choose b4\n# # ------------------------------","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## B2","metadata":{}},{"cell_type":"code","source":"# # --- SELECT YOUR MODEL HERE ---\n# SELECTED_VERSION = 'b2'  # Change to 'b1', 'b2', 'b3', or 'b4'\n# BATCH_SIZE = 16          # Lower this if you choose b4\n# # ------------------------------","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## B3","metadata":{}},{"cell_type":"code","source":"# # --- SELECT YOUR MODEL HERE ---\n# SELECTED_VERSION = 'b3'  # Change to 'b1', 'b2', 'b3', or 'b4'\n# BATCH_SIZE = 16          # Lower this if you choose b4\n# # ------------------------------","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## B4","metadata":{}},{"cell_type":"code","source":"# --- SELECT YOUR MODEL HERE ---\nSELECTED_VERSION = 'b4'  # Change to 'b1', 'b2', 'b3', or 'b4'\nBATCH_SIZE = 8          # Lower this if you choose b4\n# ------------------------------","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Get Config\nconfig = MODEL_CONFIGS[SELECTED_VERSION]\nprint(f\"🚀 Initializing {config['name']} with resolution {config['size']}...\")\n\n# 2. Get Transforms for this resolution\ntransforms = get_transforms(config['size'])\n\n# 3. Re-initialize Datasets with new transforms (Assuming df_pivot exists from your previous code)\n# Note: You need to split your df_pivot into train_df/val_df before this step if you haven't yet\n# For this example, I'll assume you are setting up the training set:\ntrain_dataset = BiomassDataset(\n    df=df_pivot, # Use your training dataframe here\n    img_dir='/kaggle/input/csiro-biomass', \n    targets=['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g'], \n    transform=transforms['train']\n)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)\n\n# 4. Initialize Model\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nmodel = BiomassModel(\n    model_name=config['name'], \n    weight_path=config['path']\n).to(device)\n\n# 5. Define Optimizer and Loss (Same as before)\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)\ncriterion = WeightedMSELoss()\n\nprint(\"Ready to train!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:43:43.859354Z","iopub.execute_input":"2025-12-03T10:43:43.859842Z","iopub.status.idle":"2025-12-03T10:43:44.044991Z","shell.execute_reply.started":"2025-12-03T10:43:43.859820Z","shell.execute_reply":"2025-12-03T10:43:44.044251Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import copy\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\n\n# --- CONFIGURATION ---\nEPOCHS = 50  # Adjust this: is better for final results, 5 is good for testing\nBEST_MODEL_PATH = \"best_model.pth\"\n\n# ---------------------------------------------------------\n# 1. PREPARE TRAIN & VALIDATION DATA\n# ---------------------------------------------------------\nprint(\"Mood: Splitting data and creating loaders...\")\n\n# Split the pivot table (80% train, 20% val)\ntrain_df, val_df = train_test_split(df_pivot, test_size=0.2, random_state=42)\n\n# Create Datasets using the dynamic 'transforms' you created earlier\n# Note: We use config['size'] implicitly because 'transforms' was generated with it\ntrain_dataset = BiomassDataset(\n    train_df, \n    img_dir='/kaggle/input/csiro-biomass', \n    targets=['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g'], \n    transform=transforms['train']\n)\n\nval_dataset = BiomassDataset(\n    val_df, \n    img_dir='/kaggle/input/csiro-biomass', \n    targets=['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g'], \n    transform=transforms['val']\n)\n\n# Create Loaders\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:57:19.801459Z","iopub.execute_input":"2025-12-03T10:57:19.801765Z","iopub.status.idle":"2025-12-03T10:57:19.811041Z","shell.execute_reply.started":"2025-12-03T10:57:19.801741Z","shell.execute_reply":"2025-12-03T10:57:19.810045Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import copy\nimport torch\nimport numpy as np\nfrom tqdm import tqdm\n\n# 1. Define Device explicitly (GPU if available, else CPU)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")\n\n# 2. Updated Training Function with Early Stopping\ndef train_model(model, train_loader, val_loader, criterion, optimizer, device, num_epochs=10, patience=3):\n    \"\"\"\n    Args:\n        patience (int): How many epochs to wait after last time validation loss improved.\n    \"\"\"\n    best_loss = float('inf')\n    best_weights = copy.deepcopy(model.state_dict())\n    \n    # Early Stopping counters\n    epochs_no_improve = 0\n    \n    for epoch in range(num_epochs):\n        print(f\"\\nEpoch {epoch+1}/{num_epochs}\")\n        print(\"-\" * 10)\n        \n        # --- TRAIN PHASE ---\n        model.train()\n        train_loss = 0.0\n        \n        pbar = tqdm(train_loader, desc=\"Training\")\n        for images, labels in pbar:\n            # Move data to the defined device\n            images, labels = images.to(device), labels.to(device)\n            \n            optimizer.zero_grad()\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            \n            train_loss += loss.item() * images.size(0)\n            pbar.set_postfix({'loss': loss.item()})\n            \n        epoch_train_loss = train_loss / len(train_loader.dataset)\n        \n        # --- VALIDATION PHASE ---\n        model.eval()\n        val_loss = 0.0\n        \n        with torch.no_grad():\n            for images, labels in tqdm(val_loader, desc=\"Validation\"):\n                images, labels = images.to(device), labels.to(device)\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n                val_loss += loss.item() * images.size(0)\n        \n        epoch_val_loss = val_loss / len(val_loader.dataset)\n        \n        print(f\"Train Loss: {epoch_train_loss:.4f} | Val Loss: {epoch_val_loss:.4f}\")\n        \n        # --- EARLY STOPPING CHECK ---\n        if epoch_val_loss < best_loss:\n            best_loss = epoch_val_loss\n            best_weights = copy.deepcopy(model.state_dict())\n            torch.save(best_weights, \"best_model.pth\")\n            print(\"✅ Validation loss improved. Model saved.\")\n            epochs_no_improve = 0  # Reset counter\n        else:\n            epochs_no_improve += 1\n            print(f\"⚠️ No improvement for {epochs_no_improve} epochs.\")\n            if epochs_no_improve >= patience:\n                print(f\"🛑 Early stopping triggered after {epoch+1} epochs!\")\n                break\n            \n    # Load best weights before returning\n    model.load_state_dict(best_weights)\n    return model\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:54:35.348109Z","iopub.execute_input":"2025-12-03T10:54:35.348471Z","iopub.status.idle":"2025-12-03T10:54:35.358346Z","shell.execute_reply.started":"2025-12-03T10:54:35.348447Z","shell.execute_reply":"2025-12-03T10:54:35.357659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------------------------------------\n# 3. RUN TRAINING\n# ---------------------------------------------------------\n# 3. Execution\n# Ensure your model is also moved to the device before training\nmodel = model.to(device)\n\n# We pass 'device' into the function now\nmodel = train_model(\n    model=model, \n    train_loader=train_loader, \n    val_loader=val_loader, \n    criterion=criterion, \n    optimizer=optimizer, \n    device=device,        # Pass the GPU/CPU device\n    num_epochs=EPOCHS, \n    patience=5            # Stop if no improvement for 5 epochs\n)\n# ---------------------------------------------------------\n# 4. INFERENCE ON TEST SET\n# ---------------------------------------------------------\nprint(\"\\n🚀 Starting Inference...\")\n\n# Prepare Test Loader (Corrected path logic)\ntest_df_raw = pd.read_csv('/kaggle/input/csiro-biomass/test.csv')\ntest_df_unique = test_df_raw.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n\ntest_dataset = BiomassDataset(\n    df=test_df_unique, \n    img_dir='/kaggle/input/csiro-biomass', # Root dir\n    targets=None, \n    transform=transforms['val'] # Use Validation transforms (No flips)\n)\n\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nmodel.eval()\npreds = []\n\nwith torch.no_grad():\n    for images in tqdm(test_loader, desc=\"Predicting\"):\n        images = images.to(device)\n        outputs = model(images)\n        outputs = torch.relu(outputs) # Enforce non-negative biomass\n        preds.append(outputs.cpu().numpy())\n\n# Stack predictions\npredictions = np.vstack(preds)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:54:40.563431Z","iopub.execute_input":"2025-12-03T10:54:40.563984Z","iopub.status.idle":"2025-12-03T10:57:19.799810Z","shell.execute_reply.started":"2025-12-03T10:54:40.563960Z","shell.execute_reply":"2025-12-03T10:57:19.798848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------------------------------------\n# 5. CREATE SUBMISSION FILE\n# ---------------------------------------------------------\ntarget_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n# Create DataFrame\npred_df = pd.DataFrame(predictions, columns=target_cols)\npred_df['image_path'] = test_df_unique['image_path']\n\n# Melt to Long Format\nsub_long = pred_df.melt(id_vars=['image_path'], \n                        value_vars=target_cols, \n                        var_name='target_name', \n                        value_name='target')\n\n# Create sample_id\nsub_long['filename_stem'] = sub_long['image_path'].apply(lambda x: x.split('/')[-1].split('.')[0])\nsub_long['sample_id'] = sub_long['filename_stem'] + '__' + sub_long['target_name']\n\n# Save\nsubmission = sub_long[['sample_id', 'target']]\nsubmission.to_csv('submission.csv', index=False)\n\nprint(\"\\n🎉 DONE! submission.csv created.\")\nprint(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T10:57:26.751689Z","iopub.execute_input":"2025-12-03T10:57:26.751968Z","iopub.status.idle":"2025-12-03T10:57:26.766341Z","shell.execute_reply.started":"2025-12-03T10:57:26.751950Z","shell.execute_reply":"2025-12-03T10:57:26.765626Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}