HoloViewsの基本#

HoloViewsは他の描画ライブラリに基づいた可視化およびデータ分析拡張ライブラリです。データに可視化タイプや次元などの注釈情報を追加することでデータの可視化を実現します。具体的なチャートのレンダリングはmatplotlibやBokehなどの描画ライブラリによって行われます。HoloViewsのドキュメントは非常に充実しており、ドキュメントサイトでは各種チャートのサンプルコードを見つけることができます。このセクションでは、主にさまざまな例を通じてHoloViewsの基本概念を紹介します。

Jupyter NotebookでHoloviewsを使用する場合、まずextension()を呼び出してチャートをレンダリングする描画ライブラリを選択する必要があります。例えば、Bokeh描画ライブラリを使用する場合、まず以下を実行します:

hv.extension('bokeh')

この章では、HoloViewsを使用してデータ可視化アプリケーションを作成する方法を紹介します。描画関連の内容に加えて、この章ではParamとPanelライブラリの使用方法も紹介します。これら2つのライブラリをHoloViewsの描画機能と組み合わせることで、インタラクティブな可視化アプリケーションを簡単に作成できます。

import pprint
import holoviews as hv
from holoviews import streams
import numpy as np
import pandas as pd
from holoviews import opts
import panel as pn

hv.extension("bokeh", inline=False, logo=False)
pn.extension()

Elementオブジェクト#

HoloViewsでは、Elementの派生クラスを使用してデータに注釈を付けます。次はElementの継承ツリーを示しています。例えば、曲線を表すCurveクラスはChartから継承し、ChartDatasetElement2Dから継承します。

from helper.python import print_subclasses

text = print_subclasses(hv.Element, return_str=True)
pn.widgets.TextAreaInput(
    value=text, width=500, height=300, styles={"font-family": "monospace"}
)

Elementオブジェクトは、複数のデータに注釈を付けることをサポートしています。例えば、以下のプログラムは4種類のデータ型(タプル型、DataFrame型、ndarray配列型、辞書型)を作成し、Curveクラスでそれぞれのデータをラップします。区別しやすいように、各Curveオブジェクトのlabel属性に異なる値を設定します。最後に、各オブジェクトのdata属性の型を確認します。c1c2DataFrameオブジェクトでデータを保存し、c1を作成する際に、タプルデータを自動的にDataFrameデータに変換します。c3のデータ型はndarrayで、c4のデータ型はOrderedDictです。

x = np.linspace(0, 4 * np.pi, 200)
y = np.sin(x)

tuple_data = (x, y)
df_data = pd.DataFrame(dict(x=x, y=y))
arr_data = np.c_[x, y]
dict_data = dict(x=x, y=y)

c1 = hv.Curve(tuple_data, label="c1")
c2 = hv.Curve(df_data, label="c2")
c3 = hv.Curve(arr_data, label="c3")
c4 = hv.Curve(dict_data, label="c4")

curves = [c1, c2, c3, c4]
print(" ".join(f"{c.label}: {type(c.data).__name__}" for c in curves))
c1: DataFrame c2: DataFrame c3: ndarray c4: OrderedDict

異なるタイプのデータは異なるインターフェースで管理されます。以下に、各Curveオブジェクトのinterface属性を確認します。

print(" ".join(f"{c.label}: {c.interface.__name__}" for c in curves))
c1: PandasInterface c2: PandasInterface c3: ArrayInterface c4: DictInterface

c3は配列を使用してデータを保存していますが、dframe()メソッドを呼び出すことで、内部のデータをDataFrameオブジェクトに変換できます。

print(c3.dframe().head())
          x         y
0  0.000000  0.000000
1  0.063148  0.063106
2  0.126295  0.125960
3  0.189443  0.188312
4  0.252590  0.249913

HoloViewsのすべてのクラスはparam.Parameterizedから継承しているため、.param.values()を使用してオブジェクトのすべてのParameter属性を確認できます。kdimsvdimsgrouplabelの4つの属性は重要で、後の章で詳しく説明します。

c4.param.values()
{'cdims': {},
 'datatype': ['dataframe',
  'dictionary',
  'grid',
  'xarray',
  'multitabular',
  'spatialpandas',
  'dask_spatialpandas',
  'dask',
  'cuDF',
  'array',
  'ibis'],
 'extents': (None, None, None, None),
 'group': 'Curve',
 'kdims': [Dimension('x')],
 'label': 'c4',
 'name': 'Curve00272',
 'vdims': [Dimension('y')]}

Tip

