さよなら無職

40歳のうちに

あと数日で41歳になります。41歳を目前にして就職先が見つかりました。就職先は数十人規模のベンチャーです。前職よりちょっと規模が小さいのですが、すでに大手企業をクライアントに抱えて、きちんと利益を出しているというところがすごいです。

プログラマ定年

35歳プログラマ定年説などと言われていますが、40歳での就職は本当に厳しかったです。多くの転職サイトに登録しても、年齢が40歳というのは企業からの検索条件から外れるのか、そのサイト上の履歴書の閲覧履歴を見たところで、月に数社程度です。
また、応募しても書類選考で17連敗でした。中には履歴書を見た上でスカウトをしてきたはずなのに、いざ応募すると書類選考落ちというケースもありました。離職期間が1年以上というのも響いたかもしれません。

面接

今回、初めて書類選考を通過し、1次面接も通過、2次面接となりました。2次面接なんて受けたのは生まれて初めてです。
これまでの就職活動での面接は3社受けたことがありますが、どこも最初の面接だけで内定が得られました。最初の2社は新卒時でしたし、3社目は三十代前半でしたので、それらのときは若さが大きく影響したのだと思います。
今朝、2次面接を受け、その場で採用決定していただき、そのまま今日から勤務してきました。

貯金の減っていく怖さ

無職生活は楽しかったのですが、年金や国民健康保険は免除申請していましたものの、貯金はどんどん減っていきました。昨年末には「今月の家賃とカードの支払いには普通預金が足りない!」と焦り、投資信託を解約したり、外貨預金を円に替えたり、どこかからかカネを引っ張ってこなくてはいけませんでした。残る金融資産もたいした額ではありませんから、就職しなければ路頭に迷うこと必至という状態でした。いまのそこそこの家賃の鉄筋コンクリートのマンションから、もっと安いアパートに引っ越したくても、審査が通らないでしょうから引っ越すこともできません。売れるモノはだいぶ売りましたが、雀の涙でした。1年後を考えると本当に胃が痛くなる状況でした。

これからはJavaプログラマ

就職先ではJavaプログラマになります。これまではC++でしたので、オブジェクト指向的な考え方はそこそこできますが、Java自体は経験がありません。しかし、即戦力になってくれと言われているのでプレッシャです。残業も多くなりそうです。このブログは継続するものの、なかなか更新できなくなるかもしれません。でも、やるしかないってわけで、もうすぐ41歳男、ここはイッパツ頑張ろうと思います。

トレンドワード抽出をちょっと変更。Jumanも使ってみる

1ツイート内での名詞はユニークにしてカウントする

以前から簡単なトレンドワード抽出をやっているのですが、いろいろ変な語が混ざってきます。2012年1月16日のベスト10を出してみると、次のようになっていました。

逐逐	    61.0
TORE	    58.0
土蜘蛛	    44.0
INFOBAR	    41.5
ケケ	    40.0
瑛	    39.8
チキンフィレダブル	    38.0
ッポイド	    37.0
A子	    32.0
イト	    30.6

この最初にある「逐逐」という形態素(と判定されたもの)が元のツイートではどう使われているのか見てみると、次のようなツイートでした。

間違い探し【中級】逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐遂逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐逐 できたらRT

「逐逐」という形態素はこの1つのツイートでのみ現れていて、それがこのトレンドワード抽出のための頻度に影響しています。今回以外にもこういうケースは多いようです。
「逐逐」を形態素とする判定自体はさておき、トレンドワードはみんなが書いているからこそトレンドになれるわけです。そのため、ツイートからの名詞(トレンドワード候補)抽出処理で、現在のように1つのツイートで同一形態素を何度もカウントするのはやめたほうが良さそうです。ツイート内で形態素(の、さらに名詞として判定されたもの)をユニークにして扱うことにします。
以前のコードからの修正は微々たるもので、いったんsetに入れてからそのsetの要素を出力するという単純なやり方で対処しました。

#!/usr/bin/ruby
#Coding:utf-8

require 'set'
require 'MeCab'

#char type
CHAR_DEFAULT = 0
CHAR_SPACE = 1
CHAR_KANJI = 2
CHAR_SYMBOL = 3
CHAR_NUMERIC = 4
CHAR_ALPHA = 5
CHAR_HIRAGANA = 6
CHAR_KATAKANA = 7
CHAR_KANJINUMERIC = 8
CHAR_GREEK = 9
CHAR_CYRILLIC = 10

def isNoun(morph_type)
  morph_type == "名詞"
end

def isSymbol(char_type)
  char_type == CHAR_SYMBOL
end

def isKanji(char_type)
  char_type == CHAR_KANJI 
end

def isAcceptableNoun(morph_inf)
  return isNoun(morph_inf[1]) \
    && !isSymbol(morph_inf[2].to_i) \
    && (morph_inf[0].length != 1 || isKanji(morph_inf[2].to_i))
end

mc = MeCab::Tagger.new('--node-format=%m\t%f[0]\t%t\n --eos-format=\tEOS\n')

while gets
  uniq_morphs = Set.new
  begin
    morphs = mc.parse($_).force_encoding("utf-8").split("\n")
  rescue
    next
  end
  morphs.each do |morph|
    morph_inf = morph.split("\t")
    uniq_morphs.add(morph_inf[0]) if isAcceptableNoun(morph_inf)
  end
  uniq_morphs.each do |uniq_morph|
    puts uniq_morph
  end
end

ついでに

mc.parse($_).force_encoding("utf-8").split("\n")

という行のsplit()でエラーになることがあるので、その場合の例外は握り潰すようにしておきました。合成文字によってエラーになるようですが、そのツイートごと無視することにします。

このスクリプトによって、同じ日のツイートを再解析してみると、トレンドワードは次のようになりました。

TORE	    57.0
瑛	    38.6
チキンフィレダブル	    38.0
INFOBAR	    37.8
ッポイド	    37.0
A子	    32.0
カミスン	    28.6
ラッキーセブン	    26.8
Lv	    24.0
ネプリーグ	    24.0

