The European Union (EU) is often criticized for having a heavy regulatory touch and being unwieldy. What is the reality? This automatically updated research document gives a real-time overview of the EU’s legislative output and efficiency.

As of 13 November 2024, the EU and its predecessors have produced 142566 regulations, 4344 directives, 37920 decisions and 1470 recommendations, according to Eur-Lex data pulled via the eurlex package for R1 and a SPARQL API maintained by the Publications Office of the European Union.2 (More example use cases, as well as arguments for integrating open data APIs into research workflows, can be found in this open access paper.)

The following table shows the most recent legislation published by the EU:

1 Number of acts over time

We start by looking at how the number of the four main legal acts changed over the lifespan of the EU. The number of acts adopted yearly is a parsimonious – but imperfect3 – proxy for the EU’s regulatory tendencies.

1.1 Plot

1.2 Dataframe

1.3 Code: Data

# packages
library(eurlex)
library(ggplot2)
library(dplyr)
library(purrr)
library(tidyr)
library(stringr)
library(rmarkdown)
library(modelsummary)
library(ggiraph)
library(equatiomatic)
library(DT)

# seed
set.seed(65234)

# current date
date_now <- Sys.Date()
date_now_f <- str_remove(format(date_now, "%d %B %Y"), "^0")
day_now <- as.integer(str_sub(date_now, 9, 10))
year_now <- as.integer(str_sub(date_now, 1, 4))

# acts
acts_proposals <- elx_make_query("any", sector = 3,
                                 include_date = TRUE,
                                 include_proposal = TRUE) |>
  elx_run_query() |> 
  select(-work)

# acts only
acts <- acts_proposals |> 
  filter(!is.na(celex),
         !date %in% c("1003-03-03")) |> 
  distinct(celex, .keep_all = T) |> 
  select(-proposal)

# in force
in_force <- elx_make_query("any", sector = 3,
                           include_force = TRUE,
                           include_date_force = TRUE) |>
  elx_run_query() |> 
  select(-work) |> 
  rename(date_force = dateforce) |> 
  arrange(celex, date_force) |> 
  distinct(celex, .keep_all = TRUE) |> 
  drop_na() |> 
  filter(between(as.Date(date_force), as.Date("1952-01-01"),
                 as.Date(date_now)))

# 4 main types of acts
regs <- acts |> filter(str_sub(celex,6,6) == "R")
decs <- acts |> filter(str_sub(celex,6,6) == "D")
dirs <- acts |> filter(str_sub(celex,6,6) == "L")
recs <- acts |> filter(str_sub(celex,6,6) == "H")

# proposals
proposals <- elx_make_query("proposal", include_date = TRUE) |> 
  elx_run_query() |> 
  select(-work,-type) |> 
  rename(date_proposal = date) |> 
  filter(!is.na(celex),
         !is.na(date_proposal)) |> 
  distinct(celex, .keep_all = TRUE)

# latest act titles and EuroVoc
recent_titles <- acts |> 
  arrange(desc(date), desc(celex)) |> 
  filter(str_sub(celex,6,6) %in% c("D","R","L","H")) |> 
  slice(1:50) |> 
  mutate(title = map_chr(str_c("http://publications.europa.eu/resource/celex/",
                               celex), 
                         possibly(eurlex::elx_fetch_data, otherwise = NA_character_), "title")) |> 
  mutate(date = as.Date(date))

# citation
cit_page <- paste("Michal Ovádek, '",rmarkdown::metadata$title,"', available at https://michalovadek.github.io/eulaw/, accessed on ", date_now_f,
                  sep = "")

1.4 Code: Plot

# select only main act types
relevant_acts <- bind_rows(Regulation = regs, Decison = decs, Directive = dirs,
                           Recommendation = recs,
                        .id = "type") |> 
  mutate(year = as.integer(str_sub(celex, 2,5))) |> 
  mutate(month_year = as.Date(str_replace(date, "[:digit:][:digit:]$", "01")))

# count
n_relevant <- relevant_acts |> 
  count(year, type)

# viz
ggplot(n_relevant, aes(x = year, y = n, fill = type, color = type)) +
  geom_col(show.legend = FALSE) +
  scale_fill_brewer(palette = "Spectral") +
  scale_color_brewer(palette = "Spectral") +
  facet_wrap(~type, scale = "free_y") +
  theme_minimal() +
  theme(legend.position = "top",
        legend.justification = "left",
        legend.title = element_text(face = "italic"),
        plot.background = element_rect(fill = "white", color = "grey88"),
        panel.grid = element_line(color = "grey95"),
        axis.text = element_text(color = "grey10"),
        title = element_text(face = "bold"),
        plot.subtitle = element_text(face = "italic"),
        strip.text = element_text(hjust = 0, face = "bold")) +
  labs(x = NULL,
       y = "Number of acts",
       title = "Number of legal acts produced by the European Union",
       subtitle = "Aggregated at the year level by date of publication")

