具体例で学ぶ!情報可視化のテクニック

第6回はてなブックマークの可視化(後編)

はじめに

最終回となる今回は、これまでの学習内容のまとめとして、はてなブックマークの人気エントリーをツリーマップとして可視化します。

この可視化では、ノードの表示位置によってブックマークのカテゴリ特性を、ノードの大きさによってブックマーク数を、そして色によってブックマークの「コメント率」を、それぞれ視覚的に表現します。

ソースコードのダウンロード

今回作成するプログラムのソースコードは、こちらから一括してダウンロードすることができます。ZIPファイルを展開して生成されるフォルダを、プロジェクトとしてNetBeansに読み込むことも可能です。

特徴量ベクトルの生成

前回のプログラムでは、はてなブックマークにユーザーが付与したタグの一覧を収集しました。このタグ情報を特徴量ベクトルに変換し、第2回で作成したMultiVectorクラスのインスタンスとして表現することを考えます。

このとき問題となるのは、MultiVectorクラスのベクトル要素の添え字が整数である一方、タグが文字列であるため、タグをそのまま添え字としては使用できないことです。

そこで、タグを整数インデックスに変換するIndexMapperクラスを以下のように作成します。IndexMapperクラスは、put()メソッドで新しいタグが追加されるたびに、0, 1, 2, ...と順にインデックスを割り振り、ハッシュマップに記憶します。割り振られたインデックスは、後からget()メソッドで取得することができます。

リスト1 IndexMapper.java
public class IndexMapper {
    private Map<Object, Integer> map = new HashMap<Object, Integer>();

    public int size() { return map.size(); }

    public boolean contains(Object obj) {
        // インデックスが割り振り済みであるかどうかを調べる
        return map.containsKey(obj);
    }

    public void put(Object obj) {
        if (!contains(obj)) {
            // 新しいオブジェクトの場合、現在のサイズを新規インデックスとして割り振る
            int newIndex = size();
            map.put(obj, newIndex);
        }
    }

    public int get(Object obj) {
        // インデックスが割り振られていない場合は例外を発生
        if (!contains(obj)) {
            throw new IllegalArgumentException("Object is not found.");
        }
        // インデックスを返す
        return map.get(obj);
    }
}

IndexMapperクラスを使って、ブックマークエントリーに付与された全てのタグをインデックス化し、タグの出現回数を成分とすることで、エントリーの特徴量を持つベクトルを生成することができます図1⁠。

図1
図1

ブックマークノードクラスの作成

タグのベクトル化が可能となったところで、ブックマークエントリーのノードクラスとなるBookmarkItemクラスを作成します。このクラスは、第2回で作成したItemクラスを継承しており、階層的クラスタリングの入力ノードとして使用することができます。

リスト2 BookmarkItem.java(部分)
public class BookmarkItem extends Item {
    ...

    public BookmarkItem(Bookmark bookmark, BookmarkDetail detail,
            IndexMapper mapper) {
        // ブックマーク数をそのまま面積化すると比率が極端になるので
        // 平方根をとって調整する
        super(bookmark.title, tagsToVector(detail, mapper),
                Math.sqrt(detail.bookmarkCount));
        this.bookmark = bookmark;
        this.detail = detail;
    }

    private static MultiVector tagsToVector(BookmarkDetail detail,
            IndexMapper mapper) {
        MultiVector vector = new MultiVector(mapper.size());
        for (String tag : detail.tags) {
            if (mapper.contains(tag)) {
                int index = mapper.get(tag);
                vector.set(index, vector.get(index) + 1);
            }
        }
        vector.normalize(); // ベクトルを正規化する
        return vector;
    }
}

MultiVectorオブジェクトを作成した後、normalize()メソッドでベクトルを正規化していることに注意してください。階層的クラスタリングにおけるベクトル間の距離計算時に、特徴の差となる「角度差」を最大限に検出するため、あらかじめベクトルを正規化して長さを揃えているのです。

「実用系ブックマーク」「議論系ブックマーク」の色分け

はてなブックマークは、純粋にページを記録する目的に基づく「実用系ブックマーク」と、ページの内容についてユーザーが意見を述べ合うことを目的とした「議論系ブックマーク」の2つの側面を持っています。一般に、ブックマークエントリーのコメント率(コメントを記入しているユーザーの割合)が高いほど、⁠議論系ブックマーク」の傾向が強くなります。

ツリーマップ上で、この「実用系」または「議論系」の度合いを可視化することを考えましょう。ブックマークエントリーのコメント率に応じて、ツリーマップ領域の色合いを変化させます。今回は、コメント率が低いほど緑色、高いほど赤色に近い色で領域を描画するようにしてみます。こうすることで、緑色の領域は「実用系⁠⁠、赤色の領域は「議論系」のエントリーと視覚的に判断できるようになります図2⁠。

図2
図2

コメント率を求めるため、前回作成したBookmarkDetailクラスとHatenaBookmarkAPIクラスに以下の追加を行い、コメント情報を取得できるようにします。

リスト3 BookmarkDetail.java
public class BookmarkDetail {
    public int bookmarkCount;
    public List tags;
    public List comments;  // コメントの一覧
}
リスト4 HatenaBookmarkAPI.java(部分)
public class HatenaBookmarkAPI {
    public BookmarkDetail getDetail(String url) {
        ...
        detail.comments = new ArrayList();
        ...
        for (int i = 0; i < bookmarks.size(); i++) {
            JSONObject item = bookmarks.getJSONObject(i);
            JSONArray tags = item.getJSONArray("tags");
            for (int j = 0; j < tags.size(); j++) {
                detail.tags.add(tags.getString(j));
            }
            // 以下、追加分
            String comment = item.getString("comment");
            if (comment != null && comment.length() > 0) {
                detail.comments.add(comment);
            }
        }
        ...
    }
}

さらに、第4回で作成したBinaryTreeMapRendererクラスを以下のように修正します。コメント率に応じて、緑から赤までのグラデーションの中から色を決定し、ノード領域を塗りつぶすようにしています。なお、BinaryTreeMapRendererクラスには、ブックマークタイトルの描画など、他にも変更点があります。完全なソースコードはこちらをご覧ください。

リスト5 BinaryTreeMapRenderer.java(部分)
public class BinaryTreeMapRenderer implements TreeMapRenderer {
    ...

    private void doRender(Graphics2D g, Node node, Rectangle2D bounds, int depth) {
        if (node instanceof BookmarkItem) {
            BookmarkItem item = (BookmarkItem) node;
            g.setPaint(bookmarkToColor(item));
            g.fill(bounds);
        }
        ...
    }

    private Color bookmarkToColor(BookmarkItem item) {
        BookmarkDetail detail = item.getDetail();
        // コメント率を計算
        float commentRate = (float) detail.comments.size()
                / (float) detail.bookmarkCount;
        // 補正をかける
        commentRate = Math.min(2f * commentRate, 1f);
        // コメント率からRGB要素を決定
        float red = 0.2f + 0.4f * commentRate;
        float green = 0.2f + 0.4f * (1f - commentRate);
        float blue = 0.2f;
        return new Color(red, green, blue);
    }

    ...
}

デモプログラムの作成

それでは、はてなブックマークの人気エントリーを可視化するDemoクラスを見てみましょう。このクラスは、前回のデモプログラムに階層的クラスタリングとツリーマップの実行処理を付け加え、さらに画像ファイルへの出力処理を追加したプログラムとなっています。

少々長くなりますが、以下にコードの全体を掲載します。

リスト6 Demo.java
public class Demo {
    // 出力ファイル名
    private static final String OUTPUT_FILE_NAME =
            "C:/visualization/hatena_bookmark.png";

    public static void main(String[] args) {
        new Demo().run();
    }

