admdongkor, 첫 커밋
github의 행정동경계 리포지토리를 업데이트한지도 9년이 다 되어간다. 2017년 5월 경에 중앙일보와의 협업으로 대선지도 시각화를 준비하고 있었는데, 투표 결과를 맵핑할 읍면동 단위 지도가 필요했다. 요새는 정부 부처 여기저기서 행정동 경계지도를 배포하고 있는데, 당시로서는 통계청의 sgis가 유일했던 것 같다.
그런데 1년에 한 번쯤, 그것도 1년 반~2년 전의 지도를 배포하고 있던 탓에 당시 선거구로 획정된 행정동 경계와 맞지 않았다. 결국 qgis에서 점들을 수정해가면서 행정구역 변화를 반영하여 2016년 2월 1일 버젼을 만들었고, 다시 수정해서 2017년 4월 18일 버젼을 만들었다. 그리고 기왕 만든 것, 행정동 지도를 찾는 누군가도 사용하시라 깃헙에 리포를 파고 올려뒀다. 아래 화면이 당시의 첫 커밋 당시다.

그 이후로도 행정동 경계를 사용해서 지도를 만들 일들이 종종 있었고, 그래서 종종 올려두던게 어느덧 9년이나 되었다. 지금은 도로명주소지도 사이트에서 한 달에 한번씩 행정동 경계를 올리는데, 막상 웹에서 좀 가벼운 버젼으로 사용하려고 하면 토폴로지 정합성 등 또 이런저런 손을 봐야 한다. 그러다보니 결국 내 손에 맞는 지도가 편해서 그냥 요새도 계속 업데이트하고 있다.
시계열 조회의 문제
최근에 인구 이동 시각화를 해봤다. 이 사이트에서도 소개를 했다.
Flowring : 인구 이동 시각화와 행정구역 데이터의 시계열 정합성
국가데이터처 MDIS에서 개방하는 국내 인구이동통계에 대해서는 이 웹사이트에서 여러차례 다룬 바 있다. 처음에 읍면동 단위의 상세한 OD 데이터를 보고 설레였던 것도 벌써 9년 전이다. 어디서
www.vw-lab.com
그런데 2024년의 '강원도' 인구를 조회했더니 아무것도 안 나온다. 당연히 그럴 것이 2023년에 강원도는 '강원특별자치도'로 바뀌었기 때문이다. 행정구역 10자리 코드를 기반으로 데이터를 처리했는데, 앞의 두 자리가 바뀌니 시계열 데이터 조회를 할 때 일일이 맞춰주지 않고는 같은 지역임에도 불구하고 마치 없는 지역처럼 다뤄졌다.
그래서 생각해봤다.
2024년의 '강원특별자치도'라고 입력해도 2023년과 그 이전까지 같은 영역의 인구를 조회하는 함수를 만들 수는 없을까? 2025기준의 '대구광역시' 시계열 인구를 조회해도 2020년의 경상북도 군위군까지 포함시켜서 조회하는 함수를 만들 수는 없을까?
그래서 만들었다. Python 라이브러리 admdongkor을.
대구의 2025년 시도 코드인 '27'을 입력하고 2011년 말 시점에서 같은 영역에는 어떤 행정구역들이 있는지 조회하고 싶으면 다음과 같이 입력하면된다.
adk.match_adm(base="20251231", region="27", target="20111231")
조회된 영역을 지도에 그려보면 아래와 같다.
2011년의 대구광역시와 2011년의 경상북도 군위군이 나온다.

