Sesija 06: {dplyr} i relacione strukture podataka u R

Fidbek se upućuje na goran.milovanovic@datakolektiv.com. Ova sveščica prati kurs Uvod u R programiranje za analizu podataka 2020/21.


U Sesiji 06 bavimo se relacionim strukturama podataka u programskom jeziku R kroz {tidyverse} paket {dplyr}. Relacione strukture podataka podrazumevaju rad kojim artikulišemo više tabela - ako govorimo o relacionim bazama podataka kojima uobičajeno pristupamo nekom verzijom SQL jezika - odn. više objekata data.frame klase ako govorimo o programskom jeziku R. Važno: relacione strukture su veoma važne i bez njih je nezamisliv svakodnevni rad u Data Science. Pratićemo prilično ono sadržaj poglavlja 13. Relational Data iz knjige R for Data Science koju koristimo na ovom kursu. Ako imate prethodnog iskustva u radu sa SQL, ovo će za vas biti verovatno lako; ako nemate, savetuje se da pročitate poglavlje 13. pažljivo pre početka ove sesije našeg kursa.

0. Osnove: ključevi, left_join() i right_join() funkcije

library(tidyverse)

Pogledajmo sledeće dve tabele:

employees <- data.frame(id = 1:5,
                        name = c("Ana", "Marija", "Petar", "Marko", "Jelena"),
                        family_name = c("Petrovic", "Markovic", "Stosic", "Manojlovic", "Petrovic"),
                        salary = c(1000, 700, 700, 1100, 1000),
                        dept = c("English", "Italian", "English", "Serbian", "Russian"), 
                        stringsAsFactors = FALSE)
print(employees)
##   id   name family_name salary    dept
## 1  1    Ana    Petrovic   1000 English
## 2  2 Marija    Markovic    700 Italian
## 3  3  Petar      Stosic    700 English
## 4  4  Marko  Manojlovic   1100 Serbian
## 5  5 Jelena    Petrovic   1000 Russian
departments <- data.frame(id = 1:4,
                          dept = c("English", "Serbian", "Italian", "Russian"),
                          size = c(20, 10, 7, 13),
                          stringsAsFactors = FALSE)
print(departments)
##   id    dept size
## 1  1 English   20
## 2  2 Serbian   10
## 3  3 Italian    7
## 4  4 Russian   13

Pretpostavimo da treba da uporedimo visinu primanja koju nalazimo u employees$salary sa veličinom svakog departmana u kompaniji. Mi te podatke u ovom minimalističkom primeru nemamo na jednom mestu: visine primanja zaposlenih nalazimo u employees dok veličine departmana nalazimo u departments$size. Šta radimo?

Ako pogledamo obe tabele, videćemo da se kolona dept nalazi u obe. Obe tabele takođe sadrže kolonu id, ali tu moramo da budemo veoma oprezni: dok u tabeli employees to polje identifikuje svakog zaposlenog, istoimeno polje u departments idenitifikuje odeljenja u kompaniji - a ne zaposlene.

Srećom, vidimo da se polje dept u obe tabele poklapa po vrednostima i idenitifikuje odeljenje i u jednoj, i u drugoj tabeli. Ono će igrati ulogu ključa u jednostavnoj left_join() operaciji u kojoj ćemo podatke iz tabele employees dodati podacima u tabeli departments. U ovoj operaciji, tabela departments igra ulogu leve, a tabela employees ulogu desne tabele. Pogledajmo:

departments <- left_join(departments,
                         select(employees,
                                salary,
                                dept),
                         by = "dept")
print(departments)
##   id    dept size salary
## 1  1 English   20   1000
## 2  1 English   20    700
## 3  2 Serbian   10   1100
## 4  3 Italian    7    700
## 5  4 Russian   13   1000

