pandasのデータフレームを結合する時に元データが左右どちらのデータソースにあったか見分ける方法

どこで見かけたか忘れてしまった(TwitterかQiitaかその辺りのはず)のですが、
pandasのデータフレームのマージをする時に便利な引数を知ったので紹介します。

DataFrame同士を列の値で結合する時、pd.mergeを使います。

how=”inner”で利用する場合は何も問題ないのですが、
left/right/outerで使う場合、結果の中に、ちゃんと左右のデータフレームにレコードが存在してうまく結合できた行と、
一方にしか存在せず、結合はしなかった行が混在します。

left_on/right_on を使って結合した場合はそこの欠損を見ればまだ見分けられるのですが、
同名列をonで結合すると見分けがつかず、少し不便です。

このような時、 indicator=True を指定しておくと、 結果に _merge という列が追加され、
各レコードが左右のデータフレームのどちらに起因しているか出力してくれます。

やってみたのがこちらです。


import pandas as pd
df_0 = pd.DataFrame(
            {
                "id": range(5),
                "key": [1, 5, 12, 7, 8],
                "value0": ["a", "b", "c", "d", "e"],
            }
        )
df_1 = pd.DataFrame(
            {
                "key": range(10),
                "value1": range(0, 100, 10),
            }
        )

df_merge = pd.merge(
        df_0,
        df_1,
        on='key',
        how="outer",
        indicator=True,
    )
print(df_merge)
"""
     id  key value0  value1      _merge
0   0.0    1      a    10.0        both
1   1.0    5      b    50.0        both
2   2.0   12      c     NaN   left_only
3   3.0    7      d    70.0        both
4   4.0    8      e    80.0        both
5   NaN    0    NaN     0.0  right_only
6   NaN    2    NaN    20.0  right_only
7   NaN    3    NaN    30.0  right_only
8   NaN    4    NaN    40.0  right_only
9   NaN    6    NaN    60.0  right_only
10  NaN    9    NaN    90.0  right_only
"""

both / left_only / right_only
で、 key の由来が確認できます。

ブログ記事数300記事達成

このブログを開設してからもうすぐ1年になります。
そして、日々せっせと書いてきた記事数がいよいよこの記事で300記事になりました。

もうしばらくしたら年間の振り返り記事も書くので時期的に微妙なのですが、
記事数のキリが良いので100記事の時の記事も見つつ、ちょっと振り返りをやってみます。
参考: 記事数が100を超えていました

日々の訪問者数がかなり伸びてきた

100記事の頃は日々20〜30人のかたがきてくださることを喜んでいたのですが、
今では平日は1日300人以上の訪問があるようになりました。(休日はもう少し減ります。)

以前はライブラリやモジュールのインストール記事や、エラーメッセージを貼り付けたような記事でアクセスの多くを稼いでいたのですが、
今では、時系列データ分析関係の記事や、pandasの使い方、scikit-learnやkerasの記事などにもある程度の量のアクセスが集まるようになりました。
(と言っても、graphvizの記事が一番人気であることは変わりませんが。)
Qiitaからの流入が出始めたのも最近のことです。

自分用のリファレンスとして便利になってきた

流石に300も記事を書くと、内容を全部覚えているわけでは無いのでこのブログで調べ物をする機会も度々発生するようになりました。
元々は、一度は気になって調べて自分が使ってるどれかの端末化サーバのどこかにメモが残ってるものが多いのですが、それを探すのはかなり手間です。
そのため、このブログに書いたような気がする内容はまずここで検索するようになりました。
当たり前ですが記事中のコードは自分が好きなスタイルで書いてるのでコピペでも使いやすく快適です。
(訪問者の方にとって使いやすいか、という点でまだ課題がある気がします。)

記事を書くためにネタを探すことも増えてきた

