学生が初めて実務でデータサイエンスを行いました

## イントロダクション

みなさん初めまして。情報工学部の4年生で、機械学習を勉強する学生団体に参加している学生です。縁あって、データサイエンティストのインターンを4ヶ月間行いました。

初めて実務でデータ分析を行うということで、あらかじめ知っていた方がよかったことをまとめ、次の案件を行うための前準備としたいと思います。

## この記事について
体系的に学んだ訳ではないのでこのTipsが100%正しいとは限りません。あくまで僕がこれまでやっていた手法よりは効率的な方法をまとめたものになります。

## 始める前の状況

履修済み
- データサイエンス
- パターン認識
- ソフトウェア工学
- コンピュータアーキテクチャ
- アルゴリズムとデータ構造
- etc...

## 案件の内容
Google satelliteから得られる衛星画像データや、田んぼのデータから田んぼをクラスタリング（守秘義務のため詳細は割愛）。

## 環境
Google Colab

## 1. 前処理
実務でのデータ分析では、前処理の良し悪しが推論結果を大きく左右します。大学のレジュメや機械学習の記事ばかり読んでいたので、ここまで前処理が重要だと思いませんでした。

### csvファイルの読み込み
初めは読み込みたいcsvファイルを別々に```pd.read_csv()```で読み込み、```df_hoge_1```、```df_hoge_2```のように変数を宣言したのですが、後々ファイル数が増えてくると何度も同じコードを書く必要が出てきます。とってもめんどくさいですね。

```Python:良くない例
df_hoge_1 = pd.read_csv('content/drive/Mydrive/hoge_1.csv')
df_hoge_2 = pd.read_csv('content/drive/Mydrive/hoge_2.csv')
```
数行のコードで済ませるために、```glob```でワイルドカードでcsvファイル名を取得してあげましょう。データフレームを```df_list```のようなリストに追加していけば、for文を回してあげるだけで簡単に取得することができます。

```Python:良い例
import glob

files = glob.glob('/content/drive/MyDrive/data/*.csv')
for filename in files:
  pd.read_csv(filename)
```

このように書くことで、後からデータが増えてもコードを変更する必要がありません。 \
あ、もちろん生データを置くフォルダは1つにまとめてくださいね。

### 欠損値処理
これに関してはデータの性質によります。```dropna```の引数を```how=any```か```how=all```のどちらにするか、NaNを0埋めするのか、補間にするのかはその場で判断しましょう。

### グラフ表示
基本的にPlotlyを使いましょう。マウスホバーは異常値の特定に便利ですし、ズームはデータ数が多い場合に役立ちます。
3次元のグラフまでなら対応できるので、積極的に使ってみてください。

また、どんな些細なグラフでもメインタイトル、x軸のタイトル、y軸のタイトルは忘れないでください。何を表すグラフなのかを、グラフのみの情報で分かるようにしましょう。来週の自分が見た時に何のグラフだったかを忘れることもありますよ。

自分はよくタイトルなどの設定方法を忘れがちなので、簡単な関数を使い回していました。

```Python:簡単なサンプル
import plotly.graph_objects as go

def show_graph(
    df,
    x_col,
    y_col,
    title="title",
    x_title="x_title",
    y_title="y_title",
    ):
  fig = go.Figure()
  fig.add_trace(go.Scatter(x=df[x_col],
                          y=df[y_col],
                          mode='lines',)
  )
  
  fig.update_layout(
      title=title,
      xaxis=dict(title=x_title),
      yaxis1=dict(title=y_title,
                  side='left'),
      yaxis2=dict(side='right',
                  showgrid=False,                              
                  overlaying='y'))
  fig.show()
```

### 補間
時系列データフレーム同士を結合する場合、互いに同じ時刻のデータを持つとは限りません。
最低でも片方のデータフレームは補間で補う必要がありますが、スプラインにしても移動平均にしてもパラメーターは手動で指定してあげる必要があります。
パラメータの更新ごとにセルを実行するのは効率が悪いので、for文などで候補を一気に突っ込んじゃいましょう。


```Python:移動平均の場合
win_size_list = [10, 20, 30, 40]

for win_size in win_size_list:
    df_rolling = df.rolling(win_size)
    show_graph(df_rolling, x_col='hoge', y_col='fuga') 
```

