{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom PIL import Image\nfrom IPython.display import display\n\nfrom sklearn.model_selection import KFold\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom sklearn.ensemble import HistGradientBoostingRegressor\n\npd.set_option(\"display.max_columns\", 100)\npd.set_option(\"display.width\", 200)\n\n# Ruta fija del dataset de la competición (ajusta si el nombre difiere)\nINPUT_DIR = Path(\"/kaggle/input/csiro-biomass\")\n\n# Si dentro hay una única subcarpeta (p.ej. csiro-biomass-public), úsala\nsubdirs = [p for p in INPUT_DIR.iterdir() if p.is_dir()]\nCOMP_DIR = subdirs[0] if len(subdirs) == 1 else INPUT_DIR\n\nprint(\"Usando carpeta de datos:\", COMP_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T03:51:33.613910Z","iopub.execute_input":"2025-11-08T03:51:33.614112Z","iopub.status.idle":"2025-11-08T03:51:37.800354Z","shell.execute_reply.started":"2025-11-08T03:51:33.614093Z","shell.execute_reply":"2025-11-08T03:51:37.799319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_csv(COMP_DIR / \"train.csv\")\ntest  = pd.read_csv(COMP_DIR / \"test.csv\")\n\nprint(\"train shape:\", train.shape)\nprint(\"test shape:\", test.shape)\n\ndisplay(train.head())\ndisplay(test.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T03:51:37.802242Z","iopub.execute_input":"2025-11-08T03:51:37.802663Z","iopub.status.idle":"2025-11-08T03:51:37.868559Z","shell.execute_reply.started":"2025-11-08T03:51:37.802630Z","shell.execute_reply":"2025-11-08T03:51:37.867683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Lista de variables objetivo (target_name)\ntarget_names = sorted(train[\"target_name\"].unique())\nprint(\"Targets:\", target_names, \"| n_targets:\", len(target_names))\n\n# Pivot: filas = image_path, columnas = target_name, valores = target\ny_wide = (\n    train.pivot(index=\"image_path\", columns=\"target_name\", values=\"target\")\n         .loc[:, target_names]\n)\nprint(\"y_wide shape:\", y_wide.shape)\ndisplay(y_wide.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T03:51:37.869294Z","iopub.execute_input":"2025-11-08T03:51:37.869550Z","iopub.status.idle":"2025-11-08T03:51:37.908225Z","shell.execute_reply.started":"2025-11-08T03:51:37.869504Z","shell.execute_reply":"2025-11-08T03:51:37.907408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_image_features(rel_path: str) -> dict:\n    \"\"\"\n    rel_path: ruta relativa como viene en image_path (p.ej. 'train/IDxxxxx.jpg')\n    Devuelve features robustos (si falla la lectura, devuelve ceros).\n    \"\"\"\n    img_path = COMP_DIR / rel_path\n\n    feats = {\n        \"mean_R\": 0.0, \"mean_G\": 0.0, \"mean_B\": 0.0,\n        \"std_R\": 0.0,  \"std_G\": 0.0,  \"std_B\": 0.0,\n        \"excess_green\": 0.0,\n        \"mean_gray\": 0.0,\n        \"mean_g_fraction\": 0.0,\n        \"prop_green_pixels\": 0.0,\n        \"p90_excess_green\": 0.0,\n    }\n\n    try:\n        with Image.open(img_path) as img:\n            img = img.convert(\"RGB\")\n            arr = np.array(img).astype(np.float32)\n\n        R = arr[:, :, 0]\n        G = arr[:, :, 1]\n        B = arr[:, :, 2]\n\n        # Estadísticos básicos\n        feats[\"mean_R\"] = R.mean()\n        feats[\"mean_G\"] = G.mean()\n        feats[\"mean_B\"] = B.mean()\n        feats[\"std_R\"]  = R.std()\n        feats[\"std_G\"]  = G.std()\n        feats[\"std_B\"]  = B.std()\n\n        # Exceso de verde\n        eg = 2*G - R - B\n        feats[\"excess_green\"] = eg.mean()\n\n        # Gris y fracción de verde\n        gray = 0.299*R + 0.587*G + 0.114*B\n        feats[\"mean_gray\"] = gray.mean()\n\n        denom = R + G + B + 1e-6\n        g_frac = G / denom\n        feats[\"mean_g_fraction\"] = g_frac.mean()\n\n        # Proporción de píxeles con verde dominante\n        mask_green = (G > R) & (G > B)\n        feats[\"prop_green_pixels\"] = mask_green.mean()\n\n        # Percentil alto de exceso de verde\n        feats[\"p90_excess_green\"] = float(np.percentile(eg, 90))\n\n    except Exception:\n        # Mantener ceros si hay problema al leer la imagen\n        pass\n\n    return feats\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T03:51:37.909292Z","iopub.execute_input":"2025-11-08T03:51:37.909597Z","iopub.status.idle":"2025-11-08T03:51:37.920801Z","shell.execute_reply.started":"2025-11-08T03:51:37.909565Z","shell.execute_reply":"2025-11-08T03:51:37.919843Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def safe_fill(df: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"Reemplaza inf/NaN por valores seguros (media de columna y luego 0).\"\"\"\n    df = df.replace([np.inf, -np.inf], np.nan)\n    df = df.fillna(df.mean()).fillna(0)\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T03:51:37.921927Z","iopub.execute_input":"2025-11-08T03:51:37.922266Z","iopub.status.idle":"2025-11-08T03:51:37.940100Z","shell.execute_reply.started":"2025-11-08T03:51:37.922237Z","shell.execute_reply":"2025-11-08T03:51:37.938900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_model_and_submission(train: pd.DataFrame,\n                               test: pd.DataFrame,\n                               target_names,\n                               y_wide: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"\n    - Extrae features (train/test)\n    - Entrena en log1p(target) con MultiOutput(HistGradientBoosting)\n    - Predice test, revierte con expm1\n    - Construye submission SOLO desde test (sin sample_submission)\n    Devuelve DataFrame ['sample_id', 'target'].\n    \"\"\"\n\n    # ===== 1) Features de TRAIN =====\n    train_images = y_wide.index.tolist()\n    feats_train = []\n    for rel in train_images:\n        f = extract_image_features(rel)\n        f[\"image_path\"] = rel\n        feats_train.append(f)\n\n    X_train_df = pd.DataFrame(feats_train).set_index(\"image_path\")\n    X_train_df = safe_fill(X_train_df)\n\n    X = X_train_df.values\n    y = y_wide.values\n    y_log = np.log1p(y)  # trabajar en log1p\n\n    print(\"X shape:\", X.shape, \"| y shape:\", y.shape)\n\n    # ===== 2) CV rápido en log1p (opcional informativo) =====\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    fold_rmse = []\n    for fold, (tr, va) in enumerate(kf.split(X)):\n        X_tr, X_va = X[tr], X[va]\n        y_tr, y_va = y_log[tr], y_log[va]\n\n        base = HistGradientBoostingRegressor(\n            max_depth=3,\n            learning_rate=0.1,\n            max_iter=300,\n            random_state=42\n        )\n        model = MultiOutputRegressor(base)\n        model.fit(X_tr, y_tr)\n\n        p = model.predict(X_va)\n        rmse = np.sqrt(((p - y_va)**2).mean(axis=0))\n        fold_rmse.append(rmse)\n        print(f\"Fold {fold} RMSE(log1p):\",\n              {t: float(r) for t, r in zip(target_names, rmse)})\n\n    print(\"\\nRMSE promedio(log1p):\",\n          {t: float(m) for t, m in zip(target_names, np.mean(fold_rmse, axis=0))})\n\n    # ===== 3) Modelo final =====\n    base_final = HistGradientBoostingRegressor(\n        max_depth=3,\n        learning_rate=0.1,\n        max_iter=300,\n        random_state=42\n    )\n    final_model = MultiOutputRegressor(base_final)\n    final_model.fit(X, y_log)\n\n    # ===== 4) Features de TEST =====\n    test_images = test[\"image_path\"].unique().tolist()\n    feats_test = []\n    for rel in test_images:\n        f = extract_image_features(rel)\n        f[\"image_path\"] = rel\n        feats_test.append(f)\n\n    X_test_df = pd.DataFrame(feats_test).set_index(\"image_path\")\n    X_test_df = safe_fill(X_test_df)\n    X_test = X_test_df.values\n\n    # ===== 5) Predicción (log1p -> original) =====\n    preds_log = final_model.predict(X_test)\n    preds = np.expm1(preds_log)\n    preds = np.clip(preds, 0, None)  # biomasa >= 0\n\n    preds_df = pd.DataFrame(preds, index=test_images, columns=target_names)\n\n    # ===== 6) Formato largo + unión con test =====\n    preds_long = (\n        preds_df.reset_index()\n                .melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"pred\")\n                .rename(columns={\"index\": \"image_path\"})\n    )\n\n    test_pred = test.merge(preds_long,\n                           on=[\"image_path\", \"target_name\"],\n                           how=\"left\")\n\n    # ===== 7) Submission solo desde test =====\n    submission = test_pred[[\"sample_id\", \"pred\"]].rename(columns={\"pred\": \"target\"})\n\n    # Sanity check\n    assert submission.shape[0] == test.shape[0], \\\n        \"El submission no tiene el mismo número de filas que test\"\n\n    return submission\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T03:51:37.941580Z","iopub.execute_input":"2025-11-08T03:51:37.941999Z","iopub.status.idle":"2025-11-08T03:51:37.960565Z","shell.execute_reply.started":"2025-11-08T03:51:37.941907Z","shell.execute_reply":"2025-11-08T03:51:37.959609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== BLOQUE PRINCIPAL A PRUEBA DE FALLOS =====\ntry:\n    print(\"Ejecutando pipeline…\")\n    submission = build_model_and_submission(\n        train=train,\n        test=test,\n        target_names=target_names,\n        y_wide=y_wide\n    )\n    print(\"Pipeline OK.\")\nexcept Exception as e:\n    print(\"ERROR en pipeline, usando fallback basado en test.\")\n    print(\"Detalle (solo visible en public run):\", repr(e))\n    # Fallback: output válido con media global de train\n    submission = test[[\"sample_id\"]].copy()\n    submission[\"target\"] = float(train[\"target\"].mean())\n\n# Guardar SIEMPRE submission.csv\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"submission.csv guardado. Filas:\", len(submission))\ndisplay(submission.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T03:51:37.963061Z","iopub.execute_input":"2025-11-08T03:51:37.963399Z","iopub.status.idle":"2025-11-08T03:52:58.678240Z","shell.execute_reply.started":"2025-11-08T03:51:37.963369Z","shell.execute_reply":"2025-11-08T03:52:58.677488Z"}},"outputs":[],"execution_count":null}]}