U radu s podacima analiza nikad ne počinje modelom, regresijom ili grafom. Prvi i često najvažniji korak je priprema podataka.

Taj proces u informatici i analitici naziva se ETL – skraćenica od:

  • Extract – dohvat podataka
  • Transform – čišćenje i preoblikovanje
  • Load – pohrana i priprema za daljnju analizu

ETL predstavlja temelj svake ozbiljne analize podataka.


Zašto je ETL potreban?

U stvarnim sustavima podaci:

  • dolaze iz različitih izvora (Excel, baze, web, API)
  • imaju različite formate
  • sadrže nedostajuće vrijednosti
  • imaju tipfelere i nelogične zapise
  • imaju neujednačene nazive varijabli
  • često nisu spremni za izravnu analizu

Primjer:

  • broj je zapisan kao tekst
  • datum je zapisan kao string
  • decimalni separator je zarez umjesto točke
  • naziv stupca sadrži razmake i posebne znakove

Bez ETL faze:

  • analiza može dati pogrešne rezultate
  • javljaju se greške pri modeliranju
  • rezultati mogu biti nereproducibilni

ETL kao profesionalni standard

U poslovnim sustavima ETL:

  • povezuje baze podataka i analitičke sustave
  • omogućuje automatizirano ažuriranje podataka
  • osigurava konzistentnost izvještaja
  • omogućuje skaliranje analitičkih procesa

U znanstvenom radu ETL:

  • osigurava transparentnost
  • omogućuje reproducibilnost
  • smanjuje rizik metodoloških grešaka

ETL nije “tehnički detalj”

Vrlo često se događa da:

  • barem 60% vremena analitičkog projekta otpada na pripremu podataka
  • sama statistička analiza čini manji dio ukupnog rada

Zato ETL nije pomoćna faza, nego:

Strukturni temelj svake ozbiljne analize podataka.


U ovom poglavlju fokus je na:

  1. Učitavanju podataka iz različitih izvora
  2. Provjeri i razumijevanju strukture podataka
  3. Čišćenju i standardizaciji
  4. Transformaciji varijabli
  5. Pohrani pripremljenih podataka

Cilj nije samo “da kod radi”, nego da:

  • podaci budu metodološki ispravni
  • postupak bude dokumentiran
  • rezultat bude ponovljiv

R i RStudio

R je statistički programski jezik namijenjen analizi podataka, modeliranju i vizualizaciji. RStudio je integrirano razvojno okruženje (IDE) koje omogućuje organiziran rad kroz:

  • skripte
  • projekte
  • dokumente (RMarkdown, Quarto)
  • upravljanje paketima
  • verzioniranje

Detaljnije upute naći ćete u Uvodu u R i RStudio.


RMarkdown i Quarto dokumenti

Što je RMarkdown?

RMarkdown je format dokumenta koji omogućuje kombiniranje:

  • teksta (Markdown)
  • R koda
  • rezultata analize
  • grafova i tablica
  • automatskog generiranja izvještaja

Datoteka ima nastavak: .Rmd

Kod se dodaje unutar tzv. chunkova:

summary(data)

RMarkdown se temelji na paketu rmarkdown i Pandoc sustavu za pretvorbu dokumenata.


Što je Quarto?

Quarto je proširena verzija koncepta RMarkdowna.

Datoteka ima nastavak: .qmd

Prednosti Quarta:

  • podržava više jezika (R, Python, Julia)
  • napredniji sustav publikacije (web stranice, knjige, prezentacije)
  • bolje rješava reproducibilnost
  • standardiziraniji YAML sustav

Kod u Quartu izgleda isto, dodaje se unutar tzv. chunkova:

summary(data)

Usporedba RMarkdowna i Quarta

Karakteristika RMarkdown Quarto
Nastavak .Rmd .qmd
Podrška jezika primarno R R, Python, Julia
Projektna struktura ograničena napredna
Web publikacija moguće prirodno integrirano
Budući razvoj održavanje aktivni razvoj (preporučeno)
YAML struktura jednostavna fleksibilnija

Kad koristiti koji?

RMarkdown je dobar za:

  • brze analize
  • jednostavne izvještaje
  • početničke projekte

Quarto je bolji izbor za:

  • ozbiljne projekte
  • knjige i skripte
  • web stranice
  • interdisciplinarni rad (R + Python)
  • dugoročnu reproducibilnost

Reproducibilnost i dokumentiranje rada

Bez obzira koristimo li RMarkdown ili Quarto, važno je:

  1. Dokumentirati svaki korak ETL procesa
  2. Navesti verziju R-a i paketa
  3. Koristiti projekte (File → New Project)
  4. Strukturirati rad u jasne sekcije
  5. Izbjegavati ručne izmjene podataka izvan skripte

Provjera verzije:

sessionInfo()
## R version 4.4.2 (2024-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 10 x64 (build 19045)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=Croatian_Croatia.utf8  LC_CTYPE=Croatian_Croatia.utf8   
## [3] LC_MONETARY=Croatian_Croatia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Croatian_Croatia.utf8    
## 
## time zone: Europe/Zagreb
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices datasets  utils     methods   base     
## 
## loaded via a namespace (and not attached):
##  [1] digest_0.6.39     R6_2.6.1          fastmap_1.2.0     xfun_0.56        
##  [5] cachem_1.1.0      knitr_1.51        htmltools_0.5.9   rmarkdown_2.30   
##  [9] lifecycle_1.0.5   cli_3.6.5         sass_0.4.10       renv_1.1.1       
## [13] jquerylib_0.1.4   compiler_4.4.2    rstudioapi_0.18.0 tools_4.4.2      
## [17] evaluate_1.0.5    bslib_0.10.0      yaml_2.3.12       jsonlite_2.0.0   
## [21] rlang_1.1.7

Komentari

# Ovo je komentar u R-u
x <- 10  # dodjeljujemo vrijednost 10 varijabli x

Kreiranje varijabli i formati podataka u R-u

U R-u se podaci pohranjuju u nekoliko osnovnih struktura. Važno je razlikovati ih jer se ETL koraci (učitavanje, čišćenje, transformacija) često svode na pretvaranje podataka iz jednog “spremnika” u drugi, uz promjenu tipova varijabli i reorganizaciju stupaca/redaka.

Ako trebate detaljniji uvod (s puno primjera i objašnjenjem rada u RStudio okruženju), koristite: Uvod u R i RStudio


Vektori

Vektor je najosnovniji format u R-u: to je niz vrijednosti istog tipa (npr. sve numeričke ili sve tekstualne).

  • koristi se za pojedinačne varijable (npr. dob, bodovi, spol)
  • može imati imena elemenata (names()), što olakšava rad i referenciranje
v <- c(2, 4, 6, 8)
names(v) <- c("a", "b", "c", "d")
v
## a b c d 
## 2 4 6 8

Napomena (često zbunjuje): vektor ne može “miješati tipove” bez posljedica. Ako ubacite znak u numerički vektor, R će sve pretvoriti u tekst.

c(1, 2, "3")   # sve postaje character
## [1] "1" "2" "3"

Matrica

Matrica je dvodimenzionalni format: tablica s recima i stupcima, ali i dalje vrijedi pravilo: svi elementi moraju biti istog tipa.

  • koristi se za numeričke izračune, linearnu algebru, modele, simulacije
  • dobra je kad su podaci “strogo” numerički i pravilnog oblika
m <- matrix(1:9, nrow = 3)
m
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9

Korisno je imenovati retke/stupce:

rownames(m) <- paste0("r", 1:3)
colnames(m) <- paste0("c", 1:3)
m
##    c1 c2 c3
## r1  1  4  7
## r2  2  5  8
## r3  3  6  9

Podatkovni okvir

Podatkovni okvir/Data frame je najčešći oblik podataka u analizi: tablica gdje su stupci varijable, a redci opažanja.

Ključna razlika u odnosu na matricu:

stupci mogu biti različitih tipova (npr. jedan stupac numerički, drugi tekstualni, treći faktor/datum).

  • tipičan format za statistiku i ETL
  • većina R funkcija i paketa pretpostavlja da radite s data frame-om (ili tibble-om)
df <- data.frame(
  ime = c("Ana", "Ivan", "Marko"),
  bodovi = c(85, 92, 78)
)
df
##     ime bodovi
## 1   Ana     85
## 2  Ivan     92
## 3 Marko     78

Brzi pregled strukture:

str(df)
## 'data.frame':    3 obs. of  2 variables:
##  $ ime   : chr  "Ana" "Ivan" "Marko"
##  $ bodovi: num  85 92 78

Napomena za ETL: nakon učitavanja podataka, prvi korak je provjeriti jesu li tipovi stupaca dobri (npr. je li datum stvarno Date, je li broj stvarno numeric, itd.).

Pristup 2. retku podatkovnog okvira:

df[2,]
##    ime bodovi
## 2 Ivan     92

Pristup 1. stupcu podatkovnog okvira:

df[,1]
## [1] "Ana"   "Ivan"  "Marko"

Pristup varijabli bodovi:

df$bodovi
## [1] 85 92 78

