제3회 풍력발전량 예측 AI 경진대회 - BARAM 2026
팀명 930geo로 참가한 한국동서발전 주최 BARAM 2026, 제3회 풍력발전량 예측 AI 경진대회 기록이다. 과제는 태백 가덕산 풍력단지의 시간당 발전량을 하루 전에 예측하는 것이었는데, 끝까지 풀어보니 이 대회의 본질은 발전량 예측이 아니라 정산 제도 자체를 최적화하는 문제였다. 결과적으로 후반 성적 개선의 대부분은 모델이 아니라 의사결정 단계에서 나왔다.
과제 이해
대상은 태백 가덕산 풍력단지의 KPX 3개 그룹, 2025년 8,760시간의 시간당 발전량(kWh)이었다. 운영 제약이 실제 입찰 제도와 동일했다는 점이 이 대회의 특징이다. 전날 13:00 KST에 가용한 예보만으로 익일 01시부터 익익일 00시까지를 예측해야 했고(리드타임 12~36시간), 재예측 기회가 하루 1회뿐이라 관측값으로 보정하는 nowcasting은 원천적으로 불가능했다.
| 그룹 | 기종 | 대수 | 설비용량 | 허브고 | 레이블 기간 | 이용률 |
| kpx_group_1 | VESTAS V126 3.6MW | 6 | 21.6 MW | 117 m | 2022–2024 | 30.7% |
| kpx_group_2 | VESTAS V126 3.6MW | 6 | 21.6 MW | 117 m | 2022–2024 | 32.8% |
| kpx_group_3 | UNISON U136 4.2MW | 5 | 21.0 MW | 117 m | 2023–2024(2년) | 26.5% |
그룹3은 2022년 레이블이 통째로 없어서 학습 데이터가 다른 그룹의 3분의 2 수준이었다. 레이블 결측 자체는 그룹1 104개, 그룹2 103개로 전체 시간 대비 1% 미만이었고, 예보 격자와 SCADA는 학습 기간 전 구간에 존재했다. 이 그룹3의 데이터 부족이 나중에 성능 격차의 원인으로 지목되지만, 실제 원인은 다른 데서 나왔다는 것을 뒤에서 확인하게 된다.
평가지표 해부: 정확도 지표가 아닌 정산 제도
총점은 \(\text{총점} = 0.5 \times (1-\text{NMAE}) + 0.5 \times \text{FICR}\) 로 정의된다. NMAE는 정규화 평균절대오차라 익숙한데, FICR(정산금 획득률)이 이 대회를 어렵게 만든 핵심이었다. FICR을 뜯어보면 세 가지 성질이 전부 비직관적이다.
첫째, 계단함수다. 재생에너지 발전량 예측제도의 정산금 구조를 그대로 가져온 것이라 오차 6%와 6.001%의 가치가 4원과 3원으로 갈린다. 미분이 불가능하니 경사 기반으로 직접 최적화할 수 없고, 분포를 만든 뒤 그 분포에서 최적의 지점을 배치하는 2단 설계가 필요하다는 뜻이었다.
둘째, 실발전량 가중이다. 분모가 \(\sum 4y\) 형태라 용량 60%를 넘는 고발전 구간이 FICR 점수의 59%를 결정한다. 무풍 시간을 아무리 잘 맞혀도 점수에는 거의 기여하지 않는다.
셋째, 10% 절단이다. 전체 시간의 41%가 채점에서 아예 빠진다. 이 세 조합을 다 이해하고 나니, 내가 만들어야 하는 것은 정확한 예측기가 아니라 예측분포로부터 기대 정산금을 최대화하는 입찰값 결정기라는 게 명확해졌다.
데이터 특성과 라벨 오염
제공된 데이터는 세 종류였다. 기상청 국지모델 LDAPS(1.5km, 시간당 16격자×35컬럼), NOAA 전지구모델 GFS(0.25°, 시간당 9격자×40컬럼, 80/100m 바람이 허브고 117m에 가장 가까움), 그리고 10분 단위 터빈별 실측 SCADA였다. SCADA는 학습 기간에만 존재해서 입력으로 쓸 수 없고, 학습 신호를 정제하고 파워커브를 학습하는 용도로만 썼다.
EDA에서 확정한 사실이 여럿 있었다. SCADA의 power_kw10m은 단위가 kW가 아니라 10분당 kWh였는데, 시간 단위로 합산해보니 레이블과 상관 0.9998, 회귀 기울기 0.986이 나와서 이 해석이 맞다는 걸 확인했다. VESTAS 쪽에는 오류코드성 극단값이 868개(0.046%) 있었는데 터빈별 min/max가 정확히 ±42,512,957로 대칭이라 명백한 센서 오류였다. 장기 정지 구간도 발견했다(wtg05 2022-01-30~03-23 52일, wtg04 2022-08-16~09-04 19일, 풍속·출력이 0에 고착). LDAPS의 lsm 컬럼은 전부 1.0 상수였고 lssrate는 사실상 0이라 정보가 없었다. 상대습도는 최대 110.4%까지 나와 클리핑이 필요했고, 운량 스케일도 LDAPS(0~1)와 GFS(0~100)가 달라 맞춰야 했다.
가장 흥미로웠던 발견은 따로 있었다. 그룹3의 성능이 세 그룹 중 가장 낮았는데, 풍속-발전량 상관은 오히려 그룹3이 가장 높았다(그룹1 0.793, 그룹2 0.811, 그룹3 0.815). 성능이 가장 나쁜 그룹의 기상-발전 관계가 가장 깨끗하다는 건 모순이었고, 처음에는 부지 난이도를 의심했다.
SCADA 10분 데이터로 역추적한 결과, 원인은 부지가 아니라 라벨 오염이었다. 터빈이 정지해 있던 시간의 라벨이 기상-발전 관계를 왜곡시키고 있었던 것이다. 가용률(정상 가동 터빈 비율)이 0.8 미만인 시간이 전체의 22.6%(그룹3 기준 2,839시간)나 됐다. 부지가 아니라 라벨의 문제였다는 이 진단이 다음 단계의 가용률 학습 필터로 이어졌다. 이 필터를 적용한 뒤 2024년 검증창에서 그룹3의 1−NMAE가 0.8731(그룹1은 0.8901)까지 올라 정확도 격차는 1.7%p까지 줄었다. 남은 격차는 FICR(0.3837 대 0.4911)에서 났는데, 이건 뒤에서 다시 다룬다.
설계: 예측기와 결정기의 분리
문제를 이렇게 정리하고 나니 전체 파이프라인의 방향이 정해졌다. 정산 제도를 손실함수 안에 녹이는 대신, 예측 모델은 분포만 만들고 그 분포를 받아 입찰값을 고르는 최적화기를 별도로 두는 구조다. 이렇게 분리해두면 밴드 폭이나 단가가 바뀌어도 학습된 예측 모델은 그대로 재사용할 수 있다는 장점이 있다.
전처리: 가용률 학습 필터
필터의 정의는 단순하다. SCADA 10분 기준으로 풍속이 4 m/s를 넘는데 출력이 0 이하이면 정지로 판정하고, 그룹 가용률(정상 가동 터빈 비율)이 0.8 미만인 시간을 학습에서만 제외했다. 검증 라벨은 원본을 그대로 두었다. 이건 평가를 조작하는 게 아니라 학습 신호를 정제하는 것이기 때문이다. 정지로 오염된 라벨은 기상-발전 관계를 체계적으로 과소예측 방향으로 왜곡시키는 원천이었다.
효과는 로컬에서 +0.0108, 리더보드 FICR 전이로 +0.0037, 2023년 창에서 부호 재현까지 +0.0027이었다. y를 가용률로 나눠 잠재 발전량을 복원하는 대안도 검토했지만 노이즈만 증폭시켜 기각했다. 오염된 구간은 보정하는 것보다 제외하는 쪽이 우월했다. 다만 잔여 리스크는 남는다. 정지 구간을 학습에서 뺐기 때문에 실제 정지 시간대에는 과대예측이 남는다(2024 검증창 채점 대상 중 그룹3의 11.2%, 편향 +0.15cap). 정지는 기상으로는 예측 불가능한 사건이고, 실무에서는 정비 계획을 입력으로 받으면 해소될 문제라고 판단했다.
추가로 SCADA 단위를 확정하고(10분당 kWh 합산), 오류코드를 마스킹하고(|power| > 800), 풍향 관례를 통일하고(UNISON −180~180°를 0~360°로), 10분→시간 집계를 레이블 정의(구간 종료 시각)와 정확히 맞추고 6개 미만이면 결측 처리했다.
피처 구성과 day-ahead 합법성
물리적으로 의미 있는 피처를 많이 파생시켰다. 모든 U/V 성분을 풍속·풍향으로 바꾸고, 멱법칙 전단지수로 허브고 117m까지 외삽했다. 발전량이 풍속의 세제곱에 비례한다는 점에서 ws³를, 공기밀도 ρ = p/(RT)와 ρ·ws³를, 대기 안정도와 램프율, 일주기 레짐도 피처로 넣었다. 주요 바람 변수는 격자별로 피벗해 정보를 보존하고 나머지는 공간통계(mean/std/min/max)로 요약했다.
정보원도 확장했다. ECMWF IFS 0.25° 오픈데이터, JMA GSM, GEFS 앙상블을 추가해 센터 간 예보가 서로 얼마나 다른지(disagreement) 자체를 불확실성 피처로 넣었다. SCADA로부터는 단지 파워커브(웨이크맵·풍향 오프셋 보정)와 나셀풍속 대비 예보풍속 편차를 만들었는데, 이때 검증용(2022–23)과 최종용(2022–24) 커브를 분리해서 적합시켜 누수를 막았다. 최종 피처 번들은 355~420열이었다.
여기서 성능과 무관하게 지켜야 하는 제약이 하나 있었다. 외부 NWP는 전부 전날 13:00 KST 이전에 배포가 완료된 런만 쓸 수 있다는 합법성 규약이다. 소스별로 모델 런 시각에서 실제 공개 시각을 역산해 여유 시간을 문서화했고, 여유가 확보되지 않는 소스는 성능이 아무리 좋아도 채택하지 않았다. TIGGE 아카이브는 센터별 라이선스가 CC BY-NC로 갈리는 걸 확인하고, 상업 주체가 포함된 이 대회 맥락에서는 보수적으로 배제했다. 추가로 검토한 KMA 상층, DWD ICON, JMA MSM, TIGGE, ECCC GEM은 증분이 없어서 기각했다. 결국 채택한 건 ECMWF, JMA, GEFS 3종뿐이었다. 리드타임을 못 맞추는 소스는 대회 점수가 올라도 실제 입찰에는 쓸 수 없다는 게 이 규약의 핵심이었다.
모델: 39분위수 딥앙상블
FICR이 계단함수라 점추정을 직접 최적화할 수 없으니, 분포를 만들고 그 분포에서 최적 지점을 고르는 2단 설계가 필요했다. 출력은 0.025 간격의 39개 분위수 헤드로 하고 각각 pinball loss로 학습시켰다. 이렇게 분위수를 조밀하게 잡은 근거는 Ehm et al.(2016)의 elicitability 논의다.
| 그룹 | 멤버 1 | 멤버 2 | 설계 의도 |
| g1 | 양방향 GRU(예보 24h 시퀀스, d_model 256), 10시드 | 동일 구조 / 다른 정보원(ECMWF 계열), 20시드 | 정보원 다양성 |
| g2 | 양방향 GRU + SCADA 파워커브 피처, 10시드 | 통합학습 MLP 768-768-384, dropout 0.4, 40시드 | 구조 다양성 |
| g3 | MLP, 10시드 | 통합학습 MLP 1024-1024-512, dropout 0.45, 40시드 | 데이터 부족 보완 |
앙상블을 묶는 원칙은 성능이 아니라 편향의 비상관성이었다. 확률을 혼합하는 대신 분위수 축을 평균하는 Vincentization을 썼는데, 이렇게 하면 예측분포의 예리함(sharpness)이 보존된다. 통합학습(pooled) 멤버는 단독으로는 세 그룹 모두에서 성능이 떨어지지만, 편향 방향이 다른 멤버들과 달라서 평균 재료로는 유효했다(그룹3 최악창에서 +0.00377). 마지막으로 정격 25~50% 구간(정산 밴드 명중 여부가 갈리는 구간)에 학습 가중을 주는 FICR 정렬 손실을 추가했다.
\[w = 1 + \lambda \cdot \exp\left(-0.5 \left(\frac{y - 0.375 \cdot \text{cap}}{0.12 \cdot \text{cap}}\right)^2\right)\]이 bandweight가 챔피언 이득의 마지막 조각이었고, 그 이득은 전량 FICR에서 나왔다.
의사결정: FICR 기대정산 최적화기
39개 분위수 점과 밴드 경계(±0.059cap, ±0.079cap)를 후보 집합으로 두고, 산식 두 항의 가중을 그대로 이식한 최적화기로 입찰값을 골랐다.
\[f^*(t) = \underset{f}{\operatorname{argmax}} \sum_{i=1}^{39} \frac{1}{39} \left[ -c_1 \cdot \frac{\lvert f-q_i\rvert}{\text{cap}} + c_2 \cdot q_i \cdot \text{price}\!\left(\frac{\lvert f-q_i\rvert}{\text{cap}}\right) \right]\]여기서 \(c_1 = 0.5/\hat{N}_1\) 은 NMAE 항, \(c_2 = 0.5/(4\hat{S})\) 은 FICR 항의 가중이고, price는 오차 6% 이내면 4원, 6~8%면 3원, 8% 초과면 0원이다. 10% 절단과 실발전량 가중도 그대로 반영했다.
argmax의 봉우리가 조금만 움직여도 점수 분산이 커지는 문제가 있었다. FICR이 밴드 안/밖을 가르는 이산 지시함수라서, 예측 상관 0.99 수준의 변화가 FICR을 −0.015나 흔드는 걸 실측했다. 그래서 잡음 σ=0.02짜리 복제본 16개의 배치를 평균하는 배깅 배치(bagged placement)로 모드 점프를 억제했고, seed를 고정해 완전히 결정적으로 만들었다. 후보 격자를 501개로 늘리거나 꼬리를 외삽하거나 복제 횟수를 늘리는 시도는 전부 개선이 없었고, soft weighting은 오히려 −0.00199, exp-κ 수축도 사전 예측대로 악화되어 이 방향의 탐색은 거기서 끝냈다.
검증 전략: 이중 윈도
테스트는 2025년인데 학습은 2022–2024년이었다. 여기서 가장 큰 리스크는 과적합이 아니라 연도 간 covariate shift였다(2025년 강풍월의 예보풍속이 +1~3 m/s 이동해 있었다). 그래서 윈도를 둘로 나눠 검증했다. 윈도 A는 2022–2023으로 학습하고 2024로 검증, 윈도 B는 2022+2024로 학습하고 2023으로 검증했다. 채택 조건은 평균이 아니라 둘 중 나쁜 쪽, 즉 최악 윈도가 문턱을 통과할 때만이었다. 3차 게이트로 주 단위 블록 부트스트랩을 2000회 돌려 P(Δ>0) ≥ 0.9를 요구했다. 2025년은 두 학습 창 어디에도 없는 해이므로, 한쪽 창에서만 우세하다는 건 채택 근거로 인정하지 않았다.
이 원칙을 어겼다가 실제로 대가를 치른 적이 있다. 2026-07-20에 그룹3 모델 교체 후보를 검증했는데, 윈도 A에서는 +0.0114였지만 윈도 B에서는 −0.0067이 나왔다. 이때 “그룹3은 2022년 데이터가 없으니 2023년 창은 못 믿을 계측기”라는 그럴듯한 논리를 만들어 이중 윈도 원칙을 우회하고 채택해버렸다. 결과는 리더보드에서 정확히 −0.0046 손실이었다. 2023년 창이 예측한 부호와 크기가 그대로 실현된 것이다. 이 사건 이후로 이중 윈도 무예외 원칙을 확립했고, 대회가 끝날 때까지 다시는 예외를 두지 않았다. 그럴듯한 논리로 검증 결과를 거스르고 싶은 유혹이 가장 위험한 순간이라는 걸 이때 배웠다.
전이 계측기
로컬에서 좋아 보이는 변경이 리더보드로 그대로 전이되는지도 별도로 측정해야 했다. 로컬 Δ(1−NMAE)는 기울기 R²가 0.564로 채택할 만했지만, 로컬 Δtotal은 R² 0.080으로 사용 불가, 로컬 ΔFICR은 R² 0.003으로 사실상 무정보였다. n=25 페어 회귀로 σ=0.00124를 구하고 문턱 +0.00129(1σ)를 사전 등록해뒀는데, 이후로는 오판이 0건이었다. 후반 2주 동안 Public 점수가 정체된 것도 이 때문이었다 — 올라가 보이는 변경들이 Private으로 전이되지 않는다는 걸 이미 알고 있었기 때문에 제출하지 않았던 것이다. 문턱을 넘는 후보가 없으면 제출하지 않는다는 원칙을 끝까지 지켰다.
결과
최종 성적은 Private 총점 0.65836(Public 0.65669), Private 1−NMAE 0.87716, Private FICR 0.43957이었다. 986팀 중 Private 최종 9위로 한국동서발전으로부터 우수상을 받았다. 순위에 운이 섞여 있었다는 건 인정하지만, 과적합을 피한 선택들은 전부 의도된 것이었다.
소거된 대안들
구성을 선택한 근거의 절반은 대안을 하나씩 소거한 기록이다.
| 축 | 시도 | 결과 |
| 외부 NWP 추가 | KMA 상층 → DWD ICON → JMA MSM → ECMWF TIGGE·ECCC GEM | 추가 4개 소스 전부 증분 없음 |
| 시드·용량 확대 | 5→10→20→40시드, 512→1024 | 잔여 상한 +0.00033 → 종결 |
| 앙상블 다양성 | 멤버 추가 | 블렌드 과평활 → FICR 붕괴(−0.0052) |
| 피처 엔지니어링 | dedup / deskew / quantile-norm | 현행 최적 구성 미달 |
| 분위수 재보정 | cross-window recalibration | −0.023 |
| 배치 스킴 | soft weighting, exp-κ, 후보격자 501, 꼬리 외삽 | 양방향 개선 없음 |
| 수준(level) 축 | 미세보정(nudge) 재스윕 | 현행 구성이 이미 정점 |
| 모델 클래스 | FT-Transformer, CatBoost, TabPFN, 격자 CNN, MOS×2, conformal/CQR | 전부 기각 |
이 중에서도 뼈아팠던 건 대회 막판의 일이다. “FICR이 실발전량 가중이니 고발전 구간을 더 정확히 맞춰야 한다”는 진단 아래, bandweight의 중심을 0.375에서 0.6~0.7로 옮겨봤다. 진단은 적중이었다 — 고발전 구간 FICR이 실제로 +0.0138 올랐다. 하지만 처방은 오류였다. 저·중발전 구간 FICR이 −0.059 붕괴하면서 전체로는 −0.014였다. 기존 중심 0.375가 이미 최적이었던 것이다. 가설이 정확하다고 해서 그 가설에서 나온 처방까지 정확한 건 아니라는 걸 이때 제대로 배웠다.
적용 가능성
대회가 끝나고 나서 가장 많이 생각하게 된 건, 이 구조가 풍력이나 이 대회에만 묶여 있지 않다는 점이었다.
FICR의 6%/8% 밴드와 4원/3원/0원 구조는 재생에너지 발전량 예측제도의 정산금 구조 그 자체이고, day-ahead 12~36h 리드타임은 하루전 시장 입찰 마감 시각 제약, 실발전량 가중은 정산금의 발전량 비례, 10% 절단은 소용량 시간 정산 제외와 그대로 대응된다. 즉 의사결정 단계에서 만든 최적화기가 곧 실무 입찰 엔진이다. 예측분포와 정산 단가표, 설비용량만 바꾸면 밴드 폭·단가가 다른 제도(타 국가·타 계약)에도 그대로 적용할 수 있고, 예측 모델의 구조나 학습 방식과도 무관하게 동작한다. 기존 운영 모델을 그대로 둔 채 의사결정 단계만 교체할 수 있다는 뜻이다.
| 제출 방식 | 총점 | 1−NMAE | FICR |
| 중앙값 q50 | 0.63928 | 0.88129 | 0.39727 |
| 최적화기 | 0.66566 | 0.88125 | 0.45006 |
| Δ | +0.02638 | −0.00004 | +0.05279 |
2024년 검증창에서 같은 39분위수 입력, 동일한 모델로 제출값만 중앙값에서 최적화기 출력으로 바꿨을 뿐인데 FICR이 +0.0528 올랐다. 정확도(1−NMAE)는 −0.00004로 사실상 그대로였다. 정확도를 희생해서 얻은 점수가 아니라, 같은 예측분포에서 정산 밴드 명중률만 끌어올린 결과라는 뜻이다.
산출물 중 세 가지는 풍력이나 이 대회에 종속되지 않는다고 본다. 첫째, 가용률 학습 필터는 “터빈이 정지한 시간의 라벨은 기상-발전 관계를 학습시키지 않는다”는 일반 원리라서 태양광 인버터 정지나 ESS 제약 운전 등 같은 구조의 모든 발전 예측에 이식할 수 있다. SCADA를 보유한 사업자라면 추가 데이터 수집 없이 바로 적용 가능하다. 둘째, 전이 계측기는 운영 모델의 교체 시점을 판단하는 모델 거버넌스 도구다. 오프라인 백테스트 개선만으로는 배포 근거가 되지 않는다는 것, 그리고 지표가 여러 항의 합일 때 어느 항이 온라인으로 전이되는지부터 재야 한다는 절차는 풍력에 국한되지 않는 ML 운영 방법론이다. 셋째, 외부 소스별 배포시각을 역산해 리드타임 합법성을 문서화하는 절차는 데이터 계약·감사 대응에 그대로 쓸 수 있다. 최종 제출물은 단일 명령으로 md5 단위까지 재현되도록 만들었는데, LightGBM 스레드 비결정성은 ±0.005로 고정하고 신경망은 분위수 덤프를 보존해 CSV 경로까지 재현했다. 운영 자원도 AWS ml.g5.xlarge(A10G 24GB) GPU 1장이면 충분했다 — 대형 클러스터는 필요 없었다.
결론
문제를 정산 제도 최적화 문제로 재정의하고, 예측분포를 만드는 예측기와 입찰값을 고르는 결정기를 분리한 것이 이 대회의 가장 중요한 결정이었다. 후반 이득은 전량 결정기에서 나왔다. 모든 채택은 이중 윈도와 사전 등록된 문턱을 통과한 것에 한정했고, 통과하지 못하면 제출하지 않았다. 전이 계측기로 어느 성분이 실제로 리더보드까지 가는지부터 측정한 것도 같은 맥락이다. 그 규율의 결과가 Public 60위에서 Private 9위(986팀 중)로, 총점 0.65836이었다. 순위에 운이 섞였다는 건 인정하지만 과적합을 피한 것은 분명히 의도된 선택이었다. 남는 것은 모델이 아니라 절차다 — 무엇을 근거로 채택했고, 무엇을 근거로 제출하지 않았는가. 그 근거는 전부 회귀로 측정되어 기록으로 남아 있다.