{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Part 1: Environment & Imports\nimport os\nimport gc\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nfrom torch import nn\nfrom torchvision import models, transforms\nfrom torchvision.models import (\n    ResNet50_Weights,\n    EfficientNet_B0_Weights,\n    MobileNet_V3_Small_Weights,\n)\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom sklearn.linear_model import Ridge\n\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nif torch.cuda.is_available():\n    device = torch.device(\"cuda\")\nelif getattr(torch.backends, \"mps\", None) and torch.backends.mps.is_available():\n    device = torch.device(\"mps\")\nelse:\n    device = torch.device(\"cpu\")\n\nprint(f\"Running on device: {device}\")\n\ndef detect_data_root() -> Path:\n    direct_candidates = [\n        Path(\"/kaggle/input/csiro-biomass\"),\n        Path(\"/kaggle/input/csiro-biomass-prediction\"),\n        Path(\"/kaggle/input/csiro-biomass-dataset\"),\n        Path(\"/kaggle/input/csiro-biomass-challenge\"),\n        Path(\"/kaggle/input/csirocometition-clip/data/raw\"),\n        Path(\"/kaggle/input/csirocometition-clip\"),\n        Path(\"data/raw\"),\n        Path.cwd(),\n    ]\n    for root in direct_candidates:\n        if (root / \"train.csv\").exists():\n            return root\n        if (root / \"data\" / \"raw\" / \"train.csv\").exists():\n            return root / \"data\" / \"raw\"\n    kaggle_input = Path(\"/kaggle/input\")\n    if kaggle_input.exists():\n        for child in kaggle_input.iterdir():\n            if not child.is_dir():\n                continue\n            if (child / \"train.csv\").exists():\n                return child\n            if (child / \"data\" / \"raw\" / \"train.csv\").exists():\n                return child / \"data\" / \"raw\"\n    raise FileNotFoundError(\"Could not locate train.csv. Please upload the competition data as a Kaggle dataset and retry.\")\n\nDATA_ROOT = detect_data_root()\nTRAIN_CSV = DATA_ROOT / \"train.csv\"\nTEST_CSV = DATA_ROOT / \"test.csv\"\nTRAIN_IMG_DIR = DATA_ROOT / \"train\"\nTEST_IMG_DIR = DATA_ROOT / \"test\"\n\nprint(f\"Data root: {DATA_ROOT}\")\nprint(f\"Train CSV: {TRAIN_CSV}\")\nprint(f\"Test CSV:  {TEST_CSV if TEST_CSV.exists() else 'missing'}\")\n\nOUTPUT_DIR = Path(\"/kaggle/working\") if Path(\"/kaggle\").exists() else Path.cwd()\nSUBMISSION_CSV = OUTPUT_DIR / \"submission.csv\"\nprint(f\"Submission path: {SUBMISSION_CSV}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:25:29.886732Z","iopub.execute_input":"2025-11-17T10:25:29.887318Z","iopub.status.idle":"2025-11-17T10:25:39.660221Z","shell.execute_reply.started":"2025-11-17T10:25:29.887278Z","shell.execute_reply":"2025-11-17T10:25:39.659169Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Part 2: Helper Functions\nTARGET_WEIGHTS = {\n    \"Dry_Total_g\": 0.5,\n    \"GDM_g\": 0.2,\n    \"Dry_Green_g\": 0.1,\n    \"Dry_Dead_g\": 0.1,\n    \"Dry_Clover_g\": 0.1,\n}\nTARGET_ORDER = list(TARGET_WEIGHTS.keys())\n\ndef weighted_r2_log1p(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n    y_all = []\n    yp_all = []\n    w_all = []\n    for idx, name in enumerate(TARGET_ORDER):\n        weight = TARGET_WEIGHTS[name]\n        yt = np.log1p(np.clip(y_true[:, idx], a_min=0, a_max=None))\n        yp = np.log1p(np.clip(y_pred[:, idx], a_min=0, a_max=None))\n        y_all.append(yt)\n        yp_all.append(yp)\n        w_all.append(np.full_like(yt, weight))\n    y_all = np.concatenate(y_all)\n    yp_all = np.concatenate(yp_all)\n    w_all = np.concatenate(w_all)\n    weighted_mean = np.average(y_all, weights=w_all)\n    ss_res = np.sum(w_all * (y_all - yp_all) ** 2)\n    ss_tot = np.sum(w_all * (y_all - weighted_mean) ** 2)\n    if ss_tot < 1e-8:\n        return 0.0\n    return float(1.0 - ss_res / ss_tot)\n\ncnn_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\ndef resolve_image_path(relative: str, primary_dir: Path) -> Path:\n    candidate_paths = [\n        primary_dir / Path(relative).name,\n        primary_dir / relative,\n        DATA_ROOT / relative,\n        Path(relative),\n    ]\n    for candidate in candidate_paths:\n        if candidate.exists():\n            return candidate\n    raise FileNotFoundError(f\"Unable to locate image: {relative}\")\n\ndef prepare_absolute_paths(relative_series: pd.Series, primary_dir: Path) -> list:\n    return [resolve_image_path(rel, primary_dir) for rel in relative_series]\n\ndef build_cnn_model(backbone: str) -> tuple:\n    if backbone == \"resnet50\":\n        model = models.resnet50(weights=ResNet50_Weights.DEFAULT)\n        model.fc = nn.Identity()\n    elif backbone == \"efficientnet_b0\":\n        model = models.efficientnet_b0(weights=EfficientNet_B0_Weights.DEFAULT)\n        model.classifier = nn.Identity()\n    elif backbone == \"mobilenet_v3_small\":\n        model = models.mobilenet_v3_small(weights=MobileNet_V3_Small_Weights.DEFAULT)\n        model.classifier = nn.Identity()\n    else:\n        raise ValueError(f\"Unknown backbone: {backbone}\")\n    model = model.to(device).eval()\n    with torch.no_grad():\n        dummy = torch.zeros((1, 3, 224, 224), device=device)\n        feature_dim = model(dummy).view(1, -1).shape[1]\n    return model, feature_dim\n\ndef extract_cnn_features(model: nn.Module, image_files: list, feature_dim: int, batch_size: int = 16) -> np.ndarray:\n    features = np.zeros((len(image_files), feature_dim), dtype=np.float32)\n    for start in range(0, len(image_files), batch_size):\n        end = min(start + batch_size, len(image_files))\n        batch_paths = image_files[start:end]\n        tensors = []\n        for path in batch_paths:\n            try:\n                with Image.open(path).convert(\"RGB\") as img:\n                    tensors.append(cnn_transform(img))\n            except Exception as exc:\n                print(f\"Warning: failed to load {path} ({exc}); substituting zeros.\")\n                tensors.append(torch.zeros((3, 224, 224)))\n        batch_tensor = torch.stack(tensors).to(device)\n        with torch.no_grad():\n            outputs = model(batch_tensor).view(batch_tensor.size(0), -1)\n        features[start:end] = outputs.cpu().numpy()\n        del batch_tensor\n        gc.collect()\n    return features\n\ndef train_cnn_regressor(model_key: str, X_train: np.ndarray, X_test: np.ndarray, y_train: np.ndarray, state_labels: np.ndarray) -> dict:\n    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\n    oof_preds = np.zeros_like(y_train, dtype=np.float32)\n    test_preds = np.zeros((X_test.shape[0], y_train.shape[1]), dtype=np.float32) if X_test.size else np.zeros((0, y_train.shape[1]), dtype=np.float32)\n    fold_scores = []\n    for fold, (tr_idx, val_idx) in enumerate(skf.split(X_train, state_labels), start=1):\n        scaler = StandardScaler()\n        X_tr = scaler.fit_transform(X_train[tr_idx])\n        X_val = scaler.transform(X_train[val_idx])\n        X_te = scaler.transform(X_test) if X_test.size else X_test\n        base_model = MultiOutputRegressor(Ridge(alpha=5.0))\n        base_model.fit(X_tr, y_train[tr_idx])\n        val_pred = base_model.predict(X_val)\n        oof_preds[val_idx] = val_pred\n        fold_score = weighted_r2_log1p(y_train[val_idx], val_pred)\n        fold_scores.append(fold_score)\n        print(f\"{model_key} Fold {fold} Kaggle R²: {fold_score:.4f}\")\n        if X_te.size:\n            test_preds += base_model.predict(X_te) / skf.n_splits\n    overall_score = weighted_r2_log1p(y_train, oof_preds)\n    print(f\"{model_key} OOF Kaggle R²: {overall_score:.4f}\")\n    return {\n        \"oof\": oof_preds,\n        \"test\": test_preds,\n        \"fold_scores\": fold_scores,\n        \"score\": overall_score,\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:25:39.661846Z","iopub.execute_input":"2025-11-17T10:25:39.662322Z","iopub.status.idle":"2025-11-17T10:25:39.685248Z","shell.execute_reply.started":"2025-11-17T10:25:39.662297Z","shell.execute_reply":"2025-11-17T10:25:39.684083Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Part 3: Load Data & Build Matrices\ntrain_long = pd.read_csv(TRAIN_CSV)\nprint(f\"Training rows (long format): {len(train_long)}\")\n\nmetadata_cols = [\"Sampling_Date\", \"State\", \"Species\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\ntrain_wide = train_long.pivot_table(\n    index=\"image_path\",\n    columns=\"target_name\",\n    values=\"target\",\n    aggfunc=\"first\",\n).reset_index()\ntrain_meta = train_long[[\"image_path\", *metadata_cols]].drop_duplicates(\"image_path\")\ntrain_wide = train_wide.merge(train_meta, on=\"image_path\", how=\"left\")\ntrain_wide = train_wide.sort_values(\"image_path\").reset_index(drop=True)\n\n# Ensure consistent target order\nmissing_targets = [t for t in TARGET_ORDER if t not in train_wide.columns]\nif missing_targets:\n    raise ValueError(f\"Missing targets in training data: {missing_targets}\")\ny_train = train_wide[TARGET_ORDER].values.astype(np.float32)\n\nstate_series = train_wide[\"State\"].fillna(\"Unknown\")\nstate_encoder = LabelEncoder()\nstate_labels = state_encoder.fit_transform(state_series)\n\ntrain_image_files = prepare_absolute_paths(train_wide[\"image_path\"], TRAIN_IMG_DIR)\nprint(f\"Resolved train images: {len(train_image_files)}\")\n\nif TEST_CSV.exists():\n    test_long = pd.read_csv(TEST_CSV)\n    test_unique = test_long[[\"image_path\"]].drop_duplicates().sort_values(\"image_path\")\n    test_image_files = prepare_absolute_paths(test_unique[\"image_path\"], TEST_IMG_DIR)\n    test_template = test_long[[\"sample_id\", \"image_path\", \"target_name\"]]\n    print(f\"Resolved test images: {len(test_image_files)}\")\nelse:\n    test_long = pd.DataFrame(columns=[\"sample_id\", \"image_path\", \"target_name\"])\n    test_unique = test_long[[\"image_path\"]]\n    test_image_files = []\n    test_template = test_long\n    print(\"Warning: test.csv not found; submission will be skipped.\")\n\nbase_model_results = {}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:25:39.686429Z","iopub.execute_input":"2025-11-17T10:25:39.686768Z","iopub.status.idle":"2025-11-17T10:25:40.203528Z","shell.execute_reply.started":"2025-11-17T10:25:39.686739Z","shell.execute_reply":"2025-11-17T10:25:40.202511Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Part 4: ResNet50 Features & Model\nresnet_model, resnet_dim = build_cnn_model(\"resnet50\")\nprint(f\"ResNet50 feature dimension: {resnet_dim}\")\nresnet_train_feats = extract_cnn_features(resnet_model, train_image_files, resnet_dim)\nresnet_test_feats = extract_cnn_features(resnet_model, test_image_files, resnet_dim) if test_image_files else np.zeros((0, resnet_dim), dtype=np.float32)\nresnet_results = train_cnn_regressor(\"ResNet50\", resnet_train_feats, resnet_test_feats, y_train, state_labels)\nbase_model_results[\"resnet50\"] = {\n    \"features_train\": resnet_train_feats,\n    \"features_test\": resnet_test_feats,\n    **resnet_results,\n}\ndel resnet_model\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:28:16.015885Z","iopub.execute_input":"2025-11-17T10:28:16.016266Z","iopub.status.idle":"2025-11-17T10:29:49.598150Z","shell.execute_reply.started":"2025-11-17T10:28:16.016243Z","shell.execute_reply":"2025-11-17T10:29:49.597285Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Part 5: EfficientNet-B0 Features & Model\nefficientnet_model, efficientnet_dim = build_cnn_model(\"efficientnet_b0\")\nprint(f\"EfficientNet-B0 feature dimension: {efficientnet_dim}\")\nefficientnet_train_feats = extract_cnn_features(efficientnet_model, train_image_files, efficientnet_dim)\nefficientnet_test_feats = extract_cnn_features(efficientnet_model, test_image_files, efficientnet_dim) if test_image_files else np.zeros((0, efficientnet_dim), dtype=np.float32)\nefficientnet_results = train_cnn_regressor(\"EfficientNet-B0\", efficientnet_train_feats, efficientnet_test_feats, y_train, state_labels)\nbase_model_results[\"efficientnet_b0\"] = {\n    \"features_train\": efficientnet_train_feats,\n    \"features_test\": efficientnet_test_feats,\n    **efficientnet_results,\n}\ndel efficientnet_model\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:29:49.599707Z","iopub.execute_input":"2025-11-17T10:29:49.600019Z","iopub.status.idle":"2025-11-17T10:30:34.528941Z","shell.execute_reply.started":"2025-11-17T10:29:49.599993Z","shell.execute_reply":"2025-11-17T10:30:34.527770Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Part 6: MobileNetV3-Small Features & Model\nmobilenet_model, mobilenet_dim = build_cnn_model(\"mobilenet_v3_small\")\nprint(f\"MobileNetV3-Small feature dimension: {mobilenet_dim}\")\nmobilenet_train_feats = extract_cnn_features(mobilenet_model, train_image_files, mobilenet_dim)\nmobilenet_test_feats = extract_cnn_features(mobilenet_model, test_image_files, mobilenet_dim) if test_image_files else np.zeros((0, mobilenet_dim), dtype=np.float32)\nmobilenet_results = train_cnn_regressor(\"MobileNetV3-Small\", mobilenet_train_feats, mobilenet_test_feats, y_train, state_labels)\nbase_model_results[\"mobilenet_v3_small\"] = {\n    \"features_train\": mobilenet_train_feats,\n    \"features_test\": mobilenet_test_feats,\n    **mobilenet_results,\n}\ndel mobilenet_model\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:30:34.529997Z","iopub.execute_input":"2025-11-17T10:30:34.530884Z","iopub.status.idle":"2025-11-17T10:31:05.074327Z","shell.execute_reply.started":"2025-11-17T10:30:34.530849Z","shell.execute_reply":"2025-11-17T10:31:05.073418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Part 7: Stacking & Averaging Ensembles\nrequired_models = {\"resnet50\", \"efficientnet_b0\", \"mobilenet_v3_small\"} - set(base_model_results.keys())\nif required_models:\n    raise RuntimeError(f\"Missing base model results for: {sorted(required_models)}\")\n\noof_blocks = [base_model_results[name][\"oof\"] for name in [\"resnet50\", \"efficientnet_b0\", \"mobilenet_v3_small\"]]\ntest_blocks = [base_model_results[name][\"test\"] for name in [\"resnet50\", \"efficientnet_b0\", \"mobilenet_v3_small\"]]\n\nstack_train_features = np.concatenate(oof_blocks, axis=1)\nstack_test_features = np.concatenate(test_blocks, axis=1) if test_blocks[0].size else np.zeros((0, len(oof_blocks) * len(TARGET_ORDER)), dtype=np.float32)\n\nscaler = StandardScaler()\nstack_train_scaled = scaler.fit_transform(stack_train_features)\nstack_test_scaled = scaler.transform(stack_test_features) if stack_test_features.size else stack_test_features\n\nmeta_model = MultiOutputRegressor(Ridge(alpha=1.0))\nmeta_model.fit(stack_train_scaled, y_train)\nstack_train_pred = meta_model.predict(stack_train_scaled)\nstack_score = weighted_r2_log1p(y_train, stack_train_pred)\nprint(f\"Stacking meta-model Kaggle R²: {stack_score:.4f}\")\n\nstack_test_pred = meta_model.predict(stack_test_scaled) if stack_test_scaled.size else np.zeros((0, len(TARGET_ORDER)), dtype=np.float32)\n\navg_train_pred = np.mean(oof_blocks, axis=0)\navg_score = weighted_r2_log1p(y_train, avg_train_pred)\nprint(f\"Simple averaging Kaggle R²: {avg_score:.4f}\")\navg_test_pred = np.mean(test_blocks, axis=0) if test_blocks[0].size else np.zeros((0, len(TARGET_ORDER)), dtype=np.float32)\n\nensemble_results = {\n    \"stack\": stack_test_pred,\n    \"avg\": avg_test_pred,\n    \"stack_score\": stack_score,\n    \"avg_score\": avg_score,\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:31:05.076165Z","iopub.execute_input":"2025-11-17T10:31:05.076467Z","iopub.status.idle":"2025-11-17T10:31:05.098061Z","shell.execute_reply.started":"2025-11-17T10:31:05.076446Z","shell.execute_reply":"2025-11-17T10:31:05.096895Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Part 8: Generate Submission\nif not test_image_files:\n    print(\"Test data not available; submission.csv will not be created.\")\nelse:\n    stack_pred_df = pd.DataFrame(np.clip(ensemble_results[\"stack\"], a_min=0, a_max=None), columns=TARGET_ORDER)\n    stack_pred_df[\"image_path\"] = test_unique[\"image_path\"].values\n    stack_pred_long = stack_pred_df.melt(id_vars=\"image_path\", value_vars=TARGET_ORDER, var_name=\"target_name\", value_name=\"prediction\")\n\n    submission_df = test_template.merge(stack_pred_long, on=[\"image_path\", \"target_name\"], how=\"left\")[[\"sample_id\", \"prediction\"]]\n    submission_df = submission_df.rename(columns={\"prediction\": \"target\"})\n    submission_df.to_csv(SUBMISSION_CSV, index=False)\n    print(f\"Saved submission to {SUBMISSION_CSV}\")\n    display(submission_df.head())\n\n    avg_pred_df = pd.DataFrame(np.clip(ensemble_results[\"avg\"], a_min=0, a_max=None), columns=TARGET_ORDER)\n    avg_pred_df[\"image_path\"] = test_unique[\"image_path\"].values\n    avg_pred_long = avg_pred_df.melt(id_vars=\"image_path\", value_vars=TARGET_ORDER, var_name=\"target_name\", value_name=\"prediction\")\n    averaging_df = test_template.merge(avg_pred_long, on=[\"image_path\", \"target_name\"], how=\"left\")[[\"sample_id\", \"prediction\"]]\n    averaging_path = OUTPUT_DIR / \"submission_average.csv\"\n    averaging_df.rename(columns={\"prediction\": \"target\"}, inplace=True)\n    averaging_df.to_csv(averaging_path, index=False)\n    print(f\"Saved optional averaging submission to {averaging_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T10:31:05.099296Z","iopub.execute_input":"2025-11-17T10:31:05.099662Z","iopub.status.idle":"2025-11-17T10:31:05.154332Z","shell.execute_reply.started":"2025-11-17T10:31:05.099639Z","shell.execute_reply":"2025-11-17T10:31:05.153418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}