Figure Friday 2025 - week 28

join the Figure Friday session on July 18, at noon Eastern Time, to showcase your creation and receive feedback from the community.

How has the perception of country-level corruption changed over time?

Answer this question and a few others by using Plotly and Dash on the corruption perception index dataset. There are two datasets; feel free to choose the one that you prefer to work with.

Things to consider:

  • what can you improve in the app or sample figure below (choropleth map)?
  • would you like to tell a different data story using a different graph?
  • can you create a different Dash app?

Sample figure:

Code for sample figure:
from dash import Dash, dcc
import dash_ag_grid as dag
import plotly.express as px
import pandas as pd

df = pd.read_csv("https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-28/CPI2024.csv")

fig = px.choropleth(df, color="Rank", locations="ISO3", hover_name="Country / Territory",
                    title="Ranking of Corruption Perceptions Index 2024")
fig.update_layout(margin={"r":0,"t":30,"l":0,"b":10})


grid = dag.AgGrid(
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i, 'filter': True, 'sortable': True} for i in df.columns],
    dashGridOptions={"pagination": True},
    # columnSize="sizeToFit"
)

app = Dash()
app.layout = [
    grid,
    dcc.Graph(figure=fig)
]


if __name__ == "__main__":
    app.run(debug=False)

Participation Instructions:

  • Create - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
  • Submit - post your creation to LinkedIn or Twitter with the hashtags #FigureFriday and #plotly by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
  • Celebrate - join the Figure Friday sessions to showcase your creation and receive feedback from the community.

:point_right: If you prefer to collaborate with others on Discord, join the Plotly Discord channel.

Data Source:

Thank you to Transparency International for the data.

1 Like

This was a quick run I had at exploring CPI 2024 source divergence using Dash and Plotly. I built a simple app to visualize how different institutional sources rate countries in terms of perceived corruption.

Each source (e.g., Bertelsmann, PRS, EIU) applies different criteria. Using Z-score normalization, the app highlights how some sources score countries significantly higher or lower than the global average — revealing patterns of divergence that may indicate institutional bias or focus.

:magnifying_glass_tilted_right: Key features:

  • Z-score heatmap across 13 CPI sources and 10 major countries
  • Plain-language guidance for interpreting divergence
  • Inline insights explaining where and why certain scores differ

More analysis coming later this week using the new Plotly Studio app, where I’m experimenting with how easy it is to spin up apps/charts on demand!

import pandas as pd
import plotly.express as px
import dash
from dash import Dash, dcc, html, Input, Output
import numpy as np

# Load full CPI 2024 data
cpi_2024 = pd.read_csv("C:/Users/tdent/Downloads/CPI2024.csv")

# Relevant source columns
source_columns = [
    'African Development Bank CPIA',
    'Bertelsmann Foundation Sustainable Governance Index',
    'Bertelsmann Foundation Transformation Index',
    'Economist Intelligence Unit Country Ratings',
    'Freedom House Nations In Transit',
    'S&P / Global Insights Country Risk Ratings',
    'IMD World Competitiveness Yearbook',
    'PERC Asia Risk Guide',
    'PRS International Country Risk Guide',
    'Varieties of Democracy Project',
    'World Bank CPIA',
    'World Economic Forum EOS',
    'World Justice Project Rule of Law Index'
]

# Create z-score normalized data for a selected set of countries
bias_df = cpi_2024[['Country / Territory', 'ISO3'] + source_columns].copy()
bias_df = bias_df.dropna(subset=source_columns, how='all')
z_scores = bias_df.copy()
for col in source_columns:
    col_mean = bias_df[col].mean()
    col_std = bias_df[col].std()
    z_scores[col] = (bias_df[col] - col_mean) / col_std

# Focused countries to show possible bias
focus_countries = ['Russia', 'China', 'United States', 'Germany', 'France', 'Iran', 'Ukraine', 'Venezuela', 'Brazil', 'India']
z_scores_filtered = z_scores[z_scores['Country / Territory'].isin(focus_countries)].set_index('Country / Territory')
z_scores_filtered = z_scores_filtered[source_columns].T.reset_index().rename(columns={'index': 'Source'})

# Build Dash app
app = Dash(__name__)

