TL;DR

  • twinx() を使えば、1つのグラフに単位の異なる2つのデータ(例:売上と成長率)を重ねて表示できる
  • 左右のY軸で色分けし、凡例を1つに統合することで見やすさが大きく変わる
  • 軸ラベルの位置やスケールのズレに注意すれば、実務のレポートやダッシュボードにそのまま使える

概要

データ分析や業務レポートを作っていると、「単位が違う2つの指標を同じグラフで比較したい」という場面によく出会います。たとえば、月ごとの売上(円)と成長率(%)、気温(℃)と降水量(mm)、サーバーのリクエスト数とレスポンスタイム(ms)など、スケールの異なるデータを1枚の図で見せたいケースです。

このとき無理に1つのY軸にまとめてしまうと、どちらかのデータがほぼ直線になって読めなくなってしまいます。そこで使われるのが「2軸グラフ(デュアルY軸グラフ)」です。matplotlibでは twinx() というメソッドを使うことで、X軸を共有しつつ左右に異なるY軸を持つグラフを簡単に作成できます。

「2軸グラフは難しそう」というイメージを持たれがちですが、実際にはAxesオブジェクトの仕組みを理解すれば非常にシンプルです。本記事では、インストールから基本的な実装、実務で使える応用例、そしてよくある落とし穴までを一通り解説します。

インストール

matplotlibはpipで簡単にインストールできます。すでにNumPyやpandasを利用している環境であれば、追加の依存関係はほとんど発生しません。

pip install matplotlib

pandasのデータフレームを使って実践的なサンプルを作る場合は、合わせてインストールしておくと便利です。

pip install matplotlib pandas numpy

バージョンを確認したい場合は以下のコードで確認できます。

import matplotlib
print(matplotlib.__version__)

本記事のサンプルコードはmatplotlib 3.x系で動作確認しています。

基本サンプル:twinx()で2軸グラフを作る

まずは最もシンプルな例から見ていきましょう。X軸を共有し、左側に売上(万円)、右側に成長率(%)を表示するグラフです。

import matplotlib.pyplot as plt

months = ["1月", "2月", "3月", "4月", "5月", "6月"]
sales = [120, 135, 150, 142, 160, 175]      # 売上(万円)
growth_rate = [5.2, 12.5, 11.1, -5.3, 12.7, 9.4]  # 成長率(%)

fig, ax1 = plt.subplots(figsize=(8, 5))

# 左側の軸(売上)
ax1.bar(months, sales, color="#4C72B0", alpha=0.8, label="売上(万円)")
ax1.set_xlabel("月")
ax1.set_ylabel("売上(万円)", color="#4C72B0")
ax1.tick_params(axis="y", labelcolor="#4C72B0")

# 右側の軸(成長率)を作成
ax2 = ax1.twinx()
ax2.plot(months, growth_rate, color="#DD8452", marker="o", linewidth=2, label="成長率(%)")
ax2.set_ylabel("成長率(%)", color="#DD8452")
ax2.tick_params(axis="y", labelcolor="#DD8452")

plt.title("月別売上と成長率")
fig.tight_layout()
plt.show()

ポイントは以下の3つです。

  1. ax1.twinx() でX軸を共有した新しいAxesオブジェクト ax2 を作る
  2. ax1 と ax2 にそれぞれ別の種類のグラフ(棒グラフ・線グラフなど)を描画できる
  3. tick_params で軸ラベルの色をデータの色と合わせることで、どの軸がどのデータに対応するか一目でわかる

実務で使える応用例:凡例を統合した2軸グラフ

実際の業務では、売上の棒グラフとKPI(達成率など)の線グラフを重ねて、かつ凡例を1つにまとめたいケースが多くあります。以下はpandasのDataFrameを使い、凡例統合まで含めた実践的なサンプルです。

import matplotlib.pyplot as plt
import pandas as pd

data = pd.DataFrame({
    "month": ["1月", "2月", "3月", "4月", "5月", "6月"],
    "revenue": [320, 345, 410, 390, 455, 480],       # 売上(万円)
    "achievement_rate": [88, 92, 101, 97, 105, 110],  # 目標達成率(%)
})

fig, ax1 = plt.subplots(figsize=(9, 5.5))

bars = ax1.bar(
    data["month"], data["revenue"],
    color="#55A868", alpha=0.75, label="売上(万円)"
)
ax1.set_ylabel("売上(万円)", fontsize=11)
ax1.set_xlabel("月", fontsize=11)

ax2 = ax1.twinx()
line, = ax2.plot(
    data["month"], data["achievement_rate"],
    color="#C44E52", marker="o", linewidth=2.5, label="目標達成率(%)"
)
ax2.set_ylabel("目標達成率(%)", fontsize=11)
ax2.axhline(100, color="gray", linestyle="--", linewidth=1, alpha=0.6)

