"""Unified Scoring (ADR-014 Phase 4). Cross-model unified score = weighted average of demand/supply percentiles. Displayed in "전체" mode when cross-model pair exists. """ import streamlit as st import pandas as pd import plotly.graph_objects as go from core.supabase_client import ( get_gap_scores, get_topic_clusters, ) def render_unified_scoring(base_ctx: dict, pair: dict): """Render unified scoring view combining demand + supply signals. Args: base_ctx: Dashboard base context. pair: Cross-model pair dict from find_cross_model_pair(). """ campaign_chatgpt = pair["campaign_chatgpt"] campaign_gemini = pair["campaign_gemini"] matches = get_gap_scores(campaign_chatgpt, campaign_gemini) if not matches: st.info("Unified Scoring에 필요한 Cross-Model 데이터가 없습니다.") return # Fetch cluster counts for weight calculation chatgpt_clusters = get_topic_clusters(campaign_chatgpt, source="chatgpt") gemini_clusters = get_topic_clusters(campaign_gemini, source="gemini") chatgpt_volume = sum(c.get("fanout_count", 0) for c in chatgpt_clusters) gemini_volume = sum(c.get("fanout_count", 0) for c in gemini_clusters) total_volume = chatgpt_volume + gemini_volume # Weight by data volume (ADR-014 spec) w_chatgpt = chatgpt_volume / total_volume if total_volume > 0 else 0.5 w_gemini = gemini_volume / total_volume if total_volume > 0 else 0.5 st.caption( "Demand(ChatGPT)와 Supply(Gemini) 신호를 데이터 볼륨 비례로 통합한 점수입니다." ) # Weight info c1, c2, c3 = st.columns(3) with c1: st.metric( "Demand 가중치", f"{w_chatgpt:.1%}", help=f"ChatGPT fanout 볼륨: {chatgpt_volume:,}", ) with c2: st.metric( "Supply 가중치", f"{w_gemini:.1%}", help=f"Gemini citation 볼륨: {gemini_volume:,}", ) with c3: st.metric("매칭 토픽", f"{len(matches)}개") st.markdown("---") # Compute unified scores scored = [] for m in matches: demand_pct = float(m.get("demand_percentile") or 0) supply_pct = float(m.get("supply_percentile") or 0) unified = w_chatgpt * demand_pct + w_gemini * supply_pct scored.append({ **m, "unified_score": unified, }) # Sort by unified score DESC scored.sort(key=lambda x: x["unified_score"], reverse=True) # --- Unified Ranking Table --- st.markdown("#### Unified Score 랭킹") st.caption( "두 모델의 신호를 통합한 순위입니다. " "Unified Score가 높을수록 Demand와 Supply 모두에서 중요한 토픽입니다." ) rows = [] for i, s in enumerate(scored, 1): demand_pct = float(s.get("demand_percentile") or 0) supply_pct = float(s.get("supply_percentile") or 0) rows.append({ "#": i, "토픽 (ChatGPT)": (s.get("chatgpt_label") or "")[:30], "토픽 (Gemini)": (s.get("gemini_label") or "")[:30], "Unified": f"{s['unified_score']:.4f}", "Demand": f"{demand_pct:.2%}", "Supply": f"{supply_pct:.2%}", "GapScore": f"{float(s.get('gap_score') or 0):.4f}", "Quadrant": s.get("quadrant", "NICHE").replace("_", " ").title(), }) df = pd.DataFrame(rows) st.dataframe(df, use_container_width=True, hide_index=True) st.markdown("---") # --- Unified Score Distribution --- st.markdown("#### Unified Score vs GapScore") st.caption( "X축은 통합 중요도(높을수록 두 모델 모두 중요), " "Y축은 기회 크기(높을수록 콘텐츠 제작 ROI가 높음)." ) _render_unified_scatter(scored) def _render_unified_scatter(scored: list[dict]): """Scatter: Unified Score (x) vs GapScore (y).""" from .cross_model import QUADRANT_COLORS, QUADRANT_LABELS xs, ys, colors, hovers, sizes = [], [], [], [], [] for s in scored: unified = s["unified_score"] gap = float(s.get("gap_score") or 0) quadrant = s.get("quadrant", "NICHE") chatgpt_label = s.get("chatgpt_label", "") xs.append(unified) ys.append(gap) colors.append(QUADRANT_COLORS.get(quadrant, "#9CA3AF")) sizes.append(max(8, min(25, unified * 30))) hovers.append( f"{chatgpt_label}
" f"Unified: {unified:.4f}
" f"GapScore: {gap:.4f}
" f"Quadrant: {QUADRANT_LABELS.get(quadrant, quadrant)}" ) fig = go.Figure() fig.add_trace(go.Scatter( x=xs, y=ys, mode="markers", marker=dict( size=sizes, color=colors, opacity=0.7, line=dict(width=0.5, color="#333"), ), text=hovers, hoverinfo="text", showlegend=False, )) fig.update_layout( title="Unified Score vs GapScore", xaxis_title="Unified Score (통합 중요도)", yaxis_title="GapScore (콘텐츠 기회)", height=450, template="plotly_white", hoverlabel=dict(bgcolor="white", font_size=12), ) st.plotly_chart(fig, use_container_width=True, key="cross_model:unified_scatter", config={"displayModeBar": False}) # Insight: Top-right quadrant = high importance + high opportunity high_unified = [s for s in scored if s["unified_score"] > 0.5] high_gap_and_unified = [ s for s in high_unified if float(s.get("gap_score") or 0) > 0.05 ] if high_gap_and_unified: st.info( f"Unified Score > 0.5 이면서 GapScore가 높은 토픽이 " f"**{len(high_gap_and_unified)}개** 있습니다. " f"이 토픽들은 두 모델 모두에서 중요하면서 콘텐츠 기회도 큰 최우선 영역입니다." )