ちなみに「TORE」はテレビ番組です。そして「瑛」はテレビドラマですごい腹筋を披露していた俳優「瑛太」が分離されて判定されたものです。テレビの影響は相変わらず大きいですね。「ッポイド」はshindanmakerの「あなたをボーカロイドにしてみったー」をやった人が多かったためです。

Juman7.0で形態素解析してみる

瑛太」の解析が崩れてしまうのはMeCabというよりは辞書(ipadic)に原因があるのだろうと思いますが、辞書を修正するにしても、現実的にはいくら頑張っても未知語(辞書にない語)はあるわけですし、文字列をどんな形態素かと捉えるためのコスト調整(というもの)はあちらを立てればこちらが立たずという感じでなかなか難しいものです。

MeCabよりも古い歴史を持つ形態素解析ソフトウェアにJumanがあります。つい最近、新しいバージョンのJuman 7.0が発表されました。
Jumanのwebページによれば、このJuman 7.0ではWebでよく書かれる、次のようなひどい日本語(?)も解析できるとのことです。

  • ビミョーだ
  • がんがる
  • ありがとー
  • 行きたぁぁぁい

改めて考えてみると、昔は文語よりも口語のほうがくだけていたように思いますが、いまは文語のほうがくだけているのかもしれません。

次の2文をMeCabとJumanで解析させてみます。

MeCabでは次のようになります。

瑛	名詞,固有名詞,人名,名,*,*,瑛,アキラ,アキラ
太っ	動詞,自立,*,*,五段・ラ行,連用タ接続,太る,フトッ,フトッ
て	助詞,接続助詞,*,*,*,*,て,テ,テ
、	記号,読点,*,*,*,*,、,、,、
『	記号,括弧開,*,*,*,*,『,『,『
ラッキー	名詞,形容動詞語幹,*,*,*,*,ラッキー,ラッキー,ラッキー
7	名詞,数,*,*,*,*,*
』	記号,括弧閉,*,*,*,*,』,』,』
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ
も	助詞,係助詞,*,*,*,*,も,モ,モ
出	動詞,自立,*,*,一段,連用形,出る,デ,デ
てる	動詞,非自立,*,*,一段,基本形,てる,テル,テル
けど	助詞,接続助詞,*,*,*,*,けど,ケド,ケド
、	記号,読点,*,*,*,*,、,、,、
『	記号,括弧開,*,*,*,*,『,『,『
ワイルド	名詞,形容動詞語幹,*,*,*,*,ワイルド,ワイルド,ワイルド
7	名詞,数,*,*,*,*,*
』	記号,括弧閉,*,*,*,*,』,』,』
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ
も	助詞,係助詞,*,*,*,*,も,モ,モ
出	動詞,自立,*,*,一段,連用形,出る,デ,デ
て	動詞,非自立,*,*,一段,未然形,てる,テ,テ
なかっ	助動詞,*,*,*,特殊・ナイ,連用タ接続,ない,ナカッ,ナカッ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
か	助詞,副助詞/並立助詞/終助詞,*,*,*,*,か,カ,カ
!?	名詞,サ変接続,*,*,*,*,*
EOS
瑛	名詞,固有名詞,人名,名,*,*,瑛,アキラ,アキラ
太い	形容詞,自立,*,*,形容詞・アウオ段,基本形,太い,フトイ,フトイ
い	名詞,一般,*,*,*,*,い,イ,イ
から	助詞,格助詞,一般,*,*,*,から,カラ,カラ
だ	助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
や	助動詞,*,*,*,特殊・ヤ,基本形,や,ヤ,ヤ
なあ	助詞,終助詞,*,*,*,*,なあ,ナア,ナー
EOS

Jumanでは次のようになります。

瑛太 瑛太 瑛太 名詞 6 普通名詞 1 * 0 * 0 "自動獲得:テキスト"
って って って 助詞 9 副助詞 2 * 0 * 0 NIL
、 、 、 特殊 1 読点 2 * 0 * 0 NIL
『 『 『 特殊 1 括弧始 3 * 0 * 0 NIL
ラッキー らっきー ラッキーだ 形容詞 3 * 0 ナ形容詞 21 語幹 1 "代表表記:ラッキーだ/らっきーだ"
7 7 7 未定義語 15 その他 1 * 0 * 0 NIL
』 』 』 特殊 1 括弧終 4 * 0 * 0 NIL
に に に 助詞 9 格助詞 1 * 0 * 0 NIL
@ に に に 助詞 9 接続助詞 3 * 0 * 0 NIL
も も も 助詞 9 副助詞 2 * 0 * 0 NIL
出て でて 出る 動詞 2 * 0 母音動詞 1 タ系連用テ形 14 "代表表記:出る/でる 補文ト 自他動詞:他:出す/だす 反義:動詞:入る/はいる"
る る る 接尾辞 14 動詞性接尾辞 7 母音動詞 1 基本形 2 "代表表記:る/る"
けど けど けど 助詞 9 接続助詞 3 * 0 * 0 NIL
、 、 、 特殊 1 読点 2 * 0 * 0 NIL
『 『 『 特殊 1 括弧始 3 * 0 * 0 NIL
ワイルド ワイルド ワイルドだ 形容詞 3 * 0 ナ形容詞 21 語幹 1 "自動獲得:テキスト"
7 7 7 未定義語 15 その他 1 * 0 * 0 NIL
』 』 』 特殊 1 括弧終 4 * 0 * 0 NIL
に に に 助詞 9 格助詞 1 * 0 * 0 NIL
@ に に に 助詞 9 接続助詞 3 * 0 * 0 NIL
も も も 助詞 9 副助詞 2 * 0 * 0 NIL
出て でて 出る 動詞 2 * 0 母音動詞 1 タ系連用テ形 14 "代表表記:出る/でる 補文ト 自他動詞:他:出す/だす 反義:動詞:入る/はいる"
なかった なかった ない 接尾辞 14 形容詞性述語接尾辞 5 イ形容詞アウオ段 18 タ形 8 "代表表記:ない/ない"
か か か 助詞 9 接続助詞 3 * 0 * 0 NIL
!? !? !? 未定義語 15 その他 1 * 0 * 0 NIL
EOS
瑛太 瑛太 瑛太 名詞 6 普通名詞 1 * 0 * 0 "自動獲得:テキスト"
いい いい いい 形容詞 3 * 0 イ形容詞イ段 19 基本形 2 "代表表記:良い/よい 反義:形容詞:悪い/わるい"
からだ からだ からだ 名詞 6 普通名詞 1 * 0 * 0 "代表表記:体/からだ 漢字読み:訓 カテゴリ:動物"
や や や 助詞 9 接続助詞 3 * 0 * 0 NIL
なあ なあ なあ 感動詞 12 * 0 * 0 * 0 "代表表記:なあ/なあ"
EOS

