6.1 文本格式数据的读写

将表格型数据读取为DataFrame对象是pandas的重要特性。下图总结了部分实现该功能的函数,read_csv和read_table可能是后期我们使用最多的函数。

索引:可以将一或多个列作为返回的DataFrame,从文件或或用户处获得列名,或者没有列名。

类型推断和数据转换:包括用户自定义的值转换和自定义的缺失值符号列表。

日期时间解析:包括组合功能,也包括将分散在多个列上的日期和时间信息组合成结果中的单个列。

迭代:支持对大型文件的分块迭代

未清洗的数据问题:跳过行、页脚、注释以及其他次要数据,比如使用逗号分隔千位的数字。

一些数据载入函数,比如pandas.read_csv,会进行类型判断,因为列的数据类型并不是数据格式的一部分。那就意味着你不必指定哪一列是数值、整数、布尔值或字符串。其他的数据格式,比如HDF5、feather和msgpack在格式中已经存储了数据类型。

处理日期和其他自定义类型可能需要额外的努力。让我们从一个小型的逗号分隔文本文件(CSV)开始(文件下载地址在作者的github):

 In [2]: !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 shell的cat命令来打印文件的原始内容。如果你的操作系统是Windows,可以使用type来代替cat达到同样的效果。

由于这个文件是逗号分隔的,我们可以使用read_csv将它读入一个DataFrame:
In [6]: df = pd.read_csv('examples/ex1.csv')

In [7]: df
Out[7]: 
   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,并指定分隔符:
In [18]: df = pd.read_table('examples/ex1.csv',sep=',')

In [19]: df
Out[19]: 
   a   b   c   d message
0  1   2   3   4   hello
1  5   6   7   8   world
2  9  10  11  12     foo

有的文件并不包括表头行。考虑以下文件:
In [21]: !cat examples/ex2.csv
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo

要读取该文件,你需要选择一些选项。你可以允许pandas自动分配默认列名,也可以自己指定列名:
In [24]: pd.read_csv('examples/ex2.csv',header=None)
Out[24]: 
   0   1   2   3      4
0  1   2   3   4  hello
1  5   6   7   8  world
2  9  10  11  12    foo

In [25]: pd.read_csv('examples/ex2.csv',names=['a','b','c','d','message'])
Out[25]: 
   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的列为索引,或将‘message’传给参数index_col。
In [28]: names = ['a','b','c','d','message']

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

当你想要从多个列中形成一个分层索引,需要传入一个包含列序号或列名的列表:
In [31]: !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

In [32]: parsed = pd.read_csv('examples/csv_mindex.csv',index_col=['key1','key2'])

In [33]: parsed
Out[33]: 
           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

