Sada je vreme da naučimo nešto o data wrangling-u, veštini dobijanja podataka u korisnom obliku za vizuelizaciju i modelovanje. Wrangling je veoma važan, bez toga ne možemo raditi sa svojim podacima! Postoje tri glavna dela wrangling-a:

Tiblovi

Kao što smo već pomenuli, tiblovi su baze podataka, ali podešene malo drugačije od običnih, kako bi nam rad bio lakši. R je star jezik, a neke stvari koje su bile korisne pre 10 ili 20 godina, sada otežavaju rad. Komplikovano je promeniti bazu R-a, pa se većina inovacija pojavljuje u paketima. Ovde ćemo opisati tibble paket. Ako želite da saznate nešto više o tiblovima, kucajte vignette("tibble").

Kreiranje tiblova

Kada smo radili sa dplyr paketom koristili smo tiblove, a koristićemo ih i sada. Međutim, većina drugih paketa koristi obične baze, ali to nije problem jer lako možemo konvertovati običnu bazu u tibl, koristeći as.tibble().

library(readr)
as_tibble(iris)
## # A tibble: 150 x 5
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
##           <dbl>       <dbl>        <dbl>       <dbl> <fct>  
##  1          5.1         3.5          1.4         0.2 setosa 
##  2          4.9         3            1.4         0.2 setosa 
##  3          4.7         3.2          1.3         0.2 setosa 
##  4          4.6         3.1          1.5         0.2 setosa 
##  5          5           3.6          1.4         0.2 setosa 
##  6          5.4         3.9          1.7         0.4 setosa 
##  7          4.6         3.4          1.4         0.3 setosa 
##  8          5           3.4          1.5         0.2 setosa 
##  9          4.4         2.9          1.4         0.2 setosa 
## 10          4.9         3.1          1.5         0.1 setosa 
## # ... with 140 more rows

Možemo kreirati novi tibl iz pojedinačnih vektora sa tibble(). Ova funkcija će automatski produžiti ulaz dužine 1, a dozvoljava i da pozivamo promenljivu koju smo upravo kreirali:

tibble(
  x = 1:5, 
  y = 1, 
  z = x ^ 2 + y
)
## # A tibble: 5 x 3
##       x     y     z
##   <int> <dbl> <dbl>
## 1     1     1     2
## 2     2     1     5
## 3     3     1    10
## 4     4     1    17
## 5     5     1    26

