Pandas 中 SettingwithCopyWarning 的原理和解決方案(轉(zhuǎn)載练链,保留學(xué)習(xí)之用)
笨熊不緊張關(guān)注
4<time datetime="2018-07-31T14:43:46.000Z" style="box-sizing: border-box; margin-right: 10px;">2018.07.31 22:43:46</time>字數(shù) 6,177閱讀 38,275
<article class="_2rhmJa" style="box-sizing: border-box; display: block; font-weight: normal; line-height: 1.8; margin-bottom: 20px; caret-color: rgb(64, 64, 64); color: rgb(64, 64, 64); font-family: -apple-system, BlinkMacSystemFont, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-caps: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration: none;">
20190630 更新:優(yōu)化一些翻譯表達攻旦,增加【太長不看】部分
原文鏈接:https://www.dataquest.io/blog/settingwithcopywarning/
原文標題:Understanding SettingwithCopyWarning in pandas
原文發(fā)布時間:5 JULY 2017(需要注意時效性掏颊,文中有一些方法已經(jīng)棄用,比如 ix
)
作者:Benjamin Pryke
譯者:Ivy Lee
學(xué)習(xí) Python 數(shù)據(jù)分析的同學(xué)總是遇到這個警告具滴,查詢中文資料呜魄,一般只能找到個別的解決辦法悔叽,不一定適用于自己遇到的情況娇澎。查到的最常見解決辦法就是直接設(shè)置為不顯示警告。這實際上并不能解決問題睹晒,搜索資料發(fā)現(xiàn)這篇英文講解 SettingWithCopyWarning
原理非常系統(tǒng)的文章括细,翻譯了一下奋单,分享給大家览濒。
太長不看
- 解決方案:學(xué)會識別鏈式索引炫彩,不惜一切代價避免使用鏈式索引
注意:如果你看不懂這里的解決方案江兢,請閱讀此文前半部分杉允,直到真正理解如何去做
- 如果要更改原始數(shù)據(jù)叔磷,請使用單一賦值操作(
loc
):``` data.loc[data.bidder == 'parakeet2004', 'bidderrate'] = 100 ``` * 如果想要一個副本改基,請確保強制讓 Pandas 創(chuàng)建副本: ``` winners = data.loc[data.bid == data.price].copy() winners.loc[304, 'bidder'] = 'therealname' ```
- 強烈不推薦直接關(guān)閉警告秕狰,不過還是提供一下關(guān)閉警告的設(shè)置方法:
pd.set_option('mode.chained_assignment', None)
- 深度解析底層代碼和歷史演變(可選閱讀)
SettingWithCopyWarning
是人們在學(xué)習(xí) Pandas 時遇到的最常見的障礙之一鸣哀。搜索引擎可以搜索到 Stack Overflow 上的問答我衬、GitHub issues 和一些論壇帖子挠羔,分別提供了該警告在某些特定情況下的含義破加。會有這么多人同樣遇到這個警告并不奇怪:有很多方法可以索引 Pandas 數(shù)據(jù)結(jié)構(gòu),每種數(shù)據(jù)結(jié)構(gòu)都有各自的細微差別,甚至 Pandas 本身并不能保證兩行代碼的運行結(jié)果看起來完全相同端仰。
本指南包含了生成警告的原因及解決方案田藐,其中還包括一些底層細節(jié)汽久,讓你更好地了解代碼內(nèi)部的運行機制景醇,最后提供了有關(guān)該話題的一些歷史情況三痰,解釋代碼底層以這樣的方式運行的原因散劫。
為了探索 SettingWithCopyWarning
获搏,我們將使用 eBay 3 天拍賣出售的 Xbox 的價格數(shù)據(jù)集,該數(shù)據(jù)集出自 Modelling Online Auctions 一書纬乍。先來了解下數(shù)據(jù)的基本結(jié)構(gòu):
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 |
如你所見,數(shù)據(jù)集的每一行都是某一次 eBay Xbox 出價信息诅蝶。下面是對數(shù)據(jù)集中每列的簡要說明:
-
auctionid
- 每次拍賣的唯一標識符 -
bid
- 本次拍賣出價 -
bidtime
- 拍賣的時長调炬,以天為單位缰泡,從投標開始累計 -
bidder
- 投標人的 eBay 用戶名 -
bidderrate
- 投標人的 eBay 用戶評級 -
openbid
- 賣方為拍賣設(shè)定的開標價 -
price
- 拍賣結(jié)束時的中標價
什么是 SettingWithCopyWarning棘钞?
首先要理解的是宜猜,SettingWithCopyWarning
是一個警告 Warning姨拥,而不是錯誤 Error叫乌。
錯誤表明某些內(nèi)容是“壞掉”的憨奸,例如無效語法(invalid syntax)或嘗試引用未定義的變量膀藐;警告的作用是提醒編程人員额各,他們的代碼可能存在潛在的錯誤或問題虾啦,但是這些操作在該編程語言中依然合法傲醉。在這種情況下硬毕,警告很可能表明一個嚴重但不容易意識到的錯誤吐咳。
SettingWithCopyWarning
告訴你韭脊,你的操作可能沒有按預(yù)期運行沪羔,需要檢查結(jié)果以確保沒有出錯。
如果代碼確實按預(yù)期工作愉豺,那么我們會很容易忽略該警告粒氧,但是 SettingWithCopyWarning
不應(yīng)該被忽略摘盆。在進行下一步操作之前孩擂,我們需要花點時間了解這一警告顯示的原因类垦。
要了解 SettingWithCopyWarning
蚤认,首先要知道砰琢,Pandas 中的某些操作會返回數(shù)據(jù)的視圖(View)陪汽,某些操作會返回數(shù)據(jù)的副本(Copy)挚冤。
如上所示训挡,左側(cè)的視圖 df2
只是原始數(shù)據(jù) df1
一個子集宴凉,而右側(cè)的副本創(chuàng)建了一個新的對象 df2
弥锄。
當我們嘗試對數(shù)據(jù)集進行更改時籽暇,這可能會出現(xiàn)問題:
根據(jù)需求戒悠,我們可能想要修改原始 df1
(左)绸狐,也可能想要修改 df2
(右)寒矿。警告讓我們知道拆融,代碼可能并沒有符合需求镜豹,修改到的可能并不是我們想要修改的那個數(shù)據(jù)集趟脂。
稍后會深入研究這個問題散怖,但是現(xiàn)在先來了解一下镇眷,警告出現(xiàn)的兩個主要原因以及對應(yīng)的解決方案欠动。
鏈式賦值(Chained Assignment)
當 Pandas 檢測到鏈式賦值(Chained Assignment)時會生成警告。為了方便后續(xù)的解釋人芽,先來解釋一些術(shù)語:
- 賦值(Assignment) - 設(shè)置某些變量值的操作萤厅,例如
data = pd.read_csv('xbox-3-day-auctions.csv')
惕味,有時會將這個操作稱之為 設(shè)置(Set) 名挥。 - 訪問(Access) - 返回某些值的操作榄融,具體參照下方的索引和鏈式索引示例剃袍。有時會將這個操作稱之為 獲取(Get) 涛救。
- 索引(Indexing) - 任何引用數(shù)據(jù)子集的賦值或訪問方法检吆,例如
data[1:5]
蹭沛。 - 鏈式索引(Chaining) - 連續(xù)使用多個索引操作,例如
data[1:5][1:3]
帚呼。
鏈式賦值是鏈式索引和賦值的組合煤杀。先快速瀏覽一下之前加載的數(shù)據(jù)集沈自,稍后將詳細介紹枯途。在這個例子中呆躲,假設(shè)我們了解到用戶 'parakeet2004'
的 bidderrate
值不正確插掂,需要修改這個 bidderrate
值辅甥,那么先來查看一下用戶 'parakeet2004'
的當前值:
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 |
有三行數(shù)據(jù)需要更新 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__':
神奇夏块!我們“創(chuàng)造”出了 SettingWithCopyWarning
!
檢查一下用戶 'parakeet2004'
的相關(guān)值政己,可以看到值沒有按預(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 |
這次警告是因為將兩個索引操作鏈接在一起,直接使用了兩次方括號的鏈式索引比較容易理解沦泌。但如果使用其他訪問方法赦肃,例如 .bidderrate
他宛、.loc[]
、.iloc[]
队塘、.ix[]
憔古,也會如此鸿市,這次的鏈式操作有:
data[data.bidder == 'parakeet2004']
['bidderrate'] = 100
以上兩個鏈式操作一個接一個地獨立執(zhí)行陌凳。第一次鏈式操作是為了 Get合敦,返回一個 DataFrame充岛,其中包含所有 bidder
等于 'parakeet2004'
的行;第二次鏈式操作是為了 Set,是在這個新返回的 DataFrame 上運行的爪膊,并沒有修改原始的 DataFrame推盛。
這種情況對應(yīng)的解決方案很簡單:使用 loc
將兩次鏈式操作組合成一步操作,確保 Pandas 進行 Set 的是原始 DataFrame瘪菌。Pandas 始終確保下面這樣的非鏈式 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
這就是警告的文本(Try using .loc[row_indexer,col_indexer] = value instead
)中建議的操作师妙,在這種情況下完美適用怔檩。
隱蔽的鏈式操作(Hidden chaining)
現(xiàn)在來看遇到 SettingWithCopyWarning
的第二種常見方式薛训。創(chuàng)建一個新的 DataFrame 來探索中標者數(shù)據(jù),因為現(xiàn)在已經(jīng)學(xué)習(xí)了鏈式賦值的內(nèi)容膊爪,請注意使用 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 |
winners
變量可能會被用來編寫一些后續(xù)代碼:
mean_win_time = winners.bidtime.mean()
... # 20 lines of code
mode_open_bid = winners.openbid.mode()
我們在偶然間發(fā)現(xiàn)了一個數(shù)據(jù)錯誤:標記為 304
的行中缺少了 bidder
值。
winners.loc[304, 'bidder']
nan
對這個例子來說,假設(shè)我們已知該投標人的真實用戶名跳芳,并據(jù)此更新數(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
另一個 SettingWithCopyWarning
!但是這次使用了 loc
吓歇,為什么還會出現(xiàn)警告城看?讓我們來看代碼的結(jié)果來一探究竟:
print(winners.loc[304, 'bidder'])
therealname
代碼確實起了預(yù)期的作用,為什么仍然出現(xiàn)警告轰胁?
鏈式索引可能在一行代碼內(nèi)發(fā)生,也可能跨越兩行代碼凹耙。因為 winners
變量是作為 Get 操作的輸出創(chuàng)建的(data.loc[data.bid == data.price]
)肖抱,它可能是原始 DataFrame 的副本提佣,也可能不是拌屏,除非檢查,否則我們不能確認端圈。對 winners
進行索引時舱权,實際上使用的就是鏈式索引。
這意味著當我們嘗試修改 winners
時鸵贬,可能也修改了 data
恭理。
在實際的代碼中涯保,相關(guān)的兩行鏈式索引代碼之間未荒,可能相距很多行其他代碼片排,追蹤問題可能會更困難率寡,但大致情況是與示例類似的迫卢。
這種情況下的警告解決方案是:創(chuàng)建新 DataFrame 時明確告知 Pandas 創(chuàng)建一個副本:
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
就這么簡單!
竅門就是冶共,學(xué)會識別鏈式索引乾蛤,不惜一切代價避免使用鏈式索引。如果要更改原始數(shù)據(jù)捅僵,請使用單一賦值操作。如果你想要一個副本庙楚,請確保你強制讓 Pandas 創(chuàng)建副本上荡。這樣既可以節(jié)省時間,也可以使代碼保持邏輯嚴密醋奠。
另外請注意榛臼,即使 SettingWithCopyWarning
只在你進行 Set 時才會發(fā)生,但在進行 Get 操作時窜司,最好也避免使用鏈式索引沛善。鏈式操作代碼效率較低,而且只要稍后進行賦值塞祈,就會導(dǎo)致問題金刁。
處理 SettingWithCopyWarning 的提示和技巧
在進行下面更深入的分析之前,讓我們看看 SettingWithCopyWarning
的更多細節(jié)议薪。
關(guān)閉警告
如果不討論如何明確地控制 SettingWithCopy
警告設(shè)置尤蛮,本文則不夠完整。Pandas 的 mode.chained_assignment
選項可以采用以下幾個值之一:
-
'raise'
- 拋出異常(exception)而不是警告 -
'warn'
- 生成警告(默認) -
None
- 完全關(guān)閉警告
例如斯议,如果要關(guān)閉警告:
pd.set_option('mode.chained_assignment', None)
data[data.bidder == 'parakeet2004']['bidderrate'] = 100
這樣沒有給出任何提示或警告产捞,除非完全了解代碼的運行情況,否則請不要嘗試哼御。只要你對想要實現(xiàn)的代碼功能有任何一丁點疑問坯临,不要關(guān)閉警告。有些開發(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)驗的 Pandas 開發(fā)人員合作開發(fā)項目看靠,或者正在開發(fā)需要高度嚴謹?shù)捻椖浚@可能特別有用液肌。
更精確使用此設(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
如你所見,這種方法可以實現(xiàn)針對性的警告設(shè)置,而不影響整個環(huán)境谤祖。
is_copy 屬性
避免警告的另一個技巧是修改 Pandas 用于解釋 SettingWithCopy
的工具之一婿滓。每個 DataFrame 都有一個 is_copy
屬性,默認情況下為 None
粥喜,但如果它是副本空幻,則會使用 weakref
引用原始 DataFrame 。通過將 is_copy
設(shè)置為 None
容客,可以避免生成警告秕铛。
winners = data.loc[data.bid == data.price]
winners.is_copy = None
winners.loc[304, 'bidder'] = 'therealname'
但是請注意,這并不會奇跡般地解決問題缩挑,反而會使錯誤檢測變得更加困難但两。
單類型 VS 多類型對象
值得強調(diào)的另一點是單類型對象和多類型對象之間的差異。如果 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 |
由于下面歷史部分中所述的原因紧阔,對多類型對象的索引 Get 操作將始終返回副本。而為了提高效率续担,索引器對單類型對象的操作幾乎總是返回一個視圖擅耽,需要注意的是,這取決于對象的內(nèi)存布局物遇,并不能完全保證乖仇。
誤報
誤報,即無意中報告鏈式賦值的情況询兴,曾經(jīng)在早期版本的 Pandas 中比較常見乃沙,但此后大部分都被解決了。為了完整起見诗舰,在本文中包含一些已修復(fù)的誤報示例也是有用的警儒。如果你在使用早期版本的 Pandas 時遇到以下任何情況,則可以安全地忽略或抑制警告(或通過升級 Pandas 版本完全避免警告?舾)
使用當前列的值蜀铲,將新列添加到 DataFrame 會生成警告,但這已得到修復(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 |
在一個 DataFrame 切片上使用 apply
方法進行 Set 時蝙茶,也會出現(xiàn)誤報艺骂,不過這也已得到修復(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
方法中存在一個錯誤,導(dǎo)致 SettingWithCopy
警告誤報”鸱現(xiàn)在蹄衷,sample
方法每次都會返回一個副本。
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 |
鏈式賦值深度解析
讓我們重用之前的例子:試圖更新 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 用 SettingWithCopyWarning
告訴我們的是愧口,代碼的行為是模棱兩可的,要理解原因和警告的措辭类茂,以下概念將會有所幫助耍属。
之前簡要了解了視圖(View)和副本(Copy)。有兩種方法可以訪問 DataFrame 的子集:可以創(chuàng)建對內(nèi)存中原始數(shù)據(jù)的引用(視圖)巩检,也可以將子集復(fù)制到新的較小的 DataFrame 中(副本)厚骗。視圖是查看 原始 數(shù)據(jù)特定部分的一種方式;副本是將該數(shù)據(jù) 復(fù)制 到內(nèi)存中的新位置兢哭。正如之前的圖表所示领舰,修改視圖將修改原始變量,而修改副本則不會迟螺。
由于某些原因(本文稍后介紹)冲秽,Pandas 中 Get 操作的輸出無法保證。索引 Pandas 數(shù)據(jù)結(jié)構(gòu)時矩父,視圖或副本都可能被返回锉桑,也就是說:對某一 DataFrame 進行 Get 操作返回一個新的 DataFrame,新的數(shù)據(jù)可能是:
- 來自原始對象的數(shù)據(jù)副本
- 沒有復(fù)制窍株,而是直接對原始對象的引用
因為不確定返回的對象是什么刨仑,而且每種可能性都有非常不同后續(xù)影響,所以忽略警告就是“玩火”夹姥。
為了更清楚地解釋視圖杉武、副本和其中的歧義,我們創(chuàng)建一個簡單的 DataFrame 并對其進行索引:
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é)到的知識辙售,我們知道 df2
可能是 df1
的視圖或 df1
子集的副本轻抱。
在解決問題之前,我們還需要再看一下鏈式索引旦部。擴展一下 'parakeet2004'
示例祈搜,將兩個索引操作鏈接在一起:
data[data.bidder == 'parakeet2004']
__intermediate__['bidderrate'] = 100
__intermediate__
表示第一個調(diào)用的輸出,對我們是完全不可見的士八。請記住容燕,如果我們使用了屬性訪問(.
+列名形式的訪問),會得到相同的有問題的結(jié)果:
data[data.bidder == 'parakeet2004'].bidderrate = 100
這同樣適用于任何其他形式的鏈式調(diào)用,因為我們正在生成中間對象 。
在底層代碼中铭若,鏈式索引意味著對 __getitem__
或 __setitem__
進行多次調(diào)用以完成單個操作墙基。這些是 特殊的 Python 方法寂纪,通過在實現(xiàn)它們類的實例上使用方括號藏澳,可以調(diào)用這些方法兄朋,這是一種語法糖徐块。下面看一下 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)意識到毛秘,SettingWithCopyWarning
是由此鏈式 __setitem__
調(diào)用生成的∽杩危可以自己嘗試一下 - 上面這些代碼的功能相同叫挟。為清楚起見,請注意第二個 __getitem__
調(diào)用(對 bidder
列)是嵌套的限煞,而不是鏈式問題的所有部分霞揉。
通常,如上面所述晰骑,Pandas 不保證 Get 操作是返回視圖還是副本适秩。如果示例中返回了一個視圖,則鏈式賦值中的第二個表達式將是對原始對象 __setitem__
的調(diào)用硕舆。但是秽荞,如果返回一個副本,那么將被修改的是副本 - 原始對象不會被修改抚官。
這就是警告中 “a value is trying to be set on a copy of a slice from a DataFrame” 的含義扬跋。由于沒有對此副本的引用,它最終將被回收 凌节。SettingWithCopyWarning
讓我們知道 Pandas 無法確定第一個 __getitem__
調(diào)用是否返回了視圖或副本钦听,因此不清楚該賦值是否更改了原始對象。換一種說法就是:“我們是否正在修改原始數(shù)據(jù)倍奢?”這一問題的答案是未知的朴上。
如果確實想要修改原始文件,警告建議的解決方案是使用 loc
將這兩個單獨的鏈式操作轉(zhuǎn)換為單個賦值操作卒煞。這樣代碼中沒有了鏈式索引痪宰,就不會再收到警告。修改后的代碼及其擴展版本如下所示:
# 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
并沒有結(jié)束問題,因為使用 loc
的 Get 操作仍然可以返回一個視圖或副本扮饶,下面是個有點復(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 |
沒有效果扛点,也沒有警告哥遮!我們在切片的副本上 Set 了一個值,但是 Pandas 沒有檢測到它 - 這就是假陰性占键。這是因為,使用 loc
之后并不意味著可以再次使用鏈式賦值元潘。這個特定的 bug畔乙,有一個未解決的 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 |
你可能懷疑翩概,是否真的有人會在實踐中遇到這樣的問題牲距。其實這比你想象的更容易出現(xiàn)。當我們像下一節(jié)中這樣做:將 DataFrame 查詢的結(jié)果賦值給變量钥庇。
隱藏的鏈式索引
再看一下之前隱藏的鏈式索引示例牍鞠,我們試圖設(shè)置 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
盡管使用了 loc
难述,還是得到了 SettingWithCopyWarning
。這可能令人非常困惑吐句,因為警告信息建議的方法胁后,我們已經(jīng)做過了。
不過嗦枢,想一下 winners
變量究竟是什么攀芯?由于我們通過 data.loc[data.bid == data.price]
將它初始化,無法知道它是原始 data
的視圖還是副本(因為 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
受到影響已添,但如果是視圖妥箕,則 winners
和 data
都將被更新。這種情況可能發(fā)生在腳本或代碼庫中相距很遠的行之間更舞,這使問題很難被追根溯源畦幢。
此處警告的意圖是提醒,自以為代碼將修改原始 DataFrame缆蝉,實際沒有修改成功宇葱,或者說我們將修改副本而不是原始數(shù)據(jù)瘦真。深入研究 Pandas GitHub repo 中的 issue,可以看到開發(fā)人員自己對這個問題的解釋黍瞧。
如何解決這個問題在很大程度上取決于自己的意圖诸尽。如果想要使用原始數(shù)據(jù)的副本,解決方案就是強制 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 子集的子集,不要使用鏈式索引矢否,可以在原始 DataFrame 上通過 loc
進行修改仲闽。例如,可以更改上面的新 winner_mask
變量或創(chuàng)建一個選擇中標者子集的新變量僵朗,如下所示:
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ù)會使未來的代碼庫維護和擴展地更加穩(wěn)健赖欣。
歷史
你可能想知道為什么要造成這么混亂的現(xiàn)狀,為什么不明確指定索引方法是返回視圖還是副本验庙,來完全避免 SettingWithCopy
問題畏鼓。要理解這個問題,必須研究 Pandas 的過去壶谒。
Pandas 確定返回一個視圖還是一個副本的邏輯云矫,源于它對 NumPy 庫的使用,這是 Pandas 庫的基礎(chǔ)汗菜。視圖實際上是通過 NumPy 進入 Pandas 的詞庫的让禀。實際上,視圖在 NumPy 中很有用陨界,因為它們能夠可預(yù)測地返回巡揍。由于 NumPy 數(shù)組是單一類型的,因此 Pandas 嘗試使用最合適的 dtype 來最小化內(nèi)存處理需求菌瘪。因此腮敌,包含單個 dtype 的 DataFrame 切片可以作為單個 NumPy 數(shù)組的視圖返回,這是一種高效處理方法俏扩。但是糜工,多類型的切片不能以相同的方式存儲在 NumPy 中。Pandas 兼顧多種索引功能录淡,并且保持高效地使用其 NumPy 內(nèi)核的能力捌木。
最終,Pandas 中的索引被設(shè)計為有用且通用的方式嫉戚,其核心并不完全與底層 NumPy 數(shù)組的功能相結(jié)合刨裆。隨著時間的推移澈圈,這些設(shè)計和功能元素之間的相互作用,導(dǎo)致了一組復(fù)雜的規(guī)則帆啃,這些規(guī)則決定了返回視圖還是副本瞬女。經(jīng)驗豐富的 Pandas 開發(fā)者通常都很滿意 Pandas 的做法,因為他們可以輕松地瀏覽其索引行為努潘。
不幸的是诽偷,對于 Pandas 的新手來說,鏈式索引幾乎不可避免慈俯,因為 Get 操作返回的就是可索引的 Pandas 對象渤刃。此外拥峦,用 Pandas 的核心開發(fā)人員之一 Jeff Reback 的話來說贴膘,“從語言的角度來看,直接檢測鏈式索引是不可能的略号,必須經(jīng)過推斷才能了解”(It is simply not possible from a language perspective to detect chain indexing directly; it has to be inferred)刑峡。
因此,在 2013 年底的 0.13.0 版本中引入了警告玄柠,作為許多開發(fā)者遇到鏈式賦值導(dǎo)致的無聲失敗的解決方案突梦。
在 0.12 版本之前,ix
索引器是最受歡迎的(在 Pandas 術(shù)語中羽利,“索引器”比如 ix
宫患,loc
和 iloc
,是一種簡單的結(jié)構(gòu)这弧,允許使用方括號來索引對象娃闲,就像數(shù)組一樣,但具有一些特殊的用法)匾浪。但是大約在 2013 年中 皇帮,Pandas 項目開始意識到日益增加的新手用戶的重要性,有動力開始提高新手用戶的使用體驗蛋辈。自從此版本發(fā)布以來属拾,loc
和 iloc
索引器因其更明確的性質(zhì)和更易于解釋的用法而受到青睞。(譯者注:pandas v0.23.3 (July 7, 2018)冷溶,其中 ix
方法已經(jīng)被棄用)
SettingWithCopyWarning
在推出后持續(xù)改進渐白,多年來在許多 GitHub issue 中得到了熱烈的討論,甚至還在不斷更新 逞频,但是要理解它礼预,仍然是成為 Pandas 專家的關(guān)鍵。
總結(jié)
SettingWithCopyWarning
的基礎(chǔ)復(fù)雜性是 Pandas 庫中為數(shù)不多的坑虏劲。這個警告的源頭深深嵌在庫的底層中托酸,不應(yīng)被忽視褒颈。Jeff Reback 自己的話 ,“Their are no cases that I am aware that you should actually ignore this warning. ……If you do certain types of indexing it will never work, others it will work. You are really playing with fire.”
幸運的是励堡,解決警告只需要識別鏈式賦值并將其修復(fù)——看完本文你唯一需要理解的事谷丸。
</article>