在某些情况下,一张表的分隔符并不是固定的,使用空白或其他方式来分隔字段。考虑如下文本文件:
In [38]: list(open('examples/ex3.txt'))
Out[38]: 
['            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+,因此我们可以得到:In [39]: result = pd.read_table('examples/ex3.txt',sep='\s+')

In [40]: result
Out[40]: 
            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来跳过第一行、第三行和第四行:
In [41]: !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
In [42]: pd.read_csv('examples/ex4.csv',skiprows=[0,2,3])
Out[42]: 
   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:
In [43]: !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
In [44]: result = pd.read_csv('examples/ex5.csv')

In [45]: result
Out[45]: 
  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

In [51]: pd.isnull(result)
Out[51]: 
   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选项可以传入一个列表或一组字符串来处理缺失值:
In [52]: result = pd.read_csv('examples/ex5.csv',na_values=['NULL'])

# 胭惜雨:这里的na_values是指把什么值指定为缺失值,如果把2设定为缺失值,那么2就会变成NaN。

In [53]: result
Out[53]: 
  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

在字典中,每列可以指定不同的缺失值标识:
In [66]: sentinels= {'message':['foo','NA'],'something':['two']}

In [67]: pd.read_csv('examples/ex5.csv',na_values=sentinels)
Out[67]: 
  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中常用的选项。

6.1.1 分块读入文本文件

当处理大型文件或找出正确的参数集来正确处理大文件时,你可能需要读入文件的一个小片段或者按小块遍历文件。

在尝试大文件之前,我们可以先对pandas的显示设置进行调整,使之更紧凑:

In [69]: pd.options.display.max_rows=10 #选项.展示.最大行数(10)

In [70]: result = pd.read_csv('examples/ex6')
In [74]: result
Out[74]: 
           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:
In [75]: pd.read_csv('examples/ex6.csv',nrows=5) # 前五行
Out[75]: 
        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作为每一块的行数:
In [77]: chunker = pd.read_csv('examples/ex6.csv',chunksize=1000)

In [78]: chunker
Out[78]: <pandas.io.parsers.TextFileReader at 0x7fdb910ed0d0>

read_csv返回的TextParser对象允许你根据chunksize遍历文件。例如,我们可以遍历ex6.csv,并对'key'列表聚合获得计数值:
In [79]: chunker = pd.read_csv('examples/ex6.csv',chunksize=1000)

In [80]: tot = pd.Series([])
<ipython-input-80-0dfcb0830956>:1: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
  tot = pd.Series([])

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

In [83]: tot = tot.sort_values(ascending=False)

In [84]: tot[:10]
Out[84]: 
E    336.0
X    327.0
L    315.0
M    309.0
K    303.0
Q    301.0
O    299.0
P    299.0
J    298.0
F    295.0
dtype: float64

TextParser 还具有get_chunk方法,允许你按照任意大小读取数据块。

6.1.2 将数据写入文本格式

数据可以到处为分隔的形式。让我们看下之前读取的CSV文件:

In [111]: data = pd.read_csv('examples/ex5.csv')

In [112]: data
Out[112]: 
  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方法,我们可以将数据导出为逗号分隔的文件:
In [114]: data.to_csv('examples/out.csv')

In [115]: !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时,控制台中打印的文本结果):
In [117]: 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

缺失值在输出时以空字符串出现。你也许想要用其他标识对缺失值进行标注:
In [120]: data.to_csv(sys.stdout,na_rep='demaxiya!')
,something,a,b,c,d,message
0,one,1,2,3.0,4,demaxiya!
1,two,5,6,demaxiya!,8,world
2,three,9,10,11.0,12,foo

如果没有其他选项被指定的话,行和列的标签都会被写入。不过两者也都可以禁止写入:
In [121]: 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

你也可以仅写入列的子集,并且按照你选择的顺序写入:
In [122]: 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方法:
In [123]: dates = pd.date_range('1/1/2000',periods=7)

In [125]: ts = pd.Series(np.arange(7),index=dates)

In [126]: ts.to_csv('examples/tseries.csv')

In [127]: !cat examples/tseries.csv
,0
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

6.1.3 使用分隔格式

绝大多数表型数据都可以使用函数pandas.read_table从硬盘中读取。然而,在某些情况下,一些手动操作可能是必不可少的。接收一个带有一行或多行错误的文件并不少见,read_table也无法解决这种情况。为了介绍一些基础工具,考虑如下的小型csv文件:

In [128]: !cat examples/ex7.csv
"a","b","c"
"1","2","3"
"1","2","3"

对于任何带有单字符分隔符的文件,你可以使用Python的内建csv模块。要使用它,需要将任一打开的文件或文件型对象传给csv.reader。
In [129]: import csv

In [130]: f = open('examples/ex7.csv')

In [131]: reader = csv.reader(f)

像遍历文件那样遍历reader会产生元组,元组的值为删除了引号的字符:
In [129]: import csv

In [130]: f = open('examples/ex7.csv')

In [131]: reader = csv.reader(f)

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

之后,你就可以自行做一些必要处理,以将数据整理为你需要的形式。让我们按部就班,首先将文件读取为行的列表:
In [133]: with open('examples/ex7.csv') as f:
     ...:     lines = list(csv.reader(f))

然后,我们将数据拆分为列名行和数据行:
In [134]: header ,values = lines[0],lines[1:]

再然后,我们使用字典推导式和表达式zip(*values)生成一个包含数据列的字典,字典中行转置成列:
In [158]: data_dict = {h: v for h,v in zip(header,zip(*values))}

In [159]: data_dict
Out[159]: {'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')}

