Population Pyramid Graphs in Python: Analyzing Deaths by Age in 2023 Data

When visualizing demographic data, adding data labels can greatly improve the accuracy and readability of your information. Today, we'll learn how to include data labels in a population pyramid graph to visualize the number of deaths by age in 2023.
Data overview
The underlying data for the above Python visualization graph is from the February '24 Statistics Korea release of the Birth and death statistics for 2023 (provisional)based on the number of deaths. Here's how that data looks in a table, with deaths in thousands.
| Age groups | Number of male deaths | Number of female deaths |
|---|---|---|
| 90+ | 15.3 | 42.1 |
| 80-89 | 63.5 | 69.6 |
| 70-79 | 45.5 | 24.6 |
| 60-69 | 34.3 | 13.2 |
| 50-59 | 18.0 | 7.2 |
| 40-49 | 7.0 | 3.8 |
| 30-39 | 2.8 | 1.5 |
| 20-29 | 1.7 | 0.9 |
| 10-19 | 0.4 | 0.4 |
| 1-9 | 0.2 | 0.1 |
| 0- | 0.3 | 0.3 |

Let's take the 2023 deaths by age data from the table above and represent it as a population pyramid graph, labeling the data with the exact number for each age group to provide more detail.
Implementing Python visualization code
import numpy as np
import matplotlib.pyplot as plt
Prepare the # data
age_groups = ['90+', '80-89', '70-79', '60-69', '50-59', '40-49',
'30-39', '20-29', '10-19', '1-9', '0-']
male_deaths = [15.3, 63.5, 45.5, 34.3, 18.0, 7.0, 2.8, 1.7, 0.4, 0.2, 0.3]
female_deaths = [42.1, 69.6, 24.6, 13.2, 7.2, 3.8, 1.5, 0.9, 0.4, 0.1, 0.3]
Generate a figure using a # GridSpec
fig = plt.figure(figsize=(15, 8))
gs = fig.add_gridspec(1, 3, width_ratios=[4, 0.01, 4]) # Set the ratio of the center column to 0.01
# Create three subplots
ax_left = fig.add_subplot(gs[0])
ax_center = fig.add_subplot(gs[1])
ax_right = fig.add_subplot(gs[2])
y_pos = np.array(len(age_groups))
# left plot (males)
male_bars = ax_left.barh(y_pos, -np.array(male_deaths), align='center',
color='#5AB1EF', height=0.7)
Plot # right side (female)
female_bars = ax_right.barh(y_pos, female_deaths, align='center',
color='#FFB848', height=0.7)
Add # data labels
def add_labels(ax, bars):
for bar in bars:
width = bar.get_width()
x = width
value = abs(width)
ax.text(x, bar.get_y() + bar.get_height()/2, f'{value}',
ha='right' if width < 0 else 'left',
va='center',
fontsize=9,
fontweight='bold',
color='black',
bbox=dict(pad=0.4, facecolor='none', edgecolor='none'))
add_labels(ax_left, male_bars)
add_labels(ax_right, female_bars)
# center plot (age groups)
ax_center.set_yticks(y_pos)
ax_center.tick_params(axis='y', length=0)
# Align age labels centered
ax_center.set_yticklabels(age_groups, ha='center')
Set the style of the # center column
ax_center.set_xlim(-0.5, 0.5) # Reduce the x-axis range so that the labels are more centered
ax_center.set_xticks([])
for spine in ax_center.spines.values():
spine.set_visible(False)
Set the style of the # left and right plots
for ax in [ax_left, ax_right]:
ax.grid(axis='x', linestyle='-', alpha=0.1)
ax.set_yticks([])
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
Set the # x-axis range
max_value = max(max(male_deaths), max(female_deaths))
ax_left.set_xlim(-max_value*1.1, 0)
ax_right.set_xlim(0, max_value*1.1)
Add # axis labels
ax_left.set_xlabel('Number of Deaths (thousands)', ha='right')
ax_right.set_xlabel('Number of Deaths (thousands)', ha='left')
Add # male/female labels
ax_left.text(-max_value/2, len(age_groups), 'Male', ha='center', va='bottom', fontsize=12)
ax_right.text(max_value/2, len(age_groups), 'Female', ha='center', va='bottom', fontsize=12)
Setting the # x-axis tick format
def format_ticks(x, p):
return f'{abs(x)}'
ax_left.xaxis.set_major_formatter(plt.FuncFormatter(format_ticks))
ax_right.xaxis.set_major_formatter(plt.FuncFormatter(format_ticks))
Add a title to the # graph
fig.suptitle('Age-Specific Mortality by Gender', y=0.95, fontsize=14)
plt.tight_layout()
plt.subplots_adjust(top=0.9) # Adjust margins for title
plt.show()Deep data analytics insights
1. Mortality disparities by age group
- 80-89 age group: peaks for men (63.5K) and women (69.6K)
- Men have higher mortality rates overall in their 70s and below
- Observe how gender gap patterns change with age
2. Lifecycle features
- Infancy (age 0): Same mortality rate for boys and girls (0.3k)
- Young adults (ages 20-49): Male mortality rate about twice that of females
- Older adults (age 80 and older): Female mortality rates spike
3. Policy implications
- The need for gender- and age-specific healthcare policies
- Demand for more healthcare for older women
- Strengthening preventive care to reduce mortality in middle-aged men

