10.向量化字符串操作

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 &amp; Brownie Layer Cake with Whipped Cream &amp; 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提供的工具可以帮我们高效的做这些工作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,893评论 2 89
  • 今天晚上回家,我和老婆吵架了,我因为有些事情没有做好,而和老婆辩论,结果我老婆就生气了。 从这件事情中,我吸取的教...
    高刚高刚阅读 129评论 0 0
  • 20170721 小面包2个月16天 宝宝一边在我怀里喝奶,我一边在跟孩子爸爸说话,小面包以为我在跟她说呢,停下喝...
    小面包妈妈阅读 191评论 0 0
  • “目的”这个词在我的概念里,应该是褒贬各半吧,在当今这个社会,好像我们所做的每一件事,都应该带有一定的目的性,然后...
    菜菜_七台河阅读 209评论 0 3