flexbus-plzen / app.py
simikkk's picture
Update app.py
03d9bcf verified
Raw
History Blame
13.3 kB
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} -&gt; {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> &nbsp;|&nbsp; MAE: <b>{mae_m:.2f}</b> &nbsp;|&nbsp;
Baseline: <b>{mae_b:.2f}</b> &nbsp;|&nbsp; 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 &nbsp;·&nbsp; Plzensky kraj &nbsp;·&nbsp;
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()