Same Crisis, Different Endings: Greece and the Euro Crisis Countries

What is this?

Between 2010 and 2013 six euro countries ran into deep trouble: Greece, Ireland, Portugal, Spain, Cyprus and Italy. Five of them needed rescue loans from the other euro countries, and all six went through years of cuts and lost jobs. They started from the same crisis, but they did not end up in the same place: in 2024 the average person in Ireland produced almost twice as much as in 2008, while in Greece it was still less than in 2008.

This data story compares the six countries, in plain words, on:

  • borrowing — how much more each country bought from abroad than it sold, before the crisis,
  • income — how much the country produces per person,
  • jobs — how many people who want work cannot find it,
  • investment — what the country spends on its future,
  • exports — how much it sells to the rest of the world,
  • people — whether its population grew or shrank.

Greece is in red throughout and the other five in gray. It follows on from Greece Since 2000, which tells the Greek story on its own. All the data comes from golemworldbank, an R package and Shiny app for exploring World Bank indicators; the page carries its own copy of the data for these six countries and of the one function it uses, so it runs without installing anything from GitHub.


Packages

Code
# install.packages(c("dplyr", "tidyr", "plotly"))
# The data and indicator_change() come from golemworldbank, copied next to this page:
# R/code_golemworldbank.R (sourced above) and R/mfi_euro_crisis.rds. For every country,
# install golemworldbank itself; its data is in Git LFS, so install it from a clone
# (install_github() can download LFS pointer files instead of the data):
#   git lfs install
#   git clone https://github.com/sedzinfo/golemworldbank.git
# then in R:
#   devtools::install_local("golemworldbank")

library(dplyr)
library(plotly)          # interactive charts

The data

golemworldbank ships the World Bank’s World Development Indicators as one long table, mfi: one row per country, indicator and year. R/mfi_euro_crisis.rds holds its rows for the six countries from 2000 onwards, every indicator. We keep the indicators this story needs, each with a short name:

Code
# golemworldbank's mfi, six countries from 2000 (with the package installed, its mfi has every country)
mfi <- readRDS(paste0(directory, "R/mfi_euro_crisis.rds"))

countries <- c("Greece", "Ireland", "Portugal", "Spain", "Cyprus", "Italy")
last_year <- 2024   # the latest year with complete numbers: 2025 is still incomplete
indicators <- c(
  income             = "GDP per capita (constant 2015 US$)",
  income_gni         = "GNI per capita (constant 2015 US$)",
  unemployment       = "Unemployment, total (% of total labor force) (modeled ILO estimate)",
  youth_unemployment = "Unemployment, youth total (% of total labor force ages 15-24) (modeled ILO estimate)",
  investment         = "Gross fixed capital formation (% of GDP)",
  current_account    = "Current account balance (% of GDP)",
  exports            = "Exports of goods and services (% of GDP)",
  population         = "Population, total"
)

df_peers <- mfi %>%
  filter(`Country Name` %in% countries, `Indicator Name` %in% indicators) %>%
  mutate(country = `Country Name`,
         year    = as.numeric(as.character(Year)),
         measure = names(indicators)[match(`Indicator Name`, indicators)]) %>%
  select(country, measure, year, value) %>%
  arrange(country, measure, year)

head(df_peers, 12)
   country         measure year      value
1   Cyprus current_account 2000  -4.887475
2   Cyprus current_account 2001  -3.099168
3   Cyprus current_account 2002  -3.320125
4   Cyprus current_account 2003  -2.007671
5   Cyprus current_account 2004  -4.773275
6   Cyprus current_account 2005  -5.266826
7   Cyprus current_account 2006  -6.367948
8   Cyprus current_account 2007  -7.636691
9   Cyprus current_account 2008 -14.385541
10  Cyprus current_account 2009  -6.670743
11  Cyprus current_account 2010 -10.762276
12  Cyprus current_account 2011  -2.093436

Each row is one country, one indicator, one year:

Column Description
country Country name
measure Short name of the indicator (the names of indicators above)
year Year of observation
value The World Bank’s value for that country and year

A few of these need a word of explanation:

  • Income (GDP per person) is the value of everything a country produces in a year, divided by the number of people who live there, in US dollars with price rises removed, so that years can be compared fairly. It is a rough measure of how well off the average person is.
  • Borrowing from abroad (the current account) is what a country earns from the rest of the world minus what it pays to it, as a share of everything it produces. A minus means it bought more than it sold and borrowed to pay the difference.
  • Investment is the money spent on things that help a country earn more later: new buildings, factories, machines, roads and equipment, as a share of everything the economy produces.

Every chart shades the euro crisis (2008 to 2013) and COVID (2020), and writes each country’s latest value at the end of its line; labels that would overlap are moved apart and joined to their line:

Code
# a measure's value for one country and year, and the change between two values
at <- function(m, cty, y) df_peers$value[df_peers$measure == m & df_peers$country == cty & df_peers$year == y]
change_pct <- function(from, to) round((to - from) / from * 100)
big <- function(v) format(round(v), big.mark = ",")

# Greece in red, the others in gray (as in the Greek Wages and Housing races)
highlight <- "Greece"
colour_of <- function(cty) ifelse(cty == highlight, "#E74C3C", "#B3B3B3")

shocks <- list(list(from = 2008,   to = 2013,   label = "Euro crisis"),
               list(from = 2019.5, to = 2020.5, label = "COVID"))

# chart height in pixels, set on the widget itself (width stays 100% of the
# page), so the chart always fills the box it is drawn in — no scroll bars.
# Returned inside a tagList: printed straight from a chunk, a widget's height
# is overridden by the chunk's figure size; inside a tagList it is kept
# The plotly toolbar goes down the right edge, so it never covers a long title
sized <- function(p, h) {
  p <- plotly::layout(p, modebar = list(orientation = "v"))
  p$height <- h
  htmltools::tagList(p)
}

# data source as a footnote inside each chart, at the bottom of the bottom
# margin `b` (in pixels), so it lands in the same place whatever the height
source_note <- function(b) {
  list(text = "Source: World Bank, World Development Indicators, via <a href='https://github.com/sedzinfo/golemworldbank'>golemworldbank</a>",
       xref = "paper", yref = "paper", x = 1, y = 0, yshift = -(b - 22),
       xanchor = "right", yanchor = "top", showarrow = FALSE,
       font = list(size = 11, color = "gray50"))
}

# end labels: each sits right of its line's last point; labels closer than
# `gap` pixels are pushed upwards and joined to their point by a leader line.
# `per_px` is how many y units one pixel of the plot is worth
end_labels <- function(ends, per_px, unit, gap = 17) {
  o <- order(ends$value)
  y <- ends$value[o]
  for (i in seq_along(y)[-1]) y[i] <- max(y[i], y[i - 1] + gap * per_px)
  ends$label_y[o] <- y
  lapply(seq_len(nrow(ends)), function(i) {
    name <- if (ends$country[i] == highlight) paste0("<b>", ends$country[i], "</b>") else ends$country[i]
    list(x = ends$year[i], y = ends$value[i], xref = "x", yref = "y",
         ax = 24, ay = -(ends$label_y[i] - ends$value[i]) / per_px,
         showarrow = TRUE, arrowhead = 0, arrowwidth = 1, arrowcolor = "#B3B3B3",
         xanchor = "left", text = paste0(name, " ", ends$text[i]),
         font = list(size = 12, color = "gray25"))
  })
}

# one line per country, Greece drawn last so it sits on top. `index_year`
# turns each line into "that year = 100"; `ref` draws a dotted reference line
peer_chart <- function(m, title, ytitle, unit = "", index_year = NULL, ref = NULL,
                       tozero = TRUE, h = 520) {
  d <- df_peers[df_peers$measure == m & df_peers$year >= 2000 & df_peers$year <= last_year, ]
  if (!is.null(index_year)) {
    d <- d %>% group_by(country) %>%
      mutate(value = value / value[year == index_year] * 100) %>% ungroup()
  }
  fmt <- function(v) if (unit == "$") paste0("$", big(v)) else paste0(round(v, 1), unit)
  p <- plot_ly()
  for (cty in c(setdiff(countries, highlight), highlight)) {
    dc <- d[d$country == cty, ]
    p <- add_lines(p, data = dc, x = ~year, y = ~value, name = cty,
                   line = list(color = colour_of(cty), width = if (cty == highlight) 3 else 2),
                   text = paste0(cty, " · ", dc$year, "<br>", fmt(dc$value)), hoverinfo = "text")
  }
  ends <- d %>% filter(!is.na(value)) %>% group_by(country) %>% slice_max(year) %>% ungroup() %>%
    mutate(text = fmt(value))
  b <- 110
  lo <- if (tozero) min(0, d$value, na.rm = TRUE) else min(d$value, na.rm = TRUE)
  per_px <- (max(d$value, na.rm = TRUE) - lo) * 1.1 / (h - 90 - b)
  shapes <- lapply(shocks, function(s) {
    list(type = "rect", xref = "x", yref = "paper", x0 = s$from, x1 = s$to, y0 = 0, y1 = 1,
         fillcolor = "gray92", line = list(width = 0), layer = "below")
  })
  if (!is.null(ref)) {
    shapes <- c(shapes, list(list(type = "line", xref = "paper", x0 = 0, x1 = 1, y0 = ref, y1 = ref,
                                  line = list(dash = "dot", color = "gray"))))
  }
  tags <- lapply(shocks, function(s) {
    list(x = (s$from + s$to) / 2, y = 1, xref = "x", yref = "paper", yanchor = "bottom",
         text = s$label, showarrow = FALSE, font = list(size = 12, color = "gray40"))
  })
  plotly::layout(p,
    title  = list(text = title, x = 0.02, xanchor = "left", y = 0.97, yanchor = "top"),
    margin = list(t = 90, r = 150, b = b),
    xaxis  = list(title = "", tickangle = -90, range = c(1999.5, max(d$year) + 0.5)),
    yaxis  = list(title = ytitle, ticksuffix = if (unit == "%") "%" else "",
                  rangemode = if (tozero) "tozero" else "normal"),
    showlegend = FALSE,
    shapes = shapes,
    annotations = c(tags, end_labels(ends, per_px, unit), list(source_note(b))),
    font   = list(size = 13, color = "gray25")) %>%
    sized(h)
}

# one bar per country, largest at the top, Greece in red; a negative bar
# carries its value inside, where it cannot run into the country names
ranked_bars <- function(values, title, h = 380) {
  d <- data.frame(country = names(values), value = unname(values))
  d <- d[order(d$value), ]
  b <- 80
  plot_ly(d, x = ~value, y = ~factor(country, levels = country), type = "bar", orientation = "h",
          marker = list(color = colour_of(d$country)),
          # rounding first (+ 0 turns -0 into 0) so -0.3 reads "+0%", never "-0%"
          text = sprintf("%+.0f%%", round(d$value) + 0),
          textposition = ifelse(d$value < 0, "inside", "outside"),
          textfont = list(color = ifelse(d$value < 0, "white", "gray25")), cliponaxis = FALSE,
          hovertext = paste0(d$country, ": ", sprintf("%+.1f%%", d$value)), hoverinfo = "text") %>%
    plotly::layout(
      title  = list(text = title, x = 0.02, xanchor = "left", y = 0.95, yanchor = "top"),
      margin = list(t = 70, r = 60, b = b, l = 90),
      xaxis  = list(title = "", ticksuffix = "%", zeroline = TRUE, zerolinecolor = "gray"),
      yaxis  = list(title = ""),
      annotations = list(source_note(b)),
      font   = list(size = 13, color = "gray25")) %>%
    sized(h)
}

Before the crisis: living on borrowed money

Code
peer_chart("current_account",
           title  = "Buying more from abroad than selling (-) or less (+), % of the economy",
           ytitle = "% of the economy", unit = "%", ref = 0, tozero = FALSE)

In the 2000s borrowing was cheap across the euro area, and for years Greece, Portugal, Spain and Cyprus bought far more from the rest of the world than they sold, borrowing the difference. In 2008 the gap was -14.6% of everything Greece produced, -14.4% in Cyprus and -11.9% in Portugal. When the 2008 financial crisis made lenders nervous, the money stopped coming, and each country had to close the gap fast. By 2013 every one of them was close to zero. Italy borrowed little from abroad and Ireland less than the others: their problems came from a government that already owed a lot (Italy) and from banks that had lent too much on houses (Ireland).

Five of the six then needed rescue loans from the other euro countries, the International Monetary Fund or both, and in return had to cut spending and raise taxes:

Country Rescue Ended
Greece three loans, 2010, 2012 and 2015 2018
Ireland 2010 2013
Portugal 2011 2014
Spain a loan to rescue its banks, 2012 2014
Cyprus 2013, which also took money from large bank deposits 2016
Italy none —

Income: from almost double to still below

To compare countries of different sizes, each line starts at 100 in 2008: 110 means 10% more than in 2008, 90 means 10% less.

Code
peer_chart("income", index_year = 2008, ref = 100, tozero = FALSE,
           title  = "Income per person, 2008 = 100 (price rises removed)",
           ytitle = "2008 = 100")
Code
income_change <- sapply(countries, function(cty) change_pct(at("income", cty, 2008), at("income", cty, 2024)))
ranked_bars(income_change, "Income per person: change from 2008 to 2024 (%)")

All six fell into the crisis, but they came out of it at very different speeds. By 2024 income per person was +91% in Ireland against 2008, +19% in Cyprus, +13% in Portugal, +8% in Spain and +3% in Italy. Greece is the only one still below 2008, at -10%: its fall was the deepest and its rescue the longest.


Ireland’s numbers need a second look

Ireland’s line shoots up from 2015 because of how GDP is counted. GDP includes everything produced in the country, and many large international companies have made Ireland the base of their business for tax reasons, so their profits count as Irish production. Much of that money goes back to the companies’ owners abroad. GNI subtracts it and counts only the income that stays with people in Ireland:

Code
d <- df_peers %>%
  filter(country == "Ireland", measure %in% c("income", "income_gni"), year <= last_year, !is.na(value)) %>%
  mutate(what = ifelse(measure == "income", "GDP per person (everything produced)", "GNI per person (income that stays)"))

p <- plot_ly(d, x = ~year, y = ~value, color = ~what, colors = c("#0082c8", "#E74C3C"),
             type = "scatter", mode = "lines+markers", marker = list(size = 7),
             text = ~paste0("Ireland · ", year, "<br>", what, ": $", big(value)), hoverinfo = "text") %>%
  plotly::layout(title  = list(text = "Ireland: what is produced and what stays (US$ per person, price rises removed)",
                               x = 0.02, xanchor = "left", y = 0.97, yanchor = "top"),
                 margin = list(t = 80, b = 150),
                 xaxis  = list(title = "", tickangle = -90),
                 yaxis  = list(title = "US$ per person", rangemode = "tozero"),
                 legend = list(orientation = "h", y = -0.2),
                 annotations = list(source_note(150)),
                 font   = list(size = 13, color = "gray25"))
sized(p, 520)

In 2024 Ireland’s GDP per person was $94,475, but its GNI per person $67,024. Even GNI is lifted by these companies, which is why Ireland’s own statistics office publishes a third measure that removes them; the World Bank does not carry it. The same companies explain the strange jumps in Ireland’s investment and borrowing lines below, such as investment of 53% of the economy in 2019, the year some of them moved patents and other intangible assets to Ireland. Ireland did recover strongly — its unemployment is among the lowest of the six — but by less than its GDP suggests.


Jobs: the deepest wound

The unemployment rate counts people who want a job and are looking for one, but cannot find one.

Code
peer_chart("unemployment", unit = "%",
           title  = "People looking for work who cannot find it (%)",
           ytitle = "% of people looking for work")

In 2013, 28% of people looking for work in Greece and 26% in Spain could not find a job — more than 1 in 4. Among young people (15 to 24) it was 58% in Greece and 55% in Spain, more than half. Unemployment has fallen everywhere since, but not to the same level: by 2024 it was 4.3% in Ireland and 4.9% in Cyprus, against 10% in Greece and 11.4% in Spain.


Investment: Greece stopped building

Code
peer_chart("investment", unit = "%",
           title  = "Investment in buildings, machines and roads (% of the economy)",
           ytitle = "% of the economy")

Investment fell in every country during the crisis. Most of them went back to around a fifth of what they produce, but Greece fell furthest — from 23% in 2008 to 12% in 2013 — and stayed there until 2019. In 2024 it was still 16%, the lowest of the six. A country that invests little for a decade has fewer new factories, machines and roads to earn from, which is one reason Greek incomes have been slowest to recover. (Ireland’s line jumps around for the reasons in the section above.)


Exports: everyone sells more abroad

Code
peer_chart("exports", unit = "%",
           title  = "Exports of goods and services (% of the economy)",
           ytitle = "% of the economy")
Code
exports_change <- sapply(countries, function(cty) round(at("exports", cty, 2024) - at("exports", cty, 2008)))
ranked_bars(exports_change, "Exports: change from 2008 to 2024 (percentage points of the economy)")

The one thing all six did after the crisis was to sell more to the rest of the world. Greek exports went from 23% of the economy in 2008 to 42% in 2024, Portugal’s from 31% to 46%. The bar chart shows the change in percentage points: Greece’s exports grew by 19 points of its economy. Ireland’s exports are bigger than its whole economy, and Cyprus’s almost as big, because exports count the full value of what is sold, including parts that were imported first.


People: who stayed and who left

Code
population_change <- sapply(countries, function(cty) change_pct(at("population", cty, 2008), at("population", cty, 2024)))
ranked_bars(population_change, "Population: change from 2008 to 2024 (%)")

During the crisis many people, often young and well educated, left to find work elsewhere. In the countries that recovered quickly, people came back and newcomers arrived: from 2008 to 2024 the population grew by 24% in Cyprus and 20% in Ireland. Greece’s shrank by 6%, also because Greek families have few children; Italy’s stayed about the same.


Comparing 2008 with 2024 with indicator_change()

golemworldbank’s indicator_change() compares every indicator a country has in two years — the same comparison as the app’s Change (%) tab. Running it for each country from 2008 to 2024 and keeping the indicators of this story gives one table of percentage changes:

Code
change_2008_2024 <- bind_rows(lapply(countries, function(cty) {
  res <- indicator_change(mfi, country = cty, year1 = 2008, year2 = 2024)
  both <- rbind(res$increased, res$decreased)
  both <- both[both$Indicator %in% indicators[names(indicators) != "current_account"], ]
  data.frame(measure = names(indicators)[match(both$Indicator, indicators)],
             country = cty, change = round(both$`Change %`))
}))

change_2008_2024 %>%
  tidyr::pivot_wider(names_from = country, values_from = change) %>%
  arrange(match(measure, names(indicators))) %>%
  as.data.frame()
             measure Greece Ireland Portugal Spain Cyprus Italy
1             income    -10      91       13     8     19     3
2         income_gni     -7      70       22    10     10     4
3       unemployment     31     -37      -14     1     30    -3
4 youth_unemployment      4     -23       31     9     51    -5
5         investment    -31     -30      -11   -27    -24     0
6            exports     85      71       47    45     94    21
7         population     -6      20        1     6     24     0

Each number is the percentage change from 2008 to 2024: income -10 for Greece means 10% less than in 2008. Unemployment, investment and exports are themselves percentages, so their change is relative to the 2008 level (unemployment in Ireland going from 6.8% to 4.3% is a change of -37%). Borrowing from abroad is left out: it is a number that can be below zero, and a percentage change of a negative number, or of one that turns from a minus into a plus, has no clear meaning.


What the charts show

Code
snapshot <- df_peers %>%
  filter(year %in% c(2008, 2013, 2024), measure %in% c("income", "unemployment", "investment", "exports")) %>%
  mutate(value = round(value, 1)) %>%
  tidyr::pivot_wider(names_from = country, values_from = value) %>%
  arrange(match(measure, names(indicators)), year) %>%
  as.data.frame()
snapshot
        measure year  Cyprus  Greece Ireland   Italy Portugal   Spain
1        income 2008 27449.2 23923.9 49461.2 33550.6  20014.5 27112.5
2        income 2013 22711.7 17644.9 47928.0 30330.0  18597.7 24498.6
3        income 2024 32757.5 21494.7 94475.0 34508.1  22664.0 29245.2
4  unemployment 2008     3.8     7.7     6.8     6.7      7.6    11.3
5  unemployment 2013    16.1    27.7    13.7    12.1     16.2    26.1
6  unemployment 2024     4.9    10.0     4.3     6.5      6.5    11.4
7    investment 2008    27.2    23.3    24.8    21.6     22.9    27.9
8    investment 2013    14.1    11.5    18.3    17.3     14.8    17.6
9    investment 2024    20.5    16.0    17.3    21.5     20.4    20.3
10      exports 2008    50.1    22.8    84.3    26.7     31.3    25.5
11      exports 2013    61.3    30.3   109.7    28.2     39.6    32.8
12      exports 2024    97.0    42.1   144.0    32.3     45.8    37.1
  • The same start: before 2008, Greece, Cyprus, Portugal and Spain bought far more from abroad than they sold and borrowed the difference. When the borrowing stopped, all of them had to cut back at once.
  • Different endings: from 2008 to 2024 income per person rose by 91% in Ireland (less in reality, see GNI), 19% in Cyprus, 13% in Portugal, 8% in Spain and 3% in Italy, and fell by 10% in Greece.
  • Jobs came back everywhere, but Greece and Spain still have the highest unemployment of the six: 10% and 11.4% in 2024, against 4.3% to 6.5% in the other four.
  • Investment is where Greece differs most: it has invested the least of the six every year since 2010.
  • Exports grew in all six — the one recipe every country followed.
  • People voted with their feet: the countries that recovered fastest gained population, Greece lost it.

Customising it

Highlight a different country

Code
highlight <- "Portugal"
peer_chart("income", index_year = 2008, ref = 100, tozero = FALSE,
           title = "Income per person, 2008 = 100", ytitle = "2008 = 100")

Add a country

R/mfi_euro_crisis.rds holds these six countries only. With golemworldbank installed (see Packages), its mfi has every country: load it instead of the file and add the country to countries.

library(golemworldbank)   # replaces mfi <- readRDS(...)
countries <- c("Greece", "Ireland", "Portugal", "Spain", "Cyprus", "Italy", "Germany")

Another pair of years

Code
recovery <- indicator_change(mfi, country = "Portugal", year1 = 2013, year2 = 2019)
head(recovery$increased)
head(recovery$decreased)

Further reading


Rendered with R 4.6.1 · plotly · golemworldbank