Spaces:
Sleeping
Sleeping
| import pandas as pd | |
| import numpy as np | |
| 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 | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| df = pd.read_csv('02_simpleml_komplet_pohyb.csv') | |
| zony = pd.read_csv('00_zony.csv') | |
| df = df.dropna() | |
| df = df[df['pocet_cest'] >= 0] | |
| df = df[df['vzdalenost'] >= 0] | |
| 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 | |
| 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': 'Ranni spicka (6-8 h)', | |
| 'dopoledne': 'Dopoledne (9-11 h)', | |
| 'odpoledne': 'Odpoledne (12-14 h)', | |
| 'vecer_spicka':'Vecerni spicka (15-18 h)', | |
| 'vecer_pozde': 'Pozdni vecer (19 h+)', | |
| } | |
| OKNO_REVERSE = {v: k for k, v in OKNO_CZ.items()} | |
| VEK_CZ = { | |
| 'deti_6_14': 'Deti (6-14 let)', | |
| 'studenti_15_19': 'Studenti (15-19 let)', | |
| 'dospeli_prace_20_64': 'Dospeli / pracujici (20-64)', | |
| 'seniori_65plus': 'Seniori (65+)', | |
| } | |
| VEK_REVERSE = {v: k for k, v in VEK_CZ.items()} | |
| UCEL_CZ = { | |
| 'prace': 'Prace / dojizdenj', | |
| 'skola': 'Skola', | |
| 'nakup_zdravi': 'Nakupy / zdravi', | |
| 'volny_cas': 'Volny cas', | |
| 'ostatni': 'Ostatni', | |
| } | |
| UCEL_REVERSE = {v: k for k, v in UCEL_CZ.items()} | |
| POCASI_MAP = {'Hezky': 0.1, 'Promenlivě': 0.4, 'Špatně': 0.7, 'Extremně špatně': 1.0} | |
| 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':'Ctvrtleti','denni_typ_enc':'Typ dne', | |
| 'casove_okno_enc':'Cas odjezdu','vekova_skupina_enc':'Vek cestujicich', | |
| 'ucel_enc':'Ucel cesty','hlavni_mod_enc':'Dopravni mod', | |
| 'udalost':'Udalost v cili','udalost_velikost':'Velikost udalosti', | |
| 'podil_spatne_pocasi':'Pocasi','vzdalenost':'Vzdalenost trasy', | |
| 'cil_velikost':'Velikost cilove obce','cil_izolace':'Izolovanost cile', | |
| 'cil_skola':'Skola v cili','cil_zamestnavatel':'Zamestnavatel v cili', | |
| 'cil_uzel':'Dopravni uzel v cili','zdroj_velikost':'Velikost vychozi obce', | |
| 'zdroj_izolace':'Izolovanost vychozi obce' | |
| } | |
| top5 = sorted(zip([FEAT_LABELS[f] for f in FEAT], mdl.feature_importances_), key=lambda x: -x[1])[:5] | |
| def enc(col, val): | |
| le = le_dict[col] | |
| return int(le.transform([val])[0]) if val in le.classes_ else 0 | |
| def predikuj(zdroj, cil, rezim_casu, okno_vyber, hodina_odjezdu, | |
| vekova_skupina, ucel, je_udalost, udalost_vel, pocasi, datum): | |
| import datetime | |
| if rezim_casu == "Casove 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" | |
| 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: | |
| rok=2025; ctvrtleti=2; denni_typ='vsedni' | |
| vc = VEK_REVERSE.get(vekova_skupina,'dospeli_prace_20_64') | |
| uc = UCEL_REVERSE.get(ucel,'prace') | |
| pc = POCASI_MAP.get(pocasi, 0.4) | |
| 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)) | |
| if pocet >= 8: | |
| stav="PEVNY SPOJ"; rezim="Spoj jede automaticky — dostatecna poptavka." | |
| akce="Standardni provoz. Neni treba zasah."; bg="#d4edda"; bc="#28a745"; ikona="🟢" | |
| elif pocet >= 4: | |
| stav="FLEXBUS — ON DEMAND" | |
| rezim="Spoj jede pouze pokud cestujici objednaji pres aplikaci (min. 1 hod. predem)." | |
| akce="Aktivovat FlexBus okno. Sledujte objednavky do 1 hod. pred odjezdem." | |
| bg="#fff3cd"; bc="#e0a800"; ikona="🟡" | |
| else: | |
| stav="SPOJ NEJEDE"; rezim="Poptavka prilis nizka — spoj se nevyplati." | |
| akce="Zvazze zachranny tarif (taxi voucher) pro izolované oblasti." | |
| bg="#f8d7da"; bc="#dc3545"; ikona="🔴" | |
| faktory=[] | |
| if je_udalost: faktory.append(f"udalost v cili (vel. {udalost_vel})") | |
| if pc>=0.7: faktory.append("spatne pocasi snizuje poptavku") | |
| if len(zc) and float(zc['izolace'].values[0])>0.4: faktory.append("izolovaná cilova obec") | |
| if denni_typ in ['sobota','nedele']: faktory.append("vikend — jiny vzorec pohybu") | |
| if casove_okno in ['rano_spicka','vecer_spicka']: faktory.append("spickova hodina — vyssi poptavka") | |
| fakt = " | ".join(faktory) if faktory else "standardni podminky" | |
| okno_label = OKNO_CZ.get(casove_okno, casove_okno) | |
| result = f""" | |
| <div style="border:2px solid {bc};background:{bg};padding:20px;border-radius:12px;font-family:sans-serif;margin-bottom:12px"> | |
| <div style="font-size:1.4em;font-weight:700;margin-bottom:4px">{ikona} {stav}</div> | |
| <div style="font-size:2.4em;font-weight:800;color:{bc};margin-bottom:2px">{pocet} cestujicich</div> | |
| <div style="color:#666;font-size:0.88em;margin-bottom:14px">+/- {round(mae_m,1)} (interval nejistoty modelu)</div> | |
| <hr style="border:none;border-top:1px solid #ccc;margin:10px 0"> | |
| <table style="width:100%;font-size:0.93em;border-collapse:collapse"> | |
| <tr><td style="padding:3px 8px;color:#555;width:145px">Trasa</td><td><b>{zdroj} -> {cil}</b></td></tr> | |
| <tr><td style="padding:3px 8px;color:#555">Datum / cas</td><td><b>{datum} {hodina_disp}</b> <span style="color:#777">({okno_label})</span></td></tr> | |
| <tr><td style="padding:3px 8px;color:#555">Cestujici</td><td>{vekova_skupina} — {ucel}</td></tr> | |
| <tr><td style="padding:3px 8px;color:#555">Klic. faktory</td><td>{fakt}</td></tr> | |
| </table> | |
| <hr style="border:none;border-top:1px solid #ccc;margin:10px 0"> | |
| <div style="margin-bottom:4px"><b>Rezim spoje:</b> {rezim}</div> | |
| <div><b>Doporucena akce:</b> {akce}</div> | |
| </div>""" | |
| imp_bars = "".join([ | |
| f'<div style="display:flex;align-items:center;gap:8px;margin:4px 0">' | |
| f'<div style="width:190px;font-size:0.85em;color:#444;flex-shrink:0">{n}</div>' | |
| f'<div style="background:#1a56db;height:10px;width:{max(int(v*500),4)}px;border-radius:4px"></div>' | |
| f'<div style="font-size:0.8em;color:#666">{v:.3f}</div></div>' | |
| for n,v in top5 | |
| ]) | |
| model_info = f""" | |
| <div style="background:#f0f4ff;border:1px solid #c7d9ff;padding:16px;border-radius:10px;font-family:sans-serif"> | |
| <b>Vykon modelu</b> | MAE: <b>{mae_m:.2f}</b> | | |
| Baseline: <b>{mae_b:.2f}</b> | Zlepseni: <b style="color:#1a56db">{zlep} %</b> | |
| <hr style="border:none;border-top:1px solid #c7d9ff;margin:10px 0"> | |
| <div style="font-size:0.9em;font-weight:600;margin-bottom:6px">Top 5 faktoru ktere ovlivnuji predpoved:</div> | |
| {imp_bars} | |
| </div>""" | |
| return result, model_info | |
| vsechny_zony = sorted(df['zdroj'].unique().tolist()) | |
| with gr.Blocks(title="FlexBus Dispatch", theme=gr.themes.Base()) as demo: | |
| gr.HTML(""" | |
| <div style="background:#1a56db;padding:22px 28px;border-radius:12px;margin-bottom:16px"> | |
| <div style="display:flex;align-items:center;gap:14px"> | |
| <span style="font-size:2.2em">🚌</span> | |
| <div> | |
| <div style="color:white;font-size:1.7em;font-weight:700;line-height:1.1">FlexBus Dispatch</div> | |
| <div style="color:#c7d9ff;font-size:0.93em;margin-top:3px"> | |
| AI system pro optimalizaci spoju · Plzensky kraj · | |
| pro dopravni podniky a krajske koordinatory | |
| </div> | |
| </div> | |
| </div> | |
| </div>""") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("#### Trasa") | |
| zdroj_in = gr.Dropdown(vsechny_zony, label="Vychozi zastavka / obec", value="Rokycany") | |
| cil_in = gr.Dropdown(vsechny_zony, label="Cilova zastavka / obec", value="Plzen-centrum") | |
| gr.Markdown("#### Cas odjezdu") | |
| datum_in = gr.Textbox(label="Datum (YYYY-MM-DD)", value="2025-09-20") | |
| rezim_in = gr.Radio(["Konkretni hodina", "Casove okno"], label="Zadani casu", value="Konkretni hodina") | |
| hodina_in = gr.Slider(0, 23, value=17, step=1, label="Hodina odjezdu", visible=True) | |
| okno_in = gr.Dropdown(list(OKNO_CZ.values()), label="Casove okno", | |
| value="Vecerni spicka (15-18 h)", visible=False) | |
| def prepni(r): | |
| return gr.update(visible=r=="Konkretni hodina"), gr.update(visible=r=="Casove okno") | |
| rezim_in.change(prepni, inputs=rezim_in, outputs=[hodina_in, okno_in]) | |
| with gr.Column(scale=1): | |
| gr.Markdown("#### Profil cestujicich") | |
| vek_in = gr.Dropdown(list(VEK_CZ.values()), label="Vekova skupina", | |
| value="Dospeli / pracujici (20-64)") | |
| ucel_in = gr.Dropdown(list(UCEL_CZ.values()), label="Ucel cesty", value="Prace / dojizdenj") | |
| gr.Markdown("#### Kontext") | |
| udalost_in = gr.Checkbox(label="V cili se kona udalost (koncert, zapas, festival...)") | |
| udalost_vel = gr.Slider(0, 5, value=0, step=1, label="Velikost udalosti (0=zadna, 5=velka)") | |
| pocasi_in = gr.Dropdown(list(POCASI_MAP.keys()), label="Predpokladane pocasi", value="Promenlivě") | |
| gr.HTML("<div style='height:8px'></div>") | |
| btn = gr.Button("Analyzovat spoj", variant="primary", size="lg") | |
| gr.HTML("<hr style='margin:8px 0'>") | |
| gr.Markdown("#### Vysledek analyzy") | |
| result_html = gr.HTML() | |
| model_html = gr.HTML() | |
| btn.click(predikuj, | |
| inputs=[zdroj_in, cil_in, rezim_in, okno_in, hodina_in, | |
| vek_in, ucel_in, udalost_in, udalost_vel, pocasi_in, datum_in], | |
| outputs=[result_html, model_html]) | |
| with gr.Accordion("O modelu a etice", open=False): | |
| gr.Markdown(f""" | |
| **Proc GradientBoosting a ne jen prumer?** | |
| Prosty prumer vidi jen "kolik jelo minule" — model navic zohlednuje vzdalenost trasy, izolovanost obce, typ dne, udalost i pocasi. | |
| Vysledek: MAE {mae_m:.2f} vs baseline {mae_b:.2f} — o {zlep} % presnejsi. | |
| Na trasach s malo daty model "pujcuje" vzorec od zon s podobnymi vlastnostmi — nespolaha jen na lokalni historii. | |
| Finalni rozhodnuti je vzdy na dispecerovi — model doporucuje, nerozhoduje. | |
| **Etika a soukromi** | |
| Vyhradne agregovane pocty — zadna jmena, tvare ani SPZ. | |
| Male obce maji garantovany minimalni spoj bez ohledu na predpoved. | |
| Nejistota je vzdy zobrazena s intervalem. | |
| **Sber dat v case** | |
| Odbavovaci system PMDP · anonymni cidla na zastavkach · kalendar akci (IDPK / PINE) · meteorologicka data. | |
| """) | |
| demo.launch() |