世卫组织的钱从哪里来到哪里去
世界卫生组织(WHO)是有影响力的全球性组织之一,本实验获取了 WHO 官网发布的 2018 - 2019 年财政预算数据,对其资金来源及使用情况做了分析,研究及可视化了其资金构成、资助项目及资金流向等问题。
输入并执行魔法命令 %matplotlib inline 。
%matplotlib inline
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore") # 屏蔽报警
数据准备
本数据集来自 WHO,WHO 在其官网向全球公开透明其资金来源情况以及援助项目情况。本数据集统计了在 2018 - 2019 年间指定自愿捐款(Specified voluntary contributions)向 WHO 捐助的资金及 WHO 使用其资金的情况。
导入数据进行初步处理并查看前 5 行。
import pandas as pd
df=pd.read_excel('https://labfile.oss.aliyuncs.com/courses/3023/World_health_organization_clearning.xlsx')
# 添加各地区代码对应的地区全称
cont_code_name={
'AF':'Africa',
'EM':'Eastern Mediterranean',
'HQ':'Headquarters',
'SE':'South East Asia',
'EU':'Europe',
'WP':'Western Pacific',
'AM':'Americas',
}
df['cont_name']=df['cont'].apply(lambda x:cont_code_name[x])
df.head()
从运行结果可知,数据集由 5 个字段构成,各字段解释见下表:
世卫组织的资金构成
根据 WHO 官方网站公开披露的 2018 - 2019 两年预算显示,WHO 的资金来源主要分为会费分摊(Assessed contributions)与接受捐赠两种形式,接受捐赠又分为指定自愿捐款(Specified voluntary contributions),核心自愿捐助(Core voluntary contributions),PIP 捐款(PIP Contributions)。
运行以下代码,可视化 WHO 的资金来源结构。
plt.rcParams['figure.figsize'] = (10, 6)
funding_structure = {
'Assessed contributions': 956900000,
'Specified voluntary contributions': 4328057662,
'Core voluntary contributions': 160592410.5,
'PIP Contributions': 178053223,
}
plt.pie(
x=funding_structure.values(),
labels=funding_structure.keys(), # 标签值
explode=[0.1, 0, 0, 0], # 饼图中每一块相对中心的偏移
textprops={'size': 20}, # 饼图中文本格式的设置
autopct='%.2f%%' # 文本字符显示格式
)
plt.title('The Contributor Funding Structure of World Health Organization', size=22)
从运行结果可知,WHO 成员国会费占其总预算比例相对较低,只有 17%,剩余部分全部来源于自愿捐款,而本实验数据集涉及的指定自愿捐款(Specified voluntary contributions)占到所有捐款的比例最高,为总预算的 76.96% 。
资助组织及被资助项目结构
按资金流向分组聚合,并将聚合结果归一化后拼接。
import numpy as np
# 筛选出流入 WHO 的资金,按资助组织进行资助金额的求和并逆序排序
funding_from = df.loc[df['direction'] == 'from'].groupby(
['orgs'])['money'].sum().sort_values(ascending=False)
# 将资金归一化处理
funding_from = funding_from/np.sum(funding_from)
# 同样的方式处理被资助项目
funding_to = df.loc[df['direction'] == 'to'].groupby(
['orgs'])['money'].sum().sort_values(ascending=False)
funding_to = funding_to/np.sum(funding_to)
# 将两个Series对象按列拼接
funding_from_to = pd.concat([funding_from, funding_to])
# 展示处理后的数据
funding_from_to
资助组织及被资助项目金额经过归一化后,其求和值均为 1 ,因此将其可视化到饼图时,每一部分的占比会被压缩到一半(因为饼图的总百分比是 100%,但是两部分求和结果是 200%)。例如,美国(United States of America)在所有资助组织中资助的金额占比为 15.17%,但经拼接后在 funding_from_to 数据并可视化到饼图后,其占比将缩小至 7.585%,因此为了显示 15.17% 数据,需要在 plt.pie 接口参数 autopct 传入格式化函数 autopct_fun,该函数将显示的百分比数据扩大为原值的 2 倍。
plt.rcParams['figure.figsize'] = (15, 15)
def autopct_fun(x):
return '%.2f%%' % (2*x)
plt.pie(x=funding_from_to,
labels=funding_from_to.index,
textprops={'size': 17},
autopct=autopct_fun)
plt.title(
'Where Does the World Health Organization Contributor Funding From and Go ?', size=28)
上面的图中因为类别太多,导致 label 都挤在一起看不清。下面我们对上面的图像进行简化。 具体的 index 的编号可以通过 np.where 来找到。
plt.rcParams['figure.figsize'] = (14, 7)
def autopct_fun(x):
if 2*x < 8:
return None
else:
return '%.2f%%' % (2*x)
plt.pie(x=funding_from_to.values,
labels=list(funding_from_to.index[0:4]) + ['' for i in funding_from_to.index[4:301]] + list(
funding_from_to.index[301:304]) + ['' for i in funding_from_to.index[304:]],
textprops={'size': 13},
autopct=autopct_fun)
plt.title(
'Where Does the World Health Organization Contributor Funding From and Go ?', size=17)
从输出结果可以看出:
WHO 最大的捐助国为美国( United States of America ),占比 15.18%,其次是盖茨基金会( Bill & Melinda Gates Foundation ),占比 12.12%,排名第三的是全球疫苗和免疫联盟( GAVI Alliance ),占比 8.19%;
WHO 最大的投资项目为根除脊髓灰质炎( Polio eradication ),占比 26.55%,其次是提升获得基本保健和营养服务( Increase access to essential healthand nutrition services ),占比 12.05%,排名第三的是可预防疾病疫苗( Vaccine-Preventable Diseases ),占比 8.96%。
不同地区的资助项目
本实验的目的是绘制两个半圆形饼图,内半圆表示各个被资助地区的百分占比,外半圆表示各个地区对应项目的百分占比,且须做到内半圆地区块与外半圆项目块颜色的主色调完全对应。由于本例相对复杂,因此分步骤进行绘图解释:
生成内半圆数据集
选择资金流向为 to 的数据,以地区进行资金聚合、逆序排序并归一化,查看前 5 行数据。
funding_cont_to = df.loc[df['direction'] == 'to'].groupby(
['cont'])['money'].sum().sort_values(ascending=False)
funding_cont_to = funding_cont_to/np.sum(funding_cont_to)
funding_cont_to.head()
内半圆的颜色变化序列
Matplolib 默认颜色序列为 TABLEAU_COLORS,可通过 cs.to_rgba 将其转变为 rgba 颜色元组(plt.pie 接口需要的颜色参数类型为 rgba 或者 rgb 格式),内半圆仍然采用默认序列的颜色。
import matplotlib.colors as cs
start_colors=[cs.to_rgba(value) for value in cs.TABLEAU_COLORS.values()]
start_colors
外半圆的颜色变化序列
外圈的项目颜色块由其对应的内圈的主题颜色确定,例如对于非洲 AF 地区的项目,如果 AF 的色块颜色为蓝色,则非洲区所有项目颜色均为蓝色,但为了将项目进行有效区分,需要将各项目颜色进行渐变色设置,其原理是进行主题色到白色的线性插值。
colors = []
for i, c in enumerate(funding_cont_to.index):
# WHO向每个大区投入的项目,按降序排序
funding_projection = df.loc[(df['direction'] == 'to') & (df['cont'] == c), [
'orgs', 'money']].sort_values('money', ascending=False)
# 获取每个地区总项目数
color_nums = len(funding_projection)
# 每个地区对应项目的主题颜色
r, g, b, a = start_colors[i]
# 根据主题颜色线性地生成由该主题颜色到白色的序列渐变色
for r_new, g_new, b_new in zip(np.linspace(r, 1, color_nums), np.linspace(g, 1, color_nums), np.linspace(b, 1, color_nums)):
colors.append((r_new, g_new, b_new, 1.0))
colors[:5]
外半圆的标签设置
由于外半圆项目数量较大,因此每个地区只显示排名前 1 的项目,其余项目不作显示。
labels = []
for i, c in enumerate(funding_cont_to.index):
funding_projection = df.loc[(df['direction'] == 'to') & (df['cont'] == c), [
'orgs', 'money']].sort_values('money', ascending=False)
show_top = 1
label = np.append(funding_projection['orgs'][:show_top],
['']*(len(funding_projection)-show_top))
labels = np.append(labels, label)
labels[:5]
生成外半圆数据集
funding_proj_to = pd.DataFrame()
for i, c in enumerate(funding_cont_to.index):
funding_projection = df.loc[(df['direction'] == 'to') & (df['cont'] == c), [
'orgs', 'money']].sort_values('money', ascending=False)
# 合并所有地区的数据
funding_proj_to = pd.concat([funding_proj_to, funding_projection])
funding_proj_to.set_index(['orgs'], inplace=True)
funding_proj_to = funding_proj_to/np.sum(funding_proj_to)
funding_proj_to.head()
绘图
半圆图的绘制原理:当传入 plt.pie 接口参数 x 的求和值小于 1 时,差额部分将形成饼图的缺口,因此将归一化的 x 数据乘以 0.5 即可获得半圆,同理,此时的百分占比被压缩成原比例的一半,需要通过 autopct_fun 函数将其显示比例数据作修正。
plt.rcParams['figure.figsize'] = (15, 15)
def autopct_fun(x):
return '%.2f%%' % (2*x)
# 外圈绘制项目
plt.pie(x=0.5*funding_proj_to, # 当x求和小于1时,饼状图则按比例显示,剩余部分为白色,此处将数据乘以 0.5,绘制结果为半圆
labels=labels,
radius=1, # 饼图半径
wedgeprops={'width': 0.4}, # 饼的径向宽度
colors=colors, # 每个色块的颜色,此处颜色的格式是 rgba 元组
textprops={'size': 17},
autopct='')
# 内圈绘制地区
plt.pie(x=0.5*funding_cont_to,
labels=funding_cont_to.index,
radius=0.45,
pctdistance=0.8, # 饼状图中表示 比例数据的文本 相对中心的位置
wedgeprops={'width': 0.3},
colors=cs.TABLEAU_COLORS,
textprops={'size': 17},
autopct=autopct_fun)
plt.ylim(0.5, 1.2)
plt.title('The Funded Zones and The Top 1 Important Funded Programs', size=30)
从输出结果可以看出,已实现了地区与地区项目的颜色对应,且地区项目由主题色到白色进行渐变显示。下表列出了每个地区排名前 2 的被资助项目:
非洲被资助项目的相对比例
非洲毫无疑问是 WHO 项目重点投资地区,以该地区最大的投资项目为对比对象,研究其他项目相对其所占比例。通过数据聚合并逆序排序的方式,获取最大项目的值,以该值为分母进行数据的归一化,此处为美观考虑,将最大项目的弧度设置为半圆。
funding_to = df.loc[(df['direction'] == 'to') & (df['cont'] == 'AF')].groupby(
['orgs'])['money'].sum().sort_values(ascending=False)
funding_to = funding_to/np.sum(funding_to)
first_large = funding_to[0]
# 计算各项目相对最大投资项目的百分比,最大投资项目比例设定为 0.5,目的是为了绘制半圆
funding_to_related = funding_to/first_large*0.5
funding_to_related.head(10).index[:10]
通过调节接口中圆圈的半径 radius 及宽度 wedgeprops 两个参数,将非洲地区被投资项目逐环式地向内绘制,最外径为投资额最大的项目,依次向内,项目投资额逐渐递减。
width = 0.03
fig, ax = plt.subplots(1, 1, figsize=(15, 15))
for i, projection_percent in enumerate(funding_to_related):
ax.pie(x=projection_percent,
startangle=90,
radius=1.0-i*width,
labels=['a'], # 输入图例文本占位符
autopct='',
textprops={'size': 0},
wedgeprops={'width': width}
)
# 修改图例为各个项目的项目名称
h, l = ax.get_legend_handles_labels()
plt.legend(handles=h,
labels=list(funding_to_related.index),
ncol=2,
bbox_to_anchor=(0.55, 0.85),
loc='upper left',
frameon=False,
fontsize=20)
plt.title('Funded Programs of Africa Weights Related to Polio Eradication',
fontsize=30,
ha='left')
运行结果可以看到各个项目相对最大项目的比例,可以看出各项目相对根除骨髓灰质炎项目的相对比例大小,整理后,相对比例如下表所示:
资助项目的资金规模分布
根据被投资项目的项目金额,将项目分成小于 1 千万,1 千万- 1 亿,大于 1 亿三个等级(单位:美金)。
funding_programs = df.loc[df['direction'] == 'to'].copy()
# 1M=100万=1e6
bins = [funding_programs['money'].min(), 1e7, 1e8,
funding_programs['money'].max()]
funding_programs['program_structure'] = pd.cut(funding_programs['money'],
bins=bins,
labels=[
'<10M', '10M-100M', '>100M']
)
funding_programs['money'] = funding_programs['money'] / \
np.sum(funding_programs['money'])
funding_programs.head()
按投资项目金额及所在地区,研究各地区项目资金结构。
# 设定随机数种子
np.random.seed(121)
row_names = ['<10M', '10M-100M', '>100M']
col_names = ['AF', 'EM', 'HQ', 'EU', 'SE', 'WP', 'AM']
fig, axs = plt.subplots(
nrows=len(row_names),
ncols=len(col_names),
figsize=(len(row_names)*7, len(col_names)*1))
for i, s in enumerate(row_names):
for j, c in enumerate(col_names):
ax = axs[i, j]
x = funding_programs.loc[(funding_programs['cont'] == c) & (
funding_programs['program_structure'] == s)]['money'].sum()
# 绘制浅色背景
ax.pie([1], radius=0.8, colors=[(0.97, 0.97, 0.97)])
# 绘制深色主体
ax.pie(x=[x],
startangle=90,
radius=1.0,
labels=['%.2f%%' % round(x*100, 2)],
autopct='%.2f%%',
colors=np.random.random(size=(1, 3)), # 随机生成一个颜色元组(rgb元组)
textprops={'size': 0},
)
ax.legend(frameon=False, fontsize=15)
ax.set_title(label='%s : %s' % (c, s), size=15)
fig.suptitle('The Funded Programs Budget Structure of Different Zones', size=20)
从输出结果可以看出:
超大金额(大于 1 亿)的投资项目集中在 Africa,Eastern Mediterranean 两个地区,Europe 等 4 个地区没有该规模项目;
中等规模项目集中在 Headquarters 地区。
被资助项目的资金规模分布-树形地图
!pip install pyecharts==1.7.1
另一种可视化结构的绘图对象为 pyecharts 包提供的 TreeMap 类,与传统的饼图不同,TreeMap 用一个个的方格表示各元素相对整体的比例。
from pyecharts import options as opts
from pyecharts.charts import TreeMap
funding_to = df.loc[df['direction'] == 'to'].groupby(
['orgs'])['money'].sum().sort_values(ascending=False)
funding_to = funding_to/np.sum(funding_to)
# 生成 TreeMap 数据,该数据为 json 格式
funding_to_treemap = [{'value': value, 'name': name}
for name, value in zip(funding_to.index, funding_to)]
tree = TreeMap()
tree.add("Funded Budget",
funding_to_treemap,
label_opts=opts.LabelOpts(position="inside", font_size=12),
drilldown_icon=" ",
leaf_depth=1)
tree.set_global_opts(
title_opts=opts.TitleOpts(
title="The Funded Programs Budget Structure",),
legend_opts=opts.LegendOpts(
pos_top='5%', textstyle_opts=opts.TextStyleOpts(font_size=15))
)
tree.render_notebook()
从输出结果可以看出前 5 类投资项目将近占到了总项目投资额的 60% 以上。
带钻取结构的树形图
TreeMap 类还提供了数据钻取功能,原理是传入的数据文件增加一个 children 字典项,children 的值为需要被钻取的 TreeMap 数据,以下代码生成了带钻取功能的数据。
数据准备
funding_to = df.loc[df['direction'] == 'to'].groupby(
['cont_name', 'orgs'], as_index=False)['money'].sum()
funding_to['money'] = funding_to['money']/np.sum(funding_to['money'])
funding_to_treemap = [
{'value': funding_to.loc[funding_to['cont_name'] == name]['money'].sum(),
'name': name,
'children':[
{
'value': funding_to.loc[(funding_to['cont_name'] == name) & (funding_to['orgs'] == org)]['money'].sum(),
'name': org,
}
for org in list(set(funding_to.loc[funding_to['cont_name'] == name, 'orgs']))
]
}
for name in list(set(funding_to['cont_name']))
]
绘图
tree = TreeMap()
tree.add("Funded Budget",
funding_to_treemap,
label_opts=opts.LabelOpts(position="inside", font_size=20),
drilldown_icon=" ",
leaf_depth=1 # 初始化图形时,显示第一层(即最高级)
)
tree.set_global_opts(
title_opts=opts.TitleOpts(
title="The Funded Programs Budget Structure of Different Zones",),
legend_opts=opts.LegendOpts(
pos_top='5%', textstyle_opts=opts.TextStyleOpts(font_size=15))
)
tree.render_notebook()
运行以上代码,输出的是地区级的结构图,当鼠标点击其中一块区域时,树形图向下钻取该区域子项目的结构图,可通过图最下面的层级按钮可以回到上一级。
被资助项目的资金规模分布-太阳花图
同样的数据结构传入 Sunburst 类,可以获得带有钻取功能的太阳花图,太阳花图带有两个饼图,内部饼图代表父级的结构组成,点击任一颜色块,数据向下钻取到该层级下子项的饼图。
from pyecharts.charts import Sunburst
funding_to = df.loc[df['direction'] == 'to'].groupby(
['cont_name', 'orgs'], as_index=False)['money'].sum()
funding_to['money'] = funding_to['money']/np.sum(funding_to['money'])
# 数据结构与树形图完全一致
funding_to_treemap = [
{'value': funding_to.loc[funding_to['cont_name'] == name]['money'].sum(),
'name': name,
'children':[
{
'value': funding_to.loc[(funding_to['cont_name'] == name) & (funding_to['orgs'] == org)]['money'].sum(),
'name': org,
}
for org in list(set(funding_to.loc[funding_to['cont_name'] == name, 'orgs']))
]
}
for name in list(set(funding_to['cont_name']))
]
sun = Sunburst()
sun.add(
"",
data_pair=funding_to_treemap,
highlight_policy="ancestor",
radius=[0, "95%"],
sort_="null",
label_opts=opts.LabelOpts(is_show=False, position='inside', font_size=5),
levels=[
{ # 外圈设置
"r0": "65%", # 内圈半径
"r": "95%", # 外圈半径
"itemStyle": {"borderWidth": 2},
"label": {"rotate": "tangential"},
},
{ # 内圈设置
"r0": "25%",
"r": "45%",
},
],
)
sun.set_global_opts(title_opts=opts.TitleOpts(title="The Funded Programs Budget Structure of Different Zones"))
sun.set_series_opts(label_opts=opts.LabelOpts(formatter="{b}"))
sun.render_notebook()
世卫组织的资金流向
饼状图是表达组成关系或者总分关系的最佳绘图对象,树形图和太阳花图在饼图的基础上,增加了交互式地钻取功能,实现了不同层级结构的总分表达。在之前的实验中,我们借助这几类对象研究了世卫组织的资金来源、资助项目,以下我们进一步研究其资金的流向。
描述流向的绘图对象主要是 桑基图,桑基图早期的应用主要是能量分布,后期在金融领域大放异彩,再后来被广泛应用到经管领域。桑基图将带有流向特征的节点通过一定宽度的线连接起来,线的疏密代表任意两节点的能量强弱。桑基图绘图的主要难点在于数据准备工作,以下分布展示其过程:
将资金量小于阈值的组织或者项目设置为其他类别
为减少桑基图可视化的节点数量,避免图片过大反而带来阅读困难,将所有资助组织和被资助项目金额小于 1000 万的均设置成其他资助者(other contributions)或其他项目(other programs)。
data = df.copy()
limit_money = 1e7
data['orgs_adjust'] = data['orgs']
data.loc[(data['direction'] == 'from') &
(data['money'] <= limit_money), 'orgs_adjust'] = 'other contributions'
data.loc[(data['direction'] == 'to') &
(data['money'] <= limit_money), 'orgs_adjust'] = 'other programs'
data.head()
数据构建
桑基图接口主要传入两个数据,一个为 nodes,一个为 links。nodes 表示需要可视化的节点,此处 nodes 由各个组织名称和各个地区名称构成,对其进行去重和拼接;links 为一个列表,列表中每个元素为一个字典,由 source,target,value 三个键构成,分别代表起始节点、结束节点和连接两节点的能量值,source 和 target 须包含在 nodes 节点列表中。节点连接一共分成两个部分,出资国家与被资助地区为资金流入关系,被资助地区与被资助项目为资金流出关系,因此在生成节点连接数据集时需要考虑 source 和 target 的方向,以下代码生成了用于桑基图接口的数据。
sankey_value='money'
# 去重后拼接
node_lists = np.append(np.unique(data['orgs_adjust']),(np.unique(data['cont'])))
nodes = [
{
'name': node
}
for node in node_lists
]
links = [
{
"source": s,
"target": t,
"value": v
}
for s, t, v in zip(data.loc[data['direction'] == 'from', 'orgs_adjust'],
data.loc[data['direction'] == 'from', 'cont'],
data.loc[data['direction'] == 'from', sankey_value],
)
]+[
{
"source": s,
"target": t,
"value": v
}
for t, s, v in zip(data.loc[data['direction'] == 'to', 'orgs_adjust'],
data.loc[data['direction'] == 'to', 'cont'],
data.loc[data['direction'] == 'to', sankey_value],
)
]
桑基图的绘制
桑基图的接口调用方式和大多数 pyecharts 接口类似。运行以下代码,可生成世卫组织 2018 -2019 年指定自愿捐款的资金流向图,该图细致地展示了每一笔资金的使用,在 WHO 官网上,也有一张类似的图,该图生动地公开了 WHO 经费使用情况,便于读者了解 WHO 的资金预算流向情况。
from pyecharts.charts import Sankey
init_opts=opts.InitOpts(width='1000px',height='1000px')
sankey = Sankey(init_opts=init_opts) # 初始化图片的宽度和高度
sankey.add(
series_name="",
nodes=nodes,
links=links,
node_gap=7, # 节点间距
label_opts=opts.LabelOpts(position='right',font_size=15), # 设置节点文本
itemstyle_opts=opts.ItemStyleOpts(border_width=2), # 设置节点宽度
linestyle_opt=opts.LineStyleOpts(color="source", width=2,curve=0.5, opacity=0.5), # 设置线型
tooltip_opts=opts.TooltipOpts(trigger_on="click") # 设置交互方式
)
sankey.set_global_opts(
title_opts=opts.TitleOpts(title="The Funding Budget Flows of Specified Voluntary Contributions of WHO in Year 2018 - 2019"),
)
sankey.render_notebook()