pandas提供了一些用于将表格型数据读取为DataFrame对象的函数。下表对其进行了总结,其中read_csvread_table可能是今后用的最多的。

以上函数的选项可以划分为以下几个大类:

  • 索引:将一个或多个列当作返回的DataFrame处理,以及是否从文件、用户获取列名;
  • 类型推断和数据转换:包括用户定义值的转换、和自定义的缺失值标记列表等;
  • 日期解析:包括组合功能,比如将分散在多个列中的日期时间信息组合成结果中的单个列;
  • 迭代:支持对大文件进行逐块迭代;
  • 不规整数据问题:跳过一些行、页脚、注释或其他一些不重要东西(比如由成千上万个都好隔开的数值数据)。

下面将简单的介绍这些函数将文本数据转换为DataFrame时所用的技术。由于工作中实际碰到的数据可能十分混乱,一些数据加载函数(尤其是read_csv)的选项逐渐变得复杂起来。pandas文档有这些参数的例子,如果在阅读某个文件感觉很难,可以通过相似的足够多的例子找到正确的参数。

其中一些函数,比如pandas.read_csv,有类型推断功能,因为列数据的类型不属于数据类型。也就是说,你不需要指定列的类型到底是数值、整数、布尔值,还是字符串。其他的数据格式,如:HDF5,Feather和msgpack,会在格式中存储数据类型。日期和其他自定义类型的处理需要多花点工夫才行。

一、CSV文本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd
print("-----read_csv-----")
df = pd.read_csv("examples/ex1.csv")
print(df)

# 还可以使用read_table,并指定分隔符
print("-----read_table-----")
print(pd.read_table("examples/ex1.csv", sep=","))

# 结果
-----read_csv-----
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-----
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

并不是所有文件都有标题行(人为指定喽),我们这样读入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
···
print("-----read_csv默认列名-----")
print(pd.read_csv("examples/ex2.csv", header=None))
print("-----read_csv指定列名-----")
print(pd.read_csv("examples/ex2.csv", names=['a', 'b', 'c', 'd', 'message']))

# 结果
-----read_csv默认列名-----
0 1 2 3 4
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
-----read_csv指定列名-----
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”:

1
2
3
4
5
6
7
8
9
10
11
12
···
names = ['a', 'b', 'c', 'd', 'message']
print("-----指定names-----")
print(pd.read_csv("examples/ex2.csv", names = names, index_col="message"))

# 结果
-----指定names-----
a b c d
message
hello 1 2 3 4
world 5 6 7 8
foo 9 10 11 12

如果希望将多个列做成一个层次化索引,只需要传入由列编号或列名组成的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
···
# 将多个列做成一个层次化索引
print("-----csv_mindex.csv-----")
print(pd.read_csv("examples/csv_mindex.csv"))
print("层次化索引")
print(pd.read_csv('examples/csv_mindex.csv', index_col=["key1", "key2"]))

# 结果
key1 key2 value1 value2
0 one a 1 2
1 one b 3 4
2 one c 5 6
3 one d 7 8
4 two a 9 10
5 two b 11 12
6 two c 13 14
7 two d 15 16
层次化索引
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

有些表格可能不是用固定的分隔符去分隔字段的(比如空白符和其他模式):

