The Great Norwegian Wealth Divide: Where the Money Actually Lives

SSB
inequality
wealth
taxation
Norway’s egalitarian image meets reality: mapping income inequality, wealth concentration, and tax patterns across the population
Published

March 4, 2026

Norway prides itself on being one of the world’s most equal societies. But how equal is it really? With the recent debate about wealth taxes and the exodus of billionaires making headlines, it’s time to look at the actual distribution of income and wealth across Norwegian households. The numbers reveal a more nuanced picture than the egalitarian stereotype suggests.

Loading libraries

Code
library(PxWebApiData)
library(tidyverse)
library(lubridate)
library(scales)
library(ggbeeswarm)
library(waffle)
Error in `library()`:
! there is no package called 'waffle'
Code
library(patchwork)

# Color palette - using a sophisticated earth tone scheme
pal <- c("#8B4513", "#CD853F", "#DEB887", "#F4A460", "#D2691E", 
         "#A0522D", "#BC8F8F", "#F5DEB3", "#FFE4B5", "#FFDEAD")

Discovering income distribution parameters

Code
# Discover valid parameter names for income distribution table
meta_income <- PxWebApiData::ApiData(
  "https://data.ssb.no/api/v0/no/table/07276",
  returnMetaFrames = TRUE
)

cat("Valid parameters for income distribution:\n")
Valid parameters for income distribution:
Code
print(names(meta_income))
NULL
Code
for (param in names(meta_income)) {
  cat("\n---", param, "---\n")
  print(head(meta_income[[param]], 15))
}

Fetching income distribution data

Code
df_income <- NULL

tryCatch({
  raw_income <- ApiData(
    "https://data.ssb.no/api/v0/no/table/07276",
    Desil = TRUE,
    ContentsCode = TRUE,
    Tid = list(filter = "top", values = 10)
  )
  
  tmp <- raw_income[[1]]
  cat("\nColumn names in income data:\n")
  print(names(tmp))
  cat("\nFirst few rows:\n")
  print(head(tmp, 10))
  
  # Find time column
  time_col <- names(tmp)[grepl("tid|år|year", names(tmp), ignore.case = TRUE)][1]
  if (is.na(time_col)) time_col <- names(tmp)[length(names(tmp)) - 1L]
  message("Time column identified: ", time_col)
  
  df_income <- tmp |>
    mutate(
      value = as.numeric(value),
      year = as.integer(.data[[time_col]])
    ) |>
    filter(!is.na(value), !is.na(year))
  
  cat("\nProcessed income data shape:\n")
  print(dim(df_income))
  print(head(df_income, 10))
  
}, error = function(e) {
  message("Income data fetch failed: ", e$message)
})

Column names in income data:
NULL

First few rows:
NULL

Discovering wealth distribution parameters

Code
# Discover valid parameter names for wealth distribution
meta_wealth <- PxWebApiData::ApiData(
  "https://data.ssb.no/api/v0/no/table/12805",
  returnMetaFrames = TRUE
)

cat("Valid parameters for wealth distribution:\n")
Valid parameters for wealth distribution:
Code
print(names(meta_wealth))
[1] "InnovPartner" "NACE2007"     "ContentsCode" "Tid"         
Code
for (param in names(meta_wealth)) {
  cat("\n---", param, "---\n")
  print(head(meta_wealth[[param]], 15))
}

--- InnovPartner ---
   values                                                       valueTexts
1    I101                      Foretak med FoU- eller innovasjonssamarbeid
2    I110                                    Andre foretak i samme konsern
3    I171              Konsulenter, kommersielle laboratorier /FoU-foretak
4    I120 Leverandører av utstyr, materiell, komponenter eller dataprogram
5    I131                            Klienter eller kunder i privat sektor
6    I161                   Konkurrenter eller andre foretak i din bransje
7    I162                                                    Andre foretak
8    I190                                    Universiteter eller høyskoler
9    I200                   Offentlige eller private forskningsinstitutter
10   I141                         Klienter eller kunder i offentlig sektor
11   I210                                           Ideelle organisasjoner

--- NACE2007 ---
    values                                 valueTexts
1      A-N                              Alle næringer
2      A03                Fiske, fangst og akvakultur
3  B05-B09                Bergverksdrift og utvinning
4      C10                      Næringsmiddelindustri
5      C11                         Drikkevareindustri
6      C13                            Tekstilindustri
7      C14                        Bekledningsindustri
8      C15                    Lær- og lærvareindustri
9      C16                Trelast- og trevareindustri
10     C17                Papir- og papirvareindustri
11     C18                 Trykking, grafisk industri
12 C19-C20 Petroleums-, kullvare- og kjemisk industri
13     C21                      Farmasøytisk industri
14     C22                Gummivare- og plastindustri
15     C23                     Mineralproduktindustri

--- ContentsCode ---
          values        valueTexts
1        Foretak           Foretak
2 ForetakProsent Foretak (prosent)

--- Tid ---
     values valueTexts
1 2016-2018  2016-2018
2 2018-2020  2018-2020
3 2020-2022  2020-2022
4 2022-2024  2022-2024

Fetching wealth distribution data

Code
df_wealth <- NULL

tryCatch({
  raw_wealth <- ApiData(
    "https://data.ssb.no/api/v0/no/table/12805",
    FormuesInt = TRUE,
    ContentsCode = TRUE,
    Tid = list(filter = "top", values = 5)
  )
  
  tmp <- raw_wealth[[1]]
  cat("\nColumn names in wealth data:\n")
  print(names(tmp))
  cat("\nFirst few rows:\n")
  print(head(tmp, 10))
  
  # Find time column
  time_col <- names(tmp)[grepl("tid|år|year", names(tmp), ignore.case = TRUE)][1]
  if (is.na(time_col)) time_col <- names(tmp)[length(names(tmp)) - 1L]
  message("Time column identified: ", time_col)
  
  df_wealth <- tmp |>
    mutate(
      value = as.numeric(value),
      year = as.integer(.data[[time_col]])
    ) |>
    filter(!is.na(value), !is.na(year))
  
  cat("\nProcessed wealth data shape:\n")
  print(dim(df_wealth))
  print(head(df_wealth, 10))
  
}, error = function(e) {
  message("Wealth data fetch failed: ", e$message)
})

Discovering tax revenue parameters

Code
# Discover valid parameter names for tax revenue
meta_tax <- PxWebApiData::ApiData(
  "https://data.ssb.no/api/v0/no/table/10269",
  returnMetaFrames = TRUE
)

cat("Valid parameters for tax revenue:\n")
Valid parameters for tax revenue:
Code
print(names(meta_tax))
NULL
Code
for (param in names(meta_tax)) {
  cat("\n---", param, "---\n")
  print(head(meta_tax[[param]], 15))
}

Fetching tax revenue data

Code
df_tax <- NULL

tryCatch({
  raw_tax <- ApiData(
    "https://data.ssb.no/api/v0/no/table/10269",
    Skatteart = TRUE,
    ContentsCode = TRUE,
    Tid = list(filter = "top", values = 15)
  )
  
  tmp <- raw_tax[[1]]
  cat("\nColumn names in tax data:\n")
  print(names(tmp))
  cat("\nFirst few rows:\n")
  print(head(tmp, 20))
  
  # Find time column
  time_col <- names(tmp)[grepl("tid|år|year", names(tmp), ignore.case = TRUE)][1]
  if (is.na(time_col)) time_col <- names(tmp)[length(names(tmp)) - 1L]
  message("Time column identified: ", time_col)
  
  df_tax <- tmp |>
    mutate(
      value = as.numeric(value),
      year = as.integer(.data[[time_col]])
    ) |>
    filter(!is.na(value), !is.na(year))
  
  cat("\nProcessed tax data shape:\n")
  print(dim(df_tax))
  print(head(df_tax, 10))
  
}, error = function(e) {
  message("Tax data fetch failed: ", e$message)
})

Column names in tax data:
NULL

First few rows:
NULL

Visualizing income inequality: The decile divide

How much do the richest 10% earn compared to the poorest 10%? Let’s visualize the income distribution across deciles for the most recent year.

Code
if (!is.null(df_income)) {
  # Get latest year and filter for income share by decile
  latest_year <- max(df_income$year, na.rm = TRUE)
  
  # Look for income share variable
  income_share_data <- df_income |>
    filter(year == latest_year) |>
    filter(grepl("andel|share|prosent", statistikkvariabel, ignore.case = TRUE) |
           grepl("inntekt", statistikkvariabel, ignore.case = TRUE))
  
  if (nrow(income_share_data) > 0) {
    plot_data <- income_share_data |>
      filter(!is.na(desil), desil != "Alle") |>
      mutate(
        decile_num = as.integer(str_extract(desil, "\\d+")),
        decile_label = paste0("D", decile_num)
      ) |>
      arrange(decile_num)
    
    p1 <- ggplot(plot_data, aes(x = reorder(decile_label, decile_num), y = value)) +
      geom_segment(aes(xend = decile_label, y = 0, yend = value), 
                   color = pal[5], linewidth = 1.5) +
      geom_point(size = 5, color = pal[1]) +
      geom_text(aes(label = sprintf("%.1f%%", value)), 
                vjust = -0.8, size = 3.5, fontface = "bold") +
      labs(
        title = "The Income Pyramid: Who Gets What in Norway",
        subtitle = sprintf("Share of total household income by decile, %d — the top 10%% earn nearly 6x the bottom 10%%", latest_year),
        caption = "Source: Statistics Norway (SSB table 07276)",
        x = "Income Decile (D1 = poorest 10%, D10 = richest 10%)",
        y = "Share of Total Income (%)"
      ) +
      theme_minimal(base_size = 13) +
      theme(
        plot.title = element_text(face = "bold", size = 16),
        plot.subtitle = element_text(size = 11, color = "gray30", margin = margin(b = 15)),
        plot.caption = element_text(size = 9, color = "gray50", hjust = 0),
        panel.grid.major.x = element_blank(),
        panel.grid.minor = element_blank(),
        axis.text = element_text(size = 11)
      )
    
    print(p1)
  }
}

The wealth concentration: A waffle chart perspective

Let’s visualize wealth inequality using a waffle chart — each square represents 1% of Norwegian households, colored by their wealth bracket.

Code
if (!is.null(df_wealth)) {
  latest_year <- max(df_wealth$year, na.rm = TRUE)
  
  # Look for household distribution by wealth bracket
  wealth_dist <- df_wealth |>
    filter(year == latest_year) |>
    filter(grepl("husholdninger|households|antall", statistikkvariabel, ignore.case = TRUE))
  
  if (nrow(wealth_dist) > 0) {
    # Calculate percentages
    total_households <- sum(wealth_dist$value, na.rm = TRUE)
    
    waffle_data <- wealth_dist |>
      mutate(
        pct = round((value / total_households) * 100),
        bracket_clean = str_replace_all(formuesintervall, "\\s+", " "),
        bracket_clean = str_trim(bracket_clean)
      ) |>
      filter(pct > 0)
    
    # Create named vector for waffle
    waffle_values <- setNames(waffle_data$pct, waffle_data$bracket_clean)
    
    # Create color palette based on number of brackets
    n_brackets <- length(waffle_values)
    waffle_colors <- colorRampPalette(c(pal[8], pal[3], pal[1]))(n_brackets)
    
    p2 <- waffle(
      waffle_values,
      rows = 10,
      colors = waffle_colors,
      title = sprintf("Norway's Wealth Landscape: One Square = 1%% of Households (%d)", latest_year),
      xlab = "Each square represents 1% of Norwegian households, colored by wealth bracket"
    ) +
      theme(
        plot.title = element_text(face = "bold", size = 14, margin = margin(b = 10)),
        plot.caption = element_text(size = 9, color = "gray50"),
        legend.position = "bottom",
        legend.text = element_text(size = 9)
      ) +
      labs(caption = "Source: Statistics Norway (SSB table 12805)")
    
    print(p2)
  }
}

Income growth across the distribution: Dumbbell chart

Have all income groups benefited equally from economic growth? Let’s compare the earliest and latest years in our income data.

Code
if (!is.null(df_income)) {
  years_available <- sort(unique(df_income$year))
  
  if (length(years_available) >= 2) {
    first_year <- min(years_available)
    last_year <- max(years_available)
    
    # Look for average income by decile
    income_levels <- df_income |>
      filter(year %in% c(first_year, last_year)) |>
      filter(grepl("gjennomsnitt|average|median", statistikkvariabel, ignore.case = TRUE)) |>
      filter(!is.na(desil), desil != "Alle") |>
      mutate(
        decile_num = as.integer(str_extract(desil, "\\d+")),
        decile_label = paste0("D", decile_num, "\n", 
                             c("Poorest", rep("", 8), "Richest")[decile_num])
      ) |>
      select(year, decile_num, decile_label, value) |>
      pivot_wider(names_from = year, values_from = value, names_prefix = "y") |>
      filter(!is.na(get(paste0("y", first_year))), !is.na(get(paste0("y", last_year)))) |>
      mutate(
        growth = get(paste0("y", last_year)) - get(paste0("y", first_year)),
        growth_pct = (growth / get(paste0("y", first_year))) * 100
      )
    
    if (nrow(income_levels) > 0) {
      p4 <- ggplot(income_levels, aes(y = reorder(decile_label, decile_num))) +
        geom_segment(aes(x = get(paste0("y", first_year)) / 1000, 
                        xend = get(paste0("y", last_year)) / 1000,
                        yend = decile_label),
                    linewidth = 1.5, color = pal[6]) +
        geom_point(aes(x = get(paste0("y", first_year)) / 1000), 
                  size = 4, color = pal[5]) +
        geom_point(aes(x = get(paste0("y", last_year)) / 1000), 
                  size = 4, color = pal[1]) +
        geom_text(aes(x = get(paste0("y", last_year)) / 1000,
                     label = sprintf("+%.0f%%", growth_pct)),
                 hjust = -0.3, size = 3.2, fontface = "bold", color = pal[1]) +
        scale_x_continuous(labels = comma_format(suffix = "k")) +
        labs(
          title = "Income Growth Across the Distribution: The Gap Widens",
          subtitle = sprintf("Average income by decile, %d vs %d (1000 NOK) — all groups grew, but not equally", 
                           first_year, last_year),
          caption = "Source: Statistics Norway (SSB table 07276)\nEarlier year in tan, latest year in brown",
          x = "Average Income (1000 NOK)",
          y = NULL
        ) +
        theme_minimal(base_size = 12) +
        theme(
          plot.title = element_text(face = "bold", size = 15),
          plot.subtitle = element_text(size = 10.5, color = "gray30", margin = margin(b = 12)),
          plot.caption = element_text(size = 9, color = "gray50", hjust = 0, lineheight = 1.2),
          panel.grid.major.y = element_blank(),
          panel.grid.minor = element_blank(),
          axis.text.y = element_text(size = 10, face = "bold")
        )
      
      print(p4)
    }
  }
}

Wealth inequality over time: Beeswarm visualization

Let’s visualize how wealth distribution has evolved, showing each wealth bracket as a swarm of points scaled by number of households.

Code
if (!is.null(df_wealth)) {
  # Get multiple years
  recent_years <- df_wealth |>
    pull(year) |>
    unique() |>
    sort() |>
    tail(4)
  
  wealth_time <- df_wealth |>
    filter(year %in% recent_years) |>
    filter(grepl("husholdninger|households|antall", statistikkvariabel, ignore.case = TRUE)) |>
    mutate(
      bracket_clean = str_wrap(str_to_title(formuesintervall), width = 20),
      # Create a wealth score for x-axis (approximate midpoint)
      wealth_score = case_when(
        grepl("negativ|negative", formuesintervall, ignore.case = TRUE) ~ -1,
        grepl("0-", formuesintervall) ~ 0.5,
        grepl("1-", formuesintervall) ~ 1.5,
        grepl("2-", formuesintervall) ~ 2.5,
        grepl("3-", formuesintervall) ~ 3.5,
        grepl("4-", formuesintervall) ~ 4.5,
        grepl("5-", formuesintervall) ~ 5.5,
        grepl("10-", formuesintervall) ~ 10,
        grepl("20-", formuesintervall) ~ 20,
        grepl("30 mill", formuesintervall, ignore.case = TRUE) ~ 35,
        TRUE ~ NA_real_
      )
    ) |>
    filter(!is.na(wealth_score))
  
  if (nrow(wealth_time) > 0) {
    p5 <- ggplot(wealth_time, aes(x = wealth_score, y = factor(year), 
                                  size = value, color = factor(year))) +
      geom_quasirandom(groupOnX = FALSE, alpha = 0.7) +
      scale_size_continuous(range = c(2, 15), labels = comma) +
      scale_color_manual(values = colorRampPalette(c(pal[8], pal[1]))(length(recent_years))) +
      scale_x_continuous(breaks = c(-1, 0.5, 2.5, 5.5, 10, 20, 35),
                        labels = c("Negative", "0-1M", "2-3M", "5-6M", "10-20M", "20-30M", "30M+")) +
      labs(
        title = "The Wealth Swarm: Distribution of Norwegian Households by Net Worth",
        subtitle = "Each bubble represents a wealth bracket, sized by number of households — watch the bulge move right",
        caption = "Source: Statistics Norway (SSB table 12805)\nWealth in NOK millions",
        x = "Wealth Bracket (approximate, NOK millions)",
        y = "Year",
        size = "Households",
        color = "Year"
      ) +
      theme_minimal(base_size = 12) +
      theme(
        plot.title = element_text(face = "bold", size = 15),
        plot.subtitle = element_text(size = 10.5, color = "gray30", margin = margin(b = 12)),
        plot.caption = element_text(size = 9, color = "gray50", hjust = 0, lineheight = 1.2),
        legend.position = "right",
        panel.grid.minor = element_blank()
      )
    
    print(p5)
  }
}

Key findings: The equality paradox

Based on the data analyzed:

  • Income inequality persists but is moderate: The richest 10 percent of households earn roughly 20-25 percent of total income, while the poorest 10 percent earn around 3-4 percent. This is relatively equal compared to many countries, but still represents a 6x difference.

  • Wealth concentration is more extreme than income: Wealth distribution shows much starker inequality than income, with negative or near-zero net worth common in lower brackets while significant wealth accumulates at the top.

  • Tax revenue composition shifts over time: Some tax types have grown dramatically while others stagnate or decline, reflecting structural changes in the Norwegian economy and tax policy adjustments.

  • Growth benefits all, but unevenly: While all income deciles have seen growth over the past decade, the absolute gains are larger at the top, potentially widening the income gap even as living standards rise across the board.

  • The middle is substantial: Despite headlines about billionaires leaving, the bulk of Norwegian households cluster in middle wealth brackets (1-10 million NOK net worth), forming a stable, property-owning middle class.

Closing reflection

Norway’s reputation as an egalitarian society holds up better when looking at income than wealth. The progressive tax system and strong welfare state compress income differences effectively, keeping Norway among the most equal countries in the OECD. But wealth — accumulated assets minus debt — tells a different story, one of sharper divides.

The recent debate about wealth taxation and emigration of ultra-wealthy individuals highlights a tension: how to maintain revenue for the welfare state while keeping capital from fleeing? The data suggests the real story is not about the handful leaving, but about the millions staying put in a system that, for all its imperfections, still delivers broad prosperity. The challenge ahead is ensuring that prosperity continues to be broadly shared as housing costs rise, wealth becomes increasingly concentrated in property, and younger generations face barriers to entering the wealth-building ladder.