PandasのDataFrameを扱う際に、実際にどの程度メモリを消費しているかを確認したかった。疎行列についても。
メモリ使用量は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にはメモリ使用量を見に行かない。引数指定で挙動を変えることができるけど、詳細については後述。
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)
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自体のメモリ使用量が加わった数字になっている。