Wizualizacja map w Elasticsearch i Kibana – GPS komunikacji miejskiej

Myślisz o analizie i wizualizacji danych geo? Czemu nie spróbować Elasticsearch? Tzw. ELK (Elasticsearch + Logstash + Kibana) to nie tylko baza NoSQL. Jest to cały system, który umożliwia przechowywanie, wyszukiwanie, analizę i wizualizację danych z dowolnego źródła w czasie rzeczywistym. W tym przypadku wykorzystamy otwarte dane lokalizacji komunikacji miejskiej w Warszawie. Wspomniałem o nich w tym artykule.

Instalacja Elasticsearch + Kibana

Tak jak opisywałem to w poprzednim wpisie, użyjemy docker + docker-compose. Poniżej YAML. ES znajdziemy pod portem 9200, natomiast Kibane pod 5601.

version: '2.2'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.5.1
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - esnet
  kibana:
    image: docker.elastic.co/kibana/kibana:7.5.1
    restart: unless-stopped
    depends_on:
      - elasticsearch
    ports:
      - 5601:5601
    volumes:
      - kibanadata:/usr/share/kibana/data
    networks:
      - esnet
 
volumes:
  esdata:
    driver: local
  kibanadata:
    driver: local
 
networks:
  esnet:

Index + Mapping

Mapa i wizualizacja „Coordinate Map” wymaga specyficznego typu geo_point. Pierwszym krokiem jest utworzenie nowego indexu z odpowiednim mappingiem. Z Elasticsearch „gadamy” po REST-cie. Polecam wtyczkę Rest Client do VS Code lub klient typu Postman. Poniżej utworzenie indexu ztm z mappingiem location oraz time.

@host = http://192.168.114.128:9200

PUT {{host}}/ztm
Content-Type: application/json

{
  "mappings": {
    "properties": {
      "location": {
        "type": "geo_point"
      },
      "time": {
        "type": "date" 
      }
    }
  }
}

Index powinien być widoczny również w ustawieniach Kibany. Akurat w tym przypadku już miał trochę rekordów.

Karmienie Łosia… w sensie ELK-a

Trzeba pobrać dane i zasilić je Elasticsearch. Jak pobrać z API dane GPS komunikacji miejskiej opisałem w tym wpisie. W tym przypadku napisałem prosty skrypt w Python który pobrane dane mapuje i robi bulk push do bazy (by nie wysyłać 1k+ pojedynczych POST-ów). Do proof of concept jest ok. Do powazniejszych zastosowań wolałbym użyć Apache Airflow.

import urllib.request, json
from elasticsearch import Elasticsearch, helpers
import datetime
import time

elastic = Elasticsearch("localhost")

def map_to_es(obj):
    entity = {}
    location = {}
    entity_time = datetime.datetime.strptime(obj["Time"], '%Y-%m-%d %H:%M:%S')
    location["lat"] = obj["Lat"]
    location["lon"] = obj["Lon"]
    entity["location"] = location
    entity["brigade"] = obj["Brigade"]
    entity["line"] = obj["Lines"]
    entity["time"] = entity_time.isoformat()
    entity["vehicle_number"] = obj["VehicleNumber"]
    return entity

while True:
    buses = ''
    trams = ''
    api_key = 'place for api-key'
    with urllib.request.urlopen("https://api.um.warszawa.pl/api/action/busestrams_get/?apikey={0}&type=1&resource_id=f2e5503e927d-4ad3-9500-4ab9e55deb59".format(api_key)) as url:
        buses = json.loads(url.read().decode())['result']

    with urllib.request.urlopen("https://api.um.warszawa.pl/api/action/busestrams_get/?apikey={0}&type=2&resource_id=f2e5503e927d-4ad3-9500-4ab9e55deb59".format(api_key)) as url:
        trams = json.loads(url.read().decode())['result']

    buses.extend(trams)
    data = map(map_to_es,buses)
    data = list(data)
    print("Sending records...")
    helpers.bulk(elastic, data, index="ztm")
    time.sleep(10)