# 凡例を1つに統合する
handles = [bars, line]
labels = [h.get_label() for h in handles]
ax1.legend(handles, labels, loc="upper left")

ax1.set_title("月別売上と目標達成率", fontsize=13, fontweight="bold")
fig.tight_layout()
plt.savefig("sales_achievement.png", dpi=150)
plt.show()

このように bars と line のオブジェクトを明示的に保持しておき、ax1.legend() にまとめて渡すことで、2つの軸にまたがるデータでも1つの凡例ボックスで表示できます。実務のレポート作成では、この「凡例の統合」が意外と見落とされがちなポイントです。

注意点

2軸グラフは便利ですが、使い方を誤ると読者を誤解させるリスクがあるため、以下の点に注意してください。

1. スケールの取り方次第で印象が大きく変わる 左右のY軸のスケール(最小値・最大値)を調整すると、相関関係がないデータでも「相関があるように見える」グラフを作れてしまいます。意図的でない限り、set_ylim() で不自然にスケールを操作しないようにしましょう。

ax1.set_ylim(0, max(data["revenue"]) * 1.2)
ax2.set_ylim(0, max(data["achievement_rate"]) * 1.2)

2. 軸と凡例の対応が分かりにくくなりやすい 色を揃える、軸ラベルに単位を明記する、凡例をまとめるなど、どのデータがどちらの軸に対応するかを必ず明示してください。特にグレースケール印刷される可能性のある資料では、色だけでなくマーカーの形状(marker="o", marker="s" など)でも区別すると安全です。

3. グリッド線の重複に注意 ax1.grid(True) と ax2.grid(True) を両方オンにすると、グリッド線が交差して見づらくなります。グリッドは片方の軸(通常は左側)だけに設定するのが基本です。

ax1.grid(axis="y", linestyle="--", alpha=0.4)
ax2.grid(False)

4. 3軸以上にはしない twinx() をさらに重ねて3軸目を作ることも技術的には可能ですが、可読性が著しく下がります。3つ目の指標を見せたい場合は、サブプロット(plt.subplots(2, 1) など)に分けるほうが読者に親切です。

5. 日本語フォントの文字化け対策 日本語ラベルを使う場合、環境によって文字が豆腸(□□□)になることがあります。IPAexGothicやNoto Sans JPなどの日本語対応フォントを指定しておきましょう。

plt.rcParams["font.family"] = "IPAexGothic"

FAQ

Q1. twinx()とtwiny()の違いは何ですか?

twinx() はX軸を共有して新しいY軸(右側)を追加するメソッドです。一方 twiny() はY軸を共有して新しいX軸(上側)を追加します。実務で圧倒的によく使われるのは twinx() で、「同じ時系列データに対して単位の異なる2つの指標を比較する」用途がほとんどです。twiny() は、例えば同じY軸に対して「摂氏」と「華氏」のように異なるX軸スケールを並べたい場合などに使います。

Q2. 2軸グラフでY軸のスケールを揃えて見やすくするコツはありますか?

完全に揃える必要はありませんが、「0を基準にする」「両方の軸で同じ比率の余白を持たせる」ことを意識すると誤解を招きにくくなります。具体的には、両軸とも set_ylim(0, 最大値 * 1.1〜1.2) のように最小値を0に固定し、最大値に少し余白を持たせる方法がよく使われます。また、matplotlibには軸を完全同期させる公式のオプションはないため、必要であれば手動で set_ylim() を調整するのが現実的な対応です。

Q3. 2軸グラフを使うべきでないケースはありますか?

はい、あります。2つのデータに意味的な関連性がない、あるいは単純に「同じグラフにまとめたいから」という理由だけで2軸グラフを使うのは避けるべきです。2軸グラフはスケール操作によって誤った印象(相関があるように見える、変化が大きく/小さく見える)を与えやすいため、データジャーナリズムや統計の文脈では批判的に見られることもあります。代わりに、サブプロットを縦に並べて個別のY軸で表示する方法(plt.subplots(2, 1, sharex=True))の方が誠実な可視化になる場合が多いです。2軸グラフは「同じ現象の異なる側面を見せたい」「読者が明確に単位の違いを理解できる」場合に限定して使うのが安全です。

まとめ

matplotlibの2軸グラフは、twinx() を軸に、色分け・凡例統合・スケール調整の3点を押さえれば決して複雑なものではありません。むしろ、業務レポートやダッシュボードで「売上と成長率」「KPIと達成率」のような組み合わせを表現する際には必須のテクニックといえます。一方で、スケール操作による誤解を避けるための注意点もセットで理解しておくことが、信頼される可視化を作る近道です。本記事のサンプルコードをベースに、自分のデータに合わせてカスタマイズしてみてください。