3  Przetwarzanie danych

Prezentacja - wczytywanie danych

Prezentacja - przetwarzanie

3.1 Wprowadzenie - pakiet tidyverse

Pakiet tidyverse to zestaw pakietów do kompleksowego przetwarzania i wizualizacji danych. Ładuje następujące pakiety:

  • ggplot2 - tworzenie wykresów,
  • dplyr - przetwarzanie danych,
  • tidyr - zmiana reprezentacji danych,
  • readr - wczytywanie danych tekstowych,
  • purrr - programowanie funkcyjne
  • tibble - sposób przechowywania danych,
  • stringr - przetwarzanie tekstów,
  • forcats - przetwarzanie faktorów
  • lubridate - operacje na datach

Manifest tidyverse ustala następujące zasady:

  • powtórne użycie istniejących struktur danych,
  • tworzenie czytelnych kodów z operatorem pipe %>% (ang. rura, przewód, łącznik).

Wobec tego załadujmy pakiet tidyverse:

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.2     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.4.2     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

W konsoli pojawi się informacja o wersji załadowanych pakietów oraz o konfliktach występujących pomiędzy pakietami. Konflikty te wynikają z takich samych nazw funkcji w różnych pakietach. Kolejność wczytywania pakietów ma znaczenie - kolejny pakiet przykryje funkcje z wcześniej wczytanego. Wywołanie przykrytej funkcji jest możliwe poprzez zapis nazwa_pakietu::nazwa_funkcji.

Korzystanie z pakietu i zasad tidyverse to dużo bardziej czytelny kod w porównaniu do wbudowanych funkcji. Poniżej przedstawiony jest przykład przetwarzania danych polegający na filtrowaniu, wyborze kolumn oraz utworzeniu nowej zmiennej.

data("ChickWeight")

# bez pakietu tidyverse

chick_15 <- ChickWeight[ChickWeight$Chick=="15",]
chick_15 <- chick_15[c("weight", "Time", "Diet"),]
chick_15$weight_kg <- chick_15$weight/1000

# z pakietem tidyverse

chick_15 <- ChickWeight %>%
  filter(Chick=="15") %>%
  select(-Chick) %>%
  mutate(weight_kg=weight/1000)

Rozwiązanie z wykorzystaniem wbudowanych funkcji to 133 znaki, natomiast wykorzystanie tidyverse to 30% oszczędność miejsca i tylko 92 znaki.

3.2 Import danych

Wczytywanie danych do R jest możliwe z wielu różnych źródeł. Funkcje, które to umożliwiają zwykle mają nazwę rozpoczynającą się od read.

Będziemy korzystać z następujących zbiorów danych:

  • movies - plik tekstowy zawierający informacje o filmach,
  • bank - plik excel zawierający dane dot. kampanii marketingowej banku, opis zmiennych,
  • rossmann - plik excel zawierający dane ze sklepów Rossmann,
  • lotto - plik tekstowy zawierający dane z losowań Lotto.

3.2.1 Pliki CSV

Do wczytywania plików csv można wykorzystać wbudowaną funkcję read.csv() lub tą pochodzącą z pakietu readr - read_csv(). W obu przypadkach wynik wczytania będzie podobny.

movies <- read.csv("data/movies.csv")

movies2 <- read_csv("data/movies.csv")
Rows: 2961 Columns: 11
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): title, genre, director
dbl (8): year, duration, gross, budget, cast_facebook_likes, votes, reviews,...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Jeśli nas plik ma nietypową strukturę to w funkcji read.csv() możemy określić dodatkowe argumenty informując o nazwach kolumn obecnych w pliku (header =), separatorze kolumn (sep =) lub separatorze miejsc dziesiętnych (dec =)

movies <- read.csv(file = "data/movies.csv", header = T, sep=",", dec=".")

3.2.2 Pliki excel

Do wczytywania plików z Excela niezbędny jest dodatkowy pakiet readxl. W funkcji read_xlsx() podajemy jako argument nazwę pliku. Możemy także dodać nazwę lub numer arkusza w argumencie (sheet =) oraz zakres komórek jako wartość argumentu range =.

library(readxl)

bank <- read_xlsx("data/bank.xlsx")

# bank <- read_xlsx("data/bank.xlsx", sheet = "dane")
# bank <- read_xlsx("data/bank.xlsx", sheet = 1)

bank_a1i30 <- read_xlsx("data/bank.xlsx", range = "A1:I30")

rossmann <- read_xlsx("data/rossmann.xlsx")

