What 215,000 legal acts reveal about European regulatory dynamics
Author
Kristian Vepsäläinen
Published
10 September 2026
Abstract
The public discourse on EU legislation treats legal acts as isolated events — a directive enters into force, a regulation is amended. This framing obscures the statistical structure of EU law. We analyse 215,000 legal acts from the EUR-Lex database using distributional and Bayesian methods, revealing four empirical regularities: (1) regulatory output follows a non-stationary process with identifiable structural breaks tied to institutional events; (2) implementation delay distributions are right-skewed and fat-tailed — the mean is a poor summary statistic; (3) act survival times follow a type-specific hazard function with substantial cross-sectional heterogeneity; and (4) the legal citation network exhibits scale-free degree distribution, implying a small number of foundational acts underpin the majority of EU law. All analyses are fully reproducible using the open-source eurlex R package.
Keywords
EU law, regulatory analysis, Bayesian statistics, survival analysis, network analysis
Point estimates dominate public discussion of EU regulation. A directive “enters into force.” A regulation is “amended.” A member state “fails to implement.” Each event is reported as a discrete fact, stripped of distributional context.
This framing is analytically impoverished. Regulatory output is a stochastic process with time-varying intensity. Implementation delay is a random variable with a fat right tail. Act survival time is a censored duration. Legal citations form a network with non-trivial topology.
None of these phenomena are adequately described by a single number. All of them reveal structure when examined as distributions.
This whitepaper presents five analyses of EU legislation using data from the EUR-Lex Cellar database, accessed through the eurlex R package [@ovadek2021]. The analyses span four methodological families: time series decomposition, distributional modelling, survival analysis, and network analysis. Together they constitute a statistical portrait of EU law that is not available in any existing regulatory report.
The intended audience is policy analysts, legal practitioners, and compliance professionals who need a quantitative understanding of EU regulatory dynamics — not as background knowledge, but as an operational input to decision-making.
What data is available
The eurlex package provides programmatic access to nine document types in EUR-Lex:
Type
Description
Analytical potential
regulation
Directly applicable law
Volume analysis, structural breaks
directive
Requires national transposition
Implementation delay, survival
decision
Commission/Council decisions
Decision-making rhythm
recommendation
Non-binding guidance
Soft vs. hard law trends
intagr
International agreements
External relations activity
caselaw
ECJ and General Court
Caseload, legal network centrality
proposal
Legislative proposals
Time-to-adoption, mortality rate
national_impl
National transposition measures
Implementation delay by country
manual
Other documents (SWD, impact assessments)
Preparatory work volume
This whitepaper uses regulation, directive, decision, recommendation, and national_impl. The remaining types — particularly caselaw and proposal — are reserved for subsequent analyses.
Data
Show code
# All data files are pre-fetched locally — CI renders without network access.# See blog series parts 1-5 for full data acquisition code.data_path <-here("data/eu/eu_saadanto_raw.rds")dir_path <-here("data/eu/eu_dir_raw.rds")nimpl_path <-here("data/eu/eu_nimpl_raw.rds")stopifnot("Run blog part 1 to create eu_saadanto_raw.rds"=file.exists(data_path),"Run blog part 2 to create eu_dir_raw.rds"=file.exists(dir_path),"Run blog part 2 to create eu_nimpl_raw.rds"=file.exists(nimpl_path))raw <-readRDS(data_path)raw_dir <-readRDS(dir_path)raw_nimpl <-readRDS(nimpl_path)# --- Base dataset ---df <- raw |>filter(!is.na(date)) |>mutate(date =as.Date(date),vuosi =year(date),saadostyyppi =case_when( resource_type =="regulation"~"Regulation", resource_type =="directive"~"Directive", resource_type =="decision"~"Decision", resource_type =="recommendation"~"Recommendation" ) ) |>filter(vuosi >=1960, vuosi <=2023)vuosi_yht <- df |>count(vuosi) |>complete(vuosi =1960:2023, fill =list(n =0))# --- Directive + implementation join ---dir_df <- raw_dir |>filter(!is.na(celex), !is.na(date)) |>mutate(date_adopted =as.Date(date),date_transpos =as.Date(datetranspos),vuosi_hyvaks =year(date_adopted),transpos_kk =as.numeric(date_transpos - date_adopted) /30.44 ) |>filter(vuosi_hyvaks >=1975, vuosi_hyvaks <=2023, transpos_kk >0, transpos_kk <240)nimpl_df <- raw_nimpl |>filter(!is.na(celex), !is.na(date)) |>mutate(date_impl =as.Date(date),vuosi_impl =year(date_impl),maakoodi =str_extract(celex, "[A-Z]{2,3}(?=_)"),dir_celex_raw =str_extract(celex, "(?<=^7)[0-9]{4}[A-Z][0-9]+"),dir_celex =paste0("3", dir_celex_raw) ) |>filter(vuosi_impl >=1986, vuosi_impl <=2024,!is.na(maakoodi), !is.na(dir_celex_raw))viive_df <- nimpl_df |>inner_join( dir_df |>select(celex, date_adopted, date_transpos, vuosi_hyvaks),by =c("dir_celex"="celex") ) |>mutate(viive_kk =as.numeric(date_impl - date_transpos) /30.44,myohassa = viive_kk >0 ) |>filter(!is.na(viive_kk), abs(viive_kk) <600)cat("Legislative acts:", nrow(df), "\n","Directives with transposition deadline:", nrow(dir_df), "\n","Implementation records:", nrow(viive_df), "\n","Countries covered:", n_distinct(viive_df$maakoodi), "\n")
Legislative acts: 208164
Directives with transposition deadline: 3753
Implementation records: 269712
Countries covered: 28
The dataset covers 208,164 legislative acts adopted between 1960 and 2023, 3753 directives with known transposition deadlines, and 269,712 directive–country implementation pairs across 28 member states.
Finding 1: Regulatory output is a non-stationary process
Annual legislative volume is neither constant nor smoothly trending. It is a step function punctuated by structural breaks tied to identifiable institutional events.
Annual EU legislative output 1960–2023 with Bayesian change point analysis. Red line = posterior mean level. Bar height = posterior probability of structural break at that year.
The Bayesian change point model identifies structural breaks with posterior probability exceeding 0.5 at years corresponding to the Single European Act (1986), the Maastricht Treaty and internal market completion (1992–1993), and the Eastern enlargement (2004). The 1990s peak — driven primarily by regulations — coincides with the preparation of accession countries rather than organic legislative growth.
The practical implication: compliance calendars and regulatory risk models that assume a stable legislative baseline are misspecified. The intensity of the regulatory process is time-varying, and the uncertainty around future intensity is quantifiable.
Finding 2: Implementation delay is a fat-tailed distribution
Distribution of directive implementation delay relative to transposition deadline. Right panel: country-level posterior probability of late implementation (Beta-Binomial model).
The delay distribution is right-skewed with a fat tail: most implementations are modestly late, but a minority are extremely late. The mean substantially overstates the typical delay; the median is a more robust summary statistic.
The Bayesian Beta-Binomial model quantifies country-level late implementation rates with explicit uncertainty. Countries with few observations receive wider credible intervals — reflecting genuine epistemic uncertainty rather than suppressing it. This is the appropriate treatment for policy monitoring: a country with 50 observations and 60% late rate is not equivalent to a country with 5,000 observations and 60% late rate.
A key methodological note: national_impl records reflect reported implementations only. Failure to report to EUR-Lex is itself informative — but is not captured in this dataset.
Finding 3: Act survival time is type-specific
Show code
surv_df <- raw |>filter(!is.na(date), !is.na(force)) |>mutate(date =as.Date(date),vuosi =year(date),saadostyyppi =case_when( resource_type =="regulation"~"Regulation", resource_type =="directive"~"Directive", resource_type =="decision"~"Decision", resource_type =="recommendation"~"Recommendation" ),tapaus =if_else(force =="false", 1L, 0L),aika_v =as.numeric(Sys.Date() - date) /365.25 ) |>filter(vuosi >=1960, vuosi <=2022, aika_v >0, !is.na(saadostyyppi))surv_obj <-Surv(time = surv_df$aika_v, event = surv_df$tapaus)km_fit <-survfit(surv_obj ~ saadostyyppi, data = surv_df)# Manual KM data extraction for ggplot2km_df <-map_dfr(c("Regulation", "Directive", "Decision", "Recommendation"),function(t) { fit <-survfit(Surv(aika_v, tapaus) ~1,data =filter(surv_df, saadostyyppi == t))tibble(time = fit$time,surv = fit$surv,lower = fit$lower,upper = fit$upper,saadostyyppi = t ) })ggplot(km_df, aes(time, surv, color = saadostyyppi, fill = saadostyyppi)) +geom_ribbon(aes(ymin = lower, ymax = upper), alpha =0.12, color =NA) +geom_line(linewidth =1.0) +scale_color_manual(values =c(col_red, col_green, col_blue, col_orange)) +scale_fill_manual(values =c(col_red, col_green, col_blue, col_orange)) +scale_x_continuous(limits =c(0, 50), breaks =seq(0, 50, 10)) +scale_y_continuous(labels =percent_format()) +labs(title ="**Act survival curves by type** — Kaplan-Meier estimator",subtitle ="Survival bias is corrected: acts still in force are treated as censored, not excluded.",x ="Age (years)", y ="S(t) — probability of remaining in force",color =NULL, fill =NULL,caption ="Source: EUR-Lex via eurlex (R). Kristian Vepsäläinen / kristianvepsalainen.com" ) +theme(legend.position ="top")
Kaplan-Meier survival curves by act type. S(t) = probability of remaining in force at time t. Shaded bands = 95% confidence intervals.
Survival analysis corrects a bias present in naive age distributions: acts still in force are not missing observations — they are right-censored. The Kaplan-Meier estimator handles censoring correctly, producing unbiased estimates of the survival function.
The type-specific curves reveal substantial heterogeneity. Decisions have the shortest median survival time, consistent with their operational rather than structural character. Recommendations show relatively high survival — non-binding instruments are rarely formally repealed even when superseded in practice.
The survival curve is the appropriate object of analysis for regulatory lifecycle questions. A single “average lifespan” number — widely reported in regulatory impact assessments — discards the shape of the distribution and the uncertainty around it.
Finding 4: The legal citation network has a scale-free degree distribution
Show code
# Note: full network analysis requires eu_lbs_raw.rds from blog part 5.# Here we illustrate the degree distribution structure using the# available legal basis data from the base dataset.if (file.exists(here("data/eu_lbs_raw.rds"))) { raw_lbs <-readRDS(here("data/eu_lbs_raw.rds"))# Extract edges from legal basis data lbs_sarake <-names(raw_lbs)[str_detect(names(raw_lbs), "lbs|legal|basis")][1] reunat <- raw_lbs |>filter(!is.na(celex), !is.na(.data[[lbs_sarake]])) |>rename(kohde =all_of(lbs_sarake)) |>mutate(kohde =str_extract(kohde, "[A-Z0-9]{5,20}")) |>filter(!is.na(kohde), celex != kohde) |>select(from = celex, to = kohde) |>distinct() indegree_dist <- reunat |>count(to, name ="indegree") |>count(indegree, name ="n_acts")ggplot(indegree_dist |>filter(indegree >0),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 ="**Degree distribution of EU legal citation network** (log-log)",subtitle ="Linear fit on log-log scale = power law. Most acts are peripheral; few are foundational hubs.",x ="In-degree (citations received, log)", y ="Number of acts (log)",caption ="Source: EUR-Lex SPARQL via eurlex (R). Kristian Vepsäläinen / kristianvepsalainen.com" )} else {# Placeholder when network data is not yet availabletibble(indegree =round(exp(seq(0, 5, length.out =200))),n_acts =round(1000* indegree^(-1.8) *exp(rnorm(200, 0, 0.3))) ) |>filter(n_acts >0) |>ggplot(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 ="**Degree distribution of EU legal citation network** (log-log)",subtitle ="Illustrative — run blog part 5 to generate from real network data.",x ="In-degree (citations received, log)", y ="Number of acts (log)",caption ="Illustrative simulation. Source: EUR-Lex via eurlex (R). Kristian Vepsäläinen / kristianvepsalainen.com" )}