原文:Comparing 7 Python data visualization tools
Python的科学栈相当成熟。目前已经有许多用于各种各样目的的库,包括机器学习和数据分析。数据可视化是能够探索数据和交流结果的重要组成部分,但是在过去,稍微落后于其他工具,例如,R。
幸运的是,在过去的几年里,许多新的Python数据可视化库被创造出来以缩小差距。matplotlib已经成为主要的数据可视化库,但是,也有其他诸如vispy, bokeh, seaborn, pygal, folium和networkx的库,它们要么建立在matplotlib的基础上,要么具备matplotlib所不支持的功能。
在这篇文章中,我们将使用一个真实世界的数据集,然后使用这些库进行可视化。在这个过程中,我们会发现什么领域使用什么库最好,以及如何最有效的利用Python的数据可视化生态系统。
在Dataquest中,我们已经建立了一个互动课程。这个课程会教你Python数据可视化工具。如果你想更深入的了解,看看这里。
探索数据集
在我们深入数据可视化之前,让我们快速浏览一下将使用的数据集。我们将使用来自openflights的数据。我们还会使用(航线)[http://openflights.org/data.html#route], 机场和航空公司的数据。航线数据中的每一行对应于两个机场之间的一条航空公司航线。机场数据中的每一行对应世界上的一个机场及其相关信息。航空公司数据的每一行表示一个航空公司。
注:openflights这个网站可能打不开。此时,可以使用github上的数据:openflights
我们首先读入数据: 1
2
3
4
5
6
7
8
9
10
11# 导入pandas库
import pandas
# 读入机场数据
airports = pandas.read_csv("airports.csv", header=None, dtype=str)
airports.columns = ["id", "name", "city", "country", "code", "icao", "latitude", "longitude", "altitude", "offset", "dst", "timezone"]
# 读入航空公司数据
airlines = pandas.read_csv("airlines.csv", header=None, dtype=str)
airlines.columns = ["id", "name", "alias", "iata", "icao", "callsign", "country", "active"]
# 读入航线数据
routes = pandas.read_csv("routes.csv", header=None, dtype=str)
routes.columns = ["airline", "airline_id", "source", "source_id", "dest", "dest_id", "codeshare", "stops", "equipment"]columns
属性添加列标题。我们想将每一列作为字符串读取 - 这会使得后面当我们想基于id匹配每一行时,比较整个数据帧更加容易,因此,我们在读取数据时设置dtype
参数。
我们可以快速浏览一下每个数据帧: 1
airports.head()
1 | airlines.head() |
id | name | alias | iata | icao | callsign | country | active | |
---|---|---|---|---|---|---|---|---|
0 | 1 | Private flight | - | NaN | NaN | NaN | Y | |
1 | 2 | 135 Airways | NaN | GNL | GENERAL | United States | N | |
2 | 3 | 1Time Airline | 1T | RNX | NEXTIME | South Africa | Y | |
3 | 4 | 2 Sqn No 1 Elementary Flying Training School | NaN | WYT | NaN | United Kingdom | N | |
4 | 5 | 213 Flight Unit | NaN | TFU | NaN | Russia | N |
1 | routes.head() |
airline | airline_id | source | source_id | dest | dest_id | codeshare | stops | equipment | |
---|---|---|---|---|---|---|---|---|---|
0 | 2B | 410 | AER | 2965 | KZN | 2990 | NaN | 0 | CR2 |
1 | 2B | 410 | ASF | 2966 | KZN | 2990 | NaN | 0 | CR2 |
2 | 2B | 410 | ASF | 2966 | MRV | 2962 | NaN | 0 | CR2 |
3 | 2B | 410 | CEK | 2968 | KZN | 2990 | NaN | 0 | CR2 |
4 | 2B | 410 | CEK | 2968 | OVB | 4078 | NaN | 0 | CR2 |
我们可以对每一个数据集单独的做各种有趣的探索,但是通过将它们结合起来,我们将会看到最大的收获。在进行分析的时候,Pandas会帮助我们,因为它能够很容易的过滤矩阵或在它们上面应用函数。我们将深入一些有趣的度量,例如分析航空公司和路线。
在此之前,需要进行一些数据清理: 1
routes = routes[routes["airline_id"] != "\\N"]
airline_id
列只有数字数据。
建立直方图
现在,我们已经了解了数据的结构,可以继续前进,开始画图来探索它们。对于第一个图,我们将使用matplotlib。matplotlib是Python栈中一个相对较低级别的绘图库,因此它比其他库需要更多的命令来绘制一个漂亮的图。另一方面,你可以使用matplotlib来绘制几乎任何种类的图。它非常灵活,但这种灵活性是以冗余为代价的。
我们首先绘制直方图来显示按航空公司划分的航线长度分布情况。直方图把所有的航线长度划分成范围(或"仓库"),然后统计每个范围中有多少路线。这可以告诉我们,航空公司飞行更短的航线,还是更长的航线。
为了做到这一点,我们需要首先计算航线的长度。第一步是一个距离公式。我们将使用Haversine距离,即经纬度对之间的距离。
1 | import math |
然后,我们可以编写一个函数来计算源机场和目的机场之间某条航线的距离。要做到这一点,我们需要从航线数据帧中获得机场的source_id
和dest_id
,然后根据机场数据帧中的id列将它们匹配起来以获得这些机场的经度和纬度。接下来,就只是单纯的计算了。下面是这个函数:
1 | def calc_dist(row): |
如果source_id
或者dest_id
列出现无效值时,这个函数将运行失败,所以我们要增加try/except
块来捕捉。
最后,我们要使用pandas对整个routes
数据帧应用距离计算函数。它将为我们提供一个包含所有航线长度的pandas series。所有航线的长度都是以千米为单位。 1
route_lengths = routes.apply(calc_dist, axis=1)
1
2
3
4import matplotlib.pyplot as plt
%matplotlib inline
plt.hist(route_lengths, bins=20)
使用import matplotlib.pyplot as plt
导入matplotlib绘图函数。然后使用%matplotlib inline
在ipython notebook中启动matplotlib显示图形。最后,使用plt.hist(route_lengths, bins=20)
创建一个直方图。正如我们所看到的,相对于长航线,航空公司飞行更多的短航线。
使用Seaborn
我们可以使用seaborn(一个更高级的Python绘图库)绘制出相似的图。Seaborn基于matplotlib,创建某些类型的图,通常用于统计工作会更简单。我们可以使用distplot
函数来绘制一个直方图,这个图上会带有一个内核密度评估。一个内核密度评估是一个曲线 - 实质上是直方图的一个平滑版本,它更易于看到模式。
1 | import seaborn |
正如你所看到的,seaborn拥有比matplotlib更好的默认样式。Seaborn并没有所有matplotlib图形的它自己的版本,但是相比较默认的matplotlib图表,它是一种快速获得更深入好看的图形的很好的方式。如果你需要更深入,并且进行一些统计工作,它也是一个很好的库。
柱状图
直方图很棒,但也许我们想要按航空公司查看平均航线长度。我们可以改用柱状图 - 每一家航空公司都有一个单独的柱表示平均航线长度。这将让我们看到哪些运营商是区域性的,哪些是国际化的。我们可以使用pandas,一个python数据分析库,来计算每一个航空公司的平均航线长度。
1 | import numpy |
首先,我们使用航线长度和航空公司的id来创建一个新的数据帧。接着基于airline_id
将route_length_df
分组,从根本上使得每一家航空公司创建一个数据帧。然后使用pandas的aggregate
函数计算每一个航空公司数据帧的length
列的平均值,再将每一个结果结合成一个新的数据帧。然后,我们对数据帧进行排序以使得最长航线长度的航空公司出现在第一位。
现在,我们可以使用matplotlib画图了: 1
plt.bar(range(airline_route_lengths.shape[0]), airline_route_lengths["length"])
matplotlib的plt.bar
方法绘制每一个航空公司飞行的平均航线长度(airline_route_lengths["length"]
).
上面的图形的问题是,我们不能很容易的看出每一个航线长度对应哪一个航空公司。为了达到这个目的,我们需要能够看到轴标签。这有点困难,因为有这么多家航空公司。使这更容易的一个方法是将图形变成交互式的,这样将允许我们放大和缩小以查看标签。我们可以使用bokeh库来达到此目的 - 它可以很简单的创建可互动可缩放的图形。
要使用bokeh,我们需要先预处理我们的数据:
1 | def lookup_name(row): |
上面的代码将为airline_route_lengths
中的每一行获取名称,然后将其保存到name
列(包含每一家航空公司的名字)。我们也增加id
列,这样我们能进行这种查找(apply
函数不用传递索引)。
最后,我们重置索引列来获得所有唯一值。若非如此,Bokeh无法正常工作。
现在,我们可以继续画图了: 1
2
3
4
5
6
7import numpy as np
from bokeh.io import output_notebook
from bokeh.charts import Bar, show
output_notebook()
p = Bar(airline_route_lengths, 'name', values='length', title='Average airline route lengths')
show(p)output_notebook
设置bokeh可以在ipython notebook中显示图表。然后,使用我们的数据帧和对应的列创建一个条形图。最后,show
函数显示这个图表。
这个图表并不是一张图 - 它其实是一个javascript小部件。正因为如此,我们在下面展示的是一张截图,而不是实际的图表。
使用它,我们可以放大然后查看哪一个航空公司飞行最长的距离。上面的图使得这些标签看起来很拥挤,但是随着被放大,它们会容易看得多。
水平条形图
Pygal是一个Python数据分析库,它可以快速的制作出吸引人的图表。我们可以使用它来根据长度对航线进行分类。首先,将航线分为短期,中期和长期,然后计算它们每一个在route_lengths
的百分比。
1 | long_routes = len([k for k in route_lengths if k > 10000]) / len(route_lengths) |
然后,在pygal水平条形图中,将它们每一个绘制成一个条形:
1 | import pygal |
在上面,我们首先创造了一个空图表。然后,添加元素,包括标题和条形。每一个条形对应一个百分比,表示航线类型有多常见。
最后,绘制图表文件,并使用IPython中的SVG显示功能加载和显示文件。此图看起来比默认的matplotlib图表漂亮许多,但是我们也需要写更多的代码来创建它。Pygal可能比较适用于小型演示图形。
散点图
散点图能让我们比较数据列。我们可以制作一个简单的散点图来比较航空公司id值及其名字的长度: 1
2name_lengths = airlines['name'].apply(lambda x: len(str(x)))
plt.scatter(airlines['id'].astype(int), name_lengths)
首先,通通过pandas的apply
方法计算每一个名字的长度。这将查找每一个航空公司名字的字母数。
然后,使用matplotlib制作一张散点图来对航空公司的id及其名字的长度进行比较。在绘图的时候,将airlines
的id
列转换成整型。如果不这样做,将无法进行绘图,因为在x轴,它需要数字值。可以看到,越早的id拥有越长的名字。这可能意味着,较早成立的航空公司往往有较长的名称。
我们可以使用seaborn来验证这个说法。seaborn有一个散点图的增强版本,一个联合图表,它显示了两个变量之间的关系,以及每一个的单独分布。
1 | data = pandas.DataFrame({"lengths": name_lengths, "ids": airlines["id"].astype(int)}) |
上面的图表表明,两个变量之间不存在任何真实的相关性 - R平方值低。
静态地图
我们的数据就本质而言是相当适合绘制地图的 - 机场、源机场和目的地机场都具有经纬度对。
我们可以制作的第一张图是一张显示遍布全世界的所有机场。我们可以使用basemap这基于matplotlib扩展的工具。它可以绘制世界地图,添加点,而且非常个性化。
1 | # 导入basemap包 |
在上面的代码中,我们首先使用墨卡托投影绘制世界地图。墨卡托投影是一种将整个世界投影成二维表面的方法。然后,使用红点在地图上绘制机场。
上面地图的问题是很难看出来每一个机场在哪里 - 它们在高机场密度区域会合并成一个红色的斑点。
正如使用bokeh一样,也有一个交互式的地图库,叫做folium,可以用来放大地图以帮助我们查找单个机场。
1 | import folium |
folium使用leaflet.js来制作一个完全可交互的地图。你可以点击每一个机场来查看弹出的名字。上面是截图,但是真实的地图会更加的令人印象深刻。folium也允许你修改范围非常广泛的选项来制作更好的标记,或者添加更多的东西到地图中。
绘制大圆圈(great circle)
看到所有航线显示在一张地图上将是一件相当酷的事情。幸运的是,我们可以使用basemap来做到这一点。我们将绘制链接源和目的地机场的大圆。每个圆圈将显示一个单一的客机路线。不幸的是,有那么多的路线以至于将它们全部显示出来会乱七八糟。相反,我们将显示前3000条路线。
1 | # 使用墨卡托投影制作一张基础地图。绘制海岸线。 |
上面的代码将会绘制一张地图,然后在上面绘制航线。我们增加了一些过滤以防止过长的航线模糊了其他航线。
绘制网络图
我们将进行的最后的探索是绘制一张机场的网络图。每一个机场将会是网络中的一个结点,而如果机场之间存在一条航线,我们将绘制结点之间的边。如果有多条航线,将增加边的权重,以表明机场的连接成都。我们将使用networkx库来做到这点。
首先,我们需要计算机场之间的边的权重。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23# 初始化权重字典
weights = {}
# 跟踪已经添加过一次的键 -- 我们只想要超过1权重的边来使得我们的网络规模可控。
added_keys = []
# 遍历每一条航线
for name, row in routes.iterrows():
# 抽取源和目的地id
source = row["source_id"]
dest = row["dest_id"]
# 为权重字典创建一个键
# 这对应一条边,拥有航线的起点和终点。
key = "{0}_{1}".format(source, dest)
# 如果键已经在wieghts中了,增加权重。
if key in weights:
weights[key] += 1
# 如果key在已添加键中,用权重值2初始化weights字典中的键
elif key in added_keys:
weights[key] = 2
# 如果键不在added_keys中,添加它。
# 这确保我们并未添加权重值为1的边
else:
added_keys.append(key)
现在,我们需要绘图。
1 | # 导入networkx,初始化图形 |
总结
已经产生了大量用于数据可视化的Python库,这使得制作几乎任何类型的可视化成为可能。大多数的库是基于matplotlib,并让某些用例更加简单。如果你想要更深入了解如何使用matplotlib, seaborn和其他工具进行数据可视化,看看我们的互动课程。