Spaces:
Runtime error
Runtime error
정상화
Browse files- app.py +168 -96
- graded_results.csv +3 -2
- output/UNKNOWN/ac_power_4135001.png +0 -0
- output/UNKNOWN/ac_power_4136001.png +0 -0
- output/UNKNOWN/ac_power_raw.png +0 -0
- output/UNKNOWN/ac_power_scaled.png +0 -0
- output/UNKNOWN/daily_yield.png +0 -0
- report.zip +2 -2
- report_UNKNOWN.zip +3 -0
- src/grader.py +193 -57
- src/utils.py +44 -0
app.py
CHANGED
|
@@ -1,136 +1,208 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
# from huggingface_hub import snapshot_download
|
| 6 |
-
#
|
| 7 |
-
# from src.about import (
|
| 8 |
-
# CITATION_BUTTON_LABEL,
|
| 9 |
-
# CITATION_BUTTON_TEXT,
|
| 10 |
-
# EVALUATION_QUEUE_TEXT,
|
| 11 |
-
# INTRODUCTION_TEXT,
|
| 12 |
-
# LLM_BENCHMARKS_TEXT,
|
| 13 |
-
# TITLE,
|
| 14 |
-
# )
|
| 15 |
-
# from src.display.css_html_js import custom_css
|
| 16 |
-
# from src.display.utils import (
|
| 17 |
-
# BENCHMARK_COLS,
|
| 18 |
-
# COLS,
|
| 19 |
-
# EVAL_COLS,
|
| 20 |
-
# EVAL_TYPES,
|
| 21 |
-
# AutoEvalColumn,
|
| 22 |
-
# ModelType,
|
| 23 |
-
# fields,
|
| 24 |
-
# WeightType,
|
| 25 |
-
# Precision
|
| 26 |
-
# )
|
| 27 |
-
# from src.envs import API, EVAL_REQUESTS_PATH, EVAL_RESULTS_PATH, QUEUE_REPO, REPO_ID, RESULTS_REPO, TOKEN
|
| 28 |
-
# from src.populate import get_evaluation_queue_df, get_leaderboard_df
|
| 29 |
-
# from src.submission.submit import add_new_eval
|
| 30 |
-
#
|
| 31 |
-
#
|
| 32 |
-
# def restart_space():
|
| 33 |
-
# API.restart_space(repo_id=REPO_ID)
|
| 34 |
-
#
|
| 35 |
-
# ### Space initialisation
|
| 36 |
-
# try:
|
| 37 |
-
# print(EVAL_REQUESTS_PATH)
|
| 38 |
-
# snapshot_download(
|
| 39 |
-
# repo_id=QUEUE_REPO, local_dir=EVAL_REQUESTS_PATH, repo_type="dataset", tqdm_class=None, etag_timeout=30, token=TOKEN
|
| 40 |
-
# )
|
| 41 |
-
# except Exception:
|
| 42 |
-
# restart_space()
|
| 43 |
-
# try:
|
| 44 |
-
# print(EVAL_RESULTS_PATH)
|
| 45 |
-
# snapshot_download(
|
| 46 |
-
# repo_id=RESULTS_REPO, local_dir=EVAL_RESULTS_PATH, repo_type="dataset", tqdm_class=None, etag_timeout=30, token=TOKEN
|
| 47 |
-
# )
|
| 48 |
-
# except Exception:
|
| 49 |
-
# restart_space()
|
| 50 |
-
#
|
| 51 |
-
#
|
| 52 |
-
#
|
| 53 |
-
# scheduler = BackgroundScheduler()
|
| 54 |
-
# scheduler.add_job(restart_space, "interval", seconds=1800)
|
| 55 |
-
# scheduler.start()
|
| 56 |
-
# demo.queue(default_concurrency_limit=40).launch()
|
| 57 |
-
|
| 58 |
|
| 59 |
import gradio as gr
|
| 60 |
import pandas as pd
|
| 61 |
-
import
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
from src.envs import RESULTS_REPO
|
| 65 |
-
from src.grader import grade #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
submission_df = pd.read_csv(file.name)
|
| 70 |
|
| 71 |
# 채점 및 리포트 생성
|
| 72 |
-
score_df, report_dir = grade(submission_df, team_id=
|
| 73 |
|
| 74 |
-
#
|
| 75 |
-
|
| 76 |
-
score_df.
|
|
|
|
| 77 |
|
| 78 |
-
#
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
# 리포트 ZIP 생성
|
| 88 |
-
report_zip = "
|
| 89 |
-
shutil.make_archive("
|
| 90 |
|
| 91 |
-
#
|
| 92 |
-
image_files
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
|
|
|
| 95 |
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
with gr.Blocks() as demo:
|
| 99 |
-
gr.Markdown("## Hackathon CSV 채점기 + 리포트 생성기")
|
| 100 |
|
| 101 |
with gr.Tabs():
|
| 102 |
with gr.Tab("평가 요청"):
|
| 103 |
with gr.Row():
|
| 104 |
with gr.Column():
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
with gr.Row():
|
| 110 |
-
submit_button = gr.Button("평가 요청")
|
| 111 |
-
with gr.Row():
|
| 112 |
-
df_output = gr.Dataframe(label="평가 지표 결과")
|
| 113 |
|
| 114 |
with gr.Column():
|
| 115 |
-
report_output = gr.File(label="리포트 ZIP 다운로드")
|
| 116 |
image_gallery = gr.Gallery(
|
| 117 |
label="Plant별 비교 그래프",
|
| 118 |
show_label=True,
|
| 119 |
-
height="auto"
|
| 120 |
)
|
| 121 |
|
| 122 |
# 업로드 → 채점 실행
|
| 123 |
submit_button.click(
|
| 124 |
fn=grade_csv,
|
| 125 |
-
inputs=csv_input,
|
| 126 |
-
outputs=[df_output, report_output, image_gallery]
|
|
|
|
| 127 |
)
|
| 128 |
|
| 129 |
with gr.Tab("리더보드"):
|
| 130 |
with gr.Row():
|
| 131 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
if __name__ == "__main__":
|
| 134 |
-
|
| 135 |
-
|
| 136 |
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import List
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
import gradio as gr
|
| 7 |
import pandas as pd
|
| 8 |
+
from huggingface_hub import snapshot_download, upload_file
|
| 9 |
+
|
| 10 |
+
# 프로젝트 구조에 맞춰 import (src.*)
|
| 11 |
+
from src.envs import RESULTS_REPO, EVAL_RESULTS_PATH
|
| 12 |
+
from src.grader import grade # grade(submission_df, team_id) -> (score_df, report_dir)
|
| 13 |
+
|
| 14 |
+
# ---------------------------
|
| 15 |
+
# 팀 비밀번호 매핑 (요청 반영)
|
| 16 |
+
# ---------------------------
|
| 17 |
+
TEAM_PWD_MAP = {
|
| 18 |
+
"02Aug#29112#h": "JN_HACK13",
|
| 19 |
+
"02Aug!86113!g": "JN_HACK14",
|
| 20 |
+
"02Aug#33114#h": "JN_HACK15",
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
# 저장 파일명 (리더보드 집계)
|
| 24 |
+
HISTORY_NAME = "graded_results.csv"
|
| 25 |
+
HISTORY_PATH = os.path.join(EVAL_RESULTS_PATH, HISTORY_NAME)
|
| 26 |
+
|
| 27 |
+
# ---------------------------
|
| 28 |
+
# 초기화: Space 재시작 대비
|
| 29 |
+
# ---------------------------
|
| 30 |
+
def init_cache() -> pd.DataFrame:
|
| 31 |
+
"""
|
| 32 |
+
- RESULTS_REPO 스냅샷을 EVAL_RESULTS_PATH로 다운로드(동기화)
|
| 33 |
+
- graded_results.csv 로드 (없으면 빈 DF 생성)
|
| 34 |
+
"""
|
| 35 |
+
os.makedirs(EVAL_RESULTS_PATH, exist_ok=True)
|
| 36 |
+
try:
|
| 37 |
+
snapshot_download(
|
| 38 |
+
repo_id=RESULTS_REPO,
|
| 39 |
+
repo_type="dataset",
|
| 40 |
+
local_dir=EVAL_RESULTS_PATH,
|
| 41 |
+
tqdm_class=None,
|
| 42 |
+
etag_timeout=30,
|
| 43 |
+
token=os.environ.get("HF_TOKEN"),
|
| 44 |
+
)
|
| 45 |
+
except Exception as e:
|
| 46 |
+
print(f"[WARN] snapshot_download failed: {e}")
|
| 47 |
+
|
| 48 |
+
if os.path.exists(HISTORY_PATH):
|
| 49 |
+
try:
|
| 50 |
+
df = pd.read_csv(HISTORY_PATH)
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"[WARN] failed to read {HISTORY_NAME}: {e}")
|
| 53 |
+
df = pd.DataFrame()
|
| 54 |
+
else:
|
| 55 |
+
df = pd.DataFrame()
|
| 56 |
+
|
| 57 |
+
# 최소 스키마 보정
|
| 58 |
+
for col in ["TEAM", "TIMESTAMP"]:
|
| 59 |
+
if col not in df.columns:
|
| 60 |
+
df[col] = []
|
| 61 |
+
return df
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# 전역 히스토리 상태 (앱 부팅 시 로드)
|
| 65 |
+
history_state = init_cache()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def save_and_upload_history(history_df: pd.DataFrame) -> None:
|
| 69 |
+
"""
|
| 70 |
+
- 로컬 graded_results.csv 저장
|
| 71 |
+
- RESULTS_REPO에 덮어쓰기 업로드
|
| 72 |
+
"""
|
| 73 |
+
history_df.to_csv(HISTORY_PATH, index=False)
|
| 74 |
+
upload_file(
|
| 75 |
+
path_or_fileobj=HISTORY_PATH,
|
| 76 |
+
path_in_repo=HISTORY_NAME,
|
| 77 |
+
repo_id=RESULTS_REPO,
|
| 78 |
+
repo_type="dataset",
|
| 79 |
+
token=os.environ.get("HF_TOKEN"),
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
|
| 83 |
+
# ---------------------------
|
| 84 |
+
# 채점 핸들러
|
| 85 |
+
# ---------------------------
|
| 86 |
+
def grade_csv(passwd: str, file):
|
| 87 |
+
global history_state
|
| 88 |
|
| 89 |
+
if file is None:
|
| 90 |
+
raise gr.Error("파일 업로드가 누락되었습니다.")
|
| 91 |
+
|
| 92 |
+
team_id = TEAM_PWD_MAP.get((passwd or "").strip(), "UNKNOWN")
|
| 93 |
+
# if not team_id:
|
| 94 |
+
# raise gr.Error("비밀번호가 올바르지 않습니다.")
|
| 95 |
+
|
| 96 |
+
# 제출 CSV 로드
|
| 97 |
submission_df = pd.read_csv(file.name)
|
| 98 |
|
| 99 |
# 채점 및 리포트 생성
|
| 100 |
+
score_df, report_dir = grade(submission_df, team_id=team_id)
|
| 101 |
|
| 102 |
+
# 메타 속성 부여
|
| 103 |
+
ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%SZ")
|
| 104 |
+
score_df.insert(0, "TEAM", team_id)
|
| 105 |
+
score_df.insert(1, "TIMESTAMP", ts)
|
| 106 |
|
| 107 |
+
# 기존 히스토리 로드(없으면 빈 DF)
|
| 108 |
+
if os.path.exists(HISTORY_PATH):
|
| 109 |
+
try:
|
| 110 |
+
saved_df = pd.read_csv(HISTORY_PATH)
|
| 111 |
+
except Exception:
|
| 112 |
+
saved_df = pd.DataFrame()
|
| 113 |
+
else:
|
| 114 |
+
saved_df = pd.DataFrame()
|
| 115 |
+
|
| 116 |
+
# 최소 스키마 통일
|
| 117 |
+
for col in set(["TEAM", "TIMESTAMP"]) - set(saved_df.columns):
|
| 118 |
+
saved_df[col] = []
|
| 119 |
+
|
| 120 |
+
# 히스토리 append
|
| 121 |
+
merged_score = pd.concat([saved_df, score_df], ignore_index=True)
|
| 122 |
+
history_state = merged_score.copy() # 전역 상태 갱신
|
| 123 |
+
|
| 124 |
+
# 로컬 저장 + Hub 업로드
|
| 125 |
+
save_and_upload_history(history_state)
|
| 126 |
|
| 127 |
# 리포트 ZIP 생성
|
| 128 |
+
report_zip = f"report_{team_id}.zip"
|
| 129 |
+
shutil.make_archive(f"report_{team_id}", "zip", report_dir)
|
| 130 |
|
| 131 |
+
# 갤러리 이미지 파일 목록
|
| 132 |
+
image_files: List[str] = [
|
| 133 |
+
os.path.join(report_dir, f)
|
| 134 |
+
for f in os.listdir(report_dir)
|
| 135 |
+
if f.lower().endswith(".png")
|
| 136 |
+
]
|
| 137 |
|
| 138 |
+
# UI 반환: 점수표, ZIP, 갤러리, 리더보드 갱신본
|
| 139 |
+
return score_df, report_zip, image_files, history_state
|
| 140 |
|
| 141 |
|
| 142 |
+
def refresh_leaderboard():
|
| 143 |
+
"""사용자 요청 시 Hub 재동기화 후 리더보드 갱신."""
|
| 144 |
+
global history_state
|
| 145 |
+
history_state = init_cache()
|
| 146 |
+
return history_state
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ---------------------------
|
| 150 |
+
# UI
|
| 151 |
+
# ---------------------------
|
| 152 |
|
| 153 |
with gr.Blocks() as demo:
|
| 154 |
+
gr.Markdown("## Hackathon CSV 채점기 + 리포트 생성기 (재시작 안전화)")
|
| 155 |
|
| 156 |
with gr.Tabs():
|
| 157 |
with gr.Tab("평가 요청"):
|
| 158 |
with gr.Row():
|
| 159 |
with gr.Column():
|
| 160 |
+
csv_input = gr.File(label="CSV 업로드", file_types=[".csv"])
|
| 161 |
+
password = gr.Textbox(label="팀 비밀번호", type="password", placeholder="팀별로 공지된 비밀번호")
|
| 162 |
+
submit_button = gr.Button("평가 요청", variant="primary")
|
| 163 |
+
df_output = gr.Dataframe(label="평가 지표 결과")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
with gr.Column():
|
| 166 |
+
report_output = gr.File(label="리포트 ZIP 다운로드", height="100px")
|
| 167 |
image_gallery = gr.Gallery(
|
| 168 |
label="Plant별 비교 그래프",
|
| 169 |
show_label=True,
|
| 170 |
+
height="auto"
|
| 171 |
)
|
| 172 |
|
| 173 |
# 업로드 → 채점 실행
|
| 174 |
submit_button.click(
|
| 175 |
fn=grade_csv,
|
| 176 |
+
inputs=[password, csv_input],
|
| 177 |
+
outputs=[df_output, report_output, image_gallery], # 마지막은 리더보드 탭의 테이블에 연결
|
| 178 |
+
queue=True
|
| 179 |
)
|
| 180 |
|
| 181 |
with gr.Tab("리더보드"):
|
| 182 |
with gr.Row():
|
| 183 |
+
leaderboard_table = gr.Dataframe(
|
| 184 |
+
value=history_state,
|
| 185 |
+
label="graded_results.csv (집계)",
|
| 186 |
+
interactive=False
|
| 187 |
+
)
|
| 188 |
+
with gr.Row():
|
| 189 |
+
refresh_btn = gr.Button("리더보드 새로고침 (Hub 동기화)")
|
| 190 |
+
|
| 191 |
+
# 새로고침: Hub → 로컬 재동기화 후 테이블 갱신
|
| 192 |
+
refresh_btn.click(
|
| 193 |
+
fn=refresh_leaderboard,
|
| 194 |
+
inputs=None,
|
| 195 |
+
outputs=leaderboard_table
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# submit_button.click의 마지막 output을 리더보드 테이블에 실제 연결
|
| 199 |
+
submit_button.click(
|
| 200 |
+
fn=lambda: history_state,
|
| 201 |
+
inputs=None,
|
| 202 |
+
outputs=leaderboard_table
|
| 203 |
+
)
|
| 204 |
|
| 205 |
if __name__ == "__main__":
|
| 206 |
+
demo.launch()
|
| 207 |
+
# demo.launch(debug=True, show_error=True, enable_monitoring=True)
|
| 208 |
|
graded_results.csv
CHANGED
|
@@ -1,2 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
| 1 |
+
TEAM,RMSE_AC,RMSE_AC_SCALED,NMAE_RANGE,NMAE_MEAN,TOTAL,TEAM_ID
|
| 2 |
+
UNKNOWN,0.0,0.0,0.0,0.0,"{'rmse': 100.0, 'nmae': np.float64(100.0), 'final': np.float64(100.0)}",
|
| 3 |
+
,493.28674800238497,0.3583109977244187,0.0,0.0,,13.0
|
output/UNKNOWN/ac_power_4135001.png
ADDED
|
output/UNKNOWN/ac_power_4136001.png
ADDED
|
output/UNKNOWN/ac_power_raw.png
ADDED
|
output/UNKNOWN/ac_power_scaled.png
ADDED
|
output/UNKNOWN/daily_yield.png
ADDED
|
report.zip
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cce5e403b7769f8611cc325afee0f004822ef3489e8a578c9ce7f6649bc07c1d
|
| 3 |
+
size 89994
|
report_UNKNOWN.zip
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:204fe86b110076e34696b62ee7cbaecf4a712703bb4c0108eb28fcd87bdcb759
|
| 3 |
+
size 223248
|
src/grader.py
CHANGED
|
@@ -1,89 +1,225 @@
|
|
| 1 |
# grader.py
|
| 2 |
import os
|
|
|
|
| 3 |
import pandas as pd
|
| 4 |
import matplotlib.pyplot as plt
|
|
|
|
| 5 |
from sklearn.preprocessing import MinMaxScaler
|
| 6 |
from sklearn.metrics import mean_absolute_error, root_mean_squared_error
|
| 7 |
|
| 8 |
-
|
| 9 |
|
|
|
|
| 10 |
|
| 11 |
-
def
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
answer = pd.read_csv(ANSWER_PATH)
|
| 14 |
-
answer = answer[['DATE_TIME', 'PLANT_ID', 'SOURCE_KEY', 'DC_POWER','AC_POWER','DAILY_YIELD']]
|
| 15 |
answer = answer.rename(columns={
|
| 16 |
'SOURCE_KEY': 'INVERTER_ID',
|
| 17 |
-
'DC_POWER': 'ANS_DC_POWER',
|
| 18 |
-
'AC_POWER': 'ANS_AC_POWER',
|
| 19 |
'DAILY_YIELD': 'ANS_DAILY_YIELD'
|
| 20 |
})
|
| 21 |
-
if 'SOURCE_KEY' in submission_df.columns:
|
| 22 |
-
submission_df = submission_df.rename(columns={"SOURCE_KEY": "INVERTER_ID"})
|
| 23 |
|
| 24 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
merged_df = pd.merge(
|
| 26 |
-
answer,
|
| 27 |
on=['DATE_TIME', 'PLANT_ID', 'INVERTER_ID'],
|
| 28 |
-
how='left'
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
#
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
metrics = {
|
|
|
|
| 42 |
"RMSE_AC": rmse_ac,
|
| 43 |
"RMSE_AC_SCALED": rmse_ac_scaled,
|
| 44 |
-
"NMAE_RANGE":
|
| 45 |
-
"NMAE_MEAN":
|
|
|
|
| 46 |
}
|
| 47 |
-
|
| 48 |
-
if 'DAILY_YIELD' in merged_df.columns:
|
| 49 |
-
mae_daily = mean_absolute_error(merged_df['ANS_DAILY_YIELD'], merged_df['DAILY_YIELD'])
|
| 50 |
-
daily_range = merged_df['ANS_DAILY_YIELD'].max() - merged_df['ANS_DAILY_YIELD'].min()
|
| 51 |
-
daily_mean = merged_df['ANS_DAILY_YIELD'].mean()
|
| 52 |
-
|
| 53 |
-
metrics["NMAE_RANGE"] = mae_daily / daily_range if daily_range != 0 else None
|
| 54 |
-
metrics["NMAE_MEAN"] = mae_daily / daily_mean if daily_mean != 0 else None
|
| 55 |
-
|
| 56 |
-
# 리포트용 결과 DF
|
| 57 |
score_df = pd.DataFrame([metrics])
|
| 58 |
|
| 59 |
-
#
|
| 60 |
-
# 리포팅
|
| 61 |
-
#
|
| 62 |
output_dir = f"output/{team_id}"
|
| 63 |
os.makedirs(output_dir, exist_ok=True)
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
)
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
)
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
return score_df, output_dir
|
| 89 |
|
|
|
|
| 1 |
# grader.py
|
| 2 |
import os
|
| 3 |
+
import math
|
| 4 |
import pandas as pd
|
| 5 |
import matplotlib.pyplot as plt
|
| 6 |
+
from typing import Tuple
|
| 7 |
from sklearn.preprocessing import MinMaxScaler
|
| 8 |
from sklearn.metrics import mean_absolute_error, root_mean_squared_error
|
| 9 |
|
| 10 |
+
from src.utils import evaluate_model # TOTAL 스코어 계산 함수
|
| 11 |
|
| 12 |
+
ANSWER_PATH = "answer.csv" # Space에 같이 넣은 정답 파일
|
| 13 |
|
| 14 |
+
def _safe_minmax(series: pd.Series) -> pd.Series:
|
| 15 |
+
"""모든 값이 같거나 전부 NaN인 경우에도 0으로 안전 스케일링."""
|
| 16 |
+
s = series.astype(float)
|
| 17 |
+
if s.notna().sum() == 0:
|
| 18 |
+
return pd.Series([float("nan")] * len(s), index=s.index)
|
| 19 |
+
val_min = s.min()
|
| 20 |
+
val_max = s.max()
|
| 21 |
+
if pd.isna(val_min) or pd.isna(val_max) or val_min == val_max:
|
| 22 |
+
# range가 0이면 전부 0.0으로(차이 정보 없음)
|
| 23 |
+
return pd.Series([0.0 if not pd.isna(v) else float("nan") for v in s], index=s.index)
|
| 24 |
+
return (s - val_min) / (val_max - val_min)
|
| 25 |
+
|
| 26 |
+
def _safe_rmse(y_true: pd.Series, y_pred: pd.Series) -> float:
|
| 27 |
+
"""NaN/무한대 방어 RMSE."""
|
| 28 |
+
df = pd.concat([y_true, y_pred], axis=1).dropna()
|
| 29 |
+
if df.shape[0] == 0:
|
| 30 |
+
return float("nan")
|
| 31 |
+
a = df.iloc[:, 0].astype(float)
|
| 32 |
+
b = df.iloc[:, 1].astype(float)
|
| 33 |
+
try:
|
| 34 |
+
return root_mean_squared_error(a, b)
|
| 35 |
+
except Exception:
|
| 36 |
+
return float("nan")
|
| 37 |
+
|
| 38 |
+
def _safe_nmae(y_true: pd.Series, y_pred: pd.Series, mode: str = "range") -> float:
|
| 39 |
+
"""
|
| 40 |
+
mode='range' -> MAE / (max(y_true) - min(y_true))
|
| 41 |
+
mode='mean' -> MAE / mean(y_true)
|
| 42 |
+
분모가 0/NaN이면 NaN 반환.
|
| 43 |
+
"""
|
| 44 |
+
df = pd.concat([y_true, y_pred], axis=1).dropna()
|
| 45 |
+
if df.shape[0] == 0:
|
| 46 |
+
return float("nan")
|
| 47 |
+
a = df.iloc[:, 0].astype(float)
|
| 48 |
+
b = df.iloc[:, 1].astype(float)
|
| 49 |
+
try:
|
| 50 |
+
mae = mean_absolute_error(a, b)
|
| 51 |
+
if mode == "range":
|
| 52 |
+
denom = a.max() - a.min()
|
| 53 |
+
else:
|
| 54 |
+
denom = a.mean()
|
| 55 |
+
if denom is None or pd.isna(denom) or denom == 0:
|
| 56 |
+
return float("nan")
|
| 57 |
+
return mae / denom
|
| 58 |
+
except Exception:
|
| 59 |
+
return float("nan")
|
| 60 |
+
|
| 61 |
+
def _plot_series(idx, y1, y2, title, ylabel, out_path):
|
| 62 |
+
plt.figure(figsize=(10, 5))
|
| 63 |
+
plt.plot(idx, y1, label="Submission")
|
| 64 |
+
plt.plot(idx, y2, label="Answer")
|
| 65 |
+
plt.xlabel("Index")
|
| 66 |
+
plt.ylabel(ylabel)
|
| 67 |
+
plt.title(title)
|
| 68 |
+
plt.legend()
|
| 69 |
+
plt.tight_layout()
|
| 70 |
+
plt.savefig(out_path)
|
| 71 |
+
plt.close()
|
| 72 |
+
|
| 73 |
+
def grade(submission_df: pd.DataFrame, team_id: str = "submission") -> Tuple[pd.DataFrame, str]:
|
| 74 |
+
"""
|
| 75 |
+
입력: 사용자가 업로드한 CSV DataFrame
|
| 76 |
+
출력: (score_df, report_dir)
|
| 77 |
+
- score_df: RMSE/NMAE/TOTAL 지표 1-row
|
| 78 |
+
- report_dir: 그래프 PNG들이 저장된 폴더 경로
|
| 79 |
+
"""
|
| 80 |
+
# --------------------------
|
| 81 |
+
# 1) 정답/제출 정규화 & 머지
|
| 82 |
+
# --------------------------
|
| 83 |
answer = pd.read_csv(ANSWER_PATH)
|
| 84 |
+
answer = answer[['DATE_TIME', 'PLANT_ID', 'SOURCE_KEY', 'DC_POWER', 'AC_POWER', 'DAILY_YIELD']]
|
| 85 |
answer = answer.rename(columns={
|
| 86 |
'SOURCE_KEY': 'INVERTER_ID',
|
| 87 |
+
'DC_POWER' : 'ANS_DC_POWER',
|
| 88 |
+
'AC_POWER' : 'ANS_AC_POWER',
|
| 89 |
'DAILY_YIELD': 'ANS_DAILY_YIELD'
|
| 90 |
})
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
# 제출 컬럼 보정
|
| 93 |
+
sub = submission_df.copy()
|
| 94 |
+
if 'SOURCE_KEY' in sub.columns and 'INVERTER_ID' not in sub.columns:
|
| 95 |
+
sub = sub.rename(columns={"SOURCE_KEY": "INVERTER_ID"})
|
| 96 |
+
|
| 97 |
+
# 타입/정렬 보정
|
| 98 |
+
for c in ['PLANT_ID', 'INVERTER_ID']:
|
| 99 |
+
if c in sub.columns:
|
| 100 |
+
sub[c] = sub[c].astype(str)
|
| 101 |
+
for c in ['PLANT_ID', 'INVERTER_ID']:
|
| 102 |
+
if c in answer.columns:
|
| 103 |
+
answer[c] = answer[c].astype(str)
|
| 104 |
+
|
| 105 |
+
# 날짜 파싱 (불가 시 원문 유지)
|
| 106 |
+
for df_ in (answer, sub):
|
| 107 |
+
if 'DATE_TIME' in df_.columns:
|
| 108 |
+
try:
|
| 109 |
+
df_['DATE_TIME'] = pd.to_datetime(df_['DATE_TIME'])
|
| 110 |
+
except Exception:
|
| 111 |
+
pass
|
| 112 |
+
|
| 113 |
merged_df = pd.merge(
|
| 114 |
+
answer, sub,
|
| 115 |
on=['DATE_TIME', 'PLANT_ID', 'INVERTER_ID'],
|
| 116 |
+
how='left',
|
| 117 |
+
suffixes=('', '_SUB')
|
| 118 |
+
).sort_values(by=['DATE_TIME', 'PLANT_ID', 'INVERTER_ID']).reset_index(drop=True)
|
| 119 |
+
|
| 120 |
+
# --------------------------
|
| 121 |
+
# 2) 스케일링 & 지표 계산
|
| 122 |
+
# --------------------------
|
| 123 |
+
# 원본 값
|
| 124 |
+
y_true_ac = merged_df.get('ANS_AC_POWER')
|
| 125 |
+
y_pred_ac = merged_df.get('AC_POWER')
|
| 126 |
+
|
| 127 |
+
# 스케일드
|
| 128 |
+
merged_df['AC_POWER_SCALED'] = _safe_minmax(merged_df.get('AC_POWER'))
|
| 129 |
+
merged_df['ANS_AC_POWER_SCALED'] = _safe_minmax(merged_df.get('ANS_AC_POWER'))
|
| 130 |
+
|
| 131 |
+
rmse_ac = _safe_rmse(y_pred_ac, y_true_ac)
|
| 132 |
+
rmse_ac_scaled = _safe_rmse(merged_df['AC_POWER_SCALED'], merged_df['ANS_AC_POWER_SCALED'])
|
| 133 |
+
|
| 134 |
+
# DAILY_YIELD
|
| 135 |
+
nmae_range, nmae_mean = float("nan"), float("nan")
|
| 136 |
+
if 'DAILY_YIELD' in merged_df.columns and 'ANS_DAILY_YIELD' in merged_df.columns:
|
| 137 |
+
nmae_range = _safe_nmae(merged_df['ANS_DAILY_YIELD'], merged_df['DAILY_YIELD'], mode="range")
|
| 138 |
+
nmae_mean = _safe_nmae(merged_df['ANS_DAILY_YIELD'], merged_df['DAILY_YIELD'], mode="mean")
|
| 139 |
+
|
| 140 |
+
# TOTAL 점수 (evaluate_model의 기대 입력에 맞춤)
|
| 141 |
+
rmse_for_total = rmse_ac if not (pd.isna(rmse_ac) or math.isinf(rmse_ac)) else None
|
| 142 |
+
nmae_for_total = nmae_range if not (pd.isna(nmae_range) or math.isinf(nmae_range)) else None
|
| 143 |
+
try:
|
| 144 |
+
total = evaluate_model(rmse_for_total, nmae_for_total)
|
| 145 |
+
except Exception:
|
| 146 |
+
total = float("nan")
|
| 147 |
|
| 148 |
metrics = {
|
| 149 |
+
# app.py에서 team_id/timestamp를 앞단에 삽입하므로, grader는 지표만 책임지게 구성.
|
| 150 |
"RMSE_AC": rmse_ac,
|
| 151 |
"RMSE_AC_SCALED": rmse_ac_scaled,
|
| 152 |
+
"NMAE_RANGE": nmae_range,
|
| 153 |
+
"NMAE_MEAN": nmae_mean,
|
| 154 |
+
"TOTAL": total,
|
| 155 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
score_df = pd.DataFrame([metrics])
|
| 157 |
|
| 158 |
+
# --------------------------
|
| 159 |
+
# 3) 리포팅 (그래프 PNG 저장)
|
| 160 |
+
# --------------------------
|
| 161 |
output_dir = f"output/{team_id}"
|
| 162 |
os.makedirs(output_dir, exist_ok=True)
|
| 163 |
|
| 164 |
+
# 인덱스: 동일한 길이의 정수 인덱스로 시각화(축 겹침 최소화)
|
| 165 |
+
merged_df = merged_df.reset_index(drop=True)
|
| 166 |
+
idx = list(range(len(merged_df)))
|
| 167 |
+
|
| 168 |
+
# (A) AC_POWER 원본 비교
|
| 169 |
+
try:
|
| 170 |
+
_plot_series(
|
| 171 |
+
idx,
|
| 172 |
+
merged_df['AC_POWER'],
|
| 173 |
+
merged_df['ANS_AC_POWER'],
|
| 174 |
+
title="AC_POWER Comparison (Raw)",
|
| 175 |
+
ylabel="AC Power",
|
| 176 |
+
out_path=f"{output_dir}/ac_power_raw.png",
|
| 177 |
)
|
| 178 |
+
except Exception:
|
| 179 |
+
pass
|
| 180 |
+
|
| 181 |
+
# (B) AC_POWER 스케일드 비교
|
| 182 |
+
try:
|
| 183 |
+
_plot_series(
|
| 184 |
+
idx,
|
| 185 |
+
merged_df['AC_POWER_SCALED'],
|
| 186 |
+
merged_df['ANS_AC_POWER_SCALED'],
|
| 187 |
+
title="AC_POWER Comparison (Scaled 0-1)",
|
| 188 |
+
ylabel="Scaled AC Power",
|
| 189 |
+
out_path=f"{output_dir}/ac_power_scaled.png",
|
| 190 |
)
|
| 191 |
+
except Exception:
|
| 192 |
+
pass
|
| 193 |
+
|
| 194 |
+
# (C) Plant 단위 원본 비교 (원 요청 유지)
|
| 195 |
+
try:
|
| 196 |
+
for plant_id in merged_df['PLANT_ID'].dropna().unique():
|
| 197 |
+
plant_data = merged_df[merged_df['PLANT_ID'] == plant_id].reset_index(drop=True)
|
| 198 |
+
pidx = list(range(len(plant_data)))
|
| 199 |
+
_plot_series(
|
| 200 |
+
pidx,
|
| 201 |
+
plant_data['AC_POWER'],
|
| 202 |
+
plant_data['ANS_AC_POWER'],
|
| 203 |
+
title=f"Plant {plant_id} - AC_POWER Comparison",
|
| 204 |
+
ylabel="AC Power",
|
| 205 |
+
out_path=f"{output_dir}/ac_power_{plant_id}.png",
|
| 206 |
+
)
|
| 207 |
+
except Exception:
|
| 208 |
+
pass
|
| 209 |
+
|
| 210 |
+
# (D) DAILY_YIELD 비교(존재 시)
|
| 211 |
+
if 'DAILY_YIELD' in merged_df.columns and 'ANS_DAILY_YIELD' in merged_df.columns:
|
| 212 |
+
try:
|
| 213 |
+
_plot_series(
|
| 214 |
+
idx,
|
| 215 |
+
merged_df['DAILY_YIELD'],
|
| 216 |
+
merged_df['ANS_DAILY_YIELD'],
|
| 217 |
+
title="DAILY_YIELD Comparison",
|
| 218 |
+
ylabel="Daily Yield",
|
| 219 |
+
out_path=f"{output_dir}/daily_yield.png",
|
| 220 |
+
)
|
| 221 |
+
except Exception:
|
| 222 |
+
pass
|
| 223 |
|
| 224 |
return score_df, output_dir
|
| 225 |
|
src/utils.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def evaluate_model(rmse, nmae=None, rmse_max=500, nmae_max=1.0):
|
| 2 |
+
"""
|
| 3 |
+
모델 성능 점수화 함수
|
| 4 |
+
- RMSE와 NMAE는 작을수록 좋음
|
| 5 |
+
- RMSE는 [0, rmse_max], NMAE는 [0, nmae_max] 구간으로 정규화
|
| 6 |
+
- NMAE가 없는 경우 0점 처리
|
| 7 |
+
- 최종 점수는 RMSE와 NMAE 점수의 단순 평균
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
rmse : float
|
| 11 |
+
모델의 RMSE 값
|
| 12 |
+
nmae : float or None
|
| 13 |
+
모델의 NMAE 값 (없으면 None)
|
| 14 |
+
rmse_max : float
|
| 15 |
+
RMSE 최대 기준값 (default=500)
|
| 16 |
+
nmae_max : float
|
| 17 |
+
NMAE 최대 기준값 (default=1.0)
|
| 18 |
+
|
| 19 |
+
Return:
|
| 20 |
+
dict : {
|
| 21 |
+
"rmse_score": float,
|
| 22 |
+
"nmae_score": float,
|
| 23 |
+
"final_score": float
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
"""
|
| 27 |
+
# RMSE 점수 (0~100)
|
| 28 |
+
rmse_score = max(0, (1 - rmse / rmse_max) * 100)
|
| 29 |
+
|
| 30 |
+
# NMAE 점수 (0~100)
|
| 31 |
+
if nmae is not None:
|
| 32 |
+
nmae_score = max(0, (1 - nmae / nmae_max) * 100)
|
| 33 |
+
else:
|
| 34 |
+
nmae_score = 0
|
| 35 |
+
|
| 36 |
+
# 최종 점수 = 단순 평균
|
| 37 |
+
final_score = (rmse_score + nmae_score) / 2
|
| 38 |
+
|
| 39 |
+
return final_score
|
| 40 |
+
# return {
|
| 41 |
+
# "rmse": round(rmse_score, 2),
|
| 42 |
+
# "nmae": round(nmae_score, 2),
|
| 43 |
+
# "final": round(final_score, 2)
|
| 44 |
+
# }
|