Want to share your content on python-bloggers? click here.
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
function showError(el, error){ el.innerHTML = ('
JavaScript Error: ' + error.message + '
' + "
This usually means there's a typo in your chart specification. " + "See the javascript console for the full traceback.
" + '
'); throw error; } const el = document.getElementById('cdr_yearly'); vegaEmbed("#cdr_yearly", spec, embedOpt) .catch(error => showError(el, error)); })(vegaEmbed);
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 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
Want to share your content on python-bloggers? click here.