Poznań w ramach projektu Otwarty Poznań udostępnia sporo danych publicznych na stronie poznan.pl. Dostęp do tych informacji odbywa się poprzez API. Jest bardzo dużo danych przestrzennych, a także informacje na temat poznańskich wydarzeń czy obiektów. Tematem tego wpisu będą parkomaty.

Pobranie danych

Na początku wczytujemy potrzebne pakiety - jsonlite do wczytania danych z API, tidyverse do kompleksowego przetwarzania danych i pozostałe do działania na obiektach przestrzennych.

# pobieranie danych
library(jsonlite)
# przetwarzanie
library(tidyverse)
# operacje przestrzenne
library(ggmap)
library(sp)
library(rgeos)
library(geosphere)
library(gmapsdistance)

d <- fromJSON("http://www.poznan.pl/mim/plan/map_service.html?mtype=pub_transport&co=parking_meters")

Dane uzyskujemy w postaci listy, która zawiera współrzędne geograficzne każdego parkomatu oraz informację o przynależności do jednej z trzech stref parkowania i ulicy. Przekształcamy listę do ramki danych.

d_coords <- data.frame(matrix(unlist(d$features$geometry$coordinates), 
                              nrow = nrow(d$features), byrow = T))
names(d_coords) <- c(c("lon", "lat"))

d_prop <- d$features$properties

strefy <- cbind(d_prop, d_coords)

head(strefy) %>%
  knitr::kable()
zone street lon lat
A Garbary 16.93762 52.40551
A Działowa 16.93105 52.41267
A plac Wielkopolski 16.93205 52.41088
A Marcinkowskiego 16.92958 52.41036
A Marcinkowskiego 16.92950 52.41095
A Marcinkowskiego 16.92951 52.41145

Wszystkie niezbędne informacje są w jednym miejscu, więc możemy zobaczyć jak to wygląda na mapie.

get_poznan <- get_map(c(16.917, 52.41), zoom = 14)
poznan <- ggmap(get_poznan)

poznan + geom_point(data=strefy, aes(x=lon, y=lat, colour=zone)) +
  scale_color_manual(values=c("#F80500", "#F4C604", "#07D500"), 
                     guide = guide_legend(title="Strefa")) +
  ylab("") + xlab("")

Największa i najdroższa strefa czerwona pokrywa całe centrum miasta. Nieco tańsza strefa pomarańczowa to przede wszystkim Jeżyce oraz okolice dworca PKP. Z kolei strefa zielona to kilka ulic w obrębie Łazarza oraz w północno-zachodniej części Jeżyc. W pozostałych częściach Poznania można (jeszcze) parkować za darmo.

Zobaczmy jeszcze jak kształtuje się gęstość parkomatów na mapie.

poznan + geom_point(data=strefy, aes(x=lon, y=lat, colour=zone)) +
  scale_color_manual(values=c("#F80500", "#F4C604", "#07D500"), 
                     guide = guide_legend(title="Strefa")) +
  ylab("") + xlab("") +
  geom_density2d(data=d_coords, aes(x=lon, y=lat), size = 0.5, bins = 8)

Najgęściej jest na Jeżycach oraz w pobliżu Starego Rynku. Niemniej na podstawie tego rysunku można zidentyfikować ulice, na których parkomatów nie ma.

Analiza danych

Spróbujmy teraz odpowiedzieć na pytanie jak daleko przeciętnie będziemy mieli do parkomatu, jeżeli zaparkujemy gdzieś w centrum. W analizie ograniczymy się do strefy A ponieważ parkując w tej strefie nie interesuje nas parkomat np. w kolorze pomarańczowym.

strefa_a <- strefy %>%
  filter(zone=="A")
  
strefa_a <- strefa_a %>%
  mutate(id=1:nrow(strefa_a))

strefa_a_sp <- strefa_a
coordinates(strefa_a_sp) <- ~lon+lat

dist <- distm(strefa_a_sp)

W pierwszej kolejności filtrujemy interesujące nas obserwacje, a następnie nadaję unikalny identyfikator, którego w danych nie ma, a który ułatwi późniejszą analizę. Aby policzyć odległości pomiędzy parkomatami muszę przekształcić ramkę danych na obiekt klasy SpatialPointDataFrame. Ostatnim krokiem jest wyznaczenie macierzy odległości pomiędzy punktami z wykorzystaniem funkcji distm z pakietu geosphere, która zwraca odległość pomiędzy dwoma punktami w linii prostej w metrach.

Na podstawie tak utworzonej macierzy dla każdego parkomatu szukamy jego najbliższego sąsiada. Tutaj nieoceniona okazała się pomoc stackoverflow i tego wpisu.

min_dist <- apply(dist, 1, function(x) order(x, decreasing=F)[2])

