Spaces:
Sleeping
Sleeping
| """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"<b>{chatgpt_label}</b><br>" | |
| f"Unified: {unified:.4f}<br>" | |
| f"GapScore: {gap:.4f}<br>" | |
| 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"์ด ํ ํฝ๋ค์ ๋ ๋ชจ๋ธ ๋ชจ๋์์ ์ค์ํ๋ฉด์ ์ฝํ ์ธ ๊ธฐํ๋ ํฐ ์ต์ฐ์ ์์ญ์ ๋๋ค." | |
| ) | |