Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -286,137 +286,147 @@ st.markdown("""
|
|
| 286 |
|
| 287 |
st.markdown('<h1 class="small-title">📊토픽모델링 for SK</h1>', unsafe_allow_html=True)
|
| 288 |
|
| 289 |
-
#
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
# 탭 1: 토픽 수 결정
|
| 293 |
-
with tab1:
|
| 294 |
-
st.header("토픽 수 결정")
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
df = pd.read_csv(uploaded_file)
|
| 300 |
-
st.session_state['df'] = df
|
| 301 |
-
st.write(df.head())
|
| 302 |
-
|
| 303 |
-
if 'df' in st.session_state:
|
| 304 |
-
df = st.session_state['df']
|
| 305 |
-
text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns)
|
| 306 |
-
topic_range = st.slider("토픽 수 범위 선택", min_value=2, max_value=20, value=(2, 10))
|
| 307 |
-
|
| 308 |
-
if st.button("Perplexity 및 Coherence 계산"):
|
| 309 |
-
perplexities, coherences = calculate_perplexity_coherence(df, text_column, default_stop_words, range(topic_range[0], topic_range[1] + 1))
|
| 310 |
-
plot_perplexity_coherence(range(topic_range[0], topic_range[1] + 1), perplexities, coherences)
|
| 311 |
-
|
| 312 |
-
# 탭 2: 전체 분석
|
| 313 |
-
with tab2:
|
| 314 |
-
st.header("전체 분석")
|
| 315 |
|
| 316 |
-
|
| 317 |
-
df = st.session_state['df']
|
| 318 |
-
text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns, key="전체분석")
|
| 319 |
-
num_topics = st.slider("토픽 수를 선택하세요", 2, 20, 5)
|
| 320 |
-
|
| 321 |
-
if st.button("토픽 모델링 실행"):
|
| 322 |
-
topic_results, lda, lda_output, tfidf_matrix, feature_names = perform_topic_modeling(df, text_column, num_topics, default_stop_words)
|
| 323 |
-
|
| 324 |
-
# 토픽 요약
|
| 325 |
-
display_topic_summary(topic_results)
|
| 326 |
-
|
| 327 |
-
# 토픽 비중 그래프
|
| 328 |
-
st.header("토픽 비중 그래프")
|
| 329 |
-
df_weights = pd.DataFrame({
|
| 330 |
-
'토픽': [f'토픽 {i+1}' for i in range(num_topics)],
|
| 331 |
-
'비중': [result['weight'] for result in topic_results]
|
| 332 |
-
})
|
| 333 |
-
|
| 334 |
-
colors = generate_colors(num_topics)
|
| 335 |
-
|
| 336 |
-
chart = alt.Chart(df_weights).mark_bar().encode(
|
| 337 |
-
x=alt.X('토픽:N', axis=alt.Axis(labelAngle=0)),
|
| 338 |
-
y=alt.Y('비중:Q', axis=alt.Axis(format=',.1f')),
|
| 339 |
-
color=alt.Color('토픽:N', scale=alt.Scale(range=colors))
|
| 340 |
-
).properties(
|
| 341 |
-
width=600,
|
| 342 |
-
height=400,
|
| 343 |
-
title='문서 내 토픽 비중 (%)'
|
| 344 |
-
)
|
| 345 |
-
|
| 346 |
-
text = chart.mark_text(
|
| 347 |
-
align='center',
|
| 348 |
-
baseline='bottom',
|
| 349 |
-
dy=-5
|
| 350 |
-
).encode(
|
| 351 |
-
text=alt.Text('비중:Q', format='.1f')
|
| 352 |
-
)
|
| 353 |
-
|
| 354 |
-
st.altair_chart(chart + text, use_container_width=True)
|
| 355 |
-
|
| 356 |
-
# 네트워크 그래프
|
| 357 |
-
G = create_network_graph(topic_results, num_words=20)
|
| 358 |
-
img_bytes = plot_network_graph(G)
|
| 359 |
-
st.image(img_bytes, caption="토픽별 상위 20개 단어 네트워크", use_column_width=True)
|
| 360 |
-
|
| 361 |
-
# 토픽 할당 데이터 다운로드
|
| 362 |
-
df['topic'] = lda_output.argmax(axis=1) + 1
|
| 363 |
-
download_topic_assignment(df)
|
| 364 |
-
|
| 365 |
-
# 종합 해석
|
| 366 |
-
api_key = st.text_input("Claude API 키를 입력하세요", type="password")
|
| 367 |
-
if api_key:
|
| 368 |
-
st.subheader("토픽 종합 해석")
|
| 369 |
-
with st.spinner("Claude AI로 토픽 해석 중..."):
|
| 370 |
-
interpretation = interpret_topics_full(api_key, topic_results)
|
| 371 |
-
st.text_area("해석 결과", value=interpretation, height=300)
|
| 372 |
-
|
| 373 |
-
# 탭 3: 조건부 분석
|
| 374 |
-
with tab3:
|
| 375 |
-
st.header("조건부 분석")
|
| 376 |
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns, key="조건부분석")
|
| 380 |
-
condition_column = st.selectbox("조건부 분석에 사용할 변수를 선택하세요", df.columns)
|
| 381 |
-
|
| 382 |
-
if pd.api.types.is_numeric_dtype(df[condition_column]):
|
| 383 |
-
analysis_method = st.radio("분석 방법 선택", ["범위 선택", "임계값 기준"])
|
| 384 |
-
|
| 385 |
-
if analysis_method == "범위 선택":
|
| 386 |
-
min_val, max_val = df[condition_column].min(), df[condition_column].max()
|
| 387 |
-
condition = st.slider(f"{condition_column} 범위 선택", float(min_val), float(max_val), (float(min_val), float(max_val)))
|
| 388 |
-
else:
|
| 389 |
-
threshold = st.number_input(f"{condition_column} 임계값 설정", min_value=float(df[condition_column].min()), max_value=float(df[condition_column].max()), value=float((df[condition_column].min() + df[condition_column].max()) / 2))
|
| 390 |
-
comparison = st.radio("비교 기준", ["이상", "이하"])
|
| 391 |
-
condition = (comparison, threshold)
|
| 392 |
-
else:
|
| 393 |
-
unique_values = df[condition_column].unique()
|
| 394 |
-
condition = st.multiselect(f"{condition_column} 값 선택", unique_values, default=unique_values)
|
| 395 |
|
| 396 |
-
|
|
|
|
|
|
|
| 397 |
|
| 398 |
-
|
| 399 |
-
if isinstance(condition, tuple):
|
| 400 |
-
if condition[0] == "이상":
|
| 401 |
-
filtered_df = df[df[condition_column] >= condition[1]]
|
| 402 |
-
else:
|
| 403 |
-
filtered_df = df[df[condition_column] <= condition[1]]
|
| 404 |
-
else:
|
| 405 |
-
filtered_df = df[df[condition_column].between(*condition)] if isinstance(condition, tuple) else df[df[condition_column].isin(condition)]
|
| 406 |
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
|
| 409 |
-
#
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
# 푸터 추가
|
| 422 |
st.markdown("""
|
|
|
|
| 286 |
|
| 287 |
st.markdown('<h1 class="small-title">📊토픽모델링 for SK</h1>', unsafe_allow_html=True)
|
| 288 |
|
| 289 |
+
# 사이드바 설정
|
| 290 |
+
with st.sidebar:
|
| 291 |
+
st.header('설정하기')
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
+
api_key = st.text_input("Claude API 키를 입력하세요", type="password")
|
| 294 |
+
if not api_key:
|
| 295 |
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
+
st.caption("Claude API가 있으면 토픽 종합 해석까지 가능합니다. 공백으로 두면 기본적인 결과만 나옵니다.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
+
stop_words_input = st.text_area("불용어 목록 (쉼표로 구분)", ', '.join(default_stop_words))
|
| 300 |
+
stop_words = [word.strip() for word in stop_words_input.split(',') if word.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
+
st.caption("결과를 보고 업데이트해주세요.")
|
| 303 |
+
|
| 304 |
+
uploaded_file = st.file_uploader("CSV 파일을 업로드하세요", type="csv")
|
| 305 |
|
| 306 |
+
st.caption("csv-UTF 형식을 사용해주세요!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
+
# 데이터 로드 및 초기 설정
|
| 309 |
+
if uploaded_file is not None:
|
| 310 |
+
try:
|
| 311 |
+
df = pd.read_csv(uploaded_file)
|
| 312 |
+
if df.empty:
|
| 313 |
+
st.error("CSV 파일에 데이터가 없습니다.")
|
| 314 |
+
else:
|
| 315 |
+
st.success("파일이 성공적으로 업로드되었습니다.")
|
| 316 |
+
st.subheader("데이터 미리보기")
|
| 317 |
+
st.write(df.head())
|
| 318 |
+
|
| 319 |
+
text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns)
|
| 320 |
+
num_topics = st.slider("토픽 수를 선택하세요", 2, 20, 5)
|
| 321 |
|
| 322 |
+
# 분석 방법 선택
|
| 323 |
+
analysis_type = st.radio("분석 방법 선택", ["전체 분석", "조건부 분석"])
|
| 324 |
+
|
| 325 |
+
# 토픽 수 검토 탭
|
| 326 |
+
st.sidebar.header('토픽 수 검토')
|
| 327 |
+
topic_range = st.sidebar.slider("토픽 수 범위 선택", min_value=2, max_value=20, value=(2, 10))
|
| 328 |
+
|
| 329 |
+
if st.sidebar.button("Perplexity 및 Coherence 계산"):
|
| 330 |
+
perplexities, coherences = calculate_perplexity_coherence(df, text_column, stop_words, range(topic_range[0], topic_range[1] + 1))
|
| 331 |
+
plot_perplexity_coherence(range(topic_range[0], topic_range[1] + 1), perplexities, coherences)
|
| 332 |
+
|
| 333 |
+
if analysis_type == "전체 분석":
|
| 334 |
+
st.header("전체 데이터 분석 결과")
|
| 335 |
+
|
| 336 |
+
with st.spinner("토픽 모델링 실행 중..."):
|
| 337 |
+
topic_results, lda, lda_output, tfidf_matrix, feature_names = perform_topic_modeling(df, text_column, num_topics, stop_words)
|
| 338 |
+
|
| 339 |
+
# 토픽 요약
|
| 340 |
+
display_topic_summary(topic_results)
|
| 341 |
+
|
| 342 |
+
# 토픽 비중 그래프
|
| 343 |
+
st.header("토픽 비중 그래프")
|
| 344 |
+
df_weights = pd.DataFrame({
|
| 345 |
+
'토픽': [f'토픽 {i+1}' for i in range(num_topics)],
|
| 346 |
+
'비중': [result['weight'] for result in topic_results]
|
| 347 |
+
})
|
| 348 |
+
|
| 349 |
+
colors = generate_colors(num_topics)
|
| 350 |
+
|
| 351 |
+
chart = alt.Chart(df_weights).mark_bar().encode(
|
| 352 |
+
x=alt.X('토픽:N', axis=alt.Axis(labelAngle=0)),
|
| 353 |
+
y=alt.Y('비중:Q', axis=alt.Axis(format=',.1f')),
|
| 354 |
+
color=alt.Color('토픽:N', scale=alt.Scale(range=colors))
|
| 355 |
+
).properties(
|
| 356 |
+
width=600,
|
| 357 |
+
height=400,
|
| 358 |
+
title='문서 내 토픽 비중 (%)'
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
text = chart.mark_text(
|
| 362 |
+
align='center',
|
| 363 |
+
baseline='bottom',
|
| 364 |
+
dy=-5
|
| 365 |
+
).encode(
|
| 366 |
+
text=alt.Text('비중:Q', format='.1f')
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
st.altair_chart(chart + text, use_container_width=True)
|
| 370 |
+
|
| 371 |
+
# 네트워크 그래프
|
| 372 |
+
st.header("토픽 단어 네트워크 그래프")
|
| 373 |
+
G = create_network_graph(topic_results, num_words=20)
|
| 374 |
+
img_bytes = plot_network_graph(G)
|
| 375 |
+
st.image(img_bytes, caption="토픽별 상위 20개 단어 네트워크", use_column_width=True)
|
| 376 |
+
|
| 377 |
+
# 토픽 할당 데이터 다운로드
|
| 378 |
+
df['topic'] = lda_output.argmax(axis=1) + 1
|
| 379 |
+
download_topic_assignment(df)
|
| 380 |
+
|
| 381 |
+
# 종합 해석
|
| 382 |
+
if api_key:
|
| 383 |
+
st.subheader("토픽 종합 해석")
|
| 384 |
+
with st.spinner("Claude AI로 토픽 해석 중..."):
|
| 385 |
+
interpretation = interpret_topics_full(api_key, topic_results)
|
| 386 |
+
st.text_area("해석 결과", value=interpretation, height=300)
|
| 387 |
+
|
| 388 |
+
elif analysis_type == "조건부 분석":
|
| 389 |
+
st.header("조건부 분석 결과")
|
| 390 |
+
|
| 391 |
+
condition_column = st.selectbox("조건부 분석에 사용할 변수를 선택하세요", df.columns)
|
| 392 |
+
if pd.api.types.is_numeric_dtype(df[condition_column]):
|
| 393 |
+
min_val, max_val = df[condition_column].min(), df[condition_column].max()
|
| 394 |
+
st.write(f"{condition_column}의 범위: {min_val:.2f} ~ {max_val:.2f}")
|
| 395 |
+
|
| 396 |
+
analysis_method = st.radio("분석 방법 선택", ["범위 선택", "임계값 기준"])
|
| 397 |
+
|
| 398 |
+
if analysis_method == "범위 선택":
|
| 399 |
+
condition = st.slider(f"{condition_column} 범위 선택", float(min_val), float(max_val), (float(min_val), float(max_val)))
|
| 400 |
+
else: # 임계값 기준
|
| 401 |
+
threshold = st.number_input(f"{condition_column} 임계값 설정", min_value=float(min_val), max_value=float(max_val), value=float((min_val + max_val) / 2))
|
| 402 |
+
comparison = st.radio("비교 기준", ["이상", "이하"])
|
| 403 |
+
condition = (comparison, threshold)
|
| 404 |
+
|
| 405 |
+
is_numeric = True
|
| 406 |
+
else:
|
| 407 |
+
unique_values = df[condition_column].unique()
|
| 408 |
+
condition = st.multiselect(f"{condition_column} 값 선택", unique_values, default=unique_values)
|
| 409 |
+
is_numeric = False
|
| 410 |
|
| 411 |
+
if st.button("토픽 모델링 실행"):
|
| 412 |
+
st.session_state.run_analysis = True
|
| 413 |
+
st.session_state.text_column = text_column
|
| 414 |
+
st.session_state.num_topics = num_topics
|
| 415 |
+
st.session_state.analysis_type = analysis_type
|
| 416 |
+
if analysis_type == "조건부 분석":
|
| 417 |
+
st.session_state.condition_column = condition_column
|
| 418 |
+
st.session_state.condition = condition
|
| 419 |
+
st.session_state.is_numeric = is_numeric
|
| 420 |
+
else:
|
| 421 |
+
st.session_state.run_analysis = False
|
| 422 |
+
except pd.errors.EmptyDataError:
|
| 423 |
+
st.error("업로드된 CSV 파일이 비어있습니다. 다시 확인해주세요.")
|
| 424 |
+
except UnicodeDecodeError:
|
| 425 |
+
st.error("파일 인코딩에 문제가 있습니다. UTF-8 인코딩으로 저장된 CSV 파일을 사용해주세요.")
|
| 426 |
+
except Exception as e:
|
| 427 |
+
st.error(f"파일을 읽는 중 오류가 발생했습니다: {str(e)}")
|
| 428 |
+
else:
|
| 429 |
+
st.info("CSV 파일을 업로드해주세요.")
|
| 430 |
|
| 431 |
# 푸터 추가
|
| 432 |
st.markdown("""
|