strefa_a_dist <- cbind(strefa_a, strefa_a[min_dist,], 
                       apply(dist, 1, function(x) sort(x, decreasing=F)[2]))

colnames(strefa_a_dist) <- c(colnames(strefa_a), 
                             paste0("n_", colnames(strefa_a)), "odl_prosta")

head(select(strefa_a_dist,-zone,-n_zone)) %>% knitr::kable()
street lon lat id n_street n_lon n_lat n_id odl_prosta
67 Garbary 16.93762 52.40551 1 Wszystkich Świętych 16.93777 52.40554 67 10.64656
106 Działowa 16.93105 52.41267 2 Działowa 16.93129 52.41204 106 72.10591
105 plac Wielkopolski 16.93205 52.41088 3 Wolnica 16.93235 52.41130 105 51.17644
192 Marcinkowskiego 16.92958 52.41036 4 23 Lutego 16.92987 52.41002 192 42.88850
6 Marcinkowskiego 16.92950 52.41095 5 Marcinkowskiego 16.92951 52.41145 6 55.75699
5 Marcinkowskiego 16.92951 52.41145 6 Marcinkowskiego 16.92950 52.41095 5 55.75699

W rezultacie uzyskaliśmy informację o najbliższym sąsiedzie oraz odległości w linii prostej dla każdego parkomatu. Zweryfikujmy odległość dla pierwszej pary, która wynosi 10 metrów.

Odległość w linii prostej się zgadza, ale w rzeczywistości nie pokonamy tej drogi w ten sposób. Idąc chodnikiem wzdłuż ulicy faktyczna odległość wynosi ponad dwa razy tyle, co odległość w linii prostej.

Wobec tego uzupełnimy posiadane dane o informacje z google maps, a pomoże w tym pakiet gmapsdistance. Bez podania klucza autoryzującego do dyspozycji jest 1000 bezpłatnych zapytań dziennie, a po weryfikacji liczba ta rośnie do 150 000 zapytań.

g_dist <- gmapsdistance(origin = paste0(strefa_a_dist$lat,"+",strefa_a_dist$lon),
                        destination = paste0(strefa_a_dist$n_lat,"+",strefa_a_dist$n_lon), 
                        mode = "walking", 
                        combinations = "pairwise")

strefa_a_dist <- strefa_a_dist %>%
  mutate(odl_google=g_dist$Distance$Distance,
         czas_google=g_dist$Time$Time)

Funkcja gmapsdistance zwraca informację o czasie (w sekundach), odległości (w metrach) oraz statusie (czy udało się pobrać dane). Dodajemy dane wynikowe do naszego zbioru. Sprawadźmy teraz korelację pomiędzy danymi z funkcji distm a gmapsdistance.

ggplot(strefa_a_dist, aes(odl_prosta, odl_google)) + 
  geom_point() +
  xlab("Odległość w linii prostej") + 
  ylab("Odległość na podstawie Google Maps") +
  theme_light()

W większości przypadków istnieje liniowa zależność pomiędzy odległością w linii prostej, a tej obliczonej na podstawie Google Maps. Zaniepokojenie budzą punkty znacznie oddalone od teoretycznej prostej, które pokazują, że dane pobrane z Google Maps nie są idealne.

Na przykład odległość pomiędzy dwoma parkomatami na ulicy Rybaki wynosi 0 metrów.

Trasa bywa także przekombinowana.

Z racji tego, że dane nie są idealne i występują wartości odstające to posłużymy się medianą do ustalenia tego ile przeciętnie zajmie nam dotarcie do najbliższego parkomatu.

strefa_a_dist %>%
  select(odl_prosta, odl_google, czas_google) %>%
  gather(Miara, wartosc) %>%
  group_by(Miara) %>%
  summarise(Mediana=round(median(wartosc))) %>%
  knitr::kable()
Miara Mediana
czas_google 58
odl_google 78
odl_prosta 62

Zgodnie z interpretacją mediany - do połowy parkomatów dotrzemy w mniej niż 58 sekund, a do drugiej połowy w 58 sekund lub więcej. Od 50% parkomatów dzieli nas odległość 78 metrów lub mniej, a od 50% parkomatów 78 metrów lub więcej wzdłuż chodnika lub 62 metry w linii prostej.

Podsumowując, w Poznaniu raczej nie będziemy zmuszeni do szukania parkomatu - zwykle powinien być w zasięgu naszego wzroku. A poza tym możemy skorzystać z mobilnych metod opłacania strefy parkowania.

Na podstawie takich i podobnych danych przestrzennych można by się pokusić o napisanie aplikacji, która wskazywałaby najbliższy parkomat na podstawie bieżącej lokalizacji.

Kod w jednym kawałku dostępny jest na githubie.