pandas.DataFrameにおけるビューとコピー
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
属性も確実な結果を返すものではない。
コピーはDataFrame
のcopy()
メソッドを呼べば確実に生成できるが、必ずビューを生成する方法はない。様々なデータを処理する可能性があるコードでビューを前提にするのは危険。
このあといくつかの例を示すが、本記事で伝えたいのは「こういう場合はビュー(またはコピー)が返されますよ」ということではなく、「ビューが返されるかコピーが返されるか分からないから気をつけよう」ということである。
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の間のメモリ共有
DataFrame
とndarray
は相互に変換できる。DataFrame
とndarray
との間でもメモリが共有される可能性がある。
この場合は恐らく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']]