読者です 読者をやめる 読者になる 読者になる

e-Stat APIをRから使う

R httr e-stat

ほぼほぼこの記事に書いてある内容なんですけど、httrでやってみたのでメモ。

e-Stat APIとは

政府統計の総合窓口(e-Stat)で提供している統計データを入手できるAPIです。ユーザ登録してAPIキーを生成すると使えます。

今のところ、以下の種類のAPIが用意されています。形式はXMLJSONJSONPがありますが、今回はJSON形式のでやってみるので、jsonが返ってくるリクエストURLを書き添えています。

  • 統計表情報取得(getStatsList):統計表の検索
  • メタ情報取得(getMetaInfo):統計表のメタ情報を取得
  • 統計データ取得(getStatsData):統計表のデータを取得
  • データセット登録(postDataset):統計データの取得条件を登録しておける
  • データセット参照(refDataset):登録されている取得条件を参照
  • データカタログ情報取得(getDataCatalog):統計表ファイルと統計データベースの検索

あんまりよく理解できてないんですが、

  1. 「統計表情報取得」で関係ありそうな統計表を検索
  2. データを取得する統計表を選ぶ
  3. 「メタ情報取得」で統計表のメタ情報を取得(絞り込みに使う)
  4. 3.で取得した情報を絞り込み条件にして「統計データ取得」で実際のデータを取得

という感じの流れになるみたいです。絞り込みは、しなくてもいいんですけどした方が取ってくるデータが少ないので早かったです。

