====== 동네예보 데이터 ====== [[기상예보:날씨예보:단기예보#5. 활용방안|{{:wiki:활용방안클릭.png?linkonly250|}}]] ===== 1. 개요 ===== 단기(동네)예보란 예보기간과 구역을 시·공간적으로 세분화하여 발표하는 예보입니다. 지역별, 시간별 차이로 인한 수요자의 불편을 최소화하기 위해 전국을 5km*5km 간격의 격자(동서 149(745km) × 남북 253(1.265km)), 총 37,697개로 나누어, 3시간 마다 읍, 면, 동 단위의 행정구역 중심으로 상세한 날씨를 제공합니다. {{단기예보.png?nolink810}} ===== 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시) 통보문을 작성하고 이를 통보하고 있습니다. {{단기예보_예보자료시각.png?nolink700}} ===== 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 |기상청_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'] [[https://apihub.kma.go.kr/getAttachFile.do?fileName=(20240305)%EB%8F%99%EB%84%A4%EC%98%88%EB%B3%B4%20%EA%B2%A9%EC%9E%90%EC%98%81%EC%97%AD%20%EC%A0%95%EB%B3%B4.pdf|동네예보 격자영역 참고자료]]에 따르면 격자데이터는 가로 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 {{:기상예보:날씨예보:colormap.png?nolink&400|}} # 분포도 표출을 위한 라이브러리 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() {{:기상예보:날씨예보:forecast_temperature.png?nolink&400|}} 위와 같은 이미지를 동적인 지도 위에 표출하기 전, 지도와 동네예보 자료(이미지)의 ''좌표계(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() {{:기상예보:날씨예보:after_reprojection.png?nolink&400|}} 마지막으로 변환된 이미지를 동적인 지도 위에 표출합니다. 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 {{:기상예보:날씨예보:forecast_map.png?nolink&400|}}