When Russiaโ€™s invasion of Ukraine sent European natural gas prices soaring in 2022, wholesale electricity prices followed. But the shock did not hit every country equally. Countries that had invested heavily in low-carbon generation โ€” nuclear, hydro, wind, solar, geothermal, and bioenergy โ€” saw smaller and shorter-lived price increases than those still reliant on fossil fuels.

This analysis uses publicly available data to quantify the relationship between a countryโ€™s pre-crisis low-carbon electricity share and the size of its price increase.


The price shock across Europe

The chart below shows monthly wholesale electricity prices for every European country in the Ember dataset. Use the dropdown to select specific countries, or view them all at once. The 2022 spike โ€” and the uneven recovery โ€” is immediately visible.

sd_prices <- SharedData$new(prices, key = ~country, group = "ts_group")

filter_select(
  id = "country_filter",
  label = "Select countries to highlight",
  sharedData = sd_prices,
  group = ~country,
  multiple = TRUE
)
plot_ly(sd_prices, x = ~date, y = ~price, color = ~country,
        type = "scatter", mode = "lines", line = list(width = 1.3),
        hovertemplate = "%{x|%b %Y}<br>%{y:.2f} c/kWh<extra>%{fullData.name}</extra>") |>
  layout(
    xaxis = list(title = ""),
    yaxis = list(title = "Wholesale price (cents / kWh)"),
    height = 420,
    legend = list(orientation = "v", x = 1.02, y = 1, font = list(size = 8)),
    hovermode = "x unified",
    margin = list(t = 10)
  ) |>
  highlight(on = "plotly_click", off = "plotly_doubleclick",
            persistent = TRUE, selectize = FALSE)

Source: Ember European Wholesale Electricity Price Data (monthly).


Who got hit hardest?

To measure the lasting impact โ€” rather than just the 2022 spike โ€” we compare each countryโ€™s 2019 average price with its average over 2023โ€“2025. This smooths out the volatile recovery and captures the new normal.

d_bar <- change |>
  mutate(
    country = factor(country, levels = country),
    col = ifelse(price_change > 0, "#e74c3c", "#2980b9")
  )

plot_ly(d_bar, y = ~country, x = ~price_change,
        type = "bar", orientation = "h",
        marker = list(color = ~col),
        text = ~paste0(ifelse(price_change > 0, "+", ""),
                       round(price_change, 2), " c/kWh"),
        textposition = "outside", textfont = list(size = 11),
        hovertemplate = paste0(
          "<b>%{y}</b><br>",
          "Change: %{x:.2f} c/kWh<br>",
          "2019: %{customdata[0]:.2f} c/kWh<br>",
          "Avg 2023-25: %{customdata[1]:.2f} c/kWh<extra></extra>"
        ),
        customdata = ~cbind(price_2019, price_post)) |>
  layout(
    xaxis = list(title = "Price change (cents / kWh)"),
    yaxis = list(title = "", categoryorder = "trace"),
    height = max(380, nrow(d_bar) * 18),
    margin = list(l = 120, r = 60),
    showlegend = FALSE
  )

Difference between the average wholesale price in 2023โ€“2025 and the 2019 baseline, in euro-cents per kWh. Red bars = price increase; blue bars = price decrease.


The low-carbon advantage

Is there a systematic relationship between a countryโ€™s pre-crisis electricity mix and the size of its price shock? We measure each countryโ€™s low-carbon share (renewx) in 2019 โ€” the combined share of nuclear, hydro, wind, solar, geothermal, and bioenergy โ€” and plot it against the price change computed above.

fit <- lm(price_change ~ renewx, data = reg_data)
s   <- summary(fit)
r2  <- round(s$r.squared, 3)
slope <- round(coef(fit)[2], 4)
se_slope <- round(s$coefficients[2, 2], 4)

slope_sign <- ifelse(slope < 0, "", "+")
slope_label <- glue("Slope: {slope_sign}{slope} c/kWh per pp  (SE = {se_slope})")

# Use ggplot + ggrepel for non-overlapping labels
p_scatter <- ggplot(reg_data, aes(renewx, price_change)) +
  geom_smooth(method = "lm", se = TRUE, colour = "#e67e22", fill = "#e67e22",
              alpha = 0.15, linewidth = 1.2) +
  geom_point(colour = "#2980b9", size = 3, alpha = 0.85,
             shape = 21, fill = "#2980b9", stroke = 0.5) +
  ggrepel::geom_text_repel(
    aes(label = country),
    size = 3.2, colour = "#2c3e50",
    max.overlaps = Inf,
    box.padding = 0.4,
    point.padding = 0.3,
    segment.color = "grey70",
    segment.size = 0.3,
    min.segment.length = 0.2,
    seed = 42
  ) +
  annotate("text",
    x = max(reg_data$renewx) * 0.97,
    y = max(reg_data$price_change) * 0.95,
    label = slope_label,
    hjust = 1, size = 4, colour = "#e67e22",
    fontface = "bold"
  ) +
  annotate("text",
    x = max(reg_data$renewx) * 0.97,
    y = max(reg_data$price_change) * 0.85,
    label = glue("R\u00b2 = {r2}"),
    hjust = 1, size = 3.8, colour = "#e67e22"
  ) +
  labs(
    x = "Low-carbon electricity share in 2019 (%)",
    y = "Price change: 2019 vs avg 2023\u20132025 (cents / kWh)"
  )

