Pandasのデータフレームの値のメモリアドレスの調査方法

Pandasのデータフレームは便利だけどメモリ管理よく分からないよね、実際どこにどう配置されているんだっけ、という雑談があって気になったので調べてみました。

# 調査方法

```python
import pandas as pd
df = pd.DataFrame({'A': [1, 2], 'B': [3.0, 4.0], 'C': [5, 6]})
for block in df._data.blocks:
    memory_address = block.values.__array_interface__['data'][0]
    memory_hex = block.values.data.hex()
    print(f"({id(block)}) {block}")
    print(f"<{memory_address}> {memory_hex}")
    print()
```

```
(4886642416) FloatBlock: slice(1, 2, 1), 1 x 2, dtype: float64
<140474854679968> 00000000000008400000000000001040

(4886642608) IntBlock: slice(0, 4, 2), 2 x 2, dtype: int64
<140474585659872> 0100000000000000020000000000000005000000000000000600000000000000
```

山カッコ内の数値がメモリアドレス、その後の数値がメモリ値の16進数表現です。
A列とC列はどちらもInt値なのでまとめてメモリ配置されていることが分かりますね。なるほどなー？

# データ構造

データフレームはBlockMangerというクラスを通して、データをブロック管理しています。このあたりの思想はPandas作者の記事『[A Roadmap for Rich Scientific Data Structures in Python](https://wesmckinney.com/blog/a-roadmap-for-rich-scientific-data-structures-in-python/)』が分かりやすいです。

上記のコードで表れた変数の型を追っていくと次のようになっていて、

- **df**: pandas.core.frame.**DataFrame**
    - df.**_data**: pandas.core.internals.managers.**BlockManager**
        - df._data.**blocks**: **tuple**
- **block**: pandas.core.internals.blocks.**Block** の子クラス（FloatBlockなど）
    - block.**values**: numpy.**ndarray**
        - block.values.**\_\_array_interface\_\_**: **dict**
        - block.values.**data**: **memoryview**

ブロックはNumPyのndarrayを保持していることが分かります。
ということで、ここからはNumPyの世界になっていて『[2.2. Advanced NumPy — Scipy lecture notes](https://scipy-lectures.org/advanced/advanced_numpy/index.html#block-of-memory)』の通り `ndarray.__array_interface__['data'][0]` でメモリアドレスを取得できます。そして `ndarray.data` でmemoryviewが取得できるので、メモリ値も覗くことができます。

なお注意点としてmemoryviewをprintすると `<memory at 0x11b6a3ad0>` というふうに表示されるのですが、これはmemoryviewのインスタンスのアドレスであって、値のアドレスとは異なります。詳しくは『[Numpy , Python3.6 - not able to understand why address is different? - Stack Overflow](https://stackoverflow.com/questions/52032545/numpy-python3-6-not-able-to-understand-why-address-is-different)』を参照してください。

# 実験

いくつか簡単なデータフレーム操作を行って、メモリ配置がどう変わるか実験してみます。

```python
df1 = df[0:1]
```

```
(4886726416) FloatBlock: slice(1, 2, 1), 1 x 1, dtype: float64
<140474854679968> 0000000000000840

(4886727088) IntBlock: slice(0, 4, 2), 2 x 1, dtype: int64
<140474585659872> 01000000000000000500000000000000
```

まずは先頭行のスライスです。メモリアドレスは変わらず、参照範囲が短くなっていることが分かりますね。
なおブロックのインスタンスは変わっています。

```python
df2 = df[1:2]
```

```
(4886798416) FloatBlock: slice(1, 2, 1), 1 x 1, dtype: float64
<140474854679976> 0000000000001040

(4886798896) IntBlock: slice(0, 4, 2), 2 x 1, dtype: int64
<140474585659880> 02000000000000000600000000000000
```

２行目のスライスです。メモリアドレスは全て+8されているので、ポインタがズレているだけで同じメモリブロックを参照していることが分かります。

```python
df['D'] = [True, False]
```

```
(4886642416) FloatBlock: slice(1, 2, 1), 1 x 2, dtype: float64
<140474854679968> 00000000000008400000000000001040

(4886642608) IntBlock: slice(0, 4, 2), 2 x 2, dtype: int64
<140474585659872> 0100000000000000020000000000000005000000000000000600000000000000

(4886800144) BoolBlock: slice(3, 4, 1), 1 x 2, dtype: bool
<140474855093504> 0100
```

列の追加です。既存の列は、メモリアドレスだけでなくブロックも変化ありませんね。

```python
df3 = df.append(df)
```

```
(4886726224) IntBlock: slice(0, 1, 1), 1 x 4, dtype: int64
<140474855531008> 0100000000000000020000000000000001000000000000000200000000000000

(4509301648) FloatBlock: slice(1, 2, 1), 1 x 4, dtype: float64
<140474585317312> 0000000000000840000000000000104000000000000008400000000000001040

(4509301840) IntBlock: slice(2, 3, 1), 1 x 4, dtype: int64
<140474585630688> 0500000000000000060000000000000005000000000000000600000000000000

(4509301552) BoolBlock: slice(3, 4, 1), 1 x 4, dtype: bool
<140474855008224> 01000100
```

行を結合してみました。メモリ配置がガラッと変わってますね。あとIntBlockが二つできてしまっています。
これはフラグメンテーションを引き起こすので、適当なタイミングでまとめてほしいところです。

```python
df4 = df3._consolidate()
```

```
(4509301552) BoolBlock: slice(3, 4, 1), 1 x 4, dtype: bool
<140474855008224> 01000100

(4509301648) FloatBlock: slice(1, 2, 1), 1 x 4, dtype: float64
<140474585317312> 0000000000000840000000000000104000000000000008400000000000001040

(4886728240) IntBlock: slice(0, 4, 2), 2 x 4, dtype: int64
<140475125920528> 01000000000000000200000000000000010000000000000002000000000000000500000000000000060000000000000005000000000000000600000000000000
```

プライベートメソッド `_consolidate()` を呼びだすと、Int値がまとめられて新しいメモリアドレスに配置されました。