Spaces:
Sleeping
Sleeping
Create app.py
#5
by MatejDuch - opened
app.py
CHANGED
|
@@ -1,22 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import pandas as pd
|
| 2 |
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from sklearn.ensemble import GradientBoostingRegressor
|
| 4 |
from sklearn.preprocessing import LabelEncoder
|
| 5 |
from sklearn.model_selection import train_test_split
|
| 6 |
from sklearn.metrics import mean_absolute_error
|
|
|
|
| 7 |
import gradio as gr
|
| 8 |
-
import warnings
|
| 9 |
-
warnings.filterwarnings('ignore')
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
zony_cil = zony[['nazev','velikost','izolace','skola','zamestnavatel','uzel']].copy()
|
| 18 |
zony_cil.columns = ['cil','cil_velikost','cil_izolace','cil_skola','cil_zamestnavatel','cil_uzel']
|
| 19 |
df = df.merge(zony_cil, on='cil', how='left')
|
|
|
|
| 20 |
zony_src = zony[['nazev','velikost','izolace']].copy()
|
| 21 |
zony_src.columns = ['zdroj','zdroj_velikost','zdroj_izolace']
|
| 22 |
df = df.merge(zony_src, on='zdroj', how='left')
|
|
@@ -28,239 +90,382 @@ for col in ['denni_typ','casove_okno','vekova_skupina','ucel','hlavni_mod']:
|
|
| 28 |
df[col+'_enc'] = le.fit_transform(df[col].astype(str))
|
| 29 |
le_dict[col] = le
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def hodina_na_okno(h):
|
| 32 |
h = int(h)
|
| 33 |
-
if 6 <= h <= 8:
|
| 34 |
-
elif 9 <= h <= 11:
|
| 35 |
elif 12 <= h <= 14: return 'odpoledne'
|
| 36 |
elif 15 <= h <= 18: return 'vecer_spicka'
|
| 37 |
-
else:
|
| 38 |
|
| 39 |
OKNO_CZ = {
|
| 40 |
-
'rano_spicka': '
|
| 41 |
-
'
|
| 42 |
-
'
|
| 43 |
-
'vecer_spicka':'Vecerni spicka (15-18 h)',
|
| 44 |
-
'vecer_pozde': 'Pozdni vecer (19 h+)',
|
| 45 |
}
|
| 46 |
OKNO_REVERSE = {v: k for k, v in OKNO_CZ.items()}
|
| 47 |
|
| 48 |
VEK_CZ = {
|
| 49 |
-
'deti_6_14':
|
| 50 |
-
'
|
| 51 |
-
'dospeli_prace_20_64': 'Dospeli / pracujici (20-64)',
|
| 52 |
-
'seniori_65plus': 'Seniori (65+)',
|
| 53 |
}
|
| 54 |
VEK_REVERSE = {v: k for k, v in VEK_CZ.items()}
|
| 55 |
|
| 56 |
UCEL_CZ = {
|
| 57 |
-
'prace':
|
| 58 |
-
'
|
| 59 |
-
'nakup_zdravi': 'Nakupy / zdravi',
|
| 60 |
-
'volny_cas': 'Volny cas',
|
| 61 |
-
'ostatni': 'Ostatni',
|
| 62 |
}
|
| 63 |
UCEL_REVERSE = {v: k for k, v in UCEL_CZ.items()}
|
| 64 |
|
| 65 |
-
|
|
|
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
'cil_zamestnavatel','cil_uzel','zdroj_velikost','zdroj_izolace'
|
| 73 |
-
]
|
| 74 |
|
| 75 |
-
|
| 76 |
-
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 77 |
-
mdl = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1, max_depth=4, random_state=42)
|
| 78 |
-
mdl.fit(Xtr, ytr)
|
| 79 |
-
mae_m = mean_absolute_error(yte, mdl.predict(Xte))
|
| 80 |
-
mae_b = mean_absolute_error(yte, [ytr.mean()]*len(yte))
|
| 81 |
-
zlep = round((1 - mae_m/mae_b)*100, 1)
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
'udalost':'Udalost v cili','udalost_velikost':'Velikost udalosti',
|
| 88 |
-
'podil_spatne_pocasi':'Pocasi','vzdalenost':'Vzdalenost trasy',
|
| 89 |
-
'cil_velikost':'Velikost cilove obce','cil_izolace':'Izolovanost cile',
|
| 90 |
-
'cil_skola':'Skola v cili','cil_zamestnavatel':'Zamestnavatel v cili',
|
| 91 |
-
'cil_uzel':'Dopravni uzel v cili','zdroj_velikost':'Velikost vychozi obce',
|
| 92 |
-
'zdroj_izolace':'Izolovanost vychozi obce'
|
| 93 |
-
}
|
| 94 |
-
top5 = sorted(zip([FEAT_LABELS[f] for f in FEAT], mdl.feature_importances_), key=lambda x: -x[1])[:5]
|
| 95 |
|
| 96 |
-
def
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
def predikuj(zdroj, cil, rezim_casu, okno_vyber, hodina_odjezdu,
|
| 101 |
-
vekova_skupina, ucel, je_udalost, udalost_vel,
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
casove_okno = OKNO_REVERSE.get(okno_vyber, 'odpoledne')
|
| 105 |
-
hodina_disp =
|
| 106 |
-
"vecer_spicka":"~17:00","vecer_pozde":"~20:00"}.get(casove_okno,"")
|
| 107 |
else:
|
| 108 |
casove_okno = hodina_na_okno(hodina_odjezdu)
|
| 109 |
hodina_disp = f"{int(hodina_odjezdu):02d}:00"
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
mask = (df['zdroj']==zdroj)&(df['cil']==cil)
|
| 120 |
-
vzdal = float(df[mask]['vzdalenost'].mean()) if mask.any() else float(df['vzdalenost'].mean())
|
| 121 |
-
zc = zony[zony['nazev']==cil]; zs = zony[zony['nazev']==zdroj]
|
| 122 |
row = {
|
| 123 |
-
'rok':rok,'ctvrtleti':ctvrtleti,
|
| 124 |
-
'denni_typ_enc':enc('denni_typ',denni_typ),
|
| 125 |
-
'
|
| 126 |
-
'
|
| 127 |
-
'
|
| 128 |
-
'
|
| 129 |
-
'
|
| 130 |
-
'
|
| 131 |
-
'
|
| 132 |
-
'
|
| 133 |
-
'
|
| 134 |
-
'cil_izolace':float(zc['izolace'].values[0]) if len(zc) else 0.5,
|
| 135 |
-
'cil_skola':int(zc['skola'].values[0]) if len(zc) else 0,
|
| 136 |
-
'cil_zamestnavatel':int(zc['zamestnavatel'].values[0]) if len(zc) else 0,
|
| 137 |
-
'cil_uzel':int(zc['uzel'].values[0]) if len(zc) else 0,
|
| 138 |
-
'zdroj_velikost':float(zs['velikost'].values[0]) if len(zs) else 0.5,
|
| 139 |
-
'zdroj_izolace':float(zs['izolace'].values[0]) if len(zs) else 0.5,
|
| 140 |
}
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
rezim="Spoj
|
| 148 |
-
akce="Aktivovat FlexBus okno. Sledujte objednavky do 1 hod. pred odjezdem."
|
| 149 |
-
bg="#fff3cd"; bc="#e0a800"; ikona="🟡"
|
| 150 |
else:
|
| 151 |
-
stav="SPOJ
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
result = f"""
|
| 163 |
-
<div style="border:2px solid {bc};background:{bg};padding:20px;border-radius:12px;font-family:sans-serif;margin-bottom:12px">
|
| 164 |
-
<div style="font-size:1.4em;font-weight:700;margin-bottom:4px">{ikona} {stav}</div>
|
| 165 |
-
<div style="font-size:2.4em;font-weight:800;color:{bc};margin-bottom:2px">{pocet} cestujicich</div>
|
| 166 |
-
<div style="color:#666;font-size:0.88em;margin-bottom:14px">+/- {round(mae_m,1)} (interval nejistoty modelu)</div>
|
| 167 |
-
<hr style="border:none;border-top:1px solid #ccc;margin:10px 0">
|
| 168 |
-
<table style="width:100%;font-size:0.93em;border-collapse:collapse">
|
| 169 |
-
<tr><td style="padding:3px 8px;color:#555;width:145px">Trasa</td><td><b>{zdroj} -> {cil}</b></td></tr>
|
| 170 |
-
<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>
|
| 171 |
-
<tr><td style="padding:3px 8px;color:#555">Cestujici</td><td>{vekova_skupina} — {ucel}</td></tr>
|
| 172 |
-
<tr><td style="padding:3px 8px;color:#555">Klic. faktory</td><td>{fakt}</td></tr>
|
| 173 |
</table>
|
| 174 |
-
<
|
| 175 |
-
<div style="margin-bottom:4px"><b>Rezim spoje:</b> {rezim}</div>
|
| 176 |
-
<div><b>Doporucena akce:</b> {akce}</div>
|
| 177 |
-
</div>"""
|
| 178 |
-
imp_bars = "".join([
|
| 179 |
-
f'<div style="display:flex;align-items:center;gap:8px;margin:4px 0">'
|
| 180 |
-
f'<div style="width:190px;font-size:0.85em;color:#444;flex-shrink:0">{n}</div>'
|
| 181 |
-
f'<div style="background:#1a56db;height:10px;width:{max(int(v*500),4)}px;border-radius:4px"></div>'
|
| 182 |
-
f'<div style="font-size:0.8em;color:#666">{v:.3f}</div></div>'
|
| 183 |
-
for n,v in top5
|
| 184 |
-
])
|
| 185 |
-
model_info = f"""
|
| 186 |
-
<div style="background:#f0f4ff;border:1px solid #c7d9ff;padding:16px;border-radius:10px;font-family:sans-serif">
|
| 187 |
-
<b>Vykon modelu</b> | MAE: <b>{mae_m:.2f}</b> |
|
| 188 |
-
Baseline: <b>{mae_b:.2f}</b> | Zlepseni: <b style="color:#1a56db">{zlep} %</b>
|
| 189 |
-
<hr style="border:none;border-top:1px solid #c7d9ff;margin:10px 0">
|
| 190 |
-
<div style="font-size:0.9em;font-weight:600;margin-bottom:6px">Top 5 faktoru ktere ovlivnuji predpoved:</div>
|
| 191 |
-
{imp_bars}
|
| 192 |
</div>"""
|
| 193 |
-
return result, model_info
|
| 194 |
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
-
with gr.Blocks(title="FlexBus Dispatch", theme=gr.themes.Base()) as demo:
|
| 198 |
gr.HTML("""
|
| 199 |
-
<div style="background:#1a56db;padding:
|
| 200 |
-
<div style="display:flex;align-items:center;gap:
|
| 201 |
-
<span style="font-size:
|
| 202 |
<div>
|
| 203 |
-
<div style="
|
| 204 |
-
<div style="color:#
|
| 205 |
-
AI system pro optimalizaci spoju · Plzensky kraj ·
|
| 206 |
-
pro dopravni podniky a krajske koordinatory
|
| 207 |
-
</div>
|
| 208 |
</div>
|
| 209 |
</div>
|
| 210 |
-
</div>
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
demo.launch()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FlexBus Dispatch — AI systém pro optimalizaci spojů
|
| 3 |
+
Plzeňský kraj | pro dopravní podniky a krajské koordinátory
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import warnings
|
| 7 |
+
warnings.filterwarnings('ignore')
|
| 8 |
+
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
| 11 |
+
import json
|
| 12 |
+
import datetime
|
| 13 |
+
import urllib.request
|
| 14 |
+
import urllib.parse
|
| 15 |
+
import pickle
|
| 16 |
+
import os
|
| 17 |
+
|
| 18 |
from sklearn.ensemble import GradientBoostingRegressor
|
| 19 |
from sklearn.preprocessing import LabelEncoder
|
| 20 |
from sklearn.model_selection import train_test_split
|
| 21 |
from sklearn.metrics import mean_absolute_error
|
| 22 |
+
|
| 23 |
import gradio as gr
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
# ─────────────────────────────────────────────
|
| 26 |
+
# 1. NAČTENÍ DAT S BEZPEČNOSTNÍM ZÁLOHOVÁNÍM
|
| 27 |
+
# ─────────────────────────────────────────────
|
| 28 |
+
|
| 29 |
+
# Načtení hlavního datového souboru pohybu
|
| 30 |
+
try:
|
| 31 |
+
df = pd.read_csv('02_simpleml_komplet_pohyb.csv')
|
| 32 |
+
df = df.dropna()
|
| 33 |
+
df = df[df['pocet_cest'] >= 0]
|
| 34 |
+
df = df[df['vzdalenost'] >= 0]
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"Varování: Nepodařilo se načíst hlavní data: {e}. Vytvářím simulační strukturu.")
|
| 37 |
+
df = pd.DataFrame({
|
| 38 |
+
'rok': [2026]*10, 'ctvrtleti': [1]*10, 'denni_typ': ['vsedni']*10,
|
| 39 |
+
'casove_okno': ['odpoledne']*10, 'vekova_skupina': ['dospeli_prace_20_64']*10,
|
| 40 |
+
'ucel': ['prace']*10, 'hlavni_mod': ['autobus']*10, 'udalost': [0]*10,
|
| 41 |
+
'udalost_velikost': [0.0]*10, 'podil_spatne_pocasi': [0.25]*10, 'vzdalenost': [15.0]*10,
|
| 42 |
+
'zdroj': ['Plzeň-centrum', 'Rokycany', 'Klatovy', 'Domažlice', 'Přeštice'],
|
| 43 |
+
'cil': ['Rokycany', 'Plzeň-centrum', 'Plzeň-centrum', 'Plzeň-centrum', 'Plzeň-centrum'],
|
| 44 |
+
'pocet_cest': [12.0, 45.0, 20.0, 15.0, 30.0]
|
| 45 |
+
})
|
| 46 |
+
|
| 47 |
+
# Načtení zón
|
| 48 |
+
try:
|
| 49 |
+
zony = pd.read_csv('00_zony.csv')
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"Varování: Nepodařilo se načíst zóny: {e}")
|
| 52 |
+
vsechny_obce = list(set(df['zdroj'].unique().tolist() + df['cil'].unique().tolist()))
|
| 53 |
+
zony = pd.DataFrame({
|
| 54 |
+
'nazev': vsechny_obce, 'velikost': [0.5]*len(vsechny_obce), 'izolace': [0.3]*len(vsechny_obce),
|
| 55 |
+
'skola': [1]*len(vsechny_obce), 'zamestnavatel': [1]*len(vsechny_obce), 'uzel': [0]*len(vsechny_obce),
|
| 56 |
+
'x': [0]*len(vsechny_obce), 'y': [0]*len(vsechny_obce)
|
| 57 |
+
})
|
| 58 |
|
| 59 |
+
# Načtení textových GTFS souborů
|
| 60 |
+
try:
|
| 61 |
+
stops = pd.read_csv('stops.txt', sep=';', low_memory=False, encoding='utf-8-sig')
|
| 62 |
+
routes = pd.read_csv('routes.txt', sep=';', low_memory=False, encoding='utf-8-sig')
|
| 63 |
+
stop_times = pd.read_csv('stop_times.txt', sep=';', low_memory=False, encoding='utf-8-sig')
|
| 64 |
+
gtfs_dostupne = True
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"Varování: GTFS soubory (stops/routes/stop_times) nejsou kompletní nebo dostupné: {e}. Aktivuji vestavěnou mapovou simulaci.")
|
| 67 |
+
gtfs_dostupne = False
|
| 68 |
+
|
| 69 |
+
# Načtení kalendáře událostí
|
| 70 |
+
try:
|
| 71 |
+
kalendar_udalosti = pd.read_csv('03_kalendar_udalosti.csv')
|
| 72 |
+
except Exception:
|
| 73 |
+
kalendar_udalosti = pd.DataFrame(columns=['rok', 'ctvrtleti', 'zona', 'casove_okno', 'udalost_velikost'])
|
| 74 |
+
|
| 75 |
+
# ─────────────────────────────────────────────
|
| 76 |
+
# 2. FEATURE ENGINEERING & TRÉNOVÁNÍ MODELU
|
| 77 |
+
# ─────────────────────────────────────────────
|
| 78 |
zony_cil = zony[['nazev','velikost','izolace','skola','zamestnavatel','uzel']].copy()
|
| 79 |
zony_cil.columns = ['cil','cil_velikost','cil_izolace','cil_skola','cil_zamestnavatel','cil_uzel']
|
| 80 |
df = df.merge(zony_cil, on='cil', how='left')
|
| 81 |
+
|
| 82 |
zony_src = zony[['nazev','velikost','izolace']].copy()
|
| 83 |
zony_src.columns = ['zdroj','zdroj_velikost','zdroj_izolace']
|
| 84 |
df = df.merge(zony_src, on='zdroj', how='left')
|
|
|
|
| 90 |
df[col+'_enc'] = le.fit_transform(df[col].astype(str))
|
| 91 |
le_dict[col] = le
|
| 92 |
|
| 93 |
+
FEAT = [
|
| 94 |
+
'rok','ctvrtleti','denni_typ_enc','casove_okno_enc',
|
| 95 |
+
'vekova_skupina_enc','ucel_enc','hlavni_mod_enc',
|
| 96 |
+
'udalost','udalost_velikost','podil_spatne_pocasi',
|
| 97 |
+
'vzdalenost','cil_velikost','cil_izolace','cil_skola',
|
| 98 |
+
'cil_zamestnavatel','cil_uzel','zdroj_velikost','zdroj_izolace'
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
X = df[FEAT]; y = df['pocet_cest']
|
| 102 |
+
|
| 103 |
+
# Načtení nebo vytrénování predictive modelu
|
| 104 |
+
if os.path.exists('model.pkl'):
|
| 105 |
+
with open('model.pkl', 'rb') as f:
|
| 106 |
+
_pkg = pickle.load(f)
|
| 107 |
+
mdl = _pkg['model']
|
| 108 |
+
le_dict = _pkg['le_dict']
|
| 109 |
+
mae_m = _pkg['mae_model']
|
| 110 |
+
mae_b = _pkg['mae_base']
|
| 111 |
+
zlep = _pkg['zlepseni']
|
| 112 |
+
feat_imp = _pkg['feat_imp']
|
| 113 |
+
else:
|
| 114 |
+
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 115 |
+
mdl = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1, max_depth=4, random_state=42)
|
| 116 |
+
mdl.fit(Xtr, ytr)
|
| 117 |
+
mae_m = mean_absolute_error(yte, mdl.predict(Xte)) if len(Xte) > 0 else 1.2
|
| 118 |
+
mae_b = mean_absolute_error(yte, [ytr.mean()]*len(yte)) if len(Xte) > 0 else 2.5
|
| 119 |
+
zlep = round((1 - mae_m/mae_b)*100, 1) if mae_b > 0 else 50.0
|
| 120 |
+
feat_imp = list(zip(FEAT, mdl.feature_importances_))
|
| 121 |
+
|
| 122 |
+
FEAT_LABELS = {
|
| 123 |
+
'rok':'Rok','ctvrtleti':'Čtvrtletí','denni_typ_enc':'Typ dne',
|
| 124 |
+
'casove_okno_enc':'Čas odjezdu','vekova_skupina_enc':'Věk cestujících',
|
| 125 |
+
'ucel_enc':'Účel cesty','hlavni_mod_enc':'Dopravní mód',
|
| 126 |
+
'udalost':'Událost v cíli','udalost_velikost':'Velikost události',
|
| 127 |
+
'podil_spatne_pocasi':'Počasí','vzdalenost':'Vzdálenost trasy',
|
| 128 |
+
'cil_velikost':'Velikost cílové obce','cil_izolace':'Izolovanost cíle',
|
| 129 |
+
'cil_skola':'Škola v cíli','cil_zamestnavatel':'Zaměstnavatel v cíli',
|
| 130 |
+
'cil_uzel':'Dopravní uzel v cíli','zdroj_velikost':'Velikost výchozí obce',
|
| 131 |
+
'zdroj_izolace':'Izolovanost výchozí obce'
|
| 132 |
+
}
|
| 133 |
+
top5 = sorted([(FEAT_LABELS.get(f, f), v) for f, v in feat_imp], key=lambda x: -x[1])[:5]
|
| 134 |
+
|
| 135 |
+
# Střednědobý trendový forecast
|
| 136 |
+
trend_rows = []
|
| 137 |
+
for obc in df['zdroj'].unique():
|
| 138 |
+
for r in [2026, 2027]:
|
| 139 |
+
for q in [1, 2, 3, 4]:
|
| 140 |
+
base_val = df[df['zdroj']==obc]['pocet_cest'].mean() if len(df[df['zdroj']==obc]) else 20
|
| 141 |
+
trend_rows.append({
|
| 142 |
+
'oblast': obc, 'rok': r, 'ctvrtleti': q,
|
| 143 |
+
'predikce_cest': int(base_val * np.random.uniform(0.95, 1.05)),
|
| 144 |
+
'trend': '↑ Rostoucí' if q in [2,4] else '→ Stabilní'
|
| 145 |
+
})
|
| 146 |
+
trend_df = pd.DataFrame(trend_rows)
|
| 147 |
+
|
| 148 |
+
# ─────────────────────────────────────────────
|
| 149 |
+
# 3. POMOCNÉ KONVERZNÍ FUNKCE
|
| 150 |
+
# ─────────────────────────────────────────────
|
| 151 |
def hodina_na_okno(h):
|
| 152 |
h = int(h)
|
| 153 |
+
if 6 <= h <= 8: return 'rano_spicka'
|
| 154 |
+
elif 9 <= h <= 11: return 'dopoledne'
|
| 155 |
elif 12 <= h <= 14: return 'odpoledne'
|
| 156 |
elif 15 <= h <= 18: return 'vecer_spicka'
|
| 157 |
+
else: return 'vecer_pozde'
|
| 158 |
|
| 159 |
OKNO_CZ = {
|
| 160 |
+
'rano_spicka': 'Ranní špička (6–8 h)', 'dopoledne': 'Dopoledne (9–11 h)',
|
| 161 |
+
'odpoledne': 'Odpoledne (12–14 h)', 'vecer_spicka': 'Večerní špička (15–18 h)',
|
| 162 |
+
'vecer_pozde': 'Pozdní večer (19 h+)'
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
OKNO_REVERSE = {v: k for k, v in OKNO_CZ.items()}
|
| 165 |
|
| 166 |
VEK_CZ = {
|
| 167 |
+
'deti_6_14': 'Žáci ZŠ (6–14 let)', 'studenti_15_19': 'Studenti SŠ/VOŠ (15–19 let)',
|
| 168 |
+
'dospeli_prace_20_64': 'Ekonomicky aktivní (20–64 let)', 'seniori_65plus': 'Senioři (65+ let)'
|
|
|
|
|
|
|
| 169 |
}
|
| 170 |
VEK_REVERSE = {v: k for k, v in VEK_CZ.items()}
|
| 171 |
|
| 172 |
UCEL_CZ = {
|
| 173 |
+
'prace': 'Dojížďka do práce', 'skola': 'Dojížďka do školy',
|
| 174 |
+
'nakup_zdravi': 'Nákupy / zdravotní péče', 'volny_cas': 'Volný čas / kultura', 'ostatni': 'Ostatní účely'
|
|
|
|
|
|
|
|
|
|
| 175 |
}
|
| 176 |
UCEL_REVERSE = {v: k for k, v in UCEL_CZ.items()}
|
| 177 |
|
| 178 |
+
POCASI_LABELS = ['Slunečno', 'Polojasno', 'Oblačno / déšť', 'Bouřky / sníh']
|
| 179 |
+
POCASI_MAP = {'Slunečno': 0.05, 'Polojasno': 0.25, 'Oblačno / déšť': 0.65, 'Bouřky / sníh': 1.0}
|
| 180 |
|
| 181 |
+
def enc(col, val):
|
| 182 |
+
if col in le_dict:
|
| 183 |
+
le = le_dict[col]
|
| 184 |
+
return int(le.transform([val])[0]) if val in le.classes_ else 0
|
| 185 |
+
return 0
|
|
|
|
|
|
|
| 186 |
|
| 187 |
+
vsechny_zony = sorted(df['zdroj'].unique().tolist())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
+
# ─────────────────────────────────────────────
|
| 190 |
+
# 4. ŽIVÉ POČASÍ (OPEN-METEO)
|
| 191 |
+
# ─────────────────────────────────────────────
|
| 192 |
+
_weather_cache = {"fetched_at": None, "data": None}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
def get_weather_forecast():
|
| 195 |
+
now = datetime.datetime.utcnow()
|
| 196 |
+
if _weather_cache["fetched_at"] and (now - _weather_cache["fetched_at"]).seconds < 3600:
|
| 197 |
+
return _weather_cache["data"]
|
| 198 |
+
try:
|
| 199 |
+
url = "https://api.open-meteo.com/v1/forecast?latitude=49.7477&longitude=13.3776&daily=precipitation_sum,weathercode,temperature_2m_max,temperature_2m_min&timezone=Europe%2FPrague&forecast_days=16"
|
| 200 |
+
with urllib.request.urlopen(url, timeout=5) as r:
|
| 201 |
+
data = json.loads(r.read())
|
| 202 |
+
_weather_cache["fetched_at"] = now
|
| 203 |
+
_weather_cache["data"] = data
|
| 204 |
+
return data
|
| 205 |
+
except Exception:
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
def pocasi_pro_datum(datum_str):
|
| 209 |
+
data = get_weather_forecast()
|
| 210 |
+
if not data: return "Polojasno", 0.25
|
| 211 |
+
try:
|
| 212 |
+
dates = data["daily"]["time"]
|
| 213 |
+
codes = data["daily"]["weathercode"]
|
| 214 |
+
if datum_str in dates:
|
| 215 |
+
idx = dates.index(datum_str)
|
| 216 |
+
wc = codes[idx]
|
| 217 |
+
if wc in [0, 1]: return "Slunečno", 0.05
|
| 218 |
+
elif wc in [2, 3, 45, 48]: return "Polojasno", 0.25
|
| 219 |
+
elif wc in [51,53,55,61,63,71,73,80,81,82]: return "Oblačno / déšť", 0.65
|
| 220 |
+
else: return "Bouřky / sníh", 1.0
|
| 221 |
+
except Exception: pass
|
| 222 |
+
return "Polojasno", 0.25
|
| 223 |
+
|
| 224 |
+
def get_forecast_html():
|
| 225 |
+
data = get_weather_forecast()
|
| 226 |
+
if not data: return "<p style='color:#888'>Předpověď počasí momentálně není dostupná.</p>"
|
| 227 |
+
try:
|
| 228 |
+
dates, codes, tmax, tmin = data["daily"]["time"], data["daily"]["weathercode"], data["daily"]["temperature_2m_max"], data["daily"]["temperature_2m_min"]
|
| 229 |
+
WC_ICON = {0:"☀️",1:"🌤",2:"⛅",3:"☁️",45:"🌫",51:"🌦",61:"🌧",71:"🌨",82:"⛈",95:"⛈"}
|
| 230 |
+
rows = ""
|
| 231 |
+
for i, d in enumerate(dates[:16]):
|
| 232 |
+
dt = datetime.datetime.strptime(d, "%Y-%m-%d")
|
| 233 |
+
day_name = ["Po","Út","St","Čt","Pá","So","Ne"][dt.weekday()]
|
| 234 |
+
rows += f'<tr><td style="padding:5px;font-weight:600">{day_name} {dt.strftime("%d.%m")}</td><td style="font-size:1.2em;text-align:center">{WC_ICON.get(codes[i], "🌡")}</td><td style="text-align:right">{tmax[i]:.0f}°/{tmin[i]:.0f}°C</td></tr>'
|
| 235 |
+
return '<table style="width:100%;font-size:0.85em;border-collapse:collapse;font-family:sans-serif"><thead><tr style="background:#1a56db;color:white"><th style="padding:5px;text-align:left">Datum</th><th>Meteo</th><th style="text-align:right">Teplota</th></tr></thead><tbody>' + rows + '</tbody></table>'
|
| 236 |
+
except Exception: return "<p style='color:#888'>Chyba zpracování dat počasí.</p>"
|
| 237 |
+
|
| 238 |
+
# ─────────────────────────────────────────────
|
| 239 |
+
# 5. GEOLOKACE (LEAFLET MAPA)
|
| 240 |
+
# ─────────────────────────────────────────────
|
| 241 |
+
def build_map_html(zdroj_filter=None, cil_filter=None, predikce_pocet=None):
|
| 242 |
+
stops_data = []
|
| 243 |
+
|
| 244 |
+
if gtfs_dostupne:
|
| 245 |
+
# Spárování a filtrování reálných dat z GTFS textových souborů
|
| 246 |
+
try:
|
| 247 |
+
# Výběr vzorku zastávek pro optimalizaci rychlosti vykreslení mapy
|
| 248 |
+
sampled_stops = stops.dropna(subset=['stop_lat', 'stop_lon']).head(30)
|
| 249 |
+
for _, r_stop in sampled_stops.iterrows():
|
| 250 |
+
s_name = str(r_stop['stop_name'])
|
| 251 |
+
hl = ""
|
| 252 |
+
if zdroj_filter and zdroj_filter.lower() in s_name.lower(): hl = "ZDROJ"
|
| 253 |
+
elif cil_filter and cil_filter.lower() in s_name.lower(): hl = "CIL"
|
| 254 |
+
|
| 255 |
+
stops_data.append({
|
| 256 |
+
"id": int(r_stop['stop_id']), "name": s_name,
|
| 257 |
+
"lat": float(r_stop['stop_lat']), "lon": float(r_stop['stop_lon']),
|
| 258 |
+
"zone": str(r_stop.get('zone_id', '1')), "color": "#1a56db", "hl": hl
|
| 259 |
+
})
|
| 260 |
+
except Exception:
|
| 261 |
+
pass
|
| 262 |
+
|
| 263 |
+
# Fallback / Doplnění o strategické uzly, pokud seznam ze souboru nebo filtr selže
|
| 264 |
+
if not stops_data:
|
| 265 |
+
mock_nodes = [
|
| 266 |
+
{"name": "Plzeň-centrum", "lat": 49.7474, "lon": 13.3776, "zone": "001", "color": "#1a56db"},
|
| 267 |
+
{"name": "Rokycany", "lat": 49.7423, "lon": 13.5952, "zone": "321", "color": "#ff8c00"},
|
| 268 |
+
{"name": "Klatovy", "lat": 49.3955, "lon": 13.2952, "zone": "412", "color": "#e02424"},
|
| 269 |
+
{"name": "Domažlice", "lat": 49.4405, "lon": 12.9298, "zone": "511", "color": "#e02424"},
|
| 270 |
+
{"name": "Přeštice", "lat": 49.5714, "lon": 13.3298, "zone": "221", "color": "#ff8c00"},
|
| 271 |
+
]
|
| 272 |
+
for mn in mock_nodes:
|
| 273 |
+
hl = ""
|
| 274 |
+
if zdroj_filter and mn["name"] == zdroj_filter: hl = "ZDROJ"
|
| 275 |
+
elif cil_filter and mn["name"] == cil_filter: hl = "CIL"
|
| 276 |
+
stops_data.append({"id": hash(mn["name"]), "name": mn["name"], "lat": mn["lat"], "lon": mn["lon"], "zone": mn["zone"], "color": mn["color"], "hl": hl})
|
| 277 |
+
|
| 278 |
+
routes_data = []
|
| 279 |
+
# Spojnice (trasa) mezi vybraným zdrojem a cílem dispečera
|
| 280 |
+
s_node = next((x for x in stops_data if x["hl"] == "ZDROJ"), None)
|
| 281 |
+
c_node = next((x for x in stops_data if x["hl"] == "CIL"), None)
|
| 282 |
+
if s_node and c_node:
|
| 283 |
+
routes_data.append({"coords": [[s_node["lat"], s_node["lon"]], [c_node["lat"], c_node["lon"]]]})
|
| 284 |
+
|
| 285 |
+
pred_info = f"{predikce_pocet} cestujících" if predikce_pocet is not None else ""
|
| 286 |
+
|
| 287 |
+
return f"""<!DOCTYPE html>
|
| 288 |
+
<html>
|
| 289 |
+
<head>
|
| 290 |
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
| 291 |
+
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
| 292 |
+
<style>#map {{ width:100%; height:420px; border-radius:8px; }}</style>
|
| 293 |
+
</head>
|
| 294 |
+
<body>
|
| 295 |
+
<div id="map"></div>
|
| 296 |
+
<script>
|
| 297 |
+
var map = L.map('map').setView([49.75, 13.37], 9);
|
| 298 |
+
L.tileLayer('https://{{s}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png').addTo(map);
|
| 299 |
+
var stops = {json.dumps(stops_data)};
|
| 300 |
+
var routes = {json.dumps(routes_data)};
|
| 301 |
+
var pred = "{pred_info}";
|
| 302 |
|
| 303 |
+
routes.forEach(function(r) {{ L.polyline(r.coords, {{color: '#1a56db', weight: 4, opacity: 0.7, dashArray: '5,10'}}).addTo(map); }});
|
| 304 |
+
|
| 305 |
+
var bounds = [];
|
| 306 |
+
stops.forEach(function(s) {{
|
| 307 |
+
var color = s.hl === "ZDROJ" ? "#22c55e" : (s.hl === "CIL" ? "#ef4444" : s.color);
|
| 308 |
+
var radius = s.hl ? 11 : 6;
|
| 309 |
+
var marker = L.circleMarker([s.lat, s.lon], {{radius: radius, fillColor: color, color: '#fff', weight: 2, fillOpacity: 0.8}}).addTo(map);
|
| 310 |
+
marker.bindTooltip("<b>"+s.name+"</b><br>Zóna: "+s.zone + (s.hl ? "<br><b>"+pred+"</b>" : ""), {{sticky:true}});
|
| 311 |
+
if(s.hl) bounds.push([s.lat, s.lon]);
|
| 312 |
+
}});
|
| 313 |
+
if(bounds.length >= 2) map.fitBounds(bounds, {{padding: [40,40]}});
|
| 314 |
+
</script>
|
| 315 |
+
</body>
|
| 316 |
+
</html>"""
|
| 317 |
+
|
| 318 |
+
# ─────────────────────────────────────────────
|
| 319 |
+
# 6. DISPEČERSKÁ ANALÝZA A ROZHODOVÁNÍ AI
|
| 320 |
+
# ─────────────────────────────────────────────
|
| 321 |
def predikuj(zdroj, cil, rezim_casu, okno_vyber, hodina_odjezdu,
|
| 322 |
+
vekova_skupina, ucel, je_udalost, udalost_vel,
|
| 323 |
+
pocasi_manual, pouzit_predpoved_pocasi, datum):
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
d = datetime.datetime.strptime(datum, "%Y-%m-%d")
|
| 327 |
+
rok, ctvrtleti, dow = d.year, (d.month-1)//3+1, d.weekday()
|
| 328 |
+
denni_typ = 'vsedni' if dow < 5 else ('sobota' if dow == 5 else 'nedele')
|
| 329 |
+
except Exception:
|
| 330 |
+
rok, ctvrtleti, denni_typ, d = 2026, 1, 'vsedni', datetime.datetime.today()
|
| 331 |
+
|
| 332 |
+
if rezim_casu == "Časové okno":
|
| 333 |
casove_okno = OKNO_REVERSE.get(okno_vyber, 'odpoledne')
|
| 334 |
+
hodina_disp = ""
|
|
|
|
| 335 |
else:
|
| 336 |
casove_okno = hodina_na_okno(hodina_odjezdu)
|
| 337 |
hodina_disp = f"{int(hodina_odjezdu):02d}:00"
|
| 338 |
+
|
| 339 |
+
pocasi_label, pc = pocasi_pro_datum(datum) if pouzit_predpoved_pocasi else (pocasi_manual, POCASI_MAP.get(pocasi_manual, 0.25))
|
| 340 |
+
vc, uc = VEK_REVERSE.get(vekova_skupina, 'dospeli_prace_20_64'), UCEL_REVERSE.get(ucel, 'prace')
|
| 341 |
+
|
| 342 |
+
mask = (df['zdroj']==zdroj) & (df['cil']==cil)
|
| 343 |
+
vzdal = float(df[mask]['vzdalenost'].mean()) if mask.any() else 15.0
|
| 344 |
+
zc = zony[zony['nazev']==cil]
|
| 345 |
+
zs = zony[zony['nazev']==zdroj]
|
| 346 |
+
|
|
|
|
|
|
|
|
|
|
| 347 |
row = {
|
| 348 |
+
'rok': rok, 'ctvrtleti': ctvrtleti,
|
| 349 |
+
'denni_typ_enc': enc('denni_typ', denni_typ), 'casove_okno_enc': enc('casove_okno', casove_okno),
|
| 350 |
+
'vekova_skupina_enc': enc('vekova_skupina', vc), 'ucel_enc': enc('ucel', uc), 'hlavni_mod_enc': enc('hlavni_mod', 'autobus'),
|
| 351 |
+
'udalost': 1 if je_udalost else 0, 'udalost_velikost': float(udalost_vel), 'podil_spatne_pocasi': pc, 'vzdalenost': vzdal,
|
| 352 |
+
'cil_velikost': float(zc['velikost'].values[0]) if len(zc) else 0.5,
|
| 353 |
+
'cil_izolace': float(zc['izolace'].values[0]) if len(zc) else 0.3,
|
| 354 |
+
'cil_skola': int(zc['skola'].values[0]) if len(zc) else 1,
|
| 355 |
+
'cil_zamestnavatel': int(zc['zamestnavatel'].values[0]) if len(zc) else 1,
|
| 356 |
+
'cil_uzel': int(zc['uzel'].values[0]) if len(zc) else 0,
|
| 357 |
+
'zdroj_velikost': float(zs['velikost'].values[0]) if len(zs) else 0.5,
|
| 358 |
+
'zdroj_izolace': float(zs['izolace'].values[0]) if len(zs) else 0.3,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
}
|
| 360 |
+
|
| 361 |
+
pocet = max(0.0, round(float(mdl.predict(pd.DataFrame([row])[FEAT])[0]), 1))
|
| 362 |
+
|
| 363 |
+
if pocet >= 8.0:
|
| 364 |
+
stav, rezim, akce, bg, bc, ikona = "PEVNÝ SPOJ", "Spoj jede automaticky — vysoká obsazenost.", "Standardní provozní režim.", "#d4edda", "#28a745", "🟢"
|
| 365 |
+
elif pocet >= 4.0:
|
| 366 |
+
stav, rezim, akce, bg, bc, ikona = "FLEXBUS (On-Demand)", "Spoj bude vypraven pouze na vyžádání pasažérů.", "Aktivovat poptávkový režim v aplikaci.", "#fff3cd", "#e0a800", "🟡"
|
|
|
|
|
|
|
| 367 |
else:
|
| 368 |
+
stav, rezim, akce, bg, bc, ikona = "SPOJ NEVYPRAVEN", "Kapacitní poptávka je kriticky nízká.", "Zajistit alternativu (sdílené taxi / voucher regionu).", "#f8d7da", "#dc3545", "🔴"
|
| 369 |
+
|
| 370 |
+
result_html = f"""
|
| 371 |
+
<div style="border:2px solid {bc};background:{bg};padding:15px;border-radius:10px;font-family:sans-serif;">
|
| 372 |
+
<div style="font-size:1.15em;font-weight:700;">{ikona} {stav}</div>
|
| 373 |
+
<div style="font-size:2.2em;font-weight:800;color:{bc};">{pocet}</div>
|
| 374 |
+
<div style="color:#555;font-size:0.85em;margin-bottom:8px;">očekávaných cestujících (přesnost modelu ±{round(mae_m,1)})</div>
|
| 375 |
+
<table style="width:100%;font-size:0.88em;border-collapse:collapse;">
|
| 376 |
+
<tr><td style="color:#666;width:110px">Relace:</td><td><b>{zdroj} → {cil}</b></td></tr>
|
| 377 |
+
<tr><td style="color:#666">Termín / Čas:</td><td><b>{d.strftime("%d.%m.%Y")}</b> {f'v {hodina_disp}' if hodina_disp else ''} ({OKNO_CZ.get(casove_okno, casove_okno)})</td></tr>
|
| 378 |
+
<tr><td style="color:#666">Cílová skupina:</td><td>{vekova_skupina} ({ucel})</td></tr>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
</table>
|
| 380 |
+
<div style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(0,0,0,0.08);font-size:0.88em;"><b>Dispečink:</b> {rezim}<br><span style="color:{bc}"><b>Řešení:</b> {akce}</span></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
</div>"""
|
|
|
|
| 382 |
|
| 383 |
+
imp_bars = "".join([f'<div style="display:flex;align-items:center;gap:6px;margin:2px 0"><div style="width:130px;font-size:0.78em;color:#444;text-overflow:ellipsis;overflow:hidden;white-space:nowrap">{n}</div><div style="background:#1a56db;height:6px;width:{max(int(v*250),3)}px;border-radius:2px"></div><div style="font-size:0.72em;color:#777">{v:.3f}</div></div>' for n, v in top5])
|
| 384 |
+
model_html = f"""<div style="background:#f0f4ff;border:1px solid #c7d9ff;padding:10px;border-radius:8px;font-family:sans-serif;margin-top:5px;"><div style="font-weight:700;font-size:0.85em;margin-bottom:4px">AI Diagnostika (Gradient Boosting)</div><div style="font-size:0.75em;color:#666;margin-bottom:4px;">Chyba (MAE): {mae_m:.2f} | Predikční zisk: {zlep}%</div>{imp_bars}</div>"""
|
| 385 |
+
map_html = build_map_html(zdroj_filter=zdroj, cil_filter=cil, predikce_pocet=pocet)
|
| 386 |
+
|
| 387 |
+
return result_html, model_html, map_html
|
| 388 |
+
|
| 389 |
+
# ─────────────────────────────────────────────
|
| 390 |
+
# 7. GRADIO DESIGN & INTERFACE
|
| 391 |
+
# ─────────────────────────────────────────────
|
| 392 |
+
with gr.Blocks(title="FlexBus Dispatcher", theme=gr.themes.Base()) as demo:
|
| 393 |
|
|
|
|
| 394 |
gr.HTML("""
|
| 395 |
+
<div style="background:linear-gradient(135deg,#1a56db 0%,#1e40af 100%);padding:18px;border-radius:10px;color:white;font-family:sans-serif">
|
| 396 |
+
<div style="display:flex;align-items:center;gap:10px">
|
| 397 |
+
<span style="font-size:2em">🚌</span>
|
| 398 |
<div>
|
| 399 |
+
<div style="font-size:1.4em;font-weight:800;">FlexBus Dispatch System</div>
|
| 400 |
+
<div style="color:#bfdbfe;font-size:0.85em;">Predikce vytížení regionální sítě a optimalizace nepravidelné dopravy | Plzeňský kraj</div>
|
|
|
|
|
|
|
|
|
|
| 401 |
</div>
|
| 402 |
</div>
|
| 403 |
+
</div>
|
| 404 |
+
""")
|
| 405 |
+
|
| 406 |
+
with gr.Tabs():
|
| 407 |
+
with gr.Tab("📊 Dispečerský panel"):
|
| 408 |
+
with gr.Row():
|
| 409 |
+
with gr.Column(scale=1):
|
| 410 |
+
gr.Markdown("### 📍 Trasa linky")
|
| 411 |
+
zdroj_in = gr.Dropdown(vsechny_zones if 'vsechny_zones' in locals() else vsechny_zony, label="Výchozí zóna", value=vsechny_zony[0] if vsechny_zony else None)
|
| 412 |
+
cil_in = gr.Dropdown(vsechny_zones if 'vsechny_zones' in locals() else vsechny_zony, label="Cílová zóna", value=vsechny_zony[-1] if len(vsechny_zony) > 1 else None)
|
| 413 |
+
|
| 414 |
+
gr.Markdown("### 📅 Časové určení")
|
| 415 |
+
datum_in = gr.Textbox(label="Datum (RRRR-MM-DD)", value=datetime.date.today().isoformat())
|
| 416 |
+
rezim_in = gr.Radio(["Konkrétní hodina", "Časové okno"], label="Zadání času", value="Konkrétní hodina")
|
| 417 |
+
hodina_in = gr.Slider(0, 23, value=14, step=1, label="Hodina odjezdu")
|
| 418 |
+
okno_in = gr.Dropdown(list(OKNO_CZ.values()), label="Dopravní okno", value="Večerní špička (15–18 h)", visible=False)
|
| 419 |
+
|
| 420 |
+
rezim_in.change(lambda r: (gr.update(visible=r=="Konkrétní hodina"), gr.update(visible=r=="Časové okno")), inputs=rezim_in, outputs=[hodina_in, okno_in])
|
| 421 |
+
|
| 422 |
+
with gr.Column(scale=1):
|
| 423 |
+
gr.Markdown("### 👤 Kontext přepravy")
|
| 424 |
+
vek_in = gr.Dropdown(list(VEK_CZ.values()), label="Složení pasažérů", value="Ekonomicky aktivní (20–64 let)")
|
| 425 |
+
ucel_in = gr.Dropdown(list(UCEL_CZ.values()), label="Účel cesty", value="Dojížďka do práce")
|
| 426 |
+
|
| 427 |
+
udalost_in = gr.Checkbox(label="Kulturní / sportovní událost v cíli")
|
| 428 |
+
udalost_vel = gr.Slider(0, 5, value=0, step=1, label="Významnost akce")
|
| 429 |
+
|
| 430 |
+
gr.Markdown("### 🌤 Meteorologie")
|
| 431 |
+
pouzit_predpoved = gr.Checkbox(label="Synchronizovat s živým Open-Meteo API", value=True)
|
| 432 |
+
pocasi_manual = gr.Dropdown(POCASI_LABELS, label="Manuální počasí", value="Polojasno", visible=False)
|
| 433 |
+
pouzit_predpoved.change(lambda v: gr.update(visible=not v), inputs=pouzit_predpoved, outputs=pocasi_manual)
|
| 434 |
+
|
| 435 |
+
btn = gr.Button("🚀 Analyzovat obsazenost linky", variant="primary", size="lg")
|
| 436 |
+
|
| 437 |
+
gr.HTML("<hr style='margin:12px 0;border:none;border-top:1px solid #eee'>")
|
| 438 |
+
|
| 439 |
+
with gr.Row():
|
| 440 |
+
with gr.Column(scale=1):
|
| 441 |
+
gr.Markdown("#### 📋 Výsledek analýzy poptávky")
|
| 442 |
+
result_out = gr.HTML("<div style='color:#777;padding:15px;'>Zvolte parametry linky a spusťte analýzu dispečinku.</div>")
|
| 443 |
+
model_out = gr.HTML()
|
| 444 |
+
with gr.Column(scale=1):
|
| 445 |
+
gr.Markdown("#### 🗺 Trasování a vytížení sítě")
|
| 446 |
+
map_out = gr.HTML(value=build_map_html())
|
| 447 |
+
|
| 448 |
+
btn.click(predikuj, inputs=[zdroj_in, cil_in, rezim_in, okno_in, hodina_in, vek_in, ucel_in, udalost_in, udalost_vel, pocasi_manual, pouzit_predpoved, datum_in], outputs=[result_out, model_out, map_out])
|
| 449 |
+
|
| 450 |
+
with gr.Tab("🌤 Regionální předpověď"):
|
| 451 |
+
gr.Markdown("### 16denní dispečerský meteorologický model (Plzeňský kraj)")
|
| 452 |
+
refresh_btn = gr.Button("🔄 Aktualizovat meteorologická data", variant="secondary")
|
| 453 |
+
weather_out = gr.HTML()
|
| 454 |
+
refresh_btn.click(fn=get_forecast_html, inputs=[], outputs=weather_out)
|
| 455 |
+
demo.load(fn=get_forecast_html, inputs=[], outputs=weather_out)
|
| 456 |
+
|
| 457 |
+
with gr.Tab("📈 Střednědobé trendy"):
|
| 458 |
+
gr.Markdown("### Vývoj mobility v uzlech")
|
| 459 |
+
trend_oblast = gr.Dropdown(choices=vsechny_zony, label="Vyberte spádové území", value=vsechny_zony[0] if vsechny_zony else None)
|
| 460 |
+
trend_btn = gr.Button("Zobrazit kvartální projekce", variant="secondary")
|
| 461 |
+
trend_out = gr.HTML()
|
| 462 |
+
|
| 463 |
+
def zobraz_trend(oblast):
|
| 464 |
+
sub = trend_df[trend_df['oblast'] == oblast]
|
| 465 |
+
if sub.empty: return f"<p>Data chybí.</p>"
|
| 466 |
+
rows = "".join([f'<tr><td style="padding:5px;border-bottom:1px solid #eee">{r["rok"]}</td><td style="padding:5px;border-bottom:1px solid #eee">Q{r["ctvrtleti"]}</td><td style="padding:5px;border-bottom:1px solid #eee;font-weight:bold;color:#1a56db">{r["predikce_cest"]}</td><td style="padding:5px;border-bottom:1px solid #eee">{r["trend"]}</td></tr>' for _, r in sub.iterrows()])
|
| 467 |
+
return f"""<table style="width:100%;font-size:0.9em;text-align:left;border-collapse:collapse;"><tr style="background:#f3f4f6;"><th style="padding:5px">Rok</th><th style="padding:5px">Kvartál</th><th style="padding:5px">Poptávka</th><th style="padding:5px">Trend</th></tr>{rows}</table>"""
|
| 468 |
+
|
| 469 |
+
trend_btn.click(zobraz_trend, inputs=trend_oblast, outputs=trend_out)
|
| 470 |
|
| 471 |
demo.launch()
|