使用 Python 的一个优势就是字符串处理起来比较容易。在此基础上创建的 Pandas 同样提供了一系列向量化字符串操作(vectorized string operation),它们都是在处理(清洗)现实工作中的数据时不可或缺的功能。在这一节中,我们将介绍 Pandas 的字符串操作,学习如何用它们对一个从网络采集来的杂乱无章的数据集进行局部清理。
前面的章节已经介绍过如何用 NumPy 和 Pandas 进行一般的运算操作,因此我们也能简便快速地对多个数组元素执行同样的操作,例如:
import numpy as np x = np.array([2, 3, 5, 7, 11, 13]) x * 2
array([ 4, 6, 10, 14, 22, 26])
向量化操作简化了纯数值的数组操作语法——我们不需要再担心数组的长度或维度,只需要关心需要的操作。然而,由于 NumPy 并没有为字符串数组提供简单的接口,因此需要通过繁琐的 for 循环来解决问题:
data = ['peter', 'Paul', 'MARY', 'gUIDO'] [s.capitalize() for s in data]
['Peter', 'Paul', 'Mary', 'Guido']
虽然这么做对于某些数据可能是有效的,但是假如数据中出现了缺失值,那么这样做就会引起异常,例如:
data = ['peter', 'Paul', None, 'MARY', 'gUIDO'] [s.capitalize() for s in data]
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last)in () 1 data = ['peter', 'Paul', None, 'MARY', 'gUIDO'] ----> 2 [s.capitalize() for s in data] in (.0) 1 data = ['peter', 'Paul', None, 'MARY', 'gUIDO'] ----> 2 [s.capitalize() for s in data] AttributeError: 'NoneType' object has no attribute 'capitalize'
Pandas 为包含字符串的 Series 和 Index 对象提供的 str 属性堪称两全其美的方法,它既可以满足向量化字符串操作的需求,又可以正确地处理缺失值。例如,我们用前面的数据 data 创建了一个 Pandas 的 Series:
import pandas as pd names = pd.Series(data) names
0 peter 1 Paul 2 None 3 MARY 4 gUIDO dtype: object
现在就可以直接调用转换大写方法 capitalize() 将所有的字符串变成大写形式,缺失值会被跳过:
names.str.capitalize()
0 Peter 1 Paul 2 None 3 Mary 4 Guido dtype: object
在 str 属性后面用 Tab 键,可以看到 Pandas 支持的所有向量化字符串方法。
如果你熟悉 Python 的字符串方法的话,就会发现 Pandas 绝大多数的字符串语法都很直观,甚至可以列成一个表格。在深入论述后面的内容之前,让我们先从这一步开始。这一节的示例将采用一些人名来演示:
monte = pd.Series(['Graham Chapman', 'John Cleese', 'Terry Gilliam', 'Eric Idle', 'Terry Jones', 'Michael Palin'])
几乎所有 Python 内置的字符串方法都被复制到 Pandas 的向量化字符串方法中。下面的表格列举了 Pandas 的 str 方法借鉴 Python 字符串方法的内容:
方法 | 方法 | 方法 | 方法 |
---|---|---|---|
len() |
lower() |
translate() |
islower() |
ljust() |
upper() |
startswith() |
isupper() |
rjust() |
find() |
endswith() |
isnumeric() |
center() |
rfind() |
isalnum() |
isdecimal() |
zfill() |
index() |
isalpha() |
split() |
strip() |
rindex() |
isdigit() |
rsplit() |
rstrip() |
capitalize() |
isspace() |
partition() |
lstrip() |
swapcase() |
istitle() |
rpartition() |
需要注意的是,这些方法的返回值不同,例如 lower() 方法返回一个字符串 Series:
monte.str.lower()
0 graham chapman 1 john cleese 2 terry gilliam 3 eric idle 4 terry jones 5 michael palin dtype: object
但是有些方法返回数值:
monte.str.len()
0 14 1 11 2 13 3 9 4 11 5 13 dtype: int64
有些方法返回布尔值:
monte.str.startswith('T')
0 False 1 False 2 True 3 False 4 True 5 False dtype: bool
还有些方法返回列表或其他复合值:
monte.str.split()
0 [Graham, Chapman] 1 [John, Cleese] 2 [Terry, Gilliam] 3 [Eric, Idle] 4 [Terry, Jones] 5 [Michael, Palin] dtype: object
在接下来的内容中,我们将进一步学习这类由列表元素构成的 Series(series-of-lists)对象。
还有一些支持正则表达式的方法可以用来处理每个字符串元素。表中的内容是 Pandas 向量化字符串方法根据 Python 标准库的 re 模块函数实现的 API。
方法 | 描述 |
---|---|
match() | 对每个元素调用 re.match(),返回布尔类型值 |
extract() | 对每个元素调用 re.match(),返回匹配的字符串组(groups) |
findall() | 对每个元素调用 re.findall() |
replace() | 用正则模式替换字符串 |
contains() | 对每个元素调用 re.search(),返回布尔类型值 |
count() | 计算符合正则模式的字符串的数量 |
split() | 等价于 str.split(),支持正则表达式 |
rsplit() | 等价于 str.rsplit(),支持正则表达式 |
通过这些方法,你就可以实现各种有趣的操作了。例如,可以提取元素前面的连续字母作为每个人的名字(first name):
monte.str.extract('([A-Za-z]+)', expand=False)
0 Graham 1 John 2 Terry 3 Eric 4 Terry 5 Michael dtype: object
我们还能实现更复杂的操作,例如找出所有开头和结尾都是辅音字母的名字——这可以用正则表达式中的开始符号(^)与结尾符号($)来实现:
monte.str.findall(r'^[^AEIOU].*[^aeiou]$')
0 [Graham Chapman] 1 [] 2 [Terry Gilliam] 3 [] 4 [Terry Jones] 5 [Michael Palin] dtype: object
能将正则表达式应用到 Series 与 DataFrame 之中的话,就有可能实现更多的数据分析与清洗方法。
还有其他一些方法也可以实现方便的操作(如表所示)。
方法 | 描述 |
---|---|
get() | 获取元素索引位置上的值,索引从 0 开始 |
slice() | 对元素进行切片取值 |
slice_replace() | 对元素进行切片替换 |
cat() | 连接字符串(此功能比较复杂,建议阅读文档) |
repeat() | 重复元素 |
normalize() | 将字符串转换为 Unicode 规范形式 |
pad() | 在字符串的左边、右边或两边增加空格 |
wrap() | 将字符串按照指定的宽度换行 |
join() | 用分隔符连接 Series 的每个元素 |
get_dummies() | 按照分隔符提取每个元素的 dummy 变量, |
.. | 转换为独热(one-hot)编码的 DataFrame |
(1) 向量化字符串的取值与切片操作。这里需要特别指出的是,get() 与 slice() 操作可以从每个字符串数组中获取向量化元素。例如,我们可以通过 str.slice(0, 3) 获取每个字符串数组的前三个字符。通过 Python 的标准取值方法也可以取得同样的效果,例如 df.str.slice(0, 3) 等价于 df.str[0:3]:
monte.str[0:3]
0 Gra 1 Joh 2 Ter 3 Eri 4 Ter 5 Mic dtype: object
df.str.get(i) 与 df.str[i] 的按索引取值效果类似。
get() 与 slice() 操作还可以在 split() 操作之后使用。例如,要获取每个姓名的姓(last name),可以结合使用 split() 与 get():
monte.str.split().str.get(-1)
0 Chapman 1 Cleese 2 Gilliam 3 Idle 4 Jones 5 Palin dtype: object
(2) 指标变量。另一个需要多花点儿时间解释的是 get_dummies() 方法。当你的数据有一列包含了若干已被编码的指标(coded indicator)时,这个方法就能派上用场了。例如,假设有一个包含了某种编码信息的数据集,如 A= 出生在美国、B= 出生在英国、C= 喜欢奶酪、D= 喜欢午餐肉:
full_monte = pd.DataFrame({'name': monte, 'info': ['B|C|D', 'B|D', 'A|C', 'B|D', 'B|C', 'B|C|D']}) full_monte
name | info | |
---|---|---|
0 | Graham Chapman | B|C|D |
1 | John Cleese | B|D |
2 | Terry Gilliam | A|C |
3 | Eric Idle | B|D |
4 | Terry Jones | B|C |
5 | Michael Palin | B|C|D |
get_dummies() 方法可以让你快速将这些指标变量分割成一个独热编码的 DataFrame(每个元素都是 0 或 1):
full_monte['info'].str.get_dummies('|')
A | B | C | D | |
---|---|---|---|---|
0 | 0 | 1 | 1 | 1 |
1 | 0 | 1 | 0 | 1 |
2 | 1 | 0 | 1 | 0 |
3 | 0 | 1 | 0 | 1 |
4 | 0 | 1 | 1 | 0 |
5 | 0 | 1 | 1 | 1 |
通过 Pandas 自带的这些字符串操作方法,你就可以建立一个功能无比强大的字符串处理程序来清洗自己的数据了。
虽然本书将不再继续介绍这些方法,但是希望你仔细阅读 Pandas 在线文档中“Working with Text Data”(http://pandas.pydata.org/pandas-docs/stable/text.html)节,或者阅读 3.14 节的相关资源。
略