note.nkmk.me

pandas.DataFrameにおけるビューとコピー

Date: 2019-09-15 / tags: Python, pandas

NumPy配列ndarrayと同様にpandas.DataFrameにもビュー(view)とコピー(copy)がある。

loc[]iloc[]pandas.DataFrameの一部を選択するなどして新たなpandas.DataFrameを生成する場合、元のオブジェクトとメモリを共有する(元のオブジェクトのメモリの一部または全部を参照する)オブジェクトをビュー、元のオブジェクトと別にメモリを新たに確保するオブジェクトをコピーという。

ビューの場合は共通のメモリを参照するので、一方のオブジェクトの要素の値を変更するともう一方の値も変更される。

ここでは以下の内容について説明する。

  • pandas.DataFrameのビューとコピーの注意点
  • loc[], iloc[]による部分選択
    • すべての列が同じデータ型dtypeの場合
    • 異なるデータ型dtypeの列がある場合
  • numpy.ndarraypandas.DataFrameの間のメモリ共有
    • numpy.ndarrayからpandas.DataFrameを生成
    • pandas.DataFrameからnumpy.ndarrayを生成

NumPy配列ndarrayにおけるビューとコピーについては以下の記事を参照。以降のサンプルコードで使用しているnp.shares_memory()についての説明はこちら。

以下の内容のpandasのバージョン0.25.1。バージョンが異なると挙動が異なる場合があるので注意。

スポンサーリンク

pandas.DataFrameのビューとコピーの注意点

pandas.DataFrameにおけるビューとコピーについてまず知っておくべきことは、少なくともバージョン0.25.1の時点では、あるpandas.DataFrameが他のpandas.DataFrameのビューであるかコピーであるかを確実に判定する手段がないということ。

Outside of simple cases, it’s very hard to predict whether it will return a view or a copy (it depends on the memory layout of the array, about which pandas makes no guarantees)
Indexing and selecting data — pandas 0.25.1 documentation

NumPyの関数であるnp.shares_memory()pandas.DataFrame_is_view属性があるが、以降で例を示すように、どちらも確実な結果を返すものではない。

コピーはpandas.DataFramecopy()メソッドを呼べば確実に生成できるが、必ずビューを生成する方法はない。様々なデータを処理する可能性があるコードでビューを前提にするのは危険。

このあといくつかの例を示すが、すべてのパターンを網羅しているわけではなく、バージョン(例は0.25.1)や環境が異なると結果が異なる可能性もあることを留意されたい。

本記事で伝えたいのは、こういう場合はビュー(またはコピー)が返されますよ、ということではなく、ビューが返されるかコピーが返されるか分からないから気をつけよう、ということである。

なお、pandas.Seriesは一列のみ(一次元)なので、すべての列が同じデータ型dtypepandas.DataFrameと同様。

loc, ilocによる部分選択

loc[]は行名・列名、iloc[]は行番号・列番号でpandas.DataFrameの範囲を選択することができる。スカラー値だけでなくスライスやリストなどで複数行・複数列を指定可能。

すべての列が同じデータ型dtypeの場合と異なるデータ型dtypeの列がある場合についての結果を示す。

いくつかのパターンで範囲を選択して新たなpandas.DataFrameを生成してnp.shares_memory()_is_view属性の結果を示したあと、最後に元のpandas.DataFrameの要素の値を変更し、生成したpandas.DataFrameの値も変更されるか(メモリを共有しているか)を確認する。

繰り返しになるが、以降のサンプルコードと結果はあくまでも一例であり、あらゆる条件で同じようにビューまたはコピーが生成されることを保証するものではない。

すべての列が同じデータ型dtypeの場合

すべての列が同じデータ型dtypeの場合。

import pandas as pd
import numpy as np

df_homo = pd.DataFrame({'a': [0, 1, 2], 'b': [3, 4, 5]})
print(df_homo)
#    a  b
# 0  0  3
# 1  1  4
# 2  2  5

print(df_homo.dtypes)
# a    int64
# b    int64
# dtype: object

スライスで選択。

df_homo_slice = df_homo.iloc[:2]
print(df_homo_slice)
#    a  b
# 0  0  3
# 1  1  4

print(np.shares_memory(df_homo, df_homo_slice))
# True

print(df_homo_slice._is_view)
# True

リストで選択。タプルやnumpy.ndarray, pandas.Seriesなどでも同様に指定可能。

df_homo_list = df_homo.iloc[[0, 1]]
print(df_homo_list)
#    a  b
# 0  0  3
# 1  1  4

print(np.shares_memory(df_homo, df_homo_list))
# False

print(df_homo_list._is_view)
# False

ブーリアンインデックス。bool値のpandas.Seriesによる選択。

print(df_homo['a'] < 2)
# 0     True
# 1     True
# 2    False
# Name: a, dtype: bool

df_homo_bool = df_homo.loc[df_homo['a'] < 2]
print(df_homo_bool)
#    a  b
# 0  0  3
# 1  1  4

print(np.shares_memory(df_homo, df_homo_bool))
# False

print(df_homo_bool._is_view)
# False

スカラー値で選択。この場合はpandas.DataFrameではなくpandas.Seriesとなる。

s_homo_scalar = df_homo.iloc[0]
print(s_homo_scalar)
# a    0
# b    3
# Name: 0, dtype: int64

print(np.shares_memory(df_homo, s_homo_scalar))
# True

print(s_homo_scalar._is_view)
# True

loc[]iloc[]を使わずに[列名]で指定。

s_homo_col = df_homo['a']
print(s_homo_col)
# 0    0
# 1    1
# 2    2
# Name: a, dtype: int64

print(np.shares_memory(df_homo, s_homo_col))
# True

print(s_homo_col._is_view)
# True

リストで複数の列名を指定。

df_homo_col_multi = df_homo[['a', 'b']]
print(df_homo_col_multi)
#    a  b
# 0  0  3
# 1  1  4
# 2  2  5

print(np.shares_memory(df_homo, df_homo_col_multi))
# False

print(df_homo_col_multi._is_view)
# False

元のpandas.DataFrameの要素の値を変更し、生成したpandas.DataFrameの値が変わっているかを確認。

df_homo.iat[0, 0] = 100
print(df_homo)
#      a  b
# 0  100  3
# 1    1  4
# 2    2  5

print(df_homo_slice)
#      a  b
# 0  100  3
# 1    1  4

print(df_homo_list)
#    a  b
# 0  0  3
# 1  1  4

print(df_homo_bool)
#    a  b
# 0  0  3
# 1  1  4

print(s_homo_scalar)
# a    100
# b      3
# Name: 0, dtype: int64

print(s_homo_col)
# 0    100
# 1      1
# 2      2
# Name: a, dtype: int64

print(df_homo_col_multi)
#    a  b
# 0  0  3
# 1  1  4
# 2  2  5

このシンプルな例では、np.shares_memory()および_is_view属性の通りの結果となった。

すべての列が同じデータ型dtypeだとデータが単独のnumpy.ndarrayに格納されるので、numpy.ndarrayの場合と同様にリストによる指定(ファンシーインデックスに相当)の場合はコピーとなり、それ以外の場合はビューとなっているものと思われる。

なお、上の例では[:2]のように行のみを指定しているが、例えば[:2, [0, 1]]のように行・列を別々に指定した場合、行・列いずれかにリストが含まれているとコピーとなる。また、[0]はビューだが[[0]](要素1個のリスト)はコピー。

異なるデータ型dtypeの列がある場合

異なるデータ型dtypeの列がある場合は複雑。以下のStack Overflowの回答では常にコピーが返されるとあるが、例外もある模様。

An indexer that gets on a multiple-dtyped object is always a copy.
python - What rules does Pandas use to generate a view vs a copy? - Stack Overflow

元のpandas.DataFrameは以下の通り。

df_hetero = pd.DataFrame({'a': [0, 1, 2], 'b': ['x', 'y', 'z']})
print(df_hetero)
#    a  b
# 0  0  x
# 1  1  y
# 2  2  z

print(df_hetero.dtypes)
# a     int64
# b    object
# dtype: object

スライスで選択。2通り。

df_hetero_slice = df_hetero.iloc[:2]
print(df_hetero_slice)
#    a  b
# 0  0  x
# 1  1  y

print(np.shares_memory(df_hetero, df_hetero_slice))
# False

print(df_hetero_slice._is_view)
# False

df_hetero_slice2 = df_hetero.iloc[:2, 0:]
print(df_hetero_slice2)
#    a  b
# 0  0  x
# 1  1  y

print(np.shares_memory(df_hetero, df_hetero_slice2))
# False

print(df_hetero_slice2._is_view)
# False

リストで選択。

df_hetero_list = df_hetero.iloc[[0, 1]]
print(df_hetero_list)
#    a  b
# 0  0  x
# 1  1  y

print(np.shares_memory(df_hetero, df_hetero_list))
# False

print(df_hetero_list._is_view)
# False

ブーリアンインデックス。

df_hetero_bool = df_hetero.loc[df_hetero['a'] < 2]
print(df_hetero_bool)
#    a  b
# 0  0  x
# 1  1  y

print(df_hetero_bool._is_view)
# False

print(df_hetero_bool._is_view)
# False

スカラー値で選択。

s_hetero_scalar = df_hetero.iloc[0]
print(s_hetero_scalar)
# a    0
# b    x
# Name: 0, dtype: object

print(np.shares_memory(df_hetero, s_hetero_scalar))
# False

print(s_hetero_scalar._is_view)
# False

loc[]iloc[]を使わずに[列名]で指定。

s_hetero_col = df_hetero['a']
print(s_hetero_col)
# 0    0
# 1    1
# 2    2
# Name: a, dtype: int64

print(np.shares_memory(df_hetero, s_hetero_col))
# False

print(s_hetero_col._is_view)
# True

リストで複数の列名を指定。

df_hetero_col_multi = df_hetero[['a', 'b']]
print(df_hetero_col_multi)
#    a  b
# 0  0  x
# 1  1  y
# 2  2  z

print(np.shares_memory(df_hetero, df_hetero_col_multi))
# False

print(df_hetero_col_multi._is_view)
# False

元のpandas.DataFrameの要素の値を変更し、生成したpandas.DataFrameの値が変わっているかを確認。

df_hetero.iat[0, 0] = 100
print(df_hetero)
#      a  b
# 0  100  x
# 1    1  y
# 2    2  z

print(df_hetero_slice)
#      a  b
# 0  100  x
# 1    1  y

print(df_hetero_slice2)
#    a  b
# 0  0  x
# 1  1  y

print(df_hetero_list)
#    a  b
# 0  0  x
# 1  1  y

print(df_hetero_bool)
#    a  b
# 0  0  x
# 1  1  y

print(s_hetero_scalar)
# a    0
# b    x
# Name: 0, dtype: object

print(s_hetero_col)
# 0    100
# 1      1
# 2      2
# Name: a, dtype: int64

print(df_hetero_col_multi)
#    a  b
# 0  0  x
# 1  1  y
# 2  2  z

スライスは省略するかどうかによって結果が異なり、np.shares_memory()_is_view属性はFalseなのにメモリが共有される(元のpandas.DataFrameの変更が反映される)場合がある。

また、[列名]での指定はnp.shares_memory()_is_view属性の結果が異なっている。_is_view属性が正しい。

ビューになるかコピーになるかをあらゆるパターンで覚えておくのは現実的ではないので、結局はその都度確認することになるだろうが、スライスによる指定は省略のありなしによってビューかコピーかが変わる可能性があることは覚えておくとよいかもしれない。

numpy.ndarrayとpandas.DataFrameの間のメモリ共有

pandas.DataFramenumpy.ndarrayは相互に変換することが可能。pandas.DataFramenumpy.ndarrayとの間でもメモリが共有される可能性がある。

この場合は恐らくnp.shares_memory()の結果を信じてよい。

pandas.DataFrame, numpy.ndarrayいずれもcopy()メソッドでコピーを生成することが可能。

numpy.ndarrayからpandas.DataFrameを生成

numpy.ndarrayからpandas.DataFrameを生成する場合。

a = np.array([[0, 1], [2, 3], [4, 5]])
print(a)
# [[0 1]
#  [2 3]
#  [4 5]]

df = pd.DataFrame(a)
print(df)
#    0  1
# 0  0  1
# 1  2  3
# 2  4  5

np.shares_memory()およびpandas.DataFrame_is_view属性はTrueを返す。

print(np.shares_memory(a, df))
# True

print(df._is_view)
# True

numpy.ndarrayの値を変更するとpandas.DataFrameに反映され、実際にビューであることが確認できる。

a[0, 0] = 100
print(a)
# [[100   1]
#  [  2   3]
#  [  4   5]]

print(df)
#      0  1
# 0  100  1
# 1    2  3
# 2    4  5

常にビューであるわけではなく、文字列の場合はコピーとなった。

a_str = np.array([['a', 'x'], ['b', 'y'], ['c', 'z']])
print(a_str)
# [['a' 'x']
#  ['b' 'y']
#  ['c' 'z']]

df_str = pd.DataFrame(a_str)
print(df_str)
#    0  1
# 0  a  x
# 1  b  y
# 2  c  z

print(np.shares_memory(a_str, df_str))
# False

print(df_str._is_view)
# False

a_str[0, 0] = 'n'
print(a_str)
# [['n' 'x']
#  ['b' 'y']
#  ['c' 'z']]

print(df_str)
#    0  1
# 0  a  x
# 1  b  y
# 2  c  z

pandas.DataFrameからnumpy.ndarrayを生成

pandas.DataFrameからnumpy.ndarrayを生成する場合。

pandas.DataFrameの各列のデータ型dtypeが同種の場合はビュー。

df_homo = pd.DataFrame({'a': [0, 1, 2], 'b': [3, 4, 5]})
print(df_homo)
#    a  b
# 0  0  3
# 1  1  4
# 2  2  5

print(df_homo.dtypes)
# a    int64
# b    int64
# dtype: object

a_homo = df_homo.values
print(a_homo)
# [[0 3]
#  [1 4]
#  [2 5]]

print(np.shares_memory(a_homo, df_homo))
# True

df_homo.iat[0, 0] = 100
print(df_homo)
#      a  b
# 0  100  3
# 1    1  4
# 2    2  5

print(a_homo)
# [[100   3]
#  [  1   4]
#  [  2   5]]

異種の場合はコピー。

df_hetero = pd.DataFrame({'a': [0, 1, 2], 'b': ['x', 'y', 'z']})
print(df_hetero)
#    a  b
# 0  0  x
# 1  1  y
# 2  2  z

print(df_hetero.dtypes)
# a     int64
# b    object
# dtype: object

a_hetero = df_hetero.values
print(a_hetero)
# [[0 'x']
#  [1 'y']
#  [2 'z']]

print(np.shares_memory(a_hetero, df_hetero))
# False

df_hetero.iat[0, 0] = 100
print(df_hetero)
#      a  b
# 0  100  x
# 1    1  y
# 2    2  z

print(a_hetero)
# [[0 'x']
#  [1 'y']
#  [2 'z']]
スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事