matplotlib inlineでSVGや高画質なPNGを使う

Python のグラフ描画パッケージ matplotlib のグラフが Jupyter Lab / Jupyter Notebook 上でボヤけて表示されるのが嫌だったので、それを改善する方法を調査した。

なお、ここで紹介する手法にはどれもデメリットがある。完璧を求めて変にストレスを感じるくらいなら、むしろ matplotlib 標準設定のまま使うことをオススメしたい。

動作確認環境

直接的にインストールしたりはしないと思うけれど、依存関係としてインストールされた重要なパッケージ matplotlib_inline は v0.1.3 だった。

高解像度な PNG 画像で出力する

実現方法

以下のように matplotlib_inline.backend_inline.set_matplotlib_formats() 関数に "retina" を指定して実行する。なお %matplotlib inline の前で実行しても後で実行しても大丈夫。

import matplotlib_inline.backend_inline

# 高解像度な PNG でグラフを出力する
matplotlib_inline.backend_inline.set_matplotlib_formats("retina")

retina を指定すると縦横それぞれ 2 倍(よって画素数 4 倍)の画像が出力され、それが標準のグラフ描画領域に詰め込んで表示される。簡単に言い換えると、「4 倍精細な画像」でグラフが表示されるようになる。これでボヤける環境は、まず無いと思う。

メリット

「表示を綺麗にする」という面では完璧で、これにしておけば確実に綺麗になる。

デメリット

画素数が 4 倍にもなるので、かなり画像サイズが大きくなる(後述)。

出力を残したまま .ipynb ファイルを Git レポジトリにコミットしたり、HTML 等に変換して誰かに送る際には注意した方が良いと思う。

ベクトル画像の SVG 形式で出力する

実現方法

ほぼ高解像度 PNG の場合と同じで、以下のようなコードを実行する。やはり %matplotlib inline の前でも後でも大丈夫。

import matplotlib_inline.backend_inline

# ベクトル画像形式の SVG でグラフを出力する
matplotlib_inline.backend_inline.set_matplotlib_formats("svg")

svg 画像はベクトル画像なので、描かれた「線」はどんなに拡大してもボヤけることはない。よって Notebook 上でも綺麗な表示となる。

メリット

基本的には綺麗な出力になる。また、少し試した限りでは高解像度な PNG (retina 指定)よりもデータサイズが少なく済むことが多い印象だった。

デメリット

端的に言うと、画像の表示には向いていない。imshow() で写真を描画してみると、標準状態と同じボヤけ具合となる。

これは、あくまで本設定は「出力形式をベクトル画像形式に変えるだけ」であり、内部的な解像度を上げているわけではないことが背景にある。というのも、出力先を SVG にしても matplotlib は出力画像を 72 ppi (matplotlib.rcParams["figure.dpi"] で取得・変更可能)で相変わらずレンダリングしている。つまり 72 ppi のグリッドに点や線を配置しているのだから、高解像度ディスプレイ環境では相変わらず補完が必要な状況。ここで、「線」についてはベクトル画像ゆえに完璧な補完が可能となり、よって線画的な(例:棒グラフ)グラフは綺麗に描画される。しかし「点」の集合として描かれた映像(つまりラスター画像)については、補完のしようがないため matplotlib 標準の表示とまったく同じ結果になってしまう。

補足

ちなみに、SVG を知っていれば「SVG 出力なら日本語フォントの設定をせずに表示できるのでは?」と思う人もいるかもしれないけれど、残念ながら不可だった。生成された SVG 画像のソースを確認したところ、matplotlib はフォントのグリフ(字形)をアウトライン化して埋め込んでいる。なので、日本語フォントを指定しなければグラフ中で日本語を使えない不便さは SVG 指定では解消されない。残念。

その他の形式について

matplotlib_inline 0.1.3 のソースを参照すると、以下の形式がサポートされているらしい。

  • png
  • retina
  • jpeg
  • svg
  • pdf

retina は前述のとおり高解像度な PNG なので、これは「形式」というと変だなとは思う。ちなみに、「Retina な(画素数 4 倍な)JPEG」は可能なのかというと、そのような画像形式を定義して追加登録すれば使えるらしい (参考)。

画像形式とデータ容量

画像形式の指定と描画内容の組み合わせごとに、テキトーなグラフを出力してブラウザから「名前を付けて保存」し、データ容量を調べた結果を次の表にまとめる。

データ容量(kB) 棒グラフ 散布図 写真
jpeg 14,973 15,328 10,020
png 30,939 90,079 3,208
retina 91,232 342,631 7,161
svg 86,909 125,082 12,938

総じて JPEG が一番小さくなるようだった。ちなみに PNG は透明色を使いたいらしく 32 bit 色のフォーマットが使われており、matplotlib 開発チームがデータ容量の節約より画質を明確に優先していることが感じ取れる。SVG はベクトル画像なので棒グラフでは小さくなるだろうと期待したところ、JPEG よりも容量が大きいという意外な結果に。不思議に思って出力された SVG の中身を覗いてみたところ、使用された全グリフのアウトラインが埋め込まれていた。なるほど、そりゃあ重たくて当然だね(text 要素として出力すればよいのに、などと思わなくもないけれど、フォントレンダリングも自前で行っているようなので難しいのだろうなぁ…)。

私見

以上の結果からの私見を書いておく(2023-08-14更新)。

  • 作業中は svg、ただし画像を扱う場合は retina
    • 分析作業中はキレイな方が気分が上がるので。
  • HTML エクスポート時は png または jpeg
    • データ容量を抑えられるため。

余談: 他の書き方について

インターネットで検索すると以下のような書き方も見つかるけれど、これらは古い書き方ということで、今は非推奨になっている。

from IPython.display import set_matplotlib_formats
set_matplotlib_formats("svg")  # Deprecated since IPython 7.23

また、以下のような magic コマンドで出力形式を切り替える方法もある:

%config InlineBackend.figure_formats = ["svg"]

こちらについては、再現手順を確立できていないのだけれど、たまに効かなくなることがあった。他の方法と混ぜて使わなければ問題ないかもしれないけれど、一応注意しておこうかな。

余談: 日本語フォントの指定方法

ついでに、個人的に使うことが少なく忘れがちな「日本語フォントを matplotlib で使う方法」も書いておこう。

import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = "Meiryo"

あるいは:

import matplotlib as mpl
mpl.rcParams["font.sans-serif"] = "Meiryo"

なお matplotlib.rcParamsmatplotlib.pyplot.rcParams は同じオブジェクトを指しているので、上記はどちらも同じ結果になる(v3.5.1 ソース上の該当箇所

以上。