Ishodi učenja:

  • Osmisliti instrument za prikupljanje mrežnih podataka (name generator/roster).

  • Pripremiti podatke u edge list / adjacency format i validirati konzistentnost.

  • Procijeniti utjecaj missing podataka i predložiti strategije ublažavanja.

  • Primijeniti etičke principe (anonimizacija, minimizacija podataka).

# Paketi korišteni u ovoj lekciji
# Manipulacija i transformacija podataka
library(dplyr)       # filter, mutate, select, joins
library(tidyr)       # pivot_longer, pivot_wider
library(stringr)     # rad sa stringovima
library(readr)       # read_csv, parse_number
library(readxl)      # read_excel
library(janitor)     # clean_names
library(purrr)       # map, map_df
library(tibble)      # tibble, rownames_to_column
library(lubridate)   # rad s datumima i vremenom

# Mrežna analiza i vizualizacija
library(igraph)      # grafovi, centralnosti, layout
library(tidygraph)   # tidy pristup grafovima
library(ggraph)      # vizualizacija grafova

# Web scraping
library(rvest)       # read_html, html_table, html_elements

# Baze podataka
library(DBI)         # sučelje za baze
library(RSQLite)     # SQLite driver

# Rad s matricama (Matrix Market, sparse matrice)
library(Matrix)      # readMM

# Anonimizacija / hash
library(digest)      # SHA-256 hash funkcija

# Javne baze podataka (npr. Eurostat)
library(eurostat)    # get_eurostat

Zašto “prikupljanje i priprema” u mrežnoj analizi?

U mrežnoj analizi kvaliteta rezultata izravno ovisi o kvaliteti podataka:

  • jesu li veze interpretabilne (što znači edge?),
  • jesu li čvorovi jednoznačno identificirani,
  • jesu li missing podaci sistematski,
  • jesu li etički i pravno prihvatljivo prikupljeni (privola, minimizacija, anonimizacija).

Osnovni pojmovi i “što točno prikupljamo?”

Jedinice: čvorovi, veze i atributi

  • Čvor (node/vertex): osoba, organizacija, račun, web-stranica, pojam…
  • Veza (edge/tie): prijateljstvo, komunikacija, suradnja, link, spominjanje, transakcija…
  • Atributi (metadata): npr. spol, odjel, godina, tip organizacije (za čvor) ili jačina veze / vrijeme / kanal (za vezu).

Ključno pitanje: što veza znači i kako se mjeri?

Primjeri:

  • “S kim ste surađivali u zadnja 2 tjedna?” (binary ili frekvencija)
  • “Kome biste se obratili za pomoć oko R-a?” (usmjereno)
  • “Koliko često razmjenjujete materijale?” (težine)

Granice mreže i ID strategija (boundary + jedinstvenost)

Prije prikupljanja podataka treba odrediti granice mreže (tko je “unutra”, tko “izvan”) i strategiju identifikacije čvorova.

  • Ako koristimo ime i prezime, postoji rizik varijanti (dijakritika, razmaci, nadimci) i potencijalno isti naziv za dvije osobe.

  • Stabilniji su interni ID-evi (npr. studentski ID, e-mail), ali to često ima etičke implikacije pa treba minimizirati i zaštititi podatke.

  • Kod NG-a je dobro imati polje koje olakšava razrješenje identiteta (npr. “ime + prezime + grupa”, ili “alias”).

# roster_ids predstavlja popis "dopuštenih" čvorova (npr. svi studenti u grupi)
roster_ids <- c("ana", "iva", "marko", "luka")

edges_tmp <- tibble::tibble(
  from = c("Ana ", "Ana", "Marko"),
  to   = c("Iva", "IvA", "Ivan"),   # "Ivan" nije u rosteru
  weight = c(1, 2, 1)
) %>%
  mutate(
    across(c(from,to), ~ str_to_lower(str_squish(.x)))
  )

# out-of-scope čvorovi (boundary problem)

setdiff(unique(edges_tmp$to), roster_ids)
## [1] "ivan"
# potencijalni konflikti: isto “normalizirano” ime iz više originala
# (u praksi bi ovdje imali mapu original->standard)

edges_tmp
## # A tibble: 3 × 3
##   from  to    weight
##   <chr> <chr>  <dbl>
## 1 ana   iva        1
## 2 ana   iva        2
## 3 marko ivan       1

Vrste mreža (minimalno potrebno za pripremu podataka)

  • Usmjerena (directed) vs neusmjerena (undirected)
  • Binary vs weighted
  • Jedna vrsta čvorova vs dvomodalna (bipartite)

Instrumenti za prikupljanje mrežnih podataka

Name generator – kada i zašto?

Name generator traži od ispitanika da sam navede imena kontakata.

  • Prednosti: fleksibilno, hvata “realnu” mrežu.

  • Rizici: spelling varijacije, propuštanje veza, kognitivno opterećenje.

Primjeri NG pitanja:

  • “Navedite do 5 kolega s kojima ste najviše razmjenjivali informacije o kolegiju u zadnja 2 tjedna.”
  • “Navedite osobe kojima biste se prvo obratili za pomoć oko zadatka.”
  • (weighted) “Za svaku navedenu osobu označite učestalost komunikacije: 1=rijetko … 5=vrlo često.”

Praktični savjet (za konzistentnost):

  • ograničite broj navoda (npr. do 5 ili 10),
  • koristite dodatna polja: “ime + prezime”, “nadimak”, “email/ID” (ako smijete),
  • pripremite pravila za standardizaciju (lowercase, trim, uklanjanje dijakritika - ako treba).

Name interpreter i pravila kodiranja odgovora

Name interpreter su dodatna pitanja koja dolaze nakon name generatora: za svaku navedenu osobu (alter) prikupljamo detalje o odnosu (npr. učestalost, kanal, tip pomoći, povjerenje). Time mreža postaje informativnija: dobivamo težine, tipove veza i/ili vremenski okvir.

U praksi je važno unaprijed definirati pravila kodiranja: kako odgovore pretvaramo u weight (0/1 ili 0–5), što znači “ne znam”, i kako tretiramo “nije primjenjivo”.

# Primjer: NG + interpreter (učestalost komunikacije 0–5 + "ne znam")
ng_raw <- tibble::tibble(
  ego = c("Ana","Ana","Ana","Marko","Marko"),
  alter = c("Iva","Marko","Luka","Ana","Iva"),
  freq = c("5", "3", "ne znam", "2", "0")   # namjerno "ne znam"
)

ng_clean <- ng_raw %>%
  mutate(
    ego   = str_to_lower(str_trim(ego)),
    alter = str_to_lower(str_trim(alter)),
    # kodiranje: "ne znam" -> NA, inače broj
    weight = suppressWarnings(as.integer(freq)),
    weight = dplyr::na_if(weight, -999)  # placeholder ako ikad koristiš
  ) %>%
  mutate(
    weight = ifelse(freq == "ne znam", NA_integer_, weight),
    weight_problem = is.na(weight) & freq != "ne znam"
  ) %>%
  select(from = ego, to = alter, weight)

ng_clean
## # A tibble: 5 × 3
##   from  to    weight
##   <chr> <chr>  <int>
## 1 ana   iva        5
## 2 ana   marko      3
## 3 ana   luka      NA
## 4 marko ana        2
## 5 marko iva        0

Roster (popis) – kada i zašto?

Roster daje ispitaniku listu svih mogućih čvorova (npr. svi studenti u grupi) i pita za veze prema svakom.

  • Prednosti: manje tipfelera, lakša validacija, bolja pokrivenost.
  • Rizici: dulje ispunjavanje, narušena privatnost, potreba za točnim popisom.

Primjer roster pitanja (binary):

  • “Označite jeste li s ovim kolegama surađivali na zadacima (da/ne).”

Primjer roster pitanja (weighted):

  • “Ocijenite koliko često komunicirate sa svakim od ovih kolega (0–5).”

Prikupljanje podataka putem interneta

Internet je danas jedan od najvažnijih izvora mrežnih podataka. Za razliku od anketa (NG/roster), gdje mi dizajniramo instrument, kod internetskih podataka često koristimo postojeće digitalne tragove.

Što su “digitalni tragovi”?

Digitalni tragovi (digital traces) su podaci nastali kao nusprodukt digitalnih aktivnosti:

  • klikovi i hyperlinkovi (web mreže),

  • e-mail komunikacija,

  • objave i spominjanja na društvenim mrežama,

  • citiranja znanstvenih radova,

  • transakcije (npr. kripto mreže),

  • interakcije u LMS sustavima (logovi - Moodle, Teams).

Takvi podaci često već imaju mrežnu strukturu: korisnik → korisnik, stranica → stranica, rad → rad, država → država.

Prema ovom popisu moglo bi se činiti kao da su mreže svuda oko nas i do takvih je podataka izrazito lako doći. Da, i ne. Naime, postoji puno već pripremljenih podataka za potrebe nastave, ali ponekad tražimo odgovore na neka pitanja ili ispitujemo mreže koje nisu prethodno dokumentirane. Zamislimo sljedeći scenarij.

Želimo analizirati mrežu osoba koje sjede u upravama/nadzornim odborima hrvatskih poduzeća (npr. “tko s kim dijeli funkcije”), ali izvori poput Infobiz/Fina, Poslovni-registar ili Fininfo imaju uvjete korištenja i/ili tehničke mjere koje znače: scrapeing nije dopušten.

Alternativni pristupi bez scrape-anja

  1. Koristi službene / otvorene izvore i licence koje to dopuštaju

Najbolji pristup je naći izvor koji eksplicitno dopušta:

preuzimanje u strojno čitljivom formatu (CSV/JSON), API pristup ili bulk download uz licencu.

Ako takav izvor postoji, to je “zlatni standard” jer je:

  • reproducibilan,

  • pravno jasniji,

  • manje podložan promjenama.

  1. Tražiti dozvolu ili komercijalni pristup (pravno najčišće)

Ako je izvor “zatvoren”, postoji korektan put: kontaktirati vlasnika baze, objasniti svrhu (nastava/istraživanje), tražiti dopuštenje ili pristup (npr. ograničen skup, testni pristup, API key, export).

  1. Ručno prikupljanje podataka (može dobro funkcionirati za male mreže)
  • kontroliramo etičke aspekte,

  • dobijemo “neuredne” podatke (normalizacija imena, duplikati, pogreške).

No, nekoliko koraka moramo proći:

Korak A: Definirati granice mreže

Primjeri granica: top 30 poduzeća po prihodu (ili po djelatnosti), poduzeća iz jedne županije, samo d.d. ili samo d.o.o., samo javna poduzeća… I objasniti zašto baš ta granica.

Korak B: Definirati što je čvor i što je veza

Najprirodnije je krenuti s dvomodalnom mrežom:

  • Čvorovi tipa 1: osobe

  • Čvorovi tipa 2: poduzeća

  • Veza: osoba ima funkciju u poduzeću

To je person–company bipartite edge list. Kasnije se radi projekcija u person–person mrežu: osoba A povezana s osobom B ako su u upravi istog poduzeća (co-board)

Korak C: Dizajnirati tablicu za prikupljanje (data entry template)

Minimalni stupci:

  • company_id (interni)
  • company_name
  • industry (opcionalno)
  • person_id (interni)
  • person_name_raw (kako piše u izvoru)
  • person_name_clean (standardizirano)
  • role (npr. uprava/nadzorni odbor)
  • role_start (opcionalno)
  • source_note (npr. “javno objavljeno”, bez linkanja na zabranjeni izvor u skripti)

Korak D: Definirati pravila čišćenja

  • trimming + lowercase (za matching)
  • uklanjanje višestrukih razmaka
  • konzistentno pisanje dijakritika (ili kompromisno: čuva se original + jedna “clean” varijanta)
  • “mapa identiteta” za spajanje duplikata (Ana Kovač vs A. Kovač)

Korak E: Kontrola kvalitete ručnog unosa

  • dvostruki unos: 10% zapisa unose dvije osobe → usporedba (inter-rater check)
  • pravilo: svaki zapis mora imati person_id i company_id
  • provjera: isti person_id + company_id ne smije se ponavljati

Prednosti i nedostaci prikupljanja mrežnih podataka s Interneta

“Ručno” prikupljanje podataka

Prednosti

  • etički i pravno najkontroliranije (minimizacija, selekcija, nema automatiziranog masovnog prikupljanja)

  • odličan materijal za učenje i istraživanje: čišćenje, standardizacija, identiteti, duplikati

  • transparentno: možete opisati proces kao dio metodologije

Nedostaci

  • sporo i podložno ljudskim greškama

  • ograničen obuhvat (N poduzeća, N osoba)

  • teško održivo za “big data” analizu

Dopušteni otvoreni izvori / API

Prednosti

  • reproducibilnost, skalabilnost

  • jasniji pravni okvir (ako je licenca eksplicitna)

Nedostaci

  • možda ne sadrži baš što trebate (npr. nema uloge, nema datuma), pa je potrebna dodatna ručna pretraga atributa

  • promjene API-ja/formatiranja

Traženje dozvole / komercijalni pristup

Prednosti

  • “najčišće” pravno, često dobijete kvalitetan export

Nedostaci

  • vrijeme i trošak

  • ograničenja korištenja (ne smijete dijeliti dataset javno)

Etička pitanja

Javno dostupno ≠ etički bez rizika

Ime osobe + funkcija + poduzeće su često osobni podaci; mreža može otkriti osjetljive obrasce (npr. “centralne” osobe u mreži poduzeća).

Minimizacija

Za mrežu često ne trebaju OIB, adresa, datum rođenja, kontakt. Dovoljno je interno person_id + uloga.

Pseudonimizacija i “linkability”

Čak i ako hashiramo imena, u maloj populaciji je moguće pogoditi identitet. Zato je bitna kontrola pristupa i objašnjenje ograničenja.

Svrha i proporcionalnost

Ako je cilj nastava (projekt), dataset treba biti minimalan i siguran. Ako je cilj istraživanje, treba jasna metodologija, pravna osnova i plan pohrane.

Dokumentiranje izvora i procesa

Ne “scrape”-amo zabranjene izvore, ali možemo dokumentirati da je ručno preuzeto iz javno vidljivih informacija i u kojem opsegu.

Glavne vrste internetskih izvora

1) Strukturni web podaci (HTML tablice)

Podaci su javno prikazani u tablicama (npr. Wikipedia, službene stranice).

  • Prednost: lako dohvatiti.

  • Rizik: promjene strukture stranice, različiti formati, fusnote, jedinice.

  • Primjer: popisi trgovinskih partnera zemalja.

2) Web scraping (ekstrakcija linkova/elemenata)

Umjesto tablica, dohvaćamo:

  • sve linkove sa stranice,

  • sve spominjane entitete,

  • strukturu navigacije.

Time dobivamo hyperlink mrežu ili mrežu referenci.

3) API pristup (npr. Eurostat, World Bank)

Neke institucije nude službene API-je i R pakete.

  • Prednost: strukturirani podaci, stabilni formati.

  • Rizik: promjene verzije API-ja, ograničenja poziva.

  • Primjer: eurostat::get_eurostat().

4) Otvoreni repozitoriji mreža

Npr. SNAP, KONECT, Network Repository.

Već strukturirani grafovi.

Često veliki (treba filtrirati).

Metapodaci nisu uvijek potpuni.

Metodološke implikacije

Prikupljanje putem interneta razlikuje se od anketnog dizajna:

Anketa Internet podaci
Mi definiramo vezu Veza je definirana platformom
Znamo populaciju Populacija je često implicitna
Missing je vidljiv Missing je skriven (npr. privatne poruke)
Granice mreže su jasne Granice su tehnološki i pravno uvjetovane

Važno pitanje: Je li digitalna interakcija dobar proxy za stvarni društveni odnos?

Primjer:

  • “Like” ≠ suradnja.

  • Hyperlink ≠ stvarna povezanost organizacija.

  • Trgovinski tok ≠ politička bliskost.

Kod internetskih podataka mi ne dizajniramo instrument, nego tumačimo već postojeću digitalnu strukturu. To znači da moramo biti svjesni:

  • tko je definirao vezu (platforma? algoritam?),

  • što je izvan vidljivog dijela (što znamo, što ne znamo i što uopće smijemo zaključivati),

  • kako tehničke odluke oblikuju mrežu.

Treba paziti na interpretaciju - tumačimo samo ono o čemu smo prikupili podatke; generalizacija na druge vrste odnosa nije ispravna.

Tehnički izazovi

