Higu`s diary

大学院生の日記です。主にプログラミングとカメラについて。最近はデータ分析・Kaggleにハマっています

データ分析でコードをクリーンに保つ技術

こんにちは、ひぐです。
最近データサイエンティストのための良いコーディング習慣という記事を読みました。
www.thoughtworks.com

こうした方がいいよなという自分の経験則が綺麗に言語化されていてよかったです。
ここではデータ分析でコードをクリーンに保つ技術について、記事の内容と自分の取り組みを合わせて紹介したいと思います。

自分はまだチームでの開発経験などが浅いため、間違っている部分もあるかもしれません。
あらかじめご了承ください汗

コードが汚くなる要因

f:id:zerebom:20200610210812p:plain:w300
コードが解くべき問題の複雑さを増長させている時、そのコードは汚いと言えます。
汚いコードは汚い部屋で探し物をする時などと同じく、簡単な作業を困難にしてしまいます。

では、どのような書き方をするとコードが汚くなるのでしょうか。

元記事には下記のような例が記載されています。

  1. 関数やクラスを使って処理を抽象化しない
  2. 一つの関数に長く複数の処理を書く
  3. ユニットテストを書かず、リファクタリング時に1から書き直す

部屋で例えると、

  • 一つの収納箱にあれこれ詰め込む
  • 物の定位置を決めず、空いているところに収納する
  • 整理してない収納箱を全てひっくり返して、再配置する

といった振る舞いと似てそうです。

処理が1箇所に纏まっていないことや、
どこに何が書いてあるかわからないことが複雑さを冗長させていると言えます。

jupyter notebookはコードを煩雑にしやすい

さらにデータ分析でおなじみのjupyter notebookは

  1. df.head()/describe()などデータの内部を確認できる機能が豊富
  2. 上下のセルから変数の中身が引き継がれる

といった特徴から、プロジェクト序盤は素早いフィードバックを得られて便利ですが、
これらの特徴は裏を返せば

  1. 変数の影響範囲が広くなりやすい
  2. 処理に影響を及ぼさないコードが増えやすい

とも言え、コード量が増えると急速に煩雑になってしまいます。

インテリアデザイナーには「平たい場所は乱雑さを蓄積しやすい」 という通説があるそうですが、
何でも1箇所に書けてしまう"notebook"は、データ分析における平たい場所であると言えます。

良いコードにする振る舞い

では良いコードにするにはどのようにすれば良いのでしょうか。
元記事では下記の5点が紹介されていました。

コードを綺麗に保つ

データ分析に限らず、綺麗なコードを書くセオリーがあります。
たとえば

  • DEAD CODEを消す
  • 処理の内容が明快にわかる変数名をつける
  • 似た記述はまとめる(DRYである)

データ分析も例外ではなく、これらのセオリーには従うべきです。
リーダブルコードなどの書籍にまとめられていて、目を通しておくべきでしょう。

関数を使って実装を抽象化する

一つの関心ごとに対しては一つの関数でまとめ、処理を抽象化するべきです。
そうすることで、以下のメリットが得られます。

  • 読みやすい
  • テストしやすい
  • 再利用しやすい(引数を与えて、ハードコーディングを防げる)

これは例を見てみるとわかりやすいです。

# bad example
pd.qcut(df['Fare'], q=4, retbins=True)[1] # returns array([0., 7.8958, 14.4542, 31.275, 512.3292])


df.loc[ df['Fare'] <= 7.90, 'Fare'] = 0
df.loc[(df['Fare'] > 7.90) & (df['Fare'] <= 14.454), 'Fare'] = 1
df.loc[(df['Fare'] > 14.454) & (df['Fare'] <= 31), 'Fare']   = 2
df.loc[ df['Fare'] > 31, 'Fare'] = 3
df['Fare'] = df['Fare'].astype(int)
df['FareBand'] = df['Fare']

# good example (after refactoring into functions)
df['FareBand'] = categorize_column(df['Fare'], num_bins=4)

good exampleの例であれば、pd.qcutの意味や引数を覚えていなくても、
連続値の'Fare'列をbin詰めする処理ができます。

イメージはこんな感じです笑
f:id:zerebom:20200611102441j:plain:w300 f:id:zerebom:20200611102458j:plain:w300 f:id:zerebom:20200611102508j:plain:w300

引用元(?):
ヘヴィメタバンド、スティール・パンサーのサッチェル氏→機材紹介がハードロック過ぎる - Togetter

なるべく早い段階でjupyter notebookを.pyに移行する

上で言及したように、notebookはコード量に伴い煩雑さが急速に増してしまいます。
したがって、コード量が増えてきたらなるべく早く.pyコードに書き換えるべきです。

notebookからpyファイルに書き換える手順は元記事で下記のように紹介されています。 f:id:zerebom:20200611102550p:plain 引用元: clean-code-ml/refactoring-process.md at master · davified/clean-code-ml · GitHub

  1. notebookが正しく動くか確認する
  2. 自動変換でpyファイルに出力する
  3. debugコードを取り除く(df.head()など)
  4. Code smell(直したい部分)をリストアップする
  5. 一纏めにしたい部分を特定する
  6. ユニットテストを書く
  7. テストを通すように記述する
  8. importを整理する
  9. 動作を確認し、commitする
  10. 繰り返す

テスト駆動開発で行う

データ分析業務もソフトウェア開発の例外にもれず、テストを書くべきです。

例えばモデルの挙動を調べるテストでは、
ベースラインで想定想定スコアを超えない場合はエラーとみなすコードを書くと良いそうです。

テストコードについては自分の知識も浅いので、またいつか改めて記事を書きたいと思います。

こまめなコミットをする

コミットを小さく頻繁に行うことで、下記のメリットが得られます。

  • 自分がどの問題に取り組んでいるか簡単に理解できる
  • 処理のロールバックが簡単にできる

自分なりの工夫点

最後に自分なりのコードを綺麗にする工夫点をいくつか紹介します。

業務ごとにコードをまとめスニペット化する

データ分析では、タスクが変わっても似たような処理を書くことが多いです。
コードをスニペットとして保存しておくと、似たような処理が必要になった時少ない作業量で書き終えることができます。

また、スニペットにすることを意識しながらコードを書くことで
自然と汎用性の高いコードが書けるようになります。

自分はGitHub GistとDashを使ってスニペットを保存しています。

gist.github.com

https://kapeli.com/dash

自分なりのルールを設ける

自分なりのルールを設けて、いつも似たコードを書くようにしています。
そうすることで他のスニペットとの互換性を良くしたり、素早くコードを書くことができます。

また自分はNN系のコードを書くときはhydraとpytorch-lightningを
使うことでいつも同じステップで書けるようにしています。

github.com
hydra.cc

データ分析のコードはあまり高級なラッパーを使うと、すぐ破綻してしまうので
その塩梅が難しいですが、うまく使えば綺麗にかけるでしょう。

メソッド名、I/Oなどを組み込み関数や有名ライブラリに近づける

sklearnやpandasなどの有名なライブラリの入出力と対応させてコードを書くことで、
他者とのコミニュケーションコストを抑えることができます。

綺麗な人のコードを見る

Kaggleなどデータ分析コンペティションでは、上位の人が解法を公開していることが多いので、
それを眺めると良いと思います。

他には、nyanpさんのnyaggleなど参考にさせていただいていますm(__)m GitHub - nyanp/nyaggle: Code for Kaggle and Offline Competitions

まとめ

以上です!
振り返ってみると当たり前のことばかりですが、全部を常に実践するのは難しい...!
綺麗なコードが書けるということはエンジニアの技量としてかなり本質的なものだと思っているので、
今後も頑張っていきたいと思います。

久しぶりにブログを書いたら、文章書くのが難しすぎてびっくりしました。
こっちも頑張っていきたいです。では〜

google-site-verification: google1c6f931fc8723fac.html