サイトアイコン 天文学者のpython・音楽・お料理レシピ

Matplotlib | 自作のスケールを定義する方法: Custom scale(Python軸スケール5)

みなさんこんにちは!このブログでは主に

の4つのトピックについて発信しています。

今回はPythonデータ解析です!Matplotlib、軸のスケールシリーズ第5回!Custom scaleという枠組みをつかって、オリジナルのスケールを定義する方法を解説します!xxxScale、xxxTransform、InvetedxxxTransformという3つのClassを作成することで、Logスケールなどと同じように軸のスケールをオリジナルのスケールに設定することができます。

この記事を読めば、Matplotlibであらかじめ用意されていないスケールを定義し、軸のスケールを設定することができるようになります!ぜひ最後までご覧ください。

Kaiko

この記事はこんな人のお悩み解決に役立ちます!

  • Pythonの作図で軸のスケールを変えたいけどどうやる?
  • あらかじめ用意されていないスケールを自分で定義して使うには?
  • 負の値を含むデータを対数スケールでプロットするには?



Abstract | 自作のスケールはCustom scaleで定義!

Matplotlibで自作のスケールを定義するには、Custom scaleという枠組みを使うのが便利で実用的です。Custom scaleで自作のスケールを定義するときは、xxxScale、xxxTransform、InvertedxxxTransformという3つのclassを作成します。さらにregistorしておくことで、通常の対数スケールなどと同じようにax.set_yscale("xxx")などと軸の設定を行うことができます。

Cusom scaleは、FunctionScaleに比べるとコード量が増えるデメリットはありますが、ax.set_yscale("xxx")などと文字列を使った軸の設定、目盛りの設定、軸の範囲の設定を行うことができます。見やすい図を作ることができるようになりますので、ぜひこの記事を参考にMatplotlibでの作図をしてみてください!



Background | PythonのスケールとScaleオブジェクト

散布図やヒストグラム、その他さまざまな図を作る際、軸のスケールを適切に設定する必要があります。Pythonの作図ライブラリMatplotlibでは、デフォルトの線形スケール以外に様々スケールでプロットすることができます。プロットしたいデータの分布に合わせて適切なスケールを選ぶことが必要です。

本シリーズの過去記事

では、線形スケールでは図を綺麗に表示できない例や、Matplotlibに予め定義されたスケールを使ってプロットする方法、Matplotlibのスケールを制御するScale Classについて解説してきました。

今回の記事では、Custom scaleという枠組みを使って自作のスケールを定義する方法を解説します。対数など、あらかじめ用意された7種類のスケール以外の変数変換を使って新たにスケールを定義することができます。Custom scaleという枠組みを使えば、文字列”xxx”による軸スケールの設定、目盛りの設定などMatplotlibで作図する上で必要な機能を実現することができます。



Data | Custom scaleの概要

オリジナルのスケールを定義することができるCustom scaleという枠組みの概要を解説します。

オリジナルスケールを定義する2つの方法

オリジナルのスケールを定義する方法には

の2つがあります。今回の記事では、これら2つの方法のうち後者のCustom scaleの方を解説していきます。

Custom scaleを使うほうが実用性が高い

Custom scaleの枠組みを使うほうがプロットする際の実用性が高いです。FunctionScaleでは変換関数と逆変換関数を用意しておくだけで、軸のスケールを所望のものに設定することができます。ところが、文字列を使った軸の設定ax.set_xscale("xxx")や軸の目盛の設定、軸の範囲など細かい設定はできません。つまり、FunctionScaleはコード量は少ない利点がありますが、実用性が低い欠点があります。

他方で、Custom scaleでは、xxxScale、xxxTransform、InvertedxxxTransformという3つのclassを定義する必要があるため、実装の際のコード量はFunctionScaleの場合に比べて多くなります。一方で、文字列を使った軸の設定、軸の目盛の設定、軸の範囲の設定など細かいながらも作図に必要な機能を一通り実現することができます。

FunctionScaleとCustom scaleのメリット・デメリットをまとめて比較すると図1のようになります。

図1. FunctionScaleとCustom scaleのメリット・デメリット比較



Custom Scaleの構成要素

Custom scaleの構成要素は

の3つのclassです。オリジナルのスケールを定義するにはこれら3つのclassを作成する必要があります。 ちなみに、これら3つのclassはmatplotlibであらかじめ用意されたスケール(例: Log, Logit, Symlog, Asinhなど)でも定義されています。

xxxScale、xxxTransform、InvertedxxxTransformの相互関係は図2のようになっています。Matplotlibで作図のときに軸の設定に使うのがxxxScale、そこから参照されるのが変換関数を与えるxxxTransform、逆変換関数を与えるInvertedxxxTransformとなります。

図2. xxxScale、xxxTransform、InvertedxxxTransformの構成要素と相互関係

目盛りの設定: set_default_locators_and_formatters()

Custom scaleの枠組みを用いたときに、目盛りの設定ができるのは、xxxScaleに目盛りの設定をするためのメソッド

set_default_locators_and_formatters()

を定義しておくことができるからです。このメソッドの中で大目盛りと小目盛りの間隔や数値の書き方を定義します。例えばLogScale classでは、

    def set_default_locators_and_formatters(self, axis):
        # docstring inherited
        axis.set_major_locator(LogLocator(self.base))
        axis.set_major_formatter(LogFormatterSciNotation(self.base))
        axis.set_minor_locator(LogLocator(self.base, self.subs))
        axis.set_minor_formatter(
            LogFormatterSciNotation(self.base,
                                    labelOnlyBase=(self.subs is not None)))

となっています(参考: matplotlibのドキュメント https://github.com/matplotlib/matplotlib/blob/v3.10.1/lib/matplotlib/scale.py#L303)。ここでは詳細を省きますが、大目盛りと小目盛りの位置とフォーマットを指定することができます。

軸の範囲の設定: limit_range_for_scale()

Custom scaleで軸の範囲を設定するには、xxxScaleのなかに

limit_range_for_scale()

というメソッドを定義しておきます。例えば、LogScale classでは、

    def limit_range_for_scale(self, vmin, vmax, minpos):
        """Limit the domain to positive values."""
        if not np.isfinite(minpos):
            minpos = 1e-300  # Should rarely (if ever) have a visible effect.

        return (minpos if vmin <= 0 else vmin,
                minpos if vmax <= 0 else vmax)

となっています(参考: matplotlibのドキュメント https://github.com/matplotlib/matplotlib/blob/v3.10.1/lib/matplotlib/scale.py#L316)。



Method | Custom scaleでオリジナルのスケールを定義する方法

ここからは実際のサンプルコードを書きながら、Custom scaleの枠組みを使ってオリジナルのスケールを定義する方法を紹介していきます。

Jupyter Notebookを使ってコードの書き方も記載するので、一緒にコードを動かしながら読んでみてください!ここで用いた関数を通常のPythonソースコードにコピーして使ったり、Pythonのファイルにまとめてimportして使ってもOKです!

Resultの章にJupyter Notebookでの実装例を掲載するので、そちらを参照していただいても構いません。この章では、簡単な解説とともにソースコードを一つずつ提示します。解説が不要な方はResultの章に飛んでいただき、Jupyter Notebookでの実装例を参考にする方が早いかもしれません。

各種事前準備

まずはJupyter Notebookのファイルを開きましょう。Jupyter Notebookの使い方については過去記事「python入門講座|pythonを使ってみよう2(Jupyter Notebookを使う方法)[第5回]」もご覧ください!

ライブラリのインポート

Jupyter Notebookを開いたらライブラリをインポートします。今回はNumPyとMatplotlib関連のライブラリをいくつかインポートしておきます。以下を実行します。

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

前提: Log-Modulus変換

ここではオリジナルのスケールとして、Log-Modulus変換を使ったLog-Modulusスケールを作成します。Log-Modulus変換は負の値も扱うことができる対数変換です。本シリーズ過去記事で扱ったSymlogやAsinhと似たものですが、変換関数がシンプルかつ連続関数であるというメリットがあります。

Log-Modulus変換の定義

Log-Modulus変換は
$$
f(x) = \mathrm{sign}(x) \log(|x| + 1)
$$
と定義されます(John and Draper, 1980)。

関数型の確認

Log-Modulus変換の関数型をプロットして確認しておきましょう。以下を実行してください。

ax = plt.subplot(111)
ax.set_xlabel('x')
ax.set_ylabel('y')
x = np.r_[
    -np.logspace(1,np.log10(1),1000),
    np.linspace(-1,1,1000),
    np.logspace(np.log10(1),1,1000)
]
y = np.sign(x) * np.log(np.fabs(x)+1)
ax.plot(x, y)

実行結果は下記、図3のようになります。

図3. Log-Modulus変換の関数型

Log-Modulus変換の特徴

Log-Modulus変換は、SymlogやAsinhと同様、変数が正負どちらであっても用いることができる対数変換です。

Symlogに対するメリット・デメリット

Log-Modulus変換にはSymlogに対して

というメリットがあります。同時に

ということがデメリットとなります。

Asinhに対するメリット・デメリット

Log-Modulus変換にはAsinhに対して

というメリットがあります。同時に

というデメリットがあります(Symlogでも同じ)。

Log-Modulus変換は1と比べて小さい値は線形のままになる

デメリットの方を少し掘り下げます。たとえば\(x=10, 100, 1000\)という値は、Log-Modulus変換によって\(f(x)=1.04, 2.00, 3.00\)と変換されます。10, 100, 1000という桁での変化が1, 2, 3という線形な変化になっています。したがって対数変換に近い変換となっていることがわかります。

一方で、値が1付近より小さい場合、例えば\(x=0.1, 0.01, 0.001\)はLog-Modulus変換によって\(f(x) = 0.04, 0.004, 0.0004\)と変換されます。桁での変化がそのまま桁での変化として残ってしまいます。すなわち\(|x|\)の値が1と比べて小さい場合には、対数的な変換が行われません。したがって、正負の値が混在する場合にLog-Modulus変換で対数的な変換をしようと思っても、\(|x|\)が小さい場合には役立ちません。そのような場合には\(|x|\)をあらかじめ十倍、百倍とスケーリングして(例えば、パーセント表示にして)1と比べて大きな値にしておく必要があります。

正負混在のデータを対数変換するときのポイント

結局、正負混在のデータを対数変換するときには

という選択肢がありますが、

などを考慮して使いやすいものを選択する必要があります。



Customスケールの作成(例: Log-Modulusスケール)

目盛りの設定など、細かい設定まで行うにはCustom scaleで自作スケールを用意します(参考: matplotlibのドキュメント https://matplotlib.org/stable/gallery/scales/custom_scale.html)。例として先ほど説明したLog-Modulusスケールを作成します。

の3つのclassを定義します。

LogModulusTransformを作成する

まずはLogModulusTransform classを作成します。Log-Modulus変換の変換関数を定義するclassです。引数はmatplotlib.scale.Transformとなります。 以下を実行しましょう。

# transformer class
class LogModulusTransform(mpl.scale.Transform):
    input_dims = output_dims = 1

    def __init__(self):
        super().__init__()
        # define parameters to set locators and formatters
        self.base = 10.
        self.linthresh = 1.
        self.linscale = 1
        self._linscale_adj = (self.linscale / (1.0 - self.base ** -1))
        self._log_base = np.log(self.base)

    def transform_non_affine(self, values):
        return np.sign(values) * np.log10(np.fabs(values) + 1)

    def inverted(self):
        return InvertedLogModulusTransform()

`def __init__(self):`というメソッドを定義のなかで、self.baseself.linthreshself.linscaleself._linscale_adjself._log_baseというパラメータを定義しています。これらはLog-Modulus変換には使いませんが、LogModulusScale classのなかで目盛りの設定を行うset_default_locators_and_formattersで必要になります。最後のdef inverted(self):というメソッド定義のなかで呼び出されるInvertedLogModulusTransform()は次に定義します。

InvertedLogModulusTransformを作成する

次にInvertedLogModulusTransformを作成します。Log-Modulus変換の逆変換関数を定義する関数です。以下を実行しましょう。

# inverted transform class
class InvertedLogModulusTransform(mpl.scale.Transform):
    input_dims = output_dims = 1

    def __init__(self):
        super().__init__()
        # define parameters to set locators and formatters
        self.base = 10
        self.linthresh = 1
        self.invlinthresh = 1
        self.linscale = 1
        self._linscale_adj = (self.linscale / (1.0 - self.base ** -1))

    def transform_non_affine(self, values):
        return np.sign(values) * (10**(np.fabs(values)) - 1)

    def inverted(self):
        return LogModulusTransform()

LogModulusScaleを作成する

最後にLogModulusScale classを定義します。以下を実行します。

# scale class
class LogModulusScale(mpl.scale.ScaleBase):
    """
    Log-Modulus scale converts positive and negative values
    into quasi-log scale.

    The scale function:
        sign(x) * log(|x| + 1)
    
    The inverse function:
        sign(x) * (10**|x| - 1)
    """
    # scale name
    name = 'logmodulus'

    def __init__(self, axis):
        super().__init__(axis)
        self._transform = LogModulusTransform()

    def get_transform(self):
        return self._transform

    def set_default_locators_and_formatters(self, axis):
        axis.set_major_locator(mpl.ticker.SymmetricalLogLocator(self.get_transform()))
        axis.set_major_formatter(mpl.ticker.LogFormatterSciNotation(10))
        axis.set_minor_locator(mpl.ticker.SymmetricalLogLocator(self.get_transform(), None))
        axis.set_minor_formatter(mpl.ticker.NullFormatter())

LogModulusScaleをRegistorする

自作のLog-ModulusスケールをRegistorする。Registorしておくと、”logmodulus”(`LogModulusScale.name`に格納された文字列)という文字列で呼び出すことができます。以下を実行します。

# register scale
mpl.scale.register_scale(LogModulusScale)

# check scale names
print(mpl.scale.get_scale_names())

LogModulusScaleというclassそのものがmatplotlib.scale.register_scale()の引数となります。実行すると以下のように出力されます。

['asinh', 'function', 'functionlog', 'linear', 'log', 'logit', 'logmodulus', 'symlog']

リストの中に”logmodulus”が含まれていることが確認できるはずです。

Registorされた”logmodulus”スケールをscale_factoryで呼び出すことができます。以下を実行します。以下を実行してみてください。

# try get scale object from name
mpl.scale.scale_factory("logmodulus", ax)

実行結果は以下のように出力されます。

<__main__.LogModulusScale at 0x116a0d460>



プロット例の作成

自作のLogModulusTransform classを使ったプロット例を紹介します。

LogModulusTransformの確認

LogModulusTransform classを使って、Log-Modulus変換が正しく行えることを確認する。以下を実行してみてください。

# get LogitTranform object
mpl_trn = LogModulusTransform()
print('transformer:', mpl_trn)

# get transform example
x_test = np.linspace(-10,10,1001)
y_ref = np.sign(x_test) * np.log10(np.fabs(x_test) + 1) # calculation by hand
y_trn = mpl_trn.transform(x_test) # calculatuion with transformer

# plot transform example
ax = plt.subplot(111)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.plot(x_test, y_ref, lw=5, label="calc by hand")
ax.plot(x_test, y_trn, lw=4, ls='dotted', label="calc by transformer")
ax.legend()

実行すると以下のように図4が表示されます。

図4. Log-Modulus変換の変換関数。青実線は手計算、橙点線はLogModulusTransform classを使用。

手計算(\(y = \mathrm{sign} \log(|x| + 1)\))とLogModulusTransform classのtransform()を使った変換が一致していることが確認できました。

“logmodulus”の文字列で軸を設定してプロット

“logmodulus”の文字列で軸を設定してplotする例を扱います。以下を実行してみましょう。

# set x and y
x = np.random.normal(size=1000)
x2 = x + np.random.normal(size=np.shape(x))
y = np.sign(x2) * (10**(np.fabs(x2)) - 1)  # inverted log-mod transform

# plot with log-scale y
fig = plt.figure(figsize=[8,4])
gs = gridspec.GridSpec(1, 2, wspace=0.3)
axs = {}

# linear scale
i = 0
axs[i] = plt.subplot(gs[i])
axs[i].set_title('Example in linear scale')
axs[i].set_xlabel('x')
axs[i].set_ylabel('y')
axs[i].scatter(x, y)

# log scale
i = 1
axs[i] = plt.subplot(gs[i])
axs[i].set_title('In Log-Modulus scale')
axs[i].set_xlabel('x')
axs[i].set_ylabel('y')
axs[i].set_yscale('logmodulus')
axs[i].scatter(x, y)

実行すると、以下のように図5が表示されます。

図5. Log-Modulusスケールを使ったプロット例

図5の右側、y軸をLog-Modulusスケールでプロットしたパネルでは、正負の値を両方含み、かつ桁で値が変化する対数的なデータの様子を表現することができています。y軸の目盛りの設定はSymlogスケールのものを流用しているため、目盛りの間隔、数字の表記も対数スケールで見やすいものになっています。

このように、Custom scaleの仕組みを使って自作のスケールを作成し、Registorしておくことで、ax.set_yscale("xxx")を使って軸のスケールを変更することができます。通常の対数スケール”log”などと同様に扱うことができて実用的です。ぜひ皆さんもやってみてください。



Result | Pythonのサンプルコード

今回のPythonコードの実装例を掲載します(Jupyter Notebook)。参考にどうぞ!



Conclusion | まとめ

最後までご覧頂きありがとうございます!
Pythonの作図ライブラリMatplotlibのCustom scaleという枠組みを使って、自作のスケールを定義する方法を紹介しました!

Matplotlibでは、xxxScale、xxxTransform、InvertedxxxTransformの3つのclassを定義することで、新たに自作のスケールを定義することができます。さらにRegistorしておくことで、通常の対数のようにax.set_yscale(“xxx”)でy軸を自作のスケールに設定することもできます!ぜひ今回の記事を参考にしてみてください!

以上「Matplotlib | 自作のスケールを定義する方法: Custom scale(Python軸スケール5)」でした!
またお会いしましょう!Ciao!



References | 参考

参考に本シリーズの記事と外部リンクをまとめます。

本シリーズの記事

外部リンク

モバイルバージョンを終了