没有 SettingWithCopyWarning 的不可预测的 pandas 切片分配行为

Posted

技术标签:

【中文标题】没有 SettingWithCopyWarning 的不可预测的 pandas 切片分配行为【英文标题】:Unpredictable pandas slice assignment behavior with no SettingWithCopyWarning 【发布时间】:2017-01-12 07:29:29 【问题描述】:

well known(和understandable)在分配给切片时,pandas 的行为本质上是不可预测的。但是我已经习惯了被SettingWithCopy警告警告。

为什么以下两个代码sn-ps都没有产生警告,哪些技术可以减少无意中编写此类代码的机会?

# pandas 0.18.1, python 3.5.1
import pandas as pd
data = pd.DataFrame('a': [1, 2, 3], 'b': ['a', 'b', 'c'])
new_data = data[['a', 'b']]
data = data['a']
new_data.loc[0, 'a'] = 100 # no warning, doesn't propagate to data

data[0] == 1
True


data = pd.DataFrame('a': [1, 2, 3], 'b': ['a', 'b', 'c'])
new_data = data['a']
data = data['a']
new_data.loc[0] = 100 # no warning, propagates to data

data[0] == 100
True

我认为解释是,pandas 仅在当前上下文仍可访问父 DataFrame 时才会产生警告。 (正如我之前的示例所示,这将是检测算法的一个弱点。)

在下一个 sn-p 中,AFAIK 无法再访问原始的两列 DataFrame,但 pandas 警告机制设法触发(幸运的是):

data = pd.DataFrame('a': [1, 2, 3], 'b': ['a', 'b', 'c'])
new_data = data['a']
data = data[['a']]
new_data.loc[0] = 100 # warning, so we're safe

编辑:

在调查此问题时,我发现了另一个缺少警告的情况:

data = pd.DataFrame('a': [1, 2, 3], 'b': ['a', 'b', 'c'])
data = data.groupby('a')
new_data = data.filter(lambda g: len(g)==1)
new_data.loc[0, 'a'] = 100 # no warning, does not propagate to data
assert data.filter(lambda g: True).loc[0, 'a'] == 1

即使几乎相同的示例确实触发了警告:

data = pd.DataFrame('a': [1, 2, 2], 'b': ['a', 'b', 'c'])
data = data.groupby('a')
new_data = data.filter(lambda g: len(g)==1)
new_data.loc[0, 'a'] = 100 # warning, does not propagate to data
assert data.filter(lambda g: True).loc[0, 'a'] == 1

更新:我在这里回复@firelynx 的答案,因为很难将其放在评论中。

在答案中,@firelynx 说第一个代码 sn-p 不会导致任何警告,因为我正在获取整个数据帧。但即使我参与其中,我仍然没有收到警告:

# pandas 0.18.1, python 3.5.1
import pandas as pd
data = pd.DataFrame('a': [1, 2, 3], 'b': ['a', 'b', 'c'], c: range(3))
new_data = data[['a', 'b']]
data = data['a']
new_data.loc[0, 'a'] = 100 # no warning, doesn't propagate to data

data[0] == 1
True

【问题讨论】:

关于你的第二个例子,没有副本。 new_datadata 是同一个对象。分配不会“传播”;它只出现在一个有两个名称指向它的对象中。更一般地说,我不认为在任何特定情况下(尤其是更复杂的情况,如您添加的 groupby 示例)会或不会引发 SettingWithCopyWarning 。这只是防止最容易捕获的错误的粗略保护措施。 @BrenBarn 你能澄清一下new_datadata 是同一个对象 的意思吗? new_data 的类型为 DataFramedata 的类型为 DataFrameGroupBy 我指的是您的 second 示例(您的第一个代码块中的第二个代码 sn-p),其中设置了 datanew_datadata['a'] @BrenBarn 我明白了,是的。鉴于 pandas 有点假设程序员在分配给切片时尝试修改父 DataFrame,因此这个示例问题最少 - 毕竟,父被正确修改并且没有发出警告。真正的问题是没有修改父级时没有警告。 对于不占用 PC 内存的数据集,与花费 15 分钟的程序员时间试图确定它是否是副本相比,CPU/内存效率的损失是微不足道的或不。 ;-) 【参考方案1】:

一步一步解释你在做什么

您创建的数据框不是视图

data = pd.DataFrame('a': [1, 2, 3], 'b': ['a', 'b', 'c'])
data._is_view
False

new_data 也不是视图,因为您正在获取所有列

new_data = data[['a', 'b']]
new_data._is_view
False

现在您将数据分配为Series 'a'

data = data['a']
type(data)
pandas.core.series.Series

什么是视图

data._is_view
True

现在您更新非副本new_data 中的值

new_data.loc[0, 'a'] = 100 # no warning, doesn't propagate to data

这不应该给出警告。它是整个数据框。

您创建的 Series 将自身标记为视图,但它不是 DataFrame,也不会像 DataFrame 视图那样工作。

避免编写这样的代码

Series 与 Dataframe 问题在 pandas 中非常常见[如果您已经使用 pandas 一段时间,则不需要引用]

问题在于你应该一直在写作

data[['a']] 不是data['a']

左侧创建一个数据框视图,右侧创建一个系列。

有些人可能会争辩说永远不要写data['a'],而是写data.a。因此,您可以为 data['a'] 代码向您的环境添加警告。

这不起作用。首先使用data.a语法会导致认知失调。

数据框是列的集合。在 python 中,我们使用[] 操作符访问集合的成员。我们通过. 操作符访问属性。切换这些会导致任何 Python 程序员的认知失调。尤其是当您开始执行 del data.a 之类的操作并注意到它不起作用时。 See this answer for more extensive explaination

清理代码以进行救援

很难看出data[['a']]data['a'] 之间的区别

这是一种气味。我们应该都不做

使用干净代码原则和python之禅的正确方法“显式胜于隐式”

这是:

columns = ['a']
data[columns]

这可能并不令人难以置信,但请看以下示例:

data[['ad', 'cpc', 'roi']]

这是什么意思?这些列是什么?你在这里得到什么数据?

这些是阅读这行代码时每个人脑海中首先出现的问题。

如何解决?不要发表评论。

ad_performance_columns = ['ad', 'cpc', 'roi']
data[ad_performance_columns]

越明确越好。

如需了解更多信息,请考虑购买一本关于简洁代码的书。也许this one

【讨论】:

在底部查看我的更新。我不明白您对为什么客户端不应在我刚刚添加的代码 sn-p 中收到警告的解释。而且我看不出您的干净代码方法会如何阻止某人在最后一个 sn-p 中编写代码。

以上是关于没有 SettingWithCopyWarning 的不可预测的 pandas 切片分配行为的主要内容,如果未能解决你的问题,请参考以下文章

没有 SettingWithCopyWarning 的不可预测的 pandas 切片分配行为

我绕过了 SettingWithCopyWarning,感觉像是错误的方式并且计算效率低下,有没有更好的方法?

pandas DataFrame 警告(SettingWithCopyWarning)

当一行DataFrame是字符串时,SettingWithCopyWarning

SettingWithCopyWarning,即使使用 loc (?) [重复]

SettingWithCopyWarning,即使使用 loc (?) [重复]