Pandas 中 SettingwithCopyWarning 的原理和解決方案

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)

  • 深度解析底層代碼和歷史演變(可選閱讀)
image

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)挚冤。

image

如上所示训挡,左側(cè)的視圖 df2 只是原始數(shù)據(jù) df1 一個子集宴凉,而右側(cè)的副本創(chuàng)建了一個新的對象 df2弥锄。

當我們嘗試對數(shù)據(jù)集進行更改時籽暇,這可能會出現(xiàn)問題:

image

根據(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

鏈式賦值深度解析

讓我們重用之前的例子:試圖更新 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 的子集:可以創(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 受到影響已添,但如果是視圖妥箕,則 winnersdata 都將被更新。這種情況可能發(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宫患,lociloc,是一種簡單的結(jié)構(gòu)这弧,允許使用方括號來索引對象娃闲,就像數(shù)組一樣,但具有一些特殊的用法)匾浪。但是大約在 2013 年中 皇帮,Pandas 項目開始意識到日益增加的新手用戶的重要性,有動力開始提高新手用戶的使用體驗蛋辈。自從此版本發(fā)布以來属拾,lociloc 索引器因其更明確的性質(zhì)和更易于解釋的用法而受到青睞。(譯者注:pandas v0.23.3 (July 7, 2018)冷溶,其中 ix 方法已經(jīng)被棄用

image

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>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市应结,隨后出現(xiàn)的幾起案子刨疼,更是在濱河造成了極大的恐慌,老刑警劉巖鹅龄,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揩慕,死亡現(xiàn)場離奇詭異,居然都是意外死亡扮休,警方通過查閱死者的電腦和手機迎卤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玷坠,“玉大人蜗搔,你說我怎么就攤上這事“吮ぃ” “怎么了樟凄?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長兄渺。 經(jīng)常有香客問我缝龄,道長,這世上最難降的妖魔是什么挂谍? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任叔壤,我火速辦了婚禮,結(jié)果婚禮上凳兵,老公的妹妹穿的比我還像新娘百新。我一直安慰自己,他們只是感情好庐扫,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布饭望。 她就那樣靜靜地躺著,像睡著了一般形庭。 火紅的嫁衣襯著肌膚如雪铅辞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天萨醒,我揣著相機與錄音斟珊,去河邊找鬼。 笑死富纸,一個胖子當著我的面吹牛囤踩,可吹牛的內(nèi)容都是我干的旨椒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼堵漱,長吁一口氣:“原來是場噩夢啊……” “哼综慎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起勤庐,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤示惊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后愉镰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體米罚,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年丈探,在試婚紗的時候發(fā)現(xiàn)自己被綠了录择。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡类嗤,死狀恐怖糊肠,靈堂內(nèi)的尸體忽然破棺而出辨宠,到底是詐尸還是另有隱情遗锣,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布嗤形,位于F島的核電站精偿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赋兵。R本人自食惡果不足惜笔咽,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望霹期。 院中可真熱鬧叶组,春花似錦、人聲如沸历造。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吭产。三九已至侣监,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間臣淤,已是汗流浹背橄霉。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留邑蒋,地道東北人姓蜂。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓按厘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钱慢。 傳聞我的和親對象是個殘疾皇子刻剥,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354