Pandas 中 SettingwithCopyWarning 的原理和解決方案
原文鏈接:https://www.dataquest.io/blog/settingwithcopywarning/
原文標(biāo)題:Understanding SettingwithCopyWarning in pandas
原文發(fā)布時(shí)間:5 JULY 2017(需要注意時(shí)效性,文中有一些方法已經(jīng)棄用,比如 ix)
作者:Benjamin Pryke
譯者:Ivy Lee
學(xué)習(xí) Python 數(shù)據(jù)分析的同學(xué)總是遇到這個(gè)警告,查詢中文資料,一般只能找到個(gè)別的解決辦法,不一定適用于自己遇到的情況。查到的最常見(jiàn)解決辦法就是直接設(shè)置為不顯示警告。搜索資料發(fā)現(xiàn)這篇英文講解 SettingWithCopyWarning 原理非常系統(tǒng)的文章,翻譯了一下,分享給大家。

SettingWithCopyWarning 是人們?cè)趯W(xué)習(xí) Pandas 時(shí)遇到的最常見(jiàn)的障礙之一。快速的網(wǎng)絡(luò)搜索可以搜索到 Stack Overflow 問(wèn)題,GitHub issues 和程序員的論壇帖子,試圖解釋這個(gè)警告在他們的特定情況下意味著什么。這么多人為此困擾并不奇怪:有很多方法可以索引 Pandas 數(shù)據(jù)結(jié)構(gòu),每種數(shù)據(jù)結(jié)構(gòu)都有自己獨(dú)特的細(xì)微差別,甚至 Pandas 本身并不能保證兩行代碼的運(yùn)行結(jié)果看起來(lái)完全相同。
本指南解釋了生成警告的原因并展示了如何解決這一警告。它還包括一些底層的細(xì)節(jié),讓你更好地了解代碼內(nèi)部發(fā)生了什么,提供了有關(guān)該話題的一些歷史記錄,讓你了解為什么代碼底層以這樣的方式運(yùn)作。
為了探索 SettingWithCopyWarning,我們將使用 Modelling Online Auctions 一書中的 eBay 3 天拍賣出售的 Xbox 的價(jià)格數(shù)據(jù)集。讓我們來(lái)看看:
import Pandas as pd
data = pd.read_csv('xbox-3-day-auctions.csv')
data.head()
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | |
|---|---|---|---|---|---|---|---|
| 0 | 8213034705 | 95.0 | 2.927373 | jake7870 | 0 | 95.0 | 117.5 |
| 1 | 8213034705 | 115.0 | 2.943484 | davidbresler2 | 1 | 95.0 | 117.5 |
| 2 | 8213034705 | 100.0 | 2.951285 | gladimacowgirl | 58 | 95.0 | 117.5 |
| 3 | 8213034705 | 117.5 | 2.998947 | daysrus | 10 | 95.0 | 117.5 |
| 4 | 8213060420 | 2.0 | 0.065266 | donnie4814 | 5 | 1.0 | 120.0 |
如你所見(jiàn),數(shù)據(jù)集的每一行都是某一次 eBay Xbox 出價(jià)信息。以下是該數(shù)據(jù)集中每列的簡(jiǎn)要說(shuō)明:
auctionid- 每次拍賣的唯一標(biāo)識(shí)符bid- 本次拍賣出價(jià)bidtime- 拍賣的時(shí)長(zhǎng),以天為單位,從投標(biāo)開(kāi)始累計(jì)bidder- 投標(biāo)人的 eBay 用戶名bidderrate- 投標(biāo)人的 eBay 用戶評(píng)級(jí)openbid- 賣方為拍賣設(shè)定的開(kāi)標(biāo)價(jià)price- 拍賣結(jié)束時(shí)的中標(biāo)價(jià)
什么是 SettingWithCopyWarning?
首先要理解的是,SettingWithCopyWarning 是一個(gè)警告,而不是錯(cuò)誤 Error。
錯(cuò)誤表明某些內(nèi)容是“壞掉”的,例如無(wú)效語(yǔ)法(invalid syntax)或嘗試引用未定義的變量。警告的作用是提醒程序員,他們的代碼可能存在潛在的錯(cuò)誤或問(wèn)題,但是這些操作仍然是該編程語(yǔ)言中的合法操作。在這種情況下,警告很可能表明一個(gè)嚴(yán)重但不容易意識(shí)到的錯(cuò)誤。
SettingWithCopyWarning 告訴你,你的操作可能沒(méi)有按預(yù)期運(yùn)行,你應(yīng)該檢查結(jié)果以確保沒(méi)有出錯(cuò)。
如果你的代碼仍然按預(yù)期工作,那么很容易忽略警告。這不是良好的實(shí)踐,SettingWithCopyWarning 不應(yīng)該被忽略。在采取下一步行動(dòng)之前,花點(diǎn)時(shí)間了解為什么會(huì)獲得這一警告。
要了解 SettingWithCopyWarning,首先需要了解 Pandas 中的某些操作可以返回?cái)?shù)據(jù)的視圖(View),而某些其他操作將返回?cái)?shù)據(jù)的副本(Copy)。

