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

@piacerex さんは **Elixir Advent Calendar 2022** の「[Eixirで機械学習に初挑戦④：データ処理に強いElixirでKaggle挑戦（前半）…「データ前処理」の基礎編 ※最新Livebook 0.8に対応](https://qiita.com/piacerex/items/a40c5d24190d59f68610)」という記事にデータ処理を`Enum`や`Map`で処理しました。Explorerはこのために作れりてますので、その部分の処理を書き直しました。

Kaggle挑戦について、上記の記事を読むことをお勧めします。

## 準備

データが必要です。このチュートリアルは「タイタニック問題」の挑戦を利用してるので、(Titanic - Machine Learning From Disaster Dataset)[https://www.kaggle.com/competitions/titanic/data]ページの右下にいる「Download All」ボタンをクリックして、保存します。`Zip`ファイルの中に3津`CSV`ファイルがあります:

1. `gender_submission.csv` - サンプルコンペの結果申し込むデータ
1. `test.csv` - 実験・確認するためのデータ
1. `train.csv` - 学習データ

ファイルを`$HOME/data/titanic`に解凍してください。これで他の挑戦のデータを区別するし、わかり安にパスにアクセンできるためですね。

## 生データをLivebookに読み込む

> Livebook最上部の「Notebook dependencies and setup」で、下記を実行し、ライブラリ群をロードしてください

```diff
  Mix.install([
-   {:csv, "~> 3.0"},
    {:exla, "~> 0.4"},
    {:axon, "~> 0.3"},
+   {:explorer, "~> 0.4"},   # 追加
+   {:kino, "~> 0.6"}        # 追加
  ])
```

:::note info
CSVパーサがExplorerにあるので、いらなくなります。ExplorerとKinoのライブラリーを以前の記事に追加します。
:::

> それから、ライブラリ「CSV」でtrain.csvをロードします

```diff
+ alias Explorer.DataFrame, as: DF
+ alias Explorer.Series, as: S
+ require Explorer.DataFrame              # For DF.mutate
  
- train_csv_raw =
-   File.stream!("train.csv")
+ train_df =
+   DF.from_csv!("data/titanic/train.csv")
-   |> CSV.decode!
-   |> Enum.to_list
  
+ Kino.DataTable.new(train_csv_raw)
```

:::note info
`DataFrame`を`DF`と`Series`を`S`にアリアスして便利です。それから`DF.mutate`などのマクロを使うために`require Explorer.DataFrame`をしています。

その後、直接`train.csv`をうちのパスから`DataFrame`にロードします。デコードやリストに変更することが必要ないです。

最後にデータをきれいなテーブルで出力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/03e5009c-a59b-9c36-65c9-9e0b30facbdf.png)
:::

## データ操作しやすくするためにマップ群に変換

> リスト内リストのままだと、ヘッダーの列名で値にアクセスするのが不便なので、ヘッダーの列名をキーとするマップ群に変換します
> 
> まず、ヘッダーをhdで取り出し、小文字化し、アトム化します

```diff
  train_df =
    DF.from_csv!("data/titanic/train.csv")
+   |> DF.rename_with(& String.downcase(&1))
 
- train_csv_header =
-   train_csv_raw
-   |> hd
-   |> Enum.map(& 1 |> String.downcase |> String.to_atom)
+ train_df_header = DF.names(train_df)
```

> 下記のように、ヘッダーを小文字化／アトム化されたリストにします

:::note info
ヘッダーがデータフレームの中にアクセスできるので、変数が本当に必要ないです。でも、すべてのヘッダーが小文字に変更することでデータフレームのマクロで使えるようになりますので、データフレームの初期化するところに追加しました。
:::

```elixir
["passengerid", "survived", "pclass", "name", "sex", "age", "sibsp", "parch", "ticket", "fare",
 "cabin", "embarked"]
```

> ~~ヘッダー列名をキーとするマップ群は、ヘッダー以降のデータ群をtlで取り出し、ヘッダーと各データをList.zipでキーワードリスト化し、Enum.intoでマップ化することで作成します~~

```diff
- train_csv_maps =
-   train_csv_raw
-   |> tl
-   |> Enum.map(& List.zip([train_csv_header, &1]) |> Enum.into(%{}))
```

> ~~これで各データをマップとして扱えるので、アクセスしやすくなります~~

:::note info
マップに変更することも必要ないです。データフレームで全部処理を簡単にできます。
:::

## 学習のための最低限の「データ前処理」

> まず大前提として、機械学習では、文字列や空白のような数値データ以外を入力として使うことはできないので、空白無の数値データのみに変換する必要があり、これを「データ前処理」で行います（これは、学習データとラベルの両方に必要です）
> 
> また、以下2つも「データ前処理」で行います
> 
> - データの中には、ラベルと相関性が無い、いわゆる「特徴と言えないデータ」が存在し得るので、その列の削除
> - ID／ラベル／学習データはモデル学習の際に別データとする必要があるため、分離
>
> なお、本講義回では、学習のための最低限の「データ前処理」のみ行い、精度向上のための工夫は次回に預けます

## ①空白値の確認

> まず、IDやラベルも含め、空白値をピックアップし、集計します

```diff
- train_csv_maps
- |> Enum.flat_map(fn map -> Map.filter(map, & elem(&1, 1) == "") |> Map.keys end)
- |> Enum.reject(& &1 == [])
- |> Enum.frequencies
+ train_headers
+ |> Enum.reduce(%{}, fn name, acc -> 
+   count = DF.pull(train_df, name) |> S.is_nil() |> S.sum()
+   if (count > 0), do: Map.put(acc, name, count), else: acc
+ end)
```

:::note info
結果がデータフレームではないと、ちょっと複雑になります。パイプラインでできませんでした。

これは何をすると、それぞれの列(フィールド)に:

- シリーズを取得して
- それぞれの値が`nil`なら`true`、`nil`以外なら`false`の新しいシリーズを作成して
- `true`の合計を数して
- 数が`0`より以上ならマップにその結果を`put`する
:::

> 以下の列に空白値があるようです

```elixir
> %{"Age" => 177, "Cabin" => 687, "Embarked" => 2}
```

> これらのうち、欠損値の補完が必要か、それとも列自体が不要かをこの後、判断します
>
> なお、「データ前処理」は、学習データだけで無く、検証データや未知データに対しても同じ処理を行う必要があるため、count_missingsで関数化しておきます

```diff
  defmodule Pre do
-   def count_missings(datas) do
+   def count_missings(df) do
-     datas
+     DF.names(df)
-     |> Enum.flat_map(fn map -> Map.filter(map, &(elem(&1, 1) == "")) |> Map.keys end)
-     |> Enum.reject(&(&1 == []))
-     |> Enum.frequencies
+     |> 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
  end
end
```

> 下記のように呼び出します

```diff
- train_csv_maps
+ train_df
  |> Pre.count_missings
```

## ②ID／ラベル／学習データを分離

> ID／ラベル／学習データの分離は、それぞれの列をマップから抜き出すseparateという関数で行います
>
> うち、ラベルについては、この段階で行列化しておきますが、以下2つの操作が必要です
>
> Nx.tensorでの行列化できる値は小数だが、ラベルは整数文字列なので、整数の後ろに「.0」を付加し、String.to_floatすることで小数化
> モデルに入力できるよう、「2次元行列のリスト」に変換する必要があるが、ラベル群は単なるリストのため、2次元行> 列で包むために、2重リスト[[～]]で囲んだ上で、Nx.tensorに渡す

```diff
  defmodule Pre do
    …
-   def separate(datas, id, label) do
+   def separate(df, id, label) do
      {
-       datas |> Enum.map(& Map.get(&1, id)),
+       DF.pull(df, id) |> S.to_list(),
-       datas |> Enum.map(& [[String.to_float("#{Map.get(&1, label)}.0")]] |> Nx.tensor),
+       DF.pull(df, label)
+       |> S.cast(:float)
+       |> S.to_list
+       |> Enum.map(& [[&1]] |> Nx.tensor),
-       datas |> Enum.map(& Map.drop(&1, [id, label]))
+       df |> DF.discard([id, label]) |> DF.to_rows
      }
    end
end
```

:::note infi
ラベルの作成することについて、`DF.pull(df, "label") |> S.cast(:float) |> S.to_tensor`でできるはずと思いましたが、この行列のリストになってしまいました:

```elixir
#Nx.Tensor<
  f64[891]
  [0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, ...]
>
```

トレーニングの関数に渡すフォーマットは以下の2次元行列のリストの形が必要です:

```Elixir
 [
   #Nx.Tensor<
     f32[1][1]
     [
       [0.0]
     ]
   >,
   #Nx.Tensor<
     f32[1][1]
     [
       [1.0]
     ]
   >,
   ...
]
```
:::

```diff
  {trains_csv_ids, train_csv_labels, train_csv_maps} =
-   train_csv_all_maps
+   train_df
-   |> Pre.separate(:passengerid, :survived)
+   |> Pre.separate("passengerid", "survived")
```

>ID／ラベル／学習データに分離され、ラベルは「2次元行列のリスト」に変換されたことが確認できます

```elixir
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
  28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, ...],
 [
   #Nx.Tensor<
     f32[1][1]
     [
       [0.0]
     ]
   >,
   #Nx.Tensor<
     f32[1][1]
     [
       ...
     ]
   >,
   ...
 ],
 [
   %{
     "age" => 22.0,
     "cabin" => nil,
     "embarked" => "S",
     "fare" => 7.25,
     "name" => "Braund, Mr. Owen Harris",
     "parch" => 0,
     "pclass" => 3,
     "sex" => "male",
     "sibsp" => 1,
     "ticket" => "A/5 21171"
   },
   ...
]   
```

## ③特徴とならない列の削除

> タイタニック問題で「特徴と言えないデータ」に該当すると思われるのは以下です
>
> - cabin：部屋番号
>   - 部屋番号は、生存率にとても高い相関性を持っているはずですが、Cabinは891件中、687件と大量のデータが欠> 損しているため、使い物にならないと判断し、削除
> - name：乗客名
>   - 乗客名は、全員が異なり、生存率にも無関係と思われる
>   - 名字が同じで、チケット番号が近い or 部屋が近い等であれば、家族乗船の可能性があり、家族全員がボートに乗れるまで待ったとき生存率が低くなる、といった仮説は考えられるが、いったん削除
> - ticket：チケット番号
>   - チケット番号そのものは、生存率に無関係と思われる
>   - 近い番号の方が、生存率の高い／低い部屋番号にまとまって配置されたという可能性は考えられるが、憶測の域を出ないので、いったん削除
>
> これら列の削除をdropという関数で行います

```diff
  defmodule Pre do
    …
-   def drop(datas, keys) do
+   def drop(df, keys) do
-     datas
-     |> Enum.map(& Map.drop(&1, keys))
+     keys
+     |> Enum.reduce(df, & DF.discard(&2, &1))
    end
  end
```

:::note info
piacereさんは`train_csv_map`を作成したあとに"Cabin", "Name", "Ticket"をマップから削除しています。私はその前にデータフレームから削除していますので、`train_csv_dropped_maps`が要らなくなります。
:::

```diff
- train_csv_dropped_maps = train_csv_maps
-   |> Pre.drop([:cabin, :name, :ticket])
+ train_df = train_df
+   |> Pre.drop(~w(cabin name ticket))

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

> cabinなどの列が削除されているのが確認できます

```elixir
 [
   %{
     "Age" => 22.0,
     "Embarked" => "S",
     "Fare" => 7.25,
     "Parch" => 0,
     "Pclass" => 3,
     "Sex" => "male",
     "SibSp" => 1
   },
   ...
]
```

## ④欠損値の補完

> 欠損値のうち、`cabin`は列ごと削除されたので、残る`age`と`emberked`が補完対象で、これを`count_missings`という関数で行います

```diff
  defmodule Pre do
    def count_missings(datas) do
    …
    def empty_replace(datas, replaces_map) do
      replaces_map
-     |> Enum.reduce(datas, fn {key, replace}, acc ->
+     |> Enum.reduce(df, fn {key, replace}, acc ->
-       acc
-       |> Enum.map(& Map.put(&1, key, String.replace(Map.get(&1, key), ~r/^$/, replace)))
+       DF.put(acc, key, DF.pull(acc, key) |> S.fill_missing(replace))
      end)
    end
  end
```

:::note info
`DF.pull`と`DF.put`でやりたくないですが、`DF.mutate`で`S.fill_missing`を使う方法はマクロの中にあるので、`key`をうまく使えませんでした。
:::

> ここではいったん、ageは「0」、emberkedは「S」で補完します（本来、どのような値で補完するかは精度に影響を与えますが、これは次回に）

```diff
- train_csv_replaced_maps = train_csv_dropped_maps
-   |> Pre.empty_replace(%{embarked: "S", age: "0"})
  train_df = train_df
    |> Pre.drop(~w(cabin name ticket))
+   |> Pre.empty_replace(%{embarked: "S", age: 0.0})

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

:::note info
`Pre.empty_replace`の代わりに、以下の方法の方が快適と思います:

```diff
  train_df = train_df
    |> Pre.drop(~w(cabin name ticket))
-   |> Pre.empty_replace(%{embarked: "S", age: 0.0})
+   |> DF.mutate(embarked: fill_missing(embarked, "S"))
+   |> DF.mutate(age: fill_missing(age, 0.0))

  {trains_csv_ids, train_csv_labels, train_csv_maps} =
    train_df
    |> Pre.separate("passengerid", "survived")
```
:::
> 欠損値が無くなったことを確認できました

```diff
    ...
  ],
  [
    %{"age" => 22.0, "embarked" => "S", "fare" => 7.25, "parch" => 0, "pclass" => 3, "sex" => "male", "sibsp" => 1},
    %{"age" => 38.0, "embarked" => "C", "fare" => 71.2833, "parch" => 0, "pclass" => 1, "sex" => "female", "sibsp" => 1},
    %{"age" => 26.0, "embarked" => "S", "fare" => 7.925, "parch" => 0, "pclass" => 3, "sex" => "female", "sibsp" => 0},
    %{"age" => 35.0, "embarked" => "S", "fare" => 53.1, "parch" => 0, "pclass" => 1, "sex" => "female", "sibsp" => 1},
    %{"age" => 35.0, "embarked" => "S", "fare" => 8.05, "parch" => 0, "pclass" => 3, "sex" => "male", "sibsp" => 0},
-   %{"age" => nil, "embarked" => "Q", "fare" => 8.4583, "parch" => 0, "pclass" => 3, "sex" => "male", "sibsp" => 0},
+   %{"age" => 0.0, "embarked" => "Q", "fare" => 8.4583, "parch" => 0, "pclass" => 3, "sex" => "male", "sibsp" => 0},
    %{"age" => 54.0, "embarked" => "S", "fare" => 51.8625, "parch" => 0, "pclass" => 1, "sex" => "male", "sibsp" => 0},
    %{"age" => 2.0, "embarked" => "S", "fare" => 21.075, "parch" => 1, "pclass" => 3, "sex" => "male", "sibsp" => 3},
    ...
    %{"age" => 40.0, "embarked" => "S", "fare" => 9.475, "parch" => 0, "pclass" => 3, "sex" => "female", ...},
    %{"age" => 27.0, "embarked" => "S", "fare" => 21.0, "parch" => 0, "pclass" => 2, ...},
-   %{"age" => nil, "embarked" => "C", "fare" => 7.8958, "parch" => 0, ...},
+   %{"age" => 0.0, "embarked" => "C", "fare" => 7.8958, "parch" => 0, ...},
    %{"age" => 3.0, "embarked" => "C", "fare" => 41.5792, ...},
    %{"age" => 19.0, "embarked" => "Q", ...},
-   %{"age" => nil, ...},
+   %{"age" => 0.0, ...},
    %{...},
    ...
  ]}
```

## ⑤カテゴリ値（種別文字列）を数値に変換

> `sex`は、「male」と「female」の2種類の文字列で男女を分類していますが、このような種別を表す文字列の数値化は、「male」を「0」、「female」を「1」のように、通番の数値に置換することで実現できます
>
> このような種別を表す文字列を「カテゴリ値」と呼びます
>
> また、通番数値はダミーの値であることから「ダミー変数」と呼ばれることもあります（Pythonのpandasでは`get_dummies`という関数でこの置換を行います）
>
> こうした置換自体を「ワンホットエンコーディング」と呼ぶこともあります
>
> さて蘊蓄はこのくらいにして、置換用コードを書いてみましょう
>
> まずは、指定列をカテゴリ値とみなし、ダミー値を振ったマップを生成する`make_dummies`という関数を追加します
>
> 指定されたキーのみを抽出し、ユニーク化した後、`Enum.with_index`でインデックスを振り出し、`Enum.into`でマップ化することで実装します
>
> このとき、`Enum.with_index`で振り出すインデックスは、整数がデフォルトなので、`separete`でラベルに行ったのと同じ方法で小数化します

```diff
  defmodule Pre do
    …
-   def make_dummies(datas, key) do
+   def make_dummies(df, key) do
+     series = DF.pull(df, key)
-     datas
-     |> Enum.map(& Map.get(&1, key))
+     map = S.to_list(series)
        |> Enum.uniq
        |> Enum.with_index(& {&1, String.to_float("#{&2}.0")})
        |> Enum.into(%{})
+     new_series = S.transform(series, & Map.get(map, &1))
+     DF.put(df, key, new_series)
    end
```

:::note info
まだ`DF.pull`と`DF.put`でデータフレームを更新しています。でも、その間にマップを作成する部分がほぼ同じです。
:::

:::note warn
データフレームは`dummies`という関数があります。でも、この`get_dummies`と関係ないです。`DataFrame.dummies(df, "column")`は`column`の列にランダムの`0`か`1`を入れます。
:::

> カテゴリ値であるsexとemberkedのダミー値入りマップを生成してみます

```diff
- Pre.make_dummies(train_csv_replaced_maps, :embarked)
+ Pre.make_dummies(train_df, "embarked")
```

```diff
- Pre.make_dummies(train_csv_replaced_maps, :sex)
+ Pre.make_dummies(train_df, "sex")
```

> ダミー値入りマップが生成可能となりました

```elixir
%{"C" => 1, "Q" => 2, "S" => 0}
```
```elixir
%{"female" => 1, "male" => 0}
```

> 次に、make_dummiesを使って、指定列をダミー値に入れ替えるto_dummiesという関数を追加します

```diff
  defmodule Pre do
    …
-   def to_dummies(datas, train_maps, keys) do
+   def to_dummies(df, keys) do
      keys
      |> Enum.reduce(datas, fn key, acc ->
-       acc
-       |> Enum.map(& Map.put(&1, key, make_dummies(train_maps, key)[Map.get(&1, key)]))
+       map = make_dummies(acc, key)
+       DF.pull(acc, key)
+       |> S.transform(& Map.get(map, &1))
+       |> then(& DF.put(acc, key, &1))
      end)
    end
```

:::note info
私のやり方では引き続いてデータフレームを編集しています。だから、分けた`train_maps`が必要ないですね。直接`keys`の列をダミーマップを作成して、データを更新します。
:::

> カテゴリ値であるsexとemberkedを置換します

```diff
- train_csv_dummied_maps = train_csv_replaced_maps
-   |> Pre.to_dummies(train_csv_replaced_maps, [:embarked, :sex])
  train_df = train_df
    |> 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")
```

> カテゴリ値が数値に置換されました

```elixir
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
  28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, ...],
 [
#Nx.Tensor<
     f32[1][1]
     [
       [0.0]
     ]
   >,
   ...
   #Nx.Tensor<
     f32[1][1]
     [
       ...
     ]
   >,
   ...
 ],
 [
   %{"age" => 22.0, "embarked" => 0, "fare" => 7.25, "parch" => 0, "pclass" => 3, "sex" => 0, "sibsp" => 1},
   %{"age" => 38.0, "embarked" => 1, "fare" => 71.2833, "parch" => 0, "pclass" => 1, "sex" => 1, "sibsp" => 1},
   %{"age" => 26.0, "embarked" => 0, "fare" => 7.925, "parch" => 0, "pclass" => 3, "sex" => 1, "sibsp" => 0},
   %{"age" => 40.0, "embarked" => 0, "fare" => 9.475, "parch" => 0, "pclass" => 3, "sex" => 1, ...},
   %{"age" => 27.0, "embarked" => 0, "fare" => 21.0, "parch" => 0, "pclass" => 2, ...},
   %{"age" => 0.0, "embarked" => 1, "fare" => 7.8958, "parch" => 0, ...},
   %{"age" => 3.0, "embarked" => 1, "fare" => 41.5792, ...},
   %{"age" => 19.0, "embarked" => 2, ...},
   %{"age" => 0.0, ...},
   %{...},
   ...
 ]}
```

## ~~⑥整数を小数に変換~~

> ~~数値文字列から数値への変換を行いますが、ここで、整数と小数が混在する列は、文字列から数値への変換を`String.to_integer`と`String.to_float`を使い分けしなければならなくて面倒なため、整数文字列を全て小数文字列に変換した上で、小数に変換します~~
>
> ~~まずは、整数値は小数表記の文字列に変換した上で、全て小数に置換するinteger_string_to_floatという関数を追加します~~
>
> ~~`integer_string_to_float`では、指定された全キーに対し、`Enum.reduce`を使って、小数化を行っていきます~~
>
> ~~整数文字列の小数文字列化は、小数点である「.」が存在しない値を整数とみなし、~~String.replace~~の正規表現置換によって後ろに「.0」を付加します~~

:::note info
これがデータフレームでは必要ないです。Axonも`:integer`, `:boolean`, `:float`などの数字がそのままで使えます。結果だけが小数点の形が必要らしいです。
:::

## ⑦数値を行列に変換

>最後に、数値をモデルに入力できるよう、行列に変換するためにmap_to_tensorという関数を追加します
>
> ここで、「2次元行列のリスト」になるよう、Map.valuesで出力されるリストを、更にリスト[～]で囲んだ上で、
>
> Nx.tensorにて行列化します

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

:::note info
そのままで使えます。
:::

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

- train_csv_datas = train_csv_nimeric_maps
+ train_csv_datas = train_csv_maps
    |> Pre.map_to_tensor
```

:::note info
データフレームを最後までに変更して、`separate`しましたので、`train_csv_numeric_maps`がないですね。でも、マップのままで作成したから、関数を変わらずに同じ結果になります。
:::

> 学習データが「2次元行列のリスト」に変換されました

```elixir
[
  #Nx.Tensor<
    f32[1][7]
    [
      [22.0, 0.0, 7.25, 0.0, 3.0, 0.0, 1.0]
    ]
  >,
  #Nx.Tensor<
    f32[1][7]
    [
      [38.0, 1.0, 71.2833023071289, 0.0, 1.0, 1.0, 1.0]
    ]
  >,
  ...
]
```

## ~~⑧「データ前処理」全体の関数化~~

> ~~ここまでの「データ前処理」をprocessという関数で1発で完了するようにします~~
>
> ~~ここで、datasとtrain_datasの2つの引数を取っている理由ですが、これはto_dummiesの実施が、検証データや未知データではカテゴリ値が網羅されていないケースへの対策として、学習データからカテゴリ値を拾うためです~~

:::note info
この処理は以下のことを`Pre`モジュールでやることらしいです。でも、全部は`separate`したの後の処理ですので、順番が違います。このままで処理を続きます。

```elixir
train_df = train_df
  |> 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
```
:::

> 更に、CSVファイルロード／マップ群変換／分離（ID／ラベル／学習データ）を処理する処理を、csv_file_to_datasとして関数化しておきます

:::note info
ここで哲学的な意見の相違が生じているようです。 問題固有の関数が Pre のような汎用モジュールに属しているとは思えません。

しかし、データフレームの読み取りとその処理を統合したいので、その部分をここに移動しましょう。

```diff
- train_df = train_df
+ 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
```

データフレームを読んで、リデュースして、処理する。これがメインプログラムですので、モジュールに隠れたくないです。
:::