AI.incheon.citizens / incheon_simulator_app.py
byoonkim's picture
Update incheon_simulator_app.py
adce559 verified
Raw
History Blame Contribute Delete
9.86 kB
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)