For Your ISHIO Blog

データ分析や機械学習やスクラムや組織とか、色々つぶやくブログです。

DataFrameのメモリサイズを節約する

新年あけましておめでとうございます。2019年最初のブログになります。本投稿では、DataFrameを扱う際のメモリサイズの節約について書きたいと思います。

私はGCP上のVMPythonの開発環境としており、Kaggleのデータセット等を利用して学習しています。Pandasを利用してDataFrameを扱うわけですが、以下のようなことに遭遇します。

  • 残念ながらお金がないので、メモリを大量に積んだVM環境を常備できない
  • Daskよりも、Pandasの方がやっぱり使い慣れている
  • kaggleのデータセットがでかすぎてメモリーエラーになる

少しでもメモリを節約するための方法をツラツラメモしています。結論としては以下です。

  1. データ読み込み時に型を指定しないと、一番大きなメモリサイズが確保されるので気を付ける
  2. 新しいカラム作ると、一番大きなメモリサイズの型が確保されるので気を付ける
  3. いらなくなったオブジェクトはお掃除

より良いテクニックあればコメントお待ちしています。

目次

利用したデータセット

KaggleのMicrosoft Malware Predictionのデータセットを利用します。8,921,483レコード、カラム数は83です。

Microsoft Malware Prediction | Kaggle

$ import pandas as pd
$ train = pd.read_csv('./data/train.csv')
$ print(train.shape) 
(8921483, 83)

メモリサイズの確認方法

定義した変数のメモリサイズ確認には、sys.getsizeof()を利用します。

$ import sys
$ sys.getsizeof(train)
20945097222

また、DataFrameの各カラムで利用されるメモリサイズの確認には、memory_usage()メソッドを利用します。

同時にカラム名とdtypeを確認すると、デフォルトではobjectint64float64になっており、各カラムで71,371,864バイトのメモリが必要です。これらは全て1レコード当たり8バイト必要となるため、各カラムは8バイト × 8,921,483レコード = 71,371,864となる計算です。

$ for col, dtype, memory in zip(train.columns, train.dtypes, train.memory_usage(index=False)): 
       print(col, dtype, memory) 
                                                                                                          
MachineIdentifier object 71371864
ProductName object 71371864
EngineVersion object 71371864
AppVersion object 71371864
AvSigVersion object 71371864
IsBeta int64 71371864
RtpStateBitfield float64 71371864
IsSxsPassiveMode int64 71371864
DefaultBrowsersIdentifier float64 71371864
AVProductStatesIdentifier float64 71371864
AVProductsInstalled float64 71371864
AVProductsEnabled float64 71371864
...

データ読み込み時にdtypeを指定する

データを読み込み、DataFrameを作成する際に、オプションとしてdtypeを指定することができます。仮に指定しない場合には、上述の通りデフォルトの型(int64、float64、object)が指定されます。

思想としては、どのようなデータでも対応できるように、整数・小数型のうち最も大きなtypeが確保されるようです。このため、実データで必要とされる以上に、メモリを確保してしまうことになります。

各typeのサイズと値の範囲は以下の通りです。dtypeを指定することによるメモリサイズの節約は、本質的には必要なメモリサイズの型を指定して、必要な分だけ利用しましょう、ということです。

type サイズ 値の範囲
int8 1バイト -128 to 127
int16 2バイト -32768 to 32767
int32 4バイト -2147483648 to 2147483647
int64 8バイト -9223372036854775808 to 9223372036854775807

私の場合は、.pyでdtypeを辞書型で定義し、それをread_csv時に読み込んでいます。これにより、もともとのインポートデータは20,945,097,222から2,477,814,730に節約出来ました。8~9分の1に節約できています

$ from lib.dtype import dtypes

$ dtypes
{'AVProductStatesIdentifier': 'float32',
 'AVProductsEnabled': 'float16',
 'AVProductsInstalled': 'float16',
 'AppVersion': 'category',
...
 'UacLuaenable': 'float32',
 'Wdft_IsGamer': 'float16',
 'Wdft_RegionIdentifier': 'float16'}

$ train = pd.read_csv('./data/train.csv', dtype=dtypes)

$ sys.getsizeof(train)
2477814730

新たな特徴量生成時のデータ型確認

object型の変数をモデリングに利用するために、pandasのfactorize()メソッドを利用してカテゴリカル変数に変換することがあります。これにより、0から始まる整数型の値に変換でき、モデル構築時に利用しやすくなります。

データの読み込み時だけであなく、新たな特徴量を生成する場合にもデータ型には注意が必要です。pd.factorize()も、デフォルトではint64型になるようですので、必要以上のメモリを確保しがちになるかと思います。pd.factorize()に限らず、新しい整数型、小数型のカラムは全てint64, float64になるようです

試しに、サンプルデータのEngineVersionを参考に確認してみます。

EngineVersionは70個のユニークな値を持つobjectです。カテゴリカル変数への変換後のカラム「factorized_EV」のデータ型はint64となっており、変換前と同様のメモリサイズが必要となっています。

実際に必要な値のrangeは0~69であるため、int8に変更してあげます。これにより、当該カラムは約8分の1に節約出来ます。

$ train.EngineVersion.head()                                                                                                 
0    1.1.15100.1
1    1.1.14600.4
2    1.1.15100.1
3    1.1.15100.1
4    1.1.15100.1
Name: EngineVersion, dtype: object

$ len(set(train.EngineVersion))                                                                                              
70

$ train.EngineVersion.memory_usage()                                                                                         
71371944

$ train["factorized_EV"], _ = pd.factorize(train.EngineVersion) 
$ train["factorized_EV"].dtypes
dtype('int64')

$ train["factorized_EV"].memory_usage()                                                                                      
71371944

$ set(train["factorized_EV"])                                                                                                
{0,
 1,
 2,
 3,
 4,
...
 67,
 68,
 69}

$ train["factorized_EV"] = train["factorized_EV"].astype('int8')
$ train["factorized_EV"].memory_usage() 
8921563

gc.collect()で不要なデータを回収する

これは、多くの方がすでに実践されているかと思います。不要なデータはGCに回収させ、使い終わったデータは解放してメモリを節約します。

$ import gc

$ del train
$ gc.collect()  
1843

まとめ

会社の環境は贅沢なのであまり気にしていなかったが、自分の開発環境では少し気を遣うことにした。のメモ。