1. 程式人生 > >資料基礎---《利用Python進行資料分析·第2版》第6章 資料載入、儲存與檔案格式

資料基礎---《利用Python進行資料分析·第2版》第6章 資料載入、儲存與檔案格式

之前自己對於numpy和pandas是要用的時候東學一點西一點,直到看到《利用Python進行資料分析·第2版》,覺得只看這一篇就夠了。非常感謝原博主的翻譯和分享。

訪問資料是使用本書所介紹的這些工具的第一步。我會著重介紹pandas的資料輸入與輸出,雖然別的庫中也有不少以此為目的的工具。

輸入輸出通常可以劃分為幾個大類:讀取文字檔案和其他更高效的磁碟儲存格式,載入資料庫中的資料,利用Web API操作網路資源。

6.1 讀寫文字格式的資料

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

函式 說明
read_csv 檔案、URL、檔案型物件中載入帶分隔符的資料。預設分隔符為逗號
read_table 從檔案、URL、檔案型物件中載入帶分隔符的資料。預設分隔符為製表符(‘t’)
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二進位制檔案格式
表6-1 pandas中的解析函式

我將大致介紹一下這些函式在將文字資料轉換為DataFrame時所用到的一些技術。這些函式的選項可以劃分為以下幾個大類:

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

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

其中一些函式,比如pandas.read_csv,有型別推斷功能,因為列資料的型別不屬於資料型別。也就是說,你不需要指定列的型別到底是數值、整數、布林值,還是字串。其它的資料格式,如HDF5、Feather和msgpack,會在格式中儲存資料型別。

日期和其他自定義型別的處理需要多花點工夫才行。首先我們來看一個以逗號分隔的(CSV)文字檔案:

In [8]: !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
import numpy as np
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

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

In [12]: !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']

雖然可以手動對資料進行規整,這裡的欄位是被數量不同的空白字元間隔開的。這種情況下,你可以傳遞一個正則表示式作為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’
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

表6-2列出了pandas.read_csv和pandas.read_table常用的選項。

表6-2: read_csv/read_table函式的引數
引數 說明
path 表示檔案系統位置、URL、檔案型物件的字串
sep或 delimiter用於對行中各欄位進行拆分的字元序列或正則表示式
header 用作列名的行號。預設為0(第一行),如果沒有 header行就應該設定為None
index_col 用作行索引的列編號或列名。可以是單個名稱/數字或由多個名稱/數字組成的列表(層次化索引)
names 用於結果的列名列表,結合 header=None
skiprows 需要忽略的行數(從檔案開始處算起),或需要跳過的行號列表(從0開始)
na_values 一組用於替換NA的值
comment 用於將註釋資訊從行尾拆分出去的字元(一個或多個)
parse_dates 嘗試將資料解析為日期,預設為Fa
keep_date_col 如果連線多列解析日期,則保持參與連線的列。預設為 False
converters 由列號/列名跟函式之間的對映關係組成的字典。例如,{foo’:f}會對foo列的所有值應用函式f
dayfirst 當解析有歧義的日期時,將其看做國際格式(例如,7/6/2012→June 7,2012)。預設為Fase
date_parser 用於解析日期的函式
nrows 需要讀取的行數(從檔案開始處算起)
iterator 返回一個 TextParser以便逐塊讀取檔案
chunksize 檔案塊的大小(用於迭代)
skip_footer 需要忽略的行數(從檔案末尾處算起)
verbose 列印各種解析器輸出資訊,比如“非數值列中缺失值的數量”等
encoding 用於 unicode的文字編碼格式。例如,“utf-8”表示用UTF-8編碼的文字
squeeze 如果資料經解析後僅含一列,則返回Series
thousands 千分位分隔符,如“,”或“.”

逐塊讀取文字檔案

在處理很大的檔案時,或找出大檔案中的引數集以便於後續處理時,你可能只想讀取檔案的一小部分或逐塊對檔案進行迭代。
首先使用office打不開;然後在python中使用基本的pandas.read_csv開啟檔案時:MemoryError;就可以考慮逐塊讀取。例如資料集是個csv檔案,大小為3.7G,並且對於資料一無所知,所以首先開啟前5行觀察資料的型別,列標籤等等.
還可以考慮Python多程序分塊讀取超大檔案。

在看大檔案之前,我們先設定pandas顯示地更緊些:

pd.options.display.max_rows = 10
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
3 0.204886 1.074134 1.388361 -0.982404 R
4 0.354628 -0.133116 0.283763 -0.837063 Q
... ... ... ... ... ...
9995 2.311896 -0.417070 -1.409599 -0.515821 L
9996 -0.479893 -0.650419 0.745152 -0.646038 E
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)#意思是1000行為一塊逐塊讀取
chunker
<pandas.io.parsers.TextFileReader at 0x1ec1f23e208>

read_csv所返回的這個TextParser物件使你可以根據chunksize對檔案進行逐塊迭代(這個時候並沒有真正把資料讀到chunker變數中)。比如說,我們可以迭代處理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
E    368.0
X    364.0
L    346.0
O    343.0
Q    340.0
     ...  
5    157.0
2    152.0
0    151.0
9    150.0
1    146.0
Length: 36, dtype: float64

TextParser還有一個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(sys.stdout)
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"
import csv
f=open('examples/ex7.csv')
reader=csv.reader(f)

對這個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:]
list(zip(header,zip(*values)))
[('a', ('1', '1')), ('b', ('2', '2')), ('c', ('3', '3'))]
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
reader = csv.reader(f, dialect=my_dialect)

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

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

可用的選項(csv.Dialect的屬性)及其功能如表6-3所示。

6-3:CSV語支選項
引數 說明
delimiter 用於分隔欄位的單字元字串。預設為“,”
lineterminator 用於寫操作的行結束符,預設為“\r\n”。讀操作將忽略此選項,它能認出跨平臺的行結束符
quotechar 用於帶有特殊字元(如分隔符)的欄位的引用符號。預設為“"”
quoting 引用約定。可選值包括csv. QUOTE_ALL(引用所有欄位)、csv.QUOTE_MINMAL(只引用帶有諸如分隔符之類特殊字元的欄位)、csv.QUOTE_NONNUMERIC以及 CSV.QUOTE_NON(不引用)。完整資訊請參考 Python的文件。預設為 QUOTE_MINIMAL
skipinitialspace 忽略分隔符後面的空白符。預設為 False
doublequote 如何處理欄位內的引用符號。如果為True,則雙寫。完整資訊及行為請參見線上文件
escapechar 用於對分隔符進行轉義的字串(如果 quoting被設定為 CSV.QUOTE_NONE的話)。預設禁用

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

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

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',
 'pet': None,
 'places_lived': ['United States', 'Spain', 'Germany'],
 'siblings': [{'age': 30, 'name': 'Scott', 'pets': ['Zeus', 'Zuko']},
  {'age': 38, 'name': 'Katie', 'pets': ['Sixes', 'Stache', 'Cisco']}]}

json.dumps則將Python物件轉換成JSON格式:#json格式的字串?

asjson =json.dumps(result)
asjson
'{"pet": null, "places_lived": ["United States", "Spain", "Germany"], "name": "Wes", "siblings": [{"name": "Scott", "age": 30, "pets": ["Zeus", "Zuko"]}, {"name": "Katie", "age": 38, "pets": ["Sixes", "Stache", "Cisco"]}]}'

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

siblings = pd.DataFrame(result['siblings'],columns=['age','name'])
siblings
age name
0 30 Scott
1 38 Katie

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