In 2021, I wrote a blog post about Swiss mortality and it turned out to be among the most read posts I have written so far. Four years later, I think it’s time for an update with the following improvements:
- 4 years more observations, in particular after Covid-19
- chart with weekly CDR for seasonality effects
- polars
- interactive altair/vega charts here in the wordpress post 🚀
We use the exact same data sources, all publicly available:
- Short-term Mortality Fluctuations from the Human Mortality Database
- deaths per month, BFS-Nummer px-x-0102020206_111, from the Federal Statistical Office of Switzerland
- population BFS-Nummer px-x-0102030000_101, from the Federal Statistical Office of Switzerland
As 4 years ago, I caution against any misinterpretation: I show only crude death rates (CDR) which do not take into account any demographic shift like changing distributions of age.
The first figure shows the CDR per year for several countries, Switzerland (CHE) among them. We fetch the data from the internet, pick some countries of interest, filter on combined gender only (pl.col("Sex") == pl.lit("b")
with “b” for both), aggregate and plot. Thanks to this blog post , I was able to integrate the altair/vega-light charts created in Python directly into this wordpress text. The difference is that I exported the altair charts as html and directly copy&pasted it into this text as html block because the html also contains the data to be plotted (as opposed to the default json output).
from datetime import datetime import polars as pl import altair as alt # https://altair-viz.github.io/user_guide/large_datasets.html alt.data_transformers.enable("vegafusion") df_original = pl.read_csv( "https://www.mortality.org/File/GetDocument/Public/STMF/Outputs/stmf.csv", skip_rows=2, # Help polars a bit: schema_overrides={ "D65_74": pl.Float64, "D75_84": pl.Float64, "D85p": pl.Float64, "DTotal": pl.Float64, }, ) df_mortality = df_original.filter( # Select country of interest and only "both" sexes. # Note: Germany "DEUTNP" and "USA" have short time series. pl.col("CountryCode").is_in(["CAN", "CHE", "FRATNP", "GBRTENW", "SWE"]), pl.col("Sex") == pl.lit("b"), ).with_columns( # Change to ISO-3166-1 ALPHA-3 codes CountryCode=pl.col("CountryCode").replace( {"FRATNP": "FRA", "GBRTENW": "England & Wales"}, ), # Create population pro rata temporis (exposure) to ease aggregation Population=pl.col("DTotal") / pl.col("RTotal"), ).with_columns( # We think that the data uses ISO 8601 week dates and we set the weekday # to 1, i.e., Monday. Date=( pl.col("Year").cast(pl.String) + "-W" + pl.col("Week").cast(pl.String).str.zfill(2) + "-1" ).str.to_date(format="%G-W%V-%u") ) chart = ( alt.Chart( df_mortality.filter(pl.col("Year") <= 2024) # The Covid-19 peaks in 2020 are better seen on weekly resolution. .group_by("Year", "CountryCode") .agg(pl.col("Population").sum(), pl.col("DTotal").sum()) .with_columns( CDR=pl.col("DTotal") / pl.col("Population"), ) ) .mark_line(tooltip=True) .encode( x="Year:T", y=alt.Y("CDR:Q", scale=alt.Scale(zero=False)), color="CountryCode:N", ) .properties( title="Crude Death Rate per Year", width=400, # default 300 ) .interactive() ) # chart.save("crude_death_rate.html") chart
Crude death rate (CDR) for Canada (CAN), Switzerland (CHE), England & Wales, France (FRA) and Sweden (SWE). Data as of 05.07.2025.
Note that the y-axis does not start at zero. Nevertheless, we see values between 0.007 and 0.0105. The big spike that we observed in the beginning of 2021 in the old post is now flattened. In 2021 all those countries showed a CDR of over 0.01, now most are below 0.09 in 2020. This shows that the data as of February 2021 was incomplete as I mentioned. Now we have the complete picture and it looks better—fortunately!
This time, I also add a chart with weekly CDRs to demonstrate the seasonality effects.
chart = ( alt.Chart( df_mortality.filter( pl.col("CountryCode") <= pl.lit("CHE"), # Last 12 years pl.col("Year") > pl.col("Year").max() - 12, ).with_columns( CDR=pl.col("DTotal") / pl.col("Population"), ) ) .mark_line(tooltip=True) .encode( x="Date:T", y=alt.Y("CDR:Q", scale=alt.Scale(zero=True)), ) .properties( title="Crude Death Rate per Week for Switzerland", width=400, # default 300 ) .interactive() ) # chart.save("crude_death_rate_per_week.html") chart
Weekly crude death rate (CDR) of Switzerland
This shows a very regular seasonal pattern with a peak in every winter.
As last time, we also collect data the deaths and population of Switzerland for more than the past 100 years. Thanks to the good Swiss government institutes that make that possible. Again, the CDR of both data sources agree within less than 1% relative error.
Have a look at the notebook linked below to see the code for this chart.
Crude death rate (CDR) for Switzerland from 1901 to 2023
Note again that the left y-axis does not start at zero, but the right y-axis does. One can see several interesting facts:
- The Swiss population is and always was growing for the last 120 years—with the only exception around 1976.
- The Spanish flu between 1918 and 1920 caused by far the largest peak in mortality in the last 120 years.
- Covid-19 caused a significant increase of mortality in 2020-2022 that seems now gone (should have added the year 2024 in the last chart, but have a look at the first one).
- The second world war is not visible in the mortality of Switzerland.
- Overall, the mortality is decreasing, but this decrease seems to have flattened in the last decade.
The Python notebook can be found at https://github.com/lorentzenchr/notebooks/blob/master/blogposts/2025-07-06%20swiss_mortality.ipynb.
Leave a Reply