soojeongcrystal commited on
Commit
5cdf611
·
verified ·
1 Parent(s): 5f4f426

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +4 -486
app.py CHANGED
@@ -274,485 +274,7 @@ if 'run_analysis' in st.session_state and st.session_state.run_analysis:
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(
327
- label="네트워크 그래프 다운로드",
328
- data=img_bytes,
329
- file_name="topic_network_graph.png",
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
- import streamlit as st
480
- import pandas as pd
481
- import numpy as np
482
- from anthropic import Anthropic, HUMAN_PROMPT, AI_PROMPT
483
- import json
484
- from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
485
- from sklearn.decomposition import LatentDirichletAllocation
486
- from konlpy.tag import Okt
487
- import re
488
- import os
489
- import altair as alt
490
- import colorsys
491
- import matplotlib.pyplot as plt
492
- import matplotlib.font_manager as fm
493
- import networkx as nx
494
- import io
495
-
496
- # Streamlit 페이지 설정
497
- st.set_page_config(layout="wide", page_title="📊 토픽모델링 for SK", page_icon="📊")
498
-
499
- # KoNLPy 형태소 분석기 초기화
500
- @st.cache_resource
501
- def load_okt():
502
- return Okt()
503
-
504
- okt = load_okt()
505
-
506
- # 기본 불용어 목록
507
- default_stop_words = ['이', '그', '저', '것', '수', '등', '들', '및', '에서', '그리고', '그래서', '또는', '그런데', '의', '대한', '간의']
508
-
509
- @st.cache_data
510
- def preprocess_text(text, stop_words):
511
- text = re.sub(r'[^가-힣\s]', '', str(text))
512
- nouns = okt.nouns(text)
513
- processed = [word for word in nouns if word not in stop_words and len(word) > 1]
514
- return ' '.join(processed)
515
-
516
- def generate_colors(n):
517
- HSV_tuples = [(x * 1.0 / n, 0.5, 0.9) for x in range(n)]
518
- return ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv)) for hsv in HSV_tuples]
519
-
520
- def create_network_graph(topic_results, num_words=10):
521
- G = nx.Graph()
522
- colors = generate_colors(len(topic_results))
523
-
524
- for idx, topic in enumerate(topic_results):
525
- words = topic['lda_words'][:num_words]
526
- color = colors[idx]
527
-
528
- for word in words:
529
- if not G.has_node(word):
530
- G.add_node(word, color=color)
531
-
532
- for i in range(len(words)):
533
- for j in range(i+1, len(words)):
534
- if not G.has_edge(words[i], words[j]):
535
- G.add_edge(words[i], words[j])
536
-
537
- return G
538
-
539
- def plot_network_graph(G):
540
- font_path = "./NanumBarunGothic.ttf"
541
- font_prop = fm.FontProperties(fname=font_path)
542
- plt.rcParams['font.family'] = font_prop.get_name()
543
-
544
- plt.figure(figsize=(12, 8))
545
-
546
- pos = nx.spring_layout(G, k=0.5, iterations=50, seed=42)
547
-
548
- node_colors = [G.nodes[node]['color'] for node in G.nodes()]
549
-
550
- nx.draw(G, pos, node_color=node_colors, with_labels=False, node_size=1000,
551
- edge_color='gray', width=0.5)
552
-
553
- for node, (x, y) in pos.items():
554
- plt.text(x, y, node, fontsize=8, ha='center', va='center',
555
- fontproperties=font_prop, fontweight='bold',
556
- bbox=dict(facecolor='white', edgecolor='none', alpha=0.7))
557
-
558
- plt.title("토픽 단어 네트워크", fontsize=16, fontproperties=font_prop)
559
- plt.axis('off')
560
-
561
- img_bytes = io.BytesIO()
562
- plt.savefig(img_bytes, format='png', dpi=300, bbox_inches='tight')
563
- img_bytes.seek(0)
564
- plt.close()
565
-
566
- return img_bytes
567
-
568
- def perform_topic_modeling(df, text_column, num_topics, stop_words):
569
- df['processed_text'] = df[text_column].apply(lambda x: preprocess_text(x, stop_words))
570
-
571
- vectorizer = CountVectorizer(max_df=0.95, min_df=2)
572
- doc_term_matrix = vectorizer.fit_transform(df['processed_text'])
573
-
574
- lda = LatentDirichletAllocation(n_components=num_topics, random_state=42)
575
- lda_output = lda.fit_transform(doc_term_matrix)
576
-
577
- tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2)
578
- tfidf_matrix = tfidf_vectorizer.fit_transform(df['processed_text'])
579
-
580
- feature_names = vectorizer.get_feature_names_out()
581
-
582
- topic_results = []
583
-
584
- for idx, topic in enumerate(lda.components_):
585
- # LDA 상위 단어를 점수 순으로 정렬
586
- lda_top_words = sorted([(feature_names[i], topic[i]) for i in range(len(topic))], key=lambda x: x[1], reverse=True)[:10]
587
- topic_docs = lda_output[:, idx].argsort()[::-1][:100]
588
- topic_tfidf = tfidf_matrix[topic_docs].mean(axis=0).A1
589
- # TF-IDF 상위 단어도 점수 순으로 정렬
590
- 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]
591
- weight = lda_output[:, idx].mean() * 100
592
-
593
- topic_name = ", ".join([word for word, _ in lda_top_words[:5]])
594
-
595
- topic_results.append({
596
- 'topic_num': idx + 1,
597
- 'topic_name': topic_name,
598
- 'lda_words': [word for word, _ in lda_top_words],
599
- 'tfidf_words': [word for word, _ in tfidf_top_words],
600
- 'weight': weight
601
- })
602
-
603
- return topic_results, lda, lda_output, tfidf_matrix, feature_names
604
-
605
- def perform_conditional_analysis(df, condition_column, text_column, num_topics, stop_words, is_numeric, condition):
606
- if is_numeric:
607
- if isinstance(condition, tuple) and condition[0] == "이상":
608
- filtered_df = df[df[condition_column] >= condition[1]]
609
- elif isinstance(condition, tuple) and condition[0] == "이하":
610
- filtered_df = df[df[condition_column] <= condition[1]]
611
- else: # 범위 선택
612
- filtered_df = df[(df[condition_column] >= condition[0]) & (df[condition_column] <= condition[1])]
613
- topic_results, _, _, _, _ = perform_topic_modeling(filtered_df, text_column, num_topics, stop_words)
614
- return {f"{condition_column} {condition}": topic_results}
615
- else:
616
- results = {}
617
- for value in condition:
618
- filtered_df = df[df[condition_column] == value]
619
- if len(filtered_df) > 0:
620
- topic_results, _, _, _, _ = perform_topic_modeling(filtered_df, text_column, num_topics, stop_words)
621
- results[value] = topic_results
622
- return results
623
-
624
- # 스타일 설정
625
- st.markdown("""
626
- <style>
627
- .css-1adrfps {
628
- padding: 0px;
629
- }
630
- .css-1kyxreq {
631
- padding: 10px;
632
- }
633
- .topic-summary {
634
- background-color: #f0f2f6;
635
- border-left: 5px solid #4e8098;
636
- padding: 10px;
637
- margin-bottom: 10px;
638
- }
639
- .small-title {
640
- font-size: 24px;
641
- }
642
- .header-text {
643
- color: #707070;
644
- text-align: right;
645
- width: 100%;
646
- padding: 10px;
647
- }
648
- </style>
649
- <div class="header-text">
650
- mySUNI Crystal.B
651
- </div>
652
- """, unsafe_allow_html=True)
653
-
654
- st.markdown('<h1 class="small-title">📊토픽모델링 for SK</h1>', unsafe_allow_html=True)
655
-
656
- # 사이드바 설정
657
- with st.sidebar:
658
- st.header('설정하기')
659
-
660
- api_key = st.text_input("Claude API 키를 입력하세요", type="password")
661
- if not api_key:
662
- api_key = os.environ.get("ANTHROPIC_API_KEY")
663
-
664
- st.caption("Claude API가 있으면 토픽 종합 해석까지 가능합니다. 공백으로 두면 기본적인 결과만 나옵니다.")
665
-
666
- stop_words_input = st.text_area("불용어 목록 (쉼표로 구분)", ', '.join(default_stop_words))
667
- stop_words = [word.strip() for word in stop_words_input.split(',') if word.strip()]
668
-
669
- st.caption("결과를 보고 업데이트해주세요.")
670
-
671
- uploaded_file = st.file_uploader("CSV 파일을 업로드하세요", type="csv")
672
-
673
- st.caption("csv-UTF형식을 사용해주세요!")
674
-
675
- # 데이터 로드 및 초기 설정
676
- if uploaded_file is not None:
677
- try:
678
- df = pd.read_csv(uploaded_file)
679
- if df.empty:
680
- st.error("CSV 파일에 데이터가 없습니다.")
681
- else:
682
- st.success("파일이 성공적으로 업로드되었습니다.")
683
- st.subheader("데이터 미리보기")
684
- st.write(df.head())
685
-
686
- text_column = st.selectbox("텍스트 컬럼을 선택하세요", df.columns)
687
- num_topics = st.slider("토픽 수를 선택하세요", 2, 20, 5)
688
-
689
- # 분석 방법 선택
690
- analysis_type = st.radio("분석 방법 선택", ["전체 분석", "조건부 분석"])
691
-
692
- if analysis_type == "조건부 분석":
693
- condition_column = st.selectbox("조건부 분석에 사용할 변수를 선택하세요", df.columns)
694
-
695
- if pd.api.types.is_numeric_dtype(df[condition_column]):
696
- min_val, max_val = df[condition_column].min(), df[condition_column].max()
697
- st.write(f"{condition_column}의 범위: {min_val:.2f} ~ {max_val:.2f}")
698
-
699
- analysis_method = st.radio("분석 방법 선택", ["범위 선택", "임계값 기준"])
700
 
