# app.py — TGN Fraud Detection | Temporal Graph Network E-commerce import streamlit as st import numpy as np import torch import os from datetime import datetime, timedelta import time import random st.set_page_config( page_title="TGN Fraud — E-commerce", page_icon="⏱️", layout="wide", initial_sidebar_state="expanded" ) st.markdown(""" """, unsafe_allow_html=True) # ── SESSION STATE ───────────────────────────────────────────── defaults = { 'trainer': None, 'treinado': False, 'data': None, 'df': None, 'usuarios': None, 'comerciantes': None, 'n_usuarios': 0, 'stream_eventos': [], 'stream_running': False, 'neo4j': None, 'neo4j_ok': False, } for k, v in defaults.items(): if k not in st.session_state: st.session_state[k] = v # ── NEO4J ───────────────────────────────────────────────────── def get_neo4j_config(): cfg = {} try: s = st.secrets if 'NEO4J_URI' in s: cfg = {'uri': s['NEO4J_URI'], 'username': s['NEO4J_USERNAME'], 'password': s['NEO4J_PASSWORD'], 'database': s.get('NEO4J_DATABASE', 'neo4j')} elif 'neo4j' in s: n = s['neo4j'] cfg = {'uri': n.get('uri',''), 'username': n.get('username',''), 'password': n.get('password',''), 'database': n.get('database','neo4j')} except Exception: pass if not cfg.get('uri'): cfg = {'uri': os.getenv('NEO4J_URI',''), 'username': os.getenv('NEO4J_USERNAME',''), 'password': os.getenv('NEO4J_PASSWORD',''), 'database': os.getenv('NEO4J_DATABASE','neo4j')} return cfg @st.cache_resource def conectar_neo4j(): try: from neo4j import GraphDatabase cfg = get_neo4j_config() if not all([cfg['uri'], cfg['username'], cfg['password']]): return None driver = GraphDatabase.driver(cfg['uri'], auth=(cfg['username'], cfg['password'])) with driver.session(database=cfg['database']) as s: s.run('RETURN 1') return driver, cfg['database'] except Exception: return None # ── LIBS ────────────────────────────────────────────────────── @st.cache_resource def carregar_libs(): try: from tgn_data import gerar_eventos_ecommerce, df_para_tensores from tgn_model import TrainerTGN return gerar_eventos_ecommerce, df_para_tensores, TrainerTGN except Exception as e: return str(e), None, None # ── CHARTS ──────────────────────────────────────────────────── def loss_svg(hist): lt = hist['loss_train']; lv = hist['loss_val']; av = hist['auc_val'] ep = len(lt) if ep == 0: return '' def pts(vals, H=120): mn,mx = min(vals),max(vals); r = mx-mn or 1 return ' '.join(f'{i*480/max(ep-1,1):.1f},{H-(v-mn)/r*H:.1f}' for i,v in enumerate(vals)) return f"""
TREINO — — Loss Train — Loss Val — AUC Val
""" def roc_svg(y_true, probs): from sklearn.metrics import roc_curve, auc fpr,tpr,_ = roc_curve(y_true, probs) ra = auc(fpr,tpr) pts = ' '.join(f'{f*460:.1f},{180-t*180:.1f}' for f,t in zip(fpr,tpr)) return f"""
ROC — AUC {ra:.3f}
FPR
""" def cm_html(cm): tn,fp,fn,tp = cm.ravel() items = [('#22c55e',tn,'TN','Legítimas\ncorretas'), ('#ef4444',fp,'FP','Falsos\nalarmes'), ('#f59e0b',fn,'FN','Fraudes\nperdidas'), ('#6366f1',tp,'TP','Fraudes\ncapturadas')] html = '
' for c,v,a,d in items: html += f'
{v}
{a}
{d}
' return html + '
' def memoria_html(mem_snap, n_show=20): """Visualiza vetores de memória dos usuários como heatmap de barras.""" usuarios_mem = mem_snap['usuarios'][:n_show] last_upd = mem_snap['last_update'][:n_show] html = '
MEMÓRIA DOS NÓS — cada linha é um usuário, cada barra é uma dimensão da memória
' for i, (mem, lu) in enumerate(zip(usuarios_mem, last_upd)): # Normaliza para [0,1] mn, mx = mem.min(), mem.max() norm = (mem - mn) / (mx - mn + 1e-8) # Mostra primeiras 20 dimensões como barras coloridas bars = '' for v in norm[:20]: pct = float(v) * 100 color = f'hsl({int(220 + v*120)},70%,55%)' bars += f'
' ativo = lu > 0 dot = '🟢' if ativo else '⚫' html += f'
{dot} U{i:03d}
{bars}
' html += '
' return html def timeline_html(eventos): """Linha do tempo dos últimos eventos do stream.""" if not eventos: return '

Nenhum evento ainda. Inicie o stream.

' html = '' for ev in reversed(eventos[-15:]): cls = 'event-fraud' if ev['fraude'] else 'event-safe' badge = f'FRAUDE {ev["prob"]:.0%}' if ev['fraude'] else f'{ev["prob"]:.0%}' html += f'''
{ev["hora"]} 👤 U{ev["uid"]:03d} 🏪 M{ev["mid"]:02d} R$ {ev["valor"]:.0f} {ev["categoria"]} {badge}
''' return html # ── SIDEBAR ─────────────────────────────────────────────────── def sidebar(): st.sidebar.markdown('## ⏱️ TGN Config') n_usuarios = st.sidebar.slider('Usuários', 100, 500, 300, 50) n_comerciantes = st.sidebar.slider('Comerciantes', 20, 150, 80, 10) n_eventos = st.sidebar.slider('Eventos treino', 1000, 5000, 3000, 500) taxa_fraude = st.sidebar.slider('Taxa fraude %', 3, 20, 8) st.sidebar.markdown('---') st.sidebar.markdown('### Hiperparâmetros') memory_dim = st.sidebar.select_slider('Memory dim', [32, 64, 128], 64) time_dim = st.sidebar.select_slider('Time dim', [16, 32, 64], 32) lr = st.sidebar.select_slider('LR', [0.0001, 0.0005, 0.001], 0.0005) epocas = st.sidebar.slider('Épocas', 10, 60, 25, 5) st.sidebar.markdown('---') if st.session_state.neo4j_ok: st.sidebar.success('🗄️ Neo4j Conectado') else: st.sidebar.warning('⚠️ Neo4j Offline') if st.session_state.treinado and st.session_state.trainer: st.sidebar.metric('Melhor AUC', f"{st.session_state.trainer.melhor_auc:.3f}") return dict(n_usuarios=n_usuarios, n_comerciantes=n_comerciantes, n_eventos=n_eventos, taxa_fraude=taxa_fraude/100, memory_dim=memory_dim, time_dim=time_dim, lr=lr, epocas=epocas) # ── MAIN ────────────────────────────────────────────────────── def main(): if st.session_state.neo4j is None: conn = conectar_neo4j() st.session_state.neo4j = conn st.session_state.neo4j_ok = conn is not None cfg = sidebar() st.markdown("""

TGN Fraud Detection

Temporal Graph Network · E-commerce · Memória Evolutiva dos Nós

""", unsafe_allow_html=True) tabs = st.tabs(['📦 Dados', '🧠 Treinar', '📊 Performance', '🧠 Memória dos Nós', '⚡ Stream ao Vivo', '🗄️ Neo4j']) # ── TAB 0: DADOS ────────────────────────────────────────── with tabs[0]: res = carregar_libs() if isinstance(res[0], str): st.error(f'Erro: {res[0]}') st.stop() gerar_eventos, df_para_tensores, TrainerTGN = res col1, col2 = st.columns([1, 2]) with col1: st.markdown('### Padrões de Fraude') for icone, nome, desc in [ ('⚡','Velocity Attack','Muitas tx em pouco tempo'), ('🃏','Card Testing','Pequena tx antes de grande'), ('🌙','Night Transaction','Compras de madrugada'), ('🛒','Categoria Anômala','Merchant incomum para o usuário'), ]: st.markdown(f'**{icone} {nome}** — {desc}') st.markdown('### Arquitetura TGN') st.markdown(""" ``` Evento (src, dst, t, feats) ↓ TimeEncoder(Δt) → cos embedding ↓ MessageFunction → msg ↓ GRU MemoryUpdater → nova memória ↓ TemporalEmbedding → node repr ↓ MLP Classifier → P(fraude) ``` """) if st.button('🔄 Gerar Dados', type='primary', use_container_width=True): with st.spinner('Gerando eventos de e-commerce...'): df, usuarios, comerciantes, n_u = gerar_eventos( n_usuarios=cfg['n_usuarios'], n_comerciantes=cfg['n_comerciantes'], n_eventos=cfg['n_eventos'], taxa_fraude=cfg['taxa_fraude'], ) data = df_para_tensores(df, n_u, cfg['n_comerciantes']) st.session_state.df = df st.session_state.data = data st.session_state.usuarios = usuarios st.session_state.comerciantes= comerciantes st.session_state.n_usuarios = n_u st.session_state.treinado = False st.session_state.trainer = None st.success('✅ Dados gerados!') with col2: if st.session_state.df is not None: df = st.session_state.df data = st.session_state.data m1,m2,m3,m4 = st.columns(4) n_fraudes = int(df['label'].sum()) for col, val, lbl in [ (m1, len(df), 'Eventos'), (m2, n_fraudes, '🚨 Fraudes'), (m3, f"{n_fraudes/len(df):.1%}", 'Taxa fraude'), (m4, data['n_nos'], 'Nós no grafo'), ]: col.markdown(f'
{val}
{lbl}
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # Distribuição de fraudes por categoria st.markdown('#### Fraudes por Categoria') fraudes_cat = df[df['label']==1]['categoria'].value_counts() legit_cat = df[df['label']==0]['categoria'].value_counts() cats = list(fraudes_cat.index) W = 440 max_v = max(fraudes_cat.max(), legit_cat.max()) bars_svg = f'' for i, cat in enumerate(cats): y = i * 36 + 10 fv = fraudes_cat.get(cat, 0) lv = legit_cat.get(cat, 0) fw = int(fv / max_v * (W-120)) lw = int(lv / max_v * (W-120)) bars_svg += ( f'{cat}' f'' f'' f'{lv}' f'{fv}' ) bars_svg += '' st.markdown(bars_svg, unsafe_allow_html=True) st.caption('🟢 Legítimas 🔴 Fraudes') else: st.info('Configure parâmetros e clique **Gerar Dados**.') # ── TAB 1: TREINAR ──────────────────────────────────────── with tabs[1]: _, _, TrainerTGN = carregar_libs() if st.session_state.data is None: st.warning('⬅️ Gere os dados primeiro.') else: col1, col2 = st.columns([1, 2]) with col1: st.markdown('### Memory dim') st.markdown(f'`{cfg["memory_dim"]}` dimensões por nó') st.markdown(f'`{st.session_state.data["n_nos"]}` nós no total') total_mem = st.session_state.data['n_nos'] * cfg['memory_dim'] * 4 st.markdown(f'Memória total: `{total_mem/1024:.1f} KB`') st.markdown('### Time Encoding') st.markdown(f'`{cfg["time_dim"]}` frequências cossenoidais') st.markdown('`Δt` → comportamento temporal') if st.button('🚀 Treinar TGN', type='primary', use_container_width=True): st.session_state.trainer = TrainerTGN( st.session_state.data, memory_dim=cfg['memory_dim'], time_dim=cfg['time_dim'], lr=cfg['lr'], ) prog = st.progress(0) status = st.empty() logs = [] log_box = col2.empty() def cb(ep, total, loss, auc, f1): prog.progress(ep/total) status.markdown(f'**Época {ep}/{total}** · Loss `{loss:.4f}` · AUC `{auc:.3f}` · F1 `{f1:.3f}`') if ep % 5 == 0 or ep == total: logs.append(f'[{ep:>3}] loss={loss:.4f} auc={auc:.3f} f1={f1:.3f}') log_box.code('\n'.join(logs[-12:])) with st.spinner('Treinando TGN...'): st.session_state.trainer.treinar(cfg['epocas'], cb) st.session_state.treinado = True st.success(f'✅ Melhor AUC: {st.session_state.trainer.melhor_auc:.3f}') with col2: if st.session_state.treinado: hist = st.session_state.trainer.historico st.components.v1.html(loss_svg(hist), height=200) # ── TAB 2: PERFORMANCE ──────────────────────────────────── with tabs[2]: if not st.session_state.treinado: st.warning('⬅️ Treine o modelo primeiro.') else: m = st.session_state.trainer.metricas_completas() cols = st.columns(5) for col, (nome, val) in zip(cols, [ ('ROC-AUC', f"{m['auc']:.3f}"), ('Avg Prec', f"{m['ap']:.3f}"), ('F1', f"{m['f1']:.3f}"), ('Precision', f"{m['precision']:.3f}"), ('Recall', f"{m['recall']:.3f}"), ]): col.markdown(f'
{val}
{nome}
', unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) c1, c2 = st.columns(2) with c1: st.markdown('### Matriz de Confusão') st.markdown(cm_html(m['cm']), unsafe_allow_html=True) with c2: st.components.v1.html(roc_svg(m['y_true'], m['probs']), height=240) # ── TAB 3: MEMÓRIA DOS NÓS ─────────────────────────────── with tabs[3]: if not st.session_state.treinado: st.warning('⬅️ Treine o modelo primeiro.') else: st.markdown('### 🧠 Estado da Memória — o que o TGN "lembra" de cada usuário') st.markdown(""" Cada linha é um usuário. Cada barra colorida é uma dimensão do vetor de memória. **Usuários com padrão de fraude** tendem a ter memória com ativações distintas. A memória evolui a cada transação via GRU — captura comportamento histórico. """) n_show = st.slider('Usuários para mostrar', 10, 50, 25) mem = st.session_state.trainer.get_memory_snapshot( st.session_state.n_usuarios) # Estatísticas da memória usuarios_mem = mem['usuarios'] normas = np.linalg.norm(usuarios_mem, axis=1) top_ativos = np.argsort(normas)[::-1] c1, c2, c3 = st.columns(3) c1.metric('Usuários com memória ativa', int((normas > 0.1).sum())) c2.metric('Norma média da memória', f'{normas.mean():.3f}') c3.metric('Usuário mais ativo', f'U{top_ativos[0]:03d}') st.markdown('
', unsafe_allow_html=True) # Ordenar por norma (mais ativos primeiro) idx_sorted = np.argsort(normas)[::-1][:n_show] mem_sorted = { 'usuarios': usuarios_mem[idx_sorted], 'last_update': mem['last_update'][idx_sorted], } st.components.v1.html(memoria_html(mem_sorted, n_show), height=n_show*26+80) # t-SNE da memória with st.expander('📊 t-SNE da memória dos usuários'): try: from sklearn.manifold import TSNE tsne = TSNE(n_components=2, random_state=42, perplexity=min(30, len(usuarios_mem)//2)) coords = tsne.fit_transform(usuarios_mem) df_ = st.session_state.df # Usuários com mais fraudes fraud_per_user = df_.groupby('src')['label'].mean() colors = [fraud_per_user.get(i, 0) for i in range(len(usuarios_mem))] # SVG scatter cx = coords[:,0]; cy = coords[:,1] mn_x,mx_x = cx.min(),cx.max(); mn_y,mx_y = cy.min(),cy.max() def sc(v,mn,mx,W): return (v-mn)/(mx-mn+1e-8)*W circles = '' for i,(x,y,c) in enumerate(zip(cx,cy,colors)): px = sc(x,mn_x,mx_x,480); py = sc(y,mn_y,mx_y,280) hue = int(120*(1-c)) circles += f'' svg = f'{circles}🟢 Baixo risco 🔴 Alto risco histórico' st.components.v1.html(svg, height=300) except Exception as e: st.info(f't-SNE indisponível: {e}') # ── TAB 4: STREAM AO VIVO ───────────────────────────────── with tabs[4]: if not st.session_state.treinado: st.warning('⬅️ Treine o modelo primeiro.') else: st.markdown('### ⚡ Simulação de Stream em Tempo Real') st.markdown('Gera transações aleatórias e classifica com o TGN treinado, atualizando a memória dos nós a cada evento.') col1, col2 = st.columns([1, 2]) with col1: n_gerar = st.number_input('Eventos a simular', 5, 100, 20) if st.button('▶️ Simular Stream', type='primary', use_container_width=True): data = st.session_state.data usuarios = st.session_state.usuarios comerciantes = st.session_state.comerciantes n_u = st.session_state.n_usuarios trainer = st.session_state.trainer categorias = ['eletronicos','viagem','alimentacao','roupas','jogos','farmacia'] novos = [] prog = st.progress(0) for i in range(int(n_gerar)): uid = random.randint(0, n_u-1) mid = random.randint(0, len(comerciantes)-1) u = usuarios[uid] m = comerciantes[mid] cat = m['categoria'] valor = max(1.0, np.random.lognormal(5, 1)) hora = random.randint(0, 23) ts = float(i) / n_gerar feats = [ np.log1p(valor)/12.0, hora/24.0, 1.0 if hora < 6 else 0.0, 1.0 if cat=='eletronicos' else 0.0, 1.0 if cat=='viagem' else 0.0, 0.0, u['score_credito'], min(u['tempo_cliente_dias']/1000, 1.0), m['risco_setor'], ] prob, mem_src = trainer.inferir_evento(uid, mid+n_u, ts, feats) novos.append({ 'uid': uid, 'mid': mid, 'valor': valor, 'categoria': cat, 'hora': f'{hora:02d}h', 'prob': prob, 'fraude': prob > 0.5, 'mem_norm': float(np.linalg.norm(mem_src)), }) prog.progress((i+1)/n_gerar) time.sleep(0.05) st.session_state.stream_eventos = ( st.session_state.stream_eventos + novos)[-50:] st.success(f'✅ {int(n_gerar)} eventos processados · ' f'{sum(1 for e in novos if e["fraude"])} fraudes detectadas') with col2: eventos = st.session_state.stream_eventos if eventos: n_fraud = sum(1 for e in eventos if e['fraude']) m1,m2,m3 = st.columns(3) m1.metric('Eventos', len(eventos)) m2.metric('Fraudes', n_fraud) m3.metric('Taxa', f'{n_fraud/len(eventos):.1%}') st.markdown('
', unsafe_allow_html=True) st.components.v1.html( f'
' f'{timeline_html(eventos)}
', height=420, scrolling=True) else: st.info('Clique em **Simular Stream** para ver eventos em tempo real.') # ── TAB 5: NEO4J ───────────────────────────────────────── with tabs[5]: st.header('🗄️ Neo4j') if not st.session_state.neo4j_ok: st.warning('Neo4j offline.') with st.expander('Como configurar'): st.markdown(""" **HF Spaces → Settings → Variables and secrets:** | Chave | Valor | |---|---| | `NEO4J_URI` | `neo4j+s://XXXXXXXX.databases.neo4j.io` | | `NEO4J_USERNAME` | `neo4j` | | `NEO4J_PASSWORD` | `sua_senha` | | `NEO4J_DATABASE` | `neo4j` | """) else: st.success('Conectado!') if st.session_state.treinado and st.button('💾 Salvar métricas no Neo4j'): driver, db = st.session_state.neo4j m = st.session_state.trainer.metricas_completas() try: with driver.session(database=db) as s: s.run(""" MERGE (r:TGNRun {ts: $ts}) SET r.auc=$auc, r.f1=$f1, r.ap=$ap, r.n_eventos=$n, r.memory_dim=$mem """, ts=datetime.now().isoformat(), auc=float(m['auc']), f1=float(m['f1']), ap=float(m['ap']), n=len(st.session_state.df), mem=cfg['memory_dim']) st.success('✅ Salvo!') except Exception as e: st.error(str(e)) if __name__ == '__main__': main()