Elixirで機械学習に初挑戦: Explorerでデータ処理 (Part 2)

この記事を読む前に、[Part 1を読んでください](https://qiita.com/westbaystars/items/d5385dc210f110f31285)。

今まで

今までのLivebookが3つの部分に分けています:

### Notebook dependencies and setup

```elixir
Mix.install([
  {:exla, "~> 0.4"},
  {:axon, "~> 0.3"},
  {:explorer, "~> 0.4"},
  {:kino, "~> 0.6"}
])
```

### 準備モジュール

```elixir
alias Explorer.DataFrame, as: DF
alias Explorer.Series, as: S
require Explorer.DataFrame              # For DF.mutate

defmodule Pre do
  def count_missings(df) do
    DF.names(df)
    |> Enum.reduce(%{}, fn name, acc -> 
      count = DF.pull(df, name) 
        |> S.is_nil() |> S.sum()
      if (count > 0), do: Map.put(acc, name, count), else: acc
    end)
  end

  def separate(df, id, label) do
    {
      DF.pull(df, id) |> S.to_list(),
      DF.pull(df, label) 
      |> S.cast(:float) 
      |> S.to_list 
      |> Enum.map(& [[&1]] |> Nx.tensor),
      df |> DF.discard([id, label]) |> DF.to_rows
    }
  end

  def drop(df, keys) do
    keys
    |> Enum.reduce(df, & DF.discard(&2, &1))
  end

  def make_dummies(df, key) do
    DF.pull(df, key)
    |>S.to_list
    |> Enum.uniq
    |> Enum.with_index(& {&1, String.to_integer("#{&2}")})
    |> Enum.into(%{})
  end

  def to_dummies(df, keys) do
    keys
    |> Enum.reduce(df, fn key, acc ->
      map = make_dummies(acc, key)
      series = DF.pull(acc, key)
      new_series = S.transform(series, & Map.get(map, &1))
      DF.put(acc, key, new_series)
    end)
  end

  def map_to_tensor(datas) do
    datas
    |> Enum.map(& [Map.values(&1)] |> Nx.tensor)
  end
end
```

### メイン・プログラム

```elixir
train_df = 
  DF.from_csv!("data/titanic/train.csv")
  |> DF.rename_with(& String.downcase(&1))
  |> Pre.drop(~w(cabin name ticket))
  |> DF.mutate(embarked: fill_missing(embarked, "S"))
  |> DF.mutate(age: fill_missing(age, 0.0))
  |> Pre.to_dummies(["embarked", "sex"])

{trains_csv_ids, train_csv_labels, train_csv_maps} =
  train_df
  |> Pre.separate("passengerid", "survived")

train_csv_datas = train_csv_maps
  |> Pre.map_to_tensor
```

これまでできたら、@piacerexさんの「[Eixirで機械学習に初挑戦④：データ処理に強いElixirでKaggle挑戦（前半）…「データ前処理」の基礎編 ※最新Livebook 0.8に対応](https://qiita.com/piacerex/items/a40c5d24190d59f68610)」記事を「ⅳ）未知データに対する予測」から続きましょう。

## 知データに対する予測

### ①未知データのロードと学習データの列差異の確認

> 未知データをロードし、学習データとの列差異を確認します

```diff
- {test_csv_header, _} = Pre.header_and_csv_datas("test.csv")
- train_csv_header -- test_csv_header
+ test_df =
+   DF.from_csv!("data/titanic/test.csv")
+   |> DF.rename_with(& String.downcase(&1))
+  
+ {test_csv_ids, _, test_csv_maps} =
+   test_df
+   |> Pre.separate("passengerid", nil)
```

:::note info
メイン・プログラムに続いて、テストデータを読み込んで、ヘッダーを小文字に変換すると`separate`の処理しましょう。
:::

>survivedが存在しません … これは、未知データにはラベルが無いのが当たり前だからです

```elixir
test_df
|> DF.names()
```

```elixir
["passengerid", "pclass", "sex", "age", "sibsp", "parch", "fare", "embarked"]
```

> これに対応するため、Pre.separateを以下のように改修します

```diff
  defmodule Pre do
    …
+   def separate(df, id, nil) do
+     {
+       DF.pull(df, id) |> S.to_list(),
+       nil,
+       df |> DF.discard(id) |> DF.to_rows
+     }
+   end
    def separate(df, id, label) do
      {
        DF.pull(df, id) |> S.to_list(),
        DF.pull(df, label) 
        |> S.cast(:float) 
        |> S.to_list 
        |> Enum.map(& [[&1]] |> Nx.tensor),
        df |> DF.discard([id, label]) |> DF.to_rows
      }
    end
    …
```

> ラベルが無いCSVファイルのときは、ラベルとして`nil`を返せれるようになりました

:::note info
`label`があるかないかをチェックするより、ラベルが`nil`と渡して、`nil`がそのTupleの位置に返せれます。
:::

```elixir
{[892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910,
  911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929,
  930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, ...], nil,
 [
   %{"age" => 34.5, "cabin" => nil, "embarked" => "Q", "fare" => 7.8292, "name" => "Kelly, Mr. James", "parch" => 0, "pclass" => 3, "sex" => "male", "sibsp" => 0, "ticket" => "330911"},
   %{"age" => 21.0, "cabin" => nil, "embarked" => "S", "fare" => 8.6625, "name" => "Cacic, Miss. Manda", "parch" => 0, "pclass" => 3, "sex" => "female", "sibsp" => 0, ...},
   %{"age" => 25.0, "cabin" => nil, "embarked" => "S", "fare" => 9.5, "name" => "Sap, Mr. Julius", "parch" => 0, "pclass" => 3, "sex" => "male", ...},
   %{"age" => nil, "cabin" => nil, "embarked" => "S", "fare" => 56.4958, "name" => "Hee, Mr. Ling", "parch" => 0, "pclass" => 3, ...},
   %{"age" => 39.0, "cabin" => nil, "embarked" => "C", "fare" => 13.4167, "name" => "Karun, Mr. Franz", "parch" => 1, ...},
   %{"age" => nil, "cabin" => "D34", "embarked" => "S", "fare" => 26.55, "name" => "Franklin, Mr. Thomas Parham", ...},
   %{"age" => 41.0, "cabin" => nil, "embarked" => "S", "fare" => 7.85, ...},
   %{"age" => 30.0, "cabin" => nil, "embarked" => "S", ...},
   %{"age" => 45.0, "cabin" => "D19", ...},
   %{"age" => 25.0, ...},
   %{...},
   ...
 ]}
```

### ②空白値の確認

> 未知データに学習データと異なる欠損値が無いかチェックします

```diff
- test_csv_maps
+ test_df
  |> Pre.count_missings
```

> すると、普通にありましたｗ … fareの欠損値補完は、現在の「データ前処理」に含まれていないため、改修が必要です

```elixir
%{"age" => 86, "cabin" => 327, "fare" => 1}
```

> ここではいったん、fareは「0」で補完します

```diff
  test_df =
    DF.from_csv!("data/titanic/test.csv")
    |> DF.rename_with(& String.downcase(&1))
+   |> Pre.drop(~w(cabin name ticket))
+   |> DF.mutate(embarked: fill_missing(embarked, "S"))
+   |> DF.mutate(age: fill_missing(age, 0.0))
+   |> DF.mutate(fare: fill_missing(fare, 0.0))

  {test_csv_ids, _, test_csv_maps} =
    test_df
    |> Pre.separate("passengerid", nil)
```

> fareが「0」で補完されたことを確認できました

```elixir
DF.filter(test_df, fare == 0.0)
```

```elixir
#Explorer.DataFrame<
  Polars[3 x 8]
  passengerid integer [1044, 1158, 1264]
  pclass integer [3, 1, 1]
  sex string ["male", "male", "male"]
  age float [60.5, 0.0, 49.0]
  sibsp integer [0, 0, 0]
  parch integer [0, 0, 0]
  fare float [0.0, 0.0, 0.0]
  embarked string ["S", "S", "S"]
>
```

### ③未知データに対する予測の実施

> 未知データの「データ前処理」も済んだので、予測を実施してみます
>
> ここで、Pre.processの第二引数に、学習データであるtrain_csv_mapsを指定することがポイントで、これは前述した「to_dummiesの実施時、検証データや未知データではカテゴリ値が網羅されていないケースへの対策として、学習データからカテゴリ値を拾うため」となります

```diff
  test_df =
    DF.from_csv!("data/titanic/test.csv")
    |> DF.rename_with(& String.downcase(&1))
    |> Pre.drop(~w(cabin name ticket))
    |> DF.mutate(embarked: fill_missing(embarked, "S"))
    |> DF.mutate(age: fill_missing(age, 0.0))
    |> DF.mutate(fare: fill_missing(fare, 0.0))
+   |> Pre.to_dummies(["embarked", "sex"])
 
  {test_csv_ids, _, test_csv_maps} =
    test_df
    |> Pre.separate("passengerid", nil)
 
+ test_csv_datas = test_csv_maps
+   |> Pre.map_to_tensor
```

> また、test_csv_idsを予測結果リストとEnum.zipで連結し、ヘッダーも付けて、提出用CSVを作成します

```elixir
{test_csv_ids, _, test_csv_maps} = Pre.csv_file_to_datas("test.csv")

result = Pre.process(test_csv_maps, train_csv_maps)
    |> Enum.map(& Axon.predict(model, trained_state, &1)
        |> Nx.to_flat_list |> List.first |> round)
    |> then(& Enum.zip(test_csv_ids, &1))
    |> Enum.map(& [elem(&1, 0), Integer.to_string(elem(&1, 1))])
    |> then(& [["PassengerId", "Survived"] | &1])
```

:::note alert
**ちょっと待って下さい!**

`model`や`trained_state`がまだ作成していませんよ!

あっ、記事の「[本講義回の最終的なコード](https://qiita.com/piacerex/items/a40c5d24190d59f68610#%E6%9C%AC%E8%AC%9B%E7%BE%A9%E5%9B%9E%E3%81%AE%E6%9C%80%E7%B5%82%E7%9A%84%E3%81%AA%E3%82%B3%E3%83%BC%E3%83%89)」の「**ⅱ）モデルの学習*」にあります。でも、何も説明がありません。
:::

:::note info
じゃ、Livebookに新しい「モデルの学習」というセクションをメイン・プログラムの下に作って、以下のコードを入力してください。

```elixir
train_datas = Enum.zip(train_csv_datas, train_csv_labels)

model =
  Axon.input("input", shape: {nil, 7})
  |> Axon.dense(48, activation: :tanh)
  |> Axon.dropout(rate: 0.2)
  |> Axon.dense(48, activation: :tanh)
  |> Axon.dense(1, activation: :sigmoid)

trained_state =
  model
  |> Axon.Loop.trainer(:mean_squared_error, Axon.Optimizers.adam(0.0005))
  |> Axon.Loop.metric(:accuracy, "Accuracy")
  |> Axon.Loop.run(train_datas, %{}, epochs: 20, compiler: EXLA)
```

それから`results`を計算しましょう:

```diff
- result = Pre.process(test_csv_maps, train_csv_maps)
+ survived = test_csv_datas
    |> Enum.map(& Axon.predict(model, trained_state, &1)
      |> Nx.to_flat_list |> List.first |> round)
-   |> then(& Enum.zip(test_csv_ids, &1))
-   |> Enum.map(& [elem(&1, 0), Integer.to_string(elem(&1, 1))])
-   |> then(& [["PassengerId", "Survived"] | &1])
+ passenger_ids = DF.pull(test_df, "passengerid") |> S.to_list()
+ result = DF.new(PassengerId: passenger_ids, Survived: survived)
+
+ Kino.DataTable.new(result)
```
:::

> 提出用CSVができました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/c5060858-5767-75b1-915f-815ef9cf4249.png)

## Kaggleに提出（submit）してみる

> Kaggleへの提出用CSVファイルをresult.csvで生成します

```diff
  result
- |> CSV.encode
- |> Enum.to_list
- |> then(& File.write("result.csv", &1))
+ |> DF.to_csv!("data/titanic/result.csv")
```

:::note info
`result`がデータフレームに入れたので、簡単にCSVファイルを出力することができます。
:::

> result.csvをKaggleの「Submit Predictions」ボタンをクリックして、アップロードします

:::note info
オリジナル記事でこのファイルを提出する方法が書いてあります。

リンク: [Kaggleに提出（submit）してみる](https://qiita.com/piacerex/items/a40c5d24190d59f68610#kaggle%E3%81%AB%E6%8F%90%E5%87%BAsubmit%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)
:::

## まとめ

いろいろ勉強になりました。Axonのことだけではなくて、いろんな処理をするためにExplorerを今まで使ったことない関数をいっぱい使いました。

@piacerexさんに特別「ありがとうございました」と言いたいです。機械学習のシリーズの記事で使えるの自信が増えています。これからのデータ分析の新しいツールになってます。

では、これからも、よろしくお願いします。