Download app.py from DuckerMaster/Fraud-Detection-Multi-Model: direct link, hf CLI and curl.
- Browser
- Download file 15 kB
-
https://huggingface.co/spaces/DuckerMaster/Fraud-Detection-Multi-Model/resolve/main/app.py
- Command line
-
hf download hf://spaces/DuckerMaster/Fraud-Detection-Multi-Model/app.py
-
curl -L -o app.py https://huggingface.co/spaces/DuckerMaster/Fraud-Detection-Multi-Model/resolve/main/app.py
15 kB
| import time | |
| import random | |
| from datetime import datetime | |
| import streamlit as st | |
| from simulator import generate_accounts, generate_transaction, score_transaction | |
| # ─── Page config ────────────────────────────────────────────────────────────── | |
| st.set_page_config( | |
| page_title="Fraud Analytics — Enterprise Monitor", | |
| page_icon="🏛️", | |
| layout="wide", | |
| initial_sidebar_state="expanded", | |
| ) | |
| # ─── Styling ────────────────────────────────────────────────────────────────── | |
| st.markdown(""" | |
| <style> | |
| /* App Background & Base Typography */ | |
| .stApp { background-color: #f8fafc; } | |
| /* Custom Table Design for Bank Grid */ | |
| .bank-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 12px; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: white; | |
| } | |
| .bank-table th { | |
| background-color: #f1f5f9; | |
| color: #475569; | |
| text-align: left; | |
| padding: 8px 10px; | |
| border-bottom: 2px solid #cbd5e1; | |
| font-weight: 600; | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| } | |
| .bank-table td { | |
| padding: 7px 10px; | |
| border-bottom: 1px solid #e2e8f0; | |
| color: #334155; | |
| vertical-align: middle; | |
| } | |
| .bank-table tr:hover { background-color: #f1f5f9; } | |
| /* Decision Styles */ | |
| .text-pass { color: #16a34a; font-weight: 600; } | |
| .text-alert { color: #d97706; font-weight: 600; } | |
| .text-block { color: #dc2626; font-weight: 600; } | |
| .status-badge { | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| font-weight: 500; | |
| } | |
| /* GitHub Card Sticked to Sidebar Bottom */ | |
| .github-card { | |
| background: #0f172a; | |
| color: #ffffff; | |
| padding: 14px; | |
| border-radius: 8px; | |
| margin-top: 50px; | |
| border: 1px solid #1e293b; | |
| } | |
| .github-card a { | |
| color: #38bdf8 !important; | |
| text-decoration: none; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 13px; | |
| } | |
| .github-card a:hover { text-decoration: underline; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ─── Session state init ─────────────────────────────────────────────────────── | |
| def _init(): | |
| st.session_state.setdefault("running", False) | |
| st.session_state.setdefault("accounts", []) | |
| st.session_state.setdefault("feed", []) # สะสมจากบนลงล่าง | |
| st.session_state.setdefault("alerts", []) # สะสมจากบนลงล่าง | |
| st.session_state.setdefault("step", 1) | |
| st.session_state.setdefault("tx_counter", 1) | |
| st.session_state.setdefault("total_tx", 0) | |
| st.session_state.setdefault("total_block", 0) | |
| st.session_state.setdefault("total_alert", 0) | |
| st.session_state.setdefault("total_pass", 0) | |
| st.session_state.setdefault("amount_risk", 0.0) | |
| _init() | |
| # ─── Sidebar (Configuration & Hidden Controls) ──────────────────────────────── | |
| with st.sidebar: | |
| st.markdown("### ⚙️ Engine Settings") | |
| st.caption("Adjust variables for the transaction generator.") | |
| st.divider() | |
| speed = st.slider("Generator Delay (seconds)", 0.3, 3.0, 1.0, 0.1) | |
| fraud_pct = st.slider("Mule Account Ratio (%)", 5, 40, 12, 1) | |
| n_accs = st.slider("Accounts Network Size", 3, 8, 5, 1) | |
| st.divider() | |
| st.markdown("**System Actions**") | |
| reset_btn = st.button("↺ Reset Simulation Data", use_container_width=True) | |
| if reset_btn: | |
| for k in ["running","accounts","feed","alerts","step","tx_counter", | |
| "total_tx","total_block","total_alert","total_pass","amount_risk"]: | |
| st.session_state.pop(k, None) | |
| _init() | |
| st.rerun() | |
| # GitHub Link ตรึงไว้ล่างสุดของ Sidebar | |
| st.markdown(""" | |
| <div class="github-card"> | |
| <p style="margin:0 0 6px 0; font-size:11px; color:#94a3b8;">Want to learn more?</p> | |
| <a href="https://github.com/PlamGG/Fraud-Detection-Multi-Model-" target="_blank"> | |
| <svg height="18" width="18" viewBox="0 0 16 16" fill="currentColor" style="color:white;"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg> | |
| Read Technical Docs | |
| </a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ─── Main Header & Dynamic Bank Controls ────────────────────────────────────── | |
| title_col, ctrl_col = st.columns([2, 1]) | |
| with title_col: | |
| st.markdown("## 🏛️ Fraud Operations — Live Matrix Dashboard") | |
| status_txt = "🟢 MONITORING ACTIVE" if st.session_state.running else "⏸️ MONITORING PAUSED" | |
| st.caption(f"{status_txt} · Model Ensemble Framework A × B · Threshold Rules Enabled") | |
| with ctrl_col: | |
| st.markdown("<div style='padding-top: 12px;'></div>", unsafe_allow_html=True) | |
| c1, c2 = st.columns(2) | |
| start_btn = c1.button("▶ Start / Resume", use_container_width=True, type="primary", disabled=st.session_state.running) | |
| pause_btn = c2.button("⏸ Pause", use_container_width=True, disabled=not st.session_state.running) | |
| if start_btn: | |
| if not st.session_state.accounts: | |
| seed = random.randint(0, 99999) | |
| st.session_state.accounts = generate_accounts(n=n_accs, seed=seed) | |
| st.session_state.running = True | |
| st.rerun() | |
| if pause_btn: | |
| st.session_state.running = False | |
| st.rerun() | |
| st.divider() | |
| # ─── Top Split Grid: Financial Metrics & Accounts Grid ──────────────────────── | |
| metric_col, watchlist_col = st.columns([6, 4], gap="medium") | |
| with metric_col: | |
| m1, m2, m3, m4 = st.columns(4) | |
| m1.metric("Processed Transactions", f"{st.session_state.total_tx} tx") | |
| m2.metric("Critical Blocked", st.session_state.total_block) | |
| m3.metric("OTP Challenges", st.session_state.total_alert) | |
| m4.metric("Funds at Risk Exposure", f"฿{st.session_state.amount_risk:,.0f}") | |
| with watchlist_col: | |
| st.markdown("<p style='font-size:12px; font-weight:600; color:#475569; margin:0 0 6px 0;'>🎯 Account Audit Matrix</p>", unsafe_allow_html=True) | |
| if st.session_state.accounts: | |
| acc_table = """<table class="bank-table"> | |
| <tr><th>Account ID</th><th>Baseline Risk</th><th>Operational Status</th></tr>""" | |
| for acc in st.session_state.accounts: | |
| icon, status_color = { | |
| "normal": ("🟢 Normal", "background:#dcfce7;color:#16a34a"), | |
| "alert": ("🟡 Warning", "background:#fef3c7;color:#d97706"), | |
| "blocked": ("🔴 Blocked", "background:#fee2e2;color:#dc2626") | |
| }.get(acc["status"], ("⚪ Unknown", "")) | |
| risk_badge = f"🔥 {acc['risk_level'].upper()}" if acc['risk_level'] == 'high' else acc['risk_level'].upper() | |
| acc_table += f"""<tr> | |
| <td><code style='font-size:11px;'>{acc['account_id']}</code></td> | |
| <td>{risk_badge}</td> | |
| <td><span class="status-badge" style="{status_color}">{icon}</span></td> | |
| </tr>""" | |
| acc_table += "</table>" | |
| st.markdown(acc_table, unsafe_allow_html=True) | |
| else: | |
| st.caption("No accounts mapped. Click 'Start / Resume' to link core systems.") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| # ─── Bottom Split Grid: Fixed-Height Containers ─────────────────────────────── | |
| feed_col, alert_col = st.columns([55, 45], gap="medium") | |
| with feed_col: | |
| st.markdown("#### ⏱️ Real-Time Transaction Ledger") | |
| st.caption("Global financial traffic logging (Ordered: $001 \\rightarrow 002 \\rightarrow 003$)") | |
| feed_box = st.container(height=480) | |
| with alert_col: | |
| st.markdown("#### 🚨 Security Incident Alerts Queue") | |
| st.caption("Decisions requiring compliance review or automated intercept") | |
| alert_box = st.container(height=480) | |
| # ─── Function to build table HTML (Append-friendly) ────────────────────────── | |
| def build_feed_table(feed_list) -> str: | |
| html_table = """<table class="bank-table"> | |
| <tr><th>Time</th><th>Tx ID</th><th>Routing Sequence</th><th>Amount</th><th>Score</th><th>Action</th></tr>""" | |
| for f_tx in feed_list[-40:]: | |
| style_cls = "text-block" if "Block" in f_tx["decision"] else "text-alert" if "Alert" in f_tx["decision"] else "text-pass" | |
| html_table += f"""<tr> | |
| <td style='color:#64748b; font-family:monospace;'>{f_tx.get('tx_time', '00:00:00')}</td> | |
| <td><strong>{f_tx['tx_id']}</strong></td> | |
| <td><code>{f_tx['account_id']}</code> ➔ <code>{f_tx['dest_id']}</code></td> | |
| <td><b>฿{f_tx['amount']:,.0f}</b></td> | |
| <td><code>{f_tx['final_score']:.2f}</code></td> | |
| <td class="{style_cls}">{f_tx['decision'].upper()}</td> | |
| </tr>""" | |
| html_table += "</table>" | |
| return html_table | |
| # ─── Simulation Core Loop & Rendering Logic ────────────────────────────────── | |
| if st.session_state.running and st.session_state.accounts: | |
| # 📌 สร้าง Placeholder ไว้ "นอกลูป" แค่ครั้งเดียว เพื่อล็อคหัวตารางให้ไม่วาดซ้ำซาก | |
| with feed_box: | |
| feed_placeholder = st.empty() | |
| with alert_box: | |
| alert_placeholder = st.empty() | |
| while st.session_state.running: | |
| # Pick account & process transaction | |
| account = random.choice(st.session_state.accounts) | |
| raw_tx = generate_transaction(account, step=st.session_state.step, tx_counter=st.session_state.tx_counter) | |
| tx = score_transaction(raw_tx) | |
| # ฝังเวลาปัจจุบันของระบบธนาคารระดับวินาที | |
| tx['tx_time'] = datetime.now().strftime("%H:%M:%S") | |
| # Handle status state mutations | |
| d = tx["decision"] | |
| if "Block" in d: account["status"] = "blocked" | |
| elif "Alert" in d: account["status"] = "alert" | |
| # สะสมข้อมูลธุรกรรมต่อท้ายลิสต์เดิมลงไปข้างล่าง | |
| st.session_state.feed = st.session_state.feed + [tx] | |
| st.session_state.total_tx += 1 | |
| st.session_state.tx_counter += 1 | |
| if "Block" in d: | |
| st.session_state.total_block += 1 | |
| st.session_state.alerts = st.session_state.alerts + [tx] | |
| st.session_state.amount_risk += tx["amount"] | |
| elif "Alert" in d: | |
| st.session_state.total_alert += 1 | |
| st.session_state.alerts = st.session_state.alerts + [tx] | |
| st.session_state.amount_risk += tx["amount"] * 0.5 | |
| else: | |
| st.session_state.total_pass += 1 | |
| if st.session_state.tx_counter % 6 == 0: | |
| st.session_state.step += 1 | |
| # 📌 เขียนทับกล่องเดิมเพื่อเพิ่มแถวใหม่ (+1 แถวต่อท้ายในตารางเดี่ยว) หัวตารางจะไม่ซ้ำแล้ว | |
| feed_placeholder.markdown(build_feed_table(st.session_state.feed), unsafe_allow_html=True) | |
| # อัปเดตฝั่ง Alerts Queue ในกล่องเดิมเช่นกันแบบเรียบเนียน | |
| with alert_placeholder.container(): | |
| if st.session_state.alerts: | |
| for a_tx in st.session_state.alerts[-20:]: | |
| title_text = f"🚨 [{a_tx['tx_time']}] {a_tx['tx_id']} | ฿{a_tx['amount']:,.0f} ➔ {a_tx['decision'].upper()}" | |
| with st.expander(title_text): | |
| st.markdown(f""" | |
| **Incident Deep-Dive Metrics:** | |
| * **Timestamp:** `{a_tx['tx_time']}` | |
| * **Source Account:** `{a_tx['account_id']}` | |
| * **Destination Target:** `{a_tx['dest_id']}` | |
| * **Model A Probability (Behavior Engine):** `{a_tx['prob_a']:.3f}` | |
| * **Model B Probability (Counterparty Network):** `{a_tx['prob_b']:.3f}` | |
| * **Weighted Ensemble Score:** `{a_tx['final_score']:.3f}` | |
| * **Pipeline Detection Layer:** `{a_tx['layer']}` | |
| * **Hard Rules Active:** `Rule 1 (Remote Access): {a_tx['rule1']}` | `Rule 2 (Hesitation Pattern): {a_tx['rule2']}` | |
| """) | |
| else: | |
| st.success("🟢 OPERATIONAL STATUS: SECURED — No anomalies flagged by models currently.") | |
| time.sleep(speed) | |
| else: | |
| # Static render loop เมื่อกด PAUSE (ตารางคงสภาพล่าสุดนิ่งๆ ไว้ให้ตรวจสอบ) | |
| with feed_box: | |
| if st.session_state.feed: | |
| st.markdown(build_feed_table(st.session_state.feed), unsafe_allow_html=True) | |
| else: | |
| st.info("System Ready. Click **▶ Start / Resume** to map ledger feeds.") | |
| with alert_box: | |
| if st.session_state.alerts: | |
| for a_tx in st.session_state.alerts[-20:]: | |
| title_text = f"🚨 [{a_tx['tx_time']}] {a_tx['tx_id']} | ฿{a_tx['amount']:,.0f} ➔ {a_tx['decision'].upper()}" | |
| with st.expander(title_text): | |
| st.markdown(f""" | |
| **Incident Deep-Dive Metrics:** | |
| * **Timestamp:** `{a_tx['tx_time']}` | |
| * **Source Account:** `{a_tx['account_id']}` | |
| * **Destination Target:** `{a_tx['dest_id']}` | |
| * **Model A Probability:** `{a_tx['prob_a']:.3f}` | |
| * **Model B Probability:** `{a_tx['prob_b']:.3f}` | |
| * **Weighted Ensemble Score:** `{a_tx['final_score']:.3f}` | |
| """) | |
| else: | |
| st.success("🟢 OPERATIONAL STATUS: SECURED — No anomalies flagged by models currently.") |