瑛太」勝負ではJumanが勝利しました。とくに2番目の文がいやらしいというか面白く、「太い」という2文字に引っ張られたせいで周辺部もダメになっていますね。
瑛太」の代わりに「杉山」を使うとMeCabでも次のようになります

杉山	名詞,固有名詞,人名,姓,*,*,杉山,スギヤマ,スギヤマ
いい	形容詞,自立,*,*,形容詞・イイ,基本形,いい,イイ,イイ
からだ	名詞,一般,*,*,*,*,からだ,カラダ,カラダ
や	助動詞,*,*,*,特殊・ヤ,基本形,や,ヤ,ヤ
なあ	助詞,終助詞,*,*,*,*,なあ,ナア,ナー

この場合は「からだ」も1つの形態素として正しく解析されますね。いいからだになりたいものです。

tweetstreamをOAuthで

BASIC認証からOAuthにするソースコード修正はわずか

以前、tweetstreamを使ってTwitter Streaming APIにアクセスすることを試しました。その後、ツイートをログファイルに残すようにしたものの、認証方式はBASIC認証のままにしていました。OAuthというものがよくわからず、面倒臭かったからなのですが、いつまでもBASIC認証ってのもアレだろ、ってことでOAuthにしてみました。
BASIC認証からOAuthにするためのソースコードの修正はわずかで、TweetStream.configureの設定部分だけの修正で済みました。

#!/usr/bin/ruby
#coding:utf-8

require 'time'
require 'rubygems'
require 'tweetstream' 

SEPARATOR="\t"
LOGDIR="."

TweetStream.configure do |config|
  config.consumer_key = 'YOUR_CONSUMER_KEY'
  config.consumer_secret = 'YOUR_CONSUMER_SECRET'
  config.oauth_token = 'YOUR_OAUTH_TOKEN'
  config.oauth_token_secret = 'YOUR_TOKEN_SECRET'
  config.auth_method = :oauth
  config.parser   = :yajl	#using Yajl-Ruby
end

TweetStream::Client.new.sample do |status|
  if status.user.lang == "ja"
    reply = status.in_reply_to_user_id == nil ? "P" : "R" #R:Reply
    dt = Time.parse(status.created_at)
    fn = dt.strftime("#{LOGDIR}/%Y%m%d")
    dt_str = dt.strftime("%Y/%m/%d#{SEPARATOR}%X")
    text = status.text.gsub("\n", " ")
    logfile = open(fn, "a");
    logfile.puts "#{dt_str}#{SEPARATOR}#{reply}#{SEPARATOR}#{status.user.screen_name}#{SEPARATOR}#{text}"
    logfile.close
  end
end

認証文字列の取得

次の部分に記述しなければならない認証文字列がどういったもので、どうやって得るのかということがよくわかりませんでした。

  config.consumer_key = 'YOUR_CONSUMER_KEY'
  config.consumer_secret = 'YOUR_CONSUMER_SECRET'
  config.oauth_token = 'YOUR_OAUTH_TOKEN'
  config.oauth_token_secret = 'YOUR_TOKEN_SECRET'

まず、consumer_key と consumer_secret ですが、twitter developersにアクセスしてTwitterのユーザ名でログインします。僕としてはこのときのアカウントはそのアプリケーション用に新規ユーザを作っておいたほうが良いような気がします。

ログインすると、「Create a new application」というボタンが現れると思うので、そのボタンを押します。「Create a new application」というボタンが現れないときは右上のアカウント名にマウスカーソルを当てて「My applications」というメニューを選び、「My applications」画面にすると「Create a new application」ボタンが表示されるはずです。
「Create a new application」ボタンを押すと「Create an application」画面になるので、Name(アプリケーション名)、Description(アプリケーションの説明)、WebSite(アプリケーションのwebページのURL)を適当に入力します。
それらを入力したら、その下のDeveloper Rules Of The Roadを読み(読んだことにして)、「Yes, I agree」チェックボックスをチェックし、CAPTCHA文字列を入力して最下部の「Create your Twitter application」ボタンを押します。
なお、Nameは全世界のTwitterアプリケーション中でユニークなものでないとダメなようで、他のアプリケーションとダブっている場合は再度入力を促されます。
次の画面で「Create my access token」ボタンを押すと、Access tokenとAccess token secretが表示されます。また、Consumer keyとConsumer secretも同じページに表示されていると思います。

これらの文字列をコピーして、上記ソースコード中のconfig.consumer_keyにはConsumer keyとして表示された文字列、config.consumer_secretにはConsumer secret文字列、config.oauth_tokenにはAccess token文字列、config.oauth_token_secretにはAccess token secret文字列を、それぞれ代入します。あとは必要に応じてLOGDIRを修正すれば実行できるはずです。

Consumer keyとConsumer secretは、そのクライアントアプリケーションが何かを示すもののようです。Access tokenとAccess token secretはそのエンドユーザがそのクライアントを使ってアクセスするためのIDとパスワード代わりのもので、一般的にはいわゆるTwitterクライアントを使い始めるとき、Twitterのページに遷移して「このアプリケーションを許可しますか」的なことをしたときに生成され、そのアプリケーションデータとして保持されるものなのでしょう。

これらの鍵類をソース直書きしているのはちょっとイヤなのですが、どう管理すれば良いのでしょうね。

