六、数据加载、存储和文件格式

读取数据并使其可访问(通常称为数据加载)是使用本书中大多数工具的必要第一步。术语解析有时也用于描述加载文本数据并将其解释为表格和不同数据类型。我将专注于使用 pandas 进行数据输入和输出,尽管其他库中有许多工具可帮助读取和写入各种格式的数据。

输入和输出通常分为几个主要类别:读取文本文件和其他更高效的磁盘格式、从数据库加载数据以及与网络源(如 Web API)交互。

6.1 以文本格式读取和写入数据

pandas 提供了许多函数,用于将表格数据读取为 DataFrame 对象。表 6.1 总结了其中一些;pandas.read_csv是本书中最常用的之一。我们将在二进制数据格式中稍后查看二进制数据格式。

表 6.1:pandas 中的文本和二进制数据加载函数

我将概述这些函数的机制,这些函数旨在将文本数据转换为 DataFrame。这些函数的可选参数可能属于几个类别:

索引

可以将一个或多个列视为返回的 DataFrame,并确定是否从文件、您提供的参数或根本不获取列名。

类型推断和数据转换

包括用户定义的值转换和自定义缺失值标记列表。

日期和时间解析

包括一种组合能力,包括将分布在多个列中的日期和时间信息组合成结果中的单个列。

迭代

支持迭代处理非常大文件的块。

不干净的数据问题

包括跳过行或页脚、注释或其他像数字数据以逗号分隔的小事物。

由于现实世界中的数据可能会很混乱,一些数据加载函数(特别是pandas.read_csv)随着时间的推移积累了很长的可选参数列表。对于不同参数的数量感到不知所措是正常的(pandas.read_csv大约有 50 个)。在线 pandas 文档有许多关于每个参数如何工作的示例,因此如果您在阅读特定文件时感到困惑,可能会有足够相似的示例帮助您找到正确的参数。

其中一些函数执行类型推断,因为列数据类型不是数据格式的一部分。这意味着您不一定需要指定哪些列是数字、整数、布尔值或字符串。其他数据格式,如 HDF5、ORC 和 Parquet,将数据类型信息嵌入到格式中。

处理日期和其他自定义类型可能需要额外的努力。

让我们从一个小的逗号分隔值(CSV)文本文件开始:

In [10]: !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,可以在 Windows 终端(或命令行)中使用type代替cat来实现相同的效果。

由于这是逗号分隔的,我们可以使用pandas.read_csv将其读入 DataFrame:

In [11]: df = pd.read_csv("examples/ex1.csv")

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

文件不总是有标题行。考虑这个文件:

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

要读取此文件,您有几个选项。您可以允许 pandas 分配默认列名,或者您可以自己指定名称:

In [14]: pd.read_csv("examples/ex2.csv", header=None)
Out[14]: 
 0   1   2   3      4
0  1   2   3   4  hello
1  5   6   7   8  world
2  9  10  11  12    foo

In [15]: pd.read_csv("examples/ex2.csv", names=["a", "b", "c", "d", "message"])
Out[15]: 
 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 的索引。您可以使用index_col参数指示您希望在索引 4 处或使用名称"message"

In [16]: names = ["a", "b", "c", "d", "message"]

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

如果要从多个列创建分层索引(在 Ch 8.1:分层索引中讨论),请传递列编号或名称的列表:

In [18]: !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 [19]: parsed = pd.read_csv("examples/csv_mindex.csv",
 ....:                      index_col=["key1", "key2"])

In [20]: parsed
Out[20]: 
 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 [21]: !cat examples/ex3.txt
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

虽然您可以手动进行一些数据处理,但这里的字段是由可变数量的空格分隔的。在这些情况下,您可以将正则表达式作为pandas.read_csv的分隔符传递。这可以通过正则表达式\s+表示,因此我们有:

In [22]: result = pd.read_csv("examples/ex3.txt", sep="\s+")

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

由于列名比数据行数少一个,pandas.read_csv推断在这种特殊情况下第一列应该是 DataFrame 的索引。

文件解析函数有许多额外的参数,可帮助您处理发生的各种异常文件格式(请参见表 6.2 中的部分列表)。例如,您可以使用skiprows跳过文件的第一、第三和第四行:

In [24]: !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 [25]: pd.read_csv("examples/ex4.csv", skiprows=[0, 2, 3])
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

处理缺失值是文件读取过程中重要且经常微妙的部分。缺失数据通常要么不存在(空字符串),要么由某个标记(占位符)值标记。默认情况下,pandas 使用一组常见的标记,例如NANULL

In [26]: !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 [27]: result = pd.read_csv("examples/ex5.csv")

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

请记住,pandas 将缺失值输出为NaN,因此在result中有两个空值或缺失值:

In [29]: pd.isna(result)
Out[29]: 
 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 [30]: result = pd.read_csv("examples/ex5.csv", na_values=["NULL"])

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

pandas.read_csv有许多默认的 NA 值表示列表,但这些默认值可以通过keep_default_na选项禁用:

In [32]: result2 = pd.read_csv("examples/ex5.csv", keep_default_na=False)

In [33]: result2
Out[33]: 
 something  a   b   c   d message
0       one  1   2   3   4      NA
1       two  5   6       8   world
2     three  9  10  11  12     foo

In [34]: result2.isna()
Out[34]: 
 something      a      b      c      d  message
0      False  False  False  False  False    False
1      False  False  False  False  False    False
2      False  False  False  False  False    False

In [35]: result3 = pd.read_csv("examples/ex5.csv", keep_default_na=False,
 ....:                       na_values=["NA"])

In [36]: result3
Out[36]: 
 something  a   b   c   d message
0       one  1   2   3   4     NaN
1       two  5   6       8   world
2     three  9  10  11  12     foo

In [37]: result3.isna()
Out[37]: 
 something      a      b      c      d  message
0      False  False  False  False  False     True
1      False  False  False  False  False    False
2      False  False  False  False  False    False

可以在字典中为每列指定不同的 NA 标记:

In [38]: sentinels = {"message": ["foo", "NA"], "something": ["two"]}

In [39]: pd.read_csv("examples/ex5.csv", na_values=sentinels,
 ....:             keep_default_na=False)
Out[39]: 
 something  a   b   c   d message
0       one  1   2   3   4     NaN
1       NaN  5   6       8   world
2     three  9  10  11  12     NaN

表 6.2 列出了pandas.read_csv中一些经常使用的选项。

表 6.2:一些pandas.read_csv函数参数

分块读取文本文件

在处理非常大的文件或找出正确的参数集以正确处理大文件时,您可能只想读取文件的一小部分或迭代文件的较小块。

在查看大文件之前,我们将 pandas 显示设置更加紧凑:

In [40]: pd.options.display.max_rows = 10

现在我们有:

In [41]: result = pd.read_csv("examples/ex6.csv")

In [42]: result
Out[42]: 
 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 [43]: pd.read_csv("examples/ex6.csv", nrows=5)
Out[43]: 
 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 [44]: chunker = pd.read_csv("examples/ex6.csv", chunksize=1000)

In [45]: type(chunker)
Out[45]: pandas.io.parsers.readers.TextFileReader

pandas.read_csv返回的TextFileReader对象允许您根据chunksize迭代文件的部分。例如,我们可以迭代ex6.csv,聚合"key"列中的值计数,如下所示:

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

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

tot = tot.sort_values(ascending=False)

然后我们有:

In [47]: tot[:10]
Out[47]: 
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

TextFileReader还配备有一个get_chunk方法,使您能够以任意大小读取文件的片段。

将数据写入文本格式

数据也可以导出为分隔格式。让我们考虑之前读取的一个 CSV 文件:

In [48]: data = pd.read_csv("examples/ex5.csv")

In [49]: data
Out[49]: 
 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 [50]: data.to_csv("examples/out.csv")

In [51]: !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 [52]: import sys

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

如果未指定其他选项,则将同时写入行标签和列标签。这两者都可以禁用:

In [55]: 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 [56]: data.to_csv(sys.stdout, index=False, columns=["a", "b", "c"])
a,b,c
1,2,3.0
5,6,
9,10,11.0

处理其他分隔格式

使用函数如 pandas.read_csv 可以从磁盘加载大多数形式的表格数据。然而,在某些情况下,可能需要一些手动处理。接收到一个或多个格式错误的行可能会导致 pandas.read_csv 出错。为了说明基本工具,考虑一个小的 CSV 文件:

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

对于任何具有单字符分隔符的文件,您可以使用 Python 的内置 csv 模块。要使用它,将任何打开的文件或类似文件的对象传递给 csv.reader

In [58]: import csv

In [59]: f = open("examples/ex7.csv")

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

像处理文件一样迭代读取器会产生去除任何引号字符的值列表:

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

In [62]: f.close()

然后,您需要进行必要的整理以将数据放入所需的形式。让我们一步一步来。首先,我们将文件读取为行列表:

In [63]: with open("examples/ex7.csv") as f:
 ....:     lines = list(csv.reader(f))

然后我们将行分割为标题行和数据行:

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

然后我们可以使用字典推导和表达式 zip(*values) 创建数据列的字典(请注意,这将在大文件上使用大量内存),将行转置为列:

In [65]: data_dict = {h: v for h, v in zip(header, zip(*values))}

In [66]: data_dict
Out[66]: {'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 dialect 选项

注意

对于具有更复杂或固定多字符分隔符的文件,您将无法使用 csv 模块。在这些情况下,您将需要使用字符串的 split 方法或正则表达式方法 re.split 进行行分割和其他清理。幸运的是,如果传递必要的选项,pandas.read_csv 能够几乎做任何您需要的事情,因此您很少需要手动解析文件。

手动 写入分隔文件,可以使用 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 对象表示法的缩写)已经成为在 Web 浏览器和其他应用程序之间通过 HTTP 请求发送数据的标准格式之一。它是比 CSV 等表格文本形式更自由的数据格式。这里是一个例子:

obj = """
{"name": "Wes",
 "cities_lived": ["Akron", "Nashville", "New York", "San Francisco"],
 "pet": null,
 "siblings": [{"name": "Scott", "age": 34, "hobbies": ["guitars", "soccer"]},
 {"name": "Katie", "age": 42, "hobbies": ["diving", "art"]}]
}
"""

JSON 几乎是有效的 Python 代码,只是其空值null和一些其他细微差别(例如不允许在列表末尾使用逗号)。基本类型是对象(字典)、数组(列表)、字符串、数字、布尔值和空值。对象中的所有键都必须是字符串。有几个 Python 库可用于读取和写入 JSON 数据。我将在这里使用json,因为它内置在 Python 标准库中。要将 JSON 字符串转换为 Python 形式,请使用json.loads

In [68]: import json

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

In [70]: result
Out[70]: 
{'name': 'Wes',
 'cities_lived': ['Akron', 'Nashville', 'New York', 'San Francisco'],
 'pet': None,
 'siblings': [{'name': 'Scott',
 'age': 34,
 'hobbies': ['guitars', 'soccer']},
 {'name': 'Katie', 'age': 42, 'hobbies': ['diving', 'art']}]}

json.dumps,另一方面,将 Python 对象转换回 JSON:

In [71]: asjson = json.dumps(result)

In [72]: asjson
Out[72]: '{"name": "Wes", "cities_lived": ["Akron", "Nashville", "New York", "San
 Francisco"], "pet": null, "siblings": [{"name": "Scott", "age": 34, "hobbies": [
"guitars", "soccer"]}, {"name": "Katie", "age": 42, "hobbies": ["diving", "art"]}
]}'

