来自《Python编程:从入门到实践》的第二篇,对数据可视化方面的初步学习,主要包括绘制随机漫步图,绘制某地区最高温和最低温分布图,以及世界人口地图三个部分。

随机漫步

随机漫步 是这样行走得到的路径:每次行走都完全是随机的,没有明确的方向,结果是由一系列随机决策决定的。

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from random import choice
import matplotlib.pyplot as plt


class RandomWalk():
"""生成一个随机漫步的类"""
def __init__(self, num_points=5000):
self.num_points = num_points
# 所有随机漫步都从[0,0]开始
self.x_values = [0]
self.y_values = [0]

def fill_work(self):
"""生成漫步包含的点,并决定每次漫步的方向"""
while len(self.x_values) < self.num_points:
# 决定在沿着x轴移动的方向以及移动的长度
x_direction = choice([1, -1])
x_distance = choice([0, 1, 2, 3, 4])
x_step = x_direction * x_distance

# 决定在沿着y轴移动的方向以及移动的长度
y_direction = choice([1, -1])
y_distance = choice([0, 1, 2, 3, 4])
y_step = y_direction * y_distance

# 拒绝原地踏步
if x_step == 0 and y_step == 0:
continue
# 获取下一个x值和y值
next_x = self.x_values[-1] + x_step
next_y = self.y_values[-1] + y_step

self.x_values.append(next_x)
self.y_values.append(next_y)


rw = RandomWalk(50000)
rw.fill_work()

# 给点着色
point_numbers = list(range(rw.num_points))
plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues,
edgecolors='none', s=1)
# 突出起点和终点
plt.scatter(0, 0, c='green', edgecolors='none', s=100)
plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red',
edgecolors='none', s=100)

# 设置绘图窗口的尺寸
# plt.figure(figsize=(10, 6))

# 隐藏坐标轴: 会出错
# plt.axes().get_xaxis().set_visible(False)
# plt.axes().get_yaxis().set_visible(False)

# 隐藏坐标轴正确方法
plt.axis('off')

plt.title("随机散点图")
plt.show()

实现效果图:

<img src=”https://huanyouchen-1252081928.cos.ap-shanghai.myqcloud.com/2018-04-28-sui-ji-man-bu.png?imageView2/0/q/75|watermark/2/text/aHVhbnlvdWNoZW4uZ2l0aHViLmlv/font/5qW35L2T/fontsize/320/fill/IzBBMEEwQQ==/dissolve/93/gravity/SouthEast/dx/10/dy/10|imageslim", width=”60%” height=”60%”>

在着色部分,使用了range() 生成了一个数字列表,其中包含的数字个数与漫步包含的点数相同。接下来,将这个列表存储在 point_numbers 中,以便后面使用它来设置每个漫步点的颜色。然后将参数c 设置为 point_numbers ,指定使用颜色映射 Blues ,并传递实参 edgecolor=none 以删除每个点周围的轮廓。最终的随机漫步图从浅蓝色渐变为深蓝色,

某地区最高温和最低温

数据来源:https://nostarch.com/pythoncrashcourse/ 将该书配套资源下载下来后,在第16章文件夹中找到文件 death_valley_2014.csv即可使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import csv
from datetime import datetime
from matplotlib import pyplot as plt

# 从文件中读取每天的最高温和最低温数据
filename = 'death_valley_2014.csv'
with open(filename) as f:
# 创建一个与该文件相关联的阅读器( reader )对象
reader = csv.reader(f)
# 模块 csv 包含函数 next() ,调用它并将阅读器对象传递给它时,它将返回文件中的下一行
header_row = next(reader)
dates, highs, lows = [], [], []
for row in reader:
try:
# 使用的很多数据集都可能缺失数据、数据格式不正确或数据本身不正确
current_date = datetime.strptime(row[0], "%Y-%m-%d")
high = int(row[1])
low = int(row[3])
except ValueError:
print(current_date, 'misssing data')
else:
dates.append(current_date)
highs.append(high)
lows.append(low)


# 根据数据绘制图形
# 形参 figsize 指定一个元组,向 matplotlib 指出绘图窗口的尺寸,单位为英寸
# 形参 dpi 向 figure() 传递该分辨率,以有效地利用可用的屏幕空间
fig = plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red', alpha=0.5)
plt.plot(dates, lows, c='blue', alpha=0.5)
# 向 fill_between() 传递了一个 x 值系列:列表 dates ,还传递了两个 y 值系列: highs 和 lows 。
# 实参 facecolor 指定了填充区域的颜色
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.2)

