{"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, gc, math, random, time, json, warnings\nimport numpy as np, pandas as pd\nfrom pathlib import Path\nfrom PIL import Image\nimport torch, torch.nn as nn, torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import OneHotEncoder\nfrom sklearn.metrics import r2_score\n\nwarnings.filterwarnings(\"ignore\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:14.221665Z","iopub.execute_input":"2025-10-30T21:35:14.221950Z","iopub.status.idle":"2025-10-30T21:35:21.808846Z","shell.execute_reply.started":"2025-10-30T21:35:14.221927Z","shell.execute_reply":"2025-10-30T21:35:21.807854Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"COMP_DIR = Path(\"/kaggle/input/csiro-biomass\")  # adjust if different\nTRAIN_CSV = COMP_DIR/\"train.csv\"\nTEST_CSV  = COMP_DIR/\"test.csv\"\nIMG_ROOT  = COMP_DIR  # train/ and test/ are relative to this\n\nTARGETS = [\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]\nWEIGHTS = {\"Dry_Green_g\":0.1,\"Dry_Dead_g\":0.1,\"Dry_Clover_g\":0.1,\"GDM_g\":0.2,\"Dry_Total_g\":0.5}\n\nSEED=1337; random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:21.810245Z","iopub.execute_input":"2025-10-30T21:35:21.810656Z","iopub.status.idle":"2025-10-30T21:35:21.886403Z","shell.execute_reply.started":"2025-10-30T21:35:21.810634Z","shell.execute_reply":"2025-10-30T21:35:21.885021Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"trn = pd.read_csv(TRAIN_CSV)\ntst = pd.read_csv(TEST_CSV)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:21.887538Z","iopub.execute_input":"2025-10-30T21:35:21.888404Z","iopub.status.idle":"2025-10-30T21:35:21.916682Z","shell.execute_reply.started":"2025-10-30T21:35:21.888365Z","shell.execute_reply":"2025-10-30T21:35:21.915871Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pivot long->wide targets\nwide = trn.pivot_table(index=[\"sample_id\",\"image_path\",\"Sampling_Date\",\"State\",\"Species\",\"Pre_GSHH_NDVI\",\"Height_Ave_cm\"],\n                       columns=\"target_name\", values=\"target\").reset_index()\nassert set(TARGETS).issubset(set(wide.columns))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:21.918461Z","iopub.execute_input":"2025-10-30T21:35:21.918732Z","iopub.status.idle":"2025-10-30T21:35:21.966392Z","shell.execute_reply.started":"2025-10-30T21:35:21.918713Z","shell.execute_reply":"2025-10-30T21:35:21.965559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Basic feature cleanup\nwide[\"Sampling_Date\"] = pd.to_datetime(wide[\"Sampling_Date\"])\nwide[\"month\"] = wide[\"Sampling_Date\"].dt.month\nwide[\"year\"]  = wide[\"Sampling_Date\"].dt.year\n# cheap season\nwide[\"season\"] = ((wide[\"month\"]%12)//3).astype(int)  # 0..3","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:21.967244Z","iopub.execute_input":"2025-10-30T21:35:21.967531Z","iopub.status.idle":"2025-10-30T21:35:21.979382Z","shell.execute_reply.started":"2025-10-30T21:35:21.967506Z","shell.execute_reply":"2025-10-30T21:35:21.978609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tokenize species (take first 3 tokens to limit sparsity)\ndef species_tokens(s):\n    toks = (s or \"\").split(\"_\")\n    return toks[:3] if toks else [\"UNK\"]\nwide[\"Species_tokens\"] = wide[\"Species\"].fillna(\"UNK\").apply(species_tokens)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:21.980377Z","iopub.execute_input":"2025-10-30T21:35:21.980668Z","iopub.status.idle":"2025-10-30T21:35:21.991775Z","shell.execute_reply.started":"2025-10-30T21:35:21.980643Z","shell.execute_reply":"2025-10-30T21:35:21.990797Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tabular columns\nnum_cols = [\"Pre_GSHH_NDVI\",\"Height_Ave_cm\",\"month\"]\ncat_cols = [\"State\",\"season\"]  # small cats; species as limited tokens below","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:21.992590Z","iopub.execute_input":"2025-10-30T21:35:21.992802Z","iopub.status.idle":"2025-10-30T21:35:22.003203Z","shell.execute_reply.started":"2025-10-30T21:35:21.992778Z","shell.execute_reply":"2025-10-30T21:35:22.002455Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# One-hot for small cats\nohe = OneHotEncoder(sparse_output=False, handle_unknown=\"ignore\")\nohe_mat = ohe.fit_transform(wide[cat_cols].fillna(\"UNK\"))\nohe_cols = ohe.get_feature_names_out(cat_cols)\nohe_df = pd.DataFrame(ohe_mat, columns=ohe_cols, index=wide.index)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.004027Z","iopub.execute_input":"2025-10-30T21:35:22.004288Z","iopub.status.idle":"2025-10-30T21:35:22.021522Z","shell.execute_reply.started":"2025-10-30T21:35:22.004263Z","shell.execute_reply":"2025-10-30T21:35:22.020797Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Species token bag-of-words (naive)\nsp_vocab = sorted({tok for toks in wide[\"Species_tokens\"] for tok in toks})\nfor tok in sp_vocab:\n    wide[f\"sp_{tok}\"] = wide[\"Species_tokens\"].apply(lambda xs: float(tok in xs))\n\ntab_df = pd.concat([wide[num_cols], ohe_df, wide[[c for c in wide.columns if c.startswith(\"sp_\")]]], axis=1)\ntab_cols = tab_df.columns.tolist()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.022563Z","iopub.execute_input":"2025-10-30T21:35:22.022785Z","iopub.status.idle":"2025-10-30T21:35:22.045914Z","shell.execute_reply.started":"2025-10-30T21:35:22.022763Z","shell.execute_reply":"2025-10-30T21:35:22.044973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGETS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n# Drop rows with all NaNs in target columns\nwide = wide.dropna(subset=TARGETS, how='all').reset_index(drop=True)\n\n# Replace remaining NaNs (partial missing components) with 0 or mean if desired\nwide[TARGETS] = wide[TARGETS].fillna(0)\n\n# Now take log1p transform\ny = np.log1p(wide[TARGETS].values.astype(\"float32\"))\n\n# Recreate bins for stratification\nbins = pd.qcut(wide[\"Dry_Total_g\"], q=10, duplicates=\"drop\").cat.codes.values\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.049104Z","iopub.execute_input":"2025-10-30T21:35:22.049439Z","iopub.status.idle":"2025-10-30T21:35:22.075005Z","shell.execute_reply.started":"2025-10-30T21:35:22.049420Z","shell.execute_reply":"2025-10-30T21:35:22.074074Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.075716Z","iopub.execute_input":"2025-10-30T21:35:22.075973Z","iopub.status.idle":"2025-10-30T21:35:22.081805Z","shell.execute_reply.started":"2025-10-30T21:35:22.075957Z","shell.execute_reply":"2025-10-30T21:35:22.081027Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Map image paths\ndef img_path(rel):\n    # train images are under train/, test under test/ at scoring time\n    return IMG_ROOT/rel","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.082735Z","iopub.execute_input":"2025-10-30T21:35:22.083088Z","iopub.status.idle":"2025-10-30T21:35:22.092497Z","shell.execute_reply.started":"2025-10-30T21:35:22.083043Z","shell.execute_reply":"2025-10-30T21:35:22.091654Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 384\ntrain_tfms = transforms.Compose([\n    transforms.RandomResizedCrop(IMG_SIZE, scale=(0.8,1.0)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.ColorJitter(0.2,0.2,0.2,0.05),\n    transforms.ToTensor(),\n])\nvalid_tfms = transforms.Compose([\n    transforms.Resize(int(IMG_SIZE*1.14)),\n    transforms.CenterCrop(IMG_SIZE),\n    transforms.ToTensor(),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.093493Z","iopub.execute_input":"2025-10-30T21:35:22.093840Z","iopub.status.idle":"2025-10-30T21:35:22.105805Z","shell.execute_reply.started":"2025-10-30T21:35:22.093793Z","shell.execute_reply":"2025-10-30T21:35:22.104964Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class PastureDataset(Dataset):\n    def __init__(self, df_idx, is_train):\n        self.df = wide.iloc[df_idx].reset_index(drop=True)\n        self.tab = tab_df.iloc[df_idx].reset_index(drop=True).values.astype(\"float32\")\n        self.targets = y[df_idx]\n        self.is_train = is_train\n        self.tfms = train_tfms if is_train else valid_tfms\n    def __len__(self): return len(self.df)\n    def __getitem__(self, i):\n        row = self.df.loc[i]\n        img = Image.open(img_path(row[\"image_path\"])).convert(\"RGB\")\n        img = self.tfms(img)\n        tab = torch.from_numpy(self.tab[i])\n        if self.is_train:\n            tgt = torch.from_numpy(self.targets[i])  # 5-dim\n            return img, tab, tgt\n        else:\n            return img, tab","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.106724Z","iopub.execute_input":"2025-10-30T21:35:22.107470Z","iopub.status.idle":"2025-10-30T21:35:22.120132Z","shell.execute_reply.started":"2025-10-30T21:35:22.107444Z","shell.execute_reply":"2025-10-30T21:35:22.119358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"USE_PRETRAINED = True  # set False if weights unavailable offline\ncnn = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT if USE_PRETRAINED else None)\nin_feats = cnn.classifier[1].in_features\ncnn.classifier = nn.Identity()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.120919Z","iopub.execute_input":"2025-10-30T21:35:22.121271Z","iopub.status.idle":"2025-10-30T21:35:22.490483Z","shell.execute_reply.started":"2025-10-30T21:35:22.121248Z","shell.execute_reply":"2025-10-30T21:35:22.489521Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tab_in = len(tab_cols)\nclass FusionModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.cnn = cnn\n        self.tab = nn.Sequential(\n            nn.Linear(tab_in, 128), nn.ReLU(inplace=True),\n            nn.Linear(128, 128), nn.ReLU(inplace=True)\n        )\n        self.head = nn.Sequential(\n            nn.Linear(in_feats + 128, 256), nn.ReLU(inplace=True),\n            nn.Dropout(0.2),\n            nn.Linear(256, 5)  # 5 targets (log space)\n        )\n    def forward(self, x_img, x_tab):\n        f_img = self.cnn(x_img)\n        f_tab = self.tab(x_tab)\n        f = torch.cat([f_img, f_tab], dim=1)\n        return self.head(f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.491548Z","iopub.execute_input":"2025-10-30T21:35:22.491892Z","iopub.status.idle":"2025-10-30T21:35:22.499304Z","shell.execute_reply.started":"2025-10-30T21:35:22.491862Z","shell.execute_reply":"2025-10-30T21:35:22.498607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def weighted_r2(y_true, y_pred):\n    # y_* are in real (not log) space here\n    scores = {}\n    total = 0.0\n    for j,t in enumerate(TARGETS):\n        r2 = r2_score(y_true[:,j], y_pred[:,j])\n        scores[t] = r2\n        total += WEIGHTS[t]*r2\n    return total, scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.499702Z","iopub.execute_input":"2025-10-30T21:35:22.499882Z","iopub.status.idle":"2025-10-30T21:35:22.513023Z","shell.execute_reply.started":"2025-10-30T21:35:22.499867Z","shell.execute_reply":"2025-10-30T21:35:22.512372Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_fold(tr_idx, va_idx, epochs=6, bs=16, lr=2e-4):\n    tr_ds = PastureDataset(tr_idx, True)\n    va_ds = PastureDataset(va_idx, False)\n    tr_ld = DataLoader(tr_ds, batch_size=bs, shuffle=True, num_workers=2, pin_memory=True)\n    va_ld = DataLoader(va_ds, batch_size=bs*2, shuffle=False, num_workers=2, pin_memory=True)\n\n    model = FusionModel().to(device)\n    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)\n    best = ( -1e9, None )\n\n    for ep in range(1, epochs+1):\n        model.train(); tr_loss=0.0\n        for img, tab, tgt in tr_ld:\n            img, tab, tgt = img.to(device), tab.to(device), tgt.to(device)\n            opt.zero_grad()\n            out = model(img, tab)\n            loss = F.mse_loss(out, tgt)  # log-space MSE\n            loss.backward(); opt.step()\n            tr_loss += loss.item()*len(img)\n\n        # validate\n        model.eval(); preds=[]; gts=[]\n        with torch.no_grad():\n            for img, tab in va_ld:\n                img, tab = img.to(device), tab.to(device)\n                o = model(img, tab)              # log preds\n                preds.append(o.cpu().numpy())\n            preds = np.vstack(preds)\n        # back-transform to grams\n        pred_real = np.expm1(preds)\n        gt_real   = np.expm1(y[va_idx])\n        wscore, per = weighted_r2(gt_real, pred_real)\n\n        if wscore > best[0]:\n            best = (wscore, { \"state_dict\": model.state_dict(), \"per\": per, \"ep\": ep })\n    return best, pred_real","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.513664Z","iopub.execute_input":"2025-10-30T21:35:22.513916Z","iopub.status.idle":"2025-10-30T21:35:22.531224Z","shell.execute_reply.started":"2025-10-30T21:35:22.513893Z","shell.execute_reply":"2025-10-30T21:35:22.530497Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\nfold_preds = np.zeros_like(y)\nfold_scores = []\n\nfor fold,(tr_idx,va_idx) in enumerate(skf.split(wide, bins), 1):\n    best, va_pred = train_one_fold(tr_idx, va_idx, epochs=6, bs=16, lr=2e-4)\n    fold_preds[va_idx] = va_pred\n    fold_scores.append(best[0])\n    print(f\"Fold {fold} weighted R2: {best[0]:.4f}  per-target: {best[1]['per']}\")\n    gc.collect(); torch.cuda.empty_cache()\n\ncv_score, _ = weighted_r2(np.expm1(y), fold_preds)\nprint(\"CV weighted R2:\", cv_score)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T21:35:22.532112Z","iopub.execute_input":"2025-10-30T21:35:22.532298Z","iopub.status.idle":"2025-10-30T22:08:35.760636Z","shell.execute_reply.started":"2025-10-30T21:35:22.532284Z","shell.execute_reply":"2025-10-30T22:08:35.759688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"full_idx = np.arange(len(wide))\nfull_ds  = PastureDataset(full_idx, True)\nfull_ld  = DataLoader(full_ds, batch_size=16, shuffle=True, num_workers=2, pin_memory=True)\n\nmodel = FusionModel().to(device)\nopt = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-4)\nfor ep in range(6):\n    model.train()\n    for img, tab, tgt in full_ld:\n        img, tab, tgt = img.to(device), tab.to(device), tgt.to(device)\n        opt.zero_grad()\n        loss = F.mse_loss(model(img, tab), tgt)\n        loss.backward(); opt.step()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T22:08:35.761804Z","iopub.execute_input":"2025-10-30T22:08:35.762252Z","iopub.status.idle":"2025-10-30T22:15:11.841139Z","shell.execute_reply.started":"2025-10-30T22:08:35.762224Z","shell.execute_reply":"2025-10-30T22:15:11.840115Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build test dataset\n# test.csv is long (one row per (image, target_name)); we need one image per unique path\ntst_long = pd.read_csv(TEST_CSV)\nuniq = tst_long[\"image_path\"].drop_duplicates().reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T22:15:11.842475Z","iopub.execute_input":"2025-10-30T22:15:11.842735Z","iopub.status.idle":"2025-10-30T22:15:11.852209Z","shell.execute_reply.started":"2025-10-30T22:15:11.842713Z","shell.execute_reply":"2025-10-30T22:15:11.851577Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# prepare tabular feats for test: mimic train pipeline\ndef lookup_row(rel_path):\n    # At test, we only know image_path and need tabular features from train? => We can use the columns available in test.csv only (often none).\n    # This baseline assumes *no extra tabular* in test, so we zero them. If test has NDVI/height later, replace here accordingly.\n    return np.zeros((len(tab_cols),), dtype=\"float32\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T22:15:11.853042Z","iopub.execute_input":"2025-10-30T22:15:11.853323Z","iopub.status.idle":"2025-10-30T22:15:11.865932Z","shell.execute_reply.started":"2025-10-30T22:15:11.853297Z","shell.execute_reply":"2025-10-30T22:15:11.865146Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, image_paths, is_train=False):\n        self.paths = image_paths\n        self.tfms  = valid_tfms\n    def __len__(self): return len(self.paths)\n    def __getitem__(self, i):\n        p = self.paths[i]\n        img = Image.open(img_path(p)).convert(\"RGB\")\n        img = self.tfms(img)\n        tab = torch.zeros(len(tab_cols), dtype=torch.float32)\n        return img, tab\n\ntds = TestDataset(uniq.values.tolist())\ntld = DataLoader(tds, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T22:15:11.866648Z","iopub.execute_input":"2025-10-30T22:15:11.866807Z","iopub.status.idle":"2025-10-30T22:15:11.874997Z","shell.execute_reply.started":"2025-10-30T22:15:11.866794Z","shell.execute_reply":"2025-10-30T22:15:11.874359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval(); test_pred = []\nwith torch.no_grad():\n    for img, tab in tld:\n        o = model(img.to(device), tab.to(device))        # log space\n        test_pred.append(o.cpu().numpy())\ntest_pred = np.vstack(test_pred)                         # (N_images, 5)\ntest_pred = np.expm1(test_pred)                          # grams","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T22:15:11.875699Z","iopub.execute_input":"2025-10-30T22:15:11.875917Z","iopub.status.idle":"2025-10-30T22:15:12.156684Z","shell.execute_reply.started":"2025-10-30T22:15:11.875900Z","shell.execute_reply":"2025-10-30T22:15:12.155922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build a mapping from image_path -> predicted dict\npred_map = {p:{t:v for t,v in zip(TARGETS, row)} for p,row in zip(uniq, test_pred)}\n\nrows = []\nfor _,r in tst_long.iterrows():\n    rows.append([r[\"sample_id\"], pred_map[r[\"image_path\"]][r[\"target_name\"]]])\nsub = pd.DataFrame(rows, columns=[\"sample_id\",\"target\"])\n\nsub.to_csv(\"submission.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T22:16:50.567233Z","iopub.execute_input":"2025-10-30T22:16:50.568077Z","iopub.status.idle":"2025-10-30T22:16:50.582647Z","shell.execute_reply.started":"2025-10-30T22:16:50.568039Z","shell.execute_reply":"2025-10-30T22:16:50.581937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}