Spaces:
Runtime error
Runtime error
| """ | |
| 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) | |