最近少し覚えたことをまとめます。
(基本的にpythonのコードと共に述べていきますが、Rの場合についても少し触れていきます。)
不要なオブジェクトの削除
「使わなくなったオブジェクトを削除してメモリを開放する」という考えです。
del
例えば以下のコードを試してみます。
import gc import psutil import numpy as np import sys nrows = 500000 ncols = 100 print('データを生成します') X = np.random.rand(nrows, ncols) print(f'データのサイズ: {sys.getsizeof(X) / 1024**2: .1f} MB') mem_before = psutil.virtual_memory().used print(f'del前のメモリ使用量: {mem_before / 1024**3 :.1f} GB') del X mem_after = psutil.virtual_memory().used mem_diff = mem_before - mem_after print(f'del後のメモリ使用量: {mem_after / 1024**3 :.1f} GB ({mem_diff / 1024**2 :.1f} MB reduced)')
上のコードを実行すると、以下のような結果になります。
データを生成します データのサイズ: 381.5 MB del前のメモリ使用量: 11.5 GB del後のメモリ使用量: 11.2 GB (362.4 MB reduced)
手動でのガベージコレクションは不要
delのあとに明示的にガベージコレクション(gc.collect()
)を使う方法も考えられますが、基本的に自動でのガベージコレクションが働くのでやらなくて問題ないみたいです。
また、ガベージコレクションを行ったとしても、メモリーがOSに返されるとは限らないようです1。
Rの場合
一方でRはオブジェクトを削除しただけではメモリ使用量が変わらないため、手動でのガベージコレクションが有効なようです2。
rm(X) gc(reset = TRUE)
データ型の最適化
numpyにはuint8
やfloat32
やint64
などの細かいデータ型が用意されています。
例えば以下のようにnp.iinfo()
やnp.finfo()
のメソッドを使うと、そのデータ型でもてる最小値・最大値などの情報を見ることができます。
import numpy as np print(np.iinfo(np.int8)) print(np.iinfo(np.int16)) print(np.iinfo(np.int32)) print(np.iinfo(np.int64)) print(np.finfo(np.float16)) print(np.finfo(np.float32)) print(np.finfo(np.float64))
Machine parameters for int8 --------------------------------------------------------------- min = -128 max = 127 --------------------------------------------------------------- Machine parameters for int16 --------------------------------------------------------------- min = -32768 max = 32767 --------------------------------------------------------------- (以下略)
8bitのint8
は-128~127の範囲の値しか表現できませんが、例えば二値変数(0, 1)や月(1~12)や日(1~31)のデータなどであればこのくらいの表現力で十分ですよね。なので最適化しましょう、というのがこのアプローチの考え方です。
自動で型変換するコード
こんな感じのやつを使っています。
import pandas as pd import numpy as np def reduce_mem_usage(df: pd.DataFrame) -> pd.DataFrame: """データフレームのすべてのカラムのデータ型を最適化してメモリ使用量を抑えるやつ""" start_mem = df.memory_usage().sum() / 1024**2 print(f'Memory usage of dataframe is {start_mem:.2f} MB') df = df.apply(_optimize_dtype, axis=0) end_mem = df.memory_usage().sum() / 1024**2 print(f'Memory usage after optimization is: {end_mem:.2f} MB') print(f'Decreased by {100 * (start_mem - end_mem) / start_mem:.1f}%') return df def _optimize_dtype(col): """あるカラム/変数のデータ型を最適化するやつ""" col_type = col.dtype if col_type != 'object' and col_type != 'datetime64[ns]': c_min = col.min() c_max = col.max() # intにできるなら変換するが、NAがあればfloatのままにする(intはNAを保持できない) is_int = False if _is_int(col): if col.isnull().sum() == 0: is_int = True else: print(f'{col.name} was not convert to int because it has NA') # bit数の最適化 if is_int: if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max: col = col.astype(np.int8) elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max: col = col.astype(np.int16) elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max: col = col.astype(np.int32) elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max: col = col.astype(np.int64) else: if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max: # feather-formatがfloat16を受け入れないためfloat32にする col = col.astype(np.float32) elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max: col = col.astype(np.float32) else: col = col.astype(np.float64) return col def _is_int(col): """あるカラム/変数が整数かどうかを判定する""" fractional, _ = np.modf(col) if np.sum(fractional) == 0: # 小数部がすべて0 return True else: return False
使用例は次のような感じです。
import numpy as np import pandas as pd import sys # データを生成 nrows = 500_000 ncols = 100 X = [np.random.randint(low=0, high=50, size=nrows) for x in range(ncols)] X = pd.DataFrame(np.array(X).T) print('データを生成 ---------------------------') print('dtypes: ') print(X.dtypes.value_counts().to_frame(name='rows')) print(f'データ型変換前のオブジェクトのサイズ: {sys.getsizeof(X) / 1024**2 : .1f} MB') print('\nデータ型を変換 -------------------------') X = reduce_mem_usage(X) print('dtypes: ') print(X.dtypes.value_counts().to_frame(name='rows')) print(f'データ型変換後のオブジェクトのサイズ: {sys.getsizeof(X) / 1024**2 : .1f} MB')
データを生成 --------------------------- dtypes: rows int64 100 データ型変換前のオブジェクトのサイズ: 381.5 MB データ型を変換 ------------------------- Memory usage of dataframe is 381.47 MB Memory usage after optimization is: 47.68 MB Decreased by 87.5% dtypes: rows int8 100 データ型変換後のオブジェクトのサイズ: 47.7 MB
このコードはkaggleのkernelを元にえじさんが改造したものを自分用に改造したものです。
えじさんのものからの変更点は
- loggerに関する部分をprintに置き換え(私がまだloggerに慣れていないため)
- floatであっても小数点以下が全部0でNAの無いカラムはintに変換する
- forループでなくapply()を使う(applyの方が何倍か速い3)
です。
疎行列クラスを使う
ダミー変数(One Hot Encodingした列)が多いデータの場合、非ゼロ要素の情報しか保持しない疎行列用のクラスを使うことでメモリ使用量を大幅に削減できます。
pythonの場合はscipy.sparseを、Rの場合は{Matrix}パッケージを使うことで効率よく疎行列を保持できます。
import pandas as pd import numpy as np import sys import scipy.sparse nrows = 500000 ncols = 500 categories = pd.Series(np.random.randint(low=0, high=ncols, size=nrows)) X = pd.get_dummies(categories) print(f'dtypes: {X.dtypes.value_counts().to_dict()}') print(f'データのサイズ: {sys.getsizeof(X) / 1024**2: .1f} MB') X_csr = scipy.sparse.csr_matrix(X, shape=X.shape) print(f'csr_matrixのサイズ: {sys.getsizeof(X_csr): .1f} Byte')
dtypes: {dtype('uint8'): 500} データのサイズ: 238.4 MB csr_matrixのサイズ: 56.0 Byte
-
使用例で使った50万行x100列のデータの変換を10回行って実行時間の平均をとると、applyを使う場合1.62±0.01秒、forループの場合19.668±0.292秒でした。↩