{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":289376056,"sourceType":"kernelVersion"},{"sourceId":289403090,"sourceType":"kernelVersion"},{"sourceId":289482646,"sourceType":"kernelVersion"},{"sourceId":289551848,"sourceType":"kernelVersion"}],"dockerImageVersionId":31234,"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)\nfrom scipy.stats import chi2_contingency\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\npd.set_option('display.max_columns', None)\n\ndirectory = '/kaggle/input/csiro-biomass'\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\n# import os\n# for 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-01T19:15:22.694648Z","iopub.execute_input":"2026-01-01T19:15:22.694896Z","iopub.status.idle":"2026-01-01T19:15:26.042878Z","shell.execute_reply.started":"2026-01-01T19:15:22.694870Z","shell.execute_reply":"2026-01-01T19:15:26.041712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test = pd.read_csv('/kaggle/input/csiro-biomass/test.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:15:26.045408Z","iopub.execute_input":"2026-01-01T19:15:26.045908Z","iopub.status.idle":"2026-01-01T19:15:26.077543Z","shell.execute_reply.started":"2026-01-01T19:15:26.045867Z","shell.execute_reply":"2026-01-01T19:15:26.076714Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport pandas as pd\n\nfrom skimage.feature import graycomatrix, graycoprops, local_binary_pattern\nfrom scipy.stats import skew, kurtosis, entropy\nfrom scipy.fft import fft2\n\nimport torch\nimport torchvision.models as models\nimport torchvision.transforms as T\n\nfrom lightgbm import LGBMRegressor\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import mean_squared_error\n\n\ndef load_image(path):\n    img = cv2.imread(path)\n    if img is None:\n        raise ValueError(f\"Cannot read image: {path}\")\n    return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n    \ndef rgb_stats(img):\n    feats = {}\n    for i, c in enumerate([\"R\",\"G\",\"B\"]):\n        ch = img[:,:,i].astype(np.float32)\n        feats[f\"mean_{c}\"] = ch.mean()\n        feats[f\"std_{c}\"] = ch.std()\n        feats[f\"skew_{c}\"] = skew(ch.flatten())\n        feats[f\"kurt_{c}\"] = kurtosis(ch.flatten())\n    return feats\n\n\ndef color_space_features(img):\n    feats = {}\n    hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)\n    lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n    for name, ch in zip([\"H\",\"S\",\"V\"], cv2.split(hsv)):\n        feats[f\"mean_{name}\"] = ch.mean()\n        feats[f\"std_{name}\"] = ch.std()\n        feats[f\"entropy_{name}\"] = entropy(np.histogram(ch,256)[0] + 1)\n    for name, ch in zip([\"L\",\"a\",\"b\"], cv2.split(lab)):\n        feats[f\"mean_{name}\"] = ch.mean()\n        feats[f\"std_{name}\"] = ch.std()\n    return feats\n\n\ndef vegetation_indices(img):\n    R, G, B = img[:,:,0].astype(float), img[:,:,1].astype(float), img[:,:,2].astype(float)\n    eps = 1e-6\n    ExG   = 2*G - R - B\n    ExR   = 1.4*R - G\n    VARI  = (G - R) / (G + R - B + eps)\n    NGRDI = (G - R) / (G + R + eps)\n    feats = {}\n    for name, arr in zip(\n        [\"ExG\",\"ExR\",\"VARI\",\"NGRDI\"],\n        [ExG, ExR, VARI, NGRDI]\n    ):\n        feats[f\"{name}_mean\"] = arr.mean()\n        feats[f\"{name}_std\"]  = arr.std()\n        feats[f\"{name}_p75\"]  = np.percentile(arr, 75)\n    return feats\n\n\ndef pixel_fractions(img):\n    hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)\n    H, S, V = cv2.split(hsv)\n    green = ((H > 35) & (H < 85) & (S > 40))\n    brown = ((H > 10) & (H < 35) & (S > 30))\n    total = img.shape[0] * img.shape[1]\n    return {\n        \"green_pixel_fraction\": green.sum() / total,\n        \"brown_pixel_fraction\": brown.sum() / total\n    }\n\n\ndef glcm_features(img):\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    gray = cv2.resize(gray, (256,256))\n    glcm = graycomatrix(\n        gray,\n        distances=[1,3],\n        angles=[0, np.pi/4, np.pi/2],\n        levels=256,\n        symmetric=True,\n        normed=True\n    )\n    feats = {}\n    for prop in [\"contrast\",\"homogeneity\",\"energy\",\"correlation\"]:\n        feats[f\"glcm_{prop}\"] = graycoprops(glcm, prop).mean()\n    return feats\n \n\ndef lbp_features(img):\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    lbp = local_binary_pattern(gray, P=8, R=1, method=\"uniform\")\n    hist, _ = np.histogram(lbp, bins=10)\n    hist = hist / (hist.sum() + 1e-6)\n    return {\n        \"lbp_entropy\": entropy(hist),\n        \"lbp_variance\": np.var(hist)\n    }\n\n\ndef edge_features(img):\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    edges = cv2.Canny(gray, 50, 150)\n    return {\n        \"edge_density\": edges.mean(),\n        \"edge_pixel_count\": edges.sum()\n    }\n\n\ndef shape_features(img):\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    contours, _ = cv2.findContours(bw, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    areas = [cv2.contourArea(c) for c in contours if cv2.contourArea(c) > 20]\n    if len(areas) == 0:\n        return {\"num_components\": 0, \"mean_component_area\": 0}\n    return {\n        \"num_components\": len(areas),\n        \"mean_component_area\": np.mean(areas),\n        \"max_component_area\": np.max(areas)\n    }\n\n\ndef frequency_features(img):\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    f = np.abs(fft2(gray))\n    return {\n        \"fft_mean\": f.mean(),\n        \"fft_std\": f.std(),\n        \"fft_entropy\": entropy(np.histogram(f.flatten(),256)[0] + 1)\n    }\n\n\n_resnet = None\ndef cnn_features(img):\n    global _resnet\n    if _resnet is None:\n        model = models.resnet18(pretrained=True)\n        model.fc = torch.nn.Identity()\n        model.eval()\n        _resnet = model\n    transform = T.Compose([\n        T.ToPILImage(),\n        T.Resize((224,224)),\n        T.ToTensor(),\n        T.Normalize(\n            mean=[0.485,0.456,0.406],\n            std=[0.229,0.224,0.225]\n        )\n    ])\n    with torch.no_grad():\n        x = transform(img).unsqueeze(0)\n        feats = _resnet(x).numpy().flatten()\n    return {f\"cnn_{i}\": v for i, v in enumerate(feats)}\n\ndef extract_all_features(image_path, use_cnn=True):\n    img = load_image(image_path)\n    feats = {}\n    feats.update(rgb_stats(img))\n    feats.update(color_space_features(img))\n    feats.update(vegetation_indices(img))\n    feats.update(pixel_fractions(img))\n    feats.update(glcm_features(img))\n    feats.update(lbp_features(img))\n    feats.update(edge_features(img))\n    feats.update(shape_features(img))\n    feats.update(frequency_features(img))\n    if use_cnn:\n        feats.update(cnn_features(img))\n\n    return pd.DataFrame([feats])\n\n\nimport numpy as np\nimport pandas as pd\nfrom heapq import heappush, heappushpop\nfrom tqdm import tqdm\nfrom scipy.cluster.hierarchy import linkage, fcluster\nfrom scipy.spatial.distance import squareform\n\n\ndef generate_and_score_features(\n    df,\n    base_features,\n    target_col,\n    eps=1e-6\n):\n    \"\"\"\n    Returns:\n        feature_scores_df : feature, corr, abs_corr\n        df_all_features   : df with all generated features\n    \"\"\"\n    y = df[target_col].values\n    df_out = df.copy()\n    scores = []\n    def score_feature(name, x):\n        if np.isnan(x).all():\n            return\n        corr = np.corrcoef(x, y)[0, 1]\n        if np.isnan(corr):\n            return\n        scores.append((name, corr, abs(corr)))\n    # ------------------------\n    # 1. Single features\n    # ------------------------\n    for f in tqdm(base_features, desc=\"Scoring base features\"):\n        score_feature(f, df_out[f].values)\n    # ------------------------\n    # 2. Pairwise crosses\n    # ------------------------\n    n = len(base_features)\n    new_cols = {}\n    for i in tqdm(range(n), desc=\"Generating crossed features\"):\n        f1 = base_features[i]\n        x1 = df_out[f1].values\n        for j in range(i + 1, n):\n            f2 = base_features[j]\n            x2 = df_out[f2].values\n            # +\n            name = f\"{f1}_plus_{f2}\"\n            new_cols[name] = x1 + x2\n            score_feature(name, new_cols[name])\n            # *\n            name = f\"{f1}_mul_{f2}\"\n            new_cols[name] = x1 * x2\n            score_feature(name, new_cols[name])\n            # /\n            name = f\"{f1}_div_{f2}\"\n            new_cols[name] = x1 / (x2 + eps)\n            score_feature(name, new_cols[name])\n    df_out = pd.concat([df_out, pd.DataFrame(new_cols)], axis=1)\n    feature_scores_df = (\n        pd.DataFrame(scores, columns=[\"feature\", \"corr\", \"abs_corr\"])\n        .sort_values(\"abs_corr\", ascending=False)\n        .reset_index(drop=True)\n    )\n    return feature_scores_df, df_out\n\n\ndef cluster_features_by_correlation(df, features, K):\n    # Compute absolute correlation\n    corr = df[features].corr().abs()\n    # Replace NaNs (from constant columns)\n    corr = corr.fillna(0.0)\n    # Numerical safety: clip to [0, 1]\n    corr = corr.clip(0.0, 1.0)\n    # Distance matrix\n    dist = 1.0 - corr\n    # Ensure exact zeros on diagonal\n    np.fill_diagonal(dist.values, 0.0)\n    # Convert to condensed distance\n    dist_condensed = squareform(dist.values, checks=False)\n    # Hierarchical clustering\n    Z = linkage(dist_condensed, method=\"average\")\n    clusters = fcluster(Z, K, criterion=\"maxclust\")\n    return pd.DataFrame({\n        \"feature\": features,\n        \"cluster\": clusters\n    })\n\n\ndef build_top_k_diverse_features(\n    df,\n    base_features,\n    target_col,\n    K\n):\n    fundamental_features = [col for col in df.columns if col not in base_features]\n    \n    # 1. Generate & score all features\n    feature_scores_df, df_all = generate_and_score_features(\n        df, base_features, target_col\n    )\n    all_features = feature_scores_df[\"feature\"].tolist()\n    # 2. Cluster features\n    cluster_df = cluster_features_by_correlation(\n        df_all, all_features, K\n    )\n    # 3. Select representatives\n    selected_df = select_cluster_representatives(\n        feature_scores_df, cluster_df\n    )\n    selected_features = selected_df[\"feature\"].tolist()\n    # 4. Final dataframe\n    final_df = df_all[fundamental_features + selected_features].copy()\n    print(final_df.shape)\n    return {\n        \"final_df\": final_df,\n        \"selected_features\": selected_features,\n        \"feature_cluster_table\": selected_df\n    }\n\n\ndef generate_selected_features_only(\n    df,\n    selected_features,\n    directory,\n    use_cnn=False,\n    eps=1e-6\n):\n    \"\"\"\n    Generate only the features required by the trained model.\n    Works even when 'target' is NOT present.\n    \"\"\"\n\n    df = df.reset_index(drop=True)\n\n    # --- extract base image features ---\n    img_df = df[['image_path']].drop_duplicates().reset_index(drop=True)\n    tqdm.pandas()\n    feats = img_df['image_path'].progress_apply(\n        lambda x: extract_all_features(f\"{directory}/{x}\", use_cnn=use_cnn)\n    )\n    feats = pd.concat(feats.tolist(), ignore_index=True)\n    img_df = pd.concat([img_df, feats], axis=1)\n\n    df = df.merge(img_df, on='image_path', how='left')\n    df = drop_duplicate_columns_strict(df)\n\n    # --- materialize ONLY required features ---\n    new_cols = {}\n\n    for feat in selected_features:\n        if feat in df.columns:\n            continue\n\n        if \"_plus_\" in feat:\n            f1, f2 = feat.split(\"_plus_\")\n            new_cols[feat] = df[f1] + df[f2]\n\n        elif \"_mul_\" in feat:\n            f1, f2 = feat.split(\"_mul_\")\n            new_cols[feat] = df[f1] * df[f2]\n\n        elif \"_div_\" in feat:\n            f1, f2 = feat.split(\"_div_\")\n            new_cols[feat] = df[f1] / (df[f2] + eps)\n\n    if new_cols:\n        df = pd.concat([df, pd.DataFrame(new_cols)], axis=1)\n\n    return df\n\n\nfrom tqdm import tqdm\nimport pandas as pd\n\ndef add_features(df, top_k=100):\n    df = df.reset_index(drop=True)\n\n    # Extract image features\n    img_df = df[['image_path']].drop_duplicates().reset_index(drop=True)\n    tqdm.pandas()\n    feats = img_df['image_path'].progress_apply(\n        lambda x: extract_all_features(f\"{directory}/{x}\", use_cnn=False)\n    )\n    feats = pd.concat(feats.tolist(), ignore_index=True)\n    img_df = pd.concat([img_df, feats], axis=1)\n\n    df = df.merge(img_df, on='image_path', how='left')\n    df = drop_duplicate_columns_strict(df)\n\n    float_cols = df.select_dtypes(include=[\"float32\", \"float64\"]).columns\n    exclude_cols = [\"target\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\n    base_features = canonicalize_features(\n        [c for c in float_cols if c not in exclude_cols]\n    )\n\n    # 🔥 clustering-based selection\n    result = build_top_k_diverse_features(\n        df=df,\n        base_features=base_features,\n        target_col=\"target\",\n        K=top_k\n    )\n\n    final_df          = result[\"final_df\"]\n    selected_features = result[\"selected_features\"]\n    cluster_table     = result[\"feature_cluster_table\"]\n\n    return final_df, selected_features, cluster_table, base_features\n\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import mean_squared_error\nfrom lightgbm import LGBMRegressor\n\ndef train_test_mse(df_sub, feature_cols):\n    X = df_sub[feature_cols]\n    y = df_sub[\"target\"]\n    X_train, X_test, y_train, y_test = train_test_split(\n        X, y, test_size=0.2, random_state=42\n    )\n    model = LGBMRegressor(\n        n_estimators=1000,\n        learning_rate=0.005,\n        max_depth=10,\n        subsample=0.8,\n        colsample_bytree=0.8,\n        random_state=42\n    )\n    model.fit(X_train, y_train)\n    y_pred = model.predict(X_test)\n    mse = mean_squared_error(y_test, y_pred)\n    return mse, model\n\n\ndef train_model(df):\n    df_feat, feature_cols, cluster_table, base_features = add_features(df)\n\n    feature_cols = list(dict.fromkeys(feature_cols))  # safety\n\n    trained_models = {}\n    results = {}\n\n    for tgt in df_feat['target_name'].unique():\n        df_tgt = df_feat[df_feat[\"target_name\"] == tgt]\n        mse, model = train_test_mse(df_tgt, feature_cols)\n        trained_models[tgt] = model\n        results[tgt] = mse\n        print(f\"{tgt} | Test MSE = {mse:.6f}\")\n\n    return trained_models, results, feature_cols, cluster_table, base_features\n\n\ndef predict_biomass(df, trained_models, feature_cols, base_features):\n    df_feat, _, _, _ = add_features(\n        df,\n        top_k=0   # no re-clustering\n    )\n\n    df_feat[\"prediction\"] = np.nan\n\n    for tgt, model in trained_models.items():\n        mask = df_feat[\"target_name\"] == tgt\n        if mask.sum() == 0:\n            continue\n        X = df_feat.loc[mask, feature_cols]\n        df_feat.loc[mask, \"prediction\"] = model.predict(X)\n\n    return df_feat\n\ndef predict_biomass_unseen(\n    df_unseen,\n    trained_models,\n    selected_features,\n    directory\n):\n    \"\"\"\n    Predict biomass on unseen data where target is NOT present.\n    \"\"\"\n\n    df_feat = generate_selected_features_only(\n        df=df_unseen,\n        selected_features=selected_features,\n        directory=directory\n    )\n\n    df_feat[\"prediction\"] = np.nan\n\n    for tgt, model in trained_models.items():\n        mask = df_feat[\"target_name\"] == tgt\n        if mask.sum() == 0:\n            continue\n\n        X = df_feat.loc[mask, selected_features]\n        df_feat.loc[mask, \"prediction\"] = model.predict(X)\n\n    return df_feat\n\n\n\ndef canonicalize_features(base_features):\n    \"\"\"\n    Remove _x/_y duplicates by keeping only one version.\n    \"\"\"\n    seen = {}\n    clean = []\n    for f in base_features:\n        root = f.replace(\"_x\", \"\").replace(\"_y\", \"\")\n        if root not in seen:\n            seen[root] = f\n            clean.append(f)\n    return clean\n\ndef drop_duplicate_columns_strict(df):\n    \"\"\"\n    Remove duplicate column NAMES (not just _x/_y).\n    Keeps first occurrence.\n    \"\"\"\n    return df.loc[:, ~df.columns.duplicated()]\n\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:15:26.078979Z","iopub.execute_input":"2026-01-01T19:15:26.079235Z","iopub.status.idle":"2026-01-01T19:15:43.864918Z","shell.execute_reply.started":"2026-01-01T19:15:26.079212Z","shell.execute_reply":"2026-01-01T19:15:43.863737Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# from tqdm import tqdm\n# import pandas as pd\n\n# def add_features(df, top_k=100):\n#     df = df.reset_index(drop=True)\n\n#     # Extract image features\n#     img_df = df[['image_path']].drop_duplicates().reset_index(drop=True)\n#     tqdm.pandas()\n#     feats = img_df['image_path'].progress_apply(\n#         lambda x: extract_all_features(f\"{directory}/{x}\", use_cnn=False)\n#     )\n#     feats = pd.concat(feats.tolist(), ignore_index=True)\n#     img_df = pd.concat([img_df, feats], axis=1)\n\n#     df = df.merge(img_df, on='image_path', how='left')\n#     df = drop_duplicate_columns_strict(df)\n\n#     float_cols = df.select_dtypes(include=[\"float32\", \"float64\"]).columns\n#     exclude_cols = [\"target\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\n#     base_features = canonicalize_features(\n#         [c for c in float_cols if c not in exclude_cols]\n#     )\n\n#     # 🔥 clustering-based selection\n#     result = build_top_k_diverse_features(\n#         df=df,\n#         base_features=base_features,\n#         target_col=\"target\",\n#         K=top_k\n#     )\n\n#     final_df          = result[\"final_df\"]\n#     selected_features = result[\"selected_features\"]\n#     cluster_table     = result[\"feature_cluster_table\"]\n\n#     return final_df, selected_features, cluster_table, base_features\n\n\n# from catboost import CatBoostRegressor\n# from sklearn.model_selection import KFold\n# from sklearn.metrics import r2_score\n# import numpy as np\n\n# def cv_catboost(df_tgt, feature_cols, n_splits=5):\n#     X = df_tgt[feature_cols].values\n#     y = df_tgt[\"target\"].values\n\n#     kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)\n#     r2_scores = []\n\n#     for tr_idx, val_idx in kf.split(X):\n#         model = CatBoostRegressor(\n#             iterations=3000,\n#             learning_rate=0.02,\n#             depth=8,\n#             l2_leaf_reg=5.0,\n#             subsample=0.8,\n#             loss_function=\"RMSE\",\n#             eval_metric=\"R2\",\n#             random_seed=42,\n#             verbose=False,\n#             allow_writing_files=False\n#         )\n\n#         model.fit(\n#             X[tr_idx], y[tr_idx],\n#             eval_set=(X[val_idx], y[val_idx]),\n#             early_stopping_rounds=200,\n#             use_best_model=True\n#         )\n\n#         preds = model.predict(X[val_idx])\n#         r2_scores.append(r2_score(y[val_idx], preds))\n\n#     return np.mean(r2_scores), np.std(r2_scores)\n\n\n# from sklearn.ensemble import ExtraTreesRegressor\n\n# def cv_extratrees(df_tgt, feature_cols, n_splits=5):\n#     X = df_tgt[feature_cols].values\n#     y = df_tgt[\"target\"].values\n\n#     kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)\n#     r2_scores = []\n\n#     for tr_idx, val_idx in kf.split(X):\n#         model = ExtraTreesRegressor(\n#             n_estimators=1200,\n#             min_samples_leaf=2,\n#             max_features=\"sqrt\",\n#             n_jobs=-1,\n#             random_state=42\n#         )\n\n#         model.fit(X[tr_idx], y[tr_idx])\n#         preds = model.predict(X[val_idx])\n#         r2_scores.append(r2_score(y[val_idx], preds))\n\n#     return np.mean(r2_scores), np.std(r2_scores)\n\n\n# def train_final_models(df_feat, feature_cols):\n#     models = {}\n#     cv_results = {}\n\n#     for tgt in df_feat[\"target_name\"].unique():\n#         print(f\"\\n🔹 Target: {tgt}\")\n#         df_tgt = df_feat[df_feat[\"target_name\"] == tgt]\n\n#         cat_mean, cat_std = cv_catboost(df_tgt, feature_cols)\n#         et_mean, et_std   = cv_extratrees(df_tgt, feature_cols)\n\n#         print(f\"CatBoost CV R²: {cat_mean:.4f} ± {cat_std:.4f}\")\n#         print(f\"ExtraTrees CV R²: {et_mean:.4f} ± {et_std:.4f}\")\n\n#         # Train final models on full data\n#         X = df_tgt[feature_cols]\n#         y = df_tgt[\"target\"]\n\n#         cat_final = CatBoostRegressor(\n#             iterations=3000,\n#             learning_rate=0.02,\n#             depth=8,\n#             l2_leaf_reg=5.0,\n#             subsample=0.8,\n#             loss_function=\"RMSE\",\n#             random_seed=42,\n#             verbose=False,\n#             allow_writing_files=False\n#         )\n#         cat_final.fit(X, y)\n\n#         et_final = ExtraTreesRegressor(\n#             n_estimators=1200,\n#             min_samples_leaf=2,\n#             max_features=\"sqrt\",\n#             n_jobs=-1,\n#             random_state=42\n#         )\n#         et_final.fit(X, y)\n\n#         # Ensemble weights (CV-based)\n#         w_cat = cat_mean / (cat_mean + et_mean)\n#         w_et  = 1.0 - w_cat\n\n#         models[tgt] = {\n#             \"cat\": cat_final,\n#             \"et\": et_final,\n#             \"weights\": (w_cat, w_et)\n#         }\n\n#         cv_results[tgt] = {\n#             \"cat_r2\": cat_mean,\n#             \"et_r2\": et_mean\n#         }\n\n#     return models, cv_results\n\n\n# def predict_biomass_ensemble(df_feat, models, feature_cols):\n#     df_feat = df_feat.copy()\n#     df_feat[\"prediction\"] = np.nan\n\n#     for tgt, bundle in models.items():\n#         mask = df_feat[\"target_name\"] == tgt\n#         if mask.sum() == 0:\n#             continue\n\n#         X = df_feat.loc[mask, feature_cols]\n#         cat_pred = bundle[\"cat\"].predict(X)\n#         et_pred  = bundle[\"et\"].predict(X)\n\n#         w_cat, w_et = bundle[\"weights\"]\n#         df_feat.loc[mask, \"prediction\"] = (\n#             w_cat * cat_pred + w_et * et_pred\n#         )\n\n#     return df_feat\n\n\n# def train_pipeline(df):\n#     df_feat, feature_cols, cluster_table, base_features = add_features(df)\n#     feature_cols = list(dict.fromkeys(feature_cols))\n\n#     models, cv_results = train_final_models(df_feat, feature_cols)\n\n#     return models, feature_cols, cv_results, cluster_table, base_features\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:15:43.866046Z","iopub.execute_input":"2026-01-01T19:15:43.866749Z","iopub.status.idle":"2026-01-01T19:15:43.874458Z","shell.execute_reply.started":"2026-01-01T19:15:43.866718Z","shell.execute_reply":"2026-01-01T19:15:43.873338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import joblib\nimport os\n\n# LOAD_DIR = \"/kaggle/input/criso-model-1/artifacts\"\nLOAD_DIR = \"/kaggle/input/criso-xgboost/artifacts\"\n# LOAD_DIR = \"/kaggle/input/criso-catboost/artifacts\"\n# LOAD_DIR = \"/kaggle/input/criso-trees/artifacts\"\n\n# Load features\nfeature_cols = joblib.load(f\"{LOAD_DIR}/feature_cols.joblib\")\n\n# Load models\ntrained_models = {}\nfor fname in os.listdir(LOAD_DIR):\n    if fname.startswith(\"lgbm_\") and fname.endswith(\".joblib\"):\n        tgt = fname.replace(\"lgbm_\", \"\").replace(\".joblib\", \"\")\n        trained_models[tgt] = joblib.load(f\"{LOAD_DIR}/{fname}\")\n\nprint(\"✅ Models and features loaded\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:06.980288Z","iopub.execute_input":"2026-01-01T19:17:06.980650Z","iopub.status.idle":"2026-01-01T19:17:07.697388Z","shell.execute_reply.started":"2026-01-01T19:17:06.980621Z","shell.execute_reply":"2026-01-01T19:17:07.696568Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# trained_models","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:15:43.922344Z","iopub.status.idle":"2026-01-01T19:15:43.922614Z","shell.execute_reply.started":"2026-01-01T19:15:43.922485Z","shell.execute_reply":"2026-01-01T19:15:43.922502Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_preds = predict_biomass_unseen(\n    df_unseen=test,\n    trained_models=trained_models,\n    selected_features=feature_cols,\n    directory=directory\n)\n\n### For Catboost\n\n# df_feat_val = generate_selected_features_only(\n#     df=test,\n#     selected_features=feature_cols,\n#     directory=directory\n# )\n\n# df_preds = predict_biomass_ensemble(\n#     df_feat_val,\n#     trained_models,\n#     feature_cols\n# )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:09.944988Z","iopub.execute_input":"2026-01-01T19:17:09.946089Z","iopub.status.idle":"2026-01-01T19:17:11.673455Z","shell.execute_reply.started":"2026-01-01T19:17:09.946053Z","shell.execute_reply":"2026-01-01T19:17:11.672609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_preds[[\n    \"sample_id\",\n    \"target_name\",\n    # \"target\",\n    \"prediction\"\n]]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:11.959903Z","iopub.execute_input":"2026-01-01T19:17:11.960967Z","iopub.status.idle":"2026-01-01T19:17:11.984720Z","shell.execute_reply.started":"2026-01-01T19:17:11.960932Z","shell.execute_reply":"2026-01-01T19:17:11.983840Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_physics_long(df):\n    df = df.copy()\n    df[\"image_id\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n\n    wide = df.pivot(index=\"image_id\", columns=\"target_name\", values=\"prediction\")\n\n    wide[\"Dry_Clover_g\"] = wide[\"GDM_g\"] - wide[\"Dry_Green_g\"]\n    wide[\"Dry_Dead_g\"] = (\n        wide[\"Dry_Total_g\"]\n        - wide[\"GDM_g\"]\n    )\n\n    long = wide.reset_index().melt(\n        id_vars=\"image_id\",\n        var_name=\"target_name\",\n        value_name=\"prediction\"\n    )\n\n    df = df.drop(columns=\"prediction\").merge(\n        long, on=[\"image_id\", \"target_name\"], how=\"left\"\n    )\n\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:13.480217Z","iopub.execute_input":"2026-01-01T19:17:13.480793Z","iopub.status.idle":"2026-01-01T19:17:13.486785Z","shell.execute_reply.started":"2026-01-01T19:17:13.480733Z","shell.execute_reply":"2026-01-01T19:17:13.485638Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_preds = apply_physics_long(df_preds)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:15.000405Z","iopub.execute_input":"2026-01-01T19:17:15.000745Z","iopub.status.idle":"2026-01-01T19:17:15.028423Z","shell.execute_reply.started":"2026-01-01T19:17:15.000710Z","shell.execute_reply":"2026-01-01T19:17:15.027510Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_preds.rename(columns = {'prediction':'target'}, inplace = True)\ndf_preds = df_preds[['sample_id','target']]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:16.344730Z","iopub.execute_input":"2026-01-01T19:17:16.345160Z","iopub.status.idle":"2026-01-01T19:17:16.351424Z","shell.execute_reply.started":"2026-01-01T19:17:16.345128Z","shell.execute_reply":"2026-01-01T19:17:16.350477Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_preds\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:20.019983Z","iopub.execute_input":"2026-01-01T19:17:20.020310Z","iopub.status.idle":"2026-01-01T19:17:20.029253Z","shell.execute_reply.started":"2026-01-01T19:17:20.020283Z","shell.execute_reply":"2026-01-01T19:17:20.028267Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_preds.to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-01T19:17:35.754939Z","iopub.execute_input":"2026-01-01T19:17:35.755303Z","iopub.status.idle":"2026-01-01T19:17:35.766049Z","shell.execute_reply.started":"2026-01-01T19:17:35.755272Z","shell.execute_reply":"2026-01-01T19:17:35.764987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}