원리는 간단하다. 두 시점의 지도 경계를 놓고 비교해서 기준시점의 경계 안에 포함되는 것, 약간 걸치는 것들을 모두 가져오는 것이다. 물론 함수는 각 영역당 겹치는 비율을 weight로 표시해서 되돌려주고, 0.01 이면 그냥 지도가 살짝 차이나서 포함된 것으로 간주해서 버릴 수 있도록 하면 된다.
원리는 간단하지만 이걸 하려면 지도 데이터와 행정동코드가 필요했다. 마침 2016년부터 모아놓은 지도들이 있었지만, 고작 10년보다는 좀 더 과거의 데이터들이 있었으면 좋겠다는 생각을 했다. 통계청 sgis를 찾아 보니 1975년의 지도부터 받을 수 있었다. 2000년까지는 5년 단위로, 그 이후는 1년 단위다. 이것으로 2016년 이전까지 가능한 지도들을 모두 채워보기로 했다.
데이터 정제 - 지도의 7자리 행정동 코드와 10자리 행정동 코드를 매칭시키기
같은 행정동인데 코드가 두 종류가 있다니. 행정동 지도에 익숙하지 않은 사람은 이게 도대체 무슨 소리인가 할 것 같다.
통계청은 각각의 행정동에 7자리 코드를 붙여서 관리하고 있었다. 그런데 행안부는 10자리 코드로 관리해왔다.
앞의 2자리는 시도 코드, 여기에 3자리를 붙여 5자리는 시군구코드다. 그리고 자리수만 같을 뿐 코드체계는 다르다. 통계청의 부산은 21번, 행안부의 부산은 26번이다.
통계청의 7자리 중 마지막 2자리는 읍면동이었다. 행안부는 남은 5자리 중 3자리는 읍면동, 마지막 2자리는 '리'를 관리하는 코드다.
그런데 행정동 변화에 따라 새로운 행정동에 새로운 코드를 부여하다 보니 2자리가 모자랐는지, 2024년 경부터 8자리 체제로 바꿔버렸다. 물론 고의로 그랬겠냐마는, 이런 변화무쌍함 때문에 시계열 분석에서의 코드 관리는 무척 고난이도가 되어버린다.
그런데 통계청이 이렇게 열심히 관리하는 8자리 코드는 대부분의 통계에서 보기가 어렵다. 통계청 주도로 조사하는 인구총조사 정도에 사용되는 것 같다. 국가통계포털에서 코드를 포함한 지역별 통계를 내려받아도, 인총을 제외한 나머지는 거의 다 10자리 행안부 코드가 딸려나온다.
하나로 관리해주면 참 좋겠지만, 당장 이루어질 일은 아니겠다. 게다가 눈 앞의 지도엔 7자리 코드가 붙어 있고, 여기에 매칭시킬 자료들은 대부분 10자리일테니, 이 두 개를 매칭시켜보기로 했다.
우선, sgis에서 7자리 행정동 코드가 붙은 지도 파일을 준비한다. 지도의 속성을 보면 아래와 같다.

그리고 국가통계포털의 주민등록인구 1992~2010 읍면동별 데이터에서 다운로드 버튼을 누른 뒤 통계표 파일 서비스 항목으로 들어가면 미리 만들어 둔 가장 상세한 버젼을 다운받을 수 있다. 19자리 코드도 딸려 나온다.

그 다음 이 두 버젼을 이름 기준으로 매칭시킨다. 위의 이름들을 보면 잘 맞는 것 같지만, 특수문자, 띄어쓰기, 연중 두 지도 시점의 불일치, '1동/제1동' 등 명칭의 미묘한 차이 등 여러가지 이유로 매칭되지 않는 것들이 많다.
어떤 것들은 시행착오를 거치면서 규칙을 정해서 변환하고 그래도 잘 되지 않는 것들이나 그냥 수동 강제 매칭을 시키는 것이 더 효율적일 경우에는 그렇게 했다.
정정 및 수동 매칭 과정을 거친 데이터들의 유형은 아래와 같다.
1. 오타 혹은 팔룡/팔용 처럼 발음과 문자 차이에서 기인한 불일치 --> 모든 발견되는 문자열 대체(replace)

2. 시군구 이름에 일부 시 누락, 혹은 시점 불일치 --> [연도+시도+시군구+읍면동] 기준으로 key 만들어서 변환

3. 약간의 시점 불일치로, 직전 혹은 직후 연도의 데이터와 매칭시키면 되는 경우

4. 시군구 단위로 이름이 잘못된 경우 --> 연도+시도+시군구 매칭 후 변환

5. 여러가지 이유로 이름 변환 후 매칭하면 잘 되는 경우는 그렇게 함

6. 그냥 직접 대응시키는 것이 효율적인 경우

위의 경우를 보면, 과연 유형이라 할 수 있을까 싶기도 하지만, 이렇게 매칭 -> 규칙정하기 -> 재매칭 -> 규칙 추가 혹은 수정 ... 의 과정을 반복하면서 1995~2015년까지의 지도에 10자리를 매칭시켰다.
아래에는 매칭 후에 매칭되지 못하고 남은 10자리 코드 세트인데 , 결국 출장소들 위주로 남은 것을 볼 수 있다. 혹은 위의 강제 규칙으로 인해 2순위로 밀려서 매칭되지 않은 것들이다.

출장소의 경우 어차피 지도에서 구획된 영역을 점유하는 것이 아니므로 읍면동 집계에서는 뺄 수 밖에 없다. 시군구 기준의 인구 합산일 때 넣어줄 수 있도록 하면 된다. 어쨌든 지금은 지도에 코드를 매칭시키는 것이 우선순위이므로 출장소의 시군구 소속은 나중의 작업으로 미뤄두기로 했다.
1975~1985년의 경우에는 10자리 코드가 존재하지 않거나 구할 수 없었으므로(10자리 코드는 1988년 부터인 듯 한데 어쨌든 찾을 수가 없었다) 그냥 7자리 뒤에 000을 붙여 10자리로 관리하기로 했다. 나중에 시계열 조회시 1990년과 1985년은 어쩔 수 없이 '변화가 있는 부분'으로 조회되긴 하겠지만, 없는 코드를 일부러 만들 수도 없고 비워놓을 수도 없어서 그렇게 결정했다.
그렇게 꽤 오랜 시간의 시행착오를 거친 결과 다음과 같이 7자리와 10자리가 매칭된 테이블들을 얻을 수 있었다.

아래처럼 문자열이 조금만 비슷할 경우 적당히 매칭시키는 경우도 있다. 10자리 코드의 행정동 이름이 '종로1.2.3.4가동'으로, 가운데 점 대신 마침표가 찍힌 경우였다.

Parquet 파일 생성
그 다음 단계는 간단한 변환 작업이다. 모든 도형 하나하나에 대해서 10자리 코드가 채워졌으므로 위의 최종 결과와 간단히 join 한다. Python의 GeoPandas로 작업했는데, 저장은 압축률이 좋은 Parquet으로 했다. Parquet는 Qgis 등에서 곧바로 수정되지는 않는 포맷이지만 shp이 여러 파일 세트로 움직이는데 비해 하나의 파일로 다룰 수 있고 db의 압축률도 좋은 편이다. 2GB라는 용량 제한도 없어서 gpkg와 더불어 최근에 많이 사용되고 있다.
emd와 sgg, sido 파일 중 일부는 각각 아래와 같다. 1975~2015년의 경우 해당 연도 말일(12월 31일)이라는 보장은 없었지만, 조회의 편의를 위해 그렇게 정했으며, 위의 매칭 작업에서도 되도록 연말 기준으로 맞췄다.
2017년 이후는 기존에 수작업으로 관리하던 geojson이 있어서 별도의 추가 매칭 없이 비교적 손쉽게 변환했다.



각 파일들은 js에서의 가벼운 사용을 위해, 혹은 단순화된 도형을 필요로 하는 사용자들이 보다 손쉽게 사용할 수 있도록 용량을 줄여서 동시에 저장해뒀다. 그 결과 emd는 개당 대략 2.3메가, sgg는 0.9MB, sido는 0.4MB 정도가 된다.
emd 도형의 속성은 아래와 같다. 통계청 7자리와 8자리가 공존하는 2020년의 경우 아래와 같이 emd7, emd8, emdcd, emdnm, sggcd, sggnm, sidocd, sidonm, 그리고 area 속성이 있다.

시군구 이름(sggnm)같은 경우에는 일부러 띄어쓰기를 하지 않았다. 읍면동, 시군구, 시도 문자열을 결합하거나 다시 분리할 때 '성남시 수정구'로 관리하는 것보다 '성남시수정구'가 편리하다. 읽는데도 그다지 혼동이 없다.
admdongkor, 시계열 매칭 인덱스 만들기
이제까지 1975~2026년까지의 행정동 경계 지도에 7자리와 10자리 코드를 매칭시키고, 이걸 emd / sgg / sido 단위로 parquet 파일로 정리하는 과정까지 다뤘다. 도형 하나하나에 코드가 부여된 지도 세트가 60여 개 시점에 걸쳐 만들어졌다.
이제 본격적으로 라이브러리의 함수를 구현할 차례다. 위에서도 한 번 예를 들었는데, "2025년의 대구 영역에 해당하는 2011년 행정구역들을 가져오기"와 같은 시계열 조회 기능이다.
adk.match_adm(base="20251231", region="27", target="20111231")
base 시점 region(여기선 2025년의 시도 코드 27, 대구) 영역을 기준으로, target 시점(2011년 말)의 어떤 읍면동이 그 영역에 걸치는지를 weight 와 함께 돌려준다. 결과는 군위군 8개 읍면 + 당시 대구 전체 emd 들이 면적 가중치(0~1) 와 함께 한 묶음으로 나온다.
사실 어려운 기능은 아니다. 그냥 두 시점의 도형을 겹쳐놓고 겹치는 도형들을 가져오면 된다. intersect라든지 하는 기능들은 이미 shapely 라이브러리에 모두 구현이 되어 있다.
그런데 한번 생각해보자. 라이브러리를 사용하는 사용자가 특정 시점의 시도, 시군구, 읍면동을 기준으로 10개 시점을 조회하려면 다음과 같은 일들을 해야 한다.
1. 10개 시점의 행정구역을 모두 다운 받기
2. 각각을 비교하기(10회~30회)
3. 비교한 내용을 집계하기
3번은 0.1초도 안 걸리는 작업인데, 1번과 2번의 시간이 길면 10초까지도 걸릴 수 있다. 사용자 입장에서는 여간 답답한 노릇이 아니다. 게다가 연산을 할 때마다 행정구역을 모두 다운받는다는 설정도 좀 무리가 있다. 사용자 캐시에 저장해두고 다음에 또 사용한다고 하더라도 굳이 그렇게 무거운 트래픽을 발생시켜야 하는걸까. 게다가 js 라이브러리로 사용하려면, 10개 시점에 100MB가 훌쩍 넘어가므로 웹에서는 다운로드 자체만으로도 부담이 꽤 되는 사이즈다.
빠른 계산을 위한 사전 인덱스 구축
이런 번거로운 과정을 피하기 위해 인덱스를 미리 만들어 두기로 했다.
자, 그럼 인덱스를 어떤 구조로 만들어야 할까?
처음에 단순하게 생각했던 안은 "모든 시점쌍의 모든 매칭 결과를 미리 계산해서 저장" 이었다. 60시점 × 60시점 × 3500 emd × 매칭 결과 = 어림잡아도 수백만 행. 디스크에는 들어가도 라이브러리 import 시 메모리에 통째로 올리기는 부담스럽다.
인접한 시점쌍만 저장해두고 멀리 떨어진 시점은 인접쌍을 합성(chain)해서 구하는 방법도 시도해보았다. 즉, 2025 → 2011 을 직접 저장하지 않더라도, 2025↔2024, 2024↔2023, … 2012↔2011 의 인접쌍 14개를 곱해 합성하면 된다. 행렬 곱이라 의외로 빠르다.
그런데 이 경우, 분동과 합동이 복잡하게 얽힐 경우 처음 시점의 형상으로 십몇년 전까지 거슬러 올라가기에는 무리가 있었다. 그래서 다시 모든 시점의 형상쌍을 직접 비교하는 방향으로 가기로 했다.
시행착오가 너무 많았으므로 일단 결론부터 깔끔하게 언급하고 가자면, 아래의 세 단계로 이루어진다.
#1. measure_v3_step1_spatial_iou.py
인접 시점쌍 모든 emd 의 IoU 계산
인접쌍 16개 × 평균 3500 emd × 평균 3500 emd 후보 → STRtree 로 좁히면 emd 당 평균 3~5 후보, 총 25만 회 정도의 도형 교차 계산.
#2. measure_v3_step2_timeline.py
IoU ≥ 0.99 인 것을 union-find 로 묶어 shape_id 부여
#3. measure_v3_step3_shape_pairs.py
각 shape_id 쌍의 weight (w_a_in_b, w_b_in_a) 저장
1단계부터 다시 보자.
클로드 코드를 사용할 때 편리한 건 여러가지 경우들을 빠른 사이클로 테스트해볼 수 있다는 점이다. 처음에 그냥 주문을 했는데 너무 느렸다. 파이썬에서 몇만회 이상의 루프를 돌거나 무언가 비효율적인 코드를 작성할 때가 많다. '빠른 방법을 찾아달라'고 하면 vectorize 하거나 STRtree에 넣어서 연관 도형을 좀 더 빠르게 찾는다든가 하는 작업 정도는 알아서 한다. 반 년 이상 클로드코드를 사용해봤는데, 초기보다 이런 최적화를 꽤 잘 한다.
그럼에도 불구하고 1단계를 10분 이하로 줄이기는 어려웠다.
shapely.intersection(ga, gb)
위의 코드만으로도 총 소요시간 10분 중 대부분을 사용하는데, 이미 루프를 도는 작업이 저 한 줄 안에서 c++로 이루어지므로, 더 이상 최적화하려면 intersect 연산 자체를 손봐야 했다. 아마도 라이브러리 안쪽으로 들어가보면 아마도 있을 것 같은 여러가지 검증 작업을 떼어낼 수도 있겠지만, 통상적으로 그런 사전 검증 작업들을 건드리는건 좀 위험하다. 그래서 10분의 연산은 감수하기로 했다.
그런데 라이브러리를 모두 구축한 후 지도를 보고 있자니 사소한 오류들이 많이 보였다. 매칭 작업에서의 내 실수도 있었고, 아예 통계청 배포 데이터 자체가 잘못된 경우들도 있었다. 그런데 수정할때마다 10분을 견디자니 좀 답답했다.
그런데 내가 수정한 파일은 62개 시점 중 1개일 때도 있었다. 그러면 변화된 파일과 연관된 쌍들만 새로 계산하면 된다. 그래서 결과로 만들어진 parquet 파일에 대해서 각각 hash를 생성해두어 저장하게 했다. 다음번에 돌릴 때 hash가 같은 파일 쌍은 계산에서 제외시킬 수 있다. 이 장치로 인해 재계산시 시간을 크게 줄일 수 있었다.
2단계의 union-find 는 가벼운 편이고, 3단계는 1단계가 만든 IoU 테이블에서 weight 를 추출하기만 하면 되니 빠르다. 두 단계를 합쳐 30초 안쪽으로 소요되어 최적화는 그 쯤에서 마무리하기로 했다.
그 다음 2단계.
1단계에서 모든 인접 시점쌍의 모든 emd 의 IoU(두 형상의 교차 면적과 합집합 면적)를 다 계산하긴 했지만, 그 결과의 가지수를 그대로 인덱스에 넣는건 낭비다. 60시점 × 평균 3500 emd × 인접쌍 수만 개 IoU 를 시계열 조회마다 다 훑게 만들면 답이 안 나온다. 게다가 3단계의 계산도 너무 오래 걸린다는 단점이 있었다.
그래서 변하지 않은 행정구역은 한 덩어리로 묶어버리기로 했다. 2단계에서 인접 시점쌍 IoU ≥ 0.99 인 형상들을 union-find 로 묶어 같은 shape_id 를 부여한다. 종로구 사직동 같은 경우 1975 부터 2026 까지 경계가 거의 안 바뀌었으니 60시점에 걸쳐 단 하나의 shape_id 만 갖는다. 이러면 timeline 인덱스에는 "사직동: shape_id 17, 60개 시점에 등장" 처럼 기록된다.
shape_id는 형상만으로 기록되지만, 시계열에서의 '다름' 판정은 형상, 행정구역이름, 행정구역코드의 3가지로 판단한다. 그래야 강원도가 강원특별자치도로 변화된 시점들을 잡아낼 수 있기 때문이다.
그리고, shape_id 는 인접쌍 IoU ≥ 0.99 인 형상들을 union-find 로 묶어 부여한다. 0.99 이상을 같다고 보는 이유는 통계청 shp 의 미세한 토폴로지 차이를 흡수하기 위함이다. 1.0 으로 엄격하게 잡으면, 실제로는 변화가 없음에도 불구하고 지도 디지타이징의 미세한 특징들까지 잡아내므로 거의 모든 시점의 거의 모든 emd 가 다른 shape_id 가 되어 시계열 추적이 무의미해진다.
그런데 이 경우도 데이터의 결함을 담아내지는 못한다. 시점에 따라서 어떤 지도들은 백령도가 전체의 10% 정도 서쪽으로 좀 더 밀려 있는 경우도 있다. 이는 아마도 지도 변환시 중부원점 동부 원점 등의 변환을 잘못해서 만들어진 것으로 추정되지만, 하나하나 교정할 수 없어서 일단 그대로 두었다.
2단계 역시 클로드 코드가 자체적으로 최적화를 하지 못해 직접 개입해서 조정해 중 결과다. 가만 보면 한 방향성의 최적화는 잘 하는 것 같은데, 다른 성질의 아이디어를 내서 절차 자체를 바꾸는 작업은 아직 잘 하지 못하는 것 같다. 물론 2026년 4월의 시점이니 앞으로 또 어떻게 바뀔지는 모르겠다. 혹은 프롬프트 엔지니어링으로 이미 가능한데 내가 잘 사용하지 못하는 것일 수도 있다.
마지막 3단계
다음과 같은 테이블을 저장한다.

