Ever wanted to gain insight into the peace levels of all countries in the world, and how they change over time? This dashboard will give you that ability. The code can be run locally, but has been hosted on pythonanywhere also: http://globalpeaceindex.pythonanywhere.com/. Basic hosting is free for now but the page might over time become unacceable (try reloading the page).
Here follows the full code of the data analysis and then using Dash to shape the front-end. Check Github for the jupyter Notebook.
import pandas as pd
import dash
# from countryinfo import CountryInfo
import matplotlib
import geopandas as gpd
file_path = "global_peace_2023.csv"
df = pd.read_csv(file_path)
world_gdf = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
world_gdf = world_gdf.to_crs(
"+proj=eck4 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
)
world_gdf["pop_density"] = world_gdf.pop_est / world_gdf.area * 10**6
df2 = pd.merge(df, world_gdf, left_on="iso3c", right_on="iso_a3", how="left")
df2 = df2.drop(["name", "iso_a3"], axis=1)
mean_scores = df2.groupby("Country")["Overall Scores"].transform("mean")
# Create a new column in the DataFrame with the mean scores
df2["Mean Overal score"] = mean_scores
gdf = gpd.GeoDataFrame(df2, geometry="geometry")
# dash app
from dash import Dash, html, dcc, Input, Output, dash_table
import plotly.graph_objs as go
from sklearn.decomposition import PCA
from sklearn.cluster import AgglomerativeClustering
import pandas as pd
from dash.dependencies import Input, Output
import plotly.figure_factory as ff
from scipy.cluster.hierarchy import linkage
import numpy as np
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt
import numpy as np
pivot_df = df2.pivot(index="Country", columns="year", values="Overall Scores")
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaled_data = scaler.fit_transform(pivot_df)
weights = np.linspace(1, 2, pivot_df.shape[1])
pivot_df = pivot_df.fillna(method="bfill", axis=1)
weighted_data = pivot_df * weights
# Perform the necessary data preprocessing for PCA
pivot_df = df2.pivot(index="Country", columns="year", values="Overall Scores")
pivot_df = pivot_df.fillna(method="bfill", axis=1).reset_index()
hc = AgglomerativeClustering(n_clusters=4, linkage="ward")
cluster_labels = hc.fit_predict(pivot_df.drop(columns="Country"))
pca = PCA(n_components=2)
pca_result = pca.fit_transform(pivot_df.drop(columns="Country"))
# Create a DataFrame with the PCA results, the cluster labels, and the original country names
pca_df = pd.DataFrame(data=pca_result, columns=["PC1", "PC2"])
pca_df["cluster_labels"] = cluster_labels
pca_df["Country"] = pivot_df["Country"]
combined_df = pca_df.merge(gdf[["Country", "continent"]], on="Country", how="left")
# dendrogram data analysis
Z = linkage(weighted_data, method="ward")
fig = ff.create_dendrogram(
weighted_data, orientation="left", labels=weighted_data.index, color_threshold=4.5
)
fig.update_layout(width=600, height=2000)
Europe_mean_scores = (
gdf[gdf["continent"] == "Europe"].groupby("year")["Overall Scores"].mean()
)
Asia_mean_scores = (
gdf[gdf["continent"] == "Asia"].groupby("year")["Overall Scores"].mean()
)
Africa_mean_scores = (
gdf[gdf["continent"] == "Africa"].groupby("year")["Overall Scores"].mean()
)
South_America_mean_scores = (
gdf[gdf["continent"] == "South America"].groupby("year")["Overall Scores"].mean()
)
North_America_mean_scores = (
gdf[gdf["continent"] == "North America"].groupby("year")["Overall Scores"].mean()
)
Oceania_mean_scores = (
gdf[gdf["continent"] == "Oceania"].groupby("year")["Overall Scores"].mean()
)
# Prepare data for Plotly
original_features = [str(year) for year in range(2008, 2024)]
correlation_matrix = pd.DataFrame(
pca.components_.T, index=original_features, columns=["PC1", "PC2"]
)
# Create a text matrix for annotations
text_matrix = [[f"{val:.2f}" for val in row] for row in correlation_matrix.values]
# Create Plotly heatmap figure
heatmap_figure = go.Figure(
data=go.Heatmap(
z=correlation_matrix.values,
x=correlation_matrix.columns,
y=correlation_matrix.index,
colorscale="rdbu",
text=text_matrix,
texttemplate="%{text}",
hoverinfo="none",
)
)
# Update layout for the figure
heatmap_figure.update_layout(
title={
"text": "Correlation between Years<br>and Principal Components",
"y": 0.9,
"x": 0.5,
"xanchor": "center",
"yanchor": "top",
},
title_font=dict(size=20, family="Verdana", color="Black"),
xaxis_title="Principal Components",
yaxis_title="Years",
yaxis_nticks=len(
correlation_matrix.index
),
)
external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
# Define the dropdown options based on your gdf DataFrame
dropdown_options = [
{"label": country, "value": country} for country in gdf["Country"].unique()
] + [
{"label": f"{continent} (Continent Average)", "value": f"{continent}_avg"}
for continent in [
"Europe",
"Asia",
"Africa",
"South America",
"North America",
"Oceania",
]
]
app.layout = html.Div(
[
html.Div(
html.H1(
"🌏 Global Peace Index Analysis 🌍",
style={
"textAlign": "center",
"color": "white",
"fontSize": "30px",
"lineHeight": "60px",
},
),
style={
"backgroundColor": "#333333",
"padding": "20px 10px",
"height": "60px",
},
),
# home page
html.Div(
[
html.Button("About", id="hello-button"),
html.Button("PCA", id="pca-button"),
html.Button("Dendrogram", id="dendrogram-button"),
html.Div(
[
dcc.Dropdown(
id="country-selector",
options=dropdown_options,
value=["Ukraine", "Russia", "Europe_avg"],
multi=True,
),
dcc.Graph(id="score-plot"),
html.Div('''This plot enables comparing any country in the world by its Peace index over time.
It also has the continents mean Peace Index. Note that how higher the Peace Index, the worse it is.
A hypothetical country with no crime or issues would have a Peace Index of 0. The value increases as Peace decreases.
Feel free to play around and compare any countries, it can show interesting insights.''',
style={
"textAlign": "center",
"width": "100%",
"padding": "10px",
},
),
],
style={
"display": "flex",
"flexDirection": "column",
"justifyContent": "center",
"padding": "20px",
"margin": "20px 10% 20px 10%",
"borderRadius": "15px",
"background": "#ffffff",
"boxShadow": "0px 0px 10px #ccc",
},
),
],
id="home-page",
style={"display": "block"},
),
# dendrogram page
html.Div(
[
html.Button("Back", id="dendrogram-back-button"),
html.Div(
[
html.H1(
"Global Peace Index Dendrogram (2008 - 2023)",
style={
"fontFamily": "Verdana",
"fontSize": "20px",
"color": "black",
"fontWeight": "normal",
"textAlign": "center",
},
),
html.Div(
className="row",
children=[
html.Div(
style={
"flex": "1",
"minWidth": "250px",
"overflowX": "auto",
}, # Adjust flex and add minWidth for proper scaling
children=[
dcc.Graph(id="dendrogram-graph", figure=fig)
],
),
# Column for example text (1/3 of the screen width)
html.Div(
style={
"maxWidth": "250px", # Set the maximum width to 250px
"flex": "1 1 auto", # Allow the div to shrink and grow, but according to its content size
"overflow": "auto", # Keep the overflow behavior
"paddingRight": "5px", # Maintain the padding on the right
"textAlign": "left",
},
children=[
html.Br(),
html.Br(),
html.P(
"Quick question: can you hypothesise what makes countries within the same color groups similar?"
),
html.Br(),
html.P(
"""What this dendogram shows are the correlations between peace levels (2008 to 2023) for all countries in the world.
I've weighed the data so correlations between recent years are valued slightly more than correlations 15 years ago. See the code for details."""
),
html.Br(),
html.P(
"""
This dendrogram clusters countries by peace levels, using branches to show
similarities or differences. Shorter branches indicate similar peace statuses,
while longer ones suggest greater disparity. We can clearly see two distinct groups (green + red vs blue + purple),
and we find sub-groups within those.
"""
),
html.Br(),
html.P(
"""The green group seems to consist of countries with the highest amount of peace. In the red group we find
countries with a high amount of peace that seems to trend upwards slower than the green group.
The blue group show countries with a consistent low amount of peace and purple is for countries with a
downward trend (so peace is decreasing).
"""
),
html.Br(),
html.P(
"""Note that a dendogram is used for exploratory analysis. The conclusions above are made after deeper analysis."""
),
],
),
],
style={
"display": "flex",
"flexDirection": "row", # Ensures horizontal layout
"alignItems": "flex-start",
},
),
],
style={
"display": "flex",
"flexDirection": "column", # Ensures vertical stacking
"justifyContent": "center",
"padding": "20px",
"margin": "20px 10% 20px 10%",
"borderRadius": "15px",
"background": "#ffffff",
"boxShadow": "0px 0px 10px #ccc", # Box shadow for depth
},
),
],
id="dendrogram-page",
style={"display": "none"},
),
# About page
html.Div(
[
html.Button("Back", id="back-button"),
html.Div(
html.P(
[
html.H1(
"About",
style={
"fontFamily": "Verdana",
"fontSize": "20px",
"color": "black",
"fontWeight": "normal",
},
),
"""This dashboard was made to gain insights into world-wide peace trends and play around with dash.""",
html.Br(),
"The dataset is from visionofhumanity but can be found on ",
html.A(
"Kaggle.",
href="https://www.kaggle.com/datasets/ddosad/global-peace-index-2023",
target="_blank",
),
html.Br(),
html.A(
"www.alexdevri.es",
href="http://www.alexdevri.es",
target="_blank",
),
" is my portfolio website.",
html.Br(),
"I also have a ",
html.A(
"Linkedin page",
href="https://www.linkedin.com/in/alex-de-vries-nl/",
target="_blank",
),
", feel free to connect.",
html.Br(),
html.Br(),
html.A(
"Github",
href="www.github.com/defreeze",
target="_blank",
),
" has the code stored to build this dashboard in one of its repos,",
html.Br(),
"but there is a good chance you're running this dashboard locally.",
html.Br(),
"So you already have the code ;)",
],
style={"whiteSpace": "pre-wrap", "textAlign": "center"},
),
style={
"display": "flex",
"justifyContent": "center",
"padding": "20px",
"margin": "20px 20% 20px 20%",
"borderRadius": "15px",
"background": "#ffffff",
"boxShadow": "0px 0px 10px #ccc",
},
),
],
id="hello-page",
style={"display": "none"},
),
# PCA page
html.Div(
[
html.Button("Back", id="pca-back-button"),
html.Button("Toggle Centroids", id="toggle-button", n_clicks=0),
html.Div(
[
dcc.Dropdown(
id="country-dropdown",
options=[{"label": "All Countries", "value": "All"}]
+ [
{"label": country, "value": country}
for country in combined_df["Country"].unique()
],
value="All",
multi=True,
clearable=False,
searchable=True,
),
dcc.Graph(id="cluster-graph"),
html.Div(
dcc.Graph(id="pca-heatmap", figure=heatmap_figure),
style={
"maxWidth": "500px",
"margin": "0 auto",
"justifyContent": "center",
},
),
],
style={
"padding": "20px",
"margin": "20px 10% 20px 10%",
"justifyContent": "center",
"alignItems": "center",
"borderRadius": "15px",
"background": "#ffffff",
"boxShadow": "0px 0px 10px #ccc",
},
),
],
id="pca-page",
style={"display": "none"},
),
]
)
# Callback to switch between home, about, PCA, and dendrogram pages
@app.callback(
[
Output("home-page", "style"),
Output("hello-page", "style"),
Output("pca-page", "style"),
Output("dendrogram-page", "style"),
],
[
Input("hello-button", "n_clicks"),
Input("back-button", "n_clicks"),
Input("pca-button", "n_clicks"),
Input("pca-back-button", "n_clicks"),
Input("dendrogram-button", "n_clicks"),
Input("dendrogram-back-button", "n_clicks"),
],
prevent_initial_call=True,
)
def display_page(
hello_n_clicks,
back_n_clicks,
pca_n_clicks,
pca_back_n_clicks,
dendrogram_n_clicks,
dendrogram_back_n_clicks,
):
ctx = dash.callback_context
if not ctx.triggered:
return (
{"display": "block"},
{"display": "none"},
{"display": "none"},
{"display": "none"},
)
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
if button_id == "hello-button":
return (
{"display": "none"},
{"display": "block"},
{"display": "none"},
{"display": "none"},
)
elif button_id == "back-button":
return (
{"display": "block"},
{"display": "none"},
{"display": "none"},
{"display": "none"},
)
elif button_id == "pca-button":
return (
{"display": "none"},
{"display": "none"},
{"display": "block"},
{"display": "none"},
)
elif button_id == "pca-back-button":
return (
{"display": "block"},
{"display": "none"},
{"display": "none"},
{"display": "none"},
)
elif button_id == "dendrogram-button":
return (
{"display": "none"},
{"display": "none"},
{"display": "none"},
{"display": "block"},
)
elif button_id == "dendrogram-back-button":
return (
{"display": "block"},
{"display": "none"},
{"display": "none"},
{"display": "none"},
)
return (
{"display": "block"},
{"display": "none"},
{"display": "none"},
{"display": "none"},
)
# Callback to update the score-plot graph based on the dropdown selection
@app.callback(Output("score-plot", "figure"), [Input("country-selector", "value")])
def update_graph(selected_values):
selected_countries = [val for val in selected_values if "_avg" not in val]
selected_continents = [
val.replace("_avg", "") for val in selected_values if "_avg" in val
]
mean_scores_by_continent = {
"Europe": Europe_mean_scores,
"Asia": Asia_mean_scores,
"Africa": Africa_mean_scores,
"South America": South_America_mean_scores,
"North America": North_America_mean_scores,
"Oceania": Oceania_mean_scores,
}
traces = []
if selected_countries:
filtered_df = gdf[gdf["Country"].isin(selected_countries)]
for country in selected_countries:
country_data = filtered_df[filtered_df["Country"] == country]
traces.append(
go.Scatter(
x=country_data["year"],
y=country_data["Overall Scores"],
mode="lines",
name=country,
)
)
for continent in selected_continents:
continent_data = mean_scores_by_continent[continent].reset_index()
traces.append(
go.Scatter(
x=continent_data["year"],
y=continent_data["Overall Scores"],
mode="lines",
name=f"{continent} Average",
line=dict(dash="dot"),
)
)
fig = go.Figure(data=traces)
fig.update_layout(
xaxis=dict(tickmode="array", tickvals=sorted(filtered_df["year"].unique())),
title="Global Peace Index by Year for Countries and Continents",
title_font=dict(size=20, family="Verdana", color="Black"),
title_x=0.5,
)
fig.update_xaxes(title_text="Year")
fig.update_yaxes(title_text="Global Peace Index (lower is better)")
return fig
# Callback to update the PCA cluster graph
@app.callback(
Output("cluster-graph", "figure"),
[
Input("country-dropdown", "value"),
Input("toggle-button", "n_clicks"),
Input("pca-button", "n_clicks"),
],
prevent_initial_call=True,
)
def update_cluster_graph(selected_countries, toggle_n_clicks, pca_n_clicks):
ctx = dash.callback_context
if not ctx.triggered:
raise dash.exceptions.PreventUpdate
trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
show_centroids = (
toggle_n_clicks % 2 == 1 if trigger_id == "toggle-button" else False
)
if (
trigger_id == "pca-button"
or selected_countries == "All"
or "All" in selected_countries
):
filtered_df = combined_df.copy()
else:
filtered_df = combined_df[combined_df["Country"].isin(selected_countries)]
fig = go.Figure()
for continent in filtered_df["continent"].unique():
continent_df = filtered_df[filtered_df["continent"] == continent]
fig.add_trace(
go.Scatter(
x=continent_df["PC1"],
y=continent_df["PC2"],
mode="markers",
marker=dict(
size=12, opacity=0.8, line=dict(width=0.5, color="DarkSlateGrey")
),
name=continent,
text=continent_df["Country"],
)
)
if show_centroids:
centroids = (
filtered_df.groupby("cluster_labels")[["PC1", "PC2"]].mean().reset_index()
)
fig.add_trace(
go.Scatter(
x=centroids["PC1"],
y=centroids["PC2"],
mode="markers",
marker=dict(
color="white",
size=12,
opacity=1,
symbol="circle",
line=dict(color="black", width=2),
),
text=centroids["cluster_labels"],
textposition="top center",
name="Centroids",
)
)
centroids = centroids.sort_values(by="PC1")
pc1_values = centroids["PC1"].values
for i in range(len(pc1_values) - 1):
mid_x = (pc1_values[i] + pc1_values[i + 1]) / 2
fig.add_shape(
type="line",
x0=mid_x,
y0=filtered_df["PC2"].min(),
x1=mid_x,
y1=filtered_df["PC2"].max(),
line=dict(color="grey", width=1, dash="dot"),
)
# Update the layout of the figure
fig.update_layout(
title="GPI Principle Component Analysis of all countries (97.49% variance explained)",
title_font=dict(size=20, family="Verdana", color="Black"),
title_x=0.5,
xaxis_title="Principal Component 1",
yaxis_title="Principal Component 2",
hovermode="closest",
legend_title_text="Continent",
)
return fig
# Run the server
if __name__ == "__main__":
app.run_server(port=8054, debug=True)
If you scrolled all the way to here you are rewarded with a free cat: 🐈