環境
- Python 3.12.4
- pandas 2.2.3
- numpy 1.26.4
Pandasの Copy-on-Write で分からなかったこと
Copy-on-Writeの「Read-only NumPy arrays」に関して、以下の内容が理解できませんでした。
The array is a copy if the initial DataFrame consists of more than one array:
In [49]: df = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})
In [50]: df.to_numpy()
Out[50]:
array([[1. , 1.5],
[2. , 2.5]])
The array shares data with the DataFrame if the DataFrame consists of only one NumPy array:
In [51]: df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
In [52]: df.to_numpy()
Out[52]:
array([[1, 3],
[2, 4]])
上記の文章を読んで、以下の疑問が発生しました。
- なぜ
In [49]
のDataFrameは複数の配列("more than one array")を含んでいるのか? - なぜ
In [51]
のDataFrameは1個のNumPy配列("only one NumPy array")を含んでいるのか? - DataFrameが含んでいる配列を、どのように確認できるのか?
分かったこと
DataFrameはすべての列でdtypeが同じ場合、1個のnumpy arrrayを保持しているようです。
When your DataFrame only has a single data type for all the columns, DataFrame.to_numpy() will return the underlying data: 1
In [49]
のDataFrameは、各列でdtypeが異なるため(a
列のdtypeはint64
、b
列のdtypeはfloat64
)"more than one array"を含んでいました。
保持しているnumpy arrayの構成は、DataFrame._mgr
プロパティで確認できました。
In [490]: df1 = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})
In [491]: df1.dtypes
Out[491]:
a int64
b float64
dtype: object
In [493]: df1._mgr
Out[493]:
BlockManager
Items: Index(['a', 'b'], dtype='object')
Axis 1: RangeIndex(start=0, stop=2, step=1)
NumpyBlock: slice(0, 1, 1), 1 x 2, dtype: int64
NumpyBlock: slice(1, 2, 1), 1 x 2, dtype: float64
In [494]: df2 = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
In [495]: df2.dtypes
Out[495]:
a int64
b int64
dtype: object
In [496]: df2._mgr
Out[496]:
BlockManager
Items: Index(['a', 'b'], dtype='object')
Axis 1: RangeIndex(start=0, stop=2, step=1)
NumpyBlock: slice(0, 2, 1), 2 x 2, dtype: int64
df1._mgr
ではNumpyBlock
が2個あるのに対して、df2._mgr
ではNumpyBlock
は1個でした。
再度確認する
「Read-only NumPy arrays」に書かれている内容が理解できたところで、再度挙動を確認しました。
複数のnumpy arrayを含むDataFrameの場合
In [499]: pd.options.mode.copy_on_write = True
In [500]: df1 = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})
In [501]: arr1 = df1.to_numpy()
# `df1`と`arr1`はメモリを共有していない(`arr1`は`df1`のコピー)ことの確認
In [502]: np.shares_memory(df1, arr1)
Out[502]: False
# `arr1`は書き換え可能であることの確認
In [513]: arr1.flags.writeable
Out[513]: True
In [507]: arr1[(0,0)]
Out[507]: 1.0
In [508]: arr1[(0,0)] = 100
In [509]: arr1
Out[509]:
array([[100. , 1.5],
[ 2. , 2.5]])
# `arr1`を書き換えても`df1`は変更されない
In [510]: df1
Out[510]:
a b
0 1 1.5
1 2 2.5
arr1
はdf1
のコピーなので、arr1
を書き換えてもdf1
は変更されないことを確認できました。
1個のnumpy arrayを含むDataFrameの場合
In [514]: pd.options.mode.copy_on_write = True
In [515]: df2 = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
In [516]: arr2 = df2.to_numpy()
# `df2`と`arr2`はメモリを共有していることの確認
In [517]: np.shares_memory(df2, arr2)
Out[517]: True
# `arr1`は書き換え不可であることの確認
In [518]: arr2.flags.writeable
Out[518]: False
In [520]: arr2[(0,0)] = 100
---------------------------------------------------------------------------
ValueError: assignment destination is read-only
arr2
は書き込み不可なので、arr2
を書き込もうとするとValueError
が発生することを確認できました。
補足
mode.copy_on_write = "warn"
を設定したときの挙動
pd.options.mode.copy_on_write = "warn"
を設定すると、Copy-on-Writeの振る舞いが変わった操作に警告が発生します。
that will warn for every operation that will change behavior with CoW. 2
しかし、1個のnumpy arrayを含むDataFrameに対してto_numpy()
を実行した場合は、警告は発生しません。
(Copyしていないから?)
In [537]: pd.options.mode.copy_on_write = "warn"
In [538]: df2 = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
In [539]: arr2 = df2.to_numpy()
In [540]: arr2.flags.writeable
Out[540]: True
DataFrame.to_numpy()
を利用しているコードでは、一度pd.options.mode.copy_on_write = True
を設定して、「読み込み専用のnumpy arrayに書き込もうとしてValueError
が発生していないか」を確認した方がよいかもしれません。