如何将 JSON 对象或对象列表转换为 DataFrame 或其他数据结构以进行分析将取决于您。方便的是,您可以将字典列表(先前是 JSON 对象)传递给 DataFrame 构造函数并选择数据字段的子集:

In [73]: siblings = pd.DataFrame(result["siblings"], columns=["name", "age"])

In [74]: siblings
Out[74]: 
 name  age
0  Scott   34
1  Katie   42

pandas.read_json可以自动将特定排列的 JSON 数据集转换为 Series 或 DataFrame。例如:

In [75]: !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 [76]: data = pd.read_json("examples/example.json")

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

有关阅读和操作 JSON 数据的扩展示例(包括嵌套记录),请参见第十三章:数据分析示例中的美国农业部食品数据库示例。

如果您需要将数据从 pandas 导出为 JSON,一种方法是在 Series 和 DataFrame 上使用to_json方法:

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

XML 和 HTML:网络抓取

Python 有许多用于读取和写入 HTML 和 XML 格式数据的库。示例包括 lxml、Beautiful Soup 和 html5lib。虽然 lxml 通常在一般情况下更快,但其他库可以更好地处理格式不正确的 HTML 或 XML 文件。

pandas 有一个内置函数pandas.read_html,它使用所有这些库自动将 HTML 文件中的表格解析为 DataFrame 对象。为了展示这是如何工作的,我下载了一个 HTML 文件(在 pandas 文档中使用)从美国联邦存款保险公司显示银行倒闭。¹首先,您必须安装一些read_html使用的附加库:

conda install lxml beautifulsoup4 html5lib

如果您没有使用 conda,pip install lxml也应该可以工作。

pandas.read_html函数有许多选项,但默认情况下它会搜索并尝试解析包含在<table>标签中的所有表格数据。结果是一个 DataFrame 对象的列表:

In [80]: tables = pd.read_html("examples/fdic_failed_bank_list.html")

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

In [82]: failures = tables[0]

In [83]: failures.head()
Out[83]: 
 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 

由于failures有许多列,pandas 会插入一个换行符\

正如您将在后面的章节中了解到的那样,从这里我们可以继续进行一些数据清理和分析,比如计算每年的银行倒闭次数:

In [84]: close_timestamps = pd.to_datetime(failures["Closing Date"])

In [85]: close_timestamps.dt.year.value_counts()
Out[85]: 
Closing Date
2010    157
2009    140
2011     92
2012     51
2008     25
 ... 
2004      4
2001      4
2007      3
2003      3
2000      2
Name: count, Length: 15, dtype: int64
使用lxml.objectify解析 XML

XML 是另一种常见的结构化数据格式,支持具有元数据的分层嵌套数据。您当前正在阅读的书实际上是从一系列大型 XML 文档创建的。

之前,我展示了pandas.read_html函数,它在底层使用 lxml 或 Beautiful Soup 来解析 HTML 中的数据。XML 和 HTML 在结构上相似,但 XML 更通用。在这里,我将展示如何使用 lxml 来解析更一般的 XML 格式中的数据的示例。

多年来,纽约大都会交通管理局(MTA)以 XML 格式发布了许多关于其公交车和火车服务的数据系列。在这里,我们将查看性能数据,这些数据包含在一组 XML 文件中。每个火车或公交车服务都有一个不同的文件(例如Performance_MNR.xml用于 Metro-North Railroad),其中包含作为一系列 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,我们解析文件并获取 XML 文件的根节点的引用:

In [86]: from lxml import objectify

In [87]: path = "datasets/mta_perf/Performance_MNR.xml"

In [88]: with open(path) as f:
 ....:     parsed = objectify.parse(f)

In [89]: root = parsed.getroot()

root.INDICATOR返回一个生成器,产生每个<INDICATOR> XML 元素。对于每条记录,我们可以通过运行以下代码填充一个标签名称(如YTD_ACTUAL)到数据值(排除一些标签)的字典:

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:

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

In [92]: perf.head()
Out[92]: 
 AGENCY_NAME                        INDICATOR_NAME 
0  Metro-North Railroad  On-Time Performance (West of Hudson)  \
1  Metro-North Railroad  On-Time Performance (West of Hudson) 
2  Metro-North Railroad  On-Time Performance (West of Hudson) 
3  Metro-North Railroad  On-Time Performance (West of Hudson) 
4  Metro-North Railroad  On-Time Performance (West of Hudson) 
 DESCRIPTION 
0  Percent of commuter trains that arrive at their destinations within 5 m...  \
1  Percent of commuter trains that arrive at their destinations within 5 m... 
2  Percent of commuter trains that arrive at their destinations within 5 m... 
3  Percent of commuter trains that arrive at their destinations within 5 m... 
4  Percent of commuter trains that arrive at their destinations within 5 m... 
 PERIOD_YEAR  PERIOD_MONTH            CATEGORY FREQUENCY INDICATOR_UNIT 
0         2008             1  Service Indicators         M              %  \
1         2008             2  Service Indicators         M              % 
2         2008             3  Service Indicators         M              % 
3         2008             4  Service Indicators         M              % 
4         2008             5  Service Indicators         M              % 
 YTD_TARGET YTD_ACTUAL MONTHLY_TARGET MONTHLY_ACTUAL 
0       95.0       96.9           95.0           96.9 
1       95.0       96.0           95.0           95.0 
2       95.0       96.3           95.0           96.9 
3       95.0       96.8           95.0           98.3 
4       95.0       96.6           95.0           95.8 

pandas 的pandas.read_xml函数将此过程转换为一行表达式:

In [93]: perf2 = pd.read_xml(path)

In [94]: perf2.head()
Out[94]: 
 INDICATOR_SEQ  PARENT_SEQ           AGENCY_NAME 
0          28445         NaN  Metro-North Railroad  \
1          28445         NaN  Metro-North Railroad 
2          28445         NaN  Metro-North Railroad 
3          28445         NaN  Metro-North Railroad 
4          28445         NaN  Metro-North Railroad 
 INDICATOR_NAME 
0  On-Time Performance (West of Hudson)  \
1  On-Time Performance (West of Hudson) 
2  On-Time Performance (West of Hudson) 
3  On-Time Performance (West of Hudson) 
4  On-Time Performance (West of Hudson) 
 DESCRIPTION 
0  Percent of commuter trains that arrive at their destinations within 5 m...  \
1  Percent of commuter trains that arrive at their destinations within 5 m... 
2  Percent of commuter trains that arrive at their destinations within 5 m... 
3  Percent of commuter trains that arrive at their destinations within 5 m... 
4  Percent of commuter trains that arrive at their destinations within 5 m... 
 PERIOD_YEAR  PERIOD_MONTH            CATEGORY FREQUENCY DESIRED_CHANGE 
0         2008             1  Service Indicators         M              U  \
1         2008             2  Service Indicators         M              U 
2         2008             3  Service Indicators         M              U 
3         2008             4  Service Indicators         M              U 
4         2008             5  Service Indicators         M              U 
 INDICATOR_UNIT  DECIMAL_PLACES YTD_TARGET YTD_ACTUAL MONTHLY_TARGET 
0              %               1      95.00      96.90          95.00  \
1              %               1      95.00      96.00          95.00 
2              %               1      95.00      96.30          95.00 
3              %               1      95.00      96.80          95.00 
4              %               1      95.00      96.60          95.00 
 MONTHLY_ACTUAL 
0          96.90 
1          95.00 
2          96.90 
3          98.30 
4          95.80 

对于更复杂的 XML 文档,请参考pandas.read_xml的文档字符串,其中描述了如何进行选择和过滤以提取感兴趣的特定表格。

6.2 二进制数据格式

以二进制格式存储(或序列化)数据的一种简单方法是使用 Python 的内置pickle模块。所有 pandas 对象都有一个to_pickle方法,它以 pickle 格式将数据写入磁盘:

In [95]: frame = pd.read_csv("examples/ex1.csv")

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

In [97]: frame.to_pickle("examples/frame_pickle")

Pickle 文件通常只能在 Python 中读取。您可以直接使用内置的pickle读取存储在文件中的任何“pickled”对象,或者更方便地使用pandas.read_pickle

In [98]: pd.read_pickle("examples/frame_pickle")
Out[98]: 
 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。pandas 在可能的情况下尽力保持向后兼容性,但在将来的某个时候可能需要“破坏”pickle 格式。

pandas 内置支持其他几种开源二进制数据格式,例如 HDF5、ORC 和 Apache Parquet。例如,如果安装pyarrow包(conda install pyarrow),则可以使用pandas.read_parquet读取 Parquet 文件:

In [100]: fec = pd.read_parquet('datasets/fec/fec.parquet')

我将在 HDF5 格式使用中给出一些 HDF5 示例。我鼓励您探索不同的文件格式,看看它们的速度和对您的分析工作的适用性。

读取 Microsoft Excel 文件

pandas 还支持使用pandas.ExcelFile类或pandas.read_excel函数读取存储在 Excel 2003(及更高版本)文件中的表格数据。在内部,这些工具使用附加包xlrdopenpyxl来分别读取旧式 XLS 和新式 XLSX 文件。这些必须使用 pip 或 conda 单独安装,而不是从 pandas 安装:

conda install openpyxl xlrd

要使用pandas.ExcelFile,请通过传递路径到xlsxlsx文件来创建一个实例:

In [101]: xlsx = pd.ExcelFile("examples/ex1.xlsx")

此对象可以显示文件中可用工作表名称的列表:

In [102]: xlsx.sheet_names
Out[102]: ['Sheet1']

可以使用parse将工作表中存储的数据读入 DataFrame:

In [103]: xlsx.parse(sheet_name="Sheet1")
Out[103]: 
 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

此 Excel 表具有索引列,因此我们可以使用index_col参数指示:

In [104]: xlsx.parse(sheet_name="Sheet1", index_col=0)
Out[104]: 
 a   b   c   d message
0  1   2   3   4   hello
1  5   6   7   8   world
2  9  10  11  12     foo

如果要在一个文件中读取多个工作表,则创建pandas.ExcelFile会更快,但您也可以简单地将文件名传递给pandas.read_excel

In [105]: frame = pd.read_excel("examples/ex1.xlsx", sheet_name="Sheet1")

In [106]: frame
Out[106]: 
 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 [107]: writer = pd.ExcelWriter("examples/ex2.xlsx")

In [108]: frame.to_excel(writer, "Sheet1")

In [109]: writer.close()

您还可以将文件路径传递给to_excel,避免使用ExcelWriter

In [110]: frame.to_excel("examples/ex2.xlsx")

使用 HDF5 格式

HDF5 是一种受尊敬的文件格式,用于存储大量科学数组数据。它作为一个 C 库可用,并且在许多其他语言中都有接口,包括 Java、Julia、MATLAB 和 Python。HDF5 中的“HDF”代表分层数据格式。每个 HDF5 文件可以存储多个数据集和支持的元数据。与更简单的格式相比,HDF5 支持各种压缩模式的即时压缩,使具有重复模式的数据能够更有效地存储。HDF5 可以是处理不适合内存的数据集的良好选择,因为您可以有效地读取和写入更大数组的小部分。

要开始使用 HDF5 和 pandas,您必须首先通过使用 conda 安装tables包来安装 PyTables:

conda install pytables

注意

请注意,PyTables 包在 PyPI 中称为“tables”,因此如果您使用 pip 安装,您将需要运行pip install tables

虽然可以直接使用 PyTables 或 h5py 库访问 HDF5 文件,但 pandas 提供了一个简化存储 Series 和 DataFrame 对象的高级接口。HDFStore类的工作方式类似于字典,并处理底层细节:

In [113]: frame = pd.DataFrame({"a": np.random.standard_normal(100)})

In [114]: store = pd.HDFStore("examples/mydata.h5")

In [115]: store["obj1"] = frame

In [116]: store["obj1_col"] = frame["a"]

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

然后可以使用相同类似字典的 API 检索 HDF5 文件中包含的对象:

In [118]: store["obj1"]
Out[118]: 
 a
0  -0.204708
1   0.478943
2  -0.519439
3  -0.555730
4   1.965781
..       ...
95  0.795253
96  0.118110
97 -0.748532
98  0.584970
99  0.152677
[100 rows x 1 columns]

HDFStore支持两种存储模式,"fixed""table"(默认为"fixed")。后者通常较慢,但支持使用特殊语法进行查询操作:

In [119]: store.put("obj2", frame, format="table")

In [120]: store.select("obj2", where=["index >= 10 and index <= 15"])
Out[120]: 
 a
10  1.007189
11 -1.296221
12  0.274992
13  0.228913
14  1.352917
15  0.886429

In [121]: store.close()

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

pandas.read_hdf函数为您提供了这些工具的快捷方式:

In [122]: frame.to_hdf("examples/mydata.h5", "obj3", format="table")

In [123]: pd.read_hdf("examples/mydata.h5", "obj3", where=["index < 5"])
Out[123]: 
 a
0 -0.204708
1  0.478943
2 -0.519439
3 -0.555730
4  1.965781

如果您愿意,可以删除您创建的 HDF5 文件,方法如下:

In [124]: import os

In [125]: os.remove("examples/mydata.h5")

注意

如果您正在处理存储在远程服务器上的数据,如 Amazon S3 或 HDFS,使用设计用于分布式存储的不同二进制格式(如Apache Parquet)可能更合适。

如果您在本地处理大量数据,我建议您探索 PyTables 和 h5py,看看它们如何满足您的需求。由于许多数据分析问题受 I/O 限制(而不是 CPU 限制),使用 HDF5 等工具可以大大加速您的应用程序。

注意

HDF5 不是数据库。它最适合于一次写入,多次读取的数据集。虽然数据可以随时添加到文件中,但如果多个写入者同时这样做,文件可能会损坏。

6.3 与 Web API 交互

许多网站都有提供数据源的公共 API,可以通过 JSON 或其他格式提供数据。有许多方法可以从 Python 访问这些 API;我推荐的一种方法是requests,可以使用 pip 或 conda 进行安装:

conda install requests

要在 GitHub 上找到 pandas 的最近 30 个问题,我们可以使用附加的requests库进行GET HTTP 请求:

In [126]: import requests

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

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

In [129]: resp.raise_for_status()

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

在使用requests.get后,始终调用raise_for_status以检查 HTTP 错误是一个好习惯。

响应对象的json方法将返回一个包含解析后的 JSON 数据的 Python 对象,作为字典或列表(取决于返回的 JSON 是什么):

In [131]: data = resp.json()

In [132]: data[0]["title"]
Out[132]: 'BUG: DataFrame.pivot mutates empty index.name attribute with typing._L
iteralGenericAlias'

由于检索到的结果基于实时数据,当您运行此代码时,您看到的结果几乎肯定会有所不同。

data中的每个元素都是一个包含 GitHub 问题页面上找到的所有数据的字典(评论除外)。我们可以直接将data传递给pandas.DataFrame并提取感兴趣的字段:

In [133]: issues = pd.DataFrame(data, columns=["number", "title",
 .....:                                      "labels", "state"])

In [134]: issues
Out[134]: 
 number 
0    52629  \
1    52628 
2    52626 
3    52625 
4    52624 
..     ... 
25   52579 
26   52577 
27   52576 
28   52571 
29   52570 
 title 
0   BUG: DataFrame.pivot mutates empty index.name attribute with typing._Li...  \
1                                 DEPR: unused keywords in DTI/TDI construtors 
2                         ENH: Infer best datetime format from a random sample 
3            BUG: ArrowExtensionArray logical_op not working in all directions 
4              ENH: pandas.core.groupby.SeriesGroupBy.apply allow raw argument 
..                                                                         ... 
25                                     BUG: Axial inconsistency of pandas.diff 
26                  BUG: describe not respecting ArrowDtype in include/exclude 
27                  BUG: describe does not distinguish between Int64 and int64 
28  BUG: `pandas.DataFrame.replace` silently fails to replace category type... 
29     BUG: DataFrame.describe include/exclude do not work for arrow datatypes 
 labels 
0   [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... \
1                                                                           [] 
2 [] 
3   [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... 
4 [{'id': 76812, 'node_id': 'MDU6TGFiZWw3NjgxMg==', 'url': 'https://api.g... 
..                                                                         ... 
25  [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... 
26 [{'id': 3303158446, 'node_id': 'MDU6TGFiZWwzMzAzMTU4NDQ2', 'url': 'http... 
27 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... 
28 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... 
29 [{'id': 76811, 'node_id': 'MDU6TGFiZWw3NjgxMQ==', 'url': 'https://api.g... 
 state 
0   open 
1   open 
2   open 
3   open 
4   open 
..   ... 
25  open 
26  open 
27  open 
28  open 
29  open 
[30 rows x 4 columns]

通过一些努力,您可以创建一些更高级的接口,用于常见的 Web API,返回 DataFrame 对象以便进行更方便的分析。

6.4 与数据库交互

在商业环境中,许多数据可能不存储在文本或 Excel 文件中。基于 SQL 的关系数据库(如 SQL Server、PostgreSQL 和 MySQL)被广泛使用,许多替代数据库也变得非常流行。数据库的选择通常取决于应用程序的性能、数据完整性和可扩展性需求。

pandas 有一些函数可以简化将 SQL 查询结果加载到 DataFrame 中。例如,我将使用 Python 内置的sqlite3驱动程序创建一个 SQLite3 数据库:

In [135]: import sqlite3

In [136]: query = """
 .....: CREATE TABLE test
 .....: (a VARCHAR(20), b VARCHAR(20),
 .....:  c REAL,        d INTEGER
 .....: );"""

In [137]: con = sqlite3.connect("mydata.sqlite")

In [138]: con.execute(query)
Out[138]: <sqlite3.Cursor at 0x188e40ac0>

In [139]: con.commit()

然后,插入一些数据行:

In [140]: data = [("Atlanta", "Georgia", 1.25, 6),
 .....:         ("Tallahassee", "Florida", 2.6, 3),
 .....:         ("Sacramento", "California", 1.7, 5)]

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

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

In [143]: con.commit()

大多数 Python SQL 驱动程序在从表中选择数据时返回一个元组列表:

In [144]: cursor = con.execute("SELECT * FROM test")

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

In [146]: rows
Out[146]: 
[('Atlanta', 'Georgia', 1.25, 6),
 ('Tallahassee', 'Florida', 2.6, 3),
 ('Sacramento', 'California', 1.7, 5)]

您可以将元组列表传递给 DataFrame 构造函数,但还需要列名,这些列名包含在游标的description属性中。请注意,对于 SQLite3,游标的description仅提供列名(其他字段,这些字段是 Python 的数据库 API 规范的一部分,为None),但对于其他一些数据库驱动程序,提供了更多的列信息:

In [147]: cursor.description
Out[147]: 
(('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 [148]: pd.DataFrame(rows, columns=[x[0] for x in cursor.description])
Out[148]: 
 a           b     c  d
0      Atlanta     Georgia  1.25  6
1  Tallahassee     Florida  2.60  3
2   Sacramento  California  1.70  5

这是一种相当复杂的操作,您不希望每次查询数据库时都重复。SQLAlchemy 项目是一个流行的 Python SQL 工具包,它抽象了 SQL 数据库之间的许多常见差异。pandas 有一个read_sql函数,可以让您轻松地从通用的 SQLAlchemy 连接中读取数据。您可以像这样使用 conda 安装 SQLAlchemy:

conda install sqlalchemy

现在,我们将使用 SQLAlchemy 连接到相同的 SQLite 数据库,并从之前创建的表中读取数据:

In [149]: import sqlalchemy as sqla

In [150]: db = sqla.create_engine("sqlite:///mydata.sqlite")

In [151]: pd.read_sql("SELECT * FROM test", db)
Out[151]: 
 a           b     c  d
0      Atlanta     Georgia  1.25  6
1  Tallahassee     Florida  2.60  3
2   Sacramento  California  1.70  5

6.5 结论

获取数据通常是数据分析过程中的第一步。在本章中,我们已经介绍了一些有用的工具,这些工具应该可以帮助您入门。在接下来的章节中,我们将深入探讨数据整理、数据可视化、时间序列分析等主题。


  1. 完整列表请参见www.fdic.gov/bank/individual/failed/banklist.html

七、数据清理和准备

在进行数据分析和建模过程中,大量时间花费在数据准备上:加载、清理、转换和重新排列。这些任务通常被报告为占据分析师 80%或更多的时间。有时,文件或数据库中存储数据的方式并不适合特定任务。许多研究人员选择使用通用编程语言(如 Python、Perl、R 或 Java)或 Unix 文本处理工具(如 sed 或 awk)对数据进行自发处理,从一种形式转换为另一种形式。幸运的是,pandas 与内置的 Python 语言功能一起,为您提供了一套高级、灵活和快速的工具,使您能够将数据转换为正确的形式。

如果您发现在本书或 pandas 库中找不到的数据操作类型,请随时在 Python 邮件列表或 pandas GitHub 网站上分享您的用例。事实上,pandas 的设计和实现很大程度上是由真实应用程序的需求驱动的。

在本章中,我讨论了有关缺失数据、重复数据、字符串操作和其他一些分析数据转换的工具。在下一章中,我将专注于以各种方式组合和重新排列数据集。

7.1 处理缺失数据

缺失数据在许多数据分析应用中很常见。pandas 的目标之一是尽可能地使处理缺失数据变得轻松。例如,默认情况下,pandas 对象上的所有描述性统计都排除缺失数据。

pandas 对象中表示缺失数据的方式有些不完美,但对于大多数真实世界的用途来说是足够的。对于float64数据类型,pandas 使用浮点值NaN(Not a Number)表示缺失数据。

我们称之为标记值:当存在时,表示缺失(或)值:

In [14]: float_data = pd.Series([1.2, -3.5, np.nan, 0])

In [15]: float_data
Out[15]: 
0    1.2
1   -3.5
2    NaN
3    0.0
dtype: float64

isna方法为我们提供一个布尔 Series,其中值为空时为True

In [16]: float_data.isna()
Out[16]: 
0    False
1    False
2     True
3    False
dtype: bool

在 pandas 中,我们采用了 R 编程语言中使用的惯例,将缺失数据称为 NA,代表不可用。在统计应用中,NA 数据可能是不存在的数据,也可能是存在但未被观察到的数据(例如通过数据收集问题)。在清理数据进行分析时,通常重要的是对缺失数据本身进行分析,以识别数据收集问题或由缺失数据引起的数据潜在偏差。

内置的 Python None值也被视为 NA:

In [17]: string_data = pd.Series(["aardvark", np.nan, None, "avocado"])

In [18]: string_data
Out[18]: 
0    aardvark
1         NaN
2        None
3     avocado
dtype: object

In [19]: string_data.isna()
Out[19]: 
0    False
1     True
2     True
3    False
dtype: bool

In [20]: float_data = pd.Series([1, 2, None], dtype='float64')

In [21]: float_data
Out[21]: 
0    1.0
1    2.0
2    NaN
dtype: float64

In [22]: float_data.isna()
Out[22]: 
0    False
1    False
2     True
dtype: bool

pandas 项目已经尝试使处理缺失数据在不同数据类型之间保持一致。像pandas.isna这样的函数抽象了许多烦人的细节。请参阅表 7.1 以获取与处理缺失数据相关的一些函数列表。

表 7.1:NA 处理对象方法

过滤缺失数据

有几种过滤缺失数据的方法。虽然您始终可以选择使用 pandas.isna 和布尔索引手动执行,但 dropna 可能会有所帮助。对于 Series,它返回仅具有非空数据和索引值的 Series:

In [23]: data = pd.Series([1, np.nan, 3.5, np.nan, 7])

In [24]: data.dropna()
Out[24]: 
0    1.0
2    3.5
4    7.0
dtype: float64

这与执行以下操作相同:

In [25]: data[data.notna()]
Out[25]: 
0    1.0
2    3.5
4    7.0
dtype: float64

对于 DataFrame 对象,有不同的方法可以删除缺失数据。您可能希望删除所有 NA 的行或列,或者仅删除包含任何 NA 的行或列。dropna 默认情况下会删除包含缺失值的任何行:

In [26]: data = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
 ....:                      [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])

In [27]: data
Out[27]: 
 0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

In [28]: data.dropna()
Out[28]: 
 0    1    2
0  1.0  6.5  3.0

传递 how="all" 将仅删除所有 NA 的行:

In [29]: data.dropna(how="all")
Out[29]: 
 0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
3  NaN  6.5  3.0

请记住,这些函数默认情况下返回新对象,不会修改原始对象的内容。

要以相同方式删除列,请传递 axis="columns"

In [30]: data[4] = np.nan

In [31]: data
Out[31]: 
 0    1    2   4
0  1.0  6.5  3.0 NaN
1  1.0  NaN  NaN NaN
2  NaN  NaN  NaN NaN
3  NaN  6.5  3.0 NaN

In [32]: data.dropna(axis="columns", how="all")
Out[32]: 
 0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

假设您只想保留包含至多一定数量缺失观察的行。您可以使用 thresh 参数指示这一点:

In [33]: df = pd.DataFrame(np.random.standard_normal((7, 3)))

In [34]: df.iloc[:4, 1] = np.nan

In [35]: df.iloc[:2, 2] = np.nan

In [36]: df
Out[36]: 
 0         1         2
0 -0.204708       NaN       NaN
1 -0.555730       NaN       NaN
2  0.092908       NaN  0.769023
3  1.246435       NaN -1.296221
4  0.274992  0.228913  1.352917
5  0.886429 -2.001637 -0.371843
6  1.669025 -0.438570 -0.539741

In [37]: df.dropna()
Out[37]: 
 0         1         2
4  0.274992  0.228913  1.352917
5  0.886429 -2.001637 -0.371843
6  1.669025 -0.438570 -0.539741

In [38]: df.dropna(thresh=2)
Out[38]: 
 0         1         2
2  0.092908       NaN  0.769023
3  1.246435       NaN -1.296221
4  0.274992  0.228913  1.352917
5  0.886429 -2.001637 -0.371843
6  1.669025 -0.438570 -0.539741

填充缺失数据

与过滤缺失数据(并可能连同其他数据一起丢弃)不同,您可能希望以任意方式填补任意数量的“空洞”。对于大多数情况,fillna 方法是要使用的主要函数。通过使用常量调用 fillna 可以用该值替换缺失值:

In [39]: df.fillna(0)
Out[39]: 
 0         1         2
0 -0.204708  0.000000  0.000000
1 -0.555730  0.000000  0.000000
2  0.092908  0.000000  0.769023
3  1.246435  0.000000 -1.296221
4  0.274992  0.228913  1.352917
5  0.886429 -2.001637 -0.371843
6  1.669025 -0.438570 -0.539741

通过字典调用 fillna,您可以为每列使用不同的填充值:

In [40]: df.fillna({1: 0.5, 2: 0})
Out[40]: 
 0         1         2
0 -0.204708  0.500000  0.000000
1 -0.555730  0.500000  0.000000
2  0.092908  0.500000  0.769023
3  1.246435  0.500000 -1.296221
4  0.274992  0.228913  1.352917
5  0.886429 -2.001637 -0.371843
6  1.669025 -0.438570 -0.539741

可用于重新索引的相同插值方法(请参见 表 5.3)也可用于 fillna

In [41]: df = pd.DataFrame(np.random.standard_normal((6, 3)))

In [42]: df.iloc[2:, 1] = np.nan

In [43]: df.iloc[4:, 2] = np.nan

In [44]: df
Out[44]: 
 0         1         2
0  0.476985  3.248944 -1.021228
1 -0.577087  0.124121  0.302614
2  0.523772       NaN  1.343810
3 -0.713544       NaN -2.370232
4 -1.860761       NaN       NaN
5 -1.265934       NaN       NaN

In [45]: df.fillna(method="ffill")
Out[45]: 
 0         1         2
0  0.476985  3.248944 -1.021228
1 -0.577087  0.124121  0.302614
2  0.523772  0.124121  1.343810
3 -0.713544  0.124121 -2.370232
4 -1.860761  0.124121 -2.370232
5 -1.265934  0.124121 -2.370232

In [46]: df.fillna(method="ffill", limit=2)
Out[46]: 
 0         1         2
0  0.476985  3.248944 -1.021228
1 -0.577087  0.124121  0.302614
2  0.523772  0.124121  1.343810
3 -0.713544  0.124121 -2.370232
4 -1.860761       NaN -2.370232
5 -1.265934       NaN -2.370232

使用 fillna,您可以做很多其他事情,比如使用中位数或平均统计数据进行简单的数据填充:

In [47]: data = pd.Series([1., np.nan, 3.5, np.nan, 7])

In [48]: data.fillna(data.mean())
Out[48]: 
0    1.000000
1    3.833333
2    3.500000
3    3.833333
4    7.000000
dtype: float64

请参见 表 7.2 了解 fillna 函数参数的参考。

表 7.2:fillna 函数参数

7.2 数据转换

到目前为止,在本章中,我们一直关注处理缺失数据。过滤、清理和其他转换是另一类重要操作。

删除重复项

DataFrame 中可能会出现重复行,原因有很多。这里是一个例子:

In [49]: data = pd.DataFrame({"k1": ["one", "two"] * 3 + ["two"],
 ....:                      "k2": [1, 1, 2, 3, 3, 4, 4]})

In [50]: data
Out[50]: 
 k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

DataFrame 方法 duplicated 返回一个布尔 Series,指示每行是否为重复行(其列值与较早行中的值完全相等):

In [51]: data.duplicated()
Out[51]: 
0    False
1    False
2    False
3    False
4    False
5    False
6     True
dtype: bool

相关地,drop_duplicates 返回一个 DataFrame,其中过滤掉 duplicated 数组为 False 的行:

In [52]: data.drop_duplicates()
Out[52]: 
 k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4

默认情况下,这两种方法都考虑所有列;或者,您可以指定任何子集来检测重复项。假设我们有一个额外的值列,并且只想基于 "k1" 列过滤重复项:

In [53]: data["v1"] = range(7)

In [54]: data
Out[54]: 
 k1  k2  v1
0  one   1   0
1  two   1   1
2  one   2   2
3  two   3   3
4  one   3   4
5  two   4   5
6  two   4   6

In [55]: data.drop_duplicates(subset=["k1"])
Out[55]: 
 k1  k2  v1
0  one   1   0
1  two   1   1

duplicateddrop_duplicates 默认保留第一个观察到的值组合。传递 keep="last" 将返回最后一个:

In [56]: data.drop_duplicates(["k1", "k2"], keep="last")
Out[56]: 
 k1  k2  v1
0  one   1   0
1  two   1   1
2  one   2   2
3  two   3   3
4  one   3   4
6  two   4   6

使用函数或映射转换数据

对于许多数据集,您可能希望根据数组、Series 或 DataFrame 中的值执行一些基于值的转换。考虑收集的关于各种肉类的假设数据:

In [57]: data = pd.DataFrame({"food": ["bacon", "pulled pork", "bacon",
 ....:                               "pastrami", "corned beef", "bacon",
 ....:                               "pastrami", "honey ham", "nova lox"],
 ....:                      "ounces": [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})

In [58]: data
Out[58]: 
 food  ounces
0        bacon     4.0
1  pulled pork     3.0
2        bacon    12.0
3     pastrami     6.0
4  corned beef     7.5
5        bacon     8.0
6     pastrami     3.0
7    honey ham     5.0
8     nova lox     6.0

假设您想要添加一个指示每种食物来自哪种动物的列。让我们写下每种不同肉类到动物种类的映射:

meat_to_animal = {
 "bacon": "pig",
 "pulled pork": "pig",
 "pastrami": "cow",
 "corned beef": "cow",
 "honey ham": "pig",
 "nova lox": "salmon"
}

Series 上的 map 方法(也在 Ch 5.2.5: 函数应用和映射 中讨论)接受一个包含映射的函数或类似字典的对象,用于对值进行转换:

In [60]: data["animal"] = data["food"].map(meat_to_animal)

In [61]: data
Out[61]: 
 food  ounces  animal
0        bacon     4.0     pig
1  pulled pork     3.0     pig
2        bacon    12.0     pig
3     pastrami     6.0     cow
4  corned beef     7.5     cow
5        bacon     8.0     pig
6     pastrami     3.0     cow
7    honey ham     5.0     pig
8     nova lox     6.0  salmon

我们也可以传递一个执行所有工作的函数:

In [62]: def get_animal(x):
 ....:     return meat_to_animal[x]

In [63]: data["food"].map(get_animal)
Out[63]: 
0       pig
1       pig
2       pig
3       cow
4       cow
5       pig
6       cow
7       pig
8    salmon
Name: food, dtype: object

使用 map 是执行逐元素转换和其他数据清理相关操作的便捷方式。

替换值

使用 fillna 方法填充缺失数据是更一般的值替换的特殊情况。正如您已经看到的,map 可以用于修改对象中的一部分值,但 replace 提供了一种更简单、更灵活的方法。让我们考虑这个 Series:

In [64]: data = pd.Series([1., -999., 2., -999., -1000., 3.])

In [65]: data
Out[65]: 
0       1.0
1    -999.0
2       2.0
3    -999.0
4   -1000.0
5       3.0
dtype: float64

-999 值可能是缺失数据的标记值。要用 pandas 理解的 NA 值替换这些值,可以使用 replace,生成一个新的 Series:

In [66]: data.replace(-999, np.nan)
Out[66]: 
0       1.0
1       NaN
2       2.0
3       NaN
4   -1000.0
5       3.0
dtype: float64

如果您想一次替换多个值,可以传递一个列表,然后是替代值:

In [67]: data.replace([-999, -1000], np.nan)
Out[67]: 
0    1.0
1    NaN
2    2.0
3    NaN
4    NaN
5    3.0
dtype: float64

要为每个值使用不同的替代值,传递一个替代列表:

In [68]: data.replace([-999, -1000], [np.nan, 0])
Out[68]: 
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64

传递的参数也可以是一个字典:

In [69]: data.replace({-999: np.nan, -1000: 0})
Out[69]: 
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64

注意

data.replace 方法与 data.str.replace 是不同的,后者执行逐元素的字符串替换。我们将在本章后面的 Series 中查看这些字符串方法。

重命名轴索引

与 Series 中的值类似,轴标签也可以通过函数或某种形式的映射进行类似转换,以生成新的、不同标记的对象。您还可以在原地修改轴,而不创建新的数据结构。这是一个简单的例子:

In [70]: data = pd.DataFrame(np.arange(12).reshape((3, 4)),
 ....:                     index=["Ohio", "Colorado", "New York"],
 ....:                     columns=["one", "two", "three", "four"])

与 Series 一样,轴索引具有 map 方法:

In [71]: def transform(x):
 ....:     return x[:4].upper()

In [72]: data.index.map(transform)
Out[72]: Index(['OHIO', 'COLO', 'NEW '], dtype='object')

您可以分配给 index 属性,直接修改 DataFrame:

In [73]: data.index = data.index.map(transform)

In [74]: data
Out[74]: 
 one  two  three  four
OHIO    0    1      2     3
COLO    4    5      6     7
NEW     8    9     10    11

如果要创建一个转换后的数据集副本而不修改原始数据集,一个有用的方法是 rename

In [75]: data.rename(index=str.title, columns=str.upper)
Out[75]: 
 ONE  TWO  THREE  FOUR
Ohio    0    1      2     3
Colo    4    5      6     7
New     8    9     10    11

值得注意的是,rename 可以与类似字典的对象一起使用,为轴标签的子集提供新值:

In [76]: data.rename(index={"OHIO": "INDIANA"},
 ....:             columns={"three": "peekaboo"})
Out[76]: 
 one  two  peekaboo  four
INDIANA    0    1         2     3
COLO       4    5         6     7
NEW        8    9        10    11

rename 可以避免手动复制 DataFrame 并为其 indexcolumns 属性分配新值的繁琐工作。

离散化和分箱

连续数据通常被离散化或以其他方式分成“箱子”进行分析。假设您有一组人的研究数据,并且想要将它们分成离散的年龄段:

In [77]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]

让我们将这些分成 18 至 25 岁、26 至 35 岁、36 至 60 岁,最后是 61 岁及以上的箱子。为此,您必须使用 pandas.cut

In [78]: bins = [18, 25, 35, 60, 100]

In [79]: age_categories = pd.cut(ages, bins)

In [80]: age_categories
Out[80]: 
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35,
 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64, right]): [(18, 25] < (25, 35] < (35, 60] < (60, 10
0]]

pandas 返回的对象是一个特殊的分类对象。您看到的输出描述了 pandas.cut 计算的箱。每个箱由一个特殊的(对于 pandas 是唯一的)区间值类型标识,其中包含每个箱的下限和上限:

In [81]: age_categories.codes
Out[81]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)

In [82]: age_categories.categories
Out[82]: IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]], dtype='interval
[int64, right]')

In [83]: age_categories.categories[0]
Out[83]: Interval(18, 25, closed='right')

In [84]: pd.value_counts(age_categories)
Out[84]: 
(18, 25]     5
(25, 35]     3
(35, 60]     3
(60, 100]    1
Name: count, dtype: int64

请注意,pd.value_counts(categories)pandas.cut 结果的箱计数。

在区间的字符串表示中,括号表示一侧是 开放的(排除的),而方括号表示一侧是 闭合的(包含的)。您可以通过传递 right=False 来更改哪一侧是闭合的:

In [85]: pd.cut(ages, bins, right=False)
Out[85]: 
[[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35,
 60), [35, 60), [25, 35)]
Length: 12
Categories (4, interval[int64, left]): [[18, 25) < [25, 35) < [35, 60) < [60, 100
)]

通过将列表或数组传递给 labels 选项,可以覆盖默认的基于区间的箱标签:

In [86]: group_names = ["Youth", "YoungAdult", "MiddleAged", "Senior"]

In [87]: pd.cut(ages, bins, labels=group_names)
Out[87]: 
['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', '
MiddleAged', 'MiddleAged', 'YoungAdult']
Length: 12
Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior']

如果将整数数量的箱传递给 pandas.cut 而不是显式的箱边界,它将基于数据中的最小值和最大值计算等长的箱。考虑一下一些均匀分布的数据被分成四等份的情况:

In [88]: data = np.random.uniform(size=20)

In [89]: pd.cut(data, 4, precision=2)
Out[89]: 
[(0.34, 0.55], (0.34, 0.55], (0.76, 0.97], (0.76, 0.97], (0.34, 0.55], ..., (0.34
, 0.55], (0.34, 0.55], (0.55, 0.76], (0.34, 0.55], (0.12, 0.34]]
Length: 20
Categories (4, interval[float64, right]): [(0.12, 0.34] < (0.34, 0.55] < (0.55, 0
.76] <
 (0.76, 0.97]]

precision=2 选项将小数精度限制为两位数。

一个与之密切相关的函数 pandas.qcut,根据样本分位数对数据进行分箱。根据数据的分布,使用 pandas.cut 通常不会导致每个箱具有相同数量的数据点。由于 pandas.qcut 使用样本分位数,因此您将获得大致相同大小的箱:

In [90]: data = np.random.standard_normal(1000)

In [91]: quartiles = pd.qcut(data, 4, precision=2)

In [92]: quartiles
Out[92]: 
[(-0.026, 0.62], (0.62, 3.93], (-0.68, -0.026], (0.62, 3.93], (-0.026, 0.62], ...
, (-0.68, -0.026], (-0.68, -0.026], (-2.96, -0.68], (0.62, 3.93], (-0.68, -0.026]
]
Length: 1000
Categories (4, interval[float64, right]): [(-2.96, -0.68] < (-0.68, -0.026] < (-0
.026, 0.62] <
 (0.62, 3.93]]

In [93]: pd.value_counts(quartiles)
Out[93]: 
(-2.96, -0.68]     250
(-0.68, -0.026]    250
(-0.026, 0.62]     250
(0.62, 3.93]       250
Name: count, dtype: int64

类似于 pandas.cut,您可以传递自己的分位数(介于 0 和 1 之间的数字):

In [94]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]).value_counts()
Out[94]: 
(-2.9499999999999997, -1.187]    100
(-1.187, -0.0265]                400
(-0.0265, 1.286]                 400
(1.286, 3.928]                   100
Name: count, dtype: int64

我们将在本章后面的聚合和分组操作讨论中再次回到pandas.cutpandas.qcut,因为这些离散化函数对于分位数和分组分析特别有用。

检测和过滤异常值

过滤或转换异常值主要是应用数组操作的问题。考虑一个包含一些正态分布数据的 DataFrame:

In [95]: data = pd.DataFrame(np.random.standard_normal((1000, 4)))

In [96]: data.describe()
Out[96]: 
 0            1            2            3
count  1000.000000  1000.000000  1000.000000  1000.000000
mean      0.049091     0.026112    -0.002544    -0.051827
std       0.996947     1.007458     0.995232     0.998311
min      -3.645860    -3.184377    -3.745356    -3.428254
25%      -0.599807    -0.612162    -0.687373    -0.747478
50%       0.047101    -0.013609    -0.022158    -0.088274
75%       0.756646     0.695298     0.699046     0.623331
max       2.653656     3.525865     2.735527     3.366626

假设您想要查找绝对值超过 3 的某一列中的值:

In [97]: col = data[2]

In [98]: col[col.abs() > 3]
Out[98]: 
41    -3.399312
136   -3.745356
Name: 2, dtype: float64

要选择所有值超过 3 或-3 的行,您可以在布尔 DataFrame 上使用any方法:

In [99]: data[(data.abs() > 3).any(axis="columns")]
Out[99]: 
 0         1         2         3
41   0.457246 -0.025907 -3.399312 -0.974657
60   1.951312  3.260383  0.963301  1.201206
136  0.508391 -0.196713 -3.745356 -1.520113
235 -0.242459 -3.056990  1.918403 -0.578828
258  0.682841  0.326045  0.425384 -3.428254
322  1.179227 -3.184377  1.369891 -1.074833
544 -3.548824  1.553205 -2.186301  1.277104
635 -0.578093  0.193299  1.397822  3.366626
782 -0.207434  3.525865  0.283070  0.544635
803 -3.645860  0.255475 -0.549574 -1.907459

data.abs() > 3周围的括号是必要的,以便在比较操作的结果上调用any方法。

可以根据这些标准设置值。以下是将值限制在区间-3 到 3 之外的代码:

In [100]: data[data.abs() > 3] = np.sign(data) * 3

In [101]: data.describe()
Out[101]: 
 0            1            2            3
count  1000.000000  1000.000000  1000.000000  1000.000000
mean      0.050286     0.025567    -0.001399    -0.051765
std       0.992920     1.004214     0.991414     0.995761
min      -3.000000    -3.000000    -3.000000    -3.000000
25%      -0.599807    -0.612162    -0.687373    -0.747478
50%       0.047101    -0.013609    -0.022158    -0.088274
75%       0.756646     0.695298     0.699046     0.623331
max       2.653656     3.000000     2.735527     3.000000

np.sign(data)语句根据data中的值是正数还是负数产生 1 和-1 值:

In [102]: np.sign(data).head()
Out[102]: 
 0    1    2    3
0 -1.0  1.0 -1.0  1.0
1  1.0 -1.0  1.0 -1.0
2  1.0  1.0  1.0 -1.0
3 -1.0 -1.0  1.0 -1.0
4 -1.0  1.0 -1.0 -1.0

排列和随机抽样

通过使用numpy.random.permutation函数,可以对 Series 或 DataFrame 中的行进行排列(随机重新排序)。调用permutation并传入您想要排列的轴的长度会产生一个整数数组,指示新的排序:

In [103]: df = pd.DataFrame(np.arange(5 * 7).reshape((5, 7)))

In [104]: df
Out[104]: 
 0   1   2   3   4   5   6
0   0   1   2   3   4   5   6
1   7   8   9  10  11  12  13
2  14  15  16  17  18  19  20
3  21  22  23  24  25  26  27
4  28  29  30  31  32  33  34

In [105]: sampler = np.random.permutation(5)

In [106]: sampler
Out[106]: array([3, 1, 4, 2, 0])

然后可以将该数组用于基于iloc的索引或等效的take函数:

In [107]: df.take(sampler)
Out[107]: 
 0   1   2   3   4   5   6
3  21  22  23  24  25  26  27
1   7   8   9  10  11  12  13
4  28  29  30  31  32  33  34
2  14  15  16  17  18  19  20
0   0   1   2   3   4   5   6

In [108]: df.iloc[sampler]
Out[108]: 
 0   1   2   3   4   5   6
3  21  22  23  24  25  26  27
1   7   8   9  10  11  12  13
4  28  29  30  31  32  33  34
2  14  15  16  17  18  19  20
0   0   1   2   3   4   5   6

通过使用axis="columns"调用take,我们还可以选择列的排列:

In [109]: column_sampler = np.random.permutation(7)

In [110]: column_sampler
Out[110]: array([4, 6, 3, 2, 1, 0, 5])

In [111]: df.take(column_sampler, axis="columns")
Out[111]: 
 4   6   3   2   1   0   5
0   4   6   3   2   1   0   5
1  11  13  10   9   8   7  12
2  18  20  17  16  15  14  19
3  25  27  24  23  22  21  26
4  32  34  31  30  29  28  33

要选择一个不带替换的随机子集(同一行不能出现两次),可以在 Series 和 DataFrame 上使用sample方法:

In [112]: df.sample(n=3)
Out[112]: 
 0   1   2   3   4   5   6
2  14  15  16  17  18  19  20
4  28  29  30  31  32  33  34
0   0   1   2   3   4   5   6

要生成一个带有替换的样本(允许重复选择),请将replace=True传递给sample

In [113]: choices = pd.Series([5, 7, -1, 6, 4])

In [114]: choices.sample(n=10, replace=True)
Out[114]: 
2   -1
0    5
3    6
1    7
4    4
0    5
4    4
0    5
4    4
4    4
dtype: int64

计算指示/虚拟变量

另一种用于统计建模或机器学习应用的转换类型是将分类变量转换为虚拟指示矩阵。如果 DataFrame 中的一列有k个不同的值,您将得到一个包含所有 1 和 0 的k列的矩阵或 DataFrame。pandas 有一个pandas.get_dummies函数可以做到这一点,尽管您也可以自己设计一个。让我们考虑一个示例 DataFrame:

In [115]: df = pd.DataFrame({"key": ["b", "b", "a", "c", "a", "b"],
 .....:                    "data1": range(6)})

In [116]: df
Out[116]: 
 key  data1
0   b      0
1   b      1
2   a      2
3   c      3
4   a      4
5   b      5

In [117]: pd.get_dummies(df["key"], dtype=float)
Out[117]: 
 a    b    c
0  0.0  1.0  0.0
1  0.0  1.0  0.0
2  1.0  0.0  0.0
3  0.0  0.0  1.0
4  1.0  0.0  0.0
5  0.0  1.0  0.0

在这里,我传递了dtype=float以将输出类型从布尔值(pandas 较新版本中的默认值)更改为浮点数。

在某些情况下,您可能希望在指示 DataFrame 的列中添加前缀,然后将其与其他数据合并。pandas.get_dummies有一个用于执行此操作的前缀参数:

In [118]: dummies = pd.get_dummies(df["key"], prefix="key", dtype=float)

In [119]: df_with_dummy = df[["data1"]].join(dummies)

In [120]: df_with_dummy
Out[120]: 
 data1  key_a  key_b  key_c
0      0    0.0    1.0    0.0
1      1    0.0    1.0    0.0
2      2    1.0    0.0    0.0
3      3    0.0    0.0    1.0
4      4    1.0    0.0    0.0
5      5    0.0    1.0    0.0

DataFrame.join方法将在下一章中详细解释。

如果 DataFrame 中的一行属于多个类别,则我们必须使用不同的方法来创建虚拟变量。让我们看一下 MovieLens 1M 数据集,该数据集在 Ch 13:数据分析示例中有更详细的研究:

In [121]: mnames = ["movie_id", "title", "genres"]

In [122]: movies = pd.read_table("datasets/movielens/movies.dat", sep="::",
 .....:                        header=None, names=mnames, engine="python")

In [123]: movies[:10]
Out[123]: 
 movie_id                               title                        genres
0         1                    Toy Story (1995)   Animation|Children's|Comedy
1         2                      Jumanji (1995)  Adventure|Children's|Fantasy
2         3             Grumpier Old Men (1995)                Comedy|Romance
3         4            Waiting to Exhale (1995)                  Comedy|Drama
4         5  Father of the Bride Part II (1995)                        Comedy
5         6                         Heat (1995)         Action|Crime|Thriller
6         7                      Sabrina (1995)                Comedy|Romance
7         8                 Tom and Huck (1995)          Adventure|Children's
8         9                 Sudden Death (1995)                        Action
9        10                    GoldenEye (1995)     Action|Adventure|Thriller

pandas 实现了一个特殊的 Series 方法str.get_dummies(以str.开头的方法将在字符串操作中更详细地讨论),处理了将多个组成员身份编码为分隔字符串的情况:

In [124]: dummies = movies["genres"].str.get_dummies("|")

In [125]: dummies.iloc[:10, :6]
Out[125]: 
 Action  Adventure  Animation  Children's  Comedy  Crime
0       0          0          1           1       1      0
1       0          1          0           1       0      0
2       0          0          0           0       1      0
3       0          0          0           0       1      0
4       0          0          0           0       1      0
5       1          0          0           0       0      1
6       0          0          0           0       1      0
7       0          1          0           1       0      0
8       1          0          0           0       0      0
9       1          1          0           0       0      0

然后,与之前一样,您可以将此与movies组合,同时在dummies DataFrame 的列名中添加"Genre_",使用add_prefix方法:

In [126]: movies_windic = movies.join(dummies.add_prefix("Genre_"))

In [127]: movies_windic.iloc[0]
Out[127]: 
movie_id                                       1
title                           Toy Story (1995)
genres               Animation|Children's|Comedy
Genre_Action                                   0
Genre_Adventure                                0
Genre_Animation                                1
Genre_Children's                               1
Genre_Comedy                                   1
Genre_Crime                                    0
Genre_Documentary                              0
Genre_Drama                                    0
Genre_Fantasy                                  0
Genre_Film-Noir                                0
Genre_Horror                                   0
Genre_Musical                                  0
Genre_Mystery                                  0
Genre_Romance                                  0
Genre_Sci-Fi                                   0
Genre_Thriller                                 0
Genre_War                                      0
Genre_Western                                  0
Name: 0, dtype: object

注意

对于更大的数据,使用这种构建具有多个成员身份的指示变量的方法并不特别快速。最好编写一个直接写入 NumPy 数组的低级函数,然后将结果包装在 DataFrame 中。

在统计应用中的一个有用的技巧是将pandas.get_dummies与像pandas.cut这样的离散化函数结合使用:*

In [128]: np.random.seed(12345) # to make the example repeatable

In [129]: values = np.random.uniform(size=10)

In [130]: values
Out[130]: 
array([0.9296, 0.3164, 0.1839, 0.2046, 0.5677, 0.5955, 0.9645, 0.6532,
 0.7489, 0.6536])

In [131]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]

In [132]: pd.get_dummies(pd.cut(values, bins))
Out[132]: 
 (0.0, 0.2]  (0.2, 0.4]  (0.4, 0.6]  (0.6, 0.8]  (0.8, 1.0]
0       False       False       False       False        True
1       False        True       False       False       False
2        True       False       False       False       False
3       False        True       False       False       False
4       False       False        True       False       False
5       False       False        True       False       False
6       False       False       False       False        True
7       False       False       False        True       False
8       False       False       False        True       False
9       False       False       False        True       False

我们稍后将再次查看pandas.get_dummies,在为建模创建虚拟变量中。

7.3 扩展数据类型

注意

这是一个较新且更高级的主题,许多 pandas 用户不需要了解太多,但我在这里完整地介绍它,因为在接下来的章节中我将引用和使用扩展数据类型。

pandas 最初是建立在 NumPy 的基础上的,NumPy 是一个主要用于处理数值数据的数组计算库。许多 pandas 概念,如缺失数据,是使用 NumPy 中可用的内容实现的,同时尽量在使用 NumPy 和 pandas 的库之间最大程度地保持兼容性。

基于 NumPy 的构建存在许多缺点,例如:

  • 对于一些数值数据类型,如整数和布尔值,缺失数据处理是不完整的。因此,当这些数据中引入缺失数据时,pandas 会将数据类型转换为float64,并使用np.nan表示空值。这导致许多 pandas 算法中出现了微妙的问题。

  • 具有大量字符串数据的数据集在计算上是昂贵的,并且使用了大量内存。

  • 一些数据类型,如时间间隔、时间增量和带时区的时间戳,如果不使用计算昂贵的 Python 对象数组,将无法有效支持。

最近,pandas 开发了一个扩展类型系统,允许添加新的数据类型,即使它们在 NumPy 中没有原生支持。这些新数据类型可以被视为与来自 NumPy 数组的数据同等重要。

让我们看一个例子,我们创建一个带有缺失值的整数 Series:

In [133]: s = pd.Series([1, 2, 3, None])

In [134]: s
Out[134]: 
0    1.0
1    2.0
2    3.0
3    NaN
dtype: float64

In [135]: s.dtype
Out[135]: dtype('float64')

主要出于向后兼容的原因,Series 使用了使用float64数据类型和np.nan表示缺失值的传统行为。我们可以使用pandas.Int64Dtype来创建这个 Series:

In [136]: s = pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype())

In [137]: s
Out[137]: 
0       1
1       2
2       3
3    <NA>
dtype: Int64

In [138]: s.isna()
Out[138]: 
0    False
1    False
2    False
3     True
dtype: bool

In [139]: s.dtype
Out[139]: Int64Dtype()

输出<NA>表示扩展类型数组中的值缺失。这使用了特殊的pandas.NA标记值:

In [140]: s[3]
Out[140]: <NA>

In [141]: s[3] is pd.NA
Out[141]: True

我们也可以使用缩写"Int64"来指定类型,而不是pd.Int64Dtype()。大写是必需的,否则它将是一个基于 NumPy 的非扩展类型:

In [142]: s = pd.Series([1, 2, 3, None], dtype="Int64")

pandas 还有一种专门用于字符串数据的扩展类型,不使用 NumPy 对象数组(需要安装 pyarrow 库):

In [143]: s = pd.Series(['one', 'two', None, 'three'], dtype=pd.StringDtype())

In [144]: s
Out[144]: 
0      one
1      two
2     <NA>
3    three
dtype: string

这些字符串数组通常使用更少的内存,并且在对大型数据集进行操作时通常更高效。

另一个重要的扩展类型是Categorical,我们将在 Categorical Data 中更详细地讨论。截至本文撰写时,可用的扩展类型的相对完整列表在表 7.3 中。

扩展类型可以传递给 Series 的astype方法,允许您在数据清理过程中轻松转换:

In [145]: df = pd.DataFrame({"A": [1, 2, None, 4],
 .....:                    "B": ["one", "two", "three", None],
 .....:                    "C": [False, None, False, True]})

In [146]: df
Out[146]: 
 A      B      C
0  1.0    one  False
1  2.0    two   None
2  NaN  three  False
3  4.0   None   True

In [147]: df["A"] = df["A"].astype("Int64")

In [148]: df["B"] = df["B"].astype("string")

In [149]: df["C"] = df["C"].astype("boolean")

In [150]: df
Out[150]: 
 A      B      C
0     1    one  False
1     2    two   <NA>
2  <NA>  three  False
3     4   <NA>   True

表 7.3:pandas 扩展数据类型

| UInt64Dtype | 64 位可空无符号整数,在传递为字符串时使用"UInt64" |

7.4 字符串操作

Python 长期以来一直是一种流行的原始数据处理语言,部分原因是它易于用于字符串和文本处理。大多数文本操作都可以通过字符串对象的内置方法简化。对于更复杂的模式匹配和文本操作,可能需要使用正则表达式。pandas 通过使您能够简洁地在整个数据数组上应用字符串和正则表达式,另外处理了缺失数据的烦恼。

Python 内置字符串对象方法

在许多字符串处理和脚本应用程序中,内置字符串方法已经足够。例如,逗号分隔的字符串可以使用split分割成多个部分:

In [151]: val = "a,b,  guido"

In [152]: val.split(",")
Out[152]: ['a', 'b', '  guido']

split通常与strip结合使用以修剪空格(包括换行符):

In [153]: pieces = [x.strip() for x in val.split(",")]

In [154]: pieces
Out[154]: ['a', 'b', 'guido']

这些子字符串可以使用加法和双冒号分隔符连接在一起:

In [155]: first, second, third = pieces

In [156]: first + "::" + second + "::" + third
Out[156]: 'a::b::guido'

但这并不是一种实用的通用方法。更快速和更符合 Python 风格的方法是将列表或元组传递给字符串"::"上的join方法:

In [157]: "::".join(pieces)
Out[157]: 'a::b::guido'

其他方法涉及定位子字符串。使用 Python 的in关键字是检测子字符串的最佳方法,尽管也可以使用indexfind

In [158]: "guido" in val
Out[158]: True

In [159]: val.index(",")
Out[159]: 1

In [160]: val.find(":")
Out[160]: -1

请注意,findindex之间的区别在于,如果未找到字符串,index会引发异常(而不是返回-1):

In [161]: val.index(":")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-161-bea4c4c30248> in <module>
----> 1 val.index(":")
ValueError: substring not found

相关地,count返回特定子字符串的出现次数:

In [162]: val.count(",")
Out[162]: 2

replace将一个模式的出现替换为另一个。通常也用于通过传递空字符串来删除模式:

In [163]: val.replace(",", "::")
Out[163]: 'a::b::  guido'

In [164]: val.replace(",", "")
Out[164]: 'ab  guido'

请参阅表 7.4 以获取 Python 的一些字符串方法列表。

正则表达式也可以与许多这些操作一起使用,您将看到。

表 7.4:Python 内置字符串方法

正则表达式

正则表达式提供了一种灵活的方式来在文本中搜索或匹配(通常更复杂的)字符串模式。单个表达式,通常称为regex,是根据正则表达式语言形成的字符串。Python 的内置re模块负责将正则表达式应用于字符串;我将在这里给出一些示例。

注意

编写正则表达式的艺术可能是一个单独的章节,因此超出了本书的范围。互联网和其他书籍上有许多优秀的教程和参考资料。

re 模块的函数分为三类:模式匹配、替换和拆分。当然,这些都是相关的;正则表达式描述了要在文本中定位的模式,然后可以用于许多目的。让我们看一个简单的例子:假设我们想要使用可变数量的空白字符(制表符、空格和换行符)来拆分字符串。

描述一个或多个空白字符的正则表达式是 \s+

In [165]: import re

In [166]: text = "foo    bar\t baz \tqux"

In [167]: re.split(r"\s+", text)
Out[167]: ['foo', 'bar', 'baz', 'qux']

当您调用 re.split(r"\s+", text) 时,正则表达式首先被 编译,然后在传递的文本上调用其 split 方法。您可以使用 re.compile 自己编译正则表达式,形成一个可重用的正则表达式对象:

In [168]: regex = re.compile(r"\s+")

In [169]: regex.split(text)
Out[169]: ['foo', 'bar', 'baz', 'qux']

如果您想要获取与正则表达式匹配的所有模式的列表,可以使用 findall 方法:

In [170]: regex.findall(text)
Out[170]: ['    ', '\t ', ' \t']

注意

为了避免在正则表达式中使用 \ 进行不必要的转义,请使用 原始 字符串字面量,如 r"C:\x",而不是等效的 "C:\\x"

如果您打算将相同的表达式应用于许多字符串,强烈建议使用 re.compile 创建一个正则表达式对象;这样可以节省 CPU 周期。

matchsearchfindall 密切相关。虽然 findall 返回字符串中的所有匹配项,但 search 只返回第一个匹配项。更严格地说,match 在字符串开头匹配。作为一个不太琐碎的例子,让我们考虑一个文本块和一个能够识别大多数电子邮件地址的正则表达式:

text = """Dave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Ryan ryan@yahoo.com"""
pattern = r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}"

# re.IGNORECASE makes the regex case insensitive
regex = re.compile(pattern, flags=re.IGNORECASE)

在文本上使用 findall 会产生一个电子邮件地址列表:

In [172]: regex.findall(text)
Out[172]: 
['dave@google.com',
 'steve@gmail.com',
 'rob@gmail.com',
 'ryan@yahoo.com']

search 为文本中的第一个电子邮件地址返回一个特殊的匹配对象。对于前面的正则表达式,匹配对象只能告诉我们模式在字符串中的起始和结束位置:

In [173]: m = regex.search(text)

In [174]: m
Out[174]: <re.Match object; span=(5, 20), match='dave@google.com'>

In [175]: text[m.start():m.end()]
Out[175]: 'dave@google.com'

regex.match 返回 None,因为它只会匹配如果模式出现在字符串的开头时:

In [176]: print(regex.match(text))
None

相关地,sub 将返回一个新字符串,其中模式的出现被新字符串替换:

In [177]: print(regex.sub("REDACTED", text))
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED

假设您想要查找电子邮件地址,并同时将每个地址分成三个组件:用户名、域名和域后缀。为此,请在模式的部分周围加上括号以进行分段:

In [178]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})"