# vars for text
year1 <- as.integer(str_sub(date_now,1,4))-1
year2 <- as.integer(str_sub(date_now,1,4))-2

n_year1 <- n_relevant |> 
  filter(year == year1) |> 
  summarise(n = sum(n, na.rm = T)) |> 
  as.integer()

n_year2 <- n_relevant |> 
  filter(year == year2) |> 
  summarise(n = sum(n, na.rm = T)) |> 
  as.integer()

nyeardif <- ifelse(n_year1 < n_year2, "less than", "more than")
nyeardif <- ifelse(n_year1 == n_year2, "the same as", nyeardif)

In 2023, the EU produced 2090 legal acts which is less than the 2127 acts in 2022. Overall, the EU’s legislative output is currently considerably lower compared to, in particular, the 1980s and 1990s.

2 Proportion of act types

So we see there is significant over-time variation in the use of the different types of legal acts, but some types of acts, notably regulations, have been historically significantly more common than other types of acts. We can visualize the evolution of proportions directly.

2.1 Plot

2.2 Code

# create df
proptime_df <- expand.grid(year = min(n_relevant$year):max(n_relevant$year),
            type = unique(n_relevant$type)) |> 
  left_join(n_relevant) |> 
  mutate(n = ifelse(is.na(n), 0L, n)) |> 
  group_by(year) |> 
  mutate(total_y = sum(n),
         proportion = n/total_y) |> 
  ungroup()

# viz
proptime_df |> 
  ggplot(aes(x = year, y = proportion, fill = type, color = type)) +
  geom_area(show.legend = TRUE) +
  geom_vline(xintercept = c(1960,1980,2000,2020), lty = 2, color = "grey70") +
  geom_hline(yintercept = 0.50, color = "grey50", lty = 3) +
  scale_fill_brewer(palette = "Spectral") +
  scale_color_brewer(palette = "Spectral") +
  theme_minimal() +
  theme(legend.position = "top",
        legend.justification = "left",
        legend.title = element_text(face = "italic"),
        plot.background = element_rect(fill = "white", color = "grey88"),
        panel.grid = element_blank(),
        axis.text = element_text(color = "grey10"),
        title = element_text(face = "bold"),
        plot.subtitle = element_text(face = "italic"),
        strip.text = element_text(hjust = 0, face = "bold")) +
  scale_x_continuous(expand = c(0,0)) +
  scale_y_continuous(expand = c(0,0)) +
  labs(x = NULL,
       y = NULL,
       color = NULL,
       fill = NULL,
       title = "Relative prevalence of legal acts",
       subtitle = "Proportion of each type of legal act in a given year")

2.3 Dataframe

We can see that although regulations used to dominate the EU’s legislative output, decisions – which are typically associated with various administrative actions – are nowadays almost equally prevalent.

The rise in the number of recommendations can be mostly attributed to the strengthening of the EU’s role in the surveillance and coordination of the Member States’ economic policies.

3 Year-on-year comparison

The daily streaming of data can be relied on to make completely up-to-date comparisons of EU legislative activity. For example, we can look at the extent to which the monthly adoption rate of legal acts this year differs from last year.

3.1 Plot

3.2 Code

# monthly output last two years
last_two_years <- relevant_acts |> 
  mutate(year = as.integer(str_sub(date, 1,4))) |> 
  filter(year %in% c(max(relevant_acts$year), max(relevant_acts$year)-1)) |> 
  count(month_year, type)

last_two_years_full <- expand.grid(month_year = unique(last_two_years$month_year),
                                   type = unique(last_two_years$type)) |> 
  left_join(last_two_years) |> 
  mutate(n = ifelse(is.na(n), 0L, n))

# year on year change
last_two_yoy_diff <- last_two_years_full |> 
  arrange(month_year) |> 
  mutate(month = str_sub(month_year, 6,7)) |> 
  group_by(month, type) |> 
  summarise(yoy_diff = diff(n)) |> 
  ungroup()

# this month for text
this_month_yoy <- last_two_yoy_diff |> 
  arrange(desc(month)) |> 
  slice(1:4) |> 
  summarise(sum = sum(yoy_diff)) |> 
  as.integer()

