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

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


全国の駅情報を提供する『駅データ.jp』という素晴らしいサイトがあります。無料でダウンロードできるCSV形式の駅データには各駅の管理鉄道会社や路線の情報だけでなく、駅の経度・緯度情報までもが含まれています。マコトニスバラシイ。イママデシラナカッタノガハズカシイ。

そんなわけで…

今回はGvizを使って、東京の地下鉄、すなわち東京メトロ+都営(東京都交通局)の路線図に挑戦してみます。

駅データの取得

まずは駅データを取得します。先のサイトのダウンロード頁からマスターデータ(m_station.csv)をDLします。サイトの仕様書頁にあるように、各駅情報は次の14フィールドで構成されています。

データ仕様

 1. 鉄道概要コード
 2. 路線コード
 3. 駅コード
 4. 路線並び順
 5. 駅並び順
 6. 駅グループコード
 7. 駅タイプ
 8. 鉄道概要名
 9. 路線名
10. 駅名
11. 都道府県コード
12. 経度
13. 緯度
14. 表示フラグ

地下鉄データの抽出

次にこのデータをRubyで読み出して、東京の地下鉄の駅情報だけを抽出します。ファイル名はmetro.rbとします。

#metro.rb

# encoding: UTF-8
require "csv"

header, *data = CSV.read('m_station.csv')

metrodata = data.select { |d| d[7].match(/東京メトロ|東京都交通局/) }
                .group_by { |d| d[8] }

metrodata # =>  {"東京メトロ銀座線"=>[["28", "28001", "2800101", "28001", "2800101", "2100201", "2", "東京メトロ", "東京メトロ銀座線", "浅草", "13", "139.797592", "35.710733", "1"], ["28", "28001", "2800102", "28001", "2800102", "2800102", "2", "東京メトロ", "東京メトロ銀座線", "田原町", "13", "139.790897", "35.709897", "1"], ... 中略 ... "日暮里・舎人ライナー"=>[["99", "99342", "9934201", "99305", "9934201", "1130218", "1", "東京都交通局", "日暮里・舎人ライナー", "日暮里", "13", "139.771287", "35.727908", "1"], ["99", "99342", "9934202", "99305", "9934202", "1130217", "1", "東京都交通局", "日暮里・舎人ライナー", "西日暮里", "13", "139.766857", "35.731954", "1"] ... ]}

地下鉄路線一覧

さて下準備が整ったので、Gvizで路線図を直ぐにでも描きたいところですが、感じをつかむためまずは路線一覧というのを作ってみます。

コードは以下のようになります。

# encoding: UTF-8
require "gviz"

Graph(:Metro) do
  global label:'Metro of Tokyo', size:16
  metrodata.each do |line, stations|
    subgraph do
      global label:line
      stlength = stations.length
      stations.each.with_index(1) do |st, i|
        st_id, st_name, st_seq = st.values_at(2, 9, 4)
        st_id = st_id.intern
        next_id = "#{st_seq.to_i+1}".intern

        edge [st_id, next_id].join('_') if i < stlength
        node st_id, label:st_name
      end
    end
  end
  save(:metro, :png)
end

ここではグラフの生成にGraphメソッド(ver.0.0.4で導入)というショートカットを使っています。これは、Gviz.new(:Metro).graphと等価です。

Graphメソッドのブロックでは、metrodataから順次駅情報を読み出し、edgeおよびnodeメソッドに必要な情報を渡してグラフ情報を生成しています。各路線毎の駅情報はsubgraphの中で読み出します。

出力を見てみます。

metro noshadow (クリックで拡大します)

簡単なコードでなかなか綺麗な一覧ができました。

色を付ける

でもやっぱり地下鉄グラフに色がないのは悲しすぎます。地下鉄のシンボルカラーを次のサイトから取得して、色を付けます。

地下鉄のシンボルカラー メトロカラー - Metro Color

足りないものは補って取得した色情報を配列で保持します。

colors = [["銀座線", "#f39700"], ["丸ノ内線", "#e60012"], ["日比谷線", "#9caeb7"], ["東西線", "#00a7db"], ["千代田線", "#009944"], ["有楽町線", "#d7c447"], ["半蔵門線", "#9b7cb6"], ["南北線", "#00ada9"], ["副都心線", "#bb641d"], ["浅草線", "#e85298"], ["三田線", "#0079c2"], ["新宿線", "#6cbb5a"], ["大江戸線", "#b6007a"], ["荒川線", "#7aaa16"], ["舎人ライナー", "#999999"]]

