DataFrame読込時のメモリを節約 - pandas
Pythonの統計ライブラリpandasでは、データフレームを読み込む際、一度にメモリ上に展開するので、巨大なデータ&非力なPCではメモリが圧迫される。
また、ある程度は型推論してくれるが、多少メモリ効率の悪い部分がある。
もし読み込みたいカラムが限定されていたり、データ型が判明しているカラムがあれば、読み込み時に指定することで、メモリを削減できる。
read_csv()の引数
read_csv の引数の内、メモリ節約に効果のある以下の3つを紹介する。
- 
- usecols
 - dtypes
 - chunksize
 
 
usecols
読み込むカラムを指定し、不要なカラムを読み込まないようにする。引数として受け付けるデータは以下のいずれか。
- カラムのindexのリスト
- 例:
[0, 2, 3, 5] 
 - カラム名のリスト(CSVのヘッダに書かれているもの、またはread_csv()のnamesパラメータで指定したもの)
- 例:
['name', 'age', 'weight'] 
 - カラム名を引数にboolを返す関数
- 例:
lambda col: col.lower() in ['aaa', 'bbb', 'ccc'] 
 
- sample.csv(整形)
 name , age , sex, weight, height ドラえもん, -100, M , 129.3 , 129.3 野比のび太, 10 , M , 33.2 , 140.0 源静香 , 10 , F , , 136.9
usecols = ['name', 'age', 'weight']
df = pd.read_csv('sample.csv', usecols=usecols)
print(df)
# => 指定したカラムのみからなるDataFrameができる
# name,        age,   weight
# ドラえもん,  -100,  129.3
# 野比のび太,  10,    140.0
# 源静香,      10,    nan
※数値は適当
dtypes
データ型を適切に指定する。受け付けるデータは以下のいずれか。
- 単一のデータ型
- 例:
np.int64 - 基本的にいろんな型が混ざってることが多いので、あまり使わない
 
 - 読み込むカラム名→データ型 の辞書
- 例:
{'name': np.object, 'age': np.int8, 'weight': np.float32} 
 
pandasでは、カラムがint型やfloat型と判定された場合、64bit環境ならnp.int64、np.float64が基本的には使われる。 取り得る最大値が大きくないなら、int8、int16、int32などを使うことで、メモリ量を削減できる。
文字列は、np.object型として読み込む。np.objectはpythonオブジェクトへのポインタを格納する、何でもアリな型。 
メモリの節約からは話は逸れるが、「数値に見えるけど文字列として処理したい」カラムをはじめから文字列として読み込むのにも役立つ。
欠損値を含むint型
np.int型では整数しか表現できないため、int型を指定したカラムに欠損値が含まれる場合は読み込みエラーになる。
np.floatなら欠損値を表現できる。以下の選択肢を取ることになる。
- float型として読み込み、そのまま処理する
 - float型として読み込み、欠損値を通常なら取り得ない整数値で代用した上でint型にキャストする
 - 欠損値のあるレコードは除外する
- read_csv()での読み込みと同時には出来ないため、以下の方法がある
- 前処理で、欠損値を除外したcsvを用意する
 - float型として読み込み、欠損値を除外した上でint型にキャストする
 
 
 
# 2. カラムの欠損値を-1で埋め、int型にキャスト df['aaa'] = df['aaa'].fillna(-1).astype(np.int32) # 3. カラムの欠損値を除去し、int型にキャスト df.dropna(subset=['aaa'], inplace=True) df['aaa'] = df['aaa'].astype(np.int32)
chunksize
上記の節約をしても、とにかくレコード数が多すぎてままならないという場合は、chunksizeに整数値を指定すると、その数ずつ読み込んでくれる。
read_csv()の返値は、「chunksize個のレコードから成るDataFrameを逐次読み込むイテレータ」となる。
dfitr = pd.read_csv('sample.csv', chunksize=10000)
df = pd.DataFrame(columns=['A','B','C'])  # 処理結果を順次結合していくための空のDataFrame
for subdf in dfitr:
    # なんか処理
    
    # 必要なカラムのみ残してdfに結合 (A,B,Cカラムのみ残る)
    df = pd.concat([df, subdf], join='inner', ignore_index=True)
実装例
列挙型データのカウント
いくつかの決まった値しか取らないデータがそれぞれ何個存在するかを、chunksizeを用いながら集計する。 
事前に取り得る値が全てわかっていなくても大丈夫。
- enum が対象カラム
 - id が便宜的に集計に利用するカラム(何でもよい)
 df.groupby後、1カラムだけを抜き出した後のcount()は、enumの各値をindexとした Series オブジェクトを返す- そのため、結果を足し込んでいく変数 counter は空の Series で初期化
 
- 加算には
Series.add(fill_value=0)を使う- 一方のみに存在するindexがあると、通常の
S1 + S2の加算では結果が NaN になるところ、この方法だと0で初期化してくれる - ただ、これを使うと
dtype=int型同士の加算でも、結果がfloat型になってしまう(?)ので、最後にint型になおす 
 
counter = pd.Series(dtype=np.float64)
for df in pd.read_csv('sample.csv', chunksize=10000):
    counter = counter.add(df.groupby('enum')['id'].count(), fill_value=0)
counter = counter.astype(np.int64)
print(counter)
# ====
# enum
# cat      2
# dog      3
# rabbit   1
# Name: id, dtype: int64
pandasをさらに効率化するラッパツール
あるいは、以下のpandasのラッパを使うことで、巨大データに対する効率的な処理をやってくれるらしい(詳しくは調べていない)
- Dask
- 操作感はあたかも1つのDataFrameに対するもののまま、chunksizeのようなDataFrame分割を内部で自動で行ってくれる
 - あと並列処理とかやってくれるので高速になる
 
 - blaze
- SQL的なクエリに変換する?
 - 複雑な計算には向いてないっぽいのと、最終更新が2年前とちょっと古め
 
 
また、後発の以下のようなツールもある。
- vaex
- Pandasより後発で、Pandasと似た機能を提供する
 - 遅延評価と並列化により、高速化・大規模データへの対応を実現
- そのためか、一旦hdf形式に書き出さないとパフォーマンスが出ないっぽい?
 
 - 今後がどうなるか
- 多くの人にはPandasで十分などだと、機能的には劣ってないのにユーザが増えないこともある
 
 
 

