Geo_Hazard__ML / app.py
nithishvenkat4's picture
Upload app.py
bb62f5c verified
Raw
History Blame Contribute Delete
24 kB
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("""
<style>
/* Main background */
.stApp { background-color: #0e1117; color: #e0e0e0; }
/* Header banner */
.hero-banner {
background: linear-gradient(135deg, #1a1f35 0%, #0d2137 50%, #1a1f35 100%);
border: 1px solid #2a3a5c;
border-radius: 12px;
padding: 2rem 2.5rem;
margin-bottom: 1.5rem;
text-align: center;
}
.hero-banner h1 { font-size: 2.4rem; font-weight: 800; color: #4fc3f7; margin: 0; }
.hero-banner p { color: #90caf9; font-size: 1.05rem; margin: 0.5rem 0 0; }
/* Risk badge colours */
.badge {
display: inline-block;
padding: 0.45rem 1.2rem;
border-radius: 20px;
font-size: 1.1rem;
font-weight: 700;
letter-spacing: 0.5px;
}
.badge-very-high { background:#b71c1c; color:#fff; }
.badge-high { background:#e65100; color:#fff; }
.badge-medium { background:#f57f17; color:#fff; }
.badge-low { background:#2e7d32; color:#fff; }
.badge-very-low { background:#1565c0; color:#fff; }
/* Metric cards */
.metric-card {
background: #1a2035;
border: 1px solid #2a3a5c;
border-radius: 10px;
padding: 1rem 1.2rem;
text-align: center;
}
.metric-card .val { font-size: 1.8rem; font-weight: 700; color: #4fc3f7; }
.metric-card .lbl { font-size: 0.8rem; color: #90a4ae; margin-top: 2px; }
/* Section headers */
.section-title {
font-size: 1.15rem;
font-weight: 700;
color: #4fc3f7;
border-left: 4px solid #4fc3f7;
padding-left: 0.7rem;
margin: 1.2rem 0 0.8rem;
}
/* Result box */
.result-box {
background: #1a2035;
border: 1px solid #2a3a5c;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
}
/* Tab styling */
.stTabs [data-baseweb="tab-list"] { gap: 4px; }
.stTabs [data-baseweb="tab"] {
background: #1a2035;
border-radius: 8px 8px 0 0;
color: #90caf9;
padding: 0.5rem 1.2rem;
}
.stTabs [aria-selected="true"] {
background: #0d2137 !important;
color: #4fc3f7 !important;
border-bottom: 2px solid #4fc3f7;
}
/* Input labels */
label { color: #b0bec5 !important; font-size: 0.88rem !important; }
/* Divider */
hr { border-color: #2a3a5c; }
/* Hide Streamlit branding */
#MainMenu, footer { visibility: hidden; }
</style>
""", 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("""
<div class="hero-banner">
<h1>๐ŸŒ GeoHazard ML</h1>
<p>Geological Hazard Risk Classification ยท Powered by XGBoost & Random Forest ยท 166,293 USGS Events ยท 14,613 Grid Cells</p>
</div>
""", 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('<div class="section-title">Global INFORM Risk Distribution</div>', 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"""
<div class="metric-card">
<div class="val" style="color:{color_map[label]}">{n:,}</div>
<div class="lbl">{label}</div>
</div>""", 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('<div class="section-title">๐Ÿ“ Location</div>', 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('<div class="section-title">๐Ÿ“ก Seismic Measurements</div>', 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('<div class="section-title">โš™๏ธ Auto-Computed Features</div>', 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('<div class="section-title">๐Ÿค– Model</div>', 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('<div class="section-title">๐ŸŽฏ Prediction Result</div>', 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"""
<div class="result-box">
<div style="color:#90a4ae; font-size:0.85rem; margin-bottom:0.5rem">PREDICTED RISK LEVEL</div>
<span class="badge {badge_cls}">{pred}</span>
<div style="margin-top:1rem; color:#b0bec5; font-size:0.9rem">
Confidence: <strong style="color:#4fc3f7">{conf:.1f}%</strong> ยท Model: <strong style="color:#4fc3f7">{m_used}</strong>
</div>
</div>
""", unsafe_allow_html=True)
# Probability bar chart
st.markdown('<div class="section-title">๐Ÿ“Š Class Probabilities</div>', 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('<div class="section-title">๐Ÿ“ Predicted Location</div>', 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<br>({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("""
<div class="result-box" style="padding:3rem">
<div style="font-size:2.5rem">๐Ÿ”ฎ</div>
<div style="color:#546e7a; margin-top:0.8rem">Fill in the values on the left<br>and click <strong>Predict Risk</strong></div>
</div>
""", unsafe_allow_html=True)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# TAB 3 โ€” MODEL PERFORMANCE
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
with tab3:
st.markdown('<div class="section-title">Model Comparison</div>', 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('<div class="section-title">Per-Class Results (XGBoost)</div>', 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('<div class="section-title">Feature Importance (Random Forest)</div>', 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('<div class="section-title">Dataset Summary</div>', 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"""
<div class="metric-card">
<div class="val">{val}</div>
<div class="lbl">{lbl}</div>
</div>""", unsafe_allow_html=True)