GeoAI_Land_Classifier / src /streamlit_app.py
mgprz's picture
Update src/streamlit_app.py
105dff5 verified
Raw
History Blame Contribute Delete
21.1 kB
"""
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,
)