第06章 數據加載、存儲與文件格式--Python for Data Analysis 2nd

訪問數據是使用本書所介紹的這些工具的第一步。我會着重介紹pandas的數據輸入與輸出,雖然別的庫中也有不少以此爲目的的工具。
輸入輸出通常可以劃分爲幾個大類:讀取文本文件和其他更高效的磁盤存儲格式,加載數據庫中的數據,利用Web API操作網絡資源。

讀寫文本格式的數據

pandas提供了一些用於將表格型數據讀取爲DataFrame對象的函數。表6-1對它們進行了總結,其中read_csv和read_table可能會是你今後用得最多的。

pandas中的解析函數

函數 說明
read_csv 從文件,URL,文件型對象中加載帶有分隔符的數據,默認分隔符爲逗號
read_table 從文件,URL,文件型對象中加載帶有分隔符的數據,默認分隔符爲製表符
read_fwf 讀取定寬列格式數據(也就是說,沒有分隔符)
read_clipboard 讀取剪切板中的數據,可以看作是read_table的剪切板。在將網頁轉化爲表格時很有用
read_excel 從Excel XLS或XLSX file讀取表格數據
read_hdf 讀取pandas寫的HDF5文件
read_html 讀取HTML文檔中的所有表格
read_json 讀取JSON(JavaScript Object Notation)字符串中的數據
read_msgpack 二進制格式編碼的pandas數據
read_pickle 讀取python pickle格式中存儲的任意對象
read_sas 讀取存儲於SAS系統自定義存儲格式的SAS數據集
read_sql (使用SQLAlchemy)讀取SQL查詢結果爲pandas的DataFrame
read_stata 讀取Stata文件格式數據集
read——feather 讀取Feather二進制文件格式

我將大致介紹一下這些函數在將文本數據轉換爲DataFrame時所用到的一些技術。這些函數的選項可以劃分爲以下幾個大類:

  • 索引:將一個或多個列當做返回的DataFrame處理,以及是否從文件、用戶獲取列名。
  • 類型推斷和數據轉換:包括用戶定義值的轉換、和自定義的缺失值標記列表等。
  • 日期解析:包括組合功能,比如將分散在多個列中的日期時間信息組合成結果中的單個列。
  • 迭代:支持對大文件進行逐塊迭代。
  • 不規整數據問題:跳過一些行、頁腳、註釋或其他一些不重要的東西(比如由成千上萬個逗號隔開的數值數據)。

因爲工作中實際碰到的數據可能十分混亂,一些數據加載函數(尤其是read_csv)的選項逐漸變得複雜起來。面對不同的參數,感到頭痛很正常(read_csv有超過50個參數)。pandas文檔有這些參數的例子,如果你感到閱讀某個文件很難,可以通過相似的足夠多的例子找到正確的參數。

其中一些函數,比如pandas.read_csv,有類型推斷功能,因爲列數據的類型不屬於數據類型。也就是說,你不需要指定列的類型到底是數值、整數、布爾值,還是字符串。其它的數據格式,如HDF5、Feather和msgpack,會在格式中存儲數據類型。
日期和其他自定義類型的處理需要多花點工夫纔行。首先我們來看一個以逗號分隔的(CSV)文本文件:

!cat examples/ex1.csv
a,b,c,d,message
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo

筆記:這裏,我用的是Unix的cat shell命令將文件的原始內容打印到屏幕上。如果你用的是Windows,你可以使用type達到同樣的效果

由於該文件以逗號分隔,所以我們可以使用read_csv將其讀入一個DataFrame:

import pandas as pd
df = pd.read_csv('examples/ex1.csv')
df
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

我們還可以使用read_table,並指定分隔符:

pd.read_table('examples/ex1.csv', sep=',')
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

並不是所有文件都有標題行。看看下面這個文件:

!cat examples/ex2.csv
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo

讀入該文件的辦法有兩個。你可以讓pandas爲其分配默認的列名,也可以自己定義列名:

pd.read_csv('examples/ex2.csv', header=None)
0 1 2 3 4
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
pd.read_csv('examples/ex2.csv', names=['a', 'b', 'c', 'd', 'message'])
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

假設你希望將message列做成DataFrame的索引。你可以明確表示要將該列放到索引4的位置上,也可以通過index_col參數指定"message":

names = ['a', 'b', 'c', 'd', 'message']

pd.read_csv('examples/ex2.csv', names=names, index_col='message')
a b c d
message
hello 1 2 3 4
world 5 6 7 8
foo 9 10 11 12

如果希望將多個列做成一個層次化索引,只需傳入由列編號或列名組成的列表即可:

