Title:

Creating a ‘Dot Plot’.

Objective:

As a Data and Reporting Analyst, I am often asked by the country missions to analyse data and create bespoke visualisations for their reports. 

Methods:

Python (Matplotlib, Seaborn), Data Visualisation

Explanation

One of the more challenging but enjoyable parts of my work at the IOM is creating bespoke visualisations for country missions. On this occasion, I was asked to create a ‘Dot Plot’, a wonderful graph that allows you to show how data points are spread across a range and can be used very effectively in time series analysis or comparisons across different categorical variables. Below, you can see one of a series I made for IOM Ethiopia. You can see the plots in the full report here.

Below is the plot, slightly adjusted for publication, alongside other visualisations that I did for the report and code blocks with how I created the first version of the plot.

Let's begin by setting libraries, fonts and some customisations


#Importing the libraries

import numpy as np 
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.font_manager as fm
import matplotlib.gridspec as gridspec
 
#Creating custom formatting and fonts

custom_font_path = r'C:\Users\miredale\ - removed for privacy reasons -\5.- Gill Sans Nova Book 5.otf'
custom_font = fm.FontProperties(fname=custom_font_path)
custom_font_path2 = r'C:\Users\miredale\Downloads\GillSansNova-SemiBold.otf'
custom_font2 = fm.FontProperties(fname=custom_font_path2)
 
#Create customisations for the plot

region_order = ['Afar', 'Amhara', 'Benishangul Gumz', 'Central Ethiopia', 'Contested Areas', 'Gambela', 'Harari', 'Oromia', 'Sidama', 'Somali', 'South West Ethiopia Peoples','Tigray', 'Average']
custom_palette = {'Sites with IDPs (SA)': '#0033a0', 'Villages with returning IDPs (VAS)': '#b3c2e3'}
    

Building the plot itself and the plot area


#Building the plot

fig = plt.figure(figsize=(16, 8))
gs = gridspec.GridSpec(1, 2, width_ratios=[20, 1])  
ax = fig.add_subplot(gs[0])
sns.stripplot(data=EduDot,
              x="Average Time to Walk to Primary School (minutes)",
              y="Region",
              hue="Analysis",
              marker='o',
              palette=custom_palette,
              size=11,
              jitter=False,
              alpha=1,
              order=region_order,
              ax=ax)


#Formatting the plot area and the x/y axes

ax.margins(x=0.07)  
fig.patch.set_facecolor('#e6effb')
ax.set_xticklabels(ax.get_xticklabels(), fontproperties=custom_font, fontsize=14, weight='bold', color='#4066b8')
ax.set_yticklabels(ax.get_yticklabels(), fontproperties=custom_font, fontsize=14, weight='bold', color='#4066b8')


ax.spines['top'].set_color('#d9dbdb')
ax.spines['bottom'].set_color('#d9dbdb')
ax.spines['left'].set_color('#d9dbdb')
ax.spines['right'].set_color('#d9dbdb')


ax.grid(True, axis='y', color='#d9dbdb', linestyle='--', linewidth=0.7)
yticks = ax.get_yticks()
yticklabels = [tick.get_text() for tick in ax.get_yticklabels()]
    

This was an interesting piece of code that I had learnt about in order to adjust the positions of the dots


#Adjusting the positions for the estimates for the 'dots'.

x_offsets = [0.4, -1.05, 0.4, 0.4, 0.4, 0.4, 0.4, -1.05, 0.4, -1.05, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.3, 0.4, -1.4, 0.4, 0.4, -1.05] 
# Custom x-offset for each label

y_offsets = [0,0,0,0,0,0,0,0,0, 0, 0, 0,0, 0, 0, 0, 0, -0.2, 0, 0, 0,0,0]   # Custom y-offset for each label
#            1,2,3,4,5,6,7,8,9,10, 11,12,13,14,15,16,17,18,19,20,21,22,23 - position of each label on the grid relative to its offset

#Check to see if my dot offsets match the length of my data 
assert len(x_offsets) == len(EduDot), "Ensure custom offset lists match data size."
 

Creating the labels for the dots


#This loop iterates over each row of EduDot and gets the x_position, which corresponds to the value 'Average Time to Walk to Primary School (minutes)' at index i. Also, it extracts the region from the EduDot['Region'] column at index i, finds the index of that region in yticklabels and retrieves the corresponding y_position.



for i in range(len(EduDot)):
    x_position = EduDot['Average Time to Walk to Primary School (minutes)'].iloc[i]
    y_position = yticks[yticklabels.index(EduDot['Region'].iloc[i])]
 
# Use custom offsets for x and y
    x_offset = x_offsets[i]
    y_offset = y_offsets[i]
    plt.text(
        x_position + x_offset,  
        y_position + y_offset,  
        str(x_position),
        fontproperties=custom_font,
        horizontalalignment='left',
        size=11,
        color='#4066b8',
        weight='bold')
    

Labels and Legends


#Handling the design of the labels and legend

plt.gcf().set_facecolor('#d9e0f1')

ax.set_facecolor('#FFFFFF')
ax.set_xticklabels(ax.get_xticklabels(), fontproperties=custom_font, fontsize=11, weight='bold', color='#4066b8')
ax.set_yticklabels(ax.get_yticklabels(), fontproperties=custom_font, fontsize=11, weight='bold', color='#4066b8')  

plt.title(' ', color='#4066b8', fontproperties=custom_font, fontsize=16, pad=12)
plt.xlabel('Average Time to Primary School (minutes)', color='#4066b8', fontproperties=custom_font, fontsize=14, labelpad=12)
plt.ylabel('', color='#4066b8', fontproperties=custom_font, fontsize=16, labelpad=12)

legend=plt.legend(title='Population Group            ', prop=custom_font, title_fontproperties=custom_font2, bbox_to_anchor=(0.989, 0.98), loc='upper right', borderaxespad=0.)
legend.get_title().set_color('#4066b8')

for text in legend.get_texts():
    text.set_color('#9a999a')  
fig.add_subplot(gs[1]).axis('off')
    

Final output


#Export the figure

plt.savefig(r"C:\Users\miredale\OneDrive - International Organization for Migration - IOM\ - removed for privacy -\SchoolDot2.pdf", format="pdf", bbox_inches='tight')
plt.show()
    

The Result