流石に最近は、明日のブログ記事を何にしようかと悩むことも増えてきました。
ただ、このブログを書く中で、各ライブラリの公式ドキュメントなどを読む習慣もついているので、
そのような時は主要なライブラリのドキュントを読み漁ってネタを探しています。
それはそれで新しい発見もあり、勉強になるので元々が必要に迫られた情報ばかりだった頃とは違った意味で勉強になると感じています。
ただ、ネタ探しに時間がかかる分、肝心の記事が内容が浅くなりがちでそれは反省しないといけません。

今後の方針について

残り少ないですが今年いっぱいは今のペースで続ける予定です。
ただ、来年の方針は考え直そうと思ってます。

1日1記事のペースでかけるネタが枯渇している一方で、時間かけてしっかり書きたい内容は色々あります。
具体的なペースとか、来年の記事数の目標とかはブログ以外の目標等も含めてしっかり練って、
年末年始の間にでも決めたいと思ってます。

時系列データ分析の話題で言えば、ベクトル自己回帰や状態空間モデルの話をまだかけていないですし。
ディープラーニングの話題でも、普段よく使っているLSTMなどの話をかけていません。
自然言語処理もまだまだで、早めに書きたいと思ってたword2vecの話題が未登場です。

1日1記事のペースを維持しようと、自分がしっかり時間かけてしっかり書きたい記事がかけずに
さっと出せる小ネタを探す傾向が最近強いので、きちんと振り返ってもっと有益な形でブログ更新を続けたいです。

Kerasのモデルやレイヤーの識別子に付く番号をリセットする

Kerasでモデルを構築する時、モデルのオブジェクトやレイヤーにname引数で名前をつけないと、
区別できるように自動的に識別子(名前)をつけてくれています。

適当な例ですが、summary()で表示すると確認できる、
次のコードのsequential_13/lstm_12/dense_14 のようなやつです。


print(model.summary()) 
"""
Model: "sequential_13"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm_12 (LSTM)               (None, 10)                640       
_________________________________________________________________
dense_14 (Dense)             (None, 3)                 33        
=================================================================
Total params: 673
Trainable params: 673
Non-trainable params: 0
_________________________________________________________________
None
"""

jupyter notebookなどで試行錯誤しているとどんどん数字が大きくなっていくのですが、
ちょっと見た目が良く無いので、数値をリセットしたくなることがあります。
(そもそもnameで名前つけてあげればいいのですが。)

そのような場合、 バックエンド関数である、 clear_session() をつかうと、識別子をリセットできます。
ドキュメント: バックエンド

由来をよく知らないのですが、 backend は K という別名でインポートする慣習があるようです。


from tensorflow.keras import backend as K
K.clear_session()

# もう一度モデル構築
model = Sequential()
model.add(LSTM(10, input_shape=(40,5), activation="tanh"))
model.add(Dense(3, activation="softmax"))
print(model.summary()) 
"""
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm (LSTM)                  (None, 10)                640       
_________________________________________________________________
dense (Dense)                (None, 3)                 33        
=================================================================
Total params: 673
Trainable params: 673
Non-trainable params: 0
_________________________________________________________________
None
"""

matplotlibで2本の線で挟まれた領域を塗りつぶす

単に何かの領域を塗りつぶしたり、時系列データの予測モデルの信頼区間の可視化などで使われたり、
関数のグラフとx軸の間を塗りつぶしたりするあいつです。

matplotlibでは、fill_between というメソッドが用意されており、これを使って実現できます。
ドキュメント: matplotlib.axes.Axes.fill_between

通常の plot は xとyの値をリストか何かで渡しますが、fill_betweenでは、y1とy2という風にyの値を2ペア渡します。
(なお、y2を省略すると、y1とx軸の間を塗りつぶしてくれます。)

また、 y1 と y2 の間を全て塗りつぶすのではなく、 where で、塗りつぶす領域を指定することもできます。
where に渡すのは x と同じ長さの True or False のリストです。
TrueとTrueの間が塗りつぶされます。
False, True, False のような孤立したTrueの分は塗りつぶされないので注意が必要です。

