Latający premierzy

Przełom lipca i sierpnia 2019 roku w polskiej polityce zdominowany był przez informacje o lotach - najpierw marszałka Sejmu, później kolejnych premierów. KPRM opublikował listę lotów kolejnych premierów z ostatnich kilku lat - zobaczmy czego nie zrobiły z nią media?

Listy te (na dzień 18 sierpnia 2019 roku) dostępne były pod linkami:

Nie znalazłem w żadnych mediach sensownego porównania pomiędzy premierami, więc porównam sobie sam. Przy okazji pokazując jak można w kilkadziesiąt minut przygotować rzetelny materiał.

Jeśli chcesz zobaczyć wynik analizy - przejdź niżej.

Przygotowanie danych

W pierwszej kolejności pobieramy pliki PDF (z powyżej podanych adresów). Drugi element to przepisanie danych do analizowanej postaci. Do wczytania PDFów w R można wykorzystać pakiet pdftools.

Pakiet ten wskazany plik czyta i zwraca jako listę z zawartością poszczególnych stron. Każdy z elementów tej listy to zwykły ciąg znaków. Na szczęście w tych dokumentach mamy do czynienia z jednakowymi tabelkami - możemy więc każdy z plików potraktować jednakowo. Poniższy kod zrobi za nas część pracy:

library(tidyverse)
library(pdftools)

# lista plików PDF
file_names <- c("dane/pbs.pdf", "dane/pdt.pdf", "dane/pek.pdf", "dane/pmm.pdf")

# tutaj będziemy trzymać pełne dane
complete = c()

# dla każdego kolejnego pliku:
for(file_name in file_names) {
  # wczytujemy PDFa
  pdf <- pdf_text(file_name)

  # miejsce na linie z aktualnie przerabianego pliku
  full <- c()

  # dla każdej ze stron w pliku
  for(i in 1:length(pdf)) {
    # dzielimy stronę na linie
    temp <- str_split(pdf[i], "\n")[[1]]
    # dodajemy linie z bierzącej strony do linii z całego pliku
    full <- c(full, temp)
  }

  # usuwamy puste linie
  full <- full[nchar(full) != 0]

  # więcej niż jedna spacja zostaje zamieniona na ;
  full <- gsub(pattern = " +", ";", full)

  # dodajemy kolumnę z nazwą pliku
  full <- paste0(full, ";", file_name)

  # dodajemy linie pliku do pełnych danych
  complete <- c(complete, full)
}

# zapisujemy pełne dane do pliku CSV
write_lines(complete, "dane/zapis.csv")

Teraz trochę ręcznej gimnastyki. Otwieramy zapisany plik CSV w Excelu i oglądamy każdy z wierszy. Docelowo chcemy uzyskać postać tabeli, gdzie każdy wiersz będzie zwierał trzy kolumny:

  • kolumna data - data w formacie dzień.miesiąc.rok (tak jak to zapisane w dokumentach)
  • kolumna trasa - trasę jaką przebył samolot w formacie lotnisko A-lotnisko B-lotnisko C; ważne żeby kolejne lotniska rozdzielone były myślnikami
  • kolumna kto - nazwę pliku z jakiego pochodzi informacja - to opisuje nam z którym premierem mamy do czynienia

Wierszy było około tysiąca, ich poprawa zajęła może z 30 minut polegających głównie na złączeniu tekstów z dwóch (czasem więcej) kolumn do jednej komórki i ewentualnym przesunięciu wiersza w prawo. Niektóre wiersze trzeba było też usunąć (nagłówki tabel). Zapewne udałoby się to zrobić z poziomu R (i odpowiednich wyrażeń regularnych - tutaj sprawdzą się znakomicie), ale ręczny przegląd danych czasem też się przydaje.

Kolejny krok to oczyszczenie danych. Lotniska nazwane są kodami, ale czasem to samo lotnisko występuje pod dwoma różnymi kodami. Maszyna tego nie zrozumie, trzeba jej pomóc.

