Skip to content

Bamboo Weekly #193: World economic outlook (solutions)

Get better at: Excel files, plotting with Plotly, Marimo widgets, window functions, and sorting.

Bamboo Weekly #193: World economic outlook (solutions)

This week, we looked at the International Monetary Fund's latest World Economic Outlook, which came out earlier this month. It says the world economy is currently experiencing contradictory forces:

In other words, things are interesting. And complicated. And messy.

Fortunately, we have data to help make things a bit easier to understand, and to compare countries and regions around the world.

Data and five questions

The data comes from the IMF's World Economic Outlook, whose main page is at https://data.imf.org/datasets/WEO. You can download a PDF report, but we'll be looking at an Excel file with the full data. You can download it from

https://data.imf.org/-/media/iData/External-Storage/Documents/0FA3DC49A04D446E9D69DBEB29979E1C/en/WEOOct2026all.xlsx

Paid subscribers, both to Bamboo Weekly and to my LernerPython+data membership program (https://LernerPython.com) get all of the questions and answers, as well as downloadable data files, downloadable versions of my notebooks, one-click access to my notebooks, and invitations to monthly office hours.

Learning goals for this week include: Working with Excel files, plotting with Plotly, Marimo widgets, window functions, and sorting.

Here are the solutions and explanations for this week's five questions:

Read the "Countries" sheet from the Excel file into a Pandas data frame. Then, looking at GDP per capita in US dollars with current prices in 2026 (NGDPDPC): What are the mean and median values across all countries, and what does that tell us? Create a boxplot with all of the per-capita GDP levels; what does that show? What 10 countries have the highest/lowest per-capita GDP? Which have grown the most in the last 25 years (for countries with values from 25 years ago)?

For starters, I loaded Pandas and Plotly:

import pandas as pd
from plotly import express as px

Next, I loaded the Excel file into a data frame, using read_excel. Fortunately, the sheet of interest to me ("Countries") was in good shape. The only tricky thing was to specify that sheet:

filename = 'data/bw-193-WEOOct2026all.xlsx'

df = (
    pd
    .read_excel(filename, sheet_name='Countries')
)

This resulted in a data frame with 8,668 rows and 81 columns.

Next, we wanted to calculate the mean and median per-capita GDP in 2026 for each country.

I started by using set_index to put the COUNTRY column (i.e., the name of each country) into the data frame's index.

Then I used .loc and pd.col to find only those rows in which INDICATOR.ID is NGDPDPC (and not, as I mistakenly indicated in the questions, NGDPD). I used the two-argument version of loc to retrieve not only specific rows, but also specific columns – in this case, the column named 2026.

This turned out to be a bit trickier than I expected, in part because of Marimo's complaints: Basically, Pandas allows you to have columns with integer names. However, Marimo complains about it, and it's probably right to do that, even if it's annoying.

So instead of retrieving a single column (i.e., a series), I put the column name in [], thus giving me a one-column data frame. I was thus able to invoke set_axis to change the name to a string.

Finally, I used agg to invoke two aggregation methods, mean and median, on the per-capita GDP numbers for all countries, and invoked round to keep the numbers easy to read:


(
    df
    .set_index('COUNTRY')        
    .loc[ pd.col('INDICATOR.ID') == 'NGDPDPC', [2026]]
    .set_axis(['2026'], axis='columns')
    .agg(['mean', 'median'])
    .round(2)    
)

The result:

	2026
mean	22846.93
median	9496.7

In other words, the mean per-capita GDP is about $23,000. But the median is about $9,500.

That they are so different is a classic example of the difference between mean and median: The mean changes when you have a few values that are either very large or very small. In this case, there are a few countries with very large numbers, and they pull up the mean. The median reflects the halfway point between countries – which is, as we see, significantly lower.

Next, I wanted a boxplot (sometimes known as a "box-and-whiskers" plot), to understand the spread of the numbers. I used pipe to pass the data frame to px.box, Plotly's box-plotting function:

(
    df
    .set_index('COUNTRY')        
    .loc[ pd.col('INDICATOR.ID') == 'NGDPDPC', [2026]]
    .set_axis(['2026'], axis='columns')
    .pipe(px.box)
)

The result:

Notice the horizontal line in the middle of the box? That's the median. By definition, half of the countries in the world have a per-capita GDP below that line, and half have one that's above. We can also see the "max" line at about $75,000/year. But in this case, it's not really the maximum, because there are so many high-level outliers (i.e., rich countries).

Remember that these numbers represent the entire country. A wealthy country will often have very poor people, and even the poorest countries have very wealthy people.

If you prefer, you can see these numbers in tabular format with describe:

(
    df
    .set_index('COUNTRY')        
    .loc[ pd.col('INDICATOR.ID') == 'NGDPDPC', [2026]]
    .set_axis(['2026'], axis='columns')
    .describe()
    .round(2)
)

The result:

	2026
count	191.0
mean	22846.93
std	31671.78
min	510.37
25%	3575.56
50%	9496.7
75%	29636.88
max	230417.11

Next, I asked which countries have the 10 highest and lowest per-capita GDP. I answered this by invoking both nlargest and nsmallest. Since I wanted both of them, I invoked them indirectly, via agg. However, by default, both methods return only five countries. By passing n=5 as a keyword argument to agg, both methods got their argument, and were thus able to return 10 rows:


(
    df
    .set_index('COUNTRY')        
    .loc[ pd.col('INDICATOR.ID') == 'NGDPDPC', [2026]]
    .set_axis(['2026'], axis='columns')
    ['2026']
    .round(2)
    .agg(['nlargest', 'nsmallest'], n=10)
)

The result:

COUNTRY	nlargest	nsmallest
Liechtenstein, Principality of	230417.11	
Luxembourg	155732.85	
Ireland	127837.36	
Switzerland	124478.24	
Norway	115188.42	
Singapore	109116.83	
Iceland	107464.45	
United States	95212.14	
Denmark	82713.54	
Netherlands, The	78535.01	
Burundi		510.37
Yemen, Republic of		566.84
Sudan		596.6
South Sudan, Republic of		641.95
Madagascar, Republic of		649.01
Mozambique, Republic of		672.6
Malawi		739.76
Niger		773.66
Somalia		849.89
Central African Republic		891.17

Finally, which countries have shown the greatest growth in per-capita GDP in the last 25 years? To find that, I retrieved the data from both 2001 and 2026, and then used diff to subtract one from the other. I then used nlargest to find the 10 biggest winners:


(
    df
    .set_index('COUNTRY')        
    .loc[ pd.col('INDICATOR.ID') == 'NGDPDPC', [2021, 2026]]
    .set_axis(['2021', '2026'], axis='columns')
    .diff(axis='columns')
    ['2026']
    .round(2)
    .nlargest(10)
)

The result:

COUNTRY	2026
Iceland	34243.65
Macao Special Administrative Region, People's Republic of China	32210.38
Guyana	29371.0
Liechtenstein, Principality of	29119.57
Singapore	28231.99
Switzerland	27578.62
Ireland	24207.21
United States	23785.64
Israel	20549.03
Luxembourg	19542.36

Some of these were very much expected, such as Iceland, Singapore, Ireland, and Israel. I was surprised that some of them, such as Singapore, Switzerland, the United States, and Luxembourg, had grown so much. And I definitely didn't expect Macao or Guyana, but here we are.

Create a choropleth map, showing the GDP per capita in US dollars. In Marimo, let the user move a slider to change which year's data will be displayed.

The first step in solving this problem was to create a slider using Marimo's UI widgets. I did that with mo.ui.slider, passing it values for when to start (2000), when to stop (2026), the step size (1), and whether to show the current value (True). I then made sure to let year_slider be displayed to the user in the notebook:

year_slider = mo.ui.slider(start=2000, stop=2026, step=1, show_value=True)
year_slider

With this in place, I put together my map:

The code:

(
    df
    .set_index('COUNTRY')        
    .loc[ pd.col('INDICATOR.ID') == 'NGDPDPC', year_slider.value]
    .round(2)
    .reset_index()
    .pipe(px.choropleth, scope='world', locations='COUNTRY', locationmode='country names', color=year_slider.value, title=f'Per-capita GDP in {year_slider.value}')
)

Sample output:

The extremely large number of blue countries — and the very tiny number of lighter-colored countries — demonstrates just how poor so much of the world is, at least when put on a scale with rich countries.

But what if we just want to stack-rank them by per-capita GDP? We can do that with the rank method applied in the middle of our method chain:

(
    df
    .set_index('COUNTRY')        
    .loc[ pd.col('INDICATOR.ID') == 'NGDPDPC', year_slider.value]
    .round(2)
    .rank()
    .reset_index()
    .pipe(px.choropleth, scope='world', locations='COUNTRY', locationmode='country names', color=year_slider.value, title=f'Per-capita GDP in {year_slider.value}')
)

The result is now a bit different: