import streamlit as st import pandas as pd import numpy as np import joblib import os import plotly.express as px import plotly.graph_objects as go # ───────────────────────────────────────────── # PAGE CONFIG # ───────────────────────────────────────────── st.set_page_config( page_title="GeoHazard ML", page_icon="🌍", layout="wide", initial_sidebar_state="collapsed", ) # ───────────────────────────────────────────── # CUSTOM CSS # ───────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ───────────────────────────────────────────── # MODEL LOADING # ───────────────────────────────────────────── @st.cache_resource(show_spinner=False) def load_models(): base = os.path.dirname(os.path.abspath(__file__)) model_dir = os.path.join(base, "models") rf = joblib.load(os.path.join(model_dir, "rf_model.pkl")) xgb = joblib.load(os.path.join(model_dir, "xgb_model.pkl")) scaler = joblib.load(os.path.join(model_dir, "scaler.pkl")) le = joblib.load(os.path.join(model_dir, "label_encoder.pkl")) feat_cols = joblib.load(os.path.join(model_dir, "feature_columns.pkl")) return rf, xgb, scaler, le, feat_cols @st.cache_data(show_spinner=False) def load_map_data(): base = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(base, "data", "processed", "featured_dataset.csv") df = pd.read_csv(path) return df # ───────────────────────────────────────────── # PREDICTION ENGINE # ───────────────────────────────────────────── RISK_ORDER = ["Very Low", "Low", "Medium", "High", "Very High"] RISK_COLORS = { "Very Low": "#1565c0", "Low": "#2e7d32", "Medium": "#f57f17", "High": "#e65100", "Very High": "#b71c1c", } BADGE_CLASS = { "Very Low": "badge-very-low", "Low": "badge-low", "Medium": "badge-medium", "High": "badge-high", "Very High": "badge-very-high", } def compute_derived(eq_count, eq_mag_mean, eq_depth_mean): freq_score = np.log1p(eq_count) energy_proxy = 10 ** (1.5 * eq_mag_mean) if eq_mag_mean > 0 else 1.0 mag_depth_ratio = eq_mag_mean / (eq_depth_mean + 1) return freq_score, energy_proxy, mag_depth_ratio def depth_cat(d): if d == 0: return 0 if d < 70: return 1 if d <= 300: return 2 return 3 def predict(model_name, rf, xgb, scaler, le, feat_cols, lat, lon, eq_count, eq_mag_mean, eq_depth_mean, has_seismic, continent): freq_score, energy_proxy, mag_depth_ratio = compute_derived(eq_count, eq_mag_mean, eq_depth_mean) lat_abs = abs(lat) lat_seismic_zone = 1 if lat_abs <= 60 else 0 seismic_intensity = energy_proxy * freq_score weighted_mag = eq_mag_mean / (eq_depth_mean + 1) dep_cat = depth_cat(eq_depth_mean) row = { "eq_count": eq_count, "eq_mag_mean": eq_mag_mean, "eq_depth_mean": eq_depth_mean, "has_seismic_activity": int(has_seismic), "eq_frequency_score": freq_score, "seismic_energy_proxy": energy_proxy, "mag_depth_ratio": mag_depth_ratio, "depth_category": dep_cat, "lat_abs": lat_abs, "lat_seismic_zone": lat_seismic_zone, "seismic_intensity": seismic_intensity, "weighted_mag": weighted_mag, "neighbor_eq_count": freq_score, # proxy: same as freq_score for live input "continent_Asia": 1 if continent == "Asia" else 0, "continent_Europe": 1 if continent == "Europe" else 0, "continent_North America": 1 if continent == "North America" else 0, "continent_Oceania": 1 if continent == "Oceania" else 0, "continent_South America": 1 if continent == "South America" else 0, } df = pd.DataFrame([row]) df = df.reindex(columns=feat_cols, fill_value=0) X = scaler.transform(df) if model_name == "Random Forest": model = rf pred = model.predict(X)[0] proba = model.predict_proba(X)[0] classes = model.classes_ else: model = xgb pred_int = model.predict(X)[0] pred = le.inverse_transform([int(pred_int)])[0] proba = model.predict_proba(X)[0] classes = le.classes_ prob_dict = {c: float(p) for c, p in zip(classes, proba)} return pred, prob_dict # ───────────────────────────────────────────── # HERO # ───────────────────────────────────────────── st.markdown("""