!cat examples/csv_mindex.csv
key1,key2,value1,value2
one,a,1,2
one,b,3,4
one,c,5,6
one,d,7,8
two,a,9,10
two,b,11,12
two,c,13,14
two,d,15,16
parsed = pd.read_csv('examples/csv_mindex.csv',
                     index_col=['key1', 'key2'])
parsed
value1 value2
key1 key2
one a 1 2
b 3 4
c 5 6
d 7 8
two a 9 10
b 11 12
c 13 14
d 15 16

有些情況下,有些表格可能不是用固定的分隔符去分隔字段的(比如空白符或其它模式)。看看下面這個文本文件:

list(open('examples/ex3.txt'))
['            A         B         C\n',
 'aaa -0.264438 -1.026059 -0.619500\n',
 'bbb  0.927272  0.302904 -0.032399\n',
 'ccc -0.264273 -0.386314 -0.217601\n',
 'ddd -0.871858 -0.348382  1.100491\n']

雖然可以手動對數據進行規整,這裏的字段是被數量不同的空白字符間隔開的。這種情況下,你可以傳遞一個正則表達式作爲read_table的分隔符。可以用正則表達式表達爲\s+,於是有:

result = pd.read_table('examples/ex3.txt', sep='\s+')
result
A B C
aaa -0.264438 -1.026059 -0.619500
bbb 0.927272 0.302904 -0.032399
ccc -0.264273 -0.386314 -0.217601
ddd -0.871858 -0.348382 1.100491

這裏,由於列名比數據行的數量少,所以read_table推斷第一列應該是DataFrame的索引。

這些解析器函數還有許多參數可以幫助你處理各種各樣的異形文件格式(表6-2列出了一些)。比如說,你可以用skiprows跳過文件的第一行、第三行和第四行:

 !cat examples/ex4.csv
# hey!
a,b,c,d,message
# just wanted to make things more difficult for you
# who reads CSV files with computers, anyway?
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo
pd.read_csv('examples/ex4.csv', skiprows=[0, 2, 3])
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

缺失值處理是文件解析任務中的一個重要組成部分。缺失數據經常是要麼沒有(空字符串),要麼用某個標記值表示。默認情況下,pandas會用一組經常出現的標記值進行識別,比如NA及NULL:

!cat examples/ex5.csv
something,a,b,c,d,message
one,1,2,3,4,NA
two,5,6,,8,world
three,9,10,11,12,foo
result = pd.read_csv('examples/ex5.csv')
result
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
 pd.isnull(result)
something a b c d message
0 False False False False False True
1 False False False True False False
2 False False False False False False

na_values可以用一個列表或集合的字符串表示缺失值:

result = pd.read_csv('examples/ex5.csv', na_values=['NULL'])
result
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo

字典的各列可以使用不同的NA標記值:

sentinels = {'message': ['foo', 'NA'], 'something': ['two']}


pd.read_csv('examples/ex5.csv', na_values=sentinels)
something a b c d message
0 one 1 2 3.0 4 NaN
1 NaN 5 6 NaN 8 world
2 three 9 10 11.0 12 NaN

pandas.read_csv和pandas.read_table常用的選項

參數 說明
path 表示文件系統位置、URL、文件型對象的字符串。
sep或delimiter 用於對行中各字段進行拆分的字符序列或正則表達式。
header 用作列名的行號。如果文件沒有標題行就將header參數設置爲None。
names 用於結果的列名列表,結合header=None,可以通過names來設置標題行。
index_col 用作行索引的列編號或行名。可以是單個名稱/數字或有多個名稱/數字組成的列表(層次化索引)。index_col = False第一列不用做行名。
uscols 返回列的子集,例如:[0,1,2] or [‘foo’, ‘bar’, ‘baz’]
dtype 整個數據或某列的數據類型,例如:{‘a’: np.float64, ‘b’: np.int32}
engine 解析引擎,C引擎快於python引擎,{‘c’, ‘python’}
converters 由列號/列名跟函數之間的映射關係組成的字典。如,{“age:”,f}會對列索引爲age列的所有值應用函數f。
true_values list, 默認爲: None。指定爲真的值
false_values list, default None。指定爲假的值
skiprows 需要忽略的行數(從0開始),設置的行數將不會進行讀取。
nrows 需要讀取的行數。
skip_footer 需要忽略的行數(從文件末尾開始計算)。
na_values 指定字符爲NA/NaN
verbose default False打印各種解析器輸出信息,如“非數值列中的缺失值的數量”等。
skip_blank_lines boolean, default True如果爲真,則跳過空行,而不是將其解釋爲NaN值。
parse_dates 嘗試將數據解析爲日期,默認爲False。
dayfirst 當解析有歧義的日期時,將其看做國際格式(例如,7/6/2012 —> June 7 , 2012)。默認爲False。
date_parser 用於解析日期的函數。
keep_date_col 默認是False,如果是True,parse_dates指定解析組合列後,保留原來的列
iterator 返回一個TextParser以便逐塊讀取文件。
chunksize 文件塊的大小(用於迭代)。
thousands 千分位分隔符,如",“或”."。
decimal str, default '.'要識別爲小數點的字符。例如,使用“,”作爲歐洲數據。
quotechar str (length 1)用於表示引用項的開始和結束的字符。帶引號的項可以包含分隔符,它將被忽略。
comment 用於註釋信息從行尾拆分出去的字符(一個或多個)。
encoding 用於unicode的文本編碼格式。例如,"utf-8"或"gbk"等文本的編碼格式。
dialect 如果提供,此參數將覆蓋以下參數的值(默認值或非默認值):分隔符、雙引號、escapechar、skipinitalspace、quotechar和引號。如果需要覆蓋值,將發出ParserWarning。有關詳細信息,請參閱csv.dialect文檔。

