jisubae commited on
Commit
4a43fed
·
1 Parent(s): cd13f52

feat: Add optional HF dataset sync for leaderboard

Browse files
Files changed (6) hide show
  1. README.md +9 -0
  2. app.py +9 -1
  3. config.py +3 -0
  4. env.example +11 -0
  5. src/leaderboard_manager.py +320 -202
  6. ui/leaderboard_tab.py +82 -3
README.md CHANGED
@@ -48,6 +48,8 @@ hf_oauth: true
48
  - Hugging Face Dataset repo
49
  - 기준 데이터: `FRESHQA_DATA_REPO_ID` / `FRESHQA_DATA_FILENAME`
50
  - (옵션) 제출 추적 저장소: `SUBMISSION_TRACKER_REPO_ID`
 
 
51
 
52
  설치:
53
  ```bash
@@ -76,6 +78,10 @@ cp env.example .env
76
  - UPSTAGE_API_KEY 또는 UPSTAGE_API_KEYS(콤마 구분)
77
  - ENABLE_SUBMISSION_LIMIT (기본: true)
78
  - SUBMISSION_TRACKER_REPO_ID (제출 제한 사용 시 필요)
 
 
 
 
79
 
80
  검증: 앱 시작 시 `Config.validate_required_configs()`가 누락된 필수 설정을 검사합니다.
81
 
@@ -108,6 +114,8 @@ Docker(옵션):
108
 
109
  3) 리더보드 탭
110
  - 제출 결과가 `data/leaderboard_results.csv`에 누적
 
 
111
  - 검색/새로고침 가능
112
 
113
  ---
@@ -122,6 +130,7 @@ Docker(옵션):
122
  - `freshqa/freshqa_acc.py::calculate_accuracy`, `process_freshqa_dataframe`
123
  5) 저장:
124
  - 리더보드: `src/leaderboard_manager.py::append_to_leaderboard_data`
 
125
  - (옵션) 제출 이력: `src/submission_tracker.py` (ENABLE_SUBMISSION_LIMIT=true 일 때만)
126
 
127
  주의: `ENABLE_SUBMISSION_LIMIT=false`인 경우, 제출 이력 추적용 Hugging Face 저장소 접근을 시도하지 않도록 코드가 반영되어 있습니다.
 
48
  - Hugging Face Dataset repo
49
  - 기준 데이터: `FRESHQA_DATA_REPO_ID` / `FRESHQA_DATA_FILENAME`
50
  - (옵션) 제출 추적 저장소: `SUBMISSION_TRACKER_REPO_ID`
51
+ - (옵션) 리더보드를 Hugging Face dataset에 백업하려면 `UPLOAD_LEADERBOARD_TO_HF=true` 설정
52
+
53
 
54
  설치:
55
  ```bash
 
78
  - UPSTAGE_API_KEY 또는 UPSTAGE_API_KEYS(콤마 구분)
79
  - ENABLE_SUBMISSION_LIMIT (기본: true)
80
  - SUBMISSION_TRACKER_REPO_ID (제출 제한 사용 시 필요)
81
+ - UPLOAD_LEADERBOARD_TO_HF
82
+ - true: 리더보드를 HF Private Dataset에도 백업(권장: 운영 환경)
83
+ - false: 로컬 CSV에만 저장(권장: 로컬 개발)
84
+
85
 
86
  검증: 앱 시작 시 `Config.validate_required_configs()`가 누락된 필수 설정을 검사합니다.
87
 
 
114
 
115
  3) 리더보드 탭
116
  - 제출 결과가 `data/leaderboard_results.csv`에 누적
117
+ - (옵션) `UPLOAD_LEADERBOARD_TO_HF=true`인 경우 Hugging Face Dataset에도
118
+ `leaderboard_results.csv`로 자동 업로드됩니다.
119
  - 검색/새로고침 가능
120
 
121
  ---
 
130
  - `freshqa/freshqa_acc.py::calculate_accuracy`, `process_freshqa_dataframe`
131
  5) 저장:
132
  - 리더보드: `src/leaderboard_manager.py::append_to_leaderboard_data`
133
+ - (옵션) 리더보드 HF 저장소 백업: `UPLOAD_LEADERBOARD_TO_HF=true`일 때만
134
  - (옵션) 제출 이력: `src/submission_tracker.py` (ENABLE_SUBMISSION_LIMIT=true 일 때만)
135
 
136
  주의: `ENABLE_SUBMISSION_LIMIT=false`인 경우, 제출 이력 추적용 Hugging Face 저장소 접근을 시도하지 않도록 코드가 반영되어 있습니다.
app.py CHANGED
@@ -40,7 +40,8 @@ def create_interface():
40
  with gr.Tabs():
41
  # 리더보드 탭
42
  with gr.Tab("🏆 리더보드"):
43
- create_leaderboard_tab()
 
44
 
45
  # 제출 및 평가 탭
46
  with gr.Tab("📤 제출 및 평가"):
@@ -49,6 +50,13 @@ def create_interface():
49
  # 데이터셋 다운로드 탭
50
  with gr.Tab("💾 데이터셋"):
51
  create_dataset_tab()
 
 
 
 
 
 
 
52
 
53
  return app
54
 
 
40
  with gr.Tabs():
41
  # 리더보드 탭
42
  with gr.Tab("🏆 리더보드"):
43
+ # ✅ 리더보드 컴포넌트와 새로고침 함수 받아오기
44
+ relaxed_table, strict_table, refresh_leaderboard = create_leaderboard_tab()
45
 
46
  # 제출 및 평가 탭
47
  with gr.Tab("📤 제출 및 평가"):
 
50
  # 데이터셋 다운로드 탭
51
  with gr.Tab("💾 데이터셋"):
52
  create_dataset_tab()
53
+
54
+ # ✅ 앱이 로드될 때마다(사용자가 페이지 처음 열 때마다) 한 번 자동으로 새로고침
55
+ app.load(
56
+ fn=refresh_leaderboard,
57
+ inputs=None,
58
+ outputs=[relaxed_table, strict_table],
59
+ )
60
 
61
  return app
62
 
config.py CHANGED
@@ -49,6 +49,9 @@ class Config:
49
  ENABLE_SUBMISSION_LIMIT = os.getenv('ENABLE_SUBMISSION_LIMIT', 'true').lower() == 'true'
50
  SUBMISSION_TRACKER_REPO_ID = os.getenv('SUBMISSION_TRACKER_REPO_ID')
51
 
 
 
 
52
  # 환경 설정
53
  IS_HUGGINGFACE_SPACES = os.getenv("SPACE_ID") is not None
54
 
 
49
  ENABLE_SUBMISSION_LIMIT = os.getenv('ENABLE_SUBMISSION_LIMIT', 'true').lower() == 'true'
50
  SUBMISSION_TRACKER_REPO_ID = os.getenv('SUBMISSION_TRACKER_REPO_ID')
51
 
52
+ # 리더보드 HF 업로드 설정
53
+ UPLOAD_LEADERBOARD_TO_HF = os.getenv('UPLOAD_LEADERBOARD_TO_HF', 'true').lower() == 'true'
54
+
55
  # 환경 설정
56
  IS_HUGGINGFACE_SPACES = os.getenv("SPACE_ID") is not None
57
 
env.example CHANGED
@@ -40,6 +40,17 @@ SUBMISSION_TRACKER_REPO_ID=james-demo-leaderboard-backend/submission-tracker
40
  # - false: 제출 제한 기능 비활성화 (로컬 테스트용)
41
  ENABLE_SUBMISSION_LIMIT=true
42
 
 
 
 
 
 
 
 
 
 
 
 
43
  # ===========================================
44
  # AI 평가 API 설정
45
  # ===========================================
 
40
  # - false: 제출 제한 기능 비활성화 (로컬 테스트용)
41
  ENABLE_SUBMISSION_LIMIT=true
42
 
43
+ # ===========================================
44
+ # 리더보드 저장 설정
45
+ # ===========================================
46
+
47
+ # 리더보드를 HuggingFace private dataset에도 저장할지 여부
48
+ # - true : 로컬 CSV 저장 + HF dataset에도 업로드 (권장: 운영/배포 환경)
49
+ # - false: 로컬 CSV에만 저장 (권장: 로컬 개발 환경)
50
+ UPLOAD_LEADERBOARD_TO_HF=false
51
+
52
+ # (참고) 리더보드는 기준 데이터와 동일한 Repository(FRESHQA_DATA_REPO_ID)에 leaderboard_results.csv 파일명으로 저장됩니다.
53
+
54
  # ===========================================
55
  # AI 평가 API 설정
56
  # ===========================================
src/leaderboard_manager.py CHANGED
@@ -1,228 +1,346 @@
1
  """
2
  리더보드 관리 모듈
3
  리더보드 데이터의 로드, 저장, 표시 준비를 담당합니다.
 
 
 
 
 
 
 
4
  """
5
 
6
- import pandas as pd
7
  import os
 
 
 
 
 
 
 
 
8
  from src.utils import file_lock
9
 
