VegaLite の基礎 (Elixir, Livebook)

**Livebook** を使ってグラフを描きたいときは **VegaLite** を使います。**VegaLite** は **Vega-Lite** の Elixirラッパーとなっており、これが結構高機能です。**Vega-Lite** ではプログラムコードを書くことなしに、**JSON specification** を書くだけで高度なグラフの描画が可能であり、ある意味新鮮です。本記事の目的は **Vega-Lite** で使われる基本用語や基本概念の整理を行うことです。そのため公式ドキュメントにあるガイドに従って、**Livebook** で確認していきたいと思います。本記事で、少しでもドキュメントが読みやすくなれば良いなと思います。
[**Introduction to Vega-Lite 公式ドキュメント**](https://vega.github.io/vega-lite/tutorials/getting_started.html)




実際のコードは **Livebook** で記述しますが、**VegaLite** では **Vl.from_json/1** という関数が用意されており、**Vega-Lite** の **JSON specification** をそのまま使うことができます。最後に **VegaLite** の **Elixir** 記法への変換も試みてあります。全体的にドキュメントの翻訳となってしまってますが、一応全て Livebook で動作確認しています。

**関連記事**
[VegaLite で埼玉県を切る( Elixir, Livebook) - Qiita](https://qiita.com/sand/items/a8b99ee8b68f49b5784a)
[Vega-Lite の View Composition (Elixir, Livebook) - Qiita](https://qiita.com/sand/items/f0e11ed907287be6436a)
[VegaLite の基礎 (Elixir, Livebook) -Qiita](https://qiita.com/sand/items/3433061b1966d6f8edb2)
[Livebook 事始め (Elixir) - Qiita](https://qiita.com/sand/items/6753c1782774b45daf6b)


# 1. Data と Encoding

まずは以下のコードを **Livebook** に入力し **Evaluate** します。
```elixir:
Mix.install([
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.4"},
  {:jason, "~> 1.2"}
])

alias VegaLite, as: Vl
```

次に以下のコードを **Evaluate** します。
```elixir:
Vl.from_json("""
{
  "data": {
    "values": [
      {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4},
      {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6},
      {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7}
    ]
  },
  "mark": "point",
  "encoding": {
    "x": {"field": "a", "type": "nominal"},
    "y": {"field": "b", "type": "quantitative"}
  }
}
""")
```

この **JSON specification** では **data** と **mark** 、 **encoding** という3つの property を設定しています。それぞれ以下に詳しく見ていきますが、まずはこれによって描かれるグラフを確認しておきましょう。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/814932d9-5afe-bc93-e4ed-31997edebb3e.png)


## 1-1. Data

**data** property では描画対象の**データソース**を指定します。ここでは **values** property を指定してインラインのデータを使うことを宣言しています。例えば **url** property で外部のデータソースを指定することも可能です。

* [Types of Data Sources](https://vega.github.io/vega-lite/docs/data.html)


## 1-2. Data を Mark で Encoding する


**mark** property では、**Data** を描画するための**グラフィカル要素**を指定します。ここでは **mark property** に **point** を指定します。

**encoding** property では、**Data** を **channel** へ **encode** します。
**encoding** は **data field** をどのように **channel** （座標）に対応させて、Dataを可視化するかを定義するものです。**key-value mapping** オブジェクトとして定義されます。**channel** の定義は、**field** と **type** で記述されます。

:::note info
**channel** はザックリ言えば、**座標** のようなものと考えられます。例えば Data を **2次元座標** で表せば異なる point でも同じ座標にプロットしてしまうところを、**3次元座標** で表せばキチンと異なる座標にプロットすることが可能なことがあります。当然 3次元の方がより多角的に Data を見ることができます。例えば **(x, y) 座標**の point に色を加え、 つまり**color軸** を設けて **(x, y, color) 座標** で表現することが可能です。
:::

ここでは **a** field を **x** channel ( x-position )　へ、 **b** field を **y** channel ( y-position )　へ と**encode** しています。Data それぞれに 違った position を与え描画することで、Dataのより有効な可視化が可能となります。**a** の **data type** として **nominal** （カテゴリ）を、**b** の **data type** として **quantitative** （数値）を指定しています。

**主な data type**
* **quantitative** - data が数値の場合
* **nominal** - data が文字列の場合
* **temporal** - data が time object の場合
* **ordinal** - data がランク順を表現している場合

もっと詳しくは以下を参照してください
* [Data Type](https://vega.github.io/vega-lite/docs/type.html)


ちなみに座標のラベルやタイトル、グリッドは自動的に付加されます。


# 2. Data Transformation: Aggregation

**Vega-Lite** は **data transformation** をサポートします。
**y** channel の定義に **"aggregate": "average"** を追加します。**x** のカテゴリごとに、**b** の平均値を計算して**y** 座標に描画します。 

```elixir:
Vl.from_json("""
{
  "data": {
    "values": [
      {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4},
      {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6},
      {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7}
    ]
  },
  "mark": "point",
  "encoding": {
    "x": {"field": "a", "type": "nominal"},
    "y": {"aggregate": "average", "field": "b", "type": "quantitative"}
  }
}
""")
```

**Evaluate** で実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/108e75ae-790e-7c74-e490-de2c95e4e8b4.png)



今度は **bar** チャートで描いてみます。**mark** type を **point** から **bar** に変更するだけです。

```elixir:
Vl.from_json("""
{
  "data": {
    "values": [
      {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4},
      {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6},
      {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7}
    ]
  },
  "mark": "bar",
  "encoding": {
    "x": {"field": "a", "type": "nominal"},
    "y": {"aggregate": "average", "field": "b", "type": "quantitative"}
  }
}
""")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/e8b5694c-3ab9-db16-1602-05240cc18a56.png)


ここで、**x** と **y** channel の定義を交換すると以下のように水平バーになります。
```elixir:
Vl.from_json("""
{
  "data": {
    "values": [
      {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4},
      {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6},
      {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7}
    ]
  },
  "mark": "bar",
  "encoding": {
    "y": {"field": "a", "type": "nominal"},
    "x": {"aggregate": "average", "field": "b", "type": "quantitative"}
  }
}
""")
```



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/1ea1776d-0f8b-cacd-3c41-b629442eb041.png)



# 3. 可視化のカスタマイズ

**property** を追加することで可視化のカスタマイズが行えます。**x** channel の定義に title property を追加することで、X軸のタイトルを変更することができます。


```elixir:
Vl.from_json("""
{
  "data": {
    "values": [
      {"a": "C", "b": 2}, {"a": "C", "b": 7}, {"a": "C", "b": 4},
      {"a": "D", "b": 1}, {"a": "D", "b": 2}, {"a": "D", "b": 6},
      {"a": "E", "b": 8}, {"a": "E", "b": 4}, {"a": "E", "b": 7}
    ]
  },
  "mark": "bar",
  "encoding": {
    "y": {"field": "a", "type": "nominal"},
    "x": {"aggregate": "average", "field": "b", "type": "quantitative",
           "title": "bの平均値"}
  }
}
""")
```
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/7ff35deb-45fc-0a6b-c4b1-125966f7d99f.png)




# 4. Data の調査
以下のドキュメントに従って進めていきます。
[Exploring Data](https://vega.github.io/vega-lite/tutorials/explore.html)

:::note warn
これ以降は **JSON specification** と共に、**VegaLite** の **Elixir** 記法のコードも併記します。
Elixir の方が JSON よりスッキリ書けますね。
:::

以下のデータリソースを使って。Dataの検査を行っていきます。
[seattle-weather.csv](https://vega.github.io/vega-lite/data/seattle-weather.csv)



**seattle-weather.csv** の頭だけを表示すると、以下のようなデータです。1,400日分の気象データが含まれています。
|date|precipitation|temp_max|temp_min|wind|weather|
|---|---|---|---|---|---|
2012-01-01|	0|	12.8|	5|	4.7|	drizzle|
2012-01-02|	10.9|	10.6|	2.8|	4.5|	rain|
2012-01-03|	0.8|	11.7|	7.2|	2.3|	rain|
2012-01-04|	20.3|	12.2|	5.6|	4.7|	rain|
2012-01-05|	1.3|	8.9|	2.8|	6.1|	rain|


**precipitation（降雨量)** を可視化します。
```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "mark": "tick",
  "encoding": {
    "x": {"field": "precipitation", "type": "quantitative"}
  }
}
""")
```

```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.mark(:tick)
|> Vl.encode_field(:x, "precipitation", type: :quantitative)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/263fdf63-07ed-1627-4f8e-19eb0f30aa3b.png)


これからわかることは、降水量が低い値に密集していることです。つまり、多くの日の場合、雨が降ることがあっても、多く降ることはない、という感じです。

降水量のヒストグラムをみると、より一層この傾向をつかむことができます。このために **y** の **encoding channel** を追加します。**x** は**連続値**のまま扱うのではなく**離散化**します。降水量を10ごとに区切り、その区間に含まれる日数を **count** するわけです。降水量の**離散化**は **"bin": true** で指定します。**type** のデフォルト値は **quantitative** なのでここでは省略してあります。 
**y** channel の **"aggregate": "count"** に注意してください。**aggregate** が **count** 指定の時は、**field** property はありません。代わりに、**x** channnel のグループごとの **data object** の数、ここでは **bin** でグループ分けされた日数をカウントしてくれます。同様に、**x** channel が **"timeUnit": "month"** でグループ化されていた場合は、月ごとの日数をカウントしてくれます。

:::note info
ビニング ( **bin** ) は、多かれ少なかれ連続する値の数をより少ない数の「ビン」にグループ化する（**離散化**）方法です。たとえば、人々のグループに関するデータがある場合、彼らの年齢をより少ない数の年齢間隔に配置することができます。
:::



```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "mark": "bar",
  "encoding": {
    "x": {"bin": true, "field": "precipitation"},
    "y": {"aggregate": "count"}
  }
}
""")
```

```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "precipitation", type: :quantitative,  bin: true)
|> Vl.encode(:y, aggregate: :count, type: :quantitative)
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/64c9d1ad-f2b8-f824-de3e-c9e573150169.png)


次はシアトルの一年を通した降水量の偏移を見ます。**Vega-Lite**は時間軸をサポートしており、date 軸の離散化が可能です。月単位で降水量の離散化を行い、その平均値の遷移を以下のように **plot** できます。月単位の離散化は **"timeUnit": "month"** で行います。

```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "mark": "line",
  "encoding": {
    "x": {"timeUnit": "month", "field": "date"},
    "y": {"aggregate": "mean", "field": "precipitation"}
  }
}
""")
```

```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.mark(:line)
|> Vl.encode_field(:x, "date", time_unit: :month)
|> Vl.encode_field(:y, "precipitation", aggregate: :mean)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/e7f98f89-8438-5f82-bf03-84b23d605684.png)

このチャートは、シアトルでは夏より冬の方が圧倒的に降水量が多いことを示しています。

次に **temperature** （温度）の、年月による推移を見ます。単に月で見るのではなく、年も考慮して遷移を見ます。**"timeUnit": "yearmonth"** で指定します。月ごとの温度の平均値ではなく、最高温度の値を使います。温度としては日中の最高温度(temp_max)を使います。



```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "mark": "line",
  "encoding": {
    "x": {"timeUnit": "yearmonth", "field": "date"},
    "y": {"aggregate": "max", "field": "temp_max"}
  }
}
""")
```

```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.mark(:line)
|> Vl.encode_field(:x, "date", time_unit: :yearmonth)
|> Vl.encode_field(:y, "temp_max", aggregate: :max)
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/44bec7d9-3ef4-35b3-1c07-4002d9cd19a6.png)

この遷移を見ると、年ごとにだんだん日中の最高温度が高くなっている傾向が見て取れます。その傾向をもっとはっきりさせます。年単位で温度の平均値をとり plot します。


```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "mark": "line",
  "encoding": {
    "x": {"timeUnit": "year", "field": "date"},
    "y": {"aggregate": "mean", "field": "temp_max"}
  }
}
""")
```


```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.mark(:line)
|> Vl.encode_field(:x, "date", time_unit: :year)
|> Vl.encode_field(:y, "temp_max", aggregate: :mean)
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/c86baf31-3797-7c92-aebb-acfbfd38f522.png)


