PandasのDataFrameのメモリ使用量を見る

概要

PandasのDataFrameを扱う際に、実際にどの程度メモリを消費しているかを確認したかった。疎行列についても。

@CretedDate 2016/02/04
@Versions python3.4, pandas0.17.1

pandas.DataFrame.info()

メモリ使用量はpandas.DataFrame.info() で見れるらしい。

# 200個のnp.intが入ったDataFrameの作成
df = pd.DataFrame( np.zeros(200).reshape(100, 2), columns=['foo', 'bar'] )
df.info()
  #=> <class 'pandas.core.frame.DataFrame'>
  #=> Int64Index: 100 entries, 0 to 99
  #=> Data columns (total 2 columns):
  #=> foo    100 non-null float64
  #=> bar    100 non-null float64
  #=> dtypes: float64(2)
  #=> memory usage: 2.3 KB

最後の行にmemory usage: 2.3KBと表示されている。データ型はfloat64(8byte)なのでデータ容量的には1.6KBだけど、pandas的には参照とか他にも保持するものがあるので2.3KBになるらしい。

このサイズはレコード数に対して概ねそのままスケールする。下記は1万レコードでの例。

# 10000個のnp.intが入ったDataFrameの作成
df = pd.DataFrame( np.zeros(20000).reshape(10000, 2), columns=['foo', 'bar'] )
df.info()
  #=> <class 'pandas.core.frame.DataFrame'>
  #=> Int64Index: 10000 entries, 0 to 9999
  #=> Data columns (total 2 columns):
  #=> foo    10000 non-null float64
  #=> bar    10000 non-null float64
  #=> dtypes: float64(2)
  #=> memory usage: 234.4 KB

100レコードで2.3KBだったのが、1万行で234.4KB。概ねレコード数に比例している。

では疎行列の場合はどうかということで、to_sparseしてからinfoを出してみる。

df = pd.DataFrame( np.zeros(200000).reshape(100000, 2), columns=['foo', 'bar'] ).to_sparse()
df.info()
  #=> <class 'pandas.sparse.frame.SparseDataFrame'>
  #=> Int64Index: 10000 entries, 0 to 9999
  #=> Data columns (total 2 columns):
  #=> foo    10000 non-null float64
  #=> bar    10000 non-null float64
  #=> dtypes: float64(2)
  #=> memory usage: 234.4 KB

あれ、サイズが変わらない。

そういえばto_sparseのfill_valueがデフォルトだとNaNなので、zeroを圧縮してくれるわけではなかった。

ということで、fill_value=0を指定して再実行。

df = pd.DataFrame( np.zeros(20000).reshape(10000, 2), columns=['foo', 'bar'] ).to_sparse(fill_value=0)
df.info()
  #=> <class 'pandas.sparse.frame.SparseDataFrame'>
  #=> Int64Index: 10000 entries, 0 to 9999
  #=> Data columns (total 2 columns):
  #=> foo    10000 non-null float64
  #=> bar    10000 non-null float64
  #=> dtypes: float64(2)
  #=> memory usage: 78.1 KB

234.4KB → 78.1KBに減った。めでたい。

上記のままだと文字列などが入っていた際に、deepにはメモリ使用量を見に行かない。引数指定で挙動を変えることができるけど、詳細については後述。

pandas.DataFrame.memory_usage()

memory_usageでもメモリ消費量は見れる。こちらは簡易版で、引数なしで実行した場合はnumpy.ndarray.nbytesを返している。

pandasの当該コードでは下記のように、pandas.DataFrame.values.nbytesの値を返している。

self.values.nbytes

実際に使ってみる。

df = pd.DataFrame( np.zeros(20000).reshape(10000, 2), columns=['foo', 'bar'] )
df.memory_usage()
  #=> foo    80000
  #=> bar    80000
  #=> dtype: int64

# 上記の結果はカラムに対してvalues.nbytesした値と同じ
df.foo.values.nbytes
  #=> 80000
df.bar.values.nbytes
  #=> 80000

memory_usageでは引数にdeepを指定することで、文字列などが入ったカラムでもデータ容量を測ることができるようになっている。

試しにランダムな文字列が入ったカラムのサイズを確認してみる。

# ランダム文字列入りDataFrameの作成
df = pd.DataFrame( np.zeros(20000).reshape(10000, 2), columns=['foo', 'bar'] )
df['baz'] = df.foo.apply(lambda x: 'random string {0:05d}'.format(np.random.randint(0, 100000)))

# bazに適当な文字列が入っている
df.head()
  #=>    foo  bar                  baz
  #=> 0     0    0  random string 38302
  #=> 1     0    0  random string 96231
  #=> 2     0    0  random string 40543
  #=> 3     0    0  random string 35821
  #=> 4     0    0  random string 94725

# 引数を指定せずにmemory_usageすると、文字列の入ったカラムbazも同一の容量で表示される
df.memory_usage()
  #=> foo    80000
  #=> bar    80000
  #=> baz    80000
  #=> dtype: int64

# deep=Trueを指定すると、bazのメモリ使用量が多くなる
df.memory_usage(deep=True)
  #=> foo     80000
  #=> bar     80000
  #=> baz    760000
  #=> dtype: int64

このようにdeep=Trueを指定しておかないと、文字列は参照の分だけしかサイズを計測されない。

deep=Trueを指定した場合は、上記の例では文字列1レコードあたり76byteと測定されている。19文字入っているので、1文字あたり4byte程度。ascii文字の割に大きい。

Pythonの文字列のメモリ使用量についてよくわかってないので、19文字のASCII文字が何byte食うものなのか確認。

import sys

# 今回の文字列のサイズ
sys.getsizeof('random string 11794')
  #=> 68

# 空文字のサイズ
sys.getsizeof('')
  #=> 49

# 空文字のbyte数から今回の文字列のbyte数を引くと、文字数と一致する
sys.getsizeof('random string 11794') - sys.getsizeof('')
  #=> 19

文字列の空オブジェクトがまず49byte使うようだ(利用バージョン、Python3.4.3。Python2.7.6では37byteだった)。

利用している文字はASCII文字だけなので1文字につき1byte加算されている計算。

DataFrame上では1レコード76byteが計上されているが、まず文字列が68byteで、それに参照のための8byteで76byteになっているものと思われる。

具体的にはpandas内で下記のように、まずnumpyのnbytesを取り、そこにlib.memory_usage_of_objectsというPandasの関数(Cythonで記述されている)でDeepなObjectのメモリサイズを取っている。

# nbytesを取る
v = self.values.nbytes
if deep and com.is_object_dtype(self):
    # deepがTrueだったら(あと型がObjectだったら)Objectのサイズを測って足す
    v += lib.memory_usage_of_objects(self.values)

info(memory_usage='deep')

memory_usage()の挙動を踏まえて、infoでもdeepに使用量を測ってみる。

df.info(memory_usage='deep')

のように指定すれば良い。

実際に使ってみる。まずはdeepを指定しない場合のサイズ。利用しているDataFrameは前述のものと同じ。

df.memory_usage(deep=True).sum()
  #=> 920000

df.info()
  #=> <class 'pandas.core.frame.DataFrame'>
  #=> Int64Index: 10000 entries, 0 to 9999
  #=> Data columns (total 3 columns):
  #=> foo    10000 non-null float64
  #=> bar    10000 non-null float64
  #=> baz    10000 non-null object
  #=> dtypes: float64(2), object(1)
  #=> memory usage: 312.5+ KB

memory_usageでは約920KB、infoでは312.5KBになっている。最終行のmemory usageの項で312.5+とプラス記号が付いているが、これは「Objectがあったから、312.5KB以上のサイズになってるよ」という親切なお知らせ。

次にDeepにメモリ使用量を見に行く。

df.info(memory_usage='deep')
  #=> <class 'pandas.core.frame.DataFrame'>
  #=> Int64Index: 10000 entries, 0 to 9999
  #=> Data columns (total 3 columns):
  #=> foo    10000 non-null float64
  #=> bar    10000 non-null float64
  #=> baz    10000 non-null object
  #=> dtypes: float64(2), object(1)
  #=> memory usage: 976.6 KB

976.6KB。memory_usage()で出た920KBに加えて、DataFrame自体のメモリ使用量が加わった数字になっている。