Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| from anthropic import Anthropic, HUMAN_PROMPT, AI_PROMPT | |
| import json | |
| from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer | |
| from sklearn.decomposition import LatentDirichletAllocation | |
| from konlpy.tag import Okt | |
| import re | |
| import os | |
| import altair as alt | |
| import colorsys | |
| import matplotlib.pyplot as plt | |
| import matplotlib.font_manager as fm | |
| import networkx as nx | |
| import io | |
| # Streamlit 페이지 설정 | |
| st.set_page_config(layout="wide", page_title="📊 토픽모델링 for SK", page_icon="📊") | |
| # KoNLPy 형태소 분석기 초기화 | |
| def load_okt(): | |
| return Okt() | |
| okt = load_okt() | |
| # 기본 불용어 목록 | |
| default_stop_words = ['이', '그', '저', '것', '수', '등', '들', '및', '에서', '그리고', '그래서', '또는', '그런데', '의', '대한', '간의'] | |
| def preprocess_text(text, stop_words): | |
| text = re.sub(r'[^가-힣\s]', '', str(text)) | |
| nouns = okt.nouns(text) | |
| processed = [word for word in nouns if word not in stop_words and len(word) > 1] | |
| return ' '.join(processed) | |
| 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 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" | |
| font_prop = fm.FontProperties(fname=font_path) | |
| plt.rcParams['font.family'] = font_prop.get_name() | |
| 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 perform_topic_modeling(df, text_column, num_topics, stop_words): | |
| 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 상위 단어를 점수 순으로 정렬 | |
| 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 | |
| # TF-IDF 상위 단어도 점수 순으로 정렬 | |
| 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 perform_conditional_analysis(df, condition_column, text_column, num_topics, stop_words, is_numeric, condition): | |
| if is_numeric: | |
| if isinstance(condition, tuple) and condition[0] == "이상": | |
| filtered_df = df[df[condition_column] >= condition[1]] | |
| elif isinstance(condition, tuple) and condition[0] == "이하": | |
| filtered_df = df[df[condition_column] <= condition[1]] | |
| else: # 범위 선택 | |
| filtered_df = df[(df[condition_column] >= condition[0]) & (df[condition_column] <= condition[1])] | |
| topic_results, _, _, _, _ = perform_topic_modeling(filtered_df, text_column, num_topics, stop_words) | |
| return {f"{condition_column} {condition}": topic_results} | |
| else: | |
| results = {} | |
| for value in condition: | |
| filtered_df = df[df[condition_column] == value] | |
| if len(filtered_df) > 0: | |
| topic_results, _, _, _, _ = perform_topic_modeling(filtered_df, text_column, num_topics, stop_words) | |
| results[value] = topic_results | |
| return results | |
| # 스타일 설정 | |
| 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) | |
| # 분석 방법 선택 | |
| analysis_type = st.radio("분석 방법 선택", ["전체 분석", "조건부 분석"]) | |
| if analysis_type == "조건부 분석": | |
| 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() | |
| st.write(f"{condition_column}의 범위: {min_val:.2f} ~ {max_val:.2f}") | |
| analysis_method = st.radio("분석 방법 선택", ["범위 선택", "임계값 기준"]) | |
| if analysis_method == "범위 선택": | |
| condition = st.slider(f"{condition_column} 범위 선택", float(min_val), float(max_val), (float(min_val), float(max_val))) | |
| 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)) | |
| comparison = st.radio("비교 기준", ["이상", "이하"]) | |
| condition = (comparison, threshold) | |
| is_numeric = True | |
| else: | |
| unique_values = df[condition_column].unique() | |
| condition = st.multiselect(f"{condition_column} 값 선택", unique_values, default=unique_values) | |
| is_numeric = False | |
| if st.button("토픽 모델링 실행"): | |
| st.session_state.run_analysis = True | |
| st.session_state.text_column = text_column | |
| st.session_state.num_topics = num_topics | |
| st.session_state.analysis_type = analysis_type | |
| if analysis_type == "조건부 분석": | |
| st.session_state.condition_column = condition_column | |
| st.session_state.condition = condition | |
| st.session_state.is_numeric = is_numeric | |
| else: | |
| st.session_state.run_analysis = False | |
| 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 파일을 업로드해주세요.") | |
| # 메인 분석 로직 | |
| if 'run_analysis' in st.session_state and st.session_state.run_analysis: | |
| if 'text_column' in st.session_state and 'num_topics' in st.session_state and 'analysis_type' in st.session_state: | |
| try: | |
| if st.session_state.analysis_type == "전체 분석": | |
| st.header("전체 데이터 분석 결과") | |
| with st.spinner("토픽 모델링 실행 중..."): | |
| topic_results, lda, lda_output, tfidf_matrix, feature_names = perform_topic_modeling( | |
| df, st.session_state.text_column, st.session_state.num_topics, stop_words | |
| ) | |
| # 토픽 요약 표시 | |
| topic_summary = ", ".join([f"토픽{info['topic_num']}({info['topic_name']}, {info['weight']:.1f}%)" for info in topic_results]) | |
| st.markdown(f'<div class="topic-summary">{topic_summary}</div>', unsafe_allow_html=True) | |
| # 토픽별 상세 정보 표시 | |
| for idx, topic_info in enumerate(topic_results): | |
| st.subheader(f"토픽 {idx + 1}") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| df_lda = pd.DataFrame(list(zip(topic_info['lda_words'], lda.components_[idx][np.argsort(lda.components_[idx])[::-1][:10]])), | |
| columns=['단어', 'LDA 점수']) | |
| st.subheader("LDA 상위 단어") | |
| st.table(df_lda.style.format({'LDA 점수': '{:.4f}'})) | |
| with col2: | |
| df_tfidf = pd.DataFrame(list(zip(topic_info['tfidf_words'], | |
| 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]])), | |
| columns=['단어', 'TF-IDF']) | |
| st.subheader("TF-IDF 상위 단어") | |
| st.table(df_tfidf.style.format({'TF-IDF': '{:.4f}'})) | |
| # 토픽 비중 그래프 | |
| st.header("토픽 비중 그래프") | |
| df_weights = pd.DataFrame({ | |
| '토픽': [f'토픽 {i+1}' for i in range(st.session_state.num_topics)], | |
| '비중': [result['weight'] for result in topic_results] | |
| }) | |
| colors = generate_colors(st.session_state.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_column_width=True) | |
| st.download_button( | |
| label="네트워크 그래프 다운로드", | |
| data=img_bytes, | |
| file_name="topic_network_graph.png", | |
| mime="image/png", | |
| key="download_graph" | |
| ) | |
| # 토픽 요약 테이블 | |
| st.subheader("토픽 요약 테이블") | |
| 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) | |
| elif st.session_state.analysis_type == "조건부 분석": | |
| st.header("조건부 분석 결과") | |
| with st.spinner("조건부 토픽 모델링 실행 중..."): | |
| conditional_results = perform_conditional_analysis( | |
| df, st.session_state.condition_column, st.session_state.text_column, | |
| st.session_state.num_topics, stop_words, st.session_state.is_numeric, | |
| st.session_state.condition | |
| ) | |
| for value, topic_results in conditional_results.items(): | |
| st.subheader(f"{st.session_state.condition_column}: {value}") | |
| # 토픽 요약 표시 | |
| topic_summary = ", ".join([f"토픽{info['topic_num']}({info['topic_name']}, {info['weight']:.1f}%)" for info in topic_results]) | |
| st.markdown(f'<div class="topic-summary">{topic_summary}</div>', unsafe_allow_html=True) | |
| # 토픽 요약 테이블 | |
| 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) | |
| # 네트워크 그래프 | |
| st.subheader(f"{value} - 토픽 단어 네트워크 그래프") | |
| G = create_network_graph(topic_results, num_words=20) | |
| img_bytes = plot_network_graph(G) | |
| st.image(img_bytes, caption=f"{value} - 토픽별 상위 20개 단어 네트워크", use_column_width=True) | |
| st.download_button( | |
| label=f"{value} - 네트워크 그래프 다운로드", | |
| data=img_bytes, | |
| file_name=f"topic_network_graph_{value}.png", | |
| mime="image/png", | |
| key=f"download_graph_{value}" | |
| ) | |
| st.markdown("---") # 각 카테고리 결과 사이에 구분선 추가 | |
| # Claude API를 사용한 토픽 해석 부분 | |
| if api_key: | |
| st.header("토픽 종합 해석") | |
| def interpret_topics_full(api_key, topic_results): | |
| anthropic = 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: | |
| completion = anthropic.completions.create( | |
| model="claude-2.1", | |
| max_tokens_to_sample=4000, | |
| prompt=f"{HUMAN_PROMPT} {prompt} {AI_PROMPT}", | |
| ) | |
| return completion.completion | |
| except Exception as e: | |
| return f"Claude API 호출 중 오류가 발생했습니다: {str(e)}" | |
| def interpret_topics_conditional(api_key, topic_results, condition): | |
| anthropic = Anthropic(api_key=api_key) | |
| prompt = f"""다음은 LDA 토픽 모델링 결과로 나온 각 토픽의 정보입니다. 이를 바탕으로 각 토픽에 대해 간략히 요약해주세요: | |
| 조건: {condition} | |
| {", ".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. 토픽명은 "[구체적인 토픽명]" 형식으로 작성해주세요. 반드시 8어절 이상으로 구체적이고 설명적으로 작성해야 합니다. | |
| 2. 예시: "구성원의 전문성 향상을 위한 체계적인 학습과 역량 개발 방안 모색", "조직의 장기적 성과 향상을 위한 핵심 학습 역량 강화 전략", | |
| "현재 컬리지 멤버들의 역할 고민과 향후 발전 방향에 대한 논의" 등 | |
| 3. 토픽명은 단순히 단어를 나열하는 것이 아니라, 토픽의 핵심 주제나 의미를 잘 나타내는 구체적이고 설명적인 문구로 만들어주세요. | |
| 위 형식에 맞춰 각 토픽에 대해 간략히 요약해주세요. 사용자가 쉽게 복사하여 사용할 수 있도록 간결하고 명확하게 작성해주세요. | |
| """ | |
| try: | |
| completion = anthropic.completions.create( | |
| model="claude-2.1", | |
| max_tokens_to_sample=2000, | |
| prompt=f"{HUMAN_PROMPT} {prompt} {AI_PROMPT}", | |
| ) | |
| return completion.completion | |
| except Exception as e: | |
| return f"Claude API 호출 중 오류가 발생했습니다: {str(e)}" | |
| if st.session_state.analysis_type == "전체 분석": | |
| col1, col2 = st.columns([3, 1]) | |
| with col2: | |
| if st.button("토픽 다시 해석하기", key="reinterpret"): | |
| st.session_state.topic_interpretation = None | |
| with col1: | |
| if 'topic_interpretation' not in st.session_state or st.session_state.topic_interpretation is None: | |
| with st.spinner("토픽 해석 중..."): | |
| st.session_state.topic_interpretation = interpret_topics_full(api_key, topic_results) | |
| st.subheader("토픽 모델링 종합 결과") | |
| st.text_area("결과를 복사하여 사용하세요:", value=st.session_state.topic_interpretation, height=500) | |
| else: | |
| for value, topic_results in conditional_results.items(): | |
| st.subheader(f"{st.session_state.condition_column}: {value} - 토픽 해석") | |
| if st.button(f"{value} - 토픽 다시 해석하기", key=f"reinterpret_{value}"): | |
| st.session_state[f'topic_interpretation_{value}'] = None | |
| if f'topic_interpretation_{value}' not in st.session_state or st.session_state[f'topic_interpretation_{value}'] is None: | |
| with st.spinner(f"{value} 토픽 해석 중..."): | |
| st.session_state[f'topic_interpretation_{value}'] = interpret_topics_conditional(api_key, topic_results, condition=f"{st.session_state.condition_column}: {value}") | |
| st.text_area(f"{value} - 결과를 복사하여 사용하세요:", value=st.session_state[f'topic_interpretation_{value}'], height=300) | |
| else: | |
| st.warning("Claude API 키가 설정되지 않았습니다. https://console.anthropic.com/settings/keys 에 접속하여 API 키를 발급받으시면 토픽명과 해석을 제공받으실 수 있습니다.") | |
| except Exception as e: | |
| st.error(f"분석 중 오류가 발생했습니다: {str(e)}") | |
| else: | |
| st.error("유효한 데이터가 없습니다. CSV 파일을 다시 업로드해주세요.") | |
| # 분석 초기화 버튼 | |
| if st.button("분석 초기화"): | |
| for key in st.session_state.keys(): | |
| if key.startswith('topic_interpretation') or key in ['run_analysis', 'run_conditional_analysis', 'conditional_topic_results']: | |
| del st.session_state[key] | |
| st.experimental_rerun() | |
| st.markdown(""" | |
| --- | |
| © 2024 SK mySUNI 행복 College. All rights reserved. 문의사항이 있으시면 연락주세요 (배수정RF, soojeong.bae@sk.com) | |
| """) | |