Here’s an interesting and astute graduation speech I listened to recently:
“A lesson learned should be a lesson shared.”
Kyle Martin
In the speech, Kyle declares that “a lesson learned should be a lesson shared.” As a dad, I think about this a lot. I want my children to be more successful than me: professionally, personally…across the board. I’m always trying to share lessons I’ve learned with them–most often, mistakes I’ve made that I hope they can avoid.
Of course, a critical component of a learned lesson is the learned part. The fact that you’ve lived enough under certain conditions to have learned a valuable tenet–good or bad–from those conditions and your responses to those conditions. I wonder if a better name for these lessons is lessons lived. As I share my lessons lived with my children, part of me thinks, “will these lessons even resonate with my children if they’ve never lived them in the first place?” Nevertheless, I keep sharing.
Another thought that occurred to me while watching this video was, what must the Salutatorian be thinking? “Heck, if he regrets earning the Valedictorian spot, give it to me!”
Have you ever rendered a chart with Pandas and/or Matplotlib where one or both of your axes (axises?) rendered as a smear of overlapping, unreadable black text?
As an example, let’s create a bar chart of COVID-19 data. [As an aside: I’ve noticed that line charts seem to automatically thin out any overlapping tick labels and tend not to fall prey to this problem.]
Load and clean up the data
After downloading the CSV data, I wrote the following code to load the data and prepare it for visualization:
df_covid_confirmed_us = pd.read_csv('./data/time_series_covid19_confirmed_US_20200720.csv')
df_covid_deaths_us = pd.read_csv('./data/time_series_covid19_deaths_US_20200720.csv')
cols_to_keep1 = [i for i, v in enumerate(df_covid_confirmed_us.columns) if v in ['Admin2', 'Province_State'] or v.endswith('20')]
cols_to_keep2 = [i for i, v in enumerate(df_covid_deaths_us.columns) if v in ['Admin2', 'Province_State'] or v.endswith('20')]
df_covid_confirmed_ohio = df_covid_confirmed_us[df_covid_confirmed_us.Province_State=='Ohio'].iloc[:,cols_to_keep1].copy()
df_covid_deaths_ohio = df_covid_deaths_us[df_covid_deaths_us.Province_State=='Ohio'].iloc[:,cols_to_keep2].copy()
df_covid_confirmed_ohio.head()
Tidy up the dataframes
The data is still a bit untidy, so I wrote this additional code to transform it into a more proper format:
I’d like to do a nice, side-by-side comparison, in bar chart form, of these two datasets. One way to do that is to concatenate both dataframes together and then render your chart from the single result. Here’s the code I wrote to concatenate both datasets together:
fig, ax = plt.subplots(figsize=(12,8))
_ = df_combined_data.groupby(['obs_date', 'data_type']).sum().unstack().plot(kind='bar', ax=ax)
# draws the tick labels at an angle
fig.autofmt_xdate()
title = 'Number of COVID-19 cases/deaths in Ohio: {0:%d %b %Y} - {1:%d %b %Y}'.format(df_combined_data.obs_date.min(),
df_combined_data.obs_date.max())
_ = ax.set_title(title)
_ = ax.set_xlabel('Date')
_ = ax.set_ylabel('Count')
# clean up the legend
original_legend = [t.get_text() for t in ax.legend().get_texts()]
new_legend = [t.replace('(cnt, ', '').replace(')', '') for t in original_legend]
_ = ax.legend(new_legend)
The X axis is a mess! Fortunately, there are a variety of ways to fix this problem: I particularly like the approach mentioned in this solution. Basically, I’m going to thin out the labels at a designated frequency. In my solution, I only show every fourth date/label. So, here’s my new code with my label fix highlighted:
fig, ax = plt.subplots(figsize=(12,8))
_ = df_combined_data.groupby(['obs_date', 'data_type']).sum().unstack().plot(kind='bar', ax=ax)
# draws the tick labels at an angle
fig.autofmt_xdate()
title = 'Number of COVID-19 cases/deaths in Ohio: {0:%d %b %Y} - {1:%d %b %Y}'.format(df_combined_data.obs_date.min(),
df_combined_data.obs_date.max())
_ = ax.set_title(title)
_ = ax.set_xlabel('Date')
_ = ax.set_ylabel('Count')
# clean up the legend
original_legend = [t.get_text() for t in ax.legend().get_texts()]
new_legend = [t.replace('(cnt, ', '').replace(')', '') for t in original_legend]
_ = ax.legend(new_legend)
# tick label fix
tick_labels = [l.get_text().replace(' 00:00:00', '') for l in ax.get_xticklabels()]
new_tick_labels = [''] * len(tick_labels)
new_tick_labels[::4] = tick_labels[::4]
_ = ax.set_xticklabels(new_tick_labels)
That X axis is much more readable now thanks to the power of Python list slicing.
In my final mini-series on cleaning up stacked bar charts (Part 1 and Part 2, in case you missed them), let’s talk about how you might order the bars of your chart.
In my last post, each bar in my chart represented a different day of the week and I allowed the bars to be ordered accordingly:
Most people would probably expect this sort of ordering. However, what if your groups don’t have an inherent order like day-of-the-week?
For my example, I generated some random email data for five fake email accounts:
import numpy as np
from datetime import date, timedelta
import pandas as pd
# names compliments of: https://frightanic.com/goodies_content/docker-names.php
email_accounts = ['fervent_saha@test.com', 'serene_cori@test.com', 'agitated_pike@test.com',
'cocky_turing@test.com', 'sad_babbage@test.com']
email_data = []
for acct in email_accounts:
for cat in ['primary', 'promotions', 'social']:
nbr_of_email = np.random.randint(50, high=100)
for i in range(0, nbr_of_email):
email_dt = date(2020, 6, 1) + timedelta(days=np.random.randint(0, high=30))
email_data.append([email_dt, acct, cat])
df_email_accts = pd.DataFrame(email_data, columns=['email_date', 'email_account', 'email_category'])
df_email_accts['email_date'] = pd.to_datetime(df_email_accts.email_date)
df_email_accts.head()
Now, let’s use a stacked bar chart to compare the emails counts, by category, of the five different email accounts:
Technically, matplotlib has ordered the email accounts alphabetically–from agitated_pike@test.com to serene_cori@test.com–but most folks probably don’t care about that: they’ll likely want the chart ordered either greatest count to least or least count to greatest.
How can you then order your stacked bar chart by the total count? There may be a more elegant way to do this in pandas, but I came up with three lines to code to get the order right.
To start with, take a look at the dataframe we get with my standard groupby and unstack approach:
What I need is a way to total the counts of the three categories–primary, promotions, and social–for each of the five email accounts and then sort the dataframe by that total.
No problem! I can use the pandas sum function with axis=1–meaning, sum across the columns–to get that total:
Putting it all together, then, here’s the code I came up with to nicely sorted my stacked bar chart in a meaningful way:
# two lines of code to provide a "total" column that can be used for sorting
df_rpt = df_email_accts.groupby(['email_account', 'email_category']).count().unstack()
df_rpt['total'] = df_rpt.sum(axis=1)
fig, ax = plt.subplots(figsize=(12,8))
# sort the dataframe by the "total" column, then drop it before rendering the chart
_ = df_rpt.sort_values('total')[df_rpt.columns.tolist()[:-1]].plot(kind='barh', stacked=True, ax=ax)
_ = ax.set_title('Email counts by category, June 2020')
_ = ax.set_xlabel('Email Count')
_ = ax.set_ylabel('Email Account')
# and, of course, clean up the legend
original_legend = [t.get_text() for t in ax.legend().get_texts()]
new_legend = [t.replace('(email_date, ', '').replace(')', '') for t in original_legend]
_ = ax.legend(new_legend, title='Category')
Recent Comments