Pristup elementu u 3. retku i 2. stupcu podatkovnog okvira:

df[3,2]
## [1] 78

Tibble

Tibble je modernija verzija data.frame objekta, razvijena u okviru paketa tibble (dio tidyverse ekosustava).

U praksi:

  • data.frame je “klasični” R objekt
  • tibble je njegova unaprijeđena i preglednija verzija

Ako koristimo dplyr, tidyr ili readr, podaci se automatski učitavaju kao tibble.

library(tibble)

tb <- tibble(
  ime = c("Ana", "Ivan", "Marko"),
  bodovi = c(85, 92, 78)
)

tb
## # A tibble: 3 × 2
##   ime   bodovi
##   <chr>  <dbl>
## 1 Ana       85
## 2 Ivan      92
## 3 Marko     78

Razlika između data.frame i tibble

Svojstvo data.frame tibble
Ispis u konzoli ispisuje sve retke prikazuje samo prvih 10 redaka
Automatska konverzija stringova u faktor ponekad nikad
Rad s nepostojećim stupcem može vratiti NULL baca grešku (sigurnije)
Indeksiranje fleksibilno strože i predvidljivije

Primjer ispisa:

class(df)
## [1] "data.frame"
class(tb)
## [1] "tbl_df"     "tbl"        "data.frame"

Zašto je tibble važan u ETL procesu?

  1. Pregledniji ispis kod velikih datasetova
  2. Ne mijenja automatski tipove podataka
  3. Bolje se integrira s:
  • dplyr
  • tidyr
  • readr
  1. Manje “tihe” konverzije (iziskuje manje provjera).

Pretvaranje između data.frame i tibble

as_tibble(df)
## # A tibble: 3 × 2
##   ime   bodovi
##   <chr>  <dbl>
## 1 Ana       85
## 2 Ivan      92
## 3 Marko     78
as.data.frame(tb)
##     ime bodovi
## 1   Ana     85
## 2  Ivan     92
## 3 Marko     78

Lista

Lista je “spremnik za sve”: može sadržavati elemente različitih tipova i različitih dimenzija (vektore, matrice, data frame-ove, modele, grafove…).

  • koristi se kad želite u jednom objektu čuvati više povezanih stvari
  • često se pojavljuje kao rezultat složenijih funkcija (npr. modeli, analize, import podataka)
lst <- list(v, m, df)
str(lst)
## List of 3
##  $ : Named num [1:4] 2 4 6 8
##   ..- attr(*, "names")= chr [1:4] "a" "b" "c" "d"
##  $ : int [1:3, 1:3] 1 2 3 4 5 6 7 8 9
##   ..- attr(*, "dimnames")=List of 2
##   .. ..$ : chr [1:3] "r1" "r2" "r3"
##   .. ..$ : chr [1:3] "c1" "c2" "c3"
##  $ :'data.frame':    3 obs. of  2 variables:
##   ..$ ime   : chr [1:3] "Ana" "Ivan" "Marko"
##   ..$ bodovi: num [1:3] 85 92 78

Možete dodati imena elemenata liste:

lst2 <- list(vektor = v, matrica = m, podaci = df)
lst2$matrica
##    c1 c2 c3
## r1  1  4  7
## r2  2  5  8
## r3  3  6  9
lst2$podaci
##     ime bodovi
## 1   Ana     85
## 2  Ivan     92
## 3 Marko     78
lst$podaci
## NULL

Kad koristiti što?

  • Vektor: jedna varijabla (niz vrijednosti istog tipa)
  • Matrica: strogo numerički 2D podaci, izračuni, simulacije
  • Data frame: okvir podataka za analizu (različiti tipovi stupaca)
  • Lista: paket više objekata u jedan (rezultati, modeli, više datasetova)

Instalacija i učitavanje paketa

install.packages() je naredba za instalaciju paketa. Naziv paketa koji instaliramo stavljamo u navodne znakove.

install.packages("psych")
install.packages("dplyr")
install.packages("tidyr")
install.packages("readr")
install.packages("readxl") # ili "openxlsx"
install.packages("DBI")
install.packages("RSQLite")
install.packages("nycflights13")
install.packages("lubridate")
install.packages("janitor")
install.packages("stringr")

psych je paket za psihometrijsku i deskriptivnu statistiku.

Koristit ćemo ga za:

  • detaljne deskriptivne pokazatelje (describe())

  • brzi pregled distribucija

  • izračun statističkih mjera u tabličnom obliku

dplyr je temeljni paket za manipulaciju podacima (transform dio ETL-a).

Omogućuje:

  • filtriranje redaka (filter())

  • odabir stupaca (select())

  • sortiranje (arrange())

  • grupiranje i agregiranje (group_by(), summarise())

  • kreiranje novih varijabli (mutate())

tidyr je paket za preoblikovanje podataka (strukturiranje podataka u „tidy” oblik).

Koristit ćemo ga za:

  • pretvaranje širokog u dugi format (pivot_longer())

  • pretvaranje dugog u široki (pivot_wider())

  • razdvajanje i spajanje varijabli

readr služi za brzo i pouzdano učitavanje tekstualnih datoteka (CSV, TXT).

Prednosti:

  • automatsko prepoznavanje tipova varijabli

  • brže učitavanje od base R funkcija

  • jasne poruke o problemima u podacima

readxl služi za učitavanje Excel (.xlsx) datoteka bez potrebe za Excelom instaliranim na računalu.

DBI je standardizirano sučelje za rad s bazama podataka u R-u.

Omogućuje:

  • povezivanje s bazom

  • slanje SQL upita

  • dohvat podataka

Koristit ćemo ga zajedno s RSQLite.

RSQLite omogućuje rad sa SQLite bazama podataka (lagane, lokalne baze).

Koristit ćemo ga za:

  • simulaciju rada s bazom podataka

  • spremanje obrađenih podataka

  • izvršavanje SQL upita

lubridate je paket za rad s datumima i vremenom.

Rad s datumima u base R-u često je:

  • osjetljiv na format

  • nepregledan

  • sklon greškama

lubridate pojednostavljuje:

  • parsiranje datuma

  • izdvajanje godine, mjeseca, dana

  • rad s vremenskim razlikama

  • konverziju vremenskih zona

janitor je paket za “higijenu podataka”.

Koristi se za:

  • standardizaciju naziva varijabli

  • brze frekvencijske tablice

  • uklanjanje praznih redaka/stupaca

  • čišćenje strukture podataka

  • najvažnija funkcija clean_names() - pretvara nazive u mala slova, razmake zamjenjuje s donjom povlakom, uklanja posebne znakove, uklanja dijakritike, osigurava sintaktičku ispravnost.

stringr se koristi za:

  • pretraživanje (str_detect)

  • zamjenu (str_replace)

  • izdvajanje (str_extract)

  • promjenu velikih/malih slova

Za više detalja, pogledajte dokumentaciju paketa.

Kako se ovi paketi uklapaju u ETL proces?

ETL faza Paketi
Extract readr, readxl, DBI, RSQLite
Transform janitor, dplyr, tidyr, lubridate, stringr
Load DBI, RSQLite
Analiza i pregled psych

Naredbom library() pozivamo (aktiviramo) paket. Kao argument upisujemo naziv paketa (bez navodnih znakova).

library(psych)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidyr)
library(readr)
library(readxl)
library(DBI)
library(RSQLite)
library(nycflights13)
library(lubridate)
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
library(janitor)
## 
## Attaching package: 'janitor'
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
library(stringr)

Učitavanje podataka

U ETL procesu, Extract znači dohvat podataka iz različitih izvora:

  • lokalne datoteke
  • internetski URL
  • baze podataka
  • javni API servisi
  • ugrađeni datasetovi

Važno je razumjeti:

  • u kojem su formatu podaci
  • kako su kodirani (UTF-8?)
  • koji je separator (, ; \t)
  • kako su zapisani decimalni brojevi (. ili ,)
  • jesu li zaglavlja ispravna

Učitavanje dokumenata s računala

Prije nego dođemo u situaciju “ne mogu naći datoteku”, moramo se kratko upoznati s radnim direktorijem i projektnom strukturom. Dakle, datoteke čitamo relativnim putanjama unutar projekta.

  • getwd(), setwd()

  • preporuka strukture: data_raw/, data_clean/, scripts/, reports/

Minimalni kod-blok:

getwd()
list.files()

Ako se dokument s podacima nalazi u direktoriju projekta, možemo ga izravno učitati pozivom odgovarajuće funkcije i navođenjem naziva dokumenta kao argument te funkcije.

CSV je tekstualna datoteka gdje su vrijednosti odvojene separatorom (najčešće zarezom).

real_estate <- read.csv("Real_estate.csv")

head(real_estate)
##   X  Price Lot.Size Waterfront Age Land.Value New.Construct Central.Air
## 1 1 132500     0.09          0  42      50000             0           0
## 2 2 181115     0.92          0   0      22300             0           0
## 3 3 109000     0.19          0 133       7300             0           0
## 4 4 155000     0.41          0  13      18700             0           0
## 5 5  86060     0.11          0   0      15000             1           1
## 6 6 120000     0.68          0  31      14000             0           0
##   Fuel.Type Heat.Type Sewer.Type Living.Area Pct.College Bedrooms Fireplaces
## 1         3         4          2         906          35        2          1
## 2         2         3          2        1953          51        3          0
## 3         2         3          3        1944          51        4          1
## 4         2         2          2        1944          51        3          1
## 5         2         2          3         840          51        2          0
## 6         2         2          2        1152          22        4          1
##   Bathrooms Rooms
## 1       1.0     5
## 2       2.5     6
## 3       1.0     8
## 4       1.5     5
## 5       1.0     3
## 6       1.0     8

Objašnjenje

  • read.csv() je base R funkcija
  • pretpostavlja separator ,
  • automatski pokušava prepoznati tip varijable
  • često vraća data.frame

Važno:

  • radni direktorij (getwd())
  • kodiranje znakova (fileEncoding = "UTF-8")

Alternativa (preporučena u modernom radu):

readr::read_csv()

Prednosti:

  • brže učitavanje
  • jasnije poruke o problemima
  • vraća tibble

Excel (.xlsx)

Excel je binarni format.

diamonds <- read_xlsx("Diamonds.xlsx")

head(diamonds)
## # A tibble: 6 × 12
##   Carat.Size Color Clarity Depth Table Cut    Report Price Log.Price Table.Depth
##        <dbl> <chr> <chr>   <dbl> <dbl> <chr>  <chr>  <dbl>     <dbl>       <dbl>
## 1       0.3  E     VVS1     60      59 Excel… GIA     1000      6.91        -1  
## 2       0.44 E     VS2      61.9    58 Excel… GIA     1000      6.91        -3.9
## 3       0.31 E     VVS1     61.3    58 Excel… GIA     1000      6.91        -3.3
## 4       0.66 K     SI1      62.8    57 Excel… GIA     1000      6.91        -5.8
## 5       0.47 H     VS2      59.1    64 Very … GIA     1000      6.91         4.9
## 6       0.4  G     VS1      62      59 Excel… GIA     1000      6.91        -3  
## # ℹ 2 more variables: Table.Depth.1 <dbl>, Test <dbl>

Objašnjenje

  • koristi paket readxl
  • nije potreban instaliran Excel
  • automatski prepoznaje tipove stupaca
  • vraća tibble

Važno:

  • Excel datoteke često sadrže:

    • prazne redove
    • dodatne retke zaglavlja
    • tekstualne vrijednosti u numeričkim stupcima

Učitavanje putem URL-a

cyber_attacks <- read.csv(
  "https://raw.githubusercontent.com/plotly/datasets/master/2014_apple_stock.csv"
)

head(cyber_attacks)
##       AAPL_x   AAPL_y
## 1 2014-01-02 77.44539
## 2 2014-01-03 77.04558
## 3 2014-01-06 74.89697
## 4 2014-01-07 75.85646
## 5 2014-01-08 75.09195
## 6 2014-01-09 76.20263
heart_rate <- read_csv(
  "https://raw.githubusercontent.com/jasp-stats/jasp-data-library/main/Heart%20Rate/Heart%20Rate.csv"
)
## Rows: 800 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): Gender, Group
## dbl (1): Heart Rate
## 
## ℹ 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.
head(heart_rate)
## # A tibble: 6 × 3
##   Gender Group   `Heart Rate`
##   <chr>  <chr>          <dbl>
## 1 Female Runners          119
## 2 Female Runners           84
## 3 Female Runners           89
## 4 Female Runners          119
## 5 Female Runners          127
## 6 Female Runners          111

Objašnjenje

  • read.table() je fleksibilnija funkcija
  • header = TRUE znači da prvi red sadrži nazive varijabli
  • separator se može specificirati (sep = "\t")

Prednost read.table():

  • nije potrebno ručno preuzeti datoteku

Prednost read_csv():

  • brži rad
  • bolja dijagnostika problema u tipovima
  • prikazuje koliko je problema pri parsiranju

Rizik:

  • URL može postati nedostupan
  • struktura podataka se može promijeniti

Za txt podatke, najčešće pristupamo na sljedeći način:

txt_url <- "http://sites.williams.edu/rdeveaux/files/2014/09/Saratoga.txt"
tmp <- tempfile(fileext = ".tsv")

download.file(txt_url, tmp, mode = "wb")  # preuzima datoteku lokalno (robustnije)

data <- read.delim(tmp, sep = "\t", header = TRUE) # pretpostavlja tab separator; ako je space/više razmaka, treba read.table() ili sep=""

Prikupljanje podataka s web stranica

Tehnika poznaka i kao web-scraping. Ovdje se fokusiramo na dohvaćanje tablica. Ovdje ćemo pristupiti tablici koja sadrži podatke o 1000 najvećih poduzeća u Hrvatskoj, koju objavljuje i redovito ažurira Lider.hr.

library(rvest)
## 
## Attaching package: 'rvest'
## The following object is masked from 'package:readr':
## 
##     guess_encoding
library(xml2)
library(rvest)

# učitamo HTML
webpage <- read_html("https://lidermedia.hr/lider-bi/tablice/1000-najvecih.html")

# pronađimo sve tablice u stranici
tables <- html_nodes(webpage, "table")

length(tables)   # koliko ih ima
## [1] 1
df <- html_table(tables, fill = TRUE)

head(df)
## [[1]]
## # A tibble: 1,000 × 15
##    `Rang 2023.` `Rang 2022.` Tvrtka                `Grad/općina` Županija Sektor
##           <int> <chr>        <chr>                 <chr>         <chr>    <chr> 
##  1            1 1            INA d.d.              Zagreb        21. Gra… 22. N…
##  2            2 3            HRVATSKA ELEKTROPRIV… Zagreb        21. Gra… 6. En…
##  3            3 2            PRVO PLINARSKO DRUŠT… Vukovar       16. Vuk… 6. En…
##  4            4 6            KONZUM PLUS d.o.o.    Zagreb        21. Gra… 43. T…
##  5            5 7            HEP PROIZVODNJA d.o.… Zagreb        21. Gra… 6. En…
##  6            6 4            MET CROATIA ENERGY T… Zagreb        21. Gra… 6. En…
##  7            7 9            LIDL HRVATSKA d.o.o.… Velika Gorica 1. Zagr… 43. T…
##  8            8 8            PETROL d.o.o.         Zagreb        21. Gra… 22. N…
##  9            9 11           SPAR HRVATSKA d.o.o.  Zagreb        21. Gra… 43. T…
## 10           10 14           ZAGREBAČKA BANKA d.d. Zagreb        21. Gra… 8. Fi…
## # ℹ 990 more rows
## # ℹ 9 more variables: `Ukupni prihod (mil €)` <dbl>,
## #   `Promjena prihoda (%)` <chr>, `Bruto dobit/ gubitak (mil. €)` <dbl>,
## #   `Promjena dobiti/gubitka (%)` <chr>, `Dobit u odnosu na prihode (%)` <chr>,
## #   `Izvoz(mil. €)` <dbl>, `Uvoz(mil. €)` <dbl>, `Broj zaposlenih` <int>,
## #   `Prosječna  neto plaća (€)` <int>

Učitavanje iz baza podataka (SQLite primjer)

U poslovnom svijetu, veliki podaci se rijetko čuvaju samo u CSV-u. Većina poslovnih sustava koristi baze podataka.

SQLite je lagana lokalna baza (bez servera).

con <- dbConnect(RSQLite::SQLite(), ":memory:")
dbWriteTable(con, "diamonds", diamonds)
dbListTables(con)

db_data <- dbGetQuery(con, "SELECT carat, price FROM diamonds LIMIT 5")

Objašnjenje

  • dbConnect() otvara vezu prema bazi
  • :memory: znači da je baza privremena
  • dbWriteTable() sprema podatke u bazu
  • dbGetQuery() izvršava SQL upit

R + SQL + ETL pipeline

U realnim sustavima:

  • podaci dolaze iz MySQL, PostgreSQL, SQL Server
  • analitika se često radi nad podacima iz baze

Učitavanje javnih baza (Eurostat)

Javne institucije nude API pristup podacima.

library(eurostat)

eu_data <- get_eurostat("nama_10_gdp", time_format = "date")
## Dataset query already saved in cache_list.json...
## Reading cache file C:\Users\Korisnik\AppData\Local\Temp\Rtmp0S7mGT/eurostat/91181ca47edc7ac431bd7af54f9a9651.rds
## Table  nama_10_gdp  read from cache file:  C:\Users\Korisnik\AppData\Local\Temp\Rtmp0S7mGT/eurostat/91181ca47edc7ac431bd7af54f9a9651.rds
head(eu_data)
## # A tibble: 6 × 6
##   freq  unit       na_item geo   TIME_PERIOD values
##   <chr> <chr>      <chr>   <chr> <date>       <dbl>
## 1 A     CLV05_MEUR B1G     AL    1995-01-01   3422.
## 2 A     CLV05_MEUR B1G     AL    1996-01-01   3676.
## 3 A     CLV05_MEUR B1G     AL    1997-01-01   3279.
## 4 A     CLV05_MEUR B1G     AL    1998-01-01   3538 
## 5 A     CLV05_MEUR B1G     AL    1999-01-01   4031.
## 6 A     CLV05_MEUR B1G     AL    2000-01-01   4265.

