(追記:2014-3-3) Gvizについてのまとめ頁を作りました。

Gvizの目次 - Rubyの世界からGraphvizの世界にこんにちは!


RubyのGraphvizラッパー「Gviz」を前回紹介しました。

Yet Another Ruby Graphviz Interfaceを作ったからみんなで大量のグラフを作って遊ぼうよ!

そこでは隣接県情報に基づき日本地図を作りました。出来は兎も角、僕はこのやり方がすっかり気に入りました。そこで今回はもう一歩進んでこの地図でデータビジュアライゼーションをしてみようと思います。題材はアメリカ合衆国です!

以下では、データの取得から順を追ってグラフ作成のやり方を説明しています。ちょっと長い投稿になります。

隣接州情報の生成

地図の形のベースとなる隣接州情報を用意します。List of U.S. state abbreviations - Wikipedia, the free encyclopediaなどから州名と略称(USPS)を取得し、USPSを使って次のような隣接州情報のファイルを作ります。

アメリカ合衆国州情報の取得

ビジュアライズするデータを見つけます。各州の人口や面積が入った情報がいいですね。ここでは次のサイトを使います。

List of U.S. states - Wikipedia, the free encyclopedia

頁のソースをちょっと覗いて構造がわかったら、次のようなコードで対象データを取得します。

取得した各データをHEADER, BODY変数に割り当ててRubyスクリプトとして保存します。

データの読み出しと統合

次にこれらの情報をRubyスクリプト上に読み出し、整理して統合します。ファイル名をusstates.rbとします。まずはuslinks.csvを読み出します。

# encoding: UTF-8
require "csv"

_, *links = CSV.read('uslinks.csv')

links = links.map do |usps, name, *link|
  [usps.intern, name, link.map(&:intern)]
end.sort

links # => [[:AK, "Alaska", [:WA]], [:AL, "Alabama", [:MS, :FL, :GA, :TN]], [:AR, "Arkansas", [:MO, :TN, :MS, :LA, :TX, :OK]], [:AZ, "Arizona", [:CA, :NV, :UT, :NM, :CO]], [:CA, "California", [:OR, :NV, :AZ]], [:CO, "Colorado", [:WY, :NE, :KS, :OK, :NM, :AZ, :UT]], [:CT, "Connecticut", [:NY, :MA, :RI]], [:DE, "Delaware", [:MD, :PA, :NJ]], [:FL, "Florida", [:AL, :GA]], [:GA, "Georgia", [:FL, :AL, :TN, :NC, :SC]],   -- 中略 --   [:TN, "Tennessee", [:KY, :VA, :NC, :GA, :AR, :MS, :AL, :MO]], [:TX, "Texas", [:NM, :OK, :AR, :LA]], [:UT, "Utah", [:ID, :WY, :CO, :NM, :AZ, :NV]], [:VA, "Virginia", [:WV, :KY, :TN, :NC, :MD]], [:VT, "Vermont", [:NY, :MA, :NH]], [:WA, "Washington", [:OR, :ID, :AK]], [:WI, "Wisconsin", [:MI, :IL, :IA, :MN]], [:WV, "West Virginia", [:OH, :PA, :MD, :VA, :KY]], [:WY, "Wyoming", [:MT, :SD, :NE, :CO, :UT, :ID]]]

次にusdata.rbを読み出して、必要なデータを取り出します。

require_relative "usdata"