この他、 interpolate という引数が用意されています。
これは where が使われていて、かつ二つの曲線が閉じている場合に、はみ出さないように綺麗に塗ってくれるオプションです。
とりあえずTrue指定しておいて良いと思います。
この後サンプルコードを紹介しますが、最後の一つのグラフはあえて interpolate を指定せずに少しガタついてるグラフにしました。


import numpy as np
import matplotlib.pyplot as plt

# データ作成
x = np.linspace(0, 2*np.pi, 101)
y1 = np.sin(x)
y2 = np.sin(2*x)

fig = plt.figure(figsize=(12, 8), facecolor="w")
ax = fig.add_subplot(2, 2, 1, title="2線の間を全て塗りつぶす")
ax.plot(x, y1, label="sin(x)")
ax.plot(x, y2, label="sin(2x)")
ax.fill_between(
    x,
    y1,
    y2,
    alpha=0.3,
    interpolate=True,
)
ax.legend()

ax = fig.add_subplot(2, 2, 2, title="Whereで塗りつぶす領域を絞り込む")
ax.plot(x, y1, label="sin(x)")
ax.plot(x, y2, label="sin(2x)")
ax.fill_between(
    x,
    y1,
    y2,
    where=(y1 >= y2),
    alpha=0.3,
    interpolate=True,
    label="sin(x)>=sin(2x)"
)
ax.fill_between(
    x,
    y1,
    y2,
    where=(y1 < y2),
    alpha=0.3,
    interpolate=True,
    label="sin(x)<sin(2x)"
)
ax.legend()

ax = fig.add_subplot(2, 2, 3, title="y2を省略するとx軸との間を塗りつぶす")
ax.plot(x, y1, label="sin(x)")
ax.fill_between(
    x,
    y1,
    alpha=0.3,
    interpolate=True,
)
ax.legend()

ax = fig.add_subplot(2, 2, 4, title="interpolate=Trueを指定しないと隙間が発生しうる")
ax.plot(x, y1, label="sin(x)")
ax.plot(x, y2, label="sin(2x)")
ax.fill_between(
    x[::10],
    y1[::10],
    y2[::10],
    alpha=0.3,
)
ax.legend()

plt.show()

結果。

matplotlibのxkcdスタイルのパラメーターを変えてみる

実用性は皆無なのですが、他にやっている人を見かけなかったのでやってみました。
前回の記事で紹介した matplotlibのxkcdスタイルの続きです。
ドキュメントを読めば明らかなのですが、 plt.xkcd()には3種類の引数を渡すことができます。

ドキュメント: matplotlib.pyplot.xkcd

3つの引数と意味はそのまま引用します。

scale : float, optional
The amplitude of the wiggle perpendicular to the source line.

length : float, optional
The length of the wiggle along the line.

randomness : float, optional
The scale factor by which the length is shrunken or expanded.

初期値は (scale=1, length=100, randomness=2) です。

色々試したところ、 scaleと randomness は 増やすと徐々にグラフが崩れていき、
length は減らすと崩れていくようです。

初期値と、それぞれ値を変更した3パターンをグラフ出力してみました。
(randomness はこれだけ変えても変化がわかりにくかったので、scaleも変更しています。)


import matplotlib.pyplot as plt
import numpy as np


# グラフを描く処理は共通化
def graph_plot(ax):
    X0 = np.linspace(0, 2*np.pi, 200)
    Y_sin = np.sin(X0)+2
    Y_cos = np.cos(X0)+2
    X1 = np.arange(7)
    Y1 = (X1 ** 2)/36
    ax.plot(X0, Y_sin, label="$y=\\sin(x)$")
    ax.plot(X0, Y_cos, label="$y=\\cos(x)$")
    ax.bar(X1, Y1, alpha=0.3, color="g")
    ax.legend()


fig = plt.figure(figsize=(12, 10), facecolor="w")
# 間隔調整
fig.subplots_adjust(hspace=0.3, wspace=0.3)
# xkcd オプションの影響を局所化するため with で使う。
with plt.xkcd(scale=1, length=100, randomness=2):
    ax = fig.add_subplot(2, 2, 1, title="default")
    graph_plot(ax)

with plt.xkcd(scale=2, length=100, randomness=2):
    ax = fig.add_subplot(2, 2, 2, title="scale=2")
    graph_plot(ax)

with plt.xkcd(scale=1, length=50, randomness=2):
    ax = fig.add_subplot(2, 2, 3, title="length = 50")
    graph_plot(ax)

with plt.xkcd(scale=2, length=100, randomness=6):
    ax = fig.add_subplot(2, 2, 4, title="scale=2, randomness=6")
    graph_plot(ax)
plt.show()

出力されるのがこちらです。

結構雰囲気変わりますね。
とはいえ、あまりやりすぎるとくどくなるので、初期設定だけで困ることもなさそうです。
(そもそもこのスタイルが必要になる場面も基本的に無いのですが。)

全くの余談ですが、matplotlibのドキュメントページのULRにxkcdをつけるとドキュメントのスタイルが変わります。
(よくみると内容も変わってっています。)

お暇な時に見比べてみてください。
https://matplotlib.org/
https://matplotlib.org/xkcd/

matplotlibでxkcd風にグラフを描く

xkcdってなんだ?って方はこちらをどうぞ。
https://xkcd.com/
wikipedia: xkcd

誰が何の目的で実装されたのか不明ですが、matplotlibにはグラフをxkcdのコミック風に出力する機能があります。
面白いので僕はこういう機能は結構好きです。

ドキュメント: matplotlib.pyplot.xkcd

使い方は簡単で、グラグを書く前、要はplotやbarなどの関数を使う前に、plt.xkcd()を差し込むだけ。
ただ、この手軽さに落とし穴がありました。一回呼び出すと戻せなくなるのです。
(調査にかなり手こずったので、この記事もどちらかというとxkcdの使い方より元への戻し方を伝えたい。)

ドキュメントにも、pcParamsを上書きしてしまうと書いてあります。

Notes
This function works by a number of rcParams, so it will probably override others you have set before.
f you want the effects of this function to be temporary, it can be used as a context manager, for example:

context manager ってのは要は with句の事のようです。(あとでちゃんと調べたい。)

要するに、 with plt.xkcd(): で有効な範囲をあらかじめ絞りましょう
参考に、以下のコードでは二つのグラフをxkcd風に書いた後に、普通のグラフを2つ作成しました。


import matplotlib.pyplot as plt
import numpy as np

data = [5, 4, 3, 2, 1]
label = [f"item {i}" for i in range(5)]
X = np.linspace(0, 2*np.pi, 200)
Y_sin = np.sin(X)
Y_cos = np.cos(X)

fig = plt.figure(figsize=(12, 9), facecolor="w")

# xkcd オプションの影響を局所化するため with で使う。
with plt.xkcd():
    ax = fig.add_subplot(2, 2, 1, title="xkcd pie chart")
    ax.pie(
        data,
        labels=label,
        autopct='%3.1f%%',  # 割合をグラフ中に明記
        counterclock=False,  # 時計回りに変更
        startangle=90,  # 開始点の位置を変更
    )
    ax = fig.add_subplot(2, 2, 2, title="xkcd plot")
    ax.plot(X, Y_sin, label="$y=\\sin(x)$")
    ax.plot(X, Y_cos, label="$y=\\cos(x)$")
    ax.legend()

ax = fig.add_subplot(2, 2, 3, title="normal pie chart")
ax.pie(
    data,
    labels=label,
    autopct='%3.1f%%',  # 割合をグラフ中に明記
    counterclock=False,  # 時計回りに変更
    startangle=90,  # 開始点の位置を変更
)

