Predikce hustoty dopravy ve městě 


Tereza Černá & Petra Horáčková

Mentoři: Anežka & Adam z aLook Analytics

Na úvod něco o nás 

Naše společná cesta začala už třetí den Akademie. Zásadní je utvořit kompatibilní dvojici a společně přijít na zajímavý projekt, proto každá konverzace mezi účastnicemi u piva připomíná výslech.

Obě jsme měly stejný cíl :  naučit se něco více z pythonu a základy machine learningu. Po třetí lekci na pivu jsme zjistily, že nás spojuje i zájem o geodata, mapy a chování lidí ve městě. A tak už byla skoro ruka v rukávě.

Zbývalo jen získat správné mentory. Nejvíce nás zaujal projekt aLook, a to Predikce zpoždění PID. S myšlenkou Smart Cities už jsme si nějakou dobu pohrávaly. Při přípravě medailonku pro mentory jsme si ujasnily priority a utvdily se o naší kompatibilitě a na MeetYourMentor jsme aLook ulovily. 

Cíl projektu a možnosti aplikace machine learningu

Protože Opendata o zpoždění PID se ukázala jako nepoužitelná, rozhodly jsme se naše seznámení s machine learningem uskutečnit skrz vytvoření modelu predikce hustoty dopravy.

Model by mohl sloužit jako základ pro optimalizaci dopravy, například aby pomocí cedulí byli řidiči informováni o budoucí koloně dřív, než v ní uvíznou. Dále by se mohla řídit doprava upravením počtu pruhů v jednotlivých směrech, nebo predikovat dopad uzavírek určité komunikace.

Příprava a čištění dat

Nejprve bylo potřeba data z navigace dostat do SQL. Problém byl, že hodnoty dvojice geo souřadnic byly uvedeny v jednom sloupci v následujícím formátu: "{'"x'": 11.111111, '"y'": 22.222222}". Sloupce byly odděleny čárkou, která se uvnitř stringu v uvozovkách ignoruje. Při importu do Kebooly se do prvního sloupce začnou zapisovat znaky uvnitř první dvojice uvozovek, tedy {'. Do následujícího sloupce zbývalo x'": 11.111111, '"y'": 22.222222}", což nezačíná ani čárkou, ani uvozovkou, která by signalizovala další sloupec a import dat skončí errorem. Problém jsme vyřešily pomocí regexů v Bashi. Postupné vymazání všech nepotřebných znaků a rozdělení na dva sloupce stávající čárkou jsme provedly následovně:

cat raw_data.csv | sed -e 's/"{'\''"x'\''": //g' | sed -e 's/ '\''"y'\''": //g' | sed -e 's/}"//g' | sed -e 's/location/location_x,location_y/g' | tee data_separated_columns.csv 

Příkaz cat vypíše postupně všechny řádky v csv souboru. Pipeline zajistí, že výstup z příkazu se použije jako vstup do následujícího. Řádek je tedy vstupem do příkazu sed, který nahradí danou sekvenci znaků jinou. Nejprve všechny znaky mezi prvními uvozovkami a mezerou nahradí ničím, pak všechny znaky, které následují po čárce mezi souřadnicemi nahradí opět ničím a nakonec všechny znaky za druhou souřadnicí až po } nahradí opět ničím. Například sed -e 's/"{'\''"x'\''": //g': -e znamená, že příkazu sed dáme jako vstup expression, který je v jednoduchých uvozovkách, kde příkaz s/ znamená substitute a /g znamená globally, kdekoliv na řádku. Celá syntax je následující: 's/string_ktery_chci_najit/string_kterym_ho_chci_nahradit/g'. Aby to nebylo tak jednoduché, obsahuje náš string ale i jednoduchou uvozovku. Pomocí escape characteru \ řekneme příkazu sed, aby ji přeskočil (patří do stringu) a následně string ukončíme jednou vážně míněnou jednoduchou uvozovkou. A tak dál. Výsledkem jsou 2 sloupce souřadnic.

Další problém byl o poznání triviálnější. Jeden se souborů byl tak veliký, že import do Kebooly se i přes několik pokusů nepovedl. Proto jsme soubor rozdělily na několik menších, které jsme v Keboole připojovaly do stávající tabulky:

tail -n +2 raw_data.csv | split -l 10000000 - split_ # rozdeli soubor po 10000000 radkach a pojenuje jednotlive splity split_[number]
for file in split_*; do head -n 1 raw_data.csv > tmp_file; cat $file >> tmp_file; mv -f tmp_file $file; donev # pro kazdej soubor split_X precte header z puvodniho raw_data.csv a prida je na prvni misto, aby mel kazdy split header