10
- def load_leaderboard_data():
11
- """리더보드 데이터 로드"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  try:
13
- # 프로젝트 루트에서 data 디렉토리 찾기
14
- current_dir = os.path.dirname(os.path.abspath(__file__)) # src/ 폴더
15
- project_root = os.path.dirname(current_dir) # 프로젝트 루트
16
- data_path = os.path.join(project_root, 'data', 'leaderboard_results.csv')
17
  df = pd.read_csv(data_path)
18
-
19
- # 기존 데이터에 evaluation_mode 컬럼이 없으면 추가
20
- if 'evaluation_mode' not in df.columns:
21
- df['evaluation_mode'] = 'Unknown'
22
-
23
- text_columns = ['model', 'description']
24
- for col in text_columns:
25
- if col not in df.columns:
26
- df[col] = pd.Series(dtype='object')
27
-
28
-
29
- # 새로운 상세 분석 컬럼들이 없으면 추가
30
- detailed_columns = [
31
- 'acc_test', 'acc_dev', 'acc_vp', 'acc_fp', 'acc_vp_one_hop', 'acc_vp_two_hop',
32
- 'acc_fp_one_hop', 'acc_fp_two_hop', 'acc_vp_old', 'acc_vp_new', 'acc_fp_old', 'acc_fp_new'
33
- ]
34
-
35
- for col in detailed_columns:
36
- if col not in df.columns:
37
- df[col] = 0.0
38
-
39
- # 도메인별 정확도 컬럼들이 없으면 추가 (freshqa_acc.py와 일치)
40
- domain_columns = [
41
- 'acc_politics', 'acc_sports', 'acc_entertainment',
42
- 'acc_weather', 'acc_world', 'acc_economy',
43
- 'acc_society', 'acc_it_science', 'acc_life_culture', 'acc_unknown'
44
- ]
45
-
46
- for col in domain_columns:
47
- if col not in df.columns:
48
- df[col] = 0.0
49
-
50
- # accuracy 기준으로 정렬 (랭킹 기준) - 빈 데이터프레임이 아닐 때만
51
- if not df.empty and 'accuracy' in df.columns:
52
- df = df.sort_values('accuracy', ascending=False).reset_index(drop=True)
53
-
54
- # rank 컬럼은 저장하지 않고 표시 시에만 계산
55
- # 숫자 컬럼들은 원본 그대로 저장 (반올림하지 않음)
56
-
57
- # 컬럼 순서를 헤더와 맞춰서 정렬 (rank 제외)
58
- column_order = [
59
- 'id', 'model', 'description', 'accuracy', 'fast_changing_accuracy',
60
- 'slow_changing_accuracy', 'never_changing_accuracy', 'acc_vp', 'acc_fp',
61
- 'acc_vp_one_hop', 'acc_vp_two_hop', 'acc_fp_one_hop', 'acc_fp_two_hop',
62
- 'acc_vp_old', 'acc_vp_new', 'acc_fp_old', 'acc_fp_new',
63
- 'acc_politics', 'acc_sports', 'acc_entertainment', 'acc_weather',
64
- 'acc_world', 'acc_economy', 'acc_society', 'acc_it_science',
65
- 'acc_life_culture', 'acc_unknown', 'total_questions', 'evaluation_date', 'evaluation_mode'
66
- ]
67
-
68
- # 존재하는 컬럼만 선택하여 순서대로 정렬
69
- available_columns = [col for col in column_order if col in df.columns]
70
- df = df[available_columns]
71
-
72
- return df
73
  except FileNotFoundError:
74
- # 초기 데이터 (rank 제외)
75
- return pd.DataFrame({
76
- 'id': [],
77
- 'model': [],
78
- 'description': [],
79
- 'accuracy': [],
80
- 'fast_changing_accuracy': [],
81
- 'slow_changing_accuracy': [],
82
- 'never_changing_accuracy': [],
83
- 'acc_vp': [],
84
- 'acc_fp': [],
85
- 'acc_vp_one_hop': [],
86
- 'acc_vp_two_hop': [],
87
- 'acc_fp_one_hop': [],
88
- 'acc_fp_two_hop': [],
89
- 'acc_vp_old': [],
90
- 'acc_vp_new': [],
91
- 'acc_fp_old': [],
92
- 'acc_fp_new': [],
93
- 'acc_politics': [],
94
- 'acc_sports': [],
95
- 'acc_entertainment': [],
96
- 'acc_weather': [],
97
- 'acc_world': [],
98
- 'acc_economy': [],
99
- 'acc_society': [],
100
- 'acc_it_science': [],
101
- 'acc_life_culture': [],
102
- 'acc_unknown': [],
103
- 'total_questions': [],
104
- 'evaluation_date': [],
105
- 'evaluation_mode': []
106
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  def append_to_leaderboard_data(new_data_list):
109
- """리더보드 데이터에 새로운 결과 추가 (파일 잠금 사용)"""
110
- current_dir = os.path.dirname(os.path.abspath(__file__)) # src/ 폴더
111
- project_root = os.path.dirname(current_dir) # 프로젝트 루트
112
- data_path = os.path.join(project_root, 'data', 'leaderboard_results.csv')
113
-
114
- # 파일 잠금을 사용하여 안전하게 읽기 -> 수정 -> 쓰기
115
- with file_lock(data_path + '.lock'):
116
- # 파일이 존재하면 읽기
 
 
 
 
117
  if os.path.exists(data_path):
118
- existing_df = pd.read_csv(data_path)
119
- for col in ['model', 'description']:
120
- if col not in existing_df.columns:
121
- existing_df[col] = pd.Series(dtype='object')
 
122
  else:
123
- # 파일이 없으면 빈 DataFrame 생성
124
- existing_df = load_leaderboard_data() # 초기 스키마 반환
125
-
126
- # 새로운 데이터 추가
 
127
  new_df = pd.DataFrame(new_data_list)
 
 
128
 
129
- # FutureWarning 방지: 빈 DataFrame은 제외하고 결합
130
  frames_to_concat = []
131
- if isinstance(existing_df, pd.DataFrame) and not existing_df.empty:
132
  frames_to_concat.append(existing_df)
133
- if isinstance(new_df, pd.DataFrame) and not new_df.empty:
134
  frames_to_concat.append(new_df)
135
-
136
  if len(frames_to_concat) == 0:
137
- # 둘 다 비어있으면 기존 스키마 유지
138
  combined_df = existing_df.copy()
139
  elif len(frames_to_concat) == 1:
140
  combined_df = frames_to_concat[0].copy()
141
  else:
142
  combined_df = pd.concat(frames_to_concat, ignore_index=True)
143
-
144
- # 정렬 (accuracy 기준)
145
- if not combined_df.empty and 'accuracy' in combined_df.columns:
146
- combined_df = combined_df.sort_values('accuracy', ascending=False).reset_index(drop=True)
147
-
148
- desired_order = [
149
- 'id', 'model', 'description', 'accuracy', 'fast_changing_accuracy',
150
- 'slow_changing_accuracy', 'never_changing_accuracy', 'acc_vp', 'acc_fp',
151
- 'acc_vp_one_hop', 'acc_vp_two_hop', 'acc_fp_one_hop', 'acc_fp_two_hop',
152
- 'acc_vp_old', 'acc_vp_new', 'acc_fp_old', 'acc_fp_new',
153
- 'acc_politics', 'acc_sports', 'acc_entertainment', 'acc_weather',
154
- 'acc_world', 'acc_economy', 'acc_society', 'acc_it_science',
155
- 'acc_life_culture', 'acc_unknown', 'total_questions', 'evaluation_date', 'evaluation_mode'
156
- ]
157
- combined_df = combined_df.reindex(columns=[col for col in desired_order if col in combined_df.columns])
158
-
159
- # 저장
160
- combined_df.to_csv(data_path, index=False)
161
-
162
- return combined_df
163
-
164
- def prepare_display_data(df, global_ranking=None):
165
- """테이블 표시용 데이터 준비 (rank 계산 및 반올림 적용)"""
166
- # 빈 데이터프레임인 경우 그대로 반환
167
- if df.empty:
168
- return df
169
-
170
- display_df = df.copy()
171
- if 'model' in display_df.columns:
172
- display_df['model'] = display_df['model'].fillna('Anonymous Model')
173
- display_df['model'] = display_df['model'].replace('', 'Anonymous Model')
174
- if 'description' in display_df.columns:
175
- display_df['description'] = display_df['description'].replace({None: '', pd.NA: ''}).fillna('')
176
-
177
- # rank 컬럼 추가
178
- if 'accuracy' in display_df.columns:
179
- if global_ranking is not None:
180
- # 전체 랭킹 정보가 제공된 경우 사용
181
- display_df['rank'] = display_df.index.map(global_ranking)
182
- else:
183
- # 전체 랭킹 정보가 없는 경우 accuracy 기준으로 정렬하여 rank 계산
184
- display_df = display_df.sort_values('accuracy', ascending=False).reset_index(drop=True)
185
-
186
- # rank 컬럼 추가 (1~3위는 아이콘, 나머지는 숫자)
187
- def get_rank_display(rank):
188
- if rank == 1:
189
- return "🥇"
190
- elif rank == 2:
191
- return "🥈"
192
- elif rank == 3:
193
- return "🥉"
194
- else:
195
- return str(rank)
196
-
197
- display_df['rank'] = [get_rank_display(i+1) for i in range(len(display_df))]
198
-
199
- # 숫자 컬럼들을 소숫점 2번째에서 반올림 (표시용으로만)
200
- numeric_columns = [
201
- 'accuracy', 'fast_changing_accuracy', 'slow_changing_accuracy', 'never_changing_accuracy',
202
- 'acc_vp', 'acc_fp', 'acc_vp_one_hop', 'acc_vp_two_hop', 'acc_fp_one_hop', 'acc_fp_two_hop',
203
- 'acc_vp_old', 'acc_vp_new', 'acc_fp_old', 'acc_fp_new',
204
- 'acc_politics', 'acc_sports', 'acc_entertainment', 'acc_weather',
205
- 'acc_world', 'acc_economy', 'acc_society', 'acc_it_science',
206
- 'acc_life_culture', 'acc_unknown'
207
- ]
208
-
209
- for col in numeric_columns:
210
- if col in display_df.columns:
211
- display_df[col] = display_df[col].round(2)
212
-
213
- # 컬럼 순서 재정렬 (rank를 맨 앞에)
214
- column_order = [
215
- 'rank', 'id', 'model', 'description', 'accuracy', 'fast_changing_accuracy',
216
- 'slow_changing_accuracy', 'never_changing_accuracy', 'acc_vp', 'acc_fp',
217
- 'acc_vp_one_hop', 'acc_vp_two_hop', 'acc_fp_one_hop', 'acc_fp_two_hop',
218
- 'acc_vp_old', 'acc_vp_new', 'acc_fp_old', 'acc_fp_new',
219
- 'acc_politics', 'acc_sports', 'acc_entertainment', 'acc_weather',
220
- 'acc_world', 'acc_economy', 'acc_society', 'acc_it_science',
221
- 'acc_life_culture', 'acc_unknown', 'total_questions', 'evaluation_date', 'evaluation_mode'
222
- ]
223
-
224
- # 존재하는 컬럼만 선택하여 순서대로 정렬
225
- available_columns = [col for col in column_order if col in display_df.columns]
226
- display_df = display_df[available_columns]
227
-
228
- return display_df
 
1
  """
