7
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

可視化ライブラリのBokehとHoloViewsを比較してみた

Last updated at Posted at 2022-01-23

これまでインタラクティブなグラフの作成ではHoloViewsを使用していたのですが、会社で使用するのであれば有名どころを抑えておいた方が良さそう、という理由でBokehについても色々勉強してみました。
折角なので、各ライブラリの特徴や使用感などを比較しながら解説してみようと思います。

目次

1.BokehとHoloViews
2.Bokehの基本
3.HoloViewsの基本
4.実際に書いて比較してみる 散布図編
  4a.Bokehによる散布図の作成
    4a-1.Bokehでデータ系列を指定する
    4a-2.Bokehでグラフ外観の設定をする
    4a-3.Bokehで色の指定をする
    4a-4.Bokehで要素(カラーバー)を追加する
  4b.HoloViewsによる散布図の作成
    4b-1.HoloViewsでデータ系列を指定する
    4b-2.HoloViewsでグラフ外観の設定をする(色指定、カラーバー追加含む)
5.実際に書いて比較してみる 折れ線グラフ編
  5a.Bokehによる折れ線グラフの作成
    5a-1.Bokehで多系列の折れ線グラフを作成する
    5a-2.Bokehでカラーパレットをカスタマイズして適用する
  5b.HoloViewsによる折れ線グラフの作成
    5b-1.HoloViewsで多系列の折れ線グラフを作成する
    5b-2.HoloViewsでカラーパレットをカスタマイズして適用する
    5b-3.HoloViewsの複合グラフに各種オプションを設定する
6.まとめ

1.BokehとHoloViews

どちらもグリグリ動かせるインタラクティブなグラフ:point_down_tone2:を作成できるPythonの可視化ライブラリです。
holoviewデモ.gif
(これは過去にHoloViewsを紹介した記事内で作成したグラフです)

公式ドキュメント
Bokeh → https://docs.bokeh.org/en/latest/
HoloViews → https://holoviews.org/

Bokehはインタラクティブなグラフ作成ツールの有名どころで、HoloViewsはBokeh等の可視化ライブラリをより簡単に使用できるようにつくられたラッパーツールです。
尚、グラフ作成ライブラリの有名どころではPlotlyもありますが、こちらは有料プランがある等ビジネスの香りがするため、候補から外しています(会社で使用するのが怖い)。

2.Bokehの基本

Bokehは、最初にfigureというグラフの下地を用意して、そこにグラフや凡例などのオブジェクトを次々と書き足していきます
イメージとしてはMatplotlibに近い感じでしょうか。

例:折れ線グラフと散布図の描画
(ライブラリのインポートなどは省略)

p = bokeh.plotting.figure(width=300, height=200)  # 描画領域生成

p.scatter(x=[1, 2, 3, 4], y=[1, 4, 9, 16])    # 散布図描画
p.line(x=[1, 2, 3, 4], y=[2, 4, 7, 11])       # 折れ線グラフ描画

display(show(p))    # 表示

    ↓
02_Bokeh_basic_graph.JPG
(何故かグラフの後にNoneと表示されてしまいます。現時点ではこれの消し方がまだ分かっていません)
デフォルト設定のデザインは、後述のHoloViewsよりやや洗練されている印象を持ちました。

3.HoloViewsの基本

(今回の記事ではライブラリインストールの説明は省いているのですが、HoloViewsの場合はバックエンドに指定するライブラリ(今回の場合Bokeh)もインストールが必要な点は一応注意が必要です)

HoloVeiwsでは、Scatter(散布図)やCurve(折れ線グラフ)をオブジェクトとして生成し、それらを重ね合わせて扱うイメージです。
重ね合わせはオブジェクト同士を(乗算記号)で掛け合わせるだけなので、シンプルで分かりやすい*のが特徴です。
(今回触れませんが、+(加算記号)で複数のグラフを並べて表示することもできます)

例:折れ線グラフと散布図の描画
(ライブラリのインポートやバックエンド指定などは省略)

scatter = hv.Scatter(data=([1, 2, 3, 4], [1, 4, 9, 16]))  # 散布図のインスタンス生成
curve = hv.Curve(data=([1, 2, 3, 4], [2, 4, 7, 11]))      # 折れ線グラフのインスタンス生成

