Pandas如何避免SettingwithCopyWarning

SettingWithCopyWarning是初学者在学习Pandas时经常会遇到的问题之一。 这篇文章主要为了解释SettingWithCopyWarning警告产生的原因,以及我们应该如何去避免它。

事实上,在Pandas中有很多种方法来实现数据结构的索引,但每一种都会有细微的差别,甚至连Pandas本身也不能保证两行代码的运行结果完全一致。

我们以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是一个警告,而不是错误。 出现SettingWithCopyWarning警告,表明代码可能没有按照预期运行,你应该检查你的代码以确保它没有出错。虽然有时候代码能够正常运行,但建议还是不要忽略SettingWithCopyWarning警告,最好是花点时间去检查一下。

在理解 SettingWithCopyWarning是什么之前,有必要知道在Pandas中,哪些操作是返回一个数据的视图(View),而哪些操作是返回一个数据的副本(Copy)。

上图中,左边的 df2 仅仅是 df1 的一个子集的视图,而右边的 df2 则是 df1 的一个子集的副本,是一个新创建的对象。

当我们想要对数据集进行修改时,可能会出现问题:

我们可能是想修改原始的数据集 df1(左),也可能想要修改 df2(右) 。这个警告提醒我们,代码可能没有符合需求,修改的不一定是我们想修改的数据集。

原因之一:链式赋值

来看一个例子: 假设我们了解到用户parakeet2004bidderrate值不正确,需要修改这个bidderrate值,那么先来查看一下用户parakeet2004的当前值:

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/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 insteadSee the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy if __name__ == '__main__':

这时候出现了 SettingWithCopyWarning警告!

检查一下用户parakeet2004的相关值,可以看到bidderrate字段的值并没有按预期改变:

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

这次警告是因为我们将两个索引操作链接在一起:

  • 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

原因之二:隐蔽的链式操作

再来看一个 例子: 我们创建一个新的DataFrame来探索中标者数据,因为现在已经学习了链式赋值的内容,请注意使用loc

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

winners变量可能会被用来编写一些后续代码:

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

假设我们发现DateFrame中索引为304的那一行,bidder字段数据缺失:

winners.loc[304, 'bidder']
nan

现在需要手工更新一下bidder的值:

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 insteadSee the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy self.obj[item] = s

这一次我们已经使用了loc,但是SettingWithCopyWarning警告还是出现了!

先来看一下代码的结果:

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

bidder的值已经更新成功了,那为什么还会出现警告呢?

链式索引可能出现在一行代码内,也可能跨越两行代码。因为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创建副本。这样既可以节省时间,也可以使代码保持逻辑严密。

总结一下

  1. 避免任何形式的链式赋值,有可能会报warning也有可能不会报。而且即使报了,可能有问题,也可能没问题。
  2. 如果需要用到多级选取,则用loc
  3. 如果需要用到拷贝,则直接加copy()函数

这也是我在学习Pandas时遇到过的问题,在这里记录一下。

本文非原创,且基于原文有所删减。原文为《SettingwithCopyWarning: How to Fix This Warning in Pandas》,有兴趣的可以去看一下。

共有 1 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注