""" 한국은행 ECOS Open API 거시경제 데이터 수집 모듈 BOK ECOS API를 통해 주요 거시경제변수를 수집: - GDP 실질성장률 - 실업률 - 한국은행 기준금리 - CD(91일) 금리 - 소비자물가지수 상승률 - 경기선행지수 순환변동치 API 문서: https://ecos.bok.or.kr/api/#/ """ import requests import pandas as pd import numpy as np import yaml from pathlib import Path from typing import Dict, List, Optional, Tuple import logging import time logger = logging.getLogger(__name__) class EcosAPI: """한국은행 ECOS Open API 클라이언트""" def __init__(self, api_key: str, base_url: str = "https://ecos.bok.or.kr/api"): self.api_key = api_key self.base_url = base_url def fetch_stat( self, stat_code: str, period: str = "A", # A=연간, Q=분기, M=월간 start_date: str = "2000", end_date: str = "2025", item_code1: str = "", item_code2: str = "", item_code3: str = "", ) -> pd.DataFrame: """ 개별 통계 시계열 데이터 조회 Parameters ---------- stat_code : str - 통계표코드 period : str - A(연간), Q(분기), M(월간) start_date : str - 검색시작일자 (YYYY, YYYYMM, YYYYQ1 등) end_date : str - 검색종료일자 item_code1~3 : str - 항목코드 Returns ------- pd.DataFrame with columns [TIME, STAT_NAME, ITEM_NAME, DATA_VALUE] """ # 항목코드가 비어있으면 공백 대체 ic1 = item_code1 if item_code1 else "?" ic2 = item_code2 if item_code2 else "?" ic3 = item_code3 if item_code3 else "?" url = ( f"{self.base_url}/StatisticSearch/" f"{self.api_key}/json/kr/1/100/" f"{stat_code}/{period}/{start_date}/{end_date}/" f"{ic1}/{ic2}/{ic3}" ) try: resp = requests.get(url, timeout=30) resp.raise_for_status() data = resp.json() if "StatisticSearch" not in data: error_msg = data.get("RESULT", {}).get("MESSAGE", "Unknown error") logger.warning(f"ECOS API 조회 실패 ({stat_code}): {error_msg}") return pd.DataFrame() rows = data["StatisticSearch"]["row"] df = pd.DataFrame(rows) # 숫자 변환 if "DATA_VALUE" in df.columns: df["DATA_VALUE"] = pd.to_numeric(df["DATA_VALUE"], errors="coerce") return df except requests.RequestException as e: logger.error(f"ECOS API 요청 실패: {e}") return pd.DataFrame() def search_stat_list(self, keyword: str) -> pd.DataFrame: """통계표 코드 검색""" url = ( f"{self.base_url}/StatisticTableList/" f"{self.api_key}/json/kr/1/100/{keyword}" ) try: resp = requests.get(url, timeout=30) data = resp.json() if "StatisticTableList" in data: return pd.DataFrame(data["StatisticTableList"]["row"]) return pd.DataFrame() except Exception as e: logger.error(f"통계표 검색 실패: {e}") return pd.DataFrame() def collect_macro_data( api_key: str, start_year: int = 2000, end_year: int = 2025 ) -> pd.DataFrame: """ 주요 거시경제변수 일괄 수집 Parameters ---------- api_key : str - ECOS API 인증키 start_year : int - 시작 연도 end_year : int - 종료 연도 Returns ------- pd.DataFrame index=연도, columns=[GDP_GROWTH, UNEMPLOYMENT, BASE_RATE, CD_RATE, CPI_GROWTH, LEADING_INDEX] """ api = EcosAPI(api_key) start = str(start_year) end = str(end_year) macro_vars = {} # ------------------------------------------------------- # 1) GDP 실질성장률 (%) # 통계표: 902Y015 (국제 주요국 경제성장률) / 항목: KOR # ------------------------------------------------------- logger.info("GDP 성장률 조회 중...") df_gdp = api.fetch_stat("902Y015", "A", start, end, "KOR") if not df_gdp.empty: gdp_series = df_gdp.set_index("TIME")["DATA_VALUE"].astype(float) gdp_series.index = gdp_series.index.astype(int) macro_vars["GDP_GROWTH"] = gdp_series time.sleep(0.5) # API rate limit # ------------------------------------------------------- # 2) 실업률 (%) # 통계표: 901Y027 (경제활동인구) / 항목: I61BC (실업률) # ------------------------------------------------------- logger.info("실업률 조회 중...") df_unemp = api.fetch_stat("901Y027", "A", start, end, "I61BC") if not df_unemp.empty: unemp_series = df_unemp.set_index("TIME")["DATA_VALUE"].astype(float) unemp_series.index = unemp_series.index.astype(int) macro_vars["UNEMPLOYMENT"] = unemp_series time.sleep(0.5) # ------------------------------------------------------- # 3) 한국은행 기준금리 (%, 연말 기준) # 통계표: 722Y001 # ------------------------------------------------------- logger.info("기준금리 조회 중...") df_rate = api.fetch_stat("722Y001", "A", start, end, "0101000") if not df_rate.empty: rate_series = df_rate.set_index("TIME")["DATA_VALUE"].astype(float) rate_series.index = rate_series.index.astype(int) macro_vars["BASE_RATE"] = rate_series time.sleep(0.5) # ------------------------------------------------------- # 4) CD(91일) 금리 (%) # 통계표: 721Y001 (시장금리) / 항목: 2010000 (CD 91일) # ------------------------------------------------------- logger.info("CD 금리 조회 중...") df_cd = api.fetch_stat("721Y001", "A", start, end, "2010000") if not df_cd.empty: cd_series = df_cd.set_index("TIME")["DATA_VALUE"].astype(float) cd_series.index = cd_series.index.astype(int) macro_vars["CD_RATE"] = cd_series time.sleep(0.5) # ------------------------------------------------------- # 5) 소비자물가지수 상승률 (%) # 통계표: 901Y009 / 항목: 0 (총지수) # 지수(level)로 조회 후 전년대비 상승률(%) 계산 # ------------------------------------------------------- logger.info("소비자물가 상승률 조회 중...") # 전년도까지 필요 → start를 1년 앞당겨 조회 df_cpi = api.fetch_stat("901Y009", "A", str(start_year - 1), end, "0") if not df_cpi.empty: cpi_level = df_cpi.set_index("TIME")["DATA_VALUE"].astype(float) cpi_level.index = cpi_level.index.astype(int) cpi_level = cpi_level.sort_index() # 전년대비 증가율 (%) cpi_growth = cpi_level.pct_change() * 100 cpi_growth = cpi_growth.loc[start_year:end_year] macro_vars["CPI_GROWTH"] = cpi_growth time.sleep(0.5) # ------------------------------------------------------- # 6) 경기선행종합지수 # 통계표: 901Y067 / 항목: I16A (선행종합지수) # 월별만 존재 → 월별 조회 후 연평균 산출 # ------------------------------------------------------- logger.info("경기선행지수 조회 중...") df_leading = api.fetch_stat( "901Y067", "M", f"{start_year}01", f"{end_year}12", "I16A" ) if not df_leading.empty: monthly = df_leading[["TIME", "DATA_VALUE"]].copy() monthly["DATA_VALUE"] = monthly["DATA_VALUE"].astype(float) monthly["YEAR"] = monthly["TIME"].str[:4].astype(int) annual_avg = monthly.groupby("YEAR")["DATA_VALUE"].mean() annual_avg = annual_avg.loc[start_year:end_year] macro_vars["LEADING_INDEX"] = annual_avg # DataFrame 결합 (각 Series의 인덱스를 정리하여 결합) if macro_vars: # 각 Series의 인덱스를 정수로 통일, 중복 제거 clean_vars = {} for name, series in macro_vars.items(): s = series.copy() s.index = s.index.astype(int) s = s[~s.index.duplicated(keep='first')] # 중복 제거 s = s.dropna() clean_vars[name] = s result = pd.DataFrame(clean_vars) result.index.name = "YEAR" result = result.sort_index() logger.info(f"ECOS API 데이터 수집 완료: {len(result)}개 연도, {len(result.columns)}개 변수") return result else: logger.warning("거시경제 데이터 수집 실패. 내장 fallback 데이터 사용.") return _fallback_macro_data(start_year, end_year) def _fallback_macro_data(start_year: int = 2000, end_year: int = 2025) -> pd.DataFrame: """ API 실패시 사용할 내장 fallback 거시경제 데이터 출처: 한국은행 경제통계시스템 (실제 공표 수치 기반) """ data = { 2000: {"GDP_GROWTH": 8.9, "UNEMPLOYMENT": 4.4, "BASE_RATE": 5.25, "CD_RATE": 7.09, "CPI_GROWTH": 2.3, "LEADING_INDEX": 101.2}, 2001: {"GDP_GROWTH": 4.5, "UNEMPLOYMENT": 4.0, "BASE_RATE": 4.00, "CD_RATE": 5.34, "CPI_GROWTH": 4.1, "LEADING_INDEX": 99.5}, 2002: {"GDP_GROWTH": 7.4, "UNEMPLOYMENT": 3.3, "BASE_RATE": 4.25, "CD_RATE": 4.99, "CPI_GROWTH": 2.8, "LEADING_INDEX": 102.3}, 2003: {"GDP_GROWTH": 2.9, "UNEMPLOYMENT": 3.6, "BASE_RATE": 3.75, "CD_RATE": 4.24, "CPI_GROWTH": 3.5, "LEADING_INDEX": 98.8}, 2004: {"GDP_GROWTH": 4.9, "UNEMPLOYMENT": 3.7, "BASE_RATE": 3.25, "CD_RATE": 3.77, "CPI_GROWTH": 3.6, "LEADING_INDEX": 100.5}, 2005: {"GDP_GROWTH": 3.9, "UNEMPLOYMENT": 3.7, "BASE_RATE": 3.75, "CD_RATE": 3.81, "CPI_GROWTH": 2.8, "LEADING_INDEX": 101.8}, 2006: {"GDP_GROWTH": 5.2, "UNEMPLOYMENT": 3.5, "BASE_RATE": 4.50, "CD_RATE": 4.72, "CPI_GROWTH": 2.2, "LEADING_INDEX": 102.5}, 2007: {"GDP_GROWTH": 5.5, "UNEMPLOYMENT": 3.2, "BASE_RATE": 5.00, "CD_RATE": 5.36, "CPI_GROWTH": 2.5, "LEADING_INDEX": 103.1}, 2008: {"GDP_GROWTH": 2.8, "UNEMPLOYMENT": 3.2, "BASE_RATE": 3.00, "CD_RATE": 5.70, "CPI_GROWTH": 4.7, "LEADING_INDEX": 96.5}, 2009: {"GDP_GROWTH": 0.8, "UNEMPLOYMENT": 3.6, "BASE_RATE": 2.00, "CD_RATE": 2.63, "CPI_GROWTH": 2.8, "LEADING_INDEX": 98.2}, 2010: {"GDP_GROWTH": 6.8, "UNEMPLOYMENT": 3.7, "BASE_RATE": 2.50, "CD_RATE": 2.80, "CPI_GROWTH": 2.9, "LEADING_INDEX": 103.0}, 2011: {"GDP_GROWTH": 3.7, "UNEMPLOYMENT": 3.4, "BASE_RATE": 3.25, "CD_RATE": 3.55, "CPI_GROWTH": 4.0, "LEADING_INDEX": 101.2}, 2012: {"GDP_GROWTH": 2.4, "UNEMPLOYMENT": 3.2, "BASE_RATE": 2.75, "CD_RATE": 3.13, "CPI_GROWTH": 2.2, "LEADING_INDEX": 100.3}, 2013: {"GDP_GROWTH": 3.2, "UNEMPLOYMENT": 3.1, "BASE_RATE": 2.50, "CD_RATE": 2.72, "CPI_GROWTH": 1.3, "LEADING_INDEX": 100.8}, 2014: {"GDP_GROWTH": 3.2, "UNEMPLOYMENT": 3.5, "BASE_RATE": 2.00, "CD_RATE": 2.36, "CPI_GROWTH": 1.3, "LEADING_INDEX": 101.0}, 2015: {"GDP_GROWTH": 2.8, "UNEMPLOYMENT": 3.6, "BASE_RATE": 1.50, "CD_RATE": 1.72, "CPI_GROWTH": 0.7, "LEADING_INDEX": 100.5}, 2016: {"GDP_GROWTH": 2.9, "UNEMPLOYMENT": 3.7, "BASE_RATE": 1.25, "CD_RATE": 1.48, "CPI_GROWTH": 1.0, "LEADING_INDEX": 99.8}, 2017: {"GDP_GROWTH": 3.2, "UNEMPLOYMENT": 3.7, "BASE_RATE": 1.50, "CD_RATE": 1.52, "CPI_GROWTH": 1.9, "LEADING_INDEX": 101.5}, 2018: {"GDP_GROWTH": 2.9, "UNEMPLOYMENT": 3.8, "BASE_RATE": 1.75, "CD_RATE": 1.85, "CPI_GROWTH": 1.5, "LEADING_INDEX": 100.8}, 2019: {"GDP_GROWTH": 2.2, "UNEMPLOYMENT": 3.8, "BASE_RATE": 1.25, "CD_RATE": 1.63, "CPI_GROWTH": 0.4, "LEADING_INDEX": 99.3}, 2020: {"GDP_GROWTH": -0.7, "UNEMPLOYMENT": 4.0, "BASE_RATE": 0.50, "CD_RATE": 0.76, "CPI_GROWTH": 0.5, "LEADING_INDEX": 97.0}, 2021: {"GDP_GROWTH": 4.3, "UNEMPLOYMENT": 3.7, "BASE_RATE": 1.00, "CD_RATE": 1.09, "CPI_GROWTH": 2.5, "LEADING_INDEX": 102.8}, 2022: {"GDP_GROWTH": 2.6, "UNEMPLOYMENT": 2.9, "BASE_RATE": 3.25, "CD_RATE": 3.77, "CPI_GROWTH": 5.1, "LEADING_INDEX": 99.2}, 2023: {"GDP_GROWTH": 1.4, "UNEMPLOYMENT": 2.7, "BASE_RATE": 3.50, "CD_RATE": 3.75, "CPI_GROWTH": 3.6, "LEADING_INDEX": 98.8}, 2024: {"GDP_GROWTH": 2.2, "UNEMPLOYMENT": 2.8, "BASE_RATE": 3.00, "CD_RATE": 3.30, "CPI_GROWTH": 2.3, "LEADING_INDEX": 99.5}, 2025: {"GDP_GROWTH": 1.8, "UNEMPLOYMENT": 3.0, "BASE_RATE": 2.75, "CD_RATE": 3.00, "CPI_GROWTH": 1.8, "LEADING_INDEX": 99.8}, } df = pd.DataFrame(data).T df.index.name = "YEAR" return df.loc[start_year:end_year] def load_macro_data(config_path: str = "config.yaml") -> pd.DataFrame: """ 설정 파일에서 API 키를 읽고 거시경제 데이터 수집 API 실패시 자동으로 fallback 데이터 사용 """ config = _load_config(config_path) api_key = config.get("ecos", {}).get("api_key", "sample") logger.info(f"ECOS API로 거시경제 데이터 수집 시작 (API key: {api_key[:4]}...)") try: df = collect_macro_data(api_key) if df.empty or len(df) < 10: logger.warning("API 데이터 부족. Fallback 데이터 사용.") df = _fallback_macro_data() return df except Exception as e: logger.warning(f"API 수집 실패: {e}. Fallback 데이터 사용.") return _fallback_macro_data() def _load_config(config_path: str) -> dict: """YAML 설정 파일 로딩""" try: with open(config_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) except FileNotFoundError: logger.warning(f"설정 파일 '{config_path}' 없음. 기본값 사용.") return {}