soojeongcrystal commited on
Commit
06e3550
·
verified ·
1 Parent(s): 3efb3b1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +323 -185
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
- import anthropic
5
  import json
6
  from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
7
  from sklearn.decomposition import LatentDirichletAllocation
@@ -87,7 +87,63 @@ def plot_network_graph(G):
87
 
88
  return img_bytes
89
 
90
- # 헤더 스타일 변경
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  st.markdown("""
92
  <style>
93
  .css-1adrfps {
@@ -131,9 +187,14 @@ with st.sidebar:
131
 
132
  stop_words_input = st.text_area("불용어 목록 (쉼표로 구분)", ', '.join(default_stop_words))
133
  stop_words = [word.strip() for word in stop_words_input.split(',') if word.strip()]
 
 
134
 
135
  uploaded_file = st.file_uploader("CSV 파일을 업로드하세요", type="csv")
136
 
 
 
 
137
  if uploaded_file is not None:
138
  try:
139
  df = pd.read_csv(uploaded_file)
@@ -147,10 +208,40 @@ if uploaded_file is not None:
147
  text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns)
148
  num_topics = st.slider("토픽 수를 선택하세요", 2, 20, 5)
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  if st.button("토픽 모델링 실행"):
151
  st.session_state.run_analysis = True
152
  st.session_state.text_column = text_column
153
  st.session_state.num_topics = num_topics
 
 
 
 
 
154
  else:
155
  st.session_state.run_analysis = False
156
  except pd.errors.EmptyDataError:
@@ -162,116 +253,74 @@ if uploaded_file is not None:
162
  else:
163
  st.info("CSV 파일을 업로드해주세요.")
164
 
 
165
  if 'run_analysis' in st.session_state and st.session_state.run_analysis:
166
- if 'text_column' in st.session_state and 'num_topics' in st.session_state:
167
  try:
168
- with st.spinner("텍스트처리 중..."):
169
- df['processed_text'] = df[st.session_state.text_column].apply(lambda x: preprocess_text(x, stop_words))
 
 
 
 
 
170
 
171
- with st.spinner("토픽 모델링 실행 중..."):
172
- vectorizer = CountVectorizer(max_df=0.95, min_df=2)
173
- doc_term_matrix = vectorizer.fit_transform(df['processed_text'])
174
 
175
- lda = LatentDirichletAllocation(n_components=st.session_state.num_topics, random_state=42)
176
- lda_output = lda.fit_transform(doc_term_matrix)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2)
179
- tfidf_matrix = tfidf_vectorizer.fit_transform(df['processed_text'])
180
 
181
- st.header("토픽 모델링 결과")
182
- feature_names = vectorizer.get_feature_names_out()
 
 
 
 
 
 
 
183
 
184
- topic_results = []
 
 
 
 
 
 
185
 
186
- for idx, topic in enumerate(lda.components_):
187
- lda_top_words = [(feature_names[i], topic[i]) for i in topic.argsort()[:-11:-1]]
188
- topic_docs = lda_output[:, idx].argsort()[::-1][:100]
189
- topic_tfidf = tfidf_matrix[topic_docs].mean(axis=0).A1
190
- tfidf_top_words = [(feature_names[i], topic_tfidf[i]) for i in topic_tfidf.argsort()[:-11:-1]]
191
- weight = lda_output[:, idx].mean() * 100
192
-
193
- topic_name = ", ".join([word for word, _ in lda_top_words[:5]])
194
-
195
- topic_results.append({
196
- 'topic_num': idx + 1,
197
- 'topic_name': topic_name,
198
- 'lda_words': [word for word, _ in lda_top_words],
199
- 'tfidf_words': [word for word, _ in tfidf_top_words],
200
- 'weight': weight
201
- })
202
-
203
- topic_summary = ", ".join([f"토픽{info['topic_num']}({info['topic_name']}, {info['weight']:.1f}%)" for info in topic_results])
204
- st.markdown(f'<div class="topic-summary">{topic_summary}</div>', unsafe_allow_html=True)
205
 
206
- for idx, topic in enumerate(lda.components_):
207
- st.subheader(f"토픽 {idx + 1}")
208
-
209
- col1, col2 = st.columns(2)
210
-
211
- with col1:
212
- lda_top_words = [(feature_names[i], topic[i]) for i in topic.argsort()[:-11:-1]]
213
- df_lda = pd.DataFrame(lda_top_words, columns=['단어', 'LDA 점수'])
214
- st.subheader("LDA 상위 단어")
215
- st.table(df_lda.style.format({'LDA 점수': '{:.4f}'}))
216
-
217
- with col2:
218
- topic_docs = lda_output[:, idx].argsort()[::-1][:100]
219
- topic_tfidf = tfidf_matrix[topic_docs].mean(axis=0).A1
220
- tfidf_top_words = [(feature_names[i], topic_tfidf[i]) for i in topic_tfidf.argsort()[:-11:-1]]
221
- df_tfidf = pd.DataFrame(tfidf_top_words, columns=['단어', 'TF-IDF'])
222
- st.subheader("TF-IDF 상위 단어")
223
- st.table(df_tfidf.style.format({'TF-IDF': '{:.4f}'}))
224
-
225
- st.header("토픽 비중 그래프")
226
- df_weights = pd.DataFrame({
227
- '토픽': [f'토픽 {i+1}' for i in range(st.session_state.num_topics)],
228
- '비중': [result['weight'] for result in topic_results]
229
- })
230
-
231
- colors = generate_colors(st.session_state.num_topics)
232
-
233
- base = alt.Chart(df_weights).encode(
234
- x=alt.X('토픽:N', axis=alt.Axis(labelAngle=0)),
235
- y=alt.Y('비중:Q', axis=alt.Axis(format=',.1f'))
236
- )
237
-
238
- bars = base.mark_bar().encode(
239
- color=alt.Color('토픽:N', scale=alt.Scale(range=colors))
240
- )
241
-
242
- text = base.mark_text(
243
- align='center',
244
- baseline='middle',
245
- dy=-10
246
- ).encode(
247
- text=alt.Text('비중:Q', format='.1f')
248
- )
249
-
250
- chart = (bars + text).properties(
251
- width=600,
252
- height=400,
253
- title='문서 내 토픽 비중 (%)'
254
- ).configure_axis(
255
- labelFontSize=12,
256
- titleFontSize=14
257
- ).configure_title(
258
- fontSize=16,
259
- font='Arial',
260
- anchor='middle',
261
- color='gray'
262
- )
263
-
264
- st.altair_chart(chart, use_container_width=True)
265
-
266
- st.header("토픽 단어 네트워크 그래프")
267
- try:
268
- if 'network_graph' not in st.session_state:
269
- G = create_network_graph(topic_results, num_words=20)
270
- img_bytes = plot_network_graph(G)
271
- st.session_state.network_graph = img_bytes
272
- else:
273
- img_bytes = st.session_state.network_graph
274
-
275
  st.image(img_bytes, caption="토픽별 상위 20개 단어 네트워크", use_column_width=True)
276
 
277
  st.download_button(
@@ -281,94 +330,176 @@ if 'run_analysis' in st.session_state and st.session_state.run_analysis:
281
  mime="image/png",
282
  key="download_graph"
283
  )
284
- except Exception as e:
285
- st.error(f"네트워크 그래프 생성 중 오류가 발생했습니다: {str(e)}")
286
-
287
- # 토픽 요약 테이블 (항상 표시)
288
- st.subheader("토픽 요약 테이블")
289
- topic_summary_df = pd.DataFrame([
290
- {
291
- '토픽 번호': f"토픽{info['topic_num']}",
292
- '비중': f"{info['weight']:.1f}%",
293
- 'LDA 상위 단어': ", ".join(info['lda_words'][:10]),
294
- 'TF-IDF 상위 단어': ", ".join(info['tfidf_words'][:10])
295
- } for info in topic_results
296
- ])
297
- st.table(topic_summary_df)
298
-
299
- def interpret_topics(api_key, topic_results):
300
- client = anthropic.Anthropic(api_key=api_key)
301
-
302
- prompt = f"""다음은 LDA 토픽 모델링 결과로 나온 각 토픽의 정보입니다. 이를 바탕으로 전체 토픽을 종합적으로 해석해주세요:
303
 
304
- {", ".join([f"토픽 {{info['topic_num']}} (비중: {{info['weight']:.1f}}%)" for info in topic_results])}
305
-
306
- 토픽의 주요 단어:
307
- """
308
- for info in topic_results:
309
- prompt += f"""
310
- 토픽 {info['topic_num']} (비중: {info['weight']:.1f}%):
311
- LDA 상위 단어: {', '.join(info['lda_words'][:10])}
312
- TF-IDF 상위 단어: {', '.join(info['tfidf_words'][:10])}
313
- """
314
 
315
- prompt += """
316
- 위 정보를 바탕으로 다음 형식에 맞춰 답변해주세요:
317
-
318
- 1. 전체 문서의 주제 요약 (3-4문장):
319
- [여기에 전체 문서의 주제를 종합적으로 설명해주세요.토픽 비중을 고려하여 중요도를 반영해주세요.]
320
-
321
- 2. 각 토픽 요약:
322
- [각 토픽 대해 다음 형식으로 요약해주세요]
323
- 토픽[번호] "[토픽명]" [비중]%
324
- • LDA 상위 단어 10개: [LDA 상위 단어 10개를 쉼표로 구분하여 나열]
325
- TF-IDF 상위 단어 10개: [TF-IDF 상위 단어 10개를 쉼표로 구분하여 나열]
326
- • 토픽명 설명: [토픽명이 이렇게 지어진 이유를 1-2문장으로 설명해주세요. LDA와 TF-IDF 상위 단어들이 어떻게 이 토픽명과 연관되는지 설명하세요.]
327
- 토픽 설명: [2-3문장으로 토픽의 전반적인 내용을 설명해주세요.]
328
-
329
- 주의사항:
330
- 1. 토픽명은 "[구체적인 토픽명]" 형식으로 작성해주세요. 반드시 8어절 이상으로 구체적이고 설명적으로 작성해야 합니다.
331
- 예시: "구성원의 전문성 향상을 위한 체계적인 학습과 역량 개발 방안 모색", "조직의 장기적 성과 향상을 위한 핵심 학습 역량 강화 전략",
332
- "현재 컬리지 멤버들의 역할 고민과 향후 발전 방향에 대한 논의" 등
333
- 2. 토픽명은 단순히 단어를 나열하는 것이 아니라, 토픽의 핵심 주제나 의미를 잘 나타내는 구체적이고 설명적인 문구로 만들어주세요.
334
- 3. 토픽명 설명에서는 왜 그러한 토픽명이 선택되었는지, LDA와 TF-IDF 상위 단어들과의 연관성을 구체적으로 설명해주세요.
335
- 4. 토픽의 LDA 상위 단어와 TF-IDF 상위 단어 10개를 반드시 포함해주세요.
336
-
337
- 위 형식에 ��춰 답변해주세요. 사용자가 쉽게 복사하여 사용할 있도록 간결하고 명확하게 작성해주세요.
338
- """
339
-
340
- try:
341
- response = client.messages.create(
342
- model="claude-3-sonnet-20240229",
343
- max_tokens=4000,
344
- temperature=0,
345
- system="당신은 토픽 모델링과 텍스트 분석 전문가입니다. 토픽 모델링 결과에 대해 명확하고 간결하며 상세한 해석을 제공합니다. 모든 응답은 반드시 한국어로만 작성해야 합니다.",
346
- messages=[
347
- {"role": "user", "content": prompt}
348
- ]
349
  )
350
- return response.content[0].text
351
- except Exception as e:
352
- return f"Claude API 호출 중 오류가 발생했습니다: {str(e)}"
353
-
354
  # Claude API를 사용한 토픽 해석 부분
355
  if api_key:
356
- st.header("토픽 종합 해석/8월 28일 코드 일부 수정 중입니다:)")
357
 
358
- col1, col2 = st.columns([3, 1])
359
- with col2:
360
- if st.button("토픽 다시 해석하기", key="reinterpret"):
361
- # 해석 결과 초기화
362
- st.session_state.topic_interpretation = None
363
-
364
- with col1:
365
- # 해석 결과가 없거나 다시 해석하기 버튼을 눌렀을 때만 새로 해석
366
- if 'topic_interpretation' not in st.session_state or st.session_state.topic_interpretation is None:
367
- with st.spinner("토픽 해석 중..."):
368
- st.session_state.topic_interpretation = interpret_topics(api_key, topic_results)
 
 
 
369
 
370
- st.subheader("토픽 모델링 종합 결과")
371
- st.text_area("결과복사하여 사용하세요:", value=st.session_state.topic_interpretation, height=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  else:
373
  st.warning("Claude API 키가 설정되지 않았습니다. https://console.anthropic.com/settings/keys 에 접속하여 API 키를 발급받으시면 토픽명과 해석을 제공받으실 수 있습니다.")
374
 
@@ -377,7 +508,14 @@ if 'run_analysis' in st.session_state and st.session_state.run_analysis:
377
  else:
378
  st.error("유효한 데이터가 없습니다. CSV 파일을 다시 업로드해주세요.")
379
 
 
 
 
 
 
 
 
380
  st.markdown("""
381
  ---
382
  © 2024 SK mySUNI 행복 College. All rights reserved. 문의사항이 있으시면 연락주세요 (배수정RF, soojeong.bae@sk.com)
383
- """)
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
+ from anthropic import Anthropic, HUMAN_PROMPT, AI_PROMPT
5
  import json
6
  from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
7
  from sklearn.decomposition import LatentDirichletAllocation
 
87
 
88
  return img_bytes
89
 
90
+ def perform_topic_modeling(df, text_column, num_topics, stop_words):
91
+ df['processed_text'] = df[text_column].apply(lambda x: preprocess_text(x, stop_words))
92
+
93
+ vectorizer = CountVectorizer(max_df=0.95, min_df=2)
94
+ doc_term_matrix = vectorizer.fit_transform(df['processed_text'])
95
+
96
+ lda = LatentDirichletAllocation(n_components=num_topics, random_state=42)
97
+ lda_output = lda.fit_transform(doc_term_matrix)
98
+
99
+ tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2)
100
+ tfidf_matrix = tfidf_vectorizer.fit_transform(df['processed_text'])
101
+
102
+ feature_names = vectorizer.get_feature_names_out()
103
+
104
+ topic_results = []
105
+
106
+ for idx, topic in enumerate(lda.components_):
107
+ # LDA 상위 단어를 점수 순으로 정렬
108
+ lda_top_words = sorted([(feature_names[i], topic[i]) for i in range(len(topic))], key=lambda x: x[1], reverse=True)[:10]
109
+ topic_docs = lda_output[:, idx].argsort()[::-1][:100]
110
+ topic_tfidf = tfidf_matrix[topic_docs].mean(axis=0).A1
111
+ # TF-IDF 상위 단어도 점수 순으로 정렬
112
+ 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]
113
+ weight = lda_output[:, idx].mean() * 100
114
+
115
+ topic_name = ", ".join([word for word, _ in lda_top_words[:5]])
116
+
117
+ topic_results.append({
118
+ 'topic_num': idx + 1,
119
+ 'topic_name': topic_name,
120
+ 'lda_words': [word for word, _ in lda_top_words],
121
+ 'tfidf_words': [word for word, _ in tfidf_top_words],
122
+ 'weight': weight
123
+ })
124
+
125
+ return topic_results, lda, lda_output, tfidf_matrix, feature_names
126
+
127
+ def perform_conditional_analysis(df, condition_column, text_column, num_topics, stop_words, is_numeric, condition):
128
+ if is_numeric:
129
+ if isinstance(condition, tuple) and condition[0] == "이상":
130
+ filtered_df = df[df[condition_column] >= condition[1]]
131
+ elif isinstance(condition, tuple) and condition[0] == "이하":
132
+ filtered_df = df[df[condition_column] <= condition[1]]
133
+ else: # 범위 선택
134
+ filtered_df = df[(df[condition_column] >= condition[0]) & (df[condition_column] <= condition[1])]
135
+ topic_results, _, _, _, _ = perform_topic_modeling(filtered_df, text_column, num_topics, stop_words)
136
+ return {f"{condition_column} {condition}": topic_results}
137
+ else:
138
+ results = {}
139
+ for value in condition:
140
+ filtered_df = df[df[condition_column] == value]
141
+ if len(filtered_df) > 0:
142
+ topic_results, _, _, _, _ = perform_topic_modeling(filtered_df, text_column, num_topics, stop_words)
143
+ results[value] = topic_results
144
+ return results
145
+
146
+ # 스타일 설정
147
  st.markdown("""
148
  <style>
149
  .css-1adrfps {
 
187
 
188
  stop_words_input = st.text_area("불용어 목록 (쉼표로 구분)", ', '.join(default_stop_words))
189
  stop_words = [word.strip() for word in stop_words_input.split(',') if word.strip()]
190
+
191
+ st.caption("결과를 보고 업데이트해주세요.")
192
 
193
  uploaded_file = st.file_uploader("CSV 파일을 업로드하세요", type="csv")
194
 
195
+ st.caption("csv-UTF형식을 사용해주세요!")
196
+
197
+ # 데이터 로드 및 초기 설정
198
  if uploaded_file is not None:
199
  try:
200
  df = pd.read_csv(uploaded_file)
 
208
  text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns)
209
  num_topics = st.slider("토픽 수를 선택하세요", 2, 20, 5)
210
 
211
+ # 분석 방법 선택
212
+ analysis_type = st.radio("분석 방법 선택", ["전체 분석", "조건부 분석"])
213
+
214
+ if analysis_type == "조건부 분석":
215
+ condition_column = st.selectbox("조건부 분석에 사용할 변수를 선택하세요", df.columns)
216
+
217
+ if pd.api.types.is_numeric_dtype(df[condition_column]):
218
+ min_val, max_val = df[condition_column].min(), df[condition_column].max()
219
+ st.write(f"{condition_column}의 범위: {min_val:.2f} ~ {max_val:.2f}")
220
+
221
+ analysis_method = st.radio("분석 방법 선택", ["범위 선택", "임계값 기준"])
222
+
223
+ if analysis_method == "범위 선택":
224
+ condition = st.slider(f"{condition_column} 범위 선택", float(min_val), float(max_val), (float(min_val), float(max_val)))
225
+ else: # 임계값 기준
226
+ threshold = st.number_input(f"{condition_column} 임계값 설정", min_value=float(min_val), max_value=float(max_val), value=float((min_val + max_val) / 2))
227
+ comparison = st.radio("비교 기준", ["이상", "이하"])
228
+ condition = (comparison, threshold)
229
+
230
+ is_numeric = True
231
+ else:
232
+ unique_values = df[condition_column].unique()
233
+ condition = st.multiselect(f"{condition_column} 값 선택", unique_values, default=unique_values)
234
+ is_numeric = False
235
+
236
  if st.button("토픽 모델링 실행"):
237
  st.session_state.run_analysis = True
238
  st.session_state.text_column = text_column
239
  st.session_state.num_topics = num_topics
240
+ st.session_state.analysis_type = analysis_type
241
+ if analysis_type == "조건부 분석":
242
+ st.session_state.condition_column = condition_column
243
+ st.session_state.condition = condition
244
+ st.session_state.is_numeric = is_numeric
245
  else:
246
  st.session_state.run_analysis = False
247
  except pd.errors.EmptyDataError:
 
253
  else:
254
  st.info("CSV 파일을 업로드해주세요.")
255
 
256
+ # 메인 분석 로직
257
  if 'run_analysis' in st.session_state and st.session_state.run_analysis:
258
+ if 'text_column' in st.session_state and 'num_topics' in st.session_state and 'analysis_type' in st.session_state:
259
  try:
260
+ if st.session_state.analysis_type == " 분석":
261
+ st.header("전체 데이터 분석 결과")
262
+
263
+ with st.spinner("토픽 모델링 실행 중..."):
264
+ topic_results, lda, lda_output, tfidf_matrix, feature_names = perform_topic_modeling(
265
+ df, st.session_state.text_column, st.session_state.num_topics, stop_words
266
+ )
267
 
268
+ # 토픽 요약 표시
269
+ topic_summary = ", ".join([f"토픽{info['topic_num']}({info['topic_name']}, {info['weight']:.1f}%)" for info in topic_results])
270
+ st.markdown(f'<div class="topic-summary">{topic_summary}</div>', unsafe_allow_html=True)
271
 
272
+ # 토픽별 상세 정보 표시
273
+ for idx, topic_info in enumerate(topic_results):
274
+ st.subheader(f"토픽 {idx + 1}")
275
+
276
+ col1, col2 = st.columns(2)
277
+
278
+ with col1:
279
+ df_lda = pd.DataFrame(list(zip(topic_info['lda_words'], lda.components_[idx][np.argsort(lda.components_[idx])[::-1][:10]])),
280
+ columns=['단어', 'LDA 점수'])
281
+ st.subheader("LDA 상위 단어")
282
+ st.table(df_lda.style.format({'LDA 점수': '{:.4f}'}))
283
+
284
+ with col2:
285
+ df_tfidf = pd.DataFrame(list(zip(topic_info['tfidf_words'],
286
+ tfidf_matrix[lda_output[:, idx].argsort()[::-1][:100]].mean(axis=0).A1[np.argsort(tfidf_matrix[lda_output[:, idx].argsort()[::-1][:100]].mean(axis=0).A1)[::-1][:10]])),
287
+ columns=['단어', 'TF-IDF'])
288
+ st.subheader("TF-IDF 상위 단어")
289
+ st.table(df_tfidf.style.format({'TF-IDF': '{:.4f}'}))
290
+
291
+ # 토픽 비중 그래프
292
+ st.header("토픽 비중 그래프")
293
+ df_weights = pd.DataFrame({
294
+ '토픽': [f'토픽 {i+1}' for i in range(st.session_state.num_topics)],
295
+ '비중': [result['weight'] for result in topic_results]
296
+ })
297
 
298
+ colors = generate_colors(st.session_state.num_topics)
 
299
 
300
+ chart = alt.Chart(df_weights).mark_bar().encode(
301
+ x=alt.X('토픽:N', axis=alt.Axis(labelAngle=0)),
302
+ y=alt.Y('비중:Q', axis=alt.Axis(format=',.1f')),
303
+ color=alt.Color('토픽:N', scale=alt.Scale(range=colors))
304
+ ).properties(
305
+ width=600,
306
+ height=400,
307
+ title='문서 내 토픽 비중 (%)'
308
+ )
309
 
310
+ text = chart.mark_text(
311
+ align='center',
312
+ baseline='bottom',
313
+ dy=-5
314
+ ).encode(
315
+ text=alt.Text('비중:Q', format='.1f')
316
+ )
317
 
318
+ st.altair_chart(chart + text, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
+ # 네트워크 그래프
321
+ st.header("토픽 단어 네트워크 그래프")
322
+ G = create_network_graph(topic_results, num_words=20)
323
+ img_bytes = plot_network_graph(G)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  st.image(img_bytes, caption="토픽별 상위 20개 단어 네트워크", use_column_width=True)
325
 
326
  st.download_button(
 
330
  mime="image/png",
331
  key="download_graph"
332
  )
333
+
334
+ # 토픽 요약 테이블
335
+ st.subheader("토픽 요약 테이블")
336
+ topic_summary_df = pd.DataFrame([
337
+ {
338
+ '토픽 번호': f"토픽{info['topic_num']}",
339
+ '비중': f"{info['weight']:.1f}%",
340
+ 'LDA 상위 단어': ", ".join(info['lda_words'][:10]),
341
+ 'TF-IDF 상위 단어': ", ".join(info['tfidf_words'][:10])
342
+ } for info in topic_results
343
+ ])
344
+ st.table(topic_summary_df)
345
+
346
+ elif st.session_state.analysis_type == "조건부 분석":
347
+ st.header("조건부 분석 결과")
 
 
 
 
348
 
349
+ with st.spinner("조건부 토픽 모델링 실행 ..."):
350
+ conditional_results = perform_conditional_analysis(
351
+ df, st.session_state.condition_column, st.session_state.text_column,
352
+ st.session_state.num_topics, stop_words, st.session_state.is_numeric,
353
+ st.session_state.condition
354
+ )
 
 
 
 
355
 
356
+ for value, topic_results in conditional_results.items():
357
+ st.subheader(f"{st.session_state.condition_column}: {value}")
358
+
359
+ # 토픽 요약 표시
360
+ topic_summary = ", ".join([f"토픽{info['topic_num']}({info['topic_name']}, {info['weight']:.1f}%)" for info in topic_results])
361
+ st.markdown(f'<div class="topic-summary">{topic_summary}</div>', unsafe_allow_html=True)
362
+
363
+ # 토픽 요약 테이블
364
+ topic_summary_df = pd.DataFrame([
365
+ {
366
+ '토픽 번호': f"토픽{info['topic_num']}",
367
+ '비중': f"{info['weight']:.1f}%",
368
+ 'LDA 상위 단어': ", ".join(info['lda_words'][:10]),
369
+ 'TF-IDF 상위 단어': ", ".join(info['tfidf_words'][:10])
370
+ } for info in topic_results
371
+ ])
372
+ st.table(topic_summary_df)
373
+
374
+ # 네트워크 그래프
375
+ st.subheader(f"{value} - 토픽 단어 네트워크 그래프")
376
+ G = create_network_graph(topic_results, num_words=20)
377
+ img_bytes = plot_network_graph(G)
378
+ st.image(img_bytes, caption=f"{value} - 토픽별 상위 20개 단어 네트워크", use_column_width=True)
379
+
380
+ st.download_button(
381
+ label=f"{value} - 네트워크 그래프 다운로드",
382
+ data=img_bytes,
383
+ file_name=f"topic_network_graph_{value}.png",
384
+ mime="image/png",
385
+ key=f"download_graph_{value}"
 
 
 
 
386
  )
387
+
388
+ st.markdown("---") # 카테고리 결과 사이에 구분선 추가
389
+
 
390
  # Claude API를 사용한 토픽 해석 부분
391
  if api_key:
392
+ st.header("토픽 종합 해석")
393
 
394
+ def interpret_topics_full(api_key, topic_results):
395
+ anthropic = Anthropic(api_key=api_key)
396
+
397
+ prompt = f"""다음은 LDA 토픽 모델링 결과 나온 각 토픽의 정보입니다. 이를 바탕으로 전체 토픽을 종합적으로 해석해주세요:
398
+
399
+ {", ".join([f"토픽 {{info['topic_num']}} (비중: {{info['weight']:.1f}}%)" for info in topic_results])}
400
+ 토픽의 주요 단어:
401
+ """
402
+ for info in topic_results:
403
+ prompt += f"""
404
+ 토픽 {info['topic_num']} (비중: {info['weight']:.1f}%):
405
+ LDA 상위 단어: {', '.join(info['lda_words'][:10])}
406
+ TF-IDF 상위 단어: {', '.join(info['tfidf_words'][:10])}
407
+ """
408
 
409
+ prompt += """
410
+ �� 정보바탕으로 다음 형식에 맞춰 답변해주세요:
411
+ 1. 전체 문서의 주제 요약 (3-4문장):
412
+ [여기에 전체 문서의 주제를 종합적으로 설명해주세요. 각 토픽의 비중을 고려하여 중요도를 반영해주세요.]
413
+ 2. 각 토픽 요약:
414
+ [각 토픽에 대해 다음 형식으로 요약해주세요]
415
+ 토픽[번호] "[토픽명]" [비중]%
416
+ • LDA 상위 단어 10개: [LDA 상위 단어 10개를 쉼표로 구분하여 나열]
417
+ • TF-IDF 상위 단어 10개: [TF-IDF 상위 단어 10개를 쉼표로 구분하여 나열]
418
+ • 토픽명 설명: [토픽명이 이렇게 지어진 이유를 1-2문장으로 설명해주세요. LDA와 TF-IDF 상위 단어들이 어떻게 이 토픽명과 연관되는지 설명하세요.]
419
+ • 토픽 설명: [2-3문장으로 토픽의 전반적인 내용을 설명해주세요.]
420
+ 주의사항:
421
+ 1. 토픽명은 "[구체적인 토픽명]" 형식으로 작성해주세요. 반드시 8어절 이상으로 구체적이고 설명적으로 작성해야 합니다.
422
+ 2. 예시: "구성원의 전문성 향상을 위한 체계적인 학습과 역량 개발 방안 모색", "조직의 장기적 성과 향상을 위한 핵심 학습 역량 강화 전략",
423
+ "현재 컬리지 멤버들의 역할 고민과 향후 발전 방향에 대한 논의" 등
424
+ 3. 토픽명은 단순히 단어를 나열하는 것이 아니라, 토픽의 핵심 주제나 의미를 잘 나타내는 구체적이고 설명적인 문구로 만들어주세요.
425
+ 4. 각 토픽의 LDA 상위 단어와 TF-IDF 상위 단어 10개를 반드시 포함해주세요.
426
+ 위 형식에 맞춰 답변해주세요. 사용자가 쉽게 복사하여 사용할 수 있도록 간결하고 명확하게 작성해주세요.
427
+ """
428
+
429
+ try:
430
+ completion = anthropic.completions.create(
431
+ model="claude-2.1",
432
+ max_tokens_to_sample=4000,
433
+ prompt=f"{HUMAN_PROMPT} {prompt} {AI_PROMPT}",
434
+ )
435
+ return completion.completion
436
+ except Exception as e:
437
+ return f"Claude API 호출 중 오류가 발생했습니다: {str(e)}"
438
+
439
+ def interpret_topics_conditional(api_key, topic_results, condition):
440
+ anthropic = Anthropic(api_key=api_key)
441
+
442
+ prompt = f"""다음은 LDA 토픽 모델링 결과로 나온 각 토픽의 정보입니다. 이를 바탕으로 각 토픽에 대해 간략히 요약해주세요:
443
+
444
+ 조건: {condition}
445
+ {", ".join([f"토픽 {{info['topic_num']}} (비중: {{info['weight']:.1f}}%)" for info in topic_results])}
446
+ 각 토픽의 주요 단어:
447
+ """
448
+ for info in topic_results:
449
+ prompt += f"""
450
+ 토픽 {info['topic_num']} (비중: {info['weight']:.1f}%):
451
+ LDA 상위 단어: {', '.join(info['lda_words'][:10])}
452
+ TF-IDF 상위 단어: {', '.join(info['tfidf_words'][:10])}
453
+ """
454
+
455
+ prompt += """
456
+ 위 정보를 바탕으로 다음 형식에 맞춰 답변해주세요:
457
+ 각 토픽 요약:
458
+ 토픽[번호] "[토픽명]" [비중]%
459
+
460
+ 주의사항:
461
+ 1. 토픽명은 "[구체적인 토픽명]" 형식으로 작성해주세요. 반드시 8어절 이상으로 구체적이고 설명적으로 작성해야 합니다.
462
+ 2. 예시: "구성원의 전문성 향상을 위한 체계적인 학습과 역량 개발 방안 모색", "조직의 장기적 성과 향상을 위한 핵심 학습 역량 강화 전략",
463
+ "현재 컬리지 멤버들의 역할 고민과 향후 발전 방향에 대한 논의" 등
464
+ 3. 토픽명은 단순히 단어를 나열하는 것이 아니라, 토픽의 핵심 주제나 의미를 잘 나타내는 구체적이고 설명적인 문구로 만들어주세요.
465
+
466
+ 위 형식에 맞춰 각 토픽에 대해 간략히 요약해주세요. 사용자가 쉽게 복사하여 사용할 수 있도록 간결하고 명확하게 작성해주세요.
467
+ """
468
+
469
+ try:
470
+ completion = anthropic.completions.create(
471
+ model="claude-2.1",
472
+ max_tokens_to_sample=2000,
473
+ prompt=f"{HUMAN_PROMPT} {prompt} {AI_PROMPT}",
474
+ )
475
+ return completion.completion
476
+ except Exception as e:
477
+ return f"Claude API 호출 중 오류가 발생했습니다: {str(e)}"
478
+
479
+ if st.session_state.analysis_type == "전체 분석":
480
+ col1, col2 = st.columns([3, 1])
481
+ with col2:
482
+ if st.button("토픽 다시 해석하기", key="reinterpret"):
483
+ st.session_state.topic_interpretation = None
484
+
485
+ with col1:
486
+ if 'topic_interpretation' not in st.session_state or st.session_state.topic_interpretation is None:
487
+ with st.spinner("토픽 해석 중..."):
488
+ st.session_state.topic_interpretation = interpret_topics_full(api_key, topic_results)
489
+
490
+ st.subheader("토픽 모델링 종합 결과")
491
+ st.text_area("결과를 복사하여 사용하세요:", value=st.session_state.topic_interpretation, height=500)
492
+ else:
493
+ for value, topic_results in conditional_results.items():
494
+ st.subheader(f"{st.session_state.condition_column}: {value} - 토픽 해석")
495
+ if st.button(f"{value} - 토픽 다시 해석하기", key=f"reinterpret_{value}"):
496
+ st.session_state[f'topic_interpretation_{value}'] = None
497
+
498
+ if f'topic_interpretation_{value}' not in st.session_state or st.session_state[f'topic_interpretation_{value}'] is None:
499
+ with st.spinner(f"{value} 토픽 해석 중..."):
500
+ st.session_state[f'topic_interpretation_{value}'] = interpret_topics_conditional(api_key, topic_results, condition=f"{st.session_state.condition_column}: {value}")
501
+
502
+ st.text_area(f"{value} - 결과를 복사하여 사용하세요:", value=st.session_state[f'topic_interpretation_{value}'], height=300)
503
  else:
504
  st.warning("Claude API 키가 설정되지 않았습니다. https://console.anthropic.com/settings/keys 에 접속하여 API 키를 발급받으시면 토픽명과 해석을 제공받으실 수 있습니다.")
505
 
 
508
  else:
509
  st.error("유효한 데이터가 없습니다. CSV 파일을 다시 업로드해주세요.")
510
 
511
+ # 분석 초기화 버튼
512
+ if st.button("분석 초기화"):
513
+ for key in st.session_state.keys():
514
+ if key.startswith('topic_interpretation') or key in ['run_analysis', 'run_conditional_analysis', 'conditional_topic_results']:
515
+ del st.session_state[key]
516
+ st.experimental_rerun()
517
+
518
  st.markdown("""
519
  ---
520
  © 2024 SK mySUNI 행복 College. All rights reserved. 문의사항이 있으시면 연락주세요 (배수정RF, soojeong.bae@sk.com)
521
+ """)