Claude Media
退職面談の離職理由を分析するClaude手順 — コードブックで分類し件数表へ

退職面談の離職理由を分析するClaude手順 — コードブックで分類し件数表へ

退職面談(エグジットインタビュー)のメモから離職理由を分類し、部署別の件数表にする手順です。氏名のローカル置換、コードブック、構造化出力、人手検証までをPythonで通します。

退職面談の記録が数十件たまると、「結局なぜ辞めているのか」を数字で語りたくなります。ところが面談メモは自由記述で、同じ不満でも書き方がばらばらです。読み込むたびに印象が変わり、人事の会議では声の大きい一件に引きずられがちです。

この記事は、メモの氏名や部署をローカルで置換し、事前に決めたコードブック(分類基準)に沿ってClaudeに分類させ、離職理由の件数表を作るまでの手順です。出力はJSON Schemaで縛る構造化出力を使い、最後に人の目で一致を確かめます。

退職面談は件数が少なく、中身が重いデータ

退職面談の記録は、アンケートの自由記述と性質が違います。年間の退職者が数十人なら、件数は数十件です。一方で1件あたりの情報量は多く、本人の氏名、上司の名前、家庭の事情、健康状態まで書かれていることがあります。

この違いが、手順の組み方を二つ決めます。

  • 件数が少ないので、割安なバッチ処理に寄せる必要はありません。1件ずつ同期のAPIで投げ、その場で結果を見られる形が扱いやすいです。数千件の自由記述をバッチで回す場合は自由記述の分類をClaude APIのバッチで回す手順が向いています
  • 1件の重みが大きいので、送る前の置換と、返ってきた結果の人手確認を省きません

360度評価のコメントを匿名のまま要約する手順はClaude Codeでの360度評価の要約にあります。こちらは「誰が書いたか」を隠す話で、本記事は「なぜ辞めたか」を数える話です。入力は似ていても、出力が文章ではなく件数表になる点が違います。

面談メモから氏名と部署をローカルで落とす

分類に氏名は要りません。送る前に、退職者本人、上司、同僚、部署、プロジェクトの名前を手元でトークンに置き換えます。置換表は手元に残し、Claudeには渡しません。

メモは1件1行のCSV(id、dept、memo)に揃え、名簿から作った置換表で順に置き換えます。

import csv
 
pairs = [(r["term"], r["token"])
         for r in csv.DictReader(open("roster.csv", encoding="utf-8"))]
pairs.sort(key=lambda p: len(p[0]), reverse=True)  # 長い語から先に
 
masked = []
for row in csv.DictReader(open("memos.csv", encoding="utf-8")):
    text = row["memo"]
    for term, token in pairs:
        text = text.replace(term, token)
    masked.append({"id": row["id"], "dept": row["dept"], "memo": text})

roster.csv は「田中,[本人]」「佐藤部長,[上司]」「営業企画部,[部署X]」のような2列です。長い語から先に置換するのは、「佐藤」を先に消すと「佐藤部長」の後半が残るためです。

dept 列は置換しません。部署別に集計したいので、集計用のラベルは元の値のまま手元に残し、Claudeに送るのは memo だけにします。

置換しても残る手がかり

置換は固有名詞しか消せません。「入社3年目で、先月の新製品発表を担当した」のような記述は、読む人が読めば本人に行き着きます。ここは人の目で確かめるしかありません。送信前に数件を声に出して読み、特定につながる表現が残っていないかを見る作業を、手順に組み込んでおきます。

健康や家庭の事情が書かれたメモは、扱いがさらに慎重になります。病歴などの要配慮個人情報を入力してよいかは要配慮個人情報をClaudeに入力してよいかで扱っています。分類の目的は「健康・家庭に関する理由だった」と数えることなので、病名や診断の詳細はメモの段階で伏せる運用にすると、送る情報が減ります。

離職理由のコードブックを先に決める

分類の枠は、Claudeに投げる前に文書として固めます。カテゴリが途中で変わると、集計した件数が比べられなくなります。

退職面談では、理由を「主たる理由」と「背景にあった理由」に分けると使いやすくなります。面談で最初に語られるのは「給与」でも、話を聞くと上司との関係が根にある、という流れはよくあるためです。次のような形を一例にします。

codebook_v1.md
# 離職理由コードブック v1
 
主たる理由を1つ、背景にあった理由を0個以上選ぶ。
 