(詳しいAPIのパラメータは公式ドキュメントを参照してください:政府統計の総合窓口(e-Stat)のAPI 仕様 | 政府統計の総合窓口(e-Stat)−API機能

統計表情報取得

例えば、「チョコレート」に関する統計表を検索するときはこんな感じです。

library(httr)
library(dplyr)

# APIキー
key <- "XXXXXX"

res <- GET(
  "http://api.e-stat.go.jp/", path = "rest/2.0/app/json/getStatsList",
  add_headers(`Accept-Encoding` = "gzip"),
  query = list(
    appId      = key,
    searchWord = "チョコレート"
  ))

返ってくる結果はこんな感じになります。入り組んだ構造になっていますが、TABLE_INFというやつが探している結果です。

j <- content(res)

str(j, max.level = 3)
#> List of 1
#>  $ GET_STATS_LIST:List of 3
#>   ..$ RESULT      :List of 3
#>   .. ..$ STATUS   : int 0
#>   .. ..$ ERROR_MSG: chr "正常に終了しました。"
#>   .. ..$ DATE     : chr "2015-10-28T19:33:32.509+09:00"
#>   ..$ PARAMETER   :List of 3
#>   .. ..$ LANG       : chr "J"
#>   .. ..$ SEARCH_WORD: chr "チョコレート"
#>   .. ..$ DATA_FORMAT: chr "J"
#>   ..$ DATALIST_INF:List of 3
#>   .. ..$ NUMBER    : int 166
#>   .. ..$ RESULT_INF:List of 2
#>   .. ..$ TABLE_INF :List of 166
#>   .. .. .. [list output truncated]

要素をひとつ覗いてみると、一階層ネストした要素があります。これをなんとかしないといけないので、purrrflatten()という関数を使います。(たぶんjsonliteflatten()とか、rlistlist.flatten()も同じ)

str(j$GET_STATS_LIST$DATALIST_INF$TABLE_INF[[1]], list.len = 6)
#> List of 13
#>  $ @id                 : chr "0000100087"
#>  $ STAT_NAME           :List of 2
#>   ..$ @code: chr "00200572"
#>   ..$ $    : chr "全国物価統計調査"
#>  $ GOV_ORG             :List of 2
#>   ..$ @code: chr "00200"
#>   ..$ $    : chr "総務省"
#>  $ STATISTICS_NAME     : chr "平成9年全国物価統計調査 大規模店舗編"
#>  $ TITLE               :List of 2
#>   ..$ @no: chr "009"
#>   ..$ $  : chr "価格分布 品目・銘柄(307),集計価格数・平均・四分位等における価格・標準偏差,全国・都市階級(10)・都道府県(47)・県内"| __truncated__
#>  $ CYCLE               : chr "-"
#>   [list output truncated]

library(purrr)

str(flatten(j$GET_STATS_LIST$DATALIST_INF$TABLE_INF[[1]]), list.len = 6)
#> List of 18
#>  $ @id                 : chr "0000100087"
#>  $ STAT_NAME.@code     : chr "00200572"
#>  $ STAT_NAME.$         : chr "全国物価統計調査"
#>  $ GOV_ORG.@code       : chr "00200"
#>  $ GOV_ORG.$           : chr "総務省"
#>  $ STATISTICS_NAME     : chr "平成9年全国物価統計調査 大規模店舗編"

ネストされていた要素がフラットになりました。これでbind_rows()すればdata.frameにできそうです。

ところが、やってみるとエラーが出ます。。

lapply(j$GET_STATS_LIST$DATALIST_INF$TABLE_INF, flatten) %>%
  bind_rows
#> Error: incompatible type (data index: 12, column: 'SURVEY_DATE', was collecting: integer (dplyr::Collecter_Impl<13>), incompatible with data of type: character

なぜかというと"SURVEY_DATE"というカラムに文字列と数値が入り混じっているからです。

j$GET_STATS_LIST$DATALIST_INF$TABLE_INF[11:13] %>% map("SURVEY_DATE")
#> [[1]]
#> [1] 200211
#> 
#> [[2]]
#> [1] "201201-201212"
#> 
#> [[3]]
#> [1] 0

この問題を避けるために、flatten()したあとにas.character()を適用してすべてcharacterになるようにします。

as_flattened_character <- function(x) lapply(flatten(x), as.character)

lapply(j$GET_STATS_LIST$DATALIST_INF$TABLE_INF, as_flattened_character)
  %>% bind_rows
#> Source: local data frame [166 x 19]
#> 
#>           @id STAT_NAME.@code      STAT_NAME.$ GOV_ORG.@code GOV_ORG.$                                STATISTICS_NAME TITLE.@no
#>         (chr)           (chr)            (chr)         (chr)     (chr)                                          (chr)     (chr)
#> 1  0000100087        00200572 全国物価統計調査         00200    総務省           平成9年全国物価統計調査 大規模店舗編       009
#> 2  0000100104        00200572 全国物価統計調査         00200    総務省           平成9年全国物価統計調査 小規模店舗編       009
#> 3  0000100125        00200572 全国物価統計調査         00200    総務省 平成9年全国物価統計調査 消費者物価地域差指数編       007
#> 4  0000100126        00200572 全国物価統計調査         00200    総務省 平成9年全国物価統計調査 消費者物価地域差指数編       008
#> 5  0000100127        00200572 全国物価統計調査         00200    総務省 平成9年全国物価統計調査 消費者物価地域差指数編       009
#> 6  0000100128        00200572 全国物価統計調査         00200    総務省 平成9年全国物価統計調査 消費者物価地域差指数編       010
#> 7  0000100129        00200572 全国物価統計調査         00200    総務省 平成9年全国物価統計調査 消費者物価地域差指数編       011
#> 8  0000100130        00200572 全国物価統計調査         00200    総務省 平成9年全国物価統計調査 消費者物価地域差指数編       012
#> 9  0000100146        00200572 全国物価統計調査         00200    総務省          平成14年全国物価統計調査 大規模店舗編       009
#> 10 0000100162        00200572 全国物価統計調査         00200    総務省          平成14年全国物価統計調査 小規模店舗編       009
#> ..        ...             ...              ...           ...       ...                                            ...       ...
#> Variables not shown: TITLE.$ (chr), CYCLE (chr), SURVEY_DATE (chr), OPEN_DATE (chr), SMALL_AREA (chr), MAIN_CATEGORY.@code (chr),
#>   MAIN_CATEGORY.$ (chr), SUB_CATEGORY.@code (chr), SUB_CATEGORY.$ (chr), OVERALL_TOTAL_NUMBER (chr), UPDATED_DATE (chr), TITLE (chr)

横幅が長いので、見たいカラムだけをサブセットするか、RStudioならView()で見ると見やすいと思います。

lapply(j$GET_STATS_LIST$DATALIST_INF$TABLE_INF, as_flattened_character) %>%
  bind_rows %>%
  View

f:id:yutannihilation:20151028195329p:plain

この表の@idが統計表のIDです。調べる統計表が決まったら、今度はそれを使ってメタ情報を取得します。

メタ情報取得

先ほどの@idstatsDataIdに指定します。

res_meta <- GET(
  "http://api.e-stat.go.jp/", path = "rest/2.0/app/json/getMetaInfo",
  add_headers(`Accept-Encoding` = "gzip"),
  query = list(
    appId      = key,
    statsDataId = "0003103532"
  ))

返ってくる結果はこんな感じです。またもや入り組んでてわかりにくいですが、CLASS_INFというのがほしい部分です。

j_meta <- content(res_meta)

str(j_meta, max.level = 4, list.len = 3)
#> List of 1
#>  $ GET_META_INFO:List of 3
#>   ..$ RESULT      :List of 3
#>   .. ..$ STATUS   : int 0
#>   .. ..$ ERROR_MSG: chr "正常に終了しました。"
#>   .. ..$ DATE     : chr "2015-10-28T19:56:57.481+09:00"
#>   ..$ PARAMETER   :List of 3
#>   .. ..$ LANG         : chr "J"
#>   .. ..$ STATS_DATA_ID: chr "0003103532"
#>   .. ..$ DATA_FORMAT  : chr "J"
#>   ..$ METADATA_INF:List of 2
#>   .. ..$ TABLE_INF:List of 13
#>   .. .. ..$ @id                 : chr "0003103532"
#>   .. .. ..$ STAT_NAME           :List of 2
#>   .. .. ..$ GOV_ORG             :List of 2
#>   .. .. .. [list output truncated]
#>   .. ..$ CLASS_INF:List of 1
#>   .. .. ..$ CLASS_OBJ:List of 5
#>   .. .. .. .. [list output truncated]

CLASS_OBJの中のCLASSという項目が各項目のメタデータになっているので、それぞれをdata.frameにします。

ここでちょっと厄介なのは、ネストのレベルが違うものが混じっていることです。ひとつめのCLASSはリストですが、ふたつめ以降のCLASSはリストのリストです。 (とか書いてもたぶん伝わらないと思うので、「ふーん」くらいに思ってもらえれば...)

str(j_meta$GET_META_INFO$METADATA_INF$CLASS_INF$CLASS_OBJ[[1]])
#> List of 3
#>  $ @id  : chr "tab"
#>  $ @name: chr "表章項目"
#>  $ CLASS:List of 3
#>   ..$ @code : chr "01"
#>   ..$ @name : chr "金額"
#>   ..$ @level: chr ""

str(j_meta$GET_META_INFO$METADATA_INF$CLASS_INF$CLASS_OBJ[[2]], list.len = 3)
#> List of 3
#>  $ @id  : chr "cat01"
#>  $ @name: chr "品目分類(27年改定)"
#>  $ CLASS:List of 703
#>   ..$ :List of 4
#>   .. ..$ @code : chr "000100000"
#>   .. ..$ @name : chr "世帯数分布(抽出率調整)"
#>   .. ..$ @level: chr "1"
#>   .. .. [list output truncated]
#>   ..$ :List of 4
#>   .. ..$ @code : chr "000200000"
#>   .. ..$ @name : chr "集計世帯数"
#>   .. ..$ @level: chr "1"
#>   .. .. [list output truncated]
#>   ..$ :List of 4
#>   .. ..$ @code : chr "000300000"
#>   .. ..$ @name : chr "世帯人員"
#>   .. ..$ @level: chr "1"
#>   .. .. [list output truncated]
#>   .. [list output truncated]

ここは、リストのリストならbind_rows()を、リストならas_dafa_frame()を適用するような関数を作って何とかします。後で参照しやすいように@idをリストの要素名にしておきます。

@nameはそれぞれがどんなメタデータか日本語で書いてあるのでわかりやすいんですが、プログラム中で使いづらいので@idにしています)

force_bind_rows <- function(x) {
  if(is.list(x[[1]])) bind_rows(x) else as_data_frame(x)
}

class_obj <- j_meta$GET_META_INFO$METADATA_INF$CLASS_INF$CLASS_OBJ
class_df <- lapply(class_obj, function(x) force_bind_rows(x$CLASS))
names(class_df) <- sapply(class_obj, function(x) x$"@id")

class_df
#> $tab
#> Source: local data frame [1 x 3]
#> 
#>   @code @name @level
#>   (chr) (chr)  (chr)
#> 1    01  金額       
#> 
#> $cat01
#> Source: local data frame [703 x 5]
#> 
#>        @code                              @name @level    @unit @parentCode
#>        (chr)                              (chr)  (chr)    (chr)       (chr)
#> 1  000100000           世帯数分布(抽出率調整)      1 一万分比          NA
#> 2  000200000                         集計世帯数      1     世帯          NA
#> 3  000300000                           世帯人員      1       人          NA
#> ..(長いので以下省略)..

# @nameはこんな感じ。
sapply(class_obj, function(x) x$"@name")
#> [1] "表章項目"             "品目分類(27年改定)" "世帯区分"             "地域区分"             "時間軸(月次)" 

目的は品目分類なので、そこから「チョコレート」を探してみましょう。