Objašnjenje

  • get_eurostat() dohvaća podatke putem API-ja
  • vraća strukturirane podatke
  • parametar time_format = "date" odmah konvertira vremensku varijablu

Prednosti:

  • uvijek najnoviji podaci
  • automatizacija ETL procesa

Učitavanje podataka iz R biblioteka

Mnogi paketi sadrže skupove podataka za vježbu.

library(nycflights13)
data(flights)

head(flights)
## # A tibble: 6 × 19
##    year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##   <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
## 1  2013     1     1      517            515         2      830            819
## 2  2013     1     1      533            529         4      850            830
## 3  2013     1     1      542            540         2      923            850
## 4  2013     1     1      544            545        -1     1004           1022
## 5  2013     1     1      554            600        -6      812            837
## 6  2013     1     1      554            558        -4      740            728
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Objašnjenje

  • nycflights13 je edukativni dataset

  • sadrži podatke o letovima iz New Yorka

  • idealan za:

    • filtriranje
    • agregaciju
    • rad s datumima
    • ETL vježbe

Prednost:

  • neovisno o internetu
  • uvijek dostupno

Inicijalni pregled podataka

Nakon učitavanja podataka, ne prelazimo odmah na analizu. Prvo radimo inicijalni pregled (data audit).

Cilj:

  • razumjeti strukturu podataka
  • uočiti tipove varijabli
  • identificirati nedostajuće vrijednosti
  • uočiti nelogične vrijednosti
  • provjeriti jesu li datumi pravilno interpretirani

Provjera strukture podataka

Klasični (base R) pristup

str(diamonds)
## tibble [2,690 × 12] (S3: tbl_df/tbl/data.frame)
##  $ Carat.Size   : num [1:2690] 0.3 0.44 0.31 0.66 0.47 0.4 0.36 0.52 0.53 0.43 ...
##  $ Color        : chr [1:2690] "E" "E" "E" "K" ...
##  $ Clarity      : chr [1:2690] "VVS1" "VS2" "VVS1" "SI1" ...
##  $ Depth        : num [1:2690] 60 61.9 61.3 62.8 59.1 62 61.3 61.7 59.4 61.5 ...
##  $ Table        : num [1:2690] 59 58 58 57 64 59 57 61 59 60 ...
##  $ Cut          : chr [1:2690] "Excellent" "Excellent" "Excellent" "Excellent" ...
##  $ Report       : chr [1:2690] "GIA" "GIA" "GIA" "GIA" ...
##  $ Price        : num [1:2690] 1000 1000 1000 1000 1000 ...
##  $ Log.Price    : num [1:2690] 6.91 6.91 6.91 6.91 6.91 ...
##  $ Table.Depth  : num [1:2690] -1 -3.9 -3.3 -5.8 4.9 -3 -4.3 -0.7 -0.4 -1.5 ...
##  $ Table.Depth.1: num [1:2690] 0.983 0.937 0.946 0.908 1.083 ...
##  $ Test         : num [1:2690] 0 1 0 2 2 2 0 0 0 2 ...
summary(diamonds)
##    Carat.Size        Color             Clarity              Depth      
##  Min.   :0.3000   Length:2690        Length:2690        Min.   :56.40  
##  1st Qu.:0.6000   Class :character   Class :character   1st Qu.:61.00  
##  Median :0.9000   Mode  :character   Mode  :character   Median :61.90  
##  Mean   :0.8701                                         Mean   :61.71  
##  3rd Qu.:1.0600                                         3rd Qu.:62.50  
##  Max.   :2.0200                                         Max.   :64.30  
##      Table           Cut               Report              Price      
##  Min.   :53.00   Length:2690        Length:2690        Min.   : 1000  
##  1st Qu.:56.00   Class :character   Class :character   1st Qu.: 1801  
##  Median :58.00   Mode  :character   Mode  :character   Median : 3604  
##  Mean   :57.86                                         Mean   : 3971  
##  3rd Qu.:59.00                                         3rd Qu.: 5544  
##  Max.   :65.00                                         Max.   :10000  
##    Log.Price      Table.Depth      Table.Depth.1         Test       
##  Min.   :6.908   Min.   :-10.800   Min.   :0.8307   Min.   :0.0000  
##  1st Qu.:7.496   1st Qu.: -5.800   1st Qu.:0.9076   1st Qu.:0.0000  
##  Median :8.190   Median : -4.200   Median :0.9325   Median :0.0000  
##  Mean   :8.084   Mean   : -3.851   Mean   :0.9382   Mean   :0.6004  
##  3rd Qu.:8.620   3rd Qu.: -2.200   3rd Qu.:0.9636   3rd Qu.:1.0000  
##  Max.   :9.210   Max.   :  7.600   Max.   :1.1348   Max.   :2.0000
  • str() prikazuje:

    • broj opažanja
    • broj varijabli
    • tip svake varijable
  • summary() daje:

    • min, Q1, median, mean, Q3, max (za numeričke)
    • frekvencije (za faktore)

Zašto je to važno?

Ako je numerička varijabla učitana kao character, analize neće raditi kako očekujemo.

Tidyverse pristup

glimpse(diamonds)
## Rows: 2,690
## Columns: 12
## $ Carat.Size    <dbl> 0.30, 0.44, 0.31, 0.66, 0.47, 0.40, 0.36, 0.52, 0.53, 0.…
## $ Color         <chr> "E", "E", "E", "K", "H", "G", "D", "H", "D", "F", "F", "…
## $ Clarity       <chr> "VVS1", "VS2", "VVS1", "SI1", "VS2", "VS1", "VS2", "SI2"…
## $ Depth         <dbl> 60.0, 61.9, 61.3, 62.8, 59.1, 62.0, 61.3, 61.7, 59.4, 61…
## $ Table         <dbl> 59, 58, 58, 57, 64, 59, 57, 61, 59, 60, 59, 58, 62, 57, …
## $ Cut           <chr> "Excellent", "Excellent", "Excellent", "Excellent", "Ver…
## $ Report        <chr> "GIA", "GIA", "GIA", "GIA", "GIA", "GIA", "GIA", "GIA", …
## $ Price         <dbl> 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1001, 10…
## $ Log.Price     <dbl> 6.907755, 6.907755, 6.907755, 6.907755, 6.907755, 6.9077…
## $ Table.Depth   <dbl> -1.0, -3.9, -3.3, -5.8, 4.9, -3.0, -4.3, -0.7, -0.4, -1.…
## $ Table.Depth.1 <dbl> 0.9833333, 0.9369951, 0.9461664, 0.9076433, 1.0829103, 0…
## $ Test          <dbl> 0, 1, 0, 2, 2, 2, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 1, 2, 0,…

glimpse():

  • pregledniji ispis
  • ne zatrpava konzolu
  • odmah pokazuje tip varijable

Prednost za velike skupove podataka.

Provjera vrijednosti koje nedostaju

Base R pristup

summary(is.na(flights))
##     year           month            day           dep_time      
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:336776    FALSE:336776    FALSE:328521   
##                                                  TRUE :8255     
##  sched_dep_time  dep_delay        arr_time       sched_arr_time 
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:328521    FALSE:328063    FALSE:336776   
##                  TRUE :8255      TRUE :8713                     
##  arr_delay        carrier          flight         tailnum       
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:327346    FALSE:336776    FALSE:336776    FALSE:334264   
##  TRUE :9430                                      TRUE :2512     
##    origin           dest          air_time        distance      
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:336776    FALSE:327346    FALSE:336776   
##                                  TRUE :9430                     
##     hour           minute        time_hour      
##  Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:336776    FALSE:336776   
## 
  • is.na() vraća TRUE/FALSE matricu
  • summary() vraća sažetak (u ovom slučaju, sažetak prebrojavanja TRUE/FALSE)

Dobivamo broj NA vrijednosti po varijabli.

Tidyverse pristup

flights %>%
  summarise(across(everything(), ~sum(is.na(.))))
## # A tibble: 1 × 19
##    year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##   <int> <int> <int>    <int>          <int>     <int>    <int>          <int>
## 1     0     0     0     8255              0      8255     8713              0
## # ℹ 11 more variables: arr_delay <int>, carrier <int>, flight <int>,
## #   tailnum <int>, origin <int>, dest <int>, air_time <int>, distance <int>,
## #   hour <int>, minute <int>, time_hour <int>

Kako se gradi ova naredba?

Na prvi pogled izgleda složeno. Zato ćemo je rastaviti korak po korak.

Operator %>% zove se pipe (cijev).

On znači:

“Uzmi objekt s lijeve strane i proslijedi ga kao prvi argument funkciji s desne strane.”

Dakle:

flights %>% summarise(...)

je isto što i:

summarise(flights, ...)

summarise() služi za izračun sažetih statistika nad podacima.

Primjer:

flightse %>%
  summarise(mean = mean(dep_tima, na.rm = TRUE))

Rezultat je jedan red (jedan sažetak).

across() znači:

“Primijeni istu funkciju na više stupaca.”

Primjer:

flights %>%
  summarise(across(dep_time, mean))

Ali mi želimo primijeniti funkciju na sve stupce, zato koristimo:

everything()

što znači:

“Odaberi sve stupce.”

~sum(is.na(.)) - ovdje koristimo anonimnu funkciju (lambda funkciju). Znak ~ znači:

“Definiram kratku funkciju.”

To je skraćeni zapis za:

function(x) sum(is.na(x))

A točka . znači:

“trenutni stupac koji se obrađuje.”

Dakle:

~sum(is.na(.))

znači:

“Za svaki stupac izračunaj koliko ima NA vrijednosti.”

Sada spojimo sve zajedno

flights %>%
  summarise(across(everything(), ~sum(is.na(.))))

Čita se ovako:

  1. Uzmi dataset flights
  2. Za svaki stupac (everything())
  3. Izračunaj broj nedostajućih vrijednosti (sum(is.na(.)))
  4. Vrati jedan red sa sažetkom

Iako djeluje kompliciranije, korisnost ovog pristupa očituje se pri:

  • radu s vrlo velikim podacima
  • nizanju naredbi

Evo primjera za ovo posljednje, provjera po grupama:

flights %>%
  group_by(origin) %>%
  summarise(across(everything(), ~sum(is.na(.))))
## # A tibble: 3 × 19
##   origin  year month   day dep_time sched_dep_time dep_delay arr_time
##   <chr>  <int> <int> <int>    <int>          <int>     <int>    <int>
## 1 EWR        0     0     0     3239              0      3239     3390
## 2 JFK        0     0     0     1863              0      1863     1995
## 3 LGA        0     0     0     3153              0      3153     3328
## # ℹ 11 more variables: sched_arr_time <int>, arr_delay <int>, carrier <int>,
## #   flight <int>, tailnum <int>, dest <int>, air_time <int>, distance <int>,
## #   hour <int>, minute <int>, time_hour <int>

Sada dobivamo broj NA vrijednosti po polazištu.

Općenito, ovu naredbu možemo zapisati kao algoritam:

FOR svaki stupac u heart_rate:
    izračunaj broj NA vrijednosti
VRATI jedan red sa svim tim brojevima

Čišćenje podataka

Čišćenje podataka je faza transformacije koja uključuje:

  • rješavanje NA vrijednosti
  • uklanjanje nelogičnih vrijednosti
  • ispravak tipfelera
  • standardizaciju tipova

Standardizacija naziva

U stvarnim podacima nazivi stupaca često su:

  • s razmacima ("Carat Size")
  • s velikim slovima ("Price", "CUT")
  • s posebnim znakovima ("Price (€)")
  • s dijakriticima ("Županija")
  • nedosljedni ("Price", "price_value", "PriceUSD")

Takvi nazivi otežavaju:

  • programiranje
  • reproducibilnost
  • pisanje filtera i modela
  • automatizaciju

Zato u ETL fazi među prvim koracima radimo standardizaciju naziva.

Zašto je to važno?

Ovo:

diamonds$`Carat Size`

je puno nepraktičnije od ovoga:

diamonds$carat_size

Osim toga, u tidyverse pristupu razmaci u nazivima stvaraju dodatne komplikacije.


Ručna standardizacija (base R)

Možemo ručno promijeniti imena:

names(diamonds)
##  [1] "Carat.Size"    "Color"         "Clarity"       "Depth"        
##  [5] "Table"         "Cut"           "Report"        "Price"        
##  [9] "Log.Price"     "Table.Depth"   "Table.Depth.1" "Test"
names(diamonds) <- tolower(names(diamonds))
names(diamonds)
##  [1] "carat.size"    "color"         "clarity"       "depth"        
##  [5] "table"         "cut"           "report"        "price"        
##  [9] "log.price"     "table.depth"   "table.depth.1" "test"

Nedostatak:

  • ručno
  • sklono greškama
  • nepraktično za veće datasetove

Automatska standardizacija (preporučeno)

Najjednostavnije rješenje je paket janitor.

library(janitor)

diamonds <- janitor::clean_names(diamonds)

names(diamonds)
##  [1] "carat_size"    "color"         "clarity"       "depth"        
##  [5] "table"         "cut"           "report"        "price"        
##  [9] "log_price"     "table_depth"   "table_depth_1" "test"

Što clean_names() radi?

  • pretvara sve u mala slova
  • razmake pretvara u _
  • uklanja posebne znakove
  • uklanja dijakritike
  • osigurava sintaktički ispravne nazive

Primjer:

Prije Poslije
“Carat Size” carat_size
“Price (€)” price_eur
“Županija” zupanija

Preporučena ETL praksa

  1. Odmah nakon učitavanja podataka:
diamonds <- janitor::clean_names(diamonds)
  1. Nakon toga dosljedno koristiti samo standardizirane nazive

  2. U analitičkom izvještaju jasno navesti da je izvršena standardizacija


Mini-provjera razmaka

any(grepl(" ", names(diamonds)))
## [1] FALSE

Ako je rezultat FALSE, u nazivima nema razmaka koji bi mogli stvarati probleme.


Zašto je ovo važno u profesionalnoj praksi?

U timskom radu:

  • standardizirani nazivi omogućuju lakšu suradnju
  • olakšavaju automatizaciju
  • smanjuju broj skrivenih grešaka
  • omogućuju lakšu integraciju s bazama podataka i API-jem

Upravljanje vrijednostima koje nedostaju

Uklanjanje iz izračuna

Jednostavno isključivanje pomoću na.rm = TRUE

Ako samo želimo izračunati pokazatelje bez brisanja podataka:

mean(flights$dep_time, na.rm = TRUE)
## [1] 1349.11
median(flights$dep_time, na.rm = TRUE)
## [1] 1401

Što znači na.rm = TRUE?

  • ignorira NA vrijednosti

  • ne briše redove

  • primjenjuje se samo u toj funkciji

Ovo je najmanje invazivna metoda jer:

  • podaci ostaju neizmijenjeni

  • samo se NA ignorira u izračunu

Važno:

Ako radimo regresiju ili model, često će model automatski izbaciti retke s NA.

Uklanjanje iz skupa podataka

Korištenje which() za identifikaciju i uklanjanje po broju retka

which() vraća indekse (brojeve redaka) gdje je uvjet zadovoljen.

na_index <- which(is.na(flights$dep_time))

Sada znamo točno koji su redci problematični.

Uklanjanje:

flights_clean <- flights[-na_index, ]
str(flights_clean)
## tibble [328,521 × 19] (S3: tbl_df/tbl/data.frame)
##  $ year          : int [1:328521] 2013 2013 2013 2013 2013 2013 2013 2013 2013 2013 ...
##  $ month         : int [1:328521] 1 1 1 1 1 1 1 1 1 1 ...
##  $ day           : int [1:328521] 1 1 1 1 1 1 1 1 1 1 ...
##  $ dep_time      : int [1:328521] 517 533 542 544 554 554 555 557 557 558 ...
##  $ sched_dep_time: int [1:328521] 515 529 540 545 600 558 600 600 600 600 ...
##  $ dep_delay     : num [1:328521] 2 4 2 -1 -6 -4 -5 -3 -3 -2 ...
##  $ arr_time      : int [1:328521] 830 850 923 1004 812 740 913 709 838 753 ...
##  $ sched_arr_time: int [1:328521] 819 830 850 1022 837 728 854 723 846 745 ...
##  $ arr_delay     : num [1:328521] 11 20 33 -18 -25 12 19 -14 -8 8 ...
##  $ carrier       : chr [1:328521] "UA" "UA" "AA" "B6" ...
##  $ flight        : int [1:328521] 1545 1714 1141 725 461 1696 507 5708 79 301 ...
##  $ tailnum       : chr [1:328521] "N14228" "N24211" "N619AA" "N804JB" ...
##  $ origin        : chr [1:328521] "EWR" "LGA" "JFK" "JFK" ...
##  $ dest          : chr [1:328521] "IAH" "IAH" "MIA" "BQN" ...
##  $ air_time      : num [1:328521] 227 227 160 183 116 150 158 53 140 138 ...
##  $ distance      : num [1:328521] 1400 1416 1089 1576 762 ...
##  $ hour          : num [1:328521] 5 5 5 5 6 5 6 6 6 6 ...
##  $ minute        : num [1:328521] 15 29 40 45 0 58 0 0 0 0 ...
##  $ time_hour     : POSIXct[1:328521], format: "2013-01-01 05:00:00" "2013-01-01 05:00:00" ...

Što znači minus ispred indeksa?

  • negativni indeks znači: “izostavi ove retke”

Ovaj pristup je eksplicitan i često se koristi kad:

  • želimo manipulirati indeksima

  • želimo dodatno analizirati raspored NA po redoslijedu

Imputacija

Base R pristup

