Python的一个强项是它可以相对简单处理和操纵字符数据。Pandas基于Python构建,并且提供了向量化字符串操作的复杂集合。这些操作在使用(清理)真实世界数据时,是必不可少的。在本章,浏览一些Pandas字符串操作,然后看看如何使用它们来清理从互联网上收集来的杂乱无章数据集。
介绍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并没有提供同样简洁的方法,因此你不得不使用更冗长的循环语法:
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)
<ipython-input-3-fc1d891ab539> in <module>()
1 data = ['peter', 'Paul', None, 'MARY', 'gUIDO']
----> 2 [s.capitalize() for s in data]
<ipython-input-3-fc1d891ab539> in <listcomp>(.0)
1 data = ['peter', 'Paul', None, 'MARY', 'gUIDO']
----> 2 [s.capitalize() for s in data]
AttributeError: 'NoneType' object has no attribute 'capitalize'
Pandas包含解决,字符串矢量化操作需求以及通过Pandas Series和索引对象的str属性来处理缺失数据,这些问题的功能。举例如下:假设我们创建一个使用这个数据的Pandas Series:
import pandas as pd
names = pd.Series(data)
names
0 peter
1 Paul
2 None
3 MARY
4 gUIDO
dtype: object
现在我们可以调用一个方法来对所有条目的首字母大写,它还可以忽略任何缺失的数据:
names.str.capitalize()
0 Peter
1 Paul
2 None
3 Mary
4 Guido
dtype: object
在str属性后使用TAB键,将会列出Pandas上所有可用的矢量化字符串方法。
字符串方法列表
如果你已经对Python中字符串操作有很好的掌握,大多数Pandas字符串语法足够直观,以至于只是把这些方法列出来就行了;在深入的辨别细微差别之前,我们将从这里开始。本章的用例使用下面的Series名称:
monte = pd.Series(['Graham Chapman', 'John Cleese', 'Terry Gilliam',
'Eric Idle', 'Terry Jones', 'Michael Palin'])
同Python类似的字符串方法
几乎所有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对象的操作.
使用正则表达是方法
另外有几种方法接受正则表达式来检查每个字符元素的内容,它们Python内置re模块的接口约定:
Method | Description |
---|---|
match() |
Call re.match() on each element, returning a boolean. |
extract() |
Call re.match() on each element, returning matched groups as strings. |
findall() |
Call re.findall() on each element |
replace() |
Replace occurrences of pattern with some other string |
contains() |
Call re.search() on each element, returning a boolean |
count() |
Count occurrences of pattern |
split() |
Equivalent to str.split() , but accepts regexps |
rsplit() |
Equivalent to str.rsplit() , but accepts regexps |
使用它们,你可以做许多有趣的操作。例如,我们可以通过在每个元素的开头请求一个连续的字符组 ,来从元素中提取第一个名字:
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条目上的能力,为数据分析和清理开发出许多新东西。
其它方法
最后,有一些杂项方法可以一些操作很方便:
Method | Description |
---|---|
get() |
Index each element |
slice() |
Slice each element |
slice_replace() |
Replace slice in each element with passed value |
cat() |
Concatenate strings |
repeat() |
Repeat values |
normalize() |
Return Unicode form of string |
pad() |
Add whitespace to left, right, or both sides of strings |
wrap() |
Split long strings into lines with length less than a given width |
join() |
Join strings in each element of the Series with passed separator |
get_dummies() |
extract dummy variables as a dataframe |
向量化条目访问和切片
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()返回的结果上。例如,为了抽取每个条目的最后一个名字,我们可以结合split() 和get():
monte.str.split().str.get(-1)
0 Chapman
1 Cleese
2 Gilliam
3 Idle
4 Jones
5 Palin
dtype: object
指示符变量
另一个需要额外解释的方法是get_dummies()。当你的数据中某行包含某种编码指示符时,这个方法很有用。例如,我们可能有个数据集包含某种编码的信息,比如: 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
info name
0 B|C|D Graham Chapman
1 B|D John Cleese
2 A|C Terry Gilliam
3 B|D Eric Idle
4 B|C Terry Jones
5 B|C|D Michael Palin
get_dummies()函数让你快速的将这些指示符分解到DataFrame中
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在线文档"Working with Text Data" ,或者参考Further Resources.
例子:菜谱数据库
这些矢量化字符串操作在处理混乱的,真实世界的数据时非常有用。这里我将通过一个例子,使用一个数据来自于网络上不同源的开放菜谱数据库。我们的目标时分解菜谱数据为成分列表,这样我们可以基于我们手头有的某些成分快速的找到一个菜谱。
用于编译的脚本可以在 https://github.com/fictivekin/openrecipes 找到,数据库当前版本的链接也可以在哪里发现。
截止2016年春,数据库大小时30M,使用如下命令下载并解压:
# !curl -O http://openrecipes.s3.amazonaws.com/recipeitems-latest.json.gz
# !gunzip recipeitems-latest.json.gz
数据库是JSON格式的,所以我们尝试用pd.read_json来读取:
try:
recipes = pd.read_json('recipeitems-latest.json')
except ValueError as e:
print("ValueError:", e)
ValueError: Trailing data
我们得到有一个ValueError: Trailing data。在网上搜索这个错误串,这看起来时由于文件中的每行本身时合法的JSON,但整个文件却不合法。让我们查一下解释是否正确:
with open('recipeitems-latest.json') as f:
line = f.readline()
pd.read_json(line).shape
(2, 12)
的确是那样,显然每行是有效的JSON,所以我们需要将每行整理在一起。一个办法是实际的构建字符串表达式包含所有的JSON条目,然后使用pd.read_json加载全部:
# read the entire file into a Python array
with open('recipeitems-latest.json', 'r') as f:
# Extract each line
data = (line.strip() for line in f)
# Reformat so each line is the element of a list
data_json = "[{0}]".format(','.join(data))
# read the result as a JSON
recipes = pd.read_json(data_json)
recipes.shape
(173278, 17)
可以看到有接近200,000菜谱,17列。让我们看一列,里面有什么:
recipes.iloc[0]
_id {'$oid': '5160756b96cc62079cc2db15'}
cookTime PT30M
creator NaN
dateModified NaN
datePublished 2013-03-11
description Late Saturday afternoon, after Marlboro Man ha...
image http://static.thepioneerwoman.com/cooking/file...
ingredients Biscuits\n3 cups All-purpose Flour\n2 Tablespo...
name Drop Biscuits and Sausage Gravy
prepTime PT10M
recipeCategory NaN
recipeInstructions NaN
recipeYield 12
source thepioneerwoman
totalTime NaN
ts {'$date': 1365276011104}
url http://thepioneerwoman.com/cooking/2013/03/dro...
Name: 0, dtype: object
那里有许多信息,但大部分是乱七八糟的,典型的从网上抓起的数据。特别的,成分列是字符串格式的;我们不得不小心的抽取我们感兴趣的信息。让我们先仔细看看这些成分:
recipes.ingredients.str.len().describe()
count 173278.000000
mean 244.617926
std 146.705285
min 0.000000
25% 147.000000
50% 221.000000
75% 314.000000
max 9067.000000
Name: ingredients, dtype: float64
成分列表的平均长度是250个字符,最小为0,最大接近1000个字符!
出于好奇,让我们看看哪种食谱有最长的成分表:
recipes.name[np.argmax(recipes.ingredients.str.len())]
'Carrot Pineapple Spice & Brownie Layer Cake with Whipped Cream & Cream Cheese Frosting and Marzipan Carrots'
这看起来确实是一个复杂难懂的菜谱。
我们可以做些其它聚合探索;比如,让我们可看看由多少用于早餐的食谱:
recipes.description.str.contains('[Bb]reakfast').sum()
3524
由多少食谱成分中含有肉桂:
recipes.ingredients.str.contains('[Cc]innamon').sum()
10526
我们甚至可以查看是否由食谱将该成分错误拼写成"cinamon"
recipes.ingredients.str.contains('[Cc]inamon').sum()
11
使用Pandas字符串工具使这样类型的数据探索成为可能。像这样的数据清理本来就是Python的所擅长的。
简易食谱推荐系统
让我们深入一步,开始做一个简易食谱推荐系统:给一个成分列表,找出所有使用这些成分的菜谱。虽然概念上很简单,但有用数据的异质性,任务很复杂。例如,并没有一个简单的操作可以从每行中抽取一个干净的成分列表。所有我们稍微做个弊:我们使用常用的成分列表,并且简单查询它们是否在每个菜谱的成分列表中。为了简单起见,让我们暂时用草药和调味品:
spice_list = ['salt', 'pepper', 'oregano', 'sage', 'parsley',
'rosemary', 'tarragon', 'thyme', 'paprika', 'cumin']
然后我们构建一个包含True和False值的布尔型DataFrame,它们用来表明这种成分是否存在于列表中:
import re
spice_df = pd.DataFrame(dict((spice, recipes.ingredients.str.contains(spice, re.IGNORECASE))
for spice in spice_list))
spice_df.head()
cumin oregano paprika parsley pepper rosemary sage salt tarragon thyme
0 False False False False False False True False False False
1 False False False False False False False False False False
2 True False False False True False False True False False
3 False False False False False False False False False False
4 False False False False False False False False False False
举个例子,假如我们想要查找使用parsley, paprika,和tarragon的菜谱。我们可以使用DataFrames的query()方法快速的找到它:
selection = spice_df.query('parsley & paprika & tarragon')
len(selection)
10
我们找到了10个带有这些成分的菜谱;让我们使用返回的选择索引来看看那些菜谱的名称
recipes.name[selection.index]
2069 All cremat with a Little Gem, dandelion and wa...
74964 Lobster with Thermidor butter
93768 Burton's Southern Fried Chicken with White Gravy
113926 Mijo's Slow Cooker Shredded Beef
137686 Asparagus Soup with Poached Eggs
140530 Fried Oyster Po’boys
158475 Lamb shank tagine with herb tabbouleh
158486 Southern fried chicken in buttermilk
163175 Fried Chicken Sliders with Pickles + Slaw
165243 Bar Tartine Cauliflower Salad
Name: name, dtype: object
现在我们已经把我们的食谱选择缩小了将近20000分之一,所以我们可以做出更明智的决定,决定我们晚餐要做什么。
比食谱更远
希望这个例子可以给你一点关于关于Pandas字符串方法如何有效地进行数据清理操的感觉。当然,建立一个稳健的菜谱推荐系统需要更多的工作。从每个菜单中提取成分列表需要大量的工作;不幸的是,各种格式的使用是这个清理过程相当耗时。这指出了数据科学中的真相,真实世界数据的整洁和清理工作通常占据大部分时间,而Pandas提供的工具可以帮我们高效的做这些工作。