確かに、日中の最高温度は、年々高くなっていると言えそうです。

次に温度差（温度変動幅）、日中の最高温度から最低温度を引いたもの、の遷移を見たいと思います。

```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "transform": [
    {"calculate": "datum.temp_max - datum.temp_min", "as": "temp_range"}
  ],
  "mark": "line",
  "encoding": {
    "x": {
      "timeUnit": "month",
      "field": "date"
    },
    "y": {
      "aggregate": "mean",
      "field": "temp_range"
    }
  }
}
""")
```


```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.transform(calculate: "datum.temp_max - datum.temp_min", as: "temp_range")
|> Vl.mark(:line)
|> Vl.encode_field(:x, "date", time_unit: :month)
|> Vl.encode_field(:y, "temp_range", aggregate: :mean)
```


ここでは **transform** を使って新しい **field** の **temp_range** を定義しています。**temp_range** は一日の温度差を表します。この新しい **field** は、他の **field** と同じように使うことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/1097317e-a4ce-cacb-8bf1-268916a655ea.png)

次に年を通してどの **weather**　がどれだけ多いのかを見たいと思います。 
* **x** channel を **"field": "date"** として **"timeUnit": "month"** で離散化する
* **y** channel はレコード数（日数）で count する
* さらに **y** 軸を、 **color** channel を追加することで、**weather** type で分割する
```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "mark": "bar",
  "encoding": {
    "x": {
      "timeUnit": "month",
      "field": "date",
      "type": "ordinal"
    },
    "y": {
      "aggregate": "count",
      "type": "quantitative"
    },
    "color": {
      "field": "weather",
      "type": "nominal"
    }
  }
}
""")
```

