【Python】ゼロや負の値があるグラフの軸を選ぶ

PythonのTopに戻る

結論:正の量にはlog、符号を残すならsymlogも候補

通常の対数軸は0と負の値をそのまま表示できない。正負にまたがる残差や差分の大きさを複数の桁にわたって見たいときは、まず線形軸を基準にし、必要に応じてsymlogを使う。ゼロ付近だけ線形、そこから外側は対数的に広がる軸である。どちらを選ぶかは図の見栄えよりも、値の符号や0が測定上どんな意味を持つかで決めたい。

対数にできない点を消すために全データへ定数を足すと、比の意味が変わる。例えば1と10の比は10だが、両方に1を足すと2と11になる。ゼロを小さな正数へ置き換える方法も、検出限界以下という意味が明確に定義されていない限り避けたい。軸の制約による非表示と、測定値そのものの変更を混同しないことが大切である。

そのまま動かせる例

負の100から正の100まで、0を含む同じ9点を3種類の軸で描く。右図のlinthresh=1は−1から1を線形領域にする指定であり、薄い帯でその範囲を示した。中央のlogではnonpositive="mask"を明示して非正値を表示から外す。値が失われたことを隠さないよう、タイトルにも条件を入れている。

コードはNumPyとMatplotlibを使い、空の作業フォルダーで実行するとscales.pngを保存する。横軸は単なる添字である。実データでは時刻や波長などの単位を付けよう。縦軸を共有すると異なる変換を比較できなくなるため、この例では各パネルが独立した縦軸を持つ。保存先に同名画像がある場合は上書きされる。

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(9)
y = np.array([-100., -10., -1., -0.1, 0., 0.1, 1., 10., 100.])
fig, axs = plt.subplots(1, 3, figsize=(10.2, 3.7), layout="constrained")
try:
    for ax in axs:
        ax.plot(x, y, "o-", color="#215f91")
        ax.set(xlabel="Index", xticks=[0, 2, 4, 6, 8])
        ax.grid(alpha=0.25)
    axs[0].set(title="Linear", ylabel="Signed signal (a.u.)")
    axs[1].set_yscale("log", nonpositive="mask")
    axs[1].set(title="Log: nonpositive masked", ylim=(0.05, 200))
    axs[2].set_yscale("symlog", linthresh=1, linscale=1)
    axs[2].axhspan(-1, 1, color="#f8deb5", alpha=0.6)
    axs[2].set(title="Symlog: linear within +/-1")
    transformed = axs[2].yaxis.get_transform().transform(y)
    assert np.isfinite(transformed).all()
    assert transformed[4] == 0
    assert np.all(np.diff(transformed) > 0)
    print("positive:", int((y > 0).sum()))
    print("nonpositive:", int((y <= 0).sum()))
    print("symlog keeps finite coordinates:", bool(np.isfinite(transformed).all()))
    fig.savefig("scales.png", dpi=160)
finally:
    plt.close(fig)
print("saved: scales.png")

実行結果

positive: 4
nonpositive: 5
symlog keeps finite coordinates: True
saved: scales.png
同じ符号付きデータの線形軸、非正値を除いた対数軸、symlog軸の比較
中央は正の4点のみを表示。右の薄い帯は−1から1の線形領域。

3枚の図で何が違うか

線形軸では−100と100が広い範囲を占め、−1から1の差は中央へ集まる。対数軸では正の4点だけが残る。0以下の5点は消えているが、配列から削除されたわけではない。右のsymlogでは全9点に有限の表示座標が与えられ、大小関係も保たれる。これを図だけでなく座標変換の結果でも確認している。

linthreshは元データと同じ単位を持つしきい値であり、データの切り捨て境界ではない。linscaleはその線形領域が図の中で占める幅を調節する。したがって同じデータでも設定を変えるとゼロ近傍の見え方が変わる。複数条件を比較する場合は同じ設定を使い、図注へ値を明記する方が比較しやすい。

軸のラベルは元の量のまま

set_yscaleで軸を変えても、配列yは元の単位の値を保持している。目盛りも元の値として表示されるので、縦軸ラベルにlog(signal)と書く必要はない。自分でnp.log10(y)を計算して線形軸へ描いた場合とは、入力する値も目盛りの読み方も異なる。両方を重ねて行えば二重変換になるので注意しよう。比較図を後で再利用するときは、元データを加工したのか、Axesの表示変換だけを変更したのかを記録すると誤解が少ない。

ゼロ近傍と非正値の扱いに注意

しきい値は測定分解能、意味のある残差の大きさ、図の目的に合わせて決める。しきい値を変えたことで傾向の解釈が変わるなら、線形軸の拡大図も併記するとよい。symlogは線形部分と対数部分の境界で勾配が滑らかにつながらない変換なので、折れ曲がりがデータ由来とは限らない。図から曲線の微分や物理的な変曲点を直接読み取らないようにする。

logの非正値処理にはmaskとclipがある。maskは描かない扱い、clipは非常に小さい正の表示側へ押し込む扱いであり、どちらも元の非正値を正しい対数として表現しているわけではない。用途によっては点が途切れたり軸端へ線が延びたりする。選んだ挙動を明示し、正値だけの図なら除いた件数も確認しておきたい。

エラーバーの中央が正でも下端が0以下なら同じ問題が起こる。対数変換した値の誤差と、元の単位で与えられた誤差幅も同じではない。まず区間の意味を確かめ、必要なら別の軸や図を選ぼう。また、symlogは表示のための変換であり、統計モデルの誤差分布を自動的に正したり、符号の異なる値の比を定義したりする機能ではない。

確認環境と参考資料

例はLinux・CPython 3.12.14・NumPy 2.3.5・Matplotlib 3.10.8(Agg)で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。

関連項目:上下幅が違うエラーバーを描く

PythonのTopに戻る