호기심 많은 분석가

[Python] 전국 행정구역별(법정동) 개수와 경계/중심 좌표 데이터 - GeoPandas 본문

Coding/시각화

[Python] 전국 행정구역별(법정동) 개수와 경계/중심 좌표 데이터 - GeoPandas

DA Hun 2022. 4. 14. 22:59

목차

     

    [Python] 전국 행정구역별 지도 시각화 (Folium, 일부-경기도/인천)

    Python 지도 시각화 Library folium을 사용해 행정구역별 시각화를 진행해보겠습니다. 필요 Site : SGIS(통계지리정보서비스), QGIS, 지도 셰이퍼  기존의 folium을 이용한 행정구역별 시각화 자료는 굉장히

    herjh0405.tistory.com

     이전 포스팅에서 우리는 지도 시각화를 하기 위해 SGIS의 shp 형태의 데이터를 QGIS와 지도 셰이퍼라는 사이트를 통해 JSON 파일로 변환시켜주었는데요. 추가 공부 중에 이 과정을 대폭 간소화시켜줄 수 있는 GeoPandas라는 라이브러리를 발견하게 되어, 이번 포스팅에서는 그 과정을 여러분들께 소개드리고자 합니다. 

     

     지난번 사용했던 SGIS 사이트의 데이터를 사용하셔도 문제없습니다. 자주 데이터를 최신화해주시는 사이트가 있어 이번에는 GIS DEVELOPER 사이트를 이용하게 되었습니다.

     

    전국 행정구역별(법정동) 개수와 중심 좌표 데이터

    • 데이터만 따로 필요하신 분도 있으실까 하여 결과물 첨부드립니다.
      • 전국 총 17개 시도, 260개 시군구, 5055개 읍면동, 15219개 리의 데이터로 이루어져 있습니다.
      • 경계좌표까지 포함하면 450MB로 20MB를 넘어 업로드가 불가해 경계좌표까지만 넣었습니다. 양해 부탁드립니다.

    bjd_info_except_boundary.csv
    4.12MB


    행정구역별 경계/중심 좌표 구하기

    Data 획득

    데이터 획득은 크게 어렵지 않습니다. GIS DEVELOPER를 방문해 필요한 데이터를 다운로드하여 줍니다. 저는 전국의 가장 최신 데이터에 대해 모두 취합하고자 하므로 2022년 3월 업데이트 분을 시도, 시군구, 읍면동, 리까지 총 4개의 데이터를 다운로드하였습니다. 

     

    대한민국 최신 행정구역(SHP) 다운로드 – GIS Developer

     

    www.gisdeveloper.co.kr

    Load Data

    import geopandas as gpd
    import pandas as pd
    import os
    
    path = '각 파일이 놓여있는 폴더 경로'
    sido = gpd.read_file(os.path.join(path, 'sido_20220324/ctp_rvn.shp'), encoding='cp949')
    sgg = gpd.read_file(os.path.join(path, 'sgg_20220324/sig.shp'), encoding='cp949')
    emd = gpd.read_file(os.path.join(path, 'emd_20220324/emd.shp'), encoding='cp949')
    li = gpd.read_file(os.path.join(path, 'li_20220324/li.shp'), encoding='cp949')

     그다음은 굉장히 간단합니다. 다운로드한 데이터를 geopandas를 이용해 read_file로 불러오면 코드와, 영문명, 국문명, 경계좌표가 확인됩니다. 이때 데이터 encoding은 'utf-8'이 아닌 'cp949'로 해야 정상적으로 불러와집니다.

     전 편에 비해 너무 쉽게 돼서 조금 당황스럽기도 했습니다.🤣 

     

    Geopandas를 이용해 불러온 데이터 확인
    데이터 확인

    중심좌표 추출

     하지만 경계좌표를 보면 우리가 흔히 아는 127, 37쯤 하는 좌표와 굉장히 숫자 차이가 나는 것을 확인할 수 있습니다. 위경도 기준이 다르기 때문인데요. to_crs 메소드를 이용해 folium에서 사용할 수 있게 변환해줍니다.

     

    to_crs 메소드를 통해 folium에서 사용할 수 있도록 변환
    좌표 변환 EPSG:4326

      그다음 필요한 중심좌표를 구하기 위해서는 .geometry.centroid 메소드를 적용해주면 되는데요. 아래와 같이 변환한 상태에서 중점을 구하면 값이 틀릴 수도 있다고 합니다. 꼭 중점을 구한 뒤 좌표 변환을 시도하는 걸 추천드립니다.

     

    좌표변환 후 ceotroid를 사용하자 경고문 발생
    좌표변환 후 ceotroid를 사용하자 경고문

    sido['center_point'] = sido['geometry'].geometry.centroid
    sido['geometry'] = sido['geometry'].to_crs(epsg=4326)
    sido['center_point'] = sido['center_point'].to_crs(epsg=4326)
    sido['경도'] = sido['center_point'].map(lambda x: x.xy[0][0])
    sido['위도'] = sido['center_point'].map(lambda x: x.xy[1][0])

    POLYGON을 통해 중점 및 위경도 추출


    행정구역별 개수 구하기

    여기까지가 제가 이번 포스팅에서 이야기하고자 했던 행정구역별 경계/중심 좌표 구하기였습니다. 이다음으로 제가 진행하고자 했던 것은 우선 저 4개의 데이터가 합쳐지는 것이었습니다. 예를 들어 읍면동 데이터에서 효자동을 발견하더라도 이게 경상북도의 효자동인지, 서울특별시의 효자동인지 직관적으로 확인할 수 없었기 때문이죠. 추가로 법정동코드는 종종 바뀌기 때문에 현재 국내 표준이 되는 행정표준관리시스템의 데이터와 일부 매칭 되지 않는 것도 있었는데요, 이 부분도 가장 최신의 국내 표준에 맞춘 데이터를 사용하고 개수를 구해보고자 합니다. 

     

    Data 획득

     행정표준관리시스템 > 코드검색 > 코드검색 > 법정동코드목록조회에서 법정동 코드 전체 자료 다운로드하여줍니다.

     

    행정표준관리시스템 내 법정동 코드 자료 다운로드
    행정표준관리시스템

    데이터 전처리

    1. 데이터 최신화 및 오류 데이터 수정
    2. 첫 번째 포스팅 글에서 적은 법정동코드 체계에 맞춰 10자리의 숫자를 시도/시군구/읍면동/리 단위로 분리(GIS DEVELOPER 데이터와 JOIN 할 수 있는 형태로 변환)

    데이터 오류 데이터 수정

    • 행정표준코드관리시스템의 데이터에서 폐지된 것은 제외하고, 오타 수정
      • 이 오타 건에 대해서는 행정안전부 운영지원단에 말씀드리고, 수정 요청드렸습니다. :) 
    • GIS DEVELOPER 데이터에서 최신화되어 있지 않은 부분이나 오타, 표기 차이 등을 수정
    df_code = pd.read_csv(os.path.join(path, '법정동코드 전체자료.txt'), encoding='cp949', sep='\t')
    df_code = df_code[df_code['폐지여부']=='존재']
    df_code['법정동코드'] = df_code['법정동코드'].astype('str')
    
    # 경기도 여주시 가남읍 화평리에 오타(빈공간) 존재
    df_code['법정동명'][7610] = '경기도 여주시 가남읍 화평리'
    
     오류 수기 수정 - 외에 더 있으나 데이터가 없음
    # 띄어쓰기
    sgg['SIG_KOR_NM'] = sgg['SIG_KOR_NM'].map(lambda x: x[:3]+' '+x[3:] if x[:3] in ['안양시','고양시','용인시','천안시','전주시','안산시'] else x)
    
    # 맞춤법
    li['LI_KOR_NM'][li[li['LI_CD']=='4148041022'].index] = '어룡리'
    
    # 3월 이후로 변한 값 수정 - 부산광역시 기장군 일광읍
    ig_idx = emd[emd['EMD_KOR_NM']=='일광면'].index
    emd['EMD_KOR_NM'][ig_idx] = '일광읍'
    emd['EMD_CD'][ig_idx] = '26710259'
    
    ss_idx = li[li['LI_CD']=='2671031021'].index[0]
    for ig_idx in range(ss_idx, ss_idx+13):
        in_val = ig_idx - 17
        li['LI_CD'][ig_idx] = f'26710259{in_val}'
    
    # 표기차이
    li['LI_KOR_NM'][li[li['LI_CD']=='4311132026'].index] = '기암리(岐岩)'
    li['LI_KOR_NM'][li[li['LI_CD']=='4311132033'].index] = '기암리(基岩)'
    li['LI_KOR_NM'][li[li['LI_CD']=='4311425322'].index] = '화산리(華山)'
    li['LI_KOR_NM'][li[li['LI_CD']=='4311425350'].index] = '화산리(花山)'
    li['LI_KOR_NM'][li[li['LI_CD']=='4729025331'].index] = '평사리(坪沙)'
    li['LI_KOR_NM'][li[li['LI_CD']=='4729025332'].index] = '평사리(平沙)'
    li['LI_KOR_NM'][li[li['LI_CD']=='4729035030'].index] = '우검리'
    li['LI_KOR_NM'][li[li['LI_CD']=='4886032024'].index] = '일물리'

    시도/시군구/읍면동/리 단위로 법정동코드 분리 및 명칭 분리

    • 사실 이 과정은 코드만 체계에 맞춰 분리하면 쉽게 JOIN이 되지만 위와 같이 데이터 오류가 생겼을 때 찾을 수 없습니다. 
      • 그래서 추후에 생길 오류에 대해 수정이 필요 없으신 분은 아래 내용에서 코드 분리하는 부분만 참고하시면 될 것 같습니다.
    더보기
    df_code['level'] = 0
    
    # 시도 코드
    df_code['sd_cd'] = df_code['법정동코드'].map(lambda x: x[:2])
    df_code['sd_nm'] = df_code['법정동명'].map(lambda x: x.split(' ')[0])
    df_code['시도'] = df_code['sd_nm']
    
    # 세종은 굉장히 특이한 성질을 가졌기에 분리
    sj_code = df_code[df_code['sd_nm']=='세종특별자치시']
    df_code = df_code[df_code['sd_nm']!='세종특별자치시']
    
    # 시도와 시군구 사이 코드
    df_code['up_sgg_cd'] = df_code['법정동코드'].map(lambda x: x[:4])
    df_code['up_sgg_nm'] = df_code['법정동명'].map(lambda x: x.split(' ')[1] if len(x.split(' '))>1 else '')
    
    # 시도와 시군구 연결 - Ex) 고양시 + 덕양구 -> 고양시 덕양구, 고양시 일산서구 등
    df_code['down_sgg_nm'] = df_code['법정동명'].map(lambda x: x.split(' ')[2] if (len(x.split(' '))>2 and x.split(' ')[2][-1] == '구') else '')
    
    # 시군구 코드
    df_code['sgg_cd'] = df_code['법정동코드'].map(lambda x: x[:5])
    df_code['sgg_nm'] = df_code['up_sgg_nm'] + ' ' + df_code['down_sgg_nm']
    df_code['sgg_nm'] = df_code['sgg_nm'].map(lambda x: x.replace(' ', '') if len(x.split(' ')[-1])==0 else x)
    
    # 읍면동 코드
    df_code['emd_cd'] = df_code['법정동코드'].map(lambda x: x[:8])
    sj_code['emd_cd'] = sj_code['법정동코드'].map(lambda x: x[:8])
    
    df_code['emd_nm_hubo'] = df_code.apply(lambda x: x['법정동명'].replace(x['sd_nm'], '').replace(x['sgg_nm'], '')[2:], axis=1)
    df_code['emd_nm'] = df_code['emd_nm_hubo'].map(lambda x: x.split(' ')[0])
    sj_code['emd_nm'] = sj_code['법정동명'].map(lambda x: x.split(' ')[1] if len(x.split(' ')) > 1 else '')
    
    df_code['li_nm'] = df_code['emd_nm_hubo'].map(lambda x: x.split(' ')[1] if len(x.split(' '))>1 else '')
    sj_code['li_nm'] = sj_code['법정동명'].map(lambda x: x.split(' ')[2] if len(x.split(' '))>2 else '')
    
    # 시도>시도-시군구>시군구>읍면동>리로 level 분류
    df_code['level'][df_code[df_code['up_sgg_nm']!=''].index] = 1 
    df_code['level'][df_code[df_code['emd_nm']!=''].index] = 2
    df_code['level'][df_code[df_code['li_nm']!=''].index] = 3
    
    sj_code['level'][sj_code[sj_code['emd_nm']!=''].index] = 2
    sj_code['level'][sj_code[sj_code['li_nm']!=''].index] = 3
    
    # Level별 갯수 확인
    df_code['시군구'] = df_code.apply(lambda x: x['sd_nm']+' '+x['sgg_nm'] if x['sgg_nm']!='' else x['시도'], axis=1)
    df_code['읍면동'] = df_code.apply(lambda x: x['sd_nm']+' '+x['sgg_nm']+' '+x['emd_nm'] if x['emd_nm']!='' else x['시군구'], axis=1)
    df_code['읍면동리'] = df_code.apply(lambda x: x['sd_nm']+' '+x['sgg_nm']+' '+x['emd_nm']+' '+x['li_nm'] if x['li_nm']!='' else x['읍면동'], axis=1)
    
    sj_code['읍면동'] = sj_code.apply(lambda x: x['sd_nm']+' '+x['emd_nm'] if x['emd_nm']!='' else x['시도'], axis=1)
    sj_code['읍면동리'] = sj_code.apply(lambda x: x['sd_nm']+' '+x['emd_nm']+' '+x['li_nm'] if x['li_nm']!='' else x['읍면동'], axis=1)
    
    # 읍면동 아래 리의 정보
    df_code = df_code.merge(df_code[df_code['level']>=3].groupby('읍면동').agg(li_list=('읍면동리','unique'),li_cnt=('읍면동리','nunique')), left_on='읍면동리', right_on='읍면동', how='left').fillna({'li_list':'','li_cnt':0})
    df_code = df_code.merge(df_code[df_code['level']>=2].groupby('시군구').agg(emd_list=('읍면동','unique'),emd_cnt=('읍면동','nunique')), left_on='읍면동', right_on='시군구', how='left').fillna({'emd_list':'','emd_cnt':0})
    df_code = df_code.merge(df_code[df_code['level']>=1].groupby('시도').agg(sgg_list=('시군구','unique'),sgg_cnt=('시군구','nunique')), left_on='시군구', right_on='시도', how='left').fillna({'sgg_list':'','sgg_cnt':0})
    
    # 읍면동 아래 리의 정보
    sj_code = sj_code.merge(sj_code[sj_code['level']>=3].groupby('읍면동').agg(li_list=('읍면동리','unique'),li_cnt=('읍면동리','nunique')), left_on='읍면동리', right_on='읍면동', how='left').fillna({'li_list':'','li_cnt':0})
    sj_code = sj_code.merge(sj_code[sj_code['level']>=2].groupby('시도').agg(emd_list=('읍면동','unique'),emd_cnt=('읍면동','nunique')), left_on='읍면동', right_on='시도', how='left').fillna({'emd_list':'','emd_cnt':0})
    
    
    df_code = df_code.merge(df_code[df_code['level']>=2].groupby('시군구').agg(hubo_li=('li_cnt','sum')), left_on='읍면동', right_on='시군구', how='left').fillna(0)
    df_code = df_code.merge(df_code[df_code['level']>=1].groupby('시도').agg(hubo2_li=('hubo_li','sum'), hubo_emd=('emd_cnt','sum')), left_on='시군구', right_on='시도', how='left').fillna(0)
    
    df_code['li_cnt'] = df_code.apply(lambda x: max(x['li_cnt'], x['hubo_li'], x['hubo2_li']), axis=1)
    df_code['emd_cnt'] = df_code.apply(lambda x: max(x['emd_cnt'], x['hubo_emd']), axis=1)
    
    sj_code['li_cnt'][0] = sj_code['li_cnt'].sum()
    
    df_code = pd.concat([df_code, sj_code]).fillna('').sort_values('법정동코드')
    
    col_list = ['법정동코드','법정동명','level','sd_cd','sd_nm','sgg_cd','sgg_nm','emd_cd','emd_nm','li_nm','sgg_cnt','emd_cnt','li_cnt','sgg_list','emd_list','li_list']
    df_code = df_code[col_list]
    df_code[['sgg_cnt', 'emd_cnt', 'li_cnt']] = df_code[['sgg_cnt', 'emd_cnt', 'li_cnt']].apply(lambda x: x.replace('',0), axis=1).astype('int')
    
    df_code = df_code[~df_code['법정동코드'].isin(df_code[(df_code['level']==1)&(df_code['emd_cnt']==0)]['법정동코드'])].reset_index(drop=True)
    
    sido_coor = pd.merge(sido, df_code[df_code['level']==0], right_on=['sd_cd', 'sd_nm'], left_on=['CTPRVN_CD', 'CTP_KOR_NM'], how='left')[['법정동코드','geometry','center_point','center_long','center_lati']]
    sgg_coor = pd.merge(sgg, df_code[df_code['level']==1], right_on=['sgg_cd', 'sgg_nm'], left_on=['SIG_CD', 'SIG_KOR_NM'], how='left')[['법정동코드','geometry','center_point','center_long','center_lati']]
    emd_coor = pd.merge(emd, df_code[df_code['level']==2], right_on=['emd_cd', 'emd_nm'], left_on=['EMD_CD', 'EMD_KOR_NM'], how='left')[['법정동코드','geometry','center_point','center_long','center_lati']]
    li_coor = pd.merge(li, df_code[df_code['level']==3], right_on=['법정동코드', 'li_nm'], left_on=['LI_CD', 'LI_KOR_NM'], how='left')[['법정동코드','geometry','center_point','center_long','center_lati']]
    
    df_coor = pd.concat([sido_coor, sgg_coor, emd_coor, li_coor])
    
    df = pd.merge(df_coor, df_code, on='법정동코드', how='right')

     

    시도/시군구/읍면동/리 단위로 법정동코드 분리 및 명칭 분리