Przydatne będą dane o lokalizacji lotnisk - znajdziemy je na stronie ourairports.com. Pobieramy plik CSV z listą lotnisk (np. dla całej Europy).

Teraz czas na poprawę i oczyszczenie danych. Najpierw zobaczymy czego nam brakuje i co trzeba poprawić. Wczytujemy plik z listą lotów i rozbijamy kolumnę z trasą samolotu z danego dnia na poszczególne lotniska:

library(tidyverse)

# poprawnione ręcznie dane
df <- read_csv2("dane/zapis.csv")

# położenie lotnisk z http://ourairports.com/
lotniska <- read_csv("dane/airports.csv")

# przygotowanie danych
loty <- df %>%
  # rozdzielenie tracy na kolejne lotniska
  mutate(trasa = str_split(trasa, "-")) %>%
  unnest(trasa)

w tabeli loty mamy teraz kolumnę trasa, która zwiera wszystkie kody lotnisk. Jeśli zrobimy

count(loty, trasa, sort = TRUE)

dostaniemy listę ułożoną wg popularności. Na początku będzie zapewne Warszawa, Gdańsk, Kraków. Warszawa może być jako EPWA lub WAW. W pliku z ourairports.com mamy tylko EPWA - stąd potrzebne korekty (najlepiej od razu na pliku zapis.csv). Jak znaleźć wszystkie lotniska, które trzeba poprawić? Ano próbujemy złączyć tabelkę o lotach z tabelką o lotniskach - to co nie uda się połączyć trzeba poprawić. Łączymy na przykład tak:

loty %>%
  distinct(trasa) %>%
  left_join(lotniska %>% select(ident, name),
            by = c("trasa" = "ident"))

To co w kolumnie name będzie mieć NA to braki. Dla tych wierszy szukamy kodu lotniska (jaki mamy) na stronie ourairports.com i przez search & replace zmieniamy w pliku zapis.csv jeden kod na taki, który podaje ourairports.com. W większości przypadków powinno się udać.

Po zmianach oczywiście zapisujemy zapis.csv.

Z tak poprawionym plikiem robimy porządek w danych doprowadzając je do postaci, gdzie w jednym wierszu będziemy mieć informacje o konkretnym locie, w konkretnym dniu, konkretnego premiera, z jednego do drugiego lotniska:

library(tidyverse)
library(lubridate)

# poprawnione ręcznie dane
df <- read_csv2("dane/zapis.csv")

# położenie lotnisk z http://ourairports.com/
lotniska <- read_csv("dane/airports.csv")

# przygotowanie danych
loty <- df %>%
  # rozdzielenie tracy na kolejne lotniska
  mutate(trasa = str_split(trasa, "-")) %>%
  unnest(trasa) %>%
  select(data, kto, lotnisko_z=trasa) %>%
  # przesuwamy o jeden lotniska
  group_by(kto, data) %>%
  mutate(lotnisko_do = lead(lotnisko_z)) %>%
  filter(!is.na(lotnisko_do)) %>%
  filter(lotnisko_do != lotnisko_z) %>%
  ungroup() %>%
  # dodajemy położenie dla lotniska źródłowego i docelowego
  left_join(lotniska %>%
              select(ident, lat_z=latitude_deg, long_z=longitude_deg, miasto_z=municipality),
            by = c("lotnisko_z" = "ident")) %>%
  left_join(lotniska %>%
              select(ident, lat_do=latitude_deg, long_do=longitude_deg, miasto_do=municipality),
            by = c("lotnisko_do" = "ident")) %>%
  # wybieramy potrzebne kolumny
  select(kto, data, lotnisko_z, lotnisko_do, lat_z, long_z, miasto_z, lat_do, long_do, miasto_do) %>%
  # usuwamy niedopasowane lotniska - uwaga: tracimy część danych!
  na.omit() %>%
  # budujemy trasę przelotu
  mutate(lot = paste0(miasto_z, " - ", miasto_do)) %>%
  # poprawiamy format daty
  rowwise() %>%
  mutate(data2 = str_match(data, "(\\d{2}\\.\\d{2}\\.\\d{4})")[[1,1]] %>% dmy()) %>%
  ungroup() %>%
  mutate(kto = toupper(kto))