scatter = scatter.opts(width=300, height=200)    # 表示設定(片方のみでOK)

scatter * curve    # 表示

    ↓
03_HoloViews_basic_graph.JPG
デフォルトではグリッドが表示されないためか、デザイン的にちょっと物足りない印象です。
ただ、この辺りは簡単に設定可能です。詳細はこの後の実例を参照。

4.実際に書いて比較してみる 散布図編

ここからはPandasのDataFrameからグラフを作成するやり方で書いていきます。

使用するデータについて(スクレイピングあり)

Scatter編では、ポケモン(第8世代)の種族値でグラフを作成します。
今回の記事の本質ではないですが、スクレイピングのコードを自分用に残しておきます。

# スクレイピング用
#ライブラリのインストール
import requests, bs4
import pandas as pd

# URLの指定
url=r"https://wiki.ポケモン.com/wiki/%E7%A8%AE%E6%97%8F%E5%80%A4%E4%B8%80%E8%A6%A7_(%E7%AC%AC%E5%85%AB%E4%B8%96%E4%BB%A3)"
# サーバにリクエストを送りレスポンスを取得
res = requests.get(url=url)
res.encoding = "utf-8"
# HTML解析
soup = bs4.BeautifulSoup(res.text, "html.parser")

# 先頭のテーブルデータ取得
table = soup.find_all("table",class_="bluetable")[0]
# テーブルからすべての行を取得
rows = table.find_all("tr")

table_data = []
for row in rows:

  # 各行のtd、thタグをすべて取得し、改行文字(\n)を削除した内包表記リストをテーブルデータに追加
  tmp_row = [cell.text.replace("\n","") for cell in row.find_all(["td", "th"])]
  table_data.append(tmp_row)

# リストからデータフレーム作成(一行目をカラム名に指定)
df = pd.DataFrame(table_data[1:], columns=table_data[0])

# int型へ変換可能なものは変換
for i, a_col in enumerate(df.iteritems()):
  try:
    df.iloc[:, i] = a_col[1].astype(int)
  # エラー名が分からない場合
  # except Exception as e:
  #   print(type(e))
  # 変換できなかった場合の処理
  except ValueError:
    print(f"列:{a_col[0]} はint型へ変換しませんでした。")

# 種族合計から散布図の各点のサイズを規定する系列sizeを算出・追加
df["size"] = 5 + ((df["合計"] - df["合計"].min())/(df["合計"].max() - df["合計"].min()) * 10)

    ↓
04_scatter_data_image.JPG

4a.Bokehによる散布図の作成

コード

# ライブラリのインポート
from bokeh.plotting import figure, show
from bokeh.io import output_notebook, push_notebook
from IPython.display import display
from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis256
from bokeh.models import LinearColorMapper, ColorBar

# GoogleColab(Jupyter)でグラフ表示するためのおまじない
output_notebook()

# 素早さの値から色に変換するカラー変換関数定義(グラフ描画時の色指定に必要)
linear_transform_color = linear_cmap(field_name="素早さ", palette=Viridis256, low= df["素早さ"].min(), high= df["素早さ"].max())

# グラフ描画領域生成
p = figure(title="ポケモン種族値", x_axis_label="攻撃", y_axis_label="防御",
           width=600, height=400)
# Scatter描画
p.scatter(x="攻撃", y="防御", source=df,
          fill_color=linear_transform_color,
          line_width=0, size="size", alpha=0.8)

# カラーマッパー定義(カラーバー表示のため必要、前述のカラー変換関数とは別物)
# color_mapper = linear_cmap["transform"]
color_mapper = LinearColorMapper(palette=Viridis256, low= df["素早さ"].min(), high= df["素早さ"].max())

# カラーバーの定義と表示
color_bar = ColorBar(color_mapper=color_mapper, title="素早さ")
p.add_layout(obj=color_bar, place="right")

# 表示
display(show(p))

 ↓
04_1_scatter_with_bokeh.JPG

モジュールのインポートやら、カラーパー表示回りやらでちょっと冗長なコードの印象でしょうか。
以下、ポイントの解説です。

4a-1.Bokehでデータ系列を指定する

