原文連接:https://www.dataquest.io/blog/settingwithcopywarning/
原文標題:Understanding SettingwithCopyWarning in pandas
原文發佈時間:5 JULY 2017(須要注意時效性,文中有一些方法已經棄用,好比 ix
)
做者:Benjamin Pryke
譯者:Ivy Leehtml
學習 Python 數據分析的同窗老是遇到這個警告,查詢中文資料,通常只能找到個別的解決辦法,不必定適用於本身遇到的狀況。查到的最多見解決辦法就是直接設置爲不顯示警告。搜索資料發現這篇英文講解 SettingWithCopyWarning
原理很是系統的文章,翻譯了一下,分享給你們。python
SettingWithCopyWarning
是人們在學習 Pandas 時遇到的最多見的障礙之一。快速的網絡搜索能夠搜索到 Stack Overflow 問題,GitHub issues 和程序員的論壇帖子,試圖解釋這個警告在他們的特定狀況下意味着什麼。這麼多人爲此困擾並不奇怪:有不少方法能夠索引 Pandas 數據結構,每種數據結構都有本身獨特的細微差異,甚至 Pandas 自己並不能保證兩行代碼的運行結果看起來徹底相同。git
本指南解釋了生成警告的緣由並展現瞭如何解決這一警告。它還包括一些底層的細節,讓你更好地瞭解代碼內部發生了什麼,提供了有關該話題的一些歷史記錄,讓你瞭解爲何代碼底層以這樣的方式運做。程序員
爲了探索 SettingWithCopyWarning
,咱們將使用 Modelling Online Auctions 一書中的 eBay 3 天拍賣出售的 Xbox 的價格數據集。讓咱們來看看:github
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 |
如你所見,數據集的每一行都是某一次 eBay Xbox 出價信息。如下是該數據集中每列的簡要說明:shell
auctionid
- 每次拍賣的惟一標識符bid
- 本次拍賣出價bidtime
- 拍賣的時長,以天爲單位,從投標開始累計bidder
- 投標人的 eBay 用戶名bidderrate
- 投標人的 eBay 用戶評級openbid
- 賣方爲拍賣設定的開標價price
- 拍賣結束時的中標價首先要理解的是,SettingWithCopyWarning
是一個警告,而不是錯誤 Error。編程
錯誤代表某些內容是「壞掉」的,例如無效語法(invalid syntax)或嘗試引用未定義的變量。警告的做用是提醒程序員,他們的代碼可能存在潛在的錯誤或問題,可是這些操做仍然是該編程語言中的合法操做。在這種狀況下,警告極可能代表一個嚴重但不容易意識到的錯誤。數組
SettingWithCopyWarning
告訴你,你的操做可能沒有按預期運行,你應該檢查結果以確保沒有出錯。安全
若是你的代碼仍然按預期工做,那麼很容易忽略警告。這不是良好的實踐,SettingWithCopyWarning
不該該被忽略。在採起下一步行動以前,花點時間瞭解爲何會得到這一警告。bash
要了解 SettingWithCopyWarning
,首先須要瞭解 Pandas 中的某些操做能夠返回數據的視圖(View),而某些其餘操做將返回數據的副本(Copy)。
如上所示,左側的視圖 df2
只是原始 df1
一個子集,而右側的副本建立了一個新的惟一對象 df2
。
當咱們嘗試對數據集進行更改時,這可能會致使問題:
根據咱們的需求,咱們可能想要修改原始 df1
(左),可能想要修改 df2
(右)。警告讓咱們知道,咱們的代碼可能並無符合需求,修改的並非咱們想要修改的那個數據集。
咱們稍後會深刻研究這個問題,可是如今先來了解一下,警告出現的兩個主要緣由以及如何解決它們。
當 Pandas 檢測鏈式賦值(Chained assignment)時會生成警告。讓咱們定義一些術語,方便後續的解釋:
data = pd.read_csv('xbox-3-day-auctions.csv')
。也被稱爲設置(set) 。data[1:5]
。data[1:5][1:3]
。鏈式賦值是鏈式索引和賦值的組合。先快速瀏覽一下以前加載的數據集,稍後咱們將詳細介紹這一點。在這個例子中,假設咱們瞭解到用戶 'parakeet2004'
的 bidderrate 不正確,咱們必須修改他的 bidderrate,首先,查看一下當前的值。
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
字段,咱們繼續往下操做:
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
!
若是檢查一下,能夠看到在這種狀況下,值沒有按預期改變:
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
這兩個鏈式操做一個接一個地獨立執行。第一次是訪問操做(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
這就是警告中建議咱們作的操做,在這種狀況下它完美地適用。
如今來看一下遇到 SettingWithCopyWarning
的第二種最多見的方式。咱們來探索中標者的數據,咱們將爲此建立一個新的 DataFrame,如今已經學習了關於鏈式賦值的內容,所以請注意使用 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
變量編寫一些後續的代碼行。
mean_win_time = winners.bidtime.mean()
... # 20 lines of code mode_open_bid = winners.openbid.mode()
偶然的機會,咱們在該 DataFrame
發現了另外一個錯誤:標記爲 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
的更多細節。
首先,若是不討論如何明確地控制 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
正如你所看到的,這種方法能夠實現針對性的警告設置,而不是影響整個環境。
避免警告的另外一個技巧是修改 Pandas 用於解釋 SettingWithCopy
的工具之一。每一個 DataFrame
都有一個 is_copy
屬性,默認狀況下爲 None
,但若是它是副本,則會使用 weakref
引用原始 DataFrame
。經過將 is_copy
設置爲 None
,能夠避免生成警告。
winners = data.loc[data.bid == data.price]
winners.is_copy = None winners.loc[304, 'bidder'] = 'therealname'
可是請注意,這並不會奇蹟般地解決問題,反而會使錯誤檢測變得很是困難。
值得強調的另外一點是單類型對象和多類型對象之間的差別。若是 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 操做將始終返回副本。然而,爲了提升效率,索引器對單類型對象的操做幾乎老是返回一個視圖,這裏須要注意的是,這取決於對象的內存佈局,並不能徹底保證。
誤報,即無心中報告鏈式賦值的狀況,曾經在早期版本的 Pandas 中比較常見,但此後大部分都被解決了。爲了完整起見,在此處包括一些已修復的誤報示例也是有用的。若是你在使用早期版本的 Pandas 時遇到如下任何狀況,則能夠安全地忽略或抑制警告(或經過升級徹底避免警告!)
使用當前列的值,將新列添加到 DataFrame
會生成警告,但這已獲得修復。
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
方法進行設置時,也會出現誤報,不過這也已獲得修復。
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
方法中存在一個錯誤,致使 SettingWithCopy
警告誤報。如今,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
的子集:能夠建立對內存中原始數據的引用(視圖),也能夠將子集複製到新的較小的 DataFrame
中(副本)。視圖是查看 原始 數據特定部分的一種方式,而副本是將該數據克隆到內存中的新位置。正如咱們以前的圖表所示,修改視圖將修改原始變量,但修改副本則不會。
因爲某些咱們將在稍後介紹的緣由,Pandas 中 get 操做的輸出沒法保證。索引 Pandas 數據結構時,視圖或副本均可能被返回,這意味着對某一 DataFrame
進行 get 操做返回一個新的 DataFrame
,這個新的數據多是:
由於咱們不知道將會發生什麼,而且每種可能性都有很是不一樣的行爲,因此忽略警告就是「玩火」。
爲了更清楚地解釋視圖、副本和其中的歧義,讓咱們建立一個簡單的 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 |
根據剛纔學到的知識,咱們知道 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
自己,具備擴展的索引功能。
使用 loc
並無結束咱們的問題,由於使用 loc
的 get 操做仍然能夠返回一個視圖或副本。讓咱們快速過一下,一個有點複雜的例子。
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 |
你可能懷疑,是否有人會在實踐中遇到這樣的問題。其實這比你想象的更容易出現。當咱們像下一節中這樣作:將 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
咱們獲得了另外一個 SettingWithCopyWarning
儘管咱們使用了 loc
。這個問題可能使人很是困惑,由於警告信息建議咱們的方法,咱們已經作過了。
不過,想一下 winners
變量。它到底是什麼?因爲咱們經過 data.loc[data.bid == data.price]
將它初始化,咱們沒法知道它是原始 data
DataFrame
的視圖仍是副本(由於 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()
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 |
這種技術會使得將來的代碼庫維護和擴展更加穩健。
你可能想知道爲何要形成這麼混亂的現狀,爲何不明確指定索引方法是返回視圖仍是副本,來徹底避免 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 的話來講,「從語言的角度來看,直接檢測鏈式索引是不可能的,必須通過推斷才能瞭解」。
所以,在 2013 年末的 0.13.0 版本中引入了警告,做爲許多開發者遇到鏈式賦值致使的無聲失敗的解決方案。
在 0.12 版本以前,ix
索引器是最受歡迎的(在 Pandas 術語中,「索引器」好比 ix
,loc
和 iloc
,是一種簡單的結構,容許使用方括號來索引對象,就像數組同樣,但具備一些特殊的用法)。可是大約在 2013 年中 ,Pandas 項目開始意識到日益增長的新手用戶的重要性,有動力開始提升新手用戶的使用體驗。自今後版本發佈以來,loc
和 iloc
索引器因其更明確的性質和更易於解釋的用法而受到青睞。(譯者注:pandas v0.23.3 (July 7, 2018),其中 ix
方法已經被棄用)
SettingWithCopyWarning
在推出後持續改進,多年來在許多 GitHub issue 中獲得了熱烈的討論 ,甚至還在不斷更新 ,可是要理解它,仍然是成爲 Pandas 專家的關鍵。
SettingWithCopyWarning
的基礎複雜性是 Pandas 庫中爲數很少的坑。這個警告的源頭深深嵌在庫的底層中,不該被忽視。Jeff Reback 本身的話 ,「我沒有找到任何你應該忽略這個警告的狀況。若是你作某些類型的索引時不起做用,而其餘狀況下起做用,你是在玩火。」
幸運的是,解決警告只須要識別鏈式賦值並修復。若是整篇文章你只瞭解到了一件事,那麼就應該是這一點。