Následně jsme data transformovaly ve Snowflake v Keboole. Na základě GPS souřadnic jsme město rozdělily na 1x1 km čtverce. Po joinu jednotlivých tabulek jsme na základě počtu záznamů o poloze a čase stanovily intenzitu dopravy v jednotlivých čtvercích. Jednotlivé záznamy jsme přiřadily ke čtverci takto:

Přiřazení záznamu ke čtverci
Přiřazení záznamu ke čtverci

Dalším krokem bylo vymyslet, jak bude náš celulární model pro simulaci hustoty dopravy fungovat. Základním principem je, že hodnota buňky v čase t+1 je ovlivněna hodnotou sousedních buněk v čase t. Se stanovením charakteristik, které budou mít na hodnotu sousední buńky vliv, tzv. features i s kostrou python kódu pro cellmodel nám pomohl Honza z aLooku. Základ celulárního modelu můžete vidět zde.

Jak model funguje? Předpokládáme, že se doprava v jednotlivých dnech chová stejně stanovíme průměrnou hustotu dopravy pro každý čtverec pro každou hodinu dne. Tak vznikne pro každý čtverec 24 stavů. Pro každý čtverec pak spočítáme charakteristiky (features), které jsou na obrázku : hodnota ve čtverci, průměrnou hodnotu pro sousední čtverce a jejich směrodatnou odchylku, průměr rozdílu mezi hodnotou ve čtverci a v sousedních čtvercích a další. Targetem, tedy hodnotou, kterou bude náš model predikovat je hodnota buňky v následující hodině. Následně se data rozdělí na ténovancí a testovaci set. Na trénovacím setu se model natrénuje a na testovacím se ověří jeho schopnost predikce.

Iniciální návrh ML modelu.
Iniciální návrh ML modelu.


Vytvoření vlastního programu v Pythonu

Pro práci s modelem v pythonu je důležité si části kódu rozdělit do oddělených class. Protože když se na kód podíváte po delší době, nebudete vědět, o co jde a co kde leží, mluvím z vlastní zkušenosti. A také to bude přehlednější i pro všechny, kteří vám budou pomáhat.


Main class: runner

Runner má za úkol dirigovat celý běh programu od načtení dat z csv souboru přes vytvoření trénovacích a testovacích setů po natrénování modelu, spuštění simulace a vykreslení všech výstupů. V detailu to potom vypadá zhruba takto:


Prvotní načtení dat - class datareader

Metoda get_df_cube se volá z námi napsané class datareader.
Datareader poté načte náš soubor do modelu následujícím způsobem:

Potřebujeme vytvořit kostku, kde budou za jednotlivé hodiny do čtverců načteny čísla reprezentující hustotu dopravy v dané poloze v čase. Vznikne tak 24 "pater" kostky. Pro vizualizaci to znamená, že budeme moci pro každou hodinu a každý čtverec zobrazit hustotu dopravy na území města ve skutečném stavu, predikci z hodiny na hodinu a později i simulaci.


Class cellmodel a kouzla se čtverci


Naše class cellmodel obsahuje několik důležitých věcí. V první řadě je to samotná kostra modelu a jeho metody fit a predict. Po vyzkoušení několika typů modelů jsme se na základě nejlepších výsledků rozhodly použít SVR (support vector regression) model z knihovny Scikit-Learn. 

Dále cellmodel obsahuje metody pro výpočet features a targets pro jednotlivé kroky a taky smyčky, které zajistí běh celé simulace čtverec po čtverci (o tom později).

Features a targets se počítají následovně. V prvním kroku čtverci, který vyšetřujeme, pomocí funkce get_neighbor_indices vypočítáme indexy všech sousedů. Kromě výpočtu sousedů jsme čtvercům na diagonálách přiřadily menší váhu, než čtvercům, které přímo sousedí s buňkou - předpokládáme, že diagonálně sousedící buňky budou mít na sebe menší vliv.

Následně metoda get_square_features vytvoří pro daný čtverec záznam do výsledného dataframe, kam uloží hodnoty sousedních čtverců, které mají souřadnice z get_neighbor_indices. Dále ukládá informace o čase, ve kterém se daný čtverec nachází. Abychom zajistily, že model bude počítat s časem jako s cyklickou proměnnou (nabývá hodnot od 0 do 23 a pak začne znovu) transformujeme hodinu h pomocí sinu a cosinu. Do targets potom přidáme stav toho samého čtverce v následující hodině.

