import streamlit as st import pandas as pd import numpy as np import plotly.express as px import plotly.graph_objects as go from io import StringIO import openpyxl import matplotlib.font_manager as fm from scipy import stats import os import plotly.figure_factory as ff #사이즈 크게 st.set_page_config(layout="wide") # 한글 폰트 설정 def set_font(): font_path = "Pretendard-Bold.ttf" # 실제 폰트 파일 경로로 변경해주세요 fm.fontManager.addfont(font_path) return {'font.family': 'Pretendard-Bold', 'axes.unicode_minus': False} # 폰트 설정을 가져옵니다 font_settings = set_font() # 세션 상태 초기화 및 관리 def manage_session_state(): if 'data' not in st.session_state: st.session_state.data = None if 'processed_data' not in st.session_state: st.session_state.processed_data = None if 'numeric_columns' not in st.session_state: st.session_state.numeric_columns = [] if 'categorical_columns' not in st.session_state: st.session_state.categorical_columns = [] if 'x_var' not in st.session_state: st.session_state.x_var = None if 'y_var' not in st.session_state: st.session_state.y_var = None if 'slicers' not in st.session_state: st.session_state.slicers = {} if 'analysis_performed' not in st.session_state: st.session_state.analysis_performed = False if 'filtered_data' not in st.session_state: st.session_state.filtered_data = None def reset_session_state(): # 세션 상태 초기화 st.session_state.data = None st.session_state.processed_data = None st.session_state.filtered_data = None st.session_state.numeric_columns = [] st.session_state.categorical_columns = [] st.session_state.x_var = None st.session_state.y_var = None st.session_state.slicers = {} st.session_state.analysis_performed = False SAMPLE_DATA_FILES = [ {"name": "과목별 노력과 성취도", "file": "subject.xlsx"}, {"name": "채점", "file": "score.xlsx"}, {"name": "출석일수와 성적", "file": "attendance.xlsx"} ] def load_sample_data(file_name): # 예시 데이터 파일 경로 file_path = os.path.join("sample_data", file_name) if file_name.endswith('.csv'): return pd.read_csv(file_path) elif file_name.endswith(('.xls', '.xlsx')): return pd.read_excel(file_path) else: st.error("지원되지 않는 파일 형식입니다.") return None # 데이터 로드 @st.cache_data def load_data(file): file_extension = file.name.split('.')[-1].lower() if file_extension == 'csv': data = pd.read_csv(file) elif file_extension in ['xls', 'xlsx']: data = pd.read_excel(file) else: st.error("지원되지 않는 파일 형식입니다. CSV, XLS, 또는 XLSX 파일을 업로드해주세요.") return None # 빈 열 이름에 기본값 부여 if data.columns.isnull().any(): data.columns = [f'Column_{i+1}' if pd.isnull(col) else col for i, col in enumerate(data.columns)] return data def manual_data_entry(): col_names = st.text_input("열 이름을 쉼표로 구분하여 입력하세요:", key="manual_col_names").split(',') col_names = [name.strip() for name in col_names if name.strip()] if col_names: num_rows = st.number_input("초기 행의 수를 입력하세요:", min_value=1, value=5, key="manual_num_rows") data = pd.DataFrame(columns=col_names, index=range(num_rows)) edited_data = st.data_editor(data, num_rows="dynamic", key="manual_data_editor") return edited_data return None def preprocess_data(data): # 데이터 타입 추론 및 변환 for column in data.columns: if data[column].dtype == 'object': try: # NaN 값을 무시하고 숫자로 변환 시도 numeric_converted = pd.to_numeric(data[column], errors='coerce') # 모든 값이 NaN이 아니라면 변환된 열을 사용 if not numeric_converted.isna().all(): data[column] = numeric_converted st.write(f"'{column}' 열을 숫자형으로 변환했습니다.") except: st.write(f"'{column}' 열은 범주형으로 유지됩니다.") # 결측치 처리 (기존 코드 유지) if data.isnull().sum().sum() > 0: st.write("결측치 처리:") for column in data.columns: if data[column].isnull().sum() > 0: method = st.selectbox(f"{column} 열의 처리 방법 선택:", ["제거", "평균으로 대체", "중앙값으로 대체", "최빈값으로 대체"], key=f"missing_{column}") if method == "제거": data = data.dropna(subset=[column]) elif method == "평균으로 대체": if pd.api.types.is_numeric_dtype(data[column]): data[column].fillna(data[column].mean(), inplace=True) else: st.warning(f"{column} 열은 숫자형이 아니어서 평균값으로 대체할 수 없습니다.") elif method == "중앙값으로 대체": if pd.api.types.is_numeric_dtype(data[column]): data[column].fillna(data[column].median(), inplace=True) else: st.warning(f"{column} 열은 숫자형이 아니어서 중앙값으로 대체할 수 없습니다.") elif method == "최빈값으로 대체": data[column].fillna(data[column].mode()[0], inplace=True) # 숫자형 열과 범주형 열 분리 st.session_state.numeric_columns = data.select_dtypes(include=['float64', 'int64']).columns.tolist() st.session_state.categorical_columns = data.select_dtypes(exclude=['float64', 'int64']).columns.tolist() return data def update_filtered_data(): st.session_state.filtered_data = apply_slicers(st.session_state.processed_data) def create_slicers(data): for col in st.session_state.categorical_columns: if col in data.columns and data[col].nunique() <= 10: st.session_state.slicers[col] = st.multiselect( f"{col} 선택", options=sorted(data[col].unique()), default=sorted(data[col].unique()), key=f"slicer_{col}", on_change=update_filtered_data ) def apply_slicers(data): filtered_data = data.copy() for col, selected_values in st.session_state.slicers.items(): if col in filtered_data.columns and selected_values: filtered_data = filtered_data[filtered_data[col].isin(selected_values)] return filtered_data def plot_correlation_heatmap(data): numeric_data = data[st.session_state.numeric_columns] if not numeric_data.empty: corr = numeric_data.corr() fig = px.imshow(corr, color_continuous_scale='RdBu_r', zmin=-1, zmax=1) fig.update_layout(title='상관관계 히트맵') st.plotly_chart(fig) else: st.warning("상관관계 히트맵을 그릴 수 있는 숫자형 열이 없습니다.") def check_normality(data, column): # 시각적 검사: Q-Q plot fig = go.Figure() qq = stats.probplot(data[column], dist="norm") fig.add_trace(go.Scatter(x=qq[0][0], y=qq[0][1], mode='markers', name='Sample Quantiles')) fig.add_trace(go.Scatter(x=qq[0][0], y=qq[1][0] * qq[0][0] + qq[1][1], mode='lines', name='Theoretical Quantiles')) fig.update_layout(title=f'Q-Q Plot for {column}', xaxis_title='Theoretical Quantiles', yaxis_title='Sample Quantiles') st.plotly_chart(fig) # 통계적 검사: Shapiro-Wilk test stat, p = stats.shapiro(data[column]) st.write(f"Shapiro-Wilk Test for {column}:") st.write(f"통계량: {stat:.4f}") st.write(f"p-value: {p:.4f}") if p > 0.05: st.write("데이터가 정규 분포를 따르는 것으로 보입니다 (귀무가설을 기각하지 못함)") else: st.write("데이터가 정규 분포를 따르지 않는 것으로 보입니다 (귀무가설 기각)") def perform_independent_ttest(data, group_column, value_column): groups = data[group_column].unique() if len(groups) != 2: st.error("독립 표본 t-검정은 정확히 두 그룹이 필요합니다.") return group1 = data[data[group_column] == groups[0]][value_column] group2 = data[data[group_column] == groups[1]][value_column] t_stat, p_value = stats.ttest_ind(group1, group2) st.write(f"독립 표본 T-검정 결과 ({group_column} 기준, {value_column} 비교):") st.write(f"그룹: {groups[0]} vs {groups[1]}") st.write(f"t-통계량: {t_stat:.4f}") st.write(f"p-value: {p_value:.4f}") if p_value < 0.05: st.write("두 그룹 간에 통계적으로 유의한 차이가 있습니다.") else: st.write("두 그룹 간에 통계적으로 유의한 차이가 없습니다.") def perform_paired_ttest(data, column1, column2): if len(data[column1]) != len(data[column2]): st.error("대응 표본 t-검정을 위해서는 두 열의 데이터 수가 같아야 합니다.") return t_stat, p_value = stats.ttest_rel(data[column1], data[column2]) st.write(f"대응 표본 T-검정 결과 ({column1} vs {column2}):") st.write(f"t-통계량: {t_stat:.4f}") st.write(f"p-value: {p_value:.4f}") if p_value < 0.05: st.write(f"{column1}과 {column2} 간에 통계적으로 유의한 차이가 있습니다.") else: st.write(f"{column1}과 {column2} 간에 통계적으로 유의한 차이가 없습니다.") def perform_onesample_ttest(data, column, test_value): t_stat, p_value = stats.ttest_1samp(data[column], test_value) st.write(f"단일 표본 T-검정 결과:") st.write(f"t-통계량: {t_stat:.4f}") st.write(f"p-value: {p_value:.4f}") if p_value < 0.05: st.write(f"표본 평균이 {test_value}와 유의하게 다릅니다.") else: st.write(f"표본 평균이 {test_value}와 유의하게 다르지 않습니다.") def plot_scatter_with_regression(data, x_var, y_var): # 회귀 분석 수행 x = data[x_var] y = data[y_var] slope, intercept, r_value, p_value, std_err = stats.linregress(x, y) # 예측값 계산 y_pred = slope * x + intercept # 잔차 계산 residuals = y - y_pred # 그래프 생성 fig = go.Figure() # 산점도 추가 (오차 막대 포함) fig.add_trace(go.Scatter( x=x, y=y, mode='markers', name='Data Points', marker=dict(color='rgba(0, 0, 255, 0.7)', size=10), error_y=dict( type='data', array=abs(residuals), visible=True, color='rgba(0, 0, 0, 0.1)', thickness=0.5, width=0 ) )) # 회귀선 추가 fig.add_trace(go.Scatter( x=x, y=y_pred, mode='lines', name='Regression Line', line=dict(color='red', width=2) )) # 레이아웃 설정 r_squared = r_value ** 2 fig.update_layout( title=f'{x_var}와 {y_var}의 관계 (R-squared: {r_squared:.3f})', xaxis_title=x_var, yaxis_title=y_var, showlegend=True, annotations=[ dict( x=0.05, y=0.95, xref='paper', yref='paper', text=f'y = {slope:.2f}x + {intercept:.2f}
R² = {r_squared:.3f}', showarrow=False, bgcolor='rgba(255, 255, 255, 0.8)', bordercolor='rgba(0, 0, 0, 0.3)', borderwidth=1 ) ] ) st.plotly_chart(fig) # 추가 통계 정보 st.write(f"상관계수: {r_value:.4f}") st.write(f"p-value: {p_value:.4f}") st.write(f"표준 오차: {std_err:.4f}") def get_active_slicers(): return {col: values for col, values in st.session_state.slicers.items() if values} def perform_independent_ttest(data, group_column, group1, group2, value_column): group1_data = data[data[group_column] == group1][value_column] group2_data = data[data[group_column] == group2][value_column] t_stat, p_value = stats.ttest_ind(group1_data, group2_data) st.write(f"독립 표본 T-검정 결과 ({group_column}: {group1} vs {group2}, {value_column} 비교):") st.write(f"t-통계량: {t_stat:.4f}") st.write(f"p-value: {p_value:.4f}") if p_value < 0.05: st.write(f"{group1}과 {group2} 간에 통계적으로 유의한 차이가 있습니다.") else: st.write(f"{group1}과 {group2} 간에 통계적으로 유의한 차이가 없습니다.") def perform_analysis(): if st.session_state.filtered_data is None: st.session_state.filtered_data = st.session_state.processed_data.copy() st.header("탐색적 데이터 분석") # 슬라이서 생성 create_slicers(st.session_state.processed_data) # 데이터가 변경될 때마다 필터링된 데이터 업데이트 st.session_state.filtered_data = apply_slicers(st.session_state.processed_data) # 3열 레이아웃 생성 col1, col2, col3 = st.columns(3) with col1: # 요약 통계 st.write("요약 통계:") st.write(st.session_state.filtered_data.describe()) # 상관관계 히트맵 st.subheader("상관관계 히트맵") plot_correlation_heatmap(st.session_state.filtered_data) with col2: # 사용자가 선택한 두 변수에 대한 산점도 및 회귀 분석 st.subheader("두 변수 간의 관계 분석") x_var = st.selectbox("X축 변수 선택", options=st.session_state.numeric_columns, key='x_var') y_var = st.selectbox("Y축 변수 선택", options=[col for col in st.session_state.numeric_columns if col != x_var], key='y_var') if x_var and y_var: plot_scatter_with_regression(st.session_state.filtered_data, x_var, y_var) with col3: st.subheader("통계적 검정") # 정규성 검정 st.write("정규성 검정") normality_column = st.selectbox("정규성 검정을 수행할 열 선택:", st.session_state.numeric_columns, key='normality_column') if st.button("정규성 검정 수행"): check_normality(st.session_state.filtered_data, normality_column) # T-검정 st.write("T-검정") test_type = st.radio("T-검정 유형 선택:", ["독립 표본", "대응 표본", "단일 표본"], key="test_type_radio") if test_type == "독립 표본": active_slicers = get_active_slicers() if active_slicers: group_column = st.selectbox("그룹 구분을 위한 열 선택:", options=list(active_slicers.keys())) available_groups = active_slicers[group_column] group1 = st.selectbox("첫 번째 그룹 선택:", options=available_groups, key="group1") group2 = st.selectbox("두 번째 그룹 선택:", options=[g for g in available_groups if g != group1], key="group2") value_column = st.selectbox("비교할 값이 있는 열 선택:", st.session_state.numeric_columns) if st.button("독립 표본 T-검정 수행"): if group1 and group2: perform_independent_ttest(st.session_state.filtered_data, group_column, group1, group2, value_column) else: st.error("두 개의 서로 다른 그룹을 선택해주세요.") else: st.warning("활성화된 슬라이서가 없습니다. 먼저 슬라이서에서 그룹을 선택해주세요.") elif test_type == "대응 표본": column1 = st.selectbox("첫 번째 열 선택:", st.session_state.numeric_columns, key="paired_col1") column2 = st.selectbox("두 번째 열 선택:", [col for col in st.session_state.numeric_columns if col != column1], key="paired_col2") if st.button("대응 표본 T-검정 수행"): perform_paired_ttest(st.session_state.filtered_data, column1, column2) elif test_type == "단일 표본": test_column = st.selectbox("검정할 열 선택:", st.session_state.numeric_columns, key="one_sample_col") test_value = st.number_input("검정 값 입력:", key="one_sample_value") if st.button("단일 표본 T-검정 수행"): perform_onesample_ttest(st.session_state.filtered_data, test_column, test_value) # '다른 데이터 분석하기' 버튼 추가 if st.button("다른 데이터 분석하기(오류가 나면 다시 눌러주세요)"): reset_session_state() st.experimental_rerun() ## 메인 def main(): st.title("모두가 할 수 있는 데이터 분석 툴킷 Data Analysis for Everyone") st.link_button("만든이 코난쌤", "https://www.youtube.com/@conanssam") manage_session_state() if st.session_state.data is None: data_input_method = st.radio("데이터 입력 방법 선택:", ("파일 업로드", "예시 데이터 사용", "수동 입력"), key="data_input_method") if data_input_method == "파일 업로드": uploaded_file = st.file_uploader("CSV, XLS, 또는 XLSX 파일을 선택하세요", type=["csv", "xls", "xlsx"], key="file_uploader") if uploaded_file is not None: st.session_state.data = load_data(uploaded_file) elif data_input_method == "예시 데이터 사용": sample_choice = st.selectbox( "예시 데이터 선택", options=[sample["name"] for sample in SAMPLE_DATA_FILES], format_func=lambda x: x ) if st.button("선택한 예시 데이터 로드"): selected_file = next(sample["file"] for sample in SAMPLE_DATA_FILES if sample["name"] == sample_choice) st.session_state.data = load_sample_data(selected_file) else: st.session_state.data = manual_data_entry() if st.session_state.data is not None: st.subheader("데이터 미리보기 및 수정") st.write("데이터를 확인하고 필요한 경우 수정하세요:") edited_data = st.data_editor( st.session_state.data, num_rows="dynamic", key="main_data_editor" ) if st.button("데이터 분석 시작", key="start_analysis") or st.session_state.analysis_performed: st.session_state.processed_data = preprocess_data(edited_data) st.session_state.analysis_performed = True if st.session_state.analysis_performed: perform_analysis() if __name__ == "__main__": main()