Pažljivo pogledajmo šta se dogodilo:

  • prvo, vidimo da su se podaci iz desne tabele employees preselili i u levu tabelu depts;
  • drugo, primetimo da je tabela depts u početku imala četiri reda, dok ih sada ima pet;
  • to je posledica činjenice da su se u desnoj tabeli emplyees našle dve opservacije - Ana i Petar, preciznije - koje su na dept imale vrednost English koju smo prethodno nalazili samo jednom u levoj tabeli departments;
  • iz čega zaključujemo da operacija (tehnički: ipak funkcija : ) left_join() unosi nove redove u levu tabelu ukoliko postoji više opservacija u desnoj tabeli koje zadovoljavaju ključ u levoj!
  • Takođe, primetite da sada postoje id.x i id.y u departments: {dplyr} left_join() je našla kolonu po imenu id u obe tabele, pa je promenila u levoj tabeli id u id.x a iz desne tabele id prenela u id.y kako bismo mogli da ih razlikujemo (napomena: iako je ovakvo ponašanje funkcije po difoltu korisno, ovakva promena imena kolona je nešto što u principu želite da izbegnete jer unosi konfuziju u imena kolona tabele sa kojom nastavljate obradu podataka).

Analiza ovde nije mnogo korisna pošto imamo samo po jednu opservaciju na vrednostima Italian, Russian, i Serbian u ovom minimalističkom primeru, ali evo:

departments %>% 
  select(dept, salary) %>% 
  group_by(dept) %>% 
  summarise(mean_salary = mean(salary),
            median_salary = median(salary), 
            n_obs = n())

Naravno, da biste izbegli dovlačenje id kolone iz desne u levu tabelu - što je proizvelo pojavu kolona id.x i id.y u rezultatu primene left_join():

# - ponovo ćemo napraviti tabelu departments:
departments <- data.frame(id = 1:4,
                          dept = c("English", "Serbian", "Italian", "Russian"),
                          size = c(20, 10, 7, 13),
                          stringsAsFactors = FALSE)
# - ide left_join() sa pipe operatorom:
departments <- departments %>% 
  left_join(select(employees, 
                   -id),
            by = "dept")
print(departments)
##   id    dept size   name family_name salary
## 1  1 English   20    Ana    Petrovic   1000
## 2  1 English   20  Petar      Stosic    700
## 3  2 Serbian   10  Marko  Manojlovic   1100
## 4  3 Italian    7 Marija    Markovic    700
## 5  4 Russian   13 Jelena    Petrovic   1000

Funkcija left_join() ima sestru koja se (zamislite) zove right_join(). Ponovo ćemo napraviti naše tabele kakve su bile pre left_join():

employees <- data.frame(id = 1:5,
                        name = c("Ana", "Marija", "Petar", "Marko", "Jelena"),
                        family_name = c("Petrovic", "Markovic", "Stosic", "Manojlovic", "Petrovic"),
                        salary = c(1000, 700, 700, 1100, 1000),
                        dept = c("English", "Italian", "English", "Serbian", "Russian"), 
                        stringsAsFactors = FALSE)
print(employees)
##   id   name family_name salary    dept
## 1  1    Ana    Petrovic   1000 English
## 2  2 Marija    Markovic    700 Italian
## 3  3  Petar      Stosic    700 English
## 4  4  Marko  Manojlovic   1100 Serbian
## 5  5 Jelena    Petrovic   1000 Russian
departments <- data.frame(id = 1:4,
                          dept = c("English", "Serbian", "Italian", "Russian"),
                          size = c(20, 10, 7, 13),
                          stringsAsFactors = FALSE)
print(departments)
##   id    dept size
## 1  1 English   20
## 2  2 Serbian   10
## 3  3 Italian    7
## 4  4 Russian   13

Isti rezultat kao prethodni možemo da dobijemo primenom right_join() i zamenom mesta argumenata:

# - ide right_join() sa pipe operatorom:
departments <- select(employees, -id) %>% 
  right_join(departments,
            by = "dept")
