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

gihyo.jpで情報可視化の特集をやってて、すごく勉強になってよかったんですけど、
プログラムがJavaで書かれてて、個人的にRubyでやりたかったんで、
勉強ついでにプログラムをRubyで書き換えてみました。
そのRubyのプログラムはGithubにおいてあるので、ご自由にお使いください。
以下のリンクのdownloadからzip、またはtarで固めたものがダウンロードできます。
GitHub - ombran/gihyojp-visualization-ruby: The Ruby version of the information visualization introduced by gihyo.jp.(unofficial)


とまあ、さすがにこれで終わるのはひどいので、プログラムの簡単な説明をします。
今回はvisualization2のフォルダにあるプログラムの説明をします。
ただしここでRubyはインストール済みであるものとします。
Rubyのインストールとかの記事はいろんなところにあるんでそっちを参考にしてください。
Linuxとかなら元々インストール済みだったりしますし。

visualization2の説明

元記事だと第2回にあたる内容になります。
このプログラムは簡単に言えば、色集合を最短距離法に基づいて階層的クラスタリングを行うものです。
詳しいことは元記事を見てもらったほうがわかりやすいと思います。

visualization2フォルダ内容

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

$ tree visualization2
visualization2
-- Demo.rb
-- Visualization
-- Cluster.rb
-- ClusterBuilder.rb
-- DistanceEvaluator.rb
-- Item.rb
-- MultiVector.rb
-- NearestDistanceEvaluator.rb
`-- Node.rb
`-- Visualization.rb

拡張子の違いはありますが、元記事のプログラムのファイル名と内容を対応させてあります。
Visualization.rbを読み込むことだけで、Visualizationフォルダ以下のファイルを全て読み込めるようにしてあります。

元記事のプログラムとの違い

プログラムの内容自体はさほど違いはないんですけど、
元プログラムと大きく違うところとしてMultiVectorクラスの引数がハッシュになっています。

# MultiVector.rb
module Visualization
  class MultiVector
    # ベクトル生成(引数はハッシュ)
    def initialize(hash={})
      @data = {}
      hash.each do |key, value|
        @data[key] = value
      end
    end

...
end

これはベクトルの次元数が違うもの同士でもクラスタリングできるようにするためです。
ちなみに今回はベクトルの次元数が全て同じなのでこの利点が実感できませんが、
visualization6ではこれによって元記事のプログラムで用いられているクラスを一つ削減できるようになっています。
まあとりあえずハッシュのほうが便利だよ、とでも思っておいてください。


その他のクラスでは言語の違いはありますが内容にほとんど変化はありません。
ただし、全てのクラスはVisualizationモジュール内に含まれるようにしてあります。
Javaでのパッケージの代わりだと考えてください。

デモプログラムの実行

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

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

class Demo
  include Visualization

  def run
    # 入力データ作成
    input = []
    color = Struct.new(:red, :green, :blue)
    input << Item.new("BLUE",    colorToVector(color.new(0,   0,   255)))
    input << Item.new("CYAN",    colorToVector(color.new(0,   255, 255)))
    input << Item.new("MAGENTA", colorToVector(color.new(255, 0,   255)))
    input << Item.new("ORANGE",  colorToVector(color.new(255, 200, 0)))
    input << Item.new("PINK",    colorToVector(color.new(255, 175, 175)))
    input << Item.new("RED",     colorToVector(color.new(255, 0,   0)))
    
    # 最短距離法に基づく階層的クラスタリングを準備
    evaluator = NearestDistanceEvaluator.new
    builder = ClusterBuilder.new(evaluator)
    
    # クラスタリングを実行
    result = builder.build(input)
    
    # クラスタリング結果を表示
    output(result, 0)
  end
  
  def colorToVector(c)
    # 色成分を3次元のベクトルに変換
    MultiVector.new({:red => c.red, :green => c.green, :blue => c.blue})
  end

  def output(node, depth)
    # インデントを表示
    depth.times do
      print "    "
    end
    if (node.kind_of? Item)
      # 末端ノードなら項目名を表示
      puts node.getName
    elsif (node.kind_of? Cluster)
      # クラスタなら"+"を表示し、子ノードを再帰的に表示
      puts "+"
      cluster = node
      output(cluster.getLeft,  depth + 1)
      output(cluster.getRight, depth + 1)
    end
  end
end

demo = Demo.new
demo.run

入力データの色情報はRGBの値をそのまま入力してあります。
Demoクラスを実行すると,以下の出力が得られます。

$ ruby Demo.rb 
+
    +
        RED
        +
            MAGENTA
            +
                ORANGE
                PINK
    +
        BLUE
        CYAN

結果は元記事と同じになってるんで、プログラムは合ってると思うんですが、
もし何か間違い等ありましたら教えていただけるとありがたいです。

以上

今回はここまでです。
ほんとに簡単な説明で申し訳ないですけど、
元記事のプログラムと対応させて見てもらえるとわかりやすいかなと思います。
他のプログラムに関しては次回以降に説明します。

いろんなソーシャルブックマークサービスのブックマーク件数を数値で取得するRubyのクラス書いた

SBMはいっぱいありますけど、ブックマーク件数を取得するには、
XMLRPCやらJSONやらRESTやらでいろいろ違うんで、
簡単に取得できるクラスが欲しいと思って作りました。
Perlで書かれてるこちらをかなり参考にさせてもらってます。
というかこれのRuby版という感じになります。
ちなみに、SBMは、はてなブックマークlivedoorクリップYahoo!ブックマーク、
del.icio.usBuzzurlFC2ブックマーク、POOKMARK Airlinesの7つ利用できるようにしています。

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

プログラム内でJSON解析のためにgemからライブラリを持ってきてるので、
以下のようにしてインストールしてください。

# gem install json

ブックマーク件数取得プログラム

ブックマーク件数取得用ライブラリのプログラムは以下のようになります。

require 'open-uri'
require 'xmlrpc/client'
require 'rexml/document'
require 'digest/md5'

require 'rubygems'
require 'json'