summary(is.na(flights))
##     year           month            day           dep_time      
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:336776    FALSE:336776    FALSE:328521   
##                                                  TRUE :8255     
##  sched_dep_time  dep_delay        arr_time       sched_arr_time 
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:328521    FALSE:328063    FALSE:336776   
##                  TRUE :8255      TRUE :8713                     
##  arr_delay        carrier          flight         tailnum       
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:327346    FALSE:336776    FALSE:336776    FALSE:334264   
##  TRUE :9430                                      TRUE :2512     
##    origin           dest          air_time        distance      
##  Mode :logical   Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:336776    FALSE:327346    FALSE:336776   
##                                  TRUE :9430                     
##     hour           minute        time_hour      
##  Mode :logical   Mode :logical   Mode :logical  
##  FALSE:336776    FALSE:336776    FALSE:336776   
## 
flights$dep_delay[is.na(flights$dep_delay)] <-   median(flights$dep_delay, na.rm = TRUE)

Što se događa?

  • lociramo NA vrijednosti
  • zamjenjujemo ih medijanom
  • na.rm = TRUE ignorira NA u računu

Zašto medijan?

  • robusniji je na outliere
  • često metodološki opravdaniji od aritmetičke sredine

Tidyverse pristup

flights <- flights %>%
  mutate(dep_delay = ifelse(is.na(dep_delay),
                       median(dep_delay, na.rm = TRUE),
                       dep_delay))

Prednost:

  • uklapa se u pipeline
  • čitljivije u složenijim transformacijama

Pogrešno upisane vrijednosti

Primjer:

x <- c("10", "12", "o5", "18")
as.numeric(x)
## Warning: NAs introduced by coercion
## [1] 10 12 NA 18

Što se događa?

  • “o5” nije numerički zapis
  • R ga pretvara u NA
  • generira upozorenje

Ispravak

x_clean <- gsub("o", "0", x)
as.numeric(x_clean)
## [1] 10 12  5 18
  • gsub() zamjenjuje znakove u stringu, u ovom slučaju zamjenjuje slovo o s nulom
  • nakon toga varijablu možemo konvertirati u numeric.

Transformacija varijabli

Transformacija znači:

  • promjena tipa
  • promjena strukture
  • dodavanje novih varijabli

Promjena tipa

diamonds$price <- as.numeric(diamonds$price)
str(diamonds)
## tibble [2,690 × 12] (S3: tbl_df/tbl/data.frame)
##  $ carat_size   : num [1:2690] 0.3 0.44 0.31 0.66 0.47 0.4 0.36 0.52 0.53 0.43 ...
##  $ color        : chr [1:2690] "E" "E" "E" "K" ...
##  $ clarity      : chr [1:2690] "VVS1" "VS2" "VVS1" "SI1" ...
##  $ depth        : num [1:2690] 60 61.9 61.3 62.8 59.1 62 61.3 61.7 59.4 61.5 ...
##  $ table        : num [1:2690] 59 58 58 57 64 59 57 61 59 60 ...
##  $ cut          : chr [1:2690] "Excellent" "Excellent" "Excellent" "Excellent" ...
##  $ report       : chr [1:2690] "GIA" "GIA" "GIA" "GIA" ...
##  $ price        : num [1:2690] 1000 1000 1000 1000 1000 ...
##  $ log_price    : num [1:2690] 6.91 6.91 6.91 6.91 6.91 ...
##  $ table_depth  : num [1:2690] -1 -3.9 -3.3 -5.8 4.9 -3 -4.3 -0.7 -0.4 -1.5 ...
##  $ table_depth_1: num [1:2690] 0.983 0.937 0.946 0.908 1.083 ...
##  $ test         : num [1:2690] 0 1 0 2 2 2 0 0 0 2 ...

Napomena: U R-u postoje tri slične naredbe koje se često miješaju:

Funkcija Što radi? Koristi se kad
is.numeric() Provjerava je li objekt numerički želimo testirati tip
as.numeric() Pretvara objekt u numerički želimo promijeniti tip
numeric() Kreira prazan numerički vektor želimo inicijalizirati objekt

slične funkcije za druge tipove

is.character() / as.character()

is.factor() / as.factor()

is.Date() / as.Date()

is.integer() / as.integer()

Tidyverse pristup

diamonds <- diamonds %>%
  mutate(price = as.numeric(price))

Prednost:

  • ne moramo koristiti $
  • jasnije kod složenijih transformacija

Faktori

diamonds$cut <- factor(diamonds$cut, 
                       levels = c("Good", "Very Good", "Excellent", "Ideal"), 
                       ordered = TRUE)
str(diamonds)
## 'data.frame':    2690 obs. of  12 variables:
##  $ carat_size   : num  0.3 0.44 0.31 0.66 0.47 0.4 0.36 0.52 0.53 0.43 ...
##  $ color        : chr  "E" "E" "E" "K" ...
##  $ clarity      : chr  "VVS1" "VS2" "VVS1" "SI1" ...
##  $ depth        : num  60 61.9 61.3 62.8 59.1 62 61.3 61.7 59.4 61.5 ...
##  $ table        : num  59 58 58 57 64 59 57 61 59 60 ...
##  $ cut          : Ord.factor w/ 4 levels "Good"<"Very Good"<..: 3 3 3 3 2 3 3 2 2 3 ...
##  $ report       : chr  "GIA" "GIA" "GIA" "GIA" ...
##  $ price        : num  1000 1000 1000 1000 1000 ...
##  $ log_price    : num  6.91 6.91 6.91 6.91 6.91 ...
##  $ table_depth  : num  -1 -3.9 -3.3 -5.8 4.9 -3 -4.3 -0.7 -0.4 -1.5 ...
##  $ table_depth_1: num  0.983 0.937 0.946 0.908 1.083 ...
##  $ test         : num  0 1 0 2 2 2 0 0 0 2 ...

Zašto je ovo važno?

Ako ne definiramo redoslijed:

  • model će tretirati varijablu kao nominalnu
  • redoslijed neće biti poznat

Kreiranje nove varijable

Cilj: dobiti informativniju mjeru.

diamonds$price_per_carat <- diamonds$price / diamonds$carat_size
str(diamonds)
## 'data.frame':    2690 obs. of  13 variables:
##  $ carat_size     : num  0.3 0.44 0.31 0.66 0.47 0.4 0.36 0.52 0.53 0.43 ...
##  $ color          : chr  "E" "E" "E" "K" ...
##  $ clarity        : chr  "VVS1" "VS2" "VVS1" "SI1" ...
##  $ depth          : num  60 61.9 61.3 62.8 59.1 62 61.3 61.7 59.4 61.5 ...
##  $ table          : num  59 58 58 57 64 59 57 61 59 60 ...
##  $ cut            : Ord.factor w/ 4 levels "Good"<"Very Good"<..: 3 3 3 3 2 3 3 2 2 3 ...
##  $ report         : chr  "GIA" "GIA" "GIA" "GIA" ...
##  $ price          : num  1000 1000 1000 1000 1000 ...
##  $ log_price      : num  6.91 6.91 6.91 6.91 6.91 ...
##  $ table_depth    : num  -1 -3.9 -3.3 -5.8 4.9 -3 -4.3 -0.7 -0.4 -1.5 ...
##  $ table_depth_1  : num  0.983 0.937 0.946 0.908 1.083 ...
##  $ test           : num  0 1 0 2 2 2 0 0 0 2 ...
##  $ price_per_carat: num  3333 2273 3226 1515 2128 ...

Tidyverse

diamonds <- diamonds %>%
  mutate(price_per_carat = price / carat_size)
glimpse(diamonds)
## Rows: 2,690
## Columns: 13
## $ carat_size      <dbl> 0.30, 0.44, 0.31, 0.66, 0.47, 0.40, 0.36, 0.52, 0.53, …
## $ color           <chr> "E", "E", "E", "K", "H", "G", "D", "H", "D", "F", "F",…
## $ clarity         <chr> "VVS1", "VS2", "VVS1", "SI1", "VS2", "VS1", "VS2", "SI…
## $ depth           <dbl> 60.0, 61.9, 61.3, 62.8, 59.1, 62.0, 61.3, 61.7, 59.4, …
## $ table           <dbl> 59, 58, 58, 57, 64, 59, 57, 61, 59, 60, 59, 58, 62, 57…
## $ cut             <ord> Excellent, Excellent, Excellent, Excellent, Very Good,…
## $ report          <chr> "GIA", "GIA", "GIA", "GIA", "GIA", "GIA", "GIA", "GIA"…
## $ price           <dbl> 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1001, …
## $ log_price       <dbl> 6.907755, 6.907755, 6.907755, 6.907755, 6.907755, 6.90…
## $ table_depth     <dbl> -1.0, -3.9, -3.3, -5.8, 4.9, -3.0, -4.3, -0.7, -0.4, -…
## $ table_depth_1   <dbl> 0.9833333, 0.9369951, 0.9461664, 0.9076433, 1.0829103,…
## $ test            <dbl> 0, 1, 0, 2, 2, 2, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 1, 2, …
## $ price_per_carat <dbl> 3333.333, 2272.727, 3225.806, 1515.152, 2127.660, 2500…

Zašto?

  • omjer često daje bolju usporedivost
  • standardizacija mjere

