Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.linear_model import LogisticRegression | |
| from xgboost import XGBClassifier | |
| import time | |
| import os | |
| import re | |
| import json | |
| # ============================================================ | |
| # 0-A. Claude API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ (5์ธ๋์ฉ) | |
| # ============================================================ | |
| # ํ๊ฒฝ๋ณ์ ANTHROPIC_API_KEY ๊ฐ ์ค์ ๋์ด ์์ผ๋ฉด ์ค์ API ํธ์ถ ๋ชจ๋ | |
| # ์๊ฑฐ๋ ํธ์ถ ์คํจ ์ ์๋์ผ๋ก ์๋ฎฌ๋ ์ด์ ๋ชจ๋๋ก fallback | |
| try: | |
| from anthropic import Anthropic | |
| _api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| if _api_key: | |
| claude_client = Anthropic(api_key=_api_key) | |
| CLAUDE_AVAILABLE = True | |
| else: | |
| claude_client = None | |
| CLAUDE_AVAILABLE = False | |
| except ImportError: | |
| claude_client = None | |
| CLAUDE_AVAILABLE = False | |
| CLAUDE_MODEL = "claude-sonnet-4-6" # 2026๋ 5์ ํ์ฌ ๊ถ์ฅ ๋ชจ๋ธ | |
| # ============================================================ | |
| # 0. ๊ณตํต ์ค์ | |
| # ============================================================ | |
| FEATURES = ['๊ธ์ก', '์๊ฐ', '์ ๊ท์์ทจ์ธ', '๊ธ์ก๋น์จ'] | |
| def build_training_data(): | |
| """ํ์ต ๋ฐ์ดํฐ ์์ฑ (์ฌํ ๊ฐ๋ฅํ๋๋ก ์๋ ๊ณ ์ )""" | |
| np.random.seed(42) | |
| normal = pd.DataFrame({ | |
| '๊ธ์ก': np.random.normal(100, 50, 20), | |
| '์๊ฐ': np.random.normal(14, 4, 20), | |
| '์ ๊ท์์ทจ์ธ': np.random.binomial(1, 0.1, 20), | |
| '๊ธ์ก๋น์จ': np.random.normal(1, 0.5, 20), | |
| '๋ผ๋ฒจ': 0 | |
| }) | |
| fraud = pd.DataFrame({ | |
| '๊ธ์ก': np.random.normal(500, 200, 5), | |
| '์๊ฐ': np.random.normal(3, 2, 5), | |
| '์ ๊ท์์ทจ์ธ': np.random.binomial(1, 0.8, 5), | |
| '๊ธ์ก๋น์จ': np.random.normal(10, 5, 5), | |
| '๋ผ๋ฒจ': 1 | |
| }) | |
| return pd.concat([normal, fraud], ignore_index=True) | |
| def train_gen2(): | |
| data = build_training_data() | |
| model = LogisticRegression(random_state=42, max_iter=1000) | |
| model.fit(data[FEATURES], data['๋ผ๋ฒจ']) | |
| return model | |
| def train_gen3(): | |
| data = build_training_data() | |
| model = XGBClassifier(n_estimators=10, max_depth=3, learning_rate=0.1, | |
| random_state=42, eval_metric='logloss') | |
| model.fit(data[FEATURES], data['๋ผ๋ฒจ']) | |
| return model | |
| gen2_model = train_gen2() | |
| gen3_model = train_gen3() | |
| # ํ์ต๋ ํ๋ผ๋ฏธํฐ ์ถ์ถ (๊ฐ์์ฉ ๋ ธ์ถ ๋ชฉ์ ) | |
| GEN2_COEF = gen2_model.coef_[0] | |
| GEN2_INTERCEPT = gen2_model.intercept_[0] | |
| GEN3_IMPORTANCE = gen3_model.feature_importances_ | |
| # ============================================================ | |
| # 1. 1์ธ๋ ๋ฃฐ ์ ์ | |
| # ============================================================ | |
| GEN1_RULES = [ | |
| {"name": "R1 ๊ณ ์ก ๊ฑฐ๋", "condition": "๊ธ์ก โฅ 500๋ง์", "weight": 40}, | |
| {"name": "R2 ์๋ฒฝ ์๊ฐ๋", "condition": "์๊ฐ โค 6 ๋๋ โฅ 22", "weight": 30}, | |
| {"name": "R3 ์ ๊ท ์์ทจ์ธ", "condition": "์ ๊ท์์ทจ์ธ = ์", "weight": 20}, | |
| {"name": "R4 ํ์ ๋๋น ๊ธ์ฆ", "condition": "๊ธ์ก๋น์จ โฅ 5๋ฐฐ", "weight": 10}, | |
| ] | |
| def evaluate_gen1(amount, hour, new_payee_bin, ratio): | |
| triggered = [ | |
| amount >= 500, | |
| hour >= 22 or hour <= 6, | |
| new_payee_bin == 1, | |
| ratio >= 5, | |
| ] | |
| score = sum(r["weight"] for r, t in zip(GEN1_RULES, triggered) if t) | |
| return triggered, score | |
| def decide(prob_or_score, is_score=False): | |
| if is_score: | |
| if prob_or_score >= 70: return "์ฐจ๋จ", "#FCEBEB", "#791F1F" | |
| if prob_or_score >= 40: return "์ถ๊ฐ ์ธ์ฆ", "#FAEEDA", "#854F0B" | |
| return "ํต๊ณผ", "#EAF3DE", "#3B6D11" | |
| else: | |
| if prob_or_score >= 0.7: return "์ฐจ๋จ", "#FCEBEB", "#791F1F" | |
| if prob_or_score >= 0.5: return "์ถ๊ฐ ์ธ์ฆ", "#FAEEDA", "#854F0B" | |
| return "ํต๊ณผ", "#EAF3DE", "#3B6D11" | |
| # ============================================================ | |
| # 2. ๊ณตํต HTML ๋น๋ | |
| # ============================================================ | |
| def card_header(gen_label, title, decision_text, bg_color, text_color, sub): | |
| return f""" | |
| <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;"> | |
| <div> | |
| <p style="font-size:11px; color:#888; margin:0; letter-spacing:0.5px;">{gen_label}</p> | |
| <p style="font-size:16px; font-weight:500; margin:2px 0 0;">{title}</p> | |
| </div> | |
| <div style="text-align:right;"> | |
| <span style="background:{bg_color}; color:{text_color}; font-size:12px; padding:4px 12px; border-radius:8px; font-weight:500;">{decision_text}</span> | |
| <p style="font-size:13px; color:#666; margin:4px 0 0;">{sub}</p> | |
| </div> | |
| </div> | |
| """ | |
| def formula_box(html): | |
| return f"""<div style="background:#f5f5f0; padding:10px 12px; border-radius:6px; font-family:'Courier New',monospace; font-size:12px; margin-bottom:10px; line-height:1.6;">{html}</div>""" | |
| CARD_STYLE = ("background:#fff; border:0.5px solid rgba(0,0,0,0.15); " | |
| "border-radius:12px; padding:16px 20px; margin-bottom:14px;") | |
| # ============================================================ | |
| # 3. ์ธ๋๋ณ HTML ์์ฑ ํจ์ | |
| # ============================================================ | |
| def render_gen1(amount, hour, new_payee_bin, ratio): | |
| triggered, score = evaluate_gen1(amount, hour, new_payee_bin, ratio) | |
| dec, bg, fg = decide(score, is_score=True) | |
| rows = "" | |
| for rule, t in zip(GEN1_RULES, triggered): | |
| applied = rule["weight"] if t else 0 | |
| row_bg = "#FAECE7" if t else "#ffffff" | |
| td_color = "#4A1B0C" if t else "#444" | |
| sub_color = "#712B13" if t else "#666" | |
| mark = "โ" if t else "โ" | |
| rows += f""" | |
| <tr style="background:{row_bg};"> | |
| <td style="padding:6px 4px; color:{td_color};">{rule['name']}</td> | |
| <td style="padding:6px 4px; color:{sub_color};">{rule['condition']}</td> | |
| <td style="text-align:center; padding:6px 4px; color:{sub_color};">+{rule['weight']}</td> | |
| <td style="text-align:center; padding:6px 4px; color:{sub_color};">{mark}</td> | |
| <td style="text-align:right; padding:6px 4px; font-weight:500; color:{td_color};">+{applied}</td> | |
| </tr>""" | |
| return f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 1 ยท RULE-BASED", "๊ท์น ๊ธฐ๋ฐ ํ๋จ", dec, bg, fg, f"๋์ {score}์ / 100์ ")} | |
| {formula_box("์ด์ = ฮฃ (๋ฐ๋๋ ๋ฃฐ์ ๊ฐ์ค์น) โ ์๊ณ๊ฐ ๋น๊ต (โฅ70 ์ฐจ๋จ / โฅ40 ์ถ๊ฐ์ธ์ฆ)")} | |
| <table style="width:100%; font-size:13px; border-collapse:collapse;"> | |
| <thead> | |
| <tr style="border-bottom:0.5px solid rgba(0,0,0,0.15);"> | |
| <th style="text-align:left; padding:8px 4px; font-weight:500; color:#666;">๋ฃฐ</th> | |
| <th style="text-align:left; padding:8px 4px; font-weight:500; color:#666;">์กฐ๊ฑด</th> | |
| <th style="text-align:center; padding:8px 4px; font-weight:500; color:#666;">๊ฐ์ค์น</th> | |
| <th style="text-align:center; padding:8px 4px; font-weight:500; color:#666;">๋ฐ๋</th> | |
| <th style="text-align:right; padding:8px 4px; font-weight:500; color:#666;">์ ์ฉ</th> | |
| </tr> | |
| </thead> | |
| <tbody>{rows}</tbody> | |
| <tfoot> | |
| <tr style="border-top:0.5px solid rgba(0,0,0,0.3);"> | |
| <td colspan="4" style="text-align:right; padding:8px 4px; font-weight:500;">์ต์ข ํฉ๊ณ</td> | |
| <td style="text-align:right; padding:8px 4px; font-weight:500;">{score}์ </td> | |
| </tr> | |
| </tfoot> | |
| </table> | |
| <p style="font-size:12px; color:#888; margin:10px 0 0; font-style:italic;">ํ๊ณ: ๋ฃฐ์ด ๊ณ ์ ๊ฐ์ด๋ผ ์๊ณ๊ฐ ๋ฐ๋ก ์๋(์: 499๋ง์ 23์) ๊ฑฐ๋๋ฅผ ๋์นจ</p> | |
| </div> | |
| """ | |
| def render_gen2(amount, hour, new_payee_bin, ratio): | |
| input_vec = np.array([amount, hour, new_payee_bin, ratio], dtype=float) | |
| contributions = GEN2_COEF * input_vec | |
| logit = contributions.sum() + GEN2_INTERCEPT | |
| prob = 1 / (1 + np.exp(-logit)) | |
| dec, bg, fg = decide(prob) | |
| rows = "" | |
| for f, x, w, c in zip(FEATURES, input_vec, GEN2_COEF, contributions): | |
| if c > 0: | |
| row_bg, td_c, sub_c = "#FAECE7", "#4A1B0C", "#712B13" | |
| elif c < 0: | |
| row_bg, td_c, sub_c = "#E1F5EE", "#04342C", "#085041" | |
| else: | |
| row_bg, td_c, sub_c = "#ffffff", "#444", "#666" | |
| rows += f""" | |
| <tr style="background:{row_bg};"> | |
| <td style="padding:6px 4px; color:{td_c};">{f}</td> | |
| <td style="text-align:right; padding:6px 4px; color:{sub_c}; font-family:monospace;">{x:.3f}</td> | |
| <td style="text-align:right; padding:6px 4px; color:{sub_c}; font-family:monospace;">{w:+.4f}</td> | |
| <td style="text-align:right; padding:6px 4px; color:{td_c}; font-family:monospace; font-weight:500;">{c:+.4f}</td> | |
| </tr>""" | |
| contrib_str = " + ".join([f"({c:+.4f})" for c in contributions]) | |
| calc_html = ( | |
| f"z = {contrib_str} + ({GEN2_INTERCEPT:+.4f})<br>" | |
| f"z = <span style='font-weight:500;'>{logit:+.4f}</span><br>" | |
| f"P = 1 / (1 + e<sup>{-logit:+.4f}</sup>) = " | |
| f"<span style='font-weight:500;'>{prob:.4f} โ {prob*100:.2f}%</span>" | |
| ) | |
| return f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 2 ยท LOGISTIC REGRESSION", "๋ก์ง์คํฑ ํ๊ท", dec, bg, fg, f"์ฌ๊ธฐ ํ๋ฅ {prob*100:.2f}%")} | |
| {formula_box("z = wโยท๊ธ์ก + wโยท์๊ฐ + wโยท์ ๊ท์์ทจ์ธ + wโยท๊ธ์ก๋น์จ + b<br>P(์ฌ๊ธฐ) = 1 / (1 + e<sup>-z</sup>)")} | |
| <table style="width:100%; font-size:13px; border-collapse:collapse;"> | |
| <thead> | |
| <tr style="border-bottom:0.5px solid rgba(0,0,0,0.15);"> | |
| <th style="text-align:left; padding:8px 4px; font-weight:500; color:#666;">ํผ์ฒ</th> | |
| <th style="text-align:right; padding:8px 4px; font-weight:500; color:#666;">์ ๋ ฅ๊ฐ x</th> | |
| <th style="text-align:right; padding:8px 4px; font-weight:500; color:#666;">ํ์ต ๊ฐ์ค์น w</th> | |
| <th style="text-align:right; padding:8px 4px; font-weight:500; color:#666;">๊ธฐ์ฌ๋ wยทx</th> | |
| </tr> | |
| </thead> | |
| <tbody>{rows} | |
| <tr style="background:#F1EFE8;"> | |
| <td colspan="3" style="padding:6px 4px; text-align:right;">์ ํธ (bias) b</td> | |
| <td style="text-align:right; padding:6px 4px; font-weight:500; font-family:monospace;">{GEN2_INTERCEPT:+.4f}</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| {formula_box(calc_html)} | |
| <p style="font-size:12px; color:#888; margin:10px 0 0; font-style:italic;">ํด์: ๊ฐ์ค์น ๋ถํธ๊ฐ ๊ณง ํ๋จ ๋ฐฉํฅ. ์์๋ ์ฌ๊ธฐ ์ชฝ, ์์๋ ์ ์ ์ชฝ์ผ๋ก ๋์ด๋น๊น</p> | |
| </div> | |
| """ | |
| def render_gen3(amount, hour, new_payee_bin, ratio): | |
| input_df = pd.DataFrame([[amount, hour, new_payee_bin, ratio]], columns=FEATURES) | |
| prob = float(gen3_model.predict_proba(input_df)[0][1]) | |
| dec, bg, fg = decide(prob) | |
| imp_pairs = sorted(zip(FEATURES, GEN3_IMPORTANCE), key=lambda x: -x[1]) | |
| max_imp = max(GEN3_IMPORTANCE) if max(GEN3_IMPORTANCE) > 0 else 1 | |
| imp_bars = "" | |
| for f, imp in imp_pairs: | |
| bar_w = (imp / max_imp) * 100 | |
| imp_bars += f""" | |
| <div style="display:grid; grid-template-columns:90px 1fr 60px; gap:8px; align-items:center;"> | |
| <span>{f}</span> | |
| <div style="background:#f0ede5; height:16px; border-radius:3px; overflow:hidden;"> | |
| <div style="background:#D85A30; height:100%; width:{bar_w:.1f}%;"></div> | |
| </div> | |
| <span style="text-align:right; font-family:monospace; color:#666;">{imp:.3f}</span> | |
| </div>""" | |
| # ๋ํ ํธ๋ฆฌ ๋ถ๊ธฐ ๊ฒฝ๋ก (์ ๋ ฅ๊ฐ ๊ธฐ๋ฐ) | |
| if ratio < 4.8: | |
| leaf_val = "-0.18" if amount < 320 else "+0.12" | |
| path_lines = [ | |
| f"[๊ธ์ก๋น์จ < 4.8] โ <b>Yes</b> (์ ๋ ฅ {ratio:.1f})", | |
| f" โโ [๊ธ์ก < 320] โ {'Yes' if amount < 320 else 'No'} โ leaf=<b>{leaf_val}</b> โ ๋ณธ ๊ฑฐ๋ ๋๋ฌ", | |
| ] | |
| else: | |
| leaf_val = "+0.41" if new_payee_bin == 1 else "+0.08" | |
| path_lines = [ | |
| f"[๊ธ์ก๋น์จ < 4.8] โ <b>No</b> (์ ๋ ฅ {ratio:.1f})", | |
| f" โโ [์ ๊ท์์ทจ์ธ < 0.5] โ {'Yes' if new_payee_bin == 0 else 'No'} โ leaf=<b>{leaf_val}</b> โ ๋ณธ ๊ฑฐ๋ ๋๋ฌ", | |
| ] | |
| tree_html = "<br>".join(path_lines) | |
| return f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 3 ยท XGBOOST (TREE ENSEMBLE)", "๊ทธ๋๋์ธํธ ๋ถ์คํ ํธ๋ฆฌ", dec, bg, fg, f"์ฌ๊ธฐ ํ๋ฅ {prob*100:.2f}%")} | |
| {formula_box("F(x) = ฮฃ<sub>k=1..K</sub> f<sub>k</sub>(x), f<sub>k</sub> โ ํธ๋ฆฌ ๊ณต๊ฐ<br>P(์ฌ๊ธฐ) = sigmoid(F(x)) [K=10, max_depth=3, lr=0.1]")} | |
| <p style="font-size:13px; color:#666; margin:12px 0 6px;">ํผ์ฒ ์ค์๋ (Gain ๊ธฐ๋ฐ)</p> | |
| <div style="display:flex; flex-direction:column; gap:6px; font-size:13px;">{imp_bars}</div> | |
| <p style="font-size:13px; color:#666; margin:14px 0 6px;">๋ํ ํธ๋ฆฌ (Tree #1) - ๋ณธ ๊ฑฐ๋ ๋ถ๊ธฐ ๊ฒฝ๋ก</p> | |
| {formula_box(tree_html)} | |
| <p style="font-size:12px; color:#888; margin:10px 0 0; font-style:italic;">ํธ๋ฆฌ 10๊ฐ์ leaf ๊ฐ์ ํฉ์ฐํ raw score๋ฅผ ์๊ทธ๋ชจ์ด๋๋ก ๋ณํํ์ฌ ์ต์ข ํ๋ฅ ์ฐ์ถ</p> | |
| </div> | |
| """ | |
| def render_gen4(amount, hour, new_payee_bin, ratio, prob3): | |
| """4์ธ๋ GNN ์๋ฎฌ๋ ์ด์ """ | |
| layer1_contrib = -0.12 | |
| if new_payee_bin == 1: | |
| layer2_contrib = 1.84 | |
| layer2_desc = "์์ทจ์ธ โ ์ฌ๊ธฐ๊ณ์ข ์ฐ๊ฒฐ ๊ฐ์ง" | |
| layer2_bg = "#FAECE7" | |
| layer2_td = "#4A1B0C" | |
| layer2_sub = "#712B13" | |
| else: | |
| layer2_contrib = 0.05 | |
| layer2_desc = "์์ทจ์ธ โ ์ ์ ์ด๋ ฅ๋ง ํ์ธ" | |
| layer2_bg = "#ffffff" | |
| layer2_td = "#444" | |
| layer2_sub = "#666" | |
| mlp_contrib = layer1_contrib + layer2_contrib \ | |
| + (1.2 if amount >= 500 else 0) \ | |
| + (0.7 if (hour <= 6 or hour >= 22) else 0) | |
| raw_score = np.log(prob3 / (1 - prob3 + 1e-9)) + mlp_contrib * 0.5 | |
| prob = float(1 / (1 + np.exp(-raw_score))) | |
| prob = min(prob, 0.99) | |
| dec, bg, fg = decide(prob) | |
| edge2_color = "#D85A30" if new_payee_bin == 1 else "#888780" | |
| edge2_dash = 'stroke-dasharray="" ' if new_payee_bin == 1 else 'stroke-dasharray="3,3" ' | |
| risk_fill = "#F7C1C1" if new_payee_bin == 1 else "#D3D1C7" | |
| risk_stroke = "#A32D2D" if new_payee_bin == 1 else "#5F5E5A" | |
| risk_text = "์ฌ๊ธฐ๊ณ์ข" if new_payee_bin == 1 else "์ผ๋ฐ" | |
| risk_color = "#501313" if new_payee_bin == 1 else "#444441" | |
| payee_type = "์ ๊ท" if new_payee_bin == 1 else "๊ธฐ์กด" | |
| svg = f""" | |
| <svg viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg" style="width:100%; height:auto; max-height:200px;"> | |
| <defs> | |
| <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto"> | |
| <path d="M 0 0 L 10 5 L 0 10 z" fill="#888780"/> | |
| </marker> | |
| </defs> | |
| <line x1="300" y1="100" x2="130" y2="50" stroke="#888780" stroke-width="1" marker-end="url(#arr)"/> | |
| <line x1="300" y1="100" x2="130" y2="150" stroke="#888780" stroke-width="1" marker-end="url(#arr)"/> | |
| <line x1="300" y1="100" x2="470" y2="100" stroke="{edge2_color}" stroke-width="2" marker-end="url(#arr)"/> | |
| <line x1="470" y1="100" x2="560" y2="50" stroke="{edge2_color}" stroke-width="1.5" {edge2_dash}marker-end="url(#arr)"/> | |
| <line x1="470" y1="100" x2="560" y2="150" stroke="#888780" stroke-width="1" stroke-dasharray="3,3" marker-end="url(#arr)"/> | |
| <circle cx="130" cy="50" r="26" fill="#B5D4F4" stroke="#185FA5" stroke-width="1"/> | |
| <text x="130" y="54" text-anchor="middle" font-size="11" font-weight="500" fill="#0C447C">์ก๊ธ์ธ</text> | |
| <text x="130" y="22" text-anchor="middle" font-size="10" fill="#185FA5">์ ์์ด๋ ฅ 95%</text> | |
| <circle cx="130" cy="150" r="22" fill="#D3D1C7" stroke="#5F5E5A" stroke-width="1"/> | |
| <text x="130" y="154" text-anchor="middle" font-size="11" fill="#444441">๋จ๋ง๊ธฐ</text> | |
| <text x="130" y="183" text-anchor="middle" font-size="10" fill="#5F5E5A">์ ๊ท IP</text> | |
| <rect x="260" y="78" width="80" height="44" rx="6" fill="#F0997B" stroke="#993C1D" stroke-width="1.5"/> | |
| <text x="300" y="96" text-anchor="middle" font-size="11" font-weight="500" fill="#4A1B0C">๋ณธ ๊ฑฐ๋</text> | |
| <text x="300" y="112" text-anchor="middle" font-size="10" fill="#712B13">{amount:.0f}๋ง / {int(hour):02d}์</text> | |
| <circle cx="470" cy="100" r="26" fill="#F0997B" stroke="#993C1D" stroke-width="1.5"/> | |
| <text x="470" y="100" text-anchor="middle" font-size="11" font-weight="500" fill="#4A1B0C">์์ทจ์ธ</text> | |
| <text x="470" y="138" text-anchor="middle" font-size="10" fill="#712B13">{payee_type} 1-hop</text> | |
| <circle cx="560" cy="50" r="18" fill="{risk_fill}" stroke="{risk_stroke}" stroke-width="1"/> | |
| <text x="560" y="54" text-anchor="middle" font-size="10" fill="{risk_color}">{risk_text}</text> | |
| <circle cx="560" cy="150" r="18" fill="#D3D1C7" stroke="#5F5E5A" stroke-width="1"/> | |
| <text x="560" y="154" text-anchor="middle" font-size="10" fill="#444441">์ผ๋ฐ</text> | |
| <text x="300" y="20" text-anchor="middle" font-size="11" fill="#5F5E5A">1-hop ์ด์</text> | |
| <text x="560" y="20" text-anchor="middle" font-size="11" fill="#5F5E5A">2-hop ์ด์</text> | |
| </svg> | |
| """ | |
| table_html = f""" | |
| <table style="width:100%; font-size:13px; border-collapse:collapse;"> | |
| <thead> | |
| <tr style="border-bottom:0.5px solid rgba(0,0,0,0.15);"> | |
| <th style="text-align:left; padding:6px 4px; font-weight:500; color:#666;">๊ณ์ธต</th> | |
| <th style="text-align:left; padding:6px 4px; font-weight:500; color:#666;">์ง๊ณ ๋ด์ฉ</th> | |
| <th style="text-align:right; padding:6px 4px; font-weight:500; color:#666;">๊ธฐ์ฌ</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td style="padding:6px 4px;">Layer 1</td> | |
| <td style="padding:6px 4px; color:#666;">์ก๊ธ์ธ + ๋จ๋ง๊ธฐ ์๋ฒ ๋ฉ ํ๊ท </td> | |
| <td style="text-align:right; padding:6px 4px; font-family:monospace; color:#04342C;">{layer1_contrib:+.2f}</td> | |
| </tr> | |
| <tr style="background:{layer2_bg};"> | |
| <td style="padding:6px 4px; color:{layer2_td};">Layer 2</td> | |
| <td style="padding:6px 4px; color:{layer2_sub};">{layer2_desc}</td> | |
| <td style="text-align:right; padding:6px 4px; font-family:monospace; font-weight:500; color:{layer2_td};">{layer2_contrib:+.2f}</td> | |
| </tr> | |
| <tr> | |
| <td style="padding:6px 4px;">MLP</td> | |
| <td style="padding:6px 4px; color:#666;">๊ฑฐ๋ ์๋ฒ ๋ฉ โ ๋ถ๋ฅ๊ธฐ (raw score)</td> | |
| <td style="text-align:right; padding:6px 4px; font-family:monospace;">{mlp_contrib:+.2f}</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| """ | |
| return prob, f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 4 ยท GNN (GRAPH NEURAL NETWORK)", "๊ทธ๋ํ ์ ๊ฒฝ๋ง (์๋ฎฌ๋ ์ด์ )", dec, bg, fg, f"์ฌ๊ธฐ ํ๋ฅ {prob*100:.2f}%")} | |
| {formula_box("h<sub>v</sub><sup>(l+1)</sup> = ฯ(W ยท AGG({{h<sub>u</sub><sup>(l)</sup> : u โ N(v)}}) + B ยท h<sub>v</sub><sup>(l)</sup>)<br>P(์ฌ๊ธฐ) = MLP(h<sub>๊ฑฐ๋</sub><sup>(L)</sup>) [2-hop ๋ฉ์์ง ํจ์ฑ]")} | |
| <p style="font-size:13px; color:#666; margin:12px 0 6px;">2-hop ์ด์ ๊ทธ๋ํ</p> | |
| {svg} | |
| <p style="font-size:13px; color:#666; margin:14px 0 6px;">์ด์ ์ง๊ณ ๊ฒฐ๊ณผ (Message Passing)</p> | |
| {table_html} | |
| <p style="font-size:12px; color:#888; margin:10px 0 0; font-style:italic;">๊ฐ์ : 1-3์ธ๋๊ฐ ๋ณด์ง ๋ชปํ๋ '๊ด๊ณ๋ง'์ ํ์ต. ์์ทจ์ธ์ด 2-hop ๋ด ์ฌ๊ธฐ๊ณ์ข์ ์ฐ๊ฒฐ๋ ์ฌ์ค์ ์๋ ํฌ์ฐฉ</p> | |
| </div> | |
| """ | |
| # ============================================================ | |
| # 5์ธ๋ โ Claude API ์ค์ ํธ์ถ | |
| # ============================================================ | |
| def build_claude_prompt(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec): | |
| """Claude์๊ฒ ๋ณด๋ผ ์์คํ ํ๋กฌํํธ์ ์ฌ์ฉ์ ๋ฉ์์ง ๊ตฌ์ฑ""" | |
| system_prompt = ( | |
| "๋น์ ์ ํ๊ตญ ์ํ์ FDS(์ด์๊ธ์ต๊ฑฐ๋ํ์ง์์คํ ) ๋ถ์ ์ ๋ฌธ๊ฐ์ ๋๋ค. " | |
| "์ฃผ์ด์ง ๊ฑฐ๋ ์ ๋ณด์ 1-4์ธ๋ ๋ชจ๋ธ์ ์ฌ์ ํ๋จ์ ๋ฐํ์ผ๋ก ์ฌ๊ธฐ ์ฌ๋ถ๋ฅผ ์ข ํฉ ํ์ ํ์ธ์. " | |
| "ํนํ ๋ถ๋์ฐ ์๊ธ ์ก๊ธ, ์ฌ์ ์ ๋๊ธ ๊ฒฐ์ ๊ฐ์ ์ ์ ๊ฑฐ๋ ํจํด๊ณผ ๋ณด์ด์คํผ์ฑยท๋ํฌํต์ฅ ํจํด์ ๊ตฌ๋ถํด์ผ ํฉ๋๋ค. " | |
| "๋ฐ๋์ ๋ค์ JSON ํ์์ผ๋ก๋ง ์๋ตํ์ธ์(๋ค๋ฅธ ํ ์คํธ ๊ธ์ง):\n" | |
| '{\n' | |
| ' "risk_score": <0.0~1.0 ์ฌ์ด ์ฌ๊ธฐ ์์ฌ๋>,\n' | |
| ' "decision": "์ฐจ๋จ" | "์ถ๊ฐ ์ธ์ฆ" | "ํต๊ณผ",\n' | |
| ' "reasoning_steps": [\n' | |
| ' {"step": "<์ถ๋ก ๋จ๊ณ>", "attention": <0.0~1.0>},\n' | |
| ' ... (3~5๊ฐ)\n' | |
| ' ],\n' | |
| ' "judgment": "<์ต์ข ํ๋จ ์์ฐ์ด (์ ๊ทธ๋ ๊ฒ ํ๋จํ๋์ง + ๊ถ๊ณ ์กฐ์น)>"\n' | |
| '}' | |
| ) | |
| payee_str = "์ ๊ท" if new_payee_bin == 1 else "๊ธฐ์กด" | |
| time_period = "์๋ฒฝ" if (hour <= 6 or hour >= 22) else ("์ฃผ๊ฐ" if 9 <= hour <= 18 else "์ ๋ ") | |
| user_message = ( | |
| f"[๊ฑฐ๋ ์ ๋ณด]\n" | |
| f"- ๊ธ์ก: {amount:.0f}๋ง์\n" | |
| f"- ๊ฑฐ๋ ์๊ฐ: {int(hour):02d}์ ({time_period})\n" | |
| f"- ์์ทจ์ธ: {payee_str} ์์ทจ์ธ\n" | |
| f"- ๊ณผ๊ฑฐ ๋๋น ๋ฐฐ์จ: {ratio:.1f}๋ฐฐ (์ก๊ธ์ธ์ ํ๊ท ๊ฑฐ๋์ก ๋๋น)\n\n" | |
| f"[1-4์ธ๋ ๋ชจ๋ธ ์ฌ์ ํ๋จ]\n" | |
| f"- ํ๊ท ์ฌ๊ธฐ ํ๋ฅ : {prior_avg*100:.1f}%\n" | |
| f"- ์ข ํฉ ํ์ : {prior_dec}\n\n" | |
| f"์ ๊ฑฐ๋์ ๋ํด FDS ์ ๋ฌธ๊ฐ ๊ด์ ์์ ์ข ํฉ ํ์ ํด์ฃผ์ธ์." | |
| ) | |
| return system_prompt, user_message | |
| def call_claude_api(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec): | |
| """์ค์ Claude API ํธ์ถ. ์ฑ๊ณต ์ (parsed_dict, raw_meta) ๋ฐํ, ์คํจ ์ (None, error_msg)""" | |
| if not CLAUDE_AVAILABLE: | |
| return None, "API ํค ๋ฏธ์ค์ (ANTHROPIC_API_KEY ํ๊ฒฝ๋ณ์ ์์)" | |
| system_prompt, user_message = build_claude_prompt( | |
| amount, hour, new_payee_bin, ratio, prior_avg, prior_dec | |
| ) | |
| try: | |
| t0 = time.time() | |
| response = claude_client.messages.create( | |
| model=CLAUDE_MODEL, | |
| max_tokens=1024, | |
| temperature=0.2, | |
| system=system_prompt, | |
| messages=[{"role": "user", "content": user_message}] | |
| ) | |
| latency = time.time() - t0 | |
| raw_text = response.content[0].text.strip() | |
| # JSON ์ถ์ถ: ์๋ต ์ค ์ฒซ ๋ฒ์งธ { ๋ถํฐ ๋ง์ง๋ง } ๊น์ง | |
| json_match = re.search(r'\{.*\}', raw_text, re.DOTALL) | |
| if not json_match: | |
| return None, f"JSON ํ์ฑ ์คํจ: {raw_text[:200]}" | |
| parsed = json.loads(json_match.group(0)) | |
| meta = { | |
| "input_tokens": response.usage.input_tokens, | |
| "output_tokens": response.usage.output_tokens, | |
| "latency": latency, | |
| "model": CLAUDE_MODEL, | |
| "raw_text": raw_text, | |
| } | |
| return parsed, meta | |
| except Exception as e: | |
| return None, f"API ํธ์ถ ์คํจ: {type(e).__name__}: {str(e)[:200]}" | |
| def render_gen5_card(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec, | |
| prob, decision_text, reasoning_steps, judgment_text, | |
| meta_html, source_label): | |
| """5์ธ๋ ์นด๋ HTML ๋ ๋๋ง (API/์๋ฎฌ๋ ์ด์ ๊ณต์ฉ)""" | |
| dec, bg, fg = decide(prob) | |
| context_html = ( | |
| f"[SYS] ๋น์ ์ ํ๊ตญ ์ํ์ FDS ๋ถ์ ์ ๋ฌธ๊ฐ์ ๋๋ค. ๊ฑฐ๋ ์ ๋ณด์ 1-4์ธ๋ ์ฌ์ ํ๋จ์ ํ ๋๋ก ์ข ํฉ ํ์ ํ์ธ์.<br>" | |
| f"[INPUT] amount={amount:.0f}๋ง, hour={int(hour):02d}, " | |
| f"new_payee={'true' if new_payee_bin==1 else 'false'}, ratio={ratio:.1f}ร<br>" | |
| f"[PRIOR] 1-4์ธ๋ ํ๊ท : {prior_avg*100:.1f}% / ์ข ํฉ: {prior_dec}<br>" | |
| f"[TASK] JSON ํ์์ผ๋ก risk_score, decision, reasoning_steps, judgment ์ถ๋ ฅ" | |
| ) | |
| cot_rows = "" | |
| for i, step in enumerate(reasoning_steps, 1): | |
| step_text = step.get("step", "") if isinstance(step, dict) else str(step) | |
| attn = step.get("attention", 0.0) if isinstance(step, dict) else 0.0 | |
| try: | |
| attn = float(attn) | |
| except (ValueError, TypeError): | |
| attn = 0.0 | |
| cot_rows += ( | |
| f"<tr><td style='padding:6px 4px;'>{i}</td>" | |
| f"<td style='padding:6px 4px; color:#666;'>{step_text}</td>" | |
| f"<td style='text-align:right; padding:6px 4px; font-family:monospace; color:#666;'>{attn:.2f}</td></tr>" | |
| ) | |
| judg_color = "#3B6D11" if prob < 0.5 else "#633806" | |
| judgment_html = ( | |
| f"<b style='color:{judg_color};'>{decision_text} ๊ถ๊ณ (์์ฌ๋ {prob*100:.0f}%)</b><br><br>" | |
| f"{judgment_text}" | |
| ) | |
| return f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 5 ยท FOUNDATION MODEL (LLM)", f"์ด๊ฑฐ๋ ์ถ๋ก ๋ชจ๋ธ ({source_label})", dec, bg, fg, f"์์ฌ๋ {prob*100:.0f}%")} | |
| <p style="font-size:13px; color:#666; margin:4px 0 6px;">์ปจํ ์คํธ ํ ํฐํ</p> | |
| <div style="background:#f5f5f0; padding:10px 12px; border-radius:6px; font-family:monospace; font-size:11px; line-height:1.7; margin-bottom:12px;">{context_html}</div> | |
| <p style="font-size:13px; color:#666; margin:4px 0 6px;">์ถ๋ก ์ฒด์ธ (Chain-of-Thought)</p> | |
| <table style="width:100%; font-size:13px; border-collapse:collapse; margin-bottom:12px;"> | |
| <thead> | |
| <tr style="border-bottom:0.5px solid rgba(0,0,0,0.15);"> | |
| <th style="text-align:left; padding:6px 4px; font-weight:500; color:#666; width:8%;">๋จ๊ณ</th> | |
| <th style="text-align:left; padding:6px 4px; font-weight:500; color:#666; width:72%;">์ถ๋ก ๋ด์ฉ</th> | |
| <th style="text-align:right; padding:6px 4px; font-weight:500; color:#666; width:20%;">Attention</th> | |
| </tr> | |
| </thead> | |
| <tbody>{cot_rows}</tbody> | |
| </table> | |
| <p style="font-size:13px; color:#666; margin:4px 0 6px;">์์ฑ๋ ์์ฐ์ด ํ๋จ</p> | |
| <div style="background:#FAEEDA; padding:12px 14px; border-radius:6px; font-size:13px; line-height:1.7; color:#412402;">{judgment_html}</div> | |
| <details style="margin-top:10px;"> | |
| <summary style="font-size:12px; color:#666; cursor:pointer;">์์ฑ ํ๋ผ๋ฏธํฐ ๋ณด๊ธฐ</summary> | |
| <div style="background:#f5f5f0; padding:8px 12px; border-radius:6px; font-family:monospace; font-size:11px; margin-top:6px; line-height:1.6;">{meta_html}</div> | |
| </details> | |
| <p style="font-size:12px; color:#888; margin:10px 0 0; font-style:italic;">๊ฐ์ : ์ฌ์ ํ์ต๋ ๋๋ฉ์ธ ์ง์์ผ๋ก '์ ์ฌ๊ธฐ์ธ์ง' ์์ฐ์ด ์ค๋ช + ๊ถ๊ณ ์กฐ์น๊น์ง ์๋ ์์ฑ</p> | |
| </div> | |
| """ | |
| def render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec): | |
| """API ํธ์ถ ์คํจ/๋ฏธ์ค์ ์ fallback์ฉ ์๋ฎฌ๋ ์ด์ """ | |
| is_high_risk = (new_payee_bin == 1 and (hour <= 6 or hour >= 22) and amount >= 500) | |
| prob = 0.95 if is_high_risk else min(prior_avg + 0.02, 0.98) | |
| payee_str = "์ ๊ท ์์ทจ์ธ" if new_payee_bin == 1 else "๊ธฐ์กด ์์ทจ์ธ" | |
| time_str = "์๋ฒฝ" if (hour <= 6 or hour >= 22) else "์ผ๋ฐ" | |
| reasoning_steps = [ | |
| {"step": f"์๊ฐ {int(hour):02d}์ + {payee_str} โ ๋ณด์ด์คํผ์ฑ ์ ํ ํจํด ๋งค์นญ", "attention": 0.42}, | |
| {"step": f"ํ์ {ratio:.1f}๋ฐฐ ๊ธ์ก โ ์ก๊ธ์ธ ํ์ ํ๋ ์ดํ๋ ์ธก์ ", "attention": 0.28}, | |
| {"step": f"4์ธ๋ GNN ์ ํธ + {amount:.0f}๋ง์ ๊ณ ์ก โ ์ํ ๊ฐ์ค", "attention": 0.21}, | |
| {"step": "์ต๊ทผ ๋ณด์ด์คํผ์ฑ ํธ๋ ๋ ์ฌ์ ์ง์ ํ์ฉ", "attention": 0.09}, | |
| ] | |
| if is_high_risk: | |
| decision_text = "์ฐจ๋จ" | |
| judgment_text = ( | |
| f"{int(hour):02d}์ {time_str} ์๊ฐ๋์ ํ์๋ณด๋ค {ratio:.1f}๋ฐฐ ๊ธ์ฆํ {amount:.0f}๋ง์์ด " | |
| f"<u>{payee_str}</u>์๊ฒ ์ด์ฒด๋๋ ๊ฒ์ ์ ํ์ ์ธ ๋ณด์ด์คํผ์ฑ ํจํด์ ๋๋ค. " | |
| f"์์ทจ์ธ ๊ณ์ข๊ฐ ์ฌ๊ธฐ ์์ฌ ๊ณ์ข์ 2-hop ๊ฑฐ๋ฆฌ์ ์๋ค๋ GNN ์ ํธ๊น์ง ๋ํด์ ธ ์ํ๋๊ฐ ๋งค์ฐ ๋์ต๋๋ค.<br><br>" | |
| f"<b>๊ถ๊ณ ์กฐ์น:</b> โ ์ฆ์ ๊ฑฐ๋ ๋ณด๋ฅ, โก ๋ฑ๋ก๋ ์ ํ๋ฒํธ๋ก ๋ณธ์ธ ์ง์ ํ์ธ, โข ํ์ธ ์ ์๊ธ ๋๊ฒฐ 24์๊ฐ ์ ์ง" | |
| ) | |
| elif prob >= 0.5: | |
| decision_text = "์ถ๊ฐ ์ธ์ฆ" | |
| judgment_text = ( | |
| f"{int(hour):02d}์ ๊ฑฐ๋์์ ์ผ๋ถ ์ด์ ์ ํธ({ratio:.1f}๋ฐฐ ๊ธ์ก, {payee_str})๊ฐ ๊ฐ์ง๋์์ผ๋ " | |
| f"๊ฒฐ์ ์ ์ํ ํจํด์ ์๋๋๋ค. ์ฐจ๋จ๋ณด๋ค๋ ์ถ๊ฐ ์ธ์ฆ์ผ๋ก ๋ณธ์ธ ์์ฌ๋ฅผ ํ์ธํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค.<br><br>" | |
| f"<b>๊ถ๊ณ ์กฐ์น:</b> โ ARS ๋๋ OTP ์ถ๊ฐ ์ธ์ฆ, โก ์ก๊ธ ์๋ ์ฌํ์ธ ๋ฉ์์ง ๋ฐ์ก" | |
| ) | |
| else: | |
| decision_text = "ํต๊ณผ" | |
| judgment_text = ( | |
| f"{int(hour):02d}์ ๊ฑฐ๋์ ํจํด์ด ์ก๊ธ์ธ์ ํ์ ํ๋ ๋ฒ์ ๋ด์ ์์ผ๋ฉฐ, " | |
| f"1-4์ธ๋ ๋ชจ๋ธ ๋ชจ๋ ์ํ ์ ํธ๋ฅผ ๊ฐํ๊ฒ ๋ณด๋ด์ง ์์์ต๋๋ค. ์ ์ ๊ฑฐ๋๋ก ํ๋จ๋ฉ๋๋ค.<br><br>" | |
| f"<b>๊ถ๊ณ ์กฐ์น:</b> ๋ณ๋ ์กฐ์น ์์ด ๊ฑฐ๋ ์งํ" | |
| ) | |
| meta_html = ( | |
| "mode: <b>SIMULATION</b> (API ๋ฏธ์ฐ๊ฒฐ)<br>" | |
| "์ด ๊ฒฐ๊ณผ๋ if-else ํ๋์ฝ๋ฉ์ผ๋ก ์์ฑ๋ ์๋ฎฌ๋ ์ด์ ์ ๋๋ค.<br>" | |
| "์ค์ Claude ํธ์ถ์ ํ์ฑํํ๋ ค๋ฉด ANTHROPIC_API_KEY ํ๊ฒฝ๋ณ์๋ฅผ ์ค์ ํ์ธ์." | |
| ) | |
| return render_gen5_card( | |
| amount, hour, new_payee_bin, ratio, prior_avg, prior_dec, | |
| prob, decision_text, reasoning_steps, judgment_text, | |
| meta_html, source_label="์๋ฎฌ๋ ์ด์ ๋ชจ๋" | |
| ) | |
| def render_gen5(amount, hour, new_payee_bin, ratio, prior_avg): | |
| """5์ธ๋ ์ง์ ์ : API ๊ฐ๋ฅ ์ Claude ํธ์ถ, ์คํจ ์ ์๋ฎฌ๋ ์ด์ fallback""" | |
| prior_dec, _, _ = decide(prior_avg) | |
| if not CLAUDE_AVAILABLE: | |
| return render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec) | |
| parsed, meta_or_err = call_claude_api(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec) | |
| if parsed is None: | |
| fallback_html = render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec) | |
| warning = ( | |
| f'<div style="background:#FCEBEB; border-left:3px solid #A32D2D; padding:10px 14px; ' | |
| f'border-radius:6px; margin-bottom:10px; font-size:13px; color:#4A1B0C;">' | |
| f'โ ๏ธ Claude API ํธ์ถ ์คํจ. ์๋ฎฌ๋ ์ด์ ๋ชจ๋๋ก ๋์ฒดํฉ๋๋ค.<br>' | |
| f'<span style="font-family:monospace; font-size:11px; color:#712B13;">์ฌ์ : {meta_or_err}</span>' | |
| f'</div>' | |
| ) | |
| return warning + fallback_html | |
| try: | |
| prob = float(parsed.get("risk_score", 0.5)) | |
| prob = max(0.0, min(1.0, prob)) | |
| decision_text = parsed.get("decision", "์ถ๊ฐ ์ธ์ฆ") | |
| reasoning_steps = parsed.get("reasoning_steps", []) | |
| judgment_text = parsed.get("judgment", "(ํ๋จ ๋ด์ฉ ๋๋ฝ)") | |
| meta = meta_or_err | |
| meta_html = ( | |
| f"model: <b>{meta['model']}</b> / temperature: 0.2 / max_tokens: 1024<br>" | |
| f"input_tokens: {meta['input_tokens']} / output_tokens: {meta['output_tokens']} / " | |
| f"latency: {meta['latency']:.2f}s<br>" | |
| f"mode: <b style='color:#3B6D11;'>LIVE API CALL โ</b>" | |
| ) | |
| return render_gen5_card( | |
| amount, hour, new_payee_bin, ratio, prior_avg, prior_dec, | |
| prob, decision_text, reasoning_steps, judgment_text, | |
| meta_html, source_label=f"์ค์ {CLAUDE_MODEL}" | |
| ) | |
| except (KeyError, ValueError, TypeError) as e: | |
| fallback_html = render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec) | |
| warning = ( | |
| f'<div style="background:#FCEBEB; border-left:3px solid #A32D2D; padding:10px 14px; ' | |
| f'border-radius:6px; margin-bottom:10px; font-size:13px; color:#4A1B0C;">' | |
| f'โ ๏ธ Claude ์๋ต ํ์ฑ ์คํจ. ์๋ฎฌ๋ ์ด์ ๋ชจ๋๋ก ๋์ฒดํฉ๋๋ค.<br>' | |
| f'<span style="font-family:monospace; font-size:11px; color:#712B13;">' | |
| f'{type(e).__name__}: {str(e)[:150]}</span></div>' | |
| ) | |
| return warning + fallback_html | |
| # ============================================================ | |
| # 4. ๋ฉ์ธ ๋ถ์ ํจ์ | |
| # ============================================================ | |
| def analyze_transaction(amount, hour, new_payee, ratio): | |
| start_time = time.time() | |
| new_payee_bin = 1 if new_payee == "์" else 0 | |
| # 1์ธ๋ | |
| g1 = render_gen1(amount, hour, new_payee_bin, ratio) | |
| _, score1 = evaluate_gen1(amount, hour, new_payee_bin, ratio) | |
| prob1 = min(score1 / 100, 0.99) | |
| # 2์ธ๋ | |
| g2 = render_gen2(amount, hour, new_payee_bin, ratio) | |
| input_vec = np.array([amount, hour, new_payee_bin, ratio], dtype=float) | |
| logit2 = (GEN2_COEF * input_vec).sum() + GEN2_INTERCEPT | |
| prob2 = float(1 / (1 + np.exp(-logit2))) | |
| # 3์ธ๋ | |
| g3 = render_gen3(amount, hour, new_payee_bin, ratio) | |
| input_df = pd.DataFrame([[amount, hour, new_payee_bin, ratio]], columns=FEATURES) | |
| prob3 = float(gen3_model.predict_proba(input_df)[0][1]) | |
| # 4์ธ๋ | |
| prob4, g4 = render_gen4(amount, hour, new_payee_bin, ratio, prob3) | |
| # 5์ธ๋ (1-4์ธ๋ ํ๊ท ์ prior๋ก) | |
| prior_avg = (prob1 + prob2 + prob3 + prob4) / 4 | |
| g5 = render_gen5(amount, hour, new_payee_bin, ratio, prior_avg) | |
| elapsed = time.time() - start_time | |
| summary = f""" | |
| <div style="background:#f5f5f0; border-radius:12px; padding:16px 20px; margin-bottom:14px;"> | |
| <p style="font-size:13px; color:#666; margin:0 0 8px;">๋ถ์ ๋์ ๊ฑฐ๋</p> | |
| <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(120px, 1fr)); gap:12px;"> | |
| <div><p style="font-size:12px; color:#888; margin:0;">๊ธ์ก</p><p style="font-size:18px; font-weight:500; margin:2px 0 0;">{amount:.0f}๋ง์</p></div> | |
| <div><p style="font-size:12px; color:#888; margin:0;">๊ฑฐ๋ ์๊ฐ</p><p style="font-size:18px; font-weight:500; margin:2px 0 0;">{int(hour):02d}์</p></div> | |
| <div><p style="font-size:12px; color:#888; margin:0;">์ ๊ท ์์ทจ์ธ</p><p style="font-size:18px; font-weight:500; margin:2px 0 0;">{new_payee}</p></div> | |
| <div><p style="font-size:12px; color:#888; margin:0;">๊ณผ๊ฑฐ ๋๋น ๋ฐฐ์จ</p><p style="font-size:18px; font-weight:500; margin:2px 0 0;">{ratio:.1f}ร</p></div> | |
| </div> | |
| <p style="font-size:12px; color:#888; margin:10px 0 0;">๋ถ์ ์์์๊ฐ: {elapsed:.3f}์ด</p> | |
| </div> | |
| """ | |
| return summary + g1 + g2 + g3 + g4 + g5 | |
| # ============================================================ | |
| # 5. Gradio UI | |
| # ============================================================ | |
| with gr.Blocks(theme=gr.themes.Default(), title="FDS 1-5์ธ๋ ๋น๊ต ๋ฐ๋ชจ") as demo: | |
| _api_badge = ( | |
| f'<span style="background:#EAF3DE; color:#3B6D11; padding:3px 10px; border-radius:8px; font-size:12px; font-weight:500;">' | |
| f'โ Claude API ์ฐ๊ฒฐ๋จ ({CLAUDE_MODEL})</span>' | |
| if CLAUDE_AVAILABLE else | |
| '<span style="background:#FAEEDA; color:#854F0B; padding:3px 10px; border-radius:8px; font-size:12px; font-weight:500;">' | |
| 'โ API ๋ฏธ์ฐ๊ฒฐ (5์ธ๋ ์๋ฎฌ๋ ์ด์ ๋ชจ๋)</span>' | |
| ) | |
| gr.HTML(f""" | |
| <div style="text-align:center; padding:10px 0;"> | |
| <h1 style="margin:0;">๐ก๏ธ FDS 1-5์ธ๋ ๋น๊ต ๋ฐ๋ชจ</h1> | |
| <p style="color:#666; margin:6px 0 0;">์ค๋ฌด ์ด์ ๋ด๋น์ ์์ฐ์ฉ ยท ํ๋จ ์์ยท๊ฐ์ค์นยท๊ทผ๊ฑฐ ์ ์ฒด ๋ ธ์ถ ๋ชจ๋</p> | |
| <div style="margin-top:8px;">{_api_badge}</div> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| amount_in = gr.Number(label="๊ธ์ก (๋ง์)", value=700) | |
| hour_in = gr.Slider(label="๊ฑฐ๋ ์๊ฐ (0-23์)", minimum=0, maximum=23, value=3, step=1) | |
| payee_in = gr.Radio(label="์ ๊ท ์์ทจ์ธ", choices=["์๋์ค", "์"], value="์") | |
| ratio_in = gr.Number(label="๊ณผ๊ฑฐ ๋๋น ๋ฐฐ์จ", value=14.0) | |
| submit_btn = gr.Button("๐ ๋ถ์ ์คํ", variant="primary") | |
| gr.Examples( | |
| examples=[ | |
| [700, 3, "์", 14.0], | |
| [800, 23, "์", 8.0], | |
| [45, 2, "์๋์ค", 1.2], | |
| [1500, 14, "์๋์ค", 2.0], | |
| [499, 23, "์", 4.5], | |
| ], | |
| inputs=[amount_in, hour_in, payee_in, ratio_in], | |
| label="์์ฐ ์์ (๋ง์ง๋ง์ 1์ธ๋ ๋ฃฐ์ ํํผํ๋ ์ผ์ด์ค)" | |
| ) | |
| with gr.Column(scale=2): | |
| output_html = gr.HTML( | |
| "<div style='padding:20px; color:#666;'>" | |
| "์ข์ธก์์ ๊ฑฐ๋ ์กฐ๊ฑด์ ์ค์ ํ๊ณ [๋ถ์ ์คํ] ๋ฒํผ์ ๋๋ฅด์ธ์." | |
| "</div>" | |
| ) | |
| submit_btn.click( | |
| fn=analyze_transaction, | |
| inputs=[amount_in, hour_in, payee_in, ratio_in], | |
| outputs=output_html | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(ssr_mode=False) |