NYC measles cases by age (2018 - 2019)

Import libraries

In [1]:
from datetime import datetime
import matplotlib.pyplot as plt
import os
import pandas as pd

Read and show the data

The data was manually collected from the NYC Health Measles webpage and saved as a CSV file. This manual approach was used because the data is small.

In [2]:
# Set (relative) path to the CSV data file
data_file = os.path.join('..', 'data', 'nyc-health', 'final', 'nyc-measles-cases-by-age.csv')

# Import data from the CSV file as a pandas dataframe
df = pd.read_csv(data_file)

# Show the data
df
Out[2]:
Start Date End Date Under 1 year 1 to 4 years 5 to 17 years 18 years and over Total
0 2018-09-01 2019-08-19 102 277 146 124 649

Extract context information

We want to show the start and end dates in the plot, to provide context. We use just the month information for consistency across the other data visualizations, especially the "NYC new case by month".

In [3]:
# Notes about the lambda function below:
# - 1. strptime transforms the raw date string to a datetime object
# - 2. strftime transforms the datetime object to a nicelly formatted date string
[start_month, end_month] = map(
    lambda x: datetime.strptime(x, '%Y-%m-%d').strftime('%b %Y'),
    df.iloc[0, :2]
)

# Show the nicelly formated date strings
[start_month, end_month]
Out[3]:
['Sep 2018', 'Aug 2019']

We also want to show the total number of cases in the plot, to provide context.

In [4]:
# Get the number of total cases
total_cases = df.iloc[0, -1]

# Check if there is a problem with the data where the reported total
# does not match the sum of the number of cases for each age group
if total_cases != df.iloc[0, 2:-1].sum():
    print('WARNING: cases for each age group do NOT add up to the reported total!')

# Show the total cases
total_cases
Out[4]:
649

Extract the data to plot

In [5]:
# Extract the data to plot
data_to_plot = df.iloc[-1, 2:-1]

# Show the data to plot
data_to_plot
Out[5]:
Under 1 year         102
1 to 4 years         277
5 to 17 years        146
18 years and over    124
Name: 0, dtype: object

Create default bar chart

In [6]:
default_fig = plt.figure()
ax = data_to_plot.plot.bar()
plt.title('NYC measles cases by age group')
plt.show()

Save default bar chart

In [7]:
# Set image file path/name (without file extension)
img_file = os.path.join('..', 'images', 'nyc-measles-cases-by-age-bar-chart-default')

# Save as PNG image
default_fig.savefig(img_file + '.png', bbox_inches='tight', dpi=200)

# Save as SVG image
default_fig.savefig(img_file + '.svg', bbox_inches='tight')

Create improved bar chart

We want the bar chart to be clear and to contain the necessary context.

To contextualize the bar chart we:

  • use a title that explictly says what the bar chart represents;
  • add text annotations that provides information about:
    • the start and end dates,
    • the total number of cases during that period, and
    • the data and image sources.

To make the bar chart as clear as possible we:

  • use an horizontal bar chart because it is easier to read than a vertical one;
  • explicitly show the number and percentage of cases for each age group;
  • use a large enough font to make all labels easy to read;
  • remove unnecessary elements (x-axis ticks and values, y-axis ticks, and plot box).
In [11]:
# Define font parameters
fn = 'Arial' # font name
fsb = 18     # font size base

# Create figure
fig = plt.figure()

# Add figure title
#fig.suptitle('NYC measles cases by age group', fontname=fn, fontsize=(fsb + 6))
plt.title('NYC measles cases by age group', fontname=fn, fontsize=(fsb + 6))

# Create the horizontal bar chart
ax = data_to_plot.plot.barh(alpha=0.3, color='red', width=0.8)

# Invert the y-axis
ax.invert_yaxis()

# Remove the x-axis ticks and values
ax.get_xaxis().set_ticks([])

# Remove the y-axis ticks only (keep the labels)
ax.yaxis.set_ticks_position('none')

# Set the y-axis labels font properties
ax.set_yticklabels(data_to_plot.keys(), fontname=fn, fontsize=fsb)

# Create labels in front of the bars showing the number and percentage of cases.
# Note: we round the percentages to the nearest integer.
for i in ax.patches:
    label = str(i.get_width()) + " (" + str(int(round(100 * i.get_width() / total_cases))) + "%)"
    ax.text(i.get_width() + 5, i.get_y() + 0.5, label, fontname=fn, fontsize=fsb)

# Remove the axes box
plt.box(False)

# Add note about the total cases
text = str(total_cases) + ' total confirmed cases from ' + start_month + ' to ' + end_month
fig.text(0.5, 0.0, text, fontname = fn, fontsize = (fsb - 2), horizontalalignment='center')

# Add note about the end of the outbreak
text = 'Community transmission was declared over on Sep 3, 2019'
fig.text(0.5, -0.1, text, fontname = fn, fontsize = (fsb - 2), horizontalalignment='center')

# Add note about the Data and Image sources
sources = 'Data: NYC Health, Image: carlos-afonso.github.io/measles'
fig.text(0.5, -0.2, sources, fontname='Lucida Console', fontsize=(fsb - 4), horizontalalignment='center')

# Show figure
plt.show()

Save improved bar chart

In [9]:
# Set image file path/name (without file extension)
img_file = os.path.join('..', 'images', 'nyc-measles-cases-by-age-bar-chart')

# Save as PNG image
fig.savefig(img_file + '.png', bbox_inches='tight', dpi=200)

# Save as SVG image
fig.savefig(img_file + '.svg', bbox_inches='tight')

Export notebook as HTML

In [10]:
# Export this notebook as a static HTML page
os.system('jupyter nbconvert --to html nyc-measles-cases-by-age-final.ipynb')
Out[10]:
0