Spaces:
Runtime error
Runtime error
| """MuleGuard pitch deck β dark command-center theme matching the live product. | |
| Large, high-contrast text. Mule-ring detection is the centerpiece (slide 7). | |
| Build: .venv/bin/python deck/build_deck.py | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from PIL import Image | |
| from pptx import Presentation | |
| from pptx.dml.color import RGBColor | |
| from pptx.enum.shapes import MSO_SHAPE | |
| from pptx.enum.text import MSO_ANCHOR, PP_ALIGN | |
| from pptx.util import Emu, Inches, Pt | |
| ROOT = Path(__file__).resolve().parents[1] | |
| FIG = ROOT / "reports" / "figures" | |
| DOCS = ROOT / "docs" | |
| META = json.load(open(ROOT / "artifacts" / "metadata.json")) | |
| RINGS = json.load(open(ROOT / "artifacts" / "rings.json")) | |
| BG = RGBColor(0x0B, 0x11, 0x19) | |
| PANEL = RGBColor(0x13, 0x1B, 0x27) | |
| PANEL2 = RGBColor(0x1A, 0x24, 0x33) | |
| BORDER = RGBColor(0x26, 0x34, 0x4A) | |
| WHITE = RGBColor(0xE7, 0xEE, 0xF6) | |
| MUTED = RGBColor(0x85, 0x93, 0xA6) | |
| TEAL = RGBColor(0x2D, 0xD4, 0xBF) | |
| RED = RGBColor(0xFF, 0x4D, 0x4F) | |
| AMBER = RGBColor(0xFF, 0x9F, 0x43) | |
| CARD_W = RGBColor(0xFF, 0xFF, 0xFF) | |
| HFONT, BFONT, MONO = "Trebuchet MS", "Calibri", "Consolas" | |
| SW, SH = Inches(13.333), Inches(7.5) | |
| prs = Presentation(); prs.slide_width, prs.slide_height = SW, SH | |
| BLANK = prs.slide_layouts[6] | |
| def slide(bg=BG): | |
| s = prs.slides.add_slide(BLANK) | |
| r = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SW, SH) | |
| r.fill.solid(); r.fill.fore_color.rgb = bg; r.line.fill.background(); r.shadow.inherit = False | |
| s.shapes._spTree.remove(r._element); s.shapes._spTree.insert(2, r._element) | |
| return s | |
| def box(s, x, y, w, h, text, size, color, *, bold=False, font=BFONT, align=PP_ALIGN.LEFT, | |
| anchor=MSO_ANCHOR.TOP, italic=False, ls=1.0): | |
| tb = s.shapes.add_textbox(x, y, w, h); tf = tb.text_frame | |
| tf.word_wrap = True; tf.vertical_anchor = anchor | |
| tf.margin_left = tf.margin_right = Pt(2); tf.margin_top = tf.margin_bottom = Pt(2) | |
| for i, ln in enumerate(text.split("\n")): | |
| p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() | |
| p.alignment = align; p.line_spacing = ls | |
| r = p.add_run(); r.text = ln | |
| r.font.size = Pt(size); r.font.bold = bold; r.font.italic = italic | |
| r.font.name = font; r.font.color.rgb = color | |
| return tb | |
| def rect(s, x, y, w, h, color, line=None, lw=1.0): | |
| r = s.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h) | |
| r.fill.solid(); r.fill.fore_color.rgb = color | |
| if line: r.line.color.rgb = line; r.line.width = Pt(lw) | |
| else: r.line.fill.background() | |
| r.shadow.inherit = False | |
| r.adjustments[0] = 0.06 | |
| return r | |
| def bar(s, x, y, w, h, color): | |
| r = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h) | |
| r.fill.solid(); r.fill.fore_color.rgb = color; r.line.fill.background(); r.shadow.inherit = False | |
| return r | |
| def node_title(s, title, color=TEAL): | |
| c = s.shapes.add_shape(MSO_SHAPE.OVAL, Inches(0.62), Inches(0.62), Inches(0.22), Inches(0.22)) | |
| c.fill.solid(); c.fill.fore_color.rgb = color; c.line.fill.background(); c.shadow.inherit = False | |
| box(s, Inches(1.0), Inches(0.45), Inches(11.7), Inches(0.95), title, 30, WHITE, | |
| bold=True, font=HFONT, anchor=MSO_ANCHOR.MIDDLE) | |
| bar(s, Inches(0.62), Inches(1.42), Inches(1.1), Pt(3), color) | |
| def stat(s, x, y, w, big, label, color=TEAL, big_size=46, panel=PANEL): | |
| rect(s, x, y, w, Inches(1.7), panel, line=BORDER) | |
| bar(s, x, y, Inches(0.06), Inches(1.7), color) | |
| box(s, x + Inches(0.12), y + Inches(0.22), w - Inches(0.2), Inches(0.95), big, big_size, color, | |
| bold=True, font=MONO, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) | |
| box(s, x + Inches(0.12), y + Inches(1.12), w - Inches(0.2), Inches(0.5), label, 12, MUTED, | |
| align=PP_ALIGN.CENTER) | |
| def pic_fit(s, path, rx, ry, rw, rh, card=False): | |
| iw, ih = Image.open(path).size; ar = iw / ih | |
| rw_e, rh_e = int(rw), int(rh) | |
| if rw_e / rh_e > ar: | |
| h = rh_e; w = int(rh_e * ar) | |
| else: | |
| w = rw_e; h = int(rw_e / ar) | |
| x = int(rx) + (rw_e - w) // 2; y = int(ry) + (rh_e - h) // 2 | |
| if card: | |
| pad = Inches(0.12) | |
| rect(s, Emu(x) - pad, Emu(y) - pad, Emu(w) + 2 * pad, Emu(h) + 2 * pad, CARD_W) | |
| s.shapes.add_picture(str(path), Emu(x), Emu(y), width=Emu(w), height=Emu(h)) | |
| cv, tm = META["cv_metrics"], META["test_metrics"] | |
| cm = tm["confusion_matrix"] | |
| # 1 ββ Title | |
| s = slide() | |
| bar(s, 0, Inches(2.95), SW, Pt(3), TEAL) | |
| c = s.shapes.add_shape(MSO_SHAPE.OVAL, Inches(0.95), Inches(1.95), Inches(0.3), Inches(0.3)) | |
| c.fill.solid(); c.fill.fore_color.rgb = TEAL; c.line.fill.background(); c.shadow.inherit = False | |
| box(s, Inches(1.5), Inches(1.55), Inches(11), Inches(1.2), "MULEGUARD", 60, WHITE, bold=True, font=HFONT) | |
| box(s, Inches(0.98), Inches(3.15), Inches(11.5), Inches(1.0), | |
| "Real-time AI/ML detection of suspicious mule accounts β and the rings they move money through.", | |
| 21, TEAL, italic=True, ls=1.1) | |
| stat(s, Inches(1.2), Inches(4.15), Inches(3.4), f"{cv['cv_pr_auc_mean']:.2f}", "CV PR-AUC", TEAL, 44) | |
| stat(s, Inches(4.95), Inches(4.15), Inches(3.4), f"{tm['recall']*100:.0f}%", "recall β mules caught", TEAL, 44) | |
| stat(s, Inches(8.7), Inches(4.15), Inches(3.4), f"{RINGS['n_rings']} rings", "mule rings surfaced", AMBER, 44) | |
| box(s, Inches(0.98), Inches(6.5), Inches(11.5), Inches(0.5), | |
| "BOI Hackathon Β· Problem Statement 2 Β· Classification of Suspicious Mule Accounts", 14, MUTED) | |
| # 2 ββ Problem | |
| s = slide(); node_title(s, "Mules move stolen money β in rings, in minutes") | |
| box(s, Inches(0.7), Inches(1.85), Inches(7.0), Inches(4.2), | |
| "Cyber-fraud relies on mule accounts to receive, layer and conceal stolen funds " | |
| "across UPI, IMPS, NEFT and cards.\n\n" | |
| "They are recruited and discarded fast, and they operate in networks β fan-in / fan-out " | |
| "and layering chains β that static rules can't keep up with.\n\n" | |
| "Banks need to learn the behaviour, flag accounts proactively, and act on the whole " | |
| "ring before the money is gone.", 19, WHITE, ls=1.2) | |
| stat(s, Inches(8.0), Inches(1.95), Inches(2.4), "βΉ11k cr+", "reported cyber-fraud\nloss, 2024 (I4C)", RED, 28) | |
| stat(s, Inches(10.65), Inches(1.95), Inches(2.0), "Minutes", "to act before\nfunds vanish", AMBER, 28) | |
| rect(s, Inches(8.0), Inches(3.95), Inches(4.65), Inches(2.1), PANEL, line=TEAL, lw=1.5) | |
| box(s, Inches(8.25), Inches(4.15), Inches(4.2), Inches(1.7), | |
| "Goal: separate mule from legitimate accounts β accurately, in real time, with auditable " | |
| "reasons, and surface the rings.", 17, WHITE, anchor=MSO_ANCHOR.MIDDLE, italic=True, ls=1.15) | |
| # 3 ββ Data | |
| s = slide(); node_title(s, "One dataset. Three hard problems at once") | |
| stat(s, Inches(0.7), Inches(1.85), Inches(2.85), "9,082", "accounts") | |
| stat(s, Inches(3.75), Inches(1.85), Inches(2.85), "3,924", "anonymized features") | |
| stat(s, Inches(6.8), Inches(1.85), Inches(2.85), "0.89%", "are mules", RED) | |
| stat(s, Inches(9.85), Inches(1.85), Inches(2.8), "81", "positives only", RED) | |
| box(s, Inches(0.7), Inches(4.0), Inches(11.9), Inches(2.6), | |
| "β’ Extreme class imbalance β 1 mule per ~112 accounts\n" | |
| "β’ Very high dimensionality β 3,924 mostly-anonymized features\n" | |
| "β’ Heavy missingness, mixed numeric & categorical types\n\n" | |
| "Accuracy is a trap: predicting βall legitβ already scores 99.1%. " | |
| "We optimize PR-AUC, recall-at-precision and F2 instead.", 19, WHITE, ls=1.35) | |
| # 4 ββ Architecture | |
| s = slide(); node_title(s, "MuleGuard β end to end") | |
| pic_fit(s, DOCS / "architecture.png", Inches(0.7), Inches(1.65), Inches(11.95), Inches(4.7), card=True) | |
| box(s, Inches(0.7), Inches(6.55), Inches(11.95), Inches(0.6), | |
| "Ingest β engineer features (+ anomaly fusion) β score β explain β detect rings β alert β " | |
| "freeze. Every stage shares the same persisted artifacts.", 14, MUTED, italic=True, | |
| align=PP_ALIGN.CENTER) | |
| # 5 ββ Rigor / leakage | |
| s = slide(); node_title(s, "We caught the leakage others would ship", RED) | |
| box(s, Inches(0.7), Inches(1.7), Inches(11.9), Inches(0.7), | |
| "A naive model scored a perfect 100% β a red flag, not a win. We traced it to two " | |
| "label-leaking features and removed them:", 18, WHITE) | |
| for i, (feat, desc, auc) in enumerate([ | |
| ("F3912", "A binary βfraud-flaggedβ indicator β 1 for 79 of 81 mules. A copy of the " | |
| "label, not learned behaviour.", "AUC 0.987"), | |
| ("F2230", "Observation month. ALL legit accounts in Oct-25, ALL mules in Sep / Nov / Dec-25 " | |
| "β a data-collection artifact.", "AUC 1.000")]): | |
| x = Inches(0.7) + i * Inches(6.1) | |
| rect(s, x, Inches(2.65), Inches(5.85), Inches(2.0), PANEL, line=RED, lw=1.5) | |
| box(s, x + Inches(0.3), Inches(2.8), Inches(2.8), Inches(0.7), feat, 30, RED, bold=True, font=MONO) | |
| box(s, x + Inches(3.2), Inches(2.92), Inches(2.4), Inches(0.5), auc, 16, MUTED, bold=True, | |
| align=PP_ALIGN.RIGHT) | |
| box(s, x + Inches(0.3), Inches(3.6), Inches(5.25), Inches(1.0), desc, 15, WHITE, ls=1.12) | |
| rect(s, Inches(0.7), Inches(4.95), Inches(11.95), Inches(1.75), PANEL, line=TEAL, lw=1.5) | |
| box(s, Inches(1.0), Inches(5.1), Inches(11.35), Inches(1.5), | |
| "After removing them, LightGBM reaches CV PR-AUC β 0.88 while a logistic baseline manages " | |
| "only 0.39. That gap is the proof: the model learns genuine non-linear behaviour β not a " | |
| "label shortcut. Honest metrics over a fake leaderboard.", 18, WHITE, anchor=MSO_ANCHOR.MIDDLE, | |
| italic=True, ls=1.18) | |
| # 6 ββ Results | |
| s = slide(); node_title(s, "Deployable β not just accurate") | |
| stat(s, Inches(0.7), Inches(1.9), Inches(2.85), f"{cv['cv_pr_auc_mean']:.2f}", "CV PR-AUC (Β±0.07)", TEAL, 50) | |
| stat(s, Inches(3.75), Inches(1.9), Inches(2.85), f"{tm['recall']*100:.0f}%", "recall β mules caught", TEAL, 50) | |
| stat(s, Inches(6.8), Inches(1.9), Inches(2.85), f"{tm['precision']*100:.0f}%", "precision", TEAL, 50) | |
| stat(s, Inches(9.85), Inches(1.9), Inches(2.8), f"{cm['tp']}/{cm['tp']+cm['fn']}", "mules caught on test", TEAL, 50) | |
| rect(s, Inches(0.7), Inches(4.0), Inches(11.95), Inches(2.3), PANEL, line=BORDER) | |
| box(s, Inches(1.05), Inches(4.3), Inches(11.3), Inches(1.8), | |
| f"On a held-out test set of 1,817 accounts:\n\n" | |
| f"β’ Caught {cm['tp']} of {cm['tp']+cm['fn']} mules, with only {cm['fp']} false positives " | |
| f"out of 1,801 legitimate accounts\n" | |
| f"β’ Just {tm['alerts_raised']} alerts raised β a {tm['alert_rate']:.1%} alert rate a real " | |
| f"fraud team can actually work", 19, WHITE, ls=1.3) | |
| # 7 ββ β Novelty: rings | |
| s = slide(); node_title(s, "We don't just flag accounts β we expose the rings", TEAL) | |
| pic_fit(s, DOCS / "mule_rings.png", Inches(0.6), Inches(1.6), Inches(8.1), Inches(5.5)) | |
| rx = Inches(8.95) | |
| stat(s, rx, Inches(1.75), Inches(3.7), f"{RINGS['mules_in_rings']}/{RINGS['total_mules']}", | |
| "mules sit inside a ring", RED, 40) | |
| stat(s, rx, Inches(3.6), Inches(3.7), f"{RINGS['n_rings']}", "dense rings surfaced", AMBER, 40) | |
| box(s, rx, Inches(5.45), Inches(3.7), Inches(1.7), | |
| "No transaction edges? We infer rings from behavioural co-similarity β a k-NN graph over the " | |
| "flagged accounts + Louvain communities. Freeze the ring, stop the circulation.", | |
| 14, WHITE, ls=1.15) | |
| # 8 ββ Explainability | |
| s = slide(); node_title(s, "Every alert is explainable") | |
| pic_fit(s, FIG / "shap_importance.png", Inches(0.7), Inches(1.7), Inches(6.0), Inches(5.0), card=True) | |
| box(s, Inches(7.1), Inches(1.85), Inches(5.5), Inches(2.6), | |
| "In banking, βwhy was this flagged?β must always have an answer.\n\n" | |
| "MuleGuard attaches SHAP reason codes to every score β the exact features driving risk up or " | |
| "down β and turns them into a one-line narrative.", 18, WHITE, ls=1.2) | |
| rect(s, Inches(7.1), Inches(4.85), Inches(5.5), Inches(1.7), PANEL, line=TEAL, lw=1.5) | |
| box(s, Inches(7.35), Inches(5.05), Inches(5.0), Inches(1.3), | |
| "βRisk 98/100 β driven mainly by F3898, F3914 and an elevated anomaly score.β", | |
| 17, WHITE, italic=True, anchor=MSO_ANCHOR.MIDDLE) | |
| # 9 ββ Live product | |
| s = slide(); node_title(s, "A live, deployed product β not a notebook") | |
| pic_fit(s, DOCS / "shot_overview.png", Inches(0.7), Inches(1.65), Inches(11.95), Inches(4.8)) | |
| box(s, Inches(0.7), Inches(6.6), Inches(11.95), Inches(0.6), | |
| "FastAPI scoring backend + Streamlit analyst console, fused live. Deployed public on " | |
| "HuggingFace, auto-synced from GitHub. SHAP reason codes on every alert.", 14, MUTED, | |
| italic=True, align=PP_ALIGN.CENTER) | |
| # 10 ββ Impact & roadmap | |
| s = slide(); node_title(s, "Impact & path to production") | |
| rect(s, Inches(0.7), Inches(1.75), Inches(5.85), Inches(4.1), PANEL, line=BORDER) | |
| box(s, Inches(1.0), Inches(2.0), Inches(5.3), Inches(0.5), "What it delivers today", 21, TEAL, | |
| bold=True, font=HFONT) | |
| box(s, Inches(1.0), Inches(2.75), Inches(5.3), Inches(3.0), | |
| "β’ Catches mules early, before funds are layered\n\n" | |
| "β’ 0.8% alert rate β ranked, explained, workable\n\n" | |
| "β’ Surfaces whole rings to freeze together\n\n" | |
| "β’ Adapts to evolving fraud via the anomaly score", 16.5, WHITE, ls=1.15) | |
| rect(s, Inches(6.8), Inches(1.75), Inches(5.85), Inches(4.1), PANEL, line=BORDER) | |
| box(s, Inches(7.1), Inches(2.0), Inches(5.3), Inches(0.5), "Productionization roadmap", 21, AMBER, | |
| bold=True, font=HFONT) | |
| box(s, Inches(7.1), Inches(2.75), Inches(5.3), Inches(3.0), | |
| "β Real regulatory feeds (I4C / NCRP) + core banking\n\n" | |
| "β Streaming scoring (Kafka) + retrain feedback loop\n\n" | |
| "β True money-flow graph for ring detection\n\n" | |
| "β Case management + auto-hold on Critical alerts", 16.5, WHITE, ls=1.15) | |
| # 11 ββ Closing | |
| s = slide() | |
| bar(s, 0, Inches(3.35), SW, Pt(3), TEAL) | |
| box(s, Inches(0.95), Inches(2.15), Inches(11.5), Inches(1.1), "MULEGUARD", 52, WHITE, bold=True, font=HFONT) | |
| box(s, Inches(0.98), Inches(3.6), Inches(11.5), Inches(1.6), | |
| "Honest ML Β· novelty detection Β· ring discovery Β· full explainability Β· a live demo.\n" | |
| "Built to actually stop fraudulent proceeds β not just to top a leaderboard.", | |
| 19, TEAL, italic=True, ls=1.25) | |
| box(s, Inches(0.98), Inches(6.4), Inches(11.5), Inches(0.5), | |
| "huggingface.co/spaces/EeshanSingh/MuleGuard Β· BOI Hackathon Β· PS2", 14, MUTED) | |
| OUT = ROOT / "deck" / "MuleGuard_Pitch.pptx" | |
| prs.save(str(OUT)) | |
| print("Saved", OUT, "Β·", len(prs.slides._sldIdLst), "slides") | |