print(departments)
##     name family_name salary    dept id size
## 1    Ana    Petrovic   1000 English  1   20
## 2 Marija    Markovic    700 Italian  3    7
## 3  Petar      Stosic    700 English  1   20
## 4  Marko  Manojlovic   1100 Serbian  2   10
## 5 Jelena    Petrovic   1000 Russian  4   13

Obratite pažnju: u rezultatu se promenila samo jedna stvar. Koja?

1. Join po dva ključa i po dva ključa koji se različito zovu

Ponekad je potrebno spojiti dve tabele preko dva ključa: na primer, po imenu, i prezimenu korisnika nekog sistema ili zaposlenog. Ponekad - a to se najčešće dešava kada ste “prljavo programirali”, ne planirajući dobro vaše strukture podataka - treba uraditi join operaciju na suštinski istom ključu koji se u dve tabele javlja pod dva različita imena.

Razmotrimo prvo join po dva ključa. Već znamo employees tabelu:

employees <- data.frame(id = 1:5,
                        name = c("Ana", "Marija", "Petar", "Marko", "Jelena"),
                        family_name = c("Petrovic", "Markovic", "Stosic", "Manojlovic", "Petrovic"),
                        salary = c(1000, 700, 700, 1100, 1000),
                        dept = c("English", "Italian", "English", "Serbian", "Russian"), 
                        stringsAsFactors = FALSE)
print(employees)
##   id   name family_name salary    dept
## 1  1    Ana    Petrovic   1000 English
## 2  2 Marija    Markovic    700 Italian
## 3  3  Petar      Stosic    700 English
## 4  4  Marko  Manojlovic   1100 Serbian
## 5  5 Jelena    Petrovic   1000 Russian

Sad da dodamo još jednu tabelu:

employees_bd <- data.frame(name = c("Ana", "Marija", "Petar", "Marko", "Jelena"),
                           family_name = c("Petrovic", "Markovic", "Stosic", "Manojlovic", "Petrovic"),
                           birthday = c("1991-10-08", 
                                        "1985-07-10", 
                                        "1991-04-03", 
                                        "1991-12-01", 
                                        "1991-12-20"),
                           stringsAsFactors = FALSE)
print(employees_bd)
##     name family_name   birthday
## 1    Ana    Petrovic 1991-10-08
## 2 Marija    Markovic 1985-07-10
## 3  Petar      Stosic 1991-04-03
## 4  Marko  Manojlovic 1991-12-01
## 5 Jelena    Petrovic 1991-12-20

Ok, druga tabela employees_bd ne samo što nema id kolonu sa vrednostima koje bi pomogle da ona igra ulogu ključa za spajanje sa employees tabelom - već ona nema id kolonu uopšte! Ako želimo da podatke iz employees dodamo podacima u employees_bd ne preostaje nam ništa drugo do da ih spajamo po dve kolone koje obe tabele sadrže: name i family_name:

employees_bd <- employees_bd %>%
  left_join(employees,
            by = c("name", "family_name"))
print(employees_bd)
##     name family_name   birthday id salary    dept
## 1    Ana    Petrovic 1991-10-08  1   1000 English
## 2 Marija    Markovic 1985-07-10  2    700 Italian
## 3  Petar      Stosic 1991-04-03  3    700 English
## 4  Marko  Manojlovic 1991-12-01  4   1100 Serbian
## 5 Jelena    Petrovic 1991-12-20  5   1000 Russian

A šta ako smo baš zabrljali pa employees_bd tabela izgleda ovako:

employees_bd <- data.frame(ime = c("Ana", "Marija", "Petar", "Marko", "Jelena"),
                           prezime = c("Petrovic", "Markovic", "Stosic", "Manojlovic", "Petrovic"),
                           birthday = c("1991-10-08", 
                                        "1985-07-10", 
                                        "1991-04-03", 
                                        "1991-12-01", 
                                        "1991-12-20"),
                           stringsAsFactors = FALSE)
print(employees_bd)
##      ime    prezime   birthday
## 1    Ana   Petrovic 1991-10-08
## 2 Marija   Markovic 1985-07-10
## 3  Petar     Stosic 1991-04-03
## 4  Marko Manojlovic 1991-12-01
## 5 Jelena   Petrovic 1991-12-20