```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "date", time_unit: :month, type: :ordinal)
|> Vl.encode(:y, aggregate: :count, type: :quantitative)
|> Vl.encode_field(:color, "weather", type: :nominal)
```


 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/3857298f-c60e-d178-cb20-bb059ef031cc.png)


:::note info
**channel** はザックリ言えば、**座標** のようなものと考えられます。例えば Data を **2次元座標** で表せば異なる point でも同じ座標にプロットしてしまうところを、**3次元座標** で表せばキチンと異なる座標にプロットすることが可能なことがあります。当然 3次元の方がより多角的に Data を見ることができます。例えば **(x, y) 座標**の point に色を加え、 つまり**color軸** を設けて **(x, y, color) 座標** で表現することが可能です。
:::

しかしデフォルトで割り当てられたカラーは、データをうまく表現できていない可能性があります。**color scale range** を使ってどの **weather** にどの **Color** を割り当てるかを指定します。ついでに **x** 軸と **legend** のタイトルもカスタマイズします。


```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/seattle-weather.csv"},
  "mark": "bar",
  "encoding": {
    "x": {
      "timeUnit": "month",
      "field": "date",
      "type": "ordinal",
      "title": "Month of the year"
    },
    "y": {
      "aggregate": "count",
      "type": "quantitative"
    },
    "color": {
      "field": "weather",
      "type": "nominal",
      "scale": {
        "domain": ["sun", "fog", "drizzle", "rain", "snow"],
        "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"]
      },
      "title": "Weather type"
    }
  }
}
""")
```


```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/seattle-weather.csv")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "date", time_unit: :month, type: :ordinal, title: "Month of the year")
|> Vl.encode(:y, aggregate: :count, type: :quantitative)
|> Vl.encode_field(:color, "weather", type: :nominal, title: "Weather type",
    scale: [domain: ["sun", "fog", "drizzle", "rain", "snow"],
            range: ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"]])
```



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/b1edb5c5-a8e5-4cab-5745-1d409eb6548c.png)




# 5. VegaLite (Elixir)
**Vega-Lite** は **JSON specification** で記述しますが、これを **VegaLite** の **Elixir** 記法に変えてみたいと思います。
[VegaLite 公式ドキュメント](https://hexdocs.pm/vega_lite/VegaLite.html)

例題として以下のドキュメントを利用します。
[Binning](https://vega.github.io/vega-lite/docs/bin.html)

## 5-1.Example: Histogram
Histogram の **JSON specification** です。
```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/movies.json"},
  "mark": "bar",
  "encoding": {
    "x": {
      "bin": true,
      "field": "IMDB Rating"
    },
    "y": {"aggregate": "count"}
  }
}
""")
```

**Elixir** での記法です

```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/movies.json")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "IMDB Rating", type: :quantitative,  bin: true)
|> Vl.encode(:y, aggregate: :count, type: :quantitative)
```

どちらも同じ結果が得られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/68d02444-04c1-ef07-b563-31dd9fc0e813.png)


## 5-2. Example: Histogram with Ordinal Scale
Ordinal Scale を持ったHistogram の **JSON specification** です。
```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/movies.json"},
  "mark": "bar",
  "encoding": {
    "x": {
      "bin": true,
      "field": "IMDB Rating",
      "type": "ordinal"
    },
    "y": {"aggregate": "count"}
  }
}
""")
```

**Elixir** での記法です
```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/movies.json")
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "IMDB Rating", type: :ordinal,  bin: true)
|> Vl.encode(:y, aggregate: :count, type: :quantitative)
```

どちらも同じ結果が得られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/e90a1fdf-d73f-9028-9d1b-e15e1a6314f7.png)


## 5-3. Example: Binned color


Binned color の **JSON specification** です。
```elixir:
Vl.from_json("""
{
  "data": {"url": "https://vega.github.io/vega-lite/data/cars.json"},
  "mark": "point",
  "encoding": {
    "x": {"field": "Horsepower", "type": "quantitative"},
    "y": {"field": "Miles_per_Gallon", "type": "quantitative"},
    "color": {"bin": true, "field": "Acceleration"}
  }
}
""")
```

**Elixir** での記法です
```elixir:
Vl.new()
|> Vl.data_from_url("https://vega.github.io/vega-lite/data/cars.json")
|> Vl.mark(:point)
|> Vl.encode_field(:x, "Horsepower", type: :quantitative)
|> Vl.encode_field(:y, "Miles_per_Gallon", type: :quantitative)
|> Vl.encode(:color, field: "Acceleration", bin: true)
```

どちらも同じ結果が得られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/da1c6e10-df9f-b50d-bbaa-fb8a7a8b0107.png)


今回は以上です。
全てのグラフがドキュメント通り、**Livebook** 上で実現できました。