トレンドワード抽出をちょっとまともに

寒い

今年は節約のために暖房を使っておらず、換気のために窓を開けていることも多いので寒いです。身も心も懐も……。今年は就職できるといいんですけれど、なかなか厳しいです。

名詞から「RT」の削除

これまでに何度も出ているように、ツイート名詞の収集すると「RT」なんてのが名詞として判定されるし、頻出名詞として出るのでイヤです。削除するのは簡単ですが、簡単だからこそ、いつでもできるし……と除去をしていませんでした。
ツイートにおける頻出名詞ということを考えると、RT以降に続く文は誰かが書いた文が再度表れているわけで、ダブってカウントしているとも言えます。
そこで、ツイート中のRT以降の文はすべて除去することにします。非公式RTの代わりにQTを使って引用する人もいるので、QTも同様に処理します。引用した文の後に自分のコメントを書く人もいますが、そこらへんは無視します。
って、MeCab形態素解析する前にツイートに対してsedを通すだけです。

sed 's/[QR]T.*$//g'

これで名詞として「RT」や「QT」が出てくることはなくなりました。

記号は名詞として扱いたくない

先日名詞などから記号などをフィルタリングしたつもりでしたが、文字種判定がイイカゲンでしたし、後日のトレンドワード抽出処理ではフィルタリング処理を経由しないので、またもや記号などが現れていました。

そこで、以前のツイートから名詞を抽出する処理に追加して、それら記号などを除去することにします。MeCabは文字種が判定できるので、その判定結果を使います。MeCabでの文字種は辞書に依存します。辞書のあるディレクトリを見てみると、それらしきファイル( /usr/share/mecab/dic/ipadic/char.def )がありました。中には次のような記述があります。

DEFAULT        0 1 0  # DEFAULT is a mandatory category!
SPACE        0 1 0   
KANJI        0 0 2 
SYMBOL         1 1 0 
NUMERIC        1 1 0 
ALPHA        1 1 0 
HIRAGANA       0 1 2 
KATAKANA       1 1 2 
KANJINUMERIC   1 1 0 
GREEK        1 1 0 
CYRILLIC       1 1 0 

この順番が文字種IDになっているような気もします。確認のために、コマンドラインMeCabを動かして文字種IDを表示してみます。

$ mecab '--node-format=%m\t%f[0]\t%t\n'
こんにちは。私は「アホ」です。
こんにちは	感動詞	6
。	記号	3
私	名詞	2
は	助詞	6
「	記号	3
アホ	名詞	7
」	記号	3
です	助動詞	6
。	記号	3
EOS

「こんにちは」「は」「です」は6となっています。char.defの定義では0から数えて6番目がHIRAGANAなので合っています。括弧や句点は3でSYMBOLに対応するし、「アホ」は7でKATAKANAに対応しています。ってことで、とりあえずchar.defのこの定義順序が文字種IDだろうってことで作業を進めます。

MeCabの文字種IDを用いて記号などをフィルタリング

ツイートからの名詞抽出処理に、この文字種IDを用いたフィルタリングを加えます。記号を省くというだけではなく、1文字の語はそれが漢字でなければ省くという処理も加えます。これは名詞として判定された1文字のひらがなやカタカナを除去するためです。

#!/usr/bin/ruby
#Coding:utf-8

require 'MeCab'

#char type
CHAR_DEFAULT = 0
CHAR_SPACE = 1
CHAR_KANJI = 2
CHAR_SYMBOL = 3
CHAR_NUMERIC = 4
CHAR_ALPHA = 5
CHAR_HIRAGANA = 6
CHAR_KATAKANA = 7
CHAR_KANJINUMERIC = 8
CHAR_GREEK = 9
CHAR_CYRILLIC = 10

def isNoun(morph_type)
  morph_type == "名詞"
end

def isSymbol(char_type)
  char_type == CHAR_SYMBOL
end

def isKanji(char_type)
  char_type == CHAR_KANJI 
end

def isAcceptableNoun(morph_inf)
  return isNoun(morph_inf[1]) \
    && !isSymbol(morph_inf[2].to_i) \
    && (morph_inf[0].length != 1 || isKanji(morph_inf[2].to_i))
end

mc = MeCab::Tagger.new('--node-format=%m\t%f[0]\t%t\n --eos-format=\tEOS\n')

while gets
  morphs = mc.parse($_).force_encoding("utf-8").split("\n")
  morphs.each do |morph|
    morph_inf = morph.split("\t")
    puts morph_inf[0] if isAcceptableNoun(morph_inf)
  end
end

元旦のトレンドワード再抽出結果

前回、元旦のトレンドワードを抽出しましたが、今回変更した処理を経たデータを使って再度やり直してみました。
なお、前回は名詞の過去の平均出現頻度を得るために抽出対象日の11日前から2日前の10日間のデータを利用していましたが、8日前から2日前の7日間に変更しました。なんとなくですが1週間分あれば充分だろうと思います。

$ ./trend1.rb 20120101
争事	   546.0
出頭	   188.0
鳥島	   142.0
大凶	   139.1
ペッタン	   129.0
大儲け	   126.0
大損	   109.0
賭け事	   106.0
学業	    92.7
謹賀	    87.8
初夢	    76.7
大吉	    73.0
トリビア	    72.3
オウム真理教	    71.0
格付け	    70.2
ゃんあけおめ	    66.0
小吉	    63.5
Gackt	    63.3
ベイベー	    60.6
里谷	    59.0
アケオメ	    55.0
大河内	    55.0
あけ	    54.3
吹豪	    53.0
屠蘇	    53.0
ハス	    52.0
年初	    50.1
ぺったん	    49.6
メヌ	    49.0
ベイベ	    45.0
末吉	    44.7
343343	    43.0
辛酸	    43.0
運勢	    42.0
苦汁	    42.0
アンダルシア	    41.4
本年	    41.0
イタミン	    40.0
ピカル	    39.5
ガックン	    39.0
YEAR	    38.2
名塚	    38.0
富澤	    37.0
QE	    37.0
マジカルバナナ	    36.0
朝生	    36.0
TIGERandBUNNY	    35.0
幸先	    35.0
川崎大師	    35.0
願望	    34.6
あっけ	    34.3
ーベイ	    34.0
ペケポン	    33.0
清原	    32.5
梅宮	    32.0
貞治	    32.0
初詣	    31.9
年男	    29.3
アケオメ	    29.0
Year	    28.7
フットンダ	    28.6
初日の出	    28.3
お神酒	    28.0
コトヨロ	    28.0
護国	    28.0
ガクト	    27.8
雑煮	    27.7
凶	    27.1
上戸	    27.1
宇賀	    27.0
フレキス	    27.0
ジャニーズカウントダウン	    26.7
CDTV	    26.6
八幡宮	    26.1
書初め	    26.0
待人	    26.0
年頭	    26.0
堤下	    26.0
アビリティレベル	    26.0
HappyNewYear	    26.0