class SBM
  # 初期設定
  @@sbms = {
    :hatena => {
      :name   => 'はてなブックマーク',
      :proxy  => 'http://b.hatena.ne.jp/xmlrpc',
      :entry  => 'http://b.hatena.ne.jp/entry/',
      :method => 'bookmark.getCount',
    },
    :livedoor => {
      :name   => 'livedoorクリップ',
      :proxy  => 'http://rpc.clip.livedoor.com/count',
      :entry  => 'http://clip.livedoor.com/page/',
      :method => 'clip.getCount',
    },
    :yahoo => {
      :name   => 'Yahoo!ブックマーク',
      :proxy  => 'http://num.bookmarks.yahoo.co.jp/yjnostb.php?urls=',
      :xpath  => '//SAVE_COUNT/@ct',
      :entry  => 'http://bookmarks.yahoo.co.jp/url?url=',
    },
    :delicious => {
      :name   => 'del.icio.us',
      :proxy  => 'http://badges.del.icio.us/feeds/json/url/data?url=',
      :entry  => 'http://del.icio.us/url/',
      :key    => 'total_posts',
    },
    :buzzurl => {
      :name   => 'Buzzurl',
      :proxy  => 'http://api.buzzurl.jp/api/counter/v1/json?url=',
      :entry  => 'http://buzzurl.jp/entry/',
      :key    => 'users',
    },
    :fc2 => {
      :name   => 'FC2ブックマーク',
      :proxy  => 'http://bookmark.fc2.com/image/users/',
      :regexp => /(\d+)\.png$/,
      :entry  => 'http://bookmark.fc2.com/search/detail?url=',
    },
    :pookmark => {
      :name   => 'POOKMARK Airlines',
      :proxy  => 'http://pookmark.jp/count/',
      :regexp => /(\d+)$/,
      :entry  => 'http://pookmark.jp/url/',
    }
  }

  attr_accessor :url

  def initialize(url)
    @url = url
  end

  # 一覧の出力
  def result
    self.get_all.each do |sbm, val|
      puts @@sbms[sbm][:name]
      puts "\t count:" + val[:count].to_s + "\t Entry:" + val[:entry]
    end
  end

  # すべてのSBMからブックマーク件数とSBMのURL取得
  def get_all
    sbm_counts = {}
    i = 0
    thread = []
    @@sbms.each do |sbm, etc|
      thread[i] = Thread.start do
        sbm_counts[sbm] = self.get(sbm)
      end
      i += 1
    end
    thread.each{|t| t.join}
    return sbm_counts
  end
  
  # 指定したSBMからブックマーク件数
  def get(sbm)
    case sbm
    when :hatena
      self.hatena
    when :livedoor
      self.livedoor
    when :yahoo
      self.yahoo
    when :delicious
      self.delicious
    when :buzzurl
      self.buzzurl
    when :fc2
      self.fc2
    when :pookmark
      self.pookmark
    else
      puts "Sorry, #{sbm} is not support."
    end
  end
  
  # SBMそれぞれ出力
  def hatena
    { :count => get_sbm_xmlrpc(:hatena),
      :entry => get_sbm_entry(:hatena) }
  end
  
  def livedoor
    { :count => get_sbm_xmlrpc(:livedoor),
      :entry => get_sbm_entry(:livedoor) }
  end
  
  def yahoo
    { :count => get_sbm_rest(:yahoo),
      :entry => get_sbm_entry(:yahoo) }
  end
  
  def delicious
    { :count => get_sbm_json(:delicious),
      :entry => get_sbm_entry(:delicious) }
  end
  
  def buzzurl
    { :count => get_sbm_json(:buzzurl),
      :entry => get_sbm_entry(:buzzurl )}
  end
  
  def fc2
    { :count => get_sbm_imageicon(:fc2),
      :entry => get_sbm_entry(:fc2) }
  end
  
  def pookmark
    { :count => get_sbm_imageicon(:pookmark),
      :entry => get_sbm_entry(:pookmark) }
  end

  
  private
  
  # XMLRPCによるブックマーク件数取得(hatena,livedoor)
  def get_sbm_xmlrpc(sbm)
    count = 0
    client = XMLRPC::Client.new2(@@sbms[sbm][:proxy])
    res = client.call2(@@sbms[sbm][:method], @url)
    res[1].each{|url, value| count = value } if res[0]
    return count
  end

  # REST(XML)によるブックマーク件数取得(Yahoo)
  def get_sbm_rest(sbm)
    count = 0
    open(@@sbms[sbm][:proxy] + @url) do |xml|
      doc   = REXML::Document.new(xml.read)
      count = REXML::XPath.first(doc, @@sbms[sbm][:xpath]).value.to_i
    end
    return count
  end
  
  # JSONによるブックマーク件数取得(delicious, buzzurl)
  def get_sbm_json(sbm)
    count = 0
    open(@@sbms[sbm][:proxy] + @url) do |json|
      data = JSON.parse(json.read)
      if data[0] != nil
        count = data[0][@@sbms[sbm][:key]].to_i
      end
    end
    return count
  end

  # 画像のURLからブックマーク件数取得(fc2, pookmark)
  def get_sbm_imageicon(sbm)
    count = 0
    open(@@sbms[sbm][:proxy] + @url) do |image|
      path = image.base_uri.path
      if path =~ @@sbms[sbm][:regexp]
        count = $1.to_i
      end
    end
    return count
  end

  # 入力されたURLに対応したSBMのURL表示
  def get_sbm_entry(sbm)
    url = @url
    url = Digest::MD5.hexdigest(@url) if sbm == :delicious
    return @@sbms[sbm][:entry] + url
  end
end

使い方

上で示したプログラムを「sbm.rb」というファイルで保存したとして、irbで試したものを以下に示します。
対象のURLは適当にGoogleで試してます。

irb(main):001:0> require 'sbm.rb'
irb(main):002:0> sbm = SBM.new('http://www.google.co.jp/')
irb(main):003:0> pp sbm.get_all
{:fc2=>
  {:count=>405,
   :entry=>
    "http://bookmark.fc2.com/search/detail?url=http://www.google.co.jp/"},
 :livedoor=>
  {:count=>293,
   :entry=>"http://clip.livedoor.com/page/http://www.google.co.jp/"},
 :pookmark=>
  {:count=>202, :entry=>"http://pookmark.jp/url/http://www.google.co.jp/"},
 :yahoo=>
  {:count=>62646,
   :entry=>"http://bookmarks.yahoo.co.jp/url?url=http://www.google.co.jp/"},
 :hatena=>
  {:count=>2133,
   :entry=>"http://b.hatena.ne.jp/entry/http://www.google.co.jp/"},
 :delicious=>
  {:count=>947,
   :entry=>"http://del.icio.us/url/9d0f4061beb6ae41f64eb124665e0768"},
 :buzzurl=>
  {:count=>62, :entry=>"http://buzzurl.jp/entry/http://www.google.co.jp/"}}