1
2
3
4
5
6
7
8
9
10
···
print("-----ex3.txt-----")
print(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+,于是有:

1
2
3
4
5
6
7
8
9
10
11
12
···
print("规整")
result = pd.read_table("examples/ex3.txt", sep="\s+") # 匹配空白字符1次或多次
print(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的索引。

这些解析器函数还有许多参数可以帮助你处理各种各样的异形文件格式,比如说,你可以用skiprows跳过文件的第一行、第三行和第四行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
···
print("原")
print(pd.read_csv("examples/ex4.csv"))
print("规整")
print(pd.read_csv("examples/ex4.csv", skiprows=[0, 2, 3]))

# 结果

# hey!
a b c d message
# just wanted to make things more difficult for... NaN NaN NaN NaN
# who reads CSV files with computers anyway? NaN NaN NaN
1 2 3 4 hello
5 6 7 8 world
9 10 11 12 foo
规整
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
···
print("原")
print(pd.read_csv("examples/ex5.csv"))
print("规整")
print(pd.isnull(pd.read_csv("examples/ex5.csv"))) # 布尔
print("na_values") # na_values可以用一个列表或集合的字符串表示缺失值
print(pd.read_csv('examples/ex5.csv', na_values=['NULL']))
print("字典") # 字典的各列可以使用不同的NA标记值
sentinels = {'message': ['foo', 'NA'], 'something': ['two']}
print(pd.read_csv('examples/ex5.csv', na_values=sentinels))

# 结果

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
规整
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
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
字典
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常用的选项:

1.1 逐块读取文本文件

在处理很大的文件时,或找出大文件中的参数集以便于后续处理时,可能只想读取文件的一小部分或逐块对文件进行迭代。

在看大文件之间,先设置pandas显示地更加紧凑一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
pd.options.display.max_rows = 10
result = pd.read_csv("example/ex6.csv")
print("----result-----")
print(result)

# 结果
-----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 x 5 columns]

如果只想读取几行(避免读取整个文件),通过nrows进行指定即可:

1
2
3
4
5
6
7
8
9
10
11
12
···
print("-----读取指定行数-----)
print(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(行数):

1
2
3
4
5
6
7
8
···
print("-----指定行数-----")
chunker = pf.read_csv("examples/ex6.csv", chunksize = 1000)
print(chunker)

# 结果
-----逐块读取文件-----
<pandas.io.parsers.readers.TextFileReader object at 0x7f81f28a5350>

read_csv()所返回是TextParser对象可以根据chunksize对文件进行逐块迭代。比如,可以通过迭代处理ex6.csv,将值计数聚合到key列中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
···
print("-----通过地迭代将值计数聚合到key列中-----")
tot = pd.Series([])
for piece in chunker:
tot = tot.add(piece["key"].value_counts(), fill_value=0)
tot = tot.sort_values(ascending=False)
print(tot[:10])

# 结果
-----通过地迭代将值计数聚合到key列中-----
E 368.0
X 364.0
L 346.0
O 343.0
Q 340.0
M 338.0
J 337.0
F 335.0
K 334.0
H 330.0
dtype: float64

TextParser还有一个get_chunk方法,它使你可以读取任意大小的块。

1.2 将数据读写到文本格式

数据可以被输出为分隔符格式的文本,利用DataFrame的to_csv方法,可以间数据写到一个以逗号分隔的文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd

data = pd.read_csv("examples/ex5.csv")
print("-----data-----")
print(data)

data.to_csv("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, 所以仅仅是打印出文本效果而已):

1
2
3
4
5
6
7
8
9
10
···
# 其他分割符
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

缺失值在输出结果中会被表示为空字符串。你可能希望将其表示为别的标记值:

1
2
3
4
5
6
7
8
9
10
···

# 替换缺失值
data.to_csv(sys.stdout, na_rep="NULL", sep = "|")

# 结果
|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

如果没有设置其它选项,则会写出行和列的标签。当然。也可以被禁用:

1
2
3
4
5
6
7
···
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

也可以只写出一部分的列,并指定顺序:

1
2
3
4
5
6
7
8
···
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方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
···
dates = pd.date_range("1/1/2000", periods=7)
ts = pd.Series(np.arange(7), index=dates)
ts.to_csv("examples/tseries.csv")

# 结果
Unnamed: 0 0
0 2000-01-01 0
1 2000-01-02 1
2 2000-01-03 2
3 2000-01-04 3
4 2000-01-05 4
5 2000-01-06 5
6 2000-01-07 6

1.3 处理分隔符格式

大部分存储到磁盘上的表格型数据都能使用Pandas.read_table进行加载。然而,有时还是需要做一些简单手工处理。由于接收到含有畸形行的文件而使read_table出毛病的情况并不少见。为了说明这些基本工具,看看下面这些简单的CSV文件:

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

对于任何单字符分隔符文件,可以直接使用Python内置的csv模块。将任意已打开的文件或文件型的对象传给csv.reader,对这个reader进行迭代将会为每行产生一个元组(并移除了所有的引号):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pandas as pd
import csv

f = open("examples/ex7.csv")
reader = csv.reader(f)

for line in reader:
print(line)


# 结果
['a', 'b', 'c']
['1', '2', '3']
['1', '2', '3']

现在,为了使数据格式合乎要求,你需要对其做一些整理工作。我们一步一步来做。首先,读取文件到一个多行的列表中

1
2
3
4
5
6
7
8
9
imoprt pandas as pd
imoprt numpy as np
with open("examples/ex7.csv") as f:
lines = list(csv.reader(f))
print("-----lines-----")
print(lines)

# 结果
[['a', 'b', 'c'], ['1', '2', '3'], ['1', '2', '3']]

然后,将这些行分为标题行和数据行

1
2
3
4
5
6
···
header, values = lines[0], lines[1:]
print(header, values)

# 结果
['a', 'b', 'c'] [['1', '2', '3'], ['1', '2', '3']]

然后,可以用字典构造式和zip(*values),后者将行转置为列,创建数据列的字典

1
2
3
4
5
6
···
data_dict = {h: v for h, v in zip(header, zip(*values))}
print(data_dict)

# 结果
{'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')}

CSV文件的形式有很多。只需定义csv.Dialect的一个子类即可定义出新格式(如专门的分隔符、字符串引用约定、行结束符等):

1
2
3
4
5
6
class my_dialect(csv.Dialect):
lineterminator = '\n'
delimiter = ';'
quotechar = '"'
quoting = csv.QUOTE_MINIMAL
reader = csv.reader(f, dialect=my_dialect)

各个CSV语支的参数也可以用关键字的形式提供给csv.reader,而无需定义子类:

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

可用的选项(csv.Dialect的属性)及其功能如表所示。

注意:对于那些使用复杂分隔符或多字符分隔符的文件,csv模块就无能为力了。这种情况下,你就只能使用字符串的split方法或正则表达式方法re.split进行行拆分和其他整理工作了

要手工输出分隔符文件,你可以使用csv.writer。它接受一个已打开且可写的文件对象以及跟csv.reader相同的那些语支和格式化选项:

1
2
3
4
5
6
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已经成为通过HTTP请求在Web浏览器和其他应用程序之间发送数据的标准格式之一。它是一种比表格型文本格式(如CSV)灵活的多的数据格式。下面是一个例子:

1
2
3
4
5
6
7
8
9
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"]}]
}
"""

除其空值nul和一些其他的细微差别(如列表末尾不允许存在多余的逗号)之外,JSON非常接近于有效的Python代码。基本类型有对象(字典)、数组(列表)、字符串、数值、布尔值以及null。对象中所有的键都必须是字符串。许多Python库都都可以读写JSON数据。下面将使用json,因为他是构建于Python标准库中的。通过json.loads即可将JSON字符串转换为Python形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import json

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"]}]
}
"""

result = json.loads(obj)
print("-----result-----")
print(result)

# 结果
-----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格式:

1
2
3
4
5
6
7
···
asjson = json.dumps(result)
print(asjson)

# 结果,注意None和null的区别
-----json.dumps()-----
{"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"]}]}