ax = fig.add_subplot(2, 2, 4, title="normal plot")
ax.plot(X, Y_sin, label="$y=\\sin(x)$")
ax.plot(X, Y_cos, label="$y=\\cos(x)$")
ax.legend()

plt.show()

出力されたグラフがこちら。

場面を選べば、プレゼンや資料などで使いやすそうなグラフですね。
あと、注意点としては、対応したフォントがないので日本語文字が使えません。

もし、 with を使わずに plt.xkcd() してしまったら、それ以降のグラフは全部、
xkcdモードで出力されてしまいます。
jupyter notebookであれば戻すためにカーネルの再起動が必要になるので気をつけましょう。

matplotlibで円グラフ

自分の分析のために円グラフを描きたい場面というのがあまりなく、誰かのためのダッシュボードや、稀に自分でも必要になるときはTableauで作成するので、
Pythonで円グラフを作成することはあまりないのですが、最近機会があったので方法メモしておきます。

円グラフは英語で pie chart というので、
matplotlibでも pie という名前の関数で作成できます。
ドキュメント: matplotlib.pyplot.pie

最小構成で作成するなら、データとラベルを渡してあげれば、それだけで描いてくれます。
データも割合ではなく数量で渡しても、勝手に合計100%になるように描いてくれるので楽です。
ただ、初期設定だと(個人的に)少し見慣れないデザインになるのでいくつかオプション設定します。
比較用にほぼ何も設定しないバージョンとそれぞれ出力したのが次のコードです。


import matplotlib.pyplot as plt

data = [5, 4, 3, 2, 1]
label = ["項目1", "項目2", "項目3", "項目4", "項目5"]

fig = plt.figure(figsize=(12, 7), facecolor="w")
ax = fig.add_subplot(1, 2, 1, title="円グラフ (初期設定)")
ax.pie(
    data,
    labels=label,
)

ax = fig.add_subplot(1, 2, 2, title="円グラフ (見た目修正)")
ax.pie(
    data,
    labels=label,
    autopct='%3.1f%%',  # 割合をグラフ中に明記
    counterclock=False,  # 時計回りに変更
    startangle=90,  # 開始点の位置を変更
)
plt.show()

結果がこちら。

どこまで工数を使うかにもよりますが、色とかももう少し工夫した方が使いやすそうですね。
(ただ、あまり凝ったことをするなら別のBIツールに任す方がオススメです。)

plot_roc_curveを試す

今回もscikit-learn 0.22.0 の新機能を試します。
今回はROC曲線を書いてくれる、plot_roc_curveです。
ドキュメント: sklearn.metrics.plot_roc_curve

元々、 roc_curve という機能はあったのですが、これを可視化するのは少しだけ面倒だったので結構期待していました。

ただ、試してみたところ、plot_roc_curveは2値分類のモデルにしか対応していないようです。
roc_curveは引数のpos_labelに対象のラベルを指定してあげれば多値分類にも対応しているので、今後に期待します。

ということで、2値分類のダミーデータを作成して、試してみます。
綺麗に分類できる問題だとROC曲線の価値がよくわからないので線形分離不可能な問題にしています。
最後にグラフを二つ出力していますが、左のが、生成したテストデータとモデルの決定境界、右側が、ROC曲線です。


from sklearn.metrics import plot_roc_curve
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import numpy as np
import matplotlib.pyplot as plt

# 綺麗に線形分類することのできないダミーデータの生成。
X0 = np.clip(2 * np.random.randn(500, 2) + np.array([1, 1]), -7, 7)
X1 = np.clip(2 * np.random.randn(500, 2) + np.array([-1, -1]), -7, 7)
X = np.concatenate([X0, X1])
y = np.array([0]*500 + [1]*500)

# 学習データとテストデータに分ける
X_train, X_test, y_train, y_test = train_test_split(
                                    X,
                                    y,
                                    test_size=0.2,
                                    stratify=y,
                                )
# モデルの作成と学習
clf = LogisticRegression()
clf.fit(X_train, y_train)

# ここから可視化
fig = plt.figure(figsize=(12, 6), facecolor="w")
ax = fig.add_subplot(1, 2, 1, aspect='equal', xlim=(-7, 7), ylim=(-7, 7))
ax.set_title("テストデータと決定境界")

# データを散布図で表示する
ax.scatter(X_test[y_test == 0, 0], X_test[y_test == 0, 1], alpha=0.7)
ax.scatter(X_test[y_test == 1, 0], X_test[y_test == 1, 1], alpha=0.7)

# 決定境界を可視化する
X_mesh, Y_mesh = np.meshgrid(np.linspace(-7, 7, 401), np.linspace(-7, 7, 401))
Z_mesh = clf.predict(np.array([X_mesh.ravel(), Y_mesh.ravel()]).T)
Z_mesh = Z_mesh.reshape(X_mesh.shape)
ax.contourf(X_mesh, Y_mesh, Z_mesh, alpha=0.1)

ax = fig.add_subplot(1, 2, 2, aspect='equal', xlim=(0, 1), ylim=(0, 1))
ax.set_title("ROC曲線",)
plot_roc_curve(clf, X_test, y_test, ax=ax)

plt.show()

出力された図がこちら。

コード全体が長いのでわかりにくくて恐縮ですが、 ROC曲線自体は1行で出力できており、非常に手軽です。
また、AUCも同時に出力してくれています。

scikit-learnにスタッキングのクラスが追加されたので使ってみる

前回に引き続き、scikit-learn 0.22.0 の新機能の紹介です。

複数の機械学習モデルを組み合わせて使う方法の一つに、スタッキング(Stacking)があります。
簡単に言えば、いつくかの機械学習のモデルの予測結果を特徴量として、別のモデルが最終的な予測を行うものです。
アイデアは単純で、効果もあるらしいのですが、これをscikit-learnでやろうとすると、少し面倒だったので僕はあまり使ってきませんでした。

それが、 StackingClassifierStackingRegressor という 二つの機能がscikit-learnに実装され、
手軽にできそうな期待が出てきたので、試してみました。
とりあえず、 StackingClassifier (分類)の方をやってみます。

サンプルのコードを参考しながら、 iris を digitsに変えてやってみました。
ランダムフォレストと、線形SVMの出力を特徴量にして、ロジスティック回帰で予測しています。


from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.ensemble import StackingClassifier

# データの準備
X, y = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        stratify=y,
        random_state=42,
    )


# 1段目として、二つのモデルを構築。
# 初期値の max_iter では収束しなかったので大きめの値を設定
estimators = [
        ('rf', RandomForestClassifier(n_estimators=10, random_state=42)),
        ('svr', make_pipeline(
                StandardScaler(),
                LinearSVC(max_iter=4000, random_state=42)
                )
            )
        ]
clf = StackingClassifier(
    estimators=estimators,
    final_estimator=LogisticRegression(max_iter=600),
)
clf.fit(X_train, y_train)
print("正解率:", clf.score(X_test, y_test))
# 正解率: 0.96

そこそこの正解率が出ましたね。
ちなみに、同じパラメータのランダムフォレストと線形SVM、の単体の正解率は次のようになります。


make_pipeline(
            StandardScaler(),
            LinearSVC(max_iter=4000, random_state=42)
        ).fit(X_train, y_train).score(X_test, y_test)

# 0.9511111111111111

RandomForestClassifier(
    n_estimators=10,
    random_state=42
).fit(X_train, y_train).score(X_test, y_test)

# 0.9333333333333333

確かにスタッキングしたモデルの方が正解率が高くなっていました。

scikit-learnのversion 0.22.0 がリリースされたので、混同行列の可視化機能を試す

先日、scikit-learnの新しいバージョンがリリースされていたことに気づきました。
Version 0.22.0 December 3 2019

色々、機能が追加されていたり、改善が施されていたりしますが、何かパッと試せるものを試してみようと眺めてみたのですが、
新機能の中に metrics.plot_confusion_matrix というのが目についたのでこれをやってみることにしました。

元々、 confusion_matrix を計算する関数はあるのですが、
出力がそっけないarray で、自分でlabelを設定したりしていたのでこれは便利そうです。

まず、元々存在する confusion_matrix で混同行列を出力してみます。


from sklearn.svm import SVC
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix

# データの読み込み
iris = load_iris()
X = iris.data
y = iris.target
class_names = iris.target_names

X_train, X_test, y_train, y_test = train_test_split(
                                        X,
                                        y,
                                        test_size=0.2,
                                        random_state=0,
                                        stratify=y,
                                    )

# モデルの作成と学習
classifier = SVC(kernel='linear', C=0.01).fit(X_train, y_train)
y_pred = classifier.predict(X_test)
print(confusion_matrix(y_test, y_pred))

"""
[[10  0  0]
 [ 0 10  0]
 [ 0  3  7]]
"""

最後の行列が出力された混同行列です。各行が正解のラベル、各列が予測したラベルに対応し、
例えば一番下の行の中央の3は、正解ラベルが2なのに、1と予測してしまったデータが3件あることを意味します。
(とても便利なのですが、行と列のどちらがどっちだったのかすぐ忘れるのが嫌でした。)

さて、次に sklearn.metrics.plot_confusion_matrix を使ってみます。
どうやら、confusion_matrixのように、正解ラベルと予測ラベルを渡すのではなく、
モデルと、データと、正解ラベルを引数に渡すようです。
こちらにサンプルコードもあるので、参考にしながらやってみます。
normalizeに4種類の設定を渡せるのでそれぞれ試しました。

データとモデルは上のコードのものをそのまま使います。


import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import plot_confusion_matrix

# 表示桁数の設定
np.set_printoptions(precision=2)

# 可視化時のタイトルと、正規化の指定
titles_options = [
        ("Confusion matrix, without normalization", None),
        ("Normalized confusion matrix: true", 'true'),
        ("Normalized confusion matrix: pred", 'pred'),
        ("Normalized confusion matrix: all", 'all'),
    ]

fig = plt.figure(figsize=(10, 10), facecolor="w")
fig.subplots_adjust(hspace=0.2, wspace=0.4)
i = 0
for title, normalize in titles_options:
    i += 1
    ax = fig.add_subplot(2, 2, i)
    disp = plot_confusion_matrix(
                        classifier,
                        X_test,
                        y_test,
                        display_labels=class_names,
                        cmap=plt.cm.Blues,
                        normalize=normalize,
                        ax=ax,
                    )

    # 画像にタイトルを表示する。
    disp.ax_.set_title(title)

    print(title)
    print(disp.confusion_matrix)
plt.show()

"""
Confusion matrix, without normalization
[[10  0  0]
 [ 0 10  0]
 [ 0  3  7]]
Normalized confusion matrix: true
[[1.  0.  0. ]
 [0.  1.  0. ]
 [0.  0.3 0.7]]
Normalized confusion matrix: pred
[[1.   0.   0.  ]
 [0.   0.77 0.  ]
 [0.   0.23 1.  ]]
Normalized confusion matrix: all
[[0.33 0.   0.  ]
 [0.   0.33 0.  ]
 [0.   0.1  0.23]]
"""

最後に表示された画像がこちら。

今回例なので4つ並べましたが、一つだけ表示する方が カラーバーの割合がいい感じにフィットします。
軸に True label、 Predicated label の表記を自動的につけてくれるのありがたいです。