# -*- coding: utf-8 -*-
"""
Created on Sun Dec 28 11:25:02 2025
@author: win11
"""
from dash import Dash, html, dcc, callback, Output, Input, State
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd
import pycountry
import numpy as np
from dash_iconify import DashIconify
df_raw = pd.read_csv('live_expectancy_at_birth.csv')
#drop column indicator_name
df = df_raw.drop("Indicator Name", axis='columns')
#keep only real countries, remove aggregations like Western Europe
iso3_countries = {c.alpha_3 for c in pycountry.countries}
dfc = df[df["Country Code"].isin(iso3_countries)]
#melt table
#all columns except country and country code
columns = dfc.columns[2:]
dfm = pd.melt(dfc, id_vars=['Country Name', 'Country Code'], value_vars=columns, var_name='Year', value_name='Age Expectancy')
dfm["Year"] = dfm["Year"].astype(int)
dfm = dfm.sort_values(["Country Code", "Year"])
######### UI-STUFF ########
#select metric map => affects primary barchart.
metric_map = {
"net_change":"Net change",
"trend_slope":"Trend (years/year)",
"max_annual_change":"Max annual change",
"pct_change":"Relative change (%)",
}
metric_descriptions = {
"net_change": "Total difference in life expectancy from the first to the last available year. Shows which countries gained or lost the most overall.",
"trend_slope": "Average annual change in life expectancy. Highlights countries with the fastest long-term improvement or decline.",
"max_annual_change": "Largest single-year change in life expectancy. Captures sudden shocks, crises, or unusual events.",
"pct_change": "Relative change in life expectancy compared to where it started. Shows progress proportionally, useful for countries with lower starting values."
}
######### FUNCTIONS TO CALCULATE METRICS #########################
def compute_change_metrics(group):
g = group.dropna(subset=["Age Expectancy"]).sort_values("Year")
yearly_diff = g["Age Expectancy"].diff()
start_le = g.iloc[0]["Age Expectancy"]
end_le = g.iloc[-1]["Age Expectancy"]
return pd.Series({
"start_year": g.iloc[0]["Year"],
"end_year": g.iloc[-1]["Year"],
"start_le": start_le,
"end_le": end_le,
"net_change": end_le - start_le,
"pct_change": (end_le - start_le) / start_le * 100,
"max_annual_change": yearly_diff.abs().max(),
"volatility": yearly_diff.std(),
"relative_volatility": yearly_diff.std() / g["Age Expectancy"].mean(),
"trend_slope": np.polyfit(g["Year"], g["Age Expectancy"], 1)[0],
"mean_le": g["Age Expectancy"].mean()
})
############ CALCULATE THE NUMBERS ##########################
metrics = (
dfm.groupby(["Country Code", "Country Name"], group_keys=False)
.apply(compute_change_metrics, include_groups=False)
.reset_index()
)
def classify(row):
x_ref = metrics["mean_le"].median()
y_ref = metrics["volatility"].median()
if row["mean_le"] >= x_ref and row["volatility"] < y_ref:
return "Stable, developed systems"
if row["mean_le"] < x_ref and row["volatility"] >= y_ref:
return "Crisis-prone / data issues"
if row["mean_le"] >= x_ref and row["volatility"] >= y_ref:
return "Shocks (pandemics, wars)"
return "Persistent structural issues"
metrics["Country Profile"] = metrics.apply(classify, axis=1)
############ PRIMARY CHART, NET CHANGE IN YEARS TOP 10 #####################
def bar_top10(value):
#value contains the column name in metrics
# "Net change": "net_change",
# "Trend (years/year)": "trend_slope",
# "Max annual change": "max_annual_change",
# "Relative change (%)": "pct_change",
if value == None:
value="net_change"
metric_labels = {
"net_change": "Change in Life Expectancy (years)",
"trend_slope": "Average Annual Change (years/year)",
"max_annual_change": "Largest Year-to-Year Change (years)",
"pct_change": "Relative Change in Life Expectancy (%)"
}
match value:
case "net_change":
plottitle = "Countries with the Largest Overall Change in Life Expectancy"
case "trend_slope":
plottitle = "Fastest Long-Term Change in Life Expectancy"
case "max_annual_change":
plottitle = "Largest Year-to-Year Fluctuations in Life Expectancy"
case "pct_change":
plottitle= "Countries with the Largest Relative Change in Life Expectancy (%)"
case _:
plottitle = "Something went wrong!" # Default case
top_n = 10
plot_df = (
metrics.sort_values(value, ascending=False)
.head(top_n)
)
#correct output for horizontal orientation
#plot_df_out = plot_df.sort_values("net_change", ascending=True).reset_index()
fig = px.bar(
plot_df,
x=value,
y="Country Name",
template="plotly_dark",
orientation="h",
title=plottitle,
labels={value: metric_labels[value], "Country Name": "Country"},
hover_data=[
"start_year",
"end_year",
"start_le",
"end_le"
]
)
fig.update_layout(yaxis=dict(categoryorder="total ascending"))
return fig
profile_descriptions = {
"Stable, developed systems":
"Consistent gains driven by strong healthcare systems, prevention, and social stability.",
"Crisis-prone / data issues":
"Frequent disruptions from conflict, epidemics, economic shocks, or inconsistent reporting.",
"Shocks (pandemics, wars)":
"Generally strong systems, but impacted by major external shocks causing temporary declines.",
"Persistent structural issues":
"Long-term challenges such as poverty, weak healthcare access, and limited infrastructure."
}
def create_scatter_stability(value):
df_plot = metrics.copy()
# assign color groups
df_plot["color_group"] = df_plot["Country Profile"]
# Override the selected country with a highlight
selected_country = value
df_plot.loc[df_plot["Country Name"] == selected_country, "color_group"] = "Selected Country"
color_map = {
"Stable, developed systems": "#1f77b4", # example blue
"Crisis-prone / data issues": "#ff7f0e", # orange
"Shocks (pandemics, wars)": "#2ca02c", # green
"Persistent structural issues": "#d62728",# red
"Selected Country": "#FFFF00" # yellow
}
fig = px.scatter(
df_plot,
x="mean_le",
y="volatility",
template="plotly_dark",
color="color_group",
hover_name="Country Name",
title="Life Expectancy Stability vs Volatility Level",
labels={
"mean_le": "Average Life Expectancy",
"volatility": "Year-to-Year Volatility",
"color_group": "Country Profile"
},
color_discrete_map=color_map
)
fig.update_yaxes(type="log")
return fig
############## APP ####################################
app = Dash(external_stylesheets=[dbc.themes.CYBORG])
app.layout = dbc.Container([
dbc.Row([
dbc.Col([
html.H1(children='In which country has "life expectancy at birth" changed the most?', style={'textAlign':'center',"fontSize":"2rem"}),
html.P("Dataset: Worldbank, year range: 1960 - 2023", style={'textAlign':'center'})
])
]),
dbc.Row([
dbc.Col([
html.H2("Changed the most: different perspectives" , style={'textAlign':'center',"fontSize":"1.5rem","margin":"1rem 0"}),
html.Div(id="top10"),
dcc.Dropdown(metric_map, 'net_change', id='dropdown-metric', style={"margin":"1rem 0"}),
html.H2("Explanation Different Metrics", style={'textAlign':'center',"fontSize":"1.5rem"}),
html.Ul([
html.Li([html.Strong("Net Change: "), metric_descriptions["net_change"]]),
html.Li([html.Strong("Trend Slope: "), metric_descriptions["trend_slope"]]),
html.Li([html.Strong("Max Annual Change: "), metric_descriptions["max_annual_change"]]),
html.Li([html.Strong("Relative Change (%): "), metric_descriptions["pct_change"]])
])
], style={"backgroundColor":"#333"}),
dbc.Col([
html.Div([
html.H2("Stability" , style={'textAlign':'center',"fontSize":"1.5rem","margin":"1rem 0"}),
html.Div(id="scatter-stability"),
dcc.Dropdown(dfm['Country Name'].unique(),placeholder="Select country to highlight", id='dropdown-country', style={"margin":"1rem 0"}),
html.H2("Explanation Country Profiles", style={'textAlign':'center',"fontSize":"1.5rem"}),
html.Ul([
html.Li([html.Strong(k), ": ", v])
for k, v in profile_descriptions.items()
])
])
], style={"backgroundColor":"#333"}),
])
], fluid=False, style={"padding":"2rem"})
@callback(
Output('scatter-stability', 'children'),
Input('dropdown-country', 'value')
)
def update_scatter(value):
return dcc.Graph(figure=create_scatter_stability(value)),
@callback(
Output('top10', 'children'),
Input('dropdown-metric', 'value')
)
def update_top10(value):
return dcc.Graph(figure=bar_top10(value)),
if __name__ == '__main__':
app.run(debug=False)