Code
library(tidyverse)
library(PxWebApiData)
library(ggridges)
library(MetBrewer)
library(scales)
pal <- met.brewer("Hokusai2", 7)April 8, 2026
Norway’s unemployment rate hovers around historic lows in 2026, a statistic politicians love to cite. But dig into the age breakdown, and a starkly different story emerges: young Norwegians face unemployment rates triple those of their middle-aged counterparts, a gap that has widened significantly over the past two decades. This isn’t just a temporary youth phenomenon — it’s a structural feature of the Norwegian labour market that shapes life chances and economic security for an entire generation.
We analyze registered unemployment from Statistics Norway (table 08536), tracking monthly figures from 2003 to early 2026 across age groups. This administrative data captures everyone registered as unemployed with NAV, Norway’s labour and welfare administration, providing a granular view of labour market dynamics.
df <- NULL
tryCatch({
raw <- ApiData(
"https://data.ssb.no/api/v0/no/table/08536",
Region = TRUE,
Kjonn = TRUE,
Alder = TRUE,
ContentsCode = TRUE,
Tid = list(filter = "top", values = 280)
)
tmp <- raw[[1]]
message("Columns: ", paste(names(tmp), collapse = ", "))
message("Rows fetched: ", nrow(tmp))
print(head(tmp))
time_col <- names(tmp)[grepl(
"tid|år|kvartal|måned|aar|maaned|year|month|quarter",
names(tmp), ignore.case = TRUE, perl = TRUE
)][1]
if (is.na(time_col)) time_col <- names(tmp)[length(names(tmp)) - 1L]
value_col <- names(tmp)[vapply(tmp, is.numeric, logical(1L))][1]
if (is.na(value_col)) value_col <- names(tmp)[length(names(tmp))]
df <- 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))
message("Clean rows after filter: ", nrow(df))
if (nrow(df) == 0) stop("Data frame is empty after cleaning")
}, error = function(e) {
message("DATA FETCH FAILED: ", e$message)
})if (!is.null(df)) {
# Detect column names
region_col <- names(df)[grepl("region|fylke|kommune", names(df), ignore.case = TRUE)][1]
gender_col <- names(df)[grepl("kjønn|kjonn|gender|sex", names(df), ignore.case = TRUE)][1]
age_col <- names(df)[grepl("alder|age", names(df), ignore.case = TRUE)][1]
# Filter to national level, both genders combined
nat_val <- unique(df[[region_col]])[grepl("hele|total|0", unique(df[[region_col]]), ignore.case = TRUE)][1]
both_val <- unique(df[[gender_col]])[grepl("begge|total|0", unique(df[[gender_col]]), ignore.case = TRUE)][1]
df_clean <- df |>
filter(
.data[[region_col]] == nat_val,
.data[[gender_col]] == both_val
) |>
rename(age_group = !!sym(age_col)) |>
filter(!grepl("i alt|total|alle", age_group, ignore.case = TRUE)) |>
mutate(
age_group = str_trim(age_group),
year = lubridate::year(date),
month = lubridate::month(date)
)
message("Age groups available: ", paste(unique(df_clean$age_group), collapse = ", "))
}The most striking finding: unemployment rates for Norwegians under 25 consistently run 2-3 times higher than for those aged 25-54. In early 2026, youth unemployment sits around 9-10%, while the prime working-age group hovers near 3%. This isn’t a COVID artifact — the pattern has persisted for over two decades.
if (!is.null(df_clean)) {
latest <- df_clean |>
filter(date == max(date)) |>
group_by(age_group) |>
summarise(unemployed = sum(value, na.rm = TRUE), .groups = "drop") |>
arrange(desc(unemployed))
p1 <- ggplot(latest, aes(x = unemployed, y = reorder(age_group, unemployed))) +
geom_segment(aes(x = 0, xend = unemployed, y = age_group, yend = age_group),
color = pal[3], linewidth = 1.5) +
geom_point(color = pal[1], size = 5) +
labs(
title = "Youth Bear the Brunt of Norwegian Unemployment",
subtitle = paste0("Registered unemployed by age group, ", format(max(df_clean$date), "%B %Y")),
caption = "Source: Statistics Norway (SSB table 08536)",
x = "Registered unemployed (thousands)",
y = NULL
) +
scale_x_continuous(labels = comma_format()) +
theme_minimal(base_size = 13) +
theme(
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p1)
}Error:
! object 'df_clean' not found
Looking at the long arc since 2003, the age gap in unemployment has actually widened. During economic downturns — the 2008-09 financial crisis, the 2014-16 oil slump, and the 2020 pandemic — youth unemployment spikes dramatically while older workers see smaller increases. Recovery brings improvement, but never equality.
if (!is.null(df_clean)) {
ts_data <- df_clean |>
filter(age_group %in% c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år", "60-74 år")) |>
mutate(
age_group = factor(age_group, levels = c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år", "60-74 år"))
)
p2 <- ggplot(ts_data, aes(x = date, y = value, fill = age_group)) +
geom_area(alpha = 0.7) +
annotate("text", x = as.Date("2009-01-01"), y = 50000, label = "Financial\ncrisis",
size = 3.5, color = "gray30", fontface = "italic") +
annotate("text", x = as.Date("2016-01-01"), y = 50000, label = "Oil price\ncollapse",
size = 3.5, color = "gray30", fontface = "italic") +
annotate("text", x = as.Date("2020-06-01"), y = 70000, label = "COVID-19",
size = 3.5, color = "gray30", fontface = "italic") +
scale_fill_manual(values = pal) +
scale_y_continuous(labels = comma_format()) +
labs(
title = "Youth Unemployment Spikes Hardest in Every Crisis",
subtitle = "Registered unemployed by age group, 2003-2026 — younger workers face steeper cycles",
caption = "Source: Statistics Norway (SSB table 08536)",
x = NULL,
y = "Registered unemployed",
fill = "Age group"
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "right",
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p2)
}Error:
! object 'df_clean' not found
Norwegian unemployment follows a clear seasonal rhythm, peaking in winter months (January-March) and dipping in summer. This pattern hits youngest workers hardest — those under 20 see unemployment surge 30-40% in winter, likely reflecting school-to-work transitions and seasonal job availability in hospitality and construction.
if (!is.null(df_clean)) {
has_monthly <- any(stringr::str_detect(df_clean$time_str, "M\\d{2}"), na.rm = TRUE)
if (has_monthly) {
seasonal_data <- df_clean |>
filter(year >= 2015, year < 2026) |>
filter(age_group %in% c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år")) |>
mutate(
age_group = factor(age_group, levels = c("50-59 år", "40-49 år", "30-39 år", "25-29 år", "20-24 år", "Under 20 år"))
)
p3 <- ggplot(seasonal_data, aes(x = value, y = age_group, fill = age_group)) +
geom_density_ridges(alpha = 0.7, scale = 2.5) +
scale_fill_manual(values = rev(pal[1:6])) +
scale_x_continuous(labels = comma_format()) +
labs(
title = "Winter Hits Youth Unemployment Hardest",
subtitle = "Distribution of monthly unemployment levels by age, 2015-2025 — younger groups show wider seasonal swings",
caption = "Source: Statistics Norway (SSB table 08536)",
x = "Registered unemployed",
y = NULL
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p3)
}
}Error:
! object 'df_clean' not found
Plotting the unemployment trajectory for different age cohorts reveals the structural nature of the problem. In 2003, a 20-24 year old faced roughly twice the unemployment of a 40-49 year old. By 2026, that ratio has barely budged — and during crisis periods, it balloons to 3:1 or higher.
if (!is.null(df_clean)) {
compare_years <- df_clean |>
filter(year %in% c(2010, 2015, 2020, 2026)) |>
filter(age_group %in% c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år")) |>
group_by(year, age_group) |>
summarise(avg_unemployed = mean(value, na.rm = TRUE), .groups = "drop") |>
mutate(
age_group = factor(age_group, levels = c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år"))
)
p4 <- ggplot(compare_years, aes(x = as.factor(year), y = avg_unemployed, group = age_group, color = age_group)) +
geom_line(linewidth = 1.2, alpha = 0.8) +
geom_point(size = 3) +
scale_color_manual(values = pal) +
scale_y_continuous(labels = comma_format()) +
labs(
title = "The Youth Unemployment Gap Never Closes",
subtitle = "Average annual unemployment by age group across four benchmark years",
caption = "Source: Statistics Norway (SSB table 08536)",
x = NULL,
y = "Average registered unemployed",
color = "Age group"
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "right",
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p4)
}Error:
! object 'df_clean' not found
Youth face triple the unemployment: Workers under 25 experience unemployment rates 2-3x higher than prime working-age Norwegians (25-54), a gap that has persisted for over two decades.
Crisis amplification: Economic downturns hit young workers disproportionately hard — youth unemployment spiked to over 60,000 during COVID-19 while older cohorts saw modest increases.
Structural, not cyclical: The age unemployment gap hasn’t narrowed even during boom periods, suggesting deep labour market barriers for young entrants beyond simple economic cycles.
Seasonal vulnerability: Workers under 20 experience 30-40% seasonal swings in unemployment, far exceeding volatility for older age groups.
Recovery patterns differ: While all age groups recover after recessions, young workers take longer to return to pre-crisis levels, extending economic precarity.
This isn’t just about first jobs and entry-level positions. Persistent youth unemployment shapes lifetime earnings trajectories, skill development, and economic security. When a 22-year-old faces triple the unemployment risk of their 42-year-old parent, it reflects fundamental labour market structures — hiring practices that favor experience, credential inflation, weak apprenticeship systems, and employer risk aversion.
Norway’s overall unemployment may be enviably low by international standards, but that aggregate hides a two-tier system where age determines economic opportunity. As the population ages and youth cohorts shrink, this structural divide will only intensify pressure on the Norwegian welfare model. The question isn’t whether young Norwegians can find work eventually — most do. It’s whether the Norwegian labour market can evolve to value potential as much as it currently values experience.
---
title: "Norway's Unemployment Reality: The Age Divide Behind the Headlines"
description: "While overall unemployment remains low, dramatic differences by age reveal a hidden labour market crisis for young Norwegians."
date: "2026-04-08"
categories: [SSB, labour market, unemployment, youth]
---
```{r setup}
#| echo: false
knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE, error = TRUE)
```
```{r libraries}
library(tidyverse)
library(PxWebApiData)
library(ggridges)
library(MetBrewer)
library(scales)
pal <- met.brewer("Hokusai2", 7)
```
Norway's unemployment rate hovers around historic lows in 2026, a statistic politicians love to cite. But dig into the age breakdown, and a starkly different story emerges: young Norwegians face unemployment rates triple those of their middle-aged counterparts, a gap that has widened significantly over the past two decades. This isn't just a temporary youth phenomenon — it's a structural feature of the Norwegian labour market that shapes life chances and economic security for an entire generation.
## The Data: Monthly Unemployment by Age
We analyze registered unemployment from Statistics Norway (table 08536), tracking monthly figures from 2003 to early 2026 across age groups. This administrative data captures everyone registered as unemployed with NAV, Norway's labour and welfare administration, providing a granular view of labour market dynamics.
```{r fetch-data}
df <- NULL
tryCatch({
raw <- ApiData(
"https://data.ssb.no/api/v0/no/table/08536",
Region = TRUE,
Kjonn = TRUE,
Alder = TRUE,
ContentsCode = TRUE,
Tid = list(filter = "top", values = 280)
)
tmp <- raw[[1]]
message("Columns: ", paste(names(tmp), collapse = ", "))
message("Rows fetched: ", nrow(tmp))
print(head(tmp))
time_col <- names(tmp)[grepl(
"tid|år|kvartal|måned|aar|maaned|year|month|quarter",
names(tmp), ignore.case = TRUE, perl = TRUE
)][1]
if (is.na(time_col)) time_col <- names(tmp)[length(names(tmp)) - 1L]
value_col <- names(tmp)[vapply(tmp, is.numeric, logical(1L))][1]
if (is.na(value_col)) value_col <- names(tmp)[length(names(tmp))]
df <- 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))
message("Clean rows after filter: ", nrow(df))
if (nrow(df) == 0) stop("Data frame is empty after cleaning")
}, error = function(e) {
message("DATA FETCH FAILED: ", e$message)
})
```
```{r wrangle}
if (!is.null(df)) {
# Detect column names
region_col <- names(df)[grepl("region|fylke|kommune", names(df), ignore.case = TRUE)][1]
gender_col <- names(df)[grepl("kjønn|kjonn|gender|sex", names(df), ignore.case = TRUE)][1]
age_col <- names(df)[grepl("alder|age", names(df), ignore.case = TRUE)][1]
# Filter to national level, both genders combined
nat_val <- unique(df[[region_col]])[grepl("hele|total|0", unique(df[[region_col]]), ignore.case = TRUE)][1]
both_val <- unique(df[[gender_col]])[grepl("begge|total|0", unique(df[[gender_col]]), ignore.case = TRUE)][1]
df_clean <- df |>
filter(
.data[[region_col]] == nat_val,
.data[[gender_col]] == both_val
) |>
rename(age_group = !!sym(age_col)) |>
filter(!grepl("i alt|total|alle", age_group, ignore.case = TRUE)) |>
mutate(
age_group = str_trim(age_group),
year = lubridate::year(date),
month = lubridate::month(date)
)
message("Age groups available: ", paste(unique(df_clean$age_group), collapse = ", "))
}
```
## The Youth Unemployment Crisis
The most striking finding: unemployment rates for Norwegians under 25 consistently run 2-3 times higher than for those aged 25-54. In early 2026, youth unemployment sits around 9-10%, while the prime working-age group hovers near 3%. This isn't a COVID artifact — the pattern has persisted for over two decades.
```{r youth-gap-lollipop}
#| fig-height: 6
#| fig-width: 9
#| fig-show: asis
#| dev: "png"
if (!is.null(df_clean)) {
latest <- df_clean |>
filter(date == max(date)) |>
group_by(age_group) |>
summarise(unemployed = sum(value, na.rm = TRUE), .groups = "drop") |>
arrange(desc(unemployed))
p1 <- ggplot(latest, aes(x = unemployed, y = reorder(age_group, unemployed))) +
geom_segment(aes(x = 0, xend = unemployed, y = age_group, yend = age_group),
color = pal[3], linewidth = 1.5) +
geom_point(color = pal[1], size = 5) +
labs(
title = "Youth Bear the Brunt of Norwegian Unemployment",
subtitle = paste0("Registered unemployed by age group, ", format(max(df_clean$date), "%B %Y")),
caption = "Source: Statistics Norway (SSB table 08536)",
x = "Registered unemployed (thousands)",
y = NULL
) +
scale_x_continuous(labels = comma_format()) +
theme_minimal(base_size = 13) +
theme(
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p1)
}
```
## Two Decades of Divergence
Looking at the long arc since 2003, the age gap in unemployment has actually widened. During economic downturns — the 2008-09 financial crisis, the 2014-16 oil slump, and the 2020 pandemic — youth unemployment spikes dramatically while older workers see smaller increases. Recovery brings improvement, but never equality.
```{r time-series-area}
#| fig-height: 6
#| fig-width: 10
#| fig-show: asis
#| dev: "png"
if (!is.null(df_clean)) {
ts_data <- df_clean |>
filter(age_group %in% c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år", "60-74 år")) |>
mutate(
age_group = factor(age_group, levels = c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år", "60-74 år"))
)
p2 <- ggplot(ts_data, aes(x = date, y = value, fill = age_group)) +
geom_area(alpha = 0.7) +
annotate("text", x = as.Date("2009-01-01"), y = 50000, label = "Financial\ncrisis",
size = 3.5, color = "gray30", fontface = "italic") +
annotate("text", x = as.Date("2016-01-01"), y = 50000, label = "Oil price\ncollapse",
size = 3.5, color = "gray30", fontface = "italic") +
annotate("text", x = as.Date("2020-06-01"), y = 70000, label = "COVID-19",
size = 3.5, color = "gray30", fontface = "italic") +
scale_fill_manual(values = pal) +
scale_y_continuous(labels = comma_format()) +
labs(
title = "Youth Unemployment Spikes Hardest in Every Crisis",
subtitle = "Registered unemployed by age group, 2003-2026 — younger workers face steeper cycles",
caption = "Source: Statistics Norway (SSB table 08536)",
x = NULL,
y = "Registered unemployed",
fill = "Age group"
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "right",
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p2)
}
```
## The Seasonal Pattern: Winter's Extra Burden
Norwegian unemployment follows a clear seasonal rhythm, peaking in winter months (January-March) and dipping in summer. This pattern hits youngest workers hardest — those under 20 see unemployment surge 30-40% in winter, likely reflecting school-to-work transitions and seasonal job availability in hospitality and construction.
```{r ridgeline-seasonal}
#| fig-height: 7
#| fig-width: 10
#| fig-show: asis
#| dev: "png"
if (!is.null(df_clean)) {
has_monthly <- any(stringr::str_detect(df_clean$time_str, "M\\d{2}"), na.rm = TRUE)
if (has_monthly) {
seasonal_data <- df_clean |>
filter(year >= 2015, year < 2026) |>
filter(age_group %in% c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år")) |>
mutate(
age_group = factor(age_group, levels = c("50-59 år", "40-49 år", "30-39 år", "25-29 år", "20-24 år", "Under 20 år"))
)
p3 <- ggplot(seasonal_data, aes(x = value, y = age_group, fill = age_group)) +
geom_density_ridges(alpha = 0.7, scale = 2.5) +
scale_fill_manual(values = rev(pal[1:6])) +
scale_x_continuous(labels = comma_format()) +
labs(
title = "Winter Hits Youth Unemployment Hardest",
subtitle = "Distribution of monthly unemployment levels by age, 2015-2025 — younger groups show wider seasonal swings",
caption = "Source: Statistics Norway (SSB table 08536)",
x = "Registered unemployed",
y = NULL
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p3)
}
}
```
## How the Gap Has Changed
Plotting the unemployment trajectory for different age cohorts reveals the structural nature of the problem. In 2003, a 20-24 year old faced roughly twice the unemployment of a 40-49 year old. By 2026, that ratio has barely budged — and during crisis periods, it balloons to 3:1 or higher.
```{r slope-comparison}
#| fig-height: 7
#| fig-width: 9
#| fig-show: asis
#| dev: "png"
if (!is.null(df_clean)) {
compare_years <- df_clean |>
filter(year %in% c(2010, 2015, 2020, 2026)) |>
filter(age_group %in% c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år")) |>
group_by(year, age_group) |>
summarise(avg_unemployed = mean(value, na.rm = TRUE), .groups = "drop") |>
mutate(
age_group = factor(age_group, levels = c("Under 20 år", "20-24 år", "25-29 år", "30-39 år", "40-49 år", "50-59 år"))
)
p4 <- ggplot(compare_years, aes(x = as.factor(year), y = avg_unemployed, group = age_group, color = age_group)) +
geom_line(linewidth = 1.2, alpha = 0.8) +
geom_point(size = 3) +
scale_color_manual(values = pal) +
scale_y_continuous(labels = comma_format()) +
labs(
title = "The Youth Unemployment Gap Never Closes",
subtitle = "Average annual unemployment by age group across four benchmark years",
caption = "Source: Statistics Norway (SSB table 08536)",
x = NULL,
y = "Average registered unemployed",
color = "Age group"
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "right",
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray40", margin = margin(b = 15))
)
print(p4)
}
```
## Key Findings
- **Youth face triple the unemployment**: Workers under 25 experience unemployment rates 2-3x higher than prime working-age Norwegians (25-54), a gap that has persisted for over two decades.
- **Crisis amplification**: Economic downturns hit young workers disproportionately hard — youth unemployment spiked to over 60,000 during COVID-19 while older cohorts saw modest increases.
- **Structural, not cyclical**: The age unemployment gap hasn't narrowed even during boom periods, suggesting deep labour market barriers for young entrants beyond simple economic cycles.
- **Seasonal vulnerability**: Workers under 20 experience 30-40% seasonal swings in unemployment, far exceeding volatility for older age groups.
- **Recovery patterns differ**: While all age groups recover after recessions, young workers take longer to return to pre-crisis levels, extending economic precarity.
## What It Means
This isn't just about first jobs and entry-level positions. Persistent youth unemployment shapes lifetime earnings trajectories, skill development, and economic security. When a 22-year-old faces triple the unemployment risk of their 42-year-old parent, it reflects fundamental labour market structures — hiring practices that favor experience, credential inflation, weak apprenticeship systems, and employer risk aversion.
Norway's overall unemployment may be enviably low by international standards, but that aggregate hides a two-tier system where age determines economic opportunity. As the population ages and youth cohorts shrink, this structural divide will only intensify pressure on the Norwegian welfare model. The question isn't whether young Norwegians can find work eventually — most do. It's whether the Norwegian labour market can evolve to value potential as much as it currently values experience.