"""리더보드 컬럼 규칙. 벤치마크 컬럼은 `<데이터셋>_<카테고리>` 명명 규칙(예: Coupang_Fire, Soil_v2_Smoke, AVG_Falldown)을 따른다. 이 규칙으로 df.columns 만 보고 카테고리/데이터셋 축을 자동 분류하고 표시/숨김 컬럼을 계산한다 — 신규 벤치마크가 등록되어도 config 목록을 손볼 필요가 없다 (기존 ON_LOAD_COLUMNS/HIDE_COLUMNS 대체). """ from typing import List, Optional, Sequence, Tuple import pandas as pd import enviroments.config as config # AVG_* 컬럼의 prefix. 데이터셋 축에서는 가상 데이터셋으로 취급한다. AVG_DATASET = "AVG" def split_benchmark_column(column: str) -> Optional[Tuple[str, str]]: """`<데이터셋>_<카테고리>` 컬럼을 (데이터셋, 카테고리)로 분해. 규칙 밖 컬럼(TASK, Model, "Model name" 등)은 None. """ if "_" not in column or " " in column: return None dataset, _, category = column.rpartition("_") if not dataset or not category: return None return dataset, category def build_hide_columns(columns: Sequence[str]) -> List[str]: """테이블에서 아예 제외할 컬럼 (숨김 카테고리 패턴 + 개별 지정).""" hidden = [] for col in columns: if col in config.HIDE_COLUMN_EXACT: hidden.append(col) continue parsed = split_benchmark_column(col) if parsed and parsed[1] in config.HIDE_CATEGORIES: hidden.append(col) return hidden def parse_axes(columns: Sequence[str]) -> Tuple[List[str], List[str]]: """df.columns → (카테고리 목록, 데이터셋 목록). 숨김 컬럼/AVG 데이터셋 제외.""" hidden = set(build_hide_columns(columns)) categories, datasets = set(), set() for col in columns: if col in hidden: continue parsed = split_benchmark_column(col) if parsed is None: continue dataset, category = parsed categories.add(category) if dataset != AVG_DATASET: datasets.add(dataset) return sorted(categories), sorted(datasets) def extra_columns(columns: Sequence[str]) -> List[str]: """벤치마크 명명 규칙 밖이면서 기본/필수 컬럼도 아닌 부가 컬럼. 예: "Datasets Used", "Infer Speed". 셀렉터의 '추가 정보' 축이 된다. """ always_on = set(config.LEADERBOARD_BASE_COLUMNS) | set(config.OFF_LOAD_COLUMNS) hidden = set(build_hide_columns(columns)) return [ col for col in columns if col not in always_on and col not in hidden and split_benchmark_column(col) is None ] def avg_categories(columns: Sequence[str]) -> List[str]: """AVG_<카테고리> 컬럼이 존재하는 카테고리 목록 (숨김 제외, df 순서).""" hidden = set(build_hide_columns(columns)) cats = [] for col in columns: if col in hidden: continue parsed = split_benchmark_column(col) if parsed and parsed[0] == AVG_DATASET: cats.append(parsed[1]) return cats def datasets_for_category(columns: Sequence[str], category: str) -> List[str]: """해당 카테고리 벤치마크(<데이터셋>_<카테고리>)를 가진 전체 데이터셋.""" out = set() for col in columns: parsed = split_benchmark_column(col) if parsed and parsed[1] == category and parsed[0] != AVG_DATASET: out.add(parsed[0]) return sorted(out) def default_avg_map(columns: Sequence[str]) -> dict: """카테고리별 AVG 계산 기준의 기본값: 정식 세트 ∩ 해당 카테고리 보유 데이터셋.""" return { cat: [ d for d in config.DEFAULT_AVG_DATASETS if d in datasets_for_category(columns, cat) ] for cat in avg_categories(columns) } def apply_avg_override(df: pd.DataFrame, avg_map: Optional[dict] = None) -> pd.DataFrame: """AVG_<카테고리> 컬럼을 카테고리별 기준 데이터셋의 행별 평균으로 대체. 구글 시트의 AVG 원본은 건드리지 않고 화면 표시값만 교체한다. - avg_map: {카테고리: [데이터셋, ...]}. 카테고리가 없거나 빈 리스트면 config.DEFAULT_AVG_DATASETS (정식 벤치마크 세트) 기준으로 계산. - 그룹 컬럼 값이 하나라도 비어 있는 모델은 빈칸 (완주한 모델만 AVG 표시). - 기준 데이터셋에 해당 카테고리 컬럼이 하나도 없으면 전체 빈칸. """ avg_map = avg_map or {} df = df.copy() for col in df.columns: parsed = split_benchmark_column(col) if parsed is None or parsed[0] != AVG_DATASET: continue category = parsed[1] selected = avg_map.get(category) or config.DEFAULT_AVG_DATASETS members = [ f"{d}_{category}" for d in selected if f"{d}_{category}" in df.columns ] if not members: df[col] = "" continue vals = df[members].apply(pd.to_numeric, errors="coerce") avg = vals.mean(axis=1) avg[vals.isna().any(axis=1)] = float("nan") df[col] = avg.map(lambda v: "" if pd.isna(v) else f"{v:.4f}") return df def compute_display_columns( columns: Sequence[str], categories: Sequence[str], datasets: Sequence[str], extras: Sequence[str] = (), ) -> List[str]: """선택된 카테고리 × 데이터셋 교집합 + 부가 컬럼의 표시 목록 (df 컬럼 순서 유지). datasets 가 비어 있으면 전체 데이터셋(AVG 포함)으로 취급. 기본/필수 컬럼(TASK, Model, Model name 등)은 항상 표시. """ selected_cats = set(categories or []) selected_ds = set(datasets or []) selected_extras = set(extras or []) always_on = set(config.LEADERBOARD_BASE_COLUMNS) | set(config.OFF_LOAD_COLUMNS) hidden = set(build_hide_columns(columns)) display = [] for col in columns: if col in hidden: continue parsed = split_benchmark_column(col) if parsed is None: if col in always_on or col in selected_extras: display.append(col) continue dataset, category = parsed if category not in selected_cats: continue if selected_ds and dataset not in selected_ds: continue display.append(col) return display