본문 바로가기
GIS Tech/GIS Data Process

지방인허가데이터 활용하기 (Python)

by mpv 2021. 1. 7.

지방행정 인허가 데이터개방: www.localdata.go.kr/

 

LOCALDATA - 지방행정인허가데이터개방

지방행정 인허가 데이터개방 의료기관, 의료기기 데이터 보기 동물, 축산 데이터 보기 게임, 공연, 관광, 문화기획, 노래방, 비디오, 숙박, 여행, 영화, 음악 데이터 보기 미용, 이용, 세탁소/빨래

www.localdata.go.kr

서울 데이터에 한하여 다음의 링크에서 다운로드 가능: seoul_localdata.json Dropbox

(다른 지역은 아래의 코드 참조하여 정제하여 사용)


해당 웹페이지에서 전체 데이터, 업종별 데이터, 혹은 지역별 데이터를 다운로드 할 수 있다. 예를들어 서울시의 데이터를 다운로드 해보자.

XML, EXCEL, CSV의 포맷을 제공하지만 CSV나 EXCEL은 버전이나 파일의 인코딩 등에 의해 깨지는 경우가 있었다. 따라서 XML로 다운로드 하는것을 추천한다. 그러면 6110000_XML.zip과 같은 이름의 파일이 다운로드 될 것이다. 이를 zip으로 압축풀거나, 리눅스 환경에서는 

unzip -I EUC-KR 6110000_XML.zip -d seouldata_XML

다음의 명령어로 EUC-KR의 인코딩을 지켜가며 seouldata_XML이라는 경로에 데이터의 압축을 푼다.

Jupyter에서 실행한 경우

각 XML 파일을 Python에서 불러오는 과정에서는 각각의 파일의 크기가 크기때문에 기본적인 xml라이브러리보다는 lxml이라는 라이브러리를 사용하는것이 편하다. 설치는 pip install lxml 을 통해 가능하다.

import os
import json
from tqdm import notebook

# seouldata_json 경로가 없는 경우 새롭게 만든다
if not os.path.isdir('seouldata_json'):
    os.mkdir('seouldata_json')

# xml파일을 하나씩 읽어 json으로 변환하여 저장한다
for fname in notebook.tqdm(os.listdir('seouldata_XML')):
    doc = etree.parse('seouldata_XML/' + fname)
    root = doc.getroot()
    header = root[0]
    mpage = {r.tag:int(r.text) for r in header[1]}
    if mpage['totalCount'] == 0:
        continue
    body = root[1]
    mdata = [{elem.tag: elem.text for elem in row} for row in body[0]]
    assert mpage['totalCount'] == len(mdata)
    with open('seouldata_json/' + fname[:-3] + 'json', 'w') as fp:
        json.dump(mdata, fp)

XML파일에서 header 부분은 column과 paging으로 구성되어 있는데 column은 컬럼의 이름과 설명 (예: opnSvcNm는 개방서비스명 등) 과 paging은 파일내의 totalCount 를 가지고 있다. 위의 코드대로 totalCount가 0일 경우에는 body에 내용이 없기 때문에 위와 같이 데이터를 json의 형태로 변환하여 저장한다. 이후 pandas 등으로 데이터를 활용하면 된다.

 

참고로 위 데이터는 각 파일마다 조금씩 데이터 컬럼이 다르다. 공통된 컬럼만을 추출하여 데이터를 구축하고 싶다면 아래의 방법을 따르면 된다.

data_cols = []
for fname in notebook.tqdm(os.listdir('seouldata_json')):
    if fname[-4:] == 'json':
        with open('seouldata_json/' + fname) as fp:
            json_data = json.load(fp)
            data_cols.append(list(json_data[0].keys()))

먼저 각 json 파일의 첫번째 아이템의 keys 값들만을 data_cols의 리스트에 추가한다.

kcols = data_cols[0]
for cols in data_cols[1:]:
    nkcols = []
    for c in cols:
        if c in kcols:
            nkcols.append(c)
    kcols = nkcols

이후 data_cols[0]를 kcols값으로 두어 다른 파일에 있는 cols에서도 공통적으로 존재할 경우에만 해당 column을 남기는 식으로 하여 공통된 컬럼리스트인 kcols 를 구한다.

