pandasのSettingWithCopyWarningを理解する (2/3)

この記事は、 SettingwithCopyWarning: How to Fix This Warning in Pandas – Dataquest の日本語訳です。3回にわたって掲載予定で、この記事は2回目です。

1回目の記事はこちら: linus-mk.hatenablog.com

f:id:soratokimitonoaidani:20190314234733p:plain

SettingWithCopyWarningを処理するためのヒントとコツ

以下でもっと詳細な分析をする前に、顕微鏡を取り出して、SettingWithCopyWarningの細かい点と核心的な点をいくつか見てみよう。

警告を消す

最初に、この記事を完全なものにするためには、SettingWithCopyの設定を明示的に制御する方法を説明せねばなるまい。pandas内のmode.chained_assignmentオプションは、以下のいずれかの値を取ることができる。

  • 'raise' - 警告の代わりに例外を上げる。
  • '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の経験の浅い開発者と一緒のプロジェクトで作業をしている場合、またはその完全性において高いレベルの厳格さまたは確実性を必要とするプロジェクトで作業をしている場合、これは特に役に立つ。

この設定を使用するより正確な方法は、コンテキストマネージャを使用することである。

# 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プロパティ

この警告を回避するためのやり方がもう一つある。それはSettingWithCopyシナリオを解釈するためにpandasが使っているツールの1つを変更することである。 それぞれのDataFrameは、is_copyプロパティを持ち、デフォルトではNoneである。コピーである場合は、is_copyプロパティはソースのDataFrameを参照するためにweakrefを使用する。is_copyNoneに設定すると、警告を生成しないようにすることができる。

winners = data.loc[data.bid == data.price]
winners.is_copy = None
winners.loc[304, 'bidder'] = 'therealname'

しかしながら、これは奇跡的に問題を解決するわけではなく、バグの検出が非常に困難になる可能性がある。

single-dtyped 対 multi-dtyped オブジェクト

強調しておく価値のあるさらなる点は、single-dtypedオブジェクトとmulti-dtypedオブジェクトの区別である。DataFrameは、そのすべての列が同じdtypeの場合、single-dtypedである。例えば:

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ではないものがある場合、multi-dtypedである。たとえば:

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

以下の「歴史」のセクション(※訳注:第3回で掲載予定)で説明している理由から、multi-dtypedオブジェクトに対するindexer-get操作は常にコピーを返す。 ただし、主に効率のために、single-dtyped型オブジェクトに対するindexer-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

2019年5月18日 追記:

3回目 linus-mk.hatenablog.com