gihyo.jpの「具体例で学ぶ!情報可視化のテクニック」のプログラムを勝手にRubyで書き換えてみた。その4

勝手にRubyで書き換えてみたプログラムも今回でラストになります。
プログラムのダウンロードは以下のリンク先のdownloadからご自由にどうぞ。
GitHub - ombran/gihyojp-visualization-ruby: The Ruby version of the information visualization introduced by gihyo.jp.(unofficial)
今回はvisualization6フォルダのプログラムについての説明になります。

visualization6の説明

元記事だと第6回にあたる内容になります。
このプログラムは、はてなブックマーク人気エントリーをツリーマップとして可視化するプログラムになります。
詳しいことは元記事を見てもらったほうがわかりやすいと思います。

visualization6フォルダの内容

visualization6のフォルダ内容は以下のようになっています。

$ tree visualization6
visualization6
-- Demo.rb
-- Visualization
-- BinaryTreeMapRenderer.rb
-- Bookmark.rb
-- BookmarkDetail.rb
-- BookmarkItem.rb
-- Cluster.rb
-- ClusterBuilder.rb
-- DistanceEvaluator.rb
-- HatenaBookmarkAPI.rb
-- Item.rb
-- MultiVector.rb
-- Node.rb
-- TreeMapRenderer.rb
`-- WardDistanceEvaluator.rb
-- Visualization.rb
`-- hatena_bookmark.png

Visualization.rbを読み込むことだけで、Visualizationフォルダ以下のファイルを全て読み込めるようにしてあります。
Demo.rbがデモ用のプログラムとなります。

RMagickとJSONライブラリのインストール

画像処理にRMagick、JSONデータのパースにJSONライブラリを使用するので、あらかじめインストールしておいてください。
まずRMagickはgemだと

# gem install rmagick

ちなみに、Ubuntuだとaptでインストールできます。

# apt-get install librmagick-ruby

次にJSONライブラリはgemでインストールできます。

# gem install json

プログラムの説明

基本的にプログラムのクラスそれぞれは元記事のプログラムと対応させてあるので、見比べられるようにしてあります。
ただ、ブックマークのタグを整数インデックスに変換するIndexMapperクラスというのが元記事のプログラムにはありますが、こちらのプログラムではMultiVectorクラスのデータがハッシュで作成しているので、IndexMapperクラスがいらなくなります。
以下のBookmarkItemクラスから元記事でのマッピングをしていないことがわかると思います。

# BookmarkItem.rb
module Visualization
  # はてなブックマークエントリに対応する末端ノード
  class BookmarkItem < Visualization::Item
    # @param bookmark:Visualization::Bookmarkオブジェクト
    # @param detail:Visualization::BookmarkDetailオブジェクト
    def initialize(bookmark, detail)
      # ブックマーク数をそのまま平均化すると比率が極端になるので
      # 平方根をとって調整する
      super(bookmark.title, tagsToVector(detail), Math.sqrt(detail.bookmarkCount))
      @bookmark = bookmark
      @detail = detail
    end
    
    def tagsToVector(detail)
      vector = Visualization::MultiVector.new
      detail.tags.each do |tag|
        vector.set(tag => (vector.get(tag).to_i + 1))
      end
      vector.normalize
      return vector
    end
    
    def getBookmark
      return @bookmark
    end
    
    def getDetail
      return @detail
    end
  end
end

あとは、画像描画をしているBinaryTreeMapRendererクラスですね。
基本的に元記事と同じですが、画像に文字を書き込んでいるdrawStringWithinメソッドは特殊なんで説明しときます。
まずプログラムは以下のようになります。

# BinaryTreeMapRenderer.rb(一部分)
module Visualization
  class BinaryTreeMapRenderer
...

    # ブックマークされたエントリのタイトル
    def drawStringWithin(draw, title, rect)
      d = draw
      # フォントサイズ
      pointsize = [(Math.sqrt(rect.width * rect.height) / 10).to_i, 10].max
      # 日本語表示のためにフォント指定(環境によって変更してください)
      d.font = "/usr/share/fonts/truetype/kochi/kochi-gothic.ttf"
      # 文字色
      d.fill("white")
      d.stroke_width(0)
      d.pointsize(pointsize)
      
      # 文字を改行しながら出力
      words = [rect.width.to_f/pointsize.to_f, 1].max.to_i
      t_ary  = title.split("")
      t_size = t_ary.size
      rows = (t_size.to_f/words.to_f).ceil
      rows.times do |i|
        start  = i * words
        finish = ((i + 1) * words) - 1
        text = t_ary[start..finish].join
        y = rect.y + pointsize * (i + 1)
        d.text(rect.x + 5, y + 5, text)
      end
    end
  end
