ill-identified diary

所属組織の見解などとは一切関係なく小難しい話しかしません

Python ユーザでも『データ可視化入門』で練習できるようにパッケージを作った + Plotnine との互換性ガイド

この記事は最終更新日から3年以上が経過しています



概要

Python での作例はここにある

https://github.com/Gedevan-Aleksizde/pysocviz/tree/main/notebooks

2021/8/8: 注意書きが英語のままだったので日本語版も追加した: https://github.com/Gedevan-Aleksizde/pysocviz/tree/main/notebooks/jp

ちなみに既に似たようなことをしている人はいるが, 5章までやって1年くらい更新していない (ただし6章以降はRの他のパッケージに依存した作例が多いことに注意)

https://github.com/jkgiesler/healy-viz


pysocviz が提供する機能

  • 現在の ggplot2 エコシステムをより再現するための関数
    • たとえば本文中でも使われている ggthemes::theme_wsj, theme_economis, ggrepel::geom_text_repel, scales::wrap_format をそこそこ似たような感じで再現した. また, broom::tidy, coefplot, statebins なども本文中で紹介されるグラフを作るのに必要なので, 限定的であるが Python で機能を再現した. notebookから抜粋するとこのような見た目になる.
  • f:id:ill-identified:20210806200335p:plain
    theme_economist っぽいテーマ
    f:id:ill-identified:20210806200358p:plain
    statebins の再現

  • Rグラフィックスの色コード
    • matplotlib の色コードの種類は少なく, plotnine で gray70 などと指定してもエラーが出るので, dcolors という名前のディクショナリに ggplot2 で標準的に使われている色名に対応するカラーコードを収録した. よって color geom_line(color=colors.get('gray70')) などと書くことができる.
    参考: matplotlib の色名一覧ggplot2 の色名一覧

    R の ggthemes, colorblindr パッケージで用意されているカラーユニバーサルデザインに基づいた配色のスケール関数も用意した.

    f:id:ill-identified:20210812185712p:plain
    カラーユニバーサルデザインに基づいた配色


  • “Data Visualization: A Practical Introduction” に対応した資料
    • ただし, 本文中の ggplot2 を使った図の再現に最低限必要なコードのみの記載で, (著作権ガン無視になるので当たり前だが) なぜそうするか, といった説明はほぼない. 原著を見るか翻訳版を買ってほしい.
      • dplyr の使い方とか回帰分析関係とかの使い方に対応する部分は省略している (グラフ作成に必要な部分はコードがある)

ggplot2 と同じようにできないところとその対策

網羅的に書くのは難しいので, 主に原著の作例を再現する際に気づいた点を挙げていく.


aes() にクオートされてない変数を指定できない

Python と R の構文の違いから, aes(x='x') のように文字列として指定する必要がある. しかしこの文字列は式として評価される (Pandas の .eval() と似ているが, 適用できる処理は aes() のほうが多く便利である). 基本的に変数は numpy 配列として扱われるため, aes(x='x.astype(str)', y='np.log(y.mean()**2)') のような書き方も可能になる.

逆に変数ではなく文字列として与えたい (補助線に aes() を与えたいときなど) 場合は aes(color='"line1"') のような書き方で対処できる.

加えて, v0.7 からは aes() 内でのみ使える関数 factor(), reorder() が使用できる. これは gplot2 で使える同名の関数の機能を提供するもので, 前者はカテゴリ変数の再定義, 後者はカテゴリ変数の並び順を再定義する.


R のように改行できない

これも構文の違いが原因. R のように

ggplot() +
  geom_point() +
  scale_x_log10()

のような改行は Python ではできない1. しかし一方で Python なら += が使用できるため

g = ggplot()
g += geom_point()
g += scale_x_log10()

という書き方はできる (R ユーザ向けに説明すると, g <- g + geom_* と同じ).


ggplot2 で使えた色名が使えない

たとえば color="gray70" などと書くとエラーが発生する. これは plotnine が matplotlib ベースであり, matplotlib にはそのような名前の色が登録されていないのが原因. pysocviz.properties.dcolors というディクショナリを用意したので,