Bokehの場合、figureに対してメソッドでグラフを描画する際にデータ系列を指定します。

# Scatter描画
p.scatter(x="攻撃", y="防御", source=df,
    fill_color=linear_cmap,
    line_width=0, size="size", alpha=0.8)
  :

sourceに使用するDataFrameを指定し、xとyに表示するカラム名を指定しています。
また、今回は各点のサイズにもDataFrameから参照しているので、ここでもカラム名を指定しています。
(sizeは種族値合計をいい感じに計算してサイズ値に変換したもので、事前にdfに追加しています)

4a-2.Bokehでグラフ外観の設定をする

figureを生成する際や、メソッドでグラフを描画する際の引数で各種設定を行ないます。

# グラフ描画領域生成
p = figure(title="ポケモン種族値", x_axis_label="攻撃", y_axis_label="防御",
    width=600, height=400)
# Scatter描画
p.scatter(x="攻撃", y="防御", source=df,
    fill_color=linear_cmap,
    line_width=0, size="size", alpha=0.8)
  :

引数名はシンプルなので、細かい解説は省略します。

4a-3.Bokehで色の指定をする

ここがBokehが面倒くさいと感じたポイントの一つです。
まず、「どの数値」を「どのように」変換して「どのカラーパレット」に適用するかを定めた色変換関数を定義する必要があります。
今回の場合

  • どの数値 ⇒ 素早さ
  • どのように ⇒ 素早さのminを下限、maxを上限とした線形変換
  • どのカラーパレット ⇒ Viridis256

で定義します。具体的には下記のように定義します。(↑と色合わせてみました)

from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis256
  :
# 素早さの値から色に変換するカラーマップ定義(グラフ描画時の色指定に必要)
linear_transform_color = linear_cmap(field_name="素早さ", palette=Viridis256, low= df["素早さ"].min(), high= df["素早さ"].max())
  :

Palettesモジュールにどのようなカラーパレットがあるかは公式リファレンスのbokeh.Palettesを参照。
後にも触れますが、パレット名は色系列+使用する色数を指定する必要があります。
そのため、使用する色数が変わればコードを書き直す必要があるなど、Bokehは色に関してかなり不自由な印象を持ちました。(一応それを回避する手段もあるようです)

あとは、定義した色変換関数をグラフ描画時に色のオプション(今回の場合fill_color)として指定するだけです。

# Scatter描画
p.scatter(x="攻撃", y="防御", source=df,
    fill_color=linear_transform_color,
    line_width=0, size="size", alpha=0.8)

4a-4.Bokehで要素(カラーバー)を追加する

要素を追加するには、定義したオブジェクトをfigureにadd_layoutで追加します。

ただ、カラーバーの場合少しややこしく、事前にカラーマッパーを定義する必要があります。このカラーマッパーは前述の色変換関数と似ていますが、別物のようです。
下記のようにbokeh.modelsからカラーマッパーをインポートしたのち、各種設定を引数に指定して定義します。

from bokeh.models import LinearColorMapper, ColorBar
  :
# カラーマッパー定義(カラーバー表示のため必要、前述のカラー変換関数とは別物)
color_mapper = LinearColorMapper(palette=Viridis256, low= df["素早さ"].min(), high= df["素早さ"].max())

次にColorBarオブジェクトを生成します。このときcolor_mapper引数に先ほどのカラーマッパーを指定します。

from bokeh.models import LinearColorMapper, ColorBar
  :
# カラーバーの定義と表示
color_bar = ColorBar(color_mapper=color_mapper, title="素早さ")

あとは、add_layoutメソッドで生成したカラーバーをグラフに追加するだけです。

p.add_layout(obj=color_bar, place="right")

カラーバーはBokehで要素を追加する例として紹介するにはやや複雑な例かもしれません。
どちらかというと折れ線グラフ編の凡例追加の方がシンプルで分かりやすいと思います。

次はHoloViewsでのグラフ作成実例です。

4b.HoloViewsによる散布図の作成

コード

# ライブラリのインポート
import holoviews as hv

# GoogleColab(Jupyter)でグラフ表示するためのおまじない
hv.extension("bokeh")

