{"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"},{"sourceId":13727488,"sourceType":"datasetVersion","datasetId":8733721},{"sourceId":13760813,"sourceType":"datasetVersion","datasetId":8756865},{"sourceId":13789158,"sourceType":"datasetVersion","datasetId":8778382}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**🧭 Plan de Trabajo – Competencia de Predicción de Biomasa (Kaggle)**","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport seaborn.objects as so\nimport cv2\nfrom tqdm import tqdm\nfrom PIL import Image\nimport random\nimport warnings\nfrom sklearn.preprocessing import LabelEncoder\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.models import vit_b_16, ViT_B_16_Weights\nimport torch\nfrom tqdm import tqdm\nimport torch.nn as nn\nfrom sklearn.model_selection import GroupKFold\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import GroupKFold\nfrom torch.utils.data import TensorDataset, DataLoader\nimport pickle","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T08:40:08.353355Z","iopub.execute_input":"2025-11-19T08:40:08.353803Z","iopub.status.idle":"2025-11-19T08:40:08.365207Z","shell.execute_reply.started":"2025-11-19T08:40:08.353774Z","shell.execute_reply":"2025-11-19T08:40:08.363979Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n#  Información general del dataset\n# =========================================================\n# ---------------------------------------------------------\n#  Cargar los datos CSV\n# ---------------------------------------------------------\ntrain_path = \"/kaggle/input/csiro-biomass/train.csv\"\ntest_path = \"/kaggle/input/csiro-biomass/test.csv\"\n\ntrain_df = pd.read_csv(train_path)\ntest_df = pd.read_csv(test_path)\n\nprint(\"\\n📘 Información general:\")\ntrain_df.info()\n\n# Número de filas y columnas\nprint(f\"\\n📏 Dimensiones del dataset: {train_df.shape}\")\n\n# Mostrar las primeras filas del dataset\ndisplay(train_df.head())\ndisplay(test_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:21.890034Z","iopub.execute_input":"2025-11-19T05:22:21.890511Z","iopub.status.idle":"2025-11-19T05:22:21.990421Z","shell.execute_reply.started":"2025-11-19T05:22:21.890485Z","shell.execute_reply":"2025-11-19T05:22:21.989592Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n#  Valores faltantes \n# =========================================================\nprint(\"\\n🔎 Valores nulos por columna:\")\nprint(train_df.isnull().sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:21.991215Z","iopub.execute_input":"2025-11-19T05:22:21.991439Z","iopub.status.idle":"2025-11-19T05:22:21.998716Z","shell.execute_reply.started":"2025-11-19T05:22:21.991422Z","shell.execute_reply":"2025-11-19T05:22:21.997788Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.groupby('target_name')['target'].describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:22.000982Z","iopub.execute_input":"2025-11-19T05:22:22.001327Z","iopub.status.idle":"2025-11-19T05:22:22.059977Z","shell.execute_reply.started":"2025-11-19T05:22:22.001305Z","shell.execute_reply":"2025-11-19T05:22:22.059063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 🌾 Distribución de las variables objetivo (sin warnings)\n# =========================================================\n\n# Ignorar warnings de seaborn/pandas\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\nwarnings.filterwarnings(\"ignore\", category=UserWarning, message=\"The figure layout has changed to tight\")\n# Lista de variables objetivo\ntarget_vars = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\nplt.figure(figsize=(15, 10))\n\nfor i, var in enumerate(target_vars, 1):\n    plt.subplot(2, 3, i)\n    \n    # Filtrar datos del target actual\n    subset = train_df[train_df['target_name'] == var].copy()\n    \n    # Reemplazar infinitos por NaN y eliminar filas nulas\n    subset['target'] = subset['target'].replace([np.inf, -np.inf], np.nan)\n    subset = subset.dropna(subset=['target'])\n    \n    sns.histplot(subset['target'], kde=True, bins=30, color='green')\n    plt.title(f'Distribución de {var}')\n    plt.xlabel('Gramos')\n    plt.ylabel('Frecuencia')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:22.060956Z","iopub.execute_input":"2025-11-19T05:22:22.061482Z","iopub.status.idle":"2025-11-19T05:22:23.561246Z","shell.execute_reply.started":"2025-11-19T05:22:22.061457Z","shell.execute_reply":"2025-11-19T05:22:23.560114Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.stats import shapiro\n\nfor var in target_vars:\n    subset = train_df[train_df['target_name'] == var]['target'].dropna()\n    stat, p = shapiro(subset)\n    print(f\"{var}: p-value={p:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:23.562195Z","iopub.execute_input":"2025-11-19T05:22:23.562456Z","iopub.status.idle":"2025-11-19T05:22:23.577063Z","shell.execute_reply.started":"2025-11-19T05:22:23.562435Z","shell.execute_reply":"2025-11-19T05:22:23.575683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 📦 Visualización tipo Boxplot de las variables objetivo\n# =========================================================\nwarnings.filterwarnings(\"ignore\")\n\n# Lista de variables objetivo\ntarget_vars = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n# Crear una lista vacía para almacenar los datos\ndata = []\n\n# Extraer los valores de cada variable objetivo\nfor var in target_vars:\n    subset = train_df[train_df['target_name'] == var].copy()\n    subset['target'] = subset['target'].replace([np.inf, -np.inf], np.nan)\n    subset = subset.dropna(subset=['target'])\n    subset['variable'] = var\n    data.append(subset[['variable', 'target']])\n\n# Combinar todos los subconjuntos en un solo DataFrame\ncombined_df = pd.concat(data)\n\n# Crear la figura\nplt.figure(figsize=(12, 6))\nsns.boxplot(x='variable', y='target', data=combined_df, palette='Spectral')\n\n# Añadir detalles\nplt.title('Boxplot de las variables objetivo (gramos)', fontsize=14)\nplt.xlabel('Variable objetivo')\nplt.ylabel('Valor (g)')\nplt.grid(True, linestyle='--', alpha=0.6)\n\n# Mostrar el gráfico\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:23.578474Z","iopub.execute_input":"2025-11-19T05:22:23.578819Z","iopub.status.idle":"2025-11-19T05:22:23.867654Z","shell.execute_reply.started":"2025-11-19T05:22:23.578789Z","shell.execute_reply":"2025-11-19T05:22:23.866460Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 📈 1️⃣ Análisis de correlación entre las variables objetivo\n# =========================================================\n\n# Ignorar warnings innecesarios\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\n\n# ---------------------------------------------------------\n# 🧩 Crear una copia del dataframe original\n# ---------------------------------------------------------\ndf_corr = train_df.copy()\n\n# ---------------------------------------------------------\n# 🧹 Limpieza básica\n# ---------------------------------------------------------\n# Reemplazar infinitos por NaN\ndf_corr[\"target\"] = df_corr[\"target\"].replace([np.inf, -np.inf], np.nan)\n\n# Eliminar filas con valores faltantes en columnas clave\ndf_corr = df_corr.dropna(subset=[\"target\", \"target_name\", \"sample_id\"])\n\n# ---------------------------------------------------------\n# 🧠 Reestructurar el dataset (una fila por muestra)\n# ---------------------------------------------------------\n# Extraer el identificador base (algunos sample_id incluyen el target)\ndf_corr[\"base_id\"] = df_corr[\"sample_id\"].apply(lambda x: str(x).split(\"_\")[0])\n\n# Pivotear: columnas = tipos de biomasa (target_name), filas = muestra (base_id)\ndf_pivot = df_corr.pivot_table(\n    index=\"base_id\",\n    columns=\"target_name\",\n    values=\"target\",\n    aggfunc=\"mean\"\n)\n\n# Eliminar filas incompletas\ndf_pivot = df_pivot.dropna(how=\"any\")\n\n# ---------------------------------------------------------\n# 📊 Calcular matriz de correlación (Pearson)\n# ---------------------------------------------------------\ncorr_matrix = df_pivot.corr(method=\"pearson\")\n\nprint(\"\\n📈 Matriz de correlación entre variables objetivo (Pearson):\\n\")\nprint(corr_matrix.round(2))\n\n# ---------------------------------------------------------\n# 🎨 Visualización de la correlación\n# ---------------------------------------------------------\nplt.figure(figsize=(12, 10))\nsns.heatmap(\n    corr_matrix,\n    annot=True,\n    fmt=\".2f\",\n    cmap=\"YlGnBu\",\n    linewidths=0.5,\n    cbar_kws={\"shrink\": 0.8}\n)\nplt.title(\"Matriz de correlación entre variables objetivo\", fontsize=14, fontweight=\"bold\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:23.868789Z","iopub.execute_input":"2025-11-19T05:22:23.869071Z","iopub.status.idle":"2025-11-19T05:22:24.259119Z","shell.execute_reply.started":"2025-11-19T05:22:23.869048Z","shell.execute_reply":"2025-11-19T05:22:24.258022Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Copia de trabajo\ndfx = train_df.copy()\n\n# Seleccionar columnas relevantes\ndfx = dfx[['image_path', 'sample_id', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm', 'target_name', 'target']]\n\nprint(\"\\n📘 Información general:\")\ndfx.info()\n\n# Número de filas y columnas\nprint(f\"\\n📏 Dimensiones del dataset: {dfx.shape}\")\n\n# Mostrar las primeras filas del dataset\ndisplay(dfx.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:24.260510Z","iopub.execute_input":"2025-11-19T05:22:24.260882Z","iopub.status.idle":"2025-11-19T05:22:24.285724Z","shell.execute_reply.started":"2025-11-19T05:22:24.260848Z","shell.execute_reply":"2025-11-19T05:22:24.284770Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# 1) CARGAR TRAIN Y CONVERTIR A FORMATO ANCHO\n# ================================================================\n\ntrain_df = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\n\ndfx = train_df.copy()\n\n# Quitar el sufijo \"__target\"\ndfx[\"sample_id\"] = dfx[\"sample_id\"].str.replace(r\"__.*\", \"\", regex=True)\n\n# Pivotear TRAIN\ndf_wide_train = dfx.pivot_table(\n    index=[\n        \"image_path\", \"sample_id\", \"Sampling_Date\", \"State\", \"Species\",\n        \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"\n    ],\n    columns=\"target_name\",\n    values=\"target\"\n).reset_index()\n\ndf_wide_train.columns.name = None\n\nprint(\"TRAIN ancho:\", df_wide_train.shape)\ndisplay(df_wide_train.head())\n\n# Agregar columna con el ID real de la imagen sin .jpg\ndf_wide_train[\"image_id\"] = df_wide_train[\"image_path\"].apply(\n    lambda x: x.split(\"/\")[-1].replace(\".jpg\", \"\")\n)\n\nprint(\"✔ df_wide_train listo.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:24.289128Z","iopub.execute_input":"2025-11-19T05:22:24.289536Z","iopub.status.idle":"2025-11-19T05:22:24.337944Z","shell.execute_reply.started":"2025-11-19T05:22:24.289511Z","shell.execute_reply":"2025-11-19T05:22:24.337047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# --- 1️⃣ Copiamos el DataFrame original ---\ndf_encoded = df_wide_train.copy()\n\n# --- 2️⃣ Codificación cíclica de la fecha ---\n# Convertir a formato datetime (si no lo está)\ndf_encoded['Sampling_Date'] = pd.to_datetime(df_encoded['Sampling_Date'], errors='coerce')\n\n# Extraer día del año\ndf_encoded['day_of_year'] = df_encoded['Sampling_Date'].dt.dayofyear\n\n# Calcular sin y cos\ndf_encoded['sin_date'] = np.sin(2 * np.pi * df_encoded['day_of_year'] / 365)\ndf_encoded['cos_date'] = np.cos(2 * np.pi * df_encoded['day_of_year'] / 365)\n\n# --- 3️⃣ Codificación de 'State' ---\nle_state = LabelEncoder()\ndf_encoded['State_encoded'] = le_state.fit_transform(df_encoded['State'])\n\n# --- 4️⃣ Codificación de 'Species' ---\nle_species = LabelEncoder()\ndf_encoded['Species_encoded'] = le_species.fit_transform(df_encoded['Species'])\n\n# --- 5️⃣ Eliminamos columnas originales si se desea ---\n#df_encoded = df_encoded.drop(columns=['Sampling_Date', 'day_of_year', 'State', 'Species', 'image_path'])\n\n# --- 6️⃣ Visualizamos las primeras filas para verificar ---\n\ndisplay(df_encoded.head(357))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:24.339295Z","iopub.execute_input":"2025-11-19T05:22:24.339627Z","iopub.status.idle":"2025-11-19T05:22:24.387452Z","shell.execute_reply.started":"2025-11-19T05:22:24.339599Z","shell.execute_reply":"2025-11-19T05:22:24.386509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_encoded = df_encoded.replace([np.inf, -np.inf], np.nan)\ndf_encoded = df_encoded.fillna(0)\n#warnings.filterwarnings(\"ignore\", category=FutureWarning)\n# Seleccionamos las variables numéricas predictoras\nnum_vars = ['Pre_GSHH_NDVI', 'Height_Ave_cm']\n\n# Definimos el tamaño de los gráficos\nplt.figure(figsize=(15, 10))\n\n# Iteramos sobre cada variable y graficamos su distribución\nfor i, var in enumerate(num_vars, 1):\n    plt.subplot(2, 2, i)\n    sns.histplot(df_encoded[var], kde=True, bins=25, color='green')\n    plt.title(f\"Distribución de {var}\")\n    plt.xlabel(var)\n    plt.ylabel(\"Frecuencia\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:24.388342Z","iopub.execute_input":"2025-11-19T05:22:24.388623Z","iopub.status.idle":"2025-11-19T05:22:24.998501Z","shell.execute_reply.started":"2025-11-19T05:22:24.388602Z","shell.execute_reply":"2025-11-19T05:22:24.997450Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sns.heatmap(df_encoded[['Pre_GSHH_NDVI', 'Height_Ave_cm']].corr(), annot=True, cmap='YlGnBu')\nplt.title('Matriz de correlación entre variables predictoras')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:24.999794Z","iopub.execute_input":"2025-11-19T05:22:25.000334Z","iopub.status.idle":"2025-11-19T05:22:25.201442Z","shell.execute_reply.started":"2025-11-19T05:22:25.000309Z","shell.execute_reply":"2025-11-19T05:22:25.200316Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Definimos predictoras y objetivos\npredictoras = ['Pre_GSHH_NDVI', 'Height_Ave_cm',]\nobjetivos = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n# Calculamos correlación cruzada (solo X vs Y)\ncorr_matrix = df_encoded[predictoras + objetivos].corr().loc[predictoras, objetivos]\n\n# Graficamos el heatmap\nplt.figure(figsize=(10, 5))\nsns.heatmap(\n    corr_matrix, \n    annot=True, \n    cmap='YlGnBu', \n    fmt=\".2f\", \n    linewidths=0.5, \n    cbar_kws={'shrink': 0.8, 'label': 'Coeficiente de correlación (r)'}\n)\n\nplt.title('Correlación entre variables predictoras y variables objetivo', \n          fontsize=14, fontweight='bold', pad=15)\nplt.xticks(rotation=45, ha='right')\nplt.yticks(rotation=0)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:25.202387Z","iopub.execute_input":"2025-11-19T05:22:25.202614Z","iopub.status.idle":"2025-11-19T05:22:25.477656Z","shell.execute_reply.started":"2025-11-19T05:22:25.202597Z","shell.execute_reply":"2025-11-19T05:22:25.476371Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sns.set(rc={\"figure.figsize\": (16, 10)})\nsns.set_style(\"whitegrid\")\n\norder_targets = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]\n\ng = sns.catplot(\n    data=train_df,\n    x=\"target_name\",\n    y=\"target\",\n    col=\"State\",\n    kind=\"box\",\n    order=order_targets,\n    showfliers=False,\n    height=5,\n    aspect=1.2,\n)\n\ng.set_titles(\"Estado = {col_name}\")\ng.set_xlabels(\"Variable objetivo\")\ng.set_ylabels(\"Valores de biomasa (g)\")\nplt.tight_layout()\nplt.show()\n###\n\npivot = train_df.pivot_table(\n    index=\"Species\",\n    columns=\"target_name\",\n    values=\"target\",\n    aggfunc=\"median\"\n)\n\nsns.heatmap(pivot, cmap=\"viridis\", annot=True, fmt=\".1f\")\nplt.title(\"Medianas de target por especie\")\nplt.show()\n####\nplt.style.use(\"seaborn-v0_8-whitegrid\")\n\n# Targets en orden fijo\norder_targets = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \n                 \"Dry_Total_g\", \"GDM_g\"]\n\nunique_states = sorted(train_df[\"State\"].unique())\n\nfig, axes = plt.subplots(2, 2, figsize=(18, 12))\naxes = axes.flatten()\n\nfor i, state in enumerate(unique_states):\n\n    subset = train_df[train_df[\"State\"] == state]\n\n    pivot = subset.pivot_table(\n        index=\"Species\",\n        columns=\"target_name\",\n        values=\"target\",\n        aggfunc=\"median\"\n    )\n\n    # Reordenar columnas según targets válidos\n    pivot = pivot.reindex(columns=order_targets)\n\n    ax = axes[i]\n    sns.heatmap(\n        pivot,\n        cmap=\"viridis\",\n        annot=True,\n        fmt=\".1f\",\n        linewidths=0.5,\n        cbar=i == 0,    # solo mostrar una barra de color\n        ax=ax\n    )\n\n    ax.set_title(f\"Medianas por Especie — Estado: {state}\", fontsize=14)\n    ax.set_xlabel(\"Variable objetivo\")\n    ax.set_ylabel(\"Especie\")\n\n# Ajustar layout\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:25.478646Z","iopub.execute_input":"2025-11-19T05:22:25.478970Z","iopub.status.idle":"2025-11-19T05:22:29.302761Z","shell.execute_reply.started":"2025-11-19T05:22:25.478946Z","shell.execute_reply":"2025-11-19T05:22:29.301681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[\"Sampling_Date\"] = pd.to_datetime(train_df[\"Sampling_Date\"])\ntrain_df[\"Year\"] = train_df[\"Sampling_Date\"].dt.year\ntrain_df[\"Month\"] = train_df[\"Sampling_Date\"].dt.month\ntrain_df[\"DayOfYear\"] = train_df[\"Sampling_Date\"].dt.dayofyear\n\nplt.figure(figsize=(15, 10))\n\nfor i, var in enumerate([\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"], 1):\n    plt.subplot(3, 2, i)\n    sns.boxplot(\n        data=train_df,\n        x=train_df[\"Sampling_Date\"].dt.month,\n        y=train_df[train_df[\"target_name\"] == var][\"target\"]\n    )\n    plt.title(f\"{var} por mes\")\n    plt.xlabel(\"Mes\")\n    plt.ylabel(\"Biomasa (g)\")\n\nplt.tight_layout()\nplt.show\n\ndf_pivot = train_df.pivot_table(\n    index=train_df[\"Sampling_Date\"].dt.month,\n    columns=\"target_name\",\n    values=\"target\",\n    aggfunc=\"median\"\n)\n\nplt.figure(figsize=(12, 8))\nsns.heatmap(\n    df_pivot,\n    cmap=\"viridis\",\n    linewidths=.2,\n    annot=True,      # ← AGREGADO\n    fmt=\".1f\",       # ← AGREGADO\n    cbar=True\n)\nplt.title(\"Mediana de biomasa por mes (global)\")\nplt.xlabel(\"Target\")\nplt.ylabel(\"Mes (1–12)\")\nplt.show()\n###\n\nstates = train_df[\"State\"].unique()\n\nfig, axes = plt.subplots(2, 2, figsize=(16, 12))  # 2 filas x 2 columnas\naxes = axes.flatten()  # Convertir a lista simple\n\nfor ax, st in zip(axes, states):\n    \n    # Pivot mensual por estado\n    df_piv = train_df[train_df[\"State\"] == st].pivot_table(\n        index=train_df[\"Sampling_Date\"].dt.month,\n        columns=\"target_name\",\n        values=\"target\",\n        aggfunc=\"median\",\n        fill_value=0\n    )\n    \n    # Heatmap con valores dentro\n    sns.heatmap(\n        df_piv,\n        cmap=\"viridis\",\n        linewidths=.2,\n        annot=True,\n        fmt=\".1f\",\n        cbar=True,\n        ax=ax\n    )\n    \n    ax.set_title(f\"Mediana de biomasa por mes — Estado: {st}\")\n    ax.set_xlabel(\"Target\")\n    ax.set_ylabel(\"Mes\")\n\n# Si sobran ejes (por seguridad) los quitamos\nfor i in range(len(states), len(axes)):\n    axes[i].axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:29.303730Z","iopub.execute_input":"2025-11-19T05:22:29.304072Z","iopub.status.idle":"2025-11-19T05:22:33.231333Z","shell.execute_reply.started":"2025-11-19T05:22:29.304046Z","shell.execute_reply":"2025-11-19T05:22:33.230426Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"###  Extraccion de embending a partir de imagenes ###\n","metadata":{}},{"cell_type":"code","source":"image_path = \"/kaggle/input/csiro-biomass/train/\"  # cambia según tu ruta\nimage_files = [os.path.join(image_path, f) for f in os.listdir(image_path) if f.endswith('.jpg')]\nprint(f\"Total de imágenes: {len(image_files)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:33.232391Z","iopub.execute_input":"2025-11-19T05:22:33.232729Z","iopub.status.idle":"2025-11-19T05:22:33.252640Z","shell.execute_reply.started":"2025-11-19T05:22:33.232702Z","shell.execute_reply":"2025-11-19T05:22:33.251556Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"warnings.filterwarnings(\"ignore\", category=FutureWarning)\n# -----------------------------\n# 1️⃣ Tamaño y proporciones\n# -----------------------------\nsizes = []\nratios = []\n\nfor img_file in tqdm(image_files, desc=\"Analizando tamaños\"):\n    with Image.open(img_file) as img:\n        w, h = img.size\n        sizes.append((w, h))\n        ratios.append(w / h)\n\ndf_sizes = pd.DataFrame(sizes, columns=['width', 'height'])\ndf_sizes['ratio'] = ratios\n\nprint(\"\\n📏 Estadísticas de tamaños:\")\ndisplay(df_sizes.describe())\n\nplt.figure(figsize=(6,4))\nsns.scatterplot(x='width', y='height', data=df_sizes)\nplt.title('Distribución de tamaños de las imágenes')\nplt.show()\n\nplt.figure(figsize=(6,4))\nsns.histplot(df_sizes['ratio'], bins=20, kde=True)\nplt.title('Proporción ancho/alto')\nplt.show()\n\n# -----------------------------\n# 2️⃣ Color promedio (RGB)\n# -----------------------------\navg_colors = []\n\nfor img_file in tqdm(image_files, desc=\"Calculando color promedio\"):\n    img = cv2.imread(img_file)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    mean_color = img.mean(axis=(0, 1))\n    avg_colors.append(mean_color)\n\ndf_colors = pd.DataFrame(avg_colors, columns=['R', 'G', 'B'])\nprint(\"\\n🎨 Estadísticas de color promedio:\")\ndisplay(df_colors.describe())\n\nplt.figure(figsize=(6,4))\nsns.kdeplot(df_colors['R'], label='Rojo')\nsns.kdeplot(df_colors['G'], label='Verde')\nsns.kdeplot(df_colors['B'], label='Azul')\nplt.title('Distribución de intensidades promedio RGB')\nplt.legend()\nplt.show()\n\n# -----------------------------\n# 3️⃣ Brillo y contraste\n# -----------------------------\nbrightness = []\ncontrast = []\n\nfor img_file in tqdm(image_files, desc=\"Analizando brillo y contraste\"):\n    img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)\n    brightness.append(np.mean(img))\n    contrast.append(np.std(img))\n\ndf_sizes['brightness'] = brightness\ndf_sizes['contrast'] = contrast\n\nplt.figure(figsize=(6,4))\nsns.scatterplot(x='brightness', y='contrast', data=df_sizes)\nplt.title('Relación Brillo vs Contraste')\nplt.show()\n\n# -----------------------------\n# 4️⃣ Muestra de imágenes\n# -----------------------------\nrandom.seed(42)  # 🔒 fija la semilla aleatoria\nsamples = random.sample(image_files, 6)\n\nplt.figure(figsize=(12,8))\nfor i, img_file in enumerate(samples):\n    img = Image.open(img_file)\n    plt.subplot(2,3,i+1)\n    plt.imshow(img)\n    plt.axis('off')\nplt.suptitle('Ejemplos de imágenes del conjunto')\nplt.show()\n\n# -----------------------------\n# 5️⃣ Imágenes atípicas\n# -----------------------------\noutliers = df_sizes[(df_sizes['brightness'] < 30) | (df_sizes['brightness'] > 230)]\nprint(f\"\\n⚠️ Imágenes extremadamente oscuras o brillantes: {len(outliers)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:22:33.254005Z","iopub.execute_input":"2025-11-19T05:22:33.254362Z","iopub.status.idle":"2025-11-19T05:23:50.411190Z","shell.execute_reply.started":"2025-11-19T05:22:33.254333Z","shell.execute_reply":"2025-11-19T05:23:50.409768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models import vit_b_16\nfrom torchvision import transforms\n\n# ==========================================================\n# 1) CONFIGURACIÓN GENERAL\n# ==========================================================\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ntransform = transforms.Compose([\n    transforms.Resize(232),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225])\n])\n\n\n# ==========================================================\n# 2) DATASET PARA FORMATO ANCHO (1 FILA = 1 IMAGEN)\n# ==========================================================\n\nclass ImageDatasetWide(Dataset):\n    def __init__(self, df, img_dir, transform):\n        self.df = df\n        self.img_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        filename = row[\"image_path\"].split(\"/\")[-1]  # ejemplo: ID123.jpg\n        full_path = os.path.join(self.img_dir, filename)\n\n        img = Image.open(full_path).convert(\"RGB\")\n        img = self.transform(img)\n\n        return img, row[\"image_id\"]  # devolvemos SOLO image_id\n\n\n# ==========================================================\n# 3) FUNCIÓN PARA EXTRAER EMBEDDINGS\n# ==========================================================\n\ndef extract_embeddings(df, img_dir, model, batch_size=32):\n    dataset = ImageDatasetWide(df, img_dir, transform)\n    loader = DataLoader(dataset, batch_size=batch_size, shuffle=False)\n\n    embs = []\n    ids = []\n\n    model.eval()\n    with torch.no_grad():\n        for imgs, image_ids in tqdm(loader, desc=f\"Extrayendo embeddings de {img_dir}\"):\n            imgs = imgs.to(device)\n            emb = model(imgs).cpu().numpy()\n            embs.append(emb)\n            ids.extend(image_ids)\n\n    return np.vstack(embs), np.array(ids)\n\n\n# ==========================================================\n# 4) CARGAR MODELO VIT-B/16 DESDE PESOS LOCALES\n# ==========================================================\n\nweights_path = \"/kaggle/input/vit-b16-weights/vit_b_16_weights.pth\"\nstate_dict = torch.load(weights_path, map_location=device)\n\nmodel = vit_b_16(weights=None).to(device)\nmodel.load_state_dict(state_dict)\nmodel.heads = torch.nn.Identity()\nmodel.eval()\n\n\n# ==========================================================\n# 5) EXTRAER EMBEDDINGS DEL TRAIN (df_wide_train)\n# ==========================================================\n\ntrain_img_dir = \"/kaggle/input/csiro-biomass/train\"\n\nemb_train, ids_train = extract_embeddings(\n    df_wide_train,\n    train_img_dir,\n    model\n)\n\nnp.save(\"embeddings_train.npy\", emb_train)\nnp.save(\"ids_train.npy\", ids_train)\n\nprint(\"✔ Embeddings TRAIN:\", emb_train.shape)   # (357, 768)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:24:10.687785Z","iopub.execute_input":"2025-11-19T05:24:10.688094Z","iopub.status.idle":"2025-11-19T05:26:29.288084Z","shell.execute_reply.started":"2025-11-19T05:24:10.688067Z","shell.execute_reply":"2025-11-19T05:26:29.286965Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Embeddings de TRAIN\nembeddings_tr = pd.DataFrame(emb_train)\nembeddings_tr.insert(0, \"image_id\", ids_train)\ndisplay(embeddings_tr.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:26:41.026575Z","iopub.execute_input":"2025-11-19T05:26:41.027611Z","iopub.status.idle":"2025-11-19T05:26:41.050026Z","shell.execute_reply.started":"2025-11-19T05:26:41.027579Z","shell.execute_reply":"2025-11-19T05:26:41.048782Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---- TRAIN ----\n# separar la columna id\nimage_ids_tr = embeddings_tr[\"image_id\"].copy()\n# embeddings puros\nemb_only_tr = embeddings_tr.drop(columns=[\"image_id\"])\n# renombrar columnas 0..767\nemb_cols = [f\"emb_{i}\" for i in range(emb_only_tr.shape[1])]\nemb_only_tr.columns = emb_cols\n# reconstruir DF ordenado\nembeddings_tr_clean = pd.concat([image_ids_tr, emb_only_tr], axis=1)\ndisplay(embeddings_tr_clean.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:26:49.454499Z","iopub.execute_input":"2025-11-19T05:26:49.454802Z","iopub.status.idle":"2025-11-19T05:26:49.480070Z","shell.execute_reply.started":"2025-11-19T05:26:49.454780Z","shell.execute_reply":"2025-11-19T05:26:49.478766Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(type(emb_train))\nprint(\"Forma:\", emb_train.shape)\nprint(\"Tipo de datos:\", emb_train.dtype)\n\nplt.hist(emb_train.flatten(), bins=100)\nplt.title(\"Distribución de valores en los embeddings\")\nplt.xlabel(\"Valor\")\nplt.ylabel(\"Frecuencia\")\nplt.show()\n\nprint(emb_train[0][:10])   # primeros 10 valores del primer embedding\nprint(emb_train[1][:10])\n\nfrom sklearn.metrics.pairwise import cosine_similarity\n\nsim = cosine_similarity([emb_train[0]], [emb_train[1]])[0][0]\nprint(\"Similitud entre imagen 0 y 1:\", sim)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:26:52.467411Z","iopub.execute_input":"2025-11-19T05:26:52.467696Z","iopub.status.idle":"2025-11-19T05:26:52.871221Z","shell.execute_reply.started":"2025-11-19T05:26:52.467676Z","shell.execute_reply":"2025-11-19T05:26:52.870224Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.manifold import TSNE\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport pandas as pd\n\n# =============================\n# 1) t-SNE en 2 dimensiones\n# =============================\n\ntsne = TSNE(\n    n_components=2,\n    perplexity=30,       # prueba luego 20, 40, 50\n    learning_rate=200,\n    n_iter=1500,\n    verbose=1,\n    random_state=42\n)\n\nemb_tsne = tsne.fit_transform(emb_train)\n\n# Crear DataFrame con resultados\ndf_tsne = pd.DataFrame({\n    \"TSNE1\": emb_tsne[:, 0],\n    \"TSNE2\": emb_tsne[:, 1],\n    \"image_id\": ids_train\n})\n\n# Añadir las columnas del train pivotado (biomasa)\ndf_tsne = df_tsne.merge(df_wide_train, on=\"image_id\", how=\"left\")\n\n# =============================\n# 2) Visualizar coloreando por biomasa total\n# =============================\n\nplt.figure(figsize=(10, 8))\nsns.scatterplot(\n    data=df_tsne,\n    x=\"TSNE1\",\n    y=\"TSNE2\",\n    hue=\"Dry_Total_g\",\n    palette=\"viridis\",\n    s=60,\n    alpha=0.8\n)\nplt.title(\"t-SNE Embeddings ViT coloreado por Dry_Total_g\")\nplt.show()\n\n# =============================\n# 3) Visualizar coloreando por Species\n# =============================\n\nplt.figure(figsize=(12, 9))\nsns.scatterplot(\n    data=df_tsne,\n    x=\"TSNE1\",\n    y=\"TSNE2\",\n    hue=\"Species\",\n    palette=\"tab20\",\n    s=50,\n    alpha=0.9,\n    legend=False  # demasiadas especies para mostrar\n)\nplt.title(\"t-SNE Embeddings ViT coloreado por Species\")\nplt.show()\n\n# =============================\n# 4) Visualizar por State\n# =============================\n\nplt.figure(figsize=(10, 8))\nsns.scatterplot(\n    data=df_tsne,\n    x=\"TSNE1\",\n    y=\"TSNE2\",\n    hue=\"State\",\n    palette=\"Set2\",\n    s=60\n)\nplt.title(\"t-SNE Embeddings ViT coloreado por State\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:27:04.398246Z","iopub.execute_input":"2025-11-19T05:27:04.398535Z","iopub.status.idle":"2025-11-19T05:27:08.599650Z","shell.execute_reply.started":"2025-11-19T05:27:04.398517Z","shell.execute_reply":"2025-11-19T05:27:08.598407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.cluster import KMeans\n# KMeans en 768 dimensiones\nk = 4\nkmeans = KMeans(n_clusters=k, random_state=42)\nclusters = kmeans.fit_predict(emb_train)\n\ndf_cluster = pd.DataFrame({\n    \"image_id\": ids_train,\n    \"cluster\": clusters\n})\n\nmerged = df_cluster.merge(df_wide_train, on=\"image_id\")\n\n# Boxplot\nplt.figure(figsize=(10,6))\nsns.boxplot(data=merged, x=\"cluster\", y=\"Dry_Total_g\", palette=\"Set3\")\nplt.title(\"Distribución de Dry_Total_g por cluster (KMeans 768D)\")\nplt.show()\n\n# Heatmap\ncluster_stats = merged.groupby(\"cluster\")[[\n    \"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"\n]].mean()\n\nplt.figure(figsize=(8,5))\nsns.heatmap(cluster_stats, annot=True, fmt=\".1f\", cmap=\"viridis\")\nplt.title(\"Promedio de biomasa por cluster (KMeans 768D)\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:27:18.536408Z","iopub.execute_input":"2025-11-19T05:27:18.536727Z","iopub.status.idle":"2025-11-19T05:27:19.270630Z","shell.execute_reply.started":"2025-11-19T05:27:18.536704Z","shell.execute_reply":"2025-11-19T05:27:19.269516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tr_final = pd.concat([df_encoded, embeddings_tr_clean], axis=1)\ndisplay(tr_final.head(357))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:27:35.820588Z","iopub.execute_input":"2025-11-19T05:27:35.821389Z","iopub.status.idle":"2025-11-19T05:27:35.852034Z","shell.execute_reply.started":"2025-11-19T05:27:35.821359Z","shell.execute_reply":"2025-11-19T05:27:35.851089Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Guardar nombres de columnas de entrada ANTES de convertir a NumPy\nfeature_cols = tr_final.drop(columns=[\n    'image_path','image_id','sample_id','Sampling_Date','State','Species',\n    'Pre_GSHH_NDVI','Height_Ave_cm','Dry_Clover_g','Dry_Dead_g',\n    'Dry_Green_g','Dry_Total_g','GDM_g','day_of_year',\n    'sin_date','cos_date','State_encoded','Species_encoded'\n]).columns.tolist()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:27:39.029715Z","iopub.execute_input":"2025-11-19T05:27:39.030035Z","iopub.status.idle":"2025-11-19T05:27:39.038380Z","shell.execute_reply.started":"2025-11-19T05:27:39.030011Z","shell.execute_reply":"2025-11-19T05:27:39.037409Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Variables de entrada (x)\nx = tr_final[feature_cols].to_numpy()\n\n# Variables objetivo (y)\ny = tr_final[['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']].to_numpy()\n\nprint(x.shape)\nprint(y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:27:47.777388Z","iopub.execute_input":"2025-11-19T05:27:47.778490Z","iopub.status.idle":"2025-11-19T05:27:47.789792Z","shell.execute_reply.started":"2025-11-19T05:27:47.778454Z","shell.execute_reply":"2025-11-19T05:27:47.788937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(tr_final.shape)\nprint(len([c for c in tr_final.columns if c.startswith(\"emb_\")]))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T05:27:51.530843Z","iopub.execute_input":"2025-11-19T05:27:51.531195Z","iopub.status.idle":"2025-11-19T05:27:51.536760Z","shell.execute_reply.started":"2025-11-19T05:27:51.531170Z","shell.execute_reply":"2025-11-19T05:27:51.535776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"feature_cols = [c for c in tr_final.columns if c.startswith(\"emb_\")]\nprint(len(feature_cols))\nprint(feature_cols[:5])\nprint(feature_cols[-5:])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T06:51:38.108269Z","iopub.execute_input":"2025-11-19T06:51:38.108577Z","iopub.status.idle":"2025-11-19T06:51:38.114797Z","shell.execute_reply.started":"2025-11-19T06:51:38.108554Z","shell.execute_reply":"2025-11-19T06:51:38.113712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"feature_cols = [c for c in tr_final.columns if c.startswith(\"emb_\")]\n\nx = tr_final[feature_cols].values.astype(np.float32)\ny = tr_final[[\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]].values.astype(np.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T06:55:46.922096Z","iopub.execute_input":"2025-11-19T06:55:46.922516Z","iopub.status.idle":"2025-11-19T06:55:46.938083Z","shell.execute_reply.started":"2025-11-19T06:55:46.922491Z","shell.execute_reply":"2025-11-19T06:55:46.936859Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##  **Construir un modelo  RED NEURONAL  multisalida y distribucion Tweedie .**","metadata":{}},{"cell_type":"code","source":"# ======================================================\n# ⭐  DEFINIR GRUPOS USANDO SOLO Sampling_Date\n# ======================================================\n\ngroups = tr_final[\"Sampling_Date\"].astype(str)\n\n# Mostrar resumen de grupos antes de entrenar\nprint(\"Total fechas únicas:\", groups.nunique())\nprint(\"\\nTamaño por fecha:\")\nprint(groups.value_counts())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T06:32:51.934961Z","iopub.execute_input":"2025-11-19T06:32:51.935633Z","iopub.status.idle":"2025-11-19T06:32:51.949607Z","shell.execute_reply.started":"2025-11-19T06:32:51.935609Z","shell.execute_reply":"2025-11-19T06:32:51.948672Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Device:\", device)\n\n# ======================================================\n# ⭐ 1. TWEEDIE LOSS ESTABLE PARA REDES\n# ======================================================\n# Esta versión está simplificada pero funciona bien para datos >= 0.\n# ● y_pred (μ) debe ser siempre positivo → por eso usamos Softplus en la red.\n# ● p define la forma de la distribución, en biomasa típicamente 1.1–1.5.\n# ● eps evita divisiones por cero y log(0).\n\nclass TweedieLoss(nn.Module):\n    def __init__(self, p=1.3, eps=1e-6):\n        super().__init__()\n        self.p = p\n        self.eps = eps  # evita valores peligrosos como μ=0\n\n    def forward(self, y_pred, y_true):\n        # Asegurar que μ nunca sea cero ni negativo\n        y_pred = torch.clamp(y_pred, min=self.eps)\n\n        # Fórmula general del log-likelihood Tweedie (sin la constante)\n        term1 = y_true * torch.pow(y_pred, 1 - self.p) / (1 - self.p)\n        term2 = torch.pow(y_pred, 2 - self.p) / (2 - self.p)\n\n        # Se usa signo negativo porque PyTorch minimiza la pérdida\n        loss = -(term1 - term2)\n        return torch.mean(loss)\n\n\n# ======================================================\n# ⭐ 2. DEFINICIÓN DEL MLP\n# ======================================================\n# Estructura del regresor:\n# ● Varias capas densas\n# ● ReLU como activación\n# ● Dropout para regularización\n# ● Softplus al final → asegura output > 0,\n#     necesario porque la pérdida Tweedie exige μ > 0.\n\nclass MLPRegressor(nn.Module):\n    def __init__(self, input_dim, hidden=[512, 256, 128], dropout=0.2):\n        super().__init__()\n\n        layers = []\n        prev = input_dim\n\n        # Crear las capas ocultas\n        for h in hidden:\n            layers.append(nn.Linear(prev, h))\n            layers.append(nn.ReLU())        # activación no lineal\n            layers.append(nn.Dropout(dropout))  # regularización\n            prev = h\n\n        # Última capa: 5 targets del problema\n        # Softplus obliga salidas positivas (Tweedie lo necesita)\n        self.model = nn.Sequential(\n            *layers,\n            nn.Linear(prev, 5),\n            nn.Softplus()\n        )\n\n    def forward(self, x):\n        return self.model(x)\n\n\n# ======================================================\n# ⭐ 3. FUNCIÓN PARA ENTRENAR CON GroupKFold\n# ======================================================\n# Esta función:\n# ● Divide los datos por grupos (Sampling_Date + State)\n# ● Evita fuga de información entre fechas/estados\n# ● Entrena 1 modelo por fold (5 modelos)\n# ● Genera predicciones OOF\n# ● Calcula métricas RMSE y R² ponderado\n\ndef train_mlp_tweedie(x, y, groups, n_folds=4):\n    gkf = GroupKFold(n_splits=n_folds)\n\n    oof_preds = np.zeros_like(y)   # aquí se guardan todas las predicciones OOF\n    models = []\n    targets = [\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]\n\n    fold = 1\n    # ======================================================\n    # 🔄 LOOP DE VALIDACIÓN CRUZADA\n    # ======================================================\n    for train_idx, val_idx in gkf.split(x, y, groups):\n\n        print(f\"\\n===== FOLD {fold} =====\")\n\n        # Convertir numpy → tensores\n        X_train = torch.tensor(x[train_idx], dtype=torch.float32).to(device)\n        y_train = torch.tensor(y[train_idx], dtype=torch.float32).to(device)\n\n        X_val = torch.tensor(x[val_idx], dtype=torch.float32).to(device)\n        y_val = torch.tensor(y[val_idx], dtype=torch.float32).to(device)\n\n        # DataLoader para minibatches\n        train_dataset = TensorDataset(X_train, y_train)\n        train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)\n\n        # Crear modelo por fold\n        model = MLPRegressor(input_dim=x.shape[1])\n        model.to(device)\n\n        # Tweedie loss optimizada\n        loss_fn = TweedieLoss(p=1.3)\n\n        # Optimizador Adam (valor estándar y estable)\n        optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\n\n        # Reduce LR si val_loss no mejora\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n            optimizer, mode=\"min\", factor=0.5, patience=5\n        )\n\n        best_loss = 1e12         # para early stopping\n        patience = 12\n        patience_counter = 0\n\n        # ======================================================\n        # 🔥 ENTRENAMIENTO DEL FOLD\n        # ======================================================\n        for epoch in range(200):\n\n            model.train()\n            batch_losses = []\n\n            for xb, yb in train_loader:\n                optimizer.zero_grad()\n\n                # Forward\n                pred = model(xb)\n                loss = loss_fn(pred, yb)\n\n                # Backprop\n                loss.backward()\n\n                # Clip gradientes para evitar explosiones\n                torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)\n\n                optimizer.step()\n                batch_losses.append(loss.item())\n\n            train_loss = np.mean(batch_losses)\n\n            # ------------------ VALIDACIÓN ------------------\n            model.eval()\n            with torch.no_grad():\n                val_pred = model(X_val)\n                val_loss = loss_fn(val_pred, y_val).item()\n\n            # Ajustar LR\n            scheduler.step(val_loss)\n\n            if epoch % 30 == 0:\n                print(f\"Epoch {epoch} | Train Loss={train_loss:.4f} | Val Loss={val_loss:.4f}\")\n\n            # ------------------ EARLY STOPPING ------------------\n            if val_loss < best_loss:\n                best_loss = val_loss\n                patience_counter = 0\n                best_state = model.state_dict()   # guardar mejor estado\n            else:\n                patience_counter += 1\n                if patience_counter >= patience:\n                    print(\"Early stopping triggered.\")\n                    break\n\n        # Cargar mejor estado del modelo\n        model.load_state_dict(best_state)\n\n        # Guardar predicciones OOF\n        model.eval()\n        with torch.no_grad():\n            pred = model(X_val).cpu().numpy()\n        oof_preds[val_idx] = pred\n\n        # RMSE por variable\n        rmse_fold = np.sqrt(np.mean((y[val_idx] - pred)**2, axis=0))\n        print(\"RMSE por variable:\", np.round(rmse_fold, 4))\n\n        models.append(model)\n        fold += 1\n\n    # ======================================================\n    # ⭐ RMSE OOF FINAL (métrica más importante)\n    # ======================================================\n\n    rmse_oof = np.sqrt(np.mean((y - oof_preds)**2, axis=0))\n    print(\"\\n🔥 RMSE OOF final:\", np.round(rmse_oof, 4))\n\n    # ======================================================\n    # ⭐ R² ponderado global OOF\n    # ======================================================\n    # Pesos según importancia definida por la competencia\n\n    pesos = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n\n    y_true_flat = y.flatten()\n    y_pred_flat = oof_preds.flatten()\n    w = np.tile(pesos, len(y))\n\n    y_mean_w = np.sum(w * y_true_flat) / np.sum(w)\n\n    SS_res = np.sum(w * (y_true_flat - y_pred_flat)**2)\n    SS_tot = np.sum(w * (y_true_flat - y_mean_w)**2)\n\n    r2_ponderado = 1 - SS_res / SS_tot\n\n    print(\"\\n📊 R² ponderado global OOF:\", round(r2_ponderado, 4))\n\n    return rmse_oof, r2_ponderado, models\n\n\n# ======================================================\n# ⭐ 4. ENTRENAR MODELO MLP TWEEDIE\n# ======================================================\n\nrmse_oof, r2_global, models_mlp = train_mlp_tweedie(\n    x, y,\n    groups=tr_final[\"Sampling_Date\"].astype(str) + \"_\" + tr_final[\"State\"]\n)\n\n\"\"\"\ngroups = tr_final[\"Sampling_Date\"]  # <- lo correcto\n\nrmse_oof, r2_global, models_mlp = train_mlp_tweedie(\n    x, y,\n    groups=groups,\n    n_folds=28        # usar 4 porque ya verificamos que funciona mejor\n)\n\"\"\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T08:12:23.769405Z","iopub.execute_input":"2025-11-19T08:12:23.769754Z","iopub.status.idle":"2025-11-19T08:12:32.541172Z","shell.execute_reply.started":"2025-11-19T08:12:23.769731Z","shell.execute_reply":"2025-11-19T08:12:32.539877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================================================\n# ⭐ ENTRENAMIENTO FINAL DEL MODELO MLP-TWEEDIE\n# ======================================================\n\ndef train_final_mlp_tweedie(x, y, epochs=200):\n\n    X = torch.tensor(x, dtype=torch.float32).to(device)\n    Y = torch.tensor(y, dtype=torch.float32).to(device)\n\n    final_dataset = TensorDataset(X, Y)\n    final_loader = DataLoader(final_dataset, batch_size=32, shuffle=True)\n\n    model = MLPRegressor(input_dim=x.shape[1]).to(device)\n    loss_fn = TweedieLoss(p=1.3)\n    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\n\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode=\"min\", factor=0.5, patience=5\n    )\n\n    print(\"\\n🚀 Entrenando modelo FINAL con TODO el dataset...\")\n\n    for epoch in range(epochs):\n\n        model.train()\n        batch_losses = []\n\n        for xb, yb in final_loader:\n            optimizer.zero_grad()\n\n            pred = model(xb)\n            loss = loss_fn(pred, yb)\n\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)\n            optimizer.step()\n\n            batch_losses.append(loss.item())\n\n        train_loss = np.mean(batch_losses)\n\n        # step del scheduler (no hay validación, se usa train_loss)\n        scheduler.step(train_loss)\n\n        if epoch % 30 == 0:\n            print(f\"Epoch {epoch} | Train Loss={train_loss:.4f}\")\n\n    print(\"\\n✔️ Entrenamiento FINAL completado.\")\n    return model\n\n#########################################\nfinal_model = train_final_mlp_tweedie(x, y, epochs=200)\n#########################################\nsave_final = {\n    \"state_dict\": final_model.state_dict(),\n    \"input_dim\": x.shape[1],\n    \"hidden\": [512, 256, 128],\n    \"dropout\": 0.2,\n    \"feature_cols\": feature_cols\n}\n\nwith open(\"final_model_mlp_tweedie.pkl\", \"wb\") as f:\n    pickle.dump(save_final, f)\n\nprint(\"📁 Guardado: final_model_mlp_tweedie.pkl\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T08:42:10.611926Z","iopub.execute_input":"2025-11-19T08:42:10.612308Z","iopub.status.idle":"2025-11-19T08:42:31.251566Z","shell.execute_reply.started":"2025-11-19T08:42:10.612285Z","shell.execute_reply":"2025-11-19T08:42:31.250357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport pickle\nfrom PIL import Image\nfrom tqdm import tqdm\n\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models import vit_b_16\nfrom torchvision import transforms\n\n# ============================================================\n# 1) Cargar test.csv del concurso\n# ============================================================\ntest_path = \"/kaggle/input/csiro-biomass/test.csv\"\ntest_df = pd.read_csv(test_path)\n\n# sample_id = \"IDxxxx__Dry_Dead_g\"  → image_id = \"IDxxxx\"\ntest_df[\"image_id\"] = test_df[\"sample_id\"].str.split(\"__\").str[0]\n\nprint(\"test_df shape:\", test_df.shape)\nprint(test_df.head())\n\n# ============================================================\n# 2) Preparar lista de imágenes ÚNICAS\n# ============================================================\nimages_df = (\n    test_df[[\"image_id\", \"image_path\"]]\n    .drop_duplicates()\n    .reset_index(drop=True)\n)\n\nprint(\"\\nUnique images in test:\", len(images_df))\nprint(images_df.head())\n\n# ============================================================\n# 3) Definir Dataset + DataLoader para el TEST\n# ============================================================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ntransform = transforms.Compose([\n    transforms.Resize(232),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\nclass TestImageDataset(Dataset):\n    def __init__(self, df, root_dir, transform=None):\n        self.df = df\n        self.root_dir = root_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = row[\"image_id\"]\n        # image_path en CSV tiene algo como: \"test/IDxxxx.jpg\"\n        rel_path = row[\"image_path\"]\n        img_path = os.path.join(self.root_dir, rel_path)  # /kaggle/input/csiro-biomass/test/IDxxxx.jpg\n\n        img = Image.open(img_path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        return img, image_id\n\ntest_img_root = \"/kaggle/input/csiro-biomass\"\n\ntest_dataset = TestImageDataset(images_df, test_img_root, transform=transform)\ntest_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=2)\n\n# ============================================================\n# 4) Cargar ViT + pesos y extraer embeddings del TEST\n# ============================================================\nweights_path = \"/kaggle/input/vit-b16-weights/vit_b_16_weights.pth\"\nstate_dict = torch.load(weights_path, map_location=device)\n\nvit_model = vit_b_16(weights=None).to(device)\nvit_model.load_state_dict(state_dict)\nvit_model.heads = torch.nn.Identity()\nvit_model.eval()\n\nall_emb = []\nall_ids = []\n\nprint(\"\\n🔄 Extrayendo embeddings del TEST...\")\nwith torch.no_grad():\n    for batch_imgs, batch_ids in tqdm(test_loader):\n        batch_imgs = batch_imgs.to(device)\n        emb = vit_model(batch_imgs).cpu().numpy()  # (batch_size, 768)\n        all_emb.append(emb)\n        all_ids.extend(batch_ids)\n\nemb_test = np.vstack(all_emb)  # (num_imagenes_test, 768)\nprint(\"emb_test shape:\", emb_test.shape)\nprint(\"len(all_ids):\", len(all_ids))\n# ============================================================\n# ============================================================\n# 🟦 INSERTAR AQUÍ LA DEFINICIÓN DEL MODELO\n# ============================================================\n\nclass MLPRegressor(nn.Module):\n    def __init__(self, input_dim, hidden=[512, 256, 128], dropout=0.2):\n        super().__init__()\n        layers = []\n        prev = input_dim\n        for h in hidden:\n            layers.append(nn.Linear(prev, h))\n            layers.append(nn.ReLU())\n            layers.append(nn.Dropout(dropout))\n            prev = h\n        self.model = nn.Sequential(\n            *layers,\n            nn.Linear(prev, 5),\n            nn.Softplus()\n        )\n    def forward(self, x):\n        return self.model(x)\n\n# (Opcional pero recomendado)\nclass TweedieLoss(nn.Module):\n    def __init__(self, p=1.3, eps=1e-6):\n        super().__init__()\n        self.p = p\n        self.eps = eps\n    def forward(self, y_pred, y_true):\n        y_pred = torch.clamp(y_pred, min=self.eps)\n        term1 = y_true * torch.pow(y_pred, 1 - self.p) / (1 - self.p)\n        term2 = torch.pow(y_pred, 2 - self.p) / (2 - self.p)\n        return torch.mean(-(term1 - term2))\n# ============================================================\n# 5) Cargar modelo final guardado\n# ============================================================\nwith open(\"/kaggle/input/final-model-mlp-tweedie/final_model_mlp_tweedie.pkl\", \"rb\") as f:\n    saved = pickle.load(f)\n\nmodel = MLPRegressor(\n    input_dim=saved[\"input_dim\"],\n    hidden=saved[\"hidden\"],\n    dropout=saved[\"dropout\"]\n).to(device)\n\nmodel.load_state_dict(saved[\"state_dict\"])\nmodel.eval()\n\n\n# ============================================================\n# 6) Convertir embeddings → DataFrame con nombres correctos\n# ============================================================\nfeature_cols = saved[\"feature_cols\"]  # 768 columnas\n\ndf_img_emb = pd.DataFrame(emb_test, columns=feature_cols)\ndf_img_emb[\"image_id\"] = all_ids\n\n# Muy importante: ordenar para que predicción corresponda al sample_id\ndf_img_emb = df_img_emb.sort_values(\"image_id\").reset_index(drop=True)\n\nprint(\"\\nEmbeddings TEST con image_id:\")\nprint(df_img_emb.head())\n\n\n\n# ============================================================\n# 7) Predicción directa con el modelo final \n# ============================================================\n\nX_test = torch.tensor(df_img_emb[feature_cols].values, dtype=torch.float32).to(device)\n\nwith torch.no_grad():\n    preds_final = model(X_test).cpu().numpy()\n\n#############################################################\n\n\n# ============================================================\n# 🔧 Definir nombres de los 5 targets en el orden EXACTO del modelo\n# ============================================================\ntargets = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n\n\nimg_pred_df = pd.DataFrame(preds_final, columns=targets)\nimg_pred_df[\"image_id\"] = df_img_emb[\"image_id\"].values\n\n# ============================================================\n# 8) Unir con test_df y construir submission\n# ============================================================\n# Merge largo: cada fila de test_df (imagen, target_name)\n# hereda las columnas de predicción de su image_id\nmerged = test_df.merge(img_pred_df, on=\"image_id\", how=\"left\")\n\n# Para cada fila, seleccionar la columna del target correcto\ndef pick_target(row):\n    return row[row[\"target_name\"]]\n\nmerged[\"target\"] = merged.apply(pick_target, axis=1)\n\n# Comprobar NaNs\nna_count = merged[\"target\"].isna().sum()\nprint(\"\\nNúmero de NaNs en 'target':\", na_count)\nassert na_count == 0, \"⚠️ Hay NaNs en las predicciones, revisar el merge.\"\n\n# Construir submission final\nsubmission = merged[[\"sample_id\", \"target\"]].copy()\n\nprint(\"\\n✅ Ejemplo de submission:\")\nprint(submission.head())\n\nprint(\"\\nsubmission shape:\", submission.shape)\n\n# Guardar CSV\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"\\n🎉 submission.csv guardado correctamente.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T10:07:31.176153Z","iopub.execute_input":"2025-11-19T10:07:31.181041Z","iopub.status.idle":"2025-11-19T10:07:35.211127Z","shell.execute_reply.started":"2025-11-19T10:07:31.180928Z","shell.execute_reply":"2025-11-19T10:07:35.209688Z"}},"outputs":[],"execution_count":null}]}