2009年12月14日月曜日

Processing: 日本全国コンビニ店舗分布地図

このブログ記事をはてなブックマークに追加

「ビジュアライジング・データ Processingによる情報視覚化手法」を読んでみたのだが、Processingの情報処理とその視覚化がとても興味深いものだった。そこで、日本地図を使って何らかの情報を視覚化したいと考え、それならば全国に存在するコンビニ店舗の分布を調べてみようと思い、Processingで作ってみることにした。

因みに以前にも「Processingで分子動力学計算」や「Processingを使ってWebカメラを監視カメラにする」などのブログ記事を書いているように、プログラミング言語Processingはビジュアル関連で広範に応用でき、そのポテンシャルはとても大きい。

まずは、コンビニの店舗情報が必要なので、gooのコンビニ店舗検索を利用し、ここから住所情報を得ることにした。ただ、一度に検索できる件数は5,000件までなので5,000件以上存在するコンビニに対してはPythonで都道府県ごとに検索してマージした。データはHTMLで書かれているのでPythonのre.findallのパターンマッチを使って一気に必要なデータに変換した。

次に、Google MapsのHTTPリクエスト経由のジオコーディングを使って住所情報を経度・緯度に変換した。この辺もPythonを使えばちょちょいのちょいだ。以下のAPIを利用する。

http://maps.google.com/maps/geo?q=住所情報&output=json&sensor=false&key=APIキー

ただし、変換は1日につき15,000件の制限が掛かっているので4万件以上ある住所情報を取得するには3日かかることになる。もっともIPアドレスによる制限なので別のIPを使えば問題ないようだ。

日本地図上にデータを表示するためには、経度・緯度情報からXY座標に変換する必要がある。日本地図は国土地理院によるとユニバーサル横メルカトル(UTM)図法が使われているらしい。簡単に言うと世界地図でよく使われているメルカトル図法の赤道に合わせている中心線を6°ごとの経度線に合わせて作成する方法らしい。そのための変換関数をPythonで以下のように作成した。本当は縮尺の微調整が入るようなのだが、今回はそこまで厳密にする必要はないのでその辺は省いた。

def UTM(lat, lng): lng0 = 35 lat0_list = ((abs(lat - 129.0), 129.0, -3.0), (abs(lat - 135.0), 135.0, 0.0), (abs(lat - 141.0), 141.0, 3.0)) lat0 = sorted(lat0_list)[0][1] lat0_diff = sorted(lat0_list)[0][2] y = (lng - lng0) / 180.0 * math.pi x = math.log(math.tan(math.pi / 4.0 + (lat - lat0) / 180.0 * math.pi / 2.0)) + math.log(math.tan(math.pi / 4.0 + lat0_diff / 180.0 * math.pi / 2.0)) * 2.0 return x, y

以下に、作成したデータの一部を示しておく。データはタブ区切りで、先頭からコンビニID、店舗名、X座標、Y座標になっている。また、ネットワーク経由で経時的にデータを取得する場合、最初にデータ数を知る必要があるため先頭行に全データ数を記述している。

0 旭川豊岡4条5丁目 0.129276 0.152887 0 旭川旭神2条 0.128996 0.152486 0 愛別町 0.132287 0.155449 0 美深西1条 0.128267 0.165430 0 厚真町 0.119953 0.134899

作成した座標データを表示させるための日本地図だが、「カビパン男と私」というサイトの地図データを利用させてもらった。ここの静止画データをペイントソフトで少しだけ加工してできあがりだ。

さて、必要なデータは全て揃ったので、ここからProcessingを使っての視覚化作業になる。とは言っても、前述の「ビジュアライジング・データ」の6章「散布図」を参考に作成したのでそれほど苦労するところはなかった。詳しいことは書籍を参考にして欲しい。日本地図の画像と座標データのスケーリングについては、日本の東西南北の大ざっぱな位置から大体のスケールを求めてあとは手作業で修正した。

以下が作成したプログラムになる。ProcessingはJavaアプレットにエクスポートする機能があるので簡単にウェブに貼り付けることができる。プログラムの使い方だが、最初は全てのコンビニの店舗が表示されている。左上のコンビニ名をクリックすることで表示・非表示を設定できる。データ数が多いのでどうしても店舗が重なってしまうが、そのあたりは表示・非表示を切り替えて対応して欲しい。

This browser does not have a Java Plug-in.
Get the latest Java Plug-in here.



コンビニデータについて。使用したコンビニを店舗数を含めて以下に挙げておく。全ての店舗を合わせると44,447店舗となる。ただし、今回作成したデータで正しく位置情報を取得できなかったものについては省いて使用したので、実際に地図上に表示されているデータは若干少なくなっている。

セブンイレブン 12,056店舗 ローソン 8,580店舗 ファミリーマート 8,463店舗 サークルK 3,342店舗 サンクス 3,449店舗 デイリーヤマザキ 1,185店舗 ミニストップ 1,980店舗 am/pm 989店舗 ポプラ 706店舗 スリーエフ 680店舗 コミュニティ・ストア 153店舗 ココストア 257店舗 SHOP99 936店舗 セイコーマート 967店舗 セーブオン 540店舗

今回のプログラミングはなかなか楽しいものだった。コンビニ店舗情報を視覚化することで、セブンイレブンは店舗数が一番多いが四国や東北北部には店舗がなく都市型だとか、ローソンは地方にも比較的広がっているのだとか、店舗数が少ないコンビニは地域密着型なのだろうとか、いろいろ見えてくるのが面白い。Processingはもっと認知され使われてもいいのではないだろうか。

追記(2009/12/14):

今回はgooのコンビニ店舗検索に載っているコンビニのみを扱ったのだが、コメントで他のコンビニについても言及されたので、セイコーマートとセーブオンの2つのコンビニについて追加してみた。所在地情報はコンビニまっぷから取得した。

追記(2009/12/25):

地図の平行移動や拡大・縮小ができる高解像度インタラクティブ版を作成してみた。

最後に作成したソースコードを示しておく。

convenience.pde

String convData = "data.tsv.gz"; String japanMap = "japan.png"; int totalCount; Place[] places; int placeCount = 0; float minX = -0.130; float maxX = 0.243; float minY = -0.157; float maxY = 0.186; PImage mapImage; PFont font; String[] convName = { "Seven-Eleven", "Lawson", "Family Mart", "Circle K", "Sunkus", "Daily YAMAZAKI", "MINISTOP", "am/pm", "POPLAR", "Three F", "COMMUNITY STORE", "Cocostore", "SHOP99" }; color[] convColor = { #ff0000, #007fff, #00ff00, #ffff00, #ff00ff, #00ffff, #7f0000, #0000ff, #007f00, #7f7f00, #7f007f, #007f7f, #7f7f7f }; boolean[] convIds = { true, true, true, true, true, true, true, true, true, true, true, true, true }; int numConv = 13; public void setup() { mapImage = loadImage(japanMap); size(mapImage.width, mapImage.height, P3D); font = loadFont("CourierNewPSMT-14.vlw"); textMode(SCREEN); textFont(font); readData(); } public void draw() { image(mapImage, 0, 0); for (int i = 0; i < placeCount; i++) places[i].draw(); for (int i = 0; i < numConv; i++) { fill(convColor[i]); text(convName[i], 15, 16 * (i + 1)); if (convIds[i]) text("*", 5, 16 * (i + 1)); } } float TX(float x) { return map(x, minX, maxX, 0, width); } float TY(float y) { return map(y, minY, maxY, height, 0); } void mousePressed() { for (int i = 0; i < numConv; i++) if (mouseX < convName[i].length() * 8 + 15 && mouseY > 16 * i && mouseY < 16 * (i + 1)) convIds[i] = !convIds[i]; } void readData() { new Slurper(); } Place parsePlace(String line) { String pieces[] = split(line, '\t'); int id = int(pieces[0]); String name = pieces[1]; float x = float(pieces[2]); float y = float(pieces[3]); return new Place(id, name, x, y); }

Place.pde

class Place { int id; String name; float x, y; public Place(int id, String name, float x, float y) { this.id = id; this.name = name; this.x = x; this.y = y; } void draw() { if (convIds[this.id]) { int xx = (int)TX(x); int yy = (int)TY(y); set(xx, yy, convColor[this.id]); } } }

Slurper.pde

class Slurper implements Runnable { Slurper() { Thread thread = new Thread(this); thread.start(); } public void run() { try { BufferedReader reader = createReader(convData); String line = reader.readLine(); totalCount = int(line); places = new Place[totalCount]; while ((line = reader.readLine()) != null) { places[placeCount] = parsePlace(line); placeCount++; } } catch (IOException e) { e.printStackTrace(); } } }

4 コメント:

匿名 さんのコメント...

これを見て佐渡にはコンビにないんだなって思ってぐぐったらセーブオンというコンビニならあることがわかりました。

参考になりました。

nox さんのコメント...

コメントありがとうございます。

今回はgooのコンビニ店舗検索に載っているコンビニのみを使ったのでセーブオンは入っていませんでした。

ただ、セーブオンも店舗数がそれなりにありますし、他にセイコーマートなども店舗数の多いコンビニみたいなので、別サイトから所在地情報を取得して、この2つについても追加してみました。

匿名 さんのコメント...

先日コメントしたものです。

レスポンスありがとうございます。
各コンビニの経営戦略のようなものが見えてとてもおもしろいです。

匿名 さんのコメント...

こんにちは。
経度・緯度情報からXY座標に変換する関数なんですが、計算式は何を参照されましたでしょうか。
きっちり計算する式は検索でたまに出るのですが、このくらい簡易的に計算を行いたいのです。
もし可能でしたらお教えください。