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

【基礎編】からご覧になることをおすすめします。

本稿ではグラフ可視化ライブラリ Altair を用いて時系列データの可視化を行います。

各種インポート

In [1]:
import urllib
import json
import pandas as pd
import altair as alt

可視化用デモデータ読込

2000〜2010年の米国業種別失業者数(千人)count および失業率rateをデモデータとして使用します。

In [2]:
data_path = "https://cdn.jsdelivr.net/npm/vega-datasets@1.29.0/data/unemployment-across-industries.json"
In [3]:
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
Out[3]:
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

1708 rows × 6 columns

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

In [4]:
df.dtypes
Out[4]:
series                 object
year                    int64
month                   int64
count                   int64
rate                  float64
date      datetime64[ns, UTC]
dtype: object

テーマの設定

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

In [5]:
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")
Out[5]:
ThemeRegistry.enable('font_config')

折れ線グラフ

時系列データはtype=temporalで指定可能です。

In [6]:
(
    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分"),
        ],
    )
)
Out[6]:

マウスオーバーによるハイライト

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

In [7]:
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)
Out[7]:

ドロップダウンによるハイライト

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

In [8]:
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)
Out[8]:

全期間の平均を表示

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

In [9]:
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)
)
Out[9]:

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

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

In [10]:
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)
)
Out[10]:

回帰分析を表示

trans_regression()で回帰分析が可能です。method回帰方法を選べます。線形回帰はmethod=linearです。

In [11]:
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
Out[11]:

エラーバンドの表示

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

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

In [12]:
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
Out[12]:

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

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

In [13]:
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)
)
Out[13]:

正規化積みあげ面グラフ

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

In [14]:
(
    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)
        ),
    )
)
Out[14]:

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

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

In [15]:
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)
)
Out[15]:

選択した項目だけで正規化

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

In [16]:
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
Out[16]:

選択期間の拡大

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

In [17]:
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
Out[17]:

©︎ 2022 keisuke ohta