Kod jest to nieskończona pętla, która pobiera dane i wrzuca je na ES. Po pobraniu mapuje rekordy, aby schemat zgadzał się z ustalonymi wcześniej mappingiem w indeksie. Jak widać tutaj, jest 5 sposobów na dodanie geo_point. Format daty w ES przyjmuje postać ISO 8601, stąd jej konwersja.

Kibana

Dodanie index pattern

Aby Kibana korzystała z utworzonego wczesniej indexu, należy go dodać do Index Patterns. W tym przypadku (w sensie proof of concept), jest to po prostu „ztm„. W normalnym zastosowaniu przydałby się index, który po myślniku miałby datę. Pojedynczy index mógłby szybko spuchnąć. Wtedy użylibyśmy np. „ztm-*„.

Chcemy, aby pole time było „filtrem czasu”.

Wizualizcje

Index dodany. Teraz w hamburgerze po lewej stronie interesują nas 3 pozycje.

W Discover możemy zobaczyć wpisy w indeksach. Jeśli nic nie widzisz, upewnij się czy wybrałeś odpowiednie okno czasowe (po prawej na górze). Czasami czas maszyny z ELK-iem jest inny niż te z logami np. w związku z innym ustawieniem strefy czasowej.

Mapa

Teraz przejdźmy do mapy. Trzeba dodać nową warstwę i jako źródło danych wybrać Documents.

Następnie zaznaczyć nasz index i pole geo_point

W tym przypadku chcemy widzieć najświeższy pomiar każdego z pojazdów. Unikalny jest numer pojazdu, wiec go wykorzystamy. Ustawiamy konfigurację warstwy tak jak poniżej.

Jeśli nie widzisz żadnych rekordów, upewnij się, że masz odpowiednie okno czasowe.

Heatmapa

Przejdźmy do wizualizacji, wybierzmy nową typu „Coordinate Map” i wskażmy index. Poniżej konfiguracja.

Po wciśnięciu guzika play zobaczymy mniej więcej coś takiego:

Wnioski

Wcześniej Elasticsearch + Kibana używałem głównie do gromadzenia i analizy logów z aplikacji. Jak widać, ma dużo więcej możliwości. Te pokazane w tym wpisie, to tak naprawdę kropla w morzu. Mapy/wizualizacje można dostosowywać do swoich potrzeb. Jedną z rzeczy, które będę chciał sprawdzić to, czy można stosować jakieś funkcje agregujące (w tym okna), by uzyskać prędkości, wektory, kierunki itp. itd. na wzór Apache Spark z tego wpisu.

Jeśli masz jakieś pomysły lub sam korzystasz z ELK-a i masz jakieś uwagi, zapraszam do komentowania ?

EDIT:

Pojawiły się komentarze o repo z kodem, więc nadrobiłem zaległości:

https://github.com/zorteran/wiadro-danych-elk-map-ztm

