APERTURE_AUDIT / app.py
Darkweb007's picture
Initial commit: model-agnostic SHAP/LIME explainability audit dashboard
41dfe78
Raw
History Blame Contribute Delete
7.14 kB
from __future__ import annotations
import os
import pandas as pd
import streamlit as st
from model import train, predict_proba, FEATURES, FEATURE_LABELS
from explain.shap_exact import exact_shap, baseline_logit
from explain.lime_local import lime_explain
st.set_page_config(page_title="Aperture Audit | Model Explainability Dashboard", page_icon="◎", layout="wide")
# ---------------------------------------------------------------------------
# Theme: violet / near-black, tech-compliance feel
# ---------------------------------------------------------------------------
st.markdown(
"""
<style>
:root {
--ap-violet: #8B5CF6;
--ap-bg: #0C0A14;
--ap-panel: #17131F;
--ap-border: #2A2438;
--ap-grey: #A79FC2;
}
.stApp { background-color: var(--ap-bg); color: #F2EFFA; }
section[data-testid="stSidebar"] { background-color: var(--ap-panel); }
h1, h2, h3 { font-family: -apple-system, 'Helvetica Neue', sans-serif; letter-spacing: -0.01em; }
.ap-hero { font-size: 2.0rem; font-weight: 700; margin-bottom: 0; color: var(--ap-violet); }
.ap-sub { color: var(--ap-grey); font-size: 0.95rem; margin-top: 0.2rem; }
.ap-card {
background: var(--ap-panel); border: 1px solid var(--ap-border); border-radius: 12px;
padding: 14px 16px; margin-bottom: 10px;
}
div[data-testid="stMetricValue"] { color: var(--ap-violet); }
</style>
""",
unsafe_allow_html=True,
)
st.markdown('<div class="ap-hero">Aperture Audit</div>', unsafe_allow_html=True)
st.markdown(
'<div class="ap-sub">A model-agnostic post-hoc audit dashboard: every credit decision comes with an '
'exact SHAP-style attribution, cross-checked against an independently derived LIME explanation.</div>',
unsafe_allow_html=True,
)
st.write("")
DATA_PATH = os.path.join(os.path.dirname(__file__), "data", "credit_applications.csv")
if "trained" not in st.session_state:
st.session_state.trained = train(DATA_PATH)
trained = st.session_state.trained
df = pd.read_csv(DATA_PATH)
with st.sidebar:
st.markdown("### ◎ Aperture Audit")
st.caption("Model-agnostic explainability dashboard, EU AI Act-style audit framing.")
st.metric("Model training accuracy", f"{trained.train_accuracy:.1%}")
st.caption(f"Logistic regression over {len(FEATURES)} features, {len(df)} synthetic applications.")
st.divider()
mode = st.radio("Choose an applicant", ["Pick from dataset", "Enter manually"])
if mode == "Pick from dataset":
idx = st.selectbox("Applicant row", options=df.index, format_func=lambda i: f"Applicant #{i}")
applicant = df.loc[idx, FEATURES].to_dict()
else:
applicant = {}
for f in FEATURES:
default = float(df[f].median())
applicant[f] = st.slider(FEATURE_LABELS[f], min_value=0.0, max_value=float(df[f].max() * 1.3), value=default)
st.markdown("#### Applicant")
cols = st.columns(len(FEATURES))
for c, f in zip(cols, FEATURES):
c.metric(FEATURE_LABELS[f], f"{applicant[f]:.1f}")
proba = predict_proba(trained, applicant)
decision = "APPROVE" if proba >= 0.5 else "DENY"
d1, d2 = st.columns([1, 3])
with d1:
st.markdown(
f'<div class="ap-card"><div style="font-size:0.8rem; color:var(--ap-grey);">MODEL DECISION</div>'
f'<div style="font-size:1.8rem; font-weight:700; color:var(--ap-violet);">{decision}</div>'
f'<div style="color:var(--ap-grey);">approval probability {proba:.1%}</div></div>',
unsafe_allow_html=True,
)
tab1, tab2, tab3 = st.tabs(["SHAP-style attribution", "LIME cross-check", "Audit narrative"])
shap_contribs = exact_shap(trained, applicant)
baseline = baseline_logit(trained)
with tab1:
st.markdown(
"Exact, closed-form Shapley attribution (valid because the underlying model is linear). "
"Each bar is this feature's exact contribution to the decision, relative to the average applicant."
)
chart_df = pd.DataFrame([{"Feature": c.label, "Contribution": c.contribution} for c in shap_contribs])
st.bar_chart(chart_df.set_index("Feature"))
st.dataframe(
pd.DataFrame([{
"Feature": c.label, "Applicant value": round(c.value, 2), "SHAP contribution (logit)": round(c.contribution, 3)
} for c in shap_contribs]),
use_container_width=True, hide_index=True,
)
st.caption(f"Baseline logit (average applicant): {baseline:.3f}")
with tab2:
st.markdown(
"An independently derived explanation: 400 small random perturbations around this applicant are fed "
"back through the real model, and a local linear regression is fit to the (perturbation, prediction) pairs. "
"Its coefficients never look at the model's internals -- only its input/output behavior."
)
lime_result = lime_explain(trained, applicant)
compare_rows = []
for c in shap_contribs:
# Compare like with like: LIME's local coefficient times this applicant's
# deviation from baseline, the same formula SHAP uses -- comparing raw
# coefficient signs against SHAP's deviation-adjusted contribution would
# be an apples-to-oranges mismatch (a feature can have a positive
# coefficient but a negative SHAP contribution if the applicant is below
# the population mean on that feature).
lime_w = lime_result["weights"][c.feature]
lime_contribution = lime_w * (c.value - trained.feature_means[c.feature])
shap_sign = "+" if c.contribution > 0 else "-"
lime_sign = "+" if lime_contribution > 0 else "-"
agree = "✅ agree" if shap_sign == lime_sign else "⚠️ disagree"
compare_rows.append({
"Feature": c.label,
"SHAP contribution": round(c.contribution, 3),
"LIME-derived contribution": round(lime_contribution, 3),
"Cross-check": agree,
})
st.dataframe(pd.DataFrame(compare_rows), use_container_width=True, hide_index=True)
st.caption(f"LIME local surrogate fidelity (R²): {lime_result['local_fidelity_r2']:.3f} &mdash; "
f"how well the local linear model explains the real model's behavior near this applicant.")
with tab3:
top3 = shap_contribs[:3]
direction_words = {True: "increased", False: "decreased"}
st.markdown(f"##### Why this applicant was **{decision}D**" if decision == "DENY" else f"##### Why this applicant was **{decision}D**")
for c in top3:
word = direction_words[c.contribution > 0]
st.markdown(
f'<div class="ap-card">Their <b>{c.label.lower()}</b> of <b>{c.value:.1f}</b> {word} their approval '
f'likelihood by <b>{abs(c.contribution):.2f}</b> logit-points relative to a typical applicant.</div>',
unsafe_allow_html=True,
)
st.caption(
"This is the kind of per-decision, feature-level explanation EU AI Act Article 13 and similar "
"regulatory frameworks require for high-stakes automated decisions: not just a prediction, but a "
"reason, traceable to specific input features, for that specific person."
)