In [179]: regex = re.compile(pattern, flags=re.IGNORECASE)

由此修改后的正则表达式生成的匹配对象将使用其 groups 方法返回模式组件的元组:

In [180]: m = regex.match("wesm@bright.net")

In [181]: m.groups()
Out[181]: ('wesm', 'bright', 'net')

当模式有组时,findall 返回一个元组列表:

In [182]: regex.findall(text)
Out[182]: 
[('dave', 'google', 'com'),
 ('steve', 'gmail', 'com'),
 ('rob', 'gmail', 'com'),
 ('ryan', 'yahoo', 'com')]

sub 还可以使用特殊符号如 \1\2 访问每个匹配中的组。符号 \1 对应于第一个匹配组,\2 对应于第二个,依此类推:

In [183]: print(regex.sub(r"Username: \1, Domain: \2, Suffix: \3", text))
Dave Username: dave, Domain: google, Suffix: com
Steve Username: steve, Domain: gmail, Suffix: com
Rob Username: rob, Domain: gmail, Suffix: com
Ryan Username: ryan, Domain: yahoo, Suffix: com

Python 中的正则表达式还有很多内容,其中大部分超出了本书的范围。表 7.5 提供了一个简要总结。

表 7.5:正则表达式方法

| sub, subn | 用替换表达式替换字符串中所有 (sub) 或前 n 次出现 (subn) 的模式;使用符号 \1, \2, ... 来引用替换字符串中的匹配组元素 |

