Marimekko Charts with Python’s Matplotlib | by Thiago Carvalho | Towards Data Science

Commonly referred to as a Mosaic plot, Spine plot, or just Mekko. This chart is often used, among other applications, as market maps to visualize industries segmented by types of customers, regions, and many other variables.
In essence, they are just a stacked bar chart where the bar widths are used to display another variable, usually the whole group’s proportion to the total.
Proportions are great for discovering patterns and comparing groups with different sizes. This article will explore how to improve the way we visualize them with Marimekko charts in Matplotlib.
We’ll go through a quick example of Marimekko charts to illustrate their usability, and by the end, we’ll see how to plot them more systematically.
First, we’ll import the libraries we’ll use.
import pandas as pdimport matplotlib.pyplot as pltimport numpy as np
In our company, 30 people in department A and 20 in department B want to keep working remotely.
y = np.array([.3, .4, .7, .8]) * np.array([100., 50., 100., 300.])x_label = ['a','b','c', 'd']plt.bar(x_label, y)plt.show()
That information alone isn’t helpful and could be very misleading since we don’t know how many people work in each department. Analyzing the proportions would be more insightful.
y = [.3, .4, .7, .8]x_label = ['a','b','c', 'd']plt.bar(x_label, y) # bottom barplt.bar(x_label, np.ones(len(y))-y, bottom=y) # top barplt.ylim(0,1)plt.yticks([0, 0.25, 0.5, 0.75, 1], ['0%', '25%', '50%', '75%', '100%'])plt.show()
But let’s say we also want to visualize all departments as one. The above chart might mislead us too. We could say it’s an overall “divided” opinion; half the departments are above 50% and the other half below, which is wrong.
To understand the departments individually and as a whole, we would require multiple visualizations or different approaches such as treemaps and Marimekko charts.
fig, ax = plt.subplots(1)y = [.3, .4, .7, .8]x = [100, 50, 100, 300]x_label = ['a','b','c', 'd']width = [i/sum(x) for i in x]# calculate x coordinates based on the width of the previous bars# same as: [0, width[0], width[0] + width[1], width[0] + width[1] + width[2]]adjusted_x, temp = [0], 0for i in width[:-1]:    temp += i    adjusted_x.append(temp)# Marimekko chartplt.bar(adjusted_x, y, width=width, align='edge', edgecolor='black')plt.bar(adjusted_x, np.ones(len(y))-y, bottom=y, width=width, align='edge', edgecolor='black')# x and y ticks (%)ax.set_yticks([0, 0.25, 0.5, 0.75, 1])ax.set_yticklabels(['0%', '25%', '50%', '75%', '100%'])ax.set_xticks([0, 0.25, 0.5, 0.75, 1])ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'])plt.ylim(0,1)plt.xlim(0,1)# twin y-axis to draw x-ticks at the topaxy = ax.twiny()axy.set_xticks([(width[i]/2)+ v for i, v in enumerate(adjusted_x)])axy.set_xticklabels(x_label, fontsize=14)plt.show()
That visualization helps us effortlessly visualize the proportions within the departments and the proportion each department represents of the total.
We can clearly see that the vast majority of people in our company want to keep working remotely, and still note that the smaller departments seem to be less keen on that idea.
Now for more systematic use of Marimekko charts, let’s try using a Pandas data frame. This way, we can switch between columns, rows, and datasets more easily.
The datasets I’ll use are from World Bank Open Data. I want to visualize the proportions for some indicators such as rural population, electricity access, and literacy rate. But I also want to illustrate the proportions of population those countries represent to the total.
World bank datasets may contain some rows with regions, sub-regions, socio-economical groups and others, but we want to compare countries with other countries. We’ll need some countries' lists to clean our data frames, such as All Countries, Latin America & Caribbean, European Union, and others.
We also need to consider that we won’t label every country; that would be too cluttered, so we’ll also need to select some countries to highlight.
Since those are extensive, I’ll leave a link to this part of the code here.
Now, let’s create some dictionaries to interact with all those lists and values more easily.
var_dict = {'Rural Population':             {'file':'API_SP.RUR.TOTL.ZS_DS2_en_csv_v2_2166125.csv',              'label':'Rural Pop. (%)',              'legend':['Rural Population', 'Urban Population'],              'min_year':1960,              'max_year':2019},                         'Access to Electricity':             {'file':'API_EG.ELC.ACCS.ZS_DS2_en_csv_v2_2164123.csv',              'label':'Access to Electricity (%)',              'legend':['Access to Electricity',                         'No Access to Electricity'],              'min_year':1990,              'max_year':2018},                        'Literacy':             {'file':'API_SE.ADT.LITR.ZS_DS2_en_csv_v2_2163525.csv',              'label':'Literacy Rate (age +15)',              'legend':['Literate Pop.', 'Illiterate Pop.'],              'min_year':1970,              'max_year':2018},                        'Unemployment':             {'file':'API_SL.UEM.TOTL.ZS_DS2_en_csv_v2_2163385.csv',              'label':'Unemployment (ILO Estimate)',              'legend':['Unemployed', 'Employed'],              'min_year':1991,              'max_year':2019}}countries_dict = {"All":all_countries,                  "European Union": eu,                   "Latin America & Caribbean": la_c,                  "East Asia and Pacific": eap,                  "Europe and Central Asia": eca,                  "Middle East and North Africa": mena,                  "North America": na,                  "South Asia" : sa,                  "Sub-Saharan Africa": ssa,                  "Custom":False}highlights_dict = {"All":highlight_all,                  "European Union": highlight_eu,                   "Latin America & Caribbean": highlight_la_c,                  "East Asia and Pacific": [],                  "Europe and Central Asia":[],                  "Middle East and North Africa":[],                  "North America":[],                  "South Asia" : [],                  "Sub-Saharan Africa":[],                  "Custom":[]}
We have everything set to interact more efficiently with our datasets. Now let’s set the variables and start cleaning.
year = 2018countries_select = "All"indicator = 'Rural Population'countries = countries_dict[countries_select]highlight = highlights_dict[countries_select]
The population data frame will be the same for whatever indicator we’re plotting. Still, the indicator data frame will depend on our selection from the previous code, in this case, the Rural population.
pop_file = 'API_SP.POP.TOTL_DS2_en_csv_v2_2163507.csv'df = pd.read_csv('../data/'+pop_file)var_df = pd.read_csv('../data/'+var_dict[indicator]['file'])df = df.merge(var_df,               left_on='Country Name',               right_on='Country Name',               suffixes=['_pop', '_var_df'])
The merged data frame will have a column for every year of the population and the variable indicator.
We’ll remove the countries that are not in our list, remove rows without values, and sort the rows by the indicator values.
# remove non-countriesdf = df[df['Country Name'].isin(countries)]# remove rows with empty values for the indicator or populationdf = df[(df['{}_var_df'.format(year)].notna() & df['{}_pop'.format(year)].notna())]# sort rows by the indicator values df.sort_values('{}_var_df'.format(year), inplace=True)
Next, we need to define Y, which is the indicator value for each country.
The width of the bars will be the population. The X is the cumulative sum of the widths.
# get yy = df['{}_var_df'.format(year)]/100# get the width of the bars (population / 1mi)width = df['{}_pop'.format(year)].values / 1000000# calculate the x position of each country# x is the cummulative sum of all previous widthsx = [0]temp = 0for i in width[:-1]:    temp += i    x.append(temp)
The last variables we’ll define relate to the x ticks and the country labels. We’ll use the highlight list to plot the ticks, get the country’s X and half the width to position the labels.
# create lists with the x ticks positions and labels# based on a list of countries we want to highlightxticks_pos = []xticks_labels = [] for i, v in enumerate(df['Country Name']):    if v in highlight:        xticks_pos.append(width[i]/2 + x[i])        xticks_labels.append(v)
Awesome, we have X, Y, width, x-tick positions and labels.
We finished the first part of our function. Now we can start plotting our bar chart and using the variables to draw a Marimekko chart.
By default, Matplotlib aligns the bars at the center of the x positions, and we need them at the edge. We’ll also need to add a contour to each bar so we can distinguish them.
fig, ax = plt.subplots(1, figsize=(24,12))# plot proportionsplt.bar(x, y,         width=width,         align='edge',         edgecolor='w',         linewidth=0.5,         color='#559A65')# plot ones - proportions at the topplt.bar(x, np.ones(len(y))-y, bottom=y,         width=width,         align='edge',         edgecolor='w',         linewidth = 0.5,         color='#477C91')
Next, we need to fix the x and y limits to remove the white space and replace the x and y ticks with some proper values.
To plot both the percentages and labels on the x-axis, we’ll need to write the first, set a twin y-axis and write the other.
fig, ax = plt.subplots(1, figsize=(24,12))plt.bar(x, y,         width=width,         align='edge',         edgecolor='w',         linewidth=0.5,         color='#559A65')plt.bar(x, np.ones(len(y))-y, bottom=y,         width=width,         align='edge',         edgecolor='w',         linewidth = 0.5,         color='#477C91')# xy limitsplt.xlim(0, x[-1]+width[-1])plt.ylim(0,1)# xy ticksplt.xticks(xticks_pos, xticks_labels, fontsize=14)plt.yticks(np.arange(0,1.1,0.25),           ['{:.0f}%'.format(i*100) for i in np.arange(0,1.1,0.25)],          fontsize=14)# twin y-axis to draw x-ticks at the topaxy = ax.twiny()# max value on the x axismax_x = (x[-1] + width[-1])# get the values for 0%, 25%, 50%, 75%, and 100%x_ticks_percent = np.arange(0, max_x+1, max_x/4)# set ticks and labelsaxy.set_xticks(x_ticks_percent)axy.set_xticklabels(['{:.0f}%'.format(i*100) for i in x_ticks_percent/max_x],fontsize=14)plt.show()
That’s it. We need to add a legend, title, some notes and get all this code in a function to switch through the indicators and countries easily.
def marimekko(countries_select, indicator,               year, countries, highlight):    ##################    ###### DATA ######    ##################    pop_file = 'API_SP.POP.TOTL_DS2_en_csv_v2_2163507.csv'    df = pd.read_csv('../data/'+pop_file)    var_df = pd.read_csv('../data/'+var_dict[indicator]['file'])    df = df.merge(var_df,                   left_on='Country Name',                   right_on='Country Name',                   suffixes=['_pop', '_var_df'])        # remove non-countries    df = df[df['Country Name'].isin(countries)]    # remove rows with empty values for the indicator or population    df = df[(df['{}_var_df'.format(year)].notna() & df['{}_pop'.format(year)].notna())]    # sort rows by the indicator values     df.sort_values('{}_var_df'.format(year), inplace=True)    # get y    y = df['{}_var_df'.format(year)]/100    # get the width of the bars (population / 1mi)    width = df['{}_pop'.format(year)].values / 1000000    # calculate the x position of each country    # x is the cummulative sum of all previous widths    x = [0]    temp = 0    for i in width[:-1]:        temp += i        x.append(temp)    # create lists with the x ticks positions and labels    # based on a list of countries we want to highlight    xticks_pos = []    xticks_labels = []     for i, v in enumerate(df['Country Name']):        if v in highlight:            xticks_pos.append(width[i]/2 + x[i])            xticks_labels.append(v)                ##########################    ########## PLOT ##########    ##########################    # define figure and axis    fig, ax = plt.subplots(1, figsize=(24,12))    # bars    plt.bar(x, y, align='edge',             width=width, edgecolor='w',             linewidth = 0.5, color='#559A65')    plt.bar(x, np.ones(len(y))-y, bottom=y,             align='edge', width=width, edgecolor='w',             linewidth = 0.5, color='#477C91')        # Add notes (Total population and Year)    total_pop = df['{}_pop'.format(year)].sum()    if total_pop > 1000000000:        total_pop = '{:.2f} bi'.format(total_pop/1000000000)    elif total_pop > 1000000:        total_pop = '{:.2f} mi'.format(total_pop/1000000)    else:        total_pop = '{:.2f}'.format(total_pop)    notes = 'Total population: {} | Year: {}'.format(total_pop,                                                     year)    plt.text(x[-1], 1.07, notes, ha='right', fontsize=20)    # title and legend    plt.legend(var_dict[indicator]['legend'],                ncol=2, loc='upper left', fontsize=20,               frameon=False, bbox_to_anchor=(0,1.11))    plt.title('{} by Country\n\n'.format(var_dict[indicator]['label']),               loc='left', fontsize=24)        # xy limits    plt.xlim(0, x[-1]+width[-1])    plt.ylim(0,1)    # xy ticks    plt.xticks(xticks_pos, xticks_labels, fontsize=14)    plt.yticks(np.arange(0,1.1,0.25),               ['{:.0f}%'.format(i*100) for i in np.arange(0,1.1,0.25)],              fontsize=14)    # twin y-axis to draw x-ticks at the top    axy = ax.twiny()    # max value on the x axis    max_x = (x[-1] + width[-1])    # get the values for 0%, 25%, 50%, 75%, and 100%    x_ticks_percent = np.arange(0, max_x+1, max_x/4)    # set ticks and labels    axy.set_xticks(x_ticks_percent)    axy.set_xticklabels(['{:.0f}%'.format(i*100) for i in x_ticks_percent/max_x],                         fontsize=14)    plt.savefig('chart.png')
Awesome, let’s try our new function.
year = 2018countries_select = "All"indicator = 'Rural Population'countries = countries_dict[countries_select]highlight = highlights_dict[countries_select]marimekko(countries_select, indicator, year, countries, highlight)
year = 2000countries_select = "European Union"indicator = 'Unemployment'countries = countries_dict[countries_select]highlight = ['Belgium', 'Czech Republic', 'France', 'Germany',              'Italy', 'Poland', 'Portugal', 'Spain', 'Sweden']marimekko(countries_select, indicator, year, countries, highlight)
Great! We have an excellent visualization that allows us to check the country's proportion and the overall proportion. All that in a function so we can easily switch between different indicators and sets of countries.
Now it’s up to you. You can improve the design, add interactivity, or use it in a notebook or script.
The code for the above examples is available here.
More Python DataViz tutorials.
Thanks for reading my article. I hope it was helpful!