문서의 이전 판입니다!
목차
동네예보 데이터
1. 개요
단기(동네)예보란 예보기간과 구역을 시·공간적으로 세분화하여 발표하는 예보입니다. 지역별, 시간별 차이로 인한 수요자의 불편을 최소화하기 위해 전국을 5km*5km 간격의 격자(동서 149(745km) × 남북 253(1.265km)), 총 37,697개로 나누어, 3시간 마다 읍, 면, 동 단위의 행정구역 중심으로 상세한 날씨를 제공합니다.
2. 요 소
1시간 기온, 낮 최고기온, 아침 최저기온, 풍향, 풍속, 동서바람성분, 남북바람성분, 하늘상태, 강수형태, 강수확률, 1시간 강수량, 1시간 신적설, 상대습도, 유의파고 요소를 제공합니다.
| 요소 파일명 | 단위 | Missing | 압축bit수 |
|---|---|---|---|
| 1시간 기온 | 1℃ | -50 | 10 |
| 일 최고기온 | 1℃ | -50 | 10 |
| 일 최저기온 | 1℃ | -50 | 10 |
| 동서바람성분 | 0.1m/s | -100 | 12 |
| 남북바람성분 | 0.1m/s | -100 | 12 |
| 풍향 | 0 deg | -1 | 10 |
| 풍속 | 0.1m/s | -1 | 10 |
| 하늘상태 | 코드값 | -1 | 4 |
| 강수형태 | 코드값 | -1 | 4 |
| 강수확률 | 1% | -1 | 8 |
| 1시간 강수량 | 범주(mm) | -1 | 8 |
| 1시간 신적설 | 범주(cm) | -1 | 8 |
| 습도 | 1% | -1 | 8 |
| 파고 | 0.1m | -1 | 8 |
※ 코드값 및 범주
- 하늘상태(SKY) 코드 : 맑음(1), 구름많음(3), 흐림(4)
- 강수형태(PTY) 코드 : 없음(0), 비(1), 비/눈(2), 눈(3), 소나기(4)
3. 발표시각
2시부터 3시간 간격으로 일 8회 발표합니다.
02시, 05시, 08시, 11시, 14시에 발표한 단기예보의 예보대상기간은 오늘(발표시각이 속한 날)부터 모레까지이며, 17시, 20시, 23시에 발표한 단기예보의 예보대상기간은 오늘부터 글피까지 입니다.
※ 단기예보를 활용하여 일 3회(05시, 11시, 17시) 통보문을 작성하고 이를 통보하고 있습니다.
4. 검색 및 획득
4.1. 검색·획득 – 기상자료개방포털
4.1.1. 자료 접근
- 사이트 : 기상자료개방포털 (data.kma.go.kr)
- 메뉴 : 데이터 → 기상예보 → 동네예보 → 단기예보
4.1.2. 자료 검색
- 조건 : 자료형태, 기간, 지점, 요소
4.2. 검색·획득 - 기상청 API허브
4.2.1. 자료 접근
- 사이트 : 기상청 API허브 (apihub.kma.go.kr)
- 메뉴 : 예·특보 → 단기예보
4.2.2. 자료 획득
- 절차: 회원가입 및 로그인 → API 메뉴 이동 → API 활용신청 → API호출
- 호출 방법: 기상청_API허브_사용_안내서.pdf
5. 활용방안
소스코드: fct_short.ipynb
5.1. 라이브러리 설치
# API 요청을 위한 requests 라이브러리와 # 분포도 표출을 위한 matplotlib 라이브러리, # 이미지 및 점의 좌표계 변환을 위한 rasterio, pyproj 라이브러리와 # 지도를 표출하기 위한 folium 라이브러리를 설치합니다. %pip install requests matplotlib rasterio pyproj folium
5.2. 동네예보 자료 다운로드
기상청 API허브에 회원가입 후 자신의 인증키를 사용하여 API 요청을 할 수 있습니다.
API 인증키를 코드상에 직접 남겨 사용하는 방식은 다른 사람에게 코드를 공유할때 인증키가 노출되는 문제가 생길 수 있습니다.
따라서 인증키를 환경변수로 등록하여 코드상에 노출되지 않도록 사용하는 것이 바람직합니다.
import os # 환경변수를 불러오는 표준 라이브러리 # 바람직하지 않은 방법 # my_api_key = 'MY_PRIVATE_API_AUTHENTICATE_KEY' # 인증키를 'myapikey'라는 이름의 환경변수로 미리 등록해놓은 뒤 이를 불러와서 사용합니다. my_api_key = os.environ['myapikey']
동네예보 격자영역 참고자료에 따르면 격자데이터는 가로 149, 세로 253개의 격자로 이루어져 있으며 해상도는 5km입니다.
또한 Lambert Conformal Conic 투영법을 사용하며 비관측영역의 경우 값을 -99로 표시합니다.
import requests # API 요청을 보내고 받는 라이브러리
import numpy as np # 배열을 다루는 라이브러리
# 동네예보(단기예보) 격자자료 다운로드 API url
api_url = 'https://apihub.kma.go.kr/api/typ01/cgi-bin/url/nph-dfs_shrt_grd'
# 요청인자로 발표시간, 발효시간, 예보변수가 있습니다.
# 1. 발표시간으로 년월일시를 입력합니다.
# 동네예보는 3시간 간격으로 생산되며
# 매일 2시, 5시, 8시, 11시, 14시, 17시, 20시, 23시에 발표됩니다.
time_forecast = '2024042508'
# 2. 발효시간으로 년월일시를 1시간 단위로 입력합니다.
# 발표시간을 기준으로 2, 5, 8, 11, 14시는 모레 자정까지,
# 17, 20, 23시는 글피 자정까지 제공합니다.
time_effect = '2024042612'
# 3. 예보변수를 입력합니다.
# TMP(기온), TMX(최고기온), TMN(최저기온), UUU(동서바람성분), VVV(남북바람성분),
# VEC(풍>향), WSD(풍속), SKY(하늘상태), PTY(강수형태), POP(강수유무), PCP(1시간 강수량),
# SNO(1시간 신적설), REH(상대습도), WAV(파고)를 입력할 수 있습니다.
variable = 'TMP'
# API 요청인자들을 묶어 dictionary로 정의합니다.
api_parameters = {
'tmfc': time_forecast,
'tmef': time_effect,
'vars': variable,
'authKey': my_api_key
}
# API 응답을 저장할 파일 형식은 txt 파일로 저장합니다.
file_name = f"{time_forecast}_{time_effect}_{variable}.txt"
# 저장된 파일이 없는 경우에만 API를 요청해 파일을 다운로드합니다.
if not os.path.exists(file_name):
# API 요청인자와 함께 API 요청
response = requests.get(api_url, params=api_parameters)
# 잘못된 응답을 받으면 에러메세지 출력
if response.status_code != 200:
raise requests.RequestException(
response.content.decode('utf-8')
)
# 받은 응답을 배열로 변환하여 모두 비관측영역 값(-99)을 가질 경우 에러 메세지 출력
elif (np.fromstring(
string=response.content,
dtype=np.float32,
count=253*149,
sep=','
) == -99).all():
raise ValueError("받은 응답이 올바르지 않습니다.")
# 그외의 올바른 응답에 대해서만 파일로 저장합니다.
else:
with open(file_name, 'wt') as f:
f.write(response.content.decode('utf-8'))
print(f'{file_name} 파일 다운로드 완료')
5.3. 동네예보 분포도 표출
# 참고자료의 설명에 맞게 각 변수들을 설정합니다. # 1. 배열의 x, y 크기를 정의합니다. nx = 149 ny = 253 # 2. 자료의 해상도를 m단위로 정의합니다. resolution = 5000 # 3. 자료의 투영법에 대응되는 위도와 기준 위경도, 기준 격자점을 정의합니다. lat_1 = 30 lat_2 = 60 lat_0 = 38 lon_0 = 126 origin_x = 43 origin_y = 136 # 앞에서 저장한 파일을 읽어 배열로 할당합니다. # 이때, 배열의 x, y와 row, column의 순서가 반대라는 것에 유의합니다. neighbor_array = np.fromfile(file_name, dtype=np.float32, sep=', ').reshape(ny, nx) # 배열의 비관측영역의 값(-99)을 나타내는 마스크를 정의합니다. # 그리고 그 영역을 -99에서 NaN값으로 변환합니다. nan_mask = (neighbor_array <= -99) neighbor_array[nan_mask] = np.nan # 배열의 행, 열 형태를 확인합니다. neighbor_array.shape
(253, 149)
동네예보 분포도를 표출하기 위해 기상청 기온 분포도에서 사용하는 색상표를 사용합니다.
# 분포도 표출을 위한 색상표
from matplotlib.colors import ListedColormap, BoundaryNorm, Normalize
# 동네예보 기온을 분포도로 표출하기 위해 62가지 색의 색상표를 RGB로 정의합니다.
colormap_temperature = ListedColormap(np.array([
# 흰색
[238, 238, 238],
# 보라색
[229, 172, 255], [218, 135, 255], [205, 97, 255], [194, 62, 255], [183, 31, 255],
[173, 7, 255], [160, 0, 247], [146, 0, 228], [135, 0, 206], [127, 0, 191],
# 파란색
[203, 204, 232], [179, 180, 222], [154, 155, 211], [128, 129, 199], [101, 103, 188],
[76, 78, 177], [52, 54, 167], [31, 33, 157], [13, 16, 150], [0, 3, 144],
# 하늘색
[172, 229, 255], [135, 217, 255], [97, 205, 255], [62, 193, 255], [31, 181, 255],
[7, 171, 255], [0, 157, 246], [0, 141, 222], [0, 128, 196], [0, 119, 179],
# 초록색
[150, 254, 150], [105, 252, 105], [64, 249, 64], [30, 243, 30], [8, 233, 8],
[0, 213, 0], [0, 189, 0], [0, 164, 0], [0, 142, 0], [0, 128, 0],
# 노란색
[255, 240, 154], [255, 234, 110], [255, 227, 67], [255, 220, 31], [255, 214, 4],
[249, 205, 0], [237, 195, 0], [224, 185, 0], [212, 176, 0], [204, 170, 0],
# 빨간색
[252, 171, 171], [250, 133, 133], [248, 96, 96], [246, 62, 62], [243, 33, 33],
[238, 11, 11], [227, 0, 0], [213, 0, 0], [200, 0, 0], [191, 0, 0],
# 검정색
[51, 51, 51]
], dtype=np.float32) / 255)
# 색상표에서 NaN값은 투명한 색상(RGBA 중 A가 0)을 나타내도록 합니다.
colormap_temperature.set_bad([0, 0, 0, 0])
# 색상표를 표출합니다.
colormap_temperature
# 분포도 표출을 위한 라이브러리
import matplotlib.pyplot as plt
# 분포도에서 색상표 위치, 크기를 조절하는 함수
from mpl_toolkits.axes_grid1 import make_axes_locatable
# 색상표의 각각의 색이 나타내는 값의 범위를 정합니다.
# 값의 범위는 표출할 배열이 가진 값의 범위에 따라 달라지도록 정합니다.
# 색상표에서 한 색상이 나타내는 범위는 0.5℃를 나타내도록 하며 같은 계열의 색상은 5단위에 맞게 정합니다.
# 배열의 평균 값이 색상표의 가운데에 위치하도록 색상표의 최대, 최소 범위를 계산합니다.
arr_mean = np.nanmean(neighbor_array)
arr_aligned_mean = arr_mean - (arr_mean % 5)
vmax = arr_aligned_mean + ((len(colormap_temperature.colors) - 2) * 0.5 / 2) + 0.5
vmin = arr_aligned_mean - ((len(colormap_temperature.colors) - 2) * 0.5 / 2) - 0.5
bounds = np.linspace(vmin, vmax, len(colormap_temperature.colors) + 1)
# 색상 개수와 값 범위를 서로 맞춥니다.
norm = BoundaryNorm(boundaries=bounds, ncolors=len(colormap_temperature.colors))
# 분포도에 표시되는 범위 값을 2칸 간격으로 구합니다.
ticks = bounds[1::2]
# 동네예보 배열 각각의 값을 색상표의 색이 나타내는 RGBA로 매핑시킵니다.
# 그리고 각 인덱스 범위를 0에서 1사이의 값으로 정규화합니다.
# 정규화를 통해 잃게된 투명색의 범위를 다시 지정합니다.
# 마지막으로 색상표의 색상으로 배열을 매핑합니다.
# 이때 배열의 형태를 (행 개수, 열 개수) 에서 (행 개수, 열 개수, RGBA)로 변환합니다.
colored_array = norm(neighbor_array)
colored_array = Normalize(
0, len(colormap_temperature.colors)
)(colored_array)
colored_array[nan_mask] = np.nan
colored_array = (colormap_temperature(colored_array) * 255).astype(np.uint8)
# 분포도를 그릴 크기를 지정합니다.
fig, ax = plt.subplots(1, 1, figsize=(7, 7))
# 배열을 표출하며 정의한 색상표와 범위를 지정합니다.
im = ax.imshow(colored_array, origin='lower', cmap=colormap_temperature, norm=norm)
# 분포도의 제목을 지정합니다.
ax.set_title(f'Forecast temperature in {time_effect}')
# 색상표의 크기와 위치를 조절합니다.
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size='5%', pad=0)
# 색상표에 표시될 글자 크기 및 제목을 설정합니다.
cbar = fig.colorbar(im, cax=cax, ticks=ticks)
cbar.ax.tick_params(labelsize=8)
cbar.ax.set_title('℃', fontsize=8)
# 분포도를 표출합니다.
plt.show()
위와 같은 이미지를 동적인 지도 위에 표출하기 전, 지도와 동네예보 자료(이미지)의 좌표계(Coordinate Reference System, CRS)를 맞추어야 합니다.
현재 동적인 지도가 사용하는 좌표계는 웹 상에서 지도를 표출할 때 많이 사용되는 EPSG:3857입니다.
동네예보 자료가 사용하는 좌표계는 Lambert Conformal Conic Projection(LCC)으로 한반도를 중심으로 표현하기 적합해 여러 데이터에서도 사용된다고 합니다.
동네예보 자료를 EPSG:3857로 변환하여 지도 위에 표출합니다.
# 좌표계 변환을 위한 변환 행렬
from rasterio.transform import Affine
# 좌표계 변환을 위해 필요한 함수
from rasterio.warp import calculate_default_transform, reproject, Resampling
# 동네예보 자료의 좌표계를 proj.4 형태의 문자열로 정의합니다.
# 투영법(Projection)은 LCC이며 LCC 좌표계 정의에 필요한 위도 선 2개(lat_1, lat_2)와
# 중심 위도, 경도(lat_0, lon_0)을 정의합니다.
# 좌표계의 x, y는 각각 오른쪽, 위로 증가하는 방향을 가지고(False easting, False northing)
# 지구 타원체를 WGS84로 정의합니다.
# 마지막으로 좌표계가 사용하는 단위는 미터(m)로 정의합니다.
source_crs = f"+proj=lcc +lat_1={lat_1} +lat_2={lat_2} +lat_0={lat_0} +lon_0={lon_0} +x_0=0 +y_0=0 +ellps=WGS84 +units=m +no_defs"
# 이미지(배열)의 행과 열이 LCC 좌표계에서의 좌표로 변환되기 위한 변환 행렬(Affine Transform Matrix)을 정의합니다.
# 변환 행렬은 먼저 이미지의 중심점을 (0, 0) 위치로 이동시킨 뒤(Translation)
# 각 이미지 픽셀이 실제로 5000m를 나타내므로 크기를 x, y로 5000배 키워주는(Scale) 역할을 합니다.
# 이때, 행렬의 곱하는 순서에 유의합니다.(AB != BA)
source_transform = Affine.scale(resolution, resolution) * Affine.translation(-origin_x, -origin_y)
# 변환 행렬을 거친 이미지가 나타내는 경계를 정의합니다.
source_bounds = {
'left': -origin_x * resolution,
'bottom': (ny - origin_y) * resolution,
'right': (nx - origin_x) * resolution,
'top': -origin_y * resolution
}
# 변환 후 이미지의 변환 행렬과 너비와 높이를 계산합니다.
dest_transform, dest_width, dest_height = calculate_default_transform(
src_crs=source_crs,
dst_crs='EPSG:3857',
width=nx,
height=ny,
**source_bounds,
)
# 변환 후의 이미지가 담길 비어있는 배열을 정의합니다.
converted_array = np.ones((dest_height, dest_width, 4), dtype=np.uint8)
# RGBA 각 채널에 대해 좌표계 변환을 수행합니다.
# 사용하는 resampling 기법으로 가까운 값을 선택하는 nearest를 선택합니다.
for i in range(4):
reproject(
source=colored_array[:, :, i],
destination=converted_array[:, :, i],
src_transform=source_transform,
src_crs=source_crs,
dst_transform=dest_transform,
dst_crs='EPSG:3857',
resampling=Resampling.nearest,
)
# 변환된 이미지를 분포도로 표출합니다.
# 이전 분포도와 다르게 원점의 위치, 너비, 높이가 달라졌음을 확인합니다.
fig, ax = plt.subplots(1, 1, figsize=(7, 7))
# 배열을 표출하며 정의한 색상표와 범위를 지정합니다.
im = ax.imshow(converted_array, cmap=colormap_temperature, norm=norm)
# 분포도의 제목을 지정합니다.
ax.set_title(f'After reprojection')
# 색상표의 크기와 위치를 조절합니다.
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size='5%', pad=0)
# 색상표에 표시될 글자 크기 및 제목을 설정합니다.
cbar = fig.colorbar(im, cax=cax, ticks=ticks)
cbar.ax.tick_params(labelsize=8)
cbar.ax.set_title('℃', fontsize=8)
# 분포도를 표출합니다.
plt.show()
마지막으로 변환된 이미지를 동적인 지도 위에 표출합니다.
import folium # 지도로 표출하기 위한 라이브러리
import branca.colormap as cm # 지도에 색상표를 표출하기 위한 색상표 모듈
# 지도에 마우스 위치를 표출하는 플러그인
from folium.plugins import MousePosition
# 점끼리의 좌표계 변환을 위한 transformer
from pyproj.transformer import Transformer
# folium을 이용한 좌표는 경도, 위도만 입력 가능하므로
# EPSG:3857 좌표계에서 EPSG:4326 좌표계로의 변환하는 transformer를 정의합니다.
degree_tranformer = Transformer.from_crs('EPSG:3857', 'EPSG:4326')
# 너비, 높이 크기가 800인 지도 생성
fig = folium.Figure(width=800, height=800)
# 지도의 중심과 나타낼 영역을 위·경도로 입력합니다.
map = folium.Map(
location=[38, 126],
zoom_start=6,
min_zoom=6,
min_lat=30,
max_lat=44,
min_lon=123,
max_lon=132,
max_bounds=True,
).add_to(fig)
# 지도에 색상표를 추가합니다.
map.add_child(cm.StepColormap(
[tuple(i) for i in colormap_temperature.colors],
vmin=vmin, vmax=vmax, tick_labels=bounds[1:-1:10].tolist(), caption="℃"
))
# 변환된 이미지를 지도 위에 겹쳐 그립니다.
# 불투명도를 0.4로 정하고
# 이미지가 그려질 범위를 경도, 위도로 입력합니다.
folium.raster_layers.ImageOverlay(
image=converted_array,
name='rain_rate',
opacity=0.4,
bounds=[
degree_tranformer.transform(*dest_transform.__mul__((0, dest_height))),
degree_tranformer.transform(*dest_transform.__mul__((dest_width, 0)))
],
zindex=1,
).add_to(map)
# 지도에 마우스 위치를 표출하는 플러그인과 layer를 조절하는 버튼을 추가합니다.
MousePosition().add_to(map)
folium.LayerControl().add_to(map)
# 지도를 표출합니다.
fig