[GIS] 공간 데이터 결합(Spatial Join)

업데이트:

개요

png

이번 포스팅에서는 공간 데이터간의 결합에 대해서 배워보자.
공간결합(spatial join)이란, 두 공간 데이터프레임을 결합(merge)하는데, key값이 아닌 위치정보에 따라 결합(overlay)해 주는 방식이다.
예전에 포스팅했던 Geopandas의 overlay함수Pandas의 merge함수 두가지 모두 의미를 담고 있고 함수 자체는 merge함수와 거의 유사하다.

  • gpd.overlay : 두 공간데이터의 위치정보(geometry)를 연산
  • pd.merge : 두 데이터프레임을 동일한 key값에 대하여 결합

예제는 이전 포스팅의 소방서 데이터와 도로명주소 전자지도의 서울특별시 법정동, 시군구 경계 데이터를 활용하자.

서울시의 법정동, 시군구 경계 데이터는 github에 올려두었다.

  • 법정동 경계 : Polygon
  • 시군구 경계 : Polygon
  • 소방서 위치 : Point
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
from shapely.geometry import Point, Polygon, LineString
from fiona.crs import from_string
epsg4326 = from_string("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
epsg5179 = from_string("+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +units=m +no_defs")

seoul_bjd = gpd.GeoDataFrame.from_file('data/TL_SCCO_EMD.shp', encoding='cp949')
seoul_bjd.crs = epsg5179
seoul_sig = gpd.GeoDataFrame.from_file('data/TL_SCCO_SIG.shp', encoding='cp949')
seoul_sig.crs = epsg5179
pt_119 = pd.read_csv('data/서울시 안전센터관할 위치정보 (좌표계_ WGS1984).csv', encoding='cp949', dtype=str)
pt_119['경도'] = pt_119['경도'].astype(float)
pt_119['위도'] = pt_119['위도'].astype(float)
pt_119['geometry'] = pt_119.apply(lambda row : Point([row['경도'], row['위도']]), axis=1)
pt_119 = gpd.GeoDataFrame(pt_119, geometry='geometry', crs = epsg4326)
pt_119 = pt_119.to_crs(epsg5179)


공간결합(gpd.sjoin)

공간 결합은 Geopandas의 sjoin함수를 활용하며 입력값들은 다음과 같다.

gpd.sjoin(left_df, right_df, how='inner', op='intersects')

  • left_df, right_df : 결합할 두 공간데이터프레임 객체
  • how : 결합할 데이터 프레임의 기준(left, right, inner,outer)
  • op : 결합할 공간연산 방식(within, intersects, contains)

다른 것은 Pandas의 merge와 동일하지만 key값으로 매칭하던 on 대신 공간 결합을 위한 연산방법으로 op를 활용한다.

3가지 방식을 제공하는데 내부에 있는지(within,contains), 교차하는지(intersects)로 사용할 수 있다.
withincontain이전 포스팅에서도 설명했지만 동일한 연산이며 기준이 다를 뿐이다.

a.contains(b) == b.within(a)로 표현할 수 있다.

Point와 Polygon 결합

먼저 Point와 Polygon 객체를 담고 있는 두 공간데이터를 결합해보자.

result = gpd.sjoin(seoul_sig, pt_119, how='left', op="intersects")
result.head()
SIG_CD SIG_ENG_NM SIG_KOR_NM geometry index_right 고유번호 센터ID 센터명 위도 경도
0 11110 Jongno-gu 종로구 POLYGON ((956615.4532424484 1953567.198968612,... 98 92 1103109 숭인119안전센터 37.575989 127.013191
0 11110 Jongno-gu 종로구 POLYGON ((956615.4532424484 1953567.198968612,... 6 6 1103108 종로119안전센터 37.579479 126.991009
0 11110 Jongno-gu 종로구 POLYGON ((956615.4532424484 1953567.198968612,... 5 5 1103104 신교119안전센터 37.580398 126.964577
0 11110 Jongno-gu 종로구 POLYGON ((956615.4532424484 1953567.198968612,... 8 8 1103105 연건119안전센터 37.580569 126.998178
0 11110 Jongno-gu 종로구 POLYGON ((956615.4532424484 1953567.198968612,... 7 7 1103102 세종로119안전센터 37.581958 126.976747

결과는 N(Polyogon) : 1(Point) (시군구 별 포함된 소방안전센터들) 일 것이다. 결국 Point는 어떤 한 위치이기 때문에(교차한다는 의미가 없음), op의 어떤 공간연산 방법을 활용하나 동일하다.
sjoin은 이렇게 Polygon에 포함된 Point객체들을 매칭하는 용도로 가장 많이 활용된다.

그런데 여기서 주의할 점으로 containwithin의 차이를 다시 설명하자면,

result_test = gpd.sjoin(seoul_sig, pt_119, how='left', op="within")
result_test.head()
SIG_CD SIG_ENG_NM SIG_KOR_NM geometry index_right 고유번호 센터ID 센터명 위도 경도
0 11110 Jongno-gu 종로구 POLYGON ((956615.4532424484 1953567.198968612,... NaN NaN NaN NaN NaN NaN
1 11140 Jung-gu 중구 POLYGON ((957890.3856818088 1952616.745568171,... NaN NaN NaN NaN NaN NaN
2 11170 Yongsan-gu 용산구 POLYGON ((953115.7610894071 1950834.083634671,... NaN NaN NaN NaN NaN NaN
3 11200 Seongdong-gu 성동구 POLYGON ((959681.109391748 1952649.604797923, ... NaN NaN NaN NaN NaN NaN
4 11215 Gwangjin-gu 광진구 POLYGON ((964825.0579498123 1952633.249624572,... NaN NaN NaN NaN NaN NaN

op='within'으로 주면 매칭이 하나도 되지 않는다. 그 이유는 위에서 설명한 것처럼 기준이 다르기 때문인데, 아래와 같이 변경하면 된다.

  • gpd.sjoin(pt_119, seoul_sig, how='left', op="within") : 두 데이터의 위치를 바꾸거나
  • gpd.sjoin(seoul_sig, pt_119, how='right', op="within") : 기준을 바꾸거나

크게 중요한 내용은 아니지만 정확히 알지 못하면 나중에 혼란이 될수도 있다.

Polygon과 Polygon 결합

Polygon끼리도 결합방식은 동일하지만 주의해야할 경우를 얘기하려고 한다.

Polygon자체가 이빨이 딱 안맞는 경우가 많아서, Polygon끼리 sjoin을 할때 포함(within, contains)여부나 교차하는지(intersects)를 정확하게 판단하기가 어렵다.

서울시 시군구 경계와 법정동 경계를 결합한다고 했을때 포함(contains)되는 경우와 교차(intersects)되는 경우를 비교해보자. 시군구 경계에 법정동 경계를 공간결합 하였으며, 종로구(11110)만 필터링하여 결과를 시각화한 것이다.

1. 포함(contains)

result1 = gpd.sjoin(seoul_sig, seoul_bjd, how='left', op='contains')
bjd_list = result1[result1["SIG_KOR_NM"]=="종로구"]["EMD_CD"].tolist()

ax = seoul_sig[seoul_sig["SIG_CD"]=="11110"].boundary.plot(color='red')
seoul_bjd[seoul_bjd["EMD_CD"].str.startswith("11110")].plot(ax=ax,color='black', alpha=0.2)
seoul_bjd[seoul_bjd["EMD_CD"].isin(bjd_list)].plot(ax=ax)
plt.show()

png

2. 교차(intersects)

result2 = gpd.sjoin(seoul_sig, seoul_bjd, how='left', op='intersects')
bjd_list = result2[result2["SIG_KOR_NM"]=="종로구"]["EMD_CD"].tolist()

ax = seoul_sig[seoul_sig["SIG_CD"]=="11110"].boundary.plot(color='red')
seoul_bjd[seoul_bjd["EMD_CD"].str.startswith("11110")].plot(ax=ax,color='black', alpha=0.2)
seoul_bjd[seoul_bjd["EMD_CD"].isin(bjd_list)].plot(ax=ax)
plt.show()

png

  • 빨강 : 종로구의 시군구 경계
  • 파랑 : 결합된 종로구의 법정동 경계
  • 회색 : 원래 종로구의 법정동 경계

위 그림과 같이 종로구(Polygon) 내의 법정동(Polygon)을 공간결합을 통해 매칭하려고 했을때, 경계가 정확히 일치하지 않기 때문에 발생하는 오차들이다.

즉, 이 경우는 공간결합이 아닌 시군구코드(SIG_CD)를 key값으로 merge하는 것이 적절하다.

하지만 이 경우도 시군구경계에 buffer를 줘서 경계를 살짝 넓힌 다음 포함(contains) 결합을 통해 추출해낼 순 있다.

3. buffer를 통해 수정

seoul_sig["geometry"] = seoul_sig["geometry"].buffer(5)
result2 = gpd.sjoin(seoul_sig, seoul_bjd, how='left', op='contains')
bjd_list = result2[result2["SIG_KOR_NM"]=="종로구"]["EMD_CD"].tolist()

ax = seoul_sig[seoul_sig["SIG_CD"]=="11110"].boundary.plot(color='red')
seoul_bjd[seoul_bjd["EMD_CD"].str.startswith("11110")].plot(ax=ax,color='black', alpha=0.2)
seoul_bjd[seoul_bjd["EMD_CD"].isin(bjd_list)].plot(ax=ax)
plt.show()

png

이렇게 공간 정보 데이터는 단순히 값(value)만 확인하는 것이 아니라, 공간 객체(geometry)간의 관계와 오류를 세부적으로 파악할 수 있고 적절하게 변형하거나 조합해야 하는 경우가 많다.

댓글남기기