【Pandas】返回 视图 还是 副本?SettingwithCopyWarning 的原理和解决方案(摘录转载)

摘录1:https://www.jianshu.com/p/c8398e508fb1

当给一个pandas 对象赋值时, 一定要小心, 避免使用 Chained Indexing进行赋值, 而应该使用 .loc[:,'column'] 方式赋值.

import pandas as pd
import numpy as np

索引方式

我们先用 Chained Index 和 loc Index分别去索引一个Dataframe中的一列.

raw_list = [list('abcd'),list('efgh'),list('ijkl'),list('mnop')]
dfmi = pd.DataFrame(raw_list, columns=pd.MultiIndex.from_product([['one', 'two'], ['first', 'second']]))
dfmi
####first....second....
##firstsecondfirstsecond
0abcd
1efgh
2ijkl
3mnop

在以上的代码中, MultiIndex 是模拟 Hierarchical Indexing, 这种Index方式允许程序员以低维度数据结构(Series, DataFrame)存储和操作任意维度的数据. MultiIndex 类可以通过 List of Arrays, List of Tuples,和两个list的笛卡尔积去创建.

注意, # 与. 在这里都是占位符, 因为没有找到Jianshu Makrdown支持HTML表格的方法, 而Markdown本身的表格语法不支持单元格合并, 所以只能这样, 下同.

现在如果要索引到 ('one', 'second') 这一列, 有两种索引方法:

1:Chained 方式

dfmi['one']['second']
0    b
1    f
2    j
3    n
Name: second, dtype: object

2: .loc 方式

dfmi.loc[:, ('one', 'second')]
0    b
1    f
2    j
3    n
Name: (one, second), dtype: object

第一种Chained方式, 等价于

dfmi.__get__item__('one').__get__item__('second') 

即会先用 dfmi['one'] 索引到一个DataFrame, 我们把它命名为dfmi_one (结果见上), 然后用 dfmi_one['second'] 去索引Series. Pandas在这个过程中调用了两次 __get_item__方法.

第二种.loc 方式, dfmi.loc[:, ('one', 'second')]等价于

dfmi.loc.__get_item__((slice(None), ('one', 'second')))

将一组嵌套tuple (slice(None), ('one', 'second')) 直接传递给 __get_item__, 只用一次操作就将期望的Series取出. 这种方式明显速度更快.

当然这只是速度上的对比, 当我们用以上两种方式赋值时,会发生什么?

不同的赋值方式与SettingWithCopyWarning

to_set_value_1 = pd.Series(list('xyza'))
dfmi_chained = pd.DataFrame(raw_list, columns=pd.MultiIndex.from_product([['one', 'two'], ['first', 'second']]))
dfmi_loc = pd.DataFrame(raw_list, columns=pd.MultiIndex.from_product([['one', 'two'], ['first', 'second']]))

在上面的代码中, 我们新建一个Series. 以及两个相同的DataFrame, 后续会分别用于chained索引并赋值, 以及.loc索引并赋值.

