This project examines demographic change in Germany using data from Destatis.
The analysis combines long-term trends in births and deaths with more recent data on international migration and population change. The aim is to examine how the components of demographic change have interacted over time.
The analysis covers two different time periods:
# Death
dth <- read_excel("12613-0002 death yrs.xlsx")
mask <- c(!(dth[5,] == "e"))
dth <- dth[,mask]
names(dth) <- dth[4,]
dth <- dth[5:80,]
dth <- dth %>%
mutate(across(2:4, as.numeric))
# Birth
brth <- read_excel("12612-0001 births.xlsx")
brth <- brth[,mask]
names(brth) <- brth[4,]
brth <- brth[5:80,]
brth <- brth %>%
mutate(across(2:4, as.numeric))
# COMPARE THE PROGRESSION OF DEATHS AND BIRTHS SINCE 1950
brth$Type <- "Birth"
dth$Type <- "Death"
dth_brth <- bind_rows(dth[,c(1,4,5)],brth[,c(1,4,5)])
ggplot(
dth_brth,
aes(
Year,
Total,
group = Type,
colour = Type
)
)+
geom_line(linewidth = 1)+
geom_point(size = 3)+
theme(
axis.text.x = element_text(angle = 90),
axis.text.y = element_text(angle = 45)
)+
labs(
title = "Progression of Deaths and Live Births since 1950"
)
The first part of the analysis looks at live births and deaths over a 75-year span.
The data show a broad decline in the number of live births over the period, with a dramatic descent starting around 1964 and stabilising around 1975. Deaths have followed a different trajectory, with the rapid growth of the 1950s and 60s stabilising around 1969 and slowly declining for three decades until another rapid increase comes around 2005. Although this increase was somewhat mimicked by an increase in live births, starting in 2021 the lines seem to be separating, with births decreasing much faster than deaths. This indicates that, in the more recent period, the difference between births and deaths has become an increasingly important component of Germany’s population dynamics. This change, however, is recent and more data needs to be collected before drawing conclusions. Overall, live birth rates show a clear strong negative tendency over the 75-year span covered by the dataset, while death rates show a positive tendency. This would suggest a decline in the overall population: a negative natural population change.
# Population
pop <- read_excel("12411-0001 population by year.xlsx")
pop <- pop[5:80,1:2]
names(pop) <- c("Year","Total")
pop$Year <- gsub("-12-31","",pop$Year)
pop$Total <- as.numeric(pop$Total)
ggplot(
pop,
aes(
Year,
Total,
group = 1
)
)+
geom_line(linewidth = 1)+
geom_point(size = 3)+
labs(
title = "Population flows since 1950"
)+
theme(
axis.text.x = element_text(angle = 90)
)
A population, however, can grow because more people are born, because fewer people die, but also because people migrate into the country, or through a combination of these processes. The graph shows that the population of the country has a strong positive tendency since the start of the dataset, contrasting strongly with the negative natural population change described above. Around 1989, moreover, there is a dramatic boom which stabilises quickly but has lasting effects on the population. Population size alone does not explain why the population changes, but this discrepancy suggests an effect of migration.
This motivates a decomposition of annual population change.
For the period 2000–2025, I combine three measures:
Conceptually:
Population change = natural population change + migration balance
# Migration per year
migy <- read_excel("12711-0005 migration with year.xlsx")
mask <- c(!(migy[7,] == "e"))
migy <- migy[,mask]
for (i in 2:10) {
migy[5,i] <- paste0(migy[5,i],"_g") # where g = German
}
for (i in 11:19) {
migy[5,i] <- paste0(migy[5,i],"_f") # where f = foreigner
}
for (i in 20:28) {
migy[5,i] <- paste0(migy[5,i],"_t") # where t = total
}
migy[5,1] <- "Year"
mask <- c(2,3,4,11,12,13,20,21,22)
for (i in mask) {
migy[5,i] <- paste0(migy[5,i],"_m") # where m = man
}
mask <- c(5,6,7,14,15,16,23,24,25)
for (i in mask) {
migy[5,i] <- paste0(migy[5,i],"_w") # where w = woman
}
mask <- c(8,9,10,17,18,19,26,27,28)
for (i in mask) {
migy[5,i] <- paste0(migy[5,i],"_t") # where t = total
}
names(migy) <- migy[5,]
migy <- migy[7:32,]
migy <- migy %>%
mutate(across(2:28,as.numeric))
# CALCULATE POPULATION CHANGE ~ BIRTH-DEATH + MIGRATION BALANCE
dth_brth_wide <- merge(
dth_brth[dth_brth$Type == "Death", c("Year", "Total")],
dth_brth[dth_brth$Type == "Birth", c("Year", "Total")],
by = "Year",
suffixes = c("_Death", "_Birth")
)
dth_brth_wide$Natural_population_change <- dth_brth_wide$Total_Birth - dth_brth_wide$Total_Death
# the migy dataset only contains data since 2000
dth_brth_mig <- merge(
dth_brth_wide[dth_brth_wide$Year >= 2000,c("Year","Natural_population_change")],
migy[,c("Year","Migration balance_t_t")],
by = "Year"
)
dth_brth_mig$Population_change <- dth_brth_mig$Natural_population_change + dth_brth_mig$`Migration balance_t_t`
dth_brth_mig <- rename(
dth_brth_mig,
Migration_balance = `Migration balance_t_t`
)
data_dbm <- dth_brth_mig %>%
pivot_longer(
cols = -`Year`,
names_to = "Type",
values_to = "Count"
)
ggplot() +
# Components
geom_col(
data = data_dbm |>
filter(Type != "Population_change"),
aes(
x = Year,
y = `Count`,
fill = Type
),
position = "identity"
) +
# Result
geom_line(
data = data_dbm |>
filter(Type == "Population_change"),
aes(
x = Year,
y = `Count`,
group = 1,
linetype = "Population change"
),
linewidth = 1
) +
geom_point(
data = data_dbm |>
filter(Type == "Population_change"),
aes(
x = Year,
y = `Count`
),
size = 2
) +
labs(
title = "Components of population change in Germany",
x = "Year",
y = "People",
fill = "Component",
linetype = "Result"
) +
theme(
axis.text.x = element_text(angle = 90)
)
The visualisation shows a clear contrast between the two components. Natural population change is negative throughout much of the period, while the migration balance is generally positive, most strongly in the periods between 2014 and 2016 and the early 2020s.
The resulting population change in Germany since 2000, therefore, depends heavily on the relationship between natural population flows and migration balance, with a heavy effect attributable to the latter.
# Migration
migc <- read_excel("12711-0008 migration with country.xlsx")
mask <- c(!(migc[7,] == "e"))
migc <- migc[,mask]
for (i in 2:4) {
migc[4,i] <- paste0(migc[4,i],"_g") # where g = German
}
for (i in 5:7) {
migc[4,i] <- paste0(migc[4,i],"_f") # where f = foreigner
}
for (i in 8:10) {
migc[4,i] <- paste0(migc[4,i],"_t") # where t = total
}
migc[4,1] <- "Country"
names(migc) <- migc[4,]
migc <- migc[7:125,]
mask <- c(!(migc[,2] == "-")) #row 61 has NA values
migc <- migc[mask,]
migc <- migc %>%
mutate(across(2:10,as.numeric))
ggplot(
migc,
aes(
reorder(Country,`Migration balance_t`),
`Migration balance_t`
)
)+
geom_col()+
theme(
axis.text.x = element_text(angle = 90)
)+
coord_flip()+
labs(
title = "Countries with higher to lower migration balance in 2025",
x = "Country",
y = "Migration balance"
)
The bar chart above shows that several of the countries with the highest positive migration balances in 2025 are countries that have experienced armed conflict, political instability, or other forms of displacement. While the data cannot establish the causes for migration to Germany, this pattern provides a direction for further investigation into the relationship between conflict, displacement, and migration. It also illustrates the substantial role that international migration can play in Germany’s population development. Despite some broad categories in the dataset, such as ‘Rest of Asia’, the data provides a useful starting point for examining migration flows by country of origin and their demographic effects.