【Python】データ可視化ライブラリ Altair ハンズオン【時系列データ編】

# 概要

※[【基礎編】](https://qiita.com/keisuke-ota/items/9b51c4352f93bd755b15)からご覧になることをおすすめします。

本稿ではグラフ可視化ライブラリ [Altair](https://altair-viz.github.io/index.html) を用いた時系列データの可視化を紹介させていただきます。。

# ハンズオン公開

[GitHub Pages](https://keisuke-h-ota.github.io/altair_practice/time_series.html)、[.ipynbファイル](https://github.com/Keisuke-H-Ota/altair_practice/blob/master/time_series.ipynb)、[Google colab](https://colab.research.google.com/drive/1LWUVrHDkba7AJzUf2AdIgXlwE17eFqQT)でハンズオンを公開しています。よろしければご活用ください。


# 各種インポート

```python
import urllib
import json
import pandas as pd
import altair as alt
```

# 可視化用デモデータ読込
2000〜2010年の米国業種別失業者数（千人）``count`` および失業率``rate``をデモデータとして使用します。


```python:可視化用デモデータ読込
data_path = "https://cdn.jsdelivr.net/npm/vega-datasets@1.29.0/data/unemployment-across-industries.json"

with urllib.request.urlopen(data_path) as f:
    unemployment_across_industries = f.read().decode("cp932").split("\n")[0]
dict_data = json.loads(unemployment_across_industries)
keys = list(dict_data[0].keys())
df = pd.DataFrame([[row[key]for key in keys] for row in dict_data], columns=keys)
df["date"] = pd.to_datetime(df["date"])
df
```

```:df
	series	year	month	count	rate	date
0	Government	2000	1	430	2.1	2000-01-01 08:00:00+00:00
1	Government	2000	2	409	2.0	2000-02-01 08:00:00+00:00
2	Government	2000	3	311	1.5	2000-03-01 08:00:00+00:00
3	Government	2000	4	269	1.3	2000-04-01 08:00:00+00:00
4	Government	2000	5	370	1.9	2000-05-01 07:00:00+00:00
...	...	...	...	...	...	...
1703	Self-employed	2009	10	610	5.9	2009-10-01 07:00:00+00:00
1704	Self-employed	2009	11	592	5.7	2009-11-01 07:00:00+00:00
1705	Self-employed	2009	12	609	5.9	2009-12-01 08:00:00+00:00
1706	Self-employed	2010	1	730	7.2	2010-01-01 08:00:00+00:00
1707	Self-employed	2010	2	680	6.5	2010-02-01 08:00:00+00:00
```

``date``列のデータ型は``datetime64[ns, UTC]``です。

```python:df.types
series                 object
year                    int64
month                   int64
count                   int64
rate                  float64
date      datetime64[ns, UTC]
dtype: object
```


# テーマの設定

<details><summary>【基礎編】と同様に全グラフ共通のテーマを設定します。</summary><div>

```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,
                # "titleAngle": 0, # Axis のtitleAngleは encode がきかなくなる
            },
            # 色分けした際の項目
            "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": 500, "height": 300},
            # 図の背景
            "background": "white",
        }
    }


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

</div></details>


# 折れ線グラフ
時系列データは``type=temporal``で指定可能です。

```python:折れ線グラフ
(
    alt.Chart(df)
    .mark_line()
    .encode(
        x=alt.X(field="date",type="temporal"),
        y=alt.Y(field="count",type="quantitative"),
        color=alt.Color(
            field="series",type="nominal",
            scale=alt.Scale(scheme="category20b"), # デフォルトより色のバリエーション数が多いです。
            legend=alt.Legend(labelFontSize=10)
        ),
        tooltip=[
            alt.Tooltip(field="count",type="temporal"),
            alt.Tooltip(field="series",type="nominal"),
            alt.Tooltip(field="date",type="temporal", format="%Y年%m月%d日 %H時%M分"),
        ],
    )
)
```

![visualization (22).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/1de8e8c0-618c-fee3-47b3-158e33292cd0.png)


# マウスオーバーによるハイライト
Altair ではマウスに最も近いポイントをハイライトさせることができます。``.alt_selection()`` でマウスの位置に近いレコードの ``series`` を表示させるために ``base`` で ``detail="series"`` としているところがポイントです。``detail`` がないと特定のレコードのみを抽出してハイライトさせることができません。

```python:マウスオーバーによるハイライト
series_mouse_selection = alt.selection(
    type="single", fields=["series"], on="mouseover", nearest=True, init={"series": "Agriculture"}
)

base = alt.Chart(df).encode(
    x=alt.X(field="date",type="temporal"),
    y=alt.Y(field="count",type="quantitative"),
    detail=alt.Detail(field="series",type="nominal"),
    tooltip=[
        alt.Tooltip(field="count",type="temporal"),
        alt.Tooltip(field="series",type="nominal"),
        alt.Tooltip(field="date",type="temporal", format="%Y年%m月%d日 %H時%M分"),
    ],
)

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

lines = base.mark_line().encode(
    color=alt.condition(
        predicate=series_mouse_selection,
        if_true=alt.value("steelblue"),
        if_false=alt.value("lightgray"),
    ),
    opacity=alt.condition(
        predicate=series_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("series:N"))
    .transform_filter(series_mouse_selection)
)

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

![visualization (23).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/6fe5cf58-be07-c5ad-aac5-fe30c5aafa1e.png)

# ドロップダウンによるハイライト
Altair ではドロップダウンで選択した項目のみをハイライトさせることができます。

```python:ドロップダウンによるハイライト
series_dropdown = alt.binding_select(options=sorted(set(df["series"])))
series_selection = alt.selection_single(
    fields=["series"], bind=series_dropdown, name="_", init={"series": "Construction"}
)

line_chart = (
    alt.Chart(df)
    .mark_line()
    .encode(
        x=alt.X(field="date",type="temporal"),
        y=alt.Y(field="count",type="quantitative"),
        detail=alt.Detail(field="series",type="nominal"),
        color=alt.condition(
            predicate=series_selection,
            if_true=alt.value("steelblue"),
            if_false=alt.value("lightgray"),
        ),
        opacity=alt.condition(
            predicate=series_selection,
            if_true=alt.value(1),
            if_false=alt.value(0.5),
        ),
    )
)
line_chart.add_selection(series_selection)
```

![スクリーンショット 2022-11-27 22.44.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/91a7981e-c182-3993-644b-4c52f845d706.png)


# 全期間の平均を表示

複数のグラフを重ねて表示させることも可能です。

```python:全期間の平均を表示
avg_line = (
    alt.Chart(df)
    .mark_rule(color="darkorange",size=2.5)
    .encode(
        y=alt.Y("mean(count):Q",axis=alt.Axis(title=None)),
        strokeDash=alt.value([7, 7]),  # dashed line: 7 pixels dash + 7 pixels space
    )
)

text = (
    alt.Chart(df)
    .mark_text(align="left",dx=5,dy=-15,text="Average",color="darkorange")
    .encode(y="mean(count):Q")
)

(
    line_chart.add_selection(series_selection)
    + avg_line.transform_filter(series_selection)
    + text.transform_filter(series_selection)
)
```

## ドラックした区間の平均を表示

2009年付近をドラックすると、その頃に失業者数が増加していたことがわかります。

```python:ドラックした区間の平均を表示
date_brush = alt.selection(type="interval", encodings=["x"])

(
    line_chart.add_selection(series_selection).add_selection(date_brush)
    + avg_line.transform_filter(series_selection).transform_filter(date_brush)
    + text.transform_filter(series_selection).transform_filter(date_brush)
)
```

![スクリーンショット 2022-11-27 22.59.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/19acb548-d993-17c1-30b5-d67eff09275d.png)

## 回帰分析
``trans_regression()``で回帰分析が可能です。``method``で[回帰方法](https://altair-viz.github.io/user_guide/transform/regression.html?highlight=linear%20regression)を選べます。線形回帰は``method=linear``です。

```python:回帰分析
pr_line = (
    line_chart
    .transform_filter(series_selection)
    .transform_regression(on="date", regression="count",method="poly")
    .mark_line(size=3,opacity=1)
    .encode(
        strokeDash=alt.value([7, 7]),  # dashed line: 7 pixels dash + 7 pixels space
        color=alt.value("darkorange"),
    )
)

line_chart.add_selection(series_selection) + pr_line
```

![visualization (24).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/c3e91dae-e6f7-6b99-4565-3d54d6aaea86.png)

# エラーバンド

ばらつきの大きさは``.mark_errorband()``で表示可能です。``.mark_errorbar()``と同様にでエラーバーで表示する統計量の種類は ``extent``で指定しましょう。引数はデフォルトでstderr（標準誤差）です。他にはstderv（標準偏差）、ci（95%信頼区間）、iqr（四分位範囲）がとれます。

以下のエラーバーは産業別失業者数の95%信頼区間を表示しています。

```python:エラーバンド
line = alt.Chart(df).mark_line().encode(x="date:T", y="mean(count):Q")

band = alt.Chart(df).mark_errorband(extent="ci").encode(x="date:T", y="count:Q")

band + line
```

![visualization (25).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/94a8aff3-ab98-3a58-4d7c-5ee366fc9d03.png)

# スライダーで選択した年の可視化

特定の1年間を可視化する手法です。

```python:
year_slider = alt.binding_range(min=2000, max=2009, step=1)
year_selection = alt.selection_single(name="year", fields=["year"],
                                      bind=year_slider, init={"year": 2000})

(
    alt.Chart(df)
    .mark_line()
    .encode(
        x=alt.X("yearmonth(date)",type="temporal",
                axis=alt.Axis(title="Year Month",labelAngle=-45)
               ),
        y=alt.Y(field="count",type="quantitative"),
        color=alt.Color(
            field="series",type="nominal",
            scale=alt.Scale(scheme="category20b"),
            legend=alt.Legend(labelFontSize=10)
        ),
        tooltip=[
            alt.Tooltip(field="count",type="temporal"),
            alt.Tooltip(field="series",type="nominal"),
            alt.Tooltip(field="date",type="temporal", format="%Y年%m月%d日 %H時%M分"),
        ],
    )
    .add_selection(year_selection)
    .transform_filter(year_selection)
)
```

2008年を選択した例です。

![スクリーンショット 2022-11-27 23.04.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/bb6c3eed-0f5f-2597-3c47-6f322d7b0302.png)

## 正規化積みあげ面グラフ

棒グラフと同様に、面グラフも ``stack="normalize"`` で正規化できます。

```python:正規化積みあげ面グラフ
(
    alt.Chart(df)
    .mark_area()
    .encode(
        x="date:T",
        y=alt.Y("count:Q", stack="normalize"),
        color=alt.Color("series:N",
            scale=alt.Scale(scheme="category20b"),
            legend=alt.Legend(labelFontSize=10)
        ),
    )
)
```

![visualization (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/47e85b31-a005-7dfb-1015-f879d0bdd8f5.png)


# 凡例で選択した項目のハイライト
**凡例**でクリックした項目がハイライトされます。2個目以降は**shiftキーを押しながら**クリックしてください。

```python:凡例で選択した項目のハイライト

series_multi_selection = alt.selection_multi(fields=["series"], bind="legend")

(
    alt.Chart(df)
    .mark_area()
    .encode(
        x="date:T",
        y=alt.Y("count:Q", stack="normalize"),
        color=alt.Color("series:N",
            scale=alt.Scale(scheme="category20b"),
            legend=alt.Legend(labelFontSize=10)
        ),
        opacity=alt.condition(series_multi_selection, alt.value(1), alt.value(0.2))
    )
    .add_selection(series_multi_selection)
)
```

![スクリーンショット 2022-11-27 23.08.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/795dbfa2-7bd2-20f0-02a4-59ef418745a3.png)

# 選択した項目だけで正規化
**凡例**でクリックした項目のみで正規化されます。2個目以降は**shiftキーを押しながら**クリックしてください。

```python:選択した項目だけで正規化
base = (
    alt.Chart(df)
    .mark_area()
    .encode(
        x="date:T",
        y=alt.Y("count:Q", stack="normalize"),
        color=alt.Color("series:N",
            scale=alt.Scale(scheme="category20b"),
            legend=alt.Legend(labelFontSize=10)
        ),
    )
)

background = base.mark_area(opacity=0)
foreground = base.add_selection(series_multi_selection).transform_filter(series_multi_selection)

background + foreground
```

![スクリーンショット 2022-11-27 23.10.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/f5e4cdbd-a78a-48a2-80dc-119135860dee.png)

## 選択期間の拡大
下の横長グラフでドラックした区間を上のグラフで表示させることが可能です。

```python:選択期間の拡大
base = (
    alt.Chart(df)
    .encode(
        x=alt.X(field="date",type="temporal"),
        y=alt.Y(field="count",type="quantitative"),
        color=alt.Color(
            field="series",type="nominal",
            scale=alt.Scale(scheme="category20b"),
            legend=alt.Legend(labelFontSize=10)
        ),
        tooltip=[
            alt.Tooltip(field="count",type="temporal"),
            alt.Tooltip(field="series",type="nominal"),
            alt.Tooltip(field="date",type="temporal", format="%Y年%m月%d日 %H時%M分"),
        ],
    )
)

upper = (
    base.mark_line()
    .encode(alt.X("date:T",scale=alt.Scale(domain=date_brush)))
    .properties(height=300)
)

lower = (
    base
    .mark_area()
    .properties(height=100)
    .add_selection(date_brush)
)

upper & lower
```
2007年以降を拡大させた場合、以下のようになります。

![スクリーンショット 2022-11-27 23.13.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612858/7e0bcc56-f019-90e2-4056-de730291ddc0.png)