そしてグラフをカラー化します。

Graph(:Metro) do
  global label:'Metro of Tokyo', size:16
+ edges arrowhead:'none', penwidth:10
+ nodes style:'bold'
  metrodata.each do |line, stations|
    subgraph do
      global label:line
      stlength = stations.length
      stations.each.with_index(1) do |st, i|
        st_id, st_name, st_seq = st.values_at(2, 9, 4)
        st_id = st_id.intern
        next_id = "#{st_seq.to_i+1}".intern
+       color = (c = colors.detect { |ln, c| line.match /#{ln}/ }) ? c[1] : "#999999"

+       edge [st_id, next_id].join('_'), color:color if i < stlength
+       node st_id, label:st_name, color:color
      end
    end
  end
  save(:metro, :png)
end

結果を見てみます。

metro noshadow

(クリックで拡大します)

キレイです。

さて、実はこの路線情報には一部間違いがあります。そう、丸ノ内線の「中野坂上」で路線は分岐しなければいけません。

これに対応したコードを入れて、地下鉄路線一覧の完成です。

Graph(:Metro) do
  global label:'Metro of Tokyo', size:16
  edges arrowhead:'none', penwidth:10
  nodes style:'bold'
  metrodata.each do |line, stations|
    subgraph do
      global label:line
      stlength = stations.length
      stations.each.with_index(1) do |st, i|
        st_id, st_name, st_seq = st.values_at(2, 9, 4)
        st_id = st_id.intern
        next_id = "#{st_seq.to_i+1}".intern
        color = (c = colors.detect { |ln, c| line.match /#{ln}/ }) ? c[1] : "#999999"

        node st_id, label:st_name, color:color
+       case st_id
+       when :'2800220' # 中野坂上
+         edge [st_id, :'2800226'].join('_'), color:color # 中野坂上 => 中野新橋
+       when :'2800225' # 荻窪
+         next
+       end

        edge [st_id, next_id].join('_'), color:color if i < stlength
      end
    end
  end
  save(:metro, :png)
end

結果です。 metro noshadow

(クリックで拡大します)

いいですね!

地下鉄路線図

さあここからが本番です。先のコードを生かしつつ、各駅の経度・緯度情報を使って地下鉄路線図を作ります。

Graphvizの各ノードはposという属性を使ってその絶対座標を指定することができます。subgraphを外しlayoutをneatoとし、最初はダメ元で各駅の経度・緯度情報をそのままposに渡してみます。最後の!を忘れずに。

Graph(:Metro) do
+ global label:'Metro of Tokyo', size:16, layout:'neato'
  edges arrowhead:'none', penwidth:10
  nodes style:'bold'

  metrodata.each do |line, stations|
-   subgraph do
    stlength = stations.length
    stations.each.with_index(1) do |st, i|
      st_id, st_name, st_seq = st.values_at(2, 9, 4)
      st_id = st_id.intern
      next_id = "#{st_seq.to_i+1}".intern
      color = (c = colors.detect { |ln, c| line.match /#{ln}/ }) ? c[1] : "#999999"

+     pos_x = st[11]
+     pos_y = st[12]

+     node st_id, label:st_name, color:color, pos:"#{pos_x},#{pos_y}!"
      case st_id
      when :'2800220' # 中野坂上
        edge [st_id, :'2800226'].join('_'), color:color # 中野坂上 => 中野新橋
      when :'2800225' # 荻窪
        next
      end

      edge [st_id, next_id].join('_'), color:color if i < stlength
    end
-   end
  end
  save(:metro, :png)
end

結果は如何に!

metro noshadow

Onz…

甘くはありませんでした…

座標の正規化

つまり駅座標情報を正規化して出力サイズに合わせて調整する必要があります。

Gviz ver0.0.4では正規化のためにNumeric#normというメソッドを用意しました。このメソッドは、任意の範囲内の特定の数値を0.0〜1.0の範囲の対応位置にマッピングします。第1引数にその任意の範囲をRangeオブジェクトで渡します。また、第2引数に所定のRangeオブジェクトを与えると、正規化する範囲を0.0〜1.0以外にすることができます。

早速metrodataから緯度経度の最大最小値を取得してRangeオブジェクトを生成し、試してみます。

flatdata = metrodata.values.flatten(1)
lon_minmax = flatdata.map { |d| d[11].to_f }.minmax
lat_minmax = flatdata.map { |d| d[12].to_f }.minmax

lon_range = Range.new(*lon_minmax) # => 139.612434..139.958972
lat_range = Range.new(*lat_minmax) # => 35.586859..35.814544

139.812935.norm(lon_range) # => 0.5785830125412325
139.812935.norm(lon_range, 100..1000) # => 620.7247112871092
35.710702.norm(lat_range) # => 0.5439225245404847
35.710702.norm(lat_range, 100..1000) # => 589.5302720864363

さてこれを使って各駅の経度・緯度を正規化し、もう一度トライします。

Graph(:Metro) do
  global label:'Metro of Tokyo', size:16, layout:'neato'
  edges arrowhead:'none', penwidth:10
  nodes style:'bold'

  metrodata.each do |line, stations|
    stlength = stations.length
    stations.each.with_index(1) do |st, i|
      st_id, st_name, st_seq = st.values_at(2, 9, 4)
      st_id = st_id.intern
      next_id = "#{st_seq.to_i+1}".intern
      color = (c = colors.detect { |ln, c| line.match /#{ln}/ }) ? c[1] : "#999999"

+     pos_x = st[11].to_f.norm(lon_range, 1000..5000).round # 10..60 for svg
+     pos_y = st[12].to_f.norm(lat_range, 1000..5000).round # 10..60 for svg

      node st_id, label:st_name, color:color, pos:"#{pos_x},#{pos_y}!"
      case st_id
      when :'2800220' # 中野坂上
        edge [st_id, :'2800226'].join('_'), color:color # 中野坂上 => 中野新橋
      when :'2800225' # 荻窪
        next
      end

      edge [st_id, next_id].join('_'), color:color if i < stlength
    end
  end
  save(:metro, :svg)
end

さあどうだ!

metro noshadow (クリックでSVGによる路線図が開きます。手動で拡大してみて下さい)

スバラシイ!

なお上記正規化範囲はトライ&エラーで獲得します。pngではうまく行かず、範囲を10..60としてSVGでの出力が成功しました。

ノードを透過カラーで表現した別バージョンも用意してみます。

Graph(:Metro) do
  global label:'Metro of Tokyo', size:16, layout:'neato'
+ edges arrowhead:'none', penwidth:2
+ nodes style:'filled', fontcolor:'white'

  metrodata.each do |line, stations|
    stlength = stations.length
    stations.each.with_index(1) do |st, i|
      st_id, st_name, st_seq = st.values_at(2, 9, 4)
      st_id = st_id.intern
      next_id = "#{st_seq.to_i+1}".intern
      color = (c = colors.detect { |ln, c| line.match /#{ln}/ }) ? c[1] : "#999999"

      pos_x = st[11].to_f.norm(lon_range, 1000..5000).round # 10..60 for svg
      pos_y = st[12].to_f.norm(lat_range, 1000..5000).round # 10..60 for svg

+     node st_id, label:st_name, fillcolor:color+'aa', pos:"#{pos_x},#{pos_y}!"
      case st_id
      when :'2800220' # 中野坂上
        edge [st_id, :'2800226'].join('_'), color:color # 中野坂上 => 中野新橋
      when :'2800225' # 荻窪
        next
      end

      edge [st_id, next_id].join('_') if i < stlength
    end
  end
  save(:metro, :svg)
end

出力です。

metro noshadow

(クリックでSVGによる路線図が開きます。手動で拡大してみて下さい)

拡大すると駅の重なりがわかると思います。

Enjoy Metro Map with Gviz!

Gviz sample: Tokyo Metro with m_station data of 駅.jp — Gist


関連記事:

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

Ruby Graphvizラッパー「Gviz」でアメリカ合衆国をデータビジュアライズしよう!

東京の地下鉄の路線サインをGviz(Ruby Graphviz Wrapper)で描く


東京メトロだいすき (「鉄おも!」別冊)



blog comments powered by Disqus
ruby_pack8

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