前回は記号やEUCでは表現できない文字が入っていましたが、マシな感じになりました。前回はいくつかあった数値がほとんど消えましたが、これはRT以降の文を削除したことが影響しています。唯一残っている「343343」は「343343+343343+343343=1030029」というツイートが複数あったことによります。これはテレビの「トリビアの泉」で「刺し身刺し身+刺し身刺し身+刺し身刺し身=お父さんはお肉」というネタをやったためです。

あとは複合語をまともに扱えるようになればだいぶマシになるのですが、複合語の扱いは敷居が高そうです。

元日の頻出名詞とトレンドワード

元日の頻出名詞

元日の頻出名詞を見てみました。さすがにいつもとは違います。

トレンドワードを抽出したい

ともあれ、これらの名詞の頻度は絶対値なので、「RT」とか「こと」とか常に上位に入っている名詞がやっぱり上位にあります。
それよりもトレンドワード的なものが見られたほうが面白そうです。

ある名詞が今日になったらいきなり出現頻度が増えたなんてときに、その名詞を知りたいものです。どうしたらそれがわかるかを考えてみました。もちろんソーシャルデータの扱いにおいて、既存の手法は腐るほどあると思います。でも、こういうのは自分で考えて試してみるっていうのが面白い(ような気がする)わけで。

ある日の名詞Aの頻度が、それ以前の頻度に比べて明らかに多いとき、その多さの比率順に表示する、という仕様にします。「それ以前の頻度に比べて明らかに多い」をどう定義するかですが、2倍なら明らかじゃないけれど3倍なら明らかかとか、10倍なら明らかだけれど9倍は明らかじゃないのかとか、閾値については悩ましいのですが、目的に応じて充分に明らかという値を考えれば良いと思います。
僕は「ある日」の11日前から2日前までの名詞毎に平均頻度を計算して、「ある日」の頻度が平均頻度の25倍以上多ければトレンディ!ってことにしました。25なんて値に意味はないので、必要に応じて変更すれば良いです。なお、「ある日」の1日前の頻度は無視しています。これは名詞の出現数は連続的に増えることを考えると、1日前の頻度については無視したほうが明確な結果が得られるからです。

頻度が1の名詞は省いて扱う

元となる頻度と名詞のデータは、data/count_nouns20120101 というような日ごとのファイルに

頻度\t名詞

というフォーマットで、頻度の多い順に記述されています。

2012年1月1日のデータを見ると、名詞(と判定されたもの)は122,090種類で、その日のファイルは122,090行になっています。そのうち、頻度が1の名詞は70,541種類です。つまり、過半数の種類の名詞はその日の(収集できた)全ツイートで1度しか出現しません。これらをきちんと扱うのもバカらしく、適当に省略して扱うことにします。

ざっとコードを書いてみました。慣れないRubyですが、それでも慣れているC++で書くより早いかもしれません。いつものように汚いコードです。

#!/usr//bin/ruby

require 'date'

def loadDayNouns2(fn, noun_freq)
  #p fn
  if infile = open(fn)
    while line = infile.gets
      freq_noun = line.split("\t")
      break if (freq_noun[0].to_i == 1) #freq==1は省く
      noun_freq[freq_noun[1].chomp] = freq_noun[0].to_i
    end
    infile.close
  end
end

def dayStr(day)
  day.to_s.gsub("-", "")
end

def loadDayNouns1(day, hash)
  day_str = dayStr(day)
  filename = "data/count_nouns" + day_str
  loadDayNouns2(filename, hash)
end

def aveFreq(noun, noun_freq_array)
  freq_sum = 0.0
  noun_freq_array.each {|noun_freq|
    freq_sum += noun_freq[noun].to_i #to_i is required?
  }
  ave = freq_sum / noun_freq_array.size
  return ave < 1.0 ? 1.0 : ave
end

daybasis = ARGV[0] ? Date.strptime(ARGV[0], "%Y%m%d") : (Date::today - 1)

noun_freq_array = Array::new
d = 11
while d >= 2
  noun_freq_array.push(Hash::new)
  loadDayNouns1(daybasis - d, noun_freq_array.last)
  d -= 1
end

daybasis_nouns = Hash::new
loadDayNouns1(daybasis, daybasis_nouns)

trend_nouns = Hash::new

GRAD_THRESHOLD = 25.0
daybasis_nouns.each_pair {|noun, freq|
  if freq > GRAD_THRESHOLD
    ave = aveFreq(noun, noun_freq_array);
    trend_nouns[noun] = freq/ave if ave * GRAD_THRESHOLD <= freq
  end
}

trend_nouns.sort_by{|noun, grad| -grad}.each do |noun, grad|
  puts sprintf("%s\t%8.1f", noun, grad)
end

「freq_sum += noun_freq[noun].to_i」という行があるのですが、ここでto_iが無いとエラーでした。なぜここにto_iが必要なのかわからないのですが、付けてあります。

trend1.rbというファイル名にしました。引数なしで実行すると昨日のトレンドワードを抽出します。「20120101」のような引数を付けて実行すると、その日(この場合は2012年1月1日)のトレンドワードを抽出します。

元日のトレンドワード