CSV文件有多重不同风格。如需根据不同的分隔符、字符串引用约定或行终止符定义一种新的格式时,我们可以用csv.Dialect定义一个简单的子类:
In [5]: class my_dialect(csv.Dialect):
   ...:     lineterminator = '\n'
   ...:     delimiter = ';'
   ...:     quotechar = '"'
   ...:     quoting = csv.QUOTE_MINIMAL
   ...: 

In [6]: reader = csv.reaer(f,dialect=my_dialect)

我们也可以不比定义子类,直接将CSV方言参数传入csv.reader的关键字参数:
In [9]: reader = csv.reader(f,dialect='|')

对于具有更复杂或固定的多字符分隔符的文件,你将无法使用csv模块。在此类情况下,你将不得不使用字符串的split方法或正则表达式方法re.solit进行拆分和其他清理工作。

这里输入后会报错:TypeError: argument 1 must be an iterator,意思是参数1必须是迭代器。怎么改呢,不知道。致敬大神也没讲这块……

那就只能略过了。

需要手动写入被分隔的文件时,你可以使用csv.writer。这个函数接收一个已经打开的可写入文件对象以及和csv.reader相同的csv方言,格式如下:
In [11]: 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'))

6.1.4 JSON数据

JSON已经成为Web浏览器和其他应用间通过HTTP请求发送数据的标准格式。它是一种比CSV等表哥文本形式更为自由的数据格式:

请看下面的例子:

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

JSON非常接近有效的Python代码,除了它的控制null和一些其他的细微差别(例如不允许列表末尾的逗号)之外。基本类型是对象(字典)、数组(列表)、字符串、数字、布尔值和空值。对象中的所有键都必须是字符串。有几个Python库用于读写JSON数据。我将在这里使用json,因为它是内置在Python标准库中的。将JSON字符串转换为Python形式时,使用json.loads方法:
In [28]: import json

In [29]: result = json.loads(obj)

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

另一方面,json.dumps可以将Python对象转换回JSON:
In [32]: asjson = json.dumps(result)

你将自行决定如何将JSON对象或对象列表转换为DataFrame或其他数据结构。比较方便的方式是将字典构成的列表(之前是JSON对象)传入DataFrame构造函数,并选出数据字段的子集:
In [34]: si = pd.DataFrame(result['siblings'],columns=['name','age'])

In [35]: si
Out[35]: 
    name  age
0  Scott   30
1  Katie   38

pandas.read_json 可以自动将JSON数据集按照指定次序转换为Series或DataFrame。
In [36]: !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数组中的每个对象是表里的一行:
In [37]: data = pd.read_json('examples/example.json')

In [38]: data
Out[38]: 
   a  b  c
0  1  2  3
1  4  5  6
2  7  8  9

如果你需要从pandas中将数据导出为JSON,一种方法是对Series和DataFrame使用to_json方法:
In [39]: 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}}

In [40]: print(data.to_json(orient='records')) # 原始格式
[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]

6.1.5 XML和HTML:网络抓取

Python拥有很多对HTML和XML格式进行读取、写入数据的库,例如lxml、beautiful soup 和html5lib。尽管lxml是相对更快的库,但其他库可以更好地处理异常的HTML或XML文件。

pandas的内建函数read_html可以使用lxml和Beautiful Soup等库将HTML中的自动解析为DataFrame对象。为了展示这个功能,你首先必须安装read_html所使用的附加库:

conda install lxml

or

pip install beautifulsoup4 html5lib
panda.read_html函数有很多选项,但是默认情况下,它会搜索并尝试解析所有包含在<table>标签中的表格型数据,返回的结果是DataFrame对象的列表:

In [42]: tables = pd.read_html('examples/fdic_failed_bank_list.html')

In [43]: len(tables)
Out[43]: 1

In [44]: failures = tables[0]

In [46]: failures.head()
Out[46]: 
                      Bank Name             City  ST  ...                Acquiring Institution        Closing Date       Updated Date
0                   Allied Bank         Mulberry  AR  ...                         Today's Bank  September 23, 2016  November 17, 2016
1  The Woodbury Banking Company         Woodbury  GA  ...                          United Bank     August 19, 2016  November 17, 2016
2        First CornerStone Bank  King of Prussia  PA  ...  First-Citizens Bank & Trust Company         May 6, 2016  September 6, 2016
3            Trust Company Bank          Memphis  TN  ...           The Bank of Fayette County      April 29, 2016  September 6, 2016
4    North Milwaukee State Bank        Milwaukee  WI  ...  First-Citizens Bank & Trust Company      March 11, 2016      June 16, 2016