Datum i vrijeme

  • Date = datum bez vremena

  • POSIXct = datum+vrijeme, dobar za logove/senzore

library(lubridate)

x <- c("2026-02-25", "2026-03-01")
x <- as.Date(x, format = "%Y-%m-%d")
str(x)
##  Date[1:2], format: "2026-02-25" "2026-03-01"
class(x)
## [1] "Date"
y <- c("1jan1960", "2jan1960", "31mar1960", "30jul1960")
y <- as.Date(y, "%d%b%Y")
str(y)
##  Date[1:4], format: NA NA NA NA
class(y)
## [1] "Date"
t <- c("2026-02-25 14:30:00", "2026-02-25 16:10:00")
t <- as.POSIXct(t, format = "%Y-%m-%d %H:%M:%OS")
str(t)
##  POSIXct[1:2], format: "2026-02-25 14:30:00" "2026-02-25 16:10:00"
class(t)
## [1] "POSIXct" "POSIXt"

Duplikati

sum(duplicated(flights))
## [1] 0
flights_unique <- flights[!duplicated(flights), ]

i tidyverse alternativa:

flights_unique <- flights %>% distinct()
glimpse(flights_unique)
## Rows: 336,776
## Columns: 19
## $ year           <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2…
## $ month          <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ day            <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ dep_time       <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 558, …
## $ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 600, …
## $ dep_delay      <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2, -1…
## $ arr_time       <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 849,…
## $ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 851,…
## $ arr_delay      <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7, -1…
## $ carrier        <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6", "…
## $ flight         <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301, 4…
## $ tailnum        <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N394…
## $ origin         <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LGA",…
## $ dest           <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IAD",…
## $ air_time       <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149, 1…
## $ distance       <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 733, …
## $ hour           <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 6…
## $ minute         <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0…
## $ time_hour      <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-01 0…

Outlieri i osnovne provjere

Provjeriti:

  • negativne vrijednosti (ima li ih gdje ne smiju biti)

  • raspone (npr. between())

  • summary(diamonds$price)

Tidyverse:

diamonds %>% filter(price < 0 | carat_size <= 0)
##  [1] carat_size      color           clarity         depth          
##  [5] table           cut             report          price          
##  [9] log_price       table_depth     table_depth_1   test           
## [13] price_per_carat
## <0 rows> (or 0-length row.names)

Podskup podataka

Prema kvalitativnom obilježju

premium_diamonds <- diamonds[diamonds$cut == "Ideal", ]
str(premium_diamonds)
## 'data.frame':    185 obs. of  13 variables:
##  $ carat_size     : num  0.51 0.41 0.33 0.33 0.4 0.32 0.37 0.38 0.31 0.35 ...
##  $ color          : chr  "I" "G" "D" "E" ...
##  $ clarity        : chr  "SI1" "VS1" "VVS2" "VVS1" ...
##  $ depth          : num  61.7 61.4 60.7 61.4 61.7 61.8 61.4 61.5 61.9 61.6 ...
##  $ table          : num  55 55 57 57 57 55 57 56 56 57 ...
##  $ cut            : Ord.factor w/ 4 levels "Good"<"Very Good"<..: 4 4 4 4 4 4 4 4 4 4 ...
##  $ report         : chr  "GIA" "GIA" "GIA" "GIA" ...
##  $ price          : num  1017 1018 1019 1036 1037 ...
##  $ log_price      : num  6.92 6.93 6.93 6.94 6.94 ...
##  $ table_depth    : num  -6.7 -6.4 -3.7 -4.4 -4.7 -6.8 -4.4 -5.5 -5.9 -4.6 ...
##  $ table_depth_1  : num  0.891 0.896 0.939 0.928 0.924 ...
##  $ test           : num  0 0 0 2 2 0 0 2 1 1 ...
##  $ price_per_carat: num  1994 2483 3088 3139 2592 ...

Tidyverse

premium_diamonds <- diamonds %>%
  filter(cut == "Ideal")

Prednost tidyverse:

  • čitljiviji kod
  • lakše kombiniranje više uvjeta

Prema kvantitativnom obilježju

large_diamonds <- diamonds[diamonds$carat_size > 2, ]
str(large_diamonds)
## 'data.frame':    2 obs. of  13 variables:
##  $ carat_size     : num  2.02 2.02
##  $ color          : chr  "K" "J"
##  $ clarity        : chr  "SI2" "SI2"
##  $ depth          : num  63.4 63.7
##  $ table          : num  56 58
##  $ cut            : Ord.factor w/ 4 levels "Good"<"Very Good"<..: 2 2
##  $ report         : chr  "GIA" "GIA"
##  $ price          : num  9217 9984
##  $ log_price      : num  9.13 9.21
##  $ table_depth    : num  -7.4 -5.7
##  $ table_depth_1  : num  0.883 0.911
##  $ test           : num  0 0
##  $ price_per_carat: num  4563 4943

Tidyverse

large_diamonds <- diamonds %>%
  filter(carat_size > 2)
glimpse(large_diamonds)
## Rows: 2
## Columns: 13
## $ carat_size      <dbl> 2.02, 2.02
## $ color           <chr> "K", "J"
## $ clarity         <chr> "SI2", "SI2"
## $ depth           <dbl> 63.4, 63.7
## $ table           <dbl> 56, 58
## $ cut             <ord> Very Good, Very Good
## $ report          <chr> "GIA", "GIA"
## $ price           <dbl> 9217, 9984
## $ log_price       <dbl> 9.128805, 9.208739
## $ table_depth     <dbl> -7.4, -5.7
## $ table_depth_1   <dbl> 0.8832808, 0.9105180
## $ test            <dbl> 0, 0
## $ price_per_carat <dbl> 4562.871, 4942.574

Random podskup

Na primjer, na nasumičan način odabiremo uzorak od 100 dijamanata. Kako je u pitanju slučajni odabir, postavljamo seed kako bismo ponovnim uzorkovanjem dobili isti uzorak.

set.seed(123)
sample_data <- diamonds[sample(nrow(diamonds), 100), ]
str(sample_data)
## 'data.frame':    100 obs. of  13 variables:
##  $ carat_size     : num  1.41 1.03 1.3 0.59 0.51 1.25 0.9 0.81 0.71 0.74 ...
##  $ color          : chr  "H" "F" "F" "E" ...
##  $ clarity        : chr  "SI1" "VS2" "SI2" "SI1" ...
##  $ depth          : num  61.7 62.3 60.4 62.6 60.2 62.8 63.8 60.1 61.1 62.2 ...
##  $ table          : num  62 56 58 59 60 61 58 59 60 58 ...
##  $ cut            : Ord.factor w/ 4 levels "Good"<"Very Good"<..: 2 3 2 3 3 2 1 3 3 3 ...
##  $ report         : chr  "GIA" "GIA" "GIA" "GIA" ...
##  $ price          : num  7995 8229 6523 1591 1156 ...
##  $ log_price      : num  8.99 9.02 8.78 7.37 7.05 ...
##  $ table_depth    : num  0.3 -6.3 -2.4 -3.6 -0.2 -1.8 -5.8 -1.1 -1.1 -4.2 ...
##  $ table_depth_1  : num  1.005 0.899 0.96 0.942 0.997 ...
##  $ test           : num  0 1 0 2 0 0 0 0 0 2 ...
##  $ price_per_carat: num  5670 7989 5018 2697 2267 ...

Tidyverse

set.seed(123)
sample_data <- diamonds %>%
  slice_sample(n = 100)
glimpse(sample_data)
## Rows: 100
## Columns: 13
## $ carat_size      <dbl> 1.41, 1.03, 1.30, 0.59, 0.51, 1.25, 0.90, 0.81, 0.71, …
## $ color           <chr> "H", "F", "F", "E", "F", "J", "G", "E", "E", "D", "F",…
## $ clarity         <chr> "SI1", "VS2", "SI2", "SI1", "SI1", "VS2", "SI2", "SI1"…
## $ depth           <dbl> 61.7, 62.3, 60.4, 62.6, 60.2, 62.8, 63.8, 60.1, 61.1, …
## $ table           <dbl> 62, 56, 58, 59, 60, 61, 58, 59, 60, 58, 57, 59, 59, 61…
## $ cut             <ord> Very Good, Excellent, Very Good, Excellent, Excellent,…
## $ report          <chr> "GIA", "GIA", "GIA", "GIA", "GIA", "GIA", "GIA", "GIA"…
## $ price           <dbl> 7995, 8229, 6523, 1591, 1156, 4950, 2877, 3167, 3339, …
## $ log_price       <dbl> 8.986572, 9.015420, 8.783090, 7.372118, 7.052721, 8.50…
## $ table_depth     <dbl> 0.3, -6.3, -2.4, -3.6, -0.2, -1.8, -5.8, -1.1, -1.1, -…
## $ table_depth_1   <dbl> 1.0048622, 0.8988764, 0.9602649, 0.9424920, 0.9966777,…
## $ test            <dbl> 0, 1, 0, 2, 0, 0, 0, 0, 0, 2, 2, 1, 0, 1, 2, 1, 0, 2, …
## $ price_per_carat <dbl> 5670.213, 7989.320, 5017.692, 2696.610, 2266.667, 3960…

