""" 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"{area_name}
Bill: %{{x}}
Cumulative: %{{y}}", )) 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="Total
Bill: %{x}
Cumulative: %{y}", )) 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="Real-Time Offence Database Growth (Cumulative)", 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="Exploitation vs Exploration: Detection Comparison", 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="Rule Weight Evolution After Self-Learning Feedback", 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"""
{title}
""", 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("""

๐Ÿงญ bATE Exploration Logic โ€” All Bills Visualised

Step 1 โ€” Uncertainty Score
unc_i = โˆ’1.8 ร— |ลทแถœหกหข โˆ’ 0.5| + 1
Max = 1.0 when fraud_score = 0.5 (model completely unsure)
Min โ‰ˆ 0.1 when fraud_score โ†’ 0 or โ†’ 1 (model very confident)
Step 2 โ€” Scale Factor (bATE)
S_i = unc_i ร— log(ลทสณแต‰แต› + ฮต)
High S_i = uncertain AND high revenue โ†’ top exploration candidates
K-means++ diversity applied on top to avoid redundant picks
""", unsafe_allow_html=True) st.markdown("
", 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"{title}", 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('
โ‘  Fraud Score vs Uncertainty Score โ€” The unc_i Curve
', unsafe_allow_html=True) st.markdown("""
The uncertainty score unc_i is a concave function maximised when fraud_score โ‰ˆ 0.5. Bills near 0.5 are the model's blind spots โ€” exploration targets. Bills near 0 or 1 are confident predictions (exploitation territory). Purple stars = actual gATE exploration picks.
""", 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"{ch}
" "Fraud Score: %{x:.3f}
" "Uncertainty: %{y:.3f}
" "" ), )) # 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=( "EXPLORATION PICK
" "Bill: %{customdata[0]}
" "Fraud Score: %{x:.3f}
" "Uncertainty: %{y:.3f}
" "Risk Area: %{customdata[1]}
" "" ), 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('
โ‘ก Uncertainty vs log(Revenue) โ€” The S_i Scale Factor Space
', unsafe_allow_html=True) st.markdown("""
S_i = unc_i ร— log(ลทสณแต‰แต› + ฮต) โ€” bills in the top-right quadrant score highest and are the primary exploration candidates: they are uncertain and financially significant. Bottom-left = low uncertainty low revenue = safe GREEN facilitation.
""", 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"{ch}
" "log(Revenue): %{x:.2f}
" "Uncertainty: %{y:.3f}
" "Scale Factor S_i: %{customdata:.3f}
" "" ), 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=( "EXPLORATION PICK
" "Bill: %{customdata[0]}
" "log(Rev): %{x:.2f}
" "Uncertainty: %{y:.3f}
" "S_i: %{customdata[1]:.3f}
" "Risk Area: %{customdata[2]}
" "" ), 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=( "FRAUD DETECTED
" "Bill: %{customdata[0]}
" "Channel: %{customdata[1]}
" "log(Rev): %{x:.2f}
" "Uncertainty: %{y:.3f}
" "" ), 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
Explore", "#CC77FF"), (0.02, 0.98, "Uncertain
Low Rev", "#F5A800"), (0.98, 0.02, "High Rev
Confident", "#0066CC"), (0.02, 0.02, "LOW S_i
Facilitate", "#00843D"), ]: fig2.add_annotation( xref="paper", yref="paper", x=xa, y=ya, text=f"{txt}", 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('
โ‘ข Scale Factor S_i vs Fraud Score โ€” Full bATE Decision Space
', unsafe_allow_html=True) st.markdown("""
The scale factor S_i (y-axis) is what bATE actually ranks bills by. Bubble size = predicted revenue. Exploration picks (โญ) cluster at mid fraud-score ร— high S_i โ€” uncertain AND valuable. Exploitation picks cluster at high fraud-score regardless of uncertainty.
""", 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=( "GREEN
Fraud Score: %{x:.3f}
" "S_i: %{y:.3f}
Revenue: $%{customdata:,.0f}" ), 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"{ch} โ€” EXPLOITATION
" "Fraud Score: %{x:.3f}
" "S_i: %{y:.3f}
" "Revenue: $%{customdata[0]:,.0f}
" "Bill: %{customdata[1]}
" "" ), 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=( "EXPLORATION PICK (gATE)
" "Bill: %{customdata[0]}
" "Risk Area: %{customdata[1]}
" "Fraud Score: %{x:.3f}
" "unc_i: %{customdata[2]:.3f}
" "S_i: %{y:.3f}
" "Pred Revenue: $%{customdata[3]:,.0f}
" "" ), 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=( "FRAUD CONFIRMED
" "Bill: %{customdata[0]}
" "Channel: %{customdata[1]}
" "Revenue Recovered: $%{customdata[2]:,.0f}
" "" ), 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('
โ‘ฃ Per Risk Area โ€” Exploration Picks in Context
', unsafe_allow_html=True) st.markdown("""
Same axes as Chart โ‘  but broken down by risk area. Shows which risk areas have the most uncertain bills and whether exploration picks are concentrated in the right zone.
""", 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"{area_cfg['icon']} {area_name.split('/')[0]}", 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"""
Total: {n_tot}
โญ Explore: {n_exp}
๐Ÿšจ Fraud: {n_fd}
Avg S_i: {a_df['scale_factor'].mean():.2f}
""", unsafe_allow_html=True) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Summary insight box # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("
", 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"""

๐Ÿ“ Exploration Selection Summary โ€” Formula Validation

Avg Fraud Score ลทแถœหกหข
{avg_fs:.3f}
Exploration picks
{all_avg_fs:.3f}
All bills (baseline)
{'โœ… Closer to 0.5 = correct' if abs(avg_fs-0.5) < abs(all_avg_fs-0.5) else 'โ†” Similar to baseline'}
Avg Uncertainty unc_i
{avg_unc:.3f}
Exploration picks
{all_avg_unc:.3f}
All bills (baseline)
{'โœ… Higher unc = targeting blind spots' if avg_unc > all_avg_unc else 'โ†” Check exploration ratio'}
Avg Scale Factor S_i
{avg_si:.3f}
Exploration picks
{all_avg_si:.3f}
All bills (baseline)
{'โœ… Higher S_i = uncertain + valuable' if avg_si > all_avg_si else 'โ†” Revenue weighting check'}
""", 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("""
โš ๏ธ
No Simulation Data Found
Please run the simulation on Page 3 first.
""", 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("""
How many bills were hit by each risk rule, and precision of each rule.
""", 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="Bills Hit per Rule (label = detection precision)", 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("""
Bills treated per channel, exploitation vs exploration split, fraud detection per channel.
""", 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="Bills per Channel", 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="Exploit vs Explore Split", 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("""
New bills discovered and added to offence database via exploration strategy (concept drift detection).
""", 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("""
Real-time offence database augmentation from Red + Yellow channel inspection feedback.
""", 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="Offence DB Coverage Radar", 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("""
Per-transaction risk scores, rule hits, cumulative weights, and weight evolution after self-learning feedback. Shows how detection efficiency drives automatic weight uplift.
""", 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('
๐Ÿ“ˆ Rule Weight Update Summary
', 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="Inspection Outcome Distribution", 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="Revenue Recovered by Risk Area", 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="Fraud Score vs Revenue Recovered (Sample 300 bills)", ) 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)