Przykładowe wiersze wyglądają następująco:

ktodatalotnisko_zlotnisko_dolat_zlong_zmiasto_zlat_dolong_domiasto_dolotdata2
PMM18.03.2019EPKKEPWA50.0777019.78480Kraków52.165720.96710WarsawKraków - Warsaw2019-03-18
PDT01.12.2008EPPOEPWA52.4210016.82630Poznań52.165720.96710WarsawPoznań - Warsaw2008-12-01
PBS29-30.11.2015EPWAEBBR52.1657020.96710Warsaw50.90144.48444BrusselsWarsaw - Brussels2015-11-30
PMM16.12.2018EPKTEPWA50.4743019.08000Katowice52.165720.96710WarsawKatowice - Warsaw2018-12-16
PDT27.08.2011EPWAEPWR52.1657020.96710Warsaw51.102716.88580WrocławWarsaw - Wrocław2011-08-27
PEK15.10.2015EBBREPWA50.901404.48444Brussels52.165720.96710WarsawBrussels - Warsaw2015-10-15
PEK03.09.2015EPWAEPKT52.1657020.96710Warsaw50.474319.08000KatowiceWarsaw - Katowice2015-09-03
PMM28.04.2018EPZGEPOK52.1385015.79860Babimost54.579718.51720GdyniaBabimost - Gdynia2018-04-28
PMM26.04.2019LUZEPWA51.2402822.71361Lublin52.165720.96710WarsawLublin - Warsaw2019-04-26
PMM09.02.2019EPRZEPWA50.1100022.01900Rzeszów52.165720.96710WarsawRzeszów - Warsaw2019-02-09

Dane są przygotowane, można przejść do opracowania materiału tak, jak nie zrobiły tego media.

Analiza

Na przykład - najpopularniejsze trasy (3/4 wszystkich dla każdego z premierów):

loty %>%
  count(kto, lot) %>%
  group_by(kto) %>%
  mutate(total = sum(n)) %>%
  ungroup() %>%
  mutate(procent_lotow = 100*n/total) %>%
  arrange(kto, desc(procent_lotow)) %>%
  group_by(kto) %>%
  mutate(total = cumsum(procent_lotow)) %>%
  filter(total <= 75) %>%
  ungroup()

Premier Donald Tusk:

Trasa lotuLiczba lotów% lotów premiera% skumulowany
Warsaw - Gdańsk20727.9727.97
Gdańsk - Warsaw16522.3050.27
Brussels - Warsaw456.0856.35
Warsaw - Brussels435.8162.16
Warsaw - Kraków263.5165.68
Katowice - Warsaw202.7068.38
Kraków - Warsaw172.3070.68
Warsaw - Katowice131.7672.43
Warsaw - Wrocław131.7674.19

Premier Ewa Kopacz:

Trasa lotuLiczba lotów% lotów premiera% skumulowany
Brussels - Warsaw1013.3313.33
Warsaw - Brussels1013.3326.67
Katowice - Warsaw79.3336.00
Warsaw - Katowice68.0044.00
Warsaw - Gdańsk45.3349.33
Wrocław - Warsaw45.3354.67
Gdańsk - Warsaw34.0058.67
Paris - Warsaw34.0062.67
Prague - Warsaw34.0066.67
Warsaw - Wrocław34.0070.67
Kraków - Warsaw22.6773.33

Premier Beata Szydło:

Trasa lotuLiczba lotów% lotów premiera% skumulowany
Kraków - Warsaw5427.027.0
Warsaw - Kraków4221.048.0
Warsaw - Brussels136.554.5
Warsaw - Katowice84.058.5
Rzeszów - Warsaw73.562.0
Brussels - Kraków63.065.0
Brussels - Warsaw52.567.5
Warsaw - Rzeszów52.570.0
Budapest - Warsaw42.072.0
Katowice - Warsaw42.074.0

