{"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":"tpuV5e8","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14550214,"sourceType":"datasetVersion","datasetId":9287806}],"dockerImageVersionId":31261,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:03.367287Z","iopub.execute_input":"2026-01-21T22:05:03.367470Z","iopub.status.idle":"2026-01-21T22:05:03.713389Z","shell.execute_reply.started":"2026-01-21T22:05:03.367450Z","shell.execute_reply":"2026-01-21T22:05:03.712460Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import GroupKFold\n\nDATA_DIR = \"/kaggle/input/csiro-biomass\"\nTRAIN_CSV = os.path.join(DATA_DIR, \"train.csv\")\nTEST_CSV  = os.path.join(DATA_DIR, \"test.csv\")\n\nprint(\"TRAIN_CSV:\", TRAIN_CSV, \"exists:\", os.path.exists(TRAIN_CSV))\nprint(\"TEST_CSV :\", TEST_CSV,  \"exists:\", os.path.exists(TEST_CSV))\n\n# ako ovo ispod pukne, znači da dataset nije pravilno \"Added\" ili je drugačije ime foldera\ndf_long = pd.read_csv(TRAIN_CSV)\n\nTARGETS = [\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]\n\ndf_wide = df_long.pivot_table(\n    index=\"image_path\",\n    columns=\"target_name\",\n    values=\"target\",\n    aggfunc=\"mean\"\n).reset_index().rename_axis(None, axis=1)\n\nmeta_cols = [\"image_path\",\"Sampling_Date\",\"State\",\"Species\",\"Pre_GSHH_NDVI\",\"Height_Ave_cm\"]\ndf_meta = df_long.drop_duplicates(\"image_path\")[meta_cols]\ndf = df_wide.merge(df_meta, on=\"image_path\", how=\"left\")\n\nmissing = [t for t in TARGETS if t not in df.columns]\nprint(\"Missing targets:\", missing)\n\ndf[\"group\"] = df[\"State\"].astype(str) + \"_\" + df[\"Sampling_Date\"].astype(str)\n\ngkf = GroupKFold(n_splits=5)\ndf[\"fold\"] = -1\nfor fold, (_, va) in enumerate(gkf.split(df, groups=df[\"group\"])):\n    df.loc[va, \"fold\"] = fold\n\nprint(\"df shape:\", df.shape)\nprint(\"fold counts:\", df[\"fold\"].value_counts().to_dict())\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:03.714446Z","iopub.execute_input":"2026-01-21T22:05:03.714729Z","iopub.status.idle":"2026-01-21T22:05:04.608085Z","shell.execute_reply.started":"2026-01-21T22:05:03.714708Z","shell.execute_reply":"2026-01-21T22:05:04.607177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def weighted_r2_long(y_true, y_pred, target_name):\n    w = np.array([W[t] for t in target_name], dtype=np.float64)\n    y_true = np.asarray(y_true, dtype=np.float64)\n    y_pred = np.asarray(y_pred, dtype=np.float64)\n\n    y_bar = (w * y_true).sum() / w.sum()\n    ss_res = (w * (y_true - y_pred) ** 2).sum()\n    ss_tot = (w * (y_true - y_bar) ** 2).sum()\n    return 1.0 - ss_res / (ss_tot + 1e-12)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:04.608645Z","iopub.execute_input":"2026-01-21T22:05:04.608847Z","iopub.status.idle":"2026-01-21T22:05:04.613002Z","shell.execute_reply.started":"2026-01-21T22:05:04.608830Z","shell.execute_reply":"2026-01-21T22:05:04.611887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_long = pd.read_csv(TRAIN_CSV)\n\ndf_wide = df_long.pivot_table(\n    index=\"image_path\",\n    columns=\"target_name\",\n    values=\"target\",\n    aggfunc=\"mean\"\n).reset_index().rename_axis(None, axis=1)\n\nmeta_cols = [\"image_path\",\"Sampling_Date\",\"State\",\"Species\",\"Pre_GSHH_NDVI\",\"Height_Ave_cm\"]\ndf_meta = df_long.drop_duplicates(\"image_path\")[meta_cols]\ndf = df_wide.merge(df_meta, on=\"image_path\", how=\"left\")\n\nfor t in TARGETS:\n    if t not in df.columns:\n        raise ValueError(f\"Missing target column: {t}\")\n\ndf[\"group\"] = df[\"State\"].astype(str) + \"_\" + df[\"Sampling_Date\"].astype(str)\n\ngkf = GroupKFold(n_splits=5)\ndf[\"fold\"] = -1\nfor fold, (_, va) in enumerate(gkf.split(df, groups=df[\"group\"])):\n    df.loc[va, \"fold\"] = fold\n\nprint(df.shape, df[\"fold\"].value_counts().to_dict())\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:04.613537Z","iopub.execute_input":"2026-01-21T22:05:04.613716Z","iopub.status.idle":"2026-01-21T22:05:04.649935Z","shell.execute_reply.started":"2026-01-21T22:05:04.613687Z","shell.execute_reply":"2026-01-21T22:05:04.649000Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from PIL import Image  # <-- obavezno pre korišćenja Image.Image u type hint-u\n\ndef load_pil(path):\n    return Image.open(path).convert(\"RGB\")\n\ndef crop_quadrants(img: Image.Image):\n    w, h = img.size\n    w2, h2 = w // 2, h // 2\n    return [\n        img.crop((0,  0,  w2, h2)),\n        img.crop((w2, 0,  w,  h2)),\n        img.crop((0,  h2, w2, h)),\n        img.crop((w2, h2, w,  h)),\n    ]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:04.650513Z","iopub.execute_input":"2026-01-21T22:05:04.650677Z","iopub.status.idle":"2026-01-21T22:05:04.666463Z","shell.execute_reply.started":"2026-01-21T22:05:04.650662Z","shell.execute_reply":"2026-01-21T22:05:04.665519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset, DataLoader\n\n# (ako koristiš torchvision transforms u istom bloku)\nfrom torchvision import transforms","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:04.667073Z","iopub.execute_input":"2026-01-21T22:05:04.667237Z","iopub.status.idle":"2026-01-21T22:05:06.584563Z","shell.execute_reply.started":"2026-01-21T22:05:04.667222Z","shell.execute_reply":"2026-01-21T22:05:06.583649Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\n\nIMG_SIZE = 384\n\ntfm_global = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomVerticalFlip(p=0.5),\n    transforms.ColorJitter(0.2,0.2,0.2,0.02),\n    transforms.ToTensor(),\n])\n\ntfm_tile = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomVerticalFlip(p=0.5),\n    transforms.ColorJitter(0.2,0.2,0.2,0.02),\n    transforms.ToTensor(),\n])\n\ntfm_val = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n])\n\nclass BiomassTileDS(Dataset):\n    def __init__(self, df_img, train=True, use_log1p=True):\n        self.df = df_img.reset_index(drop=True)\n        self.train = train\n        self.use_log1p = use_log1p\n\n    def __len__(self): return len(self.df)\n\n    def __getitem__(self, i):\n        row = self.df.iloc[i]\n        path = os.path.join(DATA_DIR, row[\"image_path\"])\n        img = load_pil(path)\n\n        tiles = crop_quadrants(img)\n\n        if self.train:\n            xg = tfm_global(img)\n            xt = torch.stack([tfm_tile(t) for t in tiles], dim=0)  # (4,3,H,W)\n        else:\n            xg = tfm_val(img)\n            xt = torch.stack([tfm_val(t) for t in tiles], dim=0)\n\n        y = row[TARGETS].values.astype(np.float32)\n        if self.use_log1p:\n            y = np.log1p(y)\n        y = torch.tensor(y, dtype=torch.float32)\n\n        return xg, xt, y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:06.585328Z","iopub.execute_input":"2026-01-21T22:05:06.585636Z","iopub.status.idle":"2026-01-21T22:05:06.592440Z","shell.execute_reply.started":"2026-01-21T22:05:06.585619Z","shell.execute_reply":"2026-01-21T22:05:06.591564Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GlobalTileRegressor(nn.Module):\n    \"\"\"\n    xg: (B,3,H,W)\n    xt: (B,4,3,H,W)\n    tile aggregation: SUM over 4 tiles (exactly as requested)\n    output: 5 regression values\n    \"\"\"\n    def __init__(self, backbone_name=\"convnext_tiny\", pretrained=True):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0, global_pool=\"avg\")\n        d = self.backbone.num_features\n\n        # head on concatenated [global_feat, tile_sum_feat]\n        self.head = nn.Sequential(\n            nn.Linear(d*2, 1024),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.2),\n            nn.Linear(1024, 5)\n        )\n\n    def forward(self, xg, xt):\n        fg = self.backbone(xg)  # (B,d)\n\n        B, T, C, H, W = xt.shape\n        xt = xt.view(B*T, C, H, W)\n        ft = self.backbone(xt).view(B, T, -1)  # (B,4,d)\n        ft_sum = ft.sum(dim=1)                 # (B,d)  <-- SUM tiles\n\n        f = torch.cat([fg, ft_sum], dim=1)\n        return self.head(f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:06.592982Z","iopub.execute_input":"2026-01-21T22:05:06.593145Z","iopub.status.idle":"2026-01-21T22:05:06.726125Z","shell.execute_reply.started":"2026-01-21T22:05:06.593130Z","shell.execute_reply":"2026-01-21T22:05:06.725033Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def oof_scalars(y_true_5, y_pred_5):\n    \"\"\"\n    Fit c_t per target: minimize SSE => c = sum(y*p)/sum(p^2)\n    y_true_5, y_pred_5: (N,5) on ORIGINAL scale\n    \"\"\"\n    c = np.zeros(5, dtype=np.float64)\n    for k in range(5):\n        p = y_pred_5[:, k].astype(np.float64)\n        y = y_true_5[:, k].astype(np.float64)\n        c[k] = (y*p).sum() / (np.square(p).sum() + 1e-12)\n    return c\n\ndef apply_scalars(pred_5, c):\n    return pred_5 * c.reshape(1,5)\n\ndef eval_oof_weighted_r2(df_val_img, y_true_5, y_pred_5):\n    # build long rows for metric\n    rows = []\n    for i in range(len(df_val_img)):\n        ip = df_val_img.iloc[i][\"image_path\"]\n        for k,t in enumerate(TARGETS):\n            rows.append((ip, t, float(y_true_5[i,k]), float(y_pred_5[i,k])))\n    m = pd.DataFrame(rows, columns=[\"image_path\",\"target_name\",\"y\",\"p\"])\n    return weighted_r2_long(m[\"y\"].values, m[\"p\"].values, m[\"target_name\"].values)\n\ndef train_cv(backbone_name=\"convnext_tiny\", epochs=10, bs=8, lr=1e-4, wd=1e-2, use_log1p=True):\n    oof_pred = np.zeros((len(df), 5), dtype=np.float32)\n\n    for fold in sorted(df[\"fold\"].unique()):\n        tr = df[df.fold != fold].copy()\n        va = df[df.fold == fold].copy()\n\n        ds_tr = BiomassTileDS(tr, train=True,  use_log1p=use_log1p)\n        ds_va = BiomassTileDS(va, train=False, use_log1p=use_log1p)\n\n        dl_tr = DataLoader(ds_tr, batch_size=bs, shuffle=True,  num_workers=2, pin_memory=torch.cuda.is_available())\n        dl_va = DataLoader(ds_va, batch_size=bs, shuffle=False, num_workers=2, pin_memory=torch.cuda.is_available())\n\n        model = GlobalTileRegressor(backbone_name=backbone_name, pretrained=True).to(device)\n        opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=wd)\n        sch = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)\n        scaler = torch.amp.GradScaler(\"cuda\", enabled=torch.cuda.is_available())\n\n        best = -1e18\n        best_state = None\n\n        for ep in range(1, epochs+1):\n            model.train()\n            for xg, xt, y in dl_tr:\n                xg, xt, y = xg.to(device), xt.to(device), y.to(device)\n                opt.zero_grad(set_to_none=True)\n                with torch.amp.autocast(\"cuda\", enabled=torch.cuda.is_available()):\n                    p = model(xg, xt)\n                    loss = F.mse_loss(p, y)\n                scaler.scale(loss).backward()\n                scaler.step(opt); scaler.update()\n\n            model.eval()\n            preds = []\n            with torch.no_grad():\n                for xg, xt, y in dl_va:\n                    xg, xt = xg.to(device), xt.to(device)\n                    p = model(xg, xt).cpu().numpy()\n                    preds.append(p)\n            preds = np.concatenate(preds, axis=0)\n\n            if use_log1p:\n                preds_orig = np.expm1(preds)\n                y_true_orig = va[TARGETS].values.astype(np.float32)\n            else:\n                preds_orig = preds\n                y_true_orig = va[TARGETS].values.astype(np.float32)\n\n            score = eval_oof_weighted_r2(va, y_true_orig, preds_orig)\n\n            if score > best:\n                best = score\n                best_state = {k:v.detach().cpu() for k,v in model.state_dict().items()}\n\n            sch.step()\n            print(f\"[{backbone_name}] fold {fold} ep {ep:02d} wR2={score:.6f} best={best:.6f}\")\n\n        model.load_state_dict(best_state)\n        model.eval()\n\n        # final val preds for fold\n        preds = []\n        with torch.no_grad():\n            for xg, xt, y in dl_va:\n                xg, xt = xg.to(device), xt.to(device)\n                p = model(xg, xt).cpu().numpy()\n                preds.append(p)\n        preds = np.concatenate(preds, axis=0)\n\n        if use_log1p:\n            preds = np.expm1(preds)\n\n        oof_pred[df.fold == fold] = preds.astype(np.float32)\n\n    y_true_all = df[TARGETS].values.astype(np.float32)\n    base = eval_oof_weighted_r2(df, y_true_all, oof_pred)\n\n    c = oof_scalars(y_true_all, oof_pred)\n    oof_scaled = apply_scalars(oof_pred, c)\n    scaled = eval_oof_weighted_r2(df, y_true_all, oof_scaled)\n\n    print(\"\\nOOF weighted R2 (raw):   \", base)\n    print(\"OOF weighted R2 (scaled):\", scaled)\n    print(\"scalars c:\", dict(zip(TARGETS, c.tolist())))\n\n    return oof_pred, c","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:06.726839Z","iopub.status.idle":"2026-01-21T22:05:06.727325Z","shell.execute_reply.started":"2026-01-21T22:05:06.726958Z","shell.execute_reply":"2026-01-21T22:05:06.726967Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def ridge_stack_per_target(oof_list, y_true_5, alpha=1.0):\n    \"\"\"\n    oof_list: list of arrays [(N,5), (N,5), ...]\n    returns: fitted ridge models per target\n    \"\"\"\n    X = np.stack(oof_list, axis=2)  # (N,5,M)\n    models = []\n    for k in range(5):\n        Xk = X[:, k, :]             # (N,M)\n        yk = y_true_5[:, k]\n        m = Ridge(alpha=alpha, fit_intercept=True, random_state=42)\n        m.fit(Xk, yk)\n        models.append(m)\n    return models\n\ndef ridge_predict_per_target(models, pred_list):\n    X = np.stack(pred_list, axis=2)  # (N,5,M)\n    out = np.zeros((X.shape[0], 5), dtype=np.float32)\n    for k,m in enumerate(models):\n        out[:, k] = m.predict(X[:, k, :]).astype(np.float32)\n    return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:06.727973Z","iopub.status.idle":"2026-01-21T22:05:06.728185Z","shell.execute_reply.started":"2026-01-21T22:05:06.728070Z","shell.execute_reply":"2026-01-21T22:05:06.728079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TestTileDS(Dataset):\n    def __init__(self, df_img):\n        self.df = df_img.reset_index(drop=True)\n    def __len__(self): return len(self.df)\n    def __getitem__(self, i):\n        path = os.path.join(DATA_DIR, self.df.iloc[i][\"image_path\"])\n        img = load_pil(path)\n        tiles = crop_quadrants(img)\n        xg = tfm_val(img)\n        xt = torch.stack([tfm_val(t) for t in tiles], dim=0)\n        return xg, xt\n\ndef train_all(backbone_name=\"convnext_small\", epochs=12, bs=8, lr=1e-4, wd=1e-2, use_log1p=True):\n    ds = BiomassTileDS(df, train=True, use_log1p=use_log1p)\n    dl = DataLoader(ds, batch_size=bs, shuffle=True, num_workers=2, pin_memory=torch.cuda.is_available())\n\n    model = GlobalTileRegressor(backbone_name=backbone_name, pretrained=True).to(device)\n    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=wd)\n    sch = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)\n    scaler = torch.amp.GradScaler(\"cuda\", enabled=torch.cuda.is_available())\n\n    for ep in range(1, epochs+1):\n        model.train()\n        losses = []\n        for xg, xt, y in dl:\n            xg, xt, y = xg.to(device), xt.to(device), y.to(device)\n            opt.zero_grad(set_to_none=True)\n            with torch.amp.autocast(\"cuda\", enabled=torch.cuda.is_available()):\n                p = model(xg, xt)\n                loss = F.mse_loss(p, y)\n            scaler.scale(loss).backward()\n            scaler.step(opt); scaler.update()\n            losses.append(loss.item())\n        sch.step()\n        print(f\"[ALL {backbone_name}] ep {ep:02d} loss={np.mean(losses):.5f}\")\n    return model\n\ndef predict_test(model, use_log1p=True):\n    test = pd.read_csv(TEST_CSV)\n    test_imgs = test[[\"image_path\"]].drop_duplicates().reset_index(drop=True)\n\n    ds = TestTileDS(test_imgs)\n    dl = DataLoader(ds, batch_size=16, shuffle=False, num_workers=2, pin_memory=torch.cuda.is_available())\n\n    model.eval()\n    preds = []\n    with torch.no_grad():\n        for xg, xt in dl:\n            xg, xt = xg.to(device), xt.to(device)\n            p = model(xg, xt).cpu().numpy()\n            preds.append(p)\n    preds = np.concatenate(preds, axis=0)\n\n    if use_log1p:\n        preds = np.expm1(preds)\n\n    return test, test_imgs, preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:05:06.728507Z","iopub.status.idle":"2026-01-21T22:05:06.728813Z","shell.execute_reply.started":"2026-01-21T22:05:06.728596Z","shell.execute_reply":"2026-01-21T22:05:06.728604Z"}},"outputs":[],"execution_count":null}]}