dfmi_chained['one']['second'] = to_set_value
/..../python3.6/site-packages/ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  """Entry point for launching an IPython kernel.
dfmi_chained
####first....second....
##firstsecondfirstsecond
0axcd
1eygh
2izkl
3maop
dfmi_loc.loc[:, ('one', 'second')] = to_set_value
dfmi_loc
####first....second....
##firstsecondfirstsecond
0axcd
1eygh
2izkl
3maop

先来看一下上面的代码发生了什么.

两种不同的赋值方式得到的结果如下:

  1. Chained 赋值:
    我们在赋值结束后会看到一个 Warning:"SettingWithCopyWarning". 查看 dfmi_chained 的值,此时对应的索引已经赋值.

  2. .loc 赋值:
    正常赋值, 没有Warning, 查看 dfmi_loc 的值, DataFrame中对应的值已经赋值成功.

那什么是 "SettingWithCopyWarning"? 为什么Chained Indexing 赋值会引发这个Warning?

"SettingWithCopyWarning" warning指你可能在给一个被Copy的Dataframe赋值, 这个赋值很可能不会影响到原Dataframe. 在这个例子里, 确实影响到了原来的Dataframe, 但在有些情况下不会.

我们把 dfmi_chained['one']['second'] = to_set_value 这个赋值语句等价于以下语句:

dfmi.__get_item__('one').__set_item_('second', to_set_value)

注意它会先使用 __get_item__('one') 拿出一个Dataframe. 在其它情况下可能是拿出一个Series, 或者其它什么对象. Pandas此时并不清楚拿到的对象, 是原对象的引用, 还是一个被Copy过的新对象. 因此也就无法保证 __set_item('second', to_set_value) 这个赋值语句, 会对原来的对象也赋值. 这也是Warning被抛出的原因.

对于第二种 .loc 索引 + 赋值的语句 dfmi.loc[:, ('one', 'second')] = to_set_value. 等价于以下语句

dfmi.loc.__set_item__((slice(None), ('one', 'second')), to_set_value)

是直接调用了 .loc属性返回值的 __set_item__ 方法. Pandas已经确保了 dataframe.loc属性就是 dataframe 自身, .loc 只不过是更改了dataframe的索引方法, 因此 .loc.__set_item__() 就是对dataframe自身进行操作的.

也可以理解为, 第二种方式,并不是先“索引”Series,再赋值. 而是直接将值赋给dataframe里对应索引的Series.

如果第二种方式赋值方法等价于

dfmi.loc.__get__item__(params).__set_item__(to_set_value)

其实也是会遇到 "SettingWithCopyWarning", 因为 __get_item__() 方法有可能返回对象的引用, 有可能返回Copy值.

结论

所以Chained Index 和 loc Index赋值的区别在于 Chained 赋值的方式是先索引到对象, 再更改对象的值. 此时索引到的对象可能只是原来对象的Copy. 而 .loc 赋值是直接去更改原始Dataframe的对应索引的值.

引用

文章大部分内容来自Pandas官方文档:

Returning a view versus a copy:
https://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

 

 

 

摘录2:https://www.jianshu.com/p/72274ccb647a

原文链接:https://www.dataquest.io/blog/settingwithcopywarning/
原文标题:Understanding SettingwithCopyWarning in pandas
原文发布时间:5 JULY 2017(需要注意时效性,文中有一些方法已经弃用,比如 ix
作者:Benjamin Pryke
译者:Ivy Lee

学习 Python 数据分析的同学总是遇到这个警告,查询中文资料,一般只能找到个别的解决办法,不一定适用于自己遇到的情况。查到的最常见解决办法就是直接设置为不显示警告。搜索资料发现这篇英文讲解 SettingWithCopyWarning 原理非常系统的文章,翻译了一下,分享给大家。

题图

SettingWithCopyWarning 是人们在学习 Pandas 时遇到的最常见的障碍之一。快速的网络搜索可以搜索到 Stack Overflow 问题,GitHub issues 和程序员的论坛帖子,试图解释这个警告在他们的特定情况下意味着什么。这么多人为此困扰并不奇怪:有很多方法可以索引 Pandas 数据结构,每种数据结构都有自己独特的细微差别,甚至 Pandas 本身并不能保证两行代码的运行结果看起来完全相同。

本指南解释了生成警告的原因并展示了如何解决这一警告。它还包括一些底层的细节,让你更好地了解代码内部发生了什么,提供了有关该话题的一些历史记录,让你了解为什么代码底层以这样的方式运作。

为了探索 SettingWithCopyWarning,我们将使用 Modelling Online Auctions 一书中的 eBay 3 天拍卖出售的 Xbox 的价格数据集。让我们来看看:

import Pandas as pd

data = pd.read_csv('xbox-3-day-auctions.csv')
data.head()
 auctionidbidbidtimebidderbidderrateopenbidprice
0821303470595.02.927373jake7870095.0117.5
18213034705115.02.943484davidbresler2195.0117.5
28213034705100.02.951285gladimacowgirl5895.0117.5
38213034705117.52.998947daysrus1095.0117.5
482130604202.00.065266donnie481451.0120.0

如你所见,数据集的每一行都是某一次 eBay Xbox 出价信息。以下是该数据集中每列的简要说明:

  • auctionid - 每次拍卖的唯一标识符
  • bid - 本次拍卖出价
  • bidtime - 拍卖的时长,以天为单位,从投标开始累计
  • bidder - 投标人的 eBay 用户名
  • bidderrate - 投标人的 eBay 用户评级
  • openbid - 卖方为拍卖设定的开标价
  • price - 拍卖结束时的中标价

什么是 SettingWithCopyWarning?

首先要理解的是,SettingWithCopyWarning 是一个警告,而不是错误 Error。

错误表明某些内容是“坏掉”的,例如无效语法(invalid syntax)或尝试引用未定义的变量。警告的作用是提醒程序员,他们的代码可能存在潜在的错误或问题,但是这些操作仍然是该编程语言中的合法操作。在这种情况下,警告很可能表明一个严重但不容易意识到的错误。

SettingWithCopyWarning 告诉你,你的操作可能没有按预期运行,你应该检查结果以确保没有出错。

如果你的代码仍然按预期工作,那么很容易忽略警告。这不是良好的实践,SettingWithCopyWarning 不应该被忽略。在采取下一步行动之前,花点时间了解为什么会获得这一警告。

要了解 SettingWithCopyWarning,首先需要了解 Pandas 中的某些操作可以返回数据的视图(View),而某些其他操作将返回数据的副本(Copy)。

View VS Copy

如上所示,左侧的视图 df2 只是原始 df1 一个子集,而右侧的副本创建了一个新的唯一对象 df2

当我们尝试对数据集进行更改时,这可能会导致问题:

修改视图或副本

根据我们的需求,我们可能想要修改原始 df1(左),可能想要修改 df2(右)。警告让我们知道,我们的代码可能并没有符合需求,修改的并不是我们想要修改的那个数据集。

我们稍后会深入研究这个问题,但是现在先来了解一下,警告出现的两个主要原因以及如何解决它们。

链式赋值(Chained assignment)

当 Pandas 检测链式赋值(Chained assignment)时会生成警告。让我们定义一些术语,方便后续的解释:

  • 赋值(Assignment) - 设置某些变量值的操作,例如 data = pd.read_csv('xbox-3-day-auctions.csv') 。也被称为设置(set)
  • 访问(Access) - 返回某些值的操作,例如下面的索引和链式索引示例。也被称为获取(get)
  • 索引(Indexing) - 引用数据子集的任何赋值或访问方法,例如 data[1:5]
  • 链式索引(Chaining) - 连续使用多个索引操作,例如data[1:5][1:3]

链式赋值是链式索引和赋值的组合。先快速浏览一下之前加载的数据集,稍后我们将详细介绍这一点。在这个例子中,假设我们了解到用户 'parakeet2004' 的 bidderrate 不正确,我们必须修改他的 bidderrate,首先,查看一下当前的值。

data[data.bidder == 'parakeet2004']
 auctionidbidbidtimebidderbidderrateopenbidprice
682130604203.000.186539parakeet200451.0120.0
7821306042010.000.186690parakeet200451.0120.0
8821306042024.990.187049parakeet200451.0120.0

我们有三行要更新 bidderrate 字段,我们继续往下操作:

data[data.bidder == 'parakeet2004']['bidderrate'] = 100
/Library/Frameworks/Python.framework/Versions/36/lib/python3.6/ipykernel/__main__.py:1:SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from aDataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation:http://Pandas.pydata.org/Pandas-docs/stable/indexinghtml#indexing-view-versus-copy
  if __name__ == '__main__':

不好了!我们神奇的造成了 SettingWithCopyWarning

如果检查一下,可以看到在这种情况下,值没有按预期改变:

data[data.bidder == 'parakeet2004']
 auctionidbidbidtimebidderbidderrateopenbidprice
682130604203.000.186539parakeet200451.0120.0
7821306042010.000.186690parakeet200451.0120.0
8821306042024.990.187049parakeet200451.0120.0

生成警告是因为我们将两个索引操作链接在一起,我们直接使用了两次方括号,所以这比较容易理解。但如果我们使用其他访问方法,例如 .bidderrate.loc[].iloc[].ix[],也是如此,我们的链式操作是:

  • data[data.bidder == 'parakeet2004']
  • ['bidderrate'] = 100

这两个链式操作一个接一个地独立执行。第一次是访问操作(get),返回一个 DataFrame,其中包含所有 bidder 等于 'parakeet2004' 的行。第二个是赋值操作(set),是在这个新的 DataFrame 上运行的,我们压根没有在原始 DataFrame 上运行。

这个解决方案很简单:使用 loc 将链式操作组合到一个操作中,以便 Pandas 可以确保 set 的是原始 DataFrame。Pandas 会始终确保下面这样的非链式 set 操作起作用。

# 设置新值
data.loc[data.bidder == 'parakeet2004', 'bidderrate'] = 100
# 检查结果
data[data.bidder == 'parakeet2004']['bidderrate']


6    100
7    100
8    100
Name: bidderrate, dtype: int64

这就是警告中建议我们做的操作,在这种情况下它完美地适用。

隐蔽的链式操作(Hidden chaining)

现在来看一下遇到 SettingWithCopyWarning 的第二种最常见的方式。我们来探索中标者的数据,我们将为此创建一个新的 DataFrame,现在已经学习了关于链式赋值的内容,因此请注意使用 loc

winners = data.loc[data.bid == data.price]
winners.head()
 auctionidbidbidtimebidderbidderrateopenbidprice
38213034705117.52.998947daysrus1095.00117.5
258213060420120.02.999722djnoeproductions171.00120.0
448213067838132.52.996632*champaignbubbles*20229.99132.5
458213067838132.52.997789*champaignbubbles*20229.99132.5
668213073509114.52.999236rr6kids41.00114.5

我们可能会使用 winners 变量编写一些后续的代码行。

mean_win_time = winners.bidtime.mean()
... # 20 lines of code
mode_open_bid = winners.openbid.mode()

偶然的机会,我们在该 DataFrame 发现了另一个错误:标记为 304 的行中缺少了 bidder 值。

winners.loc[304, 'bidder']

nan

对这个例子来说,假设我们知道这个投标人的真实用户名,并以此更新数据:

winners.loc[304, 'bidder'] = 'therealname'
/Library/Frameworks/Python.framework/Versions/36/lib/python3.6/Pandas/core/indexing.py:517:SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from aDataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://Pandas.pydata.org/Pandas-docs/stable/indexing.html#indexing-view-versus-copy
  self.obj[item] = s

另一个 SettingWithCopyWarning!但我们使用了 loc,这又是怎么回事?为了研究这一点,我们来看看代码的结果:

print(winners.loc[304, 'bidder'])

therealname

代码按预期工作了,为什么我们还是得到警告?

链式索引可能跨越两行代码发生,也可能在一行代码内发生。因为 winners 是作为 get 操作的输出创建的(data.loc[data.bid == data.price]),它可能是原始 DataFrame 的副本,也可能不是,但除非我们检查,否则我们不能了解到。当我们对 winners 进行索引时,我们实际上使用的是链式索引。

这意味着当我们尝试修改 winners 时,我们可能也修改了 data

在实际的代码中,这些行可能会跨越很大的距离,因此追踪问题可能会更困难,但情况是与示例类似的。

为了防止这种情况下的警告,解决方案是在创建新 DataFrame 时明确告知 Pandas 制作一个副本:

winners = data.loc[data.bid == data.price].copy()
winners.loc[304, 'bidder'] = 'therealname'
print(winners.loc[304, 'bidder'])
print(data.loc[304, 'bidder'])

therealname
nan

就这么简单!

窍门就是,学会识别链式索引,不惜一切代价避免使用链式索引。如果要更改原始数据,请使用单一赋值操作。如果你想要一个副本,请确保你强制让 Pandas 制作副本。这样可以节省时间,也可以使代码保持严密的逻辑。

另外请注意,即使 SettingWithCopyWarning 只在你进行 set 时才会发生,但在进行 get 操作时,最好也避免使用链式索引。链式操作较慢,而且只要你稍后决定进行赋值操作,就会导致问题。

处理 SettingWithCopyWarning 的提示和技巧

在我们进行下面更深入的分析之前,让我们“拿出显微镜”,看看 SettingWithCopyWarning 的更多细节。

关闭警告

首先,如果不讨论如何明确地控制 SettingWithCopy 设置,那么本文则不完整。Pandas 的 mode.chained_assignment 选项可以采用以下几个值之一:

  • 'raise' - 抛出异常(exception)而不是警告
  • 'warn' - 生成警告(默认)
  • None - 完全关闭警告

例如,如果要关闭警告:

pd.set_option('mode.chained_assignment', None)
data[data.bidder == 'parakeet2004']['bidderrate'] = 100

因为这样没有给我们任何警告,除非你完全了解自己在做什么,否则不建议这样做。如果你对想要实现的操作有任何一丁点的疑问,关闭警告都不被推荐。有些开发者非常重视 SettingWithCopy 甚至选择将其提升为异常,如下所示:

pd.set_option('mode.chained_assignment', 'raise')
data[data.bidder == 'parakeet2004']['bidderrate'] = 100
---------------------------------------------------------------------------
SettingWithCopyError                      Traceback (most recent call last)
<ipython-input-13-80e3669cab86> in <module>()
      1 pd.set_option('mode.chained_assignment', 'raise')
----> 2 data[data.bidder == 'parakeet2004']['bidderrate'] = 100

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/Pandas/core/frame.py in __setitem__(self, key, value)
    2427         else:
   2428             # set column
-> 2429             self._set_item(key, value)
   2430 
   2431     def _setitem_slice(self, key, value):

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/Pandas/core/frame.py in _set_item(self, key, value)
   2500         # value exception to occur first
   2501         if len(self):
-> 2502             self._check_setitem_copy()
    2503 
   2504     def insert(self, loc, column, value, allow_duplicates=False):

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/Pandas/core/generic.py in _check_setitem_copy(self, stacklevel, t, force)
   1758 
   1759             if value == 'raise':
-> 1760                 raise SettingWithCopyError(t)
   1761             elif value == 'warn':
   1762                 warnings.warn(t, SettingWithCopyWarning, stacklevel=stacklevel)

SettingWithCopyError: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://Pandas.pydata.org/Pandas-docs/stable/indexing.html#indexing-view-versus-copy

如果你正在与缺乏经验的 Pandas 开发人员合作开发项目,或者正在开发需要高度严谨的项目,这可能特别有用。

使用此设置的更精确方法是使用 上下文管理器 context manager

# resets the option we set in the previous code segment
pd.reset_option('mode.chained_assignment')

with pd.option_context('mode.chained_assignment', None):
    data[data.bidder == 'parakeet2004']['bidderrate'] = 100

正如你所看到的,这种方法可以实现针对性的警告设置,而不是影响整个环境。

is_copy 属性

避免警告的另一个技巧是修改 Pandas 用于解释 SettingWithCopy 的工具之一。每个 DataFrame 都有一个 is_copy 属性,默认情况下为 None,但如果它是副本,则会使用 weakref 引用原始 DataFrame 。通过将 is_copy 设置为 None,可以避免生成警告。

winners = data.loc[data.bid == data.price]
winners.is_copy = None
winners.loc[304, 'bidder'] = 'therealname'

但是请注意,这并不会奇迹般地解决问题,反而会使错误检测变得非常困难。

单类型 VS 多类型对象

值得强调的另一点是单类型对象和多类型对象之间的差异。如果 DataFrame 所有列都具有相同的 dtype,则它是单类型的,例如:

import numpy as np

single_dtype_df = pd.DataFrame(np.random.rand(5,2), columns=list('AB'))
print(single_dtype_df.dtypes)
single_dtype_df

A    float64
B    float64
dtype: object
 AB
00.3831970.895652
10.0779430.905245
20.4521510.677482
30.5332880.768252
40.3897990.674594

如果 DataFrame 的列不是全部具有相同的 dtype,那么它是多类型的,例如:

multiple_dtype_df = pd.DataFrame({'A': np.random.rand(5),'B': list('abcde')})
print(multiple_dtype_df.dtypes)
multiple_dtype_df

A    float64
B     object
dtype: object
 AB
00.615487a
10.946149b
20.701231c
30.756522d
40.481719e

由于下面历史部分中所述的原因,对多类型对象的索引 get 操作将始终返回副本。然而,为了提高效率,索引器对单类型对象的操作几乎总是返回一个视图,这里需要注意的是,这取决于对象的内存布局,并不能完全保证。

误报

误报,即无意中报告链式赋值的情况,曾经在早期版本的 Pandas 中比较常见,但此后大部分都被解决了。为了完整起见,在此处包括一些已修复的误报示例也是有用的。如果你在使用早期版本的 Pandas 时遇到以下任何情况,则可以安全地忽略或抑制警告(或通过升级完全避免警告!)

使用当前列的值,将新列添加到 DataFrame生成警告,但这已得到修复。

data['bidtime_hours'] = data.bidtime.map(lambda x: x * 24)
data.head(2)
 auctionidbidbidtimebidderbidderrateopenbidpricebidtime_hours
0821303470595.02.927373jake7870095.0117.570.256952
18213034705115.02.943484davidbresler2195.0117.570.643616

当在一个 DataFrame 切片上使用 apply 方法进行设置时,也会出现误报,不过这也已得到修复。

data.loc[:, 'bidtime_hours'] = data.bidtime.apply(lambda x: x * 24)
data.head(2)
 auctionidbidbidtimebidderbidderrateopenbidpricebidtime_hours
0821303470595.02.927373jake7870095.0117.570.256952
18213034705115.02.943484davidbresler2195.0117.570.643616

最后,直到 0.17.0 版本前,DataFrame.sample 方法中存在一个错误,导致 SettingWithCopy 警告误报。现在,sample 方法每次都会返回一个副本。

sample = data.sample(2)
sample.loc[:, 'price'] = 120
sample.head()
 auctionidbidbidtimebidderbidderrateopenbidpricebidtime_hours
481821540802391.012.990741sailer4eva10.9912071.777784
5038215571039100.001.965463lambonius1050.0012047.171112

链式赋值深度解析

让我们重用之前的例子:试图更新 databidder 值为 'parakeet2004' 的所有行的 bidderrate 字段。

data[data.bidder == 'parakeet2004']['bidderrate'] = 100
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ipykernel/__main__.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://Pandas.pydata.org/Pandas-docs/stable/indexing.html#indexing-view-versus-copy
  if __name__ == '__main__':

Pandas 用这个 SettingWithCopyWarning 告诉我们的是,代码的行为是模棱两可的,但要理解为什么这样做以及警告的措辞,以下概念将会有所帮助。

我们之前简要地谈过了视图(View)和副本(Copy)。有两种方法可以访问 DataFrame 的子集:可以创建对内存中原始数据的引用(视图),也可以将子集复制到新的较小的 DataFrame 中(副本)。视图是查看 原始 数据特定部分的一种方式,而副本是将该数据克隆到内存中的新位置。正如我们之前的图表所示,修改视图将修改原始变量,但修改副本则不会。

由于某些我们将在稍后介绍的原因,Pandas 中 get 操作的输出无法保证。索引 Pandas 数据结构时,视图或副本都可能被返回,这意味着对某一 DataFrame 进行 get 操作返回一个新的 DataFrame ,这个新的数据可能是:

  • 来自原始对象的数据副本。
  • 没有复制,而是直接对原始对象的引用。

因为我们不知道将会发生什么,并且每种可能性都有非常不同的行为,所以忽略警告就是“玩火”。

为了更清楚地解释视图、副本和其中的歧义,让我们创建一个简单的 DataFrame 并对其进行索引:

df1 = pd.DataFrame(np.arange(6).reshape((3,2)), columns=list('AB'))
df1
 AB
001
123
245

df1 的子集赋值给 df2

df2 = df1.loc[:1]
df2
 AB
001
123

根据刚才学到的知识,我们知道 df2 可能是 df1 的视图或 df1 子集的副本。

在解决问题之前,我们还需要再看一下链式索引。扩展一下 'parakeet2004' 示例,我们将两个索引操作链接在一起:

data[data.bidder == 'parakeet2004']
__intermediate__['bidderrate'] = 100

__intermediate__ 表示第一个调用的输出,对我们是完全不可见的。请记住,如果我们使用了属性访问,会得到相同的有问题的结果:

data[data.bidder == 'parakeet2004'].bidderrate = 100

这同样适用于任何其他形式的链式调用,因为我们正在生成中间对象

在底层代码中,链式索引意味着对 __getitem____setitem__ 进行多次调用以完成单个操作。这些是 特殊的 Python 方法,通过在实现它们的类的实例上使用方括号,可以调用这些方法,这是语法糖的一种示例。让我们看一下 Python 解释器如何执行我们示例中的内容。

# Our code
data[data.bidder == 'parakeet2004']['bidderrate'] = 100

# Code executed
data.__getitem__(data.__getitem__('bidder') == 'parakeet2004').__setitem__('bidderrate', 100)

正如你可能已经意识到的那样,SettingWithCopyWarning 是由此链式 __setitem__ 调用生成的。你可以自己尝试一下 - 上面这些代码的功能相同。为清楚起见,请注意第二个 __getitem__ 调用(对 bidder 列)是嵌套的,而不是链式问题的所有部分。

通常,如上面所述,Pandas 不保证 get 操作是返回视图还是副本。如果在我们的示例中返回了一个视图,则链式赋值中的第二个表达式将是对原始对象 __setitem__ 的调用。但是,如果返回一个副本,那么将被修改的是副本 - 原始对象不会被修改。

这就是警告中 “a value is trying to be set on a copy of a slice from a DataFrame” 的含义。由于没有对此副本的引用,它最终将被回收SettingWithCopyWarning 让我们知道 Pandas 无法确定第一个 __getitem__ 调用是否返回了视图或副本,因此不清楚该赋值是否更改了原始对象。换一种说法就是:“我们是否正在修改原始数据?”这一问题的答案是未知的。

如果我们确实想要修改原始文件,警告建议的解决方案是使用 loc 将这两个单独的链式操作转换为单个赋值操作。这样我们的代码中没有了链式索引,就不会再收到警告。我们修改后的代码及其扩展版本如下所示:

# Our code
data.loc[data.bidder == 'parakeet2004', 'bidderrate'] = 100

# Code executed
data.loc.__setitem__((data.__getitem__('bidder') == 'parakeet2004', 'bidderrate'), 100)

DataFrame 的 loc 属性保证是原始 DataFrame 本身,具有扩展的索引功能。

假阴性(False negatives)

使用 loc 并没有结束我们的问题,因为使用 loc 的 get 操作仍然可以返回一个视图或副本。让我们快速过一下,一个有点复杂的例子。

data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
 bidderratebid
61003.00
710010.00
810024.99

我们这次拉出了两列而不是一列。让我们尝试 set 所有的 bid 值。

data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]['bid'] = 5.0
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
 bidderratebid
61003.00
710010.00
810024.99

没有效果,也没有警告!我们在切片的副本上 set 了一个值但是 Pandas 没有检测到它 - 这就是假阴性。这是因为,使用 loc 之后并不意味着我们可以再次使用链式赋值。这个特定的 bug,有一个未解决的 GitHub issue

正确的解决方法如下:

data.loc[data.bidder == 'parakeet2004', 'bid'] = 5.0
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
 bidderratebid
61005
71005
81005

你可能怀疑,是否有人会在实践中遇到这样的问题。其实这比你想象的更容易出现。当我们像下一节中这样做:将 DataFrame 查询的结果赋值给变量。

隐藏的链式索引

让我们再看一下之前隐藏的链式索引示例,我们试图设置 winners 变量中,标记为 304 行的 bidder 字段。

winners = data.loc[data.bid == data.price]
winners.loc[304, 'bidder'] = 'therealname'
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/Pandas/core/indexing.py:517: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://Pandas.pydata.org/Pandas-docs/stable/indexing.html#indexing-view-versus-copy
  self.obj[item] = s

我们得到了另一个 SettingWithCopyWarning 尽管我们使用了 loc 。这个问题可能令人非常困惑,因为警告信息建议我们的方法,我们已经做过了。

不过,想一下 winners 变量。它究竟是什么?由于我们通过 data.loc[data.bid == data.price] 将它初始化,我们无法知道它是原始 data DataFrame 的视图还是副本(因为 get 操作返回视图或副本)。将初始化与生成警告的行组合在一起可以清楚地表明我们的错误。

data.loc[data.bid == data.price].loc[304, 'bidder'] = 'therealname'
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/Pandas/core/indexing.py:517: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://Pandas.pydata.org/Pandas-docs/stable/indexing.html#indexing-view-versus-copy
  self.obj[item] = s

我们再次使用了链式赋值,只是这次它被分在了两行代码中。思考这个问题的另一种方法是,问一个问题“这个操作会修改一个对象,还是两个对象?”在我们的示例中,答案是未知的:如果 winners 是副本,那么只有 winners 受到影响,但如果是视图,则 winnersdata 都将被更新。这种情况可能发生在脚本或代码库中相距很远的行之间,这使问题很难被追根溯源。

此处警告的意图是让我们意识到,我们以为代码将修改原始 DataFrame ,实际没有修改成功,或者说我们将修改副本而不是原始数据。深入研究 Pandas GitHub repo 中的 issue,你可以看到开发人员自己对这个问题的解释。

如何解决这个问题在很大程度上取决于我们自己的意图。如果我们想要使用原始数据的副本,解决方案就是强制 Pandas 制作副本。

winners = data.loc[data.bid == data.price].copy()
winners.loc[304, 'bidder'] = 'therealname'

print(data.loc[304, 'bidder']) # Original
print(winners.loc[304, 'bidder']) # Copy

nan
therealname

另一方面,如果你需要更新原始 DataFrame ,那么你应该使用原始 DataFrame 而不是重新赋值一些具有未知行为的其他变量。我们之前的代码将修改为:

# Finding the winners
winner_mask = data.bid == data.price

# Taking a peek
data.loc[winner_mask].head()

# Doing analysis
mean_win_time = data.loc[winner_mask, 'bidtime'].mean()
... # 20 lines of code
mode_open_bid = data.loc[winner_mask, 'openbid'].mode()

# Updating the username
data.loc[304, 'bidder'] = 'therealname'

在更复杂的情况下,例如修改 DataFrame 子集的子集,不要使用链式索引,可以在原始 DataFrame 上通过 loc 进行修改。例如,你可以更改上面的新 winner_mask 变量或创建一个选择中标者子集的新变量,如下所示:

high_winner_mask = winner_mask & (data.price > 150)
data.loc[high_winner_mask].head()
 auctionidbidbidtimebidderbidderrateopenbidpricebidtime_hours
2258213387444152.02.919757uconnbabydoll1975150.99152.070.074168
3288213935134207.52.983542toby249200.10207.571.605008
4168214430396199.02.990463volpendesta49.99199.071.771112
5318215582227152.52.999664ultimatum_man260.00152.571.991936

这种技术会使得未来的代码库维护和扩展更加稳健。

历史

你可能想知道为什么要造成这么混乱的现状,为什么不明确指定索引方法是返回视图还是副本,来完全避免 SettingWithCopy 问题。要理解这一点,我们必须研究 Pandas 的过去。

Pandas 确定返回一个视图还是一个副本的逻辑,源于它对 NumPy 库的使用,这是 Pandas 库的基础。视图实际上是通过 NumPy 进入 Pandas 的词库的。实际上,视图在 NumPy 中很有用,因为它们能够可预测地返回。由于 NumPy 数组是单一类型的,因此 Pandas 尝试使用最合适的 dtype 来最小化内存处理需求。因此,包含单个 dtype 的 DataFrame 切片可以作为单个 NumPy 数组的视图返回,这是一种高效处理方法。但是,多类型的切片不能以相同的方式存储在 NumPy 中。Pandas 兼顾多种索引功能,并且保持高效地使用其 NumPy 内核的能力。

最终,Pandas 中的索引被设计为有用且通用的方式,其核心并不完全与底层 NumPy 数组的功能相结合。随着时间的推移,这些设计和功能元素之间的相互作用,导致了一组复杂的规则,这些规则决定了返回视图还是副本。经验丰富的 Pandas 开发者通常都很满意 Pandas 的做法,因为他们可以轻松地浏览其索引行为。

不幸的是,对于 Pandas 的新手来说,链式索引几乎是不可避免的,因为 get 操作返回的就是可索引的 Pandas 对象。此外,用 Pandas 的核心开发人员之一 Jeff Reback 的话来说,“从语言的角度来看,直接检测链式索引是不可能的,必须经过推断才能了解”。

因此,在 2013 年底的 0.13.0 版本中引入了警告,作为许多开发者遇到链式赋值导致的无声失败的解决方案。

在 0.12 版本之前,ix 索引器是最受欢迎的(在 Pandas 术语中,“索引器”比如 ixlociloc,是一种简单的结构,允许使用方括号来索引对象,就像数组一样,但具有一些特殊的用法)。但是大约在 2013 年中 ,Pandas 项目开始意识到日益增加的新手用户的重要性,有动力开始提高新手用户的使用体验。自从此版本发布以来,lociloc 索引器因其更明确的性质和更易于解释的用法而受到青睐。(译者注:pandas v0.23.3 (July 7, 2018),其中 ix 方法已经被弃用

Google Trends: Pandas

SettingWithCopyWarning 在推出后持续改进,多年来在许多 GitHub issue 中得到了热烈的讨论 ,甚至还在不断更新 ,但是要理解它,仍然是成为 Pandas 专家的关键。

总结

SettingWithCopyWarning 的基础复杂性是 Pandas 库中为数不多的坑。这个警告的源头深深嵌在库的底层中,不应被忽视。Jeff Reback 自己的话 ,“我没有找到任何你应该忽略这个警告的情况。如果你做某些类型的索引时不起作用,而其他情况下起作用,你是在玩火。”

幸运的是,解决警告只需要识别链式赋值并修复。如果整篇文章你只了解到了一件事,那么就应该是这一点。



作者:笨熊不紧张
链接:https://www.jianshu.com/p/72274ccb647a
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值