Spaces:
Sleeping
Sleeping
| """ | |
| FlexBus — model_train.py | |
| ======================== | |
| Spusť JEDNOU před nasazením, nebo při aktualizaci dat. | |
| Co dělá: | |
| 1. Natrénuje GradientBoosting model na historických datech (2019–2025) | |
| 2. Uloží model + encodery jako model.pkl → app.py ho načte při startu | |
| 3. Natrénuje lineární trend model pro každou oblast → předpovídá vývoj | |
| poptávky do budoucna (po čtvrtletích) | |
| 4. Uloží trend předpovědi jako trend_forecast.csv | |
| 5. Vypíše přehled výkonu + feature importance | |
| Spuštění: | |
| python model_train.py | |
| Výstupní soubory (nahraj spolu s app.py na HuggingFace): | |
| model.pkl — natrénovaný GBR model + encodery | |
| trend_forecast.csv — čtvrtletní předpovědi 2026–2027 pro každou oblast | |
| """ | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| import pandas as pd | |
| import numpy as np | |
| import pickle | |
| import datetime | |
| from sklearn.ensemble import GradientBoostingRegressor | |
| from sklearn.linear_model import LinearRegression | |
| from sklearn.preprocessing import LabelEncoder | |
| from sklearn.model_selection import train_test_split, cross_val_score | |
| from sklearn.metrics import mean_absolute_error, r2_score | |
| # ───────────────────────────────────────────────────────────── | |
| # 1. NAČTENÍ A ČIŠTĚNÍ DAT | |
| # ───────────────────────────────────────────────────────────── | |
| print("=" * 60) | |
| print("FlexBus — trénování modelu") | |
| print("=" * 60) | |
| df = pd.read_csv('02_simpleml_komplet_pohyb.csv') | |
| zony = pd.read_csv('00_zony.csv') | |
| print(f"\n📂 Načteno {len(df)} záznamů, {df['rok'].min()}–{df['rok'].max()}") | |
| df = df.dropna() | |
| df = df[df['pocet_cest'] >= 0] | |
| df = df[df['vzdalenost'] >= 0] | |
| print(f" Po čištění: {len(df)} záznamů") | |
| # ───────────────────────────────────────────────────────────── | |
| # 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)) | |
| # Label encoding | |
| 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'] | |
| # ───────────────────────────────────────────────────────────── | |
| # 3. TRÉNINK HLAVNÍHO MODELU | |
| # ───────────────────────────────────────────────────────────── | |
| print("\n🔧 Trénuji GradientBoosting model...") | |
| X_train, X_test, y_train, y_test = train_test_split( | |
| X, y, test_size=0.2, random_state=42 | |
| ) | |
| model = GradientBoostingRegressor( | |
| n_estimators=200, | |
| learning_rate=0.1, | |
| max_depth=4, | |
| min_samples_leaf=3, | |
| subsample=0.85, | |
| random_state=42 | |
| ) | |
| model.fit(X_train, y_train) | |
| y_pred = model.predict(X_test) | |
| mae_model = mean_absolute_error(y_test, y_pred) | |
| mae_base = mean_absolute_error(y_test, [y_train.mean()] * len(y_test)) | |
| r2 = r2_score(y_test, y_pred) | |
| zlepseni = round((1 - mae_model / mae_base) * 100, 1) | |
| print(f"\n📊 Výsledky modelu:") | |
| print(f" MAE (model): {mae_model:.2f} cestujících") | |
| print(f" MAE (baseline): {mae_base:.2f} cestujících") | |
| print(f" Zlepšení: {zlepseni} %") | |
| print(f" R² skóre: {r2:.3f}") | |
| # Cross-validace (5-fold) — pro porotu | |
| cv_scores = cross_val_score(model, X, y, cv=5, scoring='neg_mean_absolute_error') | |
| cv_mae = -cv_scores.mean() | |
| print(f" Cross-val MAE: {cv_mae:.2f} ± {cv_scores.std():.2f} (5-fold)") | |
| # Feature importance | |
| print("\n🔍 Feature importance (top 10):") | |
| feat_imp = sorted( | |
| zip(FEAT, model.feature_importances_), | |
| key=lambda x: -x[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' | |
| } | |
| for feat, imp in feat_imp[:10]: | |
| bar = "█" * int(imp * 200) | |
| print(f" {feat_labels.get(feat, feat):<28} {bar:<20} {imp:.4f}") | |
| # ───────────────────────────────────────────────────────────── | |
| # 4. TREND FORECASTING — předpověď do budoucna po čtvrtletích | |
| # ───────────────────────────────────────────────────────────── | |
| print("\n📈 Trénuji trend model (lineární regrese per oblast)...") | |
| # Agreguj historická data na čtvrtletní úrovni per oblast | |
| ts = df.groupby(['rok', 'ctvrtleti', 'zdroj'])['pocet_cest'].sum().reset_index() | |
| ts['cas_idx'] = (ts['rok'] - ts['rok'].min()) * 4 + (ts['ctvrtleti'] - 1) | |
| forecast_rows = [] | |
| oblasti = ts['zdroj'].unique() | |
| # Budoucí čtvrtletí: 2026 Q1-Q4 + 2027 Q1-Q4 | |
| future_quarters = [] | |
| for rok in [2026, 2027]: | |
| for q in [1, 2, 3, 4]: | |
| future_quarters.append({'rok': rok, 'ctvrtleti': q}) | |
| cas_min = ts['rok'].min() | |
| for oblast in oblasti: | |
| sub = ts[ts['zdroj'] == oblast].sort_values('cas_idx') | |
| if len(sub) < 3: | |
| continue # příliš málo dat pro trend | |
| X_t = sub['cas_idx'].values.reshape(-1, 1) | |
| y_t = sub['pocet_cest'].values | |
| # Lineární trend | |
| lr = LinearRegression() | |
| lr.fit(X_t, y_t) | |
| trend_r2 = r2_score(y_t, lr.predict(X_t)) | |
| # Sezónní korekce — průměr dle čtvrtletí | |
| sezon = sub.groupby('ctvrtleti')['pocet_cest'].mean() | |
| global_mean = sub['pocet_cest'].mean() | |
| sezon_koef = {q: (sezon.get(q, global_mean) / global_mean if global_mean > 0 else 1.0) | |
| for q in [1, 2, 3, 4]} | |
| # Predikce budoucích čtvrtletí | |
| for fq in future_quarters: | |
| cas_idx = (fq['rok'] - cas_min) * 4 + (fq['ctvrtleti'] - 1) | |
| pred_base = float(lr.predict([[cas_idx]])[0]) | |
| pred_sez = pred_base * sezon_koef.get(fq['ctvrtleti'], 1.0) | |
| pred_final = max(0.0, round(pred_sez, 1)) | |
| # Směr trendu | |
| if lr.coef_[0] > 0.5: | |
| trend_dir = "↑ Rostoucí" | |
| elif lr.coef_[0] < -0.5: | |
| trend_dir = "↓ Klesající" | |
| else: | |
| trend_dir = "→ Stabilní" | |
| forecast_rows.append({ | |
| 'oblast': oblast, | |
| 'rok': fq['rok'], | |
| 'ctvrtleti': fq['ctvrtleti'], | |
| 'predikce_cest': pred_final, | |
| 'trend': trend_dir, | |
| 'trend_sklon': round(float(lr.coef_[0]), 3), | |
| 'trend_r2': round(trend_r2, 3), | |
| }) | |
| forecast_df = pd.DataFrame(forecast_rows) | |
| forecast_df.to_csv('trend_forecast.csv', index=False) | |
| print(f" Zpracováno {len(oblasti)} oblastí → trend_forecast.csv") | |
| print("\n Ukázka předpovědí:") | |
| print(forecast_df[forecast_df['rok'] == 2026][ | |
| ['oblast', 'ctvrtleti', 'predikce_cest', 'trend'] | |
| ].to_string(index=False)) | |
| # ───────────────────────────────────────────────────────────── | |
| # 5. ULOŽENÍ MODELU | |
| # ───────────────────────────────────────────────────────────── | |
| payload = { | |
| 'model': model, | |
| 'le_dict': le_dict, | |
| 'mae_model': mae_model, | |
| 'mae_base': mae_base, | |
| 'zlepseni': zlepseni, | |
| 'r2': r2, | |
| 'cv_mae': cv_mae, | |
| 'feat_imp': feat_imp, | |
| 'feat_labels': feat_labels, | |
| 'features': FEAT, | |
| } | |
| with open('model.pkl', 'wb') as f: | |
| pickle.dump(payload, f) | |
| print(f"\n✅ model.pkl uložen") | |
| print(f"✅ trend_forecast.csv uložen") | |
| print(f"\nNahraj oba soubory na HuggingFace Space spolu s app.py.") | |
| print("=" * 60) |