Spaces:
Running
Running
| 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") | |
| 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() | |
| 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๋ฒ์ค๋ก ๊ฐ์ํ ์ ์์ผ๋ฉฐ, ์๊ฐ๋์ ๋ง์ถฐ ์ต์ ์ ๊ฒฝ๋ก๋ก ๊ตฝ์ด์น๋ ๋๋ํ ๋ฃจํ๋ฅผ ํ์ฑํฉ๋๋ค.") | |