import streamlit as st import pandas as pd import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.font_manager as fm from datasets import load_dataset import os import google.generativeai as genai import re # --- MUST BE FIRST --- st.set_page_config(page_title="인천 시민 정책 AI 시뮬레이터", layout="wide") # --- Font and Environment Setup --- @st.cache_resource def setup_environment(): # Install Nanum Gothic font os.system('sudo apt-get install -y fonts-nanum') # Clear matplotlib cache to force rebuild os.system('rm -rf ~/.cache/matplotlib') # fm._rebuild() # Removed as it was causing issues try: plt.rc('font', family='NanumBarunGothic') except Exception as e: st.warning(f"폰트 설정 중 오류 발생: {e}. 기본 폰트로 표시됩니다.") setup_environment() # --- Data Loading --- @st.cache_data def load_incheon_data(): dataset = load_dataset('nvidia/Nemotron-Personas-Korea', split='train') df = pd.DataFrame(dataset) return df[df['persona'].str.contains('인천')].copy() # Helper to extract age group from persona def get_age_group_from_persona(persona_text): match = re.search(r'(\d+)세', persona_text) if match: age = int(match.group(1)) if 19 <= age <= 29: return '19-29' elif 30 <= age <= 39: return '30-39' elif 40 <= age <= 49: return '40-49' elif 50 <= age <= 59: return '50-59' elif 60 <= age <= 69: return '60-69' elif age >= 70: return '70대 이상' return '불명' # Unknown age group # --- LLM Analysis Logic --- def get_llm_score(model, persona_text, issue_or_policy): prompt = f""" 당신은 시민 태도/인지/감정 분석 전문가입니다. 다음 페르소나를 가진 인천 시민이 주어진 사안/정책/질문1에 대해 얼마나 긍정적인 태도/인지/감정을 가질지 분석하세요. [시민 페르소나]: {persona_text} [대상 사안/정책/질문]: {issue_or_policy} 결과는 반드시 0에서 100 사이의 숫자만 출력하세요. (0: 매우 부정적, 100: 매우 긍정적) 답변에는 숫자 외에 아무런 텍스트도 포함하지 마세요. """ try: response = model.generate_content(prompt) result = re.findall(r'\d+', response.text) if result: score = float(result[0]) return min(max(score, 0), 100) return 50.0 # Default to neutral if parsing fails except Exception as e: return str(e) st.title("🏙️ 인천 시민 AI 시뮬레이터") st.sidebar.header("설정") # 1. Gemini API Key - hardcoded genai.configure(api_key="입력필요") model = genai.GenerativeModel('gemini-2.5-flash') incheon_df = load_incheon_data() st.header("질문 입력") policy_input = st.text_input("물어보고 싶은 사안을 입력하세요. 상세할 수록 답변이 정확합니다 (예: 예산포함) :", "인천 신공항 건설 및 물류단지 조성") # Sample size slider sample_size = st.slider("분석할 페르소나 총 샘플 수", 1, 100, 20) # Increased max sample size for more flexibility st.markdown("---") st.subheader("페르소나 연령대별 분류") use_age_classification = st.checkbox("연령대별로 분류하여 샘플을 선택하시겠습니까?", value=False) age_group_counts = {} if use_age_classification: st.write("각 연령대별 샘플 수를 입력하세요:") age_groups = ['19-29', '30-39', '40-49', '50-59', '60-69', '70대 이상'] total_age_samples = 0 for group in age_groups: age_group_counts[group] = st.number_input(f"{group}세", min_value=0, max_value=sample_size, value=0, key=f"age_{group}") total_age_samples += age_group_counts[group] if total_age_samples != sample_size: st.error(f"연령대별 샘플 수의 합이 총 샘플 수 ({sample_size})와 일치해야 합니다. 현재 합계: {total_age_samples}") st.stop() # Stop execution if counts don't match if st.button("AI 시뮬레이션 시작"): if incheon_df.empty: st.warning("인천 관련 페르소나 데이터를 찾을 수 없습니다. 데이터셋을 확인해주세요.") st.stop() with st.spinner(f'{sample_size}명의 페르소나를 AI가 분석 중입니다...'): sample_df = pd.DataFrame() # Initialize empty dataframe if use_age_classification: incheon_df['age_group'] = incheon_df['persona'].apply(get_age_group_from_persona) for group, count in age_group_counts.items(): if count > 0: age_filtered_df = incheon_df[incheon_df['age_group'] == group] if len(age_filtered_df) < count: st.warning(f"경고: {group}세 연령대에서 요청된 {count}개 페르소나를 샘플링할 수 없습니다. 사용 가능한 페르소나 수: {len(age_filtered_df)}개. 가능한 만큼 샘플링합니다.") sample_df = pd.concat([sample_df, age_filtered_df.sample(len(age_filtered_df), random_state=42)]) else: sample_df = pd.concat([sample_df, age_filtered_df.sample(count, random_state=42)]) # If total_age_samples is 0 and use_age_classification is true, sample_df might be empty, so handle that if sample_df.empty and sample_size > 0: st.error("연령대별 샘플링 후, 선택된 페르소나가 없습니다. 연령대 입력이 올바른지 확인하세요.") st.stop() else: sample_df = incheon_df.sample(sample_size, random_state=42).copy() # Ensure that sample_df is not empty before proceeding with scoring if sample_df.empty: st.error("선택된 페르소나가 없습니다. 샘플 수 또는 연령대별 샘플링 설정을 확인하세요.") st.stop() scores = [] for persona in sample_df['persona']: score = get_llm_score(model, persona, policy_input) scores.append(score) if any(isinstance(s, str) for s in scores): st.error(f"AI 분석 중 오류가 발생했습니다: {next(s for s in scores if isinstance(s, str))}") st.stop() sample_df['acceptance_score'] = scores def get_reaction_korean(score): if score > 80: return 'Very positive' if score > 60: return 'Positive' if score > 40: return 'Neutral' if score > 20: return 'Negative' return 'Very Negative' sample_df['reaction'] = sample_df['acceptance_score'].apply(get_reaction_korean) avg_score = sample_df['acceptance_score'].mean() st.subheader("시뮬레이션 결과") st.metric("AI-simulated result", f"{avg_score:.2f} / 100") # Reaction distribution graph fig, ax = plt.subplots(figsize=(10, 6)) counts = sample_df['reaction'].value_counts().reindex(['Very positive', 'positive', 'Neutral', 'Negative', 'Very negatie']).fillna(0) counts.plot(kind='bar', ax=ax, color=['#4CAF50', '#8BC34A', '#FFEB3B', '#FF9800', '#F44336']) ax.set_title(f"Distribution:", fontsize=14) ax.set_xlabel("Attitude", fontsize=12) ax.set_ylabel("Number", fontsize=12) ax.tick_params(axis='x', rotation=45) st.pyplot(fig) st.subheader("AI 상세 분석 샘플 (5명)") for idx, row in sample_df.head(5).iterrows(): # Display 5 samples with st.expander(f"페르소나 요약: {row['persona'][:30]}..."): st.write(f"**점수:** {row['acceptance_score']}") st.write(f"**반응:** {row['reaction']}") st.write(f"**전체 페르소나:** {row['persona']}") st.subheader("전체 페르소나 분석 결과") st.dataframe(sample_df[['persona', 'acceptance_score', 'reaction']]) if use_age_classification and not sample_df.empty: st.subheader("연령대별 분석") age_analysis = sample_df.groupby('age_group')['acceptance_score'].mean().reindex(age_groups).fillna(0).reset_index() age_analysis.columns = ['연령대', '평균 수용도'] st.dataframe(age_analysis) fig_age, ax_age = plt.subplots(figsize=(10, 6)) import seaborn as sns # Import seaborn here if not already at the top sns.barplot(x='연령대', y='평균 수용도', data=age_analysis, ax=ax_age, palette='coolwarm') ax_age.set_title(f"연령대별 평균 수용도: {policy_input}", fontsize=14) ax_age.set_xlabel("연령대", fontsize=12) ax_age.set_ylabel("평균 수용도", fontsize=12) ax_age.tick_params(axis='x', rotation=45) st.pyplot(fig_age) # Optional: Detailed reaction distribution per age group st.subheader("연령대별 반응 분포") for group in age_groups: age_group_df = sample_df[sample_df['age_group'] == group] if not age_group_df.empty: st.markdown(f"#### {group}세 연령대") fig_detail, ax_detail = plt.subplots(figsize=(8, 4)) detail_counts = age_group_df['reaction'].value_counts().reindex(['매우 긍정', '긍정', '중립', '부정적/우려', '매우 부정적']).fillna(0) detail_counts.plot(kind='bar', ax=ax_detail, color=['#4CAF50', '#8BC34A', '#FFEB3B', '#FF9800', '#F44336']) ax_detail.set_title(f"{group}세 반응 분포") ax_detail.set_xlabel("반응") ax_detail.set_ylabel("인원 수") ax_detail.tick_params(axis='x', rotation=45) st.pyplot(fig_detail)