【Python】データ可視化ライブラリAltairハンズオン【基礎編】

# 概要

本稿ではグラフ可視化ライブラリ [Altair](https://altair-viz.github.io/index.html) の使い方を紹介させて頂きます。Altair はデータをインタラクティブに可視化するために開発された json のフォーマット [Vega-Lite](https://vega.github.io/vega-lite/) を Python から記述することができるライブラリです。

ぜひ[【時系列データ編】](https://qiita.com/keisuke-ota/items/5657b1388fc5ad522caa)も参照してみてください。

# ハンズオン公開
[Google Colab](https://colab.research.google.com/drive/1iZZuhCBgrZCQRN60zFxjlV5-pEiIcgr9?usp=sharing)と [GitHub Pages](https://keisuke-h-ota.github.io/altair_practice/basic.html) でハンズオンを公開しています。よろしければご活用ください。

# 可視化ツールとしての Altair の強み

Altair の強みは、高度なデータ分析が可能な Python の枠組みでインタラクティブなグラフを作成できるところにあります。同じ Python の可視化ライブラリとして最も普及している [Matplotlib](https://matplotlib.org/) はインタラクティブなグラフの作成機能があまり充実していません。一方で Excel はリストを活用することでロップダウンと連動したグラフを作成することなどはできますが、Python のように高度な分析には対応していません。

|可視化ツール|インタラクティブなグラフのカスタマイズ性|高度な分析|
|--|--|--|
|Altair|◎：Vega-Liteに則って自由に作成可能|◎：Pythonで可能|
|Matplotlib|△：``ipywighets``や``animation.ArtistAnimation()``の活用で限定的に可能|◎：Pythonで可能|
|Excel|◯：リストを活用したドロップダウンとの連動などが可能|△：基礎的な集計・検定に限る|


Altair のようにインタラクティブなグラフを作成できる Python の可視化ライブラリとして、他には [Bokeh](https://docs.bokeh.org/en/latest/) や [Plotly](https://plotly.com/python/) などがあります。それぞれ作成可能なグラフの種類やデザインが異なるため、好みに合わせて使い分けるとよいと思います。個人的な意見としては、三次元モデルの可視化が得意なのは Plotly で、二次元モデルの可視化が得意なのは Altair です。

# データ作成

今回は架空の学校で行われた期末試験の得点をデモデータとして作成します。この学校には学生が 30 人在籍し、普通、特進、理数の 3 コースが存在します。期末試験の科目は国語、数学、理科、社会、英語で各教科 100 点満点とします。


```python:データ作成
import random
import pandas as pd

# パラメータ
N = 30  # 学生の人数
mu, sigma = 60, 18 # 学力の平均と標準偏差
seed = 1

# 得点データの生成
sex = ["男", "女"]  # 性別
cl = ["普通", "理数", "特進"]  # コース
sub = ["国語", "数学", "理科", "社会", "英語"]  # 教科
id_lst = ["ID" + str(i + 1).zfill(len(str(N))) for i in range(N)]

random.seed(seed)

C = [random.randint(a=0, b=len(cl) - 1) for _ in range(N)]  # コース分け
S = [random.gauss(mu=mu, sigma=sigma) * (1 + C[n] * 0.18) for n in range(N)] # 学力
G = [random.randint(a=0, b=1) for _ in range(N)]  # 性別

T = { # テストの難易度
    "国語": {"mu": 0.75, "sigma": 0.15, "cor": 0.02},
    "数学": {"mu": 0.7, "sigma": 0.15, "cor": -0.05},
    "社会": {"mu": 0.85, "sigma": 0.03, "cor": 0.03},
    "理科": {"mu": 0.8, "sigma": 0.05, "cor": -0.02},
    "英語": {"mu": 0.8, "sigma": 0.15, "cor": 0.01},
}

df = pd.DataFrame()
df["学生番号"] = id_lst
for s in sub:
    df[s] = [
        max(0,min(int(S[i] * random.gauss(mu=T[s]["mu"], sigma=T[s]["sigma"]) * (1 + G[i] * T[s]["cor"])),100))
        for i in range(N)
    ]

df["コース"] = [cl[c] for c in C]
df["性別"] = [sex[g] for g in G]
df.head()
```

```:df
    学生番号	国語	数学	理科	社会	英語	コース 性別
0	ID01	18	7	15	17	11	普通	女
1	ID02	47	34	49	57	80	特進	女
2	ID03	32	39	41	42	42	普通	男
3	ID04	64	34	68	71	54	理数	女
4	ID05	21	41	45	51	47	普通	男
```

# インストール

本ハンズオンでは ``altair`` と ``altair_saver`` という2つのライブラリを使用します。両方とも pip でインストール可能です。

```
$ pip install altair altair_saver
```

conda でもインストールできます。詳細は[公式ドキュメント](https://altair-viz.github.io/getting_started/installation.html)を参照してください。

## google colab の場合

デフォルトで ``altair`` はインストールされています。以下ライブラリをインストールしてください。

```
! pip install -U requests
! pip install altair_saver
```

# グラフの構成

altair で作られるグラフは [chart object](https://altair-viz.github.io/user_guide/data.html) というクラスから生成されたオブジェクトです。chart object には以下の 6 種類があります。

|||
|--|--|
|``alt.Chart()`` |最も基本的なオブジェクト|
|``alt.LayerChart()``|複数の``alt.Chart()`` を重ねられるオブジェクト|
|``alt.VConcatChart()``|複数の ``alt.Chart()`` を縦に並べられるオブジェクト|
|``alt.HConcatChart()``|複数の ``alt.Chart()`` を横に並べられるオブジェクト|
|``alt.RepeatChart()``|類似の ``alt.Chart()`` を縦や横に並べられるオブジェクト|
|``alt.FacetChart()``|``alt.VConcatChart()`` や ``alt.HConcatChart()`` を重ねられるオブジェクト|

これらのオブジェクトはすべて Pandas DataFrame や json、csv などを引数とし、そのデータを可視化することができます。次は最も基本的な``alt.Chart()``の構成を見ていきましょう。


# Mark と Encoding

``alt.Chart()`` でデータを可視化するためには [.mark_XXXX()](https://altair-viz.github.io/user_guide/marks.html)と[.encode()](https://altair-viz.github.io/user_guide/encoding.html)という2つのメソッドが不可欠です。それぞれの役割は以下の通りです。

|メソッド|役割|
|--|--|
|``.mark_XXXX()``|``XXXX`` は可視化したいグラフの種類によって変わります。各グラフには [channel](https://altair-viz.github.io/user_guide/encoding.html#encoding-channels) といった要素があり、引数には全レコードで共通の処理をしたい channel をとります。例えば散布図でプロットの色や形を揃えたいときはここで指定しましょう。|
|``.encode()``|引数には1レコードごとに異なる処理をしたい channel をとります。多くのグラフにおいて X 軸の ``alt.X("列名")`` や Y 軸の``alt.Y("列名")`` は不可欠です。|

以下の散布図は X 軸に国語の得点をとり、Y軸に数学の得点をとります。また性別ごとに色を分け、コースごとにグラフを分けています。一方でプロットの大きさや透過度、形は統一しています。

```python:散布図
import altair as alt
altair_chart = (
    alt.Chart(data=df)
    .mark_point(size=30, opacity=0.9, shape="triangle")
    .encode(
        x=alt.X("国語"),
        y=alt.Y("数学"),
        row=alt.Row("コース"),
        color=alt.Color("性別")
    )
)
altair_chart
```

![visualization (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/2236a660-d729-2aba-9958-f56550ee6c10.png)


# 図の保存

``altair_saver.save()``を使えば [html](https://altair-viz.github.io/user_guide/saving_charts.html#html-format) または [json](https://altair-viz.github.io/user_guide/saving_charts.html#json-format) で保存可能です。

```python:図の保存
from altair_saver import save
save(altair_chart, "./saved_chart.html", inline=False)
save(altair_chart, "./saved_chart.json")
```


## オフラインで html 保存する際の注意点

``inline=False``にすると [vega](https://vega.github.io/vega/)、[vega-lite](https://vega.github.io/vega-lite/)、[vega-embed](https://github.com/vega/vega-embed) を外部ファイル化できます。すなわち下記3ファイルを外部から読み込むことでファイルサイズを節約できます。

*   src="https://cdn.jsdelivr.net/npm/vega@5"
*   src="https://cdn.jsdelivr.net/npm/vega-lite@5"
*   src="https://cdn.jsdelivr.net/npm/vega-embed@6"

ただしオフラインでは ``inline=True`` としてhtmlファイルに直接埋め込む必要があります。

## 他形式の保存
png、svg、形式の保存はグラフ右上の三点印``embed_options``から可能ですが解像度は調節できません。 ``altair_saver`` を使用すれば png、svg、そして pdf でも高解像度で[保存可能](https://altair-viz.github.io/user_guide/saving_charts.html#png-svg-and-pdf-format)です。
ただ Jupyter 上でこれを実行するためには更なるセットアップが必要です。詳しくは[こちら](https://yoshidabenjiro.hatenablog.com/entry/2020/08/15/191416)を参考にしてください。

# Option による軸、凡例、ヘッダーの設定

エンコーディングされた各チャネルには[option](https://altair-viz.github.io/user_guide/encoding.html#encoding-channel-options)を引数としてとることができます。option は channel ごとに仕様が異なっていたり、一部の channel でしか使えなかったりするものが多いです。よく使うものだけを覚えておくとよいでしょう。
以下の例では、散布図の軸ラベルや判例などをカスタマイズしています。


```python:Option による軸、凡例、ヘッダーの設定
altair_chart_with_options = (
    alt.Chart(data=df)
    .mark_point(size=30, opacity=0.9, shape="triangle")
    .encode(
        x=alt.X(
            "国語",
            scale=alt.Scale(domain=[0, 100], bins=[0, 20, 40, 60, 80, 100]),  # 軸の値の範囲
            axis=alt.Axis(
                ticks=False, # 軸に┬をいれるかどうか
                grid=True, # グラフの中にマス目を描くかどうか
                labelFont="Yu Gothic UI",
                labelFontSize=15,
                labelAngle=0,
                titleFontSize=18,
                titleFont="Yu Gothic UI",
                titleAngle=0,
                title="X軸のタイトルはここ",
            ),
        ),
        y=alt.Y(
            "数学",
            scale=alt.Scale(domain=[0, 100], bins=[0, 20, 40, 60, 80, 100]), 
            axis=alt.Axis(
                ticks=False,
                grid=True,
                labelFont="Yu Gothic UI",
                labelFontSize=15,
                labelAngle=0,
                titleFontSize=18,
                titleFont="Yu Gothic UI",
                titleAngle=-90,
                title="Y軸のタイトルはここ",
            ),
        ),
        row=alt.Row(
            "コース",
            header=alt.Header(
                labelFont="Yu Gothic UI",
                labelFontSize=15,
                labelAngle=-90,
                titleFontSize=18,
                titleFont="Yu Gothic UI",
                titleAngle=-90,
                title="ヘッダータイトルはここ",
            ),
        ),
        color=alt.Color(
            "性別",
            scale=alt.Scale(domain=sex, range=["steelblue", "darkred"]), # プロットの色の指定
            legend=alt.Legend(
                titleFont="Yu Gothic UI",
                title="凡例タイトルはここ",
            ),
        ),
    )
)
altair_chart_with_options
```

![visualization (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/39fba9a3-4e5e-497b-03c1-2612c6ac0a88.png)

``alt.Scale()``は ``alt.X()`` の引数では軸の値を指定するのに対し、``alt.Color()``ではプロットの色を指定しています。上記のように channel ごとに仕様が異なる option については特に注意しましょう。なお色は[alt.ColorName()](https://altair-viz.github.io/user_guide/generated/core/altair.ColorName.html)またはカラーコードで指定できます。


# グラフのサイズ指定

グラフのサイズは[.properties()](https://altair-viz.github.io/user_guide/customization.html#adjusting-chart-size)で指定します。ここではグラフのタイトルも指定できます。

```python:グラフサイズの指定
altair_chart_with_options.properties(width=200, height=100, title="グラフのタイトルはここ")
```

![visualization (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/d4622fdf-f2be-9245-41bb-f0b344978b28.png)


# テーマの設定
Altair で複数の Figure を作成する際、軸やヘッダー、凡例、グラフのサイズなどを毎回指定していてはコードが冗長になってしまいます。すべてのグラフに共通させたいパラメータは ``alt.themes.register()`` で一括登録してしまうのがオススメです。ちなみにここで指定したパラメータは、登録後に個別の``.encode()``で上書き可能です。

```python:テーマの設定
def font_config():
    labelFont = "Yu Gothic UI"
    labelFontSize = 15
    labelAngle = 0
    titleFont = "Yu Gothic UI"
    titleFontSize = 18
    titleAngle = 0
    markFont = "Yu Gothic UI"

    return {
        "config": {
            "axis": {
                "ticks": True,
                "grid": True,
                "labelFont": labelFont,
                "labelFontSize": labelFontSize,
                "labelAngle": 0,
                "titleFont": titleFont,
                "titleFontSize": titleFontSize,
            },
            # 色分けした際の項目
            "legend": {
                "labelFont": labelFont,
                "labelFontSize": labelFontSize,
                "labelAngle": labelAngle,
                "titleFont": titleFont,
                "titleFontSize": titleFontSize,
                "titleAngle": titleAngle,
            },
            # グラフ上部の文字
            "header": {
                "labelFont": labelFont,
                "labelFontSize": 20,
                "labelAngle": labelAngle,
                "titleFont": titleFont,
                "titleFontSize": 25,
                "titleAngle": titleAngle,
            },
            "mark": {"font": markFont},
            "title": {"font": titleFont, "subtitleFont": titleFont},
            # 図の大きさ
            "view": {"width": 300, "height": 300},
            # 図の背景
            "background": "white",
        }
    }


alt.themes.register(name="font_config", value=font_config)
alt.themes.enable(name="font_config")
```

# 変数型の指定

Encoding ではそのカラムの[変数の型](https://altair-viz.github.io/user_guide/encoding.html#encoding-data-types)を指定する必要があります。上記のように指定せずとも自動で補完されることもありますが、指定しないとエラーが発生することもあります。指定可能な変数の型は以下のとおりです。


|変数|``type=XXXX``|
|--|--|
|連続変数| quantitative |
|順序変数|ordinal|
|名義変数|nominal|
|時系列変数|temporal|
|地理的変数|geojson|

以下の例では"数学"の得点をあえて"順序変数"と指定することで得点の間隔が無視されています。またプロットの色を"社会"の得点とすることで得点の高低が色の濃淡によって表されています。

```python:変数型の指定
(
    alt.Chart(data=df)
    .mark_point(size=30, opacity=0.9, shape="triangle")
    .encode(
        x=alt.X("国語", type="quantitative"),
        y=alt.Y("数学", type="ordinal",sort="-y"), # sort で降順になるように並び替えました
        color=alt.Color("社会", type="quantitative", legend=None),
        shape=alt.Shape("性別", type="nominal"),
    )
    .properties(width=300, height=300, title="グラフのタイトルはここ")
)
```

![visualization (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/db574b46-c3ea-dad8-d59a-94e537e1a763.png)

なお変数型は頭文字1文字で省略することも可能です。例えば``x=alt.X("国語", type="quantitative")``は``x=alt.X("国語:Q")``と同じです。


# 縦長テーブルへの変換

RDB には横長もしくは縦長という[データの構造](https://altair-viz.github.io/user_guide/data.html#long-form-vs-wide-form-data)があります。これはメタデータをどのように持つかの違いです。


||横長|縦長|
|:---|:---|:---|
|**特徴**|メタデータを行ラベルまたは列ラベルに持つ|メタデータを RDB のフィールドに持つ|
|**長所**|テーブルのレコード数を節約できる|列ラベルや行ラベルを参照せずともメタデータはわかる|
|**短所**|行ラベルまたは列ラベルを参照しないとメタデータが分からない|テーブルのレコード数が長くなる|
|**別名**|雑然（messy）データ|整然（tidy）データ|


例えば上記の``df`` では ``国語``、``数学``、 ``理科``、 ``社会``、 ``英語``という別々の列にそれぞれの科目の得点が記録されています。そのためそれぞれの値がどの科目の得点なのかは列ラベルを参照しないとわかりません。このようなデータ構造は横長といえます。一方で``学生番号``や``コース``、``性別``などは列ラベルを参照しなくてもわかります。このようなデータ構造は縦長といえます。

Altair では ``.encode()`` や後述の ``.add_selection()``、``.transform_XXXXX()`` などの関数によってレコード1行ごとの値を参照してデータのETL（Extract、Transform、Load）が行われます。この際、列ラベルや行ラベルを参照することはできません。したがって、縦長のデータ構造に変換することによって Altair による可視化のカスタマイズ性は飛躍的に向上します。

そこで ``df`` で横長に持たれている科目ごとの得点を、``.pandas.melt()``を用いることによって縦長のデータ構造に変換しましょう。


```python:縦長データへの変形
long_df = pd.melt(df, id_vars=["学生番号", "性別", "コース"], var_name="科目", value_name="得点")
long_df
```

```:long_df
	学生番号	性別	コース 科目 得点
0	ID01	女	普通	国語	18
1	ID02	女	特進	国語	47
2	ID03	男	普通	国語	32
3	ID04	女	理数	国語	64
4	ID05	男	普通	国語	21
...	...	...	...	...	...
145	ID26	男	理数	英語	24
146	ID27	男	普通	英語	15
147	ID28	男	普通	英語	43
148	ID29	女	普通	英語	45
149	ID30	女	特進	英語	47
```

# 縦長データの可視化（箱ひげ図）


科目を縦長のデータ構造で持たせることによって科目ごとの得点の集計が可能になりました。例えば科目ごとの得点の中央値や四分位数を集計する際は以下のように箱ひげ図を ``.mark_boxplot()`` で作成するとよいでしょう。すなわち X 軸に配置する``科目``を key に groupby して Y 軸の``得点``を集計します。ちなみに　``.mark_boxplot()`` は複数の``alt.Chart()``を重ね合わせたオブジェクトを返すため、一部の仕様が他の``.mark_XXXX()``と異なります。

```python
(
    alt.Chart(long_df)
    .mark_boxplot( 
        size=30, # 箱の幅
        ticks=alt.MarkConfig(width=40), # ひげの ┸ や ┯ の幅
        median=alt.MarkConfig(color="black", size=30), # 中央値に引かれる線の設定
    )
    .encode(
        x=alt.X("科目:N"),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100],bins=[0, 20, 40, 60, 80, 100]), # domainは表示する点数の範囲
            axis=alt.Axis(title="得点"),
        ),
        column=alt.Column("性別", sort=alt.Sort(cl)),
    )
    .properties(width=300, height=300, title="各科目の得点分布")
    .interactive()
)
```

![visualization (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/834f9062-6641-1dd5-ea4b-2271aa578a94.png)

上の箱ひげ図をカーソルでなぞると中央値などの統計量がポップアップされます。この機能は**tooltips**と呼ばれています。他の``.mark_XXXX()``では tooltip channel ``alt.Tooltip()`` で指定されたカラムの値しか表示されませんが、``.mark_boxplot()`` で返される chart オブジェクトはデフォルトで tooltips を持っています。

# Aggregate と Tooltips

Altair では aggregate 関数によってグループごとの要約統計量を算出することが可能です。算出可能な要約統計量は ``"mean"``、 ``"sum"``、``"median"``、``"min"``、``"max"``、``"count"`` の6つです。

前節でふれた ``.alt.Tooltip()``でも aggregate 関数は使用可能です。

```python:Aggregate と Tooltips
bar_chart = (
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X(
            "科目:N",
            sort=alt.Sort(sub),
            axis=alt.Axis(title="5教科"),
        ),
        y=alt.Y(
            "得点:Q",
            aggregate="mean", # aggregate 関数の指定
            scale=alt.Scale(domain=[0, 100]),
            axis=alt.Axis(title="平均点"),
        ),
        color=alt.Color(
            "科目:N",
            scale=alt.Scale(
                domain=sub, range=["brown", "steelblue", "orange", "green", "purple"]
            ),
        ),
        tooltip=[
            alt.Tooltip(field="得点", aggregate="mean", format=".2f"), # 平均得点を集計して小数第二位まで表示
            alt.Tooltip(field="科目"),
        ],
    )
)
bar_chart
```

![visualization (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/1a4c0879-805e-c3fc-31ff-7699086f2917.png)


SQL の ``HAVING 句`` や pandas や pyspark の ``.agg()`` と似たような処理といえます。

# エラーバーの挿入
ばらつきの大きさを表す要約統計量をエラーバーとして可視化させることもできます。``.mark_errorbar(extent=XXX)``でエラーバーの計算方法を選択しましょう。引数はデフォルトで``stderr``（標準誤差）です。他には``stderv``（標準偏差）、``ci``（95%信頼区間）、``iqr``（四分位範囲） といった統計量がとれます。

```python:エラーバーの挿入
error_bar = (
    alt.Chart(long_df)
    .mark_errorbar(extent="ci",
                   clip=True,rule=True,size=100,
                   ticks=alt.MarkConfig(width=20,color="black"),
                  )
    .encode(
        x=alt.X(
            "科目:N",
            sort=alt.Sort(sub),
            axis=alt.Axis(title="5教科"),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
            axis=alt.Axis(title="平均点"),
        )
    )
)
bar_chart + error_bar
```

![visualization (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/0cfdc03a-fe3a-26fb-b291-e820205ae86b.png)


# 棒グラフ（積み上げ）
``alt.Y(stack=True)``で棒グラフを積み上げることができます。``stack``はデフォルトでTrueになっているため省略しても構いません。

```python:棒グラフ（積み上げ）
(
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            aggregate="sum",
            stack=True,
            scale=alt.Scale(domain=[0, 500]),
            axis=alt.Axis(title="合計点"),
        ),
        color=alt.Color(
            "科目:N",
            scale=alt.Scale(
                domain=sub, range=["brown", "steelblue", "orange", "green", "purple"]
            ),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="科目"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .properties(width=500)
)
```

![visualization (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/fed693ec-9ee9-eb0a-f722-57ff83eabe85.png)


# 棒グラフ（並列）
棒グラフの並列表示は ``alt.Row()`` または ``alt.Column()`` を活用したグラフの分割で可能です。下記の例では学生1人につき1つのグラフオブジェクトを作成しています。

```python:棒グラフ（並列）
(
    alt.Chart(long_df)
    .transform_filter((alt.datum.学生番号 <= id_lst[4]))  # 便宜上5名のみ表示
    .mark_bar()
    .encode(
        x=alt.X("得点:Q", scale=alt.Scale(domain=[0, 100])),
        y=alt.Y("科目:N", axis=None),
        color=alt.Color(
            "科目:N",
            scale=alt.Scale(
                domain=sub, range=["brown", "steelblue", "orange", "green", "purple"]
            ),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="科目"),
            alt.Tooltip(field="学生番号"),
        ],
        row=alt.Row("学生番号:N", header=alt.Header(titleAngle=-90)),
    )
    .properties(height=70, width=500)
    .configure_facet(spacing=5) # グラフごとの間隔
)
```

![visualization (21).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/9b450412-e36b-2f66-1d47-c6bdc34da4c0.png)



# 対数グラフ
対数グラフの作成は``alt.Y(scale=alt.Scale(type="log"))``で行いましょう。ちなみに0以下の値があると表示されませんので前処理で削除しておきましょう。


```python:対数グラフ
(
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            aggregate="sum",
            scale=alt.Scale(type="log"),
            axis=alt.Axis(title="合計点"),
        ),
        tooltip=[
            alt.Tooltip(field="得点",aggregate="sum"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .properties(width=500)
)
```

![visualization (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/b9472eb3-f0bf-672c-3dc7-2722c328d4c7.png)


# 割合グラフ
``stack="normalize"``で割合を算出することができます。

```python:割合グラフ
(
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",stack="normalize",
            axis=alt.Axis(format="%"),
        ),
        color=alt.Color(
            "科目:N",
            scale=alt.Scale(
                domain=sub, range=["brown", "steelblue", "orange", "green", "purple"]
            ),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="科目"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .properties(width=500)
)
```


![visualization (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/e0ff37f2-d9ea-b163-7c1d-682a31abbfec.png)


``.transform_XXXXX()``を用いると様々な前処理が可能です。以下は ``.transform_bin()``、``.transform_joinaggregate()``、``.transform_calculate()`` を組みあわせてほぼ同じグラフを作成する例です。

```python:割合グラフ（2）
(
    alt.Chart(long_df)
    .transform_bin(as_="ビン", field="学生番号:N")
    .transform_joinaggregate(合計得点="sum(得点):Q", groupby=["学生番号", "ビン"])
    .transform_calculate(割合="datum.得点/datum.合計得点")
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "割合:Q",
            axis=alt.Axis(format="%"),
        ),
        color=alt.Color(
            "科目:N",
            scale=alt.Scale(
                domain=sub, range=["brown", "steelblue", "orange", "green", "purple"]
            ),
        ),
        tooltip=[
            alt.Tooltip(field="割合",format="%"),
            alt.Tooltip(field="科目"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .properties(width=500)
)
```

![visualization (11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/d0e618eb-f5b6-2d2d-e7ff-4a1d1f468845.png)

# 積み上げヒストグラム
棒グラフと同様にヒストグラムを積みあげることも可能です。

```python:積み上げヒストグラム
(
    alt.Chart(df)
    .mark_bar()
    .encode(
        x=alt.X("国語", 
            bin=alt.Bin(step=10,extent=[0,100]),
            axis=alt.Axis(title="得点")
            ),
        y=alt.Y("国語",
            aggregate="count",
            axis=alt.Axis(title="人数",values=list(range(N))),
            stack=True,
            ),
        color=alt.Color("性別",
            scale=alt.Scale(domain=sex, range=["steelblue","darkorange"])
            ),
    )
)
```

![visualization (12).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/ed02eda9-dc84-5626-48ff-cf51a478a53c.png)


# 重ね合わせヒストグラム
棒グラフやヒストグラムは``stack=False``重ねて表示させることもできます。その場合``opacity``を1未満に設定して透過させるようにするとよいでしょう。なお Altair ではX軸とY軸の配置は交換可能です。


```python:重ね合わせヒストグラム
(
    alt.Chart(df)
    .mark_bar(opacity=0.5)
    .encode(
        y=alt.Y("国語", 
            bin=alt.Bin(step=10,extent=[0,100]),
            axis=alt.Axis(title="得点")
            ),
        x=alt.X("国語",
            aggregate="count",
            axis=alt.Axis(title="人数",values=list(range(N))),
            stack=False,
            ),
        color=alt.Color("性別",
            scale=alt.Scale(domain=sex, range=["steelblue","darkorange"])
            ),
    )
)
```

![visualization (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/fcbc9c08-96f0-35d7-c2af-4303806a1cff.png)


# 条件式によるレコードの抽出

Altair では条件式によるレコードの抽出が ``.transform_filter()`` で可能です。この操作は SQL の where 句とも類似しています。以下では国語の得点のみを抽出しています。

```python:条件式によるレコードの抽出
(
    alt.Chart(long_df)
    .transform_filter(alt.datum.科目 == "国語")
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
            axis=alt.Axis(title="国語の得点"),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .properties(width=500)
)
```

![visualization (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/da49192b-b0a2-899b-3389-60c3b09714b5.png)

# 条件式による分岐
``.encode()`` の channel には条件式（すなわち if 文）によってレコードごとに処理を変えられるものもあります。この if 文は ``alt.condition()`` の引数 ``predicate`` でとれます。``alt.condition()`` を渡すことができる channel として　``color``、``size``、``opacity`` などがあります。

```python:条件式による分岐
(
    alt.Chart(long_df)
    .transform_filter(alt.datum.科目 == "国語")
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
            axis=alt.Axis(title="国語の得点"),
        ),
        color=alt.condition(
            predicate=alt.datum.得点 >= 55, # if 文を挿入します。
            if_true=alt.value("steelblue"), # 55 点以上のレコードは青色
            if_false=alt.value("gray"), # それ以下は灰色
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .properties(width=500)
)
```

![visualization (15).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/7afe8d27-08ad-d8f5-a668-fc1f611fb834.png)

## インタラクティブな条件式の変更

``alt.condition()`` や ``.transform_filter()`` の引数である条件式は リアルタイムのカーソルの動きによって変更可能です。この条件式を変更させることができるオブジェクトが``alt.selection()`` です。このオブジェクトは``alt.Chart()`` のメソッドである``.add_selection()``の引数です。


## Binding による変更

### alt.condition()

``alt.selection_single()`` は、図の効果によってユーザーの変更を受けとるものと、プルダウンやスライダーといった別のオブジェクトの変更を受けとるものがあります。後者の場合 [binding](https://altair-viz.github.io/user_guide/interactions.html#binding-adding-data-driven-inputs) すなわち ``alt.binding()`` を引数にとります。


```python:.alt.condition()
score_slider = alt.binding_range(min=0, max=100, step=1, name="score:")
score_selection = alt.selection_single(
    fields=["合格点"], bind=score_slider, init={"合格点": 50}
)

(
    alt.Chart(long_df)
    .transform_filter(alt.datum.科目 == "国語")
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
            axis=alt.Axis(title="国語の得点"),
        ),
        color=alt.condition(
            predicate=alt.datum.得点 > score_selection.合格点,
            if_true=alt.value("steelblue"),
            if_false=alt.value("gray"),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .add_selection(score_selection)
    .properties(width=500)
)
```

![スクリーンショット 2022-11-27 20.28.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/5809ed1d-1fd3-8466-d3eb-775aee8dedbe.png)

### .transform_filter()

``.transform_filter()`` も ``alt.selection_single()`` によって変更可能です。下記例では各科目でX軸の学生順も変わる点に注意してください。

```python:.transform_filter()
subject_dropdown = alt.binding_select(options=sub)
subject_selection = alt.selection_single(
    fields=["科目"], bind=subject_dropdown, name="subject", init={"科目": "国語"}
)

(
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labels=False, title=None),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
        ),
        color=alt.condition(
            predicate=alt.datum.得点 > score_selection.合格点,
            if_true=alt.value("steelblue"),
            if_false=alt.value("gray"),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .add_selection(score_selection)
    .add_selection(subject_selection)
    .transform_filter(subject_selection)
    .properties(width=500)
)
```

![スクリーンショット 2022-11-27 20.32.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/84438d78-1660-b8f5-ae45-82ac6c7bfe49.png)


## 図の操作による変更

``alt.selection_single()`` で上記のように ``bind`` を引数にとらない場合、自身の結合元の図の操作による変更を受けとります。下記例では ``fields=["学生番号"]`` としているため、図でクリックされた学生番号のレコードが True として、それ以外の学生番号のレコードが False として処理されます。

```python:図の操作による変更
student_selection = alt.selection_single(fields=["学生番号"], init={"学生番号": id_lst[0]})

(
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
        ),
        color=alt.condition(
            predicate=student_selection,
            if_true=alt.value("steelblue"),
            if_false=alt.value("lightgray"),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .add_selection(student_selection)
    .add_selection(subject_selection)
    .transform_filter(subject_selection)
    .properties(width=500)
)
```

![スクリーンショット 2022-11-27 20.34.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/dec714f6-98db-f20a-67a7-f82acc04807f.png)

![スクリーンショット 2022-11-27 20.35.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/69754493-567a-270c-ea53-b9d9197f2dd3.png)

# 連動する図の作成

下記例では、上の棒グラフをドラッグした領域の学生数を下の棒グラフが集計します。

```python:連動する図の作成
student_brush_selection = alt.selection(type="interval", encodings=["x"])
color_by_sex = alt.Color(
    "性別:N",
    scale=alt.Scale(domain=sex, range=["steelblue", "darkorange"]),
)

upper = (
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
        ),
        color=alt.condition(
            predicate=student_brush_selection,
            if_true=color_by_sex,
            if_false=alt.value("lightgray"),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .add_selection(student_brush_selection)
    .add_selection(subject_selection)
    .transform_filter(subject_selection)
    .properties(width=500)
)

lower = (
    alt.Chart(long_df)
    .mark_bar()
    .encode(
        x=alt.X("count():Q", title="人数", scale=alt.Scale(domain=[0, N // 2])),
        y=alt.Y("コース:N"),
        color=color_by_sex,
    )
    .transform_filter(subject_selection)
    .transform_filter(student_brush_selection)
    .properties(width=500, height=150)
)

upper & lower
```

![スクリーンショット 2022-11-27 20.53.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/8107c3c8-01f6-47b2-14b8-589f937bf366.png)


# VConcatChart の活用

複数の図で共通の処理は ``alt.VConcatChart()`` でまとめることもできる。上記例は ``data=long_df`` と``.transform_filter(subject_selection)`` が2つのグラフで共通であったため``alt.VConcatChart()``を活用して下記のようにまとめることも可能である。

```python:VConcatChart の活用
student_brush_selection = alt.selection(type="interval", encodings=["x"])
color_by_sex = alt.Color(
    "性別:N",
    scale=alt.Scale(domain=sex, range=["steelblue", "darkorange"]),
)

upper = (
    alt.Chart()
    .mark_bar()
    .encode(
        x=alt.X(
            "学生番号:N",
            sort="-y",
            axis=alt.Axis(labelAngle=-90),
        ),
        y=alt.Y(
            "得点:Q",
            scale=alt.Scale(domain=[0, 100]),
        ),
        color=alt.condition(
            predicate=student_brush_selection,
            if_true=color_by_sex,
            if_false=alt.value("lightgray"),
        ),
        tooltip=[
            alt.Tooltip(field="得点"),
            alt.Tooltip(field="学生番号"),
        ],
    )
    .add_selection(student_brush_selection)
    .add_selection(subject_selection)
    .properties(width=500)
)

lower = (
    alt.Chart()
    .mark_bar()
    .encode(
        x=alt.X("count():Q", title="人数", scale=alt.Scale(domain=[0, N // 2])),
        y=alt.Y("コース:N"),
        color=color_by_sex,
    )
    .transform_filter(student_brush_selection)
    .properties(width=500, height=150)
)

(
    alt.VConcatChart(
        data=long_df,
        vconcat=(upper,lower)
    ).transform_filter(subject_selection)
)
```

# いろいろ調べてみよう

Altair には様々な機能があります！公式ページや[【時系列データ編】](https://keisuke-h-ota.github.io/altair_practice/time_series.html)などを参考にインタラクティブなグラフを作成してみましょう！

```python
subject_mouse_selection = alt.selection(
    type="single", fields=["科目"], on="mouseover", nearest=True, init={"科目": "国語"}
)

base = alt.Chart(long_df).encode(
    x=alt.X(
        "学生番号:N",
        axis=alt.Axis(labelAngle=-90),
    ),
    y=alt.Y(
        "得点:Q",
        scale=alt.Scale(domain=[0, 100]),
    ),
    detail=alt.Detail("科目:N"),
    tooltip=[
        alt.Tooltip(field="得点"),
        alt.Tooltip(field="科目"),
        alt.Tooltip(field="学生番号"),
    ],
)

points = (
    base.mark_circle()
    .encode(
        opacity=alt.condition(
            predicate=subject_mouse_selection,
            if_true=alt.value(1),
            if_false=alt.value(0),
        ),
    )
    .add_selection(subject_mouse_selection)
)

lines = base.mark_line().encode(
    color=alt.condition(
        predicate=subject_mouse_selection,
        if_true=alt.value("steelblue"),
        if_false=alt.value("lightgray"),
    ),
    opacity=alt.condition(
        predicate=subject_mouse_selection,
        if_true=alt.value(1),
        if_false=alt.value(0.5),
    ),
)

text = (
    alt.Chart()
    .mark_text(align="center", dx=0, dy=-170, fontSize=18)
    .encode(
        text=alt.Text("科目:N"),
        opacity=alt.condition(subject_mouse_selection, alt.value(1), alt.value(0)),
    )
)

(points + lines + text).properties(width=500)
```

![visualization (17).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/5d25eb7f-f8dc-23c3-acd2-d03909cbe0ea.png)

# 5000 行以上のテーブルを入力する場合

altair で下記 Error が表示される場合は入力レコードの上限を変更してください。


> altair.utils.data.MaxRowsError: 
The number of rows in your dataset is greater than the maximum allowed (5000). 
For information on how to plot larger datasets in Altair, see the documentation

```python:5000 行以上のテーブルを入力する場合
# 入力レコードの上限の変更
from altair import limit_rows, to_values
import toolz
t = lambda data: toolz.curried.pipe(data, limit_rows(max_rows=10000), to_values)
alt.data_transformers.register("custom", t)
alt.data_transformers.enable("custom")
```

# Altair チート集
随時追加していきます。

##  ストリッププロット（ジッタープロット）
プロットは学生番号に対応しています。点の粗密で分布を確認できます。

```python:ストリッププロット（ジッタープロット）
(
    alt.Chart(long_df)
    .mark_circle(size=8)
    .encode(
        x=alt.X(
            "jitter:Q",
            title=None,
            axis=alt.Axis(values=[0], ticks=True, grid=False, labels=False),
            scale=alt.Scale(),
        ),
        y=alt.Y("得点:Q"),
        column=alt.Column("科目:N"),
    )
    .transform_calculate(
        # Generate Gaussian jitter with a Box-Muller transform
        jitter="sqrt(-2*log(random()))*cos(2*PI*random())"
    )
    .properties(width=50)
)
```

![visualization (18).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/fce53985-fe14-95b1-0a21-bcdf9285cd5b.png)

## ヴァイオリンプロット
図形の幅で分布を表しています。人口ピラミットなどに用いられています。

```python:ヴァイオリンプロット
x_range = [0, 100]
bin_range = 10
(
    alt.Chart(long_df)
    .transform_density("得点", as_=["得点", "density"], extent=x_range, groupby=["科目"])
    .mark_area(orient="horizontal")
    .encode(
        x=alt.X(
            "density:Q",
            stack="center",
            impute=None,
            title=None,
            axis=alt.Axis(labels=False, values=[0], grid=False, ticks=True),
        ),
        y="得点:Q",
        color=alt.Color("科目:N", legend=None),
        column=alt.Column(
            "科目:N",
            header=alt.Header(
                titleOrient="bottom",
                labelOrient="bottom",
                labelPadding=0,
                labelFontSize=15,
                titleFontSize=18,
            ),
        ),
    )
    .configure_facet(spacing=0)
    .configure_view(stroke=None)
    .properties(width=100, height=300)
)
```

![visualization (19).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/6f81cf6c-75f2-0c10-6fdb-859d65badce1.png)

## リッジライン
図形の高さで分布を表しています。

```python:リッジライン
step = 50
overlap = 0.5
x_range = [0, 100]
bin_range = 20

(
    alt.Chart(long_df)
    .transform_bin(as_="ビン", field="得点", bin=alt.Bin(step=bin_range, extent=x_range))
    .transform_aggregate(y_axis="count()", groupby=["科目", "ビン"])
    .transform_impute(
        impute="y_axis", groupby=["科目"], key="ビン", value=0, keyvals=x_range
    )
    .mark_area(
        interpolate="monotone", fillOpacity=0.6, stroke="lightgray", strokeWidth=0.5
    )
    .encode(
        alt.X(
            "ビン:Q",
            title="得点",
            axis=alt.Axis(grid=False),
            scale=alt.Scale(domain=x_range),
        ),
        alt.Y(
            "y_axis:Q",
            stack=None,
            title=None,
            axis=None,
            scale=alt.Scale(range=[step, -step * overlap]),
        ),
        alt.Fill("科目:N", legend=None),
        alt.Row("科目:N", title=None, header=alt.Header(labelAlign="left")),
    )
    .properties(bounds="flush", width=400, height=int(step))
    .configure_facet(spacing=0)
    .configure_view(stroke=None)
    .configure_title(anchor="end")
)
```

![visualization (20).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/617a9d30-0b3f-3a42-6095-6ce17dde7b31.png)