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.
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__':