将(一个或一组)JSON对象转换成DataFrame或其他便于分析的数据结构,最简单的方法是:向DataFrame构造器传入一个字典的列表(就是原先的JSON对象),并选取数据字典的子集:

1
2
3
4
5
6
7
8
···
siblings = pd.DataFrame(result["siblings"], columns=["name", age])
print(siblings)

# 结果
name age
0 Scott 30
1 Katie 38

pandas.read_json可以自动将特别格式的JSON数据集转换成Series或DataFrame。例如:

1
2
3
[{"a": 1, "b": 2, "c": 3},
{"a": 4, "b": 5, "c": 6},
{"a": 7, "b": 8, "c": 9}]

pandas.read_json的默认选项假设JSON数组中的每个对象是表格中的一行:

1
2
3
4
5
6
7
8
9
data = pd.read_json("examples/example.json")
print(data)

# 结果
-----read_json()-----
a b c
0 1 2 3
1 4 5 6
2 7 8 9

将数据从pandas输出到JSON,可以使用to_json方法:

1
2
3
4
5
6
7
8
···
print(data.to_json())
print(data.to_json(orient="records"))

# 结果
-----to_json-----
{"a":{"0":1,"1":4,"2":7},"b":{"0":2,"1":5,"2":8},"c":{"0":3,"1":6,"2":9}}
[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]