app.layout = html.Div([
    html.H1("CPI Source Score Divergence (Z-Scores)", style={"textAlign": "center"}),
    html.P("This chart reveals how different institutional sources rate a selection of countries in 2024, relative to their global scoring behavior. Z-scores show whether a source scores a country significantly higher or lower than average."),
    html.P("How to read this chart:", style={"fontWeight": "bold"}),
    html.Ul([
        html.Li("Z-score > 0: The source scored the country higher than the global average (perceived cleaner)."),
        html.Li("Z-score < 0: The source scored the country lower than average (perceived more corrupt)."),
        html.Li("Z-scores closer to ±2 suggest notable divergence from consensus, potentially indicating bias."),
    ]),
    html.Div([
        html.P("\n🔍 Insight: The Bertelsmann Foundation Transformation Index evaluates the state of political and economic transformation based on expert analysis, often weighing democratic norms and market reforms. \nIn the case of Venezuela, it scored significantly below the global average, likely due to democratic erosion, political repression, hyperinflation, and restricted civil liberties. \nThis divergence is consistent with Bertelsmann’s strong emphasis on pluralism and institutional accountability.", style={"fontStyle": "italic", "marginTop": "1rem"}),

        html.P("\n🔍 Insight: The Economist Intelligence Unit (EIU) Country Ratings reflect expert assessments of political stability, rule of law, and civil liberties. Countries like Germany and the US score significantly above average, aligning with the EIU’s liberal-democratic lens. Meanwhile, Russia and Iran receive substantially lower scores, which is consistent with EIU's historical focus on institutional independence and electoral legitimacy.", style={"fontStyle": "italic", "marginTop": "1rem"}),

        html.P("\n🔍 Insight: The PRS International Country Risk Guide measures political, economic, and financial risk. Its relatively higher scores for China and lower scores for Ukraine or Venezuela may reflect a stronger emphasis on macroeconomic and security-related risks, which can differ from democratic governance indicators.", style={"fontStyle": "italic", "marginTop": "1rem"}),

        html.P("\n🔍 Insight: The World Justice Project (WJP) Rule of Law Index emphasizes constraints on government power, absence of corruption, and protection of fundamental rights. Countries like India or Brazil sometimes receive lower-than-average scores, suggesting gaps in enforcement and civil rights protections even in otherwise functioning democracies.", style={"fontStyle": "italic", "marginTop": "1rem"})
    ]),
    dcc.Graph(id='bias_heatmap')
])

@app.callback(
    Output('bias_heatmap', 'figure'),
    Input('bias_heatmap', 'id')
)
def update_heatmap(_):
    melted = z_scores_filtered.melt(id_vars='Source', var_name='Country', value_name='Z-Score')
    fig = px.imshow(
        z_scores_filtered.set_index('Source').T,
        labels=dict(x="Country", y="Source", color="Z-Score"),
        color_continuous_scale="RdBu",
        zmin=-2.5,
        zmax=2.5,
        aspect="auto",
        title="Source Deviation from Average (Z-Score)"
    )
    fig.update_layout(height=600, margin=dict(l=40, r=40, t=60, b=40))
    return fig

if __name__ == '__main__':
4 Likes

:tada: Happy 1-Year Anniversary to Plotly’s #FigureFriday!

Today it happened. My search for “Figure Friday week 28” returned links for 2024 and 2025. It has been 1 full year, second year is underway.

A huge shoutout and thanks to Adam Schroeder, to plotly, and to all the data lovers, designers, analytics enthusiasts and FFFs (Figure Friday Friends) for coming together every Friday to explore the art and power of data visualization. This weekly meetup has showcased stunning figures while sparking creativity, learning, and community.

Whether you came for the charts or stayed for the insights, it’s been a year of turning numbers into narratives. Here’s to the brilliant minds, the jaw-dropping graphs, and the conversations that keep us growing.

:bar_chart: Cheers to many more Fridays of inspiring figures and shared knowledge!

7 Likes

Hi @ThomasD21M. Your approach using z_scores to flag possible corruption is reasonable to me and easy to understand. If I am reading this right, a high z_scores (>+2) are just as suspect as low z_scores (<-2).

Adding the 4 insights above your chart with notes on how to interpret helps quite a bit. Great job.

1 Like

Very well said, Mike. I’ve only been actively involved for six months, and for me, it’s been a watershed. It’s helped me focus on what I’m truly passionate about, and the most important thing is meeting incredible people from a wide variety of backgrounds, always willing to give you respectful feedback. Excellent work by Adam and Plotly, without a doubt.

5 Likes

Thank you for the celebratory and beautiful words, @Mike_Purtell and @Avacsiglo21. I’m honored to be part of such a dedicated, talented, and kind community. You all make these figure Fridays not only a great place to learn and improve our data viz skils but also a place to share and grow together. I always look forward to our Friday sessions where we get see each other again and catch up :slight_smile:

4 Likes

What an interesting way to tackle and visualize the dataset, @ThomasD21M . Sweet.

Can you share a little more about the z-score interpretation? Does 1 z-score mean 1 standard deviation from the average? Thus, for example, the US is viewed by the S&P/Global Insights as one standard deviation ‘cleaner’ (less corrupt) than average, while the same country is viewed by PERC/Asia Risk Guide as 1/2 a standard deviation more corrupt than average?
That’s a big divergence.

2 Likes

Happy Anniversary Figure Friday Friends and Plotly Dash! :tada:I started here a few months ago and I’ve learned a lot of new things both professionally and technically. Thanks to @adamschroeder and everyone here.:folded_hands:t3:

3 Likes