all_data = []
for fname in notebook.tqdm(os.listdir('seouldata_json')):
    if fname[-4:] == 'json':
        with open('seouldata_json/' + fname) as fp:
            json_data = json.load(fp)
            all_data.extend([{k:item[k] for k in kcols} for item in json_data])

마지막으로 all_data에 각 데이터마다 kcols에 해당하는 값만을 아이템으로 추가하여 데이터 리스트를 만들고,

import pandas as pd
df = pd.DataFrame(all_data)

이를 통해 전체 데이터를 df라는 데이터 프레임으로 만든다.

 

마지막으로 위 pandas 데이터를 geopandas 데이터로 변환하여 사용하고 싶다면 df['x']와 df['y']를 사용하면 되는데, 이 과정에서 약간의 데이터 정제가 필요하다.

 

df['x']와 df['y']의 경우 데이터가 비어있는 경우 (None)가 있기때문에 아래의 명령어로 None이 아닌 데이터를 필터링한다.

df = df[(df['x'] == df['x']) & (df['y'] == df['y'])].copy()

이후 df['x']값을 보면 값이 object 타입인 것을 알 수 있다. 이를 float 타입으로 변환시킨다.

df['x'] = df['x'].astype(float)
df['y'] = df['y'].astype(float)

그리고 다음과 같이 df.x와 df.y값을 이용하여 gdf라는 GeoDataFrame을 만들수가 있다. 

import geopandas as gpd
gdf = gpd.GeoDataFrame(
    df, geometry=gpd.points_from_xy(x=df.x, y=df.y)
)
gdf.crs = "+proj=tmerc +lat_0=38 +lon_0=127.0028902777778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +towgs84=-115.80,474.99,674.11,1.16,-2.31,-1.63,6.43"
ngdf = gdf.to_crs('EPSG:4326')

이때 localdata.go.kr의 데이터는 다음의 TM 좌표계를 사용한다.

+proj=tmerc +lat_0=38 +lon_0=127.0028902777778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +towgs84=-115.80,474.99,674.11,1.16,-2.31,-1.63,6.43

이를 이용하여 crs를 설정하고, ngdf라고 하는 EPSG:4326으로 변환된 (위도 경도 좌표계) 데이터로 시각화가 가능하다.

from cartoframes.viz import Layer, color_category_style
Layer(ngdf.sample(frac=1).iloc[:2000], color_category_style('opnSvcNm'))

Cartoframes 시각화

내가 주로 활용하는 CartoFrames의 예제를 참고하여 업종별로 지도상에 2000개의 샘플만 표현한 결과이다.

 

import os, zipfile

save_gdf = ngdf.copy()
save_gdf.crs = 'EPSG:4326'

save_dir = 'seouldata_gis'
save_name = 'seouldata_gis'

if not os.path.isdir(save_dir):
    os.mkdir(save_dir)
save_gdf.to_file(save_dir + '/' + save_name + '.shp', encoding='euc-kr')

fantasy_zip = zipfile.ZipFile(save_dir + '/' + save_name + '.zip', 'w')
for ext in ['cpg', 'dbf', 'shp', 'shx', 'prj']:
    fantasy_zip.write(save_dir + '/' + save_name + '.' + ext, save_name + '.' + ext, compress_type = zipfile.ZIP_DEFLATED)
fantasy_zip.close()

최종적으로 shp 파일의 형태로 저장하고 싶으면 다음과 같이 저장하면된다. 그러나 encoding이나 컬럼이름의 길이 등에 의해 데이터가 손실될 수 있다. 또한 shape파일을 일일히 저장하면 데이터 용량이 너무 커지기도 한다. 이 경우에

ngdf['lng'] = ngdf.geometry.x
ngdf['lat'] = ngdf.geometry.y
ncols = list(ngdf.columns)
ncols.remove('geometry')
ngdf[ncols].to_json('seoul_localdata.json')

다음과 같이 geometry를 제외한 나머지 컬럼들을 json의 형태로 저장하여 이후 필요할때 활용하면 된다.

 

전체 코드는 github.com/SuminHan/localdata.go.kr 에 정리되어 있다.

댓글