2
  리더보드 관리 모듈
3
  리더보드 데이터의 로드, 저장, 표시 준비를 담당합니다.
4
+
5
+ - 로컬 CSV: 프로젝트 루트의 data/leaderboard_results.csv
6
+ - 선택적 HF 연동:
7
+ - repo_id: Config.FRESHQA_DATA_REPO_ID
8
+ - token : Config.HF_TOKEN
9
+ - 파일명 : leaderboard_results.csv (repo 루트)
10
+ - Config.UPLOAD_LEADERBOARD_TO_HF == True 일 때만 HF를 읽고/쓴다.
11
  """
12
 
 
13
  import os
14
+ import time
15
+ import tempfile
16
+ from typing import Optional
17
+
18
+ import pandas as pd
19
+ from huggingface_hub import HfApi, hf_hub_download
20
+
21
+ from config import Config
22
  from src.utils import file_lock
23
 
24
+
25
+ # -------------------------
26
+ # 상수 및 설정
27
+ # -------------------------
28
+
29
+ HF_LEADERBOARD_FILENAME = "leaderboard_results.csv" # HF dataset 내 파일명 (루트)
30
+ LOCAL_LEADERBOARD_FILENAME = "leaderboard_results.csv" # 로컬 data 폴더 내 파일명 (기존 유지)
31
+
32
+ HF_REPO_ID = Config.FRESHQA_DATA_REPO_ID
33
+ HF_ADMIN_TOKEN = Config.HF_TOKEN
34
+ UPLOAD_LEADERBOARD_TO_HF = Config.UPLOAD_LEADERBOARD_TO_HF
35
+
36
+ hf_api = HfApi()
37
+
38
+
39
+ # -------------------------
40
+ # 경로/초기 스키마/정규화 헬퍼
41
+ # -------------------------
42
+
43
+ def _get_local_leaderboard_path() -> str:
44
+ """프로젝트 루트 기준 로컬 리더보드 CSV 경로 반환."""
45
+ current_dir = os.path.dirname(os.path.abspath(__file__)) # src/ 폴더
46
+ project_root = os.path.dirname(current_dir) # 프로젝트 루트
47
+ return os.path.join(project_root, "data", LOCAL_LEADERBOARD_FILENAME)
48
+
49
+
50
+ def _init_empty_leaderboard_df() -> pd.DataFrame:
51
+ """초기 빈 리더보드 스키마 DataFrame."""
52
+ return pd.DataFrame({
53
+ "id": [],
54
+ "model": [],
55
+ "description": [],
56
+ "accuracy": [],
57
+ "fast_changing_accuracy": [],
58
+ "slow_changing_accuracy": [],
59
+ "never_changing_accuracy": [],
60
+ "acc_vp": [],
61
+ "acc_fp": [],
62
+ "acc_vp_one_hop": [],
63
+ "acc_vp_two_hop": [],
64
+ "acc_fp_one_hop": [],
65
+ "acc_fp_two_hop": [],
66
+ "acc_vp_old": [],
67
+ "acc_vp_new": [],
68
+ "acc_fp_old": [],
69
+ "acc_fp_new": [],
70
+ "acc_politics": [],
71
+ "acc_sports": [],
72
+ "acc_entertainment": [],
73
+ "acc_weather": [],
74
+ "acc_world": [],
75
+ "acc_economy": [],
76
+ "acc_society": [],
77
+ "acc_it_science": [],
78
+ "acc_life_culture": [],
79
+ "acc_unknown": [],
80
+ "total_questions": [],
81
+ "evaluation_date": [],
82
+ "evaluation_mode": [],
83
+ })
84
+
85
+
86
+ def _normalize_leaderboard_df(df: pd.DataFrame) -> pd.DataFrame:
87
+ """
88
+ 리더보드 DF를 스키마/정렬/컬럼 순서 기준에 맞춰 정규화한다.
89
+ (기존 load_leaderboard_data의 로직을 함수로 분리)
90
+ """
91
+ if df is None or df.empty:
92
+ return _init_empty_leaderboard_df()
93
+
94
+ df = df.copy()
95
+
96
+ # evaluation_mode가 없으면 추가
97
+ if "evaluation_mode" not in df.columns:
98
+ df["evaluation_mode"] = "Unknown"
99
+
100
+ # 텍스트 컬럼 보정
101
+ text_columns = ["model", "description"]
102
+ for col in text_columns:
103
+ if col not in df.columns:
104
+ df[col] = pd.Series(dtype="object")
105
+
106
+ # 상세 분석 컬럼 없으면 추가
107
+ detailed_columns = [
108
+ "acc_test", "acc_dev", "acc_vp", "acc_fp", "acc_vp_one_hop", "acc_vp_two_hop",
109
+ "acc_fp_one_hop", "acc_fp_two_hop", "acc_vp_old", "acc_vp_new", "acc_fp_old", "acc_fp_new",
110
+ ]
111
+ for col in detailed_columns:
112
+ if col not in df.columns:
113
+ df[col] = 0.0
114
+
115
+ # 도메인별 정확도 컬럼 없으면 추가
116
+ domain_columns = [
117
+ "acc_politics", "acc_sports", "acc_entertainment",
118
+ "acc_weather", "acc_world", "acc_economy",
119
+ "acc_society", "acc_it_science", "acc_life_culture", "acc_unknown",
120
+ ]
121
+ for col in domain_columns:
122
+ if col not in df.columns:
123
+ df[col] = 0.0
124
+
125
+ # accuracy 기준 정렬
126
+ if "accuracy" in df.columns and not df.empty:
127
+ df = df.sort_values("accuracy", ascending=False).reset_index(drop=True)
128
+
129
+ # 컬럼 순서 정렬 (rank 제외)
130
+ column_order = [
131
+ "id", "model", "description", "accuracy", "fast_changing_accuracy",
132
+ "slow_changing_accuracy", "never_changing_accuracy", "acc_vp", "acc_fp",
133
+ "acc_vp_one_hop", "acc_vp_two_hop", "acc_fp_one_hop", "acc_fp_two_hop",
134
+ "acc_vp_old", "acc_vp_new", "acc_fp_old", "acc_fp_new",
135
+ "acc_politics", "acc_sports", "acc_entertainment", "acc_weather",
136
+ "acc_world", "acc_economy", "acc_society", "acc_it_science",
137
+ "acc_life_culture", "acc_unknown", "total_questions",
138
+ "evaluation_date", "evaluation_mode",
139
+ ]
140
+ available_columns = [col for col in column_order if col in df.columns]
141
+ df = df[available_columns]
142
+
143
+ return df
144
+
145
+
146
+ def _load_local_leaderboard_df() -> pd.DataFrame:
147
+ """로컬 CSV에서 리더보드 로드 (없으면 빈 ��키마)."""
148
+ data_path = _get_local_leaderboard_path()
149
  try:
 
 
 
 
150
  df = pd.read_csv(data_path)
151
+ return _normalize_leaderboard_df(df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  except FileNotFoundError:
153
+ return _init_empty_leaderboard_df()
154
+ except Exception as e:
155
+ print(f"⚠️ 로컬 리더보드 로드 실패: {e}")
156
+ return _init_empty_leaderboard_df()
157
+
158
+
159
+ # -------------------------
160
+ # HF 연동 헬퍼
161
+ # -------------------------
162
+
163
+ def _can_use_hf() -> bool:
164
+ """HF 연동이 가능한 상태인지 여부 (Config 기반)."""
165
+ if not UPLOAD_LEADERBOARD_TO_HF:
166
+ return False
167
+ if not HF_REPO_ID or not HF_ADMIN_TOKEN:
168
+ # 설정이 없으면 HF는 건너뜀
169
+ return False
170
+ return True
171
+
172
+
173
+ def _load_leaderboard_from_hf(retries: int = 3, delay: float = 1.0) -> Optional[pd.DataFrame]:
174
+ """
175
+ HF dataset에서 리더보드 CSV를 다운로드하여 DataFrame으로 반환.
176
+ 실패 시 None 반환. 재시도 로직 포함.
177
+ """
178
+ if not _can_use_hf():
179
+ return None
180
+
181
+ last_err: Optional[Exception] = None
182
+ for attempt in range(1, retries + 1):
183
+ try:
184
+ with tempfile.TemporaryDirectory() as tmpdir:
185
+ file_path = hf_hub_download(
186
+ repo_id=HF_REPO_ID,
187
+ filename=HF_LEADERBOARD_FILENAME,
188
+ repo_type="dataset",
189
+ local_dir=tmpdir,
190
+ token=HF_ADMIN_TOKEN,
191
+ )
192
+ df = pd.read_csv(file_path)
193
+ return _normalize_leaderboard_df(df)
194
+ except Exception as e:
195
+ last_err = e
196
+ print(f"⚠️ HF 리더보드 로드 실패 (시도 {attempt}/{retries}): {e}")
197
+ if attempt < retries:
198
+ time.sleep(delay)
199
+ delay *= 2
200
+ print("❌ HF 리더보드 로드 재시도 모두 실패")
201
+ return None
202
+
203
+
204
+ def _save_leaderboard_to_hf(df: pd.DataFrame, retries: int = 3, delay: float = 1.0) -> bool:
205
+ """
206
+ HF dataset에 리더보드 CSV 업로드.
207
+ 실패 시 False 반환. 재시도 로직 포함.
208
+ """
209
+ if not _can_use_hf():
210
+ return False
211
+
212
+ df = _normalize_leaderboard_df(df)
213
+
214
+ last_err: Optional[Exception] = None
215
+ for attempt in range(1, retries + 1):
216
+ try:
217
+ with tempfile.NamedTemporaryFile(
218
+ mode="w",
219
+ encoding="utf-8",
220
+ suffix=".csv",
221
+ delete=False,
222
+ ) as tmpfile:
223
+ df.to_csv(tmpfile.name, index=False)
224
+ tmp_path = tmpfile.name
225
+
226
+ hf_api.upload_file(
227
+ path_or_fileobj=tmp_path,
228
+ path_in_repo=HF_LEADERBOARD_FILENAME,
229
+ repo_id=HF_REPO_ID,
230
+ repo_type="dataset",
231
+ token=HF_ADMIN_TOKEN,
232
+ commit_message="Update leaderboard results",
233
+ )
234
+
235
+ os.unlink(tmp_path)
236
+ return True
237
+
238
+ except Exception as e:
239
+ last_err = e
240
+ print(f"⚠️ HF 리더보드 업로드 실패 (시도 {attempt}/{retries}): {e}")
241
+ if attempt < retries:
242
+ time.sleep(delay)
243
+ delay *= 2
244
+
245
+ print(f"❌ HF 리더보드 업로드 재시도 모두 실패: {last_err}")
246
+ return False
247
+
248
+
249
+ # -------------------------
250
+ # 공개 API: 로드 / 추가
251
+ # -------------------------
252
+
253
+ def load_leaderboard_data() -> pd.DataFrame:
254
+ """
255
+ 리더보드 데이터 로드.
256
+
257
+ 동작 우선순위:
258
+ 1) Config.UPLOAD_LEADERBOARD_TO_HF == True && HF 설정 OK:
259
+ - HF에서 최신 리더보드 로드 시도
260
+ - 성공 시: 그 내용을 로컬 CSV에 덮어쓴 뒤 반환
261
+ - 실패 시: 로컬 CSV를 사용 (없으면 빈 스키마)
262
+ 2) 그 외:
263
+ - 로컬 CSV만 사용 (없으면 빈 스키마)
264
+ """
265
+ data_path = _get_local_leaderboard_path()
266
+ lock_path = data_path + ".lock"
267
+
268
+ # HF를 사용할 수 있는 경우에만 HF 우선 시도
269
+ if _can_use_hf():
270
+ with file_lock(lock_path):
271
+ hf_df = _load_leaderboard_from_hf()
272
+ if hf_df is not None:
273
+ # HF가 소스 오브 트루스: 로컬 CSV도 HF 기준으로 동기화
274
+ try:
275
+ os.makedirs(os.path.dirname(data_path), exist_ok=True)
276
+ hf_df.to_csv(data_path, index=False)
277
+ except Exception as e:
278
+ print(f"⚠️ 로컬 리더보드 동기화 실패: {e}")
279
+ return hf_df
280
+
281
+ # HF에서 못 가져오면 로컬로 폴백
282
+ local_df = _load_local_leaderboard_df()
283
+ return local_df
284
+
285
+ # HF를 사용하지 않는 경우: 로컬만
286
+ return _load_local_leaderboard_df()
287
+
288
 
289
  def append_to_leaderboard_data(new_data_list):
290
+ """
291
+ 리더보드 데이터에 새로운 결과 추가 (파일 잠금 사용).
292
+
293
+ - 항상 로컬 CSV를 업데이트
294
+ - Config.UPLOAD_LEADERBOARD_TO_HF == True 이고 HF 설정이 유효하면,
295
+ 업데이트된 전체 DF를 HF에도 업로드 (재시도 포함).
296
+ """
297
+ data_path = _get_local_leaderboard_path()
298
+ lock_path = data_path + ".lock"
299
+
300
+ with file_lock(lock_path):
301
+ # 1) 로컬 기존 데이터 로드
302
  if os.path.exists(data_path):
303
+ try:
304
+ existing_df = pd.read_csv(data_path)
305
+ except Exception as e:
306
+ print(f"⚠️ 로컬 리더보드 읽��� 실패, 빈 스키마로 진행: {e}")
307
+ existing_df = _init_empty_leaderboard_df()
308
  else:
309
+ existing_df = _init_empty_leaderboard_df()
310
+
311
+ existing_df = _normalize_leaderboard_df(existing_df)
312
+
313
+ # 2) 새로운 데이터 추가
314
  new_df = pd.DataFrame(new_data_list)
315
+ if not new_df.empty:
316
+ new_df = _normalize_leaderboard_df(new_df)
317
 
 
318
  frames_to_concat = []
319
+ if not existing_df.empty:
320
  frames_to_concat.append(existing_df)
321
+ if not new_df.empty:
322
  frames_to_concat.append(new_df)
323
+
324
  if len(frames_to_concat) == 0:
 
325
  combined_df = existing_df.copy()
326
  elif len(frames_to_concat) == 1:
327
  combined_df = frames_to_concat[0].copy()
328
  else:
329
  combined_df = pd.concat(frames_to_concat, ignore_index=True)
330
+
331
+ combined_df = _normalize_leaderboard_df(combined_df)
332
+
333
+ # 3) 로컬 저장
334
+ try:
335
+ os.makedirs(os.path.dirname(data_path), exist_ok=True)
336
+ combined_df.to_csv(data_path, index=False)
337
+ except Exception as e:
338
+ print(f"❌ 로컬 리더보드 저장 실패: {e}")
339
+
340
+ # 4) HF에도 업로드 (옵션)
341
+ if _can_use_hf():
342
+ ok = _save_leaderboard_to_hf(combined_df)
343
+ if not ok:
344
+ print("⚠️ 리더보드 HF 업로드 실패 (로컬에는 저장됨)")
345
+
346
+ return combined_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui/leaderboard_tab.py CHANGED
@@ -6,7 +6,7 @@
6
 
7
  import gradio as gr
8
  import pandas as pd
9
- from src.leaderboard_manager import load_leaderboard_data, prepare_display_data
10
 
11
 
12
  def create_leaderboard_tab():
@@ -91,6 +91,83 @@ def create_leaderboard_tab():
91
  'acc_life_culture': 'Life/Culture',
92
  'acc_unknown': 'Unknown'
93
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  def format_leaderboard(df: pd.DataFrame) -> pd.DataFrame:
96
  """리더보드에 노출할 컬럼 선택 및 헤더명 변환"""
@@ -122,6 +199,7 @@ def create_leaderboard_tab():
122
  is_empty = relaxed_df.empty and strict_df.empty
123
  return formatted_relaxed, formatted_strict, is_empty
124
 
 
125
  leaderboard_data = load_leaderboard_data()
126
  relaxed_initial, strict_initial, is_initial_empty = build_leaderboard_state(leaderboard_data)
127
 
@@ -166,7 +244,6 @@ def create_leaderboard_tab():
166
  """)