# 设置图形的格式
plt.title("2014年每日最高温和最低温", fontsize=18)
plt.xlabel('', fontsize=14)
# fig.autofmt_xdate() 来绘制斜的日期标签,以免它们彼此重叠
fig.autofmt_xdate()
plt.ylabel("温度", fontsize=14)
# 函数 tick_params() 设置刻度的样式,指定的实参将影响 x 轴和 y 轴上的刻度
# 参数which的值为 'major'、'minor'、'both',分别代表设置主刻度线、副刻度线以及同时设置
plt.tick_params(axis='both', which='major', labelsize=12)
plt.show()

实现效果图:

绘制世界人口地图

数据来源:https://huanyouchen-1252081928.cos.ap-shanghai.myqcloud.com/population_data.json

该json文件的内容是一个列表,里面每个元素都是一个包含四个键的字典:国家名、国别码、年份以及表示人口数量的值:

1
2
3
4
5
6
7
8
9
10
[
{
"Country Name": "Arab World",
"Country Code": "ARB",
"Year": "1960",
"Value": "96388069"
},
...
...
]

Pygal 中的地图制作工具要求数据为特定的格式:用国别码表示国家,以及用数字表示人口数量。处理地理政治数据时,经常需要用到几个标准化国别码集。 population_data.json 中包含的是三个字母的国别码,但 Pygal 使用两个字母的国别码。我们需要想办法根据国家名获取两个字母的国别码。

原书中的这段:

Pygal 使用的国别码存储在模块 i18n ( internationalization 的缩写)中。字典 COUNTRIES 包含的键和值分别为两个字母的国别码和国家名。要查看这些国别码,可从模块 i18n 中导入这个字典,并打印其键和值

现在已经改变了,需要从pygal.maps.world导入COUNTRIES才能正确使用:

1
2
3
4
5
6
7
8
9
10
11
12
# from pygal.il8n import COUNTRIES
# 原书中导入报错ImportError: No module named 'pygal.il8n'
# 正确方法:
from pygal.maps.world import COUNTRIES


def get_country_code(country_names):
"""根据指定的国家名,返回两个字母的国别码"""
for code, name in COUNTRIES.items():
if name == country_names:
return code
return None

countries模块在 COUNTRIES 中查找并返回两个字母的国别码以便给Pygal使用。

然后编写world_population模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import json
from pygal_maps_world.maps import World
from countries import get_country_code

filename = 'population_data.json'
with open(filename) as f:
pop_data = json.load(f)

world_population_dict = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country_name = pop_dict['Country Name']
code = get_country_code(country_name)
# Python 不能直接将包含小数点的字符串转换为整数
# 函数 float() 将字符串转换为小数,而函数 int() 丢弃小数部分,返回一个整数
population = int(float(pop_dict['Value']))
if code:
world_population_dict[code] = population
#else:
# 导致显示错误消息的原因有两个。首先,并非所有人口数量对应的都是国家,
# 有些人口数量对应的是地区(阿拉伯世界)和经济类群(所有收入水平)。
# 其次,有些统计数据使用了不同的完整国家名(如 Yemen, Rep. ,而不是 Yemen )。
# 当前,我们将忽略导致错误的数据
# print('ERROR -' + country_name)

# 根据人口数量将所有国家分为三个组
# 三组 —— 少于 1000 万的、介于 1000 万和 1亿之间的以及超过1亿的
world_pops_1, world_pops_2, world_pops_3 = {}, {}, {}
for country, pops in world_population_dict.items():
if pops < 10000000:
world_pops_1[country] = pops
elif pops < 100000000:
world_pops_2[country] = pops
else:
world_pops_3[country] = pops

world_map = World()
world_map.title = "World Population in 2010, by country"
world_map.add('0-1千万', world_pops_1)
world_map.add('1千万-1亿', world_pops_2)
world_map.add('1亿以上', world_pops_3)
world_map.render_to_file('world_population.svg')

这里面有一个小知识点,在第16行,pop_dict['Value']返回的可能是一个带有小数点的字符串。对于一个带有小数点的字符串,比如”234.176”,将其转换成整数234,直接用int("234.176")是会报错:ValueError: invalid literal for int(),因为Python不能直接将包含小数点的字符串转换为整数,所以用float() 将字符串转换为小数,再用 int() 丢弃小数部分,返回一个整数。

实现效果: