Spaces:
Sleeping
Sleeping
File size: 21,055 Bytes
105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 2c6df6e 2ad1b46 2c6df6e 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 105dff5 2ad1b46 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
"""
GeoAI Explorer - Streamlit App (datos reales)
================================================
Clasificacion de vegetacion y cobertura del suelo a partir de datos reales de
teledeteccion Landsat MSS.
Dataset historico real: Statlog (Landsat Satellite), UCI Machine Learning Repository (1993)
- 6,435 pixeles reales extraidos de imagenes satelitales Landsat MSS
- Cada fila = vecindario de 3x3 pixeles x 4 bandas espectrales (2 visibles + 2 infrarrojo cercano)
- Etiqueta de cobertura del suelo del pixel central, asignada por fotointerpretacion experta
- Fuente: Ashwin Srinivasan (1993), University of Strathclyde / UCI ML Repository
https://archive.ics.uci.edu/dataset/146/statlog+landsat+satellite
El archivo CSV real (statlog_landsat_satellite.csv) se incluye junto a esta app.
Arquitectura: igual a los dashboards anteriores -> entrenamiento on-demand guardado
en st.session_state, sin .joblib persistido en disco.
"""
import os
import warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import streamlit as st
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
roc_auc_score, roc_curve, confusion_matrix, classification_report,
)
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
warnings.filterwarnings("ignore")
st.set_page_config(
page_title="GeoAI Explorer - Vegetacion y Cobertura del Suelo",
page_icon="\U0001F30D",
layout="wide",
initial_sidebar_state="expanded",
)
st.markdown("""
<style>
.main-header { font-size: 2.3rem; font-weight: 800; color: #14532D; margin-bottom: 0.2rem; }
.sub-header { font-size: 1rem; color: #555; margin-bottom: 1.5rem; }
.section-title {
font-size: 1.3rem; font-weight: 700; color: #1B3A4B;
border-bottom: 2px solid #2E86AB; padding-bottom: 4px; margin-bottom: 1rem;
}
.info-box {
background: #EAF4FB; border: 1px solid #AED6F1; border-radius: 6px;
padding: 0.8rem 1rem; margin-bottom: 1rem; font-size: 0.9rem;
}
</style>
""", unsafe_allow_html=True)
RANDOM_STATE = 42
DATA_PATH = os.path.join(os.path.dirname(__file__), "statlog_landsat_satellite.csv")
CLASS_NAMES = {
1: "Suelo rojo", 2: "Cultivo de algodon", 3: "Suelo gris",
4: "Suelo gris humedo", 5: "Suelo con rastrojo vegetal", 7: "Suelo gris muy humedo",
}
VEGETATION_CLASSES = {2, 5} # clases con presencia de vegetacion/cultivo
CENTER_BANDS = ["px5_b1", "px5_b2", "px5_b3", "px5_b4"] # pixel central del vecindario 3x3
ALL_PIXEL_COLS = [f"px{p}_b{b}" for p in range(1, 10) for b in range(1, 5)]
# ----------------------------------------------------------------------------
# Carga de datos reales (con respaldo sintetico solo si el archivo no esta)
# ----------------------------------------------------------------------------
@st.cache_data(show_spinner=False)
def load_landsat_data(file_bytes=None):
if file_bytes is not None:
import io
return pd.read_csv(io.BytesIO(file_bytes)), "CSV subido por el usuario"
if os.path.exists(DATA_PATH):
return pd.read_csv(DATA_PATH), "Statlog (Landsat Satellite) - UCI ML Repository (datos reales, 1993)"
return generate_synthetic_fallback(), "Generador sintetico de respaldo (archivo no encontrado)"
def generate_synthetic_fallback(n=3000, seed=RANDOM_STATE):
rng = np.random.default_rng(seed)
classes = rng.choice([1, 2, 3, 4, 5, 7], n, p=[0.24, 0.11, 0.21, 0.10, 0.11, 0.23])
rows = []
for c in classes:
base = {1: 100, 2: 70, 3: 90, 4: 80, 5: 75, 7: 85}[c]
row = {f"px{p}_b{b}": int(np.clip(rng.normal(base, 15), 0, 255)) for p in range(1, 10) for b in range(1, 5)}
row["class_code"] = c
rows.append(row)
return pd.DataFrame(rows)
# ----------------------------------------------------------------------------
# Ingenieria de caracteristicas
# ----------------------------------------------------------------------------
def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
for b in range(1, 5):
cols_b = [f"px{p}_b{b}" for p in range(1, 10)]
out[f"mean_b{b}"] = out[cols_b].mean(axis=1)
out[f"std_b{b}"] = out[cols_b].std(axis=1)
# Indice tipo NDVI aproximado usando bandas 4 (NIR) y 3 (rojo) del Landsat MSS
out["pseudo_ndvi"] = (out["px5_b4"] - out["px5_b3"]) / (out["px5_b4"] + out["px5_b3"] + 1e-3)
out["brightness"] = out[CENTER_BANDS].sum(axis=1)
out["vegetation"] = out["class_code"].isin(VEGETATION_CLASSES).astype(int)
out["class_name"] = out["class_code"].map(CLASS_NAMES)
return out
ENGINEERED_COLS = ["mean_b1", "mean_b2", "mean_b3", "mean_b4", "std_b1", "std_b2", "std_b3", "std_b4", "pseudo_ndvi", "brightness"]
VEG_FEATURES = CENTER_BANDS + ENGINEERED_COLS
LC_FEATURES = CENTER_BANDS + ENGINEERED_COLS
@st.cache_resource(show_spinner=False)
def train_vegetation_model(_df_fe: pd.DataFrame, n_estimators: int, test_size: float):
X, y = _df_fe[VEG_FEATURES], _df_fe["vegetation"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=RANDOM_STATE, stratify=y)
scaler = StandardScaler()
X_train_s, X_test_s = scaler.fit_transform(X_train), scaler.transform(X_test)
model = RandomForestClassifier(n_estimators=n_estimators, max_depth=12, class_weight="balanced", random_state=RANDOM_STATE, n_jobs=-1)
model.fit(X_train_s, y_train)
y_pred, y_prob = model.predict(X_test_s), model.predict_proba(X_test_s)[:, 1]
metrics = {
"acc": accuracy_score(y_test, y_pred), "precision": precision_score(y_test, y_pred),
"recall": recall_score(y_test, y_pred), "f1": f1_score(y_test, y_pred),
"roc_auc": roc_auc_score(y_test, y_prob), "cm": confusion_matrix(y_test, y_pred),
"report": classification_report(y_test, y_pred, target_names=["Sin vegetacion", "Con vegetacion"]),
"fpr_tpr": roc_curve(y_test, y_prob),
}
gini = pd.Series(model.feature_importances_, index=VEG_FEATURES).sort_values(ascending=False)
perm = permutation_importance(model, X_test_s, y_test, n_repeats=10, random_state=RANDOM_STATE, n_jobs=-1)
perm_imp = pd.Series(perm.importances_mean, index=VEG_FEATURES).sort_values(ascending=False)
return model, scaler, metrics, gini, perm_imp
@st.cache_resource(show_spinner=False)
def train_landcover_model(_df_fe: pd.DataFrame, n_estimators: int, test_size: float):
X, y = _df_fe[LC_FEATURES], _df_fe["class_name"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=RANDOM_STATE, stratify=y)
scaler = StandardScaler()
X_train_s, X_test_s = scaler.fit_transform(X_train), scaler.transform(X_test)
model = RandomForestClassifier(n_estimators=n_estimators, max_depth=14, class_weight="balanced", random_state=RANDOM_STATE, n_jobs=-1)
model.fit(X_train_s, y_train)
y_pred = model.predict(X_test_s)
metrics = {
"acc": accuracy_score(y_test, y_pred), "f1_macro": f1_score(y_test, y_pred, average="macro"),
"f1_weighted": f1_score(y_test, y_pred, average="weighted"),
"cm": confusion_matrix(y_test, y_pred, labels=model.classes_), "labels": model.classes_,
"report": classification_report(y_test, y_pred),
}
gini = pd.Series(model.feature_importances_, index=LC_FEATURES).sort_values(ascending=False)
perm = permutation_importance(model, X_test_s, y_test, n_repeats=10, random_state=RANDOM_STATE, n_jobs=-1, scoring="f1_macro")
perm_imp = pd.Series(perm.importances_mean, index=LC_FEATURES).sort_values(ascending=False)
return model, scaler, metrics, gini, perm_imp
def predict_pixel(model, scaler, features, values: dict):
row = pd.DataFrame([values])[features]
row_s = scaler.transform(row)
probs = model.predict_proba(row_s)[0]
return dict(zip(model.classes_, probs))
# ----------------------------------------------------------------------------
# Sidebar
# ----------------------------------------------------------------------------
with st.sidebar:
st.markdown("## \u2699\ufe0f Configuracion")
st.markdown("### \U0001F4C2 Fuente de datos")
uploaded = st.file_uploader(
"Sube un CSV alternativo (opcional)", type=["csv"],
help="Si no subes nada, se usa el archivo real incluido statlog_landsat_satellite.csv "
"(Statlog Landsat Satellite, UCI ML Repository, 1993)."
)
raw_df, data_source = load_landsat_data(uploaded.read() if uploaded else None)
df_fe = engineer_features(raw_df)
st.markdown("### \U0001F916 Parametros del modelo")
n_trees = st.slider("Arboles del Bosque Aleatorio", 50, 300, 200, step=25)
test_frac = st.slider("Fraccion de test", 0.1, 0.4, 0.25, step=0.05)
train_btn = st.button("\U0001F680 Entrenar ambos modelos", width='stretch', type="primary")
for key in ["veg_model", "veg_scaler", "veg_metrics", "veg_gini", "veg_perm",
"lc_model", "lc_scaler", "lc_metrics", "lc_gini", "lc_perm"]:
if key not in st.session_state:
st.session_state[key] = None
if train_btn:
with st.spinner("Entrenando modelo de vegetacion..."):
m, s, met, gi, pi = train_vegetation_model(df_fe, n_trees, test_frac)
st.session_state.update(veg_model=m, veg_scaler=s, veg_metrics=met, veg_gini=gi, veg_perm=pi)
with st.spinner("Entrenando modelo de cobertura del suelo..."):
m2, s2, met2, gi2, pi2 = train_landcover_model(df_fe, n_trees, test_frac)
st.session_state.update(lc_model=m2, lc_scaler=s2, lc_metrics=met2, lc_gini=gi2, lc_perm=pi2)
st.success("Modelos entrenados correctamente.")
# ----------------------------------------------------------------------------
# Header
# ----------------------------------------------------------------------------
st.markdown('<p class="main-header">\U0001F30D GeoAI Explorer: Vegetacion y Cobertura del Suelo</p>', unsafe_allow_html=True)
st.markdown(
f'<p class="sub-header">Clasificacion de pixeles satelitales Landsat MSS | '
f'Fuente: <b>{data_source}</b> | {len(df_fe):,} pixeles reales</p>',
unsafe_allow_html=True,
)
tab1, tab2, tab3, tab4, tab5 = st.tabs([
"\U0001F4CA EDA", "\U0001F33F Clasificacion de vegetacion",
"\U0001F5FA Cobertura del suelo", "\U0001F52E Predictor de pixel",
"\U0001F9E0 IA Explicable",
])
# ----------------------------------------------------------------------------
# TAB 1: EDA
# ----------------------------------------------------------------------------
with tab1:
st.markdown('<p class="section-title">Analisis Exploratorio de Datos</p>', unsafe_allow_html=True)
c1, c2, c3, c4 = st.columns(4)
c1.metric("Pixeles totales", f"{len(df_fe):,}")
c2.metric("% con vegetacion", f"{df_fe['vegetation'].mean()*100:.1f}%")
c3.metric("Clases de cobertura", df_fe["class_name"].nunique())
c4.metric("Pseudo-NDVI promedio", f"{df_fe['pseudo_ndvi'].mean():.3f}")
col_a, col_b = st.columns(2)
with col_a:
st.markdown("**Distribucion de clases de cobertura del suelo (reales)**")
fig, ax = plt.subplots(figsize=(6, 4))
order = df_fe["class_name"].value_counts().index
sns.countplot(data=df_fe, y="class_name", order=order, color="#2E86AB", ax=ax)
ax.set_xlabel("Cantidad de pixeles"); ax.set_ylabel("")
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
with col_b:
st.markdown("**Pseudo-NDVI por clase de cobertura**")
fig, ax = plt.subplots(figsize=(6, 4))
sns.boxplot(data=df_fe, x="class_name", y="pseudo_ndvi", order=order, color="#74C69D", ax=ax)
ax.tick_params(axis="x", rotation=25); ax.set_xlabel("")
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
st.markdown("**Reflectancia promedio (pixel central) por banda Landsat MSS, segun clase**")
melted = df_fe.melt(id_vars="class_name", value_vars=CENTER_BANDS, var_name="banda", value_name="reflectancia")
fig, ax = plt.subplots(figsize=(12, 4.5))
sns.boxplot(data=melted, x="banda", y="reflectancia", hue="class_name", ax=ax)
ax.legend(bbox_to_anchor=(1.01, 1), loc="upper left", fontsize=8)
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
st.markdown("**Matriz de correlacion: bandas centrales, estadisticas del vecindario e indices**")
fig, ax = plt.subplots(figsize=(9, 6))
corr = df_fe[CENTER_BANDS + ENGINEERED_COLS + ["vegetation"]].corr()
sns.heatmap(corr, annot=True, fmt=".2f", cmap="RdBu_r", center=0, ax=ax)
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
with st.expander("Vista previa de los datos reales"):
st.dataframe(df_fe.head(200), width='stretch')
# ----------------------------------------------------------------------------
# TAB 2: Vegetacion
# ----------------------------------------------------------------------------
with tab2:
st.markdown('<p class="section-title">Modelo de Clasificacion de Vegetacion (binario)</p>', unsafe_allow_html=True)
st.caption("Clase positiva = pixeles fotointerpretados como cultivo de algodon o suelo con rastrojo vegetal.")
if st.session_state.veg_metrics is None:
st.markdown('<div class="info-box">Presiona <b>Entrenar ambos modelos</b> en la barra lateral para ver resultados.</div>', unsafe_allow_html=True)
else:
met = st.session_state.veg_metrics
c1, c2, c3, c4, c5 = st.columns(5)
c1.metric("Accuracy", f"{met['acc']*100:.1f}%")
c2.metric("Precision", f"{met['precision']*100:.1f}%")
c3.metric("Recall", f"{met['recall']*100:.1f}%")
c4.metric("F1-score", f"{met['f1']*100:.1f}%")
c5.metric("ROC AUC", f"{met['roc_auc']:.3f}")
col_a, col_b = st.columns(2)
with col_a:
fig, ax = plt.subplots(figsize=(5, 4.5))
sns.heatmap(met["cm"], annot=True, fmt="d", cmap="Greens", ax=ax,
xticklabels=["Sin veg.", "Con veg."], yticklabels=["Sin veg.", "Con veg."])
ax.set_xlabel("Prediccion"); ax.set_ylabel("Real"); ax.set_title("Matriz de confusion")
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
with col_b:
fpr, tpr, _ = met["fpr_tpr"]
fig, ax = plt.subplots(figsize=(5, 4.5))
ax.plot(fpr, tpr, color="#2E7D32", linewidth=2, label=f"AUC = {met['roc_auc']:.3f}")
ax.plot([0, 1], [0, 1], linestyle="--", color="gray", label="Azar")
ax.set_xlabel("Falsos positivos"); ax.set_ylabel("Verdaderos positivos"); ax.set_title("Curva ROC"); ax.legend()
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
with st.expander("Reporte de clasificacion completo"):
st.text(met["report"])
# ----------------------------------------------------------------------------
# TAB 3: Cobertura del suelo
# ----------------------------------------------------------------------------
with tab3:
st.markdown('<p class="section-title">Modelo de Cobertura del Suelo (multiclase, 6 clases reales)</p>', unsafe_allow_html=True)
if st.session_state.lc_metrics is None:
st.markdown('<div class="info-box">Presiona <b>Entrenar ambos modelos</b> en la barra lateral para ver resultados.</div>', unsafe_allow_html=True)
else:
met = st.session_state.lc_metrics
c1, c2, c3 = st.columns(3)
c1.metric("Accuracy", f"{met['acc']*100:.1f}%")
c2.metric("F1-macro", f"{met['f1_macro']*100:.1f}%")
c3.metric("F1-weighted", f"{met['f1_weighted']*100:.1f}%")
fig, ax = plt.subplots(figsize=(7, 6))
sns.heatmap(met["cm"], annot=True, fmt="d", cmap="Blues", ax=ax,
xticklabels=met["labels"], yticklabels=met["labels"])
ax.set_xlabel("Prediccion"); ax.set_ylabel("Real"); ax.tick_params(axis="x", rotation=25)
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
with st.expander("Reporte de clasificacion completo"):
st.text(met["report"])
# ----------------------------------------------------------------------------
# TAB 4: Predictor de pixel
# ----------------------------------------------------------------------------
with tab4:
st.markdown('<p class="section-title">Predictor interactivo de pixel</p>', unsafe_allow_html=True)
if st.session_state.veg_model is None:
st.markdown('<div class="info-box">Entrena los modelos primero en la barra lateral.</div>', unsafe_allow_html=True)
else:
st.markdown("Ajusta las 4 bandas espectrales del pixel central (escala Landsat MSS, 0-255):")
c1, c2, c3, c4 = st.columns(4)
b1 = c1.slider("Banda 1 (verde)", 0, 255, 90)
b2 = c2.slider("Banda 2 (rojo)", 0, 255, 110)
b3 = c3.slider("Banda 3 (NIR)", 0, 255, 110)
b4 = c4.slider("Banda 4 (NIR)", 0, 255, 90)
vals = {"px5_b1": b1, "px5_b2": b2, "px5_b3": b3, "px5_b4": b4}
for b, v in zip(range(1, 5), [b1, b2, b3, b4]):
vals[f"mean_b{b}"] = v
vals[f"std_b{b}"] = 5.0
vals["pseudo_ndvi"] = (b4 - b3) / (b4 + b3 + 1e-3)
vals["brightness"] = b1 + b2 + b3 + b4
if st.button("\U0001F52E Predecir", type="primary"):
veg_probs = predict_pixel(st.session_state.veg_model, st.session_state.veg_scaler, VEG_FEATURES, vals)
lc_probs = predict_pixel(st.session_state.lc_model, st.session_state.lc_scaler, LC_FEATURES, vals)
col_r1, col_r2 = st.columns(2)
with col_r1:
st.markdown("**Prediccion: presencia de vegetacion**")
st.metric("Probabilidad de vegetacion", f"{veg_probs.get(1, 0)*100:.1f}%")
st.progress(min(int(veg_probs.get(1, 0)*100), 100))
with col_r2:
st.markdown("**Prediccion: tipo de cobertura del suelo**")
pred_lc = max(lc_probs, key=lc_probs.get)
st.metric("Clase mas probable", pred_lc)
for cls, p in sorted(lc_probs.items(), key=lambda x: -x[1]):
st.write(f"{cls}: {p*100:.1f}%")
# ----------------------------------------------------------------------------
# TAB 5: IA Explicable
# ----------------------------------------------------------------------------
with tab5:
st.markdown('<p class="section-title">IA Explicable: Importancia de Variables</p>', unsafe_allow_html=True)
if st.session_state.veg_gini is None:
st.markdown('<div class="info-box">Entrena los modelos primero en la barra lateral.</div>', unsafe_allow_html=True)
else:
st.markdown("#### Clasificacion de vegetacion")
col_a, col_b = st.columns(2)
with col_a:
fig, ax = plt.subplots(figsize=(6, 5))
colors = plt.cm.Greens_r(np.linspace(0.2, 0.8, len(st.session_state.veg_gini)))
st.session_state.veg_gini.plot(kind="barh", ax=ax, color=colors)
ax.invert_yaxis(); ax.set_xlabel("Importancia (impureza)")
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
with col_b:
fig, ax = plt.subplots(figsize=(6, 5))
colors = plt.cm.Oranges_r(np.linspace(0.2, 0.8, len(st.session_state.veg_perm)))
st.session_state.veg_perm.plot(kind="barh", ax=ax, color=colors)
ax.invert_yaxis(); ax.set_xlabel("Caida de F1 al permutar")
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
st.markdown("#### Cobertura del suelo")
col_c, col_d = st.columns(2)
with col_c:
fig, ax = plt.subplots(figsize=(6, 5))
colors = plt.cm.Blues_r(np.linspace(0.2, 0.8, len(st.session_state.lc_gini)))
st.session_state.lc_gini.plot(kind="barh", ax=ax, color=colors)
ax.invert_yaxis(); ax.set_xlabel("Importancia (impureza)")
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
with col_d:
fig, ax = plt.subplots(figsize=(6, 5))
colors = plt.cm.Purples_r(np.linspace(0.2, 0.8, len(st.session_state.lc_perm)))
st.session_state.lc_perm.plot(kind="barh", ax=ax, color=colors)
ax.invert_yaxis(); ax.set_xlabel("Caida de F1-macro al permutar")
fig.tight_layout(); st.pyplot(fig); plt.close(fig)
st.caption(
"Las bandas del infrarrojo cercano (banda 4) y el pseudo-NDVI derivado de ellas suelen ser "
"las variables mas relevantes para distinguir vegetacion (cultivos, rastrojo) de suelos "
"desnudos, consistente con la fisica de la teledeteccion: la vegetacion sana refleja "
"fuertemente el infrarrojo cercano."
)
st.markdown("---")
st.markdown(
"<small>GeoAI Explorer. Dataset historico real: Statlog (Landsat Satellite), "
"UCI Machine Learning Repository (1993) - archivo incluido statlog_landsat_satellite.csv.</small>",
unsafe_allow_html=True,
) |