Notebookでは、セル内のコードの最後の行の演算結果がHoloViewsの表示可能なオブジェクトである場合、指定された描画ライブラリを使用してチャートとして表示されます。また、IPythonのdisplay()を使用して表示することもできます。例えば:

from IPython.display import display
display(c1)

Dimensionオブジェクト#

Holoviewsでは、Dimensionオブジェクトを使用して次元関連の情報を保存します。次のテーブルはDimensionオブジェクトのすべての属性をリストしています。

属性名

タイプ

デフォルト値

説明

name

String

‘Dimension’

次元の名前。有効なPython識別子を使用することを推奨します

label

String

None

次元の説明ラベル。表示用で、LaTeXを使用して数式を表示できます

cyclic

Boolean

False

値の範囲が周期的かどうか。例えば角度

value_format

Callable

None

値のフォーマット関数

range

Tuple

(None, None)

値の範囲

soft_range

Tuple

(None, None)

参照値の範囲

type

Parameter

None

次元値のタイプ

default

Parameter

None

デフォルト値

step

Number

None

ステップ。スライダコントロールのステップを設定するために使用されます

unit

String

None

単位

values

List

[]

候補値リスト

Elementオブジェクトでは、kdims属性を使用してキー次元リストを保存し、vdims属性を使用して値次元リストを保存します。キー次元と値次元はどちらもDimensionオブジェクトです。異なるタイプのElementオブジェクトは、これらの次元に対応するデータを異なる方法で表示します。例えば、以下はCurveオブジェクトのデフォルトのキー次元と値次元を示しています。キー次元”x”は曲線上の各点のX軸座標に対応し、値次元”y”は曲線上の各点のY軸座標に対応します。

print(hv.Curve.kdims, hv.Curve.vdims)
[Dimension('x')] [Dimension('y')]

複数の線分を表すSegmentsオブジェクトは4つのキー次元を持ち、それぞれ2つの端点のX-Y軸座標に対応します。デフォルトの値次元はありません。

print(hv.Segments.kdims, hv.Segments.vdims)
[Dimension('x0'), Dimension('y0'), Dimension('x1'), Dimension('y1')] []

データ分布を表す箱ひげ図BoxWhiskerはキー次元を持たず、デフォルトの値次元が1つだけあります。

print(hv.BoxWhisker.kdims, hv.BoxWhisker.vdims)
[] [Dimension('y')]

Elementオブジェクトを作成する際、kdimsvdimsを使用してそのキー次元と値次元を設定できます。以下に、3つの方法で次元を設定します。c5は文字列を使用して次元を設定します。これはDimensionオブジェクトのnamelabel属性を同時に設定するのと同じです。c6は2つの要素のタプルを使用して次元を設定します。これらの要素はそれぞれ次元のnamelabel属性を設定するために使用されます。c7は直接Dimensionオブジェクトを使用して次元を設定します。これにより、キーワード引数を使用して次元のすべての属性を設定できます。kdimsvdimsパラメータにリストでないオブジェクトを渡すと、長さ1のリストを渡すのと同じです。

c5 = hv.Curve(tuple_data, kdims="time", vdims="position", label="c5")
c6 = hv.Curve(tuple_data, kdims=("t", "Time"), vdims=("x", "Position"), label="c6")
c7 = hv.Curve(
    tuple_data,
    label="c7",
    kdims=hv.Dimension("t", label="Time", unit="s"),
    vdims=hv.Dimension("x", label="Position", unit="mm"),
)

for c in (c5, c6, c7):
    kdim = c.kdims[0]
    vdim = c.vdims[0]
    print(f"{c.label}: {kdim.name=} {kdim.label=} {kdim.unit=} ")
c5: kdim.name='time' kdim.label='time' kdim.unit=None 
c6: kdim.name='t' kdim.label='Time' kdim.unit=None 
c7: kdim.name='t' kdim.label='Time' kdim.unit='s' 

チャートとして表示する場合、X軸のタイトルはキー次元のlabelunit属性によって決定され、Y軸のタイトルは値次元のlabelunit属性によって決定されます。例えば、前述のc7のチャートは次のように示されています。

c7.opts(width=600, show_grid=True)

次元情報を変更する必要がある場合、直接次元オブジェクトの属性を設定することもできますが、HoloViewsではredimを使用することを推奨しています。これは元のElementオブジェクトの次元情報を変更せず、新しいElementオブジェクトを返します。直接redim()を呼び出すことで、次元オブジェクト全体を変更できます。❶例えば、c1の2つの次元名はxyです。redim()のキーワード引数を使用してこれらの次元を設定します。パラメータxに渡されるのは文字列で、HoloViewsはその文字列を名前とする次元オブジェクトを作成します。パラメータyには直接次元オブジェクトを渡します。

