{"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":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, random\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom PIL import Image\nfrom IPython.display import display\n\n# sklearn (tabular)\nfrom sklearn.model_selection import KFold\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom sklearn.ensemble import HistGradientBoostingRegressor\nfrom sklearn.linear_model import Ridge\n\n# torch (cnn)\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\n\npd.set_option(\"display.max_columns\", 100)\npd.set_option(\"display.width\", 200)\n\n# Ruta del dataset de la competición (ajusta si difiere)\nINPUT_DIR = Path(\"/kaggle/input/csiro-biomass\")\nsubdirs = [p for p in INPUT_DIR.iterdir() if p.is_dir()]\nCOMP_DIR = subdirs[0] if len(subdirs) == 1 else INPUT_DIR\nprint(\"Usando carpeta de datos:\", COMP_DIR)\n\ndef set_seed(seed=42):\n    random.seed(seed); np.random.seed(seed)\n    torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)\nset_seed(42)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\ndevice\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_csv(COMP_DIR / \"train.csv\")\ntest  = pd.read_csv(COMP_DIR / \"test.csv\")\n\nprint(\"train shape:\", train.shape)\nprint(\"test shape:\", test.shape)\ndisplay(train.head()); display(test.head())\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Orden de targets\ntarget_names = sorted(train[\"target_name\"].unique())\nprint(\"Targets:\", target_names, \"| n_targets:\", len(target_names))\n\n# Pivot ancho (image_path x target_name)\ny_wide = (train.pivot(index=\"image_path\", columns=\"target_name\", values=\"target\")\n               .loc[:, target_names])\nprint(\"y_wide shape:\", y_wide.shape)\n\n# Pesos EXACTOS de la comp\nTARGET_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}\nW_VEC = np.array([TARGET_WEIGHTS[t] for t in target_names], dtype=np.float32)\n\ndef weighted_r2_from_long(df_long_true_pred,\n                          target_col=\"target_name\", y_col=\"y_true\", yhat_col=\"y_pred\"):\n    \"\"\"R² ponderado global igual al de Kaggle.\"\"\"\n    w = df_long_true_pred[target_col].map(TARGET_WEIGHTS).values.astype(np.float64)\n    y  = df_long_true_pred[y_col].values.astype(np.float64)\n    yh = df_long_true_pred[yhat_col].values.astype(np.float64)\n    yw = np.sum(w * y) / np.sum(w)\n    ss_res = np.sum(w * (y - yh)**2)\n    ss_tot = np.sum(w * (y - yw)**2)\n    return 1.0 - ss_res / (ss_tot + 1e-12)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_image_features(rel_path: str) -> dict:\n    img_path = COMP_DIR / rel_path\n    feats = {\n        \"mean_R\":0.0,\"mean_G\":0.0,\"mean_B\":0.0,\n        \"std_R\":0.0,\"std_G\":0.0,\"std_B\":0.0,\n        \"excess_green\":0.0,\"mean_gray\":0.0,\n        \"mean_g_fraction\":0.0,\"prop_green_pixels\":0.0,\"p90_excess_green\":0.0,\n    }\n    try:\n        with Image.open(img_path) as img:\n            img = img.convert(\"RGB\")\n            arr = np.array(img).astype(np.float32)\n        R, G, B = arr[:,:,0], arr[:,:,1], arr[:,:,2]\n        feats[\"mean_R\"], feats[\"mean_G\"], feats[\"mean_B\"] = R.mean(), G.mean(), B.mean()\n        feats[\"std_R\"],  feats[\"std_G\"],  feats[\"std_B\"]  = R.std(),  G.std(),  B.std()\n        eg = 2*G - R - B\n        feats[\"excess_green\"] = eg.mean()\n        gray = 0.299*R + 0.587*G + 0.114*B\n        feats[\"mean_gray\"] = gray.mean()\n        denom = R+G+B+1e-6\n        feats[\"mean_g_fraction\"] = (G/denom).mean()\n        feats[\"prop_green_pixels\"] = ((G>R)&(G>B)).mean()\n        feats[\"p90_excess_green\"] = float(np.percentile(eg, 90))\n    except Exception:\n        pass\n    return feats\n\ndef safe_fill(df: pd.DataFrame) -> pd.DataFrame:\n    df = df.replace([np.inf,-np.inf], np.nan)\n    df = df.fillna(df.mean()).fillna(0)\n    return df\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_tabular(train_df: pd.DataFrame,\n                    y_wide_df: pd.DataFrame,\n                    test_df: pd.DataFrame,\n                    target_names) -> pd.DataFrame:\n    # Features TRAIN\n    train_images = y_wide_df.index.tolist()\n    feats_train = []\n    for rel in train_images:\n        f = extract_image_features(rel); f[\"image_path\"] = rel; feats_train.append(f)\n    X_train_df = pd.DataFrame(feats_train).set_index(\"image_path\")\n    X_train_df = safe_fill(X_train_df)\n\n    X = X_train_df.values\n    Y = y_wide_df[target_names].values\n    Y_log = np.log1p(Y)\n\n    # OOF preds para calibración y R²w CV\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    oof_pred_log = np.zeros_like(Y_log)\n    for fold, (tr, va) in enumerate(kf.split(X)):\n        X_tr, X_va = X[tr], X[va]\n        Y_tr, Y_va = Y_log[tr], Y_log[va]\n\n        preds_log_va = np.zeros_like(Y_va)\n        for j, t in enumerate(target_names):\n            w = np.full(len(X_tr), TARGET_WEIGHTS[t], dtype=np.float32)\n            hgb = HistGradientBoostingRegressor(\n                max_depth=3, learning_rate=0.1, max_iter=300, random_state=42\n            )\n            hgb.fit(X_tr, Y_tr[:, j], sample_weight=w)\n            preds_log_va[:, j] = hgb.predict(X_va)\n        oof_pred_log[va] = preds_log_va\n\n        df_true = (pd.DataFrame(np.expm1(Y_va), columns=target_names)\n                   .reset_index().melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"y_true\"))\n        df_pred = (pd.DataFrame(np.expm1(preds_log_va), columns=target_names)\n                   .reset_index().melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"y_pred\"))\n        df = df_true.merge(df_pred, on=[\"index\",\"target_name\"]).drop(columns=\"index\")\n        print(f\"[TAB] Fold {fold} R2w:\", f\"{weighted_r2_from_long(df):.4f}\")\n\n    df_true = (pd.DataFrame(np.expm1(Y_log), columns=target_names)\n               .reset_index().melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"y_true\"))\n    df_pred = (pd.DataFrame(np.expm1(oof_pred_log), columns=target_names)\n               .reset_index().melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"y_pred\"))\n    df_cv = df_true.merge(df_pred, on=[\"index\",\"target_name\"]).drop(columns=\"index\")\n    print(\"[TAB] CV R2w global:\", f\"{weighted_r2_from_long(df_cv):.4f}\")\n\n    # Calibración lineal en escala original, por target\n    calib = {}\n    for j, t in enumerate(target_names):\n        ridge = Ridge(alpha=1e-6, fit_intercept=True)\n        ridge.fit(np.expm1(oof_pred_log[:, [j]]), np.expm1(Y_log[:, j:j+1]))\n        calib[t] = (float(ridge.coef_[0][0]), float(ridge.intercept_[0]))\n\n    # Reentrenar en TODO el train\n    models = []\n    for j, t in enumerate(target_names):\n        w = np.full(len(X), TARGET_WEIGHTS[t], dtype=np.float32)\n        hgb = HistGradientBoostingRegressor(\n            max_depth=3, learning_rate=0.1, max_iter=300, random_state=42\n        )\n        hgb.fit(X, Y_log[:, j], sample_weight=w)\n        models.append(hgb)\n\n    # Features TEST\n    test_images = test_df[\"image_path\"].unique().tolist()\n    feats_test = []\n    for rel in test_images:\n        f = extract_image_features(rel); f[\"image_path\"] = rel; feats_test.append(f)\n    X_test_df = pd.DataFrame(feats_test).set_index(\"image_path\")\n    X_test_df = safe_fill(X_test_df)\n    X_test = X_test_df.values\n\n    preds_log = np.column_stack([m.predict(X_test) for m in models])\n    preds = np.expm1(preds_log); preds = np.clip(preds, 0, None)\n\n    # Aplicar calibración en original\n    for j, t in enumerate(target_names):\n        a, b = calib[t]\n        preds[:, j] = a * preds[:, j] + b\n\n    return pd.DataFrame(preds, index=test_images, columns=target_names)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CSIROTrainDataset(Dataset):\n    def __init__(self, image_paths, targets_log, transform=None):\n        self.image_paths = image_paths; self.targets_log = targets_log; self.transform = transform\n    def __len__(self): return len(self.image_paths)\n    def _safe(self, p):\n        p = COMP_DIR / p\n        try:\n            with Image.open(p) as im: im = im.convert(\"RGB\")\n        except Exception: im = Image.new(\"RGB\",(224,224),(0,0,0))\n        return im\n    def __getitem__(self, i):\n        im = self._safe(self.image_paths[i])\n        if self.transform: im = self.transform(im)\n        y = torch.tensor(self.targets_log[i], dtype=torch.float32)\n        return im, y\n\nclass CSIROTestDataset(Dataset):\n    def __init__(self, image_paths, transform=None):\n        self.image_paths = image_paths; self.transform = transform\n    def __len__(self): return len(self.image_paths)\n    def _safe(self, p):\n        p = COMP_DIR / p\n        try:\n            with Image.open(p) as im: im = im.convert(\"RGB\")\n        except Exception: im = Image.new(\"RGB\",(224,224),(0,0,0))\n        return im\n    def __getitem__(self, i):\n        im = self._safe(self.image_paths[i])\n        if self.transform: im = self.transform(im)\n        return im\n\nclass SmallCNN(nn.Module):\n    def __init__(self, n_outputs):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3,16,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),   # 224->112\n            nn.Conv2d(16,32,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),  # 112->56\n            nn.Conv2d(32,64,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),  # 56->28\n            nn.Conv2d(64,128,3,padding=1), nn.ReLU(), nn.AdaptiveAvgPool2d((1,1))\n        )\n        self.head = nn.Sequential(nn.Flatten(), nn.Linear(128,128), nn.ReLU(), nn.Linear(128,n_outputs))\n    def forward(self,x): return self.head(self.features(x))\n\ndef get_transforms():\n    train_tf = T.Compose([T.Resize((224,224)), T.RandomHorizontalFlip(0.5), T.ToTensor()])\n    test_tf  = T.Compose([T.Resize((224,224)), T.ToTensor()])\n    return train_tf, test_tf\n\ndef train_cnn_predict(train_df, y_wide_df, test_df, target_names,\n                      epochs=8, batch_size=16, lr=1e-3, wd=1e-5):\n    img_paths = y_wide_df.index.tolist()\n    Y = y_wide_df.values; Y_log = np.log1p(Y)\n\n    train_tf, test_tf = get_transforms()\n    ds = CSIROTrainDataset(img_paths, Y_log, transform=train_tf)\n    dl = DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True)\n\n    model = SmallCNN(n_outputs=len(target_names)).to(device)\n    opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)\n\n    # loss ponderada por target (igual a la comp)\n    w = torch.tensor(W_VEC, device=device).view(1,-1)\n    model.train()\n    for ep in range(epochs):\n        running=0.0\n        for xb, yb in dl:\n            xb, yb = xb.to(device), yb.to(device)\n            opt.zero_grad()\n            pred = model(xb)\n            loss = ((pred - yb)**2 * w).mean()\n            loss.backward(); opt.step()\n            running += loss.item() * xb.size(0)\n        print(f\"[CNN] Epoch {ep+1}/{epochs} - loss(log1p, weighted): {running/len(ds):.4f}\")\n\n    # Predicción test\n    test_images = test_df[\"image_path\"].unique().tolist()\n    ds_test = CSIROTestDataset(test_images, transform=test_tf)\n    dl_test = DataLoader(ds_test, batch_size=batch_size, shuffle=False, num_workers=2, pin_memory=True)\n    model.eval(); preds_log=[]\n    with torch.no_grad():\n        for xb in dl_test:\n            xb = xb.to(device)\n            preds_log.append(model(xb).cpu().numpy())\n    preds_log = np.concatenate(preds_log, axis=0)\n    preds = np.expm1(preds_log); preds = np.clip(preds, 0, None)\n\n    return pd.DataFrame(preds, index=test_images, columns=target_names)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pesos del ensamble por target: alpha = peso del TABULAR; (1-alpha) = peso de la CNN\nALPHA = {\n    \"Dry_Green_g\": 0.50,\n    \"Dry_Dead_g\":  0.50,\n    \"Dry_Clover_g\":0.50,\n    \"GDM_g\":       0.60,\n    \"Dry_Total_g\": 0.70,\n}\n\ndef make_submission_from_predictions(test_df: pd.DataFrame,\n                                     preds_df: pd.DataFrame,\n                                     target_names) -> pd.DataFrame:\n    preds_long = (preds_df.reset_index()\n                  .melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"pred\")\n                  .rename(columns={\"index\":\"image_path\"}))\n    test_pred = test_df.merge(preds_long, on=[\"image_path\",\"target_name\"], how=\"left\")\n    sub = test_pred[[\"sample_id\",\"pred\"]].rename(columns={\"pred\":\"target\"})\n    assert sub.shape[0] == test_df.shape[0], \"Tamaño de submission != test\"\n    return sub\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"try:\n    print(\">> Entrenando modelo TABULAR …\")\n    preds_tab = predict_tabular(train, y_wide, test, target_names)\n\n    print(\"\\n>> Entrenando modelo CNN …\")\n    preds_cnn = train_cnn_predict(train, y_wide, test, target_names,\n                                  epochs=8, batch_size=16, lr=1e-3, wd=1e-5)\n\n    # Ensamble por target\n    preds_ens = preds_tab.copy()\n    for t in target_names:\n        a = ALPHA[t]\n        preds_ens[t] = a*preds_tab[t] + (1-a)*preds_cnn[t]\n\n    # Consistencia del total: mezcla entre total directo y suma de componentes\n    total_from_parts = preds_ens[\"Dry_Green_g\"] + preds_ens[\"Dry_Dead_g\"] + preds_ens[\"Dry_Clover_g\"]\n    preds_ens[\"Dry_Total_g\"] = 0.5*preds_ens[\"Dry_Total_g\"] + 0.5*total_from_parts\n\n    # Construir submission\n    submission = make_submission_from_predictions(test, preds_ens, target_names)\n    print(\"Submission lista. Filas:\", len(submission))\n    display(submission.head())\n\nexcept Exception as e:\n    print(\"ERROR en pipeline, aplicando fallback basado en test.\", repr(e))\n    submission = test[[\"sample_id\"]].copy()\n    submission[\"target\"] = float(train[\"target\"].mean())\n\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"submission.csv guardado.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}