Z features a target se bude trénovat model. Před tím, než se ale bude model trénovat, musíme z dat vybrat validační set, který zatím modelem neprojde. Knihovna Scikit-Learn má pro tuhle potřebu přímo funkci train_test_split, která dělí data na train a test.


Trénování a validace modelu

Po rozdělení si vytvoříme objekt Model, který jsme si uložily pod my_model. Je to objekt SVR modelu, který má metoda fit a predict obsažené v knihovně Scikit-Learn. Fit natrénuje model na trénovací sadě, a ten si pak zapamatuje, jak má pracovat. Když potom použijeme predict na testovací sadě, model vytvoří v každé hodině h predikci pro h+1 na základě toho, co se naučil na trénovacím setu dat pomocí metody fit. Výsledné predikce porovnáme pomocí metrik R2 a MSE s targets pro test sadu a zjistíme úspěšnost modelu při předpovědi.


Simulace

Simulace v našem případě vlastně opakuje predict pro každý čtverec a každé patro jako v předchozím případě. Simulaci poskytneme natrénovaný model z předchozího kroku a pouze jedno výchozí patro. 

Simulace vezme toto patro čtverec po čtverci, u každého spočítá features, a na základě toho, co se model naučil v metodě fit udělá predikci o jednu hodinu dopředu. Až projde celou hodinu čtverec po čtverci, přesune se na další "patro" kostky, kde to samé udělá pro hodinu + 1, a predikuje pro další hodinu dopředu.

Protože výchozí patro nemusí být jenom h=0, simulace si v případě výchozího patra v jiném čase to uloží na stejnou pozici v nové prázdné kostce, simuluje až do hodiny 23 nahoru, a potom pokračuje od hodiny 0, dokud zase nedojde do výchozího bodu.

Abychom vyzkoušely všechny možnosti, daly jsme simulaci postupně jako výchozí stav všechna patra naší původní kostky a sledovaly o kolik budou výsledky (ne)přesnější. 


Příprava výsledků pro výstup

Simulace skončí tím, že máme naplněnou kostku, která obsahuje jedno výchozí patro, které odpovídá zadaným hodnotám a dalších 23 patrer vyplněných predikovanými hodnotami.

Tuto kostku je třeba zpátky "narovnat" do dataframe abychom mohly výstupy analyzovat. Pro lenost k tomu použijeme znovu metodu get_features_target, která vrací mimo jiné i vektor target, který ted' obsahuje naše nasimulované hodnoty.

Tento sloupec připojíme k původnímu dataframe vytvořenému ze vstupní kostky a získáme tak tabulku, kde jsou vedle sebe poloha, aktuální hodnota, target (hodnota v h+1), prediction (predikovaná hodnota v h+1), a simulation (nasimulovaná hodnota v h+1, která závisí na startovní hodině simulace). 


Class plotter - obrázky obrázky obrázkyyyy

Protože jsme chtěly mít výstup pokud možno automaticky po každé simulaci, bylo třeba přijít s nápadem, jak opakovaně "plotovat" výstupy a nezbláznit se při tom opakováním zmateného kódu knihovny Matplotlib pro formátování jednotlivých plotů a pod.

Vytvořily jsme proto class plotter, kam jsme všechno schovaly do objektu CustomPlotter.

Vytvořením objektu CustomPlotter se nám automaticky vytvoří naformátované "plátno" o velikosti kterou si nadefinujeme a zpřístupní se nám metody pro plotování, které jsme si připravily předem. Ty obsahují všechno potřebné pro přechroupání našeho dataframe. Jako bonus jsme ještě do plotteru schovaly kousek kódu pro uložení všech obrázků do adresáře podle hodiny a vytvoření gifu, ze všech obrázku dohromady.


Poslední kroky

Když už bylo všechno připraveno pro zobrazení výsledků, bylo třeba ještě posoudit, nakolik je simulace přesná. K tomu jsme použily dvě metriky: R2 a MSE. Pro každou hodinu simulace jsme porovnaly přesnost predikce vůči targetu a simulace vůči targetu. To nám ukázalo jak moc se model rozchází se skutečností ne jen graficky ale i v číslech.

Nakonec nezbývalo, než jen na tabulku výsledků zpátky napojit geo dataframe s naším 1x1km gridem, udělat spatial join pomocí geopandas a metriky přidat do obrázků.

