Qiita::Teamからメンバーのスキルやお仕事マップをつくろう

  • 39
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

エンジニアチームのみんながどんな事に詳しいのかをざっくり知るため、Qiita:Teamをテキストマイニングしてスキルマップを作ってみる、というお話です。

ご挨拶

こんにちは。2015年10月にfreee株式会社にJoinした @kompiro こと近藤寛喜と申します。(本日34歳になりました:tada:)普段はフルスタックエンジニアとしてfreeeのユーザーさんにマジ価値を提供すべく日々奮闘してます。

freeeはまだ若い会社ですが猛烈な勢いでサービスが成長しており、結構な量のコードがあります。僕は入社したてということもあり、これまでチームの誰がどの仕事をしてきたのかがわかりません。なにか改善すべき点があったとして、 また、ここまでのAdvent Calendarの流れを見ていただいてもわかるとおり、freeeのエンジニアのみんなには得意技、言い方を変えればとんがっている分野を持っています。freeeは以前からQiita::Teamを使っており、現在結構な量の記事があります。みんな日報だったり、普段感じていることをポエムとして記事に残したり、日々学んだことを記録したり、革命をしたり、という事が記事に残っています。

こういった記事は、みんな関心があって記録しているものばかりです。例えばrspecという単語を記事にたくさん書いている人がいれば、仕事でrspecを使って何かをしている、ということが見えてきます。そういう時に活用できる技術がテキストマイニングです。テキストマイニングって、入社するまですごい難しい技術と思っていたんですが、ツールを駆使すればそれほど難しくありません。

ここにQiita::Teamのデータがあるじゃろ? ( ^ω^) ⊃データ⊂ 
ここからタグを取ってくるじゃろ?( ^ω^) ⊃タグ⊂
ちょいと整形してmecab-dict-indexに食わせて... ( ^ω^) ≡⊃⊂≡ 
( ^ω^)⊃ MeCab辞書できあがりじゃ
後はデータから記事を取ってきて、RStudioを用意するじゃろ?( ^ω^) ⊃記事とRStudio⊂
あとはよしなにスクリプトをかけるとな... ( ^ω^) ≡⊃⊂≡
( ^ω^)⊃ メンバーがよく書く用語からスキルやお仕事をざっくり把握できるのじゃ

ということで、今日はその辺りを解説します。

MeCabの辞書を作成する

テキストマイニングでは頻度の高い単語はなにかを分析するためには形態素解析を行います。単語を数え上げるだけであれば、正規表現でも良さそうですが、例えばデータ解析に使うRなどfreeeでよく使う言語は意図しない単語にもヒットしてしまうことに悩まされるでしょう。そうならないよう、今回は形態素解析を選択しました。ツールはMeCabを使います。

標準で用意されているipadic等は 普段のお仕事にカスタマイズされているわけではありません。今回テキストマイニングしてよく使われる頻度を知りたいのは、技術用語だったり、サービスの機能だったり仕事で使う特有の用語です。

Qiitaの記事にはタグをつけられますが、Qiita::Teamの場合はメンバーが自由にタグを追加できます。タグを基に用語を抽出することで、低コストに辞書が作成できるのではないか、と考えました。ということで、まずQiitaからタグを取得しましょう。

Qiita::Teamからタグを取得する

Qiitaからのデータの取得はQiita API v2で行います。アクセストークンを設定ページから取得しておきましょう。

今回はRubyスクリプトでデータの取得を行います。まずQiita APIにアクセスするため、qiita-rbをインストールします。

$ mkdir [your project name]
$ cd [your project name]
$ bundle init
$ [edit] Gemfile

Gemfileには下記の通り、qiita-rbがあればOKです。

Gemfile
# A sample Gemfile
source "https://rubygems.org"

gem 'qiita'
gem 'pry', group: :development
gem 'pry-nav', group: :development

開発用にpryも入れておくと動作確認が捗るので入れておきます。1その後、Gemをローカルに入れましょう。

$ bundle

続いて、タグを取得するスクリプトを実行します。

get_all_tags.rb
#!/usr/bin/env ruby

require 'qiita'
def get_last_page_count(response)
  uri =  URI.parse(response.last_page_url)
  params = URI::decode_www_form(uri.query).to_h
  params['page']
end

client = Qiita::Client.new access_token: ENV['QIITA_TOKEN'], team: ENV['QIITA_TEAM']
all_tags = []
tags = client.list_tags per_page: 100, sort: 'count'
page_count = get_last_page_count tags

File.open('all_tags.csv','w') do |file|
  page_count.to_i.times do |index|
    tags = client.list_tags per_page: 100, sort: 'count', page: index + 1
    tags.body.each do |tag|
      file.write("\"#{tag['id']}\",#{tag['items_count']}\n")
    end
  end
end

単純にタグを100件ずつ取得してファイルに入れているだけです。記事数も入れていますが、辞書データには不要です。なお、環境変数からアクセストークンやチーム名を取得していますが、これらの設定はdirenvを使うといろいろ捗るのでおすすめです。

辞書データの基になるCSVを作成する

さて、タグを取得した後はmecab-dict-indexに食わせるCSVを作成します。CSVは次の形式です。

表層系,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音
rspec,*,*,4379,freee用語,固有名詞,freee用語,Qiita,*,*,rspec,*,*

表層系とは、文章中に現れる表現のことです。左文脈ID右文脈IDmecab-dict-indexで自動でつけることもできます。自動でつけさせたい場合は*を入れましょう。

コストですが、Googleスプレッドシートなど表計算ソフトで計算するとよいでしょう。式は右記の通りです。=int(max(-36000, 6000 - 200 * POW(LEN(対象のセル),1.3)))
式の理由はググッたったらひっかかった記事を参考にしました。

さて、今回作る辞書では、どれも固有の用語として扱いたいので、すべての品詞freee用語とし、品詞細分類1固有名詞品詞細分類2freee用語品詞細分類3Qiitaとしました。

活用形などは動詞の時に使います。用語のため、すべて名詞として扱うので、*でOKです。
原型は表層系のままでOKです。今回は読み発音は使わないので、*にしましょう。

CSVの作成は、実際に分析したい用語かどうか整理しながら行うとよいです。例えばテンプレートに付随するタグ日報は記事に登場しないので削除しますし、httpsGitHubQiitaなどの用語は、リンクに登場しうるため削除しておいたほうが良いでしょう。2

CSVの準備ができたら、手元にMeCab環境を用意しましょう。

# MeCabが未インストールの場合は下記を追加してください。Mac以外の環境の場合もよしなによろしくおねがいします。
$ brew install mecab mecab-ipadic
# /usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic/ -u [辞書名] -f utf-8 -t utf-8 [辞書のベースファイル]
$ /usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic/ -u freee.dic -f utf-8 -t utf-8 freee_dic.csv

これでfreee.dicというユーザー辞書が作成されます。

作成した辞書は、~/.mecabrcにuserdicとして追加するとRのスクリプトを実行する時に辞書を指定せずにすむので下記のようにしておきましょう。

$ cp /usr/local/etc/mecabrc ~/.mecabrc
# userdicを ~/.mecabrc に追記する

メンバーごとに用語の頻度を解析する

辞書ができたらユーザーごとに記事をまとめ、形態素解析を行い、頻度を解析しましょう。

Qiita::Teamから記事を取得する

記事の取得はタグと同様スクリプトを実行します。

get_all_items.rb
#!/usr/bin/env ruby

require 'qiita'
def get_last_page_count(response)
  uri =  URI.parse(response.last_page_url)
  params = URI::decode_www_form(uri.query).to_h
  params['page']
end

def find_or_create_dir(path)
  FileUtils.mkdir_p path
end

client = Qiita::Client.new access_token: ENV['QIITA_TOKEN'], team: ENV['QIITA_TEAM']
items = client.list_items per_page: 100

page_count = get_last_page_count items

parent = find_or_create_dir('items')

page_count.to_i.times do |index|
  items = client.list_items per_page: 100, page: index + 1
  items.body.each do |item|
    user_id = item['user']['id']
    dir_path = "items/#{user_id}"
    name = "#{item['title']}".gsub(/(\s| |\/)/,"_")
    find_or_create_dir(dir_path)
    puts "#{dir_path}/#{name}.txt"
    file = open("#{dir_path}/#{name}.txt",'w')
    file.write(item['body'])
    file.close
  end
end

実行後、しばらくするとitemsの下にユーザーごとに記事が取得されます。

記事からMarkdown書式を取り除くなど加工する (12/23加筆)

このまま加工を進めると、コードブロックやURLが含まれているため、純粋な記述内容と乖離してしまいます。そこで、不要な記述を取り除きましょう。

不要な記述を取り除くには、一度Markdownを解析し、再出力するのがお手軽です。Markdownのパーサーであるredcarpetを使い、テキストマイニングに不要な記述を取り除きましょう。

まず、Gemfileにredcarpetを追加し、bundleコマンドでインストールしましょう。

source "https://rubygems.org"

gem 'qiita'
gem 'redcarpet'
gem 'pry', group: :development
gem 'pry-nav', group: :development
$ bundle install

次に、下記のスクリプトを実行します。

render_plain_text.rb
#!/usr/bin/env ruby

require 'redcarpet'
require 'redcarpet/render_strip'

class StripDownAndLink < Redcarpet::Render::StripDown

  def block_code(code, language)
    ''
  end

  def block_quote(quote)
    ''
  end

  def block_html(html)
    ''
  end

  def link(link, title, content)
    "#{content}"
  end

  def image(link, title, content)
    content &&= content + " "
    "#{content}"
  end

end

def find_or_create_dir(path)
  require 'fileutils'
  FileUtils.mkdir_p path
end

markdown = Redcarpet::Markdown.new(StripDownAndLink.new, extensions = {})
target = find_or_create_dir('strip_down_all_articles')
target_dir = target[0]
content_dir = 'items'