ってことで実行してみました。そんなに速くないコンピュータですが7〜8秒でした。名詞と頻度の関係をすべてメモリに読み込んで数万件のハッシュを何個も作ってそれを扱っています。そこそこ時間がかかるかなと思いましたが、予想外に速かったです。でも、せめてあと5倍くらい速くなればwebインタフェースから呼び出せるようにしてもいいんですけどねえ。

$ ./trend1.rb 20120101
争事	   556.0
出頭	   458.2
[ 	   369.0
 ]	   283.0
鳥島	   275.0
(&#9685;	   191.0
オウム真理教	   174.0
謹賀	   138.4
大儲け	   129.0
・:*:・。♪☆	   126.0
&#9829;	   121.9
蓼	   110.0
大損	   109.0
賭け事	   108.0
学業	   106.3
初夢	   105.1
大凶	    99.1
&#10039;(	    92.0
大吉	    87.9
小吉	    87.4
Gackt	    83.3
朝生	    81.8
&#8245;)	    75.6
あけ	    74.7
HIRO	    73.6
ゃんあけおめ	    66.0
大河内	    65.0
ベイベー	    63.0
格付け	    61.9
アケオメ	    61.0
近海	    60.7
年初	    60.3
里谷	    59.0
本年	    58.5
屠蘇	    57.0
^)/&#10024;	    56.0
YEAR	    55.1
川崎大師	    54.5
上戸	    54.4
吹豪	    53.0
書初め	    53.0
ハス	    52.0
末吉	    51.7
苦汁	    51.0
ヱヴァンゲリヲン	    50.6
袢袢	    50.0
メヌ	    49.0
(´&#9763;	    49.0
辛酸	    48.0
ピカル	    47.0
イタミン	    47.0
エイトレンジャー	    46.0
名塚	    46.0
CDTV	    45.0
&#9556;&#9559;&#9556;&#9559;	    44.0
16197	    44.0
運勢	    43.6
中通り	    43.3
343343	    43.0
幸先	    42.5
初詣	    41.3
初日の出	    41.1
アンダルシア	    41.1
120101	    41.0
マジカルバナナ	    41.0
ベイベ	    40.8
ガクト	    40.8
梅宮	    40.0
貞治	    40.0
Year	    39.2
猪苗代湖	    39.2
年頭	    39.0
QE	    39.0
ガックン	    39.0
宙ぶらりん	    38.0
雑煮	    37.6
富澤	    37.0
オウム	    36.6
口々	    36.0
白也	    36.0
ペケポン	    36.0
心眼	    36.0
願望	    35.4
TIGERandBUNNY	    35.0
あっけ	    35.0
111231	    35.0
久喜	    34.4
震度	    34.4
ーベイ	    34.0
孝之	    33.8
フットンダ	    33.3
田崎	    33.3
ジャニーズカウントダウン	    33.1
年男	    33.1
コトヨロ	    33.0
&#9673;∀&#9673;	    33.0
 &#9684;	    33.0
ペッタン	    32.2
GACKT	    32.1
栄作	    32.0
凶	    31.8
お神酒	    31.0
平田	    30.0
アケオメ	    30.0
護国	    30.0
芹沢	    30.0
昨年	    29.4
ニューイヤー	    29.3
籤	    29.1
センタ	    29.0
慶び	    29.0
Yell	    29.0
年女	    28.9
堤下	    28.0
佳織	    28.0
ご来光	    28.0
year	    27.5
ラーガン	    27.1
CDTV	    27.0
フレキス	    27.0
HappyNewYear	    26.0
アビリティレベル	    26.0
ドロンパ	    26.0
由紀子	    26.0
待人	    26.0
詣で	    26.0
5088	    26.0
明夫	    25.8
賀正	    25.7
1102	    25.5

はてなダイアリーEUCでハンドリングされているため、正しく表示できていないものがあります。

本当は除去すべき記号やらワケのわからん数字もありますが、やはり単純な頻度ランキングよりも面白いです。「争事」なんていうことばがあったので、なんのこっちゃと思ったのですが、おみくじ用語なのですね。他にもおみくじ用語が多いようです。そして、たとえば「5088」なんていう数は、非公式リツイートの多かった「バルス=秒間2万5088ツイート」という文から抽出されたものだったりします。

jqPlotでグラフ表示

iPadでプログラミング

大掃除をすることもなく年末を過ごし、年が変わる前に布団に入り、年が変わってしばらくして吐き気と腹痛で起床しました。食あたりだと思うのですが、食あたりになるといつも手足が冷たくなる(ように感じる)のに、顔からは冷や汗のようなものが出ます。なぜなんでしょう。
食あたり症状は幸い軽くて済み、昼過ぎまで惰眠を貪りました。
今季は暖房を節約のために使っていないので、布団から出るのもイヤで、布団の中でiPadからsshLinuxマシンにログインし、viでプログラミングしたりもします。効率が悪いなあと思いつつ、寒さに勝てない今日この頃です。

PHPのスコープを勘違いしていた

PHPはそのスクリプトを書くのに <?php …… ?> と書きますが、このブロックが違うと別ブロックから参照できないと思っていました。つまり、この記述ブロックローカルなスコープがあるのではないかと勘違いしていたのです。

<?php
$hoge = 0;
?>

<?php
echo $hoge;
?>

そのため、上記のようなことをするなら、$hogeをglobal宣言する必要があるのかと思っていました。でも、違うということにやっと気付きました。
ということで、昨年このブログに書いたPHPスクリプトではおかしなglobal宣言があったので、その部分をこそっと修正しておきました。

時間遷移で数値が変化するならグラフにするのだ

昨日までにツイート名詞の頻度を表形式で表示をするようにしたのですが、時系列データは表だけではなくグラフとして表示したいところです。調べてみると、JQueryのアドオンのjqPlotというもので簡単にグラフ表示ができるようです。

ってことで、名詞、調査開始日時、期間を指定して日ごとの名詞頻度を表示するスクリプトを、表だけではなくグラフ表示もするようにしてみました。モデルに対してビューが2つ(表とグラフ)になったとも言えるので、モデルとビューがべったりだったのを、少々分離しました。
そういえば、今回PHPJavaScriptコードを吐き出していますが、PHPJavaScriptを混合して記述する場合にどう書くのが一般的なのかわかりません。みなさんはいろいろ別ファイルにしていそうでもあります。

<!doctype html>
<?php
function genFileName(/*const*/ &$dt)
{   
  $fn = './data/count_nouns'.$dt;
  return (file_exists($fn) && is_readable($fn)) ? $fn : ''; 
}   

//$numdate should be YYYYMMDD format.
function ymdFromNum(/*const*/ &$numdate)
{
  $ymd = array();
  if (strlen($numdate) == 8) {
    $dt_array = strptime($numdate, "%Y%m%d");
    $year = $dt_array["tm_year"] + 1900;
    $month = $dt_array["tm_mon"]+ 1;
    $day = $dt_array["tm_mday"];
    if (checkdate($month, $day, $year)) {
      $ymd['y'] = $year;
      $ymd['m'] = $month;
      $ymd['d'] = $day;
    }
  }
  return $ymd;
}

//$numdate should be YYYYMMDD format.
function jqplotdateFromNum(/*const*/ &$numdate)
{
  $ymd = ymdFromNum($numdate);
  return empty($ymd) ? "" : $ymd['y'].'-'.$ymd['m'].'-'.$ymd['d'];
}

//$numdate should be YYYYMMDD format.
function unixdateFromNum(/*const*/ &$numdate)
{
  $ymd = ymdFromNum($numdate);
  return empty($ymd) ? 0 : mktime(0, 0, 0, $ymd['m'], $ymd['d'], $ymd['y']);
}

function grep1st(/*const*/ &$keyword, /*const*/ &$fn)
{
  $hit_line = array("0", "");
  $file_handle = fopen($fn, "r");
  while (!feof($file_handle)) {
    $line = explode("\t", trim(fgets($file_handle)));
    if ($line[1] === $keyword) {
      $hit_line = $line;
      break;
    }
  }
  fclose($file_handle);
  return $hit_line;
}

function putTable(/*const*/ &$noun, /*const*/&$date_freq)
{
  echo "<table class=\"tbl\">\n";
  echo "<caption> [".htmlspecialchars($noun)."] </caption>\n";
  echo "<thead> <tr> <th>date</th> <th>freq.</th> </tr> </thead>\n";
  echo "<tbody>\n";
  foreach ($date_freq as $dt => $freq)
    echo "<tr> <td>".$dt."</td> <td class=\"val\">".$freq."</td> </tr>\n";
  echo "</tbody>\n";
  echo "</table>\n";
}

function getDateFreq(/*const*/ &$noun, /*const*/&$dt_from, /*const*/&$term)
{
  $df_vec = array();
  if ($term > 0 && !empty($noun) && $dt_from != 0) {
    for ($i = 0; $i < $term; ++$i) {
      $dt = date('Ymd', $dt_from + $i * 86400);
      $fn = genFileName($dt);
      if (empty($fn)) {
        $df_vec[$dt] = "0";
      }
      else {
        $line = grep1st($noun, $fn);
        $df_vec[$dt] = $line[0];
      }
    }
  }
  return $df_vec;
}

function putDateFreqArray(/*const*/ &$df)
{
  foreach ($df as $dt => $freq)
    echo "['".jqplotdateFromNum($dt)."',".$freq."],";
}

function maxYOfGraph(/*const*/&$df)
{
  $maxfreq = 0;
  foreach($df as $dt => $freq) {
    if ($maxfreq < $freq)
      $maxfreq = $freq;
  }

  $digit = floor(log10($maxfreq));
  $unit = pow(10, $digit);
  $ymax = ceil($maxfreq / $unit) * $unit;
  return $ymax < 10 ? 10 : $ymax;
}

$noun = $_POST['noun'];
$logdate_from = (int)$_POST['logdate_from'];
$term = (int)$_POST['term'];

if ($term <= 0 || $term > 99) {
  $errmsg = "illegal term: ".$term;
  $term = 0;
}

if (empty($noun))
  $errmsg = "noun is empty.";

$dt = unixdateFromNum($logdate_from);
if ($dt == 0)
  $errmsg = "illegal date: ".$logdate_from;

$datefreq = getDateFreq($noun, $dt, $term);
?>

<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
  <link rel="stylesheet" href="noun_order.css"  type="text/css" media="all" />
  <link rel="stylesheet" type="text/css" href="jquery/jqplot/jquery.jqplot.min.css" />
  <!--[if IE]>
  <script language="javascript" type="text/javascript" src="jquery/jqplot/excanvas.min.js"></script>
  <![endif]-->
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js"></script>
  <script type="text/javascript" src="jquery/jqplot/jquery.jqplot.min.js"></script> 
  <script type="text/javascript" src="jquery/jqplot/plugins/jqplot.dateAxisRenderer.min.js"></script>
  <script type="text/javascript" src="jquery/jqplot/plugins/jqplot.highlighter.min.js"></script>
  <script type="text/javascript" src="jquery/jquery.tablesorter.min.js"></script>
  <script type="text/javascript">
    $(function(){
      $(".tbl").tablesorter();
    });
  </script>

  <script type="text/javascript">
    $(function(){
      if (<?php echo count($datefreq) >= 2 ? "true" : "false"; ?>) {
        var line1=[
          <?php putDateFreqArray($datefreq); ?>
          ];
        var plot1 = $.jqplot('graph_noun_freq', [line1], {
          title: <?php echo "'[".$noun."]'"; ?>, 
          highlighter: {show: true},
          axes:{
            xaxis:{
              renderer:$.jqplot.DateAxisRenderer,
              tickOptions:{formatString:'%#m/%#d'}
            }, 
            yaxis:{
              tickOptions:{formatString:'%d'},
              max:<?php echo maxYOfGraph($datefreq); ?>, 
              min:0
            }
          },
          series:[{lineWidth:4, markerOptions:{style:'square'}}]
        });
      }
    })
  </script>

  <title>noun finder</title>
</head>

<body>
<?php
if (empty($datefreq)) {
  echo $errmsg."<br />\n";
}
else {
  putTable($noun, $datefreq);
  echo "<hr />\n";
  echo "<div id=\"graph_noun_freq\" style=\"width:800px; height:300px;\"></div><br />\n";
}
?>
</body>
</html>

この中で、グラフの縦軸最大値としてキリの良い数値を得るための maxYOfGraph() という関数が書いてあります。その後半部でデータ最大値 $maxfreq から縦軸最大値を決定しているのですが、このへんはプログラマ以外の人には参考になるかもしれません。

グラフ表示結果

表示してみるとこんな感じになります。大掃除は30日に終える人が一番多いのかもしれません。大晦日まで頑張っている人も多そうです。偉い。

地震被害

今日の地震で本棚の上に置いておいたスピーカが落ちました。

そのスピーカのユニットは雑誌STEREO 2010年7月号の付録キットのもので、エンクロージャ(入れ物)は1.5Lのペットボトルで工作したものです。ちなみにアンプも雑誌STEREO 2012年1月号の付録デジタルアンプを使っていました。

これらが一式落ちてしまったのですが、スピーカユニットが壊れてしまいました。振動すると何かが当たっているようでノイズが出るようになってしまいました。ユニットを外してみましたが、見える範囲には異常がなく、修理できそうにありません。

ボーカルが良い感じで鳴ってなかなか気に入っていたのですが、残念です。

system()にフォーム入力を突っ込むのはやめておく

未来の自分は信用できないわけで。今年の汚れは今年のうちに。

昨日は面倒になってしまったので危ないコードを書いてしまったのですが、公開サーバではないとは言え、さすがにやめようと思いました。ぼーっとして危険だってことを忘れて公開してしまうかもしれないし。

なので、フォーム入力を受け付けて処理・表示するコードを修正しました。該当部分だけじゃなくて、他の部分もついでにごにょごにょと。まあ、相変わらず汚いのですけれども。

<!doctype html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
  <link rel="stylesheet" href="noun_order.css"  type="text/css" media="all" />
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js"></script>
  <script type="text/javascript" src="jquery/jquery.tablesorter.min.js"></script>
  <script type="text/javascript">
    $(function(){ $(".tbl").tablesorter(); });
  </script>

  <title>noun finder</title>
</head>

<body>
<?php
function genFileName(/*const*/ &$dt)
{   
  $fn = './data/count_nouns'.$dt;
  return (file_exists($fn) && is_readable($fn)) ? $fn : ''; 
}   

//$numdate should be YYYYMMDD format.
function dateFromNum(/*const*/ &$numdate)
{
  $dt = 0;
  if (strlen($numdate) == 8) {
    $dt_array = strptime($numdate, "%Y%m%d");
    $year = $dt_array[tm_year] + 1900;
    $month = $dt_array[tm_mon]+ 1;
    $day = $dt_array[tm_mday];
    if (checkdate($month, $day, $year))
      $dt = mktime(0, 0, 0, $month, $day, $year);
  }
  return $dt;
}

function grep1st(/*const*/ &$keyword, /*const*/ &$fn)
{
  $hit_line = array("0", "");
  $file_handle = fopen($fn, "r");
  while (!feof($file_handle)) {
    $line = explode("\t", trim(fgets($file_handle)));
    if ($line[1] === $keyword) {
      $hit_line = $line;
      break;
    }
  }
  fclose($file_handle);
  return $hit_line;
}

function putTable(/*const*/ &$noun, /*const*/&$dt_from, /*const*/&$term)
{
  if ($term > 0 && !empty($noun) && $dt_from != 0) {
    echo "<table class=\"tbl\">\n";
    echo "<caption> [".htmlspecialchars($noun)."] </caption>\n";
    echo "<thead> <tr> <th>date</th> <th>freq.</th> </tr> </thead>\n";
    echo "<tbody>\n";
    for ($i = 0; $i < $term; ++$i) {
      $dt = date('Ymd', $dt_from + $i * 86400);
      echo "<tr> <td>".$dt."</td> <td class=\"val\">";
      $fn = genFileName($dt);
      if (empty($fn))
        echo "logfile not found";
      else {
        $line = grep1st($noun, $fn);
        echo $line[0];
      }
      echo "</td> </tr>\n";
    }
    echo "</tbody>\n";
    echo "</table>\n";
  }
}

$noun = $_POST['noun'];
$logdate_from = (int)$_POST['logdate_from'];
$term = (int)$_POST['term'];

if ($term <= 0 || $term > 99) {
  echo "illegal term: ".$term."<br />\n";
  $term = 0;
}

if (empty($noun))
  echo "noun is empty.<br />\n";

$dt = dateFromNum($logdate_from);
if ($dt == 0)
  echo "illegal date: ".$logdate_from."<br />\n";

putTable($noun, $dt, $term);
?>
</body>
</html>

さすがにsystem()でgrepを呼ぶよりもだいぶ遅いけれど、思ったほどでもありませんでした。grepに-m 1を付けたときのように、検索ログファイル中で最初に対象名詞が見つかったときにそのファイルの検索を終了するようにしたので、出現頻度の高い名詞の検索は早いですが、ログファイルに出てこない名詞を調べるときには遅くなります。ただ、遅いと言っても10日分のログ検索が5秒程度です。
なお、ログファイルは1日分が12〜13万行(1.8MB程度)です。

運用マシンについて

ちなみに、検索に使っているコンピュータは7年ほど前に発売されたHPのnx9100というノート型でPentium 4/2.4GHz/RAM1.5GBでHT非対応のものです。ログファイルのあるディスクもUSB2.0の外付けなのでそんなに速い環境ではありません。これにUbuntuを入れて動かしています。

「年末」の頻度

正常系の挙動は昨日と変わっていないはずですが、せっかくなので「年末」という名詞の頻度の推移を見てみます。

皆さん、年末気分が盛り上がってきているようで。

おまけ。

こちらも盛り上がっているようで。