如上所示,左側(cè)的視圖 df2 只是原始 df1 一個(gè)子集,而右側(cè)的副本創(chuàng)建了一個(gè)新的唯一對(duì)象 df2。
當(dāng)我們嘗試對(duì)數(shù)據(jù)集進(jìn)行更改時(shí),這可能會(huì)導(dǎo)致問(wèn)題:

根據(jù)我們的需求,我們可能想要修改原始 df1(左),可能想要修改 df2(右)。警告讓我們知道,我們的代碼可能并沒(méi)有符合需求,修改的并不是我們想要修改的那個(gè)數(shù)據(jù)集。
我們稍后會(huì)深入研究這個(gè)問(wèn)題,但是現(xiàn)在先來(lái)了解一下,警告出現(xiàn)的兩個(gè)主要原因以及如何解決它們。
鏈?zhǔn)劫x值(Chained assignment)
當(dāng) Pandas 檢測(cè)鏈?zhǔn)劫x值(Chained assignment)時(shí)會(huì)生成警告。讓我們定義一些術(shù)語(yǔ),方便后續(xù)的解釋:
- 賦值(Assignment) - 設(shè)置某些變量值的操作,例如
data = pd.read_csv('xbox-3-day-auctions.csv')。也被稱為設(shè)置(set) 。 - 訪問(wèn)(Access) - 返回某些值的操作,例如下面的索引和鏈?zhǔn)剿饕纠R脖环Q為獲取(get) 。
- 索引(Indexing) - 引用數(shù)據(jù)子集的任何賦值或訪問(wèn)方法,例如
data[1:5]。 - 鏈?zhǔn)剿饕–haining) - 連續(xù)使用多個(gè)索引操作,例如
data[1:5][1:3]。
鏈?zhǔn)劫x值是鏈?zhǔn)剿饕唾x值的組合。先快速瀏覽一下之前加載的數(shù)據(jù)集,稍后我們將詳細(xì)介紹這一點(diǎn)。在這個(gè)例子中,假設(shè)我們了解到用戶 'parakeet2004' 的 bidderrate 不正確,我們必須修改他的 bidderrate,首先,查看一下當(dāng)前的值。
data[data.bidder == 'parakeet2004']
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | |
|---|---|---|---|---|---|---|---|
| 6 | 8213060420 | 3.00 | 0.186539 | parakeet2004 | 5 | 1.0 | 120.0 |
| 7 | 8213060420 | 10.00 | 0.186690 | parakeet2004 | 5 | 1.0 | 120.0 |
| 8 | 8213060420 | 24.99 | 0.187049 | parakeet2004 | 5 | 1.0 | 120.0 |
我們有三行要更新 bidderrate 字段,我們繼續(xù)往下操作:
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!
如果檢查一下,可以看到在這種情況下,值沒(méi)有按預(yù)期改變:
data[data.bidder == 'parakeet2004']
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | |
|---|---|---|---|---|---|---|---|
| 6 | 8213060420 | 3.00 | 0.186539 | parakeet2004 | 5 | 1.0 | 120.0 |
| 7 | 8213060420 | 10.00 | 0.186690 | parakeet2004 | 5 | 1.0 | 120.0 |
| 8 | 8213060420 | 24.99 | 0.187049 | parakeet2004 | 5 | 1.0 | 120.0 |
生成警告是因?yàn)槲覀儗蓚€(gè)索引操作鏈接在一起,我們直接使用了兩次方括號(hào),所以這比較容易理解。但如果我們使用其他訪問(wèn)方法,例如 .bidderrate、.loc[]、.iloc[]、.ix[],也是如此,我們的鏈?zhǔn)讲僮魇牵?/p>
data[data.bidder == 'parakeet2004']['bidderrate'] = 100
這兩個(gè)鏈?zhǔn)讲僮饕粋€(gè)接一個(gè)地獨(dú)立執(zhí)行。第一次是訪問(wèn)操作(get),返回一個(gè) DataFrame,其中包含所有 bidder 等于 'parakeet2004' 的行。第二個(gè)是賦值操作(set),是在這個(gè)新的 DataFrame 上運(yùn)行的,我們壓根沒(méi)有在原始 DataFrame 上運(yùn)行。
這個(gè)解決方案很簡(jiǎn)單:使用 loc 將鏈?zhǔn)讲僮鹘M合到一個(gè)操作中,以便 Pandas 可以確保 set 的是原始 DataFrame。Pandas 會(huì)始終確保下面這樣的非鏈?zhǔn)?set 操作起作用。
# 設(shè)置新值
data.loc[data.bidder == 'parakeet2004', 'bidderrate'] = 100
# 檢查結(jié)果
data[data.bidder == 'parakeet2004']['bidderrate']
6 100
7 100
8 100
Name: bidderrate, dtype: int64
這就是警告中建議我們做的操作,在這種情況下它完美地適用。
隱蔽的鏈?zhǔn)讲僮鳎℉idden chaining)
現(xiàn)在來(lái)看一下遇到 SettingWithCopyWarning 的第二種最常見(jiàn)的方式。我們來(lái)探索中標(biāo)者的數(shù)據(jù),我們將為此創(chuàng)建一個(gè)新的 DataFrame,現(xiàn)在已經(jīng)學(xué)習(xí)了關(guān)于鏈?zhǔn)劫x值的內(nèi)容,因此請(qǐng)注意使用 loc 。
winners = data.loc[data.bid == data.price]
winners.head()
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | |
|---|---|---|---|---|---|---|---|
| 3 | 8213034705 | 117.5 | 2.998947 | daysrus | 10 | 95.00 | 117.5 |
| 25 | 8213060420 | 120.0 | 2.999722 | djnoeproductions | 17 | 1.00 | 120.0 |
| 44 | 8213067838 | 132.5 | 2.996632 | *champaignbubbles* |
202 | 29.99 | 132.5 |
| 45 | 8213067838 | 132.5 | 2.997789 | *champaignbubbles* |
202 | 29.99 | 132.5 |
| 66 | 8213073509 | 114.5 | 2.999236 | rr6kids | 4 | 1.00 | 114.5 |
我們可能會(huì)使用 winners 變量編寫一些后續(xù)的代碼行。
mean_win_time = winners.bidtime.mean()
... # 20 lines of code
mode_open_bid = winners.openbid.mode()
偶然的機(jī)會(huì),我們?cè)谠?DataFrame 發(fā)現(xiàn)了另一個(gè)錯(cuò)誤:標(biāo)記為 304 的行中缺少了 bidder 值。
winners.loc[304, 'bidder']
nan
對(duì)這個(gè)例子來(lái)說(shuō),假設(shè)我們知道這個(gè)投標(biāo)人的真實(shí)用戶名,并以此更新數(shù)據(jù):
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
另一個(gè) SettingWithCopyWarning!但我們使用了 loc,這又是怎么回事?為了研究這一點(diǎn),我們來(lái)看看代碼的結(jié)果:
print(winners.loc[304, 'bidder'])
therealname
代碼按預(yù)期工作了,為什么我們還是得到警告?
鏈?zhǔn)剿饕赡芸缭絻尚写a發(fā)生,也可能在一行代碼內(nèi)發(fā)生。因?yàn)?winners 是作為 get 操作的輸出創(chuàng)建的(data.loc[data.bid == data.price]),它可能是原始 DataFrame 的副本,也可能不是,但除非我們檢查,否則我們不能了解到。當(dāng)我們對(duì) winners 進(jìn)行索引時(shí),我們實(shí)際上使用的是鏈?zhǔn)剿饕?/p>
這意味著當(dāng)我們嘗試修改 winners 時(shí),我們可能也修改了 data。
在實(shí)際的代碼中,這些行可能會(huì)跨越很大的距離,因此追蹤問(wèn)題可能會(huì)更困難,但情況是與示例類似的。
為了防止這種情況下的警告,解決方案是在創(chuàng)建新 DataFrame 時(shí)明確告知 Pandas 制作一個(gè)副本:
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
就這么簡(jiǎn)單!
竅門就是,學(xué)會(huì)識(shí)別鏈?zhǔn)剿饕幌б磺写鷥r(jià)避免使用鏈?zhǔn)剿饕H绻脑紨?shù)據(jù),請(qǐng)使用單一賦值操作。如果你想要一個(gè)副本,請(qǐng)確保你強(qiáng)制讓 Pandas 制作副本。這樣可以節(jié)省時(shí)間,也可以使代碼保持嚴(yán)密的邏輯。
另外請(qǐng)注意,即使 SettingWithCopyWarning 只在你進(jìn)行 set 時(shí)才會(huì)發(fā)生,但在進(jìn)行 get 操作時(shí),最好也避免使用鏈?zhǔn)剿饕f準(zhǔn)讲僮鬏^慢,而且只要你稍后決定進(jìn)行賦值操作,就會(huì)導(dǎo)致問(wèn)題。
處理 SettingWithCopyWarning 的提示和技巧
在我們進(jìn)行下面更深入的分析之前,讓我們“拿出顯微鏡”,看看 SettingWithCopyWarning 的更多細(xì)節(jié)。
關(guān)閉警告
首先,如果不討論如何明確地控制 SettingWithCopy 設(shè)置,那么本文則不完整。Pandas 的 mode.chained_assignment 選項(xiàng)可以采用以下幾個(gè)值之一:
'raise'- 拋出異常(exception)而不是警告'warn'- 生成警告(默認(rèn))None- 完全關(guān)閉警告
例如,如果要關(guān)閉警告:
pd.set_option('mode.chained_assignment', None)
data[data.bidder == 'parakeet2004']['bidderrate'] = 100
因?yàn)檫@樣沒(méi)有給我們?nèi)魏尉妫悄阃耆私庾约涸谧鍪裁矗駝t不建議這樣做。如果你對(duì)想要實(shí)現(xiàn)的操作有任何一丁點(diǎn)的疑問(wèn),關(guān)閉警告都不被推薦。有些開(kāi)發(fā)者非常重視 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
如果你正在與缺乏經(jīng)驗(yàn)的 Pandas 開(kāi)發(fā)人員合作開(kāi)發(fā)項(xiàng)目,或者正在開(kāi)發(fā)需要高度嚴(yán)謹(jǐn)?shù)捻?xiàng)目,這可能特別有用。
使用此設(shè)置的更精確方法是使用 上下文管理器 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
正如你所看到的,這種方法可以實(shí)現(xiàn)針對(duì)性的警告設(shè)置,而不是影響整個(gè)環(huán)境。
is_copy 屬性
避免警告的另一個(gè)技巧是修改 Pandas 用于解釋 SettingWithCopy 的工具之一。每個(gè) DataFrame 都有一個(gè) is_copy 屬性,默認(rèn)情況下為 None,但如果它是副本,則會(huì)使用 weakref 引用原始 DataFrame 。通過(guò)將 is_copy 設(shè)置為 None,可以避免生成警告。
winners = data.loc[data.bid == data.price]
winners.is_copy = None
winners.loc[304, 'bidder'] = 'therealname'
但是請(qǐng)注意,這并不會(huì)奇跡般地解決問(wèn)題,反而會(huì)使錯(cuò)誤檢測(cè)變得非常困難。
單類型 VS 多類型對(duì)象
值得強(qiáng)調(diào)的另一點(diǎn)是單類型對(duì)象和多類型對(duì)象之間的差異。如果 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
| A | B | |
|---|---|---|
| 0 | 0.383197 | 0.895652 |
| 1 | 0.077943 | 0.905245 |
| 2 | 0.452151 | 0.677482 |
| 3 | 0.533288 | 0.768252 |
| 4 | 0.389799 | 0.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
| A | B | |
|---|---|---|
| 0 | 0.615487 | a |
| 1 | 0.946149 | b |
| 2 | 0.701231 | c |
| 3 | 0.756522 | d |
| 4 | 0.481719 | e |
由于下面歷史部分中所述的原因,對(duì)多類型對(duì)象的索引 get 操作將始終返回副本。然而,為了提高效率,索引器對(duì)單類型對(duì)象的操作幾乎總是返回一個(gè)視圖,這里需要注意的是,這取決于對(duì)象的內(nèi)存布局,并不能完全保證。
誤報(bào)
誤報(bào),即無(wú)意中報(bào)告鏈?zhǔn)劫x值的情況,曾經(jīng)在早期版本的 Pandas 中比較常見(jiàn),但此后大部分都被解決了。為了完整起見(jiàn),在此處包括一些已修復(fù)的誤報(bào)示例也是有用的。如果你在使用早期版本的 Pandas 時(shí)遇到以下任何情況,則可以安全地忽略或抑制警告(或通過(guò)升級(jí)完全避免警告!)
使用當(dāng)前列的值,將新列添加到 DataFrame 會(huì)生成警告,但這已得到修復(fù)。
data['bidtime_hours'] = data.bidtime.map(lambda x: x * 24)
data.head(2)
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | bidtime_hours | |
|---|---|---|---|---|---|---|---|---|
| 0 | 8213034705 | 95.0 | 2.927373 | jake7870 | 0 | 95.0 | 117.5 | 70.256952 |
| 1 | 8213034705 | 115.0 | 2.943484 | davidbresler2 | 1 | 95.0 | 117.5 | 70.643616 |
當(dāng)在一個(gè) DataFrame 切片上使用 apply 方法進(jìn)行設(shè)置時(shí),也會(huì)出現(xiàn)誤報(bào),不過(guò)這也已得到修復(fù)。
data.loc[:, 'bidtime_hours'] = data.bidtime.apply(lambda x: x * 24)
data.head(2)
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | bidtime_hours | |
|---|---|---|---|---|---|---|---|---|
| 0 | 8213034705 | 95.0 | 2.927373 | jake7870 | 0 | 95.0 | 117.5 | 70.256952 |
| 1 | 8213034705 | 115.0 | 2.943484 | davidbresler2 | 1 | 95.0 | 117.5 | 70.643616 |
最后,直到 0.17.0 版本前,DataFrame.sample 方法中存在一個(gè)錯(cuò)誤,導(dǎo)致 SettingWithCopy 警告誤報(bào)。現(xiàn)在,sample 方法每次都會(huì)返回一個(gè)副本。
sample = data.sample(2)
sample.loc[:, 'price'] = 120
sample.head()
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | bidtime_hours | |
|---|---|---|---|---|---|---|---|---|
| 481 | 8215408023 | 91.01 | 2.990741 | sailer4eva | 1 | 0.99 | 120 | 71.777784 |
| 503 | 8215571039 | 100.00 | 1.965463 | lambonius1 | 0 | 50.00 | 120 | 47.171112 |
鏈?zhǔn)劫x值深度解析
讓我們重用之前的例子:試圖更新 data 中 bidder 值為 '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 用這個(gè) SettingWithCopyWarning 告訴我們的是,代碼的行為是模棱兩可的,但要理解為什么這樣做以及警告的措辭,以下概念將會(huì)有所幫助。
我們之前簡(jiǎn)要地談過(guò)了視圖(View)和副本(Copy)。有兩種方法可以訪問(wèn) DataFrame 的子集:可以創(chuàng)建對(duì)內(nèi)存中原始數(shù)據(jù)的引用(視圖),也可以將子集復(fù)制到新的較小的 DataFrame 中(副本)。視圖是查看 原始 數(shù)據(jù)特定部分的一種方式,而副本是將該數(shù)據(jù)克隆到內(nèi)存中的新位置。正如我們之前的圖表所示,修改視圖將修改原始變量,但修改副本則不會(huì)。
由于某些我們將在稍后介紹的原因,Pandas 中 get 操作的輸出無(wú)法保證。索引 Pandas 數(shù)據(jù)結(jié)構(gòu)時(shí),視圖或副本都可能被返回,這意味著對(duì)某一 DataFrame 進(jìn)行 get 操作返回一個(gè)新的 DataFrame ,這個(gè)新的數(shù)據(jù)可能是:
- 來(lái)自原始對(duì)象的數(shù)據(jù)副本。
- 沒(méi)有復(fù)制,而是直接對(duì)原始對(duì)象的引用。
因?yàn)槲覀儾恢缹?huì)發(fā)生什么,并且每種可能性都有非常不同的行為,所以忽略警告就是“玩火”。
為了更清楚地解釋視圖、副本和其中的歧義,讓我們創(chuàng)建一個(gè)簡(jiǎn)單的 DataFrame 并對(duì)其進(jìn)行索引:
df1 = pd.DataFrame(np.arange(6).reshape((3,2)), columns=list('AB'))
df1
| A | B | |
|---|---|---|
| 0 | 0 | 1 |
| 1 | 2 | 3 |
| 2 | 4 | 5 |
將 df1 的子集賦值給 df2:
df2 = df1.loc[:1]
df2
| A | B | |
|---|---|---|
| 0 | 0 | 1 |
| 1 | 2 | 3 |
根據(jù)剛才學(xué)到的知識(shí),我們知道 df2 可能是 df1 的視圖或 df1 子集的副本。
在解決問(wèn)題之前,我們還需要再看一下鏈?zhǔn)剿饕U(kuò)展一下 'parakeet2004' 示例,我們將兩個(gè)索引操作鏈接在一起:
data[data.bidder == 'parakeet2004']
__intermediate__['bidderrate'] = 100
__intermediate__ 表示第一個(gè)調(diào)用的輸出,對(duì)我們是完全不可見(jiàn)的。請(qǐng)記住,如果我們使用了屬性訪問(wèn),會(huì)得到相同的有問(wèn)題的結(jié)果:
data[data.bidder == 'parakeet2004'].bidderrate = 100
這同樣適用于任何其他形式的鏈?zhǔn)秸{(diào)用,因?yàn)槲覀冋谏芍虚g對(duì)象 。
在底層代碼中,鏈?zhǔn)剿饕馕吨鴮?duì) __getitem__ 或 __setitem__ 進(jìn)行多次調(diào)用以完成單個(gè)操作。這些是 特殊的 Python 方法,通過(guò)在實(shí)現(xiàn)它們的類的實(shí)例上使用方括號(hào),可以調(diào)用這些方法,這是語(yǔ)法糖的一種示例。讓我們看一下 Python 解釋器如何執(zhí)行我們示例中的內(nèi)容。
# Our code
data[data.bidder == 'parakeet2004']['bidderrate'] = 100
# Code executed
data.__getitem__(data.__getitem__('bidder') == 'parakeet2004').__setitem__('bidderrate', 100)
正如你可能已經(jīng)意識(shí)到的那樣,SettingWithCopyWarning 是由此鏈?zhǔn)?__setitem__ 調(diào)用生成的。你可以自己嘗試一下 - 上面這些代碼的功能相同。為清楚起見(jiàn),請(qǐng)注意第二個(gè) __getitem__ 調(diào)用(對(duì) bidder 列)是嵌套的,而不是鏈?zhǔn)絾?wèn)題的所有部分。
通常,如上面所述,Pandas 不保證 get 操作是返回視圖還是副本。如果在我們的示例中返回了一個(gè)視圖,則鏈?zhǔn)劫x值中的第二個(gè)表達(dá)式將是對(duì)原始對(duì)象 __setitem__ 的調(diào)用。但是,如果返回一個(gè)副本,那么將被修改的是副本 - 原始對(duì)象不會(huì)被修改。
這就是警告中 “a value is trying to be set on a copy of a slice from a DataFrame” 的含義。由于沒(méi)有對(duì)此副本的引用,它最終將被回收 。SettingWithCopyWarning 讓我們知道 Pandas 無(wú)法確定第一個(gè) __getitem__ 調(diào)用是否返回了視圖或副本,因此不清楚該賦值是否更改了原始對(duì)象。換一種說(shuō)法就是:“我們是否正在修改原始數(shù)據(jù)?”這一問(wèn)題的答案是未知的。
如果我們確實(shí)想要修改原始文件,警告建議的解決方案是使用 loc 將這兩個(gè)單獨(dú)的鏈?zhǔn)讲僮鬓D(zhuǎn)換為單個(gè)賦值操作。這樣我們的代碼中沒(méi)有了鏈?zhǔn)剿饕筒粫?huì)再收到警告。我們修改后的代碼及其擴(kuò)展版本如下所示:
# Our code
data.loc[data.bidder == 'parakeet2004', 'bidderrate'] = 100
# Code executed
data.loc.__setitem__((data.__getitem__('bidder') == 'parakeet2004', 'bidderrate'), 100)
DataFrame 的 loc 屬性保證是原始 DataFrame 本身,具有擴(kuò)展的索引功能。
假陰性(False negatives)
使用 loc 并沒(méi)有結(jié)束我們的問(wèn)題,因?yàn)槭褂?loc 的 get 操作仍然可以返回一個(gè)視圖或副本。讓我們快速過(guò)一下,一個(gè)有點(diǎn)復(fù)雜的例子。
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
| bidderrate | bid | |
|---|---|---|
| 6 | 100 | 3.00 |
| 7 | 100 | 10.00 |
| 8 | 100 | 24.99 |
我們這次拉出了兩列而不是一列。讓我們嘗試 set 所有的 bid 值。
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]['bid'] = 5.0
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
| bidderrate | bid | |
|---|---|---|
| 6 | 100 | 3.00 |
| 7 | 100 | 10.00 |
| 8 | 100 | 24.99 |
沒(méi)有效果,也沒(méi)有警告!我們?cè)谇衅母北旧?set 了一個(gè)值但是 Pandas 沒(méi)有檢測(cè)到它 - 這就是假陰性。這是因?yàn)椋褂?loc 之后并不意味著我們可以再次使用鏈?zhǔn)劫x值。這個(gè)特定的 bug,有一個(gè)未解決的 GitHub issue 。
正確的解決方法如下:
data.loc[data.bidder == 'parakeet2004', 'bid'] = 5.0
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
| bidderrate | bid | |
|---|---|---|
| 6 | 100 | 5 |
| 7 | 100 | 5 |
| 8 | 100 | 5 |
你可能懷疑,是否有人會(huì)在實(shí)踐中遇到這樣的問(wèn)題。其實(shí)這比你想象的更容易出現(xiàn)。當(dāng)我們像下一節(jié)中這樣做:將 DataFrame 查詢的結(jié)果賦值給變量。
隱藏的鏈?zhǔn)剿饕?/h3>
讓我們?cè)倏匆幌轮半[藏的鏈?zhǔn)剿饕纠覀冊(cè)噲D設(shè)置 winners 變量中,標(biāo)記為 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
我們得到了另一個(gè) SettingWithCopyWarning 盡管我們使用了 loc 。這個(gè)問(wèn)題可能令人非常困惑,因?yàn)榫嫘畔⒔ㄗh我們的方法,我們已經(jīng)做過(guò)了。
不過(guò),想一下 winners 變量。它究竟是什么?由于我們通過(guò) data.loc[data.bid == data.price] 將它初始化,我們無(wú)法知道它是原始 data DataFrame 的視圖還是副本(因?yàn)?get 操作返回視圖或副本)。將初始化與生成警告的行組合在一起可以清楚地表明我們的錯(cuò)誤。
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
我們?cè)俅问褂昧随準(zhǔn)劫x值,只是這次它被分在了兩行代碼中。思考這個(gè)問(wèn)題的另一種方法是,問(wèn)一個(gè)問(wèn)題“這個(gè)操作會(huì)修改一個(gè)對(duì)象,還是兩個(gè)對(duì)象?”在我們的示例中,答案是未知的:如果 winners 是副本,那么只有 winners 受到影響,但如果是視圖,則 winners 和 data 都將被更新。這種情況可能發(fā)生在腳本或代碼庫(kù)中相距很遠(yuǎn)的行之間,這使問(wèn)題很難被追根溯源。
此處警告的意圖是讓我們意識(shí)到,我們以為代碼將修改原始 DataFrame ,實(shí)際沒(méi)有修改成功,或者說(shuō)我們將修改副本而不是原始數(shù)據(jù)。深入研究 Pandas GitHub repo 中的 issue,你可以看到開(kāi)發(fā)人員自己對(duì)這個(gè)問(wèn)題的解釋。
如何解決這個(gè)問(wèn)題在很大程度上取決于我們自己的意圖。如果我們想要使用原始數(shù)據(jù)的副本,解決方案就是強(qiáng)制 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 ,那么你應(yīng)該使用原始 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'
在更復(fù)雜的情況下,例如修改 DataFrame 子集的子集,不要使用鏈?zhǔn)剿饕梢栽谠?DataFrame 上通過(guò) loc 進(jìn)行修改。例如,你可以更改上面的新 winner_mask 變量或創(chuàng)建一個(gè)選擇中標(biāo)者子集的新變量,如下所示:
high_winner_mask = winner_mask & (data.price > 150)
data.loc[high_winner_mask].head()
| auctionid | bid | bidtime | bidder | bidderrate | openbid | price | bidtime_hours | |
|---|---|---|---|---|---|---|---|---|
| 225 | 8213387444 | 152.0 | 2.919757 | uconnbabydoll1975 | 15 | 0.99 | 152.0 | 70.074168 |
| 328 | 8213935134 | 207.5 | 2.983542 | toby2492 | 0 | 0.10 | 207.5 | 71.605008 |
| 416 | 8214430396 | 199.0 | 2.990463 | volpendesta | 4 | 9.99 | 199.0 | 71.771112 |
| 531 | 8215582227 | 152.5 | 2.999664 | ultimatum_man | 2 | 60.00 | 152.5 | 71.991936 |
這種技術(shù)會(huì)使得未來(lái)的代碼庫(kù)維護(hù)和擴(kuò)展更加穩(wěn)健。
歷史
你可能想知道為什么要造成這么混亂的現(xiàn)狀,為什么不明確指定索引方法是返回視圖還是副本,來(lái)完全避免 SettingWithCopy 問(wèn)題。要理解這一點(diǎn),我們必須研究 Pandas 的過(guò)去。
Pandas 確定返回一個(gè)視圖還是一個(gè)副本的邏輯,源于它對(duì) NumPy 庫(kù)的使用,這是 Pandas 庫(kù)的基礎(chǔ)。視圖實(shí)際上是通過(guò) NumPy 進(jìn)入 Pandas 的詞庫(kù)的。實(shí)際上,視圖在 NumPy 中很有用,因?yàn)樗鼈兡軌蚩深A(yù)測(cè)地返回。由于 NumPy 數(shù)組是單一類型的,因此 Pandas 嘗試使用最合適的 dtype 來(lái)最小化內(nèi)存處理需求。因此,包含單個(gè) dtype 的 DataFrame 切片可以作為單個(gè) NumPy 數(shù)組的視圖返回,這是一種高效處理方法。但是,多類型的切片不能以相同的方式存儲(chǔ)在 NumPy 中。Pandas 兼顧多種索引功能,并且保持高效地使用其 NumPy 內(nèi)核的能力。
最終,Pandas 中的索引被設(shè)計(jì)為有用且通用的方式,其核心并不完全與底層 NumPy 數(shù)組的功能相結(jié)合。隨著時(shí)間的推移,這些設(shè)計(jì)和功能元素之間的相互作用,導(dǎo)致了一組復(fù)雜的規(guī)則,這些規(guī)則決定了返回視圖還是副本。經(jīng)驗(yàn)豐富的 Pandas 開(kāi)發(fā)者通常都很滿意 Pandas 的做法,因?yàn)樗麄兛梢暂p松地瀏覽其索引行為。
不幸的是,對(duì)于 Pandas 的新手來(lái)說(shuō),鏈?zhǔn)剿饕龓缀跏遣豢杀苊獾模驗(yàn)?get 操作返回的就是可索引的 Pandas 對(duì)象。此外,用 Pandas 的核心開(kāi)發(fā)人員之一 Jeff Reback 的話來(lái)說(shuō),“從語(yǔ)言的角度來(lái)看,直接檢測(cè)鏈?zhǔn)剿饕遣豢赡艿模仨毥?jīng)過(guò)推斷才能了解”。
因此,在 2013 年底的 0.13.0 版本中引入了警告,作為許多開(kāi)發(fā)者遇到鏈?zhǔn)劫x值導(dǎo)致的無(wú)聲失敗的解決方案。
在 0.12 版本之前,ix 索引器是最受歡迎的(在 Pandas 術(shù)語(yǔ)中,“索引器”比如 ix,loc 和 iloc,是一種簡(jiǎn)單的結(jié)構(gòu),允許使用方括號(hào)來(lái)索引對(duì)象,就像數(shù)組一樣,但具有一些特殊的用法)。但是大約在 2013 年中 ,Pandas 項(xiàng)目開(kāi)始意識(shí)到日益增加的新手用戶的重要性,有動(dòng)力開(kāi)始提高新手用戶的使用體驗(yàn)。自從此版本發(fā)布以來(lái),loc 和 iloc 索引器因其更明確的性質(zhì)和更易于解釋的用法而受到青睞。(譯者注:pandas v0.23.3 (July 7, 2018),其中 ix 方法已經(jīng)被棄用)

SettingWithCopyWarning 在推出后持續(xù)改進(jìn),多年來(lái)在許多 GitHub issue 中得到了熱烈的討論 ,甚至還在不斷更新 ,但是要理解它,仍然是成為 Pandas 專家的關(guān)鍵。
總結(jié)
SettingWithCopyWarning 的基礎(chǔ)復(fù)雜性是 Pandas 庫(kù)中為數(shù)不多的坑。這個(gè)警告的源頭深深嵌在庫(kù)的底層中,不應(yīng)被忽視。Jeff Reback 自己的話 ,“我沒(méi)有找到任何你應(yīng)該忽略這個(gè)警告的情況。如果你做某些類型的索引時(shí)不起作用,而其他情況下起作用,你是在玩火。”
幸運(yùn)的是,解決警告只需要識(shí)別鏈?zhǔn)劫x值并修復(fù)。如果整篇文章你只了解到了一件事,那么就應(yīng)該是這一點(diǎn)。
作者:笨熊不緊張
鏈接:https://www.jianshu.com/p/72274ccb647a
來(lái)源:簡(jiǎn)書
簡(jiǎn)書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。

浙公網(wǎng)安備 33010602011771號(hào)