エンジニアチームのみんながどんな事に詳しいのかをざっくり知るため、Qiita:Teamをテキストマイニングしてスキルマップを作ってみる、というお話です。
ご挨拶
こんにちは。2015年10月にfreee株式会社にJoinした @kompiro こと近藤寛喜と申します。(本日34歳になりました)普段はフルスタックエンジニアとして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です。
# A sample Gemfile
source "https://rubygems.org"
gem 'qiita'
gem 'pry', group: :development
gem 'pry-nav', group: :development
開発用にpry
も入れておくと動作確認が捗るので入れておきます。1その後、Gemをローカルに入れましょう。
$ bundle
続いて、タグを取得するスクリプトを実行します。
#!/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
と右文脈ID
はmecab-dict-index
で自動でつけることもできます。自動でつけさせたい場合は*
を入れましょう。
コスト
ですが、Googleスプレッドシートなど表計算ソフトで計算するとよいでしょう。式は右記の通りです。=int(max(-36000, 6000 - 200 * POW(LEN(対象のセル),1.3)))
式の理由はググッたったらひっかかった記事を参考にしました。
さて、今回作る辞書では、どれも固有の用語として扱いたいので、すべての品詞
をfreee用語
とし、品詞細分類1
を固有名詞
、品詞細分類2
をfreee用語
、品詞細分類3
はQiita
としました。
活用形
などは動詞の時に使います。用語のため、すべて名詞として扱うので、*
でOKです。
原型
は表層系のままでOKです。今回は読み
や発音
は使わないので、*
にしましょう。
CSVの作成は、実際に分析したい用語かどうか整理しながら行うとよいです。例えばテンプレートに付随するタグ日報
は記事に登場しないので削除しますし、https
やGitHub
やQiita
などの用語は、リンクに登場しうるため削除しておいたほうが良いでしょう。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から記事を取得する
記事の取得はタグと同様スクリプトを実行します。
#!/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
次に、下記のスクリプトを実行します。
#!/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ファイルにまとめてしまいます。
#!/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)
}
まとめ
テキストマイニングを行おうと思うと辞書を作成するのが結構手間と思われがちです。しかし、みんなで入力したタグを基にすることで、テキストから注目したい用語を抽出することができました。作成した辞書は、例えば障害分析などでも「どの機能の報告が多いのか」、障害の内容から分析に使える辞書に育てられるでしょう。ぜひお試しください。[DIC]
最後に
freeeではデータからリアルを見せられるフルスタックエンジニアを募集しています。
明日はfreeeが誇る巨匠こと C++ブラックメイジ(マスター) の @terashi58 によるActiveRecordの高速化解説です!ActiveRecordの重い処理を薙ぎ払うフレアのような魔法を見せてくれます。お楽しみに。