pandas 中的字符串函数

清理混乱的数据集以进行分析通常需要大量的字符串操作。为了使事情更加复杂,包含字符串的列有时会有缺失数据:

In [184]: data = {"Dave": "dave@google.com", "Steve": "steve@gmail.com",
 .....:         "Rob": "rob@gmail.com", "Wes": np.nan}

In [185]: data = pd.Series(data)

In [186]: data
Out[186]: 
Dave     dave@google.com
Steve    steve@gmail.com
Rob        rob@gmail.com
Wes                  NaN
dtype: object

In [187]: data.isna()
Out[187]: 
Dave     False
Steve    False
Rob      False
Wes       True
dtype: bool

可以将字符串和正则表达式方法应用于每个值(传递 lambda 或其他函数)使用 data.map,但它将在 NA(空值)上失败。为了应对这一情况,Series 具有面向数组的字符串操作方法,可以跳过并传播 NA 值。这些方法通过 Series 的 str 属性访问;例如,我们可以使用 str.contains 检查每个电子邮件地址中是否包含 "gmail"

In [188]: data.str.contains("gmail")
Out[188]: 
Dave     False
Steve     True
Rob       True
Wes        NaN
dtype: object

请注意,此操作的结果具有 object 类型。pandas 具有提供对字符串、整数和布尔数据进行专门处理的扩展类型,这些类型在处理缺失数据时一直存在一些问题:

In [189]: data_as_string_ext = data.astype('string')

In [190]: data_as_string_ext
Out[190]: 
Dave     dave@google.com
Steve    steve@gmail.com
Rob        rob@gmail.com
Wes                 <NA>
dtype: string

In [191]: data_as_string_ext.str.contains("gmail")
Out[191]: 
Dave     False
Steve     True
Rob       True
Wes       <NA>
dtype: boolean

更详细地讨论了扩展类型,请参阅扩展数据类型。

也可以使用正则表达式,以及任何 re 选项,如 IGNORECASE

In [192]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})"

In [193]: data.str.findall(pattern, flags=re.IGNORECASE)
Out[193]: 
Dave     [(dave, google, com)]
Steve    [(steve, gmail, com)]
Rob        [(rob, gmail, com)]
Wes                        NaN
dtype: object

有几种进行矢量化元素检索的方法。可以使用 str.get 或索引到 str 属性:

In [194]: matches = data.str.findall(pattern, flags=re.IGNORECASE).str[0]

In [195]: matches
Out[195]: 
Dave     (dave, google, com)
Steve    (steve, gmail, com)
Rob        (rob, gmail, com)
Wes                      NaN
dtype: object

In [196]: matches.str.get(1)
Out[196]: 
Dave     google
Steve     gmail
Rob       gmail
Wes         NaN
dtype: object

您也可以使用以下语法对字符串进行切片:

In [197]: data.str[:5]
Out[197]: 
Dave     dave@
Steve    steve
Rob      rob@g
Wes        NaN
dtype: object

str.extract 方法将返回正则表达式的捕获组作为 DataFrame:

In [198]: data.str.extract(pattern, flags=re.IGNORECASE)
Out[198]: 
 0       1    2
Dave    dave  google  com
Steve  steve   gmail  com
Rob      rob   gmail  com
Wes      NaN     NaN  NaN

查看更多 pandas 字符串方法,请参阅表 7.6。

表 7.6: Series 字符串方法的部分列表

| lstrip | 修剪左侧的空白 |

7.5 分类数据

本节介绍了 pandas 的 Categorical 类型。我将展示如何通过使用它在某些 pandas 操作中实现更好的性能和内存使用。我还介绍了一些工具,这些工具可能有助于在统计和机器学习应用中使用分类数据。

背景和动机

通常,表中的一列可能包含较小一组不同值的重复实例。我们已经看到了像 uniquevalue_counts 这样的函数,它们使我们能够从数组中提取不同的值并分别计算它们的频率:

In [199]: values = pd.Series(['apple', 'orange', 'apple',
 .....:                     'apple'] * 2)

In [200]: values
Out[200]: 
0     apple
1    orange
2     apple
3     apple
4     apple
5    orange
6     apple
7     apple
dtype: object

In [201]: pd.unique(values)
Out[201]: array(['apple', 'orange'], dtype=object)

In [202]: pd.value_counts(values)
Out[202]: 
apple     6
orange    2
Name: count, dtype: int64

许多数据系统(用于数据仓库、统计计算或其他用途)已经开发了专门的方法来表示具有重复值的数据,以实现更高效的存储和计算。在数据仓库中,最佳实践是使用所谓的维度表,其中包含不同的值,并将主要观察结果存储为引用维度表的整数键:

In [203]: values = pd.Series([0, 1, 0, 0] * 2)

In [204]: dim = pd.Series(['apple', 'orange'])

In [205]: values
Out[205]: 
0    0
1    1
2    0
3    0
4    0
5    1
6    0
7    0
dtype: int64

In [206]: dim
Out[206]: 
0     apple
1    orange
dtype: object

我们可以使用take方法恢复原始的字符串 Series:

In [207]: dim.take(values)
Out[207]: 
0     apple
1    orange
0     apple
0     apple
0     apple
1    orange
0     apple
0     apple
dtype: object

这种整数表示被称为分类字典编码表示。不同值的数组可以称为数据的类别字典级别。在本书中,我们将使用术语分类类别。引用类别的整数值称为类别代码或简称代码

在进行分析时,分类表示可以显著提高性能。您还可以在保持代码不变的情况下对类别执行转换。一些可以以相对较低的成本进行的示例转换包括:

  • 重命名类别

  • 追加一个新类别而不改变现有类别的顺序或位置

pandas 中的分类扩展类型