逐塊讀取文本文件

在處理很大的文件時,或找出大文件中的參數集以便於後續處理時,你可能只想讀取文件的一小部分或逐塊對文件進行迭代。

在看大文件之前,我們先設置pandas顯示地更緊些:

pd.options.display.max_rows = 6
result = pd.read_csv('examples/ex6.csv')
result 
one two three four key
0 0.467976 -0.038649 -0.295344 -1.824726 L
1 -0.358893 1.404453 0.704965 -0.200638 B
2 -0.501840 0.659254 -0.421691 -0.057688 G
... ... ... ... ... ...
9997 0.523331 0.787112 0.486066 1.093156 K
9998 -0.362559 0.598894 -1.843201 0.887292 G
9999 -0.096376 -1.012999 -0.657431 -0.573315 0

10000 rows × 5 columns

如果只想讀取幾行(避免讀取整個文件),通過nrows進行指定即可:

pd.read_csv('examples/ex6.csv', nrows=5)
one two three four key
0 0.467976 -0.038649 -0.295344 -1.824726 L
1 -0.358893 1.404453 0.704965 -0.200638 B
2 -0.501840 0.659254 -0.421691 -0.057688 G
3 0.204886 1.074134 1.388361 -0.982404 R
4 0.354628 -0.133116 0.283763 -0.837063 Q

要逐塊讀取文件,可以指定chunksize(行數):

chunker = pd.read_csv('examples/ex6.csv', chunksize=1000)
chunker
<pandas.io.parsers.TextFileReader at 0x7f164da1de48>

read_csv所返回的這個TextParser對象使你可以根據chunksize對文件進行逐塊迭代。比如說,我們可以迭代處理ex6.csv,將值計數聚合到"key"列中,如下所示:

chunker = pd.read_csv('examples/ex6.csv', chunksize=1000)

tot = pd.Series([])
for piece in chunker:
    tot = tot.add(piece['key'].value_counts(), fill_value=0)

tot = tot.sort_values(ascending=False)

tot[:10]
E    368.0
X    364.0
L    346.0
     ...  
F    335.0
K    334.0
H    330.0
Length: 10, dtype: float64

extParser還有一個get_chunk方法,它使你可以讀取任意大小的塊。

將數據寫出到文本格式

數據也可以被輸出爲分隔符格式的文本。我們再來看看之前讀過的一個CSV文件:

data = pd.read_csv('examples/ex5.csv')
data
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo

利用DataFrame的to_csv方法,我們可以將數據寫到一個以逗號分隔的文件中:

data.to_csv('examples/out.csv')
 !cat examples/out.csv
,something,a,b,c,d,message
0,one,1,2,3.0,4,
1,two,5,6,,8,world
2,three,9,10,11.0,12,foo

當然,還可以使用其他分隔符(由於這裏直接寫出到sys.stdout,所以僅僅是打印出文本結果而已):

import sys
data.to_csv(sys.stdout, sep='|')
|something|a|b|c|d|message
0|one|1|2|3.0|4|
1|two|5|6||8|world
2|three|9|10|11.0|12|foo

缺失值在輸出結果中會被表示爲空字符串。你可能希望將其表示爲別的標記值:

data.to_csv(sys.stdout, na_rep='NULL')
,something,a,b,c,d,message
0,one,1,2,3.0,4,NULL
1,two,5,6,NULL,8,world
2,three,9,10,11.0,12,foo

如果沒有設置其他選項,則會寫出行和列的標籤。當然,它們也都可以被禁用:

data.to_csv(sys.stdout, index=False, header=False)
one,1,2,3.0,4,
two,5,6,,8,world
three,9,10,11.0,12,foo

此外,你還可以只寫出一部分的列,並以你指定的順序排列:

data.to_csv(sys.stdout, index=False, columns=['a', 'b', 'c'])
a,b,c
1,2,3.0
5,6,
9,10,11.0

Series也有一個to_csv方法:

dates = pd.date_range('1/1/2000', periods=7)
ts = pd.Series(np.arange(7), index=dates)

ts.to_csv('examples/tseries.csv')
!cat examples/tseries.csv
2000-01-01,0
2000-01-02,1
2000-01-03,2
2000-01-04,3
2000-01-05,4
2000-01-06,5
2000-01-07,6

處理分隔符格式

大部分存儲在磁盤上的表格型數據都能用pandas.read_table進行加載。然而,有時還是需要做一些手工處理。由於接收到含有畸形行的文件而使read_table出毛病的情況並不少見。爲了說明這些基本工具,看看下面這個簡單的CSV文件:

!cat examples/ex7.csv
"a","b","c"
"1","2","3"
"1","2","3"

對於任何單字符分隔符文件,可以直接使用Python內置的csv模塊。將任意已打開的文件或文件型的對象傳給csv.reader:

import csv
f = open('examples/ex7.csv')

reader = csv.reader(f)

對這個reader進行迭代將會爲每行產生一個元組(並移除了所有的引號):對這個reader進行迭代將會爲每行產生一個元組(並移除了所有的引號):

for line in reader:
    print(line)
['a', 'b', 'c']
['1', '2', '3']
['1', '2', '3']

現在,爲了使數據格式合乎要求,你需要對其做一些整理工作。我們一步一步來做。首先,讀取文件到一個多行的列表中:

with open('examples/ex7.csv') as f:
    lines = list(csv.reader(f))

然後,我們將這些行分爲標題行和數據行:

header, values = lines[0], lines[1:]

然後,我們可以用字典構造式和zip(*values),後者將行轉置爲列,創建數據列的字典:

data_dict = {h: v for h, v in zip(header, zip(*values))}
data_dict
{'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')}

CSV文件的形式有很多。只需定義csv.Dialect的一個子類即可定義出新格式(如專門的分隔符、字符串引用約定、行結束符等):

class my_dialect(csv.Dialect):
    lineterminator = '\n'
    delimiter = ';'
    quotechar = '"'
    quoting = csv.QUOTE_MINIMAL
f = open('examples/ex7.csv')
reader = csv.reader(f, dialect=my_dialect)

各個CSV語支的參數也可以用關鍵字的形式提供給csv.reader,而無需定義子類:

reader = csv.reader(f, delimiter='|')

可用的選項(csv.Dialect的屬性)及其功能如表

參數 說明
delimiter 用於分隔字段的單字符字符串,默認“,”。
lineterminator 用於寫操作的行結束符,默認爲“\r\n”
quotechar 用於帶有特殊字符(如分隔符)的字段的引用符號,默認“
quoting 引用約定。可選值包括csv.QUOTE_ALL(引用所有字段),csv.QUOTE_MINIMAL,csv.QUOTE_NONNUMERIC,csv.QUOTE_NON
skipinitialspace 忽略分隔符後面的空白字符,默認False
doublequote 如何出爐字段內的引用符號
escapechar 用於對分隔符進行轉義的字符串

筆記:對於那些使用複雜分隔符或多字符分隔符的文件,csv模塊就無能爲力了。這種情況下,你就只能使用字符串的split方法或正則表達式方法re.split進行行拆分和其他整理工作了。

要手工輸出分隔符文件,你可以使用csv.writer。它接受一個已打開且可寫的文件對象以及跟csv.reader相同的那些語支和格式化選項:

import csv
with open('mydata.csv', 'w') as f:
    writer = csv.writer(f, dialect=my_dialect)
    writer.writerow(('one', 'two', 'three'))
    writer.writerow(('1', '2', '3'))
    writer.writerow(('4', '5', '6'))
    writer.writerow(('7', '8', '9'))

JSON數據

JSON(JavaScript Object Notation的簡稱)已經成爲通過HTTP請求在Web瀏覽器和其他應用程序之間發送數據的標準格式之一。它是一種比表格型文本格式(如CSV)靈活得多的數據格式。下面是一個例子:

obj = """
{"name": "Wes",
 "places_lived": ["United States", "Spain", "Germany"],
 "pet": null,
 "siblings": [{"name": "Scott", "age": 30, "pets": ["Zeus", "Zuko"]},
              {"name": "Katie", "age": 38,
               "pets": ["Sixes", "Stache", "Cisco"]}]
}
"""

