Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import io | |
| import time | |
| import string | |
| import random | |
| import matplotlib.backends.backend_pdf | |
| import seaborn as sns | |
| from itertools import combinations | |
| from multi_facet_rasch_model import MultiFacetRaschModel | |
| from matplotlib import font_manager, rc | |
| from scipy import stats | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| # 폰트 설정 | |
| font_path = './NanumBarunGothic.ttf' | |
| font_manager.fontManager.addfont(font_path) | |
| font_name = font_manager.FontProperties(fname=font_path).get_name() | |
| rc('font', family=font_name) | |
| plt.rcParams['axes.unicode_minus'] = False | |
| def plot_facet_params(facet_name, params, labels): | |
| fig, ax = plt.subplots(figsize=(10, 6)) | |
| bars = ax.bar(range(len(params)), params, color='#4E79A7', edgecolor='none') | |
| ax.set_xlabel(facet_name, fontsize=14, fontweight='bold') | |
| ax.set_ylabel("파라미터 값", fontsize=14, fontweight='bold') | |
| ax.set_title(f"{facet_name} 파라미터", fontsize=16, fontweight='bold') | |
| ax.tick_params(axis='both', which='major', labelsize=12) | |
| # x축 레이블 설정 | |
| ax.set_xticks(range(len(params))) | |
| ax.set_xticklabels(labels, rotation=45, ha='right') | |
| for rect in bars: | |
| height = rect.get_height() | |
| ax.text(rect.get_x() + rect.get_width() / 2., height, | |
| f'{height:.2f}', | |
| ha='center', va='bottom', fontsize=10) | |
| plt.tight_layout() | |
| return fig | |
| def generate_sample_data(n_examinees=50, n_items=20, n_raters=5): | |
| np.random.seed(42) | |
| n_observations = n_examinees * n_items | |
| data = { | |
| 'examinee_id': np.repeat(range(1, n_examinees + 1), n_items), | |
| 'item_id': np.tile(range(1, n_items + 1), n_examinees), | |
| 'rater_id': np.random.randint(1, n_raters + 1, n_observations), | |
| 'score': np.random.randint(0, 5, n_observations) | |
| } | |
| return pd.DataFrame(data) | |
| def generate_unique_id(length=8): | |
| """영문과 숫자의 조합으로 고유 ID를 생성합니다.""" | |
| characters = string.ascii_letters + string.digits | |
| return ''.join(random.choice(characters) for _ in range(length)) | |
| def calculate_residuals(model, data, score_column): | |
| """잔차 계산""" | |
| try: | |
| predictions = model.predict(data) | |
| residuals = data[score_column] - predictions | |
| return residuals | |
| except AttributeError: | |
| st.error("모델에서 예측 기능을 사용할 수 없습니다.") | |
| return None | |
| def plot_residuals(residuals, facet1, facet2): | |
| """잔차 시각화""" | |
| plt.figure(figsize=(10, 6)) | |
| sns.boxplot(x=residuals[facet1], y=residuals[facet2]) | |
| plt.title(f'Residuals by {facet1} and {facet2} Combinations') | |
| plt.xlabel(facet1) | |
| plt.ylabel(facet2) | |
| st.pyplot(plt) | |
| def pattern_detection_plot(data, facet1, facet2, score_column): | |
| """상호작용 패턴 시각화""" | |
| plt.figure(figsize=(10, 6)) | |
| sns.violinplot(x=data[facet1], y=data[score_column], hue=data[facet2], split=True) | |
| plt.title(f'Interaction Pattern Between {facet1} and {facet2}') | |
| plt.xlabel(facet1) | |
| plt.ylabel(score_column) | |
| st.pyplot(plt) | |
| def plot_interaction_effects(data, facet1, facet2, score_column): | |
| # 박스플롯 | |
| fig1 = px.box(data, x=facet1, y=score_column, color=facet2, | |
| title=f'{facet1}와 {facet2}의 상호작용 효과') | |
| # 히트맵 | |
| pivot = data.pivot_table(values=score_column, index=facet1, columns=facet2, aggfunc='mean') | |
| fig2 = px.imshow(pivot, title=f'{facet1}와 {facet2}의 평균 점수 히트맵') | |
| # 두 그래프를 수직으로 배치 | |
| fig = make_subplots(rows=2, cols=1, subplot_titles=("박스플롯", "히트맵")) | |
| for trace in fig1.data: | |
| fig.add_trace(trace, row=1, col=1) | |
| fig.add_trace(fig2.data[0], row=2, col=1) | |
| fig.update_layout(height=1000, width=800, title_text=f"{facet1}와 {facet2}의 상호작용 효과 분석") | |
| return fig | |
| def set_page_style(): | |
| st.markdown( | |
| """ | |
| <style> | |
| body, h1, h2, h3, p, label, div { | |
| font-family: 'Noto Sans KR', sans-serif !important; | |
| color: black !important; | |
| } | |
| .stButton>button { | |
| background-color: #cce7ff !important; | |
| color: black !important; | |
| border-radius: 4px !important; | |
| padding: 10px 24px; | |
| font-size: 16px; | |
| text-align: center; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .stButton>button:hover { | |
| background-color: #b3d4ff !important; | |
| } | |
| .sidebar .stButton>button { | |
| width: 100% !important; | |
| margin-bottom: 10px !important; | |
| border: none !important; | |
| } | |
| .sidebar .sidebar-content { | |
| padding: 10px; | |
| } | |
| .stDataFrame { | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| } | |
| .css-1e5imcs { | |
| background-color: #ffffff !important; | |
| padding: 10px; | |
| } | |
| footer { | |
| position: fixed; | |
| left: 0; | |
| bottom: 0; | |
| width: 100%; | |
| background-color: #e8f4f8; /* 연한 하늘색 배경 */ | |
| color: #34495e; /* 진한 남색 텍스트 */ | |
| text-align: center; | |
| padding: 10px 0; | |
| font-size: 14px; | |
| border-top: 1px solid #bdd7e7; /* 약간 더 진한 하늘색 테두리 */ | |
| z-index: 1000; | |
| box-shadow: 0 -2px 5px rgba(0,0,0,0.05); /* 부드러운 그림자 */ | |
| } | |
| footer a { | |
| color: #2980b9; /* 링크 색상 */ | |
| text-decoration: none; | |
| transition: color 0.3s ease; /* 부드러운 색상 전환 효과 */ | |
| } | |
| footer a:hover { | |
| color: #3498db; /* 호버 시 색상 변경 */ | |
| text-decoration: underline; | |
| } | |
| .main .block-container { | |
| padding-bottom: 70px; /* 푸터 높이에 맞춰 여백 조정 */ | |
| } | |
| .info-box { | |
| background-color: #e7f3fe; | |
| border-left: 6px solid #2196F3; | |
| margin-bottom: 15px; | |
| padding: 4px 12px; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| def data_analysis_page(): | |
| st.markdown("<h1 style='text-align: center;'>📊 다국면 라쉬모형 분석</h1>", unsafe_allow_html=True) | |
| st.markdown("<h2 style='text-align: center; color: #6c757d;'>(Multi-Facet Rasch Model Analysis)</h2>", unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.header("1. 데이터 준비") | |
| st.info(""" | |
| 📌 분석을 시작하기에 앞서 | |
| 샘플 데이터를 확인하고, 자신의 데이터를 업로드하세요. | |
| **국면(Facet)** 은 난이도/능력/엄격성 등을 확인하고자 하는 대상입니다. | |
| 실제 피평가자, 평가자, 문항, 과제세트, 시계열 시점, 환경적 요소, 부서 등 조직정보 등 | |
| 국면이 될 수 있습니다. | |
| 국면(Facet) 열이 문자열인 경우, 문항들을 묶는 중분류(요인)별로 국면을 만들고자 하는 경우 | |
| '🔄 데이터 변환' 에서 데이터를 정리한 후 아래에 데이터를 업로드해주세요. | |
| """) | |
| st.subheader("1.1 샘플 데이터 생성") | |
| if st.button("샘플 데이터 생성"): | |
| sample_data = generate_sample_data() | |
| st.session_state['data'] = sample_data | |
| st.success("샘플 데이터가 생성되었습니다!") | |
| st.write(sample_data.head()) | |
| st.info(""" | |
| 샘플 데이터 안내: | |
| - **examinee ID** : 첫번째 국면입니다. | |
| - **item ID** : 두번째 국면입니다. | |
| - **rator ID** : 세번째 국면입니다. | |
| - **score** : 점수에 해당합니다. | |
| 문항 혹은 문항들의 집합인 중분류(요인)을 국면으로 넣으려면 '🔄 데이터 변환'페이지를 활용하세요. | |
| """) | |
| csv = sample_data.to_csv(index=False) | |
| st.download_button( | |
| label="샘플 데이터 다운로드 (CSV)", | |
| data=csv, | |
| file_name="mfrm_sample_data.csv", | |
| mime="text/csv" | |
| ) | |
| st.markdown("---") | |
| st.subheader("1.2 데이터 파일 업로드") | |
| st.info(""" | |
| 데이터 형식 안내: | |
| - 모든 국면 열과 문항별 점수(score) 열은 숫자 데이터여야 합니다. | |
| - 오류가 발생하면, 위쪽 샘플 데이터를 살펴보고 '🔄 데이터 변환'페이지를 활용하세요. | |
| - 결측치가 있는 경우 분석에서 제외됩니다. | |
| """) | |
| uploaded_file = st.file_uploader("CSV 파일 업로드", type="csv") | |
| if uploaded_file is not None: | |
| data = pd.read_csv(uploaded_file) | |
| st.session_state['data'] = data | |
| st.success("데이터가 성공적으로 업로드되었습니다!") | |
| st.write(data.head()) | |
| if 'data' in st.session_state: | |
| st.markdown("---") | |
| st.header("2. 분석 설정") | |
| columns = st.session_state['data'].columns.tolist() | |
| n_facets = st.slider("분석할 국면 수 선택", min_value=2, max_value=5, value=3) | |
| facets = [] | |
| for i in range(n_facets): | |
| facet = st.selectbox(f"국면 {i+1} 선택", options=columns, key=f"facet_{i}") | |
| facets.append(facet) | |
| score_column = st.selectbox("점수 열 선택", options=columns) | |
| if st.button("분석 실행", key="run_analysis"): | |
| with st.spinner('분석을 실행 중입니다. 잠시만 기다려주세요...'): | |
| # 데이터 타입 확인 및 변환 | |
| for col in facets + [score_column]: | |
| if not pd.api.types.is_numeric_dtype(st.session_state['data'][col]): | |
| st.error(f"{col} 열이 숫자 형식이 아닙니다. '🔄 데이터 변환' 페이지에서 데이터를 변환해 주세요.") | |
| return | |
| model_data = st.session_state['data'][facets + [score_column]].values | |
| model = MultiFacetRaschModel(*(len(st.session_state['data'][facet].unique()) for facet in facets)) | |
| progress_bar = st.progress(0) | |
| for i in range(100): | |
| time.sleep(0.05) | |
| progress_bar.progress(i + 1) | |
| try: | |
| model.fit(model_data) | |
| except Exception as e: | |
| st.error(f"모델 학습 중 오류가 발생했습니다: {str(e)}") | |
| return | |
| st.success('분석이 완료되었습니다!') | |
| st.markdown("---") | |
| st.header("3. 분석 결과") | |
| params = model.get_parameters() | |
| results_df = pd.DataFrame() | |
| for i, facet in enumerate(facets): | |
| facet_params = params[f'facet_{i}_params'] | |
| facet_df = pd.DataFrame({ | |
| '국면': facet, | |
| '레벨': range(len(facet_params)), | |
| '파라미터 값': facet_params | |
| }) | |
| results_df = pd.concat([results_df, facet_df], ignore_index=True) | |
| st.subheader("결과 테이블") | |
| st.dataframe(results_df.style.highlight_max(axis=0, subset=['파라미터 값'], color='#90EE90')) | |
| st.subheader("결과 시각화") | |
| for i, facet in enumerate(facets): | |
| facet_params = params[f'facet_{i}_params'] | |
| labels = [str(i) for i in range(len(facet_params))] # 기본 레이블 생성 | |
| fig = plot_facet_params(facet, facet_params, labels) | |
| st.pyplot(fig) | |
| st.markdown("---") | |
| st.subheader("해석 가이드") | |
| st.markdown(""" | |
| - **양수 값**: 높은 방향의 난이도/능력/엄격성을 나타냅니다. | |
| - **음수 값**: 낮은 방향의 난이도/능력/엄격성을 나타냅니다. | |
| - **값의 크기**: 파라미터의 영향력을 나타냅니다. | |
| """) | |
| st.markdown("---") | |
| st.subheader("결과 다운로드") | |
| csv = results_df.to_csv(index=False) | |
| st.download_button( | |
| label="테이블 다운로드 (CSV)", | |
| data=csv, | |
| file_name="mfrm_results.csv", | |
| mime="text/csv" | |
| ) | |
| buffer = io.BytesIO() | |
| pdf = matplotlib.backends.backend_pdf.PdfPages(buffer) | |
| for i, facet in enumerate(facets): | |
| facet_params = params[f'facet_{i}_params'] | |
| labels = [str(i) for i in range(len(facet_params))] # 기본 레이블 생성 | |
| fig = plot_facet_params(facet, facet_params, labels) | |
| pdf.savefig(fig) | |
| pdf.close() | |
| st.download_button( | |
| label="시각화 다운로드 (PDF)", | |
| data=buffer, | |
| file_name="mfrm_plots.pdf", | |
| mime="application/pdf" | |
| ) | |
| # 파라미터 해석 가이드 | |
| st.markdown("---") | |
| st.subheader("파라미터 해석 가이드") | |
| st.markdown(""" | |
| ### 국면별 파라미터 해석 방법: | |
| #### 파라미터 값의 산출 방법: | |
| - **로그 오즈 비율(Log-Odds Ratio):** 라쉬 모델의 파라미터 값은 특정 응시자가 특정 국면에서 성공할 확률과 실패할 확률의 비율을 로그 값으로 나타낸 것입니다. | |
| - **상대적 해석:** 파라미터 값은 데이터의 평균과 표준편차를 기준으로 해석해야 하며, 다른 국면과의 비교를 통해 의미를 파악해야 합니다. | |
| #### 국면별 파라미터 유형 선택: | |
| 1. **과제나 문항 (Material/Item)** ➡️ **난이도(Difficulty)** | |
| 2. **응시자, 피평가자 (Subject)** ➡️ **능력(Ability)** | |
| 3. **평가자 (Rater)** ➡️ **엄격성(Severity)** | |
| 4. **응답 특성 (Response Style):** | |
| - 파라미터의 값은 응답자의 응답 경향성(예: 일관성, 극단적 선택 등)을 평가하는 데 사용할 수 있습니다. | |
| #### 파라미터 해석: | |
| | 파라미터 | -2 (매우 낮음) | 0 (평균) | +2 (매우 높음) | | |
| |----------|---------------------|-----------------|----------------------| | |
| | **난이도 (Difficulty)** | 매우 쉬운 난이도 | 평균 난이도 | 매우 어려운 난이도 | | |
| | **능력 (Ability)** | 매우 낮은 능력 | 평균적인 능력 | 매우 뛰어난 능력 | | |
| | **엄격성 (Severity)** | 매우 관대한 기준 | 평균적인 엄격성 | 매우 엄격한 기준 | | |
| | **응답 특성 (Response Style)** | 매우 일관된 응답 | 일반적인 응답 경향 | 매우 극단적 응답 | | |
| """) | |
| # 결과 해석 부분 추가 | |
| st.markdown("---") | |
| st.header("4. 결과 파일 매핑") | |
| st.subheader("매핑 정보 및 결과 파일 업로드") | |
| st.info(""" | |
| 데이터 변환 페이지에서 문자열을 숫자로 변환한 경우, 결과 해석을 좀 더 지원해드립니다. | |
| 해당 페이지에서 다운받은 csv파일을 업로드하여 문자열로 결과를 확인할 수 있습니다. | |
| """) | |
| mapping_file = st.file_uploader("매핑 정보 CSV 파일 업로드(데이터 변환 페이지에서 다운받은 매핑 파일)", type="csv", key="mapping_file") | |
| results_file = st.file_uploader("결과 CSV 파일 업로드(이 페이지 위에서 다운받은 결과 파일)", type="csv", key="results_file") | |
| if mapping_file and results_file: | |
| mapping_df = pd.read_csv(mapping_file) | |
| results_df = pd.read_csv(results_file) | |
| st.success("파일이 성공적으로 업로드되었습니다!") | |
| st.subheader("매핑 정보 및 결과 데이터 확인") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.write("매핑 정보 파일:") | |
| st.dataframe(mapping_df) | |
| with col2: | |
| st.write("결과 파일:") | |
| st.dataframe(results_df) | |
| # 사용자에게 필요한 열 선택하도록 요청 | |
| st.subheader("매핑 정보 파일 열 선택") | |
| facet_column = st.selectbox("국면을 나타내는 열을 선택하세요:", mapping_df.columns) | |
| value_column = st.selectbox("원래 값을 나타내는 열을 선택하세요:", mapping_df.columns) | |
| level_column1 = st.selectbox("첫 번째 숫자 변환 열을 선택하세요:", mapping_df.columns) | |
| level_column2 = st.selectbox("두 번째 숫자 변환 열을 선택하세요 (없으면 '선택 안함'):", ['선택 안함'] + list(mapping_df.columns)) | |
| if st.button("결과 데이터와 매핑 정보 결합"): | |
| # 매핑 정보에서 각 국면별로 딕셔너리 생성 | |
| mapping_dict = {} | |
| for _, row in mapping_df.iterrows(): | |
| facet = row[facet_column] | |
| value = row[value_column] | |
| if 'item' in facet.lower(): | |
| facet = 'item_id' | |
| level = float(row['문항 ID']) - 1 if pd.notna(row['문항 ID']) else None | |
| elif 'factor' in facet.lower(): | |
| facet = 'factor_id' | |
| level = float(row['숫자 변환']) if pd.notna(row['숫자 변환']) else None | |
| else: | |
| level = float(row[level_column1]) if pd.notna(row[level_column1]) else (float(row[level_column2]) if level_column2 != '선택 안함' and pd.notna(row[level_column2]) else None) | |
| if facet not in mapping_dict: | |
| mapping_dict[facet] = {} | |
| if level is not None: | |
| mapping_dict[facet][level] = value | |
| # 결과 데이터에 원래 값 매핑 | |
| final_df = results_df.copy() | |
| def map_value(row): | |
| facet = row['국면'] | |
| level = row['레벨'] | |
| if facet == 'item_id': | |
| return mapping_dict.get('item_id', {}).get(level, '매핑 없음') | |
| elif facet in mapping_dict: | |
| return mapping_dict[facet].get(level, '매핑 없음') | |
| else: | |
| return '매핑 없음' | |
| final_df['원래 값'] = final_df.apply(map_value, axis=1) | |
| st.subheader("결합된 데이터 테이블") | |
| # 전체 결합된 데이터 표시 | |
| st.write("결합된 전체 데이터:") | |
| st.dataframe(final_df) | |
| st.markdown("---") | |
| # 각 국면별로 파라미터 값을 매핑 데이터와 함께 정리 | |
| for i, facet in enumerate(final_df['국면'].unique()): | |
| if i > 0: # 첫 번째 국면 전에는 구분선을 추가하지 않습니다 | |
| st.markdown("---") # 구분선 추가 | |
| st.markdown(f"### {facet} 국면 결과:") | |
| facet_df = final_df[final_df['국면'] == facet].sort_values('파라미터 값', ascending=False) | |
| st.dataframe(facet_df) | |
| # 그래프 표시 | |
| fig = plot_facet_params(facet, facet_df['파라미터 값'].values, facet_df['원래 값'].values) | |
| st.pyplot(fig) | |
| # 마지막 국면 결과 후 구분선 추가 | |
| st.markdown("---") | |
| # 매핑되지 않은 값 확인 | |
| unmapped = final_df[final_df['원래 값'] == '매핑 없음'] | |
| if not unmapped.empty: | |
| st.warning("일부 값이 매핑되지 않았습니다:") | |
| st.dataframe(unmapped) | |
| else: | |
| st.success("모든 값이 성공적으로 매핑되었습니다.") | |
| st.subheader("결합된 데이터 다운로드") | |
| csv = final_df.to_csv(index=False) | |
| st.download_button( | |
| label="결합된 데이터 다운로드 (CSV)", | |
| data=csv, | |
| file_name="merged_results.csv", | |
| mime="text/csv" | |
| ) | |
| def data_conversion_page(): | |
| st.markdown("<h1 style='text-align: center;'>🔄 데이터 변환</h1>", unsafe_allow_html=True) | |
| st.info(""" | |
| 📌 데이터 변환 페이지 활용법 | |
| 1. **데이터 형식 통일**: 다국면 라쉬모형 분석을 위해서는 모든 데이터가 숫자 형태여야 합니다. | |
| 이 페이지에서는 문자열로 된 국면 데이터를 숫자로 변환합니다. | |
| 2. **Factor 설정**: 필요한 경우, 여러 문항을 그룹화하여 factor로 설정할 수 있습니다. | |
| 이는 더 복잡한 분석 구조를 가능하게 합니다. | |
| 3. **매핑 정보 생성**: 변환 과정에서 원래 값과 변환된 숫자 값 사이의 매핑 정보를 생성합니다. | |
| 이는 나중에 결과를 해석할 때 필수적입니다. | |
| """) | |
| st.markdown("---") | |
| st.header("데이터 업로드") | |
| uploaded_file = st.file_uploader("CSV 파일 업로드", type="csv") | |
| if uploaded_file is not None: | |
| data = pd.read_csv(uploaded_file) | |
| st.success("데이터가 성공적으로 업로드되었습니다!") | |
| st.write(data.head()) | |
| st.markdown("---") | |
| st.header("데이터 변환 설정") | |
| # ID 열 선택 또는 생성 | |
| id_columns = [col for col in data.columns if 'id' in col.lower()] | |
| id_option = st.radio( | |
| "ID 열 선택", | |
| ["기존 ID 열 사용", "새 ID 열 생성"], | |
| index=0 if id_columns else 1 | |
| ) | |
| if id_option == "기존 ID 열 사용": | |
| id_column = st.selectbox("ID 열 선택", options=id_columns) | |
| else: | |
| id_column = "generated_id" | |
| data[id_column] = [generate_unique_id() for _ in range(len(data))] | |
| st.success("새로운 ID 열이 생성되었습니다.") | |
| # 기존 국면 열 선택 (여러 개 선택 가능) | |
| facet_columns = st.multiselect("국면 열 선택", options=[col for col in data.columns if col != id_column]) | |
| # Factor 사용 여부 선택 | |
| use_factor = st.radio("Factor 사용 여부", ["Factor 사용", "Factor 없이 문항만 사용"]) | |
| if use_factor == "Factor 사용": | |
| st.subheader("중분류(factor) 설정") | |
| st.info(""" | |
| 📌 중분류(factor) 설정 안내 | |
| 중분류(factor)는 여러 문항을 그룹화하는 상위 범주입니다. 예를 들어: | |
| - 진단에서 역량명 혹은 요인값, 중분류 등이 해당됩니다. | |
| 각 중분류(factor)에 해당하는 문항들을 선택하여 그룹화합니다. | |
| 이렇게 설정된 중분류(factor)는 분석 시 하나의 국면으로 처리되어, | |
| 각 factor가 결과에 미치는 영향을 파악할 수 있게 해줍니다. | |
| """) | |
| n_factors = st.number_input("중분류(factor) 수 입력", min_value=1, value=1, step=1) | |
| factors = {} | |
| for i in range(n_factors): | |
| factor_name = st.text_input(f"중분류(factor) {i+1} 이름") | |
| factor_items = st.multiselect(f"{factor_name}에 포함될 문항 선택", options=[col for col in data.columns if col not in [id_column] + facet_columns]) | |
| factors[factor_name] = factor_items | |
| else: | |
| st.subheader("문항 선택") | |
| item_columns = st.multiselect("분석에 사용할 문항 선택", options=[col for col in data.columns if col not in [id_column] + facet_columns]) | |
| if st.button("데이터 변환"): | |
| if not facet_columns and (use_factor == "Factor 사용" and not factors) or (use_factor == "Factor 없이 문항만 사용" and not item_columns): | |
| st.error("국면 열 또는 문항을 선택해주세요.") | |
| else: | |
| # 선택된 열만 남기기 | |
| if use_factor == "Factor 사용": | |
| selected_columns = [id_column] + facet_columns + [item for items in factors.values() for item in items] | |
| else: | |
| selected_columns = [id_column] + facet_columns + item_columns | |
| converted_data = data[selected_columns].copy() | |
| # 매칭 정보 저장을 위한 딕셔너리 | |
| mapping_info = {} | |
| # 각 국면 열에 대해 문자열을 숫자로 변환 | |
| for col in facet_columns: | |
| if not pd.api.types.is_numeric_dtype(converted_data[col]): | |
| mapping = {val: i for i, val in enumerate(converted_data[col].unique())} | |
| converted_data[col] = converted_data[col].map(mapping) | |
| mapping_info[col] = pd.DataFrame.from_dict(mapping, orient='index', columns=['숫자 변환']) | |
| mapping_info[col].index.name = '원래 값' | |
| if use_factor == "Factor 사용": | |
| long_data_list = [] | |
| for factor, items in factors.items(): | |
| factor_data = pd.melt(converted_data, | |
| id_vars=[id_column] + facet_columns, | |
| value_vars=items, | |
| var_name='item', | |
| value_name='score') | |
| factor_data['factor'] = factor | |
| long_data_list.append(factor_data) | |
| long_data = pd.concat(long_data_list, ignore_index=True) | |
| # 중분류(요인)를 숫자로 변환 | |
| factor_mapping = {val: i for i, val in enumerate(factors.keys())} | |
| long_data['factor_id'] = long_data['factor'].map(factor_mapping) | |
| mapping_info['factor'] = pd.DataFrame.from_dict(factor_mapping, orient='index', columns=['숫자 변환']) | |
| mapping_info['factor'].index.name = '원래 값' | |
| # 문항 열을 ID로 변환 | |
| all_items = [item for items in factors.values() for item in items] | |
| item_mapping = {item: idx for idx, item in enumerate(all_items, start=1)} | |
| long_data['item_id'] = long_data['item'].map(item_mapping) | |
| # 최종 데이터 정렬 및 열 순서 조정 | |
| long_data = long_data.sort_values([id_column, 'factor_id', 'item_id']).reset_index(drop=True) | |
| long_data = long_data[[id_column] + facet_columns + ['factor_id', 'item_id', 'score']] | |
| else: | |
| long_data = pd.melt(converted_data, | |
| id_vars=[id_column] + facet_columns, | |
| value_vars=item_columns, | |
| var_name='item', | |
| value_name='score') | |
| # 문항 열을 ID로 변환 | |
| item_mapping = {item: idx for idx, item in enumerate(item_columns, start=1)} | |
| long_data['item_id'] = long_data['item'].map(item_mapping) | |
| # 최종 데이터 정렬 및 열 순서 조정 | |
| long_data = long_data.sort_values([id_column, 'item_id']).reset_index(drop=True) | |
| long_data = long_data[[id_column] + facet_columns + ['item_id', 'score']] | |
| # 문항 매핑 정보 저장 | |
| mapping_info['item'] = pd.DataFrame.from_dict(item_mapping, orient='index', columns=['문항 ID']) | |
| mapping_info['item'].index.name = '원래 문항명' | |
| st.success("데이터 변환이 완료되었습니다!") | |
| st.write(long_data.head()) | |
| # 변환된 데이터 CSV 다운로드 | |
| csv = long_data.to_csv(index=False) | |
| st.download_button( | |
| label="변환된 데이터 다운로드 (CSV)", | |
| data=csv, | |
| file_name="converted_long_data.csv", | |
| mime="text/csv", | |
| key="download_converted_data" | |
| ) | |
| # 매칭 정보 CSV 다운로드 | |
| if mapping_info: | |
| mapping_df = pd.concat(mapping_info.values(), keys=mapping_info.keys()) | |
| mapping_csv = mapping_df.to_csv() | |
| st.download_button( | |
| label="매칭 정보 다운로드 (CSV)", | |
| data=mapping_csv, | |
| file_name="mapping_info.csv", | |
| mime="text/csv", | |
| key="download_mapping_info" | |
| ) | |
| # 매칭 정보 표시 | |
| if mapping_info: | |
| st.subheader("매칭 정보") | |
| for col, mapping in mapping_info.items(): | |
| st.write(f"{col} 열 매칭:") | |
| st.write(mapping) | |
| def guide_page(): | |
| st.markdown("<h1 style='text-align: center;'>ℹ️ 안내 페이지</h1>", unsafe_allow_html=True) | |
| st.info(""" | |
| 📌 다국면 라쉬모형 분석 도구 사용 가이드 | |
| 1. **데이터 준비**: | |
| - CSV 형식의 데이터를 준비합니다. | |
| - 각 열은 국면(예: 응시자, 문항, 평가자)을 나타내며, 마지막 열은 점수여야 합니다. | |
| 2. **데이터 변환**: | |
| - 데이터가 문자열 형태의 국면을 포함하고 있다면, '데이터 변환' 페이지에서 숫자로 변환합니다. | |
| - 변환된 데이터와 매핑 정보를 다운로드합니다. | |
| 3. **데이터 분석**: | |
| - 변환된 데이터를 업로드하고 분석을 실행합니다. | |
| - 결과를 확인하고 해석합니다. | |
| 4. **결과 해석**: | |
| - 매핑 정보를 이용해 숫자로 변환된 결과를 원래 값으로 되돌립니다. | |
| - 각 국면별 파라미터를 해석하고 시각화 결과를 확인합니다. | |
| 🔍 자세한 이론적 설명은 아래에서 확인할 수 있습니다. | |
| """) | |
| st.markdown("---") | |
| st.header("이론적 설명") | |
| st.markdown(""" | |
| 다국면 라쉬모형(Multi-Facet Rasch Model, MFRM)은 기존의 라쉬모형을 확장한 것입니다. | |
| 시험이나 평가 상황에서 여러 요인(국면)들이 결과에 미치는 영향을 동시에 분석할 수 있게 해줍니다. | |
| 이 모형은 응시자의 능력, 문항의 난이도, 채점자의 엄격성 등 여러 요인을 고려하여 | |
| 각 요인의 독립적인 영향을 측정합니다. | |
| """) | |
| st.markdown("---") | |
| st.header("주요 특징") | |
| st.markdown(""" | |
| 1. **다중 요인 분석**: 여러 국면을 동시에 고려할 수 있습니다. | |
| 2. **객관적 측정**: 각 국면의 영향을 독립적으로 측정합니다. | |
| 3. **공통 척도**: 모든 국면을 동일한 척도 상에서 비교할 수 있습니다. | |
| 4. **편향 탐지**: 특정 채점자나 문항의 편향을 식별할 수 있습니다. | |
| """) | |
| st.markdown("---") | |
| st.header("주의사항") | |
| st.markdown(""" | |
| 1. **국면 수 결정**: | |
| - 일반적으로 3-4개의 국면이 가장 흔히 사용됩니다. | |
| - 너무 많은 국면을 사용하면 모델이 복잡해지고 해석이 어려워질 수 있습니다. | |
| - 각 국면은 독립적이어야 합니다. | |
| 2. **변수 선택 시 주의사항**: | |
| - 각 국면은 측정하고자 하는 구성개념과 관련이 있어야 합니다. | |
| - 변수들은 서로 독립적이어야 합니다. | |
| - 각 국면 내의 변수들은 동일한 척도로 측정되어야 합니다. | |
| - 결측치가 너무 많은 변수는 피해야 합니다. | |
| 3. **데이터 구조**: | |
| - 각 관찰치는 모든 국면에 대한 정보를 포함해야 합니다. | |
| """) | |
| def references_page(): | |
| st.markdown("<h1 style='text-align: center;'>📚 참고 자료</h1>", unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.header("주요 참고 문헌") | |
| st.markdown(""" | |
| 1. Linacre, J. M. (1989). Many-facet Rasch measurement. Chicago: MESA Press. | |
| 2. Bond, T. G., & Fox, C. M. (2015). Applying the Rasch model: Fundamental measurement in the human sciences (3rd ed.). Routledge. | |
| 3. Eckes, T. (2011). Introduction to Many-Facet Rasch Measurement: Analyzing and Evaluating Rater-Mediated Assessments. Peter Lang. | |
| """) | |
| st.markdown("---") | |
| st.header("유용한 웹사이트") | |
| st.markdown(""" | |
| - [Rasch Measurement Analysis Software Directory](https://www.rasch.org/software.htm) | |
| - [Journal of Applied Measurement](https://jampress.org) | |
| - [Rasch Measurement Transactions](https://www.rasch.org/rmt/) | |
| """) | |
| st.markdown("---") | |
| st.header("관련 소프트웨어") | |
| st.markdown(""" | |
| - FACETS: [https://www.winsteps.com/facets.htm](https://www.winsteps.com/facets.htm) | |
| - ConQuest: [https://www.acer.org/au/conquest](https://www.acer.org/au/conquest) | |
| - TAM (R package): [https://cran.r-project.org/web/packages/TAM/](https://cran.r-project.org/web/packages/TAM/) | |
| - mirt (R package): [https://cran.r-project.org/web/packages/mirt/](https://cran.r-project.org/web/packages/mirt/) | |
| """) | |
| def advanced_analysis_page(): | |
| st.markdown("<h1 style='text-align: center;'>🔬 고급 분석</h1>", unsafe_allow_html=True) | |
| st.info(""" | |
| 이 페이지에서는 다음과 같은 고급 분석 기능을 제공합니다: | |
| 🗺️ 변수 지도 | |
| - 모든 국면의 측정값을 한 눈에 비교하고 싶을 때 사용합니다. | |
| - 응시자의 능력, 문항의 난이도, 평가자의 엄격성을 동일한 척도로 볼 수 있습니다. | |
| 🎭 Rator 엄격성 비교 | |
| - 평가자들 간의 엄격성 차이를 확인하고 싶을 때 사용합니다. | |
| - 특정 평가자가 다른 평가자들에 비해 얼마나 엄격하거나 관대한지 파악할 수 있습니다. | |
| ⚖️ 공정 점수 계산 | |
| - 평가자의 엄격성 차이를 보정한 공정한 점수가 필요할 때 사용합니다. | |
| - 서로 다른 평가자에게 평가받은 응시자들의 점수를 공정하게 비교할 수 있습니다. | |
| 🔀 상호작용 효과 분석 | |
| - 두 국면 간의 상호작용을 파악하고 싶을 때 사용합니다. | |
| - 예를 들어, 특정 평가자가 특정 유형의 문항에서 더 엄격한지, 또는 특정 그룹의 응시자들이 특정 유형의 문항에서 더 높은 성과를 보이는지 확인할 수 있습니다. | |
| 이러한 분석을 통해 평가의 공정성과 타당성을 높이고, 더 깊이 있는 인사이트를 얻을 수 있습니다. | |
| """) | |
| with st.expander("🔽 변수 지도", expanded=True): | |
| st.info(""" | |
| 🔍 변수 지도란? | |
| 변수 지도는 다국면 라쉬모형 분석의 결과를 시각적으로 표현한 것입니다. 이 지도는 모든 국면(예: 응시자, 문항, 평가자)의 측정값을 동일한 척도 상에 표시합니다. | |
| 해석 방법: | |
| 1. 각 행은 하나의 국면을 나타냅니다. | |
| 2. 각 점은 해당 국면의 한 요소(예: 특정 응시자, 특정 문항)를 나타냅니다. | |
| 3. X축은 로짓 척도를 나타내며, 오른쪽으로 갈수록 높은 값을 의미합니다. | |
| 4. 응시자의 경우, 오른쪽에 있을수록 능력이 높음을 의미합니다. | |
| 5. 문항의 경우, 오른쪽에 있을수록 난이도가 높음을 의미합니다. | |
| 6. 평가자의 경우, 오른쪽에 있을수록 엄격함을 의미합니다. | |
| 이 지도를 통해 각 국면 내의 분포와 국면 간의 관계를 한눈에 파악할 수 있습니다. | |
| 변수 지도를 그리기 위해서는 결과 데이터가 필요합니다. | |
| '📊 데이터 분석' 페이지에서 다운받은 **결과 데이터(mfrm_results.csv)** 를 업로드해주세요. | |
| """) | |
| uploaded_file = st.file_uploader("분석 결과 파일 업로드 (mfrm_results.csv)", type="csv", key="variable_map") | |
| if uploaded_file is not None: | |
| results_df = pd.read_csv(uploaded_file) | |
| st.success("파일이 성공적으로 업로드되었습니다!") | |
| # 각 국면별로 고유한 색상 할당 | |
| facets = results_df['국면'].unique() | |
| colors = px.colors.qualitative.Plotly[:len(facets)] | |
| color_map = dict(zip(facets, colors)) | |
| # 그래프 생성 | |
| fig = go.Figure() | |
| for facet in facets: | |
| facet_data = results_df[results_df['국면'] == facet] | |
| fig.add_trace(go.Scatter( | |
| x=facet_data['파라미터 값'], | |
| y=[facet] * len(facet_data), | |
| mode='markers', | |
| name=facet, | |
| marker=dict(color=color_map[facet], size=6), # 점 크기 줄임 | |
| text=[f"레벨: {level}<br>파라미터 값: {value:.2f}" for level, value in zip(facet_data['레벨'], facet_data['파라미터 값'])], | |
| hoverinfo='text' | |
| )) | |
| # 레이아웃 설정 | |
| fig.update_layout( | |
| title='변수 지도', | |
| xaxis_title='로짓 척도', | |
| yaxis_title='국면', | |
| height=max(400, 100 + 50 * len(facets)), # 최소 높이 400px | |
| yaxis=dict( | |
| categoryorder='array', | |
| categoryarray=facets[::-1], # 역순으로 정렬 | |
| ), | |
| showlegend=False, | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) # 창 너비에 맞춤 | |
| st.markdown(""" | |
| ### 변수 지도 해석 가이드 | |
| - 각 점은 해당 국면의 한 요소(예: 특정 응시자, 특정 문항)를 나타냅니다. | |
| - X축은 로짓 척도를 나타내며, 오른쪽으로 갈수록 높은 값을 의미합니다. | |
| - 응시자의 경우, 오른쪽에 있을수록 능력이 높음을 의미합니다. | |
| - 문항의 경우, 오른쪽에 있을수록 난이도가 높음을 의미합니다. | |
| - 평가자의 경우, 오른쪽에 있을수록 엄격함을 의미합니다. | |
| - 각 점 위에 마우스를 올리면 해당 요소의 레벨과 정확한 파라미터 값을 확인할 수 있습니다. | |
| """) | |
| # 결과를 테이블로 출력 | |
| st.subheader("변수 지도 결과 테이블") | |
| st.dataframe(results_df) | |
| # 결과 다운로드 버튼 | |
| csv = results_df.to_csv(index=False) | |
| st.download_button( | |
| label="변수 지도 결과 다운로드 (CSV)", | |
| data=csv, | |
| file_name="variable_map_results.csv", | |
| mime="text/csv" | |
| ) | |
| st.markdown("---") | |
| with st.expander("🔽 Rator 엄격성 비교", expanded=True): | |
| st.info(""" | |
| 🔍 Rator 엄격성 비교란? | |
| 각 Rator(평가자)의 엄격성 지수를 비교하여 평가자 간의 차이를 확인할 수 있습니다. | |
| 이를 통해 특정 평가자가 다른 평가자들에 비해 얼마나 엄격하거나 관대한지 파악할 수 있습니다. | |
| """) | |
| results_file = st.file_uploader("분석 결과 파일 업로드 (mfrm_results.csv)", type="csv", key="rator_severity") | |
| raw_data_file = st.file_uploader("원본 데이터 파일 업로드", type="csv", key="rator_raw_data") | |
| if results_file is not None and raw_data_file is not None: | |
| results_df = pd.read_csv(results_file) | |
| raw_data = pd.read_csv(raw_data_file) | |
| rator_data = results_df[results_df['국면'] == 'rater_id'].sort_values('파라미터 값', ascending=False) | |
| def get_severity_level(x): | |
| abs_x = abs(x) | |
| if abs_x < 0.5: | |
| return '보통' | |
| elif abs_x < 1: | |
| return '약간 ' + ('엄격' if x > 0 else '관대') | |
| elif abs_x < 2: | |
| return ('엄격' if x > 0 else '관대') | |
| else: | |
| return '매우 ' + ('엄격' if x > 0 else '관대') | |
| # 테이블 데이터 준비 | |
| rator_table = rator_data.copy() | |
| rator_table['엄격성 설명'] = rator_table['파라미터 값'].apply(get_severity_level) | |
| comments_map = { | |
| '매우 엄격': '평가 기준을 좀 더 유연하게 적용해 보세요.', | |
| '엄격': '다양한 관점에서 응시자의 장점을 고려해 보세요.', | |
| '약간 엄격': '약간 더 관대한 평가를 고려해 보세요.', | |
| '보통': '현재의 평가 스타일을 유지하세요.', | |
| '약간 관대': '약간 더 엄격한 평가를 고려해 보세요.', | |
| '관대': '평가 기준을 좀 더 엄격하게 적용해 보세요.', | |
| '매우 관대': '평가의 객관성을 높이기 위해 노력해 주세요.' | |
| } | |
| rator_table['Comments'] = rator_table['엄격성 설명'].map(comments_map) | |
| # 그래프 생성 | |
| fig = px.bar(rator_table, x='레벨', y='파라미터 값', | |
| labels={'레벨': 'Rator ID', '파라미터 값': '엄격성 지수'}, | |
| title='Rator 엄격성 비교', | |
| color='엄격성 설명', | |
| color_discrete_map={ | |
| '매우 엄격': 'darkred', | |
| '엄격': 'red', | |
| '약간 엄격': 'lightcoral', | |
| '보통': 'lightgrey', | |
| '약간 관대': 'lightblue', | |
| '관대': 'blue', | |
| '매우 관대': 'darkblue' | |
| }) | |
| fig.update_layout(xaxis={'categoryorder':'total descending'}) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.subheader("Rator 엄격성 비교 테이블") | |
| st.dataframe(rator_table) | |
| # 결과 다운로드 버튼 | |
| csv = rator_table.to_csv(index=False) | |
| st.download_button( | |
| label="Rator 엄격성 비교 결과 다운로드 (CSV)", | |
| data=csv, | |
| file_name="rator_severity_comparison.csv", | |
| mime="text/csv" | |
| ) | |
| # Rator별 Examinee 점수 테이블 생성 | |
| st.subheader("Rator별 Examinee 점수 테이블") | |
| def calculate_severity_index(rater_param, mean_severity): | |
| return rater_param - mean_severity | |
| def get_severity_level(severity_index): | |
| abs_x = abs(severity_index) | |
| if abs_x < 0.5: | |
| return '보통' | |
| elif abs_x < 1: | |
| return '약간 ' + ('엄격' if severity_index > 0 else '관대') | |
| elif abs_x < 2: | |
| return ('엄격' if severity_index > 0 else '관대') | |
| else: | |
| return '매우 ' + ('엄격' if severity_index > 0 else '관대') | |
| # Rator 엄격성 비교 부분 업데이트 | |
| if results_file is not None and raw_data_file is not None: | |
| results_df = pd.read_csv(results_file) | |
| raw_data = pd.read_csv(raw_data_file) | |
| rator_data = results_df[results_df['국면'] == 'rater_id'].sort_values('파라미터 값', ascending=False) | |
| mean_severity = np.mean(rator_data['파라미터 값']) | |
| # 엄격성 지표 계산 | |
| rator_data['엄격성 지표'] = rator_data['파라미터 값'].apply(lambda x: calculate_severity_index(x, mean_severity)) | |
| rator_data['엄격성 설명'] = rator_data['엄격성 지표'].apply(get_severity_level) | |
| comments_map = { | |
| '매우 엄격': '평가 기준을 좀 더 유연하게 적용해 보세요.', | |
| '엄격': '다양한 관점에서 응시자의 장점을 고려해 보세요.', | |
| '약간 엄격': '약간 더 관대한 평가를 고려해 보세요.', | |
| '보통': '현재의 평가 스타일을 유지하세요.', | |
| '약간 관대': '약간 더 엄격한 평가를 고려해 보세요.', | |
| '관대': '평가 기준을 좀 더 엄격하게 적용해 보세요.', | |
| '매우 관대': '평가의 객관성을 높이기 위해 노력해 주세요.' | |
| } | |
| rator_data['Comments'] = rator_data['엄격성 설명'].map(comments_map) | |
| # 그래프 생성 | |
| fig = px.bar(rator_data, x='레벨', y='엄격성 지표', | |
| labels={'레벨': 'Rator ID', '엄격성 지표': '엄격성 지수'}, | |
| title='Rator 엄격성 비교', | |
| color='엄격성 설명', | |
| color_discrete_map={ | |
| '매우 엄격': 'darkred', | |
| '엄격': 'red', | |
| '약간 엄격': 'lightcoral', | |
| '보통': 'lightgrey', | |
| '약간 관대': 'lightblue', | |
| '관대': 'blue', | |
| '매우 관대': 'darkblue' | |
| }) | |
| fig.update_layout(xaxis={'categoryorder':'total descending'}) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.subheader("Rator 엄격성 비교 테이블") | |
| st.dataframe(rator_data) | |
| # 결과 다운로드 버튼 | |
| csv = rator_data.to_csv(index=False) | |
| st.download_button( | |
| label="Rator 엄격성 비교 결과 다운로드 (CSV)", | |
| data=csv, | |
| file_name="rator_severity_comparison.csv", | |
| mime="text/csv" | |
| ) | |
| else: | |
| st.warning("Rator 엄격성 비교를 위해 분석 결과 파일과 원본 데이터 파일을 모두 업로드해주세요.") | |
| st.markdown("---") | |
| st.markdown("---") | |
| with st.expander("🔽 공정 점수 계산", expanded=True): | |
| st.info(""" | |
| 🔍 공정 점수란? | |
| 공정 점수는 평가자의 엄격성 차이를 보정한 점수입니다. | |
| 이를 통해 서로 다른 평가자에게 평가받은 응시자들의 점수를 공정하게 비교할 수 있습니다. | |
| 공정 점수를 계산하려면 원본 데이터와 분석 결과 데이터가 모두 필요합니다. | |
| '📊 데이터 분석' 페이지에서 사용한 원본 데이터와 결과 데이터를 모두 업로드해주세요. | |
| """) | |
| results_file = st.file_uploader("분석 결과 파일 업로드 (mfrm_results.csv)", type="csv", key="fair_score_results") | |
| raw_data_file = st.file_uploader("원본 데이터 파일 업로드", type="csv", key="fair_score_raw") | |
| if results_file is not None and raw_data_file is not None: | |
| try: | |
| results_df = pd.read_csv(results_file) | |
| raw_data = pd.read_csv(raw_data_file) | |
| st.success("파일들이 성공적으로 업로드되었습니다!") | |
| def calculate_severity_index(rater_param, mean_severity): | |
| return rater_param - mean_severity | |
| def get_severity_level(severity_index): | |
| abs_x = abs(severity_index) | |
| if abs_x < 0.5: | |
| return '보통' | |
| elif abs_x < 1: | |
| return '약간 ' + ('엄격' if severity_index > 0 else '관대') | |
| elif abs_x < 2: | |
| return ('엄격' if severity_index > 0 else '관대') | |
| else: | |
| return '매우 ' + ('엄격' if severity_index > 0 else '관대') | |
| def calculate_fair_score(raw_score, rater_severity, mean_severity, min_score, max_score): | |
| severity_index = calculate_severity_index(rater_severity, mean_severity) | |
| adjustment = severity_index / (2 * max(abs(severity_index), 1)) | |
| adjusted_score = raw_score + (max_score - min_score) * adjustment | |
| return max(min_score, min(max_score, adjusted_score)) | |
| def calculate_fair_scores(raw_data, score_col, rater_id_col, rater_severities, mean_severity): | |
| max_score = raw_data[score_col].max() | |
| min_score = raw_data[score_col].min() | |
| raw_data['fair_score'] = raw_data.apply( | |
| lambda row: calculate_fair_score( | |
| row[score_col], | |
| rater_severities[row[rater_id_col]], | |
| mean_severity, | |
| min_score, | |
| max_score | |
| ), | |
| axis=1 | |
| ) | |
| return raw_data | |
| if {'rater_id', 'score', 'examinee_id'}.issubset(raw_data.columns): | |
| rater_id_col = st.selectbox("평가자 ID 열을 선택하세요:", raw_data.columns) | |
| score_col = st.selectbox("점수 열을 선택하세요:", raw_data.columns) | |
| examinee_id_col = st.selectbox("응시자(대상자/피평가자) ID 열을 선택하세요:", raw_data.columns) | |
| rater_data = results_df[results_df['국면'] == rater_id_col] | |
| rater_severities = dict(zip(rater_data['레벨'], rater_data['파라미터 값'])) | |
| mean_severity = np.mean(list(rater_severities.values())) | |
| fair_scores = calculate_fair_scores(raw_data, score_col, rater_id_col, rater_severities, mean_severity) | |
| # Rater 엄격성 정보 추가 | |
| rater_info = pd.DataFrame({ | |
| 'rater_id': rater_severities.keys(), | |
| 'severity': rater_severities.values() | |
| }) | |
| rater_info['severity_index'] = rater_info['severity'].apply(lambda x: calculate_severity_index(x, mean_severity)) | |
| rater_info['severity_level'] = rater_info['severity_index'].apply(get_severity_level) | |
| # 시각화 | |
| unique_raters = fair_scores[rater_id_col].unique() | |
| color_discrete_sequence = px.colors.qualitative.Plotly[:len(unique_raters)] | |
| fig = px.scatter( | |
| fair_scores, | |
| x=score_col, | |
| y='fair_score', | |
| color=rater_id_col, | |
| color_discrete_sequence=color_discrete_sequence, | |
| category_orders={rater_id_col: unique_raters}, | |
| hover_data=[examinee_id_col], | |
| labels={ | |
| score_col: '원래 점수', | |
| 'fair_score': '공정 점수', | |
| rater_id_col: '평가자', | |
| examinee_id_col: '응시자' | |
| }, | |
| title='원래 점수 vs 공정 점수 (평가자별)' | |
| ) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=[fair_scores[score_col].min(), fair_scores[score_col].max()], | |
| y=[fair_scores[score_col].min(), fair_scores[score_col].max()], | |
| mode='lines', | |
| line=dict(color='black', dash='dash'), | |
| name='1:1 선' | |
| ) | |
| ) | |
| fig.update_layout( | |
| width=900, | |
| height=700, | |
| xaxis_title='원래 점수', | |
| yaxis_title='공정 점수', | |
| legend_title='평가자', | |
| font=dict(size=14), | |
| legend=dict( | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=0.01 | |
| ) | |
| ) | |
| axis_range = [ | |
| min(fair_scores[score_col].min(), fair_scores['fair_score'].min()) - 0.5, | |
| max(fair_scores[score_col].max(), fair_scores['fair_score'].max()) + 0.5 | |
| ] | |
| fig.update_xaxes(range=axis_range) | |
| fig.update_yaxes(range=axis_range) | |
| st.plotly_chart(fig) | |
| st.markdown(""" | |
| ### 해석 가이드 | |
| - **공정 점수**: 평가자 간의 엄격성 차이가 보정된 점수입니다. 공정 점수가 원래 점수보다 높으면 해당 응시자는 엄격한 평가자에게 낮은 점수를 받았을 가능성이 큽니다. | |
| - **산점도**: 점들이 검은색 대각선에 가까울수록 평가자의 엄격성이 평균에 가깝고, 멀어질수록 특정 평가자의 엄격성이 평균과 차이가 큽니다. | |
| - **색상**: 각 색상은 서로 다른 평가자를 나타냅니다. 이를 통해 평가자별 점수 부여 패턴을 확인할 수 있습니다. | |
| - **대각선 위/아래**: | |
| - 대각선 위에 있는 점들: 해당 응시자들은 평균보다 엄격한 평가자에게 평가받았을 가능성이 큽니다. | |
| - 대각선 아래에 있는 점들: 해당 응시자들은 평균보다 관대한 평가자에게 평가받았을 가능성이 큽니다. | |
| """) | |
| # 결과를 테이블로 출력 | |
| st.subheader("공정 점수 계산 결과 테이블") | |
| st.dataframe(fair_scores) | |
| st.subheader("평가자 엄격성 정보") | |
| st.dataframe(rater_info) | |
| # 결과 다운로드 버튼 | |
| csv = fair_scores.to_csv(index=False) | |
| st.download_button( | |
| label="공정 점수 계산 결과 다운로드 (CSV)", | |
| data=csv, | |
| file_name="fair_scores_results.csv", | |
| mime="text/csv" | |
| ) | |
| else: | |
| st.error("필요한 데이터 열이 누락되었습니다. 필요한 열: rater_id, score, examinee_id") | |
| except Exception as e: | |
| st.error(f"오류가 발생했습니다: {str(e)}") | |
| else: | |
| st.warning("공정 점수 계산을 위해 분석 결과 파일과 원본 데이터 파일을 모두 업로드해주세요.") | |
| st.markdown("---") | |
| with st.expander("🔽 상호작용 효과 분석", expanded=True): | |
| st.info(""" | |
| 🔍 상호작용 효과 분석이란? | |
| 상호작용 효과 분석은 두 개 이상의 국면이 서로 어떻게 영향을 미치는지 살펴보는 방법입니다. | |
| 이를 통해 특정 국면의 효과가 다른 국면의 수준에 따라 어떻게 달라지는지 파악할 수 있습니다. | |
| 예를 들어: | |
| - 특정 평가자가 특정 유형의 문항에서 더 엄격하거나 관대한지 | |
| - 특정 그룹의 응시자들이 특정 유형의 문항에서 더 높은 성과를 보이는지 | |
| 이러한 분석은 평가의 공정성과 타당성을 높이는 데 중요한 정보를 제공합니다. | |
| """) | |
| raw_data_file = st.file_uploader("원본 데이터 파일 업로드", type="csv", key="interaction_raw_data") | |
| if raw_data_file is not None: | |
| raw_data = pd.read_csv(raw_data_file) | |
| st.success("파일이 성공적으로 업로드되었습니다!") | |
| facet1 = st.selectbox("첫 번째 국면 선택", raw_data.columns) | |
| facet2 = st.selectbox("두 번째 국면 선택", [col for col in raw_data.columns if col != facet1]) | |
| score_col = st.selectbox("점수 열 선택", raw_data.columns) | |
| if st.button("상호작용 효과 분석 실행"): | |
| # 히트맵 | |
| pivot = raw_data.pivot_table(values=score_col, index=facet1, columns=facet2, aggfunc='mean') | |
| fig = px.imshow(pivot, | |
| title=f'{facet1}와 {facet2}의 평균 점수 히트맵', | |
| labels=dict(x=facet2, y=facet1, color="평균 점수"), | |
| aspect="auto") | |
| fig.update_layout(height=600, width=800) | |
| # 히트맵 colorbar 위치 조정 | |
| fig.update_layout(coloraxis=dict(colorbar=dict(title="평균 점수", len=0.6, y=0.5, thickness=20))) | |
| st.plotly_chart(fig) | |
| st.markdown(f""" | |
| ### 상호작용 효과 해석 가이드 | |
| **히트맵 해석**: | |
| - 각 셀의 색상은 해당 {facet1}와 {facet2} 조합의 평균 점수를 나타냅니다. | |
| - 색상 패턴이 균일하지 않고 특정 패턴을 보인다면, 두 국면 사이에 상호작용이 있음을 시사합니다. | |
| - 색상이 진할수록 높은 평균 점수를, 연할수록 낮은 평균 점수를 나타냅니다. | |
| **해석 예시**: | |
| - 만약 특정 {facet1} 수준에서 {facet2}에 따른 점수 변화가 다른 {facet1} 수준과 다르다면, | |
| 이는 {facet1}와 {facet2} 사이에 상호작용이 있음을 나타냅니다. | |
| - 예를 들어, 특정 유형의 문항에서 특정 그룹의 응시자들이 유독 높은(또는 낮은) 점수를 받는다면, | |
| 이는 문항 유형과 응시자 그룹 사이의 상호작용을 나타냅니다. | |
| 이러한 상호작용 효과를 발견하면, 평가 설계나 채점 기준을 재검토하거나, | |
| 특정 그룹에 대한 추가적인 교육이나 지원이 필요한지 고려해볼 수 있습니다. | |
| """) | |
| # 결과를 테이블로 출력 | |
| st.subheader("상호작용 효과 분석 결과 테이블") | |
| st.dataframe(pivot) | |
| # 결과 다운로드 버튼 | |
| csv = pivot.to_csv(index=True) | |
| st.download_button( | |
| label="상호작용 효과 분석 결과 다운로드 (CSV)", | |
| data=csv, | |
| file_name="interaction_effect_results.csv", | |
| mime="text/csv" | |
| ) | |
| else: | |
| st.warning("상호작용 효과 분석을 위해 원본 데이터 파일을 업로드해주세요.") | |
| def main(): | |
| st.set_page_config(page_title="MFRM 분석 도구", page_icon="📊", layout="wide") | |
| st.markdown(""" | |
| <style> | |
| .sidebar .stButton > button { | |
| width: 100%; | |
| background-color: #cce7ff; | |
| color: black; | |
| border-radius: 4px; | |
| border: none; | |
| padding: 10px 24px; | |
| margin-bottom: 10px; | |
| font-size: 16px; | |
| text-align: center; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .sidebar .stButton > button:hover { | |
| background-color: #b3d4ff; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.sidebar.markdown("<h1 style='text-align: center; color: #343a40; font-size: 16px; font-weight: 600;'>📊 MFRM 분석 도구</h1>", unsafe_allow_html=True) | |
| st.sidebar.markdown("<hr style='margin: 15px 0;'>", unsafe_allow_html=True) | |
| menu_items = { | |
| "ℹ️ 안내 페이지": guide_page, | |
| "🔄 데이터 변환": data_conversion_page, | |
| "📊 데이터 분석": data_analysis_page, | |
| "🔬 고급 분석": advanced_analysis_page, | |
| "📚 참고 자료": references_page | |
| } | |
| for label, func in menu_items.items(): | |
| if st.sidebar.button(label, key=label, use_container_width=True): | |
| st.session_state.page = label | |
| if "page" not in st.session_state: | |
| st.session_state.page = "ℹ️ 안내 페이지" | |
| menu_items[st.session_state.page]() | |
| st.markdown( | |
| """ | |
| <footer> | |
| © mySUNI 배수정 RF. All rights reserved. 문의사항 있으시면 연락주세요 (soojeong.bae@sk.com). | |
| </footer> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| if __name__ == "__main__": | |
| main() |