Premier Mateusz Morawiecki:

Trasa lotuLiczba lotów% lotów premiera% skumulowany
Kraków - Warsaw298.958.95
Warsaw - Kraków216.4815.43
Wrocław - Warsaw195.8621.30
Warsaw - Katowice175.2526.54
Katowice - Warsaw164.9431.48
Warsaw - Brussels154.6336.11
Warsaw - Wrocław154.6340.74
Brussels - Warsaw134.0144.75
Rzeszów - Warsaw123.7048.46
Warsaw - Gdańsk113.4051.85
Warsaw - Rzeszów113.4055.25
Gdańsk - Warsaw92.7858.02
Gdynia - Warsaw92.7860.80
Warsaw - Gdynia82.4763.27
Warsaw - Goleniow82.4765.74
Warsaw - Poznań82.4768.21
Goleniow - Warsaw72.1670.37
Lublin - Warsaw61.8572.22
Warsaw - Lublin61.8574.07

Albo - co ciekawsze - narysujmy mapkę z najczęściej pokonywanymi połączeniami przez każdego z premierów w Polsce:

# lista polskich lotnisk
lotniska_pl <- lotniska %>% filter(iso_country == "PL") %>% pull(ident)

loty %>%
  # tylko polskie lotniska
  filter(lotnisko_z %in% lotniska_pl & lotnisko_do %in% lotniska_pl) %>%
  # ile razy premier pokonywał daną trasę?
  count(kto, lat_z, long_z, miasto_z, lat_do, long_do, miasto_do) %>%
  # jaka to część jej/jego lotów
  group_by(kto) %>%
  mutate(n = n/sum(n)) %>%
  ungroup() %>%
  # rysujemy:
  ggplot() +
  # kontury Polski
  geom_polygon(data = map_data("world") %>%
                 filter(region == "Poland"),
               aes(long, lat, group=group),
               color = "gray30", fill = "gray90") +
  # połączenia lotnicze
  geom_segment(aes(x = long_z, xend = long_do,
                   y = lat_z, yend = lat_do,
                   size = n),
               alpha = 0.25, color = "red",
               show.legend = FALSE) +
  # punkty pokazujące położenie lotnisk
  geom_point(data = lotniska %>% filter(iso_country == "PL") %>%
               filter(ident %in% loty$lotnisko_z | ident %in% loty$lotnisko_do),
             aes(longitude_deg, latitude_deg),
             size = 1) +
  # nazwy miast, w których są lotniska
  geom_text(data = lotniska %>% filter(iso_country == "PL") %>%
              filter(ident %in% loty$lotnisko_z | ident %in% loty$lotnisko_do),
            aes(longitude_deg, latitude_deg, label = municipality),
            size = 2, hjust = -0.1, vjust = 0) +
  # ograniczamy zakres wykresu
  xlim(13, 25) +
  ylim(49, 55) +
  coord_map() +
  scale_size_continuous(range=c(0.5, 3)) +
  # każdy premier na swojej mapce
  facet_wrap(~kto) +
  # kosmetyka
  theme_minimal() +
  theme(axis.text = element_blank(), panel.grid = element_blank()) +
  labs(x = "", y = "")

Możemy też sprawdzić jak latali po Europie:

# gdzie latają po Europie?
loty %>%
  count(kto, lat_z, long_z, miasto_z, lat_do, long_do, miasto_do) %>%
  mutate(kto = toupper(kto)) %>%
  group_by(kto) %>%
  mutate(n = n/sum(n)) %>%
  ungroup() %>%
  ggplot() +
  borders("world", xlim = c(-10, 25), ylim = c(39, 70), fill = "gray90") +
  geom_segment(aes(x = long_z, xend = long_do,
                   y = lat_z, yend = lat_do,
                   size = n),
               alpha = 0.25, color = "red",
               show.legend = FALSE) +
  coord_map() +
  scale_size_continuous(range=c(0.5, 3)) +
  facet_wrap(~kto) +
  theme_minimal() +
  theme(axis.text = element_blank(),
        panel.grid = element_blank()) +
  labs(x = "", y = "")

