{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":[{"sourceType":"competition","sourceId":112509,"databundleVersionId":14254895},{"sourceType":"modelInstanceVersion","sourceId":101382,"databundleVersionId":9436101,"modelInstanceId":85001,"modelId":109225}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 1. Setup & Configuration","metadata":{}},{"cell_type":"code","source":"# Install required packages\n!pip install -q timm albumentations","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:34.703705Z","iopub.execute_input":"2025-12-26T05:23:34.705972Z","iopub.status.idle":"2025-12-26T05:23:40.348646Z","shell.execute_reply.started":"2025-12-26T05:23:34.705883Z","shell.execute_reply":"2025-12-26T05:23:40.346680Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\n\nimport torchvision.transforms as transforms\nimport timm\n\nfrom sklearn.model_selection import KFold\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import r2_score\n\nfrom tqdm.auto import tqdm\nimport gc\n\n# Set random seeds for reproducibility\ndef set_seed(seed=42):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:40.352828Z","iopub.execute_input":"2025-12-26T05:23:40.353335Z","iopub.status.idle":"2025-12-26T05:23:40.374480Z","shell.execute_reply.started":"2025-12-26T05:23:40.353286Z","shell.execute_reply":"2025-12-26T05:23:40.372931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Configuration\nclass CFG:\n    # Paths - point to your local data folder\n    data_dir = '/kaggle/input/csiro-biomass'  # ← Correct path to your data\n    \n    train_dir = os.path.join(data_dir, 'train')\n    test_dir = os.path.join(data_dir, 'test')\n    train_csv = os.path.join(data_dir, 'train.csv')\n    test_csv = os.path.join(data_dir, 'test.csv')\n    # Model Selection\n    model_name = 'efficientnet_b0'  # or 'efficientnet_b3', 'convnext_tiny', etc.\n    img_size = 384\n    pretrained = True\n    \n    # Training\n    n_folds = 2\n    epochs = 2\n    batch_size = 4\n    lr = 1e-4\n    weight_decay = 1e-5\n    num_workers = 1\n    drop_last = True\n    \n    # Targets\n    targets = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    target_weights = {\n        'Dry_Green_g': 0.1,\n        'Dry_Dead_g': 0.1,\n        'Dry_Clover_g': 0.1,\n        'GDM_g': 0.2,\n        'Dry_Total_g': 0.5\n    }\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nprint(f\"✓ Device: {CFG.device}\")\nprint(f\"✓ Data directory: {CFG.data_dir}\")  # ← FIXED: removed the syntax error\nprint(f\"✓ Model: {CFG.model_name}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:40.376750Z","iopub.execute_input":"2025-12-26T05:23:40.377477Z","iopub.status.idle":"2025-12-26T05:23:40.399581Z","shell.execute_reply.started":"2025-12-26T05:23:40.377429Z","shell.execute_reply":"2025-12-26T05:23:40.398392Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Data Loading & EDA","metadata":{}},{"cell_type":"code","source":"# Load data\ntrain_df = pd.read_csv(os.path.join(CFG.data_dir, '/kaggle/input/csiro-biomass/train.csv'))\ntest_df = pd.read_csv(os.path.join(CFG.data_dir, '/kaggle/input/csiro-biomass/test.csv'))\n\nprint(f\"Train shape: {train_df.shape}\")\nprint(f\"Test shape: {test_df.shape}\")\nprint(f\"\\nTrain columns: {train_df.columns.tolist()}\")\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:40.401524Z","iopub.execute_input":"2025-12-26T05:23:40.401911Z","iopub.status.idle":"2025-12-26T05:23:40.472647Z","shell.execute_reply.started":"2025-12-26T05:23:40.401874Z","shell.execute_reply":"2025-12-26T05:23:40.471216Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Restructure from long to wide format\n# Each image should have one row with all 5 targets\ntrain_wide = train_df.pivot_table(\n    index=['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'],\n    columns='target_name',\n    values='target',\n    aggfunc='first'\n).reset_index()\n\n# Flatten column names\ntrain_wide.columns.name = None\n\nprint(f\"Restructured train shape: {train_wide.shape}\")\nprint(f\"Number of unique images: {train_wide['image_path'].nunique()}\")\ntrain_wide.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:40.476204Z","iopub.execute_input":"2025-12-26T05:23:40.476628Z","iopub.status.idle":"2025-12-26T05:23:40.532480Z","shell.execute_reply.started":"2025-12-26T05:23:40.476595Z","shell.execute_reply":"2025-12-26T05:23:40.531325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check for missing values\nprint(\"Missing values in train_wide:\")\nprint(train_wide.isnull().sum())\n\n# Basic statistics of targets\nprint(\"\\nTarget statistics:\")\ntrain_wide[CFG.targets].describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:40.533923Z","iopub.execute_input":"2025-12-26T05:23:40.534297Z","iopub.status.idle":"2025-12-26T05:23:40.569917Z","shell.execute_reply.started":"2025-12-26T05:23:40.534268Z","shell.execute_reply":"2025-12-26T05:23:40.568611Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualize target distributions\nfig, axes = plt.subplots(2, 3, figsize=(15, 8))\naxes = axes.flatten()\n\nfor idx, target in enumerate(CFG.targets):\n    axes[idx].hist(train_wide[target], bins=50, edgecolor='black', alpha=0.7)\n    axes[idx].set_title(f'{target} Distribution')\n    axes[idx].set_xlabel('Biomass (g)')\n    axes[idx].set_ylabel('Frequency')\n    axes[idx].grid(alpha=0.3)\n\naxes[5].axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:40.572437Z","iopub.execute_input":"2025-12-26T05:23:40.572859Z","iopub.status.idle":"2025-12-26T05:23:56.945515Z","shell.execute_reply.started":"2025-12-26T05:23:40.572820Z","shell.execute_reply":"2025-12-26T05:23:56.944343Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Target correlations\nplt.figure(figsize=(10, 8))\ncorr_matrix = train_wide[CFG.targets].corr()\nsns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', center=0)\nplt.title('Target Correlations')\nplt.show()\n\nprint(\"\\nKey insights:\")\nprint(f\"Dry_Total should correlate with components\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:56.947023Z","iopub.execute_input":"2025-12-26T05:23:56.947420Z","iopub.status.idle":"2025-12-26T05:23:57.288877Z","shell.execute_reply.started":"2025-12-26T05:23:56.947383Z","shell.execute_reply":"2025-12-26T05:23:57.287320Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Analyze NDVI and Height relationships\nfig, axes = plt.subplots(1, 2, figsize=(15, 5))\n\nfor target in CFG.targets:\n    axes[0].scatter(train_wide['Pre_GSHH_NDVI'], train_wide[target], alpha=0.3, s=10)\n    axes[1].scatter(train_wide['Height_Ave_cm'], train_wide[target], alpha=0.3, s=10)\n\naxes[0].set_xlabel('NDVI')\naxes[0].set_ylabel('Biomass (g)')\naxes[0].set_title('NDVI vs Biomass')\naxes[0].grid(alpha=0.3)\n\naxes[1].set_xlabel('Height (cm)')\naxes[1].set_ylabel('Biomass (g)')\naxes[1].set_title('Height vs Biomass')\naxes[1].grid(alpha=0.3)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:57.290618Z","iopub.execute_input":"2025-12-26T05:23:57.291138Z","iopub.status.idle":"2025-12-26T05:23:57.795323Z","shell.execute_reply.started":"2025-12-26T05:23:57.291085Z","shell.execute_reply":"2025-12-26T05:23:57.794242Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualize sample images\nfig, axes = plt.subplots(2, 4, figsize=(16, 8))\naxes = axes.flatten()\n\nsample_indices = np.random.choice(len(train_wide), 8, replace=False)\n\nfor idx, sample_idx in enumerate(sample_indices):\n    row = train_wide.iloc[sample_idx]\n    img_path = os.path.join(CFG.data_dir, row['image_path'])\n    \n    if os.path.exists(img_path):\n        img = Image.open(img_path)\n        axes[idx].imshow(img)\n        axes[idx].set_title(f\"Total: {row['Dry_Total_g']:.1f}g\\nNDVI: {row['Pre_GSHH_NDVI']:.2f}\", fontsize=10)\n        axes[idx].axis('off')\n\nplt.suptitle('Sample Pasture Images with Biomass', fontsize=14, y=1.02)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:23:57.796814Z","iopub.execute_input":"2025-12-26T05:23:57.797146Z","iopub.status.idle":"2025-12-26T05:24:00.054774Z","shell.execute_reply.started":"2025-12-26T05:23:57.797117Z","shell.execute_reply":"2025-12-26T05:24:00.053533Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Feature Engineering","metadata":{}},{"cell_type":"code","source":"# Encode categorical features\ndef preprocess_features(df):\n    df = df.copy()\n    \n    # State encoding\n    state_encoder = LabelEncoder()\n    df['State_encoded'] = state_encoder.fit_transform(df['State'].fillna('Unknown'))\n    \n    # Extract dominant species (first in the list)\n    df['Dominant_Species'] = df['Species'].fillna('Unknown').str.split('_').str[0]\n    species_encoder = LabelEncoder()\n    df['Species_encoded'] = species_encoder.fit_transform(df['Dominant_Species'])\n    \n    # Parse date features\n    df['Sampling_Date'] = pd.to_datetime(df['Sampling_Date'], errors='coerce')\n    df['Month'] = df['Sampling_Date'].dt.month.fillna(6)  # Default to mid-year\n    df['Season'] = df['Month'].apply(lambda x: (x % 12 + 3) // 3)  # 1=Summer, 2=Autumn, 3=Winter, 4=Spring\n    \n    # Handle missing numerical values\n    df['Pre_GSHH_NDVI'] = df['Pre_GSHH_NDVI'].fillna(df['Pre_GSHH_NDVI'].median())\n    df['Height_Ave_cm'] = df['Height_Ave_cm'].fillna(df['Height_Ave_cm'].median())\n    \n    return df, state_encoder, species_encoder\n\ntrain_wide, state_encoder, species_encoder = preprocess_features(train_wide)\n\nprint(\"\\nProcessed features:\")\nprint(f\"States: {state_encoder.classes_}\")\nprint(f\"Dominant species: {species_encoder.classes_[:10]}...\")  # Show first 10\nprint(f\"\\nFeature summary:\")\nprint(train_wide[['State_encoded', 'Species_encoded', 'Month', 'Season', 'Pre_GSHH_NDVI', 'Height_Ave_cm']].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:24:00.057090Z","iopub.execute_input":"2025-12-26T05:24:00.058503Z","iopub.status.idle":"2025-12-26T05:24:00.107632Z","shell.execute_reply.started":"2025-12-26T05:24:00.058454Z","shell.execute_reply":"2025-12-26T05:24:00.106404Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Custom Weighted R² Metric","metadata":{}},{"cell_type":"code","source":"def weighted_r2_score(y_true, y_pred, weights):\n    \"\"\"\n    Calculate globally weighted R² across all targets.\n    \n    Args:\n        y_true: Ground truth values (N x 5)\n        y_pred: Predicted values (N x 5)\n        weights: List of weights for each target [0.1, 0.1, 0.1, 0.2, 0.5]\n    \n    Returns:\n        Weighted R² score\n    \"\"\"\n    y_true = np.array(y_true)\n    y_pred = np.array(y_pred)\n    weights = np.array(weights)\n    \n    # Flatten and repeat weights for each sample\n    n_samples = y_true.shape[0]\n    w = np.tile(weights, n_samples)  # Weight per (sample, target) pair\n    y_true_flat = y_true.flatten()\n    y_pred_flat = y_pred.flatten()\n    \n    # Weighted mean\n    y_mean_weighted = np.sum(w * y_true_flat) / np.sum(w)\n    \n    # Weighted R²\n    ss_res = np.sum(w * (y_true_flat - y_pred_flat) ** 2)\n    ss_tot = np.sum(w * (y_true_flat - y_mean_weighted) ** 2)\n    \n    r2 = 1 - (ss_res / ss_tot)\n    return r2\n\n# Test the metric with dummy data\ndummy_true = np.random.rand(100, 5) * 100\ndummy_pred = dummy_true + np.random.randn(100, 5) * 10\nweights = [0.1, 0.1, 0.1, 0.2, 0.5]\n\ntest_r2 = weighted_r2_score(dummy_true, dummy_pred, weights)\nprint(f\"Test weighted R²: {test_r2:.4f}\")\nprint(\"Metric implementation verified ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:24:00.109546Z","iopub.execute_input":"2025-12-26T05:24:00.110093Z","iopub.status.idle":"2025-12-26T05:24:00.139644Z","shell.execute_reply.started":"2025-12-26T05:24:00.110050Z","shell.execute_reply":"2025-12-26T05:24:00.137922Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Dataset & DataLoader","metadata":{}},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df, data_dir, targets, transform=None, is_test=False):\n        self.df = df.reset_index(drop=True)\n        self.data_dir = data_dir\n        self.targets = targets\n        self.transform = transform\n        self.is_test = is_test\n        \n        # Tabular features\n        self.tabular_features = ['Pre_GSHH_NDVI', 'Height_Ave_cm', 'State_encoded', \n                                  'Species_encoded', 'Month', 'Season']\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        \n        # Load image\n        img_path = os.path.join(self.data_dir, row['image_path'])\n        image = Image.open(img_path).convert('RGB')\n        \n        if self.transform:\n            image = self.transform(image)\n        \n        # Get tabular features\n        tabular = torch.tensor(row[self.tabular_features].values.astype(np.float32))\n        \n        if self.is_test:\n            return image, tabular\n        else:\n            # Get targets\n            targets = torch.tensor(row[self.targets].values.astype(np.float32))\n            return image, tabular, targets\n\n# Image transforms\ntrain_transform = transforms.Compose([\n    transforms.Resize((CFG.img_size, CFG.img_size)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((CFG.img_size, CFG.img_size)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nprint(\"Dataset and transforms defined ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:24:00.141794Z","iopub.execute_input":"2025-12-26T05:24:00.142437Z","iopub.status.idle":"2025-12-26T05:24:00.170854Z","shell.execute_reply.started":"2025-12-26T05:24:00.142398Z","shell.execute_reply":"2025-12-26T05:24:00.169595Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Model Architecture","metadata":{}},{"cell_type":"code","source":"class BiomassModel(nn.Module):\n    def __init__(self, model_name='efficientnet_b0', pretrained=True, num_targets=5, num_tabular_features=6):\n        super(BiomassModel, self).__init__()\n        \n        # Image encoder (pretrained CNN)\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0)\n        backbone_out_features = self.backbone.num_features\n        \n        # Tabular feature processor\n        self.tabular_bn = nn.BatchNorm1d(num_tabular_features)\n        self.tabular_fc = nn.Sequential(\n            nn.Linear(num_tabular_features, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3)\n        )\n        \n        # Fusion layer\n        fusion_input = backbone_out_features + 128\n        self.fusion = nn.Sequential(\n            nn.Linear(fusion_input, 512),\n            nn.ReLU(),\n            nn.Dropout(0.4),\n            nn.Linear(512, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3)\n        )\n        \n        # Multi-output head\n        self.heads = nn.ModuleList([\n            nn.Linear(256, 1) for _ in range(num_targets)\n        ])\n    \n    def forward(self, image, tabular):\n        # Image features\n        img_features = self.backbone(image)\n        \n        # Tabular features\n        tab_features = self.tabular_bn(tabular)\n        tab_features = self.tabular_fc(tab_features)\n        \n        # Concatenate\n        combined = torch.cat([img_features, tab_features], dim=1)\n        \n        # Fusion\n        fused = self.fusion(combined)\n        \n        # Multi-output predictions\n        outputs = [head(fused) for head in self.heads]\n        outputs = torch.cat(outputs, dim=1)\n        \n        return outputs\n\n# Test model\nmodel = BiomassModel(CFG.model_name, CFG.pretrained).to(CFG.device)\ndummy_img = torch.randn(2, 3, CFG.img_size, CFG.img_size).to(CFG.device)\ndummy_tab = torch.randn(2, 6).to(CFG.device)\noutput = model(dummy_img, dummy_tab)\nprint(f\"Model output shape: {output.shape}\")\nprint(f\"Model parameters: {sum(p.numel() for p in model.parameters()):,}\")\nprint(\"Model architecture verified ✓\")\n\ndel model, dummy_img, dummy_tab, output\ngc.collect()\ntorch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:24:00.175750Z","iopub.execute_input":"2025-12-26T05:24:00.176247Z","iopub.status.idle":"2025-12-26T05:25:01.028891Z","shell.execute_reply.started":"2025-12-26T05:24:00.176199Z","shell.execute_reply":"2025-12-26T05:25:01.027826Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Training Functions","metadata":{}},{"cell_type":"code","source":"def train_epoch(model, loader, optimizer, criterion, scaler, device):\n    model.train()\n    running_loss = 0.0\n    \n    pbar = tqdm(loader, desc='Training')\n    for images, tabular, targets in pbar:\n        images = images.to(device)\n        tabular = tabular.to(device)\n        targets = targets.to(device)\n        \n        optimizer.zero_grad()\n        \n        with autocast():\n            outputs = model(images, tabular)\n            loss = criterion(outputs, targets)\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        running_loss += loss.item()\n        pbar.set_postfix({'loss': running_loss / (pbar.n + 1)})\n    \n    return running_loss / len(loader)\n\ndef validate_epoch(model, loader, criterion, device, weights):\n    model.eval()\n    running_loss = 0.0\n    all_preds = []\n    all_targets = []\n    \n    pbar = tqdm(loader, desc='Validation')\n    with torch.no_grad():\n        for images, tabular, targets in pbar:\n            images = images.to(device)\n            tabular = tabular.to(device)\n            targets = targets.to(device)\n            \n            outputs = model(images, tabular)\n            loss = criterion(outputs, targets)\n            \n            running_loss += loss.item()\n            all_preds.append(outputs.cpu().numpy())\n            all_targets.append(targets.cpu().numpy())\n            \n            pbar.set_postfix({'loss': running_loss / (pbar.n + 1)})\n    \n    all_preds = np.vstack(all_preds)\n    all_targets = np.vstack(all_targets)\n    \n    # Calculate weighted R²\n    r2 = weighted_r2_score(all_targets, all_preds, weights)\n    \n    return running_loss / len(loader), r2, all_preds, all_targets\n\nprint(\"Training functions defined ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:25:01.030445Z","iopub.execute_input":"2025-12-26T05:25:01.030843Z","iopub.status.idle":"2025-12-26T05:25:01.045687Z","shell.execute_reply.started":"2025-12-26T05:25:01.030810Z","shell.execute_reply":"2025-12-26T05:25:01.044415Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. K-Fold Cross-Validation Training","metadata":{}},{"cell_type":"code","source":"# Prepare K-Fold\nkf = KFold(n_splits=CFG.n_folds, shuffle=True, random_state=42)\nweights_list = [CFG.target_weights[t] for t in CFG.targets]\n\noof_predictions = np.zeros((len(train_wide), len(CFG.targets)))\nfold_scores = []\nfold_models = []\n\n# Verify data directory exists before starting\nimport os\nif not os.path.exists(CFG.data_dir):\n    raise FileNotFoundError(f\"Data directory not found: {CFG.data_dir}\\nPlease update CFG.data_dir in the configuration cell!\")\n\nprint(f\"Using data directory: {CFG.data_dir}\")\nprint(f\"Training with model: {CFG.model_name}\")\n\nfor fold, (train_idx, val_idx) in enumerate(kf.split(train_wide)):\n    print(f\"\\n{'='*50}\")\n    print(f\"Fold {fold + 1}/{CFG.n_folds}\")\n    print(f\"{'='*50}\")\n    \n    # Split data\n    train_fold = train_wide.iloc[train_idx]\n    val_fold = train_wide.iloc[val_idx]\n    \n    print(f\"Train samples: {len(train_fold)} | Val samples: {len(val_fold)}\")\n    \n    # Create datasets\n    train_dataset = BiomassDataset(train_fold, CFG.data_dir, CFG.targets, train_transform, is_test=False)\n    val_dataset = BiomassDataset(val_fold, CFG.data_dir, CFG.targets, val_transform, is_test=False)\n    \n    train_loader = DataLoader(train_dataset, batch_size=CFG.batch_size, shuffle=True, \n                              num_workers=CFG.num_workers, pin_memory=True, drop_last=CFG.drop_last)\n    val_loader = DataLoader(val_dataset, batch_size=CFG.batch_size, shuffle=False, \n                            num_workers=CFG.num_workers, pin_memory=True)\n    \n    # Initialize model\n    print(f\"Initializing {CFG.model_name}...\")\n    model = BiomassModel(CFG.model_name, CFG.pretrained).to(CFG.device)\n    \n    # Loss and optimizer\n    criterion = nn.MSELoss()\n    optimizer = torch.optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.epochs, eta_min=1e-6)\n    scaler = GradScaler()\n    \n    # Training loop\n    best_r2 = -np.inf\n    best_model_wts = None\n    \n    for epoch in range(CFG.epochs):\n        print(f\"\\nEpoch {epoch + 1}/{CFG.epochs}\")\n        \n        train_loss = train_epoch(model, train_loader, optimizer, criterion, scaler, CFG.device)\n        val_loss, val_r2, val_preds, val_targets = validate_epoch(model, val_loader, criterion, CFG.device, weights_list)\n        \n        scheduler.step()\n        \n        print(f\"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val R²: {val_r2:.4f}\")\n        \n        # Save best model\n        if val_r2 > best_r2:\n            best_r2 = val_r2\n            best_model_wts = model.state_dict().copy()\n            print(f\"✓ Best R² improved to {best_r2:.4f}\")\n    \n    # Load best weights\n    model.load_state_dict(best_model_wts)\n    \n    # Final validation\n    _, final_r2, final_preds, _ = validate_epoch(model, val_loader, criterion, CFG.device, weights_list)\n    \n    # Store OOF predictions\n    oof_predictions[val_idx] = final_preds\n    \n    fold_scores.append(final_r2)\n    fold_models.append(model.cpu())\n    \n    print(f\"\\nFold {fold + 1} Best R²: {final_r2:.4f}\")\n    \n    # Cleanup\n    del train_dataset, val_dataset, train_loader, val_loader\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# Overall CV score\noverall_r2 = weighted_r2_score(train_wide[CFG.targets].values, oof_predictions, weights_list)\nprint(f\"\\n{'='*50}\")\nprint(f\"Cross-Validation Results\")\nprint(f\"{'='*50}\")\nfor i, score in enumerate(fold_scores):\n    print(f\"Fold {i+1}: R² = {score:.4f}\")\nprint(f\"\\nMean CV R²: {np.mean(fold_scores):.4f} ± {np.std(fold_scores):.4f}\")\nprint(f\"Overall OOF R²: {overall_r2:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:25:01.047219Z","iopub.execute_input":"2025-12-26T05:25:01.047538Z","iopub.status.idle":"2025-12-26T05:33:15.382341Z","shell.execute_reply.started":"2025-12-26T05:25:01.047512Z","shell.execute_reply":"2025-12-26T05:33:15.380474Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Analyze Results","metadata":{}},{"cell_type":"code","source":"# Per-target R² scores\nprint(\"Per-target R² scores (OOF):\")\nfor i, target in enumerate(CFG.targets):\n    target_r2 = r2_score(train_wide[target].values, oof_predictions[:, i])\n    print(f\"{target}: {target_r2:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:33:15.385056Z","iopub.execute_input":"2025-12-26T05:33:15.385592Z","iopub.status.idle":"2025-12-26T05:33:15.401295Z","shell.execute_reply.started":"2025-12-26T05:33:15.385524Z","shell.execute_reply":"2025-12-26T05:33:15.399790Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Prediction vs Actual plots\nfig, axes = plt.subplots(2, 3, figsize=(18, 10))\naxes = axes.flatten()\n\nfor idx, target in enumerate(CFG.targets):\n    y_true = train_wide[target].values\n    y_pred = oof_predictions[:, idx]\n    \n    axes[idx].scatter(y_true, y_pred, alpha=0.3, s=10)\n    axes[idx].plot([y_true.min(), y_true.max()], [y_true.min(), y_true.max()], 'r--', lw=2)\n    axes[idx].set_xlabel('Actual')\n    axes[idx].set_ylabel('Predicted')\n    axes[idx].set_title(f'{target} (R² = {r2_score(y_true, y_pred):.3f})')\n    axes[idx].grid(alpha=0.3)\n\naxes[5].axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:33:15.402740Z","iopub.execute_input":"2025-12-26T05:33:15.403156Z","iopub.status.idle":"2025-12-26T05:33:16.430511Z","shell.execute_reply.started":"2025-12-26T05:33:15.403105Z","shell.execute_reply":"2025-12-26T05:33:16.429251Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Test Prediction & Submission","metadata":{}},{"cell_type":"code","source":"# Prepare test data (need to group by image since test.csv is in long format)\ntest_images = test_df.groupby('image_path').first().reset_index()\n\n# We don't have actual features for test, so we'll use defaults or extract from image names if possible\n# For this competition, we'll use median values for missing features\ntest_images['State_encoded'] = train_wide['State_encoded'].mode()[0]\ntest_images['Species_encoded'] = train_wide['Species_encoded'].mode()[0]\ntest_images['Month'] = 6  # Mid-year default\ntest_images['Season'] = 4  # Spring default\ntest_images['Pre_GSHH_NDVI'] = train_wide['Pre_GSHH_NDVI'].median()\ntest_images['Height_Ave_cm'] = train_wide['Height_Ave_cm'].median()\n\nprint(f\"Test images: {len(test_images)}\")\ntest_images.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:33:16.432182Z","iopub.execute_input":"2025-12-26T05:33:16.432563Z","iopub.status.idle":"2025-12-26T05:33:16.456371Z","shell.execute_reply.started":"2025-12-26T05:33:16.432534Z","shell.execute_reply":"2025-12-26T05:33:16.454907Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create test dataset\ntest_dataset = BiomassDataset(test_images, CFG.data_dir, CFG.targets, val_transform, is_test=True)\ntest_loader = DataLoader(test_dataset, batch_size=CFG.batch_size, shuffle=False, \n                         num_workers=CFG.num_workers, pin_memory=True)\n\n# Ensemble predictions from all folds\ntest_predictions = np.zeros((len(test_images), len(CFG.targets)))\n\nfor fold, model in enumerate(fold_models):\n    print(f\"Predicting with Fold {fold + 1} model...\")\n    model.to(CFG.device)\n    model.eval()\n    \n    fold_preds = []\n    with torch.no_grad():\n        for images, tabular in tqdm(test_loader):\n            images = images.to(CFG.device)\n            tabular = tabular.to(CFG.device)\n            \n            outputs = model(images, tabular)\n            fold_preds.append(outputs.cpu().numpy())\n    \n    fold_preds = np.vstack(fold_preds)\n    test_predictions += fold_preds / CFG.n_folds\n    \n    model.cpu()\n    torch.cuda.empty_cache()\n\nprint(f\"Test predictions shape: {test_predictions.shape}\")\nprint(\"Predictions complete ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:33:16.458291Z","iopub.execute_input":"2025-12-26T05:33:16.458666Z","iopub.status.idle":"2025-12-26T05:33:18.713068Z","shell.execute_reply.started":"2025-12-26T05:33:16.458628Z","shell.execute_reply":"2025-12-26T05:33:18.711255Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create submission in required format\n# Each row in test.csv needs a prediction\nsubmission_rows = []\n\nfor idx, row in test_df.iterrows():\n    image_path = row['image_path']\n    target_name = row['target_name']\n    sample_id = row['sample_id']\n    \n    # Find the corresponding prediction\n    img_idx = test_images[test_images['image_path'] == image_path].index[0]\n    target_idx = CFG.targets.index(target_name)\n    \n    prediction = test_predictions[img_idx, target_idx]\n    \n    submission_rows.append({\n        'sample_id': sample_id,\n        'target': max(0, prediction)  # Ensure non-negative predictions\n    })\n\nsubmission = pd.DataFrame(submission_rows)\n\nprint(f\"Submission shape: {submission.shape}\")\nprint(f\"Expected shape: {len(test_df)} rows\")\nprint(f\"\\nSubmission preview:\")\nprint(submission.head(10))\nprint(f\"\\nSubmission statistics:\")\nprint(submission['target'].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:33:18.716134Z","iopub.execute_input":"2025-12-26T05:33:18.716608Z","iopub.status.idle":"2025-12-26T05:33:18.751305Z","shell.execute_reply.started":"2025-12-26T05:33:18.716560Z","shell.execute_reply":"2025-12-26T05:33:18.749795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Validate submission format\nassert submission.shape[0] == test_df.shape[0], \"Row count mismatch!\"\nassert list(submission.columns) == ['sample_id', 'target'], \"Column mismatch!\"\nassert submission['sample_id'].equals(test_df['sample_id']), \"Sample IDs don't match!\"\nassert submission['target'].notna().all(), \"Missing predictions!\"\n\nprint(\"✓ Submission validation passed!\")\n\n# Save submission\nsubmission.to_csv('submission.csv', index=False)\nprint(\"\\n✓ Submission saved to submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T05:33:18.752744Z","iopub.execute_input":"2025-12-26T05:33:18.753584Z","iopub.status.idle":"2025-12-26T05:33:18.767907Z","shell.execute_reply.started":"2025-12-26T05:33:18.753543Z","shell.execute_reply":"2025-12-26T05:33:18.766620Z"}},"outputs":[],"execution_count":null}]}