Visualizing Education Data
The last post in my series on UNESCO's SDG 4 data. Standard charts don't show much of what matters in education data. The metrics depend on each other, the data is hierarchical (region, country, level), it changes over time and by geography, and a lot of it is missing. These are the plots I found most useful, all built with Plotly.
1. Interactive correlation matrix
A heatmap where each cell shows its r value, both on hover and as a label:
def create_interactive_correlation_matrix(df):
import plotly.graph_objects as go
# Calculate correlations
corr_matrix = df.corr()
# Create heatmap with hover text
fig = go.Figure(data=go.Heatmap(
z=corr_matrix,
x=corr_matrix.columns,
y=corr_matrix.columns,
zmin=-1,
zmax=1,
colorscale='RdBu',
hoverongaps=False,
customdata=np.round(corr_matrix, 2),
hovertemplate='Correlation between %{x} and %{y}: <br>r = %{customdata}<extra></extra>'
))
# Add annotations
annotations = []
for i, row in enumerate(corr_matrix.values):
for j, value in enumerate(row):
annotations.append(
dict(
x=j,
y=i,
text=f"{value:.2f}",
showarrow=False
)
)
fig.update_layout(
title="Interactive Correlation Matrix of Educational Metrics",
annotations=annotations
)
return fig
2. Sunburst for hierarchical data
Region, then country, then education level, then disability status, all in one chart:
def create_education_sunburst(df):
import plotly.express as px
# Prepare hierarchical data
hierarchy = {
'Region': {
'Country': {
'Education_Level': {
'Disability_Status': 'Completion_Rate'
}
}
}
}
fig = px.sunburst(
df,
path=['Region', 'Country', 'Education_Level', 'Disability_Status'],
values='Completion_Rate',
color='Completion_Rate',
color_continuous_scale='Viridis'
)
fig.update_layout(title="Educational Completion Rates Hierarchy")
return fig
3. Animated map
A choropleth with a time slider shows geography and change over time together:
def create_animated_choropleth(df):
import plotly.express as px
fig = px.choropleth(
df,
locations='ISO_Code',
color='Completion_Rate',
animation_frame='Year',
color_continuous_scale='Viridis',
range_color=[0, 100],
hover_data=['Country', 'Completion_Rate', 'Infrastructure_Score']
)
fig.update_layout(
title="Global Educational Progress Over Time",
coloraxis_colorbar_title="Completion Rate (%)"
)
return fig
4. Time series with two axes and a confidence band
Completion rate and infrastructure score on the same chart, with the 95% confidence interval shaded:
def create_advanced_time_series(df):
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Create figure with secondary y-axis
fig = make_subplots(specs=[[{"secondary_y": True}]])
# Add completion rates
fig.add_trace(
go.Scatter(
x=df['Year'],
y=df['Completion_Rate'],
name="Completion Rate",
mode='lines+markers'
),
secondary_y=False
)
# Add infrastructure score
fig.add_trace(
go.Scatter(
x=df['Year'],
y=df['Infrastructure_Score'],
name="Infrastructure Score",
mode='lines+markers'
),
secondary_y=True
)
# Add confidence intervals
fig.add_trace(
go.Scatter(
x=df['Year'].tolist() + df['Year'].tolist()[::-1],
y=df['CI_Upper'].tolist() + df['CI_Lower'].tolist()[::-1],
fill='toself',
fillcolor='rgba(0,100,80,0.2)',
line=dict(color='rgba(255,255,255,0)'),
name='95% Confidence Interval'
),
secondary_y=False
)
return fig
5. Sankey diagram of student pathways
How students move from one level to the next, and where they drop out:
def create_education_sankey(df):
import plotly.graph_objects as go
fig = go.Figure(data=[go.Sankey(
node = dict(
pad = 15,
thickness = 20,
line = dict(color = "black", width = 0.5),
label = ["Primary", "Lower Secondary", "Upper Secondary",
"Completed", "Dropped Out"],
color = "blue"
),
link = dict(
source = [0, 0, 1, 1, 2, 2],
target = [1, 4, 2, 4, 3, 4],
value = [80, 20, 60, 20, 40, 20]
)
)])
fig.update_layout(title_text="Educational Progression Pathways")
return fig
Colors and accessibility
Use qualitative palettes for categories, sequential palettes for values, and diverging palettes for anything with a meaningful midpoint, like correlations:
color_schemes = {
'categorical': px.colors.qualitative.Set3,
'sequential': px.colors.sequential.Viridis,
'diverging': px.colors.diverging.RdBu
}
Keep fonts large, give the chart enough room, and always show the legend:
def make_accessible_visualization(fig):
fig.update_layout(
font=dict(size=14),
height=600,
margin=dict(t=100, b=50, l=50, r=50),
showlegend=True,
coloraxis_colorbar_title_font_size=12
)
return fig
Putting it together
Completion rates over time:
def visualize_completion_rates():
fig = create_advanced_time_series(completion_data)
fig.update_layout(
title="Completion Rates Across Educational Levels",
xaxis_title="Year",
yaxis_title="Completion Rate (%)"
)
return fig
Regional comparison:
def visualize_regional_comparison():
fig = create_animated_choropleth(regional_data)
fig.update_layout(
title="Regional Variations in Educational Outcomes",
geo=dict(showframe=False, showcoastlines=True)
)
return fig
Further reading
- Cairo, A. (2016). "The Truthful Art: Data, Charts, and Maps for Communication"
- Munzner, T. (2014). "Visualization Analysis and Design"
- Romero, C., & Ventura, S. (2020). "Educational Data Mining and Learning Analytics"
- Williamson, B. (2017). "Big Data in Education"