「社員名簿」のダミーデータをシェル（とFaker）で作る

[この記事は自サイトとのクロスポストです](https://piro.sakura.ne.jp/latest/blosxom/webtech/2019-12-17_generate-dummy-csv-with-shellcommands.htm)。

[シェル芸アドベントカレンダー2019](https://qiita.com/advent-calendar/2019/shellgei)に空きがあったので、最近やったシェルの話で参加させていただきます。

自分は日経Linux誌で[シス管系女子](https://system-admin-girl.com/)というシェルの解説記事（マンガ）を連載させていただいてます。その2020年1月号掲載分の回において、「劇中の架空の会社で、[アダムズ方式](http://www.suguru.jp/seminar/shakai/koumin/adams.html)を使って各部署から代表者を何名かずつランダム且つ公平な感じで選ぶシェルスクリプトを作る」という話をやっています。

アダムズ方式では、前の計算の結果を使って次の計算を行うというステップが何度か出てきます。今までは「1画面分だけの画面出力」が必要なケースが多かったので都度てきとうにその場の気分で偽の画面をデッチ上げていたものの、こう複雑な話になってくると、それぞれの画面間で数字に矛盾が無いようデッチ上げるのはなかなか大変です。なので、実際にそれっぽい「社員名簿」のダミーデータのCSVを作って、その処理結果をそのまま使うことにしました。

こういう場面でのダミーデータの作り方はいろいろあると思いますが、描いているのがシェルの解説なので、シェルでやってみました。というのがこの記事の内容です。

## 作りたい物

用意するダミーデータの形式は「社員番号,氏名,勤務地（拠点）,部署」の列を持つCSVという事にします。具体的には以下のような感じです。

```members.csv
2,中島 悠太,仙台,法務部
4,後藤 凛,大阪,製造部
6,清水 優斗,東京,営業部
...
1600,利奈 みんと,東京,システム部
1601,野口 七海,仙台,開発部
1602,藤本 愛美,仙台,経理部
1603,青木 真央,大阪,CS部
1604,野村 奈々,東京,営業部
1605,河野 結,大阪,CS部
```

各フィールドは完全ランダムではなく、以下のような条件を満たす感じにしたいという思惑があります。

1. 社員番号は連番にしたい。
   * 退職や転職などがあったという想定で、社員番号は適度に不連続にしたい。
   * 「特定の時期に退職者が集中している」みたいな事までは考えないことにする。
   * 最後の方の数十件くらいは、入って日が浅い人達という事で退職者無しで社員番号が連続するようにしたい（主人公のみんとちゃんもそのあたりに含めたいので）。
2. 部署・拠点には偏りを持たせたい。
   * 単純に部署をランダムに割り振ると「えっシステム部が数百人規模！？」「本社と支社が同規模ってあり得るの？」みたいな事になってしまって不自然なので。
   * 「新部署創設で特定の時期に特定の部署の人が集中している」みたいな事までは考えないことにする。
3. 特定の登場人物として、何人かは決まったID・名前・勤務地・部署にしたい。

全体の行数は劇中舞台の架空の会社の全従業員数ということになります。劇中では、全国に何カ所か拠点があるかなり大きなBtoCの会社っぽい描き方をしているので、そんな感じの会社を探して組織概要を見てみたら1000人前後とありました。これを参考にしつつ、キリのいい数字よりは半端な数字の方がそれっぽいかと思って1127人と決めました[^directory]。

[^directory]: この規模ならActive Directoryとかで認証基盤を組んでユーザーの管理もそこでやるのが当たり前なのでは？というツッコミもあろうと思いますが、なにぶん連載のテーマがシェルなので、「実はそういう技術がまだ発展していないパラレル時空だった」みたいな想像で補って頂けましたら幸いです。それか、たまたまCSVでエクスポートした物があったのでそれを使ったという話でもいいです。

## ステップ1：社員番号と名前だけの一覧を作る

> * 退職や転職などがあったという想定で、社員番号は適度に不連続にしたい。
> * 最後の方の数十件くらいは、入って日が浅い人達という事で退職者無しで社員番号が連続するようにしたい（主人公のみんとちゃんもそのあたりに含めたいので）。

この条件に合うデータにしたいので、多めに作って必要な人数分をランダムに抽出することにしました。あんまり退職者が多いとブラック企業っぽくなってしまうかなと思ったのですが、程度が分からなかったので、「退職者」まで含めた人数は無根拠でてきとうに1605件と決めました[^leave]。

[^leave]: 普通どれくらいの頻度で人が入って辞めていくのか分からなかったのですが、一般的基準に照らし合わせて離職者多すぎなのであれば、分社化したとかそういう風に想像で補って貰えれば幸いです……

日本人ぽい人名をランダムに自動生成するツールというと、自分はRubyのgemで[Faker](https://rubygems.org/gems/faker)（[使い方](https://qiita.com/Keisuke69/items/edcaadc100361cf945e2)）という物があると以前に聞いた事があります。なので、人名部分だけはそれを使って簡単なスクリプトで生成してみました。

```ruby:people.rb
#coding utf-8
require 'faker'
# 日本語のデータセットを使う
Faker::Config.locale = :ja
# 引数で指定された回数実行する
ARGV[0].to_i.times do |count|
  puts "#{count+1},#{Faker::Name.name}"
end
```

これを使って、

```bash
$ sudo gem install faker
$ all=1605
$ active=1027
$ newbie=20
$ ruby people.rb $all > members-all.csv # 退職者まで含めた一覧を用意
$ cat members-all.csv | head -n -$newbie |
    shuf -n $(($active - $newbie)) |
    sort -n -k 1 -t , > members-base-oldies.csv # ベテラン勢
$ cat members-all.csv | tail -n $newbie > members-base-newbies.csv # 新人勢
$ cat members-base-oldies.csv members-base-newbies.csv > members-base.csv # 結合して「在籍者のリスト」にする
```

* `head -n -$newbie` は、入力全体の中で「先頭から N - `$newbie` 行」つまり「末尾の `$newbie` 行を除いた残りの行」を出力する。
* `shuf -n $(($active - $newbie))` は、入力をシャッフルした中から `$active - $newbie` 行だけ取り出す。
* `sort -n -k 1 -t ,` は、入力のカンマ区切りの1列目を数値と見なしてソートする。
* `tail -n $newbie` は、入力全体の中で末尾から `$newbie` 行を出力する。

という具合に、スクリプトの実行結果から「ベテラン勢」（最後の20件以外を取り出して、そこから1107件をランダムに抽出し、最初のカラムでソートした結果）と「新人勢」（最後の20件）を取り出して連結すれば、

```csv
2,中島 悠太
4,後藤 凛
6,清水 優斗
...
```

こんな要領の1127人の「現役社員」の一覧ができます。

## ステップ2：勤務地と部署を付け足す

東京・大阪・福岡・仙台に拠点があって東京本社が一番人数が多い（人数比はとりあえず5:2:1:1と想定）ということにしたいので、数に偏りを持たせた拠点一覧を用意します。

```csv:regions.csv
東京
東京
東京
東京
東京
大阪
大阪
福岡
仙台
```

件数が少ないので、自分はvimでの手書きコピペで作りました。9行しかありませんが、実際にはこれを繰り返し使うので、1127人分に拡大してもそれぞれ同じ割合になります。

同様にして、人数に偏りを持たせた部署一覧を用意します。話の都合上多めに部署を用意したいので、聞いた事のある部署名を12個ほど列挙してみました。各部署の人数は、実際の大会社が実際どんな感じなのか自分は知らないので、想像でてきとうに決めました。

|部署|割合|
|----|----|
|法務部|2%|
|システム部|2%|
|デザイン部|3%|
|人事部|6%|
|総務部|6%|
|資材部|7%|
|経理部|7%|
|広報部|10%|
|CS部|11%|
|開発部|13%|
|製造部|16%|
|営業部|17%|


件数が多くなるので、さっきの拠点一覧と同じ要領で手書きで用意するのはちょっと大変そうです。任意の回数同じ文字列を繰り返し出力するのは `yes '文字列' | head -n 件数` でできる[^repeat]ので、`while` を使って半自動生成します。

[^repeat]: 他には `echo '文字列' | shuf -r -n 件数` というやり方もあります。

```bash
$ (cat <<__EOF__
法務部 2
システム部 2
デザイン部 3
人事部 6
総務部 6
資材部 7
経理部 7
広報部 10
CS部 11
開発部 13
製造部 16
営業部 17
__EOF__
) | while read name count; do yes "$name" | head -n $count; done > divisions.csv
```

（ヒアドキュメントの記法的には

```bash
$ cat <<__EOF__ | while ...
...
__EOF__
```

とするのが「正しい」のですが、データが後から出てくる形式は自分は読みにくく感じるため、先にヒアドキュメントを書く書き方を好んでいます。）

こうして生成した元データを使って `shuf -r -n $active` とすると、指定件数より少ない元データからでも、必要な件数分の出力を得られます。

```bash
$ cat regions.csv | shuf -r -n $active > regions-shuffled.csv
$ cat divisions.csv | shuf -r -n $active > divisions-shuffled.csv
```

それを先の社員番号と名前だけのCSVに列として付け足せば、名簿っぽいダミーデータのできあがりです。これは、与えた複数ファイルそれぞれの全行を列として横方向に結合する `paste` コマンドで行います。

```bash
$ paste -d , members-base.csv regions-shuffled.csv divisions-shuffled.csv > members-with-details.csv
```

```csv:members-with-details.csv
2,中島 悠太,仙台,法務部
4,後藤 凛,大阪,製造部
6,清水 優斗,東京,営業部
7,小島 陽太,東京,経理部
10,伊藤 陸,東京,営業部
...
```

なお、`shuf` の `-r`（`--repeat`）オプションと `-n 件数`（`--head-count=件数`）オプションを併用すると、*「与えたデータの各行と同じ登場割合で指定件数の出力をランダムに得る」のではなく、「与えた元データをシャッフルして出力する、という処理を、結果が指定件数に達するまで繰り返す」という動作になります*。そのため、結果の先頭の方にばかり「東京」がやたら出てくるみたいな偏り[^random]は発生せず、拠点と部署は全体で満遍なく登場し、充分に件数が多ければ、各拠点におおむね全部署の人がいるという結果になります[^enough]。これは、出力結果を拠点ごとに絞り込んで部署名部分だけ取り出して `uniq` した結果の件数（＝その拠点に一人でも人員がいる部署の数）を見ると確かめられます。

[^random]: ランダムに決めるというのは「次の結果がどうなるか予想できない度合いが高い」という事なので、「必ず満遍なく登場する」という予想しやすい結果になる保証は無く、結果の部分部分を見ると偏った感じになる事があります。

[^enough]: 全体の件数が少ないと、特定の部署の人がいない拠点が出てきやすくなります。

```bash
$ cat members-with-details.csv | grep 東京 | cut -d , -f 4 | sort | uniq | wc -l
12
$ cat members-with-details.csv | grep 大阪 | cut -d , -f 4 | sort | uniq | wc -l
12
$ cat members-with-details.csv | grep 福岡 | cut -d , -f 4 | sort | uniq | wc -l
12
$ cat members-with-details.csv | grep 仙台 | cut -d , -f 4 | sort | uniq | wc -l
12
```

「開発の人員を集めた拠点」とか「製造の部門を集めた拠点」とかを想定したい場合、データの作り方をまた工夫する必要があります。架空の会社の内情を想像し始めるときりが無いので、今回はそこまでは行わないことにしました。

## ステップ3：特定の登場人物に差し替える

以上でダミーデータのCSVができたわけですが、劇中には既に何人か所属が明らかになっている人達がいます。推定される入社時期から社員番号をてきとうに決めて、こんな風にCSVを用意してみました。

```csv:members-specials.csv
512,谷町 列樹,東京,システム部
1024,大野 桜子,東京,システム部
1280,麻土 科学人,東京,開発部
1598,報伝 広宣,東京,広報部  ←広報に配属された同期
1599,営 優人,東京,営業部  ←みんとちゃんと間違えられた優秀な成績の同期
1600,利奈 みんと,東京,システム部
```

まず、先のダミーのCSVから社員番号がこれらと一致する行を取り除きます。`grep` は `-f` オプションでパターンファイルを指定でき、条件を反転する `-v` オプションと組み合わせると「与えた除外リストに該当しない物だけ出力する」フィルターとして機能します。特別な人物のCSVの社員番号の列だけを取り出して除外リストにすれば、「社員番号がこれらの人物に一致しない人物だけのCSV」を得られます。

```bash
$ cat members-specials.csv | cut -d , -f 1 > special-ids.csv
$ cat members-with-details.csv | grep -v -f special-ids.csv > members-isolated.csv
```

そうしたら、それと特別な人物のCSVを連結して、再び社員番号順でソートし直します。

```bash
$ cat members-isolated.csv members-specials.csv | sort -n -k 1 -t , > members.csv
```

これで、「劇中描写にそれなりに則ったダミーの社員名簿」のできあがりです。

```members.csv
2,中島 悠太,仙台,法務部
4,後藤 凛,大阪,製造部
6,清水 優斗,東京,営業部
...
1598,報伝 広宣,東京,広報部
1599,営 優人,東京,営業部
1600,利奈 みんと,東京,システム部
1601,野口 七海,仙台,開発部
1602,藤本 愛美,仙台,経理部
1603,青木 真央,大阪,CS部
1604,野村 奈々,東京,営業部
1605,河野 結,大阪,CS部
```

## シェル芸ちゃうやん問題

以上がダミーデータの作り方の逐次解説となりますが、外部のスクリプトファイルを用意していたり、各ステップで一時ファイルをたくさん作っていたりとスマートでなく、シェル芸アドカレに参戦してるくせにシェル芸ちゃうやんけというツッコミを受けそうな気がしてきたので、最後にbashのプロセス置換[^process-replacement]を使いまくってここまでの話をワンライナーにまとめた物を示してお茶を濁しておく事にします。

[^process-replacement]: `<(コマンド列)` と書くと、そのコマンド列の実行結果の出力を保存したファイルを指定したのと同じように扱われるという、bashの便利な機能。

```bash
$ all=1605; active=1027; newbie=20; \
  specials="$(cat <<__EOF__
512,谷町 列樹,東京,システム部
1024,大野 桜子,東京,システム部
1280,麻土 科学人,東京,開発部
1598,報伝 広宣,東京,広報部 
1599,営 優人,東京,営業部 
1600,利奈 みんと,東京,システム部
__EOF__
)"; \
  people="$(ruby -r faker -e "Faker::Config.locale = :ja; $all.times {|count| puts \"#{count+1},#{Faker::Name.name}\" }")"; \
  cat \
    <(paste -d , \
      <(cat \
         <(echo "$people" | head -n -$newbie |
             shuf -n $(($active - $newbie)) |
             sort -n -k 1 -t ,) \
         <(echo "$people" | tail -n $newbie)) \
      <((cat <<__EOF__
東京 5
大阪 2
福岡 1
仙台 1
__EOF__
          ) | while read name count; do yes "$name" | head -n $count; done | shuf -r -n $active) \
      <((cat <<__EOF__
法務部 2
システム部 2
デザイン部 3
人事部 6
総務部 6
資材部 7
経理部 7
広報部 10
CS部 11
開発部 13
製造部 16
営業部 17
__EOF__
          ) | while read name count; do yes "$name" | head -n $count; done | shuf -r -n $active) |
      grep -E -v "^($(echo "$specials" | cut -d , -f 1 | paste -d '|' -s))") \
    <(echo "$specials") | sort -n -k 1 -t ,
```

## まとめ

シェルのコマンドとFakerの組み合わせでダミーデータのCSVを作る例をご紹介しました。

Fakerは完全にランダムなダミーデータを作るのに便利なのですが、多少偏りのあるダミーデータを用意しようと思うと工夫が必要です。Pure Rubyでももちろんこの記事に書いたような事はできますが、自由度が非常に高くて色々なやり方が考えられるので、「どこまでこだわるか」の見切りをつけるのが難しくて自分は却って混乱する感じがありました（実際、収拾が付かなくなって途中で放り投げてしまいました）。その点、シェルだとメジャーなコマンド群で簡単にできる事には限りがあり「簡単にできるレベルにとどめよう」という圧が自然とかかるので、入れ込みすぎないで済むというメリットがあるんじゃないかなという気がします。

この記事では各段階を迷い無く進めていますが、実際にやった時には、社員番号をランダムに抽出する部分までRubyでやってみたり、先に1605件の部署名込みのCSVを用意してから必要な件数を抽出してみたりと、あれこれ試行錯誤してどうにか希望するアウトプットを得られたという状況でした。この記事の内容は、それを後から振り返って「あっ、ここもっとこういう風に効率よくできたやん」と気付いた所をちまちま最適化した結果の、言うなれば「よそ行きのコマンド列」となっています。1つの事をやるにも解法が色々あり、期待する結果を得られていさえすればそのいずれも正解なのがシェルなので、シェルにまだ自信が無い方々は達人達の極まったシェル芸を見て萎縮してしまわず、自分なりのアプローチでスクラッチでコマンド列を組み立てる練習を重ねてみて下さい。

　

ところで、この記事で作った社員名簿には拠点の情報がありますが、日経Linux 2020年1月号掲載分の回の劇中データには拠点の情報がありません。実は、拠点の情報は[日経xTECHで展開中の「シス管系女子」過去回よりぬき＋新作Web連載](https://tech.nikkeibp.co.jp/atcl/learning/column/19/00014/)の最後の1話で使う予定で改めて付け加えた物なのでした（どんな使い方をするかは本編を見てのお楽しみ）。ただ、スケジュール的に制作が〆切デッドラインに間に合うかどうかギリギリなので、もし最後の1話が過去回の再掲になっていたら「ああ、間に合わなかったんだな……」と笑ってやって頂ければ幸いです。