flexbus-plzen / model_train.py
simikkk's picture
Update model_train.py
30827d3 verified
Raw
History Blame Contribute Delete
9.58 kB
"""
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)