""" FlexBus Dispatch — AI systém pro optimalizaci spojů Plzeňský kraj | pro dopravní podniky a krajské koordinátory """ import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np import json import datetime import urllib.request import urllib.parse from sklearn.ensemble import GradientBoostingRegressor from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error import gradio as gr # ───────────────────────────────────────────── # 1. NAČTENÍ DAT # ───────────────────────────────────────────── df = pd.read_csv('02_simpleml_komplet_pohyb.csv') zony = pd.read_csv('00_zony.csv') # GTFS zastávky a linky stops = pd.read_csv('stops.txt', sep=';', low_memory=False) routes = pd.read_csv('routes.txt', sep=';', low_memory=False) stop_times = pd.read_csv('stop_times.txt', sep=';', low_memory=False) df = df.dropna() df = df[df['pocet_cest'] >= 0] df = df[df['vzdalenost'] >= 0] # ───────────────────────────────────────────── # 2. FEATURE ENGINEERING # ───────────────────────────────────────────── zony_cil = zony[['nazev','velikost','izolace','skola','zamestnavatel','uzel']].copy() zony_cil.columns = ['cil','cil_velikost','cil_izolace','cil_skola','cil_zamestnavatel','cil_uzel'] df = df.merge(zony_cil, on='cil', how='left') zony_src = zony[['nazev','velikost','izolace']].copy() zony_src.columns = ['zdroj','zdroj_velikost','zdroj_izolace'] df = df.merge(zony_src, on='zdroj', how='left') df = df.fillna(df.median(numeric_only=True)) le_dict = {} for col in ['denni_typ','casove_okno','vekova_skupina','ucel','hlavni_mod']: le = LabelEncoder() df[col+'_enc'] = le.fit_transform(df[col].astype(str)) le_dict[col] = le FEAT = [ 'rok','ctvrtleti','denni_typ_enc','casove_okno_enc', 'vekova_skupina_enc','ucel_enc','hlavni_mod_enc', 'udalost','udalost_velikost','podil_spatne_pocasi', 'vzdalenost','cil_velikost','cil_izolace','cil_skola', 'cil_zamestnavatel','cil_uzel','zdroj_velikost','zdroj_izolace' ] X = df[FEAT]; y = df['pocet_cest'] Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42) mdl = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1, max_depth=4, random_state=42) mdl.fit(Xtr, ytr) mae_m = mean_absolute_error(yte, mdl.predict(Xte)) mae_b = mean_absolute_error(yte, [ytr.mean()]*len(yte)) zlep = round((1 - mae_m/mae_b)*100, 1) FEAT_LABELS = { 'rok':'Rok','ctvrtleti':'Čtvrtletí','denni_typ_enc':'Typ dne', 'casove_okno_enc':'Čas odjezdu','vekova_skupina_enc':'Věk cestujících', 'ucel_enc':'Účel cesty','hlavni_mod_enc':'Dopravní mód', 'udalost':'Událost v cíli','udalost_velikost':'Velikost události', 'podil_spatne_pocasi':'Počasí','vzdalenost':'Vzdálenost trasy', 'cil_velikost':'Velikost cílové obce','cil_izolace':'Izolovanost cíle', 'cil_skola':'Škola v cíli','cil_zamestnavatel':'Zaměstnavatel v cíli', 'cil_uzel':'Dopravní uzel v cíli','zdroj_velikost':'Velikost výchozí obce', 'zdroj_izolace':'Izolovanost výchozí obce' } top5 = sorted(zip([FEAT_LABELS[f] for f in FEAT], mdl.feature_importances_), key=lambda x: -x[1])[:5] # ───────────────────────────────────────────── # 3. LOOKUP MAPY A KONSTANTY # ───────────────────────────────────────────── def hodina_na_okno(h): h = int(h) if 6 <= h <= 8: return 'rano_spicka' elif 9 <= h <= 11: return 'dopoledne' elif 12 <= h <= 14: return 'odpoledne' elif 15 <= h <= 18: return 'vecer_spicka' else: return 'vecer_pozde' OKNO_CZ = { 'rano_spicka': 'Ranní špička (6–8 h)', 'dopoledne': 'Dopoledne (9–11 h)', 'odpoledne': 'Odpoledne (12–14 h)', 'vecer_spicka': 'Večerní špička (15–18 h)', 'vecer_pozde': 'Pozdní večer (19 h+)', } OKNO_REVERSE = {v: k for k, v in OKNO_CZ.items()} # Věkové skupiny — profesionální popis pro dopravní podniky, žádné zneužití VEK_CZ = { 'deti_6_14': 'Žáci ZŠ (6–14 let)', 'studenti_15_19': 'Studenti SŠ/VOŠ (15–19 let)', 'dospeli_prace_20_64': 'Ekonomicky aktivní (20–64 let)', 'seniori_65plus': 'Senioři (65+ let)', } VEK_REVERSE = {v: k for k, v in VEK_CZ.items()} UCEL_CZ = { 'prace': 'Dojížďka do práce', 'skola': 'Dojížďka do školy', 'nakup_zdravi': 'Nákupy / zdravotní péče', 'volny_cas': 'Volný čas / kultura', 'ostatni': 'Ostatní účely', } UCEL_REVERSE = {v: k for k, v in UCEL_CZ.items()} # Počasí — kategorie pro dispatcher UI POCASI_LABELS = ['Slunečno', 'Polojasno', 'Oblačno / déšť', 'Bouřky / sníh'] POCASI_MAP = {'Slunečno': 0.05, 'Polojasno': 0.25, 'Oblačno / déšť': 0.65, 'Bouřky / sníh': 1.0} def enc(col, val): le = le_dict[col] return int(le.transform([val])[0]) if val in le.classes_ else 0 vsechny_zony = sorted(df['zdroj'].unique().tolist()) # ───────────────────────────────────────────── # 4. POČASÍ — OPEN-METEO (zdarma, bez klíče) # ───────────────────────────────────────────── # Střed Plzeňského kraje PLZEN_LAT = 49.7477 PLZEN_LON = 13.3776 _weather_cache = {"fetched_at": None, "data": None} def get_weather_forecast(): """Stáhne 16denní předpověď z Open-Meteo API (cache 1 hod).""" now = datetime.datetime.utcnow() if _weather_cache["fetched_at"] and (now - _weather_cache["fetched_at"]).seconds < 3600: return _weather_cache["data"] try: url = ( f"https://api.open-meteo.com/v1/forecast" f"?latitude={PLZEN_LAT}&longitude={PLZEN_LON}" f"&daily=precipitation_sum,weathercode,temperature_2m_max,temperature_2m_min" f"&timezone=Europe%2FPrague&forecast_days=16" ) with urllib.request.urlopen(url, timeout=5) as r: data = json.loads(r.read()) _weather_cache["fetched_at"] = now _weather_cache["data"] = data return data except Exception: return None def pocasi_pro_datum(datum_str): """Vrátí (label, podil_spatne) pro konkrétní datum z předpovědi.""" data = get_weather_forecast() if not data: return "Polojasno", 0.25 try: dates = data["daily"]["time"] codes = data["daily"]["weathercode"] precip = data["daily"]["precipitation_sum"] if datum_str in dates: idx = dates.index(datum_str) wc = codes[idx] pr = precip[idx] or 0 # WMO weather codes → kategorie if wc in [0, 1]: return "Slunečno", 0.05 elif wc in [2, 3, 45, 48]: return "Polojasno", 0.25 elif wc in [51,53,55,61,63,71,73,80,81,82]: return "Oblačno / déšť", min(0.5 + pr/20, 0.9) else: return "Bouřky / sníh", 1.0 except Exception: pass return "Polojasno", 0.25 def get_forecast_html(): """Vrátí HTML tabulku 16denní předpovědi pro zobrazení v UI.""" data = get_weather_forecast() if not data: return "
Předpověď počasí není dostupná.
" try: dates = data["daily"]["time"] codes = data["daily"]["weathercode"] tmax = data["daily"]["temperature_2m_max"] tmin = data["daily"]["temperature_2m_min"] precip = data["daily"]["precipitation_sum"] WC_ICON = { 0:"☀️",1:"🌤",2:"⛅",3:"☁️",45:"🌫",48:"🌫", 51:"🌦",53:"🌧",55:"🌧",61:"🌧",63:"🌧",65:"🌧", 71:"🌨",73:"🌨",75:"❄️",80:"🌦",81:"🌧",82:"⛈", 95:"⛈",96:"⛈",99:"⛈" } rows = "" for i, d in enumerate(dates[:16]): dt = datetime.datetime.strptime(d, "%Y-%m-%d") day_name = ["Po","Út","St","Čt","Pá","So","Ne"][dt.weekday()] icon = WC_ICON.get(codes[i], "🌡") pr = precip[i] or 0 bg = "#fff3cd" if pr > 5 else ("#d4edda" if codes[i] <= 1 else "#f8f9fa") rows += ( f'| Datum | ' 'Počasí | ' 'Teplota | ' 'Srážky | ' '
|---|
Chyba při načítání předpovědi: {e}
" # ───────────────────────────────────────────── # 5. MAPA — LEAFLET.JS přes HTML # ───────────────────────────────────────────── def build_map_html(zdroj_filter=None, cil_filter=None, predikce_pocet=None): """ Sestaví interaktivní Leaflet mapu se zastávkami a linkami. - Zastávky: barevné body dle zóny, velikost dle vytížení - Linky: barevné podle typu (MHD=modrá, autobus=zelená, suburbanní=oranžová) - Tooltip na každé zastávce s názvem a predikcí """ # Připrav stops jako JSON stops_data = [] for _, row in stops.iterrows(): z = int(row.get('zone_id', 1)) # Barva zóny if z == 1: color = "#1a56db" # Plzeň centrum — modrá elif z <= 21: color = "#0e9f6e" # blízké okolí — zelená elif z <= 41: color = "#ff8c00" # vzdálenější — oranžová else: color = "#e02424" # venkov — červená # Zvýraznění vybrané trasy highlight = "" if zdroj_filter and row['stop_name'] == zdroj_filter: highlight = "ZDROJ" elif cil_filter and row['stop_name'] == cil_filter: highlight = "CIL" stops_data.append({ "id": int(row['stop_id']), "name": str(row['stop_name']), "lat": float(row['stop_lat']), "lon": float(row['stop_lon']), "zone": z, "color": color, "hl": highlight }) # Připrav linky jako JSON (trasy přes zastávky) # Seskup stop_times podle trip_id, vezmi první a poslední zastávku pro vizualizaci # (plná rekonstrukce tras by byla příliš velká — ukážeme reprezentativní sample) routes_data = [] sample_trips = stop_times['trip_id'].unique()[:80] # max 80 linek pro výkon st_indexed = stop_times[stop_times['trip_id'].isin(sample_trips)] stops_idx = stops.set_index('stop_id') for trip_id, grp in st_indexed.groupby('trip_id'): grp = grp.sort_values('stop_sequence') coords = [] for sid in grp['stop_id'].values: if sid in stops_idx.index: r = stops_idx.loc[sid] coords.append([float(r['stop_lat']), float(r['stop_lon'])]) if len(coords) >= 2: # Typ linky → barva routes_data.append({ "trip": int(trip_id), "coords": coords }) # Předpověď pro tooltip pred_info = "" if predikce_pocet is not None: pred_info = f"{predikce_pocet} cestujících" stops_json = json.dumps(stops_data) routes_json = json.dumps(routes_data) zdroj_js = json.dumps(zdroj_filter or "") cil_js = json.dumps(cil_filter or "") html = f""" """ return html # ───────────────────────────────────────────── # 6. PREDIKČNÍ FUNKCE # ───────────────────────────────────────────── def predikuj(zdroj, cil, rezim_casu, okno_vyber, hodina_odjezdu, vekova_skupina, ucel, je_udalost, udalost_vel, pocasi_manual, pouzit_predpoved_pocasi, datum): # --- Datum a čas --- try: d = datetime.datetime.strptime(datum, "%Y-%m-%d") rok = d.year; ctvrtleti = (d.month-1)//3+1; dow = d.weekday() denni_typ = 'vsedni' if dow < 5 else ('sobota' if dow == 5 else 'nedele') except Exception: rok = 2025; ctvrtleti = 2; denni_typ = 'vsedni' d = datetime.datetime.today() if rezim_casu == "Časové okno": casove_okno = OKNO_REVERSE.get(okno_vyber, 'odpoledne') hodina_disp = {"rano_spicka":"~7:00","dopoledne":"~10:00","odpoledne":"~13:00", "vecer_spicka":"~17:00","vecer_pozde":"~20:00"}.get(casove_okno,"") else: casove_okno = hodina_na_okno(hodina_odjezdu) hodina_disp = f"{int(hodina_odjezdu):02d}:00" # --- Počasí --- if pouzit_predpoved_pocasi: pocasi_label, pc = pocasi_pro_datum(datum) else: pocasi_label = pocasi_manual pc = POCASI_MAP.get(pocasi_manual, 0.25) # --- Encoding --- vc = VEK_REVERSE.get(vekova_skupina, 'dospeli_prace_20_64') uc = UCEL_REVERSE.get(ucel, 'prace') mask = (df['zdroj']==zdroj) & (df['cil']==cil) vzdal = float(df[mask]['vzdalenost'].mean()) if mask.any() else float(df['vzdalenost'].mean()) zc = zony[zony['nazev']==cil] zs = zony[zony['nazev']==zdroj] row = { 'rok':rok, 'ctvrtleti':ctvrtleti, 'denni_typ_enc': enc('denni_typ', denni_typ), 'casove_okno_enc': enc('casove_okno', casove_okno), 'vekova_skupina_enc': enc('vekova_skupina', vc), 'ucel_enc': enc('ucel', uc), 'hlavni_mod_enc': enc('hlavni_mod', 'autobus'), 'udalost': 1 if je_udalost else 0, 'udalost_velikost': float(udalost_vel), 'podil_spatne_pocasi': pc, 'vzdalenost': vzdal, 'cil_velikost': float(zc['velikost'].values[0]) if len(zc) else 0.5, 'cil_izolace': float(zc['izolace'].values[0]) if len(zc) else 0.5, 'cil_skola': int(zc['skola'].values[0]) if len(zc) else 0, 'cil_zamestnavatel':int(zc['zamestnavatel'].values[0]) if len(zc) else 0, 'cil_uzel': int(zc['uzel'].values[0]) if len(zc) else 0, 'zdroj_velikost': float(zs['velikost'].values[0]) if len(zs) else 0.5, 'zdroj_izolace': float(zs['izolace'].values[0]) if len(zs) else 0.5, } pocet = max(0.0, round(float(mdl.predict(pd.DataFrame([row])[FEAT])[0]), 1)) # --- Rozhodovací logika --- if pocet >= 8: stav = "PEVNÝ SPOJ" rezim = "Spoj jede automaticky — dostatečná poptávka." akce = "Standardní provoz. Není třeba zásah." bg = "#d4edda"; bc = "#28a745"; ikona = "🟢" elif pocet >= 4: stav = "FLEXBUS — ON DEMAND" rezim = "Spoj jede pouze při objednávce přes aplikaci (min. 1 hod. předem)." akce = "Aktivovat FlexBus okno. Sledujte objednávky do 1 hod. před odjezdem." bg = "#fff3cd"; bc = "#e0a800"; ikona = "🟡" else: stav = "SPOJ NEJEDE" rezim = "Poptávka příliš nízká — spoj se nevyplatí." akce = "Zvažte záchranný tarif (taxi voucher) pro izolované oblasti." bg = "#f8d7da"; bc = "#dc3545"; ikona = "🔴" # --- Klíčové faktory --- faktory = [] if je_udalost: faktory.append(f"událost v cíli (vel. {udalost_vel})") if pc >= 0.65: faktory.append(f"nepříznivé počasí ({pocasi_label})") if len(zc) and float(zc['izolace'].values[0]) > 0.4: faktory.append("izolovaná cílová obec") if denni_typ in ['sobota','nedele']: faktory.append("víkend — jiný vzorec pohybu") if casove_okno in ['rano_spicka','vecer_spicka']: faktory.append("špičková hodina — vyšší poptávka") if uc == 'skola' and denni_typ != 'vsedni': faktory.append("školní dojížďka o víkendu — nižší poptávka") fakt = " | ".join(faktory) if faktory else "standardní podmínky" okno_label = OKNO_CZ.get(casove_okno, casove_okno) vek_label = VEK_CZ.get(vc, vc) ucel_label = UCEL_CZ.get(uc, uc) # --- HTML výsledek --- result_html = f"""| Trasa | {zdroj} → {cil} |
| Datum / čas | {d.strftime("%d. %m. %Y")} v {hodina_disp} ({okno_label}) |
| Profil cestujících | {vek_label} — {ucel_label} |
| Počasí | {pocasi_label} {'(z předpovědi Open-Meteo)' if pouzit_predpoved_pocasi else ''} |
| Klíč. faktory | {fakt} |