redimは単純なメソッドではなく、アクセサオブジェクトです。そのメソッドを使用して、指定された次元の異なる属性を設定できます。❷redim.unit()は指定された次元のunit属性を変更し、❸redim.range()は指定された次元のrange属性を変更します。次元のrange属性を設定すると、チャートのX軸とY軸の表示範囲はその属性によって決定され、対応するデータの範囲ではなくなります。

c8 = c1.redim(x="time", y=hv.Dimension("position", label="Position"))  # ❶
c9 = c5.redim.unit(time="s", position="mm")  # ❷
c10 = c7.redim.range(t=(0, 8), x=(-2, 2))  # ❸

print(f"{c7.kdims[0].range = }, {c10.kdims[0].range = }")
c7.kdims[0].range = (None, None), c10.kdims[0].range = (0, 8)

Datasetからチャートへ#

通常、データ可視化を行う際には、データのクリーニングが既に行われているため、前述の方法でElementオブジェクトを作成し、その次元情報を設定するのは面倒です。このセクションでは、整然データから迅速にチャートを生成する方法を紹介します。

整然データ(Tidy Data)を2次元テーブルで表す場合、各列(各変数)は次元と見なすことができ、各行は1つの観測結果に対応します。変数は独立変数と従属変数に分けられます。独立変数は研究者が操作する変数で、従属変数は測定または記録される変数です。例えば、以下のプログラムは「gapminder_CJU.csv」からデータを読み取り、DataFrameオブジェクトdfを取得します。そのcountry列は国、year列は年、gdp_capitaliftpopulationpppなどの列はそれぞれ1人当たりGDP、平均寿命、1人当たりPPP、人口です。国と年は統計調査の前に決定できる変数で、独立変数です。他の変数は統計または観測の結果で、従属変数です。Holoviewsでは、通常、キー次元(kdims)を使用して独立変数を表し、値次元(vdims)を使用して従属変数を表します。独立変数、従属変数、キー次元、値次元の関係は絶対的なものではありません。例えば、複数の従属変数の関係を研究する場合、Scatterを使用して散布図を描画することができます。この場合、そのキー次元と値次元を2つの関連する従属変数に設定できます。

import pandas as pd

df = pd.read_csv("data/gapminder_CJU.csv")
df.sample(5).sort_index()
country year gdp_capita life ppp_capita population
20 China 2010 4550.0 75.8 9250.0 1370000000
22 China 2012 5330.0 76.2 11200.0 1380000000
25 China 2015 6480.0 76.5 12900.0 1410000000
36 Japan 1997 41900.0 80.8 25100.0 127000000
53 Japan 2014 46500.0 83.8 39200.0 128000000

以下に、dfを使用してDatasetデータセットを作成し、country列とyear列をキー次元として指定します。残りの列は自動的に値次元になります。Datasetを表示する際、キー次元は角括弧内に、値次元は丸括弧内に表示されます。

ds = hv.Dataset(df, kdims=["country", "year"])
ds
:Dataset   [country,year]   (gdp_capita,life,ppp_capita,population)

以下に、Dataset.to.curve()を呼び出して、データセットdsCurveオブジェクトのコンテナに変換します。パラメータkdimsvdimsを使用して、Curveオブジェクトのキー次元と値次元を指定します。dsのキー次元countryが指定されていないため、デフォルトでそれをグループ次元として使用します。グループ化操作の結果はHoloMapオブジェクトで、グループ次元countryはそのキー次元になります。print()を使用してその内部構造を確認できます。出力から、hmHoloMapオブジェクトで、そのキー次元はcountryです。この次元の各値はCurveオブジェクトに対応し、そのキー次元はyear、値次元はppp_capitaです。

hm = ds.to.curve(kdims="year", vdims="ppp_capita")
print(hm)
:HoloMap   [country]
   :Curve   [year]   (ppp_capita)

HoloMapUniformNdMappingから継承します。以下の図はUniformNdMappingのすべての派生クラスを示しています。これらのクラスは複数の要素を含むコンテナクラスで、異なる方法で複数の要素を表示しますが、内部のデータ保存方法は同じです。

print_subclasses(hv.core.UniformNdMapping)
└──UniformNdMapping
   ├──NdLayout
   ├──NdOverlay
   ├──HoloMap
   │  └──DynamicMap
   └──GridSpace
      └──GridMatrix

以下に、各オブジェクトのkdimsvdims属性を確認します。HoloMapオブジェクトは添字演算をサポートしているため、hm['China']は次元countryの値が’China’の要素を取得します。

