前沿
通过上一节谈谈Python实战数据可视化之pygal模块(基础篇)的学习,我们对pygal模块的使用有了初步的了解,本节将以实战项目来加深pygal模块的使用。从网上可以下载JSON格式的人口数据,并使用json模块来处理它们,pygal模块提供了一个适合初学者使用的地图创建工具,我们将使用它来对人口数据进行可视化,以探索全球人口的分布情况。针对JSON格式的人口数据文件,可以通过谈谈Python实战数据可视化之matplotlib模块(实战篇)章节的配套资源来下载。对于本人在学习和编码过程种遇到的问题,我都会逐一解决。
小插曲之执行效率
我在学习过程中,走各方论坛,发现一个有趣的帖子。是针对Python执行效率问题的探究------加一行代码让python的运行速度提高100倍。什么代码这么强大?我们测试看看,从1一直累加到1亿。
(1)原始代码:
import time
def foo(x, y):
tt = time.time() # time.time()函数返回当前时间的时间戳(1970 纪元年后经过的浮点秒数)
s = 0
for i in range(x, y):
s += i
print('Time used: {} sec'.format(time.time() - tt))
return s
print(foo(1, 100000000))
什么是时间戳?时间戳表示的是从 1970 年 1 月 1 日 00:00:00 开始按秒计算的偏移量(time.gmtime(0))此模块中的函数无法处理 1970 纪元年以前的日期和时间或太遥远的未来(处理极限取决于 C 函数库,对于 32 位系统来说,是 2038 年)。
运行结果如下:
(2)加一行代码,再看看结果:
from numba import jit # 添加的代码
import time
@jit # 添加的代码
def foo(x, y):
tt = time.time() # time.time()函数返回当前时间的时间戳(1970 纪元年后经过的浮点秒数)
s = 0
for i in range(x, y):
s += i
print('Time used: {} sec'.format(time.time() - tt))
return s
print(foo(1, 100000000))
运行结果如下:
总结:原始代码测试出来的是23sec,加了一行代码就编程0.25sec了,好像真的变快了将近100倍耶。具体实现原理貌似有点复杂,等以后知识面广了再研究内部原理吧。
JSON格式数据
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写。例如:
[
{
"Country Name": "Arab World",
"Country Code": "ARB",
"Year": "1960",
"Value": "96388069"
},
{
"Country Name": "Arab World",
"Country Code": "ARB",
"Year": "1961",
"Value": "98882541.4"
},
....
....
....
可以看出,这个文件实际上就是一个很长的Python列表,其中每个元素都是一个包含四个键的字典:国家名、国别码、年份以及表示人口数量的值。
提取JSON格式文件的数据
在工程目录下,创建一个world_population.py文件,并将population_data.json格式文件放到工程目录下。然后编写以下代码尝试提取经json模块转化后的格式数据:
# 导入json模块分析JSON格式文件
import json
filename = 'population_data.json'
with open(filename) as f:
# 函数json.load()将数据(文件对象)转换为Python能处理的格式,
pop_data = json.load(f) # pop_data是一个列表,每个元素都包含一个四个键的字典
for pop_dict in pop_data:
# 只刷选出2010年份的国家人口数量
if pop_dict['Year'] == '2010':
# 将每个国家的国家名、国家人口数保存并打印输出
country_name = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
print(country_name + ":" + str(population))
运行结果如下:
需要注意的是,上面代码获取pop_dict['Value']的值是一个字符串,而后面我们进行数据可视化时,人口数量必须使用数值才行,所以,我们先转化为float类型,再转化为int类型。为什么不直接转化为Int类型?这是因为当for循环遍历到人口数值是包含小数点的字符串(例如:'1127437398.85751')时,Python不能直接转化为整数,不然会出现类似下面的报错:
为了消除这种错误,正确的做法是先将'1127437398.85751'字符串转化为float类型(1127437398.85751),再转化为Int类型(1127437398)。
获取两个字母的国别码
Pygal中的地图制作工具要求数据为特定的格式:用国别码表示国家,以及用数字表示人口数量。最重要的问题是,population_data.json中包含的是三个字母的国别码,但Pygal使用两个字母的国别码(存储在i18n模块中,其实是在该模块的一个字典COUNTRIES里,该字典包含的键和值分别为两个字母的国别码和国家名)来表示国家。所以我们要解决的问题就是根据国家名在i18n模块中的字典COUNTRIES里获取两个字母的国别码。这样就可以在世界地图上表示的国别码和人口数量分别使用字典COUNTRIES里的两个字母的国别码和population_data.json文件里的人口数量。好了,说了解决问题的流程,现在我们尝试使用i18n模块来获取字典COUNTRIES里的键和值,先在工程目录下创建一个country_codes.py文件。
需要注意的是书本P327页的16.2.4小节,导入i18n模块的方法对于现在来说已经不适用了。如果导入模块的代码写“from pygal.i18n import COUNTRIES”就会报以下的错误:
应该改为“from pygal_maps_world.i18n import COUNTRIES”才行。
代码如下:
# 返回il8n模块中COUNTRIES字典中对应国家名的国别码
from pygal_maps_world.i18n import COUNTRIES
def get_country_code(country_name):
for code, name in COUNTRIES.items(): # 返回字典的所有键值对
if name == country_name: # 根据国家名返回两个字母的国别码
return code
return None # 如果没有找到则返回None
修改world_population.py文件的代码,代码如下:
# 导入json模块分析JSON格式文件
import json
from country_codes import get_country_code
filename = 'population_data.json'
with open(filename) as f:
# 函数json.load()将数据(文件对象)转换为Python能处理的格式,
pop_data = json.load(f) # pop_data是一个列表,每个元素都包含一个四个键的字典
for pop_dict in pop_data:
# 只刷选出2010年份的国家人口数量
if pop_dict['Year'] == '2010':
# 将每个国家的国家名、国家人口数保存并打印输出
country_name = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
code = get_country_code(country_name) # 将population_data.json文件获取的国家名传入函数,若存在则返回对应的国别码
if code: # 如果存在则输出国家名对应的国别码
print(code + ":" + str(population))
else:
print('ERROR - '+country_name)
运行结果如下:
从上图可以看出,其实有相当一部分国家没有对应的国别码,导致显示错误消息的原因有两个。第一,并非所有人口数量都是国家,有些是地区和经济类群。第二,有些统计数据使用了不同的完整国家名,所以识别不到。
制作世界地图
有了两个字母的国别码后,我们可以进行以下步骤:
1.构造虚拟数据制作一个世界地图显示指定了国别码的国家有哪些,还有呈现人口数量,来感受一下世界地图的宏伟。
2.绘制一个2010年真实数据的完整的世界人口地图图表。
3.根据人口数量将国家分组
4.世界人口地图图表进行样式优化处理。
(1)制作一个模拟数据世界地图
在此之前,P329页16.2.5小节的代码中调用函数创建世界地图的方法对于现在也不适用了,如果代码写“wm = pygal.Worldmap() ”会报以下错误:
将代码改为“wm = pygal.maps.world.World()”就可以通过了。代码如下:
import pygal
wm = pygal.maps.world.World() # 创建一个实例
wm.title = 'North,Central America'
# 利用add函数添加标签和国家名还有人口数量,若参数2是一个列表(只有国家名,没有指定人口数量),默认指定人口数量为1,那么就是使用同一种颜色,并且颜色深浅一样,除非人口数量不同。
# 而参数2如果是一个字典,那么说明指定国家名的同时还指定了人口数量,那么虽然使用同一种类型颜色,但根据人口数量的多少决定颜色的深浅
wm.add('North America', {'ca': 10000, 'mx': 20000, 'us': 30000})
wm.add('Central America', {'bz': 40000, 'cr': 50000, 'gt': 60000, 'hn': 70000, 'ni': 80000, 'pa': 90000, 'sv': 100000})
wm.render_to_file('americas.svg')
将americas.svg放入浏览器中显示,运行结果如下:
上面的人口数据纯属虚构。从上图可以看出,将鼠标移至国家上方便可显示国家名和人口数,North America的三个国家分别使用同一种、但深浅不一的颜色来表示,其中人口数量越大,颜色越深。而Central America的多个国家也是同样如此。
(2)绘制完整的世界人口地图
要呈现其他国家的人口数量,需要将前面处理的数据(两个字母的国别码和对应国家的人口数量)转换为Pygal要求的字典格式(即作为实参传入add函数的第二个形参)。代码如下:
import json
import pygal
from country_codes import get_country_code
filename = 'population_data.json'
with open(filename) as f:
# 函数json.load()将数据(文件对象)转换为Python能处理的格式,
pop_data = json.load(f) # pop_data是一个列表,每个元素都包含一个四个键的字典
cc_populations = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country_name = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
code = get_country_code(country_name)
if code:
cc_populations[code] = population
wm = pygal.maps.world.World()
wm.title = 'World Population in 2010,by Country'
wm.add('2010',cc_populations)
wm.render_to_file('world_population.svg')
运行结果如下:
根据上面可以总结出:若将整个世界所有国家的国别码和人口数量都放进一个字典里,再调用一个add函数添加进世界地图,那么就会把这个字典当成一个组,那么在世界地图显示时,使用同一种、但深浅不一的红颜色来表示,其中人口数量越大,颜色越深。再想想,这可不太行,因为整体来看,很难反映其人口数量的差别,为了解决这个问题,我们究其根源,主要是我们只使用了一个字典和只调用了一个add函数来表示整个世界的原因导致颜色单一,解决办法是我们可以对其进行分组处理,人口数量多的为一组,人口数量中等的为一组,人口数量少的为一组,那么就分为三组了。
根据人口数量将国家分组
针对上一节结论的分析,我们这一小节将采用分组的方式反映人口数量的差别。根据人口数量分成三组:少于1000万的、介于1000万和10亿之间的以及超过10亿的。
代码如下:
import json
import pygal
from country_codes import get_country_code
filename = 'population_data.json'
with open(filename) as f:
# 函数json.load()将数据(文件对象)转换为Python能处理的格式,
pop_data = json.load(f) # pop_data是一个列表,每个元素都包含一个四个键的字典
cc_populations = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country_name = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
code = get_country_code(country_name)
if code:
cc_populations[code] = population
# 根据人口数量将所有的国家分成三组
cc_pops_1, cc_pops_2, cc_pops_3 = {}, {}, {}
for cc, pop in cc_populations.items():
if pop < 10000000:
cc_pops_1[cc] = pop
elif pop < 1000000000:
cc_pops_2[cc] = pop
else:
cc_pops_3[cc] = pop
wm = pygal.maps.world.World() # 创建一个实例
wm.title = 'World Population in 2010,by Country'
wm.add('0-10m', cc_pops_1)
wm.add('10m-1bn', cc_pops_2)
wm.add('>1bn', cc_pops_3)
wm.render_to_file('world_population.svg')
运行结果如下:
从上图可以看出,世界地图使用三种不同的颜色,更直观地看出人口数量的差别,在每组中,各个国家都按人口从少到多以浅到深的颜色。其中中国和印度是大于10亿人的国家。
世界人口地图图表进行样式优化处理
前面的案例,采用默认的颜色设置不怎么好看,我们可以使用Pygal样式设置指令来调整颜色。Pygal样式存储在模块style中,我们从这个模块中导入了RotateStyle类,创建这个类的实例时,需要提供一个实参 —— 十六进制的 RGB 颜色。十六进制格式 的 RGB 颜色是一个以井号( # )打头的字符串,后面跟着 6 个字符,其中前两个字符表示红色分量,接下来的两个表示绿色分量,最后两个表示蓝色分量。每个分量的取值范围为 00 (没有相应的颜色) ~FF (包含最多的相应颜色)。Pygal 通常默认使用较暗的颜色主题。使用 LightColorizedStyle 加亮了地图的颜色。
代码如下:
import json
import pygal
from country_codes import get_country_code
from pygal.style import LightColorizedStyle as LCS,RotateStyle as RS # 导入RotateStyle和 LightColorizedStyle,并取了别名,后面调用就采用别名,方便多了
filename = 'population_data.json'
with open(filename) as f:
# 函数json.load()将数据(文件对象)转换为Python能处理的格式,
pop_data = json.load(f) # pop_data是一个列表,每个元素都包含一个四个键的字典
cc_populations = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country_name = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
code = get_country_code(country_name)
if code:
cc_populations[code] = population
# 根据人口数量将所有的国家分成三组
cc_pops_1, cc_pops_2, cc_pops_3 = {}, {}, {}
for cc, pop in cc_populations.items():
if pop < 10000000:
cc_pops_1[cc] = pop
elif pop < 1000000000:
cc_pops_2[cc] = pop
else:
cc_pops_3[cc] = pop
wm_style = RS('#336699',base_style=LCS) # 一个样式对象,参数指定一个十六进制的RGB颜色
wm = pygal.maps.world.World(style=wm_style) # 创建一个实例,并传入一个指定了颜色的样式对象wm_style
wm.title = 'World Population in 2010,by Country'
wm.add('0-10m', cc_pops_1)
wm.add('10m-1bn', cc_pops_2)
wm.add('>1bn', cc_pops_3)
wm.render_to_file('world_population.svg')