[5 rows x 7 columns]

因为failures有很多列,pandas在行内插入了换行符\。

你会在后续章节学到,此处我们着手一些数据清理和分析工作,比如计算每年银行倒闭的数量:
In [47]: close_timestamps = pd.to_datetime(failures['Closing Date'])

In [48]: close_timestamps.dt.year.value_counts()
Out[48]: 
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
 

to_datetime就是把数据转换为时间格式,例如2016-09-23这样的格式。

dt(就是datatime的属性).year(年).value(数据)_counts(计数):也就是说把时间属性设定为年,再把每年的数据累积计算。

6.1.5.1 使用lxml.objectify解析XML

XML是另一种常用的结构化数据格式,它使用元数据支持分层、嵌套数据。本书实际上也是从一系列大型XML文档中生成的。

之前,我展示了pandas.read_html函数,它使用lxml或beautiful soup从HTML中解析数据。XML和HTML结构类似,但是XML更通用。这里我将用一个例子展示如何使用lxml从更通用的XML格式解析数据。

In [16]: from lxml import objectify

In [21]: path = 'examples/mta_perf/Performance_MNR.xml'

In [22]: parsed = objectify.parse(open(path))

In [23]: root = parsed.getroot() # 对XML文件的根节点的引用

root.INDICATOR 返回一个生成器,可以产生一个<INDICATOR>XML元素。对于每条记录,我们将标签名称的字典(如YTD_ACTUAL)填充为数据值(不包括几个标签):
In [24]: data = [] # 存储所有数据的列表

In [25]: skip_fields = ['PARENT_SEQ','INDICATOR_SEQ','DESIRED_CHANGE','DECIMAL_PLACES']
# 这几个标签下的值不要

In [27]: 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 #如果不在,标签作为key值,这列的pyval作为value值加入字典 
    ...:     data.append(el_data) # 该组数据加入总体数据列表
    ...: 

In [29]: import pandas as pd

In [30]: perf = pd.DataFrame(data)

In [31]: perf.head()
Out[31]: 
            AGENCY_NAME                        INDICATOR_NAME  ... MONTHLY_TARGET  MONTHLY_ACTUAL
0  Metro-North Railroad  On-Time Performance (West of Hudson)  ...             95            96.9
1  Metro-North Railroad  On-Time Performance (West of Hudson)  ...             95              95
2  Metro-North Railroad  On-Time Performance (West of Hudson)  ...             95            96.9
3  Metro-North Railroad  On-Time Performance (West of Hudson)  ...             95            98.3
4  Metro-North Railroad  On-Time Performance (West of Hudson)  ...             95            95.8

[5 rows x 12 columns]

XML数据可以比例子更复杂。每个标签也可以包含元数据。考虑一个HTML连接标签,也是有效的XML:
In [32]: from io import StringIO

In [33]: tag = '<a href="http://www.google.com">Google</a>'

In [34]: root = objectify.parse(StringIO(tag)).getroot()

现在可以访问标签或链接文本中的任何字段(如href):
In [35]: root
Out[35]: <Element a at 0x7fd590e1ca00>

In [36]: root.get('href')
Out[36]: 'http://www.google.com'

In [37]: root.text
Out[37]: 'Google'

关于元数据的解读,可以看知乎上的回答,反正我目前是没懂。

6.2 二进制格式

使用Python内建的pickle序列化模块进行二进制格式操作是存储数据(也称为序列化)最高效、最方便的方式之一。pandas对象拥有一个to_pickle方法可以将数据以pickle格式写入硬盘:

In [39]: frame = pd.read_csv('examples/ex1.csv')

In [40]: frame
Out[40]: 
   a   b   c   d message
0  1   2   3   4   hello
1  5   6   7   8   world
2  9  10  11  12     foo

In [41]: frame.to_pickle('examples/frame_pickle')

你可以直接使用内建的pickle读取文件中‘pickle化’对象,或更方便的使用pandas.read_pickle做上述操作:
In [43]: pd.read_pickle('examples/frame_pickle')
Out[43]: 
   a   b   c   d message