print(f"{hm.kdims = }")
print(f'{hm["China"].kdims = }')
print(f'{hm["China"].vdims = }')
hm.kdims = [Dimension('country')]
hm["China"].kdims = [Dimension('year')]
hm["China"].vdims = [Dimension('ppp_capita')]

以下に、各オブジェクトのdata属性を確認します。これは実際のデータを保存します。HoloMapは順序付き辞書を使用してデータを保存し、CurveDataFrameオブジェクトを使用してデータを保存します。

print("HoloMap data:")
pprint.pp(hm.data)
print()
print("Curve data:")
print(hm["China"].data.head())
HoloMap data:
{('China',): :Curve   [year]   (ppp_capita),
 ('Japan',): :Curve   [year]   (ppp_capita),
 ('United States',): :Curve   [year]   (ppp_capita)}

Curve data:
  country  year  gdp_capita  life  ppp_capita  population
0   China  1990       729.0  68.7       982.0  1180000000
1   China  1991       786.0  68.8      1090.0  1190000000
2   China  1992       887.0  69.1      1260.0  1210000000
3   China  1993       998.0  69.4      1460.0  1220000000
4   China  1994      1120.0  69.7      1660.0  1230000000

hmを直接表示すると、次のグラフ(左)に示すようなドロップダウンボックスを使用して、次元countryの値を選択できます。ドロップダウンボックスの値が変更されると、チャート内の曲線が自動的に更新されます。

hm.opts(width=400)

複数の曲線を同時に表示する必要がある場合、overlay()メソッドを呼び出してNdOverlayオブジェクトに変換できます。NdOverlayオブジェクトは複数の要素を重ねて表示し、キー次元の値は凡例として表示されます。結果は次のグラフ(中)に示されています。

year_ppp_plot = hm.overlay()
print(year_ppp_plot)
year_ppp_plot
:NdOverlay   [country]
   :Curve   [year]   (ppp_capita)

以下の例では、国ごとにグループ化し、各要素はScatterオブジェクトで、そのキー次元はppp_capita、値次元はlifeとyearです。NdOverlayを使用して複数の要素を重ねて表示します。Scatterオブジェクトはキー次元をX軸座標として使用し、最初の値次元(life)を縦座標として使用します。出力チャートにマウスホバーツールhoverを追加しているため、マウスを各散布点上に置くと、表示に使用されていない値次元yearの値を見ることができます。その結果は次のグラフ(右)に示されています。

gdp_life_plot = ds.to.scatter(
    kdims=["ppp_capita"], vdims=["life", "year"], groupby=["country"]
).overlay()
gdp_life_plot.opts(width=400)

さらに、HoloMap.layout()を使用してHoloMapオブジェクトをNdLayoutオブジェクトに変換できます。これにより、複数のチャートを並べて表示することができます。次のグラフに示されています。

hm.layout()

配置と重ね合わせコンテナ#

NdLayoutNdOverlayは、複数の要素を並列または重ねて表示することができます。これらはNdで始まり、複数の次元を持つコンテナであることを示しています。HoloViewsはまた、次元情報を持たないLayoutOverlayも提供しており、これらのコンテナを使用して、次元に関連のない複数の要素を並列または重ねて表示することができます。これら2つのコンテナを簡単に作成するために、HoloViewsは+*の2つの演算子をオーバーロードしています。+を使用して複数の要素を接続すると、Layoutコンテナが作成され、これらの要素が並列表示されます。*を使用して複数の要素を接続すると、Overlayコンテナが作成され、これらの要素が重ねて表示されます。例えば、以下のコードは、同じチャート内に1つの曲線と1組の散布点を重ねて表示します。

hv.Curve((x1, y1)) * hv.Scatter((x2, y2))

以下のコードは、これらを並列表示します:

hv.Curve((x1, y1)) + hv.Scatter((x2, y2))

以下では、複数の要素を並列および重ねて表示する方法を、より複雑な例で紹介します。その結果は次のグラフに示されています。全体のチャートは2つの並列サブチャートで構成され、各サブチャートには3つの曲線とその局所的な極値が表示され、テキストで極値の大きさが表示されます。

以下のorth_poly()は、サブチャート内の1つの曲線とその極値情報を作成する関数です。プログラムでは、まずパラメータに基づいて曲線上の各点のX軸とY軸の座標値を計算します。そしてscipy.signal.find_peaks()を使用して局所的な最大値と最小値のインデックスを見つけます。❶次に、曲線を表すCurveオブジェクトを作成します。ここでは、このオブジェクトのgrouplabel属性を設定しています。これらの属性は、同じコンテナ内で異なる要素を区別するために使用できます。曲線のlabel属性は凡例に表示されます。❷極値座標を使用して散布点を表すScatterオブジェクトを作成し、ここで異なるgroup属性を設定します。❸最大値と最小値の数値を表示するために2つのLabelsオブジェクトを作成します。Labelsには、テキストの座標を表す2つのキー次元と、テキストの内容を表す1つの値次元があります。これら2つのLabelsオブジェクトを区別するために、異なるgroup属性を設定します。❹最後に、乗算記号を使用してこれら4つの要素を重ねて表示します。

import numpy as np
from scipy import special
from scipy import signal


def orth_poly(f, n, x0=-1, x1=1):
    x = np.linspace(x0, x1, 100)
    y = f(n, x)

    index_hi, _ = signal.find_peaks(y)
    index_lo, _ = signal.find_peaks(-y)
    name = f.__name__.split("_")[-1]
    label = f"{name} {n}"
    curve = hv.Curve((x, y), group="curve", label=label)  # ❶

    index = np.r_[index_hi, index_lo]
    xp = x[index]
    yp = y[index]
    peak = hv.Scatter((xp, yp), group="peak", label=label)  # ❷

    xh = x[index_hi]
    yh = y[index_hi]
    texth = ["{:4.2f}".format(v) for v in yh]
    text_hi = hv.Labels((xh, yh, texth), group="text_hi", label=label)  # ❸

    xl = x[index_lo]
    yl = y[index_lo]
    textl = ["{:4.2f}".format(v) for v in yl]
    text_lo = hv.Labels((xl, yl, textl), group="text_lo", label=label)  # ❸

    return curve * peak * text_hi * text_lo  # ❹

以下では、orth_poly()を使用して4次のチェビシェフ多項式のチャートcurve1を作成します。これはOverlayコンテナで、順序付き辞書OrderedDictを使用してその中の4つの要素を保存します。各要素に対応するキー値は、要素のgroup属性とlabel属性を使用して作成されます。キー値のテキストは大文字で始まることに注意してください。

from pprint import pp

curve1 = orth_poly(special.eval_chebyu, 4)
print(curve1)
print()
pp(curve1.data)
:Overlay
   .Curve.Chebyu_4   :Curve   [x]   (y)
   .Peak.Chebyu_4    :Scatter   [x]   (y)
   .Text_hi.Chebyu_4 :Labels   [x,y]   (Label)
   .Text_lo.Chebyu_4 :Labels   [x,y]   (Label)

{('Curve', 'Chebyu_4'): :Curve   [x]   (y),
 ('Peak', 'Chebyu_4'): :Scatter   [x]   (y),
 ('Text_hi', 'Chebyu_4'): :Labels   [x,y]   (Label),
 ('Text_lo', 'Chebyu_4'): :Labels   [x,y]   (Label)}

属性と同じ方法でその中の要素を取得できます。例えば、1次属性を使用して、すべてのgroup属性が同じ要素を取得し、新しいOverlayオブジェクトを取得できます。2次属性を使用すると、その中の1つの要素オブジェクトを取得できます。

print(curve1.Curve)
print()
print(curve1.Curve.Chebyu_4)
:Overlay
   .Chebyu_4 :Curve   [x]   (y)

:Curve   [x]   (y)

要素の数が不確定な場合、直接Overlayを使用する方が便利です。以下では、orth_poly()をループで呼び出し、3つの異なる次数のチェビシェフ多項式のOverlayコンテナを作成し、hv.Overlay()を使用してこれらのコンテナを重ね合わせます。最終的に得られるOverlayオブジェクトchebyc_plotには12の要素が含まれています。1次属性chebyc_plot.Peakを使用して、新しいOverlayオブジェクトを取得できます。その中の3つの要素のgroup属性はすべてpeakです。

chebyc_plot = hv.Overlay([orth_poly(special.eval_chebyu, n) for n in [2, 3, 4]])
print(chebyc_plot)
print()
print(chebyc_plot.Peak)
:Overlay
   .Curve.Chebyu_2   :Curve   [x]   (y)
   .Peak.Chebyu_2    :Scatter   [x]   (y)
   .Text_hi.Chebyu_2 :Labels   [x,y]   (Label)
   .Text_lo.Chebyu_2 :Labels   [x,y]   (Label)
   .Curve.Chebyu_3   :Curve   [x]   (y)
   .Peak.Chebyu_3    :Scatter   [x]   (y)
   .Text_hi.Chebyu_3 :Labels   [x,y]   (Label)
   .Text_lo.Chebyu_3 :Labels   [x,y]   (Label)
   .Curve.Chebyu_4   :Curve   [x]   (y)
   .Peak.Chebyu_4    :Scatter   [x]   (y)
   .Text_hi.Chebyu_4 :Labels   [x,y]   (Label)
   .Text_lo.Chebyu_4 :Labels   [x,y]   (Label)