oraz liczbę lotów w kolejnych miesiącach:

loty %>%
  mutate(data2 = floor_date(data2, "month")) %>%
  count(kto, data2) %>%
  ggplot() +
  geom_col(aes(data2, n, fill = kto), show.legend = FALSE) +
  geom_smooth(aes(data2, n, color = kto), show.legend = FALSE) +
  facet_wrap(~kto, scales = "free_x") +
  scale_x_date(date_labels = "%m/%Y") +
  theme_minimal() +
  labs(x = "", y = "Liczba lotów w miesiącu")

Kto lata najczęściej?

loty %>%
  group_by(kto) %>%
  summarise(mind = min(data2, na.rm = T),
            maxd = max(data2, na.rm = T),
            n = n()) %>%
  ungroup() %>%
  mutate(diff_date = as.numeric(maxd - mind)) %>%
  mutate(day_per_flight = round(diff_date/n, 2)) %>%
  select(-mind, -maxd) %>%
  set_names(c("Premier", "Liczba lotów", "Liczba dni urzędowania", "Częstotliwość lotów")) %>%
  kable() %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
PremierLiczba lotówLiczba dni urzędowaniaCzęstość lotów (co ile dni lot)
PBS2007413.70
PDT74024713.34
PEK753825.09
PMM3245981.85

Wnioski wyciągajcie sami (są mocno zależne od upodobań politycznych). Widać kilka rzeczy:

  • Beata Szydło często latała na trasie Warszawa-Kraków oraz Warszawa-Katowice (lub odwrotnie) - to 54% jej lotów
  • Donald Tusk często latał na trasie Warszawa-Gdańsk i z powrotem - to 50% jego lotów
  • Ewa Kopacz latała głównie do Brukseli, latała też najrzadziej - średnio o 5 dni
  • Mateusz Morawiecki lata dużo, więcej niż inni - ot średnio co niecałe dwa dni

Aktualizacja: nieco inaczej wyczyściłem dane i udało się wyłuskać jakimi samolotami latali poszczególni premierzy. Zestawienie w tabelce:

PremierSamolotLiczba przelotów% przelotów premiera
PDTEMB27648.68
PDTYK17130.16
PDTTU5810.23
PDTW3396.88
PDTBell132.29
PDTMI820.35
PDTYK/Bell20.35
PDTCASA10.18
PDTczarter10.18
PDTpolicja10.18
PDTW-310.18
PDTYK / EMB10.18
PDTYK/W310.18
PEKEMB4187.23
PEKW3510.64
PEKCASA12.13
PBSEMB7345.34
PBSCASA7244.72
PBSW-3 Sokół148.70
PBSB-737 czarter10.62
PBSW-3Sokół/CASA10.62
PMMEMB-17513163.59
PMMGulfstream G-5503316.02
PMMCASA3014.56
PMMŚmigłowiec W-373.40
PMMW-3 Sokół31.46
PMMG-55010.49
PMMW310.49

W tabelce powyżej jeden przelot to jedna linia z pliku źródłowego, zatem na przykład lot Warszawa - Kraków - Rzeszów - Warszawa liczony jest jako jeden, chociaż wcześniej rozbity był na trzy odcinki. Trochę to miesza i może wprowadzić konfuzję jeśli chodzi o sumowanie, ale chodzi o porównanie skali a nie aptekę

Całość opracowania danych zajęła mi może z pół dnia. Dlaczego media tego nie zrobiły?

Cenisz rzetelne dziennikarstwo? To wymagaj go od mediów, za które płacisz. Albo płać tym, których cenisz :-)