新卒の @yaitaimo です。(お手柔らかに)
本項は リブセンスアドベントカレンダー2017 テーマ「自」 の17日目の記事です。
テーマは「自」ということでしたが、省メモリの「省」を眺めていると、「自」が入っているなと思いました。
業務で少し、データを触る機会があったので、そこで得られた知見をまとめてみます。
0. はじめに
分析をしていて、
あーもっとメモリがあったらな
って思ったことはありませんか?
大きなメモリさえあれば、今の(雑な)コードでも結果がわかるはずなのに!
と感じたことは?
最近はクラウド環境でハイスペックな環境を手に入れることが出来るようになったので、札束で殴れば解決するシーンもあるかと思います。
一方で、継続的にやっていくためには、分厚い札束が必要に...
今回は、ちょっと意識するだけで、このメモリ問題を解決することのできる Tips を紹介します!
1. Python と Redshift の役割分担について考える
Python や pandas はとても便利なので、ついついデータを多めにとってきて、Python で処理をしてしまうこがあります。Python を良く利用している人などは、Redshift でやったほうが効率的な仕事も、ついつい Python で済ませようしてしまうきらいがあります。
しかし、一般的なサーバ上での Python と Redshift の処理能力には大きな差があるため、Python を利用しなければならない場面、つまりデータを探索的に分析する場面までは、Redshift で完了してしまうのが良さそうです。
さらに、今回のテーマであるメモリの面で言うと、データはなるべく集計を終わらせて、容量を減らした状態で取得するのが理想的です。SQL に対して少し面倒だなと思っている場合もありかもしれませんが、現状で効率の良い分析を行うためには、SQL を勉強するのが近道かもしれません。
内部結合・外部結合、GROUP BY 句と集計関数、WITH 句、ウインドウ関数などを利用することで、殆どの処理は SQL で行えるようになります。
取り敢えず一通りのやり方が見たい、という場合には、データ分析に効くSQL50本ノック が良かったです。
とはいえ、
- ここは Python のライブラリを使って処理したいんだ!
- SQL だと複雑な書き方になるので、読み物として後から理解しづらくなってしまうよ...
っていう場合もありますよね。
2. データを部分的に取得しつつ、加工やフィルタ処理を行う
こんな時は、Redshift からデータを部分的に取ってきて、加工とフィルタ処理をおこなっていくやり方が良さそうです。
Redshift ではクライアントサイドのメモリ容量が少ない場合に対して、カーソルを利用することで、大きな結果データから一部分ずつ取り出して処理を行う仕組みを提供しています。
Python からこの機能を利用するには、psycopg2
のサーバサイドカーソルの機能を利用するのが良さそうです。
※ サーバサイドカーソルってなんだっていうのは、PythonとDB: DBIのcursorを理解するがわかりやすかったです。
2.1 psycopg2 単体の場合
具体的なコードとしては以下の流れになります
import psycopg2
conn = psycopg2.connect(
host=host,
database=database,
port=port,
user=user,
password=password
);
cur = conn.cursor('curname')
cur.itersize = 10000
cur.execute(query)
for record in cur:
# do something
上では、名前付きのカーソルを生成して cur
に代入し、一度に取得してくる分量を itersize
に設定し、query
を実行しています。全体を取ってくるのはメモリー的に難しいとはいえ、一件ずつ取ってくるのはネットワーク的にも Redshift 的にもあまり良くないので、一度に itersize
に取得してくる分量を設定することができます。上では、その後 cur
から一件ずつ record
に受け取り、処理を行うコードを書いていますが、この時には毎回ネットワークアクセスは起きていない、という状態になります。
2.2 pandas 経由の場合
pandas の read_sql
でもやれないのかなと思って確認すると、chunksize
というオプションがありました。read_sql
の第2引数には connection か engine が利用可能で、上で用意した conn
では機能しない一方で、以下の engine では機能していました。
こちらの場合は、chunksize
に指定した量を一度に取得し、chunk
として使えるようになっています。
import redshift_sqlalchemy
from sqlalchemy import create_engine
engine = create_engine('redshift+psycopg2://{user}:{password}@{url}:{port}/{db}'.format(
user=user,
password=password,
url=url,
port=port,
db=db
))
for chunk in pd.read_sql(query, engine, chunksize=10000):
# do something
普段の SQL の実行には、sql_magic を利用しています。このため、engine をすでに用意してある点と、コード的には pandas の方がすっきりとしている点から、部分取得のコードとしては、主に pandas の方を利用しています。
3. データを pandas の DataFrame に格納して使い回す
1 や 2 を通して、データ量を減らした状態で Python の変数に格納することが出来るようになりました。分析環境のメモリ容量による制限を気にせずに、分析対象をより広く取れるようになったのではないでしょうか。
しかし、変数に対して少し処理を行って別の変数に格納する、ということを繰り返すと、1度しか利用しないのにメモリを確保してしまい、積もり積もって容量を圧迫する変数が多数産まれてしまいます。雑な例ですが、例えば以下の name_list
などがそれに当たります。
name_list = df_master.name
name_count = name_list.value_counts()
このような場合に対して、pandas では、メソッドを繋げて行くことで、中間データの変数を生成することなく、最終結果を得ることの出来る、メソッドチェーンが可能となっています。
以下のようにすると、name_list
を登場させずにすみますね。
name_count = df_master.name.value_counts()
メソッドチェーン使って処理を一筆書きで書くことで、一度取得したマスターデータを使い回すことが可能になります。
3.1 関数化のすすめ
よく使うチェーンは関数化してしまうのがおすすめです(チェーンが長くなりすぎると、読む気力が削られてしまうので)。
例えば、pipe
関数を利用することで、チェーン元の DataFrame を引数として関数適応することが可能です。
def extract_by_name(df, name):
return df[df.name == name]
def count_df(df):
return df.size
df_master.pipe(extract_by_name, name='my_name'))
df_master.pipe(count_df)
他にも様々な関数が用意されていますが、Python/pandasのデータ処理で再帰代入撲滅委員会 では具体的なコードとともに説明があり、とてもわかりやすかったです。自分は上の記事を読んでこの考え方について入門しました。
4. まとめ
いかがでしょうか、数ヶ月前の自分に説明するイメージで本稿を書いてみました。
まぁそうだよねっていう感じでしょうか。
整理すると、
- 道具は適切に使い分けること
- 部分的に処理を行うこと
- エコシステムを最大限利用すること
で、省メモリな分析へ一歩近づくことが出来そうです(入り口に立った感)
改めて整理していて、それぞれの箇所でバランスがありそうだなと感じました。例えば自分は、後から読み返しやすいノートとすることを大事にしたいなと考えていて、その文脈では SQL で済ますべきところを Python で書く場合もあってよいのかなと思ったりしてます。
メモリが足りないと嘆いたときに、やり方が悪いのではないかと優しくコメントいただいた皆様、ありがとうございました。