"""감성분석 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("검증 데이터 없음")