EP.17 で、推論による属性の付与を扱いました。そこで「危ういのは精度が低いからではなく、精度が分からないから」と書きました。
この回は、その精度をどう測るかを扱います。特に、全体では当たっているのに特定の群だけ外れるという、見つけにくい問題に焦点を当てます。
1. 平均は少数派を隠す
精度を1つの数字で表すと、件数の多い群の成績がそのまま全体の成績になります。
import numpy as np
rng = np.random.default_rng(42)
# 群ごとの件数と、その群での的中率GROUPS = { "A(多数派)": {"n": 9000, "acc": 0.95}, "B": {"n": 800, "acc": 0.88}, "C(少数派)": {"n": 200, "acc": 0.52},}
total_correct = 0total_n = 0print(f"{'群':14s}{'件数':>7}{'的中率':>9}")for name, g in GROUPS.items(): correct = int(g["n"] * g["acc"]) total_correct += correct total_n += g["n"] print(f"{name:14s}{g['n']:7d}{g['acc']:9.1%}")
print(f"\n全体の的中率: {total_correct / total_n:.1%}")print("→ 全体は良好に見えるが、C群は当てずっぽうと大差ない")実行すると、全体では 93% 前後になります。良好な数字に見えます。しかし C群では 52% ── 2択なら当てずっぽうと大差ありません。
そして、この C群に対して推定値を使うと、約半分が誤った値になります。全体の精度だけを見て「使える」と判断すると、この事実を見逃します。
学習に使えるデータが少ないので、当然といえば当然です。ただし件数が少ないぶん、全体の精度への影響も小さい。つまり外れていても数字に現れないという、二重に見つけにくい構造になっています。
2. 群ごとに測る
対処は単純で、群ごとに分けて測ることです。 と呼ばれる考え方ですが、やることは集計を分けるだけです。
import numpy as npimport pandas as pd
def accuracy_by_group(df, group_col, true_col, pred_col, min_n=30): """群ごとの的中率を出す。件数が少ない群は別に示す。""" rows = [] for name, g in df.groupby(group_col): n = len(g) acc = (g[true_col] == g[pred_col]).mean() rows.append({ "群": name, "件数": n, "的中率": acc, "判定": "件数不足" if n < min_n else "", })
out = pd.DataFrame(rows).sort_values("件数", ascending=False)
# 全体との差を出す overall = (df[true_col] == df[pred_col]).mean() out["全体との差"] = out["的中率"] - overall return out, overall
# 使い方# result, overall = accuracy_by_group(df, "region", "actual", "predicted")# print(f"全体: {overall:.1%}")# print(result.to_string(index=False))件数が少ない群を明示するのが要点です。件数が30件しかない群の的中率は、それ自体が大きく揺れます(統計入門 EP.07 参照)。低く出ても、たまたまかもしれません。
「件数不足で判断できない」という結果も、立派な結果です。「精度が低い」と「精度が分からない」は違います。後者なら、まず件数を増やすか、その群には適用しないという判断になります。
3. どの軸で分けるか
分ける軸の選び方には、優先順位があります。
| 軸 | 優先度 | 理由 |
|---|---|---|
| 件数に偏りがある軸 | 最高 | 少数派で精度が落ちやすい |
| 扱いに配慮が要る属性 | 最高 | 誤りの影響が大きい |
| 時期 | 高い | 古いデータで学習していないか |
| 地域・拠点 | 中 | データの取り方が違うことがある |
| 入力の経路 | 中 | 経路ごとに品質が違う |
3行目は見落とされがちですが重要です。古いデータで学習した仕組みは、新しいデータで精度が落ちます。時期で分けて測ると、いつから劣化しているかが見えます。
2行目については、EP.17 でも触れたとおり扱いそのものに配慮が要ります。推定の対象にしてよいか、という手前の判断があります。測る以前の問題として、その属性を推定する必要が本当にあるかを確認してください。 の観点では、推定してまで持つ必要のない属性は、持たないのが最も安全です。
4. 外れ方の中身を見る
的中率だけでなく、どう外れているかも見ます。同じ精度でも、外れ方によって影響が違います。
| 外れ方 | 何が起きるか |
|---|---|
| 特定の値に寄る | 多数派の値に全部倒れている(実質推定していない) |
| ばらばらに外れる | 情報が足りていない |
| 一方向に偏る | 系統的な誤り。原因がある |
| 極端な値で外れる | 外れ値の扱いに問題 |
1行目が実務で最も多い形です。推定した結果が、全部同じ値になっている。多数派の値を返せば、全体の精度はそれなりに出ます。しかし推定として何もしていません。
import pandas as pd
def compare_distribution(df, true_col, pred_col): """実際の分布と、推定結果の分布を比べる。""" actual = df[true_col].value_counts(normalize=True).sort_index() pred = df[pred_col].value_counts(normalize=True).sort_index()
out = pd.DataFrame({"実際": actual, "推定": pred}).fillna(0) out["差"] = out["推定"] - out["実際"] return out.sort_values("差", key=abs, ascending=False)
# 推定が特定の値に寄っていれば、その値の「差」が大きく正になる# result = compare_distribution(df, "actual", "predicted")# print(result.to_string())推定結果の分布が、実際の分布と大きく違うなら、それ自体が警告です。特に、多数派の値だけが増えているなら、1行目の状態を疑ってください。
5. 偏りが見つかったら
対処には段階があります。必ずしも精度を上げる必要はありません。
- 1その群には適用しない — 最も安全。欠損のまま残す
- 2その群のデータを増やす — 根本的だが時間がかかる
- 3群ごとに別の方法を使う — 少数派には単純な規則で
- 4推定値に信頼度を付ける — 使う側が判断できるように
- 5推定そのものをやめる — 目的に対して手段が合っていない
1番目を最初に挙げたのは、これが見落とされやすいからです。「精度が出ないなら改善しなければ」と考えがちですが、適用しないという選択のほうが正しい場面は多くあります。
EP.17 で書いたとおり、推定値は元のカラムに混ぜず、推定であることを保つ。そうしておけば、特定の群だけ推定しないという運用が自然にできます。
欠損があると気持ち悪いので埋めたくなりますが、誤った値で埋めるより、欠損のままのほうが正確です。下流で「この群はデータがない」と扱えるなら、それが最も誠実な状態です。
6. 継続して測る
一度測って終わりにしないでください。データは変わり、精度は落ちます。
- 定期的に群ごとの精度を出す — 月次など
- 新しく増えた群を検出する — 想定していなかった値が来る
- 時期での劣化を見る — 古い学習のまま使っていないか
- 閾値を決めて通知する — 一定を下回ったら知らせる
2番目が実務で効きます。カテゴリの値が増えることがあります。新しい区分が追加された、新しい地域に展開した。学習時に存在しなかった群は、当然ながら精度が出ません。
この継続的な確認は、LLM時代のテスト戦略 EP.05 で扱ったリグレッションと同じ位置づけです。変わっていないはずのものが変わっていないかを、定期的に確認する。
全体の精度は多数派の成績であり、少数派の失敗を隠す。しかも件数が少ない群ほど外れやすく、かつ数字に現れにくい。対処は群ごとに測ること。件数不足で判断できないことも結果として扱う。分ける軸は件数の偏り・配慮が要る属性・時期を優先。偏りが見つかったら、その群には適用しないのが最も安全です。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。