0  1   2   3   4   hello
1  5   6   7   8   world
2  9  10  11  12     foo

注意:pickle仅被推荐作为短期的存储格式。问题在于pickle很难确保格式的长期有效性:一个今天被pickle化的对象可能明天会因为库的新版本而无法反序列化。我们尽可能保持向后兼容性,但在将来的某个时候,可能有必要“打破”pickle格式。

pandas内建支持其他两个二进制格式:HDF5和MessagePack。我们会在下一节给出一些HDF5示例,但我推荐你尝试不同的文件格式去看看它们的速度以及它们如何用你的分析。pandas和Numpy其他的存储格式包括:
bcolz:基于Bcolz压缩库的可压缩列表二进制格式。

Feather:跨语言列式文件格式。father使用Apache箭头列式存储器格式。

6.2.1 使用HDF5格式

HDF5用于存储大量的科学数组数据。它以C库的形式提供。并且提供许多其他语言的结构,包括Java、Julia、MATLAB和python。HDF5中的HDF代表分层数据格式。每个HDF5文件可以存储多个数据集并且支持元数据。与更简单的格式相比,HDF5支持多种压缩模式的即时压缩,使得重复模式的数据可以更高效地存储。HDF5适用于处理不适合在内存中存储的超大型数据,可以使你高效地读写大型数据的一小块。

尽管可以通过PyTables或h5py等库直接访问HDF5文件,但pandas提供了一个高阶的接口,可以简化series和dataframe的存储。HDF5Store类像字典一样工作并处理低级别细节:

In [45]: import numpy as np

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

In [47]: store = pd.HDFStore('mydata.h5')

In [48]: store['obj1'] = frame

In [49]: store['obj1_col'] = frame['a']

In [50]: store
Out[50]: 
<class 'pandas.io.pytables.HDFStore'>
File path: mydata.h5

包含在HDF5文件中的对象可以使用相同的字典型API进行检索:
In [51]: store['obj1']
Out[51]: 
           a
0  -0.199672
1   0.751586
2  -0.146054
3   0.576682
4   0.785038
..       ...
95  1.528501
96 -0.816099
97 -1.868995
98 -0.294085
99  1.707586

[100 rows x 1 columns]

HDFStore支持两种存储模式,‘fixed’和‘table’。后者速度更慢,但支持一种特殊语法的查询操作:
In [54]: store.put('obj2',frame,format='table')

In [56]: store.select('obj2',where=['index >= 10 and index <= 15'])
Out[56]: 
           a
10  0.502254
11  1.045340
12  0.903673
13 -1.112728
14  0.937457
15 -0.532831

put是store['obj2']=frame方法的显式版本,但允许我们设置其他选项,如存储格式。

pandas.read_hdf函数是这些工具的快捷方式:
In [58]: frame.to_hdf('mydata.h5','obj3',format='table')
In [63]: pd.read_hdf('mydata.h5','obj3',where=['index < 5'])

报错:ValueError: The file ‘mydata.h5’ is already opened, but not in read-only mode (as requested).

原因未知,我仰赖的B站up主致敬大神也跳过了这部分。我觉得冥冥之中,我们都蠢到一块了。

如果你处理存储在远程服务器上的数据时,比如Amazon S3或HDFS,使用其他专门为分布式存储而设计的二进制格式更为合适,比如Apache parquet。parquet和其他类似的存储格式仍在发展中,因此本书中我并没有写相关内容。

如果你是在本地处理大量数据,我推荐你尝试PyTable和H5py,看看它们是否符合你的需求。由于很多数据分析的困难在于I/O密集(而不是CPU密集),使用想HDF5这样的工具可以大大加速你的应用。

HDF5并不是数据库,它是一种适合一次写入多次读取的数据集。尽管数据可以在任何时间添加到文件中,但如果多个写入者持续写入,文件可能会损坏。

6.2.2 读取Microsoft Excel文件

pandas也支持通过ExcelFile类或pandas.read_excel函数来存储存储在Excel 2003或更高版本文件中的表格型数据。这些工具内部是使用附加包xlrd和openpyx1来分别读取XLS和XLSX文件的。你可能需要使用pip或conda手动安装这些工具。

使用ExcelFile时,通过xls或xlsx的路径传入,生成一个实例:
In [67]: xlsx = pd.ExcelFile('examples/ex1.xlsx')

存储在表中的数据可以通过pandas.read_excel读取到DataFrame中:
In [70]: pd.read_excel(xlsx,'Sheet1')
Out[70]: 
   Unnamed: 0  a   b   c   d message
0           0  1   2   3   4   hello
1           1  5   6   7   8   world
2           2  9  10  11  12     foo

如果你读取的是含有多个表的文件,生成ExcelFile更快,但你也可以更简洁地将文件名传入pandas.read_excel。

In [71]:  frame = pd.read_excel('examples/ex1.xlsx','Sheet1')

In [72]: frame
Out[72]: 
   Unnamed: 0  a   b   c   d message
0           0  1   2   3   4   hello
1           1  5   6   7   8   world
2           2  9  10  11  12     foo

如需将pandas数据写入到Excel格式中,你必须先生成一个ExcelWriter,然后使用pandas对象的to_excel方法将数据写入:
In [73]: writer = pd.ExcelWriter('examples/ex2.xlsx')

In [74]: frame.to_excel(writer,'Sheet1')

In [75]: writer.save()

你也可以将文件路径传给to_excel,避免直接调用ExcelWriter:
In [76]: frame.to_excel('examples/ex2.xlsx')

6.3 与Web API交互

很多网站都有公开API,通过JSON或其他格式提供数据服务。有多种方式可以利用Python来访问API:我推荐的简单易用方式是使用requests包。

要获取GitHub上最新的30条关于pandas的问题,我们可以使用附加库requests发送一个HTTP GET请求:

In [98]: import requests

In [99]: url = 'https://api.github.com/repos/pandas-dev/pandas/issues'

In [100]: resp = requests.get(url)

In [101]: resp
Out[101]: <Response [200]>

Response(响应)对象的JSON方法将返回一个包含解析为本地Python对象的JSON字典:
In [102]: data = resp.json()

In [103]: data[0]['title']
Out[103]: 'DOC: Update CSS alignment for main documentation page'

data中每个元素都是一个包含Github问题页面上的所有数据的字典(注释除外)。我们可以将data直接传给DataFrame,并提取感兴趣的字段:
In [104]: issues = pd.DataFrame(data,columns=['number','title','labels','state'])

In [105]: issues
Out[105]: 
    number                                              title                                             labels state
0    40650  DOC: Update CSS alignment for main documentati...  [{'id': 134699, 'node_id': 'MDU6TGFiZWwxMzQ2OT...  open
1    40649                                      Excel 2blanks                                                 []  open
2    40647  ENH:  df.to_sql lacks a parameter if_not_exist...  [{'id': 76812, 'node_id': 'MDU6TGFiZWw3NjgxMg=...  open
3    40646                                               BUG:  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
4    40645  DOC: Styling guide lacks links to the referenc...  [{'id': 134699, 'node_id': 'MDU6TGFiZWwxMzQ2OT...  open
5    40644      DOC: Change user guide style notebook to reST                                                 []  open
6    40643                   TYP: fix ignores in core.groupby  [{'id': 233160, 'node_id': 'MDU6TGFiZWwyMzMxNj...  open
7    40642                                 TYP: IndexOpsMixin  [{'id': 1280988427, 'node_id': 'MDU6TGFiZWwxMj...  open
8    40640      Consolidate into contributing_environment.rst  [{'id': 134699, 'node_id': 'MDU6TGFiZWwxMzQ2OT...  open
9    40638      BUG: PandasArray.to_numpy mishandles na_value  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
10   40637                DOC: release notes for 1.3.0 Styler                                                 []  open
11   40636               add deltalake to eco system doc page                                                 []  open
12   40631                                               BUG:  [{'id': 49254273, 'node_id': 'MDU6TGFiZWw0OTI1...  open
13   40630  BUG: series.to_numpy does not work well with p...  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
14   40629  Detect Parsing errors in read_csv first row wi...  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
15   40628                           ENH: New boundary inputs                                                 []  open
16   40625  REF: SingleBlockManager dont subclass BlockMan...  [{'id': 49094459, 'node_id': 'MDU6TGFiZWw0OTA5...  open
17   40624  BUG: Arithmetic operations on 0 length Extensi...  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
18   40622  ENH: How about "cannot reindex from an index w...  [{'id': 134699, 'node_id': 'MDU6TGFiZWwxMzQ2OT...  open
19   40621    BUG: groupby any/all raising on extension types  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
20   40620          PERF: cache_readonly for Block properties  [{'id': 49094459, 'node_id': 'MDU6TGFiZWw0OTA5...  open
21   40616      DOC: _array_strptime_with_fallback method def                                                 []  open
22   40614          REF: remove DatetimeBlock, TimeDeltaBlock                                                 []  open
23   40613  Add capability to upsample with .resample().ff...                                                 []  open
24   40612                                   TYP: get_indexer  [{'id': 1280988427, 'node_id': 'MDU6TGFiZWwxMj...  open
25   40611                    Can't install Pandas on Mac M1   [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
26   40608  BUG: join/merge of DataFrame does not keep ord...  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open
27   40607                                               QST:  [{'id': 2365504893, 'node_id': 'MDU6TGFiZWwyMz...  open
28   40606         DEPR: deprecate setting Categorical._codes  [{'id': 78527356, 'node_id': 'MDU6TGFiZWw3ODUy...  open
29   40605  BUG: color list passed in as list/array/series...  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ=...  open

通过一些复杂操作,你可以创建一些更高阶的接口来访问常用的web API,以返回DataFrame对象以便于分析。

6.4 与数据库交互

在业务场景中,大部分数据并不是存储在文本或excel文件中的。基于SQL的关系型数据库(例如SQL Server、MySQL)使用广泛,很多小众数据库也变得越发流行。数据库的选择通常取决于性能、数据完整性以及应用的可伸缩性需求。

从SQL中将数据读取为DataFrame是相当简单直接的,pandas有多个函数可以简化这个过程。作为例子,我将使用Python内建的sqlite3驱动来生成一个SQlite数据库:

In [106]: import sqlite3

In [107]: quert="""
     ...: CREATE TABLE test
     ...: (a VARCHAR(20),b VARCHAR(20),
     ...: c REAL, D INTEGER);"""

In [108]: con = sqlite3.connect('mydata.sqlite')

In [110]: con.execute(quert)
Out[110]: <sqlite3.Cursor at 0x7fd590f64ce0>

In [111]: con.commit()

再插入几行数据:
In [112]: data = [('a','b',1.26,6),('c','d',2.6,3),('e','f',1.7,5)]

In [113]: 

In [113]: stmt = "INSERT INTO test VALUES(?,?,?,?)"

In [114]: con.executemany(stmt,data)
Out[114]: <sqlite3.Cursor at 0x7fd5a02da260>

In [115]: con.commit()

当从数据库的表中选择数据时,大部分的Python的SQL驱动(pyODBC\psycopg2\MYSQLDB\pymssql等)返回的是元组的列表:
In [116]: cursor = con.execute('select * from test')

In [117]: rows = cursor.fetchall()

In [118]: rows
Out[118]: [('a', 'b', 1.26, 6), ('c', 'd', 2.6, 3), ('e', 'f', 1.7, 5)]

你可以将元组的列表传给DataFrame构造函数,但你还需要包含在游标的description属性中的列名:
In [119]: cursor.description
Out[119]: 
(('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))

In [121]: pd.DataFrame(rows,columns=[x[0] for x in cursor.description])
Out[121]: 
   a  b     c  D
0  a  b  1.26  6
1  c  d  2.60  3
2  e  f  1.70  5

You与内容较多,你肯定不想每次查询数据库都重复同样多的步骤。SQLAlchemy项目是一个流行的Python SQL工作包,抽象去除了SQL数据库之间的很多常见差异。pandas有一个read_sql函数允许你从通用的SQLAlchemy链接中轻松地读取数据。这里,我将使用SQLAlchemy连接到相同的SQLite数据库,并从之前创建的表中读取数据:
In [122]: import sqlalchemy as sqla

In [123]: db = sqla.create_engine('sqlite:///mydata.sqlite')

In [124]: pd.read_sql('select * from test',db)
Out[124]: 
   a  b     c  D
0  a  b  1.26  6
1  c  d  2.60  3
2  e  f  1.70  5

本章结束~撒花~

胭惜雨

2021年03月27日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据