this_month_yoy_dir <- ifelse(this_month_yoy < 0, "fewer than", "more than")
#this_month_yoy_dir <- ifelse(this_month_yoy == 0, "the same as", this_month_yoy)

this_month_yoy_abs <- abs(this_month_yoy)

# viz
last_two_yoy_diff |> 
  ggplot(aes(x = month, y = yoy_diff, fill = type, color = type)) +
  geom_col(show.legend = FALSE) +
  geom_hline(yintercept = 0, color = "grey83", lty = 1) +
  facet_wrap(~type, scales = "free_y") +
  scale_fill_brewer(palette = "Spectral") +
  scale_color_brewer(palette = "Spectral") +
  theme_minimal() +
  theme(legend.position = "top",
        legend.justification = "left",
        legend.title = element_text(face = "italic"),
        plot.background = element_rect(fill = "white", color = "grey88"),
        panel.grid = element_blank(),
        axis.text = element_text(color = "grey10"),
        title = element_text(face = "bold"),
        plot.subtitle = element_text(face = "italic"),
        strip.text = element_text(hjust = 0, face = "bold")) +
  labs(x = "Month", y = "Year-on-year difference",
       title = "Monthly year-on-year comparison",
       subtitle = "Difference in the number of acts adopted this year compared to last year")

3.3 Dataframe

So far this month, the EU has adopted 177 legal acts fewer than the same month last year. But we are still early in the month.

4 Acts in force

Naturally, not all legal acts remain in force indefinitely, which means that many of the 204012 acts ever adopted by the EU are mere relics of the past.

According to Eur-Lex, the EU has 42604 legal acts in force at the moment, though a closer examination shows that many of those acts are practically obsolete, even if technically still valid. For example, act 31952S0004 is considered in force, but its sole purpose was to set the period for the collection of coal and steel levies in 1953.

Despite this data caveat, it is still interesting to find out how long ago the acts that are currently applicable have entered into force.

4.1 Plot

4.2 Code

# viz
in_force |> 
  filter(force == "true") |> 
  mutate(days_in_force = as.integer(as.Date(Sys.Date()) - as.Date(date_force)),
         type = str_sub(celex, 6,6)) |> 
  filter(type %in% c("R","L","H","D")) |> 
  mutate(type = case_when(type == "D" ~ "Decision",
                          type == "H" ~ "Recommendation",
                          type == "L" ~ "Directive",
                          type == "R" ~ "Regulation",
                          T ~ NA_character_)) |> 
  mutate(months_in_force = days_in_force/12) |> 
  ggplot(aes(x = days_in_force, fill = type, color = type)) +
  geom_histogram(bins = 200, show.legend = FALSE) +
  scale_fill_brewer(palette = "Spectral") +
  scale_color_brewer(palette = "Spectral") +
  theme_minimal() +
  theme(legend.position = "top",
        legend.justification = "left",
        legend.title = element_text(face = "italic"),
        plot.background = element_rect(fill = "white", color = "grey88"),
        panel.grid = element_line(color = "grey95"),
        axis.text = element_text(color = "grey10"),
        title = element_text(face = "bold"),
        plot.subtitle = element_text(face = "italic"),
        strip.text = element_text(hjust = 0, face = "bold")) +
  facet_wrap(~type, scales = "free") +
  labs(x = NULL,
       y = NULL,
       color = NULL,
       fill = NULL,
       title = "How old are currently applicable legal acts?",
       subtitle = "Histogram of the number of days since currently applicable acts entered into force")

4.3 Dataframe

We see that the bulk of the currently legally applicable acts have been adopted relatively recently.

At the same time, there is seemingly meaningful variation among the different types of acts. Directives suffer the least from the “recency bias”, while the time horizons of regulations and decisions are considerably shorter.

5 Legislative efficiency

The image of the EU as an unwieldy bureaucracy with a complicated legislative system appears often in national media. Moreover, most theoretical literature on political systems would expect the number of veto players to affect legislative efficiency. On this basis, we formulate the simple hypothesis that

H1: The more EU Member States there are, the longer it takes to pass legislation.

Most people are therefore susceptible to assume that the EU needs a lot of time to adopt legal acts. Let’s use Eur-Lex data to find out just how long it normally takes to pass legislation.

5.1 Plot

5.2 Code

# calculate maximum length of adoption and discard negative
acts_days <- acts_proposals |> 
  filter(!is.na(celex),
         !is.na(date),
         !is.na(proposal)) |> 
  left_join(proposals, by = c("proposal"="celex")) |> 
  mutate(days = as.integer(as.Date(date) - as.Date(date_proposal))) |> 
  filter(days > -1) |> 
  group_by(celex) |> 
  filter(days == max(days)) |> 
  ungroup() |> 
  distinct(celex, .keep_all = TRUE) |> 
  arrange(date) |> 
  mutate(year_adopted = as.integer(str_sub(date, 1, 4)),
         type = str_sub(celex, 6, 6),
         type = case_when(type == "R" ~ "Regulation",
                          type == "L" ~ "Directive",
                          type == "D" ~ "Decision",
                          T ~ "Other"),
         five_year = cut_interval(year_adopted, length = 5)) |> 
  filter(year_adopted > 1985) # not many complete pairs before this

# identify outliers
outliers <- acts_days |> 
  group_by(type) |> 
  mutate(max = max(days),
         mean = mean(days),
         coef = days / mean) |> 
  filter(coef > 2.9) |> 
  ungroup()

# plot
acts_days |> 
  filter(!celex %in% outliers$celex) |> 
  group_by(type) |> 
  mutate(global_mean = mean(days)) |> 
  ggplot(aes(x = five_year, y = days, color = type, fill = type)) +
  geom_boxplot(alpha = 0.1, show.legend = FALSE) +
  geom_hline(aes(yintercept = global_mean), lty = 2, color = "grey70") +
  facet_wrap(~type, dir = "h", ncol = 1, scales = "free_y") +
  scale_fill_brewer(palette = "Spectral") +
  scale_color_brewer(palette = "Spectral") +
  theme_minimal() +
  theme(legend.position = "top",
        legend.justification = "left",
        legend.title = element_text(face = "italic"),
        plot.background = element_rect(fill = "white", color = "grey88"),
        panel.grid = element_blank(),
        axis.text = element_text(color = "grey10"),
        title = element_text(face = "bold"),
        plot.subtitle = element_text(face = "italic"),
        plot.caption = element_text(face = "italic", size = 8),
        strip.text = element_text(hjust = 0, face = "bold")) +
  labs(x = NULL,
       y = "Days to adoption",
       title = "Number of days between proposal and adoption",
       subtitle = "By five-year window in which the act was adopted*",
       caption = "* Outliers are pruned from the analysis, dashed line shows the global mean")

5.3 Dataframe

Directives take by far the longest to adopt; unlike decisions and regulations, they are rarely deployed to regulate trivial matters. Nonetheless, there are few easily discernible temporal patterns in the plot. Most of all, it is not obvious that legislative efficiency would decrease as the EU expanded its membership.

However, before moving onto further investigation of the efficiency hypothesis, we should pause and take a look at some of the discarded outliers. What legislation took the longest to adopt?

It turns out that many of the slowest legislative processes concerned – and this is particularly visible recently – the conclusion of international agreements. In these situations, the speed does not depend solely on the EU, as an agreement can only be formally concluded once the legal text is successfully negotiated with a third party.

Most acts on the list of slowest legislative files took well over ten years to complete.

5.4 Regression

Regression analysis unpacks the relationship between a response (dependent) variable and one or more explanatory variables. In our case, we are interested in the relationship (or absence thereof) between the number of days it takes the EU to turn a Commission proposal into law and the number of Member States.

We will assume the relationship between the variables is functionally linear. Canonically, the linear model can be written as:

\[ Y \sim \beta_0 + X\beta_1 + \epsilon \]

\[ \epsilon \sim N(0,\sigma^2) \]

where \(Y\) is the response variable, \(X\) the explanatory variable, \(\beta_0\) and \(\beta_1\) the unknown parameters of the model and \(\epsilon\) the error term which comes from a normal distribution with mean zero.

5.4.1 Table

Linear regression model of legislative efficiency (Y_i = days)
Baseline Controls  Zero Intercept
Number of Member States 5.592*** 6.776*** 6.776***
(0.518) (0.508) (0.508)
Constant 165.366***
(10.549)
Act type = Other 104.422*** 104.422***
(30.691) (30.691)
Act type = Regulation −15.704 88.717***
(28.906) (9.915)
Act type = Decision −29.781 74.640***
(28.932) (12.488)
Act type = Directive 479.644*** 584.066***
(30.059) (13.419)
Num.Obs. 14802 14802 14802
R2 0.008 0.146 0.386
R2 Adj. 0.008 0.146 0.386
AIC 221779.3 219567.5 219567.5
BIC 221802.1 219613.1 219613.1
Log.Lik. −110886.632 −109777.750 −109777.750
RMSE 433.72 402.41 402.41
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
In the ‘Controls’ specification, ‘Other’ is the reference category.

