自分がフォローしている人たちだけのbuzztterみたいなものを実現する with Echofon

先日中川さん(id:Psychs)によってEchofon for Macのベータ版がnaan studioからリリースされました。
http://echofon.com/twitter/mac/
http://d.hatena.ne.jp/Psychs/20091001/1254351633

必要な機能をシンプルにまとめたTwitterクライアントでとても使いやすいです。(個人的にはあとjk移動がほしい)


さてEchofonはTwitterのログを保存する際にSQLiteを使っているのが一つの特徴です。
そのため自分の過去ログをいじっていろんなことができておもしろいです。

例えば自分がフォローしている人たちだけを対象にしたbuzztterみたいなことを実現することができます。
(Twitter日本語圏全体からのホットなキーワード抽出として、ぼくは@yazztterというのを作っています:))
具体的には自分のまわりで直近1時間で話題になっているキーワード(特徴語)を抽出したりできます。
O/RマッパーとしてActiveRecord, 形態素解析器としてMeCabを使ってRubyで書くと下のようになります。

$KCODE = 'u'
require 'rubygems'
require 'active_record'
require 'MeCab'

ActiveRecord::Base.establish_connection(
  :adapter => 'sqlite3',
  :dbfile => '/Users/username/Library/Application Support/Echofon/tweets_db.sql' #usernameを自分の環境に合わせて変更
)

class Status < ActiveRecord::Base
end

def calculate_statuses_tfidf
  t = Time.now.strftime('%s').to_i #現在時刻をEchofonに合わせてUNIX時間に変換
  all_statuses = []
  recent_statuses = []
  past_time = 24 #とりあえず24時間前までのログを過去ログの対象とする
  (1..past_time).each do |i|
    lower_limit = t - 3600 * i
    upper_limit = t - 3600 * (i - 1)
    statuses = Status.find(:all, :conditions => ['created_at >= ? and created_at < ?', lower_limit, upper_limit])
    word_count = Hash.new(0)
    statuses.each do |st|
      st.text.split(/,、。  /).each do |s| #句読点や半角スペースと全角スペースがあったらそこでいったん区切る
        c = MeCab::Tagger.new
        n = c.parseToNode(s)
        words = ''
        while n do
          #抽出する単語を名詞や記号などに限定して、それらが連続していればくっつける
          if (n.feature.split(',').first == '名詞') || (n.feature.split(',')[1] == '連体化') || ((n.feature.split(',').first == '記号') && (n.feature.split(',')[1] == '一般'))
            words += n.surface
          else
            if words != ''
              word_count[words] += 1
              words = ''
            end 
          end
          n = n.next
        end
      end
    end
    recent_statuses = word_count.to_a if i == 1
    all_statuses << word_count.to_a
  end
  result = []
  recent_statuses.sort{|a, b| b[1] <=> a[1]}.each do |w|
    break if w.last < 3 #1時間以内の発言数が3未満の単語は対象外とする
    tf = w.last
    df = all_statuses.flatten(1).select{|i| i.first == w.first}.size #ruby 1.8.7以上からflattenに引数をとれるようになった!
    idf = Math::log(past_time / df.to_f)
    tfidf = tf * idf
    result << [w.first, tfidf]
  end
  result.sort {|a, b| b[1] <=> a[1]}
end

if __FILE__ == $0
  calculate_statuses_tfidf.each do |i|
    puts i.first + ': ' + i.last.to_s
  end
end

コードを読んでもらえれば大体どんなことをやっているか分かると思いますが、tf-idfというキーワード抽出のためのアルゴリズムを使っています。tf-idfとは簡単に言ってしまえば、ある文書中によく出てくる単語にはキーワードになりそうなので高スコアを割り当てたいけれども、その単語が他の文書中にも出てくるようならばそれはキーワードというよりも一般的な単語と見なせるのでその分スコアを差し引く、という考え方です。
参考: [を] 形態素解析と検索APIとTF-IDFでキーワード抽出

今回は1時間ごとのログを1つの文書とし、24時間分なので全体で24の文書があるとしてtf-idfを適用しています。
ログがあれば48時間分とかにしたほうがもう少し正確なキーワード抽出ができるかもしれません。


このプログラムを実行するとこんな感じになります。


Twitterクライアントは常時起動しているとは限らないのでどうしても取りこぼしなどが出てきてしまうのは仕方のないことですが、ログをきちんと保存するクライアントであれば、今回紹介したEchofonではなくても上記と同様の手法が適用できると思うので試したりしてください!