社会ノマド

備忘録と書きもの練習帳。とくに何らかのハンドリング系と、雑多な話題に関する読書録になるかなと思います。

ホテル街を見つける(2)RとGoogleAPIでジオコーディング

前回に続いて、ホテル街を見つける技術編第二弾。前回は住所一覧のベクトルをcsvファイルにしまうところまでやりました。

zawazawalong.hatenablog.com

今回は住所一覧を元に緯度経度をゲットする ジオコーディング のやり方について。方法としてはRからGoogle APIを利用します。

流れ

  1. 検索サイトからスクレイピング(R)
  2. ジオコーディング(R)◀今回!
  3. 地図上にプロット(QGIS

今回の最終目標は以下の?を埋めたcsvファイルを吐き出すところまで。

住所 緯度 経度
北海道なんたら1
北海道なんたら2
北海道なんたら3

参照

Rで(逆)ジオコーディング

ジオコーディングをやってみる

ジオコーディングとは、住所を緯度・経度の座標値に変換すること。住所のままだとプロットの都合上塩梅が悪いので、緯度経度のセットに変換したい。例えば、東京駅の住所がわかっていたとして、わからない緯度と経度をひっぱってきたい。

住所 緯度 経度
東京都千代田区丸の内一丁目

何はなくともジオコーディングしてみよう(ここは先の参照サイト)のコードをほぼそのままお借りしてます)。

# ライブラリ読み込み
library(rjson)
library(RCurl)

# 住所
hoge <- "東京都千代田区丸の内一丁目"

# 欲しい住所を指定したURLを作成
url <- paste0("http://maps.googleapis.com/maps/api/geocode/json?address=", hoge, "&sensor=false")

# URLを適切にエンコード
url_utf8 <- URLencode(iconv(url, "", "UTF-8"))

# URLからJSONデータを取得し、リスト形式に変換
getJSON <- fromJSON(getURL(url_utf8))

# リストの中から緯度と経度を引っ張りだす
getJSON$results[[1]]$geometry$location

結果として以下の様に緯度と経度を獲得することができる。

住所 緯度 経度
東京都千代田区丸の内一丁目 35.68332 139.7674

paste0()は引数同士の間に何も挟まず、文字列を結合する(参照)。incov()は第1引数に与えた文字列を、第2引数のコードから第3引数のコードへ変換する(参照)。ここではUTF-8へと変換している。さらにURLencode()でURLエンコードへ変更する。よくわからないが、URLに入っていてはまずい文字を変換するっぽい(参照)。

ホテルの緯度・経度を獲得する

前回の記事で以下のように住所一覧を得て address.csv にしまった。今回はこの緯度と経度の?をすべて埋めたcsvファイルを作ることが目標である。

住所 緯度 経度
北海道なんたら1
北海道なんたら2
北海道なんたら3

やることは至って単純で、先ほどの「東京都千代田区丸の内一丁目」(= hoge)のところへ、順番に住所を投げ込んでいけば良い。乗っかかりっきりですが、Rで(逆)ジオコーディングさんのコードのマイナーチェンジで挑みます(感謝)。

# データの読み込み
text <- read.csv("address.csv",header=T,stringsAsFactors = FALSE)
text <- text$x

# ライブラリ読み込み
library(RCurl)
library(rjson)
library(RgoogleMaps)

# 関数作成
GEO_new <- function(address, para = list(sensor = "false", language = "ja", region = "JP"), proxy = NULL) {
  curl_set <- getCurlHandle()
  if(!is.null(proxy)) curlSetOpt(.opts = list(proxy = proxy), curl = curl_set)
  parameters <- paste0("&", names(para), "=", unname(unlist(para)), collapse = "")
  GeoRequest <- iconv(paste0("http://maps.googleapis.com/maps/api/geocode/json?address=", address, parameters), "", "UTF-8")
  geodata <- vector("list", length(address))
  for(i in seq_along(address)) {
    getJSON <- fromJSON(getURL(URLencode(GeoRequest[i]), curl = curl_set))
    if (length(getJSON$results)!=0){
        geodata[[i]] <- as.data.frame(getJSON$results[[1]]$geometry$location)
        Sys.sleep(0.5)
        print(paste(i,"=================="))}
    else {
      geodata[[i]] <- NA
      Sys.sleep(0.5)
      print(paste(i,"失敗した…"))}}
  cbind(address, do.call("rbind", geodata))
}

