DVCでデータ分析実務を想定したKaggle環境を作ろう！

この記事はKaggle Advent Calender 12/04の記事として書かれています。

Kaggleはデータ分析者の登竜門としても使われことが多いかと思います。実際自分もその一人でした。
しかし、実際にデータ分析実務をして気づきます「あれ実務、Kaggleと全然ちがくね」。

何が違うかというと、分析ゴリゴリというよりもデータ生成周りが賢くなること、それが機動的に行えることが大事なのです。

今回はこのことを意識した分析環境づくりを僕おすすめのDVCで作って見たいと思います。データ分析用のツールは他にKedroやMLFlowなどのツールがありますが、Kedroは複雑で覚えること多いですし、MLFlowはPythonのコード修正が必要だったり、そもそも実験の管理自体はハイパラチューニングとしてPython内部でやれる（どっちかというと学習器のつなげ方などのコードレベルでの変化をトラッキングしたい）。
何も題材もないと不便なので、KaggleのTitanicを題材にしてやってみようと思います。

[githubレポジトリ](https://github.com/IntenF/dvc_on_titanic_kaggle)


# DVCとは？

[DVC(Data Version Control)](https://dvc.org/doc)はGit管理下でデータ分析をするためのライブラリです。
主な機能はgitでラージファイルを扱えるようにさせる点にあります。この点はGit LFSにそっくりで、やり方もハッシュをGitに管理させるので同じです。
ただし、DVCはデータ分析用にデータパイプライン形成、実験管理などの機能も持っています。しかも基本はdvc.yamlをいじるだけで、pythonのコード自体はいつもどおりで構わないです（というかPythonである必要もない）。

また、dvcはgitの機能に似せて作っているのでgitに慣れていれば学習コストは低くすみます。

# DVC導入

導入方法は簡単です。以下のようにコードを実行するだけ（事前にgitレポジトリは作成してください）

```bash
$ pip install dvc
$ dvc init
```
すると以下のファイル・フォルダが作成されます。

- .dvc: データの実体を保存するためのキャッシュフォルダ
- .dvcignore: dvcで管理したくないファイルがあるときに明示するファイル。.gitignoreに相当します。

増えたファイルはgit管理下に置きます。

```bash
$ git add .dvc/* .gitignore
$ git commit -m "dvc init"
```

# データ追加

https://www.kaggle.com/competitions/titanic/data

KaggleからTitanic号のデータ以下の用に追加します。

```
- data
    - raw
        - train.csv
        - test.csv
        - gender_submission.csv
```

このデータをDVCに追加します。

```bash
dvc add data/raw/train.csv data/raw/test.csv data/raw/gender_submission.csv
# フォルダごと追加することも可能
# dvc add data/raw
```

実行すると[データ名].dvcというファイルと.gitignoreが生成されます。.dvcの方はデータのハッシュのファイルで、gitignoreでデータの実体のファイルをgit管理下から除外してくれています。

```bash
$ git add data/raw/.gitignore  data/raw/*
$ git commit -m "dvc add raw data"
```

今回は省きますが、dvcではS3やGCSなどのクラウドのストレージ、共有ドライブなどよく使うファイル共有先をリモートレポジトリとして登録でき、リモートレポジトリにデータをバックアップすることができます。
詳しくは[公式サイト](https://dvc.org/doc/start/data-management/data-versioning)を参考にしてください。

# データパイプライン構築

DVCを使うとデータパイプラインを簡単に作ることができます。
加工するためのコードには`src/proc.py`を使います。
パイプラインの構築方法には

1. dvc runを使う
1. dvc.yamlの編集

の2つの方法があります。
ここではより汎用的なdvc.yamlを編集する方法をとります。

ここでは学習するための加工済みデータを用意して、学習・推論するパイプラインを作ります。コードには`src/proc.py`と`src/train.py`を使います。

```src/proc.py
import sys
from pathlib import Path
import pandas as pd

ROOT = Path(__file__).resolve().parent.parent
RAW_DIR = ROOT/'data'/'raw'
OUT_DIR = ROOT/'data'/'out'

if __name__ == '__main__':
    train_csv = Path(RAW_DIR/'train.csv')
    test_csv = Path(RAW_DIR/'test.csv')
    train_df = pd.read_csv(train_csv)
    test_df = pd.read_csv(test_csv)
    test_df['Survived'] = -1
    train_df['is_Test'] = 0
    test_df['is_Test'] = 1

    concat_df = pd.concat([train_df, test_df])
    concat_df = pd.get_dummies(concat_df , dummy_na  = False, drop_first = False,columns=['Sex', 'Ticket', 'Cabin', 'Embarked'])
    concat_df['Age'] = concat_df['Age'].fillna(train_df['Age'].mean())
    concat_df['Fare'] = concat_df['Fare'].fillna(train_df['Fare'].mean())
    del concat_df['Name']

    concat_df.to_pickle(OUT_DIR/'preprocess.pkl')
```

```src/train.py
import sys
from pathlib import Path
import yaml

import pandas as pd
from sklearn import tree
from sklearn.model_selection import train_test_split

ROOT = Path(__file__).resolve().parent.parent
OUT_DIR = ROOT/'data'/'out'

if __name__ == '__main__':
    df = pd.read_pickle(OUT_DIR/'preprocess.pkl')

    train_valid = df.query('is_Test == 0').copy()
    test = df.query('is_Test == 1').copy()

    train, valid = train_test_split(train_valid, test_size=0.2, shuffle=True, random_state=0)

    train_X = train.drop(columns=['Survived', 'is_Test'])
    train_y = train['Survived']
    valid_X = valid.drop(columns=['Survived', 'is_Test'])
    valid_y = valid['Survived']
    test_X = test.drop(columns=['Survived', 'is_Test'])

    clf = tree.DecisionTreeClassifier().fit(train_X, train_y)
    accuracy = (clf.predict(valid_X) == valid_y).mean().tolist()
    obj = {'accuracy': accuracy}
    with open('data/out/summary.yaml', 'w') as f:
        yaml.dump(obj, f, encoding='utf-8')

    test['Survived'] = clf.predict(test_X)
    test[['PassengerId', 'Survived']].to_csv('data/out/gender_submission.csv', index=None)
```

次に、プロジェクト直下に`dvc.yaml`を作ります。

```dvc.yml
stages:
  data_proc:
    cmd: python src/proc.py
    deps:
    - data/raw/test.csv
    - data/raw/train.csv
    - src/proc.py
    outs:
    - data/out/preprocess.pkl
   
  train:
    cmd: python src/train.py
    deps:
    - data/out/preprocess.pkl
    - src/train.py
    outs:
    - data/out/gender_submission.csv
    metrics:
    - data/out/summary.yaml:
        cache: false
 ```

このyamlの意味は

- data_proc/train: プロセスの名前です。任意の名称をつけることができます
- cmd: このプロセス自体を示すコマンドです。
- deps: プロセスが依存するデータ・ソースコード
- outs: プロセスが生成するデータ
- metics: outs同様プロセスが生成するデータですが、dvc metricsで集計する対象となります。
    - cache: dvcで追っかける必要がないくらい容量が小さいファイルであることを明示する

dvc.yamlが書けたら

```bash
$ dvc repro
$ git add -A
$ git add commit -m "dvc repro 1"
```

を実行します。すると`cmd`で指定したコードが順に実行されます。

dvcがすごいのはここからで、train.pyを書き換えます。
例えば、シード値を書き換えます。

```train.py
# 修正前L18: 
train, valid = train_test_split(train_valid, test_size=0.2, shuffle=True, random_state=0)
# 修正後:
train, valid = train_test_split(train_valid, test_size=0.2, shuffle=True, random_state=1000)
```

コードの書き換えが起こったので、データの再出力が必要です。
しかし、書き換えたのはtrain.pyだけなのでプロセス`data_proc`は再度処理される必要はありません。なので、`dvc repro`を実行すると

```bash
$ dvc repro
'data\raw\test.csv.dvc' didn't change, skipping
'data\raw\train.csv.dvc' didn't change, skipping
Stage 'data_proc' didn't change, skipping
Running stage 'train':
> python src/train.py
Updating lock file 'dvc.lock'
```
このように、`data_proc`は実行されず`train`だけが実行されます。

また、metricsを指定したので

```bash
$ dvc metrics diff
Path                   Metric    HEAD     workspace    Change
data\out\summary.yaml  accuracy  0.86592  0.8324       -0.03352
```

このように、train.pyを書き換えたことでaccuracyがどのように変わったかが確認できます。

# まとめ

1. dvcの導入は`pip install dvc`で簡単
1. `dvc.yaml`を書くだけでデータプロセスが作れる
1. `dvc repro`を唱えるだけで、よしなにデータ再生成ができる