[GIS] 공간 데이터 결합(Spatial Join)
업데이트:
개요
이번 포스팅에서는 공간 데이터간의 결합에 대해서 배워보자.
공간결합(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
)로 사용할 수 있다.
within
과 contain
은 이전 포스팅에서도 설명했지만 동일한 연산이며 기준이 다를 뿐이다.
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객체들을 매칭하는 용도로 가장 많이 활용된다.
그런데 여기서 주의할 점으로 contain
과 within
의 차이를 다시 설명하자면,
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()
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()
- 빨강 : 종로구의 시군구 경계
- 파랑 : 결합된 종로구의 법정동 경계
- 회색 : 원래 종로구의 법정동 경계
위 그림과 같이 종로구(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()
이렇게 공간 정보 데이터는 단순히 값(value)만 확인하는 것이 아니라, 공간 객체(geometry)간의 관계와 오류를 세부적으로 파악할 수 있고 적절하게 변형하거나 조합해야 하는 경우가 많다.
댓글남기기