시설물의 개수 혹은 예산 비용이 제한되었을 때, 시설물의 서비스 수준을 높이기 위하여
주어진 제약조건 하에서 시설물이 커버하는 수요량을 최대화하는 위치를 선정하는 방법
df = pd.read_csv('./서울시 자치구별 도보 네트워크 공간정보.csv', encoding='cp949')
df = df[(df['노드링크 유형'] == 'NODE') &
(df['시군구명'] == '동대문구') &
(df['육교'] == 0) &
(df['횡단보도'] == 0)]
# 용두동 + 신설동 = 용신동
df.loc[(df['읍면동명'] == "용두동") |
(df['읍면동명'] == "신설동"), "읍면동명"] = "용신동"
df = df[['노드 WKT', '노드 ID', '시군구명', '읍면동명']].reset_index(drop=True)
# geodata 변경 buffer 생성 - 기존 24시간 가동 AED와의 유효거리 기반 수요량 b(buffer_100)
gs = gpd.GeoSeries.from_wkt(df['노드 WKT'])
df_dobo_100 = gpd.GeoDataFrame(df, geometry = gs, crs = 'epsg:4326')
df_dobo_100 = df_dobo_100.set_geometry('geometry')
df_dobo_100['buffer_100'] = df_dobo_100.to_crs('epsg:5179').buffer(100).to_crs('epsg:4326')
df_dobo_100 = df_dobo_100.set_geometry('buffer_100')
df_dobo_100['buffer_100_coordinates'] = df_dobo_100['buffer_100'].apply(polygon_to_coordinates)
df_dobo_100['lon'] = df_dobo_100['geometry'].x
df_dobo_100['lat'] = df_dobo_100['geometry'].y
df_dobo_100 = pd.merge(df_dobo_100, pop[['행정동코드명', 'a']], how= 'left', right_on = '행정동코드명', left_on = '읍면동명')
del df_dobo_100['행정동코드명'], df_dobo_100['노드 WKT']
# geodata 변경 buffer 생성 - 알고리즘 제약조건 및 시각화(buffer_200)
gs = gpd.GeoSeries.from_wkt(df['노드 WKT'])
df_dobo_200 = gpd.GeoDataFrame(df, geometry = gs, crs = 'epsg:4326')
df_dobo_200 = df_dobo_200.set_geometry('geometry')
df_dobo_200['buffer_200'] = df_dobo_200.to_crs('epsg:5179').buffer(200).to_crs('epsg:4326')
df_dobo_200 = df_dobo_200.set_geometry('buffer_200')
df_dobo_200['buffer_200_coordinates'] = df_dobo_200['buffer_200'].apply(polygon_to_coordinates)
# 경도, 위도 생성
df_dobo_200['lon'] = df_dobo_200['geometry'].x
df_dobo_200['lat'] = df_dobo_200['geometry'].y
# 수요량(인구) 병합
df_dobo_200 = pd.merge(df_dobo_200, pop[['행정동코드명', 'a']], how= 'left', right_on = '행정동코드명', left_on = '읍면동명')
del df_dobo_200['행정동코드명'], df_dobo_200['노드 WKT']
# 노드 시각화용 데이터 생성
df_dobo_viz = df_dobo_200.iloc[:, 6:11]
df_dobo_viz = pd.DataFrame(df_dobo_viz, columns = df_dobo_viz.columns)
df_dobo_200.head(5)
노드 링크 데이터의 동대문구 모습을 시각화한 것은 다음과 같다.
중간의 데이터 수정 함수는 원천 데이터인 노드-링크 데이터의 오류로 규칙성이 있어 함수로 전처리 한 후 시각화를 하였다.
추후 교통량 시각화를 하고자 하는 사람들은 활용하길 바란다.
# 노드-링크 지도에 보여주기
df = pd.read_csv('./서울시 자치구별 도보 네트워크 공간정보.csv', encoding='cp949')
df = df[(df['노드링크 유형'] == 'LINK') &
(df['시군구명'] == '동대문구')]
# 용두동 + 신설동 = 용신동
df.loc[(df['읍면동명'] == "용두동") |
(df['읍면동명'] == "신설동"), "읍면동명"] = "용신동"
df.reset_index(drop=True, inplace=True)
# 데이터 오류 정정 후 geometry 형식 변환하는 함수
def wkt_to_geometry(string):
string = re.findall(r'\d+', string)
lst = []
lst.append([float(string[0] + '.' + string[1]), float(string[2] + '.' + string[3][:-3])])
for i in range(3, len(string), 3):
try:
lst.append([float(string[i][-3:] + '.' + string[i+1]), float(string[i+2] + "." + string[i+3][:-3])])
except:
continue
return lst
# 메모리 아웃 issue로 인한 데이터 컬럼 축소
df['geometry'] = df['링크 WKT'].apply(wkt_to_geometry)
df_link = df[['링크 ID','geometry']]
layer = pdk.Layer(
'PathLayer',
df_link,
get_path='geometry',
get_width=5,
get_color='[255, 255, 120]',
pickable=True,
auto_highlight=True
)
# 지도 시각화
layer2 = pdk.Layer(
'ScatterplotLayer',
df_dobo_viz,
get_position='[lon, lat]',
get_radius=7,
get_fill_color='[255, 0, 0]',
pickable=True,
auto_highlight=True)
center = [127.0400, 37.5744]
view_state = pdk.ViewState(
longitude=center[0],
latitude=center[1],
zoom=13)
r = pdk.Deck(layers=[layer, layer2], initial_view_state=view_state)
r.to_html('./wow.html')
MCLP 알고리즘은 각 수요지점들의 수요량들을 제한된 후보지점들의 커버리지를 기준으로 최대한 많이 커버하도록 후보지들을 선택하는 알고리즘이다.
여기서 수요지점과 후보지지점을 일치시켰으므로 수요지점인 도보 노드데이터들의 각 지점의 수요량을 잘 구해주면 된다.
수요량은 a, b, c의 weighted sum인 w로 사용하였다.
pop = pd.read_csv('./동대문구_동별_연령대별_인구수.csv')
cols = pop.columns
pop['male'] = pop[cols[2]] * 0.01 + pop[cols[3]] * 0.026 + pop[cols[4]] * 0.039 + pop[cols[5]] * 0.075 + pop[cols[6]] * 0.139 + pop[cols[7]] * 0.7
pop['male'] = pop['male'] / pop['male'].max()
pop['female'] = pop[cols[8]] * 0.01 + pop[cols[9]] * 0.026 + pop[cols[10]] * 0.039 + pop[cols[11]] * 0.075 + pop[cols[12]] * 0.139 + pop[cols[13]] * 0.7
pop['female'] = pop['female'] / pop['female'].max()
pop['a'] = 0.64 * pop['male'] + 0.36 * pop['female']
total = pd.DataFrame()
for i in tqdm(df_dobo_100.index):
db = df_dobo_100.loc[i, 'buffer_100']
num = []
area = []
for j in aed_24_100.index:
aed = aed_24_100.loc[j, "buffer_100"]
num.append(db.intersects(aed))
if db.intersects(aed) == True:
area.append(db.intersection(aed).area)
info = {"intersects_num" : sum(num), "intersects_area" : sum(area)}
total = total.append(info, ignore_index=True)
# 정규화 및 최종 수요량 산출
total['intersects_num_scaled'] = (total['intersects_num'].max() - total['intersects_num']) / total['intersects_num'].max()
total['intersects_area_scaled'] = (total['intersects_area'].max() - total['intersects_area']) / total['intersects_area'].max()
df_dobo_200['b'] = total['intersects_num_scaled'].values
df_dobo_200['c'] = total['intersects_area_scaled'].values
df_dobo_200['w'] = 0.4 * df_dobo_200['a'] + 0.3 * df_dobo_200['b'] + 0.3 * df_dobo_200['c']
dobo_full = df_dobo_200.copy() # 설치 전 전체 커버리지 계산용, 밑에서 필터링 하기때문에 저장 필요
수요량을 기준으로 히트맵을 그려본 결과 AED 설치가 필요한 노란색 지점이 많은 것을 확인할 수 있었다.
heatmap_df = pd.DataFrame(dobo_full[['노드 ID', 'geometry', 'lon', 'lat', 'w']])
layer = pdk.Layer(
'ScatterplotLayer',
heatmap_df,
get_position='[lon, lat]',
get_radius=20,
get_fill_color='[255, 255 * w, 0]',
opacity = 0.5,
pickable=True,
auto_highlight=True)
# boundary layer
boundary_layer = pdk.Layer(
'PolygonLayer',
boundary,
get_polygon = 'coordinates',
get_fill_color = '[128, 128, 128]',
get_line_color = '[0,255,0]',
lineWidthScale = 20,
pickable = True,
auto_highlight = True,
opacity = 0.05
)
center = [127.0400, 37.5744]
view_state = pdk.ViewState(
longitude=center[0],
latitude=center[1],
zoom=13)
r = pdk.Deck(layers=[boundary_layer, layer], initial_view_state=view_state)
r.to_html('./wow.html')
# 수요지점으로부터 유효거리 내에 있는 후보지 집합 생성(제약조건 1)
cond_list = []
for i in tqdm(df_dobo_200.index):
buffer = df_dobo_200.loc[i, "buffer_200"]
lst = []
for j in df_dobo_200.index:
point = df_dobo_200.loc[j, "geometry"]
if point.within(buffer): # 200m 반경 기준으로 존재하는 주변 후보지 노드 ID 구하기
lst.append(df_dobo_200.loc[j, "노드 ID"])
cond_list.append(lst) # 해당 후보지를 커버할 수 있는 수요 point의 list, 집합 N
temp = df_dobo_200.copy()
temp['노드 ID'] = pd.to_numeric(temp['노드 ID'])
temp['w'].index = temp['노드 ID']
w = temp['w']
model = Model()
model.max_gap = 0.0
x = [model.add_var(name = "x%d" % i, var_type = BINARY) for i in temp['노드 ID']] # 제약 조건 3 : 포인트에 설치되는가
y = [model.add_var(name = "y%d" % i, var_type = BINARY) for i in temp['노드 ID']] # 제약 조건 4 : 포인트가 커버되는가
model.objective = maximize(xsum(w[i] * model.vars['y%d' %i] for i in temp['노드 ID'])) # 목적함수
model += xsum(model.vars['x%d' %j] for j in temp['노드 ID']) == 40 # 제약 조건 2 : 설치할 AED 개수
for num, idx in enumerate(temp['노드 ID']):
model += xsum(model.vars['x%d' %j] for j in cond_list[num]) >= model.vars['y%d' %idx] # 제약 조건 1 : cond_list(집합 N)에 속한 후보지 중 적어도 한 곳에 AED가 입지하면 i는 커버됨
start = time.time()
model.optimize()
solution = []
for j in temp['노드 ID']:
if model.vars['x%d' %j].x == 0:
solution.append(0)
else:
solution.append(1)
print(f"Time :{round(time.time() - start, 2)} s")
temp['sol'] = solution
sol = temp[temp['sol'] == 1]
sol['icon_data'] = None
for i in sol.index:
sol["icon_data"][i] = icon_data
num = ['existing 99', '+15', '+20', '+25', '+30', '+35', '+40', '+45', '+50']
a = [46.89, 60.59, 66.04, 70.93, 75.73, 76.9, 80.87, 83.44, 85.06]
b = [None, 13.7, 19.15, 24.04, 28.84, 30.01, 33.98, 36.55, 38.17]
c = [None, 13.7, 5.45, 4.89, 4.8, 1.17, 3.97, 2.57, 1.62]
coverage = pd.DataFrame([num,a,b,c]).transpose()
coverage.columns = ['Added AED', 'Coverage percent', 'Coverage growth rate', 'Marginal increase rate']
plt.style.use('bmh')
fig, ax1 = plt.subplots(figsize=(9,6))
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax1.set_title('Coverage Ratio According to the Number of Added AEDs', fontsize=16)
ax1.set_xlabel('Added AED')
ax1.set_ylabel('Coverage percent (%)', fontsize=14, color='b')
ax1.plot(coverage['Added AED'], coverage['Coverage percent'], color='b', label='Coverage percent')
plt.ylim(30,95)
ax1.tick_params(axis='y', labelcolor='b')
plt.text(6.12,77,'We select +40 AEDS',size=15)
ax2 = ax1.twinx()
ax2.set_ylim(1,14)
ax2.set_ylabel('Marginal increase rate (%)', fontsize=14, color='r')
ax2.plot(coverage['Added AED'], coverage['Marginal increase rate'], color='r', label='Marginal increase rate')
ax2.tick_params(axis='y', labelcolor='r', labelsize=13)
plt.axvline(x='+40', color='gray', linestyle='--', linewidth=1.5)
ax1.legend(loc='lower left')
ax2.legend(loc='upper right')
fig.tight_layout()
# 기존 24시간 AED
layer1 = pdk.Layer(
'PolygonLayer',
aed_24_200,
get_polygon='buffer_200_coordinates',
get_fill_color= '[216, 157, 227, 89]',
get_fill_line=[255, 255, 255, 100],
pickable=True,
auto_highlight=True)
layer2 = pdk.Layer(
"IconLayer",
aed_24_200,
get_icon="icon_data",
get_size=8,
size_scale=3,
get_position='[lon, lat]',
pickable=True,
auto_highlight=True
)
# 새롭게 추가된 AED
layer3 = pdk.Layer(
'PolygonLayer',
sol,
get_polygon='buffer_200_coordinates',
get_fill_color = '[225, 225, 0, 130]',
get_fill_line = [255, 255, 255, 100],
pickable=True,
auto_highlight=True)
layer4 = pdk.Layer(
'IconLayer',
sol,
get_icon="icon_data",
get_size=8,
size_scale=3,
get_position='[lon, lat]',
pickable=True,
auto_highlight=True
)
center = [127.0400, 37.5744]
view_state = pdk.ViewState(
longitude=center[0],
latitude=center[1],
zoom=13)
r = pdk.Deck(layers=[layer1, layer2, layer3, layer4], initial_view_state=view_state)
r.to_html('./wow.html')
대회 준비과정에서 얻은 가장 큰 교훈은 '문제를 정확하게 정의하는 것'이 얼마나 중요한지 깨달은 것이다. 많은 시행착오 중에서도 길을 잃지 않았던 것은 처음부터 하고자 하는 바가 확실했고, 데이터와 근거가 확실했으며, 전체적인 흐름이 간단명료했기 때문이였다.