3.2.3 Pliki tekstowe

Z kolei do wczytywania plików tekstowych wykorzystuje się funkcję read.table(). Wczytywany plik nie musi być zlokalizowany na dysku twardym - może to być link internetowy.

lotto <- read.table("http://www.mbnet.com.pl/dl.txt")
names(lotto) <- c("lp", "data", "numery")

3.3 Filtrowanie

Do przetwarzania danych służą funkcje z pakietu dplyr. Większość z nich jako pierwszy argument przyjmuje przetwarzany zbiór danych, ale można tego uniknąć wykorzystując symbole %>%.

Filtrowanie polega na wybraniu obserwacji, które spełniają określony warunek lub warunki. Ze zbioru movies wybierzmy wszystkie komedie:

komedie <- filter(movies, genre=="Comedy")

lub alternatywnie:

komedie <- movies %>%
  filter(genre=="Comedy")

Po zmiennej, która jest filtrowana musimy podać operator porównania czyli podwójny znak równości ==. Jeśli chcemy filtrować po większej liczbie zmiennych to kolejne warunki dodajemy po przecinku:

komedie_2012 <- movies %>%
  filter(genre=="Comedy", year==2012)

Wówczas oba warunki muszą zostać spełnione czyli pomiędzy nimi zachodzi relacja i. Równoważny zapis jest następujący:

komedie_2012 <- movies %>%
  filter(genre=="Comedy" & year==2012)

Pomiędzy warunkami może także zachodzić relacja lub. Wybieramy filmy, które są komediami lub miały swoją premierę w 2012 roku.

komedie_l_2012 <- movies %>%
  filter(genre=="Comedy" | year==2012)

Możliwy jest także wybór wielu kryteriów filtrowania poprzez operator %in%:

komedie_familijne <- movies %>%
  filter(genre %in% c("Comedy", "Family"))

movies_2000_2010 <- movies %>%
  filter(year %in% 2000:2010)

3.4 Wybieranie kolumn

Do wyboru kolumn służy funkcja select(). Zmodyfikujemy wcześniej utworzony zbiór komedie:

komedie <- movies %>%
  filter(genre=="Comedy") %>%
  select(title, year, duration, budget, rating)

Ten sam kod możemy zapisać zagnieżdżając funkcje, ale traci on w ten sposób na czytelności:

komedie <- select(filter(movies, genre=="Comedy"), title, year, duration, budget, rating)

Możemy także wskazać, które zmienne nie mają znaleźć się w zbiorze wynikowym:

komedie <- movies %>%
  filter(genre=="Comedy") %>%
  select(-genre)

Natomiast jeśli zmiennych jest więcej to musimy jest umieścić w wektorze, żeby nie pisać przed każdą zmienną znaku minus:

komedie <- movies %>%
  filter(genre=="Comedy") %>%
  select(-genre, -director, -gross, -budget)


komedie <- movies %>%
  filter(genre=="Comedy") %>%
  select(-c(genre, director, gross, budget))

Z wykorzystaniem znaku dwukropka możemy także wskazywać zakresy zmiennych:

komedie <- movies %>%
  filter(genre=="Comedy") %>%
  select(-genre, -c(gross:reviews))

3.5 Tworzenie nowych zmiennych

Do utworzenia nowej zmiennej wykorzystuje się funkcję mutate(). Utwórzmy w naszym zbiorze nową zmienną, która będzie zawierała czas trwania filmu w godzinach:

komedie <- movies %>%
  filter(genre=="Comedy") %>%
  select(-genre, -c(gross:reviews)) %>%
  mutate(dur_hour = duration/60)

Rozsądnie będzie zaokrąglić otrzymaną wartość do jednego miejsca po przecinku - służy do tego funkcja round():

komedie <- movies %>%
  filter(genre=="Comedy") %>%
  select(-genre, -c(gross:reviews)) %>%
  mutate(dur_hour = round(duration/60,1))

Z kolei funkcja transmute() tworzy zbiór w którym jest tylko nowo utworzona kolumna:

komedie_t <- movies %>%
  filter(genre=="Comedy") %>%
  select(-genre, -c(gross:reviews)) %>%
  transmute(dur_hour = round(duration/60,1))

3.6 Zmiana nazwy zmiennej

Do zmiany nazw zmiennych służy funkcja rename(). Najpierw podajemy nazwę nowej zmiennej, a po znaku równości starą nazwę:

bank <- bank %>%
  rename(karta=kredyt)

Zmiany nazwy można także dokonać z wykorzystaniem funkcji select:

bank_nowy <- bank %>%
  select(lokata=wynik)

W takim przypadku trzeba jednak pamiętać o wypisaniu wszystkich zmiennych, które mają się znaleźć w zbiorze wynikowym.

3.7 Podsumowanie danych

Funkcja summarise() służy do podsumowań danych w formie zagregowanej:

bank %>%
  summarise(saldo_srednia=mean(saldo),
            saldo_mediana=median(saldo))
# A tibble: 1 × 2
  saldo_srednia saldo_mediana
          <dbl>         <dbl>
1         1362.           448

Podsumowanie danych ma najwięcej sensu w połączniu z funkcją grupującą.

3.8 Grupowanie

Do grupowania obserwacji służy funkcja group_by(). Zobaczmy jak wyglądają statystyki salda w poszczególnych grupach wykształcenia:

bank %>%
  group_by(wykszt) %>%
  summarise(saldo_srednia=mean(saldo),
            saldo_mediana=median(saldo))
# A tibble: 4 × 3
  wykszt     saldo_srednia saldo_mediana
  <chr>              <dbl>         <dbl>
1 podstawowe         1251.           403
2 srednie            1155.           392
3 wyzsze             1758.           577
4 <NA>               1527.           568

Po przecinku w funkcji group_by() można wskazać kolejne zmienne grupujące:

bank %>%
  group_by(wykszt, hipoteka) %>%
  summarise(saldo_srednia=mean(saldo),
            saldo_mediana=median(saldo))
`summarise()` has grouped output by 'wykszt'. You can override using the
`.groups` argument.
# A tibble: 8 × 4
# Groups:   wykszt [4]
  wykszt     hipoteka saldo_srednia saldo_mediana
  <chr>      <chr>            <dbl>         <dbl>
1 podstawowe nie              1571.          521 
2 podstawowe tak              1008.          344.
3 srednie    nie              1340.          416.
4 srednie    tak              1034.          380 
5 wyzsze     nie              1919.          618 
6 wyzsze     tak              1584.          543 
7 <NA>       nie              1780.          679 
8 <NA>       tak              1207.          442 

Przydatna jest także funkcja n(), która nie przyjmuje żadnego argumentu i zwraca liczebność zbioru bądź grupy.

bank %>%
  group_by(wykszt) %>%
  summarise(liczebnosc=n(),
            saldo_srednia=mean(saldo),
            saldo_mediana=median(saldo))
# A tibble: 4 × 4
  wykszt     liczebnosc saldo_srednia saldo_mediana
  <chr>           <int>         <dbl>         <dbl>
1 podstawowe       6851         1251.           403
2 srednie         23202         1155.           392
3 wyzsze          13301         1758.           577
4 <NA>             1857         1527.           568

Jeżeli chcemy tylko wyznaczyć liczebności grup to możemy skorzystać z funkcji count():

bank %>%
  group_by(wykszt) %>%
  count()
# A tibble: 4 × 2
# Groups:   wykszt [4]
  wykszt         n
  <chr>      <int>
1 podstawowe  6851
2 srednie    23202
3 wyzsze     13301
4 <NA>        1857

Jedną z kategorii zmiennej wykształcenie jest brak danych (NA). Zamienimy tą wartość na kategorię nieustalone z wykorzystaniem funkcji mutate() oraz if_else(). Funkcja if_else() przyjmuje trzy argumenty - pierwszy (condition =) to warunek, który jest weryfikowany, następnie podajemy wartość, która ma być wprowadzona w przypadku spełnienia warunku (true =), a na końcu wartość dla niespełnionego warunku (false =). Jest to odpowiednik funkcji JEŻELI z Excela.

W omawianym przykładzie warunkiem jest sprawdzenie czy wartości zmiennej wykszt są równe NA. Jeśli tak to na ich miejsce wprowadzany jest tekst nieustalone, a w przeciwnym przypadku pozostaje oryginalna wartość.

bank %>%
  mutate(wykszt=if_else(is.na(wykszt), "nieustalone", wykszt)) %>%
  group_by(wykszt) %>%
  count()
# A tibble: 4 × 2
# Groups:   wykszt [4]
  wykszt          n
  <chr>       <int>
1 nieustalone  1857
2 podstawowe   6851
3 srednie     23202
4 wyzsze      13301

3.9 Sortowanie

Sortowanie jest możliwe z wykorzystaniem funkcji arrange(). Jako argument podajemy zmienną według, której chcemy posortować zbiór. Domyślne zbiór sortowany jest rosnąco - od wartości najmniejszych do największych:

bank_sort <- bank %>%
  arrange(saldo)

Zmiana kierunku sortowania jest możliwa po zastosowaniu funkcji desc():

bank_sort <- bank %>%
  arrange(desc(saldo))

Sortowanie możemy także zastosować do wyników podsumowania danych:

bank %>%
  group_by(wykszt) %>%
  summarise(liczebnosc=n(),
            saldo_srednia=mean(saldo),
            saldo_mediana=median(saldo)) %>%
  arrange(saldo_srednia)
# A tibble: 4 × 4
  wykszt     liczebnosc saldo_srednia saldo_mediana
  <chr>           <int>         <dbl>         <dbl>
1 srednie         23202         1155.           392
2 podstawowe       6851         1251.           403
3 <NA>             1857         1527.           568
4 wyzsze          13301         1758.           577

3.10 Łączenie zbiorów

W celu zaprezentowania funkcji łączących dane przygotujemy kilka zbiorów pomocniczych:

praca_czas <- bank %>%
  group_by(praca) %>%
  summarise(sr_czas=mean(czas))

praca_saldo <- bank %>%
  group_by(praca) %>%
  summarise(sr_saldo=mean(saldo))

zawod_saldo <- bank %>%
  rename(zawod=praca) %>%
  group_by(zawod) %>%
  summarise(sr_saldo=mean(saldo))

Do łączenia dwóch zbiorów danych służy funkcja inner_join(), która jako argumenty przyjmuje nazwy zbiorów danych oraz klucz łączenia. Jeśli w obu zbiorach występują kolumny o takich samych nazwach to zostaną potraktowane jako klucz łączenia:

praca_czas_saldo <- inner_join(praca_czas, praca_saldo)
Joining with `by = join_by(praca)`

Jeśli takie kolumny nie będą istniały to wywołanie funkcji zwróci błąd:

praca_czas_saldo <- inner_join(praca_czas, zawod_saldo)
Error in `inner_join()`:
! `by` must be supplied when `x` and `y` have no common variables.
ℹ Use `cross_join()` to perform a cross-join.

W takich przypadku należy wskazać klucz połączenia w postaci by = c("id1"="id2"):

praca_czas_saldo <- inner_join(praca_czas, zawod_saldo, by=c("praca"="zawod"))

Jeśli w jednym ze zbiorów nie ma wszystkich identyfikatorów, które znajdują się w drugim zbiorze to zastosowanie funkcji inner_join() będzie skutkowało zbiorem, w którym znajdą się tylko te obserwacje, które udało się połączyć.

praca_saldo_1500 <- praca_saldo %>%
  filter(sr_saldo > 1500)

inner_join(praca_czas, praca_saldo_1500, by="praca")
# A tibble: 6 × 3
  praca sr_czas sr_saldo
  <dbl>   <dbl>    <dbl>
1     2    289.    1522.
2     3    254.    1764.
3     5    256.    1521.
4     7    268.    1648.
5     8    287.    1984.
6    NA    238.    1772.

Jeśli chcemy pozostawić niedopasowane obserwacje to należy wykorzystać jedną z funkcji - left_join() lub right_join() w zależności od tego dla którego zbioru chcemy pozostawić wszystkie informacje.

left_join(praca_czas, praca_saldo_1500, by="praca")
# A tibble: 11 × 3
   praca sr_czas sr_saldo
   <dbl>   <dbl>    <dbl>
 1     1    247.      NA 
 2     2    289.    1522.
 3     3    254.    1764.
 4     4    246.      NA 
 5     5    256.    1521.
 6     6    263.      NA 
 7     7    268.    1648.
 8     8    287.    1984.
 9     9    253.      NA 
10    10    257.      NA 
11    NA    238.    1772.

3.11 Szeroka i wąska reprezentacja danych

Do wyjaśnienia kwestii szerokiej i wąskiej reprezentacji danych posłużymy się danymi z GUS dotyczącymi przeciętnego miesięcznego spożycie wybranych artykułów żywnościowych na 1 osobę w 2016 roku - plik.

spozycie <- read_xlsx("data/spozycie.xlsx")

Taka tabela jest przykładem szerokiej reprezentacji danych. Z kolei w niektórych sytuacjach wygodnie jest korzystać z wąskiej reprezentacji danych, a niektóre pakiety wręcz wymagają takich zbiorów wejściowych.

