Py) 서울 지반침하 위험도 지도 매핑

Py) 서울 지반침하 위험도 지도 매핑

대한경제에서 발행한 임영섭 기자님의 기사에서 공개한 “서울시 지반침하 위험지도”이미지를 활용하여 여러 정보를 매핑해본다.


개요

2025년 4월 3일 대한경제에서는 서울시 지반침하 위험지도를 공개하고 해당 지도와 관련하여 여러 기사를 발행하였다.

상기 기사에서 볼 수 있는 이미지는 다음과 같다.
서울시 지반침하 위험지도 기사 이미지 원본

상기 이미지를 그리기 위해 사용된 데이터를 현재 확보할 수 없기에 Python, Photoshop, Excel, Adobe Color 를 활용하여 최대한 유사하게 기존 지도 매핑을 시도해보았다.

데이터 준비

기본적으로 필요한 데이터는 지도 이미지를 제외하면 행정경계지도 shp파일만 있으면 되지만, 그러면 너무 재미없으니 지하철 노선도 좌표와 노선도 관련 데이터를 추가로 수집 및 가공하였다.

그리하여 최종 확보한 데이터는 다음과 같다.

  • [이미지]: 서울시 지반침하 위험지도
  • [이미지]: 환승역 표시 아이콘
  • [지도]: 서울시 행정경계 shp파일(시군구 단위)
  • [데이터]: 서울시 지하철 노선별 역별 좌표
  • [데이터]: 노선별 역별 색상

데이터 확보 방법

서울시 지반침하 위험지도

앞에서 언급한 [단독입수-서울시 지반침하 위험지도]<1>서울 도심ㆍ강남ㆍ서남권역, 지반침하 ‘경고등’ 기사에서 다운로드 받았고, 해당 이미지에서 지도 부분만 Photoshop을 활용하여 오려내어 배경을 제거한 png 파일로 가공하였다.

환승역 표시 아이콘

나무위키 환승역 문서 에서 제공하는 svg 파일을 가져와서 지도에 표기하게 조치하였다.

서울시 행정경계 shp파일

지오서비스 웹에서 시군구 단위 shp파일을 다운로드 받았으며 그 중 서울시 행정경계만 별도로 GeoPandas 라이브러리로 추출하여 별도의 shp 파일로 저장하였다. 그리고 시각화 시 해당 파일을 사용하였다.

서울시 지하철 노선별 역별 좌표

서울 열린데이터 광장에서 제공하는 서울시 역사마스터 정보를 사용하였다.

그런데 해당 데이터에서 환승역인 경우 두 개 이상의 노선별 역 위치가 약간씩 다르기 때문에 시각화 하는데 별도의 처리를 해줘야 할 수 있다.

단, 해당 데이터를 단순히 보아서는 환승역 여부를 바로 알 수 없다. 별도로 가공을 해야 하는데 이 부분이 매우 불편하다.

노선별 역별 색상

서울교통공사 사이버스테이션 사이트에서 제공하는 범례를 캡쳐 후 해당 파일은 Adobe Color 에서 테마추출 기능으로 색상의 HEX코드를 다음과 같이 추출했다.
서울교통공사 사이버스테이션 노선별 색상 추출

그냥 다음처럼 해당 사이트에서 아이콘 이미지 다운로드 받아서 사용하는 것이 나았는데 실수했다.
서울교통공사 사이버스테이션 크롬 개발자 콘솔

아무튼 노선별 색상 정보를 엑셀파일로 저장하여 이를 활용하였다.

구현 코드

코드는 다음과 같으며 지하철역 처리는 적당히 한다고 허점이 좀 있으니 양해 바란다. 그리고 데이터 다운로드는 다음의 링크를 통해 가능하다.
data_gs_risk_map.zip 다운받기 [클릭]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import pandas as pd

import geopandas as gpd
import folium
from folium.features import DivIcon
from folium.raster_layers import ImageOverlay

def folium_icon_html(color, size = 20, zindex = 10, bg = False):
if bg:
img_bg = f"<img src='https://i.namu.wiki/i/SMBvcToTCTYR_uPh11p8jdvxWXvhzavIAxCk1sKu1kLXNMCA86T13DvTvlX1JTyhL-xENFSXTO0pSe_XDsgvrJTVTzqew5jiccbyY7owSPTltQQ0pCQy6hWLSOqlqZbLOn-EfVvEFxIYlNyi9JUc1A.svg' style='width:{size}px;height:{size}px;'>"
else:
img_bg = ""