Kod internetskih podataka često se susrećemo s:

  • različitim jedinicama i valutama (npr. GBP vs EUR),

  • promjenama strukture HTML-a,

  • duplikatima i bot-aktivnostima,

  • velikim volumenom (milijuni veza),

  • vremenskom dinamikom (timestampovi).

Zbog toga je faza čišćenja i validacije još važnija nego kod anketnih podataka.

Etika i pravni okvir

Prikupljanje podataka putem interneta ne znači da su podaci “slobodni za sve”.

Ključna pitanja:

  • Jesu li podaci javno dostupni?

  • Postoji li ograničenje u uvjetima korištenja?

  • Sadrže li osobne podatke?

  • Može li kombinacija podataka dovesti do re-identifikacije pojedinaca?

Kod scraping-a treba:

  • poštivati robots.txt,

  • ne preopterećivati server (rate limiting),

  • ne prikupljati osjetljive osobne podatke,

  • jasno dokumentirati izvor i datum dohvaćanja.

Reproducibilnost internetskih podataka

Za razliku od CSV datoteke, web stranice se mijenjaju.

Zato je dobra praksa:

  • spremiti preuzetu datoteku lokalno (data_raw/),

  • zabilježiti datum dohvaćanja,

  • dokumentirati URL i verziju (ako postoji),

  • po mogućnosti koristiti API ili repozitorij umjesto scrape-a.

Formati podataka: edge list i adjacency

Edge list (najčešći format)

Edge list je tablica s barem dva stupca:

  • from = izvor
  • to = odredište

Dodatno:

  • weight (jačina), time (datum), type (kanal), itd.

Primjer:

from to weight
Ana Marko 3
Ana Iva 1

Adjacency matrix (matrica susjedstva)

Kvadratna matrica dimenzije N x N gdje je A[i,j] veza od i prema j.

  • binary: 0/1
Ana Marko Iva
Ana 0 1 1
Marko 1 0 0
Iva 1 0 0
  • weighted: npr. 0–5
Ana Marko Iva
Ana 0 3 1
Marko 3 0 0
Iva 1 0 0
  • kod neusmjerene mreže matrica je simetrična.

R formati i objekti koje koristimo u SNA

U ovom kolegiju najčešće koristimo:

  • data.frame

    • osnovni tablični objekt u R-u
    • automatski pretvara stringove u faktore (ovisno o verziji R-a)
    • manje “stroga” struktura
df <- data.frame(
  from = c("Ana","Ana","Marko"),
  to   = c("Iva","Marko","Iva"),
  weight = c(3,1,2)
)
class(df)
  • tibble

    • modernija verzija tablice
    • ne pretvara stringove u faktore
    • ljepši ispis
    • bolje ponašanje kod velikih podataka
library(tibble)
tb <- tibble(
  from = c("Ana","Ana","Marko"),
  to   = c("Iva","Marko","Iva"),
  weight = c(3,1,2)
)
class(tb)
  • matrix

    • matrica je dvodimenzionalni numerički objekt
    • brze matematičke operacije
    • dobra za linearnu algebru
    • nema “stupce s imenima” kao edge list
    • manje fleksibilna za čišćenje
    • Kod velikih mreža adjacency matrica je većinom puna nula. Zato koristimo “rijetke” matrice.
library(Matrix)
A_sparse <- Matrix(adj, sparse = TRUE)
class(A_sparse)
A_sparse
  • igraph objekt

    • igraph je specijalizirani objekt za grafove
    • To više nije tablica, nego graf s: čvorovima (V(g)), bridovima (E(g)) i atributima
library(igraph)
g <- graph_from_data_frame(tb, directed = TRUE)
class(g)
  • tbl_graph (tidygraph)

    • tbl_graph je “tidy” verzija grafa
    • omogućuje korištenje dplyr sintakse (activate(nodes), mutate(), itd.)

Prvi koraci u R-u

U radu s podacima često se koristi pojam ETL – kratica za:

  • Extract – dohvatiti podatke

  • Transform – očistiti i prilagoditi podatke

  • Load – pripremiti ih za analizu ili pohranu

U mrežnoj analizi ETL je gotovo uvijek najduži i najvažniji dio procesa.

Extract (prikupljanje)

Podaci mogu dolaziti iz:

  • anketa (name generator, roster),

  • CSV/Excel datoteka,

  • web stranica (HTML tablice, scraping),

  • baza podataka (SQLite, PostgreSQL),

  • otvorenih repozitorija (SNAP, KONECT),

  • API-ja (npr. Eurostat).

U ovoj fazi cilj je dobiti sirove podatke, bez obzira koliko su “neuredni”.

Transform (čišćenje i prilagodba)

Ovo je najkritičniji dio. U mrežnim podacima transformacija često uključuje:

  • standardizaciju ID-eva (lowercase, trim),

  • pretvorbu tipova podataka (tekst → broj, Unix time → datum),

  • uklanjanje duplikata,

  • rješavanje self-loopova,

  • agregiranje više interakcija u jednu vezu,

  • pretvorbu formata (edge list ↔︎ adjacency),

  • rješavanje missing vrijednosti,

  • usklađivanje jedinica (npr. GBP → EUR).

Bez ove faze analiza može dati potpuno pogrešne rezultate.

Load (učitavanje u analitički alat)

U mrežnoj analizi to znači:

  • pretvoriti tablicu u igraph ili tidygraph objekt,

  • pripremiti dataset za modeliranje,

  • spremiti očišćene podatke u reproducibilnom formatu.

Zašto je ETL posebno važan u mrežnoj analizi?

Za razliku od klasične tablice, u mreži:

  • jedna greška u ID-u stvara lažni čvor,

  • jedna izostavljena veza mijenja stupanj i centralnost,

  • boundary problem može promijeniti strukturu cijele mreže,

  • pogrešna transformacija može promijeniti gustoću ili zajednice.

U praksi često vrijedi: 70–80% vremena rada na projektu odlazi na ETL, a tek ostatak na analizu i interpretaciju.

Važno je razumjeti da ETL nije strogo linearan proces:

Često tek nakon vizualizacije uočimo problem i vraćamo se korak unazad. Zbog toga nije loše koristiti vizualizaciju kao oblik provjere u procesu čišćenja. No, pritom se uistinu trebamo disciplinirati - nismo gotovi s postupkom, ne trošimo vrijeme na uljepšavanje grafičkog prikaza, nego nastojimo dobiti dovoljno pregledan prikaz za identifikaciju eventualnih problema.


Učitavanje podataka

“Ručno” kreiranje mreže

edges_base <- data.frame(
  from = c("Ana", "Ana", "Marko", "Iva"),
  to   = c("Marko", "Iva", "Iva", "Ana"),
  weight = c(3, 1, 2, 1),
  stringsAsFactors = FALSE
)

edges_base
##    from    to weight
## 1   Ana Marko      3
## 2   Ana   Iva      1
## 3 Marko   Iva      2
## 4   Iva   Ana      1

Kreiranje grafa i brzi prikaz:

g <- graph_from_data_frame(edges_base, directed = TRUE)
g
## IGRAPH 37008ee DNW- 3 4 -- 
## + attr: name (v/c), weight (e/n)
## + edges from 37008ee (vertex names):
## [1] Ana  ->Marko Ana  ->Iva   Marko->Iva   Iva  ->Ana
plot(g)

CSV (najčešće u praksi)

Pretpostavka: imate datoteku edges.csv sa stupcima from, to, weight.

edges_csv <- read_csv("data_edges.csv")

Minimalna provjera:

glimpse(edges_csv)

Excel (.xlsx)

library(readxl)
edges_xlsx <- read_excel("data/edges.xlsx", sheet = 1)

Savjet: odmah “očistiti” nazive stupaca:

edges_xlsx <- edges_xlsx %>% clean_names()

clean_names() dolazi iz paketa janitor i služi za standardizaciju naziva stupaca u konzistentan, “programerski siguran” format. Najčešće pretvara nazive u snake_case (mala slova + donje povlake). Npr., razmaci u nazivima stupaca stvaraju probleme u R-u: df$'Ime i prezime' , kao i specijalni znakovi i dijakritici: “Čvor – ime ispitanika”, “Broj poruka (%)”. To može stvarati probleme kod: join operacija, eksportiranja u druge sustave i reproducibilnosti.

Ako studenti međusobno šalju CSV datoteke, jedna može imati “From”, druga “FROM”, treća “from”, četvrta “Izvor”, a peta “Čvor1”.

Što clean_names() točno radi?

Ako imamo:

df <- data.frame(
  "Ime i Prezime" = 1,
  "Godina studija" = 2,
  "Broj poruka (zadnja 2 tjedna)" = 3,
  check.names = FALSE
)

colnames(df)
## [1] "Ime i Prezime"                 "Godina studija"               
## [3] "Broj poruka (zadnja 2 tjedna)"

Primjena:

library(janitor)
df2 <- clean_names(df)
colnames(df2)
## [1] "ime_i_prezime"               "godina_studija"             
## [3] "broj_poruka_zadnja_2_tjedna"

Transformacije koje radi:

  • pretvara u mala slova
  • uklanja dijakritiku (č → c)
  • uklanja specijalne znakove
  • zamjenjuje razmake podvlakom
  • uklanja višestruke podvlake
  • uklanja početne/brojčane nedopuštene znakove
  • osigurava da su nazivi jedinstveni

Preporučena praksa:

edges <- read_csv("data_edges.csv") %>%
  clean_names()

Učitavanje podataka s weba (tablice) i scraping

Najjednostavnije: HTML tablica na stranici

Ako web stranica već ima tablicu (npr. popis odnosa / linkova):

url <- "https://example.com/page-with-table"
page <- read_html(url)

tables <- html_table(page, fill = TRUE)
length(tables)

edges_web <- tables[[1]] %>% clean_names()
glimpse(edges_web)

Primjer:

library(rvest)
library(dplyr)
library(purrr)
library(janitor)

url <- "https://en.wikipedia.org/wiki/Counties_of_Croatia"
page <- read_html(url)

# Učitamo sve HTML tablice sa stranice
tables <- html_table(page, fill = TRUE)

length(tables)  # na Wikipediji ih često ima više
## [1] 14

Sad želimo odabrati tablicu koja stvarno sadrži stupce poput “County” i “Seat”. Najpraktičnije je pregledati nazive stupaca svake tablice:

tab_info <- map_df(seq_along(tables), function(i) {
  tibble(
    i = i,
    nrow = nrow(tables[[i]]),
    ncol = ncol(tables[[i]]),
    colnames = paste(names(tables[[i]]), collapse = " | ")
  )
})

tab_info
## # A tibble: 14 × 4
##        i  nrow  ncol colnames                                                   
##    <int> <int> <int> <chr>                                                      
##  1     1     9     2 "Counties of Croatia | Counties of Croatia"                
##  2     2    12     1 "Politics of Croatia"                                      
##  3     3     8     6 "County | Seat | Population (1773)[39] | Population (1785–…
##  4     4     8     4 "County | Seat | Area | Sub-counties"                      
##  5     5     8     6 "County | Seat | Area(1886–1912)[44] | Population (1910)[4…
##  6     6    21     9 "County | Seat | Regions | Statistical Regions | Area (200…
##  7     7    33     2 "County | Period of existence"                             
##  8     8     1     2 "vteCounties of Croatia | vteCounties of Croatia"          
##  9     9    12     8 "vteCroatia articles | vteCroatia articles |  |  |  |  |  …
## 10    10     1     2 "X1 | X2"                                                  
## 11    11     1     2 "X1 | X2"                                                  
## 12    12     1     2 "X1 | X2"                                                  
## 13    13     3     2 "X1 | X2"                                                  
## 14    14     3     2 "vteFirst-level administrative divisions in European count…

Odabir tablice koja ima stupce povezane s “County” i “Seat”:

idx <- which(
  map_lgl(tables, ~ any(str_detect(names(.x), regex("County", ignore_case = TRUE)))) &
  map_lgl(tables, ~ any(str_detect(names(.x), regex("Seat", ignore_case = TRUE))))
)

idx  # često ispadne jedna vrijednost (npr. 1 ili 2), ovisi o verziji stranice
## [1] 3 4 5 6

Ovdje imamo više vrijednosti, pa možemo birati.

counties_raw <- tables[[idx[4]]]

# prije čišćenja
names(counties_raw)
## [1] "County"                    "Seat"                     
## [3] "Regions"                   "Statistical Regions"      
## [5] "Area (2006)[52]"           "Population (2021)[53]"    
## [7] "GDP per capita (2019)[54]" "Arms"                     
## [9] "Geographic coordinates"
counties <- counties_raw %>%
  clean_names()

# nakon čišćenja
names(counties)
## [1] "county"                 "seat"                   "regions"               
## [4] "statistical_regions"    "area_2006_52"           "population_2021_53"    
## [7] "gdp_per_capita_2019_54" "arms"                   "geographic_coordinates"
glimpse(counties)
## Rows: 21
## Columns: 9
## $ county                 <chr> "Bjelovar-Bilogora", "Brod-Posavina", "Dubrovni…
## $ seat                   <chr> "Bjelovar", "Slavonski Brod", "Dubrovnik", "Paz…
## $ regions                <chr> "Central Croatia", "Slavonia", "Dalmatia", "Ist…
## $ statistical_regions    <chr> "Pannonian Croatia", "Pannonian Croatia", "Adri…
## $ area_2006_52           <chr> "2,640 km2 (1,020 sq mi)", "2,030 km2 (780 sq m…
## $ population_2021_53     <chr> "101,879", "130,267", "115,564", "195,237", "11…
## $ gdp_per_capita_2019_54 <chr> "07986€9,132", "06607€8,211", "13277€14,673", "…
## $ arms                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ geographic_coordinates <chr> "45°54′10″N 16°50′51″E / 45.90278°N 16.84750°…

Vidimo sa je clean_names() odradio posao. No, ipak postoje problemi.

Problem 1: reference brojevi u nazivima stupaca

counties <- counties %>%
  rename_with(~ str_remove(.x, "_\\d+$"))

names(counties)
## [1] "county"                 "seat"                   "regions"               
## [4] "statistical_regions"    "area_2006"              "population_2021"       
## [7] "gdp_per_capita_2019"    "arms"                   "geographic_coordinates"

Problem 2: numeričke varijable su

counties <- counties %>%
  mutate(
    population_2021 = parse_number(population_2021)
  )

Pretvorba u broj. Zašto parse_number() a ne as.numeric()?

  • automatski uklanja zareze
  • ignorira tekst
  • robusniji je
counties
## # A tibble: 21 × 9
##    county            seat  regions statistical_regions area_2006 population_2021
##    <chr>             <chr> <chr>   <chr>               <chr>               <dbl>
##  1 Bjelovar-Bilogora Bjel… Centra… Pannonian Croatia   2,640 km…          101879
##  2 Brod-Posavina     Slav… Slavon… Pannonian Croatia   2,030 km…          130267
##  3 Dubrovnik-Neretva Dubr… Dalmat… Adriatic Croatia    1,781 km…          115564
##  4 Istria            Pazin Istria  Adriatic Croatia    2,813 km…          195237
##  5 Karlovac          Karl… Centra… Pannonian Croatia   3,626 km…          112195
##  6 Koprivnica-Križe… Kopr… Centra… Northern Croatia    1,748 km…          101221
##  7 Krapina-Zagorje   Krap… Centra… Northern Croatia    1,229 km…          120702
##  8 Lika-Senj         Gosp… Centra… Adriatic Croatia    5,353 km…           42748
##  9 Međimurje         CČak… Centra… Northern Croatia    0,730729…          105250
## 10 Osijek-Baranja    Osij… Slavon… Pannonian Croatia   4,155 km…          258026
## # ℹ 11 more rows
## # ℹ 3 more variables: gdp_per_capita_2019 <chr>, arms <lgl>,
## #   geographic_coordinates <chr>

Area (uklanjanje jedinica) - trenutno: “2,640 km2 (1,020 sq mi)”; Želimo samo km² dio.

counties <- counties %>%
  mutate(
    area_2006 = str_extract(area_2006, "^[0-9,]+") %>%
      parse_number()
  )
counties$area_2006
##  [1]   2640   2030   1781   2813   3626   1748   1229   5353 730729   4155
## [11]   1823   3588   4468   4540   2984   1262   2024   2454   3646   3060
## [21] 641641

GDP per capita (najzanimljiviji problem) - vidimo npr.: “07986€9,132”. To je spoj ranga, simbola € i vrijednosti. Rješenje je izvući zadnji broj (stvarni BDP):

counties <- counties %>%
  mutate(
    gdp_per_capita_2019 = str_extract(gdp_per_capita_2019, "[0-9,]+$") %>%
      parse_number()
  )
glimpse(counties)
## Rows: 21
## Columns: 9
## $ county                 <chr> "Bjelovar-Bilogora", "Brod-Posavina", "Dubrovni…
## $ seat                   <chr> "Bjelovar", "Slavonski Brod", "Dubrovnik", "Paz…
## $ regions                <chr> "Central Croatia", "Slavonia", "Dalmatia", "Ist…
## $ statistical_regions    <chr> "Pannonian Croatia", "Pannonian Croatia", "Adri…
## $ area_2006              <dbl> 2640, 2030, 1781, 2813, 3626, 1748, 1229, 5353,…
## $ population_2021        <dbl> 101879, 130267, 115564, 195237, 112195, 101221,…
## $ gdp_per_capita_2019    <dbl> 9132, 8211, 14673, 15960, 9510, 10110, 8954, 10…
## $ arms                   <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ geographic_coordinates <chr> "45°54′10″N 16°50′51″E / 45.90278°N 16.84750°…
colSums(is.na(counties))
##                 county                   seat                regions 
##                      0                      0                      0 
##    statistical_regions              area_2006        population_2021 
##                      0                      0                      0 
##    gdp_per_capita_2019                   arms geographic_coordinates 
##                      0                     21                      0
counties <- counties[, -8]
counties
## # A tibble: 21 × 8
##    county            seat  regions statistical_regions area_2006 population_2021
##    <chr>             <chr> <chr>   <chr>                   <dbl>           <dbl>
##  1 Bjelovar-Bilogora Bjel… Centra… Pannonian Croatia        2640          101879
##  2 Brod-Posavina     Slav… Slavon… Pannonian Croatia        2030          130267
##  3 Dubrovnik-Neretva Dubr… Dalmat… Adriatic Croatia         1781          115564
##  4 Istria            Pazin Istria  Adriatic Croatia         2813          195237
##  5 Karlovac          Karl… Centra… Pannonian Croatia        3626          112195
##  6 Koprivnica-Križe… Kopr… Centra… Northern Croatia         1748          101221
##  7 Krapina-Zagorje   Krap… Centra… Northern Croatia         1229          120702
##  8 Lika-Senj         Gosp… Centra… Adriatic Croatia         5353           42748
##  9 Međimurje         CČak… Centra… Northern Croatia       730729          105250
## 10 Osijek-Baranja    Osij… Slavon… Pannonian Croatia        4155          258026
## # ℹ 11 more rows
## # ℹ 2 more variables: gdp_per_capita_2019 <dbl>, geographic_coordinates <chr>
unique(counties$regions)
## [1] "Central Croatia"                             
## [2] "Slavonia"                                    
## [3] "Dalmatia"                                    
## [4] "Istria"                                      
## [5] "Central Croatia (ThroughGračac) and Dalmatia"
counties$regions
##  [1] "Central Croatia"                             
##  [2] "Slavonia"                                    
##  [3] "Dalmatia"                                    
##  [4] "Istria"                                      
##  [5] "Central Croatia"                             
##  [6] "Central Croatia"                             
##  [7] "Central Croatia"                             
##  [8] "Central Croatia"                             
##  [9] "Central Croatia"                             
## [10] "Slavonia"                                    
## [11] "Slavonia"                                    
## [12] "Central Croatia"                             
## [13] "Central Croatia"                             
## [14] "Dalmatia"                                    
## [15] "Dalmatia"                                    
## [16] "Central Croatia"                             
## [17] "Slavonia"                                    
## [18] "Slavonia"                                    
## [19] "Central Croatia (ThroughGračac) and Dalmatia"
## [20] "Central Croatia"                             
## [21] "Central Croatia"
counties$regions[19]<- "Central Croatia" 
counties$regions
##  [1] "Central Croatia" "Slavonia"        "Dalmatia"        "Istria"         
##  [5] "Central Croatia" "Central Croatia" "Central Croatia" "Central Croatia"
##  [9] "Central Croatia" "Slavonia"        "Slavonia"        "Central Croatia"
## [13] "Central Croatia" "Dalmatia"        "Dalmatia"        "Central Croatia"
## [17] "Slavonia"        "Slavonia"        "Central Croatia" "Central Croatia"
## [21] "Central Croatia"
unique(counties$statistical_regions)
## [1] "Pannonian Croatia"                 "Adriatic Croatia"                 
## [3] "Northern Croatia"                  "Zagreb, the city ofCity of Zagreb"

I sad možemo ilustrirati kreiranje grada.

edges_h <- counties %>%
  transmute(from = statistical_regions, to = county)

# Node tablica + atributi (GDP samo za county; region dobije NA)
nodes_h <- tibble(name = unique(c(edges_h$from, edges_h$to))) %>%
  mutate(
    level = if_else(name %in% edges_h$from, "region", "county")
  ) %>%
  left_join(
    counties %>% select(county, gdp_per_capita_2019),
    by = c("name" = "county")
  ) %>%
  mutate(
    # veličina: regije su fiksne, veličinu county čvora skaliramo po GDP
    size = case_when(
      level == "region" ~ 6,
      is.na(gdp_per_capita_2019) ~ 6,
      TRUE ~ gdp_per_capita_2019
      )
  )

g_h <- tbl_graph(nodes = nodes_h, edges = edges_h, directed = TRUE)

# Najjednostavniji hijerarhijski prikaz (tree layout)
ggraph(g_h, layout = "tree") +
  geom_edge_link() +
  geom_node_point(aes(size = size)) +
  geom_node_text(aes(label = name), nudge_x = 2, nudge_y = c(-0.14, -0.11, -0.08, -0.05, -0.02), size = 3) + # pozicioniranje naziva čvora
  scale_size_continuous(range = c(2, 10)) +
  theme_graph()

Probajte kreirati graf u kojem veličina populacije određuje veličinu čvora.

Učitavanje podataka iz baze (DB)

Ovdje kreiramo lokalnu SQLite bazu (jer radi svima).

Kreiranje baze i tablice (demo)

con <- dbConnect(RSQLite::SQLite(), "data/network.sqlite")

