- 개요
저번 포스트에서는 영등포 타임스퀘어 3번 게이트로 한 번에 갈 수 있는 버스가 정거하는 정류장을 찾아보았습니다.
하지만 제가 시각화하고 싶은 대상은 정류장이 아니라 "지역"입니다.
정류장이 어느 지역에 속하는지 알고, 그 지역의 경계 데이터를 시각화하면 됩니다.
제가 사용할 데이터는 저번 포스트에서 만든 버스 정류장 위치 데이터(df_merged라는 변수명에 저장했음)와 지역경계 데이터입니다.
이번 포스트에서는 크게 2단계 작업을 합니다.
1단계로 정류소가 속한 지역 찾기, 2단계로 지역 경계를 시각화를 합니다.
- 1-1단계: 정류소가 속한 지역 찾는 2가지 방법
정류소는 점 좌표로 되어있고, 지역 경계 데이터는 특정 polygon의 좌표값(여러 점을 이어서 도형이 됨)을 가지고 있습니다.
일반적으로 점이 속하는 지역을 찾는 방법은 2가지가 있습니다.
1. 정류소와 지역 경계 데이터에 같은 규칙의 지역코드 칼럼이 있는 경우
정류소 데이터에와 지역 경계 데이터에 지역코드이 있다면 있다면 지역코드를 기준으로 두 데이터를 합치면 됩니다.
저번 포스트에서도 사용한 방법인 파이썬 pandas의 merge 메서드(SQL에서는 JOIN)를 사용하면 됩니다.
조심해야할 점은 지역코드는 시군구, 읍면동에 따라 자릿수가 다릅니다.
제 지역 경계 데이터에는 읍면동 지역코드 칼럼이 있어 버스 정류소에도 읍면동 지역코드 칼럼이 있어야 합니다.
2. polygon과 점의 위치 탐색
지역 경계는 여러 점으로 구성된 polygon입니다.
당연한 말 같지만 버스 정류소는 점 좌표니까 이 점이 polygon 안에 있다면 버스 정류소가 그 지역에 있는거죠.
지역 경계끼리는 겹치는 지점이 없기 때문에 버스 정류소는 무조건 1개의 지역에만 속합니다.
- 1-2 단계: 정류소가 속한 지역 찾기
안타깝게도 버스 정류소 데이터에는 지역코드 칼럼이 없어 2번째 방법인 polygon과 점의 위치 탐색을 씁니다.
점이 특정 도형 내부에 있냐 바깥에 있냐를 탐지하는 알고리즘을 짜보는 것과 좋겠지만 geopandas 라이브러리에는 이미 이런 기능의 메서드가 있습니다.
이 작업의 핵심은 geopandas의 sjoin() 메서드입니다.
geopandas의 sjoin() 메서드는 두 데이터를 특정 기준으로 합쳐줍니다.
기존의 pandas로 처리한 버스 정류소 데이터를 geopandas로 변홥합니다.
제 코드에서 sjoin() 메서드를 쓰면 버스 정류소 데이터를 기준으로 이 점이 어느 지역 경계에 속하는 지를 지역 경계 데이터의 index로 알려줍니다.
버스 정류소 데이터 'geometry' 열에는 점좌표가, 지역 경계 데이터 'geometry' 열에는 도형의 점좌표가 저장되어 있는데 sjoin()을 하면 geopandas 데이터의 geometry 열에 버스 정류소의 점좌표만 남게 됩니다.
즉 지역 경계의 geomtery 열이 저장되지 않습니다.
저는 지역 경계의 geometry를 그려야하므로 'geometry_emd'라는 새로운 열을 만들어서 지역 경계 지리공간 정보도 저장되도록 합니다.
참고로 우리가 아는 위경도 값은 EPSG:4326 좌표계로, 지역 경계 데이터를 이 좌표계가 아니라서 좌표 변환을 해줘야합니다.
"""
버스 정류소 데이터는 pandas로 읽었었는데 geopandas로 변환
df_merged에는 영등포 타임스퀘어 3번 게이트에
한 번에 도착할 수 있는 버스의 정류소를 저장
이 데이터에서 'CRDNT_X'는 경도, 'CRDNT_Y'는 위도
"""
gdf_s = gpd.GeoDataFrame(
df_merged,
geometry=gpd.points_from_xy(df_merged['CRDNT_X'], df_merged['CRDNT_Y']),
crs='EPSG:4326'
)
"""
지역 경계 데이터를 geopandas로 읽음
"""
import geopandas as gpd
fn = r'C:\Users\zerot\Desktop\python\busVisSystem\data\읍면동'
gdf = gpd.read_file(fn, encoding='cp949')
gdf = gdf.to_crs('EPSG:4326') # 좌표변환
gdf['geometry_emd'] = gdf['geometry'].copy()
"""
predicate 속성이 'within'이면
gdf_s의 점이 gdf의 도형 내부에 있는 경우 해당 행끼리 합칩니다.
"""
gdf_join = gpd.sjoin(gdf_s, gdf, how='left', predicate='within')
"""
한 지역에 여러 정류소가 있지만 지역 경계를 여러 번 그릴 필요는 없으니까
지역 경계가 중복되는 경우 1개만 남깁니다.
그리고 공간 정보는 geometry 열에 저장되어야 하므로
gdf_polygons을 새로 만들었습니다.
"""
gdf_shp = gdf_join.groupby(by='EMD_CD').first()
gdf_polygons = gdf_shp[['geometry_emd']].copy()
gdf_polygons = gdf_polygons.set_geometry('geometry_emd')
gdf_polygons.crs = 'EPSG:4326'
- 2단계: 시각화
이전과 마찬가지로 folium 라이브러리를 씁니다.
저번 코드에서는 pandas 데이터프레임에 iterrows()를 사용해서 한 행마다 위도, 경도 좌표로 점을 찍었습니다만
geopandas를 쓰면 folium.GeoJson()으로 한 번에 그릴 수 있습니다.
import folium
m = folium.Map(location=[37.5665, 126.9780],
tiles='CartoDB dark_matter',
zoom_start=10)
"""
지역 경계를 그리는 부분
"""
folium.GeoJson(
gdf_polygons,
name='지역 경계',
style_function=lambda x: {
'color': 'green',
'fillColor': 'green',
'weight': 2,
'fillOpacity': 0.3,
}
).add_to(m)
"""
버스 정류소를 그림 (저번 코드와 동일)
"""
for idx, row in df_merged.iterrows():
folium.CircleMarker(
location=[row['CRDNT_Y'], row['CRDNT_X']], # (lat, lon)
radius=1, # 반경 크기
color="red", # 원의 색상
fill=True, # 채우기 여부
fill_color="red", # 채우기 색상
fill_opacity=0.7, # 채우기 투명도
popup=f"Calc:({idx})" # 마커를 클릭했을 때 표시될 텍스트
).add_to(m)
m
버스 정류소가 있는 지역이 초록색으로 잘 칠해져 있습니다.
'프로젝트 > 버스 한 번으로 특정 지역에 갈 수 있는 지역 찾기' 카테고리의 다른 글
[특정 지역에 버스 한 번 타고 갈 수 있는 지역 찾기] 2. 버스 정류장, 버스 정류장에 정거하는 버스 및 이 버스의 정류장 찾기 (0) | 2025.05.10 |
---|---|
[특정 지역에 버스 한 번 타고 갈 수 있는 지역 찾기] 1. 버스, 버스 정류장 데이터 수집과 확인 (0) | 2025.05.08 |
[특정 지역에 버스 한 번 타고 갈 수 있는 지역 찾기] 0. 목표 설정 (0) | 2025.05.07 |