Finalize
Population pyramid graphs with data labels give you an intuitive understanding of the exact number of deaths by age. This visualization can serve as an important basis for demographic analysis and healthcare policy formulation. See below for detailed code commentary.
If you're interested in information about the total fertility rate, which is the opposite of deaths, you can use the Total Fertility Rate Definition and Calculation: Trendy Visualizations with R post for more information.
# Code Explained
1. import the required libraries
import numpy as np import matplotlib.pyplot as plt
numpyLibraries for numerical computationmatplotlib.pyplot: A library for generating graphs2. Prepare your data
age_groups = ['90+', '80-89', '70-79', '60-69', '50-59', '40-49', '30-39', '20-29', '10-19', '1-9', '0-'] male_deaths = [15.3, 63.5, 45.5, 34.3, 18.0, 7.0, 2.8, 1.7, 0.4, 0.2, 0.3] female_deaths = [42.1, 69.6, 24.6, 13.2, 7.2, 3.8, 1.5, 0.9, 0.4, 0.1, 0.3]Define the number of deaths data for each age group and each gender as a list.
3. Set up the graph structure
fig = plt.figure(figsize=(15, 8)) gs = fig.add_gridspec(1, 3, width_ratios=[4, 0.01, 4])
figsize=(15, 8): set the overall size of the graph (15 horizontal, 8 vertical)add_gridspec: Divides the graph into three columns. Set the ratio to [4, 0.01, 4] to make the center column very narrow.4. Create a subplot
ax_left = fig.add_subplot(gs[0]) ax_center = fig.add_subplot(gs[1]) ax_right = fig.add_subplot(gs[2])Create three subplots (left, center, and right).
5. draw a bar graph
y_pos = np.array(len(age_groups)) male_bars = ax_left.barh(y_pos, -np.array(male_deaths), align='center', color='#5AB1EF', height=0.7) female_bars = ax_right.barh(y_pos, female_deaths, align='center', color='#FFB848', height=0.7)
barh: function to draw a horizontal bar graph- Convert male data to negative and display on the left
- Female data is displayed on the right
6. Add data labels
def add_labels(ax, bars): for bar in bars: width = bar.get_width() x = width value = abs(width) ax.text(x, bar.get_y() + bar.get_height()/2, f'{value}', ha='right' if width < 0 else 'left', va='center', fontsize=9, fontweight='bold', color='black', bbox=dict(pad=0.4, facecolor='none', edgecolor='none')) add_labels(ax_left, male_bars) add_labels(ax_right, female_bars)Add the corresponding numbers for each bar as labels.
7. set the center column style
ax_center.set_yticks(y_pos) ax_center.tick_params(axis='y', length=0) ax_center.set_yticklabels(age_groups, ha='center') ax_center.set_xlim(-0.5, 0.5) ax_center.set_xticks([]) for spine in ax_center.spines.values(): spine.set_visible(False)Show the age group labels in the center column, and remove the unnecessary axes and borders.
8. Set the style of the left and right plots
for ax in [ax_left, ax_right]: ax.grid(axis='x', linestyle='-', alpha=0.1) ax.set_yticks([]) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False)Add grids to the left and right plots, and remove unnecessary axes.
9. Set the X-axis range and add labels
max_value = max(max(male_deaths), max(female_deaths)) ax_left.set_xlim(-max_value*1.1, 0) ax_right.set_xlim(0, max_value*1.1) ax_left.set_xlabel('Number of Deaths (thousands)', ha='right') ax_right.set_xlabel('Number of Deaths (thousands)', ha='left') ax_left.text(-max_value/2, len(age_groups), 'Male', ha='center', va='bottom', fontsize=12) ax_right.text(max_value/2, len(age_groups), 'Female', ha='center', va='bottom', fontsize=12)Set the range for the x-axis, add axis labels and gender labels.
10. Set the X-axis scale format
def format_ticks(x, p): return f'{abs(x)}' ax_left.xaxis.set_major_formatter(plt.FuncFormatter(format_ticks)) ax_right.xaxis.set_major_formatter(plt.FuncFormatter(format_ticks))Displays the values on the x-axis scale in absolute value.
11. Finalize the graph
fig.suptitle('Age-Specific Mortality by Gender', y=0.95, fontsize=14) plt.tight_layout() plt.subplots_adjust(top=0.9) plt.show()Add a title to the graph, adjust the layout, and display the graph.





