【Python】グラフ作成ライブラリ Altair の使い方

# 更新版

https://qiita.com/keisuke-ota/items/aa93f45b3a9fc520541d

# 概要

本稿では、Python グラフ作成ライブラリ [Altair](https://altair-viz.github.io/index.html) を用いて様々なグラフを描画してみる。Altair は Pandas の DataFrame でデータを入力するのが特徴である。

# テストデータ

本稿では、Kaggle で公開されている[タイタニック号の乗客データベース](https://www.kaggle.com/c/titanic/data?select=train.csv)を用いた。データ形式は以下のとおりである。

```train.csv
PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked
1,0,3,"Braund, Mr. Owen Harris",male,22,1,0,A/5 21171,7.25,,S
2,1,1,"Cumings, Mrs. John Bradley (Florence Briggs Thayer)",female,38,1,0,PC 17599,71.2833,C85,C
3,1,3,"Heikkinen, Miss. Laina",female,26,0,0,STON/O2. 3101282,7.925,,S
4,1,1,"Futrelle, Mrs. Jacques Heath (Lily May Peel)",female,35,1,0,113803,53.1,C123,S
5,0,3,"Allen, Mr. William Henry",male,35,0,0,373450,8.05,,S
6,0,3,"Moran, Mr. James",male,,0,0,330877,8.4583,,Q
```

![スクリーンショット 2020-11-17 18.30.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/d27b85a7-d513-52d9-3167-fe4985ba1d5c.png)


データの見方を以下に示す。[参考](https://www.codexa.net/kaggle-titanic-beginner/)

* PassengerId – 乗客識別ユニークID
* Survived – 生存フラグ（0=死亡、1=生存）
* Pclass – チケットクラス
* Name – 乗客の名前
* Sex – 性別（male=男性、female＝女性）
* Age – 年齢
* SibSp – タイタニックに同乗している兄弟/配偶者の数
* parch – タイタニックに同乗している親/子供の数
* ticket – チケット番号
* fare – 料金
* cabin – 客室番号
* Embarked – 出港地（タイタニックへ乗った港）

# Altair のインストール

pip でインストール可能。

```terminal:ターミナル
pip install altair
```

# 環境

* Python: ver3.8.4
* Altair: ver4.1.0 

# 散布図: Scatter plot

Altair は、散布図を作成するのに最も適したライブラリである。数値データでも、``:O``を追加するとカテゴリーデータとして処理される。

```python:altair_demo.py
import os
import altair as alt
import pandas as pd

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

scatter_plot = alt.Chart(df).mark_circle().encode(
    x=alt.X('Age'),
    y=alt.Y('Fare'),
    column=alt.Column('Survived:O'),
    color=alt.Color('Sex', sort=['male', 'female']),
    tooltip=['Age', 'Fare', 'Name'],
    size=alt.Size('Pclass:O')
).properties(
	width=600,
	height=500
).interactive()

scatter_plot.show()
```

![スクリーンショット 2020-10-18 23.02.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/e54e9728-fe41-9169-3cd5-de04cfbf9001.png)

# 線形回帰直線

線分を引く場合、始点と終点の座標を DataFrame に作成すると、結ぶことができる。
線形回帰の切片と傾きは、[sckit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html) で求める。

```python:altair_demo.py
import os
import altair as alt
import pandas as pd
from sklearn.linear_model import LinearRegression

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

# 欠損値を含む行を削除する

linear_df = df.dropna(subset=['Age', 'Fare'], how='any', axis=0)

# 線形回帰モデルを作成

linear = LinearRegression(
    ).fit(linear_df['Age'].values.reshape(-1,1), linear_df['Fare'].values.reshape(-1,1))

# パラメータの決定

a = linear.coef_[0]
b = linear.intercept_

# 閾値の決定

x_min = df['Age'].min()
x_max = df['Age'].max()

# データフレームの作成

linear_points = pd.DataFrame({
    'Age': [x_min, x_max],
    'Fare': [a*x_min+b, a*x_max+b],
}).astype(float)

linear_line = alt.Chart(linear_points).mark_line(color='steelblue').encode(
    x=alt.X('Age'),
    y=alt.Y('Fare')
    ).properties(
    width=500,
    height=500
    ).interactive()

linear_line.show()
```
![スクリーンショット 2020-10-18 23.07.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/e5817609-a4c1-8edc-d04b-9217d23b2e70.png)

## 図の重ね合わせ

散布図と重ねて表示させることも可能である。

```python:altair_demo.py
import os
import altair as alt
import pandas as pd

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

scatter_plot = alt.Chart(df).mark_circle(size=50).encode(
    x=alt.X('Age'),
    y=alt.Y('Fare'),
).properties(
    width=500,
    height=500
).interactive()

linear_line = 上と同じ（省略）

(scatter_plot + linear_line).show()
```
![スクリーンショット 2020-10-19 10.24.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/a88ea074-87ce-ca13-ec72-ecc0a14d4dac.png)



# 箱ひげ図: Boxplot

詳細は[こちら](https://qiita.com/keisuke-ota/items/cfd5d0cf835b99d24edd)。

```python:altair_demo.py
import os
import altair as alt
import pandas as pd

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

boxplot = alt.Chart(df.dropna(subset=['Embarked'], how='any', axis=0)).mark_boxplot(size=100,ticks=alt.MarkConfig(width=30), median=alt.MarkConfig(color='black',size=100)).encode(
    x=alt.X('Survived:O',axis=alt.Axis(labelFontSize=15, ticks=True, titleFontSize=18, title='Survive',labelAngle=0)),
    y=alt.Y('Fare', axis=alt.Axis(labelFontSize=15, ticks=True, titleFontSize=18, title='Fare',labelAngle=0)),
    column=alt.Column('Embarked', sort=['S','Q','C'], header=alt.Header(labelFontSize=15, labelAngle=0, titleFontSize=18)),
    row=alt.Row('Sex', sort=['male','female'], header=alt.Header(labelFontSize=15, labelAngle=-90, titleFontSize=18)),
    color=alt.Color('Sex', sort=['male', 'female'])
).properties(
	width=600,
	height=500
).interactive()

boxplot.show()
```

![visualization-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/71cfa4b7-4a27-cb6a-267d-9827d5b48fff.png)


# ヒストグラム: Histgram

Y 軸を ``count()`` とすることで、要素を数え上げてくれる。``alt.X()``で bin の設定が可能である。

```python:altair_demo.py
import os
import altair as alt
import pandas as pd

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

histgram = alt.Chart(df).mark_bar().encode(
    x=alt.X("Age", bin=alt.Bin(step=10,extent=[0,90]),axis=alt.Axis(labelFontSize=15, ticks=True, titleFontSize=18, title='Age')),
    y=alt.Y('count()', axis=alt.Axis(labelFontSize=15, ticks=True, titleFontSize=18, title='Frequency')),
    column=alt.Column('Survived:O'),
    row=alt.Row('Sex', sort=['male','female']),
    color=alt.Color('Sex', sort=['male', 'female']),
    opacity=alt.Opacity('Sex', sort=['male', 'female'])
    ).properties(
	width=600,
	height=500
	).interactive()

histgram.show()
```
![visualization-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/5cb6f1d9-64b2-052a-b7c6-f3af4a037b33.png)

# ヒートマップ: Heatmap

```python:altair_demo.py
import os
import altair as alt
import pandas as pd

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

df = df.groupby(['Sex','Embarked']).mean().reset_index()

heatmap = alt.Chart(df).mark_rect().encode(
    x=alt.X('Sex:O',axis=alt.Axis(labelFontSize=20, ticks=True, titleFontSize=20, title='Sex',labelAngle=0)),
    y=alt.Y('Embarked:O', axis=alt.Axis(labelFontSize=20, ticks=True, titleFontSize=20, title='Embarked',labelAngle=0)),
    color='Survived:Q',
    tooltip=['Sex', 'Embarked', 'Survived']
    ).properties(
    width=600,
    height=500
    ).interactive()

heatmap.show()
```

![visualization-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/f77c0efc-5add-cab5-47a2-81f3b947b847.png)

# 折れ線グラフ: Line plot

Altair は、melt 関数を用いて dataframe を入力する。

```python:altair_demo.py
import os
import altair as alt
import pandas as pd

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

Embarked_order = ['S','C','Q']

line_df = df.groupby(['Embarked']).mean().reset_index()
line_df['order']=line_df['Embarked'].apply(lambda x: Embarked_order.index(x))
line_df = pd.melt(line_df,id_vars=['Age','Embarked','order'],var_name='index',value_name='values')
line_df['Age'] = line_df['Age'].round()
line_df['values'] = line_df['values'].round()

line = alt.Chart(line_df).mark_line().encode(
    x=alt.X('Age:O',axis=alt.Axis(labelFontSize=20, ticks=True, titleFontSize=20, title='Age',labelAngle=0)),
    y=alt.Y('values:O', axis=alt.Axis(labelFontSize=20, ticks=True, titleFontSize=20, title='values',labelAngle=0)),
    color='index',
    order='order'
    ).properties(
    width=600,
    height=500
    ).interactive()

line.show()
```

![visualization-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/c72a1c30-3f52-cee4-3482-bd401ca13a64.png)


# 図の保存方法
## HTML と VEGA
``altair_saver`` をインストールすると html や VEGA で保存できる。

```terminal:ターミナル
pip install altair_saver
```
``.interactive()`` メソッドでグラフが自由に動かせるようになる。この性質は保存先でも保たれる。

```python:altair_demo.py
import os
import altair as alt
import pandas as pd
from altair_saver import save

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

boxplot = alt.Chart(df.dropna(subset=['Embarked'], how='any', axis=0)).mark_boxplot().encode(
    x=alt.X('Survived:O'),
    y=alt.Y('Fare')).interactive()

save(boxplot,"fig.html") # HTML
save(boxplot,"fig.vl.json") # VEGA-Lite
save(boxplot,"fig.vg.json") # VEGA
```

ただし VEGA で保存する場合は ``npm`` で以下をインストールする必要がある。

```terminal
npm install vega-lite vega-cli canvas
```
[Github](https://github.com/altair-viz/altair_saver)も参考にするとよい。

## PNG と SVG
``chromedriver-binary`` をインストールしをインポートして保存する。バージョンはブラウザの Chrome と合わせておく。

```terminal
pip install chromedriver-binary
```

```python:altair_demo.py
import os
import altair as alt
import pandas as pd
from altair_saver import save
import chromedriver_binary

cwd = os.getcwd()
path = ['train.csv']
file = os.path.join(cwd, *path)

df = pd.read_table(file, sep=',', index_col=0 ,header=0)

boxplot = alt.Chart(df.dropna(subset=['Embarked'], how='any', axis=0)).mark_boxplot().encode(
    x=alt.X('Survived:O'),
    y=alt.Y('Fare')).interactive()

boxplot.save('boxplot.png')
boxplot.save('boxplot.svg')
```

## 

# 応用例

[Streamlit](https://qiita.com/keisuke-ota/items/a18f158389f1585a9aa0) と組み合わせることで、様々なデータ分析アプリケーションを作成することができる。

* [scikit-learn を用いたロバスト線形回帰](https://qiita.com/keisuke-ota/items/fdd9c28b648af46d1264)
* [MetaboanalystR を用いたメタボロームデータ解析](https://qiita.com/keisuke-ota/items/803c4299138b169eb9a2)