"""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"이 토픽들은 두 모델 모두에서 중요하면서 콘텐츠 기회도 큰 최우선 영역입니다."
)