Moguće je da tibl ima imena kolona koje nisu validna imena promenljivih u R-u, tj. ne-sintaksna imena. Na primer, možda ne počinju slovom, ili možda sadrže neobične karaktere kao što je razmak. Da bismo pozvali ove promenljive, moramo ih okružiti sa simbolom `.

tb = tibble(
  `:)` = "smile", 
  ` ` = "space",
  `2000` = "number"
)
tb
## # A tibble: 1 x 3
##   `:)`  ` `   `2000`
##   <chr> <chr> <chr> 
## 1 smile space number

Takođe, trebaće nam ovaj simbol kad radimo sa ovim promenljivama u drugim paketima, poput ggplot2, dplyr i tidyr.

Drugi način za kreiranje tiblova je tribble(), skraćenica za transponovani tibl. Funkcija tribble() radi na sledeći način: zaglavlja kolona su definisana formulama (tj. počinju sa ~), a unosi su razdvojeni zarezima. Ovo omogućava da se prikaže mala količina podataka u lako čitljivom obliku. Korisno je dodati komentar da bismo bili potpuno sigurni gde je zaglavlje.

tribble(
  ~x, ~y, ~z,
   #--|--|----
  "a", 2, 3.6,
  "b", 1, 8.5
)
## # A tibble: 2 x 3
##   x         y     z
##   <chr> <dbl> <dbl>
## 1 a         2   3.6
## 2 b         1   8.5

Tiblovi vs data.frame

Dve glavne razlike u korišćenju tibla u odnosu na klasični data.frame su prikazivanje i izdvajanje podskupova podataka.

Prikazivanje

Tiblovi imaju drugačiji način prikazivanja - prikazuje se samo prvih 10 redova i sve kolone koje staju na ekran. Ovo puno olakšava rad sa bazama koje sadrže veliku količinu podataka. Pored imena, za svaku kolonu se prikazuje i tip, lepa karakteristika pozajmljenja iz str().

library(lubridate)
tibble(
  a = now() + runif(1000) * 86400,
  b = today() + runif(1000) * 30,
  c = 1:1000,
  d = runif(1000),
  e = sample(letters, 1000, replace = TRUE)
)
## # A tibble: 1,000 x 5
##    a                   b              c     d e    
##    <dttm>              <date>     <int> <dbl> <chr>
##  1 2019-04-07 10:46:54 2019-04-27     1 0.519 m    
##  2 2019-04-07 01:30:29 2019-04-28     2 0.162 l    
##  3 2019-04-07 19:09:50 2019-04-15     3 0.545 d    
##  4 2019-04-07 11:40:49 2019-04-18     4 0.486 x    
##  5 2019-04-07 10:45:31 2019-05-05     5 0.350 f    
##  6 2019-04-06 22:24:25 2019-04-26     6 0.921 z    
##  7 2019-04-07 20:49:17 2019-04-08     7 0.235 o    
##  8 2019-04-07 02:58:12 2019-05-05     8 0.693 l    
##  9 2019-04-07 21:41:56 2019-04-29     9 0.901 v    
## 10 2019-04-07 10:53:33 2019-04-17    10 0.351 s    
## # ... with 990 more rows

Tiblovi su napravljeni tako da ne preplavimo konzolu kada prikazujemo veliku bazu podataka. Međutim, ponekad nam treba više nego što nam pruža podrazumevani prikaz. Postoji nekoliko opcija koje nam mogu pomoći.

Prvo, možemo primeniti print() na bazu i kontrolisati broj redova (n) koji želimo da prikažemo (width = Inf prikazuje sve kolone).

nycflights13::flights %>% 
  print(n = 10, width = Inf)
## # A tibble: 336,776 x 19
##     year month   day dep_time sched_dep_time dep_delay arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>
##  1  2013     1     1      517            515         2      830
##  2  2013     1     1      533            529         4      850
##  3  2013     1     1      542            540         2      923
##  4  2013     1     1      544            545        -1     1004
##  5  2013     1     1      554            600        -6      812
##  6  2013     1     1      554            558        -4      740
##  7  2013     1     1      555            600        -5      913
##  8  2013     1     1      557            600        -3      709
##  9  2013     1     1      557            600        -3      838
## 10  2013     1     1      558            600        -2      753
##    sched_arr_time arr_delay carrier flight tailnum origin dest  air_time
##             <int>     <dbl> <chr>    <int> <chr>   <chr>  <chr>    <dbl>
##  1            819        11 UA        1545 N14228  EWR    IAH        227
##  2            830        20 UA        1714 N24211  LGA    IAH        227
##  3            850        33 AA        1141 N619AA  JFK    MIA        160
##  4           1022       -18 B6         725 N804JB  JFK    BQN        183
##  5            837       -25 DL         461 N668DN  LGA    ATL        116
##  6            728        12 UA        1696 N39463  EWR    ORD        150
##  7            854        19 B6         507 N516JB  EWR    FLL        158
##  8            723       -14 EV        5708 N829AS  LGA    IAD         53
##  9            846        -8 B6          79 N593JB  JFK    MCO        140
## 10            745         8 AA         301 N3ALAA  LGA    ORD        138
##    distance  hour minute time_hour          
##       <dbl> <dbl>  <dbl> <dttm>             
##  1     1400     5     15 2013-01-01 05:00:00
##  2     1416     5     29 2013-01-01 05:00:00
##  3     1089     5     40 2013-01-01 05:00:00
##  4     1576     5     45 2013-01-01 05:00:00
##  5      762     6      0 2013-01-01 06:00:00
##  6      719     5     58 2013-01-01 05:00:00
##  7     1065     6      0 2013-01-01 06:00:00
##  8      229     6      0 2013-01-01 06:00:00
##  9      944     6      0 2013-01-01 06:00:00
## 10      733     6      0 2013-01-01 06:00:00
## # ... with 3.368e+05 more rows

Takođe, možemo podesiti podrazumevano ponašanje podešavanjem opcija:

  • options(tibble.print_max = n, tibble.print_min = m): ako ima više od n redova, prikazuje samo m redova. Možemo koristiti options(tibble.print_min = Inf) da bismo uvek ispisivali sve redove.

  • options(tibble.width = Inf) uvek prikazuje sve kolone, bez obzira na širinu ekrana.

Kompletnu listu možemo videti pozivajući package?tibble.

Poslednja opcija je, naravno, korišćenje ugrađenog RStudio “pregledača” podataka da bismo dobili prikaz kompletne baze. Ovo je uvek korisno na kraju dugog lanca manipulisanja podacima.

nycflights13::flights %>% 
  View()

Izdvajanje podskupova podataka

Ako želimo da izdvojimo jednu promenljivu, trebaće nam $ ili [[. Korišćenjem [[ se može izdvojiti po imenu ili poziciji, $ samo izdvaja po imenu, ali je malo lakše za kucanje.

df = tibble(
  x = runif(5),
  y = rnorm(5)
)

#izdvajamo po imenu
df$x
## [1] 0.2297936 0.7512522 0.2833471 0.2409461 0.8931826
#ili
df[["x"]]
## [1] 0.2297936 0.7512522 0.2833471 0.2409461 0.8931826
#izdvajamo po poziciji
df[[1]]
## [1] 0.2297936 0.7512522 0.2833471 0.2409461 0.8931826

Da bismo koristili cevi, moramo da ubacimo i specijalni “placeholder” ..

df %>% .$x
## [1] 0.2297936 0.7512522 0.2833471 0.2409461 0.8931826
df %>% .[["x"]]
## [1] 0.2297936 0.7512522 0.2833471 0.2409461 0.8931826

U poređenju sa data.frame-ovima, tiblovi su stroži: uvek pružaju upozorenje ako kolona kojoj pokušavamo da pristupimo ne postoji.

Povezivanje sa starim kodom

Neke funkcije koje su radile za data.frame-ove, ne rade za tiblove. Ako naiđemo na takvu, možemo jednostavno da koristimo as.data.frame() da se vratimo na data.frame.

class(as.data.frame(tb))
## [1] "data.frame"

Uvoz podataka

Podaci ugrađeni u R su nam jako korisni ako želimo da naučimo kako da radimo sa podacima, međutim, u nekom trenutku želimo da počnemo da radimo i sa sopstvenim podacima. Sada ćemo naučiti kako da učitamo obične tekstualne datoteke u R-u. Ovde ćemo samo ogrebati načine uvoza podataka, ali mnogi od principa se prenose i na druge oblike podataka.

Većina readr funkcija bavi se pretvaranjem datoteka u baze podataka:

Sve ove funkcije imaju istu sintaksu: kada ovladamo jednom, možemo koristiti druge sa lakoćom. Za ostatak ćemo se fokusirati na read_csv(). Ne samo da su csv fajlovi jedan od najčešćih oblika skladištenja podataka, već kada shvatimo read_csv(), možemo lako primeniti svoje znanje na sve druge funkcije u readr-u.

Prvi argument za read_csv() je najvažniji: to je putanja do datoteke za čitanje.

heights = read_csv("heights.csv")
## Parsed with column specification:
## cols(
##   earn = col_double(),
##   height = col_double(),
##   sex = col_character(),
##   ed = col_integer(),
##   age = col_integer(),
##   race = col_character()
## )

Kada pokrenemo read_csv(), štampa se ime i tip svake kolone. To je važan deo, kome ćemo se vratiti kasnije.

Takođe, možemo isporučiti csv fajl koji smo upravo kreirali. Ovo je korisno za eksperimentisanje sa readr-om i uviđanje osnovnih pravila.

read_csv("a,b,c
1,2,3
4,5,6")
## # A tibble: 2 x 3
##       a     b     c
##   <int> <int> <int>
## 1     1     2     3
## 2     4     5     6

U oba slučaja read_csv() koristi prvu liniju podataka za imena kolona, što je vrlo uobičajena konvencija. Postoje dva slučaja u kojima želimo da izmenimo ovo ponašanje:

  1. Ponekad postoji nekoliko redova metapodataka na vrhu datoteke. Možemo koristiti skip = n da preskočimo prvih n redova, ili upotrebiti comment = "#" da preskočimo sve linije koje počinju sa (npr.) #.
read_csv("The first line of metadata
  The second line of metadata
  x,y,z
  1,2,3", skip = 2)
## # A tibble: 1 x 3
##       x     y     z
##   <int> <int> <int>
## 1     1     2     3
read_csv("# A comment I want to skip
  x,y,z
  1,2,3", comment = "#")
## # A tibble: 1 x 3
##       x     y     z
##   <int> <int> <int>
## 1     1     2     3
  1. Podaci možda ne sadrže imena kolona, pa možemo koristiti col_names = FALSE da bismo funkciji read_csv() naglasili da ne tretira prvi red kao imena kolona, i ona će ih automatski obeležiti sa \(x_1,..,x_n\).
read_csv("1,2,3\n4,5,6", col_names = FALSE)
## # A tibble: 2 x 3
##      X1    X2    X3
##   <int> <int> <int>
## 1     1     2     3
## 2     4     5     6

(\n je prečica za dodavanje novog reda.)

Dodatno, možemo proslediti col_names vektor, koji će se koristiti za imena kolona.

read_csv("1,2,3\n4,5,6", col_names = c("x", "y", "z"))
## # A tibble: 2 x 3
##       x     y     z
##   <int> <int> <int>
## 1     1     2     3
## 2     4     5     6

Druga opcija koja obično zahteva podešavanje je na: ovo određuje vrednost (ili vrednosti) koje se koriste za predstavljanje nedostajućih vrednosti u našoj datoteci:

read_csv("a,b,c\n1,2,.", na = ".")
## # A tibble: 1 x 3
##       a     b c    
##   <int> <int> <chr>
## 1     1     2 <NA>

Ovo je sve što treba da znamo da bismo učitali ~75% CSV datoteka sa kojima se susrećemo u praksi. Međutim, da bismo učitali komplikovanije datoteke, moramo naučiti više o tome kako readr analizira svaku kolonu, pretvarajući ih u R vektore.

Poređenje sa baznim R-om

Ranije smo, pri učitavanju datoteka, korsitili ugrađenu read.csv() funkciju. Međutim, postoji nekoliko dobrih razloga za favorizovanje readr funkcija nad baznim ekvivalentima:

  • Obično su mnogo brže (~10x) od svojih baznih ekvivalenata.

  • One prave tiblove, ne pretvaraju vektore karaktera u faktore, ne daju imena redovima, ili ubacuju imena kolona. To su najčešće neprilike u radu sa osnovnim R funkcijama.

  • Osnovne R funkcije nasleđuju neko ponašanje od operativnih sistema i okruženja, tako da kod koji radi na jednom računaru, možda neće raditi na nekom drugom.

Raščlanjavanje vektora

Pozabavimo se sada malo parse_*() funkcijama. Ove funkcije uzimaju vektor karaktera i vraćaju vektor specifičnijeg tipa (npr. logički, celobrojni, datumi, itd.):

str(parse_logical(c("TRUE", "FALSE", "NA")))
##  logi [1:3] TRUE FALSE NA
str(parse_integer(c("1", "2", "3")))
##  int [1:3] 1 2 3
str(parse_date(c("2010-01-01", "1979-10-14")))
##  Date[1:2], format: "2010-01-01" "1979-10-14"

Ove funkcije su korisne same po sebi, ali su takođe vaĹžan deo za izgradnju readr-a. Kada saznamo kako pojedinačni parseri rade u ovom odeljku, vratićemo se nazad i videti kako se oni uklapaju u parsiranje kompletne datoteke.

Funkcije parse_*() tipa su uniformisane: prvi argument je vektor karaktera koji treba raščlaniti, a argument na navodi koje stringove treba tretirati kao nedostajuće:

parse_integer(c("1", "231", ".", "456"), na = ".")
## [1]   1 231  NA 456

Ako parsiranje ne uspe, dobićemo upozorenje:

x = parse_integer(c("123", "345", "abc", "123.45"))
## Warning in rbind(names(probs), probs_f): number of columns of result is not
## a multiple of vector length (arg 1)
## Warning: 2 parsing failures.
## row # A tibble: 2 x 4 col     row   col expected               actual expected   <int> <int> <chr>                  <chr>  actual 1     3    NA an integer             abc    row 2     4    NA no trailing characters .45

A neuspesi će nedostajati na izlazu:

x
## [1] 123 345  NA  NA
## attr(,"problems")
## # A tibble: 2 x 4
##     row   col expected               actual
##   <int> <int> <chr>                  <chr> 
## 1     3    NA an integer             abc   
## 2     4    NA no trailing characters .45

Ako postoji mnogo neuspešnih parsiranja, možemo da koristimo problems() da dobijemo kompletan skup neuspeha. Ovo vraća tibl, kojim onda možemo manipulisati uz pomoć dplyr-a.

problems(x)
## # A tibble: 2 x 4
##     row   col expected               actual
##   <int> <int> <chr>                  <chr> 
## 1     3    NA an integer             abc   
## 2     4    NA no trailing characters .45

Korišćenje parsera je uglavnom pitanje razumevanja dostupnih podataka i toga kako se nositi sa različitim tipovima ulaza. Postoji osam posebno važnih parsera:

  1. parse_logical() i parse_integer() raščlanjuju logičke i celobrojne vektore. U principu nema ničega što bi moglo poći po zlu pri korišćenju ovih parsera, pa ih nećemo dalje opisivati.

  2. parse_double() je strogo numerički parser, a parse_number() je fleksibilniji numerički parser. Oni mogu biti komplikovaniji nego što nam se čini, jer se u različitim delovima sveta brojevi pišu na različite načine.

  3. parse_character() izgleda tako jednostavnom da se čini da nam nije ni potrebna. Međutim, jedna komplikacija ga može učiniti veoma važnim: kodiranje znakova.

  4. parse_factor() kreira faktore, strukturu podataka koju R koristi za predstavljanje kategoričkih promenljivih sa fiksnim i poznatim vrednostima.

  5. parse_datetime(), parse_date() i parse_time() omogućuju da parsiramo različite datume i vremena. One su najsloženije jer postoji mnogo različitih načina pisanja datuma.

Sledeći odeljci detaljnije opisuju ove parsere:

Brojevi

Deluje da je lako analizirati broj, ali ipak, mogu se javiti tri problema:

  1. Ljudi pišu brojeve različito u različitim delovima sveta. Na primer, neke zemlje koriste tačku između celog i razlomljenog dela broja, dok druge koriste zarez.

  2. Brojevi su često okruženi drugim znakovima i imaju neki drugi kontekst, kao npr. “$1000” ili “10%”.

  3. Brojevi često sadrže znakove grupisanja cifara, kako bi ih učinili lakšim za čitanje, poput “1,000,000”, a ovi znakovi grupisanja se razlikuju širom sveta.

Da bi rešio prvi problem, readr ima locale, objekat koji određuje opcije parsiranja koje se razlikuju od mesta do mesta. Kada raščlanjujemo brojeve, najvažnija opcija koju podešavamo je karakter koji koristimo za decimalni znak. Možemo poništiti podrazumevanu vrednost za . kreiranjem novog locale i postavljanjem argumenta decimal_mark:

parse_double("1.23")
## [1] 1.23
parse_double("1,23", locale = locale(decimal_mark = ","))
## [1] 1.23

Podrazumevani locale readr-a je prilagođen SAD-u, jer je generalno R usredsređen na SAD (tj. dokumentacija je pisana na američkom engleskom jeziku). Alternativni pristup bi bio da pokušamo da pogodimo podrazumevane vrednosti iz našeg operativnog sistema. Ovo je teško dobro uraditi, i, što je još važnije, čini naš kod slabim: čak iako radi na našem računaru, možda neće raditi kada ga, na primer, pošaljemo mejlom kolegi u drugoj zemlji.

parse_number() rešava drugi problem: zanemaruje ne-numeričke znakove pre i posle broja. Ovo je posebno korisno za valute i procente, ali i radi izdvajanja brojeva ugrađenih u tekst.

parse_number("$100")
## [1] 100
parse_number("20%")
## [1] 20
parse_number("It costs $123.45")
## [1] 123.45

Poslednji problem se rešava kombinacijom parse_number() i locale: tada će parse_number() ignorisati “oznaku grupisanja”.

# Koristi se u Americi
parse_number("$123,456,789")
## [1] 123456789
# Koristi se u mnogim delovima Evrope
parse_number("123.456.789", locale = locale(grouping_mark = "."))
## [1] 123456789
# Koristi se u Svajcarskoj
parse_number("123'456'789", locale = locale(grouping_mark = "'"))
## [1] 123456789

Stringovi

Vratimo se na gore spomenuti parce_character(). Deluje nam da sve treba da bude veoma lako - funkcija treba samo da ispiše svoj unos. Nažalost, nije baš tako jednostavno, jer postoji više načina za predstavljanje istog stringa. Da bismo razumeli o čemu se radi, moramo uroniti malo u detalje o tome kako računari predstavljaju stringove. U R-u, možemo dobiti osnovnu reprezentaciju koristeći charToRaw():

charToRaw("Hadley")
## [1] 48 61 64 6c 65 79

Svaki heksadecimalni broj predstavlja bajt informacija: 48 je H, 61 je a, itd. U ovom slučaju, za kodiranje heksadecimalnih brojeva u karaktere, koristi se ASCII kodiranje. ASCII odlično radi sa predstavljanjem engleskih znakova, jer je to Američki stnadardni kod za razmenu informacija (ASCII).

Stvari postaju komplikovanije za druge jezike. Na početku, bilo je mnogo standarda za kodiranje neengleskih znakova, i da bismo ispravno interpretirali string koji nam je potreban, trebalo je da znamo i vrednosti i kodiranje. Na primer, dva često korišćena kodiranja su Latin1 (ili ISO-8859-1, koji se koristi za zapadnoevropske jezike) i Latin2 (ili ISO-8859-2, koji se koristi za istočnoevropske jezike). U Latin1, bajt b1 je “\(\pm\)”, ali u Latin2, to je “\(a\)”. Srećom, danas postoji standard koji je podržan skoro svuda: UTF-8. UTF-8 može da kodira skoro svaki karakter koji se danas koristi, kao i mnoge dodatne simbole (npr. emotikone).

readr svuda koristi UTF-8: pretpostavlja da su naši podaci UTF-8 kodirani kad ih čita, i uvek koristi UTF-8 prilikom pisanja. Ovo je dobar standard, ali neće uspeti za podatke proizvedene u starijim sistemima koji ne razumeju UTF-8. Ako se to dogodi, stringovi će izgledati čudno kada ih prikažemo. Ponekad samo jedan ili dva znaka mogu biti neispravna, a ponekad ćemo dobiti potpune besmislice. Na primer:

x1 = "El Ni\xf1o was particularly bad this year"
x2 = "\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd"

x1
#> [1] "El Ni\xf1o was particularly bad this year"
x2
#> [1] "\x82\xb1\x82\xf1\x82??\xbf\x82\xcd"

Da bismo rešili ovaj problem, moramo precizirati kodiranje u parse_character():

parse_character(x1, locale = locale(encoding = "Latin1"))
#> [1] "El Nino was particularly bad this year"
parse_character(x2, locale = locale(encoding = "Shift-JIS"))
#> [1] "???????????????"

Kako da pronađemo odgovarajuće kodiranje? Ako budemo imali sreće, taj podatak će biti uključen negde u dokumentaciji o podacima. Nažalost, to se retko dešava, pa readr obezbeđuje guess_encoding(), koji nam pomaže da pogodimo kodiranje. Ovo nije sigurno tačno, i radi bolje kad imamo puno teksta (što ovde nije slučaj), ali je zgodno za početak. Obično ćemo morati da probamo nekoliko tipova kodiranja pre nego što pronađemo odgovarajuće.

guess_encoding(charToRaw(x1))
#> # A tibble: 2 x 2
#>   encoding   confidence
#>   <chr>           <dbl>
#> 1 ISO-8859-1       0.46
#> 2 ISO-8859-9       0.23
guess_encoding(charToRaw(x2))
#> # A tibble: 1 x 2
#>   encoding confidence
#>   <chr>         <dbl>
#> 1 KOI8-R         0.42

Prvi argument u guess_encoding() može biti ili putanja do datoteke ili, kao u ovom slučaju, “raw” vektor (što je korisno ako su stringovi već u R-u).

Kodiranje je, ipak, previše bogata i složena tema, a mi samo smo malo zagrebali površinu.

Faktori

R koristi faktore za predstavljanje kategoričkih promenljivih koje imaju poznati skup mogućih vrednosti. Funkciji parse_factor() prosleđujemo vektor poznatih nivoa (ona će generisati upozorenje svaki put kada se pojavi neočekivana vrednost).

fruit = c("apple", "banana")
parse_factor(c("apple", "banana", "bananana"), levels = fruit)
## Warning: 1 parsing failure.
## row # A tibble: 1 x 4 col     row   col expected           actual   expected   <int> <int> <chr>              <chr>    actual 1     3    NA value in level set bananana
## [1] apple  banana <NA>  
## attr(,"problems")
## # A tibble: 1 x 4
##     row   col expected           actual  
##   <int> <int> <chr>              <chr>   
## 1     3    NA value in level set bananana
## Levels: apple banana

Međutim, ako imamo mnogo problematičnih unosa, često je lakše ostaviti ih kao vektore karaktera, a zatim iskoristiti neke alate (o kojima ćemo saznati u delu o faktorima i stringovima) da bismo ih očistili.

Datumi i vremena

Možemo birati između tri parsera u zavisnosti od toga da li želimo objekat tipa date (računa se kao broj dana od 01.01.1970., sa negativnim vrednostima za ranije datume), date-time (računa se kao broj sekundi od ponoći 01.01.1970.) ili time (broj sekundi od ponoći).

Kada se pozove bez dodatnih argumenata:

  • parse_datetime() očekuje ISO8601 datum-vreme. ISO8601 je međunarodni standard u kome su komponente datuma organizovane od najveće do najmanje: godina, mesec, dan, sat, minut, sekunda.
parse_datetime("2010-10-01T2010")
## [1] "2010-10-01 20:10:00 UTC"
# Ako je vreme izostavljeno, bice postavljeno na ponoc
parse_datetime("20101010")
## [1] "2010-10-10 UTC"
  • parse_date() očekuje četvorocifrenu godinu, zatim - ili /, mesec, - ili /, a onda dan:
parse_date("2010-10-01")
## [1] "2010-10-01"
  • parse_time() očekuje sate, zatim :, minute, opciono : i sekunde, i opciono am/pm.
library(hms)
parse_time("01:10 am")
## 01:10:00
parse_time("20:10:01")
## 20:10:01

Bazni R nema dobru ugrađenu klasu za podatke o vremenu, tako da mi koristimo onu koja je data u hms paketu.

Ako ove postavke ne rade za naše podatke, možemo dodati svoj vlastiti format datuma i vremena, sastavljen od sledećih delova:

Godina

%Y (4 cifre)

%y (2 cifre); 00-69 = 2000-2069, 70-99 = 1970-1999.

Mesec

%m (2 cifre)

%b (skraćeno ime: npr. “Jan”)

%B (celo ime: “Januar”)

Dan

%d (2 cifre)

Vreme

%H 0-23 sati

%I 0-12, mora se koristiti sa %p

%p AM/PM indikator

%M minuti

%S sekunde (celi brojevi)

%OS sekunde (realni brojevi)

%Z Vremenska zona (ime, kao npr. Amerika, Čikago). Treba biti oprezan sa skraćenicama (npr. “EST” predstavlja kanadsku vremensku zonu koja nema letnje računanje vremena, a ne standardno istočno vreme).

%z Odstupanje od UTC, npr. +0800

Ne-cifre

%. preskače jedan karakter koji nije cifra.

$%^{*}$ preskače bilo koji broj ne-cifara.

Najbolji način da otkrijemo ispravan format je kreiranje nekoliko primera u vektoru karaktera i testiranje jednom od funkcija parsiranja. Na primer:

parse_date("01/02/15", "%m/%d/%y")
## [1] "2015-01-02"
parse_date("01/02/15", "%d/%m/%y")
## [1] "2015-02-01"
parse_date("01/02/15", "%y/%m/%d")
## [1] "2001-02-15"

Ako koristimo %b ili %B sa ne-engleskim imenima meseci, moramo da postavimo argument locale(). Možemo pogledati listu ugrađenih jezika sa date_names_langs(), ili, ako naš jezik nije uključen, kreirati sopstveni date_names().

parse_date("1 janvier 2015", "%d %B %Y", locale = locale("fr"))
## [1] "2015-01-01"

Parsiranje datoteke

Sada kada smo naučili da rasclanimo pojedinačni vektor, vreme je da se vratimo na početak i istražimo kako readr raščlanjuje datoteku. Postoje dve nove stvari kojima ćemo se baviti u ovom odeljku:

  1. Kako readr automatski pogađa tip svake kolone.

  2. Kako prevazići podrazumevanu specifikaciju.

Strategija

readr koristi heurističke metode da odredi tip svake kolone: On čita prvih 1000 redova i na osnovu njih određuje tip svake kolone. Ovaj proces možemo pokrenuti uz pomoć funkcije guess_parser(), koja vraća nabolji readr pogodak, i parse_guess(), koja koristi tu pretpostavku za parsiranje kolone:

guess_parser("2010-10-01")
## [1] "date"
guess_parser("15:01")
## [1] "time"
guess_parser(c("TRUE", "FALSE"))
## [1] "logical"
guess_parser(c("1", "5", "9"))
## [1] "integer"
guess_parser(c("12,352,561"))
## [1] "number"
str(parse_guess("2010-10-10"))
##  Date[1:1], format: "2010-10-10"
str(parse_guess(c("1", "2", "3")))
##  int [1:3] 1 2 3

Funkcija isprobava svaki od sledećih tipova, dok ne pronađe podudaranje:

  • logički: ako sadrži samo “F”, “T”, “FALSE”, ili “TRUE”.

  • integer: ako sadrži samo numeričke karaktere (i -).

  • double: ako sadrži samo decimalne brojeve (uključujući i brojeve kao što su 4.5e-5).

  • number: ako sadrži decimalne brojeve sa oznakom grupisanja.

  • time: ako odgovara podrazumevanom time_format.

  • date: ako odgovara podrazumevanom date_format.

  • date-time: bilo koji ISO8601 datum.

Ako ne pronađe podudaranje ni sa jednim od ovih pravila, onda će kolona ostati vektor stringova.

Problemi

Ove podrazumevane vrednosti nisu uvek dobre za veće datoteke. Postoje dva osnovna problema:

  1. Prvih hiljadu redova mogu biti specijalan slučaj, pa readr pretpostavlja tip koji nije dobar za ostale podatke. Na primer, možda imamo kolonu double-ova koja sadrži samo cele brojeve u prvih 1000 redova.

  2. Kolona može da sadrži mnogo nedostajućih vrednosti. Ako prvih 1000 redova sadrži samo NA, readr će pretpostaviti da je to znakovni vektor, dok verovatno želimo da ga analiziramo kao nešto specifičnije.

CSV datoteka challenge ilustruje oba ova problema:

challenge = read_csv(readr_example("challenge.csv"))
## Parsed with column specification:
## cols(
##   x = col_integer(),
##   y = col_character()
## )
## Warning in rbind(names(probs), probs_f): number of columns of result is not
## a multiple of vector length (arg 1)
## Warning: 1000 parsing failures.
## row # A tibble: 5 x 5 col     row col   expected               actual             file               expected   <int> <chr> <chr>                  <chr>              <chr>              actual 1  1001 x     no trailing characters .23837975086644292 'C:/Program Files~ file 2  1002 x     no trailing characters .41167997173033655 'C:/Program Files~ row 3  1003 x     no trailing characters .7460716762579978  'C:/Program Files~ col 4  1004 x     no trailing characters .723450553836301   'C:/Program Files~ expected 5  1005 x     no trailing characters .614524137461558   'C:/Program Files~
## ... ................. ... .......................................................................... ........ .......................................................................... ...... .......................................................................... .... .......................................................................... ... .......................................................................... ... .......................................................................... ........ ..........................................................................
## See problems(...) for more details.

(Obratite pažnju na upotrebu readr_example() koja pronalazi putanju do datoteke uključene u paket).

Imamo dve stvari na izlazu: specifikacija kolona određena gledanjem prvih 1000 redova i informacije o prvih pet neuspelih parsiranja. Uvek je dobra ideja da se pozove problems(), tako da nesupehe možemo detaljnije istražiti:

problems(challenge)
## # A tibble: 1,000 x 5
##      row col   expected               actual             file             
##    <int> <chr> <chr>                  <chr>              <chr>            
##  1  1001 x     no trailing characters .23837975086644292 'C:/Program File~
##  2  1002 x     no trailing characters .41167997173033655 'C:/Program File~
##  3  1003 x     no trailing characters .7460716762579978  'C:/Program File~
##  4  1004 x     no trailing characters .723450553836301   'C:/Program File~
##  5  1005 x     no trailing characters .614524137461558   'C:/Program File~
##  6  1006 x     no trailing characters .473980569280684   'C:/Program File~
##  7  1007 x     no trailing characters .5784610391128808  'C:/Program File~
##  8  1008 x     no trailing characters .2415937229525298  'C:/Program File~
##  9  1009 x     no trailing characters .11437866208143532 'C:/Program File~
## 10  1010 x     no trailing characters .2983446326106787  'C:/Program File~
## # ... with 990 more rows

Dobra strategija je da radimo kolonu po kolonu dok ne nestanu problemi. Ovde možemo da vidimo da postoji mnogo problema sa parsiranjem kolone x - postoje decimalni brojevi posle celobrojnih vrednosti. To sugeriše da treba da koristimo double parser.

Da bismo popravili stvari, započnimo tako što ćemo kopirati i nalepiti specifikaciju kolona u originalni poziv:

challenge = read_csv(
  readr_example("challenge.csv"), 
  col_types = cols(
    x = col_integer(),
    y = col_character()
  )
)
## Warning in rbind(names(probs), probs_f): number of columns of result is not
## a multiple of vector length (arg 1)
## Warning: 1000 parsing failures.
## row # A tibble: 5 x 5 col     row col   expected               actual             file               expected   <int> <chr> <chr>                  <chr>              <chr>              actual 1  1001 x     no trailing characters .23837975086644292 'C:/Program Files~ file 2  1002 x     no trailing characters .41167997173033655 'C:/Program Files~ row 3  1003 x     no trailing characters .7460716762579978  'C:/Program Files~ col 4  1004 x     no trailing characters .723450553836301   'C:/Program Files~ expected 5  1005 x     no trailing characters .614524137461558   'C:/Program Files~
## ... ................. ... .......................................................................... ........ .......................................................................... ...... .......................................................................... .... .......................................................................... ... .......................................................................... ... .......................................................................... ........ ..........................................................................
## See problems(...) for more details.

Sada možemo podesiti tip x kolone:

challenge = read_csv(
  readr_example("challenge.csv"), 
  col_types = cols(
    x = col_double(),
    y = col_character()
  )
)

To rešava problem sa kolonom x, ali ako pogledamo poslednjih nekoliko redova, videćemo da su i datumi uskladišteni u vektoru y:

tail(challenge)
## # A tibble: 6 x 2
##       x y         
##   <dbl> <chr>     
## 1 0.805 2019-11-21
## 2 0.164 2018-03-29
## 3 0.472 2014-08-04
## 4 0.718 2015-08-16
## 5 0.270 2020-02-04
## 6 0.608 2019-01-06

Možemo i ovo popraviti tako što navedemo da je y kolona datuma:

challenge = read_csv(
  readr_example("challenge.csv"), 
  col_types = cols(
    x = col_double(),
    y = col_date()
  )
)
tail(challenge)
## # A tibble: 6 x 2
##       x y         
##   <dbl> <date>    
## 1 0.805 2019-11-21
## 2 0.164 2018-03-29
## 3 0.472 2014-08-04
## 4 0.718 2015-08-16
## 5 0.270 2020-02-04
## 6 0.608 2019-01-06

Svaka parse_xyz() funkcija ima odgovarajuću funkciju col_xyz(). Koristimo parse_xyz() kad su podaci već u vektoru karaktera u R-u, koristimo col_xyz() kada želimo da kažemo readr-u kako da učita podatke.

Preporučuje se da se uvek navede col_types(), koji se gradi od ispisa koji daje readr. Ovo osigurava da imamo dosledan i konzistentan princip za uvoz podataka. Ako želimo da budemo veoma strogi, koristimo stop_for_problems(): to će izbaciti grešku i zaustaviti učitavanje ako postoje problemi sa parsiranjem.

Druge strategije

Postoji nekoliko drugih strategija koje nam mogu pomoći da parsiramo datoteke:

  • U prethodnom primeru, nismo imali sreće: ako pogledamo samo još jedan red više od podrazumevanog, možemo ispravno parsirati odjednom:
challenge2 = read_csv(readr_example("challenge.csv"), guess_max = 1001)
## Parsed with column specification:
## cols(
##   x = col_double(),
##   y = col_date(format = "")
## )
challenge2
## # A tibble: 2,000 x 2
##        x y         
##    <dbl> <date>    
##  1   404 NA        
##  2  4172 NA        
##  3  3004 NA        
##  4   787 NA        
##  5    37 NA        
##  6  2332 NA        
##  7  2489 NA        
##  8  1449 NA        
##  9  3665 NA        
## 10  3863 NA        
## # ... with 1,990 more rows
  • Ponekad je lakše dijagnostikovati probleme ako samo učitamo sve kolone kao vektore karaktera:
challenge2 = read_csv(readr_example("challenge.csv"), 
  col_types = cols(.default = col_character())
)

Ovo je posebno korisno u kombinaciji sa type_convert(), koja primenjuje heurističko parsiranje na kolone karaktera u bazi.

df = tribble(
  ~x,  ~y,
  "1", "1.21",
  "2", "2.32",
  "3", "4.56"
)
df
## # A tibble: 3 x 2
##   x     y    
##   <chr> <chr>
## 1 1     1.21 
## 2 2     2.32 
## 3 3     4.56
# Obratite paznju na tipove kolona
type_convert(df)
## Parsed with column specification:
## cols(
##   x = col_integer(),
##   y = col_double()
## )
## # A tibble: 3 x 2
##       x     y
##   <int> <dbl>
## 1     1  1.21
## 2     2  2.32
## 3     3  4.56
  • Ako učitavamo jako veliku datoteku, možemo postaviti n_max() na manji broj kao što je 10,000 ili 100,000, da ubrzamo, dok ne eliminišemo uobičajene probleme.

  • Ako imamo velike probleme u parsiranju, ponekad je lakše samo učitati čitavu liniju kao vektor karaktera sa read_lines(), ili čak sve sa read_file(). Tada možemo koristiti veštine parsiranja stringova, o kojima će biti reči kasnije, da bismo raščlanili komplikovanije formate.

Ispis u datoteku

Readr dolazi i sa dve korisne funkcije za ispis podataka u datoteke: write_csv() i write_tsv(). Obe funkcije povećavaju šanse da se ispravno izvrši ispis u izlaznu datoteku tako što:

  • Uvek kodiraju stringove kao UTF-8.

  • Čuvaju datume i datum-vreme u ISO8601 formatu tako da se oni mogu lako parsirati na bilo kom mestu.

Ako želimo da izvezemo csv datoteku u Excel, koristimo write_excel_csv() - ovo piše specijalni znak (“byte order mark”) na početku datoteke koji govori Excel-u da koristimo UTF-8 kodiranje.

Najvažniji argumenti su x (baza za čuvanje) i path (lokacija gde se čuva). Takođe, možemo odrediti kako se zapisuju nedostajuće vrednosti argumentom na, i dodati append ako želimo da dopisujemo u postojeću datoteku.

write_csv(challenge, "challenge.csv")

Treba imati na umu da se informacije o tipu gube kada sačuvamo u csv.

challenge
## # A tibble: 2,000 x 2
##        x y         
##    <dbl> <date>    
##  1   404 NA        
##  2  4172 NA        
##  3  3004 NA        
##  4   787 NA        
##  5    37 NA        
##  6  2332 NA        
##  7  2489 NA        
##  8  1449 NA        
##  9  3665 NA        
## 10  3863 NA        
## # ... with 1,990 more rows
write_csv(challenge, "challenge-2.csv")
read_csv("challenge-2.csv")
## Parsed with column specification:
## cols(
##   x = col_integer(),
##   y = col_character()
## )
## Warning in rbind(names(probs), probs_f): number of columns of result is not
## a multiple of vector length (arg 1)
## Warning: 1000 parsing failures.
## row # A tibble: 5 x 5 col     row col   expected               actual             file              expected   <int> <chr> <chr>                  <chr>              <chr>             actual 1  1001 x     no trailing characters .23837975086644292 'challenge-2.csv' file 2  1002 x     no trailing characters .41167997173033655 'challenge-2.csv' row 3  1003 x     no trailing characters .7460716762579978  'challenge-2.csv' col 4  1004 x     no trailing characters .723450553836301   'challenge-2.csv' expected 5  1005 x     no trailing characters .614524137461558   'challenge-2.csv'
## ... ................. ... ......................................................................... ........ ......................................................................... ...... ......................................................................... .... ......................................................................... ... ......................................................................... ... ......................................................................... ........ .........................................................................
## See problems(...) for more details.
## # A tibble: 2,000 x 2
##        x y    
##    <int> <chr>
##  1   404 <NA> 
##  2  4172 <NA> 
##  3  3004 <NA> 
##  4   787 <NA> 
##  5    37 <NA> 
##  6  2332 <NA> 
##  7  2489 <NA> 
##  8  1449 <NA> 
##  9  3665 <NA> 
## 10  3863 <NA> 
## # ... with 1,990 more rows

Ovo čini csv-ove malo nepouzdanim za čuvanje privremenih rezultata - moramo ponovo kreirati specifikaciju kolone svaki put kada učitamo. Postoje dve alternative:

  1. write_rds() i read_rds() su uniformni omotači oko osnovnih funkcija readRDS() i saveRDS(). One čuvaju podatke u prilagođenom R binarnom formatu koji se naziva RDS:
write_rds(challenge, "challenge.rds")
read_rds("challenge.rds")
## # A tibble: 2,000 x 2
##        x y         
##    <dbl> <date>    
##  1   404 NA        
##  2  4172 NA        
##  3  3004 NA        
##  4   787 NA        
##  5    37 NA        
##  6  2332 NA        
##  7  2489 NA        
##  8  1449 NA        
##  9  3665 NA        
## 10  3863 NA        
## # ... with 1,990 more rows
  1. feather paket implementira brzi binarni format koji se može deliti preko programskih jezika:
library(feather)
write_feather(challenge, "challenge.feather")
read_feather("challenge.feather")
## # A tibble: 2,000 x 2
##        x y         
##    <dbl> <date>    
##  1   404 NA        
##  2  4172 NA        
##  3  3004 NA        
##  4   787 NA        
##  5    37 NA        
##  6  2332 NA        
##  7  2489 NA        
##  8  1449 NA        
##  9  3665 NA        
## 10  3863 NA        
## # ... with 1,990 more rows

feather je obično brĹži od RDS-a i upotrebljiv je izvan R-a, mada i RDS ima neke prednosti u odnosu na njega.

Drugi tipovi podataka

Moguće je raditi i sa drugim tipovima podataka, i preporučljivo je da počnemo sa paketima navedenim ispod. Oni nisu savršeni, ali su dobri za početak:

  • haven za SPSS, Stata i SAS datoteke.

  • readxl za excel datoteke (i .xls i .xlsx).

  • DBI omogućava pokretanje SQL upita.

Za hijerarhijske podatke, koristimo jsonlite za json, i xml2 za XML. Za druge podatke, možete pogledati link i rio paket.