MoonlightBus / app.py
upsonyeon's picture
Upload 5 files
5c03f9a verified
Raw
History Blame Contribute Delete
13.5 kB
import streamlit as st
import pandas as pd
import numpy as np
import folium
from streamlit_folium import st_folium
import requests
import json
import os
st.set_page_config(page_title="์‹ฌ์•ผ ๋Œ€์ค‘๊ตํ†ต ์ตœ์ ํ™” ๋Œ€์‹œ๋ณด๋“œ", layout="wide")
@st.cache_data
def fetch_osm_data_v4():
bbox = "37.491,127.020,37.505,127.035"
endpoints = [
"https://overpass-api.de/api/interpreter",
"https://overpass.kumi.systems/api/interpreter",
"https://overpass.osm.ch/api/interpreter"
]
query = f"""
[out:json][timeout:60];
(
relation["route"="bus"]["ref"~"N13|N15|N37|N75"]({bbox});
node["highway"="bus_stop"]({bbox});
node["amenity"~"pub|bar|nightclub|restaurant"]({bbox});
node["highway"="street_lamp"]({bbox});
node["man_made"="surveillance"]({bbox});
);
out body geom;
"""
headers = {'User-Agent': 'SmartTransitMVP/2.0'}
data = None
cache_file = "osm_backup.json"
for url in endpoints:
try:
response = requests.post(url, data=query, headers=headers, timeout=65)
response.raise_for_status()
data = response.json()
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(data, f)
break
except Exception as e:
print(f"Failed to fetch from {url}: {e}")
continue
if data is None:
if os.path.exists(cache_file):
st.warning("โš ๏ธ ์‹ค์‹œ๊ฐ„ ์„œ๋ฒ„(Overpass API) ์ ‘์† ์ง€์—ฐ์œผ๋กœ ์ธํ•ด ๊ธฐ์กด ๋ฐฑ์—… ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค.")
with open(cache_file, "r", encoding="utf-8") as f:
data = json.load(f)
else:
st.error("๋ฐ์ดํ„ฐ ์„œ๋ฒ„ ์ ‘์†์ด ์›ํ™œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.")
data = {'elements': []}
bus_stops, amenities, safety_infra, real_bus_routes = [], [], [], []
for element in data.get('elements', []):
if element['type'] == 'node':
lat = float(element['lat'])
lon = float(element['lon'])
tags = element.get('tags', {})
if 'highway' in tags and tags['highway'] == 'bus_stop':
bus_stops.append({'lat': lat, 'lon': lon, 'stop_id': str(element['id']), 'name': str(tags.get('name', '์ •๋ฅ˜์žฅ'))})
elif 'amenity' in tags:
amenities.append({'lat': lat, 'lon': lon})
elif 'highway' in tags or 'man_made' in tags:
safety_infra.append({'lat': lat, 'lon': lon})
elif element['type'] == 'relation':
tags = element.get('tags', {})
ref = str(tags.get('ref', 'N๋ฒ„์Šค'))
name = str(tags.get('name', ref))
coords = []
for member in element.get('members', []):
if member['type'] == 'way' and 'geometry' in member:
for pt in member['geometry']:
coords.append([float(pt['lat']), float(pt['lon'])])
if coords:
real_bus_routes.append({'name': name, 'ref': ref, 'coords': coords})
return pd.DataFrame(bus_stops), pd.DataFrame(amenities), pd.DataFrame(safety_infra), real_bus_routes
stops_df, amenities_df, safety_df, real_bus_routes = fetch_osm_data_v4()
@st.cache_data
def create_grid_features(_stops, _amenities, _safety):
lats = np.linspace(37.492, 37.504, 20)
lons = np.linspace(127.021, 127.034, 20)
grid_data = []
stop_coords = _stops[['lat', 'lon']].values if not _stops.empty else np.array([])
amenity_coords = _amenities[['lat', 'lon']].values if not _amenities.empty else np.array([])
safety_coords = _safety[['lat', 'lon']].values if not _safety.empty else np.array([])
for lat in lats:
for lon in lons:
point = np.array([lat, lon])
demand = int(np.sum(np.sqrt(np.sum((amenity_coords - point)**2, axis=1)) < 0.002)) if len(amenity_coords) > 0 else 0
deficit = float(np.min(np.sqrt(np.sum((stop_coords - point)**2, axis=1)))) if len(stop_coords) > 0 else 0.0
safety_count = int(np.sum(np.sqrt(np.sum((safety_coords - point)**2, axis=1)) < 0.002)) if len(safety_coords) > 0 else 0
grid_data.append({'lat': float(lat), 'lon': float(lon), 'raw_demand': demand, 'raw_deficit': deficit, 'raw_safety_count': safety_count})
df = pd.DataFrame(grid_data)
df['base_demand'] = df['raw_demand'] / df['raw_demand'].max() if df['raw_demand'].max() > 0 else 0.0
df['base_deficit'] = df['raw_deficit'] / df['raw_deficit'].max() if df['raw_deficit'].max() > 0 else 0.0
df['base_risk'] = 1.0 - (df['raw_safety_count'] / df['raw_safety_count'].max()) if df['raw_safety_count'].max() > 0 else 1.0
return df
grids_df = create_grid_features(stops_df, amenities_df, safety_df)
if not stops_df.empty and len(real_bus_routes) > 0:
nbus_coords = np.array([pt for route in real_bus_routes for pt in route['coords']])
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)
stops_df['is_nbus_stop'] = stops_df['min_dist_to_nbus'] < 0.001
else:
stops_df['is_nbus_stop'] = False
nbus_stops = stops_df[stops_df['is_nbus_stop']]
blind_stops = stops_df[~stops_df['is_nbus_stop']]
st.sidebar.header("โš™๏ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์„ค์ •")
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")
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
st.sidebar.caption("์ˆ˜๋™์œผ๋กœ ๊ฐ€์ค‘์น˜๋ฅผ ์กฐ์ ˆํ•˜๊ฑฐ๋‚˜ ์‹œ๊ฐ„๋Œ€๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ ๋…ธ์„ ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜์„ธ์š”.")
alpha = st.sidebar.slider("ํ˜ธ์ถœ ์ˆ˜์š”(์ƒ์—…/์œ ํฅ) ๊ฐ€์ค‘์น˜ (ฮฑ)", 0.0, 1.0, 0.8)
beta = st.sidebar.slider("์ •๋ฅ˜์žฅ ๊ฒฐํ•๋„ ๊ฐ€์ค‘์น˜ (ฮฒ)", 0.0, 1.0, 0.2)
gamma = st.sidebar.slider("์•ˆ์ „ ์ทจ์•ฝ๋„(์–ด๋‘์šด ๊ณจ๋ชฉ) ๊ฐ€์ค‘์น˜ (ฮณ)", 0.0, 1.0, 0.4)
drt_budget = st.sidebar.number_input("ํˆฌ์ž… ๊ฐ€๋Šฅ DRT ์ฐจ๋Ÿ‰ ์ˆ˜", min_value=1, max_value=20, value=5)
current_grids = grids_df.copy()
# ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ธฐ๋ฐ˜ ์ˆ˜์š”/์œ„ํ—˜๋„ ์กฐ์ ˆ (์‹œ๊ฐ„์ด ๋Šฆ์–ด์งˆ์ˆ˜๋ก ์œ ํฅ์ˆ˜์š” ๊ฐ์†Œ, ์ฃผ๊ฑฐ์ง€/์•ˆ์ „ ์œ„ํ—˜๋„ ๋ถ€๊ฐ)
if time_idx <= 3:
time_factor = 1.2
elif time_idx <= 7:
time_factor = 0.8
else:
time_factor = 0.3
current_grids['demand'] = current_grids['base_demand'] * time_factor
current_grids['deficit'] = current_grids['base_deficit']
current_grids['risk'] = current_grids['base_risk'] * (2.0 - time_factor)
current_grids['risk_score'] = alpha * current_grids['demand'] + beta * current_grids['deficit'] + gamma * current_grids['risk']
threshold = float(current_grids['risk_score'].quantile(0.85))
drt_targets = current_grids.nlargest(50, 'risk_score')
drt_assignments = []
for idx, grid_row in drt_targets.iterrows():
if not blind_stops.empty:
distances = np.sqrt((blind_stops['lat'] - grid_row['lat'])**2 + (blind_stops['lon'] - grid_row['lon'])**2)
drt_assignments.append(blind_stops.loc[distances.idxmin()])
unique_blind_stops = pd.DataFrame(drt_assignments).drop_duplicates('stop_id')
loop_coords = []
transfer_coords = []
closest_hubs = pd.DataFrame()
def ccw(p1, p2, p3):
return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0])
def get_convex_hull(points):
if len(points) <= 3: return points
points = sorted(points, key=lambda p: (float(p[0]), float(p[1])))
lower = []
for p in points:
while len(lower) >= 2 and ccw(lower[-2], lower[-1], p) <= 0: lower.pop()
lower.append(p)
upper = []
for p in reversed(points):
while len(upper) >= 2 and ccw(upper[-2], upper[-1], p) <= 0: upper.pop()
upper.append(p)
return lower[:-1] + upper[:-1]
if not unique_blind_stops.empty and not nbus_stops.empty:
centroid_lat = float(unique_blind_stops['lat'].mean())
centroid_lon = float(unique_blind_stops['lon'].mean())
dist_to_hub = np.sqrt((nbus_stops['lat'] - centroid_lat)**2 + (nbus_stops['lon'] - centroid_lon)**2)
closest_hubs = nbus_stops.loc[dist_to_hub.nsmallest(3).index]
transfer_coords = sorted(closest_hubs[['lat', 'lon']].values.tolist(), key=lambda x: x[1])
loop_stops = pd.concat([unique_blind_stops, closest_hubs]).drop_duplicates('stop_id')
coords = loop_stops[['lat', 'lon']].values.tolist()
hull_coords = get_convex_hull(coords)
hull_lats = [float(pt[0]) for pt in hull_coords]
hull_lons = [float(pt[1]) for pt in hull_coords]
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)]
hull_coords.append(hull_coords[0])
loop_coords = [[float(pt[0]), float(pt[1])] for pt in hull_coords]
st.title("๐ŸšŒ ์‹ฌ์•ผ ๋Œ€์ค‘๊ตํ†ต N๋ฒ„์Šค-DRT ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ")
col1, col2 = st.columns([2, 1])
with col1:
m = folium.Map(location=[37.498, 127.027], zoom_start=15, tiles="CartoDB dark_matter")
for idx, row in current_grids.iterrows():
rs = float(row['risk_score'])
if rs > 0.3:
folium.CircleMarker(
location=[float(row['lat']), float(row['lon'])], radius=4,
color="red" if rs > threshold else "orange", weight=0, fill=True,
fill_color="red" if rs > threshold else "orange", fill_opacity=float(rs),
popup=str(f"Risk: {rs:.2f}")
).add_to(m)
for idx, row in blind_stops.iterrows():
folium.CircleMarker(
location=[float(row['lat']), float(row['lon'])], radius=2, color="gray", weight=0, fill=True, fill_color="gray", tooltip="์ผ๋ฐ˜ ์ •๋ฅ˜์žฅ"
).add_to(m)
for idx, row in nbus_stops.iterrows():
folium.CircleMarker(
location=[float(row['lat']), float(row['lon'])], radius=3, color="cyan", weight=1, fill=True, fill_color="cyan",
tooltip=str(f"ํ™˜์Šน: {row['name']}")
).add_to(m)
colors = ['cyan', 'lime', 'yellow']
for i, route in enumerate(real_bus_routes):
if route['coords']:
folium.PolyLine(
locations=[[float(pt[0]), float(pt[1])] for pt in route['coords']], dash_array="15, 20",
color=str(colors[i % len(colors)]), weight=4, opacity=0.6, tooltip=str(f"N๋ฒ„์Šค: {route['name']}")
).add_to(m)
if loop_coords:
folium.PolyLine(
locations=loop_coords, dash_array="10, 15",
color='purple', weight=5, opacity=0.9, tooltip="DRT ๋ฃจํ”„"
).add_to(m)
if len(transfer_coords) >= 2:
folium.PolyLine(
locations=[[float(pt[0]), float(pt[1])] for pt in transfer_coords], dash_array="1, 10",
color='orange', weight=8, opacity=1.0, tooltip="ํ™˜์Šน ๊ตฌ์—ญ"
).add_to(m)
for idx, row in unique_blind_stops.iterrows():
folium.Marker(
location=[float(row['lat']), float(row['lon'])], popup=str(f"์Šนํ•˜์ฐจ: {row['name']}"),
icon=folium.Icon(color="purple", icon="bus", prefix="fa")
).add_to(m)
if not closest_hubs.empty:
for idx, row in closest_hubs.iterrows():
folium.CircleMarker(
location=[float(row['lat']), float(row['lon'])], radius=7, color="gold", weight=2, fill=True, fill_color="orange", fill_opacity=0.8,
tooltip=str(f"ํ™˜์Šน: {row['name']}")
).add_to(m)
# st_folium ํ˜ธ์ถœ ์‹œ ์ง๋ ฌํ™” ์—๋Ÿฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด returned_objects=[] ์˜ต์…˜ ๋ฐ ๋ช…์‹œ์  key ์ถ”๊ฐ€
st_folium(m, width=800, height=500, returned_objects=[], key="main_map")
with col2:
st.subheader("๐Ÿ’ก ๋‹ค์ด๋‚ด๋ฏน ๋ฃจํ”„ & ํ™˜์Šน ์‹œ๋‚˜๋ฆฌ์˜ค")
st.info(f"ํ˜„์žฌ ์„ ํƒ๋œ ์‹œ๊ฐ„: **{selected_time}**\n\n์‹œ๊ฐ„๋Œ€์— ๋”ฐ๋ฅธ ๊ฐ€์ค‘์น˜ ๋ณ€ํ™”๋กœ ํƒ€๊ฒŸ ์ง€์—ญ์ด ์ƒ์—…์ง€๊ตฌ์™€ ์ฃผ๊ฑฐ์ง€๊ตฌ ์‚ฌ์ด๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋„˜๋‚˜๋“ญ๋‹ˆ๋‹ค.")
if not closest_hubs.empty:
hub_names = ", ".join(str(name) for name in closest_hubs['name'].tolist())
st.warning(f"**๐Ÿ”ฅ ํ™˜์Šน ๊ณต์œ  ๊ตฌ์—ญ (Transfer Zone)**\n\nN๋ฒ„์Šค ๋…ธ์„ ์˜ **'{hub_names}'** ์ •๋ฅ˜์žฅ๋“ค์„ DRT ๋ฃจํ”„๊ฐ€ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ผ ์ฃผํ–‰ํ•˜๋ฉฐ ๊ฒน์นฉ๋‹ˆ๋‹ค. (์ง€๋„ ์ƒ **์˜ค๋ Œ์ง€์ƒ‰ ์„ **)\n\n์Šน๊ฐ์€ ์ด ๊ตฌ์—ญ ๋‚ด ์•„๋ฌด ๊ณณ์—์„œ๋‚˜ ๋‚ด๋ ค N๋ฒ„์Šค๋กœ ํŽธํ•˜๊ฒŒ ๊ฐˆ์•„ํƒˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
st.divider()
st.subheader("๐Ÿ“ˆ ๋ฐฐ์ฐจ๊ฐ„๊ฒฉ(๋Œ€๊ธฐ ์‹œ๊ฐ„) ๊ฐœ์„  ํšจ๊ณผ")
col_b, col_a, col_diff = st.columns(3)
base_n_interval = 40
drt_interval = max(5, 30 - drt_budget * 4)
col_b.metric("์‚ฌ๊ฐ์ง€๋Œ€ ๊ธฐ์กด ๋ฐฐ์ฐจ๊ฐ„๊ฒฉ", "์šดํ–‰ ์—†์Œ (โˆž)", delta=None)
col_a.metric("DRT ๋„์ž… ํ›„ ์‚ฌ๊ฐ์ง€๋Œ€", f"์•ฝ {drt_interval} ๋ถ„", delta="-โˆž ๋ถ„ (์‹ ๊ทœ ์„œ๋น„์Šค ์ฐฝ์ถœ)", delta_color="normal")
col_diff.metric("๊ธฐ์กด N๋ฒ„์Šค ๋ฐฐ์ฐจ๊ฐ„๊ฒฉ (๊ฐ„์„ )", f"์•ฝ {base_n_interval} ๋ถ„", delta="์œ ์ง€", delta_color="off")
st.markdown("> **๊ฒฐ๊ณผ**: ์ง€๋„ ์ƒ ๋น›๋‚˜๋Š” ์˜ค๋ Œ์ง€์ƒ‰ ์„ ๊ตฌ๊ฐ„(ํ™˜์Šน ๊ตฌ์—ญ)์—์„œ ๋Œ€๊ธฐ์‹œ๊ฐ„ ์—†์ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ N๋ฒ„์Šค๋กœ ๊ฐˆ์•„ํƒˆ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹œ๊ฐ„๋Œ€์— ๋งž์ถฐ ์ตœ์ ์˜ ๊ฒฝ๋กœ๋กœ ๊ตฝ์ด์น˜๋Š” ๋˜‘๋˜‘ํ•œ ๋ฃจํ”„๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค.")