dbExecute(con, "
CREATE TABLE IF NOT EXISTS edges (
  from_id TEXT,
  to_id   TEXT,
  weight  REAL
);
")
## [1] 0
dbExecute(con, "
INSERT INTO edges (from_id, to_id, weight) VALUES
('Ana','Marko',3),
('Ana','Iva',1),
('Marko','Iva',2)
;
")
## [1] 3

Čitanje iz baze u R

edges_db <- dbGetQuery(con, "SELECT * FROM edges;")
edges_db
##    from_id to_id weight
## 1      Ana Marko      3
## 2      Ana   Iva      1
## 3    Marko   Iva      2
## 4      Ana Marko      3
## 5      Ana   Iva      1
## 6    Marko   Iva      2
## 7      Ana Marko      3
## 8      Ana   Iva      1
## 9    Marko   Iva      2
## 10     Ana Marko      3
## 11     Ana   Iva      1
## 12   Marko   Iva      2
## 13     Ana Marko      3
## 14     Ana   Iva      1
## 15   Marko   Iva      2
## 16     Ana Marko      3
## 17     Ana   Iva      1
## 18   Marko   Iva      2
## 19     Ana Marko      3
## 20     Ana   Iva      1
## 21   Marko   Iva      2
## 22     Ana Marko      3
## 23     Ana   Iva      1
## 24   Marko   Iva      2
## 25     Ana Marko      3
## 26     Ana   Iva      1
## 27   Marko   Iva      2
## 28     Ana Marko      3
## 29     Ana   Iva      1
## 30   Marko   Iva      2
## 31     Ana Marko      3
## 32     Ana   Iva      1
## 33   Marko   Iva      2
## 34     Ana Marko      3
## 35     Ana   Iva      1
## 36   Marko   Iva      2
## 37     Ana Marko      3
## 38     Ana   Iva      1
## 39   Marko   Iva      2
## 40     Ana Marko      3
## 41     Ana   Iva      1
## 42   Marko   Iva      2
## 43     Ana Marko      3
## 44     Ana   Iva      1
## 45   Marko   Iva      2
## 46     Ana Marko      3
## 47     Ana   Iva      1
## 48   Marko   Iva      2
## 49     Ana Marko      3
## 50     Ana   Iva      1
## 51   Marko   Iva      2
## 52     Ana Marko      3
## 53     Ana   Iva      1
## 54   Marko   Iva      2
## 55     Ana Marko      3
## 56     Ana   Iva      1
## 57   Marko   Iva      2
## 58     Ana Marko      3
## 59     Ana   Iva      1
## 60   Marko   Iva      2
## 61     Ana Marko      3
## 62     Ana   Iva      1
## 63   Marko   Iva      2
## 64     Ana Marko      3
## 65     Ana   Iva      1
## 66   Marko   Iva      2
## 67     Ana Marko      3
## 68     Ana   Iva      1
## 69   Marko   Iva      2
## 70     Ana Marko      3
## 71     Ana   Iva      1
## 72   Marko   Iva      2
## 73     Ana Marko      3
## 74     Ana   Iva      1
## 75   Marko   Iva      2
## 76     Ana Marko      3
## 77     Ana   Iva      1
## 78   Marko   Iva      2
## 79     Ana Marko      3
## 80     Ana   Iva      1
## 81   Marko   Iva      2
dbDisconnect(con)

Otvorene baze podataka s mrežnim podacima

PostgreSQL sample database – Pagila je open-source demo baza (filmska mreža, glumci, filmovi, suradnje).

Sadrži:

  • artists
  • albums
  • tracks
  • customers
  • invoices

Možemo napraviti mrežu:

  • artist → artist (ako su na istom albumu)
  • customer → customer (ako kupuju iste izvođače)
library(DBI)
library(RSQLite)

download.file(
  "https://github.com/lerocha/chinook-database/raw/master/ChinookDatabase/DataSources/Chinook_Sqlite.sqlite",
  destfile = "data/chinook.sqlite",
  mode = "wb"
)

con <- dbConnect(RSQLite::SQLite(), "data/chinook.sqlite")

dbListTables(con)
##  [1] "Album"         "Artist"        "Customer"      "Employee"     
##  [5] "Genre"         "Invoice"       "InvoiceLine"   "MediaType"    
##  [9] "Playlist"      "PlaylistTrack" "Track"

Kreirajmo mrežu suradnje izvođača:

edges_db <- dbGetQuery(con, "
SELECT DISTINCT a1.Name AS from_id,
                a2.Name AS to_id
FROM Artist a1
JOIN Album al1 ON a1.ArtistId = al1.ArtistId
JOIN Track t1 ON al1.AlbumId = t1.AlbumId
JOIN Genre g1 ON t1.GenreId = g1.GenreId
JOIN Track t2 ON g1.GenreId = t2.GenreId
JOIN Album al2 ON t2.AlbumId = al2.AlbumId
JOIN Artist a2 ON al2.ArtistId = a2.ArtistId
WHERE a1.ArtistId < a2.ArtistId
")

glimpse(edges_db)
## Rows: 4,087
## Columns: 2
## $ from_id <chr> "AC/DC", "AC/DC", "AC/DC", "AC/DC", "AC/DC", "AC/DC", "AC/DC",…
## $ to_id   <chr> "Accept", "Aerosmith", "Alanis Morissette", "Alice In Chains",…
g3 <- graph_from_data_frame(edges_db, directed = FALSE)

plot(g3,
     vertex.size = 4,
     vertex.label = NA,
     edge.arrow.size = 0.2)

Preuzimanje skupova podataka iz repozitorija

Osnovni obrazac (vrijedi za sve)

dir.create("data", showWarnings = FALSE)

url <- "PASTE_URL_HERE"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

CSV (nekomprimiran)

library(readr)

url <- "https://example.com/edges.csv"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

edges <- read_csv(f, show_col_types = FALSE)  # očekuje zaglavlje (header)
head(edges)


# Ako nema zaglavlje (header):

edges <- read_csv(f, col_names = FALSE, show_col_types = FALSE)
names(edges) <- c("from", "to")  # ili c("from","to","weight","time")

TSV (tab-separated)

library(readr)

url <- "https://example.com/edges.tsv"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

edges <- read_tsv(f, show_col_types = FALSE)
head(edges)

TXT edge list (whitespace, 2 stupca po retku)

Tipično za SNAP .txt (često s komentarima # u headeru).

library(readr)

url <- "https://example.com/network.txt"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

edges <- read_table(
  f,
  col_names = c("from", "to"),
  comment = "#",
  show_col_types = FALSE
)
head(edges)

CSV.GZ (komprimirani CSV; npr. SNAP bitcoinotc)

library(readr)

url <- "https://snap.stanford.edu/data/soc-sign-bitcoinotc.csv.gz"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

df <- read_csv(f, col_names = FALSE, show_col_types = FALSE)
names(df) <- c("source", "target", "rating", "time")
head(df)

TXT.GZ

(komprimirani txt edge list; 2 stupca, whitespace)

library(readr)

url <- "https://snap.stanford.edu/data/ca-GrQc.txt.gz"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

edges <- read_table(
  gzfile(f, open = "rt"),
  col_names = c("from", "to"),
  comment = "#",
  show_col_types = FALSE
)

head(edges)

ZIP (unutra je .csv / .tsv / .txt)

Najjednostavnije: raspakirati u temp folder, pročitati prvu pronađenu datoteku.

library(readr)
library(stringr)

url <- "https://example.com/network.zip"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

tmp <- tempfile("unz_")
dir.create(tmp)
unzip(f, exdir = tmp)

# prva datoteku koja izgleda kao edge list
inner <- list.files(tmp, recursive = TRUE, full.names = TRUE) |>
  (\(x) x[str_detect(x, "\\.(csv|tsv|txt)$")][1])()

inner

# učitati ovisno o ekstenziji
edges <- if (str_detect(inner, "\\.csv$")) {
  read_csv(inner, show_col_types = FALSE)
} else if (str_detect(inner, "\\.tsv$")) {
  read_tsv(inner, show_col_types = FALSE)
} else {
  read_table(inner, col_names = c("from","to"), comment = "#", show_col_types = FALSE)
}

head(edges)

RDS (R objekt; rijetko)

url <- "https://example.com/network.rds"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

obj <- readRDS(f)
obj

GraphML (npr. iz Gephi/ Cytoscape export)

library(igraph)

url <- "https://example.com/network.graphml"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

g <- read_graph(f, format = "graphml")
g

GML (igraph format)

library(igraph)

url <- "https://example.com/network.gml"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

g <- read_graph(f, format = "gml")
g

Pajek .net (stariji, ali čest u SNA)

library(igraph)

url <- "https://example.com/network.net"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

g <- read_graph(f, format = "pajek")
g

Matrix Market (.mtx) – često za adjacency/sparse matrice

library(Matrix)

url <- "https://example.com/network.mtx"
f <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

A <- readMM(f)  # sparse matrica
A[1:5, 1:5]

Otvoreni repozitoriji s mrežnim podacima

Primjer: SNAP Bitcoin OTC trust network

# Preuzimanje podataka
dir.create("data", showWarnings = FALSE)

url <- "https://snap.stanford.edu/data/soc-sign-bitcoinotc.csv.gz"
f   <- file.path("data", basename(url))

if (!file.exists(f)) download.file(url, f, mode = "wb")

# Učitavanje
raw <- read_csv(f, col_names = FALSE, show_col_types = FALSE) |>
  setNames(c("source", "target", "rating", "time_unix"))

glimpse(raw)
## Rows: 35,592
## Columns: 4
## $ source    <dbl> 6, 6, 1, 4, 13, 13, 7, 2, 2, 21, 21, 21, 21, 21, 17, 17, 10,…
## $ target    <dbl> 2, 5, 15, 3, 16, 10, 5, 21, 20, 2, 1, 10, 8, 3, 3, 23, 1, 6,…
## $ rating    <dbl> 4, 2, 1, 7, 8, 8, 1, 5, 5, 5, 8, 8, 9, 7, 5, 1, 8, 7, 8, 1, …
## $ time_unix <dbl> 1289241912, 1289241942, 1289243140, 1289245277, 1289254254, …

Trebamo prilagoditi tipove podataka.

df <- raw |>
  transmute(
    source = as.character(source),
    target = as.character(target),
    rating = as.integer(rating),
    time   = as.POSIXct(time_unix, origin = "1970-01-01", tz = "UTC")
  )

glimpse(df)
## Rows: 35,592
## Columns: 4
## $ source <chr> "6", "6", "1", "4", "13", "13", "7", "2", "2", "21", "21", "21"…
## $ target <chr> "2", "5", "15", "3", "16", "10", "5", "21", "20", "2", "1", "10…
## $ rating <int> 4, 2, 1, 7, 8, 8, 1, 5, 5, 5, 8, 8, 9, 7, 5, 1, 8, 7, 8, 1, 10,…
## $ time   <dttm> 2010-11-08 18:45:11, 2010-11-08 18:45:41, 2010-11-08 19:05:40,…

Brzinska provjera konzistentnosti skupa podataka

# Provjera missing vrijednosti
colSums(is.na(df))
## source target rating   time 
##      0      0      0      0
# Provjera raspona ratinga
range(df$rating, na.rm = TRUE)
## [1] -10  10
# Self-loopovi
sum(df$source == df$target)
## [1] 0
# Duplikati (isti source-target-time-rating)
sum(duplicated(df))
## [1] 0

S obzirom da je ova mreža ogromna, izdvojit ćemo samo pozitivne veze (povjerenje).

edges_pos <- df |>
  filter(rating > 0) |>
  select(from = source, to = target, weight = rating)

g_pos <- graph_from_data_frame(edges_pos, directed = TRUE)
g_pos
## IGRAPH 3e25860 DNW- 5573 32029 -- 
## + attr: name (v/c), weight (e/n)
## + edges from 3e25860 (vertex names):
##  [1] 6 ->2  6 ->5  1 ->15 4 ->3  13->16 13->10 7 ->5  2 ->21 2 ->20 21->2 
## [11] 21->1  21->10 21->8  21->3  17->3  17->23 10->1  10->6  10->21 10->8 
## [21] 10->25 10->2  10->3  4 ->26 26->4  5 ->1  5 ->6  5 ->7  1 ->5  6 ->4 
## [31] 4 ->6  2 ->4  17->28 17->13 13->17 13->29 29->13 17->20 4 ->31 31->4 
## [41] 32->6  13->1  7 ->34 34->7  32->1  1 ->32 1 ->34 34->1  34->13 13->34
## [51] 6 ->7  7 ->6  1 ->17 1 ->31 31->1  35->6  1 ->13 36->37 37->36 35->1 
## [61] 17->1  8 ->1  7 ->29 1 ->20 37->44 44->37 39->45 39->7  39->44 44->39
## [71] 23->17 23->19 36->46 46->36 47->1  13->7  7 ->13 29->51 51->29 29->52
## + ... omitted several edges

Ovo je i dalje jako velika mreža. Zbog toga ćemo zadržati 50 čvorova s najvišim stupnjem čvora.

tg_pos <- as_tbl_graph(g_pos) %>%
  activate(nodes) %>%
  mutate(indeg = centrality_degree(mode = "in"))

top_nodes <- tg_pos %>%
  activate(nodes) %>%
  as_tibble() %>%
  arrange(desc(indeg)) %>%
  slice(1:50) %>%
  pull(name)

g_small <- induced_subgraph(as.igraph(tg_pos), vids = top_nodes)

tg_small <- as_tbl_graph(g_small) |>
  activate(nodes) |>
  mutate(indeg = centrality_degree(mode = "in"))

set.seed(123)

lay <- layout_with_fr(
  tg_small,
  niter = 2000
)

lay <- lay * 3 # ovo će ravnomjerno raširiti postojeći layout

ggraph(tg_small, layout = "manual", x = lay[,1], y = lay[,2]) +
  geom_edge_link(aes(width = weight),
                 alpha = 0.6,
                 colour = "steelblue") +
  geom_node_point(aes(size = indeg),  # veličina čvora
                  colour = "darkred",
                  alpha = 0.8) +
  scale_edge_width(range = c(0.1, 1)) +
  scale_size_continuous(range = c(1, 6)) +
  theme_graph()

Primjer: trgovanje EU s Eurostata

Poznate javne baze podataka, kao što su Eurostat ili World bank, imaju čak i pridružene pakete koji olakšavaju pretragu podatkovnih skupova i njihovo dohvaćanje (eurostat, WDI). Ovdje će se prikazati jedan primjer s podacima Eurostata.

# install.packages("eurostat")

library(eurostat)
library(dplyr)

trade_raw <- get_eurostat("ext_lt_intratrd", time_format = "date")
## indexed 0B in  0s, 0B/sindexed 2.15GB in  0s, 2.15GB/s                                                                              
glimpse(trade_raw)
## Rows: 117,576
## Columns: 7
## $ freq        <chr> "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A"…
## $ indic_et    <chr> "CONT_EXP_EU", "CONT_EXP_EU", "CONT_EXP_EU", "CONT_EXP_EU"…
## $ sitc06      <chr> "SITC0_1", "SITC0_1", "SITC0_1", "SITC0_1", "SITC0_1", "SI…
## $ partner     <chr> "EU27_2020", "EU27_2020", "EU27_2020", "EU27_2020", "EU27_…
## $ geo         <chr> "AT", "AT", "AT", "AT", "AT", "AT", "AT", "AT", "AT", "AT"…
## $ TIME_PERIOD <date> 2002-01-01, 2003-01-01, 2004-01-01, 2005-01-01, 2006-01-0…
## $ values      <dbl> 2.6, 2.8, 2.9, 2.9, 3.0, 3.0, 3.1, 3.0, 2.9, 2.9, 2.9, 2.8…
trade_eu <- trade_raw |>
  select(from = geo,
         to = partner,
         weight = values)

head(trade_eu)
## # A tibble: 6 × 3
##   from  to        weight
##   <chr> <chr>      <dbl>
## 1 AT    EU27_2020    2.6
## 2 AT    EU27_2020    2.8
## 3 AT    EU27_2020    2.9
## 4 AT    EU27_2020    2.9
## 5 AT    EU27_2020    3  
## 6 AT    EU27_2020    3
trade_clean <- trade_eu |>
  filter(!is.na(weight)) |>
  filter(weight > 0) |>
  filter(from != to) 
library(igraph)

g_eu <- graph_from_data_frame(trade_clean, directed = TRUE)

g_eu <- igraph::simplify(g_eu, edge.attr.comb = "sum")

vcount(g_eu)
## [1] 30
ecount(g_eu)
## [1] 82
g_eu
## IGRAPH 43836fa DNW- 30 82 -- 
## + attr: name (v/c), weight (e/n)
## + edges from 43836fa (vertex names):
##  [1] AT->EU27_2020     AT->EXT_EU27_2020 AT->WORLD         BE->EU27_2020    
##  [5] BE->EXT_EU27_2020 BE->WORLD         BG->EU27_2020     BG->EXT_EU27_2020
##  [9] BG->WORLD         CY->EU27_2020     CY->EXT_EU27_2020 CY->WORLD        
## [13] CZ->EU27_2020     CZ->EXT_EU27_2020 CZ->WORLD         DE->EU27_2020    
## [17] DE->EXT_EU27_2020 DE->WORLD         DK->EU27_2020     DK->EXT_EU27_2020
## [21] DK->WORLD         EE->EU27_2020     EE->EXT_EU27_2020 EE->WORLD        
## [25] EL->EU27_2020     EL->EXT_EU27_2020 EL->WORLD         ES->EU27_2020    
## [29] ES->EXT_EU27_2020 ES->WORLD         FI->EU27_2020     FI->EXT_EU27_2020
## + ... omitted several edges
plot(g_eu, layout = layout_with_fr, vertex.color = "lightblue")

vertex_colors <- ifelse(
  V(g_eu)$name == "EU27_2020", "darkblue",
  ifelse(
    V(g_eu)$name == "EXT_EU27_2020", "orange",
    ifelse(
      V(g_eu)$name == "WORLD", "red",
      "lightblue"   # all other countries
    )
  )
)

plot(
  g_eu,
  layout = layout_with_fr,
  vertex.color = vertex_colors
)


Čišćenje i priprema: korak po korak

U ovom poglavlju učimo što tipično pođe po zlu i kako to sustavno popraviti.

Duplikati veza i agregiranje težina

Problem: isti par (from,to) se pojavi više puta (više anketa, više interakcija, više vremena).

Rješenje: distinct() ili agregiranje (sum/mean/max, broj interakcija).

edges %>% count(from, to) %>% filter(n > 1)
edges_agg <- edges %>% group_by(from, to) %>% summarise(weight = sum(weight), .groups="drop")

Self-loopovi (from == to)

Problem: ljudi označe (tagiraju) sebe, logovi imaju self-events, ili greška u unosu.

Rješenje: izbaciti ili eksplicitno dopustiti (ovisno o definiciji mreže).

edges %>% filter(from == to)
edges <- edges %>% filter(from != to)

Usmjerenost vs. neusmjerenost i simetrija

Problem: instrument implicira neusmjerenost, ali podaci su usmjereni (ili obrnuto).

Rješenje: ili postići simatriju (npr. max/mean) ili zadržati directed.

# za neusmjereno: normaliziramo par (a,b)
edges_u <- edges %>% mutate(a=pmin(from,to), b=pmax(from,to)) %>%
  group_by(a,b) %>% summarise(weight = max(weight), .groups="drop")

Čvorovi “izvan roster-a” i boundary problem

Problem: NG daje imena koja nisu u popisu; ili - roster nedostaje dio populacije.

Rješenje: provjera setova + pravilo što radimo s “out-of-scope”.

setdiff(unique(edges$from), roster_ids)
setdiff(unique(edges$to), roster_ids)

“Zagađenje” ID-eva: dijakritika, znakovi, razmaci, nevidljivi znakovi

Problem: “0A0” (non-breaking space), tabovi, više razmaka, crtica vs en-dash.

Rješenje: str_squish(), uklanjanje nevidljivih znakova, transliteracija.

edges <- edges %>% mutate(across(c(from,to), \(x) str_squish(str_replace_all(x, "\u00A0", " "))))

Varijante imena i fuzzy matching (Ana K., Ana Kovač, Kovač Ana)

Problem: isti čvor pod više zapisa → “lažni” čvorovi.

Rješenje: pravila standardizacije + fuzzy matching (npr. stringdist), ručna “mapa”.

(Ovo je posebno važno ako radite name generator!)

Kodiranje missing vrijednosti

Problem: missing nije samo NA, nego ““,” “,”NA”, “N/A”, “-”, “0”, “ne znam”.

Rješenje: standardizirati u NA prije svega.

edges <- edges %>% mutate(across(everything(), ~na_if(.x, "")))

Neispravne vrijednosti (out-of-range) i logičke kontradikcije

Problem: rating izvan dopuštenog raspona, negativne težine gdje ne smiju biti, datumi u budućnosti.

Rješenje: assert-stil provjere + filtriranje/označavanje.

edges %>% filter(weight < 0 | weight > 10)

Različiti tipovi ID-eva (numeric vs character) i vodeće nule

Problem: ID “0012” postane 12, join-ovi pucaju.

Rješenje: ID uvijek kao character.

edges <- edges %>% mutate(across(c(from,to), as.character))

Vremenska komponenta (timestamp, timezone, granularnost)

Problem: Unix time vs datum string; timezone; agregacije po danu/tjednu.

Rješenje: standardizacija u POSIXct + derivacije (date/week).

Multi-edge i definicija “što je veza”

Problem: više tipova odnosa (email, chat, suradnja) u jednoj tablici.

Rješenje: type stupac + filtriranje ili multilayer pristup.

Kod roster matrica: asimetrične odgovore i “ne može procijeniti”

Problem: A kaže “da”, B kaže “ne”; ili “ne znam”.

Rješenje: pravilo postizanja simetrije (OR/AND/mean) + zaseban kod za “unknown”.

Inkonzistentni nazivi stupaca i semantika (from/to, source/target)

Problem: svaki izvor podataka ima drugačije nazive.

Rješenje: odmah nakon učitavanja clean_names() + rename() na standard.

Spajanje čvorova s atributima (node table) i problemi joinova

Problem: edge list ima čvorove koji nemaju atribute ili obrnuto.

Rješenje: provjeriti anti_join, pa odlučite: dodati missing atribute ili izbaciti čvorove.

anti_join(edges, nodes, by=c("from"="id"))

Problem izoliranih čvorova (isolates)

Izolirani čvor je čvor s stupnjem 0: nema nijednu vezu (ni ulaznu ni izlaznu). U praksi se izolati pojavljuju kada:

  • je roster potpun, ali ispitanik označi da nema suradnji / interakcija u zadanom razdoblju,

  • imamo unit nonresponse (npr. osoba nije odgovorila pa se ne pojavljuje kao izvor veza, ali je u rosteru),

  • imamo boundary problem (mreža je “odrezana” pa neke veze idu izvan obuhvata i nisu uključene),

  • kod digitalnih logova: korisnik je bio prisutan, ali nije ostavio mjerljive tragove (nema eventa).

Zašto je važno? Izolirani čvorovi utječu na mjere gustoće, prosječni stupanj i centralnosti (mnogi čvorovi imaju 0). U vizualizaciji često “zaguše” graf bez da dodaju informaciju.

Što radimo s njima? Ovisi o cilju:

  • Ako analiziramo cijelu populaciju (npr. svi studenti u grupi), izolirani čvorovi su važan rezultat (pokazuju neuključenost / perifernost).

  • Ako analiziramo strukturu interakcija među aktivnim sudionicima, izolirani čvorovi se mogu izostaviti iz grafa (ali to treba jasno dokumentirati).

  1. Izolati u igraph grafu
library(igraph)

g <- graph_from_data_frame(edges_base, directed = TRUE)

deg_all <- degree(g, mode = "all")
isolates <- names(deg_all[deg_all == 0])

isolates
## character(0)
length(isolates)
## [1] 0
  1. Uklanjanje izolata (za “graf aktivnih”)
g_no_isolates <- delete_vertices(g, isolates)

vcount(g); vcount(g_no_isolates)
## [1] 3
## [1] 3
ecount(g); ecount(g_no_isolates)
## [1] 4
## [1] 4
  1. Ako radite s rosterom: izolati nastaju tek se dodaju svi čvorovi

Edge list sam po sebi često ne sadrži izolirane čvorove, jer čvor “postoji” tek kad se pojavi u from ili to. Ako imate roster, možete eksplicitno dodati sve čvorove:

roster_ids <- c("ana","iva","marko","luka","tea")  # "tea" nema veze -> izolirani čvor

edges_tmp <- tibble::tibble(
  from = c("ana","ana","marko"),
  to   = c("iva","marko","iva")
)

g2 <- graph_from_data_frame(edges_tmp, directed = TRUE, vertices = data.frame(name = roster_ids))

deg2 <- degree(g2, mode = "all")
names(deg2[deg2 == 0])   # izolati iz rosterskog popisa
## [1] "luka" "tea"

Izolirani čvorovi nisu nužno pogreška — često su informacija o slaboj uključenosti ili o granicama mreže. Važno je odlučiti analiziramo li populaciju ili samo “aktivni” dio mreže, i tu odluku jasno zapisati.


Podaci koji nedostaju (missing) u mrežama

U mrežama missing nije samo prazna ćelija u tablici, nego mijenja strukturu mreže:

  • stupnjevi (degree) padaju,
  • centralnosti se pomiču,
  • zajednice (communities) se mogu “raspasti”,
  • mjere gustoće postaju pristrane.

Vrste (praktična interpretacija)

  • Unit nonresponse: osoba ne ispuni anketu (nedostaje cijeli red/čvor kao izvor veza)
  • Item nonresponse: osoba ispuni anketu, ali preskoči neka pitanja
  • Boundary problem: niste uključili sve relevantne čvorove (npr. postoje bitni čvorovi izvan obuhvaćene grupe)
  • Recall / naming error: NG – osoba zaboravi navesti veze

Mini simulacija: što se dogodi ako “izbacimo” dio veza?

set.seed(1)

g0 <- sample_gnp(n = 30, p = 0.08, directed = FALSE)
deg0 <- degree(g0)

# uklonimo 20% bridova (kao da su "missing")
E_to_remove <- sample(E(g0), size = floor(0.2 * gsize(g0)))
g1 <- delete_edges(g0, E_to_remove)
deg1 <- degree(g1)

summary(deg0)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   0.000   1.000   2.000   2.067   3.000   5.000
summary(deg1)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   0.000   1.000   1.500   1.667   2.000   4.000

U praksi: i mali postotak podataka koji nedostaju može promijeniti metrike.

Za mreže su ključna tri “praktična” scenarija missinga:

  • Missing edges: neke veze nisu zabilježene (npr. zaborav u NG-u).

  • Missing nodes (unit nonresponse): neki sudionici nisu odgovorili → nedostaje cijeli čvor kao izvor veza.

  • Boundary: dio relevantnih čvorova je izvan uzorka → mreža je “rezana”.

Osjetljivost procjenjujemo tako da ponovimo osnovne metrike (npr. top-5 po degree) pod više scenarija i provjerimo stabilnost.

3 scenarija na jednom sintetičkom grafu:

set.seed(1)
g0 <- sample_gnp(n = 40, p = 0.07, directed = FALSE)

plot(g0)

top5 <- function(g) {
  sort(degree(g), decreasing = TRUE)[1:5]
}
top0 <- top5(g0)

# (1) missing edges: ukloni 20% bridova
E_rm <- sample(E(g0), size = floor(0.2 * ecount(g0)))
g_edges_missing <- delete_edges(g0, E_rm)

plot(g_edges_missing)

# (2) missing nodes: ukloni 10% čvorova
V_rm <- sample(V(g0), size = floor(0.1 * vcount(g0)))
g_nodes_missing <- delete_vertices(g0, V_rm)

plot(g_edges_missing)

# (3) boundary: uzmi inducirani podgraf samo nad npr. 70% čvorova (rezanje granica)
V_keep <- sample(V(g0), size = floor(0.7 * vcount(g0)))
g_boundary <- induced_subgraph(g0, vids = V_keep)

plot(g_boundary)

list(
  original = top0,
  missing_edges = top5(g_edges_missing),
  missing_nodes = top5(g_nodes_missing),
  boundary_cut  = top5(g_boundary)
)
## $original
## [1] 6 5 5 5 5
## 
## $missing_edges
## [1] 5 5 5 5 4
## 
## $missing_nodes
## [1] 6 5 5 4 4
## 
## $boundary_cut
## [1] 5 4 4 4 3

Vidimo koliko mreža može biti drugačija ovisno o tome koliko i kakve podatke o njoj prikupimo.

Strategije ublažavanja

  1. Dizajn instrumenta: roster umjesto NG kad je moguće, jasne definicije veze, vremenski okvir.
  2. Kontrolna pitanja: npr. “Jeste li naveli sve osobe s kojima ste komunicirali barem jednom?”
  3. Kombiniranje izvora: anketa + logovi (Moodle/Slack/email) uz etičku provjeru.
  4. Analitičke strategije:
  • analiza osjetljivosti (ponovi analizu pod više scenarija missinga),
  • imputacija (oprezno i uz objašnjenje pretpostavki),
  • model-based pristupi (npr. ERGM) kad je to u planu kolegija.

Etika u čišćenju: pseudonimi, minimizacija, “linkability”

Problem: čak i nakon “čišćenja”, kombinacija atributa može re-identificirati osobu.

Rješenje: agregiranje, uklanjanje nepotrebnih atributa, hash + salt, kontrola pristupa.

(Ne)zgodan SNAP dataset kao cjeloviti primjer postupka učitavanja i čišćenja

library(readr)
library(dplyr)
library(stringr)
library(lubridate)
library(igraph)
library(DBI)
library(RSQLite)

# Preuzimanje
dir.create("data", showWarnings = FALSE)

url <- "https://snap.stanford.edu/data/soc-sign-bitcoinotc.csv.gz"
f   <- file.path("data", basename(url))
if (!file.exists(f)) download.file(url, f, mode = "wb")

# Učitavanje
# SNAP format za ovaj skup: source,target,rating,timestamp (Unix time)
df_raw <- read_csv(f, col_names = FALSE, show_col_types = FALSE) |>
  setNames(c("source", "target", "rating", "time_unix"))

glimpse(df_raw)
## Rows: 35,592
## Columns: 4
## $ source    <dbl> 6, 6, 1, 4, 13, 13, 7, 2, 2, 21, 21, 21, 21, 21, 17, 17, 10,…
## $ target    <dbl> 2, 5, 15, 3, 16, 10, 5, 21, 20, 2, 1, 10, 8, 3, 3, 23, 1, 6,…
## $ rating    <dbl> 4, 2, 1, 7, 8, 8, 1, 5, 5, 5, 8, 8, 9, 7, 5, 1, 8, 7, 8, 1, …
## $ time_unix <dbl> 1289241912, 1289241942, 1289243140, 1289245277, 1289254254, …
# Standardizacija tipova
df <- df_raw |>
  transmute(
    source = str_trim(as.character(source)),
    target = str_trim(as.character(target)),
    rating = suppressWarnings(as.integer(rating)),
    time   = as.POSIXct(time_unix, origin = "1970-01-01", tz = "UTC")
  )

glimpse(df)
## Rows: 35,592
## Columns: 4
## $ source <chr> "6", "6", "1", "4", "13", "13", "7", "2", "2", "21", "21", "21"…
## $ target <chr> "2", "5", "15", "3", "16", "10", "5", "21", "20", "2", "1", "10…
## $ rating <int> 4, 2, 1, 7, 8, 8, 1, 5, 5, 5, 8, 8, 9, 7, 5, 1, 8, 7, 8, 1, 10,…
## $ time   <dttm> 2010-11-08 18:45:11, 2010-11-08 18:45:41, 2010-11-08 19:05:40,…
# Osnovne provjere

# Missing / prazno
dq_missing <- df |>
  summarise(
    n = n(),
    na_source = sum(is.na(source) | source == ""),
    na_target = sum(is.na(target) | target == ""),
    na_rating = sum(is.na(rating)),
    na_time   = sum(is.na(time))
  )

dq_missing
## # A tibble: 1 × 5
##       n na_source na_target na_rating na_time
##   <int>     <int>     <int>     <int>   <int>
## 1 35592         0         0         0       0
# Raspon ratinga (trebao bi biti -10 do +10)
range(df$rating, na.rm = TRUE)
## [1] -10  10
# Self-loopovi (source==target)
sum(df$source == df$target, na.rm = TRUE)
## [1] 0
# Čišćenje i pravila

df_clean <- df |>
  # izbaciti retke bez ključnih polja (minimalno)
  filter(!is.na(source), source != "", !is.na(target), target != "") |>
  # izbaciti rating izvan raspona (defenzivno)
  filter(!is.na(rating), rating >= -10, rating <= 10) |>
  # ukloniti self-loopove (ovdje pretpostavljamo da postoje i nisu smisleni)
  filter(source != target) |>
  # skraćeni prikaz vremena: umjesto sekundi uzmemo samo datum (često dovoljno)
  mutate(date = as.Date(time)) |>
  select(source, target, rating, date)

glimpse(df_clean)
## Rows: 35,592
## Columns: 4
## $ source <chr> "6", "6", "1", "4", "13", "13", "7", "2", "2", "21", "21", "21"…
## $ target <chr> "2", "5", "15", "3", "16", "10", "5", "21", "20", "2", "1", "10…
## $ rating <int> 4, 2, 1, 7, 8, 8, 1, 5, 5, 5, 8, 8, 9, 7, 5, 1, 8, 7, 8, 1, 10,…
## $ date   <date> 2010-11-08, 2010-11-08, 2010-11-08, 2010-11-08, 2010-11-08, 20…
# Duplikati i multi-edge: moramo odlučiti što znači "ista veza"

# Tipično u temporalnim mrežama isti par (source,target) ima više ocjena kroz vrijeme.

# Ovdje su 2 varijante:

# Event" edge list: svaka ocjena je zaseban događaj (zadržimo sve)
edges_events <- df_clean |>
  rename(from = source, to = target, weight = rating)
edges_events
## # A tibble: 35,592 × 4
##    from  to    weight date      
##    <chr> <chr>  <int> <date>    
##  1 6     2          4 2010-11-08
##  2 6     5          2 2010-11-08
##  3 1     15         1 2010-11-08
##  4 4     3          7 2010-11-08
##  5 13    16         8 2010-11-08
##  6 13    10         8 2010-11-08
##  7 7     5          1 2010-11-10
##  8 2     21         5 2010-11-10
##  9 2     20         5 2010-11-10
## 10 21    2          5 2010-11-10
## # ℹ 35,582 more rows
# "Agregirana" mreža: jedna veza po paru (from, to)
# Pravilo: prosječna ocjena + broj interakcija
edges_agg <- df_clean |>
  group_by(source, target) |>
  summarise(
    weight_mean = mean(rating),
    n_events    = n(),
    .groups = "drop"
  ) |>
  rename(from = source, to = target)
edges_agg
## # A tibble: 35,592 × 4
##    from  to    weight_mean n_events
##    <chr> <chr>       <dbl>    <int>
##  1 1     10              7        1
##  2 1     101             1        1
##  3 1     1010            2        1
##  4 1     104             1        1
##  5 1     1053            2        1
##  6 1     109             1        1
##  7 1     110             1        1
##  8 1     111             2        1
##  9 1     1134            1        1
## 10 1     114             2        1
## # ℹ 35,582 more rows
# Validacija edge lista (brzo)
validate_simple <- function(edges) {
  list(
    missing = edges |> summarise(
      na_from = sum(is.na(from) | from == ""),
      na_to   = sum(is.na(to) | to == "")
    ),
    self_loops = sum(edges$from == edges$to, na.rm = TRUE),
    duplicates = edges |> count(from, to) |> filter(n > 1) |> nrow()
  )
}

validate_simple(edges_events)
## $missing
## # A tibble: 1 × 2
##   na_from na_to
##     <int> <int>
## 1       0     0
## 
## $self_loops
## [1] 0
## 
## $duplicates
## [1] 0
validate_simple(edges_agg)
## $missing
## # A tibble: 1 × 2
##   na_from na_to
##     <int> <int>
## 1       0     0
## 
## $self_loops
## [1] 0
## 
## $duplicates
## [1] 0
# Kreiranje grafa (primjer: pozitivne veze) 
edges_pos <- edges_events |>
  filter(weight > 0) |>
  select(from, to, weight, date)

g_pos <- graph_from_data_frame(edges_pos, directed = TRUE)
g_pos
## IGRAPH 45151f6 DNW- 5573 32029 -- 
## + attr: name (v/c), weight (e/n), date (e/n)
## + edges from 45151f6 (vertex names):
##  [1] 6 ->2  6 ->5  1 ->15 4 ->3  13->16 13->10 7 ->5  2 ->21 2 ->20 21->2 
## [11] 21->1  21->10 21->8  21->3  17->3  17->23 10->1  10->6  10->21 10->8 
## [21] 10->25 10->2  10->3  4 ->26 26->4  5 ->1  5 ->6  5 ->7  1 ->5  6 ->4 
## [31] 4 ->6  2 ->4  17->28 17->13 13->17 13->29 29->13 17->20 4 ->31 31->4 
## [41] 32->6  13->1  7 ->34 34->7  32->1  1 ->32 1 ->34 34->1  34->13 13->34
## [51] 6 ->7  7 ->6  1 ->17 1 ->31 31->1  35->6  1 ->13 36->37 37->36 35->1 
## [61] 17->1  8 ->1  7 ->29 1 ->20 37->44 44->37 39->45 39->7  39->44 44->39
## [71] 23->17 23->19 36->46 46->36 47->1  13->7  7 ->13 29->51 51->29 29->52
## + ... omitted several edges

Napomena: ranije smo za ovu mrežu kreirali graf s 50 top čvorova po stupnju (in-degree).

Drugi (ne)zgodan primjer postupka učitavanja i čišćenja: trgovinske veze zemalja (mini varijanta)

Za uistinu “nezgodan”, ali super primjer za učenje, Wikipedia je savršena, ali ne kao jedna tablica (iako smo na primjeru regija i općina u RH vidjeli da i na jednoj tablici može biti puno čišćenja), nego kao skup tablica s više stranica. Te stranice imaju različite godine, valute, fusnote, formate tablica…

Dobar početni skup stranica koje sigurno postoje:

  • List of the largest trading partners of Germany (Wikipedia) List of the largest trading partners of Italy (Wikipedia)
  • List of the largest trading partners of the United Kingdom (odličan “problematičan” primjer zbog GBP/2024) (Wikipedia)

S obzirom da ovi popisi mogu postati dugački, recimo da nas zanima samo trgovanje sa zemljama u Europskoj uniji (EU27).

eu <- c(
  "Austria","Belgium","Bulgaria","Croatia","Cyprus","Czech Republic",
  "Denmark","Estonia","Finland","France","Germany","Greece","Hungary",
  "Ireland","Italy","Latvia","Lithuania","Luxembourg","Malta",
  "Netherlands","Poland","Portugal","Romania","Slovakia","Slovenia","Spain","Sweden"
)

Paketi + EU popis + izvori

library(rvest)
library(dplyr)
library(stringr)
library(janitor)
library(readr)
library(tibble)

sources <- tibble::tribble(
  ~from_country, ~url,
  "Germany", "https://en.wikipedia.org/wiki/List_of_the_largest_trading_partners_of_Germany",
  "Italy",   "https://en.wikipedia.org/wiki/List_of_the_largest_trading_partners_of_Italy",
  "UK",  "https://en.wikipedia.org/wiki/List_of_the_largest_trading_partners_of_the_United_Kingdom"
)

Jedna stranica (Germany) — učitavanje, uvid, čišćenje, edge list

Učitamo stranicu + izvučemo sve tablice

url <- sources$url[1]  # Germany
page <- read_html(url)

tables <- html_table(page, fill = TRUE)
length(tables)
## [1] 4

Uvid: koje tablice postoje?

Najjednostavnije: pogledamo nazive stupaca za prvih nekoliko tablica.

lapply(tables[1:6], names)
## [[1]]
## [1] "X1" "X2"
## 
## [[2]]
##  [1] "X1"  "X2"  "X3"  "X4"  "X5"  "X6"  "X7"  "X8"  "X9"  "X10" "X11" "X12"
## [13] "X13" "X14" "X15" "X16" "X17" "X18" "X19" "X20" "X21" "X22" "X23" "X24"
## [25] "X25" "X26" "X27" "X28" "X29" "X30" "X31" "X32" "X33" "X34" "X35" "X36"
## [37] "X37" "X38" "X39" "X40" "X41" "X42" "X43" "X44" "X45" "X46" "X47" "X48"
## [49] "X49" "X50" "X51" "X52" "X53" "X54" "X55" "X56" "X57" "X58" "X59" "X60"
## [61] "X61" "X62" "X63" "X64" "X65" "X66" "X67" "X68"
## 
## [[3]]
## [1] "Rank"         "Country"      "Export(2023)"
## 
## [[4]]
## [1] "Rank"         "Country"      "Import(2023)"
## 
## [[5]]
## NULL
## 
## [[6]]
## NULL

Ako treba, proširimo:

lapply(tables, names)
## [[1]]
## [1] "X1" "X2"
## 
## [[2]]
##  [1] "X1"  "X2"  "X3"  "X4"  "X5"  "X6"  "X7"  "X8"  "X9"  "X10" "X11" "X12"
## [13] "X13" "X14" "X15" "X16" "X17" "X18" "X19" "X20" "X21" "X22" "X23" "X24"
## [25] "X25" "X26" "X27" "X28" "X29" "X30" "X31" "X32" "X33" "X34" "X35" "X36"
## [37] "X37" "X38" "X39" "X40" "X41" "X42" "X43" "X44" "X45" "X46" "X47" "X48"
## [49] "X49" "X50" "X51" "X52" "X53" "X54" "X55" "X56" "X57" "X58" "X59" "X60"
## [61] "X61" "X62" "X63" "X64" "X65" "X66" "X67" "X68"
## 
## [[3]]
## [1] "Rank"         "Country"      "Export(2023)"
## 
## [[4]]
## [1] "Rank"         "Country"      "Import(2023)"

Ručno odaberemo tablicu koja izgleda kao “Exports by country”.

Vidjeli smo da je to tablica broj 3.

tb <- tables[[3]]
head(tb)
## # A tibble: 6 × 3
##    Rank Country       `Export(2023)`
##   <dbl> <chr>                  <dbl>
## 1     1 United States          158. 
## 2     2 France                 120. 
## 3     3 Netherlands            112. 
## 4     4 China                   97.4
## 5     5 Poland                  90.6
## 6     6 Italy                   85.3

Očistimo nazive stupaca + preimenujemo ključne stupce

tb2 <- tb %>%
  clean_names()

names(tb2)
## [1] "rank"        "country"     "export_2023"

Sad ručno pogledamo kako se zovu stupci nakon clean_names() i preimenujemo ih u country i export.

Primjer (ovdje ćete prilagoditi ako su nazivi drugačiji):

tb3 <- tb2 %>%
  rename(
    country = country,
    export  = export_2023
  )
tb3 <- tb3[,2:3] # zadržimo samo 2. i 3. stupac

Provjera:

str(tb3)
## tibble [10 × 2] (S3: tbl_df/tbl/data.frame)
##  $ country: chr [1:10] "United States" "France" "Netherlands" "China" ...
##  $ export : num [1:10] 157.9 119.8 112 97.3 90.6 ...

Čišćenje vrijednosti + filtriranje EU partnera

from_country <- sources$from_country[1]  # Germany

edges_de <- tb3 %>%
  mutate(
    country = country %>%
      str_replace_all("\\[.*?\\]", "") %>%
      str_squish()
  ) %>%
  filter(!is.na(country), country != "") %>%
  filter(country %in% eu) %>%
  filter(country != from_country) %>%
  transmute(
    from = from_country,
    to = country,
    weight = export
  )

edges_de
## # A tibble: 6 × 3
##   from    to          weight
##   <chr>   <chr>        <dbl>
## 1 Germany France       120. 
## 2 Germany Netherlands  112. 
## 3 Germany Poland        90.6
## 4 Germany Italy         85.3
## 5 Germany Austria       80.4
## 6 Germany Belgium       60.8

To je naš edge list za Germany → EU partneri.

Ponovimo isti postupak za Italy i France + spojimo

Ovo je najjednostavnije: copy/paste blok i promijena k i i.

Prelazimo na Italiju.

k <- 2
url <- sources$url[k]
from_country <- sources$from_country[k]

page <- read_html(url)
tables <- html_table(page, fill = TRUE)

# uvid
length(tables)
## [1] 2
lapply(tables[1:6], names)
## [[1]]
## [1] "Rank"                "Country / Territory" "Total trade"        
## [4] "Italy exports"       "Italy imports"       "Tradebalance"       
## 
## [[2]]
## [1] ".mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}vte Economy of Italy"
## [2] ".mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}vte Economy of Italy"
## [3] ".mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}vte Economy of Italy"
## 
## [[3]]
## NULL
## 
## [[4]]
## NULL
## 
## [[5]]
## NULL
## 
## [[6]]
## NULL
# RUČNO postavimo ispravan indeks tablice za Italy:
tb <- tables[[1]]

tb2 <- tb %>% clean_names()
names(tb2)
## [1] "rank"              "country_territory" "total_trade"      
## [4] "italy_exports"     "italy_imports"     "tradebalance"
# RUČNO preimenuj:
tb3 <- tb2 %>%
  rename(country = country_territory, export = italy_exports)
str(tb3)
## tibble [12 × 6] (S3: tbl_df/tbl/data.frame)
##  $ rank         : chr [1:12] "-" "1" "2" "3" ...
##  $ country      : chr [1:12] "European Union" "Germany" "France" "United States" ...
##  $ total_trade  : chr [1:12] "660.3" "164.4" "110.0" "92.5" ...
##  $ export       : num [1:12] 323 74.7 63.4 67.3 19.2 33 18.5 30.5 19.3 19.8 ...
##  $ italy_imports: num [1:12] 337.3 89.7 46.6 25.2 47.6 ...
##  $ tradebalance : num [1:12] -14.3 -15 16.8 42.1 -28.4 0.2 -17.9 12.6 -7.4 3.7 ...
tb3 <- tb3[, c(2,4)]
str(tb3)
## tibble [12 × 2] (S3: tbl_df/tbl/data.frame)
##  $ country: chr [1:12] "European Union" "Germany" "France" "United States" ...
##  $ export : num [1:12] 323 74.7 63.4 67.3 19.2 33 18.5 30.5 19.3 19.8 ...
from_country <- sources$from_country[2]  # Italy

edges_it <- tb3 %>%
  mutate(
    country = country %>%
      str_replace_all("\\[.*?\\]", "") %>%
      str_squish()
  ) %>%
  filter(!is.na(country), country != "") %>%
  filter(country %in% eu) %>%
  filter(country != from_country) %>%
  transmute(
    from = from_country,
    to = country,
    weight = export
  )

edges_it
## # A tibble: 6 × 3
##   from  to          weight
##   <chr> <chr>        <dbl>
## 1 Italy Germany       74.7
## 2 Italy France        63.4
## 3 Italy Spain         33  
## 4 Italy Netherlands   18.5
## 5 Italy Belgium       19.3
## 6 Italy Poland        19.8

Sljedeće, ponavljamo istu proceduru za UK.

k <- 3
url <- sources$url[k]
from_country <- sources$from_country[k]

page <- read_html(url)
tables <- html_table(page, fill = TRUE)

length(tables)
## [1] 1
lapply(tables[1:6], names)
## [[1]]
## [1] "Rank"            "Country"         "Imports to UK"   "Exports from UK"
## [5] "Total trade"     "Trade balance"  
## 
## [[2]]
## NULL
## 
## [[3]]
## NULL
## 
## [[4]]
## NULL
## 
## [[5]]
## NULL
## 
## [[6]]
## NULL
# RUČNO postavimo indeks tablice:
tb <- tables[[1]]

tb2 <- tb %>% clean_names()
names(tb2)
## [1] "rank"            "country"         "imports_to_uk"   "exports_from_uk"
## [5] "total_trade"     "trade_balance"
# RUČNO preimenujemo:
tb3 <- tb2 %>%
  rename(country = country, export = exports_from_uk)
str(tb3)
## tibble [52 × 6] (S3: tbl_df/tbl/data.frame)
##  $ rank         : chr [1:52] "-" "-" "1" "2" ...
##  $ country      : chr [1:52] "Total for non-EU" "European Union (Total)" "United States" "Germany" ...
##  $ imports_to_uk: chr [1:52] "425,401" "450,889" "115,356" "87,621" ...
##  $ export       : chr [1:52] "504,968" "356,266" "186,735" "62,982" ...
##  $ total_trade  : chr [1:52] "930,369" "807,155" "302,091" "150,603" ...
##  $ trade_balance: chr [1:52] "79,567" "-94,623" "71,379" "-24,639" ...
tb3 <- tb3[, c(2,4)]
str(tb3)
## tibble [52 × 2] (S3: tbl_df/tbl/data.frame)
##  $ country: chr [1:52] "Total for non-EU" "European Union (Total)" "United States" "Germany" ...
##  $ export : chr [1:52] "504,968" "356,266" "186,735" "62,982" ...
tb3$export <- parse_number(tb3$export)
str(tb3)
## tibble [52 × 2] (S3: tbl_df/tbl/data.frame)
##  $ country: chr [1:52] "Total for non-EU" "European Union (Total)" "United States" "Germany" ...
##  $ export : num [1:52] 504968 356266 186735 62982 52541 ...
from_country <- sources$from_country[3]  # UK

edges_uk <- tb3 %>%
  mutate(
    country = country %>%
      str_replace_all("\\[.*?\\]", "") %>%
      str_squish()
  ) %>%
  filter(!is.na(country), country != "") %>%
  filter(country %in% eu) %>%
  filter(country != from_country) %>%
  transmute(
    from = from_country,
    to = country,
    weight = export
  )

edges_uk
## # A tibble: 21 × 3
##    from  to          weight
##    <chr> <chr>        <dbl>
##  1 UK    Germany      62982
##  2 UK    Netherlands  52541
##  3 UK    France       44647
##  4 UK    Ireland      57732
##  5 UK    Spain        19723
##  6 UK    Belgium      25681
##  7 UK    Italy        18866
##  8 UK    Poland       10487
##  9 UK    Sweden       11240
## 10 UK    Luxembourg   11663
## # ℹ 11 more rows

Spojimo prikupljene podatke u jednu tablicu.

edges_eu <- bind_rows(edges_de, edges_it, edges_uk)
edges_eu
## # A tibble: 33 × 3
##    from    to          weight
##    <chr>   <chr>        <dbl>
##  1 Germany France       120. 
##  2 Germany Netherlands  112. 
##  3 Germany Poland        90.6
##  4 Germany Italy         85.3
##  5 Germany Austria       80.4
##  6 Germany Belgium       60.8
##  7 Italy   Germany       74.7
##  8 Italy   France        63.4
##  9 Italy   Spain         33  
## 10 Italy   Netherlands   18.5
## # ℹ 23 more rows

Za UK, izvoz je izražen u milijunima GBP, a za Njemačku i Italiju milijardama eura. Weight mora imati jedinstvenu jedinicu i značenje kroz cijeli graf. Koristimo prosječni tečaj za 2023: 1 GBP = 1.15 EUR.

edges_eu[13:33, 3] <- edges_eu[13:33, 3]/ 1000*1.15
edges_eu
## # A tibble: 33 × 3
##    from    to          weight
##    <chr>   <chr>        <dbl>
##  1 Germany France       120. 
##  2 Germany Netherlands  112. 
##  3 Germany Poland        90.6
##  4 Germany Italy         85.3
##  5 Germany Austria       80.4
##  6 Germany Belgium       60.8
##  7 Italy   Germany       74.7
##  8 Italy   France        63.4
##  9 Italy   Spain         33  
## 10 Italy   Netherlands   18.5
## # ℹ 23 more rows

Da imamo prevelik skup podataka i ne možemo pregledom utvrditi poziciju u data.frame-u, koristili bi

index <- which(edges_eu$from == "UK", arr.ind = TRUE)
index
##  [1] 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

I onda bi primijenili istu naredbu

edges_eu[index, 3] <- edges_eu[index, 3]/ 1000*1.15
edges_eu

Uvid u mrežu

library(igraph)

g <- graph_from_data_frame(edges_eu, directed = TRUE)
g
## IGRAPH 4645342 DNW- 22 33 -- 
## + attr: name (v/c), weight (e/n)
## + edges from 4645342 (vertex names):
##  [1] Germany->France      Germany->Netherlands Germany->Poland     
##  [4] Germany->Italy       Germany->Austria     Germany->Belgium    
##  [7] Italy  ->Germany     Italy  ->France      Italy  ->Spain      
## [10] Italy  ->Netherlands Italy  ->Belgium     Italy  ->Poland     
## [13] UK     ->Germany     UK     ->Netherlands UK     ->France     
## [16] UK     ->Ireland     UK     ->Spain       UK     ->Belgium    
## [19] UK     ->Italy       UK     ->Poland      UK     ->Sweden     
## [22] UK     ->Luxembourg  UK     ->Denmark     UK     ->Greece     
## + ... omitted several edges
plot(g, layout = layout_with_fr)


Pretvorbe formata

Edge list -> adjacency (tidyr)

Binary adjacency:

edges_base
##    from    to weight
## 1   Ana Marko      3
## 2   Ana   Iva      1
## 3 Marko   Iva      2
## 4   Iva   Ana      1
edges_bin <- edges_base %>%
  mutate(value = 1) %>%
  select(from, to, value)

adj_bin <- edges_bin %>%
  pivot_wider(names_from = to, values_from = value, values_fill = 0)

adj_bin
## # A tibble: 3 × 4
##   from  Marko   Iva   Ana
##   <chr> <dbl> <dbl> <dbl>
## 1 Ana       1     1     0
## 2 Marko     0     1     0
## 3 Iva       0     0     1

Weighted adjacency (ako imamo weight):

adj_w <- edges_base %>%
  select(from, to, weight) %>%
  pivot_wider(names_from = to, values_from = weight, values_fill = 0)

adj_w
## # A tibble: 3 × 4
##   from  Marko   Iva   Ana
##   <chr> <dbl> <dbl> <dbl>
## 1 Ana       3     1     0
## 2 Marko     0     2     0
## 3 Iva       0     0     1

Adjacency -> edge list (matrica u long format)

# primjer adjacency matrice (ručno)
adj_m <- matrix(c(
  0,1,0,
  0,0,2,
  1,0,0
), nrow=3, byrow=TRUE)
colnames(adj_m) <- rownames(adj_m) <- c("ana","marko","iva")

adj_m
##       ana marko iva
## ana     0     1   0
## marko   0     0   2
## iva     1     0   0
edge_from_adj <- as.data.frame(adj_m) %>%
  rownames_to_column("from") %>%
  pivot_longer(-from, names_to="to", values_to="weight") %>%
  filter(weight != 0)

edge_from_adj
## # A tibble: 3 × 3
##   from  to    weight
##   <chr> <chr>  <dbl>
## 1 ana   marko      1
## 2 marko iva        2
## 3 iva   ana        1

Node table i spajanje atributa (edge + node workflow)

Mrežni podaci su često podijeljeni na:

  • edge tablicu (veze) i

  • node tablicu (atributi čvorova).

Ovo razdvajanje je standard u mrežnoj analizi i olakšava etičku minimizaciju (atribute čuvamo samo ako nam baš trebaju).

edges_demo <- edges_base %>% mutate(across(c(from,to), as.character))

# Node tablica: svi čvorovi iz from i to
nodes_demo <- tibble::tibble(
  name = unique(c(edges_demo$from, edges_demo$to))
) %>%
  mutate(
    group = ifelse(name %in% c("Ana","Iva"), "A", "B")  # demo atribut
  )

# provjera: imaju li svi čvorovi atribute?
dplyr::anti_join(
  tibble::tibble(name = unique(c(edges_demo$from, edges_demo$to))),
  nodes_demo,
  by = "name"
)
## # A tibble: 0 × 1
## # ℹ 1 variable: name <chr>
nodes_demo
## # A tibble: 3 × 2
##   name  group
##   <chr> <chr>
## 1 Ana   A    
## 2 Marko B    
## 3 Iva   A

Dvomodalne u jednomodalne mreže

Tehnički nije u pitanju format podataka, ali je u pitanju oblik konverzije. Dvomodalne mreže (two-mode) imaju dva tipa čvorova, npr. student–alat ili student–tema. Takvi podaci često dolaze kao matrica incidencije (red = student, stupac = alat). Za analizu “tko je s kim sličan”, često radimo projekciju u one-mode: student–student (povezani ako koriste isti alat).

# incidence: student -> alat (bipartite edge list)
edges_bi <- tibble::tibble(
  student = c("ana","ana","iva","marko","marko","luka"),
  tool    = c("r","moodle","r","python","moodle","r")
)

# bipartite graf
g_bi <- igraph::graph_from_data_frame(
  edges_bi %>% transmute(from = student, to = tool),
  directed = FALSE
)

plot(g_bi)

# označi tip čvora (TRUE/FALSE): studenti su TRUE
V(g_bi)$type <- V(g_bi)$name %in% unique(edges_bi$tool)  # tool=TRUE, student=FALSE (ili obrnuto)

# projekcija na studente (one-mode): student-student
proj <- bipartite_projection(g_bi, which = FALSE)  # which=FALSE -> dio gdje type==FALSE
g_students <- proj

# g_students
plot(g_students)

Pretvorbe R formata i objekta koje koristimo u SNA

Pretvorba data.frame u tibble:

as_tibble(df)

Pretvorba tibble u data.frame:

as.data.frame(tb)

Pretvorba igraph objekta u data.frame:

as_data_frame(g, what = "edges")
as_data_frame(g, what = "vertices")

Pretvorba tidygraph u igraph objekt:

as.igraph(tg)

U praksi često radimo:

CSV → tibble → čišćenje → igraph → tbl_graph → vizualizacija → export

Primjer:

edges_clean <- as_tibble(df)

g <- graph_from_data_frame(edges_clean, directed = TRUE)

tg <- as_tbl_graph(g)

tg %>%
  activate(nodes) %>%
  mutate(deg = centrality_degree())

Zašto je ovo važno?

Jer različite funkcije očekuju različite tipove objekata:

Funkcija Očekuje
filter() tibble/data.frame
degree() igraph
centrality_degree() tbl_graph
pivot_wider() tibble
readMM() matrix

Kratka provjera tipa objekta kad niste sigurni:

class(obj)
str(obj)

Validacija konzistentnosti (obavezno prije analize)

Cilj validacije: prije nego što krenemo u metrike, želimo znati da su podaci smisleni.

Tipične greške

  • nedostaje from ili to
  • prazni stringovi
  • self-loop (from == to) ako nije dopušteno
  • duplikati (iste veze više puta)
  • čvorovi koji ne postoje u rosteru (ako radimo roster)
  • kod neusmjerene mreže: postoje oba smjera s različitim težinama

Minimalni izvještaj o kvaliteti edge liste

Prije bilo kakvih mrežnih metrika napravimo kratki QA izvještaj: koliko je čvorova i veza, ima li missing vrijednosti, self-loopova i duplikata, te izgledaju li težine numerički smisleno. Ovo je “obavezni ritual” prije analize.

Kod (QA summary tablica)

qa_edges <- function(edges, directed = TRUE) {
  stopifnot(all(c("from","to") %in% names(edges)))

  edges2 <- edges %>%
    mutate(
      from = as.character(from),
      to   = as.character(to)
    )

  tibble::tibble(
    n_rows = nrow(edges2),
    n_nodes = length(unique(c(edges2$from, edges2$to))),
    n_edges = nrow(edges2),
    missing_from = sum(is.na(edges2$from) | str_trim(edges2$from) == ""),
    missing_to   = sum(is.na(edges2$to)   | str_trim(edges2$to) == ""),
    self_loops   = sum(edges2$from == edges2$to, na.rm = TRUE),
    duplicate_pairs = edges2 %>% count(from, to) %>% filter(n > 1) %>% nrow(),
    has_weight = "weight" %in% names(edges2),
    weight_non_numeric = if ("weight" %in% names(edges2)) sum(is.na(suppressWarnings(as.numeric(edges2$weight)))) else NA_integer_
  )
}

qa_edges(edges_base)
## # A tibble: 1 × 9
##   n_rows n_nodes n_edges missing_from missing_to self_loops duplicate_pairs
##    <int>   <int>   <int>        <int>      <int>      <int>           <int>
## 1      4       3       4            0          0          0               0
## # ℹ 2 more variables: has_weight <lgl>, weight_non_numeric <int>

Funkcija za brzu validaciju (edge list)

validate_edges <- function(edges, directed = TRUE, allow_self_loops = FALSE) {
  out <- list()

  # 1) obavezni stupci
  required <- c("from","to")
  out$missing_columns <- setdiff(required, names(edges))

  # 2) missing / prazno
  out$missing_from_to <- edges %>%
    mutate(
      from_missing = is.na(from) | str_trim(from)=="" ,
      to_missing   = is.na(to)   | str_trim(to)==""
    ) %>%
    filter(from_missing | to_missing)

  # 3) self loops
  out$self_loops <- edges %>% filter(from == to)
  out$self_loops_allowed <- allow_self_loops

  # 4) duplikati (isti from-to)
  out$duplicates <- edges %>%
    count(from, to, name="n") %>%
    filter(n > 1)

  # 5) simetrija kod neusmjerene
  if (!directed) {
    tmp <- edges %>%
      mutate(a = pmin(from,to), b = pmax(from,to)) %>%
      count(a,b, name="n") %>%
      filter(n > 1)
    out$undirected_pair_duplicates <- tmp
  }

  return(out)
}

v <- validate_edges(edges_clean2, directed = TRUE, allow_self_loops = FALSE)
names(v)

Prikaz najvažnijih problema:

v$missing_columns
v$missing_from_to
v$self_loops
v$duplicates

Etika: anonimizacija i minimizacija podataka

Osnovna pravila

  • Minimizacija: prikupljaj samo ono što treba za ishode (npr. ne treba OIB, adresa, broj mobitela).
  • Anonimizacija/pseudonimizacija: zamijeni identitete kodovima.
  • Razdvajanje ključa: tablica “ID -> osoba” čuva se odvojeno i ograničeno.
  • Privola i transparentnost: tko, zašto, kako dugo, tko ima pristup, mogu li odustati.

Pseudonimizacija u R-u (hash)

Ovo je praktična metoda: napravimo stabilan pseudonim iz stringa.

nodes <- unique(c(edges_base$from, edges_base$to))
node_map <- data.frame(
  name = nodes,
  pseudo_id = sapply(nodes, function(x) digest(x, algo = "sha256")),
  stringsAsFactors = FALSE
)

head(node_map)
##        name                                                        pseudo_id
## Ana     Ana c26eaeb3cea6e716a3efadd195c5616bc5d7aa97fb360c3b74f08ed9d4a0e6ae
## Marko Marko 83d46236a546aa6fd3f50adbfe833a738659fb1c38867ff42a4b9c7cb35e06e4
## Iva     Iva 5dbe0e8a32f716da26d624e9e4c74e05229f6e772f090229265b092ffe446d77

Zamjena u edge list:

edges_pseudo <- edges_base %>%
  left_join(node_map, by = c("from" = "name")) %>%
  rename(from_id = pseudo_id) %>%
  left_join(node_map, by = c("to" = "name")) %>%
  rename(to_id = pseudo_id) %>%
  select(from_id, to_id, weight)

edges_pseudo
##                                                            from_id
## 1 c26eaeb3cea6e716a3efadd195c5616bc5d7aa97fb360c3b74f08ed9d4a0e6ae
## 2 c26eaeb3cea6e716a3efadd195c5616bc5d7aa97fb360c3b74f08ed9d4a0e6ae
## 3 83d46236a546aa6fd3f50adbfe833a738659fb1c38867ff42a4b9c7cb35e06e4
## 4 5dbe0e8a32f716da26d624e9e4c74e05229f6e772f090229265b092ffe446d77
##                                                              to_id weight
## 1 83d46236a546aa6fd3f50adbfe833a738659fb1c38867ff42a4b9c7cb35e06e4      3
## 2 5dbe0e8a32f716da26d624e9e4c74e05229f6e772f090229265b092ffe446d77      1
## 3 5dbe0e8a32f716da26d624e9e4c74e05229f6e772f090229265b092ffe446d77      2
## 4 c26eaeb3cea6e716a3efadd195c5616bc5d7aa97fb360c3b74f08ed9d4a0e6ae      1

Važno: hash nije “magična” anonimizacija — ako je skup imena mali i poznat, moguće je pogoditi. Zato se u stvarnim projektima radi pažljivije (kontrola pristupa, agregiranje).


Pohrana rezultata

Kad završimo čišćenje i pripremu, cilj je da rezultate možemo:

  • ponovno učitati bez gubitka (reproducibilnost),

  • podijeliti s drugima (nastava/projekt),

  • uključiti u izvještaj (slike/grafovi).

U praksi najčešće spremamo:

  • edge list (csv, rds, xlsx),

  • node tablicu (atributi čvorova),

  • graf objekt (rds, graphml),

  • slike (png, pdf, svg).

Prije no što nastavimo sa specifičnim oblicima spremanja, podsjetimo se da je za ponovni rad najelegantnije rješenje pohraniti sve u jednom projektu (više o tome u poglavlju priručnika o projektima), a postupke dokumentirati u skripti ((više o tome u poglavlju priručnika o skriptama](https://uvod-u-r-i-r-studio.netlify.app/osnovnipojmoviiprvikoraci#skripte)). Osim skripte, moguće je koristiti R dokument R Markdown. R Markdown je izvrsna opcija ako se R koristi pri sastavljanju izvješća ili dokumenata, jer podržava pisanje i uređivanje tekstova u kombinaciji s izvođenjem koda i izlazom dokumenta u različitim formatima.

Ponekad će nam za potrebe izvješavanja biti potrebno pripremiti tablice i slike u formatima koje možemo upotrijebiti u tekstovnim i prezentacijskim dokumentima. Također, ponekad ćemo htjeti nastaviti rad na grafu koristeći neki drugi softver (npr. Gephi; Gephi je open source aplikacija za vizualizaciju mreža, a podržava i izračunavanje nekih metrika). Za takve situacije, koristit ćemo neki od sljedećih načina pohrane.

Spremanje edge liste i node tablice

CSV (najčešće i najprenosivije)

dir.create("data", showWarnings = FALSE)

write_csv(edges_demo, "data_edges_demo.csv")

# ako imate node tablicu:
# write_csv(nodes_demo, "data_nodes.csv")

RDS (najsigurnije u R-u: čuva tipove podataka)

saveRDS(edges_eu, "data/edges_eu.rds")
# učitavanje:
edges_eu2 <- readRDS("data/edges_eu.rds")

Excel (kad treba drugim ljudima)

install.packages("writexl")
library(writexl)

writexl::write_xlsx(list(edges = edges_eu, nodes = nodes_demo), "data/network_tables.xlsx")

Spremanje grafa (igraph / tidygraph)

RDS (najjednostavnije)

saveRDS(g, "data/graph_eu.rds")
g_loaded <- readRDS("data/graph_eu.rds")
g_loaded

GraphML (za Gephi / Cytoscape)

GraphML je koristan ako graf želite otvoriti u alatima izvan R-a.

write_graph(g, "data/graph_eu.graphml", format = "graphml")
# učitavanje:
# g2 <- read_graph("data/graph_eu.graphml", format = "graphml")

Spremanje slika grafa (PNG/PDF/SVG)

Base igraph plot → PNG

Najprije otvorimo “grafički uređaj” (png()), nacrtamo graf, pa zatvorimo dev.off().

dir.create("figures", showWarnings = FALSE)

png(
  filename = "figures/eu_graph.png",
  width = 1600, height = 1200, res = 200
)

plot(
  g,
  layout = layout_with_fr,
  vertex.size = 6,
  vertex.label.cex = 0.7,
  vertex.label.dist = 0.3,
  edge.arrow.size = 0.3
)

dev.off()

Najvažniji parametri koje podešavamo:

width, height, res (kvaliteta slike),

vertex.size (veličina čvorova),

vertex.label.cex (veličina teksta),

vertex.label.dist (udaljenost teksta od čvora),

edge.arrow.size (veličina strelice),

layout (npr. layout_with_fr, layout_in_circle, layout_as_tree).

PDF (za tiskanje i vektorsku kvalitetu)

PDF je odličan za skripte i članke jer je skalabilan.

pdf("figures/eu_graph.pdf", width = 8, height = 6)

plot(
  g,
  layout = layout_with_fr,
  vertex.size = 6,
  vertex.label.cex = 0.7,
  edge.arrow.size = 0.3
)

dev.off()
ggraph → ggsave (preporučeno kad radite s ggraph)

Ako graf crtate s ggraph-om, koristite ggsave().

library(ggraph)
library(tidygraph)

tg <- as_tbl_graph(g)

p <- ggraph(tg, layout = "fr") +
  geom_edge_link(alpha = 0.4) +
  geom_node_point(size = 2) +
  geom_node_text(aes(label = name), repel = TRUE, size = 3) +
  theme_graph()

p

ggsave("figures/eu_graph_ggraph.png", plot = p, width = 8, height = 6, dpi = 300)
ggsave("figures/eu_graph_ggraph.pdf", plot = p, width = 8, height = 6)

Parametri ggsave() koji su najčešće potrebni:

  • width, height (u inčima),

  • dpi (rezolucija za PNG),

  • format se određuje prema ekstenziji (.png, .pdf, .svg).

“Sve u jednom” – komplet projekta

Za rad u nastavi često je korisno spremiti: edge list + node tablicu + graf + sliku.

# Edge list
write_csv(edges_eu, "data/edges_eu.csv")

# Graf objekt
saveRDS(g, "data/g_eu.rds")

# Slika
png("figures/g_eu.png", width = 1600, height = 1200, res = 200)
plot(g, layout = layout_with_fr, vertex.size = 5, vertex.label = NA, edge.arrow.size = 0.2)
dev.off()

Mini “workflow” checklist (što raditi svaki put)

  1. Definirajte čvorove i značenje veze (directed? weighted? vremenski okvir?).
  2. Odaberite instrument (NG ili roster) + pravila za unos.
  3. Učitajte podatke (CSV/XLSX/web/DB).
  4. Očistite ID-eve (trim/lowercase/standardizacija).
  5. Provjerite tipove (npr. weight numeric).
  6. Validirajte edge list (missing, duplikati, self-loops, izolirani čvorovi, simetrija).
  7. Pretvorite u potreban format (edge list / adjacency).
  8. Procijenite missing i napravi barem jednu analizu osjetljivosti.
  9. Primijenite etička pravila (minimizacija, pseudonimi, kontrola pristupa).

Debug mini vodič

U mrežnoj analizi greške najčešće dolaze iz četiri izvora:

  • pogrešni tipovi podataka,

  • nekonzistentni ID-evi,

  • neslaganje između edge i node tablice,

  • krivi objekt (tibble vs igraph vs tbl_graph).

U nastavku su tipični scenariji i rješenja.

parse_number() baca grešku: “is.character(x) is not TRUE”

Problem: stupac je već numerički, a parse_number() očekuje karakter.

str(df)

Rješenje

Ako je stupac već num, ne treba parse_number().

df <- df %>%
  mutate(export = as.numeric(export))

Ako je factor:

df <- df %>%
  mutate(export = parse_number(as.character(export)))

graph_from_data_frame() javlja:

“Some vertex names in d are not listed in vertices”

Problem: koristite argument vertices = nodes, ali neki čvorovi iz edge liste nisu u node tablici.

Provjera

setdiff(unique(edges$from), nodes$name)
setdiff(unique(edges$to), nodes$name)

Rješenje

  • ili ažurirati node tablicu,

  • ili izbaciti sporne retke,

  • ili ne koristiti vertices= argument.

Centrality funkcija “ne radi” u tidygraph-u

Primjer greške:

arrange(desc(indeg))

# must be size n or 1, not 700

Problem: pokušavate sortirati po varijabli koja ne postoji u tom kontekstu.

Rješenje

Prvo izračunati metriku:

tg <- as_tbl_graph(g) %>%
  activate(nodes) %>%
  mutate(indeg = centrality_degree(mode = "in"))

Tek onda:

tg %>%
  activate(nodes) %>%
  as_tibble() %>%
  arrange(desc(indeg))

“Zašto mi se broj čvorova povećao nakon čišćenja?”

Problem: ID-evi nisu standardizirani (razmaci, velika/mala slova, dijakritika).

edges %>%
  distinct(from)

Rješenje

Uvijek odmah standardizirati:

edges <- edges %>%
  mutate(across(c(from, to),
                ~ str_to_lower(str_squish(.x))))

Duplikati koji “ne izgledaju kao duplikati”

Problem: postoje skriveni znakovi (0A0, tabovi).

Rješenje

edges <- edges %>%
  mutate(across(c(from,to),
                ~ str_replace_all(.x, "\u00A0", " ") %>%
                  str_squish()))

join ne radi kako treba

Ako left_join() vrati puno NA:

anti_join(edges, nodes, by = c("from" = "name"))

Tipični uzroci:

  • numeric vs character ID

  • vodeće nule

  • različita kapitalizacija

Pravilo: ID-evi uvijek kao character.

edges <- edges %>%
  mutate(across(c(from,to), as.character))

Težine (weight) nisu numeričke

Ako summary(weight) ne radi:

str(edges$weight)

Ako je chr:

edges <- edges %>%
  mutate(weight = parse_number(weight))

simplify() daje čudne rezultate

simplify() spaja paralelne bridove.

Ako želite kontrolirati agregaciju:

g <- simplify(g, edge.attr.comb = list(weight = "sum"))

Bez edge.attr.comb, težine se mogu izgubiti.

Izolirani čvorovi “nestaju”

Ako edge lista nema eksplicitno sve čvorove, izolati se ne pojavljuju.

Rješenje (roster):

g <- graph_from_data_frame(edges,
                           vertices = data.frame(name = roster_ids))

Edge list → adjacency daje “NA” umjesto 0

Problem: pivot_wider() bez values_fill.

adj <- edges %>%
  mutate(value = 1) %>%
  pivot_wider(names_from = to,
              values_from = value,
              values_fill = 0)

Greška kod induced_subgraph()

Ako vids nije validan:

V(g)$name

Provjeriti postoji li čvor.

“Layout ne radi”

Često je uzrok:

  • graf ima samo jednu komponentu s 2 čvora

  • graf je prazan (ecount(g) == 0)

Provjera:

vcount(g)
ecount(g)
components(g)

Krivi tip objekta

Ako dobijete poruku tipa: no applicable method for ‘degree’

Provjerite:

class(obj)
  • degree()igraph

  • centrality_degree()tbl_graph

  • filter()tibble

Prevelika mreža “ruši” sesiju

Prije vizualizacije:

vcount(g)
ecount(g)

Ako je > 100k bridova, raditi filtriranje ili sampling.

Hard-coded indeksi (npr. [13:33, ])

Ovo je krhko jer se redoslijed može promijeniti.

Bolje:

idx <- which(edges$from == "UK")
edges[idx, "weight"] <- edges[idx, "weight"] / 1000

Greške kod baza podataka

Ako dbGetQuery() ne vraća ništa:

dbListTables(con)

Provjeriti naziv tablice.

Unix time izgleda “čudno”

Ako vidite velike brojeve tipa 1692312345:

as.POSIXct(time_unix,
           origin = "1970-01-01",
           tz = "UTC")

“Zašto mi je gustoća 0?”

Ako je graf usmjeren, gustoća se računa drugačije.

Provjera:

edge_density(g, loops = FALSE)

Opće debug pravilo

Kad zapnete, prvo provjerite:

str(obj)
class(obj)
head(obj)
  • Provjerite tipove stupaca

  • Provjerite unique ID-eve

  • Provjerite vcount / ecount

U 90% slučajeva problem je u tipu podataka ili ID-evima.




Memento

Jedinica analize: Što je “element” (čvor/veza/događaj) u analizi.

Granice mreže: Kriteriji uključivanja čvorova i veza.

Dizajn mrežnog upitnika: Struktura pitanja za prikupljanje veza.

Name generator: Pitanje koje potiče ispitanika da navede kontakte.

Name interpreter: Pitanja o atributima navedenih kontakata/veza.

Name roster: Popis mogućih kontakata koji se označavaju.

Snowball sampling: Uzorkovanje širenjem preko veza sudionika.

Potpuna mreža: Podaci o svim čvorovima/vezama unutar definiranih granica.

Ego-centrični podaci: Podaci prikupljeni oko pojedinih ego-aktera.

Podaci koji nedostaju (missing): Nezabilježeni čvorovi/veze/atributi.

Lažne veze: Veze koje ne odražavaju stvarnu relaciju (greške/mjerenje).

Simetričnost veze: Je li veza uzajamna ili jednosmjerna.

Kodiranje veza: Pravila pretvaranja odgovora u 0/1 ili težine.

Težine veza: Brojčani intenzitet (frekvencija, jačina, trajanje).

Čišćenje podataka: Uklanjanje grešaka, duplikata, nekonzistentnosti.

Normalizacija: Usklađivanje skala/formatiranja radi usporedbe.

Transformacija podataka: Preoblikovanje (npr. wide↔︎long, projekcije).

Long vs. wide: Dugi format (red=veza) vs široki (matrica).

Matrica incidencije: Matrica čvor–događaj/čvor–grupa (two-mode).

Pretvorba dvomodnih mreža: Projekcija two-mode u one-mode mrežu.

Etika i privatnost: Zaštita identiteta i osjetljivih odnosa.

Anonimizacija: Uklanjanje/skrivanje identifikatora uz očuvanje strukture.




Pitanja za ponavljanje

1. Koji su tipični izvori za Extract fazu u SNA ETL procesu? (odaberite sve točno)

  1. CSV/Excel datoteke s edge list podacima
  2. API izvori poput Eurostata ili World Bank
  3. Funkcije za izračun centralnosti u igraph-u
  4. Baze podataka poput SQLite/PostgreSQL

2. Koje su dobre prakse prilikom dohvaćanja podataka s weba za potrebe mrežne analize?

  1. Zabilježiti datum i URL dohvaćanja podataka
  2. Ignorirati robots.txt jer su podaci javni
  3. Koristiti html_table() kad tablice već postoje
  4. Uvijek scrapati cijelu stranicu bez ograničenja

3. Koje tvrdnje najbolje opisuju “boundary problem” u fazi prikupljanja mrežnih podataka?

  1. Dio relevantnih čvorova ili veza nije uključen u obuhvat
  2. Težine veza nisu numeričke pa ih treba parsirati
  3. Mreža je “odrezana” i struktura može biti pristrana
  4. Nedostaju paketi pa se graf ne može nacrtati

4. Što je realan rizik kod prikupljanja mrežnih podataka name generator pristupom?

  1. Varijante imena stvaraju lažne čvorove u mreži
  2. Roster uvijek daje nepotpune podatke o populaciji
  3. Self-loopovi se ne mogu pojaviti ni u kojem slučaju
  4. Težine veza su automatski standardizirane na istu skalu

5. Koje opcije su legitimni “alternativni pristupi” kad scraping nije dopušten uvjetima korištenja izvora?

  1. Ručno prikupljanje malog uzorka uz jasan protokol
  2. Traženje dozvole ili komercijalnog exporta podataka
  3. Automatizirano scrap-anje uz sporiji “rate limit”
  4. Pronalaženje otvorenog izvora s jasnom licencom/API-jem

6. Koji su tipični ciljevi transformacije ID-eva čvorova prije analize?

  1. Ukloniti višestruke razmake i nevidljive znakove
  2. Pretvoriti sve ID-eve u numeričke radi brzine
  3. Standardizirati velika/mala slova radi podudaranja
  4. Ukloniti dijakritiku samo ako je to dio pravila projekta

7. Što je najčešći razlog za korištenje janitor::clean_names() nakon učitavanja tablice?

  1. Standardizacija naziva stupaca radi stabilnijeg koda
  2. Automatsko dodavanje težina bridovima u grafu
  3. Pretvaranje svih vrijednosti u numerički tip podataka
  4. Uklanjanje svih nedostajućih vrijednosti iz tablice

8. Koje transformacije su tipične kad radimo s “neurednim” numeričkim vrijednostima iz web tablica?

  1. Korištenje parse_number() za pretvorbu teksta u broj
  2. Zamjena separatora tisućica i decimalnih oznaka po potrebi
  3. Pretvorba svega u faktor radi konzistentnosti prikaza
  4. Uklanjanje jedinica mjere prije pretvorbe u numerički tip

9. Što je “multi-edge” u edge listi i koje su realne strategije obrade?

  1. Više redaka s istim (from,to) parom u podacima
  2. Agregiranje težina po paru (sum/mean/max) uz obrazloženje
  3. Zamjena svih duplikata jednom vrijednosti bez provjere
  4. Zadržavanje “event” zapisa ako analiziramo temporalnu mrežu

10. Koje tvrdnje vrijede za self-loopove (from == to) u mrežnim podacima?

  1. Mogu biti smisleni ili besmisleni, ovisno o definiciji veze
  2. Uvijek ih treba zadržati jer čuvaju informaciju o aktivnosti
  3. Često nastaju greškom unosa ili specifičnim log zapisima
  4. Tipično ih filtriramo ako nemaju interpretativni smisao

11. Koja je ispravna interpretacija “missing” u mrežnim podacima u odnosu na tablične podatke?

  1. Missing može promijeniti strukturu mreže i centralnosti
  2. Missing uvijek znači da je vrijednost u ćeliji prazna
  3. Missing može biti skriven kao “nevidljive” veze ili čvorovi
  4. Unit nonresponse može ukloniti cijeli čvor kao izvor veza

12. Koje su dobre prakse kod pretvorbe tipova podataka prije joinova i izgradnje grafa?

  1. Držati from i to kao character radi stabilnih ID-eva
  2. Pretvoriti “0012” u 12 radi lakšeg sortiranja
  3. Standardizirati whitespace i encoding prije spajanja tablica
  4. Provjeriti anti_join() da vidimo čvorove bez atributa

13. Koje radnje spadaju u “normalizaciju skala/jedinica” kod spajanja više izvora?

  1. Pretvorba GBP u EUR pomoću jasno navedenog tečaja i godine
  2. Pretvorba milijuna u milijarde radi usporedive interpretacije
  3. Miješanje valuta u istom stupcu bez dodatne dokumentacije
  4. Dodavanje varijable “unit” ili bilješke o izvoru/valuti

14. Koji su znakovi da edge lista nije konzistentna i treba dodatnu transformaciju?

  1. Postoje prazni stringovi u from ili to stupcima
  2. Postoje duplikati parova (from,to) bez jasnog pravila agregacije
  3. Svi čvorovi imaju barem jednu vezu pa je sve sigurno ispravno
  4. Težine su tipa <chr> i sadrže tekst i simbole

15. Koje tvrdnje vrijede za izolirane čvorove (isolates) i njihovu obradu?

  1. Izolati su čvorovi bez ijedne veze (degree = 0)
  2. Izolati se uvijek automatski pojavljuju u edge list tablici
  3. U roster pristupu izolati postaju vidljivi kad dodamo sve čvorove
  4. Uklanjanje izolata je dopušteno, ali odluka mora biti dokumentirana

16. Što u ETL kontekstu znači “Load” za mrežnu analizu u R-u?

  1. Pretvoriti edge list u igraph ili tbl_graph objekt
  2. Izračunati layout i “uljepšati” vizualizaciju do kraja
  3. Spremiti očišćene podatke u reproducibilnom formatu
  4. Pripremiti strukturu za analizu metrika i/ili modeliranje

17. Koje opcije su tipični “outputi” koje ima smisla pohraniti nakon pripreme mrežnih podataka?

  1. Edge list i node tablica u CSV ili RDS formatu
  2. Samo screenshot grafa, bez podataka i metapodataka
  3. Graf objekt u RDS ili GraphML formatu (ovisno o potrebi)
  4. Dokumentacija o izvoru, datumu i pravilima transformacije

18. Koje tvrdnje su točne za razliku između igraph i tidygraph::tbl_graph objekata?

  1. tbl_graph omogućuje activate(nodes) i dplyr-stil transformacije
  2. igraph se ne može pretvoriti u tablični prikaz bridova i čvorova
  3. Oba formata mogu sadržavati atribute čvorova i bridova
  4. tbl_graph je obično praktičan kad koristimo ggraph

19. Koje su dobre prakse kod spremanja “finalnih” podataka prije analize?

  1. Spremiti i “raw” i “clean” verziju (uz jasno imenovanje)
  2. Prepisati originalne sirove podatke jer “više ne trebaju”
  3. Osigurati da se učitavanjem dobije isti tip podataka (RDS prednost)
  4. Dokumentirati pretpostavke (npr. agregiranje, filtriranje, tečaj)

20. Koje tvrdnje su točne za provjeru uspješnog učitavanja u graf objekt?

  1. vcount() i ecount() su brze kontrole veličine grafa
  2. Ako plot() radi, onda su podaci sigurno metodološki ispravni
  3. as_data_frame(g, what="edges") pomaže provjeriti atribute bridova
  4. degree() ili osnovne metrike mogu otkriti ekstremne anomalije



Korištena literatura

Rawlings, C. M., Smith, J. A., McFarland, D. A., & Moody, J. (2023). Network analysis: integrating social network theory, method, and application with R (Vol. 52). Cambridge University Press.

Wickham, H., François, R., Henry, L., Müller, K., & Vaughan, D. (2026). dplyr. A Grammar of Data Manipulation. Available from, CRAN. ([CRAN][1])

Wickham, H., Vaughan, D., & Girlich, M. (2026). tidyr. Tidy Messy Data. Available from, CRAN. ([CRAN][2])

Wickham, H. (2026). stringr. Simple, Consistent Wrappers for Common String Operations. Available from, CRAN. ([CRAN][3])

Wickham, H., Hester, J., & Bryan, J. (2026). readr. Read Rectangular Text Data. Available from, CRAN. ([CRAN][4])

Wickham, H., & Bryan, J. (2026). readxl. Read Excel Files. Available from, CRAN. ([CRAN][5])

Firke, S. (2026). janitor. Simple Tools for Examining and Cleaning Dirty Data. Available from, CRAN. ([CRAN][6])

Henry, L., & Wickham, H. (2026). purrr. Functional Programming Tools. Available from, CRAN. ([CRAN][7])

Müller, K., & Wickham, H. (2026). tibble. Simple Data Frames. Available from, CRAN. ([CRAN][8])

Grolemund, G., & Wickham, H. (2026). lubridate. Make Dealing with Dates a Little Easier. Available from, CRAN. ([CRAN][9])

Csardi, G., Nepusz, T., Traag, V., Horvát, S., Zanini, F., Noom, D., Müller, K., & others (2026). igraph. Network Analysis and Visualization in R. Available from, CRAN. ([CRAN][7])

Pedersen, T. L. (2026). tidygraph. A Tidy API for Graph Manipulation. Available from, CRAN. ([CRAN][8])

Pedersen, T. L. (2026). ggraph. An Implementation of Grammar of Graphics for Graphs and Networks. Available from, CRAN. ([CRAN][9])

Wickham, H. (2025). rvest. Easily Harvest (Scrape) Web Pages. Available from, CRAN. ([CRAN][10])

Wickham, H., & James, D. A. (2025). DBI. R Database Interface. Available from, CRAN. ([CRAN][11])

Müller, K., Wickham, H., James, D. A., Falcon, S., & Hipp, D. R. (2026). RSQLite. SQLite Interface for R. Available from, CRAN. ([CRAN][12])

Bates, D., & Maechler, M. (2025). Matrix. Sparse and Dense Matrix Classes and Methods. Available from, CRAN. ([CRAN][13])

Eddelbuettel, D., & Lucas, A. (2025). digest. Create Compact Hash Digests of R Objects. Available from, CRAN. ([CRAN][14])

Lahti, L., Huovari, J., Kainu, M., Biecek, P., & contributors (2023). eurostat. Tools for Eurostat Open Data. Available from, CRAN. ([CRAN][15])

Iannone, R. (2024). DiagrammeR. Graph/Network Visualization. Available from, CRAN. ([CRAN][16])

Ooms, J., & McNamara, J. (2025). writexl. Export Data Frames to Excel ‘xlsx’ Format. Available from, CRAN. ([CRAN][17])

Ključ odgovora

  • 1: A, B, D
  • 2: A, C
  • 3: A, C
  • 4: A
  • 5: A, B, D
  • 6: A, C, D
  • 7: A
  • 8: A, B, D
  • 9: A, B, D
  • 10: A, C, D
  • 11: A, C, D
  • 12: A, C, D
  • 13: A, B, D
  • 14: A, B, D
  • 15: A, C, D
  • 16: A, C, D
  • 17: A, C, D
  • 18: A, C, D
  • 19: A, C, D
  • 20: A, C, D



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     
## 
## other attached packages:
##  [1] DiagrammeR_1.0.11 eurostat_4.0.0    digest_0.6.39     Matrix_1.7-1     
##  [5] RSQLite_2.4.6     DBI_1.2.3         rvest_1.0.5       ggraph_2.2.2     
##  [9] ggplot2_4.0.2     tidygraph_1.3.1   igraph_2.2.1      lubridate_1.9.5  
## [13] tibble_3.3.1      purrr_1.2.1       janitor_2.2.1     readxl_1.4.5     
## [17] readr_2.1.6       stringr_1.6.0     tidyr_1.3.2       dplyr_1.2.0      
## 
## loaded via a namespace (and not attached):
##  [1] tidyselect_1.2.1    viridisLite_0.4.3   farver_2.1.2       
##  [4] blob_1.3.0          viridis_0.6.5       S7_0.2.1           
##  [7] fastmap_1.2.0       tweenr_2.0.3        timechange_0.4.0   
## [10] lifecycle_1.0.5     magrittr_2.0.4      compiler_4.4.2     
## [13] rlang_1.1.7         sass_0.4.10         tools_4.4.2        
## [16] utf8_1.2.6          yaml_2.3.12         data.table_1.18.2.1
## [19] knitr_1.51          labeling_0.4.3      htmlwidgets_1.6.4  
## [22] graphlayouts_1.2.2  bit_4.6.0           classInt_0.4-11    
## [25] curl_7.0.0          here_1.0.2          plyr_1.8.9         
## [28] xml2_1.5.2          RColorBrewer_1.1-3  KernSmooth_2.23-24 
## [31] withr_3.0.2         grid_4.4.2          polyclip_1.10-7    
## [34] ISOweek_0.6-2       e1071_1.7-17        scales_1.4.0       
## [37] MASS_7.3-61         cli_3.6.5           crayon_1.5.3       
## [40] rmarkdown_2.30      generics_0.1.4      rstudioapi_0.18.0  
## [43] httr_1.4.7          tzdb_0.5.0          visNetwork_2.1.4   
## [46] cachem_1.1.0        ggforce_0.5.0       proxy_0.4-29       
## [49] regions_0.1.8       parallel_4.4.2      assertthat_0.2.1   
## [52] selectr_0.5-1       cellranger_1.1.0    vctrs_0.7.1        
## [55] jsonlite_2.0.0      hms_1.1.4           bit64_4.6.0-1      
## [58] ggrepel_0.9.6       jquerylib_0.1.4     bibtex_0.5.2       
## [61] glue_1.8.0          RefManageR_1.4.0    stringi_1.8.7      
## [64] countrycode_1.6.1   gtable_0.3.6        pillar_1.11.1      
## [67] rappdirs_0.3.4      htmltools_0.5.9     httr2_1.2.2        
## [70] R6_2.6.1            rprojroot_2.1.1     vroom_1.7.0        
## [73] evaluate_1.0.5      lattice_0.22-6      backports_1.5.0    
## [76] memoise_2.0.1       snakecase_0.11.1    renv_1.1.1         
## [79] bslib_0.10.0        class_7.3-22        Rcpp_1.1.1         
## [82] gridExtra_2.3       xfun_0.56           pkgconfig_2.0.3