- compensation(報酬): 給与・賞与・評価と報酬の連動
- manager(上司): 直属の上司の指示・評価・関わり方
- peers(人間関係): 同僚・他部署との関係、職場の雰囲気
- career(キャリア): 成長の実感、昇進、スキルの活かし先
- workload(働き方): 残業、休日、勤務地、リモート可否
- job_content(業務内容): 担当業務の内容や配属のミスマッチ
- company_outlook(会社の将来): 事業や経営への不安
- personal(個人事情): 家庭・健康・転居など会社側と無関係な事情
- other(その他): 上記に当てはまらない、または読み取れない
 
## 判断ルール
- 本人が「決め手」と述べた理由を主たる理由にする
- 「決め手」の記述がなければ、最初に語られた理由を主とする
- 事情を推測で補わない。メモにない理由は選ばない

ポイントは三つあります。

  1. other を1つだけ置く。逃げ場がないと、近いカテゴリに無理に押し込まれます
  2. 判断ルールを文章にする。主たる理由を何で決めるのかを書いておかないと、人手との突き合わせで揉めます
  3. 版を付ける。報告に載せる件数が、どの基準で数えたものかを後から言えるようにするためです

迷う境界は「含める例」「含めない例」を足すと安定します。

構造化出力で分類結果を縛る

自由な文章で答えさせると、集計の前に表記ゆれを直す作業が出ます。output_config.format にJSON Schemaを渡すと、返ってくるのはスキーマに沿ったJSONです。ドキュメントでは、制約付きデコーディングによって、JSONの構文エラーや必須項目の欠落を避けられると説明されています。

スキーマは次のとおりです。

CATEGORIES = [
    "compensation", "manager", "peers", "career", "workload",
    "job_content", "company_outlook", "personal", "other",
]
 
SCHEMA = {
    "type": "object",
    "properties": {
        "primary_reason": {"type": "string", "enum": CATEGORIES},
        "contributing_reasons": {
            "type": "array",
            "items": {"type": "string", "enum": CATEGORIES},
        },
        "evidence": {"type": "string"},
        "certainty": {"type": "string", "enum": ["high", "mid", "low"]},
    },
    "required": [
        "primary_reason", "contributing_reasons", "evidence", "certainty",
    ],
    "additionalProperties": False,
}

設計の理由を、ドキュメントの制約と合わせて書きます。

設計

スキーマ設計の判断

  • 確信度は数値でなく3段階にする

    minimum や maximum などの数値制約は使えず、指定すると400エラーになります。0〜1の確信度を返させても範囲を縛れないため、high / mid / low の列挙にしています。

  • 項目はすべて required にする

    required に入れない項目は「省略可能な引数」として数えられ、1リクエスト内の合計に24個の上限があります。今回の4項目は少なく、全部を必須にして構造を単純に保ちます。

  • 理由は短い抜き出しを求める

    思考過程や段階的な推論を項目にすると、reasoning_extraction の拒否を招くことがあります。evidence には、判断の根拠になったメモ中の語句を短く抜き出させます。

カテゴリ名は compensation のような英数字にしています。理由は二つあります。ドキュメントによれば、enum の大文字小文字は保証されず、スキーマと大文字小文字だけ違う値が返ることがあります。大文字小文字だけで区別する値を作らず、比較は大文字小文字を無視して行います。もう一つは、スキーマ自体が最大24時間キャッシュされるという記述です。氏名や社内の固有名詞を enum の値に入れる設計は避けます。

使えないスキーマ機能

enum に使えるのは文字列・数値・真偽値・nullだけで、複雑な型は入れられません。配列の minItems は0か1だけが使えます。再帰的なスキーマと外部への $ref も使えません。設計中に400エラーが出たら、まずこの一覧を疑います。SDKが変換する仕組みを含めた制限の全体はStructured outputsのJSON Schema制限にまとめています。

1件ずつ分類して結果を集める

件数が少ないので、同期のMessages APIで1件ずつ投げます。コードブックは system に置き、メモは区切りタグで囲んで最後に渡します。

import json
import anthropic
 
client = anthropic.Anthropic()
codebook = open("codebook_v1.md", encoding="utf-8").read()
valid = set(CATEGORIES)
results, failed = [], []
 
for row in masked:
    resp = client.messages.create(
        model="claude-sonnet-5-5",   # 手元で使えるモデル名に合わせる
        max_tokens=1024,
        system=codebook,
        messages=[{
            "role": "user",
            "content": f"<memo>{row['memo']}</memo>",
        }],
        output_config={
            "format": {"type": "json_schema", "schema": SCHEMA}
        },
    )
    if resp.stop_reason != "end_turn":
        failed.append((row["id"], resp.stop_reason))
        continue
    data = json.loads(resp.content[0].text)
    data["primary_reason"] = data["primary_reason"].lower()
    data["contributing_reasons"] = [
        c.lower() for c in data["contributing_reasons"]
    ]
    if data["primary_reason"] not in valid:
        failed.append((row["id"], "invalid_enum"))
        continue
    results.append({"id": row["id"], "dept": row["dept"], **data})

