---
title: "30년의 기후 기억, 파이썬으로 1초 만에 불러내기 (feat. 기상청 평년값 API)"
id: "963"
type: "post"
slug: "kma-normal-value-api-guide"
published_at: "2025-12-16T03:36:50+00:00"
modified_at: "2026-07-19T09:57:13+00:00"
url: "https://doyouknow.kr/kma-normal-value-api-guide/"
markdown_url: "https://doyouknow.kr/kma-normal-value-api-guide.md"
excerpt: "기상청 평년값 API로 30년 기후 데이터를 파이썬으로 수집하고, LSTM 모델로 기후변화를 예측하세요. 이상기후 판단부터 AI 모델링까지 완벽 가이드!"
taxonomy_category:
  - "AI"
  - "데이터셋"
  - "실전편"
taxonomy_post_tag:
  - "LSTM"
  - "공공데이터"
  - "기상청API"
  - "기후데이터"
  - "기후변화"
  - "데이터분석"
  - "데이터셋"
  - "실전전"
  - "파이썬"
  - "평년값"
---

혹시 “오늘 춥네”라고 말할 때, 진짜 ‘얼마나’ 춥다는 건지 아시나요?

일기예보에서 “평년보다 3도 높습니다”라고 할 때, 그 **‘평년’**이 뭔지 정확히 아는 분은 많지 않습니다. 하지만 이 평년값이야말로 **기후변화를 측정하는 가장 강력한 기준선(Baseline)**입니다.

2025년 현재, 전 세계는 이상기후로 신음하고 있습니다. 한국도 예외가 아니죠. “예전엔 이렇지 않았는데…”라는 말이 이제는 데이터로 증명됩니다. 기상청은 **30년간의 기후 관측 데이터를 평균한 ‘평년값’**을 공개하고 있으며, 이를 통해 우리는 기후가 얼마나 변했는지 객관적으로 확인할 수 있습니다.

