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
U mrežnoj analizi kvaliteta rezultata izravno ovisi o kvaliteti podataka:
Ključno pitanje: što veza znači i kako se mjeri?
Primjeri:
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
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:
Praktični savjet (za konzistentnost):
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 daje ispitaniku listu svih mogućih čvorova (npr. svi studenti u grupi) i pita za veze prema svakom.
Primjer roster pitanja (binary):
Primjer roster pitanja (weighted):
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
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.
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).
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:
Korak D: Definirati pravila čišćenja
Korak E: Kontrola kvalitete ručnog unosa
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
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.
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.
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.
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.
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.
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.
Edge list je tablica s barem dva stupca:
from = izvorto = odredišteDodatno:
weight (jačina), time (datum),
type (kanal), itd.Primjer:
| from | to | weight |
|---|---|---|
| Ana | Marko | 3 |
| Ana | Iva | 1 |
Kvadratna matrica dimenzije N x N gdje je
A[i,j] veza od i prema j.
| Ana | Marko | Iva | |
|---|---|---|---|
| Ana | 0 | 1 | 1 |
| Marko | 1 | 0 | 0 |
| Iva | 1 | 0 | 0 |
| Ana | Marko | Iva | |
|---|---|---|---|
| Ana | 0 | 3 | 1 |
| Marko | 3 | 0 | 0 |
| Iva | 1 | 0 | 0 |
U ovom kolegiju najčešće koristimo:
data.frame
df <- data.frame(
from = c("Ana","Ana","Marko"),
to = c("Iva","Marko","Iva"),
weight = c(3,1,2)
)
class(df)
tibble
library(tibble)
tb <- tibble(
from = c("Ana","Ana","Marko"),
to = c("Iva","Marko","Iva"),
weight = c(3,1,2)
)
class(tb)
matrix
library(Matrix)
A_sparse <- Matrix(adj, sparse = TRUE)
class(A_sparse)
A_sparse
igraph objekt
igraph je specijalizirani objekt za grafovelibrary(igraph)
g <- graph_from_data_frame(tb, directed = TRUE)
class(g)
tbl_graph (tidygraph)
tbl_graph je “tidy” verzija grafadplyr sintakse
(activate(nodes), mutate(), itd.)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.
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)
Pretpostavka: imate datoteku edges.csv sa stupcima
from, to, weight.
edges_csv <- read_csv("data_edges.csv")
Minimalna provjera:
glimpse(edges_csv)
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:
Preporučena praksa:
edges <- read_csv("data_edges.csv") %>%
clean_names()
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()?
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.
Primjer: s jedne stranice uzimamo sve linkove i radimo edge list
from = trenutna_stranica, to = link.
library(rvest)
library(dplyr)
library(stringr)
library(igraph)
start_url <- "https://www.unipu.hr"
page <- read_html(start_url)
links_raw <- page %>%
html_elements("a") %>%
html_attr("href") %>%
na.omit() %>%
as.character()
# zadržimo samo interne linkove
links_clean <- links_raw %>%
str_trim() %>%
# makni mailto, javascript, anchor
discard(~ str_detect(.x, "^mailto:|^javascript:|^#")) %>%
# dodaj bazni URL ako je relativan
map_chr(~ ifelse(str_detect(.x, "^http"),
.x,
paste0(start_url, .x))) %>%
# zadrži samo unipu domenu
keep(~ str_detect(.x, "unipu.hr")) %>%
unique()
length(links_clean)
## [1] 172
edges_links <- data.frame(
from = start_url,
to = links_clean,
stringsAsFactors = FALSE
)
head(edges_links)
## from to
## 1 https://www.unipu.hr https://www.unipu.hr/obrazovanje
## 2 https://www.unipu.hr https://www.unipu.hr/znanost_i_istrazivanja?redirect=1
## 3 https://www.unipu.hr https://www.unipu.hr/dogadjanja
## 4 https://www.unipu.hr https://www.unipu.hr/novosti
## 5 https://www.unipu.hr https://www.unipu.hr/o_sveucilistu/sveucilisna_tijela
## 6 https://www.unipu.hr https://www.unipu.hr/kontakti
g <- graph_from_data_frame(edges_links, directed = TRUE)
plot(g,
vertex.size = 6,
vertex.label.cex = 0.2,
edge.arrow.size = 0.3)
Pokušajmo sad malo dublje. Uzmimo prvih nekoliko linkova; za svaki uzmemo njihove linkove, tj. napravimo mali crawl dubine 2.
library(purrr)
# ograničimo se na prvih 6 internih stranica
level1 <- links_clean[1:6]
crawl_edges <- map_df(level1, function(u) {
tryCatch({
p <- read_html(u)
sublinks <- p %>%
html_elements("a") %>%
html_attr("href") %>%
na.omit() %>%
as.character() %>%
discard(~ str_detect(.x, "^mailto:|^javascript:|^#")) %>%
map_chr(~ ifelse(str_detect(.x, "^http"),
.x,
paste0(start_url, .x))) %>%
keep(~ str_detect(.x, "unipu.hr")) %>%
unique()
data.frame(
from = u,
to = sublinks,
stringsAsFactors = FALSE
)
}, error = function(e) NULL)
})
edges_full <- bind_rows(edges_links, crawl_edges) %>%
distinct()
nrow(edges_full)
## [1] 2379
g2 <- graph_from_data_frame(edges_full, directed = TRUE)
plot(g2,
vertex.size = 4,
vertex.label = NA,
edge.arrow.size = 0.2)
Ovaj postupak možemo ponavljati više puta. U nekom trenutku, dođemo do stranica koje vode na vanjske adrese, koje djeluju kao sink čvorovi.
Napomena o etici i pravilima (bitno): scraping treba raditi samo gdje je dopušteno (uvjeti korištenja, robots.txt, bez opterećivanja servera, bez osobnih podataka).
Ovdje kreiramo lokalnu SQLite bazu (jer radi svima).
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
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)
PostgreSQL sample database – Pagila je open-source demo baza (filmska mreža, glumci, filmovi, suradnje).
Sadrži:
Možemo napraviti mrežu:
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)
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")
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")
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)
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)
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)
(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)
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)
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
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
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
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
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]
Stanford Large Network
Dataset Collection (SNAP)
Sadrži: socijalne mreže, komunikacijske mreže, mreže
citata, suradnje, web grafove, prometne mreže, web mreže, transakcije
kripto valuta.
KONECT — Koblenz Network
Collection
Sadrži: usmjerene/neusmjerene, težinske, bipartitne,
signed mreže; mnogo domena.
Network
Repository
Sadrži: tisuće mreža + brzi pregled statistika i
preuzimanje.
# 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()
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
)
U ovom poglavlju učimo što tipično pođe po zlu i kako to sustavno popraviti.
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")
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)
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")
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)
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", " "))))
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!)
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, "")))
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)
Problem: ID “0012” postane 12, join-ovi pucaju.
Rješenje: ID uvijek kao character.
edges <- edges %>% mutate(across(c(from,to), as.character))
Problem: Unix time vs datum string; timezone; agregacije po danu/tjednu.
Rješenje: standardizacija u POSIXct + derivacije (date/week).
Problem: više tipova odnosa (email, chat, suradnja) u jednoj tablici.
Rješenje: type stupac + filtriranje ili multilayer pristup.
Problem: A kaže “da”, B kaže “ne”; ili “ne znam”.
Rješenje: pravilo postizanja simetrije (OR/AND/mean) + zaseban kod za “unknown”.
Problem: svaki izvor podataka ima drugačije nazive.
Rješenje: odmah nakon učitavanja clean_names() +
rename() na standard.
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"))
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).
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
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
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.
U mrežama missing nije samo prazna ćelija u tablici, nego mijenja strukturu mreže:
Vrste (praktična interpretacija)
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
Problem: čak i nakon “čišćenja”, kombinacija atributa može re-identificirati osobu.
Rješenje: agregiranje, uklanjanje nepotrebnih atributa, hash + salt, kontrola pristupa.
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).
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:
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)
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
# 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
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
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)
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)
Cilj validacije: prije nego što krenemo u metrike, želimo znati da su podaci smisleni.
Tipične greške
from ili toMinimalni 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
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).
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.
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")
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")
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 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()
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).
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()
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.
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.
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])
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