167
 
168
 
169
-
170
  # 통합 검색 필터 함수 (Relaxed와 Strict 모드 모두 필터링)
171
  def filter_leaderboard_data(search_text):
172
  """Relaxed와 Strict 모드 리더보드 데이터 필터링 (CSV 기반)"""
@@ -216,7 +293,6 @@ def create_leaderboard_tab():
216
  try:
217
  all_df = load_leaderboard_data()
218
  formatted_relaxed, formatted_strict, is_empty = build_leaderboard_state(all_df)
219
-
220
  return formatted_relaxed, formatted_strict
221
  except Exception as e:
222
  print(f"❌ 리더보드 새로고침 실패: {e}")
@@ -227,3 +303,6 @@ def create_leaderboard_tab():
227
  fn=refresh_leaderboard,
228
  outputs=[relaxed_leaderboard_table, strict_leaderboard_table]
229
  )
 
 
 
 
6
 
7
  import gradio as gr
8
  import pandas as pd
9
+ from src.leaderboard_manager import load_leaderboard_data
10
 
11
 
12
  def create_leaderboard_tab():
 
91
  'acc_life_culture': 'Life/Culture',
92
  'acc_unknown': 'Unknown'
93
  }
94
+
95
+ def prepare_display_data(df: pd.DataFrame, global_ranking=None) -> pd.DataFrame:
96
+ """테이블 표시용 데이터 준비 (rank 계산 및 반올림 적용)"""
97
+ # 빈 데이터프레임인 경우 그대로 반환
98
+ if df is None or df.empty:
99
+ return df if df is not None else pd.DataFrame()
100
+
101
+ display_df = df.copy()
102
+
103
+ # model / description 기본값 처리
104
+ if "model" in display_df.columns:
105
+ display_df["model"] = display_df["model"].fillna("Anonymous Model")
106
+ display_df["model"] = display_df["model"].replace("", "Anonymous Model")
107
+ if "description" in display_df.columns:
108
+ display_df["description"] = (
109
+ display_df["description"]
110
+ .replace({None: "", pd.NA: ""})
111
+ .fillna("")
112
+ )
113
+
114
+ # rank 컬럼 추가
115
+ if "accuracy" in display_df.columns:
116
+ if global_ranking is not None:
117
+ # 외부에서 전체 랭킹 정보를 제공하는 경우
118
+ display_df["rank"] = display_df.index.map(global_ranking)
119
+ else:
120
+ # accuracy 기준으로 정렬하여 rank 계산
121
+ display_df = display_df.sort_values("accuracy", ascending=False).reset_index(
122
+ drop=True
123
+ )
124
+
125
+ def get_rank_display(rank: int) -> str:
126
+ if rank == 1:
127
+ return "🥇"
128
+ elif rank == 2:
129
+ return "🥈"
130
+ elif rank == 3:
131
+ return "🥉"
132
+ else:
133
+ return str(rank)
134
+
135
+ display_df["rank"] = [get_rank_display(i + 1) for i in range(len(display_df))]
136
+
137
+ # 숫자 컬럼들을 소숫점 2번째에서 반올림 (표시용으로만)
138
+ numeric_columns = [
139
+ "accuracy",
140
+ "fast_changing_accuracy",
141
+ "slow_changing_accuracy",
142
+ "never_changing_accuracy",
143
+ "acc_vp",
144
+ "acc_fp",
145
+ "acc_vp_one_hop",
146
+ "acc_vp_two_hop",
147
+ "acc_fp_one_hop",
148
+ "acc_fp_two_hop",
149
+ "acc_vp_old",
150
+ "acc_vp_new",
151
+ "acc_fp_old",
152
+ "acc_fp_new",
153
+ "acc_politics",
154
+ "acc_sports",
155
+ "acc_entertainment",
156
+ "acc_weather",
157
+ "acc_world",
158
+ "acc_economy",
159
+ "acc_society",
160
+ "acc_it_science",
161
+ "acc_life_culture",
162
+ "acc_unknown",
163
+ ]
164
+
165
+ for col in numeric_columns:
166
+ if col in display_df.columns:
167
+ display_df[col] = display_df[col].round(2)
168
+
169
+ return display_df
170
+
171
 
