{"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"}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom PIL import Image\n\nimport tensorflow as tf\n\n\nsns.set_style(\"whitegrid\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:05.118566Z","iopub.execute_input":"2026-02-04T19:00:05.118928Z","iopub.status.idle":"2026-02-04T19:00:05.124461Z","shell.execute_reply.started":"2026-02-04T19:00:05.118901Z","shell.execute_reply":"2026-02-04T19:00:05.123559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_PATH = \"/kaggle/input/csiro-biomass/\"\ntrain = BASE_PATH + \"train.csv\"\ntest  = BASE_PATH + \"test.csv\"\n\ndf_train = pd.read_csv(train)\ndf_test = pd.read_csv(test)\n\ndisplay(df_train)\ndisplay(df_test)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:05.181164Z","iopub.execute_input":"2026-02-04T19:00:05.181908Z","iopub.status.idle":"2026-02-04T19:00:05.212307Z","shell.execute_reply.started":"2026-02-04T19:00:05.181877Z","shell.execute_reply":"2026-02-04T19:00:05.211644Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Train shape: {df_train.shape} | Test shape: {df_test.shape}\")\nprint(f\"Unique images: {df_train['image_path'].nunique()}\")\nprint(f\"Target types: {df_train['target_name'].unique()}\")\nprint(f\"\\nTarget distribution:\")\nprint(df_train['target_name'].value_counts())\nprint(f\"\\nTarget statistics:\")\nprint(df_train.groupby('target_name')['target'].describe().round(2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:05.213598Z","iopub.execute_input":"2026-02-04T19:00:05.213848Z","iopub.status.idle":"2026-02-04T19:00:05.237161Z","shell.execute_reply.started":"2026-02-04T19:00:05.213825Z","shell.execute_reply":"2026-02-04T19:00:05.236286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n\nsns.countplot(data=df_train, x=\"target_name\", ax=axes[0,0])\naxes[0,0].set_title(\"Training Samples per Target Type\")\naxes[0,0].tick_params(axis='x', rotation=45)\n\nsns.histplot(df_train[\"Pre_GSHH_NDVI\"], bins=40, ax=axes[0,1], kde=True)\naxes[0,1].set_title(\"NDVI Distribution\")\n\nsns.histplot(df_train[\"Height_Ave_cm\"], bins=40, ax=axes[1,0], kde=True, color='orange')\naxes[1,0].set_title(\"Height Distribution\")\n\nfor target in df_train['target_name'].unique():\n    subset = df_train[df_train['target_name']==target]\n    axes[1,1].scatter(subset['Pre_GSHH_NDVI'], subset['target'], alpha=0.3, label=target, s=10)\naxes[1,1].set_xlabel(\"NDVI\")\naxes[1,1].set_ylabel(\"Target (g)\")\naxes[1,1].set_title(\"NDVI vs Biomass by Target Type\")\naxes[1,1].legend(fontsize=8)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:05.238384Z","iopub.execute_input":"2026-02-04T19:00:05.238653Z","iopub.status.idle":"2026-02-04T19:00:06.232932Z","shell.execute_reply.started":"2026-02-04T19:00:05.238631Z","shell.execute_reply":"2026-02-04T19:00:06.232125Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train_corr = df_train.copy()\ndf_train_corr['base_id'] = df_train_corr['sample_id'].str.rsplit('_', n=2).str[0]\n\nwide = df_train_corr.pivot_table(index=\"base_id\", columns=\"target_name\", values=\"target\", aggfunc=\"mean\")\n\nprint(f\"Correlation matrix shape: {wide.shape}\")\nprint(f\"Number of unique images: {len(wide)}\")\nprint(f\"Target types: {list(wide.columns)}\")\nprint(f\"\\nSample of pivot table:\")\nprint(wide.head())\nprint(f\"\\nMissing values in pivot table: {wide.isnull().sum().sum()}\")\n\nplt.figure(figsize=(8,6))\nsns.heatmap(wide.corr(), annot=True, fmt=\".2f\", cmap=\"coolwarm\", vmin=-1, vmax=1, center=0)\nplt.title(\"Correlation Between Biomass Target Types\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:06.235494Z","iopub.execute_input":"2026-02-04T19:00:06.235761Z","iopub.status.idle":"2026-02-04T19:00:06.516317Z","shell.execute_reply.started":"2026-02-04T19:00:06.235737Z","shell.execute_reply":"2026-02-04T19:00:06.515462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_rows = df_train.sample(5, random_state=42)\n\nplt.figure(figsize=(20, 4))\nfor i, (idx, row) in enumerate(sample_rows.iterrows()):\n    full_path = os.path.join(BASE_PATH, row[\"image_path\"])\n    img = Image.open(full_path)\n    \n    plt.subplot(1, 5, i + 1)\n    plt.imshow(img)\n    plt.title(f\"NDVI: {row['Pre_GSHH_NDVI']:.2f}\\nHeight: {row['Height_Ave_cm']:.1f}cm\\n{row['target_name']}: {row['target']:.1f}g\", fontsize=9)\n    plt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:06.517582Z","iopub.execute_input":"2026-02-04T19:00:06.518286Z","iopub.status.idle":"2026-02-04T19:00:07.801625Z","shell.execute_reply.started":"2026-02-04T19:00:06.518259Z","shell.execute_reply":"2026-02-04T19:00:07.800617Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if 'Species' in df_train.columns:\n    print(\"Species Analysis\")\n    print(\"=\"*60)\n    print(f\"\\nUnique species: {df_train['Species'].nunique()}\")\n    print(f\"\\nSpecies distribution:\")\n    print(df_train['Species'].value_counts())\n    \n    fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n    \n    sns.countplot(data=df_train, x=\"Species\", ax=axes[0,0])\n    axes[0,0].set_title(\"Sample Count by Species\")\n    axes[0,0].tick_params(axis='x', rotation=45)\n    \n    species_target_stats = df_train.groupby('Species')['target'].agg(['mean', 'median', 'std'])\n    species_target_stats.plot(kind='bar', ax=axes[0,1])\n    axes[0,1].set_title(\"Biomass Statistics by Species\")\n    axes[0,1].set_ylabel(\"Biomass (g)\")\n    axes[0,1].tick_params(axis='x', rotation=45)\n    axes[0,1].legend(['Mean', 'Median', 'Std'])\n    \n    for species in df_train['Species'].unique():\n        subset = df_train[df_train['Species'] == species]\n        axes[1,0].scatter(subset['Pre_GSHH_NDVI'], subset['Height_Ave_cm'], \n                         alpha=0.4, label=species, s=15)\n    axes[1,0].set_xlabel(\"NDVI\")\n    axes[1,0].set_ylabel(\"Height (cm)\")\n    axes[1,0].set_title(\"NDVI vs Height by Species\")\n    axes[1,0].legend(fontsize=8)\n    \n    species_counts = df_train.groupby(['Species', 'target_name']).size().unstack(fill_value=0)\n    species_counts.plot(kind='bar', stacked=True, ax=axes[1,1])\n    axes[1,1].set_title(\"Target Type Distribution by Species\")\n    axes[1,1].set_ylabel(\"Sample Count\")\n    axes[1,1].tick_params(axis='x', rotation=45)\n    axes[1,1].legend(title=\"Target Type\", fontsize=8)\n    \n    plt.tight_layout()\n    plt.show()\n    \nelse:\n    print(\"No 'Species' column found in the dataset\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:07.803463Z","iopub.execute_input":"2026-02-04T19:00:07.803762Z","iopub.status.idle":"2026-02-04T19:00:09.263472Z","shell.execute_reply.started":"2026-02-04T19:00:07.803738Z","shell.execute_reply":"2026-02-04T19:00:09.262500Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Processing and Scaling","metadata":{}},{"cell_type":"code","source":"from sklearn.preprocessing import StandardScaler\nimport pickle\n\ndf_train_shuffled = df_train.sample(frac=1, random_state=42).reset_index(drop=True)\nsplit_idx = int(0.9 * len(df_train_shuffled))\ntrain_df_raw, val_df_raw = df_train_shuffled[:split_idx], df_train_shuffled[split_idx:]\n\nscalers = {}\ntrain_df = train_df_raw.copy()\nval_df = val_df_raw.copy()\n\nfor target_name in df_train[\"target_name\"].unique():\n    scaler = StandardScaler()\n    \n    train_mask = train_df[\"target_name\"] == target_name\n    train_df.loc[train_mask, \"target\"] = scaler.fit_transform(\n        train_df_raw.loc[train_mask, \"target\"].values.reshape(-1, 1)\n    ).flatten()\n    \n    val_mask = val_df[\"target_name\"] == target_name\n    val_df.loc[val_mask, \"target\"] = scaler.transform(\n        val_df_raw.loc[val_mask, \"target\"].values.reshape(-1, 1)\n    ).flatten()\n    \n    scalers[target_name] = scaler\n    print(f\"{target_name}: μ={scaler.mean_[0]:.2f}, σ={scaler.scale_[0]:.2f}\")\n\nwith open(\"target_scalers.pkl\", \"wb\") as f:\n    pickle.dump(scalers, f)\n\nprint(f\"\\nTrain: {len(train_df)} samples | Val: {len(val_df)} samples\")\nprint(f\"Scaled range: [{train_df['target'].min():.2f}, {train_df['target'].max():.2f}]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:09.264638Z","iopub.execute_input":"2026-02-04T19:00:09.264912Z","iopub.status.idle":"2026-02-04T19:00:09.293451Z","shell.execute_reply.started":"2026-02-04T19:00:09.264888Z","shell.execute_reply":"2026-02-04T19:00:09.292737Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset Pipeline","metadata":{}},{"cell_type":"code","source":"IMG_SIZE = (224, 224)\n\ndef load_image(path, y):\n    x = tf.io.read_file(path)\n    x = tf.io.decode_jpeg(x, channels=3)\n    x = tf.image.resize(x, IMG_SIZE)\n    x = tf.image.convert_image_dtype(x, tf.float32)\n    x = (x - 0.5) * 2.0\n    return x, y\n\ndef augment(x, y):\n    x = tf.image.random_flip_left_right(x)\n    x = tf.image.random_flip_up_down(x)\n    x = tf.image.random_brightness(x, 0.2)\n    x = tf.image.random_contrast(x, 0.8, 1.2)\n    x = tf.image.random_saturation(x, 0.8, 1.2)\n    x = tf.clip_by_value(x, -1.0, 1.0)\n    return x, y\n\ndef make_dataset(df, shuffle=True, augment_data=False, batch_size=32):\n    paths = df[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\n    targets = df[\"target\"].values.astype(\"float32\")\n    \n    ds = tf.data.Dataset.from_tensor_slices((paths, targets))\n    ds = ds.map(load_image, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if augment_data:\n        ds = ds.map(augment, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if shuffle:\n        ds = ds.shuffle(2048)\n    \n    ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    return ds\n\ntrain_ds = make_dataset(train_df, shuffle=True, augment_data=True)\nval_ds = make_dataset(val_df, shuffle=False, augment_data=False)\n\nprint(f\"Datasets created: Train={len(train_df)} | Val={len(val_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:09.294449Z","iopub.execute_input":"2026-02-04T19:00:09.294765Z","iopub.status.idle":"2026-02-04T19:00:09.412985Z","shell.execute_reply.started":"2026-02-04T19:00:09.294730Z","shell.execute_reply":"2026-02-04T19:00:09.411777Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Multi-Input Model","metadata":{}},{"cell_type":"code","source":"def make_dataset_with_metadata(df, shuffle=True, augment_data=False, batch_size=32):\n    paths = df[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\n    targets = df[\"target\"].values.astype(\"float32\")\n    \n    ndvi = df[\"Pre_GSHH_NDVI\"].values.astype(\"float32\")\n    height = df[\"Height_Ave_cm\"].values.astype(\"float32\")\n    \n    ndvi = (ndvi - ndvi.min()) / (ndvi.max() - ndvi.min() + 1e-7)\n    height = (height - height.min()) / (height.max() - height.min() + 1e-7)\n    \n    metadata = np.stack([ndvi, height], axis=1)\n    \n    def load_image_with_meta(path, meta, y):\n        x = tf.io.read_file(path)\n        x = tf.io.decode_jpeg(x, channels=3)\n        x = tf.image.resize(x, IMG_SIZE)\n        x = tf.image.convert_image_dtype(x, tf.float32)\n        x = (x - 0.5) * 2.0\n        return (x, meta), y\n    \n    def augment_with_meta(inputs, y):\n        x, meta = inputs\n        x = tf.image.random_flip_left_right(x)\n        x = tf.image.random_flip_up_down(x)\n        x = tf.image.random_brightness(x, 0.2)\n        x = tf.image.random_contrast(x, 0.8, 1.2)\n        x = tf.image.random_saturation(x, 0.8, 1.2)\n        x = tf.clip_by_value(x, -1.0, 1.0)\n        return (x, meta), y\n    \n    ds = tf.data.Dataset.from_tensor_slices((paths, metadata, targets))\n    ds = ds.map(load_image_with_meta, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if augment_data:\n        ds = ds.map(augment_with_meta, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if shuffle:\n        ds = ds.shuffle(2048)\n    \n    return ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\ntrain_ds_meta = make_dataset_with_metadata(train_df, shuffle=True, augment_data=True)\nval_ds_meta = make_dataset_with_metadata(val_df, shuffle=False, augment_data=False)\n\nprint(f\"Multi-input pipeline: {len(train_df)} train | {len(val_df)} val\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:09.413976Z","iopub.execute_input":"2026-02-04T19:00:09.414313Z","iopub.status.idle":"2026-02-04T19:00:09.527367Z","shell.execute_reply.started":"2026-02-04T19:00:09.414289Z","shell.execute_reply":"2026-02-04T19:00:09.526612Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_model():\n    img_input = tf.keras.Input(shape=(224, 224, 3), name='image')\n    \n    x = tf.keras.layers.Conv2D(32, 3, activation='relu', padding='same')(img_input)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.Conv2D(64, 3, activation='relu', padding='same')(x)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.Conv2D(128, 3, activation='relu', padding='same')(x)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.Conv2D(256, 3, activation='relu', padding='same')(x)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    \n    meta_input = tf.keras.Input(shape=(2,), name='metadata')\n    meta = tf.keras.layers.Dense(32, activation='relu')(meta_input)\n    \n    combined = tf.keras.layers.concatenate([x, meta])\n    \n    z = tf.keras.layers.Dense(128, activation='relu')(combined)\n    z = tf.keras.layers.Dropout(0.5)(z)\n    z = tf.keras.layers.Dense(64, activation='relu')(z)\n    z = tf.keras.layers.Dropout(0.3)(z)\n    \n    output = tf.keras.layers.Dense(1)(z)\n    \n    model = tf.keras.Model(inputs=[img_input, meta_input], outputs=output)\n    return model\n\nmodel_multi = build_model()\nprint(f\"Model created | Total params: {model_multi.count_params():,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:09.530008Z","iopub.execute_input":"2026-02-04T19:00:09.530356Z","iopub.status.idle":"2026-02-04T19:00:09.608393Z","shell.execute_reply.started":"2026-02-04T19:00:09.530330Z","shell.execute_reply":"2026-02-04T19:00:09.607602Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_multi.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:09.609292Z","iopub.execute_input":"2026-02-04T19:00:09.609601Z","iopub.status.idle":"2026-02-04T19:00:09.639788Z","shell.execute_reply.started":"2026-02-04T19:00:09.609550Z","shell.execute_reply":"2026-02-04T19:00:09.638872Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_multi.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n    loss='mse',\n    metrics=['mae']\n)\n\ncallback = tf.keras.callbacks.ModelCheckpoint(\n    'best_model.keras', \n    save_best_only=True, \n    monitor='val_loss'\n)\n\nprint(\"Training model...\")\nhistory_multi = model_multi.fit(\n    train_ds_meta,\n    epochs=15,\n    validation_data=val_ds_meta,\n    callbacks=[callback],\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:00:09.640749Z","iopub.execute_input":"2026-02-04T19:00:09.641089Z","iopub.status.idle":"2026-02-04T19:09:06.934043Z","shell.execute_reply.started":"2026-02-04T19:00:09.641044Z","shell.execute_reply":"2026-02-04T19:09:06.933065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Enhanced Model with Vegetation Indices (Color-Based Features)","metadata":{}},{"cell_type":"code","source":"def calculate_vegetation_indices(image):\n    R = image[..., 0]\n    G = image[..., 1]\n    B = image[..., 2]\n    \n    exg = 2.0 * G - R - B\n    exgr = exg - (1.4 * R - G)\n    \n    return tf.stack([exg, exgr], axis=-1)\n\ndef load_image_with_indices(path, y):\n    x = tf.io.read_file(path)\n    x = tf.io.decode_jpeg(x, channels=3)\n    x = tf.image.resize(x, IMG_SIZE)\n    x = tf.image.convert_image_dtype(x, tf.float32)\n    \n    indices = calculate_vegetation_indices(x)\n    x = (x - 0.5) * 2.0\n    \n    x_enhanced = tf.concat([x, indices], axis=-1)\n    return x_enhanced, y\n\ndef augment_enhanced(x, y):\n    rgb = x[..., :3]\n    indices = x[..., 3:]\n    \n    rgb = tf.image.random_flip_left_right(rgb)\n    rgb = tf.image.random_flip_up_down(rgb)\n    rgb = tf.image.random_brightness(rgb, 0.2)\n    rgb = tf.image.random_contrast(rgb, 0.8, 1.2)\n    rgb = tf.image.random_saturation(rgb, 0.8, 1.2)\n    rgb = tf.clip_by_value(rgb, -1.0, 1.0)\n    \n    x_enhanced = tf.concat([rgb, indices], axis=-1)\n    return x_enhanced, y\n\ndef make_dataset_enhanced(df, shuffle=True, augment_data=False, batch_size=32):\n    paths = df[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\n    targets = df[\"target\"].values.astype(\"float32\")\n    \n    ds = tf.data.Dataset.from_tensor_slices((paths, targets))\n    ds = ds.map(load_image_with_indices, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if augment_data:\n        ds = ds.map(augment_enhanced, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if shuffle:\n        ds = ds.shuffle(2048)\n    \n    ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    return ds\n\ntrain_ds_enhanced = make_dataset_enhanced(train_df, shuffle=True, augment_data=True)\nval_ds_enhanced = make_dataset_enhanced(val_df, shuffle=False, augment_data=False)\n\nprint(f\"Enhanced dataset with vegetation indices created\")\nprint(f\"Input shape: (224, 224, 5) - RGB + ExG + ExGR\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:09:06.935448Z","iopub.execute_input":"2026-02-04T19:09:06.935911Z","iopub.status.idle":"2026-02-04T19:09:07.506010Z","shell.execute_reply.started":"2026-02-04T19:09:06.935864Z","shell.execute_reply":"2026-02-04T19:09:07.505121Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_dataset_with_metadata_enhanced(df, shuffle=True, augment_data=False, batch_size=32):\n    paths = df[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\n    targets = df[\"target\"].values.astype(\"float32\")\n    \n    ndvi = df[\"Pre_GSHH_NDVI\"].values.astype(\"float32\")\n    height = df[\"Height_Ave_cm\"].values.astype(\"float32\")\n    \n    ndvi = (ndvi - ndvi.min()) / (ndvi.max() - ndvi.min() + 1e-7)\n    height = (height - height.min()) / (height.max() - height.min() + 1e-7)\n    \n    metadata = np.stack([ndvi, height], axis=1)\n    \n    def load_image_indices_meta(path, meta, y):\n        x = tf.io.read_file(path)\n        x = tf.io.decode_jpeg(x, channels=3)\n        x = tf.image.resize(x, IMG_SIZE)\n        x = tf.image.convert_image_dtype(x, tf.float32)\n        \n        indices = calculate_vegetation_indices(x)\n        x = (x - 0.5) * 2.0\n        x_enhanced = tf.concat([x, indices], axis=-1)\n        \n        return (x_enhanced, meta), y\n    \n    def augment_enhanced_meta(inputs, y):\n        x, meta = inputs\n        rgb = x[..., :3]\n        indices = x[..., 3:]\n        \n        rgb = tf.image.random_flip_left_right(rgb)\n        rgb = tf.image.random_flip_up_down(rgb)\n        rgb = tf.image.random_brightness(rgb, 0.2)\n        rgb = tf.image.random_contrast(rgb, 0.8, 1.2)\n        rgb = tf.image.random_saturation(rgb, 0.8, 1.2)\n        rgb = tf.clip_by_value(rgb, -1.0, 1.0)\n        \n        x_enhanced = tf.concat([rgb, indices], axis=-1)\n        return (x_enhanced, meta), y\n    \n    ds = tf.data.Dataset.from_tensor_slices((paths, metadata, targets))\n    ds = ds.map(load_image_indices_meta, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if augment_data:\n        ds = ds.map(augment_enhanced_meta, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if shuffle:\n        ds = ds.shuffle(2048)\n    \n    return ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\ntrain_ds_meta_enhanced = make_dataset_with_metadata_enhanced(train_df, shuffle=True, augment_data=True)\nval_ds_meta_enhanced = make_dataset_with_metadata_enhanced(val_df, shuffle=False, augment_data=False)\n\nprint(f\"Enhanced multi-input pipeline created\")\nprint(f\"Image: (224, 224, 5) + Metadata: (2,)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:09:07.507244Z","iopub.execute_input":"2026-02-04T19:09:07.507702Z","iopub.status.idle":"2026-02-04T19:09:07.750943Z","shell.execute_reply.started":"2026-02-04T19:09:07.507667Z","shell.execute_reply":"2026-02-04T19:09:07.749978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_enhanced_model():\n    img_input = tf.keras.Input(shape=(224, 224, 5), name='image')\n    \n    x = tf.keras.layers.Conv2D(32, 3, activation='relu', padding='same')(img_input)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.Conv2D(64, 3, activation='relu', padding='same')(x)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.Conv2D(128, 3, activation='relu', padding='same')(x)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.Conv2D(256, 3, activation='relu', padding='same')(x)\n    x = tf.keras.layers.MaxPooling2D(2)(x)\n    \n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    \n    meta_input = tf.keras.Input(shape=(2,), name='metadata')\n    meta = tf.keras.layers.Dense(32, activation='relu')(meta_input)\n    \n    combined = tf.keras.layers.concatenate([x, meta])\n    \n    z = tf.keras.layers.Dense(128, activation='relu')(combined)\n    z = tf.keras.layers.Dropout(0.5)(z)\n    z = tf.keras.layers.Dense(64, activation='relu')(z)\n    z = tf.keras.layers.Dropout(0.3)(z)\n    \n    output = tf.keras.layers.Dense(1)(z)\n    \n    model = tf.keras.Model(inputs=[img_input, meta_input], outputs=output)\n    return model\n\nmodel_enhanced = build_enhanced_model()\nprint(f\"Enhanced model created | Params: {model_enhanced.count_params():,}\")\nprint(\"Input: RGB (3 ch) + ExG + ExGR (2 ch) = 5 channels total\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:09:07.752272Z","iopub.execute_input":"2026-02-04T19:09:07.752585Z","iopub.status.idle":"2026-02-04T19:09:07.836495Z","shell.execute_reply.started":"2026-02-04T19:09:07.752558Z","shell.execute_reply":"2026-02-04T19:09:07.835748Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_enhanced.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:09:07.837376Z","iopub.execute_input":"2026-02-04T19:09:07.837680Z","iopub.status.idle":"2026-02-04T19:09:07.869638Z","shell.execute_reply.started":"2026-02-04T19:09:07.837640Z","shell.execute_reply":"2026-02-04T19:09:07.868929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_enhanced.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n    loss='mse',\n    metrics=['mae']\n)\n\ncallback_enhanced = tf.keras.callbacks.ModelCheckpoint(\n    'best_model_enhanced.keras', \n    save_best_only=True, \n    monitor='val_loss'\n)\n\nprint(\"Training enhanced model with color features...\")\nhistory_enhanced = model_enhanced.fit(\n    train_ds_meta_enhanced,\n    epochs=15,\n    validation_data=val_ds_meta_enhanced,\n    callbacks=[callback_enhanced],\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:09:07.870670Z","iopub.execute_input":"2026-02-04T19:09:07.871078Z","iopub.status.idle":"2026-02-04T19:18:45.033605Z","shell.execute_reply.started":"2026-02-04T19:09:07.871054Z","shell.execute_reply":"2026-02-04T19:18:45.032589Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Test Data Preperation","metadata":{}},{"cell_type":"code","source":"if 'target_name' in df_test.columns:\n    df_test_expanded = df_test.copy()\nelse:\n    unique_images = df_test.drop_duplicates(subset=['image_path']).copy()\n    target_names = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    expanded_rows = []\n    for _, img_row in unique_images.iterrows():\n        for target_name in target_names:\n            row = img_row.copy()\n            row['target_name'] = target_name\n            img_id = img_row['sample_id'] if 'sample_id' in img_row else img_row['image_path'].split('/')[-1].split('.')[0]\n            row['sample_id'] = f\"{img_id}_{target_name}\"\n            expanded_rows.append(row)\n    \n    df_test_expanded = pd.DataFrame(expanded_rows).reset_index(drop=True)\n\nprint(f\"Test expanded: {df_test_expanded.shape} | {df_test_expanded['target_name'].value_counts().to_dict()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:18:45.034947Z","iopub.execute_input":"2026-02-04T19:18:45.035320Z","iopub.status.idle":"2026-02-04T19:18:45.044375Z","shell.execute_reply.started":"2026-02-04T19:18:45.035290Z","shell.execute_reply":"2026-02-04T19:18:45.043257Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"RGB Model Predict\\","metadata":{}},{"cell_type":"code","source":"test_paths = df_test_expanded[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\nhas_metadata = 'Pre_GSHH_NDVI' in df_test_expanded.columns and 'Height_Ave_cm' in df_test_expanded.columns\n\nif has_metadata:\n    ndvi_test = df_test_expanded[\"Pre_GSHH_NDVI\"].values.astype(\"float32\")\n    height_test = df_test_expanded[\"Height_Ave_cm\"].values.astype(\"float32\")\n    \n    train_ndvi, train_height = train_df[\"Pre_GSHH_NDVI\"].values, train_df[\"Height_Ave_cm\"].values\n    ndvi_test = (ndvi_test - train_ndvi.min()) / (train_ndvi.max() - train_ndvi.min() + 1e-7)\n    height_test = (height_test - train_height.min()) / (train_height.max() - train_height.min() + 1e-7)\nelse:\n    train_ndvi, train_height = train_df[\"Pre_GSHH_NDVI\"].values, train_df[\"Height_Ave_cm\"].values\n    ndvi_mean = (train_ndvi.mean() - train_ndvi.min()) / (train_ndvi.max() - train_ndvi.min())\n    height_mean = (train_height.mean() - train_height.min()) / (train_height.max() - train_height.min())\n    \n    ndvi_test = np.full(len(df_test_expanded), ndvi_mean, dtype=\"float32\")\n    height_test = np.full(len(df_test_expanded), height_mean, dtype=\"float32\")\n    print(f\"Warning: Using mean imputation for metadata\")\n\nmetadata_test = np.stack([ndvi_test, height_test], axis=1)\n\ndef load_test_image_with_meta(path, meta):\n    x = tf.io.read_file(path)\n    x = tf.io.decode_jpeg(x, channels=3)\n    x = tf.image.resize(x, IMG_SIZE)\n    x = tf.image.convert_image_dtype(x, tf.float32)\n    x = (x - 0.5) * 2.0\n    return {'image': x, 'metadata': meta}\n\ntest_ds_meta = tf.data.Dataset.from_tensor_slices((test_paths, metadata_test))\ntest_ds_meta = test_ds_meta.map(load_test_image_with_meta, num_parallel_calls=tf.data.AUTOTUNE).batch(32).prefetch(tf.data.AUTOTUNE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:18:45.045761Z","iopub.execute_input":"2026-02-04T19:18:45.046136Z","iopub.status.idle":"2026-02-04T19:18:45.121792Z","shell.execute_reply.started":"2026-02-04T19:18:45.046092Z","shell.execute_reply":"2026-02-04T19:18:45.120851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Making predictions...\")\npreds_multi = model_multi.predict(test_ds_meta).squeeze()\n\npreds_multi_original = np.zeros_like(preds_multi)\nfor i, (_, row) in enumerate(df_test_expanded.iterrows()):\n    target_type = row[\"target_name\"]\n    scaled_pred = preds_multi[i]\n    preds_multi_original[i] = scalers[target_type].inverse_transform([[scaled_pred]])[0, 0]\n\npreds_multi_original = np.maximum(0, preds_multi_original)\n\nprint(f\"RGB model predictions: {len(preds_multi_original)} samples\")\nprint(f\"Range: [{preds_multi_original.min():.2f}g, {preds_multi_original.max():.2f}g]\")\nprint(f\"Mean: {preds_multi_original.mean():.2f}g\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:18:45.140598Z","iopub.execute_input":"2026-02-04T19:18:45.140927Z","iopub.status.idle":"2026-02-04T19:18:46.368397Z","shell.execute_reply.started":"2026-02-04T19:18:45.140899Z","shell.execute_reply":"2026-02-04T19:18:46.367637Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Enhanced Model Predict","metadata":{}},{"cell_type":"code","source":"test_paths_enhanced = df_test_expanded[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\n\nmetadata_test_enhanced = np.stack([ndvi_test, height_test], axis=1)\n\ndef load_test_image_indices_meta(path, meta):\n    x = tf.io.read_file(path)\n    x = tf.io.decode_jpeg(x, channels=3)\n    x = tf.image.resize(x, IMG_SIZE)\n    x = tf.image.convert_image_dtype(x, tf.float32)\n    \n    indices = calculate_vegetation_indices(x)\n    x = (x - 0.5) * 2.0\n    x_enhanced = tf.concat([x, indices], axis=-1)\n    \n    return {'image': x_enhanced, 'metadata': meta}\n\ntest_ds_meta_enhanced = tf.data.Dataset.from_tensor_slices((test_paths_enhanced, metadata_test_enhanced))\ntest_ds_meta_enhanced = test_ds_meta_enhanced.map(load_test_image_indices_meta, num_parallel_calls=tf.data.AUTOTUNE).batch(32).prefetch(tf.data.AUTOTUNE)\n\nprint(f\"Enhanced test dataset prepared with vegetation indices\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:18:46.369423Z","iopub.execute_input":"2026-02-04T19:18:46.369713Z","iopub.status.idle":"2026-02-04T19:18:46.453107Z","shell.execute_reply.started":"2026-02-04T19:18:46.369689Z","shell.execute_reply":"2026-02-04T19:18:46.452227Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Making enhanced predictions...\")\npreds_enhanced = model_enhanced.predict(test_ds_meta_enhanced).squeeze()\n\npreds_enhanced_original = np.zeros_like(preds_enhanced)\nfor i, (_, row) in enumerate(df_test_expanded.iterrows()):\n    target_type = row[\"target_name\"]\n    scaled_pred = preds_enhanced[i]\n    preds_enhanced_original[i] = scalers[target_type].inverse_transform([[scaled_pred]])[0, 0]\n\npreds_enhanced_original = np.maximum(0, preds_enhanced_original)\n\nprint(f\"Enhanced model predictions: {len(preds_enhanced_original)} samples\")\nprint(f\"Range: [{preds_enhanced_original.min():.2f}g, {preds_enhanced_original.max():.2f}g]\")\nprint(f\"Mean: {preds_enhanced_original.mean():.2f}g\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:18:46.454487Z","iopub.execute_input":"2026-02-04T19:18:46.454852Z","iopub.status.idle":"2026-02-04T19:18:47.222145Z","shell.execute_reply.started":"2026-02-04T19:18:46.454819Z","shell.execute_reply":"2026-02-04T19:18:47.221184Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Final Submission","metadata":{}},{"cell_type":"code","source":"ensemble_predictions = 0.5 * preds_multi_original + 0.5 * preds_enhanced_original\n\nfinal_submission = pd.DataFrame({\n    \"sample_id\": df_test_expanded[\"sample_id\"],\n    \"target\": ensemble_predictions,\n})\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"FINAL SUBMISSION\")\nprint(\"=\"*60)\nprint(f\"Total predictions: {len(final_submission)}\")\nprint(f\"Prediction range: [{ensemble_predictions.min():.2f}g to {ensemble_predictions.max():.2f}g]\")\nprint(f\"Average prediction: {ensemble_predictions.mean():.2f}g\")\n\nfinal_submission.to_csv(\"submission.csv\", index=False)\nprint(\"\\nSubmission saved to: submission.csv\")\nprint(\"=\"*60)\n\nprint(\"\\nModel Comparison:\")\nprint(f\"RGB model average:      {preds_multi_original.mean():.2f}g\")\nprint(f\"Enhanced model average: {preds_enhanced_original.mean():.2f}g\")\nprint(f\"Final ensemble average: {ensemble_predictions.mean():.2f}g\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T19:18:47.223547Z","iopub.execute_input":"2026-02-04T19:18:47.223836Z","iopub.status.idle":"2026-02-04T19:18:47.238282Z","shell.execute_reply.started":"2026-02-04T19:18:47.223812Z","shell.execute_reply":"2026-02-04T19:18:47.237509Z"}},"outputs":[],"execution_count":null}]}