Elixir でデータ分析 Explorer を使ってみた

## はじめに

最近の Elixir の発展は目覚ましいですね

Elixir で高速データ探索をするためのパッケージとして、
Explorer （冒険家） がリリースされたようです

https://github.com/elixir-nx/explorer

早速 Livebook で使ってみました

https://livebook.dev/

実行したコードはこちら

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

:::note info
2023/01/30 更新

最新版に更新しました
:::

## 実行環境

- Elixir: 1.14.2 OTP 24
- Livebook: 0.8.1

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

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

:::note info

Docker が使える環境であれば簡単に実行できます

https://docs.docker.com/engine/install/

Docker Desktop を無償利用できない場合は Rancher Desktop を使ってください

https://rancherdesktop.io/

:::

## セットアップ

Explorer と、テーブル表示をキレイにするための Kino をインストールします

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

https://github.com/livebook-dev/kino

## エイリアス

まず、今後使うときに便利なように、 alias を付けておきます

また、Query が使えるように require もしておきます

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

Query についてはこちらを参照してください

https://qiita.com/RyoWakabayashi/items/053296b36d8744aca9ca

## 基本操作

### データフレーム作成

マップからデータフレームを作成します

```elixir
sample_df =
  %{
    "labels" => ["a", "b", "c", "c", "c"],
    "values" => [1, 2, 3, 2, 1]
  }
  |> DataFrame.new()
```

実行結果は以下のようになります

```elixir
#Explorer.DataFrame<
  Polars[5 x 2]
  labels string ["a", "b", "c", "c", "c"]
  values integer [1, 2, 3, 2, 1]
>
```

`Polars` は Explorer のバックエンドで、Rust 製の超高速データフレームライブラリです

https://www.pola.rs/

labels　列と values 列に各値が格納され、 5 x 2 のデータフレームになっています

### データフレームのテーブル表示

`Kino.DataTable.new` を使うことでテーブル表示ができます

```elixir
Kino.DataTable.new(sample_df)
```

![data_table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/47f8a476-c834-b7a6-73c0-c1002e5acc9b.png)

### データフレームの基本統計量表示

`DataFrame.describe` で以下のような基本統計量が取得できます