위의 intersection 결과 테이블을 이용해서 어떤 시점에서 어떤 시점을 호출하든 곧바로 응답할 수 있게 되었다. 이 3단계 역시 2단계에서 변화 없는 형상들을 하나의 id로 관리하면서 시간과 용량이 대폭 줄어들었다.
이렇게 완성한 테이블을 조회할 때 역시 위의 생성 단계 구조를 따르게 된다.
예르 들어 adk.match_adm(base, region, target) 이 들어오면:
1. base 시점의 region 에 속하는 shape_id 들을 timeline 에서 추린다
2. 각 shape_id 가 target 시점에도 같은 shape_id 로 살아 있으면 → weight 1.0 으로 즉시 매칭 (대부분 케이스)
3. 살아 있지 않으면 → shape_pairs 에서 그 shape_id 의 변화 쌍을 찾아 weight 와 함께 합산
대부분의 행정구역은 시계열 내내 안 바뀌므로 2번 경로로 끝나고, shape_pairs 를 들춰보는 건 실제 분할/통합/경계 조정이 일어난 소수의 경우뿐이다. 그래서 인덱스를 통해 62×62 시점 조합 어떤 쿼리든 0.1초 안에 답하는 게 가능해진다.
라이브러리의 또 다른 기능인 find() 는 어떤 이름을 넣었을 때 그 이름이 존재하는 행정구역과 존재 시점들을 반환해 주는 함수다. 이것은 매우 간단하므로 설명을 생략하겠다.
이렇게 여러가지 함수를 위해 구축한 인덱스는 총 6MB. 1회 다운로드에 모든 연산을 빠르게 할 수 있으므로 파이썬이나 js 모두 큰 부담이 되지 않는다.
라이브러리와 인덱스, 지도 데이터의 분리
빌드된 인덱스 8개 파일은 합쳐 6MB 정도다. 처음에는 라이브러리 안에 포함시켜서 import 시점에 이걸 다 가져오도록 했다. 즉, PyPI wheel 안에 embed 했다. 사용자가 pip install admdongkor 하면 인덱스도 같이 깔리고, import 즉시 사용 가능. import 후에는 네트워크 0 으로 동작하는 깔끔한 모델이다.
그런데 운영하면서 곧바로 문제가 생겼다. 1980 sgg 데이터에서 "수성구" 가 "경상북도 수성구" 로 되어 있어 정정해야 하는데(실제로는 1980년 시점에 수성구는 대구시 소속이었다), 그러려면 인덱스를 다시 빌드해서 PyPI 0.5.5 → 0.5.6 으로 버젼업하면서 재배포해야 했다. 데이터 한 줄 정정에 라이브러리 재배포가 강제되는 구조였다.
이게 한 번 뿐이 아니었다. 60여개 시점 시계열 데이터 정정은 끊임없이 발생할 수 있다. 약간의 수정으로 재배포해야 하는데, 그때마다 PyPI 업로드를 해야 한다. 게다가 사용자 입장에서도 한번 import 하고 업데이트 하지 않으면 수정된 내용을 모르고 계속 사용할 수도 있다. (물론 오류를 내포한 상태의 재현성은 올라가겠지만...)
그래서 0.6.0 으로 올리면서 구조를 바꿨다. 인덱스 parquet 은 PyPI/npm 패키지에 embed 하지 않는다. 대신 GitHub 의 dist/data/ 디렉토리에 두고, 라이브러리는 import 시 manifest.json 을 받아 해시 비교 후 변경된 파일만 사용자 캐시 폴더에 다운로드한다. 데이터만 수정한 경우 dist/data/ push 만 하면 끝이고 라이브러리는 재배포 하지 않아도 되었다.
이 구조 변경의 부수효과로 wheel 용량이 4.2MB → 28KB 로 줄었다. 물론 첫 import 시 인덱스 다운로드에 2~3초 정도 걸리지만, 그 이후는 캐시되어 사용에 더딤은 없다. 다음 import 때 manifest 만 비교하므로 변경 없으면 다운로드 역시 하지 않는다.
배포 흐름은 아래와 같다.

