Ruby Graphvizラッパー「Gviz」でアメリカ合衆国をデータビジュアライズしよう!
(追記:2014-3-3) Gvizについてのまとめ頁を作りました。
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)
出力を見てみます。
前回同様、ヒドイ結果です。
ここでも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)
どうでしょう。
一気に地図っぽくなりました。
州統合年のビジュアライズ
次に、各州の合衆国への統合年(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)
結果を見てみます。
キレイに色分けされました。色が薄くなるに連れて統合年が遅いということを表しています。
州面積のビジュアライズ
次に各州の面積データをビジュアライズします。合衆国の州面積は最小が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)
結果を見てみます。
州面積に応じてノードのサイズが変わりました。
州人口のビジュアライズ
最後に州人口のデータをビジュアライズします。州人口をノードにおける多角形の辺の数で表現します。州面積と同様にデータ値を正規化し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)
結果を見てみます。
CaliforniaやTexasの人口が多いのが分かります。
仕上げ
Alaskaが大きすぎて全体のバランスが悪いのでこれを調整して完成とします。
(このグラフはクリックすれば拡大できます)
いいですね!
このグラフからアメリカ合衆国に関し概ね以下のことが分かります。
- アメリカは東側から西側に向けて統合が進んだ。つまり東側のほうが歴史が古い。
- 西側のほうが州の区画面積が全体として広い。
- 東側は面積は小さいが人口が多い州が多く、西側はその逆である。
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