usdata = BODY.map do |name, ipa, usps, flag, sthood, area, pop, cap, popcity, pre, gdp|
  name = name.sub(/\[\d+\]$/, '')
  sthood = sthood[/\d{4}$/]
  area = area[/(?<=\()[\d,]+/]
  [usps.intern, name, cap] +
        [sthood, area, pop, gdp].map { |e| e.delete ',' }.map(&:to_i)
end.sort

usdata # => [[:AK, "Alaska", "Juneau", 1959, 1717854, 722718, 45600], [:AL, "Alabama", "Montgomery", 1819, 135765, 4802740, 174400], [:AR, "Arkansas", "Little Rock", 1836, 137002, 2937979, 105800], [:AZ, "Arizona", "Phoenix", 1912, 295254, 6482505, 261300], [:CA, "California", "Sacramento", 1850, 423970, 37691912, 1936400], [:CO, "Colorado", "Denver", 1876, 269837, 5116796, 259700], [:CT, "Connecticut", "Hartford", 1788, 14356, 3580709, 233400],   -- 中略--   [:UT, "Utah", "Salt Lake City", 1896, 219887, 2817222, 116900], [:VA, "Virginia", "Richmond", 1788, 110785, 8096604, 427700], [:VT, "Vermont", "Montpelier", 1791, 24923, 626431, 26400], [:WA, "Washington", "Olympia", 1889, 184827, 6830038, 351100], [:WI, "Wisconsin", "Madison", 1848, 169639, 5711767, 251400], [:WV, "West Virginia", "Charleston", 1863, 62755, 1855364, 66600], [:WY, "Wyoming", "Cheyenne", 1890, 253348, 568158, 38200]]

そしてこれらのデータを統合します。

usdata = usdata.zip(links).map { |data, links| data << links.last }

usdata # => [[:AK, "Alaska", "Juneau", 1959, 1717854, 722718, 45600, [:WA]], [:AL, "Alabama", "Montgomery", 1819, 135765, 4802740, 174400, [:MS, :FL, :GA, :TN]], [:AR, "Arkansas", "Little Rock", 1836, 137002, 2937979, 105800, [:MO, :TN, :MS, :LA, :TX, :OK]], [:AZ, "Arizona", "Phoenix", 1912, 295254, 6482505, 261300, [:CA, :NV, :UT, :NM, :CO]], [:CA, "California", "Sacramento", 1850, 423970, 37691912, 1936400, [:OR, :NV, :AZ]],  -- 中略 --  [:SD, "South Dakota", "Pierre", 1889, 199905, 824082, 39900, [:ND, :MN, :IA, :NE, :WY, :MT]], [:TN, "Tennessee", "Nashville", 1796, 109247, 6403353, 250300, [:KY, :VA, :NC, :GA, :AR, :MS, :AL, :MO]], [:TX, "Texas", "Austin", 1845, 696241, 25674681, 1207432, [:NM, :OK, :AR, :LA]], [:UT, "Utah", "Salt Lake City", 1896, 219887, 2817222, 116900, [:ID, :WY, :CO, :NM, :AZ, :NV]], [:VA, "Virginia", "Richmond", 1788, 110785, 8096604, 427700, [:WV, :KY, :TN, :NC, :MD]], [:VT, "Vermont", "Montpelier", 1791, 24923, 626431, 26400, [:NY, :MA, :NH]], [:WA, "Washington", "Olympia", 1889, 184827, 6830038, 351100, [:OR, :ID, :AK]], [:WI, "Wisconsin", "Madison", 1848, 169639, 5711767, 251400, [:MI, :IL, :IA, :MN]], [:WV, "West Virginia", "Charleston", 1863, 62755, 1855364, 66600, [:OH, :PA, :MD, :VA, :KY]], [:WY, "Wyoming", "Cheyenne", 1890, 253348, 568158, 38200, [:MT, :SD, :NE, :CO, :UT, :ID]]]

Gvizでグラフ化

さて下準備が整ったので、これらのデータをGvizを使ってグラフ化します。まずは隣接州情報で地形を作ります。

require "gviz"

gv = Gviz.new(:USA)
gv.graph do
  usdata.each do |id, name, cap, sthood, area, pop, gdp, link|
    route id => link
  end
end

gv.save(:usa, :png)

出力を見てみます。

usa noshadow

前回同様、ヒドイ結果です。

ここでもlayoutをneatoにして変化を見てみます。ラベルも変えます。

gv = Gviz.new(:USA)
gv.graph do
  global layout:'neato', overlap:false
  usdata.each do |id, name, cap, sthood, area, pop, gdp, link|
    route id => link
    node id, label: name
  end
end

gv.save(:usa, :png)

どうでしょう。

usa noshadow

一気に地図っぽくなりました。

州統合年のビジュアライズ

次に、各州の合衆国への統合年(statehood)をビジュアライズします。List of U.S. states by date of statehood - Wikipedia, the free encyclopediaにある区分に従って各州を9区分し、ノードの色の違いで表現します。blues9というカラーセットを使います。

まずは9区分をRangeのリストで表現します。

era = <<EOS.lines.map { |line| Range.new *line.split('-').map(&:to_i) }
  1776-1790
  1791-1799
  1800-1819
  1820-1839
  1840-1859
  1860-1879
  1880-1899
  1900-1950
  1950-1959
EOS

era # => [1776..1790, 1791..1799, 1800..1819, 1820..1839, 1840..1859, 1860..1879, 1880..1899, 1900..1950, 1950..1959]

これを使って各州の統合年を1〜9の何れかに割り当ててfillcolorにセットします。

gv = Gviz.new(:USA)
gv.graph do
  global layout:'neato', overlap:false
  nodes colorscheme:'blues9', style:'filled'

  usdata.each do |id, name, cap, sthood, area, pop, gdp, link|
    sthood = 9 - era.index { |r| r.include? sthood }
    route id => link
    node id, label: name, fillcolor:sthood
    node id, fontcolor:'white' if sthood > 7
  end
end

gv.save(:usa, :png)

結果を見てみます。

usa noshadow

キレイに色分けされました。色が薄くなるに連れて統合年が遅いということを表しています。

州面積のビジュアライズ

次に各州の面積データをビジュアライズします。合衆国の州面積は最小が3,140km2(Rhode Island)で 最大が1,717,854km2(Alaska)です。ビジュアライズのためにこれらを正規化する(一定の範囲の値に収める)必要があります。

minmax関数で対象データの最小最大範囲を取得し、Fixnum#normを定義してこれを正規化するようにします。

def minmax(data, idx)
  data.map{ |d| d[idx-1] }.minmax.tap { |minmax| break Range.new *minmax }
end

class Fixnum
  def norm(range, tgr=1..10)
    unit = (self - range.min) / (range.max - range.min).to_f
    (unit * (tgr.max - tgr.min) + tgr.min).round
  end
end

area_minmax = minmax(usdata, 5) # => 3140..1717854

295254.norm(area_minmax) # => 3

さてこれらのメソッドを使って各州の面積を1〜6の範囲に正規化し、これを使ってノードの幅を決定します。エッジの矢印も消します。

area_minmax = minmax(usdata, 5)

gv = Gviz.new(:USA)
gv.graph do
  global layout:'neato', overlap:false
  nodes colorscheme:'blues9', style:'filled', regular:true
  edges arrowhead:'none'
  
  usdata.each do |id, name, cap, sthood, area, pop, gdp, link|
    sthood = 9 - era.index { |r| r.include? sthood }
    area = area.norm(area_minmax, 1..6)
    route id => link
    node id, label: name, fillcolor:sthood, width:area, fontsize:14*area
    node id, fontcolor:'white' if sthood > 7
  end
end

gv.save(:usa, :png)

結果を見てみます。

usa noshadow

州面積に応じてノードのサイズが変わりました。

州人口のビジュアライズ

最後に州人口のデータをビジュアライズします。州人口をノードにおける多角形の辺の数で表現します。州面積と同様にデータ値を正規化し4〜12の何れかに収めます。ノードのshapeをpolygonとして各ノードのsidesをこの数値で決定します。つまり人口が多い州ほど辺数の多い多角形となります。

pop_minmax = minmax(usdata, 6)

gv = Gviz.new(:USA)
gv.graph do
  global layout:'neato', overlap:false
  nodes colorscheme:'blues9', style:'filled', shape:'polygon', regular:true
  edges arrowhead:'none'
  
  usdata.each do |id, name, cap, sthood, area, pop, gdp, link|
    sthood = 9 - era.index { |r| r.include? sthood }
    area = area.norm(area_minmax, 1..6)
    pop = pop.norm(pop_minmax, 4..12)
    route id => link
    node id, label: name, fillcolor:sthood, width:area, fontsize:14*area, sides:pop
    node id, fontcolor:'white' if sthood > 7
  end
end

gv.save(:usa, :png)

結果を見てみます。

usa noshadow

CaliforniaやTexasの人口が多いのが分かります。

仕上げ

Alaskaが大きすぎて全体のバランスが悪いのでこれを調整して完成とします。

us states noshadow

(このグラフはクリックすれば拡大できます)

いいですね!

このグラフからアメリカ合衆国に関し概ね以下のことが分かります。

  1. アメリカは東側から西側に向けて統合が進んだ。つまり東側のほうが歴史が古い。
  2. 西側のほうが州の区画面積が全体として広い。
  3. 東側は面積は小さいが人口が多い州が多く、西側はその逆である。

Gvizを使ってアメリカ合衆国における各州の統合年、面積および人口をビジュアライズする例を紹介しました。コード全体を以下に張っておきます。

melborne's gist: 3792505 — Gist


(追記:2012-9-30) Fixnum#normの実装が間違っていたので訂正しました。その結果添付のグラフの結果が一部間違っている点ご了承下さい。

(追記:2012-10-1) Fixnum#normの実装がまた間違ってましたm(__)m


ビューティフルビジュアライゼーション (THEORY/IN/PRACTICE) by Julie Steele and Noah Iliinsky



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS