数据科学 IPython 笔记本 7.13 向量化字符串操作

7.13 向量化字符串操作

原文:Vectorized String Operations

译者:飞龙

协议:CC BY-NC-SA 4.0

本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。

Python 的一个优点是它在处理和操作字符串数据方面相对容易。Pandas 构建于此之上,并提供了一套全面的向量化字符串操作,它们成为处理(阅读“清理”部分)实际数据时所需的重要部分。在本节中,我们将介绍一些 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 SeriesIndex对象的str属性,来正确处理缺失数据。因此,例如,假设我们使用以下数据创建 Pandas 序列:

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属性上使用制表符补全,将列出 Pandas 可用的所有向量化字符串方法。

Pandas 字符串方法的表格

如果你对 Python 中的字符串操作有很好的理解,那么大多数 Pandas 字符串语法都足够直观,只需列出一个可用方法表即可。 我们将从这里开始,然后深入探讨一些细微之处。本节中的示例使用以下名称序列:

monte = pd.Series(['Graham Chapman', 'John Cleese', 'Terry Gilliam',
                   'Eric Idle', 'Terry Jones', 'Michael Palin'])

相似于 Python 字符串方法的方法

几乎所有 Python 的内置字符串方法都对应了 Pandas 向量化字符串方法。 这是一个对应 Python 字符串方法的 Pandas str方法列表:

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()那样返回字符串序列:

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
'''

在我们继续讨论的过程中,我们将看到这种列表序列对象的进一步操作。

使用正则表达式的方法

此外,有几种方法可以接受正则表达式,来检查每个字符串元素的内容,并遵循 Python 内置的re模块的一些 API 约定:

方法 描述
match() 在每个元素上调用re.match(),返回布尔值
extract() 在每个元素上调用re.match(),返回作为字符串的每个分组
findall() 在每个元素上调用re.findall()
replace() 将模式串的每次出现替换为一些其它字符串
contains() 在每个元素上调用re.search(),返回布尔值
count() 统计模式串的出现次数
split() 等价于str.split(),但是接受正则表达式
rsplit() 等价于str.rsplit(),但是接受正则表达式

有了这些,你可以进行各种有趣的操作。例如,我们可以提取每个元素的名字,通过在每个元素的开头要求一组连续字符:

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
'''

SeriesDataframe条目中简洁地应用正则表达式的能力,为分析和清理数据提供了许多可能性。

杂项方法

最后,有一些杂项方法可以执行其他方便的操作:

方法 描述
get() 索引每个元素
slice() 对每个元素切片
slice_replace() 用传递的值替换每个元素的切片
cat() 连接字符串
repeat() 重复值
normalize() 返回字符串的 Unicode 形式
pad() 在字符串的左侧,右侧或两侧添加空格
wrap() 将长字符串拆分为长度小于给定宽度的行
join() 使用传递的分隔符连接每个元素中的字符串
get_dummies() 将虚拟变量提取为数据帧

向量化的项目访问和切片

特别是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 在线文档中的“处理文本数据”,或参考“更多资源”中列出的资源。

示例:食谱数据库

在清理凌乱的真实数据的过程中,这些向量化字符串操作变得最有用。
在这里,我将使用从 Web 上的各种来源编译的开放式食谱数据库,来说明这一点。
我们的目标是,将食谱数据解析为成分列表,这样我们就可以根据手头的一些成分,快速找到配方。

用于编译它的脚本可以在 https://github.com/fictivekin/openrecipes 找到,同时也可以找到当前版本数据库的链接。

从 2016 年春季开始,这个数据库大约 30MB,可以使用以下命令下载和解压缩:

# !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,提到有“尾随数据”。在互联网上搜索此错误的文本,似乎是由于使用了一个文件,其中每行本身是一个有效的 JSON,但完整文件不是。让我们检查一下这种解释是否正确:

with open('recipeitems-latest.json') as f:
    line = f.readline()
pd.read_json(line).shape

# (2, 12)

是的,显然每一行都是有效的 JSON,所以我们需要将它们串在一起。我们可以这样做的一种方法是,实际构造一个包含所有这些 JSON 条目的字符串表示,然后用pd.read_json加载整个东西:

# 将整个文件读入 Python 数组中
with open('recipeitems-latest.json', 'r') as f:
    # 提取每一行
    data = (line.strip() for line in f)
    # 重新格式化,使每一行是列表的元素
    data_json = "[{0}]".format(','.join(data))
# 将结果读取为 JSON
recipes = pd.read_json(data_json)

recipes.shape

# (173278, 17)

我们看到有近 20 万个食谱和 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
'''

这里有很多信息,但其中很多都是非常混乱的形式,就像从 Web 上抓取的数据一样。特别是,成分列表是字符串格式;我们将不得不仔细提取我们感兴趣的信息。让我们首先仔细看看成分:

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,最多为 10,000 个字符!出于好奇,让我们看看哪个食谱有最长的成分列表:

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']

然后我们可以构建一个由TrueFalse值组成的布尔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)的食谱。我们可以使用DataFramequery()方法快速计算,在“高性能 Pandas:eval()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
'''

现在我们已经将食谱选择范围缩小了近 2 万倍,我们能够在晚餐时做出更明智的决定。

进一步探索食谱

希望这个例子为你提供了一些能在 Pandas 字符串方法中有效使用的数据清理操作类型。当然,建立一个非常强大的食谱推荐系统需要更多的工作!

从每个食谱中提取完整的成分列表,是该任务的重要部分;遗憾的是,各种所使用格式使得这是一个相对耗时的过程。这表明,在数据科学中,清理和修改现实世界的数据通常包含大部分工作,而 Pandas 提供的工具可以帮助你有效地完成这项工作。

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

推荐阅读更多精彩内容