오늘 포스팅에서는 기상청 API 시리즈의 심화 편으로, **‘지상관측데이터 평년값(일조, 구름, 기온, 습도) 조회서비스’**를 완벽하게 마스터해 보겠습니다. 이전에 다룬 [태양광 발전량 예측](https://doyouknow.kr/solar-forecast-api-python-guide/)
과 [실시간 날씨 예측 모델](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)
에서는 실시간 데이터를 다뤘다면, 오늘은 **‘기준이 되는 데이터’**를 확보하는 방법을 배웁니다.

이 API 하나면 1961년부터 2020년까지 60년치 평년값을 언제든지 조회할 수 있으며, 파이썬 코드 몇 줄로 전국의 기후 기준선을 손안에 쥘 수 있습니다!

[ez-toc]

## 평년값이란? 왜 30년일까?

**평년값(Climatological Normals)**은 특정 기후요소를 최근 30년간 평균한 값입니다. [세계기상기구(WMO)](https://public.wmo.int/en)
의 권고에 따라 **10년마다 새로운 평년값을 산출**하며, 현재는 1991~2020년 데이터가 최신 평년값입니다.

왜 하필 30년일까요? 그보다 짧으면 우연한 변동성(노이즈)에 휘둘리고, 길면 기후변화 추세를 놓치기 때문입니다. 30년이야말로 **‘기후의 성격’을 파악하기에 최적의 기간**이죠.

### 평년값의 실전 활용 분야

기상청이 제공하는 평년값은 단순한 통계 수치가 아닙니다. 실제로 우리 삶 곳곳에서 활용되고 있습니다:

**에너지 산업**

- 태양광 발전량 예측 모델의 Feature Engineering
- “오늘 발전량이 뚝 떨어졌는데, 기기 고장일까? 아니면 원래 이맘때 흐린 걸까?”
- 평년값과 비교하면 **이상 탐지(Anomaly Detection)**가 가능합니다

**농업 분야**

- 작물 재배 적기 판단, 냉해·폭염 위험도 평가
- “벼 파종은 평년 기준 4월 15일 이후가 안전합니다”

**건설·인프라**

- 구조물 설계 기준(강설량, 강수 강도 등)
- 냉난방 용량 설계

**방재·보험**

- 홍수·가뭄 예측 기준
- 기후 리스크 평가 및 보험료 산정

예를 들어, 서울의 1월 평균기온 평년값이 -2.5℃인데 올해 -5℃라면? 이는 **“평년보다 2.5℃ 낮은 이상 저온”**으로 해석되며, 난방 에너지 수요 급증을 예측할 수 있습니다.

## 기상청 평년값 API 완전 분해

기상청 API 허브의 [산업특화 > 에너지 > 태양광 섹션](https://apihub.kma.go.kr/specialApiList.do)
에서 두 번째 서비스인 **‘지상관측데이터 평년값 조회서비스’**를 찾을 수 있습니다.

### API 기본 정보

**호출 구조**

- **URL**: `https://apihub.kma.go.kr/api/typ01/url/sun_sfc_norm.php`
- **Method**: GET
- **Data Format**: 텍스트 (CSV 유사 형식, **JSON 아님 주의!**)

**제공 데이터**

- **평년값 기준 시기(ST)**: 1991(1961~1990), 2001(1971~2000), 2011(1981~2010), 2021(1991~2020)
- **시간 단위(norm)**: D(일), S(순), M(월), Y(연)
- **기후 요소**: 평균기온, 최고기온, 최저기온, 상대습도, 일조시간, 전운량

### 주요 입력 파라미터 분석

| 파라미터 | 설명 | 예시 | 필수 여부 |
| --- | --- | --- | --- |
| authKey | API 인증키 | 발급받은 키 입력 | 필수 |
| stn | 지점번호 | 108(서울), 159(부산), 0(전체) | 필수 |
| norm | 평년값 종류 | D(일), S(순), M(월), Y(연) | 필수 |
| tmst | 평년값 기준 시기 | 2021(1991~2020년) | 필수 |
| MM1, DD1 | 조회 시작 월·일 | MM1=1, DD1=1 (1월 1일) | 필수 |
| MM2, DD2 | 조회 종료 월·일 | MM2=12, DD2=31 (12월 31일) | 선택 |

**주요 지점번호**

- 서울(108), 인천(112), 수원(119)
- 부산(159), 대구(143), 광주(156)
- 대전(133), 울산(152), 제주(184)

### 출력 데이터 구조 파악

API 응답은 **CSV 형식**으로 반환되며, 주석 라인(#으로 시작)과 데이터 라인으로 구성됩니다. **JSON 아님을 주의하세요!**

**응답 예시**

```
###################################################################################################
#  남한 지상 평년값 조회
#  ST     : 평년값 기준 시기 (2001 = 1971~2000년)
#  STN    : 지점번호
#  MM     : 월
#  DD     : 일 (순별 평년값인 경우, 100=상순, 200=중순, 300=하순)
#  TA     : 평균기온(°C)
#  TA_MAX : 최고기온(°C)
#  TA_MIN : 최저기온(°C)
#  HM     : 상대습도(%)
#  SS     : 일조시간(hr)
#  CA_TOT : 전운량(1/10)
###################################################################################################
# ST,STN, MM, DD,    TA,TA_MAX,TA_MIN,    HM,    SS,CA_TOT,=
2001,108,  1,  1,  -1.2,   3.0,  -5.0,  64.2,   4.1,   4.5,=
2001,108,  1,  2,  -1.4,   2.2,  -4.5,  68.7,   4.0,   5.0,=
```

**핵심 컬럼 해설**

- **TA**: 평균기온 – 기후변화 추세 분석의 핵심 지표
- **TA_MAX/TA_MIN**: 최고/최저기온 – 극값 분석에 활용
- **HM**: 상대습도 – 체감온도 및 건조 위험도 평가
- **SS**: 일조시간 – [태양광 발전량 예측](https://doyouknow.kr/solar-forecast-api-python-guide/) 에 필수
- **CA_TOT**: 전운량 – 맑은 날/흐린 날 비율 파악

## 1단계: 초간단 API 호출 (Simple Version)

복잡한 이론은 접어두고, 일단 코드부터 돌려봅시다. 맛을 봐야 요리를 배우죠!

**가장 간단한 호출 코드**

```
import requests

# 기상청 API 설정
API_KEY = "YOUR_API_KEY"
base_url = "https://apihub.kma.go.kr/api/typ01/url/sun_sfc_norm.php"

# 파라미터 설정: 서울(108)의 2021년 기준 1월 평년값
params = {
    'authKey': API_KEY,
    'stn': 108,          # 서울
    'norm': 'D',         # 일 평년값
    'tmst': 2021,        # 1991~2020년 평년값
    'MM1': 1,            # 1월
    'DD1': 1,
    'MM2': 1,
    'DD2': 5             # 1월 1~5일
}

# API 호출
response = requests.get(base_url, params=params)
print(response.text)
```

**실행 결과**

터미널에 주석과 함께 데이터가 출력됩니다. 이게 바로 **30년간 서울의 1월 초 기온 평균**입니다!

```
# ST,STN, MM, DD,    TA,TA_MAX,TA_MIN,    HM,    SS,CA_TOT,=
2021,108,  1,  1,  -1.8,   2.2,  -5.3,  58.3,   5.2,   3.7,=
2021,108,  1,  2,  -1.9,   2.1,  -5.4,  57.1,   5.4,   3.5,=
2021,108,  1,  3,  -1.9,   2.2,  -5.4,  56.7,   5.3,   3.7,=
2021,108,  1,  4,  -1.8,   2.2,  -5.3,  56.8,   5.3,   3.7,=
2021,108,  1,  5,  -1.9,   2.2,  -5.5,  57.0,   5.2,   3.7,=
```

데이터 끝에 `=`가 붙어있고, 헤더가 `#`으로 시작하는 **기상청 특유의 독특한 구조**입니다. 파싱에 주의해야 합니다!

## 2단계: 응답 데이터 파싱 (정제된 DataFrame 만들기)

API가 던져주는 날것의 CSV 텍스트를 **Pandas DataFrame**으로 변환해 봅시다. 기상청 특유의 `#` 주석과 마지막 `=` 기호를 제거하는 로직이 핵심입니다.

```
def parse_normal_data(response_text):
    """
    기상청 평년값 API 응답을 DataFrame으로 변환
    - # 주석 라인 제거
    - 마지막 = 기호 제거
    - 숫자형 변환
    """
    lines = response_text.split('\n')
    data_rows = []
    
    columns = ['ST', 'STN', 'MM', 'DD', 'TA', 'TA_MAX', 'TA_MIN', 'HM', 'SS', 'CA_TOT']
    
    for line in lines:
        line = line.strip()
        
# 빈 줄이나 주석 건너뛰기
        if not line or line.startswith('#'):
            continue
        
# 끝에 =, 콤마 제거
        line = line.rstrip('=,').strip()
        
# 콤마로 분리
        parts = [p.strip() for p in line.split(',')]
        
# 빈 문자열 제거
        parts = [p for p in parts if p]
        
# 정확히 10개 필드가 아니면 스킵
        if len(parts) != 10:
            continue
        
        data_rows.append(parts)
    
# DataFrame 생성
    df = pd.DataFrame(data_rows, columns=columns)
    
# 숫자형 변환
    numeric_cols = ['ST', 'STN', 'MM', 'DD', 'TA', 'TA_MAX', 'TA_MIN', 'HM', 'SS', 'CA_TOT']
    for col in numeric_cols:
        df[col] = pd.to_numeric(df[col], errors='coerce')
    
    return df
```

**실행**

```
response = requests.get(base_url, params=params)
df = parse_normal_data(response.text)
print(df.head())
```

**결과**

```
     ST  STN  MM  DD   TA  TA_MAX  TA_MIN    HM   SS  CA_TOT
0  2021  108   1   1 -1.8     2.2    -5.3  58.3  5.2     3.7
1  2021  108   1   2 -1.9     2.1    -5.4  57.1  5.4     3.5
2  2021  108   1   3 -1.9     2.2    -5.4  56.7  5.3     3.7
3  2021  108   1   4 -1.8     2.2    -5.3  56.8  5.3     3.7
4  2021  108   1   5 -1.9     2.2    -5.5  57.0  5.2     3.7
```

이제 엑셀 수준의 가독성을 확보했습니다!

## 3단계: 클래스로 체계적 구조화 (Advanced)

코드가 길어질수록 유지보수는 지옥이 됩니다. **클래스 기반 설계**로 깔끔하게 정리해 봅시다.

```
import requests
import pandas as pd
from typing import Optional

class KMANormalValueAPI:
    """
    기상청 평년값 API 클라이언트
    - 재사용 가능한 구조
    - 에러 핸들링 강화
    - 데이터 검증 기능 포함
    """
    BASE_URL = "https://apihub.kma.go.kr/api/typ01/url/sun_sfc_norm.php"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
    
    def fetch_norm_data(self, 
                        stn: int,
                        start_date: str,
                        end_date: str,
                        norm: str = 'D',
                        tmst: int = 2021) -> pd.DataFrame:
        """
        평년값 데이터 조회
        
        Parameters:
        - stn: 지점번호 (108=서울, 159=부산, 0=전체)
        - start_date: 'MMDD' 형식 (예: '0101')
        - end_date: 'MMDD' 형식 (예: '1231')
        - norm: 평년값 종류 (D=일, S=순, M=월, Y=연)
        - tmst: 평년값 기준 시기 (2021=1991~2020년)
        
        Returns:
        - DataFrame: 파싱된 평년값 데이터
        """
        params = {
            'authKey': self.api_key,
            'stn': stn,
            'norm': norm,
            'tmst': tmst,
            'MM1': int(start_date[:2]),
            'DD1': int(start_date[2:]),
            'MM2': int(end_date[:2]),
            'DD2': int(end_date[2:])
        }
        
        try:
            response = requests.get(self.BASE_URL, params=params, timeout=30)
            response.raise_for_status()
            return self._parse_response(response.text)
        except requests.exceptions.RequestException as e:
            print(f"API 호출 실패: {e}")
            return pd.DataFrame()
    
    def _parse_response(self, text: str) -> pd.DataFrame:
        """응답 텍스트를 DataFrame으로 변환"""
        lines = text.split('\n')
        data_rows = []
        columns = ['ST', 'STN', 'MM', 'DD', 'TA', 'TA_MAX', 'TA_MIN', 'HM', 'SS', 'CA_TOT']
        
        for line in lines:
            line = line.strip()
            if line.startswith('#') or not line:
                continue
            
            line = line.replace('=', '').strip()
            parts = [p.strip() for p in line.split(',')]
            
            if len(parts) == 10:
                data_rows.append(parts)
        
        df = pd.DataFrame(data_rows, columns=columns)
        
# 숫자형 변환
        numeric_cols = ['ST', 'STN', 'MM', 'DD', 'TA', 'TA_MAX', 'TA_MIN', 'HM', 'SS', 'CA_TOT']
        for col in numeric_cols:
            df[col] = pd.to_numeric(df[col], errors='coerce')
        
        return df
```

**실행**

```
api = KMANormalValueAPI(api_key="YOUR_API_KEY")
df_seoul = api.fetch_norm_data(stn=108, start_date='0101', end_date='0131')
print(f"조회된 데이터: {len(df_seoul)}일치")
print(df_seoul.describe())
```

**결과**

```
조회된 데이터: 31일치
           ST    STN    MM         DD         TA     TA_MAX     TA_MIN  \
count    31.0   31.0  31.0  31.000000  31.000000  31.000000  31.000000   
mean   2021.0  108.0   1.0  16.000000  -1.974194   2.048387  -5.522581   
std       0.0    0.0   0.0   9.092121   0.287481   0.335530   0.281318   
min    2021.0  108.0   1.0   1.000000  -2.500000   1.400000  -6.100000   
25%    2021.0  108.0   1.0   8.500000  -2.200000   1.750000  -5.700000   
50%    2021.0  108.0   1.0  16.000000  -1.900000   2.100000  -5.500000   
75%    2021.0  108.0   1.0  23.500000  -1.800000   2.200000  -5.300000   
max    2021.0  108.0   1.0  31.000000  -1.500000   2.600000  -5.000000   

              HM         SS     CA_TOT  
count  31.000000  31.000000  31.000000  
mean   56.193548   5.519355   3.567742  
std     1.471493   0.312431   0.337033  
min    53.000000   5.100000   2.900000  
25%    55.650000   5.300000   3.350000  
50%    56.600000   5.500000   3.700000  
75%    57.250000   5.700000   3.850000  
max    58.300000   6.200000   4.100000  
```

이제 코드가 **재사용 가능한 도구**로 승격했습니다!

## 4단계: 여러 지역 동시 조회 및 비교

전국의 평년값을 한 번에 수집하고 비교해 봅시다.

```
import time

# 주요 도시 지점번호
CITIES = {
    '서울': 108,
    '부산': 159,
    '인천': 112,
    '광주': 156,
    '대전': 133,
    '제주': 184
}

api = KMANormalValueAPI(api_key="YOUR_API_KEY")
results = {}

for city, stn in CITIES.items():
    print(f"[{city}] 평년값 조회 중...")
    df = api.fetch_norm_data(stn=stn, start_date='0101', end_date='1231')
    
    if not df.empty:
        results[city] = df
        print(f"  ✓ {len(df)}개 데이터 수집 완료")
    else:
        print(f"  ✗ 데이터 수집 실패")
    
    time.sleep(0.5)  # API 부하 방지

# 연평균 기온 비교
annual_avg = {city: df['TA'].mean() for city, df in results.items()}
sorted_cities = sorted(annual_avg.items(), key=lambda x: x[1], reverse=True)

print("\n=== 연평균 기온 순위 (평년값 기준) ===")
for rank, (city, temp) in enumerate(sorted_cities, 1):
    print(f"{rank}위: {city} {temp:.1f}°C")
```

**결과**

```
=== 연평균 기온 순위 (평년값 기준) ===
1위: 제주 16.2°C
2위: 부산 15.0°C
3위: 광주 14.2°C
4위: 대전 13.1°C
5위: 서울 12.9°C
6위: 인천 12.5°C
```

제주가 가장 따뜻하고, 고위도 지역이 가장 추운 것을 데이터로 확인할 수 있습니다!

## 5단계: 1년치 데이터 수집 및 CSV 저장

장기 분석을 위해 전체 연도 평년값을 수집하고 파일로 저장합니다.

```
import os

def collect_annual_normal_data(api_key: str, cities: dict, output_dir: str = "normal_data"):
    """
    여러 도시의 연간 평년값 데이터를 수집하고 CSV로 저장
    """
    os.makedirs(output_dir, exist_ok=True)
    api = KMANormalValueAPI(api_key=api_key)
    
    print(f"=== 평년값 데이터 수집 시작 ===")
    
    for city, stn in cities.items():
        print(f"\n[{city}] 데이터 요청 중...")
        
# 1년 전체 조회 (1월 1일 ~ 12월 31일)
        df = api.fetch_norm_data(stn=stn, start_date='0101', end_date='1231')
        
        if not df.empty:
# 날짜 컬럼 추가 (시각화용 - 가상의 연도 2024 사용)
            df['DATE'] = pd.to_datetime(
                '2024-' + df['MM'].astype(str) + '-' + df['DD'].astype(str),
                errors='coerce'
            )
            
# CSV 저장
            filename = os.path.join(output_dir, f"{city}_평년값_2021.csv")
            df.to_csv(filename, index=False, encoding='utf-8-sig')
            print(f"  ✓ 저장 완료: {filename} ({len(df)}개 행)")
        else:
            print(f"  ✗ 데이터 없음")
        
        time.sleep(0.5)
```

**실행**

```
collect_annual_normal_data(api_key="YOUR_API_KEY", cities=CITIES)
```

이제 `normal_data/` 폴더에 도시별 CSV 파일이 생성됩니다!

## 6단계: 데이터 가공 – 이상치와 결측치 처리

실전 데이터는 완벽하지 않습니다. 관측 장비 점검이나 오류로 **결측치(-99, -999 등)**가 포함될 수 있습니다.

### 6-1. 이상치 탐지 및 제거

```
def clean_normal_data(df: pd.DataFrame) -> pd.DataFrame:
    """
    평년값 데이터 정제
    - 이상치(-99, -999 등) 제거
    - 물리적으로 불가능한 값 제거
    """
    df_clean = df.copy()
    
# 이상치 마스킹 (-99, -999, -1000 등)
    for col in ['TA', 'TA_MAX', 'TA_MIN', 'HM', 'SS', 'CA_TOT']:
        if col in df_clean.columns:
            df_clean[col] = df_clean[col].replace([-99, -999, -1000], pd.NA)
    
# 물리적 한계 체크
    df_clean.loc[df_clean['TA']  50, 'TA'] = pd.NA         # 기온 > 50°C
    df_clean.loc[df_clean['HM'] > 100, 'HM'] = pd.NA        # 습도 > 100%
    df_clean.loc[df_clean['SS'] > 24, 'SS'] = pd.NA         # 일조 > 24시간
    
# 결측치 개수 확인
    missing_counts = df_clean.isna().sum()
    if missing_counts.sum() > 0:
        print(f"결측치 발견: {missing_counts[missing_counts > 0].to_dict()}")
    
    return df_clean
```

**실행**

```
df_seoul = pd.read_csv("normal_data/서울_평년값_2021.csv")
df_clean = clean_normal_data(df_seoul)
print(f"정제 후 데이터: {len(df_clean)}행")
```

### 6-2. 결측치 보간 (Interpolation)

선형 보간법으로 결측치를 채웁니다. 평년값은 급격한 변화가 적어 보간이 효과적입니다.

```
def interpolate_missing(df: pd.DataFrame) -> pd.DataFrame:
    """
    시계열 선형 보간으로 결측치 채우기
    """
    df_interpolated = df.copy()
    
# 숫자형 컬럼만 보간
    numeric_cols = ['TA', 'TA_MAX', 'TA_MIN', 'HM', 'SS', 'CA_TOT']
    for col in numeric_cols:
        if col in df_interpolated.columns:
            before = df_interpolated[col].isna().sum()
            df_interpolated[col] = df_interpolated[col].interpolate(method='linear')
            after = df_interpolated[col].isna().sum()
            
            if before > 0:
                print(f"{col}: {before}개 결측 → {after}개 (보간 완료)")
    
    return df_interpolated
```

**실행**

```
df_final = interpolate_missing(df_clean)
print(f"\n최종 결측치: {df_final.isna().sum().sum()}개")
```

### 6-3. 데이터 무결성 검증 (Sanity Check)

평년값이므로 극단적인 이상치는 없어야 합니다. Assert로 데이터 품질을 보증합니다.

```
def validate_data(df: pd.DataFrame):
    """
    데이터 무결성 검증
    """
# 습도는 0~100 사이여야 함
    assert df['HM'].between(0, 100).all(), "❌ 습도 데이터 오류!"
    
# 전운량은 0~10 사이 (1/10 단위)
    assert df['CA_TOT'].between(0, 10).all(), "❌ 전운량 데이터 오류!"
    
# 일조시간은 0~24 사이
    assert df['SS'].between(0, 24).all(), "❌ 일조시간 데이터 오류!"
    
# 기온 논리적 일관성 (최저 

실행

validate_data(df_final)결측치 처리 전략 요약상황처리 방법단발성 결측 (1~2개)선형 보간 (Linear Interpolation)연속 결측 (3개 이상)전후 평균값 또는 제거계절성 패턴같은 월일의 평년값으로 대체물리적 한계 초과이상치로 간주하여 제거7단계: 데이터 시각화 – 기후변화 추세 확인평년값 자체는 변하지 않지만, 서로 다른 평년값 기준(1991, 2001, 2011, 2021)을 비교하면 기후변화를 시각적으로 확인할 수 있습니다.import matplotlib.pyplot as plt
import matplotlib
plt.style.use('seaborn-v0_8-whitegrid')
# 폰트 설정 (앞 단계에서 설치한 나눔바른고딕 적용)
plt.rc('font', family='NanumBarunGothic')
plt.rcParams['axes.unicode_minus'] = False

def plot_normal_comparison():
    """
    서로 다른 평년값 기준 비교 (1981~2010 vs 1991~2020)
    """
    api = KMANormalValueAPI(api_key="key")

# 두 시기의 서울 평년값 조회 (월 평년값)
    df_old = api.fetch_norm_data(stn=108, start_date='0101', end_date='1231', norm='M', tmst=2011)
    df_new = api.fetch_norm_data(stn=108, start_date='0101', end_date='1231', norm='M', tmst=2021)

    fig, ax = plt.subplots(figsize=(12, 6))

    ax.plot(df_old['MM'], df_old['TA'], marker='o', label='1981~2010년 평년값', linewidth=2)
    ax.plot(df_new['MM'], df_new['TA'], marker='s', label='1991~2020년 평년값', linewidth=2)

    ax.set_xlabel('월', fontsize=12)
    ax.set_ylabel('평균기온 (°C)', fontsize=12)
    ax.set_title('서울 월평균기온 평년값 변화 (30년 단위)', fontsize=14, fontweight='bold')
    ax.legend(fontsize=11)
    ax.grid(True, alpha=0.3)
    ax.set_xticks(range(1, 13))

    plt.tight_layout()
    plt.savefig('평년값_비교.png', dpi=150)
    plt.show()

plot_normal_comparison()결과해석: 새로운 평년값이 전반적으로 0.31°C 상승한 것을 확인할 수 있습니다. 이것이 바로 기후변화의 증거입니다!8단계: 간단한 AI 모델 – 기상 요인 간 상관관계 분석평년값 자체는 고정값이지만, 기상 요소 간의 관계를 파악하는 회귀 분석이 가능합니다. 예를 들어, “구름 양(CA_TOT)과 습도(HM)가 기온(TA)에 미치는 영향”을 선형 회귀로 분석해 봅시다.선형 회귀 모델로 기온 예측from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score, mean_absolute_error
import matplotlib.pyplot as plt
import seaborn as sns

# 데이터 로드
df_seoul = pd.read_csv("normal_data/서울_평년값_2021.csv")

# 입력(X): 전운량, 습도, 일조시간 / 출력(y): 기온
X = df_seoul[['CA_TOT', 'HM', 'SS']]
y = df_seoul['TA']

# 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 모델 학습
model = LinearRegression()
model.fit(X_train, y_train)

# 예측 및 평가
y_pred = model.predict(X_test)
r2 = r2_score(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)

print(f"R² Score (결정 계수): {r2:.4f}")
print(f"MAE (평균 절대 오차): {mae:.2f}°C")
print(f"\n회귀 계수:")
print(f"  - 전운량 (CA_TOT): {model.coef_[0]:.4f}")
print(f"  - 습도 (HM): {model.coef_[1]:.4f}")
print(f"  - 일조시간 (SS): {model.coef_[2]:.4f}")
print(f"절편: {model.intercept_:.4f}")

# 시각화: 실제 기온 vs 예측 기온
plt.figure(figsize=(10, 6))
plt.scatter(y_test, y_pred, alpha=0.7)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
plt.xlabel("실제 기온 (평년값)", fontsize=12)
plt.ylabel("예측 기온", fontsize=12)
plt.title("기상 요인과 기온의 관계 분석 (Linear Regression)", fontsize=14, fontweight='bold')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('기온_상관관계_분석.png', dpi=150)
plt.show()분석 결과R² Score (결정 계수): 0.9655
MAE (평균 절대 오차): 1.49°C

회귀 계수:
  - 전운량 (CA_TOT): 3.3151
  - 습도 (HM): 1.1371
  - 일조시간 (SS): 6.4362
절편: -111.1345해석일조시간, 전운량, 습도를 알면 평년값 데이터에서 기온의 96%를 예측할 수 있다.
```

- **회귀식**:$$\text{기온} = -111.1345 + 3.3151 \times \text{전운량} + 1.1371 \times \text{습도} + 6.4362 \times \text{일조시간}$$
- **모델 성능**: R² Score: 0.9655 (기온 변동의 96.55% 설명), MAE: 1.49°C (±1.5°C 범위 내 예측 오차)
- **해석 주의사항**: 회귀 계수는 **다중공선성(multicollinearity)** 영향으로 왜곡될 수 있습니다.
- ✅ “평년 수준이라면 기온은 얼마여야 하는가?” → 예측 가능
- ✅ 이상기후 탐지 → 예측값과 실제값의 차이로 판단
- ❌ “습도가 기온을 올린다”는 인과관계 → 신뢰 불가

이 모델을 통해 **“평년 기준선”을 확보**할 수 있습니다:

- **에너지**: 냉난방 수요 예측
- **농업**: 작물 생육 이상 감지
- **방재**: 이상기후 경보 시스템

### 상관관계 히트맵 시각화

```
# 상관계수 계산
correlation_matrix = df_seoul[['TA', 'TA_MAX', 'TA_MIN', 'HM', 'SS', 'CA_TOT']].corr()

# 히트맵 시각화
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', center=0, 
            square=True, linewidths=1, cbar_kws={"shrink": 0.8})
plt.title('기상 요소 간 상관관계 히트맵 (평년값)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('상관관계_히트맵.png', dpi=150)
plt.show()
```

## 9단계: 심화 AI 모델 – LSTM으로 평년값 패턴 학습하기

평년값 자체는 고정값이지만, **실제 관측값과 평년값의 차이(편차)**를 예측하는 AI 모델을 만들 수 있습니다. 이는 [기상청 일통계 데이터](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)
와 결합하면 더욱 강력해집니다.

### 평년값 기반 이상기후 탐지 모델

```
import torch
import torch.nn as nn
import numpy as np
from sklearn.preprocessing import MinMaxScaler

class AnomalyDetectionLSTM(nn.Module):
    """
    평년값 대비 이상 패턴 감지 LSTM
    """
    def __init__(self, input_size=6, hidden_size=64, num_layers=2, output_size=1):
        super().__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, 
                            batch_first=True, dropout=0.2)
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x):
        lstm_out, _ = self.lstm(x)
        last_output = lstm_out[:, -1, :]
        prediction = self.fc(last_output)
        return prediction

# 데이터 준비 (평년값 + 실제 관측값)
df_normal = pd.read_csv("normal_data/서울_평년값_2021.csv")
# df_actual = pd.read_csv("actual_data/서울_2023.csv")  # 실제 관측 데이터 필요

# 편차 계산 (가상 예시)
# df_merged = pd.merge(df_actual, df_normal, on=['MM', 'DD'], suffixes=('_actual', '_normal'))
# df_merged['TA_anomaly'] = df_merged['TA_actual'] - df_merged['TA_normal']

# 이상치 라벨링 (편차 > ±3°C = 이상기후)
# df_merged['is_anomaly'] = (df_merged['TA_anomaly'].abs() > 3).astype(int)

# print(f"이상기후 발생 일수: {df_merged['is_anomaly'].sum()}일 / {len(df_merged)}일")
```

### 모델 학습 및 이상기후 예측

```
from torch.utils.data import DataLoader, TensorDataset

# 특징 추출 (실제 데이터 필요)
# features = ['TA_normal', 'TA_MAX_normal', 'TA_MIN_normal', 'HM_normal', 'SS_normal', 'CA_TOT_normal']
# X = df_merged[features].values
# y = df_merged['is_anomaly'].values

# 정규화
# scaler = MinMaxScaler()
# X_scaled = scaler.fit_transform(X)

# 시퀀스 생성 (7일 윈도우)
def create_sequences(X, y, seq_length=7):
    X_seq, y_seq = [], []
    for i in range(len(X) - seq_length):
        X_seq.append(X[i:i+seq_length])
        y_seq.append(y[i+seq_length])
    return np.array(X_seq), np.array(y_seq)

# 훈련/테스트 분할 및 학습 (실제 데이터 준비 후 실행)
print("⚠️ 이 코드는 실제 관측값 데이터와 결합하여 사용합니다.")
print("평년값만으로는 시계열 예측이 불가능하므로,")
print("[기상청 일통계 API](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)와 함께 사용하세요!")
```

**활용 시나리오**

1. **평년값 API**로 기준선 확보
2. **[일통계 API](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)**로 실제 관측 데이터 수집
3. **편차 계산**: `실제값 - 평년값`
4. **LSTM 모델**로 편차 패턴 학습 및 이상기후 예측

이 방식으로 **“평년 대비 이상 고온/저온 발생 확률”**을 예측할 수 있습니다!

## 10단계: 실전 활용 – 농업 기후 컨설팅 시스템

평년값 데이터를 활용한 실용적인 사례를 살펴봅시다.

### 작물 재배 적기 판단 시스템

```
def recommend_planting_date(crop: str, region: str):
    """
    평년값 기반 작물 파종 시기 추천
    """
# 평년값 로드
    df_normal = pd.read_csv(f"normal_data/{region}_평년값_2021.csv")
    
# 작물별 생육 최적 온도 (예시)
    CROP_TEMP = {
        '벼': {'min_temp': 15, 'optimal_temp': 25},
        '고추': {'min_temp': 18, 'optimal_temp': 28},
        '배추': {'min_temp': 4, 'optimal_temp': 20}
    }
    
    if crop not in CROP_TEMP:
        return "지원하지 않는 작물입니다."
    
    min_temp = CROP_TEMP[crop]['min_temp']
    
# 평균기온이 최소 온도 이상인 첫 날 찾기
    suitable_dates = df_normal[df_normal['TA'] >= min_temp]
    
    if suitable_dates.empty:
        return f"{region}은 {crop} 재배에 적합하지 않습니다."
    
    first_date = suitable_dates.iloc[0]
    month, day = int(first_date['MM']), int(first_date['DD'])
    
    return f"{region} {crop} 권장 파종 시기: {month}월 {day}일 이후 (평년 기준)"
```

**실행**

```
print(recommend_planting_date('벼', '서울'))
print(recommend_planting_date('고추', '제주'))
```

**결과**

```
서울 벼 권장 파종 시기: 4월 28일 이후 (평년 기준)
제주 고추 권장 파종 시기: 5월 14일 이후 (평년 기준)
```

## 평년값 vs 실측값 – 무엇이 다를까?

평년값 API와 함께 알아야 할 중요한 개념이 있습니다.

| 비교 항목 | 평년값 API | 실측값 API (일통계) |
| --- | --- | --- |
| 데이터 성격 | 30년 평균값 (고정) | 실시간 관측값 (매일 갱신) |
| 용도 | 기준선 설정, 이상기후 판단 | 현재 날씨 분석, 예측 모델 학습 |
| 변경 주기 | 10년마다 | 매일 |
| 활용 예시 | “평년보다 3도 높음” | “오늘 서울 최고기온 32도” |
| 시계열 예측 | 불가능 (고정값) | 가능 (변동값) |

**조합 활용 전략**

1. **평년값 API**로 기준선 확보 ← **오늘 학습**
2. **[일통계 API](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)**로 실제 관측 데이터 수집
3. **편차 분석**으로 이상기후 감지
4. **LSTM 모델**로 향후 편차 예측

## 맺음말

기상청 평년값 API는 **30년의 기후 기억을 1초 만에 불러낼 수 있는 마법**입니다.

**오늘 배운 내용 총정리:**

✅ **평년값 개념**: 30년 평균으로 기후 기준선 설정 (WMO 표준)  
✅ **API 활용**: 지점별, 기간별 평년값 조회 (텍스트 형식 주의!)  
✅ **데이터 정제**: 이상치 제거 및 결측치 보간, Assert 검증  
✅ **시각화**: 평년값 변화로 기후변화 추세 확인 (+0.3~0.5°C 상승)  
✅ **상관관계 분석**: LinearRegression으로 기상 요소 간 관계 파악 (R²=0.82)  
✅ **AI 모델**: LSTM으로 이상기후 탐지 (실측값과 결합 시)  
✅ **실전 활용**: 농업 컨설팅, 에너지 수요 예측

기후변화는 이제 선택이 아닌 **생존의 문제**입니다. 평년값 데이터로 무장한 여러분이라면, 변화하는 기후에 한 발 앞서 대응할 수 있을 것입니다.

이전 포스팅에서 만든 **[LSTM 예측 모델](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)**에 이 **평년값 데이터를 추가 Feature**로 넣어보세요. 모델이 계절적 추세를 훨씬 더 잘 이해하게 되어 예측 성능이 비약적으로 상승할 것입니다!

데이터는 널려 있습니다. 줍는 사람이 임자입니다. 지금 바로 시작하세요!

## 외부 참고 자료

- [기상청 API 허브 – 산업특화(태양광)](https://apihub.kma.go.kr/specialApiList.do)
- [기상청 기후평년값 설명](https://data.kma.go.kr/climate/average30Years/selectAverage30YearsKoreaList.do)
- [세계기상기구(WMO) 평년값 가이드라인](https://public.wmo.int/en)
- [PyTorch 공식 튜토리얼](https://docs.pytorch.org/tutorials/)
- [Pandas 시계열 데이터 처리 가이드](https://pandas.pydata.org/docs/user_guide/timeseries.html)
- [Scikit-learn Linear Regression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html)

## 같이보기 (내부 참조 링크)

- [내일의 태양광 발전량, 파이썬으로 1초 만에 예측하기 (feat. 동서발전 API)](https://doyouknow.kr/solar-forecast-api-python-guide/)
- [기상청 산업특화(태양광) API로 PyTorch LSTM 날씨 예측 모델 만들기: 지상관측 일통계 완전 정복](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)
- [RNN과 LSTM – 시간을 기억하는 AI의 마법](https://doyouknow.kr/rnn-lstm-gru-time-series-natural-language-processing/)
- [과적합 해결 못하면 AI 프로젝트 70% 실패](https://doyouknow.kr/overfitting-underfitting-regularization-dropout-solution/)
- [하이퍼파라미터 튜닝 하나로 AI 성능 2배](https://doyouknow.kr/hyperparameter-tuning-optuna-bayesian-optimization-guide/)

### 이 글 공유하기:

- [Facebook으로 공유하기 (새 창에서 열림)Facebook](https://doyouknow.kr/kma-normal-value-api-guide/?share=facebook)
- [X에 공유 (새 창에서 열림)X](https://doyouknow.kr/kma-normal-value-api-guide/?share=x)
-

### 이것이 좋아요:

좋아하기로드 중...

### *관련*

### 관련된 글:

1. [태양광 데이터, 아직도 노가다? 기상청 무료 API로 1년치 일사량 10분 만에 자동 수집!](https://doyouknow.kr/kma-solar-radiation-sunshine-api-guide/)
2. [기상청 산업특화(태양광) API로 PyTorch LSTM 날씨 예측 모델 만들기: 지상관측 일통계 완전 정복](https://doyouknow.kr/kma-solar-energy-api-pytorch-lstm/)
3. [내일의 태양광 발전량, 파이썬으로 1초 만에 예측하기 (feat. 동서발전 API)](https://doyouknow.kr/solar-forecast-api-python-guide/)
4. [[AI 101] AI가 데이터를 이해하는 방법 – 데이터가 AI의 연료인 이유](https://doyouknow.kr/ai-data-importance-structured-unstructured-bias/)

### 댓글 남기기[응답 취소](/kma-normal-value-api-guide/#respond)
