Pandas 中 SettingwithCopyWarning 的原理和解決方案

原文連接: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?

首先要理解的是,SettingWithCopyWarning 是一個警告,而不是錯誤 Error。編程

錯誤代表某些內容是「壞掉」的,例如無效語法(invalid syntax)或嘗試引用未定義的變量。警告的做用是提醒程序員,他們的代碼可能存在潛在的錯誤或問題,可是這些操做仍然是該編程語言中的合法操做。在這種狀況下,警告極可能代表一個嚴重但不容易意識到的錯誤。數組

SettingWithCopyWarning 告訴你,你的操做可能沒有按預期運行,你應該檢查結果以確保沒有出錯。安全

若是你的代碼仍然按預期工做,那麼很容易忽略警告。這不是良好的實踐,SettingWithCopyWarning 不該該被忽略。在採起下一步行動以前,花點時間瞭解爲何會得到這一警告。bash

要了解 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,首先,查看一下當前的值。

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 

這就是警告中建議咱們作的操做,在這種狀況下它完美地適用。

隱蔽的鏈式操做(Hidden chaining)

如今來看一下遇到 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 的提示和技巧

在咱們進行下面更深刻的分析以前,讓咱們「拿出顯微鏡」,看看 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 = None winners.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 
  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

鏈式賦值深度解析

讓咱們重用以前的例子:試圖更新 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 
  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 自己,具備擴展的索引功能。

假陰性(False negatives)

使用 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 受到影響,但若是是視圖,則 winnersdata 都將被更新。這種狀況可能發生在腳本或代碼庫中相距很遠的行之間,這使問題很難被追根溯源。

此處警告的意圖是讓咱們意識到,咱們覺得代碼將修改原始 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 術語中,「索引器」好比 ixlociloc,是一種簡單的結構,容許使用方括號來索引對象,就像數組同樣,但具備一些特殊的用法)。可是大約在 2013 年中 ,Pandas 項目開始意識到日益增長的新手用戶的重要性,有動力開始提升新手用戶的使用體驗。自今後版本發佈以來,lociloc 索引器因其更明確的性質和更易於解釋的用法而受到青睞。(譯者注:pandas v0.23.3 (July 7, 2018),其中 ix 方法已經被棄用

Google Trends: Pandas

SettingWithCopyWarning 在推出後持續改進,多年來在許多 GitHub issue 中獲得了熱烈的討論 ,甚至還在不斷更新 ,可是要理解它,仍然是成爲 Pandas 專家的關鍵。

總結

SettingWithCopyWarning 的基礎複雜性是 Pandas 庫中爲數很少的坑。這個警告的源頭深深嵌在庫的底層中,不該被忽視。Jeff Reback 本身的話 ,「我沒有找到任何你應該忽略這個警告的狀況。若是你作某些類型的索引時不起做用,而其餘狀況下起做用,你是在玩火。」

幸運的是,解決警告只須要識別鏈式賦值並修復。若是整篇文章你只瞭解到了一件事,那麼就應該是這一點。

做者:笨熊不緊張 連接:https://www.jianshu.com/p/72274ccb647a 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
相關文章
相關標籤/搜索