🌍 GeoHazard ML

Geological Hazard Risk Classification · Powered by XGBoost & Random Forest · 166,293 USGS Events · 14,613 Grid Cells

""", unsafe_allow_html=True) # ───────────────────────────────────────────── # TABS # ───────────────────────────────────────────── tab1, tab2, tab3 = st.tabs(["🗺️ Global Risk Map", "🔮 Predict Risk", "📊 Model Performance"]) # ════════════════════════════════════════════ # TAB 1 — GLOBAL RISK MAP # ════════════════════════════════════════════ with tab1: st.markdown('
Global INFORM Risk Distribution
', unsafe_allow_html=True) st.caption("Each point is a 1°×1° grid cell coloured by INFORM Risk Label. Hover to see details.") with st.spinner("Loading map data..."): try: df_map = load_map_data() color_map = { "Very Low": "#1565c0", "Low": "#2e7d32", "Medium": "#f57f17", "High": "#e65100", "Very High": "#b71c1c", } cat_order = ["Very Low", "Low", "Medium", "High", "Very High"] fig_map = px.scatter_geo( df_map, lat="lat_grid", lon="lon_grid", color="risk_label", color_discrete_map=color_map, category_orders={"risk_label": cat_order}, hover_data={ "lat_grid": ":.1f", "lon_grid": ":.1f", "eq_count": True, "eq_mag_mean": ":.2f", "risk_label": True, }, labels={ "lat_grid": "Latitude", "lon_grid": "Longitude", "eq_count": "Earthquake Count", "eq_mag_mean": "Mean Magnitude", "risk_label": "Risk Level", }, title="", projection="natural earth", ) fig_map.update_traces(marker=dict(size=3, opacity=0.75)) fig_map.update_layout( paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", geo=dict( bgcolor="#0e1117", showland=True, landcolor="#1a2035", showocean=True, oceancolor="#0d1b2a", showcoastlines=True, coastlinecolor="#2a3a5c", showframe=False, ), legend=dict( title="Risk Level", bgcolor="#1a2035", bordercolor="#2a3a5c", font=dict(color="#e0e0e0"), ), margin=dict(l=0, r=0, t=10, b=0), height=520, ) st.plotly_chart(fig_map, use_container_width=True) # Quick stats row c1, c2, c3, c4, c5 = st.columns(5) counts = df_map["risk_label"].value_counts() for col, label in zip([c1, c2, c3, c4, c5], cat_order): n = counts.get(label, 0) col.markdown(f"""
{n:,}
{label}
""", unsafe_allow_html=True) except Exception as e: st.error(f"Could not load map data: {e}") st.info("Make sure `data/processed/featured_dataset.csv` is in your repo.") # ════════════════════════════════════════════ # TAB 2 — PREDICT RISK # ════════════════════════════════════════════ with tab2: # Load models model_load_error = None try: rf_model, xgb_model, scaler, label_enc, feat_cols = load_models() models_loaded = True except Exception as e: models_loaded = False model_load_error = str(e) if not models_loaded: st.error(f"Could not load models: {model_load_error}") st.info("Ensure all `.pkl` files are in the `models/` folder.") else: left, right = st.columns([1, 1], gap="large") with left: st.markdown('
📍 Location
', unsafe_allow_html=True) col_lat, col_lon = st.columns(2) with col_lat: lat_in = st.number_input("Latitude", min_value=-90.0, max_value=90.0, value=35.5, step=0.5, format="%.1f") with col_lon: lon_in = st.number_input("Longitude", min_value=-180.0, max_value=180.0, value=139.5, step=0.5, format="%.1f") continent_in = st.selectbox("Continent", ["Africa", "Asia", "Europe", "North America", "Oceania", "South America"], index=1) st.markdown('
📡 Seismic Measurements
', unsafe_allow_html=True) col_a, col_b = st.columns(2) with col_a: eq_count_in = st.number_input("Earthquake Count (M4.0+)", min_value=0, max_value=2000, value=45, step=1) eq_mag_in = st.number_input("Mean Magnitude", min_value=0.0, max_value=10.0, value=4.8, step=0.1, format="%.1f") with col_b: eq_depth_in = st.number_input("Mean Depth (km)", min_value=0.0, max_value=750.0, value=32.0, step=1.0, format="%.1f") has_seismic_in = st.checkbox("Has Seismic Activity", value=eq_count_in > 0) st.markdown('
⚙️ Auto-Computed Features
', unsafe_allow_html=True) fs, ep, mdr = compute_derived(eq_count_in, eq_mag_in, eq_depth_in) ca, cb, cc = st.columns(3) ca.metric("Frequency Score", f"{fs:.3f}") cb.metric("Energy Proxy", f"{ep:.2e}") cc.metric("Mag/Depth Ratio", f"{mdr:.3f}") st.markdown('
🤖 Model
', unsafe_allow_html=True) model_choice = st.radio("Select Model", ["XGBoost (Best)", "Random Forest"], horizontal=True) model_name = "XGBoost" if "XGBoost" in model_choice else "Random Forest" predict_btn = st.button("🔮 Predict Risk", use_container_width=True, type="primary") with right: st.markdown('
🎯 Prediction Result
', unsafe_allow_html=True) if predict_btn: with st.spinner("Running model..."): pred, prob_dict = predict( model_name, rf_model, xgb_model, scaler, label_enc, feat_cols, lat_in, lon_in, eq_count_in, eq_mag_in, eq_depth_in, has_seismic_in, continent_in ) st.session_state["pred"] = pred st.session_state["prob_dict"] = prob_dict st.session_state["lat_pred"] = lat_in st.session_state["lon_pred"] = lon_in st.session_state["model_used"] = model_name if "pred" in st.session_state: pred = st.session_state["pred"] prob_dict = st.session_state["prob_dict"] lat_p = st.session_state["lat_pred"] lon_p = st.session_state["lon_pred"] m_used = st.session_state["model_used"] badge_cls = BADGE_CLASS.get(pred, "badge-medium") conf = prob_dict.get(pred, 0) * 100 st.markdown(f"""
PREDICTED RISK LEVEL
{pred}
Confidence: {conf:.1f}% · Model: {m_used}
""", unsafe_allow_html=True) # Probability bar chart st.markdown('
📊 Class Probabilities
', unsafe_allow_html=True) prob_vals = [prob_dict.get(c, 0) * 100 for c in RISK_ORDER] bar_colors = [RISK_COLORS[c] for c in RISK_ORDER] fig_bar = go.Figure(go.Bar( x=RISK_ORDER, y=prob_vals, marker_color=bar_colors, text=[f"{v:.1f}%" for v in prob_vals], textposition="outside", textfont=dict(color="#e0e0e0", size=11), )) fig_bar.update_layout( paper_bgcolor="#0e1117", plot_bgcolor="#1a2035", xaxis=dict(tickfont=dict(color="#b0bec5"), gridcolor="#2a3a5c"), yaxis=dict(tickfont=dict(color="#b0bec5"), gridcolor="#2a3a5c", title="Probability (%)", range=[0, 110]), margin=dict(l=10, r=10, t=10, b=10), height=260, showlegend=False, ) st.plotly_chart(fig_bar, use_container_width=True) # Location map st.markdown('
📍 Predicted Location
', unsafe_allow_html=True) pin_color = RISK_COLORS.get(pred, "#f57f17") fig_loc = go.Figure(go.Scattergeo( lat=[lat_p], lon=[lon_p], mode="markers", marker=dict(size=14, color=pin_color, symbol="circle", line=dict(width=2, color="white")), text=[f"{pred} Risk
({lat_p}°, {lon_p}°)"], hoverinfo="text", )) fig_loc.update_layout( paper_bgcolor="#0e1117", geo=dict( bgcolor="#0e1117", showland=True, landcolor="#1a2035", showocean=True, oceancolor="#0d1b2a", showcoastlines=True, coastlinecolor="#2a3a5c", showframe=False, center=dict(lat=lat_p, lon=lon_p), projection_scale=3, ), margin=dict(l=0, r=0, t=0, b=0), height=240, ) st.plotly_chart(fig_loc, use_container_width=True) else: st.markdown("""
🔮
Fill in the values on the left
and click Predict Risk
""", unsafe_allow_html=True) # ════════════════════════════════════════════ # TAB 3 — MODEL PERFORMANCE # ════════════════════════════════════════════ with tab3: st.markdown('
Model Comparison
', unsafe_allow_html=True) perf_data = { "Model": ["Logistic Regression", "Random Forest", "XGBoost"], "Accuracy": ["58.5%", "76.7%", "80.5%"], "Macro F1": ["0.480", "0.650", "0.681"], "ROC-AUC": ["0.906", "0.959", "0.961"], "Status": ["Baseline", "Strong", "⭐ Best"], } df_perf = pd.DataFrame(perf_data) # Colour the best row def style_row(row): if row["Status"] == "⭐ Best": return ["background-color: #0d2137; color: #4fc3f7; font-weight:bold"] * len(row) return [""] * len(row) st.dataframe(df_perf.style.apply(style_row, axis=1), use_container_width=True, hide_index=True) st.markdown('
Per-Class Results (XGBoost)
', unsafe_allow_html=True) cls_data = { "Class": ["Very Low", "Low", "Medium", "High", "Very High"], "Precision": [0.41, 0.90, 0.79, 0.67, 0.66], "Recall": [0.36, 0.88, 0.84, 0.63, 0.68], "F1-Score": [0.39, 0.89, 0.81, 0.65, 0.67], "Test Samples": [47, 1307, 909, 405, 255], } df_cls = pd.DataFrame(cls_data) fig_cls = px.bar( df_cls.melt(id_vars="Class", value_vars=["Precision","Recall","F1-Score"]), x="Class", y="value", color="variable", barmode="group", color_discrete_sequence=["#4fc3f7", "#81c784", "#ffb74d"], labels={"value": "Score", "variable": "Metric"}, title="", category_orders={"Class": RISK_ORDER}, ) fig_cls.update_layout( paper_bgcolor="#0e1117", plot_bgcolor="#1a2035", xaxis=dict(tickfont=dict(color="#b0bec5"), gridcolor="#2a3a5c"), yaxis=dict(tickfont=dict(color="#b0bec5"), gridcolor="#2a3a5c", range=[0, 1.1]), legend=dict(bgcolor="#1a2035", font=dict(color="#e0e0e0")), margin=dict(l=10, r=10, t=20, b=10), height=340, ) st.plotly_chart(fig_cls, use_container_width=True) st.markdown('
Feature Importance (Random Forest)
', unsafe_allow_html=True) fi_data = { "Feature": ["lat_abs","neighbor_eq_count","continent_Europe","continent_South America", "continent_North America","continent_Oceania","continent_Asia", "lat_seismic_zone","eq_depth_mean","mag_depth_ratio","weighted_mag", "seismic_intensity","eq_count","eq_frequency_score","seismic_energy_proxy", "eq_mag_mean","depth_category","has_seismic_activity"], "Importance": [32.7,17.6,11.6,10.0,7.4,3.9,3.9,3.8,1.6,1.3,1.3,1.1,1.0,0.9,0.9,0.9,0.3,0.1], "Type": ["Geographic","Seismic","Geographic","Geographic","Geographic","Geographic","Geographic", "Geographic","Seismic","Seismic","Seismic","Seismic","Seismic","Seismic","Seismic", "Seismic","Seismic","Seismic"], } df_fi = pd.DataFrame(fi_data).sort_values("Importance", ascending=True) fig_fi = px.bar( df_fi, x="Importance", y="Feature", color="Type", orientation="h", color_discrete_map={"Geographic": "#4fc3f7", "Seismic": "#81c784"}, labels={"Importance": "Importance (%)"}, ) fig_fi.update_layout( paper_bgcolor="#0e1117", plot_bgcolor="#1a2035", xaxis=dict(tickfont=dict(color="#b0bec5"), gridcolor="#2a3a5c"), yaxis=dict(tickfont=dict(color="#b0bec5"), tickfont_size=11), legend=dict(bgcolor="#1a2035", font=dict(color="#e0e0e0")), margin=dict(l=10, r=10, t=10, b=10), height=480, ) st.plotly_chart(fig_fi, use_container_width=True) st.markdown('
Dataset Summary
', unsafe_allow_html=True) c1, c2, c3, c4 = st.columns(4) stats = [ ("166,293", "Raw Earthquake Events"), ("14,613", "Grid Cells (1°×1°)"), ("18", "Engineered Features"), ("80.5%", "Best Model Accuracy"), ] for col, (val, lbl) in zip([c1, c2, c3, c4], stats): col.markdown(f"""
{val}
{lbl}
""", unsafe_allow_html=True)