Samozřejmě pro všechny 4 mapy je potřeba vybrat barvu. Při hledání těch správných jsme například narazily na odstíny čokoládové! Programátoři prostě mají styl.


Zhodnocení simulace

Výstupem je tabulka a 4 krásné mapy.

Pro simulaci s počáteční hodinou 0 je zobrazen hypotetický stav dopravy v hodině h (modrá). V tabulce je uvedena přesnost modelu, vyjádřená pomocí R2 a MSE (target vs. predikce a target vs. simulace). Pro hypotetický stav ve výchozí hodině h je zobrazen target, tj. hypotetický stav v následující hodině h+1, predikce a simulace tohoto stavu. Všechny výsledky pro jednu simulaci si můžete prohlédnout v následující galerii.

A aby toho nebylo málo, tady je i gif přes celých 24 hodin!

A jak práce na projektu probíhala?

Možná to zní, jako by to celé bylo kódění na 3 večery. Ve skutečnosti za každým kusem kódu skrývá velké množství hledání, a ještě více pokusů. Na Akademii se učíme základy SQL a Pythonu. V úvodu do machine learningu nám moc pomohly 2 přednášky od našich mentorů z aLook v rámci DA. Další potřebnou teorii jsme nastudovaly v knize Machine Learning The Art and Science of Algorithms that Make Sense of Data, Flach(2012), přehledně zpracované info ohledně machine learningu v pythonu je k nalezení v Python Data Science Handbook a v dokumentaci ke knihovně Scikit-Learn.

Bylo to opravdu náročné, zvlášť na začátku, kdy se vám těžko představují abstraktní pojmy pojmy jako "napíšeš si celulární automat, kde si určíš pravidla" nebo "tam stačí jen určit, že krajní čtverce nemají souseda". Pomoc Honzy z aLook s prvním nástřelem modelu byla pro celý projekt zásadní! Následně jsme na základě návrhů a připomínek mentorů a vymýšlely a hledaly řešení. Díky tomu jsme se naučiyi hledat efektivně, a hlavně hledat opravdu to, co potřebujeme! Když jsme nevěděly jak dál, hrozně nám pomáhali kluci na skupinkách. Někdy je frustrující vidět jak něco, o co se pokoušíte 2 dny mají kluci hotové během 30 minut... Ale k tomu se jednou taky dopracujeme! Další naší velkou pomocí byly pravidelné schůzky v aLook s mentory, kteří vždycky, když jsme se zasekly, přišli s dobrou radou nebo nápadem. Nemůžu nezmínit ani Petry přítele Ondru, který často přišel s dobrým řešením. Ano, takže googlení, hledání a dotazy na všechny strany je cesta.

A co závěrem?

Náš model je na světě! Přes všechny strasti se rozběhl a z výchozích hodnot v libovolné počáteční hodině simuluje hustotu dopravy na následujících 24 hodin. A hlavně jsme se toho hrozně moc naučily!

  • Udržovat pořádek v kódu pomocí version-control systému GIT.
  • Že příprava dat není med a "primitivní" příkazy v Bashi pomohou připravit (doslova) milióny řádků pro následný import do SQL.
  • Vyčistit data v SQL a transformovat je v Keboola Connection a Snowflake.
  • Vyzkoušely jsme si různé přístupy k práci s python kódem (PyCharm IDE, Atom s Hydrogen kernel, Jupyter notebook).
  • Naučily jsme se pracovat v pythonu s (Geo)Pandas dataframes, filtrovat, spojovat a zobrazit grafický výstup.
  • Pracovat s machine learningovou knihovnu Scikit-Learn, používat její modely a metody pro výběr a validaci.
  • Vizualizovat data i geodata pomocí knihoven Shapely, Matplotlib a dalších.
  • Jako alternativa pro vizualizace v pythonu se nám osvědčilo Tableu.
  • Také jsme si vyzkoušely, že nainstalovat všechny potřebné package může být náročnější než jejich následné použití, ale s pomocí package manageru Conda a PIPu jsme to zvládly.

Za zmínku stojí i efektivní hledání a třídění informací, organizace a prioritizace činností, schopnost nebát se zeptat, spolupráce, networking a neskutečné rozšíření obzorů. Poznaly jsme, že my a data k sobě patříme, a že nás to strašně baví! A rády využijeme každou příležitost se zlepšovat ❤

Pro zájemce, zdrojové kódy najdete na GitHubu. Zdrojová data bohužel nemůžeme dát veřejně k dispozici. Grafické výstupy na tomto blogu jsou vytvořeny na základě simulovaných hodnot.