PATH_CHARS = %w(. ..)

Dir.open(content_dir).select { |e| !PATH_CHARS.include? e }.each do |user|
  items_path = "#{content_dir}/#{user}"
  puts "start strip_down_render #{items_path}"
  Dir.open(items_path).select{|e| File.file? "#{items_path}/#{e}"}.each do |item|
    target_user_path = "strip_down_all_articles/#{user}"
    target_user_dir = find_or_create_dir(target_user_path)[0]
    rendered_path = "#{target_user_dir}/#{item}"
    article_path = "#{items_path}/#{item}"
    if File.file?(article_path)
      contents = IO.read(article_path)
      rendered = markdown.render contents
      rendered.gsub!(/(?:f|ht)tps?:\/[^\s]+/, '')
      File.open(rendered_path,'w') do |rendered_file|
        rendered_file.write(rendered)
      end
    end
  end
end

しばらくするとstrip_down_all_articlesの下にテキストマイニングに不要な記述を取り除いた記事が出力されます。

メンバーごとに記事を結合する

このままだと用語の頻度の解析が面倒なので、記事を1ファイルにまとめてしまいます。

concat_by_user.rb
#!/usr/bin/env ruby

def find_or_create_dir(path)
  require 'fileutils'
  FileUtils.mkdir_p path
end

def concat_user_file(target, container, user)
  entries_path = "#{container}/#{user}"
  file_count = Dir.entries(entries_path).length
  concat_path = "#{target}/#{user}.txt"
  concat_file = File.open(concat_path,'w')
  if File.directory?(entries_path)
    Dir.entries(entries_path).each do |content|
      content_path = "#{entries_path}/#{content}"
      if File.file? content_path
        content_file = File.open(content_path,'r')
        content_file.each_line do |line|
          concat_file.write line
        end
      end
    end
  end
  concat_file.close
end

def count_create_contents(target_path, container, user)
  entries_path = "#{container}/#{user}"
  file_count = Dir.entries(entries_path).length
  target_path.write "#{user},#{file_count}\n"
end

target = find_or_create_dir('all_articles')
target_dir = target[0]
container = 'strip_down_all_articles'

File.open('articles_counter.txt','w') do |counter_path|
  Dir.open(container).each do |user|
    puts "start: #{user}"
    concat_user_file target_dir, container, user
    count_create_contents counter_path, container, user
  end
end

実行するとall_articles配下にユーザーごとにファイルが作成されます。

Rを使ってユーザーごとによく使う用語の頻度を出力する

最後にRを使ってユーザーごとに頻度をファイルに出しましょう。公式サイトからダウンロードしてもいいですし、Homebrew Caskでインストールするのもいいでしょう。

RStudioをインストールしたら起動し、RMeCabをインストールします。RMeCabはRからMeCabを使うためのラッパーです。テキストを統計的に解析するために必要なAPIが様々用意されています。例えば、今回で使うRMeCabFreqであれば、指定したファイルを形態素解析し、それぞれの単語ごとに頻度を計算します。

install.packages ("RMeCab", repos = "http://rmecab.jp/R")

その後、下記のRスクリプトを流すと、analyzed配下に頻度毎に集計したファイルが出力されます。

library('RMeCab')

dir.create('analyzed')
articles = list.files('all_articles')
for (article in articles) {
    article_path <- paste('all_articles',article, sep = "/")
    freq <- RMeCabFreq(article_path)
    filtered <- freq[freq[,2] == 'freee用語',] # freee用語は品詞です。品詞が同じもののみ選択します。
    ordered <- filtered[order(filtered$Freq,decreasing = TRUE), ]
    output_path <- paste('analyzed', article, sep='/')
    write.table(ordered,file = output_path)
}

後はgrepでもなんでもよしなに。例えばrspecで検索してみましょうか。

image

freeeでソフトウェアエンジニアのキャリアが始まった時、RSpecを書くところから始めた @hiraguri が1位でした。

まとめ

テキストマイニングを行おうと思うと辞書を作成するのが結構手間と思われがちです。しかし、みんなで入力したタグを基にすることで、テキストから注目したい用語を抽出することができました。作成した辞書は、例えば障害分析などでも「どの機能の報告が多いのか」、障害の内容から分析に使える辞書に育てられるでしょう。ぜひお試しください。[DIC]

最後に

freeeではデータからリアルを見せられるフルスタックエンジニアを募集しています

明日はfreeeが誇る巨匠こと C++ブラックメイジ(マスター) の @terashi58 によるActiveRecordの高速化解説です!ActiveRecordの重い処理を薙ぎ払うフレアのような魔法を見せてくれます。お楽しみに。


  1. よく見たらQiita gemしか使ってないですね。。 

  2. 削除してしまうとQiitaやGitHubに詳しい人がわからなくなりますね。記事を解析する際、httpで始まる文字列は解析対象に含まないようにしたほうがよさそうですね。。