from pysocviz.properties import dcolors
ggplot(d, aes(x='x', y='y'), color=colors['gray70']) + geom_point()

みたいな書き方をする. なお, 単色の場合は ggplot2 より少ないが, 一方でカラーパレットは matplotlib のほうが豊富である (後述).


ggplot2 で使えた linetype が使えない

これも matplotlib が原因の制約. ggplot2 の破線 と異なり, matplotlib の破線は3種類しかなく, 名称も異なる. この修正はちょっとやそっとではできないので, pysocviz.properties.linetypes に Plotnine で使用できる実線 + 破線3種類の名前エイリアスのみ用意した. 使い方は dcolors と同じ.


文字化けの回避

20201/8/9: plotnine の話になってなかったので加筆修正

これは去年 R の文字化け問題をまとめた時にも少し触れたが, 放置していたのでまだ完全には解決していない. plotnine で, かつ Jupyter やインタラクティブウィンドウでの表示に限定する (たぶんPNGやJPGも大丈夫. PDF形式での保存とかは考えない) ならば, theme() 関数にフォントファミリを指定するだけでなんとかなるだろう (おそらく Win/Mac/Ubuntu どれでも同じ). これが例

from plotnine import *
import pandas as pd

d = pd.DataFrame(dict(x=range(3), label=list('あいう')))

g = ggplot(d, aes('x', 'x', label='label'))
g += geom_point()
g += geom_label(nudge_x=.1, nudge_y=.1) 
g += labs(x='あああ', y='いいい', title='ううう')

g + theme(text=element_text(family='Noto Sans CJK JP'))

私の PC (Ubuntu 20.04) では以下のようになった. family='Noto Sans CJK JP' の部分は当然それぞれの環境にインストールされているフォントによって変わる. 例えば最近の Mac なら 'Hiragino Sans' (ヒラギノ角ゴ, 少し古いバージョンだと Hiragino KakuGo ProN みたいな別の名前になっていた気がする?), Windows 8以降なら 'Yu Gothic' (游ゴシック) とかで動作すると思う.

f:id:ill-identified:20210809114535p:plain
日本語表示の例

毎回 theme(...) と書くのが面倒な場合は以下のように theme_set() でデフォルトのテーマを変更してしまう.

theme_set(theme_gray(base_family='Noto Sans CJK JP'))

なおデフォルトの灰色の背景色が気に入らないなら theme_graytheme_bwtheme_classic とかに変更する. matplotlib のデフォルトの雰囲気に近いものはそのまんま theme_matplotlib という名前になっている. ただし theme_matplotlib には base_family 引数がないのでこのように書かねばならない.

theme_set(theme_matplotlib() + theme(text=element_text(family='Noto Sans CJK JP')))

しかし私は R と比べて matplotlib のフォントの認識処理をまだよく理解できておらず, 標準フォントでもたまに認識できないことがある (例えば Mac では游ゴシックはなぜかデフォルトでは認識されてない) 原因を特定できていない. また, PDF 画像として保存する際の設定も少し複雑で, まだいい方法を発見できていない.

ちなみに matplotlib を使用した全般での話としては, matplotlib.rcParams['font.family'] に, 自分の環境で使用可能なフォントファミリ名を正しく入力することで対処できる. matplotlib のフォント関連のネット上の解説はミスリードなものや間違ったものがかなり出回ってるので早めに訂正したいが, もう少し時間がかかる.


ggrepel パッケージの利用

テキストの重なり回避は geom_text/geom_label で手動調整するか, adjustText をインストールした上で adjust_text={'arrowprops': {'arrowstyle': '-'}} を指定すると ggrepel を使用したものに近いものが出力される. いちおう, ggrepel::geom_text_repel と同等の関数も pysocviz に用意したが, どうもうまく動かないことがあるようだ.


scales::percent などの単位・スケール指定

Plotnine 開発者が作った mizani パッケージを使用する. 例えばパーセント表記は以下のような書き方で可能.

from plotnine import *
from mizani.formatters import percent_format
ggplot(...) + geom_point() + scale_x_continuous(labels=percent_format())

詳細は公式ドキュメントや pysocviz の作例を参考に.


テーマや色パレットのプリセットを変更したい場合

色パレットの名称は RColorbrewer と matplotlib とである程度互換性がある. むしろ matplotlib のほうが, 最初から viridis などを使えたりとプリセットが豊富

具体的には以下のページで一覧を見ることができる.

matplotlib https://matplotlib.org/stable/tutorials/colors/colormaps.html
Rcolorbrewer https://www.r-graph-gallery.com/38-rcolorbrewers-palettes.html

上記に記載されている色パレット名で指定する場合は scale_color_brewer, scale_color_cmap, scale_colro_cmap_d を使用する. (後者は discrete に対応). *_brewer は本家と同じ構文だが, *_cmap, _cmap_d は連続・離散で最初から分けられているのでよりシンプルになっている.


subtitle/caption が表示されない

現時点(v0.8)では labs() にこれらを指定してもサブタイトルやキャプションは表示されない. これらに書き込みたいテキストは, 代わりにタイトルや軸ラベルに押し込むか, annotate() で描画領域の邪魔にならないところに書くしかない.


複数のグラフを連結できない

現時点では plotnine に種類の異なるグラフを連結する機能はない2. 原著の Ch. 8 でも cowplot パッケージを使う例があるが, これは同じ種類のグラフを連結しているので facet_grid である程度再現することができる.

ただし, facet 系の処理も同様の制約が理由で, strip label の位置移動 (strip.position) など本家 ggplot2 にある機能の一部が実装されていない.

しかし, あくまで個人の経験の範囲だが, 種類の異なるグラフを連結することが可視化として必須の処理とは思えない (単に1つの画像ファイルとしてまとめたい, というのは可視化とは別の次元の問題とする) ので, そこまで大きな問題ではないと考えている (つまり他のグラフの描き方を模索してほしい)


hjust/vjust が使えない

Plotnine ではそれぞれ ha/va という名前に置き換えられている. また, 0-1 の数値で指定できたものがそれぞれ以下のような選択式になっている.

  • ha: 'left' - 'center' - 'right' (ha='left'hjust = 0 に対応)
  • va: 'top', 'bottom', 'center', 'baseline', 'center_baseline'

グラフ内の図形やテキストの大きさのバランスがおかしい

ggplot2 とサイズの単位が違うため, 絶対値の指定だとバランスが変わる. たぶん ggplot2 は図形とテキストの単位が違う一方で, plotnine は統一している. (一方でインチで指定する必要があるものもある). また unit() に対応するものはない.

ggplot2 と違い, geom_point, geom_line, などの点や線の sizegeom_text()size はどちらもポイント単位である. 一方でテーマでの設定の多くはインチ単位である. 相対的なサイズが変わってしまうのはおそらく主にこのあたりが関係していると思われる.


geom_smooth/stat_smooth で一般化加法モデル (GAM) による平滑化ができない

現時点 (v0.8) では GAM はサポートされていない. 自分で GAM のパラメータや信頼区間を計算してプロットする必要がある. (statsmodels に GAM のクラスがある)

なお, 使用可能なモデル一覧は公式ドキュメントのこのページに書いてある.


geom_quantile の method 指定ができない

本家は qr と rqss から選択できたが plotnine にはまだこの機能はない. Python で使いやすいライブラリがあるかは不明


geom_smooth/stat_smooth の formula でアレが使えない

statsmodels の R 風 formula 構文に基づいているので R の formula の完全なエミュレーションはできない.

この formula の使い方の詳細は statsmodels と pasty の公式ドキュメントを参照.


凡例を下に配置すると軸ラベルや目盛りに重なったり, はみ出したりしてしまう

残念ながらこれも matplotlib の仕様が関係している. どうしても凡例を下部に表示したい場合, 現状は issue #245 で提示されている解決策を試すしかない.

  • テーマの legend_position にはキーワードだけでなくタプルでデフォルトからの相対座標を指定することができる.
  • テーマの legend_direction='horizontal' で凡例の水平配置を強制することで, 下へのはみ出しを防ぐ
  • テーマの subplot_adjust に matplotlib の subplot パラメータの設定をディクショナリで与える (設定できる項目は matplotlib の公式ドキュメント を参考に)

