|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from sklearn.decomposition import LatentDirichletAllocation |
|
|
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer |
|
|
from konlpy.tag import Okt |
|
|
import re |
|
|
import altair as alt |
|
|
import networkx as nx |
|
|
import io |
|
|
import matplotlib.pyplot as plt |
|
|
import matplotlib.font_manager as fm |
|
|
import colorsys |
|
|
from gensim.models import CoherenceModel |
|
|
import gensim.corpora as corpora |
|
|
from anthropic import Anthropic |
|
|
import os |
|
|
|
|
|
|
|
|
st.set_page_config(layout="wide", page_title="📊 토픽모델링 for SK", page_icon="📊") |
|
|
|
|
|
|
|
|
@st.cache_resource |
|
|
def load_okt(): |
|
|
return Okt() |
|
|
|
|
|
okt = load_okt() |
|
|
|
|
|
|
|
|
default_stop_words = ['이', '그', '저', '것', '수', '등', '들', '및', '에서', '그리고', '그래서', '또는', '그런데', '의', '대한', '간의'] |
|
|
|
|
|
@st.cache_data |
|
|
def preprocess_text(text, stop_words): |
|
|
text = re.sub(r'[^가-힣\s]', '', str(text)) |
|
|
_okt = load_okt() |
|
|
nouns = _okt.nouns(text) |
|
|
processed = [word for word in nouns if word not in stop_words and len(word) > 1] |
|
|
return ' '.join(processed) |
|
|
|
|
|
|
|
|
@st.cache_data |
|
|
def calculate_perplexity_coherence(df, text_column, stop_words, topic_range): |
|
|
df = df.copy() |
|
|
df['processed_text'] = df[text_column].apply(lambda x: preprocess_text(x, stop_words)) |
|
|
vectorizer = CountVectorizer(max_df=0.95, min_df=2) |
|
|
doc_term_matrix = vectorizer.fit_transform(df['processed_text']) |
|
|
words = vectorizer.get_feature_names_out() |
|
|
|
|
|
perplexities = [] |
|
|
coherences = [] |
|
|
dictionary = corpora.Dictionary([text.split() for text in df['processed_text']]) |
|
|
corpus = [dictionary.doc2bow(text.split()) for text in df['processed_text']] |
|
|
|
|
|
for n_topics in topic_range: |
|
|
lda = LatentDirichletAllocation(n_components=n_topics, random_state=42) |
|
|
lda_output = lda.fit_transform(doc_term_matrix) |
|
|
|
|
|
|
|
|
perplexity = lda.perplexity(doc_term_matrix) |
|
|
perplexities.append(perplexity) |
|
|
|
|
|
|
|
|
lda_topics = [[words[i] for i in topic.argsort()[:-10 - 1:-1]] for topic in lda.components_] |
|
|
coherence_model_lda = CoherenceModel(topics=lda_topics, texts=[text.split() for text in df['processed_text']], dictionary=dictionary, coherence='c_v') |
|
|
coherence = coherence_model_lda.get_coherence() |
|
|
coherences.append(coherence) |
|
|
|
|
|
|
|
|
perplexity_diffs = [-((perplexities[i] - perplexities[i-1]) / perplexities[i-1]) |
|
|
if i > 0 else 0 for i in range(len(perplexities))] |
|
|
|
|
|
|
|
|
coherence_diffs = [(coherences[i] - coherences[i-1]) / coherences[i-1] |
|
|
if i > 0 else 0 for i in range(len(coherences))] |
|
|
|
|
|
|
|
|
combined_scores = [] |
|
|
for p_diff, c_diff in zip(perplexity_diffs, coherence_diffs): |
|
|
if p_diff > 0 and c_diff > 0: |
|
|
combined_score = 2 / ((1/p_diff) + (1/c_diff)) |
|
|
else: |
|
|
combined_score = 0 |
|
|
combined_scores.append(combined_score) |
|
|
|
|
|
return perplexities, coherences, combined_scores, perplexity_diffs, coherence_diffs |
|
|
|
|
|
|
|
|
def plot_perplexity_coherence(topic_range, perplexities, coherences): |
|
|
df_metrics = pd.DataFrame({ |
|
|
'토픽 수': list(topic_range), |
|
|
'Perplexity': perplexities, |
|
|
'Coherence': coherences |
|
|
}) |
|
|
|
|
|
chart1 = alt.Chart(df_metrics).mark_line().encode( |
|
|
x=alt.X('토픽 수:Q', title='토픽 수', axis=alt.Axis(tickCount=len(list(topic_range)), values=list(topic_range))), |
|
|
y=alt.Y('Perplexity:Q', title='Perplexity', scale=alt.Scale(type='log')), |
|
|
tooltip=['토픽 수', 'Perplexity'] |
|
|
).properties( |
|
|
width=600, |
|
|
height=400, |
|
|
title='Perplexity vs 토픽 수' |
|
|
) |
|
|
|
|
|
chart2 = alt.Chart(df_metrics).mark_line(color='orange').encode( |
|
|
x=alt.X('토픽 수:Q', title='토픽 수', axis=alt.Axis(tickCount=len(list(topic_range)), values=list(topic_range))), |
|
|
y=alt.Y('Coherence:Q', title='Coherence'), |
|
|
tooltip=['토픽 수', 'Coherence'] |
|
|
).properties( |
|
|
width=600, |
|
|
height=400, |
|
|
title='Coherence vs 토픽 수' |
|
|
) |
|
|
|
|
|
st.altair_chart(chart1, use_container_width=True) |
|
|
st.altair_chart(chart2, use_container_width=True) |
|
|
|
|
|
best_coherence_index = np.argmax(coherences) |
|
|
best_topic_count = list(topic_range)[best_coherence_index] |
|
|
st.markdown(f"**추천 토픽 수**: Coherence 값이 가장 높은 **{best_topic_count}개의 토픽**") |
|
|
|
|
|
|
|
|
def perform_topic_modeling(df, text_column, num_topics, stop_words): |
|
|
df = df.copy() |
|
|
df['processed_text'] = df[text_column].apply(lambda x: preprocess_text(x, stop_words)) |
|
|
|
|
|
vectorizer = CountVectorizer(max_df=0.95, min_df=2) |
|
|
doc_term_matrix = vectorizer.fit_transform(df['processed_text']) |
|
|
|
|
|
lda = LatentDirichletAllocation(n_components=num_topics, random_state=42) |
|
|
lda_output = lda.fit_transform(doc_term_matrix) |
|
|
|
|
|
tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2) |
|
|
tfidf_matrix = tfidf_vectorizer.fit_transform(df['processed_text']) |
|
|
|
|
|
feature_names = vectorizer.get_feature_names_out() |
|
|
|
|
|
topic_results = [] |
|
|
|
|
|
for idx, topic in enumerate(lda.components_): |
|
|
lda_top_words = sorted([(feature_names[i], topic[i]) for i in range(len(topic))], key=lambda x: x[1], reverse=True)[:10] |
|
|
topic_docs = lda_output[:, idx].argsort()[::-1][:100] |
|
|
topic_tfidf = tfidf_matrix[topic_docs].mean(axis=0).A1 |
|
|
tfidf_top_words = sorted([(feature_names[i], topic_tfidf[i]) for i in range(len(topic_tfidf))], key=lambda x: x[1], reverse=True)[:10] |
|
|
weight = lda_output[:, idx].mean() * 100 |
|
|
|
|
|
topic_name = ", ".join([word for word, _ in lda_top_words[:5]]) |
|
|
|
|
|
topic_results.append({ |
|
|
'topic_num': idx + 1, |
|
|
'topic_name': topic_name, |
|
|
'lda_words': [word for word, _ in lda_top_words], |
|
|
'tfidf_words': [word for word, _ in tfidf_top_words], |
|
|
'weight': weight |
|
|
}) |
|
|
|
|
|
return topic_results, lda, lda_output, tfidf_matrix, feature_names |
|
|
|
|
|
|
|
|
def create_network_graph(topic_results, num_words=10): |
|
|
G = nx.Graph() |
|
|
colors = generate_colors(len(topic_results)) |
|
|
|
|
|
for idx, topic in enumerate(topic_results): |
|
|
words = topic['lda_words'][:num_words] |
|
|
color = colors[idx] |
|
|
|
|
|
for word in words: |
|
|
if not G.has_node(word): |
|
|
G.add_node(word, color=color) |
|
|
|
|
|
for i in range(len(words)): |
|
|
for j in range(i+1, len(words)): |
|
|
if not G.has_edge(words[i], words[j]): |
|
|
G.add_edge(words[i], words[j]) |
|
|
|
|
|
return G |
|
|
|
|
|
|
|
|
def plot_network_graph(G): |
|
|
|
|
|
font_path = "./NanumBarunGothic.ttf" |
|
|
if not os.path.exists(font_path): |
|
|
|
|
|
alt_paths = [ |
|
|
"/usr/share/fonts/truetype/nanum/NanumBarunGothic.ttf", |
|
|
"/usr/share/fonts/NanumBarunGothic.ttf", |
|
|
] |
|
|
for alt_path in alt_paths: |
|
|
if os.path.exists(alt_path): |
|
|
font_path = alt_path |
|
|
break |
|
|
|
|
|
if os.path.exists(font_path): |
|
|
font_prop = fm.FontProperties(fname=font_path) |
|
|
plt.rcParams['font.family'] = font_prop.get_name() |
|
|
else: |
|
|
font_prop = fm.FontProperties() |
|
|
|
|
|
st.warning("한글 폰트 파일을 찾을 수 없습니다. NanumBarunGothic.ttf를 앱 루트에 배치해주세요.") |
|
|
|
|
|
plt.figure(figsize=(12, 8)) |
|
|
|
|
|
pos = nx.spring_layout(G, k=0.5, iterations=50, seed=42) |
|
|
node_colors = [G.nodes[node]['color'] for node in G.nodes()] |
|
|
|
|
|
nx.draw(G, pos, node_color=node_colors, with_labels=False, node_size=1000, |
|
|
edge_color='gray', width=0.5) |
|
|
|
|
|
for node, (x, y) in pos.items(): |
|
|
plt.text(x, y, node, fontsize=8, ha='center', va='center', |
|
|
fontproperties=font_prop, fontweight='bold', |
|
|
bbox=dict(facecolor='white', edgecolor='none', alpha=0.7)) |
|
|
|
|
|
plt.title("토픽 단어 네트워크", fontsize=16, fontproperties=font_prop) |
|
|
plt.axis('off') |
|
|
|
|
|
img_bytes = io.BytesIO() |
|
|
plt.savefig(img_bytes, format='png', dpi=300, bbox_inches='tight') |
|
|
img_bytes.seek(0) |
|
|
plt.close() |
|
|
|
|
|
return img_bytes |
|
|
|
|
|
|
|
|
def generate_colors(n): |
|
|
HSV_tuples = [(x * 1.0 / n, 0.5, 0.9) for x in range(n)] |
|
|
return ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv)) for hsv in HSV_tuples] |
|
|
|
|
|
|
|
|
def display_topic_summary(topic_results): |
|
|
topic_summary_df = pd.DataFrame([ |
|
|
{ |
|
|
'토픽 번호': f"토픽{info['topic_num']}", |
|
|
'비중': f"{info['weight']:.1f}%", |
|
|
'LDA 상위 단어': ", ".join(info['lda_words'][:10]), |
|
|
'TF-IDF 상위 단어': ", ".join(info['tfidf_words'][:10]) |
|
|
} for info in topic_results |
|
|
]) |
|
|
st.table(topic_summary_df) |
|
|
|
|
|
|
|
|
def interpret_topics_full(api_key, topic_results): |
|
|
client = Anthropic(api_key=api_key) |
|
|
|
|
|
prompt = f"""다음은 LDA 토픽 모델링 결과로 나온 각 토픽의 정보입니다. 이를 바탕으로 전체 토픽을 종합적으로 해석해주세요: |
|
|
|
|
|
{", ".join([f"토픽 {info['topic_num']} ({info['weight']:.1f}%)" for info in topic_results])} |
|
|
각 토픽의 주요 단어: |
|
|
""" |
|
|
for info in topic_results: |
|
|
prompt += f""" |
|
|
토픽 {info['topic_num']} (비중: {info['weight']:.1f}%): |
|
|
LDA 상위 단어: {', '.join(info['lda_words'][:10])} |
|
|
TF-IDF 상위 단어: {', '.join(info['tfidf_words'][:10])} |
|
|
""" |
|
|
|
|
|
prompt += """ |
|
|
위 정보를 바탕으로 다음 형식에 맞춰 답변해주세요: |
|
|
1. 전체 문서의 주제 요약 (3-4문장): |
|
|
[여기에 전체 문서의 주제를 종합적으로 설명해주세요. 각 토픽의 비중을 고려하여 중요도를 반영해주세요.] |
|
|
2. 각 토픽 요약: |
|
|
[각 토픽에 대해 다음 형식으로 요약해주세요] |
|
|
토픽[번호] "[토픽명]" [비중]% |
|
|
• LDA 상위 단어 10개: [LDA 상위 단어 10개를 쉼표로 구분하여 나열] |
|
|
• TF-IDF 상위 단어 10개: [TF-IDF 상위 단어 10개를 쉼표로 구분하여 나열] |
|
|
• 토픽명 설명: [토픽명이 이렇게 지어진 이유를 1-2문장으로 설명해주세요. LDA와 TF-IDF 상위 단어들이 어떻게 이 토픽명과 연관되는지 설명하세요.] |
|
|
• 토픽 설명: [2-3문장으로 토픽의 전반적인 내용을 설명해주세요.] |
|
|
주의사항: |
|
|
1. 토픽명은 "[구체적인 토픽명]" 형식으로 작성해주세요. 반드시 8어절 이상으로 구체적이고 설명적으로 작성해야 합니다. |
|
|
2. 예시: "구성원의 전문성 향상을 위한 체계적인 학습과 역량 개발 방안 모색", "조직의 장기적 성과 향상을 위한 핵심 학습 역량 강화 전략", |
|
|
"현재 컬리지 멤버들의 역할 고민과 향후 발전 방향에 대한 논의" 등 |
|
|
3. 토픽명은 단순히 단어를 나열하는 것이 아니라, 토픽의 핵심 주제나 의미를 잘 나타내는 구체적이고 설명적인 문구로 만들어주세요. |
|
|
4. 각 토픽의 LDA 상위 단어와 TF-IDF 상위 단어 10개를 반드시 포함해주세요. |
|
|
위 형식에 맞춰 답변해주세요. 사용자가 쉽게 복사하여 사용할 수 있도록 간결하고 명확하게 작성해주세요. |
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
message = client.messages.create( |
|
|
model="claude-sonnet-4-5-20250929", |
|
|
max_tokens=4096, |
|
|
messages=[ |
|
|
{"role": "user", "content": prompt} |
|
|
] |
|
|
) |
|
|
return message.content[0].text |
|
|
except Exception as e: |
|
|
return f"Claude API 호출 중 오류가 발생했습니다: {str(e)}" |
|
|
|
|
|
|
|
|
def download_topic_assignment(df, key_suffix=''): |
|
|
csv = df.to_csv(index=False) |
|
|
st.download_button( |
|
|
label="토픽 할당 데이터 다운로드", |
|
|
data=csv, |
|
|
file_name='topic_assignment.csv', |
|
|
mime='text/csv', |
|
|
key=f"download_button_{key_suffix}" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
.css-1adrfps { |
|
|
padding: 0px; |
|
|
} |
|
|
.css-1kyxreq { |
|
|
padding: 10px; |
|
|
} |
|
|
.topic-summary { |
|
|
background-color: #f0f2f6; |
|
|
border-left: 5px solid #4e8098; |
|
|
padding: 10px; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
.small-title { |
|
|
font-size: 24px; |
|
|
} |
|
|
.header-text { |
|
|
color: #707070; |
|
|
text-align: right; |
|
|
width: 100%; |
|
|
padding: 10px; |
|
|
} |
|
|
</style> |
|
|
<div class="header-text"> |
|
|
mySUNI Crystal.B |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown('<h1 class="small-title">📊토픽모델링 for SK</h1>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.header('설정하기') |
|
|
|
|
|
api_key = st.text_input("Claude API 키를 입력하세요", type="password") |
|
|
if not api_key: |
|
|
api_key = os.environ.get("ANTHROPIC_API_KEY") |
|
|
|
|
|
st.caption("Claude API가 있으면 토픽 종합 해석까지 가능합니다. 공백으로 두면 기본적인 결과만 나옵니다.") |
|
|
|
|
|
stop_words_input = st.text_area("불용어 목록 (쉼표로 구분)", ', '.join(default_stop_words)) |
|
|
stop_words = [word.strip() for word in stop_words_input.split(',') if word.strip()] |
|
|
|
|
|
st.caption("결과를 보고 업데이트해주세요.") |
|
|
|
|
|
uploaded_file = st.file_uploader("CSV 파일을 업로드하세요", type="csv") |
|
|
|
|
|
st.caption("csv-UTF 형식을 사용해주세요!") |
|
|
|
|
|
|
|
|
if uploaded_file is not None: |
|
|
try: |
|
|
df = pd.read_csv(uploaded_file) |
|
|
if df.empty: |
|
|
st.error("CSV 파일에 데이터가 없습니다.") |
|
|
else: |
|
|
st.success("파일이 성공적으로 업로드되었습니다.") |
|
|
st.subheader("데이터 미리보기") |
|
|
st.write(df.head()) |
|
|
|
|
|
text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns) |
|
|
num_topics = st.slider("토픽 수를 선택하세요", 2, 20, 5) |
|
|
|
|
|
|
|
|
tab1, tab2, tab3 = st.tabs(["토픽 수 결정", "전체 분석", "조건부 분석"]) |
|
|
|
|
|
|
|
|
with tab1: |
|
|
st.header("토픽 수 결정") |
|
|
|
|
|
topic_range = st.slider("토픽 수 범위 선택", min_value=2, max_value=20, value=(2, 10), step=1) |
|
|
|
|
|
if st.button("Perplexity 및 Coherence 계산"): |
|
|
topic_range_list = list(range(topic_range[0], topic_range[1] + 1)) |
|
|
perplexities, coherences, combined_scores, perplexity_diffs, coherence_diffs = calculate_perplexity_coherence(df, text_column, stop_words, topic_range_list) |
|
|
|
|
|
plot_perplexity_coherence(topic_range_list, perplexities, coherences) |
|
|
|
|
|
df_metrics_combined = pd.DataFrame({ |
|
|
'토픽 수': topic_range_list, |
|
|
'Combined Score': combined_scores |
|
|
}) |
|
|
|
|
|
chart3 = alt.Chart(df_metrics_combined).mark_line(color='green').encode( |
|
|
x=alt.X('토픽 수:Q', title='토픽 수', axis=alt.Axis(tickCount=len(topic_range_list), values=topic_range_list)), |
|
|
y=alt.Y('Combined Score:Q', title='Combined Score'), |
|
|
tooltip=['토픽 수', 'Combined Score'] |
|
|
).properties( |
|
|
width=600, |
|
|
height=400, |
|
|
title='Combined Score vs 토픽 수' |
|
|
) |
|
|
|
|
|
st.altair_chart(chart3, use_container_width=True) |
|
|
|
|
|
best_combined_index = np.argmax(combined_scores) |
|
|
best_topic_count = topic_range[0] + best_combined_index |
|
|
|
|
|
st.markdown(f""" |
|
|
**추천 토픽 수**: Perplexity와 Coherence의 변화율을 종합적으로 고려하면 **{best_topic_count}개의 토픽**을 추천합니다. |
|
|
|
|
|
- Perplexity 감소율이 가장 큰 토픽 수: {topic_range[0] + np.argmax(perplexity_diffs)} |
|
|
- Coherence 증가율이 가장 큰 토픽 수: {topic_range[0] + np.argmax(coherence_diffs)} |
|
|
- Combined Score가 가장 높은 토픽 수: {best_topic_count} |
|
|
""") |
|
|
|
|
|
|
|
|
with tab2: |
|
|
st.header("전체 데이터 분석 결과") |
|
|
|
|
|
with st.spinner("토픽 모델링 실행 중..."): |
|
|
topic_results, lda, lda_output, tfidf_matrix, feature_names = perform_topic_modeling(df, text_column, num_topics, stop_words) |
|
|
|
|
|
display_topic_summary(topic_results) |
|
|
|
|
|
st.header("토픽 비중 그래프") |
|
|
df_weights = pd.DataFrame({ |
|
|
'토픽': [f'토픽 {i+1}' for i in range(num_topics)], |
|
|
'비중': [result['weight'] for result in topic_results] |
|
|
}) |
|
|
|
|
|
colors = generate_colors(num_topics) |
|
|
|
|
|
chart = alt.Chart(df_weights).mark_bar().encode( |
|
|
x=alt.X('토픽:N', axis=alt.Axis(labelAngle=0)), |
|
|
y=alt.Y('비중:Q', axis=alt.Axis(format=',.1f')), |
|
|
color=alt.Color('토픽:N', scale=alt.Scale(range=colors)) |
|
|
).properties( |
|
|
width=600, |
|
|
height=400, |
|
|
title='문서 내 토픽 비중 (%)' |
|
|
) |
|
|
|
|
|
text = chart.mark_text( |
|
|
align='center', |
|
|
baseline='bottom', |
|
|
dy=-5 |
|
|
).encode( |
|
|
text=alt.Text('비중:Q', format='.1f') |
|
|
) |
|
|
|
|
|
st.altair_chart(chart + text, use_container_width=True) |
|
|
|
|
|
st.header("토픽 단어 네트워크 그래프") |
|
|
G = create_network_graph(topic_results, num_words=20) |
|
|
img_bytes = plot_network_graph(G) |
|
|
st.image(img_bytes, caption="토픽별 상위 20개 단어 네트워크", use_container_width=True) |
|
|
|
|
|
st.download_button( |
|
|
label="네트워크 그래프 다운로드", |
|
|
data=img_bytes, |
|
|
file_name="topic_network_graph.png", |
|
|
mime="image/png", |
|
|
) |
|
|
|
|
|
df_with_topic = df.copy() |
|
|
df_with_topic['topic'] = lda_output.argmax(axis=1) + 1 |
|
|
download_topic_assignment(df_with_topic, key_suffix='전체분석') |
|
|
|
|
|
if api_key: |
|
|
st.subheader("토픽 종합 해석") |
|
|
with st.spinner("Claude AI로 토픽 해석 중..."): |
|
|
interpretation = interpret_topics_full(api_key, topic_results) |
|
|
st.text_area("해석 결과", value=interpretation, height=300) |
|
|
|
|
|
|
|
|
with tab3: |
|
|
st.header("조건부 분석 결과") |
|
|
|
|
|
condition_column = st.selectbox("조건부 분석에 사용할 변수를 선택하세요", df.columns) |
|
|
|
|
|
if pd.api.types.is_numeric_dtype(df[condition_column]): |
|
|
min_val, max_val = df[condition_column].min(), df[condition_column].max() |
|
|
analysis_method = st.radio("분석 방법 선택", ["범위 선택", "임계값 기준"]) |
|
|
|
|
|
if analysis_method == "범위 선택": |
|
|
ranges = st.slider(f"{condition_column} 범위 선택", float(min_val), float(max_val), (float(min_val), float(max_val)), step=0.1) |
|
|
conditions = [((df[condition_column] >= ranges[0]) & (df[condition_column] <= ranges[1]), f"{ranges[0]} ~ {ranges[1]}")] |
|
|
else: |
|
|
threshold = st.number_input(f"{condition_column} 임계값 설정", min_value=float(min_val), max_value=float(max_val), value=float((min_val + max_val) / 2)) |
|
|
conditions = [ |
|
|
(df[condition_column] < threshold, f"{condition_column} < {threshold}"), |
|
|
(df[condition_column] >= threshold, f"{condition_column} >= {threshold}") |
|
|
] |
|
|
else: |
|
|
unique_values = df[condition_column].unique() |
|
|
selected_values = st.multiselect(f"{condition_column} 값 선택", unique_values, default=list(unique_values)) |
|
|
conditions = [(df[condition_column] == value, str(value)) for value in selected_values] |
|
|
|
|
|
num_topics_cond = st.slider("토픽 수를 선택하세요", 2, 20, 5, key="조건부토픽수") |
|
|
|
|
|
if st.button("조건부 토픽 모델링 실행"): |
|
|
for condition, condition_name in conditions: |
|
|
st.subheader(f"조건: {condition_name}") |
|
|
|
|
|
filtered_df = df[condition].copy() |
|
|
|
|
|
if filtered_df.empty: |
|
|
st.warning(f"조건 '{condition_name}'에 해당하는 데이터가 없습니다.") |
|
|
continue |
|
|
|
|
|
topic_results, lda, lda_output, tfidf_matrix, feature_names = perform_topic_modeling(filtered_df, text_column, num_topics_cond, stop_words) |
|
|
|
|
|
display_topic_summary(topic_results) |
|
|
|
|
|
st.write("토픽 단어 네트워크 그래프") |
|
|
G = create_network_graph(topic_results, num_words=20) |
|
|
img_bytes = plot_network_graph(G) |
|
|
st.image(img_bytes, caption=f"토픽별 상위 20개 단어 네트워크 (조건: {condition_name})", use_container_width=True) |
|
|
|
|
|
st.download_button( |
|
|
label=f"네트워크 그래프 다운로드 (조건: {condition_name})", |
|
|
data=img_bytes, |
|
|
file_name=f"topic_network_graph_{condition_name}.png", |
|
|
mime="image/png", |
|
|
key=f"download_graph_{condition_name}" |
|
|
) |
|
|
|
|
|
topic_column_name = f'topic_{condition_column}' |
|
|
filtered_df[topic_column_name] = lda_output.argmax(axis=1) + 1 |
|
|
download_topic_assignment(filtered_df, key_suffix=f'조건부분석_{condition_name}') |
|
|
|
|
|
if api_key: |
|
|
st.subheader(f"조건부 분석 종합 해석 (조건: {condition_name})") |
|
|
with st.spinner(f"Claude AI로 조건부 토픽 해석 중... (조건: {condition_name})"): |
|
|
interpretation = interpret_topics_full(api_key, topic_results) |
|
|
st.text_area(f"해석 결과 (조건: {condition_name})", value=interpretation, height=300, key=f"interpretation_{condition_name}") |
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
except pd.errors.EmptyDataError: |
|
|
st.error("업로드된 CSV 파일이 비어있습니다. 다시 확인해주세요.") |
|
|
except UnicodeDecodeError: |
|
|
st.error("파일 인코딩에 문제가 있습니다. UTF-8 인코딩으로 저장된 CSV 파일을 사용해주세요.") |
|
|
except Exception as e: |
|
|
st.error(f"파일을 읽는 중 오류가 발생했습니다: {str(e)}") |
|
|
else: |
|
|
st.info("CSV 파일을 업로드해주세요.") |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
--- |
|
|
© 2025 SK mySUNI 행복 College. All rights reserved. 문의사항이 있으시면 연락주세요 (배수정RF, soojeong.bae@sk.com) |
|
|
""") |