import streamlit as st import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from scipy.stats import norm, skew import platform import os import matplotlib.font_manager as fm import warnings warnings.filterwarnings('ignore') # 심플한 한글 폰트 설정 - 앱 시작시 한번만 실행 def setup_korean_font(): """한글 폰트를 간단하게 설정하는 함수""" try: # 1. 사용자 폰트 파일 확인 script_dir = os.path.dirname(os.path.abspath(__file__)) possible_fonts = ["NanumGothic.ttf"] font_path = None for font_file in possible_fonts: candidate = os.path.join(script_dir, font_file) if os.path.exists(candidate): font_path = candidate break # 2. 폰트 적용 if font_path: # 폰트 파일이 있으면 직접 사용 plt.rcParams['font.family'] = fm.FontProperties(fname=font_path).get_name() st.sidebar.success(f"폰트 로딩 성공: {os.path.basename(font_path)}") else: # 시스템 기본 폰트 사용 if platform.system() == 'Windows': plt.rcParams['font.family'] = 'Malgun Gothic' elif platform.system() == 'Darwin': # macOS plt.rcParams['font.family'] = 'AppleGothic' else: # Linux plt.rcParams['font.family'] = 'DejaVu Sans' st.sidebar.info(f"시스템 기본 폰트 사용: {plt.rcParams['font.family']}") # 마이너스 기호 깨짐 방지 plt.rcParams['axes.unicode_minus'] = False return font_path except Exception as e: st.sidebar.warning(f"폰트 설정 오류: {e}") plt.rcParams['font.family'] = 'DejaVu Sans' plt.rcParams['axes.unicode_minus'] = False return None # 앱 시작시 폰트 설정 FONT_PATH = setup_korean_font() def analyze_scores(df): """데이터프레임을 받아 분석 결과를 표시하는 함수""" st.subheader("📋 데이터 미리보기 (상위 5개)") st.dataframe(df.head()) # 숫자 형식의 열만 선택지로 제공 numeric_columns = df.select_dtypes(include=np.number).columns.tolist() if not numeric_columns: st.error("❌ 데이터에서 분석 가능한 숫자 형식의 열을 찾을 수 없습니다.") return score_column = st.selectbox("📊 분석할 점수 열(column)을 선택하세요:", numeric_columns) if score_column: scores = df[score_column].dropna() if len(scores) == 0: st.error("❌ 선택한 열에 유효한 데이터가 없습니다.") return st.subheader(f"📈 '{score_column}' 점수 분포 분석 결과") # 1. 기본 통계량 st.write("#### 📊 기본 통계량") col1, col2, col3, col4 = st.columns(4) with col1: st.metric("평균", f"{scores.mean():.2f}") with col2: st.metric("표준편차", f"{scores.std():.2f}") with col3: st.metric("최솟값", f"{scores.min():.2f}") with col4: st.metric("최댓값", f"{scores.max():.2f}") # 상세 통계 st.write("#### 📋 상세 통계량") st.dataframe(scores.describe().to_frame().T) # 2. 분포 시각화 st.write("#### 🎨 점수 분포 시각화") try: # 한글 폰트 준비 if FONT_PATH: font_prop = fm.FontProperties(fname=FONT_PATH) else: font_prop = fm.FontProperties(family=plt.rcParams['font.family']) fig, ax = plt.subplots(figsize=(12, 7)) # 히스토그램과 KDE 곡선 sns.histplot(scores, kde=True, stat='density', alpha=0.7, ax=ax, color='skyblue') # 정규분포 곡선 추가 mu, std = norm.fit(scores) x = np.linspace(scores.min(), scores.max(), 100) y = norm.pdf(x, mu, std) ax.plot(x, y, 'r-', linewidth=2, label=f'정규분포 (μ={mu:.1f}, σ={std:.1f})') # 평균선 ax.axvline(mu, color='red', linestyle=':', linewidth=2, alpha=0.8, label=f'평균: {mu:.1f}') # 제목과 라벨 - 한글 폰트 직접 지정 ax.set_title(f'{score_column} 점수 분포 분석', fontproperties=font_prop, fontsize=16, pad=20) ax.set_xlabel('점수', fontproperties=font_prop, fontsize=12) ax.set_ylabel('밀도', fontproperties=font_prop, fontsize=12) # 범례 - 한글 폰트 적용 legend = ax.legend(prop=font_prop, fontsize=10) ax.grid(True, alpha=0.3) # 통계 정보 박스 - 한글 폰트 적용 stats_text = f'샘플 수: {len(scores)}\n평균: {mu:.2f}\n표준편차: {std:.2f}\n최솟값: {scores.min():.1f}\n최댓값: {scores.max():.1f}' ax.text(0.02, 0.98, stats_text, transform=ax.transAxes, fontproperties=font_prop, fontsize=10, verticalalignment='top', bbox=dict(boxstyle='round,pad=0.5', facecolor='lightblue', alpha=0.8)) plt.tight_layout() st.pyplot(fig) except Exception as e: st.error(f"❌ 그래프 생성 오류: {e}") # 대체 그래프 (영어만 사용) st.write("**Simple Chart (English):**") fig2, ax2 = plt.subplots(figsize=(10, 6)) ax2.hist(scores, bins=15, alpha=0.7, color='lightcoral', edgecolor='black') ax2.set_title(f'Distribution of {score_column}', fontsize=14) ax2.set_xlabel('Score') ax2.set_ylabel('Frequency') ax2.grid(True, alpha=0.3) st.pyplot(fig2) plt.close(fig2) finally: if 'fig' in locals(): plt.close(fig) # 3. 왜도 분석 st.write("#### 📐 분포 형태 분석 (왜도)") try: skewness = skew(scores) col1, col2 = st.columns([1, 2]) with col1: st.metric("왜도 (Skewness)", f"{skewness:.4f}") with col2: if skewness > 0.5: st.success("🔴 **양의 왜도**: 대부분 학생이 낮은 점수대에 분포, 소수의 고득점자 존재") elif skewness < -0.5: st.success("🔵 **음의 왜도**: 대부분 학생이 높은 점수대에 분포, 소수의 저득점자 존재") else: st.success("🟢 **대칭 분포**: 점수가 평균을 중심으로 고르게 분포") except Exception as e: st.error(f"왜도 계산 오류: {e}") # 4. 구간별 분포 st.write("#### 📋 구간별 분포") try: if scores.max() <= 100: # 100점 만점 가정 bins_labels = ['0-60', '61-70', '71-80', '81-90', '91-100'] bins_edges = [0, 60, 70, 80, 90, 100] else: # 동적 구간 생성 min_score, max_score = scores.min(), scores.max() interval = (max_score - min_score) / 5 bins_edges = [min_score + i * interval for i in range(6)] bins_labels = [f'{bins_edges[i]:.0f}-{bins_edges[i+1]:.0f}' for i in range(5)] score_counts = pd.cut(scores, bins=bins_edges, labels=bins_labels, include_lowest=True).value_counts().sort_index() score_percentages = (score_counts / len(scores) * 100).round(1) result_df = pd.DataFrame({ '구간': score_counts.index, '학생 수': score_counts.values, '비율 (%)': score_percentages.values }) st.dataframe(result_df) except Exception as e: st.warning(f"구간 분석 오류: {e}") def main(): st.set_page_config( page_title="학생 점수 분석 도구", page_icon="📊", layout="wide" ) # 제목 st.title("📊 학생 점수 분포 분석 도구") st.markdown("**CSV 파일을 업로드하거나 Google Sheets URL을 입력하여 점수 분포를 분석하세요**") # 폰트 정보 표시 with st.expander("🔧 폰트 설정 정보"): st.write(f"**현재 폰트**: {plt.rcParams['font.family']}") st.write(f"**폰트 경로**: {FONT_PATH if FONT_PATH else '시스템 기본'}") # 간단한 폰트 테스트 if st.button("폰트 테스트"): fig, ax = plt.subplots(figsize=(6, 2)) ax.text(0.5, 0.5, '한글 폰트 테스트: 점수 분포 분석', ha='center', va='center', fontsize=14) ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.axis('off') st.pyplot(fig) plt.close(fig) st.markdown("---") # 사이드바 - 데이터 입력 st.sidebar.title("📁 데이터 가져오기") source_option = st.sidebar.radio( "데이터 소스 선택:", ("📤 CSV 파일 업로드", "🔗 Google Sheets URL", "🎲 샘플 데이터") ) df = None if source_option == "📤 CSV 파일 업로드": uploaded_file = st.sidebar.file_uploader( "CSV 파일을 선택하세요", type=["csv"], help="UTF-8, CP949 등 다양한 인코딩을 자동으로 감지합니다" ) if uploaded_file: encodings = ['utf-8-sig', 'utf-8', 'cp949', 'euc-kr', 'latin1'] for encoding in encodings: try: df = pd.read_csv(uploaded_file, encoding=encoding) st.sidebar.success(f"✅ 파일 로딩 성공! (인코딩: {encoding})") break except UnicodeDecodeError: continue except Exception as e: st.sidebar.error(f"파일 읽기 오류: {e}") break if df is None: st.sidebar.error("❌ 파일 인코딩을 인식할 수 없습니다.") elif source_option == "🔗 Google Sheets URL": st.sidebar.info("💡 Google Sheets를 '웹에 게시'한 후 CSV URL을 입력하세요") url = st.sidebar.text_input( "Google Sheets CSV URL", placeholder="https://docs.google.com/spreadsheets/d/..." ) if url and "docs.google.com" in url: try: with st.spinner("📥 데이터 로딩 중..."): df = pd.read_csv(url) st.sidebar.success("✅ Google Sheets 로딩 성공!") except Exception as e: st.sidebar.error(f"❌ URL 로딩 실패: {e}") elif url: st.sidebar.warning("⚠️ 올바른 Google Sheets URL을 입력하세요") elif source_option == "🎲 샘플 데이터": if st.sidebar.button("샘플 데이터 생성"): np.random.seed(42) sample_size = st.sidebar.slider("샘플 크기", 50, 500, 100) df = pd.DataFrame({ '학생번호': range(1, sample_size + 1), '수학점수': np.random.normal(75, 15, sample_size).clip(0, 100).round(1), '영어점수': np.random.normal(80, 12, sample_size).clip(0, 100).round(1), '과학점수': np.random.normal(70, 18, sample_size).clip(0, 100).round(1), '국어점수': np.random.normal(77, 14, sample_size).clip(0, 100).round(1) }) st.sidebar.success(f"✅ {sample_size}명의 샘플 데이터 생성!") # 메인 분석 if df is not None and not df.empty: st.success(f"🎉 데이터 로딩 완료! **{len(df)}개 행, {len(df.columns)}개 열**") analyze_scores(df) else: st.info("👈 **사이드바에서 데이터를 선택하세요**") # 기능 안내 st.markdown(""" ### 🔍 주요 기능 - **📊 기본 통계**: 평균, 표준편차, 최솟값, 최댓값 등 - **📈 분포 시각화**: 히스토그램, KDE 곡선, 정규분포 비교 - **📐 왜도 분석**: 분포의 비대칭성 측정 - **📋 구간별 분포**: 점수 구간별 학생 수 및 비율 ### 📝 지원 형식 - **CSV 파일**: UTF-8, CP949, EUC-KR 등 자동 인코딩 감지 - **Google Sheets**: 웹에 게시된 시트의 CSV URL - **샘플 데이터**: 테스트용 가상 점수 데이터 """) if __name__ == '__main__': main()