{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"a19938b1","cell_type":"markdown","source":"# CSIRO Biomass — HGB + Small CNN + DINOv2 Base (518px) Ensemble\n","metadata":{}},{"id":"fbd65380","cell_type":"code","source":"\nimport os, glob, random\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\n\nfrom PIL import Image\nfrom IPython.display import display\n\n# sklearn\nfrom sklearn.model_selection import KFold\nfrom sklearn.ensemble import HistGradientBoostingRegressor\nfrom sklearn.linear_model import Ridge\nfrom sklearn.preprocessing import StandardScaler\n\n# torch\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\n\n# timm (DINOv2 Base extractor)\nimport timm\n\npd.set_option(\"display.max_columns\", 120)\npd.set_option(\"display.width\", 220)\n\n# Ruta del dataset de la competencia\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,"execution":{"iopub.status.busy":"2025-11-09T05:41:13.107929Z","iopub.execute_input":"2025-11-09T05:41:13.108280Z","iopub.status.idle":"2025-11-09T05:41:13.127099Z","shell.execute_reply.started":"2025-11-09T05:41:13.108255Z","shell.execute_reply":"2025-11-09T05:41:13.126049Z"}},"outputs":[],"execution_count":null},{"id":"bce8f74a","cell_type":"code","source":"\ntrain = 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(3)); display(test.head(3))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T05:41:18.300414Z","iopub.execute_input":"2025-11-09T05:41:18.300741Z","iopub.status.idle":"2025-11-09T05:41:18.361415Z","shell.execute_reply.started":"2025-11-09T05:41:18.300710Z","shell.execute_reply":"2025-11-09T05:41:18.360378Z"}},"outputs":[],"execution_count":null},{"id":"bee39ee8","cell_type":"code","source":"target_names = sorted(train[\"target_name\"].unique())\nprint(\"Targets:\", target_names, \"| n_targets:\", len(target_names))\n\ny_wide = (train.pivot(index=\"image_path\", columns=\"target_name\", values=\"target\")\n               .loc[:, target_names])\nprint(\"y_wide shape:\", y_wide.shape)\n\nTARGET_WEIGHTS = {\"Dry_Green_g\":0.1,\"Dry_Dead_g\":0.1,\"Dry_Clover_g\":0.1,\"GDM_g\":0.2,\"Dry_Total_g\":0.5}\nW_VEC = np.array([TARGET_WEIGHTS[t] for t in target_names], dtype=np.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T05:41:22.487846Z","iopub.execute_input":"2025-11-09T05:41:22.488929Z","iopub.status.idle":"2025-11-09T05:41:22.505723Z","shell.execute_reply.started":"2025-11-09T05:41:22.488898Z","shell.execute_reply":"2025-11-09T05:41:22.504342Z"}},"outputs":[],"execution_count":null},{"id":"62ccfaea","cell_type":"code","source":"def extract_image_features(rel_path: str) -> dict:\n    img_path = COMP_DIR / rel_path\n    feats = {\"mean_R\":0.0,\"mean_G\":0.0,\"mean_B\":0.0,\"std_R\":0.0,\"std_G\":0.0,\"std_B\":0.0,\n             \"excess_green\":0.0,\"mean_gray\":0.0,\"mean_g_fraction\":0.0,\"prop_green_pixels\":0.0,\"p90_excess_green\":0.0,\n             \"mean_H\":0.0,\"mean_S\":0.0,\"mean_V\":0.0,\"std_H\":0.0,\"std_S\":0.0,\"std_V\":0.0,\n             \"mean_L\":0.0,\"mean_A\":0.0,\"mean_Blab\":0.0,\"std_L\":0.0,\"std_A\":0.0,\"std_Blab\":0.0,\n             \"edge_density\":0.0,\"lap_var\":0.0,\"entropy\":0.0}\n    try:\n        with Image.open(img_path) as img:\n            img = img.convert(\"RGB\")\n            arr = np.asarray(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; feats[\"excess_green\"] = eg.mean()\n        gray = 0.299*R + 0.587*G + 0.114*B; feats[\"mean_gray\"] = gray.mean()\n        denom = R+G+B+1e-6; 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        hsv = np.array(Image.fromarray(arr.astype(np.uint8)).convert(\"HSV\")).astype(np.float32)\n        H,S,V = hsv[:,:,0], hsv[:,:,1], hsv[:,:,2]\n        feats[\"mean_H\"],feats[\"mean_S\"],feats[\"mean_V\"] = H.mean(),S.mean(),V.mean()\n        feats[\"std_H\"],feats[\"std_S\"],feats[\"std_V\"]    = H.std(),S.std(),V.std()\n        lab = np.array(Image.fromarray(arr.astype(np.uint8)).convert(\"LAB\")).astype(np.float32)\n        L,A,Bl = lab[:,:,0], lab[:,:,1], lab[:,:,2]\n        feats[\"mean_L\"],feats[\"mean_A\"],feats[\"mean_Blab\"] = L.mean(),A.mean(),Bl.mean()\n        feats[\"std_L\"],feats[\"std_A\"],feats[\"std_Blab\"]    = L.std(),A.std(),Bl.std()\n        try:\n            from scipy.ndimage import sobel, laplace\n            sob = np.hypot(sobel(gray,0), sobel(gray,1))\n            feats[\"edge_density\"] = (sob>sob.mean()).mean()\n            feats[\"lap_var\"] = laplace(gray).var()\n        except Exception:\n            pass\n        hist,_ = np.histogram(gray, bins=64, range=(0,255), density=True)\n        hist = hist + 1e-12\n        feats[\"entropy\"] = float(-(hist*np.log(hist)).sum())\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,"execution":{"iopub.status.busy":"2025-11-09T05:41:26.009918Z","iopub.execute_input":"2025-11-09T05:41:26.010215Z","iopub.status.idle":"2025-11-09T05:41:26.026869Z","shell.execute_reply.started":"2025-11-09T05:41:26.010194Z","shell.execute_reply":"2025-11-09T05:41:26.025766Z"}},"outputs":[],"execution_count":null},{"id":"21ca88c4","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):\n    # TRAIN features\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 para calibración\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    oof_pred_log = np.zeros_like(Y_log)\n    for tr, va in kf.split(X):\n        X_tr, X_va = X[tr], X[va]\n        Y_tr, Y_va = Y_log[tr], Y_log[va]\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(max_depth=4, learning_rate=0.1, max_iter=400, random_state=42)\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    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    # Full models\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(max_depth=4, learning_rate=0.1, max_iter=400, random_state=42)\n        hgb.fit(X, Y_log[:, j], sample_weight=w)\n        models.append(hgb)\n\n    # TEST features\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\n    for j, t in enumerate(target_names):\n        a, b = calib[t]; preds[:, j] = a * preds[:, j] + b\n\n    preds_tab = pd.DataFrame(preds, index=test_images, columns=target_names)\n    return preds_tab\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T05:41:30.676453Z","iopub.execute_input":"2025-11-09T05:41:30.676837Z","iopub.status.idle":"2025-11-09T05:41:30.692559Z","shell.execute_reply.started":"2025-11-09T05:41:30.676779Z","shell.execute_reply":"2025-11-09T05:41:30.691518Z"}},"outputs":[],"execution_count":null},{"id":"84e42d35","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        pth = COMP_DIR / p\n        try:\n            with Image.open(pth) 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        pth = COMP_DIR / p\n        try:\n            with Image.open(pth) 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),\n            nn.Conv2d(16,32,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(32,64,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),\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 tta_predict(model, image_tensor, n=4):\n    x = image_tensor.unsqueeze(0).to(device)\n    outs = []\n    with torch.no_grad():\n        outs.append(model(x).cpu().numpy()[0])\n        outs.append(model(torch.flip(x,dims=[3])).cpu().numpy()[0])\n        outs.append(model(torch.flip(x,dims=[2])).cpu().numpy()[0])\n        outs.append(model(torch.rot90(x,k=1,dims=[2,3])).cpu().numpy()[0])\n    return np.mean(outs[:n], axis=0)\n\ndef train_cnn_predict(y_wide_df, test_df, target_names,\n                      epochs=8, batch_size=16, lr=1e-3, wd=1e-5, use_tta=True):\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    w = torch.tensor(W_VEC, device=device).view(1,-1)\n    iT  = target_names.index(\"Dry_Total_g\")\n    iG  = target_names.index(\"Dry_Green_g\")\n    iD  = target_names.index(\"Dry_Dead_g\")\n    iC  = target_names.index(\"Dry_Clover_g\")\n    lambda_cons = 0.3\n\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)  # log1p\n            base = ((pred - yb)**2 * w).mean()\n            pred_lin  = torch.expm1(pred)\n            cons      = ((pred_lin[:, iT] - (pred_lin[:, iG] + pred_lin[:, iD] + pred_lin[:, iC]))**2).mean()\n            loss = base + lambda_cons*cons\n            loss.backward(); opt.step()\n            running += loss.item()*xb.size(0)\n        print(f\"[CNN] Epoch {ep+1}/{epochs} - loss_w(log1p + cons_orig): {running/len(ds):.4f}\")\n\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=1, shuffle=False, num_workers=2, pin_memory=True)\n\n    model.eval(); preds_log=[]\n    with torch.no_grad():\n        for xb in dl_test:\n            x = xb[0]\n            if use_tta:\n                pb = tta_predict(model, x, n=4)\n                preds_log.append(pb[None, :])\n            else:\n                preds_log.append(model(x.unsqueeze(0).to(device)).cpu().numpy())\n    preds_log = np.concatenate(preds_log, axis=0)\n    preds = np.expm1(preds_log); preds = np.clip(preds, 0, None)\n    preds_cnn = pd.DataFrame(preds, index=test_images, columns=target_names)\n    return preds_cnn\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T05:41:47.583707Z","iopub.execute_input":"2025-11-09T05:41:47.584022Z","iopub.status.idle":"2025-11-09T05:41:47.608592Z","shell.execute_reply.started":"2025-11-09T05:41:47.584001Z","shell.execute_reply":"2025-11-09T05:41:47.607369Z"}},"outputs":[],"execution_count":null},{"id":"cdfd4126","cell_type":"code","source":"DINO_DIR = \"/kaggle/input/dinov2/pytorch/base/1\"\nassert os.path.exists(DINO_DIR), \"No existe la carpeta de DINO en /kaggle/input/dinov2/pytorch/base/1. Añádela en Add data → Models.\"\n\n# Construye ViT-B/14 DINOv2 con num_classes=0 (embedding) y tamaño 518\narch = \"vit_base_patch14_dinov2\"\nmodel_dino = timm.create_model(arch, pretrained=False, num_classes=0, img_size=518)\nmodel_dino.eval().to(device)\n\n# Carga de pesos locales (busca safetensors/pt/bin dentro de la ruta)\nweights = []\nfor ext in (\"*.safetensors\",\"*.pt\",\"*.bin\"):\n    weights += glob.glob(os.path.join(DINO_DIR, \"**\", ext), recursive=True)\nif weights:\n    wpath = weights[0]\n    print(\"Cargando pesos DINO:\", os.path.basename(wpath))\n    try:\n        sd = torch.load(wpath, map_location=\"cpu\")\n        if isinstance(sd, dict) and \"state_dict\" in sd: sd = sd[\"state_dict\"]\n        model_dino.load_state_dict(sd, strict=False)\n    except Exception as e:\n        print(\"Aviso: no pude mapear todos los nombres; continuaré con pesos parciales/none.\", e)\nelse:\n    print(\"Aviso: no se encontraron pesos en la carpeta; el extractor rendirá menos.\")\n\n# Transform de 518px\nIMG_SIZE_DINO = 518\ndino_tf = T.Compose([\n    T.Resize((IMG_SIZE_DINO, IMG_SIZE_DINO)),\n    T.ToTensor(),\n    T.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)),\n])\n\nclass ImgOnlyDS(Dataset):\n    def __init__(self, rel_paths): self.rel = rel_paths\n    def __len__(self): return len(self.rel)\n    def __getitem__(self, i):\n        p = (COMP_DIR / self.rel[i])\n        try:\n            im = Image.open(p).convert(\"RGB\")\n        except:\n            im = Image.new(\"RGB\",(IMG_SIZE_DINO, IMG_SIZE_DINO), (0,0,0))\n        return dino_tf(im)\n\ndef dino_embeddings(paths, bs=8):   # batch 8 por memoria con 518px\n    ds = ImgOnlyDS(paths)\n    dl = DataLoader(ds, batch_size=bs, shuffle=False, num_workers=2, pin_memory=True)\n    embs = []\n    model_dino.eval()\n    with torch.no_grad():\n        for xb in dl:\n            xb = xb.to(device)\n            z = model_dino(xb)      # (B, D)\n            if isinstance(z, (list, tuple)):\n                z = z[0]\n            embs.append(z.detach().cpu().numpy())\n    return np.concatenate(embs, axis=0)\n\n# Embeddings de train/test\ntrain_imgs = y_wide.index.to_list()\ntest_imgs  = test[\"image_path\"].drop_duplicates().to_list()\nE_train = dino_embeddings(train_imgs, bs=8)\nE_test  = dino_embeddings(test_imgs,  bs=8)\n\n# Regr. por target\nscaler = StandardScaler()\nE_train_std = scaler.fit_transform(E_train)\nE_test_std  = scaler.transform(E_test)\n\nY = y_wide[target_names].values\npreds_dino = np.zeros((len(test_imgs), len(target_names)), dtype=np.float32)\nfor j,t in enumerate(target_names):\n    w = np.full(len(E_train_std), TARGET_WEIGHTS[t], dtype=np.float32)\n    reg = Ridge(alpha=1.0, fit_intercept=True)\n    reg.fit(E_train_std, Y[:, j], sample_weight=w)\n    preds_dino[:, j] = reg.predict(E_test_std)\n\npreds_dino = np.clip(preds_dino, 0, None)\npreds_dino_df = pd.DataFrame(preds_dino, index=test_imgs, columns=target_names)\npreds_dino_df.head(3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T05:41:55.401643Z","iopub.execute_input":"2025-11-09T05:41:55.401982Z","iopub.status.idle":"2025-11-09T06:01:49.291961Z","shell.execute_reply.started":"2025-11-09T05:41:55.401957Z","shell.execute_reply":"2025-11-09T06:01:49.290729Z"}},"outputs":[],"execution_count":null},{"id":"717b638a","cell_type":"code","source":"print(\">> Entrenando TABULAR (HGB) ...\")\npreds_tab = predict_tabular(train, y_wide, test, target_names)\n\nprint(\"\\n>> Entrenando Small CNN ...\")\npreds_cnn = train_cnn_predict(y_wide, test, target_names,\n                              epochs=8, batch_size=16, lr=1e-3, wd=1e-5, use_tta=True)\n\n# Blending simple por target (ajusta si tu LB cambia)\nBLEND = {\n    \"Dry_Green_g\": (0.40, 0.30, 0.30),  # (tab, cnn, dino)\n    \"Dry_Dead_g\":  (0.40, 0.30, 0.30),\n    \"Dry_Clover_g\":(0.40, 0.30, 0.30),\n    \"GDM_g\":       (0.35, 0.35, 0.30),\n    \"Dry_Total_g\": (0.30, 0.30, 0.40),  # total confía un poco más en DINO\n}\n\npreds_ens = preds_dino_df.copy()\nfor t in target_names:\n    a,b,c = BLEND[t]\n    preds_ens[t] = a*preds_tab.loc[test_imgs, t].values +                    b*preds_cnn.loc[test_imgs, t].values +                    c*preds_dino_df.loc[test_imgs, t].values\n\n# Consistencia suave para Total\ntotal_parts = preds_ens[\"Dry_Green_g\"] + preds_ens[\"Dry_Dead_g\"] + preds_ens[\"Dry_Clover_g\"]\npreds_ens[\"Dry_Total_g\"] = 0.7*preds_ens[\"Dry_Total_g\"] + 0.3*total_parts\npreds_ens = preds_ens.clip(lower=0)\n\npreds_ens.head(3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T06:02:24.659740Z","iopub.execute_input":"2025-11-09T06:02:24.662992Z","iopub.status.idle":"2025-11-09T06:09:10.060140Z","shell.execute_reply.started":"2025-11-09T06:02:24.662927Z","shell.execute_reply":"2025-11-09T06:09:10.058864Z"}},"outputs":[],"execution_count":null},{"id":"3ce3e9f5","cell_type":"code","source":"preds_long = (preds_ens.reset_index()\n              .melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"pred\")\n              .rename(columns={\"index\":\"image_path\"}))\nsubmission = (test[[\"sample_id\",\"image_path\",\"target_name\"]]\n              .merge(preds_long, on=[\"image_path\",\"target_name\"], how=\"left\")\n              [[\"sample_id\",\"pred\"]].rename(columns={\"pred\":\"target\"}))\n\nassert submission.shape[0] == test.shape[0], \"Submission size mismatch\"\nassert np.isfinite(submission[\"target\"]).all(), \"NaN/Inf en submission\"\nsubmission[\"target\"] = submission[\"target\"].clip(lower=0)\n\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"submission.csv escrito. Filas:\", len(submission))\ndisplay(submission.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T06:09:33.610652Z","iopub.execute_input":"2025-11-09T06:09:33.611045Z","iopub.status.idle":"2025-11-09T06:09:33.639319Z","shell.execute_reply.started":"2025-11-09T06:09:33.611017Z","shell.execute_reply":"2025-11-09T06:09:33.638035Z"}},"outputs":[],"execution_count":null}]}