manifest.json 에는 데이터 버전(예: 2026.04.25), 각 파일의 sha256, 그리고 데이터 수정 이력(history) 이 들어간다. adk.changelog() 함수로 사용자가 그 이력을 직접 볼 수 있도록 했다.
adk.data_version() # "2026.04.25"
adk.changelog() # 최근 수정 이력 DataFrame
사용자 API
파이썬 라이브러리인 admdongkor를 보면 몇 개의 함수가 있다.
우선 import와 기본 조회
import admdongkor as adk
# 첫 import 때 ~3초 (인덱스 다운로드 + 캐시), 이후엔 instant
adk.versions()
adk.find('대구시수성구', year=[1980])

그 다음 지도 얻기
import matplotlib
import matplotlib.pyplot as plt
sido = adk.get('20250401', 'sido')
ax = sido.plot(figsize=(8, 10), edgecolor='black', linewidth=0.3)
ax.set_title('시도 — 20250401 (원격 다운로드)')
ax.set_axis_off()
아래와 같이 지도를 확인할 수 있다.

그리고 이 라이브러리의 목표였던 adk_match
# 2025 대구(sidocd=27) 영역을 2011 시점으로 역매칭
r = adk.match_adm(base='20251231', region='27', target='20111231')
r.head(10)
# 시각화: 2011 시점 지도에 매칭된 emd 영역 표시
emd_2011 = adk.get('20111231', 'emd')
sido_2011 = adk.get('20111231', 'sido')
matched = emd_2011[emd_2011.emdcd.isin(r.emdcd)].copy()
matched = matched.merge(r[['emdcd','weight']], on='emdcd')
fig, ax = plt.subplots(figsize=(10, 11))
sido_2011.plot(ax=ax, edgecolor='black', linewidth=0.4, facecolor='lightgrey')
matched.plot(ax=ax, column='weight', cmap='Reds',
edgecolor='darkred', linewidth=0.3,
legend=True, vmin=0, vmax=1, alpha=0.8)
gunwi_geom = matched[matched.sggcd == '47720']
gunwi_geom.plot(ax=ax, facecolor='none', edgecolor='navy', linewidth=1.5)
ax.set_title('2011 읍면동 중 2025 대구 영역에 속하는 것\n(파란 테두리 = 경북 군위군, 2023 대구 편입 예정)')
ax.set_axis_off()
plot 결과는 아래 그림과 같다.

좀 더 구체적인 사례는 아래의 링크에 있다. 라이브러리의 샘플 노트북을 colab으로 연결해놓았다.
example.ipynb
Run, share, and edit Python notebooks
colab.research.google.com
시연 사이트 : admdongkor.vw-lab.com
기왕 만드는 것 라이브러리 기능을 시연해볼 수 있는 사이트도 만들어보기로 했다.
admdongkor — 대한민국 행정동 경계 지도 (1975–2026)
1975년부터 현재까지 62개 시점의 대한민국 행정동(읍면동·시군구·시도) 경계를 조회·검색·비교하는 인터랙티브 지도. 변경이력과 통계청 코드 매칭까지 한눈에.
admdongkor.vw-lab.com
클로드 코드 도움을 얻으니, 이런 사이트 하나 만드는게 하루 안 쪽으로 가능해졌다. 프론트엔드의 경우 시각적 피드백 루프가 존재해야 하므로 '딸깍'은 어렵지만, 그래도 UI 하나 조정하는데 시간을 한참 썼던 예전에 비해서는 매우 편리해졌다.
기본적으로 한 시점의 지도를 조회하고, 다른 시점을 정해서 변화를 비교해 볼 수 있다.

시연 영상

그리고, 시도나 시군구 단위로 adm_match() 기능을 한 눈에 볼 수 있도록 하는 시계열 추적 기능도 넣었다.