除其空值null和一些其他的細微差別(如列表末尾不允許存在多餘的逗號)之外,JSON非常接近於有效的Python代碼。基本類型有對象(字典)、數組(列表)、字符串、數值、布爾值以及null。對象中所有的鍵都必須是字符串。許多Python庫都可以讀寫JSON數據。我將使用json,因爲它是構建於Python標準庫中的。通過json.loads即可將JSON字符串轉換成Python形式:

import json

result = json.loads(obj)

result
{'name': 'Wes',
 'places_lived': ['United States', 'Spain', 'Germany'],
 'pet': None,
 'siblings': [{'name': 'Scott', 'age': 30, 'pets': ['Zeus', 'Zuko']},
  {'name': 'Katie', 'age': 38, 'pets': ['Sixes', 'Stache', 'Cisco']}]}

json.dumps則將Python對象轉換成JSON格式:

asjson = json.dumps(result)

如何將(一個或一組)JSON對象轉換爲DataFrame或其他便於分析的數據結構就由你決定了。最簡單方便的方式是:向DataFrame構造器傳入一個字典的列表(就是原先的JSON對象),並選取數據字段的子集:

siblings = pd.DataFrame(result['siblings'], columns=['name', 'age'])

siblings
name age
0 Scott 30
1 Katie 38

pandas.read_json可以自動將特別格式的JSON數據集轉換爲Series或DataFrame。例如:

!cat examples/example.json
[{"a": 1, "b": 2, "c": 3},
 {"a": 4, "b": 5, "c": 6},
 {"a": 7, "b": 8, "c": 9}]

pandas.read_json的默認選項假設JSON數組中的每個對象是表格中的一行:

data = pd.read_json('examples/example.json')
data
a b c
0 1 2 3
1 4 5 6
2 7 8 9

第7章中關於USDA Food Database的那個例子進一步講解了JSON數據的讀取和處理(包括嵌套記錄)。

如果你需要將數據從pandas輸出到JSON,可以使用to_json方法:

print(data.to_json())
{"a":{"0":1,"1":4,"2":7},"b":{"0":2,"1":5,"2":8},"c":{"0":3,"1":6,"2":9}}
print(data.to_json(orient='records'))
[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]

XML和HTML:Web信息收集

Python有許多可以讀寫常見的HTML和XML格式數據的庫,包括lxml、Beautiful Soup和html5lib。lxml的速度比較快,但其它的庫處理有誤的HTML或XML文件更好。
pandas有一個內置的功能,read_html,它可以使用lxml和Beautiful Soup自動將HTML文件中的表格解析爲DataFrame對象。爲了進行展示,我從美國聯邦存款保險公司下載了一個HTML文件(pandas文檔中也使用過),它記錄了銀行倒閉的情況。首先,你需要安裝read_html用到的庫:

conda install lxml
pip install beautifulsoup4 html5lib

如果你用的不是conda,可以使用pip install lxml。

pandas.read_html有一些選項,默認條件下,它會搜索、嘗試解析

標籤內的的表格數據。結果是一個列表的DataFrame對象:

tables = pd.read_html('examples/fdic_failed_bank_list.html')
len(tables)
1
failures = tables[0]
failures.head()
Bank Name City ST CERT Acquiring Institution Closing Date Updated Date
0 Allied Bank Mulberry AR 91 Today's Bank September 23, 2016 November 17, 2016
1 The Woodbury Banking Company Woodbury GA 11297 United Bank August 19, 2016 November 17, 2016
2 First CornerStone Bank King of Prussia PA 35312 First-Citizens Bank & Trust Company May 6, 2016 September 6, 2016
3 Trust Company Bank Memphis TN 9956 The Bank of Fayette County April 29, 2016 September 6, 2016
4 North Milwaukee State Bank Milwaukee WI 20364 First-Citizens Bank & Trust Company March 11, 2016 June 16, 2016

因爲failures有許多列,pandas插入了一個換行符\。

這裏,我們可以做一些數據清洗和分析(後面章節會進一步講解),比如計算按年份計算倒閉的銀行數:

 close_timestamps = pd.to_datetime(failures['Closing Date'])
close_timestamps.dt.year.value_counts()

2010    157
2009    140
2011     92
2012     51
2008     25
2013     24
2014     18
2002     11
2015      8
2016      5
2004      4
2001      4
2007      3
2003      3
2000      2
Name: Closing Date, dtype: int64

利用lxml.objectify解析XML