# ジオコーディング:3回に分けて3つのセットにしまう(ちなみにうまくいかないと思うので下記参照)
location0001_2000 <- GEO_new(text[1:2000])
location2001_4000 <- GEO_new(text[2001:4000])
location4001_6088 <- GEO_new(text[4001:6088])

# 縦につないで一つのデータセットに
location <- rbind(location0001_2000,location2001_4000,location4001_6088)

# 書き出し
write.csv(location, "location.csv", quote=FALSE, row.names=FALSE)

大きな流れは、データを読み込んで text ベクトルに住所を入れ込む。関数 GEO_new()を定義する。2000ずつジオコーディングした住所をしまいこんで、縦にくっつける。csvで書き出す。

変更点は以下の3点。第1にエラーの処理した。第2に現在の進行状況の確認と住所の獲得に失敗したかどうかの表示した。第3にAPIのリクエスト間に時間を設けた。重要な注意点もあるので順に説明する。

① エラーを吐いても止めない

当初のコードだと途中で エラーを吐いた場合止まる 。該当住所がないのか何なのか、多くはないが住所の獲得に失敗しエラーを吐いてしまった。そこでfor文の中にif文をはさみ、エラーを吐いた場合は該当箇所にNAを入れこむ仕様にした。これで止まらない。中身としては先に述べたように、getJSONにリストが入る仕様になっているのだが、失敗するとgetJSON$resultslist()が入ってしまって[[1]]にアクセスできずエラーが起きる。これを防いでいる。

② 何ケースやったか/どこで失敗したか

print()を挟み込んで 何ケース進んだかを表示 するようにした。また、 失敗した時は「失敗した…」と表示 するようにした。そのため、どのケースがNAになっているのかとりあえずわかる。

APIリクエスト制限

リクエストを過剰に送るとgoogle側からstopが入りやはりエラーする 。具体的には、

IPあたり24時間に2500リクエスト、秒間5リクエスト

参照 意外に多いGoogle Maps関連機能制限をまとめたGoogle Geocoding APIの制約に関するよくある誤解

そこで、まず2500件制限に対策すべく上記のように 2000件ずつのアクセス にしている。上記のコードを一度に回すとおそらくGEO_new(text[2001:4000])が500辺り以降ですべてエラーになるためNAが入ってしまう。3日間に分けて獲得するか、他のIPアドレスを用いて獲得しよう。yahooのサービスは制限がないためそちらを使っても良い、みたいな話もある。

次に秒間リクエストだが、Sys.sleep(0.5)アクセスごとに0.5秒止める ことにした。理論上は0.2でもいいはずなのだが、時々失敗するので試行錯誤の末0.5とした。この辺りはちょっといじったほうがいいかもしれない。

まとめ

以上で以って以下の様な location.csv ファイルがディレクトリにできたはず。やったね!

address lat lng
北海道なんたら1 xx.xxxxx xx.xxxxx
北海道なんたら2 xx.xxxxx xx.xxxxx
北海道なんたら3 NA NA
xx.xxxxx xx.xxxxx

次回はいよいよこのデータを使ってQGISで地図上へのプロット、および「GISデータを用いた分析」っぽい分析を少し行います。QGISのダウンロードはあちこちに良い解説があるのでそちらを参照してください。ではでは。

人口・駅データをプロット

目的はQGISを使って地図上に人口および駅をプロットすること。いろいろな分析の基礎になりますからやっておいて損はなさそうですね。QGISはインストール済みを想定しています。

QGISはレイヤを重ねていくことで表示するので順番に3つを重ねていきます。

  1. 日本地図
  2. 人口
  3. 駅・路線図

日本地図

国土数値情報の行政区域からshapeファイルをゲット。

レイヤ > レイヤの追加 > ベクタレイヤの追加 > ブラウズ

で該当する.shpを開けばこんな感じになります。ひとまず日本地図のプロットに成功! f:id:zawazawalong:20160115184619p:plain

人口:国勢調査データ

つづいて人口について。ここ参照です。国勢調査のデータを用います。流れは以下。

  1. 国勢調査データのダウンロード
  2. QGISへの読み込みとひも付け
  3. 人口で塗り分け

国勢調査データのダウンロード

e-Statから、国勢調査500mメッシュを取得。自分の欲しい該当コードに当たるデータをダウンロード。今回は東京を落としてみます。番号は5339。

shape形式をダウンロード。すると、「H002005112009DDSWH05339」にはshapeデータとidが、「tblT000609H53390」の方には.txt形式で国勢調査データが入っている。このとき.txtファイルの拡張子を.csvに書き換えておく。

読み込みとひも付け

読み込み

shapeファイル、csvをそれぞれベクターデータで読み込む。これで各地域を500m単位で四角く区切ったメッシュデータ(shapeファイル)と、各四角区域に何人がいるのか(csvファイル)がそれぞれ読み込まれた。

ひも付け

つづいて、各四角(shape)と人口(csv)を紐付ける必要がある。

メッシュの方(MESH5339)を右クリック。

プロパティ > 結合 > +

結合するレイヤに「tblT000609H53390」、結合するフィールド・ターゲットフィールドに「KEY_CODE」を選択してひも付け完了。MESH5339を右クリック、属性テーブルを開いてやると紐付いているはず。

人口の数値化

とここで注意なのが、人口データがstring形式になっている…。stringは文字なので塗り分けのときに数値として認識してくれない!ちょっとうまいやり方がわからないので、新しい数値の変数を作成し、そこに結合した人口を代入してやることにする。

MESH5339を右クリック、属性テーブルを開く。左上の鉛筆のアイコン「編集モード切替」を押す。続いて「新規カラムを作る」を押す。名前に「people」とかつけて、 タイプに「integer」を選ぶ (このために変数を作ってるのでココ重要)。

これで変数ができるはず。上にあるボックスで左から「people」を選び、右側に「tblT000609H53390_T000609001」(要するにひも付けた最初の列)を選んで右にある「全部更新」のボタン。暫し待つ(反映までちょっと時間かかる)。最後に左上の鉛筆のアイコン「編集モード切替」を押して編集モードを修了する。

人口で塗り分け

いよいよMESH5339を右クリック、オプションでスタイル。「段階にわけられた」を選んでカラム「people」を選ぶ。色階調を適当に選んで、分類数とか適当にいじり分類ボタン。OKをおすとこうなります(やったね!)。f:id:zawazawalong:20160115190800p:plain

駅・路線図:国土数値情報

駅と路線図をプロット

最後に駅と路線図を重ねる。国土数値情報で、鉄道を押して流れに任せてダウンロード。駅と路線を先ほどと同じ要領でベクターで読み込み。

駅のデータと路線のラインがそれぞれ別のデータで得られるのでレイヤーでチェックを入れて重ねる。

駅名を表示

Station 右クリック > オプション > ラベル

ラベルにチェックを入れて「N02_005」を選んでOKを押すと駅名も表示されます。結果が以下!

f:id:zawazawalong:20160115191411p:plain

あとは人口の人数区切りを変えたり、地域を変えたり、路線等々の色を変えたりは個人の好みで。QGISはプロットがきれいやね。

QGISでとにかく表示してみる

なんは無くとも動かしたい!!ということで、QGISはインストールされていることを前提に、以下を参照して表示までの手順。

QGIS初級編 さわってみようQGIS

データのダウンロード

まず、国土地理院から以下のデータをダウンロード。

  • 「第1.1版ラスタ(2006年公開)」の標高、TIFF
  • 「第2.1版ベクタ(2015年公開)」の全レイヤshape

QGISで表示

  • 「ラスタレイヤの追加」で「el.tif」ファイルを選択。フィルタに4326、WGS84を指定すると表示される。
  • ベクタレイヤの追加」で「.shp」ファイルを選択。

f:id:zawazawalong:20151229153650p:plain

過去50ツイート時間をリプで返す

リプライで「おい」と飛んできたら、飛ばしてきたアカウントの過去50ツイートのツイート時間を返すbotの作成。下記の機能をtwitterに実装したものです。

こんな感じを目指します。 f:id:zawazawalong:20151210160934p:plain

#!/usr/local/bin/ruby -Ku

require 'kconv' 
require 'twitter'

client = Twitter::REST::Client.new do |config|
  config.consumer_key = "なんとか"
  config.consumer_secret = "かんとか"
  config.access_token        = "うんとか"
  config.access_token_secret = "すんとか"
end


recentid = ツイート取得id初期値 #client.mentions_timeline[0].id とかで得られます

loop{
  ripu = client.mentions_timeline(:since_id => recentid)  #リプライを取得
  if ripu.size != 0
      for i in 0..(ripu.size-1)
        if ripu[i].text.index(/おい/).class==Fixnum  #「おい」を含むか判定
            user_id = ripu[i].user.id
    
            #カウント配列に0をセット
            tweet_counts = []
            (0..23).each { |j| tweet_counts[j] = 0 }
    
            #順番に該当時をカウント(本当は50より大きくしたいけどこれ以上にすると140字を超える)
            client.user_timeline(user_id, { count: 50 } ).each do |tweet|
                tweet_counts[tweet.created_at.hour] += 1
            end
            
            #リプ内容を作る(#のヒストグラム)
            hoge = ""
            (0..23).each do |j|
              hoge = hoge + "%02d"% j+"#" * tweet_counts[j] + "\n" 
            end
    
            post = "@#{ripu[i].user.screen_name} \n#{hoge}"
    
            twi_id=ripu[i].id
            client.update(post, {:in_reply_to_status_id => twi_id })
        end
      end
      
    # recentidの更新
    recentid = ripu[0].id
  end
  
  # 60秒ごとに稼働させる 
  sleep(60)
}

bot を heroku で動かす

Rubybot を作ったはいいが crontab は PC が起きてないと動かない。そこでサーバー上で bot を動かすことを試みる。

上記を参照して同じフォルダに5つのファイルを準備し、ターミナルにて以下のプログラムを走らせる。ブラウザでherokuへログインしてbotのスイッチをオンすれば完了。

  • Procfile
  • Gemfile
  • app.rb
  • config.ru
  • botfile.rb(自分のプログラム)
git init

heroku create botfile
bundle install
git add .
git commit -m'commit'

git push heroku master

heroku open

heroku上のアプリを削除

失敗したら以下のコマンドで消してやり直そう。

heroku apps:destroy --app アプリ名 --confirm アプリ名

注意すべきポイント

何度も heroku へのデプロイを試みている際、フォルダ内の「Gemfile.lock」ファイルは毎度消してから bundle を行うこと。

Stataでシークエンス分析

Sequence Analysis のやり方を日本語で紹介したものがみあたらないので、Stataのパッケージマニュアルを参考に、紹介してみる。

Sequence Analysisとは?

Seaquence Analysisとは、並び順のパターンを発見する分析である。つまり、

11111

11311

11122

77777

のように4つの並び順があるとすると、上二つはかなり似ていて、四つ目はかなり違う。三番目はそこそこ似ている。みたいなのを発見する分析。手順としては以下の二段階を踏む。

  1. 配列同士の距離を計算する
  2. 距離からクラスター分析でいくつかのパターンに分類する

今回はStataを用いてSAを行う。

データの構築とパッケージの導入

データの構築

まずはパッケージの導入からデータの加工まで。とりあえず、定型データ(全てのサンプルが同じ長さ)形式にする^1。つまり、

11111

11311

221

のように3つ目だけ短い、みたいなのを避けて全部同じ長さのデータを作る。ひとまず、3つ目の空白に9とか代入して以下の形へ。

11111

11311

22199

パッケージの導入

なんはなくともパッケージの導入。

*** パッケージ導入
ssc install sq

データはロング型で与える。つまり、以下みたいな形でズラーッと長く^2

ID order みたい変数
1 1 1
1 2 1
1 3 1
1 4 1
1 5 1
2 1 1
2 2 1
2 3 3

つづいて、配列が入ってる変数と、各配列のid、配列の順序orderを指定して、sqsetの形を指定する(xtsetやstsetみたいにidや順序を指定したデータセット)。

*** データ形式指定
sqset みたい変数 id order, ltrim

分析

基礎分析

まずは基礎的な分析を行う。いわゆる記述統計的な分析。

関数 意味
sqdes 全要素をばらしたときの頻度を表しているはず^3
sqtab 出現配列を出力。オプションは、rankは上位のみを出力。soは例えば11112と12222を同じものとしてカウント。seは12121と11112を同じものとしてカウント。
sqlength 配列の長さ 12121であれば5である。elementオプションはおそらく長さを数える単位どの要素について集計するか。
sqelemcount 配列内で異なる要素の数 11111であれば1、12121であれば2。
sqepicount 異なるエピソードの数。つまり数値の変化が何回起こったか-1 11111であれば1、12121であれば5。
*** 要素の頻度(長さ1の要素の頻度)
sqdes

*** 出現配列一覧
sqtab

* top10のみ表示
sqtab,ranks(1/10) 
* 長さを考慮せずどういう順列か( same order)
sqtab,ranks(1/10) so
* パターン構成要素が同じ配列の頻度(順不同でいかなる要素によって構成されるか same elements)
sqtab,ranks(1/10) se


* シークエンスの長さ
egen length = sqlength()
egen length1 = sqlength(),element(2)
* 異なる要素の数
egen elemnum = sqelemcount()
* エピソード単位のカウント
egen epinum = sqepicount()
* リスト表示で変数一覧
sqstatlist
* 記述統計
sqstatsum
* 1変数
sqstattab1
* クロス表
sqstattab2

図示

シークエンス分析の強みは何と言っても図示。

*** Sequence index plots
sqindexplot

シークエンス分析

以降では、いよいよ配列間の距離を計算しクラスタリングする。ここで重要なのは以下の点。

距離を求める

まず、配列間の距離は Levenshtein距離 を用いる。Levenshtein距離は、 indel-costsubstitution cost の2つの合計である。indel-cost(insert delete)とは、配列のある要素を挿入するないし、消すことである。例えば1231→131(消去)や、1231→12231(挿入)を指す。一方 substitution cost はある要素を他の要素に変更することである。例えば1231→1241(3を4に変更)。これらの操作それぞれにコストを決め(例えばindel-costを1、substitution costを2)、「コスト × 行われた手順」が Levenshtein距離となる。

ただし、これらの手順が一通りとは限らない。そこで、最小距離を求めるアルゴリズムが Needleman-Wunsch アルゴリズムである。

まずsqomで距離を求める。全ての距離を計算するオプションがfull。結構時間がかかる。名前を指定しなければSQDistというマトリックスに距離行列がしまわれる。

よくわからないが、データの形式上 sqclusterdat をして特殊な画面?に移動する。そこでクラスター分析を行い、sqclusterdat,returnで元の画面に戻って来る。クラスター分析の際、何クラスで割り当てるかはgroupの中で指定する。indexplotをクラスターごとに出力したところで今日はここまで。

*** 距離を計算(OM)
sqom, full

*** クラスター分析
sqclusterdat

* 階層的クラスター分析(今回はウォード法で行う)
* クラスターを計算
clustermat wardslinkage SQdist, name(wards) add
* デンドログラムの出力
cluster tree wards, cutnumber(20)

* カテゴリー値の割り当て
cluster generate cl3=group(3), name(wards)
cluster generate cl4=group(4), name(wards)
cluster generate cl5=group(5), name(wards)
cluster generate cl6=group(6), name(wards)

sqclusterdat, return

sqindexplot, by(cl3)

doファイル実行で文字化け

Stata14では日本語問題がだいぶ解決されたようだが、13を使っているためStataはどうにも日本語に弱い。以下のように .do ファイルを実行したら日本語が文字化け。ラベリングなどが台無しになったので対策。

do /Users/なんとかかんとか/hirakitaifile.do

ここによると、日本語はUTFでエンコードしてたらダメで、Shift_JISにする必要があるらしい。そこでテキストエディタ「mi」でdoファイルを開いて、テキストエンコーディングShift_JISに変更し保存。再度ファイルを実行したら万事解決。