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 &nbsp;|&nbsp; '
    f'Fuente: <b>{data_source}</b> &nbsp;|&nbsp; {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,
)