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( """ """, unsafe_allow_html=True ) def data_analysis_page(): st.markdown("

📊 다국면 라쉬모형 분석

", unsafe_allow_html=True) st.markdown("

(Multi-Facet Rasch Model Analysis)

", 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("

🔄 데이터 변환

", 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("

ℹ️ 안내 페이지

", 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("

📚 참고 자료

", 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("

🔬 고급 분석

", 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}
파라미터 값: {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(""" """, unsafe_allow_html=True) st.sidebar.markdown("

📊 MFRM 분석 도구

", unsafe_allow_html=True) st.sidebar.markdown("
", 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( """ """, unsafe_allow_html=True ) if __name__ == "__main__": main()