upsonyeon commited on
Commit
9f80e46
ยท
verified ยท
1 Parent(s): edb8a6e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -136
app.py CHANGED
@@ -3,14 +3,10 @@ import pandas as pd
3
  import numpy as np
4
  import folium
5
  from streamlit_folium import st_folium
6
- import math
7
  import requests
8
 
9
- st.set_page_config(page_title="์‹ฌ์•ผ ๋Œ€์ค‘๊ตํ†ต ์ตœ์ ํ™” ๋Œ€์‹œ๋ณด๋“œ (Real Data)", layout="wide")
10
 
11
- # ==========================================
12
- # 1. Fetch Real Data from OSM (Overpass API)
13
- # ==========================================
14
  @st.cache_data
15
  def fetch_osm_data_v4():
16
  bbox = "37.491,127.020,37.505,127.035"
@@ -37,32 +33,27 @@ def fetch_osm_data_v4():
37
  st.error(f"๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
38
  data = {'elements': []}
39
 
40
- bus_stops = []
41
- amenities = []
42
- safety_infra = []
43
- real_bus_routes = []
44
-
45
  for element in data.get('elements', []):
46
  if element['type'] == 'node':
47
- lat = element['lat']
48
- lon = element['lon']
49
  tags = element.get('tags', {})
50
-
51
  if 'highway' in tags and tags['highway'] == 'bus_stop':
52
- bus_stops.append({'lat': lat, 'lon': lon, 'stop_id': element['id'], 'name': tags.get('name', '์ •๋ฅ˜์žฅ')})
53
  elif 'amenity' in tags:
54
  amenities.append({'lat': lat, 'lon': lon})
55
  elif 'highway' in tags or 'man_made' in tags:
56
  safety_infra.append({'lat': lat, 'lon': lon})
57
  elif element['type'] == 'relation':
58
  tags = element.get('tags', {})
59
- ref = tags.get('ref', 'N๋ฒ„์Šค')
60
- name = tags.get('name', ref)
61
  coords = []
62
  for member in element.get('members', []):
63
  if member['type'] == 'way' and 'geometry' in member:
64
  for pt in member['geometry']:
65
- coords.append([pt['lat'], pt['lon']])
66
  if coords:
67
  real_bus_routes.append({'name': name, 'ref': ref, 'coords': coords})
68
 
@@ -70,9 +61,6 @@ def fetch_osm_data_v4():
70
 
71
  stops_df, amenities_df, safety_df, real_bus_routes = fetch_osm_data_v4()
72
 
73
- # ==========================================
74
- # 2. Grid Generation & Feature Calculation
75
- # ==========================================
76
  @st.cache_data
77
  def create_grid_features(_stops, _amenities, _safety):
78
  lats = np.linspace(37.492, 37.504, 20)
@@ -86,26 +74,22 @@ def create_grid_features(_stops, _amenities, _safety):
86
  for lat in lats:
87
  for lon in lons:
88
  point = np.array([lat, lon])
89
- demand = np.sum(np.sqrt(np.sum((amenity_coords - point)**2, axis=1)) < 0.002) if len(amenity_coords) > 0 else 0
90
- deficit = np.min(np.sqrt(np.sum((stop_coords - point)**2, axis=1))) if len(stop_coords) > 0 else 0
91
- safety_count = np.sum(np.sqrt(np.sum((safety_coords - point)**2, axis=1)) < 0.002) if len(safety_coords) > 0 else 0
92
- grid_data.append({'lat': lat, 'lon': lon, 'raw_demand': demand, 'raw_deficit': deficit, 'raw_safety_count': safety_count})
93
 
94
  df = pd.DataFrame(grid_data)
95
- df['base_demand'] = df['raw_demand'] / df['raw_demand'].max() if df['raw_demand'].max() > 0 else 0
96
- df['base_deficit'] = df['raw_deficit'] / df['raw_deficit'].max() if df['raw_deficit'].max() > 0 else 0
97
- df['base_risk'] = 1 - (df['raw_safety_count'] / df['raw_safety_count'].max()) if df['raw_safety_count'].max() > 0 else 1.0
98
-
99
  return df
100
 
101
  grids_df = create_grid_features(stops_df, amenities_df, safety_df)
102
 
103
- # ==========================================
104
- # 3. Classify Stops & DRT Loop Logic
105
- # ==========================================
106
- nbus_coords = np.array([pt for route in real_bus_routes for pt in route['coords']])
107
- if not stops_df.empty and len(nbus_coords) > 0:
108
- stops_df['min_dist_to_nbus'] = stops_df.apply(lambda r: np.min(np.sqrt((nbus_coords[:,0]-r['lat'])**2 + (nbus_coords[:,1]-r['lon'])**2)), axis=1)
109
  stops_df['is_nbus_stop'] = stops_df['min_dist_to_nbus'] < 0.001
110
  else:
111
  stops_df['is_nbus_stop'] = False
@@ -113,61 +97,33 @@ else:
113
  nbus_stops = stops_df[stops_df['is_nbus_stop']]
114
  blind_stops = stops_df[~stops_df['is_nbus_stop']]
115
 
116
- # ==========================================
117
- # 4. Sidebar & Interactivity (Dynamic Time Logic)
118
- # ==========================================
119
- st.sidebar.header("โš™๏ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์„ค์ • (OSM Data)")
120
  selected_time = st.sidebar.select_slider("์‹œ๊ฐ„๋Œ€ ์„ ํƒ", options=[f"{str(h).zfill(2)}:{str(m).zfill(2)}" for h in [22, 23, 0, 1, 2, 3, 4] for m in [0, 30]] + ["05:00"], value="23:30")
121
  time_idx = [f"{str(h).zfill(2)}:{str(m).zfill(2)}" for h in [22, 23, 0, 1, 2, 3, 4] for m in [0, 30]].index(selected_time) if selected_time != "05:00" else 14
122
 
123
- # [NEW] ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ์ˆ˜์š”(์ƒ์—…์ง€) vs ์ฃผ๊ฑฐ(์•ˆ์ „) ๊ฐ€์ค‘์น˜ ๋™์  ๋ณ€ํ™˜
124
- if time_idx <= 3: # 22:00 ~ 23:30 (์ƒ์—…์ง€๊ตฌ ํ•ซ์ŠคํŒŸ ์ค‘์‹ฌ)
125
- dyn_alpha = 0.8
126
- dyn_gamma = 0.0
127
- elif time_idx <= 7: # 00:00 ~ 01:30 (์ฃผ๊ฑฐ์ง€ ์ด๋™๊ธฐ)
128
- dyn_alpha = 0.4
129
- dyn_gamma = 0.4
130
- else: # 02:00 ~ 05:00 (์‹ฌ์•ผ ์ฃผ๊ฑฐ์ง€๊ตฌ ์ค‘์‹ฌ)
131
- dyn_alpha = 0.1
132
- dyn_gamma = 0.8
133
 
134
  dyn_beta = 0.2
135
 
136
- st.sidebar.markdown(f"""
137
- **โฐ ์‹œ๊ฐ„๋Œ€ ์ž๋™ ๋ฐ˜์˜ ๊ฐ€์ค‘์น˜**
138
- - ๐Ÿบ ์ƒ์—…/์œ ํฅ(ฮฑ): **{dyn_alpha:.1f}**
139
- - ๐Ÿšถโ€โ™‚๏ธ ์ •๋ฅ˜์žฅ๊ฒฐํ•(ฮฒ): **{dyn_beta:.1f}**
140
- - ๐Ÿ  ์ฃผ๊ฑฐ/์•ˆ์ „(ฮณ): **{dyn_gamma:.1f}**
141
- """)
142
-
143
- st.sidebar.caption("์‹œ๊ฐ„์ด ๋Šฆ์–ด์งˆ์ˆ˜๋ก ๋ฒˆํ™”๊ฐ€(์ƒ์—…์ง€)์—์„œ ์–ด๋‘์šด ๊ณจ๋ชฉ(์ฃผ๊ฑฐ์ง€)์œผ๋กœ ํƒ€๊ฒŸํŒ…์ด ์ž๋™ ์ด๋™ํ•˜๋ฉฐ ๋ฃจํ”„ ํ˜•ํƒœ๊ฐ€ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค.")
144
-
145
- alpha = dyn_alpha
146
- beta = dyn_beta
147
- gamma = dyn_gamma
148
-
149
  drt_budget = st.sidebar.number_input("ํˆฌ์ž… ๊ฐ€๋Šฅ DRT ์ฐจ๋Ÿ‰ ์ˆ˜", min_value=1, max_value=20, value=5)
150
 
151
- # ==========================================
152
- # 5. Risk Score & DRT Routing Calculation
153
- # ==========================================
154
  current_grids = grids_df.copy()
155
- # time_factor removed since dynamic alpha/gamma physically moves the hotspot
156
- current_grids['demand'] = current_grids['base_demand']
157
- current_grids['deficit'] = current_grids['base_deficit']
158
- current_grids['risk'] = current_grids['base_risk']
159
- current_grids['risk_score'] = alpha * current_grids['demand'] + beta * current_grids['deficit'] + gamma * current_grids['risk']
160
 
161
- threshold = current_grids['risk_score'].quantile(0.85)
162
- drt_targets = current_grids.nlargest(50, 'risk_score') # ๊ด‘์—ญ ๋ฃจํ”„๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๋Œ€์ƒ์„ 50๊ฐœ๋กœ ๋Œ€ํญ ํ™•๋Œ€
163
 
164
- # N๋ฒ„์Šค๊ฐ€ ์—†๋Š” ์‚ฌ๊ฐ์ง€๋Œ€ ์ •๋ฅ˜์žฅ ๋งค์นญ
165
  drt_assignments = []
166
  for idx, grid_row in drt_targets.iterrows():
167
  if not blind_stops.empty:
168
  distances = np.sqrt((blind_stops['lat'] - grid_row['lat'])**2 + (blind_stops['lon'] - grid_row['lon'])**2)
169
- nearest_stop = blind_stops.loc[distances.idxmin()]
170
- drt_assignments.append(nearest_stop)
171
 
172
  unique_blind_stops = pd.DataFrame(drt_assignments).drop_duplicates('stop_id')
173
 
@@ -180,7 +136,7 @@ def ccw(p1, p2, p3):
180
 
181
  def get_convex_hull(points):
182
  if len(points) <= 3: return points
183
- points = sorted(points, key=lambda p: (p[0], p[1]))
184
  lower = []
185
  for p in points:
186
  while len(lower) >= 2 and ccw(lower[-2], lower[-1], p) <= 0: lower.pop()
@@ -191,118 +147,83 @@ def get_convex_hull(points):
191
  upper.append(p)
192
  return lower[:-1] + upper[:-1]
193
 
194
- # ์‚ฌ๊ฐ์ง€๋Œ€ ์ •๋ฅ˜์žฅ + [ํ™˜์Šน ๊ณต์œ  ๊ตฌ๊ฐ„ 3๊ฐœ]๋กœ DRT Loop ์ƒ์„ฑ
195
  if not unique_blind_stops.empty and not nbus_stops.empty:
196
- centroid_lat = unique_blind_stops['lat'].mean()
197
- centroid_lon = unique_blind_stops['lon'].mean()
198
 
199
- # ์‚ฌ๊ฐ์ง€๋Œ€ ์ค‘์‹ฌ์—์„œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด N๋ฒ„์Šค ์ •๋ฅ˜์žฅ์„ 3๊ฐœ ์ฐพ์•„ 'ํ™˜์Šน ๊ณต์œ  ๊ตฌ๊ฐ„'๏ฟฝ๏ฟฝ๋กœ ์„ค์ •
200
  dist_to_hub = np.sqrt((nbus_stops['lat'] - centroid_lat)**2 + (nbus_stops['lon'] - centroid_lon)**2)
201
  closest_hubs = nbus_stops.loc[dist_to_hub.nsmallest(3).index]
202
-
203
- # ํ™˜์Šน ๊ตฌ๊ฐ„ ์‹œ๊ฐํ™”๋ฅผ ์œ„ํ•ด ๊ฒฝ๋„ ์ˆœ์œผ๋กœ ์ •๋ ฌ (์ผ์ง์„  ํ˜•์„ฑ)
204
- transfer_pts = closest_hubs[['lat', 'lon']].values.tolist()
205
- transfer_coords = sorted(transfer_pts, key=lambda x: x[1])
206
 
207
  loop_stops = pd.concat([unique_blind_stops, closest_hubs]).drop_duplicates('stop_id')
208
-
209
- # Convex Hull(์™ธ๊ณฝ์„ ) ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ํฌ๊ณ  ๊น”๋”ํ•œ ๋ฃจํ”„ ๋„์ถœ
210
  coords = loop_stops[['lat', 'lon']].values.tolist()
211
  hull_coords = get_convex_hull(coords)
212
 
213
- hull_lats = [pt[0] for pt in hull_coords]
214
- hull_lons = [pt[1] for pt in hull_coords]
215
  unique_blind_stops = unique_blind_stops[unique_blind_stops.apply(lambda r: any(abs(r['lat'] - hl) < 1e-6 and abs(r['lon'] - hlon) < 1e-6 for hl, hlon in zip(hull_lats, hull_lons)), axis=1)]
216
 
217
- hull_coords.append(hull_coords[0]) # ๋ฃจํ”„ ๋‹ซ๊ธฐ
218
- loop_coords = hull_coords
219
 
220
- # ==========================================
221
- # 6. Main Dashboard UI
222
- # ==========================================
223
  st.title("๐ŸšŒ ์‹ฌ์•ผ ๋Œ€์ค‘๊ตํ†ต N๋ฒ„์Šค-DRT ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ")
224
- st.markdown("์‹œ๊ฐ„๋Œ€์— ๋”ฐ๋ผ ๋ณ€ํ™”ํ•˜๋Š” ํ•ซ์ŠคํŒŸ(์ƒ์—…์ง€ โ†’ ์ฃผ๊ฑฐ์ง€)์„ ๋”ฐ๋ผ ์ˆœํ™˜ ๋…ธ์„ ์ด ์ž๋™ ๋ณ€๊ฒฝ๋˜๋ฉฐ, N๋ฒ„์Šค์™€ **ํ™˜์Šน ๊ตฌ๊ฐ„(Transfer Zone)** ์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.")
225
 
226
  col1, col2 = st.columns([2, 1])
227
 
228
  with col1:
229
  m = folium.Map(location=[37.498, 127.027], zoom_start=15, tiles="CartoDB dark_matter")
230
 
231
- # ๊ฒฉ์ž
232
  for idx, row in current_grids.iterrows():
233
- if row['risk_score'] > 0.3:
 
234
  folium.CircleMarker(
235
- location=[row['lat'], row['lon']], radius=4,
236
- color=None, fill=True, fill_color="red" if row['risk_score'] > threshold else "orange", fill_opacity=row['risk_score'],
237
- popup=f"Risk: {row['risk_score']:.2f}"
 
238
  ).add_to(m)
239
 
240
- # ์‚ฌ๊ฐ์ง€๋Œ€ ์ •๋ฅ˜์žฅ (ํฐ์ƒ‰)
241
  for idx, row in blind_stops.iterrows():
242
  folium.CircleMarker(
243
- location=[row['lat'], row['lon']], radius=2, color="gray", fill=True, fill_color="gray", tooltip="์ผ๋ฐ˜ ์ •๋ฅ˜์žฅ (์‹ฌ์•ผ๋ฒ„์Šค ๋ฏธ์šดํ–‰)"
244
  ).add_to(m)
245
 
246
- # ์‹ค์ œ N๋ฒ„์Šค ๋…ธ์„  (PolyLine)
247
  colors = ['cyan', 'lime', 'yellow']
248
  for i, route in enumerate(real_bus_routes):
249
  if route['coords']:
250
  folium.PolyLine(
251
- locations=route['coords'], dash_array="15, 20",
252
- color=colors[i % len(colors)], weight=4, opacity=0.6,
253
- tooltip=f"์‹ค์ œ ์‹ฌ์•ผ๋ฒ„์Šค ๋…ธ์„ : {route['name']}"
254
  ).add_to(m)
255
 
256
- # DRT ๋ฃจํ”„ ๋…ธ์„  (PolyLine - ๋ณด๋ผ์ƒ‰)
257
  if loop_coords:
258
  folium.PolyLine(
259
  locations=loop_coords, dash_array="10, 15",
260
- color='purple', weight=5, opacity=0.9,
261
- tooltip="๐Ÿ”ฅ ์‚ฌ๊ฐ์ง€๋Œ€ ํƒ€๊ฒŸ DRT ์ˆœํ™˜ ๋…ธ์„ "
262
  ).add_to(m)
263
 
264
- # [NEW] ํ™˜์Šน ๊ณต์œ  ๊ตฌ๊ฐ„ (Transfer Zone - ์˜ค๋ Œ์ง€์ƒ‰ ๊ตต์€ ์„ )
265
  if len(transfer_coords) >= 2:
266
  folium.PolyLine(
267
- locations=transfer_coords, dash_array="1, 10",
268
- color='orange', weight=8, opacity=1.0,
269
- tooltip="๐Ÿ”„ N๋ฒ„์Šค โ†” DRT ํ™˜์Šน ๊ตฌ์—ญ (Transfer Zone)"
270
  ).add_to(m)
271
 
272
- # DRT ์ •๋ฅ˜์žฅ ๋งˆ์ปค
273
  for idx, row in unique_blind_stops.iterrows():
274
  folium.Marker(
275
- location=[row['lat'], row['lon']], popup=f"DRT ์Šนํ•˜์ฐจ: {row['name']}",
276
  icon=folium.Icon(color="purple", icon="bus", prefix="fa")
277
  ).add_to(m)
278
 
279
- # ํ™˜์Šน ๊ฑฐ์  (N๋ฒ„์Šค ์ •๋ฅ˜์žฅ 3๊ณณ)
280
  if not closest_hubs.empty:
281
  for idx, row in closest_hubs.iterrows():
282
  folium.CircleMarker(
283
- location=[row['lat'], row['lon']], radius=7, color="gold", fill=True, fill_color="orange", fill_opacity=0.8,
284
- tooltip=f"๐Ÿ”„ ํ™˜์Šน ์ •๋ฅ˜์žฅ: {row['name']}"
285
  ).add_to(m)
286
 
287
- st_folium(m, width=800, height=500)
 
288
 
289
  with col2:
290
- st.subheader("๐Ÿ’ก ๋‹ค์ด๋‚ด๋ฏน ๋ฃจํ”„ & ํ™˜์Šน ์‹œ๋‚˜๋ฆฌ์˜ค")
291
- st.info(f"ํ˜„์žฌ ์„ ํƒ๋œ ์‹œ๊ฐ„: **{selected_time}**\n\n์‹œ๊ฐ„๋Œ€์— ๋”ฐ๋ฅธ ๊ฐ€์ค‘์น˜ ๋ณ€ํ™”๋กœ ํƒ€๊ฒŸ ์ง€์—ญ์ด ์ƒ์—…์ง€๊ตฌ์™€ ์ฃผ๊ฑฐ์ง€๊ตฌ ์‚ฌ์ด๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋„˜๋‚˜๋“ญ๋‹ˆ๋‹ค.")
292
-
293
- if not closest_hubs.empty:
294
- hub_names = ", ".join(closest_hubs['name'].tolist())
295
- st.warning(f"**๐Ÿ”ฅ ํ™˜์Šน ๊ณต์œ  ๊ตฌ์—ญ (Transfer Zone)**\n\nN๋ฒ„์Šค ๋…ธ์„ ์˜ **'{hub_names}'** ์ •๋ฅ˜์žฅ๋“ค์„ DRT ๋ฃจํ”„๊ฐ€ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ผ ์ฃผํ–‰ํ•˜๋ฉฐ ๊ฒน์นฉ๋‹ˆ๋‹ค. (์ง€๋„ ์ƒ **๋น›๋‚˜๋Š” ๊ธˆ์ƒ‰/์˜ค๋ Œ์ง€์ƒ‰ ์„ **)\n\n์Šน๊ฐ์€ ์ด ๊ตฌ์—ญ ๋‚ด ์•„๋ฌด ๊ณณ์—์„œ๋‚˜ ๋‚ด๋ ค N๋ฒ„์Šค๋กœ ํŽธํ•˜๊ฒŒ ๊ฐˆ์•„ํƒˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
296
-
297
- st.divider()
298
- st.subheader("๐Ÿ“ˆ ๋ฐฐ์ฐจ๊ฐ„๊ฒฉ(๋Œ€๊ธฐ ์‹œ๊ฐ„) ๊ฐœ์„  ํšจ๊ณผ")
299
- col_b, col_a, col_diff = st.columns(3)
300
-
301
- base_n_interval = 40
302
- drt_interval = max(5, 30 - drt_budget * 4)
303
-
304
- col_b.metric("์‚ฌ๊ฐ์ง€๋Œ€ ๊ธฐ์กด ๋ฐฐ์ฐจ๊ฐ„๊ฒฉ", "์šดํ–‰ ์—†์Œ (โˆž)", delta=None)
305
- col_a.metric("DRT ๋„์ž… ํ›„ ์‚ฌ๊ฐ์ง€๋Œ€", f"์•ฝ {drt_interval} ๋ถ„", delta="-โˆž ๋ถ„ (์‹ ๊ทœ ์„œ๋น„์Šค ์ฐฝ์ถœ)", delta_color="normal")
306
- col_diff.metric("๊ธฐ์กด N๋ฒ„์Šค ๋ฐฐ์ฐจ๊ฐ„๊ฒฉ (๊ฐ„์„ )", f"์•ฝ {base_n_interval} ๋ถ„", delta="์œ ์ง€", delta_color="off")
307
-
308
- st.markdown("> **๊ฒฐ๊ณผ**: ์ง€๋„ ์ƒ ๋น›๋‚˜๋Š” ๊ธˆ์ƒ‰ ์„ ๊ตฌ๊ฐ„(ํ™˜์Šน ๊ตฌ์—ญ)์—์„œ ๋Œ€๊ธฐ์‹œ๊ฐ„ ์—†์ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ N๋ฒ„์Šค๋กœ ๊ฐˆ์•„ํƒˆ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹œ๊ฐ„๋Œ€์— ๋งž์ถฐ ์ตœ์ ์˜ ๊ฒฝ๋กœ๋กœ ๊ตฝ์ด์น˜๋Š” ๋˜‘๋˜‘ํ•œ ๋ฃจํ”„๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค.")
 
3
  import numpy as np
4
  import folium
5
  from streamlit_folium import st_folium
 
6
  import requests
7
 
8
+ st.set_page_config(page_title="์‹ฌ์•ผ ๋Œ€์ค‘๊ตํ†ต ์ตœ์ ํ™” ๋Œ€์‹œ๋ณด๋“œ", layout="wide")
9
 
 
 
 
10
  @st.cache_data
11
  def fetch_osm_data_v4():
12
  bbox = "37.491,127.020,37.505,127.035"
 
33
  st.error(f"๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
34
  data = {'elements': []}
35
 
36
+ bus_stops, amenities, safety_infra, real_bus_routes = [], [], [], []
 
 
 
 
37
  for element in data.get('elements', []):
38
  if element['type'] == 'node':
39
+ lat = float(element['lat'])
40
+ lon = float(element['lon'])
41
  tags = element.get('tags', {})
 
42
  if 'highway' in tags and tags['highway'] == 'bus_stop':
43
+ bus_stops.append({'lat': lat, 'lon': lon, 'stop_id': str(element['id']), 'name': str(tags.get('name', '์ •๋ฅ˜์žฅ'))})
44
  elif 'amenity' in tags:
45
  amenities.append({'lat': lat, 'lon': lon})
46
  elif 'highway' in tags or 'man_made' in tags:
47
  safety_infra.append({'lat': lat, 'lon': lon})
48
  elif element['type'] == 'relation':
49
  tags = element.get('tags', {})
50
+ ref = str(tags.get('ref', 'N๋ฒ„์Šค'))
51
+ name = str(tags.get('name', ref))
52
  coords = []
53
  for member in element.get('members', []):
54
  if member['type'] == 'way' and 'geometry' in member:
55
  for pt in member['geometry']:
56
+ coords.append([float(pt['lat']), float(pt['lon'])])
57
  if coords:
58
  real_bus_routes.append({'name': name, 'ref': ref, 'coords': coords})
59
 
 
61
 
62
  stops_df, amenities_df, safety_df, real_bus_routes = fetch_osm_data_v4()
63
 
 
 
 
64
  @st.cache_data
65
  def create_grid_features(_stops, _amenities, _safety):
66
  lats = np.linspace(37.492, 37.504, 20)
 
74
  for lat in lats:
75
  for lon in lons:
76
  point = np.array([lat, lon])
77
+ demand = int(np.sum(np.sqrt(np.sum((amenity_coords - point)**2, axis=1)) < 0.002)) if len(amenity_coords) > 0 else 0
78
+ deficit = float(np.min(np.sqrt(np.sum((stop_coords - point)**2, axis=1)))) if len(stop_coords) > 0 else 0.0
79
+ safety_count = int(np.sum(np.sqrt(np.sum((safety_coords - point)**2, axis=1)) < 0.002)) if len(safety_coords) > 0 else 0
80
+ grid_data.append({'lat': float(lat), 'lon': float(lon), 'raw_demand': demand, 'raw_deficit': deficit, 'raw_safety_count': safety_count})
81
 
82
  df = pd.DataFrame(grid_data)
83
+ df['base_demand'] = df['raw_demand'] / df['raw_demand'].max() if df['raw_demand'].max() > 0 else 0.0
84
+ df['base_deficit'] = df['raw_deficit'] / df['raw_deficit'].max() if df['raw_deficit'].max() > 0 else 0.0
85
+ df['base_risk'] = 1.0 - (df['raw_safety_count'] / df['raw_safety_count'].max()) if df['raw_safety_count'].max() > 0 else 1.0
 
86
  return df
87
 
88
  grids_df = create_grid_features(stops_df, amenities_df, safety_df)
89
 
90
+ if not stops_df.empty and len(real_bus_routes) > 0:
91
+ nbus_coords = np.array([pt for route in real_bus_routes for pt in route['coords']])
92
+ stops_df['min_dist_to_nbus'] = stops_df.apply(lambda r: float(np.min(np.sqrt((nbus_coords[:,0]-r['lat'])**2 + (nbus_coords[:,1]-r['lon'])**2))), axis=1)
 
 
 
93
  stops_df['is_nbus_stop'] = stops_df['min_dist_to_nbus'] < 0.001
94
  else:
95
  stops_df['is_nbus_stop'] = False
 
97
  nbus_stops = stops_df[stops_df['is_nbus_stop']]
98
  blind_stops = stops_df[~stops_df['is_nbus_stop']]
99
 
100
+ st.sidebar.header("โš™๏ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์„ค์ •")
 
 
 
101
  selected_time = st.sidebar.select_slider("์‹œ๊ฐ„๋Œ€ ์„ ํƒ", options=[f"{str(h).zfill(2)}:{str(m).zfill(2)}" for h in [22, 23, 0, 1, 2, 3, 4] for m in [0, 30]] + ["05:00"], value="23:30")
102
  time_idx = [f"{str(h).zfill(2)}:{str(m).zfill(2)}" for h in [22, 23, 0, 1, 2, 3, 4] for m in [0, 30]].index(selected_time) if selected_time != "05:00" else 14
103
 
104
+ if time_idx <= 3:
105
+ dyn_alpha, dyn_gamma = 0.8, 0.0
106
+ elif time_idx <= 7:
107
+ dyn_alpha, dyn_gamma = 0.4, 0.4
108
+ else:
109
+ dyn_alpha, dyn_gamma = 0.1, 0.8
 
 
 
 
110
 
111
  dyn_beta = 0.2
112
 
113
+ st.sidebar.markdown(f"**โฐ ๊ฐ€์ค‘์น˜**\n- ์ƒ์—…/์œ ํฅ(ฮฑ): {dyn_alpha:.1f}\n- ์ •๋ฅ˜์žฅ๊ฒฐํ•(ฮฒ): {dyn_beta:.1f}\n- ์ฃผ๊ฑฐ/์•ˆ์ „(ฮณ): {dyn_gamma:.1f}")
 
 
 
 
 
 
 
 
 
 
 
 
114
  drt_budget = st.sidebar.number_input("ํˆฌ์ž… ๊ฐ€๋Šฅ DRT ์ฐจ๋Ÿ‰ ์ˆ˜", min_value=1, max_value=20, value=5)
115
 
 
 
 
116
  current_grids = grids_df.copy()
117
+ current_grids['risk_score'] = dyn_alpha * current_grids['base_demand'] + dyn_beta * current_grids['base_deficit'] + dyn_gamma * current_grids['base_risk']
 
 
 
 
118
 
119
+ threshold = float(current_grids['risk_score'].quantile(0.85))
120
+ drt_targets = current_grids.nlargest(50, 'risk_score')
121
 
 
122
  drt_assignments = []
123
  for idx, grid_row in drt_targets.iterrows():
124
  if not blind_stops.empty:
125
  distances = np.sqrt((blind_stops['lat'] - grid_row['lat'])**2 + (blind_stops['lon'] - grid_row['lon'])**2)
126
+ drt_assignments.append(blind_stops.loc[distances.idxmin()])
 
127
 
128
  unique_blind_stops = pd.DataFrame(drt_assignments).drop_duplicates('stop_id')
129
 
 
136
 
137
  def get_convex_hull(points):
138
  if len(points) <= 3: return points
139
+ points = sorted(points, key=lambda p: (float(p[0]), float(p[1])))
140
  lower = []
141
  for p in points:
142
  while len(lower) >= 2 and ccw(lower[-2], lower[-1], p) <= 0: lower.pop()
 
147
  upper.append(p)
148
  return lower[:-1] + upper[:-1]
149
 
 
150
  if not unique_blind_stops.empty and not nbus_stops.empty:
151
+ centroid_lat = float(unique_blind_stops['lat'].mean())
152
+ centroid_lon = float(unique_blind_stops['lon'].mean())
153
 
 
154
  dist_to_hub = np.sqrt((nbus_stops['lat'] - centroid_lat)**2 + (nbus_stops['lon'] - centroid_lon)**2)
155
  closest_hubs = nbus_stops.loc[dist_to_hub.nsmallest(3).index]
156
+ transfer_coords = sorted(closest_hubs[['lat', 'lon']].values.tolist(), key=lambda x: x[1])
 
 
 
157
 
158
  loop_stops = pd.concat([unique_blind_stops, closest_hubs]).drop_duplicates('stop_id')
 
 
159
  coords = loop_stops[['lat', 'lon']].values.tolist()
160
  hull_coords = get_convex_hull(coords)
161
 
162
+ hull_lats = [float(pt[0]) for pt in hull_coords]
163
+ hull_lons = [float(pt[1]) for pt in hull_coords]
164
  unique_blind_stops = unique_blind_stops[unique_blind_stops.apply(lambda r: any(abs(r['lat'] - hl) < 1e-6 and abs(r['lon'] - hlon) < 1e-6 for hl, hlon in zip(hull_lats, hull_lons)), axis=1)]
165
 
166
+ hull_coords.append(hull_coords[0])
167
+ loop_coords = [[float(pt[0]), float(pt[1])] for pt in hull_coords]
168
 
 
 
 
169
  st.title("๐ŸšŒ ์‹ฌ์•ผ ๋Œ€์ค‘๊ตํ†ต N๋ฒ„์Šค-DRT ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ")
 
170
 
171
  col1, col2 = st.columns([2, 1])
172
 
173
  with col1:
174
  m = folium.Map(location=[37.498, 127.027], zoom_start=15, tiles="CartoDB dark_matter")
175
 
 
176
  for idx, row in current_grids.iterrows():
177
+ rs = float(row['risk_score'])
178
+ if rs > 0.3:
179
  folium.CircleMarker(
180
+ location=[float(row['lat']), float(row['lon'])], radius=4,
181
+ color="red" if rs > threshold else "orange", weight=0, fill=True,
182
+ fill_color="red" if rs > threshold else "orange", fill_opacity=float(rs),
183
+ popup=str(f"Risk: {rs:.2f}")
184
  ).add_to(m)
185
 
 
186
  for idx, row in blind_stops.iterrows():
187
  folium.CircleMarker(
188
+ location=[float(row['lat']), float(row['lon'])], radius=2, color="gray", weight=0, fill=True, fill_color="gray", tooltip="์ผ๋ฐ˜ ์ •๋ฅ˜์žฅ"
189
  ).add_to(m)
190
 
 
191
  colors = ['cyan', 'lime', 'yellow']
192
  for i, route in enumerate(real_bus_routes):
193
  if route['coords']:
194
  folium.PolyLine(
195
+ locations=[[float(pt[0]), float(pt[1])] for pt in route['coords']], dash_array="15, 20",
196
+ color=str(colors[i % len(colors)]), weight=4, opacity=0.6, tooltip=str(f"N๋ฒ„์Šค: {route['name']}")
 
197
  ).add_to(m)
198
 
 
199
  if loop_coords:
200
  folium.PolyLine(
201
  locations=loop_coords, dash_array="10, 15",
202
+ color='purple', weight=5, opacity=0.9, tooltip="DRT ๋ฃจํ”„"
 
203
  ).add_to(m)
204
 
 
205
  if len(transfer_coords) >= 2:
206
  folium.PolyLine(
207
+ locations=[[float(pt[0]), float(pt[1])] for pt in transfer_coords], dash_array="1, 10",
208
+ color='orange', weight=8, opacity=1.0, tooltip="ํ™˜์Šน ๊ตฌ์—ญ"
 
209
  ).add_to(m)
210
 
 
211
  for idx, row in unique_blind_stops.iterrows():
212
  folium.Marker(
213
+ location=[float(row['lat']), float(row['lon'])], popup=str(f"์Šนํ•˜์ฐจ: {row['name']}"),
214
  icon=folium.Icon(color="purple", icon="bus", prefix="fa")
215
  ).add_to(m)
216
 
 
217
  if not closest_hubs.empty:
218
  for idx, row in closest_hubs.iterrows():
219
  folium.CircleMarker(
220
+ location=[float(row['lat']), float(row['lon'])], radius=7, color="gold", weight=2, fill=True, fill_color="orange", fill_opacity=0.8,
221
+ tooltip=str(f"ํ™˜์Šน: {row['name']}")
222
  ).add_to(m)
223
 
224
+ # st_folium ํ˜ธ์ถœ ์‹œ ์ง๋ ฌํ™” ์—๋Ÿฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด returned_objects=[] ์˜ต์…˜ ์ถ”๊ฐ€
225
+ st_folium(m, width=800, height=500, returned_objects=[])
226
 
227
  with col2:
228
+ st.subheader("๐Ÿ’ก ์‹œ๋‚˜๋ฆฌ์˜ค")
229
+ st.info(f"์„ ํƒ ์‹œ๊ฐ„: **{selected_time}**")