# Scatterインスタンスの生成
scatter = hv.Scatter(data=df, kdims=["攻撃"], vdims=["防御", "素早さ", "size"])
# Scatterの描画オプション指定
scatter = scatter.opts(title="ポケモン種族値", show_grid=True, width=600, height=400, 
                       size="size", color="素早さ", cmap="viridis", alpha=0.8, 
                       colorbar=True, colorbar_opts={"title":"素早さ", "bar_line_color":None, "major_tick_line_color":"white"})

# 表示 
scatter

    ↓
04_2_scatter_with_holoviews.JPG
Bokehと比べるとコードがかなりスッキリしています。

4b-1.HoloViewsでデータ系列を指定する

HoloViewsでのデータ系列の指定は、x, yの代わりにkdims, vdimsをそれぞれ指定します。

# Scatterインスタンスの生成
scatter = hv.Scatter(data=df, kdims=["攻撃"], vdims=["防御", "素早さ", "size"])
  :

kdims
key_dimensionsの略。キーとなる要素。
Scatterの場合、x軸に何を指定するかと捉えておいてOKです。
dataに指定したデータのカラム名をリストで指定します。

vdims
value_dimensionsの略。キーに対応した値となる要素。
Scatterの場合、y軸にあたる要素はvdimsに指定します。
kdims同様、dataに指定したデータのカラム名を指定しますが、vdimsに指定した値は色やサイズにも使用できます。
リストで格納された要素のうち、先頭の要素がyとして扱われるようです。

4b-2.HoloViewsでグラフ外観の設定をする(色指定、カラーバー追加含む)

HoloViewsにおけるグラフの各種設定には.optsメソッドを使用します。

# Scatterの描画オプション指定
scatter = scatter.opts(title="ポケモン種族値", show_grid=True, width=600, height=400,
      size="size", color="素早さ", cmap="viridis", alpha=0.8,
      colorbar=True, colorbar_opts={"title":"素早さ", "bar_line_color":None, "major_tick_line_color":"white"})
  :

上記のようにBokehでは面倒だった色やカラーバーの追加・設定も非常に簡素な記述で済みます。
基本的な使い方であれば非常にシンプルに書けるのがHoloViewsのメリットですね。

5.実際に書いて比較してみる 折れ線グラフ編

