Kaggleでのソースコード管理の手助け

この記事は**「Gitでコード管理しているのに、KaggleのNotebook提出用に別のコードを用意するのが面倒だ」**と感じてる人向けの記事となっております。また、Notebookで提出するというコンペである`CodeCompetition`を対象として話を進めてゆきます。`CodeCompetition`については後述します。


# はじめに
再掲しますが**「Gitでコード管理しているのに、KaggleのNotebook提出用に別のコードを用意するのが面倒だ」**。この不満、心当たりがある方が多いのではないでしょうか。

今回書く記事はその不満を解消できる方法を発見しましたので共有し、その方法について解説するというのが趣旨になります。

また記事内容ですが主にこちらの[Kaggle Kernel - lopuhin/imet-2019-submission](https://www.kaggle.com/lopuhin/imet-2019-submission)の内容を参考に書いております。

前置きのために記事では`CodeCompetition`とはから説明していますが本題から読みたい方は[#git等で管理しているファイル構造をそのまま使う](#git等で管理しているファイル構造をそのまま使う)から読んで下さい。

# CodeCompetitionとは

冒頭で触れた`CodeCompetition`について説明させてください。

[Kaggleのドキュメント](https://www.kaggle.com/docs/competitions#kernels-only-competitions)で説明されている内容を引用します。

> Some competitions are code competitions. In these competitions all submissions are made from inside of a Kaggle Notebook, and it is not possible to upload submissions to the Competition directly.

下記はそれを私が意訳したものです。
CSVなどのファイルを直接アップロードするのではなく、Notebookを利用してそこにファイル提出するまでのソースコードを書きそれを提出するという形式のコンペを指します。

開催中(2019/12/25現在)のコンペ8種の内(Featured/Researchのみカウント)6種類が`CodeCompetition`であるのでおそらく参加されている方も多いと思います。

## [参考]CodeCompetition例

赤枠で囲ったCode Competitionというタグが付けられたコンペがCodeCompetitionになります。

![code-competitionn.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40310/7bd3d9c5-10ec-ef38-3dcb-f94bc11c5945.png)

## CodeCompetitionでの提出方法

Kaggle内でコードを書こうとすると`Scripts`と`Notebooks`という２種類の記述する手段があり、`CodeCompetition`では２つの形式のどちらかに提出ファイルを生成するまでのコードを書きそれを提出する形になります。

#### Scripts
このScriptのみにコードを書いてゆきます。

![kaggle_script.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40310/1c46c116-b705-05d4-cbcc-3d1b8fdfc961.png)

### Notebooks

いわゆるJupter Notebooks、Scripts同様これのみにコードを書いてゆきます。

![kaggle_notebook.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40310/fe1ada6c-9909-20ab-b45a-e0f1b0c971d3.png)


### CodeCompetitionの不満点

デバッグやテストのしやすさを考えると、こんな感じで処理毎にモジュール(ファイル)で分けたいという思いがあると思います。([タイタニックコンペ](https://www.kaggle.com/c/titanic)に取り組んだ際の一例です)

```:タイタニックでのファイル構造例
titanic_sample
  │  dataset.py
  │  model.text
  │  predict.py
  │  train.py
  │  utils.py
  │  __init__.py
  │
  ├─resources
     gender_submission.csv
     test.csv
     train.csv

```

提出時にも同様のファイル構造で提出できればよいのですが上記で触れたようにScriptsかもしくはNotebooksにほぼ提出専用のコードを書くことになっているというのが現状の不満点です。

# Git等で管理しているファイル構造をそのまま使う！
## パッケージとしてインストールしてしまおう

遅くなりましたが、本題に入ります。

Gitで管理しているコード郡を`Notebook`環境下でそのまま使えれば、KaggleのNotebook提出用に別のコードを用意する必要なんてないわけです。というわけで`Notebook`環境下にそれらのコードを用意してしまうというのが今回説明する解決方法になります。

## 簡単な概要

これ以降手順の説明に入りますが説明が長くなるので簡単にやることを書くと、
KaggleのNotebookにGitで管理しているコードをアップロードして、それらをパッケージとしてインストールしSubmitまでの書かれたコードをコマンドライン上から実行するという流れです。


## Submitするまでの手順説明
具体的にどうやるのか手順を説明します。

説明をわかりやすくするために[タイタニックのデータ](https://www.kaggle.com/c/titanic/data)を使って説明します。
また、前処理、モデルの学習等のコードはこちらを参考にさせていただきました。
[Kaggle Kernel - sishihara/upura-kaggle-tutorial-04-lightgbm](https://www.kaggle.com/sishihara/upura-kaggle-tutorial-04-lightgbm)

### 手順①
[Github - lopuhin/kaggle-script-template](https://github.com/lopuhin/kaggle-script-template)をForkします。

### 手順②
ForkしたリポジトリをTemplateリポジトリにする。この作業は色々なコンペでこのTemplateリポジトリを使い回すためにやっています。
参考に私が作成したTemplateリポジトリのURLを貼っておきます。
[Github - wakamezake/kaggle-script-template](https://github.com/wakamezake/kaggle-script-template)

### 手順③
手順②で作成したTemplateリポジトリを使って、コンペ用のリポジトリを作成する。
[Github - wakamezake/Titanic_submit_script](https://github.com/wakamezake/Titanic_submit_script)

### 手順④
easy_goldディレクトリ([Github - wakamezake/Titanic_submit_script](https://github.com/wakamezake/Titanic_submit_script)だとtitanic_sampleにrenameしてます)にいつもどおりコードを書く。注意点として以下のように最終的に`submission.csv`が出力されるようにコードを書いてください。

```python:script.py
def run(command):
    os.system('export PYTHONPATH=${PYTHONPATH}:/kaggle/working && ' + command)

run('python setup.py develop --install-dir /kaggle/working')
run('python titanic_sample/train.py model.text --test_size 0.3')
run('python titanic_sample/predict.py model.text submission.csv')
```

そしてタイタニックでの例ですがおそらく以下のようなフォルダ構成になると思います。

```:タイタニックでのフォルダ構成例
.
│  .gitignore
│  build.py
│  README.rst
│  script_template.py
│  setup.py
│
├─build
│   .keep
│   script.py
│
├─titanic_sample
  │  dataset.py
  │  model.text
  │  predict.py
  │  train.py
  │  utils.py
  │  __init__.py
  │
  ├─resources
       gender_submission.csv
       test.csv
       train.csv
```

### 手順⑤
`build.py`を実行します。実行後`build/script.py`が生成されているのでそれを`Notebooks`に貼り付けます。その後はCommitボタンを押してsubmitするといういつもの流れです。
貼り付けた例としてこちらのカーネルを用意しました。[Kaggle Kernel - wakamezake/titanic-submission-sample](https://www.kaggle.com/wakamezake/titanic-submission-sample)

Submitするまでの手順説明は以上になります。

## 少し解説します
手順をざっと説明してきましたがやったことを解説しますと・・・

手順の中で実行した`build.py`について。まずすべてのコードをbase64でエンコードして文字列に変換します、その際エンコードしたコードのファイルパスをkeyにエンコードして文字列をvalueにした辞書を作ります。そして`script_template.py`という`script.py`を作るためのテンプレートの`file_data`部分に辞書の内容を置換します。

`script.py`を`Notebooks`に貼り付けたあとエンコードされたコードをデコードして元のファイル構造を保った状態に戻します。そして`python setup.py develop --install-dir /kaggle/working`を実行して`Notebooks`の環境にデコードしたコードをパッケージとしてインストールします。

解説は以上です。

### `build.py`

```python:build.py
#!/usr/bin/env python3
import base64
import gzip
import os
from pathlib import Path

is_Windows = True if os.name == "nt" else False


def encode_file(path: Path) -> str:
    compressed = gzip.compress(path.read_bytes(), compresslevel=9)
    return base64.b64encode(compressed).decode('utf-8')


def build_script():
    to_encode = list(Path('titanic_sample').glob('*.py')) + [Path('setup.py')]
    if is_Windows:
        # https://stackoverflow.com/questions/54671385/convert-windowspath-to-posixpath
        file_data = {str(path.as_posix()): encode_file(path) for path in
                     to_encode}
    else:
        file_data = {str(path): encode_file(path) for path in to_encode}
    template = Path('script_template.py').read_text('utf8')
    Path('build/script.py').write_text(
        template.replace('{file_data}', str(file_data)),
        encoding='utf8')


if __name__ == '__main__':
    build_script()

```

### `script_template.py`

```python:script_template.py
import gzip
import base64
import os
from pathlib import Path
from typing import Dict


# this is base64 encoded source code
file_data: Dict = {file_data} # ←ここに置換しています


for path, encoded in file_data.items():
    print(path)
    path = Path(path)
    path.parent.mkdir(exist_ok=True)
    path.write_bytes(gzip.decompress(base64.b64decode(encoded)))


def run(command):
    os.system('export PYTHONPATH=${PYTHONPATH}:/kaggle/working && ' + command)


run('python setup.py develop --install-dir /kaggle/working')
run('python titanic_sample/main.py')

```

### `script.py`

```python:script.py
import base64
import gzip
import os
from pathlib import Path
from typing import Dict

# this is base64 encoded source code
file_data: Dict = {
    'titanic_sample/dataset.py': 'H4sIAHQXAl4C/6VVUW/aMBB+R+I/WLzE0VCA7Q0pD6grVbVp7QZvCEUmOahVx7FsB8Gm/fednYQktKs2Na3ks+/7Pp/Pd2avi5woZp8E3xGeq0Jb8ojT4WA4qKeyzNWZMEOkuqwpJjNcwX+VDQd7J2KeBTAto7zIQCQGBKSWF7IRtZpxmVgwNjFKcOs28DzLLZM8TQzLlYCotFyYhvTwLfmyuLv7euvQnxfrRfLj4WFNYh8iDaJowqUq7aTWCAjftxwCwgAJookGU5Q6BROEw8ENs3AoNE+ZSJbAbIleFNwEt/mO6WfIgjEJHlPBjHHWCk7Bdjioond5QmwbyISMvCdKzXGEKHe6V0HoqDGm3OX8ddQBZAY68QhjMHc1xf1lsCdKg9IFnsPQjFkWzocDgp+zN1WgESIES4FugpwJcAfYg7e2Y7KZjskMR0yZw8RrXULYlbhkYBvtuRCS0WAV/BMeT/JCImeK/kKBOcF9gxscZzh+x/Hj7zBixp4V0ErOfVzanviSaWgDkSrKgUna9YVvhLY4dMjdpRwyjjpvcZcs5+K84j+hc7BHptMnnH9o0s13K+Xnsy733ixEISvitHVEokg3r6nHPildVi2nAStTenJbAKJgWVUelja3XwNVhnePXqwY2imx8Irs9AxYWvUhBhFPo0+NlC9lDKGr1RZ+nSPHvMY0Ze938yCmD2CTtBAIHa1KfeRHyEZtQiqFtJAps3TjNxl7aaxPg50fLxk2b9gnXJV/7cSHxoLbqsxl1cnfWO5L/xF7GOQB9L1v6jVPn8E664btuHRtfbmfTBeK9qXGhJ24iWcvKqWfLH+rcwGySlW47aXJe1vnfNvQz0kj4MdNm7Fa4NQHVBG2qCa68IKuNvQvzd+xPe0xGkcmeDZuwnGGX2m27TzY9NRiKqNt3v//2gK8WO/S0/iDVOSJsfi8x9N3SRk8neX7c1yfM+w1Gn2ZvCr5+KTQ6zQi9Q+pvtnNYQcAAA==',
    'titanic_sample/predict.py': 'H4sIAHQXAl4C/31QwWrEIBC9B/IPspcYCNJLL4UU2kPPhR5LGcw6SYUYg06W3b+vRpPdbqEi6Pjec948bWbriEk3zNJ5LIuy0Olp1MM3DZ1h0rNx6CLSO2sYaZKTPoKXZh5RKEnSI7FNZaWC/Nakyi+d0RT1ZaGwZwMShH6LwYk8r5/KgoW1tnes3a2Il8x5XxFe3/KEVGr/hB+MVTjCLOn78B/NLjQvlA39ogeOD72zaj2izm9dHdLippV2HcRIPe3+8w93wyWMQ8PCPgOhp7pJdR3Yt2lt5OBuQ5LRDViHjNDQiVdrPYVQ0uC9HrGNBsQ1iCy6wOxQBdWKiFjoI/Fs5Y7D8+2ZPYjHWkhPlxm5nuhq7bP6WNxJn1BVX0GRBDsqyMLRn/jq5W/aDdOTwnP7JkePdQpS9wxgkgYBWNuyCiDGClDlXFPIZfEDh06KTakCAAA=',
    'titanic_sample/train.py': 'H4sIAHQXAl4C/5VUTY+bMBC9R8p/sHKBSCxN1dtKPqy0Pe+q3dtqZQ0wsG4xINuJklb97x1/AEmWRiocsPzezLzxPCPV0GvLQDcDaIPr1Xolw1Yrm3fbFIqBYW1TOKTWvWJWWuhkKQyoocW8AgsGLYtRjw8vD+Lb09NLxtoeKhFhF201yE4MYN8Zn3nsE9t4JC/NYUMsNHaZREDkuLfCmjVoBSnfK+ysSbf36xWjxzeiKXxsKn+InGePpNtzXg5VNSVJN6qvsPX1N7dod3dep5G/cJMxexqQ19SvzRjpgn1r+S7/EhNQlCE1MY//uExm1KHR7nXnaXNr/kjSo/DfjJ3GxVEcoJWV24mLEiw2vZYltKJGoFxoxqOguYVAqk/r/DFM42PeQP/Xs1CCL5adqyKp+1D0WrrGGjV2JfJJ6G0l/yFkGh4od/q/57xJX/zA0soDJvcsKWQH+pQE+M8Y520Q9YdRzPEhZ8aWNPvGBHVr+OuMTyfydk5FXfRkBbfPP+/OkG6vRNH35C/d77uKwN05jKDbkzC2HwbZNYFjiBQYl6byfcyuUq6T0RzRl1eXKGCTu8M1Mvm0EfDZQdNUaYc424ylp9ldHtu6gzz7GaRTslhtaYQU85p8VQXon1glGUueyxaMcavveEzeLud0cV34tb35LZvHBvi1Pflo06WgJcW3roTXmRs4oPDL1B/q/LPZhhnJmgnRgUIhGOcsEcJNTIgkjizMb736C/g+V6a2BQAA',
    'titanic_sample/utils.py': 'H4sIAHQXAl4C/yXNsQqDMBCA4T2Qdwg4OOnVLqVCB6FFRFFw6SimBD2a5II5C337tnT8v+VHF2hjRVEKKRK1ModYAizI667zBzmwFPYVPTznZbEmQ2c4Ox6KM2hLGtwc2WzwU9gZbczDO+mKkxRDP7VVXXe3Umkiqy4q/fd0H8a26evp2oypQv+d58a/cCMvxQet+8gdkAAAAA==',
    'titanic_sample/__init__.py': 'H4sIAHQXAl4C/wMAAAAAAAAAAAA=',
    'setup.py': 'H4sIAHQXAl4C/0srys9VKE4tKS0oyc/PKVbIzC3ILyqBiPBy8XKBGRq8XApAkJeYm2qrXpJZkpiXmRxfnJhbkJOqrgORK0hMzk5MTy22jUZXEAtUocnLBQBmZqg3aQAAAA=='}

for path, encoded in file_data.items():
    print(path)
    path = Path(path)
    path.parent.mkdir(exist_ok=True)
    path.write_bytes(gzip.decompress(base64.b64decode(encoded)))


def run(command):
    os.system('export PYTHONPATH=${PYTHONPATH}:/kaggle/working && ' + command)


run('python setup.py develop --install-dir /kaggle/working')
run('python titanic_sample/main.py')
```

# 参考
- [Kaggle Kernel - lopuhin/imet-2019-submission](https://www.kaggle.com/lopuhin/imet-2019-submission)
- [Github - lopuhin/kaggle-script-template](https://github.com/lopuhin/kaggle-script-template)
- [Kaggle Kernel - sishihara/upura-kaggle-tutorial-04-lightgbm](https://www.kaggle.com/sishihara/upura-kaggle-tutorial-04-lightgbm)
- [Kaggle Docs](https://www.kaggle.com/docs/competitions#kernels-only-competitions)
- [Kaggle competitions - titanic](https://www.kaggle.com/c/titanic)