Elixir Explorer で欠損値を補完する

## はじめに

Explorer でデータ分析したい！

欠損値（nil）があるから邪魔で分析できない！

0 で埋めてしまいたい！

という人向けの記事です

Livebook で実装した全文はこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/explorer/fill_missing.livemd

## 動作環境

以下のリポジトリーのコンテナ上で Livebook を動かしています

https://github.com/RyoWakabayashi/elixir-learning

## セットアップ

Livebook でデータフレームをデータテーブルとして表示するため、 Kino をインストールします

```elixir
Mix.install([
  {:explorer, "~> 0.3"},
  {:kino, "~> 0.7"}
])
```

エイリアスをつけます

```elixir
alias Explorer.DataFrame
alias Explorer.Series
```

## データの準備

データを用意します

```elixir
df =
  %{
    x: [nil, 2.0, 3.0, nil, 5.0, 6.0, 7.0],
    y: [0.9, 1.8, 3.3, 3.7, nil, nil, nil]
  }
  |> DataFrame.new()

df
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.04.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f90a2cb1-4c17-4e5a-a7a5-e3f7c3b7a330.png)

## 固定値で補完

`Series.fill_missing` で欠損値を補完します

```elixir
filled_df = 
  df
  # データの全列に対して補完
  |> DataFrame.to_series()
  |> Enum.reduce(df, fn {col, _}, df ->
    # 対象列について、 nil の場合は 0.0 にする
    DataFrame.mutate_with(df, &%{col => Series.fill_missing(&1[col], 0.0)})
  end)

filled_df
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.04.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/57c721bd-51b5-f3a0-127e-28a822727d06.png)

## 集計値で補完

固定値ではなく、最大値、最小値、平均値などの集計値を使って補完します

補完用の関数を定義します

```elixir
fill_df = fn target_df, value -> 
  target_df
  |> DataFrame.to_series()
  |> Enum.reduce(target_df, fn {col, _}, merged_df ->
    DataFrame.mutate_with(merged_df, &%{col => Series.fill_missing(&1[col], value)})
  end)
end
```

### 前の値で補完

`:forward` を指定すると、 nil になっている行の前の行の値で補完します

先頭行が nil の場合は補完されません

```elixir
df
|> fill_df.(:forward)
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.07.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5cb7c9d8-48ea-d997-a14f-0134f27dc4e0.png)

###  次の値で補完

`:backward` を指定すると、 nil になっている行の次の行の値で補完します

最終行が nil の場合は補完されません

```elixir
df
|> fill_df.(:backward)
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.09.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5fe5e4c8-f577-90e6-e313-860a606ec18a.png)

### 最大値で補完

`:max` を指定すると、最大値で補完します

```elixir
df
|> fill_df.(:max)
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.11.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d1de43a0-b760-8a87-2633-2c5e2982c81d.png)

### 最小値で補完

`:min` を指定すると、最小値で補完します

```elixir
df
|> fill_df.(:min)
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.11.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dab9603f-69a9-723d-3bac-dfcbd8a5fbc0.png)

### 平均値で補完

`:mean` を指定すると、平均値で補完します

```elixir
df
|> fill_df.(:mean)
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.21.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9b629dfd-9ccc-5e76-db0f-cb1ec4e5936a.png)

## 様々な型で補完

Series には以下の型が存在します

- float
- integer
- boolean
- string
- date
- datetime

それぞれの型の Series を持つデータフレームを生成します

```elixir
df =
  %{
    float: [0.9, 1.8, 3.3, 3.7, nil, nil, 7.2],
    int: [1, 2, 3, nil, 5, 6, 7],
    bool: [nil, true, false, nil, true, true, false],
    str: ["a", nil, "c", "d", nil, "e", nil],
    date: [~D[2022-01-01], ~D[2000-01-03], nil, nil, nil, nil, ~D[2000-02-01]],
    datetime: [nil, ~N[2022-01-01 00:01:00], ~N[2022-01-01 00:02:00], nil, nil, ~N[2022-02-01 00:02:00], nil],
  }
  |> DataFrame.new()

df
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.24.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ca9f46c1-9bb1-1b25-8dd0-0e195618933c.png)

各列の型によって補完する値を切り替えます

```elixir
filled_df = 
  df
  |> DataFrame.to_series()
  |> Enum.reduce(df, fn {col, series}, df ->
    DataFrame.mutate_with(df, fn lazy ->
      fill_value =
        case Series.dtype(series) do
          :float ->
            0.0
          :integer ->
            0
          :string ->
            ""
          :date ->
            ~D[2022-01-01]
          :datetime ->
            ~N[2022-01-01 00:01:00]
          :boolean ->
            :min
        end

      %{col => Series.fill_missing(lazy[col], fill_value)}
    end)
  end)

filled_df
|> Kino.DataTable.new()
```

![スクリーンショット 2022-11-22 17.27.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ba357b4e-45d3-95bf-d3aa-dfc39e791259.png)

![スクリーンショット 2022-11-22 17.27.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/467de809-0716-c826-5171-fdbe2ee0516a.png)

`:boolean` の場合、 `true` や `false` だと受け付けられないため、 `:min` を指定しています

## まとめ

色々な値で補完できることが確認できました