KuuwangE commited on
Commit
0c6d96d
·
unverified ·
1 Parent(s): e442972

정상화

Browse files
app.py CHANGED
@@ -1,136 +1,208 @@
1
- # import gradio as gr
2
- # from gradio_leaderboard import Leaderboard, ColumnFilter, SelectColumns
3
- # import pandas as pd
4
- # from apscheduler.schedulers.background import BackgroundScheduler
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 os
62
- import shutil
63
- from huggingface_hub import upload_file
64
- from src.envs import RESULTS_REPO
65
- from src.grader import grade # grader.py 안에 채점 + 리포팅 함수
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
 
 
 
 
 
67
 
68
- def grade_csv(file):
 
 
 
 
 
 
 
69
  submission_df = pd.read_csv(file.name)
70
 
71
  # 채점 및 리포트 생성
72
- score_df, report_dir = grade(submission_df, team_id="team")
73
 
74
- # 점수 CSV 저장
75
- output_file = "graded_results.csv"
76
- score_df.to_csv(output_file, index=False)
 
77
 
78
- # Hugging Face Hub 업로드
79
- upload_file(
80
- path_or_fileobj=output_file,
81
- path_in_repo=output_file,
82
- repo_id=RESULTS_REPO,
83
- repo_type="dataset",
84
- token=os.environ.get("HF_TOKEN"),
85
- )
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  # 리포트 ZIP 생성
88
- report_zip = "report.zip"
89
- shutil.make_archive("report", 'zip', report_dir)
90
 
91
- # 그래프 이미지 파일 목록 (report_dir 안에 *.png)
92
- image_files = [os.path.join(report_dir, f) for f in os.listdir(report_dir) if f.endswith(".png")]
 
 
 
 
93
 
94
- return score_df, report_zip, image_files
 
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
- with gr.Row():
106
- csv_input = gr.File(label="CSV 업로드", file_types=[".csv"])
107
- with gr.Row():
108
- password = gr.Textbox(label="Jupyter Notebook 비밀번호")
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.Textbox("ASDF")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  if __name__ == "__main__":
134
- # demo.launch()
135
- demo.launch(debug=True, show_error=True)
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
- TEAM_ID,RMSE_AC,RMSE_AC_SCALED,NMAE_RANGE,NMAE_MEAN
2
- 13,493.28674800238497,0.35831099772441877,0.0,0.0
 
 
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:108cf615dc3b4efc523a07f7568036674027ba1d31170af8c6455612e2e71d35
3
- size 175825
 
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
- ANSWER_PATH = "answer.csv" # Space에 같이 넣은 정답 파일
9
 
 
10
 
11
- def grade(submission_df: pd.DataFrame, team_id: str = "submission"):
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
- # merge
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  merged_df = pd.merge(
26
- answer, submission_df,
27
  on=['DATE_TIME', 'PLANT_ID', 'INVERTER_ID'],
28
- how='left'
29
- ).sort_values(by=['DATE_TIME', 'PLANT_ID', 'INVERTER_ID'])
30
-
31
-
32
- # scaler
33
- scaler = MinMaxScaler()
34
- merged_df['AC_POWER_SCALED'] = scaler.fit_transform(merged_df[['AC_POWER']])
35
- merged_df['ANS_AC_POWER_SCALED'] = scaler.fit_transform(merged_df[['ANS_AC_POWER']])
36
-
37
- # 지표 계산
38
- rmse_ac = root_mean_squared_error(merged_df['AC_POWER'], merged_df['ANS_AC_POWER'])
39
- rmse_ac_scaled = root_mean_squared_error(merged_df['AC_POWER_SCALED'], merged_df['ANS_AC_POWER_SCALED'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  metrics = {
 
42
  "RMSE_AC": rmse_ac,
43
  "RMSE_AC_SCALED": rmse_ac_scaled,
44
- "NMAE_RANGE": 0.0,
45
- "NMAE_MEAN": 0.0,
 
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
- # 리포팅: Plant별 그래프 저장
61
- # =========================
62
  output_dir = f"output/{team_id}"
63
  os.makedirs(output_dir, exist_ok=True)
64
 
65
- for plant_id in merged_df['PLANT_ID'].unique():
66
- plt.figure(figsize=(10, 5))
67
- plant_data = merged_df[merged_df['PLANT_ID'] == plant_id]
68
-
69
- plt.plot(
70
- list(range(len(plant_data['DATE_TIME']))),
71
- plant_data['AC_POWER'],
72
- label="Submission AC_POWER"
 
 
 
 
 
73
  )
74
- plt.plot(
75
- list(range(len(plant_data['DATE_TIME']))),
76
- plant_data['ANS_AC_POWER'],
77
- label="Answer AC_POWER"
 
 
 
 
 
 
 
 
78
  )
79
-
80
- plt.xlabel('Index')
81
- plt.ylabel('AC Power')
82
- plt.title(f"Plant {plant_id} - AC_POWER Comparison")
83
- plt.legend()
84
- plt.tight_layout()
85
- plt.savefig(f"{output_dir}/ac_power_{plant_id}.png")
86
- plt.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # }