St_Hakky’s blog

Data Science / Human Resources / Web Applicationについて書きます

【Python】Pandasのメモリ使用量の削減方法のまとめ

こんにちは。

今、とある事情でPandasのメモリ使用量の削減を仕事でしているのですが、その時に改めてPandasのメモリ使用量の削減方法を調べたので、まとめてみます。

メモリ使用量の確認

今回、タスクを実施するにあたってメモリ使用量がどのくらいかかっているのかを同時に調べたんですが、Pandasに限らず、メモリ使用量を確認する方法としては、memory_profilerが良きです。以下の記事で紹介していますので、参考にしていただければと思います。

www.st-hakky-blog.com

ただ、このmemory_profilerは関数の中まで見に行ってメモリ使用量を見にいくわけではないので、関数内でピークを向かえるメモリ使用量などがわからない点に注意が必要です。

メモリ使用量の削減概要

Pandasでよくやる操作でメモリ使用量を削減する方法は、だいたい以下あたりじゃないかなと。

  • 使わないデータをできる限り読み込まないようにする
  • del文からのgc.collect
  • データ型の指定
  • その他処理面での工夫

それぞれについて紹介していきます。

使わないデータをできる限り読み込まないようにする

ぶっちゃけ今回の仕事上では色々やったんですが、これが当たり前ですが一番聞きました笑。例えばcsvでデータを読み込む時に、処理に使用するデータのみ取り込むことで、使用メモリを削減することができます。

data = pd.read_csv('./data.csv', usecols=['columnA', 'columnB', 'columnC'])

csvデータを読み込む時のメモリ使用量の削減ついては、以下の記事でも書きましたので、詳細を知りたい方は、以下の記事を。

www.st-hakky-blog.com

Pandasでは、処理の際に内部的にデータをコピーして処理するなどがあるので、カラムが多ければ多いほど、データをより食う構造に陥りやすいのではないかなと思っています。これを回避する意味でも、必要なデータだけ扱うと言うのはとても大事ですね。。。*1

del文からのgc.collect

これはPandasに限らずの定番ですが、使い終わったデータのメモリ領域を解放することが大事です。

import gc

# 何かしらの処理をしたdfがあるとする

# 削除とガーベジコレクションをする
del df
gc.collect()  

Pythonは、C言語などと違って自動でガーベジコレクションをしてくれるので、特に明示的にしなくてもまぁいいっちゃいいのですが、大きめの物をやろうとするとシビアになってくるので、都度やってます。

ただ、都度やると処理速度は落ちてしまうのですが、大きなデータでメモリがクラッシュするよりは時間食わないので、やったほうがいいと思います。

データ型の指定

Pandasにおいては、これが結構効きます。Pandasだと、データをread_csvなどで読み込んだ時に、自動で型が推定されるのですが、その際に必ず大きめの型が指定されます。float型の場合、float32で大丈夫だったとしても、float64にしてしまいます。float32とfloat64だと、単純に考えても2倍、メモリ使用量が違います。

Pandasのデータの型をしっかり指定してあげることで、使用メモリを削減できるので、いくつかのメモリ型の指定方法を書きます。

.astype

処理したり、データを読み込んだりした後、型を十分なサイズまで小さくするために、型を適切に設定するために、 astypeを使用することができます。

df['value1'] = df['value1'].astype('int8')
読み込み時(read_csv, read_json)で、型を指定する

詳しくは以下の記事でも紹介しているのですが、ファイルを読み込んだ時に、型を指定してしまうと、最初から最低限のメモリ使用量にできるので、事前にデータについてある程度わかっている場合は、これは良きです。

www.st-hakky-blog.com

例えば、以下のように設定することができます。

data = pd.read_csv('./data.csv',
            dtype={'id': np.int64,
                   'url': np.object,
                   'region': np.object,
                   'region_url': np.object,
                   'price': np.int64,
                   'year': np.float16,
                   'manufacturer': np.object,
                   'model': np.object,
                   'condition': np.object,
                   'cylinders': np.object,
                   'fuel': np.object,
                   'odometer': np.float32,
                   'title_status': np.object,
                   'transmission': np.object,
                   'vin': np.object,
                   'long': np.float16})

このように設定することで、メモリの使用量を削減することができます。

Category型にする

PandasにはCategory型と言うものがあり、特定の離散値を取る性質のデータに用いることができます。カラムの中の値のユニーク数が少ないなら、かなり大きな効果が期待できます。

data['gender'] = data['gender'].astype('category')

データの中身をみたときに、category型が使える場所については積極的に使っていくことで、メモリ使用量を削減することができます。

型の自動指定

ここまで、型を自分で指定するアプローチを書いてきましたが、Kaggleの方で以下のような便利な関数を見かけたので、こちらに載せておきます。

これは、各列の値をみて、メモリ使用量が最小になるように型を自動で設定してくれる関数です。

def reduce_mem_usage(df, verbose=True):
    numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
    start_mem = df.memory_usage().sum() / 1024**2
    for col in df.columns:
        col_type = df[col].dtypes
        if col_type in numerics:
            c_min = df[col].min()
            c_max = df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float16)
                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)
    end_mem = df.memory_usage().sum() / 1024**2
    if verbose:
        print('Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction)'.format(end_mem, 100 * (start_mem - end_mem) / start_mem))
    return df

この関数にDataFrameを入れてあげることで、メモリ使用量を削減できます。関数が処理を実行する時間のオーバーヘッドはあるものの、かなり効果がでかいです。

その他処理面での工夫

ここからは処理面の工夫でよくやるやつをメモ書き程度に書いておきます。もっと他にもある気がするのですが、思いついたら書いていこうと思います。

mergeの時にcopyオプションをoffにする

Pandasを扱っている方なら、mergeの処理をすると思うのですが、このmergeのオプションに、copyオプションがあり、これをoffにすることで、メモリ使用量を削減することができます。

Pandasのmergeの処理では、内部実装までは追いかけていないのですが、内部の挙動を外からメモリ使用量などでみている感じと、同じ職場の人から聞いた感じだと、mergeするもの・mergeされるもの・merge後のオブジェクトを、Chunkに区切って徐々にマージするっぽいです。

その時に不要なコピーを置いておくみたいなことしているらしく、これをoffにすることで、メモリ使用量を削減することができます。ただ、これによる副作用として、自分はまだであったことがないのですが、たまに変な挙動をすることもあるらしいので注意が必要です。

Numpyにして処理してから再代入

Pandasのデータだと処理がどうしても遅くなってしまう処理などをするときなどは、Numpyにして処理してから再代入するみたいな感じのほうがメモリ使用量もそうですが、処理速度の面でも恩恵を受けることがあるようなイメージがあります。ケースバイケースな感じはありますが。


以上です。

*1:だいたい横着してテーブルでかくなっていくんですが、、、