701
- if analysis_method == "범위 선택":
702
- condition = st.slider(f"{condition_column} 범위 선택", float(min_val), float(max_val), (float(min_val), float(max_val)))
703
- else: # 임계값 기준
704
- threshold = st.number_input(f"{condition_column} 임계값 설정", min_value=float(min_val), max_value=float(max_val), value=float((min_val + max_val) / 2))
705
- comparison = st.radio("비교 기준", ["이상", "이하"])
706
- condition = (comparison, threshold)
707
-
708
- is_numeric = True
709
- else:
710
- unique_values = df[condition_column].unique()
711
- condition = st.multiselect(f"{condition_column} 값 선택", unique_values, default=unique_values)
712
- is_numeric = False
713
-
714
- if st.button("토픽 모델링 실행"):
715
- st.session_state.run_analysis = True
716
- st.session_state.text_column = text_column
717
- st.session_state.num_topics = num_topics
718
- st.session_state.analysis_type = analysis_type
719
- if analysis_type == "조건부 분석":
720
- st.session_state.condition_column = condition_column
721
- st.session_state.condition = condition
722
- st.session_state.is_numeric = is_numeric
723
- else:
724
- st.session_state.run_analysis = False
725
- except pd.errors.EmptyDataError:
726
- st.error("업로드된 CSV 파일이 비어있습니다. 다시 확인해주세요.")
727
- except UnicodeDecodeError:
728
- st.error("파일 인코딩에 문제가 있습니다. UTF-8 인코딩으로 저장된 CSV 파일을 사용해주세요.")
729
- except Exception as e:
730
- st.error(f"파일을 읽는 중 오류가 발생했습니다: {str(e)}")
731
- else:
732
- st.info("CSV 파일을 업로드해주세요.")
733
-
734
- # 메인 분석 로직
735
- if 'run_analysis' in st.session_state and st.session_state.run_analysis:
736
- if 'text_column' in st.session_state and 'num_topics' in st.session_state and 'analysis_type' in st.session_state:
737
- try:
738
- if st.session_state.analysis_type == "전체 분석":
739
- st.header("전체 데이터 분석 결과")
740
-
741
- with st.spinner("토픽 모델링 실행 중..."):
742
- topic_results, lda, lda_output, tfidf_matrix, feature_names = perform_topic_modeling(
743
- df, st.session_state.text_column, st.session_state.num_topics, stop_words
744
- )
745
-
746
- # 토픽 요약 표시
747
- topic_summary = ", ".join([f"토픽{info['topic_num']}({info['topic_name']}, {info['weight']:.1f}%)" for info in topic_results])
748
- st.markdown(f'<div class="topic-summary">{topic_summary}</div>', unsafe_allow_html=True)
749
-
750
- # 토픽별 상세 정보 표시
751
- for idx, topic_info in enumerate(topic_results):
752
- st.subheader(f"토픽 {idx + 1}")
753
-
754
- col1, col2 = st.columns(2)
755
-
756
  with col1:
757
  df_lda = pd.DataFrame(list(zip(topic_info['lda_words'], lda.components_[idx][np.argsort(lda.components_[idx])[::-1][:10]])),
758
  columns=['단어', 'LDA 점수'])
@@ -959,16 +481,12 @@ if 'run_analysis' in st.session_state and st.session_state.run_analysis:
959
  with col2:
960
  if st.button("토픽 다시 해석하기", key="reinterpret"):
961
  st.session_state.topic_interpretation = None
962
-
963
- with col1:
964
- if 'topic_interpretation' not in st.session_state or st.session_state.topic_interpretation is None:
965
- with st.spinner("토픽 해석 중..."):
966
- st.session_state.topic_interpretation =
967
 
 
968
  if 'topic_interpretation' not in st.session_state or st.session_state.topic_interpretation is None:
969
  with st.spinner("토픽 해석 중..."):
970
  st.session_state.topic_interpretation = interpret_topics_full(api_key, topic_results)
971
-
972
  st.subheader("토픽 모델링 종합 결과")
973
  st.text_area("결과를 복사하여 사용하세요:", value=st.session_state.topic_interpretation, height=500)
974
  else:
@@ -976,11 +494,11 @@ if 'run_analysis' in st.session_state and st.session_state.run_analysis:
976
  st.subheader(f"{st.session_state.condition_column}: {value} - 토픽 해석")
977
  if st.button(f"{value} - 토픽 다시 해석하기", key=f"reinterpret_{value}"):
978
  st.session_state[f'topic_interpretation_{value}'] = None
979
-
980
  if f'topic_interpretation_{value}' not in st.session_state or st.session_state[f'topic_interpretation_{value}'] is None:
981
  with st.spinner(f"{value} 토픽 해석 중..."):
982
  st.session_state[f'topic_interpretation_{value}'] = interpret_topics_conditional(api_key, topic_results, condition=f"{st.session_state.condition_column}: {value}")
983
-
984
  st.text_area(f"{value} - 결과를 복사하여 사용하세요:", value=st.session_state[f'topic_interpretation_{value}'], height=300)
985
  else:
986
  st.warning("Claude API 키가 설정되지 않았습니다. https://console.anthropic.com/settings/keys 에 접속하여 API 키를 발급받으시면 토픽명과 해석을 제공받으실 수 있습니다.")
 
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 점수'])
 
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:
 
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 키를 발급받으시면 토픽명과 해석을 제공받으실 수 있습니다.")