end

d.fontでフォントの指定をしないと日本語を表示できないので気をつけてください。またこれは環境によって違うので、それぞれの環境で日本語を表示できるフォントの絶対パスを指定してください。
あとはpointsizeで文字の大きさを設定していたり、表示領域ごとに文字列を改行しながら表示しています。
ちなみにブックマーク数が多さで文字の大きさを変更してあります。

デモプログラムの実行

プログラムのデモを行うDemoクラスは以下のようになります。

# Demo.rb
$KCODE = 'u'

require 'rubygems'
require 'RMagick'
require File.dirname(__FILE__) + '/Visualization'

class Demo
  include Visualization
  
  # 出力ファイル名
  OUTPUT_FILE_NAME = 'hatena_bookmark.png'
  
  def run
    api = HatenaBookmarkAPI.new
    bookmarks = api.getHotEntries
    puts bookmarks.size.to_s + " entries."
    
    # 階層的クラスタリングの入力データを作成
    input = []
    bookmarks.each do |bookmark|
      puts bookmark.title
      puts "  [url] " + bookmark.url
      # サーバの負荷を抑えるため呼び出し間隔を空ける
      sleep(1)
      detail = api.getDetail(bookmark.url)
      input << BookmarkItem.new(bookmark, detail)
      if (detail !=  nil)
        puts "  [bookmarkCount] " + detail.bookmarkCount.to_s
        puts "  [tags] [" + detail.tags.join(", ") + "]"
      end
    end
    
    # Ward法に基づく階層的クラスタリングを準備
    evaluator = Visualization::WardDistanceEvaluator.new
    builder = Visualization::ClusterBuilder.new(evaluator)
    
    # クラスタリングを実行
    puts "クラスタリング開始(結構時間かかります)"
    result = builder.build(input)
    puts "クラスタリング終了"
    
    # クラスタリング結果を表示
    puts "画像生成開始"
    output(result)
    puts "出力ファイル:" + OUTPUT_FILE_NAME
  end
  
  def output(node)
    # 出力画像を作成
    g = Magick::Image.new(1024, 768)
    
    # グラフィックオブジェクトを作成
    d = Magick::Draw.new
    
    # 背景を白で塗りつぶす
    d.fill("white")
    d.rectangle(0, 0, g.columns, g.rows)
    d.draw(g)

    # ツリーマップの描画を実行
    renderer = Visualization::BinaryTreeMapRenderer.new
    bounds = Magick::Rectangle.new(g.columns - 40, g.rows - 40, 20, 20)
    renderer.render(g, node, bounds)
    
    # 画像をPNGファイルに保存
    g.write(OUTPUT_FILE_NAME)
  end
end

demo = Demo.new
demo.run

デモプログラムを実行すると、以下のような出力が得られます。

$ ruby Demo.rb 
30 entries.
おいしいスープのレシピ集:アルファルファモザイク
  [url] http://alfalfa.livedoor.biz/archives/51402336.html
  [bookmarkCount] 249
  [tags] [あとで読む, レシピ, お役立ち, ...
...
クラスタリング開始(結構時間かかります)
30
29
28
27
26
...
6
5
4
3
2
クラスタリング終了
画像生成開始
出力ファイル:hatena_bookmark.png

これで、hatena_bookmark.pngという以下のような画像ファイルが出力されます。
ツリーマップによってクラスタリングの結果が視覚的にわかりやすく表現されていることがわかると思います。
ただし、入力データは、はてなブックマークホッテントリに依存するので、出力される画像はその時によって変化します。

以上

今回で、はてなブックマークホッテントリのツリーマップ化ができるようになったので、これで勝手にRubyで書き換えたプログラムは終了です。
情報可視化に関してすごく勉強になりました。あと、書き換えをする中でRubyのプログラミングについてもいい勉強になりました。
こういう記事を書いてくれたgihyo.jpに感謝です。