172
  def format_leaderboard(df: pd.DataFrame) -> pd.DataFrame:
173
  """리더보드에 노출할 컬럼 선택 및 헤더명 변환"""
 
199
  is_empty = relaxed_df.empty and strict_df.empty
200
  return formatted_relaxed, formatted_strict, is_empty
201
 
202
+ # ✅ 초기 값 (앱 빌드 시점 기준)
203
  leaderboard_data = load_leaderboard_data()
204
  relaxed_initial, strict_initial, is_initial_empty = build_leaderboard_state(leaderboard_data)
205
 
 
244
  """)
245
 
246
 
 
247
  # 통합 검색 필터 함수 (Relaxed와 Strict 모드 모두 필터링)
248
  def filter_leaderboard_data(search_text):
249
  """Relaxed와 Strict 모드 리더보드 데이터 필터링 (CSV 기반)"""
 
293
  try:
294
  all_df = load_leaderboard_data()
295
  formatted_relaxed, formatted_strict, is_empty = build_leaderboard_state(all_df)
 
296
  return formatted_relaxed, formatted_strict
297
  except Exception as e:
298
  print(f"❌ 리더보드 새로고침 실패: {e}")
 
303
  fn=refresh_leaderboard,
304
  outputs=[relaxed_leaderboard_table, strict_leaderboard_table]
305
  )
306
+
307
+ # ✅ app.py에서 초기 로딩 시에도 재사용할 수 있도록 return
308
+ return relaxed_leaderboard_table, strict_leaderboard_table, refresh_leaderboard