XML(Extensible Markup Language)是另一種常見的支持分層、嵌套數據以及元數據的結構化數據格式。本書所使用的這些文件實際上來自於一個很大的XML文檔。
前面,我介紹了pandas.read_html函數,它可以使用lxml或Beautiful Soup從HTML解析數據。XML和HTML的結構很相似,但XML更爲通用。這裏,我會用一個例子演示如何利用lxml從XML格式解析數據。
紐約大都會運輸署發佈了一些有關其公交和列車服務的數據資料(http://www.mta.info/developers/download.html)。這裏,我們將看看包含在一組XML文件中的運行情況數據。每項列車或公交服務都有各自的文件(如Metro-North Railroad的文件是Performance_MNR.xml),其中每條XML記錄就是一條月度數據,如下所示:

<INDICATOR>
  <INDICATOR_SEQ>373889</INDICATOR_SEQ>
  <PARENT_SEQ></PARENT_SEQ>
  <AGENCY_NAME>Metro-North Railroad</AGENCY_NAME>
  <INDICATOR_NAME>Escalator Availability</INDICATOR_NAME>
  <DESCRIPTION>Percent of the time that escalators are operational
  systemwide. The availability rate is based on physical observations performed
  the morning of regular business days only. This is a new indicator the agency
  began reporting in 2009.</DESCRIPTION>
  <PERIOD_YEAR>2011</PERIOD_YEAR>
  <PERIOD_MONTH>12</PERIOD_MONTH>
  <CATEGORY>Service Indicators</CATEGORY>
  <FREQUENCY>M</FREQUENCY>
  <DESIRED_CHANGE>U</DESIRED_CHANGE>
  <INDICATOR_UNIT>%</INDICATOR_UNIT>
  <DECIMAL_PLACES>1</DECIMAL_PLACES>
  <YTD_TARGET>97.00</YTD_TARGET>
  <YTD_ACTUAL></YTD_ACTUAL>
  <MONTHLY_TARGET>97.00</MONTHLY_TARGET>
  <MONTHLY_ACTUAL></MONTHLY_ACTUAL>
</INDICATOR>

我們先用lxml.objectify解析該文件,然後通過getroot得到該XML文件的根節點的引用:

from lxml import objectify

path = 'examples/Performance_MNR.xml'
parsed = objectify.parse(open(path))
root = parsed.getroot()

root.INDICATOR返回一個用於產生各個XML元素的生成器。對於每條記錄,我們可以用標記名(如YTD_ACTUAL)和數據值填充一個字典(排除幾個標記):

data = []

skip_fields = ['PARENT_SEQ', 'INDICATOR_SEQ',
               'DESIRED_CHANGE', 'DECIMAL_PLACES']

for elt in root:
    el_data = {}
    for child in elt.getchildren():
        if child.tag in skip_fields:
            continue
        el_data[child.tag] = child.pyval
    data.append(el_data)

data
[{'AGENCY_NAME': 'Metro-North Railroad',
  'INDICATOR_NAME': 'Escalator Availability',
  'DESCRIPTION': 'Percent of the time that escalators are operational\n  systemwide. The availability rate is based on physical observations performed\n  the morning of regular business days only. This is a new indicator the agency\n  began reporting in 2009.',
  'PERIOD_YEAR': 2011,
  'PERIOD_MONTH': 12,
  'CATEGORY': 'Service Indicators',
  'FREQUENCY': 'M',
  'INDICATOR_UNIT': '%',
  'YTD_TARGET': 97.0,
  'YTD_ACTUAL': '',
  'MONTHLY_TARGET': 97.0,
  'MONTHLY_ACTUAL': ''}]

最後,將這組字典轉換爲一個DataFrame:

perf = pd.DataFrame(data)
perf.head()
AGENCY_NAME CATEGORY DESCRIPTION FREQUENCY INDICATOR_NAME INDICATOR_UNIT MONTHLY_ACTUAL MONTHLY_TARGET PERIOD_MONTH PERIOD_YEAR YTD_ACTUAL YTD_TARGET
0 Metro-North Railroad Service Indicators Percent of the time that escalators are operat... M Escalator Availability % 97.0 12 2011 97.0

XML數據可以比本例複雜得多。每個標記都可以有元數據。看看下面這個HTML的鏈接標籤(它也算是一段有效的XML):

from io import StringIO
tag = '<a href="http://www.google.com">Google</a>'
root = objectify.parse(StringIO(tag)).getroot()

現在就可以訪問標籤或鏈接文本中的任何字段了(如href):

 root
<Element a at 0x7f1bce06b588>
root.get('href')
'http://www.google.com'
root.text
'Google'

二進制數據格式

實現數據的高效二進制格式存儲最簡單的辦法之一是使用Python內置的pickle序列化。pandas對象都有一個用於將數據以pickle格式保存到磁盤上的to_pickle方法:

frame = pd.read_csv('examples/ex1.csv')
frame
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
frame.to_pickle('examples/frame_pickle')

你可以通過pickle直接讀取被pickle化的數據,或是使用更爲方便的pandas.read_pickle:

pd.read_pickle('examples/frame_pickle')
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

pandas內置支持兩個二進制數據格式:HDF5和MessagePack。下一節,我會給出幾個HDF5的例子,但我建議你嘗試下不同的文件格式,看看它們的速度以及是否適合你的分析工作。pandas或NumPy數據的其它存儲格式有:

bcolz:一種可壓縮的列存儲二進制格式,基於Blosc壓縮庫。
Feather:我與R語言社區的Hadley Wickham設計的一種跨語言的列存儲文件格式。Feather使用了Apache Arrow的列式內存格式。

使用HDF5格式

HDF5是一種存儲大規模科學數組數據的非常好的文件格式。它可以被作爲C標準庫,帶有許多語言的接口,如Java、Python和MATLAB等。HDF5中的HDF指的是層次型數據格式(hierarchical data format)。每個HDF5文件都含有一個文件系統式的節點結構,它使你能夠存儲多個數據集並支持元數據。與其他簡單格式相比,HDF5支持多種壓縮器的即時壓縮,還能更高效地存儲重複模式數據。對於那些非常大的無法直接放入內存的數據集,HDF5就是不錯的選擇,因爲它可以高效地分塊讀寫。

雖然可以用PyTables或h5py庫直接訪問HDF5文件,pandas提供了更爲高級的接口,可以簡化存儲Series和DataFrame對象。HDFStore類可以像字典一樣,處理低級的細節:

frame = pd.DataFrame({'a': np.random.randn(100)})

store = pd.HDFStore('mydata.h5')


store['obj1'] = frame

store['obj1_col'] = frame['a']


store
<class 'pandas.io.pytables.HDFStore'>
File path: mydata.h5

HDF5文件中的對象可以通過與字典一樣的API進行獲取:

store['obj1'].head()
a
0 3.178434
1 -0.160999
2 -0.355488
3 -0.095228
4 -1.638993

HDFStore支持兩種存儲模式,‘fixed’和’table’。後者通常會更慢,但是支持使用特殊語法進行查詢操作:

store.put('obj2', frame, format='table')

store.select('obj2', where=['index >= 10 and index <= 15'])
a
10 1.186026
11 1.568230
12 -0.556065
13 0.348825
14 0.263240
15 -1.061314
store.close()

put是store[‘obj2’] = frame方法的顯示版本,允許我們設置其它的選項,比如格式。

pandas.read_hdf函數可以快捷使用這些工具:

frame.to_hdf('mydata.h5', 'obj3', format='table')

pd.read_hdf('mydata.h5', 'obj3', where=['index < 5'])
a
0 3.178434
1 -0.160999
2 -0.355488
3 -0.095228
4 -1.638993

筆記:如果你要處理的數據位於遠程服務器,比如Amazon S3或HDFS,使用專門爲分佈式存儲(比如Apache Parquet)的二進制格式也許更加合適。Python的Parquet和其它存儲格式還在不斷的發展之中,所以這本書中沒有涉及。

如果需要本地處理海量數據,我建議你好好研究一下PyTables和h5py,看看它們能滿足你的哪些需求。。由於許多數據分析問題都是IO密集型(而不是CPU密集型),利用HDF5這樣的工具能顯著提升應用程序的效率。

注意:HDF5不是數據庫。它最適合用作“一次寫多次讀”的數據集。雖然數據可以在任何時候被添加到文件中,但如果同時發生多個寫操作,文件就可能會被破壞。

讀取Microsoft Excel文件

pandas的ExcelFile類或pandas.read_excel函數支持讀取存儲在Excel 2003(或更高版本)中的表格型數據。這兩個工具分別使用擴展包xlrd和openpyxl讀取XLS和XLSX文件。你可以用pip或conda安裝它們。
要使用ExcelFile,通過傳遞xls或xlsx路徑創建一個實例:

xlsx = pd.ExcelFile('examples/ex1.xlsx')

存儲在表單中的數據可以read_excel讀取到DataFrame(原書這裏寫的是用parse解析,但代碼中用的是read_excel,是個筆誤:只換了代碼,沒有改文字):

pd.read_excel(xlsx, 'Sheet1')
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

如果要讀取一個文件中的多個表單,創建ExcelFile會更快,但你也可以將文件名傳遞到pandas.read_excel:

frame = pd.read_excel('examples/ex1.xlsx', 'Sheet1')

frame
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

如果要將pandas數據寫入爲Excel格式,你必須首先創建一個ExcelWriter,然後使用pandas對象的to_excel方法將數據寫入到其中:

writer = pd.ExcelWriter('examples/ex2.xlsx')

frame.to_excel(writer, 'Sheet1')
writer.save()

你還可以不使用ExcelWriter,而是傳遞文件的路徑到to_excel:

frame.to_excel('examples/ex3.xlsx')

Web APIs交互

許多網站都有一些通過JSON或其他格式提供數據的公共API。通過Python訪問這些API的辦法有不少。一個簡單易用的辦法(推薦)是requests包(http://docs.python-requests.org)。
爲了搜索最新的30個GitHub上的pandas主題,我們可以發一個HTTP GET請求,使用requests擴展庫:

import requests

url = 'https://api.github.com/repos/pandas-dev/pandas/issues'
    
resp = requests.get(url)


resp 
<Response [200]>

響應對象的json方法會返回一個包含被解析過的JSON字典,加載到一個Python對象中:

data = resp.json()
data[0]['title']
'BUG: Fix groupby with MultiIndex Series corner case (#25704)'

data中的每個元素都是一個包含所有GitHub主題頁數據(不包含評論)的字典。我們可以直接傳遞數據到DataFrame,並提取感興趣的字段:

issues = pd.DataFrame(data, columns=['number', 'title',
                                         'labels', 'state'])
issues.head()
number title labels state
0 25743 BUG: Fix groupby with MultiIndex Series corner... [] open
1 25738 Dropna argument is now respected when false in... [{'id': 2822342, 'node_id': 'MDU6TGFiZWwyODIyM... open
2 25737 Resample with freq less than 10T generates dif... [] open
3 25736 Fix incorrect example in wide_to_long docstring [{'id': 134699, 'node_id': 'MDU6TGFiZWwxMzQ2OT... open
4 25734 Pandas 0.24.x throws an error when using freez... [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=... open

花費一些精力,你就可以創建一些更高級的常見的Web API的接口,返回DataFrame對象,方便進行分析。

數據庫交互

在商業場景下,大多數數據可能不是存儲在文本或Excel文件中。基於SQL的關係型數據庫(如SQL Server、PostgreSQL和MySQL等)使用非常廣泛,其它一些數據庫也很流行。數據庫的選擇通常取決於性能、數據完整性以及應用程序的伸縮性需求。

將數據從SQL加載到DataFrame的過程很簡單,此外pandas還有一些能夠簡化該過程的函數。例如,我將使用SQLite數據庫(通過Python內置的sqlite3驅動器):

import sqlite3
query = """
   CREATE TABLE test
    (a VARCHAR(20), b VARCHAR(20),
     c REAL,        d INTEGER
    );"""
con = sqlite3.connect('mydata.sqlite')
con.execute(query)
<sqlite3.Cursor at 0x7f1bca4c4570>
con.commit()

然後插入幾行數據:

data = [('Atlanta', 'Georgia', 1.25, 6),
       ('Tallahassee', 'Florida', 2.6, 3),
        ('Sacramento', 'California', 1.7, 5)]


stmt = "INSERT INTO test VALUES(?, ?, ?, ?)"

con.executemany(stmt, data)
<sqlite3.Cursor at 0x7f1bca4c42d0>
con.commit()

從表中選取數據時,大部分Python SQL驅動器(PyODBC、psycopg2、MySQLdb、pymssql等)都會返回一個元組列表:

cursor = con.execute('select * from test')

rows = cursor.fetchall()
rows
[('Atlanta', 'Georgia', 1.25, 6),
 ('Tallahassee', 'Florida', 2.6, 3),
 ('Sacramento', 'California', 1.7, 5)]

你可以將這個元組列表傳給DataFrame構造器,但還需要列名(位於光標的description屬性中):

cursor.description
(('a', None, None, None, None, None, None),
 ('b', None, None, None, None, None, None),
 ('c', None, None, None, None, None, None),
 ('d', None, None, None, None, None, None))
pd.DataFrame(rows, columns=[x[0] for x in cursor.description])
a b c d
0 Atlanta Georgia 1.25 6
1 Tallahassee Florida 2.60 3
2 Sacramento California 1.70 5
con.close()

這種數據規整操作相當多,你肯定不想每查一次數據庫就重寫一次。SQLAlchemy項目是一個流行的Python SQL工具,它抽象出了SQL數據庫中的許多常見差異。pandas有一個read_sql函數,可以讓你輕鬆的從SQLAlchemy連接讀取數據。這裏,我們用SQLAlchemy連接SQLite數據庫,並從之前創建的表讀取數據:

import sqlalchemy as sqla


db = sqla.create_engine('sqlite:///mydata.sqlite')

result = pd.read_sql('select * from test', db)

result
a b c d
0 Atlanta Georgia 1.25 6
1 Tallahassee Florida 2.60 3
2 Sacramento California 1.70 5

總結

訪問數據通常是數據分析的第一步。在本章中,我們已經學了一些有用的工具。在接下來的章節中,我們將深入研究數據規整、數據可視化、時間序列分析和其它主題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章