서울시 구별 경계 GeoJson 만들기

소환인·2023년 11월 23일
1

스터디노트

목록 보기
29/48

API 사용에 익숙해지기 위해 쓸데 없는 걸 만들며 연습하고 있습니다. 그 연습의 일환으로 V-World API를 활용하여 서울시 각 구의 경계 데이터를 수집하고, 이를 통합하여 GeoJSON 파일을 생성하는 과정을 진행해보겠습니다. 또한, 생성된 GeoJSON 파일을 이용하여 기본적인 지도 시각화를 시도해보겠습니다.

필요한 모듈 임포트

먼저 필요한 파이썬 라이브러리들을 임포트합니다.

import pandas as pd
import requests
import json
import folium

V-Wolrd에서는 다양한 국가공간정보를 제공하는데 그 중에서 시군구 경계에 대한 정보도 제공합니다. 서울시 구별 경계정보를 얻기 위해 먼저 서울시 구 목록을 서울 열린데이터 광장을 통해 얻어 오겠습니다.

서울열린데이터광장 - 구 목록 수집

API_key = 'API_KEY'
service = 'SearchFAQOfGUListService' 
gu_url = f'http://openapi.seoul.go.kr:8088/{API_key}/json/{service}/1/25'
gu_list = requests.get(gu_url).json()
print(gu_list)
{'SearchFAQOfGUListService': {'list_total_count': 25, 'RESULT': {'CODE': 'INFO-000', 'MESSAGE': '정상 처리되었습니다'}, 'row': [{'CODE': '300', 'CD_DESC': '종로구'}, {'CODE': '301', 'CD_DESC': '중구'}, {'CODE': '302', 'CD_DESC': '용산구'}, {'CODE': '303', 'CD_DESC': '성동구'}, {'CODE': '304', 'CD_DESC': '광진구'}, {'CODE': '305', 'CD_DESC': '동대문구'}, {'CODE': '306', 'CD_DESC': '중랑구'}, {'CODE': '307', 'CD_DESC': '성북구'}, {'CODE': '308', 'CD_DESC': '강북구'}, {'CODE': '309', 'CD_DESC': '도봉구'}, {'CODE': '310', 'CD_DESC': '노원구'}, {'CODE': '311', 'CD_DESC': '은평구'}, {'CODE': '312', 'CD_DESC': '서대문구'}, {'CODE': '313', 'CD_DESC': '마포구'}, {'CODE': '314', 'CD_DESC': '양천구'}, {'CODE': '315', 'CD_DESC': '강서구'}, {'CODE': '316', 'CD_DESC': '구로구'}, {'CODE': '317', 'CD_DESC': '금천구'}, {'CODE': '318', 'CD_DESC': '영등포구'}, {'CODE': '319', 'CD_DESC': '동작구'}, {'CODE': '320', 'CD_DESC': '관악구'}, {'CODE': '321', 'CD_DESC': '서초구'}, {'CODE': '322', 'CD_DESC': '강남구'}, {'CODE': '323', 'CD_DESC': '송파구'}, {'CODE': '324', 'CD_DESC': '강동구'}]}}

응답 결과를 데이터 프레임으로 변환하여 확인합니다.

df_gu = pd.DataFrame(gu_list['SearchFAQOfGUListService']['row'])
df_gu
CODE CD_DESC
0 300 종로구
1 301 중구
2 302 용산구
3 303 성동구
4 304 광진구
5 305 동대문구
6 306 중랑구
7 307 성북구
8 308 강북구
9 309 도봉구
10 310 노원구
11 311 은평구
12 312 서대문구
13 313 마포구
14 314 양천구
15 315 강서구
16 316 구로구
17 317 금천구
18 318 영등포구
19 319 동작구
20 320 관악구
21 321 서초구
22 322 강남구
23 323 송파구
24 324 강동구

브이월드 API - 시군구 경계도

이제 구 목록을 이용해 서울시 각 구의 경계 데이터를 V-World API를 사용해 수집합니다.

gu_json = []
vwolrd_key = 'AUTH KEY'
for gu in df_gu['CD_DESC']:
    url_vworld = f'https://api.vworld.kr/req/data?service=data&version=2.0&request=GetFeature&format=json&errorformat=json&size=10&page=1&data=LT_C_ADSIGG_INFO&attrfilter=sig_kor_nm:like:{gu}&columns=sig_cd,full_nm,sig_kor_nm,sig_eng_nm,ag_geom&geometry=true&attribute=true&key={vwolrd_key}&domain=https://localhost'
    result_dict = requests.get(url_vworld).json()
    gu_josn.append(result_dict)

