NikoTakSecuring the Web, One Threat at a Time.

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