{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom sklearn.model_selection import train_test_split\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\ndevice\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_PATH = \"/kaggle/input/csiro-biomass\"\n\ntrain_df = pd.read_csv(os.path.join(BASE_PATH, \"train.csv\"))\ntest_df  = pd.read_csv(os.path.join(BASE_PATH, \"test.csv\"))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_pivot = train_df.pivot_table(\n    index=[\n        \"image_path\",\n        \"Pre_GSHH_NDVI\",\n        \"Height_Ave_cm\",\n        \"State\",\n        \"Species\"\n    ],\n    columns=\"target_name\",\n    values=\"target\"\n).reset_index()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGETS = [\n    \"Dry_Green_g\",\n    \"Dry_Dead_g\",\n    \"Dry_Clover_g\",\n    \"GDM_g\",\n    \"Dry_Total_g\"\n]\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"state_enc = LabelEncoder()\nspecies_enc = LabelEncoder()\n\ntrain_pivot[\"State_enc\"] = state_enc.fit_transform(train_pivot[\"State\"])\ntrain_pivot[\"Species_enc\"] = species_enc.fit_transform(train_pivot[\"Species\"])\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"scaler = StandardScaler()\n\nnum_features = train_pivot[[\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]].values\ntrain_pivot[[\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]] = scaler.fit_transform(num_features)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data, val_data = train_test_split(\n    train_pivot,\n    test_size=0.2,\n    random_state=42\n)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def safe_value(row, col, default=0.0):\n    if col in row and pd.notna(row[col]):\n        return float(row[col])\n    return default\n\n\ndef safe_encode(series, encoder):\n    known = set(encoder.classes_)\n    return series.apply(lambda x: encoder.transform([x])[0] if x in known else 0)\n\n\n# CREATE test_meta SAFELY (this did not exist before)\ntest_meta = test_df.drop_duplicates(\"image_path\").copy()\n\n# ---- Numeric features ----\ntest_meta[\"Pre_GSHH_NDVI\"] = test_meta.apply(\n    lambda r: safe_value(r, \"Pre_GSHH_NDVI\"), axis=1\n)\n\ntest_meta[\"Height_Ave_cm\"] = test_meta.apply(\n    lambda r: safe_value(r, \"Height_Ave_cm\"), axis=1\n)\n\n# ---- Categorical features ----\nif \"State\" in test_meta.columns:\n    test_meta[\"State_enc\"] = safe_encode(test_meta[\"State\"], state_enc)\nelse:\n    test_meta[\"State_enc\"] = 0\n\nif \"Species\" in test_meta.columns:\n    test_meta[\"Species_enc\"] = safe_encode(test_meta[\"Species\"], species_enc)\nelse:\n    test_meta[\"Species_enc\"] = 0\n\n\nprint(\"test_meta created:\", test_meta.shape)\nprint(\"test_meta columns:\", test_meta.columns.tolist())\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df):\n        self.df = df.reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        img_path = os.path.join(BASE_PATH, row[\"image_path\"])\n        img = cv2.imread(img_path)\n        img = cv2.resize(img, (224, 224))\n        img = img[:, :, ::-1] / 255.0\n        img = torch.tensor(img, dtype=torch.float).permute(2, 0, 1)\n\n        tabular = torch.tensor([\n            row[\"Pre_GSHH_NDVI\"],\n            row[\"Height_Ave_cm\"],\n            row[\"State_enc\"],\n            row[\"Species_enc\"]\n        ], dtype=torch.float)\n\n        targets = torch.tensor(row[TARGETS].values, dtype=torch.float)\n\n        return img, tabular, targets\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    BiomassDataset(train_data),\n    batch_size=16,\n    shuffle=True,\n    num_workers=2\n)\n\nval_loader = DataLoader(\n    BiomassDataset(val_data),\n    batch_size=16,\n    shuffle=False,\n    num_workers=2\n)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torchvision.models as models\nimport torch.nn as nn\n\nclass BiomassModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        # NO PRETRAINED WEIGHTS (INTERNET-SAFE)\n        self.cnn = models.efficientnet_b0(weights=None)\n        self.cnn.classifier = nn.Identity()\n\n        self.tabular = nn.Sequential(\n            nn.Linear(4, 64),\n            nn.ReLU(),\n            nn.Linear(64, 32)\n        )\n\n        self.head = nn.Sequential(\n            nn.Linear(1280 + 32, 256),\n            nn.ReLU(),\n            nn.Linear(256, 5)\n        )\n\n    def forward(self, img, tab):\n        img_feat = self.cnn(img)\n        tab_feat = self.tabular(tab)\n        x = torch.cat([img_feat, tab_feat], dim=1)\n        return self.head(x)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = BiomassModel().to(device)\ncriterion = nn.MSELoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df):\n        self.df = df.reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        # ----- IMAGE -----\n        img_path = os.path.join(BASE_PATH, row[\"image_path\"])\n        img = cv2.imread(img_path)\n        img = cv2.resize(img, (224, 224))\n        img = img[:, :, ::-1] / 255.0\n        img = torch.tensor(img, dtype=torch.float32).permute(2, 0, 1)\n\n        # ----- TABULAR -----\n        tabular = torch.tensor(\n            [\n                float(row[\"Pre_GSHH_NDVI\"]),\n                float(row[\"Height_Ave_cm\"]),\n                float(row[\"State_enc\"]),\n                float(row[\"Species_enc\"]),\n            ],\n            dtype=torch.float32\n        )\n\n        # ----- TARGETS (CRITICAL FIX) -----\n        targets = torch.from_numpy(\n            row[TARGETS].to_numpy(dtype=np.float32)\n        )\n\n        return img, tabular, targets\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    BiomassDataset(train_data),\n    batch_size=16,\n    shuffle=True,\n    num_workers=2\n)\n\nval_loader = DataLoader(\n    BiomassDataset(val_data),\n    batch_size=16,\n    shuffle=False,\n    num_workers=2\n)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 7\n\nfor epoch in range(EPOCHS):\n    model.train()\n    running_loss = 0.0\n\n    for imgs, tabs, targets in train_loader:\n        imgs = imgs.to(device)\n        tabs = tabs.to(device)\n        targets = targets.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(imgs, tabs)\n        loss = criterion(outputs, targets)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item()\n\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Loss: {running_loss/len(train_loader):.4f}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\nval_loss = 0.0\n\nwith torch.no_grad():\n    for imgs, tabs, targets in val_loader:\n        imgs = imgs.to(device)\n        tabs = tabs.to(device)\n        targets = targets.to(device)\n\n        outputs = model(imgs, tabs)\n        loss = criterion(outputs, targets)\n        val_loss += loss.item()\n\nval_loss /= len(val_loader)\nprint(\"Validation Loss:\", round(val_loss, 4))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_meta = test_df.drop_duplicates(\"image_path\").copy()\n\n#Handle categorical metadata safely\nif \"State\" in test_meta.columns:\n    test_meta[\"State_enc\"] = state_enc.transform(test_meta[\"State\"])\nelse:\n    test_meta[\"State_enc\"] = 0.0\n\nif \"Species\" in test_meta.columns:\n    test_meta[\"Species_enc\"] = species_enc.transform(test_meta[\"Species\"])\nelse:\n    test_meta[\"Species_enc\"] = 0.0\n\n# Handle numeric metadata safely\nnum_cols = []\n\nfor col in [\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]:\n    if col in test_meta.columns:\n        num_cols.append(col)\n    else:\n        test_meta[col] = 0.0\n\n# ONLY scale if columns exist\nif len(num_cols) > 0:\n    test_meta[num_cols] = scaler.transform(test_meta[num_cols])\n\nprint(\"Test meta columns:\", test_meta.columns.tolist())\nprint(\"Test images:\", len(test_meta))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, df):\n        self.df = df.reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        img_path = os.path.join(BASE_PATH, row[\"image_path\"])\n        img = cv2.imread(img_path)\n\n        # PRIVATE TEST SAFE\n        if img is None:\n            img = np.zeros((160, 160, 3), dtype=np.uint8)\n        else:\n            img = cv2.resize(img, (160, 160))\n\n        img = img[:, :, ::-1] / 255.0\n        img = torch.tensor(img, dtype=torch.float32).permute(2, 0, 1)\n\n        tabular = torch.tensor(\n            [\n                row[\"Pre_GSHH_NDVI\"],\n                row[\"Height_Ave_cm\"],\n                row[\"State_enc\"],\n                row[\"Species_enc\"],\n            ],\n            dtype=torch.float32\n        )\n\n        return img, tabular\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loader = DataLoader(\n    TestDataset(test_meta),\n    batch_size=16,\n    shuffle=False,\n    num_workers=0\n)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\npredictions = []\n\nwith torch.no_grad():\n    for imgs, tabs in test_loader:\n        imgs = imgs.to(device)\n        tabs = tabs.to(device)\n\n        preds = model(imgs, tabs)\n        predictions.append(preds.cpu().numpy())\n\npredictions = np.vstack(predictions)\n\nprint(\"Predictions shape:\", predictions.shape)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"assert predictions.shape[1] == 5, \"Prediction output must have 5 targets\"\nassert len(predictions) == len(test_meta), \"Mismatch between predictions and test size\"\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGETS = [\n    \"Dry_Green_g\",\n    \"Dry_Dead_g\",\n    \"Dry_Clover_g\",\n    \"GDM_g\",\n    \"Dry_Total_g\"\n]\n\nrows = []\n\nfor i in range(len(test_meta)):\n    img_id = test_meta.iloc[i][\"image_path\"].split(\"/\")[-1].replace(\".jpg\", \"\")\n    for j, t in enumerate(TARGETS):\n        rows.append({\n            \"sample_id\": f\"{img_id}__{t}\",\n            \"target\": float(predictions[i, j])\n        })\n\nsubmission = pd.DataFrame(rows)\nsubmission.to_csv(\"/kaggle/working/submission.csv\", index=False)\n\nprint(\"Submission saved:\", submission.shape)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}