filter(class_df$cat01, stringr::str_detect(`@name`, "チョコレート"))
#> Source: local data frame [2 x 5]
#> 
#>       @code                @name @level @unit @parentCode
#>       (chr)                (chr)  (chr) (chr)       (chr)
#> 1 010800130     352 チョコレート      5    円   010800000
#> 2 010800140 353 チョコレート菓子      5    円   010800000

2つがヒットしました。これを絞り込み条件に使って、実際のデータを取ってみます。

統計データ取得

分類事項01(cat01)のコードは、cdCat01というパラメータに指定します。複数指定するときは,で区切ります。

res_data <- GET(
  "http://api.e-stat.go.jp/", path = "rest/2.0/app/json/getStatsData",
  query = list(
    appId      = key,
    statsDataId = "0003103532",
    cdCat01     = "010800130,010800140"
  ))

返ってくるデータは以下のようになっています。これまたわかりにくいですが、DATA_INFの中のVALUEというのが求める結果です。

RESULT_INFには、トータルの件数と今回取得した件数が書かれています。NEXT_KEYが存在すれば、その値をstartPositionに指定すると続きのデータを取得できます。デフォルトだと一度に取得できるレコード数は10万件です。

ちなみに、CLASS_INFにはさっき取得したメタ情報と同じもの(たぶん)が入っています。metaGetFlg=Fというのを指定すれば省略されるらしいです。

j_data <- content(res_data)

str(j_data, max.level = 4, list.len = 4)
#> List of 1
#>  $ GET_STATS_DATA:List of 3
#>   ..$ RESULT          :List of 3
#>   .. ..$ STATUS   : int 0
#>   .. ..$ ERROR_MSG: chr "正常に終了しました。"
#>   .. ..$ DATE     : chr "2015-10-28T20:34:19.927+09:00"
#>   ..$ PARAMETER       :List of 6
#>   .. ..$ LANG          : chr "J"
#>   .. ..$ STATS_DATA_ID : chr "0003103532"
#>   .. ..$ NARROWING_COND:List of 1
#>   .. .. ..$ CODE_CAT01_SELECT: chr "010800130,010800140"
#>   .. ..$ DATA_FORMAT   : chr "J"
#>   .. .. [list output truncated]
#>   ..$ STATISTICAL_DATA:List of 4
#>   .. ..$ RESULT_INF:List of 3
#>   .. .. ..$ TOTAL_NUMBER: int 12064
#>   .. .. ..$ FROM_NUMBER : int 1
#>   .. .. ..$ TO_NUMBER   : int 12064
#>   .. ..$ TABLE_INF :List of 13
#>   .. .. ..$ @id                 : chr "0003103532"
#>   .. .. ..$ STAT_NAME           :List of 2
#>   .. .. ..$ GOV_ORG             :List of 2
#>   .. .. ..$ STATISTICS_NAME     : chr "家計調査 家計収支編 二人以上の世帯"
#>   .. .. .. [list output truncated]
#>   .. ..$ CLASS_INF :List of 1
#>   .. .. ..$ CLASS_OBJ:List of 5
#>   .. .. .. .. [list output truncated]
#>   .. ..$ DATA_INF  :List of 2
#>   .. .. ..$ NOTE :List of 3
#>   .. .. ..$ VALUE:List of 12064
#>   .. .. .. .. [list output truncated]

これはすんなりbind_rows()できます。ただし、データはまだちょっと加工が必要です。

value_df <- j_data$GET_STATS_DATA$STATISTICAL_DATA$DATA_INF$VALUE %>%
  bind_rows

glimpse(value_df)
#> Observations: 12,064
#> Variables: 7
#> $ @tab   (chr) "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01", "01"...
#> $ @cat01 (chr) "010800130", "010800130", "010800130", "010800130", "010800130", "010800130", "010800130", "010800130", "010800130", "...
#> $ @cat02 (chr) "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03", "03"...
#> $ @area  (chr) "00000", "00000", "00000", "00000", "00000", "00000", "00000", "00000", "00000", "00000", "00000", "00000", "00000", "...
#> $ @time  (chr) "2015000808", "2015000707", "2015000606", "2015000505", "2015000404", "2015000303", "2015000202", "2015000101", "20140...
#> $ @unit  (chr) "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円", "円"...
#> $ $      (chr) "212", "213", "268", "286", "334", "530", "1324", "559", "548", "425", "417", "274", "201", "208", "227", "305", "341"...

データ加工

まず、なにはともあれ値を数値にしておきましょう。値が入っていないところは"-"という文字になっているので、NAになります。ワーニングが出ますがまあ意図通りなので無視しましょう。

value_df <- value_df %>%
  mutate(value = as.numeric(`$`))
#> Warning message:
#> In eval(substitute(expr), envir, enclos) : NAs introduced by coercion

次に、各カテゴリのデータをメタ情報とくっつけます。とりあえず必要そうなやつだけ。

df_time <- class_df$time %>%
  select(`@time` = `@code`, date = `@name`)

df_cat01 <- class_df$cat01 %>%
  select(`@cat01` = `@code`, item = `@name`)

df_area <- class_df$area %>%
  select(`@area` = `@code`, city = `@name`)

value_df %>%
  left_join(df_time,  by = "@time") %>%
  left_join(df_cat01, by = "@cat01") %>%
  left_join(df_area,  by = "@area")
#> Source: local data frame [12,064 x 11]
#> 
#>     @tab    @cat01 @cat02 @area      @time @unit     $ value       date             item  city
#>    (chr)     (chr)  (chr) (chr)      (chr) (chr) (chr) (dbl)      (chr)            (chr) (chr)
#> 1     01 010800130     03 00000 2015000808    円   212   212  2015年8月 352 チョコレート  全国
#> 2     01 010800130     03 00000 2015000707    円   213   213  2015年7月 352 チョコレート  全国
#> 3     01 010800130     03 00000 2015000606    円   268   268  2015年6月 352 チョコレート  全国
#> 4     01 010800130     03 00000 2015000505    円   286   286  2015年5月 352 チョコレート  全国
#> 5     01 010800130     03 00000 2015000404    円   334   334  2015年4月 352 チョコレート  全国
#> 6     01 010800130     03 00000 2015000303    円   530   530  2015年3月 352 チョコレート  全国
#> 7     01 010800130     03 00000 2015000202    円  1324  1324  2015年2月 352 チョコレート  全国
#> 8     01 010800130     03 00000 2015000101    円   559   559  2015年1月 352 チョコレート  全国
#> 9     01 010800130     03 00000 2014001212    円   548   548 2014年12月 352 チョコレート  全国
#> 10    01 010800130     03 00000 2014001111    円   425   425 2014年11月 352 チョコレート  全国
#> ..   ...       ...    ...   ...        ...   ...   ...   ...        ...              ...   ...

あとは、ちょっと邪魔そうな文字とか削ったりとかですが、そのへんはまた次回。

...次回?

勘のいい方はもうお気づきかと思いますが、これは↓の記事を「それchoroplethrでできますよ」と言うための前準備です。

wafdata.hatenablog.com

というのは、こんなつぶやきを見かけて、そ、そんなAriさんの手を煩わせるなんて申し訳ない!という露払い的な。

しかし長い前準備だった。。パッケージ化したい。