    public void run() {
        HatenaBookmarkAPI api = new HatenaBookmarkAPI();
        List<Bookmark> bookmarks = api.getHotEntries();
        System.out.println(bookmarks.size() + " entries.");

        Map<Bookmark, BookmarkDetail> details =
                new HashMap<Bookmark, BookmarkDetail>();
        IndexMapper mapper = new IndexMapper();
        for (Bookmark bookmark : bookmarks) {
            System.out.println(bookmark.title);
            System.out.println("  [url] " + bookmark.url);
            try {
                // サーバの負荷を抑えるため呼び出し間隔を空ける
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            BookmarkDetail detail = api.getDetail(bookmark.url);
            if (detail != null) {
                System.out.println("  [bookmarkCount] " + detail.bookmarkCount);
                System.out.println("  [tags] " + detail.tags);
            }
            // タグを整数インデックスにマッピングする
            for (String tag : detail.tags) {
                mapper.put(tag);
            }
            details.put(bookmark, detail);
        }
        // 階層的クラスタリングの入力データを作成
        List<Item> input = new ArrayList<Item>();
        for (Bookmark bookmark : bookmarks) {
            BookmarkDetail detail = details.get(bookmark);
            input.add(new BookmarkItem(bookmark, detail, mapper));
        }

        // Ward法に基づく階層的クラスタリングを準備
        DistanceEvaluator evaluator = new WardDistanceEvaluator();
        ClusterBuilder builder = new ClusterBuilder(evaluator);

        // クラスタリングを実行
        Node result = builder.build(input);

        // クラスタリング結果をツリーマップに出力
        output(result);
    }

    private void output(Node node) {
        // 出力用画像を作成
        BufferedImage image = new BufferedImage(1024, 768,
                BufferedImage.TYPE_INT_RGB);

        // グラフィックオブジェクトを作成
        Graphics2D g = image.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        // 背景を白で塗りつぶす
        g.setPaint(Color.WHITE);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());

        // ツリーマップの描画を実行
        TreeMapRenderer renderer = new BinaryTreeMapRenderer();
        Rectangle2D bounds = new Rectangle2D.Double(20, 20,
                image.getWidth() - 40, image.getHeight() - 40);
        renderer.render(g, node, bounds);

        // 画像をPNGファイルに保存
        try {
            ImageIO.write(image, "png", new File(OUTPUT_FILE_NAME));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

可視化の実行と結果の分析

それではいよいよ、Demoクラスを実行してみましょう。プログラムを実行すると、その時点における、はてなブックマークの最新の人気エントリーの内容が可視化され、画像ファイルとして保存されます。筆者が2008年10月7日に実験を行ったところ、図3の画像が出力されました。

図3
図3

この可視化イメージから、どのような特徴を読み取ることができるでしょうか。いくつか例を挙げてみることにします図4⁠。

図4
図4
  • イメージの左下部分には、⁠地球温暖化」⁠ノーベル物理学賞」⁠神舟7号」と、環境・サイエンス系統の話題が集まっています。
  • イメージの右の部分には、いわゆる「ネタ系統」のエントリーが集まっています。この付近には赤っぽい領域が多いことから、エントリーへのコメント率が高く、⁠議論系ブックマーク」の傾向が強いことが分かります。
  • 「ネタ系統」の領域は、さらに「2ちゃんねる系統」「新聞系統」に大別されている様子が分かります。
  • 「ネタ系統」は、1つ1つの末端領域の面積が比較的小さいことから、1エントリー当たりのブックマーク数が少ない傾向が分かります。

いかがでしょうか。こういったブックマークの特徴は、ブックマーク情報を単にテキストで羅列しただけでは、把握することが困難なものばかりです。階層的クラスタリングとツリーマップという情報可視化手法によって、これまで良く見えなかったデータの全体像が、くっきりと浮き上がってきたのです。

現実の集合知データである、はてなブックマークの人気エントリーを材料とした今回の試みによって、情報可視化の有用性を改めて実感していただけたのではないでしょうか。

最後に

これまで6回にわたり、情報可視化の基礎から実践までを学んできました。本連載では特に、情報可視化手法の具体例として階層的クラスタリングとツリーマップを取り上げ、詳しく解説を行いました。しかし、もちろんこれらは、多彩な情報可視化のテクニックのうちのほんの一部に過ぎません。

本連載では多く触れるに至らなかった「GUIとの融合」のテーマを含め、情報可視化には大きな可能性が広がっています。皆さんもぜひ、データから新たな価値を引き出すことができる、独創的な情報可視化にチャレンジしてみてください。この連載が、その何らかのヒントになれば幸いです。

おすすめ記事

記事・ニュース一覧