この方法は作例でも数箇所で使っている.


あんたの書いたサンプルコードが見づらい

Python の仕様上1行80字に収めるにはこういう改行するしかないので我慢してください.

シングル・ダブルクオーテーションが混在しているのは原著からコードをコピペしたときに修正するのがめんどくさかったらから.


その他豆知識

  • データフレームを間違って指定した (名前を間違えて存在しないものを指定するなど) した場合, データフレーム名についてのエラーよりも先に列名がないというエラーが出ることがある (ので原因に気づきにくい)

  • jupyter 上で <ggplot: (XXXXX)> のような表示が邪魔な場合は, (ggplot() + geom_point).draw(); のように .draw(); を呼び出すことで表示させないことができる. 末尾の ; も必須. これも matplotlib の制約が関係している.



よくある質問 (?)

2021/8/8 追記: はてなブックマークのコメントを眺めたところ, ggplot2 に詳しくない人間を想定した, ggplot2/plotnine がどこまでできて, 何ができないのかの補足説明が足りないと気づいたので追記した.


Q: matplotlib の何が良くないの?

同じようなグラフを書こうとすると, ほとんどの場合コードが長くなり過ぎるし, 不正確になる恐れがある. 例えば ggplot2/plotnine は散布図や折れ線グラフを色分けした時, 対応する凡例を自動で表示してくれる. 一方で matplotlib はそれも手動で書く必要がある. そしてそのように手動で行う必要のないものを手動で書くという運用は先日書いたようにミスを増やす恐れがあるので使いたくない.

というのは半分建前で matplotlib の煩雑な構文をそもそも覚える気がしない


Q: plotly や bokeh や seaborn じゃだめなの?

Plotnine を選択したのは (1) R からの転換が容易 (2) HTML 以外の媒体でも適切にグラフを描画できることにこだわった結果なので, そういうのが必要ないなら plotlybokeh でやっても問題ないと思われる. 特に plotly はなんとなく ggplot2 の構文に影響を受けている感じなので, 比較的参考にしやすいかもしれない.

最後の seaborn については, これも matplotlib がバックエンドで, かつ設計思想は ggplot2/plotnine と似ている. しかし私がこれまで使ってきて, (1) ggplot2/plotnineaes()scale_*, theme_* 関数に見られるような, 「データの意味を定義する部分」と「デザインを決める部分」との分離が, seaborn では徹底していないと感じたこと, (2) 何度か厄介な不具合3に出くわしたことがあった, (3) デフォルトデザインがちょっと過剰装飾でクドい (個人の感想です) のでだんだんと使わなくなった. plotnine もバグが皆無なわけではないが, 比較的妥協できるレベルのもの (少し見栄えが悪くなる程度) が多い. ただし, 公平な評価のために書いておくと, 私はそれ以前に長いこと ggplot2 を使ってきているので plotnine の扱いに慣れるのは早かった.


Q: ggplot2/plotnine は本当に最強無敵なの?

ライバル視している matplotlib (実際には plotnine のバックエンドで mpl. が動いているので対立させるのはおかしいが) は既に古くからあって, ググれば描きたいグラフに近い作例がすぐ見つかる, という利点があるかもしれない. しかし, Python ユーザはそもそも今まで ggplot2 の使い方についてググったことはないのではないだろうか?

私の考える, matplotlib と比較した際の plotnine の利点は,

  1. (既に書いたように) 凡例の色分けなど, ある程度自動で調整しても問題ないような, 煩雑な設定から解放される
  2. 抽象化された構文なので, 基本的なルールを覚えてれば応用がききやすい
  3. 上記に関連して, グラフの意味の定義とデザインの定義が分かれている構文なので, 後からコードの一部を書き換えて微調整したり使いまわしたりしやすい
  4. ggplot2 のコード資産も含めれば, それなりに作例が豊富
    • 少なくとも目盛りの表示調整とか初心者がわかりにくいところはだいたいスタックオーバーフローとかで既に質問と回答があると思う
    • (ただしそれぞれの作例がどれくらい豊富かの定量的な評価は難しそう)

が挙げられる.

ただし, 特定の限定的な分野で使われるグラフは自分で構築する必要がある. 本文7章でも紹介されている州ごとの得票率をカルトグラム状のヒートマップで表示させるにはかなり工夫が必要だし, ローソク足チャートなんかもやや手間がかかる (R の場合は金融関係特有のグラフは quantmod などの目的特化の専用パッケージがあるし, matplotlib と似たような構文の基本パッケージも使われていたりする).

理想というか願望を言うなら, Python でも R における ggplot2 のように, plotnine をベースとした派生パッケージのエコシステムがユーザーコミュニティから自然に生まれてくれればもっと便利になるだろう. (というか私の pysocviz が嚆矢になってくれるとありがたい)

一方で明確に欠点と言えるのは,

  1. 既に matplotlib など他のフレームワークに慣れている人にとっては, 学習し直すことはコストだと感じる
  2. 3次元散布図や surface plot が描画できない
  3. 円グラフが書けない

あたりだと思う. (1) はたぶんどうしようもないので反論できない.

しかし (2, 3) のような「〇〇ができない」というのは必ずしも問題ではなく, 「やらないほうがいいことはできない or やりづらいようになっている」と捉えることもできる. plotnine には円グラフを描く機能はないし, 3次元棒グラフなどは言うまでもない. これらは『データ可視化入門』の1章で不適切な可視化手段として批判されており, gplot2/plotnine 開発者もおそらくするべきでないと考えているので使えない. 棒グラフを3Dにしても新たな情報は提示できないし, 円グラフも適切に比率の情報を見せることができないため, 代わりにパレート図や帯グラフなどを使ったほうがいい4.

一方で3次元グラフの中では3次元の散布図や surface plot は, 真面目な用途でもそれなりに使われているようである. しかし ggplot2/plotnine は3次元のグラフ全般を想定していないので描くのは非常に困難である. ただし, ものによっては代替不可能かもしれないが, 2次元のヒートマップ (あるいは等高線) や, 複数系列の折れ線グラフで伝えたい情報を示せるかもしれない.

とはいえ, 『データ可視化入門』で導入されているグラフ作成のルールがあらゆる分野で合意されたわけではないことも事実である. まず "socviz" という名前の示すように, この本は社会科学分野での応用を考えた作例が多く, 3次元プロットは重視していない. 例えば「指導教授から『このデータはこういうグラフで示すのがルールだ (学会での慣例だ)』と言われた」「上司/顧客からこのグラフで描いてほしいと言われた」とか言われると私個人では解決するのは難しい.

そんなわけで要約するとこうなる: ある分野に特有なグラフばかり描く必要がある場合は専用パッケージを使う必要もあるし, 全くできないこともあるので plotnine は最強無敵ではない. しかしいろいろなグラフを描く必要がある場合は汎用的に使えて慣れれば応用もきく ggplot2/plotnine のほうが便利ではないか, ということになる.


その他愚痴

socviz のデータセットを Pandas データフレームに移植しようとして気づいたのだが, socviz のデータセットには変な属性情報が混入したデータセットがいくつか含まれていた. おそらく haven パッケージあたりで stata からインポートしたのだろうが, 例えばクラス名が labelled になっている変数があるなど, 誤作動の原因になりそうな余計な属性情報が見られた. 原著の範囲であれば問題は発生しないが, 今回のようにデータセットをエクスポートする際はこういった不要な属性情報はなるべく消すようにしたほうがいいと思う.

7章の statebins (全米の州ごとの選挙結果をプロットしているもの) の自作がとても大変だった. しかもこれを見るのはほとんど日本人ユーザだろうから, この作例以外で役に立つ可能性がない.




  1. バックスラッシュを使うという手があるが後から修正するのが大変なのでお勧めしない↩︎

  2. それをやるのは matploblib の仕様上大変に面倒/実現不可能だと開発者が述べている, issue #46 参照↩︎

  3. そのうち1つは前回紹介している: https://ill-identified.hatenablog.com/entry/2021/07/28/231922↩︎

  4. ただし ggplot2 には円グラフを描く機能がある https://www.r-graph-gallery.com/piechart-ggplot2.html↩︎