python Pandas SettingwithCopy 警告解決方案

原文鏈接: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()

鏈式賦值深度解析

讓我們重用之前的例子:試圖更新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

將 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 術語中,“索引器”比如 ixloc 和 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.”

幸運的是,解決警告只需要識別鏈式賦值並將其修復——看完本文你唯一需要理解的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章