stop_reason を見る理由は、ドキュメントが「スキーマに合わない出力」の二つの場面を挙げているからです。

  • refusal: 安全上の理由で拒否されたとき。ステータスは200で、課金もされ、出力はスキーマに合わない場合があります。退職理由には辛辣な言葉が含まれやすいため、拒否が混ざる可能性を見ておきます
  • max_tokens: 出力が途中で切れたとき。JSONが不完全になります。max_tokens を増やして再試行します

failed に入った件は、無理に分類せず人が読みます。

初回はコードブックの v1 を固定したまま、10件前後で試し、evidence が妥当かを目で確かめます。最初から全件を回すと、基準の不備に気づくのが遅れます。

人手の確認で一致を測ってから件数表にする

分類結果をそのまま報告に載せるのは避けます。人事担当が全件の2〜3割を抜き出して自分で分類し、Claudeの primary_reason と突き合わせます。割合は件数に応じて決めてかまいません。数十件なら、半分近く見ても作業は重くありません。

食い違いは、次の3つのどれかです。

食い違いの型直す先
コードブックの定義が曖昧直す先境界の例を足して v2 にし、全件を再分類する
メモの情報が足りない直す先other や low を許容し、件数表に注記する
分類結果の誤り直す先該当件だけ人が直し、直した事実を記録する

v2 に上げたときは、v1 の結果と混ぜません。同じ基準で数えた件数だけを一つの表に載せます。

部署別の件数表を作る

検証を終えた結果から、主たる理由の件数と、背景として挙がった頻度を集計します。

import pandas as pd
 
df = pd.DataFrame(results)
main = pd.crosstab(df["dept"], df["primary_reason"])
print(main)
 
ctx = df.explode("contributing_reasons")
ctx = ctx[ctx["contributing_reasons"].notna()]
print(ctx["contributing_reasons"].value_counts())

出力の形は次のようになります。数値は架空の例です。

部署報酬上司キャリア働き方合計
営業報酬3上司4キャリア2働き方1合計10
開発報酬1上司2キャリア5働き方2合計10
管理報酬0上司1キャリア1働き方0合計2

読むときに押さえるのは、母数が小さい点です。管理部門の2件のように、1セルが数人の退職者を表す表は、割合に直すと印象が大きく振れます。そもそも件数が少ない部署は、割合を出さず件数のままにするか、複数の部署をまとめます。

人数が少ない集計は、個人が特定されるリスクも高めます。少人数のセルを報告に載せるか、他と合算するかは、組織の運用で決めておく必要があります。

報告に書く言い方

件数表は「退職者が語った理由の分布」です。「会社の問題の大きさ」を直接示すものではありません。報告には、次の点を添えます。

  • 何件の面談メモを、どのコードブックの版で分類したか
  • 人手で確認した件数と、一致した件数
  • 面談で語られなかった理由は含まれないこと

よくあるつまずき

主たる理由が other に偏る。コードブックの定義が狭いか、メモが短すぎます。other に入った件を10件ほど読み、共通するテーマがあれば新しいカテゴリを足して v2 にします。

同じメモを2回流すと結果が変わる。件数が少ないと、揺れが表に出やすくなります。境界事例の件は、再実行の結果が割れるかを確かめ、割れる件は人が決めます。結果は最終版を保存し、集計は保存したファイルから行います。

400エラーでスキーマが拒否される。数値や文字列長の制約を入れていないか、additionalProperties が false か、省略可能な項目が多すぎないかを確認します。

Citationsと併用したい。構造化出力は引用機能と同時に使えず、併用すると400エラーになります。根拠はスキーマの evidence 項目で持たせます。

会社のデータを送ってよいか迷う。送信してよい範囲は、社内の規程と契約で決まります。個人情報を入力する前に確かめる一次資料はClaudeに個人情報を入力する前に確認する一次資料にまとめています。

まとめ

退職面談の離職理由分析は、モデルの賢さより、送る前の置換、固定したコードブック、人手での照合という周辺の手順で品質が決まります。構造化出力は、その真ん中で「集計できる形」を保証する部品です。件数表は退職者が語った理由の分布であり、原因の特定ではないと割り切ると、会議での使い方が安定します。

退職者の手続き側はCoworkでの退職者オフボーディングに分けてあります。

この記事を共有:XはてブLinkedIn