mgprz commited on
Commit
2ad1b46
·
verified ·
1 Parent(s): e67ef6c

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +497 -37
src/streamlit_app.py CHANGED
@@ -1,40 +1,500 @@
1
- import altair as alt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import numpy as np
3
  import pandas as pd
 
 
4
  import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GeoAI Explorer - Streamlit App
3
+ ================================
4
+ Clasificacion de agua y cobertura del suelo a partir de datos de teledeteccion MODIS.
5
+
6
+ Dataset historico real: nasa-cisto-data-science-group/modis-lake-powell-toy-dataset (HuggingFace)
7
+ - 7 bandas de reflectancia de superficie MODIS (MOD09GA / MOD09GQ)
8
+ - Indices espectrales: NDVI, NDWI1, NDWI2
9
+ - Etiqueta de agua/no-agua derivada del producto MOD44W de la NASA (Lake Powell)
10
+
11
+ Arquitectura: igual a los dashboards anteriores (vuelos, Mundial) -> entrenamiento on-demand
12
+ guardado en st.session_state, sin .joblib persistido en disco.
13
+ """
14
+
15
+ import warnings
16
+
17
  import numpy as np
18
  import pandas as pd
19
+ import matplotlib.pyplot as plt
20
+ import seaborn as sns
21
  import streamlit as st
22
+
23
+ from sklearn.ensemble import RandomForestClassifier
24
+ from sklearn.inspection import permutation_importance
25
+ from sklearn.metrics import (
26
+ accuracy_score, precision_score, recall_score, f1_score,
27
+ roc_auc_score, roc_curve, confusion_matrix, classification_report, f1_score,
28
+ )
29
+ from sklearn.model_selection import train_test_split
30
+ from sklearn.preprocessing import StandardScaler
31
+
32
+ warnings.filterwarnings("ignore")
33
+
34
+ st.set_page_config(
35
+ page_title="GeoAI Explorer - Agua y Cobertura del Suelo",
36
+ page_icon="\U0001F30D",
37
+ layout="wide",
38
+ initial_sidebar_state="expanded",
39
+ )
40
+
41
+ st.markdown("""
42
+ <style>
43
+ .main-header { font-size: 2.3rem; font-weight: 800; color: #14532D; margin-bottom: 0.2rem; }
44
+ .sub-header { font-size: 1rem; color: #555; margin-bottom: 1.5rem; }
45
+ .section-title {
46
+ font-size: 1.3rem; font-weight: 700; color: #1B3A4B;
47
+ border-bottom: 2px solid #2E86AB; padding-bottom: 4px; margin-bottom: 1rem;
48
+ }
49
+ .info-box {
50
+ background: #EAF4FB; border: 1px solid #AED6F1; border-radius: 6px;
51
+ padding: 0.8rem 1rem; margin-bottom: 1rem; font-size: 0.9rem;
52
+ }
53
+ </style>
54
+ """, unsafe_allow_html=True)
55
+
56
+ RANDOM_STATE = 42
57
+ DATASET_URL = "nasa-cisto-data-science-group/modis-lake-powell-toy-dataset"
58
+ LABEL_WATER = "water"
59
+ LABEL_LC = "land_cover"
60
+
61
+ BAND_COLS = [
62
+ "sur_refl_b01_1", "sur_refl_b02_1", "sur_refl_b03_1", "sur_refl_b04_1",
63
+ "sur_refl_b05_1", "sur_refl_b06_1", "sur_refl_b07_1",
64
+ ]
65
+ INDEX_COLS = ["ndvi", "ndwi1", "ndwi2"]
66
+ BASE_FEATURE_COLS = BAND_COLS + INDEX_COLS
67
+ COLS_TO_DROP = ["x_offset", "y_offset", "year", "julian_day"]
68
+
69
+ LC_PALETTE = {
70
+ "Agua": "#1B6CA8", "Vegetacion densa": "#2E7D32",
71
+ "Vegetacion escasa": "#9CCC65", "Suelo desnudo / Urbano": "#A9744F",
72
+ }
73
+
74
+
75
+ # ----------------------------------------------------------------------------
76
+ # Carga de datos: real (HuggingFace) con respaldo sintetico de mismo esquema
77
+ # ----------------------------------------------------------------------------
78
+ @st.cache_data(show_spinner=False)
79
+ def load_modis_data(file_bytes=None):
80
+ if file_bytes is not None:
81
+ import io
82
+ df = pd.read_csv(io.BytesIO(file_bytes))
83
+ return df, "CSV subido por el usuario"
84
+
85
+ try:
86
+ import datasets
87
+ ds = datasets.load_dataset(DATASET_URL, split="train")
88
+ df = pd.DataFrame(ds)
89
+ return df, "HuggingFace (datos reales MODIS Lake Powell)"
90
+ except Exception:
91
+ return generate_synthetic_modis(), "Generador sintetico de respaldo (mismo esquema MODIS)"
92
+
93
+
94
+ def generate_synthetic_modis(n_samples=6000, seed=RANDOM_STATE):
95
+ """Respaldo sin conexion: simula pixeles MODIS con la misma fisica espectral
96
+ documentada para el dataset real (agua = baja reflectancia NIR/SWIR, NDVI bajo,
97
+ NDWI alto; tierra/vegetacion = lo opuesto)."""
98
+ rng = np.random.default_rng(seed)
99
+ water = rng.binomial(1, 0.30, n_samples)
100
+
101
+ def band(mean_water, mean_land, sd):
102
+ base = np.where(water == 1, mean_water, mean_land)
103
+ return np.clip(rng.normal(base, sd, n_samples), -100, 16000).astype(np.int16)
104
+
105
+ sur_refl_b01_1 = band(900, 1800, 300)
106
+ sur_refl_b02_1 = band(1100, 3200, 500)
107
+ sur_refl_b03_1 = band(950, 1500, 250)
108
+ sur_refl_b04_1 = band(1000, 1700, 280)
109
+ sur_refl_b05_1 = band(700, 2600, 450)
110
+ sur_refl_b06_1 = band(500, 2200, 400)
111
+ sur_refl_b07_1 = band(400, 1500, 300)
112
+
113
+ ndvi = np.where(water == 1, rng.normal(-1500, 1200, n_samples), rng.normal(4500, 2000, n_samples)).clip(-20000, 20000).astype(np.int16)
114
+ ndwi1 = np.where(water == 1, rng.normal(6000, 1500, n_samples), rng.normal(-2000, 1800, n_samples)).clip(-20000, 20000).astype(np.int16)
115
+ ndwi2 = np.where(water == 1, rng.normal(5000, 1600, n_samples), rng.normal(-2500, 1700, n_samples)).clip(-20000, 20000).astype(np.int16)
116
+
117
+ return pd.DataFrame({
118
+ "x_offset": rng.integers(0, 5000, n_samples), "y_offset": rng.integers(0, 5000, n_samples),
119
+ "year": rng.choice([2018, 2019, 2020, 2021], n_samples), "julian_day": rng.integers(1, 366, n_samples),
120
+ "sur_refl_b01_1": sur_refl_b01_1, "sur_refl_b02_1": sur_refl_b02_1, "sur_refl_b03_1": sur_refl_b03_1,
121
+ "sur_refl_b04_1": sur_refl_b04_1, "sur_refl_b05_1": sur_refl_b05_1, "sur_refl_b06_1": sur_refl_b06_1,
122
+ "sur_refl_b07_1": sur_refl_b07_1, "ndvi": ndvi, "ndwi1": ndwi1, "ndwi2": ndwi2, "water": water,
123
+ })
124
+
125
+
126
+ def clean_data(df: pd.DataFrame) -> pd.DataFrame:
127
+ df = df.drop(columns=[c for c in COLS_TO_DROP if c in df.columns])
128
+ df[LABEL_WATER] = df[LABEL_WATER].astype(int)
129
+ return df.dropna(subset=BASE_FEATURE_COLS + [LABEL_WATER]).reset_index(drop=True)
130
+
131
+
132
+ def engineer_water_features(df: pd.DataFrame) -> pd.DataFrame:
133
+ out = df.copy()
134
+ out["nir_red_ratio"] = out["sur_refl_b02_1"] / (out["sur_refl_b01_1"] + 1e-3)
135
+ out["green_swir_ratio"] = out["sur_refl_b04_1"] / (out["sur_refl_b06_1"] + 1e-3)
136
+ out["brightness"] = out[BAND_COLS].sum(axis=1)
137
+ out["ndwi_minus_ndvi"] = out["ndwi1"] - out["ndvi"]
138
+ return out
139
+
140
+
141
+ WATER_ENGINEERED = ["nir_red_ratio", "green_swir_ratio", "brightness", "ndwi_minus_ndvi"]
142
+ WATER_FEATURES = BASE_FEATURE_COLS + WATER_ENGINEERED
143
+
144
+
145
+ def assign_land_cover(row):
146
+ ndvi, ndwi1 = row["ndvi"], row["ndwi1"]
147
+ if row["water"] == 1 or ndwi1 > 2000:
148
+ return "Agua"
149
+ elif ndvi > 4000:
150
+ return "Vegetacion densa"
151
+ elif ndvi > 0:
152
+ return "Vegetacion escasa"
153
+ return "Suelo desnudo / Urbano"
154
+
155
+
156
+ def engineer_land_features(df: pd.DataFrame) -> pd.DataFrame:
157
+ out = df.copy()
158
+ out[LABEL_LC] = out.apply(assign_land_cover, axis=1)
159
+ out["nir_red_ratio"] = out["sur_refl_b02_1"] / (out["sur_refl_b01_1"] + 1e-3)
160
+ out["green_swir_ratio"] = out["sur_refl_b04_1"] / (out["sur_refl_b06_1"] + 1e-3)
161
+ out["brightness"] = out[BAND_COLS].sum(axis=1)
162
+ L = 0.5
163
+ out["savi_like"] = (out["sur_refl_b02_1"] - out["sur_refl_b01_1"]) / (out["sur_refl_b02_1"] + out["sur_refl_b01_1"] + L * 10000)
164
+ return out
165
+
166
+
167
+ LAND_ENGINEERED = ["nir_red_ratio", "green_swir_ratio", "brightness", "savi_like"]
168
+ LAND_FEATURES = BASE_FEATURE_COLS + LAND_ENGINEERED
169
+
170
+
171
+ @st.cache_resource(show_spinner=False)
172
+ def train_water_model(_df_fe: pd.DataFrame, n_estimators: int, test_size: float):
173
+ X, y = _df_fe[WATER_FEATURES], _df_fe[LABEL_WATER]
174
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=RANDOM_STATE, stratify=y)
175
+ scaler = StandardScaler()
176
+ X_train_s, X_test_s = scaler.fit_transform(X_train), scaler.transform(X_test)
177
+
178
+ model = RandomForestClassifier(n_estimators=n_estimators, max_depth=12, class_weight="balanced", random_state=RANDOM_STATE, n_jobs=-1)
179
+ model.fit(X_train_s, y_train)
180
+ y_pred, y_prob = model.predict(X_test_s), model.predict_proba(X_test_s)[:, 1]
181
+
182
+ metrics = {
183
+ "acc": accuracy_score(y_test, y_pred), "precision": precision_score(y_test, y_pred),
184
+ "recall": recall_score(y_test, y_pred), "f1": f1_score(y_test, y_pred),
185
+ "roc_auc": roc_auc_score(y_test, y_prob),
186
+ "cm": confusion_matrix(y_test, y_pred), "report": classification_report(y_test, y_pred, target_names=["No-agua", "Agua"]),
187
+ "fpr_tpr": roc_curve(y_test, y_prob),
188
+ }
189
+ gini_imp = pd.Series(model.feature_importances_, index=WATER_FEATURES).sort_values(ascending=False)
190
+ perm = permutation_importance(model, X_test_s, y_test, n_repeats=10, random_state=RANDOM_STATE, n_jobs=-1)
191
+ perm_imp = pd.Series(perm.importances_mean, index=WATER_FEATURES).sort_values(ascending=False)
192
+
193
+ return model, scaler, metrics, gini_imp, perm_imp
194
+
195
+
196
+ @st.cache_resource(show_spinner=False)
197
+ def train_land_model(_df_fe: pd.DataFrame, n_estimators: int, test_size: float):
198
+ X, y = _df_fe[LAND_FEATURES], _df_fe[LABEL_LC]
199
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=RANDOM_STATE, stratify=y)
200
+ scaler = StandardScaler()
201
+ X_train_s, X_test_s = scaler.fit_transform(X_train), scaler.transform(X_test)
202
+
203
+ model = RandomForestClassifier(n_estimators=n_estimators, max_depth=14, class_weight="balanced", random_state=RANDOM_STATE, n_jobs=-1)
204
+ model.fit(X_train_s, y_train)
205
+ y_pred = model.predict(X_test_s)
206
+
207
+ metrics = {
208
+ "acc": accuracy_score(y_test, y_pred), "f1_macro": f1_score(y_test, y_pred, average="macro"),
209
+ "f1_weighted": f1_score(y_test, y_pred, average="weighted"),
210
+ "cm": confusion_matrix(y_test, y_pred, labels=model.classes_), "labels": model.classes_,
211
+ "report": classification_report(y_test, y_pred),
212
+ }
213
+ gini_imp = pd.Series(model.feature_importances_, index=LAND_FEATURES).sort_values(ascending=False)
214
+ perm = permutation_importance(model, X_test_s, y_test, n_repeats=10, random_state=RANDOM_STATE, n_jobs=-1, scoring="f1_macro")
215
+ perm_imp = pd.Series(perm.importances_mean, index=LAND_FEATURES).sort_values(ascending=False)
216
+
217
+ return model, scaler, metrics, gini_imp, perm_imp
218
+
219
+
220
+ def predict_pixel(model, scaler, features, values: dict):
221
+ row = pd.DataFrame([values])[features]
222
+ row_s = scaler.transform(row)
223
+ probs = model.predict_proba(row_s)[0]
224
+ return dict(zip(model.classes_, probs))
225
+
226
+
227
+ # ----------------------------------------------------------------------------
228
+ # Sidebar
229
+ # ----------------------------------------------------------------------------
230
+ with st.sidebar:
231
+ st.markdown("## \u2699\ufe0f Configuracion")
232
+ st.markdown("### \U0001F4C2 Fuente de datos")
233
+ uploaded = st.file_uploader(
234
+ "Sube un CSV alternativo (opcional)", type=["csv"],
235
+ help="Columnas esperadas: sur_refl_b01_1...b07_1, ndvi, ndwi1, ndwi2, water. "
236
+ "Si no subes nada, se intenta cargar el dataset real de HuggingFace "
237
+ "(MODIS Lake Powell); si no hay conexion, se usa un generador sintetico del mismo esquema."
238
+ )
239
+ raw_df, data_source = load_modis_data(uploaded.read() if uploaded else None)
240
+ df_clean = clean_data(raw_df)
241
+
242
+ st.markdown("### \U0001F916 Parametros del modelo")
243
+ n_trees = st.slider("Arboles del Bosque Aleatorio", 50, 300, 200, step=25)
244
+ test_frac = st.slider("Fraccion de test", 0.1, 0.4, 0.25, step=0.05)
245
+ train_btn = st.button("\U0001F680 Entrenar ambos modelos", width='stretch', type="primary")
246
+
247
+ for key in ["water_model", "water_scaler", "water_metrics", "water_gini", "water_perm",
248
+ "land_model", "land_scaler", "land_metrics", "land_gini", "land_perm"]:
249
+ if key not in st.session_state:
250
+ st.session_state[key] = None
251
+
252
+ df_water_fe = engineer_water_features(df_clean)
253
+ df_land_fe = engineer_land_features(df_clean)
254
+
255
+ if train_btn:
256
+ with st.spinner("Entrenando modelo de clasificacion de agua..."):
257
+ m, s, met, gi, pi = train_water_model(df_water_fe, n_trees, test_frac)
258
+ st.session_state.update(water_model=m, water_scaler=s, water_metrics=met, water_gini=gi, water_perm=pi)
259
+ with st.spinner("Entrenando modelo de cobertura del suelo..."):
260
+ m2, s2, met2, gi2, pi2 = train_land_model(df_land_fe, n_trees, test_frac)
261
+ st.session_state.update(land_model=m2, land_scaler=s2, land_metrics=met2, land_gini=gi2, land_perm=pi2)
262
+ st.success("Modelos entrenados correctamente.")
263
+
264
+ # ----------------------------------------------------------------------------
265
+ # Header
266
+ # ----------------------------------------------------------------------------
267
+ st.markdown('<p class="main-header">\U0001F30D GeoAI Explorer: Agua y Cobertura del Suelo</p>', unsafe_allow_html=True)
268
+ st.markdown(
269
+ f'<p class="sub-header">Clasificacion de pixeles satelitales MODIS &nbsp;|&nbsp; '
270
+ f'Fuente: <b>{data_source}</b> &nbsp;|&nbsp; {len(df_clean):,} pixeles</p>',
271
+ unsafe_allow_html=True,
272
+ )
273
+
274
+ tab1, tab2, tab3, tab4, tab5 = st.tabs([
275
+ "\U0001F4CA EDA", "\U0001F4A7 Clasificacion del agua",
276
+ "\U0001F33F Cobertura del suelo", "\U0001F52E Predictor de pixel",
277
+ "\U0001F9E0 IA Explicable",
278
+ ])
279
+
280
+ # ----------------------------------------------------------------------------
281
+ # TAB 1: EDA
282
+ # ----------------------------------------------------------------------------
283
+ with tab1:
284
+ st.markdown('<p class="section-title">Analisis Exploratorio de Datos</p>', unsafe_allow_html=True)
285
+
286
+ c1, c2, c3, c4 = st.columns(4)
287
+ c1.metric("Pixeles totales", f"{len(df_clean):,}")
288
+ c2.metric("% Agua", f"{df_clean[LABEL_WATER].mean()*100:.1f}%")
289
+ c3.metric("NDVI promedio", f"{df_clean['ndvi'].mean():.0f}")
290
+ c4.metric("NDWI1 promedio", f"{df_clean['ndwi1'].mean():.0f}")
291
+
292
+ col_a, col_b = st.columns(2)
293
+ with col_a:
294
+ st.markdown("**Distribucion de NDVI por clase (agua / no-agua)**")
295
+ fig, ax = plt.subplots(figsize=(6, 4))
296
+ for cls, color, label in [(0, "#A9744F", "No-agua"), (1, "#1B6CA8", "Agua")]:
297
+ sns.kdeplot(df_clean.loc[df_clean[LABEL_WATER] == cls, "ndvi"], fill=True, alpha=0.4, color=color, label=label, ax=ax)
298
+ ax.set_xlabel("NDVI"); ax.legend()
299
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
300
+
301
+ with col_b:
302
+ st.markdown("**Firma espectral: NDVI vs NDWI1**")
303
+ fig, ax = plt.subplots(figsize=(6, 4))
304
+ for cls, color, label in [(0, "#A9744F", "No-agua"), (1, "#1B6CA8", "Agua")]:
305
+ sub = df_clean[df_clean[LABEL_WATER] == cls]
306
+ ax.scatter(sub["ndvi"], sub["ndwi1"], s=8, alpha=0.4, color=color, label=label)
307
+ ax.set_xlabel("NDVI"); ax.set_ylabel("NDWI1"); ax.legend()
308
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
309
+
310
+ st.markdown("**Reflectancia de superficie por banda MODIS, segun clase**")
311
+ melted = df_clean.melt(id_vars=LABEL_WATER, value_vars=BAND_COLS, var_name="banda", value_name="reflectancia")
312
+ melted[LABEL_WATER] = melted[LABEL_WATER].map({0: "No-agua", 1: "Agua"})
313
+ fig, ax = plt.subplots(figsize=(12, 4.5))
314
+ sns.boxplot(data=melted, x="banda", y="reflectancia", hue=LABEL_WATER, palette=["#A9744F", "#1B6CA8"], ax=ax)
315
+ ax.tick_params(axis="x", rotation=15)
316
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
317
+
318
+ st.markdown("**Matriz de correlacion: bandas, indices y etiqueta de agua**")
319
+ fig, ax = plt.subplots(figsize=(9, 6))
320
+ corr = df_clean[BAND_COLS + INDEX_COLS + [LABEL_WATER]].corr()
321
+ sns.heatmap(corr, annot=True, fmt=".2f", cmap="RdBu_r", center=0, ax=ax)
322
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
323
+
324
+ with st.expander("Vista previa de los datos"):
325
+ st.dataframe(df_clean.head(200), width='stretch')
326
+
327
+ # ----------------------------------------------------------------------------
328
+ # TAB 2: Clasificacion del agua
329
+ # ----------------------------------------------------------------------------
330
+ with tab2:
331
+ st.markdown('<p class="section-title">Modelo de Clasificacion del Agua (binario)</p>', unsafe_allow_html=True)
332
+
333
+ if st.session_state.water_metrics is None:
334
+ st.markdown('<div class="info-box">Presiona <b>Entrenar ambos modelos</b> en la barra lateral para ver resultados.</div>', unsafe_allow_html=True)
335
+ else:
336
+ met = st.session_state.water_metrics
337
+ c1, c2, c3, c4, c5 = st.columns(5)
338
+ c1.metric("Accuracy", f"{met['acc']*100:.1f}%")
339
+ c2.metric("Precision", f"{met['precision']*100:.1f}%")
340
+ c3.metric("Recall", f"{met['recall']*100:.1f}%")
341
+ c4.metric("F1-score", f"{met['f1']*100:.1f}%")
342
+ c5.metric("ROC AUC", f"{met['roc_auc']:.3f}")
343
+
344
+ col_a, col_b = st.columns(2)
345
+ with col_a:
346
+ fig, ax = plt.subplots(figsize=(5, 4.5))
347
+ sns.heatmap(met["cm"], annot=True, fmt="d", cmap="Blues", ax=ax,
348
+ xticklabels=["No-agua", "Agua"], yticklabels=["No-agua", "Agua"])
349
+ ax.set_xlabel("Prediccion"); ax.set_ylabel("Real"); ax.set_title("Matriz de confusion")
350
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
351
+ with col_b:
352
+ fpr, tpr, _ = met["fpr_tpr"]
353
+ fig, ax = plt.subplots(figsize=(5, 4.5))
354
+ ax.plot(fpr, tpr, color="#1B6CA8", linewidth=2, label=f"AUC = {met['roc_auc']:.3f}")
355
+ ax.plot([0, 1], [0, 1], linestyle="--", color="gray", label="Azar")
356
+ ax.set_xlabel("Falsos positivos"); ax.set_ylabel("Verdaderos positivos"); ax.set_title("Curva ROC"); ax.legend()
357
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
358
+
359
+ with st.expander("Reporte de clasificacion completo"):
360
+ st.text(met["report"])
361
+
362
+ # ----------------------------------------------------------------------------
363
+ # TAB 3: Cobertura del suelo
364
+ # ----------------------------------------------------------------------------
365
+ with tab3:
366
+ st.markdown('<p class="section-title">Modelo de Cobertura del Suelo (multiclase)</p>', unsafe_allow_html=True)
367
+
368
+ if st.session_state.land_metrics is None:
369
+ st.markdown('<div class="info-box">Presiona <b>Entrenar ambos modelos</b> en la barra lateral para ver resultados.</div>', unsafe_allow_html=True)
370
+ else:
371
+ met = st.session_state.land_metrics
372
+ c1, c2, c3 = st.columns(3)
373
+ c1.metric("Accuracy", f"{met['acc']*100:.1f}%")
374
+ c2.metric("F1-macro", f"{met['f1_macro']*100:.1f}%")
375
+ c3.metric("F1-weighted", f"{met['f1_weighted']*100:.1f}%")
376
+
377
+ col_a, col_b = st.columns([1.1, 1])
378
+ with col_a:
379
+ fig, ax = plt.subplots(figsize=(6, 5))
380
+ sns.heatmap(met["cm"], annot=True, fmt="d", cmap="Greens", ax=ax,
381
+ xticklabels=met["labels"], yticklabels=met["labels"])
382
+ ax.set_xlabel("Prediccion"); ax.set_ylabel("Real"); ax.tick_params(axis="x", rotation=20)
383
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
384
+ with col_b:
385
+ st.markdown("**Distribucion de clases**")
386
+ fig, ax = plt.subplots(figsize=(5.5, 5))
387
+ counts = df_land_fe[LABEL_LC].value_counts()
388
+ ax.bar(counts.index, counts.values, color=[LC_PALETTE.get(c, "#888") for c in counts.index])
389
+ ax.tick_params(axis="x", rotation=20)
390
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
391
+
392
+ with st.expander("Reporte de clasificacion completo"):
393
+ st.text(met["report"])
394
+
395
+ # ----------------------------------------------------------------------------
396
+ # TAB 4: Predictor de pixel
397
+ # ----------------------------------------------------------------------------
398
+ with tab4:
399
+ st.markdown('<p class="section-title">Predictor interactivo de pixel</p>', unsafe_allow_html=True)
400
+ if st.session_state.water_model is None:
401
+ st.markdown('<div class="info-box">Entrena los modelos primero en la barra lateral.</div>', unsafe_allow_html=True)
402
+ else:
403
+ st.markdown("Ajusta los valores espectrales de un pixel hipotetico y observa ambas predicciones:")
404
+ c1, c2, c3, c4 = st.columns(4)
405
+ b01 = c1.slider("Banda 1 (rojo)", -100, 16000, 1200)
406
+ b02 = c2.slider("Banda 2 (NIR)", -100, 16000, 2200)
407
+ b03 = c3.slider("Banda 3 (azul)", -100, 16000, 1100)
408
+ b04 = c4.slider("Banda 4 (verde)", -100, 16000, 1300)
409
+ c5, c6, c7 = st.columns(3)
410
+ b05 = c5.slider("Banda 5", -100, 16000, 1800)
411
+ b06 = c6.slider("Banda 6 (SWIR)", -100, 16000, 1400)
412
+ b07 = c7.slider("Banda 7 (SWIR)", -100, 16000, 900)
413
+ c8, c9, c10 = st.columns(3)
414
+ ndvi = c8.slider("NDVI", -20000, 20000, 1500)
415
+ ndwi1 = c9.slider("NDWI1", -20000, 20000, -500)
416
+ ndwi2 = c10.slider("NDWI2", -20000, 20000, -500)
417
+
418
+ base_vals = {
419
+ "sur_refl_b01_1": b01, "sur_refl_b02_1": b02, "sur_refl_b03_1": b03, "sur_refl_b04_1": b04,
420
+ "sur_refl_b05_1": b05, "sur_refl_b06_1": b06, "sur_refl_b07_1": b07,
421
+ "ndvi": ndvi, "ndwi1": ndwi1, "ndwi2": ndwi2,
422
+ }
423
+ water_vals = dict(base_vals)
424
+ water_vals["nir_red_ratio"] = b02 / (b01 + 1e-3)
425
+ water_vals["green_swir_ratio"] = b04 / (b06 + 1e-3)
426
+ water_vals["brightness"] = b01 + b02 + b03 + b04 + b05 + b06 + b07
427
+ water_vals["ndwi_minus_ndvi"] = ndwi1 - ndvi
428
+
429
+ land_vals = dict(base_vals)
430
+ land_vals["nir_red_ratio"] = water_vals["nir_red_ratio"]
431
+ land_vals["green_swir_ratio"] = water_vals["green_swir_ratio"]
432
+ land_vals["brightness"] = water_vals["brightness"]
433
+ land_vals["savi_like"] = (b02 - b01) / (b02 + b01 + 0.5 * 10000)
434
+
435
+ if st.button("\U0001F52E Predecir", type="primary"):
436
+ water_probs = predict_pixel(st.session_state.water_model, st.session_state.water_scaler, WATER_FEATURES, water_vals)
437
+ land_probs = predict_pixel(st.session_state.land_model, st.session_state.land_scaler, LAND_FEATURES, land_vals)
438
+
439
+ col_r1, col_r2 = st.columns(2)
440
+ with col_r1:
441
+ st.markdown("**Prediccion: Agua**")
442
+ st.metric("Probabilidad de Agua", f"{water_probs.get(1, 0)*100:.1f}%")
443
+ st.progress(min(int(water_probs.get(1, 0)*100), 100))
444
+ with col_r2:
445
+ st.markdown("**Prediccion: Cobertura del suelo**")
446
+ pred_lc = max(land_probs, key=land_probs.get)
447
+ st.metric("Clase mas probable", pred_lc)
448
+ for cls, p in sorted(land_probs.items(), key=lambda x: -x[1]):
449
+ st.write(f"{cls}: {p*100:.1f}%")
450
+
451
+ # ----------------------------------------------------------------------------
452
+ # TAB 5: IA Explicable
453
+ # ----------------------------------------------------------------------------
454
+ with tab5:
455
+ st.markdown('<p class="section-title">IA Explicable: Importancia de Variables</p>', unsafe_allow_html=True)
456
+ if st.session_state.water_gini is None:
457
+ st.markdown('<div class="info-box">Entrena los modelos primero en la barra lateral.</div>', unsafe_allow_html=True)
458
+ else:
459
+ st.markdown("#### Clasificacion del agua")
460
+ col_a, col_b = st.columns(2)
461
+ with col_a:
462
+ fig, ax = plt.subplots(figsize=(6, 5))
463
+ colors = plt.cm.Blues_r(np.linspace(0.2, 0.8, len(st.session_state.water_gini)))
464
+ st.session_state.water_gini.plot(kind="barh", ax=ax, color=colors)
465
+ ax.invert_yaxis(); ax.set_xlabel("Importancia (impureza)")
466
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
467
+ with col_b:
468
+ fig, ax = plt.subplots(figsize=(6, 5))
469
+ colors = plt.cm.Oranges_r(np.linspace(0.2, 0.8, len(st.session_state.water_perm)))
470
+ st.session_state.water_perm.plot(kind="barh", ax=ax, color=colors)
471
+ ax.invert_yaxis(); ax.set_xlabel("Caida de F1 al permutar")
472
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
473
+
474
+ st.markdown("#### Cobertura del suelo")
475
+ col_c, col_d = st.columns(2)
476
+ with col_c:
477
+ fig, ax = plt.subplots(figsize=(6, 5))
478
+ colors = plt.cm.Greens_r(np.linspace(0.2, 0.8, len(st.session_state.land_gini)))
479
+ st.session_state.land_gini.plot(kind="barh", ax=ax, color=colors)
480
+ ax.invert_yaxis(); ax.set_xlabel("Importancia (impureza)")
481
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
482
+ with col_d:
483
+ fig, ax = plt.subplots(figsize=(6, 5))
484
+ colors = plt.cm.Purples_r(np.linspace(0.2, 0.8, len(st.session_state.land_perm)))
485
+ st.session_state.land_perm.plot(kind="barh", ax=ax, color=colors)
486
+ ax.invert_yaxis(); ax.set_xlabel("Caida de F1-macro al permutar")
487
+ fig.tight_layout(); st.pyplot(fig); plt.close(fig)
488
+
489
+ st.caption(
490
+ "El agua absorbe fuertemente la radiacion infrarroja cercana (NIR), lo que reduce su "
491
+ "reflectancia en banda 2 y eleva los indices NDWI. El NDVI es el indice mas relevante "
492
+ "para distinguir vegetacion densa de escasa en el modelo de cobertura del suelo."
493
+ )
494
+
495
+ st.markdown("---")
496
+ st.markdown(
497
+ "<small>GeoAI Explorer. Dataset historico real: MODIS Lake Powell "
498
+ "(nasa-cisto-data-science-group, HuggingFace) o generador sintetico de respaldo con el mismo esquema.</small>",
499
+ unsafe_allow_html=True,
500
+ )