5.4.2 Equation

Plugging in the variables and estimated coefficients, the following regression equation predicts the number of days an act would take to pass given some combination of input values:

\[ \operatorname{\widehat{days}} = 104.42() + 6.78(\operatorname{n\_ms}) - 29.78(\operatorname{type}_{\operatorname{Decision}}) + 479.64(\operatorname{type}_{\operatorname{Directive}}) - 15.7(\operatorname{type}_{\operatorname{Regulation}}) \]

5.4.3 Plot

5.4.4 Code

# number of member states over time
ms_years <- bind_rows(
  data.frame(
    year = 1952:1972,
    n_ms = 6
  ),
  data.frame(
    year = 1973:1980,
    n_ms = 9
  ),
  data.frame(
    year = 1981:1985,
    n_ms = 10
  ),
  data.frame(
    year = 1986:1994,
    n_ms = 12
  ),
  data.frame(
    year = 1995:2004,
    n_ms = 15
  ),
  data.frame(
    year = 2005:2006,
    n_ms = 25
  ),
  data.frame(
    year = 2007:2013,
    n_ms = 27
  ),
  data.frame(
    year = 2014:2018,
    n_ms = 28
  ),
  data.frame(
    year = 2019:2025,
    n_ms = 27
  )
)

# append N member states
days_ms <- acts_days |> 
  left_join(ms_years, by = c("year_adopted"="year")) |> 
  mutate(type = relevel(as.factor(type), ref = "Other"))

# regress
models <- list()

## m0
models[['Baseline']] <- lm(
  data = days_ms,
  formula = days ~ n_ms
)
names(models[['Baseline']]$coefficients)[1] <- "Constant"

## m1
models[['Controls']] <- lm(
  data = days_ms,
  formula = days ~ n_ms + type
)
names(models[['Controls']]$coefficients)[1] <- "Act type = Other"

## m2
models[['Zero Intercept']] <- lm(
  data = days_ms,
  formula = days ~ n_ms + type + 0
)

# effect size
effect <- as.numeric(round(models[['Controls']]$coefficients[which(names(models[['Controls']]$coefficients) == "n_ms")],2))

# summary for p values
mod_sum <- summary(models[['Controls']])

# p value
p_value <- mod_sum$coefficient[names(mod_sum$coefficient[,1]) == "n_ms", 4]

# p threshold
p_thresh <- 0.05

# can we reject the null
reject_null <- ifelse(effect > 0 & p_value < p_thresh, "can reject", "cannot reject")

# show table
modelsummary(models, 
             stars = TRUE,
             coef_map = c("n_ms" = "Number of Member States",
                          "Constant" = "Constant",
                          "typeOther" = "Act type = Other",
                          "Act type = Other" = "Act type = Other",
                          "typeRegulation" = "Act type = Regulation",
                          "typeDecision" = "Act type = Decision",
                          "typeDirective" = "Act type = Directive"),
             title = "Linear regression model of legislative efficiency (Y_i = days)",
             notes = list("In the 'Controls' specification, 'Other' is the reference category."))

5.4.5 Dataframe

Controlling for the type of act, the linear model predicts that for every additional Member State, the EU takes on average 6.78 days longer to adopt the legal act. This effect is statistically significant at 0.05 alpha.

This model is far too simple to establish the existence of a causal relationship between the number of Member States and legislative efficiency. Indeed, some scholars would argue that no amount of conditioning on observables is sufficient to establish causality. Moreover, Poisson regression would be more appropriate here as our dependent variable is discrete and bounded at zero.

Nonetheless, our simple analysis suggests that we can reject the null hypothesis that more Member States in the EU system are not associated with slower law-making.

It turns out that most legal acts adopted by the EU have been delegated (and implementing) acts, that is acts based on another piece of legislation rather than one of the EU Treaties (primary acts). Last year, such delegated acts represented 68 per cent of the EU’s yearly legislative output.

7 Cite

Cite this document as Michal Ovádek, ‘Legislative Output of the European Union’, available at https://michalovadek.github.io/eulaw/, accessed on 13 November 2024.


  1. See https://github.com/michalovadek/eurlex for more details.↩︎

  2. This also means that any omissions and mistakes present in Eur-Lex are carried through to the output shown here.↩︎

  3. We should also take into account the regulatory breadth and depth of each legal act. A decision increasing the tariff on steel by 5 per cent is a very different act from, say, the General Data Protection Regulation. ↩︎