"""UMAP 토픽 맵 시각화 (Plotly scatter). snapshot.coordinates: [{cluster_id, x, y, size, label}] clusters: [{id, cluster_label, attention_score, citation_density, opportunity_score, fanout_count, ...}] """ import streamlit as st import plotly.graph_objects as go def render_topic_map(clusters: list[dict], snapshot: dict | None, frame: str = "all"): """Render UMAP 2D scatter from snapshot coordinates + cluster metadata.""" if not snapshot or not snapshot.get("coordinates"): st.info("UMAP 좌표 데이터가 없습니다. 클러스터링 실행 후 생성됩니다.") return # Frame-specific guide if frame == "demand": st.markdown(""" 각 점은 하나의 **Demand 토픽** (ChatGPT sub-query 그룹)입니다. - **점 크기**: 해당 토픽의 fanout 수 (클수록 소비자가 자주 묻는 토픽) - **점 색상**: 기회 점수 (빨강 = 기회 큼, 노랑 = 보통) - **가까이 있는 점**: 유사한 검색 의도의 토픽 """) elif frame == "supply": st.markdown(""" 각 점은 하나의 **Supply 토픽** (Gemini citation quote 그룹)입니다. - **점 크기**: 해당 토픽의 인용 수 (클수록 AI가 자주 인용하는 토픽) - **점 색상**: 기회 점수 (빨강 = 기회 큼, 노랑 = 보통) - **가까이 있는 점**: 유사한 인용 주제의 토픽 """) else: st.markdown(""" 각 점은 하나의 **토픽**(AI 추가 질문 그룹)입니다. - **점 크기**: 해당 토픽의 AI 추가 질문 수 (클수록 AI가 자주 묻는 토픽) - **점 색상**: 기회 점수 (빨강 = 기회 큼, 노랑 = 보통) - **가까이 있는 점**: 유사한 주제의 토픽 """) count_label = "Citations" if frame == "supply" else "Fanouts" coords = snapshot["coordinates"] # Build cluster lookup by id cluster_map = {c["id"]: c for c in clusters} # Merge coordinate data with cluster metadata xs, ys, sizes, colors, hover_texts = [], [], [], [], [] for pt in coords: cid = pt.get("cluster_id") meta = cluster_map.get(cid, {}) xs.append(pt["x"]) ys.append(pt["y"]) fanout_count = pt.get("size", meta.get("fanout_count", 10)) # Normalize size for display (min 5, max 40) norm_size = max(5, min(40, fanout_count / 5)) sizes.append(norm_size) opp = float(meta.get("opportunity_score", 0) or 0) colors.append(opp) label = meta.get("cluster_label") or f"Cluster {pt.get('label', '?')}" attn = float(meta.get("attention_score", 0) or 0) density = float(meta.get("citation_density", 0) or 0) hover_texts.append( f"{label}
" f"Attention: {attn:.4f}
" f"Density: {density:.4f}
" f"Opportunity: {opp:.4f}
" f"{count_label}: {fanout_count}" ) fig = go.Figure() fig.add_trace(go.Scatter( x=xs, y=ys, mode="markers", marker=dict( size=sizes, color=colors, colorscale="YlOrRd", colorbar=dict(title="Opportunity"), opacity=0.7, line=dict(width=0.5, color="#333"), ), text=hover_texts, hoverinfo="text", )) fig.update_layout( title="AI 토픽 맵 (UMAP 2D Projection)", xaxis=dict(title="UMAP-1", showgrid=False, zeroline=False), yaxis=dict(title="UMAP-2", showgrid=False, zeroline=False), height=600, template="plotly_white", hoverlabel=dict(bgcolor="white", font_size=12), ) st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False}) # Algorithm params info params = snapshot.get("algorithm_params") if params: with st.expander("분석 설정 (기술 상세)"): st.json(params)