- count: 件数
- mean: 平均値
- std: [標準偏差](https://bellcurve.jp/statistics/glossary/1193.html)
- min: 最小値
- 25%: 25[パーセンタイル](https://bellcurve.jp/statistics/glossary/1388.html)
- 50%: 中央値（50パーセンタイル）
- 75%: 75パーセンタイル
- max:　最大値

```elixir
sample_df
|> DataFrame.describe()
|> Kino.DataTable.new()
```

![describe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8fcfd35a-275f-084a-9f5d-20da9862f880.png)

### 四則演算

Query によって記号による四則演算が可能です

```elixir
sample_df
|> DataFrame.mutate(
  add: values + 2,
  subtract: values - 2,
  multiply: values * 2,
  divide: values / 2,
  pow: values ** 2
)
|> Kino.DataTable.new()
```

![math.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bd39ee1a-501a-e348-fca7-d7180d006947.png)

商（整数での割り算の結果）と余りは個別の関数を使います

- quotient: 商
- remainder: 余り

```elixir
sample_df
|> DataFrame.mutate(
  quotient: quotient(values, 2),
  remainder: remainder(values, 2)
)
|> Kino.DataTable.new()
```

![mod.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b3a7520f-bd1e-db0f-2dde-569a73840592.png)

### 比較

比較演算子も使用できます

```elixir
sample_df
|> DataFrame.mutate(
  equal: values == 2,
  not_equal: values != 2,
  greater: values > 2,
  greater_equal: values >= 2,
  less: values < 2,
  less_equal: values <= 2
)
|> Kino.DataTable.new()
```

![greater.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8a48d654-e28d-ffbb-4efd-d8624b54a57b.png)

### 論理演算

`not` `or` `and` で条件を組み合わせることもできます

```elixir
sample_df
|> DataFrame.mutate(
  "not equal": Series.not(values == 2),
  "greater or equal": Series.or(values > 2, values == 2),
  "greater and equal": Series.and(values > 2, values == 2)
)
|> Kino.DataTable.new()
```

![logic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7544e7a5-aab2-239b-a512-5dd480ee7e5c.png)

`Series.all_equal` で列が完全に一致しているかを判定できます

```elixir
Series.all_equal(sample_df["labels"], sample_df["labels"])
```

```elixir
Series.all_equal(sample_df["labels"], sample_df["values"])
```

### フィルター

`DataFrame.filter` で条件に合致する行だけを取得できます

```elixir
threshold = 1

sample_df
|> DataFrame.filter(values > ^threshold)
|> Kino.DataTable.new()
```

![filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/314c78bf-a714-dc4d-2971-f4ae620fb323.png)

### 欠損値

nil になっているデータを埋めることができます

まず、 nil を含むデータフレームを用意します

```elixir
nil_df =
  %{
    "labels" => ["a", nil, "c", "c", "c"],
    "values" => [1, 2, 3, nil, 1]
  }
  |> DataFrame.new()

Kino.DataTable.new(nil_df)
```

![nilable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ff4db92a-a0d5-5aad-b107-a289878f3f43.png)

`is_nil` や `is_not_nil` で欠損値の判定ができます

```elixir
nil_df
|> DataFrame.mutate(
  is_nil: is_nil(values),
  is_not_nil: is_not_nil(values)
)
|> Kino.DataTable.new()
```

![is_nil.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/53decbd6-c15f-97c2-c495-f4f913ba63e7.png)

欠損値を補完する値を準備します

```elixir
fill_label =
  ""
  |> List.duplicate(5)
  |> Series.from_list()
```

```elixir
fill_value =
  0
  |> List.duplicate(5)
  |> Series.from_list()
```

`coalesce` で nil の場合だけ補完します

```elixir
nil_df
|> DataFrame.mutate(
  labels_filled: coalesce(labels, ^fill_label),
  values_filled: coalesce(values, ^fill_value)
)
|> Kino.DataTable.new()
```

![coalesce.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a4e97c2b-df18-f8ea-ef5a-a1ea849a7bc7.png)

### 集計

`group_by` でグループ化し、 `summarise` で集計します

```elixir
sample_df
|> DataFrame.group_by("labels")
|> DataFrame.summarise(
  count: count(values),
  n_distinct: n_distinct(values),
  min: min(values),
  max: max(values),
  sum: sum(values),
  mean: mean(values),
  median: median(values),
  "quantile 1/4": quantile(values, 0.25),
  "quantile 3/4": quantile(values, 0.75),
  variance: variance(values),
  standard_deviation: standard_deviation(values)
)
|> Kino.DataTable.new()
```

![group_by.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7e14f129-0ba6-76d4-d0a6-441e0ea27043.png)

`Series.frequencies` で値毎の件数を取得します

```elixir
sample_df["values"]
|> Series.frequencies()
|> Kino.DataTable.new()
```

![frequencies.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f087c112-bdc5-88f1-a406-eff451c7757e.png)

### 並べ替え

`DataFrame.arrange` で並べ替えます

`asc: <列名>` で昇順、 `desc: <列名>` で降順に並べます

```elixir
sample_df
|> DataFrame.arrange(asc: values)
|> Kino.DataTable.new()
```

![arrange.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6be05123-0711-39d8-1e91-7e09150fdd4e.png)

## 実際のデータを使った分析

### データの準備

適当な統計データということで、統計局のホームページから
都道府県別、性別、年齢層別の人口データを Excel 形式でダウンロードして、
CSV に出力しました

https://www.stat.go.jp/index.html

こんな感じのデータです

![スクリーンショット 2022-06-23 17.09.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b325c329-8668-76df-7684-260f723fbb09.png)

私のリポジトリーから取得できます

https://raw.githubusercontent.com/RyoWakabayashi/elixir-learning/main/livebooks/explorer/population_20211001.csv

https://raw.githubusercontent.com/RyoWakabayashi/elixir-learning/main/livebooks/explorer/assets_20151216.csv

### データの読込

CSV からデータを読みます

列名の `人口（千人）` は扱いづらいので `人口_千人` にリネームします

```elixir
population_df =
  "/home/livebook/explorer/population_20211001.csv"
  |> DataFrame.from_csv!()
  |> DataFrame.rename("人口（千人）": "人口_千人")
```

実行結果は以下のようになります

```elixir
#Explorer.DataFrame<
  Polars[282 x 4]
  都道府県 string ["北海道", "青森県", "岩手県", "宮城県", "秋田県", ...]
  年齢層 string ["15歳未満", "15歳未満", "15歳未満", "15歳未満", "15歳未満", ...]
  性別 string ["男性", "男性", "男性", "男性", "男性", ...]
  人口_千人 string ["278", "65", "66", "135", "46", ...]
>
```

`人口_千人` は数字のデータですが、文字列として読み込まれています

これは CSV ファイルの 人口（千人） 列に、一部 `"` ダブルクォーテーションで囲っている値が存在するためです

表形式で表示してみます

```elixir
DataFrame.table(population_df)
```

![スクリーンショット 2022-06-23 17.24.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/db19bfa7-5cb2-4906-502e-37128888986c.png)

ちなみに、 `limit: :infinity` を指定することで全件表示できます

指定しない場合は先頭の5件だけ表示されます

```elixir
DataFrame.table(population_df, limit: :infinity)
```

データテーブルで表示すると以下のようになります

```elixir
Kino.DataTable.new(population_df)
```

![population_table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/aac87d7d-18da-521d-4fc2-30a42fc57bd9.png)

### 重複排除

年齢層の種類を取得します

```elixir
population_df
|> DataFrame.distinct(["年齢層"])
|> DataFrame.pull("年齢層")
|> Series.to_list()
```

実行結果は以下のようになります

```elixir
["15歳未満", "15～64歳", "65歳以上"]
```

### データ抽出

以下のようにしてデータ抽出できます

`filter` で条件を指定して選択し、
`select` で項目を指定して射影します

```elixir
population_df
|> DataFrame.filter(都道府県 == "東京都")
|> DataFrame.select(["年齢層", "性別", "人口_千人"])
|> Kino.DataTable.new()
```

![population_filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/86937c9a-a58a-5a5c-b011-d89b4027356a.png)

### データ編集

`mutate` で色々データを編集できます

CSV を読み込んだとき、 `人口（千人）` の列が何故か文字列になっていたので、
float に変換してみます

`cast` で指定した列の型を変換します

```elixir
population_df
|> DataFrame.mutate(人口_千人: cast(人口_千人, :float))
|> DataFrame.filter(都道府県 == "東京都")
|> DataFrame.select(["年齢層", "性別", "人口_千人"])
|> Kino.DataTable.new()
```

![population_cast.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ae1c917c-1058-a362-ba04-8b27d3bfa919.png)

うまく変換できなかったところが空になってしまいました

どういうわけかというと、 Excel から CSV 出力するとき、
人口の数字が `1,２３４` というような桁区切りのまま出力されており、
それをうまくパースできなかったようです

![スクリーンショット 2022-06-23 18.25.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/630ae8a0-d7c8-3cf1-a526-a4625b3ea4d2.png)

カンマを取り除いてから変換しましょう

`Series.transform` で指定した列に関数を適用します

```elixir
trimmed =
  population_df["人口_千人"]
  |> Series.transform(fn input ->
    String.replace(input, ",", "")
  end)
  |> Series.cast(:float)
```

実行結果は以下のようになり、 `1463.0` などの桁区切りされていた値も数値に変換できています

```elixir
#Explorer.Series<
  Polars[282]
  float [278.0, 65.0, 66.0, 135.0, 46.0, 60.0, 103.0, 168.0, 115.0, 113.0, 440.0, 373.0, 795.0,
   549.0, 124.0, 58.0, 69.0, 48.0, 46.0, 123.0, 121.0, 220.0, 495.0, 107.0, 97.0, 148.0, 521.0,
   336.0, 77.0, 53.0, 35.0, 42.0, 118.0, 178.0, 77.0, 39.0, 58.0, 77.0, 38.0, 338.0, 55.0, 83.0,
   116.0, 68.0, 71.0, 105.0, 124.0, 1463.0, 338.0, 337.0, ...]
>
```

`DataFrame.put` で変換した値で列を上書きます

```elixir
population_df = DataFrame.put(population_df, "人口_千人", trimmed)
```

```elixir
population_df
|> DataFrame.filter(都道府県 == "東京都")
|> DataFrame.select(["年齢層", "性別", "人口_千人"])
|> Kino.DataTable.new()
```

うまく変換できました

![population_mutated.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/afa14932-2aa4-2dba-aeea-ae568d4ef302.png)

`人口_千人` と書くのが面倒になってきたので、 1,000 倍して `人口` にしておきましょう

```elixir
population_df =
  population_df
  |> DataFrame.mutate(人口_千人: 人口_千人 * 1000)
  |> DataFrame.mutate(人口_千人: cast(人口_千人, :integer))
  |> DataFrame.rename(人口_千人: "人口")

population_df
|> DataFrame.filter(都道府県 == "東京都")
|> DataFrame.select(["年齢層", "性別", "人口"])
|> Kino.DataTable.new()
```

![population_thousand.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9409f33c-d3a9-f871-b3bf-c80c73f9479e.png)

### ソート

`arrange` でソートできます

```elixir
population_df
|> DataFrame.filter(年齢層 == "15歳未満")
|> DataFrame.filter(性別 == "男性")
|> DataFrame.filter(人口 > 300_000)
|> DataFrame.select(["都道府県", "人口"])
|> DataFrame.arrange(desc: 人口)
|> Kino.DataTable.new()
```

![population_arrange.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8512cff0-2e31-01ed-8ac3-bbb3bfe4e5f6.png)

### ピボット

`pivot_wider` や `pivot_longer` で集計の軸を変更できます

年齢層の種類を列にしてみます

```elixir
population_df
|> DataFrame.pivot_wider("年齢層", "人口")
|> Kino.DataTable.new()
```

![population_pivot_wider.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a66a5d39-55f1-2adf-2acc-1a169a0c8c44.png)

### 集約

`groupby` で集約し、 `summarise` で集計値を取得できます

合計すると値が大きすぎて指数表示になるため、改めて千人単位に変換しています

```elixir
sum_df =
  population_df
  |> DataFrame.group_by(["都道府県"])
  |> DataFrame.summarise(人口_千人: sum(人口 / 1_000))
  |> DataFrame.mutate(人口_千人: cast(人口_千人, :float))

sum_df
|> DataFrame.arrange(desc: 人口_千人)
|> Kino.DataTable.new()
```

![population_sum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f7274e1f-8465-94bc-e465-2d76a22e1df3.png)

都道府県毎の男性率と女性率を出してみます

```elixir
sex_ratio_df =
  population_df
  |> DataFrame.group_by(["都道府県", "性別"])
  |> DataFrame.summarise(人口: sum(人口))
  |> DataFrame.mutate(人口: cast(人口, :float))
  |> DataFrame.pivot_wider("性別", "人口")
  |> DataFrame.mutate(合計: 男性 + 女性)
  |> DataFrame.mutate(男性率: 男性 / 合計)
  |> DataFrame.mutate(女性率: 女性 / 合計)
  |> DataFrame.select(["都道府県", "男性率", "女性率"])

sex_ratio_df
|> DataFrame.arrange(desc: 男性率)
|> Kino.DataTable.new()
```

![population_sex_ratio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6da1c791-75cd-e1cb-20cf-39cc881c9300.png)

トップの県でも女性の方が多いようです

続いて都道府県毎の高齢者率を出してみます

```elixir
elderly_rate_df =
  population_df
  |> DataFrame.group_by(["都道府県", "年齢層"])
  |> DataFrame.summarise(人口: sum(人口))
  |> DataFrame.mutate(人口: cast(人口, :float))
  |> DataFrame.pivot_wider("年齢層", "人口")
  |> DataFrame.rename("15歳未満": "young")
  |> DataFrame.rename("15～64歳": "middle")
  |> DataFrame.rename("65歳以上": "elder")
  |> DataFrame.mutate(合計: young + middle + elder)
  |> DataFrame.mutate(高齢者率: elder / 合計)
  |> DataFrame.select(["都道府県", "高齢者率"])

elderly_rate_df
|> DataFrame.arrange(desc: 高齢者率)
|> Kino.DataTable.new(sorting_enabled: true)
```

![population_elderly_ratio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/39c24102-4652-40d5-783e-f2a907b18840.png)

### 結合

別の統計データを読み込みます

```elixir
assets_df = DataFrame.from_csv!("/home/livebook/explorer/assets_20151216.csv")

Kino.DataTable.new(assets_df)
```

![assets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/80ccb372-d731-fb6f-f9e1-250afa81b625.png)

こちらもカンマが邪魔だったので、排除してから float にします

```elixir
parse_float = fn df, col ->
  df
  |> DataFrame.put(
    col,
    df[col]
    |> Series.transform(fn input ->
      String.replace(input, ",", "")
    end)
    |> Series.cast(:float)
  )
end

assets_df =
  assets_df
  |> parse_float.("年間収入")
  |> parse_float.("貯蓄現在高")
  |> parse_float.("負債現在高")

Kino.DataTable.new(assets_df)
```

![assets_mutated.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/49bcfd97-adf2-4df9-f874-bb0e17d5a1fe.png)

`join` で他のデータフレームと結合します

`how: :left` で左外部結合を指定しています

```elixir
joined_df =
  sum_df
  |> DataFrame.join(sex_ratio_df, how: :left)
  |> DataFrame.join(elderly_rate_df, how: :left)
  |> DataFrame.join(assets_df, how: :left)

Kino.DataTable.new(joined_df)
```

![joined.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7b380b67-da32-efc6-0c1a-eaf78b56a9ae.png)

すっごく簡単ですね

### データの保存

`to_csv` で CSV 形式で保存できます

```elixir
DataFrame.to_csv(joined_df, "/tmp/joined.csv")
```

### 相関係数の取得

ここからはゴリ押しです

せっかく統計データを取ってきたので、各数値の共分散や相関係数を見てみましょう

pandas だと集計値は簡単に取得できます

```python
joined_df = pd.read_csv("tmp/joined.csv")

# 共分散
joined_df.cov()

# 相関係数
joined_df.corr()
```

しかし、残念ながら Explorer には、まだこのような機能がないようです
※平均や標準偏差なら簡単に取得できます

せっかくなので、やってみます

まず、統計を取りたい列を定義します

"都道府県" 以外の列です

```elixir
cols = 
  joined_df
  |> DataFrame.names()
  |> Enum.filter(& &1 != "都道府県")
```

結果は以下のようになります

```elixir
["人口_千人", "男性率", "女性率", "高齢者率", "世帯人員", "持ち家率",
 "年間収入", "貯蓄現在高", "負債現在高"]
```

標準偏差を map に持っておきます

```elixir
std_map =
  joined_df
  |> DataFrame.select(cols)
  |> DataFrame.to_series()
  |> Enum.map(fn {key, value} -> {key, Series.standard_deviation(value)} end)
  |> Enum.into(%{})
```

各標準偏差は以下の値になります

```elixir
%{
  "世帯人員" => 0.15789960393037436,
  "人口_千人" => 2791.9761139238913,
  "女性率" => 0.009130530023194104,
  "年間収入" => 548.6312098671866,
  "持ち家率" => 6.866234804354504,
  "男性率" => 0.009130530023194095,
  "負債現在高" => 816.1966623764004,
  "貯蓄現在高" => 2867.74243208822,
  "高齢者率" => 0.03187703554831672
}
```

x,y の共分散は以下の式で表すことができます

x*yの平均 - xの平均 * yの平均

https://hiraocafe.com/note/kyoubunsan.html

というわけで、まずは各列を掛け合わせた値をデータフレームに追加します

```elixir
get_mul = fn df, col_1, col_2 ->
  DataFrame.put(
    df,
    "#{col_1}*#{col_2}",
    Series.multiply(df[col_1], df[col_2])
  )
end

covariance_df =
  cols
  |> Enum.reduce(joined_df, fn col_1, sub_df_1 ->
    cols
    |> Enum.reduce(sub_df_1, fn col_2, sub_df_2 ->
      get_mul.(sub_df_2, col_1, col_2)
    end)
  end)
```

![covariance.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/45c2152d-c33c-c332-a0bf-e8404298371f.png)

その上で各列の平均を map に取得しておきます

```elixir
select_cols =
  covariance_df
  |> DataFrame.names()
  |> Enum.filter(&(&1 != "都道府県"))

mean_map =
  covariance_df
  |> DataFrame.select(select_cols)
  |> DataFrame.to_series()
  |> Enum.map(fn {key, value} -> {key, Series.mean(value)} end)
  |> Enum.into(%{})
```

実行結果は以下のようになります

```elixir
%{
  "高齢者率" => 0.31102745326423686,
  "男性率*女性率" => 0.24963106680631075,
  "貯蓄現在高*高齢者率" => 4184.178164295221,
  "男性率*人口_千人" => 1298.276595744681,
  "貯蓄現在高*世帯人員" => 33409.922340425524,
  "高齢者率*高齢者率" => 0.09773260196459105,
  "貯蓄現在高*負債現在高" => 48792312.5106383,
  "高齢者率*人口_千人" => 770.468085106383,
  ...
}
```

そして、先程の共分散の計算式を統計対象列のすべての組み合わせに対して適用します

```elixir
get_covariance = fn col_1, col_2 ->
  Map.get(mean_map, col_1 <> "*" <> col_2) - Map.get(mean_map, col_1) * Map.get(mean_map, col_2)
end

covariance_map =
  cols
  |> Enum.map(fn col_1 ->
    cols
    |> Enum.map(fn col_2 ->
      {col_1 <> "*" <> col_2, get_covariance.(col_1, col_2)}
    end)
    |> Enum.into(%{})
  end)
  |> Enum.reduce(fn map, merged_map ->
    Map.merge(merged_map, map)
  end)
```

共分散は以下のようになります

```elixir
%{
  "男性率*女性率" => -8.159282151495861e-5,
  "貯蓄現在高*高齢者率" => -18.636547583582797,
  "男性率*人口_千人" => 8.371636326161024,
  "貯蓄現在高*世帯人員" => 82.47629244001291,
  "高齢者率*高齢者率" => 9.945252805540067e-4,
  "貯蓄現在高*負債現在高" => 978410.078768678,
  "高齢者率*人口_千人" => -60.081096795347094,
  "世帯人員*男性率" => 4.6065873934009183e-4,
  "世帯人員*高齢者率" => 6.398412987442814e-4,
  "世帯人員*年間収入" => 45.66631960162704,
  "持ち家率*持ち家率" => 46.14209144409324,
  ...
}
```

相関係数は xとyの共分散 / (xの標準偏差*yの標準偏差) なので、
これをさらに計算して map に格納します

```elixir
get_correlation = fn col_1, col_2 ->
  cond do
    col_1 == col_2 ->
      1

    true ->
      Map.get(covariance_map, col_1 <> "*" <> col_2) /
        (Map.get(std_map, col_1) * Map.get(std_map, col_2))
  end
end

correlation_map =
  cols
  |> Enum.map(fn col_1 ->
    cols
    |> Enum.map(fn col_2 ->
      {col_1 <> "*" <> col_2, get_correlation.(col_1, col_2)}
    end)
    |> Enum.into(%{})
  end)
  |> Enum.reduce(fn map, merged_map ->
    Map.merge(merged_map, map)
  end)
```

相関係数は以下のようになります

```elixir
%{
  "男性率*女性率" => -0.9787234042549128,
  "貯蓄現在高*高齢者率" => -0.2038672165452424,
  "男性率*人口_千人" => 0.32839963554761675,
  "貯蓄現在高*世帯人員" => 0.1821410965621719,
  "高齢者率*高齢者率" => 1,
  "貯蓄現在高*負債現在高" => 0.4180093161154209,
  "高齢者率*人口_千人" => -0.675069096214225,
  "世帯人員*男性率" => 0.31952311779563053,
  "世帯人員*高齢者率" => 0.12711982927356386,
  "世帯人員*年間収入" => 0.527150304322526,
  "持ち家率*持ち家率" => 1,
  ...
}
```

最後に相関係数をデータフレームに流し込んで表形式で表示します

```elixir
cols
|> Enum.map(fn col_1 ->
  %{
    col_1 =>
      cols
      |> Enum.map(fn col_2 ->
        Map.get(correlation_map, col_1 <> "*" <> col_2)
      end)
  }
end)
|> Enum.reduce(fn map, merged_map ->
  Map.merge(merged_map, map)
end)
|> Map.merge(%{"x" => cols})
|> DataFrame.new()
|> DataFrame.select(["x" | cols])
|> Kino.DataTable.new(keys: ["x" | cols], sorting_enabled: true)
```

![correlation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8e06183d-d815-8d5a-03a8-5ec790f567c0.png)

それっぽく出来上がりました

実は pandas で実行した結果と微妙に違うので、どこか失敗していそうですが:sweat_smile:

男性率と女性率には、当然強い負の相関が出ています

高齢者率と女性率の相関があるのは、おそらく女性の方が長生きだからでしょうね

世帯人員が高いほど、持ち家率も年間収入も高くなるようです

|    x     |          人口_千人          |         男性率         |         女性率          |        高齢者率         |         世帯人員         |         持ち家率         |         年間収入         |        貯蓄現在高        | 負債現在高 |
| :--: | --: | --: | --: | --: | --: | --: | --: | --: | --: |
| 人口_千人 | 1.0 | 0.32839963554761675 | -0.3283996355476253 | -0.675069096214225 | -0.38592888760786614 | -0.3201635451800164 | 0.2903848632180311 | 0.34074103001227296 | 0.6825283462577149 |
| 男性率 | 0.32839963554761675 | 1.0 | -0.9787234042549128 | -0.5954624427808082 | 0.31952311779563053 | 0.09492910399813101 | 0.6260409214159116 | 0.34008522695212134 | 0.6050084839753493 |
| 女性率 | -0.3283996355476253 | -0.9787234042549128 | 1.0 | 0.5954624427808075 | -0.3195231177956302 | -0.09492910399790425 | -0.6260409214158202 | -0.340085226952121 | -0.6050084839752573 |
| 高齢者率 | -0.675069096214225 | -0.5954624427808082 | 0.5954624427808075 | 1.0 | 0.12711982927356386 | 0.39852608818879076 | -0.27590246386820655 | -0.2038672165452424 | -0.6840201298233702 |
| 世帯人員 | -0.38592888760786614 | 0.31952311779563053 | -0.3195231177956302 | 0.12711982927356386 | 1.0 | 0.5669163291406313 | 0.527150304322526 | 0.1821410965621719 | 0.1016151699191319 |
| 持ち家率 | -0.3201635451800164 | 0.09492910399813101 | -0.09492910399790425 | 0.39852608818879076 | 0.5669163291406313 | 1.0 | 0.5819064066105291 | 0.5816831569245144 | 0.04600816107969441 |
| 年間収入 | 0.2903848632180311 | 0.6260409214159116 | -0.6260409214158202 | -0.27590246386820655 | 0.527150304322526 | 0.5819064066105291 | 1.0 | 0.7148869412009056 | 0.6141653791913659 |
| 貯蓄現在高 | 0.34074103001227296 | 0.34008522695212134 | -0.340085226952121 | -0.2038672165452424 | 0.1821410965621719 | 0.5816831569245144 | 0.7148869412009056 | 1.0 | 0.4180093161154209 |
| 負債現在高 | 0.6825283462577149 | 0.6050084839753493 | -0.6050084839752573 | -0.6840201298233702 | 0.1016151699191319 | 0.04600816107969441 | 0.6141653791913659 | 0.4180093161154209 | 1.0 |

もっとスマートで正確なやり方があると思いますが、
とりあえず Explorer でデータ分析することができました！

## おわりに

pandas と同じ感覚でデータを扱えるのは非常に便利です！

今後、もっと便利な機能が追加されることを期待しています