Customs_Selflearning_RMS / page4_results.py
rameshmoorthy's picture
Upload 9 files
67bf425 verified
Raw
History Blame Contribute Delete
44.9 kB
"""
page4_results.py β€” Simulation Results: 5 tables + charts for all analysis dimensions
"""
import streamlit as st
import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
import numpy as np
from styles import (inject_global_css, page_header, metric_row,
WCO_GOLD, WCO_BLUE, WCO_GREEN, WCO_RED,
WCO_CARD_BG, WCO_BORDER, WCO_MUTED, RISK_COLORS)
from simulation_engine import (build_rule_hit_table, build_channel_table,
build_exploration_discovery_table,
build_offence_db_table, build_risk_score_table,
RISK_AREAS)
RISK_LEVEL_COLORS = {
"FRAUD_DETECTED": "#C8102E",
"DOC_QUERY": "#F5A800",
"CLEAN": "#00843D",
"NOT_INSPECTED": "#6B85AA",
}
def offence_db_growth_chart(df):
"""Cumulative offence DB additions over bill sequence."""
df_sorted = df.sort_index()
df_sorted["cum_offence"] = df_sorted["added_to_offence_db"].cumsum()
# Break down by risk area
traces = []
for area_name, area_cfg in RISK_AREAS.items():
a_df = df_sorted[df_sorted["risk_area"] == area_name].copy()
a_df["cum"] = a_df["added_to_offence_db"].cumsum()
traces.append(go.Scatter(
x=a_df.index, y=a_df["cum"],
name=f"{area_cfg['icon']} {area_name}",
line=dict(color=area_cfg["color"], width=2),
mode="lines",
hovertemplate=f"<b>{area_name}</b><br>Bill: %{{x}}<br>Cumulative: %{{y}}<extra></extra>",
))
traces.append(go.Scatter(
x=df_sorted.index, y=df_sorted["cum_offence"],
name="πŸ“Š Total", line=dict(color=WCO_GOLD, width=3, dash="dot"),
hovertemplate="<b>Total</b><br>Bill: %{x}<br>Cumulative: %{y}<extra></extra>",
))
fig = go.Figure(traces)
fig.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(family="IBM Plex Sans", color="#D0DCF0", size=11),
height=380,
title=dict(text="<b>Real-Time Offence Database Growth (Cumulative)</b>",
font=dict(color=WCO_GOLD, size=14, family="Playfair Display"), x=0.5),
xaxis=dict(title="Bill Sequence", gridcolor="#1E3A6E"),
yaxis=dict(title="Offences Added to DB", gridcolor="#1E3A6E"),
legend=dict(bgcolor="#0F1C35", bordercolor=WCO_BORDER, font=dict(size=10)),
margin=dict(l=55, r=20, t=55, b=45),
)
return fig
def exploration_vs_exploitation_chart(df):
"""Compare detection across exploit vs explore bills."""
cats = ["Exploitation Bills", "Exploration Bills"]
exploit_df = df[df["is_exploration"] == 0]
explore_df = df[df["is_exploration"] == 1]
def detection_stats(sub):
tot = len(sub)
fraud = (sub["inspection_outcome"] == "FRAUD_DETECTED").sum()
rev = sub["detected_revenue"].sum()
rate = 100 * fraud / tot if tot else 0
return tot, fraud, rev, rate
e_tot, e_fraud, e_rev, e_rate = detection_stats(exploit_df)
x_tot, x_fraud, x_rev, x_rate = detection_stats(explore_df)
fig = go.Figure()
fig.add_trace(go.Bar(name="🎯 Exploitations", x=["Bills","Frauds Detected","Revenue ($K)"],
y=[e_tot, e_fraud, e_rev/1000],
marker_color="#0066CC", opacity=0.85,
text=[e_tot, e_fraud, f"${e_rev/1000:.1f}K"], textposition="outside",
textfont=dict(color="#D0DCF0")))
fig.add_trace(go.Bar(name="πŸ” Explorations", x=["Bills","Frauds Detected","Revenue ($K)"],
y=[x_tot, x_fraud, x_rev/1000],
marker_color="#CC77FF", opacity=0.85,
text=[x_tot, x_fraud, f"${x_rev/1000:.1f}K"], textposition="outside",
textfont=dict(color="#D0DCF0")))
fig.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(family="IBM Plex Sans", color="#D0DCF0", size=12),
height=340, barmode="group",
title=dict(text="<b>Exploitation vs Exploration: Detection Comparison</b>",
font=dict(color=WCO_GOLD, size=14, family="Playfair Display"), x=0.5),
xaxis=dict(gridcolor="#1E3A6E"),
yaxis=dict(gridcolor="#1E3A6E", title="Count / Value"),
legend=dict(bgcolor="#0F1C35", bordercolor=WCO_BORDER),
margin=dict(l=50, r=20, t=55, b=40),
)
return fig
def weight_evolution_chart(df, orig_weights, updated_weights):
"""Visualise how rule weights increased after simulation."""
rids, orig, updated, deltas = [], [], [], []
for area in RISK_AREAS.values():
for rule in area["rules"]:
rid = rule["id"]
o = orig_weights.get(rid, rule["weight"])
u = updated_weights.get(rid, o)
rids.append(rid)
orig.append(o)
updated.append(u)
deltas.append(round(u - o, 4))
fig = go.Figure()
fig.add_trace(go.Bar(x=rids, y=orig, name="Original Weights",
marker_color="#1E3A6E", opacity=0.9))
fig.add_trace(go.Bar(x=rids, y=updated, name="Updated Weights",
marker_color=WCO_GOLD, opacity=0.85))
fig.add_trace(go.Scatter(x=rids, y=deltas, name="Ξ” Weight", mode="markers+lines",
marker=dict(color=WCO_RED, size=9, symbol="triangle-up"),
line=dict(color=WCO_RED, dash="dot", width=1.5),
yaxis="y2"))
fig.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(family="IBM Plex Sans", color="#D0DCF0", size=10),
height=360, barmode="group",
title=dict(text="<b>Rule Weight Evolution After Self-Learning Feedback</b>",
font=dict(color=WCO_GOLD, size=14, family="Playfair Display"), x=0.5),
xaxis=dict(gridcolor="#1E3A6E", tickangle=-45),
yaxis=dict(title="Weight", gridcolor="#1E3A6E"),
yaxis2=dict(title="Ξ” Weight", overlaying="y", side="right",
gridcolor="#1E3A6E", showgrid=False),
legend=dict(bgcolor="#0F1C35", bordercolor=WCO_BORDER),
margin=dict(l=55, r=55, t=55, b=90),
)
return fig
def styled_table(df: pd.DataFrame, title: str, highlight_col: str = None,
color_map: dict = None):
st.markdown(f"""
<div style="color:{WCO_GOLD};font-family:'Playfair Display',serif;
font-size:15px;font-weight:700;margin:16px 0 10px;">
{title}
</div>""", unsafe_allow_html=True)
if highlight_col and color_map:
def _style(val):
col = color_map.get(val, "")
return f"background-color:{col}22;color:{col}" if col else ""
styled = df.style.applymap(_style, subset=[highlight_col]) \
.set_properties(**{"background-color": WCO_CARD_BG,
"color": "#D0DCF0", "font-size": "12px"})
st.dataframe(styled, use_container_width=True, height=min(38 * len(df) + 40, 420))
else:
st.dataframe(
df.style.set_properties(**{"background-color": WCO_CARD_BG,
"color": "#D0DCF0", "font-size": "12px"}),
use_container_width=True, height=min(38 * len(df) + 40, 420),
)
def exploration_scatter_tab(df: pd.DataFrame):
"""
Full scatter-plot breakdown of the bATE exploration logic applied to all bills.
Shows every formula component:
unc_i = -1.8 * |fraud_score - 0.5| + 1
S_i = unc_i * log(pred_revenue + Ξ΅)
Colour = channel assignment. Shape = exploration pick or not.
"""
import numpy as np
# ── Formula explanation banner ────────────────────────────────
st.markdown("""
<div class="wco-card-gold">
<h3>🧭 bATE Exploration Logic β€” All Bills Visualised</h3>
<div style="display:flex;gap:30px;flex-wrap:wrap;font-size:13px;color:#D0DCF0;line-height:2;">
<div>
<b style="color:#CC77FF;">Step 1 β€” Uncertainty Score</b><br/>
<code style="background:#111D30;padding:4px 10px;border-radius:5px;
color:#C8A951;font-size:13px;">
unc_i = βˆ’1.8 Γ— |ŷᢜˑ˒ βˆ’ 0.5| + 1
</code><br/>
<span style="color:#6B85AA;font-size:12px;">
Max = 1.0 when fraud_score = 0.5 (model completely unsure)<br/>
Min β‰ˆ 0.1 when fraud_score β†’ 0 or β†’ 1 (model very confident)
</span>
</div>
<div>
<b style="color:#CC77FF;">Step 2 β€” Scale Factor (bATE)</b><br/>
<code style="background:#111D30;padding:4px 10px;border-radius:5px;
color:#C8A951;font-size:13px;">
S_i = unc_i Γ— log(ŷʳᡉᡛ + Ξ΅)
</code><br/>
<span style="color:#6B85AA;font-size:12px;">
High S_i = uncertain AND high revenue β†’ top exploration candidates<br/>
K-means++ diversity applied on top to avoid redundant picks
</span>
</div>
</div>
</div>""", unsafe_allow_html=True)
st.markdown("<br/>", unsafe_allow_html=True)
# ── Colour / marker helpers ───────────────────────────────────
ch_color = {"RED": "#C8102E", "YELLOW": "#F5A800", "GREEN": "#00843D"}
ch_marker = {"RED": "circle", "YELLOW": "diamond", "GREEN": "square"}
def base_layout(title, xlab, ylab, h=460):
return dict(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(family="IBM Plex Sans", color="#D0DCF0", size=11),
height=h,
title=dict(text=f"<b>{title}</b>",
font=dict(color=WCO_GOLD, size=14,
family="Playfair Display"), x=0.5),
xaxis=dict(title=xlab, gridcolor="#1E3A6E", zeroline=False),
yaxis=dict(title=ylab, gridcolor="#1E3A6E", zeroline=False),
legend=dict(bgcolor="#0F1C35", bordercolor=WCO_BORDER,
font=dict(size=10)),
margin=dict(l=60, r=20, t=55, b=55),
)
# ─────────────────────────────────────────────────────────────
# SCATTER 1 β€” fraud_score vs uncertainty_score (the unc_i curve)
# ─────────────────────────────────────────────────────────────
st.markdown('<div class="section-title">β‘  Fraud Score vs Uncertainty Score β€” The unc_i Curve</div>',
unsafe_allow_html=True)
st.markdown("""<div class="alert-blue">
The <b>uncertainty score unc_i</b> is a concave function maximised when
<code>fraud_score β‰ˆ 0.5</code>. Bills near 0.5 are the model's blind spots β€”
exploration targets. Bills near 0 or 1 are confident predictions (exploitation territory).
<b style="color:#CC77FF;">Purple stars</b> = actual gATE exploration picks.
</div>""", unsafe_allow_html=True)
# Theoretical curve overlay
x_curve = np.linspace(0, 1, 300)
y_curve = -1.8 * np.abs(x_curve - 0.5) + 1
fig1 = go.Figure()
# Theoretical curve
fig1.add_trace(go.Scatter(
x=x_curve, y=y_curve,
mode="lines",
name="unc_i = βˆ’1.8Γ—|Ε·βˆ’0.5|+1 (theoretical)",
line=dict(color="#C8A951", width=2.5, dash="dot"),
hoverinfo="skip",
))
# All bills coloured by channel
for ch in ["GREEN", "YELLOW", "RED"]:
sub = df[df["channel"] == ch]
non_exp = sub[sub["is_exploration"] == 0]
fig1.add_trace(go.Scatter(
x=non_exp["fraud_score"],
y=non_exp["uncertainty_score"],
mode="markers",
name=f"{ch} (exploitation)",
marker=dict(
color=ch_color[ch], size=5, opacity=0.45,
symbol=ch_marker[ch],
line=dict(width=0),
),
hovertemplate=(
f"<b>{ch}</b><br>"
"Fraud Score: %{x:.3f}<br>"
"Uncertainty: %{y:.3f}<br>"
"<extra></extra>"
),
))
# Exploration picks on top
exp_df = df[df["is_exploration"] == 1]
fig1.add_trace(go.Scatter(
x=exp_df["fraud_score"],
y=exp_df["uncertainty_score"],
mode="markers",
name="πŸ” gATE Exploration Picks",
marker=dict(
color="#CC77FF", size=11, opacity=0.95,
symbol="star",
line=dict(color="#ffffff", width=0.8),
),
hovertemplate=(
"<b>EXPLORATION PICK</b><br>"
"Bill: %{customdata[0]}<br>"
"Fraud Score: %{x:.3f}<br>"
"Uncertainty: %{y:.3f}<br>"
"Risk Area: %{customdata[1]}<br>"
"<extra></extra>"
),
customdata=exp_df[["bill_id", "risk_area"]].values,
))
# Vertical line at 0.5
fig1.add_vline(x=0.5, line=dict(color="#CC77FF", dash="dash", width=1.2),
annotation_text="Max uncertainty (0.5)",
annotation_font_color="#CC77FF",
annotation_position="top right")
# Shaded exploration zone (0.3 – 0.7)
fig1.add_vrect(x0=0.3, x1=0.7,
fillcolor="#CC77FF", opacity=0.05,
line_width=0,
annotation_text="Exploration zone",
annotation_font_color="#CC77FF",
annotation_position="top left")
fig1.update_layout(**base_layout(
"Fraud Score vs Uncertainty Score | unc_i = βˆ’1.8 Γ— |ŷᢜˑ˒ βˆ’ 0.5| + 1",
"Fraud Score ŷᢜˑ˒ (DATE exploitation output)",
"Uncertainty Score unc_i",
h=480,
))
st.plotly_chart(fig1, use_container_width=True)
# ─────────────────────────────────────────────────────────────
# SCATTER 2 β€” uncertainty_score vs log(pred_revenue) = Scale Factor S_i
# ─────────────────────────────────────────────────────────────
st.markdown('<div class="section-title">β‘‘ Uncertainty vs log(Revenue) β€” The S_i Scale Factor Space</div>',
unsafe_allow_html=True)
st.markdown("""<div class="alert-blue">
<b>S_i = unc_i Γ— log(ŷʳᡉᡛ + Ξ΅)</b> β€” bills in the
<b style="color:#CC77FF;">top-right quadrant</b> score highest and are
the primary exploration candidates: they are uncertain <i>and</i> financially significant.
Bottom-left = low uncertainty low revenue = safe GREEN facilitation.
</div>""", unsafe_allow_html=True)
epsilon = 1e-6
df_plot = df.copy()
df_plot["log_rev"] = np.log(df_plot["pred_revenue"] + epsilon)
fig2 = go.Figure()
for ch in ["GREEN", "YELLOW", "RED"]:
sub = df_plot[(df_plot["channel"] == ch) & (df_plot["is_exploration"] == 0)]
fig2.add_trace(go.Scatter(
x=sub["log_rev"],
y=sub["uncertainty_score"],
mode="markers",
name=f"{ch}",
marker=dict(
color=ch_color[ch], size=5, opacity=0.40,
symbol=ch_marker[ch], line=dict(width=0),
),
hovertemplate=(
f"<b>{ch}</b><br>"
"log(Revenue): %{x:.2f}<br>"
"Uncertainty: %{y:.3f}<br>"
"Scale Factor S_i: %{customdata:.3f}<br>"
"<extra></extra>"
),
customdata=sub["scale_factor"],
))
# Exploration picks
exp_plot = df_plot[df_plot["is_exploration"] == 1]
fig2.add_trace(go.Scatter(
x=exp_plot["log_rev"],
y=exp_plot["uncertainty_score"],
mode="markers",
name="πŸ” Exploration Picks",
marker=dict(color="#CC77FF", size=12, opacity=0.95,
symbol="star", line=dict(color="#fff", width=0.8)),
hovertemplate=(
"<b>EXPLORATION PICK</b><br>"
"Bill: %{customdata[0]}<br>"
"log(Rev): %{x:.2f}<br>"
"Uncertainty: %{y:.3f}<br>"
"S_i: %{customdata[1]:.3f}<br>"
"Risk Area: %{customdata[2]}<br>"
"<extra></extra>"
),
customdata=exp_plot[["bill_id", "scale_factor", "risk_area"]].values,
))
# Fraud detected overlay
fraud_plot = df_plot[df_plot["inspection_outcome"] == "FRAUD_DETECTED"]
fig2.add_trace(go.Scatter(
x=fraud_plot["log_rev"],
y=fraud_plot["uncertainty_score"],
mode="markers",
name="🚨 Fraud Detected",
marker=dict(color="#FF4466", size=8, opacity=0.85,
symbol="x", line=dict(color="#FF4466", width=1.5)),
hovertemplate=(
"<b>FRAUD DETECTED</b><br>"
"Bill: %{customdata[0]}<br>"
"Channel: %{customdata[1]}<br>"
"log(Rev): %{x:.2f}<br>"
"Uncertainty: %{y:.3f}<br>"
"<extra></extra>"
),
customdata=fraud_plot[["bill_id", "channel"]].values,
))
# Quadrant annotations
x_med = df_plot["log_rev"].median()
y_med = df_plot["uncertainty_score"].median()
fig2.add_hline(y=y_med, line=dict(color="#1E3A6E", dash="dash", width=1))
fig2.add_vline(x=x_med, line=dict(color="#1E3A6E", dash="dash", width=1))
for (xa, ya, txt, col) in [
(0.98, 0.98, "HIGH S_i<br>Explore", "#CC77FF"),
(0.02, 0.98, "Uncertain<br>Low Rev", "#F5A800"),
(0.98, 0.02, "High Rev<br>Confident", "#0066CC"),
(0.02, 0.02, "LOW S_i<br>Facilitate", "#00843D"),
]:
fig2.add_annotation(
xref="paper", yref="paper", x=xa, y=ya,
text=f"<b>{txt}</b>", showarrow=False,
font=dict(color=col, size=10),
bgcolor=col+"18", bordercolor=col,
borderwidth=1, borderpad=4,
)
fig2.update_layout(**base_layout(
"Uncertainty Score vs log(Revenue) | S_i = unc_i Γ— log(ŷʳᡉᡛ + Ξ΅)",
"log(Predicted Revenue ŷʳᡉᡛ)",
"Uncertainty Score unc_i",
h=500,
))
st.plotly_chart(fig2, use_container_width=True)
# ─────────────────────────────────────────────────────────────
# SCATTER 3 β€” Scale Factor S_i vs fraud_score, sized by revenue
# ─────────────────────────────────────────────────────────────
st.markdown('<div class="section-title">β‘’ Scale Factor S_i vs Fraud Score β€” Full bATE Decision Space</div>',
unsafe_allow_html=True)
st.markdown("""<div class="alert-blue">
The <b>scale factor S_i</b> (y-axis) is what bATE actually ranks bills by.
<b>Bubble size = predicted revenue.</b>
Exploration picks (⭐) cluster at <b>mid fraud-score Γ— high S_i</b> β€” uncertain AND valuable.
Exploitation picks cluster at <b>high fraud-score</b> regardless of uncertainty.
</div>""", unsafe_allow_html=True)
# Normalise bubble size
rev_norm = ((df["pred_revenue"] - df["pred_revenue"].min()) /
(df["pred_revenue"].max() - df["pred_revenue"].min() + 1e-6))
df_plot2 = df.copy()
df_plot2["bubble"] = 5 + rev_norm * 22
fig3 = go.Figure()
# Non-selected bills (GREEN channel, not exploration)
grn = df_plot2[(df_plot2["channel"] == "GREEN")]
fig3.add_trace(go.Scatter(
x=grn["fraud_score"], y=grn["scale_factor"],
mode="markers",
name="🟒 GREEN (Facilitated)",
marker=dict(color="#00843D", size=grn["bubble"], opacity=0.25,
line=dict(width=0)),
hovertemplate=(
"<b>GREEN</b><br>Fraud Score: %{x:.3f}<br>"
"S_i: %{y:.3f}<br>Revenue: $%{customdata:,.0f}<extra></extra>"
),
customdata=grn["pred_revenue"],
))
# Exploitation picks (RED + YELLOW, not exploration)
expl_picks = df_plot2[(df_plot2["channel"].isin(["RED","YELLOW"])) &
(df_plot2["is_exploration"] == 0)]
for ch in ["YELLOW", "RED"]:
sub = expl_picks[expl_picks["channel"] == ch]
fig3.add_trace(go.Scatter(
x=sub["fraud_score"], y=sub["scale_factor"],
mode="markers",
name=f"{'πŸ”΄' if ch=='RED' else '🟑'} {ch} (Exploitation)",
marker=dict(color=ch_color[ch], size=sub["bubble"],
opacity=0.65, symbol=ch_marker[ch],
line=dict(color=ch_color[ch], width=0.5)),
hovertemplate=(
f"<b>{ch} β€” EXPLOITATION</b><br>"
"Fraud Score: %{x:.3f}<br>"
"S_i: %{y:.3f}<br>"
"Revenue: $%{customdata[0]:,.0f}<br>"
"Bill: %{customdata[1]}<br>"
"<extra></extra>"
),
customdata=sub[["pred_revenue", "bill_id"]].values,
))
# Exploration picks
ep = df_plot2[df_plot2["is_exploration"] == 1]
fig3.add_trace(go.Scatter(
x=ep["fraud_score"], y=ep["scale_factor"],
mode="markers",
name="πŸ” gATE Exploration Picks",
marker=dict(color="#CC77FF", size=ep["bubble"] + 4,
opacity=1.0, symbol="star",
line=dict(color="#ffffff", width=1)),
hovertemplate=(
"<b>EXPLORATION PICK (gATE)</b><br>"
"Bill: %{customdata[0]}<br>"
"Risk Area: %{customdata[1]}<br>"
"Fraud Score: %{x:.3f}<br>"
"unc_i: %{customdata[2]:.3f}<br>"
"S_i: %{y:.3f}<br>"
"Pred Revenue: $%{customdata[3]:,.0f}<br>"
"<extra></extra>"
),
customdata=ep[["bill_id", "risk_area",
"uncertainty_score", "pred_revenue"]].values,
))
# Fraud detected X marks
fd = df_plot2[df_plot2["inspection_outcome"] == "FRAUD_DETECTED"]
fig3.add_trace(go.Scatter(
x=fd["fraud_score"], y=fd["scale_factor"],
mode="markers",
name="🚨 Fraud Detected",
marker=dict(color="#FF4466", size=10, opacity=0.9,
symbol="x-thin", line=dict(color="#FF4466", width=2)),
hovertemplate=(
"<b>FRAUD CONFIRMED</b><br>"
"Bill: %{customdata[0]}<br>"
"Channel: %{customdata[1]}<br>"
"Revenue Recovered: $%{customdata[2]:,.0f}<br>"
"<extra></extra>"
),
customdata=fd[["bill_id", "channel", "detected_revenue"]].values,
))
# Threshold line β€” top n% by S_i (exploration selection boundary)
n_explore = ep.shape[0]
if n_explore > 0:
si_threshold = df_plot2["scale_factor"].nlargest(n_explore * 3).min()
fig3.add_hline(
y=si_threshold,
line=dict(color="#CC77FF", dash="dash", width=1.3),
annotation_text=f" gATE S_i candidate threshold",
annotation_font_color="#CC77FF",
annotation_position="right",
)
fig3.update_layout(**base_layout(
"Scale Factor S_i vs Fraud Score | Bubble Size = Predicted Revenue",
"Fraud Score ŷᢜˑ˒ (DATE output β€” Exploitation axis)",
"Scale Factor S_i = unc_i Γ— log(ŷʳᡉᡛ + Ξ΅) (Exploration axis)",
h=520,
))
st.plotly_chart(fig3, use_container_width=True)
# ─────────────────────────────────────────────────────────────
# SCATTER 4 β€” Per risk area facet: fraud_score vs uncertainty_score
# ─────────────────────────────────────────────────────────────
st.markdown('<div class="section-title">β‘£ Per Risk Area β€” Exploration Picks in Context</div>',
unsafe_allow_html=True)
st.markdown("""<div class="alert-blue">
Same axes as Chart β‘  but broken down by risk area.
Shows <b>which risk areas have the most uncertain bills</b>
and whether exploration picks are concentrated in the right zone.
</div>""", unsafe_allow_html=True)
area_names = list(RISK_AREAS.keys())
cols_fa = st.columns(5)
for i, (area_name, area_cfg) in enumerate(RISK_AREAS.items()):
a_df = df[df["risk_area"] == area_name]
color = area_cfg["color"]
exp_a = a_df[a_df["is_exploration"] == 1]
fig_fa = go.Figure()
# All bills in area
fig_fa.add_trace(go.Scatter(
x=a_df["fraud_score"], y=a_df["uncertainty_score"],
mode="markers",
name="All Bills",
marker=dict(color=color, size=5, opacity=0.35,
line=dict(width=0)),
showlegend=False,
))
# Exploration picks
if len(exp_a) > 0:
fig_fa.add_trace(go.Scatter(
x=exp_a["fraud_score"], y=exp_a["uncertainty_score"],
mode="markers",
name="Exploration",
marker=dict(color="#CC77FF", size=10, opacity=0.95,
symbol="star",
line=dict(color="#fff", width=0.6)),
showlegend=False,
))
# Fraud
fd_a = a_df[a_df["inspection_outcome"] == "FRAUD_DETECTED"]
if len(fd_a) > 0:
fig_fa.add_trace(go.Scatter(
x=fd_a["fraud_score"], y=fd_a["uncertainty_score"],
mode="markers",
name="Fraud",
marker=dict(color="#FF4466", size=7, opacity=0.85,
symbol="x-thin",
line=dict(color="#FF4466", width=1.5)),
showlegend=False,
))
fig_fa.add_vline(x=0.5, line=dict(color="#CC77FF33", width=1))
fig_fa.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(color="#D0DCF0", size=9), height=240,
title=dict(
text=f"<b>{area_cfg['icon']} {area_name.split('/')[0]}</b>",
font=dict(color=color, size=11), x=0.5,
),
xaxis=dict(title="Fraud Score", gridcolor="#1E3A6E",
range=[0,1], tickfont=dict(size=8)),
yaxis=dict(title="unc_i", gridcolor="#1E3A6E",
range=[0,1.1], tickfont=dict(size=8)),
margin=dict(l=35, r=8, t=40, b=35),
showlegend=False,
)
with cols_fa[i]:
st.plotly_chart(fig_fa, use_container_width=True)
n_exp = len(exp_a)
n_tot = len(a_df)
n_fd = len(fd_a)
st.markdown(f"""
<div style="background:#0F1C35;border:1px solid {color};
border-radius:8px;padding:10px;font-size:11px;
text-align:center;margin-top:-10px;">
<span style="color:{color};">Total:</span> {n_tot}<br/>
<span style="color:#CC77FF;">⭐ Explore:</span> {n_exp}<br/>
<span style="color:#FF4466;">🚨 Fraud:</span> {n_fd}<br/>
<span style="color:#C8A951;">Avg S_i:</span>
{a_df['scale_factor'].mean():.2f}
</div>""", unsafe_allow_html=True)
# ─────────────────────────────────────────────────────────────
# Summary insight box
# ─────────────────────────────────────────────────────────────
st.markdown("<br/>", unsafe_allow_html=True)
exp_df_s = df[df["is_exploration"] == 1]
avg_unc = exp_df_s["uncertainty_score"].mean()
avg_si = exp_df_s["scale_factor"].mean()
avg_fs = exp_df_s["fraud_score"].mean()
all_avg_unc = df["uncertainty_score"].mean()
all_avg_si = df["scale_factor"].mean()
all_avg_fs = df["fraud_score"].mean()
st.markdown(f"""
<div class="wco-card-gold">
<h3>πŸ“ Exploration Selection Summary β€” Formula Validation</h3>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px;">
<div style="background:#111D30;border-radius:8px;padding:14px;text-align:center;">
<div style="color:#6B85AA;font-size:11px;text-transform:uppercase;
letter-spacing:0.07em;margin-bottom:6px;">Avg Fraud Score ŷᢜˑ˒</div>
<div style="color:#CC77FF;font-size:22px;font-weight:700;
font-family:'Georgia',serif;">{avg_fs:.3f}</div>
<div style="color:#6B85AA;font-size:11px;">Exploration picks</div>
<div style="color:#44CC88;font-size:18px;font-weight:700;margin-top:4px;">{all_avg_fs:.3f}</div>
<div style="color:#6B85AA;font-size:11px;">All bills (baseline)</div>
<div style="color:#C8A951;font-size:11px;margin-top:6px;">
{'βœ… Closer to 0.5 = correct' if abs(avg_fs-0.5) < abs(all_avg_fs-0.5) else '↔ Similar to baseline'}
</div>
</div>
<div style="background:#111D30;border-radius:8px;padding:14px;text-align:center;">
<div style="color:#6B85AA;font-size:11px;text-transform:uppercase;
letter-spacing:0.07em;margin-bottom:6px;">Avg Uncertainty unc_i</div>
<div style="color:#CC77FF;font-size:22px;font-weight:700;
font-family:'Georgia',serif;">{avg_unc:.3f}</div>
<div style="color:#6B85AA;font-size:11px;">Exploration picks</div>
<div style="color:#44CC88;font-size:18px;font-weight:700;margin-top:4px;">{all_avg_unc:.3f}</div>
<div style="color:#6B85AA;font-size:11px;">All bills (baseline)</div>
<div style="color:#C8A951;font-size:11px;margin-top:6px;">
{'βœ… Higher unc = targeting blind spots' if avg_unc > all_avg_unc else '↔ Check exploration ratio'}
</div>
</div>
<div style="background:#111D30;border-radius:8px;padding:14px;text-align:center;">
<div style="color:#6B85AA;font-size:11px;text-transform:uppercase;
letter-spacing:0.07em;margin-bottom:6px;">Avg Scale Factor S_i</div>
<div style="color:#CC77FF;font-size:22px;font-weight:700;
font-family:'Georgia',serif;">{avg_si:.3f}</div>
<div style="color:#6B85AA;font-size:11px;">Exploration picks</div>
<div style="color:#44CC88;font-size:18px;font-weight:700;margin-top:4px;">{all_avg_si:.3f}</div>
<div style="color:#6B85AA;font-size:11px;">All bills (baseline)</div>
<div style="color:#C8A951;font-size:11px;margin-top:6px;">
{'βœ… Higher S_i = uncertain + valuable' if avg_si > all_avg_si else '↔ Revenue weighting check'}
</div>
</div>
</div>
</div>""", unsafe_allow_html=True)
def show():
inject_global_css()
page_header("πŸ“Š", "Simulation Results & Analysis",
"RULE HIT TABLES Β· OFFENCE DATABASE GROWTH Β· EXPLORATION GAINS Β· WEIGHT EVOLUTION")
if "sim_df" not in st.session_state:
st.markdown("""
<div style="background:#0F1C35;border:2px dashed #1E3A6E;border-radius:14px;
padding:60px;text-align:center;margin-top:30px;">
<div style="font-size:40px;margin-bottom:12px;">⚠️</div>
<div style="color:#F5A800;font-size:18px;font-family:'Playfair Display',serif;">
No Simulation Data Found
</div>
<div style="color:#6B85AA;margin-top:10px;">
Please run the simulation on <b>Page 3</b> first.
</div>
</div>""", unsafe_allow_html=True)
return
df = st.session_state.sim_df
updated_weights = st.session_state.get("sim_weights", {})
orig_weights = st.session_state.get("rule_weights", {})
efficiency = st.session_state.get("sim_efficiency", {})
# KPI strip
fraud_total = (df["inspection_outcome"] == "FRAUD_DETECTED").sum()
offence_adds = df["added_to_offence_db"].sum()
explore_finds = df[(df["is_exploration"]==1) & (df["added_to_offence_db"]==1)].shape[0]
revenue = df["detected_revenue"].sum()
metric_row([
(fraud_total, "Frauds Detected", WCO_RED),
(offence_adds, "Offence DB Additions", WCO_GOLD),
(explore_finds, "Exploration Discoveries", "#CC77FF"),
(f"${revenue:,.0f}", "Revenue Recovered", WCO_GREEN),
(f"{efficiency.get('hybrid',{}).get('efficiency_index',0):.3f}",
"Efficiency Index", WCO_GOLD),
])
tabs = st.tabs([
"πŸ“‹ T1: Rule Hits", "πŸ“‘ T2: Channel Stats",
"πŸ” T3: Exploration Finds", "πŸ—„οΈ T4: Offence DB",
"βš–οΈ T5: Risk Scoring", "πŸ“ˆ Charts",
"🧭 Exploration Logic",
])
# ── TABLE 1: Rule Hits ────────────────────────────────────────
with tabs[0]:
st.markdown("""<div class="alert-blue">
How many bills were hit by each risk rule, and precision of each rule.
</div>""", unsafe_allow_html=True)
df_t1 = build_rule_hit_table(df)
# Mini bar chart
fig_t1 = go.Figure(go.Bar(
x=df_t1["Rule ID"], y=df_t1["Total Bills Hit"],
marker_color=df_t1["Risk Area"].map(
{k: v["color"] for k, v in RISK_AREAS.items()}),
opacity=0.85,
text=df_t1["Precision (%)"].apply(lambda x: f"{x}%"), textposition="outside",
textfont=dict(color="#D0DCF0", size=10),
hovertext=df_t1["Rule Name"], hoverinfo="text+y",
))
fig_t1.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(color="#D0DCF0", size=10), height=280,
title=dict(text="<b>Bills Hit per Rule (label = detection precision)</b>",
font=dict(color=WCO_GOLD, size=13), x=0.5),
xaxis=dict(gridcolor="#1E3A6E", tickangle=-45),
yaxis=dict(gridcolor="#1E3A6E", title="Bills Hit"),
margin=dict(l=45, r=15, t=45, b=75),
)
st.plotly_chart(fig_t1, use_container_width=True)
styled_table(df_t1, "Table 1 β€” Risk Rule Hit Analysis")
# ── TABLE 2: Channel Stats ────────────────────────────────────
with tabs[1]:
st.markdown("""<div class="alert-blue">
Bills treated per channel, exploitation vs exploration split, fraud detection per channel.
</div>""", unsafe_allow_html=True)
df_t2 = build_channel_table(df)
c1, c2 = st.columns(2)
with c1:
fig_ch = go.Figure(go.Bar(
x=df_t2["Channel"],
y=df_t2["Total Bills"],
marker_color=["#C8102E","#F5A800","#00843D"], opacity=0.85,
text=df_t2["Total Bills"], textposition="outside",
textfont=dict(color="#D0DCF0"),
))
fig_ch.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(color="#D0DCF0", size=12), height=300,
title=dict(text="<b>Bills per Channel</b>",
font=dict(color=WCO_GOLD, size=13), x=0.5),
xaxis=dict(gridcolor="#1E3A6E"),
yaxis=dict(gridcolor="#1E3A6E"),
margin=dict(l=45, r=15, t=45, b=40),
)
st.plotly_chart(fig_ch, use_container_width=True)
with c2:
fig_pie = go.Figure(go.Pie(
labels=["Exploitation","Exploration"],
values=[df_t2["Exploitation Bills"].sum(), df_t2["Exploration Bills"].sum()],
marker=dict(colors=["#0066CC","#CC77FF"],
line=dict(color="#070E1C", width=2)),
hole=0.55, textfont=dict(color="#D0DCF0"),
))
fig_pie.update_layout(
paper_bgcolor="#070E1C",
font=dict(color="#D0DCF0"), height=300,
title=dict(text="<b>Exploit vs Explore Split</b>",
font=dict(color=WCO_GOLD, size=13), x=0.5),
legend=dict(bgcolor="#0F1C35"),
margin=dict(l=20, r=20, t=45, b=20),
)
st.plotly_chart(fig_pie, use_container_width=True)
styled_table(df_t2, "Table 2 β€” Channel Treatment Statistics")
# ── TABLE 3: Exploration Discoveries ─────────────────────────
with tabs[2]:
st.markdown("""<div class="alert-blue">
New bills discovered and added to offence database via exploration strategy (concept drift detection).
</div>""", unsafe_allow_html=True)
df_t3 = build_exploration_discovery_table(df)
st.plotly_chart(exploration_vs_exploitation_chart(df), use_container_width=True)
styled_table(df_t3, "Table 3 β€” Exploration Discovery Analysis per Risk Area")
# ── TABLE 4: Offence DB ───────────────────────────────────────
with tabs[3]:
st.markdown("""<div class="alert-blue">
Real-time offence database augmentation from Red + Yellow channel inspection feedback.
</div>""", unsafe_allow_html=True)
st.plotly_chart(offence_db_growth_chart(df), use_container_width=True)
df_t4 = build_offence_db_table(df)
styled_table(df_t4, "Table 4 β€” Offence Database Augmentation by Risk Area")
# Radar chart of offence DB by area
areas = df_t4["Risk Area"].tolist()
vals = df_t4["Total Added"].tolist()
fig_radar = go.Figure(go.Scatterpolar(
r=vals + [vals[0]], theta=areas + [areas[0]],
fill="toself", fillcolor="rgba(200,169,81,0.15)",
line=dict(color=WCO_GOLD, width=2),
marker=dict(size=8, color=WCO_GOLD),
))
fig_radar.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(color="#D0DCF0", size=11), height=360,
title=dict(text="<b>Offence DB Coverage Radar</b>",
font=dict(color=WCO_GOLD, size=13), x=0.5),
polar=dict(bgcolor="#0B1220",
angularaxis=dict(gridcolor="#1E3A6E", linecolor="#1E3A6E"),
radialaxis=dict(gridcolor="#1E3A6E", linecolor="#1E3A6E")),
margin=dict(l=60, r=60, t=55, b=40),
)
st.plotly_chart(fig_radar, use_container_width=True)
# ── TABLE 5: Risk Scoring ─────────────────────────────────────
with tabs[4]:
st.markdown("""<div class="alert-blue">
Per-transaction risk scores, rule hits, cumulative weights, and weight evolution after
self-learning feedback. Shows how detection efficiency drives automatic weight uplift.
</div>""", unsafe_allow_html=True)
df_t5 = build_risk_score_table(df, updated_weights)
# Weight evolution chart
if orig_weights and updated_weights:
st.plotly_chart(weight_evolution_chart(df, orig_weights, updated_weights),
use_container_width=True)
styled_table(df_t5, "Table 5 β€” Transaction Risk Scoring & Weight Evolution",
highlight_col="Channel",
color_map={"RED": "#C8102E", "YELLOW": "#F5A800", "GREEN": "#00843D"})
# Weight delta table
st.markdown('<div class="section-title">πŸ“ˆ Rule Weight Update Summary</div>',
unsafe_allow_html=True)
weight_rows = []
for area_name, area_cfg in RISK_AREAS.items():
for rule in area_cfg["rules"]:
rid = rule["id"]
o = orig_weights.get(rid, rule["weight"])
u = updated_weights.get(rid, o)
weight_rows.append({
"Risk Area": area_name,
"Rule ID": rid,
"Rule Name": rule["name"],
"Original Weight":f"{o:.3f}",
"Updated Weight": f"{u:.3f}",
"Ξ” Weight": f"+{u-o:.4f}" if u >= o else f"{u-o:.4f}",
"% Change": f"{100*(u-o)/o:.1f}%" if o > 0 else "β€”",
})
df_wd = pd.DataFrame(weight_rows)
styled_table(df_wd, "Rule Weight Delta After Self-Learning Cycle")
# ── Charts summary tab ────────────────────────────────────────
with tabs[5]:
c1, c2 = st.columns(2)
with c1:
# Outcome distribution pie
outcomes = df[df["channel"] != "GREEN"]["inspection_outcome"].value_counts()
fig_out = go.Figure(go.Pie(
labels=outcomes.index, values=outcomes.values,
marker=dict(colors=[RISK_LEVEL_COLORS.get(k,"#6B85AA") for k in outcomes.index],
line=dict(color="#070E1C", width=2)),
hole=0.5, textfont=dict(color="#D0DCF0"),
))
fig_out.update_layout(
paper_bgcolor="#070E1C", font=dict(color="#D0DCF0"), height=340,
title=dict(text="<b>Inspection Outcome Distribution</b>",
font=dict(color=WCO_GOLD, size=13), x=0.5),
legend=dict(bgcolor="#0F1C35"), margin=dict(l=20,r=20,t=45,b=20),
)
st.plotly_chart(fig_out, use_container_width=True)
with c2:
# Revenue by risk area
rev_by_area = df.groupby("risk_area")["detected_revenue"].sum().reset_index()
colors = [RISK_AREAS[a]["color"] for a in rev_by_area["risk_area"]]
fig_rev = go.Figure(go.Bar(
x=rev_by_area["risk_area"],
y=rev_by_area["detected_revenue"],
marker_color=colors, opacity=0.85,
text=rev_by_area["detected_revenue"].apply(lambda x: f"${x:,.0f}"),
textposition="outside", textfont=dict(color="#D0DCF0", size=10),
))
fig_rev.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(color="#D0DCF0", size=10), height=340,
title=dict(text="<b>Revenue Recovered by Risk Area</b>",
font=dict(color=WCO_GOLD, size=13), x=0.5),
xaxis=dict(gridcolor="#1E3A6E", tickangle=-25),
yaxis=dict(gridcolor="#1E3A6E", title="Revenue ($)"),
margin=dict(l=50, r=15, t=45, b=80),
)
st.plotly_chart(fig_rev, use_container_width=True)
# Fraud score scatter
sample_df = df.sample(min(300, len(df)), random_state=42)
fig_sc = px.scatter(
sample_df, x="fraud_score", y="detected_revenue",
color="channel", symbol="risk_area",
color_discrete_map={"RED":"#C8102E","YELLOW":"#F5A800","GREEN":"#00843D"},
opacity=0.7, height=400,
labels={"fraud_score":"Fraud Score (DATE)","detected_revenue":"Revenue Detected ($)"},
title="<b>Fraud Score vs Revenue Recovered (Sample 300 bills)</b>",
)
fig_sc.update_layout(
paper_bgcolor="#070E1C", plot_bgcolor="#0B1220",
font=dict(color="#D0DCF0", family="IBM Plex Sans"),
title_font=dict(color=WCO_GOLD, family="Playfair Display"),
xaxis=dict(gridcolor="#1E3A6E"), yaxis=dict(gridcolor="#1E3A6E"),
legend=dict(bgcolor="#0F1C35", bordercolor=WCO_BORDER),
margin=dict(l=55, r=20, t=55, b=45),
)
st.plotly_chart(fig_sc, use_container_width=True)
# ── TAB 7: Exploration Logic Scatter ─────────────────────────
with tabs[6]:
exploration_scatter_tab(df)