Do transformacji danych z reprezentacji szerokiej na wąską służy funkcja gather() (pol. gromadzić). Kluczowe są w niej dwa argumenty - pierwszy (key) określa nazwę nowej kolumny, która będzie zawierała nazwy zmiennych, a drugi (value) określa nazwę nowej kolumny, która będzie zawierała wartości zmiennych. Jako kolejne argumenty podaje się nazwy kolumn, które mają być transformowane lub nazwy kolumn ze znakiem minus -, które nie mają być transformowane.

spozycie_waskie <- spozycie %>%
  gather(artykul, spozycie, mieso, owoce, warzywa)

# spozycie_waskie <- spozycie %>%
#   gather(artykul, spozycie, -kod, -nazwa)

W takiej formie łatwiej podsumować dane:

spozycie_waskie %>%
  group_by(artykul) %>%
  summarise(sr_spozycie=mean(spozycie))
# A tibble: 3 × 2
  artykul sr_spozycie
  <chr>         <dbl>
1 mieso          5.47
2 owoce          3.65
3 warzywa        8.85

W porównaniu do szerokiej reprezentacji danych:

spozycie %>%
  summarise(sr_spozycie_mieso=mean(mieso),
            sr_spozycie_owoce=mean(owoce),
            sr_spozycie_warzywa=mean(warzywa))
# A tibble: 1 × 3
  sr_spozycie_mieso sr_spozycie_owoce sr_spozycie_warzywa
              <dbl>             <dbl>               <dbl>
1              5.47              3.65                8.85

Transformacja z wąskiej do szerokiej reprezentacji danych jest możliwa z zastosowaniem funkcji spread() (pol. rozprzestrzeniać). W przypadku tej funkcji niezbędne są dwa argumenty - pierwszy (key) wskazuje kolumnę zawierającą nazwy dla nowych zmiennych, a drugi argument (value) wskazuje kolumnę zawierającą wartości dla nowych zmiennych.

spozycie_szerokie <- spozycie_waskie %>%
  spread(artykul, spozycie)

3.12 Eksport danych

Zapis zbioru danych do zewnętrznego pliku jest możliwy z wykorzystaniem funkcji write.table(). Jako argumenty tej funkcji określamy: zbiór danych (x), docelowe miejsce na dysku i nazwę pliku (file), separator kolumn (sep), separator miejsc dziesiętnych (dec) oraz argument row.names = FALSE, dzięki któremu unikniemy dodatkowych numerów wierszy.

write.table(spozycie_waskie, file = "data/spozycie_w.csv", sep=";", dec=",", row.names=F)

Taki plik jest plikiem csv, który możemy otworzyć w Excelu i zapisać go z rozszerzeniem .xlsx. Teoretycznie istnieje pakiet xlsx, który umożliwia zapisywanie zbiorów od razu do Excela, ale działa w oparciu o Javę, co bywa problematyczne.

3.13 Zadania

3.13.1 Rossmann

Na podstawie zbioru Rossmann odpowiedź na pytania:

  1. Ile było sklepów o asortymencie rozszerzonym w dniu 25-02-2014?
  2. W jaki dzień tygodnia średnia liczba klientów była największa w sklepie nr 101?
  3. Sklep jakiego typu charakteryzuje się największą medianą sprzedaży?
  4. Czy w ciągu roku odległość do najbliższego sklepu konkurencji zmieniła się dla jakiegokolwiek sklepu Rossmann?
  5. Połącz dane ze sklepów Rossmann z danymi o średnim kursie EUR/PLN z 2014 roku, który można pobrać ze strony NBP. Przelicz wielkość sprzedaży na złotówki.

3.13.2 Wybory 2020

Na podstawie zbioru dotyczącego wyborów prezydenckich w 2020 roku odpowiedź na pytania:

  1. Ile obwodów głosowania miało frekwencję powyżej 80%?
  2. Ile obwodów głosowania znajduje się w Poznaniu?
  3. Ile jest obwodów według typu obszaru?
  4. Jaka była średnia frekwencja w województwach?
  5. Gdzie była największa różnica pomiędzy kandydatami?

3.13.3 Mistrzostwa Świata

Na podstawie zbioru dotyczącego wyników meczów rozegranych w ramach Mistrzostw Świata odpowiedź na pytania:

  1. Ile razy Włochy grały w finale MŚ?
  2. Jaka jest największa liczba bramek w jednym meczu?
  3. Jakie miasto najczęściej gościło piłkarzy?
  4. Jaka była średnia liczba widzów?
  5. Ile było meczów, w których drużyna prowadząca po pierwszej połowie ostatecznie przegrywała?