はじめに
最終回となる今回は、
この可視化では、
ソースコードのダウンロード
今回作成するプログラムのソースコードは、
特徴量ベクトルの生成
前回のプログラムでは、
このとき問題となるのは、
そこで、
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クラスを使って、

ブックマークノードクラスの作成
タグのベクトル化が可能となったところで、
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オブジェクトを作成した後、
「実用系ブックマーク」と「議論系ブックマーク」の色分け
はてなブックマークは、
ツリーマップ上で、

コメント率を求めるため、
public class BookmarkDetail {
public int bookmarkCount;
public List tags;
public List comments; // コメントの一覧
}
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);
}
}
...
}
}
さらに、
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);
}
...
}
デモプログラムの作成
それでは、
少々長くなりますが、
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();
}
}
}
可視化の実行と結果の分析
それではいよいよ、

この可視化イメージから、

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