{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":13636070,"sourceType":"datasetVersion","datasetId":8667402},{"sourceId":4534,"sourceType":"modelInstanceVersion","modelInstanceId":3326,"modelId":986},{"sourceId":268942,"sourceType":"modelInstanceVersion","modelInstanceId":230141,"modelId":251887}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"80c8ff90","cell_type":"markdown","source":"# CSIRO Biomass — Two‑Stream + DINOv2/SigLIP Embeddings + ElasticNet Stacking, Calibración y Proyección Exacta (T4‑Ready)","metadata":{}},{"id":"68e0f357","cell_type":"markdown","source":"**Resumen de la propuesta**\n- Two‑Stream (checkpoints previos, 5 folds, TTA 3 vistas) → 3 targets → reconstrucción a 5.\n- Embeddings DINOv2‑518 y SigLIP‑384 → ElasticNetCV por target con OOF.\n- Stacking con ElasticNetCV por target usando todas las ramas (Two‑Stream, DINO, SigLIP y externos).\n- Calibración por target con OOF y proyección exacta de constraints.\n- Seguro: omite ramas no disponibles y mantiene compatibilidad Kaggle (sin sample_submission).","metadata":{}},{"id":"93bd0adf","cell_type":"markdown","source":"**Pipeline**\n1) Carga datos. \n2) Two‑Stream OOF/Test desde checkpoints. \n3) DINO/SigLIP embeddings + EN OOF/Test.\n4) (Opcional) Cargar OOF/Test externos. \n5) Stacking EN + Calibración + Constraints. \n6) Exporta `submission.csv`. ","metadata":{}},{"id":"3777c8a6","cell_type":"markdown","source":"## Inicializa imports, rutas, semilla, flags de GPU y rutas de datos/modelos.","metadata":{}},{"id":"fc13fac4","cell_type":"code","source":"\nimport os, gc, random\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\n\nfrom PIL import Image\n\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport timm\n\nfrom sklearn.model_selection import KFold\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.linear_model import ElasticNetCV, LinearRegression\n\nDATA_DIR = Path(\"/kaggle/input/csiro-biomass\")\nWORK_DIR = Path(\"/kaggle/working\"); WORK_DIR.mkdir(exist_ok=True, parents=True)\n\nOOF_CSV_CANDIDATES = []\nTEST_CSV_CANDIDATES = []\n\nDINO_DIR   = \"/kaggle/input/dinov2/pytorch/base/1\"\nSIGLIP_DIR = None\nfor cand in [\"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\",\n             \"/kaggle/input/siglip-so400m-384\",\n             \"/kaggle/input/siglip-vitb16-384\"]:\n    if os.path.exists(cand):\n        SIGLIP_DIR = cand\n        break\n\nTS_MODEL_DIR = Path(\"/kaggle/input/csiro-exp3/convnext_exp3\")\nTS_BACKBONE  = \"convnext_small\"\nTS_IMG_SIZE  = 1000\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nAMP = torch.cuda.is_available()\nif torch.cuda.is_available():\n    torch.set_float32_matmul_precision(\"high\")\n    torch.backends.cuda.matmul.allow_tf32 = True\n    torch.backends.cudnn.allow_tf32 = True\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\nprint(\"Device:\", device)\nprint(\"DINO path ok:\", os.path.exists(DINO_DIR))\nprint(\"SigLIP path:\", SIGLIP_DIR)\nprint(\"Two‑Stream dir ok:\", TS_MODEL_DIR.exists())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:02:33.794806Z","iopub.execute_input":"2025-11-10T16:02:33.795059Z","iopub.status.idle":"2025-11-10T16:02:49.738140Z","shell.execute_reply.started":"2025-11-10T16:02:33.795034Z","shell.execute_reply":"2025-11-10T16:02:49.737359Z"}},"outputs":[],"execution_count":null},{"id":"64fcd6f6","cell_type":"markdown","source":"## Carga `train/test`, crea matriz ancha `y_wide` y define pesos por target.","metadata":{}},{"id":"8bbc0186","cell_type":"code","source":"\ntrain = pd.read_csv(DATA_DIR / \"train.csv\")\ntest  = pd.read_csv(DATA_DIR / \"test.csv\")\n\ntarget_names = sorted(train[\"target_name\"].unique())\ny_wide = train.pivot(index=\"image_path\", columns=\"target_name\", values=\"target\").loc[:, target_names]\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\ntrain_imgs = y_wide.index.tolist()\ntest_imgs  = test[\"image_path\"].drop_duplicates().tolist()\ny_matrix = y_wide[target_names].values\n\nprint(\"train images:\", len(train_imgs), \"test images:\", len(test_imgs))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:02:49.739742Z","iopub.execute_input":"2025-11-10T16:02:49.740198Z","iopub.status.idle":"2025-11-10T16:02:49.801195Z","shell.execute_reply.started":"2025-11-10T16:02:49.740178Z","shell.execute_reply":"2025-11-10T16:02:49.800507Z"}},"outputs":[],"execution_count":null},{"id":"0b3990fe","cell_type":"markdown","source":"## Lectura (opcional) de OOF/Test externos","metadata":{}},{"id":"8a7a461e","cell_type":"code","source":"\ndef try_read_csv(path):\n    try:\n        if path and os.path.exists(path):\n            df = pd.read_csv(path)\n            if \"image_path\" in df.columns:\n                df = df.set_index(\"image_path\")\n            return df\n    except Exception as e:\n        print(\"No pude leer:\", path, \"->\", e)\n    return None\n\noof_external = []\ntest_external = []\n\nfor p in OOF_CSV_CANDIDATES:\n    df = try_read_csv(p)\n    if df is not None and all(t in df.columns for t in target_names):\n        print(\"OOF externo:\", p, df.shape)\n        oof_external.append(df[target_names].reindex(train_imgs))\n\nfor p in TEST_CSV_CANDIDATES:\n    df = try_read_csv(p)\n    if df is not None and all(t in df.columns for t in target_names):\n        print(\"TEST externo:\", p, df.shape)\n        test_external.append(df[target_names].reindex(test_imgs))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:02:49.802041Z","iopub.execute_input":"2025-11-10T16:02:49.802290Z","iopub.status.idle":"2025-11-10T16:02:49.809622Z","shell.execute_reply.started":"2025-11-10T16:02:49.802271Z","shell.execute_reply":"2025-11-10T16:02:49.808656Z"}},"outputs":[],"execution_count":null},{"id":"caf32b2b","cell_type":"markdown","source":"## Define Two‑Stream (idéntico al previo): dataset L/R, TTA, arquitectura y carga de 5 folds.","metadata":{}},{"id":"4ee10c54","cell_type":"code","source":"\nfrom dataclasses import dataclass\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport cv2, torch.nn as nn\n\n@dataclass\nclass TSConfig:\n    base_path: Path = Path(\"/kaggle/input/csiro-biomass\")\n    model_dir: Path = TS_MODEL_DIR\n    model_name: str = TS_BACKBONE\n    img_size: int = TS_IMG_SIZE\n    n_folds: int = 5\n    batch_size: int = 1\n    num_workers: int = 1\n    device: torch.device = device\n\nTS_CFG = TSConfig()\n\ndef ts_get_tta(img_size):\n    base = [A.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)), ToTensorV2()]\n    return [\n        A.Compose([A.Resize(img_size, img_size), *base]),\n        A.Compose([A.HorizontalFlip(p=1.0), A.Resize(img_size, img_size), *base]),\n        A.Compose([A.VerticalFlip(p=1.0),   A.Resize(img_size, img_size), *base]),\n    ]\n\nclass TSTestDS(Dataset):\n    def __init__(self, df_unique, tta_transform, image_dir):\n        self.paths = df_unique[\"image_path\"].values\n        self.tfm = tta_transform\n        self.img_dir = image_dir\n    def __len__(self): return len(self.paths)\n    def __getitem__(self, i):\n        p = Path(self.img_dir) / Path(self.paths[i]).name\n        im = cv2.imread(str(p))\n        if im is None:\n            im = np.zeros((TS_CFG.img_size, TS_CFG.img_size*2, 3), dtype=np.uint8)\n        im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)\n        h, w = im.shape[:2]; mid = w // 2\n        l  = self.tfm(image=im[:, :mid])[\"image\"]\n        r  = self.tfm(image=im[:, mid:])[\"image\"]\n        return l, r, self.paths[i]\n\nclass TSModel(nn.Module):\n    def __init__(self, model_name):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=False, num_classes=0, global_pool='avg')\n        nf = self.backbone.num_features\n        self.head_total = nn.Sequential(nn.Linear(nf*2, nf), nn.ReLU(), nn.Dropout(0.3), nn.Linear(nf, 1))\n        self.head_gdm   = nn.Sequential(nn.Linear(nf*2, nf), nn.ReLU(), nn.Dropout(0.3), nn.Linear(nf, 1))\n        self.head_green = nn.Sequential(nn.Linear(nf*2, nf), nn.ReLU(), nn.Dropout(0.3), nn.Linear(nf, 1))\n    def forward(self, l, r):\n        fl = self.backbone(l); fr = self.backbone(r)\n        z = torch.cat([fl, fr], dim=1)\n        return self.head_total(z), self.head_gdm(z), self.head_green(z)\n\ndef ts_load_fold_models():\n    assert TS_CFG.model_dir.exists(), f\"No existe {TS_CFG.model_dir}\"\n    models = []\n    for k in range(TS_CFG.n_folds):\n        mp = TS_CFG.model_dir / f\"best_model_fold{k}.pth\"\n        assert mp.exists(), f\"Falta {mp}\"\n        m = TSModel(TS_CFG.model_name).to(TS_CFG.device).eval()\n        sd = torch.load(mp, map_location=TS_CFG.device)\n        sd = {k.replace(\"module.\",\"\"): v for k,v in sd.items()}\n        m.load_state_dict(sd, strict=True)\n        models.append(m)\n    return models\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:02:49.810475Z","iopub.execute_input":"2025-11-10T16:02:49.810762Z","iopub.status.idle":"2025-11-10T16:03:22.569581Z","shell.execute_reply.started":"2025-11-10T16:02:49.810736Z","shell.execute_reply":"2025-11-10T16:03:22.568796Z"}},"outputs":[],"execution_count":null},{"id":"8de1e5ce","cell_type":"markdown","source":"## Two‑Stream OOF en *train* (TTA + 5 folds) y reconstrucción 3→5.","metadata":{}},{"id":"c4efba69","cell_type":"code","source":"\ndef ts_oof_train(train_df):\n    df_u = train_df.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\n    img_dir = TS_CFG.base_path / \"train\"\n    tta = ts_get_tta(TS_CFG.img_size)\n    models = ts_load_fold_models()\n\n    kf = KFold(n_splits=TS_CFG.n_folds, shuffle=True, random_state=42)\n    indices = list(kf.split(df_u))\n\n    preds_total = np.zeros((len(df_u),), dtype=np.float32)\n    preds_gdm   = np.zeros((len(df_u),), dtype=np.float32)\n    preds_green = np.zeros((len(df_u),), dtype=np.float32)\n\n    for fold, (_, va_idx) in enumerate(indices):\n        m = models[fold]\n        fold_total = []; fold_gdm = []; fold_green = []\n        for view in tta:\n            ds = TSTestDS(df_u.iloc[va_idx], view, img_dir)\n            dl = DataLoader(ds, batch_size=TS_CFG.batch_size, shuffle=False,\n                            num_workers=TS_CFG.num_workers, pin_memory=True)\n            vt, vg, vg2 = [], [], []\n            with torch.no_grad(), torch.amp.autocast(\"cuda\", enabled=AMP):\n                for l, r, _ in dl:\n                    l = l.to(TS_CFG.device); r = r.to(TS_CFG.device)\n                    pt, pg, pgr = m(l, r)\n                    vt.append(pt.squeeze(1).cpu().float().numpy())\n                    vg.append(pg.squeeze(1).cpu().float().numpy())\n                    vg2.append(pgr.squeeze(1).cpu().float().numpy())\n            fold_total.append(np.concatenate(vt))\n            fold_gdm.append(np.concatenate(vg))\n            fold_green.append(np.concatenate(vg2))\n        preds_total[va_idx] = np.mean(np.stack(fold_total, 0), 0)\n        preds_gdm[va_idx]   = np.mean(np.stack(fold_gdm,   0), 0)\n        preds_green[va_idx] = np.mean(np.stack(fold_green, 0), 0)\n\n    clover = np.clip(preds_gdm - preds_green, 0, None)\n    dead   = np.clip(preds_total - preds_gdm, 0, None)\n\n    oof_df = pd.DataFrame({\n        \"image_path\": df_u[\"image_path\"].values,\n        \"Dry_Green_g\": preds_green,\n        \"Dry_Dead_g\":  dead,\n        \"Dry_Clover_g\": clover,\n        \"GDM_g\":        preds_gdm,\n        \"Dry_Total_g\":  preds_total,\n    }).set_index(\"image_path\").reindex(train_imgs)\n\n    print(\"Two‑Stream OOF shape:\", oof_df.shape)\n    return oof_df\n\nts_oof_df = ts_oof_train(train)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:03:22.570573Z","iopub.execute_input":"2025-11-10T16:03:22.570843Z","iopub.status.idle":"2025-11-10T16:05:51.341910Z","shell.execute_reply.started":"2025-11-10T16:03:22.570819Z","shell.execute_reply":"2025-11-10T16:05:51.341101Z"}},"outputs":[],"execution_count":null},{"id":"679866d2","cell_type":"markdown","source":"## Two‑Stream en *test* (TTA + ensemble 5 folds) y reconstrucción 3→5; engancha al stacker.","metadata":{}},{"id":"93bafe7b","cell_type":"code","source":"\ndef ts_test_preds(test_df):\n    df_u = test_df.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\n    img_dir = TS_CFG.base_path / \"test\"\n    tta = ts_get_tta(TS_CFG.img_size)\n    models = ts_load_fold_models()\n\n    all_total = []; all_gdm = []; all_green = []\n    for view in tta:\n        ds = TSTestDS(df_u, view, img_dir)\n        dl = DataLoader(ds, batch_size=TS_CFG.batch_size, shuffle=False,\n                        num_workers=TS_CFG.num_workers, pin_memory=True)\n        vt, vg, vg2 = [], [], []\n        with torch.no_grad(), torch.amp.autocast(\"cuda\", enabled=AMP):\n            for l, r, _ in dl:\n                l = l.to(TS_CFG.device); r = r.to(TS_CFG.device)\n                pt, pg, pgr = [], [], []\n                for m in models:\n                    a,b,c = m(l, r)\n                    pt.append(a); pg.append(b); pgr.append(c)\n                pt = torch.stack(pt).mean(0); pg = torch.stack(pg).mean(0); pgr = torch.stack(pgr).mean(0)\n                vt.append(pt.squeeze(1).cpu().float().numpy())\n                vg.append(pg.squeeze(1).cpu().float().numpy())\n                vg2.append(pgr.squeeze(1).cpu().float().numpy())\n        all_total.append(np.concatenate(vt))\n        all_gdm.append(np.concatenate(vg))\n        all_green.append(np.concatenate(vg2))\n\n    preds_total = np.mean(np.stack(all_total, 0), 0)\n    preds_gdm   = np.mean(np.stack(all_gdm,   0), 0)\n    preds_green = np.mean(np.stack(all_green, 0), 0)\n\n    clover = np.clip(preds_gdm - preds_green, 0, None)\n    dead   = np.clip(preds_total - preds_gdm, 0, None)\n\n    test_df2 = pd.DataFrame({\n        \"image_path\": df_u[\"image_path\"].values,\n        \"Dry_Green_g\": preds_green,\n        \"Dry_Dead_g\":  dead,\n        \"Dry_Clover_g\": clover,\n        \"GDM_g\":        preds_gdm,\n        \"Dry_Total_g\":  preds_total,\n    }).set_index(\"image_path\").reindex(test_imgs)\n\n    print(\"Two‑Stream TEST shape:\", test_df2.shape)\n    return test_df2\n\nts_test_df = ts_test_preds(test)\noof_external.append(ts_oof_df[target_names].reindex(train_imgs))\ntest_external.append(ts_test_df[target_names].reindex(test_imgs))\nprint(\"Two‑Stream agregado a oof_external/test_external.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:05:51.343667Z","iopub.execute_input":"2025-11-10T16:05:51.343921Z","iopub.status.idle":"2025-11-10T16:05:59.118730Z","shell.execute_reply.started":"2025-11-10T16:05:51.343900Z","shell.execute_reply":"2025-11-10T16:05:59.118043Z"}},"outputs":[],"execution_count":null},{"id":"f6aedbc7","cell_type":"markdown","source":"## Helper de embeddings (dataset simple y extracción con timm).","metadata":{}},{"id":"672229a1","cell_type":"code","source":"\nclass ImgDS(Dataset):\n    def __init__(self, relpaths, tfm, root=DATA_DIR):\n        self.rel = relpaths; self.tfm = tfm; self.root = root\n    def __len__(self): return len(self.rel)\n    def __getitem__(self, i):\n        p = Path(self.root) / self.rel[i]\n        try:\n            im = Image.open(p).convert(\"RGB\")\n        except Exception:\n            im = Image.new(\"RGB\", (512, 256), (0,0,0))\n        return self.tfm(im)\n\ndef embed_with_model(model, tfm, relpaths, bs=16):\n    dl = DataLoader(ImgDS(relpaths, tfm), batch_size=bs, shuffle=False, num_workers=2, pin_memory=True)\n    out = []\n    model.eval()\n    with torch.no_grad(), torch.amp.autocast(\"cuda\", enabled=AMP):\n        for xb in dl:\n            xb = xb.to(device)\n            z = model(xb)\n            if isinstance(z, (list, tuple)): z = z[0]\n            out.append(z.detach().cpu().float().numpy())\n    return np.concatenate(out, 0)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:05:59.121118Z","iopub.execute_input":"2025-11-10T16:05:59.121332Z","iopub.status.idle":"2025-11-10T16:05:59.128175Z","shell.execute_reply.started":"2025-11-10T16:05:59.121314Z","shell.execute_reply":"2025-11-10T16:05:59.127616Z"}},"outputs":[],"execution_count":null},{"id":"f7ea9698","cell_type":"markdown","source":"## DINOv2‑518 → embeddings y OOF/Test con ElasticNetCV por target.","metadata":{}},{"id":"c696bc3d","cell_type":"code","source":"\nEtr_dino = Ets_dino = dino_oof = dino_test = None\n\nif os.path.exists(DINO_DIR):\n    try:\n        IMG = 518\n        dino = timm.create_model(\"vit_base_patch14_dinov2\", pretrained=True, num_classes=0, img_size=IMG).to(device)\n        if torch.cuda.is_available(): dino.half()\n        tf_dino = T.Compose([T.Resize((IMG, IMG)), T.ToTensor(),\n                             T.Normalize((0.485,0.456,0.406),(0.229,0.224,0.225))])\n        print(\"Embeddings DINO...\")\n        Etr_dino = embed_with_model(dino, tf_dino, train_imgs, bs=8)\n        Ets_dino = embed_with_model(dino, tf_dino, test_imgs,  bs=8)\n\n        kf = KFold(n_splits=5, shuffle=True, random_state=42)\n        dino_oof = np.zeros_like(y_matrix, dtype=np.float32)\n        for tr, va in kf.split(Etr_dino):\n            sc = StandardScaler(); Xtr=sc.fit_transform(Etr_dino[tr]); Xva=sc.transform(Etr_dino[va])\n            pv = np.zeros_like(y_matrix[va], dtype=np.float32)\n            for j in range(y_matrix.shape[1]):\n                en = ElasticNetCV(l1_ratio=[.1,.3,.5,.7,.9,1.0], cv=3, max_iter=5000, random_state=42)\n                en.fit(Xtr, y_matrix[tr, j])\n                pv[:, j] = en.predict(Xva)\n            dino_oof[va] = np.clip(pv, 0, None)\n\n        sc_full = StandardScaler().fit(Etr_dino)\n        Xtr = sc_full.transform(Etr_dino); Xte = sc_full.transform(Ets_dino)\n        preds = []\n        for j in range(y_matrix.shape[1]):\n            en = ElasticNetCV(l1_ratio=[.1,.3,.5,.7,.9,1.0], cv=5, max_iter=5000, random_state=42)\n            en.fit(Xtr, y_matrix[:, j])\n            preds.append(en.predict(Xte))\n        dino_test = np.column_stack(preds).clip(0, None)\n\n        print(\"DINO listo:\", dino_oof.shape, dino_test.shape)\n    except Exception as e:\n        print(\"DINO error:\", e)\nelse:\n    print(\"No hay DINO dataset; se omite.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:05:59.128844Z","iopub.execute_input":"2025-11-10T16:05:59.129145Z","iopub.status.idle":"2025-11-10T16:07:04.500502Z","shell.execute_reply.started":"2025-11-10T16:05:59.129118Z","shell.execute_reply":"2025-11-10T16:07:04.499650Z"}},"outputs":[],"execution_count":null},{"id":"d5b64bbd","cell_type":"markdown","source":"## SigLIP‑384 → embeddings y OOF/Test con ElasticNetCV por target (si hay dataset).","metadata":{}},{"id":"1f97569c","cell_type":"code","source":"\nEtr_sig = Ets_sig = sig_oof = sig_test = None\n\nif SIGLIP_DIR:\n    try:\n        IMG = 384\n        model = None\n        for arch in [\"vit_so400m_patch14_siglip_384\", \"vit_base_patch14_siglip_384\", \"vit_large_patch14_384\"]:\n            try:\n                model = timm.create_model(arch, pretrained=True, num_classes=0); break\n            except Exception: continue\n        if model is None: raise RuntimeError(\"No pude inicializar SigLIP en timm.\")\n        model = model.to(device)\n        if torch.cuda.is_available(): model.half()\n        tf_sig = T.Compose([T.Resize((IMG, IMG)), T.ToTensor(),\n                            T.Normalize((0.485,0.456,0.406),(0.229,0.224,0.225))])\n        print(\"Embeddings SigLIP...\")\n        Etr_sig = embed_with_model(model, tf_sig, train_imgs, bs=16)\n        Ets_sig = embed_with_model(model, tf_sig, test_imgs,  bs=16)\n\n        kf = KFold(n_splits=5, shuffle=True, random_state=42)\n        sig_oof = np.zeros_like(y_matrix, dtype=np.float32)\n        for tr, va in kf.split(Etr_sig):\n            sc = StandardScaler(); Xtr=sc.fit_transform(Etr_sig[tr]); Xva=sc.transform(Etr_sig[va])\n            pv = np.zeros_like(y_matrix[va], dtype=np.float32)\n            for j in range(y_matrix.shape[1]):\n                en = ElasticNetCV(l1_ratio=[.1,.3,.5,.7,.9,1.0], cv=3, max_iter=5000, random_state=42)\n                en.fit(Xtr, y_matrix[tr, j])\n                pv[:, j] = en.predict(Xva)\n            sig_oof[va] = np.clip(pv, 0, None)\n\n        sc_full = StandardScaler().fit(Etr_sig)\n        Xtr = sc_full.transform(Etr_sig); Xte = sc_full.transform(Ets_sig)\n        preds = []\n        for j in range(y_matrix.shape[1]):\n            en = ElasticNetCV(l1_ratio=[.1,.3,.5,.7,.9,1.0], cv=5, max_iter=5000, random_state=42)\n            en.fit(Xtr, y_matrix[:, j])\n            preds.append(en.predict(Xte))\n        sig_test = np.column_stack(preds).clip(0, None)\n\n        print(\"SigLIP listo:\", sig_oof.shape, sig_test.shape)\n    except Exception as e:\n        print(\"SigLIP error:\", e)\nelse:\n    print(\"No hay SigLIP dataset; se omite.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:07:04.501440Z","iopub.execute_input":"2025-11-10T16:07:04.502038Z","iopub.status.idle":"2025-11-10T16:08:15.016025Z","shell.execute_reply.started":"2025-11-10T16:07:04.502013Z","shell.execute_reply":"2025-11-10T16:08:15.015297Z"}},"outputs":[],"execution_count":null},{"id":"9ce31605","cell_type":"markdown","source":"## Construye matrices OOF/Test por target con todas las ramas disponibles.","metadata":{}},{"id":"f75c266f","cell_type":"code","source":"\ndef collect_mats_per_target(j):\n    oof_cols = []\n    tst_cols = []\n    for df in oof_external:\n        oof_cols.append(df.iloc[:, j].values.reshape(-1,1))\n    for df in test_external:\n        tst_cols.append(df.iloc[:, j].values.reshape(-1,1))\n\n    # Two‑Stream siempre incluido\n    oof_cols.append(ts_oof_df.iloc[:, j].values.reshape(-1,1))\n    tst_cols.append(ts_test_df.iloc[:, j].values.reshape(-1,1))\n\n    if dino_oof is not None: oof_cols.append(dino_oof[:, [j]])\n    if dino_test is not None: tst_cols.append(dino_test[:, [j]])\n    if sig_oof  is not None:  oof_cols.append(sig_oof[:,  [j]])\n    if sig_test is not None:  tst_cols.append(sig_test[:,  [j]])\n\n    if not oof_cols or not tst_cols:\n        return None, None\n    return np.concatenate(oof_cols, 1), np.concatenate(tst_cols, 1)\n\nprint(\"Ramas externas OOF/Test:\", len(oof_external), len(test_external))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:08:15.016858Z","iopub.execute_input":"2025-11-10T16:08:15.017117Z","iopub.status.idle":"2025-11-10T16:08:15.023770Z","shell.execute_reply.started":"2025-11-10T16:08:15.017099Z","shell.execute_reply":"2025-11-10T16:08:15.023178Z"}},"outputs":[],"execution_count":null},{"id":"6ec30499","cell_type":"markdown","source":"## Calibración lineal por target con OOF (y = a + b·pred).","metadata":{}},{"id":"7c2febae","cell_type":"code","source":"\ndef calibrate_with_oof(y_true, y_pred):\n    lr = LinearRegression()\n    lr.fit(y_pred.reshape(-1,1), y_true)\n    return lambda z: lr.predict(z.reshape(-1,1))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:08:15.024807Z","iopub.execute_input":"2025-11-10T16:08:15.025162Z","iopub.status.idle":"2025-11-10T16:08:15.040441Z","shell.execute_reply.started":"2025-11-10T16:08:15.025146Z","shell.execute_reply":"2025-11-10T16:08:15.039822Z"}},"outputs":[],"execution_count":null},{"id":"5efc7917","cell_type":"markdown","source":"## Proyección exacta de constraints y no‑negatividad.","metadata":{}},{"id":"25369a53","cell_type":"code","source":"\ndef project_constraints(df, tn):\n    arr = df.values.copy()\n    g = tn.index(\"Dry_Green_g\")\n    c = tn.index(\"Dry_Clover_g\")\n    d = tn.index(\"Dry_Dead_g\")\n    G = tn.index(\"GDM_g\")\n    Tt = tn.index(\"Dry_Total_g\")\n    for i in range(len(arr)):\n        g0, c0, d0, G0, T0 = arr[i, g], arr[i, c], arr[i, d], arr[i, G], arr[i, Tt]\n        g1 = 0.5*(g0 + max(0.0, G0 - c0))\n        c1 = 0.5*(c0 + max(0.0, G0 - g0))\n        g1, c1 = max(0.0, g1), max(0.0, c1)\n        G1 = g1 + c1\n        d1 = 0.5*(d0 + max(0.0, T0 - G1))\n        d1 = max(0.0, d1)\n        arr[i, g], arr[i, c], arr[i, d] = g1, c1, d1\n        arr[i, G]  = g1 + c1\n        arr[i, Tt] = g1 + c1 + d1\n    return pd.DataFrame(arr, index=df.index, columns=df.columns)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:08:15.041120Z","iopub.execute_input":"2025-11-10T16:08:15.041336Z","iopub.status.idle":"2025-11-10T16:08:15.059377Z","shell.execute_reply.started":"2025-11-10T16:08:15.041321Z","shell.execute_reply":"2025-11-10T16:08:15.058574Z"}},"outputs":[],"execution_count":null},{"id":"c711d46f","cell_type":"markdown","source":"## Stacking (ElasticNetCV) por target + calibración OOF + constraints exactos → `submission.csv`.","metadata":{}},{"id":"1adcc5d0","cell_type":"code","source":"\npreds_stack = np.zeros((len(test_imgs), len(target_names)), dtype=np.float32)\n\nfor j, t in enumerate(target_names):\n    X_tr, X_te = collect_mats_per_target(j)\n    if X_tr is None:\n        print(f\"[WARN] Sin ramas para {t}. Predicciones = 0.0\")\n        preds_stack[:, j] = 0.0\n        continue\n\n    y_tr = y_wide[target_names].values[:, j]\n    sc = StandardScaler(with_mean=False)\n    Xtr = sc.fit_transform(X_tr); Xte = sc.transform(X_te)\n\n    en = ElasticNetCV(l1_ratio=[.1,.3,.5,.7,.9,1.0], cv=5, max_iter=5000, random_state=42)\n    en.fit(Xtr, y_tr)\n\n    oof_pred = en.predict(Xtr)\n    cal = calibrate_with_oof(y_tr, oof_pred)\n\n    preds_stack[:, j] = cal(en.predict(Xte))\n\npreds_df = pd.DataFrame(preds_stack, index=test_imgs, columns=target_names).clip(lower=0)\npreds_df = project_constraints(preds_df, target_names)\n\npreds_long = (preds_df.reset_index()\n              .melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"target\")\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\",\"target\"]])\nassert submission.shape[0] == test.shape[0], \"Submission size mismatch\"\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"submission.csv escrito. Filas:\", len(submission))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T16:08:15.060180Z","iopub.execute_input":"2025-11-10T16:08:15.060410Z","iopub.status.idle":"2025-11-10T16:08:16.321760Z","shell.execute_reply.started":"2025-11-10T16:08:15.060396Z","shell.execute_reply":"2025-11-10T16:08:16.321019Z"}},"outputs":[],"execution_count":null}]}