Näytä koodi
library (tidyverse)
library (here)
library (lubridate)
library (scales)
library (ggtext)
library (igraph)
library (tidygraph)
library (ggraph)
library (patchwork)
#library(eurlex)
col_red <- "#e63946"
col_green <- "#2a9d8f"
col_orange <- "#f4a261"
col_navy <- "#1d3557"
col_blue <- "#457b9d"
col_purple <- "#6d597a"
theme_set (
theme_minimal (base_size = 14 ) +
theme (
plot.title = element_markdown (face = "bold" , size = 15 ),
plot.subtitle = element_markdown (color = "grey40" ),
plot.caption = element_text (color = "grey55" , size = 9 ),
axis.title = element_text (color = "grey30" ),
panel.grid.minor = element_blank ()
)
)
Verkosto on jakauma — keskeisyyskin on jakauma
EU-oikeuden julkisessa keskustelussa puhutaan yksittäisistä säädöksistä. GDPR. NIS2. AI Act. Jokainen niistä esitetään itsenäisenä kokonaisuutena — ikään kuin se olisi syntynyt tyhjiöön.
Todellisuudessa EU-lainsäädäntö on verkosto . Säädökset viittaavat toisiinsa, muuttavat toisiaan, nojaavat toistensa määritelmiin. Jotkut säädökset ovat verkoston solmupisteitä (hubs ): jos ne kumotaan tai muuttuvat, kymmenien muiden säädösten tulkinta muuttuu samalla.
Tämä on “maailma on jakauma” -teeman ydin: keskeisyyskään ei ole pistemäinen ominaisuus . Se on jakauma. Useimmat säädökset ovat verkoston reunoilla — harva on solmupisteessä. Jakauman häntä on se, mikä kiinnostaa.
Datan haku: include_citations
Aiemmissa sarjan osissa havaitsimme että include_lbs = TRUE hakee oikeusperustan (Treaty-artiklaviittaukset UUID-muodossa) — ei säädösviittauksia. Verkostoanalyysiin tarvitaan include_citations = TRUE, joka palauttaa suorat viittaukset toisiin säädöksiin CELEX-muodossa.
Näytä koodi
cit_path <- here ("data/eu/eu_citations_raw.rds" )
base_path <- here ("data/eu/eu_saadanto_raw.rds" )
if (! file.exists (base_path)) {
stop ("Aja ensin osa 1: " , base_path, " puuttuu." )
}
raw_base <- readRDS (base_path)
message ("Perustilastot ladattu: " , nrow (raw_base), " riviä" )
if (! file.exists (cit_path)) {
message ("Haetaan viittausdata EUR-Lexistä (include_citations = TRUE)..." )
message ("Huomio: haku voi kestää useita minuutteja." )
tyypit <- c ("regulation" , "directive" , "decision" )
hae_viittaukset <- function (tyyppi) {
message (" → " , tyyppi)
tulos <- elx_make_query (
resource_type = tyyppi,
include_date = TRUE ,
include_celex = TRUE ,
include_citations = TRUE
) |> elx_run_query () |>
mutate (resource_type = tyyppi)
cat (tyyppi, ":" , nrow (tulos), "riviä —" ,
if (nrow (tulos) == 1e6 ) "*** RAJA OSUI *** \n " else "ok \n " )
tulos
}
raw_cit <- map (tyypit, hae_viittaukset) |> bind_rows ()
dir.create (dirname (cit_path), showWarnings = FALSE , recursive = TRUE )
saveRDS (raw_cit, cit_path)
message ("Tallennettu: " , nrow (raw_cit), " riviä → " , cit_path)
} else {
raw_cit <- readRDS (cit_path)
message ("Ladattu tiedostosta: " , nrow (raw_cit), " riviä" )
}
# Tarkistetaan sarakkeet
cat ("Sarakkeet:" , paste (names (raw_cit), collapse = ", " ), " \n " )
Sarakkeet: work, type, celex, date, citationcelex, resource_type
Näytä koodi
cat ("Rivejä yhteensä:" , nrow (raw_cit), " \n\n " )
Näytä koodi
# Katsotaan miltä citations-data näyttää
cat ("Esimerkki viittausdatasta: \n " )
Esimerkki viittausdatasta:
Näytä koodi
raw_cit |>
filter (! is.na (citationcelex)) |>
select (celex, citationcelex) |>
slice_head (n = 5 ) |>
print ()
celex citationcelex
1 31994R3141 31993R3376
2 31968Q0313 11965F020
3 31973R0715 31972R0805
4 31964Q0356 31968Q0313
5 31971Q0068 31968Q0313
Näytä koodi
# Poimitaan CELEX-numero citations-sarakkeesta
# Rakenne voi olla URI-muoto: http://...celex/32024R0903
# tai suoraan CELEX: 32024R0903
# str_extract tunnistaa molemmat
reunat_raw <- raw_cit |>
filter (! is.na (celex), ! is.na (citationcelex)) |>
mutate (
lahde = celex,
# Ensin yritetään poimia URI:n lopusta
kohde = coalesce (
str_extract (citationcelex, "(?<=celex/)[A-Z0-9]+" ),
# Jos ei URI-muoto, yritetään suoraa CELEX-muotoa
str_extract (citationcelex, "^[0-9]{5}[A-Z][0-9]+$" )
)
) |>
filter (
! is.na (kohde),
lahde != kohde, # ei itseen viittaavia silmukoita
nchar (kohde) >= 5 , # poistetaan liian lyhyet
nchar (kohde) <= 20 # poistetaan liian pitkät
) |>
select (lahde, kohde) |>
distinct ()
cat ("Viittauspareja (reunoja):" , nrow (reunat_raw), " \n " )
Viittauspareja (reunoja): 190554
Näytä koodi
cat ("Uniikkeja lähdesäädöksiä:" , n_distinct (reunat_raw$ lahde), " \n " )
Uniikkeja lähdesäädöksiä: 72836
Näytä koodi
cat ("Uniikkeja kohdesäädöksiä:" , n_distinct (reunat_raw$ kohde), " \n\n " )
Uniikkeja kohdesäädöksiä: 31263
Näytä koodi
# Tarkistetaan että kohteet näyttävät oikeilta CELEX-numeroilta
cat ("Esimerkkejä kohde-CELEX:eistä: \n " )
Esimerkkejä kohde-CELEX:eistä:
Näytä koodi
head (reunat_raw$ kohde, 10 )
[1] "31993R3376" "11965F020" "31972R0805" "31968Q0313" "31968Q0313"
[6] "31968Q0313" "31968Q0313" "31972R2484" "32004R2276" "32004R2276"
Näytä koodi
# Viittausten jakauma per säädös
cat ("Viittauksia per lähdesäädös (lähtevä aste): \n " )
Viittauksia per lähdesäädös (lähtevä aste):
Näytä koodi
reunat_raw |>
count (lahde, name = "viittauksia" ) |>
pull (viittauksia) |>
summary () |>
print ()
Min. 1st Qu. Median Mean 3rd Qu. Max.
1.000 1.000 2.000 2.616 3.000 99.000
Näytä koodi
cat (" \n Viittauksia per kohdesäädös (saapuva aste): \n " )
Viittauksia per kohdesäädös (saapuva aste):
Näytä koodi
reunat_raw |>
count (kohde, name = "viittauksia" ) |>
pull (viittauksia) |>
summary () |>
print ()
Min. 1st Qu. Median Mean 3rd Qu. Max.
1.000 1.000 1.000 6.095 3.000 919.000
Verkostorakenne
Näytä koodi
# Solmumeta perustilastoista
solmut_meta <- raw_base |>
filter (! is.na (celex), ! is.na (date)) |>
mutate (
date = as.Date (date),
vuosi = year (date),
saadostyyppi = case_when (
resource_type == "regulation" ~ "Asetus" ,
resource_type == "directive" ~ "Direktiivi" ,
resource_type == "decision" ~ "Päätös" ,
resource_type == "recommendation" ~ "Suositus" ,
TRUE ~ "Muu"
)
) |>
select (celex, saadostyyppi, vuosi) |>
distinct (celex, .keep_all = TRUE )
# Kaikki uniikkit solmut
solmut_celex <- union (reunat_raw$ lahde, reunat_raw$ kohde)
# Rakennetaan igraph suoraan — luotettavampi kuin tidygraph-muunnos
g_igraph <- graph_from_data_frame (
d = reunat_raw |> rename (from = lahde, to = kohde),
directed = TRUE ,
vertices = tibble (name = solmut_celex) |>
left_join (solmut_meta, by = c ("name" = "celex" ))
)
cat ("Verkosto rakennettu: \n " )
Näytä koodi
cat (" Solmuja:" , vcount (g_igraph), " \n " )
Näytä koodi
cat (" Reunoja:" , ecount (g_igraph), " \n " )
Näytä koodi
cat (" Tiheys:" , round (edge_density (g_igraph), 6 ), " \n " )
Näytä koodi
cat (" Solmuattribuutit:" , paste (vertex_attr_names (g_igraph), collapse = ", " ), " \n " )
Solmuattribuutit: name, saadostyyppi, vuosi
Näytä koodi
cat ("Lasketaan keskeisyysmitat... \n " )
Lasketaan keskeisyysmitat...
Näytä koodi
indegree <- degree (g_igraph, mode = "in" )
outdegree <- degree (g_igraph, mode = "out" )
pagerank <- page_rank (g_igraph, directed = TRUE )$ vector
betweenness <- betweenness (g_igraph, directed = TRUE , normalized = TRUE )
# Kootaan — V(g_igraph)$name on nyt luotettava koska
# rakensimme igraphin graph_from_data_frame:lla
solmut_df <- tibble (
celex = V (g_igraph)$ name,
indegree = indegree,
outdegree = outdegree,
pagerank = pagerank,
betweenness = betweenness
) |>
left_join (solmut_meta, by = "celex" ) |>
arrange (desc (indegree))
cat ("Top 10 saapuvan asteen mukaan: \n " )
Top 10 saapuvan asteen mukaan:
Näytä koodi
solmut_df |>
slice_head (n = 10 ) |>
select (celex, saadostyyppi, vuosi, indegree, pagerank) |>
mutate (pagerank = round (pagerank, 5 )) |>
print ()
# A tibble: 10 × 5
celex saadostyyppi vuosi indegree pagerank
<chr> <chr> <dbl> <dbl> <dbl>
1 31992R2913 Asetus 1992 919 0.00355
2 31993R1068 Asetus 1993 879 0.00178
3 31999D0468 Päätös 1999 872 0.00467
4 31995R1501 Asetus 1995 808 0.00245
5 31992R3813 Asetus 1992 777 0.00115
6 32011R0182 Asetus 2011 761 0.00135
7 32018R1725 Asetus 2018 751 0.00102
8 31975R0584 Asetus 1975 728 0.00182
9 31987R2200 Asetus 1987 725 0.00153
10 31987R1420 Asetus 1987 692 0.00139
Analyysi 1: Keskeisyysjakaumat — potenssilaki?
Näytä koodi
indegree_dist <- solmut_df |>
count (indegree, name = "n_acts" ) |>
filter (indegree > 0 )
p1 <- ggplot (indegree_dist, aes (indegree, n_acts)) +
geom_point (color = col_navy, alpha = 0.5 , size = 1.5 ) +
geom_smooth (method = "lm" , se = TRUE ,
color = col_red, fill = col_red, alpha = 0.15 ) +
scale_x_log10 (labels = comma_format ()) +
scale_y_log10 (labels = comma_format ()) +
labs (
title = "**Saapuvan asteen jakauma** (log-log)" ,
subtitle = "Suora viiva = potenssilaki. Harvoilla säädöksillä äärimmäisen paljon viittauksia." ,
x = "Saapuva aste (log)" , y = "Lukumäärä (log)"
)
p2 <- solmut_df |>
filter (pagerank > 0 ) |>
ggplot (aes (pagerank)) +
geom_histogram (aes (y = after_stat (density)),
bins = 60 , fill = col_blue,
alpha = 0.7 , color = "white" ) +
geom_density (color = col_red, linewidth = 1.0 ) +
scale_x_log10 (labels = scientific_format ()) +
labs (
title = "**PageRank-jakauma** (log-asteikko)" ,
subtitle = "Äärimmäisen vino oikealle — solmupisteet erottuvat selvästi massasta" ,
x = "PageRank (log)" , y = "Tiheys" ,
caption = "Lähde: EUR-Lex SPARQL via eurlex (R). Kristian Vepsäläinen / kristianvepsalainen.com"
)
p1 / p2
Näytä koodi
solmut_df |>
filter (! is.na (saadostyyppi), saadostyyppi != "Muu" , pagerank > 0 ) |>
ggplot (aes (pagerank, fill = saadostyyppi, color = saadostyyppi)) +
geom_density (alpha = 0.35 , linewidth = 0.8 ) +
scale_fill_manual (values = c (col_red, col_green, col_blue, col_orange)) +
scale_color_manual (values = c (col_red, col_green, col_blue, col_orange)) +
scale_x_log10 (labels = scientific_format ()) +
labs (
title = "**PageRank-jakauma säädöstyypeittäin**" ,
subtitle = "Oikealla häntä = solmupisteet. Mikä tyyppi tuottaa eniten solmupisteitä?" ,
x = "PageRank (log)" , y = "Tiheys" ,
fill = NULL , color = NULL ,
caption = "Lähde: EUR-Lex SPARQL via eurlex (R). Kristian Vepsäläinen / kristianvepsalainen.com"
) +
theme (legend.position = "top" )
Analyysi 2: Solmupisteet — EU-oikeuden peruskivet
Näytä koodi
top20 <- solmut_df |>
arrange (desc (pagerank)) |>
slice_head (n = 20 ) |>
mutate (pagerank_norm = pagerank / max (pagerank))
top20 |>
mutate (celex = fct_reorder (celex, pagerank)) |>
ggplot (aes (pagerank_norm, celex, fill = saadostyyppi)) +
geom_col (show.legend = TRUE , width = 0.75 ) +
geom_text (
aes (label = paste0 (
if_else (! is.na (vuosi), as.character (vuosi), "?" ),
" · " ,
if_else (! is.na (saadostyyppi), saadostyyppi, "?" )
)),
hjust = - 0.05 , size = 3.0
) +
scale_fill_manual (values = c (col_red, col_green, col_blue,
col_orange, col_navy),
na.value = "grey60" ) +
scale_x_continuous (expand = expansion (mult = c (0 , 0.35 )),
labels = percent_format ()) +
labs (
title = "**EU-oikeuden solmupisteet** — top 20 PageRankin mukaan" ,
subtitle = "100 % = suurin PageRank verkostossa. Nämä säädökset ovat muiden perusta." ,
x = "Suhteellinen PageRank" , y = NULL , fill = NULL ,
caption = "Lähde: EUR-Lex SPARQL via eurlex (R). Kristian Vepsäläinen / kristianvepsalainen.com"
) +
theme (legend.position = "top" )
Analyysi 3: Verkostovisualisointi — ytimen rakenne
Näytä koodi
kynnys_95 <- quantile (solmut_df$ indegree, 0.95 , na.rm = TRUE )
cat ("95. persentiili indegree:" , kynnys_95, " \n " )
95. persentiili indegree: 7
Näytä koodi
ydin_celex <- solmut_df |>
filter (indegree >= kynnys_95) |>
pull (celex)
cat ("Ydinsolmuja:" , length (ydin_celex), " \n " )
Näytä koodi
g_ydin <- induced_subgraph (
g_igraph,
vids = which (V (g_igraph)$ name %in% ydin_celex)
)
cat ("Ydinverkoston solmuja:" , vcount (g_ydin), " \n " )
Ydinverkoston solmuja: 4375
Näytä koodi
cat ("Ydinverkoston reunoja:" , ecount (g_ydin), " \n " )
Ydinverkoston reunoja: 19439
Näytä koodi
pagerank_ydin <- page_rank (g_ydin, directed = TRUE )$ vector
g_ydin_tidy <- as_tbl_graph (g_ydin) |>
mutate (
pagerank = pagerank_ydin,
etiketti = if_else (
pagerank >= quantile (pagerank_ydin, 0.90 , na.rm = TRUE ),
name,
NA_character_
)
)
set.seed (42 )
ggraph (g_ydin_tidy, layout = "fr" ) +
geom_edge_arc (
aes (alpha = after_stat (index)),
color = "grey70" ,
linewidth = 0.3 ,
show.legend = FALSE
) +
geom_node_point (
aes (size = pagerank,
color = saadostyyppi),
alpha = 0.85
) +
geom_node_text (
aes (label = etiketti),
size = 2.8 ,
repel = TRUE ,
max.overlaps = 20 ,
color = col_navy,
na.rm = TRUE
) +
scale_color_manual (
values = c (col_red, col_green, col_blue, col_orange, col_navy),
na.value = "grey60"
) +
scale_size_continuous (range = c (1.5 , 8 ), guide = "none" ) +
scale_edge_alpha (range = c (0.05 , 0.3 ), guide = "none" ) +
labs (
title = "**EU-oikeuden ydinverkosto** — top 5 % keskeisyyden mukaan" ,
subtitle = "Solmun koko = PageRank. Etiketti = kaikkein keskeisimmät säädökset." ,
color = NULL ,
caption = "Layout: Fruchterman-Reingold. Lähde: EUR-Lex SPARQL via eurlex (R). \n Kristian Vepsäläinen / kristianvepsalainen.com"
) +
theme_graph (base_family = "sans" , base_size = 12 ) +
theme (
plot.title = element_markdown (face = "bold" , size = 15 ),
plot.subtitle = element_text (color = "grey40" ),
plot.caption = element_text (color = "grey55" , size = 9 ),
legend.position = "top"
)