ノートブックの最適化はfor文の最適化に通ず

by
カテゴリ:
タグ:

データ分析は大きく

の4つの要素で成り立つと思う。できればこの順に1サイクルして終わりたいが、現実的には何サイクルも回す。そしてメンテナンス不能で読む気も失せる巨大ノートブックができあがることは、想像に難くない。

サイクルはつまりfor文だ。 RでもPythonでもノートブックでもfor文を最適化するコツは共通している。

ループせずに済む処理はforの外に出せ

巨大なfor文を読みたくないのはノートブックでも同じ。ループごとの差分がどこか確認することに疲れてしまって肝心な分析結果やグラフを読む時間が失なわれては悲しい。私は今のところ以下のように構造化している。

  1. ループ前
    1. 要旨
    2. パッケージ読み込み
    3. 関数定義
    4. データ読み込み・整形
  2. ループ内
    1. 小規模なデータ整形
    2. 分析・可視化
    3. 解釈
  3. ループ後
    1. まとめ

ループ前

要旨

ノートブックの目的や、どんなことが見えたかを簡単に書く。後からノートを見返す時のためのものだから、タイミングとしては最初に書く必要はないが、結果的にはノートブックの最初に書く。

パッケージ読み込み

モジュールが必要になったタイミングで読み込んでいるなら、すぐに先頭にまとめよう。同じモジュール読んだか定かじゃないから読み込み直す、なんてことしていたらどんどん行数が増える。

関数定義

関数もできるだけ最初に定義しておこう。せっかく作った関数はループ内の任意のタイミングで使いたいものだ。ループごとに少しずつ関数を変えたい? だったら関数を返す関数を書こう (高階関数)。

勿論、最初から完璧な関数を書く必要はない。ループ内などで同じような処理を3回したなと思ったら、関数に纏めてノートブックを整理しよう。

また、ループ内外で複雑な処理を行う場合は、1度きりのコードであっても関数として名前をつけておこう。すると、関数の名前でどんな処理をしているかが読者に伝わる。多くの場合、読者はまずどんな処理をしているか掴みたいだけで、細かいところまで求めていない。

関数定義自体もシンプルにしたいが、これは訓練が必要だと思う。折に触れてコードゴルフしつつベンチマークをして、可読性と高速化のバランスを養うことを勧める。

同じデータに対して引数を変えながら同じ関数を適用したい場合は、次のデータ読み込み・整形を先にしても良いかも知れない。しかし、データ整形時のための関数定義が発生するので、関数定義 → データ読み込み・整形 → 関数定義という流れになり、ノートブックの見通しが悪くなる。

データ読み込み・整形

ループ内でのデータ整形が最小限になるように、ここでできるだけデータを整備しておこう。 for文で変数を作る時、for文の前にリストを初期化するのと同じだ。

ループ内で同じ操作があったらループ前に出す

同じ操作を繰り返すのはハイコストですね。例えばこんな感じで修正しましょう。

Before
# ループ前
import pandas as pd
df = pd.read_csv("hoge.csv")

# ループ内

df.assign(A_centered = lambda df: df['A'] - df['A'].mean()) \
    .pipe(lambda x: f(x, 'foo'))
## 解釈

df.assign(A_centered = lambda df: df['A'] - df['A'].mean()) \
    .pipe(lambda x: f(x, 'bar'))
## 解釈
After
# ループ前
import pandas as pd
df = pd.read_csv("hoge.csv") \
    .assign(A_centered = lambda df: df['A'] - df['A'].mean())

# ループ内

f(df, 'foo')
## 解釈

f(df, 'bar')
## 解釈

ループ前に処理を纏めると、例えばassignを繰り返しているから一つに纏めようなどと、更なる単純化が可能になります。

変数の再代入を避ける

変数の再代入は、

といった問題を孕む。ノートブックをシンプルにするためには再代入を忌避しよう。

例えば

  1. A列から平均値を引いたA_centered列を作り
  2. A_centered列が正の値をとる部分を抽出した

データフレームを作りたいとする。

変数の再代入を許すなら以下の通り。

df = pd.read_csv("hoge.csv")
df['A_centered'] = df['A'] - df['A'].mean()
df = df[df['A_centered'] > 0]

再代入を撲滅するならパイプする (参考: dplyr のアレを Pandas でやる @U25CE)。

df1 = pd.read_csv("hoge.csv") \
    .assign(A_centered = lambda df: df['A'] - df['A'].mean()) \
    .filter()

特にループ内で再代入したくなったら、ループ前に戻って処理を書き直そう。後続するループでどんな処理をしたか忘れてしまった時、ノートブックを上から全て読むよりは、冒頭だけを読んで済む方が低コストだ。

ループ内

小規模なデータ整形

ループ内ではデータ整形を最小限に行います。以下のルールを心掛けましょう。

  • 整形した結果を変数に代入しない
  • 変数を作成する場合
    • 変数の使用は次のループに持ち越さない
    • 再代入しない

分析・可視化

ループ前にちゃんと関数を定義しておけば、ここは自然とシンプルになっているはずです。

解釈

シンプルさを心掛けつつも好きなだけ解釈を書き連ねましょう。

ループ後

分析結果のまとめを書きます。要旨よりも詳しく書きます。