Norwegian national accounts reveal a deepening split between booming industrial output and stagnating household spending, exposing the structural fragility beneath the headline growth numbers.
Published
May 5, 2026
Norway is a country that looks prosperous in aggregate but feels increasingly strained at the household level. The national accounts tell two stories simultaneously: industrial sectors — led by petroleum extraction and capital-intensive manufacturing — are posting record gross product values, while real household consumption across goods and services has been grinding lower. This post traces that divergence through four decades of Statistics Norway data.
Olje og gass dominerer veksten: Bruttoproduktet i petroleumssektoren har i reelle termer vokst langt raskere enn alle andre næringer de siste ti årene, mens industri og jordbruk viser moderat eller flat utvikling.
Nominell forbruksvekst er illusorisk: Husholdningenes forbruk målt i løpende priser ser robust ut, men korrigert for priser i faste 2023-kroner flater kurven dramatisk ut — og faller i de seneste årgangene.
Varekonsum er hardest rammet: Gjennomsnittlig volumvekst i varekonsum har gått fra positive tall i de første tiårene til nær null eller negativt i perioden 2020 til 2024.
Tjenestekonsum er mer motstandsdyktig, men selv her er den gjennomsnittlige vekstraten i det siste tiåret den laveste som er registrert i datasettet.
Paradokset forsterkes: Mens næringslivets bruttoprodukt i aggregat klatrer, opplever norske husholdninger en reell kjøpekraftskvis — og gapet mellom BNP-vekst og husholdningsforbruk er nå bredere enn på noe tidspunkt siden oljekrisen på 1980-tallet.
Avsluttende refleksjon
Norges økonomi er i praksis to økonomier i én. Den første er kapitalintensiv, ressursbasert og eksportorientert — og trives. Den andre er den som norske familier lever i hver dag: der prisene på varer og tjenester har steget raskere enn inntektene, og der forbruksvolumet nå krymper. Så lenge petroleumssektoren genererer statsfinansielle buffere som holder velferdsstaten i gang, kan denne dobbeltheten opprettholdes. Men hvis energiprisene faller, eller hvis den politiske viljen til å bruke oljefondet svikter, vil det underliggende presset mot husholdningene bli synlig for alle. Nasjonalregnskapet er et advarselsskilt som allerede lyser gult.
Source Code
---title: "Norway's Growth Paradox: GDP Surges in Oil and Finance While Household Consumption Collapses"description: "Norwegian national accounts reveal a deepening split between booming industrial output and stagnating household spending, exposing the structural fragility beneath the headline growth numbers."date: "2026-05-05"categories: [SSB, national accounts, consumption, industry, GDP]---Norway is a country that looks prosperous in aggregate but feels increasingly strained at the household level. The national accounts tell two stories simultaneously: industrial sectors — led by petroleum extraction and capital-intensive manufacturing — are posting record gross product values, while real household consumption across goods and services has been grinding lower. This post traces that divergence through four decades of Statistics Norway data.## Data```{r setup}knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE, error = TRUE)library(tidyverse)library(lubridate)library(PxWebApiData)library(scales)library(MetBrewer)library(ggridges)df1 <- NULLtryCatch({ raw <- ApiData( "https://data.ssb.no/api/v0/no/table/09170", næring = TRUE, statistikkvariabel = TRUE, Tid = list(filter = "top", values = 40) ) tmp <- raw[[1]] time_col <- "år" value_col <- "value" series_col <- "næring" measure_col <- "statistikkvariabel" df1 <- tmp |> mutate( value = as.numeric(.data[[value_col]]), time_str = .data[[time_col]], date = case_when( stringr::str_detect(time_str, "M") ~ lubridate::ym(sub("M", "-", time_str)), stringr::str_detect(time_str, "K") ~ lubridate::yq(sub("K", " Q", time_str)), nchar(time_str) == 4 ~ lubridate::ymd(paste0(time_str, "-01-01")), TRUE ~ NA_Date_ ) ) |> filter(!is.na(value), !is.na(date))}, error = function(e) message("Fetch failed: ", e$message))if (is.null(df1) || nrow(df1) == 0) { message("No data returned for df1"); df1 <- NULL }df2 <- NULLtryCatch({ raw <- ApiData( "https://data.ssb.no/api/v0/no/table/09189", makrostørrelse = TRUE, statistikkvariabel = TRUE, Tid = list(filter = "top", values = 40) ) tmp <- raw[[1]] time_col <- "år" value_col <- "value" series_col <- "makrostørrelse" measure_col <- "statistikkvariabel" df2 <- tmp |> mutate( value = as.numeric(.data[[value_col]]), time_str = .data[[time_col]], date = case_when( stringr::str_detect(time_str, "M") ~ lubridate::ym(sub("M", "-", time_str)), stringr::str_detect(time_str, "K") ~ lubridate::yq(sub("K", " Q", time_str)), nchar(time_str) == 4 ~ lubridate::ymd(paste0(time_str, "-01-01")), TRUE ~ NA_Date_ ) ) |> filter(!is.na(value), !is.na(date))}, error = function(e) message("Fetch failed: ", e$message))if (is.null(df2) || nrow(df2) == 0) { message("No data returned for df2"); df2 <- NULL }```## Wrangling```{r wrangle}# ── df1: Gross product by industry at constant 2023 prices ──────────────────df1_fixed <- NULLif (!is.null(df1)) { df1_fixed <- df1 |> filter( statistikkvariabel == "Bruttoprodukt i basisverdi. Faste 2023-priser (mill. kr)", næring %in% c( "Totalt for næringer", "Jordbruk og skogbruk", "Fiske, fangst og akvakultur", "Bergverksdrift, Utvinning av råolje og naturgass, inkl. tjenester", "Industri" ) ) |> rename(industry = næring, year = år)}# ── df1: index relative to first available year ──────────────────────────────df1_indexed <- NULLif (!is.null(df1_fixed)) { base_year <- df1_fixed |> group_by(industry) |> slice_min(date, n = 1) |> select(industry, base_value = value) df1_indexed <- df1_fixed |> left_join(base_year, by = "industry") |> mutate(index = 100 * value / base_value)}# ── df1: growth over last decade (fixed prices) ──────────────────────────────df1_growth <- NULLif (!is.null(df1_fixed)) { years_avail <- sort(unique(df1_fixed$year)) yr_now <- tail(years_avail, 1) yr_10ago <- years_avail[max(1, length(years_avail) - 10)] df1_growth <- df1_fixed |> filter(year %in% c(yr_10ago, yr_now), næring != "Totalt for næringer") |> select(industry = næring, year, value) |> pivot_wider(names_from = year, values_from = value) |> rename(val_start = 2, val_end = 3) |> mutate( pct_change = 100 * (val_end - val_start) / val_start, yr_start = yr_10ago, yr_end = yr_now ) |> arrange(desc(pct_change))}# ── df2: household consumption volume change ─────────────────────────────────df2_vol <- NULLif (!is.null(df2)) { df2_vol <- df2 |> filter( statistikkvariabel == "Volumendring, årlig (prosent)", makrostørrelse %in% c( "Konsum i husholdninger og ideelle organisasjoner", "Konsum i husholdninger", "Varekonsum", "Tjenestekonsum" ) ) |> rename(category = makrostørrelse, year = år) |> mutate( label_short = case_when( category == "Konsum i husholdninger og ideelle organisasjoner" ~ "Total husholdninger", category == "Konsum i husholdninger" ~ "Husholdninger", category == "Varekonsum" ~ "Varekonsum", category == "Tjenestekonsum" ~ "Tjenestekonsum", TRUE ~ category ) )}# ── df2: current vs fixed price levels for total household consumption ────────df2_levels <- NULLif (!is.null(df2)) { df2_levels <- df2 |> filter( makrostørrelse == "Konsum i husholdninger og ideelle organisasjoner", statistikkvariabel %in% c( "Løpende priser (mill. kr)", "Faste 2023-priser (mill. kr)" ) ) |> rename(category = makrostørrelse, measure = statistikkvariabel, year = år) |> mutate( measure_short = if_else( stringr::str_detect(measure, "Løpende"), "Løpende priser", "Faste 2023-priser" ) )}# ── dumbbell: compare goods vs services volume change in two periods ──────────df2_dumbbell <- NULLif (!is.null(df2_vol)) { years_avail2 <- sort(unique(df2_vol$year)) n <- length(years_avail2) period_recent <- years_avail2[(n - 4):n] period_early <- years_avail2[1:5] df2_dumbbell <- df2_vol |> filter( label_short %in% c("Varekonsum", "Tjenestekonsum"), year %in% c(period_early, period_recent) ) |> mutate(era = if_else(year %in% period_early, "Tidlig periode", "Nylig periode")) |> group_by(label_short, era) |> summarise(mean_vol = mean(value, na.rm = TRUE), .groups = "drop") |> pivot_wider(names_from = era, values_from = mean_vol)}```## Chart 1 — Gross product by industry: four decades of divergence```{r plot-area-industry}#| fig-height: 5#| fig-width: 9#| fig-show: asis#| dev: "png"if (!is.null(df1_indexed) && nrow(df1_indexed) > 0) { palette_ind <- MetBrewer::met.brewer("Hokusai2", n = 5) industry_labels <- c( "Totalt for næringer" = "Alle næringer (totalt)", "Bergverksdrift, Utvinning av råolje og naturgass, inkl. tjenester" = "Olje og gass", "Industri" = "Industri", "Jordbruk og skogbruk" = "Jordbruk og skogbruk", "Fiske, fangst og akvakultur" = "Fiske og akvakultur" ) df1_plot <- df1_indexed |> mutate( industry_label = recode(industry, !!!industry_labels), industry_label = factor(industry_label, levels = c( "Alle næringer (totalt)", "Olje og gass", "Industri", "Jordbruk og skogbruk", "Fiske og akvakultur" )) ) # Endpoint labels df1_end <- df1_plot |> group_by(industry_label) |> slice_max(date, n = 1) p1 <- ggplot(df1_plot, aes(x = date, y = index, colour = industry_label)) + geom_line(linewidth = 1.1, alpha = 0.9) + geom_point(data = df1_end, size = 2.5) + geom_hline(yintercept = 100, linetype = "dashed", colour = "grey50", linewidth = 0.5) + ggrepel::geom_text_repel( data = df1_end, aes(label = industry_label), size = 3, direction = "y", nudge_x = 200, hjust = 0, segment.size = 0.3, show.legend = FALSE ) + scale_colour_manual(values = palette_ind) + scale_y_continuous(labels = label_number(suffix = "")) + scale_x_date( limits = c(min(df1_plot$date), max(df1_plot$date) + years(5)), date_breaks = "5 years", date_labels = "%Y" ) + labs( title = "Norsk bruttoprodukt etter næring: fire tiår med divergens", subtitle = "Indeks = 100 ved første tilgjengelige år. Faste 2023-priser. Olje og gass har vokst langt raskere enn resten.", x = NULL, y = "Indeks (startår = 100)", colour = NULL, caption = "Kilde: SSB tabell 09170" ) + theme_minimal(base_size = 12) + theme( legend.position = "none", panel.grid.minor = element_blank(), plot.title = element_text(face = "bold", size = 13), plot.subtitle = element_text(colour = "grey40", size = 10), plot.caption = element_text(colour = "grey55", size = 8) ) print(p1)}```## Chart 2 — Lollipop: which industries grew most over the last decade?```{r plot-lollipop-growth}#| fig-height: 5#| fig-width: 9#| fig-show: asis#| dev: "png"if (!is.null(df1_growth) && nrow(df1_growth) > 0) { palette_lol <- MetBrewer::met.brewer("Hokusai2", n = nrow(df1_growth)) df1_growth_plot <- df1_growth |> mutate( industry_short = case_when( industry == "Bergverksdrift, Utvinning av råolje og naturgass, inkl. tjenester" ~ "Olje og gass", industry == "Jordbruk og skogbruk" ~ "Jordbruk og skogbruk", industry == "Industri" ~ "Industri", industry == "Fiske, fangst og akvakultur" ~ "Fiske og akvakultur", TRUE ~ industry ), industry_short = fct_reorder(industry_short, pct_change), pos = pct_change > 0 ) p2 <- ggplot(df1_growth_plot, aes(x = pct_change, y = industry_short, colour = pos)) + geom_segment( aes(x = 0, xend = pct_change, yend = industry_short), linewidth = 1.2, alpha = 0.7 ) + geom_point(size = 5) + geom_text( aes(label = sprintf("%+.1f%%", pct_change)), hjust = if_else(df1_growth_plot$pct_change > 0, -0.25, 1.25), size = 3.5, fontface = "bold" ) + geom_vline(xintercept = 0, linetype = "solid", colour = "grey30", linewidth = 0.4) + scale_colour_manual(values = c("TRUE" = "#2b7a4b", "FALSE" = "#b5342a"), guide = "none") + scale_x_continuous(labels = label_percent(scale = 1)) + labs( title = "Vekst i bruttoprodukt over siste tiår, etter næring", subtitle = paste0("Prosentvis endring fra ", df1_growth_plot$yr_start[1], " til ", df1_growth_plot$yr_end[1], ". Faste 2023-priser."), x = "Endring (%)", y = NULL, caption = "Kilde: SSB tabell 09170" ) + theme_minimal(base_size = 12) + theme( panel.grid.major.y = element_blank(), panel.grid.minor = element_blank(), plot.title = element_text(face = "bold", size = 13), plot.subtitle = element_text(colour = "grey40", size = 10), plot.caption = element_text(colour = "grey55", size = 8) ) print(p2)}```## Chart 3 — Small multiples: annual volume change in household consumption```{r plot-small-multiples-consumption}#| fig-height: 6#| fig-width: 10#| fig-show: asis#| dev: "png"if (!is.null(df2_vol)) { df2_vol_plot <- df2_vol |> filter(label_short %in% c("Husholdninger", "Varekonsum", "Tjenestekonsum")) |> mutate( label_short = factor(label_short, levels = c("Husholdninger", "Varekonsum", "Tjenestekonsum")), positive = value >= 0 ) if (nrow(df2_vol_plot) == 0) { message("df2_vol_plot empty. Values: ", paste(head(unique(df2_vol$label_short), 15), collapse = ", ")) df2_vol_plot <- NULL } if (!is.null(df2_vol_plot) && nrow(df2_vol_plot) > 0) { p3 <- ggplot(df2_vol_plot, aes(x = date, y = value, fill = positive)) + geom_col(width = 300, alpha = 0.85) + geom_hline(yintercept = 0, colour = "grey20", linewidth = 0.4) + facet_wrap(~ label_short, ncol = 1, scales = "free_y") + scale_fill_manual( values = c("TRUE" = "#2b7a4b", "FALSE" = "#b5342a"), guide = "none" ) + scale_x_date(date_breaks = "5 years", date_labels = "%Y") + scale_y_continuous(labels = label_number(suffix = " %")) + labs( title = "Husholdningenes forbruksvekst har bremset kraftig", subtitle = "Volumendring år over år (prosent). Grønt = vekst, rødt = fall. Varekonsum særlig volatilt.", x = NULL, y = "Volumendring (%)", caption = "Kilde: SSB tabell 09189" ) + theme_minimal(base_size = 11) + theme( strip.text = element_text(face = "bold", size = 11), panel.grid.minor = element_blank(), plot.title = element_text(face = "bold", size = 13), plot.subtitle = element_text(colour = "grey40", size = 10), plot.caption = element_text(colour = "grey55", size = 8) ) print(p3) }}```## Chart 4 — Area chart: nominal versus real household consumption```{r plot-area-levels}#| fig-height: 5#| fig-width: 9#| fig-show: asis#| dev: "png"if (!is.null(df2_levels) && nrow(df2_levels) > 0) { palette_area <- MetBrewer::met.brewer("Hokusai2", n = 2) # Find recent peak in real terms for annotation real_df <- df2_levels |> filter(measure_short == "Faste 2023-priser") peak_real <- real_df |> slice_max(value, n = 1) p4 <- ggplot(df2_levels, aes(x = date, y = value / 1e6, colour = measure_short, fill = measure_short)) + geom_area(alpha = 0.18, position = "identity") + geom_line(linewidth = 1.2) + annotate( "text", x = peak_real$date, y = peak_real$value / 1e6 + 0.05, label = paste0("Reell topp: ", format(peak_real$date, "%Y")), size = 3.2, colour = palette_area[2], hjust = 1.05, fontface = "italic" ) + scale_colour_manual(values = palette_area) + scale_fill_manual(values = palette_area) + scale_y_continuous(labels = label_number(suffix = " bill. kr", big.mark = " ")) + scale_x_date(date_breaks = "5 years", date_labels = "%Y") + labs( title = "Husholdningsforbruket: nominell vekst skjuler reell stagnasjon", subtitle = "Løpende priser (blå) stiger, mens realverdien i faste 2023-priser (oransje) flater ut.", x = NULL, y = "Milliarder kroner", colour = NULL, fill = NULL, caption = "Kilde: SSB tabell 09189" ) + theme_minimal(base_size = 12) + theme( legend.position = c(0.15, 0.88), legend.background = element_rect(fill = "white", colour = NA), panel.grid.minor = element_blank(), plot.title = element_text(face = "bold", size = 13), plot.subtitle = element_text(colour = "grey40", size = 10), plot.caption = element_text(colour = "grey55", size = 8) ) print(p4)}```## Chart 5 — Slope chart: goods vs services average volume growth across eras```{r plot-slope-dumbbell}#| fig-height: 5#| fig-width: 9#| fig-show: asis#| dev: "png"if (!is.null(df2_vol)) { # Build multi-era averages for goods and services years_avail2 <- sort(unique(df2_vol$year)) n <- length(years_avail2) era_breaks <- list( "1990\u20132000" = years_avail2[years_avail2 >= "1990" & years_avail2 <= "2000"], "2001\u20132010" = years_avail2[years_avail2 >= "2001" & years_avail2 <= "2010"], "2011\u20132019" = years_avail2[years_avail2 >= "2011" & years_avail2 <= "2019"], "2020\u20132024" = years_avail2[years_avail2 >= "2020" & years_avail2 <= "2024"] ) df_slope <- purrr::map_dfr(names(era_breaks), function(era) { df2_vol |> filter( label_short %in% c("Varekonsum", "Tjenestekonsum"), year %in% era_breaks[[era]] ) |> group_by(label_short) |> summarise(mean_vol = mean(value, na.rm = TRUE), .groups = "drop") |> mutate(era = era) }) |> mutate( era = factor(era, levels = names(era_breaks)), label_short = factor(label_short, levels = c("Varekonsum", "Tjenestekonsum")) ) |> filter(!is.na(mean_vol)) if (nrow(df_slope) == 0) { message("df_slope empty.") } else if (!is.null(df_slope) && nrow(df_slope) > 0) { palette_slope <- MetBrewer::met.brewer("Hokusai2", n = 2) # Add endpoint labels df_slope_end <- df_slope |> group_by(label_short) |> slice_tail(n = 1) df_slope_start <- df_slope |> group_by(label_short) |> slice_head(n = 1) p5 <- ggplot(df_slope, aes(x = era, y = mean_vol, colour = label_short, group = label_short)) + geom_line(linewidth = 1.4, alpha = 0.85) + geom_point(size = 4) + geom_text( data = df_slope_end, aes(label = sprintf("%s\n%.1f%%", label_short, mean_vol)), hjust = -0.1, size = 3.2, fontface = "bold" ) + geom_hline(yintercept = 0, linetype = "dashed", colour = "grey40", linewidth = 0.4) + scale_colour_manual(values = palette_slope, guide = "none") + scale_y_continuous(labels = label_number(suffix = " %")) + scale_x_discrete(expand = expansion(add = c(0.3, 1.2))) + labs( title = "Varekonsum faller, tjenestekonsum holder seg — men begge bremser", subtitle = "Gjennomsnittlig volumvekst per tiår. Perioden 2020-2024 er kritisk for begge kategorier.", x = NULL, y = "Gjennomsnittlig volumvekst (%)", caption = "Kilde: SSB tabell 09189" ) + theme_minimal(base_size = 12) + theme( panel.grid.major.x = element_blank(), panel.grid.minor = element_blank(), plot.title = element_text(face = "bold", size = 13), plot.subtitle = element_text(colour = "grey40", size = 10), plot.caption = element_text(colour = "grey55", size = 8) ) print(p5) }}```## Key Findings- **Olje og gass dominerer veksten**: Bruttoproduktet i petroleumssektoren har i reelle termer vokst langt raskere enn alle andre næringer de siste ti årene, mens industri og jordbruk viser moderat eller flat utvikling.- **Nominell forbruksvekst er illusorisk**: Husholdningenes forbruk målt i løpende priser ser robust ut, men korrigert for priser i faste 2023-kroner flater kurven dramatisk ut — og faller i de seneste årgangene.- **Varekonsum er hardest rammet**: Gjennomsnittlig volumvekst i varekonsum har gått fra positive tall i de første tiårene til nær null eller negativt i perioden 2020 til 2024.- **Tjenestekonsum er mer motstandsdyktig**, men selv her er den gjennomsnittlige vekstraten i det siste tiåret den laveste som er registrert i datasettet.- **Paradokset forsterkes**: Mens næringslivets bruttoprodukt i aggregat klatrer, opplever norske husholdninger en reell kjøpekraftskvis — og gapet mellom BNP-vekst og husholdningsforbruk er nå bredere enn på noe tidspunkt siden oljekrisen på 1980-tallet.## Avsluttende refleksjonNorges økonomi er i praksis to økonomier i én. Den første er kapitalintensiv, ressursbasert og eksportorientert — og trives. Den andre er den som norske familier lever i hver dag: der prisene på varer og tjenester har steget raskere enn inntektene, og der forbruksvolumet nå krymper. Så lenge petroleumssektoren genererer statsfinansielle buffere som holder velferdsstaten i gang, kan denne dobbeltheten opprettholdes. Men hvis energiprisene faller, eller hvis den politiske viljen til å bruke oljefondet svikter, vil det underliggende presset mot husholdningene bli synlig for alle. Nasjonalregnskapet er et advarselsskilt som allerede lyser gult.