tx_html = f'''
<div style="
width:{size}px;
height:{size}px;
background-color:{color};
border-radius:50%;
opacity:0.7;
border:0.5px solid #DDDDDD;
z-index: {zindex};">
{img_bg}
</div>
'''
return tx_html


df_map_seoul = gpd.read_file("data/map_seoul_sgg_240314.shp")

df_subway = pd.read_csv("data/서울시 역사마스터 정보.csv", encoding = "CP949")
df_subway["호선"] = df_subway["호선"].str.replace("\(.*?\)", "", regex = True)
df_subway["호선"] = df_subway["호선"].replace("공항철도1호선", "공항철도")
df_subway = df_subway.loc[df_subway["호선"].str.contains("[0-9]|중앙선|신분당선|분당선|수인선|경의선|경춘선|공항철도|신림선"), ]
df_subway_g = df_subway.groupby("역사명").head(1)[["역사명", "위도", "경도"]]
df_subway = df_subway.drop(columns = ["위도", "경도"])
df_subway = pd.merge(df_subway, df_subway_g, on = "역사명")

df_subway_tr = df_subway.groupby("역사명")["호선"].nunique().reset_index()
df_subway["환승역"] = df_subway["역사명"].isin(df_subway_tr.loc[df_subway_tr["호선"] != 1, "역사명"]) + 0

df_subway_line_colors = pd.read_excel("data/subway_seoul_line_colors.xlsx")
df_subway = pd.merge(df_subway, df_subway_line_colors, on = "호선")


m = folium.Map(location = [37.56, 126.98], zoom_start = 11,
tiles = None,
width = "800px", height = "600px")

img_overlay = ImageOverlay(
name = "Custom Bg",
image = "imgs/seoul_ground_subsidence_risk_map.png", # 배경 이미지 경로
bounds = [[37.428, 126.764], [37.702, 127.184]],
opacity = 0.4,
interactive = False,
cross_origin = False,
zindex = 1,
)
img_overlay.add_to(m)

folium.TileLayer("OpenStreetMap", opacity = 0.5).add_to(m)

folium.GeoJson(
data = df_map_seoul,
style_function = lambda feature: {
'color': '#666666',
'weight': 1,
'fillOpacity': 0,
}
).add_to(m)

val_marker_size = 10

df_subway_tr1 = df_subway.loc[df_subway["환승역"] == 1, ]
df_subway_tr1_g = df_subway_tr1.groupby("역사명")["호선"].min().reset_index()
df_subway_tr1 = pd.merge(df_subway_tr1_g, df_subway_tr1, on = ["역사명", "호선"], how = "inner").reset_index(drop = True)
for n_row in range(len(df_subway_tr1)):
ser_row = df_subway_tr1.iloc[n_row, ]
val_line_name = ser_row["호선"]
val_stn_name = ser_row["역사명"]

folium.Marker(
location = [ser_row["위도"], ser_row["경도"]],
icon = DivIcon(icon_size = (val_marker_size, val_marker_size),
html = folium_icon_html(color = ser_row["색상"],
size = val_marker_size,
bg = 1,
zindex = 50)),
tooltip = f"{val_line_name}: {val_stn_name}",
).add_to(m)

df_subway_tr0 = df_subway.loc[df_subway["환승역"] == 0, ]
df_subway_tr0 = df_subway_tr0.loc[~df_subway_tr0["역사명"].isin(df_subway_tr1["역사명"]), ]
for n_row in range(len(df_subway_tr0)):
ser_row = df_subway_tr0.iloc[n_row, ]
val_line_name = ser_row["호선"]
val_stn_name = ser_row["역사명"]

folium.Marker(
location = [ser_row["위도"], ser_row["경도"]],
icon = DivIcon(icon_size = (val_marker_size, val_marker_size),
html = folium_icon_html(color = ser_row["색상"],
size = val_marker_size,
bg = 0)),
tooltip = f"{val_line_name}: {val_stn_name}",
).add_to(m)

m

최종 결과

한계 및 미비점

folium의 경우 레이어 기능을 제공하여 특정 데이터 표출여부를 결정하는 버튼을 만들 수 있는데 시간상 못함.
전체 지하철 노선을 넣기엔 너무 복잡해서 적당히 했(는데 너무 많은 것 같기도)음
지하철 환승역의 경우 좌표가 약간씩 어긋나 보이는데 해결이 안되었음.
위험도 지도 이미지를 배경 이미지로 깔아놓은 다음에 행정경계 선에 맞춘다고 소수점 3~4자리까지 하나씩 조정하면서 맞췄는데 여기에 10분은 쓴듯.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×