17 myśli w temacie “Wizualizacja map w Elasticsearch i Kibana – GPS komunikacji miejskiej”

  1. Przyznam się, że do tej pory ELK-a używałem raczej sporadycznie i to jedynie do gromadzenia oraz przeglądania logów, dzięki więc za ten wpis, rozszerzający nieco moje pojęcie o możliwościach tego zestawu narzędzi.

    Zgadzam się z Kolegą wyżej, że jeśli robisz jakiś większy projekt, to wrzucanie kodu gdzieś na gita (w tym takich rzeczy jak pliki docker-compose) i udostępnienie czytelnikom byłoby znacznym ułatwieniem.

    PS. W nagłówku „Wizualizcje”, w którym piszesz o „lewej stornie” masz kilka literówek 😉

  2. Na ostatnim spotkaniu Elastic Warsaw User Group była świetna prezentacja pokazująca możliwości wizualizacji danych Kibany. I to co pokazał Marcin Goss robiło wrażenie 🙂

    Warto też wspomnieć, że jest opcja w Kibanie automatycznego odświeżania co daje poczucia wizualizacji live 😉

    1. Faktycznie zapomniałem wspomnieć o automatycznym odświeżaniu 🙂 Widziałem to spotkanie na meetup.com, ale niestety nie udało mi się na nim pojawić :/ wiesz czy jest nagranie/slajdy z tego wydarzenia?

  3. Bez urazy ale bardzo banalne rozwiązanie. Nawet w wizualizacji nie prezentują się numery linii na markerach. Bardzo skomplikowana infrastruktura do osiągnięcia wręcz trywialnego efektu.

    1. Tyle jeżeli chodzi o konstruktywną krytykę – bo w skrócie jest to wizualizacja geograficzna danych z pliku json. Do tego sprowadza się projekt. Jednak mam ciekawy pomysł „Big Data”. Wyznaczenie dokładnych tras przejazdu linii na podstawie zebranych danych o lokalizacji pojazdu. Oczywiście zajmie to trochę czasu ponieważ dane napływają w odstępach minimum 10 sekund ale gromadząc dane przez jakiś czas można to uzyskać. Efektem końcowym powinien być obiekt polyline z przebiegiem trasy. Dane trzeba zbierać przez wiele dni ale jak to potem wszystko uporządkować.

      1. Masz rację, że jest to banalne rozwiązanie. Chciałem sprawdzić ile pracy trzeba wykonać, by cokolwiek wizualizować. I jak widać główna „trudność” to założenie indeksu z odpowiednim mappingiem.

        Fajny pomysł. Planuję pobawić się trochę tym stack-iem. Zastanawiam się, czy można na podstawie danych geo obliczyć prędkości i zrobić „heatmapę” średnich prędkości. Obszary wolniejsze świadczyłyby o korkach itp.

        1. Nie policzysz prędkości bo masz punkty ale nie znacz prawdziwej odległości między nimi bo nie znasz trasy. Liniowa odległość między pomiarami nie mają wspólnego z przebytą drogą. Jest za mało danych do analizy. Myślę, że trzeba zacząć od opracowania tras po których poruszają się pojazdy. Nie jest to banalny problem bo trasy dla konkretnej linii bywają różne. Trochę zgłębiłem ten temat. Problem jest ciekawy ale efekty nie osiągnie się tak szybko i takimi prostymi metodami.

          1. To prawda. Precyzja GPS też jest różna. Mimo wszystko będzie to „jakaś” informacja. Kiedyś dla podobnego problemu używaliśmy http://project-osrm.org/ lub API Google Maps, by generować dodatkowe punkty między pomiarami (klient chciał by ślady na mapie nie ścinały zakrętów).

          2. Trasy autobusów są przygotowane przez społeczność OpenStreetMap, np. jedna z tras autobusu 105 w Warszawie: https://www.openstreetmap.org/relation/2460922
            Dane z OpenStreetMap można wyciągnąć przy pomocy OverPass Turbo.

            Następnie połączyć to wszystko w całość i można liczyć przebyty dystans. Ja u siebie dystansu nie liczę (dobra, może gdzieś tam liczę po coś, nie pamiętam, na pewno nie wyświetlam 😛 ), natomiast samo wyświetlanie trasy pojazdu jest – https://czynaczas.pl

  4. Dziękuje Macieju, jest to fantastyczna sugestia. Szukałem rozwiązania, bo nie chce mi się samemu ręcznie takich tras kreślić. A przecież mam zrobiony silnik który wyznacza trasy i po prostu trzeba go do tego użyć. A ja myślałem jak tu zbudować polyline z nie uporządkowanych danych o punktach i zastanawiałem się czy QGIS jakoś się nadać może do tego 🙂

    1. Jeżeli już używasz Kibana spróbuj może czy da się tam stworzyć indywidualne markery po których poznamy co to jest. Np marker z numerem linii. To mogło by kogoś zainteresować.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *