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.
Error in `library()`:
! there is no package called 'waffle'
Code
library(patchwork)# Color palette - using a sophisticated earth tone schemepal <-c("#8B4513", "#CD853F", "#DEB887", "#F4A460", "#D2691E", "#A0522D", "#BC8F8F", "#F5DEB3", "#FFE4B5", "#FFDEAD")
Discovering income distribution parameters
Code
# Discover valid parameter names for income distribution tablemeta_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 innames(meta_income)) {cat("\n---", param, "---\n")print(head(meta_income[[param]], 15))}
Fetching income distribution data
Code
df_income <-NULLtryCatch({ 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 distributionmeta_wealth <- PxWebApiData::ApiData("https://data.ssb.no/api/v0/no/table/12805",returnMetaFrames =TRUE)cat("Valid parameters for wealth distribution:\n")
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) }}
Tax revenue trends: Where the money comes from
Norway’s tax system is often praised as progressive. Let’s track how different tax types have evolved over time using a waterfall-style comparison.
Code
if (!is.null(df_tax)) {# Get first and last year to compare years_available <-sort(unique(df_tax$year))if (length(years_available) >=2) { first_year <-min(years_available) last_year <-max(years_available)# Filter for major tax categories tax_compare <- df_tax |>filter(year %in%c(first_year, last_year)) |>filter(!grepl("sum|total|tillegg", skatteart, ignore.case =TRUE)) |>group_by(skatteart, year) |>summarise(value =sum(value, na.rm =TRUE), .groups ="drop") |>pivot_wider(names_from = year, values_from = value, names_prefix ="y") |>mutate(change =get(paste0("y", last_year)) -get(paste0("y", first_year)),change_pct = (change /get(paste0("y", first_year))) *100,tax_type_clean =str_wrap(str_to_title(skatteart), width =25) ) |>filter(!is.na(change)) |>arrange(desc(abs(change))) |>slice_head(n =8) p3 <-ggplot(tax_compare, aes(x =reorder(tax_type_clean, change), y = change /1000)) +geom_segment(aes(xend = tax_type_clean, y =0, yend = change /1000),linewidth =1.2, color ="gray60") +geom_point(aes(color = change >0), size =6) +geom_text(aes(label =sprintf("%+.0f%%", change_pct)),hjust =-0.3, size =3.5, fontface ="bold") +scale_color_manual(values =c("TRUE"= pal[1], "FALSE"= pal[5])) +coord_flip() +labs(title ="Tax Revenue Evolution: Winners and Losers",subtitle =sprintf("Change in tax revenue by type, %d to %d (billion NOK)", first_year, last_year),caption ="Source: Statistics Norway (SSB table 10269)",x =NULL,y ="Change in Revenue (Billion NOK)" ) +theme_minimal(base_size =12) +theme(plot.title =element_text(face ="bold", size =15),plot.subtitle =element_text(size =11, color ="gray30", margin =margin(b =12)),plot.caption =element_text(size =9, color ="gray50", hjust =0),legend.position ="none",panel.grid.major.y =element_blank(),panel.grid.minor =element_blank(),axis.text.y =element_text(size =10) )print(p3) }}
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.
Source Code
---title: "The Great Norwegian Wealth Divide: Where the Money Actually Lives"description: "Norway's egalitarian image meets reality: mapping income inequality, wealth concentration, and tax patterns across the population"date: "2026-03-04"categories: [SSB, inequality, wealth, taxation]---```{r setup}#| echo: falseknitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE, error = TRUE)```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```{r libraries}library(PxWebApiData)library(tidyverse)library(lubridate)library(scales)library(ggbeeswarm)library(waffle)library(patchwork)# Color palette - using a sophisticated earth tone schemepal <- c("#8B4513", "#CD853F", "#DEB887", "#F4A460", "#D2691E", "#A0522D", "#BC8F8F", "#F5DEB3", "#FFE4B5", "#FFDEAD")```## Discovering income distribution parameters```{r discover-income}# Discover valid parameter names for income distribution tablemeta_income <- PxWebApiData::ApiData( "https://data.ssb.no/api/v0/no/table/07276", returnMetaFrames = TRUE)cat("Valid parameters for income distribution:\n")print(names(meta_income))for (param in names(meta_income)) { cat("\n---", param, "---\n") print(head(meta_income[[param]], 15))}```## Fetching income distribution data```{r fetch-income}df_income <- NULLtryCatch({ 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)})```## Discovering wealth distribution parameters```{r discover-wealth}# Discover valid parameter names for wealth distributionmeta_wealth <- PxWebApiData::ApiData( "https://data.ssb.no/api/v0/no/table/12805", returnMetaFrames = TRUE)cat("Valid parameters for wealth distribution:\n")print(names(meta_wealth))for (param in names(meta_wealth)) { cat("\n---", param, "---\n") print(head(meta_wealth[[param]], 15))}```## Fetching wealth distribution data```{r fetch-wealth}df_wealth <- NULLtryCatch({ 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```{r discover-tax}# Discover valid parameter names for tax revenuemeta_tax <- PxWebApiData::ApiData( "https://data.ssb.no/api/v0/no/table/10269", returnMetaFrames = TRUE)cat("Valid parameters for tax revenue:\n")print(names(meta_tax))for (param in names(meta_tax)) { cat("\n---", param, "---\n") print(head(meta_tax[[param]], 15))}```## Fetching tax revenue data```{r fetch-tax}df_tax <- NULLtryCatch({ 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)})```## Visualizing income inequality: The decile divideHow much do the richest 10% earn compared to the poorest 10%? Let's visualize the income distribution across deciles for the most recent year.```{r plot-income-deciles}#| fig-height: 6#| fig-width: 10#| fig-show: asis#| dev: "png"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 perspectiveLet's visualize wealth inequality using a waffle chart — each square represents 1% of Norwegian households, colored by their wealth bracket.```{r plot-wealth-waffle}#| fig-height: 7#| fig-width: 10#| fig-show: asis#| dev: "png"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) }}```## Tax revenue trends: Where the money comes fromNorway's tax system is often praised as progressive. Let's track how different tax types have evolved over time using a waterfall-style comparison.```{r plot-tax-waterfall}#| fig-height: 8#| fig-width: 11#| fig-show: asis#| dev: "png"if (!is.null(df_tax)) { # Get first and last year to compare years_available <- sort(unique(df_tax$year)) if (length(years_available) >= 2) { first_year <- min(years_available) last_year <- max(years_available) # Filter for major tax categories tax_compare <- df_tax |> filter(year %in% c(first_year, last_year)) |> filter(!grepl("sum|total|tillegg", skatteart, ignore.case = TRUE)) |> group_by(skatteart, year) |> summarise(value = sum(value, na.rm = TRUE), .groups = "drop") |> pivot_wider(names_from = year, values_from = value, names_prefix = "y") |> mutate( change = get(paste0("y", last_year)) - get(paste0("y", first_year)), change_pct = (change / get(paste0("y", first_year))) * 100, tax_type_clean = str_wrap(str_to_title(skatteart), width = 25) ) |> filter(!is.na(change)) |> arrange(desc(abs(change))) |> slice_head(n = 8) p3 <- ggplot(tax_compare, aes(x = reorder(tax_type_clean, change), y = change / 1000)) + geom_segment(aes(xend = tax_type_clean, y = 0, yend = change / 1000), linewidth = 1.2, color = "gray60") + geom_point(aes(color = change > 0), size = 6) + geom_text(aes(label = sprintf("%+.0f%%", change_pct)), hjust = -0.3, size = 3.5, fontface = "bold") + scale_color_manual(values = c("TRUE" = pal[1], "FALSE" = pal[5])) + coord_flip() + labs( title = "Tax Revenue Evolution: Winners and Losers", subtitle = sprintf("Change in tax revenue by type, %d to %d (billion NOK)", first_year, last_year), caption = "Source: Statistics Norway (SSB table 10269)", x = NULL, y = "Change in Revenue (Billion NOK)" ) + theme_minimal(base_size = 12) + theme( plot.title = element_text(face = "bold", size = 15), plot.subtitle = element_text(size = 11, color = "gray30", margin = margin(b = 12)), plot.caption = element_text(size = 9, color = "gray50", hjust = 0), legend.position = "none", panel.grid.major.y = element_blank(), panel.grid.minor = element_blank(), axis.text.y = element_text(size = 10) ) print(p3) }}```## Income growth across the distribution: Dumbbell chartHave all income groups benefited equally from economic growth? Let's compare the earliest and latest years in our income data.```{r plot-income-growth}#| fig-height: 7#| fig-width: 10#| fig-show: asis#| dev: "png"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 visualizationLet's visualize how wealth distribution has evolved, showing each wealth bracket as a swarm of points scaled by number of households.```{r plot-wealth-beeswarm}#| fig-height: 7#| fig-width: 11#| fig-show: asis#| dev: "png"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 paradoxBased 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 reflectionNorway'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.