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

Bar charts are by far my favourite visualization technique. They are very versatile, usually easy to read, and relatively straightforward to build.
Just like any visualization, they do have some disadvantages as well. For example, they struggle with scalability.
Too many bars in a bar chart make it confusing and hard to read. That is more than ordinary when we’re working with hierarchical categories — In other words, when we have groups and subgroups that we need to visualize.
Stacked bars are a great alternative in those cases, allowing us to compare and analyze those groups' composition.
In this article, we’ll explore how to build those visualizations with Python’s Matplotlib.
I’ll be using a simple dataset that holds data on video game copies sold worldwide. The dataset is quite outdated, but it’s suitable for the following examples.
import numpy as npimport pandas as pdimport matplotlib.pyplot as plt
Let’s read and get a look at it.
df = pd.read_csv('../data/vgsales.csv')df.head()
I want to visualize the total number of copies sold by platform and analyze the regions where they were sold.
Having the regions already separated into columns helps a lot; we only need to group the records by ‘Platform’ and sum the values from NA_Sales to Global_Sales.
Groupby → Sum → Select Fields
df_grouped = df.groupby('Platform').sum()[['NA_Sales','EU_Sales','JP_Sales','Other_Sales', 'Global_Sales']]df_grouped
That is too many values; even considering the empty records, there will be too many bars in our chart.
Let’s plot a bar for each platform and region and get a look at the result.
# define figurefig, ax = plt.subplots(1, figsize=(16, 6))# numerical xx = np.arange(0, len(df_grouped.index))# plot barsplt.bar(x - 0.3, df_grouped['NA_Sales'], width = 0.2, color = '#1D2F6F')plt.bar(x - 0.1, df_grouped['EU_Sales'], width = 0.2, color = '#8390FA')plt.bar(x + 0.1, df_grouped['JP_Sales'], width = 0.2, color = '#6EAF46')plt.bar(x + 0.3, df_grouped['Other_Sales'], width = 0.2, color = '#FAC748')# remove spinesax.spines['right'].set_visible(False)ax.spines['top'].set_visible(False)# x y detailsplt.ylabel('Millions of copies')plt.xticks(x, df_grouped.index)plt.xlim(-0.5, 31)# grid linesax.set_axisbelow(True)ax.yaxis.grid(color='gray', linestyle='dashed', alpha=0.2)# title and legendplt.title('Video Game Sales By Platform and Region', loc ='left')plt.legend(['NA', 'EU', 'JP', 'Others'], loc='upper left', ncol = 4)plt.show()
As expected, the chart is hard to read. Let’s try the stacked bar chart and add a few adjustments.
First, we can sort the values before plotting, giving us a better sense of order and making it easier to compare the bars. We’ll do so with the ‘Global Sales’ column since it has the total.
## sort valuesdf_grouped = df_grouped.sort_values('Global_Sales')df_grouped
Earlier, to build a clustered bar chart, we used a plot for each region where the width parameter and adjustments in the x-axis helped us fit each platform's four areas.
Similarly, for plotting stack bar charts, we’ll use a plot for each region. This time we’ll use the bottom/left parameter to tell Matplotlib what comes before the bars we’re drawing.
plt.bar([1,2,3,4], [10,30,20,5])plt.bar([1,2,3,4], [3,4,5,6], bottom = [10,30,20,5])plt.show()plt.barh([1,2,3,4], [10,30,20,5])plt.barh([1,2,3,4], [3,4,5,6], left = [10,30,20,5])plt.show()
Cool. We can use a loop to plot the bars, passing a list of zeros for the ‘bottom’ parameter in the first set and accumulating the following values for the next regions.
fields = ['NA_Sales','EU_Sales','JP_Sales','Other_Sales']colors = ['#1D2F6F', '#8390FA', '#6EAF46', '#FAC748']labels = ['NA', 'EU', 'JP', 'Others']# figure and axisfig, ax = plt.subplots(1, figsize=(12, 10))# plot barsleft = len(df_grouped) * [0]for idx, name in enumerate(fields):    plt.barh(df_grouped.index, df_grouped[name], left = left, color=colors[idx])    left = left + df_grouped[name]# title, legend, labelsplt.title('Video Game Sales By Platform and Region\n', loc='left')plt.legend(labels, bbox_to_anchor=([0.55, 1, 0, 0]), ncol=4, frameon=False)plt.xlabel('Millions of copies of all games')# remove spinesax.spines['right'].set_visible(False)ax.spines['left'].set_visible(False)ax.spines['top'].set_visible(False)ax.spines['bottom'].set_visible(False)# adjust limits and draw grid linesplt.ylim(-0.5, ax.get_yticks()[-1] + 0.5)ax.set_axisbelow(True)ax.xaxis.grid(color='gray', linestyle='dashed')plt.show()
Great, this is way more readable than the last one.
It’s important to remember the purpose of this chart before trying to extract any insights. The idea here is to compare the platforms' total sales and understand each platform's composition.
Comparing totals across fields and comparing regions inside one bar is ok. Comparing regions from different bars, on the other hand, can be very misleading.
In this case, we can compare the NA region across the bars since it has the same starting point for every bar, but it isn't so easy to compare the others. Take the X360, for example, it has a lower value for JP than the PS2, but it’s hard to compare if the Others value is higher or lower than the Wii.
Suppose we change the stack's order, with Other Sales as the first bar, and sort the records by Other Sales. It should be easier to tell which is more significant.
## sort valuesdf_grouped = df_grouped.sort_values('Other_Sales')fields = ['Other_Sales', 'NA_Sales','EU_Sales','JP_Sales']colors = ['#1D2F6F', '#8390FA', '#6EAF46', '#FAC748']labels = ['Others', 'NA', 'EU', 'JP']
There are two essential elements in this visualization, the order of the categories in the stack of bars and the rows' order.
If we want to emphasize one region, we can sort the records with the chosen field and use it as the left-most bar.
If we don’t, we can sort the records by the total and order the stacks with the categories that have higher values first.
Stacked bar charts are excellent for comparing categories and visualizing their composition, but we can take even more advantage of them.
We can focus on displaying parts of a whole. To achieve that we’ll have to prepare our data and calculate the proportion of sales for each region.
fields = ['Other_Sales', 'NA_Sales','EU_Sales','JP_Sales']df_grouped = df.groupby('Platform').sum()# in some cases global sales is not equal to the sum of all regions# so I'll re calculate itdf_grouped['Global_Sales'] = df_grouped[fields].sum(axis=1)# create a column for each regions proportion of global salesfor i in fields:    df_grouped['{}_Percent'.format(i)] = df_grouped[i] / df_grouped['Global_Sales']df_grouped.sort_values('NA_Sales_Percent', inplace=True)df_grouped
Now we can pretty much repeat what we did earlier with some small tweaks. Let’s also get our code into a method so we can reuse it.
# variableslabels = ['NA', 'EU', 'JP', 'Others']colors = ['#1D2F6F', '#8390FA', '#6EAF46', '#FAC748']title = 'Video Game Sales By Platform and Region\n'subtitle = 'Proportion of Games Sold by Region'def plot_stackedbar_p(df, labels, colors, title, subtitle):    fields = df.columns.tolist()        # figure and axis    fig, ax = plt.subplots(1, figsize=(12, 10))# plot bars    left = len(df) * [0]    for idx, name in enumerate(fields):        plt.barh(df.index, df[name], left = left, color=colors[idx])        left = left + df[name]# title and subtitle    plt.title(title, loc='left')    plt.text(0, ax.get_yticks()[-1] + 0.75, subtitle)# legend    plt.legend(labels, bbox_to_anchor=([0.58, 1, 0, 0]), ncol=4, frameon=False)# remove spines    ax.spines['right'].set_visible(False)    ax.spines['left'].set_visible(False)    ax.spines['top'].set_visible(False)    ax.spines['bottom'].set_visible(False)# format x ticks    xticks = np.arange(0,1.1,0.1)    xlabels = ['{}%'.format(i) for i in np.arange(0,101,10)]    plt.xticks(xticks, xlabels)# adjust limits and draw grid lines    plt.ylim(-0.5, ax.get_yticks()[-1] + 0.5)    ax.xaxis.grid(color='gray', linestyle='dashed')plt.show()    plot_stackedbar_p(df_filter, labels, colors, title, subtitle)
That’s a great way to visualize the proportion of sales for each region. It’s also easier to compare the Others category since all the bars end at the same point.
In my opinion, visualizing proportion with 100% stacked bar charts looks even better when we have only two categories. Since we have a fixed start for the first and a fixed end for the other, it’s effortless to visualize the differences and compare the values.
Lastly, let’s try building a stacked bar chart with both positive and negative values.
We’ll create a dummy data frame for this example.
# listssales_revenue = [1230, 1240, 1170, 1050, 1380, 1480, 1400, 1410, 1360, 1415, 1530]interest_revenue = [150, 155, 159, 176, 290, 240, 195, 146, 180, 182, 210]fixed_costs = [-810, -810, -815, -815, -780, -780, -750, -750, -750, -770, -910] variable_costs =[-380, -410, -415, -370, -520, -655, -715, -670, -515, -510, -420]# lists to dictmy_dict = {'sales_revenue': sales_revenue, 'interest_revenue': interest_revenue,           'fixed_costs': fixed_costs, 'variable_costs': variable_costs}# dict to dfresult_df = pd.DataFrame(my_dict)result_df
The plan is to have a positive bar, divided into sales and interest revenue, and a negative bar, divided into fixed and variable costs, for each month.
We want interest_revenue on top, so we use sales_revenue as the ‘bottom’ argument when plotting.
We want fixed_costs at the top for the negative values, but instead, we’ll plot variable_costs with fixed_costs as the ‘bottom’ argument.
plt.bar(result_df.index, result_df['interest_revenue'], bottom = result_df['sales_revenue'], color = '#5E96E9', width =0.5)plt.bar(result_df.index, result_df['variable_costs'], bottom = result_df['fixed_costs'], color = '#E17979', width =0.5)
Again, the rest is very similar to what we already did.
fig, ax = plt.subplots(1, figsize=(16, 8))plt.bar(result_df.index, result_df['sales_revenue'], color = '#337AE3', width =0.5)plt.bar(result_df.index, result_df['interest_revenue'], bottom = result_df['sales_revenue'], color = '#5E96E9', width =0.5)plt.bar(result_df.index, result_df['fixed_costs'], color = '#DB4444', width =0.5)plt.bar(result_df.index, result_df['variable_costs'], bottom = result_df['fixed_costs'], color = '#E17979', width =0.5)# x and y limitsplt.xlim(-0.6, 10.5)plt.ylim(-1600, 2000)# remove spinesax.spines['right'].set_visible(False)ax.spines['left'].set_visible(False)ax.spines['top'].set_visible(False)ax.spines['bottom'].set_visible(False)#gridax.set_axisbelow(True)ax.yaxis.grid(color='gray', linestyle='dashed', alpha=0.7)# x ticksxticks_labels = ['Jan', 'Fev', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov']plt.xticks(result_df.index , labels = xticks_labels)# title and legendlegend_label = ['Sales Revenue', 'Interest Revenue', 'Variable Costs', 'Fixed Costs']plt.legend(legend_label, ncol = 4, bbox_to_anchor=([1, 1.05, 0, 0]), frameon = False)plt.title('My Company - 2020\n', loc='left')plt.show()
That’s it. We stacked lots of bars and tried some different applications for this convenient visualization technique.
Thanks for reading my article!
Resources:
Matplotlib Bar;Matplotlib Gallery Stacked Bar;Matplotlib Gallery Horizontal Bar;