という感じでget_allでそれぞれのブックマーク数とSBMのURLのハッシュを取得できます。

そして、

irb(main):004:0> pp sbm.hatena
{:count=>2133, :entry=>"http://b.hatena.ne.jp/entry/http://www.google.co.jp/"}

irb(main):005:0> pp sbm.delicious
{:count=>947, :entry=>"http://del.icio.us/url/9d0f4061beb6ae41f64eb124665e0768"}

という感じで、はてブやdeliciouなどにも個別にアクセスできます。

あと、ちなみに、resultメソッドを実行すると

irb(main):006:0> pp sbm.result
FC2ブックマーク
         count:405       Entry:http://bookmark.fc2.com/search/detail?url=http://www.google.co.jp/
livedoorクリップ
         count:293       Entry:http://clip.livedoor.com/page/http://www.google.co.jp/
POOKMARK Airlines
         count:202       Entry:http://pookmark.jp/url/http://www.google.co.jp/
Yahoo!ブックマーク
         count:62646     Entry:http://bookmarks.yahoo.co.jp/url?url=http://www.google.co.jp/
はてなブックマーク
         count:2133      Entry:http://b.hatena.ne.jp/entry/http://www.google.co.jp/
del.icio.us
         count:947       Entry:http://del.icio.us/url/9d0f4061beb6ae41f64eb124665e0768
Buzzurl
         count:62        Entry:http://buzzurl.jp/entry/http://www.google.co.jp/

という感じで一覧を出力するようにしています。

以上

もし使いたい方がいれば、githubに登録してるんで自由に使ってもらってかまいません。
gist:27407 · GitHub

あと、どこかおかしいところとかあれば教えてもらうとありがたいです。

UbuntuでのEmeraldのインストール方法

Ubuntuをデフォルトのままじゃいまいち格好良くないので、見た目をよくしたい。
でも、できるだけ楽にテーマをかえたいってことでEmeraldを使うことにして、そのインストール方法。

インストール

$ sudo aptitude install emerald compizconfig-settings-manager fusion-icon

起動

インストールできたら、
「システムツール」->「Compiz Fusion Icon」
で、Fusion Iconを起動する。
タスクトレイにあるアイコンを右クリックして
「Select Window Decorator」->「Emerald」
でEmeraldを選択すれば、Emeraldが利用できるようになります。

テーマ設定

そして、もう一度タスクトレイにあるアイコンを右クリックして
「Emerald Theme Manager」
をクリックすれば、Emeraldのテーマを設定するウィンドウが表示されるのでここでテーマを設定します。

テーマはこの辺からダウンロードしてきてください -> /s/Compiz


ダウンロードするのめんどくさいという方は、ここリポジトリを設定して、
emerald-themesというパッケージをインストールしてください。
ただし、非公式なので自己責任でお願いします。


具体的にはUbuntu8.10の場合は以下のようになります。
まず、以下のリポジトリを設定して、

deb http://ppa.launchpad.net/portis25/ubuntu intrepid main
deb-src http://ppa.launchpad.net/portis25/ubuntu intrepid main

emerald-themesというをaptでインストール

$ sudo aptitude install emerald-themes

これだけで、Emeraldのテーマがいくつか利用可能になります。


ただし、それほど多いわけではない感じなので、ここから好みのテーマをダウンロードしてインストールしたほうがいいような気もします。

以上

結構簡単にテーマをかえたりできるので便利ですね。

githubでforkしてAutoPagerizeのオンオフをキーボードショートカットでするパッチを当ててみた

githubAutoPagerizeが公開されているので、gitの勉強ついでにforkして、この間書いたパッチを当ててみた。
タイトルの日本語おかしい気がするけど気にしないw

流れとしては、forkした後にgit cloneして、修正加えたら、git commitしてgit pushという流れです。
んで、パッチ当てたやつがこれ↓
autopagerize/autopagerize.user.js at master · ombran/autopagerize · GitHub
「shift+t」でAutoPagerizeをオンオフできるようにしたものなので、
興味のある人はご自由にどうぞ

一応差分は以下のような感じ。

@@ -28,6 +28,7 @@ var CACHE_EXPIRE = 24 * 60 * 60 * 1000
 var BASE_REMAIN_HEIGHT = 400
 var FORCE_TARGET_WINDOW = true
 var USE_COUNTER = true