三、XML和HTML:Web信息收集

3.1 HTML文件

Python有许多可以读写常见的HTML和XML格式数据的库,包括lxml、Beautiful Soup和html5lib。lxml的速度比较快,但是其他的库处理有误的HTML或XML文件更好。

pandas有一个内置的功能,read_html,它可以使用lxml和Beautiful Soup自动将HTML文件中的表格解析为DataFrame对象。下面对该功能进行演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pandas as pd

tables = pd.read_html("examples/fdic_failed_bank_list.html")
print(len(tables))
failures = tables[0]
print(failures.head())

# 结果
1
Bank Name City ST CERT \
0 Allied Bank Mulberry AR 91
1 The Woodbury Banking Company Woodbury GA 11297
2 First CornerStone Bank King of Prussia PA 35312
3 Trust Company Bank Memphis TN 9956
4 North Milwaukee State Bank Milwaukee WI 20364
Acquiring Institution Closing Date Updated Date
0 Today's Bank September 23, 2016 November 17, 2016
1 United Bank August 19, 2016 November 17, 2016
2 First-Citizens Bank & Trust Company May 6, 2016 September 6, 2016
3 The Bank of Fayette County April 29, 2016 September 6, 2016
4 First-Citizens Bank & Trust Company March 11, 2016 June 16, 2016

[5 rows x 7 columns]

由于failures有许多列,pandas出入了一个换行符\。这里,可以做一些数据清洗和分析,比如按照年份计算倒闭的银行数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 ···
close_timestamps = pd.to_datetime(failures['Closing Date'])
print(close_timestamps.dt.year.values_counts())

# 结果
Closing Date
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: count, dtype: int64

3.2 XML文件

XML(Extensible Markup Language)是另一种常见的支持分层、嵌套数据以及元数据的结构化数据格式。XML和HTML的结构很相似,但XML更为通用。下面的例子演示了如何利用lxml从XML格式解析数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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文件的根节点的引用:

1
2
3
4
5
from lxml import objectify

path = "datasets/mta_perf/Performance_MNR.xml"
parsed = objectify.parse(open(path))
root = parsed.getroot()

root.INDICTOR返回一个用于产生各个XML元素的生成器。对于每条记录,我们可以用标记名(如YTD_ACTUAL)和数据值填充一个字典(排除几个标记):

1
2
3
4
5
6
7
8
9
10
11
12
···
data = []

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

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

最后,将这组字典转换为一个DataFrame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
···
perf = pd.DataFrame(data)

print(perf.head())

# 结果
AGENCY_NAME ... MONTHLY_ACTUAL
0 Metro-North Railroad ... 96.9
1 Metro-North Railroad ... 95.0
2 Metro-North Railroad ... 96.9
3 Metro-North Railroad ... 98.3
4 Metro-North Railroad ... 95.8

[5 rows x 12 columns]

XML数据可以比本例复杂得多。每个标记都可以有元数据。