提交 dee930c4 编写于 作者: W wizardforcel

7.13-7.15

上级 5efc59d2
# 7.13 向量化字符串操作
> 原文:[Vectorized String Operations](https://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/pandas/03.10-Working-With-Strings.ipynb)
>
> 译者:[飞龙](https://github.com/wizardforcel)
>
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
>
> 本节是[《Python 数据科学手册》](https://github.com/jakevdp/PythonDataScienceHandbook)(Python Data Science Handbook)的摘录。
Python 的一个优点是它在处理和操作字符串数据方面相对容易。Pandas 构建于此之上,并提供了一套全面的向量化字符串操作,它们成为处理(阅读“清理”部分)实际数据时所需的重要部分。在本节中,我们将介绍一些 Pandas 字符串操作,然后使用它们来部分清理从互联网收集的,非常混乱的食谱数据集。
## Pandas 字符串操作简介
我们在前面的部分中看到,NumPy 和 Pandas 等工具如何扩展算术运算,使我们可以在许多数组元素上轻松快速地执行相同的操作。 例如:
```py
import numpy as np
x = np.array([2, 3, 5, 7, 11, 13])
x * 2
# array([ 4, 6, 10, 14, 22, 26])
```
这种向量化操作简化了操作数据数组的语法:我们不再需要担心数组的大小或形状,而只需要关心我们想要做什么操作。对于字符串数组,NumPy 不提供这样简单的访问,因此你使用更详细的循环语法:
```py
data = ['peter', 'Paul', 'MARY', 'gUIDO']
[s.capitalize() for s in data]
# ['Peter', 'Paul', 'Mary', 'Guido']
```
这可能足以处理一些数据,但如果有任何缺失值,它将会中断。例如:
```py
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``Index`对象的``str``属性,来正确处理缺失数据。因此,例如,假设我们使用以下数据创建 Pandas 序列:
```py
import pandas as pd
names = pd.Series(data)
names
'''
0 peter
1 Paul
2 None
3 MARY
4 gUIDO
dtype: object
'''
```
我们现在可以调用一个方法来大写所有条目,同时跳过任何缺失值:
```py
names.str.capitalize()
'''
0 Peter
1 Paul
2 None
3 Mary
4 Guido
dtype: object
'''
```
在此``str``属性上使用制表符补全,将列出 Pandas 可用的所有向量化字符串方法。
## Pandas 字符串方法的表格
如果你对 Python 中的字符串操作有很好的理解,那么大多数 Pandas 字符串语法都足够直观,只需列出一个可用方法表即可。 我们将从这里开始,然后深入探讨一些细微之处。本节中的示例使用以下名称序列:
```py
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()``那样返回字符串序列:
```py
monte.str.lower()
'''
0 graham chapman
1 john cleese
2 terry gilliam
3 eric idle
4 terry jones
5 michael palin
dtype: object
'''
```
但是一些返回数字:
```py
monte.str.len()
'''
0 14
1 11
2 13
3 9
4 11
5 13
dtype: int64
'''
```
或布尔值:
```py
monte.str.startswith('T')
'''
0 False
1 False
2 True
3 False
4 True
5 False
dtype: bool
'''
```
还有一些为每个元素返回列表或其他复合值:
```py
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()``,但是接受正则表达式 |
有了这些,你可以进行各种有趣的操作。例如,我们可以提取每个元素的名字,通过在每个元素的开头要求一组连续字符:
```py
monte.str.extract('([A-Za-z]+)', expand=False)
'''
0 Graham
1 John
2 Terry
3 Eric
4 Terry
5 Michael
dtype: object
'''
```
或者我们可以做一些更复杂的事情,比如查找所有以辅音开头和结尾的名字,利用字符串开头(``^``)和字符串结尾(``$``)正则表达式字符:
```py
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()`` | 索引每个元素 |
| ``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]``
```py
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()``
```py
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`是“喜欢垃圾邮件”:
```py
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&#124;C&#124;D | Graham Chapman |
| 1 | B&#124;D | John Cleese |
| 2 | A&#124;C | Terry Gilliam |
| 3 | B&#124;D | Eric Idle |
| 4 | B&#124;C | Terry Jones |
| 5 | B&#124;C&#124;D | Michael Palin |
``get_dummies()``例程允许你快速将这些指示符变量拆分为``DataFrame``
```py
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 在线文档中的[“处理文本数据”](http://pandas.pydata.org/pandas-docs/stable/text.html),或参考“更多资源”中列出的资源。
## 示例:食谱数据库
在清理凌乱的真实数据的过程中,这些向量化字符串操作变得最有用。
在这里,我将使用从 Web 上的各种来源编译的开放式食谱数据库,来说明这一点。
我们的目标是,将食谱数据解析为成分列表,这样我们就可以根据手头的一些成分,快速找到配方。
用于编译它的脚本可以在 <https://github.com/fictivekin/openrecipes> 找到,同时也可以找到当前版本数据库的链接。
从 2016 年春季开始,这个数据库大约 30MB,可以使用以下命令下载和解压缩:
```py
# !curl -O http://openrecipes.s3.amazonaws.com/recipeitems-latest.json.gz
# !gunzip recipeitems-latest.json.gz
```
数据库采用 JSON 格式,因此我们将尝试``pd.read_json``来读取它:
```py
try:
recipes = pd.read_json('recipeitems-latest.json')
except ValueError as e:
print("ValueError:", e)
'''
ValueError: Trailing data
'''
```
哦!我们得到了`ValueError`,提到有“尾随数据”。在互联网上搜索此错误的文本,似乎是由于使用了一个文件,其中每行本身是一个有效的 JSON,但完整文件不是。让我们检查一下这种解释是否正确:
```py
with open('recipeitems-latest.json') as f:
line = f.readline()
pd.read_json(line).shape
# (2, 12)
```
是的,显然每一行都是有效的 JSON,所以我们需要将它们串在一起。我们可以这样做的一种方法是,实际构造一个包含所有这些 JSON 条目的字符串表示,然后用``pd.read_json``加载整个东西:
```py
# 将整个文件读入 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 个列。让我们看看一行,看看我们有什么:
```py
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 上抓取的数据一样。特别是,成分列表是字符串格式;我们将不得不仔细提取我们感兴趣的信息。让我们首先仔细看看成分:
```py
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 个字符!出于好奇,让我们看看哪个食谱有最长的成分列表:
```py
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'
```
当然看起来像复杂的食谱。
我们可以执行其他聚合探索;例如,让我们看看有多少食谱是早餐食品:
```py
recipes.description.str.contains('[Bb]reakfast').sum()
# 3524
```
或者有多少食谱将肉桂列为成分:
```py
recipes.ingredients.str.contains('[Cc]innamon').sum()
# 10526
```
我们甚至可以看看,是否有任何食谱将这种成分拼错为`cinamon`
```py
recipes.ingredients.str.contains('[Cc]inamon').sum()
# 11
```
这是使用 Pandas 字符串工具可以实现的基本数据探索类型。这是 Python 真正擅长的数据整理。
### 一个简单的食谱推荐器
让我们再进一步,开始研究一个简单的食谱推荐系统:给出成分列表,找到使用所有这些成分的食谱。虽然概念上很简单,但由于数据的异质性,任务变得复杂:例如,从每一行中提取干净的成分列表并不容易。
所以我们用一些手段:我们先从一系列常见成分开始,然后仅仅搜索它们是否在每个配方的成分列表中。为简单起见,我们暂时仅仅使用草药和香料:
```py
spice_list = ['salt', 'pepper', 'oregano', 'sage', 'parsley',
'rosemary', 'tarragon', 'thyme', 'paprika', 'cumin']
```
然后我们可以构建一个由`True``False`值组成的布尔`DataFrame`,指示该成分是否出现在列表中:
```py
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)的食谱。我们可以使用``DataFrame````query()``方法快速计算,在“高性能 Pandas:``eval()````query()``”中讨论:
```py
selection = spice_df.query('parsley & paprika & tarragon')
len(selection)
# 10
```
我们发现这种组合只有 10 种食谱;让我们使用此选择返回的索引,来发现具有此组合的食谱的名称:
```py
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 提供的工具可以帮助你有效地完成这项工作。
\ No newline at end of file
此差异已折叠。
# 7.15 高性能 Pandas:`eval()``query()`
> 原文:[High-Performance Pandas: `eval()` and `query()`](https://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/pandas/03.12-Performance-Eval-and-Query.ipynb)
>
> 译者:[飞龙](https://github.com/wizardforcel)
>
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
>
> 本节是[《Python 数据科学手册》](https://github.com/jakevdp/PythonDataScienceHandbook)(Python Data Science Handbook)的摘录。
我们在前面的章节中已经看到,PyData 技术栈的力量,建立在 NumPy 和 Pandas 通过直观语法,将基本操作推送到 C 的能力的基础上:例如 NumPy 中的下你给量化/广播操作,以及 Pandas 的分组类型操作。虽然这些抽象对于许多常见用例是高效且有效的,但它们通常依赖于临时中间对象的创建,这可能产生计算时间和内存使用的开销。
从版本 0.13(2014 年 1 月发布)开始,Pandas 包含一些实验性工具,允许你直接访问速度和 C 一样的操作,而无需昂贵的中间数组分配。这些是``eval()````query()``函数,它依赖于 [Numexpr](https://github.com/pydata/numexpr) 包。在这个笔记本中,我们将逐步介绍它们的使用方法,并提供一些何时可以考虑使用它们的经验法则。
## ``query()``和``eval()``的动机:复合表达式
我们以前见过 NumPy 和 Pandas 支持快速向量化操作;例如,相加两个数组的元素时:
```py
import numpy as np
rng = np.random.RandomState(42)
x = rng.rand(1000000)
y = rng.rand(1000000)
%timeit x + y
# 100 loops, best of 3: 3.39 ms per loop
```
正如“NumPy 数组的计算:通用函数”中所讨论的,这比通过 Python 循环或推导式执行加法要快得多:
```py
%timeit np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))
# 1 loop, best of 3: 266 ms per loop
```
但是在计算复合表达式时,这种抽象可能变得不那么有效。例如,请考虑以下表达式:
```py
mask = (x > 0.5) & (y < 0.5)
```
因为 NumPy 会计算每个子表达式,所以大致相当于以下内容:
```py
tmp1 = (x > 0.5)
tmp2 = (y < 0.5)
mask = tmp1 & tmp2
```
换句话说,每个中间步骤都在内存中明确分配。如果``x````y``数组非常大,这可能会产生大量内存和计算开销。Numexpr 库使你能够逐元素计算这种类型的复合表达式,而无需分配完整的中间数组。[Numexpr 文档](https://github.com/pydata/numexpr)有更多细节,但暂时可以说,这个库接受字符串,它提供了你想要计算的 NumPy 风格的表达式:
```py
import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)
# True
```
这里的好处是,Numexpr 以不使用完整临时数组的方式计算表达式,因此可以比 NumPy 更有效,特别是对于大型数组。我们将在这里讨论的 Pandas ``eval()````query()``工具,在概念上是相似的,并且依赖于 Numexpr 包。
## 用于高效操作的``pandas.eval()``
Pandas 中的``eval()``函数接受字符串表达式,来使用``DataFrame``高效地计算操作。例如,考虑以下``DataFrame``
```py
import pandas as pd
nrows, ncols = 100000, 100
rng = np.random.RandomState(42)
df1, df2, df3, df4 = (pd.DataFrame(rng.rand(nrows, ncols))
for i in range(4))
```
要使用典型的 Pandas 方法计算所有四个`DataFrame`的和,我们可以写出总和:
```py
%timeit df1 + df2 + df3 + df4
# 10 loops, best of 3: 87.1 ms per loop
```
通过将表达式构造为字符串,可以通过``pd.eval``计算相同的结果:
```py
%timeit pd.eval('df1 + df2 + df3 + df4')
# 10 loops, best of 3: 42.2 ms per loop
```
这个表达式的``eval()``版本速度提高了约 50%(并且使用的内存更少),同时给出了相同的结果:
```py
np.allclose(df1 + df2 + df3 + df4,
pd.eval('df1 + df2 + df3 + df4'))
# True
```
### ``pd.eval()``所支持的操作
从 Pandas v0.16 开始,``pd.eval()``支持广泛的操作。为了演示这些,我们将使用以下整数``DataFrame``
```py
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))
for i in range(5))
```
#### 算术运算符
``pd.eval()``支持所有算术运算符,例如:
```py
result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2)
# True
```
#### 比较运算符
``pd.eval()``支持所有比较运算符,包括链式表达式:
```py
result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)
# True
```
#### 按位运算符
``pd.eval()``支持``&````|``按位运算符:
```py
result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)
result2 = pd.eval('(df1 < 0.5) & (df2 < 0.5) | (df3 < df4)')
np.allclose(result1, result2)
# True
```
另外,它支持在布尔表达式中使用字面`and``or`
```py
result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)
# True
```
#### 对象属性和索引
``pd.eval()``支持通过``obj.attr``语法访问对象属性,和通过``obj[index]``语法进行索引:
```py
result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(result1, result2)
# True
```
#### 其它运算符
其他操作,如函数调用,条件语句,循环和其他更复杂的结构,目前都没有在``pd.eval()``中实现。如果你想执行这些更复杂的表达式,可以使用 Numexpr 库本身。
## 用于逐列运算的``DataFrame.eval()``
就像 Pandas 有顶级的``pd.eval()``函数一样,``DataFrame````eval()``方法,它的工作方式类似。``eval()``方法的好处是列可以通过名称引用。我们将使用这个带标签的数组作为示例:
```py
df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
df.head()
```
| | A | B | C |
| --- | --- | --- | --- |
| 0 | 0.375506 | 0.406939 | 0.069938 |
| 1 | 0.069087 | 0.235615 | 0.154374 |
| 2 | 0.677945 | 0.433839 | 0.652324 |
| 3 | 0.264038 | 0.808055 | 0.347197 |
| 4 | 0.589161 | 0.252418 | 0.557789 |
使用上面的``pd.eval()``,我们可以像这样使用三列来计算表达式:
```py
result1 = (df['A'] + df['B']) / (df['C'] - 1)
result2 = pd.eval("(df.A + df.B) / (df.C - 1)")
np.allclose(result1, result2)
# True
```
``DataFrame.eval()``方法允许使用列来更简洁地求解表达式:
```py
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result3)
# True
```
请注意,我们将列名称视为要求解的表达式中的变量,结果是我们希望的结果。
### `DataFrame.eval()`中的赋值
除了刚才讨论的选项之外,``DataFrame.eval()``还允许赋值给任何列。让我们使用之前的``DataFrame``,它有列`A``B``C`
```py
df.head()
```
| | A | B | C |
| --- | --- | --- | --- |
| 0 | 0.375506 | 0.406939 | 0.069938 |
| 1 | 0.069087 | 0.235615 | 0.154374 |
| 2 | 0.677945 | 0.433839 | 0.652324 |
| 3 | 0.264038 | 0.808055 | 0.347197 |
| 4 | 0.589161 | 0.252418 | 0.557789 |
我们可以使用``df.eval()``创建一个新列``'D'``并为其赋一个从其他列计算的值:
```py
df.eval('D = (A + B) / C', inplace=True)
df.head()
```
| | A | B | C | D |
| --- | --- | --- | --- | --- |
| 0 | 0.375506 | 0.406939 | 0.069938 | 11.187620 |
| 1 | 0.069087 | 0.235615 | 0.154374 | 1.973796 |
| 2 | 0.677945 | 0.433839 | 0.652324 | 1.704344 |
| 3 | 0.264038 | 0.808055 | 0.347197 | 3.087857 |
| 4 | 0.589161 | 0.252418 | 0.557789 | 1.508776 |
以同样的方式,可以修改任何现有列:
```py
df.eval('D = (A - B) / C', inplace=True)
df.head()
```
| | A | B | C | D |
| --- | --- | --- | --- | --- |
| 0 | 0.375506 | 0.406939 | 0.069938 | -0.449425 |
| 1 | 0.069087 | 0.235615 | 0.154374 | -1.078728 |
| 2 | 0.677945 | 0.433839 | 0.652324 | 0.374209 |
| 3 | 0.264038 | 0.808055 | 0.347197 | -1.566886 |
| 4 | 0.589161 | 0.252418 | 0.557789 | 0.603708 |
### `DataFrame.eval()`中的局部变量
``DataFrame.eval()``方法支持一种额外的语法,可以使用 Python 局部变量。考虑以下:
```py
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)
# True
```
这里的`@`字符标记变量名而不是列名,并允许你高效计算涉及两个“名称空间”的表达式:列的名称空间和 Python 对象的名称空间。请注意,这个`@`字符仅由``DataFrame.eval()``方法支持,不由``pandas.eval()``函数支持,因为``pandas.eval ()``函数只能访问一个(Python)命名空间。
## `DataFrame.query()`方法
``DataFrame``有另一种基于字符串的求值方法,称为``query()``方法。考虑以下:
```py
result1 = df[(df.A < 0.5) & (df.B < 0.5)]
result2 = pd.eval('df[(df.A < 0.5) & (df.B < 0.5)]')
np.allclose(result1, result2)
# True
```
与我们讨论``DataFrame.eval()``时使用的示例一样,这是一个涉及``DataFrame``列的表达式。但是,无法使用``DataFrame.eval()``语法表达它!相反,对于这种类型的过滤操作,你可以使用``query()``方法:
```py
result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)
# True
```
除了作为更有效的计算之外,与掩码表达式相比,这更容易阅读和理解。注意``query()``方法也接受``@``标志来标记局部变量:
```py
Cmean = df['C'].mean()
result1 = df[(df.A < Cmean) & (df.B < Cmean)]
result2 = df.query('A < @Cmean and B < @Cmean')
np.allclose(result1, result2)
# True
```
## 性能:什么时候使用这些函数
在考虑是否使用这些函数时,有两个注意事项:计算时间和内存使用。内存使用是最可预测的方面。 如前所述,涉及 NumPy 数组或 Pandas `DataFrame`的每个复合表达式,都会产生隐式创建的临时数组:例如,这个:
```py
x = df[(df.A < 0.5) & (df.B < 0.5)]
```
大致相当于这个:
```py
tmp1 = df.A < 0.5
tmp2 = df.B < 0.5
tmp3 = tmp1 & tmp2
x = df[tmp3]
```
如果临时``DataFrame`的大小与可用的系统内存(通常是几千兆字节)相比很大,那么使用``eval()``或``query()``表达式是个好主意。你可以使用以下方法检查数组的大致大小(以字节为单位):
```py
df.values.nbytes
# 32000
```
在性能方面,即使你没有超出你的系统内存,``eval()``也会更快。问题是你的临时`DataFrame`与系统上的 L1 或 L2 CPU 缓存的大小相比(2016 年通常为几兆字节)如何;如果它们更大,那么``eval()``可以避免不同内存缓存之间的某些值移动,它们可能很慢。
在实践中,我发现传统方法和``eval/query``方法之间的计算时间差异,通常不大 - 如果有的话,传统方法对于较小的数组来说更快!``eval/query``的好处主要在于节省的内存,以及它们提供的有时更清晰的语法。
我们已经涵盖了``eval()``和``query()``的大部分细节;对于这些的更多信息,你可以参考 Pandas 文档。特别是,可以指定执行这些查询的不同解析器和引擎;详细信息请参阅“提升性能”部分中的讨论。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册