すべての列でdtypeが同じであるDataFrameに対して、`to_numpy()`を実行すると、DataFrameとメモリを共有しているnumpy arrayが返る

# 環境
* Python 3.12.4
* pandas 2.2.3
* numpy  1.26.4

# Pandasの Copy-on-Write で分からなかったこと
Copy-on-Writeの「[Read-only NumPy arrays](https://pandas.pydata.org/docs/user_guide/copy_on_write.html#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]

[^1]: https://pandas.pydata.org/docs/user_guide/basics.html#attributes-and-underlying-data 引用


`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]

[^2]: https://pandas.pydata.org/docs/user_guide/copy_on_write.html#migrating-to-copy-on-write 引用

しかし、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`が発生していないか」を確認した方がよいかもしれません。