{"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":31259,"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\nfrom pathlib import Path\n\n\nimport tensorflow as tf\nfrom tqdm.auto import tqdm\n\n\nsns.set_style(\"whitegrid\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train.info()\n\nna = df_train.isna().mean().sort_values(ascending=False)\ndisplay(na[na>0])\n\ndup = df_train.duplicated(subset=[\"sample_id\",\"target_name\"]).sum()\nprint(\"Duplicate sample_id-target_name rows:\", dup)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(10,5))\nsns.countplot(data=df_train, x=\"target_name\", order=df_train[\"target_name\"].value_counts().index)\nplt.xticks(rotation=20)\nplt.title(\"Training rows per target_name\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"g = sns.FacetGrid(df_train, col=\"target_name\", col_wrap=3, sharex=False, sharey=False, height=3.2)\ng.map_dataframe(sns.histplot, x=\"target\", bins=40)\ng.set_titles(\"{col_name}\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"wide = df_train.pivot_table(index=\"sample_id\", columns=\"target_name\", values=\"target\", aggfunc=\"mean\")\nwide.head()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(6,5))\nsns.heatmap(wide.corr(), annot=True, fmt=\".2f\", cmap=\"coolwarm\", vmin=-1, vmax=1)\nplt.title(\"Correlation between biomass targets\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_cols = [\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\ndf_train[num_cols].describe()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(1,2, figsize=(12,4))\nsns.histplot(df_train[\"Pre_GSHH_NDVI\"], bins=40, ax=axes[0])\naxes[0].set_title(\"NDVI distribution\")\nsns.histplot(df_train[\"Height_Ave_cm\"], bins=40, ax=axes[1])\naxes[1].set_title(\"Height distribution\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tmp = df_train[df_train[\"target_name\"]==\"Dry_Total_g\"].copy()\n\nplt.figure(figsize=(6,4))\nsns.scatterplot(data=tmp, x=\"Pre_GSHH_NDVI\", y=\"target\", alpha=0.25)\nplt.title(\"Dry_Total_g vs NDVI\")\nplt.show()\n\nplt.figure(figsize=(6,4))\nsns.scatterplot(data=tmp, x=\"Height_Ave_cm\", y=\"target\", alpha=0.25)\nplt.title(\"Dry_Total_g vs Height\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sns.scatterplot(data=df_train, x='Height_Ave_cm', y='target', hue='target_name')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sns.scatterplot(data=df_train, x='Pre_GSHH_NDVI', y='target', hue='target_name')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8,4))\nsns.countplot(data=df_train, y=\"State\", order=df_train[\"State\"].value_counts().index)\nplt.title(\"Samples per State\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"top_species = df_train[\"Species\"].value_counts().head(15)\nplt.figure(figsize=(8,5))\nsns.barplot(x=top_species.values, y=top_species.index)\nplt.title(\"Top 15 Species mixes\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train[\"Sampling_Date\"] = pd.to_datetime(df_train[\"Sampling_Date\"])\ntmp = df_train.groupby(df_train[\"Sampling_Date\"].dt.to_period(\"M\")).size()\ntmp.index = tmp.index.astype(str)\n\nplt.figure(figsize=(10,4))\ntmp.plot(kind=\"bar\")\nplt.title(\"Samples by month\")\nplt.xticks(rotation=45)\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_rows = df_train.sample(1)\n\nfull_path = BASE_PATH + sample_rows['image_path'].iloc[0]\n\nfull_path\n\nimg = Image.open(full_path)\nplt.imshow(img)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_rows = df_train.sample(5)\n\nplt.figure(figsize=(20, 5))\n\nfor i, (idx, row) in enumerate(sample_rows.iterrows()):\n    full_path = os.path.join(BASE_PATH, row[\"image_path\"])\n\n    img = Image.open(full_path)\n\n    plt.subplot(1, 5, i + 1)\n    plt.imshow(img)\n    plt.title(\n        \"\\n\".join([\n            f\"sample_id: {row['sample_id']}\",\n            f\"image_path: {row['image_path']}\",\n            f\"Sampling_Date: {row['Sampling_Date']}  State: {row['State']}\",\n            f\"Species: {row['Species']}\",\n            f\"Pre_GSHH_NDVI: {row['Pre_GSHH_NDVI']}\",\n            f\"Height_Ave_cm: {row['Height_Ave_cm']} cm\",\n            f\"target_name: {row['target_name']}\",\n            f\"target: {row['target']:.1f} g\",\n        ]),\n        loc=\"left\",\n        fontsize=8\n    )\n    plt.axis(\"off\")\n\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(tf.config.list_physical_devices('GPU'))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_by_name = df_train.groupby('target_name')['target'].mean().reset_index()\ntarget_by_name","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_submission = df_test.merge(\n    target_by_name,\n    on='target_name',\n    how='left'\n) [[\"sample_id\",\"target\"]]\n\ndf_submission","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_submission.to_csv('submission.csv', index=False)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.preprocessing import StandardScaler\nimport pickle\n\nscalers = {}\ndf_train_scaled = df_train.copy()\n\nfor target_name in df_train[\"target_name\"].unique():\n    mask = df_train[\"target_name\"] == target_name\n    scaler = StandardScaler()\n    \n    df_train_scaled.loc[mask, \"target\"] = scaler.fit_transform(\n        df_train.loc[mask, \"target\"].values.reshape(-1, 1)\n    ).flatten()\n    \n    scalers[target_name] = scaler\n    print(f\"{target_name}: mean={scaler.mean_[0]:.2f}, std={scaler.scale_[0]:.2f}\")\n\nwith open(\"target_scalers.pkl\", \"wb\") as f:\n    pickle.dump(scalers, f)\n\nprint(\"\\nTarget normalization complete!\")\nprint(f\"Original target range: [{df_train['target'].min():.2f}, {df_train['target'].max():.2f}]\")\nprint(f\"Scaled target range: [{df_train_scaled['target'].min():.2f}, {df_train_scaled['target'].max():.2f}]\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"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\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\ndf_train_shuffled = df_train_scaled.sample(frac=1, random_state=42).reset_index(drop=True)\nsplit_idx = int(0.8 * len(df_train_shuffled))\ntrain_df = df_train_shuffled[:split_idx]\nval_df = df_train_shuffled[split_idx:]\n\ntrain_ds = make_dataset(train_df, shuffle=True, augment_data=True, batch_size=32)\nval_ds = make_dataset(val_df, shuffle=False, augment_data=False, batch_size=32)\n\nprint(\"Datasets ready with augmentation!\")\nprint(f\"Train samples: {len(train_df)}, batches: {len(train_ds)}\")\nprint(f\"Val samples: {len(val_df)}, batches: {len(val_ds)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"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    ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    return ds\n\ntrain_ds_meta = make_dataset_with_metadata(train_df, shuffle=True, augment_data=True, batch_size=32)\nval_ds_meta = make_dataset_with_metadata(val_df, shuffle=False, augment_data=False, batch_size=32)\n\nprint(\"Multi-input datasets ready!\")\nprint(f\"Train: {len(train_df)} samples, Val: {len(val_df)} samples\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultiInputRegrCNN(tf.keras.Model):\n    def __init__(self):\n        super().__init__()\n\n        self.conv1 = tf.keras.layers.Conv2D(32, 3, padding=\"same\", activation=\"relu\")\n        self.bn1 = tf.keras.layers.BatchNormalization()\n        self.pool1 = tf.keras.layers.MaxPool2D()\n        \n        self.conv2 = tf.keras.layers.Conv2D(64, 3, padding=\"same\", activation=\"relu\")\n        self.bn2 = tf.keras.layers.BatchNormalization()\n        self.pool2 = tf.keras.layers.MaxPool2D()\n        \n        self.conv3 = tf.keras.layers.Conv2D(128, 3, padding=\"same\", activation=\"relu\")\n        self.bn3 = tf.keras.layers.BatchNormalization()\n        self.pool3 = tf.keras.layers.MaxPool2D()\n        \n        self.conv4 = tf.keras.layers.Conv2D(256, 3, padding=\"same\", activation=\"relu\")\n        self.bn4 = tf.keras.layers.BatchNormalization()\n        \n        self.gap = tf.keras.layers.GlobalAveragePooling2D()\n        \n        self.meta_dense1 = tf.keras.layers.Dense(16, activation=\"relu\")\n        self.meta_bn1 = tf.keras.layers.BatchNormalization()\n        self.meta_drop1 = tf.keras.layers.Dropout(0.2)\n        \n        self.fusion_dense1 = tf.keras.layers.Dense(128, activation=\"relu\")\n        self.fusion_bn = tf.keras.layers.BatchNormalization()\n        self.fusion_drop1 = tf.keras.layers.Dropout(0.4)\n        \n        self.fusion_dense2 = tf.keras.layers.Dense(64, activation=\"relu\")\n        self.fusion_drop2 = tf.keras.layers.Dropout(0.3)\n        \n        self.out = tf.keras.layers.Dense(1)\n\n    def call(self, inputs, training=False):\n        image, metadata = inputs\n        \n        x = self.conv1(image)\n        x = self.bn1(x, training=training)\n        x = self.pool1(x)\n        \n        x = self.conv2(x)\n        x = self.bn2(x, training=training)\n        x = self.pool2(x)\n        \n        x = self.conv3(x)\n        x = self.bn3(x, training=training)\n        x = self.pool3(x)\n        \n        x = self.conv4(x)\n        x = self.bn4(x, training=training)\n        x = self.gap(x)\n        \n        m = self.meta_dense1(metadata)\n        m = self.meta_bn1(m, training=training)\n        m = self.meta_drop1(m, training=training)\n        \n        combined = tf.keras.layers.concatenate([x, m])\n        \n        combined = self.fusion_dense1(combined)\n        combined = self.fusion_bn(combined, training=training)\n        combined = self.fusion_drop1(combined, training=training)\n        \n        combined = self.fusion_dense2(combined)\n        combined = self.fusion_drop2(combined, training=training)\n        \n        return self.out(combined)\n\nmodel_multi = MultiInputRegrCNN()\nprint(\"Multi-input model created!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"initial_learning_rate = 0.0001\nlr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(\n    initial_learning_rate,\n    decay_steps=len(train_ds_meta) * 10,\n    decay_rate=0.95,\n    staircase=True\n)\n\noptimizer = tf.keras.optimizers.Adam(\n    learning_rate=lr_schedule,\n    clipnorm=1.0\n)\n\nmodel_multi.compile(\n    optimizer=optimizer,\n    loss=\"mse\",\n    metrics=[\"mae\"]\n)\n\ncallbacks = [\n    tf.keras.callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=15,\n        restore_best_weights=True,\n        verbose=1,\n        min_delta=0.01\n    )\n]\n\nprint(\"Training multi-input model with image + metadata...\")\nhistory_multi = model_multi.fit(\n    train_ds_meta,\n    epochs=50,\n    steps_per_epoch=len(train_ds_meta),\n    validation_data=val_ds_meta,\n    validation_steps=len(val_ds_meta),\n    callbacks=callbacks,\n    verbose=1\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RegrCNN(tf.keras.Model):\n    def __init__(self):\n        super().__init__()\n        self.conv1 = tf.keras.layers.Conv2D(32, 3, padding=\"same\", activation=\"relu\")\n        self.bn1 = tf.keras.layers.BatchNormalization()\n        self.pool1 = tf.keras.layers.MaxPool2D()\n        \n        self.conv2 = tf.keras.layers.Conv2D(64, 3, padding=\"same\", activation=\"relu\")\n        self.bn2 = tf.keras.layers.BatchNormalization()\n        self.pool2 = tf.keras.layers.MaxPool2D()\n        \n        self.conv3 = tf.keras.layers.Conv2D(128, 3, padding=\"same\", activation=\"relu\")\n        self.bn3 = tf.keras.layers.BatchNormalization()\n        self.pool3 = tf.keras.layers.MaxPool2D()\n        \n        self.conv4 = tf.keras.layers.Conv2D(256, 3, padding=\"same\", activation=\"relu\")\n        self.bn4 = tf.keras.layers.BatchNormalization()\n        \n        self.gap = tf.keras.layers.GlobalAveragePooling2D()\n        self.dense1 = tf.keras.layers.Dense(128, activation=\"relu\")\n        self.bn5 = tf.keras.layers.BatchNormalization()\n        self.drop1 = tf.keras.layers.Dropout(0.4)\n        self.dense2 = tf.keras.layers.Dense(64, activation=\"relu\")\n        self.drop2 = tf.keras.layers.Dropout(0.3)\n        self.out = tf.keras.layers.Dense(1)\n\n    def call(self, x, training=False):\n        x = self.conv1(x)\n        x = self.bn1(x, training=training)\n        x = self.pool1(x)\n        \n        x = self.conv2(x)\n        x = self.bn2(x, training=training)\n        x = self.pool2(x)\n        \n        x = self.conv3(x)\n        x = self.bn3(x, training=training)\n        x = self.pool3(x)\n        \n        x = self.conv4(x)\n        x = self.bn4(x, training=training)\n        \n        x = self.gap(x)\n        x = self.dense1(x)\n        x = self.bn5(x, training=training)\n        x = self.drop1(x, training=training)\n        x = self.dense2(x)\n        x = self.drop2(x, training=training)\n        return self.out(x)\n\nmodel = RegrCNN()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_training_history(history):\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\n    \n    ax1.plot(history.history['loss'], label='Training Loss', linewidth=2, color='blue')\n    if 'val_loss' in history.history:\n        ax1.plot(history.history['val_loss'], label='Validation Loss', linewidth=2, color='orange')\n    ax1.set_title('Model Loss (MSE)')\n    ax1.set_xlabel('Epoch')\n    ax1.set_ylabel('Loss')\n    ax1.legend()\n    ax1.grid(True, alpha=0.3)\n    \n    ax2.plot(history.history['mae'], label='Training MAE', linewidth=2, color='blue')\n    if 'val_mae' in history.history:\n        ax2.plot(history.history['val_mae'], label='Validation MAE', linewidth=2, color='orange')\n    ax2.set_title('Model MAE (grams)')\n    ax2.set_xlabel('Epoch')\n    ax2.set_ylabel('MAE')\n    ax2.legend()\n    ax2.grid(True, alpha=0.3)\n    \n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"initial_learning_rate = 0.0001\nlr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(\n    initial_learning_rate,\n    decay_steps=len(train_ds) * 10,\n    decay_rate=0.95,\n    staircase=True\n)\n\noptimizer = tf.keras.optimizers.Adam(\n    learning_rate=lr_schedule,\n    clipnorm=1.0\n)\n\nmodel.compile(\n    optimizer=optimizer,\n    loss=\"mse\",\n    metrics=[\"mae\"]\n)\n\ncallbacks = [\n    tf.keras.callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=15,\n        restore_best_weights=True,\n        verbose=1,\n        min_delta=0.01\n    )\n]\n\nhistory = model.fit(\n    train_ds,\n    epochs=50,\n    steps_per_epoch=len(train_ds),\n    validation_data=val_ds,\n    validation_steps=len(val_ds),\n    callbacks=callbacks,\n    verbose=1\n)\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_training_history(history)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"History keys:\", list(history.history.keys()))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.save(\"/kaggle/working/regr_cnn.keras\")\nmodel.save(\"/kaggle/working/regr_cnn.h5\")\n\nprint(\"Saved to /kaggle/working/\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_paths = df_test[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\n\ndef load_test_image(path):\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\n\ntest_ds = tf.data.Dataset.from_tensor_slices(test_paths)\ntest_ds = test_ds.map(load_test_image, num_parallel_calls=tf.data.AUTOTUNE)\ntest_ds = test_ds.batch(32).prefetch(tf.data.AUTOTUNE)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = model.predict(test_ds)\npreds = preds.squeeze()\n\nprint(f\"Raw predictions (scaled): min={preds.min():.2f}, max={preds.max():.2f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"target_scalers.pkl\", \"rb\") as f:\n    scalers = pickle.load(f)\n\npreds_original_scale = np.zeros_like(preds)\nfor i, (idx, row) in enumerate(df_test.iterrows()):\n    target_name = row[\"target_name\"]\n    scaler = scalers[target_name]\n    preds_original_scale[i] = scaler.inverse_transform([[preds[i]]])[0, 0]\n\nprint(f\"Predictions (original scale): min={preds_original_scale.min():.2f}, max={preds_original_scale.max():.2f}\")\n\nsubmission = pd.DataFrame({\n    \"sample_id\": df_test[\"sample_id\"],\n    \"target\": preds_original_scale.astype(\"float32\"),\n})\n\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(f\"\\nSaved submission.csv with {len(submission)} rows\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_paths = df_test[\"image_path\"].apply(lambda p: os.path.join(BASE_PATH, p)).values\n\nndvi_test = df_test[\"Pre_GSHH_NDVI\"].values.astype(\"float32\")\nheight_test = df_test[\"Height_Ave_cm\"].values.astype(\"float32\")\n\ntrain_ndvi = train_df[\"Pre_GSHH_NDVI\"].values\ntrain_height = train_df[\"Height_Ave_cm\"].values\n\nndvi_test = (ndvi_test - train_ndvi.min()) / (train_ndvi.max() - train_ndvi.min() + 1e-7)\nheight_test = (height_test - train_height.min()) / (train_height.max() - train_height.min() + 1e-7)\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 (x, 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)\ntest_ds_meta = test_ds_meta.batch(32).prefetch(tf.data.AUTOTUNE)\n\nprint(\"Test dataset with metadata ready!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds_multi = model_multi.predict(test_ds_meta)\npreds_multi = preds_multi.squeeze()\n\nwith open(\"target_scalers.pkl\", \"rb\") as f:\n    scalers = pickle.load(f)\n\npreds_multi_original = np.zeros_like(preds_multi)\nfor i, (idx, row) in enumerate(df_test.iterrows()):\n    target_name = row[\"target_name\"]\n    scaler = scalers[target_name]\n    preds_multi_original[i] = scaler.inverse_transform([[preds_multi[i]]])[0, 0]\n\nprint(f\"Multi-input predictions: min={preds_multi_original.min():.2f}, max={preds_multi_original.max():.2f}\")\n\nsubmission_multi = pd.DataFrame({\n    \"sample_id\": df_test[\"sample_id\"],\n    \"target\": preds_multi_original.astype(\"float32\"),\n})\n\nsubmission_multi.to_csv(\"submission_multi_input.csv\", index=False)\nprint(f\"\\nSaved submission_multi_input.csv with {len(submission_multi)} rows\")\nprint(\"\\nThis should perform SIGNIFICANTLY better than image-only model!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}