原文鏈接:https://www.dataquest.io/blog/settingwithcopywarning/
原文標題:Understanding SettingwithCopyWarning in pandas
原文發佈時間:5 JULY 2017(需要注意時效性,文中有一些方法已經棄用,比如 ix
)
作者:Benjamin Pryke
譯者:Ivy Lee
學習 Python 數據分析的同學總是遇到這個警告,查詢中文資料,一般只能找到個別的解決辦法,不一定適用於自己遇到的情況。查到的最常見解決辦法就是直接設置爲不顯示警告。這實際上並不能解決問題,搜索資料發現這篇英文講解SettingWithCopyWarning
原理非常系統的文章,翻譯了一下,分享給大家。
太長不看
一、解決方案:學會識別鏈式索引,不惜一切代價避免使用鏈式索引
注意:如果你看不懂這裏的解決方案,請閱讀此文的前半部分,直到真正理解如何去做
1. 如果要更改原始數據,請使用單一賦值操作(loc
):
data.loc[data.bidder == 'parakeet2004', 'bidderrate'] = 100
2. 如果想要一個副本,請確保強制讓 Pandas 創建副本:
winners = data.loc[data.bid == data.price].copy()
winners.loc[304, 'bidder'] = 'therealname'
二、強烈不推薦直接關閉警告,不過還是提供一下關閉警告的設置方法:
pd.set_option('mode.chained_assignment', None)
三、深度解析底層代碼和歷史演變(可選閱讀)
以下是正文部分:
SettingWithCopyWarning
是人們在學習 Pandas 時遇到的最常見的障礙之一。搜索引擎可以搜索到 Stack Overflow 上的問答、GitHub issues 和一些論壇帖子,分別提供了該警告在某些特定情況下的含義。會有這麼多人同樣遇到這個警告並不奇怪:有很多方法可以索引 Pandas 數據結構,每種數據結構都有各自的細微差別,甚至 Pandas 本身並不能保證兩行代碼的運行結果看起來完全相同。
本指南包含了生成警告的原因及解決方案,其中還包括一些底層細節,讓你更好地瞭解代碼內部的運行機制,最後提供了有關該話題的一些歷史情況,解釋代碼底層以這樣的方式運行的原因。
爲了探索 SettingWithCopyWarning
,我們將使用 eBay 3 天拍賣出售的 Xbox 的價格數據集,該數據集出自 Modelling Online Auctions 一書。先來了解下數據的基本結構:
import Pandas as pd
data = pd.read_csv('xbox-3-day-auctions.csv')
data.head()
如你所見,數據集的每一行都是某一次 eBay Xbox 出價信息。下面是對數據集中每列的簡要說明:
auctionid
- 每次拍賣的唯一標識符bid
- 本次拍賣出價bidtime
- 拍賣的時長,以天爲單位,從投標開始累計bidder
- 投標人的 eBay 用戶名bidderrate
- 投標人的 eBay 用戶評級openbid
- 賣方爲拍賣設定的開標價price
- 拍賣結束時的中標價
什麼是 SettingWithCopyWarning?
首先要理解的是,SettingWithCopyWarning
是一個警告 Warning,而不是錯誤 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
值,那麼先來查看一下用戶'parakeet2004'
的當前值:
data[data.bidder == 'parakeet2004']
有三行數據需要更新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
!
檢查一下用戶'parakeet2004'
的相關值,可以看到值沒有按預期改變:
data[data.bidder == 'parakeet2004']
這次警告是因爲將兩個索引操作鏈接在一起,直接使用了兩次方括號的鏈式索引比較容易理解。但如果使用其他訪問方法,例如.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
這就是警告的文本(Try using .loc[row_indexer,col_indexer] = value instead
)中建議的操作,在這種情況下完美適用。
隱蔽的鏈式操作(Hidden chaining)
現在來看遇到SettingWithCopyWarning
的第二種常見方式。創建一個新的 DataFrame 來探索中標者數據,因爲現在已經學習了鏈式賦值的內容,請注意使用 loc
:
winners = data.loc[data.bid == data.price]
winners.head()
winners
變量可能會被用來編寫一些後續代碼:
mean_win_time = winners.bidtime.mean()
... # 20 lines of code
mode_open_bid = winners.openbid.mode()
我們在偶然間發現了一個數據錯誤:標記爲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 = Nonewinners.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
如果 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
由於下面歷史部分中所述的原因,對多類型對象的索引 Get 操作將始終返回副本。而爲了提高效率,索引器對單類型對象的操作幾乎總是返回一個視圖,需要注意的是,這取決於對象的內存佈局,並不能完全保證。
誤報
誤報,即無意中報告鏈式賦值的情況,曾經在早期版本的 Pandas 中比較常見,但此後大部分都被解決了。爲了完整起見,在本文中包含一些已修復的誤報示例也是有用的。如果你在使用早期版本的 Pandas 時遇到以下任何情況,則可以安全地忽略或抑制警告(或通過升級 Pandas 版本完全避免警告!)
使用當前列的值,將新列添加到 DataFrame 會生成警告,但這已得到修復。
data['bidtime_hours'] = data.bidtime.map(lambda x: x * 24)
data.head(2)
在一個 DataFrame 切片上使用apply
方法進行 Set 時,也會出現誤報,不過這也已得到修復。
data.loc[:, 'bidtime_hours'] = data.bidtime.apply(lambda x: x * 24)
data.head(2)
直到 0.17.0 版本前,DataFrame.sample
方法中存在一個錯誤,導致SettingWithCopy
警告誤報。現在,sample
方法每次都會返回一個副本。
sample = data.sample(2)
sample.loc[:, 'price'] = 120
sample.head()
鏈式賦值深度解析
讓我們重用之前的例子:試圖更新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 的子集:可以創建對內存中原始數據的引用(視圖),也可以將子集複製到新的較小的 DataFrame 中(副本)。視圖是查看 原始 數據特定部分的一種方式;副本是將該數據 複製 到內存中的新位置。正如之前的圖表所示,修改視圖將修改原始變量,而修改副本則不會。
由於某些原因(本文稍後介紹),Pandas 中 Get 操作的輸出無法保證。索引 Pandas 數據結構時,視圖或副本都可能被返回,也就是說:對某一 DataFrame 進行 Get 操作返回一個新的 DataFrame,新的數據可能是:
- 來自原始對象的數據副本
- 沒有複製,而是直接對原始對象的引用
因爲不確定返回的對象是什麼,而且每種可能性都有非常不同後續影響,所以忽略警告就是“玩火”。
爲了更清楚地解釋視圖、副本和其中的歧義,我們創建一個簡單的 DataFrame 並對其進行索引:
df1 = pd.DataFrame(np.arange(6).reshape((3,2)), columns=list('AB'))
df1
將 df1
的子集賦值給 df2
:
df2 = df1.loc[:1]
df2
根據剛纔學到的知識,我們知道 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')]
這次拉出了兩列而不是一列。下面嘗試 Set 所有的bid
值。
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]['bid'] = 5.0
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
沒有效果,也沒有警告!我們在切片的副本上 Set 了一個值,但是 Pandas 沒有檢測到它 - 這就是假陰性。這是因爲,使用 loc
之後並不意味着可以再次使用鏈式賦值。這個特定的 bug,有一個未解決的 GitHub issue 。
正確的解決方法如下:
data.loc[data.bidder == 'parakeet2004', 'bid'] = 5.0
data.loc[data.bidder == 'parakeet2004', ('bidderrate', 'bid')]
你可能懷疑,是否真的有人會在實踐中遇到這樣的問題。其實這比你想象的更容易出現。當我們像下一節中這樣做:將 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
儘管使用了 loc
,還是得到了 SettingWithCopyWarning
。這可能令人非常困惑,因爲警告信息建議的方法,我們已經做過了。
不過,想一下 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
都將被更新。這種情況可能發生在腳本或代碼庫中相距很遠的行之間,這使問題很難被追根溯源。
此處警告的意圖是提醒,自以爲代碼將修改原始 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()
這種技術會使未來的代碼庫維護和擴展地更加穩健。
歷史
你可能想知道爲什麼要造成這麼混亂的現狀,爲什麼不明確指定索引方法是返回視圖還是副本,來完全避免 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 的話來說,“從語言的角度來看,直接檢測鏈式索引是不可能的,必須經過推斷才能瞭解”(It is simply not possible from a language perspective to detect chain indexing directly; it has to be inferred)。
因此,在 2013 年底的 0.13.0 版本中引入了警告,作爲許多開發者遇到鏈式賦值導致的無聲失敗的解決方案。
在 0.12 版本之前,ix 索引器是最受歡迎的(在 Pandas 術語中,“索引器”比如 ix
,loc
和 iloc
,是一種簡單的結構,允許使用方括號來索引對象,就像數組一樣,但具有一些特殊的用法)。但是大約在 2013 年 ,Pandas 項目開始意識到日益增加的新手用戶的重要性,有動力開始提高新手用戶的使用體驗。自從此版本發佈以來,loc
和 iloc
索引器因其更明確的性質和更易於解釋的用法而受到青睞。(譯者注:pandas v0.23.3 (July 7, 2018),其中 ix
方法已經被棄用)
Google Trends: Pandas
SettingWithCopyWarning
在推出後持續改進,多年來在許多 GitHub issue 中得到了熱烈的討論 ,甚至還在不斷更新 ,但是要理解它,仍然是成爲 Pandas 專家的關鍵。
總結
SettingWithCopyWarning
的基礎複雜性是 Pandas 庫中爲數不多的坑。這個警告的源頭深深嵌在庫的底層中,不應被忽視。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.”
幸運的是,解決警告只需要識別鏈式賦值並將其修復——看完本文你唯一需要理解的