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

Modified: | Tags: Python, pandas

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

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

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

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

本記事のサンプルコードのpandasとNumPyのバージョンは以下の通り。バージョンによって仕様が異なる可能性があるので注意。

import pandas as pd
import numpy as np

print(pd.__version__)
# 2.1.4

print(np.__version__)
# 1.26.2

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

DataFrameにおけるビューとコピーについてまず知っておくべきことは、少なくともバージョン2.1.4の時点では、あるDataFrameが他の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 2.1.4 documentation

以降で例を示すように、np.shares_memory()DataFrame_is_view属性も確実な結果を返すものではない。

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

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

loc, ilocによる部分選択

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

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

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

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

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

すべての列が同じデータ型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

スライスで選択。

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

リストで選択。タプルやndarray, 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

ブーリアンインデックス。タプルやndarray, Seriesなどでも同様に指定可能。

df_homo_bool = df_homo.loc[[True, False, True]]
print(df_homo_bool)
#    A  B
# 0  0  3
# 2  2  5

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

print(df_homo_bool._is_view)
# False

スカラー値で選択。この場合はDataFrameではなく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_list = df_homo[['A', 'B']]
print(df_homo_col_list)
#    A  B
# 0  0  3
# 1  1  4
# 2  2  5

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

print(df_homo_col_list._is_view)
# False

元のDataFrameの要素の値を変更し、生成した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
# 2  2  5

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_list)
#    A  B
# 0  0  3
# 1  1  4
# 2  2  5

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

リストおよびブーリアンインデックスでの指定ではコピーが生成され、他の場合はビュー。

なお、上の例では[: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

以下の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_row = df_hetero.iloc[:2]
print(df_hetero_slice_row)
#    A  B
# 0  0  x
# 1  1  y

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

print(df_hetero_slice_row._is_view)
# False

df_hetero_slice_row_col = df_hetero.iloc[:2, 0:]
print(df_hetero_slice_row_col)
#    A  B
# 0  0  x
# 1  1  y

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

print(df_hetero_slice_row_col._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[[True, False, True]]
print(df_hetero_bool)
#    A  B
# 0  0  x
# 2  2  z

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_list = df_hetero[['A', 'B']]
print(df_hetero_col_list)
#    A  B
# 0  0  x
# 1  1  y
# 2  2  z

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

print(df_hetero_col_list._is_view)
# False

元のDataFrameの要素の値を変更し、生成した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_row)
#      A  B
# 0  100  x
# 1    1  y

print(df_hetero_slice_row_col)
#    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
# 2  2  z

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_list)
#    A  B
# 0  0  x
# 1  1  y
# 2  2  z

行のみをスライスで選択した場合はnp.shares_memory()_is_view属性はFalseなのにメモリが共有される(元のDataFrameの変更が反映される)。

また、[列名]での指定はnp.shares_memory()False_is_view属性はTrueだが、実際は元のDataFrameの変更が反映されており_is_view属性が正しい。

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

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

DataFramendarrayは相互に変換できる。DataFramendarrayとの間でもメモリが共有される可能性がある。

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

DataFrame, ndarrayいずれもcopy()メソッドでコピーを生成できる。

numpy.ndarrayからpandas.DataFrameを生成

ndarrayから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  2
# 0  0  1  2
# 1  3  4  5

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

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

print(df._is_view)
# True

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

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

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

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

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

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

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

print(df_str._is_view)
# False

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

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

pandas.DataFrameからnumpy.ndarrayを生成

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

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

df_homo = pd.DataFrame([[0, 1, 2], [3, 4, 5]])
print(df_homo)
#    0  1  2
# 0  0  1  2
# 1  3  4  5

print(df_homo.dtypes)
# 0    int64
# 1    int64
# 2    int64
# dtype: object

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

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

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

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

異種の場合はコピー。

df_hetero = pd.DataFrame([[0, 'x'], [1, 'y']])
print(df_hetero)
#    0  1
# 0  0  x
# 1  1  y

print(df_hetero.dtypes)
# 0     int64
# 1    object
# dtype: object

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

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

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

print(a_hetero)
# [[0 'x']
#  [1 'y']]

関連カテゴリー

関連記事