:Overlay
   .Chebyu_2 :Scatter   [x]   (y)
   .Chebyu_3 :Scatter   [x]   (y)
   .Chebyu_4 :Scatter   [x]   (y)

上記と同じ方法を使用して、3つの異なる次数のルジャンドル多項式のOverlayコンテナlegendre_plotを作成し、加算記号を使用してこれら2つのOverlayコンテナを並列表示します。得られる結果はLayoutオブジェクトです。その中の2つのOverlayオブジェクトにgrouplabel属性を指定していないため、Holoviewsは自動的に対応するキー値を指定します。

legendre_plot = hv.Overlay([orth_poly(special.eval_legendre, n) for n in [2, 3, 4]])
plots = chebyc_plot + legendre_plot
print(plots)
:Layout
   .Overlay.I  :Overlay
      .Curve.Chebyu_2   :Curve   [x]   (y)
      .Peak.Chebyu_2    :Scatter   [x]   (y)
      .Text_hi.Chebyu_2 :Labels   [x,y]   (Label)
      .Text_lo.Chebyu_2 :Labels   [x,y]   (Label)
      .Curve.Chebyu_3   :Curve   [x]   (y)
      .Peak.Chebyu_3    :Scatter   [x]   (y)
      .Text_hi.Chebyu_3 :Labels   [x,y]   (Label)
      .Text_lo.Chebyu_3 :Labels   [x,y]   (Label)
      .Curve.Chebyu_4   :Curve   [x]   (y)
      .Peak.Chebyu_4    :Scatter   [x]   (y)
      .Text_hi.Chebyu_4 :Labels   [x,y]   (Label)
      .Text_lo.Chebyu_4 :Labels   [x,y]   (Label)
   .Overlay.II :Overlay
      .Curve.Legendre_2   :Curve   [x]   (y)
      .Peak.Legendre_2    :Scatter   [x]   (y)
      .Text_hi.Legendre_2 :Labels   [x,y]   (Label)
      .Text_lo.Legendre_2 :Labels   [x,y]   (Label)
      .Curve.Legendre_3   :Curve   [x]   (y)
      .Peak.Legendre_3    :Scatter   [x]   (y)
      .Text_hi.Legendre_3 :Labels   [x,y]   (Label)
      .Text_lo.Legendre_3 :Labels   [x,y]   (Label)
      .Curve.Legendre_4   :Curve   [x]   (y)
      .Peak.Legendre_4    :Scatter   [x]   (y)
      .Text_hi.Legendre_4 :Labels   [x,y]   (Label)
      .Text_lo.Legendre_4 :Labels   [x,y]   (Label)

多段属性を使用して、その中の任意の要素を取得できます。例えば:

el = plots.Overlay.II.Peak.Legendre_3
print(f"{el}, {el.group=}, {el.label=}")
:Scatter   [x]   (y), el.group='peak', el.label='legendre 3'

最後に、plotsを表示する際に、opts()を使用してその中の各要素の表示オプションを設定します。特定の要素に属性を設定する必要がある場合、最初のパラメータを使用してそのオプションに対応するgrouplabel属性を指定できます。ここでは、group属性がtext_hiLabelsオブジェクトとtext_loLabelsオブジェクトに異なるオプションを設定しています。

plots.opts(
    opts.Layout(shared_axes=False),
    opts.Scatter(size=10, marker="x"),
    opts.Curve(alpha=0.7),
    opts.Overlay(show_grid=True, width=400, legend_cols=3),
    opts.Labels(text_font_size="9pt"),
    opts.Labels("text_hi", text_baseline="bottom"),
    opts.Labels("text_lo", text_baseline="top"),
)

コンテナのネスト#

UniformNdMappingの派生クラスは多重にネストでき、複雑なグラフを描画します。以下に、コンテナタイプのネストを紹介するための例を示します。以下のrun_sim()は、scipy.integrate.solve_ivp()を使用して質量、バネ、ダンパーシステムの微分方程式を解き、Curve曲線要素を返します。

from scipy import integrate
import numpy as np

def spring_sys(t, y, m, b, k, F):
    x, v = y
    dx = v
    dv = (F - k * x - b * v) / m
    return dx, dv