Osnovne vizualizacije podataka

Kvalitativne varijable

barplot(table(diamonds$cut))

Strukturni krug je popularan, ali neprimjeren za precizniju usporedbu.

cut_freq <- table(diamonds$cut)

pie(cut_freq,
    main = "Distribucija reza (Cut)",
    clockwise = TRUE)

Kombinacija kvalitativnih varijabli

table_cut_color <- table(diamonds$cut, diamonds$color)
barplot(table_cut_color, beside = TRUE)

tab_cut_color <- table(diamonds$cut, diamonds$color)

mosaicplot(tab_cut_color,
           main = "Mosaic: Cut × Color",
           las = 2)

Kako čitati mosaic:

  • širina stupca ~ ukupna frekvencija prve varijable

  • visine unutar stupca ~ udjeli druge varijable

  • promatramo uzorke ploha


Kvantitativne varijable

hist(diamonds$price, breaks = 30)

boxplot(diamonds$price)

Kvantitativna ~ kvalitativna

boxplot(price ~ cut, data = diamonds)


Tablični prikaz podataka

Jednostavna tablica

Jednostavna tablica prikazuje jednu varijablu za jedan statistički skup. Ako je u pitanju numerička varijabla, onda kažemo da su podaci grupirani, jer će u predstupcu biti razredi. Za kvalitativnu varijablu, u predstupcu će biti zapisani modaliteti. Prvi stupac se odnosi na frekvencije.

freq_price <- table(cut(diamonds$price, breaks = 10))
freq_price
## 
##     (991,1.9e+03] (1.9e+03,2.8e+03] (2.8e+03,3.7e+03] (3.7e+03,4.6e+03] 
##               720               399               264               349 
## (4.6e+03,5.5e+03] (5.5e+03,6.4e+03] (6.4e+03,7.3e+03] (7.3e+03,8.2e+03] 
##               272               200               162               128 
## (8.2e+03,9.1e+03]   (9.1e+03,1e+04] 
##                95               101
tabyl(diamonds$cut) # iz paketa janitor
##  diamonds$cut    n    percent
##          Good  165 0.06133829
##     Very Good 1064 0.39553903
##     Excellent 1276 0.47434944
##         Ideal  185 0.06877323

Tablica kontingencije

Tablica kontingencije je sažeti prikaz frekvencija koje se dobivaju presjekom dvije varijable, mjerene za jedan statistički skup.

table(diamonds$cut, diamonds$color)
##            
##               D   E   F   G   H   I   J   K
##   Good       16  29  23  22  23  23  21   8
##   Very Good 106 209 164 130 154 124 121  56
##   Excellent 139 227 213 209 192 142 102  52
##   Ideal      16  39  31  35  25  27   9   3

Skupna tablica

Skupna tablica predstavlja frekvencije modaliteta (ili razreda) jedne varijable mjerene za više statističkih skupova. Ako dijamante s različitim rezom promatramo zasebno, pri čemu je svaki tip reza zasebni statistički skup, tada ćemo tablicu kreirati na ovaj način:

diamonds$price_class <- cut(diamonds$price, breaks = 10)

head(diamonds$price_class)
## [1] (991,1.9e+03] (991,1.9e+03] (991,1.9e+03] (991,1.9e+03] (991,1.9e+03]
## [6] (991,1.9e+03]
## 10 Levels: (991,1.9e+03] (1.9e+03,2.8e+03] ... (9.1e+03,1e+04]
table(diamonds$price_class, diamonds$cut)
##                    
##                     Good Very Good Excellent Ideal
##   (991,1.9e+03]       45       285       344    46
##   (1.9e+03,2.8e+03]   16       153       205    25
##   (2.8e+03,3.7e+03]   26       111       107    20
##   (3.7e+03,4.6e+03]   22       179       132    16
##   (4.6e+03,5.5e+03]   22       101       130    19
##   (5.5e+03,6.4e+03]   16        74        95    15
##   (6.4e+03,7.3e+03]    4        62        87     9
##   (7.3e+03,8.2e+03]    6        42        74     6
##   (8.2e+03,9.1e+03]    6        28        49    12
##   (9.1e+03,1e+04]      2        29        53    17

Tablica statističkih pokazatelja

sp <- describe(diamonds$price)
sp
##    vars    n    mean      sd median trimmed     mad  min   max range skew
## X1    1 2690 3971.47 2420.23   3604  3724.7 2686.47 1000 10000  9000 0.68
##    kurtosis    se
## X1    -0.51 46.66
t(sp) # transponiramo za klasični prikaz
##                     X1
## vars         1.0000000
## n         2690.0000000
## mean      3971.4713755
## sd        2420.2341906
## median    3604.0000000
## trimmed   3724.6970260
## mad       2686.4712000
## min       1000.0000000
## max      10000.0000000
## range     9000.0000000
## skew         0.6849823
## kurtosis    -0.5088668
## se          46.6639236

Pohrana

U ETL procesu, faza Load znači da “pripremljene” podatke i rezultate:

  • spremamo u datoteke (CSV, Excel, RDS…)

  • spremamo u bazu podataka

  • spremamo grafove kao slike

  • spremamo tablice kao izvještajne artefakte (CSV/HTML)

Cilj je da rezultat bude ponovljiv i prenosiv (npr. za kolege, za kasniji rad, za izvještaj).

Preporuka: struktura mapa u projektu

dir.create("data_raw", showWarnings = FALSE)
dir.create("data_clean", showWarnings = FALSE)
dir.create("outputs", showWarnings = FALSE)
dir.create("outputs/figures", showWarnings = FALSE)
dir.create("outputs/tables", showWarnings = FALSE)

Pohrana “clean” verzije

Pohrana u csv

readr::write_csv(diamonds, "data_clean/diamonds_clean.csv") # datoteka data_clean, dokument diamonds_clean.csv

Pohrana u xlsx

library(openxlsx)
write.xlsx(diamonds, "data_clean/diamonds_clean.xlsx")

Pohrana u RDS

RDS čuva objekt “kako jest” (tipove varijabli, faktore, datume). Najbolja opcija za ponovno korištenje u R-u.

saveRDS(diamonds, "data_clean/diamonds_clean.rds")

Pohrana u SQLite bazu

con <- dbConnect(RSQLite::SQLite(), "data_clean/etl_example.sqlite")

dbWriteTable(con, "diamonds_clean", diamonds, overwrite = TRUE)
dbListTables(con)

dbDisconnect(con)

Pohrana rezultata

Pohrana slike (grafičkog prikaza)

U R-u graf možemo spremiti tako da:

  • otvorimo “uređaj” (npr. PNG)

  • nacrtamo graf

  • zatvorimo uređaj (dev.off())

Base R graf → PNG

png("outputs/figures/hist_price.png", width = 900, height = 600)
hist(diamonds$price, breaks = 30, main = "Histogram cijena", xlab = "Price")
dev.off()

ggplot graf → PNG

# install.packages("ggplot2")
library(ggplot2)

p <- ggplot(diamonds, aes(x = Price)) + geom_histogram(bins = 30)

ggsave("outputs/figures/gg_hist_price.png", plot = p, width = 8, height = 5, dpi = 150)

PDF (dobro za izvještaje i tisak)

pdf("outputs/figures/boxplot_price_by_cut.pdf", width = 8, height = 5)
boxplot(price ~ cut, data = diamonds, main = "Price ~ Cut", xlab = "Cut", ylab = "Price")
dev.off()

Pohrana tablica

Tablica frekvencija → CSV

Primjer: frekvencije razreda cijena.

freq_price <- table(cut(diamonds$price, breaks = 10))
freq_price
## 
##     (991,1.9e+03] (1.9e+03,2.8e+03] (2.8e+03,3.7e+03] (3.7e+03,4.6e+03] 
##               720               399               264               349 
## (4.6e+03,5.5e+03] (5.5e+03,6.4e+03] (6.4e+03,7.3e+03] (7.3e+03,8.2e+03] 
##               272               200               162               128 
## (8.2e+03,9.1e+03]   (9.1e+03,1e+04] 
##                95               101

Pretvorba u data frame i spremanje:

freq_price_df <- as.data.frame(freq_price)
names(freq_price_df) <- c("Price_class", "Frequency")

readr::write_csv(freq_price_df, "outputs/tables/freq_price.csv")

Kontingencijska tablica (Cut × Color) → CSV

tab_cut_color <- table(diamonds$cut, diamonds$color)
tab_cut_color

Spremanje:

tab_df <- as.data.frame(tab_cut_color)
names(tab_df) <- c("Cut", "Color", "Frequency")

readr::write_csv(tab_df, "outputs/tables/cut_by_color.csv")

Tablica statističkih pokazatelja → CSV

sp <- psych::describe(diamonds$price)
sp_df <- as.data.frame(t(sp))

readr::write_csv(sp_df, "outputs/tables/describe_price.csv")
sp_df

Tablica u HTML (za web/izvještaj)

library(knitr)
knitr::kable(freq_price_df, caption = "Frekvencijska tablica: Price (razredi)")