pandas 具有专门的Categorical扩展类型,用于保存使用基于整数的分类表示或编码的数据。这是一种流行的数据压缩技术,适用于具有许多相似值出现的数据,并且可以提供更快的性能和更低的内存使用,特别是对于字符串数据。

让我们考虑之前的示例 Series:

In [208]: fruits = ['apple', 'orange', 'apple', 'apple'] * 2

In [209]: N = len(fruits)

In [210]: rng = np.random.default_rng(seed=12345)

In [211]: df = pd.DataFrame({'fruit': fruits,
 .....:                    'basket_id': np.arange(N),
 .....:                    'count': rng.integers(3, 15, size=N),
 .....:                    'weight': rng.uniform(0, 4, size=N)},
 .....:                   columns=['basket_id', 'fruit', 'count', 'weight'])

In [212]: df
Out[212]: 
 basket_id   fruit  count    weight
0          0   apple     11  1.564438
1          1  orange      5  1.331256
2          2   apple     12  2.393235
3          3   apple      6  0.746937
4          4   apple      5  2.691024
5          5  orange     12  3.767211
6          6   apple     10  0.992983
7          7   apple     11  3.795525

这里,df['fruit']是 Python 字符串对象的数组。我们可以通过调用以下方式将其转换为分类:

In [213]: fruit_cat = df['fruit'].astype('category')

In [214]: fruit_cat
Out[214]: 
0     apple
1    orange
2     apple
3     apple
4     apple
5    orange
6     apple
7     apple
Name: fruit, dtype: category
Categories (2, object): ['apple', 'orange']

现在,fruit_cat的值是pandas.Categorical的一个实例,您可以通过.array属性访问:

In [215]: c = fruit_cat.array

In [216]: type(c)
Out[216]: pandas.core.arrays.categorical.Categorical

Categorical对象具有categoriescodes属性:

In [217]: c.categories
Out[217]: Index(['apple', 'orange'], dtype='object')

In [218]: c.codes
Out[218]: array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8)

可以使用cat访问器更轻松地访问这些,这将在 Categorical Methods 中很快解释。

获取代码和类别之间的映射的一个有用技巧是:

In [219]: dict(enumerate(c.categories))
Out[219]: {0: 'apple', 1: 'orange'}

您可以通过分配转换后的结果将 DataFrame 列转换为分类:

In [220]: df['fruit'] = df['fruit'].astype('category')

In [221]: df["fruit"]
Out[221]: 
0     apple
1    orange
2     apple
3     apple
4     apple
5    orange
6     apple
7     apple
Name: fruit, dtype: category
Categories (2, object): ['apple', 'orange']

您还可以直接从其他类型的 Python 序列创建pandas.Categorical

In [222]: my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])

In [223]: my_categories
Out[223]: 
['foo', 'bar', 'baz', 'foo', 'bar']
Categories (3, object): ['bar', 'baz', 'foo']

如果您从另一个来源获得了分类编码数据,可以使用替代的from_codes构造函数:

In [224]: categories = ['foo', 'bar', 'baz']

In [225]: codes = [0, 1, 2, 0, 0, 1]

In [226]: my_cats_2 = pd.Categorical.from_codes(codes, categories)

In [227]: my_cats_2
Out[227]: 
['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
Categories (3, object): ['foo', 'bar', 'baz']

除非明确指定,分类转换假定类别没有特定的排序。因此,categories数组的顺序可能会根据输入数据的顺序而有所不同。在使用from_codes或任何其他构造函数时,您可以指示类别具有有意义的排序:

In [228]: ordered_cat = pd.Categorical.from_codes(codes, categories,
 .....:                                         ordered=True)

In [229]: ordered_cat
Out[229]: 
['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
Categories (3, object): ['foo' < 'bar' < 'baz']

输出[foo < bar < baz]表示'foo'在排序中位于'bar'之前,依此类推。无序的分类实例可以通过as_ordered变为有序:

In [230]: my_cats_2.as_ordered()
Out[230]: 
['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
Categories (3, object): ['foo' < 'bar' < 'baz']

最后一点,分类数据不一定是字符串,尽管我只展示了字符串示例。分类数组可以由任何不可变的值类型组成。

使用 Categoricals 进行计算

与非编码版本(如字符串数组)相比,在 pandas 中使用Categorical通常表现相同。在处理分类数据时,pandas 的某些部分,如groupby函数,表现更好。还有一些函数可以利用ordered标志。

让我们考虑一些随机数值数据,并使用pandas.qcut分箱函数。这将返回pandas.Categorical;我们在本书的早期使用了pandas.cut,但忽略了分类的工作原理的细节:

In [231]: rng = np.random.default_rng(seed=12345)

In [232]: draws = rng.standard_normal(1000)

In [233]: draws[:5]
Out[233]: array([-1.4238,  1.2637, -0.8707, -0.2592, -0.0753])

让我们计算一下这些数据的四分位数分箱,并提取一些统计数据:

In [234]: bins = pd.qcut(draws, 4)

In [235]: bins
Out[235]: 
[(-3.121, -0.675], (0.687, 3.211], (-3.121, -0.675], (-0.675, 0.0134], (-0.675, 0
.0134], ..., (0.0134, 0.687], (0.0134, 0.687], (-0.675, 0.0134], (0.0134, 0.687],
 (-0.675, 0.0134]]
Length: 1000
Categories (4, interval[float64, right]): [(-3.121, -0.675] < (-0.675, 0.0134] < 
(0.0134, 0.687] <
 (0.687, 3.211]]

尽管有用,确切的样本四分位数可能不如四分位数名称有用于生成报告。我们可以通过qcutlabels参数实现这一点:

In [236]: bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

In [237]: bins
Out[237]: 
['Q1', 'Q4', 'Q1', 'Q2', 'Q2', ..., 'Q3', 'Q3', 'Q2', 'Q3', 'Q2']
Length: 1000
Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']

In [238]: bins.codes[:10]
Out[238]: array([0, 3, 0, 1, 1, 0, 0, 2, 2, 0], dtype=int8)

标记的bins分类不包含数据中的箱边信息,因此我们可以使用groupby来提取一些摘要统计信息:

In [239]: bins = pd.Series(bins, name='quartile')

In [240]: results = (pd.Series(draws)
 .....:            .groupby(bins)
 .....:            .agg(['count', 'min', 'max'])
 .....:            .reset_index())

In [241]: results
Out[241]: 
 quartile  count       min       max
0       Q1    250 -3.119609 -0.678494
1       Q2    250 -0.673305  0.008009
2       Q3    250  0.018753  0.686183
3       Q4    250  0.688282  3.211418

结果中的'quartile'列保留了来自bins的原始分类信息,包括排序:

In [242]: results['quartile']
Out[242]: 
0    Q1
1    Q2
2    Q3
3    Q4
Name: quartile, dtype: category
Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']
分类数据的更好性能

在本节开头,我说过分类类型可以提高性能和内存使用,所以让我们看一些例子。考虑一些具有 1000 万个元素和少量不同类别的 Series:

In [243]: N = 10_000_000

In [244]: labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))

现在我们将labels转换为分类:

In [245]: categories = labels.astype('category')

现在我们注意到labels使用的内存比categories要多得多:

In [246]: labels.memory_usage(deep=True)
Out[246]: 600000128

In [247]: categories.memory_usage(deep=True)
Out[247]: 10000540

当然,转换为类别并不是免费的,但这是一次性的成本:

In [248]: %time _ = labels.astype('category')
CPU times: user 279 ms, sys: 6.06 ms, total: 285 ms
Wall time: 285 ms

由于底层算法使用基于整数的代码数组而不是字符串数组,因此使用分类的 GroupBy 操作可以显着提高性能。这里我们比较了使用 GroupBy 机制的value_counts()的性能:

In [249]: %timeit labels.value_counts()
331 ms +- 5.39 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)

In [250]: %timeit categories.value_counts()
15.6 ms +- 152 us per loop (mean +- std. dev. of 7 runs, 100 loops each)

分类方法

包含分类数据的 Series 具有几个类似于Series.str专门的字符串方法的特殊方法。这也提供了方便访问类别和代码。考虑 Series:

In [251]: s = pd.Series(['a', 'b', 'c', 'd'] * 2)

In [252]: cat_s = s.astype('category')

In [253]: cat_s
Out[253]: 
0    a
1    b
2    c
3    d
4    a
5    b
6    c
7    d
dtype: category
Categories (4, object): ['a', 'b', 'c', 'd']

特殊的访问器属性cat提供了对分类方法的访问:

In [254]: cat_s.cat.codes
Out[254]: 
0    0
1    1
2    2
3    3
4    0
5    1
6    2
7    3
dtype: int8

In [255]: cat_s.cat.categories
Out[255]: Index(['a', 'b', 'c', 'd'], dtype='object')

假设我们知道此数据的实际类别集扩展到数据中观察到的四个值之外。我们可以使用set_categories方法来更改它们:

In [256]: actual_categories = ['a', 'b', 'c', 'd', 'e']

In [257]: cat_s2 = cat_s.cat.set_categories(actual_categories)

In [258]: cat_s2
Out[258]: 
0    a
1    b
2    c
3    d
4    a
5    b
6    c
7    d
dtype: category
Categories (5, object): ['a', 'b', 'c', 'd', 'e']

虽然数据看起来没有改变,但使用它们的操作将反映新的类别。例如,如果存在,value_counts会尊重类别:

In [259]: cat_s.value_counts()
Out[259]: 
a    2
b    2
c    2
d    2
Name: count, dtype: int64

In [260]: cat_s2.value_counts()
Out[260]: 
a    2
b    2
c    2
d    2
e    0
Name: count, dtype: int64

在大型数据集中,分类通常被用作一种方便的工具,用于节省内存和提高性能。在过滤大型 DataFrame 或 Series 之后,许多类别可能不会出现在数据中。为了帮助解决这个问题,我们可以使用remove_unused_categories方法来修剪未观察到的类别:

In [261]: cat_s3 = cat_s[cat_s.isin(['a', 'b'])]

In [262]: cat_s3
Out[262]: 
0    a
1    b
4    a
5    b
dtype: category
Categories (4, object): ['a', 'b', 'c', 'd']

In [263]: cat_s3.cat.remove_unused_categories()
Out[263]: 
0    a
1    b
4    a
5    b
dtype: category
Categories (2, object): ['a', 'b']

请参见表 7.7 列出的可用分类方法。

表 7.7:pandas 中 Series 的分类方法

为建模创建虚拟变量

当您使用统计或机器学习工具时,通常会将分类数据转换为虚拟变量,也称为独热编码。这涉及创建一个 DataFrame,其中每个不同的类别都有一列;这些列包含给定类别的出现为 1,否则为 0。

考虑前面的例子:

In [264]: cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')

如本章前面提到的,pandas.get_dummies函数将这个一维分类数据转换为包含虚拟变量的 DataFrame:

In [265]: pd.get_dummies(cat_s, dtype=float)
Out[265]: 
 a    b    c    d
0  1.0  0.0  0.0  0.0
1  0.0  1.0  0.0  0.0
2  0.0  0.0  1.0  0.0
3  0.0  0.0  0.0  1.0
4  1.0  0.0  0.0  0.0
5  0.0  1.0  0.0  0.0
6  0.0  0.0  1.0  0.0
7  0.0  0.0  0.0  1.0

7.6 结论

有效的数据准备可以通过使您花更多时间分析数据而不是准备分析数据来显着提高生产率。本章中我们探讨了许多工具,但这里的覆盖范围并不全面。在下一章中,我们将探讨 pandas 的连接和分组功能。

02-04 23:21