## 2. ひとまずの学習
意外と簡単なモデルで課題解決できることもあります。最新モデルは実行に時間がかかるものも多いので、線形回帰などの簡単な学習を試してみましょう。
今回は分類タスクですが、回帰直線のyを閾値で区切ってあげれば分類を行うことができます。

## 3. 本番の学習
分類モデルにはロジスティック回帰、SVM、決定木など多数のモデルがあります。各モデルの特性を深く知りながら使う必要がありますが、実際予想外のモデルを利用した方がスコアが高くなることがあります。学習にかかる時間が1ケタ分なら、とりあえずデータを突っ込んでみるのがいいでしょう。
また、`sklearn`で手軽に出せるスコアが`accuracy`のみなので、`cross_validate`を用いて複数のスコアを出せるようにしましょう（交差検証については後述）。
僕は最低でも`recall`、`precision`、`f1`は使っています。

## 4. 交差検証
実務では0.9以上の精度が出ることは稀です。精度が高すぎるモデルが出たら逆に疑いましょう。
そのような状況を防ぐために、交差検証（cross_validation）を行います。
過学習を防ぎ、汎化性能を上げることが期待できます。
ちなみに僕は正解ラベルを含んだデータで決定木を学習していたことがあり、交差検証を行なっても全モデルの精度が100%になっていたことがありました。

## 5. スタッキング
今回のデータセットではaccuracyやf値などを用いていたため、スコアごとに最も良いモデルがバラけていました。
そこで、スタッキングを用いてそれぞれのモデルの主張を全て拾い上げてあげましょう。今回はどのモデルも比較的有用だったので、Ridgeでまとめたいと思います。
最終的にどのような重みで学習されたのかをグラフにしてあげるとより丁寧でしょう。

```Python:Ridgeの擬似コード
clf = Ridge(alpha=0.1, random_state=42)
# y_predsはモデルの出力をデータフレームにまとめたもの yは正解ラベル
clf.fit(y_preds, y)
plt.figure(figsize=(12, 7))
plt.title('Ridge')
plt.bar(x=y_preds.columns, height=clf.coef_[0])
plt.xticks(rotation=90) # モデル名が日本語のため、回転させて文字が重ならないようにする
plt.tight_layout();
```


## 6. コーディングのTips
### データをいじるときは都度新しい変数を用意する
欠損値を削除したときや補間を行ったとき、グラフ表示すると「なんか違う」と思うことがあります。その際、元のデータフレームを更新していると別の補間方法を試せませんよね？
後で綺麗なコードにすればいいので、調査段階ではたくさん変数を作りましょう。

```Python:良くない例
df = df.rolling(100, center=True)
show_graph(df, x_col='hoge', y_col='fuga') # 思ってたんと違うグラフが表示される

df = pd.read_csv('content/drive/Mydrive/hoge.csv')
df_rolling_100 = df.rolling(100, center=True)
show_graph(df_rolling_100, x_col='hoge', y_col='fuga')
```

```Python:良い例
df_rolling_100 = df.rolling(100, center=True)
show_graph(df_rolling_100, x_col='hoge', y_col='fuga') # 思ってたんと違うグラフが表示される

df_rolling_1000 = df.rolling(1000, center=True)
show_graph(df_rolling_1000, x_col='hoge', y_col='fuga')
```
### キーワード引数を明示的に書く
僕のPython歴が浅いのが一番の原因だと思いますが、位置引数で指定すると「何を指定しているんだっけ」となることが時々あります。慣れないうちはキーワード引数を書いてあげましょう。

```Python:良くない例(csvへの書き込み)
with open('hoge.csv', 'a') as f:
    writer = csv.writer(f)
    writer.writerow(hoge_list)
```

```Python:良い例
with open('hoge.csv', mode='a') as f:
    writer = csv.writer(f)
    writer.writerow(hoge_list)
```

### 初めのセルでまとめてimportしない
別のノートブックに同じ処理をコピペするとき、import文もまとめて持っていきたいです。
パッケージは必要になったセルでインポートするようにしましょう。

## 最後に
ここまで紹介してきた方法は、書籍や記事などで「良い方法」と紹介されたものではありません。一般的に非推奨な方法があったらコメントで教えてくださると嬉しいです。