그 동안 admdongkor 데이터를 관리하면서 가장 번거로웠던 케이스인 부천, 그리고 그 밖에도 창원 등 여러가지 시군구의 변화 이력을 쉽게 볼 수 있도록 했다.
우리나라에서 행정동 시계열 데이터를 다루어 본 사람이라면 누구나 이 '시계열 지옥'에 공감할 것 같다. 행정구역으로 관리되는 데이터들이 인구 외에도 꽤 많은데, 이 변화 무쌍한 시간의 굴곡이 제대로 데이터로 관리되고 있는 것 같지 않다. 오류도 많은데, 정답을 찾기도 어렵다. 배포하는 지도 코드와 갖고 있는 데이터의 코드가 다른데 국가통계 포털의 QA를 봐도 사이다같은 답변을 찾기가 힘들다.
어쨌든 만들었으니, 그리고 당분간은 지속적으로 데이터를 관리할 계획이므로, 부디 '행정동 경계'를 찾는 사람들이 admdongkor까지 잘 닿기를 바란다. 아니, 이제는 '행정동 변화 이력'을 찾는 사람들도 잘 찾아서 사용했으면 좋겠다.
GitHub - vuski/admdongkor: 대한민국 행정동 경계 파일
대한민국 행정동 경계 파일. Contribute to vuski/admdongkor development by creating an account on GitHub.
github.com
'Function' 카테고리의 다른 글
| Flowring : 인구 이동 시각화와 행정구역 데이터의 시계열 정합성 (0) | 2026.04.03 |
|---|---|
| Urban Density Profiler : 경계 너머 도시 들여다보기 (1) | 2026.04.02 |
| 1.7MB에 전국 100m 격자 인구 데이터 넣기 (0) | 2026.03.27 |
| Roaring Bitmap을 사용한 로컬데이터(localdata.kr) 브라우징 최적화 (1) | 2026.03.23 |
| OpenStreetMap과 GTFS 데이터를 이용한 도시 자원 접근성 분석 방법 (2) | 2025.03.27 |