def spring_sim(x0, v0, m, b, k, F, tend, n):
    t = np.linspace(0, tend, n)
    res = integrate.solve_ivp(spring_sys, (0, tend), (x0, v0), t_eval=t, args=(m, b, k, F))
    return res.t, res.y[0], res.y[1]

dim_time = hv.Dimension('t', label='time', unit='s')
dim_pos = vdims=hv.Dimension('x', label='x', unit='m')

def run_sim(x0, v0, m, b, k, F, tend, n): 
    t, x, _ = spring_sim(x0, v0, m, b, k, F, tend, n)
    return hv.Curve((t, x), kdims=dim_time, vdims=dim_pos)

以下では、numpy.mgrid[]を使用して、mbkFの4つのパラメータの4次元グリッドを迅速に作成します。そして、各パラメータの組み合わせに対してrun_sim()を呼び出してHoloMapオブジェクトhmを作成します。出力からわかるように、これは4つのキーディメンションを持ち、各キーディメンションの値はCurve要素に対応し、Curve要素のキーディメンションはt、値ディメンションはxです。

from itertools import product

hm = hv.HoloMap(kdims=[
    hv.Dimension('m', label='mass', unit='kg'),
    hv.Dimension('b', label='dumper', unit=''),
    hv.Dimension('k', label='spring', unit='N/m'),
    hv.Dimension('F', label='force', unit='N'),
])

for args in np.mgrid[0.5:2:3j, 0.1:1.0:3j, 0.5:3:3j, 0:1:3j].reshape(4, -1).T:
    hm[tuple(args)] = run_sim(1.0, 0.0, *args, tend=10, n=50)

print(hm)
:HoloMap   [m,b,k,F]
   :Curve   [t]   (x)

以下では、コンテナオブジェクトのoverlay()layout()メソッドをチェーンしてNdLayoutコンテナオブジェクトを取得し、そのcols()を呼び出してコンテナオブジェクトの列数を設定し、最後にopts()を呼び出して各オブジェクトの表示オプションを設定します。出力からわかるように、plotsNdLayoutコンテナであり、mbの2つのキーディメンションを持ちます。その各要素はHoloMapコンテナであり、Fの1つのキーディメンションを持ちます。その各要素はNdOverlayコンテナであり、kの1つのキーディメンションを持ちます。

options = [
    opts.NdOverlay(legend_position='top_left', legend_cols=3), 
    opts.Curve(show_grid=True, width=300)    
]    
    
plots = hm.overlay('k').layout(['m', 'b']).cols(3).opts(*options)
print(plots)
:NdLayout   [m,b]
   :HoloMap   [F]
      :NdOverlay   [k]
         :Curve   [t]   (x)
plots

plotsの表示結果からわかるように、Ndlayoutmbの2つのディメンションを並列表示し、NdOverlayはディメンションkを1つのサブプロットに重ねて表示し、HoloMapはディメンションFをスライダーコントロールとして表示します。このコントロールを使用して、ディメンションFに対応する値を変更できます。

プロットオプション#

HoloViewsでは、要素とコンテナオブジェクトはデータと次元間の関係を記述するためにのみ使用され、色や線種などのプロット関連の属性は持っていません。しかし、実際のプロットでは、これらのプロット関連のオプションが不可欠です。異なる要素のプロットオプションは異なるため、HoloViewsはこれらのオプションを簡単に作成するためにoptsオブジェクトを提供しています。optsオブジェクトのメソッド名は要素のクラス名と同じで、キーワード引数を使用して各オプションの値を設定します。例えば、以下はCurveScatterに適用されるオプションを作成します。widthはチャートの幅を設定し、show_gridは座標グリッドを表示するかどうかを設定し、line_widthは線幅を設定し、colorは線の色を設定します。オプションを作成する際に、対応するgroup属性とlabel属性を指定することもできます。例えば、scatter_opt2group属性がLargeのオブジェクトにのみ有効です。異なるオブジェクトは異なるオプションをサポートしています。hv.help()関数を使用して、各要素がサポートするオプションのリストとそのヘルプ説明を参照できます。例えば、hv.help(hv.Curve)です。

from holoviews import opts

curve_opt = opts.Curve(width=600, show_grid=True, line_width=4, color="green")
scatter_opt = opts.Scatter(size=16, marker="x", color="red")
scatter_opt2 = opts.Scatter("Large", size=24, color="blue")