使用するデータについて(スクレイピングなし)
折れ線グラフでは、厚労省が発表している日本の喫煙率の推移を描画します。 [https://www.health-net.or.jp/tobacco/product/pd100000.html](https://www.health-net.or.jp/tobacco/product/pd100000.html) 本当はコチラのデータもスクレイピングで取得したかったのですが、うまくいかず断念。素直にデータをエクセルにコピペして加工 ⇒ csvに保存してDataFrame(インスタンス名:df2)として読み込んでいます。 ![05_lineplot_data_image.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/bfd0d8b2-2acc-cc03-8b77-c0b3a2de23bd.jpeg) 厚労省のサイトにあるグラフは、色に脈絡がなくて見づらいと感じたので、色系統と濃淡で色に意味合いを与えながら、グラフを描画していきたいと思います。

5a.Bokehによる折れ線グラフの作成

コード

# ライブラリのインポート
from bokeh.plotting import figure, show
from bokeh.io import output_notebook, push_notebook
from IPython.display import display
from bokeh.models import Legend
from bokeh.palettes import Blues7, Reds7

# GoogleColab(Jupyter)でグラフ表示するためのおまじない
output_notebook()

# グラフ描画領域生成
p = figure(title="喫煙率の推移", x_axis_label="年次", y_axis_label="喫煙率", width=600, height=400)

# 凡例(グラフ外)の設定と描画
legend = Legend(location="center")
p.add_layout(obj=legend, place="right")

# 色の設定(palettesの中身はタプルのため結合可)
# 試用する色は6色*2だが、最薄の色が見づらいため7色用意してスライスで最薄を除外
color_list = Blues7[:-1] + Reds7[:-1]

# カラム名と色を同時に読み込み、カラム数分折れ線グラフ描画を繰り返す
for column_name, color in zip(df2.columns, color_list):
  p.line(x="index", y=column_name , source=df2, legend_label=column_name, line_width=2, color=color)

# 表示
display(show(p))

    ↓
05_1_lineplot_with_bokeh.JPG
モジュールインポート部は相変わらず冗長ですが、それ以外は比較的スッキリしたコードで書けます。

5a-1.Bokehで多系列の折れ線グラフを作成する

複数の折れ線グラフの描画は、for文で同じfigureに繰り返しプロットするだけです。

# グラフ描画領域生成
p = figure(title="喫煙率の推移", x_axis_label="年次", y_axis_label="喫煙率", width=600, height=400)
  :
# カラム名と色を同時に読み込み、カラム数分折れ線グラフ描画を繰り返す
for column_name, color in zip(df2.columns, color_list):
    p.line(x="index", y=column_name , source=df2, legend_label=column_name, line_width=2, color=color)
  :

legend_nameにもカラム名を指定することで、凡例の表示にカラム名をそのまま使用できます。

5a-2.Bokehでカラーパレットをカスタマイズして適用する

Bokehで複数系統のカラーパレットを使用したい場合、あらかじめ意図したカラーパレット作成しておき、
そのパレットに基づいてグラフを描画する必要があります。

パレットの中身は表示してみると分かるのですが、('#084594', '#2171b5', …)のように16進数の色情報のタプルです。そのため、そのため普通のタプルと同様に結合したり、スライスで一部情報を取り出すことができます。

  :
from bokeh.palettes import Blues7, Reds7
  :
# 色の設定(palettesの中身はタプルのため結合可)
# 試用する色は6色*2だが、最薄の色が見づらいため7色用意してスライスで最薄を除外
color_list = Blues7[:-1] + Reds7[:-1]
  :

そのようにして作成したパレットを、前述のfor文の中で取り出し、引数colorに一つずつ設定しています。

# カラム名と色を同時に読み込み、カラム数分折れ線グラフ描画を繰り返す
for column_name, color in zip(df2.columns, color_list):
    p.line(x="index", y=column_name , source=df2, legend_label=column_name, line_width=2, color=color)
  :

このやり方の場合、難点はやはり色の柔軟性が無いこと。具体的には下記のようなデメリットがあります。

  • DataFrameのカラムの順番を把握したうえでカラーパレットを作らないといけない
  • カラムの数が変動したら呼び出すカラーパレット名から変更しないといけない
  • Bluesなどは最大でも9色のパレットしかない。

複雑なデータなどを可視化するときは工夫が必要そうです…。

5b.HoloViewsによる折れ線グラフの作成

コード

# ライブラリのインポート
import holoviews as hv
from holoviews import opts

# GoogleColab(Jupyter)でグラフ表示するためのおまじない
hv.extension("bokeh")

# 各カラムをもとにした折れ線グラフを男女別に辞書作成(keyはカラム名)
dict_male = {col_name:hv.Curve(df2[col_name]) for col_name in df2.columns if "男性" in col_name}
dict_female = {col_name:hv.Curve(df2[col_name]) for col_name in df2.columns if "女性" in col_name}
# 辞書のグラフを男女それぞれ合算
male_graph = hv.NdOverlay(dict_male)
female_graph = hv.NdOverlay(dict_female)

# 男女それぞれのグラフに色を指定
# Palettesのrangeは色のどこからどこまでを使用するか。0-1の範囲のタプルで指定。
male_graph = male_graph.opts(opts.Curve(color=hv.Palette("Blues", range=(0, 0.8))))
female_graph = female_graph.opts(opts.Curve(color=hv.Palette("Reds", range=(0, 0.8))))

# 男女のグラフを合算
all_graph = male_graph * female_graph
# グラフの設定
all_graph = all_graph.opts(title="喫煙率の推移", xlabel="年次", ylabel="喫煙率",
                           width=600,height=400,legend_position="right", show_grid=True)

# 表示
all_graph

    ↓
05_2_lineplot_with_holoviews.JPG
こちらはBokehよりコードがやや長くなってしまいました。
ただ、一部は男女のデータで同じことを繰り返しているだけのため、実際には見た目ほどコード記述量はありません。

5b-1.HoloViewsで多系列の折れ線グラフを作成する

Bokehのときと同じアプローチ(forで繰り返す)も可能ですが、今回はHoloViews独自のNdOverlayを使って作成します。

まず、重ねたいグラフをvalue、その時のグラフ名(系列名)をkeyにした辞書を作成します。
今回は辞書型の内包表記を使用しました。
(可読性が下がっている気がするので、正直for文で作ってしまっても良いと思います)

# 各カラムを系列にした折れ線グラフを男女別に辞書作成(keyはカラム名)
dict_male = {col_name:hv.Curve(df2[col_name]) for col_name in df2.columns if "男性" in col_name}
dict_female = {col_name:hv.Curve(df2[col_name]) for col_name in df2.columns if "女性" in col_name}
  :

そして、作成した辞書をhv.NdOverlayの引数に指定します。すると、辞書のvalueがすべて合算された複合グラフが生成されます。

# 辞書のグラフを男女それぞれ合算
male_graph = hv.NdOverlay(dict_male)
female_graph = hv.NdOverlay(dict_female)
  :

そして、生成したグラフはさらに重ねることができるので、最後に男女それぞれのグラフを乗算記号*で重ねています。

# 男女のグラフを合算
all_graph = male_graph * female_graph

5b-2.HoloViewsでカラーパレットをカスタマイズして適用する

color引数にパレット名を指定するだけでもOKなのですが、hv.Paletteメソッドを使用すればパレットのカスタマイズも可能です。
今回はrangeを指定して、カラーパレットの一部領域のみを使用するようにしています。
(例えばタプルで(0,0.8)を与えれば、全体を0~1とした場合の0~0.8を使用するようになる)

# 男女それぞれのグラフに色を指定
# Palettesのrangeは色のどこからどこまでを使用するか。0-1の範囲のタプルで指定。
male_graph = male_graph.opts(opts.Curve(color=hv.Palette("Blues", range=(0, 0.8))))
female_graph = female_graph.opts(opts.Curve(color=hv.Palette("Reds", range=(0, 0.8))))
  :

5b-3.HoloViewsの複合グラフに各種オプションを設定する

前述のNdOverlayや*で合算したグラフは素の折れ線グラフとは扱いの異なる複合グラフとなります。
これらに前項で指定したような線の色など折れ線グラフ特有のオプションを設定しようとするとエラーになります。
複合グラフに対し線の色などの各グラフ特有の設定を行なう場合、モジュールのoptsを使用します。

from holoviews import opts
  :
# 男女それぞれのグラフに色を指定
# Palettesのrangeは色のどこからどこまでを使用するか。0-1の範囲のタプルで指定。
male_graph = male_graph.opts(opts.Curve(color=hv.Palette("Blues", range=(0, 0.8))))
female_graph = female_graph.opts(opts.Curve(color=hv.Palette("Reds", range=(0, 0.8))))
  :

複合グラフ.optsの引数に、opts.グラフの種類名(オプションの中身)を指定します。

尚、全グラフ共通のような設定(title、widthなど)は複合グラフでもそのままoptsメソッドで指定可能です。

# グラフの設定
all_graph = all_graph.opts(title="喫煙率の推移", xlabel="年次", ylabel="喫煙率",
        width=600, height=400, legend_position="right", show_grid=True)

これで一通りのグラフ作成の説明は終了です。お疲れさまでした。

6.まとめ

Bokeh

  • figureオブジェクトを生成し、そこにグラフや凡例などの要素を付け足していくイメージ
  • HoloViewsに比べるとコード量は増えてしまうが、それほど難解ではない
  • ただし、色回りの設定は何かと面倒

HoloViews

  • グラフオブジェクトを生成した後、それを乗算記号(*)などで重ね合わせていくイメージ
  • コードがすごくシンプルに書ける。色回りで悩むこともあまりない
  • dimensions(kdims、vdims)の概念が少し特殊
所感
  • どちらのライブラリも日本語の情報量が限られており、英語のドキュメントを読み解くのに苦労しました
    (英語力が乏しいので間違ってたこと書いていたらごめんなさい。。。)
  • HoloViewsで細かい設定をしていくのに、Bokehの知識が必要だったりするので、今回Bokehの理解が進んだことは今後HoloViewsを使っていく上でもプラスに働きそうです。
7
12
3

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
7
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?