20
25

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-05

概要

本稿ではグラフ可視化ライブラリ Altair の使い方を紹介させて頂きます。Altair はデータをインタラクティブに可視化するために開発された json のフォーマット Vega-Lite を Python から記述することができるライブラリです。

ぜひ【時系列データ編】も参照してみてください。

ハンズオン公開

Google ColabGitHub Pages でハンズオンを公開しています。よろしければご活用ください。

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

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

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

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

データ作成

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

データ作成
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	普通	男

インストール

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

$ pip install altair altair_saver

conda でもインストールできます。詳細は公式ドキュメントを参照してください。

google colab の場合

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

! pip install -U requests
! pip install altair_saver

グラフの構成

altair で作られるグラフは chart object というクラスから生成されたオブジェクトです。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().encode()という2つのメソッドが不可欠です。それぞれの役割は以下の通りです。

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

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

散布図
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

図の保存

altair_saver.save()を使えば html または json で保存可能です。

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

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

inline=Falseにすると vegavega-litevega-embed を外部ファイル化できます。すなわち下記3ファイルを外部から読み込むことでファイルサイズを節約できます。

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

他形式の保存

png、svg、形式の保存はグラフ右上の三点印embed_optionsから可能ですが解像度は調節できません。 altair_saver を使用すれば png、svg、そして pdf でも高解像度で保存可能です。
ただ Jupyter 上でこれを実行するためには更なるセットアップが必要です。詳しくはこちらを参考にしてください。

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

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

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

alt.Scale()alt.X() の引数では軸の値を指定するのに対し、alt.Color()ではプロットの色を指定しています。上記のように channel ごとに仕様が異なる option については特に注意しましょう。なお色はalt.ColorName()またはカラーコードで指定できます。

グラフのサイズ指定

グラフのサイズは.properties()で指定します。ここではグラフのタイトルも指定できます。

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

visualization (3).png

テーマの設定

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

テーマの設定
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 ではそのカラムの変数の型を指定する必要があります。上記のように指定せずとも自動で補完されることもありますが、指定しないとエラーが発生することもあります。指定可能な変数の型は以下のとおりです。

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

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

変数型の指定
(
    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

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

縦長テーブルへの変換

RDB には横長もしくは縦長というデータの構造があります。これはメタデータをどのように持つかの違いです。

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

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

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

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

縦長データへの変形
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()と異なります。

(
    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

上の箱ひげ図をカーソルでなぞると中央値などの統計量がポップアップされます。この機能は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 関数は使用可能です。

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

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

エラーバーの挿入

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

エラーバーの挿入
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

棒グラフ(積み上げ)

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

棒グラフ(積み上げ)
(
    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

棒グラフ(並列)

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

棒グラフ(並列)
(
    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

対数グラフ

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

対数グラフ
(
    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

割合グラフ

stack="normalize"で割合を算出することができます。

割合グラフ
(
    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

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

割合グラフ(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

積み上げヒストグラム

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

積み上げヒストグラム
(
    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

重ね合わせヒストグラム

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

重ね合わせヒストグラム
(
    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

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

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

条件式によるレコードの抽出
(
    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

条件式による分岐

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

条件式による分岐
(
    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

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

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

Binding による変更

alt.condition()

alt.selection_single() は、図の効果によってユーザーの変更を受けとるものと、プルダウンやスライダーといった別のオブジェクトの変更を受けとるものがあります。後者の場合 binding すなわち alt.binding() を引数にとります。

.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

.transform_filter()

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

.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

図の操作による変更

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

図の操作による変更
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

スクリーンショット 2022-11-27 20.35.10.png

連動する図の作成

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

連動する図の作成
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

VConcatChart の活用

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

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 には様々な機能があります!公式ページや【時系列データ編】などを参考にインタラクティブなグラフを作成してみましょう!

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

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

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 チート集

随時追加していきます。

ストリッププロット(ジッタープロット)

プロットは学生番号に対応しています。点の粗密で分布を確認できます。

ストリッププロット(ジッタープロット)
(
    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

ヴァイオリンプロット

図形の幅で分布を表しています。人口ピラミットなどに用いられています。

ヴァイオリンプロット
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

リッジライン

図形の高さで分布を表しています。

リッジライン
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

20
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
25