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

前回に引き続き、今回も勝手にRubyで書き換えたプログラムの簡単な説明をします。
プログラムのダウンロードは以下のリンク先のdownloadからご自由にどうぞ。
GitHub - ombran/gihyojp-visualization-ruby: The Ruby version of the information visualization introduced by gihyo.jp.(unofficial)

今回はvisualization4のフォルダにあるプログラムの説明します。

visualization4の説明

元記事だと第3回第4回にあたる内容になります。
このプログラムは階層的クラスタリングの実行結果をツリーマップで表現するものです。
詳しいことは元記事を見てもらったほうがわかりやすいと思います。

visualization4フォルダの内容

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

$ tree visualization4/
visualization4/
-- Demo.rb
-- Visualization
-- BinaryTreeMapRenderer.rb
-- Cluster.rb
-- ClusterBuilder.rb
-- ColorItem.rb
-- DistanceEvaluator.rb
-- Item.rb
-- MultiVector.rb
-- Node.rb
-- TreeMapRenderer.rb
`-- WardDistanceEvaluator.rb
-- Visualization.rb
`-- treemap.png

今回も前回同様、元記事のプログラムと拡張子の違いはありますが、ファイル名と内容を対応させてあります。
Visualization.rbを読み込むことだけで、Visualizationフォルダ以下のファイルを全て読み込めるようにしてあります。
Demo.rbがデモ用のプログラムで、treemap.pngは出力結果となります。

RMagickのインストール

プログラムの説明の前に、RMagickのインストール方法について説明します。
今回のプログラムでは画像を扱うので、画像処理用のライブラリとしてRMagickを用いるためです。
gemだと以下のようになります。

# gem install rmagick

RMagickのインストールにはライブラリが色々必要になりますけど、
それはいろんなところで書かれてると思うので頑張ってください。
ちなみに、Ubuntuとかなら、aptで簡単にインストールできます。

# apt-get install librmagick-ruby

プログラムの説明

今回のプログラムで元記事のプログラムと大きな違いは、画像描画部分になります。
そもそも使ってるライブラリ違うんで、当然といえば当然ですね。
その画像描画のプログラムはBinaryTreeMapRenderer.rbで、以下のようになります。

# BinaryTreeMapRenderer.rb
require 'rubygems'
require 'RMagick'

module Visualization
  #
  # 領域の2分割を再帰的に繰り返し、ツリーマップの描画を行う
  #
  class BinaryTreeMapRenderer
    include Visualization::TreeMapRenderer
    
    def render(graphic, node, bounds)
      doRender(graphic, node, bounds, 0)
    end
    
    def doRender(graphic, node, bounds, depth)
      d = Magick::Draw.new
      if (node.kind_of? Visualization::ColorItem)
        # ノードが色項目の場合は、その色で長方形を塗りつぶす
        d.fill("rgb(#{node.getVector.data[:red]}, #{node.getVector.data[:green]}, #{node.getVector.data[:blue]}, #{depth})")
        d.rectangle(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height)
      elsif (node.kind_of? Visualization::Cluster)
        cluster = node
        
        # 子ノードchild1とchild2を取得
        child1 = cluster.getLeft
        child2 = cluster.getRight
        # child1の面積の方が大きくなるようにする
        if (child1.getArea < child2.getArea)
          temp = child1
          child1 = child2
          child2 = temp
        end
        
        # 子ノードの面積比を計算
        area1 = child1.getArea
        area2 = child2.getArea
        ratio1 = area1 / (area1 + area2)
        ratio2 = area2 / (area1 + area2)
        
        x = bounds.x
        y = bounds.y
        w = bounds.width
        h = bounds.height

        rect1 = nil
        rect2 = nil
        # 領域分割を実行
        if (w > h)
          # boundsが横長の場合、左右に分割
          rect1 = Magick::Rectangle.new(ratio1 * w, h, x, y)
          rect2 = Magick::Rectangle.new(ratio2 * w, h, x + ratio1 * w, y)
        else
          # boundsが縦長の場合、上下に分割
          rect1 = Magick::Rectangle.new(w, ratio1 * h, x, y)
          rect2 = Magick::Rectangle.new(w, ratio2 * h, x, y + ratio1 * h)
        end
        # 子ノードを再帰的に処理する
        doRender(graphic, child1, rect1, depth + 1)
        doRender(graphic, child2, rect2, depth + 1)
      end
    
      # 輪郭を階層の深さに応じた太さで描画
      d.fill("transparent")
      borderWidth = [8 - depth, 1].max
      d.stroke_width(borderWidth)
      d.stroke("black")
      d.rectangle(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height)
      d.draw(graphic)
    end
  end
end

doRenderメソッドのgraphicはMagick::Imageオブジェクト、boundsはMagick::Rectangleオブジェクトになります。
d.fillで塗りつぶしの色を設定し、d.rectangleで四角の描画を行います。
あとd.stroke_widthで線の幅指定やd.strokeとかで線の色を指定したりしています。
そして最後にd.drawとすることで図形の元のMagick::Imageオブジェクトに図形の描画を行っています。
RMagickのメソッドの詳しい説明はこちらにあるので、細かいことはそちらをご覧ください。
ちなみに、元記事とライブラリなどは違いますが、プログラムの形式はほぼ同じになっているので、
元記事と比較しながら見れると思います。

デモプログラムの実行

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

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

class Demo
  include Visualization
  
  OUTPUT_FILE_NAME = 'treemap.png'
  
  def run
    color = Struct.new(:red, :green, :blue)
    # ランダムな色データを100個作成
    input = []
    100.times do |i|
      c = color.new(rand(256), rand(256), rand(256))
      area  = 0.2 + 0.8 * rand
      input << ColorItem.new(c, area)
    end
    
    # Ward法に基づく階層的クラスタリングを準備
    evaluator = WardDistanceEvaluator.new
    builder = ClusterBuilder.new(evaluator)
    
    # クラスタリングを実行
    puts "クラスタリング開始(結構時間かかります)"
    result = builder.build(input)
    puts "クラスタリング終了"
    
    # クラスタリング結果を表示
    puts "画像生成開始"
    output(result)
    puts "出力ファイル:" + OUTPUT_FILE_NAME
  end
  
  def output(node)
    # 400x400ピクセルの画像を作成
    g = Magick::Image.new(400, 400)
    
    # グラフィックオブジェクトを作成
    d = Magick::Draw.new
    
    # 背景を白で塗りつぶす
    d.fill("white")
    d.rectangle(0, 0, g.columns, g.rows)
    d.draw(g)

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

demo = Demo.new
demo.run

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

$ ruby Demo.rb 
クラスタリング開始(結構時間かかります)
100
99
98
97
96
95
...
5
4
3
2
クラスタリング終了
画像生成開始
出力ファイル:treemap.png

これで、treemap.pngという以下のような画像が出力されます。
ツリーマップによってクラスタリングの結果が視覚的にわかりやすく表現されていることがわかると思います。
ただし、入力データはランダムに作成しているので、出力される画像は毎回違うものとなります。

以上

今回はここまでです。
細かい部分はプログラムにコメントを書いてるんでそちらを読んでください。
あと、間違ってる部分などありましたら教えていただけるとありがたいです。
残りのプログラムについては次回以降説明します。