自然言語処理を使って弊社エンジニアメンバーの特徴を言語化してみた

こんにちは! Hajimariの新卒2年目の栗岡です!

普段は受託開発を行いながら、採用担当をしています! 開発と採用の2足草鞋は内定者インターンから含めるとかれこれ2年になります。

今回は、プログラミングの力を使って、エンジニア採用でちょっと課題と感じていることを少しでも解消してみようと思います! (結論、そんなに簡単じゃなかったです、、、)

感じている課題

弊社のエンジニア組織/メンバーの特徴を言語化できていない故に、3年後・5年後にエンジニア組織の強みが分からなくなりそう、、、、

今は、会社自体が若く、小規模であるため、業務の幅の広さや裁量権の大きさを打ち出せるけれど、もし数年後従業員数が100人を超えたときに、どんな風に候補者さんに弊社のエンジニア組織を魅力的に思ってもらえる のか結構悩んでいます。

やること

組織は人の集まりなので、とりあえず、エンジニア組織のメンバーの特徴を改めて理解し、言語化したいと思います。 ざっくり言うと、「弊社のエンジニア組織の特徴としては、〜な人と・・・な人にカテゴライズできて〜」ってことを言えるようになりたい。

具体的な方法

wantedlyの入社ブログから、それぞれメンバーの特徴を抽出・分類する。 ちゃんと考えて書いた言葉は、人の特徴を表すので、とりあえずやってみるにはちょうど良いかなと思いました。

実装方法としては、自然言語処理でおなじみtf-idfでブログからメンバーの特徴を抽出しました。

www.sejuku.net

いろんな人が書いた文章を比較した際に、その人っぽい単語・言葉を見つけるって感じですかね。

自然言語処理や統計分析は大学の頃にほんの少しだけ触った程度ですので、初心者中の初心者です

やってみた

普段データ分析で遊ぶときは、jupyter notebookを使うのですが、今回、色々と探していると、Streamlitと言うデータ分析の結果を凄くwebっぽく表示してくれるフレームワーク があったので使ってみました。 なので、言語もpythonです。 www.streamlit.io

形態素解析 ブログの内容を単語ごとに区切る

前段階として、アナログにwantedlyのブログをコピペし、メンバーごとのテキストファイルを作りました。

 # nameにはメンバーの名前がloopで入ります
 def sprit_sentence_to_word(name):
  # 辞書登録(mecab-ipadic-neologd
  option = "-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd"
  tagger = MeCab.Tagger("-Ochasen " + option)
  # アナログに作ったテキストデータリンク
  open_link = "/text_data/wantedly/" + name + ".txt"
  f = open(open_link)
  text = f.read()
  f.close()
  # パースする 
  res = tagger.parseToNode(text)
  #  ストップワードリスト取得
  path = "stop_words.txt"
  #  日本語のストップワードを取得し、リスト化します
  stop_words = create_stopwords(path)
  words = []
  while res:
        # ストップワード除去
         if res.surface not in stop_words:
              word = res.surface
              part_of_speech = res.feature.split(",")[0]
              sub_part_of_speech = res.feature.split(",")[1]
              if part_of_speech in ['名詞', '動詞', '形容詞']:
                  if sub_part_of_speech not in ['空白', '*']:
                      words.append(word)
           res = res.next
      return words
形態素解析した結果をファイル保存
  data = sprit_sentence_to_word(name)
     open_link = "/output/wantedly/" + name + ".txt"
      with open(open_link, 'w', encoding='utf-8') as f:
         f.write(' '.join(data))


 # こんな感じ単語区切りになり保存されます
  石川県 生まれ 札幌 住み 現在 千葉県 住ん 5歳 高校 卒業 サッカー 漬け 日々 大好き サッカー 辞め 
  休日 1 ミリ 出 多い ん インドア ん 笑 大学 青山学院大学 進学
TF-IDFの計算
 def analysis(name):
  contents = []
  eng_names = []
  open_link = "/output/wantedly/" + name + ".txt"
  
  # ファイルをオープンする
  test_data = open(open_link, "r")
  # データを書き込む
  contents.append(test_data.read())
  # ファイルクローズ
  test_data.close()
  # エンジニアの名前も詰める
  eng_names.append(name)

  # データフレームにする
  df = pd.DataFrame({'name': eng_names,
                    'text': contents})
   
  # TF-IDFの計算
  # TF-IDFの上位200単語・単語頻出度10%~90%を取得
  tfidf_vectorizer = TfidfVectorizer(use_idf=True, min_df=0.1, max_df=0.90, max_features=200)
 
  # Tfidf値を取得
  tfidf_matrix = tfidf_vectorizer.fit_transform(df['text'])
  
  # index 順の単語リスト
  terms = tfidf_vectorizer.get_feature_names()
  tfidfs = tfidf_matrix.toarray()

  # TF-IDF表示
  show_tfidf(terms, tfidfs, eng_names)
 
 def show_tfidf(terms, tfidfs, eng_names):
  df = pd.DataFrame(
    tfidfs,
    columns=terms,
    index=eng_names)
 #表で出力
 st.write(df)

こんな感じに出力されました。 数値が1に近いほど特徴的な単語になります。200単語あるので全てお見せすることは難しいですが、、 f:id:marron-web-engineer:20200811091345p:plain

ちょっと考察ができそうな単語をいくつかみてみます。

f:id:marron-web-engineer:20200811234346p:plain

事業という単語からは、エンジニアとして事業を作っていきたいと普段から発信している中野が高い数値となっています。

仲間という単語では、CTOの柳澤と新卒1年目の保坂に高い数値が出てます。 仲間(一緒に働く人)が軸に入っている候補者の方との面談にアサインしたいです。

f:id:marron-web-engineer:20200811234401p:plain 成長・技術という単語では、板橋に高い数値が出ています。 ブログを読んでもらってから面接を行い、入社からの成長を技術周りの事を交えて話してもらえると候補者体験として効果的かもしれません。

それぞれの単語をみていくだけでもたくさんの考察や気づきがあるのですが、やりたいことはエンジニアメンバーを近い特徴同士でグルーピングすることなので、次元圧縮を行いメンバーそれぞれの特徴をまとめて数値にします。 今回は、次元圧縮をちょっと話題になっているらしいのでt-SNEを使ってみます。 詳しくはこちらをご覧ください。

qiita.com

次元圧縮
 # tsne次元圧縮 
 # tfidf_matrix:上部で表示したtfidfデータ
 # df.name:エンジニアメンバー名前データ
 do_tsne(tfidf_matrix, df.name)

 def do_tsne(tfidf_matrix, name):
  # 2次元に変換 回転数1200
  tsne = TSNE(n_components=2, perplexity=50, n_iter=1200)
  tsne_tfidf = tsne.fit_transform(tfidf_matrix)

  # データフレームにセット
  df_tsne = pd.DataFrame(tsne.embedding_[:, 0],columns = ["x"])
  df_tsne["y"] = pd.DataFrame(tsne.embedding_[:, 1])
  # プロットする際に名前で表示させる
  df_tsne["name"] = df.name
  # テーブルで表示 
  st.write(df_tsne)

  # プロットグラフで表示
  c = alt.Chart(df_tsne).mark_point().encode(
    x='x',
    y='y',
   ).mark_text(
    align='left',
    baseline='middle',
    dx=10
   ).encode(
    text='name',
   )
  st.altair_chart(c, use_container_width=True)

結果

f:id:marron-web-engineer:20200812002537p:plain

微妙。。。散らばっているので、特徴ごとにグルーピングもできず、、、、 一箇所くらいまとまりあれば面白かったのですが、、、、 そんなに簡単にはいかないそうです。リベンジします。

最後に、ちょっと流行ってるword2vecで〜に〜を聞いてみたシリーズに乗っかり、

*word2vecとは、文章中の単語や単語間の意味を数値化(ベクトル化)する、googleの偉い方が考えたニューラルネットワークモデルです。

Hajimariのエンジニア組織にビジョンである「自立」を聞いてみました。 tf-idf同様wantedlyの入社ブログが元データです。

参考にさせて頂きました。

note.com

ソースコードのメインは以下の通り

 def filler_word2vec():
  # sample.txtには、ブログデータが単語区切りで入っています。
  sentences = word2vec.LineSentence('/output/sample.txt')
  # ブログデータベクトル化
  # 出現5単語未満切り捨て
  model = word2vec.Word2Vec(
   sentences,
   sg=1,
   size=100
   min_count=5,
   window=8,
   hs=1,
   iter=5
  )
  
  #学習モデル作成
  model.save('/model/sample.model')
 
 def analysis():
  load_model_path = '/model/sample.model'
  #学習モデル読み込み
  model = word2vec.Word2Vec.load(load_model_path)
  
  #自立に近い意味合いの単語を出力
  results = model.wv.most_similar(positive=['自立'])
  return results
 
 model = filler_word2vec()
 results = analysis()

f:id:marron-web-engineer:20200812011225p:plain

*1に近いほど類似しています

データ量が少ない関係で、数値が固まってしまいましたが、

「夢のために、新しいことに挑戦し、働く」

といった弊社らしい「自立」を解釈できるアウトプットになりました。 (少し無理やり感もありますが、、笑)

まとめ

  • 元となるデータがwantedlyでよかったのか?

  • データ数少なすぎじゃないか?

  • 分析の方法が本当に適切なのか?

などなど突っ込みどころ&反省もある内容であったと思いますが、採用という定性的で感覚が大きいテーマにおいて、言葉を数値化することで分かることも一定ありそうだなと思いました。 ちょっと大学で自然言語っぽいこと齧った程度では歯が立たないことは実感させられましたが、せっかくエンジニアと他の職務も兼務できる環境なので、プログラミングをいろんな分野に使っていけたらなと思います!

夢のために、新しいことに挑戦し、働く エンジニア組織ですが、まだまだ仲間がたりません!事業や組織作りに興味がある・モチベーション高い仲間と働きたいエンジニアさんがいらっしゃいましたら、是非お話しましょう!

www.wantedly.com

www.wantedly.com

speakerdeck.com