メカとら / tidyverseで学ぶ人事データ分析 記事一覧 運営者について お問い合わせ

tidyverseで学ぶ人事データ分析

残業時間、勤続年数、退職、エンゲージメントスコア。人事の現場で扱うデータをそのまま題材にして、R(tidyverse/tidymodels)でのデータ加工・可視化・モデリングを解説しています。全24章、サンプルデータ付きです。

書いているのは現役の人事担当者です。Rの教科書としての完成度では専門家の書いた定番書に及びませんが、「人事データ特有の扱いにくさ」に踏み込んだ日本語の解説は他にあまりありません。等級のような順序のある因子、入退社日から勤続年数を出す処理、母数の小さい部署の扱い、個人が特定されうる集計の危うさ。そのあたりを実務の感覚で書いています。

Rそのものが初めてなら、先に読むべき本があります

文法や環境構築から体系的に学びたい場合は、宋財泫・矢内勇生『私たちのR: ベストプラクティスの探求』(無料公開)か、Hadley Wickhamらの『R for Data Science』(邦訳『Rではじめるデータサイエンス』)のほうが確実です。網羅性でも正確さでも、そちらが上です。

このサイトが役に立つのは、Rの基本操作はひととおり分かっていて、手元の人事データで何をどうすればいいかの当たりをつけたいときだと思ってください。

サンプルデータ

解説で使う架空の人事データです。従業員1,200名分、6列。実在の企業のデータではなく、乱数から生成した疑似データです。手元にコピーして同じコードを試せます。

列名内容備考
部署factor営業・開発・製造・人事・経理人数に偏りあり(人事は100名)
等級ordered factorG1〜G5順序つき。並べ替えに forcats を使う
勤続年数double0.3〜32年右に裾を引く分布
月間残業時間double0〜92時間部署差が大きい
エンゲージメントdouble10〜100のスコア残業時間と負の相関を持たせてある
退職character在籍/退職全体の約15%が退職

hr_sample.csv をダウンロード(UTF-8 BOM付き、Excelでも文字化けしません)

library(tidyverse)

hr <- read_csv("hr_sample.csv") |>
  mutate(
    部署 = factor(部署, levels = c("営業","開発","製造","人事","経理")),
    等級 = factor(等級, levels = c("G1","G2","G3","G4","G5"), ordered = TRUE)
  )

人事データで最初にやること

以下は、このサンプルデータに対して実際に手を動かした例です。コードと、それを実行して得られる図を並べています。人事データを扱ううえでつまずきやすい点は、図の下に書きました。

1. 部署ごとの残業時間を比べる

平均値だけを並べた棒グラフにしないこと。人事データは分布の形にこそ情報があります。

部署別の月間残業時間を箱ひげ図で比較したグラフ。開発と営業が高く、製造・経理が低い
hr |>
  ggplot(aes(部署, 月間残業時間, fill = 部署)) +
  geom_boxplot(alpha = 0.75) +
  labs(
    title = "部署別 月間残業時間の分布",
    x = NULL, y = "月間残業時間(時間)"
  ) +
  theme_minimal(base_family = "Noto Sans CJK JP") +
  theme(legend.position = "none")
開発と営業の中央値が高いのはひと目で分かりますが、実務で効いてくるのはひげの長さです。開発は上に70時間近くまで伸びていて、平均は40時間でも一部に長時間労働者が張りついている。36協定の特別条項に関わるのはこの層で、平均値だけ見ていると見落とします。

2. 残業とエンゲージメントの関係を見る

2変数の関係を確認する場面。散布図に回帰直線を重ねるのが基本形です。

月間残業時間とエンゲージメントスコアの散布図。右下がりの回帰直線が引かれている
hr |>
  ggplot(aes(月間残業時間, エンゲージメント)) +
  geom_point(alpha = 0.28, size = 1.4) +
  geom_smooth(method = "lm") +
  labs(
    title = "月間残業時間とエンゲージメントスコアの関係",
    x = "月間残業時間(時間)",
    y = "エンゲージメントスコア"
  ) +
  theme_minimal(base_family = "Noto Sans CJK JP")
右下がりの関係が出ますが、これは「残業を減らせばエンゲージメントが上がる」という意味ではありません。エンゲージメントが低い人が転職活動を始めて残業を減らしている、という逆向きの因果もありえます。人事の施策検討でこの図を出すと因果として読まれがちなので、「相関がある」以上のことは言わない、と最初に断っておくのが安全です。geom_point()alpha を下げているのは、1,200点が重なって濃さが分からなくなるのを避けるためです。

3. 勤続年数ごとの退職割合を出す

実数ではなく構成比で見たいとき。position = "fill" を使います。

勤続年数区分ごとの退職割合を100%積み上げ棒グラフで示したもの。3〜5年が最も高い
hr |>
  mutate(勤続区分 = cut(
    勤続年数,
    breaks = c(0, 1, 3, 5, 10, 20, 35),
    labels = c("1年未満", "1〜3年", "3〜5年",
               "5〜10年", "10〜20年", "20年以上")
  )) |>
  ggplot(aes(勤続区分, fill = 退職)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "勤続年数区分ごとの退職割合",
       x = "勤続年数", y = "構成比")
