"""감성분석 Feature 요약 카드.
기존 sections/executive_summary.py + quick_overview.py + KPI 통합.
"""
import streamlit as st
from core.charts import (
create_confidence_tier_pie_chart,
create_nudge_by_cej_bar_chart,
create_platform_bar_chart,
)
def get_risk_grade(high_nudges: int) -> tuple[str, str, str]:
"""Get risk grade based on HIGH tier nudge count."""
if high_nudges == 0:
return "A", "grade-a", "우수 (부정 언급 없음)"
elif high_nudges <= 10:
return "B", "grade-b", "양호"
elif high_nudges <= 30:
return "C", "grade-c", "주의 필요"
else:
return "D", "grade-d", "즉시 대응"
def render_summary(data: dict):
"""감성분석 요약 카드 렌더링."""
total_nudge = data.get("total_nudge", 0)
high_count = data.get("high_count", 0)
medium_count = data.get("medium_count", 0)
risk_score = data.get("risk_score", 0.0)
tier_stats = data.get("tier_stats") or {}
platform_stats = data.get("platform_stats") or {}
cej_stats = data.get("cej_stats") or {}
candidates = data.get("candidates") or []
campaign_overview = data.get("campaign_overview") or {}
# --- Sentiment Summary Card ---
grade, grade_class, grade_desc = get_risk_grade(high_count)
if total_nudge == 0:
nudge_insight = "부정 언급 없음"
elif high_count == 0:
nudge_insight = f"잠재 리스크 {total_nudge}건 (확신도 낮음)"
else:
nudge_insight = f"HIGH {high_count}건 / 총 {total_nudge}건"
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown(f"""
건강 등급
{grade}
{grade_desc}
""", unsafe_allow_html=True)
with col2:
st.metric("🔴 HIGH", f"{high_count}건", help="≥85% 확신도 - 즉시 대응 권장")
with col3:
st.metric("리스크 점수", f"{risk_score:.1f}", help="가중 평균 점수")
with col4:
citation_total = sum(c.get("citation_count", 0) or 0 for c in candidates)
st.metric("총 인용 소스", f"{citation_total}개")
# --- Pipeline Overview (collapsible) ---
with st.expander("📊 데이터 파이프라인 상세", expanded=False):
_render_pipeline_overview(campaign_overview)
# --- Quick Charts ---
st.markdown("---")
chart_col1, chart_col2, chart_col3, chart_col4 = st.columns(4)
with chart_col1:
st.markdown("##### Confidence Tier 분포")
if tier_stats and sum(tier_stats.values()) > 0:
fig = create_confidence_tier_pie_chart(tier_stats)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
else:
st.info("부정 언급이 없습니다")
with chart_col2:
st.markdown("##### 플랫폼별 분포")
if platform_stats and sum(platform_stats.values()) > 0:
fig = create_platform_bar_chart(platform_stats)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
else:
st.info("플랫폼 데이터가 없습니다")
with chart_col3:
st.markdown("##### CEJ 단계별 분포")
if cej_stats and sum(cej_stats.values()) > 0:
fig = create_nudge_by_cej_bar_chart(cej_stats)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
else:
st.info("CEJ 데이터가 없습니다")
with chart_col4:
_render_llm_verification_summary(campaign_overview)
def _render_pipeline_overview(campaign_overview: dict):
"""데이터 파이프라인 현황."""
overview_total = campaign_overview.get("total_answers", 0)
overview_ih_neg = campaign_overview.get("in_house_negative_count", 0)
overview_llm_done = campaign_overview.get("llm_verified_in_house", 0)
overview_llm_pending = campaign_overview.get("llm_pending", 0)
overview_llm_confirmed = campaign_overview.get("llm_confirmed_negative", 0)
pipe1, pipe2, pipe3, pipe4, pipe5 = st.columns(5)
with pipe1:
st.metric(
label="전체 AI 답변",
value=f"{overview_total:,}건",
help="감성 분석이 완료된 전체 AI 답변 수",
)
with pipe2:
ih_rate = (overview_ih_neg / overview_total * 100) if overview_total > 0 else 0
st.metric(
label="1차 부정 감지 (DeBERTa)",
value=f"{overview_ih_neg:,}건",
delta=f"{ih_rate:.1f}%",
delta_color="inverse",
help="자사 브랜드에 대한 부정 감성이 감지된 답변 (ABSA 기반)",
)
with pipe3:
verify_rate = (overview_llm_done / overview_ih_neg * 100) if overview_ih_neg > 0 else 0
st.metric(
label="2차 검증 완료 (LLM)",
value=f"{overview_llm_done:,}건",
delta=f"{verify_rate:.0f}% 완료",
delta_color="normal" if verify_rate >= 90 else "off",
help="LLM 2차 검증이 완료된 건수",
)
with pipe4:
st.metric(
label="2차 검증 대기",
value=f"{overview_llm_pending:,}건",
help="아직 LLM 2차 검증이 안 된 건수",
)
with pipe5:
confirm_rate = (overview_llm_confirmed / overview_llm_done * 100) if overview_llm_done > 0 else 0
st.metric(
label="최종 정탐",
value=f"{overview_llm_confirmed:,}건",
delta=f"정탐률 {confirm_rate:.1f}%",
help="1차 + 2차 검증 모두에서 부정으로 확정된 건수",
)
def _render_llm_verification_summary(campaign_overview: dict):
"""LLM 2차 검증 요약."""
st.markdown("##### 🤖 LLM 2차 검증")
overview_llm_done = campaign_overview.get("llm_verified_in_house", 0)
overview_llm_pending = campaign_overview.get("llm_pending", 0)
overview_llm_confirmed = campaign_overview.get("llm_confirmed_negative", 0)
if overview_llm_done > 0:
fp_count = overview_llm_done - overview_llm_confirmed
fp_rate = (fp_count / overview_llm_done * 100) if overview_llm_done > 0 else 0
st.metric(
label="검증 완료",
value=f"{overview_llm_done}건",
delta=f"오탐 {fp_count}건 ({fp_rate:.0f}%)",
delta_color="inverse",
)
st.caption(f"✅ 정탐: {overview_llm_confirmed}건 | ❌ 오탐: {fp_count}건")
if overview_llm_pending > 0:
st.caption(f"⏳ 대기: {overview_llm_pending}건")
elif overview_llm_pending > 0:
st.info(f"⏳ {overview_llm_pending}건 검증 대기 중")
else:
st.info("검증 데이터 없음")