'
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'''
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''
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''
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()