構成比のグラフは母数が見えなくなるのが最大の欠点です。「20年以上」の退職率が低く見えても、その区分に何人いるのかはこの図から読めません。20人しかいない区分の5%と、400人いる区分の5%はまったく重みが違います。実際に報告資料に載せるときは、geom_text() で各バーに人数を添えるか、実数の棒グラフを併記してください。

4. 部署 × 等級の人員構成を見る

クロス集計をそのまま図にする場合。geom_tile() でヒートマップにします。

部署と等級のクロス集計をヒートマップで表示し、各セルに人数を記載したもの
hr |>
  count(部署, 等級, name = "人数") |>
  ggplot(aes(等級, 部署, fill = 人数)) +
  geom_tile(color = "white", linewidth = 0.8) +
  geom_text(aes(label = 人数), size = 3.2) +
  scale_fill_gradient(low = "#EFF6FF", high = "#60A5FA") +
  labs(title = "部署 × 等級の人員構成", y = NULL)
人事データでヒートマップを作るときはセルの人数が一桁になる箇所に注意してください。「人事部のG5が2名」まで出してしまうと、社内の誰が見ても個人が特定できます。公開資料や部門長への共有資料では、少人数セルを伏せるか、等級を粗くまとめる処理を挟むのが実務上のマナーです。色だけのヒートマップにせず geom_text() で数値を出しているのは、色の濃淡は人間が正確に読み取れないためです。

5. 部署ごとに分けて分布を確認する

1枚に重ねると潰れる場合、ファセットで分割します。

部署別に月間残業時間のヒストグラムを5面に分割して表示したもの
hr |>
  ggplot(aes(月間残業時間)) +
  geom_histogram(bins = 26) +
  facet_wrap(~ 部署, ncol = 3) +
  labs(title = "部署別 月間残業時間のヒストグラム",
       x = "月間残業時間(時間)", y = "人数")
既定ではすべての面で軸が共通になります。これは意図的にそのままにしておくべき場面が多いです。scales = "free_y" を付けると各面が見やすくはなりますが、人数の少ない人事部と364名の営業部が同じ高さに見えてしまい、比較の図として成立しなくなります。見やすさより比較可能性を優先してください。

6. 時系列で推移を追う

月次の集計値を折れ線で並べる、報告資料でいちばん出番の多い形です。

2025年度の部署別平均残業時間の月次推移を折れ線グラフで示したもの
勤怠 |>
  group_by(年月, 部署) |>
  summarise(平均残業時間 = mean(残業時間), .groups = "drop") |>
  ggplot(aes(年月, 平均残業時間, color = 部署)) +
  geom_line(linewidth = 0.9) +
  geom_point(size = 1.6) +
  labs(title = "部署別 平均残業時間の推移(2025年度)",
       x = NULL, y = "平均残業時間(時間)")
人事の月次データは営業日数の違いが推移に紛れ込みます。5月や1月は連休で営業日が少なく、残業の総時間も自然に下がる。前年同月と比べるか、営業日数で割った1日あたりに直さないと、施策の効果と暦の効果が区別できません。lubridate の章で日付の扱いを詳しく書いています。

掲載している図について:上記のグラフは、ggplot2と同じGrammar of Graphicsを実装したplotnine(Python)で描画したものです。掲載しているRコードを実行して得られる図と内容は一致しますが、既定テーマの細部(フォントの太さ、余白の取り方など)は環境によって多少異なります。

目次(全24章)

上から順に読む必要はありません。手元のデータでやりたいことが決まっているなら、該当する章から入ってください。

導入

データを加工する

可視化する

統計とモデリング

応用

人事データを扱うときに気をつけていること

個人が特定できる粒度で出さない

分析そのものより神経を使うのがここです。部署 × 等級 × 性別のようにクロスを重ねると、セルの人数はあっという間に一桁になります。「該当者2名」の平均年収は、事実上その2人の年収を公開しているのと変わりません。集計結果を人に見せる前に、最小セルの人数を必ず確認する習慣をつけています。

予測モデルの結果を個人の処遇に直結させない

退職予測モデルは技術的には作れますし、このサイトでも作り方を書いています。ただ、出てきた確率を使って特定の個人に何かをする、という使い方には慎重であるべきだと考えています。モデルは過去のパターンを再現するだけで、過去に偏りがあればそれも学習します。使うとしても、個人ではなく「どの属性の層で離職が起きやすいか」を把握して職場環境の改善につなげる、という粒度にとどめるのが現実的です。

統計的に有意であることと、実務的に意味があることは別

サンプルサイズが大きくなれば、ごくわずかな差でもp値は小さくなります。「残業時間が1時間増えると退職確率が0.3ポイント上がる(p<0.001)」という結果が出たとして、それが施策を打つに値する大きさなのかは別の判断です。効果量と信頼区間を必ずセットで見るようにしています。

参考にしている資料

このサイトを書くにあたって参照している一次情報と、より体系的な日本語資料です。

最終更新:2026年8月5日|掲載しているコードは動作を確認していますが、パッケージのバージョンによって挙動が変わることがあります。サンプルデータは乱数から生成した架空のもので、実在の組織のデータではありません。誤りを見つけた場合はお問い合わせからご連絡いただけると助かります。