退職面談の離職理由を分析する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に投げる前に文書として固めます。カテゴリが途中で変わると、集計した件数が比べられなくなります。
退職面談では、理由を「主たる理由」と「背景にあった理由」に分けると使いやすくなります。面談で最初に語られるのは「給与」でも、話を聞くと上司との関係が根にある、という流れはよくあるためです。次のような形を一例にします。
# 離職理由コードブック v1
主たる理由を1つ、背景にあった理由を0個以上選ぶ。
- compensation(報酬): 給与・賞与・評価と報酬の連動
- manager(上司): 直属の上司の指示・評価・関わり方
- peers(人間関係): 同僚・他部署との関係、職場の雰囲気
- career(キャリア): 成長の実感、昇進、スキルの活かし先
- workload(働き方): 残業、休日、勤務地、リモート可否
- job_content(業務内容): 担当業務の内容や配属のミスマッチ
- company_outlook(会社の将来): 事業や経営への不安
- personal(個人事情): 家庭・健康・転居など会社側と無関係な事情
- other(その他): 上記に当てはまらない、または読み取れない
## 判断ルール
- 本人が「決め手」と述べた理由を主たる理由にする
- 「決め手」の記述がなければ、最初に語られた理由を主とする
- 事情を推測で補わない。メモにない理由は選ばないポイントは三つあります。
otherを1つだけ置く。逃げ場がないと、近いカテゴリに無理に押し込まれます- 判断ルールを文章にする。主たる理由を何で決めるのかを書いておかないと、人手との突き合わせで揉めます
- 版を付ける。報告に載せる件数が、どの基準で数えたものかを後から言えるようにするためです
迷う境界は「含める例」「含めない例」を足すと安定します。
構造化出力で分類結果を縛る
自由な文章で答えさせると、集計の前に表記ゆれを直す作業が出ます。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での退職者オフボーディングに分けてあります。