要素とコンテナオブジェクトのopts()options()メソッドを使用して、そのオブジェクトにオプションを指定できます。これらは複数のオプションオブジェクトを受け取ることができます。obj.opts()objのオプションを設定し、obj.options()objを複製し、複製オブジェクトのオプションを設定して、複製オブジェクトを返します。opts.info()はオブジェクトに関連するオプションを表示するために使用できます。show_defaultsパラメータがTrueの場合、各オプションのデフォルトオプションが表示されます。opts.clear()を使用して、オブジェクトに対応するすべてのオプションを削除できます。オプション関連の情報はオブジェクト内に保存されていません。HoloViewsは、すべてのオプションとオブジェクト間の関係を保存するためにグローバルオブジェクトを使用しています。

以下では、まずOverlayオブジェクトを作成します。次に、上記で作成した2つのオプションオブジェクトをopts()メソッドに渡します。

x = np.linspace(0, 4 * np.pi, 50)
y = np.sin(x)
data = x, y
data2 = x[::5], y[::5]
overlay = hv.Curve(data) * hv.Scatter(data) * hv.Scatter(data2, group="Large")
overlay.opts(curve_opt, scatter_opt, scatter_opt2)
overlay.opts.info()
print()
overlay.opts.clear()
overlay.opts.info(show_defaults=True)
:Overlay
   .Curve.I   :Curve   [x]   (y)
    | Options(color='green', line_width=4, show_grid=True, width=600)
   .Scatter.I :Scatter   [x]   (y)
    | Options(color='red', marker='x', size=16)
   .Large.I   :Scatter   [x]   (y)
    | Options(color='blue', marker='x', size=24)

:Overlay
 | Options(axiswise=False, click_policy='mute', framewise=False)
   .Curve.I   :Curve   [x]   (y)
    | Options(axiswise=False, color=Cycle(), framewise=False, line_width=2, muted_alpha=0.2)
   .Scatter.I :Scatter   [x]   (y)
    | Options(axiswise=False, cmap='kbc_r', color=Cycle(), framewise=False, muted_alpha=0.2,
    |         size=np.float64(2.449489742783178))
   .Large.I   :Scatter   [x]   (y)
    | Options(axiswise=False, cmap='kbc_r', color=Cycle(), framewise=False, muted_alpha=0.2,
    |         size=np.float64(2.449489742783178))

デフォルトオプションはopts.defaults()を使用して設定できます。例えば、この章のNotebookの冒頭部分で、以下のプログラムを使用してScatterCurve、およびNdOverlayのデフォルトオプションを設定しています。

opts.defaults(
    opts.Scatter(tools=['hover'], width=500, show_grid=True),     
    opts.Curve(tools=['hover'], width=500, show_grid=True), 
    opts.NdOverlay(legend_position='left'),
)

opts()を使用して、データの特定の次元に基づいてプロット属性を決定することができます。例えば、Pointsオブジェクトは2次元の散布点を表し、デフォルトではその2つのキー次元が散布点の座標を決定します。opts()を使用して、散布点のサイズ、色、形状などのプロット属性をデータの特定の次元に関連付けることができます。以下の例では、散布点を使用してlifeppp_capitayear、およびpopulationの4つの次元間の関係を表示します。ここで、lifeppp_capitaは点のX-Y軸座標であり、yearは点の色を決定し、populationは点のサイズを決定します。次元を使用してプロット属性を計算する場合、dimオブジェクトを使用してその次元をラップする必要があります。dimオブジェクトはすべての演算子といくつかの一般的なメソッドをサポートしています。例えば、以下では、population次元を使用して散布点のサイズを計算する際に、まずそのnorm()メソッドを呼び出してデータを正規化し、0〜1の範囲にマッピングし、その平方根を取り、拡大係数10を乗算し、小さな点を表示するために定数を加えます。その表示結果は次のグラフに示されています。

from holoviews import dim

points = ds.to.points(
    kdims=["life", "ppp_capita"], vdims=["year", "population"], groupby=[]
).opts(
    opts.Points(
        color=dim("year"),
        size=dim("population").norm() ** 0.5 * 10 + 2,
        colorbar=True,
        cmap="Viridis",
        width=500,
        show_grid=True,
    )
)
points

dimオブジェクトで構成される式は、計算プロセス全体を保存するだけで、実際の計算は行われません。指定されたデータに適用して結果を計算する必要があります。pointsオブジェクトを表示する際に、dimオブジェクトを含む式が計算されます。これはdim式のapply()メソッドを呼び出すことと同等です。

size_expr = dim("population").norm() ** 0.5 * 10 + 2
size = size_expr.apply(ds)
print(size_expr)
print(f"{size.min() = }, {size.max() = }")
(((dim('population').norm())**0.5)*10)+2
size.min() = np.float64(2.0), size.max() = np.float64(12.0)