Onda se to u {dplyr} radi ovako:

employees_bd <- employees_bd %>%
  left_join(employees,
            by = c("ime" = "name", 
                   "prezime" = "family_name"))
print(employees_bd)
##      ime    prezime   birthday id salary    dept
## 1    Ana   Petrovic 1991-10-08  1   1000 English
## 2 Marija   Markovic 1985-07-10  2    700 Italian
## 3  Petar     Stosic 1991-04-03  3    700 English
## 4  Marko Manojlovic 1991-12-01  4   1100 Serbian
## 5 Jelena   Petrovic 1991-12-20  5   1000 Russian

Razmislite: kako je left_join() odabrala da imenuje kolone rezultata u ovom primeru?

2. Značaj planiranja relacionih struktura

A da je Perica odmah odabrao da ove dve tabele imaju iste id kolone, što bi svako ko pažljivo planira relacione strukture za analizu uradio…

employees_bd <- data.frame(id = 1:5,
                           ime = c("Ana", "Marija", "Petar", "Marko", "Jelena"),
                           prezime = c("Petrovic", "Markovic", "Stosic", "Manojlovic", "Petrovic"),
                           birthday = c("1991-10-08", 
                                        "1985-07-10", 
                                        "1991-04-03", 
                                        "1991-12-01", 
                                        "1991-12-20"),
                           stringsAsFactors = FALSE)
employees_bd <- employees_bd %>%
  left_join(employees,
            by = "id")
print(employees_bd)
##   id    ime    prezime   birthday   name family_name salary    dept
## 1  1    Ana   Petrovic 1991-10-08    Ana    Petrovic   1000 English
## 2  2 Marija   Markovic 1985-07-10 Marija    Markovic    700 Italian
## 3  3  Petar     Stosic 1991-04-03  Petar      Stosic    700 English
## 4  4  Marko Manojlovic 1991-12-01  Marko  Manojlovic   1100 Serbian
## 5  5 Jelena   Petrovic 1991-12-20 Jelena    Petrovic   1000 Russian

Ne, Perice, kad planiraš relacione strukture ne želiš da udvajaš podatke, ne trebaju ti ime i name, prezime i family name; relacione strukture naime služe upravo tome da se uštedi u reprezentaciji podataka, da se ona optimizuje, a ne tako… Ovako:

employees_bd <- data.frame(id = 1:5,
                           birthday = c("1991-10-08", 
                                        "1985-07-10", 
                                        "1991-04-03", 
                                        "1991-12-01", 
                                        "1991-12-20"),
                           stringsAsFactors = FALSE)
print(employees_bd)
##   id   birthday
## 1  1 1991-10-08
## 2  2 1985-07-10
## 3  3 1991-04-03
## 4  4 1991-12-01
## 5  5 1991-12-20

I onda samo:

employees_bd <- employees_bd %>%
  left_join(employees,
            by = "id")
print(employees_bd)
##   id   birthday   name family_name salary    dept
## 1  1 1991-10-08    Ana    Petrovic   1000 English
## 2  2 1985-07-10 Marija    Markovic    700 Italian
## 3  3 1991-04-03  Petar      Stosic    700 English
## 4  4 1991-12-01  Marko  Manojlovic   1100 Serbian
## 5  5 1991-12-20 Jelena    Petrovic   1000 Russian

R Markdown

R Markdown je ono što koristimo da bismo razvili ove sveščice. Evo knjige iz koje se može naučiti rad u toj jednostavnoj ekstenziji R: R Markdown: The Definitive Guide, Yihui Xie, J. J. Allaire, Garrett Grolemunds..


Goran S. Milovanović, Data Scientist & Vlasnik, DataKolektiv.
Kontakt: goran.milovanovic@datakolektiv.com. Ovo je besplatan i slobodan softver: GPL v2.0.