""" 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(""" """, 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('

\U0001F30D GeoAI Explorer: Vegetacion y Cobertura del Suelo

', unsafe_allow_html=True) st.markdown( f'

Clasificacion de pixeles satelitales Landsat MSS  |  ' f'Fuente: {data_source}  |  {len(df_fe):,} pixeles reales

', 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('

Analisis Exploratorio de Datos

', 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('

Modelo de Clasificacion de Vegetacion (binario)

', 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('
Presiona Entrenar ambos modelos en la barra lateral para ver resultados.
', 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('

Modelo de Cobertura del Suelo (multiclase, 6 clases reales)

', unsafe_allow_html=True) if st.session_state.lc_metrics is None: st.markdown('
Presiona Entrenar ambos modelos en la barra lateral para ver resultados.
', 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('

Predictor interactivo de pixel

', unsafe_allow_html=True) if st.session_state.veg_model is None: st.markdown('
Entrena los modelos primero en la barra lateral.
', 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('

IA Explicable: Importancia de Variables

', unsafe_allow_html=True) if st.session_state.veg_gini is None: st.markdown('
Entrena los modelos primero en la barra lateral.
', 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( "GeoAI Explorer. Dataset historico real: Statlog (Landsat Satellite), " "UCI Machine Learning Repository (1993) - archivo incluido statlog_landsat_satellite.csv.", unsafe_allow_html=True, )