第 16 章 下載數據
在本章中,你將從網上下載資料,並對這些資料進行視覺化。網上的資料多得難以置信,且大多未經過仔細檢查。如果能夠對這些資料進行分析,你就能發現別人沒有發現的規律和關聯。
我們將訪問並視覺化以兩種常見格式存儲的資料:CSV 和JSON。我們將使用Python模組csv 來處理以CSV(逗號分隔的值)格式存儲的天氣資料,找出兩個不同地區在一段時間內的最高溫度和最低溫度。然後,我們將使用matplotlib根據下載的資料創建一個圖表,展示兩個不同地區的氣溫變化:阿拉斯加錫特卡和加利福尼亞死亡穀。在本章的後面,我們將使用模組json 來訪問以JSON格式存儲的人口資料,並使用Pygal繪製一幅按國別劃分的人口地圖。
閱讀本章後,你將能夠處理各種類型和格式的資料集,並對如何創建複雜的圖表有更深入的認識。要處理各種真實世界的資料集,必須能夠訪問並視覺化各種類型和格式的線上資料。
16.1 CSV檔案格式
要在文字檔中存儲資料,最簡單的方式是將資料作為一系列以逗號分隔的值 (CSV)寫入文件。這樣的檔稱為CSV檔。例如,下面是一行CSV格式的天氣資料:
2014-1-5,61,44,26,18,7,-1,56,30,9,30.34,30.27,30.15,,,,10,4,,0.00,0,,195
這是阿拉斯加錫特卡2014年1月5日的天氣資料,其中包含當天的最高氣溫和最低氣溫,還有眾多其他資料。CSV檔對人來說閱讀起來比較麻煩,但程式可輕鬆地提取並處理其中的值,這有助於加快資料分析過程。
我們將首先處理少量錫特卡的CSV格式的天氣資料,這些資料可在本書的配套資源(https://www.nostarch.com/pythoncrashcourse/ )中找到。請將文件sitka_weather_07-2014.csv複製到存儲本章程式的資料夾中(下載本書的配套資源後,你就有了這個項目所需的所有檔)。
注意 這個專案使用的天氣資料是從http://www.wunderground.com/history/ 下載而來的。
16.1.1 分析CSV檔頭
csv 模組包含在Python標準庫中,可用於分析CSV檔中的資料行,讓我們能夠快速提取感興趣的值。下面先來查看這個文件的第一行,其中包含一系列有關資料的描述:
highs_lows.py
import csv
filename = 'sitka_weather_07-2014.csv'
❶ with open(filename) as f:
❷ reader = csv.reader(f)
❸ header_row = next(reader)
print(header_row)
導入模組csv 後,我們將要使用的檔的名稱存儲在filename 中。接下來,我們打開這個檔,並將結果檔物件存儲在f 中(見❶)。然後,我們調用csv.reader() ,並將前面存儲的檔物件作為實參傳遞給它,從而創建一個與該檔相關聯的閱讀器(reader )物件(見❷)。我們將這個閱讀器物件存儲在reader 中。
模組csv 包含函數next() ,調用它並將閱讀器物件傳遞給它時,它將返回檔中的下一行。在前面的代碼中,我們只調用了next() 一次,因此得到的是檔的第一行,其中包含檔頭(見❸)。我們將返回的資料存儲在header_row 中。正如你看到的,header_row 包含與天氣相關的檔頭,指出了每行都包含哪些資料:
['AKDT', 'Max TemperatureF', 'Mean TemperatureF', 'Min TemperatureF',
'Max Dew PointF', 'MeanDew PointF', 'Min DewpointF', 'Max Humidity',
' Mean Humidity', ' Min Humidity', ' Max Sea Level PressureIn',
' Mean Sea Level PressureIn', ' Min Sea Level PressureIn',
' Max VisibilityMiles', ' Mean VisibilityMiles', ' Min VisibilityMiles',
' Max Wind SpeedMPH', ' Mean Wind SpeedMPH', ' Max Gust SpeedMPH',
'PrecipitationIn', ' CloudCover', ' Events', ' WindDirDegrees']
reader處理檔中以逗號分隔的第一行資料,並將每項資料都作為一個元素存儲在清單中。檔頭AKDT 表示阿拉斯加時間(Alaska Daylight Time),其位置表明每行的第一個值都是日期或時間。檔頭Max TemperatureF 指出每行的第二個值都是當天的最高華氏溫度。可通過閱讀其他的檔頭來確定檔包含的資訊類型。
注意 檔頭的格式並非總是一致的,空格和單位可能出現在奇怪的地方。這在原始資料檔案中很常見,但不會帶來任何問題。
16.1.2 列印檔案頭及其位置
為讓檔頭資料更容易理解,將列表中的每個檔頭及其位置列印出來:
highs_lows.py
--snip--
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
❶ for index, column_header in enumerate(header_row):
print(index, column_header)
我們對列表調用了enumerate() (見❶)來獲取每個元素的索引及其值。(請注意,我們刪除了代碼行print(header_row) ,轉而顯示這個更詳細的版本。)
輸出如下,其中指出了每個檔頭的索引:
0 AKDT
1 Max TemperatureF
2 Mean TemperatureF
3 Min TemperatureF
--snip--
20 CloudCover
21 Events
22 WindDirDegrees
從中可知,日期和最高氣溫分別存儲在第0列和第1列。為研究這些資料,我們將處理sitka_weather_07-2014.csv中的每行資料,並提取其中索引為0和1的值。
16.1.3 提取並讀取資料
知道需要哪些列中的資料後,我們來讀取一些資料。首先讀取每天的最高氣溫:
highs_lows.py
import csv
# 從文件中獲取最高氣溫
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
❶ highs = []
❷ for row in reader:
❸ highs.append(row[1])
print(highs)
我們創建了一個名為highs 的空列表(見❶),再遍歷文件中餘下的各行(見❷)。閱讀器物件從其停留的地方繼續往下讀取CSV檔,每次都自動返回當前所處位置的下一行。由於我們已經讀取了檔頭行,這個迴圈將從第二行開始——從這行開始包含的是實際資料。每次執行該迴圈時,我們都將索引1處(第2列)的資料附加到highs 末尾(見❸)。
下面顯示了highs 現在存儲的資料:
['64', '71', '64', '59', '69', '62', '61', '55', '57', '61', '57', '59', '57',
'61', '64', '61', '59', '63', '60', '57', '69', '63', '62', '59', '57', '57',
'61', '59', '61', '61', '66']
我們提取了每天的最高氣溫,並將它們作為字串整潔地存儲在一個清單中。
下面使用int() 將這些字串轉換為數位,讓matplotlib能夠讀取它們:
highs_lows.py
--snip--
highs = []
for row in reader:
❶ high = int(row[1])
highs.append(high)
print(highs)
在❶處,我們將表示氣溫的字串轉換成了數位,再將其附加到列表末尾。這樣,最終的清單將包含以數字表示的每日最高氣溫:
[64, 71, 64, 59, 69, 62, 61, 55, 57, 61, 57, 59, 57, 61, 64, 61, 59, 63, 60, 57,
69, 63, 62, 59, 57, 57, 61, 59, 61, 61, 66]
下面來對這些資料進行視覺化。
16.1.4 繪製氣溫圖表
為視覺化這些氣溫資料,我們首先使用matplotlib創建一個顯示每日最高氣溫的簡單圖形,如下所示:
highs_lows.py
import csv
from matplotlib import pyplot as plt
# 從文件中獲取最高氣溫
--snip--
# 根據資料繪製圖形
fig = plt.figure(dpi=128, figsize=(10, 6))
❶ plt.plot(highs, c='red')
# 設置圖形的格式
❷ plt.title("Daily high temperatures, July 2014", fontsize=24)
❸ plt.xlabel('', fontsize=16)
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)
plt.show()
我們將最高氣溫列表傳給plot() (見❶),並傳遞c='red' 以便將資料點繪製為紅色(紅色顯示最高氣溫,藍色顯示最低氣溫)。接下來,我們設置了一些其他的格式,如字體大小和標籤(見❷),這些都在第15章介紹過。鑒於我們還沒有添加日期,因此沒有給x 軸添加標籤,但plt.xlabel() 確實修改了字體大小,讓預設標籤更容易看清。圖16-1顯示了繪製的圖表:一個簡單的折線圖,顯示了阿拉斯加錫特卡2014年7月每天的最高氣溫。
16.1.5 模組datetime
下麵在圖表中添加日期,使其更有用。在天氣資料檔案中,第一個日期在第二行:
2014-7-1,64,56,50,53,51,48,96,83,58,30.19,--snip--
讀取該資料時,獲得的是一個字串,因為我們需要想辦法將字串'2014-7-1' 轉換為一個表示相應日期的物件。為創建一個表示2014年7月1日的物件,可使用模組datetime 中的方法strptime() 。我們在終端會話中看看strptime() 的工作原理:
>>> from datetime import datetime
>>> first_date = datetime.strptime('2014-7-1', '%Y-%m-%d')
>>> print(first_date)
2014-07-01 00:00:00
我們首先導入了模組datetime 中的datetime 類,然後調用方法strptime() ,並將包含所需日期的字串作為第一個實參。第二個實參告訴Python如何設置日期的格式。在這個示例中,'%Y-' 讓Python將字串中第一個連字號前面的部分視為四位元的年份;'%m-' 讓Python將第二個連字號前面的部分視為表示月份的數位;而'%d' 讓Python將字串的最後一部分視為月份中的一天(1~31)。
方法strptime() 可接受各種實參,並根據它們來決定如何解讀日期。表16-1列出了其中一些這樣的實參。
表16-1 模組datetime中設置日期和時間格式的實參
實參 | 含義 |
%A | 星期的名稱,如Monday |
%B | 月份名,如January |
%m | 用數字表示的月份(01~12) |
%d | 用數字表示月份中的一天(01~31) |
%Y | 四位的年份,如2015 |
%y | 兩位的年份,如15 |
%H | 24小時制的小時數(00~23) |
%I | 12小時制的小時數(01~12) |
%p | am或pm |
%M | 分鐘數(00~59) |
%S | 秒數(00~61) |
16.1.6 在圖表中添加日期
知道如何處理CSV檔中的日期後,就可對氣溫圖形進行改進了,即提取日期和最高氣溫,並將它們傳遞給plot() ,如下所示:
highs_lows.py
import csv
from datetime import datetime
from matplotlib import pyplot as plt
# 從檔中獲取日期和最高氣溫
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
❶ dates, highs = [], []
for row in reader:
❷ current_date = datetime.strptime(row[0], "%Y-%m-%d")
dates.append(current_date)
high = int(row[1])
highs.append(high)
# 根據資料繪製圖形
fig = plt.figure(dpi=128, figsize=(10, 6))
❸ plt.plot(dates, highs, c='red')
# 設置圖形的格式
plt.title("Daily high temperatures, July 2014", fontsize=24)
plt.xlabel('', fontsize=16)
❹ fig.autofmt_xdate()
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)
plt.show()
我們創建了兩個空清單,用於存儲從檔中提取的日期和最高氣溫(見❶)。然後,我們將包含日期資訊的資料(row[0] )轉換為datetime 對象(見❷),並將其附加到列表dates 末尾。在❸處,我們將日期和最高氣溫值傳遞給plot() 。在❹處,我們調用了fig.autofmt_xdate() 來繪製斜的日期標籤,以免它們彼此重疊。圖16-2顯示了改進後的圖表。
16.1.7 涵蓋更長的時間
設置好圖表後,我們來添加更多的資料,以成一幅更複雜的錫特卡天氣圖。請將文件sitka_weather_2014.csv複製到存儲本章程式的資料夾中,該檔包含Weather Underground提供的整年的錫特卡天氣資料。
現在可以創建覆蓋整年的天氣圖了:
highs_lows.py
--snip--
# 從檔中獲取日期和最高氣溫
❶ filename = 'sitka_weather_2014.csv'
with open(filename) as f:
--snip--
# 設置圖形的格式
❷ plt.title("Daily high temperatures - 2014", fontsize=24)
plt.xlabel('', fontsize=16)
--snip--
我們修改了檔案名,以使用新的資料檔案sitka_weather_2014.csv(見❶);我們還修改了圖表的標題,以反映其內容的變化(見❷)。圖16-3顯示了生成的圖形。
16.1.8 再繪製一個資料數列
圖16-3所示的改進後的圖表顯示了大量意義深遠的資料,但我們可以在其中再添加最低氣溫資料,使其更有用。為此,需要從資料檔案中提取最低氣溫,並將它們添加到圖表中,如下所示:
highs_lows.py
--snip--
# 從檔中獲取日期、最高氣溫和最低氣溫
filename = 'sitka_weather_2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
❶ dates, highs, lows = [], [], []
for row in reader:
current_date = datetime.strptime(row[0], "%Y-%m-%d")
dates.append(current_date)
high = int(row[1])
highs.append(high)
❷ low = int(row[3])
lows.append(low)
# 根據資料繪製圖形
fig = plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red')
❸ plt.plot(dates, lows, c='blue')
# 設置圖形的格式
❹ plt.title("Daily high and low temperatures - 2014", fontsize=24)
--snip--
在❶處,我們添加了空清單lows ,用於存儲最低氣溫。接下來,我們從每行的第4列(row[3] )提取每天的最低氣溫,並存儲它們(見❷)。在❸處,我們添加了一個對plot() 的調用,以使用藍色繪製最低氣溫。最後,我們修改了標題(見❹)。圖16-4顯示了這樣繪製出來的圖表。
16.1.9 給圖表區域著色
添加兩個資料數列後,我們就可以瞭解每天的氣溫範圍了。下面來給這個圖表做最後的修飾,通過著色來呈現每天的氣溫範圍。為此,我們將使用方法fill_between() ,它接受一個 x 值系列和兩個 y 值系列,並填充兩個 y 值系列之間的空間:
highs_lows.py
--snip--
# 根據資料繪製圖形
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)
❷ plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)
--snip--
❶處的實參alpha 指定顏色的透明度。Alpha 值為0表示完全透明,1(默認設置)表示完全不透明。通過將alpha 設置為0.5,可讓紅色和藍色折線的顏色看起來更淺。
在❷處,我們向fill_between() 傳遞了一個 x 值系列:清單dates ,還傳遞了兩個 y 值系列:highs 和lows 。實參facecolor 指定了填充區域的顏色,我們還將alpha 設置成了較小的值0.1,讓填充區域將兩個資料數列連接起來的同時不分散觀察者的注意力。圖16-5顯示了最高氣溫和最低氣溫之間的區域被填充的圖表。
通過著色,讓兩個資料集之間的區域顯而易見。
16.1.10 錯誤檢查
我們應該能夠使用有關任何地方的天氣資料來運行highs_lows.py中的代碼,但有些氣象站會偶爾出現故障,未能收集部分或全部其應該收集的資料。缺失資料可能會引發異常,如果不妥善地處理,還可能導致程式崩潰。
例如,我們來看看生成加利福尼亞死亡穀的氣溫圖時出現的情況。將文件death_valley_2014.csv複製到本章程式所在的資料夾,再修改highs_lows.py,使其生成死亡穀的氣溫圖:
highs_lows.py
--snip--
# 從檔中獲取日期、最高氣溫和最低氣溫
filename = 'death_valley_2014.csv'
with open(filename) as f:
--snip--
運行這個程式時,出現了一個錯誤,如下述輸出的最後一行所示:
Traceback (most recent call last):
File "highs_lows.py", line 17, in <module>
high = int(row[1])
ValueError: invalid literal for int() with base 10: ''
該traceback指出,Python無法處理其中一天的最高氣溫,因為它無法將空字串(' ' )轉換為整數。只要看一下death_valley_2014.csv,就能發現其中的問題:
2014-2-16,,,,,,,,,,,,,,,,,,,0.00,,,-1
其中好像沒有記錄2014年2月16日的資料,表示最高溫度的字串為空。為解決這種問題,我們在從CSV檔中讀取值時執行錯誤檢查代碼,對分析資料集時可能出現的異常進行處理,如下所示:
highs_lows.py
--snip--
# 從檔中獲取日期、最高氣溫和最低氣溫
filename = 'death_valley_2014.csv'
with open(filename) as f:
reader = csv.reader(f)
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, 'missing data')
else:
❸ dates.append(current_date)
highs.append(high)
lows.append(low)
#根據資料繪製圖形
--snip--
#設置圖形的格式
❹ title = "Daily high and low temperatures - 2014\nDeath Valley, CA"
plt.title(title, fontsize=20)
--snip--
對於每一行,我們都嘗試從中提取日期、最高氣溫和最低氣溫(見❶)。只要缺失其中一項資料,Python就會引發ValueError 異常,而我們可這樣處理:列印一條錯誤消息,指出缺失資料的日期(見❷)。列印錯誤消息後,迴圈將接著處理下一行。如果獲取特定日期的所有資料時沒有發生錯誤,將運行else 代碼塊,並將資料附加到相應清單的末尾(見❸)。鑒於我們繪圖時使用的是有關另一個地方的資訊,我們修改了標題,在圖表中指出了這個地方(見❹)。
如果你現在運行highs_lows.py ,將發現缺失資料的日期只有一個:
2014-02-16 missing data
圖16-6顯示了繪製出的圖形。
將這個圖表與錫特卡的圖表對比可知,總體而言,死亡穀比阿拉斯加東南部暖和,這可能符合預期,但這個沙漠中每天的溫差也更大,從著色區域的高度可以明顯看出這一點。
使用的很多資料集都可能缺失資料、資料格式不正確或資料本身不正確。對於這樣的情形,可使用本書前半部分介紹的工具來處理。在這裡,我們使用了一個try-except-else 代碼塊來處理資料缺失的問題。在有些情況下,需要使用continue 來跳過一些資料,或者使用remove() 或del 將已提取的資料刪除。可採用任何管用的方法,只要能進行精確而有意義的視覺化就好。
動手試一試
16-1 三藩市 :三藩市的氣溫更接近於錫特卡還是死亡穀呢?請繪製一個顯示三藩市最高氣溫和最低氣溫的圖表,並進行比較。可從http://www.wunderground.com/history/ 下載幾乎任何地方的天氣資料。為此,請輸入相應的地方和日期範圍,滾動到頁面底部,找到名為Comma-Delimited File的連結,再按一下該連結,將資料存儲為CSV檔。
16-2 比較錫特卡和死亡穀的氣溫 :在有關錫特卡和死亡穀的圖表中,氣溫刻度反映了資料範圍的不同。為準確地比較錫特卡和死亡穀的氣溫範圍,需要在y 軸上使用相同的刻度。為此,請修改圖16-5和圖16-6所示圖表的y 軸設置,對錫特卡和死亡穀的氣溫範圍進行直接比較(你也可以對任何兩個地方的氣溫範圍進行比較)。你還可以嘗試在一個圖表中呈現這兩個資料集。
16-3 降雨量 :選擇你感興趣的任何地方,通過視覺化將其降雨量呈現出來。為此,可先只涵蓋一個月的資料,確定代碼正確無誤後,再使用一整年的資料來運行它。
16-4 探索 :生成一些圖表,對你好奇的任何地方的其他天氣資料進行研究。
16.2 製作世界人口地圖:JSON格式
在本節中,你將下載JSON格式的人口資料,並使用json 模組來處理它們。Pygal提供了一個適合初學者使用的地圖創建工具,你將使用它來對人口資料進行視覺化,以探索全球人口的分佈情況。
16.2.1 下載世界人口資料
將文件population_data.json複製到本章程式所在的資料夾中,這個檔包含全球大部分國家1960~2010年的人口資料。Open Knowledge Foundation(http://data.okfn.org/ )提供了大量可以免費使用的資料集,這些資料就來自其中一個資料集。
16.2.2 提取相關的資料
我們來研究一下population_data.json,看看如何著手處理這個檔中的資料:
population_data.json
[
{
"Country Name": "Arab World",
"Country Code": "ARB",
"Year": "1960",
"Value": "96388069"
},
{
"Country Name": "Arab World",
"Country Code": "ARB",
"Year": "1961",
"Value": "98882541.4"
},
--snip--
]
這個檔實際上就是一個很長的Python清單,其中每個元素都是一個包含四個鍵的字典:國家名、國別碼、年份以及表示人口數量的值。我們只關心每個國家2010年的人口數量,因此我們首先編寫一個列印這些資訊的程式:
world_population.py
import json
# 將資料載入到一個清單中
filename = 'population_data.json'
with open(filename) as f:
❶ pop_data = json.load(f)
# 列印每個國家2010年的人口數量
❷ for pop_dict in pop_data:
❸ if pop_dict['Year'] == '2010':
❹ country_name = pop_dict['Country Name']
population = pop_dict['Value']
print(country_name + ": " + population)
我們首先導入了模組json ,以便能夠正確地載入檔中的資料,然後,我們將資料存儲在pop_data 中(見❶)。函數json.load() 將資料轉換為Python能夠處理的格式,這裡是一個列表。在❷處,我們遍歷pop_data 中的每個元素。每個元素都是一個字典,包含四個鍵—值對,我們將每個字典依次存儲在pop_dict 中。
在❸處,我們檢查字典的'Year' 鍵對應的值是否是2010(由於population_data.json中的值都是用引號括起的,因此我們執行的是字串比較)。如果年份為2010,我們就將與'Country Name' 相關聯的值存儲到country_name 中,並將與'Value' 相關聯的值存儲在population 中(見❹)。接下來,我們列印每個國家的名稱和人口數量。
輸出為一系列國家的名稱和人口數量:
Arab World: 357868000
Caribbean small states: 6880000
East Asia & Pacific (all income levels): 2201536674
--snip--
Zimbabwe: 12571000
我們捕獲的資料並非都包含準確的國家名,但這開了一個好頭。現在,我們需要將資料轉換為Pygal能夠處理的格式。
16.2.3 將字串轉換為數位值
population_data.json中的每個鍵和值都是字串。為處理這些人口資料,我們需要將表示人口數量的字串轉換為數位值,為此我們使用函數int() :
world_population.py
--snip--
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country_name = pop_dict['Country Name']
❶ population = int(pop_dict['Value'])
❷ print(country_name + ": " + str(population))
在❶處,我們將每個人口數量詞都存儲為數位格式。列印人口數量詞時,需要將其轉換為字串(見❷)。
然而,對於有些值,這種轉換會導致錯誤,如下所示:
Arab World: 357868000
Caribbean small states: 6880000
East Asia & Pacific (all income levels): 2201536674
--snip--
Traceback (most recent call last):
File "print_populations.py", line 12, in <module>
population = int(pop_dict['Value'])
❶ ValueError: invalid literal for int() with base 10: '1127437398.85751'
原始資料的格式常常不統一,因此經常會出現錯誤。導致上述錯誤的原因是,Python不能直接將包含小數點的字串'1127437398.85751' 轉換為整數(這個小數值可能是人口資料缺失時通過插值得到的)。為消除這種錯誤,我們先將字串轉換為浮點數,再將浮點數轉換為整數:
world_population.py
--snip--
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
print(country + ": " + str(population))
函數float() 將字串轉換為小數,而函數int() 丟棄小數部分,返回一個整數。現在,我們可以列印2010年的完整人口資料,不會導致錯誤了:
Arab World: 357868000
Caribbean small states: 6880000
East Asia & Pacific (all income levels): 2201536674
--snip--
Zimbabwe: 12571000
每個字串都成功地轉換成了浮點數,再轉換為整數。以數位格式存儲人口數量詞後,就可以使用它們來製作世界人口地圖了。
16.2.4 獲取兩個字母的國別碼
製作地圖前,還需要解決資料存在的最後一個問題。Pygal中的地圖製作工具要求資料為特定的格式:用國別碼表示國家,以及用數字表示人口數量。處理地理政治資料時,經常需要用到幾個標準化國別碼集。population_data.json中包含的是三個字母的國別碼,但Pygal使用兩個字母的國別碼。我們需要想辦法根據國家名獲取兩個字母的國別碼。
Pygal使用的國別碼存儲在模組i18n (internationalization的縮寫)中。字典COUNTRIES 包含的鍵和值分別為兩個字母的國別碼和國家名。要查看這些國別碼,可從模組i18n 中導入這個字典,並列印其鍵和值:
countries.py
from pygal.i18n import COUNTRIES
❶ for country_code in sorted(COUNTRIES.keys()):
print(country_code, COUNTRIES[country_code])
在上面的for 迴圈中,我們讓Python將鍵按字母順序排序(見❶),然後列印每個國別碼及其對應的國家:
ad Andorra
ae United Arab Emirates
af Afghanistan
--snip--
zw Zimbabwe
為獲取國別碼,我們將編寫一個函數,它在COUNTRIES 中查找並返回國別碼。我們將這個函數放在一個名為country_codes 的模組中,以便能夠在視覺化程式中導入它:
country_codes.py
from pygal.i18n import COUNTRIES
❶ def get_country_code(country_name):
"""根據指定的國家,返回Pygal使用的兩個字母的國別碼"""
❷ for code, name in COUNTRIES.items():
❸ if name == country_name:
return code
# 如果沒有找到指定的國家,就返回None
❹ return None
print(get_country_code('Andorra'))
print(get_country_code('United Arab Emirates'))
print(get_country_code('Afghanistan'))
get_country_code() 接受國家名,並將其存儲在形參country_name 中(見❶)。接下來,我們遍歷COUNTRIES 中的國家名—國別碼對(見❷);如果找到指定的國家名,就返回相應的國別碼(見❸)。在迴圈後面,我們在沒有找到指定的國家名時返回None (見❹)。最後,我們使用了三個國家名來調用這個函數,以核實它能否正確地工作。與預期的一樣,這個程式輸出了三個由兩個字母組成的國別碼:
ad
ae
af
使用這個函數前,先將country_codes.py中的print 語句刪除。
接下來,在world_population.py中導入get_country_code :
world_population.py
import json
from country_codes import get_country_code
--snip--
# 列印每個國家2010年的人口數量
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:
❷ print(code + ": "+ str(population))
❸ else:
print('ERROR - ' + country_name)
提取國家名和人口數量後,我們將國別碼存儲在code 中,如果沒有國別碼,就在其中存儲None (見❶)。如果返回了國別碼,就列印國別碼和相應國家的人口數量(見❷)。如果沒有找到國別碼,就顯示一條錯誤消息,其中包含無法找到國別碼的國家的名稱(見❸)。如果你運行這個程式,將看到一些國別碼和相應國家的人口數量,還有一些錯誤消息:
ERROR - Arab World
ERROR - Caribbean small states
ERROR - East Asia & Pacific (all income levels)
--snip--
af: 34385000
al: 3205000
dz: 35468000
--snip--
ERROR - Yemen, Rep.
zm: 12927000
zw: 12571000
導致顯示錯誤消息的原因有兩個。首先,並非所有人口數量對應的都是國家,有些人口數量對應的是地區(阿拉伯世界)和經濟類群(所有收入水準)。其次,有些統計資料使用了不同的完整國家名(如Yemen, Rep.,而不是Yemen)。當前,我們將忽略導致錯誤的資料,看看根據成功恢復了的資料製作出的地圖是什麼樣的。
16.2.5 製作世界地圖
有了國別碼後,製作世界地圖易如反掌。Pygal提供了圖表類型Worldmap ,可説明你製作呈現各國資料的世界地圖。為演示如何使用Worldmap ,我們來創建一個突出北美、中美和南美的簡單地圖:
americas.py
import pygal
❶ wm = pygal.Worldmap()
wm.title = 'North, Central, and South America'
❷ wm.add('North America', ['ca', 'mx', 'us'])
wm.add('Central America', ['bz', 'cr', 'gt', 'hn', 'ni', 'pa', 'sv'])
wm.add('South America', ['ar', 'bo', 'br', 'cl', 'co', 'ec', 'gf',
'gy', 'pe', 'py', 'sr', 'uy', 've'])
❸ wm.render_to_file('americas.svg')
在❶處,我們創建了一個Worldmap 實例,並設置了該地圖的的title 屬性。在❷處,我們使用了方法add() ,它接受一個標籤和一個列表,其中後者包含我們要突出的國家的國別碼。每次調用add() 都將為指定的國家選擇一種新顏色,並在圖表左邊顯示該顏色和指定的標籤。我們要以同一種顏色顯示整個北美地區,因此第一次調用add() 時,在傳遞給它的列表中包含'ca' 、'mx' 和'us' ,以同時突出加拿大、墨西哥和美國。接下來,對中美和南美國家做同樣的處理。
❸處的方法render_to_file() 創建一個包含該圖表的.svg檔,你可以在流覽器中打開它。輸出是一幅以不同顏色突出北美、中美和南美的地圖,如圖16-7所示。
知道如何創建包含彩色區域、顏色標示和標籤的地圖後,我們在地圖中添加資料,以顯示有關國家的資訊。
16.2.6 在世界地圖上呈現數位資料
為練習在地圖上呈現數位資料,我們來創建一幅地圖,顯示三個北美國家的人口數量:
na_populations.py
import pygal
wm = pygal.Worldmap()
wm.title = 'Populations of Countries in North America'
❶ wm.add('North America', {'ca': 34126000, 'us': 309349000, 'mx': 113423000})
wm.render_to_file('na_populations.svg')
首先,創建了一個Worldmap 實例並設置了標題。接下來,使用了方法add() ,但這次通過第二個實參傳遞了一個字典而不是清單(見❶)。這個字典將兩個字母的Pygal國別碼作為鍵,將人口數量作為值。Pygal根據這些數位自動給不同國家著以深淺不一的顏色(人口最少的國家顏色最淺,人口最多的國家顏色最深),如圖16-8所示。
這幅地圖具有交互性:如果你將滑鼠指向某個國家,將看到其人口數量。下面在這個地圖中添加更多的資料。
16.2.7 繪製完整的世界人口地圖
要呈現其他國家的人口數量,需要將前面處理的資料轉換為Pygal要求的字典格式:鍵為兩個字母的國別碼,值為人口數量。為此,在world_population.py中添加如下代碼:
world_population.py
import json
import pygal
from country_codes import get_country_code
# 將數據載入到清單中
--snip--
# 創建一個包含人口數量的字典
❶ cc_populations = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
code = get_country_code(country)
if code:
❷ cc_populations[code] = population
❸ wm = pygal.Worldmap()
wm.title = 'World Population in 2010, by Country'
❹ wm.add('2010', cc_populations)
wm.render_to_file('world_population.svg')
我們首先導入了pygal 。在❶處,我們創建了一個空字典,用於以Pygal要求的格式存儲國別碼和人口數量。在❷處,如果返回了國別碼,就將國別碼和人口數量分別作為鍵和值填充字典cc_populations 。我們還刪除了所有的print 語句。
在❸處,我們創建了一個Worldmap 實例,並設置其title 屬性。在❹處,我們調用了add() ,並向它傳遞由國別碼和人口數量組成的字典。圖16-9顯示了生成的地圖。
有幾個國家沒有相關的資料,我們將其顯示為黑色,但對於大多數國家,都根據其人口數量進行了著色。本章後面將處理資料缺失的問題,這裡先來修改著色,以更準確地反映各國的人口數量。在當前的地圖中,很多國家都是淺色的,只有兩個國家是深色的。對大多數國家而言,顏色深淺的差別不足以反映其人口數量的差別。為修復這種問題,我們將根據人口數量將國家分組,再分別給每個組著色。
16.2.8 根據人口數量將國家分組
印度和中國的人口比其他國家多得多,但在當前的地圖中,它們的顏色與其他國家差別較小。中國和印度的人口都超過了10億,接下來人口最多的國家是美國,但只有大約3億。下面不將所有國家都作為一個編組,而是根據人口數量分成三組——少於1000萬的、介於1000萬和10億之間的以及超過10億的:
world_population.py
--snip--
# 創建一個包含人口資料的字典
cc_populations = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
--snip--
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
# 看看每組分別包含多少個國家
❸ print(len(cc_pops_1), len(cc_pops_2), len(cc_pops_3))
wm = pygal.Worldmap()
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')
為將國家分組,我們創建了三個空字典(見❶)。接下來,遍歷cc_populations ,檢查每個國家的人口數量(見❷)。if-elif-else 代碼塊將每個國別碼-人口數量對加入到合適的字典(cc_pops_1 、cc_pops_2 或cc_pops_3 )中。
在❸處,我們列印這些字典的長度,以獲悉每個分組的規模。繪製地圖時,我們將全部三個分組都添加到Worldmap 中(見❹)。如果你現在運行這個程式,首先看到的將是每個分組的規模:
85 69 2
上述輸出表明,人口少於1000萬的國家有85個,人口介於1000萬和10億之間的國家有69個,還有兩個國家比較特殊,其人口都超過了10億。這樣的分組看起來足夠了,讓地圖包含豐富的資訊。圖16-10顯示了生成的地圖。
現在使用了三種不同的顏色,讓我們能夠看出人口數量上的差別。在每組中,各個國家都按人口從少到多著以從淺到深的顏色。
16.2.9 使用Pygal設置世界地圖的樣式
在這個地圖中,根據人口將國家分組雖然很有效,但默認的顏色設置很難看。例如,在這裡,Pygal選擇了鮮豔的粉色和綠色基色。下面使用Pygal樣式設置指令來調整顏色。
我們也讓Pygal使用一種基色,但將指定該基色,並讓三個分組的顏色差別更大:
world_population.py
import json
import pygal
❶ from pygal.style import RotateStyle
--snip--
# 根據人口數量將所有的國家分成三組
cc_pops_1, cc_pops_2, cc_pops_3 = {}, {}, {}
for cc, pop in cc_populations.items():
if pop < 10000000:
--snip--
❷ wm_style = RotateStyle('#336699')
❸ wm = pygal.Worldmap(style=wm_style)
wm.title = 'World Population in 2010, by Country'
--snip--
Pygal樣式存儲在模組style 中,我們從這個模組中導入了樣式RotateStyle (見❶)。創建這個類的實例時,需要提供一個實參——十六進位的RGB顏色(見❷);Pygal將根據指定的顏色為每組選擇顏色。十六進位格式 的RGB顏色是一個以井號(#)打頭的字串,後面跟著6個字元,其中前兩個字元表示紅色分量,接下來的兩個表示綠色分量,最後兩個表示藍色分量。每個分量的取值範圍為00 (沒有相應的顏色)~FF (包含最多的相應顏色)。如果你線上搜索hex color chooser(十六進位顏色選擇器 ),可找到讓你能夠嘗試選擇不同的顏色並顯示其RGB值的工具。這裡使用的顏色值(#336699)混合了少量的紅色(33)、多一些的綠色(66)和更多一些的藍色(99),它為RotateStyle 提供了一種淡藍色基色。
RotateStyle 返回一個樣式物件,我們將其存儲在wm_style 中。為使用這個樣式物件,我們在創建Worldmap 實例時以關鍵字實參的方式傳遞它(見❸)。更新後的地圖如圖16-11所示。
前面的樣式設置讓地圖的顏色更一致,也更容易區分不同的編組。
16.2.10 加亮顏色主題
Pygal通常預設使用較暗的顏色主題。為方便印刷,我使用LightColorizedStyle 加亮了地圖的顏色。這個類修改整個圖表的主題,包括背景色、標籤以及各個國家的顏色。要使用這個樣式,先導入它:
from pygal.style import LightColorizedStyle
然後就可獨立地使用LightColorizedStyle 了,例如:
wm_style = LightColorizedStyle
然而使用這個類時,你不能直接控制使用的顏色,Pygal將選擇默認的基色。要設置顏色,可使用RotateStyle ,並將LightColorizedStyle 作為基本樣式。為此,導入LightColorizedStyle 和RotateStyle :
from pygal.style import LightColorizedStyle, RotateStyle
再使用RotateStyle 創建一種樣式,並傳入另一個實參base_style :
wm_style = RotateStyle('#336699', base_style=LightColorizedStyle)
這設置了較亮的主題,同時根據通過實參傳遞的顏色給各個國家著色。使用這種樣式時,生成的圖表與本書的螢幕截圖更一致。
嘗試為不同的視覺化選擇合適的樣式設置指令時,在import 語句中指定別名會有所幫助:
from pygal.style import LightColorizedStyle as LCS, RotateStyle as RS
這樣,樣式定義將更短:
wm_style = RS('#336699', base_style=LCS)
通過使用幾個樣式設置指令,就能很好地控制圖表和地圖的外觀。
動手試一試
16-5 涵蓋所有國家 :本節制作人口地圖時,對於大約12個國家,程式不能自動確定其兩個字母的國別碼。請找出這些國家,在字典COUNTRIES 中找到它們的國別碼;然後,對於每個這樣的國家,都在get_country_code() 中添加一個if-elif 代碼塊,以返回其國別碼:
if country_name == 'Yemen, Rep.'
return 'ye'
elif --snip--
將這些代碼放在遍歷COUNTRIES 的迴圈和語句return None 之間。完成這樣的修改後,你看到的地圖將更完整。
16-6 國內生產總值 :Open Knowledge Foundation提供了一個資料集,其中包含全球各國的國內生產總值(GDP),可在http://data.okfn.org/data/core/gdp/ 找到這個資料集。請下載這個資料集的JSON版本,並繪製一個圖表,將全球各國最近一年的GDP呈現出來。
16-7 選擇你自己的資料 :世界銀行(The World Bank)提供了很多資料集,其中包含有關全球各國的資訊。請訪問http://data.worldbank.org/indicator/ ,並找到一個你感興趣的資料集。按一下該資料集,再按一下連結Download Data並選擇CSV。你將收到三個CSV檔,其中兩個包含字樣Metadata,你應使用第三個CSV檔。編寫一個程式,生成一個字典,它將兩個字母的Pygal國別碼作為鍵,並將你從這個檔中選擇的資料作為值。使用Worldmap 製作一個地圖,在其中呈現這些資料,並根據你的喜好設置這個地圖的樣式。
16-8 測試模組country_codes :我們編寫模組country_codes 時,使用了print 語句來核實get_country_code() 能否按預期那樣工作。請利用你在第11章學到的知識,為這個函數編寫合適的測試。
16.3 小結
在本章中,你學習了:如何使用網上的資料集;如何處理CSV和JSON檔,以及如何提取你感興趣的資料;如何使用matplotlib來處理以往的天氣資料,包括如何使用模組datetime ,以及如何在同一個圖表中繪製多個資料數列;如何使用Pygal繪製呈現各國資料的世界地圖,以及如何設置Pygal地圖和圖表的樣式。
有了使用CSV和JSON檔的經驗後,你將能夠處理幾乎任何要分析的資料。大多數線上資料集都可以以這兩種格式中的一種或兩種下載。學習使用這兩種格式為學習使用其他格式的資料做好了準備。
在下一章,你將編寫自動從網上採集資料並對其進行視覺化的程式。如果你只是將程式設計作為業餘愛好,學會這些技能可以增加樂趣;如果你有志于成為專業程式師,就必須掌握這些技能。












0 留言:
發佈留言