<a href="http://s2.51cto.com/wyfs02/M00/9E/BA/wKioL1mVOnPghWt6AANkyQ2VswM731.jpg-wh_651x-s_1259276027.jpg" target="_blank"></a>
Python 的科學棧相當成熟,各種應用場景都有相關的子產品,包括機器學習和資料分析。資料可視化是發現資料和展示結果的重要一環,隻不過過去以來,相對于 R 這樣的工具,發展還是落後一些。
幸運的是,過去幾年出現了很多新的Python資料可視化庫,彌補了一些這方面的差距。matplotlib 已經成為事實上的資料可視化方面最主要的庫,此外還有很多其他庫,例如vispy,bokeh, seaborn, pyga, folium 和 networkx,這些庫有些是建構在 matplotlib 之上,還有些有其他一些功能。
本文會基于一份真實的資料,使用這些庫來對資料進行可視化。通過這些對比,我們期望了解每個庫所适用的範圍,以及如何更好的利用整個 Python 的資料可視化的生态系統。
我們在 Dataquest 建了一個互動課程,教你如何使用 Python 的資料可視化工具。如果你打算深入學習,可以點這裡。
探索資料集
在我們探讨資料的可視化之前,讓我們先來快速的浏覽一下我們将要處理的資料集。我們将要使用的資料來自 openflights。我們将要使用航線資料集、機場資料集、航空公司資料集。其中,路徑資料的每一行對應的是兩個機場之間的飛行路徑;機場資料的每一行對應的是世界上的某一個機場,并且給出了相關資訊;航空公司的資料的每一行給出的是每一個航空公司。
首先我們先讀取資料:
# Import the pandas library.
import pandas
# Read in the airports data.
airports = pandas.read_csv("airports.csv", header=None, dtype=str)
airports.columns = ["id", "name", "city", "country", "code", "icao", "latitude", "longitude", "altitude", "offset", "dst", "timezone"]
# Read in the airlines data.
airlines = pandas.read_csv("airlines.csv", header=None, dtype=str)
airlines.columns = ["id", "name", "alias", "iata", "icao", "callsign", "country", "active"]
# Read in the routes data.
routes = pandas.read_csv("routes.csv", header=None, dtype=str)
routes.columns = ["airline", "airline_id", "source", "source_id", "dest", "dest_id", "codeshare", "stops", "equipment"]
這些資料沒有列的首選項,是以我們通過指派 column 屬性來添加列的首選項。我們想要将每一列作為字元串進行讀取,因為這樣做可以簡化後續以行 id 為比對,對不同的資料架構進行比較的步驟。我們在讀取資料時設定了 dtype 屬性值達到這一目的。
我們可以快速浏覽一下每一個資料集的資料架構。
airports.head()
<a href="http://s1.51cto.com/wyfs02/M02/00/0A/wKiom1mVOtSQCCr9AAFMoLPpI7U673.png" target="_blank"></a>
airlines.head()
<a href="http://s2.51cto.com/wyfs02/M02/9E/BA/wKioL1mVOu6BWJMgAACmKa5xc5E159.png" target="_blank"></a>
routes.head()
<a href="http://s5.51cto.com/wyfs02/M00/9E/BA/wKioL1mVOwPxOMDYAACakFI29V0228.png" target="_blank"></a>
我們可以分别對每一個單獨的資料集做許多不同有趣的探索,但是隻要将它們結合起來分析才能取得最大的收獲。Pandas 将會幫助我們分析資料,因為它能夠有效的過濾權值或者通過它來應用一些函數。我們将會深入幾個有趣的權值因子,比如分析航空公司和航線。
那麼在此之前我們需要做一些資料清洗的工作。
routes = routes[routes["airline_id"] != "\\N"]
這一行指令就確定了我們在 airline_id 這一列隻含有數值型資料。
制作柱狀圖
現在我們了解了資料的結構,我們可以進一步地開始描點來繼續探索這個問題。首先,我們将要使用 matplotlib 這個工具,matplotlib 是一個相對底層的 Python 棧中的描點庫,是以它比其他的工具庫要多敲一些指令來做出一個好看的曲線。另外一方面,你可以使用 matplotlib 幾乎做出任何的曲線,這是因為它十分的靈活,而靈活的代價就是非常難于使用。
我們首先通過做出一個柱狀圖來顯示不同的航空公司的航線長度分布。一個柱狀圖将所有的航線的長度分割到不同的值域,然後對落入到不同的值域範圍内的航線進行計數。從中我們可以知道哪些航空公司的航線長,哪些航空公司的航線短。
為了達到這一點,我們需要首先計算一下航線的長度,第一步就要使用距離公式,我們将會使用餘弦半正矢距離公式來計算經緯度刻畫的兩個點之間的距離。
import math
def haversine(lon1, lat1, lon2, lat2):
# Convert coordinates to floats.
lon1, lat1, lon2, lat2 = [float(lon1), float(lat1), float(lon2), float(lat2)]
# Convert to radians from degrees.
lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2])
# Compute distance.
dlon = lon2 - lon1
dlat = lat2 - lat1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
km = 6367 * c
return km
然後我們就可以使用一個函數來計算起點機場和終點機場之間的單程距離。我們需要從路線資料架構得到機場資料架構所對應的 source_id 和 dest_id,然後與機場的資料集的 id 列相比對,然後就隻要計算就行了,這個函數是這樣的:
def calc_dist(row):
dist = 0
try:
# Match source and destination to get coordinates.
source = airports[airports["id"] == row["source_id"]].iloc[0]
dest = airports[airports["id"] == row["dest_id"]].iloc[0]
# Use coordinates to compute distance.
dist = haversine(dest["longitude"], dest["latitude"], source["longitude"], source["latitude"])
except (ValueError, IndexError):
pass
return dist
如果 source_id 和 dest_id 列沒有有效值的話,那麼這個函數會報錯。是以我們需要增加 try/catch 子產品對這種無效的情況進行捕捉。
最後,我們将要使用 pandas 來将距離計算的函數運用到 routes 資料架構。這将會使我們得到包含所有的航線線長度的 pandas 序列,其中航線線的長度都是以公裡做機關。
route_lengths = routes.apply(calc_dist, axis=1)
現在我們就有了航線距離的序列了,我們将會建立一個柱狀圖,它将會将資料歸類到對應的範圍之内,然後計數分别有多少的航線落入到不同的每個範圍:
import matplotlib.pyplot as plt
%matplotlib inline
plt.hist(route_lengths, bins=20)
<a href="http://s3.51cto.com/wyfs02/M02/00/0A/wKiom1mVO2_B-YngAABHfcazmQE081.png" target="_blank"></a>
我們用 import matplotlib.pyplot as plt 導入 matplotlib 描點函數。然後我們就使用 %matplotlib inline 來設定 matplotlib 在 ipython 的 notebook 中描點,最終我們就利用 plt.hist(route_lengths, bins=20) 得到了一個柱狀圖。正如我們看到的,航空公司傾向于運作近距離的短程航線,而不是遠距離的遠端航線。
使用 seaborn
我們可以利用 seaborn 來做類似的描點,seaborn 是一個 Python 的進階庫。Seaborn 建立在 matplotlib 的基礎之上,做一些類型的描點,這些工作常常與簡單的統計工作有關。我們可以基于一個核心的機率密度的期望,使用 distplot 函數來描繪一個柱狀圖。一個核心的密度期望是一個曲線 —— 本質上是一個比柱狀圖平滑一點的,更容易看出其中的規律的曲線。
import seaborn
seaborn.distplot(route_lengths, bins=20)
<a href="http://s4.51cto.com/wyfs02/M00/00/0A/wKiom1mVO5qRWfvNAADYcOCzxfE663.png" target="_blank"></a>
正如你所看到的那樣,seaborn 同時有着更加好看的預設風格。seaborn 不含有與每個 matplotlib 的版本相對應的版本,但是它的确是一個很好的快速描點工具,而且相比于 matplotlib 的預設圖表可以更好的幫助我們了解資料背後的含義。如果你想更深入的做一些統計方面的工作的話,seaborn 也不失為一個很好的庫。
條形圖
柱狀圖也雖然很好,但是有時候我們會需要航空公司的平均路線長度。這時候我們可以使用條形圖--每條航線都會有一個單獨的狀态條,顯示航空公司航線的平均長度。從中我們可以看出哪家是國内航空公司哪家是國際航空公司。我們可以使用pandas,一個python的資料分析庫,來酸楚每個航空公司的平均航線長度。
import numpy
# Put relevant columns into a dataframe.
route_length_df = pandas.DataFrame({"length": route_lengths, "id": routes["airline_id"]})
# Compute the mean route length per airline.
airline_route_lengths = route_length_df.groupby("id").aggregate(numpy.mean)
# Sort by length so we can make a better chart.
airline_route_lengths = airline_route_lengths.sort("length", ascending=False)
我們首先用航線長度和航空公司的id來搭建一個新的資料架構。我們基于airline_id把route_length_df拆分成組,為每個航空公司建立一個大體的資料架構。然後我們調用pandas的aggregate函數來擷取航空公司資料架構中長度列的均值,然後把每個擷取到的值重組到一個新的資料模型裡。之後把資料模型進行排序,這樣就使得擁有最多航線的航空公司拍到了前面。
這樣就可以使用matplotlib把結果畫出來。
plt.bar(range(airline_route_lengths.shape[0]), airline_route_lengths["length"])
<a href="http://s3.51cto.com/wyfs02/M00/00/0A/wKiom1mVPFCzs2kgAAB9bXa-P6U894.png" target="_blank"></a>
Matplotlib的plt.bar方法根據每個資料模型的航空公司平均航線長度(airline_route_lengths[“length”])來做圖。
問題是我們想看出哪家航空公司擁有的航線長度是什麼并不容易。為了解決這個問題,我們需要能夠看到坐标軸标簽。這有點難,畢竟有這麼多的航空公司。一個能使問題變得簡單的方法是使圖表具有互動性,這樣能實作放大跟縮小來檢視軸标簽。我們可以使用bokeh庫來實作這個--它能便捷的實作互動性,作出可縮放的圖表。
要使用booked,我們需要先對資料進行預處理:
def lookup_name(row):
# Match the row id to the id in the airlines dataframe so we can get the name.
name = airlines["name"][airlines["id"] == row["id"]].iloc[0]
name = ""
return name
# Add the index (the airline ids) as a column.
airline_route_lengths["id"] = airline_route_lengths.index.copy()
# Find all the airline names.
airline_route_lengths["name"] = airline_route_lengths.apply(lookup_name, axis=1)
# Remove duplicate values in the index.
airline_route_lengths.index = range(airline_route_lengths.shape[0])
上面的代碼會擷取airline_route_lengths中每列的名字,然後添加到name列上,這裡存貯着每個航空公司的名字。我們也添加到id列上以實作查找(apply函數不傳index)。
最後,我們重置索引序列以得到所有的特殊值。沒有這一步,Bokeh 無法正常運作。
現在,我們可以繼續說圖表問題:
import 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 建立背景虛化,在 iPython 的 notebook 裡畫出圖。然後,使用資料幀和特定序列制作條形圖。最後,顯示功能會顯示出該圖。
這個圖實際上不是一個圖像--它是一個 JavaScript 插件。是以,我們在下面展示的是一幅螢幕截圖,而不是真實的表格。
有了它,我們可以放大,看哪一趟航班的飛行路線最長。上面的圖像讓這些表格看起來擠在了一起,但放大以後,看起來就友善多了。
水準條形圖
Pygal 是一個能快速制作出有吸引力表格的資料分析庫。我們可以用它來按長度分解路由。首先把我們的路由分成短、中、長三個距離,并在 route_lengths 裡計算出它們各占的百分比。
long_routes = len([k for k in route_lengths if k > 10000]) / len(route_lengths)
medium_routes = len([k for k in route_lengths if k < 10000 and k > 2000]) / len(route_lengths)
short_routes = len([k for k in route_lengths if k < 2000]) / len(route_lengths)
然後我們可以在 Pygal 的水準條形圖裡把每一個都繪成條形圖:
import pygal
from IPython.display import SVG
chart = pygal.HorizontalBar()
chart.title = 'Long, medium, and short routes'
chart.add('Long', long_routes * 100)
chart.add('Medium', medium_routes * 100)
chart.add('Short', short_routes * 100)
chart.render_to_file('routes.svg')
SVG(filename='routes.svg')
<a href="http://s3.51cto.com/wyfs02/M02/00/0A/wKiom1mVPD3CCVgfAACKroSedfY904.png" target="_blank"></a>
首先,我們建立一個空圖。然後,我們添加元素,包括标題和條形圖。每個條形圖通過百分比值(最大值是100)顯示出該類路由的使用頻率。
最後,我們把圖表渲染成檔案,用 IPython 的 SVG 功能載入并展示檔案。這個圖看上去比預設的 matplotlib 圖好多了。但是為了制作出這個圖,我們要寫的代碼也多很多。是以,Pygal 可能比較适用于制作小型的展示用圖表。
散點圖
在散點圖裡,我們能夠縱向比較資料。我們可以做一個簡單的散點圖來比較航空公司的 id 号和航空公司名稱的長度:
name_lengths = airlines["name"].apply(lambda x: len(str(x)))
plt.scatter(airlines["id"].astype(int), name_lengths)
<a href="http://s3.51cto.com/wyfs02/M02/00/0A/wKiom1mVPH6gwXJkAAHLdwm_IHI279.png" target="_blank"></a>
首先,我們使用 pandasapplymethod 計算每個名稱的長度。它将找到每個航空公司的名字字元的數量。然後,我們使用 matplotlib 做一個散點圖來比較航空 id 的長度。當我們繪制時,我們把 theidcolumn of airlines 轉換為整數類型。如果我們不這樣做是行不通的,因為它需要在 x 軸上的數值。我們可以看到不少的長名字都出現在早先的 id 中。這可能意味着航空公司在成立前往往有較長的名字。
我們可以使用 seaborn 驗證這個直覺。Seaborn 增強版的散點圖,一個聯合的點,它顯示了兩個變量是相關的,并有着類似地分布。
data = pandas.DataFrame({"lengths": name_lengths, "ids": airlines["id"].astype(int)})
seaborn.jointplot(x="ids", y="lengths", data=data)
<a href="http://s3.51cto.com/wyfs02/M01/00/0A/wKiom1mVPJzDSxtEAAHR1O2SDfo484.png" target="_blank"></a>
上面的圖表明,兩個變量之間的相關性是不明确的——r 的平方值是低的。
靜态 maps
我們的資料天然的适合繪圖-機場有經度和緯度對,對于出發和目的機場來說也是。
第一張圖做的是顯示全世界的所有機場。可以用擴充于 matplotlib 的 basemap 來做這個。這允許畫世界地圖和添加點,而且很容易定制。
# Import the basemap package
from mpl_toolkits.basemap import Basemap
# Create a map on which to draw. We're using a mercator projection, and showing the whole world.
m = Basemap(projection='merc',llcrnrlat=-80,urcrnrlat=80,llcrnrlon=-180,urcrnrlon=180,lat_ts=20,resolution='c')
# Draw coastlines, and the edges of the map.
m.drawcoastlines()
m.drawmapboundary()
# Convert latitude and longitude to x and y coordinates
x, y = m(list(airports["longitude"].astype(float)), list(airports["latitude"].astype(float)))
# Use matplotlib to draw the points onto the map.
m.scatter(x,y,1,marker='o',color='red')
# Show the plot.
plt.show()
在上面的代碼中,首先用 mercator projection 畫一個世界地圖。墨卡托投影是将整個世界的繪圖投射到二位曲面。然後,在地圖上用紅點點畫機場。
上面地圖的問題是找到每個機場在哪是困難的-他們就是在機場密度高的區域合并城一團紅色斑點。
就像聚焦不清楚,有個互動制圖的庫,folium,可以進行放大地圖來幫助我們找到個别的機場。
import folium
# Get a basic world map.
airports_map = folium.Map(location=[30, 0], zoom_start=2)
# Draw markers on the map.
for name, row in airports.iterrows():
# For some reason, this one airport causes issues with the map.
if row["name"] != "South Pole Station":
airports_map.circle_marker(location=[row["latitude"], row["longitude"]], popup=row["name"])
# Create and show the map.
airports_map.create_map('airports.html')
airports_map
Folium 使用 leaflet.js 來制作全互動式地圖。你可以點選每一個機場在彈出框中看名字。在上邊顯示一個截屏,但是實際的地圖更令人印象深刻。Folium 也允許非常廣闊的修改選項來做更好的标注,或者添加更多的東西到地圖上。
畫弧線
在地圖上看到所有的航空路線是很酷的,幸運的是,我們可以使用 basemap 來做這件事。我們将畫弧線連接配接所有的機場出發地和目的地。每個弧線想展示一個段都航線的路徑。不幸的是,展示所有的線路又有太多的路由,這将會是一團糟。替代,我們隻現實前 3000 個路由。
# Make a base map with a mercator projection. Draw the coastlines.
# Iterate through the first 3000 rows.
for name, row in routes[:3000].iterrows():
# Get the source and dest airports.
# Don't draw overly long routes.
if abs(float(source["longitude"]) - float(dest["longitude"])) < 90:
# Draw a great circle between source and dest airports.
m.drawgreatcircle(float(source["longitude"]), float(source["latitude"]), float(dest["longitude"]), float(dest["latitude"]),linewidth=1,color='b')
# Show the map.
<a href="http://s4.51cto.com/wyfs02/M02/9E/BA/wKioL1mVPP6jNslFAAOJKY4POD8060.png" target="_blank"></a>
上面的代碼将會畫一個地圖,然後再在地圖上畫線路。我們添加一了寫過濾器來阻止過長的幹擾其他路由的長路由。
畫網絡圖
我們将做的最終的探索是畫一個機場網絡圖。每個機場将會是網絡中的一個節點,并且如果兩點之間有路由将劃出節點之間的連線。如果有多重路由,将添加線的權重,以顯示機場連接配接的更多。将使用 networkx 庫來做這個功能。
首先,計算機場之間連線的權重。
# Initialize the weights dictionary.
weights = {}
# Keep track of keys that have been added once -- we only want edges with a weight of more than 1 to keep our network size manageable.
added_keys = []
# Iterate through each route.
for name, row in routes.iterrows():
# Extract the source and dest airport ids.
source = row["source_id"]
dest = row["dest_id"]
# Create a key for the weights dictionary.
# This corresponds to one edge, and has the start and end of the route.
key = "{0}_{1}".format(source, dest)
# If the key is already in weights, increment the weight.
if key in weights:
weights[key] += 1
# If the key is in added keys, initialize the key in the weights dictionary, with a weight of 2.
elif key in added_keys:
weights[key] = 2
# If the key isn't in added_keys yet, append it.
# This ensures that we aren't adding edges with a weight of 1.
else:
added_keys.append(key)
一旦上面的代碼運作,這個權重字典就包含了每兩個機場之間權重大于或等于 2 的連線。是以任何機場有兩個或者更多連接配接的路由将會顯示出來。
# Import networkx and initialize the graph.
import networkx as nx
graph = nx.Graph()
# Keep track of added nodes in this set so we don't add twice.
nodes = set()
# Iterate through each edge.
for k, weight in weights.items():
# Split the source and dest ids and convert to integers.
source, dest = k.split("_")
source, dest = [int(source), int(dest)]
# Add the source if it isn't in the nodes.
if source not in nodes:
graph.add_node(source)
# Add the dest if it isn't in the nodes.
if dest not in nodes:
graph.add_node(dest)
# Add both source and dest to the nodes set.
# Sets don't allow duplicates.
nodes.add(source)
nodes.add(dest)
# Add the edge to the graph.
graph.add_edge(source, dest, weight=weight)
pos=nx.spring_layout(graph)
# Draw the nodes and edges.
nx.draw_networkx_nodes(graph,pos, node_color='red', node_size=10, alpha=0.8)
nx.draw_networkx_edges(graph,pos,width=1.0,alpha=1)
plt.show()
總結
有一個成長的資料可視化的 Python 庫,它可能會制作任意一種可視化。大多數庫基于 matplotlib 建構的并且確定一些用例更簡單。如果你想更深入的學習怎樣使用 matplotlib,seaborn 和其他工具來可視化資料,在這兒檢出其他課程。
本文作者:Vik Paruchuri
來源:51CTO