scikit-learnのconfusion_matrixは、行が正解、列が予測です。labelsを指定すると、その順番が行と列の両方へ使われます。 図のラベル名だけを入れ替えても、数値の並びは変わりません。まず一件の入力をどのセルに数えるかで、向きと対応を確認してください。
対象は、分類モデルの学習を始め、混同行列の見方や表示順に迷っている人です。今回は請求・使い方・返金という三種類の架空問い合わせを八件用意します。予測も説明のために手で作った値です。実在のモデルを測定した精度や、サービスの性能を示す数値ではありません。
公式資料・コードの確認日:2026年10月30日。confusion_matrixの公式API資料を参照しています。資料と実行確認のscikit-learnは1.9.1、Pythonは3.12.14です。入力の対応、ラベル順、正規化方法を保存してから、利用中の版でも小さい例で照合してください。
行と列、labelsの順番を八件の原表で確かめる
予測値だけでなく、正解とIDを一緒に用意する
混同行列を作る前に、一行が何を表すかを決めます。今回の一行は一件の問い合わせで、idはその識別子、actualは人が用意した正解分類、predictedは手作りの予測例です。同じIDの正解と予測を比較するので、二つの配列の長さが同じだけでは十分ではありません。
たとえば正解を受付順、予測を処理終了順で並べると、同じ位置に別の問い合わせが来る場合があります。数値としては混同行列を計算できても、違う問い合わせ同士を比較した結果になります。元の受付IDへ予測を戻し、同じ対象で比較する準備が必要です。
八件の受付は、八人の利用者という意味でもありません。同じ人が二件問い合わせれば、この教材の単位では二件です。利用者ごとに評価したいなら、受付をどのようにまとめるかを別に決める必要があります。行列へ入れる前に、一行と一件の意味を固定しておきましょう。
ここでは一つのrecordsから二つの配列を作り、この対応を明示します。billingは請求、usageは使い方、refundは返金という教材上の名称です。英語名の順序や、重要な業務の順序を、暗黙のまま同じものとして扱わないようにしてください。
from sklearn.metrics import confusion_matrix
import numpy as np
records = [
{"id": "T01", "actual": "refund", "predicted": "refund"},
{"id": "T02", "actual": "refund", "predicted": "billing"},
{"id": "T03", "actual": "billing", "predicted": "billing"},
{"id": "T04", "actual": "billing", "predicted": "usage"},
{"id": "T05", "actual": "billing", "predicted": "usage"},
{"id": "T06", "actual": "usage", "predicted": "usage"},
{"id": "T07", "actual": "usage", "predicted": "refund"},
{"id": "T08", "actual": "usage", "predicted": "usage"},
]
assert len({r["id"] for r in records}) == len(records) == 8
y_true = [r["actual"] for r in records]
y_pred = [r["predicted"] for r in records]
labels = ["billing", "usage", "refund"]
assert set(y_true) | set(y_pred) == set(labels)
実際の正解分類を作るときも、曖昧な問い合わせの扱いを決めておきます。「請求について聞きたいが、返金も希望している」などの複数意図を無理に一つへ決めた場合、分類規則の違いがモデルの間違いに見えることがあります。正解の基準と保留の条件は、行列の外に残します。
予測が別の処理から返る場合は、元の順番を維持していると思い込まず、IDで取り出し直します。次は八件の予測を逆順で受け取ったと仮定する例です。受信した並びをそのままy_predへ使わず、正解側のID順に対応する予測を取り出します。
received = list(reversed(records))
received_ids = [r["id"] for r in received]
assert len(received_ids) == len(set(received_ids))
assert set(received_ids) == {r["id"] for r in records}
pred_by_id = {r["id"]: r["predicted"] for r in received}
aligned_pred = [pred_by_id[r["id"]] for r in records]
assert aligned_pred == y_pred
辞書へ入れる前にIDの重複を確認している点も大切です。同じIDが二回返った状態をそのまま辞書へ変えると、片方が上書きされて重複を見落とす場合があります。欠けたIDや想定外のIDも、元の集合と比べて確かめます。配列の長さだけでは、重複と欠落が同時に起きた状態を見つけられません。
正解のない予測を除外する必要があるなら、何件を何の理由で外したかを評価の記録へ残します。予測できなかった項目を黙って落とした表と、すべての入力を対象にした表は同じ評価ではありません。「予測を返した件数」と「分類が一致した件数」を分けて扱いましょう。
一件をセルへ置き、行が正解・列が予測と確認する
T04は、正解がbillingで予測がusageです。したがってbillingの行とusageの列が交わるセルへ一件として数えます。T05も同じ組み合わせなので、このセルは二件になります。行列を読むときに、まずこのような一件の対応を言葉で説明できるかを確認します。
T02は逆向きの例で、正解refundから予測billingへ進みます。これはrefund行・billing列の一件です。「請求に見えた返金」と「返金に見えた請求」を同じ誤りとまとめると、どの資料や判定基準を見直すかが分かりにくくなります。セルは誤りの方向も表しています。
cm = confusion_matrix(y_true, y_pred, labels=labels)
expected = np.array([[1, 2, 0], [0, 2, 1], [1, 0, 1]])
assert np.array_equal(cm, expected)
assert cm[0, 1] == 2 # 正解billing、予測usage
assert cm[2, 0] == 1 # 正解refund、予測billing
assert cm.sum() == 8
assert np.trace(cm) == 4
print(cm)
対角成分は、正解と予測のラベルが一致した件数です。今回の四件はT01、T03、T06、T08に対応します。行列の対角だけを見て終わらず、原表からこの四つを取り出して確認すると、分類名と数値の対応も検算できます。
この段階で示している四件という数値は、固定した教材の集計結果です。実運用の精度を判断するには、対象データの選び方、正解の定義、訓練と評価の分離なども必要になります。訓練・評価を分けて前処理する手順と同様に、表が計算できたことと評価が適切なことは分けて確認します。
labelsを指定しない順序と、自分で決めた順序を分ける
公式では、labelsを省略したときは正解か予測に現れるラベルをソートした順に使うと案内しています。今回の英語名ならbilling、refund、usageの順です。最初に原表へ出てきたrefundが先頭になるわけでも、重要な項目が先頭になるわけでもありません。
今回のlabelsはbilling、usage、refundなので、既定の順とは異なります。ラベルを省略した行列と指定した行列では、同じ八件を数えていても、セルの位置が入れ替わります。この違いをデータやモデルが変わったと判断しないようにします。
default_labels = sorted(set(y_true) | set(y_pred))
assert default_labels == ["billing", "refund", "usage"]
default_cm = confusion_matrix(y_true, y_pred)
positions = [default_labels.index(label) for label in labels]
reordered = default_cm[np.ix_(positions, positions)]
assert np.array_equal(reordered, cm)
assert default_cm.sum() == cm.sum() == 8
順序を変える場合は、行と列の両方を同じラベル順へ揃えます。片方だけを入れ替えると、対角に同じ分類同士が並ばなくなり、正解の位置も読めなくなります。見た目を整える目的の変更でも、数値と軸の対応を一緒に扱ってください。
評価のたびに出現ラベルが変わる場合は、比較用のラベル一覧を先に固定する方法もあります。ただし、一覧にない予測値が出ていないかを先に確認します。知らない分類を黙って省くより、件数と対象IDを別に示す方が、評価範囲を説明しやすくなります。
たとえば出力側だけにbillingの末尾へ空白が付いた値があると、別の分類として扱われる場合があります。表示名が似ていても同じ値とは限りません。正規化するなら、どの空白や表記ゆれを同一視するかを先に定め、変換前の値も残します。評価を通すために都合よく分類を変更しないようにしてください。
図の表示名だけを並べ替えると、数値と意味がずれる
行列の数値がbilling、usage、refundの順なのに、図の目盛りだけを返金、請求、使い方へ変えると、セルの意味が食い違います。ラベルを日本語へ翻訳することと、ラベル順を入れ替えることは別です。表示用の名称は、計算に渡したlabelsから順に作ります。
たとえば先頭の値1は、今回のcmではbilling同士の一致です。ここへ「返金」という表示名を付けても、返金同士の値へ変わるわけではありません。数値がもっともらしい範囲にあると、図だけから誤りに気付きにくいため、元のラベル一覧を保存しておく意味があります。
display_map = {"billing": "請求", "usage": "使い方", "refund": "返金"}
display_labels = [display_map[label] for label in labels]
assert display_labels == ["請求", "使い方", "返金"]
table_rows = [
{"actual": display_labels[i],
"predicted_counts": dict(zip(display_labels, cm[i].tolist()))}
for i in range(len(labels))
]
assert table_rows[0]["predicted_counts"]["使い方"] == 2
色を濃くする、軸を回転する、といった図の調整より先に、行列のshapeとラベル名の数を照合します。表示した名前と、計算に使った値の一覧を同じ記録に入れておくと、図を別の資料へ貼り付けた後でも確かめられます。画像だけの保存では、順序の根拠を後から追いにくくなります。
| 正解 | 予測:請求 | 予測:使い方 | 予測:返金 | 行の合計 |
|---|---|---|---|---|
| 請求 | 1 | 2 | 0 | 3 |
| 使い方 | 0 | 2 | 1 | 3 |
| 返金 | 1 | 0 | 1 | 2 |
y_trueとy_predを逆にしても、合計や対角では気付けない
引数を逆にすると、同じlabelsを使った今回の行列は転置した形になります。行と列を交換するため、billingからusageへの二件はusage行・billing列へ移ります。一方、合計八件と対角四件は同じです。合計と正解数だけの確認では、引数の逆転を検出できません。
このため、先ほどのT04のように方向が分かる例を一つ残します。実際の評価では「正解の列」「モデルが出した列」を変数名で区別し、評価関数へ渡す順番も一緒に確認します。変数名がaとbだけでは、後からコードを読み返しても役割が分かりません。
swapped = confusion_matrix(y_pred, y_true, labels=labels)
assert np.array_equal(swapped, cm.T)
assert swapped.sum() == cm.sum() == 8
assert np.trace(swapped) == np.trace(cm) == 4
assert swapped[0, 1] == 0 and cm[0, 1] == 2
返金の見逃しを減らしたいのか、返金窓口へ誤って回す件数を減らしたいのかでも、見るセルは違います。行と列を取り違えると、改善したい誤りの方向まで逆に伝えてしまいます。「何を何と予測した件数か」を文章にしてから、図の軸を確認すると混乱を減らせます。
IDに対応する予測が処理順で返る場合にも、並びの照合を忘れないようにします。変換後のラベル番号を元へ戻す処理は、LabelEncoderの対応表と未知ラベルの確認にも関係します。行列へ渡す前の値が、どの分類名に当たるかを確認してください。
対象件数と正規化の分母を確認し、説明できる評価表にする
labelsから一種類を外すと、並び替えだけではなくなる
labelsは順序の指定に加えて、対象ラベルの選択にも使えます。今回billingとusageだけを指定すると、refundに関わる行や列が表示されなくなります。これは返金を請求や使い方へまとめた処理ではありません。行列で数える対象を絞った結果です。
除外されるのはT01、T02、T07の三件です。T02は予測だけ見ればbillingですが、正解がrefundなので二種類の行列には入りません。T07は正解usageでも、予測refundなので入りません。片側のラベルだけを見て除外件数を数えると、入力八件との対応が合わなくなります。
selected = ["billing", "usage"]
subset_cm = confusion_matrix(y_true, y_pred, labels=selected)
excluded = [r["id"] for r in records
if r["actual"] not in selected or r["predicted"] not in selected]
assert subset_cm.tolist() == [[1, 2], [0, 2]]
assert subset_cm.sum() == 5
assert excluded == ["T01", "T02", "T07"]
assert subset_cm.sum() + len(excluded) == len(records)
対象を絞ること自体が間違いなのではありません。ある二つの分類に限定して確認したい場合には目的に合うことがあります。ただし、八件のうち五件を数えた表を、元の八件すべての評価として見せないようにします。対象と除外条件を表の注記へ残してください。
とくに、予測に未知の分類が混じっている状態でラベル一覧を固定すると、評価の外へ落ちた件数に気付かないことがあります。表示した行列の合計が入力件数に一致するかを先に検算し、一致しない場合は、サンプル重みや対象ラベルの指定を確認します。
normalizeは、どこを分母にした割合かで読み分ける
正規化した行列は件数の行列と違い、セルが割合になります。公式ではnormalize=’true’は正解の行、’pred’は予測の列、’all’は全体を分母として扱います。同じセルでも、割る相手が違うため、表示される数値は一致するとは限りません。
billing同士の一致は一件です。正解billingは三件あるので行で割れば三分の一、予測billingは二件あるので列で割れば二分の一、全体八件で割れば八分の一です。数字の違いをモデルの変化と判断せず、分母にどの件数を使ったかを確かめます。
by_true = confusion_matrix(y_true, y_pred, labels=labels, normalize="true")
by_pred = confusion_matrix(y_true, y_pred, labels=labels, normalize="pred")
by_all = confusion_matrix(y_true, y_pred, labels=labels, normalize="all")
assert np.isclose(by_true[0, 0], 1 / 3)
assert np.isclose(by_pred[0, 0], 1 / 2)
assert np.isclose(by_all[0, 0], 1 / 8)
assert np.allclose(by_true.sum(axis=1), [1, 1, 1])
assert np.allclose(by_pred.sum(axis=0), [1, 1, 1])
assert np.isclose(by_all.sum(), 1)
| 設定 | この教材の分母 | 読む内容 |
|---|---|---|
| normalize=None | 割らない | 一致した請求は1件 |
| normalize="true" | 正解が請求の3件 | 本来請求である対象のうち一致した割合 |
| normalize="pred" | 予測が請求の2件 | 請求へ振り分けた対象のうち一致した割合 |
| normalize="all" | 評価全体の8件 | 全体に占める請求同士の一致 |
正規化の結果だけを保存すると、同じ二分の一でも、二件中一件なのか二百件中百件なのかが分かりません。割合の表と件数の表をセットで残すと、サンプルが少ない分類も読み取れます。丸めた表示値の和が少しずれる場合は、保存した丸め前の値とも照合してください。
出現しない分類のゼロ行を、低い性能と読み替えない
比較用のラベル一覧を固定すると、今回の評価データに現れない分類が入ることがあります。その分類の行がゼロであっても、正解を一件も評価していないなら、その分類を見抜けなかったという意味にはなりません。対象件数がゼロであることと、正解できなかったことを分けます。
この教材へfutureという分類名を追加してみます。八件の正解にも予測にもfutureは現れないため、その行と列はゼロです。行を分母に正規化した結果も数値ではゼロになりますが、ここで「futureの再現率は実用に足りない」と結論づけるだけの評価対象はありません。
with_empty = labels + ["future"]
empty_cm = confusion_matrix(y_true, y_pred, labels=with_empty)
empty_norm = confusion_matrix(y_true, y_pred, labels=with_empty, normalize="true")
assert empty_cm.shape == (4, 4)
assert empty_cm[3].sum() == empty_cm[:, 3].sum() == 0
assert np.all(empty_norm[3] == 0)
assert empty_cm.sum() == 8
サンプルがない分類を隠すのか、ゼロ件と表示するのかは、比較する目的に合わせて決めます。毎回の表のサイズを揃えたいなら、ゼロ件であることを明記して表示できます。いずれの場合も、表示上の数値だけで分類の適性や学習の成果を採点しないようにしてください。
評価用データの分割によって分類が消えた場合は、元のクラス数と分割後の件数を確かめます。stratifyのエラーとクラス数を確認する記事も参考になります。ただし、分割の設定を変えてエラーが消えたことだけで、必要な分類が十分に評価されたとは判断できません。
三クラスを二値へまとめると、正解数の意味も変わる
返金か、それ以外かだけを確かめたい場合は、billingとusageをotherへまとめる別の評価が考えられます。これはlabelsからrefundを外す操作とは違います。八件を残したまま、分類の意味を二種類へ定義し直す操作です。
今回はbillingをusageと予測した二件が、二値化した後はother同士の一致になります。三クラスでは間違いだった二件を、二値の目的では正解として扱うため、一致数が四件から六件へ変わります。数値が上がったからといって、元の三分類が改善したわけではありません。
true_binary = ["refund" if v == "refund" else "other" for v in y_true]
pred_binary = ["refund" if v == "refund" else "other" for v in y_pred]
binary_cm = confusion_matrix(true_binary, pred_binary, labels=["other", "refund"])
assert binary_cm.tolist() == [[5, 1], [1, 1]]
tn, fp, fn, tp = binary_cm.ravel()
assert (tn, fp, fn, tp) == (5, 1, 1, 1)
assert np.trace(binary_cm) == 6 and binary_cm.sum() == 8
assert np.trace(cm) == 4
四つの値へ展開する書き方は、このように二種類のラベル順を決めた行列で確認します。三クラスの行列を平らにすると九つの値になり、四種類のTN・FP・FN・TPへそのまま読み替えることはできません。二値化した目的と、正例をどちらにしたかを一緒に示してください。
説明変数のカテゴリを符号化する話と、評価時の分類をまとめる話も別です。OneHotEncoderの未知カテゴリと全ゼロの確認とは対象の段階が違います。前処理で使う分類名と評価で使う分類名を混同せず、どの目的の一覧なのかを残しましょう。
行列から原文へ戻れる記録を残し、次の確認を決める
混同行列でbillingからusageへ二件の誤りがあると分かったら、次はT04とT05の原文や正解基準を確認します。行列は、問題の所在を小さくまとめる道具です。その数値だけでは、文章が曖昧だったのか、正解の分類に揺れがあったのか、モデル側の課題なのかは分かりません。
記録には、評価データの対象期間、IDの対応方法、正解の作り方、ラベル順、正規化の設定、除外件数を残します。重み付けをした場合は、行列の合計が単純な受付件数ではなくなる点も書きます。今回の例では重みを使っていないため、合計八件を原表と直接照合できています。
モデル同士を比較するときは、行列の軸と評価対象を揃えてから数値を比べます。ラベルを外した結果や二値へまとめた結果を、元の評価と同じ表題で示さないようにしてください。交差検証の指標を確認する記事と同じく、指標の値より先に、何をどの範囲で測ったのかを説明できることが大切です。
| 評価の作り方 | 数える対象 | 一致数 |
|---|---|---|
| 元の三クラス | 全8件 | 4件 |
| labelsを請求・使い方へ限定 | 双方が対象ラベルの5件 | 3件 |
| 返金・その他へ二値化 | 全8件を二種類へ変換 | 6件 |
三つの一致数を並べて、最大の六件を「もっともよいモデル」と呼ぶことはできません。同じ固定の予測を別の目的で集計しただけだからです。分類をまとめるほど区別しなくてよい誤りが増える場合があります。どの区別が仕事で必要なのかを定義した後に、その目的に合う評価を選びます。
資料へ載せるときは「行:正解、列:予測、labels:billing・usage・refund、対象八件、重みなし、正規化なし」のように注記を添えると、読み手も表の意味をたどれます。割合の図へ差し替える場合は、その注記の正規化設定も更新します。画像の色だけを変える変更と、指標の意味を変える変更を一緒に扱わないことが、再確認しやすい資料につながります。
評価表を読む前提となるPythonやAI・機械学習を順に学びたい人は、Python Winnerの講座内容も比較できます。公式にはAI・機械学習マスターを含む目的別の講座を掲載しています。混同行列や評価設計のどこまでを扱うかは、講座名だけでは判断できません。今の理解度、学びたい評価方法、課題の質問範囲、受講総額を無料カウンセリング・受講相談で確認してください。今回の八件を説明できるなら、まず別の小さな分類例でも軸と母数を照合してみましょう。(公式情報確認日:2026年10月30日、Python Winner公式)。
よくある質問
Q. confusion_matrixの行と列はどちらが正解ですか?
scikit-learnでは行が正解、列が予測です。外部の図には別の配置もあるため、行列の軸名と、引数y_true・y_predの順番を確認してください。
Q. labelsを省略したときは、最初に出た分類から並びますか?
正解か予測に現れるラベルをソートした順に使います。今回の既定順はbilling・refund・usageです。業務上の順へ固定したい場合はlabelsを明示します。
Q. 図のラベル名だけを入れ替えてもよいですか?
順番だけを変える場合は、行列の行と列も同じ順に並べ替える必要があります。翻訳するだけなら、計算に使ったlabelsから同じ順に表示名を作ります。
Q. labelsを二つ指定すると、残りはその他へまとまりますか?
自動でその他へはまとまりません。教材ではrefundを外すと、正解か予測にrefundを含む三件が行列から外れ、合計五件になります。その他へまとめたいなら分類を変換する別の評価です。
Q. normalize=trueとpredは何が違いますか?
引数の値は文字列で指定します。trueは正解の行、predは予測の列を分母にします。同じ請求の一致一件でも、教材ではそれぞれ三分の一と二分の一になります。
Q. ゼロ行の分類は、性能が低いという意味ですか?
その分類の正解サンプルがゼロ件なら、性能を評価する対象がありません。数値だけで低い性能と結論づけず、分類ごとの実件数と、予測に出ている件数を併せて確認します。
ラベル順、正解と予測の向き、対象件数の三点を揃えてから、割合を読みます。 八件の原表へ戻って各セルを説明できれば、表示の取り違えと分類の誤りを分けて調べやすくなります。