+var TOGGLE_KEY = 'shift+t';
 var SITEINFO_IMPORT_URLS = [
     'http://wedata.net/databases/AutoPagerize/items.json',
 ]
@@ -109,6 +110,8 @@ var AutoPager = function(info) {
         (Math.round(scrollHeight * 0.8))
     this.remainHeight = scrollHeight - bottom + BASE_REMAIN_HEIGHT
     this.onScroll()
+
+    window.addEventListener("keydown", function(e){ self.keydown(e) }, false);
 }
 
 AutoPager.prototype.getPageElementsBottom = function() {
@@ -119,6 +122,40 @@ AutoPager.prototype.getPageElementsBottom = function() {
     catch(e) {}
 }
 
+AutoPager.prototype.keydown = function(event){
+    var keys = TOGGLE_KEY.toLowerCase().split('+');
+
+    var special_keys = {
+       27: 'esc', 9: 'tab', 32: 'space', 13: 'return', 8: 'backspace',
+       145: 'scroll', 20: 'capslock', 144: 'numlock', 19: 'pause',
+       45: 'insert', 36: 'home', 46: 'del',35: 'end', 33: 'pageup',
+       34: 'pagedown', 37: 'left', 38: 'up', 39: 'right',40: 'down',
+       112: 'f1',113: 'f2', 114: 'f3', 115: 'f4', 116: 'f5', 117: 'f6',
+       118: 'f7', 119: 'f8', 120: 'f9', 121: 'f10', 122: 'f11', 123: 'f12'
+    };
+
+    var code = event.which,
+       character = String.fromCharCode(code).toLowerCase(),
+       special = special_keys[code],
+       shift = event.shiftKey,
+       ctrl = event.ctrlKey,
+       alt = event.altKey;
+
+    var modif = { "shift": false, "ctrl": false, "alt": false };
+
+    for(var i=0; i<keys.length; i++){
+       if(keys[i]=="shift" || keys[i]=="ctrl" || keys[i]=="alt"){
+           modif[keys[i]] = true;
+           keys.splice(i, 1);
+       }
+    }
+    if(shift == modif.shift && ctrl == modif.ctrl && alt == modif.alt){
+       if(special == keys[0] || character == keys[0]){
+           this.toggle();
+       }
+    }
+}
+
 AutoPager.prototype.initHelp = function() {
     var helpDiv = document.createElement('div')
     helpDiv.setAttribute('id', 'autopagerize_help')

DragScrollをgithubに移動しました

以前作っていたグリモンを、何かと便利だと思ったのでgithubで管理するようにしました。
正確にはDiscover gists · GitHubで、コピペしただけですけど。

そのページがこちらです。
drag and page scroll · GitHub
内容は特別変更はないです。

以上

コピペだけでできたので、すごい簡単でした。
ちなみに、githubでの自分のページはこちらです。
ombran (Nobuhiro Nakashima) · GitHub

Google Chromeのabout:ページリスト

こことかこことかにGoogle Chromeのabout:ページのリストが載ってたんでメモ

  • about:version
  • about:plugins
  • about:cache
  • about:memory
  • about:stats
  • about:histograms
  • about:dns
  • about:network
  • about:crash
  • about:hang
  • about:internets

以上

about:internetsは所謂Easter Eggってやつですね。あとabout:hangは実行するとそのタブがハングアップしますので注意してください。まあこれでタブそれぞれが別プロセスで動いてるっていう確認にもなりますね。
詳しくは以下を見てください。
Google Chrome's about: Pages
http://lifehacker.com/5045164/google-chromes-full-list-of-special-about-pages

TwitterとRemember The Milk連携時の文字化けの対処法

Twitterと連携できて、スマートリストとかが便利そうなRemember The Milkを使うことにしたんですけど、
Twitterとの連携でいきなり文字化けして困ったんで、その対処法のメモ。
対処法といっても

Twitterの言語設定を「英語 - English」に変更する

これだけです。
理由はよくわかりませんが、日本語版TwitterでRTMにタスク追加しても文字化けしてしまいます。
ですが、英語版に切り替えることでこの問題は解決します。

以上

文字化けして困ってた方は、言語設定を英語にすればたぶん直りますよ。
ただし、これはWebからポストした場合で、クライアントからのポストでは文字化けしてしまいます。
誰か、クライアントからでも文字化けしない方法教えて><