p_scatter

Each dot is a European country. The orange line is the OLS best fit; the shaded band shows the 95% confidence interval. Countries with a higher low-carbon share in 2019 experienced smaller price increases.
ยฉ Ralf Martin

For every additional percentage point of low-carbon electricity in 2019, the post-crisis price increase was -0.033ย cents/kWh smaller on average.


Regression results

cat(glue("**N = {nrow(reg_data)} countries &emsp;|&emsp; ",
         "R\u00b2 = {r2} &emsp;|&emsp; ",
         "Adj. R\u00b2 = {round(s$adj.r.squared, 3)} &emsp;|&emsp; ",
         "F-statistic p = {format.pval(pf(s$fstatistic[1], s$fstatistic[2], s$fstatistic[3], lower.tail=FALSE), digits=3)}**\n\n"))
## **N = 29 countries &emsp;|&emsp; Rยฒ = 0.207 &emsp;|&emsp; Adj. Rยฒ = 0.177 &emsp;|&emsp; F-statistic p = 0.0132**
broom::tidy(fit, conf.int = TRUE) |>
  mutate(across(where(is.numeric), ~round(.x, 4))) |>
  knitr::kable(
    col.names = c("Term", "Estimate", "Std. Error", "t", "p-value",
                  "CI low (2.5%)", "CI high (97.5%)"),
    caption = "OLS regression: price change (cents/kWh) ~ low-carbon share (%)"
  )
OLS regression: price change (cents/kWh) ~ low-carbon share (%)
Term Estimate Std. Error t p-value CI low (2.5%) CI high (97.5%)
(Intercept) 6.4001 0.7976 8.0243 0.0000 4.7636 8.0367
renewx -0.0329 0.0124 -2.6533 0.0132 -0.0583 -0.0075

What if countries hadnโ€™t expanded low-carbon power?

Between 2010 and 2019 many European countries significantly increased their low-carbon electricity share. Using the regression slope estimated above, we can ask: how much higher would the post-crisis price increase have been if each country had stayed at its 2010 low-carbon share?

For each country we compute the change in renewx from 2010 to 2019, multiply by the regression slope, and interpret the result as the price-change savings attributable to that expansion.

slope_est <- coef(fit)[2]  # cents/kWh per percentage point of renewx

cf <- reg_data |>
  inner_join(gen_2010, by = "country") |>
  filter(!is.na(renewx_2010)) |>
  mutate(
    renewx_gain     = renewx - renewx_2010,          # pp increase 2010->2019
    price_saving    = renewx_gain * slope_est,        # implied price saving (negative = saved)
    counterfactual  = price_change - price_saving     # price change if stuck at 2010 mix
  ) |>
  arrange(desc(renewx_gain))

How much did expanding low-carbon generation save each country?

ggplot(cf, aes(renewx_gain, price_saving)) +
  geom_point(colour = "#27ae60", size = 3, alpha = 0.85,
             shape = 21, fill = "#27ae60", stroke = 0.5) +
  ggrepel::geom_text_repel(
    aes(label = country),
    size = 3.2, colour = "#2c3e50",
    max.overlaps = Inf,
    box.padding = 0.4,
    point.padding = 0.3,
    segment.color = "grey70",
    segment.size = 0.3,
    min.segment.length = 0.2,
    seed = 42
  ) +
  labs(
    x = "Increase in low-carbon share, 2010 to 2019 (pp)",
    y = "Implied price saving (cents / kWh)"
  )

Each dot is a country. The x-axis shows how much the low-carbon share grew between 2010 and 2019; the y-axis shows the implied reduction in the post-crisis price increase, based on the regression slope (-0.0329 c/kWh per pp). Countries in the bottom-right expanded low-carbon the most and benefited the most.

Actual vs counterfactual price increase

The chart below compares the actual price increase (2019 โ†’ avg 2023โ€“25) with the counterfactual โ€” what the increase would have been if the country had kept its 2010 low-carbon share.

cf_long <- cf |>
  select(country, Actual = price_change, Counterfactual = counterfactual) |>
  arrange(Actual) |>
  mutate(country = factor(country, levels = country)) |>
  pivot_longer(cols = c(Actual, Counterfactual),
               names_to = "scenario", values_to = "change")

plot_ly(cf_long, y = ~country, x = ~change, color = ~scenario,
        type = "bar", orientation = "h",
        colors = c("Actual" = "#2980b9", "Counterfactual" = "#e74c3c"),
        hovertemplate = paste0("<b>%{y}</b><br>",
                               "%{fullData.name}: %{x:.2f} c/kWh<extra></extra>")) |>
  layout(
    barmode = "group",
    xaxis = list(title = "Price change (cents / kWh)"),
    yaxis = list(title = "", categoryorder = "trace"),
    height = max(400, n_distinct(cf$country) * 22),
    margin = list(l = 120),
    legend = list(x = 0.7, y = 0.05, bgcolor = "rgba(255,255,255,0.8)")
  )

Blue = actual price change. Red = counterfactual price change if the country had stayed at its 2010 low-carbon share. The gap between the bars is the estimated benefit of expanding low-carbon generation between 2010 and 2019.

cf |>
  select(Country = country,
         `Renewx 2010 (%)` = renewx_2010,
         `Renewx 2019 (%)` = renewx,
         `Gain (pp)` = renewx_gain,
         `Actual change (c/kWh)` = price_change,
         `Counterfactual (c/kWh)` = counterfactual,
         `Saving (c/kWh)` = price_saving) |>
  mutate(across(where(is.numeric), ~round(.x, 2))) |>
  knitr::kable(caption = "Counterfactual analysis: implied savings from low-carbon expansion 2010-2019")
Counterfactual analysis: implied savings from low-carbon expansion 2010-2019
Country Renewx 2010 (%) Renewx 2019 (%) Gain (pp) Actual change (c/kWh) Counterfactual (c/kWh) Saving (c/kWh)
Luxembourg 8.33 74.77 66.43 4.98 7.17 -2.19
Lithuania 18.20 73.95 55.75 4.32 6.15 -1.83
Denmark 32.08 78.55 46.47 4.03 5.56 -1.53
United Kingdom 23.12 53.77 30.65 4.71 5.72 -1.01
Ireland 13.23 38.39 25.16 6.63 7.45 -0.83
Finland 58.26 81.26 23.00 0.34 1.10 -0.76
Estonia 8.10 28.12 20.03 4.06 4.72 -0.66
Greece 18.60 33.58 14.98 4.42 4.91 -0.49
Italy 25.95 39.94 13.99 6.68 7.15 -0.46
Germany 39.41 52.70 13.29 4.98 5.42 -0.44
Hungary 50.27 61.23 10.96 5.52 5.88 -0.36
Austria 66.34 77.30 10.96 5.42 5.78 -0.36
Netherlands 12.84 22.25 9.41 4.58 4.89 -0.31
Belgium 58.58 67.94 9.35 4.45 4.76 -0.31
Bulgaria 45.74 55.01 9.27 5.72 6.03 -0.30
Poland 6.94 15.60 8.66 5.05 5.34 -0.28
Romania 52.83 61.11 8.28 5.49 5.77 -0.27
Czechia 39.79 47.03 7.23 5.41 5.65 -0.24
Spain 53.65 58.90 5.25 2.48 2.65 -0.17
Slovenia 64.02 68.34 4.32 5.16 5.30 -0.14
Sweden 94.25 98.01 3.76 0.50 0.62 -0.12
Croatia 62.88 66.01 3.13 5.22 5.32 -0.10
Slovakia 74.73 77.74 3.01 5.86 5.96 -0.10
Norway 95.79 97.94 2.14 0.86 0.93 -0.07
Switzerland 96.61 97.31 0.70 5.31 5.33 -0.02
France 89.84 90.52 0.68 3.33 3.36 -0.02
Portugal 53.01 53.12 0.11 2.55 2.55 0.00
Serbia 31.77 28.51 -3.26 5.74 5.63 0.11
Latvia 54.90 49.53 -5.37 4.30 4.12 0.18

Data & methodology

Item Detail
Prices Ember European Wholesale Electricity Price Data (monthly, day-ahead)
Generation mix Our World in Data / Ember + Energy Institute
Price unit Euro-cents per kWh (= EUR/MWh รท 10)
Base year 2019 (pre-pandemic, pre-crisis)
Comparison period Average of 2023, 2024, and 2025 (where available)
Low-carbon share (renewx) Nuclear + hydro + wind + solar + geothermal + bioenergy (% of electricity, 2019)
Method OLS regression of absolute price change on 2019 low-carbon share

All data is freely available without API keys. Code is shown behind the Code buttons above each chart.