gu_json
[{'response': {'service': {'name': 'data',
    'version': '2.0',
    'operation': 'GetFeature',
    'time': '26(ms)'},
   'status': 'OK',
   'record': {'total': '1', 'current': '1'},
   'page': {'total': '1', 'current': '1', 'size': '10'},
   'result': {'featureCollection': {'type': 'FeatureCollection',
     'bbox': [126.94889856321107,
      37.56581995079256,
      127.0233653243673,
      37.632374766651424],
     'features': [{'type': 'Feature',
       'geometry': {'type': 'MultiPolygon',
        'coordinates': [[[[127.00864326221883, 37.58046825113493],
           [127.00871274905404, 37.58045116421941],
           [127.00876564011087, 37.58044310616639],
           [127.00890785297045, 37.58042423069648],
           [127.00913781377906, 37.58039352848355],
           [127.00916523299747, 37.580390715633335],
           [127.00923792440939, 37.58038254796624],
           [127.00926870590294, 37.58037916725067],
           [127.0092779097711, 37.580378031403306],... 
                          ...]]]},
       'properties': {'sig_cd': '11350',
        'full_nm': '서울특별시 노원구',
        'sig_kor_nm': '노원구',
        'sig_eng_nm': 'Nowon-gu'},
       'id': 'LT_C_ADSIGG_INFO.8761'}]}}}},

응답 결과는 이렇게 구별로 시군구 코드, 구 이름, 구별 경계정보가 Json 형식으로 되어 있습니다.

GeoJSON 파일 생성 및 저장

수집한 데이터를 GeoJSON 형식으로 변환하고 파일로 저장합니다.

# 서울시 25개 구의 경계 데이터 수집 및 합치기
features = []
for gu_data in gu_json:  # gu_json 25개 구의 API 응답 데이터 리스트
   gu_name = gu_data['response']['result']['featureCollection']['features'][0]['properties']['sig_kor_nm']
   feature = {
       "type": "Feature",
       "id": gu_name,  # 구명을 id로 추가
       "geometry": gu_data['response']['result']['featureCollection']['features'][0]['geometry'],
       "properties": {
           "name": gu_name
       }
   }
   features.append(feature)

geojson_data = {
   "type": "FeatureCollection",
   "features": features
}

응답결과 중, 구별 경계정보만 정리해 딕셔너리 형태로 만듭니다. 나중에 시각화 과정을 편하게 하기 위해 구이름을 'id'로 추가합니다. 정리한 데이터를 이제 파일로 저장합니다.

# GeoJSON 파일 저장
with open('../data/seoul_gu_boundaries.geojson', 'w', encoding='utf-8') as f:
    json.dump(geojson_data, f, ensure_ascii=False)

ensure_ascii=False 이걸 지정해주지 않았을 땐, id로 지정해놓은 구이름이 아스키코드로 바뀌어서 저장되었었습니다.

지도 시각화

저장한 GeoJSON 파일을 이용해 서울시 구별 경계를 지도에 시각화합니다.

def style_function(feature):
    return {
        'opacity': 0.7,
        'weight': 1,
        'color': 'white',
        'fillOpacity': 0.2,
        'dashArray': '5, 5',
    }
# Folium 지도 객체 생성 및 GeoJson 레이어 추가
m = folium.Map(
    location=[37.5651, 126.98955], 
    zoom_start=11,
    tiles='cartodb dark_matter'
    )
folium.GeoJson(
    '../data/seoul_gu_boundaries.geojson',
    style_function=style_function
).add_to(m)

m

이번엔 지난번 웹 스크래핑을 통해 수집한 서울시 스타벅스 매장 데이터를 활용해 구별 스타벅스 매장수를 Choropleth로 시각화합니다.

# 구별 스타벅스 매장수
geo_path = '../data/seoul_gu_boundaries.geojson'
geo_str = json.load(open(geo_path, encoding='utf-8'))

starbucks_map = folium.Map(
    location=[37.5651, 126.98955], 
    zoom_start=11,
    tiles='cartodb dark_matter'
    )

folium.Choropleth(
    geo_data=geo_str,
    data=df_result,
    columns=['구', '스타벅스_매장수'],
    fill_color='YlGn',
    fill_opacity=0.7,
    line_opacity=0.5,
    key_on='feature.id'
).add_to(starbucks_map)

starbucks_map

시각화에 새로 만든 GeoJSON 파일을 사용해 봤는데 문제가 없어 보입니다.

정리

서울시 경계정보와 같은 GeoJSON 데이터는 인터넷 검색을 통해 쉽게 찾을 수 있지만, 이 포스트를 작성하면서 직접 데이터를 수집하고 변환하는 과정을 경험함으로써 필요한 지역의 시각화를 직접 해볼 수 있는 방법을 익힐 수 있었습니다. 비록 당장 필요하지 않은 GeoJSON 파일을 만드는 과정이었을지라도, 직접 만들어보며 느낀 성취감은 분명히 의미 있는 경험이었습니다. 이번 작업을 통해 API 사용에 더 익숙해지는 것이 목표였지만, 여전히 더 많은 연습과 경험이 필요함을 느낍니다.

profile
돌고돌아

0개의 댓글